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()で単語分割するため、スペースのない日本語では動作しない。韓国語はスペースがあるので相対的にマシだが、同期精度は言語によって差が大きい。
動作フロー
Pythonスクリプトをinclude_str!()でRustバイナリにコンパイル時埋め込み
永続的PythonサーバープロセスをstdinN/stdout JSONプロトコルで制御
UVR Mel-Band RoFormerでリードボーカルのみ分離(バッキングボーカルは伴奏に残留)
LRCLIB APIで既存歌詞検索→なければWhisperX転写+強制アラインメント
McLeod Pitch Detectionを純粋Rustで~40Hz周期実行、Tauriイベントでフロントエンド送信
初回起動時にuvでPython 3.10+PyTorch+MLモデルを自動ブートストラップ