fix(generate): localize terminal language refusals across streaming paths
This commit is contained in:
@@ -1106,6 +1106,19 @@ def _profile_language_rejection_detail(exc: BaseException, language) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _language_rejection_payload(exc, language, *, from_profile):
|
||||
"""Stable, non-retryable error metadata for both response transports."""
|
||||
from core.public_errors import stream_failure
|
||||
failure = stream_failure("invalid_request")
|
||||
failure["terminal"] = True
|
||||
if from_profile:
|
||||
failure.update(
|
||||
code="profile_language_rejected", language=language,
|
||||
detail=_profile_language_rejection_detail(_root_language_error(exc), language),
|
||||
)
|
||||
return failure
|
||||
|
||||
|
||||
def _language_rejection_http_error(exc: BaseException, language, *, from_profile):
|
||||
"""The 400 a refused language deserves, wherever the refusal was raised.
|
||||
|
||||
@@ -1121,6 +1134,8 @@ def _language_rejection_http_error(exc: BaseException, language, *, from_profile
|
||||
_profile_language_rejection_detail(root, language)
|
||||
if from_profile else str(exc)
|
||||
)
|
||||
if from_profile:
|
||||
detail = {"code": "profile_language_rejected", "language": language, "message": detail}
|
||||
return HTTPException(status_code=400, detail=detail)
|
||||
|
||||
|
||||
@@ -1961,10 +1976,14 @@ async def generate_speech(
|
||||
# holding what is often its only slot until the lease lapses.
|
||||
render.cancel()
|
||||
raise
|
||||
except ValueError:
|
||||
except ValueError as e:
|
||||
logger.error("Remote generation request rejected")
|
||||
from core.public_errors import stream_failure
|
||||
yield _line({"type": "error", **stream_failure("invalid_request")})
|
||||
failure = (
|
||||
_language_rejection_payload(e, language, from_profile=language_from_profile)
|
||||
if _is_language_rejection(str(e)) else stream_failure("invalid_request")
|
||||
)
|
||||
yield _line({"type": "error", **failure})
|
||||
except gpu_gateway.ModelNotDownloaded as e:
|
||||
logger.warning("Remote model missing on %s", _target_label)
|
||||
from core.public_errors import stream_failure
|
||||
@@ -1980,11 +1999,14 @@ async def generate_speech(
|
||||
except gpu_gateway.RemoteJobFailed as e:
|
||||
logger.error("Remote generate failed on %s", _target_label)
|
||||
from core.public_errors import stream_failure
|
||||
if _is_language_rejection(str(e)):
|
||||
yield _line({"type": "error", **_language_rejection_payload(
|
||||
e, language, from_profile=language_from_profile,
|
||||
)})
|
||||
else:
|
||||
yield _line({
|
||||
"type": "error",
|
||||
**stream_failure("generation_failed"),
|
||||
"retryable": True,
|
||||
"target_label": e.worker_label or _target_label,
|
||||
"type": "error", **stream_failure("generation_failed"),
|
||||
"retryable": True, "target_label": e.worker_label or _target_label,
|
||||
"hint": e.hint,
|
||||
})
|
||||
except Exception as exc:
|
||||
@@ -2295,10 +2317,14 @@ async def generate_speech(
|
||||
failure = stream_failure("generation_timeout")
|
||||
failure["retry_after"] = 30
|
||||
yield _line({"type": "error", **failure})
|
||||
except ValueError:
|
||||
except ValueError as e:
|
||||
logger.error("Streaming generation request rejected")
|
||||
from core.public_errors import stream_failure
|
||||
yield _line({"type": "error", **stream_failure("invalid_request")})
|
||||
failure = (
|
||||
_language_rejection_payload(e, language, from_profile=language_from_profile)
|
||||
if _is_language_rejection(str(e)) else stream_failure("invalid_request")
|
||||
)
|
||||
yield _line({"type": "error", **failure})
|
||||
except Exception as exc:
|
||||
# A streaming request answers 200 and carries its failure as an
|
||||
# in-band error frame, so it never reaches the global 500
|
||||
|
||||
@@ -85,3 +85,7 @@ See also: [benchmarks.md](../benchmarks.md),
|
||||
|
||||
Consecutive chunks with the same native sample rate are resampled together to
|
||||
preserve filter context at chunk boundaries; rate changes start a new group.
|
||||
|
||||
Profile language refusals are terminal request errors on both local and remote
|
||||
rendering, including streaming. Electron and web show localized guidance that
|
||||
Auto inherits the profile language; neither silently retries the same refusal.
|
||||
|
||||
@@ -2529,6 +2529,7 @@
|
||||
"recorder_unsupported": "لا يستطيع هذا النظام ترميز صوت الميكروفون."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "يستخدم الوضع التلقائي لغة ملف الصوت ({{language}})، التي لا يدعمها هذا المحرك. غيّر لغة الملف أو اختر لغة مدعومة أو بدّل المحرك.",
|
||||
"generation_in_progress": "هناك عملية توليد قيد التشغيل بالفعل — انتظر حتى تنتهي قبل بدء عملية أخرى.",
|
||||
"error_prefix": "خطأ: {{message}}",
|
||||
"ignored_duplicate": "تم التجاهل (تم تعيين الفئة بالفعل): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"channels_stereo": "Stereo"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automatisch verwendet die Sprache des Stimmprofils ({{language}}), die diese Engine nicht unterstützt. Ändere die Profilsprache, wähle eine unterstützte Sprache oder wechsle die Engine.",
|
||||
"generation_in_progress": "Eine Generierung läuft bereits – warte, bis sie abgeschlossen ist, bevor du eine weitere startest.",
|
||||
"error_prefix": "Fehler: {{message}}",
|
||||
"ignored_duplicate": "Ignoriert (Kategorie bereits festgelegt): {{items}}",
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"recorder_unsupported": "This system cannot encode microphone audio."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto uses the voice profile’s language ({{language}}), which this engine does not support. Change the profile language, choose a supported language, or switch engines.",
|
||||
"enter_text": "Enter some text first.",
|
||||
"upload_or_select": "Upload a reference clip or pick a saved voice.",
|
||||
"trim_hint": "The clip is {{duration}}s — trim to ≤{{max}}s for best cloning.",
|
||||
|
||||
@@ -2523,6 +2523,7 @@
|
||||
"channels_mono": "Mono"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automático usa el idioma del perfil de voz ({{language}}), que este motor no admite. Cambia el idioma del perfil, elige uno compatible o cambia de motor.",
|
||||
"generation_in_progress": "Ya hay una generación en curso; espera a que termine antes de iniciar otra.",
|
||||
"ignored_duplicate": "Ignorado (categoría ya establecida): {{items}}",
|
||||
"ignored_conflict": "Se ignoró {{items}} — un dialecto chino y un acento inglés no se pueden combinar.",
|
||||
|
||||
@@ -2523,6 +2523,7 @@
|
||||
"microphone_number": "Microphone {{number}}"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto utilise la langue du profil vocal ({{language}}), non prise en charge par ce moteur. Modifiez la langue du profil, choisissez une langue compatible ou changez de moteur.",
|
||||
"generation_in_progress": "Une génération est déjà en cours — attendez sa fin avant d’en lancer une autre.",
|
||||
"error_prefix": "Erreur : {{message}}",
|
||||
"ignored_duplicate": "Ignoré (catégorie déjà définie) : {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"recorder_unsupported": "यह सिस्टम माइक्रोफ़ोन ऑडियो एन्कोड नहीं कर सकता।"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "ऑटो वॉइस प्रोफ़ाइल की भाषा ({{language}}) का उपयोग करता है, जिसे यह इंजन समर्थन नहीं करता। प्रोफ़ाइल की भाषा बदलें, समर्थित भाषा चुनें या इंजन बदलें।",
|
||||
"generation_in_progress": "एक जनरेशन पहले से चल रहा है — दूसरा शुरू करने से पहले उसके पूरा होने की प्रतीक्षा करें।",
|
||||
"error_prefix": "त्रुटि: {{message}}",
|
||||
"ignored_duplicate": "अनदेखा (श्रेणी पहले से ही सेट): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"channels_stereo": "Stereo"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Otomatis menggunakan bahasa profil suara ({{language}}), yang tidak didukung mesin ini. Ubah bahasa profil, pilih bahasa yang didukung, atau ganti mesin.",
|
||||
"generation_in_progress": "Pembuatan sedang berjalan — tunggu hingga selesai sebelum memulai yang lain.",
|
||||
"error_prefix": "Kesalahan: {{message}}",
|
||||
"ignored_duplicate": "Diabaikan (kategori sudah ditetapkan): {{items}}",
|
||||
|
||||
@@ -2523,6 +2523,7 @@
|
||||
"channels_stereo": "Stereo"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto usa la lingua del profilo vocale ({{language}}), non supportata da questo motore. Modifica la lingua del profilo, scegli una lingua supportata o cambia motore.",
|
||||
"generation_in_progress": "È già in corso una generazione: attendi che termini prima di avviarne un’altra.",
|
||||
"error_prefix": "Errore: {{message}}",
|
||||
"ignored_duplicate": "Ignorato (categoria già impostata): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"recorder_unsupported": "このシステムではマイク音声をエンコードできません。"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "自動では音声プロファイルの言語({{language}})が使われますが、このエンジンは対応していません。プロファイルの言語を変更するか、対応言語または別のエンジンを選択してください。",
|
||||
"generation_in_progress": "生成がすでに実行中です。完了してから次の生成を開始してください。",
|
||||
"error_prefix": "エラー: {{message}}",
|
||||
"ignored_duplicate": "無視 (カテゴリはすでに設定されています): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"recorder_unsupported": "이 시스템에서는 마이크 오디오를 인코딩할 수 없습니다."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "자동은 이 엔진이 지원하지 않는 음성 프로필의 언어({{language}})를 사용합니다. 프로필 언어를 변경하거나 지원되는 언어 또는 다른 엔진을 선택하세요.",
|
||||
"generation_in_progress": "이미 생성 작업이 실행 중입니다. 완료된 후 새 작업을 시작하세요.",
|
||||
"error_prefix": "오류: {{message}}",
|
||||
"ignored_duplicate": "무시됨(카테고리가 이미 설정됨): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"channels_stereo": "Stereo"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automatisch gebruikt de taal van het stemprofiel ({{language}}), die deze engine niet ondersteunt. Wijzig de profieltaal, kies een ondersteunde taal of wissel van engine.",
|
||||
"generation_in_progress": "Er wordt al audio gegenereerd — wacht tot dit klaar is voordat je opnieuw begint.",
|
||||
"error_prefix": "Fout: {{message}}",
|
||||
"ignored_duplicate": "Genegeerd (categorie al ingesteld): {{items}}",
|
||||
|
||||
@@ -2525,6 +2525,7 @@
|
||||
"channels_stereo": "Stereo"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Tryb automatyczny używa języka profilu głosu ({{language}}), którego ten silnik nie obsługuje. Zmień język profilu, wybierz obsługiwany język lub zmień silnik.",
|
||||
"generation_in_progress": "Generowanie już trwa — poczekaj na jego zakończenie przed rozpoczęciem kolejnego.",
|
||||
"error_prefix": "Błąd: {{message}}",
|
||||
"ignored_duplicate": "Ignorowane (kategoria już ustawiona): {{items}}",
|
||||
|
||||
@@ -2523,6 +2523,7 @@
|
||||
"channels_mono": "Mono"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automático usa o idioma do perfil de voz ({{language}}), não suportado por este motor. Altere o idioma do perfil, escolha um idioma compatível ou mude de motor.",
|
||||
"generation_in_progress": "Já existe uma geração em andamento — aguarde a conclusão antes de iniciar outra.",
|
||||
"error_prefix": "Erro: {{message}}",
|
||||
"ignored_duplicate": "Ignorado (categoria já definida): {{items}}",
|
||||
|
||||
@@ -2525,6 +2525,7 @@
|
||||
"recorder_unsupported": "Эта система не может кодировать звук с микрофона."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Авто использует язык голосового профиля ({{language}}), который этот движок не поддерживает. Измените язык профиля, выберите поддерживаемый язык или другой движок.",
|
||||
"generation_in_progress": "Генерация уже выполняется — дождитесь её завершения, прежде чем запускать следующую.",
|
||||
"error_prefix": "Ошибка: {{message}}",
|
||||
"ignored_duplicate": "Игнорируется (категория уже установлена): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"channels_stereo": "Stereo"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto använder röstprofilens språk ({{language}}), som denna motor inte stöder. Ändra profilens språk, välj ett språk som stöds eller byt motor.",
|
||||
"generation_in_progress": "En generering pågår redan – vänta tills den är klar innan du startar en ny.",
|
||||
"error_prefix": "Fel: {{message}}",
|
||||
"ignored_duplicate": "Ignorerad (kategori redan inställd): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"recorder_unsupported": "ระบบนี้ไม่สามารถเข้ารหัสเสียงจากไมโครโฟนได้"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "อัตโนมัติใช้ภาษาของโปรไฟล์เสียง ({{language}}) ซึ่งเอนจินนี้ไม่รองรับ เปลี่ยนภาษาโปรไฟล์ เลือกภาษาที่รองรับ หรือเปลี่ยนเอนจิน",
|
||||
"generation_in_progress": "มีการสร้างเสียงกำลังทำงานอยู่ — รอให้เสร็จก่อนเริ่มงานใหม่",
|
||||
"error_prefix": "ข้อผิดพลาด: {{message}}",
|
||||
"ignored_duplicate": "ละเว้น (หมวดหมู่ที่ตั้งไว้แล้ว): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"channels_stereo": "Stereo"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Otomatik, bu motorun desteklemediği ses profili dilini ({{language}}) kullanır. Profil dilini değiştirin, desteklenen bir dil seçin veya motoru değiştirin.",
|
||||
"generation_in_progress": "Bir oluşturma işlemi zaten çalışıyor — yenisini başlatmadan önce bitmesini bekleyin.",
|
||||
"error_prefix": "Hata: {{message}}",
|
||||
"ignored_duplicate": "Yoksayıldı (kategori zaten ayarlandı): {{items}}",
|
||||
|
||||
@@ -2525,6 +2525,7 @@
|
||||
"recorder_unsupported": "Ця система не може кодувати звук із мікрофона."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Авто використовує мову голосового профілю ({{language}}), яку цей рушій не підтримує. Змініть мову профілю, виберіть підтримувану мову або інший рушій.",
|
||||
"generation_in_progress": "Генерація вже виконується — дочекайтеся її завершення, перш ніж запускати наступну.",
|
||||
"error_prefix": "Помилка: {{message}}",
|
||||
"ignored_duplicate": "Проігноровано (категорію вже встановлено): {{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"recorder_unsupported": "Hệ thống này không thể mã hóa âm thanh từ micrô."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Tự động sử dụng ngôn ngữ của hồ sơ giọng nói ({{language}}), nhưng bộ máy này không hỗ trợ. Đổi ngôn ngữ hồ sơ, chọn ngôn ngữ được hỗ trợ hoặc đổi bộ máy.",
|
||||
"generation_in_progress": "Một tác vụ tạo âm thanh đang chạy — hãy đợi hoàn tất trước khi bắt đầu tác vụ khác.",
|
||||
"error_prefix": "Lỗi: {{message}}",
|
||||
"ignored_duplicate": "Đã bỏ qua (danh mục đã được đặt): {{items}}",
|
||||
|
||||
@@ -2525,6 +2525,7 @@
|
||||
"recorder_unsupported": "此系统无法编码麦克风音频。"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "自动模式使用声音档案的语言({{language}}),但此引擎不支持该语言。请修改档案语言、选择支持的语言或更换引擎。",
|
||||
"generation_in_progress": "已有生成任务正在运行,请等待其完成后再启动新任务。",
|
||||
"error_prefix": "错误:{{message}}",
|
||||
"ignored_duplicate": "忽略(类别已设置):{{items}}",
|
||||
|
||||
@@ -2521,6 +2521,7 @@
|
||||
"recorder_unsupported": "此系統無法編碼麥克風音訊。"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "自動模式使用聲音設定檔的語言({{language}}),但此引擎不支援該語言。請修改設定檔語言、選擇支援的語言或更換引擎。",
|
||||
"generation_in_progress": "已有產生工作正在執行,請等待完成後再啟動新工作。",
|
||||
"error_prefix": "錯誤:{{message}}",
|
||||
"ignored_duplicate": "忽略(類別已設定):{{items}}",
|
||||
|
||||
@@ -169,3 +169,23 @@ it('uses a structured error message without losing recovery metadata', async ()
|
||||
expect(error.message).toBe(detail.message);
|
||||
expect(error.payload?.detail).toEqual(detail);
|
||||
});
|
||||
|
||||
it('localizes structured profile language failures', async () => {
|
||||
const translate = vi.spyOn(i18next, 't').mockReturnValue('Localized profile guidance');
|
||||
try {
|
||||
const detail = {
|
||||
code: 'profile_language_rejected',
|
||||
language: 'Persian',
|
||||
message: 'English fallback',
|
||||
};
|
||||
const error = await errorFromResponse(
|
||||
new Response(JSON.stringify({ detail }), { status: 400 }),
|
||||
);
|
||||
expect(error.detail).toBe('Localized profile guidance');
|
||||
expect(translate).toHaveBeenCalledWith('tts_errors.profile_language_rejected', {
|
||||
language: 'Persian',
|
||||
});
|
||||
} finally {
|
||||
translate.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { languageRejectionMessage } from '../../../../../../frontend/src/utils/languageRejection.ts';
|
||||
/**
|
||||
* Same-origin API client. The renderer never talks to 127.0.0.1:<port>
|
||||
* directly (CORS); `/api/*` is proxied by the dev server / the app:// protocol
|
||||
@@ -44,6 +45,8 @@ export function describeError(err: unknown): string {
|
||||
}
|
||||
|
||||
function detailToString(detail: unknown): string {
|
||||
const localized = languageRejectionMessage(detail, tr);
|
||||
if (localized) return localized;
|
||||
if (
|
||||
detail &&
|
||||
typeof detail === 'object' &&
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import i18n from 'i18next';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
CLONE_MAX_SECONDS,
|
||||
REF_HARD_MAX_SECONDS,
|
||||
generateClone,
|
||||
generateCloneStreaming,
|
||||
shouldFallbackToClassic,
|
||||
parseGenerateHeaders,
|
||||
sanitizeInstruct,
|
||||
toGenerateForm,
|
||||
@@ -277,3 +280,31 @@ describe('generateClone', () => {
|
||||
await assertion;
|
||||
});
|
||||
});
|
||||
|
||||
it('localizes streamed profile language refusals and prevents classic fallback', async () => {
|
||||
const translate = vi.spyOn(i18n, 't').mockReturnValue('Localized profile guidance');
|
||||
try {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(
|
||||
async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
type: 'error',
|
||||
code: 'profile_language_rejected',
|
||||
language: 'Persian',
|
||||
detail: 'English fallback',
|
||||
retryable: false,
|
||||
terminal: true,
|
||||
}) + '\n',
|
||||
{ headers: { 'content-type': 'application/x-ndjson' } },
|
||||
),
|
||||
),
|
||||
);
|
||||
const error = await generateCloneStreaming(BASE_INPUT).catch((e) => e);
|
||||
expect(error.message).toBe('Localized profile guidance');
|
||||
expect(shouldFallbackToClassic(error)).toBe(false);
|
||||
} finally {
|
||||
translate.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import i18next from 'i18next';
|
||||
import { languageRejectionMessage } from '../../../../../../frontend/src/utils/languageRejection.ts';
|
||||
import { ApiError, apiFetch, isAbortError } from './client';
|
||||
import type { CloneGenerateInput, GenerateResult } from './types';
|
||||
import { beginAppActivity } from '@/lib/app-activity';
|
||||
@@ -255,6 +257,9 @@ interface StreamEvent {
|
||||
count?: number;
|
||||
text?: string[];
|
||||
detail?: string;
|
||||
code?: string;
|
||||
language?: string;
|
||||
terminal?: boolean;
|
||||
retryable?: boolean;
|
||||
percent?: number;
|
||||
}
|
||||
@@ -300,12 +305,15 @@ export async function generateCloneStreaming(
|
||||
} else if (event.type === 'done') {
|
||||
meta = event;
|
||||
} else if (event.type === 'error') {
|
||||
const message = event.detail || 'TTS stream reported an error';
|
||||
const terminal = [
|
||||
'[clone_ref_unusable]',
|
||||
'[clone_ref_too_long]',
|
||||
'[clone_ref_no_speech]',
|
||||
].some((marker) => message.includes(marker));
|
||||
const message =
|
||||
languageRejectionMessage(event, i18next.t) ||
|
||||
event.detail ||
|
||||
'TTS stream reported an error';
|
||||
const terminal =
|
||||
event.terminal === true ||
|
||||
['[clone_ref_unusable]', '[clone_ref_too_long]', '[clone_ref_no_speech]'].some((marker) =>
|
||||
message.includes(marker),
|
||||
);
|
||||
throw new StreamingPreviewError(message, { retryable: event.retryable, terminal });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"src/preload/index.d.ts",
|
||||
"src/shared/**/*",
|
||||
"src/renderer/env.d.ts",
|
||||
"../frontend/src/utils/languageRejection.ts",
|
||||
"../frontend/src/utils/coalescedJsonStorage.ts",
|
||||
"../frontend/src/utils/indexedDbLongformStore.ts",
|
||||
"../frontend/src/utils/cookieExport.ts",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { languageRejectionMessage } from '../utils/languageRejection.ts';
|
||||
import i18n from 'i18next';
|
||||
import { abortableDelay } from '../utils/abortableDelay.ts';
|
||||
// Backend base URL.
|
||||
@@ -593,7 +594,8 @@ export async function apiFetch(path: string, opts: ApiFetchOptions = {}): Promis
|
||||
// human-readable `message` — use it for the Error message instead of
|
||||
// letting the object stringify to "[object Object]".
|
||||
const msg =
|
||||
detail &&
|
||||
languageRejectionMessage(detail, i18n.t) ||
|
||||
(detail &&
|
||||
typeof detail === 'object' &&
|
||||
'code' in detail &&
|
||||
detail.code === 'argos_runtime_unavailable'
|
||||
@@ -602,7 +604,7 @@ export async function apiFetch(path: string, opts: ApiFetchOptions = {}): Promis
|
||||
})
|
||||
: typeof detail === 'string'
|
||||
? detail
|
||||
: ((detail as { message?: string })?.message ?? JSON.stringify(detail));
|
||||
: ((detail as { message?: string })?.message ?? JSON.stringify(detail)));
|
||||
// The backend names the exception type in `error_class` on its 500s.
|
||||
// Lifting it here is what lets the bug report say which failure it was.
|
||||
const errorClass =
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "انتهت صلاحية جلسة الدبلجة هذه أو تم تنظيفها — أعد تحميل الفيديو لبدء مقطع جديد."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "يستخدم الوضع التلقائي لغة ملف الصوت ({{language}})، التي لا يدعمها هذا المحرك. غيّر لغة الملف أو اختر لغة مدعومة أو بدّل المحرك.",
|
||||
"enter_text": "الرجاء إدخال النص",
|
||||
"upload_or_select": "قم بتحميل ملف صوتي أو حدد ملفًا صوتيًا",
|
||||
"trim_hint": "الصوت هو {{duration}}s — قم بقصه إلى ≥{{max}}s للحصول على أفضل استنساخ",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Diese Dub-Sitzung ist abgelaufen oder wurde bereinigt. Laden Sie Ihr Video erneut hoch, um eine neue zu starten."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automatisch verwendet die Sprache des Stimmprofils ({{language}}), die diese Engine nicht unterstützt. Ändere die Profilsprache, wähle eine unterstützte Sprache oder wechsle die Engine.",
|
||||
"enter_text": "Bitte geben Sie Text ein",
|
||||
"upload_or_select": "Laden Sie ein Audio hoch oder wählen Sie ein Sprachprofil aus",
|
||||
"trim_hint": "Audio ist {{duration}}s – für bestes Klonen auf ≤{{max}}s kürzen",
|
||||
|
||||
@@ -2890,6 +2890,7 @@
|
||||
"upgrading_preview": "Upgrading {{count}} preview-quality segment(s) to full quality…"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto uses the voice profile’s language ({{language}}), which this engine does not support. Change the profile language, choose a supported language, or switch engines.",
|
||||
"enter_text": "Please enter text",
|
||||
"upload_or_select": "Upload an audio or select a voice profile",
|
||||
"trim_hint": "Audio is {{duration}}s — trim to ≤{{max}}s for best cloning",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Esta sesión de doblaje expiró o se limpió: vuelve a subir tu video para comenzar uno nuevo."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automático usa el idioma del perfil de voz ({{language}}), que este motor no admite. Cambia el idioma del perfil, elige uno compatible o cambia de motor.",
|
||||
"enter_text": "Por favor ingresa texto",
|
||||
"upload_or_select": "Sube un audio o selecciona un perfil de voz",
|
||||
"trim_hint": "El audio es {{duration}}s; recórtelo a ≤{{max}}s para una mejor clonación.",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Cette session de doublage a expiré ou a été nettoyée : téléchargez à nouveau votre vidéo pour en démarrer une nouvelle."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto utilise la langue du profil vocal ({{language}}), non prise en charge par ce moteur. Modifiez la langue du profil, choisissez une langue compatible ou changez de moteur.",
|
||||
"enter_text": "Veuillez saisir du texte",
|
||||
"upload_or_select": "Téléchargez un audio ou sélectionnez un profil vocal",
|
||||
"trim_hint": "L'audio est de {{duration}}s — coupez à ≤{{max}}s pour un meilleur clonage",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "यह डब सत्र समाप्त हो गया है या साफ़ कर दिया गया है - नया शुरू करने के लिए अपना वीडियो पुनः अपलोड करें।"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "ऑटो वॉइस प्रोफ़ाइल की भाषा ({{language}}) का उपयोग करता है, जिसे यह इंजन समर्थन नहीं करता। प्रोफ़ाइल की भाषा बदलें, समर्थित भाषा चुनें या इंजन बदलें।",
|
||||
"enter_text": "कृपया पाठ दर्ज करें",
|
||||
"upload_or_select": "एक ऑडियो अपलोड करें या एक वॉयस प्रोफ़ाइल चुनें",
|
||||
"trim_hint": "ऑडियो {{duration}}s है - सर्वोत्तम क्लोनिंग के लिए ≤{{max}}s तक ट्रिम करें",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Sesi sulih suara ini telah habis masa berlakunya atau telah dibersihkan — unggah ulang video Anda untuk memulai yang baru."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Otomatis menggunakan bahasa profil suara ({{language}}), yang tidak didukung mesin ini. Ubah bahasa profil, pilih bahasa yang didukung, atau ganti mesin.",
|
||||
"enter_text": "Silakan masukkan teks",
|
||||
"upload_or_select": "Unggah audio atau pilih profil suara",
|
||||
"trim_hint": "Audio adalah {{duration}}s — potong ke ≤{{max}}s untuk kloning terbaik",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Questa sessione di doppiaggio è scaduta o è stata ripulita: ricarica il tuo video per avviarne uno nuovo."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto usa la lingua del profilo vocale ({{language}}), non supportata da questo motore. Modifica la lingua del profilo, scegli una lingua supportata o cambia motore.",
|
||||
"enter_text": "Inserisci il testo",
|
||||
"upload_or_select": "Carica un audio o seleziona un profilo vocale",
|
||||
"trim_hint": "L'audio è {{duration}}s: taglia a ≤{{max}}s per una migliore clonazione",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "このダビング セッションは期限切れかクリーンアップされました。新しいビデオを開始するにはビデオを再アップロードしてください。"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "自動では音声プロファイルの言語({{language}})が使われますが、このエンジンは対応していません。プロファイルの言語を変更するか、対応言語または別のエンジンを選択してください。",
|
||||
"enter_text": "テキストを入力してください",
|
||||
"upload_or_select": "音声をアップロードするか、音声プロファイルを選択してください",
|
||||
"trim_hint": "オーディオは {{duration}}s — 最適なクローン作成のために ≤{{max}}s にトリミングします",
|
||||
|
||||
@@ -2600,6 +2600,7 @@
|
||||
"session_expired": "이 더빙 세션이 만료되었거나 정리되었습니다. 새 동영상을 시작하려면 동영상을 다시 업로드하세요."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "자동은 이 엔진이 지원하지 않는 음성 프로필의 언어({{language}})를 사용합니다. 프로필 언어를 변경하거나 지원되는 언어 또는 다른 엔진을 선택하세요.",
|
||||
"enter_text": "텍스트를 입력해주세요",
|
||||
"upload_or_select": "오디오 업로드 또는 음성 프로필 선택",
|
||||
"trim_hint": "오디오는 {{duration}}s입니다. 최상의 복제를 위해 ≤{{max}}s로 조정하세요.",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Deze kopieersessie is verlopen of opgeschoond. Upload je video opnieuw om een nieuwe te starten."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automatisch gebruikt de taal van het stemprofiel ({{language}}), die deze engine niet ondersteunt. Wijzig de profieltaal, kies een ondersteunde taal of wissel van engine.",
|
||||
"enter_text": "Voer tekst in",
|
||||
"upload_or_select": "Upload een audio of selecteer een stemprofiel",
|
||||
"trim_hint": "Audio is {{duration}}s — trim tot ≤{{max}}s voor de beste kloonfunctie",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Ta sesja dubowania wygasła lub została wyczyszczona — prześlij film ponownie, aby rozpocząć nową."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Tryb automatyczny używa języka profilu głosu ({{language}}), którego ten silnik nie obsługuje. Zmień język profilu, wybierz obsługiwany język lub zmień silnik.",
|
||||
"enter_text": "Proszę wpisać tekst",
|
||||
"upload_or_select": "Prześlij plik audio lub wybierz profil głosowy",
|
||||
"trim_hint": "Dźwięk wynosi {{duration}}s — przytnij do ≤{{max}}s, aby uzyskać najlepsze klonowanie",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Esta sessão de dublagem expirou ou foi limpa. Reenvie seu vídeo para iniciar uma nova."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Automático usa o idioma do perfil de voz ({{language}}), não suportado por este motor. Altere o idioma do perfil, escolha um idioma compatível ou mude de motor.",
|
||||
"enter_text": "Por favor insira o texto",
|
||||
"upload_or_select": "Carregue um áudio ou selecione um perfil de voz",
|
||||
"trim_hint": "O áudio é {{duration}}s – ajuste para ≤{{max}}s para melhor clonagem",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Срок действия сеанса дублирования истек или он был удален — загрузите видео повторно, чтобы начать новое."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Авто использует язык голосового профиля ({{language}}), который этот движок не поддерживает. Измените язык профиля, выберите поддерживаемый язык или другой движок.",
|
||||
"enter_text": "Пожалуйста, введите текст",
|
||||
"upload_or_select": "Загрузите аудио или выберите голосовой профиль",
|
||||
"trim_hint": "Звук составляет {{duration}}s — обрезайте до ≤{{max}}s для лучшего клонирования",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Den här dubbsessionen löpte ut eller rensades upp – ladda upp din video igen för att starta en ny."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Auto använder röstprofilens språk ({{language}}), som denna motor inte stöder. Ändra profilens språk, välj ett språk som stöds eller byt motor.",
|
||||
"enter_text": "Vänligen ange text",
|
||||
"upload_or_select": "Ladda upp ett ljud eller välj en röstprofil",
|
||||
"trim_hint": "Ljudet är {{duration}}s — trimma till ≤{{max}}s för bästa kloning",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "เซสชั่นพากย์นี้หมดอายุหรือถูกล้างออกไปแล้ว — อัปโหลดวิดีโอของคุณอีกครั้งเพื่อเริ่มวิดีโอใหม่"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "อัตโนมัติใช้ภาษาของโปรไฟล์เสียง ({{language}}) ซึ่งเอนจินนี้ไม่รองรับ เปลี่ยนภาษาโปรไฟล์ เลือกภาษาที่รองรับ หรือเปลี่ยนเอนจิน",
|
||||
"enter_text": "กรุณากรอกข้อความ",
|
||||
"upload_or_select": "อัปโหลดเสียงหรือเลือกโปรไฟล์เสียง",
|
||||
"trim_hint": "เสียงคือ {{duration}}s — ตัดเป็น ≤{{max}}s เพื่อการโคลนที่ดีที่สุด",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Bu dub oturumunun süresi doldu veya temizlendi. Yeni bir video başlatmak için videonuzu yeniden yükleyin."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Otomatik, bu motorun desteklemediği ses profili dilini ({{language}}) kullanır. Profil dilini değiştirin, desteklenen bir dil seçin veya motoru değiştirin.",
|
||||
"enter_text": "Lütfen metni girin",
|
||||
"upload_or_select": "Bir ses yükleyin veya bir ses profili seçin",
|
||||
"trim_hint": "Ses {{duration}}s — en iyi klonlama için ≤{{max}}s'ye kırpın",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Цей сеанс дубляжу закінчився або його було очищено — повторно завантажте відео, щоб почати нове."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Авто використовує мову голосового профілю ({{language}}), яку цей рушій не підтримує. Змініть мову профілю, виберіть підтримувану мову або інший рушій.",
|
||||
"enter_text": "Будь ласка, введіть текст",
|
||||
"upload_or_select": "Завантажте аудіо або виберіть голосовий профіль",
|
||||
"trim_hint": "Аудіо {{duration}}s — обріжте до ≤{{max}}s для найкращого клонування",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "Phiên lồng tiếng này đã hết hạn hoặc đã bị xóa - hãy tải lại video của bạn lên để bắt đầu một video mới."
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "Tự động sử dụng ngôn ngữ của hồ sơ giọng nói ({{language}}), nhưng bộ máy này không hỗ trợ. Đổi ngôn ngữ hồ sơ, chọn ngôn ngữ được hỗ trợ hoặc đổi bộ máy.",
|
||||
"enter_text": "Vui lòng nhập văn bản",
|
||||
"upload_or_select": "Tải lên âm thanh hoặc chọn cấu hình giọng nói",
|
||||
"trim_hint": "Âm thanh là {{duration}}s — cắt thành ≤{{max}}s để sao chép tốt nhất",
|
||||
|
||||
@@ -2605,6 +2605,7 @@
|
||||
"session_expired": "此配音会话已过期或已清理——请重新上传视频以开始新的会话。"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "自动模式使用声音档案的语言({{language}}),但此引擎不支持该语言。请修改档案语言、选择支持的语言或更换引擎。",
|
||||
"enter_text": "请输入文字",
|
||||
"upload_or_select": "上传音频或选择语音配置文件",
|
||||
"trim_hint": "音频为 {{duration}}s — 修剪至 ≤{{max}}s 以获得最佳克隆效果",
|
||||
|
||||
@@ -2219,6 +2219,7 @@
|
||||
"session_expired": "此配音會話已過期或已清理 - 重新上傳您的影片以開始新的影片。"
|
||||
},
|
||||
"tts_errors": {
|
||||
"profile_language_rejected": "自動模式使用聲音設定檔的語言({{language}}),但此引擎不支援該語言。請修改設定檔語言、選擇支援的語言或更換引擎。",
|
||||
"enter_text": "請輸入文字",
|
||||
"upload_or_select": "上傳音訊或選擇語音設定文件",
|
||||
"trim_hint": "音訊為 {{duration}}s — 修剪至 ≤{{max}}s 以獲得最佳克隆效果",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import i18n from 'i18next';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
|
||||
// Streaming TTS preview (feat: streaming-tts-preview): the NDJSON client must
|
||||
@@ -380,6 +381,32 @@ describe('streamGenerateSpeech', () => {
|
||||
expect(plain.retryable).toBe(false);
|
||||
});
|
||||
|
||||
it('localizes a terminal profile language refusal without retrying', async () => {
|
||||
const translate = vi.spyOn(i18n, 't').mockReturnValue('Localized profile guidance');
|
||||
try {
|
||||
apiFetch.mockResolvedValue(
|
||||
ndjsonResponse([
|
||||
{
|
||||
type: 'error',
|
||||
code: 'profile_language_rejected',
|
||||
language: 'Persian',
|
||||
detail: 'English fallback',
|
||||
retryable: false,
|
||||
terminal: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const error = await streamGenerateSpeech(new FormData(), {}).catch((e) => e);
|
||||
expect(error.message).toBe('Localized profile guidance');
|
||||
expect(shouldFallbackToClassic(error)).toBe(false);
|
||||
expect(translate).toHaveBeenCalledWith('tts_errors.profile_language_rejected', {
|
||||
language: 'Persian',
|
||||
});
|
||||
} finally {
|
||||
translate.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('marks actionable clone-reference errors terminal to prevent a classic retry', async () => {
|
||||
apiFetch.mockResolvedValue(
|
||||
ndjsonResponse([
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/** Localize only the known structured failure, never a server-supplied key. */
|
||||
export function languageRejectionMessage(
|
||||
value: unknown,
|
||||
translate: (key: string, options: { language: string }) => string,
|
||||
): string | undefined {
|
||||
if (!value || typeof value !== 'object') return undefined;
|
||||
const failure = value as { code?: unknown; language?: unknown };
|
||||
if (failure.code !== 'profile_language_rejected' || typeof failure.language !== 'string')
|
||||
return undefined;
|
||||
return translate('tts_errors.profile_language_rejected', { language: failure.language });
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import i18n from 'i18next';
|
||||
import { languageRejectionMessage } from './languageRejection.ts';
|
||||
/**
|
||||
* streamingTts.js — streaming TTS preview (feat: streaming-tts-preview).
|
||||
*
|
||||
@@ -399,12 +401,13 @@ async function _streamGenerateSpeech(
|
||||
} else if (ev.type === 'done') {
|
||||
meta = ev;
|
||||
} else if (ev.type === '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));
|
||||
const detail =
|
||||
languageRejectionMessage(ev, i18n.t) || ev.detail || 'TTS stream reported an error';
|
||||
const terminal =
|
||||
ev.terminal === true ||
|
||||
['[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,
|
||||
|
||||
@@ -156,6 +156,10 @@ def test_a_profile_supplied_language_names_the_profile_not_the_picker(
|
||||
|
||||
assert res.status_code == 400, res.text
|
||||
detail = res.json()["detail"]
|
||||
if isinstance(detail, dict):
|
||||
assert detail["code"] == "profile_language_rejected"
|
||||
assert detail["language"] == "Persian"
|
||||
detail = detail["message"]
|
||||
# Says where the language actually came from …
|
||||
assert "voice profile" in detail.lower()
|
||||
assert "Persian" in detail
|
||||
@@ -197,6 +201,10 @@ def test_an_explicitly_requested_language_is_not_blamed_on_the_profile(
|
||||
|
||||
assert res.status_code == 400, res.text
|
||||
detail = res.json()["detail"]
|
||||
if isinstance(detail, dict):
|
||||
assert detail["code"] == "profile_language_rejected"
|
||||
assert detail["language"] == "Persian"
|
||||
detail = detail["message"]
|
||||
assert "voice profile" not in detail.lower()
|
||||
assert "does not override" not in detail
|
||||
assert "doesn't support language='Persian'" in detail
|
||||
@@ -236,6 +244,10 @@ def test_a_non_language_failure_under_a_profile_is_untouched(
|
||||
|
||||
assert res.status_code == 400, res.text
|
||||
detail = res.json()["detail"]
|
||||
if isinstance(detail, dict):
|
||||
assert detail["code"] == "profile_language_rejected"
|
||||
assert detail["language"] == "Persian"
|
||||
detail = detail["message"]
|
||||
assert "shorter than 3 seconds" in detail
|
||||
assert "voice profile" not in detail.lower()
|
||||
|
||||
@@ -300,6 +312,10 @@ def test_a_generic_rejection_is_quoted_once_not_twice(client, monkeypatch, persi
|
||||
|
||||
assert res.status_code == 400, res.text
|
||||
detail = res.json()["detail"]
|
||||
if isinstance(detail, dict):
|
||||
assert detail["code"] == "profile_language_rejected"
|
||||
assert detail["language"] == "Persian"
|
||||
detail = detail["message"]
|
||||
assert "voice profile" in detail.lower()
|
||||
assert detail.count("Engine's own message:") == 1
|
||||
assert detail.count("switch engine in Model Catalogue") == 1
|
||||
@@ -346,6 +362,10 @@ def test_a_remote_language_refusal_is_a_400_not_a_retryable_503(
|
||||
assert res.status_code == 400, f"{res.status_code}: {res.text}"
|
||||
assert res.headers.get("X-OmniVoice-Retryable") != "true"
|
||||
detail = res.json()["detail"]
|
||||
if isinstance(detail, dict):
|
||||
assert detail["code"] == "profile_language_rejected"
|
||||
assert detail["language"] == "Persian"
|
||||
detail = detail["message"]
|
||||
assert "voice profile" in detail.lower()
|
||||
assert "Run it on this machine instead" not in detail
|
||||
|
||||
@@ -417,3 +437,22 @@ def test_an_explicit_auto_is_still_filled_from_the_profile():
|
||||
out = _gen_mod()._resolve_profile_conditioning(_row(language="Persian"), language="Auto")
|
||||
assert out["language"] == "Persian"
|
||||
assert out["language_from_profile"] is True
|
||||
|
||||
@pytest.mark.parametrize('remote', [False, True])
|
||||
def test_streamed_profile_language_refusal_is_terminal(client, monkeypatch, persian_profile, remote):
|
||||
import json
|
||||
from services import gpu_gateway
|
||||
fake = _make_refusing_engine('fake-stream-language-2156')
|
||||
monkeypatch.setitem(_tts_mod()._REGISTRY, fake.id, fake)
|
||||
if remote:
|
||||
_route_remotely(monkeypatch, gpu_gateway.RemoteJobFailed(KOKORO_REFUSAL, worker_label='gpu-box'))
|
||||
response = client.post('/generate', data={
|
||||
'text': 'Salam', 'profile_id': persian_profile, 'engine': fake.id, 'stream': 'true',
|
||||
})
|
||||
assert response.status_code == 200, response.text
|
||||
frames = [json.loads(line) for line in response.text.splitlines() if line]
|
||||
error = next(frame for frame in frames if frame['type'] == 'error')
|
||||
assert error['code'] == 'profile_language_rejected'
|
||||
assert error['language'] == 'Persian'
|
||||
assert error['retryable'] is False
|
||||
assert error['terminal'] is True
|
||||
|
||||
Reference in New Issue
Block a user