import type { AssetTagEntry } from './prompt-rules.js'; /** One registered voice clone in `voice-clones.json`. */ export interface VoiceCloneEntry { /** Voice-asset name (what an @Name tag / character binding resolves to). */ name: string; /** Optional character this voice belongs to (so the character always speaks it). */ character?: string; /** The source audio sample this clone was built from. */ sourceAudio: string; /** The built blank-video-with-audio clip (the reference the model consumes). */ clipPath: string; /** * Durable public URL for `clipPath`, hosted on Go Bananas. Required for the * seedance-direct r2v voice-lock (the remote API needs a hosted URL, not a local * path). Populated by `voice-clone --execute` when GB media hosting is available; * absent when hosting is unavailable (the clip stays a local path). */ hostedUrl?: string; /** When sliced for drift control: the per-line clip paths (in order). */ slices?: string[]; /** Optional human description of the voice character/tone. */ description?: string; /** Duration of the source audio in milliseconds (0 when not probed / dry-run). */ durationMs?: number; } export interface VoiceClonesArtifact { schemaVersion: 1; projectSlug: string; generatedAt: string; voices: VoiceCloneEntry[]; } export interface VoiceClonesLookup { voices: VoiceCloneEntry[]; /** name (lowercased) -> entry. */ voiceByName: Map; /** character (lowercased) -> entry, for character→voice binding. */ voiceByCharacter: Map; /** * name (lowercased) -> AssetTagEntry, ready for buildAssetTagLookup's * `voicesByName`. The referencePath is the blank-video clip so it classifies * into Seedance `reference_videos`. */ voiceEntryByName: Map; } export declare function voiceClonesPathFor(root: string, slug: string): string; /** Directory the built blank-video clips live in (under the project). */ export declare function voiceCloneClipsDir(root: string, slug: string): string; export declare function writeVoiceClones(root: string, slug: string, artifact: VoiceClonesArtifact): Promise; /** * Read `artifacts/voice-clones.json`. Returns empty lookups when absent (graceful — * the execution layer then injects no voice references, byte-identical to today). A * present-but-malformed file is NOT swallowed (JSON.parse surfaces it). */ export declare function readVoiceClones(root: string, slug: string): Promise; export interface BlankVideoPlanOptions { /** Source audio sample path. */ audioPath: string; /** Output clip path (.mp4). */ outputPath: string; /** Black-frame canvas width (default 1280). */ width?: number; /** Black-frame canvas height (default 720). */ height?: number; /** Optional fixed slice window for drift control (e.g. 2 seconds). */ startSeconds?: number; durationSeconds?: number; /** * Clamp the OUTPUT to this many seconds (output-side `-t`). x264 keeps encoding * frames after the audio ends, so `-shortest` alone lets the video stream overshoot * the audio (e.g. 14s audio -> 16.2s video) and trip the r2v duration cap. Setting * this to the audio length forces the whole clip to exactly that duration. */ clampSeconds?: number; } /** * Build the ffmpeg argv for one black-frame-plus-audio clip WITHOUT spawning. * * `-f lavfi -i color=c=black:s=WxH:r=24` synthesizes the black video; the audio * sample is the second input. When a slice window is given, `-ss -t ` * trims the AUDIO input. `clampSeconds` adds an output-side `-t` that forces the whole * clip to that length — necessary because `-shortest` alone lets x264 overshoot the * audio (trailing video frames), which would trip the r2v duration cap. PURE — splice * the result straight into a runFfmpeg call. Throws on a non-audio source (code 1). */ export declare function planBlankVideoWithAudio(opts: BlankVideoPlanOptions): string[]; export interface VoiceCloneInput { /** Voice-asset name. */ name: string; /** Source audio sample path. */ audioPath: string; /** Optional character to bind the voice to. */ character?: string; /** Optional voice description. */ description?: string; /** Slice the recording into windows of this many seconds (drift fix; 0/undefined = single clip). */ sliceSeconds?: number; /** Black-frame canvas. */ width?: number; height?: number; } /** * Max reference-video length the Seedance/Dreamina r2v voice-lock accepts * (`dreamina-seedance-2-0` rejects refs over this with HTTP 500). A longer voice * clip is warned about at clone time so the operator slices it before it hits the * provider as an opaque 500. */ export declare const VOICE_REF_MAX_SECONDS = 15.2; export interface BuildVoiceCloneOptions { workspaceRoot: string; slug: string; /** ISO timestamp (injected so the artifact stays deterministic/testable). */ generatedAt: string; /** When true, plan only: return the entry + ffmpeg commands but spawn nothing. */ dryRun?: boolean; /** Override the ffmpeg binary (tests / non-standard installs). */ ffmpegBin?: string; /** * Host the built clip and return its durable URL (or null when unavailable). * Injected so it is testable and so dry-runs never touch the network; the handler * wires the real Go Bananas uploader. Omitted/undefined -> no hosting attempted. */ hostMedia?: (clipPath: string) => Promise; } export interface BuildVoiceCloneResult { entry: VoiceCloneEntry; /** The ffmpeg command line(s) that were (or, on dryRun, would be) run. */ commands: string[]; /** Advisory warnings (duration over the r2v cap, hosting unavailable, …). */ warnings: string[]; } /** * Build a single voice clone: render its blank-video-with-audio clip(s) and return * the artifact entry. On `dryRun`, no ffmpeg is spawned (commands are still * returned for inspection) and durationMs is 0. * * Slicing: when `sliceSeconds > 0`, the source is probed for its length and one * `-NN.mp4` clip is emitted per window; `clipPath` points at the first slice * (the primary reference) and `slices[]` lists them all. Without slicing, a single * `.mp4` clip is built. */ export declare function buildVoiceClone(input: VoiceCloneInput, opts: BuildVoiceCloneOptions): Promise; /** * Build one voice clone and merge it into `voice-clones.json` (replacing any entry * with the same name). Returns the persisted artifact + the build result. On * `dryRun`, nothing is written and nothing is spawned — the would-be artifact is * returned for inspection. */ export declare function registerVoiceClone(input: VoiceCloneInput, opts: BuildVoiceCloneOptions): Promise<{ artifact: VoiceClonesArtifact; result: BuildVoiceCloneResult; }>; //# sourceMappingURL=voice-clone.d.ts.map