From 9faf94c1af3e6b9bb77ee49933c888b4ba782708 Mon Sep 17 00:00:00 2001 From: Miao Yang Date: Fri, 30 Oct 2026 15:22:49 +0800 Subject: [PATCH] fix(skills,cli): one credits rule; ask before dropping a step that won't fit On dev with a 100-credit limit the agent priced a re-shoot with --plan and dropped it without asking (J5), and in J6 it told the person a price. SKILL.md's Credits rule now covers both: --plan is for the agent, and when a step won't fit (a --plan over what is left, or a refused call) the agent says what doesn't fit and asks before dropping or shrinking it. setup-providers.md's older confirm-before-paying rule points to it. --plan now runs the same checks as the real call (checkRequest), so it no longer prices a request that would be refused. The re-shoot recipe in people.md gains the --prompt it needs. heygen.md: a sign-in also makes images, and --plan's usd is the API charge rounded to $1/60 while its credits come from the list price. --- packages/cli/src/commands/media-use/generate.ts | 4 ++-- packages/cli/src/heygen/generate.test.ts | 17 ++++++++++++++++- packages/cli/src/heygen/generate.ts | 9 ++++++++- skills-manifest.json | 4 ++-- skills/media-use/SKILL.md | 2 +- skills/media-use/references/heygen.md | 4 ++-- skills/media-use/references/setup-providers.md | 5 ++--- skills/reverse-any-video/references/people.md | 3 ++- 8 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/commands/media-use/generate.ts b/packages/cli/src/commands/media-use/generate.ts index 9480e712ce..f0d92a2647 100644 --- a/packages/cli/src/commands/media-use/generate.ts +++ b/packages/cli/src/commands/media-use/generate.ts @@ -4,7 +4,7 @@ import { jsonArg, num, runMeasure } from "../../measure/command.js"; import { Fix } from "../../measure/media.js"; import { GENERATE_TYPES, - checkGuide, + checkRequest, generate, parseMask, type GenerateRequest, @@ -140,7 +140,7 @@ export default defineCommand({ const price = priceOf(request); const credits = creditsOf(price); if (args.plan) { - checkGuide(request); + checkRequest(request); return { plan: true, type, diff --git a/packages/cli/src/heygen/generate.test.ts b/packages/cli/src/heygen/generate.test.ts index 00924fa6ad..83dec22dff 100644 --- a/packages/cli/src/heygen/generate.test.ts +++ b/packages/cli/src/heygen/generate.test.ts @@ -10,7 +10,7 @@ vi.mock("../cloud/auth.js", () => ({ resolveCloudBaseUrl: () => "https://api.test", })); -const { generate, parseMask, urlOf } = await import("./generate.js"); +const { checkRequest, generate, parseMask, urlOf } = await import("./generate.js"); const { checkLimit, creditsOf, priceOf } = await import("./pricing.js"); const ffmpeg = findFFmpeg(); const project = mkdtempSync(join(tmpdir(), "hf-generate-test-")); @@ -332,6 +332,21 @@ describe("generate", () => { expect(priceOf({ type: "voice", out: "a.mp3" }).usd).toBeNull(); }); + it("prices only a request the real call would accept", () => { + // A re-shoot without --prompt used to get a price from --plan, then fail when sent. + const shot = { + type: "video" as const, + out: "a.mp4", + references: ["plate.png"], + audio: "voice.m4a", + }; + expect(() => checkRequest(shot)).toThrow("--prompt is required for this type"); + expect(() => checkRequest({ ...shot, prompt: "she talks to the camera" })).not.toThrow(); + expect(() => checkRequest({ ...shot, prompt: "x", firstFrame: "f.png" })).toThrow( + "--first-frame cannot be used", + ); + }); + it("keeps a call under the person's credit limit, or refuses it before sending", () => { const shot = creditsOf(priceOf({ type: "video", out: "a.mp4", seconds: 6 })); expect(shot).toBe(108); diff --git a/packages/cli/src/heygen/generate.ts b/packages/cli/src/heygen/generate.ts index 3ba455d9b5..bdde8ea1ce 100644 --- a/packages/cli/src/heygen/generate.ts +++ b/packages/cli/src/heygen/generate.ts @@ -280,7 +280,7 @@ const COUNT = ["", "one", "two", "three", "four", "five", "six"]; * Refuses a --motion clip holding more than one shot before anything is sent: HeyGen follows the guide's cut into * a cut of its own, so each shot is its own generation. */ -export function checkGuide(r: Pick): void { +function checkGuide(r: Pick): void { if (!r.motion) return; const { cuts, length } = cutsInside(r.motion, GUIDE_MAX_S); if (!cuts.length) return; @@ -296,6 +296,13 @@ export function checkGuide(r: Pick): void { ); } +/** What the real call would refuse, checked without sending anything: `--plan` prices only a request that would run. + * The route's body runs with a stand-in for the media, so its required flags and conflicts are checked as sent. */ +export function checkRequest(r: GenerateRequest): void { + ROUTES[r.type].body(r, () => ({ type: "base64", media_type: "", data: "" })); + checkGuide(r); +} + /** The nearest folder at or above `from` holding index.html. */ function projectOf(from: string): string | null { for (let dir = resolve(from); ; dir = dirname(dir)) { diff --git a/skills-manifest.json b/skills-manifest.json index b209ee5f9f..487949f27d 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,7 +18,7 @@ "files": 13 }, "media-use": { - "hash": "6af3426f9dc2809f", + "hash": "83a92e63c25225c6", "files": 117 }, "remotion-to-hyperframes": { @@ -26,7 +26,7 @@ "files": 76 }, "reverse-any-video": { - "hash": "515f16c74b1116fc", + "hash": "492e250fb5107be3", "files": 4 }, "slideshow": { diff --git a/skills/media-use/SKILL.md b/skills/media-use/SKILL.md index 108efa9905..becb55e912 100644 --- a/skills/media-use/SKILL.md +++ b/skills/media-use/SKILL.md @@ -35,7 +35,7 @@ It prints one line, `resolved → (, )`. List reusab **HeyGen.** HeyGen calls also need the [`heygen` CLI](https://developers.heygen.com/cli); if it is missing, give the link and wait until they say it is installed. `npx hyperframes media-use generate` makes images, shots, talking photos, voices and music. The direct `heygen` recipes bill the HeyGen CLI's own login, not HyperFrames: use them only when that separate payer is selected. -**Credits.** Plan for the best result: don't swap in cheaper or free options, or drop items, unless the person asks to save cost. Never tell the person prices, estimates or what a run used; money comes up only when a call is refused. A credit limit the person sets is the one exception: pass what is left of it as `--max-credits` on every `generate`, subtract each call's `credits`, and when a call is refused for the limit, say what does not fit and ask; never go past it. A refused call names a `billing` command: tell the person why, ask them to run it the same way, then retry the step. +**Credits.** Plan for the best result: don't swap in cheaper or free options, or drop items, unless the person asks to save cost. Never tell the person prices, estimates or what a run used; `--plan` is for you. A credit limit the person sets: pass what is left of it as `--max-credits` on every `generate`, minus each call's `credits`, and never go past it. When a step won't fit (a `--plan` over what is left, or a refused call), say what doesn't fit and ask before dropping or shrinking it. A refused call names a `billing` command: tell the person why, ask them to run it the same way, then retry the step. ## Looks on real footage diff --git a/skills/media-use/references/heygen.md b/skills/media-use/references/heygen.md index 737622c969..1f7ff27fcb 100644 --- a/skills/media-use/references/heygen.md +++ b/skills/media-use/references/heygen.md @@ -9,7 +9,7 @@ | Credential | Pays with | Covers | | -------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | API key | API credits | everything below | -| Sign-in (OAuth) | the plan's allowance on subscription credits | avatar videos, video shots and voices; images, music, transcription and video analysis take an API key | +| Sign-in (OAuth) | the plan's allowance on subscription credits | avatar videos, video shots, images and voices; music, transcription and video analysis take an API key | | Inside the HyperFrames app | the app | whatever the app allows; nothing to set up | A refusal names its fix: `api_key_required` needs an API key, `402` or `insufficient_credit` needs a top-up, `hf_usage_exhausted` means the plan's allowance is used up. @@ -47,7 +47,7 @@ The file lands at `--out` and the media ledger records it as generated, with the | music | lyria-3-pro | $0.08 a track | | voice | the voice's engine | charged per character, not priced ahead | -HeyGen charges in units of $1/60, rounded up; `--plan` prints that charged price. Voice ids come from `heygen voice list` or `npx hyperframes media-use resolve --type voice`; a cloned voice from `heygen voice clone create` (`operations.md`). +`--plan`'s `usd` is what an API key is charged (HeyGen rounds up to units of $1/60); its `credits` is what a HyperFrames plan charges, from the list price, with no such rounding. Voice ids come from `heygen voice list` or `npx hyperframes media-use resolve --type voice`; a cloned voice from `heygen voice clone create` (`operations.md`). ## Transcription: `transcribe --engine heygen` diff --git a/skills/media-use/references/setup-providers.md b/skills/media-use/references/setup-providers.md index be05a5c844..05bebae682 100644 --- a/skills/media-use/references/setup-providers.md +++ b/skills/media-use/references/setup-providers.md @@ -52,9 +52,8 @@ in `references/operations.md`). Local Kokoro (voice), mflux (image), and LTX (video) run on-device (free, private, offline once cached). The `codex` CLI remains the ChatGPT-sub image -upsell. Cost rule (X4): the agent confirms before an agent-initiated paid call; -a user-requested one just runs — `heygen.video` is flagged paid (metered free -allowance) so an agent-initiated `resolve --type video` confirms first. +upsell. Paid calls follow the Credits rule in `SKILL.md`: no prices unasked, +and a question only when a step won't fit. To force a specific generator (e.g. a user says "make this image with codex"), pass `--provider codex`: it pins resolution to that provider and skips the diff --git a/skills/reverse-any-video/references/people.md b/skills/reverse-any-video/references/people.md index 03fe38b36a..301e06504a 100644 --- a/skills/reverse-any-video/references/people.md +++ b/skills/reverse-any-video/references/people.md @@ -10,7 +10,8 @@ Every person on screen, speaking or not, is re-shot with HeyGen so the original' ```bash npx hyperframes media-use generate --type video --reference --motion \ - --mask --audio --aspect --seconds --out .mp4 + --mask --audio --aspect --seconds \ + --prompt "" --out .mp4 ``` `--motion` is the original shot itself, cut at its cut times: HeyGen follows its body and camera motion and takes the face from the plate. It also copies text burned into the guide, so give one `--mask` per drawn box in that shot (the clip's pixels and seconds). Mask the text's own box, never a full-width band: a gesture under a mask is lost. A person who does not speak gets no `--audio`.