re extract multilang transcripts.ts
本地来源:seo-llm/raw/seo知识库/TXT整理/seo方法论/seo-knowledge-base/Scripts-for-Youtube/re-extract-multilang-transcripts.ts.txt
/**
* 重新提取 SeedVR2 视频的多语言字幕
* 支持英文、中文、日文、越南语等多种语言
*
* 运行方式: pnpm tsx scripts/re-extract-multilang-transcripts.ts
*/
import fs from 'node:fs';
import path from 'node:path';
import { getSubtitles } from 'youtube-caption-extractor';
// 延迟函数
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// 支持的语言列表(按优先级排序)
const SUPPORTED_LANGUAGES = [
{ code: 'en', name: 'English' },
{ code: 'zh-Hans', name: 'Chinese (Simplified)' },
{ code: 'zh-Hant', name: 'Chinese (Traditional)' },
{ code: 'zh', name: 'Chinese' },
{ code: 'ja', name: 'Japanese' },
{ code: 'vi', name: 'Vietnamese' },
{ code: 'ko', name: 'Korean' },
{ code: 'es', name: 'Spanish' },
{ code: 'fr', name: 'French' },
{ code: 'de', name: 'German' },
{ code: 'pt', name: 'Portuguese' },
{ code: 'ru', name: 'Russian' },
];
// 提取单个视频的字幕(支持多语言)
async function extractTranscriptMultiLang(
videoId: string,
index: number,
total: number
) {
console.log(`\n[${index}/${total}] 提取视频: ${videoId}`);
// 首先尝试不指定语言(获取默认字幕)
try {
console.log(' → 尝试获取默认字幕...');
const captions = await getSubtitles({ videoID: videoId });
if (captions && captions.length > 0) {
const script = captions
.map((item) => item.text)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
console.log(` ✅ 成功! 获取默认字幕,长度: ${script.length} 字符`);
return {
videoId,
success: true,
hasTranscript: true,
script,
scriptLength: script.length,
language: 'auto',
languageName: 'Auto-detected',
extractedAt: new Date().toISOString(),
error: null,
};
}
} catch (error: any) {
console.log(` ⚠️ 默认字幕失败: ${error.message}`);
}
// 尝试各种语言
for (const lang of SUPPORTED_LANGUAGES) {
try {
console.log(` → 尝试 ${lang.name} (${lang.code})...`);
const captions = await getSubtitles({
videoID: videoId,
lang: lang.code,
});
if (captions && captions.length > 0) {
const script = captions
.map((item) => item.text)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
console.log(
` ✅ 成功! 找到 ${lang.name} 字幕,长度: ${script.length} 字符`
);
return {
videoId,
success: true,
hasTranscript: true,
script,
scriptLength: script.length,
language: lang.code,
languageName: lang.name,
extractedAt: new Date().toISOString(),
error: null,
};
}
} catch (error: any) {
// 继续尝试下一种语言
continue;
}
// 每次尝试之间短暂延迟
await delay(300);
}
// 所有语言都失败
console.log(' ❌ 所有语言都没有找到字幕');
return {
videoId,
success: true,
hasTranscript: false,
script: '',
scriptLength: 0,
language: null,
languageName: null,
extractedAt: new Date().toISOString(),
error: 'No captions in any language',
};
}
// 主函数
async function main() {
console.log('🚀 开始重新提取 SeedVR2 视频字幕(多语言支持)\n');
console.log('使用库: youtube-caption-extractor (最新版)\n');
console.log(`支持的语言: ${SUPPORTED_LANGUAGES.length} 种`);
console.log('='.repeat(80));
// 读取现有的 JSON 文件
const jsonPath = path.join(process.cwd(), 'SEO/Videos/seedvr2.json');
if (!fs.existsSync(jsonPath)) {
console.error(`❌ 文件不存在: ${jsonPath}`);
process.exit(1);
}
console.log(`\n📖 读取文件: ${jsonPath}`);
const data = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
// 只处理没有字幕的视频
const videosWithoutTranscript = data.videos.filter(
(v: any) => !v.hasTranscript
);
const videosWithTranscript = data.videos.filter((v: any) => v.hasTranscript);
console.log(`📊 总视频数: ${data.videos.length}`);
console.log(`✅ 已有字幕: ${videosWithTranscript.length}`);
console.log(`❌ 无字幕: ${videosWithoutTranscript.length}`);
console.log('\n开始提取无字幕视频的字幕...\n');
// 提取无字幕视频的字幕
const results: Array<{
videoId: string;
success: boolean;
hasTranscript: boolean;
script: string;
scriptLength: number;
language: string | null;
languageName: string | null;
extractedAt: string;
error: string | null;
}> = [];
for (let i = 0; i < videosWithoutTranscript.length; i++) {
const video = videosWithoutTranscript[i];
const result = await extractTranscriptMultiLang(
video.id,
i + 1,
videosWithoutTranscript.length
);
results.push(result);
// 延迟避免限流
if (i < videosWithoutTranscript.length - 1) {
await delay(1500);
}
}
// 统计结果
const successCount = results.filter((r) => r.success).length;
const hasTranscriptCount = results.filter((r) => r.hasTranscript).length;
const failedCount = results.filter((r) => !r.success).length;
console.log(`\n${'='.repeat(80)}`);
console.log('\n📊 提取结果统计:\n');
console.log(`处理视频数: ${results.length}`);
console.log(
`成功访问: ${successCount} (${((successCount / results.length) * 100).toFixed(1)}%)`
);
console.log(
`新增字幕: ${hasTranscriptCount} (${((hasTranscriptCount / results.length) * 100).toFixed(1)}%)`
);
console.log(
`仍无字幕: ${failedCount} (${((failedCount / results.length) * 100).toFixed(1)}%)`
);
// 按语言统计
const languageStats: { [key: string]: number } = {};
results
.filter((r) => r.hasTranscript && r.languageName)
.forEach((r) => {
const langName = r.languageName!;
languageStats[langName] = (languageStats[langName] || 0) + 1;
});
if (Object.keys(languageStats).length > 0) {
console.log('\n📊 语言分布:');
for (const [lang, count] of Object.entries(languageStats)) {
console.log(` - ${lang}: ${count} 个视频`);
}
}
// 更新 JSON 数据
const updatedVideos = data.videos.map((video: any) => {
const result = results.find((r) => r.videoId === video.id);
if (!result) {
// 保留原有字幕
return video;
}
return {
...video,
script: result.script,
hasTranscript: result.hasTranscript,
scriptLength: result.scriptLength,
language: result.language,
languageName: result.languageName,
extractedAt: result.extractedAt,
extractError: result.error,
};
});
const totalWithTranscript = updatedVideos.filter(
(v: any) => v.hasTranscript
).length;
const totalWithoutTranscript = updatedVideos.filter(
(v: any) => !v.hasTranscript
).length;
const updatedData = {
...data,
videos: updatedVideos,
statistics: {
totalVideos: updatedVideos.length,
successfulExtracts: updatedVideos.length,
videosWithTranscript: totalWithTranscript,
failedExtracts: totalWithoutTranscript,
lastUpdated: new Date().toISOString(),
},
metadata: {
...data.metadata,
totalVideos: updatedVideos.length,
successfulExtracts: updatedVideos.length,
failedExtracts: totalWithoutTranscript,
videosWithTranscript: totalWithTranscript,
lastUpdated: new Date().toISOString(),
extractionLibrary: 'youtube-caption-extractor',
extractionVersion: '1.9.1',
multiLanguageSupport: true,
supportedLanguages: SUPPORTED_LANGUAGES.length,
},
};
// 创建备份
const backupPath = jsonPath.replace('.json', `-backup-${Date.now()}.json`);
console.log(`\n💾 创建备份: ${backupPath}`);
fs.writeFileSync(backupPath, JSON.stringify(data, null, 2));
// 保存更新后的数据
console.log(`\n✍️ 保存更新后的数据: ${jsonPath}`);
fs.writeFileSync(jsonPath, JSON.stringify(updatedData, null, 2));
// 同时更新 data/seo 目录
const dataSeoPath = path.join(
process.cwd(),
'data/seo',
`seedvr2-complete-${Date.now()}.json`
);
console.log(`\n💾 保存副本到: ${dataSeoPath}`);
fs.writeFileSync(dataSeoPath, JSON.stringify(updatedData, null, 2));
// 显示新增字幕的视频列表
if (hasTranscriptCount > 0) {
console.log(`\n✅ 新增字幕的视频 (${hasTranscriptCount}个):\n`);
results
.filter((r) => r.hasTranscript)
.forEach((r, i) => {
const video = data.videos.find((v: any) => v.id === r.videoId);
console.log(
`${i + 1}. [${r.languageName}] [${r.scriptLength.toLocaleString()} 字符] ${video.title}`
);
});
}
// 显示仍然没有字幕的视频
if (failedCount > 0) {
console.log(`\n❌ 仍然无字幕的视频 (${failedCount}个):\n`);
results
.filter((r) => !r.hasTranscript)
.forEach((r) => {
const video = data.videos.find((v: any) => v.id === r.videoId);
console.log(` - ${video.title}`);
console.log(` URL: https://www.youtube.com/watch?v=${r.videoId}\n`);
});
}
console.log('\n✨ 全部完成!');
console.log('\n📊 最终统计:');
console.log(` 总视频数: ${updatedData.statistics.totalVideos}`);
console.log(
` 有字幕: ${updatedData.statistics.videosWithTranscript} (${((totalWithTranscript / updatedData.statistics.totalVideos) * 100).toFixed(1)}%)`
);
console.log(
` 无字幕: ${updatedData.statistics.failedExtracts} (${((totalWithoutTranscript / updatedData.statistics.totalVideos) * 100).toFixed(1)}%)`
);
console.log(
` 新增: ${hasTranscriptCount} 个视频获得了字幕 (${videosWithTranscript.length} → ${totalWithTranscript})`
);
}
// 运行
main().catch((error) => {
console.error('\n❌ 错误:', error);
process.exit(1);
});
本文档为站内渲染。原始文件本地路径:saas/source/seo-llm/raw-seo知识库-TXT整理-seo方法论-seo-knowledge-base-Scripts-for-Youtu-beac61.txt(仅本地保留,不入库不部署)