Narrator Agent
| File | lib/agents/narratorAgent.ts |
| Triggered by | app/api/generate-audio/route.ts |
| TTS engine | gemini-2.5-flash-preview-tts (Vertex AI, via lib/gcp/tts.ts) |
| Storage | Google Cloud Storage (via lib/gcp/storage.ts) |
What it does
Converts story page text to speech and uploads the audio to Google Cloud Storage. Also generates short spoken quiz feedback as inline base64 audio. Called once per page in parallel after the story is written.
This agent does NOT use ADK (LlmAgent / FunctionTool). It is a plain async function that calls synthesizeSpeech() and uploadAndSign() directly — no LLM routing layer. This reduces audio generation time from ~28s to ~12–15s per page.
Two modes
Mode 1 — Page narration (runNarratorAgent)
Called once per story page during generation.
- Input:
text,storyId,pageNumber,voice,language - Output:
audioUrl— GCS signed URL - Storage path:
audio/{storyId}/page-{pageNumber}.mp3 - Played when: Child navigates to that page in the story viewer
Mode 2 — Feedback audio (runNarratorFeedback)
Called after a child answers a quiz question (fallback path only).
- Input:
text,language - Output:
audioData— base64 WAV data URI (not stored in GCS) - Played when: Feedback is shown after quiz answer
Retry logic (page narration only)
runNarratorAgent retries TTS up to 4 attempts with exponential backoff capped at 30s:
| Attempt | Max delay before retry |
|---|---|
| 1 | 3s |
| 2 | 6s |
| 3 | 12s |
| 4 (final) | throws error |
Retries on:
fetch failed(network error)Timeout- HTTP
429(rate limit) UND_ERR_HEADERS_TIMEOUT
runNarratorFeedback has no retry — a single synthesizeSpeech() call; failure returns { audioData: null }.
Voice
Default voice: Aoede — warm, child-friendly narration.
Configurable per request via the voice parameter. Voice selection affects tone and gender of narration.
Supported languages
All 21 languages supported by StoryWriter are also supported for narration. The TTS engine resolves the language code to a full language name (e.g. pt_pt → "Portuguese (Portugal)") and passes it to the Gemini TTS model in the prompt.
Input / Output
// Page narration input
{
text: string; // Story page text
storyId: string; // Firestore story ID
pageNumber: number; // 1-based
voice?: string; // default: "Aoede"
language?: string; // default: "en"
}
// Page narration output
{
audioUrl: string | null; // GCS signed URL, or null if failed
pageNumber: number;
error?: string;
}
// Feedback audio input
{
text: string;
language?: string; // default: "en"
}
// Feedback audio output
{
audioData: string | null; // "data:audio/wav;base64,..."
error?: string;
}