Skip to content

[Bug]: Custom OpenAI-compatible TTS provider added via the dialog is treated as unconfigured — narration silently skipped, no error #1471

Description

@nirvanaslash

Bug Description

Adding a custom OpenAI-compatible TTS provider through Settings → TTS → + Add produces a provider that the UI reports as configured and that passes Test TTS, but which the generation pipeline then treats as not configured. The narration step is skipped silently: no audio is produced, no error is shown, and no tts entry is recorded in data/usage/*.jsonl.

Root cause: the "Add provider" dialog stores the entered Base URL in customDefaultBaseUrl and leaves baseUrl empty, but isTTSProviderConfigured() only looks at baseUrl. Every other consumer falls back to customDefaultBaseUrl, so the UI looks healthy while the gate is closed.

  • lib/store/settings.ts:1363-1380 — addCustomTTSProvider writes baseUrl: '' (line 1369) and customDefaultBaseUrl: baseUrl (line 1373), plus enabled: true (1370) and auto-selects the provider (1379).
  • lib/audio/provider-enablement.ts:31-42 — TTSEnablementConfig has no customDefaultBaseUrl field at all.
  • lib/audio/provider-enablement.ts:68-74 — the custom-provider branch is hasText(config.apiKey) || hasText(config.baseUrl) || (config.customVoices?.length ?? 0) > 0; with an empty apiKey, an empty baseUrl and no custom voices it returns false, so isTTSProviderEnabled() is false.
  • lib/hooks/use-scene-generator.ts:346 — if (!isTTSProviderEnabled(...)) return null; → narration is dropped without a diagnostic.
  • By contrast lib/hooks/use-scene-generator.ts:374 sends ttsBaseUrl: ttsProviderConfig?.baseUrl || ttsProviderConfig?.customDefaultBaseUrl || undefined, so the request path does honour the dialog value. The same fallback appears at components/settings/tts-settings.tsx:201 (Test TTS), :220 (displayed request URL) and :496 (input placeholder).

Steps to Reproduce

  1. pnpm dev; open http://localhost:3000.
  2. Start any OpenAI-compatible TTS server reachable at e.g. http://127.0.0.1:8020/v1 (a local IndexTTS2 shim in my case) and set ALLOW_LOCAL_NETWORKS=true.
  3. Settings → TTS → + Add; fill only the dialog: Name My Local TTS, Default Base URL http://127.0.0.1:8020/v1, leave "requires API key" unchecked, leave the model blank. Click Add.
  4. In the provider panel that opens, do not retype the URL into the panel's own Base URL input (it shows the value only as a grey placeholder), and do not add any custom voice.
  5. Click Test TTS → it succeeds (audio plays).
  6. Generate a classroom (or press "Regenerate all narration" in the editor) while Enable TTS is on and this provider is selected.

Expected Behavior

A custom provider whose Base URL was supplied in the Add dialog is considered configured, so narration is synthesized — the request path already knows how to resolve that URL (see use-scene-generator.ts:374).

Actual Behavior

  • No /api/generate/tts request is made at all: no tts rows appear in data/usage/*.jsonl, and the TTS server receives no HTTP request.
  • The classroom generates successfully but without narration; every speech action has no audioId/audioUrl.
  • Nothing is logged as an error on either the server or the client.
  • In the editor timeline every line stays "Not voiced"; pressing "Regenerate narration" does nothing visible, because regenerateSpeechAudio (lib/audio/regenerate-speech-tts.ts:120) passes the isManagedTtsActive() check and then hits the same return null in generateAndStoreTTS.

Workaround: retype or paste the URL into the panel's Base URL input (which then writes baseUrl), or add at least one custom voice — either makes isTTSProviderConfigured() return true.

Deployment Method

Local development (npm run dev / pnpm dev / yarn dev)

OpenMAIC Version

v1.0.1 (main @ d5be393, 2026-09-11)

Affected Area

Model / provider integration

Browser

Google Chrome 152.0.7977.83

Operating System

Windows 11 Pro for Workstations, build 26200 (x64)

Relevant Logs

The diagnostic signature of this bug is an **absence** of logs. Server output for a 4-scene run with TTS enabled and the custom provider selected:


[INFO] [Scene Content API] Generating content: "<title>" (slide) [model=...]
[INFO] [Classroom] Scene "<title>": N actions
[INFO] [Scene Content API] Generating content: "<title>" (quiz) [model=...]
# generation completes
[INFO] [Classroom] Pipeline complete: 4 scenes generated


There is **no** `POST /api/generate/tts` request, **no** `[TTS API]` line, **no** error and **no** warning. The client console shows nothing TTS-related. And `data/usage/2026-09.jsonl` gains no `"source":"tts"` rows for the run, while the same run with the built-in (server-configured) OpenAI-compatible TTS provider does record them — which is how the two paths were distinguished.

Screenshots or Recordings

No response

Additional Context

Suggested fix (2 lines): declare the field in the interface and accept it in the predicate.

// lib/audio/provider-enablement.ts
export interface TTSEnablementConfig {
  apiKey?: string;
  baseUrl?: string;
  customDefaultBaseUrl?: string;   // <-- add
  // ...
}

if (isCustomTTSProvider(providerId)) {
  return (
    hasText(config.apiKey) ||
    hasText(config.baseUrl) ||
    hasText(config.customDefaultBaseUrl) ||   // <-- add
    (config.customVoices?.length ?? 0) > 0
  );
}

This keeps behaviour consistent with use-scene-generator.ts:374 and tts-settings.tsx:201/220. The alternative — making the Add dialog write baseUrl directly instead of customDefaultBaseUrl — would also work, but the predicate should still tolerate the fallback field for providers already saved in users' localStorage.

Why the trap is easy to fall into: the panel's Base URL input is bound to value={ttsProvidersConfig[id]?.baseUrl || ''} with the dialog value supplied only as a placeholder (components/settings/tts-settings.tsx:486-506), so the field looks filled. Meanwhile the "Request URL" preview and Test TTS both use the fallback, so everything appears configured.

Environment: local native deployment (no Docker), Node v24.15.0, pnpm 11.21.0, self-hosted OpenAI-compatible TTS on 127.0.0.1:8020 (ALLOW_LOCAL_NETWORKS=true), narration enabled with the custom provider selected. Verified by inspecting the live settings store: ttsEnabled=true, ttsProviderId=custom-tts-..., {baseUrl:"http://127.0.0.1:8020/v1", customDefaultBaseUrl:"http://127.0.0.1:8020/v1", enabled:true, apiKey:"", customVoices:[], serverDisabled:false} — everything but customDefaultBaseUrl is empty when the gate is consulted.

Happy to open a PR with the two-line fix plus a unit test for the predicate if that is welcome.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions