From ea2bbb9fd94c784e6630c239b161cebc7048b83a Mon Sep 17 00:00:00 2001 From: accusys Date: Mon, 16 Mar 2026 15:17:31 +0800 Subject: [PATCH] docs: Add Rust development specification - Project structure and module design - Code style and naming conventions - Error handling patterns - Async programming guidelines - External process integration (Python scripts) - Testing strategy - Logging with tracing - Performance optimization - Security considerations - CLI command design - Version control guidelines --- docs/RUST_DEVELOPMENT.md | 651 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 651 insertions(+) create mode 100644 docs/RUST_DEVELOPMENT.md diff --git a/docs/RUST_DEVELOPMENT.md b/docs/RUST_DEVELOPMENT.md new file mode 100644 index 0000000..a5d3c5b --- /dev/null +++ b/docs/RUST_DEVELOPMENT.md @@ -0,0 +1,651 @@ +# 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 提交訊息規範 + +``` +(): + + + +