(args: Record<string, unknown>)
| 595 | */ |
| 596 | // fallow-ignore-next-line complexity |
| 597 | async function runRenderBatch(args: Record<string, unknown>): Promise<void> { |
| 598 | const projectDir = args.target as string | undefined; |
| 599 | const batchPath = args.batch as string | undefined; |
| 600 | if (!projectDir || !batchPath) { |
| 601 | console.error( |
| 602 | "[cloudrun render-batch] usage: hyperframes cloudrun render-batch <projectDir> --batch <file.jsonl> --width <px> --height <px>", |
| 603 | ); |
| 604 | process.exit(1); |
| 605 | } |
| 606 | const width = parsePositiveInt(args.width, "--width"); |
| 607 | const height = parsePositiveInt(args.height, "--height"); |
| 608 | if (width === undefined || height === undefined) { |
| 609 | console.error("[cloudrun render-batch] --width and --height are required."); |
| 610 | process.exit(1); |
| 611 | } |
| 612 | const fps = parseIntFlag(args.fps) ?? 30; |
| 613 | if (fps !== 24 && fps !== 30 && fps !== 60) { |
| 614 | console.error(`[cloudrun render-batch] --fps must be 24, 30, or 60; got ${fps}.`); |
| 615 | process.exit(1); |
| 616 | } |
| 617 | if (!existsSync(resolve(batchPath))) { |
| 618 | console.error(`[cloudrun render-batch] batch file not found: ${batchPath}`); |
| 619 | process.exit(1); |
| 620 | } |
| 621 | const entries = parseBatchFile(resolve(batchPath)); |
| 622 | if (entries.length === 0) { |
| 623 | console.error("[cloudrun render-batch] batch file has no entries."); |
| 624 | process.exit(1); |
| 625 | } |
| 626 | |
| 627 | const dryRun = Boolean(args["dry-run"]); |
| 628 | if (dryRun) { |
| 629 | const manifest = entries.map((e, i) => ({ |
| 630 | line: i + 1, |
| 631 | outputKey: e.outputKey, |
| 632 | status: "would-start", |
| 633 | })); |
| 634 | console.log(JSON.stringify(manifest, null, 2)); |
| 635 | return; |
| 636 | } |
| 637 | |
| 638 | const state = readState(args); |
| 639 | const maxConcurrent = |
| 640 | parsePositiveInt(args["max-concurrent"], "--max-concurrent") ?? DEFAULT_BATCH_MAX_CONCURRENT; |
| 641 | const { deploySite, renderToCloudRun } = await import("@hyperframes/gcp-cloud-run/sdk"); |
| 642 | |
| 643 | // Upload the project once; every entry reuses the same content-addressed |
| 644 | // site handle so the tar+upload cost is paid a single time. |
| 645 | const siteHandle = await deploySite({ |
| 646 | projectDir: resolve(projectDir), |
| 647 | bucketName: state.bucketName, |
| 648 | siteId: args["site-id"] as string | undefined, |
| 649 | }); |
| 650 | |
| 651 | const results: Array<{ outputKey: string; executionName?: string; error?: string }> = []; |
| 652 | // Start executions in fixed-size waves so we never exceed `maxConcurrent` |
| 653 | // in-flight CreateExecution calls. |
| 654 | for (let i = 0; i < entries.length; i += maxConcurrent) { |
no test coverage detected