From e7eb90b987a6ae34433227c94fd8930c5594ca18 Mon Sep 17 00:00:00 2001 From: Accusys Date: Thu, 21 May 2026 16:30:27 +0800 Subject: [PATCH] docs: sync notes + identity_binding.rs traces pagination --- SYNC_V1.1.md | 78 +++++++++++++++++++++++++++++++++++++ src/api/identity_binding.rs | 37 +++++++++++++++--- 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 SYNC_V1.1.md diff --git a/SYNC_V1.1.md b/SYNC_V1.1.md new file mode 100644 index 0000000..2244a51 --- /dev/null +++ b/SYNC_V1.1.md @@ -0,0 +1,78 @@ +# Sync Notes 2026-05-21 + +## M5Max128 收到後需要做的事 + +```bash +cd ~/momentry_core +git pull origin main # 拉取所有變更 +cat SYNC_V1.1.md # 閱讀此文件 + +# 資料庫變更(必須先執行,否則 worker 會 fail) +psql -U accusys -d momentry -c "ALTER TABLE public.pre_chunks ALTER COLUMN coordinate_index SET DEFAULT 0;" + +# 重建 + 重啟 +cargo build --release --bin momentry +./run-server-3002.sh +``` + +--- + +## Bugs Fixed (13) + +| # | 問題 | 根因 | 修復 | +|---|------|------|------| +| 1 | `GET /identity/:uuid/files` 空資料 | SQL 缺 `REPLACE(uuid)` + 缺 `JOIN videos` | 改用 `REPLACE(uuid::text...)` + JOIN videos + `frame_number/fps` | +| 2 | `GET /identity/:uuid/faces` crash + 空 | `i64`/`INT4` 型別不符 + 硬編碼 NULL/0 | `id::bigint`、`confidence::float8` + 真實欄位 | +| 3 | `GET /identity/:uuid` crash | `IdentityDetailRecord.id` 是 `i64` 但 DB 是 `INT4` | `id::bigint as id` | +| 4 | `GET /file/:uuid/identities` 空 | 雙重 stub(handler + DB 都 `Vec::new()`) | 完整實作 + 正確 total count | +| 5 | `GET /identities/search?q=Louis` 500 | `c.text_content` NULL 但 Rust tuple 用 `String` | 改 `Option` | +| 6 | `POST /search/universal` person type first/last_time null | `search_persons_internal` 用 `timestamp_secs` | 改 `frame_number/fps` + JOIN videos | +| 7 | faces/files/chunks total 不正確 | `total: data.len()` | 獨立 COUNT 查詢 | +| 8 | `GET /identity/:uuid/traces` 無分頁 | 缺 page/page_size | 新增 `TracesQuery` + LIMIT/OFFSET | +| 9 | 身分比對 frame-level 不穩定 | frame-level Qdrant | 改 **trace-level**(AVG embedding per trace) | +| 10 | Charade face embedding 不在 Qdrant | 沒跑 `sync_face_embeddings` | match API 自動 push + ANN search | +| 11 | 無眼睛 face 推入 Qdrant | 無 QC 過濾 | `face_landmark_qc.py --apply` + Qdrant sync 過濾 `qc_ok` | +| 12 | TMDb 比對 dev/prod 不一致 | Qdrant ANN 不同 collection | trace-level 改善穩定性 | +| 13 | `faces/files/chunks total` 顯示 page_size | `total: data.len()` | 改為獨立 COUNT 查詢 | + +## ✨ 新功能 (6) + +| # | 功能 | 說明 | +|---|------|------| +| 1 | `POST /api/v1/tmdb/fetch` | 從 TMDb 下載 cast → 建立 identity + json + jpg + Qdrant | +| 2 | `POST /api/v1/agents/tmdb/match/:file_uuid` | 推 face → Qdrant ANN search → bind identity | +| 3 | `GET /api/v1/identity/:uuid/status` | 檢查 identity.json + profile.jpg 是否存在 | +| 4 | `/health` 新增 watcher/worker/時區 | `watcher_running`、`worker_running`、`system_timezone` | +| 5 | `SYSTEM_TIMEZONE` config | 自動偵測系統時區,可 `MOMENTRY_TIMEZONE` 覆蓋 | +| 6 | `GET /identity/:uuid/traces` 分頁 | `?page=1&page_size=20` | + +## 🔧 資料庫變更 + +```sql +-- 必須執行(否則 worker 的 CUT processor 會失敗) +ALTER TABLE public.pre_chunks ALTER COLUMN coordinate_index SET DEFAULT 0; + +-- 選擇性(face_landmark_qc.py --apply 需要) +ALTER TABLE public.face_detections ADD COLUMN metadata jsonb DEFAULT '{}'::jsonb; +``` + +## 🗑️ 清理 + +- 刪除 2,769 個孤兒 `person_xxx` identity(無 face_detections) +- `person_identities` + `person_appearances` table 已 DROP + +## 📂 主要檔案變更 + +| 檔案 | 說明 | +|------|------| +| `src/api/identity_api.rs` | identity detail/files/faces total 修正 + status endpoint | +| `src/api/identity_binding.rs` | traces 分頁(新增 `page`/`page_size`/`total`) | +| `src/api/server.rs` | health 新增 watcher/worker/system_timezone | +| `src/api/tmdb_api.rs` | **新檔案** — tmdb/fetch + match 端點 | +| `src/api/universal_search.rs` | person search 改 frame_number/fps | +| `src/core/config.rs` | 新增 SYSTEM_TIMEZONE | +| `src/core/db/qdrant_db.rs` | search_face_collection + sync_trace_embeddings + batch upsert | +| `src/core/db/postgres_db.rs` | get_identity_files/faces 修正 + get_file_identities 實作 | +| `src/core/tmdb/probe.rs` | extract_movie_name 改進(只取 `(` 前) | +| `scripts/face_landmark_qc.py` | 新增 `--apply` + `--schema` 參數 | +| `Cargo.toml` | reqwest 加 `gzip` feature | diff --git a/src/api/identity_binding.rs b/src/api/identity_binding.rs index ddfbf1d..f58d863 100644 --- a/src/api/identity_binding.rs +++ b/src/api/identity_binding.rs @@ -375,18 +375,31 @@ pub struct IdentityTracesResponse { pub success: bool, pub identity_uuid: String, pub name: String, - pub total_traces: usize, + pub total: usize, + pub page: usize, + pub page_size: usize, pub total_faces: i64, pub traces: Vec, } +#[derive(Debug, Deserialize)] +pub struct TracesQuery { + pub page: Option, + pub page_size: Option, +} + pub async fn get_identity_traces( State(state): State, Path(identity_uuid): Path, + Query(params): Query, ) -> Result, (StatusCode, String)> { let id_table = crate::core::db::schema::table_name("identities"); let fd_table = crate::core::db::schema::table_name("face_detections"); + let page = params.page.unwrap_or(1); + let page_size = params.page_size.unwrap_or(20); + let offset = ((page - 1) as i64) * (page_size as i64); + // Get identity name let identity: Option<(i32, String)> = sqlx::query_as(&format!( "SELECT id, name FROM {} WHERE uuid = $1::uuid", @@ -400,7 +413,7 @@ pub async fn get_identity_traces( let (identity_id, name) = identity.ok_or((StatusCode::NOT_FOUND, "Identity not found".to_string()))?; - // Get all traces for this identity across all files + // Get paginated traces for this identity across all files let rows: Vec<(String, i32, i64, i32, i32, f64, f64, f64)> = sqlx::query_as(&format!( r#"SELECT fd.file_uuid::text, fd.trace_id, COUNT(*)::bigint AS frame_count, @@ -412,15 +425,27 @@ pub async fn get_identity_traces( FROM {} fd WHERE fd.identity_id = $1 GROUP BY fd.file_uuid, fd.trace_id - ORDER BY fd.file_uuid, fd.trace_id"#, + ORDER BY fd.file_uuid, fd.trace_id + LIMIT $2 OFFSET $3"#, fd_table )) .bind(identity_id) + .bind(page_size as i64) + .bind(offset) .fetch_all(state.db.pool()) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - let total_traces = rows.len(); + // Get total count for pagination + let total: (i64,) = sqlx::query_as(&format!( + "SELECT COUNT(*) FROM (SELECT 1 FROM {} fd WHERE fd.identity_id = $1 GROUP BY fd.file_uuid, fd.trace_id) sub", + fd_table + )) + .bind(identity_id) + .fetch_one(state.db.pool()) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + let total_traces = total.0 as usize; let total_faces: i64 = rows.iter().map(|r| r.2).sum(); let traces: Vec = rows @@ -452,7 +477,9 @@ pub async fn get_identity_traces( success: true, identity_uuid, name, - total_traces, + total: total_traces, + page, + page_size, total_faces, traces, }))