/** * Overnight batch video queue. * * A "batch" is a JSON manifest the operator authors listing many independent * video jobs. It compiles into ONE {@link VideoExecutionPayload} with N tasks, * which is exactly the shape the native route transports (native-runway, * native-dreamina, native-seedance) already accept and loop over. This module * is pure/deterministic apart from the small fs helpers at the bottom: * * - readBatchManifest() — graceful parse, throws on malformed (no silent fallback) * - buildBatchPayload() — pure: manifest -> VideoExecutionPayload * - queue-state helpers — persist /batch-queue.json, idempotent rollup * - clip mapping helpers — scene-.mp4 -> clips/.mp4 * * The default route is the FREE runway-useapi explore mode, so a large queue * can run unattended overnight at zero credit cost (low-res/slow drafts). */ import type { VideoExecutionPayload, VideoExecutionTask } from './types.js'; /** Routes the batch queue can target. A subset of ProviderRouteId — the three * routes with in-process native submit/poll transports that loop payload.tasks. */ export type BatchRouteId = 'runway-useapi' | 'dreamina-useapi' | 'seedance-direct'; export interface BatchQueueJob { /** Stable, operator-chosen id. Used as the downloaded clip filename. */ id: string; prompt: string; /** Optional first-frame keyframe — a local path or a public http(s) URL. */ keyframe?: string; /** * Optional END keyframe — a local path or public http(s) URL. With `keyframe` * set, the clip animates from `keyframe` (first frame) to `endKeyframe` (last * frame) via Seedance-2 keyframe interpolation, turning two stills into one * continuous shot. Requires `keyframe`; mutually exclusive with * `characterRefs`. */ endKeyframe?: string; /** * Optional character reference images (local paths or public http(s) URLs) — * one per character, delivered to the provider's reference slot (Runway * `imageAssetId1..N`, Dreamina `omni_N_imageRef`) via `referenceRole: * 'character'`. Unlike `keyframe`, these are NEVER used as the video's first * frame, so a lone character sheet does not open the clip on the character * grid (the grid-open bug fixed on main). Mutually exclusive with `keyframe`. */ characterRefs?: string[]; /** Per-job duration override; falls back to defaults.seconds, then 10. */ seconds?: number; /** Per-job aspect-ratio override (recorded; provider uses the batch default). */ aspectRatio?: string; } export interface BatchQueueManifest { schemaVersion: 1; route?: BatchRouteId; defaults?: { seconds?: number; aspectRatio?: string; resolution?: string; }; jobs: BatchQueueJob[]; } export type BatchJobStatus = 'pending' | 'done' | 'failed'; export interface BatchQueueJobState { id: string; sceneIndex: number; taskId: string; status: BatchJobStatus; /** Set once the clip has been copied to clips/.mp4. */ clipPath?: string; error?: string; /** * ISO timestamp of when this job's scene was (re)submitted. Used by wedge * detection to measure stall time. Optional for backward compat: queues * written before this field fall back to the batch-level state.submittedAt. */ submittedAt?: string; /** * The real provider-issued task id captured from the native submit's * rawResult (e.g. a Runway useapi task id). Distinct from the synthetic * `taskId` (`#`) the batch queue keys downloads on. * Optional/best-effort: absent when the transport did not surface a per-scene * id, so queues written before this field still load unchanged. */ providerTaskId?: string; /** * How many times this scene has been auto-resubmitted (Phase 3 PR2). Absent / * 0 means it has never been resubmitted; bounded by --max-resubmits. Optional * for backward compat: queues written before this field load unchanged. */ resubmitCount?: number; /** * When a scene is re-submitted it lives under a NEW externalJobId. This field * records that id so the monitor polls the scene's CURRENT job-state file * (`.vclaw-jobs/.json`) rather than the dead original. * Absent means the scene is still owned by the batch-level state.externalJobId * (the default for every job until it is resubmitted), so existing queues are * unchanged. */ activeExternalJobId?: string; /** * The compiled single-scene execution task captured at submit time. Persisted * so an auto-resubmit can rebuild a byte-identical one-task payload (same * prompt / referencePaths / referenceRole / duration / inputKind) WITHOUT * re-reading the original manifest. Optional/best-effort: absent on queues * written before this field, which simply cannot be auto-resubmitted. */ task?: VideoExecutionTask; } export interface BatchQueueState { schemaVersion: 1; externalJobId: string; route: BatchRouteId; outputDir: string; workspaceRoot: string; submittedAt: string; jobs: BatchQueueJobState[]; } export interface BatchQueueRollup { total: number; done: number; pending: number; failed: number; /** True when no job is still pending (every job is done or failed). */ terminal: boolean; } /** * Reads + validates a batch manifest. Throws loudly on malformed JSON or * missing required fields — no silent fallback. The only default applied is * `route` -> "runway-useapi" when omitted (the free explore default). */ export declare function readBatchManifest(path: string): Promise; /** * Pure: compiles a manifest into a single VideoExecutionPayload whose `tasks` * map jobs 1:1. Job index becomes sceneIndex; job.keyframe becomes the task's * sole referencePath; duration resolves job.seconds -> defaults.seconds -> 10. * * The resulting payload is byte-compatible with what the native route * transports already consume, so submit/poll need no batch-specific code path. */ export declare function buildBatchPayload(manifest: BatchQueueManifest, opts: { workspaceRoot: string; outputDir: string; }): VideoExecutionPayload; /** The path the native transport writes a completed scene to. */ export declare function sceneOutputPathFor(outputDir: string, sceneIndex: number): string; /** The stable per-job clip path the batch monitor copies completed scenes to. */ export declare function clipPathForJob(outputDir: string, jobId: string): string; export declare function batchQueueStatePath(outputDir: string): string; export declare function batchStatusPath(outputDir: string): string; export declare function writeBatchQueueState(state: BatchQueueState): Promise; export declare function readBatchQueueState(outputDir: string): Promise; /** * Reads the native transport's per-scene job-state file * (`/.vclaw-jobs/.json`) and returns a map from * sceneIndex to `{ status, error }`. All three transports (runway, dreamina, * seedance) write this file with an identical `scenes[]` shape. * * Returns an empty map when the file is absent (e.g. a very early poll pass * before the transport has flushed state). The caller treats absent scenes as * still-pending. */ export declare function readNativeJobSceneStates(outputDir: string, externalJobId: string): Promise>; /** Counts done/pending/failed and reports whether the queue is fully terminal. */ export declare function rollupBatchQueueState(state: BatchQueueState): BatchQueueRollup; /** * Runway free explore mode allows ~1 concurrent job and returns * canUseExploreMode:false / HTTP 429 / rate-limit text when saturated. Detect * that signal from a monitor pass's combined stdout+stderr so the loop can back * off instead of hammering at the normal interval. Pure/deterministic. */ export declare function isExploreThrottled(passOutput: string): boolean; /** * Pure: extracts the provider-issued per-scene task ids from a native submit's * `rawResult`. Defensively reads `rawResult.submittedScenes` (an array of * `{ sceneIndex: number, taskId: string }`) and returns a Map sceneIndex -> * taskId. Tolerant by design — a missing/non-object rawResult, a missing or * non-array `submittedScenes`, or partial/wrong-typed entries are skipped * rather than throwing. Returns an empty Map when nothing usable is present. */ export declare function extractProviderTaskIds(rawResult: unknown): Map; /** * Pure: exponential backoff for the explore-throttle case. Returns * `baseMs * 2^consecutiveThrottles`, clamped to `[baseMs, capMs]`. A * `consecutiveThrottles <= 0` returns `baseMs` (the normal interval). Used by * the monitor loop to widen the poll interval while Runway explore mode is * saturated instead of hammering at the flat interval. */ export declare function nextBackoffMs(baseMs: number, consecutiveThrottles: number, capMs: number): number; export interface WedgedScene { id: string; sceneIndex: number; stalledMinutes: number; } /** * A scene is "wedged" when its job is still pending, the native transport still * reports it 'submitted' (never reached completed/failed — e.g. a Runway explore * queue that never returns it), and it has made no progress for more than * stallMinutes. `stallMinutes <= 0` disables detection. Per-job submittedAt falls * back to the batch-level state.submittedAt for queues written before that field * existed, so old `batch-queue.json` files still work. Pure/deterministic. */ export declare function detectWedgedScenes(state: BatchQueueState, sceneStates: Map, nowMs: number, stallMinutes: number): WedgedScene[]; /** * Pure: the distinct set of job-state ids the monitor must poll. A scene that * has never been resubmitted is owned by the batch-level `state.externalJobId`; * a re-submitted scene is owned by its `activeExternalJobId`. The monitor reads * `.vclaw-jobs/.json` for each id in this set and merges the per-scene * states. Order is preserved (first occurrence wins); deduped. When no job has * been resubmitted this returns exactly `[state.externalJobId]`, so the default * monitor behaviour is unchanged. */ export declare function activeJobIdsFor(state: BatchQueueState): string[]; /** * Pure: plans which wedged scenes are eligible for an auto-resubmit this pass. * Delegates wedge detection to {@link detectWedgedScenes} (same stall policy), * then keeps only the wedged scenes whose job has been resubmitted fewer than * `maxResubmits` times (`resubmitCount ?? 0 < maxResubmits`). Returns the planned * `{ sceneIndex, jobId }` list — the side-effecting submit + state mutation lives * in the CLI, not here. Empty when `stallMinutes <= 0`, when nothing is wedged, * or when every wedged scene has hit its resubmit cap. Never throws. */ export declare function planResubmits(state: BatchQueueState, sceneStates: Map, nowMs: number, opts: { stallMinutes: number; maxResubmits: number; }): { sceneIndex: number; jobId: string; }[]; /** * Pure: builds a single-task VideoExecutionPayload for ONE scene of an existing * batch state, reusing that scene's persisted `task` (same prompt / * referencePaths / referenceRole / duration / inputKind) so a resubmit * reproduces the same job. The task keeps its ORIGINAL `sceneIndex` so the * transport writes `scene-.mp4` into the same outputDir the batch monitor * reads. The payload's routeId/outputDir/workspaceRoot/profile come from the * batch state (the route is the credit-safety-guarded runway-useapi). * * Throws if the scene is unknown or its `task` was not captured at submit time * (an old queue) — the CLI guards against the latter before calling this, so a * resubmit is simply skipped rather than crashing the monitor. */ export declare function buildSingleSceneResubmitPayload(state: BatchQueueState, sceneIndex: number): VideoExecutionPayload; /** * Returns a copy of `state` with the given scene's taskId/submittedAt cleared so * a subsequent submit pass re-queues it. Pure — does not mutate the input. */ export declare function clearWedgedScene(state: BatchQueueState, sceneIndex: number): BatchQueueState; /** * Applies wedge handling to a freshly-polled batch state. Pure/deterministic: * given the current jobs, the transport's per-scene states, the clock, and the * stall policy, returns the wedged scenes and — when `failWedged` is set — a new * state with those still-pending wedged jobs marked 'failed'. Failing them lets * the queue reach `terminal` so the monitor can stop instead of polling a stuck * scene until its deadline. Does NOT mutate the input. * * `stallMinutes <= 0` disables detection (returns the state unchanged + an empty * list), so the monitor's default behaviour is byte-identical to before. */ export declare function applyWedgeHandling(state: BatchQueueState, sceneStates: Map, nowMs: number, opts: { stallMinutes: number; failWedged: boolean; }): { state: BatchQueueState; wedged: WedgedScene[]; }; //# sourceMappingURL=batch-queue.d.ts.map