Windows11でAI言語モデルPhi3と音声入出力チャットしてみた!

※ 当サイトではアフィリエイト広告を利用しています。リンクは広告リンクも含みます。

この記事は約24分で読めます。
広告


ローカル環境(パソコン)に言語モデルをダウンロードし
Windows標準のPowerShellで音声入出チャットをしてみます。
Ollamaを使えば、文字ベースのチャットは、そのままでできます。
そこに音声入出の機能を加えます。起動後は、音声のみで操作します。
しかし、WEB上に公開したりアプリケーションとして一つにまとまって動くものではありません。
完全に個人でパソコンで利用するものになります。

期待できない点
・言語モデルphi3-miniの軽量版なため回答のパフォーマンスは低い。
・言語モデルphi3の出力テキストが全部出てから音声変換するので
リアルタイムでの会話というわけにはいきません。かなり待ちます。
・音声入出力にMicrosoftのもの(無料)を使っているのでネット接続は必要です。
(ネットを切った状態でphi3とのテキストでの会話は確認したらできました。)

必要な環境は
Windows11(10は未確認)、パソコンはCPU。(GPUは使ってません。)
事前にな必要ものVsコード、Anacondaになります。

手順は
1、Ollamaと言語モデルphi3のパソコンへのインストール
2、Anaconda仮想環境のCreate
(仮想環境を新たにつくらなくても動くと思いますが、
Anacondaのbaseの仮想環境にpipでいろいろライブラリを追加していくのは
何かあったときに大変です。
新しく仮想環境をつくっておけば、いらなくなったり、変な動作したら
その仮想環境を削除することもできます。)
3、ターミナルからの文字入力、テキストの音声ファイル変換、再生、音声入力と個別に実行して見ていきます。
4、最終的なコードを書く。
Youtubeにしてあります。実演部分

広告

Ollamaのインストールとphi3のダウンロード

方法は以下参照。

パソコンにインストールしてWindowsPowerShellを立ち上げ
ollama run phi3
をするとターミナルでphi3とやり取りできます。
それと同時にパソコン内にサーバーが立ち上がっているようです。(常駐)
今回、使うPythonのライブラリ(ollama-python)のドキュメント
Ollama Python LibraryのReadMEに
Prerequisites(前提 条件)
You need to have a local ollama server running to be able to continue.
(続行するには、ローカルの ollama サーバーが実行されている必要があります。)
と書いてあるので
以降pythonで操作するときは、ライブラリollama-pythonを使って
そのサーバーとやりとりしていると思われます。
目次へ

Anacondaで仮想環境Create

Anaconda Navigatorを立ち上げ
環境の名前(ollamatestと付けました)を付けてCreate
OpenTerminal
mkdir ollamatest
cd ollamatest
code .
でVsコードを立ち上げる。
表示→ターミナルで
ollama-pythonのライブラリーを入れます。
動画のtest1.pyはここのコードです。以下参照

Anacondaの仮想環境について

なぜAnaconda仮想環境をCreateして使うのか

AnacondaでPythonの実行環境を用意している場合は以下も気にしておいてください。
Anacondaでpipを使う際にはいくつかの注意点があります。Anacondaは独自のパッケージ管理システムを持っており、pipとは異なる依存関係の管理を行います。そのため、Anaconda環境内でpipを使用すると、依存関係の衝突や環境の破壊が起こる可能性があり、場合によってはAnaconda自体の再インストールが必要になることもあります。一般的には、Anaconda環境ではcondaコマンドを使用し、pipは必要なパッケージがcondaで利用できない場合に限定して使用することが推奨されています。(Bing談)
私、自身、Anacondaで新たに仮想環境をCreateしないで今まで使ってきて
pipを使ったことで致命的な問題がおきた経験はないですが。
最近は念のためpipする場合は仮想環境をCreateして、
そこにpipして使う方がいいと思ってます。
その際、混乱するのが仮想環境とターミナルの関係です。
そのことについて後述します。
目次へ

Tips
ちなみに仮想環境を立ち上げないで実行すると以下のようにpipでインストールしたのに
モデュール(ライブラリ)がないというエラーになります。
File “C:\Users\user\ollamatest\test7.py”, line 2, in
import ollama
ModuleNotFoundError: No module named ‘ollama’
ということで仮想環境は、それぞれ独立していると思います。
またJupiterNotebookはbaseの仮想環境で動いてると思われます。
JupiterNotebookと新しく作ったVsコードで使用する仮想環境は同時に使用できています。
JupiterNotebookを別の仮想環境で使用するのは、ちょっと難しそうです。
参考:https://qiita.com/yakisobamilk/items/867dce8e53824146ce05

仮想環境とターミナルの関係

conda env listを使うとAnaconda仮想環境の一覧が表示されます。
パソコンを立ち上げた直後にPowerShellで実行した場合
(base) PS C:\Users\user\ollamatest> conda env list
# conda environments:
#
base                  *  C:\Users\user\anaconda3
Django_node              C:\Users\user\anaconda3\envs\Django_node
ollamatest               C:\Users\user\anaconda3\envs\ollamatest

AnacondaNavigatorを立ち上げEnviromentsで「ollamatest」をクリックした後
(この場合立ち上がるのはPowerShellではなく旧式のコマンドプロンプト)
Open terminalで実行した場合。
(ollamatest) C:\Users\user>conda env list
# conda environments:
#
base                     C:\Users\user\anaconda3
Django_node              C:\Users\user\anaconda3\envs\Django_node
ollamatest            *  C:\Users\user\anaconda3\envs\ollamatest
全てのアプリからコマンドプロンプトを立ち上げ実行した場合
以下のように、どこにも*はついてません。
C:\Users\user>conda env list
# conda environments:
#
base                     C:\Users\user\anaconda3
Django_node              C:\Users\user\anaconda3\envs\Django_node
ollamatest               C:\Users\user\anaconda3\envs\ollamatest
また左の()には仮想環境名が入っているようです。
1番最後にやったものは仮想環境と関係ないのでPythonコードは実行できません。
python+ENTERとすると「入手」という画面が立ち上がります。
ここで入手してインストールするとパソコンにPythonがインストールされると思いますが
Anacondaを使っている場合は仮想環境で実行すればいいので必要ないと思います。
上の2つはpythonの対話モードが起動されます。抜けるときはexit()

またollama run phi3は、どの仮想環境でもパソコンのターミナルからも実行できる。
これはnotepad.exe()メモ帳と同じなので。パソコンにインストールされているものが
実行されていると思われる。
なのでphi3がダウンロードされていない状態でollama run phi3を
仮想環境で実行してもパソコンにインストールされると思います。
目次へ

ターミナルから質問文入力

test1.pyという名前で以下作成。python test.pyで実行。
入力して、Enterを押すと、そのままプリントされる。
終了+Enterで終了。
動画のtest2.py

while True:
    situmon = input("質問を入力してください(終了するには '終了' と入力してください): ")
    if situmon.lower() == '終了':
        print("プログラムを終了します。")
        break
    # hentou部分(質問に対する回答をここに記述)
    hentou = f"質問 '{situmon}' に対する回答です。"
    print(hentou)

テキストから音声へ変換

参考https://uepon.hatenadiary.com/entry/2024/02/12/150425
Windowsの場合、以下pipだけで使えました。

pip install edge-tts

参考サイトのかたはLinuxでやっておられますが、Pythonコードは同じなので
夏目漱石の変換例は、そのまま同じでできます。
edge-ttsのドキュメント
ここにPythonのサンプルが載っています。
恐らく参考で使われているのはsync_audio_generation_in_async_context.pyのようです。
複雑で何をやっているのかよく分からないので1番Basicな例と比較してひも解くことにしました。
1番ベーシックな例:basic_generation.py
動画のtest3.py

#!/usr/bin/env python3

"""
Basic example of edge_tts usage.
"""
import asyncio
import edge_tts

TEXT = "Hello World!"
VOICE = "en-GB-SoniaNeural"
OUTPUT_FILE = "test.mp3"

async def amain() -> None:
    """Main function"""
    communicate = edge_tts.Communicate(TEXT, VOICE)
    await communicate.save(OUTPUT_FILE)

if __name__ == "__main__":
#これの役割は、このファイルbasic_generation.pyを
#別のファイルから関数(amain())だけ使いたい場合は、ここが実行されない。
    asyncio.run(amain())

シンプルにしてやってみる。
非同期(async/await)をなくし、if __name__ == “__main__”:なども削除して以下で実験。

import asyncio
import edge_tts

TEXT = "Hello World!"
VOICE = "en-GB-SoniaNeural"
OUTPUT_FILE = "test.mp3"

communicate = edge_tts.Communicate(TEXT, VOICE)
communicate.save(OUTPUT_FILE)
エラーRuntimeWarning: coroutine 'Communicate.save' was never awaited
  communicate.save(OUTPUT_FILE)
communicate.saveが非同期関数で定義されていて(communicate.pyでasync def save(がある)
使うときはawaitを使わなければならない。
await キーワードを使用するためには、非同期関数の内部で使用する必要があり以下になる。
async def amain() -> None:
    communicate = edge_tts.Communicate(TEXT, VOICE)
    await communicate.save(OUTPUT_FILE)
-> None: は関数の戻り値の型ヒントを示し、この場合、関数が None を返すことを示しています。
型ヒントがない場合、関数の戻り値の型は暗黙的になりますが、Pythonは動的型付け言語であるため、エラーにはなりません。ただし、型ヒントがあることでコードの可読性が向上し、IDEや静的解析ツールによるサポートが受けやすくなります。

同期関数でやってみる

communicate.save(OUTPUT_FILE)をsync_audio_generation_in_async_context.pyで使っている下の同期関数に変えて実施してみる
communicate.save_sync(OUTPUT_FILE)  # 同期関数で音声ファイルを保存
結果はうまくいきます。
どうやらこの2つの関数の違いで処理が違うようです。

sync_audio_generation_in_async_context.py(恐らく参考で使われている)の
basic_generation.pyを比較することによってえた利点。(ChatGPTに聞いた)

  • 同期関数の再利用: 非同期関数内で同期関数を呼び出す方法を示しており、既存の同期関数を非同期環境で再利用することができます。
  • 柔軟な設計: 非同期プログラム内で同期処理を実行するための柔軟な設計を提供します。
  • エラー処理: 非同期処理中に同期関数を安全に呼び出す方法を提供し、特定のシナリオ(例えば、同期関数の使用が避けられない場合)でのエラー処理が容易になります。

このため、sync_audio_generation_in_async_context.pyは、
非同期環境で同期関数を利用する必要がある場合に特に有用です。(ChatGPT談)
ということだがあまり具体的な場面が想像できないので問題発生時に、こういうのもあるということで思い出すために長々と書いてます。

sync_audio_generation_in_async_context.py(恐らく参考で使われている)のChatGPT解説

sync_audio_generation_in_async_context.py内のコメント
"""
This example shows that sync version of save function also works when run from 
a sync function called itself from an async function.
The simple implementation of save_sync() with only asyncio.run would fail in this scenario, 
that's why ThreadPoolExecutor is used in implementation.

"""
訳
"""
この例では、非同期関数から同期関数を呼び出して音声ファイルを保存する方法を示しています。
単純にasyncio.runを使用しただけのsave_sync()の実装では、このシナリオでは失敗するため、
ThreadPoolExecutorが使用されています。
"""

asyncio.runを使って同期関数(async defで定義していないもの)を実行してみた。

import asyncio
import edge_tts

TEXT = "riding up back on the street Did my chances to glory"
VOICE = "en-GB-SoniaNeural"
OUTPUT_FILE = "test8.mp3"

def sync_main() -> None:
    """メイン関数(同期バージョン)"""
    communicate = edge_tts.Communicate(TEXT, VOICE)
    communicate.save_sync(OUTPUT_FILE)  # 同期関数で音声ファイルを保存
asyncio.run(sync_main())

asyncio.runはコルーチンは async def 文で実装されたものしか使えないようなのでエラーになった。
最終的にはbasic_generation.py(非同期関数で音声ファイルを保存する方法)とsync_audio_generation_in_async_context.py(非同期関数から同期関数を呼び出して音声ファイルを保存する方法)をベースにしたタイプを作って両方とも動きました。(後述)
同期、非同期の話はややこしいのでbasic_generation.pyを使って問題が発生してから
考えたい。

音声再生

ファイル名.mp3(上で作った音声ファイルなど)をコマンドが実行しているところにおき以下で音が再生できます。

pip install playsound  

動画のtest4.py

from playsound import playsound
playsound("ファイル名.mp3")

音声入力

Windows標準の音声入力が使える。
ターミナルにカーソルがある状態で
Windowsキー+H
マイクにしゃべれば文字が入力されます。
目次へ

最終コード

出力の音が大きいので注意してください。
上でやってきたものをベースに最終コードを作りました。
ライブラリが1個追加でpipが必要です。

pip install pyautogui

Windows+HをPythonから使用するために使ってます。詳細は、こちら↓

コードのポイント

  • mode=jp(日本語)とen(英語)で切り替えられる。enのとき日本語で話すとエラーになるので注意。
  • promptのroleの文章は、よく検討していないが、確かに、ここの文は回答に影響する。
  • メタコメント(なぜその回答をしたかについての自分の説明など)なしで、とroleに書いたが、あまり効かなかった。
  • メタコメント部分はifで”Instruction” “指示”というワードで除外したが、パターンが決まっているわけではなさそうなので排除しきれない。
  • ファイルに音声を保存するのではなく一時ファイルに保存するようにしたら音声ファイルの削除などの管理が不要で楽になった。

コード

イチゲをOFUSEで応援する(御質問でもOKです)Vプリカでのお支払いがおすすめです。

basic_generation.pyベース(動画内test7.py)

import asyncio
import ollama
import edge_tts
import time
import os
import tempfile
from playsound import playsound
import pyautogui
# モードの設定('jp'は日本語、'en'は英語)
mode='jp'

def yaritori(situmon,mode):
    # 見やすくするため
    print("\n")
    hentou = ''
    # カジュアルな会話スタイルの指示を追加
    if mode == 'jp':
        rolemodel={'role': 'system', 'content': 'あなたは親しみやすい会話パートナーです。気軽に話してください。メタコメントは出力しないでください。できるだけ100文字以内で短く返答してください。'}
    else:
        rolemodel={'role': 'system', 'content': 'You are a friendly and patient English teacher for beginners. Please keep your responses within 100 characters.'}
    # プロンプトの設定
    prompt = [
        rolemodel,
        {'role': 'user', 'content': situmon}
    ]
    # Ollamaのチャットストリームを開始
    stream = ollama.chat(
        model='phi3',
        messages=prompt,
        stream=True,
    )
    # ストリームからの出力を読み取る
    for chunk in stream:
        content = chunk['message']['content']
        # "Instruction"や"指示"が含まれる場合、ストリーム出力を停止
        hentou += content
        if "Instruction" in hentou or "指示" in hentou:
            print("\n話が止まらなくなった!ちょっと待ってね")
            # Instruction や 指示が含まれている場合、hentouから削除する
            hentou = hentou.replace("Instruction", "").replace("指示", "")
            break
        if content != '指':
            print(content, end='', flush=True)

    # 最終的な回答を改行して出力
    print("\n")
    # 最後の文字が '指' の場合は削除する
    if hentou.endswith('指'):
        hentou = hentou[:-1]
    TEXT = hentou
    # 音声合成の設定
    if mode == 'jp':
        VOICE = "ja-JP-NanamiNeural"
    else:
        VOICE = "en-GB-SoniaNeural"
    RATE = "-5%"
    # 非同期で音声ファイルを生成する関数
    async def amain(output_file) -> None:
        communicate = edge_tts.Communicate(TEXT, VOICE, rate=RATE)
        await communicate.save(output_file)

    # 一時ファイルを作成
    with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_audio:
        OUTPUT_FILE = temp_audio.name
    # 非同期関数を実行して音声ファイルを生成
    asyncio.run(amain(OUTPUT_FILE))
    # ファイルが完全に書き込まれるまで待機
    time.sleep(1)

    # 音声ファイルを再生し、再生後に削除
    if os.path.exists(OUTPUT_FILE):
        try:
            playsound(OUTPUT_FILE)
        #except playsound.PlaysoundException as e:
        except Exception as e:
            print(f"再生中にエラーが発生しました: {e}")
        finally:
            os.remove(OUTPUT_FILE)  # 一時ファイルを削除
    else:
        print(f"ファイル {OUTPUT_FILE} が見つかりませんでした。")

while True:
    # 音声入力開始
    pyautogui.hotkey('win', 'h') 
    situmon = input("質問を入力してください(終了するには '終了' と入力してください): ")
    # 音声入力停止
    pyautogui.hotkey('win', 'h') 
    # 改行という文字を削除
    situmon=situmon.replace('改行','').replace('解除','')
    # 終了条件のチェック
    if mode == 'jp':
        if situmon.lower() == '終了' or situmon.lower() == '終了。':
            print("プログラムを終了します。")
            break
    else:
        if 'end' in situmon or 'End' in situmon:
            print("プログラムを終了します。")
            break
    # 質問を処理する関数を呼び出し
    yaritori(situmon,mode)

以下使っていませんが。sync_audio_generation_in_async_context.pyベース

import asyncio
import ollama
import edge_tts
import time
import os
import tempfile
from playsound import playsound

def yaritori(situmon):
    # 見やすくするため
    print("\n")
    hentou = ''
    # カジュアルな会話スタイルの指示を追加
    prompt = [
        {'role': 'system', 'content': 'あなたは親しみやすい会話パートナーです。気軽に話してください。'},
        {'role': 'user', 'content': situmon}
    ]
    stream = ollama.chat(
        model='phi3',
        messages=prompt,
        stream=True,
    )

    for chunk in stream:
        content = chunk['message']['content']
        # "Instruction"や"指示"が含まれる場合、ストリーム出力を停止

        hentou += content
        if "Instruction" in hentou or "指示" in hentou:
            print("\n話が止まらなくなった!ちょっと待ってね")
            # Instruction や 指示が含まれている場合、hentouから削除する
            hentou = hentou.replace("Instruction", "").replace("指示", "")
            break
        if content != '指':
            print(content, end='', flush=True)

    # 最終的な回答を改行して出力
    print("\n")
    # 最後の文字が '指' の場合は削除する
    if hentou.endswith('指'):
        hentou = hentou[:-1]
    TEXT = hentou
    VOICE = "ja-JP-NanamiNeural"
    RATE = "-5%"

    async def amain(output_file) -> None:
        communicate = edge_tts.Communicate(TEXT, VOICE, rate=RATE)
        await communicate.save(output_file)

    # Create a temporary file
    with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_audio:
        OUTPUT_FILE = temp_audio.name

    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
        loop.run_until_complete(amain(OUTPUT_FILE))
    finally:
        loop.close()

    # Ensure the file is written completely
    time.sleep(1)

    # Check if the file exists
    if os.path.exists(OUTPUT_FILE):
        try:
            playsound(OUTPUT_FILE)
        except playsound.PlaysoundException as e:
            print(f"再生中にエラーが発生しました: {e}")
        finally:
            os.remove(OUTPUT_FILE)  # Delete the temporary file
    else:
        print(f"ファイル {OUTPUT_FILE} が見つかりませんでした。")

while True:
    situmon = input("質問を入力してください(終了するには '終了' と入力してください): ")
    if situmon.lower() == '終了' or situmon.lower() == '終了。':
        print("プログラムを終了します。")
        break
    yaritori(situmon)

所感

phi3-miniの機能は、そこそこですが、
実際の利用はPythonで周辺の機能を作るための練習台として利用するものだと思います。
環境が許せば、もう少しパラメータ数の多い大きいモデルに変えたほうがいいです。
言語モデル自体の機能に期待する場合は
WEB上で利用できるChatGPT、Bing、Geminiを使うほうがいいです。

イチゲをOFUSEで応援する(御質問でもOKです)Vプリカでのお支払いがおすすめです。
MENTAやってます(ichige)
目次へ

タイトルとURLをコピーしました