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.
This commit is contained in:
Miao Yang
2026-10-30 15:22:49 +08:00
parent 442ef728a3
commit 9faf94c1af
8 changed files with 35 additions and 13 deletions
@@ -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,
+16 -1
View File
@@ -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);
+8 -1
View File
@@ -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<GenerateRequest, "motion">): void {
function checkGuide(r: Pick<GenerateRequest, "motion">): 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<GenerateRequest, "motion">): 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)) {
+2 -2
View File
@@ -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": {
+1 -1
View File
@@ -35,7 +35,7 @@ It prints one line, `resolved <id> → <path> (<type>, <metadata>)`. 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
+2 -2
View File
@@ -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`
@@ -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
@@ -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 <plate> --motion <the original shot> \
--mask <x,y,w,h,a-b> --audio <the original voice over that shot> --aspect <the source's> --seconds <n> --out <shot>.mp4
--mask <x,y,w,h,a-b> --audio <the original voice over that shot> --aspect <the source's> --seconds <n> \
--prompt "<who, where, what they do, the camera>" --out <shot>.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`.