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; } /** * The reference a route that takes the voice as AUDIO (`reapi-seedance`) * should carry for a clone: the source audio when it is a readable audio file * on this machine, otherwise the local black-frame clip, whose track the * transport extracts. Never the hosted URL — a remote mp4 cannot be extracted, * and sent as a video it would be billed. Pure apart from `exists`. */ export declare function voiceReferenceForAudioRoute(entry: Pick, exists: (path: string) => boolean): string; /** * The `@VoiceName` tag lookup an audio route should resolve tags against: the same * names and descriptors as `voiceEntryByName`, but each referencePath swapped for * the audio-route reference above. Without this a tagged voice resolves to the * black-frame mp4 and is sent as a VIDEO reference — which `reapi-seedance` bills * on top of the output, while audio rides free. `audioRoute: false` returns the * map unchanged, so every other route keeps today's mp4. Pure apart from `exists`. */ export declare function voiceTagLookupForAudioRoute(lookup: Pick, audioRoute: boolean, exists: (path: string) => boolean): Map; /** * The subset of a scene's tag-resolved references that are voice clips. The caller * marks these as the voice (free audio) instead of letting them count as identity * images — a voice-only tag must not turn a chain-seeded scene into a reference-set * render and demote its keyframe. `tagLookup` is the voice map above. Pure. */ export declare function voiceTagReferences(referencedPaths: readonly string[], tagLookup: ReadonlyMap): string[]; /** * The voice references a scene's CAST contributes: a character bound to a clone with * `voice-clone --character` speaks it in every scene it appears in, no tag needed. On * an audio route that is the clone's source audio; elsewhere the durable hosted clip, * falling back to the local black-frame mp4. Pure apart from `exists`. */ export declare function boundVoiceReferences(characters: readonly string[], lookup: Pick | null, audioRoute: boolean, exists: (path: string) => boolean): string[]; 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