import type { VeniceClient } from '../venice/client.js'; import type { EpisodeScript, SeriesState } from '../series/types.js'; import { type RenderVideoOptions } from './video-generator.js'; /** The render primitive, injectable so tests can drive the scheduler offline. */ export type RenderFn = (client: VeniceClient, options: RenderVideoOptions) => Promise; /** watch = fast disposable Turbo draft; create = identity-locked Max R2V. */ export type LoopMode = 'watch' | 'create'; export declare const LOOP_MODEL_T2V = "minimax-h3-max-turbo-text-to-video"; export declare const LOOP_MODEL_I2V = "minimax-h3-max-turbo-image-to-video"; export declare const CREATE_MODEL_R2V = "minimax-h3-max-reference-to-video"; export declare const CREATE_MODEL_I2V = "minimax-h3-max-image-to-video"; export declare const CREATE_MODEL_T2V = "minimax-h3-max-text-to-video"; export declare const LOOP_DEFAULT_DURATION = "15s"; export declare const LOOP_DEFAULT_BUDGET_USD = 2; export declare const LOOP_DEFAULT_MAX_TAKES = 3; /** Default draft resolution per mode: 480P for watch, 768P (finish) for create. */ export declare function defaultLoopResolution(mode: LoopMode): string; export interface LoopTake { n: number; /** Project-relative path to the take mp4 (for /media URLs). */ file: string; costUsd: number; at: string; } export type LoopShotStatus = 'idle' | 'rendering' | 'ready' | 'error'; export interface LoopShotState { shotNumber: number; key: string; status: LoopShotStatus; /** 1-based take number currently promoted for playback, or null if none yet. */ currentTake: number | null; pinned: boolean; lastError?: string; /** Given up on after MAX_CONSECUTIVE_SHOT_ERRORS failures; no longer scheduled. */ failed?: boolean; takes: LoopTake[]; } export interface LoopManifest { version: number; episode: number; mode: LoopMode; chain: boolean; /** Whether shots are prompted to end on the character's face (chained loops). */ faceContinuity: boolean; model: { t2v: string; i2v: string; r2v?: string; }; resolution: string; duration: string; budgetUsd: number; maxTakes: number; unbounded: boolean; once: boolean; spendUsd: number; running: boolean; startedAt: string; updatedAt: string; shots: LoopShotState[]; } /** Structural subset of the web EventHub — avoids a mini-drama → web import. */ export interface LoopBroadcaster { broadcast(event: string, data: unknown): void; } export interface LoopEngineOptions { client: VeniceClient; series: SeriesState; script: EpisodeScript; episode: number; /** Absolute project directory (series.outputDir). */ projectDir: string; /** Absolute episode directory (getEpisodeDir). */ episodeDir: string; /** Project slug used in SSE payloads / media URLs. Defaults to series.slug. */ slug?: string; /** watch = Turbo draft (default); create = identity-locked Max R2V. */ mode?: LoopMode; resolution?: string; duration?: string; budgetUsd?: number; maxTakes?: number; /** Render one take per shot, then stop scheduling. */ once?: boolean; /** Ignore the take-buffer cap AND budget — regenerate forever until stopped. */ unbounded?: boolean; /** * Chain clips for continuous playback (default true): shot 1 renders normally, * every later shot renders i2v off the previous shot's current-take LAST frame * so the loop flows as one piece. Turn off (`--no-chain`) to render each shot * independently (in create mode that keeps per-shot R2V identity locking). */ chain?: boolean; /** * When chaining, prompt each shot to end on the character's face for smoother * i2v continuations (default true). Automatically suppressed when the chain * i2v model rejects face start frames (MiniMax), where a face frame would kill * the next render — see `i2vRejectsFaceStartFrame` / AGENTS.md anti-pattern 31. */ faceContinuity?: boolean; broadcaster?: LoopBroadcaster; /** Optional log sink (defaults to console.log). */ log?: (line: string) => void; /** Override the render primitive (tests). Defaults to renderVideoFile. */ render?: RenderFn; /** Backoff after a failed take (ms). Injectable for tests; defaults to 5s. */ errorBackoffMs?: number; } /** * Continuous, in-process render worker for one project/episode. One take in * flight at a time (serial): Turbo is fast enough that a serial loop still * outruns playback, and it keeps the Venice client's rate limiter happy. */ export declare class LoopEngine { private readonly client; private readonly series; private readonly script; private readonly episode; private readonly projectDir; private readonly episodeDir; private readonly slug; private readonly mode; private readonly chain; private readonly faceContinuity; private readonly usdPerSec; private readonly initialBudgetUsd; private readonly sceneDir; private readonly loopDir; private readonly resolution; private readonly duration; private readonly once; private unbounded; private budgetUsd; private maxTakes; private readonly broadcaster?; private readonly log; private readonly render; private readonly errorBackoffMs; private shots; private spendUsd; private startedAt; private running; private workerActive; /** Resolves the worker's idle wait early when new work is requested. */ private wake?; constructor(options: LoopEngineOptions); /** * Whether to append the "end on the character's face" note to chained shot * prompts: requested (faceContinuity) AND chaining AND the chain i2v model * accepts face start frames. On MiniMax i2v a face-bearing start frame kills * the next render, so it is suppressed there (AGENTS.md anti-pattern 31). */ private faceContinuityActive; private resolveDuration; /** Every take renders the full loop length (default 15s), both modes. */ private durationForShot; /** Estimated cost of one take, for the budget guard. */ private costPerTake; private budgetExhausted; private allSettled; /** Load any prior manifest so takes / pins / spend survive a restart. */ init(): Promise; start(config?: { budgetUsd?: number; maxTakes?: number; unbounded?: boolean; }): Promise; stop(): Promise; pin(shotNumber: number): Promise; unpin(shotNumber: number): Promise; /** Force a fresh take of one shot next. */ regenerate(shotNumber: number): Promise; status(): LoopManifest; private runWorker; private pickNext; /** Index of a shot in playback (shot) order. */ private indexOf; /** Absolute path to a shot's current-take mp4, if it has one. */ private currentClipPath; private idle; private renderTake; /** The render spec for one take: prompt, chosen lane, and any reference bytes. */ private resolveTake; /** * Lean prompt for one shot (watch draft, or any chained i2v shot). Reuses * buildVideoPrompt with a cloned series whose video lanes all point at a * simple-prompt model, so the resolver takes the `promptStyle: 'simple'` * branch (no directorial blocks, no reference tags, no lip-sync routing) and * the H3 Max dialogue-improv path. Model + duration are overridden for the * actual t2v/i2v lane. Falls back to a minimal prompt if the series has no * aesthetic yet. */ private buildLeanPrompt; /** * Append the face-continuity note so the shot ends on the character's face and * the next chained i2v continues smoothly. Applied only when active for this * loop (see `faceContinuityActive`) AND the shot actually has a character — * telling a pure-atmosphere shot to "end on the character's face" is noise. */ private withFaceContinuity; /** Series clone whose video lanes target the H3 Max (non-Turbo) family. */ private createSeries; private fallbackPrompt; /** * Ring buffer: keep at most `maxTakes` candidate takes per shot so an infinite * loop can't fill the disk. Drops the oldest take that is neither the current * one nor pinned, deleting its file. The current take is never pruned. */ private pruneTakes; private manifestPath; private snapshot; private persist; private loadManifest; private emit; } //# sourceMappingURL=loop-engine.d.ts.map