Tagging and finding footage
tags.json (schema video_review_os.tags.v2) makes a raw library findable. v2 is
strictly additive over v1: the source-level tags record keeps every key it always had
and gains a visual summary, joined by a new per-clip clips list.
video-review-os tag <project> # writes tags.json
video-review-os library speech-search "…" # find a spoken phrase across every project
Source level
event_type, performer, track, venue, energy, has_vocal, orientation,
usable_vertical, duration, quality_flags, and a first-class bucket. A clip never
leaves the tagger without a bucket — the default is broll_trending.
Bucket logic is deliberately simple and inspectable:
- identified performer + vocal + event type →
real_gfa(the high-craft lane) - vocal + second-person address ("you", "here's how", "if you") →
direct_camera - everything else →
config.tagging.default_bucket
Populate [tagging] roster and venues with the names you actually shoot; matching is
whole-word against filename plus transcript. An empty roster is why performer is null.
Clip level
Each entry in clips carries topics (frequent meaningful words from that clip's text),
has_speech, the visual attributes perception found, reframe_needed, and
quality_flags. With no perception.json the visual fields are all unknown and
everything else still works — perception is optional, not a prerequisite.
tags["visual"] rolls the per-clip readings into a source-level summary. unknown never
wins a vote there: a source with two known readings and eight unknowns describes itself by
the two.
When a tag is wrong
Correct it in the tag_overrides.json sidecar, keyed by approval_key — never by editing
tags.json, which is regenerated. Overrides keyed by content hash survive re-tagging;
overrides keyed by position do not, which is why the sidecar shape is what it is.
Using a model for tagging
Same contract as every other stage — a provider name in config, no vendor in the code:
[tagging]
provider = "generic-http"
hosted_endpoint_env = "VIDEO_REVIEW_TAG_ENDPOINT"
hosted_api_key_env = "VIDEO_REVIEW_TAG_API_KEY"
The response is sanitized: unknown buckets and orientations fall back to the deterministic value, so a hostile or confused endpoint cannot introduce a bucket the rest of the system does not understand. Verify this holds if you extend the schema — every new field needs a sanitize rule and a test.