知识库首页 seo-llm 资料 re-extract-multilang-transcripts.ts.txt

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(仅本地保留,不入库不部署)