Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,8 +449,8 @@ import assemblyai as aai
aai.settings.api_key = "<YOUR_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.",
Expand Down Expand Up @@ -790,8 +790,8 @@ aai.settings.api_key = "<YOUR_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
)

Expand Down
2 changes: 1 addition & 1 deletion assemblyai/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.5.1"
__version__ = "1.5.2"
17 changes: 13 additions & 4 deletions assemblyai/dictation/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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(
Expand Down
25 changes: 17 additions & 8 deletions assemblyai/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -2741,19 +2742,19 @@ 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
first, most recent last). Gives the model the dialogue that preceded this
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."""
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
32 changes: 27 additions & 5 deletions tests/unit/test_dictation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down
50 changes: 40 additions & 10 deletions tests/unit/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Loading