From 17215038596a2ef3d4bbd6f968a4863fc3fb6422 Mon Sep 17 00:00:00 2001 From: umiyosh Date: Fri, 6 Nov 2026 01:07:35 +0900 Subject: [PATCH 1/6] feat(cli): run API calls in parallel, log step times and assemble scene files --- skills/motion-render/scripts/audio.ts | 57 +++++++++-- skills/motion-render/scripts/cli.ts | 55 ++++++---- skills/motion-render/scripts/inorder.ts | 23 +++++ skills/motion-render/scripts/layout.ts | 14 ++- skills/motion-render/scripts/render.ts | 28 +++-- skills/motion-render/scripts/scenes.ts | 68 ++++++++++++ skills/motion-render/scripts/sfx.ts | 19 +++- skills/motion-render/scripts/steps.ts | 131 ++++++++++++++++++++++++ tests/audio.test.ts | 57 ++++++++++- tests/inorder.test.ts | 30 +++++- tests/scenes.test.ts | 118 +++++++++++++++++++++ tests/sfx.test.ts | 25 +++++ tests/steps.test.ts | 103 +++++++++++++++++++ 13 files changed, 681 insertions(+), 47 deletions(-) create mode 100644 skills/motion-render/scripts/scenes.ts create mode 100644 skills/motion-render/scripts/steps.ts create mode 100644 tests/scenes.test.ts create mode 100644 tests/steps.test.ts diff --git a/skills/motion-render/scripts/audio.ts b/skills/motion-render/scripts/audio.ts index 038d91e..cd532b9 100644 --- a/skills/motion-render/scripts/audio.ts +++ b/skills/motion-render/scripts/audio.ts @@ -5,6 +5,7 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process"; import { createRequire } from "node:module"; import { errorMessage, field, readJson } from "./json.ts"; import { limit } from "./limit.ts"; +import { inParallel } from "./inorder.ts"; import type { Script } from "./work.ts"; const API = "https://generativelanguage.googleapis.com/v1beta/models"; @@ -120,15 +121,49 @@ export async function volumeStats( return { mean, max }; } +// A provider's "too many requests" (HTTP 429). Calls run several at once, so a rate limit is a normal +// answer: the call waits `wait` ms and tries again without using up one of its attempts. +export class Busy extends Error { + wait: number; + constructor(message: string, wait: number) { + super(message); + this.wait = wait; + } +} +// Most a call waits on rate limits, in time and in tries, before a 429 counts as a failed attempt. +const BUSY_TOTAL_MS = 180_000; +const BUSY_TRIES = 10; + +// The wait an HTTP 429 asks for: Gemini's RetryInfo ("retryDelay": "23s") or a Retry-After header +// in seconds, else `fallback` ms. Never more than a minute at a time. +export function busyWait(body: unknown, retryAfter: string | null, fallback: number): number { + const details = field(field(body, "error"), "details"); + const delay = Array.isArray(details) + ? details.map((d: unknown) => field(d, "retryDelay")).find((d) => typeof d === "string") + : undefined; + const s = typeof delay === "string" ? parseFloat(delay) : Number(retryAfter ?? NaN); + return Math.min(60_000, Number.isFinite(s) && s >= 0 ? s * 1000 : fallback); +} + // Runs `attempt` up to RETRIES times, logging each failure under `label`, and rethrows the last error. export async function withRetries(label: string, attempt: () => Promise): Promise { let last: unknown; - for (let i = 1; i <= RETRIES; i++) { + let waited = 0, + waits = 0; + for (let i = 1; i <= RETRIES;) { try { return await attempt(); } catch (e) { last = e; + if (e instanceof Busy && waits < BUSY_TRIES && waited + e.wait <= BUSY_TOTAL_MS) { + console.error(`motion-render: ${label} is rate-limited, trying again in ${e.wait / 1000}s`); + waited += e.wait; + waits++; + await new Promise((ok) => setTimeout(ok, e.wait)); + continue; + } console.error(`motion-render: ${label} attempt ${i}/${RETRIES} failed: ${errorMessage(e)}`); + i++; } } throw last; @@ -163,9 +198,10 @@ async function generateAudio(model: string, body: object): Promise