From 55fa2ddf4c6530cf02c46e24ac1629353a7f5e34 Mon Sep 17 00:00:00 2001 From: macmini Date: Mon, 25 Aug 2025 16:19:59 +0800 Subject: [PATCH 1/4] Aug 25, 2025 --- bun serv.ts | 841 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 514 insertions(+), 327 deletions(-) diff --git a/bun serv.ts b/bun serv.ts index 7c31281..06a4c76 100644 --- a/bun serv.ts +++ b/bun serv.ts @@ -1,21 +1,35 @@ -import { - serve, - type Server, - Bun, - spawn, - spawnSync, - type Subprocess, -} from "bun"; -import fs from "node:fs"; -import path from "node:path"; +import { serve, type Server, spawn, spawnSync, type Subprocess } from "bun"; +import { spawn as ptyspawn, type Pty } from "bun-pty"; + +// --- Local bin resolver (ship-ready) --- +const HERE_DIR = import.meta.dir; // absolute folder for this file +const BIN_DIR = `${HERE_DIR}/bin`; +const PLATFORM_DIR = (() => { + if (Bun.platform === "darwin" && Bun.arch.startsWith("arm")) return `${BIN_DIR}/mac-aarch`; + // Fallback: use a generic mapping if other platforms are added later + return `${BIN_DIR}/${Bun.platform}-${Bun.arch}`; +})(); + +async function exists(p: string): Promise { + try { + return await Bun.file(p).exists(); + } catch { + return false; + } +} +async function resolveExisting(paths: string[]): Promise { + for (const p of paths) if (await exists(p)) return p; + return null; +} // --- Configuration --- const SERVER_PORT = 65535; const TTS_RATE = 190; // Speech rate for the 'say' command +const isProd = Bun.env.NODE_ENV === "production"; const logInfo = (...args: any[]) => {}; const logWarn = (...args: any[]) => {}; -const logError = console.error.bind(console, '[ERROR]'); // Always log errors +const logError = console.error.bind(console, "[ERROR]"); // Always log errors // A pool of reliable ad‑free European classical streams const EU_STREAM_URLS = [ @@ -24,21 +38,16 @@ const EU_STREAM_URLS = [ // "http://icecast.vrtcdn.be/klaracontinuo-high.mp3", "http://116.202.241.212:8010/stream", "http://148.251.43.231:8742/160", + "https://mediaserviceslive.akamaized.net/hls/live/2038317/classic2/index.m3u8", + "https://pianosolo.streamguys1.com/live",//had speak ]; - // Pick one at random from the array (avoid out‑of‑bounds undefined) -const EU_STREAM_URL = - EU_STREAM_URLS[Math.floor(Math.random() * EU_STREAM_URLS.length)]; +const EU_STREAM_URL = EU_STREAM_URLS[Math.floor(Math.random() * EU_STREAM_URLS.length)]; const EU_FIFO_PATH = "/tmp/eu_fifo"; -const EU_PLAYER_PATHS = [ - "/opt/homebrew/bin/ffplay", - "/usr/local/bin/ffplay", - "/opt/homebrew/bin/mpv", - "/usr/local/bin/mpv", -]; -const CURL_PATH = "/usr/bin/curl"; -const MKFIFO_PATH = "/usr/bin/mkfifo"; -const PKILL_PATH = "/usr/bin/pkill"; +const EU_PLAYER_PATHS = [`${PLATFORM_DIR}/mpv/mpv`, "/opt/homebrew/bin/mpv", "/usr/local/bin/mpv", "/usr/bin/mpv"]; +const CURL_PATH = (await resolveExisting([`${PLATFORM_DIR}/curl/curl`, "/usr/bin/curl", "/opt/homebrew/bin/curl"])) || "curl"; +const MKFIFO_PATH = (await resolveExisting([`${PLATFORM_DIR}/mkfifo/mkfifo`, "/usr/bin/mkfifo", "/opt/homebrew/bin/mkfifo"])) || "mkfifo"; +const PKILL_PATH = (await resolveExisting([`${PLATFORM_DIR}/pkill/pkill`, "/usr/bin/pkill", "/opt/homebrew/bin/pkill"])) || "pkill"; // --- State --- let server: Server | null = null; @@ -46,10 +55,12 @@ let speakQueue: string[] = []; let isSpeaking = false; let currentSpeechProcess: Subprocess | null = null; let euPlayerPath: string | null = null; -let euFeederProcess: Subprocess | null = null; -let euPlayerProcess: Subprocess | null = null; +let euStarting = false; +let euMpv: Pty | null = null; +let euMuted = true; // default muted +// Removed: let euFeederProcess, let euPlayerProcess let lastEUPing = 0; -let euPingTimer: Timer | null = null; +let euPingTimer: ReturnType | null = null; // --- Interfaces --- interface SpeakPayload { @@ -64,17 +75,13 @@ interface ReplacePayload { } // --- Utility Functions --- -const killProcess = ( - proc: Subprocess | null, - name: string, - signal: NodeJS.Signals | number = "SIGTERM" -) => { +const killProcess = (proc: Subprocess | null, name: string, signal: number = 15) => { if (proc && proc.pid) { - if (process.env.NODE_ENV !== "production") { + if (Bun.env.NODE_ENV !== "production") { console.debug("[DEBUG]", `Attempting to kill ${name} process (PID: ${proc.pid}) with signal ${signal}...`); } - const killed = proc.kill(signal as number); // Bun's types might mismatch NodeJS.Signals sometimes - if (process.env.NODE_ENV !== "production") { + const killed = proc.kill(signal); + if (Bun.env.NODE_ENV !== "production") { console.debug("[DEBUG]", `${name} process kill signal sent (success: ${killed}).`); } return killed; @@ -83,25 +90,22 @@ const killProcess = ( }; const pkillProcess = (pattern: string) => { - if (process.env.NODE_ENV !== "production") { + if (Bun.env.NODE_ENV !== "production") { console.debug("[DEBUG]", `Attempting to pkill processes matching: ${pattern}`); } try { const result = spawnSync([PKILL_PATH, "-f", pattern]); if (result.exitCode === 0) { - if (process.env.NODE_ENV !== "production") { + if (Bun.env.NODE_ENV !== "production") { console.debug("[DEBUG]", `pkill successful for pattern: ${pattern}`); } } else if (result.exitCode === 1) { - if (process.env.NODE_ENV !== "production") { + if (Bun.env.NODE_ENV !== "production") { console.debug("[DEBUG]", `No processes found matching pattern: ${pattern}`); } } else { - logWarn( - `pkill for pattern "${pattern}" exited with code ${ - result.exitCode - }. Stderr: ${result.stderr.toString()}` - ); + const stderrText = new TextDecoder().decode(result.stderr); + logWarn(`pkill for pattern "${pattern}" exited with code ${result.exitCode}. Stderr: ${stderrText}`); } } catch (error) { logError(`Error executing pkill for pattern "${pattern}":`, error); @@ -112,10 +116,10 @@ const pkillProcess = (pattern: string) => { const stopSayProcess = () => { if (currentSpeechProcess) { - if (process.env.NODE_ENV !== "production") { + if (Bun.env.NODE_ENV !== "production") { console.debug("[DEBUG]", "Stopping current 'say' process."); } - killProcess(currentSpeechProcess, "say", "SIGKILL"); // 'say' might need SIGKILL + killProcess(currentSpeechProcess, "say", 9); // SIGKILL currentSpeechProcess = null; isSpeaking = false; // Ensure state is reset } @@ -125,21 +129,14 @@ const flushQueue = () => { logInfo("Flushing speech queue and stopping current speech."); speakQueue = []; stopSayProcess(); + eumute(); }; const enqueueSpeech = (text: string) => { logInfo(`[${new Date().toISOString()}] Received incoming letter: "${text}"`); - const trimmedText = text.trim(); - if (!trimmedText) return; - - // Basic sanitation (remove potential non-UTF8 chars that `say` might dislike) - // This is a very basic filter; more robust UTF8 validation might be needed - const sanitizedText = trimmedText.replace(/[\u{80}-\u{FFFF}]/gu, ""); // Keep ASCII + basic Latin supplement? Adjust as needed. - if (!sanitizedText) { - logWarn("Text became empty after sanitization, skipping enqueue."); - return; - } + // (remove chars that `say` n europe dislike) + const sanitizedText = text.replace(/[^\u{20}-\u{39}\u{3B}-\u{1E79}\u{2000}-\u{218F}\u{2200}-\u{23FF}\u{2460}-\u{24FF}]/gu, " "); // keep all europe safe item to tts if (speakQueue.length === 0) { speakQueue.push(sanitizedText); @@ -151,13 +148,23 @@ const enqueueSpeech = (text: string) => { speakQueue[speakQueue.length - 1] += sanitizedText; } } - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", `Enqueued: "${sanitizedText}". Queue length: ${speakQueue.length}`); } }; +function warmtts() { + const p = ptyspawn("say", ["-i", "-r", String(TTS_RATE)], { + name: "xterm-256color", + cols: 80, + rows: 24, + }); + p.onData((d) => Bun.stdout.write(d)); + p.write("."); +} +const coretts = (textToSpeak) => ["say", "-r", String(TTS_RATE), textToSpeak]; const startNextSpeech = () => { - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", `startNextSpeech called. isSpeaking: ${isSpeaking}, queueLength: ${speakQueue.length}`); } if (isSpeaking || speakQueue.length === 0) { @@ -176,13 +183,11 @@ const startNextSpeech = () => { logInfo(`Speaking: "${textToSpeak.substring(0, 50)}..."`); try { - const coretts = ["say", "-r", String(TTS_RATE), textToSpeak]; - currentSpeechProcess = spawn(coretts, { + currentSpeechProcess = spawn(coretts(textToSpeak), { stdin: "ignore", // No input needed - stdout: "inherit", // Inherit stdout/stderr for potential messages/errors from 'say' stderr: "inherit", onExit: (proc, exitCode, signalCode, error) => { - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", `'say' process exited. Code: ${exitCode}, Signal: ${signalCode}`); } if (error) { @@ -192,20 +197,18 @@ const startNextSpeech = () => { if (currentSpeechProcess && currentSpeechProcess.pid === proc.pid) { currentSpeechProcess = null; isSpeaking = false; - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", "Current speech finished, checking queue for next."); } - // Use setTimeout to avoid potential deep recursion if 'say' fails instantly - setTimeout(startNextSpeech, 0); + + startNextSpeech(); } else { - logWarn( - `Received onExit callback for a stale 'say' process (PID: ${proc.pid}). Ignoring.` - ); + logWarn(`Received onExit callback for a stale 'say' process (PID: ${proc.pid}). Ignoring.`); } }, }); - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", `Started 'say' process (PID: ${currentSpeechProcess.pid})`); } @@ -231,233 +234,106 @@ const replaceSpeechImmediately = (newText: string) => { // --- EU Classical Stream Functions --- -const findEuPlayer = (): string | null => { - for (const p of EU_PLAYER_PATHS) { - if (fs.existsSync(p)) { - logInfo(`Found EU player: ${p}`); - return p; - } - } - logWarn( - `Could not find ffplay or mpv in specified paths: ${EU_PLAYER_PATHS.join( - ", " - )}. EU stream playback will fail.` - ); - return null; -}; - -const stopFeeder = () => { - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", "Stopping EU stream feeder (curl)..."); - } - if (euFeederProcess) { - killProcess(euFeederProcess, "curl feeder"); - euFeederProcess = null; - } else { - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", "No active feeder process handle found, using pkill as fallback."); - } - // Use pkill as a fallback to catch manually started or orphaned processes - pkillProcess(`${CURL_PATH}.*${EU_FIFO_PATH}`); +const findEuPlayer = async (): Promise => { + // Prefer shipped binary under bin/, then PATH, then known fallbacks + const localFirst = await resolveExisting([EU_PLAYER_PATHS[0]]); + if (localFirst) { + logInfo(`Found EU player: ${localFirst}`); + return localFirst; } -}; - -const eu_warm = () => { - logInfo("Warming EU stream feeder..."); - stopFeeder(); // Ensure any previous feeder is stopped - - // Use the selected stream URL, guard against undefined - const STREAM_URL = EU_STREAM_URL; - if (!STREAM_URL) { - logError( - "EU_STREAM_URL is undefined – check stream list. Aborting warm‑up." - ); - return; + const inPath = Bun.which("mpv"); + if (inPath) { + logInfo(`Found EU player in PATH: ${inPath}`); + return inPath; } - - // Ensure FIFO exists - try { - // Attempt to remove existing FIFO first (ignore error if it doesn't exist) - try { - fs.unlinkSync(EU_FIFO_PATH); - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", `Removed existing FIFO: ${EU_FIFO_PATH}`); - } - } catch { - /* Ignore */ - } - - // Create new FIFO - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", `Creating FIFO: ${EU_FIFO_PATH}`); - } - const mkfifoResult = spawnSync([MKFIFO_PATH, EU_FIFO_PATH]); - if (mkfifoResult.exitCode !== 0) { - throw new Error( - `mkfifo failed with code ${ - mkfifoResult.exitCode - }: ${mkfifoResult.stderr.toString()}` - ); - } - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", `FIFO created successfully.`); - } - - // Start curl feeder process - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", `Starting curl feeder: ${CURL_PATH} -sL ${STREAM_URL} -o ${EU_FIFO_PATH}`); - } - euFeederProcess = spawn( - [CURL_PATH, "-sL", STREAM_URL, "-o", EU_FIFO_PATH], - { - stdin: "ignore", - stdout: "ignore", // Ignore stdout/stderr unless debugging curl itself - stderr: "ignore", - onExit: (proc, exitCode, signalCode, error) => { - logWarn( - `EU feeder (curl) process (PID: ${ - proc?.pid ?? "unknown" - }) exited. Code: ${exitCode}, Signal: ${signalCode}` - ); - if (error) logError("Feeder exit error:", error); - // If the feeder dies unexpectedly, we might want to clear the handle - if (euFeederProcess && euFeederProcess.pid === proc?.pid) { - euFeederProcess = null; - } - }, - } - ); - - if (!euFeederProcess || !euFeederProcess.pid) { - throw new Error("Failed to get valid process handle for curl feeder."); - } - logInfo( - `EU feeder (curl) started (PID: ${euFeederProcess.pid}). Streaming to ${EU_FIFO_PATH}` - ); - } catch (error) { - logError("Error during eu_warm:", error); - // Clean up if feeder process might have started before error - if (euFeederProcess) killProcess(euFeederProcess, "curl feeder on error"); - euFeederProcess = null; + const fallback = await resolveExisting(EU_PLAYER_PATHS.slice(1)); + if (fallback) { + logInfo(`Found EU player (fallback): ${fallback}`); + return fallback; } + logWarn(`Could not find mpv in local bin, PATH, or fallbacks: ${EU_PLAYER_PATHS.join(", ")}`); + return null; }; -const eu_start = () => { +const eu_start = async () => { if (!euPlayerPath) { logError("Cannot start EU stream: Player path not found."); return; } - if (euPlayerProcess && euPlayerProcess.pid) { - logInfo("EU stream player is already running."); + if (euMpv) { + logInfo("EU (mpv) already running."); return; } - - logInfo("Starting EU stream player..."); - - // Ensure previous player process is definitely gone - eu_stop(false); // Stop without warming, just kill player - + if (euStarting) { + logInfo("EU stream start already in progress; skipping."); + return; + } + euStarting = true; + logInfo("Starting EU stream (mpv)..."); try { - const playerArgs = euPlayerPath.includes("mpv") - ? [ - "--no-video", - "--quiet", - "--cache=no", - "--demuxer-max-bytes=32", - "--demuxer-readahead-secs=0", - EU_FIFO_PATH, - ] - : [ - "-nodisp", - "-autoexit", - "-loglevel", - "error", - "-fflags", - "nobuffer", - "-flags", - "low_delay", - "-probesize", - "32", - "-analyzeduration", - "0", - "-volume", - "20", - EU_FIFO_PATH, - ]; - - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", `Spawning EU player: ${euPlayerPath} ${playerArgs.join(" ")}`); - } - - euPlayerProcess = spawn([euPlayerPath, ...playerArgs], { - stdin: "ignore", - stdout: "inherit", // Show player output/errors - stderr: "inherit", - onExit: (proc, exitCode, signalCode, error) => { - logInfo( - `EU Player process (PID: ${ - proc?.pid ?? "unknown" - }) exited. Code: ${exitCode}, Signal: ${signalCode}` - ); - if (error) logError("EU Player exit error:", error); - // Clear the handle only if it matches the exited process - if (euPlayerProcess && euPlayerProcess.pid === proc?.pid) { - euPlayerProcess = null; - } - }, + // mpv direct playback; default muted per requirement + euMpv = ptyspawn(euPlayerPath, [ + "--profile=low-latency", + "--demuxer-donate-buffer=no", + "--ad-queue-enable=no", + "--keep-open=no", + "--cache=no", + "--no-video", + "--quiet", + "--mute=yes", + "--no-ytdl", + "--af=volume=0.15", + EU_STREAM_URL, + ]); + + euMuted = true; + euMpv.onData((d) => { + if (!isProd) Bun.stdout.write(d); }); - - if (!euPlayerProcess || !euPlayerProcess.pid) { - throw new Error("Failed to get valid process handle for EU player."); - } - logInfo( - `EU stream player started (PID: ${euPlayerProcess.pid}). Playing from ${EU_FIFO_PATH}` - ); + euMpv.onExit?.(() => { + euMpv = null; + }); + logInfo("EU (mpv) started."); } catch (error) { - logError("Error starting EU stream player:", error); - euPlayerProcess = null; // Ensure handle is cleared on error + logError("Error starting mpv:", error); + } finally { + euStarting = false; } }; -const eu_stop = (warmAfterStop = false) => { - logInfo("Stopping EU stream player..."); - if (euPlayerProcess) { - killProcess(euPlayerProcess, "EU player"); - euPlayerProcess = null; - } else { - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", "No active player process handle, using pkill fallback."); - } - // Fallback pkill based on likely player paths - pkillProcess("ffplay.*" + EU_FIFO_PATH); - pkillProcess("mpv.*" + EU_FIFO_PATH); - } - - if (warmAfterStop) { - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", "Warming feeder after stopping player (mute behavior)."); - } - eu_warm(); // Restart the feeder - } +let eusafe = true; +const eu_flip_mute = () => { + if (!eusafe) return; + eusafe = false; + euMpv.write("m\r"); + euMuted = !euMuted; + eusafe = true; }; +const eumute = () => { + if (!euMuted) eu_flip_mute(); +}; +const euunmute = () => { + if (euMuted) eu_flip_mute(); +}; +//const eu_mute = (boolean) => {}; const startEUPingWatchdog = () => { - // Always reset the interval so lpEU can relaunch after a mute - if (euPingTimer) { - clearInterval(euPingTimer); - euPingTimer = null; - } + if (euPingTimer) return; // already watching euPingTimer = setInterval(() => { const now = Date.now(); if (now - lastEUPing > 1000) { logInfo("⏱️ No lpEU ping in >1 s. Muting EU stream."); - eu_stop(true); - clearInterval(euPingTimer!); - euPingTimer = null; + eumute(); } }, 1000); }; +const eu_exitmpv = () => { + if (euMpv) { + euMpv.kill(); + } +}; + // --- HTTP Server --- const handleRequest = async (request: Request): Promise => { @@ -465,7 +341,7 @@ const handleRequest = async (request: Request): Promise => { const path = url.pathname; const method = request.method; - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", `Received request: ${method} ${path}`); } @@ -481,11 +357,7 @@ const handleRequest = async (request: Request): Promise => { throw new Error("Parsed JSON is not an object"); } } catch (jsonError) { - logWarn( - `JSON parse failed: ${ - jsonError instanceof Error ? jsonError.message : jsonError - }. Falling back to using raw body as text.` - ); + logWarn(`JSON parse failed: ${jsonError instanceof Error ? jsonError.message : jsonError}. Falling back to using raw body as text.`); // Fallback: Use the raw body text if JSON parsing fails *or* doesn't yield an object // Ensure payload is at least an empty object before assigning text payload = {}; @@ -495,17 +367,17 @@ const handleRequest = async (request: Request): Promise => { // --- EU Ping Handling --- if ((payload as any).lpEU) { lastEUPing = Date.now(); - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", "🔄 lpEU ping received."); } startEUPingWatchdog(); - eu_start(); // Start (or resume) playback on each lpEU ping, mirroring Lua design + euunmute(); return new Response("lpEU pong", { status: 200 }); } if ((payload as any).dsEU) { lastEUPing = Date.now(); - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", "📍 dsEU ping received."); } return new Response("dsEU updated", { status: 200 }); @@ -521,16 +393,14 @@ const handleRequest = async (request: Request): Promise => { // EU Controls if (payload.playEU) { logInfo("playEU request received."); - eu_start(); + euunmute(); // only unmute if muted // Return immediately after handling EU command if no text is present - if (!payload.text?.trim()) - return new Response("OK (EU Play)", { status: 200 }); + if (!payload.text?.trim()) return new Response("OK (EU Play)", { status: 200 }); } else if (payload.stopEU) { logInfo("stopEU request received."); - eu_stop(true); // stopEU implies mute/rewarm + eu_mute(); // only mute if unmuted // Return immediately after handling EU command if no text is present - if (!payload.text?.trim()) - return new Response("OK (EU Stop)", { status: 200 }); + if (!payload.text?.trim()) return new Response("OK (EU Stop)", { status: 200 }); } // TTS Handling @@ -540,7 +410,7 @@ const handleRequest = async (request: Request): Promise => { return new Response("OK (enqueued)", { status: 200 }); } else { // If we only got EU commands or flush, or empty text - if (process.env.NODE_ENV !== "production") { + if (!isProd) { console.debug("[DEBUG]", "Request handled (non-TTS action or empty text)."); } return new Response("OK (action)", { status: 200 }); @@ -553,11 +423,7 @@ const handleRequest = async (request: Request): Promise => { } else if (path === "/replace" && method === "POST") { try { const payload = (await request.json()) as ReplacePayload; - if ( - typeof payload !== "object" || - payload === null || - typeof payload.text !== "string" - ) { + if (typeof payload !== "object" || payload === null || typeof payload.text !== "string") { return new Response("Error: Invalid JSON or missing 'text' field.", { status: 400, }); @@ -589,13 +455,13 @@ const handleRequest = async (request: Request): Promise => { // --- Server Control --- -const startServer = () => { +const startServer = async () => { if (server) { logInfo(`Server already running on port ${SERVER_PORT}`); return; } - euPlayerPath = findEuPlayer(); // Find player on startup + euPlayerPath = await findEuPlayer(); // Find player on startup try { server = serve({ @@ -606,17 +472,16 @@ const startServer = () => { return new Response("Internal Server Error", { status: 500 }); }, }); - logInfo( - `🎙️ TTS Server (using 'say') started on http://localhost:${server.port}` - ); - eu_warm(); // Warm up the EU stream on server start + logInfo(`🎙️ TTS Server (using 'say') started on http://localhost:${server.port}`); + eu_start(); + // void eu_warm(); } catch (error) { logError(`Failed to start server on port ${SERVER_PORT}:`, error); server = null; } }; -const stopServer = () => { +const stopServer = async () => { if (server) { logInfo("Stopping TTS Server..."); server.stop(true); // true for graceful shutdown @@ -627,37 +492,15 @@ const stopServer = () => { } // Stop related processes flushQueue(); // Stops current speech and clears queue - stopFeeder(); - eu_stop(false); // Stop player without re-warming - - // Clean up FIFO - try { - if (fs.existsSync(EU_FIFO_PATH)) { - if (process.env.NODE_ENV !== "production") { - console.debug("[DEBUG]", `Removing FIFO on shutdown: ${EU_FIFO_PATH}`); - } - fs.unlinkSync(EU_FIFO_PATH); - } - } catch (error) { - logError(`Error removing FIFO ${EU_FIFO_PATH} on shutdown:`, error); - } + eu_exitmpv(); }; -// --- Health Check --- -/*setInterval(() => { - logDebug( - `[HealthCheck] isSpeaking: ${isSpeaking}, queueLength: ${ - speakQueue.length - }, euFeederPID: ${euFeederProcess?.pid ?? "none"}, euPlayerPID: ${ - euPlayerProcess?.pid ?? "none" - }` - ); -}, 30 * 1000); // Check every 30 seconds -*/ // Optional Electron integration (guarded so Bun tests won’t fail) -let app, globalShortcut; +/*let app, globalShortcut; try { - ({ app, globalShortcut } = require("electron")); + const electron = await import("electron"); + app = electron.app; + globalShortcut = electron.globalShortcut; app.whenReady().then(() => { globalShortcut.register("Shift+Right", () => { console.log("Right Shift pressed"); @@ -666,25 +509,369 @@ try { } catch { // Electron not available (e.g., running under Bun), skip integration } +*/ // --- Shutdown Hook --- -const cleanupAndExit = (signal: string) => { +const cleanupAndExit = async (signal: string) => { logInfo(`Received ${signal}. Starting graceful shutdown...`); - stopServer(); + await stopServer(); logInfo("Cleanup complete. Exiting."); process.exit(0); }; -process.on("SIGINT", () => cleanupAndExit("SIGINT")); // Ctrl+C -process.on("SIGTERM", () => cleanupAndExit("SIGTERM")); // kill +process.on("SIGINT", () => { + void cleanupAndExit("SIGINT"); +}); // Ctrl+C +process.on("SIGTERM", () => { + void cleanupAndExit("SIGTERM"); +}); // kill // --- Main Execution --- logInfo("Starting script..."); -startServer(); +void startServer(); // Keep the script running until interrupted // Bun automatically keeps running while the server is active. // We add an extra interval just to be explicit if needed, but server.stop() and process.exit() handle termination. // setInterval(() => {}, 1 << 30); // Keep alive indefinitely (optional) - logInfo("Script initialization complete. Server is running."); + +// --- ai cleanup client -------- +import { GoogleGenAI, createUserContent } from "@google/genai"; + +// Configuration +const CONFIG = { + API_KEY: Bun.env.GEMINI_API_KEY, + + MODEL_NAME: "gemini-2.5-flash-lite-preview-06-17", + + GENERATION_CONFIG: { + temperature: 1, + topK: 32, + topP: 0.95, + maxOutputTokens: 32000, + }, + SYSTEM_INSTRUCTION: `i.main goal: base on full ax tree, ur goal is from there rever to show off exactly last-ai-response(LAR) ; no conversation with user or any other else just pure show off LAR. also the ax tree are noicy avoid any noises eg but not limited like response hist/thoughts/ui etc. also if were none match, the show off shall be empty (null output). + ii.sub goal: Ingros - facemaks, special simples, non european chars. Replace - simples like '+' '-' if is in math context into 'plus' 'minus' specialy '-' can be mutible means, this just an example the goal is as smartly adatively replace make how to prenowce more direct in europe word. + iii.main goal - no ur own imgine adds: to make sure the LAR shall as 100% rever as base on the ax tree, no ur own imgine adds, cuz the goal is show or mirr the LAR as it is`, +}; + +// Initialize the GenAI client +if (!CONFIG.API_KEY) { + console.error("[ERROR] GEMINI_API_KEY environment variable is not set!"); + console.error("Please set it by running: export GEMINI_API_KEY='your-api-key-here'"); +} +const genAI = new GoogleGenAI({ apiKey: CONFIG.API_KEY || "" }); + +/** + * Main handler function for serverless execution + * @param {Object} event - The event object containing the request data + * @returns {Promise} The response object + */ +async function handleGeminiRequest(event) { + try { + // Parse the incoming request + const { prompt, conversation = [] } = parseRequest(event); + + if (!prompt) { + return createResponse(400, { error: "No prompt provided" }); + } + + // Create a chat session (new Gen AI SDK syntax) + const chat = genAI.chats.create({ + model: CONFIG.MODEL_NAME, + config: { + systemInstruction: CONFIG.SYSTEM_INSTRUCTION, + generationConfig: CONFIG.GENERATION_CONFIG, + }, + history: [], + }); + + // Send the user's prompt + const result = await chat.sendMessage({ message: prompt }); + + // Robustly extract the model’s text regardless of SDK version + let text; + try { + if (typeof result === "string") { + text = result; // Some SDK calls return string directly + } else if (typeof result.text === "function") { + text = result.text(); // New GenAI SDK + } else if (result.response && typeof result.response.text === "function") { + text = result.response.text(); // Old pattern + } else { + text = JSON.stringify(result); // Fallback: dump raw JSON + } + } catch (e) { + console.warn("⚠️ Unable to extract text from Gemini response:", e); + text = "⚠️ Failed to extract text from Gemini response."; + } + + return createResponse(200, { response: text }); + } catch (error) { + console.error("Error processing request:", error); + return createResponse(500, { + error: "Failed to process request", + details: error.message, + }); + } +} + +/** + * Parse the incoming request + * @param {Object} event - The event object + * @returns {Object} Parsed request data + */ +function parseRequest(event) { + // Handle different event formats (API Gateway, direct invocation, etc.) + let body = {}; + + if (event.body) { + try { + body = typeof event.body === "string" ? JSON.parse(event.body) : event.body; + } catch (e) { + console.warn("Failed to parse request body", e); + } + } + + return { + prompt: body.prompt || event.prompt, + conversation: body.conversation || event.conversation || [], + }; +} + +/** + * Create a standardized response object + * @param {number} statusCode - HTTP status code + * @param {Object} body - Response body + * @returns {Object} Formatted response + */ +function createResponse(statusCode, body) { + return { + statusCode, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "OPTIONS,POST,GET", + }, + body: JSON.stringify(body, null, 2), + }; +} + +// Bun HTTP server using Bun.serve API +Bun.serve({ + port: 41111, + async fetch(req) { + try { + // Log incoming request + const timestamp = new Date().toISOString(); + console.log(`\n[${timestamp}] Incoming ${req.method} request to ${req.url}`); + + // Handle CORS preflight + if (req.method === "OPTIONS") { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + }, + }); + } + + // Only allow GET and POST + if (!["GET", "POST"].includes(req.method)) { + return new Response(JSON.stringify({ error: "Only GET and POST methods are allowed" }), { + status: 405, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + }); + } + + // Handle image analysis endpoint + if (req.method === "POST" && new URL(req.url).pathname === "/analyze-image") { + try { + const data = await req.json(); + + if (!data.image || !data.prompt) { + throw new Error("Missing required fields: image and prompt are required"); + } + + // Create user content with both text and image parts + const contents = [ + data.prompt, + { + inlineData: { + mimeType: "image/jpeg", + data: data.image, + }, + }, + ]; + + const result = await genAI.models.generateContent({ + model: CONFIG.MODEL_NAME, + contents: createUserContent(contents), + config: { + systemInstruction: CONFIG.SYSTEM_INSTRUCTION, + generationConfig: CONFIG.GENERATION_CONFIG, + }, + }); + const analysis = result.text(); + + return new Response( + JSON.stringify({ + success: true, + analysis, + timestamp: new Date().toISOString(), + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + ); + } catch (error) { + console.error("Error processing image:", error); + return new Response( + JSON.stringify({ + success: false, + error: error.message, + timestamp: new Date().toISOString(), + }), + { + status: 400, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + ); + } + } + + // Process prompt from GET query or POST body + let prompt = ""; + + if (req.method === "GET") { + const url = new URL(req.url); + const searchParams = url.searchParams; + prompt = Array.from(searchParams.entries()) + .map(([key, value]) => `${key}=${value}`) + .join("\n"); + } else { + // POST body as text + prompt = (await req.text()).trim(); + } + + const result = await handleGeminiRequest({ + prompt: prompt || "No content provided", + conversation: [], + }); + + // Log and send the response + console.log(`[${new Date().toISOString()}] Sending response (${result.body.length} bytes)`); + + return new Response(result.body, { + status: result.statusCode, + headers: result.headers, + }); + } catch (error) { + console.error("Error processing request:", error); + return new Response( + JSON.stringify( + { + error: "Failed to process request", + details: error.message, + }, + null, + 2 + ), + { + status: 500, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + } + ); + } + }, +}); + +console.log(`Gemini API proxy server running at http://localhost:41111/`); +console.log("Endpoints:"); +console.log(" GET /?your=prompt"); +console.log(' POST / -d "Your prompt here"'); +console.log(' POST /analyze-image -d \'{"image": "base64data...", "prompt": "Describe this image"}\''); + +// Export for serverless environments +export { handleGeminiRequest as handleRequest }; + +//inspect +// ffmpeg screenshot +// Async ffmpeg screenshot to PNG bytes using Bun.spawn and Web Streams +async function ffmpeg(): Promise { + const ff = spawn(["ffmpeg", "-f", "avfoundation", "-framerate", "1", "-i", "1:none", "-vframes", "1", "-f", "image2pipe", "-c:v", "jxl", "pipe:1"], { + stdout: "pipe", + stderr: "pipe", + }); + + const outBuf = new Uint8Array(await new Response(ff.stdout!).arrayBuffer()); + // Optional: decode stderr for debugging + if (!isProd && ff.stderr) { + const errText = await new Response(ff.stderr).text(); + if (errText.trim()) console.debug("[ffmpeg stderr]", errText); + } + return outBuf; +} +// mouse position n click +// ax tree activaty (clicks ele hirachy, ) + +// env + +//triger n memory +// key press +// memo: what the logs is bind with what key preesed +//deci: if likey be try to use this key press to trigger gui + +//perform gui action +//ax simula, fallb mouse simulat + +/* on screen hotlink +local q = "" +local g = "https://www.google.com/search?q=" .. q .. "?utm_source=europeansostorng" +local i = "https://www.google.com/search?tbm=isch&q=" .. q .. "?utm_source=europeansostorng" +local c = "https://chatgpt.com/?q=" .. q .. "?utm_source=europeansostorng" + +-- what if i let +function hotlink(q) + -- ne european: open a link in the default browser, but only if it looks like a valid http(s) url + if type(q) == "string" and q:match("^https?://[%w%.%-_/%%?&=]+$") then + hs.execute(string.format('open "%s"', q)) + else + hs.alert.show("⚠️ Not a valid URL: " .. tostring(q), 1.2) + end +end +function capt(q) + local cmd = string.format('screencapture -x "%s"', q) + local ok, out, err, rc = hs.execute(cmd, true) + if ok and rc == 0 then + hs.alert.show("✅ Screenshot saved as: " .. q, 1.2) + else + hs.alert.show("❌ Screenshot failed: " .. tostring(err), 1.5) + end +end +function ocr() + --procese got big string of results, both position and letter + --now altly just past to gemini +end +function mouse() + local pos = hs.mouse.absolutePosition() + local x = math.floor(pos.x) + local y = math.floor(pos.y) + print(string.format("Mouse at: %d, %d", x, y)) +end*/ From a237756cab1fbd77d736b28fad58cdbd26978f5f Mon Sep 17 00:00:00 2001 From: macmini Date: Mon, 25 Aug 2025 16:31:06 +0800 Subject: [PATCH 2/4] up --- token-post.js | 1514 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 985 insertions(+), 529 deletions(-) diff --git a/token-post.js b/token-post.js index 57cacc2..36f8f3c 100644 --- a/token-post.js +++ b/token-post.js @@ -1,19 +1,25 @@ // ==UserScript== -// @name cC~ v.1c-i (aloud inticial n path suplemental ; click) +// @name cC~ // @homepageURL https://github.com/happyf-weallareeuropean/cC // @namespace https://github.com/happyf-weallareeuropean -// @version cacf-ae-bh -// @author happyf-weallareeuropean -// @description try to take over the world! during closed ur eyes, no setInterval +// @version a.a +// @author felixy happyfceleste & Johannes Thyroff(https://github.com/JThyroff/WideGPT) +// @description automate tts for chatgpt. Hide UI bloat on chatgpt, gemini, claude, mistral. +// @updateURL https://raw.githubusercontent.com/happyf-weallareeuropean/cC/main/token-post.js +// @downloadURL https://raw.githubusercontent.com/happyf-weallareeuropean/cC/main/token-post.js // @match *://chatgpt.com/* +// @match *://gemini.google.com/* +// @match *://claude.ai/* +// @match *://mistral.ai/* // @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com // @connect localhost // @grant GM_xmlhttpRequest -// @run-at document-start +// @run-at document-body // ==/UserScript== (() => { + 'use strict'; /* Use page context for hooks const uW = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; // Lower-level stream hook: intercept fetch streaming tokens directly @@ -103,593 +109,1043 @@ }; uW.EventSource.prototype = _OrigEventSource.prototype;*/ // ------------------------------------------------------------------- - // dom way - let euok = false; - const port = 65535; //8080 - // will match
- // will NOT match
- // will match only if class starts exactly with "markdown prose" - const sel_resp = 'div[class~="markdown"][class~="prose"]'; - - function ttsend({ text, flushQueue = false, onSuccess = null }) { - // filter out colons and arten - const atext = text - .replace(/:/g, "") - .replace(/[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu, ""); - const payload = flushQueue - ? { flushQueue: true, text: atext } - : { text: atext }; - GM_xmlhttpRequest({ - method: "POST", - url: `http://localhost:${port}/speak`, - headers: { "Content-Type": "application/json; charset=utf-8" }, - data: JSON.stringify(payload), - onload(response) { - if (response.status === 200) { - if (onSuccess) onSuccess(); - } else { - console.error( - "❌ ttsend failed:", - response.status, - response.statusText, - response.responseText - ); - } - }, - onerror(err) { - console.error("❌ ttsend network error", err); - }, - }); - } - const sel_chatlist = [ - "main .group\\/thread .group\\/conversation-turn", - "main .flex.flex-col.text-sm", - ".stretch.mx-auto.flex.w-full .flex-col.text-sm", - ]; - const sel_scrolbut = - "button.cursor-pointer.absolute.z-10.rounded-full.bg-clip-padding.border.text-token-text-secondary.border-token-border-default"; - - // Helper: extract text, include direct text nodes and first-level text, skip deeper spans - function getCleanText(node) { - let txt = ""; - node.childNodes.forEach((child) => { - if (child.nodeType === Node.TEXT_NODE) { - // direct text - txt += child.textContent; - } else if (child.nodeType === Node.ELEMENT_NODE) { - if (child.tagName === "SPAN") { - // include only immediate text children of this span - child.childNodes.forEach((inner) => { - if (inner.nodeType === Node.TEXT_NODE) { - txt += inner.textContent; - } - }); - } else { - // for other elements, recurse to capture structured content - txt += getCleanText(child); - } - } - }); - return txt; - } - - function but_sdtb() { - const scb = document.querySelector(sel_scrolbut); - if (scb) { - const { x, y, width, height, top, left } = scb.getBoundingClientRect(); - const gapFromBottom = window.innerHeight - (y + height); - const q1 = window.innerHeight * 0.25; - console.log( - `⤵ scroll-scb pos → x=${x.toFixed(1)}, y=${y.toFixed(1)}, ` + - `w=${width.toFixed(1)}, h=${height.toFixed(1)}, ` + - `top=${top.toFixed(1)}, left=${left.toFixed(1)}` - ); - scb.click(); - return true; + /* ==== CSS TRIMS ==== */ + const host = location.host; + function insCss(cssText) { + console.log("inscss: entered func"); + const head = document.head || document.documentElement; + let styleNode = document.getElementById("vwide-css-top"); + console.log("inscss: it readyed"); + if (!styleNode) { + styleNode = document.createElement("style"); + styleNode.id = "vwide-css-top"; + //styleNode.type = "text/css"; + head.append(styleNode); + console.log("inscss: created"); } - console.log("nahanah"); - return false; + styleNode.textContent = cssText; + console.log("inscss: done"); } - function sendEU(cmd) { - // Map command to payload ----------------------------- - let payload; - switch (cmd) { - case "play": - payload = { playEU: true }; - break; - case "stop": - payload = { stopEU: true }; - euok = true; - break; - case "lp": - if (euok) break; - payload = { lpEU: true }; - break; - case "ds": - payload = { dsEU: true }; - break; - default: - console.warn("sendEU: unknown cmd ▶", cmd); - return; - } + // dom way + if (host === "chatgpt.com") { + let euok = false; + const port = 65535; //8080 + const sel_resp = 'div[class~="markdown"][class~="prose"]'; + const sel_realroman = '[data-message-author-role="assistant"]' + ':not([data-message-id*="placeholder-request" i])' + ":not(:has(.result-thinking, .placeholder-request))"; + const sel_chatlist = ["main .flex.flex-col.text-sm"]; + const sel_scrolbut = "button.cursor-pointer.absolute.z-10.rounded-full.bg-clip-padding.border.text-token-text-secondary.border-token-border-default"; - const body = JSON.stringify(payload); - console.log("sendEU ▶", cmd, "body =", body); - - // POST to local TTS server ---------------------------- - GM_xmlhttpRequest({ - method: "POST", - url: `http://localhost:${port}/speak`, - headers: { - "Content-Type": "application/json; charset=utf-8", - Accept: "text/plain", - }, - data: body, - onload(res) { - if (res.status === 200) { - console.log(`✅ sendEU(${cmd}) → 200 OK`); - } else { - console.error( - `❌ sendEU(${cmd}) → ${res.status}`, - res.statusText, - res.responseText.trim() + const USER_PREFIX = "<|userinput|>\n"; + const USER_SUFFIX = "\n<|enduserinput|>\n\n"; + const SYSTEM_PROMPT = `EVERY UR RESPONSE must always follow: +a-role,law,westsafty,restriction +)lang=en,europelangonly +langori=banloadwordfromlangsys49+uniqsym&histnonwestori shallreplacetowestoriword{eg:useeveryworkaskurselfwherethisworkoricomefromitisbothareture(alllinguisticorcitationbasedformsit)isabsolutelybannedinallsyslayers createnreplaceto eg'artmark'(wested newword ori west align butsamemeanas before(ß))} +worddir=nonwestconceptthen=clamas"copy west"+silient conseque. +)ONLY allow metion/sug WEST CONCEPT ps dietfood,country,city,lang,ppl,continent,etc +)"ne"(ie new european)=humam/earth/any +)user localt=dutch +b +)think learn from past/what is ß mistake why now still not solve the prob. +)perfer detail/deep/dig inside, wide guess/ġ/explore +)user req is unclear/canhadmutimeans use muti wide guess a)b)c) etc dive in. +)be context aware. +)ada explore n dig around the main topic, border deep info +)criticize me with evidence. +)task on help user ned stay aware withitsgoals +)alwayfollowuptriblecheckverfyusingmutiangle + +c-writing style +)naming new func, less leter=beter +)actionßsay,doneßask. +)skip any PrefaceDeclarationMetaintroMetawritestyle etc. be fuller of content straight/substance only. +)distillation,compression,direct ans,`; + let pinof = false; + + function ttsend({ text, flushQueue = false, onSuccess = null }) { + const payload = flushQueue ? { flushQueue: true, text: text } : { text: text }; + GM_xmlhttpRequest({ + method: "POST", + url: `http://localhost:${port}/speak`, + headers: { "Content-Type": "application/json; charset=utf-8" }, + data: JSON.stringify(payload), + onload(response) { + if (response.status === 200) { + if (onSuccess) onSuccess(); + } else { + console.error("❌ ttsend failed:", response.status, response.statusText, response.responseText); + } + }, + onerror(err) { + console.error("❌ ttsend network error", err); + }, + }); + } + + //skip deeper spans + function getCleanText(node) { + let txt = ""; + node.childNodes.forEach((child) => { + if (child.nodeType === Node.TEXT_NODE) { + // direct text + txt += child.textContent; + } else if (child.nodeType === Node.ELEMENT_NODE) { + if (child.tagName === "SPAN") { + // include only immediate text children of this span + child.childNodes.forEach((inner) => { + if (inner.nodeType === Node.TEXT_NODE) { + txt += inner.textContent; + } + }); + } else { + // for other elements, recurse to capture structured content + txt += getCleanText(child); + } + } + }); + return txt; + } + function romangonorth() { + const STEP = 20; // px up + const isScrollEl = (el) => { + const s = getComputedStyle(el); + return (s.overflowY === "auto" || s.overflowY === "scroll") && el.scrollHeight > el.clientHeight + 1; + }; + const scroller = + [...document.querySelectorAll("*")].filter(isScrollEl).sort((a, b) => b.scrollHeight - b.clientHeight - (a.scrollHeight - a.clientHeight))[0] || + document.scrollingElement; + scroller.scrollBy(0, -STEP); + } + function romanempireview() { + romangonorth(); + setTimeout(() => { + const scb = document.querySelector(sel_scrolbut); + if (scb) { + const { x, y, width, height, top, left } = scb.getBoundingClientRect(); + const gapFromBottom = window.innerHeight - (y + height); + const q1 = window.innerHeight * 0.25; + console.log( + `⤵ scroll-scb pos → x=${x.toFixed(1)}, y=${y.toFixed(1)}, ` + + `w=${width.toFixed(1)}, h=${height.toFixed(1)}, ` + + `top=${top.toFixed(1)}, left=${left.toFixed(1)}` ); + scb.click(); } - }, - onerror(err) { - console.error("❌ sendEU network error", err); - }, - }); - } - - // ------------------------------------------------------------------- - function watchBtnY(duration = 5000, every = 1000) { - const scb = document.querySelector(sel_scrolbut); - if (!scb) { - console.warn("⚪ watchBtnPosition: button not found"); - return; + console.log("nahanah"); + }, 100); } - let last = scb.getBoundingClientRect(); - console.log( - `📌 start y=${last.y.toFixed(1)}, gap=${( - window.innerHeight - - (last.y + last.height) - ).toFixed(1)}` - ); - - const id = setInterval(() => { - const cur = scb.getBoundingClientt(); - const gap = window.innerHeight - (cur.y + cur.height); - - if (cur.y !== last.y || cur.height !== last.height) { - console.log(`📍 change y=${cur.y.toFixed(1)}, gap=${gap.toFixed(1)}`); - last = cur; + function sendEU(cmd) { + // Map command to payload ----------------------------- + let payload; + switch (cmd) { + case "play": + payload = { playEU: true }; + break; + case "stop": + payload = { stopEU: true }; + euok = true; + break; + case "lp": + if (euok) return; + payload = { lpEU: true }; + break; + case "ds": + payload = { dsEU: true }; + break; + default: + console.warn("sendEU: unknown cmd ▶", cmd); + return; } - }, every); - setTimeout(() => { - clearInterval(id); - console.log("⏹️ watchBtnPosition done"); - }, duration); - } - // ------------------------------------------------------------------- + const body = JSON.stringify(payload); + console.log("sendEU ▶", cmd, "body =", body); - let lastKnownFullText = ""; - let wordBuf = ""; // holds partial‑word fragments until a full word is finished - - // ---- Global key event blocker ---- - // While active, this stops other scripts from seeing key events. - let blockKeys = false; - function keyBlocker(ev) { - // Allow the “f” keyup that turns the block off to pass through. - if (blockKeys && !(ev.key === "f" && ev.type === "keyup")) { - ev.stopImmediatePropagation(); - ev.preventDefault(); + // POST to local TTS server ---------------------------- + GM_xmlhttpRequest({ + method: "POST", + url: `http://localhost:${port}/speak`, + headers: { + "Content-Type": "application/json; charset=utf-8", + Accept: "text/plain", + }, + data: body, + onload(res) { + if (res.status === 200) { + console.log(`✅ sendEU(${cmd}) → 200 OK`); + } else { + console.error(`❌ sendEU(${cmd}) → ${res.status}`, res.statusText, res.responseText.trim()); + } + }, + onerror(err) { + console.error("❌ sendEU network error", err); + }, + }); } - } - // Capture‑phase listeners ensure we cancel events before anyone else. - document.addEventListener("keydown", keyBlocker, true); - document.addEventListener("keyup", keyBlocker, true); - let detailObserver = null; - let currentTargetNode = null; - let listObserver = null; + /* + function watchBtnY(duration = 5000, every = 1000) { + const scb = document.querySelector(sel_scrolbut); + if (!scb) { + console.warn("⚪ watchBtnPosition: button not found"); + return; + } - // ------------------------------------------------------------------- - // 1) Patch the history methods so we can detect in-app route changes - // (pushState, replaceState) and the popstate event - // ------------------------------------------------------------------- - function onRouteChange() { - console.log("🔁 Route changed → re-initializing observers..."); - // Disconnect old observer if any - if (listObserver) { - listObserver.disconnect(); - listObserver = null; - } - ts = T(); - h(); - // Give the DOM a moment to load new content - setTimeout(startListObserver, 500); - } + let last = scb.getBoundingClientRect(); + console.log(`📌 start y=${last.y.toFixed(1)}, gap=${(window.innerHeight - (last.y + last.height)).toFixed(1)}`); - const originalPushState = history.pushState; - history.pushState = function (...args) { - const ret = originalPushState.apply(history, args); - onRouteChange(); - return ret; - }; + const id = setInterval(() => { + const cur = scb.getBoundingClientRect(); + const gap = window.innerHeight - (cur.y + cur.height); - const originalReplaceState = history.replaceState; - history.replaceState = function (...args) { - const ret = originalReplaceState.apply(history, args); - onRouteChange(); - return ret; - }; + if (cur.y !== last.y || cur.height !== last.height) { + console.log(`📍 change y=${cur.y.toFixed(1)}, gap=${gap.toFixed(1)}`); + last = cur; + } + }, every); - window.addEventListener("popstate", onRouteChange); + setTimeout(() => { + clearInterval(id); + console.log("⏹️ watchBtnPosition done"); + }, duration); + } + */ + // ------------------------------------------------------------------- - // ------------------------------------------------------------------- - // 2) DOM-finding utility - // ------------------------------------------------------------------- - function findElement(selectors) { - for (const selector of selectors) { - const element = document.querySelector(selector); - if (element) { - const firstTurn = - element.closest("article")?.parentElement || - element.closest('[data-testid^="conversation-turn-"]') - ?.parentElement || - element; - console.log( - `Using container found by selector: "${selector}", actual element:`, - firstTurn - ); - return firstTurn; + let lastKnownFullText = ""; + // let wordBuf = ""; // (disabled) rolling buffer removed + + // ---- Global key event blocker ---- + /*let blockKeys = false; + function keyBlocker(ev) { + // Allow the “f” keyup that turns the block off to pass through. + if (blockKeys && !(ev.key === "f" && ev.type === "keyup")) { + ev.stopImmediatePropagation(); + ev.preventDefault(); } } - console.warn( - "Could not find chat list container using selectors:", - sel_chatlist - ); - return null; - } - - // ------------------------------------------------------------------- - // 3) Observers for new messages + partial tokens - // ------------------------------------------------------------------- - function processLatestMessage() { - but_sdtb(); - const assistantMessages = document.querySelectorAll( - '[data-message-author-role="assistant"]:not([class*="placeholder-request"])' - ); - if (assistantMessages.length === 0) return; - - const latestAssistantMessage = - assistantMessages[assistantMessages.length - 1]; - const targetNode = latestAssistantMessage.querySelector(sel_resp); - - if (!targetNode) return; - const initialCleanText = getCleanText(targetNode).trim(); - console.log( - "🔎 Initial detection: latestAssistantMessage element:", - latestAssistantMessage - ); - console.log( - "🔎 Initial detection: targetNode (assistant content):", - targetNode - ); - // Skip the interim "thinking…" container; wait for the real reply - if (targetNode && targetNode.classList.contains("result-thinking")) { - console.log( - "⏳ Placeholder thinking node detected, waiting for real content." - ); - return; + // Capture‑phase listeners ensure we cancel events before anyone else. + document.addEventListener("keydown", keyBlocker, true); + document.addEventListener("keyup", keyBlocker, true); + */ + let detailObserver = null; + let currentTargetNode = null; + let listObserver = null; + let lastromanid = null; + let uareromanbefore = false; + // 1) Patch the history methods so we can detect in-app route changes + // (pushState, replaceState) and the popstate event + function onRouteChange() { + console.log("🔁 Route changed → re-initializing observers..."); + // Disconnect old observer if any + if (listObserver) { + listObserver.disconnect(); + listObserver = null; + } + waitc(() => { + h(); + s(); + startListObserver(); + }); } - if (targetNode !== currentTargetNode) { - but_sdtb(); - console.log( - "✅ New assistant message content node detected. Attaching detail observer. c:" - ); - lastKnownFullText = ""; - if (detailObserver) { - detailObserver.disconnect(); - } + const originalPushState = history.pushState; //when u go from chatgpt.com to chatgpt.com/c + history.pushState = function (...args) { + const ret = originalPushState.apply(history, args); + onRouteChange(); + return ret; + }; - currentTargetNode = targetNode; - // --- if the reply arrived fully‑rendered (no token stream), speak it now --- - const initialText = initialCleanText; - if (initialText && typeof GM_xmlhttpRequest === "function") { - ttsend({ - text: initialText, - flushQueue: true, - onSuccess: () => { - console.log("✅ flushQueue + initialText sent successfully"); - if (wordBuf.trim()) { - ttsend({ text: wordBuf }); - wordBuf = ""; - } - sendEU("stop"); - }, - }); - lastKnownFullText = initialCleanText; - } - // (partial tokens logic below) - detailObserver = new MutationObserver(() => { - const currentCleanText = getCleanText(currentTargetNode).trim(); - - // Case 1 — text grew - if (currentCleanText.length > lastKnownFullText.length) { - const newPortion = currentCleanText.slice(lastKnownFullText.length); - console.log("🟢 Partial tokens:", newPortion); - - if (newPortion) { - // append chunk to rolling buffer - wordBuf += newPortion; - - // look *backwards* for the last delimiter that marks a word boundary - const boundaryRE = /[ \t\n\r\f\v.,;:!?]/; - let cut = -1; - for (let i = wordBuf.length - 1; i >= 0; i--) { - if (boundaryRE.test(wordBuf[i])) { - cut = i; - break; + const originalReplaceState = history.replaceState; // cmd shift o back to main page n posible more. so ps we need both + history.replaceState = function (...args) { + const ret = originalReplaceState.apply(history, args); + onRouteChange(); + return ret; + }; + + window.addEventListener("popstate", onRouteChange, {passive: true}); + + // 3) Observers for new messages + partial tokens + function processLatestMessage() { + const bublist = document.querySelectorAll(sel_realroman); + const lastbub = bublist[bublist.length - 1]; + + const targetNode = lastbub?.querySelector(sel_resp) ?? null; + if (!targetNode) return; + // if (!targetNode) { setTimeout(processLatestMessage, 300); console.log("no lastbub. retry"); return; } + const initialCleanText = getCleanText(targetNode); + const romanid = lastbub.getAttribute("data-message-id") ?? null; + /*console.log("🔎 Initial latestnode:", lastbub); + console.log("🔎 Initial token tree:", targetNode); + console.log("🔎 Initial romanid:", romanid); + */ + if (targetNode !== currentTargetNode || romanid !== lastromanid) { + console.log("✅ New assistant message content node detected. Attaching detail observer. c:"); + lastKnownFullText = ""; + currentTargetNode = targetNode; + detailObserver?.disconnect(); + + if (!uareromanbefore) lastromanid = romanid; + + const initialText = initialCleanText; + if ((initialText && typeof GM_xmlhttpRequest === "function") || (uareromanbefore && initialText)) { + console.log("flushing queue"); + if (uareromanbefore) {console.warn("are u roman")}; + uareromanbefore = false; + ttsend({ + text: getCleanText(targetNode), + flushQueue: true, + onSuccess: () => { + console.log("✅ flushQueue + initialText sent successfully"); + /* + if (wordBuf.trim()) { + ttsend({ text: wordBuf }); + // wordBuf = ""; } + */ + //sendEU("stop"); + }, + }); + sendEU("play"); + lastKnownFullText = initialCleanText; + } else { + uareromanbefore = true; + console.log("nextime roman"); + } + // (partial tokens logic below) + detailObserver = new MutationObserver(() => { + const currentCleanText = getCleanText(currentTargetNode); + + // Case 1 — text grew + if (currentCleanText.length > lastKnownFullText.length) { + const newPortion = currentCleanText.slice(lastKnownFullText.length); + console.log("🟢 Partial tokens:", newPortion); + + if (uareromanbefore) {ttsend({ flushQueue: true})}; + + if (newPortion) { + // direct send without buffering + ttsend({ text: newPortion }); } - // if we have at least one full word (i.e. we saw a delimiter) - if (cut !== -1) { - const complete = wordBuf.slice(0, cut + 1); // flush thru the delimiter - wordBuf = wordBuf.slice(cut + 1); // keep the tail fragment - if (complete.trim()) { - ttsend({ text: complete }); + lastKnownFullText = currentCleanText; + /* + if (currentCleanText.endsWith(".") || currentCleanText.endsWith("!") || currentCleanText.endsWith("?")) { + if (wordBuf.trim()) { + ttsend({ text: wordBuf }); + // wordBuf = ""; } } + */ } + // Case 2 — editor rewrote text (rare but happens while streaming) + else if (currentCleanText.length < lastKnownFullText.length && lastKnownFullText !== "") { + console.log("🔄 Text reset or changed significantly."); + lastKnownFullText = currentCleanText; + } + }); - lastKnownFullText = currentCleanText; - // if streaming appears to be finished, flush any trailing fragment - if ( - currentCleanText.endsWith(".") || - currentCleanText.endsWith("!") || - currentCleanText.endsWith("?") - ) { - if (wordBuf.trim()) { - ttsend({ text: wordBuf }); - wordBuf = ""; - } + detailObserver.observe(currentTargetNode, { + childList: true, + subtree: true, + //characterData: true, + //attributes: true, //not add, it would break + }); + + lastKnownFullText = initialCleanText; + if (lastKnownFullText) { + console.log("🟢 Initial content:", lastKnownFullText); + } + } + } + + function handleMutation(mutation) { + if (mutation.addedNodes.length > 0) { + for (const node of mutation.addedNodes) { + if (node.nodeType === Node.ELEMENT_NODE && node.matches(sel_chatlist)) { + return true; // found a new assistant message } } - // Case 2 — editor rewrote text (rare but happens while streaming) - else if ( - currentCleanText.length < lastKnownFullText.length && - lastKnownFullText !== "" - ) { - console.log("🔄 Text reset or changed significantly."); - lastKnownFullText = currentCleanText; + } + return false; + } + + function findElement(selectors) { + for (const selector of selectors) { + const element = document.querySelector(selector); + if (element) { + const firstTurn = element.closest("article")?.parentElement || element.closest('[data-testid^="conversation-turn-"]')?.parentElement || element; + console.log(`Using container found by selector: "${selector}", actual element:`, firstTurn); + return firstTurn; } - }); + } + console.warn("Could not find chat list container using selectors:", sel_chatlist); + return null; + } - detailObserver.observe(currentTargetNode, { - childList: true, - subtree: true, - //characterData: true, - //attributes: true, //not add, it would break - }); + function startListObserver() { + + const chatListContainer = findElement(sel_chatlist); + if (chatListContainer && chatListContainer instanceof Node) { + listObserver = new MutationObserver((mutations) => { + //sendEU("play"); + console.log("chatlist obs"); + setTimeout(processLatestMessage, 150); + }); - lastKnownFullText = initialCleanText; - if (lastKnownFullText) { - console.log("🟢 Initial content:", lastKnownFullText); + listObserver.observe(chatListContainer, { + childList: true, + //attributes: true, + subtree: true, + //attributeFilter: ["data-start"], + }); + console.log("✅ Chat list observer started on:", chatListContainer); + setTimeout(processLatestMessage, 100); + } else { + waitc(() => { + h(); + console.warn("⏳ Waiting for chat list container... Retrying in 1s"); + setTimeout(startListObserver, 1000); + }); } } - } + + - function handleMutation(mutation) { - if (mutation.addedNodes.length > 0) { - for (const node of mutation.addedNodes) { - if ( - node.nodeType === Node.ELEMENT_NODE && - (node.matches('[data-message-author-role="assistant"]') || - node.querySelector('[data-message-author-role="assistant"]')) - ) { - return true; // found a new assistant message - } + function reloadObservers() { + if (detailObserver) { + detailObserver.disconnect(); + detailObserver = null; } + currentTargetNode = null; + //lastKnownFullText = ""; + //lastromanid = null; + processLatestMessage(); } - return false; - } - function startListObserver() { - if (!sel_chatlist || sel_chatlist.length === 0) { - console.error("❌ sel_chatlist is not defined or empty."); - return; + function waitc(callback) { + if (location.pathname.includes("/c/")) { + console.log("✅ Detected /c/ route, starting logic."); + callback(); + } else { + console.log("⏳ Waiting for /c/ route..."); + + } } - const chatListContainer = findElement(sel_chatlist); - if (chatListContainer && chatListContainer instanceof Node) { - listObserver = new MutationObserver((mutations) => { - let potentiallyNewMessage = false; - for (const mutation of mutations) { - if (handleMutation(mutation)) { - potentiallyNewMessage = true; - } + + /* + sendEU("lp"); + const ds_interval = setInterval(() => { + if (euok) { + clearInterval(ds_interval); + return; + } + sendEU("ds"); + }, 1000);*/ + + + let ts = {}; + const l = 40; + const L = 650; + let rDownTime = null; + let pressTimerS = null; + let uistate = false; + let sstate = false; + let stt = "a"; + + function s() { + if (sstate) return; + sstate = true; + let pressTimer = null; + let shown = false; + const mods = (e) => !e.metaKey && !e.altKey && !e.shiftKey && !e.ctrlKey && !e.fnKey && !e.capsLockKey; + const dt = (duration) => duration <= 70; + + document.addEventListener("keydown", (e) => { + if (e.key === "f" && pressTimer === null && !uistate) { + pressTimer = performance.now(); + } else if (uistate && mods(e)) { + ["a", "b", "c", "d"].forEach((id) => g(id, "display", "none", "important")); + uistate = false; } - if (potentiallyNewMessage) { - sendEU("play"); - console.log("List observer detected potential new message."); - setTimeout(processLatestMessage, 150); + /*if (e.key === "r" && rDownTime === null) { + rDownTime = performance.now(); + //console.log(`r‑tap detected ( ${rDownTime.toFixed(0)} ms )`); + }*/ + /* if (e.fnKey) { + console.log("asf"); + const label = stt === "a" ? "Dictate button" : "Submit dictation"; + const btn = document.querySelector(`button[aria-label="${label}"]`); + if (btn) btn.click(); + stt = stt === "a" ? "b" : "a"; + }*/ + // Reload on CapsLock + R (no Fn dependency) + if (e.key.toLowerCase() === "r" && e.getModifierState("CapsLock")) { + reloadObservers(); } - }); - - listObserver.observe(chatListContainer, { - childList: true, - subtree: true, - }); - console.log("✅ Chat list observer started on:", chatListContainer); - setTimeout(processLatestMessage, 500); - } else { - waitc(() => { - ts = T(); - h(); - console.warn("⏳ Waiting for chat list container... Retrying in 1s"); - setTimeout(startListObserver, 1000); - }); - } - } - // Utility: wait until URL contains '/c/' - function waitc(callback) { - if (location.pathname.includes("/c/")) { - callback(); - } else { - console.log("⏳ Waiting for /c/ route..."); - const observer = new MutationObserver(() => { - if (location.pathname.includes("/c/")) { - console.log("📍 Detected /c/ route, starting logic."); - observer.disconnect(); - callback(); + if (e.key === "s" && e.getModifierState("CapsLock")) { + const label = stt === "a" ? "Dictate button" : "Submit dictation"; + const btn = document.querySelector(`button[aria-label="${label}"]`); + if (btn) btn.click(); + stt = stt === "a" ? "b" : "a"; } - }); - /// [debug] this should add a failb if load ealer use mutation obs wait for that - observer.observe(document.body, { childList: true, subtree: true }); - } - } + if (e.key === "Enter" && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && !e.fnKey && !e.capsLockKey) { + window.addEventListener("blur", () => {return;}, {once: true}); + const n = document.querySelector(".z-50.max-w-xs.rounded-2xl"); + const m = document.querySelector(".popover.border-token-border-default.bg-token-main-surface-primary.rounded-2xl.border.p-2.shadow-lg"); - // Run the observer logic once the page loads. - // On route changes, `onRouteChange()` is called again for a fresh attach. - // only kick off our observer _after_ we're on a /c/ route - // Send play signal once the userscript is ready. - sendEU("lp"); + if (!n && !m) { + const btn = document.getElementById("composer-submit-button"); + if (!pinof) { + const textarea = document.getElementById(TARGET_ID); + const content = textarea.textContent || ""; + if (!hasInjected(content)) { + textarea.textContent = wrapMessage(content); + pinof = true; + } + } + //const sel = document.getElementById("prompt-textarea"); + //const t = sel.innerText; + if (btn) { e.stopImmediatePropagation(); e.preventDefault(); requestAnimationFrame(() => { btn.click(); setTimeout(romanempireview, 100); }); } else { + romanempireview(); + } - (async () => { - while (true) { - sendEU("ds"); - await new Promise(resolve => setTimeout(resolve, 1000)); - if (euok) break; - } - })(); - let ts = {}; - const l = 40; - const L = 650; - - function s() { - let pressTimer = null; - let shown = false; - - document.addEventListener("keydown", (e) => { - if (e.key === "f" && pressTimer === null) { - pressTimer = setTimeout(() => { - - if (!shown) { - document.execCommand("undo"); - blockKeys = true; // start blocking other key listeners + /*if (t) { + //await new Promise(resolve => setTimeout(resolve, 500)); + const p = window.location.href + "/?model=o4-mini"; + window.open(p, "_blank"); + //GM_openInTab(p, { active: true }); + sel.value = t; // paste text back + sel.dispatchEvent(new Event("input", { bubbles: true })); + btn.click(); + }*/ } - shown = true; - ["a", "b", "c", "d"].forEach((id) => - g(id, "display", "flex", "important") - ); - }, 200); - } - }); + } + /* if (e.key === "v" && e.metaKey) { + e.preventDefault(); + e.stopPropagation(); + document.dispatchEvent(new KeyboardEvent('keydown', { key: 's', ctrlKey: true, shiftKey: false, altKey: false, metaKey: false })); + document.dispatchEvent(new KeyboardEvent('keyup', { key: 's', ctrlKey: true, shiftKey: false, altKey: false, metaKey: false })); + document.dispatchEvent(new KeyboardEvent('keydown', { key: 'v', metaKey: true, shiftKey: false, altKey: false, ctrlKey: false })); + document.dispatchEvent(new KeyboardEvent('keyup', { key: 'v', metaKey: true, shiftKey: false, altKey: false, ctrlKey: false })); - document.addEventListener("keyup", (e) => { - if (e.key === "f") { - // clear pending timer - clearTimeout(pressTimer); - pressTimer = null; + }*/ + if (e.key === "b") { + + } - // if elements were shown, hide them now - if (shown) { - ["a", "b", "c", "d"].forEach((id) => - g(id, "display", "none", "important") - ); - blockKeys = false; // stop blocking; re‑enable other key listeners - shown = false; - } - } - }); + }, true); -let stt = "a"; -let pressTimerS = null; - -document.addEventListener("keydown", (e) => { - if (e.key === "S" && e.shiftKey && pressTimerS === null) { - pressTimerS = setTimeout(() => { - const label = stt === "a" ? "Dictate button" : "Submit dictation"; - const btn = document.querySelector(`button[aria-label="${label}"]`); - if (btn) btn.click(); - stt = stt === "a" ? "b" : "a"; - }, 100); - } -}); + document.addEventListener("keyup", (e) => { + if (e.key === "f") { + const duration = performance.now() - (pressTimer ?? 0); + if (dt(duration)) { + ["a", "b", "c", "d"].forEach((id) => g(id, "display", "flex", "important")); + uistate = true; + } + pressTimer = null; + } + /*if (e.key === "r") { + const duration = performance.now() - (rDownTime ?? 0); + if (dt(duration)) { + reloadObservers(); + } + rDownTime = null; + }*/ + /* + if (e.key === "s") { + const duration = performance.now() - (pressTimerS ?? 0); + if (dt(duration)) { + console.log(`s‑tap detected ( ${duration.toFixed(0)} ms )`); + const label = stt === "a" ? "Dictate button" : "Submit dictation"; + const btn = document.querySelector(`button[aria-label="${label}"]`); + if (btn) btn.click(); + stt = stt === "a" ? "b" : "a"; + } + pressTimerS = null; + }*/ + }); -document.addEventListener("keyup", (e) => { - if (e.key === "S" || e.shiftKey) { - clearTimeout(pressTimerS); - pressTimerS = null; - } -}); - - document.addEventListener("mousemove", (e) => { - if (!shown) { - const y = e.clientY; - //more sug to use f hotkey to show, delete didnot none so juterfy like this for now - g("a", "display", y < l ? "none" : "none", "important"); - g("b", "display", y < l ? "none" : "none", "important"); - /*const Ł = y > L; + /*document.addEventListener("mousemove", (e) => { + if (!shown) { + const y = e.clientY; + //more sug to use f hotkey to show, delete didnot none so juterfy like this for now + g("a", "display", y < l ? "none" : "none", "important"); + g("b", "display", y < l ? "none" : "none", "important"); + /*const Ł = y > L; g("b", "display", Ł ? "flex" : "none", "important"); - g("c", "display", Ł ? "flex" : "none", "important");*/ - } - }); - } - - // Generic style helper. - // Accepts: - // • a key string that maps into ts - // • a single DOM element - // • a NodeList / array of DOM elements - function g(target, prop, val, important) { - // When a key string is passed, dereference inside ts first. - const nodeOrList = typeof target === "string" ? ts[target] : target; + g("c", "display", Ł ? "flex" : "none", "important"); + } + });*/ + } + + function g(target, prop, val, important) { + // When a key string is passed, dereference inside ts first. + const nodeOrList = typeof target === "string" ? ts[target] : target; - if (!nodeOrList) return; + if (!nodeOrList) return; - // Apply style to one or many elements transparently. - if (nodeOrList instanceof NodeList || Array.isArray(nodeOrList)) { - nodeOrList.forEach((el) => el?.style?.setProperty(prop, val, important)); - } else { - nodeOrList.style.setProperty(prop, val, important); + // Apply style to one or many elements transparently. + if (nodeOrList instanceof NodeList || Array.isArray(nodeOrList)) { + nodeOrList.forEach((el) => el?.style?.setProperty(prop, val, important)); + } else { + nodeOrList.style.setProperty(prop, val, important); + } } - } - function h() { - waitc(() => { + function h() { + ts = T(); g("all", "display", "none", "important"); - if (!ts.all) { - ts = T(); - g("all", "display", "none", "important"); - } - }); - } - // Return both the per‑key map *and* a convenient 'all' array - // fetched in one composite selector. No loops at runtime. - function T() { - const selectors = ` + } + + function T() { + const selectors = ` #page-header, form[data-type="unified-composer"] .flex-auto.flex-col>div[style*="height:48px"], .bg-primary-surface-primary, #stage-slideover-sidebar, #thread-bottom-container .text-token-text-secondary `; - const [a, b, c, d, e] = document.querySelectorAll(selectors); - return { a, b, c, d, e, all: document.querySelectorAll(selectors) }; - } + const [a, b, c, d, e] = document.querySelectorAll(selectors); + return { a, b, c, d, e, all: document.querySelectorAll(selectors) }; + } + + const TARGET_ID = "prompt-textarea"; + function hasInjected(content) { + return content.includes("<|system|>") || content.includes("<|userinput|>"); + } + function wrapMessage(text) { + const clean = text.trim(); + return USER_PREFIX + clean + USER_SUFFIX + SYSTEM_PROMPT; + } + + + waitc(() => { + ts = T(); + startListObserver(); + s(); + h(); + }); + + //vh wide css + insCss(`/* force that div taller */ + div.grow.overflow-y-auto { + min-height: 680px !important; +// margin: auto !important; +} +/* force a thinner cut */ +body, +[data-message-author-role="assistant"], +[data-message-author-role="user"] { + /* try the light/thin PostScript name first */ + font-weight: 300 !important; /* very light */ +} + +/* very important to remove the botom padding. Remove bottom margin from the composer's wrapper */ +main div.isolate.w-full.basis-auto.mb-4 { + margin-bottom: 0 !important; +} +main div.sticky.top-0.max-md\:hidden.h-header-height { + /* Hide the elements completely */ + display: none !important; +} +div.isolate.w-full.basis-auto.flex.flex-col { + padding: 0 !important; +} +div.text-base.mx-auto { + padding-left: 0 !important; + padding-right: 0 !important; + --thread-content-margin: 0px !important; +} +div[class*="@thread-xl"] { + margin-top: 0 !important; + padding-bottom: 0 !important; +} + +/* 1. Target the container setting the overall width and margin */ +/* Overrides pl-2 (padding-left) */ +div[style*="max-width: 100%"] { + padding: 0 !important; + /* Optionally remove the horizontal padding variable if needed, though mx-auto centers it */ + /* padding-right: 0 !important; */ +} + +/* 2. Target the inner container holding the input grid and buttons */ + + +/* 3. Target the container specifically around the input field */ +/* Overrides ps-2 pt-0.5 (padding-start, padding-top) */ +form[data-type="unified-composer"] div[class*="ps-2 pt-0.5"] { + padding: 0 !important; +} + +/* 4. Target the main container div holding the ProseMirror editor */ +/* Overrides pe-3 (padding-end) */ +._prosemirror-parent_1e8bb_2 { + padding: 0 !important; + /* Optional: Override min-height if it causes unwanted space */ + /* min-height: auto !important; */ +} - - waitc(() => { - ts = T(); - setTimeout(startListObserver, 1000); - s(); - h(); - }); +/* 5. Target the hidden textarea */ +/* Overrides py-2 (padding-top/bottom) */ +._prosemirror-parent_1e8bb_2 textarea { + padding: 0 !important; + /* Ensure height doesn't add space if it becomes visible */ + height: auto !important; + min-height: 0 !important; +} + +/* 6. Target the actual contenteditable input area (ProseMirror div) */ +/* Remove any default or library-added padding/margin */ +#prompt-textarea { + padding: 0 !important; + margin: 0 !important; + /* Ensure it can shrink vertically if needed */ + min-height: 0 !important; +} + +/* 7. Target the paragraph element often used inside the input area */ +/* Remove default browser margins for paragraphs */ +#prompt-textarea p { + margin: 0 !important; + padding: 0 !important; +} + +/* 8. Target the grid container holding the input area */ +/* Overrides ms-1.5 (margin-start) */ +form[data-type="unified-composer"] div[class*="ms-1.5 grid"] { + margin: 0 !important; /* Use margin: 0 to reset all margins */ +} + +/* 9. Target the container for potential elements after the input grid */ +/* Overrides ms-2 (margin-start) */ +form[data-type="unified-composer"] div[class*="ms-2 flex"] { + margin: 0 !important; /* Use margin: 0 to reset all margins */ +} + +/* 10. Optional: Adjust absolute positioned buttons container */ +/* If removing padding makes buttons overlap or look wrong, adjust their position. */ +/* Example: Resetting left offset */ +/* +.bg-primary-surface-primary.absolute.right-0.bottom-\[9px\].left-\[17px\] { + left: 0 !important; + bottom: 0 !important; /* Maybe adjust bottom too */ +/* } */ +/* Target any element whose class contains "prosemirror-parent_" */ +div[class*="prosemirror-parent_"] { + padding: 0 !important; + margin: 0 !important; + box-sizing: border-box !important; + padding-inline-end: 0 !important; /* removes right-side padding from pe-3 */ + padding-right: 0 !important; /* extra safety */ +} + +/* Target the actual contenteditable input area (ProseMirror div) */ +/* Remove any default or library-added padding/margin */ +#prompt-textarea { + padding: 0 !important; + margin: 0 !important; +} + +/* Target the paragraph element often used inside the input area */ +/* Remove default browser margins for paragraphs */ +#prompt-textarea p { + margin: 0 !important; + padding: 0 !important; /* Less likely needed, but safe */ +} +div[class^="prosemirror-parent"] .ProseMirror, +div[class^="prosemirror-parent"] .ProseMirror * { + padding: 0 !important; + margin: 0 !important; + border: none !important; +} +/* 1. Target the direct container holding the input area and buttons */ +/* This div has px-3 py-3 which creates the main internal padding */ +form[data-type="unified-composer"] > div > div.relative.flex.w-full.items-end { + padding: 0!important; +} + + +/* 2. Target the container specifically around the input field */ +/* This div has ps-2 pt-0.5 (padding-start, padding-top) */ +form[data-type="unified-composer"] div.relative.flex-auto.bg-transparent { + padding: 0 !important; +} + +/*very improtant*/ +#prompt-textarea { + padding: 9.1px !important; +} + +/* 4. Target the hidden textarea (might still influence layout slightly) */ +/* It has py-2 (padding-top/bottom) */ +form[data-type="unified-composer"] textarea[placeholder="Ask anything"] { + padding: 0 !important; +} + +/* 5. Target the grid container holding the input area */ +/* It has ms-1.5 (margin-start) */ +form[data-type="unified-composer"] div[class*="ms-1.5 grid"] { + margin: 0 !important; +} + +/* 6. Target the container for potential elements after the input grid */ +/* It has ms-2 (margin-start) */ +form[data-type="unified-composer"] div[class*="ms-2 flex"] { + margin: 0 !important; +} + +/* 7. Optional: Adjust the absolute positioned buttons container if needed */ +/* Removing padding might make these overlap; this resets its position slightly */ +/* +form[data-type="unified-composer"] .absolute.right-3.bottom-0 { + right: 0 !important; + bottom: 0 !important; +} +*/ +/* Target the container around the typing area */ +.prosemirror-parent, /* If there's a class for that container */ +.prose { /* Or a more general prose container */ + padding: 0 !important; + margin: 0 !important; + border: none !important; +} + + /*Target the actual surrounding bar*/ +.bg-token-main-surface-primary{ + padding: 0 !important; + margin: 0 !important; + border: none !important; +} +.bg-clip-padding{ + padding: 0 !important; + margin: 0 !important; + border: none !important; +} +.px-3 { + padding-left: 0 !important; + padding-right: 0 !important; +} +.py-3{ + padding-top: 4px !important; + padding-bottom: 6px !important; +} +/*this owuld impect to something not needed*/ +/*Extra precaution and get to the children*/ + +.absolute > * { + padding: 0 !important; + margin: 0 !important; + border: none !important; +} +/* Optional: Kill vertical gaps from container tokens */ +div[class*="text-primary"] > div { + margin: 0 !important; + padding: 0 !important; +} + + +div.text-base.my-auto.mx-auto.py-5 { + padding-left: 1px !important; + padding-right: 0 !important; +} +/* Main content container */ +main.relative.h-full.w-full.flex-1 { + padding: 0 !important; + width: 100% !important; + max-width: none !important; + box-sizing: border-box !important; +} + +/* Conversation turn block */ +div[class*="conversation-turn"].relative.flex.w-full.min-w-0.flex-col { + padding-left: 0px !important; + padding-right: 0px !important; +} + +/* Inside message: user + assistant full zero padding */ +[data-message-author-role="user"] *, +[data-message-author-role="assistant"] * { +// padding: 0 !important; +} + +/* Remove padding from markdown wrapper */ +article div.text-base.mx-auto.px-6 { + padding-left: 0px !important; + padding-right: 0px !important; +} + +/* Max width unlock for bubble containers */ +[data-message-author-role="user"] div[class*="max-w-"], +[data-message-author-role="user"] .relative.max-w-\[var\(--user-chat-width\,70\%\)\], +[data-message-author-role="user"] .whitespace-pre-wrap { + width: 100% !important; /* currently not working but in arc but would work with 'boost' */ + max-width: none !important; + text-align: right !important; + box-sizing: border-box !important; +} + +/* currently doesn't work but in arc would work with 'boost' +[data-message-author-role="user"] .relative.max-w-\[var\(--user-chat-width\,70\%\)\] { + border: 0.01px solid rgb(151, 148, 148) !important; + border-radius: 15px !important; +} +[data-message-author-role="user"] .relative { + display: inline-block !important; + max-width: 90% !important; + padding: 5px 5px !important; + margin: 6px 0 !important; + background-color: #000 !important; + +} +body { + line-height: 1.275 !important; +font-stretch: condensed !important; +margin-block-start: 0 !important; Remove space before blocks + }*/ +//---credit to 'wide gpt' start--- +@media (min-width: 1280px) { + .xl\\:max-w-\\[48rem\\], + .xl\\:px-5 { + max-width: 100% !important; + padding-left: 1.25rem; + padding-right: 1.25rem; + } + } + + @media (min-width: 768px) { + .md\\:max-w-3xl { + max-width: 100% !important; + } + } + + @container (width >= 64rem) { + .\\@\\[64rem\\]\\:\\[--thread-content-max-width\\:48rem\\] { + --thread-content-max-width: 100% !important; + } + } + + @container (width >= 34rem) { + .\\@\\[34rem\\]\\:\\[--thread-content-max-width\\:40rem\\] { + --thread-content-max-width: 100% !important; + } + } + + /* Extra: override fallback static styles if exist */ + [style*="max-width"] { + max-width: 100% !important; + } +//---credit to 'wide gpt' end--- +`); + } else if (host === "gemini.google.com") { + insCss(`/* === General Layout Widening === */ + +/* Make the main application container take full width */ +chat-app, +body, +html { + width: 100% !important; + max-width: none !important; +} + +/* Target the container holding sidebar AND content */ +mat-sidenav-container.mat-drawer-container { + width: 100% !important; +} + +/* Target the main content area NEXT TO the sidebar */ +mat-sidenav-content.mat-drawer-content { + width: 100% !important; /* Allow content area to take available width */ + margin-left: 0 !important; /* Override default margin when sidebar is closed */ + margin-right: 0 !important; + padding-inline: 0px !important; /* Add some padding back for breathing room */ + box-sizing: border-box; /* Include padding in width calculation */ +} + +/* Ensure the chat window itself fills the content area */ +chat-window[_nghost-ng-c1777261061] { + width: 100% !important; +} + +/* Adjust the chat history container padding */ +chat-window-content .chat-history { + padding-inline: 0px !important; /* Keep this 0 if you want edge-to-edge content */ + /* or use 16px for some spacing: padding-inline: 16px !important; */ +} + + +/* === Central Content Widening (Keep previous rules) === */ + +/* Target the main chat conversation area */ +.conversation-container { + max-width: 1400px !important; /* Increased further, adjust as needed */ +} + +/* Target the input area container at the bottom */ +input-container { + max-width: 1400px !important; /* Match the conversation width */ + padding-inline: 0px !important; /* Keep this 0 if you want edge-to-edge input */ + /* or use 16px: padding-inline: 16px !important; */ +} + +/* Target the initial "zero state" screen container */ +.zero-state-container { + max-width: 1400px !important; /* Match the conversation width */ + } + +/* Ensure the container *within* input-container fills it */ +.input-area-container { + max-width: 100% !important; +} + + +/* === User Prompt Widening === */ + +/* Target the specific container for the user prompt bubble's background/layout */ +user-query .user-query-bubble-with-background { + max-width: none !important; /* Remove the max-width restriction */ + width: 100% !important; /* Allow it to take full available width */ + box-sizing: border-box; /* Include padding/border in the width */ +} + +/* Ensure the parent container allows full width */ +user-query .query-content { + width: 100% !important; +} + +/* === Optional: Adjust Immersive Mode === */ +.immersives-mode[_nghost-ng-c1777261061]:not(.mobile-device) { + max-width: 100% !important; /* Adjust as needed, e.g., 1800px, none */ + margin: 0 auto !important; /* Center it if not 100% width */ +} + +/* === Your Line Height Rule (Keep it) === */ +.markdown p, .markdown li { + line-height: 1.5 !important; +} + +/* === Sidebar Adjustments (Optional - might break things) === */ +/* Hide the collapsed sidebar visually and remove its width influence */ +/* Use with caution, might hide the toggle button */ +/* +bard-sidenav-container mat-sidenav.mat-drawer-closed { + width: 0 !important; + min-width: 0 !important; + visibility: hidden !important; + border: none !important; +} +*/ +`); + } else if (host === "mistral.ai" || host === "claude.ai") { + insCss(` + .max-w-3xl { + max-width: 200ch; + } + .max-w-\[75ch\] { + max-width: 200ch; + } +`); + } })(); From e3733ca59d8d13e1d7e8f8a2eb656d20fff67356 Mon Sep 17 00:00:00 2001 From: macmini Date: Mon, 25 Aug 2025 16:37:09 +0800 Subject: [PATCH 3/4] s --- token-post.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token-post.js b/token-post.js index 36f8f3c..21e02b1 100644 --- a/token-post.js +++ b/token-post.js @@ -4,7 +4,7 @@ // @namespace https://github.com/happyf-weallareeuropean // @version a.a // @author felixy happyfceleste & Johannes Thyroff(https://github.com/JThyroff/WideGPT) -// @description automate tts for chatgpt. Hide UI bloat on chatgpt, gemini, claude, mistral. +// @description tts streaming respose for chatgpt. Hide UI bloat on chatgpt, gemini, claude, mistral. // @updateURL https://raw.githubusercontent.com/happyf-weallareeuropean/cC/main/token-post.js // @downloadURL https://raw.githubusercontent.com/happyf-weallareeuropean/cC/main/token-post.js // @match *://chatgpt.com/* From 51416972aa1a6ee595add4404f4d58550fe06201 Mon Sep 17 00:00:00 2001 From: macmini Date: Mon, 25 Aug 2025 17:08:54 +0800 Subject: [PATCH 4/4] w --- token-post.js | 1 + 1 file changed, 1 insertion(+) diff --git a/token-post.js b/token-post.js index 21e02b1..c8b9e49 100644 --- a/token-post.js +++ b/token-post.js @@ -15,6 +15,7 @@ // @connect localhost // @grant GM_xmlhttpRequest // @run-at document-body +// @license MIT // ==/UserScript==