実行環境
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()
これで実行し
音声を
まともに動きますか
と入力すると
入力された音声テキスト一覧:
「まともに動きますか」は、少々不自然な表現です。
より自然で適切な表現は、「正常に動きますか」「問題なく動きますか」などになります。
「まとも」は通常、「正当・真正」という意味合いで用いられ、
物事の本来あるべき姿や道理に反しないことを示します。
例えば、「彼はまともな理由で解雇されたわけではなかった」のように使います。
一方、「動く」は「正常に機能する・問題なく作動する」という意味合いで用いられます。
ですから、文中で「まとも」を用いる必要性が低く、
より適切な表現を選ぶと自然な日本語になります。
というようになる
意図したのは
「正常に動きますか」「問題なく動きますか
というように変換してほしい
なのでプロンプトを変更する