Bangumi 词条信息翻译器

使用 Ollama/OpenAI格式的 API/Gemini API 翻译 Bangumi 的作品简介和角色简介,支持模型切换、缓存翻译记录、术语表和提示格式选择。自动检测中文,日文,英文.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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("检测到中文");
        }
    }

})();