From 66716d23145838e30bcd7136f7a63966d95bc042 Mon Sep 17 00:00:00 2001 From: koplenov Date: Sun, 5 Apr 2026 02:31:48 +0300 Subject: [PATCH] replace voice recorder realization for support chrome --- src/components/deepfake/ChatterboxTTS.tsx | 620 ++++++++++++---------- 1 file changed, 326 insertions(+), 294 deletions(-) diff --git a/src/components/deepfake/ChatterboxTTS.tsx b/src/components/deepfake/ChatterboxTTS.tsx index a423e0e..0bfc190 100644 --- a/src/components/deepfake/ChatterboxTTS.tsx +++ b/src/components/deepfake/ChatterboxTTS.tsx @@ -1,356 +1,388 @@ import { useState, useRef, useEffect } from 'react'; import { Recorder } from '../../lib/recorder'; +import VoiceCards from './VoiceCards'; import './ChatterboxTTS.css'; const API_URL = 'https://back.hack.kinsle.ru/process-audio'; const ChatterboxTTS = () => { - // Recording state - const [isRecording, setIsRecording] = useState(false); - const [hasRecording, setHasRecording] = useState(false); - const [isUsingRecordedVoice, setIsUsingRecordedVoice] = useState(false); - const [recordStatus, setRecordStatus] = useState('Нажмите для записи'); - const [recordTime, setRecordTime] = useState(''); - const [showVoiceText, setShowVoiceText] = useState(false); - const [showVoicePreview, setShowVoicePreview] = useState(false); - const [recordedAudioUrl, setRecordedAudioUrl] = useState(''); + // Recording state + const [isRecording, setIsRecording] = useState(false); + const [hasRecording, setHasRecording] = useState(false); + const [isUsingRecordedVoice, setIsUsingRecordedVoice] = useState(false); + const [recordStatus, setRecordStatus] = useState('Нажмите для записи'); + const [recordTime, setRecordTime] = useState(''); + const [showVoiceText, setShowVoiceText] = useState(false); + const [showVoicePreview, setShowVoicePreview] = useState(false); + const [recordedAudioUrl, setRecordedAudioUrl] = useState(''); - // File state - const [fileName, setFileName] = useState(''); - const [isDragOver, setIsDragOver] = useState(false); + // File state + const [fileName, setFileName] = useState(''); + const [isDragOver, setIsDragOver] = useState(false); - // Form state - const [text, setText] = useState('В прошлом месяце мы достигли нового рубежа.'); - const [language, setLanguage] = useState('ru'); - const [exaggeration, setExaggeration] = useState(0.5); - const [temperature, setTemperature] = useState(0.8); - const [cfgWeight, setCfgWeight] = useState(0.5); - const [seed, setSeed] = useState(0); + // Form state + const [text, setText] = useState('В прошлом месяце мы достигли нового рубежа.'); + const [language, setLanguage] = useState('ru'); + const [exaggeration, setExaggeration] = useState(0.5); + const [temperature, setTemperature] = useState(0.8); + const [cfgWeight, setCfgWeight] = useState(0.5); + const [seed, setSeed] = useState(0); - // UI state - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [resultUrl, setResultUrl] = useState(''); + // UI state + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [resultUrl, setResultUrl] = useState(''); - // Refs (не тригерят ре-рендер) - const recorderRef = useRef(null); - const audioContextRef = useRef(null); - const streamRef = useRef(null); - const recordedBlobRef = useRef(null); - const recordingStartRef = useRef(0); - const timerIntervalRef = useRef | null>(null); - const fileInputRef = useRef(null); - const fileRef = useRef(null); + // Refs (не тригерят ре-рендер) + const recorderRef = useRef(null); + const audioContextRef = useRef(null); + const streamRef = useRef(null); + const recordedBlobRef = useRef(null); + const recordingStartRef = useRef(0); + const timerIntervalRef = useRef | null>(null); + const fileInputRef = useRef(null); + const fileRef = useRef(null); - // Запрос микрофона при монтировании - useEffect(() => { - navigator.mediaDevices.getUserMedia({ audio: true }) - .then(stream => stream.getTracks().forEach(t => t.stop())) - .catch(() => {}); - }, []); + // Запрос микрофона при монтировании + useEffect(() => { + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(stream => stream.getTracks().forEach(t => t.stop())) + .catch(() => { }); + }, []); - const updateTimer = () => { - const elapsed = Math.floor((Date.now() - recordingStartRef.current) / 1000); - const mins = Math.floor(elapsed / 60).toString().padStart(2, '0'); - const secs = (elapsed % 60).toString().padStart(2, '0'); - setRecordTime(`${mins}:${secs}`); - }; + const updateTimer = () => { + const elapsed = Math.floor((Date.now() - recordingStartRef.current) / 1000); + const mins = Math.floor(elapsed / 60).toString().padStart(2, '0'); + const secs = (elapsed % 60).toString().padStart(2, '0'); + setRecordTime(`${mins}:${secs}`); + }; - const startRecording = async () => { - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - streamRef.current = stream; + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + streamRef.current = stream; - const audioContext = new AudioContext(); - audioContextRef.current = audioContext; + const audioContext = new AudioContext(); + audioContextRef.current = audioContext; - const source = audioContext.createMediaStreamSource(stream); - const recorder = await Recorder.create(source, { numChannels: 1 }); - recorderRef.current = recorder; + const source = audioContext.createMediaStreamSource(stream); + const recorder = await Recorder.create(source, { numChannels: 1 }); + recorderRef.current = recorder; - setIsRecording(true); - setHasRecording(false); - setShowVoiceText(true); - setShowVoicePreview(false); - setIsUsingRecordedVoice(false); - setRecordStatus('Идёт запись...'); - setRecordTime('00:00'); + setIsRecording(true); + setHasRecording(false); + setShowVoiceText(true); + setShowVoicePreview(false); + setIsUsingRecordedVoice(false); + setRecordStatus('Идёт запись...'); + setRecordTime('00:00'); - recordingStartRef.current = Date.now(); - updateTimer(); - timerIntervalRef.current = setInterval(updateTimer, 1000); - } catch (err) { - alert('Не удалось получить доступ к микрофону: ' + (err as Error).message); - } - }; + recordingStartRef.current = Date.now(); + updateTimer(); + timerIntervalRef.current = setInterval(updateTimer, 1000); + } catch (err) { + alert('Не удалось получить доступ к микрофону: ' + (err as Error).message); + } + }; - const stopRecording = () => { - const recorder = recorderRef.current; - if (!recorder) return; + const stopRecording = () => { + const recorder = recorderRef.current; + if (!recorder) return; - recorder.stop(); + recorder.stop(); - // Сразу обновляем UI — не ждём окончания экспорта - setIsRecording(false); - setRecordStatus('Обработка...'); - if (timerIntervalRef.current) { - clearInterval(timerIntervalRef.current); - timerIntervalRef.current = null; - } - setRecordTime(''); + // Сразу обновляем UI — не ждём окончания экспорта + setIsRecording(false); + setRecordStatus('Обработка...'); + if (timerIntervalRef.current) { + clearInterval(timerIntervalRef.current); + timerIntervalRef.current = null; + } + setRecordTime(''); - recorder.exportWAV((blob) => { - recorder.clear(); - recorder.destroy(); - recorderRef.current = null; + recorder.exportWAV((blob) => { + recorder.clear(); + recorder.destroy(); + recorderRef.current = null; - streamRef.current?.getTracks().forEach(t => t.stop()); - streamRef.current = null; - audioContextRef.current?.close(); - audioContextRef.current = null; + streamRef.current?.getTracks().forEach(t => t.stop()); + streamRef.current = null; + audioContextRef.current?.close(); + audioContextRef.current = null; - recordedBlobRef.current = blob; - setRecordedAudioUrl(URL.createObjectURL(blob)); - setShowVoicePreview(true); - setShowVoiceText(false); - setRecordStatus('Запись завершена'); - setHasRecording(true); - }); - }; + recordedBlobRef.current = blob; + setRecordedAudioUrl(URL.createObjectURL(blob)); + setShowVoicePreview(true); + setShowVoiceText(false); + setRecordStatus('Запись завершена'); + setHasRecording(true); + }); + }; - const handleRecordClick = () => { - if (isRecording) stopRecording(); - else startRecording(); - }; + const handleRecordClick = () => { + if (isRecording) stopRecording(); + else startRecording(); + }; - const handleRetry = () => { - setShowVoicePreview(false); - setHasRecording(false); - setIsUsingRecordedVoice(false); - setRecordStatus('Нажмите для записи'); - recordedBlobRef.current = null; - }; + const handleRetry = () => { + setShowVoicePreview(false); + setHasRecording(false); + setIsUsingRecordedVoice(false); + setRecordStatus('Нажмите для записи'); + recordedBlobRef.current = null; + }; - const handleUseRecording = () => { - setIsUsingRecordedVoice(true); - setRecordStatus('✅ Голосовое сообщение сохранено'); - // Сбрасываем файл - fileRef.current = null; - setFileName(''); - if (fileInputRef.current) fileInputRef.current.value = ''; - }; + const handleUseRecording = () => { + setIsUsingRecordedVoice(true); + setRecordStatus('✅ Голосовое сообщение сохранено'); + // Сбрасываем файл + fileRef.current = null; + setFileName(''); + if (fileInputRef.current) fileInputRef.current.value = ''; + }; - const updateFileName = (file: File) => { - fileRef.current = file; - setFileName(file.name); - setIsUsingRecordedVoice(false); - }; + const updateFileName = (file: File) => { + fileRef.current = file; + setFileName(file.name); + setIsUsingRecordedVoice(false); + }; - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragOver(false); - const file = e.dataTransfer.files[0]; - if (file) updateFileName(file); - }; + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + const file = e.dataTransfer.files[0]; + if (file) updateFileName(file); + }; - const handleFileChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) updateFileName(file); - }; + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) updateFileName(file); + }; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - setResultUrl(''); - setError(''); - setLoading(true); + setResultUrl(''); + setError(''); + setLoading(true); - try { - const formData = new FormData(); + try { + const formData = new FormData(); - if (isUsingRecordedVoice) { - if (!recordedBlobRef.current) { - throw new Error('Запись голоса потеряна — запишите снова'); - } - formData.append('audio_file', recordedBlobRef.current, 'voice_recording.wav'); - } else if (fileRef.current) { - formData.append('audio_file', fileRef.current); - } else { - throw new Error('Выберите аудио файл или запишите голос'); - } + if (isUsingRecordedVoice) { + if (!recordedBlobRef.current) { + throw new Error('Запись голоса потеряна — запишите снова'); + } + formData.append('audio_file', recordedBlobRef.current, 'voice_recording.wav'); + } else if (fileRef.current) { + formData.append('audio_file', fileRef.current); + } else { + throw new Error('Выберите аудио файл или запишите голос'); + } - formData.append('text', text); - formData.append('language_id', language); - formData.append('exaggeration', String(exaggeration)); - formData.append('temperature', String(temperature)); - formData.append('seed_num', String(seed)); - formData.append('cfg_weight', String(cfgWeight)); + formData.append('text', text); + formData.append('language_id', language); + formData.append('exaggeration', String(exaggeration)); + formData.append('temperature', String(temperature)); + formData.append('seed_num', String(seed)); + formData.append('cfg_weight', String(cfgWeight)); - console.log('[ChatterboxTTS] отправка:', { - audio: isUsingRecordedVoice ? 'recorded blob' : fileRef.current?.name, - text, language, exaggeration, temperature, cfgWeight, seed, - }); + console.log('[ChatterboxTTS] отправка:', { + audio: isUsingRecordedVoice ? 'recorded blob' : fileRef.current?.name, + text, language, exaggeration, temperature, cfgWeight, seed, + }); - const response = await fetch(API_URL, { method: 'POST', body: formData }); + const response = await fetch(API_URL, { method: 'POST', body: formData }); - const contentType = response.headers.get('Content-Type') ?? ''; - console.log('[ChatterboxTTS] ответ:', response.status, contentType); + const contentType = response.headers.get('Content-Type') ?? ''; + console.log('[ChatterboxTTS] ответ:', response.status, contentType); - if (!response.ok || contentType.includes('application/json')) { - const errData = await response.json().catch(() => ({})); - const msg = (errData as { error?: string }).error; - if (msg) throw new Error(msg); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - throw new Error('Сервер вернул JSON вместо аудио — возможно audio_file не принят'); - } + if (!response.ok || contentType.includes('application/json')) { + const errData = await response.json().catch(() => ({})); + const msg = (errData as { error?: string }).error; + if (msg) throw new Error(msg); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + throw new Error('Сервер вернул JSON вместо аудио — возможно audio_file не принят'); + } - const blob = await response.blob(); - setResultUrl(URL.createObjectURL(blob)); - } catch (err) { - setError('❌ Ошибка: ' + (err as Error).message); - } finally { - setLoading(false); - } - }; + const blob = await response.blob(); + setResultUrl(URL.createObjectURL(blob)); + } catch (err) { + setError('❌ Ошибка: ' + (err as Error).message); + } finally { + setLoading(false); + } + }; - const voiceSectionClass = [ - 'voice-section', - isRecording ? 'recording' : '', - hasRecording && !isRecording ? 'has-recording' : '', - ].filter(Boolean).join(' '); + const voiceSectionClass = [ + 'voice-section', + isRecording ? 'recording' : '', + hasRecording && !isRecording ? 'has-recording' : '', + ].filter(Boolean).join(' '); - return ( -
-

🎙️ Chatterbox TTS

+ return ( +
+

Подделка голоса

-
- {/* Запись голоса */} -
-
- 🎤 Запишите голосовое сообщение для проверки на бота - {isUsingRecordedVoice && ✓ Готово} -
+

+ Deepfake — синтез правдоподобных поддельных изображений, видео и звука при помощи искусственного интеллекта. Чаще всего дипфейки изображают известных людей в вымышленных ситуациях. +

- +

+ Давайте на нашем примере разберемся с этим! Для этого нам нужен небольшой отрывок вашего голоса. Не переживайте - это безопасно! Мы трепетно относимся к чувствительной информации, не храним ваши данные после обработки и не передаем их третьим лицам - все строго в соответствии с политикой соглашения. +

-
{recordStatus}
- {recordTime &&
{recordTime}
} + + {/* Запись голоса */} +
+
+ 🎤 Запишите голосовое сообщение - достаточно 10-15 секунд вашего голоса + {isUsingRecordedVoice && ✓ Готово} +
- {showVoiceText && ( -
- 📢 Произнесите: "Хакатон 2026 французский стиль" -
- )} + - {showVoicePreview && ( -
-
- )} -
+
{recordStatus}
+ {recordTime &&
{recordTime}
} - {/* Загрузка файла */} -
или загрузите файл
+ {showVoiceText && ( +
+ 📢 Произнесите, например: "Хакатон 2026 походим модуль deepfake, кейс Цент Инвест команда Атейкин" +
+ )} -
-
fileInputRef.current?.click()} - onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }} - onDragLeave={() => setIsDragOver(false)} - onDrop={handleDrop} - style={isDragOver ? { borderColor: '#667eea' } : undefined} - > - -
{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}
- {fileName &&
{fileName}
} -
-
+ {showVoicePreview && ( +
+
+ )} +
- {/* Текст */} -
- -