import type { VeniceClient } from '../venice/client.js'; import type { SeriesState } from '../series/types.js'; import { type RenderVideoOptions } from './video-generator.js'; /** The render primitive, injectable so tests can drive the engine offline. */ export type RenderFn = (client: VeniceClient, options: RenderVideoOptions) => Promise; /** The writer primitive, injectable so tests can author beats offline. */ export type AuthorFn = (input: AuthorInput) => Promise; export declare const STREAM_MODEL_T2V = "minimax-h3-max-text-to-video"; export declare const STREAM_MODEL_I2V = "minimax-h3-max-image-to-video"; export { STREAM_WRITER_CHOICES, STREAM_VIDEO_CHOICES, STREAM_DEFAULT_WRITER } from './stream-choices.js'; export declare const STREAM_DEFAULT_DURATION = "15s"; export declare const STREAM_DEFAULT_BUDGET_USD = 2; export declare const STREAM_DEFAULT_RESOLUTION = "480P"; /** How many recent beats the writer sees verbatim; older ones live in the summary. */ export declare const STREAM_RECENT_BEATS = 6; /** * How many beats the writer authors AHEAD of the renderer by default. The * writer and the renderer run as a producer/consumer pair: the writer keeps up * to this many authored beats waiting in a buffer so a render never blocks on a * writer-model call (AGENTS.md rule 60(l)). 0 turns the buffer off — author * each beat just before it renders, the pre-2.24 serial behaviour. */ export declare const STREAM_DEFAULT_LOOKAHEAD = 15; /** * When a chained render dies server-side, the START FRAME is the usual cause * (MiniMax i2v rejects some frames after billing — AGENTS.md anti-pattern 31). * Retrying the same frame is a guaranteed repeat. So each retry steps back * through the previous beat's clip by these offsets (seconds from the end) and * keeps the beat that was already written. A step of a second or two is * invisible in the story; a skipped beat is not. */ export declare const STREAM_CHAIN_STEP_BACK_SEC: number[]; /** * How many chained (i2v) render failures on one beat before the engine gives * up on the chain for that beat and renders it t2v instead. A t2v beat is a * soft reset: the picture re-establishes from the beat text and the scene * memory, identity drifts for one beat, and the story keeps going. Without it a * single face-ending beat kills the whole stream (anti-pattern 31: MiniMax i2v * dies server-side on a face-bearing start frame, after billing). Two chained * attempts cover the 0s and 0.5s step-backs; a face that fills the frame for * the whole tail of a 15s clip does not leave in 3s either. */ export declare const STREAM_CHAIN_FAILURES_BEFORE_RESET = 2; /** What the writer produces for one beat. A subset of ShotScript, plus memory. */ export interface AuthoredBeat { /** One or two sentences: what happens on screen. Present tense. */ description: string; /** Who is on screen. Names must match series.characters. */ characters: string[]; dialogue: { character: string; line: string; delivery?: string; } | null; /** Diegetic sound for this beat. */ sfx: string | null; cameraMovement: string; /** One sentence for the story memory: what changed. */ summary: string; } export interface AuthorInput { series: SeriesState; /** Beat number being written (1-based). */ beatNumber: number; /** Rolling memory: summaries of every prior beat, oldest first. */ storySoFar: string; /** The last few beats verbatim, oldest first (rendered or still buffered). */ recentBeats: ReadonlyArray<{ n: number; beat: AuthoredBeat; }>; /** Operator direction that applies to every beat (e.g. "laugh track"). */ direction?: string; } export interface StreamBeat { n: number; /** Project-relative path to the beat mp4 (for /media URLs). */ file: string; beat: AuthoredBeat; /** t2v-reset: a chained render failed repeatedly, so this beat re-established the picture from text. */ lane: 't2v' | 'i2v' | 't2v-reset'; costUsd: number; at: string; /** * Exactly what was sent to the video model, so an operator can take the * prompt elsewhere and fine-tune it. `startFrame` is project-relative. */ render?: { model: string; prompt: string; resolution?: string; duration: string; startFrame?: string; }; } export type StreamStatus = 'idle' | 'writing' | 'rendering' | 'error'; export interface StreamManifest { version: number; episode: number; model: { t2v: string; i2v: string; writer: string; }; /** Family key for the current video lanes (stream-choices.ts). */ videoFamily: string; resolution: string; duration: string; budgetUsd: number; unbounded: boolean; /** How many beats the writer authors ahead of the renderer (0 = serial). */ lookahead: number; /** Keep the look-ahead buffer topped up as the renderer drains it. */ autoRefill: boolean; /** Authored-but-not-yet-rendered beats waiting in the buffer, right now. */ buffered: number; spendUsd: number; running: boolean; status: StreamStatus; lastError?: string; /** Beat number currently being written/rendered, when status is not idle. */ inFlight?: number; direction?: string; startedAt: string; updatedAt: string; beats: StreamBeat[]; /** The look-ahead buffer's authored beats, so a resume renders them without re-authoring. */ pendingBeats?: AuthoredBeat[]; /** Selectable writers and video families, so the UI can offer them with speed/cost hints. */ choices?: { writers: ReadonlyArray<{ id: string; label: string; medianSec: number; reliability: string; privacy: string; note: string; }>; video: ReadonlyArray<{ id: string; label: string; usdPer15s: number; renderSecApprox: number; speed: string; resolutions: string[]; note: string; }>; }; } /** Structural subset of the web EventHub — avoids a mini-drama → web import. */ export interface StreamBroadcaster { broadcast(event: string, data: unknown): void; } export interface StreamEngineOptions { client: VeniceClient; series: SeriesState; 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; /** Model that writes beats. Defaults to STREAM_DEFAULT_WRITER (fast), not the project's intelligence model. */ writerModel?: string; /** Video family key or lane model id (stream-choices.ts). Defaults to MiniMax H3 Max. */ videoFamily?: string; resolution?: string; duration?: string; budgetUsd?: number; /** No budget cap — stream until stopped. */ unbounded?: boolean; /** Standing direction folded into every beat's writer prompt. */ direction?: string; /** Optional opening beat, used verbatim for beat 1 instead of asking the writer. */ openingBeat?: AuthoredBeat; /** * Pre-written beats, consumed in order before the live writer is asked * (`--beats-file`). The live writer is only the fallback past the last one, * so a fully-scripted run never calls the writer model. */ scriptedBeats?: AuthoredBeat[]; /** * How many beats the writer authors ahead of the renderer (the look-ahead * buffer depth). Defaults to STREAM_DEFAULT_LOOKAHEAD (15). 0 = serial: the * beat is authored just before it renders, so a render waits on the writer. */ lookahead?: number; /** * Keep the buffer topped up to `lookahead` as the renderer drains it * (default true). False fills the buffer once, then authors on demand. */ autoRefill?: boolean; broadcaster?: StreamBroadcaster; log?: (line: string) => void; /** Override the render primitive (tests). Defaults to renderVideoFile. */ render?: RenderFn; /** Override the writer (tests). Defaults to a chatJson call on the writer model. */ author?: AuthorFn; errorBackoffMs?: number; } export declare function buildStreamSystemPrompt(series: SeriesState, direction?: string): string; export declare function buildStreamUserPrompt(input: AuthorInput): string; /** Coerce a writer's output to the locked cast's spelling and a complete beat. */ export declare function normalizeBeat(raw: Partial, series: SeriesState): AuthoredBeat; /** * A writer that serves pre-written beats in order, then hands the pen to the * live writer past the last one. Used by `stream --beats-file`: an operator * (or an agent) authors N beats up front and the stream renders them without * calling the writer model at all. Beat N in the file is beat N of the stream * — the file is indexed by position, and entries recovered from * `exportStreamJson`'s `{ n, authored }` shape are unwrapped automatically. */ export declare function makeScriptedAuthor(scripted: readonly AuthoredBeat[], fallback: AuthorFn, log?: (line: string) => void): AuthorFn; /** Structural check + unwrap for a parsed beats file. Use before normalizeBeat. */ export declare function parseScriptedBeats(raw: unknown): Partial[]; /** Default writer: one chatJson call on the intelligence model. */ export declare function makeChatAuthor(client: VeniceClient, model: string): AuthorFn; /** * Serial worker: write beat N, render it off beat N-1's last frame, persist, * repeat. One beat in flight at a time — the chain is inherently serial. */ export declare class StreamEngine { private readonly client; private readonly series; private readonly episode; private readonly projectDir; private readonly episodeDir; private readonly slug; private writerModel; private readonly streamDir; private video; private resolution; private duration; private readonly initialBudgetUsd; private readonly direction?; private readonly openingBeat?; private readonly broadcaster?; private readonly log; private readonly render; private author; private readonly authorOverride?; /** Pre-written beats served before the live writer (options.scriptedBeats). */ private readonly scriptedBeats; private readonly explicitWriter; private readonly explicitVideo; private readonly errorBackoffMs; private unbounded; private budgetUsd; private beats; private spendUsd; private startedAt; private running; private workerActive; private status; private lastError?; private inFlight?; private consecutiveErrors; /** Resolves the worker's paused wait when Start is clicked. */ private wake?; /** True while prime() renders beat 1 with the worker otherwise paused. */ private priming; /** Target buffer depth: how many beats the writer stays ahead of the render. */ private lookahead; /** Keep the buffer topped up as the renderer drains it. */ private autoRefill; /** * Authored-but-not-yet-rendered beats, in order. `buffer[0]` is the beat the * renderer is on right now (beat `beats.length + 1`), kept there across render * retries and only shifted off once it lands. `buffer[i]` is beat * `beats.length + 1 + i`. A crash preserves it via the manifest's pendingBeats. */ private buffer; /** The writer loop should keep authoring ahead (set by start/prime, cleared by stop/budget). */ private writerRunning; /** A runWriter() invocation is currently alive (guards against a second loop). */ private writerActive; private writerConsecutiveErrors; /** Resolves the writer's idle wait (buffer drained, config or budget changed). */ private writerWake?; /** Resolves the renderer's wait for the writer to put a beat in the buffer. */ private renderWake?; constructor(options: StreamEngineOptions); /** * The writer for the next beat: pre-written beats first (when present), * then the injected author override (tests), then the chat writer. A writer * switch (configure / a resumed manifest) rebuilds through here, so * scripted beats keep serving and only the fallback changes. */ private buildAuthor; private resolveDuration; private costPerBeat; /** * Switch the writer and/or the video family. Applies to the NEXT beat; the * beat in flight finishes on the models it started with. The i2v chain is * unaffected by a family change — the start frame is a PNG, so any i2v lane * can pick it up. Resolution snaps to the new family's draft tier unless the * caller passes one that the family supports. */ configure(config: { writer?: string; videoFamily?: string; resolution?: string; lookahead?: number; autoRefill?: boolean; }): Promise; private budgetExhausted; init(): Promise; /** * Render the opening beat, then wait. Nothing else RENDERS until start() is * called (the operator clicks Start in the UI), but the look-ahead writer * begins authoring the next beats into the buffer immediately — so when Start * is clicked the buffer is already deep and rendering runs back to back with * no writer wait. Resolves once beat 1 is on disk, or immediately if a beat * already exists on disk. */ prime(): Promise; start(config?: { budgetUsd?: number; unbounded?: boolean; }): Promise; stop(): Promise; state(): StreamManifest; /** * The number of beats the writer aims to have authored (rendered + buffered) * right now: `beats.length + lookahead`, capped so it never authors beats the * budget can never render. Returns the current authored count (no new work) * when the writer is off. */ private authorTarget; /** Start the look-ahead writer loop if it should run and isn't already alive. */ private ensureWriter; private wakeWriter; private wakeRender; /** Sleep until woken (drain / config / budget / stop) or the safety timeout. */ private sleepWriter; /** The render worker waits here for the writer to put a beat in the buffer. */ private waitForBuffer; /** * Author beats into the buffer, staying `lookahead` ahead of the renderer. * Runs concurrently with runWorker so a render never blocks on a writer call. * Exits when stopped, when the depth is reached and refill is off, or after * repeated author failures (the render worker then authors inline and surfaces * the error — a broken writer must not silently stall the stream). */ private runWriter; private runWorker; /** The authored beats so far, oldest first, as {n, beat}: rendered then buffered. */ private authoredList; /** The writer's memory for authoring beat n: the story so far + recent beats verbatim. */ private buildAuthorContext; /** Produce the authored beat for position n (opening beat verbatim, else the writer). */ private authorBeatFor; /** Render the next beat off the buffer. Returns false on failure. */ private renderNext; private fail; private toShot; /** * Lean prompt through the harness's own builder with every lane pinned to a * simple-prompt model, so the H3 Max branch runs (no directorial blocks, no * reference tags, dialogue as improv intent). Falls back to a minimal prompt * when the series has no aesthetic yet. */ private buildPrompt; private manifestPath; private snapshot; private persist; private loadManifest; private emit; } /** Everything an operator needs to re-run or fine-tune the beats elsewhere. */ export declare function exportStreamJson(manifest: StreamManifest, series: SeriesState): string; export declare function exportStreamMarkdown(manifest: StreamManifest, series: SeriesState): string; //# sourceMappingURL=stream-engine.d.ts.map