Date: 2026-04-19 Status: COMPLETE — validated end-to-end
Migration 0004_phase2_entity_layer applied successfully.
| Table / Column | Change |
|---|---|
character |
New table (project_id, name, cluster_label, reference_count, confirmed) |
face_track |
New table (shot_id, asset_id, track_index, start_pts, end_pts, bbox_series JSONB, embedding ARRAY(Float) 512d, character_id FK, confidence, qdrant_point_id) |
object_track |
New table (shot_id, asset_id, track_index, label, confidence, start_pts, end_pts, bbox_series JSONB, crop_path) |
transcript_turn.character_id |
FK constraint added (column existed from Phase 1) |
asset.enrich_done |
Boolean flag added |
- Model: InsightFace
buffalo_l(installed: v0.7.3) - Runs on every keyframe per shot
- IoU-based greedy tracker across keyframes (ByteTrack was not installed —
lapbuild failure) - Per-track embedding: mean of per-frame 512d embeddings, L2-normalised
- Writes
FaceTrackrows, upserts toface_insightfaceQdrant collection (512d cosine) - Gated by
settings.face_detection_enabled(default:true) - Increments
enrich_barrier:{asset_id}Redis counter on completion
- Model: GroundingDINO SwinT-OGC
- Weights not present at
/workspace/weights/groundingdino/— stage skips gracefully - When weights are present: runs on representative keyframe per shot, writes
ObjectTrackrows, saves 224×224 crops tomedia-cropsMinIO bucket - Default detection labels:
person . camera . microphone . monitor . clapperboard - Gated by
settings.object_detection_enabled(default:true) and weights presence check - Increments
enrich_barrier:{asset_id}Redis counter on completion (even when skipping)
- CPU-only; runs in the
ingestworker - Per-shot metrics:
motion_score— mean Farneback optical flow magnitude between consecutive keyframesmean_brightness,mean_contrast— Y-channel stats across keyframespalette_hex— k-means (k=5) in LAB colour space, hex strings sorted by cluster sizeshot_size_class— heuristic from tallest face bbox height: ECU/CU/MCU/MS/LS/ELS/unknown
- Updates
Shotcolumns directly (no Qdrant write) - Increments
enrich_barrier:{asset_id}Redis counter on completion
All three stages share a Redis counter enrich_barrier:{asset_id} (TTL: 1 hour).
When count reaches 3, _finalize_enrichment sets ingest_state='complete' and enrich_done=true.
pending → probing → segmenting → keyframing → transcribing → embedding → enriching → complete
↑
embed_asset sets 'enriching' and enqueues 6a/6b/6c
| Collection | Dimensions | Distance | Written by |
|---|---|---|---|
face_insightface |
512 | Cosine | Stage 6a |
object_crop_qwen3vl |
2048 | Cosine | Stage 6b (stub — requires GroundingDINO + embed worker) |
face_insightface is created on-demand by Stage 6a with payload indexes on: asset_id, shot_id, character_id, project_id, deleted.
POST /projects/{pid}/cluster_characters
- DBSCAN on all face embeddings in the project (cosine distance, eps=0.4, min_samples=2)
- Creates
Characterrows per cluster (cluster_0,cluster_1, …) - Sets
face_track.character_idfor all assigned tracks - Noise tracks (label=-1) left unassigned
- Idempotent: re-uses existing
Characterrows bycluster_label
GET /projects/{pid}/characters — list all characters
GET /projects/{pid}/characters/{cid} — single character
PATCH /projects/{pid}/characters/{cid} — set name, confirmed
New fields on SearchFilters:
| Field | Type | Description |
|---|---|---|
character_ids |
list[str] |
Restrict to shots where any of these characters appear |
object_labels |
list[str] |
Restrict to shots with any of these object labels |
motion_score_min |
float |
Lower bound on shot motion score |
motion_score_max |
float |
Upper bound on shot motion score |
palette_hex |
list[str] |
Stub — reserved for nearest-colour filter |
| Package | Version | Venv |
|---|---|---|
| insightface | 0.7.3 | pipeline-v2 |
| scikit-learn | (already present) | pipeline-v2 |
| opencv-python | 4.10.0 | pipeline-v2 |
GroundingDINO: not installed (weights/install handled externally per brief).
ByteTrack (lap): build failed — IoU fallback used instead.
Asset: test_clip.mp4 (90s, 1 shot, 2 keyframes)
Asset ID: 13b30e6f-80d4-49d7-a93c-44c62e17dbf2
Project: test-project-001
| Stage | Status | Result |
|---|---|---|
| 6a face detection | ✓ | 0 face_tracks (test clip has no visible face) |
| 6b object detection | ✓ | 1 object_track: clapperboard (conf=0.504), crop saved to MinIO |
| 6c shot metadata | ✓ | motion=19.34, brightness=127.4, contrast=64.2, palette 5 hex, size=unknown |
| enrich barrier | ✓ | All 3 stages incremented; asset→complete, enrich_done=True, key deleted |
| face_insightface | ✓ | Collection created, 0 points (no faces) |
| character clustering | ✓ | 0 characters (correct for 0 face tracks) |
Worker routing confirmed:
- Stage 6a (faces) + 6c (shot_meta) →
ingestqueue (pipeline-v2 venv) - Stage 6b (objects) →
objectsqueue (groundingdino venv, transformers==4.37.2)
GroundingDINO venv fix: asyncpg was missing from the groundingdino venv
(needed by db.base which uses asyncpg driver). Added to venv; also added to
scripts/start_worker_objects.sh dependency notes. The start_worker_objects.sh
script now launches the GroundingDINO worker correctly with PYTHONPATH=/workspace/pipeline.
- ByteTrack —
lapCython build fails in this environment; greedy IoU tracker used as fallback. ReID across shots would require a proper multi-frame tracker. object_crop_qwen3vlcollection — crop embeddings not yet wired (requires a follow-up embed job that runs Qwen3-VL over crop images in MinIO).- Visual cross-modal search — still deferred (text query 4096d vs visual collection 2048d).
- No face data on test asset — character clustering validated structurally (correct 0-result handling); end-to-end face→cluster pipeline requires an asset with visible faces.