mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user