diff --git a/README.md b/README.md index 6330757c..20a56de9 100644 --- a/README.md +++ b/README.md @@ -449,8 +449,8 @@ import assemblyai as aai aai.settings.api_key = "" config = aai.SyncTranscriptionConfig( - prompt="Transcribe verbatim. Preserve disfluencies.", # max 4096 chars - keyterms_prompt=["AssemblyAI", "Universal", "U3-Pro"], # max 2048 chars total + prompt="Transcribe verbatim. Preserve disfluencies.", # max 6000 chars + keyterms_prompt=["AssemblyAI", "Universal", "U3-Pro"], # max 100 terms / 8000 chars conversation_context=[ # prior turns from the same conversation, oldest first "I'd like to book a flight to Denver.", @@ -790,8 +790,8 @@ aai.settings.api_key = "" config = aai.DictationConfig( language_codes=["en", "es"], # one code, or several for multilingual audio - stt_prompt="A doctor dictating a visit note.", # what the audio is about; ≤4096 chars - keyterms_prompt=["AssemblyAI", "Universal"], # bias the decoder; ≤2048 chars total + stt_prompt="A doctor dictating a visit note.", # what the audio is about; ≤6000 chars + keyterms_prompt=["AssemblyAI", "Universal"], # bias the decoder; ≤100 terms / 8000 chars llm_instruction="Fix punctuation only.", # optional LLM pass ) diff --git a/assemblyai/__version__.py b/assemblyai/__version__.py index 0f228f25..5197c5f5 100644 --- a/assemblyai/__version__.py +++ b/assemblyai/__version__.py @@ -1 +1 @@ -__version__ = "1.5.1" +__version__ = "1.5.2" diff --git a/assemblyai/dictation/v1/models.py b/assemblyai/dictation/v1/models.py index 7c689d5b..a0fc3886 100644 --- a/assemblyai/dictation/v1/models.py +++ b/assemblyai/dictation/v1/models.py @@ -50,9 +50,10 @@ def __init__( self.retry_after = retry_after -_DICTATION_MAX_KEYTERMS_PROMPT_LEN = 2048 +_DICTATION_MAX_KEYTERMS_PROMPT_LEN = 8000 +_DICTATION_MAX_KEYTERMS_COUNT = 100 _DICTATION_MAX_LLM_INSTRUCTION_LEN = 2048 -_DICTATION_MAX_STT_PROMPT_LEN = 4096 +_DICTATION_MAX_STT_PROMPT_LEN = 6000 class DictationConfig(BaseModel): @@ -95,10 +96,10 @@ class DictationConfig(BaseModel): about, e.g. "A doctor dictating a patient visit note." It describes the situation rather than instructing the model, and steers the decoder as it writes the transcript — where `llm_instruction` reshapes the transcript - afterwards. Max 4096 characters.""" + afterwards. Max 6000 characters.""" keyterms_prompt: Optional[List[str]] = None - "Keyterms biasing the decoder. Whitespace is stripped and empty terms dropped. Max 2048 characters total." + "Keyterms biasing the decoder. Whitespace is stripped and empty terms dropped. Max 100 terms / 8000 characters total." llm_instruction: Optional[str] = Field( default=None, max_length=_DICTATION_MAX_LLM_INSTRUCTION_LEN @@ -116,6 +117,10 @@ def _normalize_keyterms_prompt(cls, v): if not v: return None terms = [t.strip() for t in v if t and t.strip()] + if len(terms) > _DICTATION_MAX_KEYTERMS_COUNT: + raise ValueError( + f"keyterms_prompt exceeds {_DICTATION_MAX_KEYTERMS_COUNT} terms (got {len(terms)})" + ) total = sum(len(t) for t in terms) if total > _DICTATION_MAX_KEYTERMS_PROMPT_LEN: raise ValueError( @@ -133,6 +138,10 @@ def _normalize_keyterms_prompt(cls, v): if not v: return None terms = [t.strip() for t in v if t and t.strip()] + if len(terms) > _DICTATION_MAX_KEYTERMS_COUNT: + raise ValueError( + f"keyterms_prompt exceeds {_DICTATION_MAX_KEYTERMS_COUNT} terms (got {len(terms)})" + ) total = sum(len(t) for t in terms) if total > _DICTATION_MAX_KEYTERMS_PROMPT_LEN: raise ValueError( diff --git a/assemblyai/types.py b/assemblyai/types.py index 11ae861f..eb4c6c89 100644 --- a/assemblyai/types.py +++ b/assemblyai/types.py @@ -2690,10 +2690,11 @@ class ListTranscriptResponse(BaseModel): # Caps mirror the sync service's `config` part. `prompt` and `keyterms_prompt` # over their caps are rejected; `conversation_context` over its caps is # trimmed (oldest turns first), matching the server. -_SYNC_MAX_PROMPT_LEN = 4096 -_SYNC_MAX_KEYTERMS_PROMPT_LEN = 2048 -_SYNC_MAX_CONVERSATION_CONTEXT_TURNS = 100 -_SYNC_MAX_CONVERSATION_CONTEXT_LEN = 4096 +_SYNC_MAX_PROMPT_LEN = 6000 +_SYNC_MAX_KEYTERMS_PROMPT_LEN = 8000 +_SYNC_MAX_KEYTERMS_COUNT = 100 +_SYNC_MAX_CONVERSATION_CONTEXT_TURNS = 500 +_SYNC_MAX_CONVERSATION_CONTEXT_LEN = 16000 def _normalize_conversation_context(v): @@ -2741,10 +2742,10 @@ class SyncTranscriptionConfig(BaseModel): "The sync speech model to route to. Sent as the `X-AAI-Model` header." prompt: Optional[str] = Field(default=None, max_length=_SYNC_MAX_PROMPT_LEN) - "Custom transcription instruction prepended to the model's system prompt. Max 4096 characters." + "Custom transcription instruction prepended to the model's system prompt. Max 6000 characters." keyterms_prompt: Optional[List[str]] = None - "Keyterms biasing the decoder. Whitespace is stripped and empty terms dropped. Max 2048 characters total." + "Keyterms biasing the decoder. Whitespace is stripped and empty terms dropped. Max 100 terms / 8000 characters total." conversation_context: Optional[Union[str, List[str]]] = None """Prior turns from the same conversation, in chronological order (oldest @@ -2752,8 +2753,8 @@ class SyncTranscriptionConfig(BaseModel): audio so it transcribes the clip with better continuity and proper-noun consistency. Include turns from either side of the conversation (e.g. a voice agent's replies) as separate entries; entries carry no speaker labels. - A single string is accepted and treated as one turn. Capped at 100 turns / - 4096 characters total — over-cap context is trimmed (oldest turns dropped + A single string is accepted and treated as one turn. Capped at 500 turns / + 16000 characters total — over-cap context is trimmed (oldest turns dropped first), not rejected, and the oldest turns are likewise dropped first when the prompt exceeds the model token budget, so put the most recent turn last.""" @@ -2785,6 +2786,10 @@ def _normalize_keyterms_prompt(cls, v): if not v: return None terms = [t.strip() for t in v if t and t.strip()] + if len(terms) > _SYNC_MAX_KEYTERMS_COUNT: + raise ValueError( + f"keyterms_prompt exceeds {_SYNC_MAX_KEYTERMS_COUNT} terms (got {len(terms)})" + ) total = sum(len(t) for t in terms) if total > _SYNC_MAX_KEYTERMS_PROMPT_LEN: raise ValueError( @@ -2804,6 +2809,10 @@ def _normalize_keyterms_prompt(cls, v): if not v: return None terms = [t.strip() for t in v if t and t.strip()] + if len(terms) > _SYNC_MAX_KEYTERMS_COUNT: + raise ValueError( + f"keyterms_prompt exceeds {_SYNC_MAX_KEYTERMS_COUNT} terms (got {len(terms)})" + ) total = sum(len(t) for t in terms) if total > _SYNC_MAX_KEYTERMS_PROMPT_LEN: raise ValueError( diff --git a/tests/unit/test_dictation.py b/tests/unit/test_dictation.py index 6ae74a8b..0258f983 100644 --- a/tests/unit/test_dictation.py +++ b/tests/unit/test_dictation.py @@ -16,6 +16,11 @@ from pytest_httpx import HTTPXMock import assemblyai as aai +from assemblyai.dictation.v1.models import ( + _DICTATION_MAX_KEYTERMS_COUNT, + _DICTATION_MAX_KEYTERMS_PROMPT_LEN, + _DICTATION_MAX_STT_PROMPT_LEN, +) from assemblyai._multipart import _STREAM_READ_SIZE from assemblyai.dictation.v1 import api @@ -581,15 +586,32 @@ def test_transcribe_live_sends_stt_prompt(httpx_mock: HTTPXMock): def test_stt_prompt_too_long_raises(): - # Given an stt_prompt exceeding the 4096-char cap + # Given an stt_prompt exceeding the character cap with pytest.raises(ValueError): - aai.DictationConfig(stt_prompt="x" * 5000) + aai.DictationConfig(stt_prompt="x" * (_DICTATION_MAX_STT_PROMPT_LEN + 1)) def test_keyterms_prompt_too_long_raises(): - # Given a keyterms_prompt exceeding the 2048-char cap - with pytest.raises(ValueError, match="keyterms_prompt exceeds"): - aai.DictationConfig(keyterms_prompt=["x" * 3000]) + # Given a keyterms_prompt exceeding the character cap + with pytest.raises(ValueError, match="characters"): + aai.DictationConfig( + keyterms_prompt=["x" * (_DICTATION_MAX_KEYTERMS_PROMPT_LEN + 1)] + ) + + +def test_keyterms_prompt_too_many_terms_raises(): + # Given more terms than the count cap allows, each short enough that the + # character cap is not the binding constraint + with pytest.raises(ValueError, match="terms"): + aai.DictationConfig(keyterms_prompt=["t"] * (_DICTATION_MAX_KEYTERMS_COUNT + 1)) + + +def test_keyterms_prompt_at_the_count_cap_accepted(): + # Given exactly as many terms as the cap allows + config = aai.DictationConfig(keyterms_prompt=["t"] * _DICTATION_MAX_KEYTERMS_COUNT) + + # Then the cap is inclusive + assert len(config.keyterms_prompt) == _DICTATION_MAX_KEYTERMS_COUNT def test_llm_instruction_too_long_raises(): diff --git a/tests/unit/test_sync.py b/tests/unit/test_sync.py index 8ab98a3a..054101cc 100644 --- a/tests/unit/test_sync.py +++ b/tests/unit/test_sync.py @@ -3,6 +3,12 @@ from pytest_httpx import HTTPXMock import assemblyai as aai +from assemblyai.types import ( + _SYNC_MAX_CONVERSATION_CONTEXT_LEN, + _SYNC_MAX_CONVERSATION_CONTEXT_TURNS, + _SYNC_MAX_KEYTERMS_COUNT, + _SYNC_MAX_KEYTERMS_PROMPT_LEN, +) aai.settings.api_key = "test" @@ -147,25 +153,28 @@ def test_transcribe_coerces_conversation_context_string(httpx_mock: HTTPXMock): def test_conversation_context_trims_oldest_turns_over_char_cap(): - # Given conversation_context whose total length exceeds the 4096-char cap - config = aai.SyncTranscriptionConfig(conversation_context=["a" * 3000, "b" * 3000]) + # Given two turns that together exceed the character cap + half = _SYNC_MAX_CONVERSATION_CONTEXT_LEN // 2 + 1 + config = aai.SyncTranscriptionConfig(conversation_context=["a" * half, "b" * half]) # Then the oldest turn is dropped and the most recent turn is kept - assert config.conversation_context == ["b" * 3000] + assert config.conversation_context == ["b" * half] def test_conversation_context_trims_oldest_turns_over_turn_cap(): - # Given conversation_context with more than 100 turns - turns = [f"turn {i}" for i in range(120)] + # Given conversation_context with more turns than the cap allows + turns = [f"turn {i}" for i in range(_SYNC_MAX_CONVERSATION_CONTEXT_TURNS + 20)] config = aai.SyncTranscriptionConfig(conversation_context=turns) - # Then it is trimmed to the 100 most recent turns, oldest dropped first + # Then it is trimmed to the most recent capped-many, oldest dropped first assert config.conversation_context == turns[20:] def test_conversation_context_empties_when_single_turn_over_char_cap(): # Given a single turn that alone exceeds the character cap - config = aai.SyncTranscriptionConfig(conversation_context=["a" * 5000]) + config = aai.SyncTranscriptionConfig( + conversation_context=["a" * (_SYNC_MAX_CONVERSATION_CONTEXT_LEN + 1)] + ) # Then the context trims to nothing rather than raising assert config.conversation_context is None @@ -308,10 +317,31 @@ def test_transcribe_path_input(httpx_mock: HTTPXMock, tmp_path): def test_keyterms_prompt_too_long_raises(): - # Given a keyterms_prompt exceeding the 2048-char cap + # Given a keyterms_prompt exceeding the character cap # When building the config, Then validation fails immediately - with pytest.raises(ValueError, match="keyterms_prompt exceeds"): - aai.SyncTranscriptionConfig(keyterms_prompt=["x" * 3000]) + with pytest.raises(ValueError, match="characters"): + aai.SyncTranscriptionConfig( + keyterms_prompt=["x" * (_SYNC_MAX_KEYTERMS_PROMPT_LEN + 1)] + ) + + +def test_keyterms_prompt_too_many_terms_raises(): + # Given more terms than the count cap allows, each short enough that the + # character cap is not the binding constraint + with pytest.raises(ValueError, match="terms"): + aai.SyncTranscriptionConfig( + keyterms_prompt=["t"] * (_SYNC_MAX_KEYTERMS_COUNT + 1) + ) + + +def test_keyterms_prompt_at_the_count_cap_accepted(): + # Given exactly as many terms as the cap allows + config = aai.SyncTranscriptionConfig( + keyterms_prompt=["t"] * _SYNC_MAX_KEYTERMS_COUNT + ) + + # Then the cap is inclusive + assert len(config.keyterms_prompt) == _SYNC_MAX_KEYTERMS_COUNT def test_problem_details_envelope_maps_to_sync_transcript_error(