音声認識のエンジンと、そのサーバ〜クライアントを作ってみます。
関連
- ◯
- サーバ証明書を取得〜更新する(ワイルドカード証明書、ネームサーバ経由):Let's Encrypt
検証
- ・
- クラウド:GCP
- ・
- コンテナ:Docker
- ・
- ゲストOS:Ubuntu 22.04
- ・
- ウェブブラウザ:Chrome
- ・
- 画像生成アプリ:ComfyUI
概要
- ◯
- 背景
いまは音声認識のモデルを、ローカル環境で動かすことができます。
- ◯
- 対応
ここでは、音声認識のエンジンと、そのサーバ〜クライアントの、かんたんな実装を試みます。
- ◯
- 仕様:バイナリデータ
JavaScript の MediaRecorder は、既定で、Blob 形式のチャンクを作ります(Blob は File の上位クラスで、生のデータが格納されます)
いっぽう Unity の C# は、生データをバイト配列としてあつかうのが基本です。
- ◯
- 検討:データの変換
音声認識で音声データをあつかう場合、フロントエンドで生成されたデータ形式と、バックエンドのエンジンがあつかえる形式が同じとはかぎりません。
データの変換が必要になることがありますが、それをどこで行うか(フロントエンド側かバックエンド側か)で、それぞれのコード量が違ってきます。
ここでは、多様な形式に対応できるよう、バックエンド側で音声データの変換を行います。[※1][※2][※3]
- ◯
- 構成
Wav2Vec により、自己教師学習だけで、音声認識ができる環境が整っていますーーここでは、Wav2Vec で学習した日本語対応のモデルを使います:
- ・
- https://huggingface.co/NTQAI/wav2vec2-large-japanese
- ※1
- たとえば WAV 形式への変換の場合、フロントエンド(JavaScript, Unity)での変換には、いろいろな試みがあります(これはもともと WAV 形式が主流だったことにもよりますが)ーーただ JavaScript にせよ Unity にせよ、ほんらい UI に特化したものなので、変換は役割が違います(変換などの重い処理に、かならずしも向いているわけではありません)。
- ※2
- バックエンド側(Python)では、いろいろな変換のためのライブラリが使えます。別プロセスでもいいなら、強力な変換ツールも利用できます(FFmpeg, ImageMagick, ...)ーー標準入力/標準出力を使えば、オンメモリで処理が可能だったりします。
- ※3
- バックエンド側で変換することで、あらゆる変換に対応できるようになります。JavaScript の MediaRecorder は、むしろ映像が主体ですし(webm, webp)。クライアントが Unity なら、録音した PCM 音源を、バイト配列としてそのまま渡すことができますしね。
設置
リモートのサーバで、コンテナ向けのフォルダ群を作ります:
$ cd ${HOME}/system $ mkdir vir104 $ mkdir vol104 $ cd ${HOME}/system/vol104 $ mkdir bin
コンテナの初期設定ファイルを作ります:
- ・
- ${HOME}/system/vir104/Dockerfile
FROM ubuntu:22.04 RUN apt-get -y update RUN apt-get -y upgrade RUN apt-get -y install python3 RUN apt-get -y install pip RUN ln -s /usr/bin/python3 /usr/bin/python
コンテナを作成し、いったん起動します。更新分は、適宜コミットします:
$ docker build --no-cache -t ubuntu:vir104 ${HOME}/system/vir104 $ docker commit cnt104 ubuntu:vir104 $ docker run -it --rm --gpus all -v ${HOME}/system:/system -v /exp001/hgf:/root/.cache/huggingface --name cnt104 ubuntu:vir104
コンテナ上で、ライブラリ群を設置します:
$ pip install torch $ pip install transformers $ pip install accelerate $ pip install sentencepiece $ pip install protobuf $ pip install bitsandbytes # ウェブサーバ/クライアント向けライブラリ群(非同期) $ pip install aiohttp # 音声認識のための変換ライブラリ群 $ apt install ffmpeg $ pip install soundfile
コンテナ上で、認証を行います:
$ apt install git $ huggingface-cli login token: ******** $ git config --global credential.helper store
実装
- ◯
- バックエンド:エンジン
- ・
- appstt/bin/sttprc.py
import io import subprocess import soundfile as sf import torch from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor class SttPrc(): def __init__(self, numsmp, nmodel): self.numsmp = numsmp self.prcstt = Wav2Vec2Processor.from_pretrained(nmodel) self.ctcstt = Wav2Vec2ForCTC.from_pretrained(nmodel) self.cmdffm = ["ffmpeg", "-i", "pipe:0", "-f", "wav", "-ar", str(self.numsmp), "pipe:1"] def infstt(self, datsnd): prcffm = subprocess.Popen(self.cmdffm, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) datwav, errors = prcffm.communicate(input=datsnd) if prcffm.returncode != 0: raise Exception("err: " + errors.decode('utf-8')) arrwav, _ = sf.read(io.BytesIO(datwav)) tsrwav = self.prcstt(arrwav, sampling_rate=self.numsmp, return_tensors="pt", padding=True) with torch.no_grad(): logits = self.ctcstt(tsrwav.input_values, attention_mask=tsrwav.attention_mask).logits idsprd = torch.argmax(logits, dim=-1) txtprd = self.prcstt.batch_decode(idsprd) return txtprd[0]
- ◯
- バックエンド:サーバ
- ・
- appstt/bin/sttsrv.py
import json import asyncio from aiohttp import web import ssl from sttprc import SttPrc async def res001(datreq): global txtres, evtstt # EVT datsnd = await datreq.read() txtres = sttprc.infstt(datsnd) #print(txtres) # DBG evtstt.set() # EVT datres = web.Response( headers={ 'Access-Control-Allow-Origin': '*' } ) return datres async def opt001(datreq): datres = web.Response( headers={ 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Access-Control-Allow-Origin': '*', } ) return datres async def res002(datreq): global txtres, evtstt # EVT await evtstt.wait() # EVT datres = web.Response( text=json.dumps({"txtres": txtres}), headers={ "Content-Type": "application/json" } ) evtstt.clear() # EVT return datres prttgt = 8082 adr001 = "/putsnd" adr002 = "/gettxt" pthcrt = '/system/vol105/etc_nginx/letsencrypt/<server_remote>/fullchain.pem' pthkey = '/system/vol105/etc_nginx/letsencrypt/<server_remote>/privkey.pem' nmodel = "NTQAI/wav2vec2-large-japanese" numsmp = 16000 txtres = "" evtstt = asyncio.Event() # EVT sttprc = SttPrc(numsmp, nmodel) appweb = web.Application() appweb.add_routes([web.post(adr001, res001)]) appweb.add_routes([web.options(adr001, opt001)]) appweb.add_routes([web.get(adr002, res002)]) sslcon = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) sslcon.load_cert_chain(pthcrt, pthkey) web.run_app(appweb, port=prttgt, ssl_context=sslcon)
- ◯
- バックエンド:クライアント(取得:テキスト←サーバ)
- ・
- appstt/bin/sttclt.py
import asyncio import aiohttp async def req001(adr001): async with aiohttp.ClientSession() as sssweb: async with sssweb.get(adr001) as datres: dicres = await datres.json() return dicres adr001 = "https://<server_remote>:8082/gettxt" dicres = asyncio.run(req001(adr001)) print(dicres["txtres"])
- ◯
- フロントエンド:クライアント(送付:サウンド→サーバ)
- ・
- appstt/web/sttclt.htm
<html lang="ja"> <head> <meta charset="UTF-8"> </head> <body> <button onclick="req001()">sttclt</button> <script> const adr001 = 'https://<server_remote>:8082/putsnd' const mscrec = 5000 const req001 = async () => { const stream = await navigator.mediaDevices.getUserMedia({audio: true}); const devrec = new MediaRecorder(stream); devrec.start(); let cnksnd = []; devrec.ondataavailable = e => { cnksnd.push(e.data); }; devrec.onstop = async () => { const blbsnd = new Blob(cnksnd, {type: 'audio/webm'}); await fetch(adr001, { method: 'POST', headers: { 'Content-Type': 'audio/webm' }, body: blbsnd }); cnksnd = []; stream.getTracks().forEach(itmtrk => itmtrk.stop()); }; setTimeout(() => { if (devrec.state === 'recording') { devrec.stop(); } }, mscrec); } </script> </body> </html>
利用
コンテナを起動します:
$ docker run -it --rm \ -p 8082:8082 \ --gpus all \ --net net101 -h <server_remote> \ -v ${HOME}/system:/system \ -v /exp001/hgf:/root/.cache/huggingface \ --name cnt104 ubuntu:vir104
コンテナ上で、サーバを起動し〜クライアントを実行します:
# サーバ $ python /system/vol104/bin/sttsrv.py & # クライアント $ python /system/vol104/bin/sttclt.py
ローカルのウェブブラウザで、クライアントを実行します:
> appstt/web/sttclt.htm