mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 13:20:39 -07:00
Refactor audio generation components and improve debugging capabilities
- Introduced useGenerationForm hook to streamline audio generation form handling, including validation and model download management. - Updated FloatingGenerateBox and GenerationForm components to utilize the new hook, enhancing code organization and reducing duplication. - Replaced console logging with a debug utility for better logging control during audio playback and generation processes. - Improved error handling in HistoryTable and MainEditor components by integrating toast notifications for user feedback. - Adjusted audio recording duration limits across various components for consistency.
This commit is contained in:
@@ -1,9 +1,6 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Loader2, Sparkles } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
|
||||
import {
|
||||
@@ -14,24 +11,11 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';
|
||||
import { useGeneration } from '@/lib/hooks/useGeneration';
|
||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||
import { LANGUAGE_OPTIONS } from '@/lib/constants/languages';
|
||||
import { useGenerationForm } from '@/lib/hooks/useGenerationForm';
|
||||
import { useProfile } from '@/lib/hooks/useProfiles';
|
||||
import { useGenerationStore } from '@/stores/generationStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
|
||||
const generationSchema = z.object({
|
||||
text: z.string().min(1, 'Text is required').max(5000),
|
||||
language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),
|
||||
modelSize: z.enum(['1.7B', '0.6B']).optional(),
|
||||
});
|
||||
|
||||
type GenerationFormValues = z.infer<typeof generationSchema>;
|
||||
|
||||
interface FloatingGenerateBoxProps {
|
||||
isPlayerOpen: boolean;
|
||||
}
|
||||
@@ -39,27 +23,12 @@ interface FloatingGenerateBoxProps {
|
||||
export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps) {
|
||||
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
|
||||
const { data: selectedProfile } = useProfile(selectedProfileId || '');
|
||||
const generation = useGeneration();
|
||||
const { toast } = useToast();
|
||||
const setAudio = usePlayerStore((state) => state.setAudio);
|
||||
const setIsGenerating = useGenerationStore((state) => state.setIsGenerating);
|
||||
const [downloadingModelName, setDownloadingModelName] = useState<string | null>(null);
|
||||
const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useModelDownloadToast({
|
||||
modelName: downloadingModelName || '',
|
||||
displayName: downloadingDisplayName || '',
|
||||
enabled: !!downloadingModelName,
|
||||
});
|
||||
|
||||
const form = useForm<GenerationFormValues>({
|
||||
resolver: zodResolver(generationSchema),
|
||||
defaultValues: {
|
||||
text: '',
|
||||
language: 'en',
|
||||
modelSize: '1.7B',
|
||||
const { form, handleSubmit, isPending } = useGenerationForm({
|
||||
onSuccess: () => {
|
||||
setIsExpanded(false);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,62 +62,8 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
||||
};
|
||||
}, [isExpanded]);
|
||||
|
||||
async function onSubmit(data: GenerationFormValues) {
|
||||
if (!selectedProfileId) {
|
||||
toast({
|
||||
title: 'No profile selected',
|
||||
description: 'Please select a voice profile from the cards above.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
|
||||
const modelName = `qwen-tts-${data.modelSize}`;
|
||||
const displayName = data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B';
|
||||
|
||||
try {
|
||||
const modelStatus = await apiClient.getModelStatus();
|
||||
const model = modelStatus.models.find((m) => m.model_name === modelName);
|
||||
|
||||
if (model && !model.downloaded) {
|
||||
setDownloadingModelName(modelName);
|
||||
setDownloadingDisplayName(displayName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check model status:', error);
|
||||
}
|
||||
|
||||
const result = await generation.mutateAsync({
|
||||
profile_id: selectedProfileId,
|
||||
text: data.text,
|
||||
language: data.language,
|
||||
model_size: data.modelSize,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Generation complete!',
|
||||
description: `Audio generated (${result.duration.toFixed(2)}s)`,
|
||||
});
|
||||
|
||||
const audioUrl = apiClient.getAudioUrl(result.id);
|
||||
setAudio(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
|
||||
|
||||
form.reset();
|
||||
setIsExpanded(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Generation failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to generate audio',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
setDownloadingModelName(null);
|
||||
setDownloadingDisplayName(null);
|
||||
}
|
||||
async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {
|
||||
await handleSubmit(data, selectedProfileId);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -168,7 +83,6 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
||||
<div className="flex gap-2">
|
||||
<motion.div
|
||||
className="flex-1"
|
||||
// animate={{ marginBottom: isExpanded ? '0.75rem' : '0' }}
|
||||
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||
>
|
||||
<FormField
|
||||
@@ -202,11 +116,11 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={generation.isPending || !selectedProfileId}
|
||||
disabled={isPending || !selectedProfileId}
|
||||
className="h-10 w-10 rounded-full bg-accent hover:bg-accent/90 hover:scale-105 text-accent-foreground shadow-lg hover:shadow-accent/50 shrink-0 transition-all duration-200"
|
||||
size="icon"
|
||||
>
|
||||
{generation.isPending ? (
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Sparkles className="h-4 w-4" />
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Loader2, Mic } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import * as z from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
@@ -23,118 +19,19 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { LANGUAGE_CODES, LANGUAGE_OPTIONS, type LanguageCode } from '@/lib/constants/languages';
|
||||
import { useGeneration } from '@/lib/hooks/useGeneration';
|
||||
import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast';
|
||||
import { LANGUAGE_OPTIONS } from '@/lib/constants/languages';
|
||||
import { useGenerationForm } from '@/lib/hooks/useGenerationForm';
|
||||
import { useProfile } from '@/lib/hooks/useProfiles';
|
||||
import { useGenerationStore } from '@/stores/generationStore';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useUIStore } from '@/stores/uiStore';
|
||||
|
||||
const generationSchema = z.object({
|
||||
text: z.string().min(1, 'Text is required').max(5000),
|
||||
language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]),
|
||||
seed: z.number().int().optional(),
|
||||
modelSize: z.enum(['1.7B', '0.6B']).optional(),
|
||||
instruct: z.string().max(500).optional(),
|
||||
});
|
||||
|
||||
type GenerationFormValues = z.infer<typeof generationSchema>;
|
||||
|
||||
export function GenerationForm() {
|
||||
const selectedProfileId = useUIStore((state) => state.selectedProfileId);
|
||||
const { data: selectedProfile } = useProfile(selectedProfileId || '');
|
||||
const generation = useGeneration();
|
||||
const { toast } = useToast();
|
||||
const setAudio = usePlayerStore((state) => state.setAudio);
|
||||
const setIsGenerating = useGenerationStore((state) => state.setIsGenerating);
|
||||
const [downloadingModelName, setDownloadingModelName] = useState<string | null>(null);
|
||||
const [downloadingDisplayName, setDownloadingDisplayName] = useState<string | null>(null);
|
||||
|
||||
// Use the download toast hook to show progress when model is downloading
|
||||
useModelDownloadToast({
|
||||
modelName: downloadingModelName || '',
|
||||
displayName: downloadingDisplayName || '',
|
||||
enabled: !!downloadingModelName,
|
||||
});
|
||||
const { form, handleSubmit, isPending } = useGenerationForm();
|
||||
|
||||
const form = useForm<GenerationFormValues>({
|
||||
resolver: zodResolver(generationSchema),
|
||||
defaultValues: {
|
||||
text: '',
|
||||
language: 'en',
|
||||
seed: undefined,
|
||||
modelSize: '1.7B',
|
||||
instruct: '',
|
||||
},
|
||||
});
|
||||
|
||||
async function onSubmit(data: GenerationFormValues) {
|
||||
if (!selectedProfileId) {
|
||||
toast({
|
||||
title: 'No profile selected',
|
||||
description: 'Please select a voice profile from the cards above.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsGenerating(true);
|
||||
|
||||
// Determine model name and display name
|
||||
const modelName = `qwen-tts-${data.modelSize}`;
|
||||
const displayName = data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B';
|
||||
|
||||
// Check if model is downloaded before starting generation
|
||||
try {
|
||||
const modelStatus = await apiClient.getModelStatus();
|
||||
const model = modelStatus.models.find((m) => m.model_name === modelName);
|
||||
|
||||
if (model && !model.downloaded) {
|
||||
// Model is not downloaded, enable download toast
|
||||
setDownloadingModelName(modelName);
|
||||
setDownloadingDisplayName(displayName);
|
||||
}
|
||||
} catch (error) {
|
||||
// If status check fails, continue anyway - generation will handle it
|
||||
console.error('Failed to check model status:', error);
|
||||
}
|
||||
|
||||
// Proceed with generation (which will trigger download if needed)
|
||||
const result = await generation.mutateAsync({
|
||||
profile_id: selectedProfileId,
|
||||
text: data.text,
|
||||
language: data.language,
|
||||
seed: data.seed,
|
||||
model_size: data.modelSize,
|
||||
instruct: data.instruct || undefined,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Generation complete!',
|
||||
description: `Audio generated (${result.duration.toFixed(2)}s)`,
|
||||
});
|
||||
|
||||
// Autoplay the generated audio
|
||||
const audioUrl = apiClient.getAudioUrl(result.id);
|
||||
setAudio(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
|
||||
|
||||
form.reset();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Generation failed',
|
||||
description: error instanceof Error ? error.message : 'Failed to generate audio',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
// Clear download state after generation completes
|
||||
setDownloadingModelName(null);
|
||||
setDownloadingDisplayName(null);
|
||||
}
|
||||
async function onSubmit(data: Parameters<typeof handleSubmit>[0]) {
|
||||
await handleSubmit(data, selectedProfileId);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -276,9 +173,9 @@ export function GenerationForm() {
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={generation.isPending || !selectedProfileId}
|
||||
disabled={isPending || !selectedProfileId}
|
||||
>
|
||||
{generation.isPending ? (
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Generating...
|
||||
|
||||
Reference in New Issue
Block a user