-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: chunked TTS generation for long text (engine-agnostic) #266
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
70ca7f6
837f852
97292ec
9aa7080
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; | ||
| import { Slider } from '@/components/ui/slider'; | ||
| import { useServerStore } from '@/stores/serverStore'; | ||
|
|
||
| export function GenerationSettings() { | ||
| const maxChunkChars = useServerStore((state) => state.maxChunkChars); | ||
| const setMaxChunkChars = useServerStore((state) => state.setMaxChunkChars); | ||
| const crossfadeMs = useServerStore((state) => state.crossfadeMs); | ||
| const setCrossfadeMs = useServerStore((state) => state.setCrossfadeMs); | ||
|
|
||
| return ( | ||
| <Card role="region" aria-label="Generation Settings" tabIndex={0}> | ||
| <CardHeader> | ||
| <CardTitle>Generation Settings</CardTitle> | ||
| <CardDescription> | ||
| Controls for long text generation. These settings apply to all engines. | ||
| </CardDescription> | ||
| </CardHeader> | ||
| <CardContent> | ||
| <div className="space-y-6"> | ||
| <div className="space-y-3"> | ||
| <div className="flex items-center justify-between"> | ||
| <label htmlFor="maxChunkChars" className="text-sm font-medium leading-none"> | ||
| Auto-chunking limit | ||
| </label> | ||
| <span className="text-sm tabular-nums text-muted-foreground"> | ||
| {maxChunkChars} chars | ||
| </span> | ||
| </div> | ||
| <Slider | ||
| id="maxChunkChars" | ||
| value={[maxChunkChars]} | ||
| onValueChange={([value]) => setMaxChunkChars(value)} | ||
| min={100} | ||
| max={2000} | ||
| step={50} | ||
| aria-label="Auto-chunking character limit" | ||
| /> | ||
| <p className="text-sm text-muted-foreground"> | ||
| Long text is split into chunks at sentence boundaries before generating. Lower values | ||
| can improve quality for long outputs. | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="space-y-3"> | ||
| <div className="flex items-center justify-between"> | ||
| <label htmlFor="crossfadeMs" className="text-sm font-medium leading-none"> | ||
| Chunk crossfade | ||
| </label> | ||
| <span className="text-sm tabular-nums text-muted-foreground"> | ||
| {crossfadeMs === 0 ? 'Cut' : `${crossfadeMs}ms`} | ||
| </span> | ||
| </div> | ||
| <Slider | ||
| id="crossfadeMs" | ||
| value={[crossfadeMs]} | ||
| onValueChange={([value]) => setCrossfadeMs(value)} | ||
| min={0} | ||
| max={200} | ||
| step={10} | ||
| aria-label="Chunk crossfade duration" | ||
| /> | ||
| <p className="text-sm text-muted-foreground"> | ||
| Blends audio between chunks to smooth transitions. Set to 0 for a hard cut. | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -342,17 +342,18 @@ export function ModelManagement() { | |||||||||||||||||||||||||
| setDetailOpen(true); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const ttsModels = modelStatus?.models.filter((m) => m.model_name.startsWith('qwen-tts')) ?? []; | ||||||||||||||||||||||||||
| const otherTtsModels = | ||||||||||||||||||||||||||
| const voiceModels = | ||||||||||||||||||||||||||
| modelStatus?.models.filter( | ||||||||||||||||||||||||||
| (m) => m.model_name.startsWith('luxtts') || m.model_name.startsWith('chatterbox'), | ||||||||||||||||||||||||||
| (m) => | ||||||||||||||||||||||||||
| m.model_name.startsWith('qwen-tts') || | ||||||||||||||||||||||||||
| m.model_name.startsWith('luxtts') || | ||||||||||||||||||||||||||
| m.model_name.startsWith('chatterbox'), | ||||||||||||||||||||||||||
| ) ?? []; | ||||||||||||||||||||||||||
| const whisperModels = modelStatus?.models.filter((m) => m.model_name.startsWith('whisper')) ?? []; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Build sections | ||||||||||||||||||||||||||
| const sections: { label: string; models: ModelStatus[] }[] = [ | ||||||||||||||||||||||||||
| { label: 'Voice Generation', models: ttsModels }, | ||||||||||||||||||||||||||
| ...(otherTtsModels.length > 0 ? [{ label: 'Other Voice Models', models: otherTtsModels }] : []), | ||||||||||||||||||||||||||
| { label: 'Voice Generation', models: voiceModels }, | ||||||||||||||||||||||||||
| { label: 'Transcription', models: whisperModels }, | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
@@ -564,12 +565,6 @@ export function ModelManagement() { | |||||||||||||||||||||||||
| Loaded | ||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| {freshSelectedModel.downloaded && !freshSelectedModel.loaded && ( | ||||||||||||||||||||||||||
| <Badge variant="secondary" className="text-xs"> | ||||||||||||||||||||||||||
| <CircleCheck className="h-3 w-3 mr-1" /> | ||||||||||||||||||||||||||
| Downloaded | ||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| {selectedState?.hasError && ( | ||||||||||||||||||||||||||
| <Badge variant="destructive" className="text-xs"> | ||||||||||||||||||||||||||
| <CircleX className="h-3 w-3 mr-1" /> | ||||||||||||||||||||||||||
|
|
@@ -595,24 +590,6 @@ export function ModelManagement() { | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {hfModelInfo && ( | ||||||||||||||||||||||||||
| <div className="space-y-3"> | ||||||||||||||||||||||||||
| {/* Stats row */} | ||||||||||||||||||||||||||
| <div className="flex items-center gap-4 text-xs text-muted-foreground"> | ||||||||||||||||||||||||||
| <span className="flex items-center gap-1" title="Downloads"> | ||||||||||||||||||||||||||
| <Download className="h-3.5 w-3.5" /> | ||||||||||||||||||||||||||
| {formatDownloads(hfModelInfo.downloads)} | ||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||
| <span className="flex items-center gap-1" title="Likes"> | ||||||||||||||||||||||||||
| <Heart className="h-3.5 w-3.5" /> | ||||||||||||||||||||||||||
| {formatDownloads(hfModelInfo.likes)} | ||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||
| {license && ( | ||||||||||||||||||||||||||
| <span className="flex items-center gap-1" title="License"> | ||||||||||||||||||||||||||
| <Scale className="h-3.5 w-3.5" /> | ||||||||||||||||||||||||||
| {formatLicense(license)} | ||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Pipeline tag + author */} | ||||||||||||||||||||||||||
| <div className="flex flex-wrap gap-1.5"> | ||||||||||||||||||||||||||
| {hfModelInfo.pipeline_tag && ( | ||||||||||||||||||||||||||
|
|
@@ -632,6 +609,24 @@ export function ModelManagement() { | |||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Stats row */} | ||||||||||||||||||||||||||
| <div className="flex items-center gap-4 text-xs text-muted-foreground"> | ||||||||||||||||||||||||||
| <span className="flex items-center gap-1" title="Downloads"> | ||||||||||||||||||||||||||
| <Download className="h-3.5 w-3.5" /> | ||||||||||||||||||||||||||
| {formatDownloads(hfModelInfo.downloads)} | ||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||
| <span className="flex items-center gap-1" title="Likes"> | ||||||||||||||||||||||||||
| <Heart className="h-3.5 w-3.5" /> | ||||||||||||||||||||||||||
| {formatDownloads(hfModelInfo.likes)} | ||||||||||||||||||||||||||
|
Comment on lines
+615
to
+620
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Verify local typing and current call-site assumptions for HuggingFace stats fields.
# 1) Check HuggingFaceModelInfo field nullability in frontend types
fd "types.ts" app/src -t f -x rg -n "interface HuggingFaceModelInfo|downloads|likes" {}
# 2) Inspect all formatDownloads call sites in this file
rg -n "formatDownloads\\(" app/src/components/ServerSettings/ModelManagement.tsx -C 2Repository: jamiepine/voicebox Length of output: 914 🏁 Script executed: # Get more context around the problematic lines
sed -n '590,650p' app/src/components/ServerSettings/ModelManagement.tsx | cat -nRepository: jamiepine/voicebox Length of output: 3353 🏁 Script executed: # Check where hfModelInfo is defined/assigned
rg -n "hfModelInfo\s*=" app/src/components/ServerSettings/ModelManagement.tsx -B 2 -A 2Repository: jamiepine/voicebox Length of output: 44 🏁 Script executed: # Look for where this JSX is rendered and how hfModelInfo is obtained
rg -n "hfModelInfo" app/src/components/ServerSettings/ModelManagement.tsx | head -20Repository: jamiepine/voicebox Length of output: 1215 🏁 Script executed: # Get the complete HuggingFaceModelInfo interface definition
sed -n '131,150p' app/src/components/ServerSettings/ModelManagement.tsx | cat -nRepository: jamiepine/voicebox Length of output: 824 🏁 Script executed: # Also check the formatDownloads function to see if it guards against undefined
sed -n '51,60p' app/src/components/ServerSettings/ModelManagement.tsx | cat -nRepository: jamiepine/voicebox Length of output: 459 🏁 Script executed: # Get HuggingFaceModelInfo interface from the file
rg -n "export interface HuggingFaceModelInfo" app/src/components/ServerSettings/ModelManagement.tsx -A 20Repository: jamiepine/voicebox Length of output: 44 🏁 Script executed: # Find types.ts files in the project
fd "types.ts" app/src -type fRepository: jamiepine/voicebox Length of output: 232 🏁 Script executed: # Check if HuggingFaceModelInfo is imported or defined elsewhere
rg -n "interface HuggingFaceModelInfo" app/src --type tsRepository: jamiepine/voicebox Length of output: 130 🏁 Script executed: # Get the full HuggingFaceModelInfo interface definition
sed -n '131,165p' app/src/lib/api/types.ts | cat -nRepository: jamiepine/voicebox Length of output: 1030 Guard HF stats formatting against missing numeric fields. At line 616 and 620, unguarded 🛡️ Proposed fix- {formatDownloads(hfModelInfo.downloads)}
+ {formatDownloads(hfModelInfo.downloads ?? 0)}
...
- {formatDownloads(hfModelInfo.likes)}
+ {formatDownloads(hfModelInfo.likes ?? 0)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||
| {license && ( | ||||||||||||||||||||||||||
| <span className="flex items-center gap-1" title="License"> | ||||||||||||||||||||||||||
| <Scale className="h-3.5 w-3.5" /> | ||||||||||||||||||||||||||
| {formatLicense(license)} | ||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Languages */} | ||||||||||||||||||||||||||
| {hfModelInfo.cardData?.language && hfModelInfo.cardData.language.length > 0 && ( | ||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||
|
|
@@ -647,8 +642,8 @@ export function ModelManagement() { | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Disk size */} | ||||||||||||||||||||||||||
| {freshSelectedModel.downloaded && freshSelectedModel.size_mb && ( | ||||||||||||||||||||||||||
| <div className="flex items-center gap-2 text-sm text-muted-foreground"> | ||||||||||||||||||||||||||
| <HardDrive className="h-4 w-4" /> | ||||||||||||||||||||||||||
| <div className="flex items-center gap-2 text-xs text-muted-foreground"> | ||||||||||||||||||||||||||
| <HardDrive className="h-3.5 w-3.5" /> | ||||||||||||||||||||||||||
| <span>{formatSize(freshSelectedModel.size_mb)} on disk</span> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
|
|
@@ -661,7 +656,7 @@ export function ModelManagement() { | |||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Actions */} | ||||||||||||||||||||||||||
| <div className="flex items-center gap-2 pt-2 border-t"> | ||||||||||||||||||||||||||
| <div className="flex items-center gap-2 pt-2"> | ||||||||||||||||||||||||||
| {selectedState?.hasError ? ( | ||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Render VRAM badge when usage is
0 MB.Line 116 uses a truthy check, so
0won’t render even though it’s valid data.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents