From 6604080d4d157c327aa456bba7f86799b4b35c3d Mon Sep 17 00:00:00 2001 From: Ryan Eloff Date: Thu, 10 Sep 2026 14:36:50 +0000 Subject: [PATCH 1/3] Add typed system prompt language hints to transcription requests --- assemblyai/__init__.py | 2 + assemblyai/types.py | 82 +++++++++++++++++++++++++++ tests/unit/test_language_hints.py | 92 +++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 tests/unit/test_language_hints.py 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/types.py b/assemblyai/types.py index b0af7309..f5493b73 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -851,7 +851,66 @@ 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 containing that default. 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, info): + return cls._languages(value, info.data.get("default_language")) + + else: + + class Config: + extra = "forbid" + + @validator("default_language") + def validate_default(cls, value): + return cls._code(value) + + @validator("languages") + def validate_languages(cls, value, values): + return cls._languages(value, values.get("default_language")) + + @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, default): + 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") + if default is not None and default not in codes: + raise ValueError("languages must include default_language") + 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 +1138,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 +1250,8 @@ 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 = language_hints @property def raw(self) -> RawTranscriptionConfig: @@ -1237,6 +1300,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 +2374,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..6358e264 --- /dev/null +++ b/tests/unit/test_language_hints.py @@ -0,0 +1,92 @@ +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"]}, + ], +) +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": ["fr"]}, + {"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" From 1cd824fd1d1f4ee2f0d071add0d3373319c226c4 Mon Sep 17 00:00:00 2001 From: Ryan Eloff Date: Thu, 10 Sep 2026 14:46:39 +0000 Subject: [PATCH 2/3] Allow target-only lists with a different default language --- assemblyai/types.py | 14 ++++++-------- tests/unit/test_language_hints.py | 3 ++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/assemblyai/types.py b/assemblyai/types.py index f5493b73..43149ed5 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -855,7 +855,7 @@ 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 containing that default. The server validates supported + ordered, unique list; the default may be outside it. The server validates supported codes; omitting this object leaves the system prompt unconditioned. """ @@ -872,8 +872,8 @@ def validate_default(cls, value): @field_validator("languages") @classmethod - def validate_languages(cls, value, info): - return cls._languages(value, info.data.get("default_language")) + def validate_languages(cls, value): + return cls._languages(value) else: @@ -885,8 +885,8 @@ def validate_default(cls, value): return cls._code(value) @validator("languages") - def validate_languages(cls, value, values): - return cls._languages(value, values.get("default_language")) + def validate_languages(cls, value): + return cls._languages(value) @staticmethod def _code(value): @@ -896,14 +896,12 @@ def _code(value): return code @classmethod - def _languages(cls, value, default): + 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") - if default is not None and default not in codes: - raise ValueError("languages must include default_language") return codes diff --git a/tests/unit/test_language_hints.py b/tests/unit/test_language_hints.py index 6358e264..378aa5c5 100644 --- a/tests/unit/test_language_hints.py +++ b/tests/unit/test_language_hints.py @@ -17,6 +17,8 @@ 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): @@ -69,7 +71,6 @@ def test_per_file_config_copies_and_clear(): {"default_language": ""}, {"default_language": "English"}, {"default_language": "en", "languages": []}, - {"default_language": "en", "languages": ["fr"]}, {"default_language": "en", "languages": ["en", "EN"]}, {"default_language": "en", "unknown": "value"}, ], From 495c5a0333b32dbe086283d36f7238e984b841d3 Mon Sep 17 00:00:00 2001 From: Ryan Eloff Date: Thu, 10 Sep 2026 15:40:26 +0000 Subject: [PATCH 3/3] Fix language hint constructor typing and bump SDK version --- assemblyai/__version__.py | 2 +- assemblyai/types.py | 6 +++++- tests/unit/test_language_hints.py | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) 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 43149ed5..9fadb199 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -1249,7 +1249,11 @@ def __init__( self.speech_understanding = speech_understanding self.domain = domain if language_hints is not None or raw_transcription_config is None: - self.language_hints = language_hints + self.language_hints = ( + LanguageHints(**language_hints) + if isinstance(language_hints, dict) + else language_hints + ) @property def raw(self) -> RawTranscriptionConfig: diff --git a/tests/unit/test_language_hints.py b/tests/unit/test_language_hints.py index 378aa5c5..ab7af97f 100644 --- a/tests/unit/test_language_hints.py +++ b/tests/unit/test_language_hints.py @@ -91,3 +91,11 @@ def test_raw_configuration_preserves_hints(): 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"]