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
pnpm dev; open http://localhost:3000.
- 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.
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.
- 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.
- Click Test TTS → it succeeds (audio plays).
- 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.
Bug Description
Adding a custom OpenAI-compatible TTS provider through
Settings → TTS → + Addproduces 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 nottsentry is recorded indata/usage/*.jsonl.Root cause: the "Add provider" dialog stores the entered Base URL in
customDefaultBaseUrland leavesbaseUrlempty, butisTTSProviderConfigured()only looks atbaseUrl. Every other consumer falls back tocustomDefaultBaseUrl, so the UI looks healthy while the gate is closed.lib/store/settings.ts:1363-1380—addCustomTTSProviderwritesbaseUrl: ''(line 1369) andcustomDefaultBaseUrl: baseUrl(line 1373), plusenabled: true(1370) and auto-selects the provider (1379).lib/audio/provider-enablement.ts:31-42—TTSEnablementConfighas nocustomDefaultBaseUrlfield at all.lib/audio/provider-enablement.ts:68-74— the custom-provider branch ishasText(config.apiKey) || hasText(config.baseUrl) || (config.customVoices?.length ?? 0) > 0; with an emptyapiKey, an emptybaseUrland no custom voices it returns false, soisTTSProviderEnabled()is false.lib/hooks/use-scene-generator.ts:346—if (!isTTSProviderEnabled(...)) return null;→ narration is dropped without a diagnostic.lib/hooks/use-scene-generator.ts:374sendsttsBaseUrl: ttsProviderConfig?.baseUrl || ttsProviderConfig?.customDefaultBaseUrl || undefined, so the request path does honour the dialog value. The same fallback appears atcomponents/settings/tts-settings.tsx:201(Test TTS),:220(displayed request URL) and:496(input placeholder).Steps to Reproduce
pnpm dev; openhttp://localhost:3000.http://127.0.0.1:8020/v1(a local IndexTTS2 shim in my case) and setALLOW_LOCAL_NETWORKS=true.Settings → TTS → + Add; fill only the dialog: NameMy Local TTS, Default Base URLhttp://127.0.0.1:8020/v1, leave "requires API key" unchecked, leave the model blank. Click Add.Enable TTSis 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
/api/generate/ttsrequest is made at all: nottsrows appear indata/usage/*.jsonl, and the TTS server receives no HTTP request.audioId/audioUrl.regenerateSpeechAudio(lib/audio/regenerate-speech-tts.ts:120) passes theisManagedTtsActive()check and then hits the samereturn nullingenerateAndStoreTTS.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 makesisTTSProviderConfigured()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
Screenshots or Recordings
No response
Additional Context
Suggested fix (2 lines): declare the field in the interface and accept it in the predicate.
This keeps behaviour consistent with
use-scene-generator.ts:374andtts-settings.tsx:201/220. The alternative — making the Add dialog writebaseUrldirectly instead ofcustomDefaultBaseUrl— 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 aplaceholder(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 butcustomDefaultBaseUrlis 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.