您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Slowly classifies 4chan thumbnails, saves results, and adds a UI to view them.
// ==UserScript== // @name 4chan Thumbnail Classifier (Advanced) // @namespace http://tampermonkey.net/ // @version 3.0 // @description Slowly classifies 4chan thumbnails, saves results, and adds a UI to view them. // @author wormpilled // @match https://boards.4chan.org/g/catalog // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @connect workers.dev // ==/UserScript== (function() { 'use strict'; // --- CONFIGURATION --- const WORKER_URL = 'https://image-classification-worker.YOUUUUUUUUU.workers.dev/'; // !!! MAKE SURE THIS IS CORRECT !!! const PROCESSING_DELAY_MS = 500; // Delay in milliseconds between each API call. const STORAGE_PREFIX = '4chan_classifier_'; // Used for localStorage keys. // --- STYLES --- GM_addStyle(` .thumb.classified { border: 3px solid #FF4500; /* A nice orange-red border */ box-sizing: border-box; } .classification-icon { cursor: pointer; font-size: 14px; margin-left: 5px; filter: grayscale(1); opacity: 0.6; transition: all 0.2s ease-in-out; } .classification-icon:hover { filter: grayscale(0); opacity: 1; transform: scale(1.2); } #classifier-popup-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 9999; display: flex; justify-content: center; align-items: center; } #classifier-popup-content { background: #282c34; color: #abb2bf; padding: 20px; border-radius: 8px; border: 1px solid #3e4451; min-width: 300px; text-align: left; } #classifier-popup-content h3 { margin-top: 0; border-bottom: 1px solid #3e4451; padding-bottom: 10px; } #classifier-popup-content ul { list-style: none; padding: 0; } #classifier-popup-content li { margin-bottom: 5px; } `); // --- CORE LOGIC --- // Function to show classification results in a popup function showResultsPopup(results, threadId) { // Remove any existing popup const existingPopup = document.getElementById('classifier-popup-overlay'); if (existingPopup) existingPopup.remove(); const overlay = document.createElement('div'); overlay.id = 'classifier-popup-overlay'; const content = document.createElement('div'); content.id = 'classifier-popup-content'; content.innerHTML = `<h3>Classification for #${threadId}</h3>`; const list = document.createElement('ul'); results.forEach(result => { const listItem = document.createElement('li'); const scorePercent = (result.score * 100).toFixed(1); listItem.textContent = `${result.label}: ${scorePercent}%`; list.appendChild(listItem); }); content.appendChild(list); overlay.appendChild(content); // Click anywhere on the overlay to close it overlay.addEventListener('click', () => overlay.remove()); document.body.appendChild(overlay); } // Updates the UI for a given thread (adds border and icon) function updateThreadUI(thread, results) { const thumb = thread.querySelector('img.thumb'); const meta = thread.querySelector('.meta'); const threadId = thread.id.replace('thread-', ''); if (!thumb || !meta) return; // 1. Add the red border thumb.classList.add('classified'); // 2. Add the clickable icon if (!meta.querySelector('.classification-icon')) { const icon = document.createElement('span'); icon.className = 'classification-icon'; icon.textContent = '🏷️'; // Tag emoji icon.title = 'View Classification Tags'; icon.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); showResultsPopup(results, threadId); }); meta.appendChild(icon); } } // Fetches classification from the worker and stores it async function classifyAndStore(thread) { const thumb = thread.querySelector('img.thumb'); const threadId = thread.id.replace('thread-', ''); const storageKey = STORAGE_PREFIX + threadId; return new Promise((resolve, reject) => { console.log(`[Classifier] Processing thread #${threadId}...`); GM_xmlhttpRequest({ method: 'POST', url: WORKER_URL, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify({ imageUrl: thumb.src }), onload: async function(response) { if (response.status === 200) { const results = JSON.parse(response.responseText); await GM_setValue(storageKey, JSON.stringify(results)); // Store results console.log(`[Classifier] SUCCESS for #${threadId}:`, results[0].label); updateThreadUI(thread, results); resolve(); } else { console.error(`[Classifier] ERROR for #${threadId}: Worker returned status ${response.status}`); reject(); } }, onerror: function(response) { console.error(`[Classifier] FATAL for #${threadId}: Request failed.`); reject(); } }); }); } // Main function to start the process async function initializeClassifier() { console.log('[Classifier] Initializing...'); const allThreads = document.querySelectorAll('.thread'); const threadsToProcess = []; for (const thread of allThreads) { const threadId = thread.id.replace('thread-', ''); const storageKey = STORAGE_PREFIX + threadId; const storedResults = await GM_getValue(storageKey, null); if (storedResults) { // Already classified, just update the UI from storage console.log(`[Classifier] Loading #${threadId} from storage.`); updateThreadUI(thread, JSON.parse(storedResults)); } else { // Not classified, add to the queue threadsToProcess.push(thread); } } console.log(`[Classifier] Found ${threadsToProcess.length} new thumbnails to classify.`); // Process the queue slowly for (const thread of threadsToProcess) { try { await classifyAndStore(thread); // Wait for the specified delay before the next iteration await new Promise(resolve => setTimeout(resolve, PROCESSING_DELAY_MS)); } catch (error) { console.warn('[Classifier] An error occurred, continuing to next item.'); } } console.log('[Classifier] All new thumbnails have been processed.'); } // Run the script after the page has loaded window.addEventListener('load', initializeClassifier); })();