The CLI backend reloads the ~3GB model per frame (~2min/frame). Add a SERVER mode:
when LLAMACPP_SERVER_BIN is set, start llama-server once, POST base64 frames to its
OpenAI /v1/chat/completions endpoint on loopback, stop it at the end. Same answers,
~3x faster on a full clip (measured: 3 frames 181s incl. one-time load vs ~6-8min).
CLI mode (LLAMACPP_MTMD_BIN) remains the simple fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
groundOverlay sampled the payoff shot THROUGH the final (button/outcome) shot, so a
post-payoff reaction frame could supply the overlay (bowling: 'SMILING' from the
bowler lowering his arms after the celebration). Confine the overlay caption to the
payoff (slow-mo) shot — the decisive moment the judge chose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Provisioned and tested the stronger Tier-2 judge on this Intel Mac (CPU-only):
- vision_caption_llamacpp.py: force CPU (-ngl 0 --no-mmproj-offload) because the
integrated GPU times out on the vision encoder (Metal command-buffer timeout);
extract the final assistant turn from the chat-templated output.
- docs/LOCAL-MODELS.md: the VERIFIED build+run recipe, incl. two real gotchas ->
Command Line Tools libc++ mismatch (add -isystem <SDK>/usr/include/c++/v1 to the
cmake flags, else ggml-base fails on <array>), and the Intel-GPU Metal timeout.
Verified result: where moondream described the bowling celebration as "standing in
a bowling alley", Qwen2.5-VL-3B says "raising their arms in a celebratory gesture"
(highlight-worthiness 1.0) and rates turn-around/anticipation low (0.2 / 0.15). End
to end, the director's judge now chooses time=10.5s score=1.0 (the celebration) vs
moondream's 13.0s score=0.15 (the turn-away). Model weights are gitignored under
models/ (Apache-2.0, provisioned offline).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The director's vision JUDGE is only as good as its model, and moondream2 can't
perceive some actions on hard footage (R15 ceiling). Make the captioner backend a
config choice so a stronger local VLM drops in with no code change:
- tools/vision_caption_llamacpp.py: same manifest->JSON contract as
tools/vision_caption.py, but backed by llama.cpp `llama-mtmd-cli` (GGUF). Runs on
this x86 CPU via AVX and bypasses the torch==2.2.2 / transformers 4.x trap
entirely (no PyTorch). Model/mmproj/binary paths come from env vars; fully
offline, serverless (per-frame CLI, mmap stays warm).
- editing.vision-caption-script selects the worker (default: moondream). The Java
HighlightVisionDirector now reads the configured script instead of a hardcoded
path -- nothing else changes.
- docs/LOCAL-MODELS.md: provisioning + enablement for Qwen2.5-VL-3B (Apache-2.0)
via llama.cpp; alternatives (Qwen3-VL, Gemma 3 4B). Honest note: likely improves
the bowling case but unverified until tested with real weights.
mvn verify: 293 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Choosing WHICH moment is the highlight is a question of meaning, not motion or
loudness, and there is no generic rule in measurement alone: on the real bowling
clip a camera turn-away has the highest motion AND is louder than the celebration.
Positional bands / thresholds only move which video breaks. So responsibilities
are now split, with zero per-video constants:
- HighlightMontageDirector.candidatePeaks: measurement PROPOSES the intensity
(motion+audio) local maxima, strongest-first, min-separated. No opinion on which
is the highlight; no band, no threshold.
- HighlightVisionDirector.rankDecisiveMoment: the vision model JUDGES each
candidate by highlight-worthiness (a celebration/goal outranks a loud turn-away
or an "about to..." build; anticipation is not the payoff). Highest score wins;
falls back to the strongest peak only if the model declines.
- composeMontageAt: builds the action segment (measured onset -> chosen peak ->
measured resolution that sweeps in the outcome+reaction) around the choice.
- MomentChooser interface makes the judge a drop-in: a stronger local VLM plugs in
with no director changes.
Removes the previous positional-band / semantic-weight heuristics.
Honest, verified ceiling (documented in R15): the judge is only as good as its
eyes. moondream2 perceives some actions (soccer: "kicking a soccer ball" -> the
goal is chosen correctly) but not others -- on distant portrait bowling footage it
describes every frame as "standing"/"walking" and never sees the celebration
(a posture prompt collapsed to a constant "Standing still"). When it can't
discriminate, candidates tie and it falls back to the loudest peak. This is a
model-capability limit, not a design flaw; the fix is a stronger VLM (drop-in).
mvn verify: 293 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The highlight could cut off before the actual action and then assert an action
that was never shown (a "KICK" overlay over a stop-before-the-kick cut). Root
causes, all generic and fixed here:
- Localization (A): the climax search only scanned the central 25-80% band on
audio(+semantic), structurally excluding a late payoff and often landing on the
anticipation. Now it searches nearly the whole clip and localizes primarily by
MOTION (the decisive action is a motion event), audio equal, semantic a weak
tie-breaker, with a robust percentile-based camera-whip guard.
- Outcome (B): the window ended at a fixed climax+offset. It now extends past the
climax until motion settles (the action AND its result), whip-guarded and capped.
- Honest overlay (C): a DENSE pass over the shown payoff window (not 7 sparse
whole-clip frames) picks the best action frame; an honesty rule never asserts an
action the window does not show -- anticipation gets a grounded teaser question
instead (HighlightVisionDirector.groundOverlay/honestOverlayText/isAnticipatory).
The VLM no longer picks the moment (it mislocalized onto anticipation), only
grounds the overlay.
- Pacing cap: montage-max-build-seconds bounds the single pre-climax build shot so
a distant action spike on a long source can't create one runaway shot (dead air
plus an impractically long generated score).
Also: re-ingesting a source whose name already exists in processed/ no longer
fails -- moveToDirectory picks a unique "<name>-<n>.<ext>" instead of refusing.
Verified end-to-end on a new 63s landscape soccer clip: the director now finds the
scoring kick (ball in net), keeps the outcome, honestly labels "KICK", 24fps/420p/
-16 LUFS, beat-synced, subject-followed. mvn verify: 292 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AGENTS.md: portable onboarding for any assistant (Codex/Claude/etc.) at the repo
root -- what the project is, read-order, build/test/run, hard constraints, honest
status, env gotchas. Makes the whole repo usable by any AI, not just Claude.
- docs/gate-b-review-bowling.md: the production-readiness scorecard. Gate A
(technical) measured = PASS; Gate B (human creative rubric) = PENDING. Output is
production-ready only when BOTH pass; this is the artifact a human fills to decide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
Port the run-to-final flow and the local model runtime/version-trap facts from
Claude-specific auto-memory into repo Markdown so any assistant or human reading
the repo (Codex, Claude, etc.) has them. Runbook updated for the auto-director
(the plan is generated automatically now; manual authoring is an override).
Linked from the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
Deterministic build across environments. Closes the wrapper follow-up noted when
CI was added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
Multi-stage build (maven:3.9-temurin-21 -> eclipse-temurin:21-jre): builds the jar,
installs ffmpeg, runs as a non-root user. The large, non-commercially-licensed AI
models and Python venv are deliberately NOT baked in -- mount them read-only at
runtime; REST/folder workflows need no models. .dockerignore keeps generated media,
models, and local state out of the build context.
Not build-validated in this environment (no running Docker daemon); the file is a
standard reviewable artifact and a starting point -- no-egress operation, scanning,
and further hardening remain to be validated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
POST /v1/edit-projects/{projectId}:render was unauthenticated -- anyone could
trigger a render. It now requires an approved.flag in the project directory and
returns 409 otherwise (config video-clipping.editing.require-render-approval,
default true). This closes the "no check at all" hole; it is a basic presence gate,
not yet authenticated/digest-bound authorization (a remaining hardening item).
New SpringBootTest asserts 409 without approval; the delegation unit test disables
the gate. mvn verify 271/0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
- .github/workflows/ci.yml: build + test on every push/PR (JDK 21 + ffmpeg,
mvn -B verify, uploads surefire reports). Closes the "no CI" gap.
- README.md: honest overview of the local offline highlight pipeline, the R1-R9
cinematic quality rules, how to build/test/run, the local models and their
(non-commercial) licenses, constraints, and limitations. Explicitly states it is
a PoC, not production-hardened. Closes the "no README" gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
A slow-motion shot now decelerates smoothly instead of snapping to slow-mo:
speedRampSetpts builds a log-integrated setpts that ramps playback speed from
normal (1.0) down to below the target across the shot. The shot stays a SINGLE
segment (so the R5 per-shot push-in is preserved) and the existing -t pin keeps
the planned output duration. Normal-speed shots keep a plain constant setpts.
Unit-tested; verified in a real render (payoff carries the ramp, output valid).
Completes R6 (crossfades + speed-ramp).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
R6 ran the video xfade and audio acrossfade in one filtergraph on the same inputs,
which starved the audio path and truncated it (final audio 4.2s vs 9.7s video) --
so the music appeared to end early. Split into two passes (xfadeVideoCommand video
only, acrossfadeAudioCommand audio only) and mux them. Verified: final audio now
9.8s matching the 9.67s video, music present through the end. Isolation and unit
tests confirm the two chains each produce the full compressed length.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
MusicGen's internal structure is uncontrolled, so the mix applies a deterministic
swell envelope to the score (volume='min(1,0.5+0.5*t/peak)':eval=frame): the music
amplitude rises from 0.5x to full over the run-up to the payoff, then holds. The
peak is the payoff (slow-motion) beat's timeline midpoint on the crossfade-
compressed timeline -- generic, driven only by the plan, no content assumptions.
No-ops when there is no slow-mo beat. Filter string unit-tested; verified in a real
render (swell peaks at 6.83s, output -16.1 LUFS).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
The vision director now captions several beat frames (two questions per frame in
one worker call: a discriminative description + a punchy label) and turns the
descriptions into a per-window "highlight-worthiness" curve via generic
emotion/action/idle keyword scoring (semanticScore/semanticCurve). The montage
director blends that curve with audio to place the payoff on the semantically
strongest moment; the payoff label becomes the bold overlay and the description
flavors the music. Everything is content-agnostic and fails soft to the measured
cut. Verified on bowling: the payoff moved onto moondream's detected celebration.
Honest limit: a small VLM on distant subjects is only weakly discriminative;
descriptive questions beat terse ones (which collapse to a constant answer).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
Overlays are now large (fontsize 84) with a thick outline + strong drop shadow so
they read on any background, and animate in: a snappy 0.18s alpha punch plus a
34px rise-up over 0.22s, with a soft ease-out. Placed on the payoff beat so the
entrance lands on the musical/edit accent. This presents the Tier-2 vision
director semantic caption ("STRIKE") boldly. Overlay style/animation asserted in
the existing overlay test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
Beats now blend instead of hard-cutting (incl. the cut into the slow-mo payoff)
via an xfade + acrossfade chain (xfadeTimelineCommand). The timeline compresses
by (n-1)*xf, so shiftOverlayForCrossfade re-times overlays and the reported
duration is reduced to keep overlays, loudness mastering, and QA aligned. Opt-in
via editing.crossfade-seconds (0 = hard cuts default; localpoc 0.25), clamped to
half the shortest beat. Offset math + overlay shift unit-tested; verified on the
bowling cut (visible dissolve, STRIKE overlay stayed on the payoff).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
Single-pass loudnorm in the mix is only ~+/-2 LUFS accurate, so the auto-rendered
output could land quiet (e.g. -18.7 LUFS vs the -16 target). After the mix,
probeIntegratedLoudness measures the file, loudnessGainDb computes the corrective
gain, and masterLoudness applies it with a brickwall limiter for true peak. No-ops
when already on target or when the measurement is implausible; handles MusicGen
loudness variance. loudnessGainDb unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
Rendering (whole service, driven by source measurements, not constants):
- Orientation-aware geometry: FfmpegClipInspector reads display rotation and
stores effective dims; HighlightFfmpegRenderer.outputGeometry renders portrait
sources portrait and skips the 2.39 letterbox on portrait (landscape unchanged).
- Dynamic exposure: probeSourceLuma measures the frames; exposureNormalizationFilter
maps the mean toward a target; the grade is now exposure-preserving (no crushed
subjects: bowling final went ~63 -> ~100 mean luma).
- Motion-adaptive in-shot push-in (zoompan), amount from per-shot YDIF.
- Audio mix ducks source audio under the generated score so it leads.
- Highlight duration is no longer capped (validator + config).
Automatic director (plans were hand-authored before):
- Tier 1 HighlightMontageDirector: composes the montage from measured motion (YDIF)
and audio-energy (RMS) curves -- setup, continuous action/tension, slow-mo payoff
on the audio climax, resolution button, camera-whip tail trimmed.
- Tier 2 HighlightVisionDirector + tools/vision_caption.py: a local, offline
vision-language model (moondream2) captions the payoff frame and augments the
montage with a semantic overlay ("STRIKE") and scene-informed music; fails soft.
- Wired into the scheduler behind auto-director-enabled / vision-director-enabled
(on in the localpoc profile).
Docs: cinematic-quality-rules.md (R1-R5, R9 both tiers), poc-plan milestones.
Tests: mvn -o verify -> 262 passing, 0 failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MPuJXQyAeWpFcTtcnxo1UN
A montage edits many short shots pulled from ANYWHERE in the source (not a few
fixed contiguous highlight windows), sequenced establishing -> quick detail cuts ->
slow-motion hero, over one continuous music bed -- much closer to how a car film is
actually cut.
- MontagePlan model + director/montage.json shot list (per-shot source time,
duration, punch-in zoom, speed).
- HighlightDirectorFlowService.processMontage: builds one EditPlan from the shot
list (bypassing the highlight-window validator), one continuous music cue,
distributed voiceover, and titles; reuses asset prep + worker + renderer.
- Renderer: explicit per-shot framing via a "zoom=" token (falls back to the
progressive punch-in); pin each segment to its exact target duration with -t so
frame-quantization drift cannot accumulate across many short shots.
Verified on the DJI source: 18 shots, 16.7 s, -16.5 LUFS, TP -2.8 dBTP, QA green.
The edit now reads as a real montage (varied framing + detail cuts + hero) rather
than slow pans. mvn -o verify green (248). Remaining ceiling is the source footage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Turn each beat from one long pan into an edit:
- Flow splits every beat into contiguous cuts (opening 2, rising 3, hero 2) with
hard cuts between and sequential timeline positions; playback speed is preserved
so the hero stays slow-motion across its cuts.
- Renderer frames each cut with a tighter center crop that fills the frame and
hides the mundane location, plus a progressive punch-in (each successive cut of a
beat steps tighter), so the sequence reads as deliberate.
- Widen the duration QA tolerance to the frozen acceptance value (0.25 s): multi-cut
and slow-motion accumulate small per-segment frame-quantization drift.
Verified: 7 cuts (2/3/2), final 27.4 s, TP -2.7 dBTP. mvn -o verify green (248).
Known follow-up: integrated loudness runs low (~-18.7 LUFS) on the sparse music-bed
mix; single-pass loudnorm undershoots -- needs two-pass or a louder bed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Add afade in (0.12s) / out (0.25s) to each narration line so it eases in and out
instead of hard-cutting -- the hard cut read as un-cinematic. Voice remains
present and leading (measured onset ramp -18 -> -17 dB).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Root cause of "no voiceover": in audioMixCommand the [voice] label was used both
as the sidechaincompress key AND as an amix input. FFmpeg does NOT auto-split a
reused label, so the ducking sidechain consumed the voice entirely and the amix
reference got no audio -- the narration never made it into any final mix (every
render played ducked music with a silent hole where the voice should be).
Fix: explicitly `asplit=2[voice_key][voice_mix]` so one copy keys the duck and one
copy stays in the mix. Measured: voice window went from ~-30..-53 dB (silent) to
~-16 dB (present, leading the bed). Revert the compensating +8 dB over-boost back
to unity; music bed lowered to -14 dB so narration leads cleanly.
Also make SFX direction OPTIONAL in the validator (matching the earlier
voiceover-optional change) so a clean music+narration edit with no sound effects
validates. Regression test added for the asplit. mvn -o verify green (248 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Push the highlight output toward a genuinely cinematic look after a human review
found it functional-but-not-cinematic:
- HighlightFfmpegRenderer: add a 2.39:1 letterbox and subtle film grain to each
graded segment; raise overlay placement above the letterbox bar. (A time-based
crop-zoom push-in was prototyped and removed: FFmpeg crop cannot use the `t`
variable for width/height; a zoompan push-in is a possible follow-up.)
- HighlightDirectorPlanValidator: make voiceover OPTIONAL so a music-driven edit
can carry no narration (lines still validated when present).
Combined with a no-voiceover, driving-orchestral, slow-motion-hero director plan,
this yields a letterboxed, richly graded, music-led cinematic cut. mvn -o verify
green (247 tests, 0 failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
The local-cv provider was never exercised end to end (its bootstrap was
prohibited), and hid a latent bug: the JDK HttpClient defaulted to HTTP/2 and
negotiated an h2c cleartext upgrade that the HTTP/1.1-only worker (uvicorn/h11)
mishandled by dropping the request body, so every call returned HTTP 422. Pin the
client to HTTP/1.1.
With this fix the resident YOLOv8 worker (run offline against the existing
yolov8n.pt, no bootstrap script) classifies the sample source as CAR_VLOG at 0.95
with measured OpenCV blur/exposure and a real car label, replacing the previous
filename-keyword GENERIC_VLOG fallback. Provider remains opt-in via runtime
override; the committed localpoc profile keeps the heuristic default.
Also ignore yolov*.pt.license.txt (provenance for the git-ignored weights).
mvn -o verify green (247 tests, 0 failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
The highlight renderer now selects a filmic grade per story beat, read from the
EditPlan style key (safeKey: style_<category>_<storyPurpose>), so no change to the
shared EditDecision or its serialization is needed:
- opening_hook: calmer, cooler, softer contrast (linear_contrast, gentle vignette)
- rising_energy / default: balanced base grade (medium_contrast)
- hero_payoff: richest, warmer, stronger S-curve and deeper vignette (strong_contrast)
Threads plan.style() into segmentCommand -> cinematicVisualFilter. Multi-clip
FfmpegEditRenderer untouched. Test added; mvn -o verify green (247 tests, 0 failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Replace the plain hard-cut white caption with a refined 48px caption, a soft drop
shadow, a thin subtle border, and a smooth alpha fade in and out (0.4s ramps)
within each overlay timeline window. Highlight renderer only; the multi-clip
renderer and overlay placement/safe-area logic are unchanged. Test added;
mvn -o verify green (246 tests, 0 failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Record the operator-approved first render and the verified grade + true-peak
limiter results (TP -1.7/-2.7/-2.8 dBFS, I -16.3 LUFS) with commit references.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Phase 3 quality fixes for the highlight renderer (multi-clip FfmpegEditRenderer
left untouched):
- cinematicVisualFilter: replace the weak fixed eq with a deliberate filmic grade
(curves medium_contrast S-curve + teal-orange colorbalance + eq + unsharp +
vignette). Richer blue, warm highlights, tonal contrast. Still one uniform look;
beat/category-specific grading is future work (needs a validated grade enum).
- audio mix: add a brickwall limiter (alimiter limit=0.72) after loudnorm. The
first render clipped at 0.0 dBFS true peak; measured re-render now lands
-1.7/-2.7/-2.8 dBFS per highlight (all within the -1.5 dBTP gate), integrated
loudness -16.3 LUFS.
Test updated; mvn -o verify green (245 tests, 0 failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Activated with --spring.profiles.active=localpoc. Points only at pre-provisioned
local model paths (Piper voice, MusicGen, AudioLDM2), disables bootstrap
auto-start, uses heuristic visual analysis, isolates PoC input/output dirs, and
keeps render disabled + director approval required. Base/production defaults are
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR
Replace the unusable audiocraft path (requires xformers, which has no Intel-Mac
build) with runtimes proven to work offline on this machine:
- music: transformers MusicGen (facebook/musicgen-small)
- sfx: diffusers AudioLDM2 (cvssp/audioldm2), resampled 16k -> 48k
- voiceover: Piper (unchanged), normalized to 48 kHz mono
The worker CLI contract and exit codes are preserved, so the Java
LocalAssetSynthesizer license gate and fail-closed behavior are unchanged.
Add tools/provision_local_models.py to materialize models into models/ from the
local HF cache with no network. Models and their license sidecars live under the
git-ignored models/ dir; both audio models are non-commercial (CC-BY-NC-4.0 /
CC-BY-NC-SA-4.0), recorded for later production review.
Add docs/cinematic-highlight-poc-plan.md tracking the PoC plan and milestones.
mvn -o verify: 245 tests, 0 failures/errors/skips (unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bg76sLc43Wc3j5ZcLkboYR