diff --git a/assemblyai/__init__.py b/assemblyai/__init__.py index 982f0ceb..8a6c740f 100644 --- a/assemblyai/__init__.py +++ b/assemblyai/__init__.py @@ -23,6 +23,7 @@ KeytermsPromptOptions, LanguageCode, LanguageDetectionOptions, + LanguageHints, ListTranscriptParameters, ListTranscriptResponse, PageDetails, @@ -117,6 +118,7 @@ def __getattr__(name: str): "KeytermsPromptOptions", "LanguageCode", "LanguageDetectionOptions", + "LanguageHints", "ListTranscriptParameters", "ListTranscriptResponse", "PageDetails", diff --git a/assemblyai/__version__.py b/assemblyai/__version__.py index 67bc602a..9c73af26 100644 --- a/assemblyai/__version__.py +++ b/assemblyai/__version__.py @@ -1 +1 @@ -__version__ = "1.3.0" +__version__ = "1.3.1" diff --git a/assemblyai/types.py b/assemblyai/types.py index b0af7309..9fadb199 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -851,7 +851,64 @@ def validate_max_speakers(cls, v, values): return v +class LanguageHints(BaseModel): + """Optional system-prompt conditioning, independent of language routing. + + The default is a canonical language code. When supplied, languages is an + ordered, unique list; the default may be outside it. The server validates supported + codes; omitting this object leaves the system prompt unconditioned. + """ + + default_language: str + languages: Optional[List[str]] = None + + if pydantic_v2: + model_config = ConfigDict(extra="forbid") + + @field_validator("default_language") + @classmethod + def validate_default(cls, value): + return cls._code(value) + + @field_validator("languages") + @classmethod + def validate_languages(cls, value): + return cls._languages(value) + + else: + + class Config: + extra = "forbid" + + @validator("default_language") + def validate_default(cls, value): + return cls._code(value) + + @validator("languages") + def validate_languages(cls, value): + return cls._languages(value) + + @staticmethod + def _code(value): + code = value.strip().lower() + if not code.isascii() or not code.isalpha() or len(code) not in (2, 3): + raise ValueError("language hints require canonical language codes") + return code + + @classmethod + def _languages(cls, value): + if value is None: + return None + codes = [cls._code(code) for code in value] + if not codes or len(set(codes)) != len(codes): + raise ValueError("languages must be a nonempty list of unique codes") + return codes + + class RawTranscriptionConfig(BaseModel): + language_hints: Optional[LanguageHints] = None + "Optional system-prompt language default and ordered language list." + language_code: Optional[Union[str, LanguageCode]] = None """ The language of your audio file. Possible values are found in Supported Languages. @@ -1079,11 +1136,13 @@ def __init__( keyterms_prompt_options: Optional[KeytermsPromptOptions] = None, speech_understanding: Optional[SpeechUnderstandingRequest] = None, domain: Optional[str] = None, + language_hints: Optional[Union[LanguageHints, Dict[str, Any]]] = None, ) -> None: """ Args: language_code: The language of your audio file. Possible values are found in Supported Languages. language_codes: A list of language codes for multilingual/code-switching audio. + language_hints: Optional system-prompt conditioning, independent of routing. punctuate: Enable Automatic Punctuation format_text: Enable Text Formatting dual_channel: Enable Dual Channel transcription @@ -1189,6 +1248,12 @@ def __init__( self.keyterms_prompt_options = keyterms_prompt_options self.speech_understanding = speech_understanding self.domain = domain + if language_hints is not None or raw_transcription_config is None: + self.language_hints = ( + LanguageHints(**language_hints) + if isinstance(language_hints, dict) + else language_hints + ) @property def raw(self) -> RawTranscriptionConfig: @@ -1237,6 +1302,22 @@ def prompt(self, prompt: Optional[str]) -> None: "Sets the prompt to use for the transcription." self._raw_transcription_config.prompt = prompt + @property + def language_hints(self) -> Optional[LanguageHints]: + """The system-prompt language default and optional ordered language list.""" + return self._raw_transcription_config.language_hints + + @language_hints.setter + def language_hints( + self, value: Optional[Union[LanguageHints, Dict[str, Any]]] + ) -> None: + if value is None: + self._raw_transcription_config.language_hints = None + elif isinstance(value, LanguageHints): + self._raw_transcription_config.language_hints = value.copy(deep=True) + else: + self._raw_transcription_config.language_hints = LanguageHints(**value) + @property def temperature(self) -> Optional[float]: "The temperature to use for the transcription." @@ -2295,6 +2376,9 @@ class BaseTranscript(BaseModel): Available transcription features """ + language_hints: Optional[LanguageHints] = None + "Optional system-prompt language default and ordered language list." + language_code: Optional[Union[str, LanguageCode]] = None """ The language of your audio file. Possible values are found in Supported Languages. diff --git a/tests/unit/test_language_hints.py b/tests/unit/test_language_hints.py new file mode 100644 index 00000000..ab7af97f --- /dev/null +++ b/tests/unit/test_language_hints.py @@ -0,0 +1,101 @@ +import copy +import json + +import httpx +import pytest + +import assemblyai as aai +from assemblyai.api import ENDPOINT_TRANSCRIPT +from tests.unit import factories + +aai.settings.api_key = "test" + + +@pytest.mark.parametrize( + "hint", + [ + None, + {"default_language": "en"}, + {"default_language": "en", "languages": ["de", "fr", "en"]}, + {"default_language": "en", "languages": ["fr"]}, + {"default_language": "de", "languages": ["hi", "en"]}, + ], +) +def test_hint_survives_actual_submit_request(httpx_mock, hint): + response = factories.generate_dict_factory( + factories.TranscriptProcessingResponseFactory + )() + httpx_mock.add_response( + url=f"{aai.settings.base_url}{ENDPOINT_TRANSCRIPT}", + method="POST", + status_code=httpx.codes.OK, + json=response, + ) + config = aai.TranscriptionConfig( + language_hints=hint, + prompt="Transcribe exactly.", + language_detection=True, + language_codes=["fr", "en"], + ) + aai.Transcriber().submit("https://example.org/audio.wav", config=config) + payload = json.loads(httpx_mock.get_request().content) + if hint is None: + assert "language_hints" not in payload + else: + assert payload["language_hints"] == hint + assert payload["prompt"] == "Transcribe exactly." + assert payload["language_detection"] is True + assert payload["language_codes"] == ["fr", "en"] + + +def test_per_file_config_copies_and_clear(): + shared = aai.TranscriptionConfig( + language_hints={"default_language": "en", "languages": ["en", "fr"]} + ) + french = copy.deepcopy(shared) + french.language_hints = {"default_language": "fr", "languages": ["fr", "en"]} + assert shared.language_hints.default_language == "en" + assert shared.language_hints.languages == ["en", "fr"] + french.language_hints = None + assert french.language_hints is None + model = aai.LanguageHints(default_language="en", languages=["de", "en"]) + shared.language_hints = model + model.languages.append("fr") + assert shared.language_hints.languages == ["de", "en"] + + +@pytest.mark.parametrize( + "hint", + [ + {}, + {"default_language": ""}, + {"default_language": "English"}, + {"default_language": "en", "languages": []}, + {"default_language": "en", "languages": ["en", "EN"]}, + {"default_language": "en", "unknown": "value"}, + ], +) +def test_invalid_hint_rejected(hint): + with pytest.raises(ValueError): + aai.TranscriptionConfig(language_hints=hint) + + +def test_raw_configuration_preserves_hints(): + raw = aai.RawTranscriptionConfig( + language_hints={"default_language": "fr", "languages": ["en", "fr"]} + ) + config = aai.TranscriptionConfig(raw_transcription_config=raw) + assert config.language_hints.default_language == "fr" + assert config.language_hints.languages == ["en", "fr"] + override = aai.TranscriptionConfig( + raw_transcription_config=raw, language_hints={"default_language": "de"} + ) + assert override.language_hints.default_language == "de" + + +def test_constructor_copies_typed_hint(): + hint = aai.LanguageHints(default_language="en", languages=["fr"]) + config = aai.TranscriptionConfig(language_hints=hint) + hint.languages.append("de") + assert config.language_hints.default_language == "en" + assert config.language_hints.languages == ["fr"]