videotomp3transcriptor/src/cli.js
StillHammer 849412c3bd Initial commit: Video to MP3 Transcriptor
- YouTube video/playlist download as MP3 (yt-dlp)
- Audio transcription with OpenAI (gpt-4o-transcribe, whisper-1)
- Translation with GPT-4o-mini (chunking for long texts)
- Web interface with progress bars and drag & drop
- CLI and REST API interfaces
- Linux shell scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 11:40:23 +08:00

170 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
import { Command } from 'commander';
import dotenv from 'dotenv';
import path from 'path';
import { download, downloadVideo, downloadPlaylist, getInfo } from './services/youtube.js';
import { transcribeFile, transcribeAndSave, transcribeMultiple, getAvailableModels } from './services/transcription.js';
// Load environment variables
dotenv.config();
const program = new Command();
program
.name('ytmp3')
.description('Download YouTube videos/playlists to MP3 and transcribe them')
.version('1.0.0');
// Download command
program
.command('download <url>')
.alias('dl')
.description('Download a YouTube video or playlist as MP3')
.option('-o, --output <dir>', 'Output directory', './output')
.action(async (url, options) => {
try {
console.log('Fetching video info...');
const result = await download(url, { outputDir: options.output });
console.log('\n--- Download Complete ---');
if (result.playlistTitle) {
console.log(`Playlist: ${result.playlistTitle}`);
}
console.log(`Downloaded: ${result.successCount}/${result.totalVideos} videos`);
result.videos.forEach(v => {
if (v.success) {
console.log(`${v.title}`);
} else {
console.log(`${v.title} - ${v.error}`);
}
});
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
// Transcribe command (from existing MP3)
program
.command('transcribe <file>')
.alias('tr')
.description('Transcribe an existing audio file')
.option('-l, --language <lang>', 'Language code (e.g., en, fr, zh)')
.option('-f, --format <format>', 'Output format (txt, srt, vtt)', 'txt')
.option('-m, --model <model>', 'Transcription model (gpt-4o-transcribe, gpt-4o-mini-transcribe, whisper-1)', 'gpt-4o-transcribe')
.action(async (file, options) => {
try {
if (!process.env.OPENAI_API_KEY) {
console.error('Error: OPENAI_API_KEY not set in environment');
process.exit(1);
}
console.log(`Transcribing: ${file}`);
const result = await transcribeAndSave(file, {
language: options.language,
responseFormat: options.format === 'txt' ? 'text' : options.format,
outputFormat: options.format,
model: options.model,
});
console.log('\n--- Transcription Complete ---');
console.log(`Model: ${result.model}`);
console.log(`Output: ${result.transcriptionPath}`);
console.log('\nPreview:');
console.log(result.text.substring(0, 500) + (result.text.length > 500 ? '...' : ''));
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
// Download + Transcribe command
program
.command('process <url>')
.alias('p')
.description('Download and transcribe a YouTube video or playlist')
.option('-o, --output <dir>', 'Output directory', './output')
.option('-l, --language <lang>', 'Language code for transcription')
.option('-f, --format <format>', 'Transcription format (txt, srt, vtt)', 'txt')
.option('-m, --model <model>', 'Transcription model (gpt-4o-transcribe, gpt-4o-mini-transcribe, whisper-1)', 'gpt-4o-transcribe')
.action(async (url, options) => {
try {
if (!process.env.OPENAI_API_KEY) {
console.error('Error: OPENAI_API_KEY not set in environment');
process.exit(1);
}
// Step 1: Download
console.log('Step 1: Downloading...');
const downloadResult = await download(url, { outputDir: options.output });
console.log(`Downloaded: ${downloadResult.successCount}/${downloadResult.totalVideos} videos\n`);
// Step 2: Transcribe
console.log(`Step 2: Transcribing with ${options.model}...`);
const successfulDownloads = downloadResult.videos.filter(v => v.success);
const filePaths = successfulDownloads.map(v => v.filePath);
const transcribeResult = await transcribeMultiple(filePaths, {
language: options.language,
responseFormat: options.format === 'txt' ? 'text' : options.format,
outputFormat: options.format,
model: options.model,
});
console.log('\n--- Process Complete ---');
if (downloadResult.playlistTitle) {
console.log(`Playlist: ${downloadResult.playlistTitle}`);
}
console.log(`Downloaded: ${downloadResult.successCount}/${downloadResult.totalVideos}`);
console.log(`Transcribed: ${transcribeResult.successCount}/${transcribeResult.totalFiles}`);
transcribeResult.results.forEach(r => {
if (r.success) {
console.log(`${path.basename(r.transcriptionPath)}`);
} else {
console.log(`${path.basename(r.filePath)} - ${r.error}`);
}
});
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
// Info command
program
.command('info <url>')
.description('Get info about a YouTube video or playlist')
.action(async (url) => {
try {
const info = await getInfo(url);
console.log('\n--- Video/Playlist Info ---');
console.log(`Title: ${info.title}`);
console.log(`Type: ${info._type || 'video'}`);
if (info._type === 'playlist') {
console.log(`Videos: ${info.entries?.length || 0}`);
if (info.entries) {
info.entries.slice(0, 10).forEach((e, i) => {
console.log(` ${i + 1}. ${e.title}`);
});
if (info.entries.length > 10) {
console.log(` ... and ${info.entries.length - 10} more`);
}
}
} else {
console.log(`Duration: ${Math.floor(info.duration / 60)}:${String(info.duration % 60).padStart(2, '0')}`);
console.log(`Channel: ${info.channel}`);
}
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
});
program.parse();