mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-10-19 09:13:16 +00:00
fix: error hints, Italian in the language menu and dev fallbacks (docs review) (#1457)
* fix: error hints, Italian menu entry and dev fallbacks found by the docs review - error_classifier: read timeouts get their own message naming ESPERANTO_LLM_TIMEOUT; connect failures keep "Could not connect". Auth hint points to Manage -> Models; provision's "Please go to Manage" message is recognized again. - Podcast voice hint and the empty-episodes text point to real screens. - LanguageToggle renders the registered languages list, so it-IT shows up (and future locales can't be missed); it-IT date locale added. The per-language common.* labels it used are removed. - make start-all uses the shipped compose file. - Legacy SURREAL_ADDRESS/SURREAL_PORT fallback builds ws://host:port/rpc. - .env.example uses the real OPEN_NOTEBOOK_CHUNK_* names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review (IPv6 legacy address, connection-timed-out wording, stronger tests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: assert exception class for OS connection timeouts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a66c6ae412
commit
37217b01de
26 changed files with 189 additions and 290 deletions
|
|
@ -62,9 +62,9 @@ SURREAL_DATABASE=open_notebook
|
|||
# OMLX_API_BASE=http://localhost:11435/v1
|
||||
# OMLX_API_KEY= # optional; only if oMLX was started with --api-key
|
||||
|
||||
# Content processing
|
||||
# CHUNK_SIZE=1500
|
||||
# CHUNK_OVERLAP=150
|
||||
# Content processing: embedding chunk size, in tokens
|
||||
# OPEN_NOTEBOOK_CHUNK_SIZE=400
|
||||
# OPEN_NOTEBOOK_CHUNK_OVERLAP=60 # default: 15% of the chunk size
|
||||
|
||||
# Remote Crawl4AI server for the crawl4ai URL engine (no local install needed).
|
||||
# Crawl4AI Docker >= 0.9.0 requires a bearer token for external connections.
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Fixed
|
||||
- **Docs no longer recommend settings that do nothing.** `API_CLIENT_TIMEOUT` was never read by Open Notebook; the timeout guidance now covers what actually applies: `ESPERANTO_LLM_TIMEOUT` per model call (keep it below 600) and the web UI's 10-minute request limit (`NEXT_PUBLIC_API_TIMEOUT_MS`, build time only). Remaining `SURREAL_COMMANDS_MAX_TASKS` advice now uses `OPEN_NOTEBOOK_WORKER_MAX_TASKS`. Docker instructions no longer say to edit `.env` and `docker compose restart`: the shipped compose file doesn't load `.env` into the container and `restart` doesn't reload environment changes, so they now point to the `open_notebook` service's `environment:` block and `docker compose up -d`
|
||||
- **Timeouts no longer read as connection failures.** A model that doesn't answer within `ESPERANTO_LLM_TIMEOUT` used to report "Could not connect to the AI provider"; it now says the provider took too long and how to raise the limit. Connection failures (including connect timeouts) keep the old message
|
||||
- Error hints point to screens that exist: an authentication failure sends you to Manage → Models (not "Settings → Credentials"), and an unsupported podcast voice to Podcasts → Profiles. The empty Podcasts page points to the Generate Podcast button instead of chat screens that can't start a podcast
|
||||
- **Italian is selectable in the language menu**, and dates are formatted in Italian. The menu now lists every registered language, so a new translation can't be left out again
|
||||
- `make start-all` starts SurrealDB again (it pointed to a compose file removed long ago)
|
||||
- The legacy `SURREAL_ADDRESS` / `SURREAL_PORT` fallback builds a valid database URL (`ws://host:port/rpc`, was `ws://host/rpc:port`); an address that already includes the port is kept as is
|
||||
- `.env.example` names the chunking variables Open Notebook actually reads (`OPEN_NOTEBOOK_CHUNK_SIZE` / `OPEN_NOTEBOOK_CHUNK_OVERLAP`)
|
||||
|
||||
## [1.15.0] - 2026-10-04
|
||||
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -176,7 +176,7 @@ worker-restart: worker-stop
|
|||
start-all:
|
||||
@echo "🚀 Starting Open Notebook (Database + API + Worker + Frontend)..."
|
||||
@echo "📊 Starting SurrealDB..."
|
||||
@docker compose -f docker-compose.dev.yml up -d surrealdb
|
||||
@docker compose up -d surrealdb
|
||||
@sleep 3
|
||||
@echo "🔧 Starting API backend..."
|
||||
@uv run run_api.py &
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ def explain_generation_failure(error_msg: str) -> Optional[str]:
|
|||
if "Voice name" in error_msg and "not supported" in error_msg:
|
||||
return (
|
||||
"The speaker profile's voice_id is not valid for its TTS model. "
|
||||
"Check the voices in Settings -> Speaker Profiles against the ones "
|
||||
"Check the speaker profile voices in Podcasts -> Profiles against the ones "
|
||||
"your voice model provides (the profiles seeded on install use "
|
||||
"OpenAI voice names)."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@ import {
|
|||
} from '@/components/ui/dropdown-menu'
|
||||
import { Languages } from 'lucide-react'
|
||||
import { useTranslation } from '@/lib/hooks/use-translation'
|
||||
import { languages, type LanguageCode } from '@/lib/locales'
|
||||
|
||||
// Every language registered in lib/locales is listed, so a new locale can't be
|
||||
// left out of the menu. Browser-detected codes ("pt", "zh-Hant") match by prefix.
|
||||
function isActive(code: LanguageCode, current: string): boolean {
|
||||
if (code === 'zh-CN') return current === 'zh-CN' || current === 'zh' || current.startsWith('zh-Hans')
|
||||
if (code === 'zh-TW') return current === 'zh-TW' || current.startsWith('zh-Hant')
|
||||
return current === code || current.startsWith(code.split('-')[0])
|
||||
}
|
||||
|
||||
interface LanguageToggleProps {
|
||||
iconOnly?: boolean
|
||||
|
|
@ -34,84 +43,15 @@ export function LanguageToggle({ iconOnly = false }: LanguageToggleProps) {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('en-US')}
|
||||
className={currentLang === 'en-US' || currentLang.startsWith('en') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.english')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('ca-ES')}
|
||||
className={currentLang === 'ca-ES' || currentLang.startsWith('ca') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.catalan')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('zh-CN')}
|
||||
className={currentLang === 'zh-CN' || currentLang.startsWith('zh-Hans') || currentLang === 'zh' ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.chinese')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('zh-TW')}
|
||||
className={currentLang === 'zh-TW' || currentLang.startsWith('zh-Hant') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.traditionalChinese')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('pt-BR')}
|
||||
className={currentLang === 'pt-BR' || currentLang.startsWith('pt') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.portuguese')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('ja-JP')}
|
||||
className={currentLang === 'ja-JP' || currentLang.startsWith('ja') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.japanese')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('fr-FR')}
|
||||
className={currentLang === 'fr-FR' || currentLang.startsWith('fr') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.french')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('ru-RU')}
|
||||
className={currentLang === 'ru-RU' || currentLang.startsWith('ru') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.russian')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('bn-IN')}
|
||||
className={currentLang === 'bn-IN' || currentLang.startsWith('bn') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.bengali')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('es-ES')}
|
||||
className={currentLang === 'es-ES' || currentLang.startsWith('es') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.spanish')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('de-DE')}
|
||||
className={currentLang === 'de-DE' || currentLang.startsWith('de') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.german')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('pl-PL')}
|
||||
className={currentLang === 'pl-PL' || currentLang.startsWith('pl') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.polish')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setLanguage('tr-TR')}
|
||||
className={currentLang === 'tr-TR' || currentLang.startsWith('tr') ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{t('common.turkish')}</span>
|
||||
</DropdownMenuItem>
|
||||
{languages.map(({ code, label }) => (
|
||||
<DropdownMenuItem
|
||||
key={code}
|
||||
onClick={() => setLanguage(code)}
|
||||
className={isActive(code, currentLang) ? 'bg-accent' : ''}
|
||||
>
|
||||
<span>{label}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const bnIN = {
|
|||
newNotebook: "নতুন নোটবুক",
|
||||
newPodcast: "নতুন পডকাস্ট",
|
||||
language: "ভাষা",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "উৎস",
|
||||
notebook: "নোটবুক",
|
||||
podcast: "পডকাস্ট",
|
||||
|
|
@ -132,8 +121,6 @@ export const bnIN = {
|
|||
errorDetails: "ত্রুটির বিবরণ",
|
||||
editTransformation: "ট্রান্সফরমেশন সম্পাদনা",
|
||||
retry: "আবার চেষ্টা করুন",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "সম্পন্ন",
|
||||
saveSuccess: "সফলভাবে সংরক্ষিত",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const bnIN = {
|
|||
loadErrorTitle: "এপিসোড লোড করতে ব্যর্থ",
|
||||
loadErrorDesc: "আমরা সর্বশেষ পডকাস্ট এপিসোড আনতে পারিনি। শীঘ্রই আবার চেষ্টা করুন।",
|
||||
loadingEpisodes: "এপিসোড লোড হচ্ছে...",
|
||||
noEpisodesYet: "এখনও কোন পডকাস্ট এপিসোড নেই। নোটবুক বা সোর্স চ্যাট ইন্টারফেস থেকে আপনার প্রথমটি তৈরি করুন।",
|
||||
noEpisodesYet: "এখনও কোন পডকাস্ট এপিসোড নেই। আপনার প্রথমটি তৈরি করতে \"পডকাস্ট তৈরি করুন\" ক্লিক করুন।",
|
||||
statusRunningTitle: "বর্তমানে প্রক্রিয়াকরণ",
|
||||
statusRunningDesc: "এপিসোড যা সক্রিয়ভাবে সম্পদ তৈরি করছে।",
|
||||
statusPendingTitle: "কিউ / অপেক্ষমাণ",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const caES = {
|
|||
newNotebook: "Quadern nou",
|
||||
newPodcast: "Podcast nou",
|
||||
language: "Idioma",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Font",
|
||||
notebook: "Quadern",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -132,8 +121,6 @@ export const caES = {
|
|||
errorDetails: "Detalls de l'error",
|
||||
editTransformation: "Edita la transformació",
|
||||
retry: "Torna-ho a provar",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "completat",
|
||||
saveSuccess: "S'ha desat correctament",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const caES = {
|
|||
loadErrorTitle: "Ha fallat la càrrega dels episodis",
|
||||
loadErrorDesc: "No s'han pogut obtenir els darrers episodis de podcast. Torna-ho a provar en breu.",
|
||||
loadingEpisodes: "S'estan carregant els episodis…",
|
||||
noEpisodesYet: "Encara no hi ha episodis de podcast. Genera el primer des de les interfícies de xat del quadern o la font.",
|
||||
noEpisodesYet: "Encara no hi ha episodis de podcast. Fes clic a Genera un podcast per crear el primer.",
|
||||
statusRunningTitle: "S'està processant",
|
||||
statusRunningDesc: "Episodis que estan generant recursos activament.",
|
||||
statusPendingTitle: "En cua / Pendent",
|
||||
|
|
|
|||
|
|
@ -25,17 +25,6 @@ export const deDE = {
|
|||
newNotebook: "Neues Notebook",
|
||||
newPodcast: "Neuer Podcast",
|
||||
language: "Sprache",
|
||||
english: "Englisch",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
spanish: "Español",
|
||||
catalan: "Català",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Quelle",
|
||||
notebook: "Notebook",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -135,8 +124,6 @@ export const deDE = {
|
|||
errorDetails: "Fehlerdetails",
|
||||
editTransformation: "Transformation bearbeiten",
|
||||
retry: "Erneut versuchen",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "abgeschlossen",
|
||||
saveSuccess: "Erfolgreich gespeichert",
|
||||
contextModes: {
|
||||
|
|
@ -589,7 +576,7 @@ export const deDE = {
|
|||
loadErrorTitle: "Episoden konnten nicht geladen werden",
|
||||
loadErrorDesc: "Die neuesten Podcast-Episoden konnten nicht abgerufen werden. Versuche es gleich erneut.",
|
||||
loadingEpisodes: "Episoden werden geladen…",
|
||||
noEpisodesYet: "Noch keine Podcast-Episoden. Erzeuge deine erste Episode über die Chat-Oberflächen eines Notebooks oder einer Quelle.",
|
||||
noEpisodesYet: "Noch keine Podcast-Episoden. Klicke auf Podcast erzeugen, um deine erste Episode zu erstellen.",
|
||||
statusRunningTitle: "Aktuell in Verarbeitung",
|
||||
statusRunningDesc: "Episoden, deren Inhalte gerade erzeugt werden.",
|
||||
statusPendingTitle: "In Warteschlange / ausstehend",
|
||||
|
|
|
|||
|
|
@ -20,17 +20,6 @@ export const enUS = {
|
|||
newNotebook: "New Notebook",
|
||||
newPodcast: "New Podcast",
|
||||
language: "Language",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Source",
|
||||
notebook: "Notebook",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -130,8 +119,6 @@ export const enUS = {
|
|||
errorDetails: "Error Details",
|
||||
editTransformation: "Edit Transformation",
|
||||
retry: "Try Again",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "completed",
|
||||
saveSuccess: "Saved successfully",
|
||||
contextModes: {
|
||||
|
|
@ -584,7 +571,7 @@ export const enUS = {
|
|||
loadErrorTitle: "Failed to load episodes",
|
||||
loadErrorDesc: "We could not fetch the latest podcast episodes. Try again shortly.",
|
||||
loadingEpisodes: "Loading episodes…",
|
||||
noEpisodesYet: "No podcast episodes yet. Generate your first one from the notebook or source chat interfaces.",
|
||||
noEpisodesYet: "No podcast episodes yet. Click Generate Podcast to create your first one.",
|
||||
statusRunningTitle: "Currently Processing",
|
||||
statusRunningDesc: "Episodes that are actively generating assets.",
|
||||
statusPendingTitle: "Queued / Pending",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const esES = {
|
|||
newNotebook: "Nuevo cuaderno",
|
||||
newPodcast: "Nuevo podcast",
|
||||
language: "Idioma",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Fuente",
|
||||
notebook: "Cuaderno",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -132,8 +121,6 @@ export const esES = {
|
|||
errorDetails: "Detalles del error",
|
||||
editTransformation: "Editar transformación",
|
||||
retry: "Reintentar",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "completado",
|
||||
saveSuccess: "Guardado exitosamente",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const esES = {
|
|||
loadErrorTitle: "Error al cargar episodios",
|
||||
loadErrorDesc: "No pudimos obtener los episodios más recientes. Intenta de nuevo en un momento.",
|
||||
loadingEpisodes: "Cargando episodios…",
|
||||
noEpisodesYet: "Aún no hay episodios de podcast. Genera tu primero desde las interfaces de chat de cuadernos o fuentes.",
|
||||
noEpisodesYet: "Aún no hay episodios de podcast. Haz clic en Generar podcast para crear el primero.",
|
||||
statusRunningTitle: "Procesando actualmente",
|
||||
statusRunningDesc: "Episodios que están generando activos activamente.",
|
||||
statusPendingTitle: "En cola / Pendientes",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const frFR = {
|
|||
newNotebook: "Nouveau Carnet",
|
||||
newPodcast: "Nouveau Podcast",
|
||||
language: "Langue",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Source",
|
||||
notebook: "Carnet",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -132,8 +121,6 @@ export const frFR = {
|
|||
errorDetails: "Détails de l'erreur",
|
||||
editTransformation: "Modifier la transformation",
|
||||
retry: "Réessayer",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "terminé",
|
||||
saveSuccess: "Enregistré avec succès",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const frFR = {
|
|||
loadErrorTitle: "Échec du chargement des épisodes",
|
||||
loadErrorDesc: "Nous n'avons pas pu récupérer les derniers épisodes. Réessayez dans un instant.",
|
||||
loadingEpisodes: "Chargement des épisodes…",
|
||||
noEpisodesYet: "Aucun épisode de podcast pour le moment. Générez votre premier depuis le carnet ou les interfaces de chat.",
|
||||
noEpisodesYet: "Aucun épisode de podcast pour le moment. Cliquez sur Générer un podcast pour créer le premier.",
|
||||
statusRunningTitle: "En cours de traitement",
|
||||
statusRunningDesc: "Épisodes dont les ressources sont activement en cours de génération.",
|
||||
statusPendingTitle: "En file d'attente / En attente",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const itIT = {
|
|||
newNotebook: "Nuovo quaderno",
|
||||
newPodcast: "Nuovo podcast",
|
||||
language: "Lingua",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Fonte",
|
||||
notebook: "Quaderno",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -132,8 +121,6 @@ export const itIT = {
|
|||
errorDetails: "Dettagli errore",
|
||||
editTransformation: "Modifica trasformazione",
|
||||
retry: "Riprova",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "completato",
|
||||
saveSuccess: "Salvato con successo",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const itIT = {
|
|||
loadErrorTitle: "Impossibile caricare gli episodi",
|
||||
loadErrorDesc: "Non siamo riusciti a recuperare gli ultimi episodi podcast. Riprova tra poco.",
|
||||
loadingEpisodes: "Caricamento episodi…",
|
||||
noEpisodesYet: "Ancora nessun episodio podcast. Genera il tuo primo dalle interfacce chat di quaderni o fonti.",
|
||||
noEpisodesYet: "Ancora nessun episodio podcast. Fai clic su Genera Podcast per creare il primo.",
|
||||
statusRunningTitle: "In elaborazione",
|
||||
statusRunningDesc: "Episodi che stanno attivamente generando risorse.",
|
||||
statusPendingTitle: "In coda / In attesa",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const jaJP = {
|
|||
newNotebook: "新規ノートブック",
|
||||
newPodcast: "新規ポッドキャスト",
|
||||
language: "言語",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "ソース",
|
||||
notebook: "ノートブック",
|
||||
podcast: "ポッドキャスト",
|
||||
|
|
@ -132,8 +121,6 @@ export const jaJP = {
|
|||
errorDetails: "エラー詳細",
|
||||
editTransformation: "変換を編集",
|
||||
retry: "再試行",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "完了",
|
||||
saveSuccess: "保存しました",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const jaJP = {
|
|||
loadErrorTitle: "エピソードの読み込みに失敗しました",
|
||||
loadErrorDesc: "最新のポッドキャストエピソードを取得できませんでした。しばらくしてから再試行してください。",
|
||||
loadingEpisodes: "エピソードを読み込み中...",
|
||||
noEpisodesYet: "ポッドキャストエピソードがまだありません。ノートブックまたはソースのチャットインターフェースから最初のエピソードを生成してください。",
|
||||
noEpisodesYet: "ポッドキャストエピソードがまだありません。「ポッドキャストを生成」をクリックして最初のエピソードを作成してください。",
|
||||
statusRunningTitle: "処理中",
|
||||
statusRunningDesc: "現在アセットを生成中のエピソード。",
|
||||
statusPendingTitle: "キュー待ち / 保留中",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const plPL = {
|
|||
newNotebook: "Nowy notatnik",
|
||||
newPodcast: "Nowy podcast",
|
||||
language: "Język",
|
||||
english: "Angielski",
|
||||
chinese: "Chiński (uproszczony)",
|
||||
japanese: "Japoński",
|
||||
french: "Francuski",
|
||||
russian: "Rosyjski",
|
||||
bengali: "Bengalski",
|
||||
spanish: "Hiszpański",
|
||||
catalan: "Kataloński",
|
||||
german: "Niemiecki",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Źródło",
|
||||
notebook: "Notatnik",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -132,8 +121,6 @@ export const plPL = {
|
|||
errorDetails: "Szczegóły błędu",
|
||||
editTransformation: "Edytuj transformację",
|
||||
retry: "Spróbuj ponownie",
|
||||
traditionalChinese: "Chiński (tradycyjny)",
|
||||
portuguese: "Portugalski",
|
||||
completed: "zakończono",
|
||||
saveSuccess: "Zapisano pomyślnie",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const plPL = {
|
|||
loadErrorTitle: "Nie udało się załadować odcinków",
|
||||
loadErrorDesc: "Nie mogliśmy pobrać najnowszych odcinków podcastu. Spróbuj ponownie za chwilę.",
|
||||
loadingEpisodes: "Ładowanie odcinków…",
|
||||
noEpisodesYet: "Brak odcinków podcastu. Wygeneruj pierwszy z poziomu notatnika lub interfejsu czatu źródła.",
|
||||
noEpisodesYet: "Brak odcinków podcastu. Kliknij Wygeneruj podcast, aby utworzyć pierwszy.",
|
||||
statusRunningTitle: "Obecnie przetwarzane",
|
||||
statusRunningDesc: "Odcinki, dla których aktywnie generowane są materiały.",
|
||||
statusPendingTitle: "W kolejce / Oczekujące",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const ptBR = {
|
|||
newNotebook: "Novo Caderno",
|
||||
newPodcast: "Novo Podcast",
|
||||
language: "Idioma",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Fonte",
|
||||
notebook: "Caderno",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -132,8 +121,6 @@ export const ptBR = {
|
|||
errorDetails: "Detalhes do Erro",
|
||||
editTransformation: "Editar Transformação",
|
||||
retry: "Tentar Novamente",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "concluído",
|
||||
saveSuccess: "Salvo com sucesso",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const ptBR = {
|
|||
loadErrorTitle: "Falha ao carregar episódios",
|
||||
loadErrorDesc: "Não foi possível buscar os episódios de podcast mais recentes. Tente novamente em breve.",
|
||||
loadingEpisodes: "Carregando episódios…",
|
||||
noEpisodesYet: "Nenhum episódio de podcast ainda. Gere seu primeiro a partir das interfaces de chat de caderno ou fonte.",
|
||||
noEpisodesYet: "Nenhum episódio de podcast ainda. Clique em Gerar Podcast para criar o primeiro.",
|
||||
statusRunningTitle: "Processando Atualmente",
|
||||
statusRunningDesc: "Episódios que estão gerando ativos ativamente.",
|
||||
statusPendingTitle: "Na Fila / Pendentes",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const ruRU = {
|
|||
newNotebook: "Новый блокнот",
|
||||
newPodcast: "Новый подкаст",
|
||||
language: "Язык",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Источник",
|
||||
notebook: "Блокнот",
|
||||
podcast: "Подкаст",
|
||||
|
|
@ -132,8 +121,6 @@ export const ruRU = {
|
|||
errorDetails: "Детали ошибки",
|
||||
editTransformation: "Редактировать трансформацию",
|
||||
retry: "Повторить",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "завершено",
|
||||
saveSuccess: "Успешно сохранено",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const ruRU = {
|
|||
loadErrorTitle: "Не удалось загрузить эпизоды",
|
||||
loadErrorDesc: "Не удалось получить последние эпизоды подкастов. Попробуйте позже.",
|
||||
loadingEpisodes: "Загрузка эпизодов…",
|
||||
noEpisodesYet: "Пока нет эпизодов подкастов. Сгенерируйте первый из интерфейса чата блокнота или источника.",
|
||||
noEpisodesYet: "Пока нет эпизодов подкастов. Нажмите «Сгенерировать подкаст», чтобы создать первый.",
|
||||
statusRunningTitle: "В процессе",
|
||||
statusRunningDesc: "Эпизоды, для которых активно генерируются материалы.",
|
||||
statusPendingTitle: "В очереди / Ожидание",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const trTR = {
|
|||
newNotebook: "Yeni Defter",
|
||||
newPodcast: "Yeni Podcast",
|
||||
language: "Dil",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "Kaynak",
|
||||
notebook: "Defter",
|
||||
podcast: "Podcast",
|
||||
|
|
@ -132,8 +121,6 @@ export const trTR = {
|
|||
errorDetails: "Hata Ayrıntıları",
|
||||
editTransformation: "Dönüşümü Düzenle",
|
||||
retry: "Tekrar Dene",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "Português",
|
||||
completed: "tamamlandı",
|
||||
saveSuccess: "Başarıyla kaydedildi",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const trTR = {
|
|||
loadErrorTitle: "Bölümler yüklenemedi",
|
||||
loadErrorDesc: "En son podcast bölümleri alınamadı. Kısa süre sonra tekrar deneyin.",
|
||||
loadingEpisodes: "Bölümler yükleniyor…",
|
||||
noEpisodesYet: "Henüz podcast bölümü yok. Defter veya kaynak sohbet arayüzlerinden ilk bölümünüzü oluşturun.",
|
||||
noEpisodesYet: "Henüz podcast bölümü yok. İlk bölümünüzü oluşturmak için Podcast Oluştur'a tıklayın.",
|
||||
statusRunningTitle: "Şu An İşleniyor",
|
||||
statusRunningDesc: "Aktif olarak içerik oluşturan bölümler.",
|
||||
statusPendingTitle: "Kuyrukta / Beklemede",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const zhCN = {
|
|||
newNotebook: "新建笔记本",
|
||||
newPodcast: "新建播客",
|
||||
language: "语言",
|
||||
english: "English",
|
||||
chinese: "简体中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "来源",
|
||||
notebook: "笔记本",
|
||||
podcast: "播客",
|
||||
|
|
@ -132,8 +121,6 @@ export const zhCN = {
|
|||
errorDetails: "错误详情",
|
||||
editTransformation: "编辑转换规则",
|
||||
retry: "重试",
|
||||
traditionalChinese: "繁体中文",
|
||||
portuguese: "葡萄牙语",
|
||||
completed: "已完成",
|
||||
saveSuccess: "保存成功",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const zhCN = {
|
|||
loadErrorTitle: "加载单集失败",
|
||||
loadErrorDesc: "无法获取最新的播客单集。请稍后重试。",
|
||||
loadingEpisodes: "正在加载单集...",
|
||||
noEpisodesYet: "暂无播客单集。从笔记本或来源聊天界面开始生成您的第一个单集。",
|
||||
noEpisodesYet: "暂无播客单集。点击“生成播客”创建您的第一个单集。",
|
||||
statusRunningTitle: "正在处理中",
|
||||
statusRunningDesc: "正在活跃生成资产的单集。",
|
||||
statusPendingTitle: "排队中 / 待处理",
|
||||
|
|
|
|||
|
|
@ -22,17 +22,6 @@ export const zhTW = {
|
|||
newNotebook: "新增筆記本",
|
||||
newPodcast: "新增播客",
|
||||
language: "語言",
|
||||
english: "English",
|
||||
chinese: "簡體中文",
|
||||
japanese: "日本語",
|
||||
french: "Français",
|
||||
russian: "Русский",
|
||||
bengali: "বাংলা",
|
||||
catalan: "Català",
|
||||
spanish: "Español",
|
||||
german: "Deutsch",
|
||||
polish: "Polski",
|
||||
turkish: "Türkçe",
|
||||
source: "來源",
|
||||
notebook: "筆記本",
|
||||
podcast: "播客",
|
||||
|
|
@ -132,8 +121,6 @@ export const zhTW = {
|
|||
errorDetails: "錯誤詳情",
|
||||
editTransformation: "編輯轉換規則",
|
||||
retry: "重試",
|
||||
traditionalChinese: "繁體中文",
|
||||
portuguese: "葡萄牙語",
|
||||
completed: "已完成",
|
||||
saveSuccess: "儲存成功",
|
||||
contextModes: {
|
||||
|
|
@ -586,7 +573,7 @@ export const zhTW = {
|
|||
loadErrorTitle: "載入單集失敗",
|
||||
loadErrorDesc: "無法獲取最新的播客單集。請稍後重試。",
|
||||
loadingEpisodes: "正在載入單集...",
|
||||
noEpisodesYet: "暫無播客單集。從筆記本或來源聊天介面開始生成您的第一個單集。",
|
||||
noEpisodesYet: "暫無播客單集。點擊「生成播客」建立您的第一個單集。",
|
||||
statusRunningTitle: "正在處理中",
|
||||
statusRunningDesc: "正在活躍生成資產的單集。",
|
||||
statusPendingTitle: "排隊中 / 待處理",
|
||||
|
|
|
|||
22
frontend/src/lib/utils/date-locale.test.ts
Normal file
22
frontend/src/lib/utils/date-locale.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { enUS, it as itLocale } from 'date-fns/locale'
|
||||
|
||||
import { languages } from '@/lib/locales'
|
||||
import { getDateLocale } from './date-locale'
|
||||
|
||||
describe('getDateLocale', () => {
|
||||
it('formats Italian dates in Italian', () => {
|
||||
expect(getDateLocale('it-IT')).toBe(itLocale)
|
||||
})
|
||||
|
||||
it('has a date locale for every registered language', () => {
|
||||
for (const { code } of languages) {
|
||||
if (code === 'en-US') continue
|
||||
expect(getDateLocale(code), code).not.toBe(enUS)
|
||||
}
|
||||
})
|
||||
|
||||
it('falls back to English for unknown codes', () => {
|
||||
expect(getDateLocale('xx-XX')).toBe(enUS)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { zhCN, enUS, zhTW, ptBR, ja, fr, ru, bn, ca, es, de, pl, tr, Locale } from 'date-fns/locale'
|
||||
import { zhCN, enUS, zhTW, ptBR, ja, it, fr, ru, bn, ca, es, de, pl, tr, Locale } from 'date-fns/locale'
|
||||
|
||||
/**
|
||||
* Mapping of language codes to date-fns locales.
|
||||
|
|
@ -10,6 +10,7 @@ const LOCALE_MAP: Record<string, Locale> = {
|
|||
'en-US': enUS,
|
||||
'pt-BR': ptBR,
|
||||
'ja-JP': ja,
|
||||
'it-IT': it,
|
||||
'fr-FR': fr,
|
||||
'ru-RU': ru,
|
||||
'bn-IN': bn,
|
||||
|
|
|
|||
|
|
@ -43,10 +43,12 @@ def get_database_url() -> str:
|
|||
if surreal_url:
|
||||
return surreal_url
|
||||
|
||||
# Fallback to old format - WebSocket URL format
|
||||
# Fallback to the legacy variables. SURREAL_ADDRESS may already carry the
|
||||
# port ("host:8000", "[::1]:8000"); SURREAL_PORT only applies when it doesn't.
|
||||
address = os.getenv("SURREAL_ADDRESS", "localhost")
|
||||
port = os.getenv("SURREAL_PORT", "8000")
|
||||
return f"ws://{address}/rpc:{port}"
|
||||
if ":" not in address.rsplit("]", 1)[-1]:
|
||||
address = f"{address}:{os.getenv('SURREAL_PORT', '8000')}"
|
||||
return f"ws://{address}/rpc"
|
||||
|
||||
|
||||
def get_database_password() -> str:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ _CLASSIFICATION_RULES: list[tuple[list[str], type[OpenNotebookError], str | None
|
|||
(
|
||||
["authentication", "unauthorized", "invalid api key", "invalid_api_key", "401"],
|
||||
AuthenticationError,
|
||||
"Authentication failed. Please check your API key in Settings -> Credentials.",
|
||||
"Authentication failed. Please check your API key in Manage -> Models.",
|
||||
),
|
||||
# Rate limit errors
|
||||
(
|
||||
|
|
@ -39,23 +39,29 @@ _CLASSIFICATION_RULES: list[tuple[list[str], type[OpenNotebookError], str | None
|
|||
),
|
||||
# Configuration errors from provision.py (pass through)
|
||||
(
|
||||
["no model configured", "please go to settings"],
|
||||
["no model configured", "please go to settings", "please go to manage"],
|
||||
ConfigurationError,
|
||||
None,
|
||||
),
|
||||
# Network errors
|
||||
# Network errors (connect timeouts included: the provider was never reached)
|
||||
(
|
||||
[
|
||||
"connecterror",
|
||||
"timeoutexception",
|
||||
"connecttimeout",
|
||||
"connection timed out",
|
||||
"connection refused",
|
||||
"connection error",
|
||||
"timed out",
|
||||
"timeout",
|
||||
],
|
||||
NetworkError,
|
||||
"Could not connect to the AI provider. Please check your network connection and provider URL.",
|
||||
),
|
||||
# Read timeouts: the provider was reached but didn't answer within
|
||||
# ESPERANTO_LLM_TIMEOUT (180 s unless set)
|
||||
(
|
||||
["timeoutexception", "timed out", "timeout"],
|
||||
NetworkError,
|
||||
"The AI provider took too long to respond. Try again, use a faster model, or raise ESPERANTO_LLM_TIMEOUT (180 seconds by default).",
|
||||
),
|
||||
# Context length errors
|
||||
(
|
||||
[
|
||||
|
|
|
|||
44
tests/test_database_url.py
Normal file
44
tests/test_database_url.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""get_database_url() fallback to the legacy SURREAL_ADDRESS / SURREAL_PORT variables."""
|
||||
|
||||
import pytest
|
||||
|
||||
from open_notebook.database.repository import get_database_url
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clean_env(monkeypatch):
|
||||
for name in ("SURREAL_URL", "SURREAL_ADDRESS", "SURREAL_PORT"):
|
||||
monkeypatch.delenv(name, raising=False)
|
||||
|
||||
|
||||
def test_surreal_url_wins(monkeypatch):
|
||||
monkeypatch.setenv("SURREAL_URL", "ws://db:8000/rpc")
|
||||
monkeypatch.setenv("SURREAL_ADDRESS", "other")
|
||||
assert get_database_url() == "ws://db:8000/rpc"
|
||||
|
||||
|
||||
def test_defaults():
|
||||
assert get_database_url() == "ws://localhost:8000/rpc"
|
||||
|
||||
|
||||
def test_address_and_port(monkeypatch):
|
||||
monkeypatch.setenv("SURREAL_ADDRESS", "db.internal")
|
||||
monkeypatch.setenv("SURREAL_PORT", "8018")
|
||||
assert get_database_url() == "ws://db.internal:8018/rpc"
|
||||
|
||||
|
||||
def test_bracketed_ipv6_without_port_gets_surreal_port(monkeypatch):
|
||||
monkeypatch.setenv("SURREAL_ADDRESS", "[::1]")
|
||||
monkeypatch.setenv("SURREAL_PORT", "8018")
|
||||
assert get_database_url() == "ws://[::1]:8018/rpc"
|
||||
|
||||
|
||||
def test_bracketed_ipv6_with_port(monkeypatch):
|
||||
monkeypatch.setenv("SURREAL_ADDRESS", "[::1]:8000")
|
||||
assert get_database_url() == "ws://[::1]:8000/rpc"
|
||||
|
||||
|
||||
def test_address_with_port_ignores_surreal_port(monkeypatch):
|
||||
monkeypatch.setenv("SURREAL_ADDRESS", "10.1.2.3:8000")
|
||||
monkeypatch.setenv("SURREAL_PORT", "9999")
|
||||
assert get_database_url() == "ws://10.1.2.3:8000/rpc"
|
||||
60
tests/test_error_classifier.py
Normal file
60
tests/test_error_classifier.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""classify_error() tells a slow provider apart from an unreachable one."""
|
||||
|
||||
import httpx
|
||||
|
||||
from open_notebook.exceptions import (
|
||||
AuthenticationError,
|
||||
ConfigurationError,
|
||||
NetworkError,
|
||||
)
|
||||
from open_notebook.utils.error_classifier import classify_error
|
||||
|
||||
|
||||
def test_read_timeout_says_the_provider_was_slow():
|
||||
exc_class, message = classify_error(
|
||||
httpx.ReadTimeout("The read operation timed out")
|
||||
)
|
||||
assert exc_class is NetworkError
|
||||
assert "took too long" in message
|
||||
assert "ESPERANTO_LLM_TIMEOUT" in message
|
||||
|
||||
|
||||
def test_openai_style_timeout_says_the_provider_was_slow():
|
||||
class APITimeoutError(Exception):
|
||||
pass
|
||||
|
||||
_, message = classify_error(APITimeoutError("Request timed out."))
|
||||
assert "took too long" in message
|
||||
|
||||
|
||||
def test_connect_timeout_is_a_connection_problem():
|
||||
exc_class, message = classify_error(httpx.ConnectTimeout("timed out"))
|
||||
assert exc_class is NetworkError
|
||||
assert "Could not connect" in message
|
||||
|
||||
|
||||
def test_connection_refused_is_a_connection_problem():
|
||||
exc_class, message = classify_error(
|
||||
httpx.ConnectError("[Errno 111] Connection refused")
|
||||
)
|
||||
assert exc_class is NetworkError
|
||||
assert "Could not connect" in message
|
||||
|
||||
|
||||
def test_os_connection_timed_out_is_a_connection_problem():
|
||||
exc_class, message = classify_error(OSError("[Errno 110] Connection timed out"))
|
||||
assert exc_class is NetworkError
|
||||
assert "Could not connect" in message
|
||||
|
||||
|
||||
def test_auth_error_points_to_the_models_page():
|
||||
exc_class, message = classify_error(Exception("401 invalid api key"))
|
||||
assert exc_class is AuthenticationError
|
||||
assert "Manage -> Models" in message
|
||||
|
||||
|
||||
def test_missing_default_model_passes_through():
|
||||
raw = "No model configured for default for type=chat. Please go to Manage → Models and configure a default model for 'chat'."
|
||||
exc_class, message = classify_error(ValueError(raw))
|
||||
assert exc_class is ConfigurationError
|
||||
assert message == raw
|
||||
|
|
@ -50,7 +50,7 @@ class TestExplainGenerationFailure:
|
|||
)
|
||||
assert hint is not None
|
||||
assert "voice_id" in hint
|
||||
assert "Speaker Profiles" in hint
|
||||
assert "Podcasts -> Profiles" in hint
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"message",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue