- 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>
170 lines
5.8 KiB
JavaScript
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();
|