SurfSense/surfsense_backend/app/podcasts/resolution/language.py

64 lines
2 KiB
Python

"""Resolve the brief's language without spending tokens at the gate.
The chain mirrors the agreed policy: reuse the language the user last chose, and
otherwise default to English (which the user can still override in the brief). We
deliberately never guess the language from the source content — proposing a
language the user did not ask for is worse than a predictable default.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
# What a brand-new user with no signal gets, and what every chain ends on.
DEFAULT_LANGUAGE = "en"
@dataclass(frozen=True, slots=True)
class LanguageContext:
"""Signals available when proposing a language for a fresh podcast."""
last_used: str | None = None
class LanguageResolver(ABC):
"""One step in the language fallback chain."""
@abstractmethod
def resolve(self, context: LanguageContext) -> str | None:
"""Return a language tag, or ``None`` to defer to the next resolver."""
class LastUsedLanguage(LanguageResolver):
"""Reuse the language from the user's previous podcast."""
def resolve(self, context: LanguageContext) -> str | None:
return context.last_used
class DefaultLanguage(LanguageResolver):
"""Terminal step: always yields the default so the chain never fails."""
def resolve(self, context: LanguageContext) -> str | None:
return DEFAULT_LANGUAGE
# Order encodes the policy; prepend stronger signals here as they appear.
DEFAULT_LANGUAGE_CHAIN: tuple[LanguageResolver, ...] = (
LastUsedLanguage(),
DefaultLanguage(),
)
def resolve_language(
context: LanguageContext,
chain: tuple[LanguageResolver, ...] = DEFAULT_LANGUAGE_CHAIN,
) -> str:
"""Walk ``chain`` and return the first language a resolver yields."""
for resolver in chain:
language = resolver.resolve(context)
if language:
return language.strip()
# The default resolver guarantees a value; this guards a misconfigured chain.
return DEFAULT_LANGUAGE