# Rust 開發規範 - Momentry Core 本規範定義 Momentry Core 專案的 Rust 開發標準,確保程式碼品質與一致性。 ## 1. 專案結構 ### 1.1 目錄架構 ``` src/ ├── main.rs # CLI 入口點 ├── lib.rs # 函式庫導出 ├── cli/ │ ├── mod.rs │ └── commands/ # CLI 命令模組 ├── core/ │ ├── mod.rs │ ├── chunk/ # 影片分段邏輯 │ │ ├── mod.rs │ │ ├── splitter.rs │ │ └── types.rs │ ├── db/ # 資料庫抽象層 │ │ ├── mod.rs │ │ ├── postgres_db.rs │ │ ├── mongodb_db.rs │ │ ├── redis_db.rs │ │ └── qdrant_db.rs │ ├── processor/ # 影片處理器 │ │ ├── mod.rs │ │ ├── asr.rs # 語音識別 │ │ ├── asrx.rs # 說話者分離 │ │ ├── ocr.rs # 文字辨識 │ │ ├── yolo.rs # 物件偵測 │ │ ├── face.rs # 人臉偵測 │ │ └── pose.rs # 姿態估計 │ ├── embedding/ # 向量嵌入 │ ├── probe/ # ffprobe 整合 │ ├── storage/ # 檔案管理 │ └── thumbnail/ # 縮圖生成 ├── api/ # HTTP API │ ├── mod.rs │ └── routes/ ├── player/ # 影片播放 └── watcher/ # 檔案監控 ``` ### 1.2 模組設計原則 - **單一職責**: 每個模組專注於一項功能 - **介面抽象**: 使用 trait 定義資料庫、操作器等介面 - **依賴注入**: 透過建構函式注入依賴 ```rust pub trait VideoProcessor: Send + Sync { async fn process(&self, video_path: &str) -> Result; } ``` ## 2. 程式碼風格 ### 2.1 命名規範 | 類型 | 規範 | 範例 | |------|------|------| | 結構體/列舉 | PascalCase | `VideoRecord`, `ChunkType` | | 函式/變數 | snake_case | `get_video_by_uuid` | | Trait | PascalCase + er 尾碼 | `Database`, `ChunkStore` | | 檔案 | snake_case | `postgres_db.rs` | | 常量 | SCREAMING_SNAKE_CASE | `MAX_CHUNK_SIZE` | | 模組 | snake_case | `chunk`, `processor` | ### 2.2 匯入順序 ```rust // 1. 標準庫 use std::path::Path; use std::process::Command; // 2. 外部庫 use anyhow::{Context, Result}; use async_trait::async_trait; use serde::{Deserialize, Serialize}; use tokio::fs; // 3. 內部模組 use crate::core::chunk::Chunk; use crate::core::db::PostgresDb; ``` ### 2.3 行寬與格式 - 最大行寬: 100 字元 - 使用 4 空格縮排 - 啟用 clippy 與 fmt ```bash # 格式化 cargo fmt # 檢查格式 cargo fmt -- --check # Lint cargo clippy --all-features ``` ## 3. 錯誤處理 ### 3.1 錯誤類型選擇 | 情境 | 錯誤類型 | 原因 | |------|----------|------| | 應用程式 | `anyhow::Result` | 提供靈活的錯誤傳播 | | 函式庫 | `thiserror` | 定義明確的錯誤類型 | | API 錯誤 | 自定義 Error enum | 提供客戶端錯誤碼 | ### 3.2 錯誤處理範例 ```rust use anyhow::{Context, Result, bail}; fn process_video(video_path: &str) -> Result { // 使用 context 提供錯誤上下文 let output = Command::new("ffprobe") .args(["-v", "quiet", "-print_format", "json", "-show_format", video_path]) .output() .context("Failed to run ffprobe")?; // 使用 bail 進行早期返回 if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!("ffprobe failed: {}", stderr); } // 解析輸出 let metadata: Metadata = serde_json::from_slice(&output.stdout) .context("Failed to parse ffprobe output")?; Ok(metadata) } ``` ### 3.3 自定義錯誤 (適用於函式庫) ```rust use thiserror::Error; #[derive(Error, Debug)] pub enum VideoError { #[error("Video not found: {0}")] NotFound(String), #[error("Invalid codec: {0}")] InvalidCodec(String), #[error("Processing failed: {0}")] ProcessingError(#[from] std::io::Error), } ``` ## 4. 异步編程 ### 4.1 Tokio 配置 ```rust // Cargo.toml tokio = { version = "1", features = ["full"] } ``` ### 4.2 Async Trait ```rust use async_trait::async_trait; #[async_trait] pub trait Database: Send + Sync { async fn init() -> Result where Self: Sized; async fn get_video(&self, uuid: &str) -> Result>; async fn store_chunk(&self, chunk: &Chunk) -> Result<()>; } ``` ### 4.3 避免常見陷阱 ```rust // ❌ 錯誤: 在同步上下文中調用 async 函式 fn bad_example() { let result = db.get_video("xxx"); // 編譯錯誤 } // ✅ 正確: 使用 #[tokio::main] #[tokio::main] async fn main() { let result = db.get_video("xxx").await; } // ❌ 錯誤: 阻塞執行緒池 async fn bad_practice() { let data = std::fs::read_to_string("file.txt").unwrap(); // 阻塞 } // ✅ 正確: 使用 tokio::fs async fn good_practice() { let data = tokio::fs::read_to_string("file.txt").await.unwrap(); } ``` ## 5. 外部程序整合 ### 5.1 Python 腳本呼叫 當需要使用 Python 生態系工具 (如 faster-whisper, YOLO) 時: ```rust pub async fn process_asr(video_path: &str, output_path: &str) -> Result { let script_path = Path::new(env!("CARGO_MANIFEST_DIR")) .join("scripts") .join("asr_processor.py"); // 使用 venv 中的 Python,確保版本隔離 let venv_python = Path::new(env!("CARGO_MANIFEST_DIR")) .join("venv") .join("bin") .join("python"); // 執行腳本 let output = Command::new(venv_python) .arg(script_path) .arg(video_path) .arg(output_path) .output() .context("Failed to run ASR processor")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); bail!("ASR failed: {}", stderr); } // 讀取輸出 let json_str = std::fs::read_to_string(output_path) .context("Failed to read ASR output")?; let result: AsrResult = serde_json::from_str(&json_str) .context("Failed to parse ASR output")?; Ok(result) } ``` ### 5.2 進度回報 透過 stderr 回報進度,供 Rust 端解析: ```python # Python 腳本 import sys print(f"ASR_START", file=sys.stderr) print(f"ASR_LANGUAGE:{detected_lang}", file=sys.stderr) print(f"ASR_PROGRESS:{count}", file=sys.stderr) print(f"ASR_COMPLETE:{total}", file=sys.stderr) ``` ```rust // Rust 端解析 let stderr = String::from_utf8_lossy(&output.stderr); for line in stderr.lines() { if line.starts_with("ASR_PROGRESS:") { let count = line.trim_start_matches("ASR_PROGRESS:"); println!("[ASR] Processed {} segments...", count); } } ``` ## 6. 測試策略 ### 6.1 單元測試 ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_chunk_creation() { let chunk = Chunk::new( "test-uuid".to_string(), 0, ChunkType::Sentence, 0.0, 10.0, serde_json::json!({"text": "Hello"}), ); assert_eq!(chunk.uuid, "test-uuid"); assert_eq!(chunk.chunk_type, ChunkType::Sentence); } } ``` ### 6.2 整合測試 ```rust #[cfg(test)] mod integration { use super::*; #[tokio::test] async fn test_database_connection() { let db = PostgresDb::init().await.unwrap(); let videos = db.list_videos().await.unwrap(); assert!(videos.len() >= 0); } } ``` ### 6.3 測試資料 - 使用測試資料庫隔離測試環境 - 避免在測試中使用真實敏感資料 - 使用 mock 物件模擬外部依賴 ```rust #[cfg(test)] mod mocks { pub struct MockVideoProcessor { pub result: AsrResult, } impl VideoProcessor for MockVideoProcessor { async fn process(&self, _video_path: &str) -> Result { Ok(self.result.clone()) } } } ``` ## 7. 日誌與監控 ### 7.1 日誌規範 - **使用 tracing**: 不要使用 `println!` - **結構化日誌**: 使用訊息 + 欄位 ```rust use tracing::{info, warn, error}; fn process_video(uuid: &str) -> Result<()> { info!(uuid = uuid, "Starting video processing"); match do_processing(uuid) { Ok(_) => info!(uuid = uuid, "Processing completed"), Err(e) => { error!(uuid = uuid, error = %e, "Processing failed"); return Err(e); } } } ``` ### 7.2 初始化日誌 ```rust use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; fn init_logging() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "momentry_core=info,tokio=warn".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); } ``` ## 8. 性能優化 ### 8.1 避免拷貝 ```rust // ❌ 拷貝 let data = record.clone(); // ✅ 引用 fn process(data: &Data) { } // ✅ 或使用 Arc 共用 use std::sync::Arc; let shared = Arc::new(data); ``` ### 8.2 批量操作 ```rust // ❌ 逐筆插入 for item in items { db.insert(&item).await?; } // ✅ 批量插入 db.insert_batch(&items).await?; ``` ### 8.3 連線池 ```rust // 使用 sqlx 連線池 let pool = SqlxPool::connect(&DATABASE_URL).await?; let db = PostgresDb::new(pool); ``` ## 9. 安全考量 ### 9.1 敏感資訊 - **不要**將密碼、API Key 寫入程式碼 - 使用環境變數或設定檔 - .env 檔案加入 .gitignore ```rust // ❌ 硬編碼密碼 let password = "secret123"; // ✅ 使用環境變數 let password = std::env::var("DATABASE_PASSWORD") .context("DATABASE_PASSWORD must be set")?; ``` ### 9.2 命令注入 ```rust // ❌ 危險: 直接使用使用者輸入 let cmd = format!("ffprobe {}", user_input); // ✅ 安全: 使用參數化 Command::new("ffprobe") .arg(user_input) // 自動轉義 .output(); ``` ## 10. 文件編寫 ### 10.1 結構體/函式文件 ```rust /// 代表一個影片記錄 /// /// # Fields /// * `id` - 資料庫 ID /// * `uuid` - 唯一識別碼 /// * `duration` - 影片時長 (秒) /// /// # Example /// ``` /// let video = VideoRecord { /// id: 1, /// uuid: "abc123".to_string(), /// duration: 120.5, /// }; /// ``` pub struct VideoRecord { pub id: i64, pub uuid: String, pub duration: f64, } ``` ### 10.2 API 文件 ```rust /// 取得影片記錄 /// /// # Arguments /// * `uuid` - 影片的 UUID /// /// # Returns /// * `Ok(Some(VideoRecord))` - 找到影片 /// * `Ok(None)` - 影片不存在 /// * `Err` - 資料庫錯誤 /// /// # Errors /// 如果資料庫連線失敗,返回資料庫錯誤 pub async fn get_video(&self, uuid: &str) -> Result>; ``` ## 11. CLI 命令設計 ### 11.1 命令結構 使用 clap derive: ```rust use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(name = "momentry")] #[command(about = "Digital asset management system")] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Register a video file Register { /// Path to video file path: String, }, /// Process video Process { /// UUID or path target: String, }, /// Generate chunks Chunk { /// Video UUID uuid: String, }, } ``` ### 11.2 錯誤處理 ```rust match &cli.command { Commands::Register { path } => { if !Path::new(path).exists() { eprintln!("Error: File not found: {}", path); std::process::exit(1); } // ... } } ``` ## 12. 依賴管理 ### 12.1 版本約束 ```toml # Cargo.toml [dependencies] anyhow = "1.0" # 精確版本 tokio = { version = "1", features = ["full"] } # 範圍版本 serde = "1.0" # 精確版本 ``` ### 12.2 避免依賴地獄 - 審查依賴數量 - 優先使用標準庫 - 選擇維護良好的套件 ## 13. 建構與部署 ### 13.1 建構命令 ```bash # 開發建構 cargo build # 發布建構 cargo build --release # 單一二進制 cargo build --bin momentry ``` ### 13.2 檢查清單 ```bash # 格式化 cargo fmt -- --check # Lint cargo clippy --all-features -- -D warnings # 類型檢查 cargo check --all-features # 測試 cargo test ``` ## 14. 版本控制 ### 14.1 提交訊息規範 ``` ():