Project home: system spec, working notes, and setup
Gives the system specification an owner. It defines the PR-nnn project requirements and SR-nnn cross-component contracts that every component spec traces up to, and until now it lived in no repository at all. The three component repositories are linked from the README and gitignored here rather than added as submodules. A submodule pins a commit, so with feature branches and worktrees in flight across the components, every component commit would leave this repository's pointer stale. The dependency is meant to run the other way: components pull this repository in for the shared tooling and system spec, both of which change rarely. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
# Component repositories. They are separate repos with their own remotes; this
|
||||
# README explains how to clone them. They are deliberately NOT submodules —
|
||||
# see "Why not submodules for the components" in README.md.
|
||||
/jRay/
|
||||
/scene-actor-extraction/
|
||||
/JRay-public-server/
|
||||
|
||||
# Python artefacts from the traceability tooling
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Generated traceability output — regenerate with the gate, never hand-edit.
|
||||
traces-report.json
|
||||
|
||||
# Editor / OS noise
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*~
|
||||
.DS_Store
|
||||
@@ -0,0 +1,105 @@
|
||||
# JRay — working notes
|
||||
|
||||
Three components. Start with the system spec; each repo's spec implements it.
|
||||
|
||||
| Doc | Owns |
|
||||
|---|---|
|
||||
| [`SPEC.md`](SPEC.md) | **System** requirements — anything spanning more than one repo |
|
||||
| [`scene-actor-extraction/docs/SPEC.md`](scene-actor-extraction/docs/SPEC.md) | Extraction pipeline software requirements |
|
||||
| [`scene-actor-extraction/docs/requirements.md`](scene-actor-extraction/docs/requirements.md) | Stable requirement IDs, statuses, verification plan |
|
||||
| [`scene-actor-extraction/docs/plan.md`](scene-actor-extraction/docs/plan.md) | Per-requirement implementation notes and dependencies |
|
||||
| [`jRay/SPEC.md`](jRay/SPEC.md) | Truth-file format, Jellyfin plugin API |
|
||||
| [`JRay-public-server/SPEC.md`](JRay-public-server/SPEC.md) | Jmanifest exchange, cut matching, audio signature |
|
||||
|
||||
---
|
||||
|
||||
## Invariants — do not violate without an explicit, recorded agreement
|
||||
|
||||
### Always use the calibrated probability. Never a raw cosine.
|
||||
|
||||
**Every similarity is converted through the sigmoid calibration before it is
|
||||
used, compared, or thresholded.** This applies everywhere — identity matching,
|
||||
tracking association, gallery expansion admission, cluster merging — not just at
|
||||
the final identity decision.
|
||||
|
||||
A raw cosine threshold is an unfalsifiable magic number: it means something
|
||||
different for every model, every gallery, and every face size, and it cannot be
|
||||
combined with anything else. A calibrated probability means the same thing
|
||||
everywhere and composes (see the per-track Bayesian accumulation in A9).
|
||||
|
||||
**If a raw cosine is ever used, it is an agreed exception and must be recorded as
|
||||
such in the spec, with the reason.** Absent that, treat any bare cosine
|
||||
comparison in the code as a defect to be fixed. The constants being retired for
|
||||
this reason include `track_max_embed_dist`, `cut_revive_sim`,
|
||||
`expand_novelty_sim` and `expand_track_spread_max`.
|
||||
|
||||
**Agreed exceptions so far** (do not "fix" these):
|
||||
|
||||
| Where | Why |
|
||||
|---|---|
|
||||
| `GR-008` — distributional outlier check on an actor's own references | The sigmoid is a monotonic squash: right for *decisions*, wrong for characterising a *distribution*, since it compresses exactly the tails where outliers live. Shape is a property of the metric space. Scope is analysis only — every match decision still goes through the calibration. |
|
||||
|
||||
### Every model gets the input it was trained for
|
||||
|
||||
Cost is reduced by running a model **less often** or on **fewer regions** — never
|
||||
by degrading what a single inference sees. A model run off-distribution returns
|
||||
confident, plausible, wrong output, and the error is invisible without a study
|
||||
that should not have been needed. (See A5: TransNetV2 must be fed at native frame
|
||||
rate, not a cheaper 12 fps.)
|
||||
|
||||
### Presence is scene-scoped, not instantaneous
|
||||
|
||||
The question is *"is this actor in this scene?"*, not *"is their face visible in
|
||||
this frame?"* — because that is what the X-Ray ground truth records. An actor who
|
||||
turns away, is occluded, or is off-camera while the shot cuts to whoever they are
|
||||
speaking to **is still present**. Reporting them absent is not a stricter answer;
|
||||
it is an answer to a different question. (SR-002.)
|
||||
|
||||
A window ends at the **last sighting**, never after it. Gaps *between* sightings
|
||||
are absorbed; time *after* the final one never is.
|
||||
|
||||
### The public server stores no binary content
|
||||
|
||||
Its abuse defence is structural — no images, no base64, no opaque blobs, no
|
||||
extension points. This is what makes it safe for a volunteer to operate. Any
|
||||
proposal to ship images or embeddings through it is a proposal to delete that
|
||||
property, not an incremental feature. (SR-004.)
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
### Tag code with the requirement it implements
|
||||
|
||||
House format, matching [`JellyTau`](../JellyTau/docs/traces-quick-ref.md) so the
|
||||
tooling ports directly. Pipe separates requirement types, comma separates IDs
|
||||
within a type:
|
||||
|
||||
```cpp
|
||||
/// TRACES: AR-012, AR-013 | SR-002
|
||||
struct TrackRegistry { … };
|
||||
```
|
||||
|
||||
- Tag the unit that **decides**, not every helper it calls.
|
||||
- **Tests carry TRACES too** (`UT-nnn`, `IT-nnn`) — that is what shows a
|
||||
requirement is *verified*, not merely implemented.
|
||||
- **A deliberate raw-cosine use is tagged `EXCEPTION: AR-024 <reason>`.** Per the
|
||||
invariant above, a bare cosine with no recorded exception is a defect.
|
||||
|
||||
Chain: `PR-nnn → SR-nnn → component requirement → TRACES`, defined in
|
||||
[`SPEC.md`](SPEC.md) §6. Registers live in each component's
|
||||
`docs/requirements.md`; **IDs are permanent and never reused**, since renumbering
|
||||
is what creates orphan tags.
|
||||
|
||||
When adding a requirement, give it a parent and a register row. When adding code,
|
||||
give it a tag.
|
||||
|
||||
- **Specs are requirements + current-state deltas.** Each requirement carries
|
||||
`Current:` and `Gap:` so the document doubles as a work list. Keep that shape
|
||||
when editing.
|
||||
- **State understanding before writing it into a doc.** Design here has been
|
||||
settled by discussion, and the corrections have usually been *simpler* than the
|
||||
proposal. Confirm first; do not write elaborate mechanism into a spec on an
|
||||
unverified assumption.
|
||||
- Breaking format changes are **batched into one `schema_version` bump** and
|
||||
coordinated across all three repos (SR-003).
|
||||
@@ -0,0 +1,123 @@
|
||||
# JRay
|
||||
|
||||
**A self-hosted alternative to Amazon X-Ray.** While watching, the viewer can
|
||||
see who is on screen — for their own library, on their own hardware, with nobody
|
||||
else learning what they own.
|
||||
|
||||
This is the project home. It owns the [system specification](SPEC.md) — the
|
||||
requirements that span more than one component — and the tooling shared between
|
||||
them. The code lives in three separate repositories, linked below.
|
||||
|
||||
---
|
||||
|
||||
## The three components
|
||||
|
||||
| Repository | Role | Language |
|
||||
|---|---|---|
|
||||
| [`scene-actor-extraction`](https://gitea.tourolle.paris/dtourolle/scene-actor-extraction) | Derives presence data from a media file | C++ / Python |
|
||||
| [`jRay`](https://gitea.tourolle.paris/dtourolle/jRay) | Jellyfin plugin: surfaces it in the player, owns the truth-file format | C# |
|
||||
| [`JRay-public-server`](https://gitea.tourolle.paris/dtourolle/JRay-public-server) | Exchanges presence data between instances | Rust |
|
||||
|
||||
They ship independently — the plugin to Jellyfin's catalogue, the server as a
|
||||
binary, the pipeline to a GPU host — which is why they are separate repositories
|
||||
rather than one monorepo.
|
||||
|
||||
```
|
||||
media file ──► extraction ──► truth file (sidecar or pushed) ──► plugin ──► player overlay
|
||||
▲ │
|
||||
│ ▼
|
||||
gallery Jmanifest ──► public server ──► other instances
|
||||
(Jellyfin + TMDB)
|
||||
```
|
||||
|
||||
Two axes, deliberately separate: **presence data** flows outward and is
|
||||
shareable, being timings against public identifiers. **Gallery data** — the
|
||||
actor reference faces — is built locally and never leaves the instance (SR-005).
|
||||
|
||||
---
|
||||
|
||||
## Setting up
|
||||
|
||||
Clone this repository, then the components beside it:
|
||||
|
||||
```sh
|
||||
git clone git@gitea.tourolle.paris:dtourolle/jray-project.git
|
||||
cd jray-project
|
||||
|
||||
git clone git@gitea.tourolle.paris:dtourolle/scene-actor-extraction.git
|
||||
git clone git@gitea.tourolle.paris:dtourolle/jRay.git
|
||||
git clone git@gitea.tourolle.paris:dtourolle/JRay-public-server.git
|
||||
```
|
||||
|
||||
The component directories are `.gitignore`d here, so they sit beside the system
|
||||
spec without this repository trying to track them. Each has its own README with
|
||||
build instructions.
|
||||
|
||||
### Why not submodules for the components
|
||||
|
||||
A submodule pins a commit. With feature branches and worktrees in flight across
|
||||
the components, every component commit would leave this repository's pointer
|
||||
stale and its `git status` dirty until someone committed a pointer bump — churn
|
||||
that buys nothing, since the components are developed together in one directory
|
||||
anyway.
|
||||
|
||||
The dependency runs the other way instead: **components pull *this* repository
|
||||
in** for the shared tooling and the system spec, both of which change rarely.
|
||||
That is the asymmetry submodules suit.
|
||||
|
||||
---
|
||||
|
||||
## Where to start reading
|
||||
|
||||
| Doc | Owns |
|
||||
|---|---|
|
||||
| [`SPEC.md`](SPEC.md) | **System requirements** — `PR-nnn` project goals, `SR-nnn` cross-component contracts |
|
||||
| [`CLAUDE.md`](CLAUDE.md) | Working notes and the invariants that must not be violated silently |
|
||||
| Each repo's `SPEC.md` | That component's software requirements |
|
||||
| Each repo's `docs/requirements.md` | Its stable requirement IDs, status, and verification plan |
|
||||
|
||||
Read the system spec first. Every component requirement traces up to an
|
||||
`SR-nnn`, and every `SR-nnn` to a `PR-nnn`, so the chain explains *why* a given
|
||||
piece of code exists.
|
||||
|
||||
---
|
||||
|
||||
## Requirement traceability
|
||||
|
||||
Requirements are traceable **up** to the project goal and **down** to the code
|
||||
implementing them. A requirement nothing traces to is either unnecessary or
|
||||
unimplemented, and both are worth knowing.
|
||||
|
||||
```
|
||||
PR-nnn project requirement (SPEC.md §1) — why the system exists
|
||||
└─ SR-nnn system requirement (SPEC.md §3) — what spans components
|
||||
└─ component requirement (each repo's docs/requirements.md)
|
||||
└─ TRACES tag (source)
|
||||
```
|
||||
|
||||
Tag the code that *satisfies* a requirement:
|
||||
|
||||
```rust
|
||||
/// TRACES: UR-003 | SR-004
|
||||
pub fn validate_manifest(m: Jmanifest) -> VResult<ValidManifest> { … }
|
||||
```
|
||||
|
||||
A pipe separates requirement *types*; a comma separates IDs within a type. Tag
|
||||
the unit that decides, not every helper it calls — a tag on every function is
|
||||
noise, and rots faster than it helps.
|
||||
|
||||
The gate reports coverage, orphan tags (an ID no register defines), and untraced
|
||||
requirements. Two rules it inherits from JellyTau, both learned the hard way:
|
||||
|
||||
- **Denominators are read from the register at run time, never hardcoded.** A
|
||||
gate that divides by a frozen literal reported 158% coverage for months and so
|
||||
could never fail — worse than no gate, because it was trusted.
|
||||
- **Coverage above 100% is a hard failure**, not a pass. It means the
|
||||
computation is broken, and it is the signal that catches the above
|
||||
immediately.
|
||||
|
||||
---
|
||||
|
||||
## Licence
|
||||
|
||||
GPLv3, matching the Jellyfin plugin it serves.
|
||||
@@ -0,0 +1,474 @@
|
||||
# JRay — system specification
|
||||
|
||||
Status: **draft**.
|
||||
|
||||
This is the **system** spec: it owns the requirements that span more than one
|
||||
component, and the contracts between them. Each component repo carries a
|
||||
*software* spec whose job is to implement this one.
|
||||
|
||||
| Component | Software spec | Role |
|
||||
|---|---|---|
|
||||
| `scene-actor-extraction` | [`docs/SPEC.md`](scene-actor-extraction/docs/SPEC.md) | Produces presence data from media |
|
||||
| `jRay` (Jellyfin plugin) | [`SPEC.md`](jRay/SPEC.md) | Consumes and displays it; owns the truth-file format |
|
||||
| `JRay-public-server` | [`SPEC.md`](JRay-public-server/SPEC.md) | Exchanges presence data between instances |
|
||||
|
||||
A requirement belongs here if changing it requires changing more than one repo.
|
||||
Everything else belongs in a component spec.
|
||||
|
||||
---
|
||||
|
||||
## 1. What the system does
|
||||
|
||||
**Project goal: a self-hosted alternative to Amazon X-Ray.** While watching, the
|
||||
viewer can see who is on screen — for their own library, on their own hardware,
|
||||
with nobody else learning what they own.
|
||||
|
||||
Everything below traces to that. The project requirements are the top of the
|
||||
traceability chain (§6):
|
||||
|
||||
| ID | Project requirement | Why it is not negotiable |
|
||||
|---|---|---|
|
||||
| **PR-001** | Show which actors are on screen at a given moment, in the player | The product. Without it there is nothing. |
|
||||
| **PR-002** | Answer at **scene** granularity, as X-Ray does | The thing being replaced works this way, and it is the more useful question (SR-002) |
|
||||
| **PR-003** | Fully automatic — no per-title manual work | Manual labelling does not scale past a handful of titles; a library is thousands |
|
||||
| **PR-004** | Run on the user's own hardware, against their own library | Self-hosted is the point; a cloud dependency recreates what is being replaced |
|
||||
| **PR-005** | Leak nothing about what the user owns | A service that must be told which titles you hold is not an alternative to a service that already knows |
|
||||
| **PR-006** | Compute once per *cut*, not once per user | Extraction is expensive; identical cuts yield identical timings, so repeating the work per-user is pure waste |
|
||||
|
||||
Three capabilities follow:
|
||||
|
||||
1. **Extract** — derive presence data from a media file (`scene-actor-extraction`).
|
||||
Serves PR-001, PR-002, PR-003, PR-004.
|
||||
2. **Serve** — surface it in the player at the right moment (`jRay`). Serves PR-001.
|
||||
3. **Share** — let one user's extraction spare every other user the same compute
|
||||
(`JRay-public-server`). Serves PR-006, constrained by PR-005.
|
||||
|
||||
---
|
||||
|
||||
## 2. Data flow
|
||||
|
||||
```
|
||||
media file ──► extraction ──► truth file (sidecar or pushed) ──► plugin ──► player overlay
|
||||
▲ │
|
||||
│ ▼
|
||||
gallery Jmanifest ──► public server ──► other instances
|
||||
(Jellyfin + TMDB)
|
||||
```
|
||||
|
||||
Two independent axes, deliberately:
|
||||
|
||||
- **Presence data** flows outward and is shareable — it is a set of timings
|
||||
against public identifiers.
|
||||
- **Gallery data** (actor reference faces) is built locally from the user's own
|
||||
Jellyfin instance plus TMDB, and does **not** flow outward. See SR-005.
|
||||
|
||||
---
|
||||
|
||||
## 3. System requirements
|
||||
|
||||
### SR-001 — Identity is expressed in public identifiers
|
||||
|
||||
Every actor reference crossing a component boundary is a `tmdb_id` / `imdb_id`
|
||||
(and, locally, a Jellyfin Person GUID). Never a name alone, never a local index.
|
||||
|
||||
Rationale: names are ambiguous and unstable; local indices are meaningless
|
||||
outside the process that made them. Public identifiers also make the public
|
||||
server's abuse model tractable — it stores references to entities that already
|
||||
exist in TMDB, not attacker-authored text (`JRay-public-server/SPEC.md` §5a).
|
||||
|
||||
### SR-002 — Presence is scene-scoped, not instantaneous
|
||||
|
||||
**The question the system answers is "is this actor in this scene?", not "is this
|
||||
actor's face visible in this frame?"**
|
||||
|
||||
This is the defining semantic of the whole system, so it is stated here rather
|
||||
than in any component spec.
|
||||
|
||||
It is set by the ground truth we emulate. Amazon X-Ray ships one cast list per
|
||||
scene span — there is no per-frame or per-second annotation anywhere in its data
|
||||
(`scene-actor-extraction/docs/methodology.md`). If X-Ray credits five actors to a
|
||||
30-second scene, all five are present for all 30 seconds, including the seconds
|
||||
where only one is on camera.
|
||||
|
||||
Consequences that bind every component:
|
||||
|
||||
- **An actor who turns away, is occluded, or is off-camera while the shot cuts to
|
||||
whoever they are speaking to is still present.** Reporting them absent is not a
|
||||
stricter answer, it is an answer to a different question.
|
||||
- A window is a claim about **scene membership**, not a recognition event. A
|
||||
consumer must never interpret window boundaries as "the face was detected here".
|
||||
- Gaps shorter than the re-acquisition timeout are absorbed *into* a window and
|
||||
claimed as presence. Two separate windows therefore mean a genuine departure and
|
||||
return, not merely a break in detection.
|
||||
- Windows may be numerous; consumers must not assume a handful of long ones.
|
||||
|
||||
**Why this is the differentiator.** A plugin that decodes a single frame on pause
|
||||
and runs face detection on it answers the instantaneous question. Against
|
||||
scene-level truth it produces a false negative every time the camera is on someone
|
||||
else — which, in dialogue, is most of the time. It also cannot know who *just*
|
||||
left or is about to speak. Precomputed scene-scoped presence is a different and
|
||||
strictly more useful product, and the extra cost is paid once per title rather
|
||||
than on every pause.
|
||||
|
||||
**A window ends at the last sighting, never after it.** This is the rule that
|
||||
prevents over-claiming, and the previous design broke it: `extinction_sec` kept
|
||||
an actor *active* for 57 s past their last detection, so presence ran on into
|
||||
whatever followed — most visibly the closing credits, inheriting the final
|
||||
scene's cast (the Downton Abbey recall collapse, `lvface-deep-dive.md`).
|
||||
|
||||
Ending each window at the last frame the face was actually seen removes that
|
||||
class of error outright. Gaps *between* two sightings are absorbed (an actor who
|
||||
turns away is still in the scene); time *after* the final sighting never is.
|
||||
|
||||
Cuts and scene boundaries are **association hints, not presence events**. Both
|
||||
say the same thing to the tracker — spatial continuity is broken, so associate on
|
||||
embedding similarity rather than position — and neither closes a window. An actor
|
||||
who genuinely continues across a boundary is kept; one who does not reappear
|
||||
times out and closes at their last sighting.
|
||||
|
||||
### SR-003 — Schema changes are coordinated across all three repos
|
||||
|
||||
The truth file and the Jmanifest are consumed by components that ship
|
||||
independently. Therefore:
|
||||
|
||||
- `schema_version` is a **system-level** version, incremented once per breaking
|
||||
change, and every repo's spec references the same number.
|
||||
- Breaking changes are **batched**. Two pending breaks ship as one bump.
|
||||
- A consumer encountering an unknown `schema_version` refuses or warns; it never
|
||||
guesses.
|
||||
|
||||
**Currently pending — these ship together as one bump:**
|
||||
|
||||
| Change | Origin | Effect |
|
||||
|---|---|---|
|
||||
| Remove `anneal_sec` | SR-002 / A6 | Field describes a mechanism that no longer exists |
|
||||
| Add `extinction_sec` | A6 | Its successor: the parameter that shapes window extent |
|
||||
| Add `gallery_scope` | C1 | `global` / `limited` — the strongest quality signal between competing manifests |
|
||||
| Per-window belief | A6 / A9 | Windows carry the posterior that justified them, plus identification route |
|
||||
| Add audio signature | C2 | Self-identifying truth files |
|
||||
|
||||
Removing `anneal_sec` rather than retaining it as a vestigial `0` is deliberate:
|
||||
a field naming a mechanism the pipeline no longer has is actively misleading to
|
||||
anyone reading a manifest, and would outlive everyone who remembers why it is
|
||||
zero.
|
||||
|
||||
**Per-window belief changes the shape of `scenes`**, which is currently a list of
|
||||
float pairs. It becomes a list of objects carrying the interval plus its
|
||||
posterior and route. Note this **does not** weaken SR-004: the added fields are a
|
||||
bounded float and a small enumerated string, so an accepted manifest still
|
||||
contains only numbers and closed-vocabulary values. No free-form channel is
|
||||
opened.
|
||||
|
||||
Owner of the bump: to be assigned (see §4).
|
||||
|
||||
### SR-004 — The public server never stores binary content
|
||||
|
||||
The server's abuse defence is structural, not procedural: **there is nowhere to
|
||||
put a payload**. An accepted manifest contains only bounded numbers, constrained
|
||||
identifiers, and — at most — names that must resolve to real TMDB persons
|
||||
(`JRay-public-server/SPEC.md` §5a, Threat 1). No binary, no images, no URLs, no
|
||||
base64, no extension points.
|
||||
|
||||
**This is load-bearing and must not be eroded.** It is what makes it safe for a
|
||||
volunteer to operate an instance: there is no CSAM risk and no payload-smuggling
|
||||
risk because no field can carry either. Any proposal to ship images, embeddings,
|
||||
or opaque blobs through the manifest server is a proposal to delete that
|
||||
property, and must be treated as such rather than as an incremental feature.
|
||||
|
||||
**The audio signature is the one encoded field, and it is not a precedent.** It
|
||||
is base64, so the flat reading of "no base64" needs qualifying — otherwise
|
||||
someone later cites SR-004 to argue the signature out, or cites the signature to
|
||||
argue images in. Neither is right. What makes it acceptable is that it is
|
||||
**fully constrained**, not that it is small:
|
||||
|
||||
- fixed length (~1290 bytes) derived from a fixed 120 s window — not
|
||||
caller-chosen;
|
||||
- fixed alphabet, validated on parse;
|
||||
- every byte is a peak-bin index plus a 2-bit energy class, so the value space is
|
||||
enumerable and a payload cannot be smuggled in it;
|
||||
- it is verifiable — a server can recompute it from the same input and compare.
|
||||
|
||||
The test for any future encoded field is that list, not its size. A field whose
|
||||
length or content is attacker-chosen fails it regardless of how small it is.
|
||||
|
||||
### SR-005 — Gallery data never leaves the instance
|
||||
|
||||
Actor reference faces are built from the user's own Jellyfin instance and TMDB
|
||||
profile images. **They are never shared, exported, or transmitted — not between
|
||||
instances, not to a server, and not by a user who wants to.** There is no export
|
||||
path, deliberately.
|
||||
|
||||
This is the strict form on purpose. A user-initiated export was considered and
|
||||
rejected: it moves a legal judgement onto the user while the capability, and
|
||||
therefore the exposure, belongs to the software.
|
||||
|
||||
Three reasons, each independently sufficient:
|
||||
|
||||
- **Legal risk.** TMDB profile images carry their own licensing; face crops
|
||||
extracted from a film are derivative works of a copyrighted title; and faces
|
||||
are personal data and likenesses. Recognising one's own library locally is a
|
||||
materially different act from distributing crops or their derivatives to third
|
||||
parties. Not shipping them avoids the question rather than answering it.
|
||||
- **SR-004.** Images cannot cross the public server without destroying its
|
||||
structural abuse defence.
|
||||
- **PR-005.** A mature gallery is a direct fingerprint of a library. One that has
|
||||
learned an actor's profile views across forty of *your* films encodes which
|
||||
forty films those were.
|
||||
|
||||
That third reason **strengthened** with D4. When the gallery held only downloaded
|
||||
headshots it was largely public data; now it accumulates harvested embeddings
|
||||
derived from the user's own media, and later human-confirmed associations. The
|
||||
artifact became more valuable and more personal at the same time.
|
||||
|
||||
Two consequences, both accepted:
|
||||
|
||||
- **Gallery quality is per-instance.** Two users with the same film may get
|
||||
different results. Accepted — the alternative is the exposure above.
|
||||
- **Human associations do not compound across users.** Everyone labels their own
|
||||
unknowns. Accepted for the same reason.
|
||||
|
||||
---
|
||||
|
||||
## 4. Human-in-the-loop actor association
|
||||
|
||||
*Proposed; not yet designed in any component spec.*
|
||||
|
||||
The pipeline necessarily produces **unidentified tracks** — a face that is
|
||||
genuinely someone, sustained across many frames, that the gallery cannot name.
|
||||
Today they are discarded. They are the system's best signal about its own blind
|
||||
spots, and a user watching the film usually knows exactly who the person is.
|
||||
|
||||
**The capability:** surface unidentified tracks in the plugin, let the user
|
||||
associate one with an actor, and feed that association back into the local
|
||||
gallery so subsequent extractions recognise them.
|
||||
|
||||
This is valuable precisely where automatic extraction is weakest: character
|
||||
actors, ageing across a career, heavy makeup, and cast members TMDB has no
|
||||
usable headshot for.
|
||||
|
||||
### What each component contributes
|
||||
|
||||
| Component | Contribution |
|
||||
|---|---|
|
||||
| `scene-actor-extraction` | Retain unidentified tracks, **clustered into one entity per unknown person** (A7.4): embeddings, metadata, and **context crops** — a wider region than the 112×112 aligned face, so a human can actually recognise the person and the shot they are in |
|
||||
| `jRay` | Review UI: show the cluster's crops, let the user pick an actor from the title's cast (or search TMDB), record the association |
|
||||
| `scene-actor-extraction` | Ingest confirmed associations into the local gallery as additional reference embeddings for that `tmdb_id` |
|
||||
|
||||
**The unit of review is a person, not a track.** Because unknown tracks are
|
||||
clustered before they reach a human, the question is "who is this person, who
|
||||
appears in these twelve places?" rather than twelve disconnected questions about
|
||||
twelve faces. One answer resolves the whole cluster. This is what makes the
|
||||
feature tractable to use rather than tedious — and the clustering is already
|
||||
required for recognition (A7.4), so the review UI inherits it for free.
|
||||
|
||||
The 112×112 aligned crop is optimised for ArcFace, not for humans — tightly
|
||||
cropped, geometrically normalised, often unrecognisable out of context. A
|
||||
separate, larger crop is required for the review UI. Storage is bounded by
|
||||
retaining a handful of representative frames per track, not every frame.
|
||||
|
||||
### Where the gallery lives — settled: local only
|
||||
|
||||
The appeal of moving gallery ownership to a server is obvious: one user's
|
||||
association would help everyone, and the gallery would improve monotonically
|
||||
across the community rather than per-instance.
|
||||
|
||||
**It is nonetheless ruled out.** SR-005 is strict, on legal grounds, and that
|
||||
disposes of both the shared-service and the user-initiated-export variants.
|
||||
|
||||
Two further reasons it would not have worked even setting rights aside:
|
||||
|
||||
- **SR-004 blocks the manifest server specifically.** Shipping crops destroys its
|
||||
structural abuse defence, and embeddings are not a loophole: a 512-float array
|
||||
is a 2 KB opaque binary field, exactly the extension point the design
|
||||
deliberately lacks. Unit-norm constraint does not meaningfully reduce its
|
||||
payload capacity.
|
||||
- **A gallery-sharing service would be a different service** — accepting binary
|
||||
content, therefore needing moderation, provenance, and an operator willing to
|
||||
carry that liability. Bolting it onto the manifest server would forfeit the
|
||||
property that makes the manifest server safe to run.
|
||||
|
||||
**So: associations stay on the instance that made them.** No new trust surface,
|
||||
no rights exposure, no sharing benefit. This is not a compromise position — the
|
||||
recognition benefit is almost entirely captured locally anyway, since the
|
||||
associations that matter to a user are for the titles that user owns.
|
||||
|
||||
---
|
||||
|
||||
## 5. Open system questions
|
||||
|
||||
1. **Who drives the SR-003 bump?** Three repos must move together. **The
|
||||
transition half is settled: flag day** — the plugin accepts `schema_version: 2`
|
||||
and rejects everything else, on every path, with no dual-accept period
|
||||
(`JR-003`). All three components are pre-release, and a v1 read path would be
|
||||
the one nobody exercises, so it is the one that would rot while being carried
|
||||
through every subsequent change to the reader.
|
||||
|
||||
The consequence is accepted and must be stated to users rather than
|
||||
discovered: **existing v1 sidecar files stop being read** and stay dark until
|
||||
the library is re-extracted. The plugin logs this per item, naming the file
|
||||
and the version found, so a stale item is not mistaken for an un-extracted
|
||||
one.
|
||||
|
||||
Still open: **who sequences the three merges**, and whether the server should
|
||||
accept `jmanifest_version: 1` for a period after the plugin stops — its
|
||||
exposure is different, since it holds other people's contributions rather
|
||||
than one user's files.
|
||||
2. **Should unidentified presence be published?** An unowned track is *someone*
|
||||
on screen. Emitting it as anonymous presence ("a face is here, unnamed") would
|
||||
let the plugin show "unidentified person" rather than nothing — and would give
|
||||
§4's review UI its work queue directly from the truth file. It also enlarges
|
||||
the format and invites consumers to display something unhelpful. Undecided.
|
||||
3. **Episode-level gallery scope.** A series' recurring cast is stable across
|
||||
episodes; re-deriving the gallery per episode is wasteful, and associations
|
||||
made while watching episode 1 should apply to episode 12. Not yet specified
|
||||
anywhere.
|
||||
|
||||
---
|
||||
|
||||
## 6. Traceability
|
||||
|
||||
Every requirement must be traceable **up** to the project goal and **down** to the
|
||||
code that implements it. A requirement nothing traces to is either unnecessary or
|
||||
unimplemented, and both are worth knowing.
|
||||
|
||||
### The chain
|
||||
|
||||
```
|
||||
PR-n project requirement (this document, §1) — why the system exists
|
||||
└─ SR-n system requirement (this document, §3) — what spans components
|
||||
└─ A1…E8 / §n software requirement (component specs) — what one repo does
|
||||
└─ @implements tag (source) — where it actually is
|
||||
```
|
||||
|
||||
### ID scheme
|
||||
|
||||
Zero-padded three digits throughout. **IDs are permanent**: a withdrawn
|
||||
requirement is marked `Withdrawn` and its number never reused, because
|
||||
renumbering is exactly what produces orphan tags.
|
||||
|
||||
| Prefix | Scope | Register |
|
||||
|---|---|---|
|
||||
| `PR-nnn` | Project | This document §1 |
|
||||
| `SR-nnn` | System | This document §3 |
|
||||
| `AR-nnn` | Extraction — algorithm | [`scene-actor-extraction/docs/requirements.md`](scene-actor-extraction/docs/requirements.md) |
|
||||
| `DP-nnn` | Extraction — deployment | ″ |
|
||||
| `IR-nnn` | Extraction — integration | ″ |
|
||||
| `GR-nnn` | Extraction — gallery | ″ |
|
||||
| `VR-nnn` | Extraction — validation | ″ |
|
||||
| `JR-nnn` | Plugin — all of it | [`jRay/docs/requirements.md`](jRay/docs/requirements.md) |
|
||||
| `UR-nnn` | Public server — all of it | `JRay-public-server/docs/requirements.md` |
|
||||
| `UT-nnn` / `IT-nnn` | Tests | Per-component register |
|
||||
|
||||
Each component keeps a `requirements.md` register — the authoritative ID list,
|
||||
with prose in its `SPEC.md`. **The CI gate reads its denominators from the
|
||||
register at run time.**
|
||||
|
||||
**`JR` is flat where extraction is themed.** Extraction splits by concern because
|
||||
its five areas have genuinely different verification strategies and audiences;
|
||||
the plugin is one deployable with one audience, so a flat namespace with
|
||||
thematic *section headings* carries the same grouping without a prefix that can
|
||||
go stale. The server's `UR` is flat for the same reason.
|
||||
|
||||
**Two register gaps remain, recorded rather than omitted:**
|
||||
|
||||
- `JRay-public-server/docs/requirements.md` **does not exist**, though its
|
||||
`SPEC.md` §0 cites it as authoritative and its `UR-nnn` IDs are already
|
||||
regularised. Its matrix rows stay empty until it does.
|
||||
- `UT-nnn`/`IT-nnn` are per-component, so `UT-001` will exist in more than one
|
||||
repo. Fine while the gate runs per repo; ambiguous the moment a rollup spans
|
||||
them.
|
||||
|
||||
### Code annotation
|
||||
|
||||
**Format follows the house convention already in use in
|
||||
[`JellyTau`](../JellyTau/docs/traces-quick-ref.md)** — adopt its tooling rather
|
||||
than inventing a parallel scheme:
|
||||
|
||||
```
|
||||
// TRACES: UR-001, UR-002 | DR-003
|
||||
```
|
||||
|
||||
Pipe separates requirement *types*; comma separates IDs within a type.
|
||||
|
||||
```cpp
|
||||
/// TRACES: AR-006, AR-009 | SR-002
|
||||
struct TrackRegistry { … };
|
||||
```
|
||||
|
||||
```python
|
||||
def build_gallery(...):
|
||||
"""Build a gallery from Jellyfin plus TMDB fallback.
|
||||
|
||||
TRACES: GR-001 | SR-005
|
||||
"""
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- The tag names **what the code satisfies**, not where it lives. One requirement
|
||||
may be tagged in several places; one place may satisfy several requirements.
|
||||
- Tag the unit that *decides*, not every helper it calls. A tag on every function
|
||||
is noise and rots faster than it helps.
|
||||
- **Tests carry TRACES too** (`UT-nnn`, `IT-nnn`), which is what demonstrates a
|
||||
requirement is *verified* rather than merely implemented.
|
||||
- **A deliberate exception to an invariant is tagged** —
|
||||
`EXCEPTION: AR-009 raw cosine used here because <reason>`. Per
|
||||
[`CLAUDE.md`](CLAUDE.md), a bare cosine with no recorded exception is a defect.
|
||||
|
||||
### Verification
|
||||
|
||||
**Requirement: a CI gate that checks the chain.** Hand-maintained traceability
|
||||
rots within weeks; only the automated check makes it worth having. The matrix is
|
||||
**generated, never hand-written**.
|
||||
|
||||
It must report:
|
||||
|
||||
- **Coverage** — requirements with at least one TRACES tag, over requirements
|
||||
defined.
|
||||
- **Orphan tags** — a TRACES naming an ID that no spec defines. This is what
|
||||
renumbering produces, and what a typo produces.
|
||||
- **Untraced requirements** — a software requirement citing no `SR-nnn`, or an
|
||||
`SR-nnn` citing no `PR-nnn`. Catches scope creep: work serving no stated goal.
|
||||
- **Unserved goals** — a `PR-nnn` or `SR-nnn` nothing traces up to, which is how
|
||||
a goal quietly stops being pursued.
|
||||
|
||||
**CI has no GPU** — an Intel N100 board. A requirement verifiable only on GPU
|
||||
hardware must be reported as *tagged but unexecuted*, never counted as covered.
|
||||
A gate that treats "has a test that never runs" as passing is the same failure
|
||||
mode as the 158% coverage bug below: it reports success it cannot substantiate.
|
||||
Component registers carry the per-requirement verification tier.
|
||||
|
||||
**Two rules inherited from JellyTau's gate, both learned the hard way** (see
|
||||
`JellyTau/docs/specs/traceability-gate-repair.md`):
|
||||
|
||||
1. **Denominators are computed from the spec at run time — never hardcoded.**
|
||||
JellyTau's gate divided by frozen literals while the requirement count grew to
|
||||
211, so it reported *158% coverage* and the threshold could never trip. A gate
|
||||
that cannot fail is worse than no gate, because it is trusted.
|
||||
2. **Coverage above 100% is a hard failure**, not a pass. It means the
|
||||
computation is broken, and it is the signal that would have caught (1)
|
||||
immediately.
|
||||
|
||||
### Matrix
|
||||
|
||||
**Generated, not hand-written** — `docs/traceability.md` in each component,
|
||||
produced by the extract-traces tool and timestamped. The per-requirement parent
|
||||
links live in each register's `Traces to` column; the tool rolls them up.
|
||||
|
||||
Porting from JellyTau: `scripts/extract-traces.ts` and
|
||||
`scripts/generate-traceability-matrix.sh` need a C++/Python scanner added
|
||||
alongside the existing TypeScript/Rust/Svelte one, and the workflow adapting.
|
||||
The gate logic transfers unchanged.
|
||||
|
||||
Two facts the rollup should surface, noted now because they are easy to
|
||||
misread as gaps:
|
||||
|
||||
- **`DP-*` and `VR-*` trace to no system requirement.** Deployment and parameter
|
||||
studies are single-repo concerns serving `PR-004` and `PR-002` directly. This
|
||||
is correct.
|
||||
- **`PR-005` (leak nothing) has no software row at all.** It is satisfied
|
||||
*structurally* — `SR-004` (server holds no binary) and `GR-005` (gallery never
|
||||
leaves the instance) — rather than by any component doing something. It cannot
|
||||
be verified by pointing at code, and it dies the moment either prohibition is
|
||||
relaxed. A goal preserved only by prohibitions needs watching precisely because
|
||||
nothing traces to it.
|
||||
Reference in New Issue
Block a user