mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
feat(skills): keep a generate call under the person's credit limit
media-use generate prices each call in HyperFrames credits (an estimate at the backend's 1,000 credits per list dollar) and --max-credits refuses a call that would go past what is left of the limit, before anything is sent. media-use and reverse-any-video pass the limit only when the person names one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a0f6ca7469
commit
4b70b52eaa
@@ -10,7 +10,7 @@ import {
|
||||
type GenerateRequest,
|
||||
type GenerateType,
|
||||
} from "../../heygen/generate.js";
|
||||
import { priceOf } from "../../heygen/pricing.js";
|
||||
import { checkLimit, creditsOf, priceOf } from "../../heygen/pricing.js";
|
||||
|
||||
export const examples: Example[] = [
|
||||
[
|
||||
@@ -99,6 +99,11 @@ export default defineCommand({
|
||||
description: "music: allow singing (default instrumental)",
|
||||
default: false,
|
||||
},
|
||||
"max-credits": {
|
||||
type: "string",
|
||||
description:
|
||||
"The person's credit limit, less what earlier calls used: make nothing when this call would cost more",
|
||||
},
|
||||
plan: {
|
||||
type: "boolean",
|
||||
description: "Print the price and what would be sent; make nothing",
|
||||
@@ -133,6 +138,7 @@ export default defineCommand({
|
||||
instrumental: !args.vocals,
|
||||
};
|
||||
const price = priceOf(request);
|
||||
const credits = creditsOf(price);
|
||||
if (args.plan) {
|
||||
checkGuide(request);
|
||||
return {
|
||||
@@ -140,11 +146,13 @@ export default defineCommand({
|
||||
type,
|
||||
out: request.out,
|
||||
price,
|
||||
credits,
|
||||
next: "after the person agrees to this price, run the same command without --plan",
|
||||
};
|
||||
}
|
||||
checkLimit(credits, num(args["max-credits"], "--max-credits"));
|
||||
const made = await generate(request, (status) => console.error(`generate: ${status}…`));
|
||||
return { ...made, price };
|
||||
return { ...made, price, credits };
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ vi.mock("../cloud/auth.js", () => ({
|
||||
}));
|
||||
|
||||
const { generate, parseMask, urlOf } = await import("./generate.js");
|
||||
const { priceOf } = await import("./pricing.js");
|
||||
const { checkLimit, creditsOf, priceOf } = await import("./pricing.js");
|
||||
const ffmpeg = findFFmpeg();
|
||||
const project = mkdtempSync(join(tmpdir(), "hf-generate-test-"));
|
||||
writeFileSync(join(project, "index.html"), "<html></html>");
|
||||
@@ -331,4 +331,13 @@ describe("generate", () => {
|
||||
).toBe(0.07);
|
||||
expect(priceOf({ type: "voice", out: "a.mp3" }).usd).toBeNull();
|
||||
});
|
||||
|
||||
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(120);
|
||||
expect(() => checkLimit(shot, 120)).not.toThrow();
|
||||
expect(() => checkLimit(shot, 30)).toThrow("about 120 credits, over --max-credits 30");
|
||||
expect(() => checkLimit(null, 30)).toThrow("cannot be priced ahead");
|
||||
expect(() => checkLimit(null, undefined)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Prices before a paid call, from the HyperFrames app's creation pricing (hyperframes-internal
|
||||
// packages/desktop/shared/creationPricing.mjs, 2026-10-22). HeyGen charges in units of $1/60, rounded up; the label
|
||||
// rounds up to the cent. Routes HeyGen does not price ahead say so instead of guessing.
|
||||
import { probeVideo, round } from "../measure/media.js";
|
||||
import { Fix, probeVideo, round } from "../measure/media.js";
|
||||
import { GUIDE_MAX_S, type GenerateRequest } from "./generate.js";
|
||||
|
||||
export interface Price {
|
||||
@@ -73,3 +73,27 @@ export function priceOf(r: GenerateRequest): Price {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// HyperFrames price rows are written at 1,000 credits per dollar of list price (experiment-framework
|
||||
// heygen/services/payments/hyperframes/catalog.py, HF_CREDITS_PER_USD), so this is an estimate, not the price book.
|
||||
const HF_CREDITS_PER_USD = 1000;
|
||||
|
||||
export const creditsOf = (p: Price): number | null =>
|
||||
p.usd === null ? null : Math.ceil(p.usd * HF_CREDITS_PER_USD - 1e-9);
|
||||
|
||||
/** Refuses, before anything is sent, a call that would not fit what is left of the person's credit limit. */
|
||||
export function checkLimit(credits: number | null, limit: number | undefined): void {
|
||||
if (limit === undefined) return;
|
||||
const fix =
|
||||
"tell the person what their limit leaves out, then ask whether to raise it or make the video without this piece";
|
||||
if (credits === null)
|
||||
throw new Fix(
|
||||
`this call cannot be priced ahead, so it cannot be kept under --max-credits ${limit}; nothing was made`,
|
||||
fix,
|
||||
);
|
||||
if (credits > limit)
|
||||
throw new Fix(
|
||||
`this call is about ${credits} credits, over --max-credits ${limit}; nothing was made or charged`,
|
||||
fix,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"files": 13
|
||||
},
|
||||
"media-use": {
|
||||
"hash": "1a7d1b228edc28b9",
|
||||
"hash": "66f323c1f995aa23",
|
||||
"files": 117
|
||||
},
|
||||
"remotion-to-hyperframes": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"files": 76
|
||||
},
|
||||
"reverse-any-video": {
|
||||
"hash": "b7a34caab79eadce",
|
||||
"hash": "3ede55ed2d1eaa54",
|
||||
"files": 4
|
||||
},
|
||||
"slideshow": {
|
||||
|
||||
@@ -35,7 +35,7 @@ It prints one line, `resolved <id> → <path> (<type>, <metadata>)`. List reusab
|
||||
|
||||
**Generate.** `npx hyperframes media-use generate` makes images, shots, talking photos, voices and music (`references/heygen.md`).
|
||||
|
||||
**Credits.** Plan for the best result; don't swap in cheaper or free options, or drop items, to save credits 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 refused call names a `billing` command: tell the person why, ask them to run it the same way, then retry the step. `--provider <name>` pins a generator the person names. `--local-only` skips every network provider.
|
||||
**Credits.** Plan for the best result; don't swap in cheaper or free options, or drop items, to save credits 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. `--provider <name>` pins a generator the person names. `--local-only` skips every network provider.
|
||||
|
||||
## Looks on real footage
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ A refusal names its fix: `api_key_required` needs an API key, `402` or `insuffic
|
||||
|
||||
## Making media: `media-use generate`
|
||||
|
||||
Make it without quoting a price or asking for a budget; `--plan` only prints what a call would cost, for your own planning; never pass it on. Make a piece again only for a fault you can name (a cut inside it, text in it, the wrong framing), once, never to chase a motion or look score; a `content_policy_violation` gets one reworded try.
|
||||
Make it without quoting a price or asking for a budget; `--plan` only prints what a call would cost (`credits` is an estimate in HyperFrames credits), for your own planning; never pass it on. When the person names a credit limit, keep under it with `--max-credits` (SKILL.md, Credits). Make a piece again only for a fault you can name (a cut inside it, text in it, the wrong framing), once, never to chase a motion or look score; a `content_policy_violation` gets one reworded try.
|
||||
|
||||
```bash
|
||||
# a still, edited from a reference picture: a clean plate with the words removed
|
||||
|
||||
@@ -15,6 +15,8 @@ Every person on screen, speaking or not, is re-shot with HeyGen so the original'
|
||||
|
||||
`--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`.
|
||||
|
||||
A credit limit the person named: price every shot first with `--plan`, keep the people that fit, pass what is left as `--max-credits`, and ask before cutting a person to stay under it.
|
||||
|
||||
4. **Check each shot as it arrives:** no cut inside it (generate prints `cuts_inside`, and refuses a guide with a cut), none of the original's text, the face box within 5% of the original's (`npx hyperframes measure face`). Regenerate a failing shot once; then trim it to its good part.
|
||||
5. **Frame it:** scale and move each shot so its face box sits on the original's at the shot's start and end (`npx hyperframes measure face <shot> --against <video> --from a --to b` prints the `scale`, `x`, `y` to set), tweening between them only where the original's camera moves. The framing jumps at cuts and nowhere else.
|
||||
6. **Text behind the person:** where the original's text passes behind them, cut them out of the shot (`npx hyperframes remove-background <shot> -o <cutout>.webm`) and stack shot, text, cutout.
|
||||
|
||||
Reference in New Issue
Block a user