Closed8

音声から感情や音声イベントなども抽出できるASRモデル「SenseVoice」を試す

kun432kun432

CosyVoice2を試したのだが、

https://zenn.dev/kun432/scraps/87f26758ef4606

その時に同じ開発元からSenseVoiceというのがリリースされていることを知った。

GitHubレポジトリ

https://github.com/FunAudioLLM/SenseVoice

日本語READMEがある

https://github.com/FunAudioLLM/SenseVoice/tree/main/README_ja.md

SenseVoiceは、音声認識(ASR)、言語識別(LID)、音声感情認識(SER)、および音響イベント分類(AEC)または音響イベント検出(AED)を含む音声理解能力を備えた音声基盤モデルです。本プロジェクトでは、SenseVoiceモデルの紹介と、複数のタスクテストセットでのベンチマーク、およびモデルの体験に必要な環境のインストールと推論方法を提供します。

コア機能 🎯

SenseVoiceは、高精度な多言語音声認識、感情認識、および音声イベント検出に焦点を当てています。

  • 多言語認識: 40万時間以上のデータを使用してトレーニングされ、50以上の言語をサポートし、認識性能はWhisperモデルを上回ります。
  • リッチテキスト認識:
    • 優れた感情認識能力を持ち、テストデータで現在の最良の感情認識モデルの効果を達成および上回ります。
    • 音声イベント検出能力を提供し、音楽、拍手、笑い声、泣き声、咳、くしゃみなどのさまざまな一般的な人間とコンピュータのインタラクションイベントを検出します。
  • 効率的な推論: SenseVoice-Smallモデルは非自己回帰エンドツーエンドフレームワークを採用しており、推論遅延が非常に低く、10秒の音声の推論に70msしかかかりません。Whisper-Largeより15倍高速です。
  • 簡単な微調整: 便利な微調整スクリプトと戦略を提供し、ユーザーがビジネスシナリオに応じてロングテールサンプルの問題を簡単に解決できるようにします。
  • サービス展開: マルチコンカレントリクエストをサポートする完全なサービス展開パイプラインを提供し、クライアントサイドの言語にはPython、C++、HTML、Java、C#などがあります。

で、いまいちよくわからなかったのだが、SenseVoiceのライセンスを追っかけていくと、FunASRというプロジェクトにたどり着く。

https://github.com/modelscope/FunASR

FunASRは、ASRに関する様々なタスク向けのツールキットで、それぞれのタスク向けに様々な事前学習モデルを提供している。

FunASR:基礎的なエンドツーエンド音声認識ツールキット

FunASR は、音声認識における学術研究と産業応用の橋渡しを目指しています。産業グレードの音声認識モデルの学習およびファインチューニングをサポートすることで、研究者や開発者は音声認識モデルの研究とプロダクション展開をより容易に行うことができ、音声認識エコシステムの発展を促進します。ASR for Fun!

ハイライト

  • FunASR は、音声認識(ASR)、音声活動検出(VAD)、句読点復元、言語モデル、話者認証、話者ダイアライゼーション、マルチトーカーASRなどの多様な機能を提供する基礎的な音声認識ツールキットです。FunASR は推論や事前学習モデルのファインチューニングを支援する便利なスクリプトやチュートリアルを提供しています。
  • 我々は ModelScopehuggingface 上で、学術・産業向けの事前学習済みモデルを多数公開しています。詳細は Model Zoo をご覧ください。代表的な非自己回帰型エンドツーエンド音声認識モデルである Paraformer-large は、高精度・高効率・容易なデプロイという特長があり、音声認識サービスの迅速な構築を支援します。サービスのデプロイに関する詳細は デプロイドキュメント を参照してください。

モデルズー(Model Zoo)

FunASR は、産業データに基づいた多数の事前学習済みモデルをオープンソースで公開しています。FunASR のモデルは モデル使用許諾契約 に従って、自由に使用・コピー・改変・共有することができます。以下は代表的なモデルの一覧です。その他のモデルについては Model Zoo をご参照ください。

(注:⭐ は ModelScope モデルズー、🤗 は Huggingface モデルズー、🍀 は OpenAI モデルズーを示します)

モデル名 タスク詳細 学習データ パラメータ数
SenseVoiceSmall
( 🤗)
音声認識(ASR)、数値変換(ITN)、言語識別(LID)、感情認識(SER)、イベント検出(AED)など複数の音声理解機能を備え、中国語・広東語・英語・日本語・韓国語などに対応 300,000 時間 234M
paraformer-zh
( 🤗)
音声認識(非ストリーミング)、タイムスタンプ付き 60,000 時間(中国語) 220M
paraformer-zh-streaming
( 🤗)
音声認識(ストリーミング) 60,000 時間(中国語) 220M
paraformer-en
( 🤗)
音声認識(非ストリーミング)、タイムスタンプなし 50,000 時間(英語) 220M
conformer-en
( 🤗)
音声認識(非ストリーミング) 50,000 時間(英語) 220M
ct-punc
( 🤗)
句読点復元 1億文(中国語・英語) 290M
fsmn-vad
( 🤗)
音声活動検出(VAD) 5,000 時間(中国語・英語) 0.4M
fsmn-kws
()
キーワードスポッティング(ストリーミング) 5,000 時間(中国語) 0.7M
fa-zh
( 🤗)
タイムスタンプ予測 5,000 時間(中国語) 38M
cam++
( 🤗)
話者認証/話者ダイアライゼーション 5,000 時間 7.2M
Whisper-large-v3
( 🍀)
音声認識(タイムスタンプあり、非ストリーミング) 多言語 1,550M
Whisper-large-v3-turbo
( 🍀)
音声認識(タイムスタンプあり、非ストリーミング) 多言語 809M
Qwen-Audio
( 🤗)
音声テキストマルチモーダルモデル(事前学習) 多言語 8B
Qwen-Audio-Chat
( 🤗)
音声テキストマルチモーダルモデル(チャット) 多言語 8B
emotion2vec+large
( 🤗)
音声感情認識 40,000 時間 300M

これを踏まえると、で、SenseVoiceはFunASRプロジェクトが提供しているプロジェクト・モデルの1つ、という感じに見える。

よって、ライセンスはFunASRの元となり、コードはMITだが、事前学習モデルは独自ライセンスになっている。

ライセンス

本プロジェクトは MIT ライセンス のもとでライセンスされています。FunASR には、他のリポジトリから改変されたコードや、他のオープンソースライセンスの下にある各種サードパーティコンポーネントも含まれています。

事前学習モデルの使用には、モデルライセンス が適用されます。

https://github.com/modelscope/FunASR/tree/main/MODEL_LICENSE

kun432kun432

ただ、以下を見る限り、

https://funaudiollm.github.io/

  • FunAudioLLMというLLMを使った会話向けのASR/TTSモデルのプロジェクト
    • ASRがSenseVoice
    • TTSがCosyVoice

という感じもあってややこしい。FunAudioLLMもFunASR、どちらもアリババのプロジェクトではあるっぽいのだけど。

kun432kun432

まずはColaboratory T4で。

レポジトリクローン

!git clone https://github.com/FunAudioLLM/SenseVoice
%cd SenseVoice

パッケージインストール

!pip install -r requirements.txt

で、ここで一旦ランタイムを変更する。requirements.txtでnumpyをダウングレードすることになるが、ランタイムを再起動しないと反映されず、モデルロード時にエラーになってしまうため。

ランタイム再起動後は移動したディレクトリが元に戻ってしまうので再度移動。

%cd SenseVoice

モデルロード。VADなども適用されているみたい。

from funasr import AutoModel
from funasr.utils.postprocess_utils import rich_transcription_postprocess

model_dir = "iic/SenseVoiceSmall"


model = AutoModel(
    model=model_dir,
    trust_remote_code=True,
    remote_code="./model.py",    
    vad_model="fsmn-vad",
    vad_kwargs={"max_single_segment_time": 30000},
    device="cuda:0",
)

モデルロード後のVRAM消費は1GBぐらい。

出力
Sun May 25 14:11:28 2025       
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.15              Driver Version: 550.54.15      CUDA Version: 12.4     |
|-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  Tesla T4                       Off |   00000000:00:04.0 Off |                    0 |
| N/A   72C    P0             32W /   70W |    1068MiB /  15360MiB |      0%      Default |
|                                         |                        |                  N/A |
+-----------------------------------------+------------------------+----------------------+

では早速推論。推論のデモ用WAVファイルが用意されているが、これはモデルをインストールしたディレクトリにある。このディレクトリを確認。

print(model.model_path)
出力
/root/.cache/modelscope/hub/models/iic/SenseVoiceSmall
!ls /root/.cache/modelscope/hub/models/iic/SenseVoiceSmall
出力
am.mvn				      config.yaml  model.pt
chn_jpn_yue_eng_ko_spectok.bpe.model  example	   README.md
configuration.json		      fig	   tokens.json

上記の exampleディレクトリの中に各言語ごとのmp3ファイルが用意されている。

!ls /root/.cache/modelscope/hub/models/iic/SenseVoiceSmall/example
出力
en.mp3	ja.mp3	ko.mp3	yue.mp3  zh.mp3

Colaboratory上で確認するには以下のようにすれば良い。

from IPython.display import Audio

for lang in ["zh", "en", "yue", "ja", "ko"]:
    print(f"===== {lang} =====")
    display(Audio(f"{model.model_path}/example/{lang}.mp3"))

ではまるっと推論してみる。

for lang in ["zh", "en", "yue", "ja", "ko"]:
    print(f"===== {lang} =====")

    res = model.generate(
        input=f"{model.model_path}/example/{lang}.mp3",
        cache={},
        language=lang,
        use_itn=True,
        batch_size_s=60,
        merge_vad=True,  #
        merge_length_s=15,
    )
    text = rich_transcription_postprocess(res[0]["text"])
    print("\nASR結果: ", text)
出力
===== zh =====
rtf_avg: 0.009: 100%|██████████| 1/1 [00:00<00:00, 19.32it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
{'load_data': '0.000', 'extract_feat': '0.008', 'forward': '0.078', 'batch_size': '1', 'rtf': '0.015'}, : 100%|██████████| 1/1 [00:00<00:00, 12.73it/s]
rtf_avg: 0.015: 100%|██████████| 1/1 [00:00<00:00, 11.88it/s]
rtf_avg: 0.016, time_speech:  5.616, time_escape: 0.091: 100%|██████████| 1/1 [00:00<00:00,  9.61it/s]

ASR結果:  开放时间早上9点至下午5点。
===== en =====
rtf_avg: 0.006: 100%|██████████| 1/1 [00:00<00:00, 23.05it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
{'load_data': '0.000', 'extract_feat': '0.009', 'forward': '0.082', 'batch_size': '1', 'rtf': '0.013'}, : 100%|██████████| 1/1 [00:00<00:00, 12.10it/s]
rtf_avg: 0.013: 100%|██████████| 1/1 [00:00<00:00, 11.23it/s]
rtf_avg: 0.014, time_speech:  7.176, time_escape: 0.098: 100%|██████████| 1/1 [00:00<00:00,  8.87it/s]

ASR結果:  The tribal chieftain called for the boy and presented him with 50 pieces of gold.
===== yue =====
rtf_avg: 0.006: 100%|██████████| 1/1 [00:00<00:00, 26.99it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
{'load_data': '0.000', 'extract_feat': '0.008', 'forward': '0.077', 'batch_size': '1', 'rtf': '0.020'}, : 100%|██████████| 1/1 [00:00<00:00, 12.79it/s]
rtf_avg: 0.020: 100%|██████████| 1/1 [00:00<00:00, 11.56it/s]
rtf_avg: 0.018, time_speech:  5.184, time_escape: 0.093: 100%|██████████| 1/1 [00:00<00:00,  9.57it/s]

ASR結果:  呢几个字都表达唔到,我想讲嘅意思。
===== ja =====
rtf_avg: 0.005: 100%|██████████| 1/1 [00:00<00:00, 23.62it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
{'load_data': '0.000', 'extract_feat': '0.009', 'forward': '0.082', 'batch_size': '1', 'rtf': '0.013'}, : 100%|██████████| 1/1 [00:00<00:00, 12.04it/s]
rtf_avg: 0.013: 100%|██████████| 1/1 [00:00<00:00, 11.12it/s]
rtf_avg: 0.013, time_speech:  7.224, time_escape: 0.097: 100%|██████████| 1/1 [00:00<00:00,  8.91it/s]

ASR結果:  うち の 中学 は 弁当 制 で 持っ ていけない 場 合は、50 円 の 学校 販売 の パン を 買う。
===== ko =====
rtf_avg: 0.006: 100%|██████████| 1/1 [00:00<00:00, 31.39it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
  0%|          | 0/1 [00:00<?, ?it/s]
{'load_data': '0.000', 'extract_feat': '0.006', 'forward': '0.077', 'batch_size': '1', 'rtf': '0.021'}, : 100%|██████████| 1/1 [00:00<00:00, 12.96it/s]
rtf_avg: 0.021: 100%|██████████| 1/1 [00:00<00:00, 11.92it/s]
rtf_avg: 0.020, time_speech:  4.644, time_escape: 0.091: 100%|██████████| 1/1 [00:00<00:00, 10.04it/s]
ASR結果:  조 금만 생각 을 하 면서 살 면 훨씬 편할 거야.

推論時のログも出力されて少し見にくいので結果部分だけを抜き出すとこうなっている。

出力
===== zh =====
ASR結果:  开放时间早上9点至下午5点。
===== en =====
ASR結果:  The tribal chieftain called for the boy and presented him with 50 pieces of gold.
===== yue =====
ASR結果:  呢几个字都表达唔到,我想讲嘅意思。
===== ja =====
ASR結果:  うち の 中学 は 弁当 制 で 持っ ていけない 場 合は、50 円 の 学校 販売 の パン を 買う。
===== ko =====
ASR結果:  조 금만 생각 을 하 면서 살 면 훨씬 편할 거야.

で実際のレスポンスには以下のような結果が入っている。

res
[{'key': 'ko',
  'text': '<|ko|><|NEUTRAL|><|Speech|><|withitn|>조 금만 생각 을 하 면서 살 면 훨씬 편할 거야.'}]

textに、言語・感情・文字起こし結果が含まれているのがわかる。

kun432kun432

次に感情の抽出。上記の結果では感情はNEUTRALとなっていたが、他にどのような感情を抽出できるのかを見てみる。

以下のデータセットを使わせていただく。

https://sites.google.com/site/shinnosuketakamichi/research-topics/jvnv_corpus

JVNV: a Japanese emotional speech corpus with both verbal content and nonverbal vocalizations (言語音声と非言語音声を持つ日本語感情音声コーパス)

JVNV (Japanese emotional speech corpus with Verbal content and Nonverbal Vocalizations) は,言語音声と非言語音声から成る日本語感情音声コーパスです.非言語音声には,感情を表す笑い声や泣き声のような,日常会話で使用されるものが含まれます.コーパスには,4 話者・6 感情(怒り,嫌悪,恐れ,幸せ,悲しみ,驚き)による 3.94 時間の音声が含まれます.各発話は,指定された感情を表現しており,少なくとも1つの非言語音声を含みます.音声と書き起こしテキストに加え,本コーパスには各発話の非言語発話の時間区間が含まれます.

データセットを取得して解凍

!wget https://ss-takashi.sakura.ne.jp/corpus/jvnv/jvnv_ver1.zip
!unzip jvnv_ver1.zip

ディレクトリの詳細は上記に含まれるreadme.mdを参照してもらうとして、ざっくりだと中身はこんな感じで、感情ごとに分けられたディレクトリ内にそれぞれのWAVファイルが入っている。

jvnv_v1/F1
├── anger
│   ├── free
│   │   ├── F1_anger_free_01.wav
│   │   ├── F1_anger_free_02.wav
│   │   ├── F1_anger_free_03.wav
(snip)
│   └── regular
│       ├── F1_anger_regular_01.wav
│       ├── F1_anger_regular_02.wav
│       ├── F1_anger_regular_03.wav
(snip)
├── disgust
│   ├── free
│   │   ├── F1_disgust_free_01.wav
│   │   ├── F1_disgust_free_02.wav
│   │   ├── F1_disgust_free_03.wav
(snip)
│   └── regular
│       ├── F1_disgust_regular_01.wav
│       ├── F1_disgust_regular_02.wav
│       ├── F1_disgust_regular_03.wav
(snip)
├── fear
│   ├── free
│   │   ├── F1_fear_free_01.wav
│   │   ├── F1_fear_free_02.wav
│   │   ├── F1_fear_free_03.wav
(snip)
│   └── regular
│       ├── F1_fear_regular_01.wav
│       ├── F1_fear_regular_02.wav
│       ├── F1_fear_regular_03.wav
(snip)
├── happy
│   ├── free
│   │   ├── F1_happy_free_01.wav
│   │   ├── F1_happy_free_02.wav
│   │   ├── F1_happy_free_03.wav
(snip)
│   └── regular
│       ├── F1_happy_regular_01.wav
│       ├── F1_happy_regular_02.wav
│       ├── F1_happy_regular_03.wav
(snip)
├── sad
│   ├── free
│   │   ├── F1_sad_free_01.wav
│   │   ├── F1_sad_free_02.wav
│   │   ├── F1_sad_free_03.wav
(snip)
│   └── regular
│       ├── F1_sad_regular_01.wav
│       ├── F1_sad_regular_02.wav
│       ├── F1_sad_regular_03.wav
(snip)
└── surprise
    ├── free
    │   ├── F1_surprise_free_01.wav
    │   ├── F1_surprise_free_02.wav
    │   ├── F1_surprise_free_03.wav
(snip)
    └── regular
        ├── F1_surprise_regular_01.wav
        ├── F1_surprise_regular_02.wav
        ├── F1_surprise_regular_03.wav
(snip)

これを使って感情抽出も含めた文字起こしをやってみる。とりあえず1つお試し。

res = model.generate(
    input="jvnv_v1/F1/anger/free/F1_anger_free_01.wav",
    cache={},
    language="ja",
    use_itn=True,
    batch_size_s=60,
    merge_vad=True,  #
    merge_length_s=15,
)
text = rich_transcription_postprocess(res[0]["text"])
print("\nASR結果: ", text)
出力
ASR結果:  あんた は 誰 だと 思っ て いる の 生意気 な 態度 は 許せ ない。

レスポンスを見てみる。

res
出力
[{'key': 'F1_anger_free_01',
  'text': '<|ja|><|EMO_UNKNOWN|><|Speech|><|withitn|>あんた は 誰 だと 思っ て いる の 生意気 な 態度 は 許せ ない。'}]

感情の部分がEMO_UNKNOWNになっている。色々試してみても、ほぼすべてEMO_UNKNOWNとなってしまう・・・

GitHubのIssueを見ると以下を見つけた。

https://github.com/FunAudioLLM/SenseVoice/issues/31

どうやらban_emo_unkというパラメータを有効にすると、EMO_UNKNOWNが禁止され、何かしらの感情が割り当てられる様子。

ということで上記データセットの各感情ごとのファイルをまるっと推論させてみる。

for file in [
    "jvnv_v1/F1/anger/free/F1_anger_free_01.wav",
    "jvnv_v1/F1/disgust/free/F1_disgust_free_01.wav",
    "jvnv_v1/F1/happy/free/F1_happy_free_01.wav",
    "jvnv_v1/F1/fear/free/F1_fear_free_01.wav",
    "jvnv_v1/F1/sad/free/F1_sad_free_01.wav",
    "jvnv_v1/F1/surprise/free/F1_surprise_free_01.wav",
]:
    print(f"##### {file} #####")
    res = model.generate(
        input=file,
        cache={},
        language="ja",
        use_itn=True,
        batch_size_s=60,
        merge_vad=True,  #
        merge_length_s=15,
        ban_emo_unk=True,
    )
    text = rich_transcription_postprocess(res[0]["text"])
    print("\nASR結果: ", text)
    print("レスポンス: ", res)

結果。推論中の余計なログは削除している。

出力
##### jvnv_v1/F1/anger/free/F1_anger_free_01.wav #####
ASR結果:  あんた は 誰 だと 思っ て いる の 生意気 な 態度 は 許せ ない。😡
レスポンス:  [{'key': 'F1_anger_free_01', 'text': '<|ja|><|ANGRY|><|Speech|><|withitn|>あんた は 誰 だと 思っ て いる の 生意気 な 態度 は 許せ ない。'}]

##### jvnv_v1/F1/disgust/free/F1_disgust_free_01.wav #####
ASR結果:  げ 彼女の下品な言葉遣いは本当に耐えられない。😡
レスポンス:  [{'key': 'F1_disgust_free_01', 'text': '<|ja|><|ANGRY|><|Speech|><|withitn|>げ 彼女の下品な言葉遣いは本当に耐えられない。'}]

##### jvnv_v1/F1/happy/free/F1_happy_free_01.wav #####
ASR結果:  私たちは今、お金に困ることなく生活できてとても幸せです ヤホー!😊
レスポンス:  [{'key': 'F1_happy_free_01', 'text': '<|ja|><|HAPPY|><|Speech|><|withitn|>私たちは今、お金に困ることなく生活できてとても幸せです ヤホー!'}]

##### jvnv_v1/F1/fear/free/F1_fear_free_01.wav #####
ASR結果:  テロの疑いがある荷物が見つかったというニュースを聞いて、 空港で爆発が起きてしまうのではないかと恐ろらしく思った。😮
レスポンス:  [{'key': 'F1_fear_free_01', 'text': '<|ja|><|SURPRISED|><|Speech|><|withitn|>テロの疑いがある荷物が見つかったというニュースを聞いて、 空港で爆発が起きてしまうのではないかと恐ろらしく思った。'}]

##### jvnv_v1/F1/sad/free/F1_sad_free_01.wav #####
ASR結果:  最近 の 私 の 人生 は 何 も かもが 停滞 して いて どうし よ うもなく 悲しい。😔
レスポンス:  [{'key': 'F1_sad_free_01', 'text': '<|ja|><|SAD|><|Speech|><|withitn|>最近 の 私 の 人生 は 何 も かもが 停滞 して いて どうし よ うもなく 悲しい。'}]

##### jvnv_v1/F1/surprise/free/F1_surprise_free_01.wav #####
ASR結果:  え、週刊 誌 に 掲載 され たん で すか 信じ られ ない。😮
レスポンス:  [{'key': 'F1_surprise_free_01', 'text': '<|ja|><|SURPRISED|><|Speech|><|withitn|>え、週刊 誌 に 掲載 され たん で すか 信じ られ ない。'}]

感情が絵文字に置き換えられているのがわかる。

感情というか推論結果を絵文字に置き換えているのは、FunASRパッケージのrich_transcription_postprocessで、多分この辺だと思う。SenseVoiceがこの全部の感情を学習しているかはわからないけど。

https://github.com/modelscope/FunASR/blob/8cd0d4aab71939e56539ff6c1027b37ad4324433/funasr/utils/postprocess_utils.py#L336-L366

あと今回のデータセットには含まれていないかもだが、上記の絵文字にも含まれている通り、非言語イベントも認識されるみたい。

音声イベント検出能力を提供し、音楽、拍手、笑い声、泣き声、咳、くしゃみなどのさまざまな一般的な人間とコンピュータのインタラクションイベントを検出します。

kun432kun432

ちなみに、今回短い文章しか渡してなくて、長い文章でどうなるかはわからないけど、試した限りでは推論はめちゃめちゃ速い。平均300msぐらいで返って来ているように思える。

kun432kun432

その他

今回試していないが以下のようなこともできるみたい

  • WebUI
  • APIサーバ
  • ファインチューニング

また、SenseVoiceを使用したプロジェクトも紹介されていて、色々使い勝手が良さそう。

注目すべきサードパーティの取り組み

  • Triton (GPU) デプロイメントのベストプラクティス: Triton + TensorRT を使用し、FP32 でテスト。V100 GPU で加速比 526 を達成。FP16 のサポートは進行中です。リポジトリ
  • Sherpa-onnx デプロイメントのベストプラクティス: SenseVoice を10種類のプログラミング言語(C++, C, Python, C#, Go, Swift, Kotlin, Java, JavaScript, Dart)で使用可能。また、iOS, Android, Raspberry Pi などのプラットフォームでも SenseVoice をデプロイできます。リポジトリ
  • SenseVoice.cpp GGMLに基づいて純粋なC/C++でSenseVoiceを推測し、3ビット、4ビット、5ビット、8ビット量子化などをサポートし、サードパーティの依存関係はありません。
  • streaming-sensevoice: ストリーム型SenseVoiceは、チャンク(chunk)方式で推論を行います。擬似ストリーミング処理を実現するために、一部の精度を犠牲にして切り捨て注意機構(truncated attention)を採用しています。さらに、この技術はCTCプレフィックスビームサーチ(CTC prefix beam search)とホットワード強化機能もサポートしています。
  • OmniSenseVoice は、超高速推論とバッチ処理のために最適化されています。
  • SenseVoice Hotword: ニューラルネットワークホットワード強化,WeNetにおけるCPPNベースのニューラルネットワークホットワード強化のオープンソース
kun432kun432

まとめ

今回精度についてはあまり考慮していないのだけども、とりあえず推論が非常に速い&感情抽出ができるのは良いと感じるし、いろいろな拡張プロジェクトもあって、上手く使えばこれだけでいろいろできそうな気がした。

モデルライセンスは独自なのでそこが問題なければ。

kun432kun432

SenseVoice.cpp

https://github.com/lovemefan/SenseVoice.cpp

1. 機能

  1. ggml をベースにしており、他のサードパーティライブラリに依存せず、エッジ展開にコミットしています。
  2. 特徴抽出はkaldi-native-fbankライブラリを参考にしており、マルチスレッド特徴抽出をサポートしています。
  3. Flash attentionデコーディングをサポートしています。
  4. Q3、Q4、Q5、Q6、Q8の量子化をサポートしています。

1.1 バックエンドサポート

より多くのバックエンドをサポートしています。理論上、ggmlは以下のバックエンドをサポートしており、今後の適応は段階的に行われます。貢献は歓迎されます。

バックエンド デバイス サポート
CPU すべて
Metal Apple Silicon
BLAS すべて
CUDA Nvidia GPU
Vulkan GPU
Cann Ascend NPU 未テスト
BLIS すべて
SYCL Intel および Nvidia GPU
MUSA Moore Threads GPU
hipBLAS AMD GPU

ローカルのMac(M2 Pro)でやってみる。

レポジトリクローン

git clone https://github.com/lovemefan/SenseVoice.cpp && cd SenseVoice.cpp
git submodule sync && git submodule update --init --recursive

ビルド

mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release .. && make -j 8

binの下にあるsense-voice-mainコマンドを使う。まずUsage。

./bin/sense-voice-main --help
出力
usage: ./bin/sense-voice-main [options] file0.wav file1.wav ...

options:
  -h,        --help              [default] show this help message and exit
  -t N,      --threads N         [4      ] number of threads to use during computation
  -p N,      --processors N      [1      ] number of processors to use during computation
  -ot N,     --offset-t N        [0      ] time offset in milliseconds
  -on N,     --offset-n N        [0      ] segment index offset
  -d  N,     --duration N        [0      ] duration of audio to process in milliseconds
  -mc N,     --max-context N     [-1     ] maximum number of text context tokens to store
  -ml N,     --max-len N         [0      ] maximum segment length in characters
  -sow,      --split-on-word     [false  ] split on word rather than on token
  -bo N,     --best-of N         [5      ] number of best candidates to keep
  -bs N,     --beam-size N       [5      ] beam size for beam search
  -ac N,     --audio-ctx N       [0      ] audio context size (0 - all)
  -wt N,     --word-thold N      [0.01   ] word timestamp probability threshold
  -et N,     --entropy-thold N   [2.40   ] entropy threshold for decoder fail
  -lpt N,    --logprob-thold N   [-1.00  ] log probability threshold for decoder fail
  -tp,       --temperature N     [0.00   ] The sampling temperature, between 0 and 1
  -tpi,      --temperature-inc N [0.20   ] The increment of temperature, between 0 and 1
  -debug,    --debug-mode        [false  ] enable debug mode (eg. dump log_mel)
  -di,       --diarize           [false  ] stereo audio diarization
  -tdrz,     --tinydiarize       [false  ] enable tinydiarize (requires a tdrz model)
  -nf,       --no-fallback       [false  ] do not use temperature fallback while decoding
  -otxt,     --output-txt        [false  ] output result in a text file
  -osrt,     --output-srt        [false  ] output result in a srt file
  -ocsv,     --output-csv        [false  ] output result in a CSV file
  -oj,       --output-json       [false  ] output result in a JSON file
  -ojf,      --output-json-full  [false  ] include more information in the JSON file
  -of FNAME, --output-file FNAME [       ] output file path (without file extension)
  -np,       --no-prints         [false  ] do not print anything other than the results
  -ps,       --print-special     [false  ] print special tokens
  -pc,       --print-colors      [false  ] print colors
  -pp,       --print-progress    [false  ] print progress
  -nt,       --no-timestamps     [false  ] do not print timestamps
  -l LANG,   --language LANG     [auto   ] spoken language ('auto' for auto-detect), support [`zh`, `en`, `yue`, `ja`, `ko`
             --prompt PROMPT     [       ] initial prompt (max n_text_ctx/2 tokens)
  -m FNAME,  --model FNAME       [models/ggml-base.en.bin] model path
  -f FNAME,  --file FNAME        [       ] input WAV file path
             --min_speech_duration_ms   [250    ] min_speech_duration_ms
             --max_speech_duration_ms   [15000  ] log probability threshold for decoder fail
             --min_silence_duration_ms   [100    ] min_silence_duration_ms
             --speech_pad_ms     [30     ] speech_pad_ms
  -oved D,   --ov-e-device DNAME [CPU    ] the OpenVINO device used for encode inference
  -ls,       --log-score         [false  ] log best decoder scores of tokens
  -ng,       --no-gpu            [false  ] disable GPU
  -fa,       --flash-attn        [false  ] flash attention
  -itn,      --use-itn           [false  ] use itn
  -prefix,      --use-prefix           [false  ] use itn

モデルをダウンロード。READMEでは以下のようにレポジトリクローンしているが、中にはGGUFが10ファイル(ざっくり2GB強)あるので、個別にダウンロードしたほうが良さそうに思った。今回はREADMEに従う。

git lfs install
git clone https://huggingface.co/lovemefan/sense-voice-gguf.git

ではWAVファイルから文字起こししてみる。自分が過去に開催した勉強会のYouTube動画から冒頭5分程度の音声を抜き出したをサンプルとして使う。

https://www.youtube.com/watch?v=Yl2kR6zLRY8

量子化はFP32にした。

./bin/sense-voice-main \
    -m sense-voice-gguf/sense-voice-small-fp32.gguf \
    -ng \
    -t 4 \
    ./voice_lunch_jp_5min.wav

結果

出力
[0.90-6.14] はいじゃあ始めますちょっとまだ来られてない方もいらっしゃるんですけど
[6.98-7.87] こいスランチ
[8.10-9.25] デピー始めます
[9.66-10.88] 那さん嗯对
[11.55-12.48] はい
[13.92-23.14] はい日曜日にお集まりいただきましてありがとうございますと今日は久しぶりにですねオフラインということでと今日はですねスペシャルなゲストをお二人
[23.49-26.11] していただいておりますということで
[26.53-31.74] はいと今日ちょっとトピックに回りますけれどもとボイスローの使用であるレデンリームさんと
[31.90-36.51] あととセールスフォースのとかジダデザインのディレクタである
[36.74-39.17] ブレックベネスさんに来ていただいてます
[40.10-40.67] ということで
[40.90-42.62] 日本に来ていただいてありがとうございました
[44.45-44.83] はい
[45.54-52.19] で今日はちょっとこのお二人にまた後で色ろ々と聞こうというとコーナーがありますのでとそこでまたいろいろと
[52.45-53.41] たいと思います
[53.63-58.37] で今日のアジェンダなんですけどもとちょっと時間過ぎちゃいましたがまず最初にボイスランチジェーピー
[58.59-61.28] についてっていうとことあと会場のところですね少し
[61.95-66.88] ご説明させていただいて一つ目のセッションでまず私の方から
[67.04-73.31] えっとボイスソロー二千二十二年のとき新機能とかですねその辺の話を少しさせていただいて
[73.57-78.53] あとと二つ目のセッションでとグレーデンさんとグルさんにいろいろか茂が
[78.78-81.44] カンバセーショナルデザインですねについて
[81.63-82.69] なんでも聞こうぜみたいな
[82.88-84.83] ところを予定しておりますでその後
[85.82-89.47] 十五時からと十五時で一旦ま終了という形で
[89.66-103.07] させていただいてちょっと一応ボイナピタしか記念撮影は必須ですよねなのでそれだけさせていただいてその後ちょっと一時間ぐらいあの簡単にあのお菓子と飲み物を用意してますので懇親会というのをそのさせていただこうと
[103.30-104.19] 思っています
[104.58-107.04] でとボイストランチジェーピーについてんですけども
[107.55-110.53] とボイスラチはボイス弱ワイとか音性関連ですね
[110.75-119.14] そういった技術にと実際に戦わっている人もしくは興味がある人そちのためのグローバルなコミュニティという形になっていてとボイ一ランチのと
[119.58-126.18] 日本リージョンていう形がボイスランチジェーピーになってますでっと過去もまずっとやってますけどオンラインフラインでいろんな音声
[126.46-132.32] のとデザインだったり技術だったりっていうところで情報とかを共有してみんなで業界盛り上げていこうぜと
[132.48-134.02] いうようなことでやっております
[134.18-135.20] で今日の
[135.42-139.10] とハッシュタグですねえとシ五イ三のジェピーでいろいろてる
[139.46-140.32] 気やしてください
[141.34-144.13] であと会場ですねと今回とグラニカ様の
[144.35-149.15] ご声意で利用させてただいきますありがとうございますでぜひこちらもシェアをお願いしたいですと
[149.34-151.07] で響と配信のところも
[151.30-155.52] いろいろとやっていただいてますって非常に感謝しておりますでちょっと今あのごめんなさい
[155.71-156.42] 脱びた
[156.74-157.60] 今あの
[157.79-163.49] とコロナでえと会場に来られる方とかもあまりいらないということでされてないんですけれどもあの
[163.78-173.63] 通常はなんかこうでアイオティー機器のとかガジェットとかを展示されているようなのでとそういったものがある時こ今度ですねまた体験してみていただければなと思っていますと
[173.95-175.04] いうとこで
[175.20-177.09] あとすみませんえとトイレば
[177.44-180.10] こちらであとタバコ吸れる方はこちらの
[180.35-183.30] ころになってますのでよろしくお願いします
[183.84-186.82] はいということで最初の挨拶はこれで
[188.64-191.26] じゃあまず私の方のセッションから
[192.64-196.29] させていただきますというところでボイスロアプデス二千二十二と
[197.02-202.72] いうところでえっと今年の新知について少しお話をしますと自己紹介です
[202.98-209.15] えと清水と申しますと神戸でインフラのエンジニアをやってますたのでえと普段はクバネテスとかエラベスとかテラホムと
[209.89-212.48] をいじってまして最近ちょっとフリーランスになります
[213.12-221.95] でとちょっと調べてみたらボイスロー一番最初に始めたのが二千十九年の頭ぐらいなので大体四年弱ぐらいですね色々と触ってまして
[222.34-228.86] あとと音声関連のコミュニティとこではとボイスランチエーピー今回のやつですね以外にと
[229.09-231.68] エイジャグアマゾンアレクタージャパンユーザーグループ
[232.42-234.02] とかあとっとボイスローの
[234.24-237.92] と日本語ユーザーグループということでブイエフジェーユージーっていうのをやっています
[239.07-244.10] はいえっと日本コメディの方はフェイスブックの方でとやってますのでもしよろしければ
[244.38-246.18] 見ていただければなと思います
[247.78-252.54] あと2年ぐらい前にですねと技術書店の方でここに今日スタッフで来ていただいてる
[252.77-255.74] 皆さんとですね一緒にあの同時にしでということで
[255.97-266.08] と作ったんですけれどももうこれちょっと二年ぐらい経って中身がだいぶ古くなってしまっているのですにちょっと販売は収用しております今日ちょっと持ってきたかったんですけどすいません忘れてしまいました
[266.40-268.22] はいなのでこういうこともやっています

main: decoder audio use 10.214819 s, rtf is 0.038008.

そういえばオリジナルでこういう長めのファイルからの推論はやってなかった。実際にやってみ他感じからすると、精度的にもう一声頑張ってほしいかなぁ。

なお、--ngをつけているのでCPUでの推論となるが、それでも処理は速いように思う。Apple Siliconの場合はMetalが使えるので、このオプションを外せば良い。あと、感情抽出などもできるはずで、これを出力するには--use-prefix / -prefix を付与する。

./bin/sense-voice-main \
    -m sense-voice-gguf/sense-voice-small-fp32.gguf \
    -t 4 \
    -prefix \
    ./voice_lunch_jp_5min.wav
出力
[0.90-6.14] <|ja|><|NEUTRAL|><|Speech|><|woitn|>はいじゃあ始めますちょっとまだ来られてない方もいらっしゃるんですけど
[6.98-7.87] <|ja|><|NEUTRAL|><|Speech|><|woitn|>こいスランチュ
[8.10-9.25] <|ja|><|NEUTRAL|><|Speech|><|woitn|>デピー始めます
[9.66-10.88] <|zh|><|NEUTRAL|><|Speech|><|woitn|>那さん嗯对
[11.55-12.48] <|ja|><|NEUTRAL|><|Speech|><|woitn|>はい
[13.92-23.14] <|ja|><|HAPPY|><|Speech|><|woitn|>はい日曜日にお集まりいただきましてありがとうございますと今日は久しぶりにですねオフラインということでと今日はですねスペシャルなゲストをお二人
[23.49-26.11] <|ja|><|NEUTRAL|><|Speech|><|woitn|>していただいておりますということで
[26.53-31.74] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>はいと今日ちょっとトピックに回りますけれどもとボイスローの使用であるレデンリームさんと
[31.90-36.51] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>あととセールスフォースのとかジダデザインのディレクタである
[36.74-39.17] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>ブレックベネスさんに来ていただいてます
[40.10-40.67] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>ということで
[40.90-42.62] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>日本に来ていただいてありがとうございました
[44.45-44.83] <|ja|><|NEUTRAL|><|Speech|><|woitn|>はい
[45.54-52.19] <|ja|><|HAPPY|><|Speech|><|woitn|>で今日はちょっとこのお二人にまた後で色ろ々と聞こうというとコーナーがありますのでとそこでまたいろいろと
[52.45-53.41] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>たいと思います
[53.63-58.37] <|ja|><|HAPPY|><|Speech|><|woitn|>で今日のアジェンダなんですけどもとちょっと時間過ぎちゃいましたがまず最初にボイスランチジェーピー
[58.59-61.28] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>についてっていうとことあと会場のところですね少し
[61.95-66.88] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>ご説明させていただいて一つ目のセッションでまず私の方から
[67.04-73.31] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>えっとボイスソロー二千二十二年のとき新機能とかですねその辺の話を少しさせていただいて
[73.57-78.53] <|ja|><|NEUTRAL|><|Speech|><|woitn|>あとと二つ目のセッションでとグレーデンさんとグルさんにいろいろか茂が
[78.78-81.44] <|ja|><|HAPPY|><|Speech|><|woitn|>カンバセーショナルデザインですねについて
[81.63-82.69] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>なんでも聞こうぜみたいな
[82.88-84.83] <|ja|><|NEUTRAL|><|Speech|><|woitn|>ところを予定しておりますでその後
[85.82-89.47] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>十五時からと十五時で一旦ま終了という形で
[89.66-103.07] <|ja|><|HAPPY|><|Speech|><|woitn|>させていただいてちょっと一応ボイナピタしか記念撮影は必須ですよねなのでそれだけさせていただいてその後ちょっと一時間ぐらいあの簡単にあのお菓子と飲み物を用意してますので懇親会というのをそのさせていただこうと
[103.30-104.19] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>思っています
[104.58-107.04] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>でとボイストランチジェーピーについてんですけども
[107.55-110.53] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>とボイスラチはボイス弱ワイとか音性関連ですね
[110.75-119.14] <|ja|><|HAPPY|><|Speech|><|woitn|>そういった技術にと実際に戦わっている人もしくは興味がある人そちのためのグローバルなコミュニティという形になっていてとボイ一ランチのと
[119.58-126.18] <|ja|><|HAPPY|><|Speech|><|woitn|>日本リージョンていう形がボイスランチジェーピーになってますでっと過去もまずっとやってますけどオンラインフラインでいろんな音声
[126.46-132.32] <|ja|><|NEUTRAL|><|Speech|><|woitn|>のとデザインだったり技術だったりっていうところで情報とかを共有してみんなで業界盛り上げていこうぜと
[132.48-134.02] <|ja|><|NEUTRAL|><|Speech|><|woitn|>いうようなことでやっております
[134.18-135.20] <|ja|><|NEUTRAL|><|Speech|><|woitn|>で今日の
[135.42-139.10] <|ja|><|NEUTRAL|><|Speech|><|woitn|>とハッシュタグですねえとシ五イ三のジェピーでいろいろてる
[139.46-140.32] <|ja|><|NEUTRAL|><|Speech|><|woitn|>気やしてください
[141.34-144.13] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>であと会場ですねと今回とグラニカ様の
[144.35-149.15] <|ja|><|NEUTRAL|><|Speech|><|woitn|>ご声意で利用させてただいきますありがとうございますでぜひこちらもシェアをお願いしたいですと
[149.34-151.07] <|ja|><|NEUTRAL|><|Speech|><|woitn|>で響と配信のところも
[151.30-155.52] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>いろいろとやっていただいてますって非常に感謝しておりますでちょっと今あのごめんなさい
[155.71-156.42] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>脱びた
[156.74-157.60] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>今あの
[157.79-163.49] <|ja|><|HAPPY|><|Speech|><|woitn|>とコロナでえと会場に来られる方とかもあまりいらないということでされてないんですけれどもあの
[163.78-173.63] <|ja|><|HAPPY|><|Speech|><|woitn|>通常はなんかこうでアイオティー機器のとかガジェットとかを展示されているようなのでとそういったものがある時こ今度ですねまた体験してみていただければなと思っていますと
[173.95-175.04] <|ja|><|NEUTRAL|><|Speech|><|woitn|>いうとこで
[175.20-177.09] <|ja|><|NEUTRAL|><|Speech|><|woitn|>あとすみませんえとトイレば
[177.44-180.10] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>こちらであとタバコ吸れる方はこちらの
[180.35-183.30] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>ころになってますのでよろしくお願いします
[183.84-186.82] <|ja|><|NEUTRAL|><|Speech|><|woitn|>はいということで最初の挨拶はこれで
[188.64-191.26] <|ja|><|NEUTRAL|><|Speech|><|woitn|>じゃあまず私の方のセッションから
[192.64-196.29] <|ja|><|NEUTRAL|><|Speech|><|woitn|>させていただきますというところでボイスロアプデス二千二十二と
[197.02-202.72] <|ja|><|NEUTRAL|><|Speech|><|woitn|>いうところでえっと今年の新知について少しお話をしますと自己紹介です
[202.98-209.15] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>えと清水と申しますと神戸でインフラのエンジニアをやってますたのでえと普段はクバネテスとかエラベスとかテラホムと
[209.89-212.48] <|ja|><|NEUTRAL|><|Speech|><|woitn|>をいじってまして最近ちょっとフリーランスになります
[213.12-221.95] <|ja|><|HAPPY|><|Speech|><|woitn|>でとちょっと調べてみたらボイスロー一番最初に始めたのが二千十九年の頭ぐらいなので大体四年弱ぐらいですね色々と触ってまして
[222.34-228.86] <|ja|><|NEUTRAL|><|Speech|><|woitn|>あとと音声関連のコミュニティとこではとボイスランチエーピー今回のやつですね以外にと
[229.09-231.68] <|ja|><|NEUTRAL|><|Speech|><|woitn|>エイジャグアマゾンアレクタージャパンユーザーグループ
[232.42-234.02] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>とかあとっとボイスローの
[234.24-237.92] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>と日本語ユーザーグループということでブイエフジェーユージーっていうのをやっています
[239.07-244.10] <|ja|><|HAPPY|><|Speech|><|woitn|>はいえっと日本コメディの方はフェイスブックの方でとやってますのでもしよろしければ
[244.38-246.18] <|ja|><|NEUTRAL|><|Speech|><|woitn|>見ていただければなと思います
[247.78-252.54] <|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>あと2年ぐらい前にですねと技術書店の方でここに今日スタッフで来ていただいてる
[252.77-255.74] <|ja|><|HAPPY|><|Speech|><|woitn|>皆さんとですね一緒にあの同時にしでということで
[255.97-266.08] <|ja|><|HAPPY|><|Speech|><|woitn|>と作ったんですけれどももうこれちょっと二年ぐらい経って中身がだいぶ古くなってしまっているのですにちょっと販売は収用しております今日ちょっと持ってきたかったんですけどすいません忘れてしまいました
[266.40-268.22] <|ja|><|NEUTRAL|><|Speech|><|woitn|>はいなのでこういうこともやっています

main: decoder audio use 3.400275 s, rtf is 0.012652.

まあ感情がついている時もあるという感じかな。

マイクからのリアルタイム文字起こしはsense-voice-streamを使う。libsdl2-devが必要とあるが、Macの場合は多分sdl2をHomebrewでインストールすればよいと思う。自分はもう入れていた。あと、sense-voice-mainではVADがデフォルト有効になっていたが、こちらはデフォルト無効なので--use-vadで有効にできる。

./bin/sense-voice-stream \
    -m sense-voice-gguf/sense-voice-small-fp32.gguf \
    --use-vad

こんな感じでリアルタイムで文字起こしされる。レスポンスはいい感じに思える。

出力
main: using VAD, will print when mute time longer than 1000 ms or nomute time longer than 8000 ms

[3.60-4.30]こんにちは
[8.00-9.80]センスボイスのテストです
[14.20-16.60]正しく文字起こしてきているでしょうか

こちらも--use-prefixをつけると感情抽出結果が追加される。

./bin/sense-voice-stream \
    -m sense-voice-gguf/sense-voice-small-fp32.gguf \
    --use-vad \
    --use-prefix \
    --language ja
出力
main: using VAD, will print when mute time longer than 1000 ms or nomute time longer than 8000 ms

[1.10-3.10]<|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>こんにちは
[5.60-8.10]<|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>センス講師のテストです
[11.70-13.90]<|ja|><|NEUTRAL|><|Speech|><|woitn|>正しく文字起こしてきているでしょうか
[16.00-18.90]<|ja|><|EMO_UNKNOWN|><|Speech|><|woitn|>感情が全然乗っかってこないんじゃないか

のだが、なかなか感情が抽出されないね・・・

このスクラップは3ヶ月前にクローズされました