Greasy Fork is available in English.

🎬 YouTube AI 翻译助手 Pro - 实时翻译+AI配音

🚀 强大的 YouTube 视频翻译工具 | ✨ 实时英译中 | 🎯 智能AI翻译 | 🔊 自然语音朗读 | 📝 内容智能总结 | 💫 支持多种AI模型和语音引擎 | 🎨 优雅界面设计 | 让观看YouTube视频更轻松愉快!

  1. // ==UserScript==
  2. // @name 🎬 YouTube AI 翻译助手 Pro - 实时翻译+AI配音
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0.2
  5. // @license MIT
  6. // @author wangwangit
  7. // @description 🚀 强大的 YouTube 视频翻译工具 | ✨ 实时英译中 | 🎯 智能AI翻译 | 🔊 自然语音朗读 | 📝 内容智能总结 | 💫 支持多种AI模型和语音引擎 | 🎨 优雅界面设计 | 让观看YouTube视频更轻松愉快!
  8. // @match *://*.youtube.com/*
  9. // @grant GM_xmlhttpRequest
  10. // @connect xxxx
  11. // @connect xxxx
  12. // @connect api.x.ai
  13. // @run-at document-end
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19.  
  20. // 1. 首先声明全局配置变量
  21. let CONFIG;
  22.  
  23.  
  24.  
  25.  
  26. // 配置管理器
  27. class ConfigManager {
  28. static CONFIG_KEY = 'youtube_config';
  29.  
  30. static getDefaultConfig() {
  31. return {
  32. AI_MODELS: {
  33. TYPE: 'OPENAI',
  34. XAI: {
  35. API_KEY: '你的密钥',
  36. API_URL: '你的api地址,注意,要携带/v1/chat/completions',
  37. MODEL: 'grok-beta',
  38. STREAM: false
  39. },
  40. OPENAI: {
  41. API_KEY: '你的密钥',
  42. API_URL: '你的api地址,注意,要携带/v1/chat/completions',
  43. MODEL: '你想要使用的模型名称',
  44. STREAM: true
  45. }
  46. },
  47. TTS: {
  48. TYPE: 'BROWSER',
  49. VITS: {
  50. BASE_URL: 'xxxx',
  51. DEFAULT_VOICE: "char_model/原神/珊瑚宫心海/牌局的形势千变万化,想要获胜的话…有时候也必须兵行险着。.wav"
  52. },
  53. BROWSER: {
  54. RATE: 1.0,
  55. PITCH: 1.0,
  56. VOLUME: 1.0,
  57. VOICE: null
  58. }
  59. },
  60. CACHE: {
  61. AUDIO_SIZE: 500,
  62. TRANS_SIZE: 500
  63. }
  64. };
  65. }
  66.  
  67. static saveConfig(config) {
  68. try {
  69. const configString = JSON.stringify(config);
  70. localStorage.setItem('youtubeTranslatorConfig', configString);
  71. console.log('配置已保存:', config);
  72. } catch (error) {
  73. console.error('保存配置失败:', error);
  74. }
  75. }
  76.  
  77. static loadConfig() {
  78. try {
  79. const savedConfig = localStorage.getItem('youtubeTranslatorConfig');
  80. if (savedConfig) {
  81. const parsedConfig = JSON.parse(savedConfig);
  82. // 合并保存的配置和默认配置
  83. CONFIG = {...this.getDefaultConfig(), ...parsedConfig};
  84. console.log('已加载保存的配置:', CONFIG);
  85. }
  86. return CONFIG;
  87. } catch (error) {
  88. console.error('加载配置失败:', error);
  89. return CONFIG;
  90. }
  91. }
  92. }
  93.  
  94. // 初始化默认配置
  95. CONFIG = ConfigManager.getDefaultConfig();
  96. // 加载保存的配置
  97. CONFIG = ConfigManager.loadConfig();
  98. // 2. 创建基础缓存类
  99. class BaseCache {
  100.  
  101. /**
  102. * @description: 构造函数,初始化缓存。
  103. * @param {number} capacity - 缓存容量。
  104. * @param {string} prefix - 缓存键前缀。
  105. */
  106. constructor(capacity, prefix) {
  107. this.cache = new LRUCache(capacity);
  108. this.prefix = prefix;
  109. }
  110.  
  111. /**
  112. * @description: 生成缓存键。
  113. * @param {string} text - 用于生成缓存键的文本。
  114. * @param {number} startTime - 开始时间。
  115. * @return {string} - 生成的缓存键。
  116. */
  117. generateCacheKey(startTime) {
  118. const uid = getUid();
  119. const key = `${this.prefix}${uid}${startTime}`;
  120.  
  121. // console.log('生成缓存键:', {
  122. // 前缀: this.prefix,
  123. // 开始时间: startTime,
  124. // 原始文本: text.slice(0, 30) + '...',
  125. // 缓存键: key
  126. // });
  127.  
  128. return key;
  129. }
  130.  
  131.  
  132.  
  133.  
  134. /**
  135. * @description: 将缓存保存到 localStorage。
  136. * @param {string} storageKey - localStorage 键。
  137. * @return {Promise<void>}
  138. * @throws {Error} - 保存缓存失败时抛出异常。
  139. */
  140. async saveToStorage(storageKey) {
  141. try {
  142. const cacheData = {};
  143. this.cache.cache.forEach((value, key) => {
  144. cacheData[key] = value;
  145. });
  146.  
  147. localStorage.setItem(storageKey, JSON.stringify(cacheData));
  148.  
  149. // console.log('cache', '缓存已保存:', {
  150. // 缓存条目数: Object.keys(cacheData).length,
  151. // 存储大小: JSON.stringify(cacheData).length + ' bytes'
  152. // });
  153. } catch (error) {
  154. console.log('error', '保存缓存失败:', error);
  155. }
  156. }
  157.  
  158. /**
  159. * @description: 从 localStorage 加载缓存。
  160. * @param {string} storageKey - localStorage 键。
  161. * @return {Promise<null|object>} - 加载的缓存数据,如果未找到则返回 null。
  162. * @throws {Error} - 加载缓存失败时抛出异常。
  163. */
  164. async loadFromStorage(storageKey) {
  165. try {
  166. console.log('loadFromStorage', '开始加载缓存:', storageKey);
  167. const cacheData = localStorage.getItem(storageKey);
  168. if (!cacheData) {
  169. console.log('warning', '未找到缓存数据');
  170. return null;
  171. }
  172.  
  173. const parsedCache = JSON.parse(cacheData);
  174. Object.entries(parsedCache).forEach(([key, value]) => {
  175. this.cache.put(key, value);
  176. });
  177.  
  178. // console.log('success', '已加载缓存:', {
  179. // 缓存条目数: this.cache.size,
  180. // 缓存容量: this.cache.capacity
  181. // });
  182. } catch (error) {
  183. console.log('error', '加载缓存失败:', error);
  184. }
  185. }
  186.  
  187.  
  188.  
  189. }
  190.  
  191. // LRU缓存实现
  192. class LRUCache {
  193. /**
  194. * @description: 构造函数,初始化LRU缓存。
  195. * @param {number} capacity - 缓存容量。
  196. */
  197. constructor(capacity) {
  198. this.capacity = capacity;
  199. this.cache = new Map();
  200.  
  201. // 最大历史记录数
  202. this.maxHistorySize = 10;
  203. }
  204.  
  205. /**
  206. * @description: 获取缓存值。
  207. * @param {string} key - 缓存键。
  208. * @return {any} - 缓存值,如果未找到则返回 null。
  209. */
  210. get(key) {
  211. if (!this.cache.has(key)) return null;
  212. const value = this.cache.get(key);
  213. this.cache.delete(key);
  214. this.cache.set(key, value); // 更新访问时间
  215. return value;
  216. }
  217.  
  218. /**
  219. * @description: 设置缓存值。
  220. * @param {string} key - 缓存键。
  221. * @param {any} value - 缓存值。
  222. * @return {void}
  223. */
  224. put(key, value) {
  225. if (this.cache.has(key)) {
  226. this.cache.delete(key);
  227. } else if (this.cache.size >= this.capacity) {
  228. // 移除最近最少使用的条目
  229. this.cache.delete(this.cache.keys().next().value);
  230. }
  231. this.cache.set(key, value);
  232. }
  233.  
  234. /**
  235. * @description: 检查缓存中是否存在键。
  236. * @param {string} key - 缓存键。
  237. * @return {boolean} - 如果存在则返回 true,否则返回 false。
  238. */
  239. has(key) {
  240. return this.cache.has(key);
  241. }
  242.  
  243. /**
  244. * @description: 清空缓存。
  245. * @return {void}
  246. */
  247. clear() {
  248. this.cache.clear();
  249. }
  250. }
  251.  
  252.  
  253.  
  254. // 音频管理器
  255. class AudioManager extends BaseCache {
  256. constructor() {
  257. super(CONFIG.CACHE.AUDIO_SIZE, 'audio' + getUid());
  258. this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
  259. this.db = null;
  260. this.dbName = 'YTTranslatorAudio';
  261. this.storeName = 'audioBuffers';
  262. this.initDB();
  263.  
  264. // 添加浏览器TTS初始化
  265. this.synth = window.speechSynthesis;
  266. this.currentUtterance = null;
  267. }
  268.  
  269.  
  270. /**
  271. * @description: 停止当前音频播放。
  272. * @return {void}
  273. */
  274. async stopVideo() {
  275. if (CONFIG.TTS.TYPE === 'BROWSER' && this.currentUtterance) {
  276. this.synth.cancel();
  277. this.currentUtterance = null;
  278. this.isPlaying = false;
  279. } else if (this.currentSource) {
  280. try {
  281. this.currentSource.stop();
  282. this.currentSource.disconnect();
  283. this.currentSource = null;
  284. this.isPlaying = false;
  285. console.log('停止当前音频播放');
  286. } catch (error) {
  287. console.error('停止音频失败:', error);
  288. }
  289. }
  290. }
  291.  
  292.  
  293.  
  294. /**
  295. * @description: 处理 SSE 响应
  296. * @param {string} eventId - 事件ID
  297. * @return {Promise<string>} - 音频URL
  298. */
  299. async handleSSEResponse(eventId) {
  300. return new Promise((resolve, reject) => {
  301. let xhr = new XMLHttpRequest();
  302. xhr.open('GET', `${CONFIG.TTS.EDGE.BASE_URL}/call/textToSpeech/${eventId}`, true);
  303. xhr.setRequestHeader('Accept', 'text/event-stream');
  304. xhr.setRequestHeader('Cache-Control', 'no-cache');
  305.  
  306. let buffer = '';
  307.  
  308. xhr.onreadystatechange = function() {
  309. if (xhr.readyState === 3) {
  310. let newData = xhr.responseText.substring(buffer.length);
  311. buffer = xhr.responseText;
  312.  
  313. let lines = newData.split('\n');
  314. lines.forEach(line => {
  315. if (line.startsWith('data:')) {
  316. try {
  317. const jsonData = JSON.parse(line.slice(5));
  318. if (Array.isArray(jsonData) && jsonData[0]?.path) {
  319. xhr.abort();
  320. const url = `${CONFIG.TTS.EDGE.BASE_URL}/file=${jsonData[0].path}`;
  321. resolve(url);
  322. }
  323. } catch (e) {
  324. console.log('解析SSE数据失败:', e);
  325. }
  326. }
  327. });
  328. }
  329. };
  330.  
  331. xhr.onerror = reject;
  332. xhr.send();
  333.  
  334. // 30秒超时
  335. setTimeout(() => {
  336. xhr.abort();
  337. reject(new Error('SSE请求超时'));
  338. }, 300000);
  339. });
  340. }
  341.  
  342. /**
  343. * @description: 播放音频。
  344. * @param {AudioBuffer} buffer - 要播放的 AudioBuffer。
  345. * @param {number} startTime - 开始时间 (可选)。
  346. * @return {Promise<void>} - 播放完成的 Promise。
  347. * @throws {Error} - 播放失败时抛出异常。
  348. */
  349. async playAudio(buffer) {
  350. if (CONFIG.TTS.TYPE === 'BROWSER') {
  351. return new Promise((resolve, reject) => {
  352. try {
  353. this.synth.cancel(); // 停止当前播放
  354. console.log('浏览器TTS模式下直接返回翻译文本');
  355. const utterance = new SpeechSynthesisUtterance(buffer);
  356. this.currentUtterance = utterance;
  357.  
  358. // 设置语音参数
  359. utterance.lang = 'zh-CN';
  360. utterance.rate = CONFIG.TTS.BROWSER.RATE;
  361. utterance.pitch = CONFIG.TTS.BROWSER.PITCH;
  362. utterance.volume = CONFIG.TTS.BROWSER.VOLUME;
  363.  
  364. // 设置选中的语音
  365. if (CONFIG.TTS.BROWSER.VOICE) {
  366. const voices = speechSynthesis.getVoices();
  367. const selectedVoice = voices.find(voice =>
  368. voice.name === CONFIG.TTS.BROWSER.VOICE.name &&
  369. voice.lang === CONFIG.TTS.BROWSER.VOICE.lang
  370. );
  371. if (selectedVoice) {
  372. utterance.voice = selectedVoice;
  373. }
  374. }
  375.  
  376.  
  377. utterance.onend = () => {
  378. this.isPlaying = false;
  379. this.currentUtterance = null;
  380. resolve();
  381. };
  382.  
  383. utterance.onerror = (error) => {
  384. this.isPlaying = false;
  385. this.currentUtterance = null;
  386. reject(error);
  387. };
  388.  
  389. this.isPlaying = true;
  390. this.synth.speak(utterance);
  391. } catch (error) {
  392. this.isPlaying = false;
  393. this.currentUtterance = null;
  394. reject(error);
  395. }
  396. });
  397. } else {
  398. return new Promise((resolve, reject) => {
  399. try {
  400. //打印当前播放器状态
  401. //console.log('当前播放器状态:', this.shouldPlay);
  402. // 检查是否应该播放 - 修改逻辑
  403. // if (!this.shouldPlay) { // 改为检查 !this.shouldPlay
  404. // console.log('播放已停止,跳过音频播放');
  405. // return resolve(); // 直接返回,不播放音频
  406. // }
  407.  
  408. // 停止当前播放
  409. // if (this.currentSource) {
  410. // console.log('我要停止当前播放');
  411. // this.stop();
  412. // }
  413.  
  414. // 创建新的音频源
  415. const source = this.audioContext.createBufferSource();
  416. source.buffer = buffer;
  417. source.connect(this.audioContext.destination);
  418. this.currentSource = source;
  419. this.isPlaying = true;
  420.  
  421. // 监听播放完成
  422. source.onended = () => {
  423. this.isPlaying = false;
  424. this.currentSource = null;
  425. resolve();
  426. };
  427.  
  428. // 开始播放
  429. source.start(0);
  430. } catch (error) {
  431. this.isPlaying = false;
  432. this.currentSource = null;
  433. reject(error);
  434. }
  435. });
  436. }
  437. }
  438.  
  439.  
  440.  
  441. // 初始化IndexedDB
  442. async initDB() {
  443. return new Promise((resolve, reject) => {
  444. const request = indexedDB.open(this.dbName, 1);
  445.  
  446. request.onerror = () => {
  447. console.error('打开数据库失败:', request.error);
  448. reject(request.error);
  449. };
  450. request.onsuccess = () => {
  451. this.db = request.result;
  452. console.log('数据库连接成功');
  453. resolve();
  454. };
  455.  
  456. request.onupgradeneeded = (event) => {
  457. const db = event.target.result;
  458. if (!db.objectStoreNames.contains(this.storeName)) {
  459. db.createObjectStore(this.storeName);
  460. console.log('创建音频缓存存储空间');
  461. }
  462. };
  463. });
  464. }
  465.  
  466. /**
  467. * @description: 将 AudioBuffer 序列化为可存储的对象。
  468. * @param {AudioBuffer} audioBuffer - 要序列化的 AudioBuffer。
  469. * @return {object} - 序列化后的对象。
  470. */
  471. serializeAudioBuffer(audioBuffer) {
  472. const channelData = [];
  473. for (let i = 0; i < audioBuffer.numberOfChannels; i++) {
  474. channelData.push(Array.from(audioBuffer.getChannelData(i)));
  475. }
  476.  
  477. return {
  478. channelData,
  479. sampleRate: audioBuffer.sampleRate,
  480. length: audioBuffer.length,
  481. duration: audioBuffer.duration,
  482. numberOfChannels: audioBuffer.numberOfChannels
  483. };
  484. }
  485.  
  486.  
  487. /**
  488. * @description: 将序列化后的对象反序列化为 AudioBuffer。
  489. * @param {object} data - 序列化后的对象。
  490. * @return {Promise<AudioBuffer>} - 反序列化后的 AudioBuffer。
  491. */
  492. async deserializeAudioBuffer(data) {
  493. const audioBuffer = this.audioContext.createBuffer(
  494. data.numberOfChannels,
  495. data.length,
  496. data.sampleRate
  497. );
  498.  
  499. for (let i = 0; i < data.numberOfChannels; i++) {
  500. const channelData = new Float32Array(data.channelData[i]);
  501. audioBuffer.copyToChannel(channelData, i);
  502. }
  503.  
  504. return audioBuffer;
  505. }
  506.  
  507. /**
  508. * @description: 将音频数据保存到 IndexedDB。
  509. * @param {string} key - 缓存键。
  510. * @param {AudioBuffer} audioBuffer - 要保存的 AudioBuffer。
  511. * @return {Promise<void>} - 保存完成的 Promise。
  512. * @throws {Error} - 保存失败时抛出异常。
  513. */
  514. async saveToIndexedDB(key, audioBuffer) {
  515. if (!this.db) await this.initIndexedDB();
  516.  
  517. return new Promise((resolve, reject) => {
  518. const transaction = this.db.transaction([this.storeName], 'readwrite');
  519. const store = transaction.objectStore(this.storeName);
  520. const serializedData = this.serializeAudioBuffer(audioBuffer);
  521.  
  522. const request = store.put(serializedData, key);
  523.  
  524. request.onsuccess = () => {
  525. console.log('音频数据已保存到 IndexedDB:', key);
  526. resolve();
  527. };
  528.  
  529. request.onerror = () => {
  530. console.error('保存音频数据失败:', request.error);
  531. reject(request.error);
  532. };
  533. });
  534. }
  535.  
  536.  
  537. // 从 IndexedDB 加载
  538. async loadFromIndexedDB(key) {
  539. if (!this.db) await this.initIndexedDB();
  540.  
  541. return new Promise((resolve, reject) => {
  542. const transaction = this.db.transaction([this.storeName], 'readonly');
  543. const store = transaction.objectStore(this.storeName);
  544. const request = store.get(key);
  545.  
  546. request.onsuccess = async () => {
  547. if (request.result) {
  548. try {
  549. const audioBuffer = await this.deserializeAudioBuffer(request.result);
  550. // console.log('从 IndexedDB 加载音频数据成功:', key);
  551. resolve(audioBuffer);
  552. } catch (error) {
  553. console.error('反序列化音频数据失败:', error);
  554. reject(error);
  555. }
  556. } else {
  557. resolve(null);
  558. }
  559. };
  560.  
  561. request.onerror = () => {
  562. console.error('加载音频数据失败:', request.error);
  563. reject(request.error);
  564. };
  565. });
  566. }
  567.  
  568.  
  569. // 获取音频
  570. async getAudio(newSubtitles, startTime) {
  571.  
  572. if (CONFIG.TTS.TYPE === 'BROWSER') {
  573. // 浏览器TTS模式下直接返回翻译文本
  574. return newSubtitles.translation;
  575. }
  576.  
  577. const cacheKey = this.generateCacheKey(startTime);
  578.  
  579. // 检查缓存
  580. try {
  581. const cached = await this.loadFromIndexedDB(cacheKey);
  582. if (cached) {
  583. console.log('使用缓存的音频:', cacheKey);
  584. return cached;
  585. }
  586. } catch (error) {
  587. console.error('读取音频缓存失败:', error);
  588. }
  589.  
  590. // 获取新音频
  591. try {
  592. const audioBuffer = await this.fetchAudioWithRetry(newSubtitles.translation, newSubtitles.duration);
  593. // 保存到缓存
  594. await this.saveToIndexedDB(cacheKey, audioBuffer);
  595. return audioBuffer;
  596. } catch (error) {
  597. console.error('获取音频失败:', error);
  598. throw error;
  599. }
  600. }
  601.  
  602.  
  603. /**
  604. * @description: 使用重试机制获取音频。
  605. * @param {string} text - 要转换为音频的文本。
  606. * @param {number} duration - 预期音频持续时间。
  607. * @return {Promise<AudioBuffer|null>} - 获取的 AudioBuffer,如果失败则返回 null。
  608. */
  609. async fetchAudioWithRetry(text, duration) {
  610. console.log('开始获取音频:', {
  611. 文本: text,
  612. 持续时间: duration
  613. });
  614. // 添加更细致的语速调整
  615. const wordsCount = text.length;
  616. const avgCharDuration = 0.2; // 每个字符的平均时长
  617. const expectedDuration = wordsCount * avgCharDuration;
  618. let speed_factor = duration ? expectedDuration / duration : 1.0;
  619.  
  620. // 使用更平滑的映射函数
  621. if (speed_factor < 0.8) {
  622. speed_factor = 0.8 + (speed_factor / 0.8) * 0.2;
  623. } else if (speed_factor > 1.2) {
  624. speed_factor = 1.2 - (1.2 / speed_factor) * 0.2;
  625. }
  626.  
  627. // 添加音频时长验证
  628. const buffer = await this.fetchAudio(text, speed_factor);
  629. return buffer;
  630. }
  631.  
  632. /**
  633. * @description: 获取音频数据。
  634. * @param {string} text - 要转换为音频的文本。
  635. * @param {number} speed_factor - 语速因子。
  636. * @return {Promise<AudioBuffer>} - 获取的 AudioBuffer。
  637. * @throws {Error} - 获取音频失败时抛出异常。
  638. */
  639. async fetchAudio(text, speed_factor = 1.0) {
  640. if (CONFIG.TTS.TYPE === 'EDGE') {
  641. return await this.fetchAudioEdge(text);
  642. } else {
  643. // 原有的 VITS 方法
  644. const params = new URLSearchParams({
  645. text: text,
  646. text_lang: "zh",
  647. ref_audio_path: CONFIG.TTS.VITS.DEFAULT_VOICE,
  648. prompt_lang: "zh",
  649. prompt_text: "牌局的形势千变万化,想要获胜的话…有时候也必须兵行险着。",
  650. top_k: "5",
  651. top_p: "1",
  652. temperature: "0.8",
  653. speed_factor: speed_factor,
  654. fragment_interval: "0.3"
  655. });
  656.  
  657. return new Promise((resolve, reject) => {
  658. GM_xmlhttpRequest({
  659. method: 'GET',
  660. url: `${CONFIG.TTS.VITS.BASE_URL}?${params.toString()}`,
  661. responseType: 'arraybuffer',
  662. headers: {
  663. 'Accept': '*/*',
  664. 'Origin': 'https://xxxx',
  665. 'Referer': 'https://xxxx'
  666. },
  667. onload: async (response) => {
  668. try {
  669. if (response.status !== 200) {
  670. throw new Error(`HTTP Error: ${response.status}`);
  671. }
  672. const audioBuffer = await this.audioContext.decodeAudioData(response.response);
  673. resolve(audioBuffer);
  674. } catch (error) {
  675. reject(error);
  676. }
  677. },
  678. onerror: reject
  679. });
  680. });
  681. }
  682. }
  683.  
  684. // 批量预加载音频
  685. async preloadAudioBatch(subtitles, concurrentLimit = 3) {
  686. // 创建任务数组
  687. const tasks = subtitles.map(sub => ({
  688. text: sub.translation,
  689. startTime: sub.startTime
  690. }));
  691.  
  692. // 并发控制
  693. const results = [];
  694. for (let i = 0; i < subtitles.length; i += concurrentLimit) {
  695. const batch = subtitles.slice(i, i + concurrentLimit);
  696. const promises = batch.map(task =>
  697. this.getAudio(task, task.startTime)
  698. .catch(error => {
  699. console.error('音频加载失败:', error);
  700. return null;
  701. })
  702. );
  703.  
  704. const batchResults = await Promise.all(promises);
  705. results.push(...batchResults);
  706. // 简单进度显示
  707. console.log(`音频加载进度: ${i + batch.length}/${tasks.length}`);
  708. // 等待500毫秒
  709. await new Promise(resolve => setTimeout(resolve, 500));
  710. }
  711.  
  712. return results;
  713. }
  714.  
  715.  
  716. }
  717.  
  718. // 添加翻译管理器类
  719. class TranslationManager extends BaseCache {
  720. constructor() {
  721. super(CONFIG.CACHE.TRANS_SIZE, 'trans' + getUid());
  722. this.hasCache = false; // 添加缓存标志
  723. this.currentModel = CONFIG.AI_MODELS.TYPE;
  724. this.newSubtitles = [];
  725. // 定期保存缓存
  726. // setInterval(() => this.saveToStorage('ytTranslatorTransCache' + getUid()), 30000);
  727. this.loadFromStorage('ytTranslatorTransCache' + getUid());
  728. }
  729.  
  730.  
  731.  
  732. // 根据不同模型构建请求体
  733. buildRequestBody(text, modelConfig) {
  734. const systemPrompt = `你是一位资深的Netflix字幕翻译专家,精通英汉翻译,对影视作品的文化内涵和语言特点有深刻理解。你的任务是将英文Netflix字幕翻译成自然流畅、符合中文表达习惯的中文字幕,并对字幕进行必要的合并和调整,以提升观众的观影体验。
  735.  
  736. **输入格式**:
  737. 每行字幕格式为:"时间戳@@@英文字幕"
  738.  
  739. **输出格式**:
  740. 每行字幕格式为:"时间戳@@@合并后的英文字幕@@@合并后的中文翻译"
  741.  
  742. **翻译流程**:
  743.  
  744. 1. **字幕合并与优化**:
  745. - 分析连续最多3行的字幕及其上下文,酌情合并:
  746. - 同一人物的连续短句,构成完整表达。
  747. - 对前一句的补充说明或解释。
  748. - 表达并列关系或因果关系的短句。
  749. - 不合并的情况:
  750. - 不同人物的对话。
  751. - 场景切换或情绪转变。
  752. - 语气词或简短感叹词需单独保留以传达情感。
  753. - **合并后中文翻译应尽量控制在20-30个汉字之间**。如超过30个汉字,请尝试拆分,并根据句意调整时间戳,确保每句长度合理,避免字幕过长影响观影体验。
  754.  
  755. 2. **翻译要求**:
  756. - **准确传达**原文的语气、情感、文化背景和潜台词。
  757. - **译文自然流畅**,符合中文表达习惯。
  758. - **妥善处理**俚语、习语、文化特定表达、语气词、情感表达等。
  759. - **保持对话连贯性**,处理好人称代词和指代关系,确保人物语气一致。
  760. - 避免误译、漏译、错译。
  761.  
  762. 3. **输出规范**:
  763. - **格式**:"时间戳@@@合并后的英文字幕@@@合并后的中文翻译"
  764. - **每条字幕独立一行**,不添加任何额外注释或说明。
  765. - **时间戳格式正确**,保留3位小数。
  766.  
  767. **示例**:
  768.  
  769. *正面示例*:
  770.  
  771. 输入:
  772. 01.234@@@What are you doing?
  773. 01.876@@@I'm reading a book.
  774. 02.345@@@It's about a detective.
  775.  
  776. 输出:
  777. 01.234@@@What are you doing? I'm reading a book. It's about a detective.@@@你在做什么?我在读一本关于侦探的书。
  778.  
  779. 输入:
  780. 03.456@@@The car exploded.
  781. 04.123@@@Run!
  782.  
  783. 输出:
  784. 03.456@@@The car exploded.@@@汽车爆炸了!
  785. 04.123@@@Run!@@@快跑!
  786.  
  787. 输入:
  788. 05.678@@@He's a real piece of work.
  789. 06.345@@@You can say that again.
  790.  
  791. 输出:
  792. 05.678@@@He's a real piece of work.@@@他真是个怪胎。
  793. 06.345@@@You can say that again.@@@你说得对极了。
  794.  
  795. *反面示例*:
  796.  
  797. 当合并后中文翻译过长,需要拆分:
  798.  
  799. 输入:
  800. 01.234@@@He picked up the phone.
  801. 01.876@@@He dialed a number.
  802. 02.345@@@And he started talking. It was a long and complicated conversation.
  803.  
  804. 错误输出:
  805. 01.234@@@He picked up the phone. He dialed a number. And he started talking. It was a long and complicated conversation.@@@他拿起电话,拨了个号码,然后开始说话。这是一段漫长而复杂的对话。
  806.  
  807. 正确输出:
  808. 01.234@@@He picked up the phone. He dialed a number.@@@他拿起电话,拨了个号码。
  809. 01.876@@@And he started talking.@@@然后他开始说话。
  810. 02.345@@@It was a long and complicated conversation.@@@这是一段漫长而复杂的对话。
  811. `;
  812.  
  813. const baseBody = {
  814. messages: [
  815. { role: "system", content: systemPrompt },
  816. { role: "user", content: text }
  817. ],
  818. model: modelConfig.MODEL,
  819. temperature: 0.2
  820. };
  821.  
  822. // 只在支持流式的模型中添加 stream 参数
  823.  
  824. if (modelConfig.STREAM) {
  825. baseBody.stream = true;
  826. }else{
  827. baseBody.stream = false;
  828. }
  829.  
  830. return baseBody;
  831. }
  832.  
  833. // 从不同模型的响应中提取翻译文本
  834. extractTranslation(data) {
  835. const modelConfig = CONFIG.AI_MODELS[this.currentModel];
  836.  
  837. if (modelConfig.STREAM) {
  838. // 流式响应格式
  839. return data.choices[0]?.delta?.content || '';
  840. } else {
  841. // 非流式响应格式
  842. return data.choices[0]?.message?.content || '';
  843. }
  844. }
  845.  
  846.  
  847. // 非流式翻译方法
  848. async normalTranslation(text) {
  849. const modelConfig = CONFIG.AI_MODELS[this.currentModel];
  850. if (!modelConfig) {
  851. throw new Error(`未找到模型配置: ${this.currentModel}`);
  852. }
  853.  
  854. const headers = {
  855. 'Content-Type': 'application/json',
  856. 'Authorization': `Bearer ${modelConfig.API_KEY}`
  857. };
  858.  
  859. const requestBody = this.buildRequestBody(text, modelConfig);
  860.  
  861. try {
  862. const response = await fetch(modelConfig.API_URL, {
  863. method: 'POST',
  864. headers: headers,
  865. body: JSON.stringify(requestBody)
  866. });
  867.  
  868. if (!response.ok) {
  869. throw new Error(`HTTP error! status: ${response.status}`);
  870. }
  871.  
  872. const data = await response.json();
  873. return this.extractTranslation(data);
  874. } catch (error) {
  875. console.error('非流式翻译失败:', error);
  876. throw error;
  877. }
  878. }
  879.  
  880.  
  881. // 新增流式翻译方法
  882. async streamTranslation(text) {
  883. const modelConfig = CONFIG.AI_MODELS[this.currentModel];
  884. if (!modelConfig) {
  885. throw new Error(`未找到模型配置: ${this.currentModel}`);
  886. }
  887.  
  888. const headers = {
  889. 'Content-Type': 'application/json',
  890. 'Authorization': `Bearer ${modelConfig.API_KEY}`
  891. };
  892.  
  893. // 根据不同模型构建请求体
  894. const requestBody = this.buildRequestBody(text, modelConfig);
  895.  
  896. try {
  897. const response = await fetch(modelConfig.API_URL, {
  898. method: 'POST',
  899. headers: headers,
  900. body: JSON.stringify(requestBody)
  901. });
  902.  
  903. if (!response.ok) {
  904. throw new Error(`HTTP error! status: ${response.status}`);
  905. }
  906.  
  907. const reader = response.body.getReader();
  908. let decoder = new TextDecoder();
  909. let buffer = '';
  910. let translation = '';
  911.  
  912. while (true) {
  913. const {value, done} = await reader.read();
  914. if (done) break;
  915.  
  916. buffer += decoder.decode(value, {stream: true});
  917. const lines = buffer.split('\n');
  918.  
  919. // 处理完整的行
  920. for (let i = 0; i < lines.length - 1; i++) {
  921. const line = lines[i].trim();
  922. if (!line || line === 'data: [DONE]') continue;
  923.  
  924. if (line.startsWith('data: ')) {
  925. const data = JSON.parse(line.slice(5));
  926. translation += this.extractTranslation(data);
  927. }
  928. }
  929.  
  930. // 保留未完成的行
  931. buffer = lines[lines.length - 1];
  932. }
  933.  
  934. return translation.trim();
  935. } catch (error) {
  936. console.error('流式翻译失败:', error);
  937. throw error;
  938. }
  939. }
  940.  
  941.  
  942. /**
  943. * @description: 获取字幕总结
  944. * @param {Array<SubtitleEntry>} subtitles - 字幕数组
  945. * @return {Promise<string>} - 总结文本
  946. */
  947. async getSummary(subtitles) {
  948. try {
  949. // 将所有字幕文本合并
  950. const allText = subtitles
  951. .map(sub => `${sub.text}\n${sub.translation || ''}`)
  952. .join('\n');
  953.  
  954. const prompt = `请用中文总结以下视频内容的要点(不超过300字):\n\n${allText}`;
  955.  
  956. const response = await fetch(this.API_URL, {
  957. method: 'POST',
  958. headers: {
  959. 'Content-Type': 'application/json',
  960. 'Authorization': `Bearer ${this.API_KEY}`
  961. },
  962. body: JSON.stringify({
  963. messages: [
  964. {
  965. role: "system",
  966. content: "你是一个专业的视频内容总结专家。请简明扼要地总结视频的主要内容,重点和关键信息。"
  967. },
  968. {
  969. role: "user",
  970. content: prompt
  971. }
  972. ],
  973. model: "grok-beta",
  974. stream: false,
  975. temperature: 0.3
  976. })
  977. });
  978.  
  979. if (!response.ok) {
  980. throw new Error(`HTTP error! status: ${response.status}`);
  981. }
  982.  
  983. const data = await response.json();
  984. return data.choices[0].message.content.trim();
  985. } catch (error) {
  986. console.error('获取总结失败:', error);
  987. throw error;
  988. }
  989. }
  990.  
  991. // 批量翻译字幕
  992. async translateBatch(subtitles) {
  993. if (!subtitles || subtitles.length === 0) return [];
  994.  
  995. // 获取字幕数量
  996. const subLength = parseInt(localStorage.getItem('subLength' + getUid()) || '0');
  997. console.log('字幕数量:', subLength);
  998. // 获取缓存中字幕数量
  999. const cachedSubLength = this.cache.cache.size;
  1000. console.log('缓存中字幕数量:', cachedSubLength);
  1001.  
  1002. if(cachedSubLength <= subLength && cachedSubLength > 0){
  1003. // 打印缓存信息
  1004. console.log('✅ 使用现有缓存', this.cache.cache);
  1005. return Array.from(this.cache.cache.values()).sort((a, b) => a.startTime - b.startTime);
  1006. }
  1007.  
  1008. try {
  1009. // 将字幕转换为特定格式: 时间点@@@文本
  1010. const formattedSubtitles = subtitles.map(sub =>
  1011. `${sub.startTime.toFixed(3)}@@@${sub.text}`
  1012. ).join('\n');
  1013.  
  1014. // console.log('开始批量翻译:', {
  1015. // 字幕数量: subtitles.length,
  1016. // 样本: formattedSubtitles
  1017. // });
  1018.  
  1019. const translation = await this.fetchTranslation(formattedSubtitles);
  1020. // 解析翻译结果
  1021. const translationLines = translation.split('\n').filter(line => line.trim());
  1022. console.log('翻译完成:', {
  1023. 翻译结果数: translationLines.length,
  1024. 样本: translationLines
  1025. });
  1026.  
  1027.  
  1028.  
  1029.  
  1030. // 重置新字幕数组
  1031. this.newSubtitles = [];
  1032.  
  1033. // 遍历翻译结果
  1034. for (let i = 0; i < translationLines.length; i++) {
  1035. const line = translationLines[i];
  1036. const [timeStr, oldText, translatedText] = line.split('@@@');
  1037. if (!timeStr || !oldText || !translatedText) continue;
  1038.  
  1039. const startTime = parseFloat(timeStr);
  1040.  
  1041. // 查找这个时间点对应的原字幕
  1042. const originalSub = subtitles.find(s => Math.abs(s.startTime - startTime) < 0.1);
  1043. if (!originalSub) continue;
  1044.  
  1045. // 创建新的字幕条目
  1046. const newSubtitle = new SubtitleEntry(oldText, startTime, originalSub.duration);
  1047. newSubtitle.translation = translatedText;
  1048.  
  1049. // 查找下一个翻译行的时间点(如果存在)
  1050. // if (i < translationLines.length - 1) {
  1051. // const nextLine = translationLines[i + 1];
  1052. // const [nextTimeStr] = nextLine.split('@@@');
  1053. // const nextTime = parseFloat(nextTimeStr);
  1054.  
  1055. // // 查找两个时间点之间的所有原文字幕
  1056. // const intermediateSubtitles = subtitles.filter(sub =>
  1057. // sub.startTime > startTime &&
  1058. // sub.startTime < nextTime
  1059. // );
  1060.  
  1061. // // 如果存在中间字幕,合并原文
  1062. // if (intermediateSubtitles.length > 0) {
  1063. // newSubtitle.text = [originalSub.text, ...intermediateSubtitles.map(sub => sub.text)].join(' ');
  1064. // // 更新持续时间为最后一个字幕的结束时间
  1065. // const lastSub = intermediateSubtitles[intermediateSubtitles.length - 1];
  1066. // newSubtitle.duration = (lastSub.startTime + lastSub.duration) - startTime;
  1067. // }
  1068. // }
  1069.  
  1070. this.newSubtitles.push(newSubtitle);
  1071. }
  1072.  
  1073. // 按时间排序
  1074. this.newSubtitles.sort((a, b) => a.startTime - b.startTime);
  1075.  
  1076. // 调整持续时间,确保不会重叠
  1077. for (let i = 0; i < this.newSubtitles.length - 1; i++) {
  1078. const currentSub = this.newSubtitles[i];
  1079. const nextSub = this.newSubtitles[i + 1];
  1080.  
  1081. if (currentSub.startTime + currentSub.duration > nextSub.startTime) {
  1082. currentSub.duration = nextSub.startTime - currentSub.startTime;
  1083. }
  1084. }
  1085.  
  1086. console.log('字幕重构完成:', {
  1087. 原字幕数: subtitles.length,
  1088. 新字幕数: this.newSubtitles.length,
  1089. 样本: this.newSubtitles.slice(0, 3).map(sub => ({
  1090. 时间: sub.startTime,
  1091. 持续: sub.duration,
  1092. 原文: sub.text,
  1093. 译文: sub.translation
  1094. }))
  1095. });
  1096.  
  1097. // 将翻译结果保存到缓存
  1098. this.newSubtitles.forEach(sub => {
  1099. this.cache.put(this.generateCacheKey(sub.startTime), sub);
  1100. });
  1101.  
  1102. // 在storage中保存缓存,记录当前字幕数量
  1103. localStorage.setItem('subLength' + getUid(), this.newSubtitles.length);
  1104.  
  1105. // 设置缓存标志
  1106. this.hasCache = true;
  1107.  
  1108. // 返回重构后的字幕数组
  1109. return this.newSubtitles;
  1110. } catch (error) {
  1111. console.error('批量翻译失败:', error);
  1112. throw error;
  1113. }
  1114. }
  1115.  
  1116.  
  1117.  
  1118.  
  1119. // 调用翻译API
  1120. async fetchTranslation(text) {
  1121. console.log('开始翻译:', {
  1122. 文本长度: text.length,
  1123. 使用模型: this.currentModel,
  1124. 是否流式: CONFIG.AI_MODELS[this.currentModel].STREAM,
  1125. 具体模型: CONFIG.AI_MODELS[this.currentModel].MODEL
  1126. });
  1127.  
  1128. const MAX_LENGTH = 10000; // 设置单次翻译的最大字符数
  1129. const MIN_SEGMENT_SIZE = 3000; // 最小分段大小
  1130. const DELAY_BETWEEN_REQUESTS = 5000; // 请求间隔5秒
  1131.  
  1132. // 如果文本长度在限制范围内,直接翻译
  1133. if (text.length <= MAX_LENGTH) {
  1134. return CONFIG.AI_MODELS[this.currentModel].STREAM ?
  1135. await this.streamTranslation(text) :
  1136. await this.normalTranslation(text);
  1137. }
  1138.  
  1139. try {
  1140. // 将文本按换行符分割成行
  1141. const lines = text.split('\n');
  1142. const segments = [];
  1143. let currentSegment = [];
  1144. let currentLength = 0;
  1145.  
  1146. // 智能分段
  1147. for (const line of lines) {
  1148. if (currentLength + line.length > MAX_LENGTH ||
  1149. (currentLength > MIN_SEGMENT_SIZE && line.includes('@@@'))) {
  1150. if (currentSegment.length > 0) {
  1151. segments.push(currentSegment.join('\n'));
  1152. currentSegment = [];
  1153. currentLength = 0;
  1154. }
  1155. }
  1156.  
  1157. currentSegment.push(line);
  1158. currentLength += line.length;
  1159. }
  1160.  
  1161. // 添加最后一段
  1162. if (currentSegment.length > 0) {
  1163. segments.push(currentSegment.join('\n'));
  1164. }
  1165.  
  1166. console.log('文本分段完成:', {
  1167. 总行数: lines.length,
  1168. 分段数: segments.length,
  1169. 各段长度: segments.map(s => s.length)
  1170. });
  1171.  
  1172. // 串行处理所有分段,每次请求之间添加延时
  1173. const translations = [];
  1174. for (let i = 0; i < segments.length; i++) {
  1175. // 如果不是第一个请求,等待指定时间
  1176. if (i > 0) {
  1177. console.log(`等待 ${DELAY_BETWEEN_REQUESTS/1000} 秒后继续下一个请求...`);
  1178. await new Promise(resolve => setTimeout(resolve, DELAY_BETWEEN_REQUESTS));
  1179. }
  1180.  
  1181. console.log(`开始处理第 ${i + 1}/${segments.length} 段`);
  1182. const translation = await (CONFIG.AI_MODELS[this.currentModel].STREAM ?
  1183. this.streamTranslation(segments[i]) :
  1184. this.normalTranslation(segments[i]));
  1185.  
  1186. translations.push(translation);
  1187. console.log(`第 ${i + 1} 段翻译完成`);
  1188. }
  1189.  
  1190. // 合并结果
  1191. const combinedTranslation = translations.join('\n');
  1192.  
  1193. console.log('所有分段翻译完成,合并后行数:', combinedTranslation.split('\n').length);
  1194. return combinedTranslation;
  1195.  
  1196. } catch (error) {
  1197. console.error('分段翻译失败:', error);
  1198. throw error;
  1199. }
  1200. }
  1201.  
  1202. }
  1203.  
  1204. // 添加视频控制器类
  1205. class VideoController {
  1206. constructor() {
  1207. this.player = PlayerManager.getInstance().player;
  1208. this.videoElement = PlayerManager.getInstance().videoElement;
  1209. this.subtitleManager = new SubtitleManager();
  1210. this.isPlaying = false;
  1211. // 打印变量信息
  1212. console.log("VideoController: " ,this.player, this.videoElement, this.subtitleManager)
  1213. }
  1214.  
  1215.  
  1216. // 播放视频
  1217. playVideo() {
  1218. if (this.player && typeof this.player.playVideo === 'function') {
  1219. this.player.playVideo();
  1220. this.isPlaying = true;
  1221. console.log('视频开始播放');
  1222. } else if (this.videoElement) {
  1223. this.videoElement.play();
  1224. this.isPlaying = true;
  1225. console.log('视频开始播放(HTML5)');
  1226. }
  1227. }
  1228.  
  1229. // 暂停视频
  1230. pauseVideo() {
  1231. if (this.player && typeof this.player.pauseVideo === 'function') {
  1232. this.player.pauseVideo();
  1233. this.isPlaying = false;
  1234. console.log('视频已暂停');
  1235. } else if (this.videoElement) {
  1236. this.videoElement.pause();
  1237. this.isPlaying = false;
  1238. console.log('视频已暂停(HTML5)');
  1239. }
  1240. }
  1241.  
  1242. // 获取当前播放时间
  1243. getCurrentTime() {
  1244. if (this.player && typeof this.player.getCurrentTime === 'function') {
  1245. return this.player.getCurrentTime();
  1246. } else if (this.videoElement) {
  1247. return this.videoElement.currentTime;
  1248. }
  1249. return 0;
  1250. }
  1251.  
  1252. // 获取视频状态
  1253. getPlayerState() {
  1254. if (this.player && typeof this.player.getPlayerState === 'function') {
  1255. return this.player.getPlayerState();
  1256. } else if (this.videoElement) {
  1257. return this.videoElement.paused ? 2 : 1; // 1:播放中 2:暂停
  1258. }
  1259. return -1;
  1260. }
  1261. }
  1262.  
  1263. // 主控制器
  1264. class YouTubeTranslator {
  1265.  
  1266. constructor() {
  1267. // 加载配置
  1268. window.CONFIG = ConfigManager.loadConfig();
  1269. this.playerManager = PlayerManager.getInstance();
  1270. this.subtitleManager = new SubtitleManager();
  1271. this.translationManager = new TranslationManager();
  1272. this.audioManager = new AudioManager();
  1273. this.currentVideoId = this.getVideoId();
  1274. this.player = this.playerManager.player;
  1275. this.isPlaying = false;
  1276. //console.log("播放器管理器: " ,this.playerManager.player)
  1277. this.uiManager = null; // 添加 uiManager 属性
  1278. // 上一条播放的字幕时间戳
  1279. this.lastPlayedSubtitleTime = 0;
  1280.  
  1281. }
  1282.  
  1283.  
  1284. /**
  1285. * @description: 处理配置更新
  1286. * @param {string} key - 配置键
  1287. * @param {any} value - 新的配置值
  1288. */
  1289. onConfigUpdate(key, value) {
  1290. console.log('翻译器收到配置更新:', {
  1291. 配置项: key,
  1292. 新值: value
  1293. });
  1294.  
  1295. // 如果是模型相关的配置更新
  1296. if (key.startsWith('AI_MODELS')) {
  1297. // 更新翻译管理器的当前模型
  1298. if (key === 'AI_MODELS.TYPE') {
  1299. this.translationManager.currentModel = value;
  1300. console.log('切换翻译模型:', {
  1301. 新模型: value,
  1302. 模型名称: CONFIG.AI_MODELS[value].MODEL,
  1303. 流式响应: CONFIG.AI_MODELS[value].STREAM
  1304. });
  1305. }
  1306. }
  1307.  
  1308. // 如果是TTS相关的配置更新
  1309. if (key.startsWith('TTS')) {
  1310. // 可以在这里添加TTS配置更新的处理逻辑
  1311. console.log('TTS配置已更新');
  1312. }
  1313. }
  1314.  
  1315. async generateSummary() {
  1316. try {
  1317. if (!this.subtitleManager.subtitles.length) {
  1318. throw new Error('没有可用的字幕');
  1319. }
  1320. return await this.translationManager.getSummary(this.subtitleManager.subtitles);
  1321. } catch (error) {
  1322. console.error('生成总结失败:', error);
  1323. throw error;
  1324. }
  1325. }
  1326.  
  1327. // 添加设置 UI 管理器的方法
  1328. setUIManager(uiManager) {
  1329. this.uiManager = uiManager;
  1330. }
  1331.  
  1332. startPeriodicCheck() {
  1333. if (this.checkInterval) {
  1334. clearInterval(this.checkInterval);
  1335. this.checkInterval = null;
  1336. }
  1337.  
  1338. this.checkInterval = setInterval(async () => {
  1339. if (!this.isActive) {
  1340. clearInterval(this.checkInterval);
  1341. this.checkInterval = null;
  1342. return;
  1343. }
  1344. //console.log('检查播放状态...');
  1345.  
  1346. try {
  1347. // 如果当前正在播放音频,跳过这次检查
  1348. if (this.isPlayingAudio) {
  1349. return;
  1350. }
  1351. const currentTime = this.player.getCurrentTime();
  1352. // 快3秒
  1353. // 获取当前时间并加3秒提前量
  1354. let checkTime = currentTime + 2;
  1355. //console.log('当前播放时间:', currentTime);
  1356. // 获取当前时间点的字幕
  1357. const currentSubtitle = this.subtitleManager.findSubtitleAtTime(checkTime);
  1358. // 如果当前时间点没有字幕,跳过
  1359. if (!currentSubtitle) return;
  1360.  
  1361. if(currentSubtitle.startTime <= this.lastPlayedSubtitleTime){
  1362. return;
  1363. }
  1364.  
  1365. // 检查是否已经播放过这个字幕
  1366. if (this.lastPlayedSubtitleTime === currentSubtitle.startTime) {
  1367. return;
  1368. }
  1369.  
  1370. // 生成缓存键
  1371. const cacheKey = this.audioManager.generateCacheKey(
  1372. currentSubtitle.startTime
  1373. );
  1374.  
  1375. // 更新UI显示最近的字幕
  1376. if (this.uiManager) {
  1377. this.uiManager.updateSubtitleDisplay(currentSubtitle);
  1378. }
  1379. this.lastPlayedSubtitleTime = currentSubtitle.startTime;
  1380.  
  1381. if (CONFIG.TTS.TYPE === 'BROWSER') {
  1382. // 设置播放状态
  1383. this.isPlayingAudio = true;
  1384. // console.log('浏览器TTS模式',CONFIG.TTS.BROWSER.VOICE,currentSubtitle);
  1385.  
  1386. // 播放音频
  1387. try{
  1388. await this.audioManager.playAudio(currentSubtitle.translation);
  1389. } finally {
  1390. // 确保播放完成后重置状态
  1391. this.isPlayingAudio = false;
  1392. }
  1393. }else{
  1394.  
  1395. // 从缓存获取音频
  1396. const cachedAudio = await this.audioManager.loadFromIndexedDB(cacheKey);
  1397. if (cachedAudio) {
  1398. // 再次检查状态,防止在加载音频过程中状态发生变化
  1399. if (this.isPlayingAudio || !this.isActive) {
  1400. return;
  1401. }
  1402.  
  1403. console.log('找到缓存音频,准备播放:', {
  1404. 时间点: currentSubtitle.startTime,
  1405. 原文: currentSubtitle.text,
  1406. 译文: currentSubtitle.translation
  1407. });
  1408. // 设置播放状态
  1409. this.isPlayingAudio = true;
  1410.  
  1411.  
  1412. try {
  1413. // 播放音频
  1414. await this.audioManager.playAudio(cachedAudio);
  1415. // 记录已播放的字幕时间戳
  1416. this.lastPlayedSubtitleTime = currentSubtitle.startTime;
  1417. } finally {
  1418. // 确保播放完成后重置状态
  1419. this.isPlayingAudio = false;
  1420. }
  1421. }
  1422. }
  1423. } catch (error) {
  1424. console.error('定期检查出错:', error);
  1425. this.isPlayingAudio = false;
  1426. }
  1427. }, 1000); // 每秒检查一次
  1428. }
  1429.  
  1430.  
  1431. // 在 startTranslator 方法中添加调用
  1432. async startTranslator() {
  1433. try {
  1434. this.isActive = true;
  1435. console.log('开始启动翻译器...');
  1436. // 开始定时检查任务
  1437. this.startPeriodicCheck();
  1438. console.log('翻译器启动完成');
  1439. this.uiManager.updateStatus('开始播放', 'success');
  1440. } catch (error) {
  1441. console.error('启动失败:', error);
  1442. this.uiManager.updateStatus(`启动失败: ${error.message}`, 'error');
  1443. this.isActive = false;
  1444. }
  1445. }
  1446.  
  1447. // 在 stopTranslator 方法中添加清理
  1448. stopTranslator() {
  1449. console.log('停止翻译器...');
  1450. this.isPlayingAudio = false; // 重置播放状态
  1451. // 清除定时检查
  1452. if (this.checkInterval) {
  1453. clearInterval(this.checkInterval);
  1454. this.checkInterval = null;
  1455. }
  1456. }
  1457.  
  1458.  
  1459. // 添加翻译所有字幕的方法
  1460. async translateAllSubtitles() {
  1461. try {
  1462. console.log('开始翻译所有字幕...');
  1463. const subtitles = this.subtitleManager.subtitles;
  1464. const newSubtitles = await this.translationManager.translateBatch(subtitles);
  1465. console.log('所有字幕翻译完成,字幕数:', newSubtitles.length);
  1466. // 开始预加载音频
  1467. console.log('开始预加载音频...');
  1468. await this.audioManager.preloadAudioBatch(newSubtitles);
  1469. // 更新字幕管理器中的字幕数组
  1470. this.subtitleManager.subtitles = newSubtitles;
  1471.  
  1472. console.log('所有字幕翻译和音频加载完成');
  1473. return true;
  1474. } catch (error) {
  1475. console.error('翻译字幕失败:', error);
  1476. throw error;
  1477. }
  1478. }
  1479.  
  1480.  
  1481. async loadSubtitles() {
  1482. if (!this.currentVideoId) {
  1483. throw new Error('未找到视频ID');
  1484. }
  1485.  
  1486. try {
  1487. const hasSubtitles = await this.subtitleManager.loadSubtitles(this.currentVideoId);
  1488. if (!hasSubtitles) {
  1489. throw new Error('未找到字幕');
  1490. }
  1491. return true;
  1492. } catch (error) {
  1493. console.error('加载字幕失败:', error);
  1494. throw error;
  1495. }
  1496. }
  1497.  
  1498. getVideoId() {
  1499. try {
  1500. // 检查是否在YouTube账户页面
  1501. if (window.location.href.includes('accounts.youtube.com')) {
  1502. return null;
  1503. }
  1504.  
  1505. // 方法1: 从URL获取
  1506. const url = window.location.href;
  1507. console.log("当前页面URL:", url);
  1508.  
  1509. if (url.includes('youtube.com')) {
  1510. // 标准观看页面
  1511. if (url.includes('/watch')) {
  1512. const urlParams = new URLSearchParams(window.location.search);
  1513. const videoId = urlParams.get('v');
  1514. if (videoId) {
  1515. console.log("从URL参数获取到视频ID:", videoId);
  1516. return videoId;
  1517. }
  1518. }
  1519.  
  1520. // 短视频格式
  1521. if (url.includes('/shorts/')) {
  1522. const matches = url.match(/\/shorts\/([a-zA-Z0-9_-]{11})/);
  1523. if (matches && matches[1]) {
  1524. console.log("从shorts URL获取到视频ID:", matches[1]);
  1525. return matches[1];
  1526. }
  1527. }
  1528. }
  1529.  
  1530. // 方法2: 从视频元素获取
  1531. const videoElement = document.querySelector('video');
  1532. if (videoElement) {
  1533. // 从视频源获取
  1534. const videoSrc = videoElement.src;
  1535. if (videoSrc) {
  1536. const videoIdMatch = videoSrc.match(/\/([a-zA-Z0-9_-]{11})/);
  1537. if (videoIdMatch && videoIdMatch[1]) {
  1538. console.log("从视频源获取到视频ID:", videoIdMatch[1]);
  1539. return videoIdMatch[1];
  1540. }
  1541. }
  1542.  
  1543. // 从播放器容器获取
  1544. const playerContainer = document.getElementById('movie_player') ||
  1545. document.querySelector('.html5-video-player');
  1546. if (playerContainer) {
  1547. const dataVideoId = playerContainer.getAttribute('video-id') ||
  1548. playerContainer.getAttribute('data-video-id');
  1549. if (dataVideoId) {
  1550. console.log("从播放器容器获取到视频ID:", dataVideoId);
  1551. return dataVideoId;
  1552. }
  1553. }
  1554. }
  1555.  
  1556. // 方法3: 从页面元数据获取
  1557. const ytdPlayerConfig = document.querySelector('ytd-player');
  1558. if (ytdPlayerConfig) {
  1559. const videoData = ytdPlayerConfig.getAttribute('video-id');
  1560. if (videoData) {
  1561. console.log("从ytd-player获取到视频ID:", videoData);
  1562. return videoData;
  1563. }
  1564. }
  1565.  
  1566. // 方法4: 从页面脚本数据获取
  1567. const scripts = document.getElementsByTagName('script');
  1568. for (const script of scripts) {
  1569. const content = script.textContent;
  1570. if (content && content.includes('"videoId"')) {
  1571. const match = content.match(/"videoId":\s*"([a-zA-Z0-9_-]{11})"/);
  1572. if (match && match[1]) {
  1573. console.log("从页面脚本获取到视频ID:", match[1]);
  1574. return match[1];
  1575. }
  1576. }
  1577. }
  1578.  
  1579. // 如果所有方法都失败,等待页面加载完成后重试
  1580. if (document.readyState !== 'complete') {
  1581. console.log("页面未完全加载,返回null");
  1582. return null;
  1583. }
  1584.  
  1585. throw new Error('未在当前页面找到有效的YouTube视频');
  1586. } catch (error) {
  1587. console.error('获取视频ID失败:', error);
  1588. return null;
  1589. }
  1590. }
  1591. }
  1592.  
  1593. // 添加播放器管理类(单例模式)
  1594. class PlayerManager {
  1595. constructor() {
  1596. // 如果已经存在实例,直接返回
  1597. if (PlayerManager.instance) {
  1598. return PlayerManager.instance;
  1599. }
  1600.  
  1601. this._player = null;
  1602. this._videoElement = null;
  1603. this._initialized = false;
  1604. PlayerManager.instance = this;
  1605. }
  1606.  
  1607. // 获取实例的静态方法
  1608. static getInstance() {
  1609. if (!PlayerManager.instance) {
  1610. PlayerManager.instance = new PlayerManager();
  1611. }
  1612. return PlayerManager.instance;
  1613. }
  1614.  
  1615. // 初始化播放器
  1616. async initialize() {
  1617. if (this._initialized) {
  1618. return this._player;
  1619. }
  1620.  
  1621. try {
  1622. await this.waitForYouTubePlayer();
  1623. this._initialized = true;
  1624. console.log('播放器管理器初始化成功');
  1625. return this._player;
  1626. } catch (error) {
  1627. console.error('播放器管理器初始化失败:', error);
  1628. throw error;
  1629. }
  1630. }
  1631.  
  1632. // 等待YouTube播放器加载
  1633. async waitForYouTubePlayer() {
  1634. return new Promise((resolve, reject) => {
  1635. let attempts = 0;
  1636. const maxAttempts = 20;
  1637. const interval = setInterval(() => {
  1638. const player = document.querySelector('#movie_player');
  1639. const videoElement = document.querySelector('video');
  1640.  
  1641. if (player && typeof player.getCurrentTime === 'function') {
  1642. clearInterval(interval);
  1643. this._player = player;
  1644. this._videoElement = videoElement;
  1645. console.log('成功获取YouTube播放器');
  1646. resolve(player);
  1647. } else if (++attempts >= maxAttempts) {
  1648. clearInterval(interval);
  1649. reject(new Error('无法获取YouTube播放器'));
  1650. }
  1651. }, 500);
  1652. });
  1653. }
  1654.  
  1655. // 获取播放器实例
  1656. get player() {
  1657. return this._player;
  1658. }
  1659.  
  1660. // 获取video元素
  1661. get videoElement() {
  1662. return this._videoElement;
  1663. }
  1664.  
  1665. // 检查播放器是否已初始化
  1666. get isInitialized() {
  1667. return this._initialized;
  1668. }
  1669. }
  1670.  
  1671.  
  1672. // 字幕条目类
  1673. class SubtitleEntry {
  1674. constructor(text, startTime, duration) {
  1675. this.text = text;
  1676. this.startTime = startTime;
  1677. this.duration = duration;
  1678. this.translation = null;
  1679. this.audioBuffer = null;
  1680. }
  1681. }
  1682.  
  1683. // 字幕管理器类
  1684. class SubtitleManager {
  1685. constructor() {
  1686. this.subtitles = [];
  1687. this.currentIndex = 0;
  1688. }
  1689.  
  1690. /**
  1691. * @description: 加载字幕。
  1692. * @param {string} videoId - 视频 ID。
  1693. * @return {Promise<boolean>} - 是否成功加载字幕。
  1694. * @throws {Error} - 加载字幕失败时抛出异常。
  1695. */
  1696. async loadSubtitles(videoId) {
  1697. try {
  1698. // 获取页面HTML内容
  1699. const response = await fetch(`https://www.youtube.com/watch?v=${videoId}`);
  1700. const html = await response.text();
  1701.  
  1702. // 使用正则表达式匹配字幕URL
  1703. const timedTextMatch = html.match(/https:\/\/www\.youtube\.com\/api\/timedtext\?[^"]+/);
  1704. if (!timedTextMatch) {
  1705. throw new Error('未找到字幕URL');
  1706. }
  1707.  
  1708. // 构建字幕URL
  1709. const url = new URL(timedTextMatch[0].replace(/\\u0026/g, '&'));
  1710. url.searchParams.set('lang', 'en'); // 设置为英文字幕
  1711. const subtitleUrl = url.toString();
  1712.  
  1713. console.log('获取字幕:', subtitleUrl);
  1714. const subtitleResponse = await fetch(subtitleUrl);
  1715. const subtitleXML = await subtitleResponse.text();
  1716. // console.log('字幕XML:', subtitleXML); // 添加日志输出
  1717.  
  1718. // 解析字幕
  1719. const textRegex = /<text[^>]*>([\s\S]*?)<\/text>/g;
  1720. this.subtitles = [];
  1721. let match;
  1722.  
  1723. while ((match = textRegex.exec(subtitleXML)) !== null) {
  1724. const text = match[1]
  1725. .replace(/&quot;/g, '"')
  1726. .replace(/&apos;/g, "'")
  1727. .replace(/&lt;/g, '<')
  1728. .replace(/&gt;/g, '>')
  1729. .replace(/&amp;/g, '&')
  1730. .replace(/&#39;/g, "'")
  1731. .replace(/&#34;/g, '"')
  1732. .replace(/\n/g, ' ')
  1733. .trim();
  1734.  
  1735. if (text) { // 只添加非空文本
  1736. // 获取开始时间和持续时间
  1737. const startMatch = match[0].match(/start="([^"]+)"/);
  1738. const durMatch = match[0].match(/dur="([^"]+)"/);
  1739.  
  1740. const startTime = startMatch ? parseFloat(startMatch[1]) : 0;
  1741. const duration = durMatch ? parseFloat(durMatch[1]) : 0;
  1742.  
  1743. this.subtitles.push(new SubtitleEntry(text, startTime, duration));
  1744. }
  1745. }
  1746. // 解析完字幕后进行排序
  1747. this.subtitles.sort((a, b) => a.startTime - b.startTime);
  1748. console.log(`成功加载 ${this.subtitles.length} 条字幕`);
  1749. return this.subtitles.length > 0;
  1750. } catch (error) {
  1751. console.error('获取字幕时出错:', error);
  1752. throw error;
  1753. }
  1754. }
  1755.  
  1756. /**
  1757. * @description: 获取指定时间范围内的字幕。
  1758. * @param {number} startTime - 开始时间。
  1759. * @param {number} endTime - 结束时间。
  1760. * @return {Array<SubtitleEntry>} - 指定时间范围内的字幕数组。
  1761. */
  1762. getSubtitlesInRange(startTime, endTime) {
  1763. return this.subtitles.filter(sub =>
  1764. sub.startTime >= startTime && sub.startTime <= endTime
  1765. );
  1766. }
  1767.  
  1768. /**
  1769. * @description: 查找指定时间点对应的字幕。
  1770. * @param {number} time - 时间点。
  1771. * @return {SubtitleEntry|null} - 找到的字幕,如果未找到则返回 null。
  1772. */
  1773. findSubtitleAtTime(time) {
  1774. try {
  1775. // 获取所有字幕的时间点
  1776. const timePoints = this.subtitles.map(sub => ({
  1777. time: sub.startTime,
  1778. subtitle: sub
  1779. }));
  1780.  
  1781. // 按时间排序
  1782. timePoints.sort((a, b) => a.time - b.time);
  1783.  
  1784. // 找到小于等于当前时间的最后一条字幕
  1785. let targetSubtitle = null;
  1786. for (let i = timePoints.length - 1; i >= 0; i--) {
  1787. if (timePoints[i].time <= time) {
  1788. targetSubtitle = timePoints[i].subtitle;
  1789. break;
  1790. }
  1791. }
  1792.  
  1793. if (targetSubtitle) {
  1794. // console.log('找到目标字幕:', {
  1795. // 当前时间: time,
  1796. // 字幕: {
  1797. // 文本: targetSubtitle.text,
  1798. // 开始时间: targetSubtitle.startTime,
  1799. // 持续时间: targetSubtitle.duration
  1800. // }
  1801. // });
  1802. return targetSubtitle;
  1803. }
  1804.  
  1805. // 如果没有找到小于等于当前时间的字幕,返回第一条字幕
  1806. if (timePoints.length > 0 && time < timePoints[0].time) {
  1807. const firstSubtitle = timePoints[0].subtitle;
  1808. console.log('返回第一条字幕:', {
  1809. 当前时间: time,
  1810. 字幕: {
  1811. 文本: firstSubtitle.text,
  1812. 开始时间: firstSubtitle.startTime,
  1813. 持续时间: firstSubtitle.duration
  1814. }
  1815. });
  1816. return firstSubtitle;
  1817. }
  1818.  
  1819. console.log('未找到合适的字幕:', {
  1820. 当前时间: time,
  1821. 字幕总数: this.subtitles.length
  1822. });
  1823. return null;
  1824.  
  1825. } catch (error) {
  1826. console.error('查找字幕时出错:', error);
  1827. return null;
  1828. }
  1829. }
  1830.  
  1831. }
  1832.  
  1833.  
  1834.  
  1835.  
  1836.  
  1837.  
  1838.  
  1839.  
  1840. // UI管理器
  1841. class UIManager {
  1842. constructor(videoController,translator) {
  1843. this.container = null;
  1844. this.statusDisplay = null;
  1845. this.startButton = null;
  1846. this.pauseButton = null;
  1847. this.loadSubtitlesButton = null;
  1848.  
  1849. this.isCollapsed = false;
  1850. this.videoController = videoController;
  1851. this.translator = translator;
  1852. this.lastDisplayedSubtitleId = null; // 添加追踪变量
  1853. this.createConfigPanel();
  1854. this.createUI();
  1855.  
  1856. this.attachEventListeners();
  1857.  
  1858. }
  1859.  
  1860.  
  1861.  
  1862.  
  1863. createUI() {
  1864. // 创建主容器
  1865. this.container = document.createElement('div');
  1866. this.container.style.cssText = `
  1867. position: fixed;
  1868. top: 20px;
  1869. right: 20px;
  1870. width: 390px;
  1871. background: rgba(33, 33, 33, 0.9);
  1872. border-radius: 8px;
  1873. padding: 15px;
  1874. color: #fff;
  1875. font-family: Arial, sans-serif;
  1876. z-index: 9999;
  1877. transition: all 0.3s ease;
  1878. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
  1879. `;
  1880.  
  1881. // 创建顶部栏
  1882. const topBar = this.createTopBar();
  1883. this.container.appendChild(topBar);
  1884.  
  1885. // 创建主内容容器
  1886. this.mainContent = document.createElement('div');
  1887. this.mainContent.style.cssText = `
  1888. transition: all 0.3s ease;
  1889. `;
  1890.  
  1891. // 创建控制按钮
  1892. const controls = this.createControls();
  1893. this.mainContent.appendChild(controls);
  1894.  
  1895. // 创建状态显示区域
  1896. this.createStatusDisplay();
  1897. this.mainContent.appendChild(this.statusDisplay);
  1898.  
  1899. // 创建TTS面板
  1900. this.createTTSPanel();
  1901.  
  1902. // 创建并添加总结面板
  1903. this.createSummaryPanel();
  1904.  
  1905. this.container.appendChild(this.mainContent);
  1906. document.body.appendChild(this.container);
  1907.  
  1908. // 创建配置面板
  1909. this.createConfigPanel();
  1910.  
  1911. // 使面板可拖动
  1912. this.makeDraggable(topBar);
  1913. }
  1914.  
  1915.  
  1916. createTTSPanel() {
  1917. const ttsPanel = document.createElement('div');
  1918. ttsPanel.style.cssText = `
  1919. margin-top: 15px;
  1920. padding: 15px;
  1921. background: rgba(33, 150, 243, 0.1);
  1922. border-radius: 8px;
  1923. border-left: 4px solid #2196F3;
  1924. `;
  1925.  
  1926. // TTS类型选择
  1927. const typeContainer = document.createElement('div');
  1928. typeContainer.style.cssText = `
  1929. margin-bottom: 12px;
  1930. display: flex;
  1931. align-items: center;
  1932. `;
  1933.  
  1934. const typeLabel = document.createElement('label');
  1935. typeLabel.textContent = 'TTS引擎: ';
  1936. typeLabel.style.cssText = `
  1937. color: #fff;
  1938. margin-right: 10px;
  1939. font-size: 14px;
  1940. font-weight: 500;
  1941. `;
  1942.  
  1943. const typeSelect = document.createElement('select');
  1944. typeSelect.style.cssText = `
  1945. padding: 8px 12px;
  1946. border-radius: 4px;
  1947. background: rgba(255, 255, 255, 0.9);
  1948. color: #333;
  1949. border: 1px solid rgba(33, 150, 243, 0.3);
  1950. font-size: 14px;
  1951. cursor: pointer;
  1952. outline: none;
  1953. transition: all 0.3s ease;
  1954. `;
  1955.  
  1956. ['BROWSER'].forEach(type => {
  1957. const option = document.createElement('option');
  1958. option.value = type;
  1959. option.textContent = type;
  1960. if (CONFIG.TTS.TYPE === type) {
  1961. option.selected = true;
  1962. }
  1963. typeSelect.appendChild(option);
  1964. });
  1965.  
  1966. // 声音选择
  1967. const voiceContainer = document.createElement('div');
  1968. voiceContainer.style.cssText = `
  1969. margin-top: 12px;
  1970. display: flex;
  1971. align-items: center;
  1972. `;
  1973.  
  1974. const voiceLabel = document.createElement('label');
  1975. voiceLabel.textContent = '声音: ';
  1976. voiceLabel.style.cssText = `
  1977. color: #fff;
  1978. margin-right: 10px;
  1979. font-size: 14px;
  1980. font-weight: 500;
  1981. `;
  1982.  
  1983. const voiceSelect = document.createElement('select');
  1984. voiceSelect.style.cssText = `
  1985. padding: 8px 12px;
  1986. border-radius: 4px;
  1987. background: rgba(255, 255, 255, 0.9);
  1988. color: #333;
  1989. border: 1px solid rgba(33, 150, 243, 0.3);
  1990. font-size: 14px;
  1991. cursor: pointer;
  1992. outline: none;
  1993. transition: all 0.3s ease;
  1994. width: 200px;
  1995. `;
  1996.  
  1997. // 更新声音选项的函数
  1998. const updateVoiceOptions = () => {
  1999. // 清空现有选项
  2000. while (voiceSelect.firstChild) {
  2001. voiceSelect.removeChild(voiceSelect.firstChild);
  2002. }
  2003.  
  2004. if (typeSelect.value === 'EDGE') {
  2005. Object.entries(CONFIG.TTS.EDGE.VOICES).forEach(([id, name]) => {
  2006. const option = document.createElement('option');
  2007. option.value = id;
  2008. option.textContent = name;
  2009. if (id === CONFIG.TTS.EDGE.DEFAULT_VOICE) {
  2010. option.selected = true;
  2011. }
  2012. voiceSelect.appendChild(option);
  2013. });
  2014. }
  2015.  
  2016. if (CONFIG.TTS.TYPE === 'VITS') {
  2017. const option = document.createElement('option');
  2018. option.value = CONFIG.TTS.VITS.DEFAULT_VOICE;
  2019. option.textContent = '珊瑚宫心海';
  2020. option.selected = true;
  2021. voiceSelect.appendChild(option);
  2022. }
  2023. if (CONFIG.TTS.TYPE === 'BROWSER') {
  2024. // 浏览器 TTS 模式下获取系统语音列表
  2025. const populateVoiceList = () => {
  2026. const voices = speechSynthesis.getVoices();
  2027. // 过滤只包含 Chinese 的语音
  2028. const chineseVoices = voices.filter(voice =>
  2029. voice.lang.toLowerCase().includes('zh-cn')
  2030. );
  2031.  
  2032. if (chineseVoices.length === 0) {
  2033. // 如果没有找到中文语音,添加提示选项
  2034. const option = document.createElement('option');
  2035. option.textContent = '未找到中文语音';
  2036. option.disabled = true;
  2037. voiceSelect.appendChild(option);
  2038. } else {
  2039. chineseVoices.forEach(voice => {
  2040. const option = document.createElement('option');
  2041. option.textContent = `${voice.name} (${voice.lang})`;
  2042. if (voice.default) {
  2043. option.textContent += ' — DEFAULT';
  2044. }
  2045. option.setAttribute('data-lang', voice.lang);
  2046. option.setAttribute('data-name', voice.name);
  2047. voiceSelect.appendChild(option);
  2048. });
  2049.  
  2050. // 如果有已保存的语音设置,选中对应选项
  2051. if (CONFIG.TTS.BROWSER.VOICE) {
  2052. const savedVoice = Array.from(voiceSelect.options).find(option =>
  2053. option.getAttribute('data-name') === CONFIG.TTS.BROWSER.VOICE.name &&
  2054. option.getAttribute('data-lang') === CONFIG.TTS.BROWSER.VOICE.lang
  2055. );
  2056. if (savedVoice) {
  2057. savedVoice.selected = true;
  2058. }
  2059. }
  2060. }
  2061.  
  2062. // 调试输出
  2063. console.log('可用的中文语音:', chineseVoices.map(v => ({
  2064. name: v.name,
  2065. lang: v.lang,
  2066. default: v.default
  2067. })));
  2068. };
  2069.  
  2070. // 初始填充语音列表
  2071. populateVoiceList();
  2072.  
  2073. // 监听语音列表变化
  2074. if (typeof speechSynthesis !== 'undefined' &&
  2075. speechSynthesis.onvoiceschanged !== undefined) {
  2076. speechSynthesis.onvoiceschanged = populateVoiceList;
  2077. }
  2078.  
  2079. }
  2080. };
  2081.  
  2082. // 初始化声音选项
  2083. updateVoiceOptions();
  2084.  
  2085. // 添加事件监听器
  2086. typeSelect.addEventListener('change', () => {
  2087. CONFIG.TTS.TYPE = typeSelect.value;
  2088. updateVoiceOptions();
  2089. });
  2090.  
  2091. voiceSelect.addEventListener('change', (e) => {
  2092. const selectedOption = e.target.selectedOptions[0];
  2093. if (typeSelect.value === 'BROWSER') {
  2094. // 保存选中的浏览器语音信息
  2095. CONFIG.TTS.BROWSER.VOICE = {
  2096. name: selectedOption.getAttribute('data-name'),
  2097. lang: selectedOption.getAttribute('data-lang')
  2098. };
  2099. } else if (typeSelect.value === 'EDGE') {
  2100. CONFIG.TTS.EDGE.DEFAULT_VOICE = selectedOption.value;
  2101. } else {
  2102. CONFIG.TTS.VITS.DEFAULT_VOICE = selectedOption.value;
  2103. }
  2104. });
  2105.  
  2106. // 组装面板
  2107. typeContainer.appendChild(typeLabel);
  2108. typeContainer.appendChild(typeSelect);
  2109. voiceContainer.appendChild(voiceLabel);
  2110. voiceContainer.appendChild(voiceSelect);
  2111.  
  2112. ttsPanel.appendChild(typeContainer);
  2113. ttsPanel.appendChild(voiceContainer);
  2114.  
  2115. // 添加到主内容区域
  2116. if (this.mainContent) {
  2117. this.mainContent.appendChild(ttsPanel);
  2118. }
  2119.  
  2120. // 创建 AI 模型选择面板(移到这里,只创建一次)
  2121. // this.createAIModelPanel();
  2122. }
  2123.  
  2124. // 分离 AI 模型面板创建为独立方法
  2125. createAIModelPanel() {
  2126. const aiModelPanel = document.createElement('div');
  2127. aiModelPanel.style.cssText = `
  2128. margin-top: 15px;
  2129. padding: 15px;
  2130. background: rgba(33, 150, 243, 0.1);
  2131. border-radius: 8px;
  2132. border-left: 4px solid #2196F3;
  2133. `;
  2134.  
  2135. const modelContainer = document.createElement('div');
  2136. modelContainer.style.cssText = `
  2137. display: flex;
  2138. align-items: center;
  2139. margin-bottom: 12px;
  2140. `;
  2141.  
  2142. const modelLabel = document.createElement('label');
  2143. modelLabel.textContent = 'AI 模型: ';
  2144. modelLabel.style.cssText = `
  2145. color: #fff;
  2146. margin-right: 10px;
  2147. font-size: 14px;
  2148. font-weight: 500;
  2149. `;
  2150.  
  2151. const modelSelect = document.createElement('select');
  2152. modelSelect.style.cssText = `
  2153. padding: 8px 12px;
  2154. border-radius: 4px;
  2155. background: rgba(255, 255, 255, 0.9);
  2156. color: #333;
  2157. border: 1px solid rgba(33, 150, 243, 0.3);
  2158. font-size: 14px;
  2159. cursor: pointer;
  2160. outline: none;
  2161. transition: all 0.3s ease;
  2162. width: 200px;
  2163. `;
  2164.  
  2165. // 添加可用的 AI 模型选项
  2166. Object.keys(CONFIG.AI_MODELS).forEach(model => {
  2167. if (model !== 'TYPE') {
  2168. const option = document.createElement('option');
  2169. option.value = model;
  2170. option.textContent = `${model} (${CONFIG.AI_MODELS[model].MODEL})`;
  2171. if (CONFIG.AI_MODELS.TYPE === model) {
  2172. option.selected = true;
  2173. }
  2174. modelSelect.appendChild(option);
  2175. }
  2176. });
  2177.  
  2178. // 添加事件监听器
  2179. modelSelect.addEventListener('change', () => {
  2180. CONFIG.AI_MODELS.TYPE = modelSelect.value;
  2181. this.translator.translationManager.currentModel = modelSelect.value;
  2182. this.updateStatus(`已切换至 ${modelSelect.value} 模型`, 'info');
  2183. });
  2184.  
  2185. // 添加悬停效果
  2186. modelSelect.addEventListener('mouseover', () => {
  2187. modelSelect.style.borderColor = 'rgba(33, 150, 243, 0.6)';
  2188. modelSelect.style.boxShadow = '0 0 5px rgba(33, 150, 243, 0.3)';
  2189. });
  2190.  
  2191. modelSelect.addEventListener('mouseout', () => {
  2192. modelSelect.style.borderColor = 'rgba(33, 150, 243, 0.3)';
  2193. modelSelect.style.boxShadow = 'none';
  2194. });
  2195.  
  2196. modelContainer.appendChild(modelLabel);
  2197. modelContainer.appendChild(modelSelect);
  2198. aiModelPanel.appendChild(modelContainer);
  2199.  
  2200. // 添加到主内容区域
  2201. if (this.mainContent) {
  2202. this.mainContent.appendChild(aiModelPanel);
  2203. }
  2204. }
  2205.  
  2206. createTopBar() {
  2207. const topBar = document.createElement('div');
  2208. topBar.style.cssText = `
  2209. display: flex;
  2210. justify-content: space-between;
  2211. align-items: center;
  2212. margin-bottom: 10px;
  2213. cursor: move;
  2214. padding: 5px;
  2215. `;
  2216.  
  2217. // 标题
  2218. const title = document.createElement('div');
  2219. title.textContent = 'YouTube 实时翻译';
  2220. title.style.cssText = `
  2221. font-weight: bold;
  2222. font-size: 14px;
  2223. `;
  2224.  
  2225. // 按钮容器
  2226. const buttonContainer = document.createElement('div');
  2227. buttonContainer.style.cssText = `
  2228. display: flex;
  2229. gap: 8px;
  2230. `;
  2231.  
  2232. // 折叠按钮
  2233. this.toggleButton = document.createElement('button');
  2234. this.toggleButton.textContent = '↑';
  2235. this.toggleButton.style.cssText = `
  2236. background: none;
  2237. border: none;
  2238. color: #fff;
  2239. cursor: pointer;
  2240. padding: 2px 6px;
  2241. font-size: 14px;
  2242. border-radius: 4px;
  2243. transition: background 0.2s;
  2244. `;
  2245.  
  2246. // 添加配置按钮
  2247. const configButton = document.createElement('button');
  2248. configButton.textContent = '⚙️';
  2249. configButton.style.cssText = `
  2250. background: none;
  2251. border: none;
  2252. color: #fff;
  2253. cursor: pointer;
  2254. padding: 2px 6px;
  2255. font-size: 14px;
  2256. border-radius: 4px;
  2257. transition: background 0.2s;
  2258. margin-right: 8px;
  2259. `;
  2260.  
  2261. configButton.addEventListener('click', () => this.toggleConfigPanel());
  2262.  
  2263. this.toggleButton.addEventListener('click', () => this.toggleCollapse());
  2264.  
  2265. buttonContainer.appendChild(configButton);
  2266. buttonContainer.appendChild(this.toggleButton);
  2267. topBar.appendChild(title);
  2268. topBar.appendChild(buttonContainer);
  2269.  
  2270. return topBar;
  2271. }
  2272.  
  2273.  
  2274. createConfigPanel() {
  2275. this.configPanel = document.createElement('div');
  2276. this.configPanel.style.cssText = `
  2277. position: fixed;
  2278. top: 50%;
  2279. left: 50%;
  2280. transform: translate(-50%, -50%);
  2281. width: 400px;
  2282. background: rgba(33, 33, 33, 0.95);
  2283. border-radius: 12px;
  2284. padding: 20px;
  2285. color: #fff;
  2286. display: none;
  2287. z-index: 10000;
  2288. box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
  2289. `;
  2290.  
  2291. // 添加标题和关闭按钮
  2292. const header = document.createElement('div');
  2293. header.style.cssText = `
  2294. display: flex;
  2295. justify-content: space-between;
  2296. align-items: center;
  2297. margin-bottom: 20px;
  2298. padding-bottom: 10px;
  2299. border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  2300. `;
  2301.  
  2302. const title = document.createElement('h3');
  2303. title.textContent = '配置设置';
  2304. title.style.margin = '0';
  2305.  
  2306. const closeButton = document.createElement('button');
  2307. closeButton.textContent = '×';
  2308. closeButton.style.cssText = `
  2309. background: none;
  2310. border: none;
  2311. color: #fff;
  2312. font-size: 20px;
  2313. cursor: pointer;
  2314. padding: 0 5px;
  2315. `;
  2316. closeButton.addEventListener('click', () => this.toggleConfigPanel());
  2317.  
  2318. header.appendChild(title);
  2319. header.appendChild(closeButton);
  2320. this.configPanel.appendChild(header);
  2321.  
  2322. // 创建配置选项
  2323. const configSections = [
  2324. {
  2325. title: 'AI 模型设置',
  2326. settings: [
  2327. {
  2328. type: 'select',
  2329. label: '模型类型',
  2330. key: 'AI_MODELS.TYPE',
  2331. options: ['OPENAI'],
  2332. value: CONFIG.AI_MODELS.TYPE
  2333. },
  2334. {
  2335. type: 'text',
  2336. label: 'API密钥',
  2337. key: 'AI_MODELS.OPENAI.API_KEY',
  2338. value: CONFIG.AI_MODELS.OPENAI.API_KEY
  2339. },
  2340. {
  2341. type: 'text',
  2342. label: 'API地址',
  2343. key: 'AI_MODELS.OPENAI.API_URL',
  2344. value: CONFIG.AI_MODELS.OPENAI.API_URL
  2345. },
  2346. {
  2347. type: 'text',
  2348. label: '模型名称',
  2349. key: 'AI_MODELS.OPENAI.MODEL',
  2350. value: CONFIG.AI_MODELS.OPENAI.MODEL
  2351. },
  2352. {
  2353. type: 'select',
  2354. label: '流式响应',
  2355. key: 'AI_MODELS.OPENAI.STREAM',
  2356. options: ['true', 'false'],
  2357. value: CONFIG.AI_MODELS.OPENAI.STREAM.toString()
  2358. }
  2359. ]
  2360. }
  2361. // {
  2362. // title: 'TTS 设置',
  2363. // settings: [
  2364. // {
  2365. // type: 'select',
  2366. // label: 'TTS引擎',
  2367. // key: 'TTS.TYPE',
  2368. // options: ['EDGE', 'VITS', 'BROWSER'],
  2369. // value: CONFIG.TTS.TYPE
  2370. // },
  2371. // {
  2372. // type: 'select',
  2373. // label: 'EDGE声音',
  2374. // key: 'TTS.EDGE.DEFAULT_VOICE',
  2375. // options: Object.keys(CONFIG.TTS.EDGE.VOICES),
  2376. // value: CONFIG.TTS.EDGE.DEFAULT_VOICE,
  2377. // dependsOn: {
  2378. // key: 'TTS.TYPE',
  2379. // value: 'EDGE'
  2380. // }
  2381. // },
  2382. // {
  2383. // type: 'select',
  2384. // label: 'VITS声音',
  2385. // key: 'TTS.VITS.DEFAULT_VOICE',
  2386. // options: ['珊瑚宫心海'], // 可以根据实际声音列表扩展
  2387. // value: CONFIG.TTS.VITS.DEFAULT_VOICE,
  2388. // dependsOn: {
  2389. // key: 'TTS.TYPE',
  2390. // value: 'VITS'
  2391. // }
  2392. // },
  2393. // {
  2394. // type: 'range',
  2395. // label: '语速',
  2396. // key: 'TTS.BROWSER.RATE',
  2397. // min: 0.5,
  2398. // max: 2,
  2399. // step: 0.1,
  2400. // value: CONFIG.TTS.BROWSER.RATE,
  2401. // dependsOn: {
  2402. // key: 'TTS.TYPE',
  2403. // value: 'BROWSER'
  2404. // }
  2405. // },
  2406. // {
  2407. // type: 'range',
  2408. // label: '音量',
  2409. // key: 'TTS.BROWSER.VOLUME',
  2410. // min: 0,
  2411. // max: 1,
  2412. // step: 0.1,
  2413. // value: CONFIG.TTS.BROWSER.VOLUME,
  2414. // dependsOn: {
  2415. // key: 'TTS.TYPE',
  2416. // value: 'BROWSER'
  2417. // }
  2418. // },
  2419. // {
  2420. // type: 'range',
  2421. // label: '音调',
  2422. // key: 'TTS.BROWSER.PITCH',
  2423. // min: 0.5,
  2424. // max: 2,
  2425. // step: 0.1,
  2426. // value: CONFIG.TTS.BROWSER.PITCH,
  2427. // dependsOn: {
  2428. // key: 'TTS.TYPE',
  2429. // value: 'BROWSER'
  2430. // }
  2431. // }
  2432. // ]
  2433. // },
  2434. // {
  2435. // title: '缓存设置',
  2436. // settings: [
  2437. // {
  2438. // type: 'number',
  2439. // label: '音频缓存大小',
  2440. // key: 'CACHE.AUDIO_SIZE',
  2441. // min: 100,
  2442. // max: 1000,
  2443. // value: CONFIG.CACHE.AUDIO_SIZE
  2444. // },
  2445. // {
  2446. // type: 'number',
  2447. // label: '翻译缓存大小',
  2448. // key: 'CACHE.TRANS_SIZE',
  2449. // min: 100,
  2450. // max: 1000,
  2451. // value: CONFIG.CACHE.TRANS_SIZE
  2452. // }
  2453. // ]
  2454. // }
  2455. ];
  2456.  
  2457. configSections.forEach(section => {
  2458. const sectionEl = this.createConfigSection(section);
  2459. this.configPanel.appendChild(sectionEl);
  2460. });
  2461.  
  2462. document.body.appendChild(this.configPanel);
  2463. }
  2464.  
  2465.  
  2466. // 在 updateConfig 方法中添加模型切换的处理
  2467. updateConfig(key, value) {
  2468. // 将点分隔的键转换为嵌套对象访问
  2469. const keys = key.split('.');
  2470. let current = CONFIG;
  2471. for (let i = 0; i < keys.length - 1; i++) {
  2472. current = current[keys[i]];
  2473. }
  2474.  
  2475. // 特殊处理布尔值
  2476. if (value === 'true') value = true;
  2477. if (value === 'false') value = false;
  2478.  
  2479. current[keys[keys.length - 1]] = value;
  2480. // 触发配置更新事件
  2481. document.dispatchEvent(new CustomEvent('configUpdate', {
  2482. detail: { key, value }
  2483. }));
  2484.  
  2485. // 保存配置
  2486. ConfigManager.saveConfig(CONFIG);
  2487.  
  2488. // 打印模型相关的配置变更
  2489. if (key.startsWith('AI_MODELS')) {
  2490. console.log('AI模型配置已更新:', {
  2491. 配置项: key,
  2492. 新值: value,
  2493. 当前模型类型: CONFIG.AI_MODELS.TYPE,
  2494. 模型名称: CONFIG.AI_MODELS[CONFIG.AI_MODELS.TYPE].MODEL,
  2495. 流式响应: CONFIG.AI_MODELS[CONFIG.AI_MODELS.TYPE].STREAM
  2496. });
  2497. }
  2498.  
  2499. // 通知更新 - 添加错误处理
  2500. if (this.translator && typeof this.translator.onConfigUpdate === 'function') {
  2501. this.translator.onConfigUpdate(key, value);
  2502. } else {
  2503. console.warn('翻译器未初始化或不支持配置更新');
  2504. }
  2505. }
  2506.  
  2507.  
  2508. createConfigSection(section) {
  2509. const sectionEl = document.createElement('div');
  2510. sectionEl.style.marginBottom = '20px';
  2511.  
  2512. const title = document.createElement('h4');
  2513. title.textContent = section.title;
  2514. title.style.marginBottom = '10px';
  2515. sectionEl.appendChild(title);
  2516.  
  2517. section.settings.forEach(setting => {
  2518. const settingEl = this.createConfigSetting(setting);
  2519. sectionEl.appendChild(settingEl);
  2520. });
  2521.  
  2522. return sectionEl;
  2523. }
  2524.  
  2525. createConfigSetting(setting) {
  2526. const container = document.createElement('div');
  2527. container.style.cssText = `
  2528. margin-bottom: 15px;
  2529. display: flex;
  2530. align-items: center;
  2531. gap: 10px;
  2532. `;
  2533.  
  2534. const label = document.createElement('label');
  2535. label.textContent = setting.label;
  2536. label.style.cssText = `
  2537. width: 120px;
  2538. color: #fff;
  2539. font-size: 14px;
  2540. `;
  2541.  
  2542. // 添加依赖关系处理
  2543. if (setting.dependsOn) {
  2544. const updateVisibility = () => {
  2545. const dependencyValue = this.getConfigValue(setting.dependsOn.key);
  2546. container.style.display = dependencyValue === setting.dependsOn.value ? 'flex' : 'none';
  2547. };
  2548.  
  2549. // 监听依赖项的变化
  2550. document.addEventListener('configUpdate', (e) => {
  2551. if (e.detail.key === setting.dependsOn.key) {
  2552. updateVisibility();
  2553. }
  2554. });
  2555.  
  2556. // 初始化可见性
  2557. updateVisibility();
  2558. }
  2559.  
  2560. let input;
  2561. switch (setting.type) {
  2562. case 'select':
  2563. input = document.createElement('select');
  2564. setting.options.forEach(option => {
  2565. const opt = document.createElement('option');
  2566. opt.value = option;
  2567. opt.textContent = option;
  2568. opt.selected = option === setting.value;
  2569. // 设置选项样式
  2570. opt.style.cssText = `
  2571. background: #2f2f2f;
  2572. color: #fff;
  2573. padding: 8px;
  2574. `;
  2575. input.appendChild(opt);
  2576. });
  2577. // 为select元素添加特殊样式
  2578. input.style.cssText = `
  2579. padding: 8px 12px;
  2580. border-radius: 4px;
  2581. background: #2f2f2f;
  2582. color: #fff;
  2583. border: 1px solid #4CAF50;
  2584. font-size: 14px;
  2585. cursor: pointer;
  2586. outline: none;
  2587. width: 200px;
  2588. transition: all 0.3s ease;
  2589. appearance: none;
  2590. -webkit-appearance: none;
  2591. -moz-appearance: none;
  2592. background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
  2593. background-repeat: no-repeat;
  2594. background-position: right 8px center;
  2595. background-size: 16px;
  2596. padding-right: 32px;
  2597. `;
  2598. break;
  2599.  
  2600. case 'text':
  2601. input = document.createElement('input');
  2602. input.type = 'text';
  2603. input.value = setting.value;
  2604. input.style.cssText = `
  2605. padding: 8px 12px;
  2606. border-radius: 4px;
  2607. background: #2f2f2f;
  2608. color: #fff;
  2609. border: 1px solid #4CAF50;
  2610. font-size: 14px;
  2611. width: 200px;
  2612. outline: none;
  2613. transition: all 0.3s ease;
  2614. `;
  2615. break;
  2616.  
  2617. case 'number':
  2618. input = document.createElement('input');
  2619. input.type = 'number';
  2620. input.min = setting.min;
  2621. input.max = setting.max;
  2622. input.value = setting.value;
  2623. input.style.cssText = `
  2624. padding: 8px 12px;
  2625. border-radius: 4px;
  2626. background: #2f2f2f;
  2627. color: #fff;
  2628. border: 1px solid #4CAF50;
  2629. font-size: 14px;
  2630. width: 200px;
  2631. outline: none;
  2632. transition: all 0.3s ease;
  2633. `;
  2634. break;
  2635.  
  2636. case 'range':
  2637. input = document.createElement('input');
  2638. input.type = 'range';
  2639. input.min = setting.min;
  2640. input.max = setting.max;
  2641. input.step = setting.step;
  2642. input.value = setting.value;
  2643. input.style.cssText = `
  2644. width: 200px;
  2645. height: 4px;
  2646. border-radius: 2px;
  2647. background: #4CAF50;
  2648. outline: none;
  2649. opacity: 0.7;
  2650. transition: all 0.3s ease;
  2651. -webkit-appearance: none;
  2652. `;
  2653. break;
  2654. }
  2655.  
  2656. // 添加悬停效果
  2657. if (setting.type !== 'range') {
  2658. input.addEventListener('mouseover', () => {
  2659. input.style.borderColor = '#66BB6A';
  2660. input.style.boxShadow = '0 0 5px rgba(76, 175, 80, 0.3)';
  2661. });
  2662.  
  2663. input.addEventListener('mouseout', () => {
  2664. input.style.borderColor = '#4CAF50';
  2665. input.style.boxShadow = 'none';
  2666. });
  2667.  
  2668. input.addEventListener('focus', () => {
  2669. input.style.borderColor = '#66BB6A';
  2670. input.style.boxShadow = '0 0 5px rgba(76, 175, 80, 0.3)';
  2671. });
  2672.  
  2673. input.addEventListener('blur', () => {
  2674. input.style.borderColor = '#4CAF50';
  2675. input.style.boxShadow = 'none';
  2676. });
  2677. }
  2678.  
  2679. // 为range类型添加特殊样式
  2680. if (setting.type === 'range') {
  2681. input.addEventListener('mouseover', () => {
  2682. input.style.opacity = '1';
  2683. });
  2684.  
  2685. input.addEventListener('mouseout', () => {
  2686. input.style.opacity = '0.7';
  2687. });
  2688.  
  2689. // 添加滑块样式
  2690. const styleSheet = document.createElement('style');
  2691. styleSheet.textContent = `
  2692. input[type=range]::-webkit-slider-thumb {
  2693. -webkit-appearance: none;
  2694. appearance: none;
  2695. width: 16px;
  2696. height: 16px;
  2697. border-radius: 50%;
  2698. background: #fff;
  2699. cursor: pointer;
  2700. transition: all 0.3s ease;
  2701. }
  2702. input[type=range]::-webkit-slider-thumb:hover {
  2703. background: #e0e0e0;
  2704. transform: scale(1.1);
  2705. }
  2706. `;
  2707. document.head.appendChild(styleSheet);
  2708. }
  2709.  
  2710. input.addEventListener('change', (e) => {
  2711. let value = e.target.value;
  2712. if (setting.type === 'number' || setting.type === 'range') {
  2713. value = parseFloat(value);
  2714. }
  2715. this.updateConfig(setting.key, value);
  2716. });
  2717.  
  2718. container.appendChild(label);
  2719. container.appendChild(input);
  2720.  
  2721. return container;
  2722. }
  2723.  
  2724. toggleConfigPanel() {
  2725. if (!this.configPanel) {
  2726. this.createConfigPanel();
  2727. }
  2728. const isVisible = this.configPanel.style.display === 'block';
  2729. this.configPanel.style.display = isVisible ? 'none' : 'block';
  2730. }
  2731.  
  2732.  
  2733. // 添加获取配置值的辅助方法
  2734. getConfigValue(key) {
  2735. const keys = key.split('.');
  2736. let value = CONFIG;
  2737. for (const k of keys) {
  2738. value = value[k];
  2739. }
  2740. return value;
  2741. }
  2742. createControls() {
  2743. const controls = document.createElement('div');
  2744. controls.style.cssText = `
  2745. display: flex;
  2746. gap: 10px;
  2747. margin-bottom: 15px;
  2748. `;
  2749.  
  2750. // 加载字幕按钮
  2751. this.loadSubtitlesButton = this.createButton('加载字幕', '#2196F3');
  2752.  
  2753. // 开始按钮
  2754. this.startButton = this.createButton('开始播放', '#4CAF50');
  2755. this.startButton.disabled = true;
  2756. this.startButton.style.opacity = '0.5';
  2757. this.startButton.style.cursor = 'not-allowed';
  2758.  
  2759. // 暂停按钮
  2760. this.pauseButton = this.createButton('停止播放', '#FF5722');
  2761. this.pauseButton.style.display = 'block';
  2762.  
  2763. // 新增总结按钮
  2764. this.summaryButton = this.createButton('生成总结', '#9C27B0');
  2765. this.summaryButton.style.display = 'block'; // 添加这一行
  2766.  
  2767.  
  2768. controls.appendChild(this.loadSubtitlesButton);
  2769. controls.appendChild(this.startButton);
  2770. controls.appendChild(this.pauseButton);
  2771. controls.appendChild(this.summaryButton);
  2772. return controls;
  2773. }
  2774.  
  2775.  
  2776. createSummaryPanel() {
  2777. this.summaryPanel = document.createElement('div');
  2778. this.summaryPanel.style.cssText = `
  2779. margin-top: 15px;
  2780. padding: 15px;
  2781. background: rgba(156, 39, 176, 0.1);
  2782. border-radius: 8px;
  2783. border-left: 4px solid #9C27B0;
  2784. display: none;
  2785. transition: all 0.3s ease;
  2786. `;
  2787.  
  2788. const title = document.createElement('div');
  2789. title.textContent = '视频内容总结';
  2790. title.style.cssText = `
  2791. font-weight: bold;
  2792. margin-bottom: 10px;
  2793. color: #9C27B0;
  2794. font-size: 14px;
  2795. display: flex;
  2796. justify-content: space-between;
  2797. align-items: center;
  2798. `;
  2799.  
  2800. // 添加复制按钮
  2801. const copyButton = document.createElement('button');
  2802. copyButton.textContent = '复制';
  2803. copyButton.style.cssText = `
  2804. background: #9C27B0;
  2805. color: white;
  2806. border: none;
  2807. border-radius: 4px;
  2808. padding: 4px 8px;
  2809. font-size: 12px;
  2810. cursor: pointer;
  2811. transition: all 0.2s ease;
  2812. `;
  2813.  
  2814. copyButton.addEventListener('mouseover', () => {
  2815. copyButton.style.background = '#7B1FA2';
  2816. });
  2817.  
  2818. copyButton.addEventListener('mouseout', () => {
  2819. copyButton.style.background = '#9C27B0';
  2820. });
  2821.  
  2822. copyButton.addEventListener('click', () => {
  2823. navigator.clipboard.writeText(this.summaryContent.textContent)
  2824. .then(() => {
  2825. copyButton.textContent = '已复制';
  2826. setTimeout(() => {
  2827. copyButton.textContent = '复制';
  2828. }, 2000);
  2829. })
  2830. .catch(err => console.error('复制失败:', err));
  2831. });
  2832.  
  2833. title.appendChild(copyButton);
  2834.  
  2835. this.summaryContent = document.createElement('div');
  2836. this.summaryContent.style.cssText = `
  2837. font-size: 14px;
  2838. line-height: 1.6;
  2839. color: #fff;
  2840. white-space: pre-wrap;
  2841. margin-top: 10px;
  2842. max-height: 400px;
  2843. overflow-y: auto;
  2844. padding-right: 10px;
  2845. `;
  2846.  
  2847. // 添加滚动条样式
  2848. this.summaryContent.style.cssText += `
  2849. scrollbar-width: thin;
  2850. scrollbar-color: #9C27B0 rgba(156, 39, 176, 0.1);
  2851. `;
  2852.  
  2853. this.summaryPanel.appendChild(title);
  2854. this.summaryPanel.appendChild(this.summaryContent);
  2855. this.mainContent.appendChild(this.summaryPanel);
  2856. }
  2857.  
  2858.  
  2859. // 添加字幕显示方法
  2860. updateSubtitleDisplay(subtitle) {
  2861.  
  2862. // 生成字幕唯一ID (使用时间戳和文本组合)
  2863. const subtitleId = `${subtitle.startTime}-${subtitle.text}`;
  2864.  
  2865. // 检查是否已经显示过这条字幕
  2866. if (this.lastDisplayedSubtitleId === subtitleId) {
  2867. return; // 如果是相同字幕,直接返回
  2868. }
  2869.  
  2870. const entry = document.createElement('div');
  2871. entry.style.cssText = `
  2872. margin: 10px 0;
  2873. padding: 12px;
  2874. background: rgba(255, 255, 255, 0.1);
  2875. border-radius: 8px;
  2876. border-left: 4px solid #4CAF50;
  2877. transition: all 0.3s ease;
  2878. `;
  2879.  
  2880. // 添加鼠标悬停效果
  2881. entry.addEventListener('mouseover', () => {
  2882. entry.style.background = 'rgba(255, 255, 255, 0.15)';
  2883. entry.style.transform = 'translateX(5px)';
  2884. });
  2885.  
  2886. entry.addEventListener('mouseout', () => {
  2887. entry.style.background = 'rgba(255, 255, 255, 0.1)';
  2888. entry.style.transform = 'translateX(0)';
  2889. });
  2890.  
  2891. // 显示时间信息
  2892. const timeInfo = document.createElement('div');
  2893. timeInfo.style.cssText = `
  2894. color: #888;
  2895. font-size: 11px;
  2896. margin-bottom: 8px;
  2897. font-family: monospace;
  2898. `;
  2899. timeInfo.textContent = `⏱ ${subtitle.startTime.toFixed(2)}s - ${(subtitle.startTime + subtitle.duration).toFixed(2)}s`;
  2900. entry.appendChild(timeInfo);
  2901.  
  2902. // 显示原文
  2903. const originalText = document.createElement('div');
  2904. originalText.style.cssText = `
  2905. color: #bbb;
  2906. margin: 6px 0;
  2907. font-size: 13px;
  2908. line-height: 1.4;
  2909. padding-left: 20px;
  2910. position: relative;
  2911. `;
  2912.  
  2913. // 创建图标元素
  2914. const originalIcon = document.createElement('span');
  2915. originalIcon.style.cssText = `
  2916. position: absolute;
  2917. left: 0;
  2918. `;
  2919. originalIcon.textContent = '💢';
  2920.  
  2921. // 创建文本元素
  2922. const originalTextContent = document.createElement('span');
  2923. originalTextContent.textContent = subtitle.text;
  2924.  
  2925. originalText.appendChild(originalIcon);
  2926. originalText.appendChild(originalTextContent);
  2927. entry.appendChild(originalText);
  2928.  
  2929. // 显示译文
  2930. if (subtitle.translation) {
  2931. const translatedText = document.createElement('div');
  2932. translatedText.style.cssText = `
  2933. color: #fff;
  2934. margin: 6px 0;
  2935. font-size: 14px;
  2936. line-height: 1.4;
  2937. font-weight: 500;
  2938. padding-left: 20px;
  2939. position: relative;
  2940. `;
  2941.  
  2942. // 创建译文图标元素
  2943. const translatedIcon = document.createElement('span');
  2944. translatedIcon.style.cssText = `
  2945. position: absolute;
  2946. left: 0;
  2947. `;
  2948. translatedIcon.textContent = '🤖';
  2949.  
  2950. // 创建译文文本元素
  2951. const translatedTextContent = document.createElement('span');
  2952. translatedTextContent.textContent = subtitle.translation;
  2953.  
  2954. translatedText.appendChild(translatedIcon);
  2955. translatedText.appendChild(translatedTextContent);
  2956. entry.appendChild(translatedText);
  2957. }
  2958.  
  2959. // 更新最后显示的字幕ID
  2960. this.lastDisplayedSubtitleId = subtitleId;
  2961. this.statusDisplay.appendChild(entry);
  2962. this.statusDisplay.scrollTop = this.statusDisplay.scrollHeight;
  2963. }
  2964.  
  2965. // 添加事件监听器
  2966. attachEventListeners() {
  2967. // 加载字幕按钮事件
  2968. this.loadSubtitlesButton.addEventListener('click', async () => {
  2969. this.loadSubtitlesButton.disabled = true;
  2970. this.loadSubtitlesButton.textContent = '正在加载字幕...';
  2971.  
  2972. try {
  2973. // 加载字幕
  2974. await this.translator.loadSubtitles();
  2975. this.updateStatus(`已加载 ${this.translator.subtitleManager.subtitles.length} 条字幕`, 'success');
  2976.  
  2977. // 开始翻译
  2978. this.updateStatus('正在翻译字幕...', 'info');
  2979. await this.translator.translateAllSubtitles();
  2980. this.updateStatus('字幕翻译完成', 'success');
  2981.  
  2982. // 更新UI状态
  2983. this.loadSubtitlesButton.style.display = 'none';
  2984. this.summaryButton.style.display = 'block';
  2985. this.startButton.disabled = false;
  2986. this.startButton.style.opacity = '1';
  2987. this.startButton.style.cursor = 'pointer';
  2988.  
  2989. // 显示翻译样本
  2990. // const allSubtitles = this.translator.subtitleManager.subtitles;
  2991. // if (allSubtitles) {
  2992. // allSubtitles.forEach(sub => {
  2993. // this.updateSubtitleDisplay(sub);
  2994. // });
  2995. // }
  2996. } catch (error) {
  2997. this.loadSubtitlesButton.disabled = false;
  2998. this.loadSubtitlesButton.textContent = '重试加载字幕';
  2999. this.updateStatus(`加载字幕失败: ${error.message}`, 'error');
  3000. }
  3001. });
  3002.  
  3003.  
  3004. // 开始播放按钮事件
  3005. this.startButton.addEventListener('click', async () => {
  3006. try {
  3007. this.startButton.style.display = 'none';
  3008. this.pauseButton.style.display = 'block';
  3009. this.translator.startTranslator();
  3010. this.videoController.playVideo();
  3011. //this.updateStatus('开始播放', 'success');
  3012. } catch (error) {
  3013. this.updateStatus(`播放失败: ${error.message}`, 'error');
  3014. this.startButton.style.display = 'block';
  3015. this.pauseButton.style.display = 'none';
  3016. }
  3017. });
  3018.  
  3019. // 暂停按钮事件
  3020. this.pauseButton.addEventListener('click', () => {
  3021. this.pauseButton.style.display = 'none';
  3022. this.startButton.style.display = 'block';
  3023. this.videoController.pauseVideo();
  3024. this.updateStatus('播放已暂停', 'info');
  3025. });
  3026.  
  3027. // 总结按钮事件
  3028. this.summaryButton.addEventListener('click', async () => {
  3029. try {
  3030. this.summaryButton.disabled = true;
  3031. this.summaryButton.textContent = '正在生成总结...';
  3032. this.updateStatus('正在生成视频内容总结...', 'info');
  3033.  
  3034. const summary = await this.translator.generateSummary();
  3035.  
  3036. this.summaryContent.textContent = summary;
  3037. this.summaryPanel.style.display = 'block';
  3038. this.updateStatus('总结生成完成', 'success');
  3039. } catch (error) {
  3040. this.updateStatus(`生成总结失败: ${error.message}`, 'error');
  3041. } finally {
  3042. this.summaryButton.disabled = false;
  3043. this.summaryButton.textContent = '生成总结';
  3044. }
  3045. });
  3046. }
  3047.  
  3048. createButton(text, color) {
  3049. const button = document.createElement('button');
  3050. button.textContent = text;
  3051. button.style.cssText = `
  3052. padding: 10px 20px;
  3053. border: none;
  3054. border-radius: 8px;
  3055. background: ${color};
  3056. color: white;
  3057. cursor: pointer;
  3058. font-size: 14px;
  3059. flex: 1;
  3060. transition: all 0.3s ease;
  3061. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  3062. `;
  3063.  
  3064. button.addEventListener('mouseover', () => {
  3065. button.style.transform = 'translateY(-2px)';
  3066. button.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.3)';
  3067. });
  3068.  
  3069. button.addEventListener('mouseout', () => {
  3070. button.style.transform = 'translateY(0)';
  3071. button.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.2)';
  3072. });
  3073.  
  3074. return button;
  3075. }
  3076.  
  3077. createStatusDisplay() {
  3078. this.statusDisplay = document.createElement('div');
  3079. this.statusDisplay.style.cssText = `
  3080. margin-top: 15px;
  3081. max-height: 450px;
  3082. max-width: 400px;
  3083. overflow-y: auto;
  3084. padding: 10px;
  3085. background: rgba(0, 0, 0, 0.2);
  3086. border-radius: 8px;
  3087. font-size: 14px;
  3088. line-height: 1.5;
  3089. `;
  3090. }
  3091.  
  3092. toggleCollapse() {
  3093. this.isCollapsed = !this.isCollapsed;
  3094. if (this.isCollapsed) {
  3095. this.mainContent.style.display = 'none';
  3096. this.container.style.width = '200px';
  3097. this.toggleButton.textContent = '↓';
  3098. } else {
  3099. this.mainContent.style.display = 'block';
  3100. this.container.style.width = '300px';
  3101. this.toggleButton.textContent = '↑';
  3102. }
  3103. }
  3104.  
  3105. makeDraggable(dragHandle) {
  3106. let isDragging = false;
  3107. let currentX;
  3108. let currentY;
  3109. let initialX;
  3110. let initialY;
  3111. let xOffset = 0;
  3112. let yOffset = 0;
  3113.  
  3114. dragHandle.addEventListener('mousedown', (e) => {
  3115. initialX = e.clientX - xOffset;
  3116. initialY = e.clientY - yOffset;
  3117. if (e.target === dragHandle) {
  3118. isDragging = true;
  3119. }
  3120. });
  3121.  
  3122. document.addEventListener('mousemove', (e) => {
  3123. if (isDragging) {
  3124. e.preventDefault();
  3125. currentX = e.clientX - initialX;
  3126. currentY = e.clientY - initialY;
  3127. xOffset = currentX;
  3128. yOffset = currentY;
  3129.  
  3130. const maxX = window.innerWidth - this.container.offsetWidth;
  3131. const maxY = window.innerHeight - this.container.offsetHeight;
  3132. xOffset = Math.min(Math.max(0, xOffset), maxX);
  3133. yOffset = Math.min(Math.max(0, yOffset), maxY);
  3134.  
  3135. this.container.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
  3136. }
  3137. });
  3138.  
  3139. document.addEventListener('mouseup', () => {
  3140. initialX = currentX;
  3141. initialY = currentY;
  3142. isDragging = false;
  3143. });
  3144. }
  3145.  
  3146. updateStatus(message, type = 'info') {
  3147. const entry = document.createElement('div');
  3148. entry.style.cssText = `
  3149. margin-bottom: 8px;
  3150. padding: 4px 8px;
  3151. border-radius: 4px;
  3152. font-size: 13px;
  3153. ${type === 'error' ? 'background: rgba(244, 67, 54, 0.2); color: #ff8a80;' : ''}
  3154. `;
  3155. entry.textContent = `${type === 'error' ? '❌ ' : ''}${message}`;
  3156. this.statusDisplay.appendChild(entry);
  3157. this.statusDisplay.scrollTop = this.statusDisplay.scrollHeight;
  3158. }
  3159. }
  3160.  
  3161. // 初始化应用
  3162. async function initializeApp() {
  3163. // 检查是否在YouTube账户页面
  3164. if (window.location.href.includes('accounts.youtube.com')) {
  3165. console.log('在账户页面,跳过初始化');
  3166. return;
  3167. }
  3168. const playerManager = PlayerManager.getInstance();
  3169. await playerManager.initialize();
  3170. // 启动前10秒内每秒检查一次播放状态
  3171. const player = playerManager.player;
  3172. //console.log("播放器信息: " ,player)
  3173.  
  3174. // 创建视频控制器
  3175. const videoController = new VideoController();
  3176.  
  3177. // 创建翻译器
  3178. const translator = new YouTubeTranslator();
  3179.  
  3180.  
  3181. // 创建UI管理器
  3182. const ui = new UIManager(videoController,translator);
  3183.  
  3184. // 设置 UI 管理器
  3185. translator.setUIManager(ui);
  3186.  
  3187. // 获取视频ID
  3188. const videoId = translator.getVideoId();
  3189.  
  3190. if (videoId) {
  3191. console.log('成功获取视频ID: ', videoId);
  3192. let checkCount = 0;
  3193. // 启动前10秒内每秒检查一次播放状态
  3194. const checkInterval = setInterval(() => {
  3195. if (checkCount >= 5) {
  3196. clearInterval(checkInterval);
  3197. return;
  3198. }
  3199.  
  3200. if (player && typeof player.getPlayerState === 'function' && player.getPlayerState() === 1) {
  3201. player.pauseVideo();
  3202. console.log('视频已自动暂停');
  3203. }
  3204.  
  3205. checkCount++;
  3206. }, 1000);
  3207. translator.initialize().catch(error => {
  3208. console.error('初始化失败:', error);
  3209. });
  3210. } else if (retryCount < maxRetries) {
  3211. console.log(`未获取到视频ID${retryInterval/1000}秒后重试 (${retryCount + 1}/${maxRetries})`);
  3212. retryCount++;
  3213. setTimeout(tryInitialize, retryInterval);
  3214. } else {
  3215. console.log('达到最大重试次数,初始化失败');
  3216. }
  3217.  
  3218. }
  3219.  
  3220.  
  3221. // 页面加载完成后启动应用
  3222. if (document.readyState === 'loading') {
  3223. document.addEventListener('DOMContentLoaded', initializeApp);
  3224. } else {
  3225. initializeApp();
  3226. }
  3227.  
  3228. function getUid() {
  3229. try {
  3230. // 检查是否在YouTube账户页面
  3231. if (window.location.href.includes('accounts.youtube.com')) {
  3232. return null;
  3233. }
  3234.  
  3235. // 方法1: 从URL获取
  3236. const url = window.location.href;
  3237. //console.log("当前页面URL:", url);
  3238.  
  3239. if (url.includes('youtube.com')) {
  3240. // 标准观看页面
  3241. if (url.includes('/watch')) {
  3242. const urlParams = new URLSearchParams(window.location.search);
  3243. const videoId = urlParams.get('v');
  3244. if (videoId) {
  3245. // console.log("从URL参数获取到视频ID:", videoId);
  3246. return videoId;
  3247. }
  3248. }
  3249.  
  3250. // 短视频格式
  3251. if (url.includes('/shorts/')) {
  3252. const matches = url.match(/\/shorts\/([a-zA-Z0-9_-]{11})/);
  3253. if (matches && matches[1]) {
  3254. console.log("从shorts URL获取到视频ID:", matches[1]);
  3255. return matches[1];
  3256. }
  3257. }
  3258. }
  3259.  
  3260. // 方法2: 从视频元素获取
  3261. const videoElement = document.querySelector('video');
  3262. if (videoElement) {
  3263. // 从视频源获取
  3264. const videoSrc = videoElement.src;
  3265. if (videoSrc) {
  3266. const videoIdMatch = videoSrc.match(/\/([a-zA-Z0-9_-]{11})/);
  3267. if (videoIdMatch && videoIdMatch[1]) {
  3268. console.log("从视频源获取到视频ID:", videoIdMatch[1]);
  3269. return videoIdMatch[1];
  3270. }
  3271. }
  3272.  
  3273. // 从播放器容器获取
  3274. const playerContainer = document.getElementById('movie_player') ||
  3275. document.querySelector('.html5-video-player');
  3276. if (playerContainer) {
  3277. const dataVideoId = playerContainer.getAttribute('video-id') ||
  3278. playerContainer.getAttribute('data-video-id');
  3279. if (dataVideoId) {
  3280. console.log("从播放器容器获取到视频ID:", dataVideoId);
  3281. return dataVideoId;
  3282. }
  3283. }
  3284. }
  3285.  
  3286. // 方法3: 从页面元数据获取
  3287. const ytdPlayerConfig = document.querySelector('ytd-player');
  3288. if (ytdPlayerConfig) {
  3289. const videoData = ytdPlayerConfig.getAttribute('video-id');
  3290. if (videoData) {
  3291. console.log("从ytd-player获取到视频ID:", videoData);
  3292. return videoData;
  3293. }
  3294. }
  3295.  
  3296. // 方法4: 从页面脚本数据获取
  3297. const scripts = document.getElementsByTagName('script');
  3298. for (const script of scripts) {
  3299. const content = script.textContent;
  3300. if (content && content.includes('"videoId"')) {
  3301. const match = content.match(/"videoId":\s*"([a-zA-Z0-9_-]{11})"/);
  3302. if (match && match[1]) {
  3303. console.log("从页面脚本获取到视频ID:", match[1]);
  3304. return match[1];
  3305. }
  3306. }
  3307. }
  3308.  
  3309. // 如果所有方法都失败,等待页面加载完成后重试
  3310. if (document.readyState !== 'complete') {
  3311. console.log("页面未完全加载,返回null");
  3312. return null;
  3313. }
  3314.  
  3315. throw new Error('未在当前页面找到有效的YouTube视频');
  3316. } catch (error) {
  3317. console.error('获取视频ID失败:', error);
  3318. return null;
  3319. }
  3320. }
  3321.  
  3322. })();