DeepDanbooru 魔法串生成器

基于DeepDanbooru生成nai魔法串

当前为 2025-06-12 提交的版本,查看 最新版本

// ==UserScript==
// @name         DeepDanbooru 魔法串生成器
// @namespace    http://tampermonkey.net/
// @version      1.04
// @description  基于DeepDanbooru生成nai魔法串
// @author       a1606
// @license      MIT
// @match        http://dev.kanotype.net:8003/deepdanbooru/view/general/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @connect      translation.googleapis.com
// @connect      translate.googleapis.com
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    const STATE = {
        roles: [],
        presets: [],
        artists: [],
        ignore: [],
        selectedRoles: new Set(),
        selectedPresets: new Set(),
        selectedArtists: new Set(),
        selectedTags: new Set(),
        googleApiKey: ''
    };

    async function loadStorage() {
        STATE.roles = await GM_getValue('custom_roles', []);
        STATE.presets = await GM_getValue('custom_presets', []);
        STATE.artists = await GM_getValue('custom_artists', []);
        STATE.ignore = await GM_getValue('ignored_tags', []);
        STATE.selectedRoles = new Set(await GM_getValue('selected_roles', []));
        STATE.selectedPresets = new Set(await GM_getValue('selected_presets', []));
        STATE.selectedArtists = new Set(await GM_getValue('selected_artists', []));
        STATE.selectedTags = new Set(await GM_getValue('selected_tags', []));
        STATE.googleApiKey = await GM_getValue('google_api_key', '');
    }

    async function saveStorage() {
        await GM_setValue('custom_roles', STATE.roles);
        await GM_setValue('custom_presets', STATE.presets);
        await GM_setValue('custom_artists', STATE.artists);
        await GM_setValue('ignored_tags', STATE.ignore);
        await GM_setValue('selected_roles', [...STATE.selectedRoles]);
        await GM_setValue('selected_presets', [...STATE.selectedPresets]);
        await GM_setValue('selected_artists', [...STATE.selectedArtists]);
        await GM_setValue('selected_tags', [...STATE.selectedTags]);
        await GM_setValue('google_api_key', STATE.googleApiKey);
    }

    function splitNoteAndContent(item) {
        const idx = item.indexOf('::');
        return idx !== -1 ? [item.slice(0, idx), item.slice(idx + 2)] : [null, item];
    }

    function createButtonBar(tagTable) {
        const wrapper = document.createElement('div');
        wrapper.style.margin = '10px 0';

        const copyBtn = document.createElement('button');
        copyBtn.textContent = '🧙 生成魔法串';
        copyBtn.style.marginRight = '10px';

        const settingBtn = document.createElement('button');
        settingBtn.textContent = '⚙️ 设置';

        const translateBtn = document.createElement('button');
        translateBtn.textContent = '🌐翻译Tag';
        translateBtn.style.marginLeft = '10px';
        translateBtn.onclick = () => translateTagsGoogle();

        copyBtn.onclick = () => generateMagic(tagTable);
        settingBtn.onclick = openSettingsPanel;

        wrapper.append(copyBtn, settingBtn, translateBtn);
        tagTable.parentNode.insertBefore(wrapper, tagTable);

        const rows = tagTable.querySelectorAll('tr');
        rows.forEach(row => {
            const cells = row.querySelectorAll('td');
            if (cells.length >= 2) {
                const tag = cells[0].textContent.trim();
                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.checked = true;
                checkbox.style.marginRight = '6px';
                cells[0].prepend(checkbox);
                checkbox.dataset.tag = tag;

                const groupTitle = row.closest('tbody')?.previousElementSibling?.textContent?.trim();
                if (groupTitle === 'Character Tags' || groupTitle === 'System Tags') {
                    checkbox.checked = false;
                }

                for (const entry of STATE.ignore) {
                    const [, content] = splitNoteAndContent(entry);
                    if (tag === content && STATE.selectedTags.has(entry)) {
                        checkbox.checked = false;
                        break;
                    }
                }
            }
        });
    }

    function translateTagsGoogle() {
        const rows = document.querySelectorAll('table tr');
        const tagPairs = [];
        const texts = [];

        rows.forEach(row => {
            const cells = row.querySelectorAll('td');
            if (cells.length >= 2) {
                const tag = cells[0].textContent.trim();
                if (!tag.includes(':') && !row.querySelector('.tag-zh')) {
                    const readable = tag.replace(/_/g, ' ');
                    tagPairs.push({ tag, readable, cell: cells[0] });
                    texts.push(readable);
                }
            }
        });

        if (texts.length === 0) return;

        // 备用免费接口
        function fallbackTranslate() {
            const batchSize = 40;
            let completed = 0;
            let allTranslations = [];
            function handleBatch(start) {
                const batch = texts.slice(start, start + batchSize);
                if (batch.length === 0) {
                    if (allTranslations.length !== tagPairs.length) {
                        alert('备用翻译结果数量不一致');
                        return;
                    }
                    tagPairs.forEach((pair, i) => {
                        const el = document.createElement('span');
                        el.className = 'tag-zh';
                        el.textContent = `(${allTranslations[i]})`;
                        el.style.marginLeft = '6px';
                        el.style.color = '#888';
                        el.style.fontSize = '0.9em';
                        pair.cell.appendChild(el);
                    });
                    return;
                }
                GM_xmlhttpRequest({
                    method: "GET",
                    url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=zh-CN&dt=t&q=${encodeURIComponent(batch.join('\n'))}`,
                    onload: res => {
                        try {
                            const json = JSON.parse(res.responseText);
                            const translations = json[0].map(item => item[0].replace(/\\n/g, ''));
                            allTranslations = allTranslations.concat(translations);
                            handleBatch(start + batchSize);
                        } catch (e) {
                            alert('备用Google翻译失败:' + e.message);
                        }
                    },
                    onerror: () => alert('无法连接备用Google翻译接口,请检查网络环境')
                });
            }
            handleBatch(0);
        }

        if (!STATE.googleApiKey) {
            fallbackTranslate();
            return;
        }

        GM_xmlhttpRequest({
            method: "POST",
            url: `https://translation.googleapis.com/language/translate/v2?key=${STATE.googleApiKey}`,
            headers: { "Content-Type": "application/json" },
            data: JSON.stringify({
                q: texts,
                source: "en",
                target: "zh-CN",
                format: "text"
            }),
            onload: res => {
                try {
                    const json = JSON.parse(res.responseText);
                    if (json.error) throw new Error(json.error.message);

                    const translations = json.data?.translations;
                    if (!Array.isArray(translations) || translations.length !== tagPairs.length) {
                        throw new Error("翻译结果数量不一致");
                    }
                    tagPairs.forEach((pair, i) => {
                        const el = document.createElement('span');
                        el.className = 'tag-zh';
                        el.textContent = `(${translations[i].translatedText})`;
                        el.style.marginLeft = '6px';
                        el.style.color = '#888';
                        el.style.fontSize = '0.9em';
                        pair.cell.appendChild(el);
                    });
                } catch (e) {
                    // 主接口出错,降级到免费接口
                    fallbackTranslate();
                }
            },
            onerror: () => {
                fallbackTranslate();
            }
        });
    }

    function shouldIgnore(tag) {
        for (const entry of STATE.ignore) {
            const [, content] = splitNoteAndContent(entry);
            if (tag === content && STATE.selectedTags.has(entry)) {
                return true;
            }
        }
        return false;
    }

    function generateMagic(tagTable) {
        const rows = tagTable.querySelectorAll('tr');
        const tagList = [];

        rows.forEach(row => {
            const cells = row.querySelectorAll('td');
            if (cells.length >= 2) {
                const tag = cells[0].textContent.trim();
                const pureTag = tag.replace(/(.*?)/g, '').trim();
                const checkbox = cells[0].querySelector('input[type="checkbox"]');
                const score = parseFloat(cells[1].textContent.trim()).toFixed(2);
                if (
                    checkbox?.checked &&
                    !pureTag.includes(':') &&
                    !shouldIgnore(pureTag)
                ) {
                    tagList.push(`${pureTag}:${score}`);
                }
            }
        });

        const parts = [];
        if (STATE.selectedRoles.size > 0)
            parts.push([...STATE.selectedRoles].map(e => splitNoteAndContent(e)[1]).join(' '));

        if (STATE.selectedPresets.size > 0)
            parts.push([...STATE.selectedPresets].map(e => splitNoteAndContent(e)[1]).join(' '));

        let tagStr = tagList.join(', ');
        if (tagStr.length > 0 && !tagStr.endsWith(',')) tagStr += ',';
        parts.push(tagStr);

        if (STATE.selectedArtists.size > 0)
            parts.push([...STATE.selectedArtists].map(e => splitNoteAndContent(e)[1]).join(' '));

        const final = parts.join('\n');
        GM_setClipboard(final);
        alert('✅ 魔法串已复制!');
    }

    function enableDragSorting(wrapper, list, selectedSet, isTextOnly, key) {
        let draggingElem;

        wrapper.addEventListener('dragstart', e => {
            if (e.target.classList.contains('draggable-item')) {
                draggingElem = e.target;
                e.dataTransfer.effectAllowed = 'move';
            }
        });

        wrapper.addEventListener('dragover', e => {
            e.preventDefault();
            const target = e.target.closest('.draggable-item');
            if (target && target !== draggingElem) {
                const rect = target.getBoundingClientRect();
                const next = (e.clientY - rect.top) > rect.height / 2;
                target.parentNode.insertBefore(draggingElem, next ? target.nextSibling : target);
            }
        });

        wrapper.addEventListener('drop', async () => {
            const items = [...wrapper.querySelectorAll('.draggable-item')].map(row => {
                const span = row.querySelector('span');
                const textarea = row.querySelector('textarea');
                return textarea ? `${span.textContent}::${textarea.value}` : span.textContent;
            });
            list.splice(0, list.length, ...items);
            await saveStorage();
        });
    }

    function createManager(label, key, list, selectedSet, isTextOnly = false) {
        const wrapper = document.createElement('div');
        wrapper.style.margin = '10px 0';

        const row1 = document.createElement('div');
        row1.style.display = 'flex';
        row1.style.alignItems = 'center';
        row1.style.marginBottom = '4px';

        const row2 = document.createElement('div');
        row2.style.display = 'flex';
        row2.style.alignItems = 'center';

        const noteInput = document.createElement('input');
        noteInput.placeholder = `备注`;
        noteInput.style.marginRight = '5px';
        noteInput.style.flex = '1';

        const addBtn = document.createElement('button');
        addBtn.textContent = '添加';
        addBtn.style.width = '50px';
        addBtn.style.height = '30px';
        addBtn.onclick = async () => {
            const note = noteInput.value.trim();
            const content = contentInput.value.trim();
            const final = note ? `${note}::${content}` : content;
            if (content && !list.includes(final)) {
                list.push(final);
                if (selectedSet) selectedSet.add(final);
                await saveStorage();
                wrapper.append(renderItemRow(final, selectedSet, list, isTextOnly));
                noteInput.value = '';
                contentInput.value = '';
            }
        };

        const contentInput = document.createElement('textarea');
        contentInput.placeholder = `内容(将作为输出 tag)`;
        contentInput.style.flex = '1';
        contentInput.style.height = '96px';
        contentInput.style.overflowY = 'scroll';
        contentInput.style.resize = 'none';

        row1.append(noteInput, addBtn);
        row2.append(contentInput);
        wrapper.append(row1, row2);

        list.forEach(item => wrapper.append(renderItemRow(item, selectedSet, list, isTextOnly)));

        enableDragSorting(wrapper, list, selectedSet, isTextOnly, key);

        return wrapper;
    }

    function renderItemRow(item, selectedSet, list, isTextOnly) {
        const row = document.createElement('div');
        row.className = 'draggable-item';
        row.draggable = true;
        row.style.display = 'flex';
        row.style.alignItems = 'center';
        row.style.marginTop = '4px';
        row.style.flexDirection = 'column';
        row.style.border = '1px dashed #ccc';
        row.style.padding = '2px';

        const [note, content] = item.includes('::') ? item.split('::') : [null, item];

        const top = document.createElement('div');
        top.style.display = 'flex';
        top.style.alignItems = 'center';
        top.style.width = '100%';

        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = selectedSet?.has(item);
        checkbox.disabled = isTextOnly && !selectedSet;
        checkbox.onchange = async () => {
            if (checkbox.checked) selectedSet.add(item);
            else selectedSet.delete(item);
            await saveStorage();
        };

        const label = document.createElement('span');
        label.textContent = note || content;
        label.style.margin = '0 5px';
        label.style.flex = '1';

        const toggleBtn = document.createElement('button');
        toggleBtn.textContent = '▸';
        toggleBtn.style.border = 'none';
        toggleBtn.style.background = 'transparent';
        toggleBtn.style.cursor = 'pointer';
        toggleBtn.style.height = '24px';
        toggleBtn.style.width = '24px';
        toggleBtn.style.padding = '0';
        toggleBtn.onclick = () => {
            const expanded = toggleBtn.textContent === '▾';
            toggleBtn.textContent = expanded ? '▸' : '▾';
            detailBox.style.display = expanded ? 'none' : 'block';
        };

        const delBtn = document.createElement('button');
        delBtn.textContent = '✖';
        delBtn.style.border = 'none';
        delBtn.style.background = 'transparent';
        delBtn.style.fontSize = '10px';
        delBtn.style.width = '24px';
        delBtn.style.height = '24px';
        delBtn.onclick = async () => {
            if (!confirm(`是否要删除${note || content}?`)) return;
            const idx = list.indexOf(item);
            if (idx > -1) list.splice(idx, 1);
            selectedSet?.delete(item);
            await saveStorage();
            row.remove();
        };

        const detailBox = document.createElement('textarea');
        detailBox.style.display = 'none';
        detailBox.style.fontSize = '12px';
        detailBox.style.color = '#333';
        detailBox.style.margin = '5px 0 0 20px';
        detailBox.style.width = '90%';
        detailBox.style.height = '96px';
        detailBox.style.overflowY = 'scroll';
        detailBox.style.resize = 'none';
        detailBox.value = content;

        top.append(checkbox, label, toggleBtn, delBtn);
        row.append(top, detailBox);

        return row;
    }

    function openSettingsPanel() {
        let panel = document.querySelector('#magic-settings');
        if (panel) return panel.style.display = 'block';

        panel = document.createElement('div');
        panel.id = 'magic-settings';
        panel.style.position = 'fixed';
        panel.style.top = '50px';
        panel.style.left = '10px';
        panel.style.background = 'white';
        panel.style.border = '1px solid gray';
        panel.style.padding = '10px';
        panel.style.zIndex = '9999';
        panel.style.width = '380px';

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '关闭';
        closeBtn.onclick = () => { panel.style.display = 'none'; };

        const saveAllBtn = document.createElement('button');
        saveAllBtn.textContent = '保存';
        saveAllBtn.style.marginRight = '10px';
        saveAllBtn.onclick = async () => {
            await saveStorage();
            alert('所有设置已保存!');
        };

        const tabMenu = document.createElement('div');
        tabMenu.style.display = 'flex';
        tabMenu.style.gap = '10px';
        tabMenu.style.marginBottom = '10px';

        const contentWrapper = document.createElement('div');

        const tabs = [
            { label: '角色', key: 'custom_roles', list: STATE.roles, selected: STATE.selectedRoles },
            { label: '预设', key: 'custom_presets', list: STATE.presets, selected: STATE.selectedPresets },
            { label: '艺术家', key: 'custom_artists', list: STATE.artists, selected: STATE.selectedArtists },
            { label: '忽略Tag', key: 'ignored_tags', list: STATE.ignore, selected: STATE.selectedTags, isTextOnly: true },
            { label: 'API', key: 'google_api_key' }
        ];

        tabs.forEach((tab, idx) => {
            const btn = document.createElement('button');
            btn.textContent = tab.label;
            btn.style.padding = '2px 6px';
            btn.onclick = () => {
                [...tabMenu.children].forEach(b => b.style.background = '');
                btn.style.background = '#def';
                contentWrapper.innerHTML = '';
                if (tab.key === 'google_api_key') {
                    const apiDiv = document.createElement('div');
                    apiDiv.style.display = 'flex';
                    apiDiv.style.flexDirection = 'column';
                    apiDiv.style.gap = '8px';

                    const apiTip = document.createElement('div');
                    apiTip.style.marginBottom = '4px';
                    apiTip.style.color = '#555';
                    apiTip.style.fontSize = '13px';
                    apiTip.innerHTML = '输入谷歌翻译API Key<br>留空使用质量较低的免费API';

                    const input = document.createElement('input');
                    input.type = 'text';
                    input.placeholder = '请输入API Key';
                    input.value = STATE.googleApiKey || '';
                    input.style.width = '100%';

                    const saveBtn = document.createElement('button');
                    saveBtn.textContent = '保存API Key';
                    saveBtn.onclick = async () => {
                        STATE.googleApiKey = input.value.trim();
                        await saveStorage();
                        alert('API Key已保存');
                    };
                    saveBtn.style.margin = '8px 0';

                    apiDiv.appendChild(apiTip);
                    apiDiv.appendChild(input);
                    apiDiv.appendChild(saveBtn);
                    contentWrapper.appendChild(apiDiv);
                } else {
                    contentWrapper.append(createManager(tab.label, tab.key, tab.list, tab.selected, tab.isTextOnly));
                }
            };
            if (idx === 0) btn.style.background = '#def';
            tabMenu.appendChild(btn);
        });

        contentWrapper.append(createManager(tabs[0].label, tabs[0].key, tabs[0].list, tabs[0].selected));

        panel.append(tabMenu, contentWrapper, saveAllBtn, closeBtn);
        document.body.appendChild(panel);
    }

    async function init() {
        const container = document.querySelector('.container');
        const tagTable = container?.querySelector('table');
        if (!tagTable) return setTimeout(init, 500);
        await loadStorage();
        createButtonBar(tagTable);
    }

    init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址