From 8e1ea7adc263bfd95d41b0e8e3f63cb8efbe014c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 23 Jun 2026 15:17:30 -0400 Subject: [PATCH] feat(studio): marquee selection, AE presets, per-keyframe ease + velocity fitting Marquee selection: click+drag on empty canvas draws a dashed selection rectangle. All elements whose OBB intersects are group-selected via SAT. Shift+marquee adds to selection. Click on empty deselects. Per-keyframe easing: each keyframe segment has its own ease, editable via expandable bezier curve editor in the Animation panel. Parser preserves per-keyframe ease through round-trips (ease-only updates preserve existing properties). AE Easy Ease presets: correct After Effects bezier values (0.333, 0, 0.667, 1) in the preset grid. Velocity-based curve fitting: gesture recordings analyze velocity profile and assign per-keyframe custom eases automatically. Gesture smoothing: Gaussian-weighted moving average (from PR #1658) + easeEach support for keyframed tweens + fetch-cancellation race fix in useGsapAnimationsForElement. --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- docs/changelog.mdx | 54 +++ packages/aws-lambda/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/cli.ts | 10 +- packages/cli/src/commands/doctor.ts | 14 + packages/cli/src/commands/events.ts | 64 ++++ packages/cli/src/commands/init.ts | 24 +- packages/cli/src/commands/render.test.ts | 15 + packages/cli/src/commands/render.ts | 15 +- packages/cli/src/commands/skills.ts | 44 ++- packages/cli/src/templates/_shared/AGENTS.md | 4 +- packages/cli/src/templates/_shared/CLAUDE.md | 4 +- packages/cli/src/utils/dockerRunArgs.test.ts | 14 + packages/cli/src/utils/dockerRunArgs.ts | 6 + packages/cli/src/whisper/manager.ts | 8 +- packages/core/package.json | 2 +- .../compiler/inlineSubCompositions.test.ts | 27 +- .../src/compiler/inlineSubCompositions.ts | 40 +-- packages/core/src/parsers/gsapParser.ts | 35 +- packages/core/src/parsers/gsapWriterAcorn.ts | 15 +- packages/core/src/runtime/diagnostics.ts | 2 - packages/core/src/runtime/init.ts | 43 ++- packages/core/src/runtime/media.ts | 33 +- packages/core/src/studio-api/routes/files.ts | 4 +- packages/engine/package.json | 2 +- .../src/services/screenshotService.test.ts | 22 ++ .../engine/src/services/screenshotService.ts | 15 +- packages/engine/src/types.ts | 7 + packages/gcp-cloud-run/package.json | 2 +- packages/player/package.json | 2 +- packages/producer/package.json | 2 +- .../src/services/distributed/renderChunk.ts | 1 + .../producer/src/services/fileServer.test.ts | 38 +++ packages/producer/src/services/fileServer.ts | 11 +- .../src/services/htmlCompiler.test.ts | 9 +- .../producer/src/services/htmlCompiler.ts | 3 + .../src/services/render/observability.ts | 1 + .../src/services/renderOrchestrator.ts | 20 +- packages/sdk/package.json | 2 +- packages/shader-transitions/package.json | 2 +- .../src/captions/hooks/useCaptionSync.ts | 2 +- .../src/components/StudioPreviewArea.tsx | 13 +- .../src/components/StudioRightPanel.tsx | 2 + .../src/components/editor/AnimationCard.tsx | 322 ++++-------------- .../components/editor/AnimationCardParts.tsx | 220 ++++++++++++ .../components/editor/BlockParamsPanel.tsx | 12 +- .../src/components/editor/DomEditOverlay.tsx | 212 +++++++++--- .../components/editor/EaseCurveSection.tsx | 12 +- .../editor/GsapAnimationSection.tsx | 2 + .../components/editor/KeyframeEaseList.tsx | 63 ++++ .../src/components/editor/PropertyPanel.tsx | 2 + .../editor/gsapAnimationCallbacks.ts | 1 + .../editor/gsapAnimationConstants.ts | 6 + .../src/components/editor/manualOffsetDrag.ts | 8 + .../src/components/editor/marqueeCommit.ts | 168 +++++++++ .../components/editor/propertyPanelHelpers.ts | 1 + .../components/editor/snapTargetCollection.ts | 3 - .../src/components/panels/SlideshowPanel.tsx | 1 - .../studio/src/contexts/DomEditContext.tsx | 8 + .../src/hooks/gsapDragPositionCommit.ts | 3 +- .../studio/src/hooks/gsapRuntimeBridge.ts | 15 +- .../studio/src/hooks/gsapRuntimeReaders.ts | 6 +- .../studio/src/hooks/useDomEditCommits.ts | 4 - .../studio/src/hooks/useDomEditPreviewSync.ts | 1 - .../studio/src/hooks/useDomEditSession.ts | 23 ++ .../studio/src/hooks/useDomEditTextCommits.ts | 12 +- packages/studio/src/hooks/useDomSelection.ts | 37 ++ packages/studio/src/hooks/useGestureCommit.ts | 21 +- .../studio/src/hooks/useGestureRecording.ts | 3 +- .../studio/src/hooks/useGsapAnimationOps.ts | 4 +- .../studio/src/hooks/useGsapTweenCache.ts | 30 +- .../studio/src/player/components/Player.tsx | 3 - .../src/player/hooks/useTimelinePlayer.ts | 15 +- .../player/hooks/useTimelineSyncCallbacks.ts | 7 +- .../studio/src/player/lib/playbackAdapter.ts | 5 +- .../src/player/lib/timelineIframeHelpers.ts | 6 +- packages/studio/src/telemetry/client.ts | 6 - packages/studio/src/utils/editDebugLog.ts | 13 +- .../studio/src/utils/gestureSmoother.test.ts | 48 +++ packages/studio/src/utils/gestureSmoother.ts | 43 +++ .../studio/src/utils/marqueeGeometry.test.ts | 123 +++++++ packages/studio/src/utils/marqueeGeometry.ts | 172 ++++++++++ packages/studio/src/utils/optimisticUpdate.ts | 3 +- packages/studio/src/utils/sourcePatcher.ts | 3 - .../src/utils/velocityEaseFitter.test.ts | 58 ++++ .../studio/src/utils/velocityEaseFitter.ts | 116 +++++++ releases/v0.7.4.md | 53 +++ skills/music-to-video/SKILL.md | 6 +- .../music-to-video/scripts/validate-plan.mjs | 17 +- .../music-to-video/sub-agents/frame-worker.md | 1 + skills/product-launch-video/SKILL.md | 4 +- .../sub-agents/frame-worker.md | 11 +- 95 files changed, 1992 insertions(+), 564 deletions(-) create mode 100644 packages/cli/src/commands/events.ts create mode 100644 packages/studio/src/components/editor/AnimationCardParts.tsx create mode 100644 packages/studio/src/components/editor/KeyframeEaseList.tsx create mode 100644 packages/studio/src/components/editor/marqueeCommit.ts create mode 100644 packages/studio/src/utils/gestureSmoother.test.ts create mode 100644 packages/studio/src/utils/gestureSmoother.ts create mode 100644 packages/studio/src/utils/marqueeGeometry.test.ts create mode 100644 packages/studio/src/utils/marqueeGeometry.ts create mode 100644 packages/studio/src/utils/velocityEaseFitter.test.ts create mode 100644 packages/studio/src/utils/velocityEaseFitter.ts create mode 100644 releases/v0.7.4.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ac7f59e65c..7be6e7b517 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hyperframes", "description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", - "version": "0.7.3", + "version": "0.7.4", "author": { "name": "HeyGen", "email": "hyperframes@heygen.com", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 3e0c34fd68..26fab65be3 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "hyperframes", - "version": "0.7.3", + "version": "0.7.4", "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", "author": { "name": "HeyGen", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 1139d27e28..b00d918add 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "$schema": "https://cursor.com/schemas/cursor-plugin/plugin.json", "name": "hyperframes", "displayName": "HyperFrames by HeyGen", - "version": "0.7.3", + "version": "0.7.4", "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", "author": { "name": "HeyGen", diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 4fc675a3d7..2c8a9c6d40 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -8,6 +8,60 @@ Recent HyperFrames releases, including user-facing features, fixes, and migratio {/* New release entries are prepended by `bun run changelog:draft --write`. */} + +This stable release improves render reliability and skill authoring: producer duration +now falls back to sub-composition timing, empty sub-composition files no longer abort +renders, and Studio timeline styling handles missing tags safely. It also ships +music-to-video workflow updates, per-skill telemetry, and clearer CLI install/debug +guidance. + +## Features + +- **CLI:** Per-skill usage telemetry ([4961e4e4](https://github.com/heygen-com/hyperframes/commit/4961e4e4770a9b38eb34fef957c0ac8764614ce8)) +- **Skills:** Unify bgm-to-video flows into music-to-video ([a34d3dba](https://github.com/heygen-com/hyperframes/commit/a34d3dba4d15b3d6830bc546b7b6c423a1304477)) +- **Skills:** Add bgm-to-video skill ([d596379e](https://github.com/heygen-com/hyperframes/commit/d596379e52536672366e889602004a65bc8136d9)) +- **Skills:** Guide install when a matched workflow isn't installed ([08328b04](https://github.com/heygen-com/hyperframes/commit/08328b04e63e16c7bea469237552c1d9587dcfec), [#1647](https://github.com/heygen-com/hyperframes/pull/1647)) + +## Fixes + +- **Producer:** Derive duration from sub-composition timing when root has no data-duration ([45a9440c](https://github.com/heygen-com/hyperframes/commit/45a9440c61073314dd8d3ac34813d108c1e051e7), [#1680](https://github.com/heygen-com/hyperframes/pull/1680)) +- **Core:** Skip empty sub-composition files instead of aborting render ([656c1200](https://github.com/heygen-com/hyperframes/commit/656c1200ee2e42fe2affa9641af8fcf0ccbb5683), [#1678](https://github.com/heygen-com/hyperframes/pull/1678)) +- **Studio:** Guard against null tag in timeline track style ([d4aa5f93](https://github.com/heygen-com/hyperframes/commit/d4aa5f93e16c564d5248da99cd22101b314d3d63), [#1679](https://github.com/heygen-com/hyperframes/pull/1679)) +- **CLI:** Improve whisper-cpp install guidance and add doctor check ([344618e2](https://github.com/heygen-com/hyperframes/commit/344618e22aaf27b526d16ef219dfe193ae8920fd), [#1681](https://github.com/heygen-com/hyperframes/pull/1681)) +- **Engine:** Restore fast screenshot path for viewport captures ([f622e5a7](https://github.com/heygen-com/hyperframes/commit/f622e5a7ba3f840bda2703a93784ea0105d21fef), [#1670](https://github.com/heygen-com/hyperframes/pull/1670)) +- **CLI:** Install skills into project dir on non-interactive init ([bf4f34b3](https://github.com/heygen-com/hyperframes/commit/bf4f34b359c252bfac25061d19bd178b28da5588), [#1671](https://github.com/heygen-com/hyperframes/pull/1671)) +- **CLI:** Expose render debug mode ([7133c396](https://github.com/heygen-com/hyperframes/commit/7133c396eb29f4d28f3d25b8dc8dcf017bc70618)) +- Unblock music video ci checks ([5d1bff51](https://github.com/heygen-com/hyperframes/commit/5d1bff51b5a21145b9d84c95d21447ebb90efc13)) +- **Producer:** Store css-var-fonts baseline as raw binary, not LFS pointer ([119284d3](https://github.com/heygen-com/hyperframes/commit/119284d377878e38df60469a6bbeaf6d6a8ed857)) +- **Producer:** Restore css-var-fonts regression baseline ([f012b8b8](https://github.com/heygen-com/hyperframes/commit/f012b8b84691c6a5989e11c0d228c5320d8d6843)) +- **Lint:** Catch CSS↔GSAP transform conflicts in scoped selectors and frame sub-compositions ([9ccae863](https://github.com/heygen-com/hyperframes/commit/9ccae8637291262b8a2e1a1779e4ee7ad186f675)) + +## Docs & Examples + +- **Skills:** Add music-source brief to music-to-video Step 0 ([7a464689](https://github.com/heygen-com/hyperframes/commit/7a4646894f0d3d5edcc185f1e1a61dcce11345ed)) +- **Skills:** Register music-to-video in the hyperframes router ([44b04324](https://github.com/heygen-com/hyperframes/commit/44b04324b1576d80a2fb5a1bdf8dda0fd049a6ff)) +- **Skills:** Add beat-synced montage authoring recipe ([0dcf914e](https://github.com/heygen-com/hyperframes/commit/0dcf914e14ae74ddfe6e8c6118113929bcfb894f)) + +## Catalog + +- Refine music-to-video planning catalogs ([a23ca348](https://github.com/heygen-com/hyperframes/commit/a23ca3487f712d9df9ed72e1d6b16683dfc4bd2f)) + +## Internal + +- **Lint:** Keep the fallow audit gate green ([4a8e2f1c](https://github.com/heygen-com/hyperframes/commit/4a8e2f1cd0a1d156ac485fd084230da8c407b152)) + +## Other Changes + +- Merge pull request #1672 from heygen-com/fix/skill-authoring-fixes ([26810856](https://github.com/heygen-com/hyperframes/commit/2681085624056aeac851b03fd46efe512017c2a7)) +- **Skills:** Apply oxfmt to music-to-video and router docs ([265b0273](https://github.com/heygen-com/hyperframes/commit/265b02738e35d21b5455a640323551f0e3cf3c6e)) + +[View the full commit range](https://github.com/heygen-com/hyperframes/compare/v0.7.3...v0.7.4). + + import("./commands/skills.js").then((m) => m.default), feedback: () => import("./commands/feedback.js").then((m) => m.default), telemetry: () => import("./commands/telemetry.js").then((m) => m.default), + events: () => import("./commands/events.js").then((m) => m.default), validate: () => import("./commands/validate.js").then((m) => m.default), snapshot: () => import("./commands/snapshot.js").then((m) => m.default), capture: () => import("./commands/capture.js").then((m) => m.default), @@ -194,7 +195,10 @@ let _trackCommandResult: | undefined; let _printUpdateNotice: (() => void) | undefined; -if (!isHelp && command !== "telemetry" && command !== "unknown") { +// `events` is a telemetry-internal beacon: it self-tracks + self-flushes, so it +// skips the per-command wrapper (no duplicate cli_command, no first-run notice +// printed into a skill's captured output). +if (!isHelp && command !== "telemetry" && command !== "events" && command !== "unknown") { import("./telemetry/index.js").then((mod) => { _flush = mod.flush; _flushSync = mod.flushSync; @@ -206,7 +210,9 @@ if (!isHelp && command !== "telemetry" && command !== "unknown") { }); } -if (!isHelp && !hasJsonFlag && command !== "upgrade") { +// `events` skips the update check too — a skill-usage beacon must not add +// network latency or trigger a background self-upgrade on the calling skill. +if (!isHelp && !hasJsonFlag && command !== "upgrade" && command !== "events") { // Report any completed auto-install from the previous run first, before // kicking off the next check — so the user sees "updated to vX" once and // we don't over-print. diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index c5779c8426..6619b164d8 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -145,6 +145,19 @@ function checkEnvironment(): CheckResult { return { ok: true, detail: parts.join(" \u00B7 ") }; } +async function checkWhisper(): Promise { + const { findWhisper, getInstallInstructions } = await import("../whisper/manager.js"); + const result = findWhisper(); + if (result) { + return { ok: true, detail: result.executablePath }; + } + return { + ok: false, + detail: "Not found (optional \u2014 needed for transcription)", + hint: getInstallInstructions(), + }; +} + export interface CheckOutcome { name: string; ok: boolean; @@ -213,6 +226,7 @@ export default defineCommand({ } checks.push({ name: "Environment", run: checkEnvironment }); + checks.push({ name: "whisper-cpp", run: checkWhisper }); const outcomes: CheckOutcome[] = []; for (const check of checks) { diff --git a/packages/cli/src/commands/events.ts b/packages/cli/src/commands/events.ts new file mode 100644 index 0000000000..7fb466c4f9 --- /dev/null +++ b/packages/cli/src/commands/events.ts @@ -0,0 +1,64 @@ +import { defineCommand } from "citty"; +import { trackEvent, flush } from "../telemetry/client.js"; + +// Skill-usage telemetry endpoint. A skill reports its own invocation/outcome — +// ideally from its own bundled script, so it fires deterministically rather +// than relying on the agent to remember: +// +// npx hyperframes events --skill=product-launch-video +// npx hyperframes events --skill=product-launch-video --event=skill_completed --outcome=success +// +// Rides the SAME anonymous PostHog pipeline + consent gates as every other CLI +// event (DO_NOT_TRACK / telemetry opt-out, anonymous install UUID, IP stripped). +// +// Telemetry must NEVER break the calling skill: every arg is optional (a missing +// or malformed value is a silent no-op, not a non-zero exit), the body is +// guarded, and flush() carries its own hard timeout. This command always exits 0. + +const ALLOWED_EVENTS = ["skill_invoked", "skill_completed"]; +const ALLOWED_OUTCOMES = ["success", "error", "abort"]; +// Skill names are lowercase slugs (e.g. "product-launch-video"). Anything that +// doesn't match is dropped, so a caller can't push high-cardinality or PII +// strings (paths, shell output, free text) into the anonymous event stream. +const SKILL_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/; + +export default defineCommand({ + meta: { + name: "events", + description: + "Emit an anonymous skill-usage telemetry event (skills report their own invocation/outcome). Honors DO_NOT_TRACK / telemetry opt-out.", + }, + args: { + skill: { + type: "string", + description: "Authoring skill slug, e.g. product-launch-video", + }, + event: { + type: "string", + description: "Event name: skill_invoked | skill_completed (default: skill_invoked)", + default: "skill_invoked", + }, + outcome: { + type: "string", + description: "Optional outcome for completion events: success | error | abort", + }, + }, + async run({ args }) { + // Best-effort: nothing here may fail the skill that called us. Missing or + // malformed input is a silent no-op rather than a non-zero exit. + try { + const skill = typeof args.skill === "string" ? args.skill.trim() : ""; + if (!SKILL_SLUG.test(skill)) return; // missing / non-slug → no-op + + const event = ALLOWED_EVENTS.includes(args.event) ? args.event : "skill_invoked"; + const props: Record = { authoring_skill: skill }; + if (args.outcome && ALLOWED_OUTCOMES.includes(args.outcome)) { + props["outcome"] = args.outcome; + } + trackEvent(event, props); + await flush(); + } catch { + // swallow — telemetry must never surface a non-zero exit to the caller + } + }, +}); diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index dc577a5e6b..02b0dc3bd5 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -800,11 +800,27 @@ export default defineCommand({ for (const f of readdirSync(destDir).filter((f) => !f.startsWith("."))) { console.log(` ${c.accent(f)}`); } + + if (!skipSkills) { + const { installAllSkills } = await import("./skills.js"); + // --yes keeps it non-interactive. When Claude Code is driving + // (CLAUDECODE env var), target its native dir so skills land in + // .claude/skills/ instead of only .agents/skills/. + const args = process.env["CLAUDECODE"] ? ["--agent", "claude-code", "--yes"] : ["--yes"]; + await installAllSkills({ cwd: destDir, extraArgs: args }); + } + console.log(); console.log("Get started:"); console.log(); - console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`); - console.log(` ${c.accent("npx skills add heygen-com/hyperframes")}`); + if (skipSkills) { + console.log(` ${c.accent("1.")} Install AI coding skills (one-time):`); + console.log(` ${c.accent("npx skills add heygen-com/hyperframes --yes")}`); + } else { + console.log( + ` ${c.accent("1.")} Restart your AI agent (new session) so it loads the skills.`, + ); + } console.log(); console.log(` ${c.accent("2.")} Open this project with your AI coding agent:`); console.log( @@ -1018,8 +1034,8 @@ export default defineCommand({ process.exit(0); } if (installSkills) { - const skillsCmd = await import("./skills.js").then((m) => m.default); - await runCommand(skillsCmd, { rawArgs: [] }); + const { installAllSkills } = await import("./skills.js"); + await installAllSkills({ cwd: destDir }); } } diff --git a/packages/cli/src/commands/render.test.ts b/packages/cli/src/commands/render.test.ts index 6f35d40a64..f6e0c089df 100644 --- a/packages/cli/src/commands/render.test.ts +++ b/packages/cli/src/commands/render.test.ts @@ -252,6 +252,21 @@ describe("renderLocal browser GPU config", () => { expect(producerState.createdJobs[0]?.videoFrameFormat).toBe("png"); }); + it("forwards debug mode to createRenderJob", async () => { + await renderLocal("/tmp/project", "/tmp/out.mp4", { + fps: { num: 30, den: 1 }, + quality: "standard", + format: "mp4", + gpu: false, + browserGpuMode: "software", + hdrMode: "auto", + quiet: true, + debug: true, + }); + + expect(producerState.createdJobs[0]?.debug).toBe(true); + }); + it("omits variables from createRenderJob when not provided", async () => { await renderLocal("/tmp/project", "/tmp/out.mp4", { fps: { num: 30, den: 1 }, diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 333704f48a..bf65c3c645 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -241,6 +241,12 @@ export default defineCommand({ description: "Suppress verbose output", default: false, }, + debug: { + type: "boolean", + description: + "Write full render diagnostics and keep intermediate artifacts under the producer .debug directory.", + default: false, + }, strict: { type: "boolean", description: "Fail render on lint errors", @@ -548,6 +554,7 @@ export default defineCommand({ const browserGpuArg = args["browser-gpu"]; const browserGpuMode = resolveBrowserGpuForCli(useDocker, browserGpuArg); const quiet = args.quiet ?? false; + const debug = args.debug ?? false; const batchJson = args.json ?? false; const effectiveQuiet = quiet || (batchPath != null && batchJson); const strictAll = args["strict-all"] ?? false; @@ -763,6 +770,7 @@ export default defineCommand({ pageNavigationTimeoutMs, protocolTimeout, playerReadyTimeout, + debug, exitAfterComplete: false, throwOnError: true, skipFeedback: true, @@ -815,6 +823,7 @@ export default defineCommand({ videoBitrate, videoFrameFormat, quiet, + debug, variables, entryFile, outputResolution, @@ -840,6 +849,7 @@ export default defineCommand({ videoFrameFormat, quiet, browserPath, + debug, variables, entryFile, outputResolution, @@ -876,6 +886,7 @@ interface RenderOptions { videoBitrate?: string; videoFrameFormat?: VideoFrameFormat; quiet: boolean; + debug?: boolean; browserPath?: string; variables?: Record; entryFile?: string; @@ -1124,6 +1135,7 @@ async function renderDocker( entryFile: options.entryFile, outputResolution: options.outputResolution, pageSideCompositing: options.pageSideCompositing, + debug: options.debug, pageNavigationTimeoutMs: options.pageNavigationTimeoutMs, }, }); @@ -1206,7 +1218,7 @@ export async function renderLocal( const startTime = Date.now(); const logger = createRenderTelemetryLogger( - producer.createConsoleLogger?.("info") ?? createNoopProducerLogger(), + producer.createConsoleLogger?.(options.debug ? "debug" : "info") ?? createNoopProducerLogger(), ); const job = producer.createRenderJob({ @@ -1233,6 +1245,7 @@ export async function renderLocal( variables: options.variables, entryFile: options.entryFile, outputResolution: options.outputResolution, + debug: options.debug, }); const onProgress = options.quiet diff --git a/packages/cli/src/commands/skills.ts b/packages/cli/src/commands/skills.ts index c53e2208ea..2894058c90 100644 --- a/packages/cli/src/commands/skills.ts +++ b/packages/cli/src/commands/skills.ts @@ -14,12 +14,16 @@ function hasNpx(): boolean { } } -function runSkillsAdd(repo: string): Promise { - const npx = buildNpxCommand(["skills", "add", repo, "--all"]); +function runSkillsAdd( + repo: string, + opts: { cwd?: string; extraArgs?: string[] } = {}, +): Promise { + const npx = buildNpxCommand(["skills", "add", repo, ...(opts.extraArgs ?? ["--all"])]); return new Promise((resolve, reject) => { const child = spawn(npx.command, npx.args, { stdio: "inherit", timeout: 120_000, + cwd: opts.cwd, // GH #316 — the upstream `skills` CLI shells out to `git clone`. // When Git's clone-hook protection is active (shipped on by // default in 2.45.1, reverted in 2.45.2, still present on many @@ -40,6 +44,26 @@ function runSkillsAdd(repo: string): Promise { const SOURCES = [{ name: "HyperFrames", repo: "heygen-com/hyperframes" }]; +export async function installAllSkills( + opts: { cwd?: string; extraArgs?: string[] } = {}, +): Promise { + if (!hasNpx()) { + clack.log.error(c.error("npx not found. Install Node.js and retry.")); + return; + } + + for (const source of SOURCES) { + console.log(); + console.log(c.bold(`Installing ${source.name} skills...`)); + console.log(); + try { + await runSkillsAdd(source.repo, opts); + } catch { + console.log(c.dim(`${source.name} skills skipped`)); + } + } +} + export default defineCommand({ meta: { name: "skills", @@ -47,20 +71,6 @@ export default defineCommand({ }, args: {}, async run() { - if (!hasNpx()) { - clack.log.error(c.error("npx not found. Install Node.js and retry.")); - return; - } - - for (const source of SOURCES) { - console.log(); - console.log(c.bold(`Installing ${source.name} skills...`)); - console.log(); - try { - await runSkillsAdd(source.repo); - } catch { - console.log(c.dim(`${source.name} skills skipped`)); - } - } + await installAllSkills(); }, }); diff --git a/packages/cli/src/templates/_shared/AGENTS.md b/packages/cli/src/templates/_shared/AGENTS.md index 684ced94c9..14ecc1bed0 100644 --- a/packages/cli/src/templates/_shared/AGENTS.md +++ b/packages/cli/src/templates/_shared/AGENTS.md @@ -21,8 +21,8 @@ The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes- > **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`. -> **Skills not available?** Ask the user to run `npx hyperframes skills` and restart their -> agent session, or install manually: `npx skills add heygen-com/hyperframes`. +> **Skills not available or need updating?** Run `npx skills add heygen-com/hyperframes` +> and restart the agent session so the new skills load. ## Commands diff --git a/packages/cli/src/templates/_shared/CLAUDE.md b/packages/cli/src/templates/_shared/CLAUDE.md index 684ced94c9..14ecc1bed0 100644 --- a/packages/cli/src/templates/_shared/CLAUDE.md +++ b/packages/cli/src/templates/_shared/CLAUDE.md @@ -21,8 +21,8 @@ The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes- > **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`. -> **Skills not available?** Ask the user to run `npx hyperframes skills` and restart their -> agent session, or install manually: `npx skills add heygen-com/hyperframes`. +> **Skills not available or need updating?** Run `npx skills add heygen-com/hyperframes` +> and restart the agent session so the new skills load. ## Commands diff --git a/packages/cli/src/utils/dockerRunArgs.test.ts b/packages/cli/src/utils/dockerRunArgs.test.ts index 810c090ce4..78c138842f 100644 --- a/packages/cli/src/utils/dockerRunArgs.test.ts +++ b/packages/cli/src/utils/dockerRunArgs.test.ts @@ -171,6 +171,7 @@ describe("buildDockerRunArgs", () => { videoBitrate: undefined, videoFrameFormat: "png", quiet: true, + debug: true, entryFile: "compositions/intro.html", }, }); @@ -188,6 +189,7 @@ describe("buildDockerRunArgs", () => { expect(args).toContain("--video-frame-format"); expect(args).toContain("png"); expect(args).toContain("--quiet"); + expect(args).toContain("--debug"); expect(args).toContain("--gpu"); expect(args).toContain("--no-browser-gpu"); expect(args).toContain("--hdr"); @@ -352,6 +354,18 @@ describe("buildDockerRunArgs", () => { expect(args).toContain("--no-page-side-compositing"); }); + it("keeps Docker debug artifacts under the mounted output directory", () => { + const args = buildDockerRunArgs({ + ...FIXED_INPUT, + options: { ...BASE, debug: true }, + }); + const envIdx = args.indexOf("PRODUCER_RENDERS_DIR=/output/renders"); + const imageIdx = args.indexOf(FIXED_INPUT.imageTag); + expect(envIdx).toBeGreaterThan(-1); + expect(envIdx).toBeLessThan(imageIdx); + expect(args).toContain("--debug"); + }); + it("omits --no-page-side-compositing when pageSideCompositing is not explicitly false", () => { const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE }); expect(args).not.toContain("--no-page-side-compositing"); diff --git a/packages/cli/src/utils/dockerRunArgs.ts b/packages/cli/src/utils/dockerRunArgs.ts index 832460e612..508d966b9d 100644 --- a/packages/cli/src/utils/dockerRunArgs.ts +++ b/packages/cli/src/utils/dockerRunArgs.ts @@ -50,6 +50,7 @@ export interface DockerRenderOptions { videoBitrate?: string; videoFrameFormat?: "auto" | "jpg" | "png"; quiet: boolean; + debug?: boolean; variables?: Record; entryFile?: string; /** Output resolution preset (e.g. "landscape-4k"). Forwarded as `--resolution`. */ @@ -109,6 +110,10 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] { `${projectDir}:/project:ro`, "-v", `${outputDir}:/output`, + // Keep debug artifacts on the mounted host output path. The producer roots + // `.debug` at dirname(PRODUCER_RENDERS_DIR), so `/output/renders` maps to + // `/output/.debug/` instead of a disposable container path. + ...(options.debug ? ["-e", "PRODUCER_RENDERS_DIR=/output/renders"] : []), imageTag, "/project", "--output", @@ -128,6 +133,7 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] { ? ["--video-frame-format", options.videoFrameFormat] : []), ...(options.quiet ? ["--quiet"] : []), + ...(options.debug ? ["--debug"] : []), ...(options.gpu ? ["--gpu"] : []), ...(options.browserGpu ? [] : ["--no-browser-gpu"]), ...(options.hdrMode === "force-hdr" ? ["--hdr"] : []), diff --git a/packages/cli/src/whisper/manager.ts b/packages/cli/src/whisper/manager.ts index 05a7c74489..7985283382 100644 --- a/packages/cli/src/whisper/manager.ts +++ b/packages/cli/src/whisper/manager.ts @@ -151,10 +151,16 @@ export function findWhisper(): WhisperResult | undefined { return findFromEnv() ?? findFromSystem() ?? findBuiltBinary(); } -function getInstallInstructions(): string { +export function getInstallInstructions(): string { if (platform() === "darwin") { return "brew install whisper-cpp"; } + if (platform() === "linux") { + return "Build from source: https://github.com/ggml-org/whisper.cpp#building (requires cmake and a C compiler)"; + } + if (platform() === "win32") { + return "Build with cmake: https://github.com/ggml-org/whisper.cpp#building"; + } return "See https://github.com/ggml-org/whisper.cpp#building"; } diff --git a/packages/core/package.json b/packages/core/package.json index d5f81ea51d..18d6871504 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@hyperframes/core", - "version": "0.7.3", + "version": "0.7.4", "description": "", "repository": { "type": "git", diff --git a/packages/core/src/compiler/inlineSubCompositions.test.ts b/packages/core/src/compiler/inlineSubCompositions.test.ts index 452d1e8e8f..c86583199a 100644 --- a/packages/core/src/compiler/inlineSubCompositions.test.ts +++ b/packages/core/src/compiler/inlineSubCompositions.test.ts @@ -38,18 +38,27 @@ function makeHostDocument(compId: string) { } describe("inlineSubCompositions – #ID selector scoping divergence", () => { - it("throws an actionable error when a resolved sub-composition file is empty", () => { + it.each([ + { label: "empty", html: "" }, + { label: "whitespace-only", html: " \n \t " }, + { + label: "valid-parse-empty-body", + html: "", + }, + ])("skips $label sub-composition files gracefully", ({ html }) => { const document = makeHostDocument("intro"); const host = document.querySelector('[data-composition-src="intro.html"]')!; + const missing: string[] = []; - expect(() => - inlineSubCompositions(document, [host], { - resolveHtml: () => "", - parseHtml: (html) => parseHTML(html).document, - }), - ).toThrow( - "Composition HTML is empty or could not be parsed: intro.html. Check that the file referenced by data-composition-src contains valid HTML.", - ); + const result = inlineSubCompositions(document, [host], { + resolveHtml: () => html, + parseHtml: (h) => parseHTML(h).document, + onMissingComposition: (src) => missing.push(src), + }); + + expect(missing).toEqual(["intro.html"]); + expect(result.styles).toHaveLength(0); + expect(result.scripts).toHaveLength(0); }); it("producer path (no flattenInnerRoot): strips inner root, losing #id attribute", () => { diff --git a/packages/core/src/compiler/inlineSubCompositions.ts b/packages/core/src/compiler/inlineSubCompositions.ts index f6e0b6167e..14e8341dd6 100644 --- a/packages/core/src/compiler/inlineSubCompositions.ts +++ b/packages/core/src/compiler/inlineSubCompositions.ts @@ -125,24 +125,6 @@ function defaultBuildScopeSelector(compId: string): string { return `[data-composition-id="${escaped}"]`; } -function emptyCompositionHtmlError(src: string): Error { - return new Error( - `Composition HTML is empty or could not be parsed: ${src}. Check that the file referenced by data-composition-src contains valid HTML.`, - ); -} - -function assertNonEmptyCompositionHtml(html: string, src: string): void { - if (!html.trim()) { - throw emptyCompositionHtmlError(src); - } -} - -function assertParsedCompositionDocument(doc: Document, src: string): void { - if (!doc.documentElement) { - throw emptyCompositionHtmlError(src); - } -} - // --------------------------------------------------------------------------- // Core implementation // --------------------------------------------------------------------------- @@ -194,16 +176,16 @@ export function inlineSubCompositions( if (!src) continue; const compHtml = resolveHtml(src); - if (compHtml == null) { - if (onMissingComposition) { - onMissingComposition(src); - } + if (compHtml == null || !compHtml.trim()) { + onMissingComposition?.(src); continue; } - assertNonEmptyCompositionHtml(compHtml, src); const compDoc = parseHtml(compHtml); - assertParsedCompositionDocument(compDoc, src); + if (!compDoc.documentElement) { + onMissingComposition?.(src); + continue; + } // Determine composition IDs let compId: string | null; @@ -220,9 +202,15 @@ export function inlineSubCompositions( // Find content: prefer