Ollamaで音声入力テキストの修正をする

実行環境
M1 MacbookAir 16GB

Ollamaで音声入力テキストの修正をする

Ollamaを使って日本語として自然な文章に構成する部分を追加

 touch ollama_text_correction.py

でファイルを作成

# ollama_text_correction.py
import requests
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")  # 使用するモデルを指定
    
    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config
    
    def correct_text(self, text):
        url = "http://localhost:11434/api/generate"  # OllamaのAPIエンドポイント
        headers = {
            "Content-Type": "application/json"
        }
        payload = {
            "model": self.model,
            "prompt": f"以下の文を正しい日本語に構成してください:\n{text}"
        }
        response = requests.post(url, headers=headers, json=payload)
        if response.status_code == 200:
            corrected_text = response.json()["text"]
            return corrected_text.strip()
        else:
            raise Exception(f"Error from Ollama API: {response.status_code}, {response.text}")

config.json にOllamaのモデル設定を追加
{
  "token": "LINE notify の token",
  "ollama_model": "elyza:jp8b"
}

音声入力後にOllamaを使ってテキストを修正するように、メインスクリプトを更新

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time
from line_notify import LineNotify  # 作成したLineNotifyモジュールをインポート

def main():
    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()

    # 入力された音声テキストを格納するリスト
    recognized_texts = []

    # LINE Notifyのモジュールを初期化(config.jsonからトークンを読み込む)
    line_notify = LineNotify("config.json")

    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()

        # 処理が2秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 5:
            print("2秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("無音状態が続いたため、ループを終了します。")
            break  # 無音でループを抜ける
        
        # 音声をテキストに変換
        text = fasterWhispermodel.audio2text(audio_data)
        recognized_texts.append(text)  # テキストをリストに追加
        print(text)

    # ループ終了後に、入力した音声テキストを改行付きで一覧表示
    if recognized_texts:
        message = "\n".join(recognized_texts)
        print("\n入力された音声テキスト一覧:")
        print(message)

        # LINE Notifyでメッセージを送信
        line_notify.send(f"入力された音声テキスト一覧:\n{message}")
    else:
        print("入力メッセージはありませんでした")

if __name__ == "__main__":
    main()

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time
from line_notify import LineNotify  # 作成したLineNotifyモジュールをインポート
from ollama_text_correction import OllamaTextCorrector  # Ollamaによる修正モジュールをインポート

def main():
    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()

    # 入力された音声テキストを格納するリスト
    recognized_texts = []

    # LINE Notifyのモジュールを初期化(config.jsonからトークンを読み込む)
    line_notify = LineNotify("config.json")
    
    # Ollamaのテキスト修正モジュールを初期化
    text_corrector = OllamaTextCorrector("config.json")

    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()

        # 処理が2秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 5:
            print("2秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("無音状態が続いたため、ループを終了します。")
            break  # 無音でループを抜ける
        
        # 音声をテキストに変換
        text = fasterWhispermodel.audio2text(audio_data)
        
        # Ollamaでテキストを構成
        corrected_text = text_corrector.correct_text(text)
        
        recognized_texts.append(corrected_text)  # 構成されたテキストをリストに追加
        print(corrected_text)

    # ループ終了後に、入力した音声テキストを改行付きで一覧表示
    if recognized_texts:
        message = "\n".join(recognized_texts)
        print("\n入力された音声テキスト一覧:")
        print(message)

        # LINE Notifyでメッセージを送信
        line_notify.send(f"入力された音声テキスト一覧:\n{message}")
    else:
        print("入力メッセージはありませんでした")

if __name__ == "__main__":
    main()

に変更

しかし

[2024-09-14 00:48:22.490] [ctranslate2] [thread 305229] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
stand by ready OK
recording...
finished
Traceback (most recent call last):
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/requests/models.py", line 963, in json
    return complexjson.loads(self.content.decode(encoding), **kwargs)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/json/decoder.py", line 340, in decode
    raise JSONDecodeError("Extra data", s, end)
json.decoder.JSONDecodeError: Extra data: line 2 column 1 (char 94)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/main.py", line 55, in <module>
    main()
  File "/Users/snowpool/aw10s/linebot/main.py", line 38, in main
    corrected_text = text_corrector.correct_text(text)
  File "/Users/snowpool/aw10s/linebot/ollama_text_correction.py", line 26, in correct_text
    corrected_text = response.json()["text"]
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/requests/models.py", line 971, in json
    raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
requests.exceptions.JSONDecodeError: Extra data: line 2 column 1 (char 94)

となる

ollama_text_correction.py の
correct_text 関数にデバッグ用の出力を追加し、レスポンスの内容をテキストで表示

# ollama_text_correction.py
import requests
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")  # 使用するモデルを指定
    
    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config
    
    def correct_text(self, text):
        url = "http://localhost:11434/api/generate"  # OllamaのAPIエンドポイント
        headers = {
            "Content-Type": "application/json"
        }
        payload = {
            "model": self.model,
            "prompt": f"以下の文を正しい日本語に構成してください:\n{text}"
        }
        response = requests.post(url, headers=headers, json=payload)
        
        # レスポンスをテキストで表示して確認
        print(f"API Response: {response.text}")

        # レスポンスがJSONとして正しいか確認
        try:
            corrected_text = response.json()["text"]
            return corrected_text.strip()
        except json.JSONDecodeError as e:
            print(f"JSONDecodeError: {e}")
            return None

実行結果は

[2024-09-14 00:50:32.751] [ctranslate2] [thread 306834] [warning] The compute type inferred from the saved model is float16, but the target device or backend do not support efficient float16 computation. The model weights have been automatically converted to use the float32 compute type instead.
stand by ready OK
recording...
finished
API Response: {"model":"elyza:jp8b","created_at":"2024-09-13T15:50:43.828714Z","response":"独","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:43.915193Z","response":"り","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:43.999869Z","response":"言","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.084866Z","response":"のような","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.170081Z","response":"短","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.254747Z","response":"い","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.341826Z","response":"文章","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.428313Z","response":"ですが","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.513551Z","response":"、","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.599198Z","response":"問題","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.6867Z","response":"ない","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.775178Z","response":"です","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.864271Z","response":"。","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:44.951287Z","response":"正","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.037784Z","response":"しい","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.123048Z","response":"日本","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.21019Z","response":"語","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.297796Z","response":"に","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.384251Z","response":"構","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.471506Z","response":"成","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.56044Z","response":"した","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.64597Z","response":"文","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.732028Z","response":"は","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.821103Z","response":"次","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.908953Z","response":"の","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:45.997249Z","response":"通り","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.088031Z","response":"です","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.183558Z","response":"。\n\n","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.277991Z","response":"「","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.370159Z","response":"天","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.46265Z","response":"気","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.563932Z","response":"」","done":false}
{"model":"elyza:jp8b","created_at":"2024-09-13T15:50:46.664274Z","response":"","done":true,"done_reason":"stop","context":[128006,882,128007,271,88852,16144,17161,30512,37656,102800,102433,102158,20230,106391,13153,39926,72315,512,36827,95221,128009,128006,78191,128007,271,106063,31431,78244,120950,106649,16995,83125,119627,5486,109606,100604,38641,1811,37656,102800,102433,102158,20230,106391,13153,56051,17161,15682,33671,16144,121640,38641,3490,13177,36827,95221,10646],"total_duration":6901465958,"load_duration":40178541,"prompt_eval_count":26,"prompt_eval_duration":4023220000,"eval_count":33,"eval_duration":2836184000}

JSONDecodeError: Extra data: line 2 column 1 (char 94)
None
stand by ready OK
recording...
finished
10秒間音声が入力されなかったため、ループを終了します。
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/main.py", line 55, in <module>
    main()
  File "/Users/snowpool/aw10s/linebot/main.py", line 45, in main
    message = "\n".join(recognized_texts)
TypeError: sequence item 0: expected str instance, NoneType found

となる

このエラーは、2つの問題が原因です。
1. Ollama APIのレスポンスが複数行にわたっていることが原因:
response.json()が一つのJSONオブジェクトを期待しているのに対して、
複数行のJSONレスポンスが返されています。

これは、Ollamaが複数の部分に分けてレスポンスを返しているためです。
2. NoneTypeがrecognized_textsに含まれていることが原因:
correct_text関数でNoneが返され、
recognized_textsリストに追加されているため、TypeErrorが発生しています。

1. Ollamaのレスポンスを段階的に収集する:
複数のJSONオブジェクトが連続して返されている場合、手動でそれを収集し、
一つの完全なテキストに結合する処理を追加します。

2. Noneの扱いを改善する: Noneがリストに追加されないように修正します。

なので

ollama_text_correction.py

を修正

Ollamaのレスポンスを部分的に受け取り、テキストを結合するようにする

# ollama_text_correction.py
import requests
import json

class OllamaTextCorrector:
    def __init__(self, config_file_path):
        self.config = self._load_config(config_file_path)
        self.model = self.config.get("ollama_model", "elyza:jp8b")  # 使用するモデルを指定
    
    def _load_config(self, config_file_path):
        with open(config_file_path, 'r') as file:
            config = json.load(file)
        return config
    
    def correct_text(self, text):
        url = "http://localhost:11434/api/generate"  # OllamaのAPIエンドポイント
        headers = {
            "Content-Type": "application/json"
        }
        payload = {
            "model": self.model,
            "prompt": f"以下の文を正しい日本語に構成してください:\n{text}"
        }
        response = requests.post(url, headers=headers, json=payload)

        # レスポンスをテキストで確認して、複数の部分を結合
        full_response = ""
        for line in response.text.splitlines():
            try:
                json_line = json.loads(line)  # 各行をJSONとして処理
                if "response" in json_line:
                    full_response += json_line["response"]  # テキスト部分を結合
            except json.JSONDecodeError as e:
                print(f"JSONDecodeError: {e}")

        return full_response.strip() if full_response else None

そしてmain.py
Noneがリストに追加されないように修正

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time
from line_notify import LineNotify  # 作成したLineNotifyモジュールをインポート
from ollama_text_correction import OllamaTextCorrector  # Ollamaによる修正モジュールをインポート

def main():
    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()

    # 入力された音声テキストを格納するリスト
    recognized_texts = []

    # LINE Notifyのモジュールを初期化(config.jsonからトークンを読み込む)
    line_notify = LineNotify("config.json")
    
    # Ollamaのテキスト修正モジュールを初期化
    text_corrector = OllamaTextCorrector("config.json")

    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()

        # 処理が10秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 10:
            print("10秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("無音状態が続いたため、ループを終了します。")
            break  # 無音でループを抜ける
        
        # 音声をテキストに変換
        text = fasterWhispermodel.audio2text(audio_data)
        
        # Ollamaでテキストを構成
        corrected_text = text_corrector.correct_text(text)
        
        if corrected_text:  # Noneが返された場合はスキップ
            recognized_texts.append(corrected_text)
            print(corrected_text)

    # ループ終了後に、入力した音声テキストを改行付きで一覧表示
    if recognized_texts:
        message = "\n".join(recognized_texts)
        print("\n入力された音声テキスト一覧:")
        print(message)

        # LINE Notifyでメッセージを送信
        line_notify.send(f"入力された音声テキスト一覧:\n{message}")
    else:
        print("入力メッセージはありませんでした")

if __name__ == "__main__":
    main()

これで実行し
音声を
まともに動きますか
と入力すると

入力された音声テキスト一覧:
「まともに動きますか」は、少々不自然な表現です。
より自然で適切な表現は、「正常に動きますか」「問題なく動きますか」などになります。

「まとも」は通常、「正当・真正」という意味合いで用いられ、
物事の本来あるべき姿や道理に反しないことを示します。
例えば、「彼はまともな理由で解雇されたわけではなかった」のように使います。

一方、「動く」は「正常に機能する・問題なく作動する」という意味合いで用いられます。
ですから、文中で「まとも」を用いる必要性が低く、
より適切な表現を選ぶと自然な日本語になります。

というようになる

意図したのは
「正常に動きますか」「問題なく動きますか
というように変換してほしい

なのでプロンプトを変更する

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です