Merge PR #1600: bound long cloning references
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -40,6 +40,7 @@ the frozen-backend fallback mirror it for their toolchains.
|
||||
- `OMNIVOICE_ASR_BACKEND=omnivoice` now selects the PyTorch-native Whisper path, so the documented ROCm escape hatch no longer fails as an unknown engine (#1582) — thanks @patmansk!
|
||||
- Network Sharing from Windows MSI/portable installs now serves the bundled web interface to LAN devices instead of redirecting them to their own `localhost` (#1589) — thanks @TWIISTED-STUDIOS!
|
||||
- Exported dubbed videos now mark the dubbed language as the default audio stream while keeping Original available as an explicit choice (#1575) — thanks @invio-a11y!
|
||||
- Cloning references can no longer exhaust system memory: transcript-free clips up to 75 seconds are searched in five bounded passages, longer clips ask to be trimmed, and supplied transcripts remain capped at 20 seconds to preserve alignment (#1578) — thanks @ACKAPOB!
|
||||
- Stored artifact subpaths now resolve after moving a data directory between Windows, macOS, Linux, and Docker, while traversal and symlink escapes remain blocked (#1559) — thanks @Eman-Yousaf!
|
||||
- A remote browser hitting an API-key-configured server's admin 403 now gets the API-key login form instead of endless console 403s, while desktop and PIN-only/no-key servers keep the plain loopback error so guests are never offered a login no key can satisfy (#1568) — thanks @paoloantinori!
|
||||
- The crash-isolated ASR sidecar and its download preflight now agree on which model to load — setting the shared faster-whisper model variable applies to both variants instead of the sidecar quietly using a different one (#1556)
|
||||
|
||||
@@ -50,7 +50,12 @@ The env var overrides the persisted UI choice.
|
||||
- Cloning takes a short reference clip (`ref_audio`); 3–10 seconds is the
|
||||
sweet spot. A transcript of the clip improves conditioning — if the profile
|
||||
has none, VoiceStudio transcribes the clip automatically on first use and
|
||||
saves the result to the profile.
|
||||
saves the result to the profile. A clip with a supplied transcript is limited
|
||||
to 20 seconds so the two stay aligned; trim both to the same passage. Without
|
||||
a transcript, VoiceStudio can search up to 75 seconds in five contiguous,
|
||||
bounded transcription passes and selects the passage with detected speech.
|
||||
Longer clips must be trimmed first. If no spoken words are detected, trim to
|
||||
a clear 3–10 second passage or provide its matching transcript.
|
||||
- Encoded voice references persist on disk (`prompt_cache/` in the app data
|
||||
dir), so the first generation with a known voice after a restart skips the
|
||||
re-encode and any transcription pass. Set `OMNIVOICE_PROMPT_DISK_CACHE=0`
|
||||
|
||||
@@ -6,6 +6,7 @@ import { playBlobAudio, playPing } from '../utils/media';
|
||||
import {
|
||||
StreamingPreviewError,
|
||||
resolveRemoteTtsTarget,
|
||||
shouldFallbackToClassic,
|
||||
streamGenerateSpeech,
|
||||
supportsStreamingPreview,
|
||||
} from '../utils/streamingTts';
|
||||
@@ -318,7 +319,7 @@ export default function useTTS({ selectedProfile, setSelectedProfile, loadHistor
|
||||
// same error. Surface the backend's actionable message instead.
|
||||
// Non-retryable failures (transport drop, Web Audio glitch) keep the
|
||||
// classic fallback: there the whole-file path genuinely can succeed.
|
||||
if (err.retryable) {
|
||||
if (!shouldFallbackToClassic(err)) {
|
||||
addBreadcrumb('generate:stream-retryable-abort');
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "خطأ: {{message}}",
|
||||
"ignored_unsupported": "تم تجاهل التعليمات غير المدعومة: {{items}}",
|
||||
"ignored_duplicate": "تم التجاهل (تم تعيين الفئة بالفعل): {{items}}",
|
||||
"ref_audio_unusable": "تعذّر سماع أي كلام في الصوت المرجعي — المقطع صامت أو شبه صامت، فلا يوجد صوت لاستنساخه. سجّل مجددًا أقرب إلى الميكروفون (وتأكد من اختيار جهاز الإدخال الصحيح)، أو اختر مقطعًا آخر."
|
||||
"ref_audio_unusable": "تعذّر سماع أي كلام في الصوت المرجعي — المقطع صامت أو شبه صامت، فلا يوجد صوت لاستنساخه. سجّل مجددًا أقرب إلى الميكروفون (وتأكد من اختيار جهاز الإدخال الصحيح)، أو اختر مقطعًا آخر.",
|
||||
"ref_audio_too_long": "يمكن أن يبلغ طول المرجع 20 ثانية كحد أقصى مع نص، أو 75 ثانية من دونه. قصّه إلى مقطع كلام واضح بطول 3–10 ثوانٍ؛ وإذا أرفقت نصًا فقصّه إلى المقطع نفسه.",
|
||||
"ref_audio_no_speech": "لم يكتشف التعرف التلقائي أي كلمات منطوقة في المرجع. قصّه إلى مقطع كلام واضح بطول 3–10 ثوانٍ أو أرفق نصًا مطابقًا."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "المشاركة والوصول عن بعد",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Fehler: {{message}}",
|
||||
"ignored_unsupported": "Nicht unterstützte Anweisung ignoriert: {{items}}",
|
||||
"ignored_duplicate": "Ignoriert (Kategorie bereits festgelegt): {{items}}",
|
||||
"ref_audio_unusable": "Im Referenz-Audio war keine Sprache zu hören — der Clip ist still oder fast still, es gibt also keine Stimme zum Klonen. Nimm näher am Mikrofon neu auf (prüfe, ob das richtige Eingabegerät gewählt ist und der Pegel ausschlägt) oder wähle einen anderen Clip."
|
||||
"ref_audio_unusable": "Im Referenz-Audio war keine Sprache zu hören — der Clip ist still oder fast still, es gibt also keine Stimme zum Klonen. Nimm näher am Mikrofon neu auf (prüfe, ob das richtige Eingabegerät gewählt ist und der Pegel ausschlägt) oder wähle einen anderen Clip.",
|
||||
"ref_audio_too_long": "Eine Referenz darf mit Transkript höchstens 20 Sekunden, ohne Transkript höchstens 75 Sekunden lang sein. Kürze sie auf eine klare Sprachpassage von 3–10 Sekunden; bei mitgeliefertem Text muss das Transkript denselben Abschnitt abdecken.",
|
||||
"ref_audio_no_speech": "Die automatische Spracherkennung hat keine gesprochenen Wörter gefunden. Kürze die Referenz auf eine klare Sprachpassage von 3–10 Sekunden oder gib ein passendes Transkript an."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Teilen und Fernzugriff",
|
||||
|
||||
@@ -2730,7 +2730,9 @@
|
||||
"error_prefix": "Error: {{message}}",
|
||||
"ignored_unsupported": "Ignored unsupported instruct: {{items}}",
|
||||
"ignored_duplicate": "Ignored (category already set): {{items}}",
|
||||
"ref_audio_unusable": "No speech could be heard in the reference audio — the clip is silent or nearly silent, so there is no voice to clone. Record again closer to the microphone (check that the right input device is selected and its level moves while you speak), or choose a different clip."
|
||||
"ref_audio_unusable": "No speech could be heard in the reference audio — the clip is silent or nearly silent, so there is no voice to clone. Record again closer to the microphone (check that the right input device is selected and its level moves while you speak), or choose a different clip.",
|
||||
"ref_audio_too_long": "A reference can be at most 20 seconds with a supplied transcript, or 75 seconds without one. Trim it to a clear 3–10 second speech passage; if you supply text, trim the transcript to the same passage.",
|
||||
"ref_audio_no_speech": "Automatic speech detection found no spoken words in the reference. Trim it to a clear 3–10 second speech passage, or supply a matching transcript."
|
||||
},
|
||||
"tts": {
|
||||
"routingFallback": "Running on CPU — this engine has no GPU path on your machine, so generation will be slower. {{reason}}",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Error: {{message}}",
|
||||
"ignored_unsupported": "Instrucción no admitida ignorada: {{items}}",
|
||||
"ignored_duplicate": "Ignorado (categoría ya establecida): {{items}}",
|
||||
"ref_audio_unusable": "No se detectó voz en el audio de referencia: el clip está en silencio o casi en silencio, así que no hay voz que clonar. Graba de nuevo más cerca del micrófono (comprueba que está seleccionado el dispositivo de entrada correcto y que el nivel se mueve) o elige otro clip."
|
||||
"ref_audio_unusable": "No se detectó voz en el audio de referencia: el clip está en silencio o casi en silencio, así que no hay voz que clonar. Graba de nuevo más cerca del micrófono (comprueba que está seleccionado el dispositivo de entrada correcto y que el nivel se mueve) o elige otro clip.",
|
||||
"ref_audio_too_long": "Una referencia puede durar como máximo 20 segundos con transcripción o 75 segundos sin ella. Recórtala a un fragmento de voz claro de 3–10 segundos; si aportas texto, recorta la transcripción al mismo fragmento.",
|
||||
"ref_audio_no_speech": "La detección automática no encontró palabras habladas en la referencia. Recórtala a un fragmento de voz claro de 3–10 segundos o proporciona una transcripción coincidente."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Compartir y acceso remoto",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Erreur : {{message}}",
|
||||
"ignored_unsupported": "Instruction non prise en charge ignorée : {{items}}",
|
||||
"ignored_duplicate": "Ignoré (catégorie déjà définie) : {{items}}",
|
||||
"ref_audio_unusable": "Aucune voix n'a été détectée dans l'audio de référence — le clip est silencieux ou presque, il n'y a donc aucune voix à cloner. Réenregistrez plus près du micro (vérifiez que le bon périphérique d'entrée est sélectionné et que le niveau bouge) ou choisissez un autre clip."
|
||||
"ref_audio_unusable": "Aucune voix n'a été détectée dans l'audio de référence — le clip est silencieux ou presque, il n'y a donc aucune voix à cloner. Réenregistrez plus près du micro (vérifiez que le bon périphérique d'entrée est sélectionné et que le niveau bouge) ou choisissez un autre clip.",
|
||||
"ref_audio_too_long": "Une référence peut durer au maximum 20 secondes avec transcription, ou 75 secondes sans transcription. Coupez-la sur un passage vocal clair de 3 à 10 secondes ; si vous fournissez du texte, coupez la transcription sur le même passage.",
|
||||
"ref_audio_no_speech": "La détection automatique n'a trouvé aucune parole dans la référence. Coupez-la sur un passage vocal clair de 3 à 10 secondes ou fournissez une transcription correspondante."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Partage et accès à distance",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "त्रुटि: {{message}}",
|
||||
"ignored_unsupported": "अनदेखा असमर्थित निर्देश: {{items}}",
|
||||
"ignored_duplicate": "अनदेखा (श्रेणी पहले से ही सेट): {{items}}",
|
||||
"ref_audio_unusable": "संदर्भ ऑडियो में कोई आवाज़ सुनाई नहीं दी — क्लिप मौन या लगभग मौन है, इसलिए क्लोन करने के लिए कोई आवाज़ नहीं है। माइक्रोफ़ोन के और पास से दोबारा रिकॉर्ड करें (जांचें कि सही इनपुट डिवाइस चुना गया है) या कोई दूसरी क्लिप चुनें।"
|
||||
"ref_audio_unusable": "संदर्भ ऑडियो में कोई आवाज़ सुनाई नहीं दी — क्लिप मौन या लगभग मौन है, इसलिए क्लोन करने के लिए कोई आवाज़ नहीं है। माइक्रोफ़ोन के और पास से दोबारा रिकॉर्ड करें (जांचें कि सही इनपुट डिवाइस चुना गया है) या कोई दूसरी क्लिप चुनें।",
|
||||
"ref_audio_too_long": "ट्रांसक्रिप्ट के साथ संदर्भ अधिकतम 20 सेकंड और उसके बिना अधिकतम 75 सेकंड का हो सकता है। इसे 3–10 सेकंड के स्पष्ट भाषण अंश तक काटें; टेक्स्ट देने पर ट्रांसक्रिप्ट को भी उसी अंश तक काटें।",
|
||||
"ref_audio_no_speech": "स्वचालित पहचान को संदर्भ में बोले गए शब्द नहीं मिले। इसे 3–10 सेकंड के स्पष्ट भाषण अंश तक काटें या मेल खाता ट्रांसक्रिप्ट दें।"
|
||||
},
|
||||
"sharing": {
|
||||
"title": "साझाकरण और रिमोट एक्सेस",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Kesalahan: {{message}}",
|
||||
"ignored_unsupported": "Mengabaikan instruksi yang tidak didukung: {{items}}",
|
||||
"ignored_duplicate": "Diabaikan (kategori sudah ditetapkan): {{items}}",
|
||||
"ref_audio_unusable": "Tidak ada suara yang terdengar di audio referensi — klipnya senyap atau hampir senyap, jadi tidak ada suara untuk dikloning. Rekam ulang lebih dekat ke mikrofon (pastikan perangkat input yang benar dipilih dan meterannya bergerak), atau pilih klip lain."
|
||||
"ref_audio_unusable": "Tidak ada suara yang terdengar di audio referensi — klipnya senyap atau hampir senyap, jadi tidak ada suara untuk dikloning. Rekam ulang lebih dekat ke mikrofon (pastikan perangkat input yang benar dipilih dan meterannya bergerak), atau pilih klip lain.",
|
||||
"ref_audio_too_long": "Referensi maksimal 20 detik dengan transkrip atau 75 detik tanpa transkrip. Potong menjadi bagian ucapan jelas sepanjang 3–10 detik; jika menyertakan teks, potong transkrip ke bagian yang sama.",
|
||||
"ref_audio_no_speech": "Deteksi otomatis tidak menemukan kata yang diucapkan dalam referensi. Potong menjadi bagian ucapan jelas sepanjang 3–10 detik atau sertakan transkrip yang cocok."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Berbagi & Akses Jarak Jauh",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Errore: {{message}}",
|
||||
"ignored_unsupported": "Istruzione non supportata ignorata: {{items}}",
|
||||
"ignored_duplicate": "Ignorato (categoria già impostata): {{items}}",
|
||||
"ref_audio_unusable": "Nessuna voce rilevata nell'audio di riferimento: la clip è silenziosa o quasi, quindi non c'è alcuna voce da clonare. Registra di nuovo più vicino al microfono (verifica che sia selezionato il dispositivo di ingresso corretto e che il livello si muova) oppure scegli un'altra clip."
|
||||
"ref_audio_unusable": "Nessuna voce rilevata nell'audio di riferimento: la clip è silenziosa o quasi, quindi non c'è alcuna voce da clonare. Registra di nuovo più vicino al microfono (verifica che sia selezionato il dispositivo di ingresso corretto e che il livello si muova) oppure scegli un'altra clip.",
|
||||
"ref_audio_too_long": "Un riferimento può durare al massimo 20 secondi con trascrizione o 75 secondi senza. Taglialo a un passaggio parlato chiaro di 3–10 secondi; se fornisci il testo, taglia la trascrizione sullo stesso passaggio.",
|
||||
"ref_audio_no_speech": "Il rilevamento automatico non ha trovato parole pronunciate nel riferimento. Taglialo a un passaggio parlato chiaro di 3–10 secondi o fornisci una trascrizione corrispondente."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Condivisione e accesso remoto",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "エラー: {{message}}",
|
||||
"ignored_unsupported": "サポートされていない命令は無視されました: {{items}}",
|
||||
"ignored_duplicate": "無視 (カテゴリはすでに設定されています): {{items}}",
|
||||
"ref_audio_unusable": "リファレンス音声から声を検出できませんでした。クリップが無音またはほぼ無音のため、クローンする声がありません。マイクに近づいて録音し直すか(正しい入力デバイスが選択されているか確認してください)、別のクリップを選んでください。"
|
||||
"ref_audio_unusable": "リファレンス音声から声を検出できませんでした。クリップが無音またはほぼ無音のため、クローンする声がありません。マイクに近づいて録音し直すか(正しい入力デバイスが選択されているか確認してください)、別のクリップを選んでください。",
|
||||
"ref_audio_too_long": "参照音声は文字起こし付きなら最長20秒、なしなら最長75秒です。明瞭な発話を含む3〜10秒に切り詰め、テキストを指定する場合は文字起こしも同じ区間に合わせてください。",
|
||||
"ref_audio_no_speech": "自動音声検出で参照音声から発話が見つかりませんでした。明瞭な発話を含む3〜10秒に切り詰めるか、一致する文字起こしを指定してください。"
|
||||
},
|
||||
"sharing": {
|
||||
"title": "共有とリモートアクセス",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "오류: {{message}}",
|
||||
"ignored_unsupported": "지원되지 않는 지침을 무시했습니다: {{items}}",
|
||||
"ignored_duplicate": "무시됨(카테고리가 이미 설정됨): {{items}}",
|
||||
"ref_audio_unusable": "레퍼런스 오디오에서 음성이 감지되지 않았습니다. 클립이 무음이거나 거의 무음이라 복제할 목소리가 없습니다. 마이크에 더 가까이서 다시 녹음하거나(올바른 입력 장치가 선택되어 있는지 확인) 다른 클립을 선택하세요."
|
||||
"ref_audio_unusable": "레퍼런스 오디오에서 음성이 감지되지 않았습니다. 클립이 무음이거나 거의 무음이라 복제할 목소리가 없습니다. 마이크에 더 가까이서 다시 녹음하거나(올바른 입력 장치가 선택되어 있는지 확인) 다른 클립을 선택하세요.",
|
||||
"ref_audio_too_long": "참조 오디오는 텍스트가 있으면 최대 20초, 없으면 최대 75초까지 사용할 수 있습니다. 명확한 음성이 있는 3~10초 구간으로 자르고, 텍스트를 제공한다면 같은 구간에 맞추세요.",
|
||||
"ref_audio_no_speech": "자동 음성 감지에서 참조 오디오의 발화 단어를 찾지 못했습니다. 명확한 음성이 있는 3~10초 구간으로 자르거나 일치하는 텍스트를 제공하세요."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "공유 및 원격 액세스",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Fout: {{message}}",
|
||||
"ignored_unsupported": "Genegeerde niet-ondersteunde instructie: {{items}}",
|
||||
"ignored_duplicate": "Genegeerd (categorie al ingesteld): {{items}}",
|
||||
"ref_audio_unusable": "Er is geen spraak gehoord in de referentie-audio — de clip is stil of bijna stil, dus er is geen stem om te klonen. Neem opnieuw op dichter bij de microfoon (controleer of het juiste invoerapparaat is geselecteerd en het niveau beweegt) of kies een andere clip."
|
||||
"ref_audio_unusable": "Er is geen spraak gehoord in de referentie-audio — de clip is stil of bijna stil, dus er is geen stem om te klonen. Neem opnieuw op dichter bij de microfoon (controleer of het juiste invoerapparaat is geselecteerd en het niveau beweegt) of kies een andere clip.",
|
||||
"ref_audio_too_long": "Een referentie mag maximaal 20 seconden duren met transcript, of 75 seconden zonder transcript. Knip deze tot een duidelijke gesproken passage van 3–10 seconden; knip een meegeleverd transcript tot dezelfde passage.",
|
||||
"ref_audio_no_speech": "De automatische spraakdetectie vond geen gesproken woorden in de referentie. Knip deze tot een duidelijke gesproken passage van 3–10 seconden of geef een passend transcript op."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Delen en externe toegang",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Błąd: {{message}}",
|
||||
"ignored_unsupported": "Ignorowana nieobsługiwana instrukcja: {{items}}",
|
||||
"ignored_duplicate": "Ignorowane (kategoria już ustawiona): {{items}}",
|
||||
"ref_audio_unusable": "W nagraniu referencyjnym nie wykryto mowy — klip jest cichy lub prawie cichy, więc nie ma głosu do sklonowania. Nagraj ponownie bliżej mikrofonu (sprawdź, czy wybrano właściwe urządzenie wejściowe i czy poziom się porusza) albo wybierz inny klip."
|
||||
"ref_audio_unusable": "W nagraniu referencyjnym nie wykryto mowy — klip jest cichy lub prawie cichy, więc nie ma głosu do sklonowania. Nagraj ponownie bliżej mikrofonu (sprawdź, czy wybrano właściwe urządzenie wejściowe i czy poziom się porusza) albo wybierz inny klip.",
|
||||
"ref_audio_too_long": "Nagranie referencyjne może mieć najwyżej 20 sekund z transkrypcją lub 75 sekund bez niej. Przytnij je do wyraźnego fragmentu mowy 3–10 sekund; jeśli podajesz tekst, przytnij transkrypcję do tego samego fragmentu.",
|
||||
"ref_audio_no_speech": "Automatyczne wykrywanie mowy nie znalazło wypowiedzianych słów. Przytnij nagranie do wyraźnego fragmentu mowy 3–10 sekund lub podaj pasującą transkrypcję."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Udostępnianie i dostęp zdalny",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Erro: {{message}}",
|
||||
"ignored_unsupported": "Instrução não suportada ignorada: {{items}}",
|
||||
"ignored_duplicate": "Ignorado (categoria já definida): {{items}}",
|
||||
"ref_audio_unusable": "Nenhuma fala foi detectada no áudio de referência — o clipe está em silêncio ou quase, então não há voz para clonar. Grave novamente mais perto do microfone (confira se o dispositivo de entrada correto está selecionado e se o nível se move) ou escolha outro clipe."
|
||||
"ref_audio_unusable": "Nenhuma fala foi detectada no áudio de referência — o clipe está em silêncio ou quase, então não há voz para clonar. Grave novamente mais perto do microfone (confira se o dispositivo de entrada correto está selecionado e se o nível se move) ou escolha outro clipe.",
|
||||
"ref_audio_too_long": "Uma referência pode ter no máximo 20 segundos com transcrição ou 75 segundos sem ela. Corte-a para um trecho de fala claro de 3–10 segundos; se fornecer texto, corte a transcrição para o mesmo trecho.",
|
||||
"ref_audio_no_speech": "A detecção automática não encontrou palavras faladas na referência. Corte-a para um trecho de fala claro de 3–10 segundos ou forneça uma transcrição correspondente."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Compartilhamento e acesso remoto",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Ошибка: {{message}}",
|
||||
"ignored_unsupported": "Игнорируется неподдерживаемая инструкция: {{items}}",
|
||||
"ignored_duplicate": "Игнорируется (категория уже установлена): {{items}}",
|
||||
"ref_audio_unusable": "В референсном аудио не слышно речи — клип тихий или полностью беззвучный, клонировать нечего. Запишите заново ближе к микрофону (проверьте, что выбрано правильное устройство ввода и уровень записи движется) или выберите другой клип."
|
||||
"ref_audio_unusable": "В референсном аудио не слышно речи — клип тихий или полностью беззвучный, клонировать нечего. Запишите заново ближе к микрофону (проверьте, что выбрано правильное устройство ввода и уровень записи движется) или выберите другой клип.",
|
||||
"ref_audio_too_long": "Референс может длиться не более 20 секунд с расшифровкой или 75 секунд без неё. Обрежьте его до чёткого речевого фрагмента длиной 3–10 секунд; если добавляете текст, обрежьте расшифровку до того же фрагмента.",
|
||||
"ref_audio_no_speech": "Автоматическое распознавание не обнаружило речи в референсе. Обрежьте его до чёткого речевого фрагмента длиной 3–10 секунд или добавьте соответствующую расшифровку."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Совместное использование и удаленный доступ",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Fel: {{message}}",
|
||||
"ignored_unsupported": "Ignorerad instruktion som inte stöds: {{items}}",
|
||||
"ignored_duplicate": "Ignorerad (kategori redan inställd): {{items}}",
|
||||
"ref_audio_unusable": "Inget tal kunde höras i referensljudet — klippet är tyst eller nästan tyst, så det finns ingen röst att klona. Spela in igen närmare mikrofonen (kontrollera att rätt ingångsenhet är vald och att nivån rör sig) eller välj ett annat klipp."
|
||||
"ref_audio_unusable": "Inget tal kunde höras i referensljudet — klippet är tyst eller nästan tyst, så det finns ingen röst att klona. Spela in igen närmare mikrofonen (kontrollera att rätt ingångsenhet är vald och att nivån rör sig) eller välj ett annat klipp.",
|
||||
"ref_audio_too_long": "En referens får vara högst 20 sekunder med transkription eller 75 sekunder utan. Klipp den till ett tydligt talavsnitt på 3–10 sekunder; om du anger text ska transkriptionen klippas till samma avsnitt.",
|
||||
"ref_audio_no_speech": "Den automatiska taldetekteringen hittade inga talade ord. Klipp referensen till ett tydligt talavsnitt på 3–10 sekunder eller ange en matchande transkription."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Delning och fjärråtkomst",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "ข้อผิดพลาด: {{message}}",
|
||||
"ignored_unsupported": "ละเว้นคำสั่งที่ไม่สนับสนุน: {{items}}",
|
||||
"ignored_duplicate": "ละเว้น (หมวดหมู่ที่ตั้งไว้แล้ว): {{items}}",
|
||||
"ref_audio_unusable": "ไม่ได้ยินเสียงพูดในไฟล์เสียงอ้างอิง — คลิปเงียบหรือเกือบไม่มีเสียง จึงไม่มีเสียงให้โคลน โปรดอัดใหม่ให้ใกล้ไมโครโฟนมากขึ้น (ตรวจสอบว่าเลือกอุปกรณ์รับเสียงถูกต้อง) หรือเลือกคลิปอื่น"
|
||||
"ref_audio_unusable": "ไม่ได้ยินเสียงพูดในไฟล์เสียงอ้างอิง — คลิปเงียบหรือเกือบไม่มีเสียง จึงไม่มีเสียงให้โคลน โปรดอัดใหม่ให้ใกล้ไมโครโฟนมากขึ้น (ตรวจสอบว่าเลือกอุปกรณ์รับเสียงถูกต้อง) หรือเลือกคลิปอื่น",
|
||||
"ref_audio_too_long": "เสียงอ้างอิงยาวได้ไม่เกิน 20 วินาทีเมื่อมีข้อความถอดเสียง หรือ 75 วินาทีเมื่อไม่มี ตัดให้เหลือช่วงคำพูดชัดเจน 3–10 วินาที และหากมีข้อความให้ตัดตรงกับช่วงเดียวกัน",
|
||||
"ref_audio_no_speech": "การตรวจจับอัตโนมัติไม่พบคำพูดในเสียงอ้างอิง ตัดให้เหลือช่วงคำพูดชัดเจน 3–10 วินาที หรือใส่ข้อความถอดเสียงที่ตรงกัน"
|
||||
},
|
||||
"sharing": {
|
||||
"title": "การแชร์และการเข้าถึงระยะไกล",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Hata: {{message}}",
|
||||
"ignored_unsupported": "Desteklenmeyen talimat göz ardı edildi: {{items}}",
|
||||
"ignored_duplicate": "Yoksayıldı (kategori zaten ayarlandı): {{items}}",
|
||||
"ref_audio_unusable": "Referans seste konuşma duyulamadı — klip sessiz ya da neredeyse sessiz, bu yüzden klonlanacak bir ses yok. Mikrofona daha yakın şekilde yeniden kayıt yapın (doğru giriş aygıtının seçili olduğundan ve seviyenin hareket ettiğinden emin olun) veya başka bir klip seçin."
|
||||
"ref_audio_unusable": "Referans seste konuşma duyulamadı — klip sessiz ya da neredeyse sessiz, bu yüzden klonlanacak bir ses yok. Mikrofona daha yakın şekilde yeniden kayıt yapın (doğru giriş aygıtının seçili olduğundan ve seviyenin hareket ettiğinden emin olun) veya başka bir klip seçin.",
|
||||
"ref_audio_too_long": "Bir referans, transkriptle en fazla 20 saniye; transkriptsiz en fazla 75 saniye olabilir. Sesi 3–10 saniyelik net bir konuşma bölümüne kırpın; metin sağlıyorsanız transkripti de aynı bölüme kırpın.",
|
||||
"ref_audio_no_speech": "Otomatik konuşma algılama referansta söylenmiş kelime bulamadı. Sesi 3–10 saniyelik net bir konuşma bölümüne kırpın veya eşleşen bir transkript sağlayın."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Paylaşım ve Uzaktan Erişim",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Помилка: {{message}}",
|
||||
"ignored_unsupported": "Ігнорована непідтримувана інструкція: {{items}}",
|
||||
"ignored_duplicate": "Проігноровано (категорію вже встановлено): {{items}}",
|
||||
"ref_audio_unusable": "У референсному аудіо не чути мовлення — кліп тихий або повністю беззвучний, тож голосу для клонування немає. Запишіть ще раз ближче до мікрофона (перевірте, чи вибрано правильний пристрій введення і чи рухається рівень) або виберіть інший кліп."
|
||||
"ref_audio_unusable": "У референсному аудіо не чути мовлення — кліп тихий або повністю беззвучний, тож голосу для клонування немає. Запишіть ще раз ближче до мікрофона (перевірте, чи вибрано правильний пристрій введення і чи рухається рівень) або виберіть інший кліп.",
|
||||
"ref_audio_too_long": "Референс може тривати не більше 20 секунд із транскрипцією або 75 секунд без неї. Обріжте його до чіткого мовного уривка тривалістю 3–10 секунд; якщо додаєте текст, обріжте транскрипцію до того самого уривка.",
|
||||
"ref_audio_no_speech": "Автоматичне розпізнавання не виявило мовлення в референсі. Обріжте його до чіткого мовного уривка тривалістю 3–10 секунд або додайте відповідну транскрипцію."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Спільне використання та віддалений доступ",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "Lỗi: {{message}}",
|
||||
"ignored_unsupported": "Đã bỏ qua hướng dẫn không được hỗ trợ: {{items}}",
|
||||
"ignored_duplicate": "Đã bỏ qua (danh mục đã được đặt): {{items}}",
|
||||
"ref_audio_unusable": "Không nghe thấy giọng nói trong âm thanh tham chiếu — đoạn clip im lặng hoặc gần như im lặng nên không có giọng để nhân bản. Hãy thu âm lại gần micro hơn (kiểm tra đã chọn đúng thiết bị đầu vào và mức âm có dao động) hoặc chọn một clip khác."
|
||||
"ref_audio_unusable": "Không nghe thấy giọng nói trong âm thanh tham chiếu — đoạn clip im lặng hoặc gần như im lặng nên không có giọng để nhân bản. Hãy thu âm lại gần micro hơn (kiểm tra đã chọn đúng thiết bị đầu vào và mức âm có dao động) hoặc chọn một clip khác.",
|
||||
"ref_audio_too_long": "Âm thanh tham chiếu dài tối đa 20 giây khi có bản chép lời hoặc 75 giây khi không có. Hãy cắt thành đoạn lời nói rõ dài 3–10 giây; nếu cung cấp văn bản, hãy cắt bản chép lời theo đúng đoạn đó.",
|
||||
"ref_audio_no_speech": "Tính năng phát hiện tự động không tìm thấy lời nói trong âm thanh tham chiếu. Hãy cắt thành đoạn lời nói rõ dài 3–10 giây hoặc cung cấp bản chép lời khớp."
|
||||
},
|
||||
"sharing": {
|
||||
"title": "Chia sẻ & Truy cập từ xa",
|
||||
|
||||
@@ -2066,7 +2066,9 @@
|
||||
"error_prefix": "错误:{{message}}",
|
||||
"ignored_unsupported": "忽略不支持的指令:{{items}}",
|
||||
"ignored_duplicate": "忽略(类别已设置):{{items}}",
|
||||
"ref_audio_unusable": "参考音频中听不到任何语音——片段是静音或几乎静音的,因此没有可克隆的声音。请靠近麦克风重新录制(检查是否选择了正确的输入设备、电平是否有跳动),或换一段其他音频。"
|
||||
"ref_audio_unusable": "参考音频中听不到任何语音——片段是静音或几乎静音的,因此没有可克隆的声音。请靠近麦克风重新录制(检查是否选择了正确的输入设备、电平是否有跳动),或换一段其他音频。",
|
||||
"ref_audio_too_long": "参考音频在提供文字稿时最长20秒,未提供时最长75秒。请裁剪为含清晰语音的3–10秒片段;若提供文字稿,请将其裁剪到同一片段。",
|
||||
"ref_audio_no_speech": "自动语音检测未在参考音频中发现口语。请裁剪为含清晰语音的3–10秒片段,或提供匹配的文字稿。"
|
||||
},
|
||||
"sharing": {
|
||||
"title": "共享和远程访问",
|
||||
|
||||
@@ -2059,7 +2059,9 @@
|
||||
"error_prefix": "錯誤:{{message}}",
|
||||
"ignored_unsupported": "忽略不支援的指令:{{items}}",
|
||||
"ignored_duplicate": "忽略(類別已設定):{{items}}",
|
||||
"ref_audio_unusable": "參考音訊中聽不到任何語音——片段為靜音或幾乎靜音,因此沒有可複製的聲音。請靠近麥克風重新錄製(檢查是否選擇了正確的輸入裝置、音量電平是否有跳動),或改用其他片段。"
|
||||
"ref_audio_unusable": "參考音訊中聽不到任何語音——片段為靜音或幾乎靜音,因此沒有可複製的聲音。請靠近麥克風重新錄製(檢查是否選擇了正確的輸入裝置、音量電平是否有跳動),或改用其他片段。",
|
||||
"ref_audio_too_long": "參考音訊在提供文字稿時最長20秒,未提供時最長75秒。請裁剪為含清晰語音的3–10秒片段;若提供文字稿,請將其裁剪到同一片段。",
|
||||
"ref_audio_no_speech": "自動語音偵測未在參考音訊中找到口語。請裁剪為含清晰語音的3–10秒片段,或提供相符的文字稿。"
|
||||
},
|
||||
"sharing": {
|
||||
"title": "共享和遠端存取",
|
||||
|
||||
@@ -45,6 +45,24 @@ describe('toastErrorWithReport user-fixable marker mapping (#1188)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('shows localized trimming guidance for an overlong aligned reference', () => {
|
||||
const detail =
|
||||
'[clone_ref_too_long] Reference audio is 184.3 seconds long; supplied transcripts ' +
|
||||
'support at most 20 seconds.';
|
||||
toastErrorWithReport(detail, new Error(detail));
|
||||
expect(toastErrorMock).toHaveBeenCalledWith('t:tts_errors.ref_audio_too_long', {
|
||||
duration: 8000,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows localized guidance when bounded selection finds no speech', () => {
|
||||
const detail = '[clone_ref_no_speech] Automatic speech detection found no spoken words.';
|
||||
toastErrorWithReport(detail, new Error(detail));
|
||||
expect(toastErrorMock).toHaveBeenCalledWith('t:tts_errors.ref_audio_no_speech', {
|
||||
duration: 8000,
|
||||
});
|
||||
});
|
||||
|
||||
it('unmarked errors keep the Report-action toast (JSX renderer, not a plain string)', () => {
|
||||
toastErrorWithReport('Error: something exploded', new Error('something exploded'));
|
||||
expect(toastErrorMock).toHaveBeenCalledTimes(1);
|
||||
@@ -58,6 +76,8 @@ describe('toastErrorWithReport user-fixable marker mapping (#1188)', () => {
|
||||
for (const f of files) {
|
||||
const locale = JSON.parse(fs.readFileSync(path.join(localesDir, f), 'utf8'));
|
||||
expect(locale.tts_errors?.ref_audio_unusable, `${f} missing the key`).toBeTruthy();
|
||||
expect(locale.tts_errors?.ref_audio_too_long, `${f} missing the key`).toBeTruthy();
|
||||
expect(locale.tts_errors?.ref_audio_no_speech, `${f} missing the key`).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const {
|
||||
decodePcm16Base64,
|
||||
peaksFromChunkList,
|
||||
StreamingPreviewError,
|
||||
shouldFallbackToClassic,
|
||||
} = await import('../utils/streamingTts');
|
||||
const { getPlaybackTrack, stopActivePlayback, seekActivePlayback } =
|
||||
await import('../utils/playback');
|
||||
@@ -312,6 +313,21 @@ describe('streamGenerateSpeech', () => {
|
||||
expect(plain.retryable).toBe(false);
|
||||
});
|
||||
|
||||
it('marks actionable clone-reference errors terminal to prevent a classic retry', async () => {
|
||||
apiFetch.mockResolvedValue(
|
||||
ndjsonResponse([
|
||||
startEvent(1),
|
||||
{ type: 'error', detail: '[clone_ref_no_speech] trim the reference' },
|
||||
]),
|
||||
);
|
||||
|
||||
const err = await streamGenerateSpeech(new FormData(), {}).catch((e) => e);
|
||||
|
||||
expect(err).toBeInstanceOf(StreamingPreviewError);
|
||||
expect(err.terminal).toBe(true);
|
||||
expect(shouldFallbackToClassic(err)).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects with StreamingPreviewError when the stream ends without done', async () => {
|
||||
apiFetch.mockResolvedValue(ndjsonResponse([startEvent(2), chunkEvent(0)]));
|
||||
await expect(streamGenerateSpeech(new FormData(), {})).rejects.toThrow(StreamingPreviewError);
|
||||
|
||||
@@ -27,6 +27,8 @@ import { openBugReport } from './bugReport';
|
||||
// omnivoice/utils/audio.py, [shutting_down] in main.py) — keep in sync.
|
||||
const USER_FIXABLE_MARKERS = {
|
||||
'[clone_ref_unusable]': 'tts_errors.ref_audio_unusable',
|
||||
'[clone_ref_too_long]': 'tts_errors.ref_audio_too_long',
|
||||
'[clone_ref_no_speech]': 'tts_errors.ref_audio_no_speech',
|
||||
'[shutting_down]': 'errors.backend_shutting_down',
|
||||
};
|
||||
|
||||
|
||||
@@ -79,9 +79,13 @@ export class StreamingPreviewError extends Error {
|
||||
// the same error. Carried here so useTTS can tell the two apart.
|
||||
this.retryable = opts?.retryable === true;
|
||||
this.retryAfter = opts?.retryAfter ?? null;
|
||||
this.terminal = opts?.terminal === true;
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldFallbackToClassic = (error) =>
|
||||
error instanceof StreamingPreviewError && !error.retryable && !error.terminal;
|
||||
|
||||
/** base64 → Int16 PCM → Float32 samples in [-1, 1]. Exported for tests. */
|
||||
export const decodePcm16Base64 = (b64) => {
|
||||
const bin = atob(b64);
|
||||
@@ -380,9 +384,16 @@ async function _streamGenerateSpeech(
|
||||
} else if (ev.type === 'done') {
|
||||
meta = ev;
|
||||
} else if (ev.type === 'error') {
|
||||
throw new StreamingPreviewError(ev.detail || 'TTS stream reported an error', {
|
||||
const detail = ev.detail || 'TTS stream reported an error';
|
||||
const terminal = [
|
||||
'[clone_ref_unusable]',
|
||||
'[clone_ref_too_long]',
|
||||
'[clone_ref_no_speech]',
|
||||
].some((marker) => detail.includes(marker));
|
||||
throw new StreamingPreviewError(detail, {
|
||||
retryable: ev.retryable === true,
|
||||
retryAfter: ev.retry_after ?? null,
|
||||
terminal,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,11 +53,12 @@ from transformers.modeling_outputs import ModelOutput
|
||||
from transformers.models.auto import CONFIG_MAPPING, AutoConfig
|
||||
|
||||
from omnivoice.utils.audio import (
|
||||
CLONE_REF_NO_SPEECH_MARKER,
|
||||
CLONE_REF_TOO_LONG_MARKER,
|
||||
cross_fade_chunks,
|
||||
fade_and_pad_audio,
|
||||
load_audio,
|
||||
remove_silence_safe,
|
||||
trim_long_audio,
|
||||
validate_clone_reference,
|
||||
)
|
||||
from omnivoice.utils.duration import RuleDurationEstimator
|
||||
@@ -787,14 +788,61 @@ class OmniVoice(PreTrainedModel):
|
||||
if 0 < ref_rms < 0.1:
|
||||
ref_wav = ref_wav * 0.1 / ref_rms
|
||||
|
||||
if preprocess_prompt:
|
||||
# Trim long reference audio (>20s) by splitting at the largest silence gap.
|
||||
# Skip trimming when ref_text is user-provided, otherwise the
|
||||
# trimmed audio will no longer match the full transcript.
|
||||
if ref_text is None:
|
||||
ref_wav = trim_long_audio(
|
||||
ref_wav, self.sampling_rate, trim_threshold=20.0
|
||||
ref_duration = ref_wav.size(-1) / self.sampling_rate
|
||||
if ref_duration > 20.0:
|
||||
if ref_text is not None:
|
||||
raise ValueError(
|
||||
f"{CLONE_REF_TOO_LONG_MARKER} Reference audio is "
|
||||
f"{ref_duration:.1f} seconds long; supplied transcripts support "
|
||||
"at most 20 seconds. Trim both the audio and transcript to the "
|
||||
"same 3-10 second passage, or omit the transcript so VoiceStudio "
|
||||
"can trim and transcribe the clip automatically."
|
||||
)
|
||||
# Transcript-free automatic selection examines every passage, but
|
||||
# caps the work at five bounded ASR calls. Longer references need
|
||||
# an explicit user-selected passage rather than a lossy sampling
|
||||
# policy that could silently miss speech between fixed windows.
|
||||
max_samples = int(15.0 * self.sampling_rate)
|
||||
max_auto_samples = 5 * max_samples
|
||||
if ref_wav.size(-1) > max_auto_samples:
|
||||
raise ValueError(
|
||||
f"{CLONE_REF_TOO_LONG_MARKER} Reference audio is "
|
||||
f"{ref_duration:.1f} seconds long; automatic transcript-free "
|
||||
"selection supports at most 75 seconds. Trim the audio to a "
|
||||
"3-10 second speech passage, or supply a matching transcript."
|
||||
)
|
||||
|
||||
original_power = ref_wav.abs().amax(dim=0).square()
|
||||
activity_cap = max(float(original_power.mean()) * 100.0, 1e-10)
|
||||
|
||||
def activity_score(audio):
|
||||
power = audio.abs().amax(dim=0).square().clamp_max(activity_cap)
|
||||
return float(power.double().sum())
|
||||
|
||||
if self._asr_pipe is None:
|
||||
logger.info("ASR model not loaded yet, loading on-the-fly ...")
|
||||
self.load_asr_model()
|
||||
candidates = list(ref_wav.split(max_samples, dim=-1))
|
||||
|
||||
def speech_score(text):
|
||||
return len(re.sub(r"[^\w]+", "", text or "", flags=re.UNICODE))
|
||||
|
||||
transcribed = [
|
||||
(self.transcribe((candidate, self.sampling_rate)), candidate)
|
||||
for candidate in candidates
|
||||
]
|
||||
ref_text, ref_wav = max(
|
||||
transcribed,
|
||||
key=lambda item: (speech_score(item[0]), activity_score(item[1])),
|
||||
)
|
||||
if speech_score(ref_text) == 0:
|
||||
raise ValueError(
|
||||
f"{CLONE_REF_NO_SPEECH_MARKER} Automatic speech detection "
|
||||
"could not find spoken words in the reference. Trim it to a "
|
||||
"clear 3-10 second speech passage, or supply a matching transcript."
|
||||
)
|
||||
|
||||
if preprocess_prompt:
|
||||
# #1188: the fixed -50 dBFS silence threshold used to consume a
|
||||
# quiet-but-real recording wholesale and dead-end with
|
||||
# "Reference audio is empty after silence removal". The safe
|
||||
@@ -808,6 +856,11 @@ class OmniVoice(PreTrainedModel):
|
||||
trail_sil=200,
|
||||
)
|
||||
|
||||
# Cropping and silence removal happen after the original validation.
|
||||
# Re-check the actual aligned passage that ASR/tokenization will see.
|
||||
prepared_rms = torch.sqrt(torch.mean(torch.square(ref_wav))).item()
|
||||
validate_clone_reference(ref_wav, prepared_rms)
|
||||
|
||||
ref_duration = ref_wav.size(-1) / self.sampling_rate
|
||||
if ref_duration > 20.0:
|
||||
logger.warning(
|
||||
|
||||
@@ -38,6 +38,8 @@ logger = logging.getLogger(__name__)
|
||||
# instead of the raw English detail (issue #1188). The frontend matches this
|
||||
# exact string (frontend/src/utils/errorToast.jsx) — change both or neither.
|
||||
CLONE_REF_UNUSABLE_MARKER = "[clone_ref_unusable]"
|
||||
CLONE_REF_TOO_LONG_MARKER = "[clone_ref_too_long]"
|
||||
CLONE_REF_NO_SPEECH_MARKER = "[clone_ref_no_speech]"
|
||||
|
||||
|
||||
def load_audio(audio_path: str, sampling_rate: int):
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
"""Long cloning references must fail before unbounded tokenizer allocation (#1578)."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import soundfile as sf
|
||||
import torch
|
||||
|
||||
class _Tokenizer:
|
||||
config = SimpleNamespace(hop_length=320)
|
||||
device = "cpu"
|
||||
|
||||
def __init__(self, reject=True):
|
||||
self.reject = reject
|
||||
self.seen_samples = None
|
||||
|
||||
def encode(self, audio):
|
||||
self.seen_samples = audio.shape[-1]
|
||||
self.seen_peak = float(audio.abs().max())
|
||||
if self.reject:
|
||||
raise AssertionError("an unsafe reference reached the audio tokenizer")
|
||||
return SimpleNamespace(audio_codes=torch.zeros((1, 1, 1), dtype=torch.long))
|
||||
|
||||
|
||||
def _model(*, reject_tokenization=True):
|
||||
from omnivoice.models.omnivoice import OmniVoice
|
||||
|
||||
model = OmniVoice.__new__(OmniVoice)
|
||||
model.sampling_rate = 24_000
|
||||
model.audio_tokenizer = _Tokenizer(reject=reject_tokenization)
|
||||
return model
|
||||
|
||||
|
||||
def test_supplied_transcript_rejects_long_tensor_before_tokenization():
|
||||
audio = torch.full((1, 21 * 24_000), 0.1)
|
||||
|
||||
with pytest.raises(ValueError, match=r"\[clone_ref_too_long\].*at most 20 seconds"):
|
||||
_model().create_voice_clone_prompt(
|
||||
(audio, 24_000),
|
||||
ref_text="Transcript supplied by the user.",
|
||||
preprocess_prompt=True,
|
||||
)
|
||||
|
||||
|
||||
def test_supplied_transcript_rejects_long_file_before_tokenization(tmp_path):
|
||||
path = tmp_path / "long-reference.wav"
|
||||
sf.write(path, torch.full((21 * 24_000,), 0.1).numpy(), 24_000)
|
||||
|
||||
with pytest.raises(ValueError, match=r"\[clone_ref_too_long\].*at most 20 seconds"):
|
||||
_model().create_voice_clone_prompt(
|
||||
str(path),
|
||||
ref_text="Transcript supplied by the user.",
|
||||
preprocess_prompt=True,
|
||||
)
|
||||
|
||||
|
||||
def test_missing_transcript_is_safely_trimmed_even_when_preprocessing_is_disabled():
|
||||
model = _model(reject_tokenization=False)
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda _audio: "Automatically aligned transcript."
|
||||
audio = torch.full((1, 21 * 24_000), 0.1)
|
||||
|
||||
model.create_voice_clone_prompt(
|
||||
(audio, 24_000),
|
||||
ref_text=None,
|
||||
preprocess_prompt=False,
|
||||
)
|
||||
|
||||
assert model.audio_tokenizer.seen_samples <= 15 * 24_000
|
||||
|
||||
|
||||
def test_missing_transcript_still_has_a_hard_bound_when_silence_split_cannot_trim():
|
||||
model = _model(reject_tokenization=False)
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda _audio: "Automatically aligned transcript."
|
||||
audio = torch.full((1, 21 * 24_000), 0.1)
|
||||
|
||||
model.create_voice_clone_prompt((audio, 24_000), ref_text=None)
|
||||
|
||||
assert model.audio_tokenizer.seen_samples <= 15 * 24_000
|
||||
|
||||
|
||||
def test_hard_bound_keeps_late_speech_instead_of_cropping_only_silence(monkeypatch):
|
||||
model = _model(reject_tokenization=False)
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda _audio: "Automatically aligned transcript."
|
||||
monkeypatch.setattr(
|
||||
"omnivoice.models.omnivoice.remove_silence_safe",
|
||||
lambda audio, *_args, **_kwargs: audio,
|
||||
)
|
||||
audio = torch.zeros((1, 21 * 24_000))
|
||||
audio[:, 16 * 24_000 :] = 0.1
|
||||
|
||||
model.create_voice_clone_prompt((audio, 24_000), ref_text=None)
|
||||
|
||||
assert model.audio_tokenizer.seen_samples <= 15 * 24_000
|
||||
assert model.audio_tokenizer.seen_peak > 0
|
||||
|
||||
|
||||
def test_hard_bound_prefers_speech_over_distant_transient(monkeypatch):
|
||||
model = _model(reject_tokenization=False)
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda _audio: "Automatically aligned transcript."
|
||||
monkeypatch.setattr(
|
||||
"omnivoice.models.omnivoice.remove_silence_safe",
|
||||
lambda audio, *_args, **_kwargs: audio,
|
||||
)
|
||||
audio = torch.zeros((1, 31 * 24_000))
|
||||
audio[:, 1 * 24_000] = 1.0
|
||||
audio[:, 26 * 24_000 :] = 0.01
|
||||
|
||||
model.create_voice_clone_prompt((audio, 24_000), ref_text=None)
|
||||
|
||||
assert model.audio_tokenizer.seen_samples <= 15 * 24_000
|
||||
# Quiet references are RMS-normalized before cropping. The speech peak is
|
||||
# therefore non-zero but remains far below the isolated transient.
|
||||
assert 0 < model.audio_tokenizer.seen_peak < 1
|
||||
|
||||
|
||||
def test_real_silence_splitter_does_not_keep_silent_prefix(monkeypatch):
|
||||
model = _model(reject_tokenization=False)
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda _audio: "Automatically aligned transcript."
|
||||
monkeypatch.setattr(
|
||||
"omnivoice.models.omnivoice.remove_silence_safe",
|
||||
lambda audio, *_args, **_kwargs: audio,
|
||||
)
|
||||
audio = torch.zeros((1, 21 * 24_000))
|
||||
audio[:, 16 * 24_000 :] = 0.1
|
||||
|
||||
model.create_voice_clone_prompt((audio, 24_000), ref_text=None)
|
||||
|
||||
assert model.audio_tokenizer.seen_samples <= 15 * 24_000
|
||||
assert model.audio_tokenizer.seen_peak > 0
|
||||
|
||||
|
||||
def test_real_silence_splitter_ignores_transient_before_late_speech(monkeypatch):
|
||||
model = _model(reject_tokenization=False)
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda _audio: "Automatically aligned transcript."
|
||||
monkeypatch.setattr(
|
||||
"omnivoice.models.omnivoice.remove_silence_safe",
|
||||
lambda audio, *_args, **_kwargs: audio,
|
||||
)
|
||||
audio = torch.zeros((1, 21 * 24_000))
|
||||
audio[:, 1 * 24_000] = 1.0
|
||||
audio[:, 16 * 24_000 :] = 0.01
|
||||
|
||||
model.create_voice_clone_prompt((audio, 24_000), ref_text=None)
|
||||
|
||||
assert model.audio_tokenizer.seen_samples <= 15 * 24_000
|
||||
assert 0 < model.audio_tokenizer.seen_peak < 1
|
||||
|
||||
|
||||
def test_speech_aware_bound_prefers_quiet_voice_over_loud_non_speech(monkeypatch):
|
||||
model = _model(reject_tokenization=False)
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda candidate: (
|
||||
"Spoken words." if float(candidate[0].abs().mean()) < 0.05 else ""
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"omnivoice.models.omnivoice.remove_silence_safe",
|
||||
lambda audio, *_args, **_kwargs: audio,
|
||||
)
|
||||
audio = torch.full((1, 30 * 24_000), 0.4)
|
||||
audio[:, 15 * 24_000 :] = 0.02
|
||||
|
||||
prompt = model.create_voice_clone_prompt((audio, 24_000), ref_text=None)
|
||||
|
||||
assert prompt.ref_text == "Spoken words."
|
||||
assert model.audio_tokenizer.seen_peak < 0.1
|
||||
|
||||
|
||||
def test_far_late_speech_at_auto_select_limit_survives_with_five_asr_calls(monkeypatch):
|
||||
model = _model(reject_tokenization=False)
|
||||
model.sampling_rate = 100
|
||||
model._asr_pipe = object()
|
||||
calls = []
|
||||
|
||||
def transcribe(candidate):
|
||||
calls.append(candidate[0])
|
||||
return "Late speech." if float(candidate[0][0, -1]) > 0 else ""
|
||||
|
||||
model.transcribe = transcribe
|
||||
monkeypatch.setattr(
|
||||
"omnivoice.models.omnivoice.remove_silence_safe",
|
||||
lambda audio, *_args, **_kwargs: audio,
|
||||
)
|
||||
audio = torch.zeros((1, 75 * 100))
|
||||
audio[:, -5 * 100 :] = 0.02
|
||||
|
||||
prompt = model.create_voice_clone_prompt((audio, 100), ref_text=None)
|
||||
|
||||
assert prompt.ref_text == "Late speech."
|
||||
assert len(calls) == 5
|
||||
|
||||
|
||||
def test_auto_select_rejects_one_sample_over_limit_before_asr():
|
||||
model = _model(reject_tokenization=False)
|
||||
model.sampling_rate = 100
|
||||
model._asr_pipe = object()
|
||||
calls = []
|
||||
model.transcribe = lambda candidate: calls.append(candidate) or "Speech."
|
||||
audio = torch.full((1, 75 * 100 + 1), 0.1)
|
||||
|
||||
with pytest.raises(ValueError, match=r"\[clone_ref_too_long\].*at most 75 seconds"):
|
||||
model.create_voice_clone_prompt((audio, 100), ref_text=None)
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_auto_select_tiles_final_partial_window_without_gaps(monkeypatch):
|
||||
model = _model(reject_tokenization=False)
|
||||
model.sampling_rate = 100
|
||||
model._asr_pipe = object()
|
||||
lengths = []
|
||||
|
||||
def transcribe(candidate):
|
||||
lengths.append(candidate[0].size(-1))
|
||||
return "Speech."
|
||||
|
||||
model.transcribe = transcribe
|
||||
monkeypatch.setattr(
|
||||
"omnivoice.models.omnivoice.remove_silence_safe",
|
||||
lambda audio, *_args, **_kwargs: audio,
|
||||
)
|
||||
audio = torch.full((1, 61 * 100), 0.1)
|
||||
|
||||
model.create_voice_clone_prompt((audio, 100), ref_text=None)
|
||||
|
||||
assert lengths == [1500, 1500, 1500, 1500, 100]
|
||||
|
||||
|
||||
def test_auto_select_rejects_punctuation_only_transcripts_before_tokenizer():
|
||||
model = _model(reject_tokenization=True)
|
||||
model.sampling_rate = 100
|
||||
model._asr_pipe = object()
|
||||
model.transcribe = lambda _candidate: "...?!"
|
||||
audio = torch.full((1, 21 * 100), 0.1)
|
||||
|
||||
with pytest.raises(ValueError, match=r"\[clone_ref_no_speech\].*could not find spoken words"):
|
||||
model.create_voice_clone_prompt((audio, 100), ref_text=None)
|
||||
Reference in New Issue
Block a user