Greasy Fork

Greasy Fork is available in English.

NovelAI图像生成汉化

NovelAI图像生成的简体中文汉化脚本

当前为 2025-07-22 提交的版本,查看 最新版本

// ==UserScript==
// @name         NovelAI图像生成汉化
// @namespace    https://github.com/qiqi20020612/NovelAI-zh_CN
// @version      3.4
// @description  NovelAI图像生成的简体中文汉化脚本
// @author       Z某ZMou
// @match        https://novelai.net/image
// @match        https://novelai.net/inspect
// @icon         https://novelai.net/icons/novelai-round.png
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @license      GPL-3.0-or-later
// ==/UserScript==

(function () {
    'use strict';

    // 获取用户设置,如果未设置则默认为启用标题修改
    var isTitleModificationEnabled = true; // 先设置一个默认值
    if (typeof GM_getValue === 'function') { // 检查 GM_getValue 是否存在且是一个函数
        isTitleModificationEnabled = GM_getValue('isTitleModificationEnabled', true);
    }

    // 设置页面语言为中文
    document.documentElement.lang = 'zh-CN';

    // 文本替换映射表
    const translationMap = {
        // Osano
        'This website utilizes technologies such as cookies to enable essential site functionality, as well as for analytics, personalization, and targeted advertising.': '本网站使用 Cookie 等技术来实现网站的基本功能,以及进行分析、个性化和有针对性的广告。',
        'To learn more, view the following link:': '要了解更多信息,请查看以下链接:',
        'Manage Preferences': '管理偏好',
        // 'Storage Preferences': '存储偏好',
        'When you visit websites, they may store or retrieve data about you using cookies and similar technologies ': '当您访问网站时,网站可能会使用 Cookie 和类似技术',
        '. Cookies may be necessary for the basic functionality of the website as well as other purposes. You have the option of disabling certain types of cookies, though doing so may impact your experience on the website.': '存储或检索有关您的数据。网站的基本功能和其他目的可能都需要 Cookie。您可以选择禁用某些类型的 Cookie,但这样做可能会影响您在网站上的体验。',
        'Essential': '必要',
        'Required to enable basic website functionality. You may not disable essential cookies.': '为实现网站基本功能所必需。您不得禁用基本 Cookie。',
        'View Cookies': '查看 Cookie',
        'Targeted Advertising': '有针对性的广告',
        'Used to deliver advertising that is more relevant to you and your interests. May also be used to limit the number of times you see an advertisement and measure the effectiveness of advertising campaigns.  Advertising networks usually place them with the website operator’s permission.': '用于提供与您和您的兴趣更相关的广告。也可用于限制您看到广告的次数和衡量广告活动的效果。广告网络通常在征得网站运营者的同意后投放广告。',
        'Personalization': '个性化',
        'Allow the website to remember choices you make ': '允许网站记住您所做的选择',
        'such as your username, language, or the region you are in': '如您的用户名、语言或您所在的地区',
        ' and provide enhanced, more personal features. For example, a website may provide you with local weather reports or traffic news by storing data about your general location.': ',并提供更强、更个性化的功能。例如,网站可以通过存储有关您的大致位置的数据,为您提供当地天气预报或交通新闻。',
        'Analytics': '分析',
        'Help the website operator understand how its website performs, how visitors interact with the site, and whether there may be technical issues.': '帮助网站运营商了解其网站的运行情况、访客与网站的互动情况以及是否存在技术问题。',

        // 欢迎弹窗
        'Welcome to NovelAI!': '欢迎来到 NovelAI!',
        'Your ': '你的',
        ' includes:': '包括:',
        '30 free high quality image generations': '免费生成30张高质量图像',
        'Full access to our latest model': '我们最新模型的完全访问权限',
        'No credit card required': '无需信用卡',
        'Start Free Trial': '开始免费体验',
        'Create an account and start for free': '创建一个账号开始免费体验',
        'Sign Up': '注册',
        'Log In': '登录',

        // 注册登录
        'Welcome back!': '欢迎回来!',
        'Ready to create your first masterpiece': '准备好开始创作您的第一幅杰作了吗',
        'Register for free and unlock ': '免费注册并解锁',
        '30 high quality': '30张高质量',
        ' generations.': '图像生成。',
        'Already have an account': '已经有账号了吗',
        'Email': '电子邮件',
        'Password': '密码',
        'Remember me': '记住我',
        'Login is remembered for 30 days': '保持登录30天',
        'Login will not be remembered': '登录不会被记住',
        'Repeat Password': '确认密码',
        'Would you like to receive emails about updates from us in the future': '您希望今后收到有关我们更新信息的电子邮件吗',
        // 'Sign In': '登录',
        // 'Start Creating!': '开始创作!',
        'Reset Password': '忘记密码',
        'NOTE:': '注意:',
        'Please take good care of your login credentials. Due to local encryption, losing your email or password results in permanently losing access to your remotely stored stories.': '请妥善保管您的登录凭证。由于数据在本地加密保存,丢失电子邮件或密码将导致永久无法访问远程存储的故事。',
        'By registering, you agree to consent to the NovelAI ': '注册即表示您同意 NovelAI ',
        'Privacy Policy and Terms of Service': '隐私政策和服务条款',
        'This site is protected by reCAPTCHA and the Google': '本网站受 reCAPTCHA 保护, Google ',
        'Privacy Policy': '隐私政策',
        'Terms of Service': '服务条款',
        'apply.': '适用。',

        // 历史记录
        'History': '历史记录',
        'Click on an image to set your settings to the ones used to generate it': '左键点击图像可以快速应用生成该图像时的原始设置',
        'except for any init image': '种子和原始图像不会被复制',
        'Delete this image': '确定要删除这张图片吗',
        'Delete it': '确认删除',
        'No, keep it': '不,取消',
        'Download ZIP': '打包下载全部图片',
        'Download all images? This could take a while, or fail entirely, with large numbers of images.': '确实要下载所有图像吗?如果图像数量较多,可能会需要一些时间,或导致下载失败。',
        'Downloading': '正在下载',
        'images...': '张图片...',
        'Images downloaded': '图片已开始下载',

        // 个人菜单
        'Author': '作者',
        'Anonymous Trial': '游客试用',
        'Not Subscribed': '未订阅',
        'Account': '账号',
        'Account Settings': '账号设置',
        'End Session': '结束会话',
        'Logout': '退出',
        'Other': '其它',
        'Quick Start Gallery': '快速上手图库',
        'Director Tools': '导演工具',
        'Text Generation': '文本生成',
        'Tokenizer': '分词器',
        'Help': '帮助',
        'Tutorial': '教程',
        'Documentation': '文档',
        'Version ': '版本 ',

        // 快速上手图库
        'Get Started': '让我们开始吧',
        'Get Inspiration from our quick start gallery!': '从我们的快速上手图库中获取灵感!',
        'Click an image to copy the prompt.': '点击图片以复制提示词。',
        'Copied!': '已复制!',

        // 模型选择
        'New': '新增',
        'A version of our newest model trained on a curated subset of images. Recommended for streaming.': '我们最新模型的一个版本,该模型是在经过精心挑选的图像集上进行训练的。建议用于流式传输。',
        'Our newest and best model.': '我们最新、最好的模型。',
        'Legacy': '旧版',
        'Our V4 model trained on a curated subset of images. No longer recommended for use.': '我们V4模型的一个版本,该模型是在经过精心挑选的图像集上进行训练的。不再推荐使用。',
        'Our V4 model. No longer recommended for use.': '我们的V4模型。不再推荐使用。',
        'Our previous model. No longer recommended for use.': '我们以前的模型。不再推荐使用。',
        'One of our older models. No longer recommended for use.': '我们以前的模型之一。不再推荐使用。',

        // 提示词
        'Prompt': '提示词',
        'You are currently using Anime mode.': '您当前使用的是动漫模式。',
        'The mode changes the tag suggestions and adds a dataset tag to the prompt. You can click the icon to switch.': '切换模式会更改Tag建议,并在提示中添加数据集Tag。您可以点击图标进行切换。',
        'You are currently using Furry mode.': '您当前使用的是Furry模式。',
        // 'or': '或者',
        'Randomize': '随机生成',
        'Random Prompt': '随机提示词',
        'Quality Tags Enabled': '已启用质量优化',
        'Added to the end of the prompt:': '会在输入的提示词结尾添加以下提示词:',
        'Undesired Content': '负面提示词',
        'Reattach Undesired Content': '拼接负面提示词',
        'Detach Undesired Content': '拆分负面提示词',
        'UC Preset Enabled': '已启用负面提示词预设',
        'Added to the beginning of the UC:': '会在输入的负面提示词前面添加以下提示词:',
        'This prompt is using ': '输入的部分占',
        ' of the currently used ': '个Token,共使用了',
        ' tokens. Max total tokens: ': '个。该部分最大Token数:',
        'Prompt Settings': '提示词设置',
        'Add Quality Tags': '添加质量优化Tag',
        'The prompt will be used unmodified.': '不会修改提示词。',
        'Tags to increase quality will be prepended to the prompt.': '会在提示词中添加能够提高质量的Tag。',
        'Undesired Content Preset': '负面提示词预设',
        'Heavy': '全面',
        // 全面档负面提示词:nsfw, lowres, {bad}, error, fewer, extra, missing, worst quality, jpeg artifacts, bad quality, watermark, unfinished, displeasing, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract]
        'Light': '精简',
        // 精简档负面提示词:nsfw, lowres, jpeg artifacts, worst quality, watermark, blurry, very displeasing
        'Human Focus': '以人为本',
        // 聚焦角色档负面提示词:nsfw, lowres, {bad}, error, fewer, extra, missing, worst quality, jpeg artifacts, bad quality, watermark, unfinished, displeasing, chromatic aberration, signature, extra digits, artistic error, username, scan, [abstract], bad anatomy, bad hands, @_@, mismatched pupils, heart-shaped pupils, glowing eyes
        'None': '无',
        // 即使选择无,也会添加负面提示词:lowres
        'Disable Tag Suggestions': '禁用Tag建议',
        'Highlight Emphasis': '高亮强调部分',

        // 图生图
        'Add a Base Img': '上传基准图片',
        'Optional': '可选',
        'What do you want to do with this image': '您想要怎样处理这张图片',
        'Image2Image': '图生图',
        'Transform your image.': '改变您的图片。',
        'Strength': '强度',
        'Noise': '噪声',
        'Inpaint Image': '重绘图像',
        'This image has metadata': '这张图片带有元数据',
        'Did you want to import that instead': '您想要怎样导入它',
        'Characters': '角色',
        'Append': '附加',
        'Settings': '设置',
        'Import Metadata': '导入元数据',
        'Clean Imports': '清除增益',
        'Remove': '移除',
        ', add spaces after commas': ',在逗号后添加空格',

        // 角色提示词
        'Character Prompts': '角色提示词',
        'Customize separate characters.': '自定义每个角色。',
        'Click to edit a character.': '点击以编辑一个角色。',
        'Clear Random Prompt Characters': '清除随机提示词角色',
        'Add Character': '添加角色',
        'Female': '女性',
        'Male': '男性',
        'Character': '角色',
        'Position': '位置',
        'AI’s Choice': '由AI决定',
        'Character Positions': '角色位置',
        'Global': '全局',
        'Set Character ': '调整角色',
        '’s Position': '的位置',
        'Adjust': '调整',
        'Done': '完成',

        // 其他工具
        'Vibe Transfer': '氛围转移',
        'Change the image, keep the vision.': '改变图像,保留视觉。',
        'Normalize Reference Strength Values': '标准化参考强度值',
        'Imported': '已导入',
        'Vibe Transfer reference image': '张氛围转移参考图片',
        'Enable/Disable Vibe Transfer Reference Image': '启用/禁用氛围转移参考图片',
        'Reference Strength': '参考强度',
        'Information Extracted': '信息提取度',
        'Encoding required. This will cost': '需要编码。这将使下一次生成的成本增加',
        'on the next generation.': '。',
        'Learn more here.': '点击此处了解更多信息。',

        // 图像设置
        'Image Settings': '图像设置',
        'Normal': '中等尺寸',
        'Large': '大型尺寸',
        'Wallpaper': '壁纸(特大尺寸)',
        'Small': '小型尺寸',
        'Custom': '自定义',
        'Portrait': '竖向',
        'Landscape': '横向',
        'Square': '方形',
        'Number of Images': '生成数量',

        // AI设置
        'AI Settings': 'AI设置',
        'Reset Settings': '重置设置',
        'Reset all settings to default': '确实要恢复所有设置为默认状态吗',
        'Yes': '确定',
        'Cancel': '取消',
        'Steps': '步数',
        'Guidance': '引导值',
        'Prompt Guidance': '提示词引导值',
        'Variety': '多样性',
        'Enable guidance only after body has been formed, to improve diversity and saturation of samples. May reduce relevance.': '只应在身体形成后再启用引导功能,可以提高样本的多样性和饱和度。可能会降低相关性。',
        'Seed': '种子',
        // 'Enter a seed': '输入一个种子',
        'Sampler': '采样器',
        'Recommended': '推荐',
        'Advanced Settings': '高级设置',
        'Prompt Guidance Rescale': '缩放提示词引导值',
        'Noise Schedule': '噪声计划表',
        'N/A': '随机',

        // 生成按钮
        'Free Trial': '免费体验',
        'image generations remaining.': '张图片可生成。',
        'Seen enough': '没用够吗',
        'Check out our plans': '查看我们的计划',
        'Generate 1 Image': '生成1张图像',
        'Generate 2 Images': '生成2张图像',
        'Generate 3 Images': '生成3张图像',
        'Generate 4 Images': '生成4张图像',
    };

    // 创建一个高效的正则表达式,一次性匹配所有需要翻译的词
    // 使用 Object.keys 获取所有要翻译的英文原文
    // 用 | 连接成一个大的 "或" 逻辑的正则

    // 1. 获取所有键并按长度从长到短排序,避免"Account"优先于"Account Settings"被匹配
    // const sortedKeys = Object.keys(translationMap).sort((a, b) => b.length - a.length);

    // 2. 将每个键视为一个独立的单词,用单词边界 \b 包裹
    //    注意:这里我们不能简单地在外面套一层 \b,因为有些键本身可能包含非单词字符(如 'Version ')
    //    更稳妥的方式是为每个“干净”的键添加边界。但为了简单起见,我們先用一个通用方法。
    //    一个更健壮的通用方法是把所有键用 | 连接后,再用括号包起来,然后在这个整体前后加 \b。
    //
    //    注意:在字符串中表示反斜杠 \ 需要写成 \\

    // const regex = new RegExp('\\b(' + sortedKeys.join('|') + ')\\b', 'g');
    const regex = new RegExp(Object.keys(translationMap).sort((a, b) => b.length - a.length).join('|'), 'g');

    // 替换函数
    function replaceText(node) {
        // 只检查文本节点,并且内容不能是纯空格
        if (node.nodeType !== Node.TEXT_NODE || !node.nodeValue.trim()) {
            return;
        }

        // 使用一个正则表达式一次性替换所有匹配到的词
        // replace函数的第二个参数可以是函数,它会对每个匹配项调用这个函数
        // 'match' 就是匹配到的英文原文(比如 'Account')
        let replacedValue = node.nodeValue.replace(regex, (match) => translationMap[match]);

        // 只有在内容真的发生改变时才去修改 DOM,避免不必要的重绘
        if (node.nodeValue !== replacedValue) {
            node.nodeValue = replacedValue;
        }
    }

    // 遍历函数
    function traverseAndReplace(node) {
        // 创建一个TreeWalker,它能高效地只遍历文本节点!
        const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
        let currentNode;
        while (currentNode = walker.nextNode()) {
            replaceText(currentNode);
        }
    }

    // 修改网页标题函数 (保持不变)
    function modifyPageTitle() {
        if (!isTitleModificationEnabled) {
            return;
        }
        const currentPageUrl = window.location.href;
        if (currentPageUrl.includes('inspect')) {
            document.title = '检视图像参数 - NovelAI';
        } else {
            var originalTitle = document.title;
            document.title = '图像生成 - NovelAI';
            Object.defineProperty(document, 'title', {
                get: function () {
                    return originalTitle;
                },
                set: function (value) {
                    if (value !== originalTitle) {
                        console.log('已拦截标题修改:', value);
                    }
                }
            });
        }
    }

    // 菜单相关函数
    function updateMenuText() {
        // 只有在GM_registerMenuCommand和相关API可用时才执行菜单逻辑
        if (typeof GM_registerMenuCommand !== 'function' || typeof GM_unregisterMenuCommand !== 'function' || typeof GM_setValue !== 'function') {
            console.log('GM API not available, skipping menu command registration.'); // 在控制台打印一条信息,方便调试
            return; // 直接退出函数
        }

        var menuText = isTitleModificationEnabled ? '禁用标题修改' : '启用标题修改';
        // 为了避免重复注册,先清空旧的(虽然在这个脚本里影响不大,但是好习惯)
        if (window.myMenuCommandId) {
            GM_unregisterMenuCommand(window.myMenuCommandId);
        }
        window.myMenuCommandId = GM_registerMenuCommand(menuText, function () {
            isTitleModificationEnabled = !isTitleModificationEnabled;
            GM_setValue('isTitleModificationEnabled', isTitleModificationEnabled);
            alert('标题修改功能' + (isTitleModificationEnabled ? '已启用' : '已禁用') + ',现在页面标题' + (isTitleModificationEnabled ? '无法' : '能够') + '反映图像生成的进度。\n提示:刷新前请确保生成的图像、参数等已经被保存!');
            updateMenuText();
        });
    }

    let observer = null; // 将 observer 声明在外面,方便管理

    // 核心翻译函数,保持不变
    function translateNode(node) {
        // 使用 TreeWalker 高效遍历所有文本节点
        const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
        let currentNode;
        while (currentNode = walker.nextNode()) {
            replaceText(currentNode);
        }
    }

    // 启动翻译和监视
    function startTranslation() {
        console.log("关键元素已找到,开始首次全局翻译并启动 MutationObserver。");

        // 1. 对整个页面进行一次完整的初始翻译
        translateNode(document.body);
        modifyPageTitle(); // 标题修改也在这里执行

        // 2. 创建并启动 MutationObserver 来监视后续变化
        observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach(node => {
                        // 只处理元素节点,忽略纯文本节点等
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            translateNode(node);
                        }
                    });
                }
                // 有些变化可能不是添加节点,而是改变了现有节点的文本内容
                // 虽然这种情况较少见,但为了保险起见,可以处理 characterData 变化
                if (mutation.type === 'characterData' && mutation.target.parentNode) {
                    // 只需翻译受影响的文本节点的父元素即可
                    translateNode(mutation.target.parentNode);
                }
            }
        });

        // 监视整个 body 的子节点、后代树和文本内容的变化
        observer.observe(document.body, {
            childList: true,
            subtree: true,
            characterData: true // 增加对文本内容变化的监视
        });
    }

    // 侦察兵函数:轮询检查关键元素是否存在
    function waitForElement(selector, callback) {
        const element = document.querySelector(selector);
        if (element) {
            // 元素已存在,执行回调函数
            callback();
        } else {
            // 元素不存在,设置一个定时器,稍后重试
            const intervalId = setInterval(() => {
                const element = document.querySelector(selector);
                if (element) {
                    // 找到了!清除定时器并执行回调
                    clearInterval(intervalId);
                    callback();
                }
            }, 200); // 每200毫秒检查一次

            // 设置一个超时,以防页面永远加载不出来,避免无限轮询
            setTimeout(() => {
                clearInterval(intervalId);
                console.log(`等待关键元素 "${selector}" 超时,脚本可能无法正常工作。`);
            }, 15000); // 15秒后超时
        }
    }

    // 脚本入口
    function init() {
        updateMenuText(); // 菜单设置可以立即执行

        // 我们等待一个明确的、由React渲染出来的元素出现
        // 比如包含 "Prompt" 输入框的容器。经过检查,它的父容器有一个 `data-testid="prompt-input"` 属性。
        // 这是一个非常稳定和理想的目标!
        // 如果这个选择器失效,可以换成 `textarea[placeholder*="your prompt"]` 等
        const keyElementSelector = '.prompt-input-box-prompt';

        waitForElement(keyElementSelector, startTranslation);
    }

    // 页面加载后运行脚本入口
    init();

})(); // 脚本IIFE的结束括号