DEV Community

Kaushikcoderpy
Kaushikcoderpy

Posted on

Fandom Fire — A GPU-Accelerated, AI-Powered Roast Battle Engine

This is a submission for Weekend Challenge: Passion Edition

What I Built

Fandom Fire is a real-time, procedural AI roast battle engine. You pick any two fictional characters — Superman vs Thor, Batman vs Joker, Goku vs Naruto — and the engine writes original roasts, synthesizes distinct character voices, and projects a gorgeous audio-reactive fluid visualizer directly onto your GPU.

The entire experience is driven by three AI systems working in concert:

  1. Google Gemini (gemini-3.1-flash-lite) writes the roasts procedurally — no scripts, no templates. Every battle is unique.
  2. ElevenLabs (eleven_v3) gives each fighter a distinct, high-fidelity voice. Fighter A gets a deep, aggressive tone. Fighter B gets a sharp, theatrical British delivery.
  3. A Custom GLSL Fragment Shader renders a Gemini Live-inspired fluid wave that physically reacts to the bass frequencies of each voice in real-time.

FANDOM FIRE HOME PAGE

There is no backend. No npm. No build step. Just three files (index.html, style.css, app.js), a Python static server, and raw browser APIs.

Demo

🎥 Watch the Demo Video

Code

GitHub logo Kaushikcoderpy / Fandom-Fire

Fandom Fire is a real-time, procedural AI roast battle engine. You pick any two fictional characters — Superman vs Thor, Batman vs Joker, Goku vs Naruto — and the engine writes original roasts, synthesizes distinct character voices, and projects a gorgeous audio-reactive fluid visualizer directly onto your GPU.

🔥 Fandom Fire

Fandom Fire is a sensory-heavy, hardware-accelerated, procedural AI-generated roast battle engine. It takes any two fictional characters, generates custom roasts on the fly using Google Gemini, synthesizes their distinct voices using ElevenLabs, and projects a gorgeous, audio-reactive fluid visualizer inspired directly by Gemini Live.

Built entirely on a pure frontend stack—no backend, no npm installs, and no local build systems required.


🚀 Key Visual & Architectural Highlights

1. 🌌 Gemini Live Fluid Wave (WebGL + GLSL)

  • We replaced standard, boring 2D cards and orbiting wireframes with a full-viewport screen-space custom GLSL Fragment Shader running directly on the GPU.
  • Blends organic, eye-comforting color gradients
    • Sky Blue (#022aff) representing the Left Fighter.
    • Premium Indigo/Violet (#7a1ef0) blending in the center.
    • Rose Red/Pink (#f50d6b) representing the Right Fighter.
  • An organic mathematical wave deforms at the bottom of the screen using multi-octave simplex/value noise…

How I Built It

The Architecture (3 Layers)

The engine operates as a pipeline with three distinct layers:

[User Input] → [Gemini Roast Generator] → [ElevenLabs Voice Synth] → [Web Audio FFT] → [GLSL Shader]
Enter fullscreen mode Exit fullscreen mode

Layer 1 generates the text. Layer 2 gives it a voice. Layer 3 makes the voice visible.

HOW IT WORKS


Layer 1: Gemini Roast Generation (Best Use of Google AI)

When you click "ENGAGE BATTLE ENGINE," the app fires off alternating prompts to gemini-3.1-flash-lite:

async function generateBattle(f1, f2) {
    const turns = [
        { attacker: f1, defender: f2, side: 'left' },
        { attacker: f2, defender: f1, side: 'right' },
        { attacker: f1, defender: f2, side: 'left' },
        { attacker: f2, defender: f1, side: 'right' }
    ];

    for(let i=0; i<turns.length; i++) {
        if(!isBattling) break;
        const turn = turns[i];
        const prompt = `You are ${turn.attacker}. Write a single, highly 
          aggressive, funny, and punchy 1-2 sentence roast insulting 
          ${turn.defender}. Use VERY SIMPLE, EASY TO UNDERSTAND language. 
          Do not include quotes, hashtags, or emojis. Just the raw dialogue. 
          Speak in: ${preferredLang}.`;

        const response = await fetch(
          `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-flash-lite:generateContent?key=${geminiKey}`, 
          {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                contents: [{ parts: [{ text: prompt }] }],
                systemInstruction: {
                    parts: [{ text: "You are a roast character. Output ONLY the raw roast text. No formatting." }]
                }
            })
        });

        const data = await response.json();
        const roastText = data.candidates[0].content.parts[0].text
          .replace(/["\n]/g, '').trim();

        roastQueue.push({
            attacker: turn.attacker,
            side: turn.side,
            text: roastText,
            dmg: Math.floor(Math.random() * 21) + 40,
            voice: turn.side === 'left' ? VOICE_LEFT : VOICE_RIGHT
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Key design decision: We use gemini-3.1-flash-lite specifically because latency matters more than depth here. The roasts need to land fast. The systemInstruction enforces raw text output — no markdown, no emoji, no quotes — so the pipeline never chokes on formatting artifacts.


Layer 2: ElevenLabs Voice Synthesis (Best Use of ElevenLabs)

Each generated roast is immediately piped to ElevenLabs' eleven_v3 model with character-specific voice IDs:

function playTurn(turn) {
    return new Promise(async (resolve) => {
        // Fetch synthesized audio from ElevenLabs
        const response = await fetch(
          `https://api.elevenlabs.io/v1/text-to-speech/${turn.voice}?output_format=mp3_44100_128`, 
          {
            method: 'POST',
            headers: { 'xi-api-key': elevenKey, 'Content-Type': 'application/json' },
            body: JSON.stringify({
                text: turn.text,
                model_id: "eleven_v3",
                voice_settings: { stability: 0.5, similarity_boost: 0.75 }
            })
        });

        // Decode directly into Web Audio API buffer
        const arrayBuffer = await response.arrayBuffer();
        const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);

        // Route through analyser node for FFT data extraction
        const source = audioContext.createBufferSource();
        source.buffer = audioBuffer;
        source.connect(analyser);        // ← FFT tap point
        analyser.connect(audioContext.destination);
        source.start(0);
    });
}
Enter fullscreen mode Exit fullscreen mode

The critical routing decision: instead of playing the audio directly to audioContext.destination, we insert an AnalyserNode in between. This gives us access to real-time frequency data (getByteFrequencyData) without affecting playback quality. This is the bridge between Layer 2 and Layer 3.


Layer 3: The GLSL Fluid Wave (The Visual Heartbeat)

This is where the magic lives. A full-viewport PlaneGeometry is rendered via Three.js with a custom ShaderMaterial. The fragment shader generates an organic, flowing liquid wave inspired by Google's Gemini Live interface:

void main() {
    vec2 uv = gl_FragCoord.xy / u_resolution.xy;

    // Multi-octave noise generates organic wave forms
    float n1 = noise(vec2(uv.x * 2.5 + u_time * 0.6, u_time * 0.4));
    float n2 = noise(vec2(uv.x * 4.5 - u_time * 0.4, u_time * 0.3));

    // Audio volumes directly modulate wave height
    float leftHeight = u_leftVolume * 0.28;
    float rightHeight = u_rightVolume * 0.28;

    float waveHeight = 0.15 
        + n1 * (0.12 + leftHeight) 
        + n2 * (0.08 + rightHeight);

    // Smooth, glowing boundary (no harsh edges)
    float alpha = smoothstep(-0.35, 0.05, waveHeight - uv.y);

    // Premium gradient: Sky Blue → Violet → Rose Pink
    vec3 colorLeft  = vec3(0.02, 0.42, 0.98);
    vec3 colorMid   = vec3(0.48, 0.12, 0.94);
    vec3 colorRight = vec3(0.96, 0.05, 0.42);

    vec3 gradColor = mix(colorLeft, colorMid, smoothstep(0.1, 0.5, uv.x));
    gradColor = mix(gradColor, colorRight, smoothstep(0.5, 0.9, uv.x));

    // Brightness surges with voice volume
    float intensity = 1.0 + (u_leftVolume + u_rightVolume) * 1.5;

    vec3 bgColor = vec3(0.004, 0.004, 0.008) * (1.0 - uv.y);
    gl_FragColor = vec4(mix(bgColor, gradColor * intensity, alpha), 1.0);
}
Enter fullscreen mode Exit fullscreen mode

And the JavaScript render loop that feeds it live audio data every frame:

function animate() {
    requestAnimationFrame(animate);
    uniforms.u_time.value = clock.getElapsedTime();

    if(analyser && isBattling) {
        const dataArray = new Uint8Array(analyser.frequencyBinCount);
        analyser.getByteFrequencyData(dataArray);

        // Extract bass frequencies (bins 0-19) for organic pulsing
        let sum = 0;
        for(let i = 0; i < 20; i++) sum += dataArray[i];
        let normalized = (sum / 20.0) / 255.0;

        // Lerp prevents jarring visual jumps between frames
        if (currentSpeakerSide === 'left') {
            uniforms.u_leftVolume.value = THREE.MathUtils.lerp(
              uniforms.u_leftVolume.value, normalized, 0.2);
            uniforms.u_rightVolume.value = THREE.MathUtils.lerp(
              uniforms.u_rightVolume.value, 0, 0.1);
        } else if (currentSpeakerSide === 'right') {
            uniforms.u_rightVolume.value = THREE.MathUtils.lerp(
              uniforms.u_rightVolume.value, normalized, 0.2);
            uniforms.u_leftVolume.value = THREE.MathUtils.lerp(
              uniforms.u_leftVolume.value, 0, 0.1);
        }
    }
    renderer.render(scene, camera);
}
Enter fullscreen mode Exit fullscreen mode

The lerp (linear interpolation) on the volume values is essential — without it, the wave would jitter violently between frames. The 0.2 attack / 0.1 decay rates create a smooth, organic breathing effect that feels alive.


The UI: Tactile Brutalism

The visual language is deliberately anti-glassmorphism:

  • Zero border-radius anywhere in the entire app
  • Sharp 1px solid white borders on pitch black
  • SVG-based fractal noise overlay for film grain texture
  • Chromatic aberration text animation on roast impact
  • Health bars with inline damage ticks (-45DMG) that appear post-speech

The Playback Queue Architecture

The entire battle runs on a producer-consumer queue pattern:

generateBattle() → pushes roasts to roastQueue[]
playbackWorker() → shifts from roastQueue[], plays each turn sequentially
Enter fullscreen mode Exit fullscreen mode

generateBattle() runs ahead, pre-fetching roasts from Gemini while the current turn is still playing. This hides API latency behind the ElevenLabs audio playback window. The user never waits.


Prize Categories

  • Best Use of Google AI — Gemini gemini-3.1-flash-lite generates all roast dialogue procedurally with zero templates.
  • Best Use of ElevenLabseleven_v3 synthesizes distinct character voices routed through Web Audio for real-time FFT-driven shader visuals.

Zero-Dependency Stack

Layer Technology
Rendering Three.js r128 (CDN)
Animation GSAP 3.12.2 (CDN)
AI Text Google Gemini gemini-3.1-flash-lite
AI Voice ElevenLabs eleven_v3
Audio Analysis Native Web Audio API
GPU Shaders Custom GLSL Fragment Shader
Build System None. python -m http.server 8000

Top comments (1)

Collapse
 
frank_signorini profile image
Frank

This is a fascinating concept! Did you use something like