@@ -8,6 +8,8 @@ import { Textarea } from "@/components/ui/textarea";
8
8
import { uploadAudioFile , checkVoiceStatus , VoiceStatus , VoiceStatusResponse , VoiceInfo , listAvailableVoices } from '../lib/bytedanceTts' ;
9
9
import { logger } from '../utils/logger' ;
10
10
import { Alert , AlertDescription } from "@/components/ui/alert" ;
11
+ import { Tabs , TabsContent , TabsList , TabsTrigger } from "@/components/ui/tabs" ;
12
+ import { Mic , Square , Upload } from "lucide-react" ;
11
13
12
14
const ModelName = "ByteDanceRecorder" ;
13
15
@@ -45,6 +47,14 @@ export function ByteDanceRecorder({ onVoiceCloned }: ByteDanceRecorderProps) {
45
47
const [ noiseReduction , setNoiseReduction ] = useState ( true ) ;
46
48
const [ volumeNormalization , setVolumeNormalization ] = useState ( true ) ;
47
49
50
+ // Add new recording related state
51
+ const [ isRecording , setIsRecording ] = useState ( false ) ;
52
+ const [ duration , setDuration ] = useState ( 0 ) ;
53
+ const mediaRecorder = useRef < MediaRecorder | null > ( null ) ;
54
+ const chunks = useRef < Blob [ ] > ( [ ] ) ;
55
+ const timerRef = useRef < number > ( ) ;
56
+ const maxDuration = 300 ; // 5 minutes in seconds
57
+
48
58
// 加载可用音色列表
49
59
useEffect ( ( ) => {
50
60
async function loadVoices ( ) {
@@ -217,6 +227,80 @@ export function ByteDanceRecorder({ onVoiceCloned }: ByteDanceRecorderProps) {
217
227
) ;
218
228
} ;
219
229
230
+ // Add recording functions
231
+ const startRecording = async ( ) => {
232
+ try {
233
+ const stream = await navigator . mediaDevices . getUserMedia ( { audio : true } ) ;
234
+ mediaRecorder . current = new MediaRecorder ( stream ) ;
235
+
236
+ mediaRecorder . current . ondataavailable = ( e ) => {
237
+ if ( e . data . size > 0 ) {
238
+ chunks . current . push ( e . data ) ;
239
+ }
240
+ } ;
241
+
242
+ mediaRecorder . current . onstop = async ( ) => {
243
+ const blob = new Blob ( chunks . current , { type : 'audio/wav' } ) ;
244
+ const file = new File ( [ blob ] , `recording-${ Date . now ( ) } .wav` , { type : 'audio/wav' } ) ;
245
+
246
+ try {
247
+ setIsUploading ( true ) ;
248
+ const voiceId = await uploadAudioFile ( file , selectedVoiceId , {
249
+ language,
250
+ modelType,
251
+ textValidation,
252
+ noiseReduction,
253
+ volumeNormalization
254
+ } ) ;
255
+
256
+ setCurrentVoiceId ( voiceId ) ;
257
+ setTrainingStatus ( { status : VoiceStatus . Training } ) ;
258
+ } catch ( error ) {
259
+ logger . log ( `Recording upload failed: ${ error } ` , 'ERROR' , ModelName ) ;
260
+ alert ( '上传失败,请重试' ) ;
261
+ } finally {
262
+ setIsUploading ( false ) ;
263
+ }
264
+
265
+ chunks . current = [ ] ;
266
+ setDuration ( 0 ) ;
267
+ } ;
268
+
269
+ mediaRecorder . current . start ( ) ;
270
+ setIsRecording ( true ) ;
271
+
272
+ // Start timer
273
+ timerRef . current = window . setInterval ( ( ) => {
274
+ setDuration ( prev => {
275
+ if ( prev >= maxDuration - 1 ) {
276
+ stopRecording ( ) ;
277
+ return 0 ;
278
+ }
279
+ return prev + 1 ;
280
+ } ) ;
281
+ } , 1000 ) ;
282
+
283
+ } catch ( error ) {
284
+ logger . log ( `Recording failed: ${ error } ` , 'ERROR' , ModelName ) ;
285
+ alert ( '无法访问麦克风' ) ;
286
+ }
287
+ } ;
288
+
289
+ const stopRecording = ( ) => {
290
+ if ( mediaRecorder . current && isRecording ) {
291
+ mediaRecorder . current . stop ( ) ;
292
+ mediaRecorder . current . stream . getTracks ( ) . forEach ( track => track . stop ( ) ) ;
293
+ clearInterval ( timerRef . current ) ;
294
+ setIsRecording ( false ) ;
295
+ }
296
+ } ;
297
+
298
+ const formatTime = ( seconds : number ) => {
299
+ const mins = Math . floor ( seconds / 60 ) ;
300
+ const secs = seconds % 60 ;
301
+ return `${ mins } :${ secs . toString ( ) . padStart ( 2 , '0' ) } ` ;
302
+ } ;
303
+
220
304
return (
221
305
< div className = "space-y-6" >
222
306
< div className = "space-y-4" >
@@ -254,24 +338,62 @@ export function ByteDanceRecorder({ onVoiceCloned }: ByteDanceRecorderProps) {
254
338
</ div >
255
339
</ div >
256
340
257
- < input
258
- type = "file"
259
- ref = { fileInputRef }
260
- onChange = { handleFileSelect }
261
- accept = ".mp3,.wav,.ogg,.m4a,.aac"
262
- className = "hidden"
263
- />
264
- < Button
265
- onClick = { ( ) => fileInputRef . current ?. click ( ) }
266
- disabled = { isUploading || ! selectedVoiceId }
267
- variant = "outline"
268
- className = "w-full"
269
- >
270
- { isUploading ? '上传中...' : '选择音频文件' }
271
- </ Button >
272
- { isUploading && (
273
- < Progress value = { uploadProgress } className = "w-full" />
274
- ) }
341
+ < Tabs defaultValue = "record" className = "w-full" >
342
+ < TabsList className = "grid w-full grid-cols-2" >
343
+ < TabsTrigger value = "record" > 实时录音</ TabsTrigger >
344
+ < TabsTrigger value = "upload" > 文件上传</ TabsTrigger >
345
+ </ TabsList >
346
+
347
+ < TabsContent value = "record" className = "space-y-4" >
348
+ < div className = "flex items-center gap-4" >
349
+ { isRecording ? (
350
+ < Button
351
+ variant = "destructive"
352
+ onClick = { stopRecording }
353
+ disabled = { ! selectedVoiceId }
354
+ className = "gap-2"
355
+ >
356
+ < Square className = "h-4 w-4" />
357
+ 停止录音 ({ formatTime ( duration ) } )
358
+ </ Button >
359
+ ) : (
360
+ < Button
361
+ onClick = { startRecording }
362
+ disabled = { ! selectedVoiceId }
363
+ className = "gap-2"
364
+ >
365
+ < Mic className = "h-4 w-4" />
366
+ 开始录音
367
+ </ Button >
368
+ ) }
369
+ </ div >
370
+ < div className = "text-sm text-muted-foreground" >
371
+ 最长录音时间:{ Math . floor ( maxDuration / 60 ) } 分钟
372
+ </ div >
373
+ </ TabsContent >
374
+
375
+ < TabsContent value = "upload" className = "space-y-4" >
376
+ < input
377
+ type = "file"
378
+ ref = { fileInputRef }
379
+ onChange = { handleFileSelect }
380
+ accept = ".mp3,.wav,.ogg,.m4a,.aac"
381
+ className = "hidden"
382
+ />
383
+ < Button
384
+ onClick = { ( ) => fileInputRef . current ?. click ( ) }
385
+ disabled = { isUploading || ! selectedVoiceId }
386
+ variant = "outline"
387
+ className = "w-full gap-2"
388
+ >
389
+ < Upload className = "h-4 w-4" />
390
+ { isUploading ? '上传中...' : '选择音频文件' }
391
+ </ Button >
392
+ { isUploading && (
393
+ < Progress value = { uploadProgress } className = "w-full" />
394
+ ) }
395
+ </ TabsContent >
396
+ </ Tabs >
275
397
</ div >
276
398
277
399
< div className = "space-y-4" >
0 commit comments