Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions aai_cli/init/templates/audio-transcription/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ uvicorn api.index:app --reload --port 3000

- `api/settings.py`: backend customization for AssemblyAI config, sample URL, and LLM Gateway model.
- `api/index.py`: server routes. Keep `ASSEMBLYAI_API_KEY` here on the server.
- `public/static/app.js`: browser workflow, polling, tab rendering, and transcript Q&A UI.
- `public/static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point.
- `public/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks.
- `static/app.js`: browser workflow, polling, tab rendering, and transcript Q&A UI.
- `static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point.
- `static/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks.

## Change Points

- Transcription features: edit `TRANSCRIPTION_CONFIG_KWARGS` in `api/settings.py`.
- Sample audio URL: edit `SAMPLE_URL` in `api/settings.py` and the matching input value in `public/index.html`.
- Sample audio URL: edit `SAMPLE_URL` in `api/settings.py` and the matching input value in `static/index.html`.
- LLM answer behavior: edit `LLM_MODEL` in `api/settings.py` or the `/api/ask` prompt in `api/index.py`.
- Transcript display: edit renderer functions in `public/static/app.js`.
- Visual theme/layout: edit the monotone Vercel-style tokens in `public/static/styles.css` before changing component rules.
- Transcript display: edit renderer functions in `static/app.js`.
- Visual theme/layout: edit the monotone Vercel-style tokens in `static/styles.css` before changing component rules.
- UI state styling: status, tabs, and sentiment use `data-state`, `.is-active`, or `data-sentiment`; prefer CSS changes over JS class rewrites.

## Invariants

- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `public/index.html` or `public/static/`.
- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `static/index.html` or `static/`.
- Keep every browser `fetch("/api/...")` route registered in `api/index.py`.
- Keep `/api/status/{transcript_id}` non-blocking; do not use SDK helpers that wait for completion in that polling route.
- Keep the app buildless unless the user explicitly asks for a frontend toolchain.
6 changes: 3 additions & 3 deletions aai_cli/init/templates/audio-transcription/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ uvicorn api.index:app --reload --port 3000

Push this folder to a Git repo and import it on Vercel. Set `ASSEMBLYAI_API_KEY`
as a Vercel environment variable (the local `.env` is git-ignored and not deployed).
No extra config is needed: Vercel serves the static page and discovers the
FastAPI app in `api/index.py`.
No extra config is needed: Vercel discovers the FastAPI app in `api/index.py`,
which serves the page and its `static/` assets itself.

## Ideas to extend

- Show chapter summaries and highlight timestamps.
- Add a waveform / audio player synced to the transcript.
- Swap the analysis features in `TRANSCRIPTION_CONFIG_KWARGS` (`api/settings.py`).
- Change transcript rendering in `public/static/app.js`.
- Change transcript rendering in `static/app.js`.
11 changes: 7 additions & 4 deletions aai_cli/init/templates/audio-transcription/api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
GET /api/status/{id} -> poll; returns the full transcript JSON when complete
POST /api/ask -> ask a question about a transcript via the LLM Gateway

The browser (public/index.html + public/static/app.js) submits a URL or file, then
The browser (static/index.html + static/app.js) submits a URL or file, then
polls status.
Your API key stays on the server — the browser never sees it.
"""
Expand Down Expand Up @@ -36,14 +36,17 @@
CONFIG = aai.TranscriptionConfig(**settings.TRANSCRIPTION_CONFIG_KWARGS)

ROOT = Path(__file__).resolve().parent.parent
PUBLIC = ROOT / "public"
# Front-end assets live in static/, NOT public/: Vercel auto-serves a public/ dir
# from its CDN and omits it from the function bundle, so FastAPI couldn't read these
# files. A plain static/ dir ships with the function and lets FastAPI own all routing.
STATIC = ROOT / "static"
app = FastAPI()
app.mount("/static", StaticFiles(directory=PUBLIC / "static"), name="static")
app.mount("/static", StaticFiles(directory=STATIC), name="static")


@app.get("/")
def index() -> FileResponse:
return FileResponse(PUBLIC / "index.html")
return FileResponse(STATIC / "index.html")


def _submit(audio: str) -> dict[str, str]:
Expand Down
18 changes: 9 additions & 9 deletions aai_cli/init/templates/live-captions/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ uvicorn api.index:app --reload --port 3000

- `api/settings.py`: backend token host, token path, WebSocket path, and token expiry.
- `api/index.py`: `/api/token` route. Keep `ASSEMBLYAI_API_KEY` here on the server.
- `public/static/app.js`: browser state, WebSocket lifecycle, and Streaming API params.
- `public/static/audio.js`: microphone pipeline and PCM downsampling helpers.
- `public/static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point.
- `public/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks.
- `static/app.js`: browser state, WebSocket lifecycle, and Streaming API params.
- `static/audio.js`: microphone pipeline and PCM downsampling helpers.
- `static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point.
- `static/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks.

## Change Points

- Streaming model, sample rate, encoding, and turn formatting: edit `STREAMING_CONFIG` in `public/static/app.js`.
- Streaming model, sample rate, encoding, and turn formatting: edit `STREAMING_CONFIG` in `static/app.js`.
- Backend token lifetime or non-production hosts: edit `api/settings.py`.
- Caption rendering: edit `onMessage` in `public/static/app.js`.
- Microphone/downsampling behavior: edit `public/static/audio.js`.
- Visual theme/layout: edit the monotone Vercel-style tokens in `public/static/styles.css` before changing component rules.
- Caption rendering: edit `onMessage` in `static/app.js`.
- Microphone/downsampling behavior: edit `static/audio.js`.
- Visual theme/layout: edit the monotone Vercel-style tokens in `static/styles.css` before changing component rules.
- UI state styling: record and status state use `data-state`; prefer CSS changes over JS class rewrites.

## Invariants

- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `public/index.html` or `public/static/`.
- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `static/index.html` or `static/`.
- Streaming token auth uses the raw API key in the backend `Authorization` header, not `Bearer`.
- Keep the browser connected directly to AssemblyAI; do not proxy the audio stream through FastAPI unless the user asks.
- Keep the app buildless unless the user explicitly asks for a frontend toolchain.
9 changes: 6 additions & 3 deletions aai_cli/init/templates/live-captions/api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
from api import settings

ROOT = Path(__file__).resolve().parent.parent
PUBLIC = ROOT / "public"
# Front-end assets live in static/, NOT public/: Vercel auto-serves a public/ dir
# from its CDN and omits it from the function bundle, so FastAPI couldn't read these
# files. A plain static/ dir ships with the function and lets FastAPI own all routing.
STATIC = ROOT / "static"
app = FastAPI()
app.mount("/static", StaticFiles(directory=PUBLIC / "static"), name="static")
app.mount("/static", StaticFiles(directory=STATIC), name="static")


@app.get("/")
def index() -> FileResponse:
return FileResponse(PUBLIC / "index.html")
return FileResponse(STATIC / "index.html")


@app.post("/api/token")
Expand Down
18 changes: 9 additions & 9 deletions aai_cli/init/templates/voice-agent/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,23 @@ uvicorn api.index:app --reload --port 3000

- `api/settings.py`: backend token host, token path, WebSocket path, and token expiry.
- `api/index.py`: `/api/token` route. Keep `ASSEMBLYAI_API_KEY` here on the server.
- `public/static/app.js`: Voice Agent session config, WebSocket lifecycle, UI state, and event handling.
- `public/static/audio.js`: microphone pipeline, PCM conversion, playback queue, and barge-in helpers.
- `public/static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point.
- `public/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks.
- `static/app.js`: Voice Agent session config, WebSocket lifecycle, UI state, and event handling.
- `static/audio.js`: microphone pipeline, PCM conversion, playback queue, and barge-in helpers.
- `static/styles.css`: visual styling only; the top `:root` block is the primary theme/layout edit point.
- `static/index.html`: page structure and static asset links. IDs are JavaScript hooks; classes are styling hooks.

## Change Points

- Agent prompt, greeting, voice, audio formats, and microphone constraints: edit `SESSION_CONFIG` in `public/static/app.js`.
- Agent prompt, greeting, voice, audio formats, and microphone constraints: edit `SESSION_CONFIG` in `static/app.js`.
- Backend token lifetime or non-production hosts: edit `api/settings.py`.
- Transcript log rendering: edit `addTurn` in `public/static/app.js`.
- Playback, barge-in, or PCM conversion: edit `public/static/audio.js`.
- Visual theme/layout: edit the monotone Vercel-style tokens in `public/static/styles.css` before changing component rules.
- Transcript log rendering: edit `addTurn` in `static/app.js`.
- Playback, barge-in, or PCM conversion: edit `static/audio.js`.
- Visual theme/layout: edit the monotone Vercel-style tokens in `static/styles.css` before changing component rules.
- UI state styling: connection, status, and speaker state use `data-state` or `data-speaker`; prefer CSS changes over JS class rewrites.

## Invariants

- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `public/index.html` or `public/static/`.
- Never expose `ASSEMBLYAI_API_KEY` or any server secret in `static/index.html` or `static/`.
- Voice Agent token auth uses `Authorization: Bearer ...` in the backend. This differs from Streaming token auth.
- Voice Agent `greeting` is spoken literally by TTS; write the exact words the user should hear.
- `reply.audio` carries base64 PCM on the `data` field.
Expand Down
2 changes: 1 addition & 1 deletion aai_cli/init/templates/voice-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ is needed.

## Ideas to extend

- Change the `greeting`, `systemPrompt`, or `voice` in `SESSION_CONFIG` (`public/static/app.js`).
- Change the `greeting`, `systemPrompt`, or `voice` in `SESSION_CONFIG` (`static/app.js`).
- Add tools (function calling) so the agent can look things up or take actions.
- Tune `input.turn_detection` (`min_silence`/`max_silence`) inside `SESSION_CONFIG`.
9 changes: 6 additions & 3 deletions aai_cli/init/templates/voice-agent/api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@
from api import settings

ROOT = Path(__file__).resolve().parent.parent
PUBLIC = ROOT / "public"
# Front-end assets live in static/, NOT public/: Vercel auto-serves a public/ dir
# from its CDN and omits it from the function bundle, so FastAPI couldn't read these
# files. A plain static/ dir ships with the function and lets FastAPI own all routing.
STATIC = ROOT / "static"
app = FastAPI()
app.mount("/static", StaticFiles(directory=PUBLIC / "static"), name="static")
app.mount("/static", StaticFiles(directory=STATIC), name="static")


@app.get("/")
def index() -> FileResponse:
return FileResponse(PUBLIC / "index.html")
return FileResponse(STATIC / "index.html")


@app.post("/api/token")
Expand Down
27 changes: 13 additions & 14 deletions scripts/template_contract_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
"api/index.py",
"api/__init__.py",
"api/settings.py",
"public/index.html",
"public/static/app.js",
"public/static/styles.css",
"static/index.html",
"static/app.js",
"static/styles.css",
"requirements.txt",
"README.md",
"AGENTS.md",
Expand Down Expand Up @@ -50,27 +50,26 @@ def _required_files(template: str, path: Path) -> None:
for rel in _REQUIRED_FILES:
if not (path / rel).exists():
_fail(f"{template}: missing {rel}")
if (
template in {"live-captions", "voice-agent"}
and not (path / "public/static/audio.js").exists()
):
_fail(f"{template}: missing public/static/audio.js")
if template in {"live-captions", "voice-agent"} and not (path / "static/audio.js").exists():
_fail(f"{template}: missing static/audio.js")


def _html_static_refs(template: str, path: Path) -> None:
html = (path / "public/index.html").read_text(encoding="utf-8")
html = (path / "static/index.html").read_text(encoding="utf-8")
refs = set(re.findall(r'(?:href|src)=["\'](/static/[^"\']+)', html))
if not refs:
_fail(f"{template}: public/index.html should load static assets")
_fail(f"{template}: static/index.html should load static assets")
for ref in refs:
if not (path / "public" / ref.lstrip("/")).exists():
_fail(f"{template}: public/index.html references missing asset {ref!r}")
# /static/* is mounted on the static/ dir itself, so the URL path maps
# directly to a file under the template root (e.g. /static/app.js -> static/app.js).
if not (path / ref.lstrip("/")).exists():
_fail(f"{template}: static/index.html references missing asset {ref!r}")


def _frontend_routes(template: str, path: Path) -> None:
frontend = (path / "public/index.html").read_text(encoding="utf-8")
frontend = (path / "static/index.html").read_text(encoding="utf-8")
frontend += "\n".join(
asset.read_text(encoding="utf-8") for asset in (path / "public/static").glob("*.js")
asset.read_text(encoding="utf-8") for asset in (path / "static").glob("*.js")
)
fetched = set(re.findall(r'fetch\(\s*["\'`](/api/[^"\'`?]+)', frontend))
fetched |= set(re.findall(r'["\'`](/api/[A-Za-z0-9_\-/]+?)(?:/?\$\{|/?["\'`]\s*\+)', frontend))
Expand Down
2 changes: 1 addition & 1 deletion tests/test_init_scaffold.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def test_scaffold_copies_files_and_renames_dotfiles(tmp_path):
target = tmp_path / "app"
scaffold.scaffold("audio-transcription", target, api_key="sk-real-key")
assert (target / "api" / "index.py").exists()
assert (target / "public" / "index.html").exists()
assert (target / "static" / "index.html").exists()
assert not (target / "vercel.json").exists()
# dotfile templates are renamed to their dotted names
assert (target / ".gitignore").exists()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_init_template_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def fake_get(url, params=None, headers=None):

def test_page_reads_reply_audio_from_data_field():
# reply.audio carries the base64 PCM in `data` (not `audio`); guard the regression.
app_js = (TEMPLATE_DIR / "public" / "static" / "app.js").read_text()
app_js = (TEMPLATE_DIR / "static" / "app.js").read_text()
assert "reply.audio" in app_js
assert "event.data" in app_js

Expand Down
30 changes: 15 additions & 15 deletions tests/test_init_template_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ def test_required_files_present(template_dir):
"api/index.py",
"api/__init__.py",
"api/settings.py",
"public/index.html",
"public/static/app.js",
"public/static/styles.css",
"static/index.html",
"static/app.js",
"static/styles.css",
"requirements.txt",
"README.md",
"AGENTS.md",
Expand All @@ -38,25 +38,27 @@ def test_required_files_present(template_dir):

def test_realtime_templates_have_audio_helpers(template_dir):
if template_dir.name in {"live-captions", "voice-agent"}:
assert (template_dir / "public" / "static" / "audio.js").exists()
assert (template_dir / "static" / "audio.js").exists()


def test_static_assets_referenced_by_html_exist(template_dir):
html = (template_dir / "public" / "index.html").read_text()
html = (template_dir / "static" / "index.html").read_text()
refs = set(re.findall(r'(?:href|src)=["\'](/static/[^"\']+)', html))
assert refs, f"{template_dir.name}: public/index.html should load static assets"
assert refs, f"{template_dir.name}: static/index.html should load static assets"
for ref in refs:
assert (template_dir / "public" / ref.lstrip("/")).exists(), (
f"{template_dir.name}: public/index.html references missing asset {ref!r}"
# /static/* is mounted on the static/ dir itself, so the URL path maps
# directly to a file under the template root (/static/app.js -> static/app.js).
assert (template_dir / ref.lstrip("/")).exists(), (
f"{template_dir.name}: static/index.html references missing asset {ref!r}"
)


def test_codex_edit_points_are_explicit(template_dir):
notes = (template_dir / "AGENTS.md").read_text()
app_js = (template_dir / "public" / "static" / "app.js").read_text()
app_js = (template_dir / "static" / "app.js").read_text()
assert "ASSEMBLYAI_API_KEY" in notes
assert "buildless" in notes
assert "public/static/app.js" in notes
assert "static/app.js" in notes
assert "_CONFIG" in app_js


Expand All @@ -67,10 +69,8 @@ def test_no_committed_dotenv_or_real_key(template_dir):

def test_frontend_routes_exist_in_backend(template_dir):
"""Every /api path the page fetches must be a route the backend registers."""
frontend = (template_dir / "public" / "index.html").read_text()
frontend += "\n".join(
path.read_text() for path in (template_dir / "public" / "static").glob("*.js")
)
frontend = (template_dir / "static" / "index.html").read_text()
frontend += "\n".join(path.read_text() for path in (template_dir / "static").glob("*.js"))
fetched = set(re.findall(r'fetch\(\s*["\'`](/api/[^"\'`?]+)', frontend))
# Also catch template-literal paths like fetch(`/api/status/${id}`) and "/api/x/" + id
fetched |= set(re.findall(r'["\'`](/api/[A-Za-z0-9_\-/]+?)(?:/?\$\{|/?["\'`]\s*\+)', frontend))
Expand All @@ -80,7 +80,7 @@ def test_frontend_routes_exist_in_backend(template_dir):
for path in fetched:
base = path.rstrip("/")
assert any(base == r or base.startswith(r + "/") for r in registered_bases), (
f"{template_dir.name}: public/index.html fetches {path!r}, "
f"{template_dir.name}: static/index.html fetches {path!r}, "
f"not registered in api/index.py (routes: {sorted(registered_bases)})"
)

Expand Down
10 changes: 5 additions & 5 deletions tests/test_init_template_transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ def test_required_files_exist():
for rel in (
"api/index.py",
"api/settings.py",
"public/index.html",
"public/static/app.js",
"public/static/styles.css",
"static/index.html",
"static/app.js",
"static/styles.css",
"requirements.txt",
"README.md",
"AGENTS.md",
Expand All @@ -68,8 +68,8 @@ def test_base_url_env_is_applied(monkeypatch, mocker):

def test_page_explores_all_features_and_speakers():
# Guard the UI surface: each audio-intelligence view + per-speaker coloring stay wired.
html = (TEMPLATE_DIR / "public" / "index.html").read_text()
app_js = (TEMPLATE_DIR / "public" / "static" / "app.js").read_text()
html = (TEMPLATE_DIR / "static" / "index.html").read_text()
app_js = (TEMPLATE_DIR / "static" / "app.js").read_text()
ui_src = html + app_js
for token in (
"chapters",
Expand Down
Loading