Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faaa71fa09 | ||
|
|
054c4c8b8f | ||
|
|
43d2c976c3 | ||
|
|
a5299daf6e | ||
|
|
458116f118 | ||
|
|
2ea5737bbd | ||
|
|
5d2f673a81 |
@@ -0,0 +1,145 @@
|
||||
name: Traceability Validation
|
||||
|
||||
# Mirrors JellyTau's .gitea/workflows/traceability-check.yml. The extractor is
|
||||
# stdlib Python, so there is no toolchain install step and no jq.
|
||||
#
|
||||
# This workflow is component-agnostic: every repo-specific setting - which ID
|
||||
# prefixes count, which file suffixes are source, which directories to scan,
|
||||
# the threshold - lives in traceability.toml at the repo root, and the same
|
||||
# extractor is shared by all three JRay components. Copying this file into
|
||||
# another component needs no edits.
|
||||
#
|
||||
# NOTE: the runner here is an Intel N100 with no discrete GPU. This job is only
|
||||
# ever static analysis of source comments plus markdown parsing, so it is cheap;
|
||||
# the requirements it reports as "tagged but unexecuted" are the ones that need
|
||||
# a GPU host, and they are deliberately never counted as covered.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
validate-traces:
|
||||
runs-on: linux/amd64
|
||||
name: Check requirement traces
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Check Python is available
|
||||
run: |
|
||||
set -e
|
||||
command -v python3 >/dev/null 2>&1 || {
|
||||
echo "python3 is missing from the runner image."
|
||||
echo "The traceability tooling is stdlib-only Python;"
|
||||
echo "3.9+ with CLI flags, 3.11+ to read traceability.toml."
|
||||
exit 1
|
||||
}
|
||||
python3 --version
|
||||
|
||||
# The gate's own arithmetic is the thing being trusted, so its tests run
|
||||
# before it does. JellyTau's gate was believed for months while it was
|
||||
# dividing by frozen literals; untested gate logic is how that happens.
|
||||
- name: Test the extractor
|
||||
run: python3 scripts/traceability/test_extract_traces.py
|
||||
|
||||
# Threshold policy and every other repo-specific setting live in
|
||||
# traceability.toml, not here, so local runs and CI runs cannot disagree
|
||||
# about what "passing" means. Denominators come from docs/requirements.md
|
||||
# at run time and are never hardcoded -- in this file or anywhere else.
|
||||
#
|
||||
# A misconfigured run (zero requirements parsed, zero files scanned) is a
|
||||
# hard failure rather than a plausible-looking 0%.
|
||||
- name: Traceability gate
|
||||
run: sh scripts/traceability/traceability-gate.sh
|
||||
|
||||
- name: Check modified files for traces
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
set -e
|
||||
echo "Checking modified sources for TRACES tags..."
|
||||
|
||||
# The extensions come from the report the gate just wrote, which got
|
||||
# them from traceability.toml. Restating them here would be a second
|
||||
# place for the source-file definition to live, and the two would
|
||||
# drift the first time a language is added.
|
||||
PATTERN=$(python3 -c "
|
||||
import json, re, sys
|
||||
suffixes = json.load(open('traces-report.json'))['config']['sourceSuffixes']
|
||||
print('(' + '|'.join(re.escape(s) + '\$' for s in suffixes) + ')')
|
||||
")
|
||||
echo "Source suffixes from traceability.toml: $PATTERN"
|
||||
|
||||
CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" \
|
||||
| grep -E "$PATTERN" || true)
|
||||
|
||||
if [ -z "$CHANGED" ]; then
|
||||
echo "No source files changed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED" | sed 's/^/ /'
|
||||
echo ""
|
||||
|
||||
# Advisory by design: not every file implements a requirement, and a
|
||||
# tag on every function is noise that rots faster than it helps
|
||||
# (CLAUDE.md: tag the unit that decides). This step exists to prompt,
|
||||
# not to block. The blocking checks are in the gate step above.
|
||||
#
|
||||
# Piped into the loop rather than a here-string, and `case` rather
|
||||
# than `[[ == ]]`, so this works under dash as well as bash. The loop
|
||||
# body runs in a subshell, so misses are recorded in a file.
|
||||
MISSING=$(mktemp)
|
||||
echo "$CHANGED" | while IFS= read -r file; do
|
||||
case "$file" in
|
||||
*/test_*.py|*_test.py|*Tests.cs|tests/*|*/tests/*) continue ;;
|
||||
esac
|
||||
[ -f "$file" ] || continue
|
||||
if ! grep -q 'TRACES:' "$file"; then
|
||||
echo " no TRACES tag: $file"
|
||||
echo "$file" >> "$MISSING"
|
||||
fi
|
||||
done
|
||||
|
||||
COUNT=$(wc -l < "$MISSING" | tr -d ' ')
|
||||
rm -f "$MISSING"
|
||||
|
||||
if [ "$COUNT" -gt 0 ]; then
|
||||
echo ""
|
||||
echo "$COUNT changed file(s) carry no requirement tag."
|
||||
echo "Format: // TRACES: AR-012, AR-013 | SR-002"
|
||||
echo " (pipe separates requirement types, comma separates IDs)"
|
||||
echo "A deliberate invariant exception is tagged separately:"
|
||||
echo " // EXCEPTION: AR-024 <reason>"
|
||||
echo "See CLAUDE.md and SPEC.md section 6."
|
||||
fi
|
||||
|
||||
- name: Report summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "Traceability matrix: docs/traceability.md"
|
||||
echo ""
|
||||
head -40 docs/traceability.md || true
|
||||
|
||||
- name: Save reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: traceability-reports
|
||||
path: |
|
||||
traces-report.json
|
||||
docs/traceability.md
|
||||
retention-days: 30
|
||||
+10
-1
@@ -18,8 +18,17 @@ endif()
|
||||
add_subdirectory(external/KPN)
|
||||
|
||||
# OpenCV (video decode, image ops, DNN inference, face detection)
|
||||
find_package(OpenCV 4 REQUIRED COMPONENTS
|
||||
# Accept 4 or 5: the APIs used here are stable across both, and distros have
|
||||
# begun shipping 5.x as the default (Arch/CachyOS). find_package's version
|
||||
# argument is a minimum, but OpenCV's config rejects a 5.x install when 4 is
|
||||
# requested, so probe for 5 first and fall back to 4.
|
||||
find_package(OpenCV 5 QUIET COMPONENTS
|
||||
core imgproc imgcodecs videoio dnn objdetect highgui)
|
||||
if(NOT OpenCV_FOUND)
|
||||
find_package(OpenCV 4 REQUIRED COMPONENTS
|
||||
core imgproc imgcodecs videoio dnn objdetect highgui)
|
||||
endif()
|
||||
message(STATUS "OpenCV: ${OpenCV_VERSION}")
|
||||
|
||||
# ── Model paths ───────────────────────────────────────────────────────────────
|
||||
# Defined early so the backend object libraries below can embed it.
|
||||
|
||||
+1210
File diff suppressed because it is too large
Load Diff
+339
@@ -0,0 +1,339 @@
|
||||
# Implementation plan — per requirement
|
||||
|
||||
One entry per requirement that needs work. Requirements marked `Done` in
|
||||
[`requirements.md`](requirements.md) are omitted.
|
||||
|
||||
**Ordering is derived from dependencies, not assigned to phases.** Each entry
|
||||
lists what it depends on; anything with no unmet dependency is startable. This
|
||||
replaces the earlier phase-based plan, which encoded ordering assumptions that
|
||||
stopped being true as the design changed.
|
||||
|
||||
Verification for each requirement is specified in
|
||||
[`requirements.md`](requirements.md) — this document covers *how to build it*,
|
||||
not how to prove it.
|
||||
|
||||
---
|
||||
|
||||
## Startable now (no unmet dependencies)
|
||||
|
||||
`GR-004` · `IR-004` · `IR-005` · `IR-007` · `IR-008` · `VR-005` · `AR-011` ·
|
||||
`AR-023` extension · tooling port
|
||||
|
||||
These touch disjoint files and can proceed concurrently.
|
||||
|
||||
## Blocked on the registry
|
||||
|
||||
Everything in `AR-007` … `AR-022` depends on `AR-012`/`AR-013` landing first,
|
||||
because they all read or write track state. **This group is one coherent
|
||||
refactor, not parallel work** — splitting it across concurrent efforts produces
|
||||
incompatible designs in the same files.
|
||||
|
||||
---
|
||||
|
||||
# Algorithm
|
||||
|
||||
## AR-012, AR-013 — TrackRegistry (the spine)
|
||||
|
||||
**Depends on:** nothing. **Blocks:** AR-007, AR-008, AR-014 … AR-022.
|
||||
|
||||
Everything else in Part A waits on this, so it goes first.
|
||||
|
||||
### Ownership: a shared resource, not a node
|
||||
|
||||
The registry is **external to the dataflow network**, created in `main` and
|
||||
handed to each node that needs it as `std::shared_ptr<TrackRegistry>`. Lifetime
|
||||
is guaranteed by refcount rather than by the "object must outlive the node"
|
||||
convention, so no ordering assumption exists between network teardown and
|
||||
registry destruction.
|
||||
|
||||
This is idiomatic here: node functors are already constructed outside the network
|
||||
and passed by reference (`main.cpp:186-207`), and KPN provides `SharedResource<T>`
|
||||
for state shared across nodes (KPN SPEC §163, §445).
|
||||
|
||||
Not a node, because ownership is not a stage in the stream — it is state several
|
||||
stages read and write, whose final answer is only known when a track dies.
|
||||
Not inside `TrackGallery`, because that would couple presence to `expand_gallery`,
|
||||
a switchable feature.
|
||||
|
||||
**The registry *is* the tracker's state.** `FaceTrackerFunc` does not keep its own
|
||||
`tracks_`/`inactive_` maps and mirror them in — it operates on the registry
|
||||
directly. Two parallel copies could disagree, and every divergence would surface
|
||||
as wrong presence windows, silently.
|
||||
|
||||
### Per-track state
|
||||
|
||||
```
|
||||
Track
|
||||
first_seen : double set once, at creation
|
||||
last_seen : optional<double> UNSET while on screen; set to the last
|
||||
on-screen timestamp when the face is lost
|
||||
actor : optional<int> set when a posterior crosses the threshold
|
||||
belief : {actor_idx -> accumulated_logodds} Bayesian, not a tally
|
||||
embedding : Embedding running directional mean, for association
|
||||
```
|
||||
|
||||
`last_seen` carries the entire liveness state. Unset = on screen; set = went off
|
||||
at T. No separate missing-frames counter, no expired flag — the optional *is* the
|
||||
state machine, and it subsumes the current two-pool split (`tracks_` = unset,
|
||||
`inactive_` = set).
|
||||
|
||||
### Lifecycle
|
||||
|
||||
```
|
||||
face detected, no match → new track, first_seen = t, last_seen = unset
|
||||
actor identified → update belief; set actor when threshold crossed
|
||||
face lost → last_seen = t_last_on_screen (stays revivable)
|
||||
face seen again, embedding match → last_seen = unset (same track continues)
|
||||
tick(t), t - last_seen > timeout → emit to aggregator, DELETE the entry
|
||||
```
|
||||
|
||||
A presence window is `[first_seen, last_seen]`. Nothing else.
|
||||
|
||||
**Interior gaps are claimed; the trailing cool-down is not.** A face lost at t₁
|
||||
and re-acquired at t₂ within the timeout never closed its track, so the actor is
|
||||
present across `[t₁, t₂]` — correct, since someone briefly occluded or off-camera
|
||||
has not left the scene. But a track that dies ends at `last_seen`, not at the
|
||||
moment of death. That asymmetry is what removes the old `extinction_sec`
|
||||
over-claim.
|
||||
|
||||
**Reaping is a handoff, not a deletion into a holding pen.** The dead track goes
|
||||
to the result aggregator immediately and the registry drops it, so the registry
|
||||
holds only live tracks and its size is bounded by concurrent on-screen faces.
|
||||
|
||||
### Interface
|
||||
|
||||
```
|
||||
TrackRegistry
|
||||
tick(timestamp) ← FaceTrackerFunc, every frame
|
||||
candidates() -> span<Track&> → all live tracks
|
||||
create(timestamp, embedding) -> track_id
|
||||
mark_seen(track_id, timestamp, embedding) → updates mean, clears last_seen
|
||||
mark_lost(track_id, last_on_screen_timestamp)
|
||||
on_vote(track_id, actor_idx, posterior) ← IdentityMatcherFunc
|
||||
owner(track_id) -> optional<actor_idx> → TrackGallery
|
||||
on_track_dead : callback(DeadTrack) → ResultSinkFunc
|
||||
flush() ← at EOF
|
||||
```
|
||||
|
||||
`candidates()` returns **one pool**; `last_seen` tells the caller whether IoU
|
||||
applies. There is no separate revival path — matching a dormant track is ordinary
|
||||
inter-frame association.
|
||||
|
||||
`tick()` advances the clock so dead tracks are reaped independently of detection
|
||||
activity; without it a track only dies when some *other* face happens to appear.
|
||||
|
||||
### Locking
|
||||
|
||||
The tracker mutates registry state across a frame's association pass, so that
|
||||
pass holds the lock for its duration (a `frame_scope()` handle). Every other
|
||||
caller's operations must be individually atomic. A single `std::mutex` over the
|
||||
whole registry is the right start — contention is a few small updates per frame
|
||||
against per-frame work measured in GPU milliseconds.
|
||||
|
||||
Two cases constrain the API:
|
||||
|
||||
- `owner()` is a **read-modify-read** in disguise: `TrackGallery` calls it while
|
||||
`IdentityMatcher` may be voting on the same track. Tally and verdict must be
|
||||
read under one lock as a snapshot, or a track can be both unowned and owned
|
||||
within a single promotion decision.
|
||||
- `on_vote()` arrives downstream of the tracker's `tick()` for the same frame, so
|
||||
a vote may land after the clock moved on. **Rule: a vote for a known track
|
||||
always lands on its tally, regardless of clock.** Only reaping is clock-driven.
|
||||
A vote for an already-reaped track is dropped and **counted** — a nonzero count
|
||||
means the timeout is shorter than the matcher's lag.
|
||||
|
||||
`on_track_dead` fires from inside `tick()` while the frame lock is held, so the
|
||||
callback must not re-enter the registry. Keep it to a push onto the aggregator's
|
||||
storage.
|
||||
|
||||
## AR-016 — EOF flush
|
||||
|
||||
**Depends on:** AR-012.
|
||||
|
||||
`flush()` emits every still-live track through the same callback, closing at
|
||||
`last_seen` if set and the final tick timestamp otherwise. Idempotent, leaving the
|
||||
registry empty; the sink's `written_.exchange(true)` guard
|
||||
(`result_sink_node.hpp:66`) shows the shape.
|
||||
|
||||
Must run on **every** termination path that produces output. Not SIGTERM during
|
||||
opportunistic runs (DP-004) — those push no partial result, so there is nothing
|
||||
to flush.
|
||||
|
||||
Without it a film ending mid-shot silently drops its closing cast, which looks
|
||||
like a recognition miss rather than a bookkeeping bug.
|
||||
|
||||
## AR-014, AR-015 — Contradiction rules
|
||||
|
||||
**Depends on:** AR-012, AR-025.
|
||||
|
||||
| Condition | Meaning | Action |
|
||||
|---|---|---|
|
||||
| Belief on one track swaps A → B | `track_id` carried across a viewpoint change onto a different person | Close at `last_seen`, open a new track for B at the swap frame |
|
||||
| Two **live** tracks owned by one actor | One person split in two, or an identity attached to the wrong track | Treat as a detected cut: reset affected state, re-associate on embedding |
|
||||
|
||||
The second makes identity a **third cut detector**, independent of histogram and
|
||||
TransNetV2, firing where those failed. Detect it via a reverse index
|
||||
`actor_idx → live track_ids`, so the condition is caught on the update that
|
||||
causes it rather than by scanning.
|
||||
|
||||
Both counted and reported — the rates measure how often tracking is silently
|
||||
wrong, which nothing currently reveals.
|
||||
|
||||
## AR-007, AR-008 — Tracker on one pool
|
||||
|
||||
**Depends on:** AR-012, AR-024.
|
||||
|
||||
`FaceTrackerFunc` is constructed with the registry and uses it as state; its
|
||||
`tracks_`/`inactive_` maps and the cross-cut revival branch collapse into one
|
||||
pool keyed on `last_seen`. Per frame: `tick()`, association over `candidates()`,
|
||||
then `create`/`mark_seen`/`mark_lost`.
|
||||
|
||||
`track_alpha` becomes **frame-dependent** — normal frames use the tuned blend,
|
||||
frames flagged `is_cut`/`is_scene_boundary` drop toward embedding-only.
|
||||
|
||||
## AR-024 — Probability space everywhere
|
||||
|
||||
**Depends on:** AR-023. **Blocks:** AR-007, AR-018, AR-021, AR-025.
|
||||
|
||||
Cuts across tracker, matcher and expansion, so it lands with the registry work
|
||||
rather than after it. Retires `track_max_embed_dist`, `cut_revive_sim`,
|
||||
`expand_novelty_sim`, `expand_track_spread_max`.
|
||||
|
||||
Enforcement is a **static grep check** for bare cosine outside a tagged
|
||||
`EXCEPTION` — a unit test cannot prove absence across a codebase.
|
||||
|
||||
## AR-025 — Bayesian accumulation
|
||||
|
||||
**Depends on:** AR-023, AR-024.
|
||||
|
||||
Log-odds per candidate actor, added per frame. `on_vote()` is an *update*, not an
|
||||
increment.
|
||||
|
||||
**The independence problem must be handled explicitly.** Consecutive frames are
|
||||
highly correlated; naive accumulation drives the posterior to certainty on what is
|
||||
effectively one observation. Preferred mitigation: update only on sufficiently
|
||||
novel observations, reusing the diversity buffer's existing judgement rather than
|
||||
inventing a second one. The registry should receive already-discounted evidence.
|
||||
|
||||
## AR-017 — Claims carry belief and route
|
||||
|
||||
**Depends on:** AR-012, AR-025. `DeadTrack` carries posterior plus how it was
|
||||
identified (live / deferred / pooled).
|
||||
|
||||
## AR-018 … AR-021 — Expansion, deferred pass, clustering
|
||||
|
||||
**Depends on:** AR-012, AR-024, AR-026.
|
||||
|
||||
Ordering within the group: AR-018 (banded store) → AR-019 (annex) → AR-020 (TBI
|
||||
queue + deferred pass) → AR-021 (clustering).
|
||||
|
||||
AR-021 needs the temporal cannot-link constraint from track extents, so it cannot
|
||||
start before AR-012. The annex must be a **contiguous matrix** with promotions
|
||||
appended (AR-026), not a list.
|
||||
|
||||
**Output timing changes:** the sink can no longer finalise at EOF — the deferred
|
||||
pass runs after and may add windows (IR-003).
|
||||
|
||||
## AR-022 — Unidentified capture
|
||||
|
||||
**Depends on:** AR-020. Unidentified = TBI entries surviving the deferred pass.
|
||||
Context crops opt-in behind `--dump-unidentified-crops`.
|
||||
|
||||
## AR-001 … AR-004 — Detection and backpressure
|
||||
|
||||
**Depends on:** nothing (AR-002, AR-011); AR-004 blocks AR-003.
|
||||
|
||||
- **AR-002** — `min_face_px` → 66, expressed in original resolution.
|
||||
- **AR-011** — feed TransNetV2 at native rate; derive the dedup window from
|
||||
source fps rather than the hardcoded `0.04 s`.
|
||||
- **AR-004** — backpressure. `kMaxFaces` (`identity_matcher_node.hpp:133`)
|
||||
currently **throws**; channel capacities of 16 (`main.cpp:204-207`) were sized
|
||||
against ≤10 faces/frame. Must block on bytes in flight, not item counts.
|
||||
- **AR-003** — remove `max_faces`. **Gated on AR-004**, not a follow-up to it.
|
||||
|
||||
## AR-026, AR-027 — GEMM and scale
|
||||
|
||||
**Depends on:** nothing to start. The annex CPU loop
|
||||
(`identity_matcher_node.hpp:159-162`) moves into the GEMM path.
|
||||
|
||||
---
|
||||
|
||||
# Gallery
|
||||
|
||||
## GR-004 — Model binding
|
||||
|
||||
**Depends on:** nothing. **Startable immediately, highest value per line.**
|
||||
|
||||
Stamp embedder identity into the gallery at build; verify at load in
|
||||
`scene_analyze`, `replay.py` and the optimizer. Mismatch is a hard error naming
|
||||
both sides.
|
||||
|
||||
Cross-model similarities are meaningless but *look* plausible — this fails
|
||||
silently and expensively, and it would corrupt every measurement taken during the
|
||||
rest of this work.
|
||||
|
||||
## GR-003 — Coverage reporting
|
||||
|
||||
**Depends on:** nothing. Surface what calibration already computes and discards
|
||||
(`kHistBins = 200`): zero-image actors, under-referenced actors, dedup counts,
|
||||
and the intra/inter PDFs.
|
||||
|
||||
## GR-006 … GR-008 — Provenance tiers
|
||||
|
||||
**Depends on:** AR-019. Tier per embedding (baked / harvested / confirmed);
|
||||
harvested persisted but flagged; bell-curve outlier check
|
||||
(`EXCEPTION: AR-024`).
|
||||
|
||||
---
|
||||
|
||||
# Integration
|
||||
|
||||
## IR-004, IR-005, IR-007, IR-008 — Audio signature
|
||||
|
||||
**Depends on:** nothing. **Fully independent — no existing pipeline file is
|
||||
touched.** Best candidate for concurrent work.
|
||||
|
||||
Implement server spec §3 exactly. Audio decode is a second stream from the
|
||||
already-linked FFmpeg. Media < 120 s: no signature, no offset. Emit and honour
|
||||
the `v1:` prefix.
|
||||
|
||||
The golden-vector fixture is shared with the plugin repo and runs on CPU, so the
|
||||
one place two implementations must agree bit-for-bit is verifiable in CI.
|
||||
|
||||
## IR-001 … IR-003 — Truth file
|
||||
|
||||
**Depends on:** AR-017 (belief), AR-020 (output timing).
|
||||
|
||||
Windows carry belief and route; `extraction.*` gains `extinction_sec` and
|
||||
`gallery_scope`; `anneal_sec` removed. All breaking → **one** coordinated
|
||||
`schema_version` bump with IR-004 (SR-003).
|
||||
|
||||
---
|
||||
|
||||
# Validation
|
||||
|
||||
## VR-005 — Minimum face size study
|
||||
|
||||
**Depends on:** nothing. Standalone Python, no C++ contact. Produces the measured
|
||||
value replacing AR-002's 66 px estimate.
|
||||
|
||||
## VR-001 — Dump audit
|
||||
|
||||
**Depends on:** nothing. Read-only investigation: confirm the HDF5 dump preserves
|
||||
everything needed to reconstruct tracks deterministically, including the
|
||||
park/revive path. **Prerequisite for the CI strategy**, since T2 replay is how
|
||||
most of AR-007 … AR-022 is verified.
|
||||
|
||||
## VR-006 … VR-009
|
||||
|
||||
**Depends on:** their subjects landing. VR-009 (posterior calibration holds)
|
||||
depends on AR-025 and is what stops the Bayesian accumulation being decoration.
|
||||
|
||||
---
|
||||
|
||||
# Withdrawn from the old plan
|
||||
|
||||
The phase structure, the `--presence-mode {frame,track}` flag, and "Phase 2 —
|
||||
retune `anneal_sec`/`extinction_sec`". Those constants are withdrawn rather than
|
||||
retuned; comparison against old behaviour uses recorded reference output instead
|
||||
of a second live code path.
|
||||
@@ -0,0 +1,268 @@
|
||||
# scene-actor-extraction — requirements register
|
||||
|
||||
Stable IDs for every requirement in [`SPEC.md`](SPEC.md), which holds the prose.
|
||||
This file is the **authoritative list**; the CI gate reads its denominators from
|
||||
here (see [`../../SPEC.md`](../../SPEC.md) §6).
|
||||
|
||||
**IDs are permanent.** A withdrawn requirement is marked `Withdrawn` and its
|
||||
number is never reused — renumbering is what produces orphan TRACES tags. This
|
||||
register replaces the earlier thematic `A1…E8` scheme, which had already produced
|
||||
an `A1a` and an out-of-order `E6`.
|
||||
|
||||
Tag code with `// TRACES: AR-012 | SR-002`.
|
||||
|
||||
| Type | Scope |
|
||||
|---|---|
|
||||
| `AR` | Algorithm — the extraction pipeline itself |
|
||||
| `DP` | Deployment — how it runs |
|
||||
| `IR` | Integration — contracts with other components |
|
||||
| `GR` | Gallery — building and maintaining actor references |
|
||||
| `VR` | Validation — parameter studies and benchmarks |
|
||||
| `UT` / `IT` | Unit / integration tests |
|
||||
|
||||
Status: `Done` · `In Progress` · `Planned` · `TBD` · `Withdrawn`
|
||||
|
||||
---
|
||||
|
||||
## Algorithm (AR)
|
||||
|
||||
| ID | Requirement | Traces to | Priority | Status |
|
||||
|---|---|---|---|---|
|
||||
| AR-001 | Detect faces in sampled frames; emit bbox, confidence, 5-point landmarks in original pixel space | SR-002 | High | Done |
|
||||
| AR-002 | Minimum face size 66×66 px, expressed in **original** resolution (decoupled from `dense_scale`) | SR-002 | High | Planned |
|
||||
| AR-003 | No fixed per-frame face cap — crowd scenes must not lose background cast | SR-002 | Medium | Planned |
|
||||
| AR-004 | Backpressure: unbounded faces/frame absorbed by slowing, never by dropping or throwing | SR-002 | High | Planned |
|
||||
| AR-005 | Align to 112×112 via ArcFace 5-point similarity transform | SR-002 | High | Done |
|
||||
| AR-006 | 512-d L2-normalised embeddings, batched | SR-002 | High | Done |
|
||||
| AR-007 | Associate detections by IoU + embedding, with **frame-dependent** weighting | SR-002 | High | In Progress |
|
||||
| AR-008 | One track pool keyed on `last_seen`; no separate revival path | SR-002 | High | Planned |
|
||||
| AR-009 | Camera-cut detection (histogram) as an association hint | SR-002 | High | Done |
|
||||
| AR-010 | Scene-boundary detection (TransNetV2) as an association hint | SR-002 | Medium | In Progress |
|
||||
| AR-011 | **Every model is fed the input it was trained for** — cost reduced by running less often, never by degrading one inference | SR-002 | High | Planned |
|
||||
| AR-012 | Presence follows **track extent**, not per-frame recognition | **SR-002** | High | Planned |
|
||||
| AR-013 | `last_seen` optional state machine; window ends at last sighting, never after | SR-002 | High | Planned |
|
||||
| AR-014 | Belief swap A→B terminates the track and starts a new one | SR-002 | Medium | Planned |
|
||||
| AR-015 | Two live tracks owned by one actor ⇒ treat as a detected cut, re-associate | SR-002 | Medium | Planned |
|
||||
| AR-016 | All tracks closed at EOF — a film ends with faces on screen | SR-002 | High | Planned |
|
||||
| AR-017 | Every presence claim carries its belief and identification route | SR-002 | High | Planned |
|
||||
| AR-018 | Per-subject embedding store with banded admission (novel enough, safe enough) | SR-005 | Medium | Planned |
|
||||
| AR-019 | Per-film gallery annex from owned tracks; acquires the non-frontal views TMDB lacks | SR-005 | Medium | In Progress |
|
||||
| AR-020 | Deferred re-identification of unknown tracks against the final expanded gallery | SR-005 | High | Planned |
|
||||
| AR-021 | Cluster unknown tracks into one entity per person, under temporal cannot-link constraints | SR-005 | Medium | Planned |
|
||||
| AR-022 | Capture still-unidentified tracks: embeddings, metadata, **context crops** for human review | §4 | Medium | Planned |
|
||||
| AR-023 | Fit sigmoid calibration from intra/inter similarity distributions | SR-002 | High | Done |
|
||||
| AR-024 | **Always the calibrated probability, never a raw cosine** — exceptions recorded | SR-002 | High | Planned |
|
||||
| AR-025 | Per-track Bayesian accumulation in log-odds, with correlated-observation discounting | SR-002 | High | Planned |
|
||||
| AR-026 | All similarity computed as GEMM, including annex and deferred pass | SR-001 | High | In Progress |
|
||||
| AR-027 | Throughput acceptable for **arbitrary** gallery size | SR-001 | High | Planned |
|
||||
|
||||
## Deployment (DP)
|
||||
|
||||
| ID | Requirement | Traces to | Priority | Status |
|
||||
|---|---|---|---|---|
|
||||
| DP-001 | One analysis core; modes are front-ends and must not fork pipeline logic | PR-004 | High | Done |
|
||||
| DP-002 | Batch CLI over one title | PR-004 | High | Done |
|
||||
| DP-003 | On-demand resident service with bounded, observable queue | PR-004 | Medium | Planned |
|
||||
| DP-004 | Opportunistic/idle mode: external trigger, hard stop, implicit re-queue | PR-004 | Medium | Planned |
|
||||
| DP-005 | Native installer, no Docker; Fedora + Arch | PR-004 | Medium | Planned |
|
||||
| DP-006 | Background incremental gallery refresh on a timer | PR-003 | Medium | Planned |
|
||||
|
||||
## Integration (IR)
|
||||
|
||||
| ID | Requirement | Traces to | Priority | Status |
|
||||
|---|---|---|---|---|
|
||||
| IR-001 | Emit the JRay truth format as sibling `.jray.json` | SR-003 | High | Done |
|
||||
| IR-002 | Windows carry belief + route; `extraction.*` carries `extinction_sec`, `gallery_scope` | SR-003 | High | Planned |
|
||||
| IR-003 | Output written **after** the deferred pass, not at EOF | SR-003 | High | Planned |
|
||||
| IR-004 | Compute the audio signature exactly per server spec §3 | SR-003 | Medium | Planned |
|
||||
| IR-005 | Golden-vector fixture shared with the plugin repo to prove bit-exactness | SR-003 | High | Planned |
|
||||
| IR-007 | Media < 120 s: emit no signature, apply no sync offset — identical rule in both producers | SR-003 | Low | Planned |
|
||||
| IR-008 | Emit and honour the signature's own `v1:` version prefix | SR-003 | Low | Planned |
|
||||
| IR-006 | Jellyfin round-trip: pull pending queue, push complete results only | SR-001 | High | Done |
|
||||
|
||||
## Gallery (GR)
|
||||
|
||||
| ID | Requirement | Traces to | Priority | Status |
|
||||
|---|---|---|---|---|
|
||||
| GR-001 | Build gallery from Jellyfin library cast, TMDB profile fallback | SR-001, SR-005 | High | Done |
|
||||
| GR-002 | Incremental `--merge` refresh without re-embedding known actors | PR-003 | High | Done |
|
||||
| GR-003 | Report coverage: zero-image actors, under-referenced actors, dedup, calibration PDFs | SR-001 | Medium | Planned |
|
||||
| GR-004 | Stamp embedder identity into the gallery; **hard startup error** on mismatch | SR-001 | High | Planned |
|
||||
| GR-005 | Gallery data never leaves the instance | **SR-005** | High | Done |
|
||||
| GR-006 | Provenance tiers: baked / harvested / confirmed, distinguishable per embedding | SR-005 | High | Planned |
|
||||
| GR-007 | Persist harvested embeddings **flagged and reviewable**, never silently equal to baked | SR-005 | Medium | Planned |
|
||||
| GR-008 | Flag distributional outliers among an actor's references (poisoning guard) — `EXCEPTION: AR-024` | SR-005 | Medium | Planned |
|
||||
| GR-009 | Human-confirmed associations persist and improve future extractions | §4 | Medium | TBD |
|
||||
|
||||
## Validation (VR)
|
||||
|
||||
| ID | Requirement | Traces to | Priority | Status |
|
||||
|---|---|---|---|---|
|
||||
| VR-001 | HDF5 post-inference dump at the embedded-frame boundary | PR-002 | High | Done |
|
||||
| VR-002 | Replay drives the **real** KPN nodes, not a reimplementation | PR-002 | High | Done |
|
||||
| VR-003 | Scoring: micro-F1 against X-Ray, precision/recall logged at every evaluation | PR-002 | High | Done |
|
||||
| VR-004 | Reproducible validation corpus with ground truth | PR-002 | High | Done |
|
||||
| VR-005 | Minimum face size study — TPI/FPI vs probe size, gallery held at native res | PR-002 | Medium | Planned |
|
||||
| VR-006 | Re-tune `scene_threshold` once native-rate decode lands | PR-002 | Low | Planned |
|
||||
| VR-007 | Expansion band, clustering threshold, and deferred-pass ablation | PR-002 | Medium | Planned |
|
||||
| VR-008 | Gallery scaling benchmark — throughput vs gallery size | PR-002 | Medium | Planned |
|
||||
| VR-009 | Verify accumulated posteriors are calibrated against held-out tracks | PR-002 | High | Planned |
|
||||
|
||||
---
|
||||
|
||||
## Verification strategy
|
||||
|
||||
**CI runs on an Intel N100 with no discrete GPU.** That is a hard constraint on
|
||||
how each requirement can be verified, and it shapes the test design rather than
|
||||
merely limiting it.
|
||||
|
||||
Four tiers, in decreasing order of preference:
|
||||
|
||||
| Tier | Runs in CI | What it covers |
|
||||
|---|---|---|
|
||||
| **T1 — CPU unit** | Yes | Pure logic: registry state machine, belief accumulation, clustering, band admission, calibration maths |
|
||||
| **T2 — Replay** | Yes | Real pipeline nodes driven from an HDF5 fixture — no GPU, no video |
|
||||
| **T3 — CPU inference** | Yes, slowly | ORT CPU provider over a handful of frames; smoke tests only |
|
||||
| **T4 — GPU** | **No** | Throughput, TRT engines, large-gallery GEMM |
|
||||
|
||||
**T2 is the reason this is workable.** The HDF5 dump (VR-001) captures state
|
||||
after decode → detect → align → embed and before tracking and matching, so
|
||||
everything downstream — which is where nearly all of the new design lives — is
|
||||
cheap CPU maths replayable from a fixture. Tracking, presence windows, belief
|
||||
accumulation, expansion, deferred re-identification and clustering are all
|
||||
verifiable on an N100 at full fidelity, not in miniature.
|
||||
|
||||
That was already true for the optimizer. It now doubles as the CI strategy, which
|
||||
is a strong argument for keeping the dump schema honest (VR-001) and for the
|
||||
replay driving the *real* nodes rather than a reimplementation (VR-002).
|
||||
|
||||
**Small committed fixtures are required.** A few HDF5 dumps covering the awkward
|
||||
cases — a cut, a belief swap, two live tracks converging, a film ending
|
||||
mid-track, an unknown track that only resolves after expansion — are worth more
|
||||
than a large corpus, and they are small enough to commit.
|
||||
|
||||
**T4 requirements cannot pass in CI, and the gate must not pretend otherwise.**
|
||||
For these, CI verifies that a test *exists and is tagged*, not that it passes;
|
||||
the run happens on a GPU host, nightly or manually, and reports separately. A
|
||||
requirement whose only evidence is a test that never executes should be visible
|
||||
as such rather than counted as covered.
|
||||
|
||||
| Requirement | Tier | Note |
|
||||
|---|---|---|
|
||||
| AR-001, AR-005, AR-006 | T3 | Smoke only — correctness of detection/embedding is a model property, not ours |
|
||||
| AR-002 | T2 | Size filtering is arithmetic on dumped bboxes |
|
||||
| AR-003, AR-004 | T1 + T4 | Backpressure logic is unit-testable; saturation behaviour needs real load |
|
||||
| AR-007 … AR-017 | **T2** | The core of the redesign — fully replayable |
|
||||
| AR-018 … AR-022 | **T2** | Expansion, deferred pass, clustering: all post-embedding |
|
||||
| AR-023 … AR-025 | T1 | Calibration fit and log-odds accumulation are pure maths |
|
||||
| AR-026, AR-027 | T4 | GEMM throughput and scaling — GPU host only |
|
||||
| DP-* | T1 + manual | Lifecycle logic unit-tested; install paths are manual |
|
||||
| IR-001 … IR-003 | T1 | Serialisation against a golden truth file |
|
||||
| IR-004, IR-005 | **T1** | Audio signature is CPU DSP — the golden-vector fixture runs anywhere, which is precisely why it is the right cross-repo check |
|
||||
| GR-001 … GR-005 | T1 + T3 | Gallery assembly is I/O and bookkeeping; embedding is T3 smoke |
|
||||
| GR-006 … GR-008 | T1 | Tiering and outlier detection operate on stored embeddings |
|
||||
| VR-* | Out of CI | Studies are run deliberately and their results committed as documents |
|
||||
|
||||
**One consequence worth stating:** AR-027 (arbitrary gallery scale) is
|
||||
structurally unverifiable on the CI host. It needs a GPU host and a synthetic
|
||||
large gallery, so it is the requirement most likely to silently regress. Its
|
||||
benchmark (VR-008) should run on a schedule rather than on demand.
|
||||
|
||||
### Fixtures — precomputed inference, pulled by CI
|
||||
|
||||
The N100 cannot run inference at any useful rate, so **inference output is
|
||||
precomputed on a GPU host and consumed by CI as data.** This converts most of
|
||||
what looks like GPU work into pure CPU replay.
|
||||
|
||||
| Fixture | Contents | Size | Storage |
|
||||
|---|---|---|---|
|
||||
| **Edge-case dumps** | ~6 short clips (30–60 s), one per awkward behaviour | ~1 MB each | **Committed in-repo** |
|
||||
| **Corpus dumps** | Full-length titles from the validation corpus | ~30 MB each | Pinned artifact, fetched by checksum |
|
||||
| **Synthetic gallery** | Random unit-norm embeddings, fixed seed | small | Generated at test time |
|
||||
| **Golden truth files** | Expected output for each edge-case dump | KB | Committed |
|
||||
| **Audio golden vectors** | Short WAV + expected signature | KB | Committed, **shared with the plugin repo** |
|
||||
|
||||
Edge-case dumps are small enough to commit, and being in-repo means they version
|
||||
with the code that reads them. Corpus dumps are pulled by pinned checksum from
|
||||
the artifact store rather than committed, since they are large and change only
|
||||
when the dump schema does.
|
||||
|
||||
**Generation must be reproducible and versioned.** A script, run on a GPU host,
|
||||
regenerates every fixture from source clips; it is re-run when the VR-001 schema
|
||||
version bumps. A fixture whose provenance is unknown is worse than no fixture,
|
||||
because it will be trusted.
|
||||
|
||||
> **The limitation that must stay visible:** replay fixtures freeze upstream
|
||||
> behaviour. A test driven from a dump verifies AR-007 onward *given those
|
||||
> embeddings* — it cannot detect a regression in detection, alignment or
|
||||
> embedding, because those produced the fixture. Nothing in CI can. That gap is
|
||||
> covered only by the T3 smoke test and the scheduled GPU run, and it should not
|
||||
> be papered over by a high replay-coverage number.
|
||||
|
||||
### Per-requirement verification plan
|
||||
|
||||
| ID | Tier | Test asserts | Edge cases to cover |
|
||||
|---|---|---|---|
|
||||
| AR-001 | T3 | Detector returns plausible boxes on a known frame | — smoke only |
|
||||
| AR-002 | T2 | Faces below 66 px (original res) are dropped | Exactly at threshold; with `dense_scale` 0.5 — the interaction that motivated the requirement |
|
||||
| AR-003 | T2 | No cap applied; a 40-face frame yields 40 | Crowd frame |
|
||||
| AR-004 | T1 | Saturated input blocks rather than drops or throws | Bounded queue at capacity; **byte-based** limit with large crops; SIGTERM mid-block |
|
||||
| AR-005 | T1 | Known landmarks → expected 112×112 warp | Landmarks near frame edge; degenerate/collinear points |
|
||||
| AR-006 | T3 | Embeddings are unit-norm | Batch smaller than, equal to, larger than `embed_batch_size` |
|
||||
| AR-007 | T2 | Association picks the right track | Two faces crossing paths; one leaving frame as another enters |
|
||||
| AR-008 | T2 | One pool; dormant tracks match on embedding, not IoU | Dormant track whose old bbox overlaps a *different* new face — must not match on position |
|
||||
| AR-009/010 | T2 | Cut/boundary shifts weighting toward embedding | Cut with same people; cut with all-new people |
|
||||
| AR-011 | T1 | TransNetV2 receives native-rate frames | Source at 24/25/30 fps — dedup window derived, not assumed |
|
||||
| AR-012 | **T2** | Window spans full track extent, not first recognition | Actor recognised only at track end — window must still start at `first_seen` |
|
||||
| AR-013 | **T2** | `last_seen` set/unset; window ends at last sighting | Gap just under vs just over timeout; reappearance after timeout → two windows |
|
||||
| AR-014 | T2 | Belief swap closes one window, opens another | No blended window; no overlap at the swap frame |
|
||||
| AR-015 | T2 | Two live tracks on one actor trigger re-association | Counter increments |
|
||||
| AR-016 | **T2** | Every track closed at EOF | Film ending mid-shot — window ends at final frame, not dropped |
|
||||
| AR-017 | T1 | Claim carries posterior and route | Deferred and pooled routes distinguishable |
|
||||
| AR-018 | T1 | Band admits only within bounds | At each bound exactly; store never admits below lower bound |
|
||||
| AR-019 | T2 | Promotion only when all three signals quiet | Cut mid-track blocks promotion |
|
||||
| AR-020 | **T2** | Unknown resolved after expansion | Track failing at minute 12, resolved at EOF — the ordering-independence claim |
|
||||
| AR-021 | T2 | Clustering merges same person, respects cannot-link | **Temporally overlapping tracks never merge**; measure how many merges the constraint rejects |
|
||||
| AR-022 | T1 | Context crops retained, bounded per track | Track running for minutes |
|
||||
| AR-023 | T1 | Sigmoid fit on synthetic separable data | Too few positive pairs → `valid=false`, fallback engages |
|
||||
| AR-024 | **Static check** | No bare cosine outside a tagged `EXCEPTION` | Grep-based; this is the invariant's enforcement |
|
||||
| AR-025 | T1 | Log-odds accumulate; correlated frames discounted | 30 identical frames must **not** reach the certainty of 30 diverse ones |
|
||||
| AR-026 | T1 + T4 | GEMM path produces same result as reference loop | Equivalence on small input in CI; throughput on GPU host |
|
||||
| AR-027 | **T4** | Throughput at 10²…10⁵ actors | Scheduled, not on-demand |
|
||||
| IR-001/002 | T1 | Serialised output matches golden file | Zero-length window; actor with many windows |
|
||||
| IR-003 | T1 | Output written after deferred pass | Not at EOF |
|
||||
| IR-004/005 | **T1** | Signature matches golden vector bit-for-bit | **Media < 120 s → no signature**; identical result in both repos |
|
||||
| GR-004 | T1 | Mismatched embedder → hard startup error | Error names both sides |
|
||||
| GR-008 | T1 | Outlier flagged among an actor's references | Injected poisoned embedding detected |
|
||||
| VR-009 | T1 | Posterior calibration holds | A 0.99 posterior is wrong ~1% of the time on held-out tracks |
|
||||
|
||||
Three of these are worth singling out because they verify claims that would
|
||||
otherwise be assertions: **AR-012** (window starts at `first_seen` even when
|
||||
recognition comes late) is the entire point of the redesign; **AR-020** (a track
|
||||
failing mid-film resolves at EOF) is the claim that ordering stops mattering; and
|
||||
**AR-025** (30 identical frames ≠ 30 diverse ones) is what stops the Bayesian
|
||||
accumulation from being decoration.
|
||||
|
||||
---
|
||||
|
||||
## Withdrawn
|
||||
|
||||
| ID | Requirement | Reason |
|
||||
|---|---|---|
|
||||
| — | `anneal_sec` window merging | Superseded by AR-012/AR-013: a track survives its own gaps, so there is nothing to anneal |
|
||||
| — | `extinction_sec` actor keep-alive | Superseded by AR-013: windows end at last sighting, which is what this over-claimed |
|
||||
|
||||
Both were deleted rather than retained at zero — a field naming a mechanism the
|
||||
pipeline no longer has is actively misleading (see `SPEC.md` A6.6).
|
||||
|
||||
---
|
||||
|
||||
## Notes on coverage
|
||||
|
||||
- **VR-*** traces to PR-002 (scene-granularity answers) rather than to a system
|
||||
requirement: parameter studies are single-repo work serving accuracy, and this
|
||||
is correct rather than a gap.
|
||||
- **PR-005** (leak nothing) has no `AR`/`DP` row. It is satisfied *structurally*
|
||||
by SR-004 and GR-005 — the server holds no binary, the gallery never leaves the
|
||||
instance — not by any component doing something. It cannot be verified by
|
||||
pointing at code, and it dies the moment either prohibition is relaxed.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Requirements traceability matrix
|
||||
|
||||
<!-- GENERATED FILE - do not edit by hand. -->
|
||||
<!-- Regenerate: scripts/traceability/traceability-gate.sh -->
|
||||
|
||||
**Generated:** 2026-07-30T16:35:36+00:00
|
||||
|
||||
Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this repo's CI host can execute (`T1, T2, T3, static`).
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 86 |
|
||||
| TRACES tags found | 0 |
|
||||
| EXCEPTION tags found | 0 |
|
||||
| Requirements defined | 59 |
|
||||
| Requirements covered | 0 |
|
||||
| **Coverage** | **0.0%** (0/59) |
|
||||
| Coverage of CI-executable scope | 0.0% (0/50) |
|
||||
| Tagged but unexecuted in CI | 0 |
|
||||
| Orphan tags | 0 |
|
||||
|
||||
### By type
|
||||
|
||||
| Type | Covered | Tagged but unexecuted | Defined |
|
||||
|---|---|---|---|
|
||||
| AR | 0 | 0 | 27 |
|
||||
| DP | 0 | 0 | 6 |
|
||||
| IR | 0 | 0 | 8 |
|
||||
| GR | 0 | 0 | 9 |
|
||||
| VR | 0 | 0 | 9 |
|
||||
|
||||
|
||||
## Not executable in CI
|
||||
|
||||
These requirements have no verification tier this repo's CI host can run, so a tag on them is evidence of *intent*, not of verification. They are never counted as covered.
|
||||
|
||||
| ID | Tiers | Tagged in source | Requirement |
|
||||
|---|---|---|---|
|
||||
| AR-027 | T4 | no | Throughput acceptable for **arbitrary** gallery size |
|
||||
| VR-001 | out-of-ci | no | HDF5 post-inference dump at the embedded-frame boundary |
|
||||
| VR-002 | out-of-ci | no | Replay drives the **real** KPN nodes, not a reimplementation |
|
||||
| VR-003 | out-of-ci | no | Scoring: micro-F1 against X-Ray, precision/recall logged at every eva… |
|
||||
| VR-004 | out-of-ci | no | Reproducible validation corpus with ground truth |
|
||||
| VR-005 | out-of-ci | no | Minimum face size study — TPI/FPI vs probe size, gallery held at nati… |
|
||||
| VR-006 | out-of-ci | no | Re-tune `scene_threshold` once native-rate decode lands |
|
||||
| VR-007 | out-of-ci | no | Expansion band, clustering threshold, and deferred-pass ablation |
|
||||
| VR-008 | out-of-ci | no | Gallery scaling benchmark — throughput vs gallery size |
|
||||
|
||||
## Orphan tags
|
||||
|
||||
A tag naming an ID `requirements.md` does not define. This is what renumbering produces, and what a typo produces.
|
||||
|
||||
_None._
|
||||
|
||||
## Requirements tracing up to nothing
|
||||
|
||||
A register row whose `Traces to` cell names no parent. Work serving no stated goal is how scope creeps in, and it is invisible unless something looks.
|
||||
|
||||
_None._
|
||||
|
||||
## Recorded exceptions
|
||||
|
||||
Deliberate, documented departures from an invariant (`EXCEPTION: XX-nnn <reason>`). Reported separately and never counted as coverage — an exception is a decision to be reviewed, not evidence a requirement is met.
|
||||
|
||||
_None._
|
||||
|
||||
## Register
|
||||
|
||||
| ID | Status | Tier | Traces to | Trace state | Tagged in | Requirement |
|
||||
|---|---|---|---|---|---|---|
|
||||
| AR-001 | Done | T3 | SR-002 | untagged | - | Detect faces in sampled frames; emit bbox, confidence, 5-point landma… |
|
||||
| AR-002 | Planned | T2 | SR-002 | untagged | - | Minimum face size 66×66 px, expressed in **original** resolution (dec… |
|
||||
| AR-003 | Planned | T1, T2, T4 | SR-002 | untagged | - | No fixed per-frame face cap — crowd scenes must not lose background c… |
|
||||
| AR-004 | Planned | T1, T4 | SR-002 | untagged | - | Backpressure: unbounded faces/frame absorbed by slowing, never by dro… |
|
||||
| AR-005 | Done | T1, T3 | SR-002 | untagged | - | Align to 112×112 via ArcFace 5-point similarity transform |
|
||||
| AR-006 | Done | T3 | SR-002 | untagged | - | 512-d L2-normalised embeddings, batched |
|
||||
| AR-007 | In Progress | T2 | SR-002 | untagged | - | Associate detections by IoU + embedding, with **frame-dependent** wei… |
|
||||
| AR-008 | Planned | T2 | SR-002 | untagged | - | One track pool keyed on `last_seen`; no separate revival path |
|
||||
| AR-009 | Done | T2 | SR-002 | untagged | - | Camera-cut detection (histogram) as an association hint |
|
||||
| AR-010 | In Progress | T2 | SR-002 | untagged | - | Scene-boundary detection (TransNetV2) as an association hint |
|
||||
| AR-011 | Planned | T1, T2 | SR-002 | untagged | - | **Every model is fed the input it was trained for** — cost reduced by… |
|
||||
| AR-012 | Planned | T2 | **SR-002** | untagged | - | Presence follows **track extent**, not per-frame recognition |
|
||||
| AR-013 | Planned | T2 | SR-002 | untagged | - | `last_seen` optional state machine; window ends at last sighting, nev… |
|
||||
| AR-014 | Planned | T2 | SR-002 | untagged | - | Belief swap A→B terminates the track and starts a new one |
|
||||
| AR-015 | Planned | T2 | SR-002 | untagged | - | Two live tracks owned by one actor ⇒ treat as a detected cut, re-asso… |
|
||||
| AR-016 | Planned | T2 | SR-002 | untagged | - | All tracks closed at EOF — a film ends with faces on screen |
|
||||
| AR-017 | Planned | T1, T2 | SR-002 | untagged | - | Every presence claim carries its belief and identification route |
|
||||
| AR-018 | Planned | T1, T2 | SR-005 | untagged | - | Per-subject embedding store with banded admission (novel enough, safe… |
|
||||
| AR-019 | In Progress | T2 | SR-005 | untagged | - | Per-film gallery annex from owned tracks; acquires the non-frontal vi… |
|
||||
| AR-020 | Planned | T2 | SR-005 | untagged | - | Deferred re-identification of unknown tracks against the final expand… |
|
||||
| AR-021 | Planned | T2 | SR-005 | untagged | - | Cluster unknown tracks into one entity per person, under temporal can… |
|
||||
| AR-022 | Planned | T1, T2 | §4 | untagged | - | Capture still-unidentified tracks: embeddings, metadata, **context cr… |
|
||||
| AR-023 | Done | T1 | SR-002 | untagged | - | Fit sigmoid calibration from intra/inter similarity distributions |
|
||||
| AR-024 | Planned | T1, static | SR-002 | untagged | - | **Always the calibrated probability, never a raw cosine** — exception… |
|
||||
| AR-025 | Planned | T1 | SR-002 | untagged | - | Per-track Bayesian accumulation in log-odds, with correlated-observat… |
|
||||
| AR-026 | In Progress | T1, T4 | SR-001 | untagged | - | All similarity computed as GEMM, including annex and deferred pass |
|
||||
| AR-027 | Planned | T4 | SR-001 | untagged | - | Throughput acceptable for **arbitrary** gallery size |
|
||||
| DP-001 | Done | T1, manual | PR-004 | untagged | - | One analysis core; modes are front-ends and must not fork pipeline lo… |
|
||||
| DP-002 | Done | T1, manual | PR-004 | untagged | - | Batch CLI over one title |
|
||||
| DP-003 | Planned | T1, manual | PR-004 | untagged | - | On-demand resident service with bounded, observable queue |
|
||||
| DP-004 | Planned | T1, manual | PR-004 | untagged | - | Opportunistic/idle mode: external trigger, hard stop, implicit re-que… |
|
||||
| DP-005 | Planned | T1, manual | PR-004 | untagged | - | Native installer, no Docker; Fedora + Arch |
|
||||
| DP-006 | Planned | T1, manual | PR-003 | untagged | - | Background incremental gallery refresh on a timer |
|
||||
| IR-001 | Done | T1 | SR-003 | untagged | - | Emit the JRay truth format as sibling `.jray.json` |
|
||||
| IR-002 | Planned | T1 | SR-003 | untagged | - | Windows carry belief + route; `extraction.*` carries `extinction_sec`… |
|
||||
| IR-003 | Planned | T1 | SR-003 | untagged | - | Output written **after** the deferred pass, not at EOF |
|
||||
| IR-004 | Planned | T1 | SR-003 | untagged | - | Compute the audio signature exactly per server spec §3 |
|
||||
| IR-005 | Planned | T1 | SR-003 | untagged | - | Golden-vector fixture shared with the plugin repo to prove bit-exactn… |
|
||||
| IR-006 | Done | unset | SR-001 | untagged | - | Jellyfin round-trip: pull pending queue, push complete results only |
|
||||
| IR-007 | Planned | unset | SR-003 | untagged | - | Media < 120 s: emit no signature, apply no sync offset — identical ru… |
|
||||
| IR-008 | Planned | unset | SR-003 | untagged | - | Emit and honour the signature's own `v1:` version prefix |
|
||||
| GR-001 | Done | T1, T3 | SR-001, SR-005 | untagged | - | Build gallery from Jellyfin library cast, TMDB profile fallback |
|
||||
| GR-002 | Done | T1, T3 | PR-003 | untagged | - | Incremental `--merge` refresh without re-embedding known actors |
|
||||
| GR-003 | Planned | T1, T3 | SR-001 | untagged | - | Report coverage: zero-image actors, under-referenced actors, dedup, c… |
|
||||
| GR-004 | Planned | T1, T3 | SR-001 | untagged | - | Stamp embedder identity into the gallery; **hard startup error** on m… |
|
||||
| GR-005 | Done | T1, T3 | **SR-005** | untagged | - | Gallery data never leaves the instance |
|
||||
| GR-006 | Planned | T1 | SR-005 | untagged | - | Provenance tiers: baked / harvested / confirmed, distinguishable per … |
|
||||
| GR-007 | Planned | T1 | SR-005 | untagged | - | Persist harvested embeddings **flagged and reviewable**, never silent… |
|
||||
| GR-008 | Planned | T1 | SR-005 | untagged | - | Flag distributional outliers among an actor's references (poisoning g… |
|
||||
| GR-009 | TBD | unset | §4 | untagged | - | Human-confirmed associations persist and improve future extractions |
|
||||
| VR-001 | Done | out-of-ci | PR-002 | untagged | - | HDF5 post-inference dump at the embedded-frame boundary |
|
||||
| VR-002 | Done | out-of-ci | PR-002 | untagged | - | Replay drives the **real** KPN nodes, not a reimplementation |
|
||||
| VR-003 | Done | out-of-ci | PR-002 | untagged | - | Scoring: micro-F1 against X-Ray, precision/recall logged at every eva… |
|
||||
| VR-004 | Done | out-of-ci | PR-002 | untagged | - | Reproducible validation corpus with ground truth |
|
||||
| VR-005 | Planned | out-of-ci | PR-002 | untagged | - | Minimum face size study — TPI/FPI vs probe size, gallery held at nati… |
|
||||
| VR-006 | Planned | out-of-ci | PR-002 | untagged | - | Re-tune `scene_threshold` once native-rate decode lands |
|
||||
| VR-007 | Planned | out-of-ci | PR-002 | untagged | - | Expansion band, clustering threshold, and deferred-pass ablation |
|
||||
| VR-008 | Planned | out-of-ci | PR-002 | untagged | - | Gallery scaling benchmark — throughput vs gallery size |
|
||||
| VR-009 | Planned | T1, out-of-ci | PR-002 | untagged | - | Verify accumulated posteriors are calibrated against held-out tracks |
|
||||
|
||||
## Detailed mapping
|
||||
|
||||
_No TRACES tags found yet. Tags are added as code is written; an empty matrix on a new tree is the correct reading, not a failure._
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
# Profiles must match src/arcface_embedder.hpp and src/scrfd_decoder.hpp:
|
||||
# ArcFace : min=1x3x112x112 opt=Nx3x112x112 max=Nx3x112x112 (N = embed batch)
|
||||
# SCRFD : 1x3x640x640 (fixed; we letterbox to this)
|
||||
# TransNetV2 : 1x100x27x48x3 (fixed; scene detector window), input tensor "input"
|
||||
# TransNetV2 : 1x100x27x48x3 (fixed; scene detector window)
|
||||
#
|
||||
# Input tensor names are read from each ONNX model at runtime rather than
|
||||
# hardcoded, since they differ between models (LVFace-B: "data", arcface_r18:
|
||||
# "input", arcface_w600k_{r50,mbf}: "input.1").
|
||||
#
|
||||
# These trtexec-built engines are *not* picked up by the ORT TRT EP cache —
|
||||
# ORT uses its own engine format. The point of this script is:
|
||||
@@ -27,24 +31,43 @@ SCENE_MODEL="${SCENE_MODEL:-$MODELS/transnetv2.onnx}"
|
||||
|
||||
run() { echo "+ $*"; "$@"; }
|
||||
|
||||
echo "== ArcFace =="
|
||||
# The input tensor name is not the same across models — LVFace-B uses "data",
|
||||
# arcface_r18 uses "input", and arcface_w600k_{r50,mbf} use "input.1". A
|
||||
# hardcoded name makes trtexec fail with "Cannot find input tensor with name
|
||||
# ...", so read it from the model instead.
|
||||
input_name() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
except ImportError:
|
||||
sys.exit("onnxruntime is required to read the model's input name")
|
||||
sess = ort.InferenceSession(sys.argv[1], providers=["CPUExecutionProvider"])
|
||||
print(sess.get_inputs()[0].name)
|
||||
PY
|
||||
}
|
||||
|
||||
ARCFACE_IN="$(input_name "$ARCFACE_MODEL")"
|
||||
SCRFD_IN="$(input_name "$SCRFD_MODEL")"
|
||||
|
||||
echo "== ArcFace == (input tensor: $ARCFACE_IN)"
|
||||
run trtexec \
|
||||
--onnx="$ARCFACE_MODEL" \
|
||||
--fp16 \
|
||||
--minShapes=input.1:1x3x112x112 \
|
||||
--optShapes=input.1:${EMBED_BATCH}x3x112x112 \
|
||||
--maxShapes=input.1:${EMBED_BATCH}x3x112x112 \
|
||||
--minShapes="$ARCFACE_IN":1x3x112x112 \
|
||||
--optShapes="$ARCFACE_IN":${EMBED_BATCH}x3x112x112 \
|
||||
--maxShapes="$ARCFACE_IN":${EMBED_BATCH}x3x112x112 \
|
||||
--saveEngine="$OUT/arcface.$(basename "$ARCFACE_MODEL" .onnx).b${EMBED_BATCH}.fp16.engine" \
|
||||
--useCudaGraph
|
||||
|
||||
echo
|
||||
echo "== SCRFD =="
|
||||
echo "== SCRFD == (input tensor: $SCRFD_IN)"
|
||||
run trtexec \
|
||||
--onnx="$SCRFD_MODEL" \
|
||||
--fp16 \
|
||||
--minShapes=input.1:1x3x640x640 \
|
||||
--optShapes=input.1:1x3x640x640 \
|
||||
--maxShapes=input.1:1x3x640x640 \
|
||||
--minShapes="$SCRFD_IN":1x3x640x640 \
|
||||
--optShapes="$SCRFD_IN":1x3x640x640 \
|
||||
--maxShapes="$SCRFD_IN":1x3x640x640 \
|
||||
--saveEngine="$OUT/scrfd.$(basename "$SCRFD_MODEL" .onnx).640.fp16.engine" \
|
||||
--useCudaGraph
|
||||
|
||||
@@ -53,12 +76,14 @@ if [[ -f "$SCENE_MODEL" ]]; then
|
||||
echo "== TransNetV2 (scene detector) =="
|
||||
# Fixed 1x100x27x48x3 window. The raw-TRT scene detector backend loads this
|
||||
# engine directly via --scene-detector-engine; the ORT-TRT EP builds its own.
|
||||
# No --*Shapes here: TransNetV2's input is fully static (1x100x27x48x3
|
||||
# with no dynamic dimensions), and TensorRT rejects explicit shape
|
||||
# profiles for such a model — "Static model does not take explicit shapes
|
||||
# since the shape of inference tensors will be determined by the model
|
||||
# itself". The shape comes from the model.
|
||||
run trtexec \
|
||||
--onnx="$SCENE_MODEL" \
|
||||
--fp16 \
|
||||
--minShapes=input:1x100x27x48x3 \
|
||||
--optShapes=input:1x100x27x48x3 \
|
||||
--maxShapes=input:1x100x27x48x3 \
|
||||
--saveEngine="$OUT/transnetv2.100x27x48.fp16.engine" \
|
||||
--useCudaGraph
|
||||
else
|
||||
|
||||
@@ -147,11 +147,14 @@ def download_person_images(base_url: str, api_key: str, person_id: str,
|
||||
|
||||
def fetch_actor_images(base_url: str, api_key: str, pid: str, info: dict,
|
||||
images_per_actor: int, actor_dir: Path,
|
||||
fetch_imdb_ids: bool, tmdb_key: str | None
|
||||
fetch_imdb_ids: bool, tmdb_key: str | None,
|
||||
fetch_overfetch: float = 1.0
|
||||
) -> tuple[list[Path], str | None, str | None]:
|
||||
"""Network-bound: download Jellyfin image(s), then fall back to TMDB if short."""
|
||||
name = info["name"]
|
||||
print(f"{name} ({pid}) — in {len(info['appearances'])} title(s)", file=sys.stderr)
|
||||
# Over-fetch target: see the TMDB block below.
|
||||
tmdb_budget = int(images_per_actor * fetch_overfetch)
|
||||
image_paths = download_person_images(base_url, api_key, pid, actor_dir, images_per_actor)
|
||||
|
||||
# Need the IMDB id for the TMDB /find lookup, to persist it (--fetch-imdb-ids),
|
||||
@@ -177,8 +180,13 @@ def fetch_actor_images(base_url: str, api_key: str, pid: str, info: dict,
|
||||
except requests.RequestException as e:
|
||||
print(f" [warn] {name}: TMDB lookup failed: {e}", file=sys.stderr)
|
||||
|
||||
if len(image_paths) < images_per_actor and tmdb_urls:
|
||||
needed = images_per_actor - len(image_paths)
|
||||
# Over-fetch from TMDB: near-duplicate stills (the same photo at different
|
||||
# crops/resolutions) are dropped after embedding, so downloading exactly
|
||||
# images_per_actor would leave the actor short of that many *distinct*
|
||||
# embeddings. Pulling extra candidates lets the dedup filter discard
|
||||
# duplicates while still reaching the target.
|
||||
if len(image_paths) < tmdb_budget and tmdb_urls:
|
||||
needed = tmdb_budget - len(image_paths)
|
||||
print(f" {name}: Jellyfin image missing/incomplete, falling back to TMDB "
|
||||
f"({len(tmdb_urls)} image(s) available)…", file=sys.stderr)
|
||||
image_paths += download_images(tmdb_urls, actor_dir, needed, start_index=len(image_paths))
|
||||
@@ -186,10 +194,10 @@ def fetch_actor_images(base_url: str, api_key: str, pid: str, info: dict,
|
||||
# Last resort: a CC-licensed Commons headshot via Wikidata, keyed by the
|
||||
# actor's IMDB id. Catches actors TMDB has no usable image for (or that the
|
||||
# name search missed entirely).
|
||||
if len(image_paths) < images_per_actor and imdb_id:
|
||||
if len(image_paths) < tmdb_budget and imdb_id:
|
||||
wiki_urls = wikidata_image_urls(imdb_id)
|
||||
if wiki_urls:
|
||||
needed = images_per_actor - len(image_paths)
|
||||
needed = tmdb_budget - len(image_paths)
|
||||
print(f" {name}: still short, falling back to Wikidata/Commons "
|
||||
f"({len(wiki_urls)} image(s) available)…", file=sys.stderr)
|
||||
image_paths += download_images(wiki_urls, actor_dir, needed,
|
||||
@@ -198,9 +206,39 @@ def fetch_actor_images(base_url: str, api_key: str, pid: str, info: dict,
|
||||
return image_paths, imdb_id, tmdb_id
|
||||
|
||||
|
||||
# Default cosine-distance tolerance below which two embeddings of the same
|
||||
# actor are treated as the same image. Embeddings are L2-normalised by the
|
||||
# backend, so cosine similarity is a plain dot product and the distance is
|
||||
# 1 - dot. Expanding an actor's photo set via TMDB frequently returns the same
|
||||
# still at different crops/resolutions; those embed to nearly identical vectors
|
||||
# and add gallery size and match cost without adding information.
|
||||
DEDUP_TOL = 1e-3
|
||||
|
||||
|
||||
def _cosine(a, b) -> float:
|
||||
"""Cosine similarity of two L2-normalised embeddings."""
|
||||
return float(sum(x * y for x, y in zip(a, b)))
|
||||
|
||||
|
||||
def _near_duplicate(emb, existing, tol: float) -> int | None:
|
||||
"""Index of the first embedding within `tol` cosine distance of `emb`.
|
||||
|
||||
Returns None when `emb` is sufficiently distinct from everything in
|
||||
`existing`. tol <= 0 disables the check.
|
||||
"""
|
||||
if tol <= 0:
|
||||
return None
|
||||
for i, prev in enumerate(existing):
|
||||
if 1.0 - _cosine(emb, prev) < tol:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def embed_actor(pid: str, info: dict, image_paths: list[Path],
|
||||
imdb_id: str | None, tmdb_id: str | None, embedder, fetch_imdb_ids: bool,
|
||||
embed_executor: concurrent.futures.ThreadPoolExecutor) -> tuple[dict | None, str | None]:
|
||||
embed_executor: concurrent.futures.ThreadPoolExecutor,
|
||||
dedup_tol: float = DEDUP_TOL,
|
||||
max_embeddings: int = 0) -> tuple[dict | None, str | None]:
|
||||
"""GPU-bound: run sae_embed, always on embed_executor's single dedicated thread.
|
||||
|
||||
onnxruntime's CUDA EP / cudnn_frontend execution plans are not safe to run
|
||||
@@ -217,20 +255,35 @@ def embed_actor(pid: str, info: dict, image_paths: list[Path],
|
||||
|
||||
embeddings = []
|
||||
source_images = []
|
||||
n_dup = 0
|
||||
print(f" {name}: embedding {len(image_paths)} image(s)…", file=sys.stderr)
|
||||
for path in image_paths:
|
||||
res = embed_executor.submit(embedder.embed, str(path)).result()
|
||||
if not res.ok:
|
||||
print(f" [skip] {name}/{path.name}: {res.error}", file=sys.stderr)
|
||||
continue
|
||||
embeddings.append(res.embedding)
|
||||
emb = res.embedding
|
||||
dup = _near_duplicate(emb, embeddings, dedup_tol)
|
||||
if dup is not None:
|
||||
n_dup += 1
|
||||
print(f" [dup] {name}/{path.name}: matches {source_images[dup]} "
|
||||
f"(cos={_cosine(emb, embeddings[dup]):.6f}), not stored",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
embeddings.append(emb)
|
||||
source_images.append(path.name)
|
||||
# Stop once we have the requested number of *distinct* embeddings; the
|
||||
# extra candidates were only fetched to absorb duplicates.
|
||||
if max_embeddings and len(embeddings) >= max_embeddings:
|
||||
break
|
||||
|
||||
if not embeddings:
|
||||
print(f" {name}: no valid embeddings, skipping actor", file=sys.stderr)
|
||||
return None, "no valid embeddings"
|
||||
|
||||
print(f" {name}: → {len(embeddings)} embedding(s) stored", file=sys.stderr)
|
||||
dup_note = f" ({n_dup} near-duplicate(s) dropped)" if n_dup else ""
|
||||
print(f" {name}: → {len(embeddings)} embedding(s) stored{dup_note}",
|
||||
file=sys.stderr)
|
||||
return {
|
||||
"imdb_id": imdb_id if (fetch_imdb_ids and imdb_id) else "",
|
||||
"tmdb_id": tmdb_id or "",
|
||||
@@ -245,12 +298,16 @@ def embed_actor(pid: str, info: dict, image_paths: list[Path],
|
||||
def process_actor(pid: str, info: dict, base_url: str, api_key: str,
|
||||
embedder, images_per_actor: int, image_root: Path,
|
||||
fetch_imdb_ids: bool, tmdb_key: str | None,
|
||||
embed_executor: concurrent.futures.ThreadPoolExecutor) -> tuple[dict | None, str | None]:
|
||||
embed_executor: concurrent.futures.ThreadPoolExecutor,
|
||||
dedup_tol: float = DEDUP_TOL,
|
||||
fetch_overfetch: float = 1.0) -> tuple[dict | None, str | None]:
|
||||
safe_name = info["name"].replace(" ", "_")
|
||||
actor_dir = image_root / f"{pid}_{safe_name}"
|
||||
image_paths, imdb_id, tmdb_id = fetch_actor_images(
|
||||
base_url, api_key, pid, info, images_per_actor, actor_dir, fetch_imdb_ids, tmdb_key)
|
||||
return embed_actor(pid, info, image_paths, imdb_id, tmdb_id, embedder, fetch_imdb_ids, embed_executor)
|
||||
base_url, api_key, pid, info, images_per_actor, actor_dir, fetch_imdb_ids,
|
||||
tmdb_key, fetch_overfetch)
|
||||
return embed_actor(pid, info, image_paths, imdb_id, tmdb_id, embedder, fetch_imdb_ids,
|
||||
embed_executor, dedup_tol, images_per_actor)
|
||||
|
||||
|
||||
# ── Gallery assembly ─────────────────────────────────────────────────────────
|
||||
@@ -258,7 +315,9 @@ def process_actor(pid: str, info: dict, base_url: str, api_key: str,
|
||||
def build_gallery(base_url: str, api_key: str, embedder, item_types: list[str],
|
||||
images_per_actor: int, image_root: Path,
|
||||
fetch_imdb_ids: bool, existing_actors: dict,
|
||||
tmdb_key: str | None = None, workers: int = 8) -> tuple[dict, list[dict]]:
|
||||
tmdb_key: str | None = None, workers: int = 8,
|
||||
dedup_tol: float = DEDUP_TOL,
|
||||
fetch_overfetch: float = 1.0) -> tuple[dict, list[dict]]:
|
||||
actors = collect_actors(base_url, api_key, item_types)
|
||||
|
||||
gallery_actors = []
|
||||
@@ -298,7 +357,8 @@ def build_gallery(base_url: str, api_key: str, embedder, item_types: list[str],
|
||||
futures = {
|
||||
executor.submit(process_actor, pid, info, base_url, api_key, embedder,
|
||||
images_per_actor, image_root,
|
||||
fetch_imdb_ids, tmdb_key, embed_executor): (pid, info["name"])
|
||||
fetch_imdb_ids, tmdb_key, embed_executor,
|
||||
dedup_tol, fetch_overfetch): (pid, info["name"])
|
||||
for pid, info in todo
|
||||
}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
@@ -343,6 +403,16 @@ def main():
|
||||
help="Directory containing ONNX models (default: models/)")
|
||||
parser.add_argument("--arcface", default=None,
|
||||
help="Path to ArcFace ONNX model (overrides --models-dir selection)")
|
||||
parser.add_argument("--dedup-tol", type=float, default=DEDUP_TOL,
|
||||
help=f"Cosine-distance threshold below which a new embedding is treated "
|
||||
f"as a duplicate of one already stored for that actor and dropped "
|
||||
f"(default: {DEDUP_TOL}). TMDB often returns the same still at "
|
||||
f"different crops. Set 0 to keep every embedding.")
|
||||
parser.add_argument("--overfetch", type=float, default=2.0,
|
||||
help="Download this multiple of --images-per-actor as candidates, then "
|
||||
"keep the first N that survive dedup (default: 2.0). Raise it for "
|
||||
"actors whose TMDB galleries are mostly duplicates; 1.0 disables "
|
||||
"over-fetching.")
|
||||
parser.add_argument("--images-per-actor", type=int, default=10,
|
||||
help="Images to download per actor (default: 10). Jellyfin usually has "
|
||||
"only 1, so the rest come from the TMDB/Wikidata fallbacks; more "
|
||||
@@ -392,6 +462,8 @@ def main():
|
||||
image_root=image_root,
|
||||
fetch_imdb_ids=args.fetch_imdb_ids,
|
||||
existing_actors=existing_actors,
|
||||
dedup_tol=args.dedup_tol,
|
||||
fetch_overfetch=args.overfetch,
|
||||
tmdb_key=args.tmdb_key,
|
||||
workers=args.workers,
|
||||
)
|
||||
|
||||
@@ -33,4 +33,14 @@ def load_embedder(build_dir: str, models_dir: str, arcface: str | None = None,
|
||||
if not Path(model).is_file():
|
||||
sys.exit(f"{name} model not found: {model}\nRun: bash scripts/download_models.sh")
|
||||
|
||||
return sae_embed.FaceEmbedder(detector_path, arcface_path, conf, nms, max_side)
|
||||
# A TRT-backend build cannot load .onnx; it needs pre-built engines from
|
||||
# scripts/build_trt_engines.sh. Pass them when present (ignored by ORT).
|
||||
trt = Path(models_path).parent / "trt_cache"
|
||||
det_engine = trt / "scrfd.scrfd_500m_bnkps.640.fp16.engine"
|
||||
arc_engine = trt / f"arcface.{Path(arcface_path).stem}.b4.fp16.engine"
|
||||
|
||||
return sae_embed.FaceEmbedder(
|
||||
detector_path, arcface_path, conf, nms, max_side,
|
||||
str(det_engine) if det_engine.is_file() else "",
|
||||
str(arc_engine) if arc_engine.is_file() else "",
|
||||
)
|
||||
|
||||
Executable
+1643
File diff suppressed because it is too large
Load Diff
Executable
+1047
File diff suppressed because it is too large
Load Diff
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Requirement traceability gate. Run locally exactly as CI runs it, from the
|
||||
# component repo root:
|
||||
#
|
||||
# scripts/traceability/traceability-gate.sh
|
||||
#
|
||||
# Writes the JSON report and the markdown matrix, prints the coverage report,
|
||||
# and exits non-zero when the gate fails.
|
||||
#
|
||||
# This script is shared by every JRay component, so it knows nothing about any
|
||||
# one repo. All repo-specific settings - requirement ID prefixes, source
|
||||
# suffixes, scan roots, register path, thresholds - live in `traceability.toml`
|
||||
# at the component repo root. Run
|
||||
#
|
||||
# scripts/traceability/extract_traces.py --print-example-config
|
||||
#
|
||||
# for the annotated schema. A repo whose config is wrong parses zero
|
||||
# requirements or scans zero files, and the gate refuses to report rather than
|
||||
# printing a misleading 0%.
|
||||
#
|
||||
# Environment (all optional; each overrides the config file):
|
||||
# TRACES_CONFIG path to traceability.toml
|
||||
# TRACES_ROOT repo root (default: nearest dir containing traceability.toml)
|
||||
# MIN_COVERAGE minimum overall coverage percent
|
||||
# ALLOW_ORPHANS 1 to report orphan tags without failing
|
||||
# TRACES_JSON JSON report path
|
||||
# TRACES_MD markdown matrix path
|
||||
# SYSTEM_SPEC SPEC.md defining PR/SR; enables PR/SR orphan checking
|
||||
# PYTHON interpreter (default: python3)
|
||||
#
|
||||
# Threshold policy belongs in traceability.toml, not here and not in the
|
||||
# workflow YAML: a threshold written in two places is a threshold that will
|
||||
# disagree with itself.
|
||||
#
|
||||
# POSIX sh, no bashisms, no jq - the extractor does its own arithmetic and
|
||||
# printing, so CI needs nothing beyond python3.
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
command -v "$PYTHON" >/dev/null 2>&1 || {
|
||||
echo "FAILED: $PYTHON not found. The traceability gate needs Python 3.9+," >&2
|
||||
echo " or 3.11+ to read traceability.toml." >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
set -- --format coverage
|
||||
|
||||
# Explicit `if` rather than `[ ... ] && ...`, because a trailing false test in
|
||||
# an && list exits under `set -e` in some POSIX shells.
|
||||
if [ -n "${TRACES_CONFIG:-}" ]; then set -- "$@" --config "$TRACES_CONFIG"; fi
|
||||
if [ -n "${TRACES_ROOT:-}" ]; then set -- "$@" --root "$TRACES_ROOT"; fi
|
||||
if [ -n "${MIN_COVERAGE:-}" ]; then set -- "$@" --min-coverage "$MIN_COVERAGE"; fi
|
||||
if [ -n "${TRACES_JSON:-}" ]; then set -- "$@" --json-out "$TRACES_JSON"; fi
|
||||
if [ -n "${TRACES_MD:-}" ]; then set -- "$@" --markdown-out "$TRACES_MD"; fi
|
||||
if [ -n "${SYSTEM_SPEC:-}" ]; then set -- "$@" --system-spec "$SYSTEM_SPEC"; fi
|
||||
if [ "${ALLOW_ORPHANS:-0}" = "1" ]; then set -- "$@" --allow-orphans; fi
|
||||
|
||||
exec "$PYTHON" "$SCRIPT_DIR/extract_traces.py" "$@"
|
||||
@@ -511,8 +511,17 @@ public:
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
context_->setInputShape(input_name_.c_str(),
|
||||
nvinfer1::Dims5{1, kWindow, kFrameH, kFrameW, 3});
|
||||
// TensorRT 10 removed the fixed-rank Dims5 helper (Dims2..Dims4 remain
|
||||
// in NvInferLegacyDims.h). Build the rank-5 shape via the generic Dims,
|
||||
// which works on both 8.x and 10.x.
|
||||
nvinfer1::Dims shape{};
|
||||
shape.nbDims = 5;
|
||||
shape.d[0] = 1;
|
||||
shape.d[1] = kWindow;
|
||||
shape.d[2] = kFrameH;
|
||||
shape.d[3] = kFrameW;
|
||||
shape.d[4] = 3;
|
||||
context_->setInputShape(input_name_.c_str(), shape);
|
||||
|
||||
check_cuda(cudaMemcpyAsync(d_input_, buf.data(), buf.size() * 4,
|
||||
cudaMemcpyHostToDevice, stream_), "H2D input");
|
||||
|
||||
@@ -32,16 +32,23 @@ struct FaceEmbedResult {
|
||||
|
||||
class FaceEmbedderEngine {
|
||||
public:
|
||||
// detector_engine/arcface_engine are optional paths to pre-built TensorRT
|
||||
// engines. They are required when built with SAE_INFERENCE_BACKEND=TRT
|
||||
// (which cannot load .onnx directly) and ignored by the ORT backend.
|
||||
FaceEmbedderEngine(const std::string& detector_model,
|
||||
const std::string& arcface_model,
|
||||
float conf = 0.5f, float nms = 0.4f, int max_side = 500)
|
||||
float conf = 0.5f, float nms = 0.4f, int max_side = 500,
|
||||
const std::string& detector_engine = "",
|
||||
const std::string& arcface_engine = "")
|
||||
: max_side_(max_side)
|
||||
{
|
||||
Config cfg;
|
||||
cfg.detector_model = detector_model;
|
||||
cfg.arcface_model = arcface_model;
|
||||
cfg.detector_conf = conf;
|
||||
cfg.detector_nms = nms;
|
||||
cfg.detector_model = detector_model;
|
||||
cfg.arcface_model = arcface_model;
|
||||
cfg.detector_engine = detector_engine;
|
||||
cfg.arcface_engine = arcface_engine;
|
||||
cfg.detector_conf = conf;
|
||||
cfg.detector_nms = nms;
|
||||
detector_ = make_face_detector(cfg);
|
||||
embedder_ = make_face_embedder(cfg);
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ NB_MODULE(sae_embed, m) {
|
||||
});
|
||||
|
||||
nb::class_<FaceEmbedderEngine>(m, "FaceEmbedder")
|
||||
.def(nb::init<std::string, std::string, float, float, int>(),
|
||||
.def(nb::init<std::string, std::string, float, float, int,
|
||||
std::string, std::string>(),
|
||||
"detector_model"_a, "arcface_model"_a,
|
||||
"conf"_a = 0.5f, "nms"_a = 0.4f, "max_side"_a = 500)
|
||||
"conf"_a = 0.5f, "nms"_a = 0.4f, "max_side"_a = 500,
|
||||
"detector_engine"_a = "", "arcface_engine"_a = "")
|
||||
.def("embed", &FaceEmbedderEngine::embed_path, "path"_a,
|
||||
nb::call_guard<nb::gil_scoped_release>(),
|
||||
"Detect the highest-confidence face in the image, align it, and "
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Traceability configuration for scene-actor-extraction.
|
||||
#
|
||||
# Read by the shared extractor (scripts/traceability/extract_traces.py), which
|
||||
# is the same implementation every JRay component uses. Everything repo-specific
|
||||
# lives here rather than in the tool; run `extract_traces.py
|
||||
# --print-example-config` for the annotated schema.
|
||||
#
|
||||
# This file's directory is taken as the repo root, so the gate works from any
|
||||
# subdirectory.
|
||||
|
||||
# The prefixes this repo's register defines. Nothing else enters the fraction:
|
||||
# UT/IT are evidence for requirements, PR/SR belong to the system spec.
|
||||
requirement_types = ["AR", "DP", "IR", "GR", "VR"]
|
||||
|
||||
# C++ pipeline plus the Python tooling, optimizer and validation scripts.
|
||||
languages = ["cpp", "python"]
|
||||
|
||||
source_roots = ["src", "tests", "scripts", "experiments", "eval"]
|
||||
|
||||
# CI is an Intel N100 with no discrete GPU. T4 is deliberately absent: a
|
||||
# requirement verifiable only on GPU hardware is reported as tagged but
|
||||
# unexecuted and never counted as covered, because counting a test that cannot
|
||||
# run is the same failure mode as JellyTau's 158% coverage bug.
|
||||
ci_executable_tiers = ["T1", "T2", "T3", "static"]
|
||||
|
||||
# Threshold policy. 0 today because almost nothing is tagged yet - tags land as
|
||||
# the pipeline is built. This is not a gate that cannot fail: orphan tags, a
|
||||
# >100% ratio, a register that parses to nothing and an empty source scan are
|
||||
# all hard failures already. Ratchet this up as tags land; never reset it down.
|
||||
min_coverage = 0.0
|
||||
|
||||
# The system spec owning PR/SR is vendored per-component as a submodule. Point
|
||||
# at it once that lands to turn on PR/SR orphan checking:
|
||||
# system_spec = "scripts/vendor/jray-project/SPEC.md"
|
||||
Reference in New Issue
Block a user