最近使用Fooocus,比使用WebUI A1111來的頻繁,主要是設定夠簡單,能省很多步驟,而且生圖速度真的快很多很多,一張1152x896的圖幾秒鐘就好了(Extreme Speed)的模式(顯卡才3060),A1111這尺寸就得跑30秒才能出一張圖。


Fooocus真的很不錯,但是最近想要可以批次生圖,就是可以輸入很多組prompt,自動運行完能自動換下一組prompt自動運行,我找了一圈,都沒找到這功能

去官方github討論區也沒找到解決辦法,原本想用它的API來實現,但找不到文檔,給的gradio api真不知道在寫啥東東....雖然有個API分支(但不太想另外安裝)


最後決定自己寫個外掛腳本,來實現這個批次執行的功能。

首先得先安裝竄改猴(油猴) tampermonkey,這個Chrome擴充工具可以讓我們在指定頁面注入js,這樣可以增加自訂功能。

使用擴充工具的好處是不用改Fooocus源碼,之後Fooocus有新版版也不會因為動了源碼影響升級

壞處是假如Fooocus更新有變動UI的話,這腳本可能就運行不了。但基本上這個概率比較小。

竄改猴

https://chromewebstore.google.com/detail/%E7%AF%A1%E6%94%B9%E7%8C%B4/dhdgffkkebhmkfjojejmpbldmpobfkfo

之後在竄改猴新增腳本,將下面的腳本貼入,然後保存起來

// ==UserScript==
// @name         Fooocus批次提示詞執行任務
// @namespace    https://blog.aidec.tw/
// @version      2024-06-25
// @description  Fooocus 批次提示詞執行任務功能,可以在批次執行的欄位輸入多筆提示詞,每行一組。
// @author       AidecLi
// @match        http://127.0.0.1:7865/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=0.1
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    let isProcessing = false;
    let stopRequested=false;
    let origPrompts = '';
    let promptInputTextarea, processedTextarea, startButton, stopButton, progressBar, statusDiv;

    // 添加UI元素
    function addUI() {
        const targetElement = document.querySelector('#component-3');
        if (!targetElement) {
            console.error('Target element #component-3 not found');
            return;
        }
        const container = document.createElement('div');
        container.style.marginTop = '20px';
        container.style.display = 'flex';
        container.style.flexDirection = 'column';
        container.style.gap = '10px';

        promptInputTextarea = document.createElement('textarea');
        promptInputTextarea.id = 'promptList';
        promptInputTextarea.rows = 5;
        promptInputTextarea.style.width = '100%';
        promptInputTextarea.placeholder = '待處理的提示詞,每行一組';

        processedTextarea = document.createElement('textarea');
        processedTextarea.id = 'processedList';
        processedTextarea.rows = 5;
        processedTextarea.style.width = '100%';
        processedTextarea.placeholder = '已處理的提示詞';
        processedTextarea.readOnly = true;

        startButton = document.createElement('button');
        startButton.textContent = '開始批次處理';
        startButton.onclick = processPrompts;
        startButton.style.padding = '5px 10px';
        startButton.style.cursor = 'pointer';
        startButton.className = 'lg secondary type_row svelte-cmf5ev';

        stopButton = document.createElement('button');
        stopButton.textContent = '停止批次處理';
        stopButton.onclick = stopProcessing;
        stopButton.style.padding = '5px 10px';
        stopButton.style.cursor = 'pointer';
        stopButton.className = 'lg secondary type_row svelte-cmf5ev';

        progressBar = document.createElement('progress');
        progressBar.id = 'progressBar';
        progressBar.value = 0;
        progressBar.max = 100;
        progressBar.style.width = '100%';

        statusDiv = document.createElement('div');
        statusDiv.id = 'statusDiv';
        statusDiv.textContent = '狀態:尚未開始';
        statusDiv.style.fontWeight = 'bold';
        container.appendChild(statusDiv);
        container.appendChild(promptInputTextarea);
        container.appendChild(processedTextarea);
        container.appendChild(startButton);
        container.appendChild(stopButton);
        container.appendChild(progressBar);
        targetElement.appendChild(container);
    }
    //處理批次提示詞
    async function processPrompts() {
        if (isProcessing) return;
        isProcessing = true;
        stopRequested = false;
        disableUI();

        const promptList = promptInputTextarea.value.split('\n').filter(p => p.trim() !== '');
        console.log(promptList);
        const totalPrompts = promptList.length;
        const positivePromptTextarea = document.querySelector('#positive_prompt textarea');
        const generateButton = document.querySelector('#generate_button');
        origPrompts = promptInputTextarea.value;
        let i = 0;
        for (const prompt of promptList) {
            if (stopRequested) break;
            statusDiv.textContent = `正在處理第 ${i + 1} 組,共 ${totalPrompts} 組 Prompt: ${prompt}`;
            console.log(prompt);
            positivePromptTextarea.value = prompt;
            positivePromptTextarea.dispatchEvent(new Event('input', { bubbles: true }));
            console.log('開始執行'+prompt)
            await new Promise(resolve => setTimeout(resolve, 500));
            generateButton.click();
            await new Promise(resolve => setTimeout(resolve, 1000));
            await waitForGeneration(generateButton);
            if (stopRequested) break;
            console.log('結束執行'+prompt)
            // 將已處理的移到已處理區
            processedTextarea.value += prompt + '\n';
            //將已處理的從待處理區移除
            document.getElementById('promptList').value = waitPromptList(promptList,i+1);
            //更新進度條
            progressBar.value = ((i + 1) / totalPrompts) * 100;
            // 等待3秒再繼續下一個提示詞
            statusDiv.textContent = `第 ${i + 1} 組,已處理完畢,等待2秒後進入下一組。`;
            await new Promise(resolve => setTimeout(resolve, 2000));
            i++;
        }

        if (stopRequested) {
            statusDiv.textContent = '處理已被中止';
        } else {
            statusDiv.textContent = '本次所有提示詞已處理完畢';
        }
        isProcessing = false;
        enableUI();
        //還原回原本的Prompt
        promptInputTextarea.value = origPrompts;
    }

    // 停止處理
    function stopProcessing() {
        stopRequested = true;
        isProcessing = false;
        enableUI();
        //也停止本輪正在運作的生圖
        const stopButton = document.querySelector('#stop_button');
        stopButton.click();
        promptInputTextarea.value = origPrompts;
        processedTextarea.value = '';
    }

    // 將尚未處理的提示詞合併成字串
    function waitPromptList(promptList, currentIndex) {
        return promptList.slice(currentIndex).join('\n');
    }

    // 禁用UI元素
    function disableUI() {
        promptInputTextarea.disabled = true;
        startButton.disabled = true;
        startButton.textContent = '處理中...';
    }

    //啟用UI元素
    function enableUI() {
        promptInputTextarea.disabled = false;
        startButton.disabled = false;
        startButton.textContent = '開始批次處理';
    }

    //等待生成過程
    function waitForGeneration(button) {
        return new Promise(resolve => {
            const observer = new MutationObserver(() => {
                if (!button.disabled && !button.hidden) {
                    // 等待按鈕
                    setTimeout(() => {
                        observer.disconnect();
                        resolve();
                    }, 1500);
                }
            });

            observer.observe(button, { attributes: true, attributeFilter: ['disabled', 'hidden'] });
            if (stopRequested) resolve();
            // 如果按鈕可用且沒有hidden,則繼續
            if (!button.disabled && !button.hidden) {
                observer.disconnect();
                resolve();
            }
        });
    }

    //將自訂UI添加到畫面上
    function initScript() {
        const maxAttempts = 10;
        let attempts = 0;

        function tryAddUI() {
            if (document.querySelector('#component-3')) {
                addUI();
            } else if (attempts < maxAttempts) {
                attempts++;
                setTimeout(tryAddUI, 1000); // 等待1秒重新嘗試
            } else {
                console.error('Failed to find #component-3 after ' + maxAttempts + ' attempts');
            }
        }

        tryAddUI();
    }

    // 頁面完成加載UI到畫面
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', initScript);
    } else {
        initScript();
    }
})();

之後在Fooocus重新整理頁面,應該就能使用了~ 完整操作的看下方影片,用寫的怕解釋不清楚。

基本上就是

  1. 安裝竄改猴

  2. 新增腳本,貼入腳本

  3. 在重新整理Fooocus就能看到多了批次處理區

  4. 將多筆prompt輸入,按批次執行就可以了


文章轉載或引用,請先告知並保留原文出處與連結!!(單純分享或非營利的只需保留原文出處,不用告知)

原文連結:
https://blog.aidec.tw/post/fooocus-batch-prompt-task-run
若有業務合作需求,可寫信至: opweb666@gmail.com