AJ50 commited on
Commit
abd73a3
·
1 Parent(s): e049981

Add Song Generation UI component with language toggle and 3-tab navigation

Browse files
frontend/src/components/common/TabNavigation.tsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import { Mic, Music, Disc3 } from 'lucide-react'
3
+
4
+ interface TabNavigationProps {
5
+ activeTab: 'enrollment' | 'synthesis' | 'song-generation'
6
+ onTabChange: (tab: 'enrollment' | 'synthesis' | 'song-generation') => void
7
+ }
8
+
9
+ export const TabNavigation: React.FC<TabNavigationProps> = ({ activeTab, onTabChange }) => {
10
+ const tabs = [
11
+ { id: 'enrollment', label: 'Voice Enrollment', icon: Mic },
12
+ { id: 'synthesis', label: 'Speech Synthesis', icon: Music },
13
+ { id: 'song-generation', label: 'Song Generation', icon: Disc3 },
14
+ ] as const
15
+
16
+ return (
17
+ <div className="flex gap-2 mb-6 bg-white p-2 rounded-lg border border-gray-200">
18
+ {tabs.map(({ id, label, icon: Icon }) => (
19
+ <button
20
+ key={id}
21
+ onClick={() => onTabChange(id)}
22
+ className={`flex-1 py-2 px-3 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${
23
+ activeTab === id
24
+ ? 'bg-blue-600 text-white shadow-md'
25
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
26
+ }`}
27
+ >
28
+ <Icon className="h-4 w-4" />
29
+ <span className="hidden sm:inline">{label}</span>
30
+ <span className="sm:hidden text-xs">{label.split(' ')[0]}</span>
31
+ </button>
32
+ ))}
33
+ </div>
34
+ )
35
+ }
frontend/src/components/forms/SongGeneration.tsx ADDED
@@ -0,0 +1,285 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+ import { Alert, AlertDescription } from '@/components/ui/alert'
3
+ import { AlertCircle, Upload, Music, Loader2 } from 'lucide-react'
4
+
5
+ interface Voice {
6
+ id: string
7
+ name: string
8
+ createdAt: string
9
+ }
10
+
11
+ interface SongGenerationProps {
12
+ voices: Voice[]
13
+ language: 'english' | 'hindi'
14
+ onLanguageChange: (lang: 'english' | 'hindi') => void
15
+ }
16
+
17
+ export const SongGeneration: React.FC<SongGenerationProps> = ({
18
+ voices,
19
+ language,
20
+ onLanguageChange,
21
+ }) => {
22
+ const [songFile, setSongFile] = useState<File | null>(null)
23
+ const [selectedVoice, setSelectedVoice] = useState<string>(voices[0]?.id || '')
24
+ const [addEffects, setAddEffects] = useState(true)
25
+ const [isConverting, setIsConverting] = useState(false)
26
+ const [progress, setProgress] = useState(0)
27
+ const [outputAudio, setOutputAudio] = useState<string | null>(null)
28
+ const [error, setError] = useState<string | null>(null)
29
+ const [successMessage, setSuccessMessage] = useState<string | null>(null)
30
+
31
+ const handleSongSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
32
+ const file = e.target.files?.[0]
33
+ if (file) {
34
+ if (!['audio/mpeg', 'audio/wav', 'audio/mp4', 'audio/flac'].includes(file.type)) {
35
+ setError('Please select a valid audio file (MP3, WAV, M4A, FLAC)')
36
+ setSongFile(null)
37
+ return
38
+ }
39
+ setSongFile(file)
40
+ setError(null)
41
+ }
42
+ }
43
+
44
+ const handleConvertSong = async () => {
45
+ if (!songFile) {
46
+ setError('Please select a song file')
47
+ return
48
+ }
49
+
50
+ if (!selectedVoice) {
51
+ setError('Please select an enrolled voice')
52
+ return
53
+ }
54
+
55
+ setIsConverting(true)
56
+ setProgress(0)
57
+ setError(null)
58
+ setSuccessMessage(null)
59
+
60
+ try {
61
+ const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000'
62
+
63
+ // Simulate progress
64
+ const progressInterval = setInterval(() => {
65
+ setProgress((prev) => Math.min(prev + 10, 90))
66
+ }, 2000)
67
+
68
+ const formData = new FormData()
69
+ formData.append('song', songFile)
70
+ formData.append('voice_id', selectedVoice)
71
+ formData.append('language', language)
72
+ formData.append('add_effects', addEffects ? 'true' : 'false')
73
+
74
+ console.log('Converting song with:', {
75
+ voice: selectedVoice,
76
+ language,
77
+ addEffects,
78
+ })
79
+
80
+ const response = await fetch(`${API_BASE_URL}/api/convert_song`, {
81
+ method: 'POST',
82
+ body: formData,
83
+ })
84
+
85
+ clearInterval(progressInterval)
86
+ setProgress(100)
87
+
88
+ if (!response.ok) {
89
+ const errorData = await response.json()
90
+ throw new Error(errorData.error || `Server error: ${response.status}`)
91
+ }
92
+
93
+ const result = await response.json()
94
+
95
+ if (result.success) {
96
+ const audioUrl = `${API_BASE_URL}${result.audio_url}`
97
+ setOutputAudio(audioUrl)
98
+ setSuccessMessage('✅ Song converted successfully! Your voice is now in the song.')
99
+ setSongFile(null)
100
+ setProgress(0)
101
+ } else {
102
+ throw new Error(result.error || 'Conversion failed')
103
+ }
104
+ } catch (err) {
105
+ console.error('Song conversion error:', err)
106
+ setError(err instanceof Error ? err.message : 'Failed to convert song')
107
+ setProgress(0)
108
+ } finally {
109
+ setIsConverting(false)
110
+ }
111
+ }
112
+
113
+ return (
114
+ <div className="w-full space-y-6">
115
+ {/* Language Selector */}
116
+ <div className="flex gap-2">
117
+ <button
118
+ onClick={() => onLanguageChange('english')}
119
+ className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all ${
120
+ language === 'english'
121
+ ? 'bg-blue-600 text-white shadow-lg'
122
+ : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
123
+ }`}
124
+ >
125
+ 🇬🇧 English
126
+ </button>
127
+ <button
128
+ onClick={() => onLanguageChange('hindi')}
129
+ className={`flex-1 py-2 px-4 rounded-lg font-medium transition-all ${
130
+ language === 'hindi'
131
+ ? 'bg-orange-600 text-white shadow-lg'
132
+ : 'bg-gray-200 text-gray-700 hover:bg-gray-300'
133
+ }`}
134
+ >
135
+ 🇮🇳 हिन्दी
136
+ </button>
137
+ </div>
138
+
139
+ {/* Info Alert */}
140
+ <Alert className="bg-blue-50 border-blue-200">
141
+ <Music className="h-4 w-4 text-blue-600" />
142
+ <AlertDescription className="text-blue-800">
143
+ <strong>How it works:</strong> Upload a song → Select your enrolled voice →
144
+ Your voice will replace the original singer! (Quality: 6-7/10, Processing time: 6-10 min)
145
+ </AlertDescription>
146
+ </Alert>
147
+
148
+ {/* Song Upload */}
149
+ <div className="space-y-2">
150
+ <label className="block text-sm font-medium text-gray-700">
151
+ 📁 Upload Song File (MP3, WAV, M4A, FLAC)
152
+ </label>
153
+ <div className="relative">
154
+ <input
155
+ type="file"
156
+ accept="audio/*"
157
+ onChange={handleSongSelect}
158
+ disabled={isConverting}
159
+ className="w-full px-4 py-2 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:border-blue-500 disabled:opacity-50"
160
+ />
161
+ <Upload className="absolute right-3 top-3 h-5 w-5 text-gray-400" />
162
+ </div>
163
+ {songFile && (
164
+ <p className="text-sm text-green-600 font-medium">
165
+ ✓ Song selected: {songFile.name} ({(songFile.size / 1024 / 1024).toFixed(1)}MB)
166
+ </p>
167
+ )}
168
+ </div>
169
+
170
+ {/* Voice Selection */}
171
+ <div className="space-y-2">
172
+ <label className="block text-sm font-medium text-gray-700">
173
+ 🎤 Select Enrolled Voice
174
+ </label>
175
+ {voices.length === 0 ? (
176
+ <p className="text-sm text-gray-500 italic">No voices enrolled. Please enroll a voice first.</p>
177
+ ) : (
178
+ <select
179
+ value={selectedVoice}
180
+ onChange={(e) => setSelectedVoice(e.target.value)}
181
+ disabled={isConverting}
182
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50"
183
+ >
184
+ <option value="">-- Select a voice --</option>
185
+ {voices.map((voice) => (
186
+ <option key={voice.id} value={voice.id}>
187
+ {voice.name} (Added {new Date(voice.createdAt).toLocaleDateString()})
188
+ </option>
189
+ ))}
190
+ </select>
191
+ )}
192
+ </div>
193
+
194
+ {/* Effects Toggle */}
195
+ <div className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
196
+ <input
197
+ type="checkbox"
198
+ id="addEffects"
199
+ checked={addEffects}
200
+ onChange={(e) => setAddEffects(e.target.checked)}
201
+ disabled={isConverting}
202
+ className="w-4 h-4 cursor-pointer"
203
+ />
204
+ <label htmlFor="addEffects" className="flex-1 text-sm font-medium text-gray-700 cursor-pointer">
205
+ ✨ Add Effects (Reverb & Compression)
206
+ </label>
207
+ </div>
208
+
209
+ {/* Error Alert */}
210
+ {error && (
211
+ <Alert className="bg-red-50 border-red-200">
212
+ <AlertCircle className="h-4 w-4 text-red-600" />
213
+ <AlertDescription className="text-red-800">{error}</AlertDescription>
214
+ </Alert>
215
+ )}
216
+
217
+ {/* Success Alert */}
218
+ {successMessage && (
219
+ <Alert className="bg-green-50 border-green-200">
220
+ <AlertDescription className="text-green-800">{successMessage}</AlertDescription>
221
+ </Alert>
222
+ )}
223
+
224
+ {/* Progress Bar */}
225
+ {isConverting && progress > 0 && (
226
+ <div className="space-y-2">
227
+ <div className="flex justify-between text-sm">
228
+ <span className="text-gray-700 font-medium">Converting...</span>
229
+ <span className="text-gray-600">{progress}%</span>
230
+ </div>
231
+ <div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
232
+ <div
233
+ className="h-full bg-gradient-to-r from-blue-500 to-purple-500 transition-all duration-300"
234
+ style={{ width: `${progress}%` }}
235
+ />
236
+ </div>
237
+ </div>
238
+ )}
239
+
240
+ {/* Convert Button */}
241
+ <button
242
+ onClick={handleConvertSong}
243
+ disabled={!songFile || !selectedVoice || isConverting}
244
+ className="w-full py-3 px-4 bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg font-semibold hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
245
+ >
246
+ {isConverting ? (
247
+ <>
248
+ <Loader2 className="h-5 w-5 animate-spin" />
249
+ Converting Your Song...
250
+ </>
251
+ ) : (
252
+ <>
253
+ <Music className="h-5 w-5" />
254
+ 🎬 Convert Song to My Voice
255
+ </>
256
+ )}
257
+ </button>
258
+
259
+ {/* Output Audio */}
260
+ {outputAudio && (
261
+ <div className="space-y-3 p-4 bg-green-50 rounded-lg border border-green-200">
262
+ <h3 className="font-semibold text-green-900">🎉 Your Song is Ready!</h3>
263
+ <audio
264
+ controls
265
+ src={outputAudio}
266
+ className="w-full rounded-lg"
267
+ />
268
+ <a
269
+ href={outputAudio}
270
+ download="converted_song.wav"
271
+ className="inline-block px-4 py-2 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 transition-colors"
272
+ >
273
+ 📥 Download Song
274
+ </a>
275
+ </div>
276
+ )}
277
+
278
+ {/* Quality Info */}
279
+ <div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
280
+ <strong>⚠️ Quality Note:</strong> Output is AI-processed (6-7/10 quality).
281
+ Works best with pop/rock songs. Enable effects for better sound!
282
+ </div>
283
+ </div>
284
+ )
285
+ }
frontend/src/pages/Index.tsx CHANGED
@@ -3,9 +3,10 @@ import { Button } from '@/components/ui/button';
3
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
5
  import { Badge } from '@/components/ui/badge';
6
- import { Mic, Volume2, Brain, Globe, Zap, Users, Sparkles, Sun, Moon, ArrowDown } from 'lucide-react';
7
  import VoiceEnrollment from '@/components/forms/VoiceEnrollment';
8
  import SpeechSynthesis from '@/components/forms/SpeechSynthesis';
 
9
  import ParticleField from '@/components/three/ParticleField';
10
  import FloatingElements from '@/components/three/FloatingElements';
11
  import ErrorBoundary from '@/components/common/ErrorBoundary';
@@ -21,6 +22,7 @@ interface Voice {
21
 
22
  const Index = () => {
23
  const [enrolledVoices, setEnrolledVoices] = useState<Voice[]>([]);
 
24
  const { toast } = useToast();
25
  const [theme, setTheme] = useState<'dark' | 'light'>('dark');
26
 
@@ -199,14 +201,21 @@ const Index = () => {
199
  </div>
200
 
201
  <Tabs defaultValue="enroll" className="w-full">
202
- <TabsList className="grid w-full grid-cols-2 mb-8">
203
  <TabsTrigger value="enroll" className="flex items-center space-x-2">
204
  <Users className="w-4 h-4" />
205
- <span>Voice Enrollment</span>
 
206
  </TabsTrigger>
207
  <TabsTrigger value="synthesize" className="flex items-center space-x-2">
208
  <Volume2 className="w-4 h-4" />
209
- <span>Speech Synthesis</span>
 
 
 
 
 
 
210
  </TabsTrigger>
211
  </TabsList>
212
 
@@ -266,6 +275,35 @@ const Index = () => {
266
  </Card>
267
  )}
268
  </TabsContent>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  </Tabs>
270
  </div>
271
  </section>
 
3
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
4
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
5
  import { Badge } from '@/components/ui/badge';
6
+ import { Mic, Volume2, Brain, Globe, Zap, Users, Sparkles, Sun, Moon, ArrowDown, Music } from 'lucide-react';
7
  import VoiceEnrollment from '@/components/forms/VoiceEnrollment';
8
  import SpeechSynthesis from '@/components/forms/SpeechSynthesis';
9
+ import { SongGeneration } from '@/components/forms/SongGeneration';
10
  import ParticleField from '@/components/three/ParticleField';
11
  import FloatingElements from '@/components/three/FloatingElements';
12
  import ErrorBoundary from '@/components/common/ErrorBoundary';
 
22
 
23
  const Index = () => {
24
  const [enrolledVoices, setEnrolledVoices] = useState<Voice[]>([]);
25
+ const [language, setLanguage] = useState<'english' | 'hindi'>('english');
26
  const { toast } = useToast();
27
  const [theme, setTheme] = useState<'dark' | 'light'>('dark');
28
 
 
201
  </div>
202
 
203
  <Tabs defaultValue="enroll" className="w-full">
204
+ <TabsList className="grid w-full grid-cols-3 mb-8">
205
  <TabsTrigger value="enroll" className="flex items-center space-x-2">
206
  <Users className="w-4 h-4" />
207
+ <span className="hidden sm:inline">Voice Enrollment</span>
208
+ <span className="sm:hidden">Enroll</span>
209
  </TabsTrigger>
210
  <TabsTrigger value="synthesize" className="flex items-center space-x-2">
211
  <Volume2 className="w-4 h-4" />
212
+ <span className="hidden sm:inline">Speech Synthesis</span>
213
+ <span className="sm:hidden">Synth</span>
214
+ </TabsTrigger>
215
+ <TabsTrigger value="song" className="flex items-center space-x-2">
216
+ <Music className="w-4 h-4" />
217
+ <span className="hidden sm:inline">Song Generation</span>
218
+ <span className="sm:hidden">Song</span>
219
  </TabsTrigger>
220
  </TabsList>
221
 
 
275
  </Card>
276
  )}
277
  </TabsContent>
278
+
279
+ <TabsContent value="song" className="space-y-6">
280
+ {enrolledVoices.length > 0 ? (
281
+ <SongGeneration
282
+ voices={enrolledVoices}
283
+ language={language}
284
+ onLanguageChange={setLanguage}
285
+ />
286
+ ) : (
287
+ <Card className="glass-effect border-dashed border-accent/50">
288
+ <CardContent className="flex flex-col items-center justify-center py-12">
289
+ <Music className="w-16 h-16 text-muted-foreground mb-4" />
290
+ <h3 className="text-lg font-medium mb-2">No voices enrolled</h3>
291
+ <p className="text-muted-foreground text-center mb-4">
292
+ Enroll your voice first to generate songs with your voice
293
+ </p>
294
+ <Button
295
+ variant="outline"
296
+ onClick={() => {
297
+ const enrollTab = document.querySelector('[value="enroll"]') as HTMLElement;
298
+ enrollTab?.click();
299
+ }}
300
+ >
301
+ Go to Voice Enrollment
302
+ </Button>
303
+ </CardContent>
304
+ </Card>
305
+ )}
306
+ </TabsContent>
307
  </Tabs>
308
  </div>
309
  </section>