本記事はFIXERが提供する「cloud.config Tech Blog」に掲載された「大規模言語モデル(LLM)で話し相手を(全部ローカルで)作ってみた」を再編集したものです。
GPU有効活用シリーズの第3弾です。第1弾、第2弾はこちらからどうぞ→前編 後編 第2弾
今年から一人暮らしを始めたのですが、家にいると思っている以上に口から何かを発することが無いことに気づきました。Discordなりで家にいても人と会話することができないわけではないのですが、わざわざ人を呼び出すのも気が引けます。
なので最近流行りのLLMに話し相手になってもらうことにしました。
前提条件
すべてローカルで完結すること。
会話文の生成はChatGPTやAzure Open AI、最近はAmazon bedrockなど、Speech to TextやText to SpeechもAzure、AWS、GCPと大抵のクラウドにAPIはありますが、せっかくLLMをローカルで動かすのだからどうせなら全部ローカルにしようとなりました。
なので“どこどこのAPIの方が性能いいぞ”とかのツッコミは心の内にしまっておいてください。
また、細かいプロンプト調整とかには手が回らなかったので、そのあたりの調整は試すときに試行錯誤してみてください。
環境
CPU:Ryzen 5 3600
メモリー:DDR4 32GB
GPU:GeForce RTX 4070(VRAM 12GB)
OS:Windows 11
Docker Desktop(Docker compose)
実行環境準備
ローカルでLLMを動かすには text-generation-webuiのリポジトリを使います。第1弾の後編と同じく、Dockerで実行しましょう。
元々は拡張機能として作りたかったのですがどうもうまく動かなかったので、APIサーバーとしてのtext-generation-web-uiでコンテナひとつ、text to speech、speech to text、フロントを担うコンテナひとつで合計2つのコンテナ使用します。
ファイル構造は以下の通りです。
. ├─front/ │ ├─whisper/ │ ├─index.html │ ├─server.py │ └─Dockerfile ├─text-generation-webui/ │├─... │... │└─... └─compose.yml
index.htmlはとりあえず動きさえすればいいので、CSSも無しに最低限の機能(録音、再生、やり取りの表示)を実装しただけになっています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<div>
<ol id="chat">
</ol>
</div>
<div class="flex gap-8 py-6">
<form>
<input type="button" , value="録音開始" , id="start">
</form>
</div>
<div class="flex gap-8 py-6">
<form>
<input type="button" , value="録音終了" , id="end">
</form>
</div>
<div>
<select name="models" id="models">
<option value="medium">--Please choose an option--</option>
{% for model in models %}
<option value={{ model }} id="model">{{ model }}</option>
{% endfor %}
</select>
</div>
</div>
<script>
(() => {
let recorder = null;
let chunks = [];
let startButton = null;
let endButton = null;
function startup() {
navigator.mediaDevices.getUserMedia({
audio: true
}).then((stream) => {
recorder = new MediaRecorder(stream)
recorder.onstop = async (_) => {
const blob = new Blob(chunks, { "type": "audio/webm" });
const uploadFormData = new FormData();
uploadFormData.append("file", blob);
uploadFormData.append("name", document.getElementById("models").value)
const headers = new Headers();
const resp = await fetch(new URL(`${location.href}v1/audio/transcriptions`), {
headers: headers,
body: uploadFormData,
method: "POST",
});
resp.text().then((text) => {
console.log(text)
const obj = JSON.parse(text)
const li = document.getElementById("chat")
const input = document.createElement("li")
input.innerText = `You: ${obj.input}`
const output = document.createElement("li")
output.innerText = `Chara: ${obj.output}`
const bgm1 = new Audio(`/v1/audio/play?text=${obj.output}`);
bgm1.type = "audio/wav"
bgm1.addEventListener("canplay", () => {
li.appendChild(input)
li.appendChild(output)
bgm1.play()
})
});
chunks = [];
};
recorder.ondataavailable = (e) => {
chunks.push(e.data)
};
})
startButton = document.getElementById("start");
endButton = document.getElementById("end");
startButton.addEventListener("click", () => {
console.log("start", !!recorder)
if (recorder != null) {
recorder.start(1000)
}
});
endButton.addEventListener("click", () => {
console.log("end")
recorder.stop();
});
}
window.addEventListener("load", startup, false);
})();
</script>
</body>
</html>
server.pyでは送られてきた音声をテキストに変換、text-generation-webuiのAPIにリクエストを送って返信をまた音声にして返しています。
Speech to TextにはWhisperを、Text to SpeechにはVALL-E-Xを使いました。
Python
from fastapi import FastAPI, File, UploadFile
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.requests import Request
from fastapi.templating import Jinja2Templates
import whisper
from whisper import available_models
import numpy as np
from ffmpeg import input, Error
import uvicorn
import requests
import json
from utils.generation import SAMPLE_RATE, generate_audio, preload_models
from scipy.io.wavfile import write as write_wav
download_root = './whisper'
app = FastAPI()
templates = Jinja2Templates(directory='.')
preload_models()
@app.get("/",response_class=HTMLResponse)
async def home(request: Request):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"models": available_models()
}
)
@app.post("/v1/audio/transcriptions")
async def create_file(file:UploadFile = File(...), name:str="medium"):
contents = await file.read()
text = transcribe(name,contents)
headers = {
"Content-Type": "application/json"
}
data = {
"messages": [
{
"role": "system",
"content": "You must answer in Japanese and short sentences."
},
{
"role": "user",
"content": text
}
],
"mode": "instruct",
"instruction_template": "Alpaca"
}
response = requests.post(
url="http://text-generation-webui:5000/v1/chat/completions",
headers=headers,
data=json.dumps(data),
verify=False
)
assistant_message = response.json()['choices'][0]['message']['content']
resp = {
"input": text,
"output": assistant_message
}
return resp
@app.get("/v1/audio/play", response_class=FileResponse)
async def play(text:str=""):
audio_array = generate_audio(text)
write_wav("audio.wav", SAMPLE_RATE, audio_array)
return FileResponse("audio.wav")
def transcribe(name:str,data:bytes):
ndarray = load_audio(data)
model = whisper.load_model(name, download_root=download_root,device='cuda')
result = model.transcribe(audio=ndarray, verbose=True, language='ja')
return result["text"]
def load_audio(file_bytes: bytes, sr: int = 16_000) -> np.ndarray:
try:
out, _ = (
input('pipe:', threads=0)
.output("pipe:", format="s16le", acodec="pcm_s16le", ac=1, ar=sr)
.run_async(pipe_stdin=True, pipe_stdout=True)
).communicate(input=file_bytes)
except Error as e:
raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e
return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
uvicorn.run(app, host="0.0.0.0",port=8000)
そしてDockerfileはこれを使います
FROM nvidia/cuda:12.1.1-devel-ubuntu22.04 ENV DEBIAN_FRONTEND noninteractive RUN apt-get update && \ apt-get --no-install-recommends -y install curl git ffmpeg python3 python3-dev python3-pip libcudnn8 libcudnn8-dev && \ pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu121 && \ pip install git+https://github.com/openai/whisper.git faster_whisper fastapi python-multipart ffmpeg-python "uvicorn[standard]" RUN git clone https://github.com/Plachtaa/VALL-E-X.git /app && \ pip install -r /app/requirements.txt ENV LD_LIBRARY_PATH /usr/local/cuda/lib64/:$LD_LIBRARY_PATH ENV PYTHONPATH /app:$PYTHONPATH COPY . /app WORKDIR /app CMD [ "python3", "server.py" ]
実行の事前準備として、compose.ymlの用意をするのですが、text-generation-web-uiのdockerディレクトリにあるものを参考に書き加えたうえで、text-generation-webuiディレクトリやfrontendディレクトリと同じ階層に配置します。
version: '3'
services:
front:
build:
context: front
dockerfile: Dockerfile
container_name: front
tty: true
ports:
- "8000:8000"
volumes:
- ./front/whisper:/app/whisper
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [ gpu ]
text-generation-webui:
build:
context: ./text-generation-webui
dockerfile: docker/nvidia/Dockerfile
args:
# specify which cuda version your card supports: https://developer.nvidia.com/cuda-gpus
TORCH_CUDA_ARCH_LIST: ${TORCH_CUDA_ARCH_LIST:-7.5}
BUILD_EXTENSIONS: ${BUILD_EXTENSIONS:-openai}
APP_GID: ${APP_GID:-6972}
APP_UID: ${APP_UID-6972}
env_file: ./text-generation-webui/.env
user: "${APP_RUNTIME_UID:-6972}:${APP_RUNTIME_GID:-6972}"
ports:
- "${HOST_PORT:-7860}:${CONTAINER_PORT:-7860}"
- "${HOST_API_PORT:-5000}:${CONTAINER_API_PORT:-5000}"
stdin_open: true
tty: true
volumes:
- ./text-generation-webui/characters:/home/app/text-generation-webui/characters
- ./text-generation-webui/extensions:/home/app/text-generation-webui/extensions
- ./text-generation-webui/loras:/home/app/text-generation-webui/loras
- ./text-generation-webui/models:/home/app/text-generation-webui/models
- ./text-generation-webui/presets:/home/app/text-generation-webui/presets
- ./text-generation-webui/prompts:/home/app/text-generation-webui/prompts
- ./text-generation-webui/softprompts:/home/app/text-generation-webui/softprompts
- ./text-generation-webui/training:/home/app/text-generation-webui/training
- ./text-generation-webui/cloudflared:/etc/cloudflared
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [ gpu ]
後はDocker用のドキュメントを見ながら
text-generation-webui/modelsにお好きなモデルを配置し、text-generation-web-ui/docker/.env.sampleを.envにリネームしたうえで書き換えます。
今回はモデルにTheBloke_StableBeluga-7B-GPTQを使用し、TORCH_CUDA_ARCH_LISTは8.9としました。
# by default the Dockerfile specifies these versions: 3.5;5.0;6.0;6.1;7.0;7.5;8.0;8.6+PTX # however for me to work i had to specify the exact version for my card ( 2060 ) it was 7.5 # https://developer.nvidia.com/cuda-gpus you can find the version for your card here TORCH_CUDA_ARCH_LIST=8.9 # your command-line flags go here: CLI_ARGS=--model TheBloke_StableBeluga-7B-GPTQ --listen --api # the port the webui binds to on the host HOST_PORT=7860 # the port the webui binds to inside the container CONTAINER_PORT=7860 # the port the api binds to on the host HOST_API_PORT=5000 # the port the api binds to inside the container CONTAINER_API_PORT=5000 # Comma separated extensions to build BUILD_EXTENSIONS="openai" # Set APP_RUNTIME_GID to an appropriate host system group to enable access to mounted volumes # You can find your current host user group id with the command `id -g` APP_RUNTIME_GID=6972 # override default app build permissions (handy for deploying to cloud) #APP_GID=6972 #APP_UID=6972
編集が終わったら、
docker compose up -d
でコンテナを起動します。
ビルドが終わればコンテナの起動自体はすぐに終わりますが、コンテナをたてた直後はモデルをロードしないとAPIが反応を返してくれないので数分待ちます。
実行の様子
録音開始のボタンを押して話しかけ停止を押すと、かなり時間がかかりますが返事が返ってきました。
画像生成とは違ってGeforce RTX 4070であろうと、精度と時間どちらで見てもかなりキツイですね。
またVALL-E-Xは、“声が生成する度に変わってしまう”や“文章が長くなると合成音声が崩壊する”、といったことが起こるのでvoicevoxあたりを使うのが現実的だと思います。
まだまだプロンプトにも改善の余地はありますが、ひとまず何かしらの応答を音声で返すことができました。
最後に
一人暮らしでも寂しくない話相手を作ることはできたのですが、客観的にみるとディスプレーに向かって話しかけたら虚空から返事が返ってくるというなかなかシュールというか、これはこれで残念なシチュエーションになってしまいました。
音声アシスタントのような形にすればよかったかもしれないとほんのちょっとだけ後悔しています。
ただレスポンスがめちゃくちゃ遅くて、正直使い物になりません。
なので冒頭でローカルでやることのツッコミは心の中にしまってもらってなんですが、絶対にこちらの記事の方を参考にした方がいいです。
小野亮太朗/FIXER
(おの りょうたろう)
23卒エンジニア。SBCでNAS、VPN、マイクラとかのサーバを建てて遊んでる人。
動的型付けよりは静的型付けが好き。


この連載の記事
-
TECH
仕訳を生成AIが自動で行うプロンプトを作ってみた -
TECH
学生向けの生成AI講義で人気があったプロンプト演習3つ(+α) -
TECH
ユースケースが見つけやすい! 便利な「Microsoft 365 Copilot 活用ベストプラクティス集」を入手しよう -
TECH
自治体業務でどう使う? 生成AIアイデアソンに自治体職員が挑戦 -
TECH
生成AIで360°パノラマ画像を作る! 最新研究でやってみた -
TECH
RAGの精度を改善する現実的な方法4つ、AWS Summitで学んだ -
TECH
過去問から例題をAIで生成、データベーススペシャリスト試験に再挑戦 -
TECH
ウェビナーの構成・タイトル・告知、すべて生成AIに手伝ってもらった -
TECH
6種類のLLMに「ワンナイト人狼」をやらせてみた結果… -
TECH
システムエンジニア目線で見たプロンプトエンジニアリングのコツ -
TECH
アプリ開発、TypeScriptやCSSのコード作成もすべてGaiXerにお任せしてみた - この連載の一覧へ


