# 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. 外部程序整合 當需要使用 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. Python 與 Node.js 混用規範 本專案同時使用 Python 和 Node.js (n8n),需建立明確的版本隔離與管理規範。 ### 6.1 架構概述 ``` ┌─────────────────────────────────────────────────────────────┐ │ Momentry Core │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Rust │ │ Python │ │ Node.js │ │ │ │ (Core) │───▶ │ (Scripts) │ │ (n8n) │ │ │ │ │ │ │ │ │ │ │ │ - CLI │ │ - ASR │ │ - Workflow │ │ │ │ - DB │ │ - Thumb │ │ - API │ │ │ │ - Storage │ │ - OCR │ │ - Webhooks │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 資料庫 / 檔案系統 / Qdrant │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### 6.2 Python 版本管理 #### 6.2.1 版本鎖定 | 版本 | 用途 | 路徑 | |------|------|------| | 3.11.14 | 影片處理腳本 | `/opt/homebrew/bin/python3.11` | #### 6.2.2 虛擬環境 使用專案隔離的 venv: ```bash # 建立虛擬環境 cd /Users/accusys/momentry_core_0.1 python3.11 -m venv venv # 啟用 source venv/bin/activate # 安裝依賴 pip install faster-whisper opencv-python python-dotenv ``` #### 6.2.3 Rust 呼叫 Python ```rust 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) .output() .context("Failed to run Python script")?; ``` ### 6.3 Node.js 版本管理 #### 6.3.1 版本鎖定 參考 `docs/NODEJS.md`: | 版本 | 用途 | 路徑 | |------|------|------| | 22.22.1 | n8n | `/opt/homebrew/opt/node@22/bin/node` | #### 6.3.2 n8n 服務配置 使用 launchd plist 隔離: ```xml ProgramArguments /opt/homebrew/opt/node@22/bin/node /opt/homebrew/lib/node_modules/n8n/bin/n8n start EnvironmentVariables PATH /opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:... ``` ### 6.4 Python + Node.js 共存原則 #### 6.4.1 隔離原則 | 原則 | 說明 | |------|------| | **獨立路徑** | Python 用 venv 路徑,Node.js 用 node@22 路徑 | | **獨立環境** | n8n 服務使用 launchd plist,不與 Rust 共享環境 | | **明確版本** | 所有腳本明確指定直譯器路徑 | | **PORT 分配** | n8n: 5678/5690, API: 另行分配 | #### 6.4.2 環境變數隔離 ```bash # Rust 專案 .env DATABASE_URL=postgres://... # n8n plist N8N_ENCRYPTION_KEY=xxx N8N_BASIC_AUTH_ACTIVE=true # 勿混用,避免 Rust 讀到 n8n 環境變數 ``` ### 6.5 工作流程整合 #### 6.5.1 Rust → Python ``` Rust CLI ──▶ Python Script ──▶ JSON Output ──▶ Rust Parse │ │ └── venv/bin/python └── faster-whisper ``` #### 6.5.2 Rust → n8n Webhook ```rust // 觸發 n8n workflow use reqwest; pub async fn trigger_n8n_webhook(webhook_url: &str, payload: &str) -> Result<()> { let client = reqwest::Client::new(); client.post(webhook_url) .json(payload) .send() .await .context("Failed to trigger n8n webhook")?; Ok(()) } ``` #### 6.5.3 n8n → Rust API ``` n8n Workflow ──▶ HTTP Request Node ──▶ Rust API Server │ ┌───────┴───────┐ │ axum server │ │ /api/webhook │ └───────────────┘ ``` ### 6.6 監控配置 #### 6.6.1 獨立監控腳本 ```bash # monitor/service/node_monitor.sh # 監控 n8n Node.js 版本 # monitor/service/python_monitor.sh # 監控 Python 腳本執行狀態 ``` #### 6.6.2 健康檢查 ```yaml # monitor_config.yaml services: - name: "n8n" type: "http" port: 5678 check_url: "http://localhost:5678/" - name: "Python Scripts" type: "process" check: "pgrep -f asr_processor.py" ``` ### 6.7 排程管理 #### 6.7.1 備份排程 (Python 腳本) ```bash # crontab 0 3 * * * /Users/accusys/momentry/scripts/backup_all.sh ``` #### 6.7.2 n8n 工作流排程 - 由 n8n 內建排程節點管理 - 不與 crontab 衝突 ### 6.8 故障排除 #### 6.8.1 常見問題 | 問題 | 原因 | 解決方案 | |------|------|----------| | n8n 版本警告 | 使用 Node 25.x | 確認 plist 使用 node@22 | | Python 腳本找不到模組 | 未啟用 venv | 使用 venv/bin/python | | 執行權限錯誤 | shebang 錯誤 | 確認 #!/opt/homebrew/bin/python3.11 | | Port 被佔用 | 多個服務使用相同 port | 分配獨立 port | #### 6.8.2 診斷命令 ```bash # 檢查 Python 版本 which python /opt/homebrew/bin/python3.11 --version # 檢查 Node.js 版本 /opt/homebrew/opt/node@22/bin/node --version # 檢查 n8n 程序 ps aux | grep n8n # 檢查 Python 程序 ps aux | grep python # 檢查 Port 佔用 lsof -i :5678 # n8n ``` ### 6.9 新增服務決策 ``` 新服務需要哪種執行環境? │ ├─ Python 腳本 ──▶ 使用專案 venv │ (路徑: venv/bin/python) │ ├─ Node.js 工具 ──▶ 評估版本需求 │ │ │ ├─ 支援 Node 22 ──▶ 使用 node@22 │ │ │ └─ 需要其他版本 ──▶ 安裝新版本 (如 node@20) │ └─ 現有服務依賴 ──▶ 根據現有服務配置 ``` ### 6.10 文件維護 當新增 Python 或 Node.js 服務時: 1. 更新本文檔的版本表格 2. 建立對應的監控腳本 3. 如需 launchd plist,建立並加入 `momentry_runtime/plist/` 4. 更新 `docs/NODEJS.md` 或 `docs/PYTHON.md` ### 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); } } ``` ## 7. 測試策略 ### 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()) } } } ``` ## 8. 日誌與監控 ### 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(); } ``` ## 9. 性能優化 ### 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); ``` ## 10. 安全考量 ### 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(); ``` ## 11. 文件編寫 ### 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>; ``` ## 12. 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); } // ... } } ``` ## 13. 依賴管理 ### 12.1 版本約束 ```toml # Cargo.toml [dependencies] anyhow = "1.0" # 精確版本 tokio = { version = "1", features = ["full"] } # 範圍版本 serde = "1.0" # 精確版本 ``` ### 12.2 避免依賴地獄 - 審查依賴數量 - 優先使用標準庫 - 選擇維護良好的套件 ## 14. 建構與部署 ### 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 ``` ## 15. 版本控制 ### 14.1 提交訊息規範 ``` ():