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:
Jamie Pine
2026-01-28 14:30:33 -08:00
parent 07a91a2381
commit 7208f51eee
14 changed files with 320 additions and 364 deletions
@@ -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...