Local video clipping infrastructure for creators and small teams.
Drop raw videos into a folder. Video Review OS creates project folders, transcribes the source, finds candidate clips, scores clip quality, turns the gate's findings into actionable repair ops, assembles finished multi-range edit drafts (trim filler, drop dead air, combine moments, add bridge/title cards), and renders them to MP4, writes caption files, pulls frame stills, makes thumbnail and scene-card SVG drafts, prepares copy, builds a static review dashboard, and can prepare an approved manual post queue.
It does not just sort clips into keep/reject. It repairs and assembles them into watchable drafts automatically — then waits for a human.
The default is still review-only. It does not publish automatically, does not connect social accounts by default, and does not move files into handoff folders unless you explicitly build that workflow around it. Reject clips are never assembled, never rendered, and never queued.
Video Review OS is for the first-pass review work that happens before a clip becomes public:
- Ingest raw videos from a local watch folder.
- Inspect media with
ffprobe. - Transcribe with local Whisper, faster-whisper, hosted adapters, or deterministic fallback.
- Select candidate clip ranges from transcript and timing data.
- Tag each source with a content record (event type, performer, venue, energy, vocal presence, orientation, content bucket) so a raw library is findable and feeds a content conveyor.
- Flag clips that fail from the audience's point of view, and emit deterministic repair ops (trim the weak ending, drop the awkward pause, cut the filler opening) instead of only flags.
- Propose a storyboard (deterministic fallback, or your own LLM endpoint) that can combine, reorder, and bridge clips into finished assemblies.
- Resolve assemblies into a concrete edit decision list (EDL) of source ranges plus title/bridge cards, applying the repair ops.
- Cut/render MP4 drafts with
ffmpeg, including multi-range assemblies stitched from several source ranges plus generated cards. - Write SRT and VTT caption sidecars.
- Optionally burn captions into MP4 renders.
- Extract representative frame stills for each candidate clip.
- Generate thumbnail and vertical scene-card SVG drafts from frames.
- Overlay optional local mascot/logo assets from config.
- Draft hooks, titles, captions, and review notes.
- Build
dashboard.jsonand a staticindex.htmlreview dashboard. - Track local approval state tied to the exact source hash, time range, and transcript text.
- Prepare
post_queue.jsononly for clips that are approved and rendered. - Diarize transcripts with an optional, pluggable provider so search and clipping can filter by speaker id or role.
- Search spoken phrases across every ingested project at once — archive mining, not just one-video clipping.
- Capture reusable proof assets (hooks, testimonials, objection answers) into a durable, source-backed quote ledger, independent of whether a clip ever renders.
- Classify candidate/quote intent and score them against platform profiles (TikTok, testimonial, ad hook, case study) as an additive layer on top of the quality gate.
- Dedupe near-identical or overlapping quotes — within a project or across the whole library — and surface the best version of a topic.
- Track lineage from source video through transcript, quote, candidate, storyboard, assembly, render, and post queue, and detect when an upstream change leaves an approval or quote stale.
- Export evidence-backed quote banks, hooks, ad briefs, and Brain/Kai handoff bundles as local files. Nothing here publishes.
| Output | What it means |
|---|---|
| Source project folder | A clean local folder for each raw video. |
source.json |
File hash, original path, active path, media metadata, streams, codecs, and safety state. |
transcript.json |
Transcript segments and word timestamps when transcription is configured. |
clips.json |
Candidate clip ranges with keep, trim, review, or reject decisions. Candidates also carry a transcript_sha256 lineage ref and, once score has run, an additive intent/intent_confidence/profile_scores layer that never changes the decision. |
tags.json |
Content tag record for the source: event type, performer, venue, energy, vocal presence, orientation, usable_vertical, and a first-class content bucket. |
quality_gate entries |
Audience-first flags plus repair_ops (actionable trim/drop/lead-in instructions) for each candidate. |
silence.json |
Detected audio dead-air intervals (silencedetect) the assembly layer drops. |
storyboard.json |
Proposal for how candidates become assemblies: members, order, bridge/title cards, rationale. |
assemblies.json |
Finished edit decision lists (EDLs): ordered source ranges + cards, applied/unresolved ops, signature. Reject clips never appear. |
assembly_renders.json |
Render manifest for the auto-generated assembly MP4 drafts. |
assembly_captions.json + captions/assemblies/ |
Full-timeline SRT/VTT for each assembly, plus per-segment SRTs burned into renders. |
assembly_post_queue.json |
Manual post queue for approved, rendered assemblies. Auto-publishing remains disabled. |
speech_hits.json |
Timestamp-backed transcript search hits from speech-search (speaker-aware schema, diarization-ready). |
speaker_roles.json |
Manually assigned/corrected speaker roles (e.g. speaker_2 → customer), keyed by diarized speaker id. |
quote_ledger.json |
Reusable, source-backed proof assets (hooks, testimonials, objection answers) with a candidate → approved → used (or rejected/superseded) status lifecycle. |
assembly_edits.json |
Human edit ops (nudge/drop/move/card text) addressed by stable segment uid, applied at assemble time. |
exports/ + exports.json |
OpenTimelineIO (.otio) and CMX3600 (.edl) interchange exports per assembly, with signature-based staleness tracking. |
exports/content/ |
Evidence-backed content exports: quotes/hooks (markdown or json), ad_brief.md, brain.json, kai_brief.md. Local files only — never published. |
library/index.json |
Cross-project transcript summary built by library index, one row per ingested project. |
captions/ |
SRT and VTT caption sidecars for visible candidates. |
captions.json |
Caption manifest with cue counts and source signature. |
scenes/ |
Frame stills extracted from candidate clip ranges. |
scenes.json |
Scene-frame manifest with timestamps and extraction status. |
visuals/ |
SVG thumbnail drafts and vertical scene-card drafts made from frames. |
visuals.json |
Visual draft manifest, including optional mascot/logo usage. |
drafts/copy.json |
Hook, title, caption, copy quality notes, and deterministic fallback copy. |
renders/ |
Optional MP4 drafts for clips allowed by policy. |
renders.json |
Render manifest, including whether captions were burned in. |
dashboard.json |
Machine-readable review state. |
dashboard/index.html |
Static mobile-friendly dashboard for human review. |
approvals.json |
Local approval state tied to the exact source clip and time range. |
post_queue.json |
Manual post queue for approved rendered clips. Auto-publishing remains disabled. |
Mermaid source
flowchart LR
A["Raw video folder"] --> B["Ingest project"]
B --> C["ffprobe media inspection"]
B --> D["Transcribe words"]
C --> E["Clip candidate selection"]
D --> E
E --> F["Audience quality gate + repair ops"]
F --> G{"Decision"}
G -->|keep / trim / review| T["Storyboard proposal (combine / reorder / bridge)"]
G -->|reject| J["Never assembled, never rendered"]
SIL["Detect audio silence"] --> U
T --> U["Assemble EDL: ranges + cards, apply repair ops, drop silence"]
U --> V["Render assembly MP4 (single-pass: reframe + loudnorm + music + captions)"]
F --> K["SRT/VTT captions"]
F --> L["Frame extraction"]
L --> M["Thumbnail SVGs"]
L --> N["Scene-card SVGs"]
K --> O["Static dashboard"]
M --> O
N --> O
V --> O
J --> O
O --> P["Human approval (assembly signature)"]
P --> Q["Manual post queue"]
Q --> R["Optional adapter"]
R --> S["Explicit publish action"]
Hosted clip tools are useful when you want speed, templates, social account connection, and polished editor workflows.
Video Review OS is different. It is a local-first review pipeline for teams that want to inspect every artifact before anything leaves the machine.
It is built around simple files:
- JSON sidecars.
- SRT/VTT captions.
- Extracted JPEG frames.
- SVG visual drafts.
- MP4 render drafts.
- Static dashboard files.
No hidden database is required for the MVP. No source video is deleted.
| Tool | Best at | What Video Review OS does differently |
|---|---|---|
| Opus Clip | Hosted long-video-to-shorts workflows, AI captions, reframing, B-roll, scheduling, and social publishing. | Runs local-first, keeps JSON/SRT/VTT/SVG/MP4 artifacts inspectable, and defaults to no social connection or auto-publish. |
| Descript | Full video and podcast editing, transcription, recording, cleanup, and collaboration. | Does not try to replace a full editor. It creates a review queue, clip candidates, frames, captions, and draft assets before editing or posting. |
| Riverside Magic Clips | Recording plus AI-generated social clips, captions, and shareable outputs. | Works from ordinary local folders and emphasizes artifact auditability, quality flags, and approval state. |
| CapCut | Creative editing, templates, captions, effects, and AI-assisted video creation. | Focuses on review infrastructure: safe cutting, captions, scene frames, visual drafts, and explicit approval before handoff. |
| Manual editing | Maximum human judgment. | Reduces the first-pass review burden while keeping the final decision with a person. |
Use a hosted editor when you want an all-in-one creative suite.
Use Video Review OS when you want a boring local pipeline that shows its work.
The quality gate scores clips from the audience's point of view, not just transcript length.
It flags:
- Starts mid-thought.
- Missing context.
- Filler-heavy openings.
- Stammers and restarts.
- Slate or outtake markers such as
cut five. - Repeated placeholder or nonsense words.
- Incomplete ranges.
- Awkward silence or long pauses.
- Too-short standalone clips.
- Weak ending words.
- Generic hooks and low-value captions.
The quality gate decides whether a range is usable. The tagger answers a different question: what is this source, and where does it belong? tag reads source.json and transcript.json and writes one tags.json record per source video:
| Tag | Derived from | Example |
|---|---|---|
event_type |
filename + transcript keywords | wedding, gala, corporate |
performer / vocal_performer |
whole-word match against roster |
Olivia |
venue |
whole-word match against venues |
configured name |
energy |
transcript keywords | low, build, peak |
has_vocal |
audio stream present + transcript text | true / false |
orientation / usable_vertical |
stream geometry | horizontal → not usable vertical |
quality_flags |
media facts | horizontal_needs_reframe, no_audio_stream |
bucket |
the above, mapped to a content lane | real_gfa, direct_camera, broll_trending |
The auto-tags are a deterministic first pass meant to be inspected and refined — same contract as the storyboard and copy layers. The fallback provider is heuristic and always available; point provider = "generic-http" at your own endpoint to refine them, and the response is sanitized so a provider can only return known buckets and orientations. Every source leaves the tagger with exactly one bucket (default_bucket when no signal is found), so a downstream conveyor can plan against the content mix.
| Decision | Meaning | Default render behavior |
|---|---|---|
keep |
Strong enough to become a draft. | Becomes an assembly; renders when rendering is requested. |
trim |
Promising, but needs edit work. | Becomes an assembly with its repair ops applied (filler/pause/weak-ending trimmed). |
review |
Unclear; needs a person to inspect it. | Becomes an assembly; repair ops applied, but flagged for human attention. |
reject |
Bad range, outtake, too short, or unsafe to use. | Never assembled, never renders, never enters the post queue. |
The decision is no longer the end of the story. The quality gate also emits repair ops — concrete, deterministic edit instructions derived from the same evidence that produced the flags:
| Repair op | Triggered by | What it does |
|---|---|---|
trim_start |
Filler-heavy opening, stammer/restart | Move the in-point past the leading filler or restart. |
trim_end |
Weak ending word | Move the out-point before the trailing "and / so / but". |
drop_span |
Awkward pause | Split the range to remove the longest dead-air gap. |
add_lead_in |
Starts mid-thought, missing context | Suggestion only — needs an adjacent range, so it is left for the storyboard or a person to resolve (never guessed). |
A clip decision says whether a single range is usable. An assembly is the actual deliverable: an ordered edit decision list (EDL) of source ranges and generated cards, with repair ops applied. A plain single-range clip is just a one-segment assembly, so nothing about the simple case changes.
Two layers produce them:
- Storyboard (
storyboard.json) proposes how candidates become assemblies — which clips to combine, in what order, and where to insert title/bridge cards. The defaultfallbackprovider is deterministic (one assembly per non-reject clip). Pointprovider = "generic-http"at your own LLM endpoint to combine, reorder, and bridge moments into a stronger narrative. The hosted output is sanitized: a provider can never smuggle in a rejected or non-existent clip. - Assembly (
assemblies.json) resolves the proposal into a concrete EDL, applying each clip's repair ops (drop_spansplits a range,trim_*shrinks it), inserting card segments, and computing a signature over the whole ordered list.
{
"schema_version": "video_review_os.assemblies.v1",
"policy": { "reject_never_included": true, "auto_publish_enabled": false, "review_only": true },
"assemblies": [
{
"assembly_id": "asm-001",
"kind": "multi",
"rationale": "Combine the setup and the payoff; drop the dead air; bridge with a card.",
"source_clip_ids": ["clip-001", "clip-002"],
"member_decisions": ["keep", "trim"],
"segments": [
{ "kind": "card", "card_kind": "title", "text": "How we cut this", "duration": 1.6, "svg_path": "..." },
{ "kind": "source", "source_sha256": "abc...", "start": 1.0, "end": 7.0, "from_clip_id": "clip-001" },
{ "kind": "card", "card_kind": "bridge", "text": "Six weeks later", "duration": 1.6, "svg_path": "..." },
{ "kind": "source", "source_sha256": "abc...", "start": 12.0, "end": 15.0, "from_clip_id": "clip-002" },
{ "kind": "source", "source_sha256": "abc...", "start": 17.0, "end": 24.0, "from_clip_id": "clip-002" }
],
"applied_ops": [ { "op": "trim_end", "to": 7.0 }, { "op": "drop_span", "start": 15.0, "end": 17.0 } ],
"unresolved_ops": [],
"total_duration": 19.2,
"renderable": true,
"assembly_signature": "sha256-over-the-ordered-segment-list"
}
]
}The assembly layer is built to produce something a person would actually post, not a rough cut:
- Single-pass render.
render-assembliesbuilds the whole draft in oneffmpegfilter_complexpass — no per-segment intermediate files, no concat-demuxer. This avoids the AAC encoder-priming drift that accumulates when you concat-copy many re-encoded clips. - Audio-truth silence removal.
silencerunssilencedetectover the source and the assembly drops real dead air (breaths, room tone, untranscribed pauses), not just transcript word-gaps. - Hook-first. Title cards default to the tail (
title_card_position), so the strongest moment opens the short instead of a branded slate. - Heuristic combiner. The fallback storyboard merges a clip that starts mid-thought with its immediate lead-in, and folds in adjacent short clips — so
add_lead_ingets resolved without an LLM. Conservative: source-order only, never reorders, caps at 3 clips. - Reframe, not letterbox.
fit_mode = "blur"fills the vertical frame with a blurred background behind the contained source (no black bars);cropandpadare also available. - Finished audio. The final mix is loudness-normalized (EBU R128,
loudness_lufs) and an optionalmusic_pathis mixed in, ducked under the voice — so silent cards don't drop out. - Styled, burned-in captions on the final timeline via libass
force_style(caption_style). - Title/bridge cards draw their text with
drawtextusing an auto-detected (or configured) font; with no font they fall back to a clean color slate (text still lives in the SVG sidecar) — deterministic, never a crash. - Per-platform duration cap (
max_total_seconds): over-length assemblies are flagged (over_duration_target) and trailing segments trimmed (recorded intrimmed_seconds), never silently. - Idempotent re-render: an assembly whose signature and caption state match an existing render on disk is reported
cachedinstead of re-encoded.
Captions are applied to the final assembled timeline, not just to source clips. assembly-captions (run automatically before a burning render) re-bases each source segment's words to its position on the output timeline and writes a full-timeline captions/assemblies/<assembly_id>.srt/.vtt sidecar, plus per-segment SRTs that render-assemblies --burn-captions burns into each cut (cards stay clean). The captions always match the current EDL, so re-cutting an assembly regenerates them.
Three surfaces let a human (or any kind of editor) steer the edit without hand-editing JSON — all inside the same review-only safety model:
- Edit by what was said.
speech-search <project> "<phrase>"finds exact phrase matches in the transcript (word-timestamp precision, segment-text fallback) and writesspeech_hits.json.speech-clip <project> "<phrase>" --margin 0.4turns a hit into a normal candidate — expanded by lead/trail margins, tagged with speech provenance, and run through the same quality gate and reject rules as automatic candidates.speech-clip <project> --words START:ENDcuts from an explicit transcript word-index range instead of a phrase (this is what the dashboard's click-to-create uses). - Nudge, drop, move, retitle. Every resolved assembly segment carries a stable
segment_uidthat survives re-assembly.edit-assembly <project> <asm-id> --nudge-in UID:0.5 --nudge-out UID:-0.3 --drop UID --move UID:2 --card-text UID:"New title"records ops inassembly_edits.json; the nextassembleapplies them (clamped, never inverting a range, never dropping the last source segment), recordsapplied_edits/unmatched_edits, and recomputesassembly_signature— so every hand edit invalidates prior approvals. - Open the draft in a real NLE.
export-otiowrites an OpenTimelineIO.otiotimeline (source segments as clips with media references, cards as gaps with markers) andexport-edlwrites CMX3600 EDL — both stdlib-only, deterministic, and recorded inexports.jsonwith theassembly_signaturethey were cut from, so stale exports are detectable. Export-only interchange: rejected or non-renderable assemblies are never exported.
Past a certain point, the most valuable thing in a growing local library isn't the next raw video — it's a proof point you already recorded six projects ago. These pieces turn the repo from single-video clipping into archive mining, entirely on top of the same review-only, approval-safe posture as everything else here.
- Speaker-aware transcripts (optional).
diarize <project>runs a pluggable provider (noneby default; pointprovider = "generic-http"at your own endpoint) that assigns aspeaker_idto transcript words. Diarization is never required — transcripts without it stay valid, andspeech-search/speech-clipgain--speaker/--speaker-rolefilters once it's run. Roles (customer,founder,agent, ...) are corrected independently viaassign-speaker <project> <speaker_id> <role>, written tospeaker_roles.json— a role fix never requires re-diarizing. - Cross-project search.
library speech-search "<phrase>"searches every ingested project's transcript at once and returns stable, project-qualified refs (kai-demo-001::124.2-131.8) instead of loose prose. Like project-level search, it is strictly read-only.library indexwrites a per-project transcript summary tolibrary/index.json. - A durable quote ledger.
quote add <project>turns a search hit, an explicit transcript word range, an existing clip candidate, or a manually provided range into aquote_ledger.jsonentry — a reusable proof asset (hook, testimonial, objection answer) that doesn't depend on a clip ever being rendered.quote status <project> <quote_id> <status>moves it throughcandidate → approved → used(orrejected/superseded); onlyapproved/usedquotes are ever treated as exportable proof. - Intent + platform scoring.
score <project> --profile {tiktok,testimonial,ad-hook,case-study}classifies each candidate's intent (hook,objection,testimonial,cta, ...) and scores it against a platform profile, storing both as an additive layer onclips.jsonalongside — never instead of — the quality gate's owndecision. - Dedupe + best quote.
dedupe <project>(orlibrary dedupefor the whole archive) groups near-duplicate quotes — overlapping source ranges and/or near-identical transcript text — and marks the weaker onesupersededwith a pointer to the stronger one. It never deletes, and it never touches a quote a person already approved or used.library best-quote "<topic>"composes search, dedupe, and ranking into the top 3–10 source-backed moments for a topic across the whole library. - Lineage. Every generated artifact already carries its parents implicitly (source hash, transcript hash,
clip_candidate_id,assembly_signature, ...);lineage <project> [ref]makes that explicit — printing one artifact's ancestors and children (or every known ref, with no ref given).lineage <project> --stalere-derives each approval's key/signature and each quote's source candidate from the current artifacts and reports anything that has drifted since — e.g. a re-run ofselect-clipsthat shifted a range out from under an existing approval. - Evidence-backed exports.
export-quotes/export-hooks(--format markdown|json),export-ad-brief,export-brain, andexport-kai-briefpackage the ledger's approved/used quotes (plus separately-approved candidates) as local files underexports/content/— every item keeps its source project, filename, hash, timestamp range, transcript text, and a stable ref. Exports are local-file only; nothing here sends or publishes anything, and a rejected clip or an unapproved quote never appears in one.
video-review-os serve upgrades the static dashboard into a full review-and-edit surface. It runs the file-drop watcher, serves the dashboard, and exposes a localhost JSON API that the page's editor controls call — every endpoint is a thin wrapper over the exact same sidecar writers the CLI uses, so reject rules, approval signatures, and review-only policy apply unchanged. Opened as a plain file (no server), the same dashboard stays read-only: the editor controls only appear when /api/health answers.
In the browser you can:
- Create a clip by clicking the transcript. Click a start word and an end word, set the margin, hit Create clip from selection — the server runs the word range through
speech-clip's machinery (same quality gate; a too-short selection lands in Rejected, never renders). - Approve clips and assemblies (writes
approvals.json, keyed to content hashes/signatures as always). - Trim, drop, move, retitle per assembly segment (±0.1s/±0.5s in/out nudges, reorder arrows, drop, card-text prompt). Ops are recorded to
assembly_edits.json; the Apply edits (re-assemble) button re-runsassemble, which applies them and recomputesassembly_signature— stale approvals invalidate automatically. Revert edits clears the sidecar and restores the original cut. - Re-render and replay. A Re-render button re-runs
render-assemblies; rendered MP4s (and frame stills/thumbnails) are served over HTTP from/media/<project>/...(strictly confined to the projects directory, with byte-range support so<video>seeking works).
The dashboard distinguishes pending edits (recorded since the last assemble run) from applied ones, so you always know whether the cut on screen reflects your changes. serve binds to 127.0.0.1 by default now that it can mutate review state (--host 0.0.0.0 to opt into LAN exposure); nothing here publishes.
Video Review OS does not stop at transcript text.
For each visible candidate, it can create:
captions/<clip_id>.srtcaptions/<clip_id>.vttscenes/<clip_id>/frame-001.jpgscenes/<clip_id>/frame-002.jpgscenes/<clip_id>/frame-003.jpgvisuals/thumbnails/<clip_id>.svgvisuals/scene-cards/<clip_id>.svg
The visual drafts use extracted frames as the background or scene stack. If mascot_image_path or logo_image_path is set in config, those local assets are embedded into the generated SVGs.
This repo is published under KaiCalls, so it includes a branded example showing the actual mascot overlay path.
Included example files:
examples/assets/kai-mascot-hero.pngexamples/assets/sample-frame.svgexamples/kai-mascot-overlay-demo/config.tomlexamples/kai-mascot-overlay-demo/project/visuals/thumbnails/clip-001.svgexamples/kai-mascot-overlay-demo/project/visuals/scene-cards/clip-001.svg
The core pipeline is still config-driven. Replace mascot_image_path with your own transparent PNG if you are not using the KaiCalls branded demo.
Approval does not carry forward unless the regenerated draft matches the same:
- Source video hash.
- Clip start time.
- Clip end time.
- Transcript text hash.
For an assembly, approval is tied to the assembly_signature — a hash over the entire ordered segment list (every source range and every card's text/duration). Reordering segments, re-cutting any range, or editing a bridge card invalidates the signature, so an approval can never carry forward onto a different edit. Approve with approve-assembly.
Approval is local review state. It is not publishing permission.
This project can prepare a manual post queue. It does not publish by default.
post_queue.json (single clips) and assembly_post_queue.json (assembly drafts) only mark an item ready when:
- It is not rejected (an assembly with any reject member can never reach the queue).
- It has a matching local approval (clip
approval_key/assembly_signature). - It has a rendered MP4 draft.
Actual platform posting should live in an explicit adapter that you wire in yourself. That adapter should still require an approval check before sending anything.
Prerequisites:
- Python 3.11+
ffmpegffprobe
git clone https://github.com/YOUR-ORG/video-review-os.git
cd video-review-os
python -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev]"For local Whisper:
python -m pip install -e ".[whisper]"For faster-whisper:
python -m pip install -e ".[faster-whisper]"video-review-os init-config --path config.toml
mkdir -p raw
cp /path/to/video.mp4 raw/
video-review-os --config config.toml run-once
video-review-os --config config.toml dashboardOpen:
dashboard/index.html
Render default draft clips:
video-review-os --config config.toml run-once --renderRender with burned captions:
video-review-os --config config.toml run-once --render --burn-captionsPrepare a manual post queue after review and render:
video-review-os --config config.toml approve projects/sample-video-abc123 clip-001
video-review-os --config config.toml post-queue projects/sample-video-abc123 --platform genericvideo-review-os init-config --path config.toml
video-review-os --config config.toml scan
video-review-os --config config.toml ingest raw/video.mp4
video-review-os --config config.toml transcribe projects/sample-video-abc123
video-review-os --config config.toml select-clips projects/sample-video-abc123
video-review-os --config config.toml draft-copy projects/sample-video-abc123
video-review-os --config config.toml tag projects/sample-video-abc123
video-review-os --config config.toml captions projects/sample-video-abc123
video-review-os --config config.toml scenes projects/sample-video-abc123
video-review-os --config config.toml visuals projects/sample-video-abc123
video-review-os --config config.toml silence projects/sample-video-abc123
video-review-os --config config.toml speech-search projects/sample-video-abc123 "pricing was easy"
video-review-os --config config.toml speech-clip projects/sample-video-abc123 "pricing was easy" --margin 0.4
video-review-os --config config.toml diarize projects/sample-video-abc123
video-review-os --config config.toml speakers projects/sample-video-abc123
video-review-os --config config.toml assign-speaker projects/sample-video-abc123 speaker_1 founder
video-review-os --config config.toml speech-search projects/sample-video-abc123 "pricing" --speaker-role founder
video-review-os --config config.toml library speech-search "pricing was easy"
video-review-os --config config.toml library index
video-review-os --config config.toml library dedupe --by transcript
video-review-os --config config.toml library best-quote "pricing objection"
video-review-os --config config.toml quote add projects/sample-video-abc123 --from-candidate speech-001 --tag testimonial --tag booking
video-review-os --config config.toml quote status projects/sample-video-abc123 quote_20260719_001 approved
video-review-os --config config.toml quote list projects/sample-video-abc123 --status approved
video-review-os --config config.toml score projects/sample-video-abc123 --profile tiktok
video-review-os --config config.toml dedupe projects/sample-video-abc123
video-review-os --config config.toml lineage projects/sample-video-abc123 candidate:clip-001
video-review-os --config config.toml lineage projects/sample-video-abc123 --stale
video-review-os --config config.toml export-quotes projects/sample-video-abc123 --format markdown
video-review-os --config config.toml export-hooks projects/sample-video-abc123 --format json
video-review-os --config config.toml export-ad-brief projects/sample-video-abc123
video-review-os --config config.toml export-brain projects/sample-video-abc123
video-review-os --config config.toml export-kai-brief projects/sample-video-abc123
video-review-os --config config.toml storyboard projects/sample-video-abc123
video-review-os --config config.toml assemble projects/sample-video-abc123
video-review-os --config config.toml edit-assembly projects/sample-video-abc123 asm-001 --nudge-in a1b2c3d4e5f6:0.5 --drop f6e5d4c3b2a1
video-review-os --config config.toml edit-assembly projects/sample-video-abc123 asm-001 --list
video-review-os --config config.toml export-otio projects/sample-video-abc123
video-review-os --config config.toml export-edl projects/sample-video-abc123 --assembly asm-001
video-review-os --config config.toml assembly-captions projects/sample-video-abc123
video-review-os --config config.toml render-assemblies projects/sample-video-abc123
video-review-os --config config.toml render-assemblies projects/sample-video-abc123 --burn-captions
video-review-os --config config.toml render-assemblies projects/sample-video-abc123 --dry-run
video-review-os --config config.toml render projects/sample-video-abc123 # legacy single-clip cuts
video-review-os --config config.toml render projects/sample-video-abc123 --include trim
video-review-os --config config.toml approve projects/sample-video-abc123 clip-001 --reviewer "local-reviewer"
video-review-os --config config.toml approve-assembly projects/sample-video-abc123 asm-001
video-review-os --config config.toml post-queue projects/sample-video-abc123 --platform generic
video-review-os --config config.toml assembly-post-queue projects/sample-video-abc123 --platform generic
video-review-os --config config.toml dashboard
video-review-os --config config.toml run-once # ingest -> ... -> storyboard -> assemble
video-review-os --config config.toml run-once --render # also renders the assembled drafts
video-review-os --config config.toml watch --interval 60config.toml:
[paths]
watch_dir = "./raw"
projects_dir = "./projects"
dashboard_dir = "./dashboard"
library_dir = "./library"
[media]
ffmpeg_path = "ffmpeg"
ffprobe_path = "ffprobe"
copy_source_to_project = false
[transcription]
provider = "fallback" # fallback, whisper, faster-whisper, generic-http
model = "base"
language = ""
device = "cpu"
compute_type = "int8"
hosted_endpoint_env = "VIDEO_REVIEW_TRANSCRIBE_ENDPOINT"
hosted_api_key_env = "VIDEO_REVIEW_TRANSCRIBE_API_KEY"
[diarization]
# Optional per-word speaker metadata. Options: none, generic-http.
# Transcripts without diarization stay valid either way.
provider = "none"
hosted_endpoint_env = "VIDEO_REVIEW_DIARIZE_ENDPOINT"
hosted_api_key_env = "VIDEO_REVIEW_DIARIZE_API_KEY"
[quality_gate]
keep_threshold = 80
trim_threshold = 65
review_threshold = 45
min_clip_seconds = 12.0
ideal_min_seconds = 18.0
max_clip_seconds = 90.0
awkward_pause_seconds = 2.2
opening_word_window = 8
[copy]
provider = "fallback" # fallback, generic-http
hosted_endpoint_env = "VIDEO_REVIEW_COPY_ENDPOINT"
hosted_api_key_env = "VIDEO_REVIEW_COPY_API_KEY"
[tagging]
provider = "fallback" # fallback, generic-http
hosted_endpoint_env = "VIDEO_REVIEW_TAG_ENDPOINT"
hosted_api_key_env = "VIDEO_REVIEW_TAG_API_KEY"
roster = [] # performer names matched (whole-word) in filename + transcript
venues = [] # venue names matched the same way
buckets = ["broll_trending", "real_gfa", "direct_camera", "broll_own_audio", "broll_no_text_trending"]
default_bucket = "broll_trending"
[captions]
max_chars = 42
max_seconds = 3.5
include_decisions = ["keep", "trim", "review"]
[scenes]
frames_per_clip = 3
image_extension = "jpg"
include_decisions = ["keep", "trim", "review"]
[visuals]
thumbnail_width = 1280
thumbnail_height = 720
scene_card_width = 1080
scene_card_height = 1920
brand_accent = "#2563eb"
background = "#111827"
text_color = "#ffffff"
mascot_image_path = ""
logo_image_path = ""
include_decisions = ["keep", "trim", "review"]
[storyboard]
# How clips become assemblies. fallback = one assembly per non-reject clip.
# generic-http = your own LLM endpoint that may combine/reorder/bridge.
provider = "fallback"
hosted_endpoint_env = "VIDEO_REVIEW_STORYBOARD_ENDPOINT"
hosted_api_key_env = "VIDEO_REVIEW_STORYBOARD_API_KEY"
[assembly]
include_decisions = ["keep", "trim", "review"] # reject is never assembled
apply_repair_ops = true
combine = true # merge mid-thought clips with their lead-in + adjacent shorts
max_segments = 12
max_total_seconds = 60.0 # platform target; longer assemblies are flagged and trimmed
title_card_position = "tail" # head | tail | none — "tail" keeps the hook up front
width = 1080
height = 1920
fps = 30
fit_mode = "blur" # blur | crop | pad — fills the vertical frame without black bars
card_background = "#111827"
card_text_color = "#ffffff"
card_font_path = "" # blank = auto-detect a system font; cards draw their text into the video
card_font_size = 72
card_seconds = 1.6
min_segment_seconds = 0.4
loudness_lufs = -14.0 # normalize the final mix (EBU R128)
music_path = "" # optional background music bed, ducked under the voice
music_volume = 0.12
silence_min_seconds = 0.6 # waveform dead-air longer than this is dropped
silence_noise = "-30dB"
caption_style = "FontName=Arial,FontSize=16,Bold=1,Outline=3,Shadow=0,Alignment=2,MarginV=120,PrimaryColour=&H00FFFFFF&,OutlineColour=&H00000000&"
[render]
video_codec = "libx264"
audio_codec = "aac"
preset = "veryfast"
crf = 23
default_decisions = ["keep"]KaiCalls branded demo config:
[visuals]
brand_accent = "#5C8CFF"
background = "#12121A"
mascot_image_path = "../assets/kai-mascot-hero.png"
include_decisions = ["keep", "trim", "review"]src/video_review_os/
ingest.py
transcribe.py
diarization.py
quality_gate.py
clip_select.py
speech.py
library.py
quote.py
intent.py
dedupe.py
lineage.py
content_export.py
tagging.py
captions.py
scenes.py
visuals.py
silence.py
storyboard.py
assembly.py
render.py
copy.py
dashboard.py
approval.py
posting.py
export.py
cli.py
tests/
examples/
docs/diagrams/
visuals.json:
{
"schema_version": "video_review_os.visuals.v1",
"project_id": "sample-video-abc123",
"source_sha256": "abc123...",
"outputs": {
"thumbnails_dir": "projects/sample-video-abc123/visuals/thumbnails",
"scene_cards_dir": "projects/sample-video-abc123/visuals/scene-cards"
},
"visuals": [
{
"clip_id": "clip-001",
"decision": "keep",
"status": "written",
"thumbnail_svg": "projects/sample-video-abc123/visuals/thumbnails/clip-001.svg",
"scene_card_svg": "projects/sample-video-abc123/visuals/scene-cards/clip-001.svg",
"frame_count": 3,
"uses_mascot": false,
"uses_logo": false
}
]
}post_queue.json:
{
"schema_version": "video_review_os.post_queue.v1",
"platform": "generic",
"auto_publish_enabled": false,
"requires_explicit_adapter": true,
"items": [
{
"clip_id": "clip-001",
"status": "ready_for_manual_post",
"media_path": "projects/sample-video-abc123/renders/clip-001-keep.mp4",
"title": "Draft title",
"caption": "Draft caption"
}
]
}python -m pip install -e ".[dev]"
python -m pytestManual smoke test:
- Put one video in
raw/. - Run
video-review-os --config config.toml run-once. - Check the project folder for
source.json,transcript.json,clips.json,captions.json,scenes.json,visuals.json,drafts/copy.json,storyboard.json, andassemblies.json. - Run
video-review-os --config config.toml render-assemblies <project> --dry-run, then without--dry-runto producerenders/assemblies/<assembly_id>.mp4. - Run
video-review-os --config config.toml dashboard. - Confirm
dashboard/index.htmlshows the auto-generated assemblies (with their EDLs and repair ops), clip candidates, frame stills, visual drafts, decisions, approval state, and a collapsed "Rejected" section.
- Raw videos stay local unless you configure a hosted adapter.
- Do not put API keys in
config.toml; use environment variables. - Treat generated transcripts as sensitive.
- Treat project folders as sensitive because JSON sidecars can contain local file paths and transcript text.
- Mascot and logo assets are local paths in config.
- This repository includes a KaiCalls mascot example so the frame-overlay workflow is visible. Replace it for your own brand.
- Review generated copy and visual drafts before using them externally.
- Source videos are never deleted by this tool.
- No auto-publishing by default.
- No hidden uploads.
- No automatic social account connection.
- No automatic movement into platform handoff folders.
- No private deployment assumptions.
- No claim that a
keepclip or rendered assembly is approved for publication. - No assembling, rendering, or queuing of
rejectclips. - No auto-applied edits that aren't recorded as inspectable repair ops in the EDL.
- No attempt to replace human editorial review.
- No destructive source video cleanup.