A continued fork of OpenMontage — agentic video production with a cross-project asset library at its core.
What This Fork Adds · Quick Start · Pipelines · How It Works · Providers · Agent Guide
Fork notice (AGPL § 5). OpenBoomhauer is a community-maintained fork of calesthio/OpenMontage, continued after upstream development slowed accepting contributions. Original work © the OpenMontage contributors, licensed under AGPL-3.0. This fork preserves the same license and adds the features listed below. Upstream's logo and demo videos remain in their original locations; this fork has not commissioned its own.
Turn your AI coding assistant into a full video production studio. Describe what you want in plain language — your agent handles research, scripting, asset generation, editing, and final composition.
Important distinction: OpenBoomhauer can make image-based videos, but it can also make a real video video for free/open-source workflows: the agent builds a corpus from free stock footage and open archives, retrieves actual motion clips, edits them into a timeline, and renders a finished piece. That is not the usual "animate a handful of stills and call it video" trick.
The video samples below were produced and uploaded by upstream calesthio/OpenMontage. They demonstrate the base system's capabilities — which this fork inherits without modification. For features distinctive to OpenBoomhauer (asset library, local ACE-Step music, F5-TTS voice, etc.), see What This Fork Adds below.
signal-from-tomorrow_final_with_music_upload_v2.mp4
"SIGNAL FROM TOMORROW" — a cinematic sci-fi trailer fully produced through OpenMontage: concept, script, scene plan, Veo-generated motion clips, soundtrack, and Remotion composition.
the_last_banana_v3_github.mp4
"THE LAST BANANA" — a 60-second Pixar-style animated short about a lonely banana who finds friendship with a kiwi. 6 Kling v3-generated motion clips (via fal.ai), Google Chirp3-HD narration, royalty-free piano music, TikTok-style word-level captions, and Remotion composition. Total cost: $1.33.
void-linkedin.mp4
"VOID — Neural Interface" — a product ad produced with just one API key (OpenAI). 4 AI-generated images (gpt-image-1), TTS narration, auto-sourced royalty-free music, word-level subtitles via WhisperX, and Remotion data visualizations. Total cost: $0.69. Zero manual asset work.
candyland.mp4
"Afternoon in Candyland" — a Ghibli-style anime animation. A little girl's whimsical afternoon adventure through candy gates, gumdrop rivers, and lollipop gardens. 12 FLUX-generated images with multi-image crossfade, cinematic camera motion (zoom, pan, Ken Burns), sparkle/petal/firefly particle overlays, and ambient music with auto-detected energy offset. Total cost: $0.15. No video generation, no manual editing.
mori-no-seishin.mp4
"Mori no Seishin" — a Ghibli-style anime animation of a forest spirit's journey through ancient woods. 12 FLUX-generated images with parallax crossfade, drift and pan camera motion, firefly and petal particles, cinematic vignette lighting, and ambient forest soundtrack. Total cost: $0.15. Still images brought to life through Remotion's animation engine.
deep-ocean.mp4
"Into the Abyss" — a deep ocean exploration rendered in anime style. Bioluminescent gardens, coral cathedrals, and creatures of light — 12 FLUX-generated images with sparkle and mist particle overlays, light-ray effects, smooth camera motion, and ambient oceanic soundtrack. Total cost: $0.15. Zero video generation APIs needed.
Subscribe to @OpenMontage on YouTube to see new videos as they ship — every video includes the full prompt, pipeline, tools used, and cost so you can reproduce it yourself.
OpenBoomhauer extends upstream OpenMontage with first-class cross-project asset reuse and a handful of self-hostable provider integrations. All work below is AGPL-3.0 like the upstream and is authored on top of upstream main.
How entries are labeled. Each subsection header carries one of four tags so you can scan by intent:
[Feature]— a new capability the upstream doesn't have (a new tool, integration, or whole workflow)[Enhancement]— a meaningful upgrade to something that already existed (extending an existing tool, adding a missing parameter, polishing UX)[Fix]— corrects a bug or restores broken behavior in something that previously worked or claimed to work[Docs]— documentation deliverable (no code change)
Shared catalog and SHA-addressed canonical store for every image, stock photo, AI generation, and music track ever produced in the repo — with usage tracking, region/tag classification, anti-overuse rotation, and a self-contained dashboard.
scripts/asset_library_index.py— content-hash every project asset into a canonical store and write a deduplicated catalogscripts/asset_library_search.py— query the catalog; default sort surfaces under-used hits so the same handful don't get re-stagedscripts/asset_library_stage.py— copy a canonical asset into a project's per-project asset directory (assets/images/<kind>/)scripts/asset_library_classify.py— backfillcontent_type(photo / brochure / event / art / unknown) on existing catalog rows via the heuristic classifier inasset_library_index.py; idempotent,--forcere-runs after heuristic tweaks. Search and stage CLIs gain a--content-typefilter with a protective default (--kind=archivaldefaults tophoto) so brochures and event-shots stay out of hero-shot picks. Conservative-by-default: ambiguous rows route tounknownrather than guessing.scripts/asset_library_vlm_classify.py— VLM-assisted promotion ofcontent_type=unknownrows using Claude vision (Haiku 4.5 default). Two-pass design: dry-run writes proposals toasset_library/vlm_proposals.jsonlfor review, then--commitapplies them via atomic rewrite. Includes drift detection (proposals whosecurrent_content_typeno longer matches the catalog are skipped), per-row cost telemetry,--max-costbudget abort, and long-edge image downscaling (1024px JPEG) before upload to control token spend.scripts/audit_candidates.py— grid-preview sheet for batch visual audit of staged candidates BEFORE building the edit PLAN. Cross-references each candidate againstcatalog.jsonlby content hash: catalogcontent_typeof brochure/event/art OR filename matches (LOC_,_Celebrates_,_Public_Lands_Day, …) get hard-flagged (red border + reason badge); wide-aspect (>1.8:1) candidates with no positive classification get soft-flagged (amber border). Cell labels followA1/B3convention so the agent can refer to flagged cells unambiguously in reports.--quiet-cleanemits asuspicious_only.pngfor focused review.scripts/asset_library_dashboard.html— static dashboard (usage histogram, by-kind/source breakdown, over-used table). Self-contained: double-click and an embedded sample loads automaticallyscripts/dashboard.py—python scripts/dashboard.pyto serve over HTTP and open the browser; also wired as a/dashboardClaude Code slash commandexamples/generate_demo_catalog.py+examples/embed_demo_into_dashboard.py— deterministic synthetic data so the dashboard is demo-able without indexing real projectsdocs/protocol.md— three-protocol playbook (pre-fetch search → post-render refresh → canonical store)
The asset library dashboard rendering its embedded demo catalog. Charts cover usage distribution, asset kind, and provenance source; the bottom tables surface over-used hits and under-used candidates for rotation.
Try the dashboard in 30 seconds — no API keys, no Node, no Python deps beyond the standard library:
git clone https://github.com/billdauterive/OpenBoomhauer.git
cd OpenBoomhauer
python scripts/dashboard.pyThe launcher binds 127.0.0.1, opens your browser at the dashboard, and serves the embedded 37-record demo catalog out of the box. Stop with Ctrl+C. To replace the demo with your own data, run python scripts/asset_library_index.py first to build a catalog from your projects/ directory.
[Fix] Scaffold normalizes all-caps hook overlay so F5-TTS speaks it instead of spelling it (fix/scaffold-hook-overlay-normalization)
F5-TTS reads consecutive uppercase letters as an initialism. Upstream Airtable Transcript fields put the visual hook-overlay text as the first line of beat 1 in ALL CAPS with a trailing emoji (e.g., THIS PLACE IS HIDING SOMETHING WILD 🌿); the scaffolder copied that line verbatim into script.json beats[0].narration, so F5 produced "T-H-I-S-P-L-A-C-E..."-style letter-spelled gibberish for the opening ~5 seconds of every video. Caught after a three-video batch shipped with the bug; rendering + re-shipping was a manual ~30 minutes per video.
The scaffolder's bracket and pipe parsers now normalize the first line of each beat via normalize_hook_overlay_line when it's predominantly uppercase (≥8 letter chars, ≥60% upper-case ratio). The normalizer lowercases the line, strips pictographic Unicode (BMP So category + SMP plane), capitalizes the first letter, and appends a sentence terminator if missing. Body lines are deliberately untouched so short legitimate acronyms (USS, NASA, ATV) in the middle of narration are preserved.
scripts/scaffold_project.py::_strip_pictographic+normalize_hook_overlay_line— new helpers + call sites in_parse_pipe_format(whole-line normalization, pipe beats are one line) and_parse_bracket_format(first-line-only normalization to protect body acronyms)tests/scripts/test_scaffold_normalize_hook_overlay.py— 10 offline cases covering the bug reproduction (bracket parser normalizes beat-1 first line, emoji stripped, sentence terminator appended), edge cases (short acronyms preserved, already-sentence-case left alone, mixed case with proper nouns left alone), parser variants (inline-on-header bracket format, pipe format with all-caps narration), and the body-line acronym preservation guarantee
[Docs] GDrive OAuth setup — updated console nav + refresh_gdrive_token.py helper (docs/gdrive-oauth-publish-helper)
Google rebranded the "OAuth consent screen" page to "Google Auth Platform" in 2025, so the old scripts/ship_to_gdrive.py setup steps pointed at stale URLs and a tab name (Test users / SAVE AND CONTINUE) that no longer matches the UI. Worse, the PUBLISH APP step was buried as a parenthetical "heads-up" inside the consent-screen step, so first-time setters-up landed on a Testing-mode consent screen and re-hit a browser prompt every 7 days when the refresh token expired.
The docstring now points at the new sidebar URLs (/auth/branding, /auth/audience, /apis/credentials) and promotes PUBLISH APP to its own numbered step 4 with the rationale spelled out (Testing-mode 7-day expiry vs. permanent tokens post-publish) and the one-time "Google hasn't verified this app → Advanced → Continue" walkthrough that every Published-but-unverified app shows on first consent.
Same change adds scripts/refresh_gdrive_token.py — a small CLI that forces a fresh OAuth flow without needing a fake video upload to trigger one. Use it after publishing the consent screen so the now-permanent refresh token is minted immediately instead of waiting 7 days for the Testing-mode token to expire.
scripts/ship_to_gdrive.pydocstring — updated URLs for the rebranded Google Auth Platform UI; PUBLISH APP promoted from heads-up parenthetical to numbered step 4; explicit "Advanced → Continue past the unverified-app warning" instructionscripts/refresh_gdrive_token.py— new CLI wrapper aroundGoogleDrivePublisher._build_service+service.about()to mint a fresh refresh token + confirm signed-in email; SystemExit / network failures surface to stderr with exit code 1tests/scripts/test_refresh_gdrive_token.py— 3 offline cases (success path prints email + exits 0, missing-env-var surfaces the publisher's clear SystemExit text + exits 1, network failure during the verifyingabout()call exits 1)
Refines the existing _downscale_image_in_place() helper on scripts/asset_library_stage.py with: explicit (orig_w, orig_h, new_w, new_h) return so callers can log the post-downscale dimensions; JPEG quality bumped 88 → 92 (matches the standalone downscale_assets_for_remotion.py workaround pattern this helper obsoletes); an explicit idempotent fast-path so under-threshold files skip the re-encode entirely; first-class unit tests so the behavior can't regress silently.
scripts/asset_library_stage.py::_downscale_image_in_place— wider return tuple + idempotent fast-path + q92 + docstring rewrite citing the wikimedia clamp paritytests/scripts/test_asset_library_stage_downscale.py— 4 offline cases (8192px JPEG → ≤4096, idempotent no-op on 3000px JPEG, opt-out via--max-dim 0keeps 8192, PNG stays PNG format)
Ship multiple videos back-to-back with two-slot overlap: iter N+1's TTS / asset fetch / music generation runs concurrently with iter N's Remotion render. For queues of 3+ videos this saves ~30-60s per iter (mostly by keeping the F5-TTS daemon and ACE-Step server warm across iterations instead of paying the cold-init each time).
scripts/queue_runner.py— orchestrator with--dry-runmode (gantt-style schedule from per-project sim hints), per-iter timing logs, daemon lifecycle management, signal-safe PID cleanup, and a defensiveshutdown_daemons()step that skips gracefully when an optional shutdown helper isn't on the branchscripts/queue_examples/two_demo_projects.yaml+three_demo_projects.yaml— synthetic queues with sim hints so the dry-run can produce a real schedule without scaffolding actual projects
Architecture: one render_slot semaphore (cap=1, since concurrent Remotion renders saturate a single GPU) + one prerender_slot (cap=1, since F5-TTS and ACE-Step each hold a GPU resource). Combined, this gives at most ONE pre-render bundle running in parallel with ONE render — exactly the two-slot ceiling the design calls for.
[Enhancement] Scaffold build_edit.py motion-alternation error + optional auto-fix (feat/build-edit-motion-alternation)
The scaffolded build_edit.py's Adjacent identical motion validator previously raised with a minimal cut {i}: {motion_a} -> {motion_b} message — a flat cut index and the two colliding motion names, nothing else. The PLAN-writer (subagent or main agent) burned 2-3 retry iterations per failure mapping the cut index back to a beat ID, then guessing whether the collision was a beat-boundary case (b_N last cut shares motion with b_{N+1} first cut) or an internal-to-beat case, then enumerating which motion swaps would still be legal. Per-failure cost: ~30s of agent reasoning + one rebuild iteration.
This rewrites the error to embed (a) the beat ID containing the collision, (b) the boundary-vs-internal status, (c) the list of motions that would still be legal for that slot given the surrounding cuts, and (d) a pointer to memory/feedback_motion_alternation_boundary_collisions.md for context. PLAN-writer no longer has to reverse-engineer the collision shape. Same commit adds an opt-in auto-fix gate: OPENMONTAGE_AUTOFIX_MOTION_ALTERNATION=1 triggers a deterministic in-PLAN motion swap from the still-legal-motions list with a stderr WARN: line naming the cut + the swap. Off by default to preserve agent intent — the agent decides whether to opt in for the next iteration.
scripts/scaffold_project.py::_BUILD_EDIT_TEMPLATE— rewritten validator: buildscuts_by_beatmap up front; on collision walks the beat-boundary line vs internal-to-beat distinction; computes still-legal motions =motion_pool - {prev, next}; the error string embeds beat-id + boundary status + still-legal list + memory pointer. Optional auto-fix branch readsOPENMONTAGE_AUTOFIX_MOTION_ALTERNATIONand swaps deterministically (alphabetically-first still-legal motion wins)tests/scripts/test_scaffold_project_motion_alternation.py— 5 offline cases: rendered template parses + carries the new error-message markers; default-raise behavior on a synthetic beat-boundary collision PLAN; internal-collision case labels the in-beat position correctly; autofix env var =1swaps the motion deterministically and emits the WARN; autofix env var unset preserves the old raise behavior
[Fix] Scaffold render.ps1 template — drop 2>&1, use all-streams capture (fix/scaffold-render-no-stderr-redirect)
PowerShell's bash-style stderr-to-stdout redirect (2>&1) on native executables (npx, node, ffmpeg) wraps every stderr line as an ErrorRecord of type NativeCommandError and flips $? to false even when the binary exits cleanly. Post-patch session mining caught 13 sessions / 150 false-failure occurrences in the May 20 → Jun 14 window — each one cost ~30s of investigation before the agent realized the render had actually succeeded. The scaffolded render.ps1 was a frequent source: every project created by scripts/scaffold_project.py inherits this template, and every render-time invocation of npx remotion render tripped the NativeCommandError wrapper if the script redirected with 2>&1.
The scaffolded template now (a) builds an absolute log path via Join-Path $repoRoot BEFORE the Push-Location remotion-composer block (sister-rule guard for relative-path-with-Push-Location), (b) redirects npx remotion render output via PowerShell's all-streams operator (*>) to that absolute log path, capturing both stdout and stderr in one file without ErrorRecord wrapping, and (c) updates the failure-throw to point operators at the log file. An inline comment explains the rationale so future template edits don't regress.
scripts/scaffold_project.py::_RENDER_PS1_TEMPLATE— absolute log path viaJoin-Path $repoRootbeforePush-Location;*>all-streams redirect replacing the prior pattern; failure-throw references the log path; inline rationale comment guards against re-adding2>&1tests/scripts/test_scaffold_project_render_template.py— 3 offline cases: no2>&1anywhere in the rendered template, the rendered template uses the*>all-streams redirect, and the log path resolves to absolute viaJoin-Path $repoRoot. Exerciseemit_helpersdirectly — no Airtable round trip, no real subprocess
[Fix] Queue runner treats Whisper rc!=0 as advisory when whisper_words.json exists (fix/queue-runner-whisper-cudnn)
The Whisper transcribe step crashes with STATUS_STACK_BUFFER_OVERRUN (0xC0000409 = 3221226505) in the cuDNN cleanup path AFTER whisper_words.json is fully written. Observed at ~50-60% per-iter rate in multi-video sprints. Each crash aborted the entire queue runner, requiring manual build_edit + render recovery (3-5 min per affected iter) even though the actual transcription output was already on disk and complete. Complement to the proactive warmup_whisper.py pre-warm fix that already shipped — pre-warm reduces the cold-init crash rate, but the cuDNN cleanup-path crash still occurs intermittently on warm iters.
The transcribe phase now post-checks for whisper_words.json after the subprocess returns. If the file exists and is non-empty, the runner soft-warns (transcribe rc={rc} but whisper_words.json was written; treating as advisory) and continues. Hard-fail is reserved for the genuine transcription-failure case where rc != 0 AND the file is missing or empty. Equivalent to the fetch_archival partial-success pattern in the sibling assets-phase fix — the queue runner now treats "exit code is bad but output is good" as advisory across both phases.
scripts/queue_runner.py::transcribe_phase— post-rc check forwhisper_words.json; soft-warn branch when file exists; hard-fail only when rc != 0 AND file missing/emptytests/scripts/test_queue_runner_patches.py— 4 new cases (10 total) covering: nonzero rc + file written → advisory soft-warn + continue; nonzero rc + file missing → hard-fail with clear message; zero rc → success; central CLI helper missing → hard-fail before subprocess. All offline,tmp_path+AsyncMockfixtures
[Feature] Scaffold wires Pexels region-gate into generated fetch_pexels.py + emits region_desc as first-class field (feat/pexels-region-gate-scaffold)
The T-008 Pexels region-drift vision gate (stage_pexels_with_region_check) shipped on main via feat/pexels-region-gate as a pair of helpers in tools/asset_staging.py — but the scaffolded per-project fetch_pexels.py never actually called them. Every new project's stock fetch ran without the gate; region-mismatched candidates (Mediterranean villas for Southwestern desert queries, Alpine chalets for Gulf Coast wetlands) slipped through unchecked. Operators caught them at the manual visual-audit step or post-render, both of which cost more than catching them at the save step would have.
This wires the gate into the scaffolded _PEXELS_TEMPLATE and adds region_desc as a first-class top-level field in script.json. The wiring is three pieces. First, the template imports both helpers (resolve_region_desc + stage_pexels_with_region_check) and reads an opt-out env var (OPENMONTAGE_PEXELS_REGION_CHECK=0 disables the gate for batch / offline iterations where vision API quota is precious). Second, REGION_DESC resolves via the shared resolve_region_desc(script_path, project_slug) helper using the canonical three-step fallback (script.json::region_desc → script.json::title → project slug) — the same code path the unit-test suite exercises. Third, the save loop calls stage_pexels_with_region_check(file, REGION_DESC) on every saved file: model judges YES → keep; NO → delete + log REJECTED; API error → fail open (keep, log) so a flaky vision endpoint never strands a project mid-fetch.
Same change promotes region_desc to a first-class script.json field via emit_script_json — defaults to the hook text (scaffolded hooks reliably carry the place anchor for regional short-form content) with an inline comment telling the agent to refine to a 2-5 word geographic anchor when warranted ("Gulf Coast tidal marsh", "Pacific Northwest old-growth forest" — a tighter consistency target for the vision model than a hook slogan).
scripts/scaffold_project.py::_PEXELS_TEMPLATE— addsimport os; importsresolve_region_desc+stage_pexels_with_region_check; setsREGION_DESCvia the shared helper; readsOPENMONTAGE_PEXELS_REGION_CHECKopt-out; calls the per-file gate inside the save loop withREJECTEDand fail-openERRloggingscripts/scaffold_project.py::emit_script_json— addsregion_descas a top-levelscript.jsonfield, defaulting to the hook text, with an agent-facing comment about refining to a 2-5 word geographic anchortests/scripts/test_scaffold_pexels_region_gate_wiring.py— 6 offline cases: both helpers imported into the rendered template,REGION_DESCset via the shared resolver (and the older inline private function is retired),OPENMONTAGE_PEXELS_REGION_CHECKopt-out env var read, gate called inside the save loop withREJECTEDandERRlog lines, rendered template still parses as valid Python, andemit_script_jsonwritesregion_descas a top-level string keyed off the hook text
[Fix] Scaffolded fetch_pexels.py refuses empty / placeholder QUERIES (fix/scaffold-pexels-empty-query-guard)
The scaffolder seeds projects//artifacts/fetch_pexels.py's QUERIES list from each beat's visual hint. When the seed collapses to "" (Airtable Visuals column empty, brackets-only, etc.) or stays at a literal TODO placeholder, the generated helper hits Pexels with query= and either 400s on every entry or burns the session-wide rate limit before erroring further down the pipeline. One observed 9-project batch shipped 7 with zero Pexels stock for exactly this reason and the whole batch came back rate-limited — diagnosing seven projects' empty stock dirs after the fact is much more expensive than catching the bug once at module load.
This adds an assert_queries_nonempty(QUERIES, __file__) guard at the top of the scaffolded fetch_pexels.py — runs IMMEDIATELY after the QUERIES list is defined, BEFORE registry.discover(), the Pexels tool lookup, or the output-dir creation, so a bad QUERIES list short-circuits with zero side effects. The helper itself lives in tools/asset_staging.py so other scaffolded scripts can reuse it. Error message names the source file path AND every offending prefix so the operator's fix is one grep away.
tools/asset_staging.py::assert_queries_nonempty(queries, source_file)— pure-stdlib guard; rejects empty / whitespace-only / TODO-placeholder queries; raisesSystemExit(1)naming the file path + every bad entry's(index, prefix, reason)tripletools/asset_staging.py::_PEXELS_QUERY_TODO_TOKENS— case-insensitive denylist ("TODO","TODO_query","TODO_visual","<query>"); easy to extendscripts/scaffold_project.py::_PEXELS_TEMPLATE— imports the guard; calls it immediately afterQUERIESis defined; reordersregistry.discover()+ Pexels tool lookup +OUT.mkdir()to AFTER the guard so a bad QUERIES list short-circuits before any registry or filesystem calltests/scripts/test_fetch_pexels_empty_query_guard.py— 7 offline cases: happy path, empty string, whitespace-only, TODO token (case-insensitive), multi-bad rollup (every offender named in the error), and two template-integrity regex checks that the rendered_PEXELS_TEMPLATEimports the guard AND calls it before any pexels registry call
[Enhancement] Queue runner assets-phase pre-staged skip + partial-success soft-warn (feat/queue-runner-assets-phase-resilience)
The multi-iteration queue runner's assets phase had two recurring operator-facing failure modes that surfaced across enough sessions to graduate from "annoying" to "blocking 3-batch sprints". First, re-running fetch_archival.py on a project that was already hand-staged from the asset library paid the full Wikimedia 1.2s pace-budget for zero new files — every hand-staged project paid a 30-60s tax on every queue iter. Second, a single Wikimedia tail-end 429 on a long fetch would hard-fail the queue (landed == 0 check fires on rc != 0) even when 20+ files had already successfully landed, forcing the operator to either re-run the whole assets phase or hand-edit the queue to skip past it.
This adds two complementary guards to assets_phase. Pre-staged skip: when a project has >= PRESTAGED_THRESHOLD (=8) archival files AND >=8 stock files already on disk, the assets phase skips both fetch_archival.py and fetch_pexels.py with a notes entry and continues — turns a re-run into a no-op. Partial-success soft-warn: fetch_archival.py returning rc != 0 now checks whether any files actually landed in projects/<name>/assets/images/archival/. Zero files → hard-fail (existing behavior, message gains "AND zero files landed"). One or more files → log "fetch_archival rc={rc} but {N} archival files landed (likely a tail-end 429); continuing" and proceed. Both fetchers also now run with cwd=REPO so the scaffolded helpers' repo-relative paths resolve correctly under the queue runner.
scripts/queue_runner.py::PRESTAGED_THRESHOLD = 8— module-level constant; tune-in-one-placescripts/queue_runner.py::assets_phase— pre-staged skip check (counts files in both kind dirs); soft-warn branch on partial-success rc;cwd=REPOthreaded into both fetcherstests/scripts/test_queue_runner_patches.py— 6 offline cases viaasyncio.run+AsyncMockover_run_subprocess_capture_pid: pre-staged skip avoids both fetchers, under-threshold runs fetch, soft-warn with files-landed, hard-fail with zero files, tts phase chains narration → concat in order withcwd=REPO(locks in pre-existing behavior the previous commit also touched), tts hard-fails when concat helper missing. Puretmp_path+ monkeypatch — no real subprocess, no network
First-class integration with the ACE-Step 1.5 local music model — MIT-licensed output (fully owned), vocals + lyrics support, runs on a 24 GB consumer GPU.
tools/audio/acestep_music.py— calls a local ACE-Step REST server with warmup, stale-PID cleanup, and one-shot shutdownscripts/acestep_shutdown.py— idempotent VRAM release helper
Offline alternative to cloud TTS — clones a target voice from a short reference clip, runs locally on a single GPU.
tools/audio/f5tts.py— F5-TTS_v1_Base integration with configurable speed, seed, and output format- Warm-worker daemon (
experimental/f5tts-warm-worker-daemon): keeps the F5-TTS model loaded between calls so back-to-back beats don't pay ~30-60s of cold-init per invocation. First request from a cold worker pays the model-load cost; every subsequent request reuses the same model in-memory and finishes in ~2-5s per beat. Uses stdlibhttp.server(single-client, single-endpoint localhost — no FastAPI / uvicorn overhead). Per-callshutdown_after_generation=Truereleases VRAM voluntarily; an env var (OPENMONTAGE_F5TTS_SHUTDOWN) lets the queue runner signal the last-iter shutdown. tools/audio/_f5tts_worker.py— the worker process. Holds the F5TTS instance (~6 GB VRAM) for its lifetime. HTTP wire protocol:POST /inferreturns the generated wav;POST /shutdownis the graceful exit route (drops VRAM before exiting).scripts/f5tts_shutdown.py— idempotent VRAM release helper. Reads the PID captured at worker launch (~/.cache/openmontage/f5tts_server.pid), tries the worker's HTTP/shutdownroute first (cleanest), then falls back to signal-based stop. Only the exact PID captured at launch is touched — never broadly killspython.exe.
Replaces the per-project Wikimedia fetch script pattern with a single YAML-driven CLI that's rate-limit compliant out of the box.
scripts/fetch_archival.py— project-agnostic config-driven fetcher (additive by default,--cleanfor fresh runs)scripts/fetch_archival.example.yaml— schema reference withqueries,keep_keywords,mime_allow,min_width
Cross-session corpus pattern mining across ~/.claude/projects/<repo-slug>/*.jsonl. Surfaces durable error / validator-rejection patterns that single-session reflection can't see — the first run across one operator's 131-session corpus surfaced build_edit DUPLICATE IMAGE rejection in 78 of 131 sessions and Adjacent identical motion in 75 of 131 sessions. Read-only, no LLM calls, no network.
scripts/mine_session_transcripts.py— YAML-driven CLI with auto-detect transcript directory,--since YYYY-MM-DDmtime filter,--repeat-threshold NREPEAT marker,--anonymizesnippet scrubbingscripts/session_mining_patterns.yaml— default 14-pattern rule set. Each pattern supportsruntime_only+skip_if_line_containsfor noise filtering (memory-file echoes, miner self-references)tests/scripts/test_mine_session_transcripts.py— 10 pytest cases (offline synthetic JSONL fixtures, <0.5s)docs/session-transcript-mining.md— operator README with scan scope, schema, cadence guidance, and the--since 2026-06-12recommendation forp4b_remotion_runtime_crashafter the Remotion 4096 clamp ship date
Bypasses Pixabay's Cloudflare bot challenge so the music-fetcher fallback chain keeps working when upstream returns 403s.
Compliant User-Agent with contact URL, ≥1.2s request pacing, Retry-After honoring, and visible status codes. Fixes the silent 429s upstream's fetcher saw after Wikimedia's April-2025 anonymous-client enforcement.
Wikimedia exposes two independent throttles — commons.wikimedia.org/w/api.php (search/metadata) and upload.wikimedia.org/wikipedia/commons/... (file bytes). A run can pass the API rate gate cleanly and still hit 429s during the download phase. The original single-pool _RateLimiter paused EVERY future request once either endpoint 429'd, which over-throttled the unaffected pool.
This rework routes the two URL classes through SEPARATE _RetryPool holders so a 429 on one endpoint never blocks the other. Both pools share the WIKIMEDIA_MAX_RETRIES env-var cap (default 5) but their next_legal_request_time clocks are independent. WIKIMEDIA_MIN_INTERVAL_SECONDS retained for backward compatibility.
tools/video/stock_sources/wikimedia.py— replaces_RateLimiterwith two_RetryPoolinstances (one per host);_request_with_poolroutes by URL host;_max_retries()reads the env-var contract.- Tests at
tests/tools/test_wikimedia_two_pool_retry.py— 6 cases: happy path, single-pool 429 isolation (both directions), independentRetry-Afterclocks, env-var cap respected. All offline.
[Fix] fetch_archival hard-fails on YAML/JSON parse errors with file + location (fix/fetch-archival-yaml-parse-hard-fail)
When the per-project wikimedia_queries.yaml had a syntax defect (e.g. - "alpha" "beta" — two quoted scalars on one list line, no separator), PyYAML's ParserError fell into a generic except Exception returning rc=2 with the message failed to load config <path>: <repr>. In queue mode this is easy to miss: the run continues past fetch_archival, lands 0 archival images, and the failure only surfaces ~30+ minutes later when the PLAN-fill subagent notices the archival directory is empty.
This wraps yaml.safe_load (and json.loads for .json configs) with a ConfigParseError that carries the file path, 1-indexed line/column extracted from PyYAML's problem_mark, and the remediation hint "Fix YAML before re-running." main() catches the new exception and exits 1 (distinct from the rc=2 path used for missing files / unsupported extensions). rc=1 lets queue_runner's landed == 0 hard-fail check fire cleanly on a parse error while the stderr message gives the operator the specific defect immediately.
scripts/fetch_archival.py::ConfigParseError— new exception carryingpath,underlying,line,column; string form includes the remediation hintscripts/fetch_archival.py::_load_yaml_or_json— catchesyaml.YAMLError/json.JSONDecodeErrorand re-raises asConfigParseErrorwith location infoscripts/fetch_archival.py::load_config— new public helper (raisesConfigParseError/FileNotFoundError/ValueError);_load_configkept as back-compat aliasscripts/fetch_archival.py::main— distinct exit codes: 1 on parse error, 2 on missing config / non-mapping root / unsupported extensiontests/scripts/test_fetch_archival_yaml_parse_fail.py— 8 offline cases: happy-path YAML, happy-path JSON, the defect raises with path + line + hint, JSON parse errors also surface asConfigParseError, non-mapping root is a distinctValueError, missing config isFileNotFoundError, subprocess hits rc=1 with path + hint in stderr, subprocess on missing config hits rc=2
[Feature] fetch_archival per-query limit override + lower default (feat/fetch-archival-per-query-limit-override)
A single project config often mixes query types with very different Commons coverage. A narrow technical query (a binomial-name biology term, e.g.) returns ≤ 8 hits and the extra slots from the old per_query_limit: 15 default ran the 1.2s-pacing pool dry without any payback. A broad regional query with thin Commons coverage may need 12-15 to land 2-3 usable candidates after the keep_keywords filter. The fixed-per-run default forced operators to either tune the run for the broadest query (over-fetch every narrow one) or for the narrowest (under-fetch every broad one).
This adds a {q, limit} dict form to wikimedia_queries.yaml entries that overrides per_query_limit for that one query — bare-string entries still use the run default. The default itself drops from 15 → 8 to make the common case (narrow technical queries) cheap, with broad-coverage queries opting into a higher per-entry limit only when they actually need it. The per-entry limit is echoed in the per-query log line so over/under-fetch is visible in stderr per run. _normalize_query validates dict-form entries: missing q (or its query alias), non-string q, non-int limit, non-positive limit, or any non-string non-dict entry all raise ValueError with the query name in the message.
scripts/fetch_archival.py::_normalize_query(entry, default_limit)— pure-function helper returning(query_text, per_query_limit). Both dict-key aliases (qandquery) accepted;limitfalls back todefault_limitwhen absentscripts/fetch_archival.py::DEFAULT_PER_QUERY_LIMIT— dropped from15to8scripts/fetch_archival.py::fetch— per-querySearchFiltersnow built inside the loop (was hoisted) soper_page=limitcan vary; per-query log line gains(limit N)scripts/fetch_archival.pymodule docstring +scripts/fetch_archival.example.yaml— both advertise the dict form and the new defaulttests/scripts/test_fetch_archival_per_query_limit.py— 18 offline cases: default constant is 8, bare-string with default and threaded-default, dict-form override +queryalias + missing-limit fallback +limit: 1edge case, every validation failure (missingq, non-stringq, emptyq, non-int limit, zero limit, negative limit), and a parametrized sweep over unsupported entry types (int, float, None, list, tuple)
[Feature] fetch_archival baked-in Western-landmark default skip-patterns (feat/fetch-archival-western-landmark-skip-defaults)
Operators targeting one US region recurrently pull famous-Western-US-landmark hits from Wikimedia Commons when fetching for unrelated subjects — same-name state parks across multiple states (Chimney Rock NE / CO vs same-named subjects elsewhere), 19th-century Albert Bierstadt landscape paintings showing up on generic "landscape" queries, and overlapping place names like Mount Rushmore / Crater Lake / Yosemite / Yellowstone polluting any generic mountain / lake / valley / geyser search. The shared wikimedia_known_collisions.yaml covered per-project blocklists, but Wikimedia's relevance ranker made these famous-landmark collisions the single biggest collision class across multiple unrelated projects — moving the gate ahead of per-project YAML editing closes the recurring failure class once.
This bakes a DEFAULT_SKIP_PATTERNS tuple of the most-frequent Western-US-landmark name collisions into scripts/fetch_archival.py and merges it ahead of both the shared collisions YAML and any per-project skip_patterns: list. The merge is monotonic: per-project YAML may EXTEND the defaults but cannot disable any single entry. Operators whose subject IS a Western-US national park can clear the defaults locally for that one project. Same change also makes _matches_skip_filter separator-insensitive (underscores in titles normalize to spaces before substring comparison) so raw Wikimedia filenames like Chimney_Rock_Nebraska.jpg match a "chimney rock nebraska" pattern without requiring the operator to spell out every underscore variant.
scripts/fetch_archival.py::DEFAULT_SKIP_PATTERNS— 26-entry tuple covering Bierstadt + Sioux Village + Oregon Trail engravings, Chimney Rock NE/CO (multiple comma/state-suffix variants), Point Reyes, Mount Rushmore, Crater Lake (+ National Park), Yosemite + El Capitan + Half Dome, Grand Canyon, Yellowstone + Old Faithful, Devils Tower (apostrophe + non-apostrophe), Monument Valley, Zion / Bryce Canyon / Archesscripts/fetch_archival.py::_merge_skip_patterns(*sources)— generalized from(shared, per_project)to*sources; backward-compatible with the old two-arg call shape; monotonic union, case-insensitive de-dup, order preservedscripts/fetch_archival.py::_matches_skip_filter— separator-insensitive matching (underscores → spaces on both sides)scripts/fetch_archival.py::resolve_skip_patterns— threadsDEFAULT_SKIP_PATTERNSin as the first source, ahead of shared + per-projectscripts/fetch_archival.pymodule docstring## Behavior— collision-filtering entry rewritten for the three-source merge + separator normalizationtests/scripts/test_fetch_archival_western_landmark_defaults.py— 25 offline cases: target-region filename (a non-collision example) is NOT filtered, Bierstadt + Chimney Rock NE filenames ARE filtered, parametrized coverage of the remaining 18 famous-Western-landmark filenames, per-project cannot disable a default, per-project can add on top of defaults, underscore-normalization round-trip (pattern-with-spaces matches title-with-underscores and vice versa), and a defensive sanity check that the tuple is structurally clean (lowercase, no whitespace, non-empty)tests/scripts/test_fetch_archival_shared_collisions.py— three existingtest_resolve_*cases updated to expectDEFAULT_SKIP_PATTERNSprepended inresolve_skip_patternsoutput
[Enhancement] fetch_archival skip-collision telemetry split + docs (feat/fetch-archival-skip-collision-telemetry)
The per-project wikimedia_queries.yaml::skip_patterns: field already lands on Wikimedia search results (it was introduced alongside the shared-collisions loader in feat/wikimedia-shared-collisions). What it didn't have was per-run visibility: any candidate dropped by skip_patterns was lumped into skipped_filter alongside MIME-rejected and keep_keywords-rejected candidates, hiding the collision-class size from the operator. The field was also missing from the module docstring's ## Behavior and ## Config schema sections and from scripts/fetch_archival.example.yaml, making the field undiscoverable to a new operator reading the script before reading PR #30.
This splits skipped_collision into its own counter (and its own summary-dict key, and its own segment of the stderr summary line) so the operator can see how big the collision class is per run and decide whether the per-project skip_patterns: list needs to grow. The module docstring and example.yaml gain a documented skip_patterns: block — anonymized commented-out illustrative entries make the field-level intent (same-name-different-place place-name collisions Wikimedia returns) obvious without needing to read PR #30 first.
scripts/fetch_archival.py::fetch— newskipped_collisioncounter;_matches_skip_filterhits no longer incrementskipped_filter. Counter is exposed in both the dry-run summary dict and the post-download summary dict. Stderr summary line splits into three labeled segments (already in manifest/filtered (mime/keep)/dropped by skip_patterns)scripts/fetch_archival.pymodule docstring — new## Behaviorentry for collision filtering (semantics, ordering vskeep_keywords, union with shared list, telemetry surface); newskip_patterns: []row in## Config schemascripts/fetch_archival.example.yaml— newskip_patterns:block with three anonymized commented illustrative examplestests/scripts/test_fetch_archival_skip_collision_telemetry.py— 4 offline cases: skip_patterns hits count underskipped_collisionnotskipped_filter, MIME + keep_keywords rejects stay underskipped_filternotskipped_collision, summary always carries theskipped_collisionkey, and a title that matches BOTH a keep_keyword AND a skip_pattern lands inskipped_collision(skip applied AFTER keep)
Centralized cross-project skip-pattern loader for Wikimedia Commons title-collision filtering. Per-project wikimedia_queries.yaml::skip_patterns: lists already exist on a per-project basis (e.g. dropping "manchester, united kingdom" when fetching for Manchester, NH); this PR adds a repo-root wikimedia_known_collisions.yaml that the fetcher unions with each per-project list (shared first, case-insensitive de-dup). Per-project EXTENDS shared, never replaces it. Loader fails open — missing or malformed shared file degrades to per-project-only without raising. Ships as a starter (wikimedia_known_collisions.yaml.example) plus a docs page explaining when to add a pattern to the shared file vs. keep it per-project.
scripts/fetch_archival.py— addsSHARED_COLLISIONS_FILENAME,load_shared_skip_patterns(),_merge_skip_patterns(), publicresolve_skip_patterns(). Also introduces per-projectskip_patterns:config field with_matches_skip_filter().fetch()gains an optionalrepo_rootparameter so tests can stage a fake shared file intmp_path.wikimedia_known_collisions.yaml.example— empty starter at repo root with educational commentsdocs/wikimedia-known-collisions.md— when-to-add and pattern-format guide- Tests at
tests/scripts/test_fetch_archival_shared_collisions.py— 8 offline cases: missing file, well-formed file, malformed file (fails open), per-project extension, case-insensitive de-dup, missing-shared-file fallback, case-insensitive substring match, empty-patterns no-op
scripts/ship_to_gdrive.py previously created a fresh <name>-<epoch>/ folder on every invocation. Re-shipping a project with a corrected publish_metadata.json (e.g. after a caption or tag fix) left a duplicate Drive folder alongside the original — operator had to clean up manually. This enhancement adds two flags so caption-only corrections update the existing folder in place rather than spawning a new one.
--update-folder <folder-id>— skip_create_folder, reuse the supplied Drive folder ID directly. Markdown sidecar is overwritten viafiles().update(); MP4 replaced only when SHA-256 differs (idempotent viaappProperties.openmontage_sha256).--update-latest— convenience that lists<project>-*folders underGDRIVE_RENDERS_PARENT_ID, picks the most-recent bycreatedTime, behaves like--update-folder <that-id>. Fails cleanly when no match.- Default behavior unchanged: omit both flags and shipping still creates a new folder.
Touches:
scripts/ship_to_gdrive.py— mutually-exclusive flag group,_resolve_latest_folder_id()helper, docstring updatedtools/publishers/google_drive.py—existing_folder_idinput field, new helpers_upload_or_replace_videos,_upload_or_replace_bytes,_find_file_in_folder,_replace_file_path,_replace_file_bytes,_get_folder_metadata,find_latest_folder,_sha256_file. Fresh_upload_filenow stashes SHA-256 inappProperties.openmontage_sha256so future updates detect idempotency.- Tests at
tests/scripts/test_ship_to_gdrive_update_folder.py— 4 offline cases:--update-folderskips folder-create + replaces stale files; matching-SHA MP4 upload is skipped;--update-latestresolves most-recent match; default mode unchanged.
Facebook AudioCraft's MusicGen running on-device — no API key, no REST server boot, simpler/faster cold-start than ACE-Step for short instrumental beds. Sits naturally as a fallback in the music chain when ACE-Step isn't running or the brief doesn't need vocals.
tools/audio/musicgen_local.py— small/medium/large model auto-discovery
Google's Gemini image model wired into the registry. Uses the existing GOOGLE_API_KEY / GEMINI_API_KEY; useful when prompt-following accuracy matters more than the FLUX aesthetic.
tools/graphics/gemini_image.py
Local video gen via any ComfyUI install on the network. Ships four prebuilt workflows — LTX-2.3 distilled (default + 8 GB-tuned + b07-tuned variants) and an SD 1.5 smoke test — plus a UI-to-API JSON converter so you can drag-drop a workflow into ComfyUI's canvas, export, and the converter rewrites it into the API-submittable format.
tools/video/comfyui_video.py— REST-style clienttools/video/comfyui_workflows/— 4 prebuilt workflows + UI→API converter- Env:
COMFYUI_BASE_URL(defaults tohttp://127.0.0.1:8188)
Same CLI surface as the prior shipped version — --project, --ceiling, --dry-run, --script, --beat-offsets, distinct exit codes (1=beat_offsets missing with concat-voiceover hint, 2=script.json missing) — but the internals are reorganized for safety + testability: atomic write via .tmp + os.replace so concurrent readers never see a half-written script.json; pure recompute(script, windows, ceiling) function split from I/O for unit-testability; _build_window_map prefers explicit duration_seconds then falls back to end_seconds - start_seconds for legacy schemas.
scripts/recompute_asset_hints.py— pure-function split, atomic write, distinct exit code constants, schema-tolerant window maptests/scripts/test_recompute_asset_hints.py— 8 offline cases (happy path, idempotent, never-decrease,--dry-runno-write,--ceilingoverride, exit-code differentiation,--script/--beat-offsetspath overrides)
An upstream automation on an Airtable Scripts table silently capped some Description rows at exactly 500 characters, mid-word. scaffold_project.py then copied the truncated text verbatim into script.json and publish_metadata.json, which fed the published caption — caption ended mid-word and nobody caught it until the published render landed somewhere visible. This guard refuses to scaffold when the Description length is in [495, 505] AND the last non-whitespace character isn't sentence-ending punctuation (., !, ?, ", ', ”, ’) or the call-to-action emoji (👇).
scripts/scaffold_project.py::_looks_truncated_description(text)— the guard functionmain()calls it after fetching the Airtable record; on positive match, hard-exits with the offending length + tail substring + remediation hint ("fix the Airtable Description so it ends with sentence-ending punctuation, a hashtag, or 👇 before scaffolding")- Constants
_DESC_TRUNCATION_ZONE = (495, 505)and_DESC_OK_FINAL_CHARSare easy to tune if the upstream limit changes - Tests at
tests/scripts/test_scaffold_project_truncation_guard.py— 25 offline parametrized cases covering in-zone mid-word cuts, in-zone well-formed endings (period, question mark, exclamation, straight + curly quotes, 👇), out-of-zone strings (always treated as OK), boundary cases (495 + 505), trailing whitespace before punctuation, and empty string. One pinned-behavior test documents that a trailing hashtag-block (last character alphanumeric) is still flagged as truncated under the current_DESC_OK_FINAL_CHARSset.
The guard catches a class of silent-failure that's particularly hard to debug because the bad data looks valid on inspection — neither the Airtable UI nor the scaffold log flagged it. The 495-505 zone is narrow enough that legitimate Descriptions ending naturally near the 500-char mark aren't blocked.
Per-beat frame sampling for post-render visual audits. The standing pattern is mid-beat ffmpeg -ss <t> plus visual inspection — useful but periodically lands inside the 0.4s cross-fade overlap between cuts and surfaces a ghosted dissolve frame that mimics a render defect. This CLI reads edit_decisions.json (or beat_offsets.json + cut count), computes t = win_start + i*sub_dur + sub_dur/2 for each cut (strictly inside the cut, away from BOTH overlap edges), and emits one PNG per sampled cut. Output filename <beat-id>_t<int(t)>.png for cross-session comparison.
scripts/audit_render_frames.py— CLI:--project <name>,--out-dir <path>,--sample-per-beat <int>(default 1). Raises cleanly ifsub_dur <= GAP_S(no safe sample possible). Pure-Python, mocks-friendly — works from Bash, WSL, GitHub Actions.tests/scripts/test_audit_render_frames.py— 6 offline cases: synthetic edit_decisions, mockedsubprocess.run, sampledtvalues verified strictly inside cut windows + away from overlap edges, multi-sample variant, missing-config error, empty-beats error.
scaffold_project.py reads os.environ["AIRTABLE_API_KEY"] at startup. PowerShell sessions inherit it from shell init so the gap never surfaces in the production path, but Bash, WSL, and GitHub Actions invocations crash immediately with AIRTABLE_API_KEY (or AIRTABLE_PERSONAL_ACCESS_TOKEN) is required in env because nothing loaded .env. Operators worked around it by wrapping every call: python -c "from dotenv import load_dotenv; load_dotenv(); ..." — friction every time.
Calls tools.base_tool._load_dotenv() at module import (before any os.environ read), matching the convention every other tool in the registry already follows.
scripts/scaffold_project.py—from tools.base_tool import _load_dotenv+ call at top of module, before the field-ID constantstests/scripts/test_scaffold_project_dotenv.py— 2 offline cases: (1) staged synthetic repo skeleton intmp_pathwith a synthetic.env; after_force_reload_scaffold()popstoolspackage +scaffold_projectfromsys.modulesand re-imports,os.environ["AIRTABLE_API_KEY"]matches the synthetic value; (2) defensive source-grep that asserts the dotenv-load reference is present inscaffold_project.py(catches accidental refactor that removes the auto-load).
[Enhancement] Scaffold tighter derive_project_name heuristic (feat/scaffold-derive-project-name-tighten)
The auto-derived project name becomes the render filename. The original heuristic only dropped a handful of stop-words, which produced unusable names like wa-has-farming-town-40-miles-from-any-interstate from a long hook — too long, contained digits, contained filler ('has', 'from', 'any'), and didn't surface the topic. Worse, the 40-char length cap kicked in mid-word producing trailing fragments.
Tightens the heuristic in order:
- Strip digits per token.
'40'disappears;'i90'collapses to'i'(then drops as too-short). - Drop stop-words (case-insensitive):
a, an, the, is, of, in, on, at, to, for, and, or, but, has, have, from, any, with, this, that. - Drop tokens shorter than
_MIN_TOKEN_LEN(default 2) after digit strip. - Cap at
_MAX_TOKENS(default 4) so the rendered filename stays manageable. - Existing 40-char length cap with word-boundary truncation applies last.
Empty / all-stop-words / all-digits input falls back to 'untitled' with a stderr WARN so the operator notices. Single-token results also emit a WARN suggesting --project <kebab> for a more descriptive name.
scripts/scaffold_project.py::derive_project_name()— heuristic rewrite + tunable_STOP_WORDS,_MIN_TOKEN_LEN,_MAX_TOKENSconstants- Tests at
tests/scripts/test_scaffold_project_derive_name.py— 14 offline parametrized cases covering digit-strip, stop-word, token-cap, length-trim, untitled fallback (with WARN assertion), single-token WARN, and the trivial-clean-hook path
Five script-stage validators that catch in 5 seconds what mid-pipeline failure modes catch after 70 minutes — bundled so the operator reviews one PR instead of five. Every gate is offline, deterministic, and CPU-only; together they trip on the recurring scaffolder-output failure modes a private production run accumulated across multi-iteration shipping sprints.
scripts/check_prep_complete.py— seven mandatory checks:publish_metadata.jsonunderartifacts/(not project root),wikimedia_queries.yamlparses,fetch_pexels.pyexists,script.jsonbeat narrations contain no digit chars ([0-9]),publish_metadata.jsontags contain noTODO_placeholders,fetch_pexels.pyQUERIES has no empty/whitespace strings, projected voiceover length ≥ 61s at 150 WPM (T-021). Plus advisory image-dedup that scansassets/images/*/for cross-source byte-duplicates (T-020)scripts/scaffold_project.py::_BUILD_EDIT_TEMPLATE::W()— glob-prefix fallback when the exact archival path is missing. Wikimedia downloads commonly land asCgs05202.jpg.jpgwhile the PLAN namesCgs05202.jpg; the helper resolves the single-prefix match and logs a one-line warning. Zero / multiple matches still raise (T-010)scripts/scaffold_project.py::needs_superlative_review()— regex WARN at scaffold time on potential superlative claims (oldest,first,world's largest,two least visited X, etc.) that need WebSearch verification before TTS. Mirrors the existing digit-normalization WARN block (T-009)tools/asset_staging.py::dedupe_project_image_dirs()— companion todedupe_existing_dirswith a single project-rooted entry point. Walks every immediate subdirectory of<project_root>/assets/images/and hashes every regular file across the set, keeping the lexicographically-smallest survivor per hash group (T-020)- Tests: 61 offline cases across
tests/scripts/test_check_prep_complete*.py,tests/scripts/test_scaffold_project_build_edit_glob_fallback.py,tests/scripts/test_scaffold_project_superlative_warn.py,tests/tools/test_asset_staging_content_dedupe.py
scaffold_project.py originally accepted only the pipe-format Airtable transcript (0:00–0:14 | [visual] | narration). Operators who prefer the more compact bracket-header style ([0:00–0:14] on its own line, narration on the next lines) had to re-format every record before scaffolding. This adds a fallback parser plus an extension that supports both header shapes:
[0:00–0:14]— explicit start and end timestamps, narration on subsequent line(s)[0:00]— end timestamp omitted; inferred from the next beat's start (last beat falls back tostart + _SINGLE_BRACKET_TAIL_S, default 12s — the average closing-beat length in tested transcripts)- Trailing narration on the same line as the bracket header is also accepted:
[0:00] foo bar
The pipe-format parser is tried first; bracket parsers run as fallback when no pipe lines match. Both styles can be mixed within a single record (one header per line; the parser routes by shape).
scripts/scaffold_project.py::_parse_bracket_format()— two-pass parser. Pass 1 collects(start, end_or_none, narration_lines); pass 2 fills missing ends and emitsParsedBeat. Last beat falls back tostart + _SINGLE_BRACKET_TAIL_S._TRANSCRIPT_BRACKET_HEADERregex accepts both shapes with optional inline narration._SINGLE_BRACKET_TAIL_S = 12.0— tunable closing-beat fallback.- Tests at
tests/scripts/test_scaffold_project_bracket_parser.py— 12 offline cases covering two-timestamp headers, single-timestamp inference from the next beat's start, last-beat fallback to_SINGLE_BRACKET_TAIL_S, inline narration on the bracket-header line, inline + next-line narration joined with a single space, mixed-shape transcripts, empty input, transcripts with no bracket headers, sequential beat-id assignment, and both hyphen + en-dash separators in two-timestamp headers.
[Enhancement] Scaffold generate_music.py duration-resolver hardening (feat/scaffold-music-auto-duration)
The scaffolded generate_music.py already reads voiceover_duration_seconds from artifacts/beat_offsets.json and computes ceil(vo + TAIL_SECONDS), so the music bed sizes to the actual voiceover length rather than a typed-in number that drifts when the voiceover changes. What it didn't have was defensive validation: on a missing key or wrong type, _resolve_duration() passed the raw value straight into float(vo) and raised a generic TypeError (or worse, silently produced a wrong-sized bed when vo was True or a numeric string). The operator's diagnostic was a stack trace, not a useful message naming the broken artifact.
This tightens the resolver: missing file path is embedded in the error message naming concat_voiceover.py as the prior step to re-run; missing / non-positive / non-numeric voiceover_duration_seconds all raise SystemExit with the offending value in the message (covers None, 0, negatives, strings, and bool via an explicit isinstance(_, bool) exclusion so True doesn't sneak past isinstance(_, int)). The per-run operator-facing print also gains a tail-seconds breakdown (voiceover + 5s tail) so the chosen duration is auditable from the run log without grepping the JSON sidecar.
scripts/scaffold_project.py::_MUSIC_TEMPLATE::_resolve_duration— defensive validation: file-missing path embeds the artifact + remediation hint; missing-key / non-positive / non-numeric /boolall raiseSystemExitnaming the offending value; per-run print labels the tail-seconds splittests/scripts/test_scaffold_project_music_template_duration.py— 8 offline cases: template-integrity (rendered template carries the resolver markers + the new validation strings + compiles as Python); two happy-path cases (floatvoand integervoboth ceil correctly with the 5s tail); five failure-mode cases (missing file path includes the artifact name, missing key raisesSystemExitnotTypeError,vo: 0,vo: "119"numeric-string, andvo: Truebool all rejected by the positivity /isinstancegates)
The scaffolded build_edit.py stub's top-of-file docstring already advertises three locked invariants the build script will enforce against the agent's PLAN — hash-uniqueness, motion alternation, and the 3.5s per-cut ceiling. The 3.5s ceiling is a per-cut maximum, but long beats with too few cuts blow past it silently: a 22s beat with 5 cuts averages 4.4s/cut and violates the invariant without any per-cut check the build script enforces ever tripping. The agent only finds out at render-time, after the PLAN is filled and assets are staged.
This adds one line to the stub's docstring that calls the rule out in front of the agent BEFORE they fill the PLAN: "any beat whose narration > 20s gets >= 6 cuts (keeps avg under the 3.5s ceiling)". The rule is the same math the 3.5s ceiling already encodes (len(cuts) * 3.5 >= beat.duration_seconds), stated as a positive cut-count target so the agent can sanity-check the PLAN against artifacts/beat_offsets.json in their head while they're still filling cuts.
scripts/scaffold_project.py::_BUILD_EDIT_TEMPLATE— one new docstring line immediately after the existing invariants sentencetests/scripts/test_scaffold_project_cut_density_doc.py— 3 offline template-integrity cases: the rule text is present in the rendered stub, lives inside the leading docstring (not buried later), and the rendered stub still compiles as valid Python
Per-image gpt-4o-mini vision check that rejects region-mismatched Pexels stock (e.g. Mediterranean villas for Southwestern desert queries, Alpine chalets for Gulf Coast wetlands) before it can reach the edit PLAN. Cache-backed (SHA-256 + region key) so each unique image is judged once; UNSURE / API-error fail open (keep file), only outright NO triggers rejection. ~$0.001/image at detail=low.
tools/asset_staging.py::stage_pexels_with_region_check(file, region_desc)— the gate, called after each savetools/asset_staging.py::resolve_region_desc(script_path, project_slug)— canonical resolver (region_desc->title-> slug fallback chain)tests/tools/test_asset_staging_region_check.py— 10 offline tests with mocked OpenAI client- Cache:
~/.cache/openmontage/pexels_region_check.json
Full-resolution complement to the fetch-time Pexels region gate. The thumbnail audit grid catches the obvious off-region drift but misses subtler "rural blacktop" vs "Mediterranean two-lane" mistakes that only resolve at full frame size. The script extracts the middle frame of each beat from the final render, runs the same vision check the fetch-time gate uses, and flags any beat whose verdict is NO as REGION_DRIFT. Intentionally a standalone CLI rather than a wired phase — operators decide when / whether to integrate it into queue_runner.py after populating the anchor vocabulary.
Auto-skips when the operator's anchor vocabulary doesn't match region_desc, so brand-poll or generic-comparison projects don't spend vision API calls on nothing. Vocabulary is operator-configured via place_anchors.yaml at the repo root — see place_anchors.example.yaml for the schema. Out of the box lib.geo_anchors.is_place_anchored returns False for every input (empty default vocabulary), so the audit auto-skips every project until the YAML is populated.
lib/geo_anchors.py— YAML-driven anchor detector. Case-insensitive substring matching for state names / region terms / multi-word counties, word-boundary matching for short state codes / single-word counties (so common short names don't false-positive on neighbouring English). Empty default vocabulary; honorsOPENMONTAGE_PLACE_ANCHORS_PATHenv override for tests;reload_config()clears the module cache when the YAML is mutated mid-sessionplace_anchors.example.yaml— copy toplace_anchors.yaml(gitignored) and populate with your region's terms. The schema separatesstate_names/state_codes/counties/region_terms/weak_anchors(documentation-only) so the matching rules can be tuned per categoryscripts/check_render_region_match.py— per-beat ffmpeg middle-frame extract + vision check viatools.asset_staging._vision_check_region. Advisory exit 1 on drift, exit 0 withSKIPon auto-skip paths (empty / non-anchored / env-forced-off / missing render), exit 2 on operator error (missingbeat_offsets.json)- Tests: 19 offline cases at
tests/lib/test_geo_anchors.py(every matching rule + YAML loader graceful degradation + cache reload semantics) + 10 offline cases attests/scripts/test_check_render_region_match.py(mocked OpenAI client + mocked ffmpeg, synthetic per-test anchor YAML)
Two runtime-phase improvements that compress the cold-init tail and re-stage legacy oversize images. Both shipped as offline-testable scripts so they exercise their state machines without GPU or model downloads.
scripts/warmup_whisper.py— pre-warms cuDNN at iter-0 start so the transcribe phase doesn't lose 60-180s on the cold "loading large-v3 on cuda" path.queue_runner.pyspawns it as a background subprocess in parallel with the CPU-only iter-0 phases; the handle is joined just before the transcribe phase with a 240s ceiling, then the warmup process exits cleanly. Best-effort: missingfaster_whisper, no CUDA, or transcribe error all skip without raising (T-023)scripts/backfill_asset_library_downscale.py— one-shot CLI that walks the asset library and downscales any file whose long edge exceeds the configured ceiling (default 4096px). Companions the--max-dimflag onasset_library_stage.py(which clamps at fetch time) by retroactively fixing pre-flag legacy files. Idempotent: under-threshold files are not re-encoded; PNG sources stay PNG; JPEG q92 mirrors the existing downscale workaround.--dry-runflag for inventory inspection before mutating (T-019)- Tests: 27 offline cases across
tests/scripts/test_warmup_whisper.py(mockedWhisperModel, all device-fallback branches),tests/scripts/test_queue_runner_warmup_dispatch.py(mockedsubprocess.Popen, iter-0-spawn-once / iter-1+-skip / dry-run-skip / join-before-transcribe ordering / timeout-logs-no-raise),tests/scripts/test_backfill_asset_library_downscale.py(PIL synthetic fixtures, idempotent re-runs, dry-run mode)
A batch of polish across existing tools, distilled from a long-running private production tree:
- Whisper transcriber — cuDNN cold-init heartbeat (every 20s) +
cuda → cpuauto-fallback ladder, so first-call hangs no longer look like "stuck" - SDXL
local_diffusion—compellong-prompt support (lifts the 77 CLIP-token cap), single-file safetensors loading,model_familyrouting - OpenAI Image —
gpt-image-2model enum,backgroundparameter (transparent/opaque/auto),reference_image_path(s)for image edits - I2V pipeline routing — Wan and similar models route to
WanImageToVideoPipelinewhen in image-to-video mode - Freesound music — updated
fallback_toolschain - Remotion
Explainer+CinematicRenderer— first-scene fade-in opens at 0.78 opacity so overlaid hook text reads cleanly CaptionOverlay— 30% bottom padding to clear vertical-platform UI danger zonesscripts/audio_amplitude.py— RMS-amplitude utility for music-ducking decisions- 13 pipeline director skills — workflow improvements and library-first asset-discovery callouts across
animation/character-animation/cinematic/documentary-montage/explainer/hybrid/talking-head
Bug fixes against tools the fork inherited:
- Remotion
Root.tsx—calculateMetadatathrows on emptyprops.cutsinstead of silently returning a 60-second default (catches the PowerShell--propspath-resolution bug at composition init); also registers theExplainerVertical1080×1920 composition. pexels_image—per_page > 1callers now receive one file per photo (output_path is treated as a directory; each file is namedpexels_<id>.jpgand per-photo metadata is exposed inToolResult.data["photos"]). Schema defaultper_pagechanged5 → 1so the documented default matches actual behavior. Version bumped0.1.0 → 0.2.0(#12).
The build_edit.py validator rejects PLANs that reuse the same image bytes. Cross-session mining surfaced this firing in 78 of 131 sessions (414 total occurrences). Root cause: Pexels content-level duplicates — the same photo bytes returned for different queries, then accepted into different beat folders by dedupe_pexels_save (which only deduped within a single fetch). Fix has two parts:
tools/asset_staging.py::dedupe_existing_dirs()— cross-directory content-hash dedup, applied across all per-beatassets/images/stock/<beat>/subdirs after each new fetchscripts/audit_candidates.py— content-hash duplicate flagging in the audit grid (red border +content-duplicate sha=<8>reason badge), so cross-source duplicates surface before PLAN- Tests at
tests/tools/test_asset_staging.py(6 cases) andtests/scripts/test_audit_candidates.py(7 added cases) — all offline, synthetic fixtures
AGENT_GUIDE.md— Critical Production Rules section — silent-failure-prevention rules (visual cadence, ffprobe verification, process safety, central CLIs for Wikimedia + Whisper, ACE-Step VRAM release, Remotion--propsabsolute-path rule) plus pre/post-render checklists. The Pre-TTS Narration Rules section (spell out numbers, no compound-number hyphens) is here too.docs/PRODUCTION_DEFAULTS.md— craft layer with the longer "why" for each ruledocs/protocol.md(also atskills/meta/asset-library-protocol.md) — three-protocol asset-library playbook.env.example— every env var the codebase reads (25+), grouped by capability, with cost/license notes per provider and a "Zero-key path" callout pointing at the Brooklyn Bridge example/dashboardslash command — Claude Code one-liner to launch the asset library dashboardassets/logo.png— originalgpt-image-2-generated logo (no upstream-mark inheritance)
Every feature above is consolidated on
mainas ofv0.1.0. The branches that introduced them (feat/asset-cache-extract,feat/acestep-music,feat/f5tts,feat/central-fetch-archival,fix/pixabay-cloudscraper,fix/wikimedia-rate-limit) have been merged and deleted — clone and you're up.
A complete 72-second documentary on the human cost of the Brooklyn Bridge — John Roebling's tetanus death, Washington Roebling's caisson disease, Emily Roebling effectively running the project until the 1883 opening — produced end-to-end through the documentary-montage pipeline using only free / local providers: F5-TTS for narration (with a public-domain LibriVox reference voice), ACE-Step 1.5 for music, Wikimedia Commons for archival imagery, faster-Whisper for transcription, Remotion for composition. Zero paid API calls.
Captioned variant shown above. The repository also ships a clean version without burned captions for platforms that supply their own.
Browse the full source bundle in examples/projects/brooklyn-bridge/ — 15 archival images (all Public Domain / CC0), 8 per-beat narration MP3s + the concatenated voiceover, the ACE-Step music score, the LibriVox voice reference + transcript, every pipeline artifact (brief → script → scene plan → asset manifest → edit decisions → props → render report), the Python helpers that produced each, and a NOTICE.md with per-asset license + attribution.
To regenerate from scratch (zero-key path):
python scripts/fetch_archival.py --config examples/projects/brooklyn-bridge/artifacts/wikimedia_queries.yaml
python examples/projects/brooklyn-bridge/artifacts/generate_narration.py
python examples/projects/brooklyn-bridge/artifacts/generate_music.py
python scripts/transcribe_voiceover.py --project brooklyn-bridge
python examples/projects/brooklyn-bridge/artifacts/build_edit.py
pwsh examples/projects/brooklyn-bridge/artifacts/render.ps1Expect ~15–25 minutes on a single-GPU box.
Starting from a reference video is often faster than starting from a blank prompt.
OpenBoomhauer can start from a YouTube video, Short, Reel, TikTok, or local clip and turn it into a grounded production plan:
- Paste a reference video
- The agent analyzes transcript, pacing, scenes, keyframes, and style
- You get 2-3 differentiated concepts, an honest tool path, cost estimates, and a sample before full production
"Here's a YouTube Short I love. Make me something like this, but about quantum computing."
What you get back is not "best guess prompt spaghetti." You get:
- What it keeps from the reference: pacing, hook style, structure, tone
- What it changes: topic, visual treatment, angle, narration approach
- What it will cost at your target duration, before asset generation starts
- What it will actually look like with your currently available tools
Works with Claude Code, Cursor, Copilot, Windsurf, Codex — any AI coding assistant that can read files and run code.
- Python 3.10+ — python.org
- FFmpeg —
brew install ffmpeg/sudo apt install ffmpeg/ ffmpeg.org - Node.js 18+ — nodejs.org
- An AI coding assistant — Claude Code, Cursor, Copilot, Windsurf, or Codex
git clone https://github.com/billdauterive/OpenBoomhauer.git
cd OpenBoomhauer
make setupOpen the project in your AI coding assistant and tell it what you want:
"Make a 60-second animated explainer about how neural networks learn"
Or if you want the real-footage path:
"Make a 75-second documentary montage about city life in the rain. Use real footage only, no narration, elegiac tone, with music."
That's it. The agent researches your topic with live web search, generates AI images, writes and narrates the script with voice direction, finds royalty-free background music automatically, burns in word-level subtitles, and renders the final video. Before you see anything, the system runs a multi-point self-review — ffprobe validation, frame sampling, audio level analysis, delivery promise verification, and subtitle checks. Every provider selection is scored across 7 dimensions with an auditable decision log. Every creative decision gets your approval.
No
make? Run manually:pip install -r requirements.txt && cd remotion-composer && npm install && cd .. && pip install piper-tts && cp .env.example .envWindows: If
npm installfails withERR_INVALID_ARG_TYPE, usenpx --yes npm installinstead.
This repo is built for agentic operation. If you're an OpenClaw-style agent, here is the shortest path to becoming useful fast:
- Read the contract first
Start with
AGENT_GUIDE.md, thenPROJECT_CONTEXT.md. - Do not improvise the production workflow
OpenBoomhauer is pipeline-driven. Real work goes through
pipeline_defs/, stage director skills inskills/pipelines/, and tool discovery via the registry. - Check the actual capability envelope
Run:
python -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.support_envelope(), indent=2))" python -c "from tools.tool_registry import registry; import json; registry.discover(); print(json.dumps(registry.provider_menu(), indent=2))"
- Treat every video request as a pipeline selection problem Pick the right pipeline first, then read the manifest, then read the stage skill, then use tools.
# .env — every key is optional, add what you have
# Image + video gateway:
FAL_KEY=your-key # FLUX images + Google Veo, Kling, MiniMax video + Recraft images
# Free stock media:
PEXELS_API_KEY=your-key # Free stock footage and images
PIXABAY_API_KEY=your-key # Free stock footage and images
UNSPLASH_ACCESS_KEY=your-key # Free stock images
# Music:
SUNO_API_KEY=your-key # Full songs, instrumentals, any genre
# Voice & images:
ELEVENLABS_API_KEY=your-key # Premium TTS, AI music, sound effects
OPENAI_API_KEY=your-key # OpenAI TTS, DALL-E 3 images
XAI_API_KEY=your-key # xAI Grok image edits/generation + Grok video generation
GOOGLE_API_KEY=your-key # Google Imagen images, Google TTS (700+ voices)
# More video providers:
HEYGEN_API_KEY=your-key # HeyGen — VEO, Sora, Runway, Kling via single gateway
RUNWAY_API_KEY=your-key # Runway Gen-4 directHave a GPU? Unlock free local video generation
make install-gpu
# Then add to .env:
VIDEO_GEN_LOCAL_ENABLED=true
VIDEO_GEN_LOCAL_MODEL=wan2.1-1.3b # or wan2.1-14b, hunyuan-1.5, ltx2-local, cogvideo-5bYou don't need paid API keys to make real videos. Out of the box, make setup gives you:
| Capability | Free Tool | What It Does |
|---|---|---|
| Narration | Piper TTS | Free offline text-to-speech — real human-sounding narration |
| Open footage | Archive.org + NASA + Wikimedia Commons | Free/open archival footage, educational media, and documentary texture |
| Extra stock | Pexels + Unsplash + Pixabay | Free stock footage/images (developer keys are free to get) |
| Composition (React) | Remotion | React-based rendering — spring-animated image scenes, text cards, stat cards, charts, TikTok-style word-level captions, TalkingHead |
| Composition (HTML/GSAP) | HyperFrames | HTML/CSS/GSAP rendering — kinetic typography, product promos, launch reels, registry blocks, website-to-video, rigged SVG character animation |
| Post-production | FFmpeg | Encoding, subtitle burn-in, audio mixing, color grading |
| Subtitles | Built-in | Auto-generated captions with word-level timing |
OpenBoomhauer picks between Remotion and HyperFrames at proposal time (locked as render_runtime). Remotion is the default for data-driven explainers and anything using the existing React scene stack; HyperFrames is the default for motion-graphics-heavy briefs that express naturally as HTML + GSAP, including the character-animation pipeline's SVG/GSAP rig output. See skills/core/hyperframes.md for the full decision matrix.
Two free-ish paths:
- Image-based video: Piper narrates your script, images provide the visuals, and Remotion animates them into a polished edit.
- Local character animation: SVG rigs, pose libraries, GSAP timelines, and HyperFrames render cartoon character acting to
projects/<project-name>/renders/final.mp4. - Real-footage video: the documentary montage pipeline builds a CLIP-searchable corpus from Archive.org, NASA, Wikimedia Commons, and optional free-key sources like Pexels and Unsplash, then cuts together actual motion footage into a finished video.
If you want the second one, prompt for a documentary montage, tone poem, or stock-footage collage, and explicitly say use real footage only.
Copy any of these into your AI coding assistant after setup. Each one runs a full production pipeline.
"Here's a YouTube short I love. Make me something like this, but about CRISPR for high school students."
"Analyze this Reel and give me 3 original variants I could make for my own product launch."
"I like the pacing and hook in this video. Keep that energy, but turn it into a 45-second explainer about black holes."
"Make a 45-second animated explainer about why the sky is blue"
"Create a 60-second video about the history of the internet, with narration and captions"
"Make a data-driven explainer about coffee consumption around the world"
"Make a 90-second documentary montage about what a city feels like at 4am. Use real footage only, no narration, elegiac tone."
"Create a 60-second Adam-Curtis-style archival collage about 1950s consumer optimism. Prefer Archive.org and Wikimedia footage."
"Cut together a dreamlike montage about coming home in the rain using real stock footage only. Music yes, narration no."
"Create a 30-second Ghibli-style animated video of a magical floating library in the clouds at golden hour"
"Make a 30-second anime-style animation of an underwater temple with bioluminescent coral and ancient ruins"
"Create an animated explainer about how CRISPR gene editing works, using AI-generated visuals"
"Make a product launch teaser for a fictional smart water bottle called AquaPulse"
"Create a cinematic 30-second trailer for a sci-fi concept: humanity receives a warning from 1000 years in the future"
"Make a 90-second animated explainer about quantum computing for middle school students, with a fun narrator voice and custom soundtrack"
Want more? See the full Prompt Gallery for tested prompts with expected costs and output examples, or run make demo to render zero-key demo videos instantly.
Each pipeline is a complete production workflow, from idea to finished video.
| Pipeline | What It Produces | Best For |
|---|---|---|
| Animated Explainer | AI-generated explainer with research, narration, visuals, music | Educational content, tutorials, topic breakdowns |
| Animation | Motion graphics, kinetic typography, animated sequences | Social media, product demos, abstract concepts |
| Avatar Spokesperson | Avatar-driven presenter videos | Corporate comms, training, announcements |
| Cinematic | Trailer, teaser, and mood-driven edits | Brand films, teasers, promotional content |
| Clip Factory | Batch of ranked short-form clips from one long source | Repurposing long content for social media |
| Documentary Montage | Thematic montage cut from a CLIP-indexed corpus of free stock footage and open archives (Pexels, Archive.org, NASA, Wikimedia, Unsplash) | Video essays, mood pieces, retrieval-first B-roll edits, real-footage videos without paid generation APIs |
| Hybrid | Source footage + AI-generated support visuals | Enhancing existing footage with graphics |
| Localization & Dub | Subtitle, dub, and translate existing video | Multi-language distribution |
| Podcast Repurpose | Podcast highlights to video | Podcast marketing, audiogram videos |
| Screen Demo | Polished software screen recordings and walkthroughs | Product demos, tutorials, documentation |
| Talking Head | Footage-led speaker videos | Presentations, vlogs, interviews |
Every pipeline follows the same structured flow:
research -> proposal -> script -> scene_plan -> assets -> edit -> compose
Each stage has a dedicated director skill — a markdown instruction file that teaches the agent exactly how to execute that stage. The agent reads the skill, uses the tools, self-reviews, checkpoints state, and asks for human approval at creative decision points.
Web research is a first-class stage. Before writing a single word of script, the agent searches YouTube, Reddit, Hacker News, news sites, and academic sources. It gathers data points, audience questions, trending angles, and visual references — then cites everything in a structured research brief. Your videos are grounded in real, current information, not hallucinated facts.
Most AI video tools give you a single clip from a prompt. OpenBoomhauer gives you an end-to-end production pipeline — the same structured process a real production team follows, automated by your AI agent.
Most "free AI video" stacks quietly mean "animate still images." OpenBoomhauer can do that too, but it can also build a finished video from real footage pulled from free/open sources, ranked semantically, edited intentionally, and rendered as a proper timeline.
Edit your own talking-head footage. Generate a fully animated explainer from scratch. Cut a 2-hour podcast into a dozen social clips. Translate and dub your content into 10 languages. Build a cinematic brand teaser from stock footage and AI-generated scenes. If a production team can make it, OpenBoomhauer can orchestrate it.
- 12 production pipelines — explainers, talking heads, screen demos, cinematic trailers, animations, podcasts, localization, documentary montages, and more
- 52 production tools — spanning video generation, image creation, text-to-speech, music, audio mixing, subtitles, enhancement, and analysis
- 400+ agent skills — production skills, pipeline directors, creative techniques, quality checklists, and deep technology knowledge packs that teach the agent how to use every tool like an expert
- Reference-driven creation — paste a video you like and the agent turns it into a grounded, differentiated production plan instead of forcing you to invent the perfect prompt from scratch
- Real-footage documentary creation without paid video models — build actual edited videos from free/open motion footage and archival sources, not just Ken Burns over images
- Live web research built in — before writing a single word of script, the agent runs 15-25+ web searches across YouTube, Reddit, news sites, and academic sources to ground your video in real, current data
- Both free/local AND cloud providers — every capability supports open-source local alternatives alongside premium APIs. Use what you have.
- No vendor lock-in — swap providers freely. The scored selector ranks every provider across 7 dimensions (task fit, output quality, control, reliability, cost efficiency, latency, continuity) and picks the best match automatically.
- Production-grade quality gates — delivery promise enforcement blocks slideshow-looking renders, pre-compose validation catches broken plans before wasting GPU time, and mandatory post-render self-review (ffprobe + frame extraction + audio analysis) ensures the agent never presents garbage. Every provider choice, style decision, and fallback gets logged in an auditable decision trail.
- Budget governance built in — cost estimation before execution, spend caps, per-action approval thresholds. No surprise bills.
OpenBoomhauer uses an agent-first architecture. There is no code orchestrator. Your AI coding assistant IS the orchestrator.
You: "Make an explainer video about how black holes form"
|
v
Agent reads pipeline manifest (YAML) -- stages, tools, review criteria, success gates
|
v
Agent reads stage director skill (Markdown) -- HOW to execute each stage
|
v
Agent calls Python tools -- scored provider selection ranks every tool across 7 dimensions
|
v
Agent self-reviews using reviewer skill -- schema validation, playbook compliance, quality checks
|
v
Agent checkpoints state (JSON) -- resumable, with decision log and cost snapshot
|
v
Agent presents for your approval -- you stay in control at every creative decision
|
v
Pre-compose validation gate -- delivery promise, slideshow risk, renderer governance
|
v
Render (Remotion or FFmpeg) -- composition engine matched to visual grammar
|
v
Post-render self-review -- ffprobe, frame extraction, audio analysis, promise verification
|
v
Final video output -- only if self-review passes
Python provides tools and persistence. All creative decisions, orchestration logic, review criteria, and quality standards live in readable instruction files (YAML manifests + Markdown skills) that you can inspect and customize. Every decision is logged with alternatives considered, confidence scores, and the reasoning behind each choice.
OpenBoomhauer/
├── tools/ # 48 Python tools (the agent's hands)
│ ├── video/ # 13 video gen tools + compose, stitch, trim
│ ├── audio/ # 4 TTS providers + Suno/ElevenLabs music, mixing, enhancement
│ ├── graphics/ # 9 image/graphics generation tools + diagrams, code snippets, math
│ ├── enhancement/ # Upscale, bg remove, face enhance, color grade
│ ├── analysis/ # Transcription, scene detect, frame sampling
│ ├── avatar/ # Talking head, lip sync
│ └── subtitle/ # SRT/VTT generation
│
├── pipeline_defs/ # YAML pipeline manifests (the agent's playbook)
├── skills/ # Markdown skill files (the agent's knowledge)
│ ├── pipelines/ # Per-pipeline stage director skills
│ ├── creative/ # Creative technique skills
│ ├── core/ # Core tool skills
│ └── meta/ # Reviewer, checkpoint protocol
│
├── schemas/ # 15 JSON Schemas (contract validation)
├── styles/ # Visual style playbooks (YAML)
├── remotion-composer/ # React/Remotion video composition engine
├── lib/ # Core infrastructure (config, checkpoints, pipeline loader)
└── tests/ # Contract tests, QA integration tests, eval harness
Layer 1: tools/ + pipeline_defs/ "What exists" — executable capabilities + orchestration
Layer 2: skills/ "How to use it" — OpenBoomhauer conventions and quality bars
Layer 3: .agents/skills/ "How it works" — external technology knowledge packs
Each tool declares which Layer 3 skills it relies on. The agent reads Layer 1 to know what's available, Layer 2 to know how OpenBoomhauer wants it used, and Layer 3 for deep technical knowledge when needed.
Full setup guide with pricing and free tiers:
docs/PROVIDERS.md
Video Generation — 14 providers
| Provider | Type | Notes |
|---|---|---|
| Kling | Cloud API | High quality, fast |
| Runway Gen-4 | Cloud API | Cinematic quality, Gen-3 Alpha Turbo / Gen-4 Turbo / Gen-4 Aleph |
| Google Veo 3 | Cloud API | Long-form, cinematic. Via fal.ai or HeyGen. |
| Grok Imagine Video | Cloud API | Strong reference-image video and xAI-native short-form generation |
| Higgsfield | Cloud API | Multi-model orchestrator with Soul ID for character consistency |
| MiniMax | Cloud API | Cost-effective |
| HeyGen | Cloud API | Multi-model gateway |
| WAN 2.1 | Local GPU | Free, 1.3B and 14B variants |
| Hunyuan | Local GPU | Free, high quality |
| CogVideo | Local GPU | Free, 2B and 5B variants |
| LTX-Video | Local GPU / Modal | Free locally, or self-hosted cloud |
| Pexels | Stock | Free stock footage |
| Pixabay | Stock | Free stock footage |
| Wikimedia Commons | Stock | Free/open stock footage and archival video |
Image Generation — 10 tools/providers
| Provider | Type | Notes |
|---|---|---|
| FLUX | Cloud API | State-of-the-art quality |
| Google Imagen | Cloud API | Imagen 4 — high-quality, multiple aspect ratios |
| Grok Imagine Image | Cloud API | Strong image edits, style transfer, and multi-image compositing |
| DALL-E 3 | Cloud API | OpenAI's image model |
| Recraft | Cloud API | Design-focused generation |
| Local Diffusion | Local GPU | Stable Diffusion, free |
| Pexels | Stock | Free stock images |
| Pixabay | Stock | Free stock images |
| Unsplash | Stock | Free stock images |
| ManimCE | Local | Mathematical animations |
Text-to-Speech — 4 providers
| Provider | Type | Notes |
|---|---|---|
| ElevenLabs | Cloud API | Premium voice quality |
| Google TTS | Cloud API | 700+ voices, 50+ languages — best for localization |
| OpenAI TTS | Cloud API | Fast, affordable |
| Piper | Local | Completely free, offline |
Music, Sound & Post-Production
Music & Sound:
| Provider | Type | Notes |
|---|---|---|
| Suno AI | Cloud API | Full song generation with vocals, lyrics, any genre. Up to 8 minutes. |
| ElevenLabs Music | Cloud API | AI music generation |
| ElevenLabs SFX | Cloud API | Sound effect generation |
Post-Production (always available, always free):
| Tool | What It Does |
|---|---|
| FFmpeg | Video composition, encoding, subtitle burn-in, audio muxing |
| Video Stitch | Multi-clip assembly, crossfades, picture-in-picture, spatial layouts |
| Video Trimmer | Precision cutting and extraction |
| Audio Mixer | Multi-track mixing, ducking, fades |
| Audio Enhance | Noise reduction, normalization |
| Color Grade | LUT-based color grading |
| Subtitle Gen | SRT/VTT generation from timestamps |
Enhancement:
| Tool | What It Does |
|---|---|
| Upscale | Real-ESRGAN image/video upscaling |
| Background Remove | rembg / U2Net background removal |
| Face Enhance | Face quality enhancement |
| Face Restore | CodeFormer / GFPGAN face restoration |
Analysis:
| Tool | What It Does |
|---|---|
| Transcriber | WhisperX speech-to-text with word-level timestamps |
| Scene Detect | Automatic scene boundary detection |
| Frame Sampler | Intelligent frame extraction |
| Video Understand | CLIP/BLIP-2 vision-language analysis |
Avatar & Lip Sync:
| Tool | What It Does |
|---|---|
| Talking Head | SadTalker / MuseTalk avatar animation |
| Lip Sync | Wav2Lip audio-driven lip synchronization |
Composition & Rendering:
| Engine | Type | What It Does |
|---|---|---|
| Remotion | Local (Node.js) | React-based programmatic video — spring-animated image scenes, stat reveals, section titles, hero cards, TikTok-style word-by-word captions, scene transitions (fade/slide/wipe/flip), Google Fonts, audio with fade curves, and the TalkingHead avatar composition. When no video generation providers are configured, the agent generates still images and Remotion turns them into fully animated video. |
| HyperFrames | Local (Node.js ≥ 22) | HTML/CSS/GSAP programmatic video — kinetic typography, product promos, launch reels, custom motion graphics, registry blocks (data charts, grain overlays, shader transitions), website-to-video workflows, and rigged SVG character animation. Consumed via npx hyperframes; no monorepo checkout needed. |
| FFmpeg | Local | Core video assembly, encoding, subtitle burn, audio muxing, color grading |
Runtime is chosen at proposal (render_runtime) and locked through edit_decisions. Silent swaps between runtimes are a governance violation — see skills/core/hyperframes.md.
Style playbooks define the visual language for your productions:
| Playbook | Best For |
|---|---|
| Clean Professional | Corporate, educational, SaaS |
| Flat Motion Graphics | Social media, TikTok, startups |
| Minimalist Diagram | Technical deep-dives, architecture |
Playbooks control typography, color palettes, motion styles, audio profiles, and quality rules. The agent reads the playbook and applies it consistently across all generated assets.
Built-in render profiles for every major platform:
| Profile | Resolution | Aspect Ratio |
|---|---|---|
| YouTube Landscape | 1920x1080 | 16:9 |
| YouTube 4K | 3840x2160 | 16:9 |
| YouTube Shorts | 1080x1920 | 9:16 |
| Instagram Reels | 1080x1920 | 9:16 |
| Instagram Feed | 1080x1080 | 1:1 |
| TikTok | 1080x1920 | 9:16 |
| 1920x1080 | 16:9 | |
| Cinematic | 2560x1080 | 21:9 |
OpenBoomhauer treats video production like real engineering — with quality gates, audit trails, and enforcement at every stage.
- Pre-compose validation — blocks render if the delivery promise is violated (e.g. "motion-led" video with 80% still images), slideshow risk score is critical, or renderer family is missing. Catches broken plans before wasting GPU time.
- Post-render self-review — after every render, the runtime runs ffprobe validation, extracts frames at 4 positions to check for black frames and broken overlays, analyzes audio levels for silence and clipping, verifies the delivery promise was honored, and checks subtitle presence. If the review fails, the video is not presented.
- Slideshow risk scoring — 6-dimension analysis (repetition, decorative visuals, weak motion, shot intent, typography overreliance, unsupported cinematic claims) prevents "animated PowerPoint" outputs.
- Source media inspection — when users supply their own footage, the system probes every file (resolution, codec, audio channels, duration) and builds planning implications before a single creative decision is made. No hallucinating content from filenames.
Every tool selection (video generation, image generation, TTS, music) runs through a 7-dimension scoring engine: task fit (30%), output quality (20%), control features (15%), reliability (15%), cost efficiency (10%), latency (5%), continuity (5%). The winning provider and its score are logged in the decision trail with all alternatives considered.
Selectors normalize loose brief context before scoring. If the agent only knows something like "Pixar-style animated short with character consistency," the selector expands that into scorer-friendly intent and style signals instead of requiring a perfectly pre-shaped task_context.
Selector outputs also surface the chosen provider's agent_skills, so the agent can immediately read the right Layer 3 provider skill before writing prompts.
Every major creative and technical choice — provider selection, style/playbook choice, music track, voice selection, renderer family, any fallback or downgrade — is logged with alternatives considered, confidence scores, and reasoning. The cumulative decision log persists across all stages so you can trace exactly why the output looks the way it does.
- Estimate before execution — see what it will cost
- Reserve budget — lock funds before the call
- Reconcile after — record actual spend
- Configurable modes —
observe(track only),warn(log overruns),cap(hard limit) - Per-action approval — pause for confirmation above a threshold (default: $0.50)
- Total budget cap — default $10, fully configurable
No surprise bills. The agent tells you what it will cost before it spends.
OpenBoomhauer works with any AI coding assistant that can read files and execute Python. Dedicated instruction files are included for:
| Platform | Config File |
|---|---|
| Claude Code | CLAUDE.md |
| Cursor | CURSOR.md + .cursor/rules/ |
| GitHub Copilot | COPILOT.md + .github/copilot-instructions.md |
| Codex | CODEX.md |
| Windsurf | .windsurfrules |
All platform files point to the shared AGENT_GUIDE.md (operating guide and agent contract) and PROJECT_CONTEXT.md (architecture reference).
Coming soon: Local LLM support via Ollama and LM Studio — run the full production pipeline without any cloud LLM.
OpenBoomhauer is built to be extended. The two most common contributions:
- Create a Python file in the appropriate
tools/subdirectory - Inherit from
BaseTooland implement the tool contract - The registry auto-discovers it — no manual registration needed
- Add a skill file if the tool needs usage guidance
- Create a YAML manifest in
pipeline_defs/ - Create stage director skills in
skills/pipelines/<your-pipeline>/ - Reference existing tools — or add new ones if needed
See docs/ARCHITECTURE.md for the full technical reference, docs/PROVIDERS.md for the complete provider guide (setup, pricing, free tiers), and AGENT_GUIDE.md for the agent contract.
- This fork: GitHub Issues for fork-specific bugs and feature requests against the asset library, ACE-Step / F5-TTS integrations, central archival fetcher, and the rate-limit fixes.
- Upstream: calesthio/OpenMontage Discussions — Show-and-Tell, Ideas, and Q&A for the base project remain a good place for general OpenMontage questions, prompt-sharing, and workflow chat.
For fork-specific bugs and discussion: GitHub Issues on this repository.
For upstream OpenMontage questions: upstream Issues and Discussions.
# Run contract tests (no API keys needed)
make test-contracts
# Run all tests
make testOpenBoomhauer — A continued fork of OpenMontage with a cross-project asset library, local music + voice providers, and rate-limit-compliant archival fetching.
If this fork looks useful to you, a star would really mean a lot — and consider starring upstream OpenMontage too, since this work is built on theirs.

