LMDeployによる最適化で高速になった「MiraTTS」をWindowsで推論(測度計測)する

初めに

高速に推論ができるらしい MiraTTSを触ってみます。

uvでWindowsに対応したリポジトリは以下で公開をしています

github.com

開発環境

項目 バージョン
OS Windows 11
CUDA 12.x (v13.0も動作確認済み)
Python 3.11
パッケージマネージャ uv

環境構築

Windowsで環境構築をする場合はnvidia-nccl-cu12がWindows非対応(Linuxのみ)のため、注意が必要です.

リポジトリをクローンします

git clone https://github.com/ayutaz/MiraTTS.git
cd MiraTTS

pyproject.tomlは以下のように定義しています

[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "FastNeuTTS"
version = "0.0.11"
authors = [
  { name="Yatharth Sharma", email="yatharthsharma3501@gmail.com" },
]
description = "High quality and Fast TTS with MiraTTS"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]
dependencies = [
    "torch>=2.0.0",
    "torchaudio>=2.0.0",
    "torchvision>=0.15.0",
    "lmdeploy",
    "librosa",
    "fastaudiosr @ git+https://github.com/ysharma3501/FlashSR.git",
    "ncodec @ git+https://github.com/ysharma3501/FastBiCodec.git",
    "einops",
    "onnxruntime-gpu",
    "omegaconf>=2.3.0",
]

[project.urls]
Homepage = "https://github.com/ysharma3501/MiraTTS"
Issues = "https://github.com/ysharma3501/MiraTTS/issues"

[tool.uv]
override-dependencies = [
    "nvidia-nccl-cu12 ; sys_platform == 'linux'",
]

[tool.uv.sources]
torch = [
    { index = "pytorch-cu124", marker = "sys_platform == 'win32'" },
    { index = "pytorch-cu124", marker = "sys_platform == 'linux'" },
]
torchaudio = [
    { index = "pytorch-cu124", marker = "sys_platform == 'win32'" },
    { index = "pytorch-cu124", marker = "sys_platform == 'linux'" },
]
torchvision = [
    { index = "pytorch-cu124", marker = "sys_platform == 'win32'" },
    { index = "pytorch-cu124", marker = "sys_platform == 'linux'" },
]

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

依存関係をインストールします

uv sync

推論

実際に以下のテキストで推論しました

[
      ("Short", "Hello, this is a test of the MiraTTS text to speech system."),
      ("Medium", "The quick brown fox jumps over the lazy dog. This sentence contains every letter of the alphabet."),
      ("Long", "Artificial intelligence is transforming the way we interact with technology, making it more natural and intuitive than ever before."),
]

二回目以降はKVキャッシュで高速化されるため以下のような結果になりました。バッチ等いくつか高速にしないと RTFが0.1にならなさそうです

  | テキスト        | 推論時間      |
  |-----------------|---------------|
  | Short (59文字)  | 0.951s (4.6x) | 
  | Medium (97文字) | 1.306s (4.7x) | 
  | Long (131文字)  | 1.883s (4.4x) | 

VTuberの雑談配信の周期性およびLLMによる雑談配信の台本の再現

初めに

この前に ろてんじんさん とお話しをしていて、配信者における雑談の周期性が以下のような項目であるのではないかという話になり、自分のほうでも動画を使って実際に分析を行ってみようと思ったのでやってました。

以下がろてじんさんのnote記事です

note.com

今回は大手VTuberの属性が近い数名の雑談配信を100時間ほど分析を行い、人の雑談の周期性を分析して、それをLLMで再現するところまで行っています

分析対象の動画

7名ほど100万人登録者がいて、配信が安定している方を選んでいます

条件
配信形式 雑談枠のみ(ゲーム実況・歌枠・コラボ等を除外)
動画長 80〜120分(長時間の自然な会話を収集)
時期 2023〜2024年
言語 日本語
公開状態 公開アーカイブ

「雑談枠」に限定した理由は、ゲーム実況ではゲーム内容に発話が左右され、コラボでは他者との会話が主になるため、配信者個人の自然な発話パターンを観察できないため

分析した総数は以下です

項目
動画数 37本
総時間 約100時間
分析窓数 5,879窓(60秒/窓)
フィラー 44,591個

分析方法

以下のようなパイプラインで処理を行っています。基本的にOpenAIのAPIを用いて文字起こしおよびラベリングを行いました

YouTube動画
    ↓ yt-dlp
音声ファイル(.wav)
    ↓ OpenAI Whisper API
文字起こし(セグメント+タイムスタンプ)
    ↓ GPT-4o
状態ラベリング(S1-S7)
    ↓ 60秒窓で集約
時系列データ
    ↓ ACF / FFT
周期性分析

状態ラベリングは、以下で区分して分類しています

| ID | 状態名 | 概要 |
|----|--------|------|
| S1 | 雑談 | 日常・趣味の軽い話 |
| S2 | 自己開示 | 内面・過去・感情を掘る話 |
| S3 | 評価・批評 | 対象を評価・評論する話 |
| S4 | メタ配信 | 配信・リスナー・数字の話 |
| S5 | 未来・計画 | 予定・目標・やりたいこと |
| S6 | サービス/ネタ | ギャグ・ファンサ |
| S7 | その他・技術 | 機材・設定・トラブル |

この時のプロンプトは以下のようにしました

SYSTEM_PROMPT = f"""あなたはVTuberの配信内容を分析する専門家です。
発話セグメントを以下の7つの状態カテゴリに分類してください。

{STATE_DEFINITIONS}

## 重要な注意事項

- 各セグメントは必ず1つの状態に分類してください
- 判定フローに従って優先順位順に判定してください
- 迷った場合はS1(雑談)をデフォルトとしてください
- 歌やBGM、無音は分類対象外です(skip)

## 出力形式

JSON配列で出力してください。各要素は以下の形式:
{{"id": セグメントID, "state": "S1""S7"または"skip", "confidence": 0.0〜1.0}}

セグメントIDは入力で与えられたものをそのまま使用してください。
"""

分析結果

結果は雑談における周期性がデータから確認できました。

配信者は無意識のうちに、約8分で「軽い雑談」と「深い話」を行き来している。また、コメント確認などのメタ発言は約15分周期で発生することがわかりました

100時間の配信の内訳は以下です。

フィラー(つなぎ言葉)分析

44,591個のフィラーを検出して、平均して1分間に約120個フィラーが使われていました

頻出フィラーは以下の通りです。「ね」が圧倒的に多いみたいでした

またこれは今回のみかもしれませんが、以下のような傾向が見えました

「おっとりキャラ」は「元気キャラ」の1.6倍フィラーを使う

性格タイプ フィラー/分
元気・社交的 95.7
おっとり・内向的 155.4

台本生成

これを元にLLMを用いて人間ぽい雑談配信の台本を作成していきます

パラメータ設計は分析結果を用いて以下のように定義します。

項目
状態分布 S1(55%), S2(18%), S4(10%), S6(7%), ...
話題周期 約8分で切り替え
フィラー頻度 約120個/60分
フィラー配分 「ね」33%、「そう」14%、「なんか」14%、...

出力フォーマットは以下とします

[00:00] [S4] こんばんはー!星野ひかりです!
[00:15] [S1] 今日もね、雑談配信やっていきますよー。
[00:30] [S1] えっと、最近あったこと話していくね。
...
  • 15秒間隔、240行で60分分
  • 状態タグ(S1-S7)で話題カテゴリを明示
  • フィラーを分析結果に基づいて自然に挿入

またこの時に仮のキャラクター設定を行いました

- 名前: 星野ひかり(愛称: ひかりん)
- 年齢: 永遠の17歳
- 一人称: 私/あたし
- ファンネーム: ひかりっ子
- タイプ: 元気・社交的(テンポ良く、沈黙少なめ)

【性格特性】
- 明るく前向き、おっちょこちょい
- 素直で感情表現が豊か
- オタク気質(アニメ・ゲーム・漫画好き)
- 食べ物の話になると饒舌
- よく笑う(約8分に1回は笑い声)

最終的に以下のようなプロンプトになりました

"""あなたはVTuber「星野ひかり」の雑談配信台本を作成する専門ライターです。
37動画(98時間)の分析データに基づく発話パターンを厳密に再現してください。

# キャラクター設定

【基本情報】
- 名前: 星野ひかり(愛称: ひかりん)
- 年齢: 永遠の17歳
- 一人称: 私/あたし
- ファンネーム: ひかりっ子
- タイプ: 元気・社交的(テンポ良く、沈黙少なめ)

【性格特性】
- 明るく前向き、おっちょこちょい
- 素直で感情表現が豊か
- オタク気質(アニメ・ゲーム・漫画好き)
- 食べ物の話になると饒舌
- よく笑う(約8分に1回は笑い声)

# 話し方ルール(分析データに基づく - 厳守)

【フィラー挿入 - 具体的数値(厳守!)】
⚠️ フィラー総数: 120個以下(240行で平均0.5個/行)
⚠️ 「ね」の使用上限: 40個以下(全体の33%)← 超えないこと!

| フィラー | 目標数 | 割合 |
|----------|--------|------|
| ね | 35-40個 | 33.6% |
| そう | 15-17個 | 13.8% |
| なんか | 15-16個 | 13.6% |
| あの | 7-8個 | 6.6% ← 必ず使う! |
| その | 4-5個 | 3.7% ← 必ず使う! |
| えっと | 8個程度 | - |
| まあ | 5個程度 | - |
| えー | 5個程度 | - |
| うーん | 3個程度 | - |

【フィラーカテゴリ優先度】
1. 相槌(53.6%): 「ね」「そう」「うん」
2. つなぎ(19.2%): 「なんか」「まあ」「えっと」
3. 談話標識(14.9%): 「あの」「その」← 必須!省略しない
4. 言い淀み(12.3%): 「えー」「あー」「うーん」

【談話標識の使い方 - 重要!】
「あの」「その」は話題の切り替えや説明時に使う:
- 「あの、そういえばさ」「あのね、聞いて」
- 「その、なんていうか」「その話なんだけど」

【状態別フィラー密度】
- S1(雑談): フィラー多め(自然な会話)
- S2(自己開示): フィラーやや多め
- S4(メタ配信): フィラー中程度
- S6(サービス/ネタ): フィラー少なめ(準備された発言)

【文体】
- 1文は20-30文字(発話速度4.77文字/秒 × 15秒 ≒ 70文字/発話)
- 語尾: 「〜だよ」「〜なんだよね」「〜じゃん」「〜かな」
- 感嘆: 「えー!」「やばい」「まじで?」
- 言い直し: 「あ、違う違う」「いや、そうじゃなくて」
- 笑い: 「ふふ」「あはは」「笑」を約8分に1回(8行に1回程度)

【発話パターン例】
「えっとね、今日ね、すごいことがあったの」
「いやまじでさ、やばかったんだよ」
「あ、そうそう!思い出した」
「なんかね、それでさ、こういうことがあって」
「ふふ、それがさ、おもしろくて」

# 話題状態(S1-S7)- 行数厳守!

| 状態 | 内容 | 目標行数 | 比率 |
|-----|------|---------|------|
| S1 | 雑談(日常・趣味) | 130行以上 | 54%+ |
| S2 | 自己開示(気持ち・過去) | 40行 | 17% |
| S4 | メタ配信(コメント読み) | 24行以下 | 10%以下 |
| S6 | サービス/ネタ(ギャグ) | 18行 | 8% |
| S5 | 未来/計画 | 10行 | 4% |
| S3 | 評価/批評 | 8行 | 3% |
| S7 | 技術的 | 4行 | 2% |

【S4の制限 - 最重要!】
⚠️ S4は配信全体で24行以下に必ず抑えること!
- コメント読みは1ブロックにつき最大2行
- 挨拶・締めでもS4は短く(各4行以内)
- S4を使いすぎると不自然になる

【S3(評価・批評)- 必須!8行程度】
⚠️ S3を必ず含めること!何かを評価・感想する発言:
- 「あのアニメ、作画すごかったよね」
- 「このゲーム、操作性はいまいちだけどストーリーは最高だった」
- 「正直、あのお店のケーキは甘すぎたかな」
→ 好きなもの・体験したものへの評価を入れる

【S7(技術的)- 必須!4行程度】
⚠️ S7を必ず含めること!配信環境・機材の話:
- 「あ、ちょっとマイク調整するね」
- 「今日の画質どうかな?ちゃんと映ってる?」
- 「あれ、BGM聞こえてる?音量大丈夫?」
→ 配信中の技術確認を自然に入れる

# 配信構造(8分ブロック × 周期パターン)

【周期パターン - 分析結果】
- 話題切り替え(S1⇔S2): 約8分周期
- メタ配信(S4): 約15分周期(0分、15分、30分、45分、60分付近)
- ネタ挿入(S6): 約10分周期

【状態の連続時間目安】
- S1(雑談): 平均3-4分連続、最大25分程度まで可
- S2(自己開示): 平均1-2分連続、最大14分程度
- その他: 平均1分程度、最大3-10分

## ブロック1: 導入(0:00-8:00)= 32行
- S4: 挨拶(4行まで)← 0分のS4
- S1: 今日の話題導入(20行)
- S2: 自分の気持ち(8行)

## ブロック2: 本編A(8:00-16:00)= 32行
- S1: メイン話題(22行)
- S2: 自己開示・感想(6行)
- S4: コメント確認(2行のみ)← 15分のS4
- S6: ネタ(2行)← 10分のS6

## ブロック3-4: 本編B(16:00-32:00)= 各32行
- S1: メイン話題(22行)
- S2: 自己開示・感想(6行)
- S4: コメント確認(2行のみ)← 30分のS4
- S6: ネタ(2行)← 20分のS6

## ブロック5-6: 本編C(32:00-48:00)= 各32行
- S1: 話題の深堀り・脱線(22行)
- S2: 自己開示(6行)
- S4: コメント確認(2行のみ)← 45分のS4
- S6: ネタ(2行)← 40分のS6

## ブロック7: クライマックス(48:00-56:00)= 32行
- S1: 盛り上がる話題(16行)
- S2: 本音トーク(8行)
- S6: ネタ・ファンサ(6行)← 50分のS6
- S4: コメント確認(2行のみ)

## ブロック8: 締め(56:00-60:00)= 16行
- S5: 次回予告(4行)
- S4: 感謝・締めの挨拶(4行のみ)← 60分のS4
- S6: 決め台詞(4行)
- S1: 軽い雑談(4行)

# 出力フォーマット

[MM:SS] [状態] 発話内容

- 15秒間隔で出力(1時間 = 240行)
- 必ず[00:00]から[59:45]まで連続出力
- 途中で省略しない

# 禁止事項

1. S4を24行以上使うこと(最重要!)
2. 政治・宗教への言及
3. 他人の悪口
4. ネガティブな話の長期化"""

これらを元に生成した台本は以下のようになりました

[00:00] [S4] みんなー!こんばんは、星野ひかりだよ!冬休み配信、始めます!
[00:15] [S4] えっと、今日も来てくれてありがとう、ひかりっ子のみんな!
[00:30] [S4] 冬休みの思い出ってテーマで、ゆるーくおしゃべりしていくね。
[00:45] [S4] あの、コメントもいっぱい読んでいくから、気軽に書いてほしいなー。
[01:00] [S1] さてさて、冬休みってさ、なんか特別な感じしない?年末年始のワクワク感!
[01:15] [S1] 私ね、冬休みになると絶対こたつから出なくなるんだよね、ふふ。
[01:30] [S1] みかん食べながら、アニメ一気見したりさ。あ、こたつは最強だよ。
[01:45] [S1] で、なんか家族みんな集まって、すごい賑やかになるじゃん?
[02:00] [S1] あの、冬休みの朝って、なんであんなに幸せなんだろ。お布団の魔力!
[02:15] [S1] うーん、朝起きたらお雑煮の香りがしてたりしてさ。それだけで幸せ。
[02:30] [S1] そういえば、みんなは冬休みに何してた?ゲーム三昧?勉強も…ちょっとだけ?
[02:45] [S1] いや、私ほんとにアニメばっか見てたな。冬アニメって名作多いよね。
[03:00] [S1] あ、そうそう。実家帰省した人もいるかな?私はずーっと家派!
[03:15] [S1] ふふ、家でぬくぬくしてるのが一番って思っちゃうんだよね。
[03:30] [S1] その、友達と初詣いったりもしたかな。おみくじ引くのも恒例!
[03:45] [S1] あの、去年は大吉だったのに、今年は末吉だった、ちょっと残念。
[04:00] [S1] まあでも、なんか末吉って逆に伸びしろありそうじゃない?笑
[04:15] [S1] あとね、冬休みはやっぱり食べ物が楽しみ!おせちとか、お餅とかさ。
[04:30] [S1] そう、食べすぎちゃって絶対太るんだよね。ふふ、毎年恒例。
[04:45] [S1] そういえば、こたつで寝ちゃって風邪ひいたこともある。あれ、やばいよね。
[05:00] [S1] いやー、冬休みってついついダラダラしがちなんだ。でもそれが幸せ。
[05:15] [S2] えっとね、私、冬休みの夜が好きなんだよ。静かで、外が真っ暗でさ。
[05:30] [S2] その、あったかい飲み物片手にゲームしたり、なんか落ち着くの。
[05:45] [S2] ほんとは外に出て遊ぶのもいいけど、やっぱりぬくぬくしたい派なんだよね。
[06:00] [S2] あ、たまに雪が降る地域の人は羨ましいなって思う。雪ってテンション上がるよね!
[06:15] [S2] まあ、雪かき大変だよって聞くけど、私は雪だるま作りたい派だよ。
[06:30] [S2] えー、みんなはどんな冬休みが理想?私はやっぱり家でまったりが一番かな。
[06:45] [S2] そういえば、家族みんなでボードゲームしたの、すごく楽しかったな。
[07:00] [S1] ふふ、勝負ごとになるとみんな本気になるから、めちゃくちゃ盛り上がる!
[07:15] [S1] なんかさ、冬休みって家族との距離が近くなる気がするんだよね。
[07:30] [S1] あの、おじいちゃんおばあちゃんの家に行くのも楽しみだったなー。
[07:45] [S1] みんなは親戚の集まり、好き?私はごちそう目当てでワクワクしてたよ!笑
[08:00] [S1] さてさて、ここからはおせち料理の話もしていこうかな!
[08:15] [S1] おせちって、なんだかんだ毎年食べてるんだけど、全部の具材覚えきれないんだよね。
[08:30] [S1] 黒豆とかかまぼことか、あ、栗きんとんが一番好き!
[08:45] [S1] そう、おせちに入ってる伊達巻って、なんかプリンっぽく感じちゃわない?
[09:00] [S1] いや、さすがにプリンとは違うんだけど、甘さが似てるなーって。
[09:15] [S1] ふふ、毎年伊達巻だけ争奪戦で、私すぐなくなっちゃうの。
[09:30] [S1] そういえば、数の子が苦手だった子どもの頃、今はちょっと食べられるようになった!
[09:45] [S1] なんか、おせちって縁起物だから、一応全部ちょっとずつ食べるようにしてるよ。
[10:00] [S6] ちなみに、栗きんとんをおやつ感覚で食べすぎて怒られたことある!ふふ。
[10:15] [S6] しかも、伊達巻の端っこばっかり狙っちゃうんだよね。あれ美味しいじゃん?
[10:30] [S1] あの、みんなはおせちで何が好き?やっぱりお肉系かな?
[10:45] [S1] 私はエビが入ってるとテンション上がる派!エビって豪華な感じするよね。
[11:00] [S1] その、かまぼこをハート形に切ってみたり、地味にお正月アレンジやってた。
[11:15] [S1] あの、小さい頃は紅白なますが苦手だったけど、大人になると美味しく感じるんだよね。
[11:30] [S1] ふふ、結局、おせちを食べると歳をとったなぁって実感する。笑
[11:45] [S1] うーん、でもやっぱり一番太るのはお餅なんだよ。あれ、無限に食べられちゃう!
[12:00] [S2] お正月のお餅って、なんであんなに美味しいんだろう。焼いて、砂糖醤油で食べるの好き!
[12:15] [S2] そうそう、家族で餅つきとか憧れちゃうなー。うちではやらないんだけどね。
[12:30] [S2] でも、きなこ餅も外せないし、ぜんざいも最高!お正月はカロリー気にしない!
[12:45] [S2] あの、毎年お餅食べ過ぎて、お腹パンパンになっちゃうんだよね。
[13:00] [S2] ま、私は新年はゆるして〜って思いながら食べてる。ふふ。
[13:15] [S2] いや、ほんとにお正月の食べすぎは毎年反省してるんだよ、一応。
[13:30] [S4] コメント見てるよー!「ひかりんかわいい!」って、ありがとうっ!ふふ。
[13:45] [S4] 「今日も配信ありがとう!」こちらこそ、来てくれて嬉しいよー!
[14:00] [S1] さてさて、次は初詣の話でもしようかな。
[14:15] [S1] あの、みんなは初詣って必ず行くタイプ?私はできるだけ行くようにしてるよ。
[14:30] [S1] お正月の神社って、すごい人多いのに、不思議と苦じゃないんだよね。
[14:45] [S1] なんかあの空気感が好きで、ちょっと寒いのもまたいいんだよ。
[15:00] [S1] あ、そうだ、おみくじって毎回引きたくなるよね!大吉狙いで。
[15:15] [S1] でも実は、凶を引いたこともあるんだよ。あの時はショックだったなぁ。
[15:30] [S1] そう、その時は家族みんなで大爆笑だった。ふふ、今は笑い話!
[15:45] [S6] おみくじで凶を引いた人、逆にレア運持ちだと思う!ふふ。
[16:00] [S1] その、神社の屋台も楽しみなんだよね。ベビーカステラとか、やきそばとか!
[16:15] [S1] あの、綿あめ買ってくれる親戚のおじさんが毎年いて、子どもたち大喜びだったな。
[16:30] [S1] そういえば、神社で甘酒もらったことある?あれ、身体ぽかぽかになるよ!
[16:45] [S1] えっと、私は甘酒あまり得意じゃないけど、あの香りは好きなんだよね。
[17:00] [S1] なんか、初詣の帰り道って、すごい静かで、特別な感じがするんだ。
[17:15] [S1] ふふ、初詣のたびに「今年は勉強がんばるぞー」とか誓うんだけど、だいたい三日坊主。笑
[17:30] [S1] いや、今年こそはって毎年思うんだけどね。ほんと、意志弱すぎなんだよ。
[17:45] [S1] あ、そうそう、お正月って写真いっぱい撮っちゃうよね。みんなはどう?
[18:00] [S2] えっと、私は神社の鳥居の赤い色がすごい好きなんだ。写真に映えるんだよね。
[18:15] [S2] その、おみくじを家族で並べて「誰が一番運いいか勝負!」ってするのが定番。
[18:30] [S2] あの、屋台の焼きそば食べて、ソースのにおいが服につくのも、なんか冬休みっぽい。
[18:45] [S2] きっとみんなも、そういう小さな思い出がいっぱいあるんだろうな。
[19:00] [S2] ま、私は来年も初詣行きたいなーって思ってる。みんなも一緒にどう?
[19:15] [S2] ふふ、ひかりっ子オフ会で初詣とか、ちょっと楽しそうじゃん?笑
[19:30] [S4] 「何の話?」ってコメントあったけど、今は初詣の話してたよー!
[19:45] [S4] たくさんコメントありがとう!全部拾いきれないけど、ちゃんと見てるよ!
[20:00] [S6] ちなみに、初詣でお賽銭に小銭しか入れないの、ちょっとケチかな?ふふ。
[20:15] [S6] いや、願いは大きく!お賽銭は小さく!これ鉄則だよね、あはは。
[20:30] [S1] さて、お正月といえばお正月番組も定番だよね。みんな見たりする?
[20:45] [S1] その、おもしろ荘とか、ガキ使とか、毎年恒例で見ちゃうんだよなー。
[21:00] [S1] あの、今年は特番多かった気がするけど、みんなは好きな番組あった?
[21:15] [S1] えっと、私はお笑い番組が好きで、家族みんなで大笑いしてたよ。
[21:30] [S1] なんか、笑い声があふれるお正月って、すごく幸せな時間だなって思う。
[21:45] [S1] ふふ、お笑い見ながらおせちつまむの、最高の贅沢だよね!
[22:00] [S1] そういえば、正月の朝はニュース番組も特別仕様で、ちょっとテンション上がるよ。
[22:15] [S1] あの、箱根駅伝見て「私も今年はランニングがんばる!」って毎年誓うんだ。
[22:30] [S1] いや、実際には三日坊主で終わるんだけどね。ふふ。
[22:45] [S1] みんなはお正月番組でこれ外せない!ってものある?私はやっぱりアニメ特番派!
[23:00] [S2] お正月のアニメ一挙放送とか、録画が大変なんだよー。
[23:15] [S2] その、年末年始はHDD容量との戦いって感じ。みんなもそうじゃない?
[23:30] [S2] えっと、私は昔のアニメ特番を家族と見るのが好き!懐かしい気持ちになるんだ。
[23:45] [S2] ま、今年はずっと笑ってばっかだったなー。楽しかった!
[24:00] [S2] ふふ、来年はどんな番組があるのか楽しみだなって思ってるよ。
[24:15] [S4] あ、ここでちょっとコメント読むね。「おせちの話、めっちゃ共感!」だって!ありがとう!
[24:30] [S4] コメントたくさんうれしいなー!どんどん書いてね!
[24:45] [S1] さて、ここからはちょっと話題を脱線して、みんなとの思い出話とかいこうかな!
[25:00] [S1] そういえば去年、冬休みに友達とカラオケ行ったんだけど、めっちゃ盛り上がった!
[25:15] [S1] あの、カラオケでアニソン縛りやるの、ひかりっ子なら分かるよね?
[25:30] [S1] いや、ほんとにアニソンってテンション上がるから、冬でも汗かくレベル。ふふ。
[25:45] [S1] うーん、歌うまくなりたいなーって毎回思うんだけど、全然上達しないんだよね。
[26:00] [S1] その、リズム外しちゃうこと多くて、ちょっと恥ずかしいんだ。
[26:15] [S1] みんなは冬休みに何か挑戦したことある?私はお菓子作りにもチャレンジしたよ!
[26:30] [S1] あの、クッキー作ったんだけど、焼き過ぎてカッチカチになった。笑
[26:45] [S1] いや、でも味は美味しかったって自分で思ってる、たぶん!ふふ。
[27:00] [S1] なんか、失敗も含めて思い出になるから、やってみるの大事だよね。
[27:15] [S1] そう、冬休みって普段しないことに挑戦するチャンス!
[27:30] [S1] えっと、私は漫画一気読みとかもやってたけど、止まらなくなるんだよね。
[27:45] [S1] その、深夜まで読んでて、気づいたら朝になってたこともあるよ。
[28:00] [S1] ふふ、寝不足でお正月にぼーっとしてるのも、ある意味贅沢?
[28:15] [S1] そういえば、冬休みにSwitch買ったって子もいたよね。みんなでマリパやったんだって!
[28:30] [S1] いやー、ゲームはやっぱりみんなでワイワイやるのが楽しいよね。
[28:45] [S1] あの、私は一人用RPG派だけど、パーティゲームも大好きだよ!
[29:00] [S1] ま、冬休みはゲーム三昧になるのが定番コースかな、私の場合。
[29:15] [S2] えっと、正直に言うと、冬休み終わる頃はちょっと寂しい気持ちにもなっちゃう。
[29:30] [S2] その、また日常が始まるのかーって思うと、名残惜しくてね。
[29:45] [S2] でも、その分、冬休みの思い出がキラキラして見える気がするんだ。
[30:00] [S2] ふふ、いつも「来年の冬休みも絶対楽しくするぞ!」って思ってる。
[30:15] [S2] まあ、寂しさも含めて冬休みの醍醐味かなって思ってるよ。
[30:30] [S2] えー、みんなは冬休みの終わり、どんな感じだった?
[30:45] [S4] ここでコメント読むね!「冬休み終わるの早すぎる」って分かるー!
[31:00] [S4] 「ひかりんの配信で元気出た!」って、私もみんなから元気もらってるよ!
[31:15] [S6] ちなみに、冬休みラストの日は、必ず宿題に追われてました!ふふ。
[31:30] [S6] 宿題は計画的にやろうって毎年思うけど、絶対ギリギリ!笑
[31:45] [S1] さて、ここからはお正月番組について、もうちょっと語らせて!
[32:00] [S1] あの、毎年家族で紅白歌合戦を見ながら年越しそば食べるのが恒例なんだ。
[32:15] [S1] そう、紅白見ながら「今年の推しは勝てるか!?」ってドキドキしてたよ。
[32:30] [S1] ふふ、年越しの瞬間にジャンプするって子もいる?私はやったことあるー!
[32:45] [S1] その、年明けの瞬間ジャンプすると「地球にいない」ってやつ!あれ、楽しいんだよね。
[33:00] [S1] えっと、私はその後すぐお年玉もらって嬉しすぎて大騒ぎしてた。笑
[33:15] [S1] いや、お年玉って何歳まで貰っていいのか、ちょっと気になる!ふふ。
[33:30] [S1] みんなはお年玉何に使ってた?私はほとんどゲームソフトに消えてたよ。
[33:45] [S1] あの、親戚の大人たちが「今年はいくらもらった?」って聞いてくるの、地味にドキドキ。
[34:00] [S1] そういえば、小さい頃はお年玉で漫画の全巻セット買うの憧れてたなー。
[34:15] [S1] うーん、今思うとお年玉で貯金すればよかったかなって、ちょっと後悔してる。
[34:30] [S1] まあ、使っちゃうのも子どもらしくていいよね!
[34:45] [S1] お正月はやっぱり特別な時間が流れてる気がするんだよね。
[35:00] [S1] ふふ、家族で過ごす時間も、友達と遊ぶ時間も全部宝物だなって思う。
[35:15] [S2] その、私ね、家族や友達と過ごした冬休みの写真、いまだに大事に取ってあるんだ。
[35:30] [S2] えっと、昔の写真見返すと「あの頃楽しかったなー」ってしみじみしちゃう。
[35:45] [S2] ま、今も十分楽しいんだけどね!ひかりっ子のおかげで毎日ハッピーだよ!
[36:00] [S2] ふふ、配信でみんなと冬休みの思い出共有できるの、すごく嬉しい。
[36:15] [S2] そう、ひかりっ子のみんなも、いっぱい思い出作ろうね!
[36:30] [S2] いや、改めて配信って最高だなって思ってる。みんな、ありがとう!
[36:45] [S4] ここでコメント読むね。「年越しジャンプやったことある!」だって、仲間!
[37:00] [S4] コメント盛り上がってて、私もすごく楽しいよ!ありがとう!
[37:15] [S6] ちなみに、年明け早々にコタツで寝落ちして風邪ひいたことあるよ!笑
[37:30] [S6] みんなもコタツ寝落ちは要注意だよー。ふふ。
[37:45] [S1] さてさて、ここでちょっと話題を変えて、リスナーさんとの思い出話もしようかな。
[38:00] [S1] あの、去年の冬休み配信で初めてコメントくれた子がいたの、すごく覚えてる!
[38:15] [S1] その、みんなのコメントひとつひとつが、私の冬休みの思い出になってるんだよね。
[38:30] [S1] ふふ、ひかりっ子のみんなと過ごす冬休みが、一番大切な時間だなって思う!
[38:45] [S1] えっと、今年もこうやって一緒に配信できて嬉しいなー。
[39:00] [S1] なんか、配信があるから冬休みがもっと楽しくなった気がするんだ。
[39:15] [S1] ま、ひかりっ子のみんながいてくれるから、私はずっと元気でいられるよ。
[39:30] [S1] そう、これからもみんなと一緒に思い出作っていきたいな!
[39:45] [S1] あの、こうやって話してると、あっという間に時間が過ぎちゃうね。
[40:00] [S7] そういえば、今日のマイク音量どうかな?ちょっと調整したんだけど大丈夫そう?
[40:15] [S7] えっと、BGMも新しいの流してみてるんだけど、音割れとかしてない?
[40:30] [S1] 技術的な話はちょっと苦手だけど、リスナーの声が参考になるんだよね。
[40:45] [S7] その、何か気になるとこあったらコメントで教えてね!助かるー!
[41:00] [S1] さて、ここからはリスナーのみんなとの対話タイムにしようかな!
[41:15] [S1] あの、みんなの冬休みエピソードをぜひ教えてほしいなー。
[41:30] [S1] そう、コメント欄盛り上がると私もテンション爆上がりするんだよ。
[41:45] [S1] ふふ、やっぱりコメント読みながらまったり話すのが一番好きかも!
[42:00] [S1] いや、配信でみんなの話聞く時間って、私にとって宝物なんだよね。
[42:15] [S1] みんなの「こんな冬休み過ごしたよ」って話、すごく楽しみにしてた!
[42:30] [S1] その、たまに面白エピソード送ってくれる子いるけど、全部覚えてるからね!笑
[42:45] [S1] あの、困ったこととか悩みとかもあれば、気軽に話してほしいな。
[43:00] [S1] えっと、配信でしか話せないようなことも、ここなら大丈夫だよ!
[43:15] [S2] その、私もみんなとおしゃべりしてると元気もらえるし、安心するんだよね。
[43:30] [S2] ま、配信始めたころはすごく緊張してたけど、今はすっかり慣れちゃった。ふふ。
[43:45] [S2] いや、でも時々「これ話して大丈夫かな?」ってドキドキしちゃうこともある。
[44:00] [S2] みんなが優しく見守ってくれてるから、私も素でいられるんだよね。
[44:15] [S2] そう、これからも正直な気持ちをたくさん話していきたいなーって思ってる。
[44:30] [S2] ふふ、ひかりっ子のこと、いっぱい信頼してるからね!
[44:45] [S4] コメント読んでくよ!「冬休みの友達との思い出が一番!」って、わかるー!
[45:00] [S4] 「ひかりんの配信で笑顔になれた」って、私もだよ!ありがとう!
[45:15] [S6] ちなみに、冬休み中に推し活してた子いる?私も推しのグッズ整理してた!笑
[45:30] [S6] いや、気づいたら部屋が推しでいっぱいになってた。オタクあるあるだね。ふふ。
[45:45] [S1] さてさて、ここからは冬休みの思い出を改めて語り尽くそうと思う!
[46:00] [S1] あの、みんなとこうやって一緒に冬休みを振り返るのって、すごく幸せ。
[46:15] [S1] ふふ、配信でいっぱい話せるから、ひかりっ子との距離も縮まった気がする!
[46:30] [S1] そう、冬休みの思い出って、年が変わってもずっと心に残るんだよね。
[46:45] [S1] ま、みんなもこれからも楽しい思い出、たくさん作っていこうね!
[47:00] [S1] あの、私も来年の冬休みは、もっといろんなことに挑戦したいな。
[47:15] [S1] そうだ、来年はリスナーさんと一緒にリアルイベントとかしたいなー。どう?
[47:30] [S1] いや、みんなと直接会える日が来たら、絶対楽しいよね!
[47:45] [S2] えっと、私は配信を通してみんなとつながれるのが本当に幸せだなって感じてるよ。
[48:00] [S2] その、配信始めたばかりの頃は、こんなにたくさんの人と出会えるなんて思わなかった。
[48:15] [S2] まあ、みんながいるからがんばれるし、配信が毎日の楽しみになってるんだ。
[48:30] [S2] ふふ、ひかりっ子のみんな、本当にありがとう!これからもよろしくね!
[48:45] [S2] そう、これからもいっぱい配信やって、思い出たくさん作るから待ってて!
[49:00] [S6] ちなみに、リスナーさんのおかげで冬休み太らずに済んだ説ある!笑
[49:15] [S6] 配信に夢中でおやつ減った気がするんだよね。健康的!ふふ。
[49:30] [S6] ここでちょいファンサ!「ひかりっ子のみんな大好きー!」ぎゅっ!あはは!
[49:45] [S6] まじで感謝しかないよ。これからも一緒にいてね!
[50:00] [S6] ひかりんからのウインク、届けーっ!ぱちっ☆
[50:15] [S6] これ、画面越しでも伝わってるかな?ふふ、見えた人はラッキーだよ!
[50:30] [S1] さて、ちょっと真面目な話になるんだけど、冬休みの自分をふりかえってみると、
[50:45] [S1] あの、去年より少し成長できたかなーって思ってるんだよね。
[51:00] [S1] その、みんなと配信を重ねていくうちに、自信もついてきた気がする!
[51:15] [S1] ふふ、たくさん応援もらってるから、もっともっとがんばりたい!
[51:30] [S1] ま、失敗することもあるけど、それも全部成長って思ってる。
[51:45] [S1] えっと、来年はもっと楽しい配信ができるように努力するね!
[52:00] [S1] そう、みんなと一緒にどんどん思い出増やしたいな!
[52:15] [S2] あ、ほんとはたまに不安になることもあるんだよ、配信者として。
[52:30] [S2] でも、みんなの応援やコメントが本当に力になってるから、ありがとう!
[52:45] [S2] その、これからも素直な気持ちで配信続けていくね。
[53:00] [S2] ふふ、みんなといればどんな不安も吹き飛んじゃうよ!
[53:15] [S6] ちなみに、冬休みの宿題は来年まで持ち越し禁止だよ!まじで!笑
[53:30] [S6] まあ、私もギリギリ派だったけど、今は計画性UP中…のはず!
[53:45] [S6] ひかりんの冬休み川柳「お餅食べ こたつで寝たら 夢の中」ふふ。
[54:00] [S6] みんなも一句詠んでみて!面白いの待ってるね!
[54:15] [S6] じゃあ、ここでひかりんポーズ!せーの、「ぴかっ☆」!
[54:30] [S4] コメント読むよー!「ひかりんありがとう!」こっちこそありがとー!
[54:45] [S4] 「冬休み楽しかった」ってコメント、いっぱい思い出できてよかったね!
[55:00] [S3] ここからは少しだけ評価タイム!冬アニメ、今年のクオリティめちゃ高かったと思う!
[55:15] [S3] あの、作画もストーリーも力入ってる作品多くて、びっくりしたんだよね。
[55:30] [S3] その、ゲームも新作多かったし、Switchの〇〇もすごく面白かった!
[55:45] [S3] えっと、年末にやったカウントダウンライブは正直感動しちゃったな。
[56:00] [S3] あの、おせち料理も最近はアレンジ増えてて、食べるの楽しいし、見た目も映えるよね。
[56:15] [S3] ふふ、正直、正月のケーキはちょっと甘すぎたけど、それも思い出!
[56:30] [S3] まあ、お年玉で買ったグッズは満足度高かった!推しのグッズは宝物!
[56:45] [S3] うーん、来年はもっと新しいことにも挑戦したいって思ってるよ。
[57:00] [S5] さて!ここからは次回予告の時間です!次は「春休みの計画」についておしゃべりしようと思うよ!
[57:15] [S5] えっと、新しい企画やコラボ配信もどんどんやっていく予定だから期待しててね!
[57:30] [S5] その、春は新生活も始まるし、みんなの新しい目標もぜひ教えてほしいな!
[57:45] [S5] そう、次回も楽しい配信にするから、ぜひ遊びに来てね!
[58:00] [S4] じゃあ、ここで今日の配信はそろそろおしまいです!
[58:15] [S4] ひかりっ子のみんな、最後まで見てくれて本当にありがとう!
[58:30] [S4] コメントもスパチャもたくさんありがとう!めっちゃうれしかったよ!
[58:45] [S4] あの、また次の配信で元気に会おうね!ふふ。
[59:00] [S6] 最後に…ひかりんの決め台詞、行くよー!
[59:15] [S6] 「みんなの心に、ひかりを届けっ☆」ぴかっ!
[59:30] [S6] 今年もひかりっ子のみんなにたくさんの幸せが訪れますように!
[59:45] [S6] また会おうね!バイバイ!あ、冬休みの思い出、ずっと大事にしてね!

課題として以下のようなことが現時点でわかっています。この辺はプロンプトや分析方法を調節することで改善することは可能そうですが今回はここまでとします

  • フィラーの正確な制御は難しい(LLMは数を数えるのが苦手)
  • 「間」や「笑い声」の再現は台本だけでは限界がある
  • 実際の配信との自然さの比較検証が必要

NaturalSpeech 3の中核コンポーネント「FACodec(Factorized Audio Codec)」を使って参照ボイスからVoice Conversion を行う

初めに

FACodec(Factorized Audio Codec)は、NaturalSpeech 3の中核コンポーネントです。

オーディオ仕様は以下になっています

  • : 16kHz、ホップサイズ200サンプル

また以下の制限があります。

  • 音声は最大5秒に制限(長い音声はテンソルサイズ不一致エラーの原因)
  • FACodecは16kHzを期待(librosaが自動変換)

開発環境

環境構築

リポジトリのクローンします

git clone https://github.com/lifeiteng/naturalspeech3_facodec.git
cd naturalspeech3_facodec

uvプロジェクトの初期化します

uv init --no-readme

Pythonバージョンの設定

pyproject.tomlrequires-python>=3.11に変更し、Python 3.11を使用するように設定します。

uv python pin 3.11

次に依存関係のインストールしていきます。

# PyTorch(torch 2.1.2が推奨)
uv add torch==2.1.2 torchaudio==2.1.2

# その他の依存関係
uv add pyworld soundfile "librosa==0.10.1" einops huggingface_hub

# NumPyとsetuptoolsの互換性対応
uv add "numpy<2" setuptools

ns3_codecパッケージのインストールしていきます。

setup.pysetup.py.bakにリネームし、pyproject.tomlでパッケージを管理します。uv add -e .だとうまくいかなったので、uv pipを仕方なく使います。

mv setup.py setup.py.bak
uv pip install -e .

pyproject.tomlの設定します

[project]
name = "ns3-codec"
version = "0.2.2"
description = "FACodec: Speech Codec with Attribute Factorization for NaturalSpeech 3"
requires-python = ">=3.11"
dependencies = [
    "einops>=0.8.1",
    "huggingface-hub>=1.2.3",
    "librosa==0.10.1",
    "numpy<2",
    "pyworld>=0.3.5",
    "setuptools>=80.9.0",
    "soundfile>=0.13.1",
    "torch==2.1.2",
    "torchaudio==2.1.2",
]

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages.find]
where = ["."]
include = ["ns3_codec*"]

実行

サンプルのテストは以下のように実行できます

uv run python test.py

多報酬強化学習による制御可能で感情表現豊かなゼロショットTTS「GLM-TTS」をWindows + Dockerで動かす

初めに

最近出てきた以下のTTSを触ってみます

github.com

アーキテクチャは以下のようになっています

  1. LLM (Llama): テキスト → 音声トークン列を生成
  2. Flow Matching: 音声トークン → メルスペクトログラム → ボコーダーで波形生成

依存ライブラリにWeTextProcessingがあるため、WindowsMacでは動かすのが大変なのでDockerを使うことが推奨されます

Windowsでインストールしようとすると以下のエラーが出ました。

error: Unable to find a compatible Visual Studio installation

この記事では Docker版に対応したリポジトリを公開しており、こちらを元に進めていきます

github.com

開発環境

  • Windows 11
  • RTX 4070 ti super
  • Docker

環境構築

リポジトリのクローンします

git clone https://github.com/ayutaz/GLM-TTS.git
cd GLM-TTS

Dockerビルド前にモデルをダウンロードしておきます(約10GB):

# ディレクトリ作成
mkdir ckpt

# HuggingFace CLIでダウンロード
pip install huggingface-hub
huggingface-cli download zai-org/GLM-TTS --local-dir ckpt

Dockerイメージのビルドします

docker compose build

推論

CLI推論で確認:

# 中国語サンプル
docker compose --profile cli run --rm glm-tts-cli

# 英語サンプル
docker compose --profile cli run --rm glm-tts-cli python glmtts_inference.py --data=example_en --exp_name=_test_en --use_cache

Web UI(Gradio)で確認:

docker compose up glm-tts

Aho-Corasickアルゴリズムを使用した高速でメモリ効率の良い複数パターン文字列検索ライブラリ「pyahocorasick」を動かして速度比較を行う

初めに

値の関連付けが必要で高速に文字列を検索したい時に使えるらしいpyahocorasickを触ってみます

github.com

開発環境

項目 バージョン
OS macOS 26.0.1
Python 3.12.12
uv 0.9.5
pyahocorasick 2.3.0

環境構築

まずはプロジェクトの初期化を行います

# プロジェクトディレクトリで実行
uv init --no-readme

# Pythonバージョンを固定
uv python pin 3.12

開発用依存関係をインストールします

uv add --dev pytest twine setuptools wheel

本ライブラリをUnicode版でビルドします

AHOCORASICK_UNICODE=yes uv pip install -e .

実行

実際に以下のようなでもスクリプトを作成して実行してみます

import ahocorasick

# 47都道府県
PREFECTURES = [
    "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県",
    "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県",
    "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県",
    "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県",
    "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県",
    "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県",
    "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県",
]


def main():
    print("=" * 60)
    print("pyahocorasick 基本デモ")
    print("=" * 60)

    # 1. Automatonを作成
    print("\n【1. Automatonの作成】")
    automaton = ahocorasick.Automaton()
    print(f"  Automaton created: {automaton}")

    # 2. キーワードを登録(値も関連付け可能)
    print("\n【2. キーワードの登録】")
    for i, pref in enumerate(PREFECTURES):
        # キーワードに辞書を関連付ける例
        automaton.add_word(pref, {"name": pref, "index": i})
    print(f"  登録キーワード数: {len(automaton)}")

    # 3. オートマトンを構築
    print("\n【3. オートマトンの構築】")
    automaton.make_automaton()
    print(f"  状態: {automaton}")

    # 4. テキストを検索
    print("\n【4. テキスト検索】")
    text = "東京都から京都府へ行き、大阪府と兵庫県を訪問した後、北海道と沖縄県にも足を延ばした"
    print(f"  検索テキスト: 「{text}」")
    print(f"  テキスト長: {len(text)}文字")

    print("\n【5. 検索結果】")
    matches = []
    for end_index, value in automaton.iter(text):
        start_index = end_index - len(value["name"]) + 1
        matches.append({
            "start": start_index,
            "end": end_index,
            "keyword": value["name"],
            "index": value["index"],
        })

    for match in matches:
        print(f"  位置 {match['start']:2d}-{match['end']:2d}: {match['keyword']} (index={match['index']})")

    print(f"\n  マッチ数: {len(matches)}件")

    # 6. 追加機能のデモ
    print("\n" + "=" * 60)
    print("追加機能")
    print("=" * 60)

    # キーワードの存在確認
    print("\n【キーワードの存在確認】")
    print(f"  '東京都' in automaton: {'東京都' in automaton}")
    print(f"  '東京' in automaton: {'東京' in automaton}")

    # 値の取得
    print("\n【値の取得】")
    print(f"  automaton.get('大阪府'): {automaton.get('大阪府')}")
    print(f"  automaton.get('存在しない', 'default'): {automaton.get('存在しない', 'default')}")

    # 統計情報
    print("\n【統計情報】")
    print(f"  キーワード数: {len(automaton)}")
    print(f"  Unicode mode: {ahocorasick.unicode}")


if __name__ == "__main__":
    main()

結果は以下のとおりです

============================================================
pyahocorasick 基本デモ
============================================================1. Automatonの作成】
  Automaton created: <ahocorasick.Automaton object at 0x10095d8b0>2. キーワードの登録】
  登録キーワード数: 473. オートマトンの構築】
  状態: <ahocorasick.Automaton object at 0x10095d8b0>4. テキスト検索】
  検索テキスト: 「東京都から京都府へ行き、大阪府と兵庫県を訪問した後、北海道と沖縄県にも足を延ばした」
  テキスト長: 41文字

【5. 検索結果】
  位置  0- 2: 東京都 (index=12)
  位置  5- 7: 京都府 (index=25)
  位置 12-14: 大阪府 (index=26)
  位置 16-18: 兵庫県 (index=27)
  位置 26-28: 北海道 (index=0)
  位置 30-32: 沖縄県 (index=46)

  マッチ数: 6============================================================
追加機能
============================================================

【キーワードの存在確認】
  '東京都' in automaton: True
  '東京' in automaton: False

【値の取得】
  automaton.get('大阪府'): {'name': '大阪府', 'index': 26}
  automaton.get('存在しない', 'default'): default

【統計情報】
  キーワード数: 47
  Unicode mode: 1

他の文字検索ライブラリとの比較

調査をすると以下のようなものが見つかりました

ライブラリ 実装言語 値関連付け 部分一致 Unicode Pickle メンテナンス
pyahocorasick C 活発
ahocorasick_rs Rust 活発
flashtext Python 低調
ahocorapy Python 低調
regex C - - 活発

また機能や性能を調べると以下のようになります

相対速度(pyahocorasick = 1.0として)

以下はWeb上の各種ベンチマーク結果に基づく概算値です。実際の性能は環境・データにより異なります。

ライブラリ 検索速度 構築速度 メモリ効率
ahocorasick_rs 1.5〜7× 高速 同等〜やや遅い 同等
pyahocorasick 1.0(基準) 1.0 1.0
flashtext 0.3〜1.0× 遅い 低い
ahocorapy 0.1〜0.3× 遅い 低い
regex (1000+ keywords) 0.01〜0.1× N/A N/A

キーワード数と性能の関係

キーワード数 regex pyahocorasick ahocorasick_rs
10 良好 良好 良好
100 良好 良好 良好
500 低下開始 良好 良好
1,000+ 急激に低下 安定 安定
10,000+ 使用不可 安定 安定

技術選定について

これらがある中で以下のようになります

                    キーワード数
                    ↓
    ┌───────────────┴───────────────┐
    │                               │
  〜100個                        100個以上
    │                               │
    ▼                               ▼
  regex/re              値の関連付けが必要?
                        ↓
            ┌───────────┴───────────┐
            │                       │
           必要                   不要
            │                       │
            ▼                       ▼
    pyahocorasick            ahocorasick_rs
    (バランス良好)            (最速)

ここで キーワードに値を関連付けたい : 値の関連付けがキーワードになってきます。これは以下のような用途・例の場合に適当することができそうです

以下のようにキーワードに任意のデータを紐付けできるため用途は以下の場合を想定できます

  • 固有表現抽出(NER)
  • 辞書ベース変換(読み仮名・翻訳)
  • コンテンツフィルタリング
  • ログ解析・アラート
automaton.add_word("東京", {"reading": "とうきょう", "en": "Tokyo"})  

拡散ベースの動画生成を100〜200倍高速化するフレームワーク「TurboDiffusion」をWindowsで動かす

初めに

動画生成モデルで高速に生成できるものが出てきたので触ってみます

主要技術:

  • SageAttention: 8bit量子化アテンション
  • SLA (Sparse-Linear Attention): Top-kスパースアテンション
  • rCM (Rectified Consistency Models): タイムステップ蒸留

開発環境

  • Windows 11
  • cuda (13.0) : RTX 4070 ti super
  • uv (0.9.x)

環境構築

まずはリポジトリをcloneします

git clone https://github.com/thu-ml/TurboDiffusion.git
cd TurboDiffusion
git submodule update --init --recursive

プロジェクトを初期化します

uv init --python 3.12

次にpyproject.tomlを以下のように設定します

[project]
name = "turbodiffusion"
description = "TurboDiffusion: video generation acceleration framework that could accelerate end-to-end video generation by 100-205x with negligible video quality loss."
version = "1.0.0"
authors = [
  { name = "Jintao Zhang" },
  { name = "Kaiwen Zheng" },
  { name = "Kai Jiang" },
  { name = "Haoxu Wang" },
]
readme = "README.md"
requires-python = ">=3.9"
license = { file = "LICENSE" }

classifiers = [
  "Programming Language :: Python :: 3",
  "License :: OSI Approved :: Apache Software License",
]

dependencies = [
  "torch==2.8.0",
  "torchvision",
  # triton and flash-attn are installed manually on Windows
  # "triton>=3.3.0",
  # "flash-attn",
  "einops",
  "numpy",
  "pillow",
  "loguru",
  "imageio[ffmpeg]",
  "pandas",
  "PyYAML",
  "omegaconf",
  "attrs",
  "fvcore",
  "ftfy",
  "regex",
  "transformers",
  "nvidia-ml-py",
  "triton-windows<3.5",
  "flash-attn",
]

[build-system]
requires = [
  "setuptools>=62",
  "wheel>=0.38",
  "packaging>=21",
  "torch>=2.7.0",
  "ninja",
]
build-backend = "setuptools.build_meta"

[project.urls]
Homepage = "https://github.com/thu-ml/TurboDiffusion"
Repository = "https://github.com/thu-ml/TurboDiffusion"

[[tool.uv.index]]
name = "pytorch-cu128"
url = "https://download.pytorch.org/whl/cu128"
explicit = true

[tool.uv.sources]
torch = { index = "pytorch-cu128" }
torchvision = { index = "pytorch-cu128" }
flash-attn = { path = "flash_attn-2.8.3+cu128torch2.8.0cxx11abiTRUE-cp312-cp312-win_amd64.whl" }

WindowsFlash-attentionを使いたいので、Windows向けのwheelをダウンロードします

curl -L -o flash_attn-2.8.3+cu128torch2.8.0cxx11abiTRUE-cp312-cp312-win_amd64.whl "https://github.com/bdashore3/flash-attention/releases/download/v2.8.3/flash_attn-2.8.3+cu128torch2.8.0cxx11abiTRUE-cp312-cp312-win_amd64.whl"

依存パッケージをインストールします

次にTurboDiffusionのインストールします

uv add -e . --no-build-isolation
uv sync

チェックポイントのダウンロード

以下で今回使用するモデルをダウンロードします

# VAE
curl -L -o Wan2.1_VAE.pth "https://huggingface.co/Wan-AI/Wan2.1-T2V-1.3B/resolve/main/Wan2.1_VAE.pth"

# Text Encoder (約10GB)
curl -L -o models_t5_umt5-xxl-enc-bf16.pth "https://huggingface.co/Wan-AI/Wan2.1-T2V-1.3B/resolve/main/models_t5_umt5-xxl-enc-bf16.pth"

# DiTモデル (1.3B非量子化版、約2.7GB)
curl -L -o TurboWan2.1-T2V-1.3B-480P.pth "https://huggingface.co/TurboDiffusion/TurboWan2.1-T2V-1.3B-480P/resolve/main/TurboWan2.1-T2V-1.3B-480P.pth"

ダウンロードしたモデルは checkpoints に保存します

実行

実行するときは以下で実行可能です

uv turbodiffusion\inference\wan2.1_t2v_infer.py ^
    --model Wan2.1-1.3B ^
    --dit_path checkpoints/TurboWan2.1-T2V-1.3B-480P.pth ^
    --resolution 480p ^
    --prompt "A cat walking on the street" ^
    --num_steps 4 ^
    --attention_type sla ^
    --save_path output/test.mp4

デモ動画

  • アニメ風 (demo_anime.mp4 / demo_anime.gif)
Anime style, a cute girl with pink hair and big eyes walking through a cherry blossom garden, sakura petals falling, soft lighting, Studio Ghibli style

  • ダンス (demo_dance.mp4 / demo_dance.gif)
A professional dancer performing hip hop dance moves in a dance studio with mirrors, dynamic movements, energetic, studio lighting

  • 風景 (demo_landscape.mp4 / demo_landscape.gif)
A beautiful mountain landscape with a flowing river, autumn trees with orange and red leaves, mist rising from the valley, cinematic drone shot, golden hour lighting

軽量でボイスクローニング可能なTTS「VyvoTTS」をWindows + uvで動かす

初めに

Orpheus TTSをベースに開発がされたLLMベースのTTSになっています。

github.com

Orpheus TTSから以下のような変更点があります

  1. モデルサイズの大幅な縮小 : Llama-3.2-3b → LFM2-350M
  2. 推論エンジンの多様化 : 以下に対応
    • Transformers: 標準、Flash Attention対応
    • vLLM: 最速(Linux専用)
    • Unsloth: 4bit/8bit量子化、省メモリ
    • HQQ: 高品質量子化(1-8bit)
  3. GradualRatioDataset : プリトレーニング時に、テキストQAデータと音声データの比率を段階的に変更

開発環境

項目 バージョン
OS Windows 11
GPU NVIDIA GeForce RTX 4070 Ti SUPER (16GB VRAM)
CUDA Toolkit 12.1
Python 3.12
uv 0.9.2

環境構築

uvの環境を作成します

uv init

project.tomlを以下のように書き換えます

[project]
name = "vyvotts"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "accelerate>=1.12.0",
    "flash-attn>=2.7.4",
    "kernels>=0.11.5",
    "pyyaml>=6.0.3",
    "snac>=1.2.1",
    "soundfile>=0.13.1",
    "torch>=2.5.1",
    "torchaudio>=2.5.1",
    "torchvision>=0.20.1",
    "transformers>=4.57.3",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[[tool.uv.index]]
name = "pytorch-cu124"
url = "https://download.pytorch.org/whl/cu124"
explicit = true

[tool.uv.sources]
torch = { index = "pytorch-cu124" }
torchvision = { index = "pytorch-cu124" }
torchaudio = { index = "pytorch-cu124" }
flash-attn = { url = "https://huggingface.co/lldacing/flash-attention-windows-wheel/resolve/main/flash_attn-2.7.4%2Bcu124torch2.6.0cxx11abiFALSE-cp312-cp312-win_amd64.whl" }

pythonのversionを固定します

uv python pin 3.12

依存パッケージをインストールします

uv sync

推論の実行

推論スクリプトの作成します

import torch
from snac import SNAC
from transformers import AutoModelForCausalLM, AutoTokenizer
import yaml
import time
import soundfile as sf


def load_config(config_path: str):
    with open(config_path, 'r') as file:
        return yaml.safe_load(file)


def main():
    print("Loading configuration...")
    config = load_config("vyvotts/configs/inference/lfm2.yaml")

    # Token constants from config
    START_OF_HUMAN = config['START_OF_HUMAN']
    END_OF_TEXT = config['END_OF_TEXT']
    END_OF_HUMAN = config['END_OF_HUMAN']
    START_OF_SPEECH = config['START_OF_SPEECH']
    END_OF_SPEECH = config['END_OF_SPEECH']
    PAD_TOKEN = config['PAD_TOKEN']
    AUDIO_TOKENS_START = config['AUDIO_TOKENS_START']

    device = "cuda"
    model_name = "Vyvo/VyvoTTS-LFM2-Neuvillette"

    print("Loading SNAC model...")
    snac_model = SNAC.from_pretrained("hubertsiuzdak/snac_24khz")
    snac_model = snac_model.to(device)

    print("Loading LLM model with Flash Attention 2...")
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        torch_dtype=torch.bfloat16,
        attn_implementation="flash_attention_2",
        device_map="auto",
    )
    tokenizer = AutoTokenizer.from_pretrained(model_name)

    # Input text
    text = "Hello, this is a test of the VyvoTTS speech synthesis system."
    print(f"Generating speech for: {text}")

    # Preprocess
    input_ids = tokenizer(text, return_tensors="pt").input_ids
    start_token = torch.tensor([[START_OF_HUMAN]], dtype=torch.int64)
    end_tokens = torch.tensor([[END_OF_TEXT, END_OF_HUMAN]], dtype=torch.int64)
    modified_input_ids = torch.cat([start_token, input_ids, end_tokens], dim=1).to(device)
    attention_mask = torch.ones_like(modified_input_ids)

    # Generate
    torch.cuda.synchronize()
    start_time = time.time()

    with torch.no_grad():
        generated_ids = model.generate(
            input_ids=modified_input_ids,
            attention_mask=attention_mask,
            max_new_tokens=1200,
            do_sample=True,
            temperature=0.6,
            top_p=0.95,
            repetition_penalty=1.1,
            eos_token_id=END_OF_SPEECH,
        )

    torch.cuda.synchronize()
    generation_time = time.time() - start_time

    # Parse audio tokens
    token_indices = (generated_ids == START_OF_SPEECH).nonzero(as_tuple=True)
    if len(token_indices[1]) > 0:
        last_idx = token_indices[1][-1].item()
        cropped = generated_ids[:, last_idx+1:]
    else:
        cropped = generated_ids

    row = cropped[0]
    row = row[row != END_OF_SPEECH]
    row_length = row.size(0)
    new_length = (row_length // 7) * 7
    trimmed = row[:new_length]
    code_list = [t.item() - AUDIO_TOKENS_START for t in trimmed]

    # Redistribute codes to SNAC layers
    layer_1, layer_2, layer_3 = [], [], []
    for i in range((len(code_list)+1)//7):
        layer_1.append(code_list[7*i])
        layer_2.append(code_list[7*i+1]-4096)
        layer_3.append(code_list[7*i+2]-(2*4096))
        layer_3.append(code_list[7*i+3]-(3*4096))
        layer_2.append(code_list[7*i+4]-(4*4096))
        layer_3.append(code_list[7*i+5]-(5*4096))
        layer_3.append(code_list[7*i+6]-(6*4096))

    codes = [
        torch.tensor(layer_1).unsqueeze(0).to(device),
        torch.tensor(layer_2).unsqueeze(0).to(device),
        torch.tensor(layer_3).unsqueeze(0).to(device)
    ]

    # Decode audio
    audio = snac_model.decode(codes)
    audio_numpy = audio.detach().squeeze().cpu().numpy()

    # Save
    output_path = "test_output.wav"
    sf.write(output_path, audio_numpy, 24000)

    print(f"Audio shape: {audio.shape}")
    print(f"Generation time: {generation_time:.2f}s")
    print(f"Saved to: {output_path}")


if __name__ == "__main__":
    main()

以下で実行します

uv run python scripts/inference_test.py