Files
momentry_core/docs/RUST_DEVELOPMENT.md
accusys 383201cacd feat: Initial v0.9 release with API Key authentication
## v0.9.20260325_144654

### Features
- API Key Authentication System
- Job Worker System
- V2 Backup Versioning

### Bug Fixes
- get_processor_results_by_job column mapping

Co-authored-by: OpenCode
2026-03-25 14:53:41 +08:00

991 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Rust 開發規範 - Momentry Core
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-16 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
| V1.1 | 2026-03-21 | 新增 PythonExecutor 模組說明 | OpenCode | - |
---
本規範定義 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
│ │ ├── executor.rs # Python 腳本統一執行器 (含超時控制)
│ │ ├── 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<ProcessResult>;
}
```
## 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<T>` | 提供靈活的錯誤傳播 |
| 函式庫 | `thiserror` | 定義明確的錯誤類型 |
| API 錯誤 | 自定義 Error enum | 提供客戶端錯誤碼 |
### 3.2 錯誤處理範例
```rust
use anyhow::{Context, Result, bail};
fn process_video(video_path: &str) -> Result<VideoMetadata> {
// 使用 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<Self>
where Self: Sized;
async fn get_video(&self, uuid: &str) -> Result<Option<VideoRecord>>;
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<AsrResult> {
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);
}
}
```
### 5.3 PythonExecutor 統一執行器
使用 `PythonExecutor` 封裝 Python 腳本執行邏輯:
```rust
use momentry_core::core::processor::{PythonExecutor, validate_python_env};
// 驗證 Python 環境
fn init() -> Result<()> {
validate_python_env()?;
Ok(())
}
// 使用 Executor 執行腳本
async fn run_script() -> Result<()> {
let executor = PythonExecutor::new()?;
executor.run(
"asr_processor.py",
&["/path/to/video.mp4", "/path/to/output.json"],
Some("job-uuid"),
"ASR",
Some(Duration::from_secs(3600)), // 1小時超時
).await?;
Ok(())
}
```
#### Processor 超時設定
| Processor | 超時 | 說明 |
|----------|------|------|
| ASR | 1 小時 | 語音識別 |
| ASRx | 2 小時 | 說話者分離 |
| YOLO | 2 小時 | 物件偵測 |
| OCR | 2 小時 | 文字辨識 |
| Face | 2 小時 | 人臉偵測 |
| Pose | 2 小時 | 姿態估計 |
| Cut | 1 小時 | 場景偵測 |
---
## 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
<!-- com.momentry.n8n.main.plist -->
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/node@22/bin/node</string>
<string>/opt/homebrew/lib/node_modules/n8n/bin/n8n</string>
<string>start</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:...</string>
</dict>
```
### 6.4 Python + Node.js 共存原則
#### 6.4.1 隔離原則
| 原則 | 說明 |
|------|------|
| **獨立路徑** | Python 用 venv 路徑Node.js 用 node@22 路徑 |
| **獨立環境** | n8n 服務使用 launchd plist不與 Rust 共享環境 |
| **明確版本** | 所有腳本明確指定直譯器路徑 |
| **PORT 分配** | n8n: 5678/5679, 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<AsrResult> {
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<Option<VideoRecord>>;
```
## 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 提交訊息規範
```
<type>(<scope>): <subject>
<body>
<footer>
```
類型:
- `feat`: 新功能
- `fix`: 錯誤修復
- `docs`: 文件變更
- `style`: 格式調整
- `refactor`: 重構
- `test`: 測試變更
- `chore`: 建構/工具變更
範例:
```
feat(processor): Add ASR progress reporting
- Add stderr parsing for progress updates
- Support ASR_START, ASR_PROGRESS, ASR_COMPLETE markers
Closes #123
```
### 14.2 分支策略
- `main`: 穩定版本
- `feature/*`: 新功能開發
- `fix/*`: 錯誤修復
- `refactor/*`: 重構
---
## 附錄: 快速參考
### 建構
```bash
cargo build --release
cargo run -- --help
```
### 品質檢查
```bash
cargo fmt -- --check
cargo clippy --all-features
cargo check --all-features
cargo test
```
### 依賴
```bash
cargo add <package>
cargo tree
cargo outdated
```