"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JobManager = void 0; const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const jobStore_1 = require("../db/jobStore"); const processor_1 = require("../../utils/processor"); const subtitleGenerator_1 = require("./subtitleGenerator"); const muxer_1 = require("./muxer"); const config_1 = require("../../config/config"); const mediaUtils_1 = require("../../utils/mediaUtils"); const events_1 = require("events"); class JobManager { constructor() { this.queue = []; this.processing = false; this.pausedJobs = new Set(); this.emitter = new events_1.EventEmitter(); this.pollInterval = null; this.recoverStuckJobs(); this.emitter.setMaxListeners(100); } recoverStuckJobs() { const jobs = (0, jobStore_1.getAllJobs)(); for (const job of jobs) { if (job.status === 'processing') { (0, jobStore_1.updateJobStatus)(job.id, 'failed', 'Server restarted while job was in progress. Click Restart to resume from the last checkpoint.'); } } } createJob(videoPath, configOverride = {}, outputOptions = {}) { const baseConfig = (0, config_1.getDefaultConfig)(); const mergedConfig = { ...baseConfig, ...configOverride }; const filename = path_1.default.basename(videoPath); const opts = { audio: outputOptions.audio !== false, subtitles: outputOptions.subtitles !== false, muxed: outputOptions.muxed || false }; return (0, jobStore_1.createJob)(videoPath, filename, mergedConfig, opts); } async startJob(jobId) { const job = (0, jobStore_1.getJob)(jobId); if (!job) throw new Error('Job not found'); if (job.status === 'processing') throw new Error('Job is already processing'); if (job.status === 'completed') throw new Error('Job is already completed'); (0, jobStore_1.updateJobStatus)(jobId, 'queued'); this.queue.push(jobId); this.processNext(); } async pauseJob(jobId) { const job = (0, jobStore_1.getJob)(jobId); if (!job) throw new Error('Job not found'); if (job.status !== 'processing') throw new Error('Only processing jobs can be paused'); this.pausedJobs.add(jobId); (0, jobStore_1.updateJobStatus)(jobId, 'paused'); this.emitProgress(jobId); } async restartJob(jobId) { const job = (0, jobStore_1.getJob)(jobId); if (!job) throw new Error('Job not found'); if (job.status !== 'failed' && job.status !== 'paused' && job.status !== 'cancelled') { throw new Error('Only failed, paused, or cancelled jobs can be restarted'); } this.pausedJobs.delete(jobId); (0, jobStore_1.updateJobStatus)(jobId, 'queued'); this.queue.push(jobId); this.processNext(); } async cancelJob(jobId) { const job = (0, jobStore_1.getJob)(jobId); if (!job) throw new Error('Job not found'); if (job.status === 'processing') { this.pausedJobs.add(jobId); } (0, jobStore_1.updateJobStatus)(jobId, 'cancelled'); this.emitProgress(jobId); } deleteJob(jobId) { const job = (0, jobStore_1.getJob)(jobId); if (!job) throw new Error('Job not found'); if (job.status === 'processing') throw new Error('Cannot delete a running job'); (0, jobStore_1.deleteJob)(jobId); } listJobs() { return (0, jobStore_1.getAllJobs)(); } onJobProgress(jobId, callback) { this.emitter.on(`progress:${jobId}`, callback); if (!this.pollInterval) { this.pollInterval = setInterval(() => { for (const id of this.emitter.eventNames()) { const eventName = String(id); if (eventName.startsWith('progress:')) { const jId = eventName.replace('progress:', ''); this.emitProgress(jId); } } }, 2000); } return () => { this.emitter.off(`progress:${jobId}`, callback); }; } emitProgress(jobId) { const job = (0, jobStore_1.getJob)(jobId); if (!job) return; const data = { id: job.id, status: job.status, progress: job.progress, currentIndex: job.current_index, totalUnits: job.total_units, segments: JSON.parse(job.segments || '[]'), error: job.error, output_audio: job.output_audio, output_subtitles_srt: job.output_subtitles_srt, output_subtitles_vtt: job.output_subtitles_vtt, output_muxed: job.output_muxed }; this.emitter.emit(`progress:${jobId}`, data); } async processNext() { if (this.processing) return; while (this.queue.length > 0) { this.processing = true; const jobId = this.queue.shift(); const job = (0, jobStore_1.getJob)(jobId); if (!job || job.status !== 'queued') continue; try { await this.processJob(job); } catch (err) { console.error(`Job ${jobId} failed:`, err.message); } } this.processing = false; } async processJob(job) { (0, jobStore_1.updateJobStatus)(job.id, 'processing'); this.emitProgress(job.id); const config = JSON.parse(job.config); const outputOptions = JSON.parse(job.output_options); const existingSegments = JSON.parse(job.segments || '[]'); const lastContext = JSON.parse(job.last_context || '{}'); const startIndex = existingSegments.length > 0 ? job.current_index : 0; const startTimePosition = job.current_time_position || 0; const videoDuration = (0, mediaUtils_1.getVideoDuration)(job.video_path); const totalUnits = config.batchTimeMode ? Math.floor(videoDuration / config.batchWindowDuration) : Math.floor(videoDuration / config.captureIntervalSeconds); (0, jobStore_1.saveCheckpoint)(job.id, JSON.stringify(existingSegments), startIndex, totalUnits, startTimePosition, JSON.stringify(lastContext), 0); this.emitProgress(job.id); try { const result = await (0, processor_1.generateAudioDescriptionFromOptions)(job.video_path, config, { startIndex, existingSegments, lastContext, currentTimePosition: startTimePosition, onProgress: (info) => { if (this.pausedJobs.has(job.id)) { throw new Error('JOB_PAUSED'); } const allSegments = existingSegments.length > 0 && info.index === startIndex ? [...existingSegments, info.segment] : (() => { const currentJob = (0, jobStore_1.getJob)(job.id); if (!currentJob) return [info.segment]; const segs = JSON.parse(currentJob.segments || '[]'); segs.push(info.segment); return segs; })(); const progress = totalUnits > 0 ? Math.min(((info.index + 1) / totalUnits) * 100, 99) : 50; (0, jobStore_1.saveCheckpoint)(job.id, JSON.stringify(allSegments), info.index + 1, totalUnits, info.segment.startTime + info.segment.duration + (config.batchTimeMode ? 0.5 : 0.25), JSON.stringify(lastContext), progress); this.emitProgress(job.id); } }); // All segments from the result const segments = result.segments || []; // Combine audio segments into final audio (use the result's pre-combined file) const outputAudio = result.audioDescriptionFile; let outputSubtitlesSrt = null; let outputSubtitlesVtt = null; let outputMuxed = null; const baseName = path_1.default.basename(job.video_path, path_1.default.extname(job.video_path)); const outputDir = config.outputDir; if (outputOptions.subtitles && segments.length > 0) { const srtPath = path_1.default.join(outputDir, `${baseName}_description.srt`); const vttPath = path_1.default.join(outputDir, `${baseName}_description.vtt`); fs_1.default.writeFileSync(srtPath, (0, subtitleGenerator_1.generateSRT)(segments, videoDuration)); fs_1.default.writeFileSync(vttPath, (0, subtitleGenerator_1.generateVTT)(segments, videoDuration)); outputSubtitlesSrt = srtPath; outputSubtitlesVtt = vttPath; } if (outputOptions.muxed && fs_1.default.existsSync(outputAudio)) { const muxedPath = path_1.default.join(outputDir, `${baseName}_described.mkv`); (0, muxer_1.muxAudioDescription)(job.video_path, outputAudio, muxedPath); outputMuxed = muxedPath; } (0, jobStore_1.saveJobOutputs)(job.id, { audio: outputAudio, subtitlesSrt: outputSubtitlesSrt || undefined, subtitlesVtt: outputSubtitlesVtt || undefined, muxed: outputMuxed || undefined }); (0, jobStore_1.saveCheckpoint)(job.id, JSON.stringify(segments), totalUnits, totalUnits, 0, '{}', 100); (0, jobStore_1.updateJobStatus)(job.id, 'completed'); this.emitProgress(job.id); } catch (err) { if (err.message === 'JOB_PAUSED') { (0, jobStore_1.updateJobStatus)(job.id, 'paused'); this.emitProgress(job.id); return; } const errorMsg = err.message || 'Unknown error'; (0, jobStore_1.updateJobStatus)(job.id, 'failed', errorMsg); this.emitProgress(job.id); } } } exports.JobManager = JobManager; //# sourceMappingURL=jobManager.js.map