replace voice recorder realization for support chrome
This commit is contained in:
parent
729a3c18ac
commit
66716d2314
1 changed files with 326 additions and 294 deletions
|
|
@ -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<Recorder | null>(null);
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const recordedBlobRef = useRef<Blob | null>(null);
|
||||
const recordingStartRef = useRef<number>(0);
|
||||
const timerIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const fileRef = useRef<File | null>(null);
|
||||
// Refs (не тригерят ре-рендер)
|
||||
const recorderRef = useRef<Recorder | null>(null);
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const streamRef = useRef<MediaStream | null>(null);
|
||||
const recordedBlobRef = useRef<Blob | null>(null);
|
||||
const recordingStartRef = useRef<number>(0);
|
||||
const timerIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const fileRef = useRef<File | null>(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<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) updateFileName(file);
|
||||
};
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="chatterbox-container">
|
||||
<h1>🎙️ Chatterbox TTS</h1>
|
||||
return (
|
||||
<div className="chatterbox-container">
|
||||
<h1>Подделка голоса</h1>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Запись голоса */}
|
||||
<div className={voiceSectionClass}>
|
||||
<div className="voice-label">
|
||||
🎤 Запишите голосовое сообщение для проверки на бота
|
||||
{isUsingRecordedVoice && <span className="check-badge">✓ Готово</span>}
|
||||
</div>
|
||||
<p>
|
||||
Deepfake — синтез правдоподобных поддельных изображений, видео и звука при помощи искусственного интеллекта. Чаще всего дипфейки изображают известных людей в вымышленных ситуациях.
|
||||
</p>
|
||||
|
||||
<button type="button" className={`record-btn${isRecording ? ' recording' : ''}`} onClick={handleRecordClick}>
|
||||
{isRecording ? '⏹' : '⏺'}
|
||||
</button>
|
||||
<p>
|
||||
Давайте на нашем примере разберемся с этим! Для этого нам нужен небольшой отрывок вашего голоса. Не переживайте - это безопасно! Мы трепетно относимся к чувствительной информации, не храним ваши данные после обработки и не передаем их третьим лицам - все строго в соответствии с политикой соглашения.
|
||||
</p>
|
||||
|
||||
<div className="record-status">{recordStatus}</div>
|
||||
{recordTime && <div className="record-timer">{recordTime}</div>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* Запись голоса */}
|
||||
<div className={voiceSectionClass}>
|
||||
<div className="voice-label">
|
||||
🎤 Запишите голосовое сообщение - достаточно 10-15 секунд вашего голоса
|
||||
{isUsingRecordedVoice && <span className="check-badge">✓ Готово</span>}
|
||||
</div>
|
||||
|
||||
{showVoiceText && (
|
||||
<div className="voice-text">
|
||||
📢 Произнесите: <span style={{ color: '#667eea' }}>"Хакатон 2026 французский стиль"</span>
|
||||
</div>
|
||||
)}
|
||||
<button type="button" className={`record-btn${isRecording ? ' recording' : ''}`} onClick={handleRecordClick}>
|
||||
{isRecording ? '⏹' : '⏺'}
|
||||
</button>
|
||||
|
||||
{showVoicePreview && (
|
||||
<div className="voice-preview">
|
||||
<audio src={recordedAudioUrl} controls />
|
||||
<div className="voice-actions">
|
||||
<button type="button" className="btn-retry" onClick={handleRetry}>🔄 Перезаписать</button>
|
||||
<button type="button" className="btn-use" onClick={handleUseRecording}>✅ Использовать</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="record-status">{recordStatus}</div>
|
||||
{recordTime && <div className="record-timer">{recordTime}</div>}
|
||||
|
||||
{/* Загрузка файла */}
|
||||
<div className="or-divider">или загрузите файл</div>
|
||||
{showVoiceText && (
|
||||
<div className="voice-text">
|
||||
📢 Произнесите, например: <span style={{ color: '#667eea' }}>"Хакатон 2026 походим модуль deepfake, кейс Цент Инвест команда Атейкин"</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<div
|
||||
className={`file-input-wrapper${fileName ? ' has-file' : ''}`}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
|
||||
onDragLeave={() => setIsDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
style={isDragOver ? { borderColor: '#667eea' } : undefined}
|
||||
>
|
||||
<input ref={fileInputRef} type="file" accept="audio/*" onChange={handleFileChange} style={{ display: 'none' }} />
|
||||
<div>{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}</div>
|
||||
{fileName && <div className="file-name">{fileName}</div>}
|
||||
</div>
|
||||
</div>
|
||||
{showVoicePreview && (
|
||||
<div className="voice-preview">
|
||||
<audio src={recordedAudioUrl} controls />
|
||||
<div className="voice-actions">
|
||||
<button type="button" className="btn-retry" onClick={handleRetry}>🔄 Перезаписать</button>
|
||||
<button type="button" className="btn-use" onClick={handleUseRecording}>✅ Использовать</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Текст */}
|
||||
<div className="form-group">
|
||||
<label>Текст для синтеза (макс 300 символов)</label>
|
||||
<textarea maxLength={300} value={text} onChange={e => setText(e.target.value)} placeholder="Введите текст..." />
|
||||
<div className="char-counter"><span>{text.length}</span>/300</div>
|
||||
</div>
|
||||
{/* Загрузка файла */}
|
||||
<div className="or-divider">или загрузите файл фрагмента голоса, желательно свой :D</div>
|
||||
|
||||
{/* Язык */}
|
||||
<div className="form-group">
|
||||
<label>Язык</label>
|
||||
<select value={language} onChange={e => setLanguage(e.target.value)}>
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="de">German</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="hi">Hindi</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div
|
||||
className={`file-input-wrapper${fileName ? ' has-file' : ''}`}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
|
||||
onDragLeave={() => setIsDragOver(false)}
|
||||
onDrop={handleDrop}
|
||||
style={isDragOver ? { borderColor: '#667eea' } : undefined}
|
||||
>
|
||||
<input ref={fileInputRef} type="file" accept="audio/*" onChange={handleFileChange} style={{ display: 'none' }} />
|
||||
<div>{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}</div>
|
||||
{fileName && <div className="file-name">{fileName}</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Слайдеры */}
|
||||
<div className="form-group">
|
||||
<label>Exaggeration (экспрессивность)</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.25" max="2" step="0.05" value={exaggeration} onChange={e => setExaggeration(Number(e.target.value))} />
|
||||
<span className="slider-value">{exaggeration}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Дипфейки часто создают и применяют в преступных целях. Их могут использовать для распространения ложных новостей, обмана или прохождения аудио- и видео- аутентификации.
|
||||
</p>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Temperature (разнообразие)</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.05" max="5" step="0.05" value={temperature} onChange={e => setTemperature(Number(e.target.value))} />
|
||||
<span className="slider-value">{temperature}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
Имея небольшой отрывок речи человека, мошенники могут полностью возсоздать интонацию, произношение и темперамет голоса.
|
||||
</p>
|
||||
|
||||
<div className="form-group">
|
||||
<label>CFG Weight</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.2" max="1" step="0.05" value={cfgWeight} onChange={e => setCfgWeight(Number(e.target.value))} />
|
||||
<span className="slider-value">{cfgWeight}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="two-col">
|
||||
<div className="form-group">
|
||||
<label>Random Seed (0 = random)</label>
|
||||
<input type="number" value={seed} min={0} onChange={e => setSeed(Number(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
{((isUsingRecordedVoice && recordedBlobRef.current) || fileName) && (
|
||||
<div>
|
||||
<p>
|
||||
Вот примеры дипфейков.
|
||||
</p>
|
||||
|
||||
<button type="submit" className="submit-btn" disabled={loading}>
|
||||
🚀 Сгенерировать аудио
|
||||
</button>
|
||||
</form>
|
||||
<VoiceCards voiceBlob={recordedBlobRef.current} />
|
||||
|
||||
{loading && (
|
||||
<div className="loading-block">
|
||||
<div className="spinner" />
|
||||
<p>Генерируем аудио... Это может занять 10-30 секунд</p>
|
||||
</div>
|
||||
)}
|
||||
<h2>
|
||||
Вы можете попробовать самостоятельно сделат дипфейк из своего голоса!
|
||||
</h2>
|
||||
|
||||
{error && <div className="error-block">{error}</div>}
|
||||
{/* Текст */}
|
||||
<div className="form-group">
|
||||
<label>Текст для синтеза (макс 300 символов)</label>
|
||||
<textarea maxLength={300} value={text} onChange={e => setText(e.target.value)} placeholder="Введите текст..." />
|
||||
<div className="char-counter"><span>{text.length}</span>/300</div>
|
||||
</div>
|
||||
|
||||
{resultUrl && (
|
||||
<div className="result-block">
|
||||
<h3>✅ Готово!</h3>
|
||||
<audio src={resultUrl} controls />
|
||||
<br />
|
||||
<a href={resultUrl} className="download-btn" download="output.wav">💾 Скачать WAV</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{/* Язык */}
|
||||
{/* <div className="form-group">
|
||||
<label>Язык</label>
|
||||
<select value={language} onChange={e => setLanguage(e.target.value)}>
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="de">German</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="hi">Hindi</option>
|
||||
</select>
|
||||
</div> */}
|
||||
|
||||
{/* Слайдеры */}
|
||||
<div className="form-group">
|
||||
<label>Exaggeration (экспрессивность)</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.25" max="2" step="0.05" value={exaggeration} onChange={e => setExaggeration(Number(e.target.value))} />
|
||||
<span className="slider-value">{exaggeration}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Temperature (разнообразие)</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.05" max="5" step="0.05" value={temperature} onChange={e => setTemperature(Number(e.target.value))} />
|
||||
<span className="slider-value">{temperature}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>CFG Weight</label>
|
||||
<div className="slider-group">
|
||||
<input type="range" min="0.2" max="1" step="0.05" value={cfgWeight} onChange={e => setCfgWeight(Number(e.target.value))} />
|
||||
<span className="slider-value">{cfgWeight}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="two-col">
|
||||
<div className="form-group">
|
||||
<label>Random Seed (0 = random)</label>
|
||||
<input type="number" value={seed} min={0} onChange={e => setSeed(Number(e.target.value))} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="submit-btn" disabled={loading}>
|
||||
🚀 Сгенерировать аудио
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{loading && (
|
||||
<div className="loading-block">
|
||||
<div className="spinner" />
|
||||
<p>Генерируем аудио... Это может занять 5-15 секунд</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && <div className="error-block">{error}</div>}
|
||||
|
||||
{resultUrl && (
|
||||
<div className="result-block">
|
||||
<h3>✅ Готово!</h3>
|
||||
<audio src={resultUrl} controls />
|
||||
<br />
|
||||
<a href={resultUrl} className="download-btn" download="output.wav">💾 Скачать WAV'ку!</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatterboxTTS;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue