MkItYs

MkItYs > AI・交渉・物語の自動生成 > 

images

音声認識のエンジン/サーバ/クライアントを作る:Python (Transformers, aiohttp), Javascript (MediaRecorder)

images

音声認識のエンジンと、そのサーバ〜クライアントを作ってみます。

関連


サーバ証明書を取得〜更新する(ワイルドカード証明書、ネームサーバ経由):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