{"record":{"id":"4cd65fb53071a05c","repo":"moeru-ai/airi","slug":"streaming-tts-not-authenticated","errorCode":null,"errorMessage":"streaming-tts: not authenticated","messagePattern":"streaming-tts: not authenticated","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/speech/streaming-session.ts","lineNumber":91,"sourceCode":" *\n * Use when:\n * - The stage speech pipeline's per-segment `tts()` callback wants to use\n *   the streaming gateway instead of HTTP `/audio/speech`.\n *\n * Expects:\n * - The user is authenticated; `getAuthToken()` returns a JWT or one is\n *   passed in `options.token`.\n * - The server has `STREAMING_TTS_UPSTREAM` configured.\n *\n * Returns:\n * - `{ audio, sentences, byteLength }` once `session.finished` arrives.\n * - Rejects with the upstream `error.message` on a server error event.\n * - Rejects with the abort reason on signal abort.\n */\nexport async function streamingSynthesize(options: StreamingTtsSessionOptions): Promise<StreamingTtsSessionResult> {\n  const token = options.token ?? getAuthToken()\n  if (!token)\n    throw new Error('streaming-tts: not authenticated')\n\n  const baseUrl = options.serverUrl ?? SERVER_URL\n  const wsUrl = toWebSocketUrl(baseUrl, '/api/v1/audio/speech/ws', token, {\n    ttsTrigger: options.ttsTrigger ?? 'manual',\n    ttsSource: options.ttsSource ?? 'manual_preview',\n    ttsVoiceType: options.ttsVoiceType ?? 'unknown',\n  })\n\n  const audioChunks: ArrayBuffer[] = []\n  const sentences: StreamingTtsSessionResult['sentences'] = []\n  let totalBytes = 0\n\n  return new Promise<StreamingTtsSessionResult>((resolve, reject) => {\n    const ws = new WebSocket(wsUrl)\n    ws.binaryType = 'arraybuffer'\n\n    let settled = false\n    function settle(action: () => void) {","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/libs/speech/streaming-session.ts#L73-L109","documentation":"Thrown synchronously at the top of streamingSynthesize when no auth token is available. The function needs a Bearer JWT to open the WebSocket at /api/v1/audio/speech/ws; it first checks options.token then falls back to getAuthToken(). If both are falsy it throws before constructing the ws URL, preventing an unauthenticated connection attempt.","triggerScenarios":"Calling streamingSynthesize({ model, voice, input }) while the user is not logged in (getAuthToken() returns null/empty) and options.token is omitted; the auth store has not been hydrated yet on app startup; the session expired and the token was cleared.","commonSituations":"TTS invoked during the brief window before auth bootstrap completes; logged-out user triggering streaming TTS from settings test or chat auto-TTS; token refresh failed silently leaving getAuthToken empty; misordered app init where speech pipeline starts before auth.","solutions":["Ensure the user is authenticated before invoking streamingSynthesize; gate the call on a non-empty auth token.","If calling during startup, await auth hydration before initializing the speech pipeline.","Pass an explicit options.token when orchestrating server-side or with a freshly refreshed token.","Catch this error and redirect the user to login rather than retrying blindly."],"exampleFix":"// before\nconst { audio } = await streamingSynthesize({ model, voice, input })\n\n// after: guard on auth state\nconst token = getAuthToken()\nif (!token) {\n  // prompt login / skip TTS\n  return\n}\nconst { audio } = await streamingSynthesize({ model, voice, input, token })","handlingStrategy":"validation","validationCode":"import { getAuthToken } from '../auth'\n\nfunction canStreamTts(tokenOverride?: string): boolean {\n  return !!(tokenOverride ?? getAuthToken())\n}\n\n// before calling\nif (!canStreamTts(options.token)) {\n  // prompt login or skip\n  return\n}\nawait streamingSynthesize(options)","typeGuard":"function hasStreamingTtsCredentials(options: StreamingTtsSessionOptions): boolean {\n  return !!(options.token ?? getAuthToken())\n}","tryCatchPattern":"try {\n  const { audio } = await streamingSynthesize(options)\n}\ncatch (error) {\n  if (String(error).includes('not authenticated')) {\n    // redirect to login / refresh token\n  }\n  throw error\n}","preventionTips":["Await auth hydration before initializing the speech pipeline at startup.","Gate streaming TTS UI controls on a non-empty auth token.","Pass an explicitly refreshed token via options.token when orchestrating server-side."],"tags":["auth","tts","streaming","websocket","validation"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}