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()

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

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

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

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

というようになる

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

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

Whisper の結果をLINEで送る

Whisper の結果をLINEで送る

実行環境
M1 MacbookAir 16GB

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time

def main():

    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()
    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()
                # 処理が10秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 5:
            print("10秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("5秒以上の無音状態が続いたため、ループを終了します。")
            break  # 無音5秒以上でループを抜ける
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)

if __name__ == "__main__":
    main()

としたら停止するが実際には10秒以上かかったので
これを五秒程度にしたらうまく動く

次は line notify でこの文章を送信してみる

しかし、その前にこのtext部分をリストなどで格納してから送信する必要がある

まずここまでのソースを移動しておく

cp -rp module ~/aw10s/linebot
cp -rp configs/ ~/aw10s/linebot 
cp main.py ~/aw10s/linebot
で
cd ~/aw10s/linebot
python main.py

を実行したらエラー

Config.iniがないと出る
これは

cp -rp configs ~/aw10s/linebot

とすることで解決

これでここまで行った音声の自動終了はできたので
入力をリストなどにまとめるようにする

これは以前音声ファイルを出力した時の応用でできるはず

ループを抜けた時に入力した音声を改行付きで一覧表示したい

入力した音声をループを抜けたときに改行付きで一覧表示するには、音声認識されたテキストをリストに格納し、ループを抜けた後にそのリスト内のテキストを改行付きで表示する

recognized_textsリストの作成
* recognized_textsというリストを作成し、各ループで音声認識されたテキストをそのリストに追加
recognized_texts.append(text)
* 各ループで変換されたテキストをrecognized_textsリストに追加
ループ終了後の一覧表示
* ループを抜けた後に、print(“\n”.join(recognized_texts))を使って、リスト内のテキストを改行付きで表示

をmain.pyに追加する

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time

def main():

    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()
    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()
                # 処理が10秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 5:
            print("10秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("5秒以上の無音状態が続いたため、ループを終了します。")
            break  # 無音5秒以上でループを抜ける
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)

if __name__ == "__main__":
    main()
を

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time

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

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

    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)

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

if __name__ == "__main__":
    main()

に変更し実行

[2024-09-06 06:03:52.900] [ctranslate2] [thread 17609052] [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
今日のハンバーグは何にしようか
stand by ready OK
recording...
finished
和風ソースか
stand by ready OK
recording...
finished
オニオンソース
stand by ready OK
recording...
finished
デミグラスソース
stand by ready OK
recording...
finished
出しようかな
stand by ready OK
recording...
finished
今日の買い物リスト
stand by ready OK
recording...
finished
2秒間音声が入力されなかったため、ループを終了します。

入力された音声テキスト一覧:
今日のハンバーグは何にしようか
和風ソースか
オニオンソース
デミグラスソース
出しようかな
今日の買い物リスト

というように一覧表示ができた

次はこれを line notify で送信してみる

これは以前作成した在庫管理送信の機能から使う
チラシの方だとOCR処理が入っているのでより複雑だった

今回の場合、画像は不要で
Whisperで文字起こしした音声を送信するのが目的

 cp ../store_adversting_list/config.json .


LINEのAPIキーが格納されているファイルをコピー

今回はconfig.jsonの中はtoken 以外は削除しておく

{
  "token": "取得したアクセストークン"
}

次に

touch ilne_notify.py

でline notify の処理のモジュールを作成

# line_notify.py
import json
import requests

class LineNotify:
    def __init__(self, token_file_path):
        self.token = self._load_token(token_file_path)

    # JSONファイルからアクセストークンを読み込む
    def _load_token(self, token_file_path):
        with open(token_file_path, 'r') as file:
            data = json.load(file)
        return data["token"]

    # LINE Notifyでメッセージを送信
    def send(self, message):
        url = "https://notify-api.line.me/api/notify"
        headers = {
            "Authorization": f"Bearer {self.token}"
        }
        data = {
            "message": message
        }
        response = requests.post(url, headers=headers, data=data)
        if response.status_code != 200:
            raise Exception(f"Error sending message: {response.status_code}, {response.text}")
        return response

として保存

Main.pyを

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time

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

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

    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)

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

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モジュールをインポート

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

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

    # LINE Notifyのモジュールを初期化(アクセストークンを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)

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

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

if __name__ == "__main__":
    main()

へ変更

これで実行すると音声で入力したものがline notify で送信される

しかし、これだと音声入力がなくても送信されるため、
もし入力リストが空ならログ表示だけにする

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()

とすることで入力がない場合には
Line notifyで送信しなくなる

停止処理を5秒たったら録音を終了させる

停止処理を5秒たったら終了させる

import numpy as np
import sounddevice as sd
import os
import configparser
import errno

class Recorderconfig:
    def __init__(self, config_ini_path='./configs/config.ini'):
        # iniファイルの読み込み
        self.config_ini = configparser.ConfigParser()
        
        # 指定したiniファイルが存在しない場合、エラー発生
        if not os.path.exists(config_ini_path):
            raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), config_ini_path)
        
        self.config_ini.read(config_ini_path, encoding='utf-8')
        Recorder_items = self.config_ini.items('Recorder')
        self.Recorder_config_dict = dict(Recorder_items)

class Recorder:
    def __init__(self, config_ini_path='./configs/config.ini'):
        Recorder_config = Recorderconfig(config_ini_path=config_ini_path)
        config_dict = Recorder_config.Recorder_config_dict
        
        self.fs = int(config_dict["fs"])
        self.silence_threshold = float(config_dict["silence_threshold"])
        self.min_duration = float(config_dict["min_duration"])
        self.amplitude_threshold = float(config_dict["amplitude_threshold"])
        self.start_threshold = float(config_dict["start_threshold"])

    def speech2audio(self):
        record_Flag = False
        non_recorded_data = []
        recorded_audio = []
        silent_time = 0
        input_time = 0
        start_threshold = 0.3
        all_time = 0
        
        with sd.InputStream(samplerate=self.fs, channels=1) as stream:
            while True:
                data, overflowed = stream.read(int(self.fs * self.min_duration))
                all_time += 1
                if all_time == 10:
                    print("stand by ready OK")
                elif all_time >= 10:
                    if np.max(np.abs(data)) > self.amplitude_threshold and not record_Flag:
                        input_time += self.min_duration
                        if input_time >= start_threshold:
                            record_Flag = True
                            print("recording...")
                            recorded_audio = non_recorded_data[int(-1*start_threshold*10)-2:]

                    else:
                        input_time = 0

                    if overflowed:
                        print("Overflow occurred. Some samples might have been lost.")
                    if record_Flag:
                        recorded_audio.append(data)

                    else:
                        non_recorded_data.append(data)

                    # 無音が10秒以上続いたらループを抜ける
                    if np.all(np.abs(data) < self.amplitude_threshold):
                        silent_time += self.min_duration
                        if silent_time >= 10:  # 無音が10秒続いたらNoneを返す
                            print("finished")
                            record_Flag = False
                            return None
                    else:
                        silent_time = 0

        # 録音データが存在しない場合はNoneを返す
        if len(recorded_audio) == 0:
            return None

        audio_data = np.concatenate(recorded_audio, axis=0)
        return audio_data

として

from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder

def main():
    recorder = Recorder()  # Recorderのインスタンス作成
    fasterWhispermodel = FasterWhisperModel()  # FasterWhisperModelのインスタンス作成

    while True:
        audio_data = recorder.speech2audio()  # 音声の取得
        if audio_data is None:
            print("5秒以上の無音状態が続いたため、ループを終了します。")
            break  # 無音5秒以上でループを抜ける

        text = fasterWhispermodel.audio2text(audio_data)  # 音声をテキストに変換
        print(text)  # テキストを表示

if __name__ == "__main__":
    main()

としたが

[2024-09-05 06:19:29.705] [ctranslate2] [thread 17518403] [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
5秒以上の無音状態が続いたため、ループを終了します。

となるが今度は入力ができなくなった

単純に

import sounddevice as sd
from module.module_whisper import FasterWhisperModel
from module.module_recorder import Recorder
import time

def main():

    recorder = Recorder()
    fasterWhispermodel = FasterWhisperModel()
    while True:
        start_time = time.time()  # 処理開始時刻を記録
        audio_data = recorder.speech2audio()
                # 処理が10秒間行われなかった場合はループを抜ける
        if time.time() - start_time >= 5:
            print("10秒間音声が入力されなかったため、ループを終了します。")
            break

        if audio_data is None:
            print("5秒以上の無音状態が続いたため、ループを終了します。")
            break  # 無音5秒以上でループを抜ける
        text = fasterWhispermodel.audio2text(audio_data)
        print(text)

if __name__ == "__main__":
    main()

としたら停止するが実際には10秒以上かかっている

これを五秒程度にしたらうまく動く

次は line notify でこの文章を送信してみる

Faster WhisperとPyAudioを使用して、マイクから音声をリアルタイムで取得し、音声をテキストに変換する

Faster WhisperとPyAudioを使用して、マイクから音声をリアルタイムで取得し、音声をテキストに変換する

pip install faster-whisper pyaudio

でライブラリインストール

録音ファイルは
mic_rec.py
とする

import pyaudio
import numpy as np
import faster_whisper

# Faster Whisperのモデルをロードします(モデルパスは適宜変更してください)
model = faster_whisper.WhisperModel("large-v2", device="cpu")  # または "cuda" でGPUを使用

# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024

# PyAudioのインスタンスを作成
audio = pyaudio.PyAudio()

# マイクから音声を取得するストリームを開きます
stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

print("Listening...")

# 音声データをバッファとして取得してリアルタイムに処理します
try:
    while True:
        # 音声データを取得
        data = stream.read(CHUNK)
        # NumPy配列に変換
        audio_data = np.frombuffer(data, dtype=np.int16)
        # Faster Whisperに音声データを渡してテキストを取得
        segments, _ = model.transcribe(audio_data)
        # 取得したセグメントを出力
        for segment in segments:
            print(f"Text: {segment.text}")
except KeyboardInterrupt:
    # 終了処理
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()


生成されたコードだが
モデルは
large-v3
を使い

CPUを使う

なお以前
/aw10s/whisper/test.py
で試した時には

model = WhisperModel("large-v3", device="cpu", compute_type="int8")

としたので
この設定を使う

import pyaudio
import numpy as np
from faster_whisper import WhisperModel


# Faster Whisperのモデルをロードします(モデルパスは適宜変更してください)
# model = faster_whisper.WhisperModel("large-v2", device="cpu")  # または "cuda" でGPUを使用
model = WhisperModel("large-v3", device="cpu", compute_type="int8")
# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 1024

# PyAudioのインスタンスを作成
audio = pyaudio.PyAudio()

# マイクから音声を取得するストリームを開きます
stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

print("Listening...")

# 音声データをバッファとして取得してリアルタイムに処理します
try:
    while True:
        # 音声データを取得
        data = stream.read(CHUNK)
        # NumPy配列に変換
        audio_data = np.frombuffer(data, dtype=np.int16)
        # Faster Whisperに音声データを渡してテキストを取得
        segments, _ = model.transcribe(audio_data)
        # 取得したセグメントを出力
        for segment in segments:
            print(f"Text: {segment.text}")
except KeyboardInterrupt:
    # 終了処理
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()

で実行

しかし

Listening...
Text:  Takk for att du så på!
Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/mic_rec.py", line 27, in <module>
    data = stream.read(CHUNK)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/pyaudio/__init__.py", line 570, in read
    return pa.read_stream(self._stream, num_frames,
OSError: [Errno -9981] Input overflowed

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/linebot/mic_rec.py", line 39, in <module>
    stream.stop_stream()
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/pyaudio/__init__.py", line 500, in stop_stream
    pa.stop_stream(self._stream)
OSError: Stream not open

となる

OSError: [Errno -9981] Input overflowed は、
PyAudio がバッファから音声データを適切に読み取れなかったことを示しています。
この問題は、バッファサイズが小さいか、処理が遅いために発生することがあります。
とのこと

バッファサイズの調整:
CHUNK のサイズを大きくすることで、入力オーバーフローを防ぐことができます。
例えば、1024 を 2048 や 4096 に変更してみてください
ということで

CHUNK = 2048  # もしくは 4096

で試す

また
エラーハンドリングの追加: オーバーフローエラーが発生した場合に備え、
例外処理を追加してストリームを正しく閉じるようにします。

try:
    while True:
        data = stream.read(CHUNK, exception_on_overflow=False)
        audio_data = np.frombuffer(data, dtype=np.int16)
        segments, _ = model.transcribe(audio_data)
        for segment in segments:
            print(f"Text: {segment.text}")
except OSError as e:
    print(f"Error: {e}")
except KeyboardInterrupt:
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()

これらを追加して

import pyaudio
import numpy as np
from faster_whisper import WhisperModel

# Faster Whisperのモデルをロードします
model = WhisperModel("large-v3", device="cpu", compute_type="int8")

# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 2048  # バッファサイズを大きく変更

# PyAudioのインスタンスを作成
audio = pyaudio.PyAudio()

# マイクから音声を取得するストリームを開きます
stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

print("Listening...")

# 音声データをバッファとして取得してリアルタイムに処理します
try:
    while True:
        # 音声データを取得、オーバーフロー時に例外を発生させない
        data = stream.read(CHUNK, exception_on_overflow=False)
        # NumPy配列に変換
        audio_data = np.frombuffer(data, dtype=np.int16)
        # Faster Whisperに音声データを渡してテキストを取得
        segments, _ = model.transcribe(audio_data)
        # 取得したセグメントを出力
        for segment in segments:
            print(f"Text: {segment.text}")
except OSError as e:
    print(f"Error: {e}")
except KeyboardInterrupt:
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()

としたが
実行すると

Listening... Text: Takk for att du så med. Text: Teksting av Nicolai Winther Text: Teksting av Nicolai Winther Text: Продолжение следует... Text: Teksting av Nicolai Winther ^CTerminating... 

となる

Faster Whisperモデルが認識する言語が様々であるため、
期待される言語での認識がうまくいっていない可能性があります。
これを改善するために、
特定の言語を指定してモデルがその言語に焦点を当てて認識するように設定することが有効です。

Faster Whisperモデルで特定の言語を指定するためには、
transcribe メソッドに language パラメータを追加して使用します。例えば、
日本語を指定したい場合は、language=”ja” とします。

ということで

import pyaudio
import numpy as np
from faster_whisper import WhisperModel

# Faster Whisperのモデルをロードします
model = WhisperModel("large-v3", device="cpu", compute_type="int8")

# 音声設定
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 16000
CHUNK = 2048  # バッファサイズを大きく変更

# PyAudioのインスタンスを作成
audio = pyaudio.PyAudio()

# マイクから音声を取得するストリームを開きます
stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

print("Listening...")

# 音声データをバッファとして取得してリアルタイムに処理します
try:
    while True:
        # 音声データを取得、オーバーフロー時に例外を発生させない
        data = stream.read(CHUNK, exception_on_overflow=False)
        # NumPy配列に変換
        audio_data = np.frombuffer(data, dtype=np.int16)
        # Faster Whisperに音声データを渡してテキストを取得
        segments, _ = model.transcribe(audio_data, language="ja")  # 言語を日本語に指定
        # 取得したセグメントを出力
        for segment in segments:
            print(f"Text: {segment.text}")
except OSError as e:
    print(f"Error: {e}")
except KeyboardInterrupt:
    print("Terminating...")
finally:
    stream.stop_stream()
    stream.close()
    audio.terminate()

実行すると

Text: ご視聴ありがとうございました
Text: ご視聴ありがとうございました
Text: ご視聴ありがとうございました

となる

多分設定が足りていない

kotoba-whisper-v1.0

large3 より速いし日本語特化らしい
https://zenn.dev/asap/articles/ba8fcb1880165e

これと
https://zenn.dev/asap/articles/2c0d421e68ef16

生成AIをローカルで簡単に 【Part5.5 faster-whisper+マイク録音編】
と組み合わせることにする

M1MacbookAir にfaster-whisper を入れる

M1MacbookAir にfaster-whisper を入れる

https://microai.jp/blog/6cf7f278-e06a-42e1-953b-67eedd9c1ff8

m1mac16Gでの実行方法が掲載されてる

Apple Silicon用に最適化されたML用のライブラリMLXで最速Whisperを試す
も古いけど参考になると思う

M1 Macで音声認識AI Whisper
にもある

やはりGPUは必要らしい

実行時間
M1 Mac (MEM: 16G) で実行した結果(cudaなし=int8)
モデルの読み込み: 4.79秒
文字起こし: 93.72秒
GPUを使用していないとやはり遅いらしい

MLX の Whisperを検討したが
https://qiita.com/syukan3/items/5cdf2735d81d438929a9
によれば16GBのメモリでは厳しいらしい

ということで環境も同じような

文字起こしのライブラリ faster-whisper が超簡単で超早くて超正確!
を参考に行う

mkdir whisper
cd whisper

で作業ディレクトリ移動

pip install faster-whisper

音声ファイル操作のサードパーティ製ライブラリ

pip install pydub
pip install pandas

openpyxl(xcelファイル(.xlsx形式)を読み書きするため

pip install openpyxl

も追加でインストールしておく

brew install ffmpeg

でffmpeg も入れておく

しかし

arch -arm64 brew install ffmpeg

としないと

ffmpeg 7.0.1 is already installed but outdated (so it will be upgraded). Error: Cannot install under Rosetta 2 in ARM default prefix (/opt/homebrew)! To rerun under ARM use: arch -arm64 brew install ... To install under x86_64, install Homebrew into /usr/local. 

となる

これは
ARMアーキテクチャ用のHomebrewを使う必要があるため

実行後

Removing: /Users/snowpool/Library/Caches/Homebrew/python-packaging_bottle_manifest--24.0... (1.8KB)
Pruned 0 symbolic links and 2 directories from /opt/homebrew
==> Caveats
==> rust
zsh completions have been installed to:
  /opt/homebrew/share/zsh/site-functions

となるが

このメッセージは、Homebrewがキャッシュをクリアし、
シンボリックリンクやディレクトリの整理を行ったことを示しています。
また、Rustのzsh用の補完機能が
/opt/homebrew/share/zsh/site-functions にインストールされたことを知らせています。

特にエラーは表示されていないため、
ffmpeg のインストールやアップグレードが正常に進行したと考えられます。
次のステップとして、ffmpeg が正しくインストールされているかを確認するために、
以下のコマンドを実行してバージョン情報を確認してみてください

ということで

ffmpeg -version

を実行

ffmpeg version 7.0.2 Copyright (c) 2000-2024 the FFmpeg developers
built with Apple clang version 15.0.0 (clang-1500.3.9.4)
configuration: --prefix=/opt/homebrew/Cellar/ffmpeg/7.0.2 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags='-Wl,-ld_classic' --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libharfbuzz --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox --enable-audiotoolbox --enable-neon
libavutil      59.  8.100 / 59.  8.100
libavcodec     61.  3.100 / 61.  3.100
libavformat    61.  1.100 / 61.  1.100
libavdevice    61.  1.100 / 61.  1.100
libavfilter    10.  1.100 / 10.  1.100
libswscale      8.  1.100 /  8.  1.100
libswresample   5.  1.100 /  5.  1.100
libpostproc    58.  1.100 / 58.  1.100

となるので問題ない

pip install torch

これもインストール

from faster_whisper import WhisperModel

# large-v3モデルのロード
model = WhisperModel("large-v3", device="cuda", compute_type="float16")

# 音声ファイルをテキストに変換
segments, info = model.transcribe("path_to_your_audio_file.wav")

# 結果を表示
for segment in segments:
    print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")

とすることが多いけど
M1mac なので cuda は使えない

なお
faster-whisper ライブラリを使用する際、
指定したモデル(例えば large-v3)は、初回使用時に自動的にダウンロードされます。
モデルはローカルに保存されるため、次回以降は再ダウンロードする必要はありません
とのこと

touch test.py

でファイル作成

from faster_whisper import WhisperModel

# large-v3モデルのロード(CPUを使用)
model = WhisperModel("large-v3", device="cpu", compute_type="int8")

# 音声ファイルをテキストに変換
segments, info = model.transcribe("path_to_your_audio_file.wav")

# 結果を表示
for segment in segments:
    print(f"[{segment.start:.2f}s -> {segment.end:.2f}s] {segment.text}")

これを実行

python test.py 

モデルダウンロード時間は10分くらい

Downloading config.json: 100%|█████████████| 2.39k/2.39k [00:00<00:00, 8.90MB/s]
Downloading (…)rocessor_config.json: 100%|█████| 340/340 [00:00<00:00, 1.15MB/s]
Downloading tokenizer.json: 100%|██████████| 2.48M/2.48M [00:01<00:00, 2.08MB/s]
Downloading vocabulary.json: 100%|██████████| 1.07M/1.07M [00:01<00:00, 726kB/s]
Downloading model.bin: 100%|███████████████| 3.09G/3.09G [05:35<00:00, 9.21MB/s]
Traceback (most recent call last):█████████| 3.09G/3.09G [05:35<00:00, 9.24MB/s]
  File "/Users/snowpool/aw10s/whisper/test.py", line 7, in <module>
    segments, info = model.transcribe("path_to_your_audio_file.wav")
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/faster_whisper/transcribe.py", line 319, in transcribe
    audio = decode_audio(audio, sampling_rate=sampling_rate)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/faster_whisper/audio.py", line 46, in decode_audio
    with av.open(input_file, mode="r", metadata_errors="ignore") as container:
  File "av/container/core.pyx", line 420, in av.container.core.open
  File "av/container/core.pyx", line 266, in av.container.core.Container.__cinit__
  File "av/container/core.pyx", line 286, in av.container.core.Container.err_check
  File "av/error.pyx", line 326, in av.error.err_check
av.error.FileNotFoundError: [Errno 2] No such file or directory: 'path_to_your_audio_file.wav'

となった

原因は単純でフィルを指定していないため

https://microai.jp/blog/6cf7f278-e06a-42e1-953b-67eedd9c1ff8
を参考に
https://www.youtube.com/shorts/tLxGgAVvLwU
のYouTube 動画をダウンロードしてみる

yt-dlp -x --audio-format mp3 --output "output.mp4" "https://www.youtube.com/shorts/tLxGgAVvLwU" 

を実行すると

[youtube] Extracting URL: https://www.youtube.com/shorts/tLxGgAVvLwU [youtube] tLxGgAVvLwU: Downloading webpage [youtube] tLxGgAVvLwU: Downloading ios player API JSON [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] HTTP Error 400: Bad Request. Retrying (1/3)... [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] HTTP Error 400: Bad Request. Retrying (2/3)... [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] HTTP Error 400: Bad Request. Retrying (3/3)... [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] Unable to download API page: HTTP Error 400: Bad Request (caused by <HTTPError 400: Bad Request>); please report this issue on https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using yt-dlp -U [youtube] tLxGgAVvLwU: Downloading player 53afa3ce WARNING: [youtube] tLxGgAVvLwU: nsig extraction failed: You may experience throttling for some formats n = nvo-1xpBtSvwgEdI4 ; player = https://www.youtube.com/s/player/53afa3ce/player_ias.vflset/en_US/base.js WARNING: [youtube] tLxGgAVvLwU: nsig extraction failed: You may experience throttling for some formats n = TE4MbILAq1Ti1ox53 ; player = https://www.youtube.com/s/player/53afa3ce/player_ias.vflset/en_US/base.js [youtube] tLxGgAVvLwU: Downloading m3u8 information [info] tLxGgAVvLwU: Downloading 1 format(s): 140 [download] Destination: output.mp4 [download] 100% of 786.76KiB in 00:00:00 at 3.80MiB/s [FixupM4a] Correcting container of "output.mp4" [ExtractAudio] Destination: output.mp4.mp3 

となって output.mp4.mp3 が作成される

これは、yt-dlp が元のMP4ファイルから音声を抽出してMP3形式に変換した結果

音声のみのMP4ファイルを作成するには

yt-dlp -f mp4 --output "output.mp4" "https://www.youtube.com/shorts/tLxGgAVvLwU"

とするらしいが

 [youtube] Extracting URL: https://www.youtube.com/shorts/tLxGgAVvLwU [youtube] tLxGgAVvLwU: Downloading webpage [youtube] tLxGgAVvLwU: Downloading ios player API JSON [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] HTTP Error 400: Bad Request. Retrying (1/3)... [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] HTTP Error 400: Bad Request. Retrying (2/3)... [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] HTTP Error 400: Bad Request. Retrying (3/3)... [youtube] tLxGgAVvLwU: Downloading android player API JSON WARNING: [youtube] YouTube said: ERROR - Precondition check failed. WARNING: [youtube] Unable to download API page: HTTP Error 400: Bad Request (caused by <HTTPError 400: Bad Request>); please report this issue on https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template. Confirm you are on the latest version using yt-dlp -U [youtube] tLxGgAVvLwU: Downloading player 53afa3ce WARNING: [youtube] tLxGgAVvLwU: nsig extraction failed: You may experience throttling for some formats n = xIaDKrIXPH5wjB2nw ; player = https://www.youtube.com/s/player/53afa3ce/player_ias.vflset/en_US/base.js WARNING: [youtube] tLxGgAVvLwU: nsig extraction failed: You may experience throttling for some formats n = yF3boOCKXfHqyR5qo ; player = https://www.youtube.com/s/player/53afa3ce/player_ias.vflset/en_US/base.js [youtube] tLxGgAVvLwU: Downloading m3u8 information [info] tLxGgAVvLwU: Downloading 1 format(s): 18 ERROR: unable to download video data: HTTP Error 403: Forbidden 

となる

このエラーは、YouTubeがアクセスをブロックしていることが原因で発生しています。
特に、yt-dlp が特定のAPIやプレイヤーの情報にアクセスできなくなっている場合に起こります

なお
–force-generic-extractor オプションを使う
–referer オプションを使う
ではだめだった

とりあえずテストで音声の文字起こしができればいいので
これはとりあえず保留

次に

import torch
from faster_whisper import WhisperModel

target_file = "sample.mp4"
model_size = "large-v3"
compute_type_for_gpu = "float16" # or int8_float16
compute_type_for_cup = "int8"
beam_size = 5

# CUDA が利用可能か確認
if torch.cuda.is_available():
    model = WhisperModel(model_size, device="cuda", compute_type=compute_type_for_gpu)
else:
    # CPU で INT8 を使用
    model = WhisperModel(model_size, device="cpu", compute_type=compute_type_for_cup)


segments, info = model.transcribe(target_file, beam_size=beam_size)

print("Detected language '%s' with probability %f" % (info.language, info.language_probability))

for segment in segments:
    print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))

を実行しようとしたが

M1macなのでソース変更の可能性をGPTで調べた

import torch
from faster_whisper import WhisperModel

target_file = "sample.mp4"
model_size = "large-v3"
compute_type_for_gpu = "float16" # or int8_float16
compute_type_for_cpu = "int8"
beam_size = 5

# MPSが利用可能か確認 (AppleシリコンのGPU)
if torch.backends.mps.is_available():
    model = WhisperModel(model_size, device="mps", compute_type=compute_type_for_gpu)
else:
    # CPUで INT8 を使用
    model = WhisperModel(model_size, device="cpu", compute_type=compute_type_for_cpu)

segments, info = model.transcribe(target_file, beam_size=beam_size)

print("Detected language '%s' with probability %f" % (info.language, info.language_probability))

for segment in segments:
    print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))

というように

AppleシリコンのGPUを活用するために
mps(Metal Performance Shaders)デバイスを使うように変更することができます。

ということで
mpsデバイスを使うにあたり設定やインストールが必要なものは?

. PyTorchのインストール
mpsバックエンドはPyTorch 1.12.0以降でサポートされています。
まず、最新のPyTorchをインストールする必要があります

pip install torch torchvision torchaudio

次に使えるか確認したいので

vim check.py

import torch

print(torch.backends.mps.is_available())

として保存

これを

python check.py 

として

True

なのでOK

xcode-select --install

を念の為実行

xcode-select: note: Command line tools are already installed. Use "Software Update" in System Settings or the softwareupdate command line interface to install updates

既にインストール済みだった

mv output.mp4.mp3 output.mp3

でファイルをリネーム

次に

touch sample.py

import torch
from faster_whisper import WhisperModel

target_file = "output.mp3"
model_size = "large-v3"
compute_type_for_gpu = "float16" # or int8_float16
compute_type_for_cpu = "int8"
beam_size = 5

# MPSが利用可能か確認 (AppleシリコンのGPU)
if torch.backends.mps.is_available():
    model = WhisperModel(model_size, device="mps", compute_type=compute_type_for_gpu)
else:
    # CPUで INT8 を使用
    model = WhisperModel(model_size, device="cpu", compute_type=compute_type_for_cpu)

segments, info = model.transcribe(target_file, beam_size=beam_size)

print("Detected language '%s' with probability %f" % (info.language, info.language_probability))

for segment in segments:
    print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))

として保存

これを実行したら

Traceback (most recent call last):
  File "/Users/snowpool/aw10s/whisper/sample.py", line 12, in <module>
    model = WhisperModel(model_size, device="mps", compute_type=compute_type_for_gpu)
  File "/Users/snowpool/.pyenv/versions/3.10.6/lib/python3.10/site-packages/faster_whisper/transcribe.py", line 145, in __init__
    self.model = ctranslate2.models.Whisper(
ValueError: unsupported device mps

となった

結局MPSはサポートされていないので
CPUで動かすことに

import torch
from faster_whisper import WhisperModel

target_file = "output.mp3"
model_size = "large-v3"
compute_type_for_gpu = "float16" # or int8_float16
compute_type_for_cup = "int8"
beam_size = 5

# CUDA が利用可能か確認
if torch.cuda.is_available():
    model = WhisperModel(model_size, device="cuda", compute_type=compute_type_for_gpu)
else:
    # CPU で INT8 を使用
    model = WhisperModel(model_size, device="cpu", compute_type=compute_type_for_cup)


segments, info = model.transcribe(target_file, beam_size=beam_size)

print("Detected language '%s' with probability %f" % (info.language, info.language_probability))

for segment in segments:
    print("[%.2fs -> %.2fs] %s" % (segment.start, segment.end, segment.text))

として再度実行する

 python sample.py > output.txt

を実行すると

[0.00s -> 1.78s] 食べないダイエットでリバウンドを繰り返し
[1.78s -> 3.78s] 正しく食べることでやっと痩せられた私の
[3.78s -> 5.48s] 一日の食事を解説していきます
[5.48s -> 7.44s] 朝はバナナとオールブランで糖質
[7.44s -> 8.40s] くるみで脂質
[8.40s -> 9.52s] 無脂肪ヨーグルトと
[9.52s -> 12.00s] 低糖質低脂質のミルクで脂肪質とってます
[12.00s -> 13.94s] 詳しい作り方はYouTubeで紹介してます
[13.94s -> 16.02s] 昼はサラダ、胸からお米です
[16.02s -> 17.92s] 胸からは揚げ物なんだけど
[17.92s -> 19.72s] 脂質が低くておすすめです
[19.72s -> 21.94s] 運動量も身長も平均な方であれば
[21.94s -> 23.72s] 4歩から5歩ぐらいがおすすめです
[23.72s -> 24.68s] 夜はサラダと
[24.68s -> 27.28s] あやのさんがヘルシーダイエットクラブで教えてくれた
[27.28s -> 28.84s] バターチキンカレーです
[28.84s -> 31.40s] カレーもノーオイルで自分で手作りしたりとか
[31.40s -> 33.86s] こういう脂質が低いカレールーを使ったりとか
[33.86s -> 37.04s] 市販のレトルトカレーでも脂質が低いものがあるので
[37.04s -> 39.22s] ダイエット中でもカレーを楽しめます
[39.22s -> 41.08s] 市販のものの裏面を見るコツは
[41.08s -> 42.64s] YouTubeで詳しく解説してます
[42.64s -> 45.68s] 自分の食事の適正量や過不足がわからないよっていう方は
[45.68s -> 46.56s] 数日でいいので
[46.56s -> 48.16s] アスケンなどで可視化してみると
[48.16s -> 49.52s] 気づきがあるかもしれません

というように文字起こしができた

次はマイクから文字起こしできるようにする