From aa32e0e5b0f316fd466679419d30ac7791b6f8cc Mon Sep 17 00:00:00 2001 From: Dawid Wenderski Date: Tue, 24 Feb 2026 11:05:55 +0100 Subject: [PATCH 1/2] Fix TTS voice/lang defaults using names instead of slugs API model defaults return display names (e.g. "English", "Vivian") but select dropdowns use slugs as values. This caused lang/voice not matching any option on startup, and the voice fallback showing all voices from all languages with duplicate React keys. - Add resolveLangSlug/resolveVoiceSlug helpers that match by slug OR name - Apply resolution in both auto-defaults effect and handleSetDefaults - Match language by slug or name when filtering voices - Remove all-voices fallback (return empty until lang is set by effect) Co-Authored-By: Claude Opus 4.6 --- src/components/EndpointForm.tsx | 61 +++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/src/components/EndpointForm.tsx b/src/components/EndpointForm.tsx index 50cca11..5671b9d 100644 --- a/src/components/EndpointForm.tsx +++ b/src/components/EndpointForm.tsx @@ -47,6 +47,28 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: const prevModelSlugRef = useRef(undefined); const savedModelsRef = useRef>({}); + // Resolve lang/voice default values (API returns names, selects use slugs) + const resolveLangSlug = useCallback((value: string, model: typeof selectedModel): string => { + if (!model?.languages) return value; + const match = model.languages.find((l) => l.slug === value || l.name === value); + return match ? match.slug : value; + }, []); + + const resolveVoiceSlug = useCallback((value: string, langSlug: string, model: typeof selectedModel): string => { + if (!model?.languages) return value; + const lang = model.languages.find((l) => l.slug === langSlug); + if (lang) { + const match = lang.voices.find((v) => v.slug === value || v.name === value); + return match ? match.slug : value; + } + // Search all languages as fallback + for (const l of model.languages) { + const match = l.voices.find((v) => v.slug === value || v.name === value); + if (match) return match.slug; + } + return value; + }, []); + // Get model defaults/limits/features from API data const modelDefaults = selectedModel?.info && !Array.isArray(selectedModel.info) ? selectedModel.info.defaults : undefined; @@ -141,9 +163,14 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: }); // Skip negative_prompt defaults — API returns placeholder text like "Negative prompt" // which is not a useful default value. Users should fill this in themselves. - // Auto-set lang/voice defaults for TTS - if (defaults.lang !== undefined) newValues['lang'] = defaults.lang as string; - if (defaults.voice !== undefined) newValues['voice'] = defaults.voice as string; + // Auto-set lang/voice defaults for TTS (resolve names to slugs) + if (defaults.lang !== undefined) { + newValues['lang'] = resolveLangSlug(defaults.lang as string, selectedModel); + } + if (defaults.voice !== undefined) { + const langSlug = (newValues['lang'] ?? '') as string; + newValues['voice'] = resolveVoiceSlug(defaults.voice as string, langSlug, selectedModel); + } if (defaults.format !== undefined) newValues['format'] = defaults.format as string; if (defaults.sample_rate !== undefined) newValues['sample_rate'] = defaults.sample_rate as number; return newValues; @@ -161,7 +188,7 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: }); return newDisabled; }); - }, [selectedModelSlug, selectedModel]); + }, [selectedModelSlug, selectedModel, resolveLangSlug, resolveVoiceSlug]); const handleChange = useCallback((name: string, value: JsonValue) => { setValues((prev) => { @@ -299,8 +326,14 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: newValues[field] = defaults[field] as number; } }); - if (defaults.lang !== undefined) newValues['lang'] = defaults.lang as string; - if (defaults.voice !== undefined) newValues['voice'] = defaults.voice as string; + // Resolve names to slugs for TTS defaults + if (defaults.lang !== undefined) { + newValues['lang'] = resolveLangSlug(defaults.lang as string, selectedModel); + } + if (defaults.voice !== undefined) { + const langSlug = (newValues['lang'] ?? '') as string; + newValues['voice'] = resolveVoiceSlug(defaults.voice as string, langSlug, selectedModel); + } if (defaults.format !== undefined) newValues['format'] = defaults.format as string; if (defaults.sample_rate !== undefined) newValues['sample_rate'] = defaults.sample_rate as number; return newValues; @@ -317,7 +350,7 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: }); return newDisabled; }); - }, [modelDefaults, modelSupportsField]); + }, [modelDefaults, modelSupportsField, selectedModel, resolveLangSlug, resolveVoiceSlug]); // Get effective param with model limits/defaults applied const getEffectiveParam = useCallback((param: EndpointParam): EndpointParam => { @@ -366,20 +399,18 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: if (paramName === 'voice' && selectedModel.languages) { const selectedLang = values['lang'] as string; - const language = selectedModel.languages.find((l) => l.slug === selectedLang); + // Also try matching by name (API defaults may use name instead of slug) + const language = selectedModel.languages.find( + (l) => l.slug === selectedLang || l.name === selectedLang + ); if (language) { return language.voices.map((v) => ({ value: v.slug, label: `${v.name} (${v.gender === 'female' ? 'F' : 'M'})`, })); } - // Fallback: show all voices grouped by language - return selectedModel.languages.flatMap((l) => - l.voices.map((v) => ({ - value: v.slug, - label: `${v.name} (${l.slug.toUpperCase()}, ${v.gender === 'female' ? 'F' : 'M'})`, - })) - ); + // No language selected yet — return empty (defaults effect will set lang shortly) + return []; } return undefined; From a3b2d19dda2759bb6eabeea4ba98f887f87ecaed Mon Sep 17 00:00:00 2001 From: Dawid Wenderski Date: Tue, 24 Feb 2026 11:22:00 +0100 Subject: [PATCH 2/2] Use explicit DeApiModel type and normalize langSlug in resolveVoiceSlug Address PR review: replace typeof selectedModel with DeApiModel | undefined for clarity, and use resolveLangSlug inside resolveVoiceSlug to handle cases where langSlug is a display name instead of a slug. Co-Authored-By: Claude Opus 4.6 --- src/components/EndpointForm.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/EndpointForm.tsx b/src/components/EndpointForm.tsx index 5671b9d..5503b1d 100644 --- a/src/components/EndpointForm.tsx +++ b/src/components/EndpointForm.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Loader2, CircleDollarSign, Play, ChevronRight, RotateCcw, Dices } from 'lucide-react'; -import { EndpointDefinition, EndpointParam, JsonValue } from '@/lib/types'; +import { EndpointDefinition, EndpointParam, JsonValue, DeApiModel } from '@/lib/types'; import { useModelsContext } from '@/components/ModelsContext'; import { ModelInfo } from '@/components/ModelInfo'; import { FormField } from '@/components/form/FormField'; @@ -48,15 +48,16 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: const savedModelsRef = useRef>({}); // Resolve lang/voice default values (API returns names, selects use slugs) - const resolveLangSlug = useCallback((value: string, model: typeof selectedModel): string => { + const resolveLangSlug = useCallback((value: string, model: DeApiModel | undefined): string => { if (!model?.languages) return value; const match = model.languages.find((l) => l.slug === value || l.name === value); return match ? match.slug : value; }, []); - const resolveVoiceSlug = useCallback((value: string, langSlug: string, model: typeof selectedModel): string => { + const resolveVoiceSlug = useCallback((value: string, langSlug: string, model: DeApiModel | undefined): string => { if (!model?.languages) return value; - const lang = model.languages.find((l) => l.slug === langSlug); + const normalizedLangSlug = resolveLangSlug(langSlug, model); + const lang = model.languages.find((l) => l.slug === normalizedLangSlug); if (lang) { const match = lang.voices.find((v) => v.slug === value || v.name === value); return match ? match.slug : value; @@ -67,7 +68,7 @@ export function EndpointForm({ endpoint, onSubmit, onPriceCheck, isSubmitting }: if (match) return match.slug; } return value; - }, []); + }, [resolveLangSlug]); // Get model defaults/limits/features from API data const modelDefaults =