# Project export: Cal-GPT (Guitar Pedal Technologies)

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Get the guitar effect in your head in 5 seconds. Describe a guitar tone in natural language, CalGPT agents communicate to come up with effect parameters, sends it to your pedal.
- Devpost: https://devpost.com/software/cal-gpt-guitar-pedal-technologies
- GitHub: https://github.com/belizsoyak/calgpt
- Video: https://www.youtube.com/embed/oYWVFcqSAs8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — sainik (20 commits), Claude Sonnet 4.6 (19 commits), belizsoyak (10 commits), Lachlan Watts-Tobin (4 commits)

## Devpost submission (written by the team)

### Inspiration

Every guitarist knows the feeling of hearing a tone in their head — "warm 70s blues with a bit of slapback" — and spending twenty minutes hunched over a pedalboard trying to dial it in. We wanted to fix that. The bigger vision is the studio: imagine a producer in a recording session saying "give me something like early Clapton, but darker" and having the effects update automatically on every amp in the room. No interruption. No gear knowledge required. Just describe it and play. For guitarists, that starts with being able to say "SRV" or "psychedelic Hendrix with heavy reverb" and having AI understand exactly what gear, what parameters, what feel — and build it for you.

### What it does

CalGPT turns a plain-English description of a guitar tone into a complete, ready-to-play effect chain — overdrive, chorus, delay, reverb — with every parameter set. Five AI agents coordinate in real time on Band: research_agent — receives the user's message first. If you name an artist, it describes their known gear and signature sound. If you give a vibe, it translates it into gear language ("warm bluesy = mid-gain OD, light reverb, slapback delay"). It then hands off to vibe_agent with that context. vibe_agent — the core engineer. Takes the research context and generates the full JSON effect chain. Sends it to both critic_agent and memory_agent simultaneously. critic_agent — reviews the chain for technical issues: high drive with heavy reverb clashes, delay feedback above 0.85 risks runaway. If it finds a problem, it bounces the chain back to vibe_agent with a specific fix. If the chain is solid, it tells memory_agent to log it. memory_agent — extracts 3–5 tone keywords from every approved chain and builds a running session profile ("warm, vintage, Texas blues, mid-gain"). Sends that profile back to vibe_agent so every new chain reflects what you've been reaching for feedback_agent — when you hit 👎, it diagnoses the specific parameter causing the problem and proposes three quick fixes ("Less reverb", "Softer drive", "Add warmth"), then routes the rejection back to vibe_agent to revise. The agents don't follow a hardcoded pipeline — they communicate through Band's @mention routing. critic_agent can loop vibe_agent back for a revision. memory_agent can inform future chains. Each agent only activates when @mentioned, which is what makes the coordination feel like a real studio conversation. Studio mode: Type a tone or an artist name, watch it render as interactive stomp-box knobs in real time. Performance mode: Build a setlist, precompute every song's tone, flip through them with a single tap mid-set. Real pedal output: Every tone is pushed straight to the hardware. Our ESP32 firmware receives the settings over WiFi and runs the effect DSP on-device — the same numbers that move the on-screen knobs move the real thing.

### How we built it

The idea that made everything click is a parameter contract: the AI never sends code to the pedal — it sends numbers. Claude (via the Anthropic API) takes a tone description and returns strict JSON — an ordered chain of effects, each with normalized parameters. That same contract drives two interchangeable engines: a software engine (Spotify's pedalboard in Python) for instant preview, and our ESP32 firmware for the real pedal. So we could build and demo the whole thing in software, then bolt on hardware without rewriting the brain. Frontend: React + Tailwind (Vite) — a Studio chat that renders the chain as knobs, plus a Performance view with the setlist and Start / Prev / Next. Backend: FastAPI (Python). One agent call turns a vibe into a validated effect chain, and every value is clamped server-side so a hallucinated parameter can never reach the audio engine. Audio engine: pedalboard maps our normalized params to studio-quality effects. Hardware: ESP32 firmware (C++) with a single DSP function that applies the whole chain to a 32-bit signal sample-by-sample, plus a WiFi endpoint that receives the flat parameter set. Each effect is its own small equation, composed in signal order: $$\text{out} = \text{Reverb}\Big(\text{Delay}\big(\text{Tremolo}\big(\text{Vibrato}\big(\text{Overdrive}(x)\big)\big)\big)\Big)$$ where overdrive is a soft-clip waveshaper \( y = \tanh(k\,x) \), vibrato/chorus is a delay line modulated by an LFO \( d(t) = d_0 + \text{depth}\cdot\sin(2\pi f t) \), and delay is feedback \( y[n] = x[n] + g\,y[n-D] \). To run a whole set without depending on flaky venue WiFi, we export the setlist's tones to a CSV and load it onto the pedal's flash (LittleFS) — so the pedal switches tones locally off a footswitch, no network needed mid-song. We built fast and in parallel as a team of four, using Claude Code to move quickly while keeping every change additive so our work never collided.

### Challenges we ran into

The hardware-software connection didn't make it in time. The architecture is solid — the firmware receives parameters correctly, the software generates them correctly — but getting the two reliably talking over campus WiFi during the hackathon ran out of clock. Client isolation on the venue network meant our laptop often couldn't reach the pedal directly. The fallback (CSV flash load) works, but the live wireless push that would have closed the loop didn't get a stable demo window. The bridge code exists; we just needed more time.

### Accomplishments we're proud of

We got the full pipeline working end to end: describe a tone in plain English, watch it render as knobs, and see the exact parameters land on a real pedal over WiFi. Performance mode flips a live pedal through an entire setlist with one tap. And the swappable-backend architecture means the same agent output drives software or hardware — the thing that let four people build a hardware product in a single weekend without it falling apart.

### What we learned

We learned how to decouple an AI "brain" from a real-time system with a clean contract, and how much that buys you: testability, parallel teamwork, and a hardware path that never blocks the software demo. We picked up real DSP (waveshaping, modulated delay, comb/allpass reverb), the tradeoffs between offline and real-time audio, and a team git workflow built on small additive changes. Mostly we learned that the hard part of an AI hardware project isn't the AI or the hardware — it's the interface between them.

### What's next

Live guitar through the ESP32 with an I2S codec — the full real-time pedal experience Hands-free setlist switching triggered by a tap pattern on the strings Voice input — describe tones out loud while you play The studio vision at scale: a session where the engineer says what they want and every amp in the room updates automatically

## README (from the GitHub repository)

# CalGPT (Guitar Pedal Technology)🎸

> Describe a guitar tone in plain language. Five AI agents research it, engineer it, critique it, and remember your preferences — all in real time.

The bigger vision: a studio session where a producer says *"give me early Clapton, but darker"* and the effects update automatically. No gear knowledge required.

---

## Multi-agent architecture (Band)

The CalGPT UI posts your message to a shared Band room. Five agents coordinate autonomously from there:

```
User message
    │
    ▼
research_agent   — identifies artist gear or translates descriptors into gear language
    │
    ▼
vibe_agent       — engineers the full JSON effect chain
    │
  ┌─┴─┐
  ▼   ▼
critic_agent    memory_agent
  │   └── logs tone keywords, sends profile back to vibe_agent
  │
  └── (if issue) → vibe_agent  "reduce reverb mix to 0.3"
       (if solid) → memory_agent  "log this"
```

`feedback_agent` activates on 👎 — diagnoses the chain and routes 3 quick fixes back to `vibe_agent`.

---

## Features

- **Studio mode** — type a tone or artist name, get an effect chain rendered as interactive stomp-box knobs
- **Performance mode** — build a setlist, precompute every song's tone, flip through with Prev/Next
- **Live audio** — run your guitar through the chain in the browser via Web Audio API
- **Hardware bridge** — ESP32 firmware receives the JSON chain over WiFi and runs the DSP on-device. Setlists export as CSV and load onto flash so tone switching works off a footswitch with no network needed mid-show. *(WiFi live-push didn't make it in time — the architecture is ready, the hardware-software connection needed more time.)*

---

## Stack

| Layer | Tech |
|---|---|
| Agent communication | [Band](https://app.band.ai) |  
| Band Room | https://app.band.ai/chat/a6f18fd6-e45c-4f10-81d0-ab23fa52e646 |
| AI | Anthropic `claude-sonnet-4-6` |
| Backend | Python FastAPI + WebSocket |
| DSP / preview | Spotify `pedalboard` |
| Frontend | React + Vite + Tailwind CSS v4 |
| Live audio | Tone.js + Web Audio API |
| Hardware | ESP32 (C++ firmware) |

---

## Running locally

```bash
# backend
cd backend && python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # ANTHROPIC_API_KEY + BAND_* vars
uvicorn main:app --reload

# band agents (4 terminals)
python band_research_agent.py
python band_vibe_agent.py
python band_critic_agent.py
python band_memory_agent.py

# frontend
cd frontend && npm install && npm run dev
```

---

## Environment

```bash
ANTHROPIC_API_KEY=sk-ant-...
BAND_REST_URL=https://app.band.ai/
BAND_WS_URL=wss://app.band.ai/api/v1/socket/websocket
BAND_ROOM_ID=<your-room-uuid>
```

Agent credentials in `backend/agent_config.yaml` (gitignored):
```yaml
vibe_agent:
  agent_id: "..."
  api_key:  "band_a_..."
# research_agent, critic_agent, memory_agent, feedback_agent
```

---

## Effect schema

```json
{
  "preset_name": "Texas Crunch",
  "effects": [
    { "type": "overdrive", "drive": 0.6, "tone": 0.5, "mix": 0.9 },
    { "type": "delay",     "time_ms": 220, "feedback": 0.3, "mix": 0.25 },
    { "type": "reverb",    "size": 0.35, "damping": 0.5, "mix": 0.2 }
  ]
}
```


## Detected evidence (automated analysis)

Indexed codebase: 30 recognized source files, 113 KB.
- Anthropic (technology) — detected in the code
- C++ (language) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (39 of 39)

```
.gitignore
backend/.env.example
backend/agent.py
backend/band_bus.py
backend/band_critic_agent.py
backend/band_feedback_agent.py
backend/band_memory_agent.py
backend/band_research_agent.py
backend/band_vibe_agent.py
backend/critic_agent.py
backend/esp32_bridge.py
backend/feedback_agent.py
backend/fx_engine.py
backend/main.py
backend/memory_agent.py
backend/mock_pedal.py
backend/requirements.txt
backend/research_agent.py
backend/session.py
backend/vibe_agent.py
firmware/calgpt_pedal_hwtest/calgpt_pedal_hwtest.ino
firmware/calgpt_pedal_webtest/calgpt_pedal_webtest.ino
firmware/calgpt_pedal.ino
firmware/calgpt_pedal/calgpt_pedal_hwtest.ino
firmware/calgpt_pedal/calgpt_pedal.ino
firmware/calgpt_pedal/firmware_test.cpp
firmware/CONNECTING_THE_ESP.md
frontend/eslint.config.js
frontend/index.html
frontend/package.json
frontend/README.md
frontend/src/App.css
frontend/src/App.jsx
frontend/src/audioEngine.js
frontend/src/index.css
frontend/src/main.jsx
frontend/vite.config.js
README.md
REVIEW.md
```

### Dependencies

- backend/requirements.txt: anthropic@>=0.25.0, fastapi@>=0.110.0, httpx@>=0.27.0, pedalboard@>=0.9.0, python-dotenv@>=1.0.0, requests@>=2.31.0, uvicorn[standard]@>=0.27.0, websockets@>=12.0
- frontend/package.json: @eslint/js@^10.0.1, @tailwindcss/vite@^4.3.1, @types/react@^19.2.14, @types/react-dom@^19.2.3, @vitejs/plugin-react@^6.0.1, eslint@^10.3.0, eslint-plugin-react-hooks@^7.1.1, eslint-plugin-react-refresh@^0.5.2, globals@^17.6.0, react@^19.2.6, react-dom@^19.2.6, tailwindcss@^4.3.1, tone@^15.1.22, vite@^8.0.12

### Recent commits (newest first)

- Merge pull request #10 from belizsoyak/austins-work
- Merge branch 'main' of https://github.com/belizsoyak/calgpt
- network test
- Fix formatting in README.md for Band Room link
- Update README with Band Room link
- Update README.md
- Update README and switch Band listener to REST polling
- Add dark/light mode toggle, branding subtitle, and fix text colors
- Merge branch 'main' of https://github.com/belizsoyak/calgpt
- Fix Band agent-to-agent communication and tighten agent prompts
- Merge pull request #9 from belizsoyak/fix/boot-and-firmware-review
- Fix backend boot crash, make frontend backend URL configurable, add review notes
- Merge branch 'main' of https://github.com/belizsoyak/calgpt
- added firmware test
- Add band_research_agent for artist name lookup on Band
- Add band_memory_agent, fix CSS import order, update vibe agent to tag all agents
- Add Band backbone, feedback agent, and rock UI
- Merge branch 'main' of https://github.com/belizsoyak/calgpt
- Merge pull request #8 from belizsoyak/feat/setlist-csv
- Docs: document WiFi setlist fetch on boot + POST /reload

## Key source files (fetched from GitHub, selected and truncated for size)

### REVIEW.md

```markdown
# CalGPT — Setup & Review Notes

Summary of a local run-through + firmware review. Covers what was verified, what
had to be fixed to boot, and the bugs worth addressing.

> **TL;DR** — The app runs locally and `/vibe` works end-to-end against Claude,
> but **one startup bug had to be patched** (`backend/band_bus.py` imports an
> uninstalled `band` SDK at module top, crashing the whole backend — still
> present upstream). The repo is otherwise current with `main`. The **firmware
> has two issues worth fixing before hardware**: a **32-bit audio overflow** in
> the reverb/delay feedback (reproduced — a sustained loud note wraps on 1600/2000
> samples → audible crackle), and **three out-of-sync copies** of the firmware
> that will cause fixes to land in only one. Architecture is otherwise solid and
> the DSP test harness builds and runs cleanly.

---

## ✅ Run-through (verified working)

- **Backend + frontend both run locally** on Python 3.13 / Node 22. All deps
  install cleanly, including `pedalboard` + `numpy`.
- **Live `/vibe` works end-to-end** — a real Claude call (`claude-sonnet-4-6`)
  returned a valid effect chain. `/health` and the `/preview` audio render
  (returns a real WAV) also pass with no API key.
- Ran on ports **8001 (backend)** / **5174 (frontend)** only to avoid another
  copy already holding 8000/5173.

## 📦 Repo currency

- Diffed against the latest `belizsoyak/calgpt` `main` (top commit `fec0c97`):
  this copy was **already up to date** — no new frontend or backend changes
  upstream.

---

## 🔧 Fixes needed to boot

### 1. `backend/band_bus.py` — startup crash (still present upstream)
`band_bus.py` did `from band.config import load_agent_config` at module top, but
the `band` SDK is **not in `requirements.txt` and not installed**, so importing
`main.py` failed and the **entire backend wouldn't start** — even though the
band.ai feature is optional and gated behind the `BAND_ROOM_ID` env var.

Fix applied — make the import optional so the module loads without the SDK:

```python
try:
    from band.config import load_agent_config
except ImportError:  # band SDK is optional — features gated behind BAND_ROOM_ID
    load_agent_config = None
```

### 2. `frontend/src/App.jsx` — hardcoded backend URL
`API` / `WS_URL` are hardcoded to `localhost:8000` with no env override. Pointed
them at `8001` for local testing; revert to `8000` for the standard setup, or
make them read an env var (`import.meta.env.VITE_API_URL`) so it's configurable.

---

## 🎸 Firmware review (`firmware/`)

### 🔴 High — reverb/delay feedback overflows 32-bit audio
The DSP runs at full int32 scale (±2³¹) with **no headroom** in the feedback
sums:

```c
float wet = x + tail;              // reverb
reverb_buf[w] = (int32_t)wet;      // truncates -> hard wraparound
```

A feedback comb at gain 0.6 settles to `x/(1-0.6) = 2.5·x` for sustained input.
Reproduced with a sustained 0.8-full-scale note (the SRV overdrive+reverb
`hwtest` preset):

```
input mag       = 1.
[truncated — 3850 more characters]
```

### firmware/CONNECTING_THE_ESP.md

```markdown
# Connecting the ESP32 pedal

Handoff notes for taking CalGPT from the software demo to a real pedal.

## What was added

- **Backend:** `GET /setlist/{id}/export.csv` — dumps a setlist's per-song tone
  parameters as CSV (one row per song). Reuses `chain_to_flat()` from
  `esp32_bridge.py`, so the columns match what the firmware expects.
- **Firmware (`calgpt_pedal.ino`):** loads `setlist.csv` from LittleFS into a
  `ToneParams[]`, and switches the active tone per song. Songs advance via a
  **footswitch (GPIO)** or over **WiFi** — both work at once.

## The 13-column contract (exact order — do not reorder)

Both sides parse **by position**, so the export order and the firmware's read
order must stay identical:

```
od_drive, od_tone, od_mix,
vib_rate, vib_depth, vib_mix,
trem_rate, trem_depth, trem_mix,
dl_time_ms, dl_feedback, dl_mix,
rv_mix
```

The CSV file has a header row `song,<those 13 columns>` and then one row per
song: the song name followed by the 13 values. The firmware **skips the header**
and, on each data row, **skips the first field (song name)** and reads the 13
floats by position into a `ToneParams`.

Keep `struct ToneParams`'s field names/order aligned to these columns.

## Getting `setlist.csv` onto flash (two ways, both built in)

1. **WiFi fetch from the backend (default).** Set `EXPORT_URL` near the top of
   the sketch to your backend's export endpoint:
   ```
   #define EXPORT_URL "http://<LAPTOP_IP>:8000/setlist/<SETLIST_ID>/export.csv"
   ```
   Use the laptop's **LAN IP** (e.g. `192.168.1.42`) — **not** `localhost` /
   `127.0.0.1`, which the ESP can't reach — and the `SETLIST_ID` returned by
   `POST /setlist`. `fetchSetlistFromBackend()` does an `HTTPClient` GET and, on
   `200`, streams the body straight to `/setlist.csv` on LittleFS.
   - **On boot:** `setup()` calls it right after WiFi connects (before loading).
   - **On demand:** `POST /reload` to the pedal re-fetches, reloads, and jumps to
     song 0, responding `{"ok":<bool>,"songs":<count>}`. Handy for updating the
     setlist without re-flashing.
2. **Arduino "ESP32 LittleFS Data Upload" plugin (offline fallback).** Create a
   `data/` folder next to the sketch, drop `setlist.csv` in it, and run the
   upload tool — it writes the file to the LittleFS partition at `/setlist.csv`.
   Used automatically if the WiFi fetch is unset/unreachable.

`setup()` order: `LittleFS.begin(true)` → `fetchSetlistFromBackend(EXPORT_URL)`
→ `loadSetlistCSV("/setlist.csv")` → `applySong(0)`. So a flash-uploaded file
still works even with no backend reachable.

## Wiring the footswitch

- Pin: `NEXT_PIN` (default **GPIO 15** — change in the sketch to any free GPIO).
- Wire a momentary button between that pin and **GND**.
- The pin uses `INPUT_PULLUP`, so it idles HIGH and reads LOW when pressed; the
  press is debounced (~30 ms) and calls `nextSong()`.
- `prevSong()` exists too if you want to wire a second button.

## The one remaining hardware step: I2S audio I/O

Everything
[truncated — 1040 more characters]
```

### backend/requirements.txt

```
fastapi>=0.110.0
uvicorn[standard]>=0.27.0
anthropic>=0.25.0
python-dotenv>=1.0.0
pedalboard>=0.9.0
requests>=2.31.0
websockets>=12.0
httpx>=0.27.0

```

### frontend/package.json

```
{
  "name": "frontend",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.2.6",
    "react-dom": "^19.2.6",
    "tone": "^15.1.22"
  },
  "devDependencies": {
    "@eslint/js": "^10.0.1",
    "@tailwindcss/vite": "^4.3.1",
    "@types/react": "^19.2.14",
    "@types/react-dom": "^19.2.3",
    "@vitejs/plugin-react": "^6.0.1",
    "eslint": "^10.3.0",
    "eslint-plugin-react-hooks": "^7.1.1",
    "eslint-plugin-react-refresh": "^0.5.2",
    "globals": "^17.6.0",
    "tailwindcss": "^4.3.1",
    "vite": "^8.0.12"
  }
}

```

### backend/main.py

```python
import asyncio
import os
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from agent import generate_preset
from session import SessionManager
from vibe_agent import run_vibe_agent
from critic_agent import run_critic_agent
from research_agent import lookup_artist
from memory_agent import update_memory
from feedback_agent import get_quick_fixes
from band_bus import send_to_room, listen_for_responses

app = FastAPI(title="CalGPT")
sessions = SessionManager()


@app.on_event("startup")
async def startup():
    room_id = os.getenv("BAND_ROOM_ID", "").strip()
    if not room_id:
        return

    async def forward(msg):
        for sid in list(sessions.connections.keys()):
            await sessions.send(sid, {
                "type": "agent_message",
                "agent": msg["agent"],
                "content": msg["content"],
            })

    asyncio.create_task(listen_for_responses(room_id, forward))

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)


class VibeRequest(BaseModel):
    vibe: str


@app.get("/health")
def health():
    return {"status": "ok"}


@app.post("/vibe")
async def vibe(req: VibeRequest):
    hit = lookup_artist(req.vibe)
    if hit:
        return hit
    return await generate_preset(req.vibe)


@app.websocket("/ws/{session_id}")
async def websocket_endpoint(websocket: WebSocket, session_id: str):
    await sessions.connect(session_id, websocket)
    try:
        while True:
            data = await websocket.receive_json()
            msg_type = data.get("type", "chat")

            if msg_type == "feedback":
                rating = data.get("rating")
                contract = data.get("contract")
                if rating == "positive" and contract:
                    sessions.add_approved_chain(session_id, contract)
                    asyncio.create_task(update_memory(session_id, sessions))
                    await sessions.send(session_id, {"type": "feedback_saved"})
                elif rating == "negative" and contract:
                    fixes = await get_quick_fixes(contract, session_id, sessions)
                    await sessions.send(session_id, {"type": "quick_fixes", "fixes": fixes})
                continue

            if msg_type == "quick_fix":
                fix = data.get("fix", "")
                contract = data.get("contract")
                if contract:
                    sessions.add_rejection(session_id, fix, contract)
                message = fix
            else:
                message = data.get("message", "").strip()

            if not message:
                continue

            # Research agent: instant preset for known artists
            hit = lookup_artist(message)
            if hit:
                await sessions.send(session_id, {
                    "type": "chain_update",
                    "agent": "vibe",
                    "message": f"Loaded {hit['preset_name']}.",
                    "contract": hit,
                })
                asyncio.create_task(run_critic_agent(session_id, hit, sessions))
                continue

            # Inject memory context from prior turns
            memory = sessions.get_memory(session_id)
            if memory:
                message = f"{message} [tone profile: {memory}]"

            # Always run direct agents for instant UI response
            result = await run_vibe_agent(session_id, message, sessions)
            asyncio.create_task(run_critic_agent(session_id, result["contract"], sessions))
            asyncio.create_task(update_memory(session_id, sessions))

            # Also fire-and-forget to Band room for agent-to-agent communication
            if os.getenv("BAND_ROOM_ID", "").strip():
                asyncio.create_task(send_to_room(message))
    except WebSocketDisconnect:
        sessions.disconnect(session_id)


# --- hardware bridge (feat/hardware-bridge) -------------------------------
from fastapi.concurrency import run_in_threadpool
from esp32_bridge import push_to_pedal


class PedalRequest(BaseModel):
    vibe: str
    esp_ip: str


@app.post("/pedal")
async def pedal(req: PedalRequest):
    chain = await generate_preset(req.vibe)
    pushed = await run_in_threadpool(push_to_pedal, chain, req.esp_ip)
    return {"chain": chain, "pushed": pushed}


# --- performance mode (feat/performance-mode) -----------------------------
# In-memory setlist store. NOTE: resets on server restart — fine for the demo.
import uuid
from typing import List

setlists: dict = {}


class Song(BaseModel):
    song_name: str
    vibe: str


class SetlistRequest(BaseModel):
    name: str
    esp_ip: str
    songs: List[Song]


@app.post("/setlist")
async def create_setlist(req: SetlistRequest):
    try:
        # Precompute every song's tone NOW so transitions are instant later.
        songs = []
        for s in req.songs:
            chain = await generate_preset(s.vibe)
            songs.append({"song_name": s.song_name, "vibe": s.vibe, "chain": chain})

        sid = uuid.uuid4().hex[:8]
        setlist = {
            "id": sid,
            "name": req.name,
            "esp_ip": req.esp_ip,
            "current": -1,
            "songs": songs,
        }
        setlists[sid] = setlist
        return setlist
    except Exception as e:
        return {"error": str(e)}


@app.get("/setlist/{sid}")
async def get_setlist(sid: str):
    try:
        setlist = setlists.get(sid)
        if setlist is None:
            return {"error": "setlist not found"}
        return setlist
    except Exception as e:
        return {"error": str(e)}


async def _go_to(sid: str, index: int):
    """Move to a song index, push its precomputed chain, return the active state."""
    setlist = setlists.get(sid)
    if setlist is None:
        return {"error": "setlist not found"}
    setlist["current"] = index
    song = s
[truncated — 5944 more characters]
```

### frontend/src/main.jsx

```javascript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

```

### frontend/src/App.jsx

```javascript
import { useState, useEffect, useRef } from 'react'
import * as audioEngine from './audioEngine'

// Backend location is configurable via env (VITE_API_URL / VITE_WS_URL),
// defaulting to the standard local backend on port 8000.
const API = import.meta.env.VITE_API_URL || 'http://localhost:8000'
const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws'

function generateSessionId() {
  return Math.random().toString(36).slice(2)
}

function stripJson(content) {
  return content.replace(/```json[\s\S]*?```/g, '').replace(/@\w+/g, m => m).trim().slice(0, 120)
}

function parseContract(content) {
  const match = content.match(/```json\s*([\s\S]*?)\s*```/)
  if (match) {
    try {
      const data = JSON.parse(match[1])
      if (data.effects) return data
      if (data.contract?.effects) return data.contract
    } catch {}
  }
  try {
    const data = JSON.parse(content)
    if (data.effects) return data
    if (data.contract?.effects) return data.contract
  } catch {}
  return null
}

export default function App() {
  const [darkMode, setDarkMode] = useState(() => localStorage.getItem('calgpt_dark') !== 'false')
  const [user, setUser] = useState(() => localStorage.getItem('calgpt_user') || '')
  const [sessionId] = useState(generateSessionId)
  const [messages, setMessages] = useState([])
  const [contract, setContract] = useState(null)
  const [input, setInput] = useState('')
  const [connected, setConnected] = useState(false)
  const [view, setView] = useState('studio')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  const [live, setLive] = useState(false)
  const [source, setSource] = useState('file')   // 'file' | 'guitar'
  const [devices, setDevices] = useState([])
  const [deviceId, setDeviceId] = useState('')
  const [audioError, setAudioError] = useState(null)
  const [feedbackState, setFeedbackState] = useState(null) // null | 'saved' | 'quick_fixes'
  const [quickFixes, setQuickFixes] = useState([])
  const [agentLog, setAgentLog] = useState([])
  const wsRef = useRef(null)
  const bottomRef = useRef(null)

  useEffect(() => {
    document.documentElement.classList.toggle('dark', darkMode)
    localStorage.setItem('calgpt_dark', darkMode)
  }, [darkMode])

  function signOut() {
    if (audioEngine.isPlaying()) { audioEngine.stop(); setLive(false) }
    localStorage.removeItem('calgpt_user')
    setUser('')
  }

  async function changeSource(mode) {
    setSource(mode)
    setAudioError(null)
    try {
      await audioEngine.setSource(mode)   // seamless swap if already live
      if (mode === 'guitar') setDevices(await audioEngine.listInputDevices())
    } catch (err) {
      console.error('source switch failed:', err)
      setAudioError('Could not access input device')
    }
  }

  function changeDevice(id) {
    setDeviceId(id)
    audioEngine.setInputDevice(id).catch(err => {
      console.error('device switch failed:', err)
      setAudioError('Could not switch input device')
    })
  }

  // toggle real-time audio; Tone.start() must run inside this click handler
  async function toggleLive() {
    if (audioEngine.isPlaying()) {
      audioEngine.stop()
      setLive(false)
    } else {
      setAudioError(null)
      try {
        await audioEngine.start()
        setLive(true)
        if (contract) audioEngine.applyChain(contract)
        // device labels are available now that permission was granted
        if (source === 'guitar') setDevices(await audioEngine.listInputDevices())
      } catch (err) {
        console.error('live audio failed:', err)
        setAudioError(source === 'guitar' ? 'Microphone/input permission denied' : 'Could not start audio')
      }
    }
  }

  async function onPickLoop(e) {
    const file = e.target.files?.[0]
    if (!file) return
    try {
      await audioEngine.loadSource(file)
    } catch (err) {
      console.error('loop load failed:', err)
    }
  }

  // populate the device list when entering guitar mode
  useEffect(() => {
    if (source === 'guitar') audioEngine.listInputDevices().then(setDevices).catch(() => {})
  }, [source])

  // while live, morph the loop whenever the Studio chain changes
  useEffect(() => {
    if (live && contract) audioEngine.applyChain(contract)
  }, [contract, live])

  useEffect(() => {
    const ws = new WebSocket(`${WS_URL}/${sessionId}`)
    ws.onopen = () => setConnected(true)
    ws.onclose = () => setConnected(false)
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      if (data.type === 'chain_update') {
        setContract(data.contract)
        setMessages(prev => [...prev, { role: 'vibe', text: data.message }])
        setLoading(false)
        setFeedbackState(null)
        setQuickFixes([])
      } else if (data.type === 'critic_message') {
        setMessages(prev => [...prev, { role: 'critic', text: data.message }])
      } else if (data.type === 'agent_message') {
        setMessages(prev => [...prev, { role: data.agent, text: data.content }])
        setAgentLog(prev => [...prev.slice(-19), {
          agent: data.agent,
          text: stripJson(data.content),
          ts: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
        }])
        setLoading(false)
        // If VibeAgent included a JSON contract, parse and apply it
        if (data.agent === 'VibeAgent') {
          const contract = parseContract(data.content)
          if (contract) {
            setContract(contract)
            setFeedbackState(null)
            setQuickFixes([])
          }
        }
      } else if (data.type === 'quick_fixes') {
        setQuickFixes(data.fixes)
        setFeedbackState('quick_fixes')
      } else if (data.type === 'feedback_saved') {
        setFeedbackState('saved')
        setTimeout(() => setFeedbackState(null), 2000)
      }
    }
    wsRef.current = ws
    return () => ws.close()
  }, [sessionId])

  useEffect(() => {
    bottomRef.current?.scrollI
[truncated — 26613 more characters]
```

### frontend/vite.config.js

```javascript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})

```

### frontend/index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>frontend</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

```

### frontend/eslint.config.js

```javascript
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{js,jsx}'],
    extends: [
      js.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      globals: globals.browser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
  },
])

```

[20 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]