Rewrite frontend as single self-contained HTML file — all CSS/JS inline, no external files to fail loading
This commit is contained in:
3
dist/server/app.d.ts
vendored
Normal file
3
dist/server/app.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import express from 'express';
|
||||
import { JobManager } from './services/jobManager';
|
||||
export declare function createApp(jobManager: JobManager): express.Application;
|
||||
31
dist/server/app.js
vendored
Normal file
31
dist/server/app.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createApp = createApp;
|
||||
const express_1 = __importDefault(require("express"));
|
||||
const cors_1 = __importDefault(require("cors"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const auth_1 = require("./middleware/auth");
|
||||
const auth_2 = __importDefault(require("./routes/auth"));
|
||||
const config_1 = __importDefault(require("./routes/config"));
|
||||
const files_1 = __importDefault(require("./routes/files"));
|
||||
const jobs_1 = require("./routes/jobs");
|
||||
function createApp(jobManager) {
|
||||
const app = (0, express_1.default)();
|
||||
app.use((0, cors_1.default)());
|
||||
app.use(express_1.default.json({ limit: '50mb' }));
|
||||
// Auth middleware
|
||||
app.use(auth_1.basicAuth);
|
||||
// API routes
|
||||
app.use('/api/auth', auth_2.default);
|
||||
app.use('/api/config', config_1.default);
|
||||
app.use('/api/files', files_1.default);
|
||||
app.use('/api/jobs', (0, jobs_1.createJobsRouter)(jobManager));
|
||||
// Serve static frontend from src/server/public (works with ts-node and compiled)
|
||||
const publicDir = path_1.default.resolve(__dirname, '..', '..', 'src', 'server', 'public');
|
||||
app.use(express_1.default.static(publicDir));
|
||||
return app;
|
||||
}
|
||||
//# sourceMappingURL=app.js.map
|
||||
1
dist/server/app.js.map
vendored
Normal file
1
dist/server/app.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":";;;;;AAWA,8BAoBC;AA/BD,sDAA8B;AAC9B,gDAAwB;AACxB,gDAAwB;AAExB,4CAA8C;AAC9C,yDAAuC;AACvC,6DAA2C;AAC3C,2DAAyC;AACzC,wCAAiD;AAGjD,SAAgB,SAAS,CAAC,UAAsB;IAC9C,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,GAAG,CAAC,GAAG,CAAC,IAAA,cAAI,GAAE,CAAC,CAAC;IAChB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;IAEzC,kBAAkB;IAClB,GAAG,CAAC,GAAG,CAAC,gBAAS,CAAC,CAAC;IAEnB,aAAa;IACb,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,cAAU,CAAC,CAAC;IACjC,GAAG,CAAC,GAAG,CAAC,aAAa,EAAE,gBAAY,CAAC,CAAC;IACrC,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,eAAW,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,IAAA,uBAAgB,EAAC,UAAU,CAAC,CAAC,CAAC;IAEnD,iFAAiF;IACjF,MAAM,SAAS,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACjF,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IAEnC,OAAO,GAAG,CAAC;AACb,CAAC"}
|
||||
3
dist/server/db/index.d.ts
vendored
Normal file
3
dist/server/db/index.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import Database from 'better-sqlite3';
|
||||
export declare function getDb(): Database.Database;
|
||||
export declare function closeDb(): void;
|
||||
62
dist/server/db/index.js
vendored
Normal file
62
dist/server/db/index.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getDb = getDb;
|
||||
exports.closeDb = closeDb;
|
||||
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const DB_PATH = path_1.default.resolve('./data/server.db');
|
||||
let db;
|
||||
function getDb() {
|
||||
if (!db) {
|
||||
const dir = path_1.default.dirname(DB_PATH);
|
||||
if (!fs_1.default.existsSync(dir)) {
|
||||
fs_1.default.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
db = new better_sqlite3_1.default(DB_PATH);
|
||||
db.pragma('journal_mode = WAL');
|
||||
db.pragma('foreign_keys = ON');
|
||||
migrate();
|
||||
}
|
||||
return db;
|
||||
}
|
||||
function migrate() {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS jobs (
|
||||
id TEXT PRIMARY KEY,
|
||||
video_path TEXT NOT NULL,
|
||||
video_filename TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
config TEXT NOT NULL,
|
||||
progress REAL DEFAULT 0,
|
||||
current_index INTEGER DEFAULT 0,
|
||||
total_units INTEGER DEFAULT 0,
|
||||
segments TEXT DEFAULT '[]',
|
||||
last_context TEXT DEFAULT '{}',
|
||||
current_time_position REAL DEFAULT 0,
|
||||
error TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
completed_at TEXT,
|
||||
output_audio TEXT,
|
||||
output_subtitles_srt TEXT,
|
||||
output_subtitles_vtt TEXT,
|
||||
output_muxed TEXT,
|
||||
output_options TEXT DEFAULT '{}'
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
}
|
||||
function closeDb() {
|
||||
if (db) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/server/db/index.js.map
vendored
Normal file
1
dist/server/db/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/db/index.ts"],"names":[],"mappings":";;;;;AAQA,sBAYC;AAkCD,0BAIC;AA1DD,oEAAsC;AACtC,gDAAwB;AACxB,4CAAoB;AAEpB,MAAM,OAAO,GAAG,cAAI,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;AAEjD,IAAI,EAAqB,CAAC;AAE1B,SAAgB,KAAK;IACnB,IAAI,CAAC,EAAE,EAAE,CAAC;QACR,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAClC,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,YAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,CAAC;QACD,EAAE,GAAG,IAAI,wBAAQ,CAAC,OAAO,CAAC,CAAC;QAC3B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;QAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;QAC/B,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,OAAO;IACd,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BP,CAAC,CAAC;AACL,CAAC;AAED,SAAgB,OAAO;IACrB,IAAI,EAAE,EAAE,CAAC;QACP,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
|
||||
42
dist/server/db/jobStore.d.ts
vendored
Normal file
42
dist/server/db/jobStore.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
export interface OutputOptions {
|
||||
audio: boolean;
|
||||
subtitles: boolean;
|
||||
muxed: boolean;
|
||||
}
|
||||
export interface Job {
|
||||
id: string;
|
||||
video_path: string;
|
||||
video_filename: string;
|
||||
status: 'pending' | 'queued' | 'processing' | 'paused' | 'completed' | 'failed' | 'cancelled';
|
||||
config: string;
|
||||
progress: number;
|
||||
current_index: number;
|
||||
total_units: number;
|
||||
segments: string;
|
||||
last_context: string;
|
||||
current_time_position: number;
|
||||
error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
completed_at: string | null;
|
||||
output_audio: string | null;
|
||||
output_subtitles_srt: string | null;
|
||||
output_subtitles_vtt: string | null;
|
||||
output_muxed: string | null;
|
||||
output_options: string;
|
||||
}
|
||||
export declare function getAllJobs(): Job[];
|
||||
export declare function getJob(id: string): Job | undefined;
|
||||
export declare function createJob(videoPath: string, filename: string, config: object, outputOptions: OutputOptions): Job;
|
||||
export declare function updateJobStatus(id: string, status: Job['status'], error?: string): void;
|
||||
export declare function saveCheckpoint(id: string, segments: string, currentIndex: number, totalUnits: number, currentTimePosition: number, lastContext: string, progress: number): void;
|
||||
export declare function saveJobOutputs(id: string, outputs: {
|
||||
audio?: string;
|
||||
subtitlesSrt?: string;
|
||||
subtitlesVtt?: string;
|
||||
muxed?: string;
|
||||
}): void;
|
||||
export declare function deleteJob(id: string): void;
|
||||
export declare function getConfigValue(key: string): string | undefined;
|
||||
export declare function setConfigValue(key: string, value: string): void;
|
||||
export declare function getAllConfig(): Record<string, string>;
|
||||
77
dist/server/db/jobStore.js
vendored
Normal file
77
dist/server/db/jobStore.js
vendored
Normal file
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.getAllJobs = getAllJobs;
|
||||
exports.getJob = getJob;
|
||||
exports.createJob = createJob;
|
||||
exports.updateJobStatus = updateJobStatus;
|
||||
exports.saveCheckpoint = saveCheckpoint;
|
||||
exports.saveJobOutputs = saveJobOutputs;
|
||||
exports.deleteJob = deleteJob;
|
||||
exports.getConfigValue = getConfigValue;
|
||||
exports.setConfigValue = setConfigValue;
|
||||
exports.getAllConfig = getAllConfig;
|
||||
const db_1 = require("../db");
|
||||
const uuid_1 = require("uuid");
|
||||
function getAllJobs() {
|
||||
const db = (0, db_1.getDb)();
|
||||
return db.prepare('SELECT * FROM jobs ORDER BY created_at DESC').all();
|
||||
}
|
||||
function getJob(id) {
|
||||
const db = (0, db_1.getDb)();
|
||||
return db.prepare('SELECT * FROM jobs WHERE id = ?').get(id);
|
||||
}
|
||||
function createJob(videoPath, filename, config, outputOptions) {
|
||||
const db = (0, db_1.getDb)();
|
||||
const id = (0, uuid_1.v4)();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(`
|
||||
INSERT INTO jobs (id, video_path, video_filename, config, output_options, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(id, videoPath, filename, JSON.stringify(config), JSON.stringify(outputOptions), now, now);
|
||||
return getJob(id);
|
||||
}
|
||||
function updateJobStatus(id, status, error) {
|
||||
const db = (0, db_1.getDb)();
|
||||
const now = new Date().toISOString();
|
||||
const completedAt = status === 'completed' ? now : null;
|
||||
db.prepare(`
|
||||
UPDATE jobs SET status = ?, error = ?, updated_at = ?, completed_at = ? WHERE id = ?
|
||||
`).run(status, error || null, now, completedAt, id);
|
||||
}
|
||||
function saveCheckpoint(id, segments, currentIndex, totalUnits, currentTimePosition, lastContext, progress) {
|
||||
const db = (0, db_1.getDb)();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(`
|
||||
UPDATE jobs SET segments = ?, current_index = ?, total_units = ?, current_time_position = ?, last_context = ?, progress = ?, updated_at = ? WHERE id = ?
|
||||
`).run(segments, currentIndex, totalUnits, currentTimePosition, lastContext, progress, now, id);
|
||||
}
|
||||
function saveJobOutputs(id, outputs) {
|
||||
const db = (0, db_1.getDb)();
|
||||
const now = new Date().toISOString();
|
||||
db.prepare(`
|
||||
UPDATE jobs SET output_audio = ?, output_subtitles_srt = ?, output_subtitles_vtt = ?, output_muxed = ?, updated_at = ? WHERE id = ?
|
||||
`).run(outputs.audio || null, outputs.subtitlesSrt || null, outputs.subtitlesVtt || null, outputs.muxed || null, now, id);
|
||||
}
|
||||
function deleteJob(id) {
|
||||
const db = (0, db_1.getDb)();
|
||||
db.prepare('DELETE FROM jobs WHERE id = ?').run(id);
|
||||
}
|
||||
function getConfigValue(key) {
|
||||
const db = (0, db_1.getDb)();
|
||||
const row = db.prepare('SELECT value FROM config WHERE key = ?').get(key);
|
||||
return row?.value;
|
||||
}
|
||||
function setConfigValue(key, value) {
|
||||
const db = (0, db_1.getDb)();
|
||||
db.prepare('INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)').run(key, value);
|
||||
}
|
||||
function getAllConfig() {
|
||||
const db = (0, db_1.getDb)();
|
||||
const rows = db.prepare('SELECT key, value FROM config').all();
|
||||
const config = {};
|
||||
for (const row of rows) {
|
||||
config[row.key] = row.value;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
//# sourceMappingURL=jobStore.js.map
|
||||
1
dist/server/db/jobStore.js.map
vendored
Normal file
1
dist/server/db/jobStore.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"jobStore.js","sourceRoot":"","sources":["../../../src/server/db/jobStore.ts"],"names":[],"mappings":"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"}
|
||||
1
dist/server/index.d.ts
vendored
Normal file
1
dist/server/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import 'dotenv/config';
|
||||
37
dist/server/index.js
vendored
Normal file
37
dist/server/index.js
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
require("dotenv/config");
|
||||
const app_1 = require("./app");
|
||||
const jobManager_1 = require("./services/jobManager");
|
||||
const db_1 = require("./db");
|
||||
const PORT = parseInt(process.env.SERVER_PORT || '3000', 10);
|
||||
const USERNAME = process.env.SERVER_USERNAME || 'admin';
|
||||
const PASSWORD = process.env.SERVER_PASSWORD || 'aidio2024';
|
||||
// Initialize database
|
||||
(0, db_1.getDb)();
|
||||
// Create job manager
|
||||
const jobManager = new jobManager_1.JobManager();
|
||||
// Create app
|
||||
const app = (0, app_1.createApp)(jobManager);
|
||||
app.listen(PORT, () => {
|
||||
console.log(`
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ Audio Description Server v1.0 ║
|
||||
║ http://localhost:${PORT} ║
|
||||
║ ║
|
||||
║ Username: ${USERNAME.padEnd(41)}║
|
||||
║ Password: ${PASSWORD.padEnd(41)}║
|
||||
╚══════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\nShutting down...');
|
||||
(0, db_1.closeDb)();
|
||||
process.exit(0);
|
||||
});
|
||||
process.on('SIGTERM', () => {
|
||||
(0, db_1.closeDb)();
|
||||
process.exit(0);
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/server/index.js.map
vendored
Normal file
1
dist/server/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":";;AAAA,yBAAuB;AACvB,+BAAkC;AAClC,sDAAmD;AACnD,6BAAsC;AAEtC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;AAC7D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC;AACxD,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,WAAW,CAAC;AAE5D,sBAAsB;AACtB,IAAA,UAAK,GAAE,CAAC;AAER,qBAAqB;AACrB,MAAM,UAAU,GAAG,IAAI,uBAAU,EAAE,CAAC;AAEpC,aAAa;AACb,MAAM,GAAG,GAAG,IAAA,eAAS,EAAC,UAAU,CAAC,CAAC;AAElC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACpB,OAAO,CAAC,GAAG,CAAC;;;yBAGW,IAAI;;kBAEX,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;kBACnB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;;GAElC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,oBAAoB;AACpB,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;IACxB,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAClC,IAAA,YAAO,GAAE,CAAC;IACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACzB,IAAA,YAAO,GAAE,CAAC;IACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
||||
2
dist/server/middleware/auth.d.ts
vendored
Normal file
2
dist/server/middleware/auth.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
export declare function basicAuth(req: Request, res: Response, next: NextFunction): void;
|
||||
32
dist/server/middleware/auth.js
vendored
Normal file
32
dist/server/middleware/auth.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.basicAuth = basicAuth;
|
||||
const AUTH_USERNAME = process.env.SERVER_USERNAME || 'admin';
|
||||
const AUTH_PASSWORD = process.env.SERVER_PASSWORD || 'aidio2024';
|
||||
function basicAuth(req, res, next) {
|
||||
// Allow login/check endpoints and all non-API routes (static files, HTML)
|
||||
if (req.path === '/api/auth/login' || req.path === '/api/auth/check' || !req.path.startsWith('/api/')) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
// Support token via query param for SSE (EventSource doesn't support custom headers)
|
||||
let authHeader = req.headers.authorization;
|
||||
if (!authHeader && req.query.token) {
|
||||
const token = Array.isArray(req.query.token) ? req.query.token[0] : req.query.token;
|
||||
authHeader = `Basic ${token}`;
|
||||
}
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Audio Description Server"');
|
||||
res.status(401).json({ error: 'Authentication required' });
|
||||
return;
|
||||
}
|
||||
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8');
|
||||
const [username, password] = credentials.split(':');
|
||||
if (username === AUTH_USERNAME && password === AUTH_PASSWORD) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="Audio Description Server"');
|
||||
res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
//# sourceMappingURL=auth.js.map
|
||||
1
dist/server/middleware/auth.js.map
vendored
Normal file
1
dist/server/middleware/auth.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/server/middleware/auth.ts"],"names":[],"mappings":";;AAKA,8BA8BC;AAjCD,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC;AAC7D,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,WAAW,CAAC;AAEjE,SAAgB,SAAS,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;IACvE,0EAA0E;IAC1E,IAAI,GAAG,CAAC,IAAI,KAAK,iBAAiB,IAAI,GAAG,CAAC,IAAI,KAAK,iBAAiB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACtG,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,qFAAqF;IACrF,IAAI,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAC3C,IAAI,CAAC,UAAU,IAAI,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC;QACpF,UAAU,GAAG,SAAS,KAAK,EAAE,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,wCAAwC,CAAC,CAAC;QAC5E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;QAC3D,OAAO;IACT,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjF,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEpD,IAAI,QAAQ,KAAK,aAAa,IAAI,QAAQ,KAAK,aAAa,EAAE,CAAC;QAC7D,IAAI,EAAE,CAAC;QACP,OAAO;IACT,CAAC;IAED,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,wCAAwC,CAAC,CAAC;IAC5E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;AACzD,CAAC"}
|
||||
76
dist/server/public/app.d.ts
vendored
Normal file
76
dist/server/public/app.d.ts
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
interface Job {
|
||||
id: string;
|
||||
video_path: string;
|
||||
video_filename: string;
|
||||
status: 'pending' | 'queued' | 'processing' | 'paused' | 'completed' | 'failed' | 'cancelled';
|
||||
config: string;
|
||||
progress: number;
|
||||
current_index: number;
|
||||
total_units: number;
|
||||
segments: string;
|
||||
last_context: string;
|
||||
current_time_position: number;
|
||||
error: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
completed_at: string | null;
|
||||
output_audio: string | null;
|
||||
output_subtitles_srt: string | null;
|
||||
output_subtitles_vtt: string | null;
|
||||
output_muxed: string | null;
|
||||
output_options: string;
|
||||
}
|
||||
interface AudioSegment {
|
||||
audioFile: string;
|
||||
startTime: number;
|
||||
duration: number;
|
||||
description: string;
|
||||
}
|
||||
interface ProgressData {
|
||||
id: string;
|
||||
status: string;
|
||||
progress: number;
|
||||
currentIndex: number;
|
||||
totalUnits: number;
|
||||
segments: AudioSegment[];
|
||||
error: string | null;
|
||||
output_audio: string | null;
|
||||
output_subtitles_srt: string | null;
|
||||
output_subtitles_vtt: string | null;
|
||||
output_muxed: string | null;
|
||||
}
|
||||
interface FileInfo {
|
||||
filename: string;
|
||||
filePath: string;
|
||||
size: number;
|
||||
}
|
||||
declare let authToken: string | null;
|
||||
declare let selectedFilePath: string | null;
|
||||
declare const sseMap: Map<string, EventSource>;
|
||||
declare let pollTimer: number | null;
|
||||
declare const $: (sel: string) => HTMLElement;
|
||||
declare const $$: (sel: string) => NodeListOf<HTMLElement>;
|
||||
declare const el: (id: string) => HTMLElement;
|
||||
declare function apiHeaders(): Record<string, string>;
|
||||
declare function api(method: string, url: string, body?: unknown): Promise<Response>;
|
||||
declare function apiJson<T>(method: string, url: string, body?: unknown): Promise<T>;
|
||||
declare function showLoginScreen(): void;
|
||||
declare function showMainScreen(): void;
|
||||
declare function switchTab(name: string): void;
|
||||
declare function escapeHtml(str: string | null | undefined): string;
|
||||
declare function formatSize(bytes: number): string;
|
||||
declare function loadBrowseFiles(): Promise<void>;
|
||||
declare const videoUpload: HTMLInputElement;
|
||||
declare const uploadName: HTMLElement;
|
||||
declare function loadJobs(): Promise<void>;
|
||||
declare function renderJobs(jobs: Job[]): void;
|
||||
declare function handleJobAction(id: string, action: string): Promise<void>;
|
||||
declare function startPolling(): void;
|
||||
declare function connectSSE(jobId: string): void;
|
||||
declare function updateJobCard(jobId: string, data: ProgressData): void;
|
||||
declare function loadSettings(): Promise<void>;
|
||||
declare let selectedFiles: Set<string>;
|
||||
declare function loadFilesList(): Promise<void>;
|
||||
declare function updateFileSelection(): void;
|
||||
declare function loadConfigDefaults(): Promise<void>;
|
||||
declare function initApp(): void;
|
||||
568
dist/server/public/app.js
vendored
Normal file
568
dist/server/public/app.js
vendored
Normal file
@@ -0,0 +1,568 @@
|
||||
"use strict";
|
||||
// ── Types ────────────────────────────────────────────
|
||||
// ── State ────────────────────────────────────────────
|
||||
let authToken = sessionStorage.getItem('authToken');
|
||||
let selectedFilePath = null;
|
||||
const sseMap = new Map();
|
||||
let pollTimer = null;
|
||||
// ── DOM helpers ───────────────────────────────────────
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
const $$ = (sel) => document.querySelectorAll(sel);
|
||||
const el = (id) => document.getElementById(id);
|
||||
// ── API ───────────────────────────────────────────────
|
||||
function apiHeaders() {
|
||||
const h = { 'Content-Type': 'application/json' };
|
||||
if (authToken)
|
||||
h['Authorization'] = `Basic ${authToken}`;
|
||||
return h;
|
||||
}
|
||||
async function api(method, url, body) {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: apiHeaders(),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (res.status === 401) {
|
||||
sessionStorage.removeItem('authToken');
|
||||
authToken = null;
|
||||
showLoginScreen();
|
||||
throw new Error('Unauthorized');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
async function apiJson(method, url, body) {
|
||||
const res = await api(method, url, body);
|
||||
const data = await res.json();
|
||||
if (!res.ok)
|
||||
throw new Error(data.error || 'Request failed');
|
||||
return data;
|
||||
}
|
||||
// ── Screen switching ──────────────────────────────────
|
||||
function showLoginScreen() {
|
||||
el('login-screen').classList.remove('hidden');
|
||||
el('main-screen').classList.add('hidden');
|
||||
}
|
||||
function showMainScreen() {
|
||||
el('login-screen').classList.add('hidden');
|
||||
el('main-screen').classList.remove('hidden');
|
||||
}
|
||||
// ── Tab navigation ────────────────────────────────────
|
||||
function switchTab(name) {
|
||||
$$('button.tab').forEach(b => b.classList.remove('active'));
|
||||
document.querySelector(`button.tab[data-tab="${name}"]`)?.classList.add('active');
|
||||
$$('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
const pane = document.getElementById(name);
|
||||
if (pane)
|
||||
pane.classList.add('active');
|
||||
if (name === 'dashboard')
|
||||
loadJobs();
|
||||
if (name === 'files')
|
||||
loadFilesList();
|
||||
}
|
||||
$$('button.tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => switchTab(btn.dataset.tab || ''));
|
||||
});
|
||||
// ── Mini tabs (video source) ──────────────────────────
|
||||
$$('button.tab-mini').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
$$('button.tab-mini').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
$$('.src-panel').forEach(p => p.classList.remove('active'));
|
||||
const panel = document.getElementById('src-' + (btn.dataset.src || ''));
|
||||
if (panel)
|
||||
panel.classList.add('active');
|
||||
});
|
||||
});
|
||||
// ── Login ─────────────────────────────────────────────
|
||||
el('login-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = el('login-username').value;
|
||||
const password = el('login-password').value;
|
||||
try {
|
||||
const res = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.authenticated) {
|
||||
authToken = data.token;
|
||||
if (authToken)
|
||||
sessionStorage.setItem('authToken', authToken);
|
||||
showMainScreen();
|
||||
initApp();
|
||||
}
|
||||
else {
|
||||
el('login-error').textContent = data.error;
|
||||
el('login-error').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
catch {
|
||||
el('login-error').textContent = 'Connection failed';
|
||||
el('login-error').classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
el('logout-btn').addEventListener('click', () => {
|
||||
sessionStorage.removeItem('authToken');
|
||||
authToken = null;
|
||||
sseMap.forEach(s => s.close());
|
||||
sseMap.clear();
|
||||
if (pollTimer)
|
||||
clearInterval(pollTimer);
|
||||
showLoginScreen();
|
||||
});
|
||||
// ── Utils ─────────────────────────────────────────────
|
||||
function escapeHtml(str) {
|
||||
if (!str)
|
||||
return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
function formatSize(bytes) {
|
||||
if (!bytes)
|
||||
return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let i = 0;
|
||||
let size = bytes;
|
||||
while (size >= 1024 && i < units.length - 1) {
|
||||
size /= 1024;
|
||||
i++;
|
||||
}
|
||||
return `${size.toFixed(1)} ${units[i]}`;
|
||||
}
|
||||
// ── Browse files (for New Job) ────────────────────────
|
||||
async function loadBrowseFiles() {
|
||||
try {
|
||||
const data = await apiJson('GET', '/api/files');
|
||||
const sel = el('video-select');
|
||||
sel.innerHTML = '<option value="">-- Select file --</option>';
|
||||
data.files.forEach(f => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = f.filePath;
|
||||
opt.textContent = `${f.filename} (${formatSize(f.size)})`;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
el('refresh-files').addEventListener('click', loadBrowseFiles);
|
||||
el('video-select').addEventListener('change', function () {
|
||||
if (this.value)
|
||||
selectedFilePath = this.value;
|
||||
});
|
||||
// ── File upload ───────────────────────────────────────
|
||||
const videoUpload = el('video-upload');
|
||||
const uploadName = el('upload-name');
|
||||
videoUpload.addEventListener('change', function () {
|
||||
if (this.files?.length) {
|
||||
selectedFilePath = null; // will upload on submit
|
||||
uploadName.textContent = `Selected: ${this.files[0].name} (${formatSize(this.files[0].size)})`;
|
||||
}
|
||||
else {
|
||||
uploadName.textContent = '';
|
||||
}
|
||||
});
|
||||
// ── YouTube download ──────────────────────────────────
|
||||
el('download-url').addEventListener('click', async () => {
|
||||
const url = el('youtube-url').value;
|
||||
if (!url)
|
||||
return;
|
||||
const status = el('download-status');
|
||||
status.textContent = 'Downloading...';
|
||||
status.className = 'status';
|
||||
try {
|
||||
const data = await apiJson('POST', '/api/files/youtube', { url });
|
||||
status.textContent = `Downloaded: ${data.filename}`;
|
||||
status.className = 'status success';
|
||||
selectedFilePath = data.filePath;
|
||||
const sel = el('video-select');
|
||||
const opt = document.createElement('option');
|
||||
opt.value = data.filePath;
|
||||
opt.textContent = data.filename;
|
||||
opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
catch (err) {
|
||||
status.textContent = `Error: ${err.message}`;
|
||||
status.className = 'status error';
|
||||
}
|
||||
});
|
||||
// ── New Job form ──────────────────────────────────────
|
||||
el('new-job-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
if (!selectedFilePath) {
|
||||
if (videoUpload.files?.length) {
|
||||
const formData = new FormData();
|
||||
formData.append('video', videoUpload.files[0]);
|
||||
try {
|
||||
const headers = {};
|
||||
if (authToken)
|
||||
headers['Authorization'] = `Basic ${authToken}`;
|
||||
const res = await fetch('/api/files/upload', { method: 'POST', headers, body: formData });
|
||||
const data = await res.json();
|
||||
if (!res.ok)
|
||||
throw new Error(data.error || 'Upload failed');
|
||||
selectedFilePath = data.filePath;
|
||||
}
|
||||
catch (err) {
|
||||
alert('Upload error: ' + err.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
alert('Please select a video file or source');
|
||||
return;
|
||||
}
|
||||
}
|
||||
const fd = new FormData(e.target);
|
||||
const config = {};
|
||||
for (const [key, val] of fd.entries()) {
|
||||
if (key === '')
|
||||
continue;
|
||||
if (val === 'on')
|
||||
config[key] = true;
|
||||
else if (val === 'off')
|
||||
config[key] = false;
|
||||
else if (!isNaN(val) && val !== '')
|
||||
config[key] = parseFloat(val);
|
||||
else
|
||||
config[key] = val;
|
||||
}
|
||||
const outputOptions = {
|
||||
audio: fd.get('output-audio') === 'on',
|
||||
subtitles: fd.get('output-subtitles') === 'on',
|
||||
muxed: fd.get('output-muxed') === 'on',
|
||||
};
|
||||
if (config.visionProvider) {
|
||||
const vp = {};
|
||||
vp[config.visionProvider] = {
|
||||
model: config.visionModel || 'gpt-4o',
|
||||
maxTokens: config.visionMaxTokens ? parseInt(config.visionMaxTokens) : 300,
|
||||
};
|
||||
config.visionProviders = vp;
|
||||
}
|
||||
if (config.ttsProvider) {
|
||||
const tp = {};
|
||||
tp[config.ttsProvider] = {
|
||||
model: config.ttsModel || 'tts-1',
|
||||
voice: config.ttsVoice || 'alloy',
|
||||
};
|
||||
config.ttsProviders = tp;
|
||||
}
|
||||
delete config.visionModel;
|
||||
delete config.visionMaxTokens;
|
||||
delete config.ttsModel;
|
||||
delete config['output-audio'];
|
||||
delete config['output-subtitles'];
|
||||
delete config['output-muxed'];
|
||||
try {
|
||||
const data = await apiJson('POST', '/api/jobs', {
|
||||
videoPath: selectedFilePath,
|
||||
config,
|
||||
outputOptions,
|
||||
});
|
||||
await apiJson('POST', `/api/jobs/${data.job.id}/start`);
|
||||
selectedFilePath = null;
|
||||
videoUpload.value = '';
|
||||
uploadName.textContent = '';
|
||||
el('new-job-form').reset();
|
||||
switchTab('dashboard');
|
||||
}
|
||||
catch (err) {
|
||||
alert('Error creating job: ' + err.message);
|
||||
}
|
||||
});
|
||||
// ── Job list & rendering ──────────────────────────────
|
||||
async function loadJobs() {
|
||||
try {
|
||||
const data = await apiJson('GET', '/api/jobs');
|
||||
renderJobs(data.jobs);
|
||||
data.jobs.forEach(j => {
|
||||
if (j.status === 'processing' || j.status === 'queued') {
|
||||
connectSSE(j.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
function renderJobs(jobs) {
|
||||
const container = el('jobs-list');
|
||||
if (!jobs.length) {
|
||||
container.innerHTML = '<p class="empty">No jobs yet. Create one from the "New Job" tab.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = jobs.map(j => {
|
||||
const segs = JSON.parse(j.segments || '[]');
|
||||
const progressClass = j.status === 'completed' ? 'completed' : j.status === 'failed' ? 'failed' : '';
|
||||
const downloads = [];
|
||||
if (j.status === 'completed') {
|
||||
if (j.output_audio)
|
||||
downloads.push(`<a href="/api/jobs/${j.id}/download/audio" download>Audio</a>`);
|
||||
if (j.output_subtitles_srt)
|
||||
downloads.push(`<a href="/api/jobs/${j.id}/download/subtitles?format=srt" download>SRT</a>`);
|
||||
if (j.output_subtitles_vtt)
|
||||
downloads.push(`<a href="/api/jobs/${j.id}/download/subtitles?format=vtt" download>VTT</a>`);
|
||||
if (j.output_muxed)
|
||||
downloads.push(`<a href="/api/jobs/${j.id}/download/muxed" download>Muxed</a>`);
|
||||
}
|
||||
let actions = '';
|
||||
if (j.status === 'pending' || j.status === 'queued') {
|
||||
actions += `<button class="act-start" data-id="${j.id}">Start</button>`;
|
||||
}
|
||||
if (j.status === 'processing') {
|
||||
actions += `<button class="act-pause" data-id="${j.id}">Pause</button>`;
|
||||
}
|
||||
if (j.status === 'failed' || j.status === 'paused' || j.status === 'cancelled') {
|
||||
actions += `<button class="act-restart" data-id="${j.id}">Restart</button>`;
|
||||
}
|
||||
if (j.status !== 'processing') {
|
||||
actions += `<button class="act-delete danger" data-id="${j.id}">Delete</button>`;
|
||||
}
|
||||
return `
|
||||
<div class="job-card" data-id="${j.id}">
|
||||
<div class="job-card-header">
|
||||
<h3>${escapeHtml(j.video_filename)}</h3>
|
||||
<div class="job-actions">${actions}</div>
|
||||
</div>
|
||||
<span class="status-badge status-${j.status}">${j.status}</span>
|
||||
<div class="progress-bar"><div class="progress-fill ${progressClass}" style="width:${j.progress}%"></div></div>
|
||||
<div class="job-meta">
|
||||
<span>${Math.round(j.progress)}%</span>
|
||||
<span>Idx: ${j.current_index}/${j.total_units}</span>
|
||||
<span>${new Date(j.created_at).toLocaleString()}</span>
|
||||
</div>
|
||||
${j.error ? `<div class="error-msg">${escapeHtml(j.error)}</div>` : ''}
|
||||
${downloads.length ? `<div class="download-links">${downloads.join('')}</div>` : ''}
|
||||
<div class="job-detail" data-id="${j.id}">
|
||||
<div class="segment-log">${segs.map((s, i) => `<div class="segment-entry"><span class="segment-time">[${s.startTime.toFixed(1)}s]</span> ${escapeHtml(s.description)}</div>`).join('')}</div>
|
||||
</div>
|
||||
<button class="toggle-detail" data-id="${j.id}">${segs.length} segments</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
// Wire up action buttons
|
||||
container.querySelectorAll('.act-start').forEach(b => b.addEventListener('click', () => handleJobAction(b.dataset.id || '', 'start')));
|
||||
container.querySelectorAll('.act-pause').forEach(b => b.addEventListener('click', () => handleJobAction(b.dataset.id || '', 'pause')));
|
||||
container.querySelectorAll('.act-restart').forEach(b => b.addEventListener('click', () => handleJobAction(b.dataset.id || '', 'restart')));
|
||||
container.querySelectorAll('.act-delete').forEach(b => b.addEventListener('click', () => handleJobAction(b.dataset.id || '', 'delete')));
|
||||
container.querySelectorAll('.toggle-detail').forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const jobId = b.dataset.id || '';
|
||||
const detail = container.querySelector(`.job-detail[data-id="${jobId}"]`);
|
||||
if (!detail)
|
||||
return;
|
||||
detail.classList.toggle('open');
|
||||
const job = jobs.find(j => j.id === jobId);
|
||||
const segs = job ? JSON.parse(job.segments || '[]') : [];
|
||||
b.textContent = detail.classList.contains('open') ? 'Hide segments' : `${segs.length} segments`;
|
||||
});
|
||||
});
|
||||
}
|
||||
async function handleJobAction(id, action) {
|
||||
const method = action === 'delete' ? 'DELETE' : 'POST';
|
||||
const url = `/api/jobs/${id}${action === 'delete' ? '' : '/' + action}`;
|
||||
try {
|
||||
await api(method, url);
|
||||
loadJobs();
|
||||
}
|
||||
catch (err) {
|
||||
alert(`Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
el('refresh-jobs').addEventListener('click', loadJobs);
|
||||
// ── Polling ───────────────────────────────────────────
|
||||
function startPolling() {
|
||||
if (pollTimer)
|
||||
return;
|
||||
pollTimer = window.setInterval(loadJobs, 5000);
|
||||
}
|
||||
// ── SSE live progress ─────────────────────────────────
|
||||
function connectSSE(jobId) {
|
||||
if (sseMap.has(jobId))
|
||||
return;
|
||||
const es = new EventSource(`/api/jobs/${jobId}/progress?token=${encodeURIComponent(authToken)}`);
|
||||
es.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
updateJobCard(jobId, data);
|
||||
if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
|
||||
es.close();
|
||||
sseMap.delete(jobId);
|
||||
}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
sseMap.delete(jobId);
|
||||
};
|
||||
sseMap.set(jobId, es);
|
||||
}
|
||||
function updateJobCard(jobId, data) {
|
||||
const card = document.querySelector(`.job-card[data-id="${jobId}"]`);
|
||||
if (!card)
|
||||
return;
|
||||
const badge = card.querySelector('.status-badge');
|
||||
if (badge) {
|
||||
badge.className = `status-badge status-${data.status}`;
|
||||
badge.textContent = data.status;
|
||||
}
|
||||
const fill = card.querySelector('.progress-fill');
|
||||
if (fill) {
|
||||
fill.style.width = data.progress + '%';
|
||||
fill.className = 'progress-fill';
|
||||
if (data.status === 'completed')
|
||||
fill.classList.add('completed');
|
||||
else if (data.status === 'failed')
|
||||
fill.classList.add('failed');
|
||||
}
|
||||
const metaSpans = card.querySelectorAll('.job-meta span');
|
||||
if (metaSpans[0])
|
||||
metaSpans[0].textContent = Math.round(data.progress) + '%';
|
||||
if (metaSpans[1])
|
||||
metaSpans[1].textContent = `Idx: ${data.currentIndex}/${data.totalUnits}`;
|
||||
const log = card.querySelector('.segment-log');
|
||||
if (log && data.segments) {
|
||||
log.innerHTML = data.segments.map(s => `<div class="segment-entry"><span class="segment-time">[${s.startTime.toFixed(1)}s]</span> ${escapeHtml(s.description)}</div>`).join('');
|
||||
}
|
||||
const toggleBtn = card.querySelector('.toggle-detail');
|
||||
if (toggleBtn && data.segments) {
|
||||
toggleBtn.textContent = `${data.segments.length} segments`;
|
||||
}
|
||||
}
|
||||
// ── Settings ──────────────────────────────────────────
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const data = await apiJson('GET', '/api/config');
|
||||
const container = el('settings-fields');
|
||||
const entries = Object.entries(data.config || {});
|
||||
if (!entries.length) {
|
||||
container.innerHTML = '<p class="empty">No custom settings yet. Settings from .env are used as defaults.</p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = entries.map(([key, value]) => `<label>${escapeHtml(key)} <input type="text" name="${escapeHtml(key)}" value="${escapeHtml(String(value))}"></label>`).join('');
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
el('settings-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const fd = new FormData(e.target);
|
||||
const config = {};
|
||||
for (const [key, val] of fd.entries()) {
|
||||
config[key] = val;
|
||||
}
|
||||
try {
|
||||
await apiJson('PUT', '/api/config', config);
|
||||
alert('Settings saved');
|
||||
}
|
||||
catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
});
|
||||
// ── Files list ────────────────────────────────────────
|
||||
let selectedFiles = new Set();
|
||||
async function loadFilesList() {
|
||||
try {
|
||||
const data = await apiJson('GET', '/api/files');
|
||||
const tbody = document.querySelector('#files-table tbody');
|
||||
tbody.innerHTML = data.files.map(f => `
|
||||
<tr>
|
||||
<td><input type="checkbox" class="file-checkbox" data-path="${escapeHtml(f.filePath)}"></td>
|
||||
<td>${escapeHtml(f.filename)}</td>
|
||||
<td>${formatSize(f.size)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
tbody.querySelectorAll('.file-checkbox').forEach(cb => {
|
||||
cb.addEventListener('change', updateFileSelection);
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
function updateFileSelection() {
|
||||
selectedFiles.clear();
|
||||
document.querySelectorAll('.file-checkbox:checked').forEach(cb => {
|
||||
if (cb.dataset.path)
|
||||
selectedFiles.add(cb.dataset.path);
|
||||
});
|
||||
el('delete-selected-files').disabled = selectedFiles.size === 0;
|
||||
}
|
||||
el('select-all-files').addEventListener('change', function () {
|
||||
document.querySelectorAll('.file-checkbox').forEach(cb => {
|
||||
cb.checked = this.checked;
|
||||
});
|
||||
updateFileSelection();
|
||||
});
|
||||
el('delete-selected-files').addEventListener('click', () => {
|
||||
if (!confirm(`Delete ${selectedFiles.size} file(s)?`))
|
||||
return;
|
||||
alert('File deletion not yet implemented');
|
||||
});
|
||||
el('refresh-files-list').addEventListener('click', loadFilesList);
|
||||
// ── Config defaults for New Job form ─────────────────
|
||||
async function loadConfigDefaults() {
|
||||
try {
|
||||
const data = await apiJson('GET', '/api/config');
|
||||
const c = data.config || {};
|
||||
if (c.visionProvider) {
|
||||
const sel = document.querySelector('[name="visionProvider"]');
|
||||
if (sel) {
|
||||
sel.innerHTML = '<option value="openai">OpenAI</option><option value="gemini">Gemini</option><option value="ollama">Ollama</option><option value="openrouter">OpenRouter</option>';
|
||||
sel.value = c.visionProvider;
|
||||
}
|
||||
}
|
||||
if (c.ttsProvider) {
|
||||
const sel = document.querySelector('[name="ttsProvider"]');
|
||||
if (sel) {
|
||||
sel.innerHTML = '<option value="openai">OpenAI</option><option value="elevenlabs">ElevenLabs</option><option value="google">Google Cloud</option>';
|
||||
sel.value = c.ttsProvider;
|
||||
}
|
||||
}
|
||||
const fields = [
|
||||
['visionModel'], ['ttsModel'], ['ttsVoice'], ['ttsSpeedFactor'],
|
||||
['ttsInstructions', 'textarea'], ['batchWindowDuration'], ['framesInBatch'],
|
||||
['captureIntervalSeconds'], ['contextWindowSize'],
|
||||
['defaultPrompt', 'textarea'], ['changePrompt', 'textarea'], ['batchPrompt', 'textarea'],
|
||||
];
|
||||
for (const [name, tag] of fields) {
|
||||
const el = document.querySelector(`[name="${name}"]`);
|
||||
if (el && c[name] !== undefined)
|
||||
el.value = c[name];
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
// ── Init ──────────────────────────────────────────────
|
||||
function initApp() {
|
||||
loadJobs();
|
||||
loadBrowseFiles();
|
||||
loadConfigDefaults();
|
||||
startPolling();
|
||||
}
|
||||
// ── Startup ───────────────────────────────────────────
|
||||
(async () => {
|
||||
if (authToken) {
|
||||
try {
|
||||
const res = await fetch('/api/auth/check', {
|
||||
headers: { Authorization: `Basic ${authToken}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.authenticated) {
|
||||
showMainScreen();
|
||||
initApp();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch { /* fall through to login */ }
|
||||
}
|
||||
showLoginScreen();
|
||||
})();
|
||||
//# sourceMappingURL=app.js.map
|
||||
1
dist/server/public/app.js.map
vendored
Normal file
1
dist/server/public/app.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
2
dist/server/routes/auth.d.ts
vendored
Normal file
2
dist/server/routes/auth.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
30
dist/server/routes/auth.js
vendored
Normal file
30
dist/server/routes/auth.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const router = (0, express_1.Router)();
|
||||
router.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const serverUser = process.env.SERVER_USERNAME || 'admin';
|
||||
const serverPass = process.env.SERVER_PASSWORD || 'aidio2024';
|
||||
if (username === serverUser && password === serverPass) {
|
||||
const token = Buffer.from(`${username}:${password}`).toString('base64');
|
||||
res.json({ authenticated: true, token, username });
|
||||
}
|
||||
else {
|
||||
res.status(401).json({ authenticated: false, error: 'Invalid credentials' });
|
||||
}
|
||||
});
|
||||
router.get('/check', (req, res) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||
res.json({ authenticated: false });
|
||||
return;
|
||||
}
|
||||
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8');
|
||||
const [username, password] = credentials.split(':');
|
||||
const serverUser = process.env.SERVER_USERNAME || 'admin';
|
||||
const serverPass = process.env.SERVER_PASSWORD || 'aidio2024';
|
||||
res.json({ authenticated: username === serverUser && password === serverPass, username });
|
||||
});
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=auth.js.map
|
||||
1
dist/server/routes/auth.js.map
vendored
Normal file
1
dist/server/routes/auth.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/server/routes/auth.ts"],"names":[],"mappings":";;AAAA,qCAAoD;AAEpD,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;IACpD,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;IACxC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC;IAC1D,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,WAAW,CAAC;IAE9D,IAAI,QAAQ,KAAK,UAAU,IAAI,QAAQ,KAAK,UAAU,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACxE,GAAG,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrD,CAAC;SAAM,CAAC;QACN,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;IACnD,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAC7C,IAAI,CAAC,UAAU,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,GAAG,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;QACnC,OAAO;IACT,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACjF,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpD,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC;IAC1D,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,WAAW,CAAC;IAE9D,GAAG,CAAC,IAAI,CAAC,EAAE,aAAa,EAAE,QAAQ,KAAK,UAAU,IAAI,QAAQ,KAAK,UAAU,EAAE,QAAQ,EAAE,CAAC,CAAC;AAC5F,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}
|
||||
2
dist/server/routes/config.d.ts
vendored
Normal file
2
dist/server/routes/config.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
23
dist/server/routes/config.js
vendored
Normal file
23
dist/server/routes/config.js
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const jobStore_1 = require("../db/jobStore");
|
||||
const router = (0, express_1.Router)();
|
||||
router.get('/', (_req, res) => {
|
||||
const config = (0, jobStore_1.getAllConfig)();
|
||||
res.json({ config });
|
||||
});
|
||||
router.put('/', (req, res) => {
|
||||
const updates = req.body;
|
||||
if (typeof updates !== 'object' || updates === null) {
|
||||
res.status(400).json({ error: 'Body must be a JSON object of key-value pairs' });
|
||||
return;
|
||||
}
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
(0, jobStore_1.setConfigValue)(key, String(value));
|
||||
}
|
||||
const config = (0, jobStore_1.getAllConfig)();
|
||||
res.json({ config });
|
||||
});
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=config.js.map
|
||||
1
dist/server/routes/config.js.map
vendored
Normal file
1
dist/server/routes/config.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../../src/server/routes/config.ts"],"names":[],"mappings":";;AAAA,qCAAoD;AACpD,6CAA8D;AAE9D,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;IAC/C,MAAM,MAAM,GAAG,IAAA,uBAAY,GAAE,CAAC;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;IAC9C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC;IACzB,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACpD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC,CAAC;QACjF,OAAO;IACT,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,IAAA,yBAAc,EAAC,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACrC,CAAC;IACD,MAAM,MAAM,GAAG,IAAA,uBAAY,GAAE,CAAC;IAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}
|
||||
2
dist/server/routes/files.d.ts
vendored
Normal file
2
dist/server/routes/files.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
87
dist/server/routes/files.js
vendored
Normal file
87
dist/server/routes/files.js
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const express_1 = require("express");
|
||||
const multer_1 = __importDefault(require("multer"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const ytDlp_1 = require("../services/ytDlp");
|
||||
const UPLOADS_DIR = path_1.default.resolve('./uploads');
|
||||
const storage = multer_1.default.diskStorage({
|
||||
destination: (_req, _file, cb) => {
|
||||
if (!fs_1.default.existsSync(UPLOADS_DIR)) {
|
||||
fs_1.default.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||
}
|
||||
cb(null, UPLOADS_DIR);
|
||||
},
|
||||
filename: (_req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
cb(null, uniqueSuffix + path_1.default.extname(file.originalname));
|
||||
}
|
||||
});
|
||||
const upload = (0, multer_1.default)({
|
||||
storage,
|
||||
fileFilter: (_req, file, cb) => {
|
||||
const allowedMimes = [
|
||||
'video/mp4', 'video/webm', 'video/x-matroska', 'video/quicktime',
|
||||
'video/x-msvideo', 'video/mpeg', 'video/x-ms-wmv', 'video/x-flv'
|
||||
];
|
||||
if (allowedMimes.includes(file.mimetype) || file.originalname.match(/\.(mp4|mkv|webm|mov|avi|mpg|mpeg|wmv|flv)$/i)) {
|
||||
cb(null, true);
|
||||
}
|
||||
else {
|
||||
cb(new Error('Invalid file type. Only video files are allowed.'));
|
||||
}
|
||||
},
|
||||
limits: { fileSize: 10 * 1024 * 1024 * 1024 } // 10GB
|
||||
});
|
||||
const router = (0, express_1.Router)();
|
||||
router.post('/upload', upload.single('video'), (req, res) => {
|
||||
if (!req.file) {
|
||||
res.status(400).json({ error: 'No video file uploaded' });
|
||||
return;
|
||||
}
|
||||
res.json({
|
||||
filePath: req.file.path,
|
||||
filename: req.file.originalname,
|
||||
size: req.file.size
|
||||
});
|
||||
});
|
||||
router.get('/', (_req, res) => {
|
||||
if (!fs_1.default.existsSync(UPLOADS_DIR)) {
|
||||
res.json({ files: [] });
|
||||
return;
|
||||
}
|
||||
const entries = fs_1.default.readdirSync(UPLOADS_DIR, { withFileTypes: true });
|
||||
const files = entries
|
||||
.filter(e => e.isFile())
|
||||
.map(e => ({
|
||||
filename: e.name,
|
||||
filePath: path_1.default.join(UPLOADS_DIR, e.name),
|
||||
size: fs_1.default.statSync(path_1.default.join(UPLOADS_DIR, e.name)).size
|
||||
}))
|
||||
.sort((a, b) => b.filePath.localeCompare(a.filePath));
|
||||
res.json({ files });
|
||||
});
|
||||
router.post('/youtube', (req, res) => {
|
||||
if (!(0, ytDlp_1.isYtDlpAvailable)()) {
|
||||
res.status(400).json({ error: 'yt-dlp is not installed or not in PATH' });
|
||||
return;
|
||||
}
|
||||
const { url } = req.body;
|
||||
if (!url) {
|
||||
res.status(400).json({ error: 'URL is required' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = (0, ytDlp_1.downloadVideo)(url, UPLOADS_DIR);
|
||||
res.json(result);
|
||||
}
|
||||
catch (err) {
|
||||
res.status(500).json({ error: `Failed to download: ${err.message}` });
|
||||
}
|
||||
});
|
||||
exports.default = router;
|
||||
//# sourceMappingURL=files.js.map
|
||||
1
dist/server/routes/files.js.map
vendored
Normal file
1
dist/server/routes/files.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/server/routes/files.ts"],"names":[],"mappings":";;;;;AAAA,qCAAoD;AACpD,oDAA4B;AAC5B,gDAAwB;AACxB,4CAAoB;AACpB,6CAAoE;AAEpE,MAAM,WAAW,GAAG,cAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;AAE9C,MAAM,OAAO,GAAG,gBAAM,CAAC,WAAW,CAAC;IACjC,WAAW,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE;QAC/B,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAChC,YAAE,CAAC,SAAS,CAAC,WAAW,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;QACD,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACxB,CAAC;IACD,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAC3B,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;QACxE,EAAE,CAAC,IAAI,EAAE,YAAY,GAAG,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IAC3D,CAAC;CACF,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,IAAA,gBAAM,EAAC;IACpB,OAAO;IACP,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAC7B,MAAM,YAAY,GAAG;YACnB,WAAW,EAAE,YAAY,EAAE,kBAAkB,EAAE,iBAAiB;YAChE,iBAAiB,EAAE,YAAY,EAAE,gBAAgB,EAAE,aAAa;SACjE,CAAC;QACF,IAAI,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,6CAA6C,CAAC,EAAE,CAAC;YACnH,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACjB,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC,CAAC;QACpE,CAAC;IACH,CAAC;IACD,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC,OAAO;CACtD,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;AAExB,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;IAC7E,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;QACd,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC,CAAC;QAC1D,OAAO;IACT,CAAC;IACD,GAAG,CAAC,IAAI,CAAC;QACP,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI;QACvB,QAAQ,EAAE,GAAG,CAAC,IAAI,CAAC,YAAY;QAC/B,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI;KACpB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAa,EAAE,GAAa,EAAE,EAAE;IAC/C,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;QACxB,OAAO;IACT,CAAC;IACD,MAAM,OAAO,GAAG,YAAE,CAAC,WAAW,CAAC,WAAW,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACrE,MAAM,KAAK,GAAG,OAAO;SAClB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACvB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACT,QAAQ,EAAE,CAAC,CAAC,IAAI;QAChB,QAAQ,EAAE,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC;QACxC,IAAI,EAAE,YAAE,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI;KACvD,CAAC,CAAC;SACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxD,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AACtB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC,GAAY,EAAE,GAAa,EAAE,EAAE;IACtD,IAAI,CAAC,IAAA,wBAAgB,GAAE,EAAE,CAAC;QACxB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,wCAAwC,EAAE,CAAC,CAAC;QAC1E,OAAO;IACT,CAAC;IAED,MAAM,EAAE,GAAG,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;IACzB,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACnD,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,qBAAa,EAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAC/C,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnB,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACxE,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,kBAAe,MAAM,CAAC"}
|
||||
3
dist/server/routes/jobs.d.ts
vendored
Normal file
3
dist/server/routes/jobs.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Router } from 'express';
|
||||
import { JobManager } from '../services/jobManager';
|
||||
export declare function createJobsRouter(jobManager: JobManager): Router;
|
||||
164
dist/server/routes/jobs.js
vendored
Normal file
164
dist/server/routes/jobs.js
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.createJobsRouter = createJobsRouter;
|
||||
const express_1 = require("express");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
const jobStore_1 = require("../db/jobStore");
|
||||
function getParam(req, name) {
|
||||
const val = req.params[name];
|
||||
return Array.isArray(val) ? val[0] : val;
|
||||
}
|
||||
function createJobsRouter(jobManager) {
|
||||
const router = (0, express_1.Router)();
|
||||
router.get('/', (_req, res) => {
|
||||
const jobs = jobManager.listJobs();
|
||||
res.json({ jobs });
|
||||
});
|
||||
router.post('/', (req, res) => {
|
||||
const { videoPath, config, outputOptions } = req.body;
|
||||
if (!videoPath) {
|
||||
res.status(400).json({ error: 'videoPath is required' });
|
||||
return;
|
||||
}
|
||||
if (!fs_1.default.existsSync(videoPath)) {
|
||||
res.status(400).json({ error: `Video file not found: ${videoPath}` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const job = jobManager.createJob(videoPath, config || {}, outputOptions || {});
|
||||
res.status(201).json({ job });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
router.get('/:id', (req, res) => {
|
||||
const job = (0, jobStore_1.getJob)(getParam(req, 'id'));
|
||||
if (!job) {
|
||||
res.status(404).json({ error: 'Job not found' });
|
||||
return;
|
||||
}
|
||||
res.json({ job });
|
||||
});
|
||||
router.post('/:id/start', async (req, res) => {
|
||||
try {
|
||||
await jobManager.startJob(getParam(req, 'id'));
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
router.post('/:id/pause', async (req, res) => {
|
||||
try {
|
||||
await jobManager.pauseJob(getParam(req, 'id'));
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
router.post('/:id/restart', async (req, res) => {
|
||||
try {
|
||||
await jobManager.restartJob(getParam(req, 'id'));
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
router.post('/:id/cancel', async (req, res) => {
|
||||
try {
|
||||
await jobManager.cancelJob(getParam(req, 'id'));
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
router.delete('/:id', (req, res) => {
|
||||
try {
|
||||
jobManager.deleteJob(getParam(req, 'id'));
|
||||
res.json({ success: true });
|
||||
}
|
||||
catch (err) {
|
||||
res.status(400).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
router.get('/:id/progress', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
const sendProgress = (data) => {
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
const initialJob = (0, jobStore_1.getJob)(getParam(req, 'id'));
|
||||
if (initialJob) {
|
||||
sendProgress({
|
||||
id: initialJob.id,
|
||||
status: initialJob.status,
|
||||
progress: initialJob.progress,
|
||||
currentIndex: initialJob.current_index,
|
||||
totalUnits: initialJob.total_units,
|
||||
segments: JSON.parse(initialJob.segments),
|
||||
error: initialJob.error,
|
||||
output_audio: initialJob.output_audio,
|
||||
output_subtitles_srt: initialJob.output_subtitles_srt,
|
||||
output_subtitles_vtt: initialJob.output_subtitles_vtt,
|
||||
output_muxed: initialJob.output_muxed
|
||||
});
|
||||
}
|
||||
const unsubscribe = jobManager.onJobProgress(getParam(req, 'id'), (data) => {
|
||||
if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
|
||||
sendProgress(data);
|
||||
res.end();
|
||||
unsubscribe();
|
||||
return;
|
||||
}
|
||||
sendProgress(data);
|
||||
});
|
||||
req.on('close', () => {
|
||||
unsubscribe();
|
||||
});
|
||||
});
|
||||
router.get('/:id/download/:type', (req, res) => {
|
||||
const job = (0, jobStore_1.getJob)(getParam(req, 'id'));
|
||||
if (!job) {
|
||||
res.status(404).json({ error: 'Job not found' });
|
||||
return;
|
||||
}
|
||||
const type = getParam(req, 'type');
|
||||
let filePath = null;
|
||||
let filename = '';
|
||||
switch (type) {
|
||||
case 'audio':
|
||||
filePath = job.output_audio;
|
||||
filename = `${path_1.default.basename(job.video_filename, path_1.default.extname(job.video_filename))}_description.mp3`;
|
||||
break;
|
||||
case 'subtitles':
|
||||
const format = req.query.format || 'srt';
|
||||
filePath = format === 'vtt' ? job.output_subtitles_vtt : job.output_subtitles_srt;
|
||||
filename = `${path_1.default.basename(job.video_filename, path_1.default.extname(job.video_filename))}_description.${format}`;
|
||||
break;
|
||||
case 'muxed':
|
||||
filePath = job.output_muxed;
|
||||
filename = `${path_1.default.basename(job.video_filename, path_1.default.extname(job.video_filename))}_described.mkv`;
|
||||
break;
|
||||
default:
|
||||
res.status(400).json({ error: 'Invalid download type' });
|
||||
return;
|
||||
}
|
||||
if (!filePath || !fs_1.default.existsSync(filePath)) {
|
||||
res.status(404).json({ error: 'Output file not found' });
|
||||
return;
|
||||
}
|
||||
res.download(filePath, filename);
|
||||
});
|
||||
return router;
|
||||
}
|
||||
//# sourceMappingURL=jobs.js.map
|
||||
1
dist/server/routes/jobs.js.map
vendored
Normal file
1
dist/server/routes/jobs.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
37
dist/server/services/jobManager.d.ts
vendored
Normal file
37
dist/server/services/jobManager.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Job, OutputOptions } from '../db/jobStore';
|
||||
import { Config } from '../../config/config';
|
||||
import { AudioSegment } from '../../interfaces';
|
||||
interface ProgressData {
|
||||
id: string;
|
||||
status: string;
|
||||
progress: number;
|
||||
currentIndex: number;
|
||||
totalUnits: number;
|
||||
segments: AudioSegment[];
|
||||
error: string | null;
|
||||
output_audio: string | null;
|
||||
output_subtitles_srt: string | null;
|
||||
output_subtitles_vtt: string | null;
|
||||
output_muxed: string | null;
|
||||
}
|
||||
export declare class JobManager {
|
||||
private queue;
|
||||
private processing;
|
||||
private pausedJobs;
|
||||
private emitter;
|
||||
private pollInterval;
|
||||
constructor();
|
||||
private recoverStuckJobs;
|
||||
createJob(videoPath: string, configOverride?: Partial<Config>, outputOptions?: Partial<OutputOptions>): Job;
|
||||
startJob(jobId: string): Promise<void>;
|
||||
pauseJob(jobId: string): Promise<void>;
|
||||
restartJob(jobId: string): Promise<void>;
|
||||
cancelJob(jobId: string): Promise<void>;
|
||||
deleteJob(jobId: string): void;
|
||||
listJobs(): Job[];
|
||||
onJobProgress(jobId: string, callback: (data: ProgressData) => void): () => void;
|
||||
private emitProgress;
|
||||
private processNext;
|
||||
private processJob;
|
||||
}
|
||||
export {};
|
||||
239
dist/server/services/jobManager.js
vendored
Normal file
239
dist/server/services/jobManager.js
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
"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
|
||||
1
dist/server/services/jobManager.js.map
vendored
Normal file
1
dist/server/services/jobManager.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/server/services/muxer.d.ts
vendored
Normal file
1
dist/server/services/muxer.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export declare function muxAudioDescription(videoPath: string, audioPath: string, outputPath: string): void;
|
||||
29
dist/server/services/muxer.js
vendored
Normal file
29
dist/server/services/muxer.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.muxAudioDescription = muxAudioDescription;
|
||||
const child_process_1 = require("child_process");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
function muxAudioDescription(videoPath, audioPath, outputPath) {
|
||||
const ext = path_1.default.extname(outputPath).toLowerCase();
|
||||
const isMkv = ext === '.mkv';
|
||||
const cmd = [
|
||||
'ffmpeg -v error',
|
||||
`-i "${videoPath}"`,
|
||||
`-i "${audioPath}"`,
|
||||
'-map 0:v',
|
||||
'-map 0:a?',
|
||||
'-map 1:a',
|
||||
'-c:v copy',
|
||||
'-c:a copy',
|
||||
isMkv
|
||||
? '-metadata:s:a:1 title="Audio Description"'
|
||||
: '-metadata:s:a:1 title="Audio Description"',
|
||||
`"${outputPath}"`,
|
||||
'-y'
|
||||
].join(' ');
|
||||
(0, child_process_1.execSync)(cmd);
|
||||
}
|
||||
//# sourceMappingURL=muxer.js.map
|
||||
1
dist/server/services/muxer.js.map
vendored
Normal file
1
dist/server/services/muxer.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"muxer.js","sourceRoot":"","sources":["../../../src/server/services/muxer.ts"],"names":[],"mappings":";;;;;AAGA,kDAyBC;AA5BD,iDAAyC;AACzC,gDAAwB;AAExB,SAAgB,mBAAmB,CACjC,SAAiB,EACjB,SAAiB,EACjB,UAAkB;IAElB,MAAM,GAAG,GAAG,cAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,CAAC;IACnD,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,CAAC;IAE7B,MAAM,GAAG,GAAG;QACV,iBAAiB;QACjB,OAAO,SAAS,GAAG;QACnB,OAAO,SAAS,GAAG;QACnB,UAAU;QACV,WAAW;QACX,UAAU;QACV,WAAW;QACX,WAAW;QACX,KAAK;YACH,CAAC,CAAC,2CAA2C;YAC7C,CAAC,CAAC,2CAA2C;QAC/C,IAAI,UAAU,GAAG;QACjB,IAAI;KACL,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEZ,IAAA,wBAAQ,EAAC,GAAG,CAAC,CAAC;AAChB,CAAC"}
|
||||
3
dist/server/services/subtitleGenerator.d.ts
vendored
Normal file
3
dist/server/services/subtitleGenerator.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import { AudioSegment } from '../../interfaces';
|
||||
export declare function generateSRT(segments: AudioSegment[], videoDuration: number): string;
|
||||
export declare function generateVTT(segments: AudioSegment[], videoDuration: number): string;
|
||||
65
dist/server/services/subtitleGenerator.js
vendored
Normal file
65
dist/server/services/subtitleGenerator.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.generateSRT = generateSRT;
|
||||
exports.generateVTT = generateVTT;
|
||||
function formatSrtTime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
const ms = Math.floor((seconds % 1) * 1000);
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
|
||||
}
|
||||
function formatVttTime(seconds) {
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
const ms = Math.floor((seconds % 1) * 1000);
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
|
||||
}
|
||||
function cleanDescription(text) {
|
||||
return text.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
function generateSRT(segments, videoDuration) {
|
||||
if (segments.length === 0)
|
||||
return '';
|
||||
const sorted = [...segments].sort((a, b) => a.startTime - b.startTime);
|
||||
const lines = [];
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const seg = sorted[i];
|
||||
const startTime = seg.startTime;
|
||||
let endTime;
|
||||
if (i < sorted.length - 1) {
|
||||
endTime = sorted[i + 1].startTime;
|
||||
}
|
||||
else {
|
||||
endTime = Math.min(seg.startTime + seg.duration + 0.5, videoDuration);
|
||||
}
|
||||
lines.push((i + 1).toString());
|
||||
lines.push(`${formatSrtTime(startTime)} --> ${formatSrtTime(endTime)}`);
|
||||
lines.push(cleanDescription(seg.description));
|
||||
lines.push('');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
function generateVTT(segments, videoDuration) {
|
||||
if (segments.length === 0)
|
||||
return '';
|
||||
const sorted = [...segments].sort((a, b) => a.startTime - b.startTime);
|
||||
const lines = ['WEBVTT', ''];
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const seg = sorted[i];
|
||||
const startTime = seg.startTime;
|
||||
let endTime;
|
||||
if (i < sorted.length - 1) {
|
||||
endTime = sorted[i + 1].startTime;
|
||||
}
|
||||
else {
|
||||
endTime = Math.min(seg.startTime + seg.duration + 0.5, videoDuration);
|
||||
}
|
||||
lines.push(`${formatVttTime(startTime)} --> ${formatVttTime(endTime)}`);
|
||||
lines.push(cleanDescription(seg.description));
|
||||
lines.push('');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
//# sourceMappingURL=subtitleGenerator.js.map
|
||||
1
dist/server/services/subtitleGenerator.js.map
vendored
Normal file
1
dist/server/services/subtitleGenerator.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"subtitleGenerator.js","sourceRoot":"","sources":["../../../src/server/services/subtitleGenerator.ts"],"names":[],"mappings":"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"}
|
||||
7
dist/server/services/ytDlp.d.ts
vendored
Normal file
7
dist/server/services/ytDlp.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface YtDlpResult {
|
||||
filePath: string;
|
||||
filename: string;
|
||||
title: string;
|
||||
}
|
||||
export declare function isYtDlpAvailable(): boolean;
|
||||
export declare function downloadVideo(url: string, outputDir: string): YtDlpResult;
|
||||
38
dist/server/services/ytDlp.js
vendored
Normal file
38
dist/server/services/ytDlp.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
"use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isYtDlpAvailable = isYtDlpAvailable;
|
||||
exports.downloadVideo = downloadVideo;
|
||||
const child_process_1 = require("child_process");
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const fs_1 = __importDefault(require("fs"));
|
||||
function isYtDlpAvailable() {
|
||||
try {
|
||||
(0, child_process_1.execSync)('yt-dlp --version', { stdio: 'pipe' });
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function downloadVideo(url, outputDir) {
|
||||
if (!fs_1.default.existsSync(outputDir)) {
|
||||
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
const outputTemplate = path_1.default.join(outputDir, '%(title)s.%(ext)s');
|
||||
const result = (0, child_process_1.execSync)(`yt-dlp -f "best[ext=mp4]/best" -o "${outputTemplate}" --print filename --print title "${url}"`, { encoding: 'utf-8', timeout: 600000 });
|
||||
const lines = result.trim().split('\n');
|
||||
const filename = lines[0]?.trim();
|
||||
const title = lines[1]?.trim() || filename;
|
||||
if (!filename) {
|
||||
throw new Error('yt-dlp: Failed to parse downloaded filename');
|
||||
}
|
||||
const filePath = path_1.default.resolve(outputDir, filename);
|
||||
if (!fs_1.default.existsSync(filePath)) {
|
||||
throw new Error(`yt-dlp: Downloaded file not found at ${filePath}`);
|
||||
}
|
||||
return { filePath, filename, title };
|
||||
}
|
||||
//# sourceMappingURL=ytDlp.js.map
|
||||
1
dist/server/services/ytDlp.js.map
vendored
Normal file
1
dist/server/services/ytDlp.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"ytDlp.js","sourceRoot":"","sources":["../../../src/server/services/ytDlp.ts"],"names":[],"mappings":";;;;;AAUA,4CAOC;AAED,sCA2BC;AA9CD,iDAAyC;AACzC,gDAAwB;AACxB,4CAAoB;AAQpB,SAAgB,gBAAgB;IAC9B,IAAI,CAAC;QACH,IAAA,wBAAQ,EAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;QAChD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAgB,aAAa,CAAC,GAAW,EAAE,SAAiB;IAC1D,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,YAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,cAAc,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,CAAC;IAEjE,MAAM,MAAM,GAAG,IAAA,wBAAQ,EACrB,sCAAsC,cAAc,qCAAqC,GAAG,GAAG,EAC/F,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CACvC,CAAC;IAEF,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,QAAQ,CAAC;IAE3C,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAEnD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,wCAAwC,QAAQ,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AACvC,CAAC"}
|
||||
Reference in New Issue
Block a user