"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MediaProcessorService = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const util_1 = require("util");
const utils_1 = require("../utils");
const types_1 = require("../types");
const execPromise = (0, util_1.promisify)(child_process_1.exec);
async function execAsync(command) {
    const { stdout, stderr } = await execPromise(command);
    return stdout + stderr;
}
class MediaProcessorService {
    clients;
    constructor(clients) {
        this.clients = clients;
    }
    async chunk(filePath, outputDir, CHUNK_LENGTH_SECONDS = 30, reuseExistingChunks = true, onProgress) {
        const parsed = path.parse(filePath);
        const fileName = parsed.name;
        const outputDirPath = path.join(outputDir, `${fileName}/chunks`);
        await fs.promises.mkdir(outputDirPath, { recursive: true });
        const doneFilePath = path.join(outputDirPath, ".chunking_done");
        const doneFileExists = await (0, utils_1.fileExists)(doneFilePath);
        const existingFolderFiles = await fs.promises.readdir(outputDirPath);
        const existingChunkNames = existingFolderFiles.filter((f) => f.includes("chunk") && f.endsWith(".mp3"));
        if (existingChunkNames.length > 0 && doneFileExists) {
            if (reuseExistingChunks) {
                console.log("Chunks already exist, skipping");
                return existingChunkNames.map((chunkName) => path.join(outputDirPath, chunkName));
            }
            else {
                for (const file of existingFolderFiles) {
                    fs.rmSync(path.join(outputDirPath, file), { recursive: true });
                }
            }
        }
        const inputExt = path.extname(filePath).toLowerCase().replace('.', '');
        const isAlreadyMp3 = inputExt === 'mp3';
        const audioCodecArgs = isAlreadyMp3
            ? '-acodec copy'
            : '-acodec libmp3lame -ac 1 -b:a 32k -threads 0';
        await new Promise((resolve, reject) => {
            let totalDurationSeconds = 0;
            (0, child_process_1.exec)(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${filePath}"`, (err, stdout) => {
                if (!err && stdout.trim()) {
                    totalDurationSeconds = parseFloat(stdout.trim()) || 0;
                }
                const args = [
                    '-i', filePath,
                    '-f', 'segment',
                    '-segment_time', String(CHUNK_LENGTH_SECONDS),
                    '-map', '0:a:0',
                    ...audioCodecArgs.split(' '),
                    '-vn',
                    ...(onProgress ? ['-progress', 'pipe:1'] : []),
                    `${outputDirPath}/chunk%04d.mp3`,
                ];
                const proc = (0, child_process_1.spawn)('ffmpeg', args);
                let stdoutBuf = '';
                proc.stdout?.on('data', (data) => {
                    stdoutBuf += data.toString();
                    if (onProgress && totalDurationSeconds > 0) {
                        const match = stdoutBuf.match(/out_time_ms=(\d+)/g);
                        if (match) {
                            const last = match[match.length - 1];
                            const ms = parseInt(last.split('=')[1], 10);
                            const fraction = Math.min(ms / 1000 / totalDurationSeconds, 1);
                            onProgress(fraction);
                            stdoutBuf = stdoutBuf.slice(-500);
                        }
                    }
                });
                proc.on('close', (code) => {
                    if (code === 0)
                        resolve();
                    else
                        reject(new Error(`ffmpeg exited with code ${code}`));
                });
                proc.on('error', reject);
            });
        });
        await fs.promises.writeFile(doneFilePath, "done");
        const folderFiles = await fs.promises.readdir(outputDirPath);
        const chunkNames = folderFiles.filter((f) => f.includes("chunk") && f.endsWith(".mp3"));
        console.log("Chunked into", chunkNames.length, "chunks");
        return chunkNames.map((chunkName) => path.join(outputDirPath, chunkName));
    }
    async *streamTranscription(files, outputPath, reusePreviousTranscript = true) {
        const exists = await (0, utils_1.fileExists)(outputPath);
        if (exists && reusePreviousTranscript) {
            console.log("Transcription already exists, using cached data");
            const contents = await (0, utils_1.readFile)(outputPath);
            const data = JSON.parse(contents.toString());
            for (const item of data)
                yield item;
            return;
        }
        const allTranscripts = [];
        for (const file of files) {
            const chunkName = path.parse(file).name;
            const chunkTranscriptPath = path.join(path.dirname(outputPath), `/chunks/${chunkName}.txt`);
            const chunkExists = await (0, utils_1.fileExists)(chunkTranscriptPath);
            if (chunkExists && reusePreviousTranscript) {
                const contents = await (0, utils_1.readFile)(chunkTranscriptPath);
                const cached = {
                    chunkPath: chunkTranscriptPath,
                    text: contents.toString(),
                    usd_cost: 0,
                };
                yield cached;
                allTranscripts.push(cached);
                continue;
            }
            console.log("Transcribing", file);
            const fileBuffer = fs.readFileSync(file);
            const transcript = await this.clients
                .createAudioTranscription("openai", {
                file: fileBuffer,
                fileName: path.basename(file),
                model: "whisper-1",
            })
                .catch((e) => {
                console.error("Error transcribing", file, e);
                return { text: "" };
            });
            await (0, utils_1.mkdir)(path.dirname(chunkTranscriptPath), { recursive: true });
            await fs.promises.writeFile(chunkTranscriptPath, transcript.text);
            const data = {
                chunkPath: chunkTranscriptPath,
                text: transcript.text,
                usd_cost: 30 * 0.0001,
            };
            yield data;
            allTranscripts.push(data);
        }
        fs.writeFileSync(outputPath, JSON.stringify(allTranscripts, null, 2));
    }
    async transcribeChunks(files, outputPath, reusePreviousTranscript = true) {
        const exists = await (0, utils_1.fileExists)(outputPath);
        if (exists && reusePreviousTranscript) {
            const contents = await (0, utils_1.readFile)(outputPath);
            return JSON.parse(contents.toString());
        }
        const fullText = [];
        for await (const { text } of this.streamTranscription(files, outputPath, reusePreviousTranscript)) {
            fullText.push(text);
        }
        await fs.promises.writeFile(outputPath, JSON.stringify(fullText));
        return fullText;
    }
    async processAudio(filePath, reusePreviousTranscript = true, chunkTime = 30) {
        const parsed = path.parse(filePath);
        const outputPath = `${parsed.dir}/${parsed.name}/transcript.json`;
        const exists = await (0, utils_1.fileExists)(outputPath);
        if (exists && reusePreviousTranscript) {
            const fileContent = (await (0, utils_1.readFile)(outputPath, "utf8"));
            return outputPath.endsWith("txt")
                ? fileContent.split("\n")
                : JSON.parse(fileContent);
        }
        const chunks = await this.chunk(filePath, parsed.dir, chunkTime, reusePreviousTranscript);
        return this.transcribeChunks(chunks, outputPath, reusePreviousTranscript);
    }
    async *streamKeyFrameExtraction(filePath, videoJsonPath, reusePreviousKeyframes = true, interval = 10) {
        if (reusePreviousKeyframes && fs.existsSync(videoJsonPath)) {
            const contents = await (0, utils_1.readFile)(videoJsonPath);
            const data = JSON.parse(contents.toString());
            for (const keyframe of data)
                yield { ...keyframe, usd_cost: 0 };
            return;
        }
        const outputDir = path.dirname(videoJsonPath);
        const keyframesDir = path.join(outputDir, "keyframes");
        await fs.promises.mkdir(keyframesDir, { recursive: true });
        const command = `ffmpeg -i "${filePath}" -vf "fps=1/${interval},scale=640:-1" "${keyframesDir}/frame%04d.jpg"`;
        await execAsync(command);
        const keyframes = await fs.promises.readdir(keyframesDir);
        const allKeyframes = [];
        for (const keyframe of keyframes) {
            const keyframePath = path.join(keyframesDir, keyframe);
            const keyframeName = path.parse(keyframe).name;
            const keyframeDescriptionPath = path.join(keyframesDir, `${keyframeName}.json`);
            const descriptionExists = await (0, utils_1.fileExists)(keyframeDescriptionPath);
            if (descriptionExists && reusePreviousKeyframes) {
                const cached = await (0, utils_1.readFile)(keyframeDescriptionPath);
                const cachedJson = JSON.parse(cached.toString());
                yield { ...cachedJson, usd_cost: 0 };
                allKeyframes.push(cachedJson);
                continue;
            }
            const description = await this.describeKeyframe(keyframePath);
            const frameNumber = parseInt(keyframe.match(/\d+/)?.[0] ?? "0", 10);
            const keyframeJson = {
                path: keyframePath,
                description: description.choices[0].message.content,
                timestamp: frameNumber * interval,
                usd_cost: description.usd_cost,
            };
            await fs.promises.writeFile(keyframeDescriptionPath, JSON.stringify(keyframeJson, null, 2));
            yield keyframeJson;
            allKeyframes.push(keyframeJson);
        }
        await fs.promises.writeFile(videoJsonPath, JSON.stringify(allKeyframes, null, 2));
    }
    async extractKeyframes(filePath, outputPath, reusePreviousKeyframes = true, interval = 10) {
        const keyframes = [];
        for await (const keyframe of this.streamKeyFrameExtraction(filePath, outputPath, reusePreviousKeyframes, interval)) {
            keyframes.push(keyframe);
        }
        await fs.promises.writeFile(outputPath, JSON.stringify(keyframes, null, 2));
        return keyframes;
    }
    async describeKeyframe(keyframePath) {
        const question = "Describe this image in detail, focusing on the main elements and actions visible.";
        const base64 = await fs.promises.readFile(keyframePath, {
            encoding: "base64",
        });
        const image = `data:image/jpeg;base64,${base64}`;
        return this.clients.createCompletion("openai", {
            model: types_1.Models.openai.GPT_4o,
            max_tokens: 2500,
            timeout: 20000,
            messages: [
                {
                    role: "user",
                    content: [
                        { type: "text", text: question },
                        { type: "image_url", image_url: { url: image } },
                    ],
                },
            ],
        });
    }
    async *streamProcessVideo(filePath, reusePreviousTranscript = true, chunkTime = 30, onChunkingProgress) {
        const parsed = path.parse(filePath);
        const videoJson = `${parsed.dir}/${parsed.name}/video.json`;
        console.log("Processing audio...");
        const transcriptions = this.streamProcessAudio(filePath, reusePreviousTranscript, chunkTime, onChunkingProgress);
        console.log("Extracting keyframes...");
        const videoAnalysis = this.streamKeyFrameExtraction(filePath, videoJson, reusePreviousTranscript, chunkTime);
        for await (const frame of videoAnalysis) {
            const transcription = (await transcriptions.next())
                ?.value;
            yield {
                frame,
                transcription: transcription || {
                    chunkPath: "",
                    text: "[missing transcript]",
                    usd_cost: 0,
                },
            };
        }
    }
    async *streamProcessAudio(filePath, reusePreviousTranscript = true, chunkTime = 30, onChunkingProgress) {
        const parsed = path.parse(filePath);
        const outputPath = `${parsed.dir}/${parsed.name}/transcript.json`;
        const exists = await (0, utils_1.fileExists)(outputPath);
        if (exists && reusePreviousTranscript) {
            console.log(`Transcription ${outputPath} already exists, using cached data`);
            const fileContent = await (0, utils_1.readFile)(outputPath, "utf8");
            const lines = outputPath.endsWith("txt")
                ? fileContent.split("\n")
                : JSON.parse(fileContent);
            for (const line of lines) {
                if (typeof line === "string") {
                    yield { chunkPath: "", text: line, usd_cost: 0 };
                }
                else {
                    yield line;
                }
            }
            return;
        }
        const chunks = await this.chunk(filePath, parsed.dir, chunkTime, reusePreviousTranscript, onChunkingProgress);
        for await (const chunk of this.streamTranscription(chunks, outputPath, reusePreviousTranscript)) {
            yield chunk;
        }
    }
}
exports.MediaProcessorService = MediaProcessorService;
//# sourceMappingURL=MediaProcessorService.js.map