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 { useState, useRef, useEffect } from 'react';
|
||||||
import { Recorder } from '../../lib/recorder';
|
import { Recorder } from '../../lib/recorder';
|
||||||
|
import VoiceCards from './VoiceCards';
|
||||||
import './ChatterboxTTS.css';
|
import './ChatterboxTTS.css';
|
||||||
|
|
||||||
const API_URL = 'https://back.hack.kinsle.ru/process-audio';
|
const API_URL = 'https://back.hack.kinsle.ru/process-audio';
|
||||||
|
|
||||||
const ChatterboxTTS = () => {
|
const ChatterboxTTS = () => {
|
||||||
// Recording state
|
// Recording state
|
||||||
const [isRecording, setIsRecording] = useState(false);
|
const [isRecording, setIsRecording] = useState(false);
|
||||||
const [hasRecording, setHasRecording] = useState(false);
|
const [hasRecording, setHasRecording] = useState(false);
|
||||||
const [isUsingRecordedVoice, setIsUsingRecordedVoice] = useState(false);
|
const [isUsingRecordedVoice, setIsUsingRecordedVoice] = useState(false);
|
||||||
const [recordStatus, setRecordStatus] = useState('Нажмите для записи');
|
const [recordStatus, setRecordStatus] = useState('Нажмите для записи');
|
||||||
const [recordTime, setRecordTime] = useState('');
|
const [recordTime, setRecordTime] = useState('');
|
||||||
const [showVoiceText, setShowVoiceText] = useState(false);
|
const [showVoiceText, setShowVoiceText] = useState(false);
|
||||||
const [showVoicePreview, setShowVoicePreview] = useState(false);
|
const [showVoicePreview, setShowVoicePreview] = useState(false);
|
||||||
const [recordedAudioUrl, setRecordedAudioUrl] = useState('');
|
const [recordedAudioUrl, setRecordedAudioUrl] = useState('');
|
||||||
|
|
||||||
// File state
|
// File state
|
||||||
const [fileName, setFileName] = useState('');
|
const [fileName, setFileName] = useState('');
|
||||||
const [isDragOver, setIsDragOver] = useState(false);
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [text, setText] = useState('В прошлом месяце мы достигли нового рубежа.');
|
const [text, setText] = useState('В прошлом месяце мы достигли нового рубежа.');
|
||||||
const [language, setLanguage] = useState('ru');
|
const [language, setLanguage] = useState('ru');
|
||||||
const [exaggeration, setExaggeration] = useState(0.5);
|
const [exaggeration, setExaggeration] = useState(0.5);
|
||||||
const [temperature, setTemperature] = useState(0.8);
|
const [temperature, setTemperature] = useState(0.8);
|
||||||
const [cfgWeight, setCfgWeight] = useState(0.5);
|
const [cfgWeight, setCfgWeight] = useState(0.5);
|
||||||
const [seed, setSeed] = useState(0);
|
const [seed, setSeed] = useState(0);
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [resultUrl, setResultUrl] = useState('');
|
const [resultUrl, setResultUrl] = useState('');
|
||||||
|
|
||||||
// Refs (не тригерят ре-рендер)
|
// Refs (не тригерят ре-рендер)
|
||||||
const recorderRef = useRef<Recorder | null>(null);
|
const recorderRef = useRef<Recorder | null>(null);
|
||||||
const audioContextRef = useRef<AudioContext | null>(null);
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
const streamRef = useRef<MediaStream | null>(null);
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
const recordedBlobRef = useRef<Blob | null>(null);
|
const recordedBlobRef = useRef<Blob | null>(null);
|
||||||
const recordingStartRef = useRef<number>(0);
|
const recordingStartRef = useRef<number>(0);
|
||||||
const timerIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const timerIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const fileRef = useRef<File | null>(null);
|
const fileRef = useRef<File | null>(null);
|
||||||
|
|
||||||
// Запрос микрофона при монтировании
|
// Запрос микрофона при монтировании
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
.then(stream => stream.getTracks().forEach(t => t.stop()))
|
.then(stream => stream.getTracks().forEach(t => t.stop()))
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateTimer = () => {
|
const updateTimer = () => {
|
||||||
const elapsed = Math.floor((Date.now() - recordingStartRef.current) / 1000);
|
const elapsed = Math.floor((Date.now() - recordingStartRef.current) / 1000);
|
||||||
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
|
const mins = Math.floor(elapsed / 60).toString().padStart(2, '0');
|
||||||
const secs = (elapsed % 60).toString().padStart(2, '0');
|
const secs = (elapsed % 60).toString().padStart(2, '0');
|
||||||
setRecordTime(`${mins}:${secs}`);
|
setRecordTime(`${mins}:${secs}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const startRecording = async () => {
|
const startRecording = async () => {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
streamRef.current = stream;
|
streamRef.current = stream;
|
||||||
|
|
||||||
const audioContext = new AudioContext();
|
const audioContext = new AudioContext();
|
||||||
audioContextRef.current = audioContext;
|
audioContextRef.current = audioContext;
|
||||||
|
|
||||||
const source = audioContext.createMediaStreamSource(stream);
|
const source = audioContext.createMediaStreamSource(stream);
|
||||||
const recorder = await Recorder.create(source, { numChannels: 1 });
|
const recorder = await Recorder.create(source, { numChannels: 1 });
|
||||||
recorderRef.current = recorder;
|
recorderRef.current = recorder;
|
||||||
|
|
||||||
setIsRecording(true);
|
setIsRecording(true);
|
||||||
setHasRecording(false);
|
setHasRecording(false);
|
||||||
setShowVoiceText(true);
|
setShowVoiceText(true);
|
||||||
setShowVoicePreview(false);
|
setShowVoicePreview(false);
|
||||||
setIsUsingRecordedVoice(false);
|
setIsUsingRecordedVoice(false);
|
||||||
setRecordStatus('Идёт запись...');
|
setRecordStatus('Идёт запись...');
|
||||||
setRecordTime('00:00');
|
setRecordTime('00:00');
|
||||||
|
|
||||||
recordingStartRef.current = Date.now();
|
recordingStartRef.current = Date.now();
|
||||||
updateTimer();
|
updateTimer();
|
||||||
timerIntervalRef.current = setInterval(updateTimer, 1000);
|
timerIntervalRef.current = setInterval(updateTimer, 1000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Не удалось получить доступ к микрофону: ' + (err as Error).message);
|
alert('Не удалось получить доступ к микрофону: ' + (err as Error).message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopRecording = () => {
|
const stopRecording = () => {
|
||||||
const recorder = recorderRef.current;
|
const recorder = recorderRef.current;
|
||||||
if (!recorder) return;
|
if (!recorder) return;
|
||||||
|
|
||||||
recorder.stop();
|
recorder.stop();
|
||||||
|
|
||||||
// Сразу обновляем UI — не ждём окончания экспорта
|
// Сразу обновляем UI — не ждём окончания экспорта
|
||||||
setIsRecording(false);
|
setIsRecording(false);
|
||||||
setRecordStatus('Обработка...');
|
setRecordStatus('Обработка...');
|
||||||
if (timerIntervalRef.current) {
|
if (timerIntervalRef.current) {
|
||||||
clearInterval(timerIntervalRef.current);
|
clearInterval(timerIntervalRef.current);
|
||||||
timerIntervalRef.current = null;
|
timerIntervalRef.current = null;
|
||||||
}
|
}
|
||||||
setRecordTime('');
|
setRecordTime('');
|
||||||
|
|
||||||
recorder.exportWAV((blob) => {
|
recorder.exportWAV((blob) => {
|
||||||
recorder.clear();
|
recorder.clear();
|
||||||
recorder.destroy();
|
recorder.destroy();
|
||||||
recorderRef.current = null;
|
recorderRef.current = null;
|
||||||
|
|
||||||
streamRef.current?.getTracks().forEach(t => t.stop());
|
streamRef.current?.getTracks().forEach(t => t.stop());
|
||||||
streamRef.current = null;
|
streamRef.current = null;
|
||||||
audioContextRef.current?.close();
|
audioContextRef.current?.close();
|
||||||
audioContextRef.current = null;
|
audioContextRef.current = null;
|
||||||
|
|
||||||
recordedBlobRef.current = blob;
|
recordedBlobRef.current = blob;
|
||||||
setRecordedAudioUrl(URL.createObjectURL(blob));
|
setRecordedAudioUrl(URL.createObjectURL(blob));
|
||||||
setShowVoicePreview(true);
|
setShowVoicePreview(true);
|
||||||
setShowVoiceText(false);
|
setShowVoiceText(false);
|
||||||
setRecordStatus('Запись завершена');
|
setRecordStatus('Запись завершена');
|
||||||
setHasRecording(true);
|
setHasRecording(true);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRecordClick = () => {
|
const handleRecordClick = () => {
|
||||||
if (isRecording) stopRecording();
|
if (isRecording) stopRecording();
|
||||||
else startRecording();
|
else startRecording();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRetry = () => {
|
const handleRetry = () => {
|
||||||
setShowVoicePreview(false);
|
setShowVoicePreview(false);
|
||||||
setHasRecording(false);
|
setHasRecording(false);
|
||||||
setIsUsingRecordedVoice(false);
|
setIsUsingRecordedVoice(false);
|
||||||
setRecordStatus('Нажмите для записи');
|
setRecordStatus('Нажмите для записи');
|
||||||
recordedBlobRef.current = null;
|
recordedBlobRef.current = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUseRecording = () => {
|
const handleUseRecording = () => {
|
||||||
setIsUsingRecordedVoice(true);
|
setIsUsingRecordedVoice(true);
|
||||||
setRecordStatus('✅ Голосовое сообщение сохранено');
|
setRecordStatus('✅ Голосовое сообщение сохранено');
|
||||||
// Сбрасываем файл
|
// Сбрасываем файл
|
||||||
fileRef.current = null;
|
fileRef.current = null;
|
||||||
setFileName('');
|
setFileName('');
|
||||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateFileName = (file: File) => {
|
const updateFileName = (file: File) => {
|
||||||
fileRef.current = file;
|
fileRef.current = file;
|
||||||
setFileName(file.name);
|
setFileName(file.name);
|
||||||
setIsUsingRecordedVoice(false);
|
setIsUsingRecordedVoice(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragOver(false);
|
setIsDragOver(false);
|
||||||
const file = e.dataTransfer.files[0];
|
const file = e.dataTransfer.files[0];
|
||||||
if (file) updateFileName(file);
|
if (file) updateFileName(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) updateFileName(file);
|
if (file) updateFileName(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
setResultUrl('');
|
setResultUrl('');
|
||||||
setError('');
|
setError('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
if (isUsingRecordedVoice) {
|
if (isUsingRecordedVoice) {
|
||||||
if (!recordedBlobRef.current) {
|
if (!recordedBlobRef.current) {
|
||||||
throw new Error('Запись голоса потеряна — запишите снова');
|
throw new Error('Запись голоса потеряна — запишите снова');
|
||||||
}
|
}
|
||||||
formData.append('audio_file', recordedBlobRef.current, 'voice_recording.wav');
|
formData.append('audio_file', recordedBlobRef.current, 'voice_recording.wav');
|
||||||
} else if (fileRef.current) {
|
} else if (fileRef.current) {
|
||||||
formData.append('audio_file', fileRef.current);
|
formData.append('audio_file', fileRef.current);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Выберите аудио файл или запишите голос');
|
throw new Error('Выберите аудио файл или запишите голос');
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append('text', text);
|
formData.append('text', text);
|
||||||
formData.append('language_id', language);
|
formData.append('language_id', language);
|
||||||
formData.append('exaggeration', String(exaggeration));
|
formData.append('exaggeration', String(exaggeration));
|
||||||
formData.append('temperature', String(temperature));
|
formData.append('temperature', String(temperature));
|
||||||
formData.append('seed_num', String(seed));
|
formData.append('seed_num', String(seed));
|
||||||
formData.append('cfg_weight', String(cfgWeight));
|
formData.append('cfg_weight', String(cfgWeight));
|
||||||
|
|
||||||
console.log('[ChatterboxTTS] отправка:', {
|
console.log('[ChatterboxTTS] отправка:', {
|
||||||
audio: isUsingRecordedVoice ? 'recorded blob' : fileRef.current?.name,
|
audio: isUsingRecordedVoice ? 'recorded blob' : fileRef.current?.name,
|
||||||
text, language, exaggeration, temperature, cfgWeight, seed,
|
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') ?? '';
|
const contentType = response.headers.get('Content-Type') ?? '';
|
||||||
console.log('[ChatterboxTTS] ответ:', response.status, contentType);
|
console.log('[ChatterboxTTS] ответ:', response.status, contentType);
|
||||||
|
|
||||||
if (!response.ok || contentType.includes('application/json')) {
|
if (!response.ok || contentType.includes('application/json')) {
|
||||||
const errData = await response.json().catch(() => ({}));
|
const errData = await response.json().catch(() => ({}));
|
||||||
const msg = (errData as { error?: string }).error;
|
const msg = (errData as { error?: string }).error;
|
||||||
if (msg) throw new Error(msg);
|
if (msg) throw new Error(msg);
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
throw new Error('Сервер вернул JSON вместо аудио — возможно audio_file не принят');
|
throw new Error('Сервер вернул JSON вместо аудио — возможно audio_file не принят');
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await response.blob();
|
const blob = await response.blob();
|
||||||
setResultUrl(URL.createObjectURL(blob));
|
setResultUrl(URL.createObjectURL(blob));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('❌ Ошибка: ' + (err as Error).message);
|
setError('❌ Ошибка: ' + (err as Error).message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const voiceSectionClass = [
|
const voiceSectionClass = [
|
||||||
'voice-section',
|
'voice-section',
|
||||||
isRecording ? 'recording' : '',
|
isRecording ? 'recording' : '',
|
||||||
hasRecording && !isRecording ? 'has-recording' : '',
|
hasRecording && !isRecording ? 'has-recording' : '',
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chatterbox-container">
|
<div className="chatterbox-container">
|
||||||
<h1>🎙️ Chatterbox TTS</h1>
|
<h1>Подделка голоса</h1>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<p>
|
||||||
{/* Запись голоса */}
|
Deepfake — синтез правдоподобных поддельных изображений, видео и звука при помощи искусственного интеллекта. Чаще всего дипфейки изображают известных людей в вымышленных ситуациях.
|
||||||
<div className={voiceSectionClass}>
|
</p>
|
||||||
<div className="voice-label">
|
|
||||||
🎤 Запишите голосовое сообщение для проверки на бота
|
|
||||||
{isUsingRecordedVoice && <span className="check-badge">✓ Готово</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" className={`record-btn${isRecording ? ' recording' : ''}`} onClick={handleRecordClick}>
|
<p>
|
||||||
{isRecording ? '⏹' : '⏺'}
|
Давайте на нашем примере разберемся с этим! Для этого нам нужен небольшой отрывок вашего голоса. Не переживайте - это безопасно! Мы трепетно относимся к чувствительной информации, не храним ваши данные после обработки и не передаем их третьим лицам - все строго в соответствии с политикой соглашения.
|
||||||
</button>
|
</p>
|
||||||
|
|
||||||
<div className="record-status">{recordStatus}</div>
|
<form onSubmit={handleSubmit}>
|
||||||
{recordTime && <div className="record-timer">{recordTime}</div>}
|
{/* Запись голоса */}
|
||||||
|
<div className={voiceSectionClass}>
|
||||||
|
<div className="voice-label">
|
||||||
|
🎤 Запишите голосовое сообщение - достаточно 10-15 секунд вашего голоса
|
||||||
|
{isUsingRecordedVoice && <span className="check-badge">✓ Готово</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
{showVoiceText && (
|
<button type="button" className={`record-btn${isRecording ? ' recording' : ''}`} onClick={handleRecordClick}>
|
||||||
<div className="voice-text">
|
{isRecording ? '⏹' : '⏺'}
|
||||||
📢 Произнесите: <span style={{ color: '#667eea' }}>"Хакатон 2026 французский стиль"</span>
|
</button>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showVoicePreview && (
|
<div className="record-status">{recordStatus}</div>
|
||||||
<div className="voice-preview">
|
{recordTime && <div className="record-timer">{recordTime}</div>}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Загрузка файла */}
|
{showVoiceText && (
|
||||||
<div className="or-divider">или загрузите файл</div>
|
<div className="voice-text">
|
||||||
|
📢 Произнесите, например: <span style={{ color: '#667eea' }}>"Хакатон 2026 походим модуль deepfake, кейс Цент Инвест команда Атейкин"</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="form-group">
|
{showVoicePreview && (
|
||||||
<div
|
<div className="voice-preview">
|
||||||
className={`file-input-wrapper${fileName ? ' has-file' : ''}`}
|
<audio src={recordedAudioUrl} controls />
|
||||||
onClick={() => fileInputRef.current?.click()}
|
<div className="voice-actions">
|
||||||
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
|
<button type="button" className="btn-retry" onClick={handleRetry}>🔄 Перезаписать</button>
|
||||||
onDragLeave={() => setIsDragOver(false)}
|
<button type="button" className="btn-use" onClick={handleUseRecording}>✅ Использовать</button>
|
||||||
onDrop={handleDrop}
|
</div>
|
||||||
style={isDragOver ? { borderColor: '#667eea' } : undefined}
|
</div>
|
||||||
>
|
)}
|
||||||
<input ref={fileInputRef} type="file" accept="audio/*" onChange={handleFileChange} style={{ display: 'none' }} />
|
</div>
|
||||||
<div>{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}</div>
|
|
||||||
{fileName && <div className="file-name">{fileName}</div>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Текст */}
|
{/* Загрузка файла */}
|
||||||
<div className="form-group">
|
<div className="or-divider">или загрузите файл фрагмента голоса, желательно свой :D</div>
|
||||||
<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="form-group">
|
||||||
<div className="form-group">
|
<div
|
||||||
<label>Язык</label>
|
className={`file-input-wrapper${fileName ? ' has-file' : ''}`}
|
||||||
<select value={language} onChange={e => setLanguage(e.target.value)}>
|
onClick={() => fileInputRef.current?.click()}
|
||||||
<option value="en">English</option>
|
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
|
||||||
<option value="ru">Russian</option>
|
onDragLeave={() => setIsDragOver(false)}
|
||||||
<option value="de">German</option>
|
onDrop={handleDrop}
|
||||||
<option value="es">Spanish</option>
|
style={isDragOver ? { borderColor: '#667eea' } : undefined}
|
||||||
<option value="fr">French</option>
|
>
|
||||||
<option value="it">Italian</option>
|
<input ref={fileInputRef} type="file" accept="audio/*" onChange={handleFileChange} style={{ display: 'none' }} />
|
||||||
<option value="pt">Portuguese</option>
|
<div>{fileName ? '✅ Файл выбран' : '📁 Кликни или перетащи аудио файл'}</div>
|
||||||
<option value="hi">Hindi</option>
|
{fileName && <div className="file-name">{fileName}</div>}
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Слайдеры */}
|
<p>
|
||||||
<div className="form-group">
|
Дипфейки часто создают и применяют в преступных целях. Их могут использовать для распространения ложных новостей, обмана или прохождения аудио- и видео- аутентификации.
|
||||||
<label>Exaggeration (экспрессивность)</label>
|
</p>
|
||||||
<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">
|
<p>
|
||||||
<label>Temperature (разнообразие)</label>
|
Имея небольшой отрывок речи человека, мошенники могут полностью возсоздать интонацию, произношение и темперамет голоса.
|
||||||
<div className="slider-group">
|
</p>
|
||||||
<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">
|
{((isUsingRecordedVoice && recordedBlobRef.current) || fileName) && (
|
||||||
<div className="form-group">
|
<div>
|
||||||
<label>Random Seed (0 = random)</label>
|
<p>
|
||||||
<input type="number" value={seed} min={0} onChange={e => setSeed(Number(e.target.value))} />
|
Вот примеры дипфейков.
|
||||||
</div>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" className="submit-btn" disabled={loading}>
|
<VoiceCards voiceBlob={recordedBlobRef.current} />
|
||||||
🚀 Сгенерировать аудио
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{loading && (
|
<h2>
|
||||||
<div className="loading-block">
|
Вы можете попробовать самостоятельно сделат дипфейк из своего голоса!
|
||||||
<div className="spinner" />
|
</h2>
|
||||||
<p>Генерируем аудио... Это может занять 10-30 секунд</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{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">
|
{/* <div className="form-group">
|
||||||
<h3>✅ Готово!</h3>
|
<label>Язык</label>
|
||||||
<audio src={resultUrl} controls />
|
<select value={language} onChange={e => setLanguage(e.target.value)}>
|
||||||
<br />
|
<option value="en">English</option>
|
||||||
<a href={resultUrl} className="download-btn" download="output.wav">💾 Скачать WAV</a>
|
<option value="ru">Russian</option>
|
||||||
</div>
|
<option value="de">German</option>
|
||||||
)}
|
<option value="es">Spanish</option>
|
||||||
</div>
|
<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;
|
export default ChatterboxTTS;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue