From 08d259187121fa75951d691a4bacf6f5c45a782f Mon Sep 17 00:00:00 2001 From: kiritowoo <295860553+kiritowoo@users.noreply.github.com> Date: Wed, 24 Jun 2026 15:47:56 +0800 Subject: [PATCH 1/2] feat(telemetry): attribute renders to the authoring workflow skill Add an optional `--skill` flag to `hyperframes render` and tag the `render_complete` / `render_error` events with `authoring_skill`, so render usage can be broken down per authoring workflow. The value is slug-gated (a malformed value is ignored) and the existing anonymous / opt-out telemetry pipeline is otherwise unchanged. Each end-user workflow that renders now passes `--skill=` on its render command: embedded-captions, faceless-explainer, graphic-overlays, motion-graphics, music-to-video, pr-to-video, product-launch-video, remotion-to-hyperframes, website-to-video. Not instrumented, by design: general-video renders freeform with no canonical render command to attach to, and slideshow produces an interactive deck rather than a rendered video. Both can follow up if per-skill numbers are wanted. --- packages/cli/src/commands/render.ts | 24 +++++++++++++++++++ packages/cli/src/telemetry/events.ts | 6 +++++ .../scripts/render-and-composite.sh | 4 ++-- skills/faceless-explainer/SKILL.md | 2 +- skills/graphic-overlays/SKILL.md | 1 + skills/motion-graphics/SKILL.md | 2 +- skills/motion-graphics/agents/finalize.md | 2 +- skills/music-to-video/SKILL.md | 2 +- skills/pr-to-video/SKILL.md | 2 +- skills/product-launch-video/SKILL.md | 2 +- skills/remotion-to-hyperframes/SKILL.md | 2 +- .../references/step-6-validate.md | 8 +++---- 12 files changed, 44 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index bf65c3c645..1642b2b6fb 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -175,6 +175,12 @@ export default defineCommand({ description: "Quality: draft, standard, high", default: "standard", }, + skill: { + type: "string", + description: + "Authoring workflow skill that initiated this render (e.g. product-launch-video). " + + "Recorded on anonymous render telemetry for per-skill usage breakdowns; ignored unless it is a slug.", + }, format: { type: "string", description: @@ -379,6 +385,16 @@ export default defineCommand({ } const quality = qualityRaw as "draft" | "standard" | "high"; + // ── Authoring skill (telemetry attribution) ──────────────────────────── + // Optional slug naming the workflow skill that drove this render (e.g. + // "product-launch-video"), tagged onto render telemetry for per-skill usage + // breakdowns. Slug-gated so a caller can't push high-cardinality or PII + // strings into the anonymous event stream; a missing/invalid value is omitted. + const authoringSkill = + typeof args.skill === "string" && /^[a-z0-9][a-z0-9-]{0,63}$/.test(args.skill) + ? args.skill + : undefined; + // ── Validate format ───────────────────────────────────────────────── const formatRaw = args.format ?? "mp4"; const format = parseRenderFormat(formatRaw); @@ -755,6 +771,7 @@ export default defineCommand({ const renderOptionsBase: RenderOptions = { fps, quality, + authoringSkill, format, workers, gpu: useGpu, @@ -812,6 +829,7 @@ export default defineCommand({ await renderDocker(project.dir, outputPath, { fps, quality, + authoringSkill, format, gifLoop, workers, @@ -837,6 +855,7 @@ export default defineCommand({ await renderLocal(project.dir, outputPath, { fps, quality, + authoringSkill, format, gifLoop, workers, @@ -870,6 +889,8 @@ export interface SingleRenderResult { interface RenderOptions { fps: Fps; quality: "draft" | "standard" | "high"; + /** Authoring workflow skill that drove this render (telemetry attribution). */ + authoringSkill?: string; format: RenderFormat; gifLoop?: number; workers?: number; @@ -1171,6 +1192,7 @@ async function renderDocker( workers: options.workers, docker: true, gpu: options.gpu, + authoringSkill: options.authoringSkill, ...getMemorySnapshot(), }); @@ -1410,6 +1432,7 @@ function handleRenderError( docker, workers: options.workers, gpu: options.gpu, + authoringSkill: options.authoringSkill, elapsedMs: Date.now() - startTime, errorMessage: message, failedStage, @@ -1455,6 +1478,7 @@ function trackRenderMetrics( workers: options.workers ?? perf?.workers, docker, gpu: options.gpu, + authoringSkill: options.authoringSkill, staticDedupEnabled: perf?.staticDedup?.enabled, staticDedupArmed: perf?.staticDedup?.armed, staticDedupSkipReason: perf?.staticDedup?.skipReason, diff --git a/packages/cli/src/telemetry/events.ts b/packages/cli/src/telemetry/events.ts index e9e8e8d7e1..bbcc34bf26 100644 --- a/packages/cli/src/telemetry/events.ts +++ b/packages/cli/src/telemetry/events.ts @@ -98,6 +98,8 @@ export function trackRenderComplete( durationMs: number; fps: number; quality: string; + /** Authoring workflow skill that drove this render (e.g. "product-launch-video"). */ + authoringSkill?: string; workers?: number; docker: boolean; gpu: boolean; @@ -155,6 +157,7 @@ export function trackRenderComplete( duration_ms: props.durationMs, fps: props.fps, quality: props.quality, + authoring_skill: props.authoringSkill, workers: props.workers, docker: props.docker, gpu: props.gpu, @@ -202,6 +205,8 @@ export function trackRenderError( props: { fps: number; quality: string; + /** Authoring workflow skill that drove this render (e.g. "product-launch-video"). */ + authoringSkill?: string; docker: boolean; workers?: number; gpu?: boolean; @@ -221,6 +226,7 @@ export function trackRenderError( { fps: props.fps, quality: props.quality, + authoring_skill: props.authoringSkill, docker: props.docker, workers: props.workers, gpu: props.gpu, diff --git a/skills/embedded-captions/scripts/render-and-composite.sh b/skills/embedded-captions/scripts/render-and-composite.sh index 1d4ecb6ddb..07f5beff21 100755 --- a/skills/embedded-captions/scripts/render-and-composite.sh +++ b/skills/embedded-captions/scripts/render-and-composite.sh @@ -256,9 +256,9 @@ hf_render_dir() { # bash 3.2 (macOS) throws on empty-array expansion under `set -u`, so branch # explicitly instead of splatting an optional --format array. if [[ -n "$fmt" ]]; then - node "$HF_CLI" render --dir "$proj" --fps "$FPS" --format "$fmt" --crf 11 -o "$out" & + node "$HF_CLI" render --skill embedded-captions --dir "$proj" --fps "$FPS" --format "$fmt" --crf 11 -o "$out" & else - node "$HF_CLI" render --dir "$proj" --fps "$FPS" --crf 11 -o "$out" & + node "$HF_CLI" render --skill embedded-captions --dir "$proj" --fps "$FPS" --crf 11 -o "$out" & fi local pid=$! start=$SECONDS elapsed while kill -0 "$pid" 2>/dev/null; do diff --git a/skills/faceless-explainer/SKILL.md b/skills/faceless-explainer/SKILL.md index 1e801fc4fd..1a8d43e2aa 100644 --- a/skills/faceless-explainer/SKILL.md +++ b/skills/faceless-explainer/SKILL.md @@ -162,7 +162,7 @@ Preview: `npx hyperframes preview` Render only after user approval: -`npx hyperframes render --quality high --output renders/video.mp4` +`npx hyperframes render --skill=faceless-explainer --quality high --output renders/video.mp4` Do not rerun `lint`, `validate`, `inspect`, or `snapshot` after rendering unless the user asks. diff --git a/skills/graphic-overlays/SKILL.md b/skills/graphic-overlays/SKILL.md index ca312b13cf..8e50a7fcd2 100644 --- a/skills/graphic-overlays/SKILL.md +++ b/skills/graphic-overlays/SKILL.md @@ -1152,6 +1152,7 @@ decides where the actual visible card sits. ```bash cd "$WORK_DIR" PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \ + --skill=graphic-overlays \ -o output.mp4 \ --fps 30 ``` diff --git a/skills/motion-graphics/SKILL.md b/skills/motion-graphics/SKILL.md index e5466b4fce..39fba133b8 100644 --- a/skills/motion-graphics/SKILL.md +++ b/skills/motion-graphics/SKILL.md @@ -121,7 +121,7 @@ Dispatch a subagent. prompt = full `agents/builder.md` + dispatch context (`shot ### Step 5 — Render (Bash) ```bash -(cd "$PROJECT_DIR" && npx hyperframes render . -q draft -o ./renders/video.mp4) +(cd "$PROJECT_DIR" && npx hyperframes render . --skill=motion-graphics -q draft -o ./renders/video.mp4) # transparent overlay variant: --format webm (or mov) ``` diff --git a/skills/motion-graphics/agents/finalize.md b/skills/motion-graphics/agents/finalize.md index e107074368..5795b191d3 100644 --- a/skills/motion-graphics/agents/finalize.md +++ b/skills/motion-graphics/agents/finalize.md @@ -10,7 +10,7 @@ Snapshot visual QA + one in-place fix pass + render. Dispatched only when Step 6 1. **Snapshots** — `npx hyperframes inspect . --at `; eyeball for: overflow / off-canvas, text collisions, empty frames, wrong content, motion that doesn't read. 2. **One in-place repair pass** — `Edit` `compositions/index.html` for the visible issues. **Never change a fixed `data-duration`** (timing is set upstream; changing it breaks assembly). Re-run `lint`/`inspect`. -3. **Render** — `(cd "$PROJECT_DIR" && npx hyperframes render . -q -o ./renders/video.mp4)` (add `--format webm` for an alpha overlay export). Verify the mp4 exists + duration matches. +3. **Render** — `(cd "$PROJECT_DIR" && npx hyperframes render . --skill=motion-graphics -q -o ./renders/video.mp4)` (add `--format webm` for an alpha overlay export). Verify the mp4 exists + duration matches. ## STOP / escalate diff --git a/skills/music-to-video/SKILL.md b/skills/music-to-video/SKILL.md index 7aa224478b..5c3e4d0dea 100644 --- a/skills/music-to-video/SKILL.md +++ b/skills/music-to-video/SKILL.md @@ -148,7 +148,7 @@ Run the CLI on the **assembled project** — that's the correct unit (the per-fr Inspect at `t=0`, each frame start, the strongest DROP / SURGE, every `hard_stops[].t`, and the final frame. On failure, make the **cheapest safe fix**: edit the offending `compositions/frames/NN-*.html` for a local issue; **re-dispatch that one frame-worker** only when a whole frame must be rebuilt; go back to Step 3 only if the plan is creatively wrong. Never change duration or audio timing to hide a sync issue. Once the gates pass, pause for user review, then render only on approval: ```bash -( cd "$PROJECT_DIR" && npx hyperframes render . -q draft -o renders/video.mp4 --fps 30 ) +( cd "$PROJECT_DIR" && npx hyperframes render . --skill=music-to-video -q draft -o renders/video.mp4 --fps 30 ) ``` **Gate:** `lint` / `validate` / `inspect` passed; the user approved; `renders/video.mp4` exists with audio, duration == `audiomap.audio.duration_sec`. The final reply states the MP4 path and duration. diff --git a/skills/pr-to-video/SKILL.md b/skills/pr-to-video/SKILL.md index e17699422a..84d01789c7 100644 --- a/skills/pr-to-video/SKILL.md +++ b/skills/pr-to-video/SKILL.md @@ -180,7 +180,7 @@ Preview: `npx hyperframes preview` Render only after user approval: -`npx hyperframes render --quality high --output renders/video.mp4` +`npx hyperframes render --skill=pr-to-video --quality high --output renders/video.mp4` Do not rerun `lint`, `validate`, `inspect`, or `snapshot` after rendering unless the user asks. diff --git a/skills/product-launch-video/SKILL.md b/skills/product-launch-video/SKILL.md index 78ce4873c0..24b3b96096 100644 --- a/skills/product-launch-video/SKILL.md +++ b/skills/product-launch-video/SKILL.md @@ -169,7 +169,7 @@ Preview: `npx hyperframes preview` Render only after user approval: -`npx hyperframes render --quality high --output renders/video.mp4` +`npx hyperframes render --skill=product-launch-video --quality high --output renders/video.mp4` Do not rerun `lint`, `validate`, `inspect`, or `snapshot` after rendering unless the user asks. diff --git a/skills/remotion-to-hyperframes/SKILL.md b/skills/remotion-to-hyperframes/SKILL.md index 887432f923..d0cdcbd1ab 100644 --- a/skills/remotion-to-hyperframes/SKILL.md +++ b/skills/remotion-to-hyperframes/SKILL.md @@ -86,7 +86,7 @@ Run the eval harness — [`references/eval.md`](references/eval.md) for the full cd remotion-src && npx remotion render out/baseline.mp4 # Render HF translation -cd ../hf-src && npx hyperframes render --output ../hf.mp4 +cd ../hf-src && npx hyperframes render --skill=remotion-to-hyperframes --output ../hf.mp4 # SSIM diff ../../scripts/render_diff.sh ./remotion-src/out/baseline.mp4 ./hf.mp4 ./diff diff --git a/skills/website-to-video/references/step-6-validate.md b/skills/website-to-video/references/step-6-validate.md index 74390cb4ca..87bedd5093 100644 --- a/skills/website-to-video/references/step-6-validate.md +++ b/skills/website-to-video/references/step-6-validate.md @@ -313,16 +313,16 @@ When rendering, **always specify quality and resolution explicitly.** Don't use ```bash # Standard quality, 1080p landscape (default for most videos) -npx hyperframes render --output renders/.mp4 --quality standard --fps 30 +npx hyperframes render --skill=website-to-video --output renders/.mp4 --quality standard --fps 30 # High quality for final delivery -npx hyperframes render --output renders/.mp4 --quality high --fps 30 +npx hyperframes render --skill=website-to-video --output renders/.mp4 --quality high --fps 30 # Portrait for Instagram Stories / TikTok -npx hyperframes render --output renders/.mp4 --quality standard --fps 30 --resolution portrait +npx hyperframes render --skill=website-to-video --output renders/.mp4 --quality standard --fps 30 --resolution portrait # 4K for premium output -npx hyperframes render --output renders/.mp4 --quality high --fps 30 --resolution 4k +npx hyperframes render --skill=website-to-video --output renders/.mp4 --quality high --fps 30 --resolution 4k ``` **Available options:** From cbd5a3df0d89cdbea39c62e5773f7cc9498c918e Mon Sep 17 00:00:00 2001 From: kiritowoo <295860553+kiritowoo@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:00:10 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(telemetry):=20address=20review=20?= =?UTF-8?q?=E2=80=94=20shared=20slug=20util,=20equals-form=20flag,=20inval?= =?UTF-8?q?id-value=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract the SKILL_SLUG regex + a normalizeSkillSlug() helper into telemetry/skill.ts, shared by the `events` and `render` commands (the regex was duplicated). `render` adopts normalizeSkillSlug (so it now trims the value, matching `events`); `events` references the shared SKILL_SLUG. + unit test. - `render` warns on a non-empty but invalid --skill value (e.g. a camelCase typo) so attribution isn't silently lost — stderr only, never fails the render. - embedded-captions render script: `--skill embedded-captions` -> `--skill=embedded-captions`. On an older CLI that does not declare --skill, the space form leaks the value as a positional and clobbers the project dir (resolveProject fails); the equals form is parsed as a self-delimiting flag and safely ignored. Verified via Node parseArgs(strict:false). Addresses review feedback on the PR (shared util + .trim drift, version-skew safety, invalid-value visibility). --- packages/cli/src/commands/events.ts | 5 +-- packages/cli/src/commands/render.ts | 19 ++++++--- packages/cli/src/telemetry/skill.test.ts | 41 +++++++++++++++++++ packages/cli/src/telemetry/skill.ts | 22 ++++++++++ .../scripts/render-and-composite.sh | 4 +- 5 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/telemetry/skill.test.ts create mode 100644 packages/cli/src/telemetry/skill.ts diff --git a/packages/cli/src/commands/events.ts b/packages/cli/src/commands/events.ts index 7fb466c4f9..53d74c8936 100644 --- a/packages/cli/src/commands/events.ts +++ b/packages/cli/src/commands/events.ts @@ -1,5 +1,6 @@ import { defineCommand } from "citty"; import { trackEvent, flush } from "../telemetry/client.js"; +import { SKILL_SLUG } from "../telemetry/skill.js"; // Skill-usage telemetry endpoint. A skill reports its own invocation/outcome — // ideally from its own bundled script, so it fires deterministically rather @@ -17,10 +18,6 @@ import { trackEvent, flush } from "../telemetry/client.js"; 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: { diff --git a/packages/cli/src/commands/render.ts b/packages/cli/src/commands/render.ts index 1642b2b6fb..77660857dd 100644 --- a/packages/cli/src/commands/render.ts +++ b/packages/cli/src/commands/render.ts @@ -64,6 +64,7 @@ import { } from "../telemetry/events.js"; import { maybePromptRenderFeedback } from "../telemetry/feedback.js"; import { renderJobObservabilityTelemetryPayload } from "../telemetry/renderObservability.js"; +import { normalizeSkillSlug } from "../telemetry/skill.js"; import { bytesToMb } from "../telemetry/system.js"; import { VERSION } from "../version.js"; import { isDevMode } from "../utils/env.js"; @@ -388,12 +389,18 @@ export default defineCommand({ // ── Authoring skill (telemetry attribution) ──────────────────────────── // Optional slug naming the workflow skill that drove this render (e.g. // "product-launch-video"), tagged onto render telemetry for per-skill usage - // breakdowns. Slug-gated so a caller can't push high-cardinality or PII - // strings into the anonymous event stream; a missing/invalid value is omitted. - const authoringSkill = - typeof args.skill === "string" && /^[a-z0-9][a-z0-9-]{0,63}$/.test(args.skill) - ? args.skill - : undefined; + // breakdowns. Slug-gated (shared with the `events` command) so a caller + // can't push high-cardinality or PII strings into the anonymous event + // stream; a missing/invalid value is omitted. + const authoringSkill = normalizeSkillSlug(args.skill); + if (typeof args.skill === "string" && args.skill.trim() !== "" && !authoringSkill) { + // Surface a typo (e.g. camelCase) instead of silently losing attribution. + // Warning only — never fails the render. + process.stderr.write( + `hyperframes: ignoring --skill="${args.skill}" — not a valid slug ` + + "(lowercase letters/digits/hyphens, max 64); this render will be unattributed.\n", + ); + } // ── Validate format ───────────────────────────────────────────────── const formatRaw = args.format ?? "mp4"; diff --git a/packages/cli/src/telemetry/skill.test.ts b/packages/cli/src/telemetry/skill.test.ts new file mode 100644 index 0000000000..dcc3a9120c --- /dev/null +++ b/packages/cli/src/telemetry/skill.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { normalizeSkillSlug } from "./skill.js"; + +describe("normalizeSkillSlug", () => { + it("accepts valid slugs unchanged", () => { + for (const s of [ + "product-launch-video", + "pr-to-video", + "embedded-captions", + "a", + "a1", + "x".repeat(64), + ]) { + expect(normalizeSkillSlug(s)).toBe(s); + } + }); + + it("trims surrounding whitespace", () => { + expect(normalizeSkillSlug(" pr-to-video ")).toBe("pr-to-video"); + }); + + it("drops invalid values (returns undefined)", () => { + for (const s of [ + "", + " ", + "MotionGraphics", + "has space", + "under_score", + "-leading", + "x".repeat(65), + "café", + ]) { + expect(normalizeSkillSlug(s)).toBeUndefined(); + } + }); + + it("drops non-string input", () => { + expect(normalizeSkillSlug(undefined)).toBeUndefined(); + expect(normalizeSkillSlug(123)).toBeUndefined(); + }); +}); diff --git a/packages/cli/src/telemetry/skill.ts b/packages/cli/src/telemetry/skill.ts new file mode 100644 index 0000000000..69408eeb89 --- /dev/null +++ b/packages/cli/src/telemetry/skill.ts @@ -0,0 +1,22 @@ +/** + * Authoring-skill slug helpers, shared by the `events` and `render` commands. + * + * A skill slug names the authoring workflow that drove a telemetry event + * (e.g. "product-launch-video"). Values are slug-gated so a caller can't push + * high-cardinality or PII strings (paths, shell output, free text) into the + * anonymous event stream. + */ + +/** Lowercase slug: starts alphanumeric, then alphanumerics/hyphens, max 64 chars. */ +export const SKILL_SLUG = /^[a-z0-9][a-z0-9-]{0,63}$/; + +/** + * Trim and validate a raw `--skill` value. Returns the slug, or `undefined` + * when the value is missing or not a valid slug (so the telemetry property is + * simply omitted rather than carrying garbage). + */ +export function normalizeSkillSlug(raw: unknown): string | undefined { + if (typeof raw !== "string") return undefined; + const slug = raw.trim(); + return SKILL_SLUG.test(slug) ? slug : undefined; +} diff --git a/skills/embedded-captions/scripts/render-and-composite.sh b/skills/embedded-captions/scripts/render-and-composite.sh index 07f5beff21..c7741d0c74 100755 --- a/skills/embedded-captions/scripts/render-and-composite.sh +++ b/skills/embedded-captions/scripts/render-and-composite.sh @@ -256,9 +256,9 @@ hf_render_dir() { # bash 3.2 (macOS) throws on empty-array expansion under `set -u`, so branch # explicitly instead of splatting an optional --format array. if [[ -n "$fmt" ]]; then - node "$HF_CLI" render --skill embedded-captions --dir "$proj" --fps "$FPS" --format "$fmt" --crf 11 -o "$out" & + node "$HF_CLI" render --skill=embedded-captions --dir "$proj" --fps "$FPS" --format "$fmt" --crf 11 -o "$out" & else - node "$HF_CLI" render --skill embedded-captions --dir "$proj" --fps "$FPS" --crf 11 -o "$out" & + node "$HF_CLI" render --skill=embedded-captions --dir "$proj" --fps "$FPS" --crf 11 -o "$out" & fi local pid=$! start=$SECONDS elapsed while kill -0 "$pid" 2>/dev/null; do