Spaces:
Sleeping
Sleeping
File size: 6,797 Bytes
5008b66 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Upload, Mic } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import api from '@/services/api';
import AudioRecorder from '../audio/AudioRecorder';
interface VoiceEnrollmentProps {
onEnrollmentComplete?: (voiceData: any) => void;
className?: string;
}
export default function VoiceEnrollment({ onEnrollmentComplete, className = "" }: VoiceEnrollmentProps) {
const [voiceName, setVoiceName] = useState('');
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [recordedAudio, setRecordedAudio] = useState<{ blob: Blob; url: string } | null>(null);
const [isUploading, setIsUploading] = useState(false);
const { toast } = useToast();
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
if (file.type.startsWith('audio/')) {
setSelectedFile(file);
setRecordedAudio(null); // Clear recorded audio if file is selected
} else {
toast({
title: "Invalid file type",
description: "Please select an audio file (.mp3, .wav, .m4a, etc.)",
variant: "destructive"
});
}
}
};
const handleRecordingComplete = (audioBlob: Blob, audioUrl: string) => {
setRecordedAudio({ blob: audioBlob, url: audioUrl });
setSelectedFile(null); // Clear file selection if recording is made
};
const handleEnrollment = async () => {
if (!voiceName.trim()) {
toast({
title: "Voice name required",
description: "Please enter a name for this voice",
variant: "destructive"
});
return;
}
if (!selectedFile && !recordedAudio) {
toast({
title: "No audio provided",
description: "Please either upload an audio file or record your voice",
variant: "destructive"
});
return;
}
setIsUploading(true);
try {
// Prepare form data for API call
const formData = new FormData();
formData.append('voice_name', voiceName);
if (selectedFile) {
formData.append('audio', selectedFile);
} else if (recordedAudio) {
// Convert blob to file
const file = new File([recordedAudio.blob], `${voiceName}.wav`, { type: 'audio/wav' });
formData.append('audio', file);
}
// Call backend API
const result = await api.enrollVoice(formData);
const voiceData = {
id: result.voice_id,
name: voiceName,
audioData: selectedFile || recordedAudio?.blob,
audioUrl: selectedFile ? URL.createObjectURL(selectedFile) : recordedAudio?.url,
createdAt: new Date().toISOString()
};
onEnrollmentComplete?.(voiceData);
toast({
title: "Voice enrolled successfully!",
description: `Voice "${voiceName}" has been added to your collection`
});
// Reset form
setVoiceName('');
setSelectedFile(null);
setRecordedAudio(null);
} catch (error) {
console.error('Enrollment error:', error);
toast({
title: "Enrollment failed",
description: error instanceof Error ? error.message : "There was an error enrolling your voice. Please try again.",
variant: "destructive"
});
} finally {
setIsUploading(false);
}
};
return (
<Card className={`glass-effect ${className}`}>
<CardHeader>
<CardTitle className="gradient-text">Enroll Your Voice</CardTitle>
<CardDescription>
Create a voice profile by uploading an audio file or recording directly
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Voice Name Input */}
<div className="space-y-2">
<Label htmlFor="voice-name">Voice Name</Label>
<Input
id="voice-name"
type="text"
placeholder="e.g., My Voice, Professional Tone, etc."
value={voiceName}
onChange={(e) => setVoiceName(e.target.value)}
className="bg-surface border-border"
/>
</div>
{/* Audio Input Tabs */}
<Tabs defaultValue="record" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="record" className="flex items-center space-x-2">
<Mic className="w-4 h-4" />
<span>Record</span>
</TabsTrigger>
<TabsTrigger value="upload" className="flex items-center space-x-2">
<Upload className="w-4 h-4" />
<span>Upload</span>
</TabsTrigger>
</TabsList>
<TabsContent value="record" className="space-y-4">
<AudioRecorder onRecordingComplete={handleRecordingComplete} />
{recordedAudio && (
<div className="text-center text-sm text-muted-foreground">
✓ Recording completed
</div>
)}
</TabsContent>
<TabsContent value="upload" className="space-y-4">
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center">
<Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
<Label htmlFor="audio-upload" className="cursor-pointer">
<span className="text-lg font-medium">Choose audio file</span>
<p className="text-sm text-muted-foreground mt-2">
Supports MP3, WAV, M4A and other audio formats
</p>
</Label>
<Input
id="audio-upload"
type="file"
accept="audio/*"
onChange={handleFileSelect}
className="hidden"
/>
</div>
{selectedFile && (
<div className="text-center text-sm text-muted-foreground">
✓ Selected: {selectedFile.name}
</div>
)}
</TabsContent>
</Tabs>
{/* Enrollment Button */}
<Button
onClick={handleEnrollment}
disabled={isUploading || !voiceName.trim() || (!selectedFile && !recordedAudio)}
size="lg"
className="w-full bg-primary hover:bg-primary/90 glow-primary"
>
{isUploading ? 'Enrolling Voice...' : 'Enroll Voice'}
</Button>
</CardContent>
</Card>
);
} |