您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
使用 Ollama/OpenAI格式的 API/Gemini API 翻译 Bangumi 的作品简介和角色简介,支持模型切换、缓存翻译记录、术语表和提示格式选择。自动检测中文,日文,英文.
// ==UserScript== // @name Bangumi 词条信息翻译器 // @namespace http://tampermonkey.net/ // @version 1.7 // @description 使用 Ollama/OpenAI格式的 API/Gemini API 翻译 Bangumi 的作品简介和角色简介,支持模型切换、缓存翻译记录、术语表和提示格式选择。自动检测中文,日文,英文. // @author Sedoruee // @match https://bgm.tv/subject/* // @match https://bgm.tv/character/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; // 配置选项 const config = { apiType: 'ollama', // 'ollama', 'openai', 'gemini' ollamaEndpoint: 'http://185.60.44.146:2222/api/generate', openaiEndpoint: 'https://sedoruee.top/v1/chat/completions', openaiApiKey: 'sk-xxxxxxxxxxxxxxxxxxxxxx', geminiEndpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent', geminiApiKey: 'AIzaSxxxxxxxxxxxxxxxxx6KHl4', // Google Gemini API Key models: { ollama: { subject: 'qwen2.5:14b', character: 'qwen2.5:14b', }, openai: { subject: 'deepseek-ai/DeepSeek-V3', character: 'deepseek-ai/DeepSeek-V3' }, gemini: { subject: 'gemini-2.0-flash-exp', character: 'gemini-2.0-flash-exp' } }, subject: { autoTranslate: 0, useNewPromptFormat: 1 }, character: { autoTranslate: 0, useNewPromptFormat: 1, glossary: [ {"src": "可选", "dst": "可选", "info": "可选"}, {"src": "可选", "dst": "可选"}, ] } }; // 获取页面类型和ID const isSubjectPage = window.location.href.includes('/subject/'); const pageType = isSubjectPage ? 'subject' : 'character'; const id = window.location.href.match(isSubjectPage ? /subject\/(\d+)/ : /character\/(\d+)/)[1]; // 获取配置 const { autoTranslate, useNewPromptFormat, glossary } = config[pageType]; const model = config.models[config.apiType][pageType]; const detailElement = isSubjectPage ? document.getElementById('subject_summary') : document.querySelector('div.detail'); const cacheKey = `translatedText_${id}`; // 统一缓存键,不包含模型 const translatedFlagKey = `translatedFlag_${id}`; // 翻译完成标记 // 显示状态 function displayStatus(message) { console.log(message); const statusDiv = document.getElementById('translationStatus') || document.createElement('div'); statusDiv.id = 'translationStatus'; statusDiv.textContent = message; statusDiv.style.marginTop = '5px'; if (isSubjectPage) { detailElement.parentNode.insertBefore(statusDiv, detailElement.nextSibling); } else { detailElement.appendChild(statusDiv); } } // 检查文本是否包含中文 function isChinese(text) { return /[\u4e00-\u9fa5]/.test(text); } // 检查文本是否包含日文,并检查日文占比 function shouldTranslateJapanese(text) { const hiragana = [ "あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ", "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ", "ま", "み", "む", "め", "も", "や", "ゆ", "よ", "ら", "り", "る", "れ", "ろ", "わ", "を", "ん", "ぁ", "ぃ", "ぅ", "ぇ", "ぉ", "ゃ", "ゅ", "ょ", "っ", "が", "ぎ", "ぐ", "げ", "ご", "ざ", "じ", "ず", "ぜ", "ぞ", "だ", "ぢ", "づ", "で", "ど", "ば", "び", "ぶ", "べ", "ぼ", "ぱ", "ぴ", "ぷ", "ぺ", "ぽ" ]; const katakana = [ "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ヲ", "ン", "ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ", "ガ", "ギ", "グ", "ゲ", "ゴ", "ザ", "ジ", "ズ", "ゼ", "ゾ", "ダ", "ヂ", "ヅ", "デ", "ド", "バ", "ビ", "ブ", "ベ", "ボ", "パ", "ピ", "プ", "ぺ", "ポ" ]; const japaneseChars = text.match(new RegExp(`[${hiragana.join("")}${katakana.join("")}]`, 'g')) || []; return japaneseChars.length / text.length >= 0.2; } // 检查文本是否包含英文,并检查英文占比 function shouldTranslateEnglish(text) { const englishChars = text.match(/[a-zA-Z]/g) || []; return englishChars.length / text.length >= 0.60; } // 显示翻译结果 function displayTranslation(translatedText) { const translatedDiv = document.createElement('div'); translatedDiv.style.marginTop = '10px'; translatedDiv.innerHTML = `<hr><h3>翻译结果:</h3><p>${translatedText.replace(/\n/g, '<br>')}</p>`; if (isSubjectPage) { detailElement.parentNode.insertBefore(translatedDiv, document.getElementById('translationStatus').nextSibling); } else { detailElement.appendChild(translatedDiv); } } // 构建 Prompt function buildPrompt(textToTranslate) { let prompt; if (useNewPromptFormat === 1) { let glossaryText = ""; if (glossary && glossary.length > 0) { const glossaryLines = glossary.map(item => { const info = item.info ? ` #${item.info}` : ""; return `${item.src}->${item.dst}${info}`; }); glossaryText = "根据以下术语表:\n" + glossaryLines.join('\n') + "\n"; } prompt = `<|im_start|>system\n你是一个轻小说翻译模型,可以流畅通顺地以日本轻小说的风格将日文翻译成简体中文,并联系上下文正确使用人称代词,不擅自添加原文中没有的代词。<|im_end|>\n` + `<|im_start|>user\n${glossaryText}将下面的日文文本翻译成中文:${textToTranslate}<|im_end|>\n` + `<|im_start|>assistant\n`; } else { prompt = `把这段日语文本直接翻译为中文文本,不保留任何非中文语言和额外内容: "\n\n${textToTranslate}"`; } return prompt; } // 使用 Ollama API 进行翻译 function translateWithOllama(textToTranslate) { const prompt = buildPrompt(textToTranslate); const requestBody = { model, prompt, stream: false }; GM_xmlhttpRequest({ method: 'POST', url: config.ollamaEndpoint, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(requestBody), onload: function(response) { try { const responseJson = JSON.parse(response.responseText); if (responseJson.response) { const translatedText = responseJson.response; displayTranslation(translatedText); GM_setValue(cacheKey, translatedText); GM_setValue(translatedFlagKey, true); // 设置翻译完成标记 displayStatus("翻译完成"); } else { displayStatus('翻译失败: ' + (responseJson.error || 'Unexpected response format.')); } } catch (error) { displayStatus('解析JSON响应失败: ' + error); } }, onerror: function(error) { displayStatus('请求失败: ' + error); } }); } // 使用 OpenAI 格式 API 进行翻译 function translateWithOpenAI(textToTranslate) { const prompt = buildPrompt(textToTranslate); const requestBody = { model: model, messages: [ { role: "user", content: prompt } ], stream: false }; GM_xmlhttpRequest({ method: 'POST', url: config.openaiEndpoint, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.openaiApiKey}` }, data: JSON.stringify(requestBody), onload: function(response) { try { const responseJson = JSON.parse(response.responseText); if (responseJson.choices && responseJson.choices.length > 0) { const translatedText = responseJson.choices[0].message.content; displayTranslation(translatedText); GM_setValue(cacheKey, translatedText); GM_setValue(translatedFlagKey, true); // 设置翻译完成标记 displayStatus("翻译完成"); } else { displayStatus('翻译失败: ' + (responseJson.error || 'Unexpected response format.')); } } catch (error) { displayStatus('解析JSON响应失败: ' + error); } }, onerror: function(error) { displayStatus('请求失败: ' + error); } }); } // 使用 Gemini API 进行翻译 function translateWithGemini(textToTranslate) { const prompt = buildPrompt(textToTranslate); const requestBody = { contents: [ { parts: [ { text: prompt } ] } ] }; GM_xmlhttpRequest({ method: 'POST', url: config.geminiEndpoint + "?key=" + config.geminiApiKey, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(requestBody), onload: function(response) { try { const responseJson = JSON.parse(response.responseText); if (responseJson.candidates && responseJson.candidates.length > 0) { const translatedText = responseJson.candidates[0].content.parts[0].text; displayTranslation(translatedText); GM_setValue(cacheKey, translatedText); GM_setValue(translatedFlagKey, true); // 设置翻译完成标记 displayStatus("翻译完成"); } else { displayStatus('翻译失败: ' + (responseJson.error || 'Unexpected response format.')); } } catch (error) { displayStatus('解析JSON响应失败: ' + error); } }, onerror: function(error) { displayStatus('请求失败: ' + error); } }); } // 执行翻译 function translate() { try { const textToTranslate = detailElement.innerText; displayStatus("翻译中..."); switch (config.apiType) { case 'ollama': translateWithOllama(textToTranslate); break; case 'openai': translateWithOpenAI(textToTranslate); break; case 'gemini': translateWithGemini(textToTranslate) break; default: displayStatus('未知的API类型'); } } catch (error) { displayStatus('翻译过程中发生错误: ' + error); } } displayStatus("正在检测..."); // 检查是否已经翻译过 const hasTranslated = GM_getValue(translatedFlagKey, false); if (hasTranslated) { displayStatus("已经翻译过,跳过..."); const cachedData = GM_getValue(cacheKey); if (cachedData) { displayStatus("正在调用缓存..."); displayTranslation(cachedData); displayStatus("调用缓存完成"); } } else { if (shouldTranslateJapanese(detailElement.innerText) || shouldTranslateEnglish(detailElement.innerText)) { const cachedData = GM_getValue(cacheKey); if (cachedData) { displayStatus("正在调用缓存..."); displayTranslation(cachedData); displayStatus("调用缓存完成"); GM_setValue(translatedFlagKey, true); // 标记为已翻译,即使使用了缓存 } else { displayStatus("未找到缓存,开始翻译..."); translate(); } } else { displayStatus("检测到中文"); } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址