239 lines
10 KiB
JavaScript
239 lines
10 KiB
JavaScript
|
|
"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
|