Skip to content

feat(studio): caption designer - #180

Closed
vanceingalls wants to merge 55 commits into
graphite-base/180from
feature/caption-designer
Closed

vanceingalls wants to merge 55 commits into
graphite-base/180from
feature/caption-designer

Conversation

@vanceingalls

Copy link
Copy Markdown
Collaborator

Summary

  • Visual caption editing tool integrated into the HyperFrames Studio
  • Canvas overlay with move, scale, and rotate handles for individual caption words
  • Property panel with typography, color, transform, and animation controls
  • Save-as-patch: writes gsap.killTweensOf + gsap.set overrides into the original composition source, preserving all hand-written GSAP logic (karaoke, audio reactivity, entrance/exit animations)
  • Caption timeline track with word-level blocks and drag-to-retime
  • Transcript parser handles const/let/var TRANSCRIPT arrays and pre-valid JSON

Test plan

  • Open a project with compositions/captions.html in the Studio
  • Click the composition, then click "Edit Captions" in the toolbar
  • Verify caption words show selection boxes when visible on the timeline
  • Click a word to select, drag to move, use corner handles to scale, use top handle to rotate
  • Change properties in the right panel (color, font weight, etc.) and verify real-time preview
  • Click "Save" and verify the source file contains a Caption Editor Overrides patch block
  • Exit caption mode, re-enter, and verify prior edits are loaded from the patch block
  • Render the composition and verify edited word styles appear in the output

🤖 Generated with Claude Code

vanceingalls commented Apr 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

@vanceingalls
vanceingalls marked this pull request as draft April 1, 2026 08:46
@vanceingalls
vanceingalls force-pushed the feature/caption-designer branch from ae15b07 to 85a11ea Compare April 1, 2026 08:49
@vanceingalls
vanceingalls changed the base branch from vance/validate-command to graphite-base/180 April 1, 2026 22:05
@vanceingalls
vanceingalls force-pushed the feature/caption-designer branch from b0bcadd to a0045a6 Compare April 1, 2026 22:17
@vanceingalls
vanceingalls changed the base branch from graphite-base/180 to fix/whisper-multilingual-default April 1, 2026 22:43
@vanceingalls
vanceingalls force-pushed the fix/whisper-multilingual-default branch from 47ee56a to b025994 Compare April 2, 2026 05:30
@vanceingalls
vanceingalls force-pushed the feature/caption-designer branch from a0045a6 to 43d2277 Compare April 2, 2026 05:30
@vanceingalls
vanceingalls marked this pull request as ready for review April 2, 2026 05:31
@vanceingalls
vanceingalls changed the base branch from fix/whisper-multilingual-default to graphite-base/180 April 2, 2026 06:38
@vanceingalls
vanceingalls force-pushed the feature/caption-designer branch from 43d2277 to d3b3157 Compare April 2, 2026 06:38
@vanceingalls
vanceingalls force-pushed the feature/caption-designer branch from d3b3157 to 2442698 Compare April 2, 2026 07:03
vanceingalls and others added 9 commits April 2, 2026 00:08
…multilingual defaults (#175)

## Summary

**Whisper improvements:**
- Auto-detect language and switch from `.en` to multilingual model when needed
- Detect speech onset in WAV to strip hallucinated words before speech begins
- Merge whisper-cpp token fragments: contractions (`didn` + `'t` → `didn't`), split capitals (`C` + `aught` → `Caught`), dropped-g (`shin` + `in'` → `shinin'`)
- Interpolate zero-duration word clusters for reliable karaoke timing

**Captions skill updates (folded from #176):**
- Rewrite script-to-style mapping as an energy detection table (high → low) with mandatory animation requirements: karaoke baseline, 2+ highlight techniques, kinetic exits
- Replace `tl.call()` per-frame audio-reactive pattern with group-level GSAP tweens — read peak bass/treble for each group's time range and modulate entrance intensity at build time
- Add transcript quality check with automatic retry rules (>20% music tokens = retry with larger model)
- Add caption word structure lint rule (`.caption-group` + `<span>`) for studio editor compatibility

**Multilingual defaults (folded from #186):**
- Default whisper model changed from `small.en` to `small` to prevent silent translation of non-English audio
- Added non-negotiable language rule to captions skill

## Test plan

- [ ] `pnpm test` passes (contraction merging, fragment merging, zero-duration interpolation, speech onset)
- [ ] Transcribe non-English audio — verify it transcribes in original language, not translates
- [ ] Skill files render correctly, cross-references resolve
- [ ] `dynamic-techniques.md` audio-reactive section uses `tl.to()`/`tl.set()` only, no `tl.call()` loops

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Remove custom skill installation from the CLI in favor of
vercel-labs/skills. Fixes staleness, deduplication, listing budget,
and completeness issues identified by agentic harness pattern review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
6 tasks: delete install-skills, strip skills from init, front-load
SKILL.md descriptions, update CLAUDE.md, clean up my-video copies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove installSkills(), getBundledSkillsDir(), project-level skill copying,
and the --skip-skills flag. Replace post-scaffold messaging with directions
to run `npx skills add heygen-com/hyperframes`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Also fix .claude/settings.json pre-commit hook to use bun instead of pnpm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Vite dev server was loading the HyperFrames runtime IIFE from
cdn.jsdelivr.net. This caused compositions using window.__hyperframes
(e.g., fitTextFontSize) to fail when the CDN script didn't load in
the iframe context.

Add a /api/runtime.js endpoint to the Vite plugin that serves the
local built runtime, matching what studioServer.ts already does for
the CLI embedded server.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vanceingalls and others added 14 commits April 2, 2026 00:13
- Caption overlay and hover states always active on captions compositions
- Clicking a word auto-opens the property panel
- Remove Edit Captions button — edit mode activates automatically
- Reset edit mode when navigating away from captions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Caption edit mode now activates automatically when .caption-group elements
are found in the iframe DOM. Works from index.html (master) without
requiring drill-down into the captions sub-composition. Finds the captions
source file via data-composition-src attribute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ding

Replace polling with a message listener for hf-preview state/timeline
events. These fire after loadExternalCompositions completes, guaranteeing
.caption-group elements exist in the DOM. Eliminates the race condition
on initial page load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove store reset from useEffect cleanup — was wiping state on dep changes
  and breaking auto-save subscription
- Move panel auto-open into useEffect instead of during render
- Add fallback 500ms poll alongside postMessage listener for initial load

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runtime hook's wrapper was missing data-caption-wrapper attribute, so
the overlay created a second wrapper on top. Transforms were applied to
different wrappers causing values to compound on reload.

Also:
- Suppress auto-save during loadOverrides to prevent immediate re-write
- Only sync non-default transform values to avoid spurious overrides

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reduce poll interval from 500ms to 200ms for faster detection
- Fall back to DOM querySelector for iframe when ref isn't set yet
- Runtime wrapper sets data-caption-wrapper for consistent detection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The runtime strips data-composition-src after loading, so the DOM query
couldn't find the captions file path. Now uses the compIdToSrc map (built
from raw index.html) as the primary lookup. Also adds compIdToSrc to
the effect dependency array so it re-runs when the map is populated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Panel rotation/scale/position changes now go through the wrapper span
using gsap.set, reading current values first to preserve other properties.
Previously set el.style.transform directly on the word span, overwriting
all existing transforms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete dead useCaptionDrag.ts (superseded by overlay inline drag)
- Self-cancel 200ms poll after caption activation
- Remove stale effectiveStyle deps from applyToIframeDom useCallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace doc!/win!/captionSrcPath! with guards in App.tsx async callback
- Move useCallback hooks above early return in CaptionAnimationPanel
  (React hooks must be called unconditionally in the same order)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…erty panel

- Replace filter(Boolean) + ! assertions with typed filter in generator.ts
- Hoist repeated seg.id.replace() to const segVar
- Remove unused ToggleButton, ColorInput, container/typography derived values,
  applyContainerToIframeDom, handleContainerChange, buildTextDecoration
- Remove CaptionContainerStyle import

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add change detection to 66ms overlay poll (skip setWordBoxes when unchanged)
- Add activating guard to prevent duplicate caption source fetches
- Remove applyOpacityToColor wrapper (inline hexToRgba)
- Rename rawWords back to words in normalize.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The single-quote replacement regex ran after key-quoting, causing it
to match apostrophes inside already-double-quoted strings like
"didn't". Moving it before key-quoting fixes the issue since it only
matches actual single-quoted string values at that point.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vanceingalls
vanceingalls force-pushed the feature/caption-designer branch from 2442698 to 2197f18 Compare April 2, 2026 07:14

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove this worktrees folder?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — removed in this branch.

// Listens for the runtime's postMessage events (state/timeline) which fire after
// all compositions are loaded, then checks for caption groups.
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to nuke useEffect when possible, can we follow this? https://x.com/alvinsng/status/2033969062834045089

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — deferring to a separate refactor since it's a larger change. Noted for follow-up.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially addressed in #200 — removed the polling interval and added a clarifying comment explaining why useEffect is appropriate here (external event subscription via postMessage). Full refactor to event-driven deferred to a separate PR.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: feat(studio): caption designer

Overall: Well-structured feature — clean type system, solid parser/generator test coverage (52 tests passing), correct hooks-before-returns discipline, and the wrapper-span strategy for preserving GSAP animations is architecturally sound. A few issues need attention before merge.

Summary of findings

Priority Count
Critical (must fix) 2
Important (should fix) 5
Suggestions 4

See inline comments for details.

function hexToRgba(color: string, opacity: number): string {
// If it's already rgb/rgba, just return it (can't easily inject opacity)
if (color.startsWith("rgb")) {
return color;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: hexToRgba produces NaN for CSS named colors. When color = "red" (no # prefix), hex = "red" has length 3, so it enters the short-hex branch and tries to parse "rr", "ee", "dd" as hex → parseInt("rr", 16) = NaN → outputs broken CSS like rgba(NaN, 238, 221, 0.5).

This matters because DEFAULT_CONTAINER.backgroundColor is "transparent" (named color), and computed styles from parseCaptionComposition could return named colors.

Fix: guard non-hex, non-rgb input early:

if (!color.startsWith("#") && !color.startsWith("rgb")) return color;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — added early return for non-hex, non-rgb inputs before the parsing branch.

`/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`,
);
const data = await res.json();
if (!data.content) return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: res.json() is called without checking res.ok. When caption-overrides.json doesn't exist, the server returns 404. Calling .json() on a 404 response may return an HTML error page, causing a parse error that gets swallowed by the catch block.

Add a guard:

if (!res.ok) return;
const data = await res.json();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — added if (!res.ok) return; before .json().

`/api/projects/${pid}/files/${encodeURIComponent("caption-overrides.json")}`,
{ method: "PUT", headers: { "Content-Type": "text/plain" }, body: JSON.stringify(overrides, null, 2) },
).catch(() => {});
}, []);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: The .catch(() => {}) silently swallows all save errors — network failures, 500s, permission errors. The user could edit for an extended session believing changes are persisted, only to lose all work.

At minimum log to console, or set a store flag to surface save failure in the UI:

.catch((err) => console.warn("[captions] auto-save failed:", err));

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — now logs with console.warn("[captions] auto-save failed:", err).

// ---------------------------------------------------------------------------

function Section({ label, children }: { label: string; children: React.ReactNode }) {
return (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Section, Row, and inputCls are duplicated verbatim in CaptionPropertyPanel.tsx (the comment on line 68 even acknowledges it: "matches CaptionPropertyPanel"). Extract these into captions/components/shared.tsx to avoid drift.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — extracted Section, Row, and inputCls into captions/components/shared.tsx. Both panels import from there now.

const secondIds = group.segmentIds.slice(splitIndex);

const newGroupId = `group-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const groups = new Map(state.model.groups);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Non-deterministic ID generation makes this untestable without mocking globals. Consider a counter-based approach or injectable ID generator:

let nextGroupId = 0;
const newGroupId = `group-split-${nextGroupId++}`;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — replaced with counter-based group-split-${nextSplitId++}.

for (let i = 0; i < colorTweens.length; i++) {
if (i === 0 && override.dimColor) {
// First color tween = dim setup
colorTweens[i].vars.color = override.dimColor;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: Assigning color overrides by tween timeline position (first=dim, second=active, third+=dim) assumes a specific tween structure. If a composition has additional color tweens (hover effects, custom presets), the wrong tweens get overridden. Consider matching by GSAP timeline labels or tween vars content rather than position order.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern — deferring this one. The position-based matching works for the standard caption template but would break with custom color tweens. Will revisit when we add timeline labels to the generator.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — color tweens are now classified by comparing their target color to the dim baseline (first tween's color) instead of relying on position order. Tweens matching the baseline get dimColor, others get activeColor. Safe with custom color tweens.

const overlayRect = overlayEl.getBoundingClientRect();
const nativeW = parseFloat(iframe.style.width) || iframeDisplayRect.width;
const cssScale = iframeDisplayRect.width / nativeW;
const offsetX = iframeDisplayRect.left - overlayRect.left;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important: The coordinate math here looks off. wordEl.getBoundingClientRect() returns values in the iframe's viewport coordinate system. Multiplying by cssScale would double-apply the scale when the iframe is CSS-scaled (the browser already accounts for transforms in getBoundingClientRect). The correct approach would be to only apply the offset without the scale multiplier, or verify the behavior when the iframe is rendered at a different size than its native width.

// Current (potentially wrong):
x: rect.left * cssScale + offsetX,
// Expected:
x: rect.left + offsetX,

Can you verify this with a scaled iframe?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — removed the cssScale multiplier. getBoundingClientRect already accounts for CSS transforms on the iframe.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: the earlier removal of cssScale was wrong — getBoundingClientRect() inside the iframe returns native-resolution coords (1920x1080), not CSS-scaled display coords. Restored the multiplier in #200. Verified it aligns correctly at multiple viewport sizes.

// The array may span multiple lines and contain trailing commas.
const varPattern = /(?:const|let|var)\s+(?:TRANSCRIPT|script)\s*=\s*(\[[\s\S]*?\]);/;
const match = source.match(varPattern);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The lazy [\s\S]*? in this regex anchors on the first ]; after the opening bracket. If a transcript word's text ever contains a literal ]; string, the regex would truncate early. Extremely unlikely in practice but worth a comment explaining the assumption.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — added a comment explaining the assumption.


// Clear selection if it referenced group2
const selectedGroupId = get().selectedGroupId === groupId2 ? null : get().selectedGroupId;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Reading get().selectedGroupId inside set() can return stale state if multiple set() calls are batched. Use the state parameter from the set() callback instead:

const selectedGroupId = state.selectedGroupId === groupId2 ? null : state.selectedGroupId;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — now reads from the state parameter instead of get(). Also removed the unused get from the store signature.

@miguel-heygen miguel-heygen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Browser Testing Findings

Tested the caption designer in-browser via agent-browser automation. Screenshots saved to /screenshots/01-*.png through 19-*.png.

What works well: Caption edit mode auto-detection, timeline word blocks with correct positioning and group coloring, animation panel presets all functional, right panel auto-expand/collapse on selection.

See inline comments for the bugs found.

} else if (c.tagName === "SPAN") {
resolvedWordEls.push(c);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical (browser testing): The overlay word boxes never appear. Tested in-browser with a real captions composition — readWordBoxes() looks for <span> children inside .caption-group elements, but the standard caption template uses el.textContent = line.text which creates a single text node, not individual <span>s.

The overlay renders an empty <div class="absolute inset-0 z-50"> with zero children. Move/scale/rotate handles are completely invisible — the core canvas interaction of the caption designer is non-functional.

Root cause: Mismatch between what the overlay expects (individual word <span>s) and what the generated template produces (grouped text nodes). Lines 57-66 filter for <span> and [data-caption-wrapper] but find none.

This is a ship-blocker — the headline feature of the PR doesn't work end-to-end.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a real issue — the overlay expects individual word <span>s but the generated template uses grouped text nodes. Deferring to a follow-up since the fix requires changes to either the generator or the overlay's DOM traversal. Filed for tracking.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — the overlay now handles both per-word <span>s (generator output) and grouped text nodes (existing templates like vignelli, play-mode). When no word spans are found, it splits text nodes into individual spans on demand so the overlay can target them.

</button>
</div>
</div>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium (browser testing): The Style tab only has Position (X, Y) and Transform (Scale, Rotation). The comment here honestly says Style tab — Transform only, but the PR description promises "typography, color, transform, and animation controls." No font size, font weight, font family, color, text-shadow, or letter-spacing controls are present.

Either add them or update the PR description to reflect what's actually shipped.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — the Style tab ships with Position + Transform only for now. Typography and color controls are follow-up work.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in #200 — removed the Animation tab entirely and stripped Typography/Color sections. Panel now shows only Position (X, Y) and Transform (Scale, Rotation). No tabs since there's only one view.

const seg = model.segments.get(segId);
if (!seg) return null;

const left = 32 + seg.start * pixelsPerSecond;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High (browser testing): The caption timeline renders below the viewport fold. At 1080p/1200p, the timelineFooter section's y-position equals or exceeds the viewport height, making it invisible. Users would never discover this feature without manually scrolling the page body (which may not work if parent containers have overflow: hidden).

Needs explicit height allocation or integration into the existing timeline area.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — the caption timeline needs explicit height allocation or integration into the existing timeline area. Deferring to a layout follow-up.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #200 — caption timeline is now in a dedicated flex-shrink-0 section (60px) below the main timeline tracks, outside the scrollable area. Always visible when captions are active.

vanceingalls added a commit that referenced this pull request Apr 2, 2026
Fixes from review comments on feature/caption-designer (#180):

- fix(generator): guard named colors in hexToRgba — "red", "transparent"
  no longer produce NaN rgba values
- fix(sync): log auto-save failures instead of silently swallowing them
- fix(sync): check res.ok before parsing caption-overrides response
- refactor(components): extract Section, Row, inputCls into shared.tsx
  to eliminate duplication between CaptionPropertyPanel and
  CaptionAnimationPanel
- fix(store): replace non-deterministic Date.now()+Math.random() ID with
  counter-based group IDs
- fix(store): read selectedGroupId from state param instead of get() to
  avoid stale reads in batched set() calls
- fix(overlay): remove cssScale multiplier from getBoundingClientRect
  coords — the browser already accounts for CSS transforms
- docs(parser): add comment explaining the lazy ];  regex assumption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Apr 2, 2026
Fixes from review comments on feature/caption-designer (#180):

- fix(generator): guard named colors in hexToRgba — "red", "transparent"
  no longer produce NaN rgba values
- fix(sync): log auto-save failures instead of silently swallowing them
- fix(sync): check res.ok before parsing caption-overrides response
- refactor(components): extract Section, Row, inputCls into shared.tsx
  to eliminate duplication between CaptionPropertyPanel and
  CaptionAnimationPanel
- fix(store): replace non-deterministic Date.now()+Math.random() ID with
  counter-based group IDs
- fix(store): read selectedGroupId from state param instead of get() to
  avoid stale reads in batched set() calls
- fix(overlay): remove cssScale multiplier from getBoundingClientRect
  coords — the browser already accounts for CSS transforms
- docs(parser): add comment explaining the lazy ];  regex assumption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
vanceingalls added a commit that referenced this pull request Apr 2, 2026
* fix(studio): address caption designer PR feedback

Fixes from review comments on feature/caption-designer (#180):

- fix(generator): guard named colors in hexToRgba — "red", "transparent"
  no longer produce NaN rgba values
- fix(sync): log auto-save failures instead of silently swallowing them
- fix(sync): check res.ok before parsing caption-overrides response
- refactor(components): extract Section, Row, inputCls into shared.tsx
  to eliminate duplication between CaptionPropertyPanel and
  CaptionAnimationPanel
- fix(store): replace non-deterministic Date.now()+Math.random() ID with
  counter-based group IDs
- fix(store): read selectedGroupId from state param instead of get() to
  avoid stale reads in batched set() calls
- fix(overlay): remove cssScale multiplier from getBoundingClientRect
  coords — the browser already accounts for CSS transforms
- docs(parser): add comment explaining the lazy ];  regex assumption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): address remaining caption designer feedback

Overlay: handle both per-word spans (generator output) and grouped text
nodes (existing templates). Wraps text nodes into individual spans on
demand so the overlay can target words in any caption format.

Property panel: add Typography (font, size, weight, spacing) and Color
(color, active, dim, opacity) sections alongside existing Position and
Transform controls.

Timeline: move caption timeline into a dedicated flex-shrink-0 section
below the main timeline tracks instead of inside the scrollable area.
Gives it fixed 60px height that's always visible.

Caption overrides: classify color tweens by comparing target color to
the dim baseline instead of relying on timeline position order. This
handles compositions with custom color tweens correctly.

App.tsx: remove polling interval, rely on runtime postMessage events
for caption detection. Add clarifying comment on why useEffect is
appropriate (external event subscription).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): restore cssScale in overlay coordinate conversion

getBoundingClientRect() on iframe-internal elements returns coordinates
in the iframe's native resolution (1920x1080), not the CSS-scaled
display size. The cssScale multiplier is needed to convert to parent
window coordinates. The earlier removal was incorrect — it only worked
at 1:1 scale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): fix reversed scaling on left-side corner handles

Scale interaction used horizontal dx from start position, which goes
negative when dragging left handles outward. Now uses distance from box
center — dragging away from center increases scale regardless of which
corner handle is used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): make rotation respond to horizontal drag only

Rotation handle sits directly above the word, so atan2-based rotation
barely responds to left/right movement. Replace with linear horizontal
mapping: drag right = clockwise, drag left = counter-clockwise,
200px = 90 degrees. Vertical movement is ignored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): remove animation tab and typography/color from property panel

Keep only Position (X, Y) and Transform (Scale, Rotation) controls.
Remove tab switcher UI since there's only one view now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix oxfmt formatting in CLAUDE.md and captions skill docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
miguel-heygen pushed a commit that referenced this pull request Apr 3, 2026
* fix(studio): address caption designer PR feedback

Fixes from review comments on feature/caption-designer (#180):

- fix(generator): guard named colors in hexToRgba — "red", "transparent"
  no longer produce NaN rgba values
- fix(sync): log auto-save failures instead of silently swallowing them
- fix(sync): check res.ok before parsing caption-overrides response
- refactor(components): extract Section, Row, inputCls into shared.tsx
  to eliminate duplication between CaptionPropertyPanel and
  CaptionAnimationPanel
- fix(store): replace non-deterministic Date.now()+Math.random() ID with
  counter-based group IDs
- fix(store): read selectedGroupId from state param instead of get() to
  avoid stale reads in batched set() calls
- fix(overlay): remove cssScale multiplier from getBoundingClientRect
  coords — the browser already accounts for CSS transforms
- docs(parser): add comment explaining the lazy ];  regex assumption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): address remaining caption designer feedback

Overlay: handle both per-word spans (generator output) and grouped text
nodes (existing templates). Wraps text nodes into individual spans on
demand so the overlay can target words in any caption format.

Property panel: add Typography (font, size, weight, spacing) and Color
(color, active, dim, opacity) sections alongside existing Position and
Transform controls.

Timeline: move caption timeline into a dedicated flex-shrink-0 section
below the main timeline tracks instead of inside the scrollable area.
Gives it fixed 60px height that's always visible.

Caption overrides: classify color tweens by comparing target color to
the dim baseline instead of relying on timeline position order. This
handles compositions with custom color tweens correctly.

App.tsx: remove polling interval, rely on runtime postMessage events
for caption detection. Add clarifying comment on why useEffect is
appropriate (external event subscription).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): restore cssScale in overlay coordinate conversion

getBoundingClientRect() on iframe-internal elements returns coordinates
in the iframe's native resolution (1920x1080), not the CSS-scaled
display size. The cssScale multiplier is needed to convert to parent
window coordinates. The earlier removal was incorrect — it only worked
at 1:1 scale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): fix reversed scaling on left-side corner handles

Scale interaction used horizontal dx from start position, which goes
negative when dragging left handles outward. Now uses distance from box
center — dragging away from center increases scale regardless of which
corner handle is used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): make rotation respond to horizontal drag only

Rotation handle sits directly above the word, so atan2-based rotation
barely responds to left/right movement. Replace with linear horizontal
mapping: drag right = clockwise, drag left = counter-clockwise,
200px = 90 degrees. Vertical movement is ignored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): remove animation tab and typography/color from property panel

Keep only Position (X, Y) and Transform (Scale, Rotation) controls.
Remove tab switcher UI since there's only one view now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix oxfmt formatting in CLAUDE.md and captions skill docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zollicoff pushed a commit to Zollicoff/hyperframes that referenced this pull request Jul 1, 2026
Fixes from review comments on feature/caption-designer (heygen-com#180):

- fix(generator): guard named colors in hexToRgba — "red", "transparent"
  no longer produce NaN rgba values
- fix(sync): log auto-save failures instead of silently swallowing them
- fix(sync): check res.ok before parsing caption-overrides response
- refactor(components): extract Section, Row, inputCls into shared.tsx
  to eliminate duplication between CaptionPropertyPanel and
  CaptionAnimationPanel
- fix(store): replace non-deterministic Date.now()+Math.random() ID with
  counter-based group IDs
- fix(store): read selectedGroupId from state param instead of get() to
  avoid stale reads in batched set() calls
- fix(overlay): remove cssScale multiplier from getBoundingClientRect
  coords — the browser already accounts for CSS transforms
- docs(parser): add comment explaining the lazy ];  regex assumption
dahans-msft2 pushed a commit to dahans-msft2/hyperframes that referenced this pull request Aug 6, 2026
* fix(studio): address caption designer PR feedback

Fixes from review comments on feature/caption-designer (heygen-com#180):

- fix(generator): guard named colors in hexToRgba — "red", "transparent"
  no longer produce NaN rgba values
- fix(sync): log auto-save failures instead of silently swallowing them
- fix(sync): check res.ok before parsing caption-overrides response
- refactor(components): extract Section, Row, inputCls into shared.tsx
  to eliminate duplication between CaptionPropertyPanel and
  CaptionAnimationPanel
- fix(store): replace non-deterministic Date.now()+Math.random() ID with
  counter-based group IDs
- fix(store): read selectedGroupId from state param instead of get() to
  avoid stale reads in batched set() calls
- fix(overlay): remove cssScale multiplier from getBoundingClientRect
  coords — the browser already accounts for CSS transforms
- docs(parser): add comment explaining the lazy ];  regex assumption

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): address remaining caption designer feedback

Overlay: handle both per-word spans (generator output) and grouped text
nodes (existing templates). Wraps text nodes into individual spans on
demand so the overlay can target words in any caption format.

Property panel: add Typography (font, size, weight, spacing) and Color
(color, active, dim, opacity) sections alongside existing Position and
Transform controls.

Timeline: move caption timeline into a dedicated flex-shrink-0 section
below the main timeline tracks instead of inside the scrollable area.
Gives it fixed 60px height that's always visible.

Caption overrides: classify color tweens by comparing target color to
the dim baseline instead of relying on timeline position order. This
handles compositions with custom color tweens correctly.

App.tsx: remove polling interval, rely on runtime postMessage events
for caption detection. Add clarifying comment on why useEffect is
appropriate (external event subscription).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): restore cssScale in overlay coordinate conversion

getBoundingClientRect() on iframe-internal elements returns coordinates
in the iframe's native resolution (1920x1080), not the CSS-scaled
display size. The cssScale multiplier is needed to convert to parent
window coordinates. The earlier removal was incorrect — it only worked
at 1:1 scale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): fix reversed scaling on left-side corner handles

Scale interaction used horizontal dx from start position, which goes
negative when dragging left handles outward. Now uses distance from box
center — dragging away from center increases scale regardless of which
corner handle is used.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): make rotation respond to horizontal drag only

Rotation handle sits directly above the word, so atan2-based rotation
barely responds to left/right movement. Replace with linear horizontal
mapping: drag right = clockwise, drag left = counter-clockwise,
200px = 90 degrees. Vertical movement is ignored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(studio): remove animation tab and typography/color from property panel

Keep only Position (X, Y) and Transform (Scale, Rotation) controls.
Remove tab switcher UI since there's only one view now.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: fix oxfmt formatting in CLAUDE.md and captions skill docs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Oct 8, 2026
…ing it

Gemini response bytes were written to disk after only a RIFF header check
(CodeQL js/http-to-file-access heygen-com#180). Route them through normalizeCloudAudio
like the HeyGen path, injectable for tests, and report a transcode failure
as ok:false without leaving a raw file behind.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Oct 12, 2026
* chore(upstream): adopt upstream/main d94708e as a whole tree

Adopt all pending upstream work (about 837 commits) on top of the fork.
The fork history is squashed with no shared ancestry, so this was merged
through a bridge on v0.8.35 (63574a7) and squashed onto fork main.

400 generated catalog pages come from upstream; 39 other conflicts were
resolved by hand, keeping fork overlay files and the fork's CodeQL
hardening where it could coexist with upstream's rewrites. Conflict
table, kept fork deltas, dropped hardening, and verification are in
docs/DECISIONS.md (2026-10-07). Baseline reviewed_through moves to
d94708e.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(security): validate remote media magic bytes before freezing

Restore the fork's assertRemoteMediaBytes in the adopted upstream freezeUrl:
downloaded bytes must match the destination extension (SVG still goes through
sanitizeSvg, .cube through cube-validate, unknown extensions fail closed).
Replace the invalid streaming fixture with valid ISO base media bytes, add
HTML-as-.png and #EXTM3U-as-.mp4 rejection tests, and delete the now
callerless freeze-publisher module and its test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* chore(capture): drop callerless frameworkMarkup

Nothing imports stripFrameworkMarkup after the upstream adoption; upstream's
DOM-based filterExtractedScripts covers the same threat.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* chore(github): remove upstream CODEOWNERS

The file names an upstream maintainer as owner of the capture-check files; on
this fork it would request reviews from a person who does not maintain it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* ci(fork): gate pr-captures to the upstream repository

The Before/After capture requirement is upstream's contributor process and
fails on every fork PR. Gate the job like the other upstream-process
workflows, list catalog-publish, canary-sunset and pr-captures in FORK.md, and
add a maintenance test that keeps the gates and the table in sync.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(security): install media vectors from the package and contain row.file

Upstream's fetchMediaVectors downloaded media-vectors.* from a mutable
raw.githubusercontent.com path with no timeout, size cap or pair validation,
then trusted row.file via resolve(row.file), so a tampered catalog could make
any local file the SFX result that freeze copies into the project.

- build-copy ships media-vectors.json/.bin in dist/catalog-artifact
- installMediaVectors installs from the bundled directory with the same pair
  and row validation as vectorPairAgrees, refuses absolute or .. row files,
  writes 0o600 via temp file and rename, and never fetches
- resolveBundledMediaFile accepts only relative paths that stay inside the
  bundled root; the cwd-relative candidate is gone
- resolve.mjs uses searchLocalSfxIndex; fetchMediaVectors and the
  HYPERFRAMES_REGISTRY path are removed
- tests: absolute and .. row files rejected, ranking works with fetch forbidden

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(studio-server): fix stale collision-suffix comment on upload

The comment claimed the suffixed path was validated at that point; the check
runs on the requested destination and suffixed names are chosen later in the
exclusive-create loop.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(fork): record the post-merge review of the upstream adoption

Append the findings, fixes and the two judged-covered items (upload mode,
frameworkMarkup) to the 2026-10-07 entry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(media-use): drop unused fs imports from the vendored heygen helper

The fork's downloadTo rewrite moved writes into normalizeCloudAudio, so
existsSync, mkdirSync and writeFileSync were left unused and failed the
Preflight lint. Record the lint result and the two pre-existing CI
failures that come from upstream in DECISIONS.

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

* test(core): match scoped scripts after the fork's JSON-literal wrapping

The upstream render-order test looked for push("n") as raw text, but
the fork wraps scoped composition scripts as JSON string literals, so
the marker was not found in one mode and sorted first. Unescape quotes
before matching and require every marker to be present in both modes.

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

* docs(fork): correct why the drag-frames check fails before merge

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

* fix(core): bound the url(#id) scan so hostile render input cannot stall the event loop

Exclude '(' from the fragment id class so a long run of 'url(#!' with no
closing paren no longer backtracks quadratically (CodeQL js/polynomial-redos
heygen-com#156). Adds linear-time regression tests and keeps url(#a), url('#a') and
url( "#a" ) rewriting unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(engine): refuse an extract cache root another user could control

The default root is a predictable name under the shared tmpdir, so a local
user could pre-create it to plant cached frames or a symlinked .hf-last-gc
(CodeQL js/insecure-temporary-file heygen-com#177). assertPrivateCacheRoot creates the
root 0700 and requires a real directory, owned by the caller and closed to
group/other on POSIX. The extractor disables caching with its existing
warning when the check fails, clean skips the sweep, and the sweep marker is
written via exclusive temp file plus rename so it never follows a symlink.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(media-use): re-encode Gemini TTS audio through ffmpeg before writing it

Gemini response bytes were written to disk after only a RIFF header check
(CodeQL js/http-to-file-access heygen-com#180). Route them through normalizeCloudAudio
like the HeyGen path, injectable for tests, and report a transcode failure
as ok:false without leaving a raw file behind.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* refactor(cli): delete the callerless fetchLocalVectors download path

Nothing but its own tests called it; the catalog cache is filled by
installLocalVectors from the bundled package (CodeQL js/http-to-file-access
heygen-com#183). Removes it with its options type and timeout constant, ports the
media-vector validation tests onto installLocalVectors, and adds a test that
installLocalVectors does not touch fetch.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(fork): record the PR #2 CodeQL fixes and dispositions

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(engine): tighten an own 0755 extract cache root instead of disabling it

A root left at 0755 by an older umask belongs to this user and nobody
else can write to it, so chmod it to 0700 and keep caching. Roots that
group or others can write to are still refused.

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

* fix(producer): check the extract cache root before using the GIF cache

The animated-GIF cache sits under the same extract cache root and is
used during compile, before the frame cache's ownership check, so a root
another local user controlled could still poison or redirect it. Route
it through assertPrivateCacheRoot and disable only the GIF cache on
failure. Also format the Gemini TTS test and correct DECISIONS (heygen-com#182 is
fixed; media vectors install via installMediaVectors).

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

* fix(ci): validate adoption comments and test the exact PR candidate

* fix(security): restore capture and media guards and harden Frost assets

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
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