🎤

Nightingaleコード分析 — Rust+MLで作ったオープンソースカラオケアプリの構造

Tauri 2 + Python MLサーバー + UVR/Demucs音声分離 + WhisperX歌詞同期 + リアルタイムピッチ検出

GitHubリポジトリ(rzru/nightingale)の直接分析結果。

アーキテクチャ — BevyではなくTauri 2

当初「Rust + Bevyゲームエンジン」と報じられたが、実際のコードはTauri 2デスクトップアプリ(Rustバックエンド + Reactフロントエンド)だ。

Cargoワークスペース構造:

  • app-core/ — 純粋Rustライブラリ。ビジネスロジック、MLオーケストレーション、キャッシング、ベンダーブートストラッピング

  • client/src-tauri/ — Tauri 2シェル。app-coreを#[tauri::command]で公開、cpalでマイク入力、pitch-detectionでピッチ検出

  • xtask/ — ビルドツーリング

「ゲームエンジン」要素はフロントエンドのGPUシェーダー背景(Plasma、Aurora、Nebula等)のみ。WebGL/WebGPUでレンダリング。

音声分離 — 最も核心的な部分

2つの分離バックエンドを支援。config.separator()で選択、デフォルトは"karaoke"

1. UVR Karaokeモデル(デフォルト)

app-core/analyzer/stems.pyに実装:

KARAOKE_MODEL = "mel_band_roformer_karaoke_aufr33_viperx_sdr_10.1956.ckpt"
separator = Separator(model_file_dir=models_dir, output_dir=work_dir)
separator.load_model(KARAOKE_MODEL)
output_files = separator.separate(audio_path)

Mel-Band RoFormerアーキテクチャ、aufr33+viperx訓練モデル、SDR 10.1956。特徴はリードボーカルのみ分離し、バッキングボーカルは伴奏に残す — カラオケに最適な動作。

audio-separatorパッケージがONNX Runtimeで実行。NVIDIA GPUではCUDA、Apple SiliconではCoreMLを使用。

2. Demucs(代替)

Facebookのhtdemucsモデルを直接使用:

model = get_model("htdemucs")
sources = apply_model(model, wav_scaled[None], device=device, shifts=1, overlap=0.25)[0]
vocals = sources[vocals_idx]
instrumental = wav.to(device) - vocals

Demucsは4ステム分離だが、Nightingaleはボーカルのみ抽出し原本 - ボーカル = 伴奏で計算。

RustからPython MLを管理する方法

アーキテクチャで最も巧妙な部分。Pythonスクリプト10個がコンパイル時にRustバイナリに埋め込まれる

const STEMS_PY: &str = include_str!("../analyzer/stems.py");

初回起動時に~/.nightingale/vendor/analyzer/に展開。Rustが永続的Pythonサーバープロセスをスポーンし、stdin/stdout JSONプロトコルで通信。

サーバーが永続的なのでWhisperXモデルが曲間でメモリに残る。CUDA OOM発生時はサーバーをkillしてGPUをクリーンにした後再スポーン。

処理パイプライン

pipeline.py::run_pipeline()がオーケストレーション:
1. キー検出 — FFTベースKrumhansl-Schmucklerアルゴリズム
2. ステム分離 — UVR KaraokeまたはDemucs
3. 歌詞 — LRCLIBを先に検索、なければWhisperXで転写
4. キャッシュ — blake3ハッシュベース、未変更ファイルは再分析不要

歌詞同期

LRCLIB優先検索: lrclib.net APIを検索、アルバム一致+再生時間近接度でランキング。

WhisperX転写(歌詞がない場合):
1. RMSエネルギーでボーカル区間検出
2. 80Hzハイパスフィルター+RMS正規化
3. マルチウィンドウ言語検出(30秒×4ウィンドウ、投票)
4. 転写+強制アラインメントで単語単位タイムスタンプ
5. 脱落単語復旧+タイムスタンプ補間
6. 表示セグメント構成(1行最大10語)

ハルシネーションフィルタリングでWhisper特有の幻覚フレーズを除去。

OOMフォールバック:GPU試行→モデル解放後GPU再試行→CPUフォールバック。

リアルタイムピッチ検出

純粋Rust実装、Python不要。client/src-tauri/src/microphones.rs

  • cpalでクロスプラットフォームマイク入力

  • McLeod Pitch Detectionアルゴリズム

  • 2048サンプルウィンドウ、80-1000Hz範囲

  • ~40HzでTauriイベントシステム経由でReactフロントエンドにイベント発信

  • フロントエンドが検出ピッチと期待ピッチを比較しスコア計算

自己完結型バイナリブートストラップ

初回起動時6ステップ:FFmpegダウンロード→uvダウンロード→Python 3.10インストール→venv作成→MLパッケージインストール→埋め込みスクリプト展開。

GPU検出:macOS ARM→MPS、NVIDIA→CUDA(compute capでcu126/cu128選択)、それ以外→CPU。

CJK歌詞の制限

WhisperXの単語アラインメントがline_text.split()で単語分割するため、スペースのない日本語では動作しない。韓国語はスペースがあるので相対的にマシだが、同期精度は言語によって差が大きい。

動作フロー

1

Pythonスクリプトをinclude_str!()でRustバイナリにコンパイル時埋め込み

2

永続的PythonサーバープロセスをstdinN/stdout JSONプロトコルで制御

3

UVR Mel-Band RoFormerでリードボーカルのみ分離(バッキングボーカルは伴奏に残留)

4

LRCLIB APIで既存歌詞検索→なければWhisperX転写+強制アラインメント

5

McLeod Pitch Detectionを純粋Rustで~40Hz周期実行、Tauriイベントでフロントエンド送信

6

初回起動時にuvでPython 3.10+PyTorch+MLモデルを自動ブートストラップ

ユースケース

Rust↔Python ML統合 — include_str!埋め込み+永続プロセスパターン 音声分離実装 — UVR vs Demucs選択基準とONNX Runtime活用 自己完結型デスクトップアプリ — uv+自動ブートストラップでML依存性管理