diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 8546c740f66..373150f334e 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -2076,6 +2076,7 @@ Render parameters mirror local `hyperframes render` where they overlap: | `--fps` | `30` | Integer, 1–240 | | `--quality` | `standard` | `draft`, `standard`, or `high` | | `--format` | `mp4` | `mp4`, `webm`, or `mov` | +| `--page-motion-blur` | current cloud behavior | Pass `off` to disable authored page motion blur. Omit to preserve current behavior. Sends the public API field `page_motion_blur: "off"`. | | `--resolution` | `1080p` | `1080p` or `4k`. 4k bills at 1.5×. | | `--aspect-ratio` | auto | `16:9`, `9:16`, or `1:1`. Auto-detected from a local project's `data-width` and `data-height`, so you rarely need it. The renderer cannot reshape a composition — a value that disagrees with the authored ratio is an error. With `--asset-id` or `--url` the server defaults to `16:9`. | | `--composition` / `-c` | `index.html` | Entry HTML file inside the zip | diff --git a/packages/aws-lambda/src/sdk/validateConfig.test.ts b/packages/aws-lambda/src/sdk/validateConfig.test.ts index 8edba86ae16..4806691e007 100644 --- a/packages/aws-lambda/src/sdk/validateConfig.test.ts +++ b/packages/aws-lambda/src/sdk/validateConfig.test.ts @@ -31,6 +31,7 @@ describe("validateDistributedRenderConfig", () => { codec: "h265", quality: "high", crf: 18, + pageMotionBlur: "off", chunkSize: 240, maxParallelChunks: 16, runtimeCap: "lambda", @@ -42,6 +43,11 @@ describe("validateDistributedRenderConfig", () => { it.each([ ["null config", null as unknown as SerializableDistributedRenderConfig, "config"], + [ + "unsupported page motion blur", + { ...VALID, pageMotionBlur: "on" as never } satisfies SerializableDistributedRenderConfig, + "config.pageMotionBlur", + ], [ "wrong fps", { ...VALID, fps: 50 as never } satisfies SerializableDistributedRenderConfig, diff --git a/packages/cli/src/cloud/_gen/client.ts b/packages/cli/src/cloud/_gen/client.ts index d4b523fd4de..b8863c6de5c 100644 --- a/packages/cli/src/cloud/_gen/client.ts +++ b/packages/cli/src/cloud/_gen/client.ts @@ -204,7 +204,7 @@ export class HyperframesCloudClient { /** * Upload Asset * - * Uploads a file (image, video, audio, PDF, or SRT subtitle) and returns an asset_id for use in other endpoints. Max 32 MB. Supported types: png, jpeg, mp4, webm, mp3, wav, pdf, srt. + * Uploads a file (image, video, audio, PDF, or SRT subtitle) and returns an asset_id for use in other endpoints. Max 50 MB, or 32 MB for an image. Supported types: png, jpeg, mp4, webm, mp3, wav, pdf, srt. A file is checked against the content policy after it is uploaded; if it is blocked, the asset is removed and later requests for it return not found. */ async uploadAsset(args: { file: Blob | Buffer | Uint8Array; diff --git a/packages/cli/src/cloud/_gen/types.ts b/packages/cli/src/cloud/_gen/types.ts index 999e31001d6..c2a1d717798 100644 --- a/packages/cli/src/cloud/_gen/types.ts +++ b/packages/cli/src/cloud/_gen/types.ts @@ -174,6 +174,16 @@ export interface CreateHyperframesRenderRequest { * Output container/codec. */ format?: "mp4" | "webm" | "mov"; + /** + * Overrides the quality preset's constant rate factor (lower is higher quality + * and a larger file). Integer 0-51 for mp4, 0-63 for webm; ignored for mov. + * Omit to use the preset's CRF. + */ + crf?: number | null; + /** + * Set 'off' to disable page motion blur. Omit or null to keep behavior. + */ + page_motion_blur?: "off" | null; /** * Output resolution tier. Defaults to '1080p'. Pass '4k' for 4K renders * (billed at 1.5x). @@ -305,6 +315,13 @@ export interface HyperframesRenderDetail { * Error description. Present only when status is 'failed'. */ failure_message?: string | null; + /** + * Why billing refused a finished render: 'hf_usage_exhausted', + * 'hf_subscription_required' or 'api_key_required'. Present only when status + * is 'failed' for that reason; nothing was charged, so the render can be sent + * again on a HeyGen API key. + */ + failure_code?: string | null; } /** @@ -339,6 +356,11 @@ export interface StandardAPIError { * errors by node). */ errors?: Array; + /** + * Request identifier, also returned in the X-HeyGen-Request-Id response + * header. Include it when contacting support. + */ + request_id?: string; } export interface StandardAPIErrorDetail { diff --git a/packages/cli/src/commands/cloud/render.pageMotionBlur.test.ts b/packages/cli/src/commands/cloud/render.pageMotionBlur.test.ts new file mode 100644 index 00000000000..4bcd223a4a9 --- /dev/null +++ b/packages/cli/src/commands/cloud/render.pageMotionBlur.test.ts @@ -0,0 +1,52 @@ +import { runCommand } from "citty"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import render from "./render.js"; +import { CliUsageError, consumeCommandResult } from "../../utils/commandResult.js"; + +const bodies: unknown[] = []; +const baseArgs = ["--asset-id", "asst_fixture", "--no-wait", "--json"]; + +beforeEach(() => { + bodies.length = 0; + vi.stubEnv("HEYGEN_API_KEY", "fixture-key"); + vi.stubEnv("HEYGEN_API_URL", "https://cloud.fixture.invalid"); + vi.spyOn(console, "log").mockImplementation(() => undefined); + vi.spyOn(console, "error").mockImplementation(() => undefined); + vi.stubGlobal("fetch", async (url: unknown, init?: RequestInit) => { + expect(String(url)).toBe("https://cloud.fixture.invalid/v3/hyperframes/renders"); + expect(init?.method).toBe("POST"); + if (typeof init?.body !== "string") throw new Error("Expected a JSON submit body"); + const body: unknown = JSON.parse(init.body); + bodies.push(body); + return Response.json({ data: { render_id: "hfr_fixture" } }, { status: 202 }); + }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); + vi.restoreAllMocks(); + consumeCommandResult(); +}); + +describe("cloud render page motion blur", () => { + it("writes explicit off into the public API request", async () => { + await runCommand(render, { rawArgs: [...baseArgs, "--page-motion-blur", "off"] }); + expect(bodies).toHaveLength(1); + expect(bodies[0]).toHaveProperty("page_motion_blur", "off"); + expect(bodies[0]).not.toHaveProperty("pageMotionBlur"); + }); + + it("omits the override when the flag is absent", async () => { + await runCommand(render, { rawArgs: baseArgs }); + expect(bodies).toHaveLength(1); + expect(bodies[0]).not.toHaveProperty("page_motion_blur"); + }); + + it.each(["on", "false", "", "OFF"])("rejects %j before any cloud request", async (value) => { + await expect( + runCommand(render, { rawArgs: [...baseArgs, "--page-motion-blur", value] }), + ).rejects.toThrow(CliUsageError); + expect(bodies).toHaveLength(0); + }); +}); diff --git a/packages/cli/src/commands/cloud/render.ts b/packages/cli/src/commands/cloud/render.ts index a43dce08030..87d64ba2393 100644 --- a/packages/cli/src/commands/cloud/render.ts +++ b/packages/cli/src/commands/cloud/render.ts @@ -118,6 +118,11 @@ export default defineCommand({ fps: { type: "string", description: "Frames per second (1-240). Default: 30." }, quality: { type: "string", description: "draft | standard | high (default: standard)" }, format: { type: "string", description: "mp4 | webm | mov (default: mp4)" }, + "page-motion-blur": { + type: "string", + description: + "Set off to disable authored page motion blur. Omit to keep the current cloud behavior.", + }, resolution: { type: "string", description: "Resolution tier: 1080p | 4k (default: 1080p; 4k is billed at 1.5x)", @@ -214,6 +219,9 @@ export default defineCommand({ const fps = parseIntFlag(args.fps, { flag: "--fps", min: 1, max: 240 }); const quality = parseEnumFlag(args.quality, VALID_QUALITY, { flag: "--quality" }); const format = parseEnumFlag(args.format, VALID_FORMAT, { flag: "--format" }); + const pageMotionBlur = parseEnumFlag(args["page-motion-blur"], ["off"] as const, { + flag: "--page-motion-blur", + }); const resolution = parseEnumFlag(args.resolution, VALID_RESOLUTION, { flag: "--resolution", }); @@ -282,6 +290,7 @@ export default defineCommand({ quality, format, resolution, + pageMotionBlur, aspectRatio, composition: args.composition, variables, @@ -802,6 +811,7 @@ interface SubmitOptions { fps: number | undefined; quality: "draft" | "standard" | "high" | undefined; format: "mp4" | "webm" | "mov" | undefined; + pageMotionBlur: NonNullable | undefined; resolution: CreateHyperframesRenderRequest["resolution"] | undefined; aspectRatio: CreateHyperframesRenderRequest["aspect_ratio"] | undefined; composition: string | undefined; @@ -830,6 +840,7 @@ function buildRenderBody(opts: SubmitOptions): CreateHyperframesRenderRequest { if (opts.fps !== undefined) body.fps = opts.fps; if (opts.quality !== undefined) body.quality = opts.quality; if (opts.format !== undefined) body.format = opts.format; + if (opts.pageMotionBlur !== undefined) body.page_motion_blur = opts.pageMotionBlur; if (opts.resolution !== undefined) body.resolution = opts.resolution; if (opts.aspectRatio !== undefined) body.aspect_ratio = opts.aspectRatio; if (opts.composition !== undefined) body.composition = opts.composition; diff --git a/packages/producer/README.md b/packages/producer/README.md index 515ae7692f5..3367179ce88 100644 --- a/packages/producer/README.md +++ b/packages/producer/README.md @@ -18,18 +18,13 @@ npm install @hyperframes/producer import { createRenderJob, executeRenderJob } from "@hyperframes/producer"; const job = createRenderJob({ - inputPath: "./my-composition.html", - outputPath: "./output.mp4", - width: 1920, - height: 1080, fps: 30, + quality: "standard", }); -const result = await executeRenderJob(job, (progress) => { - console.log(`${Math.round(progress.percent * 100)}%`); +await executeRenderJob(job, "./my-project", "./output.mp4", (currentJob) => { + console.log(`${Math.round(currentJob.progress)}%`); }); - -console.log(result.outputPath); // ./output.mp4 ``` ### Run as an HTTP server @@ -40,24 +35,27 @@ The producer can also run as a render server, accepting render requests over HTT import { startServer } from "@hyperframes/producer"; await startServer({ port: 8080 }); -// POST /render with a RenderConfig body +// POST /render or /render/stream with projectDir, html, or previewUrl and render options ``` +Both HTTP routes accept optional `pageMotionBlur: "off"` to disable authored page +motion blur. Omit it to preserve the composition's behavior. Other explicit values +are rejected before a render starts. This is separate from engine sub-frame +`motionBlur` sampling. + ### Configuration `RenderConfig` controls the render pipeline: -| Option | Default | Description | -| ------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `inputPath` | — | Path to the HTML composition | -| `outputPath` | — | Output video file path (or directory, for `format: "png-sequence"` and `format: "hls"`) | -| `width` | 1920 | Frame width in pixels | -| `height` | 1080 | Frame height in pixels | -| `fps` | 30 | Frames per second (24, 30, or 60) | -| `quality` | `"standard"` | Encoder preset (`"draft"`, `"standard"`, `"high"`) | -| `format` | `"mp4"` | Output container — `"mp4"`, `"webm"`, `"mov"`, `"gif"`, `"png-sequence"`, or `"hls"`. See [Transparent Video Output](#transparent-video-output) and [HLS Output](#hls-output) below. | -| `hlsSegmentSeconds` | `4` | HLS target segment length in whole seconds. Ignored for every other format. | -| `videoFrameFormat` | `"auto"` | Source video frame extraction format — `"auto"`, `"jpg"`, or `"png"`. Use `"png"` for UI recordings, screen captures, and color-sensitive source videos. | +| Option | Default | Description | +| ------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `entryFile` | `index.html` | Entry composition inside the project directory. Its authored dimensions set the canvas size. | +| `fps` | required | Frame rate as an integer or exact `{ num, den }` rational. | +| `quality` | required | Encoder preset (`"draft"`, `"standard"`, `"high"`). | +| `pageMotionBlur` | authored setting | Set `"off"` to disable page motion blur; omit to preserve authored behavior. | +| `format` | `"mp4"` | Output container — `"mp4"`, `"webm"`, `"mov"`, `"gif"`, `"png-sequence"`, or `"hls"`. See [Transparent Video Output](#transparent-video-output) and [HLS Output](#hls-output) below. | +| `hlsSegmentSeconds` | `4` | HLS target segment length in whole seconds. Ignored for every other format. | +| `videoFrameFormat` | `"auto"` | Source video frame extraction format — `"auto"`, `"jpg"`, or `"png"`. Use `"png"` for UI recordings, screen captures, and color-sensitive source videos. | ## Transparent Video Output @@ -68,7 +66,7 @@ The producer can render HTML compositions to formats that carry a **true alpha c | `"mp4"` (default) | H.264 (yuv420p) or H.265 + HDR10 | No | AAC | Streaming, social, default deliverable | | `"webm"` | VP9 + yuva420p | **True alpha** | Opus | Web playback as overlay (`