Files
momentry_core_0_1/docs/RUST_DEVELOPMENT.md
accusys 9bbfaa1d06 docs: Add Python and Node.js integration specification
- Architecture overview for mixed runtime environments
- Python version management with venv
- Node.js version locking for n8n
- Isolation principles for coexisting environments
- Workflow integration patterns
- Monitoring configuration
- Troubleshooting guide
- New service decision tree
2026-03-16 15:22:39 +08:00

21 KiB
Raw Blame History

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 定義資料庫、操作器等介面
  • 依賴注入: 透過建構函式注入依賴
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 匯入順序

// 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
# 格式化
cargo fmt

# 檢查格式
cargo fmt -- --check

# Lint
cargo clippy --all-features

3. 錯誤處理

3.1 錯誤類型選擇

情境 錯誤類型 原因
應用程式 anyhow::Result<T> 提供靈活的錯誤傳播
函式庫 thiserror 定義明確的錯誤類型
API 錯誤 自定義 Error enum 提供客戶端錯誤碼

3.2 錯誤處理範例

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 自定義錯誤 (適用於函式庫)

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 配置

// Cargo.toml
tokio = { version = "1", features = ["full"] }

4.2 Async Trait

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 避免常見陷阱

// ❌ 錯誤: 在同步上下文中調用 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) 時:

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 腳本
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 端解析
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

# 建立虛擬環境
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

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 隔離:

<!-- 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/5690, API: 另行分配

6.4.2 環境變數隔離

# 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

// 觸發 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 獨立監控腳本

# monitor/service/node_monitor.sh
# 監控 n8n Node.js 版本

# monitor/service/python_monitor.sh  
# 監控 Python 腳本執行狀態

6.6.2 健康檢查

# 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 腳本)

# 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 診斷命令

# 檢查 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.mddocs/PYTHON.md

5.2 進度回報

透過 stderr 回報進度,供 Rust 端解析:

# 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 端解析
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 單元測試

#[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 整合測試

#[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 物件模擬外部依賴
#[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!
  • 結構化日誌: 使用訊息 + 欄位
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 初始化日誌

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 避免拷貝

// ❌ 拷貝
let data = record.clone();

// ✅ 引用
fn process(data: &Data) { }

// ✅ 或使用 Arc 共用
use std::sync::Arc;
let shared = Arc::new(data);

8.2 批量操作

// ❌ 逐筆插入
for item in items {
    db.insert(&item).await?;
}

// ✅ 批量插入
db.insert_batch(&items).await?;

8.3 連線池

// 使用 sqlx 連線池
let pool = SqlxPool::connect(&DATABASE_URL).await?;
let db = PostgresDb::new(pool);

10. 安全考量

9.1 敏感資訊

  • 不要將密碼、API Key 寫入程式碼
  • 使用環境變數或設定檔
  • .env 檔案加入 .gitignore
// ❌ 硬編碼密碼
let password = "secret123";

// ✅ 使用環境變數
let password = std::env::var("DATABASE_PASSWORD")
    .context("DATABASE_PASSWORD must be set")?;

9.2 命令注入

// ❌ 危險: 直接使用使用者輸入
let cmd = format!("ffprobe {}", user_input);

// ✅ 安全: 使用參數化
Command::new("ffprobe")
    .arg(user_input) // 自動轉義
    .output();

11. 文件編寫

10.1 結構體/函式文件

/// 代表一個影片記錄
///
/// # 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 文件

/// 取得影片記錄
///
/// # 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

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 錯誤處理

match &cli.command {
    Commands::Register { path } => {
        if !Path::new(path).exists() {
            eprintln!("Error: File not found: {}", path);
            std::process::exit(1);
        }
        // ...
    }
}

13. 依賴管理

12.1 版本約束

# Cargo.toml
[dependencies]
anyhow = "1.0"           # 精確版本
tokio = { version = "1", features = ["full"] }  # 範圍版本
serde = "1.0"           # 精確版本

12.2 避免依賴地獄

  • 審查依賴數量
  • 優先使用標準庫
  • 選擇維護良好的套件

14. 建構與部署

13.1 建構命令

# 開發建構
cargo build

# 發布建構
cargo build --release

# 單一二進制
cargo build --bin momentry

13.2 檢查清單

# 格式化
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/*: 重構

附錄: 快速參考

建構

cargo build --release
cargo run -- --help

品質檢查

cargo fmt -- --check
cargo clippy --all-features
cargo check --all-features
cargo test

依賴

cargo add <package>
cargo tree
cargo outdated