import { createWriteStream } from "node:fs";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";
import os from "node:os";
import path from "node:path";
import { composeShot } from "./ffmpeg-compose.js";
import { mergeClips } from "./ffmpeg-merge.js";
import { ensureFont } from "./font.js";
import { generateImage } from "./image-gen.js";
import { synthesize } from "./tts.js";
function extFor(role) {
    return role === "audio" || role === "bgm" ? "mp3" : role === "image" ? "png" : "mp4";
}
async function download(url, dest, signal) {
    const res = await fetch(url, { signal });
    if (!res.ok || !res.body) {
        throw new Error(`download failed ${res.status} for ${url.slice(0, 80)}`);
    }
    await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
}
async function uploadPut(url, file, signal) {
    const body = await readFile(file);
    const res = await fetch(url, {
        method: "PUT",
        body,
        headers: { "content-type": "video/mp4" },
        signal,
    });
    if (!res.ok) {
        throw new Error(`upload PUT failed ${res.status}: ${await res.text().catch(() => "")}`);
    }
}
export async function runRender(spec, opts) {
    const log = opts.logger ?? console;
    const base = path.join(opts.workspace, "render");
    const tmp = await mkdtemp(path.join(base.startsWith(os.tmpdir()) ? base : os.tmpdir(), "dp-render-"));
    try {
        const outPath = path.join(tmp, "out.mp4");
        const width = spec.params?.width ?? 1080;
        const height = spec.params?.height ?? 1920;
        if (spec.render_kind === "produce_shot") {
            await producShot(spec, opts, tmp, outPath, width, height);
        }
        else if (spec.render_kind === "merge") {
            const clips = [];
            let bgm;
            for (let i = 0; i < spec.inputs.length; i++) {
                const inp = spec.inputs[i];
                const dest = path.join(tmp, `${inp.role}-${i}.${extFor(inp.role)}`);
                await download(inp.get_url, dest, opts.signal);
                if (inp.role === "bgm")
                    bgm = dest;
                else
                    clips.push(dest);
            }
            if (clips.length === 0)
                throw new Error("merge: no input clips");
            await mergeClips(clips, outPath, { bgm, bgmVolume: spec.params?.bgm_volume });
        }
        else {
            throw new Error(`unsupported render_kind: ${spec.render_kind}`);
        }
        await uploadPut(spec.output_put_url, outPath, opts.signal);
        log.info(`[render] ${spec.render_kind} → ${spec.output_r2_key}`);
        return { output_r2_key: spec.output_r2_key, duration_ms: spec.params?.duration_ms, width, height };
    }
    finally {
        await rm(tmp, { recursive: true, force: true }).catch(() => undefined);
    }
}
/** produce_shot: generate visual + voice on the worker, then compose. */
async function producShot(spec, opts, tmp, outPath, width, height) {
    // 1) Visual: stock URL (center-sourced) → footage; image_prompt → generate.
    let videoPath;
    let imagePath;
    const visual = spec.visual;
    if (visual?.kind === "url" && visual.url) {
        videoPath = path.join(tmp, "footage.mp4");
        await download(visual.url, videoPath, opts.signal);
    }
    else if (visual?.kind === "image_prompt") {
        if (!opts.llm?.imageGeneration) {
            throw new Error("this worker's llm_profile has no image generation configured (set image_model/image_api_key)");
        }
        imagePath = path.join(tmp, "image.png");
        await generateImage({ baseUrl: opts.llm.baseUrl, apiKey: opts.llm.imageGeneration.apiKey, model: opts.llm.imageGeneration.model }, visual.prompt ?? "", `${width}x${height}`, imagePath);
    }
    // 2) Voice (profile TTS or free edge-tts).
    let audioPath;
    if (spec.narration) {
        audioPath = path.join(tmp, "audio.mp3");
        await synthesize(spec.tts?.engine ?? "edge", opts.llm ? { baseUrl: opts.llm.baseUrl, apiKey: opts.llm.apiKey } : undefined, spec.narration, spec.tts?.voice, audioPath);
    }
    // 3) Compose (Ken Burns for stills) + subtitle.
    const fontFile = spec.subtitle_text ? await ensureFont(opts.workspace) : null;
    const st = spec.subtitle_style;
    await composeShot({
        video: videoPath,
        image: imagePath,
        motion: spec.motion ?? "none",
        audio: audioPath,
        subtitle: spec.subtitle_text,
        subtitleStyle: st
            ? { color: st.color, fontSizeRatio: st.font_size_ratio, stroke: st.stroke, background: st.background, position: st.position }
            : undefined,
        fontFile,
        fade: spec.params?.transition === "fade",
        width,
        height,
        durationMs: spec.params?.duration_ms,
        out: outPath,
    });
}
//# sourceMappingURL=render-runner.js.map