Skip to main content

Examples Web

examples/web is one of two reference Next.js apps in the Charivo workspace, and the one to read when comparing client styles. Read it as a current integration example, not just a demo. The other is Examples Companion, a realtime-only app.

What It Covers

The app exercises the current package stack:

  • Live2D rendering through @charivo/render-live2d and @charivo/render
  • LLM chat through Gemini and OpenAI, each in a server-route and a browser-direct form, plus an OpenClaw proxy (dev builds only) and a stub
  • TTS through Gemini and OpenAI, each in a server-route and a browser-direct form, plus a browser-native player
  • STT through Gemini and OpenAI, each in a streaming (@charivo/stt/gemini-live, @charivo/stt/openai-realtime), a server-route and a browser-direct form, plus a browser-native transcriber
  • realtime voice sessions through @charivo/realtime/remote and /api/realtime, over the OpenAI Agents WebRTC adapter or the Gemini Live WebSocket adapter, chosen in the settings menu (Gemini Live by default)
  • avatar expression/motion/gaze tool calling through @charivo/avatar, wired into both LLM chat and realtime voice sessions

Lifecycle Split

The current hook split is deliberate:

  • useLive2D owns canvas mount and unmount
  • useCharivoChat owns Charivo setup, render/LLM/TTS/STT manager attachment, event subscription, and teardown
  • useRealtimeMode owns the realtime manager: it attaches and detaches it on the shared Charivo instance, and starts and stops the session

This keeps renderer lifecycle separate from conversation lifecycle, and both separate from realtime session lifecycle.

API Routes

The current reference app ships:

  • POST /api/chat-openai Uses @charivo/server/openai with model gpt-4.1-nano
  • POST /api/chat-openclaw Uses @charivo/server/openclaw. Local-only, matching the two menu entries it serves: it answers 404 in a production build, where OPENCLAW_BASE_URL defaults to the server's own localhost and forwarding OPENCLAW_TOKEN to a published gateway would expose it
  • POST /api/chat-gemini Uses @charivo/server/gemini with model gemini-3.5-flash-lite
  • POST /api/tts-openai Uses @charivo/server/openai with model gpt-4o-mini-tts. The route resolves the voice itself and passes it explicitly, so the provider default is never consulted: a supplied voice must be on the allowlist or the request is rejected with 400, and the sage fallback applies only when none is sent
  • POST /api/tts-gemini Uses @charivo/server/gemini with model gemini-3.1-flash-tts-preview. Same voice-resolution behavior as /api/tts-openai, against a Gemini-specific allowlist with a Kore fallback. The route streams PCM when the request carries Accept: audio/pcm (which the Gemini Remote TTS option sends) and answers WAV otherwise. Text is capped at 400 characters: on the buffered path that keeps one synthesis, or a failed answer plus its retry, inside the route's 25s deadline (kept under the remote player's fixed 30s timeout); on the streaming path the same cap avoids a length past which a stream can end on a spurious SAFETY finish reason instead of completing (measured: 2,358 characters streamed 96.60s of audio before SAFETY, 1,182 completed with STOP). speed is accepted but ignored
  • POST /api/stt-openai Uses @charivo/server/openai with model whisper-1
  • POST /api/stt-gemini Uses @charivo/server/gemini with model gemini-3.5-transcribe. Same multipart contract and 1 MB cap as /api/stt-openai. The free tier allows 3 requests per minute on this model, so a fourth within a minute fails with the route's generic 500 — the demo does not throttle
  • POST /api/stt-openai-realtime Exchanges the OpenAI streaming transcriber's SDP offer with OpenAI so the browser never holds a key
  • POST /api/stt-gemini-live Mints a single-use ephemeral Gemini Live token for @charivo/stt/gemini-live, pinning the transcription model and manual VAD in the token's own setup, and returns it with the websocket url
  • POST /api/realtime Uses @charivo/server/openai or @charivo/server/gemini, as session.provider selects, to create a realtime session bootstrap; either branch rebuilds the session config server side, and the Gemini branch also requires transport: "websocket"

Demo Safeguards

The routes are unauthenticated by design — they are a demo, not a deployable backend. What they do carry is cost bounding, worth copying even though the auth is missing: cost-bearing session fields are pinned server side (examples/web/src/app/api/demo-limits.ts), TTS text and realtime instructions/tools are size-capped, voices come from a per-provider allowlist, and a client-side timer caps a production realtime session at 90 seconds (15 minutes in development) because the browser talks to the provider directly once bootstrapped and the server can no longer hang up. A separate timer with the same limit arms on STT recording, where either streaming transcriber holds an equally wall-clock-billed session for as long as it records.

Runtime Modes

The settings UI intentionally exposes several implementation styles in one place:

  • remote API paths for production-oriented flows, which is where every leg starts: chat and TTS on Gemini Remote, transcription on Gemini Live rather than the unary Gemini Remote route, whose request-per-minute allowance is the tighter of the two (a silent recording then ends in a timeout rather than an empty transcript)
  • browser-direct OpenAI, Gemini, and OpenClaw paths for development and testing (the OpenClaw options are hidden in production builds — they need a gateway on OPENCLAW_BASE_URL, which defaults to localhost); TTS and STT mirror the LLM split with their own Gemini Remote and Gemini Direct (Dev) options
  • browser-native TTS and STT paths for zero-server speech experiments
  • two streaming STT paths, backed by /api/stt-openai-realtime and /api/stt-gemini-live
  • a realtime provider selector, OpenAI Realtime or Gemini Live, that starts on Gemini Live and locks while a call is connecting or up
  • stub LLM mode for deterministic UI work

Files To Read

When To Use It

Use examples/web when you want:

  • a concrete browser integration example
  • working API route examples
  • a reference for lifecycle boundaries between renderer setup and chat/session setup
  • a place to compare runtime choices before designing your own app shell