Rust + UniFFI で macOS 動画エディターを作る: ソロファウンダーのアーキテクチャ
私は NexClip AI—— macOS 向け、長尺クリエイター用の AI 動画エディターを作っています。ソロファウンダーとして、コアエンジンは一度書いて複数プラットフォームで共有する必要がありました。それを可能にしたのが Rust + UniFFI です。

課題
動画編集者には大きなボトルネックがあります —— 長尺の素材からハイライトクリップを特定すること。45 分の講義動画なら、手作業で丸 1 日かかることもあります —— 観て、トピックを特定し、トランスクリプトを読み、タイムスタンプをマッピングし、クリーンなクリップを作る。
NexClip AI はこれをパイプラインで自動化します:
- 文字起こし —— 単語レベルのタイムスタンプ付き音声認識
- NLP 処理 —— 日本語 / 英語の文・文節分割
- タイムコード補正 —— 精度向上のためのセグメント境界の調整
- クリッププランニング —— 選択されたトピックからクリップを生成
ステップ 2〜4 はコンピュート負荷が高く、オンデバイス実行が必要でした。これを効率よく処理でき、かつどこでも動く言語が必要でした。
なぜ Rust か?
妥協のないパフォーマンス
タイムコード補正は音声 RMS データを解析し、無音区間を検出し、境界をリアルタイムで調整します。Rust はランタイムオーバーヘッドゼロでネイティブコードにコンパイルされます。
メモリ安全性の保証
単語レベルのタイミングデータは、精密な浮動小数境界を持つ数千の struct を扱います。Rust の所有権モデルは、C/C++ で私を悩ませたであろうバグを未然に防ぎます。
一度書いて、どこでも動かす
同じ Rust クレートが、macOS/iOS 用の静的ライブラリ(UniFFI 経由)、Web 用の WASM モジュール、Node.js 用のネイティブアドオン(Napi 経由)にコンパイルされます。1 つの実装、3 つのプラットフォーム。
クレートアーキテクチャ
crates/ ├── kirinuki-core # Shared types & error definitions ├── kirinuki-timecode # Timecode correction engine ├── kirinuki-clip # Clip planning & optimization ├── kirinuki-nlp # Japanese NLP engine ├── kirinuki-nlp-english # English NLP engine ├── kirinuki-ffi-swift # Swift bindings (UniFFI) └── kirinuki-ffi-node # Node.js bindings (Napi)
各クレートは単一の責務を持ちます。FFI クレートはプラットフォーム型とコア型の変換を行う薄いラッパー —— ビジネスロジックは FFI レイヤーには置きません。
コア型: 基盤
pub struct Segment {
pub start: f64, // seconds
pub end: f64,
pub text: String,
pub confidence: Option<f64>,
pub words: Vec<Word>,
}
pub struct Word {
pub start: f64,
pub end: f64,
pub word: String,
pub confidence: Option<f64>,
}これらの型がパイプライン全体を流れます —— 文字起こしから最終的なクリッププランまで。シンプルでフラットで、FFI 境界を越えたシリアライズも簡単です。
タイムコード補正: 精度が物を言う領域
音声認識のタイムスタンプは近似値です。2.34 秒と報告された単語が、実際には 2.31 秒から始まっているかもしれない。数百単語も重なればエラーが累積します。
タイムコード補正エンジンはセグメントを複数の精緻化ステージに通します —— 音声特性を解析し、境界を自然な区切り点に揃え、衝突を解決し —— ミリ秒レベルの精度を達成します。
pub fn adjust_all_segments(
segments: &[Segment],
options: &AdjustOptions,
) -> AdjustmentResult結果:セグメント境界は数ミリ秒の精度。
日本語 NLP: GiNZA から Vibrato へ
当初は Python ベースの NLP ライブラリ(GiNZA)を Docker コンテナで動かしていました。問題は明らかでした:
- • コールドスタート:約 10 秒
- • Python ランタイムが必要
- • ネットワーク往復のオーバーヘッド
- • オンデバイスで動かせない
Vibrato—— ピュア Rust の形態素解析器 —— への移行で、Docker・Python・ネットワーク依存を完全に排除しました。日本語の文分割、文節検出、品詞タグ付けはすべてアプリ内でネイティブに動きます。
UniFFI: Swift へのブリッジ

UniFFI(Mozilla 製)は Rust コードから Swift バインディングを自動生成します。C ヘッダーを手書きする必要はありません。
#[derive(Debug, Clone, uniffi::Record)]
pub struct SwiftWord {
pub start: f64,
pub end: f64,
pub word: String,
pub confidence: Option<f64>,
}
#[uniffi::export]
pub fn correct_timecodes(
segments: Vec<SwiftSegment>,
options: Option<SwiftAdjustOptions>,
) -> SwiftAdjustmentResult {
let core_segments = segments.into_iter()
.map(Into::into).collect();
let core_options = options
.map(Into::into).unwrap_or_default();
let result = kirinuki_timecode::adjust_all_segments(
&core_segments, &core_options,
);
result.into()
}UniFFI はネイティブな Swift 型を含む .swift ファイル、FFI 境界用の C ヘッダー、そして Xcode 用のモジュールマップを生成します。生成された Swift コードは呼び出し側から見て完全にネイティブ —— UnsafePointer は一切見えません。
ビルドパイプライン
# 1. Compile Rust → static library cargo build --release -p kirinuki-ffi-swift \ --target aarch64-apple-darwin # 2. Generate Swift bindings cargo run -p kirinuki-ffi-swift --bin uniffi-bindgen \ -- generate \ --library target/.../libkirinuki_ffi_swift.dylib \ --language swift --out-dir Sources # 3. Package as XCFramework xcodebuild -create-xcframework \ -library target/.../libkirinuki_ffi_swift.a \ -headers Headers \ -output KirinukiCore.xcframework
XCFramework は SPM のバイナリターゲットとして消費されます:
let package = Package(
name: "KirinukiCore",
platforms: [.macOS(.v13), .iOS(.v16)],
targets: [
.binaryTarget(
name: "KirinukiCoreFFI",
path: "KirinukiCore.xcframework"
),
.target(
name: "KirinukiCore",
dependencies: ["KirinukiCoreFFI"]
),
]
)Swift 側: Actor ベースのサービス
各 Rust モジュールは Swift Actor にマッピングされます:
actor TimecodeService {
func correctTimecodes(
_ segments: [Segment],
options: SwiftAdjustOptions? = nil
) async -> CorrectionResult {
await Task.detached(priority: .userInitiated) {
let swiftSegments = segments
.map { $0.toSwiftSegment() }
let result = KirinukiCore.correctTimecodes(
segments: swiftSegments,
options: options
)
return CorrectionResult(from: result)
}.value
}
}Actor がスレッドセーフティを提供します。Task.detached は重い計算をメインスレッドから外し、UI の応答性を保ちます。
得られた教訓
UniFFI の proc macros > UDL ファイル
proc macros なら型定義が Rust コード内に留まります —— 別ファイルのスキーマを管理する必要がありません。
FFI 型はフラットに保つ
ネストした enum や複雑なジェネリクスは FFI 境界をうまく越えません。nullable フィールドにはシンプルな struct と Option<T> を使いましょう。
重いリソースにはシングルトンパターン
NLP 辞書は大きい。起動時に 1 度ロードし、セッション中は使い回しましょう。
双方向変換は避けられない
すべての型にコア型への変換用の From<SwiftType> とその逆が必要になります。ボイラープレートですが、コアロジックをクリーンかつプラットフォーム非依存に保てます。
Rust レイヤー単体でテストする
Rust のユニットテストはミリ秒で走ります。アルゴリズムの正しさを確かめるのに Xcode のビルドを待つ必要はありません。
結果
7
Rust クレート
70+
エクスポート関数
0
unsafe Swift コード
1
ビルドスクリプト
ソロファウンダーとして、Rust + UniFFI はパフォーマンスクリティカルなコードを一度書いてどこにでも出荷することを可能にしてくれます。初期セットアップのコストは確かにありますが、見返りは絶大です。本格的な計算を必要とするネイティブアプリにとって、Rust + UniFFI は驚くほど痛みの少ないブリッジを提供してくれます。
よくある質問
コアエンジンに Swift ではなく Rust を使う理由は?
Rust はメモリ安全性、ゼロコスト抽象化、クロスプラットフォームのコンパイルを提供します。同じクレートが macOS/iOS、Web(WASM)、Node.js にコンパイルされます。コアを Rust で一度書くことで、プラットフォームごとに別実装を保守する手間が省けます。
UniFFI とは何か、Swift とどう連携するのか?
UniFFI は Rust コードから Swift バインディングを自動生成する Mozilla プロジェクトです。Rust の struct や関数に UniFFI の proc macros を注釈すると、ネイティブな Swift 型、C ヘッダー、Xcode 用のモジュールマップが生成されます。手動のブリッジは不要です。
NexClip AI は Rust クレートを何個使っている?
7 つです:kirinuki-core(共通型)、kirinuki-timecode(タイムコード補正)、kirinuki-clip(クリッププランニング)、kirinuki-nlp(日本語 NLP)、kirinuki-nlp-english(英語 NLP)、kirinuki-ffi-swift(Swift バインディング)、kirinuki-ffi-node(Node.js バインディング)。合わせて UniFFI 経由で 70+ の関数をエクスポートしています。
NexClip AI を試す
macOS 版、無料。動画を読み込んで、AI が抽出したすべてのトピックを見て、重要なものを選ぶ。
macOS 版を無料で試す
NexClip AI
Topic-Based Editing: Pick your topics. Get your clips.
Related Articles
Topic-Based Editing for Educators
Use CaseTopic-Based Editing for Podcasters
Comparisonvs AI Auto-Clipping (OpusClip)
Comparisonvs Text-Based Editing (Descript)
Comparisonvs Munch & Vizard
Comparisonvs Chat-Based Editing (Riverside, Cutback)
Comparisonvs Prompt-Based Editing
Comparison7 Best OpusClip Alternatives for Long-Form Video