Rewrite frontend as single self-contained HTML file — all CSS/JS inline, no external files to fail loading

This commit is contained in:
2026-05-13 17:24:10 +02:00
parent 3432d362e2
commit ddb0f88257
116 changed files with 4240 additions and 921 deletions

29
dist/cli/args.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
export interface CLIArgs {
_: (string | number)[];
$0: string;
video_file_path?: string;
captureIntervalSeconds?: number;
contextWindowSize?: number;
visionProvider?: string;
visionModel?: string;
ttsProvider?: string;
ttsModel?: string;
ttsVoice?: string;
ttsSpeedFactor?: number;
ttsInstructions?: string;
outputDir?: string;
tempDir?: string;
batchTimeMode?: boolean;
batchWindowDuration?: number;
framesInBatch?: number;
defaultPrompt?: string;
changePrompt?: string;
batchPrompt?: string;
estimate?: boolean;
config?: string;
saveConfig?: string;
}
/**
* Parse command line arguments
*/
export declare function parseCommandLineArgs(): CLIArgs;

120
dist/cli/args.js vendored Normal file
View File

@@ -0,0 +1,120 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseCommandLineArgs = parseCommandLineArgs;
const yargs_1 = __importDefault(require("yargs/yargs"));
const helpers_1 = require("yargs/helpers");
/**
* Parse command line arguments
*/
function parseCommandLineArgs() {
const parsed = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
.usage('Usage: $0 <video_file_path> [options]')
.positional('video_file_path', {
describe: 'Path to the input video file',
type: 'string'
})
.option('captureIntervalSeconds', {
alias: 'i',
describe: 'Interval in seconds between frame captures',
type: 'number'
})
.option('contextWindowSize', {
alias: 'c',
describe: 'Number of frames to keep in context',
type: 'number'
})
.option('visionProvider', {
describe: 'Provider to use for vision AI',
type: 'string'
})
.option('visionModel', {
describe: 'Model to use for vision AI',
type: 'string'
})
.option('ttsProvider', {
describe: 'Provider to use for text-to-speech',
type: 'string'
})
.option('ttsModel', {
alias: 'm',
describe: 'TTS model to use',
type: 'string'
})
.option('ttsVoice', {
alias: 'v',
describe: 'Voice to use for text-to-speech',
type: 'string'
})
.option('ttsSpeedFactor', {
alias: 's',
describe: 'Speed factor for the audio playback',
type: 'number'
})
.option('ttsInstructions', {
describe: 'Instructions for TTS voice style (gpt-4o-mini-tts)',
type: 'string'
})
.option('outputDir', {
alias: 'o',
describe: 'Directory for output files',
type: 'string'
})
.option('tempDir', {
alias: 't',
describe: 'Directory for temporary files',
type: 'string'
})
.option('batchTimeMode', {
alias: 'b',
describe: 'Use batch time mode for processing',
type: 'boolean'
})
.option('batchWindowDuration', {
describe: 'Duration in seconds for each batch window',
type: 'number'
})
.option('framesInBatch', {
describe: 'Number of frames to capture within each batch',
type: 'number'
})
.option('defaultPrompt', {
describe: 'Prompt for describing individual frames',
type: 'string'
})
.option('changePrompt', {
describe: 'Prompt for describing changes between frames',
type: 'string'
})
.option('batchPrompt', {
describe: 'Prompt for describing batches of frames',
type: 'string'
})
.option('estimate', {
alias: 'e',
describe: 'Only estimate the cost without generating the audio description',
type: 'boolean',
default: false
})
.option('config', {
alias: 'f',
describe: 'Path to JSON config file',
type: 'string'
})
.option('saveConfig', {
describe: 'Save current configuration to specified JSON file',
type: 'string'
})
.help()
.alias('help', 'h')
.example('$0 video.mp4', 'Process a video with default settings')
.example('$0 video.mp4 --ttsVoice nova --visionProvider openai', 'Process with custom voice and vision provider')
.example('$0 video.mp4 --estimate', 'Only estimate the processing cost')
.example('$0 video.mp4 --config myconfig.json', 'Use settings from a config file')
.example('$0 video.mp4 --saveConfig myconfig.json', 'Save current settings to a config file')
.argv;
return parsed;
}
//# sourceMappingURL=args.js.map

1
dist/cli/args.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":";;;;;AAgCA,oDA4GC;AA5ID,wDAAgC;AAChC,2CAAwC;AA4BxC;;GAEG;AACH,SAAgB,oBAAoB;IAClC,MAAM,MAAM,GAAG,IAAA,eAAK,EAAC,IAAA,iBAAO,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC;SACxC,KAAK,CAAC,uCAAuC,CAAC;SAC9C,UAAU,CAAC,iBAAiB,EAAE;QAC7B,QAAQ,EAAE,8BAA8B;QACxC,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,wBAAwB,EAAE;QAChC,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,4CAA4C;QACtD,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,mBAAmB,EAAE;QAC3B,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,qCAAqC;QAC/C,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,gBAAgB,EAAE;QACxB,QAAQ,EAAE,+BAA+B;QACzC,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,aAAa,EAAE;QACrB,QAAQ,EAAE,4BAA4B;QACtC,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,aAAa,EAAE;QACrB,QAAQ,EAAE,oCAAoC;QAC9C,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,kBAAkB;QAC5B,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,iCAAiC;QAC3C,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,gBAAgB,EAAE;QACxB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,qCAAqC;QAC/C,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,iBAAiB,EAAE;QACzB,QAAQ,EAAE,oDAAoD;QAC9D,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,WAAW,EAAE;QACnB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,4BAA4B;QACtC,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,SAAS,EAAE;QACjB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,+BAA+B;QACzC,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,eAAe,EAAE;QACvB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,oCAAoC;QAC9C,IAAI,EAAE,SAAS;KAChB,CAAC;SACD,MAAM,CAAC,qBAAqB,EAAE;QAC7B,QAAQ,EAAE,2CAA2C;QACrD,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,eAAe,EAAE;QACvB,QAAQ,EAAE,+CAA+C;QACzD,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,eAAe,EAAE;QACvB,QAAQ,EAAE,yCAAyC;QACnD,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,cAAc,EAAE;QACtB,QAAQ,EAAE,8CAA8C;QACxD,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,aAAa,EAAE;QACrB,QAAQ,EAAE,yCAAyC;QACnD,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,UAAU,EAAE;QAClB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,iEAAiE;QAC3E,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,KAAK;KACf,CAAC;SACD,MAAM,CAAC,QAAQ,EAAE;QAChB,KAAK,EAAE,GAAG;QACV,QAAQ,EAAE,0BAA0B;QACpC,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,MAAM,CAAC,YAAY,EAAE;QACpB,QAAQ,EAAE,mDAAmD;QAC7D,IAAI,EAAE,QAAQ;KACf,CAAC;SACD,IAAI,EAAE;SACN,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC;SAClB,OAAO,CAAC,cAAc,EAAE,uCAAuC,CAAC;SAChE,OAAO,CAAC,sDAAsD,EAAE,+CAA+C,CAAC;SAChH,OAAO,CAAC,yBAAyB,EAAE,mCAAmC,CAAC;SACvE,OAAO,CAAC,qCAAqC,EAAE,iCAAiC,CAAC;SACjF,OAAO,CAAC,yCAAyC,EAAE,wCAAwC,CAAC;SAC5F,IAA0B,CAAC;IAE9B,OAAO,MAAM,CAAC;AAChB,CAAC"}

1
dist/cli/index.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import 'dotenv/config';

83
dist/cli/index.js vendored Normal file
View File

@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
require("dotenv/config");
const config_1 = require("../config/config");
const stats_1 = require("../config/stats");
const visionProviderFactory_1 = require("../providers/vision/visionProviderFactory");
const ttsProviderFactory_1 = require("../providers/tts/ttsProviderFactory");
const processor_1 = require("../utils/processor");
const costEstimator_1 = require("../utils/costEstimator");
const configUtils_1 = require("../utils/configUtils");
const args_1 = require("./args");
async function main() {
const argv = (0, args_1.parseCommandLineArgs)();
let config = (0, config_1.getDefaultConfig)();
if (argv.config) {
const fileConfig = (0, configUtils_1.loadConfigFromFile)(argv.config);
config = { ...config, ...fileConfig };
}
const argvObj = argv;
Object.keys(argvObj).forEach(key => {
if (key !== '_' && key !== '$0' && key !== 'config' && key !== 'saveConfig' &&
key !== 'estimate' && key !== 'help' && key !== 'version' &&
argvObj[key] !== undefined) {
config[key] = argvObj[key];
}
});
if (argv.visionModel) {
if (!config.visionProviders[config.visionProvider]) {
config.visionProviders[config.visionProvider] = { model: '' };
}
config.visionProviders[config.visionProvider].model = argv.visionModel;
}
if (argv.ttsModel) {
if (!config.ttsProviders[config.ttsProvider]) {
config.ttsProviders[config.ttsProvider] = { model: '' };
}
config.ttsProviders[config.ttsProvider].model = argv.ttsModel;
}
if (argv.ttsVoice) {
if (!config.ttsProviders[config.ttsProvider]) {
config.ttsProviders[config.ttsProvider] = { model: '', voice: '' };
}
config.ttsProviders[config.ttsProvider].voice = argv.ttsVoice;
}
if (argv.saveConfig) {
(0, configUtils_1.saveConfigToFile)(argv.saveConfig, config);
}
if (argv._.length < 1) {
console.error('Error: No video file specified');
console.log('Usage: node script.js <video_file_path> [options]');
console.log('Use --help for more information');
process.exit(1);
}
const videoFilePath = String(argv._[0]);
if (argv.estimate) {
try {
const costBreakdown = await (0, costEstimator_1.estimateCost)(videoFilePath, config);
console.log('\n=== COST ESTIMATION ===');
console.log(JSON.stringify(costBreakdown, null, 2));
console.log(`\nEstimated total cost: ${costBreakdown.apiCosts.total}`);
console.log(`Estimated processing time: ${costBreakdown.estimates.estimatedProcessingTimeMinutes.toFixed(1)} minutes`);
console.log('Note: Actual costs may vary based on image complexity and actual response lengths.');
}
catch (err) {
console.error('Error estimating costs:', err);
}
}
else {
try {
const stats = (0, stats_1.createStats)();
const visionProvider = visionProviderFactory_1.VisionProviderFactory.getProvider(config);
const ttsProvider = ttsProviderFactory_1.TTSProviderFactory.getProvider(config);
await (0, processor_1.generateAudioDescription)(videoFilePath, visionProvider, ttsProvider, config, stats);
}
catch (err) {
console.error('Error generating audio description:', err);
}
}
}
if (require.main === module) {
main().catch(err => console.error('Unhandled error:', err));
}
//# sourceMappingURL=index.js.map

1
dist/cli/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"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"}

32
dist/config/config.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
import { VisionProviderConfig, TTSProviderConfig } from '../interfaces';
export interface Config {
captureIntervalSeconds: number;
contextWindowSize: number;
defaultPrompt: string;
changePrompt: string;
batchPrompt: string;
visionProvider: string;
visionModel: string;
visionProviders: {
[key: string]: VisionProviderConfig;
};
ttsProvider: string;
ttsVoice: string;
ttsSpeedFactor: number;
ttsInstructions?: string;
ttsProviders: {
[key: string]: TTSProviderConfig;
};
outputDir: string;
tempDir: string;
batchTimeMode: boolean;
batchWindowDuration: number;
framesInBatch: number;
}
/**
* Get default configuration options.
* Uses a function so that process.env is read at call time
* (after dotenv has been loaded), not at module import time.
*/
export declare function getDefaultConfig(): Config;
export declare const defaultConfig: Config;

77
dist/config/config.js vendored Normal file
View File

@@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultConfig = void 0;
exports.getDefaultConfig = getDefaultConfig;
/**
* Get default configuration options.
* Uses a function so that process.env is read at call time
* (after dotenv has been loaded), not at module import time.
*/
function getDefaultConfig() {
return {
captureIntervalSeconds: 10,
contextWindowSize: 5,
defaultPrompt: "Describe this frame from a video in 1-2 sentences for someone who cannot see it. Focus on key visual elements. Avoid using terms like 'in this frame', simply describe the actual frame. Keep sentences short and concise, as this will be used to generate an audio track which is overlayed on the video.",
changePrompt: "Describe what has changed between these frames in 1-2 sentences for someone who cannot see the video. Focus on significant visual changes only. Avoid talking about meta information such as 'in this frame', or 'the significant change is', and merely describe the actual change taking place. Only describe the changes relevant to the last frame. The previous frames are attached for you to build context and build situational awareness. Keep it short and concise, as your text will be used to generate audio description tracks to be played with the video.",
batchPrompt: "Describe the sequence of frames in this batch over time for someone who cannot see it. Focus on what happens, changes, or stands out visually during these seconds. Keep it to 1-3 concise sentences, avoiding words like 'in these frames'—just describe what's happening. Use context from the previous batch if relevant. Keep sentences short and concise. Avoid speculation or overly verbose or unnecessary sentences. Try not to use nested sentences and keep sentences short to help flow. This will be used for audio description and mixed back in with the video file later, so we need to maintain consistency and quick pacing. Avoid using phrases such as 'as evidenced by' or 'suggesting'. Only focus on describing the visual scene. Do not repeat information given in the previous prompt, and focus only on what has changed since that description. Avoid talking about the scene or sequence, simply focus on the action within these frames. The listener knows that this is a video, so we do not need to remind them. Also avoid overusing phrases such as 'the scene shifts', the shifting or perspective change should be evident from the description of the sequence itself.",
// Vision AI settings
visionProvider: "openai",
visionModel: "gpt-5.4-mini",
visionProviders: {
openai: {
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-5.4-mini",
maxTokens: 300
},
gemini: {
apiKey: process.env.GOOGLE_API_KEY,
model: "gemini-2.0-flash",
maxTokens: 300
},
ollama: {
baseUrl: "http://localhost:11434",
model: "gemma3:12b",
maxTokens: 3000
},
openrouter: {
apiKey: process.env.OPENROUTER_API_KEY,
model: "anthropic/claude-sonnet-4.5",
baseUrl: "https://openrouter.ai/api/v1",
maxTokens: 300
}
},
// TTS settings
ttsProvider: "openai",
ttsVoice: "alloy",
ttsSpeedFactor: 1.5,
ttsInstructions: "Speak in a calm, narrating tone suitable for audio descriptions. Keep a steady pace and clear enunciation.",
ttsProviders: {
openai: {
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-4o-mini-tts",
voice: "shimmer"
},
elevenlabs: {
apiKey: process.env.ELEVENLABS_API_KEY,
model: "eleven_multilingual_v2",
voice: "JBFqnCBsd6RMkjVDRZzb"
},
google: {
apiKey: process.env.GOOGLE_CLOUD_TTS_KEY,
keyFilename: process.env.GOOGLE_CLOUD_TTS_KEYFILE,
model: "chirp-hd",
voice: "en-US-Chirp-HD-F"
}
},
// Video processing settings
outputDir: "./desc/output/",
tempDir: "./desc/tmp/",
batchTimeMode: true,
batchWindowDuration: 15,
framesInBatch: 10,
};
}
// Keep a static export alias for backward compatibility
// (but callers should prefer getDefaultConfig() for correct env loading)
exports.defaultConfig = getDefaultConfig();
//# sourceMappingURL=config.js.map

1
dist/config/config.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config/config.ts"],"names":[],"mappings":";;;AAsCA,4CAkEC;AAvED;;;;GAIG;AACH,SAAgB,gBAAgB;IAC9B,OAAO;QACL,sBAAsB,EAAE,EAAE;QAC1B,iBAAiB,EAAE,CAAC;QACpB,aAAa,EAAE,6SAA6S;QAC5T,YAAY,EAAE,2iBAA2iB;QACzjB,WAAW,EAAE,6oCAA6oC;QAE1pC,qBAAqB;QACrB,cAAc,EAAE,QAAQ;QACxB,WAAW,EAAE,cAAc;QAC3B,eAAe,EAAE;YACf,MAAM,EAAE;gBACN,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;gBAClC,KAAK,EAAE,cAAc;gBACrB,SAAS,EAAE,GAAG;aACf;YACD,MAAM,EAAE;gBACN,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;gBAClC,KAAK,EAAE,kBAAkB;gBACzB,SAAS,EAAE,GAAG;aACf;YACD,MAAM,EAAE;gBACN,OAAO,EAAE,wBAAwB;gBACjC,KAAK,EAAE,YAAY;gBACnB,SAAS,EAAE,IAAI;aAChB;YACD,UAAU,EAAE;gBACV,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB;gBACtC,KAAK,EAAE,6BAA6B;gBACpC,OAAO,EAAE,8BAA8B;gBACvC,SAAS,EAAE,GAAG;aACf;SACF;QAED,eAAe;QACf,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,OAAO;QACjB,cAAc,EAAE,GAAG;QACnB,eAAe,EAAE,4GAA4G;QAC7H,YAAY,EAAE;YACZ,MAAM,EAAE;gBACN,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;gBAClC,KAAK,EAAE,iBAAiB;gBACxB,KAAK,EAAE,SAAS;aACjB;YACD,UAAU,EAAE;gBACV,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB;gBACtC,KAAK,EAAE,wBAAwB;gBAC/B,KAAK,EAAE,sBAAsB;aAC9B;YACD,MAAM,EAAE;gBACN,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,oBAAoB;gBACxC,WAAW,EAAE,OAAO,CAAC,GAAG,CAAC,wBAAwB;gBACjD,KAAK,EAAE,UAAU;gBACjB,KAAK,EAAE,kBAAkB;aAC1B;SACF;QAED,4BAA4B;QAC5B,SAAS,EAAE,gBAAgB;QAC3B,OAAO,EAAE,aAAa;QACtB,aAAa,EAAE,IAAI;QACnB,mBAAmB,EAAE,EAAE;QACvB,aAAa,EAAE,EAAE;KAClB,CAAC;AACJ,CAAC;AAED,wDAAwD;AACxD,yEAAyE;AAC5D,QAAA,aAAa,GAAG,gBAAgB,EAAE,CAAC"}

2
dist/config/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { Config, getDefaultConfig, defaultConfig } from './config';
export { createStats, printStats } from './stats';

10
dist/config/index.js vendored Normal file
View File

@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.printStats = exports.createStats = exports.defaultConfig = exports.getDefaultConfig = void 0;
var config_1 = require("./config");
Object.defineProperty(exports, "getDefaultConfig", { enumerable: true, get: function () { return config_1.getDefaultConfig; } });
Object.defineProperty(exports, "defaultConfig", { enumerable: true, get: function () { return config_1.defaultConfig; } });
var stats_1 = require("./stats");
Object.defineProperty(exports, "createStats", { enumerable: true, get: function () { return stats_1.createStats; } });
Object.defineProperty(exports, "printStats", { enumerable: true, get: function () { return stats_1.printStats; } });
//# sourceMappingURL=index.js.map

1
dist/config/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":";;;AAAA,mCAAmE;AAAlD,0GAAA,gBAAgB,OAAA;AAAE,uGAAA,aAAa,OAAA;AAChD,iCAAkD;AAAzC,oGAAA,WAAW,OAAA;AAAE,mGAAA,UAAU,OAAA"}

9
dist/config/stats.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import { Stats } from '../interfaces';
import { Config } from './config';
export declare const createStats: () => Stats;
/**
* Print out statistics
* @param stats - Statistics object
* @param settings - Configuration settings
*/
export declare function printStats(stats: Stats, settings: Config): void;

72
dist/config/stats.js vendored Normal file
View File

@@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createStats = void 0;
exports.printStats = printStats;
// Initialize stats object
const createStats = () => ({
totalFrames: 0,
totalBatches: 0,
totalVisionInputCost: 0,
totalVisionOutputCost: 0,
totalTTSCost: 0,
totalCost: 0
});
exports.createStats = createStats;
// Pricing constants (as of March 2025)
const pricing = {
vision: {
openai: {
'gpt-4o': {
input: 0.0025,
output: 0.01
}
},
gemini: {
'gemini-pro-vision': {
input: 0.0025,
output: 0.0025
}
}
},
tts: {
openai: {
'tts-1': 0.015,
'tts-1-hd': 0.030
}
}
};
/**
* Print out statistics
* @param stats - Statistics object
* @param settings - Configuration settings
*/
function printStats(stats, settings) {
// Get the pricing for the selected providers
const visionProvider = settings.visionProvider;
const visionModel = settings.visionProviders[visionProvider].model;
const ttsProvider = settings.ttsProvider;
const ttsModel = settings.ttsProviders[ttsProvider].model;
// Check if the pricing data exists
const visionPricing = pricing.vision[visionProvider]?.[visionModel];
const ttsPricing = pricing.tts[ttsProvider]?.[ttsModel];
if (!visionPricing) {
console.warn(`Warning: No pricing data for vision provider "${visionProvider}" and model "${visionModel}".`);
}
if (!ttsPricing) {
console.warn(`Warning: No pricing data for TTS provider "${ttsProvider}" and model "${ttsModel}".`);
}
// Calculate prices using available pricing data
const visionInputCost = visionPricing ? (stats.totalVisionInputCost * visionPricing.input / 1000) : 0;
const visionOutputCost = visionPricing ? (stats.totalVisionOutputCost * visionPricing.output / 1000) : 0;
const ttsCost = ttsPricing ? (stats.totalTTSCost * ttsPricing / 1000) : 0;
const totalCost = visionInputCost + visionOutputCost + ttsCost;
// Print out the stats
console.log('\n=== STATISTICS ===');
console.log(`Vision provider: ${visionProvider}, Model: ${visionModel}`);
console.log(`TTS provider: ${ttsProvider}, Model: ${ttsModel}`);
console.log(`Total vision input cost: ${visionInputCost.toFixed(4)}`);
console.log(`Total vision output cost: ${visionOutputCost.toFixed(4)}`);
console.log(`Total TTS cost: ${ttsCost.toFixed(4)}`);
console.log(`Total cost: ${totalCost.toFixed(4)}`);
}
//# sourceMappingURL=stats.js.map

1
dist/config/stats.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"stats.js","sourceRoot":"","sources":["../../src/config/stats.ts"],"names":[],"mappings":";;;AA6CA,gCAiCC;AA3ED,0BAA0B;AACnB,MAAM,WAAW,GAAG,GAAU,EAAE,CAAC,CAAC;IACvC,WAAW,EAAE,CAAC;IACd,YAAY,EAAE,CAAC;IACf,oBAAoB,EAAE,CAAC;IACvB,qBAAqB,EAAE,CAAC;IACxB,YAAY,EAAE,CAAC;IACf,SAAS,EAAE,CAAC;CACb,CAAC,CAAC;AAPU,QAAA,WAAW,eAOrB;AAEH,uCAAuC;AACvC,MAAM,OAAO,GAGT;IACF,MAAM,EAAE;QACN,MAAM,EAAE;YACN,QAAQ,EAAE;gBACR,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,IAAI;aACb;SACF;QACD,MAAM,EAAE;YACN,mBAAmB,EAAE;gBACnB,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,MAAM;aACf;SACF;KACF;IACD,GAAG,EAAE;QACH,MAAM,EAAE;YACN,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,KAAK;SAClB;KACF;CACF,CAAC;AAEF;;;;GAIG;AACH,SAAgB,UAAU,CAAC,KAAY,EAAE,QAAgB;IACvD,6CAA6C;IAC7C,MAAM,cAAc,GAAG,QAAQ,CAAC,cAAc,CAAC;IAC/C,MAAM,WAAW,GAAG,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,KAAK,CAAC;IACnE,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,CAAC;IACzC,MAAM,QAAQ,GAAG,QAAQ,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC,KAAK,CAAC;IAE1D,mCAAmC;IACnC,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC;IACpE,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;IAExD,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,OAAO,CAAC,IAAI,CAAC,iDAAiD,cAAc,gBAAgB,WAAW,IAAI,CAAC,CAAC;IAC/G,CAAC;IAED,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CAAC,8CAA8C,WAAW,gBAAgB,QAAQ,IAAI,CAAC,CAAC;IACtG,CAAC;IAED,gDAAgD;IAChD,MAAM,eAAe,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,GAAG,aAAa,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtG,MAAM,gBAAgB,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,qBAAqB,GAAG,aAAa,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzG,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,YAAY,GAAI,UAAqB,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtF,MAAM,SAAS,GAAG,eAAe,GAAG,gBAAgB,GAAG,OAAO,CAAC;IAE/D,sBAAsB;IACtB,OAAO,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC;IACpC,OAAO,CAAC,GAAG,CAAC,oBAAoB,cAAc,YAAY,WAAW,EAAE,CAAC,CAAC;IACzE,OAAO,CAAC,GAAG,CAAC,iBAAiB,WAAW,YAAY,QAAQ,EAAE,CAAC,CAAC;IAChE,OAAO,CAAC,GAAG,CAAC,4BAA4B,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,6BAA6B,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACxE,OAAO,CAAC,GAAG,CAAC,mBAAmB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,eAAe,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACrD,CAAC"}

10
dist/index.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
import 'dotenv/config';
export { generateAudioDescriptionFromOptions, generateAudioDescription } from './utils/processor';
export { estimateCost } from './utils/costEstimator';
export { getDefaultConfig, defaultConfig } from './config/config';
export { VisionProviderFactory } from './providers/vision/visionProviderFactory';
export { TTSProviderFactory } from './providers/tts/ttsProviderFactory';
export { createStats, printStats } from './config/stats';
export { loadConfigFromFile, saveConfigToFile } from './utils/configUtils';
export type { Config } from './config/config';
export type { ProcessingResult, ProcessingOptions, ProgressInfo, CostBreakdown, Stats, VisionProvider, TTSProvider, AudioSegment, BatchContext, VisionResult, TTSResult, VisionProviderConfig, TTSProviderConfig, TTSOptions } from './interfaces';

107
dist/index.js vendored Normal file
View File

@@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveConfigToFile = exports.loadConfigFromFile = exports.printStats = exports.createStats = exports.TTSProviderFactory = exports.VisionProviderFactory = exports.defaultConfig = exports.getDefaultConfig = exports.estimateCost = exports.generateAudioDescription = exports.generateAudioDescriptionFromOptions = void 0;
require("dotenv/config");
const config_1 = require("./config/config");
const stats_1 = require("./config/stats");
const visionProviderFactory_1 = require("./providers/vision/visionProviderFactory");
const ttsProviderFactory_1 = require("./providers/tts/ttsProviderFactory");
const processor_1 = require("./utils/processor");
const costEstimator_1 = require("./utils/costEstimator");
const configUtils_1 = require("./utils/configUtils");
const args_1 = require("./cli/args");
// Export functions and types for use as a module
var processor_2 = require("./utils/processor");
Object.defineProperty(exports, "generateAudioDescriptionFromOptions", { enumerable: true, get: function () { return processor_2.generateAudioDescriptionFromOptions; } });
Object.defineProperty(exports, "generateAudioDescription", { enumerable: true, get: function () { return processor_2.generateAudioDescription; } });
var costEstimator_2 = require("./utils/costEstimator");
Object.defineProperty(exports, "estimateCost", { enumerable: true, get: function () { return costEstimator_2.estimateCost; } });
var config_2 = require("./config/config");
Object.defineProperty(exports, "getDefaultConfig", { enumerable: true, get: function () { return config_2.getDefaultConfig; } });
Object.defineProperty(exports, "defaultConfig", { enumerable: true, get: function () { return config_2.defaultConfig; } });
var visionProviderFactory_2 = require("./providers/vision/visionProviderFactory");
Object.defineProperty(exports, "VisionProviderFactory", { enumerable: true, get: function () { return visionProviderFactory_2.VisionProviderFactory; } });
var ttsProviderFactory_2 = require("./providers/tts/ttsProviderFactory");
Object.defineProperty(exports, "TTSProviderFactory", { enumerable: true, get: function () { return ttsProviderFactory_2.TTSProviderFactory; } });
var stats_2 = require("./config/stats");
Object.defineProperty(exports, "createStats", { enumerable: true, get: function () { return stats_2.createStats; } });
Object.defineProperty(exports, "printStats", { enumerable: true, get: function () { return stats_2.printStats; } });
var configUtils_2 = require("./utils/configUtils");
Object.defineProperty(exports, "loadConfigFromFile", { enumerable: true, get: function () { return configUtils_2.loadConfigFromFile; } });
Object.defineProperty(exports, "saveConfigToFile", { enumerable: true, get: function () { return configUtils_2.saveConfigToFile; } });
// CLI entry point when run directly
if (require.main === module) {
main().catch(err => console.error('Unhandled error:', err));
}
async function main() {
const argv = (0, args_1.parseCommandLineArgs)();
let config = (0, config_1.getDefaultConfig)();
if (argv.config) {
const fileConfig = (0, configUtils_1.loadConfigFromFile)(argv.config);
config = { ...config, ...fileConfig };
}
const argvObj = argv;
Object.keys(argvObj).forEach(key => {
if (key !== '_' && key !== '$0' && key !== 'config' && key !== 'saveConfig' &&
key !== 'estimate' && key !== 'help' && key !== 'version' &&
argvObj[key] !== undefined) {
config[key] = argvObj[key];
}
});
if (argv.visionModel) {
if (!config.visionProviders[config.visionProvider]) {
config.visionProviders[config.visionProvider] = { model: '' };
}
config.visionProviders[config.visionProvider].model = argv.visionModel;
}
if (argv.ttsModel) {
if (!config.ttsProviders[config.ttsProvider]) {
config.ttsProviders[config.ttsProvider] = { model: '' };
}
config.ttsProviders[config.ttsProvider].model = argv.ttsModel;
}
if (argv.ttsVoice) {
if (!config.ttsProviders[config.ttsProvider]) {
config.ttsProviders[config.ttsProvider] = { model: '', voice: '' };
}
config.ttsProviders[config.ttsProvider].voice = argv.ttsVoice;
}
if (argv.ttsInstructions) {
config.ttsInstructions = argv.ttsInstructions;
}
if (argv.saveConfig) {
(0, configUtils_1.saveConfigToFile)(argv.saveConfig, config);
}
if (argv._.length < 1) {
console.error('Error: No video file specified');
console.log('Usage: node dist/index.js <video_file_path> [options]');
console.log('Use --help for more information');
process.exit(1);
}
const videoFilePath = String(argv._[0]);
if (argv.estimate) {
try {
const costBreakdown = await (0, costEstimator_1.estimateCost)(videoFilePath, config);
console.log('\n=== COST ESTIMATION ===');
console.log(JSON.stringify(costBreakdown, null, 2));
console.log(`\nEstimated total cost: ${costBreakdown.apiCosts.total}`);
console.log(`Estimated processing time: ${costBreakdown.estimates.estimatedProcessingTimeMinutes.toFixed(1)} minutes`);
console.log('Note: Actual costs may vary based on image complexity and actual response lengths.');
}
catch (err) {
console.error('Error estimating costs:', err);
}
}
else {
try {
const stats = (0, stats_1.createStats)();
const visionProvider = visionProviderFactory_1.VisionProviderFactory.getProvider(config);
const ttsProvider = ttsProviderFactory_1.TTSProviderFactory.getProvider(config);
await (0, processor_1.generateAudioDescription)(videoFilePath, visionProvider, ttsProvider, config, stats);
}
catch (err) {
console.error('Error generating audio description:', err);
}
}
}
//# sourceMappingURL=index.js.map

1
dist/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}

99
dist/interfaces/index.d.ts vendored Normal file
View File

@@ -0,0 +1,99 @@
export interface VisionUsage {
inputTokens: number;
outputTokens: number;
totalTokens: number;
}
export interface VisionResult {
description: string;
usage: VisionUsage;
}
export interface VisionProviderConfig {
apiKey?: string;
model: string;
maxTokens?: number;
baseUrl?: string;
}
export interface VisionProvider {
describeImage(imagePath: string, prompt: string): Promise<VisionResult>;
compareImages(image1Path: string, image2Path: string, prompt: string): Promise<VisionResult>;
describeBatch(imagePaths: string[], lastBatchContext: any, prompt: string): Promise<VisionResult>;
}
export interface TTSResult {
duration: number;
cost: number;
}
export interface TTSOptions {
voice?: string;
model?: string;
speedFactor?: number;
instructions?: string;
}
export interface TTSProviderConfig {
apiKey?: string;
model: string;
voice?: string;
keyFilename?: string;
}
export interface TTSProvider {
textToSpeech(text: string, outputPath: string, options?: TTSOptions): Promise<TTSResult>;
}
export interface AudioSegment {
audioFile: string;
startTime: number;
duration: number;
description: string;
}
export interface Stats {
totalFrames: number;
totalBatches: number;
totalVisionInputCost: number;
totalVisionOutputCost: number;
totalTTSCost: number;
totalCost: number;
}
export interface BatchContext {
lastDescription?: string;
lastFramePaths?: string[];
}
export interface ProgressInfo {
type: 'frame' | 'batch';
index: number;
total: number;
segment: AudioSegment;
}
export interface ProcessingOptions {
startIndex?: number;
existingSegments?: AudioSegment[];
lastContext?: BatchContext;
currentTimePosition?: number;
onProgress?: (info: ProgressInfo) => void;
}
export interface ProcessingResult {
videoFile: string;
audioDescriptionFile: string;
segments: AudioSegment[];
}
export interface CostBreakdown {
videoInfo: {
duration: number;
totalUnits: number;
unitType: string;
processingInterval: number;
};
providerInfo: {
visionProvider: string;
visionModel: string;
ttsProvider: string;
ttsModel: string;
};
apiCosts: {
visionInput: string;
visionOutput: string;
tts: string;
total: string;
};
estimates: {
totalAPICallsToProviders: number;
estimatedProcessingTimeMinutes: number;
};
}

4
dist/interfaces/index.js vendored Normal file
View File

@@ -0,0 +1,4 @@
"use strict";
// Common interfaces for the application
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=index.js.map

1
dist/interfaces/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/interfaces/index.ts"],"names":[],"mappings":";AAAA,wCAAwC"}

View File

@@ -0,0 +1,8 @@
import { TTSProvider, TTSProviderConfig, TTSOptions, TTSResult } from '../../interfaces';
export declare class ElevenLabsTTSProvider implements TTSProvider {
private config;
private axiosInstance;
private lastRequestId;
constructor(config: TTSProviderConfig);
textToSpeech(text: string, outputPath: string, options?: TTSOptions): Promise<TTSResult>;
}

View File

@@ -0,0 +1,79 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElevenLabsTTSProvider = void 0;
const fs_1 = __importDefault(require("fs"));
const child_process_1 = require("child_process");
const axios_1 = __importDefault(require("axios"));
const mediaUtils_1 = require("../../utils/mediaUtils");
class ElevenLabsTTSProvider {
constructor(config) {
this.lastRequestId = null;
this.config = config;
this.axiosInstance = axios_1.default.create({
baseURL: 'https://api.elevenlabs.io/v1',
headers: {
'xi-api-key': config.apiKey,
'Content-Type': 'application/json'
}
});
}
async textToSpeech(text, outputPath, options = {}) {
try {
const voice = options.voice || this.config.voice || 'JBFqnCBsd6RMkjVDRZzb';
const model = options.model || this.config.model || 'eleven_multilingual_v2';
const speedFactor = options.speedFactor || 1.0;
const requestBody = {
text,
model_id: model,
voice_settings: {
stability: 0.5,
similarity_boost: 0.75,
speed: speedFactor,
use_speaker_boost: true
}
};
if (this.lastRequestId) {
requestBody.previous_request_ids = [this.lastRequestId];
}
const tempOutputPath = outputPath.replace(/\.\w+$/, '_temp$&');
const response = await this.axiosInstance.post(`/text-to-speech/${voice}`, requestBody, {
params: { output_format: 'mp3_44100_128' },
responseType: 'arraybuffer'
});
this.lastRequestId = response.headers['request-id'] || null;
const audioBuffer = Buffer.from(response.data);
fs_1.default.writeFileSync(tempOutputPath, audioBuffer);
const cost = text.length;
if (speedFactor !== 1.0) {
(0, child_process_1.execSync)(`ffmpeg -v error -i "${tempOutputPath}" -filter:a "atempo=${speedFactor}" -c:a libmp3lame -q:a 2 "${outputPath}" -y`);
fs_1.default.unlinkSync(tempOutputPath);
}
else {
fs_1.default.renameSync(tempOutputPath, outputPath);
}
const audioDuration = (0, mediaUtils_1.getAudioDuration)(outputPath);
return {
duration: audioDuration,
cost: cost
};
}
catch (error) {
if (error.response) {
console.error(`ElevenLabs TTS error (${error.response.status}):`, Buffer.from(error.response.data).toString());
}
else {
console.error('ElevenLabs TTS error:', error.message);
}
(0, child_process_1.execSync)(`ffmpeg -v error -f lavfi -i anullsrc=r=24000:cl=mono -t 1 -q:a 9 -acodec libmp3lame "${outputPath}" -y`);
return {
duration: 1,
cost: 0
};
}
}
}
exports.ElevenLabsTTSProvider = ElevenLabsTTSProvider;
//# sourceMappingURL=elevenLabsTTSProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"elevenLabsTTSProvider.js","sourceRoot":"","sources":["../../../src/providers/tts/elevenLabsTTSProvider.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,8 @@
import { TTSProvider, TTSProviderConfig, TTSOptions, TTSResult } from '../../interfaces';
export declare class GoogleCloudTTSProvider implements TTSProvider {
private config;
private client;
constructor(config: TTSProviderConfig);
textToSpeech(text: string, outputPath: string, options?: TTSOptions): Promise<TTSResult>;
private extractLanguageCode;
}

View File

@@ -0,0 +1,80 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoogleCloudTTSProvider = void 0;
const fs_1 = __importDefault(require("fs"));
const child_process_1 = require("child_process");
const text_to_speech_1 = require("@google-cloud/text-to-speech");
const mediaUtils_1 = require("../../utils/mediaUtils");
class GoogleCloudTTSProvider {
constructor(config) {
this.config = config;
const clientConfig = {
apiKey: config.apiKey,
fallback: true
};
if (config.keyFilename) {
clientConfig.keyFilename = config.keyFilename;
}
this.client = new text_to_speech_1.TextToSpeechClient(clientConfig);
}
async textToSpeech(text, outputPath, options = {}) {
try {
const voice = options.voice || this.config.voice || 'en-US-Chirp-HD-F';
const model = options.model || this.config.model || 'chirp-hd';
const speedFactor = options.speedFactor || 1.0;
const request = {
input: { text },
voice: {
languageCode: this.extractLanguageCode(voice),
name: voice
},
audioConfig: {
audioEncoding: 'MP3',
speakingRate: speedFactor
}
};
const [response] = await this.client.synthesizeSpeech(request);
if (!response.audioContent) {
throw new Error('No audio content returned from Google Cloud TTS');
}
const audioBuffer = response.audioContent instanceof Uint8Array
? Buffer.from(response.audioContent)
: Buffer.from(response.audioContent);
const tempOutputPath = outputPath.replace(/\.\w+$/, '_temp$&');
fs_1.default.writeFileSync(tempOutputPath, audioBuffer);
const cost = text.length;
if (speedFactor !== 1.0) {
(0, child_process_1.execSync)(`ffmpeg -v error -i "${tempOutputPath}" -filter:a "atempo=${speedFactor}" -c:a libmp3lame -q:a 2 "${outputPath}" -y`);
fs_1.default.unlinkSync(tempOutputPath);
}
else {
fs_1.default.renameSync(tempOutputPath, outputPath);
}
const audioDuration = (0, mediaUtils_1.getAudioDuration)(outputPath);
return {
duration: audioDuration,
cost: cost
};
}
catch (error) {
console.error('Google Cloud TTS error:', error.message);
(0, child_process_1.execSync)(`ffmpeg -v error -f lavfi -i anullsrc=r=24000:cl=mono -t 1 -q:a 9 -acodec libmp3lame "${outputPath}" -y`);
return {
duration: 1,
cost: 0
};
}
}
extractLanguageCode(voiceName) {
const parts = voiceName.split('-');
if (parts.length >= 2) {
return `${parts[0]}-${parts[1]}`;
}
return 'en-US';
}
}
exports.GoogleCloudTTSProvider = GoogleCloudTTSProvider;
//# sourceMappingURL=googleCloudTTSProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"googleCloudTTSProvider.js","sourceRoot":"","sources":["../../../src/providers/tts/googleCloudTTSProvider.ts"],"names":[],"mappings":"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"}

4
dist/providers/tts/index.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
export * from './ttsProviderFactory';
export * from './openAITTSProvider';
export * from './elevenLabsTTSProvider';
export * from './googleCloudTTSProvider';

21
dist/providers/tts/index.js vendored Normal file
View File

@@ -0,0 +1,21 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./ttsProviderFactory"), exports);
__exportStar(require("./openAITTSProvider"), exports);
__exportStar(require("./elevenLabsTTSProvider"), exports);
__exportStar(require("./googleCloudTTSProvider"), exports);
//# sourceMappingURL=index.js.map

1
dist/providers/tts/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/providers/tts/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,uDAAqC;AACrC,sDAAoC;AACpC,0DAAwC;AACxC,2DAAyC"}

View File

@@ -0,0 +1,17 @@
import { TTSProvider, TTSProviderConfig, TTSOptions, TTSResult } from '../../interfaces';
/**
* OpenAI TTS Provider Implementation
*/
export declare class OpenAITTSProvider implements TTSProvider {
private config;
private openai;
constructor(config: TTSProviderConfig);
/**
* Convert text to speech
* @param text - Text to convert to speech
* @param outputPath - Output path for the audio file
* @param options - Additional options
* @returns Duration of the generated audio in seconds and cost
*/
textToSpeech(text: string, outputPath: string, options?: TTSOptions): Promise<TTSResult>;
}

75
dist/providers/tts/openAITTSProvider.js vendored Normal file
View File

@@ -0,0 +1,75 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAITTSProvider = void 0;
const fs_1 = __importDefault(require("fs"));
const child_process_1 = require("child_process");
const openai_1 = require("openai");
const mediaUtils_1 = require("../../utils/mediaUtils");
/**
* OpenAI TTS Provider Implementation
*/
class OpenAITTSProvider {
constructor(config) {
this.config = config;
this.openai = new openai_1.OpenAI({
apiKey: config.apiKey,
});
}
/**
* Convert text to speech
* @param text - Text to convert to speech
* @param outputPath - Output path for the audio file
* @param options - Additional options
* @returns Duration of the generated audio in seconds and cost
*/
async textToSpeech(text, outputPath, options = {}) {
try {
// Get the options, with defaults from config
const voice = options.voice || this.config.voice;
const model = options.model || this.config.model;
const speedFactor = options.speedFactor || 1.0;
// Generate the initial TTS output
const tempOutputPath = outputPath.replace(/\.\w+$/, '_temp$&');
const mp3 = await this.openai.audio.speech.create({
model: model,
voice: voice,
input: text,
...(options.instructions ? { instructions: options.instructions } : {})
});
// Cost calculation is based on character count
const cost = text.length;
const buffer = Buffer.from(await mp3.arrayBuffer());
fs_1.default.writeFileSync(tempOutputPath, buffer);
// Speed up the audio using FFmpeg if needed
if (speedFactor !== 1.0) {
(0, child_process_1.execSync)(`ffmpeg -v error -i "${tempOutputPath}" -filter:a "atempo=${speedFactor}" -c:a libmp3lame -q:a 2 "${outputPath}" -y`);
// Clean up temporary file
fs_1.default.unlinkSync(tempOutputPath);
}
else {
// Just use the file as is
fs_1.default.renameSync(tempOutputPath, outputPath);
}
// Get actual audio duration for accurate timing
const audioDuration = (0, mediaUtils_1.getAudioDuration)(outputPath);
return {
duration: audioDuration,
cost: cost
};
}
catch (error) {
console.error("Error generating speech:", error);
// Create a silent audio file if TTS fails
(0, child_process_1.execSync)(`ffmpeg -v error -f lavfi -i anullsrc=r=24000:cl=mono -t 1 -q:a 9 -acodec libmp3lame "${outputPath}" -y`);
return {
duration: 1,
cost: 0
};
}
}
}
exports.OpenAITTSProvider = OpenAITTSProvider;
//# sourceMappingURL=openAITTSProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"openAITTSProvider.js","sourceRoot":"","sources":["../../../src/providers/tts/openAITTSProvider.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,iDAAyC;AACzC,mCAAgC;AAEhC,uDAA0D;AAE1D;;GAEG;AACH,MAAa,iBAAiB;IAI5B,YAAY,MAAyB;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,IAAI,eAAM,CAAC;YACvB,MAAM,EAAE,MAAM,CAAC,MAAM;SACtB,CAAC,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAChB,IAAY,EACZ,UAAkB,EAClB,UAAsB,EAAE;QAExB,IAAI,CAAC;YACH,6CAA6C;YAC7C,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACjD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACjD,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,GAAG,CAAC;YAE/C,kCAAkC;YAClC,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;YAE/D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;gBAChD,KAAK,EAAE,KAAK;gBACZ,KAAK,EAAE,KAAY;gBACnB,KAAK,EAAE,IAAI;gBACX,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACxE,CAAC,CAAC;YAEH,+CAA+C;YAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC;YAEzB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;YACpD,YAAE,CAAC,aAAa,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;YAEzC,4CAA4C;YAC5C,IAAI,WAAW,KAAK,GAAG,EAAE,CAAC;gBACxB,IAAA,wBAAQ,EAAC,uBAAuB,cAAc,uBAAuB,WAAW,6BAA6B,UAAU,MAAM,CAAC,CAAC;gBAC/H,0BAA0B;gBAC1B,YAAE,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,0BAA0B;gBAC1B,YAAE,CAAC,UAAU,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;YAC5C,CAAC;YAED,gDAAgD;YAChD,MAAM,aAAa,GAAG,IAAA,6BAAgB,EAAC,UAAU,CAAC,CAAC;YAEnD,OAAO;gBACL,QAAQ,EAAE,aAAa;gBACvB,IAAI,EAAE,IAAI;aACX,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,KAAK,CAAC,CAAC;YACjD,0CAA0C;YAC1C,IAAA,wBAAQ,EAAC,wFAAwF,UAAU,MAAM,CAAC,CAAC;YACnH,OAAO;gBACL,QAAQ,EAAE,CAAC;gBACX,IAAI,EAAE,CAAC;aACR,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AAxED,8CAwEC"}

View File

@@ -0,0 +1,8 @@
import { TTSProvider } from '../../interfaces';
import { Config } from '../../config/config';
/**
* Factory for creating TTS providers
*/
export declare class TTSProviderFactory {
static getProvider(config: Config): TTSProvider;
}

View File

@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TTSProviderFactory = void 0;
const openAITTSProvider_1 = require("./openAITTSProvider");
const elevenLabsTTSProvider_1 = require("./elevenLabsTTSProvider");
const googleCloudTTSProvider_1 = require("./googleCloudTTSProvider");
/**
* Factory for creating TTS providers
*/
class TTSProviderFactory {
static getProvider(config) {
const providerName = config.ttsProvider;
const providerConfig = config.ttsProviders[providerName];
if (!providerConfig) {
throw new Error(`TTS provider "${providerName}" not configured.`);
}
switch (providerName) {
case 'openai':
return new openAITTSProvider_1.OpenAITTSProvider(providerConfig);
case 'elevenlabs':
return new elevenLabsTTSProvider_1.ElevenLabsTTSProvider(providerConfig);
case 'google':
return new googleCloudTTSProvider_1.GoogleCloudTTSProvider(providerConfig);
// Add other providers here
default:
throw new Error(`TTS provider "${providerName}" not implemented.`);
}
}
}
exports.TTSProviderFactory = TTSProviderFactory;
//# sourceMappingURL=ttsProviderFactory.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ttsProviderFactory.js","sourceRoot":"","sources":["../../../src/providers/tts/ttsProviderFactory.ts"],"names":[],"mappings":";;;AAEA,2DAAwD;AACxD,mEAAgE;AAChE,qEAAkE;AAElE;;GAEG;AACH,MAAa,kBAAkB;IAC7B,MAAM,CAAC,WAAW,CAAC,MAAc;QAC/B,MAAM,YAAY,GAAG,MAAM,CAAC,WAAW,CAAC;QACxC,MAAM,cAAc,GAAG,MAAM,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;QAEzD,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,iBAAiB,YAAY,mBAAmB,CAAC,CAAC;QACpE,CAAC;QAED,QAAQ,YAAY,EAAE,CAAC;YACrB,KAAK,QAAQ;gBACX,OAAO,IAAI,qCAAiB,CAAC,cAAc,CAAC,CAAC;YAC/C,KAAK,YAAY;gBACf,OAAO,IAAI,6CAAqB,CAAC,cAAc,CAAC,CAAC;YACnD,KAAK,QAAQ;gBACX,OAAO,IAAI,+CAAsB,CAAC,cAAc,CAAC,CAAC;YACpD,2BAA2B;YAC3B;gBACE,MAAM,IAAI,KAAK,CAAC,iBAAiB,YAAY,oBAAoB,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;CACF;AArBD,gDAqBC"}

View File

@@ -0,0 +1,33 @@
import { VisionProvider, VisionProviderConfig, VisionResult, BatchContext } from '../../interfaces';
/**
* Google Gemini Vision Provider Implementation
*/
export declare class GeminiVisionProvider implements VisionProvider {
private config;
private genAI;
private model;
constructor(config: VisionProviderConfig);
/**
* Describe a single image
* @param imagePath - Path to the image file
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
describeImage(imagePath: string, prompt: string): Promise<VisionResult>;
/**
* Compare two images and describe the differences
* @param image1Path - Path to the first image
* @param image2Path - Path to the second image
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
compareImages(image1Path: string, image2Path: string, prompt: string): Promise<VisionResult>;
/**
* Describe a batch of images
* @param imagePaths - Array of paths to the images
* @param lastBatchContext - Context from the previous batch
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
describeBatch(imagePaths: string[], lastBatchContext: BatchContext, prompt: string): Promise<VisionResult>;
}

View File

@@ -0,0 +1,162 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeminiVisionProvider = void 0;
const fs_1 = __importDefault(require("fs"));
const generative_ai_1 = require("@google/generative-ai");
/**
* Google Gemini Vision Provider Implementation
*/
class GeminiVisionProvider {
constructor(config) {
this.config = config;
this.genAI = new generative_ai_1.GoogleGenerativeAI(config.apiKey);
this.model = this.genAI.getGenerativeModel({ model: config.model });
}
/**
* Describe a single image
* @param imagePath - Path to the image file
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async describeImage(imagePath, prompt) {
try {
const imageData = fs_1.default.readFileSync(imagePath);
const mimeType = 'image/jpeg'; // Assuming JPEG, could be detected based on file extension
// Create a file part for the image
const imagePart = {
inlineData: {
data: imageData.toString('base64'),
mimeType
}
};
// Generate content using Gemini
const result = await this.model.generateContent([prompt, imagePart]);
const response = await result.response;
const text = response.text();
// Gemini doesn't provide token usage information in the same way as OpenAI
// We'll estimate based on prompt length and response length
const inputTokens = Math.ceil(prompt.length / 4) + 1000; // rough estimate for image
const outputTokens = Math.ceil(text.length / 4);
return {
description: text,
usage: {
inputTokens,
outputTokens,
totalTokens: inputTokens + outputTokens
}
};
}
catch (error) {
console.error("Error describing image with Gemini:", error);
return {
description: "Unable to describe this image.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
/**
* Compare two images and describe the differences
* @param image1Path - Path to the first image
* @param image2Path - Path to the second image
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async compareImages(image1Path, image2Path, prompt) {
try {
const image1Data = fs_1.default.readFileSync(image1Path);
const image2Data = fs_1.default.readFileSync(image2Path);
const mimeType = 'image/jpeg'; // Assuming JPEG, could be detected based on file extension
// Create file parts for both images
const image1Part = {
inlineData: {
data: image1Data.toString('base64'),
mimeType
}
};
const image2Part = {
inlineData: {
data: image2Data.toString('base64'),
mimeType
}
};
// Generate content using Gemini with both images
const result = await this.model.generateContent([prompt, image1Part, image2Part]);
const response = await result.response;
const text = response.text();
// Estimate token usage
const inputTokens = Math.ceil(prompt.length / 4) + 2000; // rough estimate for two images
const outputTokens = Math.ceil(text.length / 4);
return {
description: text,
usage: {
inputTokens,
outputTokens,
totalTokens: inputTokens + outputTokens
}
};
}
catch (error) {
console.error("Error comparing images with Gemini:", error);
return {
description: "Unable to describe the differences between these images.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
/**
* Describe a batch of images
* @param imagePaths - Array of paths to the images
* @param lastBatchContext - Context from the previous batch
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async describeBatch(imagePaths, lastBatchContext, prompt) {
try {
// Create a prompt that includes context from the last batch if available
let contextualPrompt = prompt;
if (lastBatchContext && lastBatchContext.lastDescription) {
contextualPrompt = `Previous batch summary: ${lastBatchContext.lastDescription}\n\n${prompt}`;
}
// Create content parts array starting with the prompt
const contentParts = [contextualPrompt];
// Add all images to the content parts
for (const imagePath of imagePaths) {
const imageData = fs_1.default.readFileSync(imagePath);
const mimeType = 'image/jpeg'; // Assuming JPEG, could be detected based on file extension
contentParts.push({
inlineData: {
data: imageData.toString('base64'),
mimeType
}
});
}
// Generate content using Gemini with all images
const result = await this.model.generateContent(contentParts);
const response = await result.response;
const text = response.text();
// Estimate token usage
const inputTokens = Math.ceil(contextualPrompt.length / 4) + (1000 * imagePaths.length); // rough estimate
const outputTokens = Math.ceil(text.length / 4);
return {
description: text,
usage: {
inputTokens,
outputTokens,
totalTokens: inputTokens + outputTokens
}
};
}
catch (error) {
console.error("Error describing batch of images with Gemini:", error);
return {
description: "Unable to describe this batch of images.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
}
exports.GeminiVisionProvider = GeminiVisionProvider;
//# sourceMappingURL=geminiVisionProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"geminiVisionProvider.js","sourceRoot":"","sources":["../../../src/providers/vision/geminiVisionProvider.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,yDAA2D;AAG3D;;GAEG;AACH,MAAa,oBAAoB;IAK/B,YAAY,MAA4B;QACtC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,KAAK,GAAG,IAAI,kCAAkB,CAAC,MAAM,CAAC,MAAO,CAAC,CAAC;QACpD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IACtE,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,aAAa,CAAC,SAAiB,EAAE,MAAc;QACnD,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,YAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;YAC7C,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,2DAA2D;YAE1F,mCAAmC;YACnC,MAAM,SAAS,GAAG;gBAChB,UAAU,EAAE;oBACV,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBAClC,QAAQ;iBACT;aACF,CAAC;YAEF,gCAAgC;YAChC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;YACrE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC;YACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;YAE7B,2EAA2E;YAC3E,4DAA4D;YAC5D,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,2BAA2B;YACpF,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAEhD,OAAO;gBACL,WAAW,EAAE,IAAI;gBACjB,KAAK,EAAE;oBACL,WAAW;oBACX,YAAY;oBACZ,WAAW,EAAE,WAAW,GAAG,YAAY;iBACxC;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAC;YAC5D,OAAO;gBACL,WAAW,EAAE,gCAAgC;gBAC7C,KAAK,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE;aAC3D,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,aAAa,CAAC,UAAkB,EAAE,UAAkB,EAAE,MAAc;QACxE,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;YAC/C,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,UAAU,CAAC,CAAC;YAC/C,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,2DAA2D;YAE1F,oCAAoC;YACpC,MAAM,UAAU,GAAG;gBACjB,UAAU,EAAE;oBACV,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBACnC,QAAQ;iBACT;aACF,CAAC;YAEF,MAAM,UAAU,GAAG;gBACjB,UAAU,EAAE;oBACV,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC;oBACnC,QAAQ;iBACT;aACF,CAAC;YAEF,iDAAiD;YACjD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;YAClF,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC;YACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;YAE7B,uBAAuB;YACvB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,gCAAgC;YACzF,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAEhD,OAAO;gBACL,WAAW,EAAE,IAAI;gBACjB,KAAK,EAAE;oBACL,WAAW;oBACX,YAAY;oBACZ,WAAW,EAAE,WAAW,GAAG,YAAY;iBACxC;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAC;YAC5D,OAAO;gBACL,WAAW,EAAE,0DAA0D;gBACvE,KAAK,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE;aAC3D,CAAC;QACJ,CAAC;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,aAAa,CACjB,UAAoB,EACpB,gBAA8B,EAC9B,MAAc;QAEd,IAAI,CAAC;YACH,yEAAyE;YACzE,IAAI,gBAAgB,GAAG,MAAM,CAAC;YAC9B,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,eAAe,EAAE,CAAC;gBACzD,gBAAgB,GAAG,2BAA2B,gBAAgB,CAAC,eAAe,OAAO,MAAM,EAAE,CAAC;YAChG,CAAC;YAED,sDAAsD;YACtD,MAAM,YAAY,GAAU,CAAC,gBAAgB,CAAC,CAAC;YAE/C,sCAAsC;YACtC,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;gBACnC,MAAM,SAAS,GAAG,YAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;gBAC7C,MAAM,QAAQ,GAAG,YAAY,CAAC,CAAC,2DAA2D;gBAE1F,YAAY,CAAC,IAAI,CAAC;oBAChB,UAAU,EAAE;wBACV,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;wBAClC,QAAQ;qBACT;iBACF,CAAC,CAAC;YACL,CAAC;YAED,gDAAgD;YAChD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;YAC9D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC;YACvC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;YAE7B,uBAAuB;YACvB,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,iBAAiB;YAC1G,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAEhD,OAAO;gBACL,WAAW,EAAE,IAAI;gBACjB,KAAK,EAAE;oBACL,WAAW;oBACX,YAAY;oBACZ,WAAW,EAAE,WAAW,GAAG,YAAY;iBACxC;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,KAAK,CAAC,CAAC;YACtE,OAAO;gBACL,WAAW,EAAE,0CAA0C;gBACvD,KAAK,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE;aAC3D,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AA3KD,oDA2KC"}

5
dist/providers/vision/index.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
export * from './visionProviderFactory';
export * from './openAIVisionProvider';
export * from './geminiVisionProvider';
export * from './ollamaVisionProvider';
export * from './openRouterVisionProvider';

22
dist/providers/vision/index.js vendored Normal file
View File

@@ -0,0 +1,22 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./visionProviderFactory"), exports);
__exportStar(require("./openAIVisionProvider"), exports);
__exportStar(require("./geminiVisionProvider"), exports);
__exportStar(require("./ollamaVisionProvider"), exports);
__exportStar(require("./openRouterVisionProvider"), exports);
//# sourceMappingURL=index.js.map

1
dist/providers/vision/index.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/providers/vision/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0DAAwC;AACxC,yDAAuC;AACvC,yDAAuC;AACvC,yDAAuC;AACvC,6DAA2C"}

View File

@@ -0,0 +1,33 @@
import { VisionProvider, VisionProviderConfig, VisionResult, BatchContext } from '../../interfaces';
/**
* Ollama Vision Provider Implementation
* See: https://github.com/ollama/ollama/blob/main/docs/api.md
*/
export declare class OllamaVisionProvider implements VisionProvider {
private config;
private axiosInstance;
constructor(config: VisionProviderConfig);
/**
* Describe a single image
* @param imagePath - Path to the image file
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
describeImage(imagePath: string, prompt: string): Promise<VisionResult>;
/**
* Compare two images and describe differences
* @param image1Path - Path to the first image
* @param image2Path - Path to the second image
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
compareImages(image1Path: string, image2Path: string, prompt: string): Promise<VisionResult>;
/**
* Describe a batch of images
* @param imagePaths - Array of paths to the images
* @param lastBatchContext - Context from the previous batch (optional)
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
describeBatch(imagePaths: string[], lastBatchContext: BatchContext, prompt: string): Promise<VisionResult>;
}

View File

@@ -0,0 +1,141 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OllamaVisionProvider = void 0;
const fs_1 = __importDefault(require("fs"));
const axios_1 = __importDefault(require("axios"));
/**
* Ollama Vision Provider Implementation
* See: https://github.com/ollama/ollama/blob/main/docs/api.md
*/
class OllamaVisionProvider {
constructor(config) {
this.config = config;
this.axiosInstance = axios_1.default.create({
baseURL: config.baseUrl || "http://localhost:11434",
headers: { "Content-Type": "application/json" }
});
}
/**
* Describe a single image
* @param imagePath - Path to the image file
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async describeImage(imagePath, prompt) {
try {
const imageData = fs_1.default.readFileSync(imagePath);
const base64Image = imageData.toString('base64');
const response = await this.axiosInstance.post('/api/generate', {
model: this.config.model,
prompt: prompt,
images: [base64Image],
stream: false,
options: {
max_tokens: this.config.maxTokens || 300,
temperature: 0.1
}
});
const combinedText = response.data.response || "";
return {
description: combinedText.trim(),
usage: {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0
}
};
}
catch (error) {
console.error("Ollama describeImage error:", error);
return {
description: "Unable to describe this image.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
/**
* Compare two images and describe differences
* @param image1Path - Path to the first image
* @param image2Path - Path to the second image
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async compareImages(image1Path, image2Path, prompt) {
try {
const image1Data = fs_1.default.readFileSync(image1Path).toString('base64');
const image2Data = fs_1.default.readFileSync(image2Path).toString('base64');
const response = await this.axiosInstance.post('/api/generate', {
model: this.config.model,
prompt: prompt,
images: [image1Data, image2Data],
stream: false,
options: {
max_tokens: this.config.maxTokens || 300,
temperature: 0.2
}
});
const combinedText = response.data.response || "";
return {
description: combinedText.trim(),
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
catch (error) {
console.error("Ollama compareImages error:", error);
return {
description: "Unable to describe the differences.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
/**
* Describe a batch of images
* @param imagePaths - Array of paths to the images
* @param lastBatchContext - Context from the previous batch (optional)
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async describeBatch(imagePaths, lastBatchContext, prompt) {
try {
let userPrompt = prompt;
// If there's context, prepend it. This helps maintain a storyline across batches.
if (lastBatchContext && lastBatchContext.lastDescription) {
userPrompt = `Previous batch summary: ${lastBatchContext.lastDescription}\n\n${prompt}`;
}
// Convert images to base64
const imagesBase64 = imagePaths.map(fp => {
const imageData = fs_1.default.readFileSync(fp);
return imageData.toString('base64');
});
const response = await this.axiosInstance.post('/api/generate', {
model: this.config.model,
prompt: userPrompt,
images: imagesBase64,
stream: false,
options: {
max_tokens: this.config.maxTokens || 300,
temperature: 0.2
}
}, {
timeout: 120000 // Timeout in milliseconds, e.g., 5000 ms = 5 seconds
});
const combinedText = response.data.response || "";
return {
description: combinedText.trim(),
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
catch (error) {
console.error("Ollama describeBatch error:", error);
return {
description: "Unable to describe this batch of images.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
}
exports.OllamaVisionProvider = OllamaVisionProvider;
//# sourceMappingURL=ollamaVisionProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ollamaVisionProvider.js","sourceRoot":"","sources":["../../../src/providers/vision/ollamaVisionProvider.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,32 @@
import { VisionProvider, VisionProviderConfig, VisionResult, BatchContext } from '../../interfaces';
/**
* OpenAI Vision Provider Implementation
*/
export declare class OpenAIVisionProvider implements VisionProvider {
private config;
private openai;
constructor(config: VisionProviderConfig);
/**
* Describe a single image
* @param imagePath - Path to the image file
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
describeImage(imagePath: string, prompt: string): Promise<VisionResult>;
/**
* Compare two images and describe the differences
* @param image1Path - Path to the first image
* @param image2Path - Path to the second image
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
compareImages(image1Path: string, image2Path: string, prompt: string): Promise<VisionResult>;
/**
* Describe a batch of images
* @param imagePaths - Array of paths to the images
* @param lastBatchContext - Context from the previous batch
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
describeBatch(imagePaths: string[], lastBatchContext: BatchContext, prompt: string): Promise<VisionResult>;
}

View File

@@ -0,0 +1,182 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAIVisionProvider = void 0;
const fs_1 = __importDefault(require("fs"));
const openai_1 = require("openai");
/**
* OpenAI Vision Provider Implementation
*/
class OpenAIVisionProvider {
constructor(config) {
this.config = config;
this.openai = new openai_1.OpenAI({
apiKey: config.apiKey,
});
}
/**
* Describe a single image
* @param imagePath - Path to the image file
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async describeImage(imagePath, prompt) {
try {
const imageData = fs_1.default.readFileSync(imagePath);
const base64Image = imageData.toString('base64');
const response = await this.openai.chat.completions.create({
model: this.config.model,
temperature: 0.1,
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
}
]
}
],
max_completion_tokens: this.config.maxTokens || 300
});
return {
description: response.choices[0].message.content?.trim() || "No description generated.",
usage: {
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0
}
};
}
catch (error) {
console.error("Error describing image:", error);
return {
description: "Unable to describe this image.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
/**
* Compare two images and describe the differences
* @param image1Path - Path to the first image
* @param image2Path - Path to the second image
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async compareImages(image1Path, image2Path, prompt) {
try {
const image1Data = fs_1.default.readFileSync(image1Path);
const image2Data = fs_1.default.readFileSync(image2Path);
const base64Image1 = image1Data.toString('base64');
const base64Image2 = image2Data.toString('base64');
const response = await this.openai.chat.completions.create({
model: this.config.model,
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image1}`
}
},
{
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64Image2}`
}
}
]
}
],
max_completion_tokens: this.config.maxTokens || 300
});
return {
description: response.choices[0].message.content?.trim() || "No description generated.",
usage: {
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0
}
};
}
catch (error) {
console.error("Error comparing images:", error);
return {
description: "Unable to describe the differences between these images.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
/**
* Describe a batch of images
* @param imagePaths - Array of paths to the images
* @param lastBatchContext - Context from the previous batch
* @param prompt - Prompt for the AI
* @returns Description and usage stats
*/
async describeBatch(imagePaths, lastBatchContext, prompt) {
try {
// Convert images to base64
const imagesBase64 = imagePaths.map(fp => {
const imageData = fs_1.default.readFileSync(fp);
return imageData.toString('base64');
});
// Build the messages array for the chat completion
const messages = [
{
role: "user",
content: [
{ type: "text", text: prompt }
]
}
];
// If we have some text context from the last batch, inject that as well
if (lastBatchContext && lastBatchContext.lastDescription) {
messages.unshift({
role: "system",
content: `Previous batch summary: ${lastBatchContext.lastDescription}`
});
}
// Append each image in the new batch
imagesBase64.forEach(base64 => {
messages[messages.length - 1].content.push({
type: "image_url",
image_url: {
url: `data:image/jpeg;base64,${base64}`
}
});
});
const response = await this.openai.chat.completions.create({
model: this.config.model,
messages,
max_completion_tokens: this.config.maxTokens || 300
});
return {
description: response.choices[0].message.content?.trim() || "No description generated.",
usage: {
inputTokens: response.usage?.prompt_tokens || 0,
outputTokens: response.usage?.completion_tokens || 0,
totalTokens: response.usage?.total_tokens || 0
}
};
}
catch (error) {
console.error("Error describing batch of images:", error);
return {
description: "Unable to describe this batch of images.",
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
}
exports.OpenAIVisionProvider = OpenAIVisionProvider;
//# sourceMappingURL=openAIVisionProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"openAIVisionProvider.js","sourceRoot":"","sources":["../../../src/providers/vision/openAIVisionProvider.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,9 @@
import { VisionProvider, VisionProviderConfig, VisionResult, BatchContext } from '../../interfaces';
export declare class OpenRouterVisionProvider implements VisionProvider {
private config;
private axiosInstance;
constructor(config: VisionProviderConfig);
describeImage(imagePath: string, prompt: string): Promise<VisionResult>;
compareImages(image1Path: string, image2Path: string, prompt: string): Promise<VisionResult>;
describeBatch(imagePaths: string[], lastBatchContext: BatchContext, prompt: string): Promise<VisionResult>;
}

View File

@@ -0,0 +1,161 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenRouterVisionProvider = void 0;
const fs_1 = __importDefault(require("fs"));
const axios_1 = __importDefault(require("axios"));
class OpenRouterVisionProvider {
constructor(config) {
this.config = config;
this.axiosInstance = axios_1.default.create({
baseURL: config.baseUrl || 'https://openrouter.ai/api/v1',
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json',
'HTTP-Referer': 'https://github.com/anomalyco/aidio-description',
'X-Title': 'Aidio Description Generator'
}
});
}
async describeImage(imagePath, prompt) {
try {
const imageData = fs_1.default.readFileSync(imagePath);
const base64Image = imageData.toString('base64');
const response = await this.axiosInstance.post('/chat/completions', {
model: this.config.model,
temperature: 0.1,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
{
type: 'image_url',
image_url: {
url: `data:image/jpeg;base64,${base64Image}`
}
}
]
}
],
max_tokens: this.config.maxTokens || 300
});
const data = response.data;
return {
description: data.choices?.[0]?.message?.content?.trim() || 'No description generated.',
usage: {
inputTokens: data.usage?.prompt_tokens || 0,
outputTokens: data.usage?.completion_tokens || 0,
totalTokens: data.usage?.total_tokens || 0
}
};
}
catch (error) {
console.error('OpenRouter describeImage error:', error.response?.data || error.message);
return {
description: 'Unable to describe this image.',
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
async compareImages(image1Path, image2Path, prompt) {
try {
const image1Data = fs_1.default.readFileSync(image1Path);
const image2Data = fs_1.default.readFileSync(image2Path);
const base64Image1 = image1Data.toString('base64');
const base64Image2 = image2Data.toString('base64');
const response = await this.axiosInstance.post('/chat/completions', {
model: this.config.model,
temperature: 0.1,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
{
type: 'image_url',
image_url: { url: `data:image/jpeg;base64,${base64Image1}` }
},
{
type: 'image_url',
image_url: { url: `data:image/jpeg;base64,${base64Image2}` }
}
]
}
],
max_tokens: this.config.maxTokens || 300
});
const data = response.data;
return {
description: data.choices?.[0]?.message?.content?.trim() || 'No description generated.',
usage: {
inputTokens: data.usage?.prompt_tokens || 0,
outputTokens: data.usage?.completion_tokens || 0,
totalTokens: data.usage?.total_tokens || 0
}
};
}
catch (error) {
console.error('OpenRouter compareImages error:', error.response?.data || error.message);
return {
description: 'Unable to describe the differences between these images.',
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
async describeBatch(imagePaths, lastBatchContext, prompt) {
try {
const imagesBase64 = imagePaths.map(fp => {
const imageData = fs_1.default.readFileSync(fp);
return imageData.toString('base64');
});
const messages = [
{
role: 'user',
content: [
{ type: 'text', text: prompt }
]
}
];
if (lastBatchContext && lastBatchContext.lastDescription) {
messages.unshift({
role: 'system',
content: `Previous batch summary: ${lastBatchContext.lastDescription}`
});
}
imagesBase64.forEach(base64 => {
messages[messages.length - 1].content.push({
type: 'image_url',
image_url: {
url: `data:image/jpeg;base64,${base64}`
}
});
});
const response = await this.axiosInstance.post('/chat/completions', {
model: this.config.model,
messages,
max_tokens: this.config.maxTokens || 300
});
const data = response.data;
return {
description: data.choices?.[0]?.message?.content?.trim() || 'No description generated.',
usage: {
inputTokens: data.usage?.prompt_tokens || 0,
outputTokens: data.usage?.completion_tokens || 0,
totalTokens: data.usage?.total_tokens || 0
}
};
}
catch (error) {
console.error('OpenRouter describeBatch error:', error.response?.data || error.message);
return {
description: 'Unable to describe this batch of images.',
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
};
}
}
}
exports.OpenRouterVisionProvider = OpenRouterVisionProvider;
//# sourceMappingURL=openRouterVisionProvider.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"openRouterVisionProvider.js","sourceRoot":"","sources":["../../../src/providers/vision/openRouterVisionProvider.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,8 @@
import { VisionProvider } from '../../interfaces';
import { Config } from '../../config/config';
/**
* Factory for creating vision AI providers
*/
export declare class VisionProviderFactory {
static getProvider(config: Config): VisionProvider;
}

View File

@@ -0,0 +1,34 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VisionProviderFactory = void 0;
const openAIVisionProvider_1 = require("./openAIVisionProvider");
const geminiVisionProvider_1 = require("./geminiVisionProvider");
const ollamaVisionProvider_1 = require("./ollamaVisionProvider");
const openRouterVisionProvider_1 = require("./openRouterVisionProvider");
/**
* Factory for creating vision AI providers
*/
class VisionProviderFactory {
static getProvider(config) {
const providerName = config.visionProvider;
const providerConfig = config.visionProviders[providerName];
if (!providerConfig) {
throw new Error(`Vision provider "${providerName}" not configured.`);
}
switch (providerName) {
case 'openai':
return new openAIVisionProvider_1.OpenAIVisionProvider(providerConfig);
case 'gemini':
return new geminiVisionProvider_1.GeminiVisionProvider(providerConfig);
case "ollama":
return new ollamaVisionProvider_1.OllamaVisionProvider(providerConfig);
case 'openrouter':
return new openRouterVisionProvider_1.OpenRouterVisionProvider(providerConfig);
// Add other providers here
default:
throw new Error(`Vision provider "${providerName}" not implemented.`);
}
}
}
exports.VisionProviderFactory = VisionProviderFactory;
//# sourceMappingURL=visionProviderFactory.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"visionProviderFactory.js","sourceRoot":"","sources":["../../../src/providers/vision/visionProviderFactory.ts"],"names":[],"mappings":";;;AAEA,iEAA8D;AAC9D,iEAA8D;AAC9D,iEAA8D;AAC9D,yEAAsE;AAEtE;;GAEG;AACH,MAAa,qBAAqB;IAChC,MAAM,CAAC,WAAW,CAAC,MAAc;QAC/B,MAAM,YAAY,GAAG,MAAM,CAAC,cAAc,CAAC;QAC3C,MAAM,cAAc,GAAG,MAAM,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;QAE5D,IAAI,CAAC,cAAc,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,oBAAoB,YAAY,mBAAmB,CAAC,CAAC;QACvE,CAAC;QAED,QAAQ,YAAY,EAAE,CAAC;YACrB,KAAK,QAAQ;gBACX,OAAO,IAAI,2CAAoB,CAAC,cAAc,CAAC,CAAC;YAClD,KAAK,QAAQ;gBACX,OAAO,IAAI,2CAAoB,CAAC,cAAc,CAAC,CAAC;YAClD,KAAK,QAAQ;gBACX,OAAO,IAAI,2CAAoB,CAAC,cAAc,CAAC,CAAC;YAClD,KAAK,YAAY;gBACf,OAAO,IAAI,mDAAwB,CAAC,cAAc,CAAC,CAAC;YACtD,2BAA2B;YAC3B;gBACE,MAAM,IAAI,KAAK,CAAC,oBAAoB,YAAY,oBAAoB,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC;CACF;AAvBD,sDAuBC"}

3
dist/server/app.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
import 'dotenv/config';

37
dist/server/index.js vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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

File diff suppressed because one or more lines are too long

2
dist/server/routes/auth.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1 @@
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../../src/server/routes/files.ts"],"names":[],"mappings":"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"}

3
dist/server/routes/jobs.d.ts vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

37
dist/server/services/jobManager.d.ts vendored Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

1
dist/server/services/muxer.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export declare function muxAudioDescription(videoPath: string, audioPath: string, outputPath: string): void;

29
dist/server/services/muxer.js vendored Normal file
View 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
View 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"}

View 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;

View 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

View 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
View 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
View 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
View 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"}

13
dist/utils/configUtils.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
import { Config } from '../config/config';
/**
* Load configuration from a JSON file
* @param filePath - Path to the configuration file
* @returns Configuration object
*/
export declare function loadConfigFromFile(filePath: string): Partial<Config>;
/**
* Save configuration to a JSON file
* @param filePath - Path to save the configuration file
* @param config - Configuration object to save
*/
export declare function saveConfigToFile(filePath: string, config: any): void;

44
dist/utils/configUtils.js vendored Normal file
View File

@@ -0,0 +1,44 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadConfigFromFile = loadConfigFromFile;
exports.saveConfigToFile = saveConfigToFile;
const fs_1 = __importDefault(require("fs"));
/**
* Load configuration from a JSON file
* @param filePath - Path to the configuration file
* @returns Configuration object
*/
function loadConfigFromFile(filePath) {
try {
const configFile = fs_1.default.readFileSync(filePath, 'utf8');
const config = JSON.parse(configFile);
console.log(`Loaded configuration from ${filePath}`);
return config;
}
catch (error) {
console.error(`Error loading config from ${filePath}:`, error);
process.exit(1);
}
}
/**
* Save configuration to a JSON file
* @param filePath - Path to save the configuration file
* @param config - Configuration object to save
*/
function saveConfigToFile(filePath, config) {
try {
// Filter out non-configuration properties
const configToSave = { ...config };
const keysToExclude = ['_', '$0', 'video_file_path', 'estimate', 'config', 'saveConfig', 'help', 'version', 'h'];
keysToExclude.forEach(key => delete configToSave[key]);
fs_1.default.writeFileSync(filePath, JSON.stringify(configToSave, null, 2), 'utf8');
console.log(`Configuration saved to ${filePath}`);
}
catch (error) {
console.error(`Error saving config to ${filePath}:`, error);
}
}
//# sourceMappingURL=configUtils.js.map

1
dist/utils/configUtils.js.map vendored Normal file
View File

@@ -0,0 +1 @@
{"version":3,"file":"configUtils.js","sourceRoot":"","sources":["../../src/utils/configUtils.ts"],"names":[],"mappings":";;;;;AAQA,gDAUC;AAOD,4CAYC;AArCD,4CAAoB;AAGpB;;;;GAIG;AACH,SAAgB,kBAAkB,CAAC,QAAgB;IACjD,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,6BAA6B,QAAQ,EAAE,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,6BAA6B,QAAQ,GAAG,EAAE,KAAK,CAAC,CAAC;QAC/D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,QAAgB,EAAE,MAAW;IAC5D,IAAI,CAAC;QACH,0CAA0C;QAC1C,MAAM,YAAY,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;QACnC,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,iBAAiB,EAAE,UAAU,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC;QACjH,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC;QAEvD,YAAE,CAAC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1E,OAAO,CAAC,GAAG,CAAC,0BAA0B,QAAQ,EAAE,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,0BAA0B,QAAQ,GAAG,EAAE,KAAK,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC"}

9
dist/utils/costEstimator.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
import { Config } from '../config/config';
import { CostBreakdown } from '../interfaces';
/**
* Estimate the cost of generating audio descriptions for a video
* @param videoFilePath - Path to the input video file
* @param options - Optional configuration overrides
* @returns Cost estimation breakdown
*/
export declare function estimateCost(videoFilePath: string, options?: Partial<Config>): Promise<CostBreakdown>;

Some files were not shown because too many files have changed in this diff Show More