Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/generation/media-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
Mic,
SlidersHorizontal,
ChevronRight,
Play,

Check warning on line 12 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'Play' is defined but never used. Allowed unused vars must match /^_/u
Loader2,

Check warning on line 13 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'Loader2' is defined but never used. Allowed unused vars must match /^_/u
} from 'lucide-react';
import { toast } from 'sonner';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
Expand All @@ -24,7 +24,7 @@
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Slider } from '@/components/ui/slider';

Check warning on line 27 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'Slider' is defined but never used. Allowed unused vars must match /^_/u
import { Switch } from '@/components/ui/switch';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/hooks/use-i18n';
Expand Down Expand Up @@ -88,6 +88,7 @@
'azure-tts': t('settings.providerAzureTTS'),
'glm-tts': t('settings.providerGLMTTS'),
'qwen-tts': t('settings.providerQwenTTS'),
'doubao-tts': t('settings.providerDoubaoTTS'),
'elevenlabs-tts': t('settings.providerElevenLabsTTS'),
'browser-native-tts': t('settings.providerBrowserNativeTTS'),
};
Expand Down Expand Up @@ -135,9 +136,9 @@
const ttsVoice = useSettingsStore((s) => s.ttsVoice);
const ttsSpeed = useSettingsStore((s) => s.ttsSpeed);
const ttsProvidersConfig = useSettingsStore((s) => s.ttsProvidersConfig);
const setTTSProvider = useSettingsStore((s) => s.setTTSProvider);

Check warning on line 139 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'setTTSProvider' is assigned a value but never used. Allowed unused vars must match /^_/u
const setTTSVoice = useSettingsStore((s) => s.setTTSVoice);

Check warning on line 140 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'setTTSVoice' is assigned a value but never used. Allowed unused vars must match /^_/u
const setTTSSpeed = useSettingsStore((s) => s.setTTSSpeed);

Check warning on line 141 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'setTTSSpeed' is assigned a value but never used. Allowed unused vars must match /^_/u

const asrProviderId = useSettingsStore((s) => s.asrProviderId);
const asrLanguage = useSettingsStore((s) => s.asrLanguage);
Expand Down Expand Up @@ -165,7 +166,7 @@
needsKey: boolean,
) => !needsKey || !!configs[id]?.apiKey || !!configs[id]?.isServerConfigured;

const ttsSpeedRange = TTS_PROVIDERS[ttsProviderId]?.speedRange;

Check warning on line 169 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'ttsSpeedRange' is assigned a value but never used. Allowed unused vars must match /^_/u

// ─── Dynamic browser voices ───
const [browserVoices, setBrowserVoices] = useState<SpeechSynthesisVoice[]>([]);
Expand Down Expand Up @@ -214,7 +215,7 @@

// TTS: grouped by provider, voices as items (matching Image/Video pattern)
// Browser-native voices are split into sub-groups by language.
const ttsGroups = useMemo(() => {

Check warning on line 218 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'ttsGroups' is assigned a value but never used. Allowed unused vars must match /^_/u
const groups: SelectGroupData[] = [];

for (const p of Object.values(TTS_PROVIDERS)) {
Expand Down Expand Up @@ -259,7 +260,7 @@
}, [ttsProvidersConfig, locale, browserVoices, t]);

// TTS preview
const handlePreview = useCallback(async () => {

Check warning on line 263 in components/generation/media-popover.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck & Unit Tests

'handlePreview' is assigned a value but never used. Allowed unused vars must match /^_/u
if (previewing) {
stopPreview();
return;
Expand Down
1 change: 1 addition & 0 deletions components/settings/audio-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function getTTSProviderName(providerId: TTSProviderId, t: (key: string) => strin
'azure-tts': t('settings.providerAzureTTS'),
'glm-tts': t('settings.providerGLMTTS'),
'qwen-tts': t('settings.providerQwenTTS'),
'doubao-tts': t('settings.providerDoubaoTTS'),
'elevenlabs-tts': t('settings.providerElevenLabsTTS'),
'browser-native-tts': t('settings.providerBrowserNativeTTS'),
};
Expand Down
1 change: 1 addition & 0 deletions components/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ function getTTSProviderName(providerId: TTSProviderId, t: (key: string) => strin
'azure-tts': t('settings.providerAzureTTS'),
'glm-tts': t('settings.providerGLMTTS'),
'qwen-tts': t('settings.providerQwenTTS'),
'doubao-tts': t('settings.providerDoubaoTTS'),
'elevenlabs-tts': t('settings.providerElevenLabsTTS'),
'browser-native-tts': t('settings.providerBrowserNativeTTS'),
};
Expand Down
136 changes: 106 additions & 30 deletions components/settings/tts-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,23 @@ export function TTSSettings({ selectedProviderId }: TTSSettingsProps) {
const [testMessage, setTestMessage] = useState('');
const { previewing: testingTTS, startPreview, stopPreview } = useTTSPreview();

// Doubao TTS uses compound "appId:accessKey" — split for separate UI fields
const isDoubao = selectedProviderId === 'doubao-tts';
const rawApiKey = ttsProvidersConfig[selectedProviderId]?.apiKey || '';
const doubaoColonIdx = rawApiKey.indexOf(':');
const doubaoAppId = isDoubao && doubaoColonIdx > 0 ? rawApiKey.slice(0, doubaoColonIdx) : '';
const doubaoAccessKey =
isDoubao && doubaoColonIdx > 0
? rawApiKey.slice(doubaoColonIdx + 1)
: isDoubao
? rawApiKey
: '';

const setDoubaoCompoundKey = (appId: string, accessKey: string) => {
const combined = appId && accessKey ? `${appId}:${accessKey}` : appId || accessKey;
setTTSProviderConfig(selectedProviderId, { apiKey: combined });
};

// Update test text when language changes
useEffect(() => {
setTestText(t('settings.ttsTestTextDefault'));
Expand Down Expand Up @@ -97,37 +114,93 @@ export function TTSSettings({ selectedProviderId }: TTSSettingsProps) {
{/* API Key & Base URL */}
{(ttsProvider.requiresApiKey || isServerConfigured) && (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm">{t('settings.ttsApiKey')}</Label>
<div className="relative">
<Input
name={`tts-api-key-${selectedProviderId}`}
type={showApiKey ? 'text' : 'password'}
autoComplete="new-password"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
placeholder={
isServerConfigured ? t('settings.optionalOverride') : t('settings.enterApiKey')
}
value={ttsProvidersConfig[selectedProviderId]?.apiKey || ''}
onChange={(e) =>
setTTSProviderConfig(selectedProviderId, {
apiKey: e.target.value,
})
}
className="font-mono text-sm pr-10"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
<div className={cn('grid gap-4', isDoubao ? 'grid-cols-3' : 'grid-cols-2')}>
{isDoubao ? (
<>
<div className="space-y-2">
<Label className="text-sm">App ID</Label>
<div className="relative">
<Input
name={`tts-app-id-${selectedProviderId}`}
type={showApiKey ? 'text' : 'password'}
autoComplete="new-password"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
placeholder={
isServerConfigured
? t('settings.optionalOverride')
: t('settings.enterApiKey')
}
value={doubaoAppId}
onChange={(e) => setDoubaoCompoundKey(e.target.value, doubaoAccessKey)}
className="font-mono text-sm pr-10"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm">Access Key</Label>
<div className="relative">
<Input
name={`tts-access-key-${selectedProviderId}`}
type={showApiKey ? 'text' : 'password'}
autoComplete="new-password"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
placeholder={
isServerConfigured
? t('settings.optionalOverride')
: t('settings.enterApiKey')
}
value={doubaoAccessKey}
onChange={(e) => setDoubaoCompoundKey(doubaoAppId, e.target.value)}
className="font-mono text-sm"
/>
</div>
</div>
</>
) : (
<div className="space-y-2">
<Label className="text-sm">{t('settings.ttsApiKey')}</Label>
<div className="relative">
<Input
name={`tts-api-key-${selectedProviderId}`}
type={showApiKey ? 'text' : 'password'}
autoComplete="new-password"
autoCapitalize="none"
autoCorrect="off"
spellCheck={false}
placeholder={
isServerConfigured
? t('settings.optionalOverride')
: t('settings.enterApiKey')
}
value={ttsProvidersConfig[selectedProviderId]?.apiKey || ''}
onChange={(e) =>
setTTSProviderConfig(selectedProviderId, {
apiKey: e.target.value,
})
}
className="font-mono text-sm pr-10"
/>
<button
type="button"
onClick={() => setShowApiKey(!showApiKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
</div>
)}
<div className="space-y-2">
<Label className="text-sm">{t('settings.ttsBaseUrl')}</Label>
<Input
Expand Down Expand Up @@ -167,6 +240,9 @@ export function TTSSettings({ selectedProviderId }: TTSSettingsProps) {
case 'elevenlabs-tts':
endpointPath = '/text-to-speech';
break;
case 'doubao-tts':
endpointPath = '/unidirectional';
break;
}
if (!endpointPath) return null;
return (
Expand Down
100 changes: 100 additions & 0 deletions lib/audio/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,105 @@ export const TTS_PROVIDERS: Record<TTSProviderId, TTSProviderConfig> = {
supportedFormats: ['mp3', 'wav', 'pcm'],
},

'doubao-tts': {
id: 'doubao-tts',
name: '豆包 TTS 2.0(火山引擎)',
requiresApiKey: true,
defaultBaseUrl: 'https://openspeech.bytedance.com/api/v3/tts',
icon: '/logos/doubao.svg',
voices: [
{ id: 'zh_female_vv_uranus_bigtts', name: 'Vivi 2.0', language: 'zh-CN', gender: 'female' },
{
id: 'zh_female_xiaohe_uranus_bigtts',
name: '小何 2.0',
language: 'zh-CN',
gender: 'female',
},
{
id: 'zh_male_m191_uranus_bigtts',
name: '云舟 2.0',
language: 'zh-CN',
gender: 'male',
},
{
id: 'zh_male_taocheng_uranus_bigtts',
name: '小天 2.0',
language: 'zh-CN',
gender: 'male',
},
{
id: 'zh_male_liufei_uranus_bigtts',
name: '刘飞 2.0',
language: 'zh-CN',
gender: 'male',
},
{
id: 'zh_female_qingxinnvsheng_uranus_bigtts',
name: '清新女声 2.0',
language: 'zh-CN',
gender: 'female',
},
{
id: 'zh_female_cancan_uranus_bigtts',
name: '知性灿灿 2.0',
language: 'zh-CN',
gender: 'female',
},
{
id: 'zh_female_shuangkuaisisi_uranus_bigtts',
name: '爽快思思 2.0',
language: 'zh-CN',
gender: 'female',
},
{
id: 'zh_female_tianmeixiaoyuan_uranus_bigtts',
name: '甜美小源 2.0',
language: 'zh-CN',
gender: 'female',
},
{
id: 'zh_female_linjianvhai_uranus_bigtts',
name: '邻家女孩 2.0',
language: 'zh-CN',
gender: 'female',
},
{
id: 'zh_male_shaonianzixin_uranus_bigtts',
name: '少年梓辛 2.0',
language: 'zh-CN',
gender: 'male',
},
{
id: 'zh_male_ruyayichen_uranus_bigtts',
name: '儒雅逸辰 2.0',
language: 'zh-CN',
gender: 'male',
},
{
id: 'zh_female_yingyujiaoxue_uranus_bigtts',
name: 'Tina老师 2.0',
language: 'zh-CN',
gender: 'female',
},
{
id: 'zh_female_kefunvsheng_uranus_bigtts',
name: '暖阳女声 2.0',
language: 'zh-CN',
gender: 'female',
},
{ id: 'en_male_tim_uranus_bigtts', name: 'Tim', language: 'en-US', gender: 'male' },
{ id: 'en_female_dacey_uranus_bigtts', name: 'Dacey', language: 'en-US', gender: 'female' },
{
id: 'en_female_stokie_uranus_bigtts',
name: 'Stokie',
language: 'en-US',
gender: 'female',
},
],
supportedFormats: ['mp3', 'ogg_opus', 'pcm'],
speedRange: { min: 0.5, max: 2.0, default: 1.0 },
},

'elevenlabs-tts': {
id: 'elevenlabs-tts',
name: 'ElevenLabs TTS',
Expand Down Expand Up @@ -895,6 +994,7 @@ export const DEFAULT_TTS_VOICES: Record<TTSProviderId, string> = {
'azure-tts': 'zh-CN-XiaoxiaoNeural',
'glm-tts': 'tongtong',
'qwen-tts': 'Cherry',
'doubao-tts': 'zh_female_vv_uranus_bigtts',
'elevenlabs-tts': 'EXAVITQu4vr4xnSDxMaL',
'browser-native-tts': 'default',
};
Expand Down
Loading
Loading