Skip to content

fix: composition contract repairs found by an end-to-end validation run - #518

Open
farhadganji371 wants to merge 4 commits into
OpenMontageInc:mainfrom
farhadganji371:openmontage-hardening
Open

farhadganji371 wants to merge 4 commits into
OpenMontageInc:mainfrom
farhadganji371:openmontage-hardening

Conversation

@farhadganji371

Copy link
Copy Markdown

Summary

This PR hardens OpenMontage's Remotion composition and video rendering contracts based on defects found during a full end-to-end production validation run.

The changes focus on compatibility, validation, subtitle rendering, theme handling, and slideshow-risk analysis while preserving existing default behaviour where possible.

Changes

  • Improve light-theme compatibility across Remotion components.
  • Add exact KPI formatting controls, including decimal precision and optional abbreviation.
  • Add early validation for invalid overlay timing and unsupported overlay types.
  • Add governed templated composition provenance through composition_props_path.
  • Repair canonical subtitle schema → ASS/libass rendering behaviour.
  • Support canonical subtitle font, size, colour, outline, background and position settings.
  • Scale subtitle font size correctly against ASS PlayRes.
  • Add canonical subtitles.vertical_margin with video-space pixel conversion to ASS MarginV.
  • Preserve explicit native/ASS subtitle overrides as highest precedence.
  • Restore slideshow-risk analysis against the canonical scene_plan.scenes representation.
  • Keep slideshow-risk enforcement report-only by default.
  • Add opt-in themeConfigWins precedence without changing legacy preset behaviour.
  • Add regression fixtures, theme goldens, contract documentation and engineering provenance records.
  • Correct stale subtitle MarginV guidance in the talking-head compose director.

Compatibility notes

  • Canonical subtitle fields that were previously accepted but effectively ignored now correctly affect rendered captions.
  • Existing explicit native/ASS subtitle overrides retain precedence.
  • Slideshow-risk enforcement remains report_only by default. Automatic blocking is not enabled.
  • themeConfigWins remains opt-in. Existing preset precedence remains the default.
  • Artifact schema additions are optional and existing artifact schema versions are unchanged.
  • No protected generated media or validation-only project files are included in the PR.

Testing

Validated locally after the complete change set:

  • 1553 passed
  • 12 skipped
  • 0 failed
  • TypeScript/typecheck: PASS
  • Artifact schemas: 29/29 valid
  • git diff --check: clean
  • Dark-theme compatibility goldens preserved.
  • Protected regression video baselines remained byte-identical.
  • Clean-clone test independence verified for the new slideshow-risk fixtures.
  • Optional validation-only demo props correctly skip when absent.
  • Validation used no paid APIs or external generation services.

Committed component/tool versions:

  • video_compose: 0.2.0
  • composition_validator: 0.2.0
  • remotion-composer: 1.1.0

Checklist

  • The change is focused on a single logical concern: composition/rendering contract hardening identified through end-to-end validation.
  • I ran the relevant tests locally.
  • I updated documentation where behaviour or usage changed.
  • No unrelated build artifacts or local configuration are included in the diff.

Farhad Ganji and others added 4 commits August 19, 2026 17:08
Post-validation Revision 1. An end-to-end zero-cost production surfaced
several component and validation defects; each is fixed conservatively so
existing dark-theme output is byte-identical.

- StatCard: Explainer never forwarded a text colour, so the subtitle fell
  back to #FFFFFF and was unreadable on light playbooks (E1).
- KPIGrid: add optional `decimals` / `abbreviate`; defaults reproduce the
  previous rounded/abbreviated output exactly (E2).
- HeroTitle / SectionTitle / StatReveal: accept optional theme colours,
  defaulting to today's constants. Explainer forwards theme-derived values
  only when the resolved surface is light, so dark themes are untouched (E3).
- ComparisonCard: forward cut.leftColor / cut.rightColor when supplied;
  component defaults are unchanged when they are not (E4).
- composition_validator: reject overlay timing errors before Remotion
  renders, naming in_seconds/out_seconds instead of failing mid-render with
  a NaN Sequence error (E7). Bumped to 0.2.0.
- edit_decisions: add the optional `templated` provenance block so a
  templated run records which composition props it rendered, without
  duplicating Remotion scene props into cuts (E8).

Tests render real frames via the Remotion CLI and measure them with Pillow;
no JS test runner or new production dependency is introduced. Dark-theme
goldens captured before the fix are committed as compatibility evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tical margin

Post-validation Revisions 2A and 2C. Canonical subtitle settings on
edit_decisions.subtitles were largely inert: the renderer spoke a different
vocabulary, so most declarations were silently dropped.

Fixed:
- `subtitles.style` is schema-typed as a display-style STRING, but the
  resolver called .items() on it, raising AttributeError deep inside compose
  for any schema-valid artifact. Strings are now recorded as a display style;
  an unusable type fails with a clear message instead of crashing.
- Canonical `color`, `outline_color`, `background` and `position` now reach
  the renderer as PrimaryColour / OutlineColour / BackColour / Alignment.
- CSS colours are converted to ASS &HAABBGGRR (blue-green-red order, with
  alpha inverted from opacity to transparency); ASS values pass through.
- Font size is converted against the real libass canvas. ffmpeg renders SRT
  on a 384x288 script canvas (verified from its own generated header), so a
  raw pass-through made a requested 42px render ~3.75x too large. Sizes are
  now expressed in video pixels and scaled by the probed output height.

Added:
- Optional `subtitles.vertical_margin` (integer, video-space pixels, the
  same contract as video_stitch.pip_margin), converted to ASS MarginV. This
  removes the last reason to reach for a raw ASS override: the governed
  contract alone now reproduces the intended caption placement.

Precedence is defaults -> playbook -> canonical fields -> explicit native
ASS override, so a caller that already speaks ASS is never overwritten by a
higher-level field. Omitting every canonical field reproduces the previous
force_style byte for byte.

Revisions 2A and 2C are committed together because their changes interleave
line-by-line inside _build_subtitle_style and the resolver defaults; the
per-revision history is preserved in engineering/post-validation-2026-08-19/.

Behavioural note: projects that declared canonical subtitle fields, or that
relied on a style playbook while burning subtitles, will see captions change
because those declarations finally take effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Post-validation Revision 2B. The slideshow-risk gate had never actually run
in production.

_pre_compose_validation did `scenes = scene_plan or []`, but its only caller
passes the scene_plan ARTIFACT ({version, style_playbook, scenes[], ...}).
Iterating that dict yields its string keys, so every scoring dimension raised
"'str' object has no attribute 'get'" and a bare `except` swallowed the
failure as a one-line log warning.

- Normalise the input at the boundary: the artifact dict, a bare scenes list
  and None are all accepted; anything else is reported rather than iterated.
- Make failures visible: the exception type, message and the scene
  representation used are surfaced as a structured warning and recorded on
  VideoCompose.last_slideshow_risk. No more silent skips.
- Run REPORT-ONLY by default. A `fail` verdict warns and does not stop
  compose. The original blocking branch is preserved and re-enabled only by
  OPENMONTAGE_SLIDESHOW_RISK_ENFORCEMENT=block, following the existing
  OPENMONTAGE_* convention. Blocking is NOT enabled by this change.

Analysis recorded in engineering/post-validation-2026-08-19/E6_ANALYSIS.json:
the same production scores 0.67/strong from its scene_plan but 3.42/revise
from cuts, because edit_decisions.cuts cannot carry shot_intent, roles,
hero_moment or shot_language (additionalProperties: false). scene_plan.scenes
is therefore the canonical representation; the cuts fallback is diagnostic
only and must not drive enforcement.

Tests read committed fixtures under tests/fixtures/slideshow_risk/ rather
than the gitignored validation project, so they pass in a clean clone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Post-validation Revision 2D plus consolidation of Revisions 1-2D.

Theme precedence (E10): resolveTheme returned a built-in preset as soon as
theme/playbook matched one, discarding an explicit themeConfig. That
contradicts how video_compose builds a themeConfig from the playbook's own
palette ("not picked from a preset menu"), so a preset-named playbook could
never apply its YAML colours.

- Add the optional composition prop `themeConfigWins`. When set, resolution
  becomes DEFAULT_THEME -> named preset -> themeConfig, merged field by
  field so a partial config refines a preset instead of blanking it.
- Legacy precedence remains the DEFAULT: omit the flag and a recognised
  preset still wins. Verified byte-identical across all four built-in
  presets (16/16 golden frames), and no shipped demo would change.

Consolidation:
- video_compose 0.1.0 -> 0.2.0, remotion-composer 1.0.0 -> 1.1.0. Artifact
  schema versions are unchanged; every schema addition is optional.
- Add CHANGELOG.md and docs/COMPOSITION_CONTRACTS.md covering the subtitle
  contract, theme precedence and slideshow-risk behaviour.
- Correct stale guidance in the talking-head compose director that treated
  ASS MarginV as frame pixels; on a 1920-high frame MarginV=160 is roughly
  1067px from the edge, i.e. mid-frame.
- Retain the small text/JSON engineering evidence (freeze records, analyses,
  checksums, provenance) plus FOLLOW_UPS.md. Generated binary evidence and
  the local validation composition props stay untracked; their SHA-256s and
  provenance are recorded in BASELINE.json.

Not enabled by default: slideshow-risk blocking, and themeConfig precedence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oujinhaoai

Copy link
Copy Markdown

Independent reproduction on a different machine — confirms the diagnosis in this PR.

  • Repo: calesthio/OpenMontage @ 08e2151
  • OS: macOS 15 (Darwin 25.6.0), Mac mini M4
  • Python 3.10, Node v26.7.0
  • Pipeline: animated-explainer, runtime: remotion, playbook: flat-motion-graphics

Confirming the need for "Restore slideshow-risk analysis against the canonical scene_plan.scenes representation" — on main the gate is dead on the documented call path.

Passing the full scene_plan artifact to video_compose(operation="render"), as skills/pipelines/explainer/compose-director.md implies, produces:

Could not compute slideshow risk: 'str' object has no attribute 'get'

video_compose.py:1455 does scenes = scene_plan or [], so iterating the artifact dict yields string keys and score_slideshow_risk raises immediately; the surrounding except Exception downgrades it to a single log.warning and the render proceeds with no risk assessment. Nothing in final_review records that the gate did not run.

Passing scene_plan["scenes"] manually makes it work — it returned {'average': 0.67, 'verdict': 'strong'} for the same composition. So the gate only functions when you omit scene_plan and let it fall back to deriving scenes from cuts, or when you pass the inner list.

Worth considering alongside this PR: surfacing the swallowed exception into final_review.issues_found, so a dead gate is visible rather than silent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants