diff --git a/.fern/metadata.json b/.fern/metadata.json index 9210e15b..b4a8cece 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,7 +1,7 @@ { "cliVersion": "5.44.6", "generatorName": "fernapi/fern-python-sdk", - "generatorVersion": "5.14.18", + "generatorVersion": "5.23.0", "generatorConfig": { "client": { "class_name": "BaseClient", @@ -12,12 +12,13 @@ "use_typeddict_requests": true, "should_generate_websocket_clients": true, "enable_wire_tests": true, + "runtime_version": true, "pydantic_config": { "skip_validation": true } }, - "originGitCommit": "ff8fd2b74fdd5c081e9f111d59d3619f7e286dd8", + "originGitCommit": "03f06776bbb692c49f8d76c4230bca7f4bc4bca7", "originGitCommitIsDirty": true, "invokedBy": "manual", - "sdkVersion": "7.6.0" + "sdkVersion": "7.6.1" } \ No newline at end of file diff --git a/.fernignore b/.fernignore index a5763df0..e3cc7ae6 100644 --- a/.fernignore +++ b/.fernignore @@ -93,6 +93,15 @@ src/deepgram/types/deepgram_listen_provider_v2.py src/deepgram/agent/v1/types/agent_v1settings_agent_listen_provider.py src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider.py +# Backward-compat patch for the 2026-07-31 AgentV1UpdateListen provider retype. The +# generated `provider` field is now the required discriminated union +# AgentV1UpdateListenListenProvider (_V1/_V2, discriminant `version`). This generated +# model carries a hand-added model_validator(mode='before') / root_validator(pre=True) +# that coerces a legacy DeepgramListenProviderV1/V2 (or a dict lacking the version +# discriminant) into the new shape so existing callers keep working. Remove and unfreeze +# when the old provider payloads are retired in a future major. +src/deepgram/agent/v1/types/agent_v1update_listen_listen.py + # Hand-written compat shim recreating ListenV2CloseStreamType, which Fern removed in the # 2026-06-15 regen (docs #946). The original generated type wrongly allowed # Union[Literal["Finalize","CloseStream","KeepAlive"], Any] — v2 copied v1's control-message @@ -101,16 +110,6 @@ src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider.py # resurrecting the invalid values. Frozen so Fern won't delete it again. src/deepgram/listen/v2/types/listen_v2close_stream_type.py -# Backward-compat shim for the `stt_latency` removal from AgentV1LatencyReport (the -# 2026-07-20 regen; API change in deepgram-docs #1006). LatencyReport is a -# server-emitted (read-only) message, so re-adding the optional `stt_latency` field -# has no request/wire impact — it keeps `report.stt_latency` resolving (to None, since -# the server no longer emits it) instead of raising AttributeError, so the removal -# stays a minor release rather than a major break. Remove the field and unfreeze when -# it is intentionally retired in a future major release. -src/deepgram/agent/v1/types/agent_v1latency_report.py -src/deepgram/agent/v1/requests/agent_v1latency_report.py - # Package __init__.py files that carry hand-applied legacy alias re-exports for the # above shims. Fern would otherwise regenerate these and strip the legacy entries on # every regen. Frozen to preserve the public-import surface for renamed types/params. @@ -132,17 +131,23 @@ src/deepgram/listen/v2/types/__init__.py # Deepgram's websocket query strings. HTTP raw_clients hand params to httpx # directly and are unaffected; httpx accepts the pre-stringified values fine. # [temporarily frozen — manual patches listed above] -src/deepgram/core/query_encoder.py +# Credential redaction in the error types. Every websocket `connect()` path raises +# ApiError(headers=dict(headers), ...) with the FULL request headers, and both error +# types stringify their headers dict -- so an unredacted `Authorization` reached +# str(e), tracebacks, log aggregators and error trackers (which serialise attributes +# as well as the message). Both now mask credential values at construction via +# _secure_logging.redact_sensitive_headers, keeping non-sensitive headers +# (dg-request-id) for debugging. Same threat _secure_logging.py already covers for +# the websockets DEBUG handshake logs; this is the other path to it. Unfreeze if the +# generator starts redacting credentials itself. + +src/deepgram/core/api_error.py +src/deepgram/core/parse_error.py -# Carries `# x-release-please-version` annotations on the User-Agent and -# X-Fern-SDK-Version header lines so release-please bumps the wire telemetry -# version in lockstep with the published package version (it is a `generic` -# extra-file in .github/release-please-config.json). Fern regenerates this file -# from scratch and would strip the annotations, so it is frozen to protect them. -# [temporarily frozen — manual patches listed above] -src/deepgram/core/client_wrapper.py +src/deepgram/core/query_encoder.py # Hand-written custom tests +tests/custom/test_api_error_redaction.py tests/custom/test_agent_history.py tests/custom/test_agent_update_listen.py tests/custom/test_compat_aliases.py @@ -156,6 +161,7 @@ tests/custom/test_query_encoder.py tests/custom/test_secure_logging.py tests/custom/test_socket_client_shims.py tests/custom/test_speak_v2_connect_wire.py +tests/custom/test_speak_v2_interrupt_configure.py tests/custom/test_speak_v2_socket.py tests/custom/test_text_builder.py tests/custom/test_transport.py @@ -164,6 +170,13 @@ tests/typecheck/compat_aliases.py # Wire test with restored compatibility coverage for legacy create-key request alias tests/wire/test_manage_v1_projects_keys.py +# Wire test with restored query-parameter coverage for manage.v1.projects.requests.list. +# The 2026-08-11 regen simplified the upstream spec *example* for this endpoint, and Fern +# derives the wire test from the example, so the generated test dropped all ten optional +# query params (including the datetime -> ISO-8601 Z encoding) while the client signature +# still forwards them. Frozen so future regens do not silently drop that check again. +tests/wire/test_manage_v1_projects_requests.py + # Manual standalone tests tests/manual diff --git a/.github/release-please-config.json b/.github/release-please-config.json index c86c4914..dc3946e4 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -16,10 +16,6 @@ "path": "pyproject.toml", "jsonpath": "$.tool.poetry.version" }, - { - "type": "generic", - "path": "src/deepgram/core/client_wrapper.py" - }, { "type": "json", "path": ".fern/metadata.json", diff --git a/AGENTS.md b/AGENTS.md index 2d811fc6..4a680b38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,7 @@ Current permanently frozen files: - `tests/custom/test_compat_aliases.py` — hand-written regression test for backward-compatible alias imports after regen renames - `tests/custom/test_query_encoder.py` — hand-written regression test that `core/query_encoder.py` coerces Python bools to lowercase `"true"`/`"false"` before `urlencode` so websocket query strings stay wire-correct - `tests/custom/test_secure_logging.py` — hand-written regression test that the `websockets` Authorization-header DEBUG logs are redacted (API key never logged in clear text) +- `tests/custom/test_speak_v2_interrupt_configure.py` — hand-written coverage for the Speak V2 barge-in / mid-stream reconfigure surface (`send_interrupt`, `send_configure`, `SpeechInterrupted`, `ConfigureSuccess`/`ConfigureFailure`, and the `speed`/`expressivity` connect params) - `tests/custom/test_text_builder.py`, `tests/custom/test_transport.py` — hand-written tests - `tests/typecheck/compat_aliases.py` — hand-written mypy `assert_type` coverage for backward-compatible alias TypedDicts - `tests/manual/` — manual standalone tests @@ -53,17 +54,27 @@ How to identify: Current temporarily frozen files: - `src/deepgram/speak/v1/socket_client.py` — optional message param defaults, broad exception catch -- `src/deepgram/speak/v2/socket_client.py` — same (optional `send_flush`/`send_close` defaults, broad exception catch); new websocket TTS client added in the 2026-07-08 regen +- `src/deepgram/speak/v2/socket_client.py` — same (optional `send_flush`/`send_close`/`send_interrupt` defaults, broad exception catch); new websocket TTS client added in the 2026-07-08 regen. `send_interrupt` carries no required payload so it takes the same optional-default treatment as the other control sends; `send_configure` deliberately keeps its required argument (a Configure with no settings is meaningless) - `src/deepgram/listen/v1/socket_client.py` — same -- `src/deepgram/listen/v2/socket_client.py` — same + `send_configure` typing.Any/raw shim, response Union uses typing.Any instead of `ListenV2ConfigureSuccess` +- `src/deepgram/listen/v2/socket_client.py` — same (broad except, optional `send_close_stream` default). As of the 2026-08-11 regen the generator properly types `send_configure(ListenV2Configure)` and puts `ListenV2ConfigureSuccess` in the response Union, so those are taken from the generator; the only `send_configure` patch retained is runtime tolerance for a raw dict (sent verbatim) for back-compat with pre-typed-model callers - `src/deepgram/agent/v1/socket_client.py` — same + `_sanitize_numeric_types` - `src/deepgram/agent/v1/types/agent_v1settings_agent_context.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent.py`, `src/deepgram/agent/v1/types/agent_v1settings.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent_context.py`, `src/deepgram/agent/v1/requests/agent_v1settings_agent.py`, `src/deepgram/agent/v1/requests/agent_v1settings.py` — backward-compat patches for the 2026-05-05 Agent Settings schema restructure. These preserve callable `AgentV1SettingsAgent(...)`, keep `AgentV1Settings.agent` accepting both that wrapper and `agent_id` strings, restore the legacy request TypedDict shapes, remap legacy `messages=[...]` / nested `context=AgentV1SettingsAgentContext(messages=[...])` usage into the new `context={"messages": [...]}` wire shape, and keep read-side `obj.messages` access working. +- `src/deepgram/core/api_error.py`, `src/deepgram/core/parse_error.py` — credential redaction. Every websocket `connect()` path raises `ApiError(headers=dict(headers), ...)` with the full request headers, and both error types stringify that dict, so an unredacted `Authorization` reached `str(e)`, tracebacks, log aggregators and error trackers (which serialise attributes as well as the message). Both now mask credential values at construction via `_secure_logging.redact_sensitive_headers`, preserving non-sensitive headers (`dg-request-id`) for debugging. This is the same threat `_secure_logging.py` covers for the `websockets` DEBUG handshake logs, via the other path to it. Regression coverage in `tests/custom/test_api_error_redaction.py`. Unfreeze if the generator starts redacting credentials itself. - `src/deepgram/core/query_encoder.py` — coerces Python bools to lowercase `"true"`/`"false"` before they reach `urllib.parse.urlencode` (which would otherwise produce `"True"`/`"False"` via `str()` and break websocket query strings). Only the four `*/connect()` paths call `urlencode`; HTTP raw clients hand params to httpx, which lowercases bools itself, so the patch is a no-op for the HTTP path. Once Fern's websocket codegen normalizes bools (or the spec types these as `boolean` end-to-end), this can be unfrozen. -- `src/deepgram/core/client_wrapper.py` — carries `# x-release-please-version` annotations on the `User-Agent` and `X-Fern-SDK-Version` header lines. `client_wrapper.py` is listed as a `generic` extra-file in `.github/release-please-config.json`, but the generic updater only rewrites annotated lines, so without these comments release-please never bumps the wire telemetry version and it drifts from the published package version (e.g. main shipping 7.4.0 while sending 7.3.2). Fern regenerates this file from scratch and would strip the annotations, so it is frozen to protect them. On regen, re-apply the two annotation comments to the freshly generated header lines. Unfreeze only if Fern's generator starts emitting the annotations itself. +- `src/deepgram/types/deepgram_listen_provider_v2.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_listen_provider.py`, `src/deepgram/agent/v1/types/agent_v1settings_agent_context_listen_provider.py` — behavioural back-compat shim for the `language_hint` -> `language_hints` rename (2026-06-15 regen). The public field was historically (incorrectly) singular and accepted a str or a list; the API field is `language_hints` (a list, and the server uses `deny_unknown_fields` so the singular key is rejected on the wire). Each carries a hand-added `model_validator(mode='before')` / `root_validator(pre=True)` that remaps a legacy `language_hint=` kwarg and drops the dead singular key. Remove and unfreeze when the singular alias is retired in a future major. +- `src/deepgram/agent/v1/types/agent_v1update_listen_listen.py` — backward-compat patch for the 2026-07-31 `AgentV1UpdateListen` provider retype. The `provider` field changed from a bare `DeepgramListenProviderV2` to the required discriminated union `AgentV1UpdateListenListenProvider` (`_V1`/`_V2`, discriminant `version`). Carries a hand-added `model_validator(mode='before')` / `root_validator(pre=True)` that coerces a legacy `DeepgramListenProviderV1`/`V2` (or a dict lacking the `version` discriminant) into the new shape so existing callers keep working. Remove and unfreeze when the old provider payloads are retired in a future major. NOTE: this patch was silently lost once (it was absent from `.fernignore`, so a regen overwrote it) — keep it frozen. - `tests/wire/test_manage_v1_projects_keys.py` — restored wire coverage for the legacy `CreateKeyV1RequestOneParams` request alias so future regens do not silently drop that compatibility check +- `tests/wire/test_manage_v1_projects_requests.py` — restored query-parameter coverage for `manage.v1.projects.requests.list`. The 2026-08-11 regen simplified the upstream spec *example*, and Fern derives the wire test from the example, so all ten optional query params (and the `datetime` → ISO-8601 `Z` encoding) lost their assertions while the client signature still forwarded them. Frozen for the same reason as the `_keys.py` entry above. - `src/deepgram/__init__.py`, `src/deepgram/agent/__init__.py`, `src/deepgram/agent/v1/__init__.py`, `src/deepgram/agent/v1/types/__init__.py`, `src/deepgram/agent/v1/requests/__init__.py`, `src/deepgram/types/__init__.py`, `src/deepgram/requests/__init__.py` — package `__init__.py` files carrying hand-applied legacy alias re-exports for `CreateKeyV1RequestOne`, `AgentV1HistoryContent`, `AgentV1HistoryFunctionCalls`, `AgentV1SettingsAgentContextMessagesItemContent`, `AgentV1SettingsAgentContextMessagesItemFunctionCalls` (and their `*Params` variants). Fern would otherwise regenerate these and strip the legacy entries. After unfreezing for the next regen and reviewing the new generated content, re-apply the legacy re-exports plus any genuine new entries Fern added. - `src/deepgram/listen/__init__.py`, `src/deepgram/listen/v2/__init__.py`, `src/deepgram/listen/v2/types/__init__.py` — `__init__.py` files re-exporting the hand-written `ListenV2CloseStreamType` shim. Frozen so Fern won't strip the re-export on regen. Same handling as the package `__init__.py` files above: after unfreezing, re-apply the `ListenV2CloseStreamType` re-export plus any genuine new entries Fern added. +### Unfrozen files with residual manual concerns + +Files Fern now owns outright, but that carry a caveat worth knowing before the next regen: + +- `src/deepgram/core/client_wrapper.py` — unfrozen in the 2026-08-11 regen. The generator's `runtime_version: true` mode now derives the wire version from `importlib.metadata.version("deepgram-sdk")`, which supersedes the old hand-applied `# x-release-please-version` annotations (its `generic` entry was removed from `.github/release-please-config.json` at the same time, since with the annotations gone it was a no-op). **Caveat:** the `except PackageNotFoundError` fallback is a hardcoded version literal that nothing bumps — release-please's `extra-files` now covers only `pyproject.toml` and `.fern/metadata.json`. It only applies where package metadata is undiscoverable (source checkout on `sys.path`, vendored copy, zipapp, some frozen bundles), and it is refreshed only by the next regen. Accepted deliberately rather than re-freezing the file; if that fallback ever needs to be accurate on those paths, re-add a release-please `generic` entry with an annotation on the fallback line. +- `src/deepgram/agent/v1/types/agent_v1latency_report.py` and its `requests/` twin — unfrozen in the 2026-08-11 regen because the spec restored `stt_latency` natively, making the hand-added read-side field redundant. `tests/custom/test_latency_report_stt_compat.py` stays frozen and still guards the field's presence, so a future spec removal is caught rather than silently re-breaking readers. + ### Prepare repo for regeneration 1. **Create a new branch** off `main` named `lo/sdk-gen-`. diff --git a/examples/26-text-to-speech-barge-in-v2.py b/examples/26-text-to-speech-barge-in-v2.py new file mode 100644 index 00000000..c8c70ed0 --- /dev/null +++ b/examples/26-text-to-speech-barge-in-v2.py @@ -0,0 +1,112 @@ +""" +Example: Barge-in and mid-stream reconfiguration on the Speak V2 (Flux) WebSocket + +Two capabilities that need bidirectional timing, so this example is async -- +`start_listening()` blocks in sync mode, which leaves no way to send a message +while audio is still streaming back. + +1. Barge-in (`send_interrupt`). Stops the current turn. Pass a `playback_offset` + with how much audio the user has actually *heard* and the server replies with + a `SpeechInterrupted` telling you which words were spoken and which were not + -- exactly what you need to keep a conversation transcript honest. Two rules + worth knowing: the offset is cumulative from the start of the session (not the + start of the turn), and each `Interrupt` must advance past the previous one. + Omit the offset and you still stop the turn, but `text_spoken` / + `text_remaining` come back empty because the server cannot know where you were. + +2. Mid-stream reconfiguration (`send_configure`). Changes the speech rate without + reconnecting. The server acknowledges with `ConfigureSuccess` (echoing what it + applied) or a typed `ConfigureFailure` -- the SDK does not range-check `speed`, + so an out-of-range value comes back as `SPEED_OUT_OF_RANGE` rather than raising + locally. +""" + +import asyncio + +from dotenv import load_dotenv + +load_dotenv() + +from deepgram import AsyncDeepgramClient +from deepgram.speak.v2.types import ( + SpeakV2Configure, + SpeakV2Interrupt, + SpeakV2InterruptPlaybackOffset, + SpeakV2Speak, +) + +# 24 kHz linear16 mono: 2 bytes per sample, so 48 bytes per millisecond. +BYTES_PER_MS = 24000 * 2 // 1000 + +TEXT = ( + "This sentence is deliberately long so that audio is still streaming when the " + "interrupt is sent, which is what makes the barge-in observable." +) + + +async def main() -> None: + client = AsyncDeepgramClient() + + async with client.speak.v2.connect( + model="flux-alexis-en", + encoding="linear16", + sample_rate="24000", + speed=1.0, + ) as connection: + audio_bytes = 0 + interrupted = asyncio.Event() + + async def consume() -> None: + nonlocal audio_bytes + async for message in connection: + if isinstance(message, bytes): + audio_bytes += len(message) + continue + + msg_type = getattr(message, "type", None) + + if msg_type == "ConfigureSuccess": + print(f"Configure applied: speed={getattr(message.applied, 'speed', None)}") + elif msg_type == "ConfigureFailure": + # e.g. SPEED_OUT_OF_RANGE / SPEED_INCREMENT_INVALID + print(f"Configure rejected [{message.code}]: {message.description}") + elif msg_type == "SpeechInterrupted": + print(f"Interrupted after {message.audio_played_ms} ms of audio") + print(f" spoken : {message.text_spoken!r}") + print(f" remaining: {message.text_remaining!r}") + interrupted.set() + else: + print(f"Received {msg_type} event") + + consumer = asyncio.create_task(consume()) + + # Mid-stream speed change, acknowledged by the server. + await connection.send_configure(SpeakV2Configure(speed=1.05)) + + await connection.send_speak(SpeakV2Speak(text=TEXT)) + + # Let some audio actually stream back, then barge in at the point the + # listener had reached. Offset is cumulative from session start. + await asyncio.sleep(2) + played_ms = audio_bytes // BYTES_PER_MS + print(f"Barging in at {played_ms} ms ({audio_bytes} bytes received)") + await connection.send_interrupt( + SpeakV2Interrupt( + playback_offset=SpeakV2InterruptPlaybackOffset(type="time_ms", value=played_ms) + ) + ) + + try: + await asyncio.wait_for(interrupted.wait(), timeout=10) + except asyncio.TimeoutError: + print("No SpeechInterrupted received before the timeout") + + await connection.send_close() + consumer.cancel() + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except Exception as e: + print(f"Error: {e}") diff --git a/poetry.lock b/poetry.lock index 04674910..d6772d99 100644 --- a/poetry.lock +++ b/poetry.lock @@ -15,132 +15,132 @@ files = [ [[package]] name = "aiohttp" -version = "3.14.1" +version = "3.14.3" description = "Async http client/server framework (asyncio)" optional = true python-versions = ">=3.10" groups = ["main"] markers = "extra == \"aiohttp\"" files = [ - {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8f6bb621e5863cfe8fe5ff5468002d200ec31f30f1280b259dc505b02595099e"}, - {file = "aiohttp-3.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f7215cb3933784f79ed20e5f050e15984f390424339b22375d5a53c933a0491"}, - {file = "aiohttp-3.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9d4e294455b23a68c9b8f042d0e8e377a265bcb15332753695f6e5b6819e0ce"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b238af795833d5731d049d82bc84b768ae6f8f97f0495963b3ed9935c5901cc3"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4e5e0ae56914ecdbf446493addefc0159053dd53962cef37d7839f37f73d505"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:092e4ce3619a7c6dee52a6bdabda973d9b34b66781f840ce93c7e0cec30cf521"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb33777ea21e8b7ecde0e6fc84f598be0a1192eab1a63bc746d75aa75d38e7bd"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23119f8fd4f5d16902ed459b63b100bcd269628075162bddac56cc7b5273b3fb"}, - {file = "aiohttp-3.14.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57fc6745a4b7d0f5a9eb4f40a69718be6c0bc1b8368cc9fe89e90118719f4f42"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6fd35beba67c4183b09375c5fff9accb47524191a244a99f95fd4472f5402c2b"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:672b9d65f42eb877f5c3f234a4547e4e1a226ca8c2eed879bb34670a0ce51192"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:24ba13339fed9251d9b1a1bec8c7ab84c0d1675d79d33501e11f94f8b9a84e05"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:94da27378da0610e341c4d30de29a191672683cc82b8f9556e8f7c7212a020fe"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:52cdac9432d8b4a719f35094a818d95adcae0f0b4fe9b9b921909e0c87de9e7d"}, - {file = "aiohttp-3.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:672ac254412a24d0d0cf00a9e6c238877e4be5e5fa2d188832c1244f45f31966"}, - {file = "aiohttp-3.14.1-cp310-cp310-win32.whl", hash = "sha256:2fe3607e71acc6ebb0ec8e492a247bf7a291226192dc0084236dfc12478916f6"}, - {file = "aiohttp-3.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:30099eda75a53c32efb0920e9c33c195314d2cc1c680fbfd30894932ac5f27df"}, - {file = "aiohttp-3.14.1-cp310-cp310-win_arm64.whl", hash = "sha256:5a837f49d901f9e368651b676912bff1104ed8c1a83b280bcd7b29adccef5c9c"}, - {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2"}, - {file = "aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f"}, - {file = "aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730"}, - {file = "aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85"}, - {file = "aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3"}, - {file = "aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126"}, - {file = "aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5"}, - {file = "aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b"}, - {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480"}, - {file = "aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d"}, - {file = "aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c"}, - {file = "aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087"}, - {file = "aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3"}, - {file = "aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4"}, - {file = "aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271"}, - {file = "aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847"}, - {file = "aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178"}, - {file = "aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf"}, - {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd"}, - {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe"}, - {file = "aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4"}, - {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876"}, - {file = "aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da"}, - {file = "aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451"}, - {file = "aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345"}, - {file = "aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588"}, - {file = "aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780"}, - {file = "aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a"}, - {file = "aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8"}, - {file = "aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15"}, - {file = "aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8"}, - {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3"}, - {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba"}, - {file = "aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397"}, - {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448"}, - {file = "aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004"}, - {file = "aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602"}, - {file = "aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95"}, - {file = "aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444"}, - {file = "aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0"}, - {file = "aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719"}, - {file = "aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec"}, - {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2"}, - {file = "aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340"}, - {file = "aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3"}, - {file = "aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d"}, - {file = "aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c"}, - {file = "aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365"}, - {file = "aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9"}, - {file = "aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6"}, - {file = "aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, ] [package.dependencies] @@ -176,14 +176,14 @@ typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.0" description = "Reusable constraint types to use with typing.Annotated" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, ] [[package]] @@ -247,14 +247,14 @@ files = [ [[package]] name = "certifi" -version = "2026.6.17" +version = "2026.7.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main", "dev"] files = [ - {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, - {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, ] [[package]] @@ -609,15 +609,15 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "httpx-aiohttp" -version = "0.1.8" +version = "0.1.12" description = "Aiohttp transport for HTTPX" optional = true python-versions = ">=3.8" groups = ["main"] markers = "extra == \"aiohttp\"" files = [ - {file = "httpx_aiohttp-0.1.8-py3-none-any.whl", hash = "sha256:b7bd958d1331f3759a38a0ba22ad29832cb63ca69498c17735228055bf78fa7e"}, - {file = "httpx_aiohttp-0.1.8.tar.gz", hash = "sha256:756c5e74cdb568c3248ba63fe82bfe8bbe64b928728720f7eaac64b3cf46f308"}, + {file = "httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141"}, + {file = "httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c"}, ] [package.dependencies] @@ -879,14 +879,14 @@ files = [ [[package]] name = "packaging" -version = "26.2" +version = "26.3" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, - {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, + {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, + {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, ] [[package]] @@ -1410,14 +1410,14 @@ files = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20260716" +version = "2.9.0.20260807" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "types_python_dateutil-2.9.0.20260716-py3-none-any.whl", hash = "sha256:1ae41d51a5c5f6bbeeb7f1f34df7086d55ff1605cf88d2ed71a7a276e1a7794e"}, - {file = "types_python_dateutil-2.9.0.20260716.tar.gz", hash = "sha256:1d55d1c3024bdb4861bb6a6622c9ec800c433d87bdc5b16fb84cdd0eed4ef2cb"}, + {file = "types_python_dateutil-2.9.0.20260807-py3-none-any.whl", hash = "sha256:54aa3707350ed7a9cc0776fd2f6739679d6967d11b40150985e81edcb86df4db"}, + {file = "types_python_dateutil-2.9.0.20260807.tar.gz", hash = "sha256:e0b8a90d464c8684c66b7b8e4556d9074afdddcc56ca45323f0987134f9e7034"}, ] [[package]] @@ -1449,18 +1449,18 @@ files = [ [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.3" description = "Runtime typing introspection tools" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, - {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, + {file = "typing_inspection-0.4.3-py3-none-any.whl", hash = "sha256:5f42b23858a91e0b4ef521f5418f03a0da3c9216fd2995ef5e73463100e676cd"}, + {file = "typing_inspection-0.4.3.tar.gz", hash = "sha256:c5f9ec1530b5c1e2c9bc34a84d9a3466ed1b2f3f2fa9f901368d9c5596210e4d"}, ] [package.dependencies] -typing-extensions = ">=4.12.0" +typing-extensions = ">=4.15.0" [[package]] name = "urllib3" @@ -1725,4 +1725,4 @@ aiohttp = ["aiohttp", "httpx-aiohttp"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "9720594db9f54f592921cb5f95880967c27258ad9fc3ddde5f3f9f174cfb0044" +content-hash = "da81cd862a1fa855037469ff02370744d0fdb4d757a53d9bae377f971e6f0fd2" diff --git a/pyproject.toml b/pyproject.toml index c56fdd2e..41703540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "deepgram-sdk" -version = "7.6.0" +version = "7.6.1" description = "" readme = "README.md" authors = [] @@ -38,9 +38,9 @@ Repository = 'https://github.com/deepgram/deepgram-python-sdk' [tool.poetry.dependencies] python = "^3.10" -aiohttp = { version = ">=3.14.0,<4", optional = true, python = ">=3.10"} +aiohttp = { version = ">=3.14.1,<4", optional = true, python = ">=3.10"} httpx = ">=0.21.2" -httpx-aiohttp = { version = "0.1.8", optional = true, python = ">=3.10"} +httpx-aiohttp = { version = "^0.1.8", optional = true, python = ">=3.10"} pydantic = ">= 1.9.2" pydantic-core = ">=2.18.2,<3.0.0" typing_extensions = ">= 4.0.0" diff --git a/reference.md b/reference.md index 05e739af..bfd7e5ad 100644 --- a/reference.md +++ b/reference.md @@ -5398,6 +5398,17 @@ asyncio.run(main()) +
+
+ +**`send_configure(message: ListenV2Configure)`** — Update the transcription configuration mid-stream + +- `connection.send_configure(ListenV2Configure(...))` — Acknowledged by a `ListenV2ConfigureSuccess` (or `ListenV2ConfigureFailure`) on the response stream +- A raw `dict` is also accepted and sent verbatim, for back-compat with callers written before this message was typed + +
+
+ @@ -5485,6 +5496,14 @@ asyncio.run(main()) +
+
+ +**redact:** `typing.Optional[ListenV2Redact]` — Redact sensitive information from transcripts (`numbers`, `aggressive_numbers`) + +
+
+
@@ -5974,9 +5993,30 @@ asyncio.run(main()) - `connection.send_flush()` +
+
+
+
+ +**`send_interrupt(message: SpeakV2Interrupt = None)`** — Stop the current turn (barge-in) + +- `connection.send_interrupt()` — Stop immediately +- `connection.send_interrupt(SpeakV2Interrupt(playback_offset=SpeakV2InterruptPlaybackOffset(type="time_ms", value=1500)))` — Report how much audio the listener actually heard, so the returned `SpeakV2SpeechInterrupted` reports which words were spoken (`text_spoken`) and which were not (`text_remaining`). The offset is cumulative from the start of the session, and each interrupt must advance past the previous one. +
+
+
+ +**`send_configure(message: SpeakV2Configure)`** — Change the speech rate mid-stream + +- `connection.send_configure(SpeakV2Configure(speed=1.05))` — Acknowledged by a `SpeakV2ConfigureSuccess` echoing what was applied, or a `SpeakV2ConfigureFailure` carrying a typed code such as `SPEED_OUT_OF_RANGE` + +
+
+ +
@@ -6017,6 +6057,22 @@ asyncio.run(main())
+
+
+ +**speed:** `typing.Optional[SpeakV2Speed]` — Speech-rate multiplier. `1.0` is the model's nominal rate. Accepted values: `0.85`, `0.90`, `0.95`, `1.00`, `1.05`, `1.10`, `1.15` — any other value is rejected with `SPEED_OUT_OF_RANGE` or `SPEED_INCREMENT_INVALID` + +
+
+ +
+
+ +**expressivity:** `typing.Optional[SpeakV2Expressivity]` — Expressive range of the generated speech. Accepted values: `-2`, `-1`, `0`, `1`, `2` — `0` is the voice's nominal delivery, negative is flatter and more restrained, positive is more animated + +
+
+
diff --git a/src/deepgram/__init__.py b/src/deepgram/__init__.py index 98b9b5ad..52de45c1 100644 --- a/src/deepgram/__init__.py +++ b/src/deepgram/__init__.py @@ -6,6 +6,12 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .types import ( + GoogleThinkProviderVersion, + ListenV2Redact, + SpeakV2Expressivity, + SpeakV2Speed, + ) from .types import ( AgentConfigurationV1, AgentThinkModelsV1Response, @@ -431,6 +437,10 @@ ) from .version import __version__ _dynamic_imports: typing.Dict[str, str] = { + "GoogleThinkProviderVersion": ".types", + "ListenV2Redact": ".types", + "SpeakV2Expressivity": ".types", + "SpeakV2Speed": ".types", "AgentConfigurationV1": ".types", "AgentConfigurationV1Params": ".requests", "AgentThinkModelsV1Response": ".types", @@ -885,6 +895,10 @@ def __dir__(): __all__ = [ + "GoogleThinkProviderVersion", + "ListenV2Redact", + "SpeakV2Expressivity", + "SpeakV2Speed", "AgentConfigurationV1", "AgentConfigurationV1Params", "AgentThinkModelsV1Response", diff --git a/src/deepgram/_secure_logging.py b/src/deepgram/_secure_logging.py index 7b15d2f6..32ab58c5 100644 --- a/src/deepgram/_secure_logging.py +++ b/src/deepgram/_secure_logging.py @@ -22,7 +22,10 @@ import typing # Header names (lower-cased) whose values must never be logged in clear text. -_SENSITIVE_HEADERS = frozenset({"authorization", "proxy-authorization"}) +# ``sec-websocket-protocol`` carries the API key in the "custom headers not supported" +# auth pattern the connect() docstrings recommend (value is ``"token, "``), so its +# value is a credential too. ``_mask_value`` renders that as ``"token, [REDACTED]"``. +_SENSITIVE_HEADERS = frozenset({"authorization", "proxy-authorization", "sec-websocket-protocol"}) # Replacement for the credential portion of a sensitive header value. _REDACTED = "[REDACTED]" @@ -46,6 +49,31 @@ def _mask_value(value: typing.Any) -> str: return _REDACTED +def redact_sensitive_headers( + headers: typing.Optional[typing.Mapping[str, str]], +) -> typing.Optional[typing.Dict[str, str]]: + """Return a copy of ``headers`` with credential values masked. + + Used by the error types in ``core/`` so a failed request can never carry the + API key into an exception message. Non-sensitive headers are preserved intact + because they carry real debugging value (``dg-request-id`` in particular). + + ``None`` in, ``None`` out, so callers can pass an optional header mapping + straight through. + """ + if headers is None: + return None + try: + return { + name: (_mask_value(value) if isinstance(name, str) and name.lower() in _SENSITIVE_HEADERS else value) + for name, value in headers.items() + } + except Exception: + # Redaction must never be the reason an error path fails. If the mapping + # is not iterable as expected, drop it entirely rather than risk leaking. + return None + + class RedactCredentialsFilter(logging.Filter): """Masks sensitive header values in ``websockets`` handshake debug records. diff --git a/src/deepgram/agent/__init__.py b/src/deepgram/agent/__init__.py index 410c3fd0..554532fb 100644 --- a/src/deepgram/agent/__init__.py +++ b/src/deepgram/agent/__init__.py @@ -6,6 +6,14 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .v1 import ( + AgentV1UpdateListenListenProvider, + AgentV1UpdateListenListenProviderParams, + AgentV1UpdateListenListenProvider_V1, + AgentV1UpdateListenListenProvider_V1Params, + AgentV1UpdateListenListenProvider_V2, + AgentV1UpdateListenListenProvider_V2Params, + ) from . import v1 from .v1 import ( AgentV1AgentAudioDone, @@ -150,6 +158,12 @@ FunctionCallHistoryMessageParams, ) _dynamic_imports: typing.Dict[str, str] = { + "AgentV1UpdateListenListenProvider": ".v1", + "AgentV1UpdateListenListenProviderParams": ".v1", + "AgentV1UpdateListenListenProvider_V1": ".v1", + "AgentV1UpdateListenListenProvider_V1Params": ".v1", + "AgentV1UpdateListenListenProvider_V2": ".v1", + "AgentV1UpdateListenListenProvider_V2Params": ".v1", "AgentV1AgentAudioDone": ".v1", "AgentV1AgentAudioDoneParams": ".v1", "AgentV1AgentStartedSpeaking": ".v1", @@ -316,6 +330,12 @@ def __dir__(): __all__ = [ + "AgentV1UpdateListenListenProvider", + "AgentV1UpdateListenListenProviderParams", + "AgentV1UpdateListenListenProvider_V1", + "AgentV1UpdateListenListenProvider_V1Params", + "AgentV1UpdateListenListenProvider_V2", + "AgentV1UpdateListenListenProvider_V2Params", "AgentV1AgentAudioDone", "AgentV1AgentAudioDoneParams", "AgentV1AgentStartedSpeaking", diff --git a/src/deepgram/agent/v1/__init__.py b/src/deepgram/agent/v1/__init__.py index e40c7a5e..5530528c 100644 --- a/src/deepgram/agent/v1/__init__.py +++ b/src/deepgram/agent/v1/__init__.py @@ -6,6 +6,16 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .types import ( + AgentV1UpdateListenListenProvider, + AgentV1UpdateListenListenProvider_V1, + AgentV1UpdateListenListenProvider_V2, + ) + from .requests import ( + AgentV1UpdateListenListenProviderParams, + AgentV1UpdateListenListenProvider_V1Params, + AgentV1UpdateListenListenProvider_V2Params, + ) from .types import ( AgentV1AgentAudioDone, AgentV1AgentStartedSpeaking, @@ -152,6 +162,12 @@ FunctionCallHistoryMessageParams, ) _dynamic_imports: typing.Dict[str, str] = { + "AgentV1UpdateListenListenProvider": ".types", + "AgentV1UpdateListenListenProviderParams": ".requests", + "AgentV1UpdateListenListenProvider_V1": ".types", + "AgentV1UpdateListenListenProvider_V1Params": ".requests", + "AgentV1UpdateListenListenProvider_V2": ".types", + "AgentV1UpdateListenListenProvider_V2Params": ".requests", "AgentV1AgentAudioDone": ".types", "AgentV1AgentAudioDoneParams": ".requests", "AgentV1AgentStartedSpeaking": ".types", @@ -318,6 +334,12 @@ def __dir__(): __all__ = [ + "AgentV1UpdateListenListenProvider", + "AgentV1UpdateListenListenProviderParams", + "AgentV1UpdateListenListenProvider_V1", + "AgentV1UpdateListenListenProvider_V1Params", + "AgentV1UpdateListenListenProvider_V2", + "AgentV1UpdateListenListenProvider_V2Params", "AgentV1AgentAudioDone", "AgentV1AgentAudioDoneParams", "AgentV1AgentStartedSpeaking", diff --git a/src/deepgram/agent/v1/requests/__init__.py b/src/deepgram/agent/v1/requests/__init__.py index 82a3d46a..180e1675 100644 --- a/src/deepgram/agent/v1/requests/__init__.py +++ b/src/deepgram/agent/v1/requests/__init__.py @@ -6,6 +6,11 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .agent_v1update_listen_listen_provider import ( + AgentV1UpdateListenListenProviderParams, + AgentV1UpdateListenListenProvider_V1Params, + AgentV1UpdateListenListenProvider_V2Params, + ) from .agent_v1agent_audio_done import AgentV1AgentAudioDoneParams from .agent_v1agent_started_speaking import AgentV1AgentStartedSpeakingParams from .agent_v1agent_thinking import AgentV1AgentThinkingParams @@ -89,6 +94,9 @@ from .conversation_history_message import ConversationHistoryMessageParams from .function_call_history_message import FunctionCallHistoryMessageParams _dynamic_imports: typing.Dict[str, str] = { + "AgentV1UpdateListenListenProviderParams": ".agent_v1update_listen_listen_provider", + "AgentV1UpdateListenListenProvider_V1Params": ".agent_v1update_listen_listen_provider", + "AgentV1UpdateListenListenProvider_V2Params": ".agent_v1update_listen_listen_provider", "AgentV1AgentAudioDoneParams": ".agent_v1agent_audio_done", "AgentV1AgentStartedSpeakingParams": ".agent_v1agent_started_speaking", "AgentV1AgentThinkingParams": ".agent_v1agent_thinking", @@ -180,6 +188,9 @@ def __dir__(): __all__ = [ + "AgentV1UpdateListenListenProviderParams", + "AgentV1UpdateListenListenProvider_V1Params", + "AgentV1UpdateListenListenProvider_V2Params", "AgentV1AgentAudioDoneParams", "AgentV1AgentStartedSpeakingParams", "AgentV1AgentThinkingParams", diff --git a/src/deepgram/agent/v1/requests/agent_v1latency_report.py b/src/deepgram/agent/v1/requests/agent_v1latency_report.py index e62a6caf..bbbd696d 100644 --- a/src/deepgram/agent/v1/requests/agent_v1latency_report.py +++ b/src/deepgram/agent/v1/requests/agent_v1latency_report.py @@ -11,13 +11,9 @@ class AgentV1LatencyReportParams(typing_extensions.TypedDict): Message type identifier for the latency report """ - # Backward-compat: mirrors the read-side shim in types/agent_v1latency_report.py. - # The API spec removed `stt_latency` (deepgram-docs #1006); kept here so the - # public request TypedDict shape is unchanged. No wire impact — the client never - # sends a LatencyReport. Remove and unfreeze in a future major. stt_latency: typing_extensions.NotRequired[float] """ - Deprecated. Speech-to-text latency, no longer reported by the server. + Speech-to-text: time from audio received to transcript produced, in seconds """ ttt_token_latency: typing_extensions.NotRequired[float] diff --git a/src/deepgram/agent/v1/requests/agent_v1update_listen.py b/src/deepgram/agent/v1/requests/agent_v1update_listen.py index 7309bc9e..2fa82d00 100644 --- a/src/deepgram/agent/v1/requests/agent_v1update_listen.py +++ b/src/deepgram/agent/v1/requests/agent_v1update_listen.py @@ -14,5 +14,5 @@ class AgentV1UpdateListenParams(typing_extensions.TypedDict): listen: AgentV1UpdateListenListenParams """ - Listen configuration to update. Contains a provider object with the same schema as Settings. The provider identity (type, version, model) is required and must match the current session. + Listen configuration to update. Contains a provider object with the same schema as Settings. The model and language can be changed mid-session. Keyterms can only be updated mid-session for Flux models. """ diff --git a/src/deepgram/agent/v1/requests/agent_v1update_listen_listen.py b/src/deepgram/agent/v1/requests/agent_v1update_listen_listen.py index 2a91d9f5..fbd4518c 100644 --- a/src/deepgram/agent/v1/requests/agent_v1update_listen_listen.py +++ b/src/deepgram/agent/v1/requests/agent_v1update_listen_listen.py @@ -1,12 +1,12 @@ # This file was auto-generated by Fern from our API Definition. import typing_extensions -from ....requests.deepgram_listen_provider_v2 import DeepgramListenProviderV2Params +from .agent_v1update_listen_listen_provider import AgentV1UpdateListenListenProviderParams class AgentV1UpdateListenListenParams(typing_extensions.TypedDict): """ - Listen configuration to update. Contains a provider object with the same schema as Settings. The provider identity (type, version, model) is required and must match the current session. + Listen configuration to update. Contains a provider object with the same schema as Settings. The model and language can be changed mid-session. Keyterms can only be updated mid-session for Flux models. """ - provider: DeepgramListenProviderV2Params + provider: AgentV1UpdateListenListenProviderParams diff --git a/src/deepgram/agent/v1/requests/agent_v1update_listen_listen_provider.py b/src/deepgram/agent/v1/requests/agent_v1update_listen_listen_provider.py new file mode 100644 index 00000000..40d038e0 --- /dev/null +++ b/src/deepgram/agent/v1/requests/agent_v1update_listen_listen_provider.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import typing_extensions + + +class AgentV1UpdateListenListenProvider_V1Params(typing_extensions.TypedDict): + version: typing.Literal["v1"] + type: typing.Literal["deepgram"] + model: typing_extensions.NotRequired[str] + language: typing_extensions.NotRequired[str] + keyterms: typing_extensions.NotRequired[typing.Sequence[str]] + smart_format: typing_extensions.NotRequired[bool] + + +class AgentV1UpdateListenListenProvider_V2Params(typing_extensions.TypedDict): + version: typing.Literal["v2"] + type: typing.Literal["deepgram"] + model: str + language_hints: typing_extensions.NotRequired[typing.Sequence[str]] + eot_threshold: typing_extensions.NotRequired[float] + eager_eot_threshold: typing_extensions.NotRequired[float] + eot_timeout_ms: typing_extensions.NotRequired[int] + keyterms: typing_extensions.NotRequired[typing.Sequence[str]] + + +AgentV1UpdateListenListenProviderParams = typing.Union[ + AgentV1UpdateListenListenProvider_V1Params, AgentV1UpdateListenListenProvider_V2Params +] diff --git a/src/deepgram/agent/v1/types/__init__.py b/src/deepgram/agent/v1/types/__init__.py index 0d00a631..3371b027 100644 --- a/src/deepgram/agent/v1/types/__init__.py +++ b/src/deepgram/agent/v1/types/__init__.py @@ -6,6 +6,11 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .agent_v1update_listen_listen_provider import ( + AgentV1UpdateListenListenProvider, + AgentV1UpdateListenListenProvider_V1, + AgentV1UpdateListenListenProvider_V2, + ) from .agent_v1agent_audio_done import AgentV1AgentAudioDone from .agent_v1agent_started_speaking import AgentV1AgentStartedSpeaking from .agent_v1agent_thinking import AgentV1AgentThinking @@ -97,6 +102,9 @@ from .conversation_history_message import ConversationHistoryMessage from .function_call_history_message import FunctionCallHistoryMessage _dynamic_imports: typing.Dict[str, str] = { + "AgentV1UpdateListenListenProvider": ".agent_v1update_listen_listen_provider", + "AgentV1UpdateListenListenProvider_V1": ".agent_v1update_listen_listen_provider", + "AgentV1UpdateListenListenProvider_V2": ".agent_v1update_listen_listen_provider", "AgentV1AgentAudioDone": ".agent_v1agent_audio_done", "AgentV1AgentStartedSpeaking": ".agent_v1agent_started_speaking", "AgentV1AgentThinking": ".agent_v1agent_thinking", @@ -196,6 +204,9 @@ def __dir__(): __all__ = [ + "AgentV1UpdateListenListenProvider", + "AgentV1UpdateListenListenProvider_V1", + "AgentV1UpdateListenListenProvider_V2", "AgentV1AgentAudioDone", "AgentV1AgentStartedSpeaking", "AgentV1AgentThinking", diff --git a/src/deepgram/agent/v1/types/agent_v1latency_report.py b/src/deepgram/agent/v1/types/agent_v1latency_report.py index f39d3b01..a20e3f06 100644 --- a/src/deepgram/agent/v1/types/agent_v1latency_report.py +++ b/src/deepgram/agent/v1/types/agent_v1latency_report.py @@ -13,15 +13,9 @@ class AgentV1LatencyReport(UncheckedBaseModel): Message type identifier for the latency report """ - # Backward-compat: the API spec removed `stt_latency` from the LatencyReport - # schema (deepgram-docs #1006). LatencyReport is a server-emitted (read-only) - # message, so re-adding the field has no request/wire impact — it simply keeps - # `report.stt_latency` resolving (to None, since the server no longer emits it) - # instead of raising AttributeError, avoiding a major-version break. Remove this - # shim and unfreeze when the field is intentionally retired in a future major. stt_latency: typing.Optional[float] = pydantic.Field(default=None) """ - Deprecated. Speech-to-text latency, no longer reported by the server. + Speech-to-text: time from audio received to transcript produced, in seconds """ ttt_token_latency: typing.Optional[float] = pydantic.Field(default=None) diff --git a/src/deepgram/agent/v1/types/agent_v1update_listen.py b/src/deepgram/agent/v1/types/agent_v1update_listen.py index a25a4ae1..94a7a5df 100644 --- a/src/deepgram/agent/v1/types/agent_v1update_listen.py +++ b/src/deepgram/agent/v1/types/agent_v1update_listen.py @@ -16,7 +16,7 @@ class AgentV1UpdateListen(UncheckedBaseModel): listen: AgentV1UpdateListenListen = pydantic.Field() """ - Listen configuration to update. Contains a provider object with the same schema as Settings. The provider identity (type, version, model) is required and must match the current session. + Listen configuration to update. Contains a provider object with the same schema as Settings. The model and language can be changed mid-session. Keyterms can only be updated mid-session for Flux models. """ if IS_PYDANTIC_V2: diff --git a/src/deepgram/agent/v1/types/agent_v1update_listen_listen.py b/src/deepgram/agent/v1/types/agent_v1update_listen_listen.py index 386483eb..62273600 100644 --- a/src/deepgram/agent/v1/types/agent_v1update_listen_listen.py +++ b/src/deepgram/agent/v1/types/agent_v1update_listen_listen.py @@ -5,15 +5,71 @@ import pydantic from ....core.pydantic_utilities import IS_PYDANTIC_V2 from ....core.unchecked_base_model import UncheckedBaseModel +from ....types.deepgram_listen_provider_v1 import DeepgramListenProviderV1 from ....types.deepgram_listen_provider_v2 import DeepgramListenProviderV2 +from .agent_v1update_listen_listen_provider import AgentV1UpdateListenListenProvider + +# Fields that exist only on the v1 (Listen V1) provider shape. Used to infer the +# intended version of a legacy unversioned dict from its own keys, rather than +# blanket-stamping "v2" onto a dict that is clearly describing a v1 provider. +_V1_ONLY_KEYS = frozenset({"language", "smart_format"}) + + +def _coerce_legacy_update_listen_provider(values: typing.Any) -> typing.Any: + # Backward-compat: before the 2026-07-31 regen the provider field was a bare + # DeepgramListenProviderV2. The regen replaced it with the versioned + # AgentV1UpdateListenListenProvider union (discriminant="version"). Coerce a + # legacy Deepgram provider (or a dict lacking the version discriminant) into + # the new shape so existing callers keep producing the correct wire payload. + if not isinstance(values, dict): + return values + provider = values.get("provider") + if isinstance(provider, DeepgramListenProviderV2): + values = dict(values) + values["provider"] = {**provider.dict(), "version": "v2"} + elif isinstance(provider, DeepgramListenProviderV1): + values = dict(values) + values["provider"] = {**provider.dict(), "version": "v1"} + elif isinstance(provider, dict) and "version" not in provider: + values = dict(values) + version = "v1" if _V1_ONLY_KEYS & provider.keys() else "v2" + provider_cls = DeepgramListenProviderV1 if version == "v1" else DeepgramListenProviderV2 + try: + # Route the dict through the same shim model the instance branches + # use, so its own `model_validator(mode="before")` runs. That is what + # remaps a deprecated `language_hint=` to `language_hints` and drops + # the singular key -- the API uses deny_unknown_fields, so leaving it + # in is a hard rejection. Going through the model keeps ONE remap + # implementation instead of duplicating it here. + coerced = provider_cls(**provider).dict() + except Exception: + # A malformed dict (e.g. missing the required `model`) makes the + # model constructor raise. Pre-regen this path passed the dict + # through untouched, so fall back to that rather than turning a + # compat shim into a new source of exceptions. + coerced = dict(provider) + values["provider"] = {**coerced, "version": version} + return values class AgentV1UpdateListenListen(UncheckedBaseModel): """ - Listen configuration to update. Contains a provider object with the same schema as Settings. The provider identity (type, version, model) is required and must match the current session. + Listen configuration to update. Contains a provider object with the same schema as Settings. The model and language can be changed mid-session. Keyterms can only be updated mid-session for Flux models. """ - provider: DeepgramListenProviderV2 + provider: AgentV1UpdateListenListenProvider + + if IS_PYDANTIC_V2: + + @pydantic.model_validator(mode="before") + @classmethod + def _migrate_legacy_provider(cls, values: typing.Any) -> typing.Any: + return _coerce_legacy_update_listen_provider(values) + else: + + @pydantic.root_validator(pre=True) # type: ignore[deprecated] + def _migrate_legacy_provider(cls, values: typing.Any) -> typing.Any: # type: ignore[no-redef] + return _coerce_legacy_update_listen_provider(values) if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/deepgram/agent/v1/types/agent_v1update_listen_listen_provider.py b/src/deepgram/agent/v1/types/agent_v1update_listen_listen_provider.py new file mode 100644 index 00000000..26420cc4 --- /dev/null +++ b/src/deepgram/agent/v1/types/agent_v1update_listen_listen_provider.py @@ -0,0 +1,54 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +import pydantic +import typing_extensions +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel, UnionMetadata + + +class AgentV1UpdateListenListenProvider_V1(UncheckedBaseModel): + version: typing.Literal["v1"] = "v1" + type: typing.Literal["deepgram"] = "deepgram" + model: typing.Optional[str] = None + language: typing.Optional[str] = None + keyterms: typing.Optional[typing.List[str]] = None + smart_format: typing.Optional[bool] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +class AgentV1UpdateListenListenProvider_V2(UncheckedBaseModel): + version: typing.Literal["v2"] = "v2" + type: typing.Literal["deepgram"] = "deepgram" + model: str + language_hints: typing.Optional[typing.List[str]] = None + eot_threshold: typing.Optional[float] = None + eager_eot_threshold: typing.Optional[float] = None + eot_timeout_ms: typing.Optional[int] = None + keyterms: typing.Optional[typing.List[str]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow + + +AgentV1UpdateListenListenProvider = typing_extensions.Annotated[ + typing.Union[AgentV1UpdateListenListenProvider_V1, AgentV1UpdateListenListenProvider_V2], + UnionMetadata(discriminant="version"), +] diff --git a/src/deepgram/base_client.py b/src/deepgram/base_client.py index ab515926..ef741870 100644 --- a/src/deepgram/base_client.py +++ b/src/deepgram/base_client.py @@ -47,6 +47,12 @@ class BaseClient: max_retries : typing.Optional[int] The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. @@ -73,13 +79,13 @@ def __init__( headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.Client] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - _defaulted_timeout = ( - timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read - ) + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None _defaulted_max_retries = max_retries if max_retries is not None else 2 if api_key is None: raise ApiError( @@ -96,6 +102,8 @@ def __init__( else httpx.Client(timeout=_defaulted_timeout), timeout=_defaulted_timeout, max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, logging=logging, ) self._agent: typing.Optional[AgentClient] = None @@ -215,6 +223,12 @@ class AsyncBaseClient: max_retries : typing.Optional[int] The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. + stream_reconnection_enabled : typing.Optional[bool] + Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. + + max_stream_reconnection_attempts : typing.Optional[int] + The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. + follow_redirects : typing.Optional[bool] Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. @@ -241,13 +255,13 @@ def __init__( headers: typing.Optional[typing.Dict[str, str]] = None, timeout: typing.Optional[float] = None, max_retries: typing.Optional[int] = None, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, follow_redirects: typing.Optional[bool] = True, httpx_client: typing.Optional[httpx.AsyncClient] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): - _defaulted_timeout = ( - timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read - ) + _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None _defaulted_max_retries = max_retries if max_retries is not None else 2 if api_key is None: raise ApiError( @@ -262,6 +276,8 @@ def __init__( else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), timeout=_defaulted_timeout, max_retries=_defaulted_max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, logging=logging, ) self._agent: typing.Optional[AsyncAgentClient] = None diff --git a/src/deepgram/core/api_error.py b/src/deepgram/core/api_error.py index 6f850a60..a24abadc 100644 --- a/src/deepgram/core/api_error.py +++ b/src/deepgram/core/api_error.py @@ -2,6 +2,8 @@ from typing import Any, Dict, Optional +from .._secure_logging import redact_sensitive_headers + class ApiError(Exception): headers: Optional[Dict[str, str]] @@ -15,7 +17,13 @@ def __init__( status_code: Optional[int] = None, body: Any = None, ) -> None: - self.headers = headers + # Mask credential headers at construction, not just in __str__. The + # websocket connect paths raise with the full request headers, so an + # unredacted Authorization would otherwise reach anywhere this exception + # goes: str(e), a traceback, a log aggregator, or an error tracker (which + # serialises attributes as well as the message). Non-sensitive headers are + # kept -- dg-request-id is the main reason to look at them at all. + self.headers = redact_sensitive_headers(headers) self.status_code = status_code self.body = body diff --git a/src/deepgram/core/client_wrapper.py b/src/deepgram/core/client_wrapper.py index acc02d24..3f0a18a8 100644 --- a/src/deepgram/core/client_wrapper.py +++ b/src/deepgram/core/client_wrapper.py @@ -17,6 +17,8 @@ def __init__( environment: DeepgramClientEnvironment, timeout: typing.Optional[float] = None, max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, ): self.api_key = api_key @@ -24,18 +26,26 @@ def __init__( self._environment = environment self._timeout = timeout self._max_retries = max_retries + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts self._logging = logging def get_headers(self) -> typing.Dict[str, str]: import platform + from importlib import metadata as _fern_importlib_metadata + + try: + _sdk_version = _fern_importlib_metadata.version("deepgram-sdk") + except _fern_importlib_metadata.PackageNotFoundError: + _sdk_version = "7.6.1" headers: typing.Dict[str, str] = { - "User-Agent": "deepgram-sdk/7.6.0", # x-release-please-version + "User-Agent": "deepgram-sdk/" + _sdk_version, "X-Fern-Language": "Python", "X-Fern-Runtime": f"python/{platform.python_version()}", "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", "X-Fern-SDK-Name": "deepgram-sdk", - "X-Fern-SDK-Version": "7.6.0", # x-release-please-version + "X-Fern-SDK-Version": _sdk_version, **(self.get_custom_headers() or {}), } headers["Authorization"] = f"Token {self.api_key}" @@ -53,6 +63,12 @@ def get_timeout(self) -> typing.Optional[float]: def get_max_retries(self) -> int: return self._max_retries + def get_stream_reconnection_enabled(self) -> bool: + return self._stream_reconnection_enabled if self._stream_reconnection_enabled is not None else True + + def get_max_stream_reconnection_attempts(self) -> typing.Optional[int]: + return self._max_stream_reconnection_attempts + class SyncClientWrapper(BaseClientWrapper): def __init__( @@ -63,6 +79,8 @@ def __init__( environment: DeepgramClientEnvironment, timeout: typing.Optional[float] = None, max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, httpx_client: httpx.Client, ): @@ -72,6 +90,8 @@ def __init__( environment=environment, timeout=timeout, max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, logging=logging, ) self.httpx_client = HttpClient( @@ -92,6 +112,8 @@ def __init__( environment: DeepgramClientEnvironment, timeout: typing.Optional[float] = None, max_retries: int = 2, + stream_reconnection_enabled: typing.Optional[bool] = None, + max_stream_reconnection_attempts: typing.Optional[int] = None, logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, httpx_client: httpx.AsyncClient, @@ -102,6 +124,8 @@ def __init__( environment=environment, timeout=timeout, max_retries=max_retries, + stream_reconnection_enabled=stream_reconnection_enabled, + max_stream_reconnection_attempts=max_stream_reconnection_attempts, logging=logging, ) self._async_token = async_token diff --git a/src/deepgram/core/http_client.py b/src/deepgram/core/http_client.py index f686c571..409d2670 100644 --- a/src/deepgram/core/http_client.py +++ b/src/deepgram/core/http_client.py @@ -3,6 +3,7 @@ import asyncio import email.utils import re +import socket import time import typing from contextlib import asynccontextmanager, contextmanager @@ -23,6 +24,39 @@ JITTER_FACTOR = 0.2 # 20% random jitter +def get_keepalive_socket_options( + idle: int = 60, + intvl: int = 30, + cnt: int = 5, +) -> typing.List[typing.Tuple[int, int, int]]: + """ + Build TCP keepalive socket options for the current platform. + + Keepalive probes keep otherwise-idle connections alive so that long, + non-streaming requests survive idle-connection reaping by a firewall, + load balancer, or NAT. The available socket constants are OS-dependent, + so each option is guarded and only emitted when the platform defines it: + + - ``SO_KEEPALIVE`` is portable (Linux/macOS/Windows). + - The idle-before-first-probe knob is ``TCP_KEEPIDLE`` on Linux and modern + Windows, but ``TCP_KEEPALIVE`` on macOS. + - ``TCP_KEEPINTVL`` / ``TCP_KEEPCNT`` exist on Linux/macOS/modern Windows. + + Passing these tuples to ``httpx.HTTPTransport(socket_options=...)`` / + ``httpx.AsyncHTTPTransport(socket_options=...)`` applies them to every + connection the transport opens. + """ + opts: typing.List[typing.Tuple[int, int, int]] = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] + idle_const = getattr(socket, "TCP_KEEPIDLE", None) or getattr(socket, "TCP_KEEPALIVE", None) + if idle_const: + opts.append((socket.IPPROTO_TCP, idle_const, idle)) + if hasattr(socket, "TCP_KEEPINTVL"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl)) + if hasattr(socket, "TCP_KEEPCNT"): + opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt)) + return opts + + def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: """ This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. @@ -312,11 +346,14 @@ def request( force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) - timeout = ( - request_options.get("timeout_in_seconds") + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None else self.base_timeout() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) @@ -472,11 +509,14 @@ def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.Iterator[httpx.Response]: base_url = self.get_base_url(base_url) - timeout = ( - request_options.get("timeout_in_seconds") + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None else self.base_timeout() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT request_files: typing.Optional[RequestFiles] = ( convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) @@ -601,11 +641,14 @@ async def request( force_multipart: typing.Optional[bool] = None, ) -> httpx.Response: base_url = self.get_base_url(base_url) - timeout = ( - request_options.get("timeout_in_seconds") + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None else self.base_timeout() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT request_files: typing.Optional[RequestFiles] = ( convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) @@ -764,11 +807,14 @@ async def stream( force_multipart: typing.Optional[bool] = None, ) -> typing.AsyncIterator[httpx.Response]: base_url = self.get_base_url(base_url) - timeout = ( - request_options.get("timeout_in_seconds") + _timeout = ( + request_options.get("timeout") + if request_options is not None and request_options.get("timeout") is not None + else request_options.get("timeout_in_seconds") if request_options is not None and request_options.get("timeout_in_seconds") is not None else self.base_timeout() ) + timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT request_files: typing.Optional[RequestFiles] = ( convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) diff --git a/src/deepgram/core/http_sse/_api.py b/src/deepgram/core/http_sse/_api.py index fd137301..9ca5602c 100644 --- a/src/deepgram/core/http_sse/_api.py +++ b/src/deepgram/core/http_sse/_api.py @@ -2,9 +2,20 @@ import codecs import re +import time from contextlib import asynccontextmanager, contextmanager -from typing import Any, AsyncGenerator, AsyncIterator, Iterator - +from typing import ( + Any, + AsyncContextManager, + AsyncGenerator, + AsyncIterator, + Callable, + ContextManager, + Iterator, + Optional, +) + +import anyio import httpx from ._decoders import SSEDecoder from ._exceptions import SSEError @@ -12,21 +23,60 @@ MAX_LINE_SIZE: int = 1_048_576 # 1 MiB +# Reconnection defaults, mirroring the TypeScript SDK's Stream implementation. +DEFAULT_MAX_RECONNECTION_ATTEMPTS: int = 5 +DEFAULT_RECONNECT_DELAY_MS: int = 1_000 +MAX_RECONNECT_DELAY_MS: int = 30_000 + +# A reconnect callback re-issues the original request (with a ``Last-Event-ID`` +# header set to the supplied event id) and returns a *context manager* yielding +# a fresh streaming ``httpx.Response``. Sync clients supply a sync context +# manager; async clients supply an async one. class EventSource: - def __init__(self, response: httpx.Response) -> None: + def __init__( + self, + response: httpx.Response, + *, + resumable: bool = False, + stream_reconnection_enabled: bool = True, + max_stream_reconnection_attempts: Optional[int] = None, + stream_terminator: Optional[str] = None, + reconnect: Optional[Callable[[str], Any]] = None, + ) -> None: self._response = response + self._resumable = resumable + self._stream_reconnection_enabled = stream_reconnection_enabled + self._max_stream_reconnection_attempts = max_stream_reconnection_attempts + self._stream_terminator = stream_terminator + self._reconnect = reconnect + + @staticmethod + def _is_event_stream(response: httpx.Response) -> bool: + content_type = response.headers.get("content-type", "").partition(";")[0] + return "text/event-stream" in content_type def _check_content_type(self) -> None: - content_type = self._response.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: + if not self._is_event_stream(self._response): + content_type = self._response.headers.get("content-type", "").partition(";")[0] raise SSEError( f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" ) - def _get_charset(self) -> str: + def _is_reconnect_response_usable(self, response: httpx.Response) -> bool: + """Whether a reconnected response can be resumed as an SSE stream. + + ``httpx.stream`` does not raise on non-success status, so a resume that + returns an error page (e.g. ``200 text/html`` or a ``500`` body) would + otherwise be parsed as SSE and yield garbage/zero events. Such a + response is treated as a failed attempt (back off and retry) instead. + """ + return response.status_code < 400 and self._is_event_stream(response) + + def _get_charset(self, response: Optional[httpx.Response] = None) -> str: """Extract charset from Content-Type header, fallback to UTF-8.""" - content_type = self._response.headers.get("content-type", "") + resolved = response if response is not None else self._response + content_type = resolved.headers.get("content-type", "") # Parse charset parameter using regex charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) @@ -60,14 +110,83 @@ def _normalize_sse_line_endings(buf: str) -> str: return buf[:-1].replace("\r", "\n") + "\r" return buf.replace("\r", "\n") - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - charset = self._get_charset() - text_decoder = codecs.getincrementaldecoder(charset)(errors="replace") - + def _new_text_decoder(self, response: Optional[httpx.Response] = None) -> "codecs.IncrementalDecoder": + return codecs.getincrementaldecoder(self._get_charset(response))(errors="replace") + + def _reconnect_applicable(self) -> bool: + """Whether reconnection is configured for this stream at all. + + This is the terminator-gating half of the reconnect decision, kept + separate from :meth:`_should_reconnect` (which additionally requires a + last *dispatched* id and an unexhausted attempt budget). The split lets + a mid-stream transport error terminate consistently: + - a stream that can never reconnect (non-resumable, no terminator, + disabled, or no callback) must re-raise the error to the caller, so a + truncated stream is not mistaken for a clean completion; + - a resumable stream that has merely run out of attempts (or has no id + to resume from) ends cleanly — the same way an exhausted empty/error + -body resume already does, matching the TypeScript ``return``. + """ + return ( + self._resumable + and self._stream_terminator is not None + and self._stream_reconnection_enabled + and self._reconnect is not None + ) + + def _should_reconnect(self, last_dispatched_id: Optional[str], reconnect_attempts: int) -> bool: + """Decide whether a prematurely-ended stream should be reconnected. + + Mirrors the TypeScript ``shouldReconnect`` gating: + - only resumable SSE endpoints with a configured terminator, reconnect + enabled, and a reconnect callback are eligible (see + :meth:`_reconnect_applicable`); + - a last *dispatched* event id must exist to resume from; + - the consecutive-failed-attempt cap must not be exceeded. + """ + if not self._reconnect_applicable(): + return False + if not last_dispatched_id: + return False + max_attempts = ( + self._max_stream_reconnection_attempts + if self._max_stream_reconnection_attempts is not None + else DEFAULT_MAX_RECONNECTION_ATTEMPTS + ) + if reconnect_attempts >= max_attempts: + return False + return True + + def _reconnect_delay_seconds(self, last_retry: Optional[int]) -> float: + """Backoff before a reconnect. + + Uses the server's most recent ``retry:`` directive (milliseconds) when + present, otherwise a default of ``DEFAULT_RECONNECT_DELAY_MS``, clamped + to ``MAX_RECONNECT_DELAY_MS``. + """ + base_ms = last_retry if (last_retry is not None and last_retry > 0) else DEFAULT_RECONNECT_DELAY_MS + return min(base_ms, MAX_RECONNECT_DELAY_MS) / 1000.0 + + def _sleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``time.sleep`` blocks the calling thread but remains interruptible by + # signals (e.g. ``KeyboardInterrupt``), which propagate out and abort + # the reconnect without issuing another request. + time.sleep(self._reconnect_delay_seconds(last_retry)) + + async def _asleep_before_reconnect(self, last_retry: Optional[int]) -> None: + # ``anyio.sleep`` is cancellation-aware: if the consumer cancels the task + # or closes the async generator mid-delay, this raises (and no further + # request is issued) instead of blocking for the whole interval. + await anyio.sleep(self._reconnect_delay_seconds(last_retry)) + + def _decode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: buf = "" - for chunk in self._response.iter_bytes(): + for chunk in response.iter_bytes(): buf += text_decoder.decode(chunk) buf = self._normalize_sse_line_endings(buf) @@ -82,34 +201,16 @@ def iter_sse(self) -> Iterator[ServerSentEvent]: f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" ) - # Flush any remaining bytes from the incremental decoder - buf += text_decoder.decode(b"", final=True) - buf = buf.replace("\r\n", "\n").replace("\r", "\n") - - if len(buf) > MAX_LINE_SIZE: - raise SSEError( - f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" - ) - - while "\n" in buf: - line, buf = buf.split("\n", 1) - sse = decoder.decode(line) - if sse is not None: - yield sse - - if buf.strip(): - sse = decoder.decode(buf) - if sse is not None: - yield sse - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - charset = self._get_charset() - text_decoder = codecs.getincrementaldecoder(charset)(errors="replace") + yield from self._flush_decoder(buf, decoder, text_decoder) + async def _adecode_response( + self, + response: httpx.Response, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> AsyncGenerator[ServerSentEvent, None]: buf = "" - async for chunk in self._response.aiter_bytes(): + async for chunk in response.aiter_bytes(): buf += text_decoder.decode(chunk) buf = self._normalize_sse_line_endings(buf) @@ -124,6 +225,15 @@ async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" ) + for sse in self._flush_decoder(buf, decoder, text_decoder): + yield sse + + def _flush_decoder( + self, + buf: str, + decoder: SSEDecoder, + text_decoder: "codecs.IncrementalDecoder", + ) -> Iterator[ServerSentEvent]: # Flush any remaining bytes from the incremental decoder buf += text_decoder.decode(b"", final=True) buf = buf.replace("\r\n", "\n").replace("\r", "\n") @@ -144,6 +254,181 @@ async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: if sse is not None: yield sse + def iter_sse(self) -> Iterator[ServerSentEvent]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + # Consecutive failed reconnection attempts. Reset to 0 whenever an event + # is successfully dispatched (reset-on-progress) — matching browser + # `EventSource` semantics: a server that emits >=1 event then drops on + # every connection can reconnect indefinitely. + reconnect_attempts = 0 + + # ``None`` means there is no live stream to read this iteration (e.g. a + # failed reconnect); the loop then re-evaluates the reconnect decision + # without re-reading an exhausted response. + response: Optional[httpx.Response] = self._response + # Context manager for a response we opened ourselves and must close. + # The initial response is owned by the caller, so it starts as None. + owned_cm: Optional[ContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._decode_response(response, decoder, text_decoder) + while True: + try: + sse = next(events) + except StopIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + # ``next`` is used rather than ``for`` so this cannot + # swallow a ``GeneratorExit`` raised at a ``yield``. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + self._sleep_before_reconnect(last_retry) + + # Close the previously-opened reconnect response before opening + # a new one so we never hold more than one extra connection. + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: ContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = cm.__enter__() + except Exception: + # A failed reconnect consumes an attempt; back off and retry. + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + # Null/empty body or a non-SSE/error response (e.g. 204/304, + # a 500, or an HTML error page): treat as a failed attempt. + response = None + continue + + response = new_response + # Drop any partial event left over from the dropped stream, but + # keep the last event id (per the SSE spec) and start a fresh + # incremental text decoder for the new connection. + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + owned_cm.__exit__(None, None, None) + + async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: + self._check_content_type() + decoder = SSEDecoder() + text_decoder = self._new_text_decoder() + + last_dispatched_id: Optional[str] = None + last_retry: Optional[int] = None + reconnect_attempts = 0 + + response: Optional[httpx.Response] = self._response + owned_cm: Optional[AsyncContextManager[httpx.Response]] = None + try: + while True: + if response is not None: + events = self._adecode_response(response, decoder, text_decoder) + while True: + try: + sse = await events.__anext__() + except StopAsyncIteration: + break + except SSEError: + # A protocol violation (e.g. an oversized line) is a + # genuine error, not a dropped connection; propagate it. + # Listed first because ``SSEError`` subclasses + # ``httpx.TransportError``. + raise + except httpx.TransportError: + # A transport error mid-stream (e.g. the server dropped + # the connection: ``ReadError``/``RemoteProtocolError``) + # is a premature end. Only swallow it when reconnection + # is configured for this stream; otherwise re-raise so a + # non-resumable stream still surfaces the error to the + # caller instead of looking like a clean completion. + # When reconnection is applicable but the attempt budget + # is exhausted, we ``break`` and end cleanly below — the + # same way an exhausted empty/error-body resume does, so + # give-up is consistent regardless of failure shape. + if not self._reconnect_applicable(): + raise + break + yield sse + if sse.id: + last_dispatched_id = sse.id + if sse.retry is not None: + last_retry = sse.retry + reconnect_attempts = 0 + + if not self._should_reconnect(last_dispatched_id, reconnect_attempts): + return + reconnect_attempts += 1 + + await self._asleep_before_reconnect(last_retry) + + if owned_cm is not None: + await owned_cm.__aexit__(None, None, None) + owned_cm = None + + assert self._reconnect is not None # guaranteed by _should_reconnect + try: + cm: AsyncContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") + new_response = await cm.__aenter__() + except Exception: + response = None + continue + owned_cm = cm + if new_response is None or not self._is_reconnect_response_usable(new_response): + response = None + continue + + response = new_response + decoder.reset_in_progress_event() + text_decoder = self._new_text_decoder(new_response) + finally: + if owned_cm is not None: + # Shield the close so a cancellation delivered while reading a + # reconnected response still fully tears the connection down + # instead of leaking it until the client is closed. + with anyio.CancelScope(shield=True): + await owned_cm.__aexit__(None, None, None) + @contextmanager def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: diff --git a/src/deepgram/core/http_sse/_decoders.py b/src/deepgram/core/http_sse/_decoders.py index 339b0890..1f6b35ec 100644 --- a/src/deepgram/core/http_sse/_decoders.py +++ b/src/deepgram/core/http_sse/_decoders.py @@ -12,6 +12,19 @@ def __init__(self) -> None: self._last_event_id = "" self._retry: Optional[int] = None + def reset_in_progress_event(self) -> None: + """Discard any partially-parsed (undispatched) event. + + Used when a stream ends mid-event before reconnecting: the buffered + ``event``/``data``/``retry`` fields of the never-dispatched event must + be dropped so they do not corrupt the first event of the reconnected + stream. Per the SSE spec the last event id is *not* reset here — it + persists across connections. + """ + self._event = "" + self._data = [] + self._retry = None + def decode(self, line: str) -> Optional[ServerSentEvent]: # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 diff --git a/src/deepgram/core/parse_error.py b/src/deepgram/core/parse_error.py index 4527c6a8..bfa3a2f7 100644 --- a/src/deepgram/core/parse_error.py +++ b/src/deepgram/core/parse_error.py @@ -2,6 +2,8 @@ from typing import Any, Dict, Optional +from .._secure_logging import redact_sensitive_headers + class ParsingError(Exception): """ @@ -23,7 +25,9 @@ def __init__( body: Any = None, cause: Optional[Exception] = None, ) -> None: - self.headers = headers + # See the note in core/api_error.py: credential headers are masked at + # construction so they cannot reach a log, traceback, or error tracker. + self.headers = redact_sensitive_headers(headers) self.status_code = status_code self.body = body self.cause = cause diff --git a/src/deepgram/core/request_options.py b/src/deepgram/core/request_options.py index 1b388044..caa6f669 100644 --- a/src/deepgram/core/request_options.py +++ b/src/deepgram/core/request_options.py @@ -14,7 +14,9 @@ class RequestOptions(typing.TypedDict, total=False): This is used primarily as an optional final parameter for service functions. Attributes: - - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + - timeout: float. The number of seconds to await an API call before timing out. + + - timeout_in_seconds: int. Deprecated alias for `timeout`; both are in seconds. Prefer `timeout`. - max_retries: int. The max number of retries to attempt if the API call fails. @@ -27,9 +29,12 @@ class RequestOptions(typing.TypedDict, total=False): - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. """ + timeout: NotRequired[float] timeout_in_seconds: NotRequired[int] max_retries: NotRequired[int] additional_headers: NotRequired[typing.Dict[str, typing.Any]] additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] chunk_size: NotRequired[int] + stream_reconnection_enabled: NotRequired[bool] + max_stream_reconnection_attempts: NotRequired[int] diff --git a/src/deepgram/listen/v1/media/client.py b/src/deepgram/listen/v1/media/client.py index 866110b9..a8f6655b 100644 --- a/src/deepgram/listen/v1/media/client.py +++ b/src/deepgram/listen/v1/media/client.py @@ -142,7 +142,11 @@ def transcribe_url( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands @@ -402,7 +406,11 @@ def transcribe_file( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands @@ -639,7 +647,11 @@ async def transcribe_url( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands @@ -907,7 +919,11 @@ async def transcribe_file( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands diff --git a/src/deepgram/listen/v1/media/raw_client.py b/src/deepgram/listen/v1/media/raw_client.py index d21a900a..e8c3767c 100644 --- a/src/deepgram/listen/v1/media/raw_client.py +++ b/src/deepgram/listen/v1/media/raw_client.py @@ -137,7 +137,11 @@ def transcribe_url( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands @@ -388,7 +392,11 @@ def transcribe_file( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands @@ -642,7 +650,11 @@ async def transcribe_url( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands @@ -893,7 +905,11 @@ async def transcribe_file( Filler Words can help transcribe interruptions in your audio, like "uh" and "um" keyterm : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Key term prompting can boost or suppress specialized terminology and brands. Only compatible with Nova-3 + Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + + `keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + + To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. keywords : typing.Optional[typing.Union[str, typing.Sequence[str]]] Keywords can boost or suppress specialized terminology and brands diff --git a/src/deepgram/listen/v2/client.py b/src/deepgram/listen/v2/client.py index 28277568..eb92e294 100644 --- a/src/deepgram/listen/v2/client.py +++ b/src/deepgram/listen/v2/client.py @@ -23,6 +23,7 @@ from ...types.listen_v2model import ListenV2Model from ...types.listen_v2numerals import ListenV2Numerals from ...types.listen_v2profanity_filter import ListenV2ProfanityFilter +from ...types.listen_v2redact import ListenV2Redact from ...types.listen_v2sample_rate import ListenV2SampleRate from ...types.listen_v2tag import ListenV2Tag from .raw_client import AsyncRawV2Client, RawV2Client @@ -63,6 +64,7 @@ def connect( language_hint: typing.Optional[ListenV2LanguageHintParams] = None, profanity_filter: typing.Optional[ListenV2ProfanityFilter] = None, numerals: typing.Optional[ListenV2Numerals] = None, + redact: typing.Optional[ListenV2Redact] = None, mip_opt_out: typing.Optional[ListenV2MipOptOut] = None, tag: typing.Optional[ListenV2Tag] = None, authorization: typing.Optional[str] = None, @@ -94,6 +96,8 @@ def connect( numerals : typing.Optional[ListenV2Numerals] + redact : typing.Optional[ListenV2Redact] + mip_opt_out : typing.Optional[ListenV2MipOptOut] tag : typing.Optional[ListenV2Tag] @@ -137,6 +141,7 @@ def connect( ), "profanity_filter": profanity_filter, "numerals": numerals, + "redact": redact, "mip_opt_out": mip_opt_out, "tag": tag, **( @@ -202,6 +207,7 @@ async def connect( language_hint: typing.Optional[ListenV2LanguageHintParams] = None, profanity_filter: typing.Optional[ListenV2ProfanityFilter] = None, numerals: typing.Optional[ListenV2Numerals] = None, + redact: typing.Optional[ListenV2Redact] = None, mip_opt_out: typing.Optional[ListenV2MipOptOut] = None, tag: typing.Optional[ListenV2Tag] = None, authorization: typing.Optional[str] = None, @@ -233,6 +239,8 @@ async def connect( numerals : typing.Optional[ListenV2Numerals] + redact : typing.Optional[ListenV2Redact] + mip_opt_out : typing.Optional[ListenV2MipOptOut] tag : typing.Optional[ListenV2Tag] @@ -276,6 +284,7 @@ async def connect( ), "profanity_filter": profanity_filter, "numerals": numerals, + "redact": redact, "mip_opt_out": mip_opt_out, "tag": tag, **( diff --git a/src/deepgram/listen/v2/raw_client.py b/src/deepgram/listen/v2/raw_client.py index 52b0ab9e..304c2dac 100644 --- a/src/deepgram/listen/v2/raw_client.py +++ b/src/deepgram/listen/v2/raw_client.py @@ -23,6 +23,7 @@ from ...types.listen_v2model import ListenV2Model from ...types.listen_v2numerals import ListenV2Numerals from ...types.listen_v2profanity_filter import ListenV2ProfanityFilter +from ...types.listen_v2redact import ListenV2Redact from ...types.listen_v2sample_rate import ListenV2SampleRate from ...types.listen_v2tag import ListenV2Tag from .socket_client import AsyncV2SocketClient, V2SocketClient @@ -51,6 +52,7 @@ def connect( language_hint: typing.Optional[ListenV2LanguageHintParams] = None, profanity_filter: typing.Optional[ListenV2ProfanityFilter] = None, numerals: typing.Optional[ListenV2Numerals] = None, + redact: typing.Optional[ListenV2Redact] = None, mip_opt_out: typing.Optional[ListenV2MipOptOut] = None, tag: typing.Optional[ListenV2Tag] = None, authorization: typing.Optional[str] = None, @@ -82,6 +84,8 @@ def connect( numerals : typing.Optional[ListenV2Numerals] + redact : typing.Optional[ListenV2Redact] + mip_opt_out : typing.Optional[ListenV2MipOptOut] tag : typing.Optional[ListenV2Tag] @@ -125,6 +129,7 @@ def connect( ), "profanity_filter": profanity_filter, "numerals": numerals, + "redact": redact, "mip_opt_out": mip_opt_out, "tag": tag, **( @@ -179,6 +184,7 @@ async def connect( language_hint: typing.Optional[ListenV2LanguageHintParams] = None, profanity_filter: typing.Optional[ListenV2ProfanityFilter] = None, numerals: typing.Optional[ListenV2Numerals] = None, + redact: typing.Optional[ListenV2Redact] = None, mip_opt_out: typing.Optional[ListenV2MipOptOut] = None, tag: typing.Optional[ListenV2Tag] = None, authorization: typing.Optional[str] = None, @@ -210,6 +216,8 @@ async def connect( numerals : typing.Optional[ListenV2Numerals] + redact : typing.Optional[ListenV2Redact] + mip_opt_out : typing.Optional[ListenV2MipOptOut] tag : typing.Optional[ListenV2Tag] @@ -253,6 +261,7 @@ async def connect( ), "profanity_filter": profanity_filter, "numerals": numerals, + "redact": redact, "mip_opt_out": mip_opt_out, "tag": tag, **( diff --git a/src/deepgram/listen/v2/socket_client.py b/src/deepgram/listen/v2/socket_client.py index d214f0f5..13f81517 100644 --- a/src/deepgram/listen/v2/socket_client.py +++ b/src/deepgram/listen/v2/socket_client.py @@ -8,7 +8,9 @@ from ...core.events import EventEmitterMixin, EventType from ...core.unchecked_base_model import construct_type from .types.listen_v2close_stream import ListenV2CloseStream +from .types.listen_v2configure import ListenV2Configure from .types.listen_v2configure_failure import ListenV2ConfigureFailure +from .types.listen_v2configure_success import ListenV2ConfigureSuccess from .types.listen_v2connected import ListenV2Connected from .types.listen_v2fatal_error import ListenV2FatalError from .types.listen_v2turn_info import ListenV2TurnInfo @@ -20,7 +22,7 @@ _logger = logging.getLogger(__name__) V2SocketClientResponse = typing.Union[ - ListenV2Connected, ListenV2TurnInfo, typing.Any, ListenV2ConfigureFailure, ListenV2FatalError + ListenV2Connected, ListenV2TurnInfo, ListenV2ConfigureSuccess, ListenV2ConfigureFailure, ListenV2FatalError ] @@ -86,12 +88,18 @@ async def send_close_stream(self, message: typing.Optional[ListenV2CloseStream] """ await self._send_model(message or ListenV2CloseStream(type="CloseStream")) - async def send_configure(self, message: typing.Any) -> None: + async def send_configure( + self, message: typing.Union[ListenV2Configure, typing.Dict[str, typing.Any]] + ) -> None: """ Send a message to the websocket connection. - The message will be sent as a typing.Any. + The message will be sent as a ListenV2Configure. A raw dict is also + accepted and sent verbatim for back-compat with pre-typed-model callers. """ - await self._send(message) + if isinstance(message, dict): + await self._send(message) + else: + await self._send_model(message) async def recv(self) -> V2SocketClientResponse: """ @@ -184,12 +192,16 @@ def send_close_stream(self, message: typing.Optional[ListenV2CloseStream] = None """ self._send_model(message or ListenV2CloseStream(type="CloseStream")) - def send_configure(self, message: typing.Any) -> None: + def send_configure(self, message: typing.Union[ListenV2Configure, typing.Dict[str, typing.Any]]) -> None: """ Send a message to the websocket connection. - The message will be sent as a typing.Any. + The message will be sent as a ListenV2Configure. A raw dict is also + accepted and sent verbatim for back-compat with pre-typed-model callers. """ - self._send(message) + if isinstance(message, dict): + self._send(message) + else: + self._send_model(message) def recv(self) -> V2SocketClientResponse: """ diff --git a/src/deepgram/manage/v1/projects/requests/client.py b/src/deepgram/manage/v1/projects/requests/client.py index 6d1435aa..7d71b8c5 100644 --- a/src/deepgram/manage/v1/projects/requests/client.py +++ b/src/deepgram/manage/v1/projects/requests/client.py @@ -93,29 +93,13 @@ def list( Examples -------- - import datetime - from deepgram import DeepgramClient client = DeepgramClient( api_key="YOUR_API_KEY", ) client.manage.v1.projects.requests.list( - project_id="123456-7890-1234-5678-901234", - start=datetime.datetime.fromisoformat( - "2024-01-15 09:30:00+00:00", - ), - end=datetime.datetime.fromisoformat( - "2024-01-15 09:30:00+00:00", - ), - limit=1.1, - page=1.1, - accessor="12345678-1234-1234-1234-123456789012", - request_id="12345678-1234-1234-1234-123456789012", - deployment="hosted", - endpoint="listen", - method="sync", - status="succeeded", + project_id="12345678-90ab-cdef-1234-567890abcdef", ) """ _response = self._raw_client.list( @@ -164,8 +148,8 @@ def get( api_key="YOUR_API_KEY", ) client.manage.v1.projects.requests.get( - project_id="123456-7890-1234-5678-901234", - request_id="123456-7890-1234-5678-901234", + project_id="12345678-90ab-cdef-1234-567890abcdef", + request_id="a3f1c9d2-4b7e-4f9a-8c3d-2e5f7b9a1c0d", ) """ _response = self._raw_client.get(project_id, request_id, request_options=request_options) @@ -252,7 +236,6 @@ async def list( Examples -------- import asyncio - import datetime from deepgram import AsyncDeepgramClient @@ -263,21 +246,7 @@ async def list( async def main() -> None: await client.manage.v1.projects.requests.list( - project_id="123456-7890-1234-5678-901234", - start=datetime.datetime.fromisoformat( - "2024-01-15 09:30:00+00:00", - ), - end=datetime.datetime.fromisoformat( - "2024-01-15 09:30:00+00:00", - ), - limit=1.1, - page=1.1, - accessor="12345678-1234-1234-1234-123456789012", - request_id="12345678-1234-1234-1234-123456789012", - deployment="hosted", - endpoint="listen", - method="sync", - status="succeeded", + project_id="12345678-90ab-cdef-1234-567890abcdef", ) @@ -334,8 +303,8 @@ async def get( async def main() -> None: await client.manage.v1.projects.requests.get( - project_id="123456-7890-1234-5678-901234", - request_id="123456-7890-1234-5678-901234", + project_id="12345678-90ab-cdef-1234-567890abcdef", + request_id="a3f1c9d2-4b7e-4f9a-8c3d-2e5f7b9a1c0d", ) diff --git a/src/deepgram/requests/deepgram.py b/src/deepgram/requests/deepgram.py index a5ad6104..d0a5e24f 100644 --- a/src/deepgram/requests/deepgram.py +++ b/src/deepgram/requests/deepgram.py @@ -7,15 +7,19 @@ class DeepgramParams(typing_extensions.TypedDict): + """ + Deepgram text-to-speech provider. Aura models use version v1 (default); Flux TTS uses version v2 and a flux-* model. + """ + type: typing.Literal["deepgram"] - version: typing_extensions.NotRequired[typing.Literal["v1"]] + version: typing_extensions.NotRequired[str] """ - The REST API version for the Deepgram text-to-speech API + The Deepgram text-to-speech model family. Accepted values: `v1` (Aura, the default) and `v2` (Flux TTS). Use `v1` with an aura-* model and `v2` with a flux-* model. Defaults to `v1` when omitted. """ model: DeepgramSpeakProviderModel """ - Deepgram TTS model + Deepgram TTS model. Aura models (version v1) use the aura-* voices; Flux TTS (version v2) uses the flux-{voice}-{language} voices (e.g. flux-alexis-en). """ speed: typing_extensions.NotRequired[float] diff --git a/src/deepgram/requests/google.py b/src/deepgram/requests/google.py index 7c183372..3610ee95 100644 --- a/src/deepgram/requests/google.py +++ b/src/deepgram/requests/google.py @@ -4,13 +4,14 @@ import typing_extensions from ..types.google_think_provider_model import GoogleThinkProviderModel +from ..types.google_think_provider_version import GoogleThinkProviderVersion class GoogleParams(typing_extensions.TypedDict): type: typing.Literal["google"] - version: typing_extensions.NotRequired[typing.Literal["v1beta"]] + version: typing_extensions.NotRequired[GoogleThinkProviderVersion] """ - The REST API version for the Google generative language API + The Google API used for the request: ai-studio-v1beta for the AI Studio API, or gemini-enterprise-agent-v1 for the Gemini Enterprise Agent (GEA) API. v1beta is accepted as an alias for ai-studio-v1beta. Defaults based on the Deepgram Voice Agent endpoint you connect to. """ model: GoogleThinkProviderModel diff --git a/src/deepgram/requests/speak_settings_v1provider.py b/src/deepgram/requests/speak_settings_v1provider.py index db3fac4e..a70fed91 100644 --- a/src/deepgram/requests/speak_settings_v1provider.py +++ b/src/deepgram/requests/speak_settings_v1provider.py @@ -18,7 +18,7 @@ class SpeakSettingsV1Provider_DeepgramParams(typing_extensions.TypedDict): type: typing.Literal["deepgram"] - version: typing_extensions.NotRequired[typing.Literal["v1"]] + version: typing_extensions.NotRequired[str] model: DeepgramSpeakProviderModel speed: typing_extensions.NotRequired[float] diff --git a/src/deepgram/requests/think_settings_v1provider.py b/src/deepgram/requests/think_settings_v1provider.py index 1174d0d8..91222666 100644 --- a/src/deepgram/requests/think_settings_v1provider.py +++ b/src/deepgram/requests/think_settings_v1provider.py @@ -8,6 +8,7 @@ from ..types.anthropic_think_provider_model import AnthropicThinkProviderModel from ..types.aws_bedrock_think_provider_model import AwsBedrockThinkProviderModel from ..types.google_think_provider_model import GoogleThinkProviderModel +from ..types.google_think_provider_version import GoogleThinkProviderVersion from ..types.groq_think_provider_reasoning_mode import GroqThinkProviderReasoningMode from ..types.open_ai_think_provider_model import OpenAiThinkProviderModel from ..types.open_ai_think_provider_reasoning_mode import OpenAiThinkProviderReasoningMode @@ -38,7 +39,7 @@ class ThinkSettingsV1Provider_AnthropicParams(typing_extensions.TypedDict): class ThinkSettingsV1Provider_GoogleParams(typing_extensions.TypedDict): type: typing.Literal["google"] - version: typing_extensions.NotRequired[typing.Literal["v1beta"]] + version: typing_extensions.NotRequired[GoogleThinkProviderVersion] model: GoogleThinkProviderModel temperature: typing_extensions.NotRequired[float] diff --git a/src/deepgram/speak/__init__.py b/src/deepgram/speak/__init__.py index 27653587..968bc106 100644 --- a/src/deepgram/speak/__init__.py +++ b/src/deepgram/speak/__init__.py @@ -33,6 +33,15 @@ from .v2 import ( SpeakV2Close, SpeakV2CloseParams, + SpeakV2Configure, + SpeakV2ConfigureFailure, + SpeakV2ConfigureFailureCode, + SpeakV2ConfigureFailureParams, + SpeakV2ConfigureParams, + SpeakV2ConfigureSuccess, + SpeakV2ConfigureSuccessApplied, + SpeakV2ConfigureSuccessAppliedParams, + SpeakV2ConfigureSuccessParams, SpeakV2Connected, SpeakV2ConnectedParams, SpeakV2Error, @@ -42,10 +51,20 @@ SpeakV2FlushParams, SpeakV2Flushed, SpeakV2FlushedParams, + SpeakV2Interrupt, + SpeakV2InterruptParams, + SpeakV2InterruptPlaybackOffset, + SpeakV2InterruptPlaybackOffsetParams, SpeakV2SessionMetadata, SpeakV2SessionMetadataParams, SpeakV2Speak, SpeakV2SpeakParams, + SpeakV2SpeechInterrupted, + SpeakV2SpeechInterruptedMetadata, + SpeakV2SpeechInterruptedMetadataControlsApplied, + SpeakV2SpeechInterruptedMetadataControlsAppliedParams, + SpeakV2SpeechInterruptedMetadataParams, + SpeakV2SpeechInterruptedParams, SpeakV2SpeechMetadata, SpeakV2SpeechMetadataControlsApplied, SpeakV2SpeechMetadataControlsAppliedParams, @@ -79,6 +98,15 @@ "SpeakV1WarningParams": ".v1", "SpeakV2Close": ".v2", "SpeakV2CloseParams": ".v2", + "SpeakV2Configure": ".v2", + "SpeakV2ConfigureFailure": ".v2", + "SpeakV2ConfigureFailureCode": ".v2", + "SpeakV2ConfigureFailureParams": ".v2", + "SpeakV2ConfigureParams": ".v2", + "SpeakV2ConfigureSuccess": ".v2", + "SpeakV2ConfigureSuccessApplied": ".v2", + "SpeakV2ConfigureSuccessAppliedParams": ".v2", + "SpeakV2ConfigureSuccessParams": ".v2", "SpeakV2Connected": ".v2", "SpeakV2ConnectedParams": ".v2", "SpeakV2Error": ".v2", @@ -88,10 +116,20 @@ "SpeakV2FlushParams": ".v2", "SpeakV2Flushed": ".v2", "SpeakV2FlushedParams": ".v2", + "SpeakV2Interrupt": ".v2", + "SpeakV2InterruptParams": ".v2", + "SpeakV2InterruptPlaybackOffset": ".v2", + "SpeakV2InterruptPlaybackOffsetParams": ".v2", "SpeakV2SessionMetadata": ".v2", "SpeakV2SessionMetadataParams": ".v2", "SpeakV2Speak": ".v2", "SpeakV2SpeakParams": ".v2", + "SpeakV2SpeechInterrupted": ".v2", + "SpeakV2SpeechInterruptedMetadata": ".v2", + "SpeakV2SpeechInterruptedMetadataControlsApplied": ".v2", + "SpeakV2SpeechInterruptedMetadataControlsAppliedParams": ".v2", + "SpeakV2SpeechInterruptedMetadataParams": ".v2", + "SpeakV2SpeechInterruptedParams": ".v2", "SpeakV2SpeechMetadata": ".v2", "SpeakV2SpeechMetadataControlsApplied": ".v2", "SpeakV2SpeechMetadataControlsAppliedParams": ".v2", @@ -150,6 +188,15 @@ def __dir__(): "SpeakV1WarningParams", "SpeakV2Close", "SpeakV2CloseParams", + "SpeakV2Configure", + "SpeakV2ConfigureFailure", + "SpeakV2ConfigureFailureCode", + "SpeakV2ConfigureFailureParams", + "SpeakV2ConfigureParams", + "SpeakV2ConfigureSuccess", + "SpeakV2ConfigureSuccessApplied", + "SpeakV2ConfigureSuccessAppliedParams", + "SpeakV2ConfigureSuccessParams", "SpeakV2Connected", "SpeakV2ConnectedParams", "SpeakV2Error", @@ -159,10 +206,20 @@ def __dir__(): "SpeakV2FlushParams", "SpeakV2Flushed", "SpeakV2FlushedParams", + "SpeakV2Interrupt", + "SpeakV2InterruptParams", + "SpeakV2InterruptPlaybackOffset", + "SpeakV2InterruptPlaybackOffsetParams", "SpeakV2SessionMetadata", "SpeakV2SessionMetadataParams", "SpeakV2Speak", "SpeakV2SpeakParams", + "SpeakV2SpeechInterrupted", + "SpeakV2SpeechInterruptedMetadata", + "SpeakV2SpeechInterruptedMetadataControlsApplied", + "SpeakV2SpeechInterruptedMetadataControlsAppliedParams", + "SpeakV2SpeechInterruptedMetadataParams", + "SpeakV2SpeechInterruptedParams", "SpeakV2SpeechMetadata", "SpeakV2SpeechMetadataControlsApplied", "SpeakV2SpeechMetadataControlsAppliedParams", diff --git a/src/deepgram/speak/v2/__init__.py b/src/deepgram/speak/v2/__init__.py index fde39ffd..96a467d1 100644 --- a/src/deepgram/speak/v2/__init__.py +++ b/src/deepgram/speak/v2/__init__.py @@ -8,13 +8,23 @@ if typing.TYPE_CHECKING: from .types import ( SpeakV2Close, + SpeakV2Configure, + SpeakV2ConfigureFailure, + SpeakV2ConfigureFailureCode, + SpeakV2ConfigureSuccess, + SpeakV2ConfigureSuccessApplied, SpeakV2Connected, SpeakV2Error, SpeakV2ErrorCode, SpeakV2Flush, SpeakV2Flushed, + SpeakV2Interrupt, + SpeakV2InterruptPlaybackOffset, SpeakV2SessionMetadata, SpeakV2Speak, + SpeakV2SpeechInterrupted, + SpeakV2SpeechInterruptedMetadata, + SpeakV2SpeechInterruptedMetadataControlsApplied, SpeakV2SpeechMetadata, SpeakV2SpeechMetadataControlsApplied, SpeakV2SpeechStarted, @@ -24,12 +34,21 @@ from .audio import AudioGenerateRequestCallbackMethod, AudioGenerateRequestContainer, AudioGenerateRequestEncoding from .requests import ( SpeakV2CloseParams, + SpeakV2ConfigureFailureParams, + SpeakV2ConfigureParams, + SpeakV2ConfigureSuccessAppliedParams, + SpeakV2ConfigureSuccessParams, SpeakV2ConnectedParams, SpeakV2ErrorParams, SpeakV2FlushParams, SpeakV2FlushedParams, + SpeakV2InterruptParams, + SpeakV2InterruptPlaybackOffsetParams, SpeakV2SessionMetadataParams, SpeakV2SpeakParams, + SpeakV2SpeechInterruptedMetadataControlsAppliedParams, + SpeakV2SpeechInterruptedMetadataParams, + SpeakV2SpeechInterruptedParams, SpeakV2SpeechMetadataControlsAppliedParams, SpeakV2SpeechMetadataParams, SpeakV2SpeechStartedParams, @@ -41,6 +60,15 @@ "AudioGenerateRequestEncoding": ".audio", "SpeakV2Close": ".types", "SpeakV2CloseParams": ".requests", + "SpeakV2Configure": ".types", + "SpeakV2ConfigureFailure": ".types", + "SpeakV2ConfigureFailureCode": ".types", + "SpeakV2ConfigureFailureParams": ".requests", + "SpeakV2ConfigureParams": ".requests", + "SpeakV2ConfigureSuccess": ".types", + "SpeakV2ConfigureSuccessApplied": ".types", + "SpeakV2ConfigureSuccessAppliedParams": ".requests", + "SpeakV2ConfigureSuccessParams": ".requests", "SpeakV2Connected": ".types", "SpeakV2ConnectedParams": ".requests", "SpeakV2Error": ".types", @@ -50,10 +78,20 @@ "SpeakV2FlushParams": ".requests", "SpeakV2Flushed": ".types", "SpeakV2FlushedParams": ".requests", + "SpeakV2Interrupt": ".types", + "SpeakV2InterruptParams": ".requests", + "SpeakV2InterruptPlaybackOffset": ".types", + "SpeakV2InterruptPlaybackOffsetParams": ".requests", "SpeakV2SessionMetadata": ".types", "SpeakV2SessionMetadataParams": ".requests", "SpeakV2Speak": ".types", "SpeakV2SpeakParams": ".requests", + "SpeakV2SpeechInterrupted": ".types", + "SpeakV2SpeechInterruptedMetadata": ".types", + "SpeakV2SpeechInterruptedMetadataControlsApplied": ".types", + "SpeakV2SpeechInterruptedMetadataControlsAppliedParams": ".requests", + "SpeakV2SpeechInterruptedMetadataParams": ".requests", + "SpeakV2SpeechInterruptedParams": ".requests", "SpeakV2SpeechMetadata": ".types", "SpeakV2SpeechMetadataControlsApplied": ".types", "SpeakV2SpeechMetadataControlsAppliedParams": ".requests", @@ -93,6 +131,15 @@ def __dir__(): "AudioGenerateRequestEncoding", "SpeakV2Close", "SpeakV2CloseParams", + "SpeakV2Configure", + "SpeakV2ConfigureFailure", + "SpeakV2ConfigureFailureCode", + "SpeakV2ConfigureFailureParams", + "SpeakV2ConfigureParams", + "SpeakV2ConfigureSuccess", + "SpeakV2ConfigureSuccessApplied", + "SpeakV2ConfigureSuccessAppliedParams", + "SpeakV2ConfigureSuccessParams", "SpeakV2Connected", "SpeakV2ConnectedParams", "SpeakV2Error", @@ -102,10 +149,20 @@ def __dir__(): "SpeakV2FlushParams", "SpeakV2Flushed", "SpeakV2FlushedParams", + "SpeakV2Interrupt", + "SpeakV2InterruptParams", + "SpeakV2InterruptPlaybackOffset", + "SpeakV2InterruptPlaybackOffsetParams", "SpeakV2SessionMetadata", "SpeakV2SessionMetadataParams", "SpeakV2Speak", "SpeakV2SpeakParams", + "SpeakV2SpeechInterrupted", + "SpeakV2SpeechInterruptedMetadata", + "SpeakV2SpeechInterruptedMetadataControlsApplied", + "SpeakV2SpeechInterruptedMetadataControlsAppliedParams", + "SpeakV2SpeechInterruptedMetadataParams", + "SpeakV2SpeechInterruptedParams", "SpeakV2SpeechMetadata", "SpeakV2SpeechMetadataControlsApplied", "SpeakV2SpeechMetadataControlsAppliedParams", diff --git a/src/deepgram/speak/v2/audio/client.py b/src/deepgram/speak/v2/audio/client.py index 9a01d204..47193389 100644 --- a/src/deepgram/speak/v2/audio/client.py +++ b/src/deepgram/speak/v2/audio/client.py @@ -40,7 +40,9 @@ def generate( bit_rate: typing.Optional[int] = None, container: typing.Optional[AudioGenerateRequestContainer] = None, encoding: typing.Optional[AudioGenerateRequestEncoding] = None, + expressivity: typing.Optional[int] = None, sample_rate: typing.Optional[int] = None, + speed: typing.Optional[float] = None, priority: typing.Optional[typing.Literal["low"]] = None, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Iterator[bytes]: @@ -53,7 +55,7 @@ def generate( Flux TTS model used to synthesize the submitted text, in the form `flux-{voice}-{language}` (for example, `flux-alexis-en`). Required; unlike the v1 (Aura) endpoint there is no default and only flux models are accepted. English-only at launch. text : str - The text content to be converted to speech. The server normalizes and preprocesses the text (e.g. stripping inline controls) before synthesis. + The text content to be converted to speech. The server normalizes and preprocesses the text before synthesis. Inline pause and pronunciation controls are not yet applied; they are stripped from the text before synthesis. callback : typing.Optional[str] URL to which we'll make the callback request @@ -76,9 +78,15 @@ def generate( encoding : typing.Optional[AudioGenerateRequestEncoding] Encoding allows you to specify the expected encoding of your audio output + expressivity : typing.Optional[int] + Expressive range of the generated speech. `0` is the voice's nominal delivery; negative values are flatter and more restrained, positive values more animated. + sample_rate : typing.Optional[int] Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + speed : typing.Optional[float] + Speaking rate multiplier that adjusts the pace of generated speech while preserving natural prosody and voice quality. Only the multipliers listed here are accepted — the range is 0.85 to 1.15 in 0.05 increments. Not yet supported in all languages. + priority : typing.Optional[typing.Literal["low"]] Processing priority for asynchronous (callback) requests. The only supported value is low. @@ -112,7 +120,9 @@ def generate( bit_rate=bit_rate, container=container, encoding=encoding, + expressivity=expressivity, sample_rate=sample_rate, + speed=speed, priority=priority, request_options=request_options, ) as r: @@ -146,7 +156,9 @@ async def generate( bit_rate: typing.Optional[int] = None, container: typing.Optional[AudioGenerateRequestContainer] = None, encoding: typing.Optional[AudioGenerateRequestEncoding] = None, + expressivity: typing.Optional[int] = None, sample_rate: typing.Optional[int] = None, + speed: typing.Optional[float] = None, priority: typing.Optional[typing.Literal["low"]] = None, request_options: typing.Optional[RequestOptions] = None, ) -> typing.AsyncIterator[bytes]: @@ -159,7 +171,7 @@ async def generate( Flux TTS model used to synthesize the submitted text, in the form `flux-{voice}-{language}` (for example, `flux-alexis-en`). Required; unlike the v1 (Aura) endpoint there is no default and only flux models are accepted. English-only at launch. text : str - The text content to be converted to speech. The server normalizes and preprocesses the text (e.g. stripping inline controls) before synthesis. + The text content to be converted to speech. The server normalizes and preprocesses the text before synthesis. Inline pause and pronunciation controls are not yet applied; they are stripped from the text before synthesis. callback : typing.Optional[str] URL to which we'll make the callback request @@ -182,9 +194,15 @@ async def generate( encoding : typing.Optional[AudioGenerateRequestEncoding] Encoding allows you to specify the expected encoding of your audio output + expressivity : typing.Optional[int] + Expressive range of the generated speech. `0` is the voice's nominal delivery; negative values are flatter and more restrained, positive values more animated. + sample_rate : typing.Optional[int] Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + speed : typing.Optional[float] + Speaking rate multiplier that adjusts the pace of generated speech while preserving natural prosody and voice quality. Only the multipliers listed here are accepted — the range is 0.85 to 1.15 in 0.05 increments. Not yet supported in all languages. + priority : typing.Optional[typing.Literal["low"]] Processing priority for asynchronous (callback) requests. The only supported value is low. @@ -226,7 +244,9 @@ async def main() -> None: bit_rate=bit_rate, container=container, encoding=encoding, + expressivity=expressivity, sample_rate=sample_rate, + speed=speed, priority=priority, request_options=request_options, ) as r: diff --git a/src/deepgram/speak/v2/audio/raw_client.py b/src/deepgram/speak/v2/audio/raw_client.py index 4000017e..e53e0eb7 100644 --- a/src/deepgram/speak/v2/audio/raw_client.py +++ b/src/deepgram/speak/v2/audio/raw_client.py @@ -37,7 +37,9 @@ def generate( bit_rate: typing.Optional[int] = None, container: typing.Optional[AudioGenerateRequestContainer] = None, encoding: typing.Optional[AudioGenerateRequestEncoding] = None, + expressivity: typing.Optional[int] = None, sample_rate: typing.Optional[int] = None, + speed: typing.Optional[float] = None, priority: typing.Optional[typing.Literal["low"]] = None, request_options: typing.Optional[RequestOptions] = None, ) -> typing.Iterator[HttpResponse[typing.Iterator[bytes]]]: @@ -50,7 +52,7 @@ def generate( Flux TTS model used to synthesize the submitted text, in the form `flux-{voice}-{language}` (for example, `flux-alexis-en`). Required; unlike the v1 (Aura) endpoint there is no default and only flux models are accepted. English-only at launch. text : str - The text content to be converted to speech. The server normalizes and preprocesses the text (e.g. stripping inline controls) before synthesis. + The text content to be converted to speech. The server normalizes and preprocesses the text before synthesis. Inline pause and pronunciation controls are not yet applied; they are stripped from the text before synthesis. callback : typing.Optional[str] URL to which we'll make the callback request @@ -73,9 +75,15 @@ def generate( encoding : typing.Optional[AudioGenerateRequestEncoding] Encoding allows you to specify the expected encoding of your audio output + expressivity : typing.Optional[int] + Expressive range of the generated speech. `0` is the voice's nominal delivery; negative values are flatter and more restrained, positive values more animated. + sample_rate : typing.Optional[int] Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + speed : typing.Optional[float] + Speaking rate multiplier that adjusts the pace of generated speech while preserving natural prosody and voice quality. Only the multipliers listed here are accepted — the range is 0.85 to 1.15 in 0.05 increments. Not yet supported in all languages. + priority : typing.Optional[typing.Literal["low"]] Processing priority for asynchronous (callback) requests. The only supported value is low. @@ -99,8 +107,10 @@ def generate( "bit_rate": bit_rate, "container": container, "encoding": encoding, + "expressivity": expressivity, "model": model, "sample_rate": sample_rate, + "speed": speed, "priority": priority, }, json={ @@ -166,7 +176,9 @@ async def generate( bit_rate: typing.Optional[int] = None, container: typing.Optional[AudioGenerateRequestContainer] = None, encoding: typing.Optional[AudioGenerateRequestEncoding] = None, + expressivity: typing.Optional[int] = None, sample_rate: typing.Optional[int] = None, + speed: typing.Optional[float] = None, priority: typing.Optional[typing.Literal["low"]] = None, request_options: typing.Optional[RequestOptions] = None, ) -> typing.AsyncIterator[AsyncHttpResponse[typing.AsyncIterator[bytes]]]: @@ -179,7 +191,7 @@ async def generate( Flux TTS model used to synthesize the submitted text, in the form `flux-{voice}-{language}` (for example, `flux-alexis-en`). Required; unlike the v1 (Aura) endpoint there is no default and only flux models are accepted. English-only at launch. text : str - The text content to be converted to speech. The server normalizes and preprocesses the text (e.g. stripping inline controls) before synthesis. + The text content to be converted to speech. The server normalizes and preprocesses the text before synthesis. Inline pause and pronunciation controls are not yet applied; they are stripped from the text before synthesis. callback : typing.Optional[str] URL to which we'll make the callback request @@ -202,9 +214,15 @@ async def generate( encoding : typing.Optional[AudioGenerateRequestEncoding] Encoding allows you to specify the expected encoding of your audio output + expressivity : typing.Optional[int] + Expressive range of the generated speech. `0` is the voice's nominal delivery; negative values are flatter and more restrained, positive values more animated. + sample_rate : typing.Optional[int] Sample Rate specifies the sample rate for the output audio. Based on the encoding, different sample rates are supported. For some encodings, the sample rate is not configurable + speed : typing.Optional[float] + Speaking rate multiplier that adjusts the pace of generated speech while preserving natural prosody and voice quality. Only the multipliers listed here are accepted — the range is 0.85 to 1.15 in 0.05 increments. Not yet supported in all languages. + priority : typing.Optional[typing.Literal["low"]] Processing priority for asynchronous (callback) requests. The only supported value is low. @@ -228,8 +246,10 @@ async def generate( "bit_rate": bit_rate, "container": container, "encoding": encoding, + "expressivity": expressivity, "model": model, "sample_rate": sample_rate, + "speed": speed, "priority": priority, }, json={ diff --git a/src/deepgram/speak/v2/client.py b/src/deepgram/speak/v2/client.py index 64aafea5..d53c7022 100644 --- a/src/deepgram/speak/v2/client.py +++ b/src/deepgram/speak/v2/client.py @@ -15,9 +15,11 @@ from ...core.request_options import RequestOptions from ...core.websocket_compat import InvalidWebSocketStatus, get_status_code from ...types.speak_v2encoding import SpeakV2Encoding +from ...types.speak_v2expressivity import SpeakV2Expressivity from ...types.speak_v2mip_opt_out import SpeakV2MipOptOut from ...types.speak_v2model import SpeakV2Model from ...types.speak_v2sample_rate import SpeakV2SampleRate +from ...types.speak_v2speed import SpeakV2Speed from ...types.speak_v2tag import SpeakV2Tag from .raw_client import AsyncRawV2Client, RawV2Client from .socket_client import AsyncV2SocketClient, V2SocketClient @@ -55,6 +57,8 @@ def connect( model: SpeakV2Model, encoding: typing.Optional[SpeakV2Encoding] = None, sample_rate: typing.Optional[SpeakV2SampleRate] = None, + speed: typing.Optional[SpeakV2Speed] = None, + expressivity: typing.Optional[SpeakV2Expressivity] = None, mip_opt_out: typing.Optional[SpeakV2MipOptOut] = None, tag: typing.Optional[SpeakV2Tag] = None, authorization: typing.Optional[str] = None, @@ -73,6 +77,10 @@ def connect( sample_rate : typing.Optional[SpeakV2SampleRate] + speed : typing.Optional[SpeakV2Speed] + + expressivity : typing.Optional[SpeakV2Expressivity] + mip_opt_out : typing.Optional[SpeakV2MipOptOut] tag : typing.Optional[SpeakV2Tag] @@ -97,6 +105,8 @@ def connect( "model": model, "encoding": encoding, "sample_rate": sample_rate, + "speed": speed, + "expressivity": expressivity, "mip_opt_out": mip_opt_out, "tag": tag, **( @@ -165,6 +175,8 @@ async def connect( model: SpeakV2Model, encoding: typing.Optional[SpeakV2Encoding] = None, sample_rate: typing.Optional[SpeakV2SampleRate] = None, + speed: typing.Optional[SpeakV2Speed] = None, + expressivity: typing.Optional[SpeakV2Expressivity] = None, mip_opt_out: typing.Optional[SpeakV2MipOptOut] = None, tag: typing.Optional[SpeakV2Tag] = None, authorization: typing.Optional[str] = None, @@ -183,6 +195,10 @@ async def connect( sample_rate : typing.Optional[SpeakV2SampleRate] + speed : typing.Optional[SpeakV2Speed] + + expressivity : typing.Optional[SpeakV2Expressivity] + mip_opt_out : typing.Optional[SpeakV2MipOptOut] tag : typing.Optional[SpeakV2Tag] @@ -207,6 +223,8 @@ async def connect( "model": model, "encoding": encoding, "sample_rate": sample_rate, + "speed": speed, + "expressivity": expressivity, "mip_opt_out": mip_opt_out, "tag": tag, **( diff --git a/src/deepgram/speak/v2/raw_client.py b/src/deepgram/speak/v2/raw_client.py index 4854672f..f11eef00 100644 --- a/src/deepgram/speak/v2/raw_client.py +++ b/src/deepgram/speak/v2/raw_client.py @@ -13,9 +13,11 @@ from ...core.request_options import RequestOptions from ...core.websocket_compat import InvalidWebSocketStatus, get_status_code from ...types.speak_v2encoding import SpeakV2Encoding +from ...types.speak_v2expressivity import SpeakV2Expressivity from ...types.speak_v2mip_opt_out import SpeakV2MipOptOut from ...types.speak_v2model import SpeakV2Model from ...types.speak_v2sample_rate import SpeakV2SampleRate +from ...types.speak_v2speed import SpeakV2Speed from ...types.speak_v2tag import SpeakV2Tag from .socket_client import AsyncV2SocketClient, V2SocketClient @@ -36,6 +38,8 @@ def connect( model: SpeakV2Model, encoding: typing.Optional[SpeakV2Encoding] = None, sample_rate: typing.Optional[SpeakV2SampleRate] = None, + speed: typing.Optional[SpeakV2Speed] = None, + expressivity: typing.Optional[SpeakV2Expressivity] = None, mip_opt_out: typing.Optional[SpeakV2MipOptOut] = None, tag: typing.Optional[SpeakV2Tag] = None, authorization: typing.Optional[str] = None, @@ -54,6 +58,10 @@ def connect( sample_rate : typing.Optional[SpeakV2SampleRate] + speed : typing.Optional[SpeakV2Speed] + + expressivity : typing.Optional[SpeakV2Expressivity] + mip_opt_out : typing.Optional[SpeakV2MipOptOut] tag : typing.Optional[SpeakV2Tag] @@ -78,6 +86,8 @@ def connect( "model": model, "encoding": encoding, "sample_rate": sample_rate, + "speed": speed, + "expressivity": expressivity, "mip_opt_out": mip_opt_out, "tag": tag, **( @@ -125,6 +135,8 @@ async def connect( model: SpeakV2Model, encoding: typing.Optional[SpeakV2Encoding] = None, sample_rate: typing.Optional[SpeakV2SampleRate] = None, + speed: typing.Optional[SpeakV2Speed] = None, + expressivity: typing.Optional[SpeakV2Expressivity] = None, mip_opt_out: typing.Optional[SpeakV2MipOptOut] = None, tag: typing.Optional[SpeakV2Tag] = None, authorization: typing.Optional[str] = None, @@ -143,6 +155,10 @@ async def connect( sample_rate : typing.Optional[SpeakV2SampleRate] + speed : typing.Optional[SpeakV2Speed] + + expressivity : typing.Optional[SpeakV2Expressivity] + mip_opt_out : typing.Optional[SpeakV2MipOptOut] tag : typing.Optional[SpeakV2Tag] @@ -167,6 +183,8 @@ async def connect( "model": model, "encoding": encoding, "sample_rate": sample_rate, + "speed": speed, + "expressivity": expressivity, "mip_opt_out": mip_opt_out, "tag": tag, **( diff --git a/src/deepgram/speak/v2/requests/__init__.py b/src/deepgram/speak/v2/requests/__init__.py index 6da073b9..21eb2f13 100644 --- a/src/deepgram/speak/v2/requests/__init__.py +++ b/src/deepgram/speak/v2/requests/__init__.py @@ -7,24 +7,44 @@ if typing.TYPE_CHECKING: from .speak_v2close import SpeakV2CloseParams + from .speak_v2configure import SpeakV2ConfigureParams + from .speak_v2configure_failure import SpeakV2ConfigureFailureParams + from .speak_v2configure_success import SpeakV2ConfigureSuccessParams + from .speak_v2configure_success_applied import SpeakV2ConfigureSuccessAppliedParams from .speak_v2connected import SpeakV2ConnectedParams from .speak_v2error import SpeakV2ErrorParams from .speak_v2flush import SpeakV2FlushParams from .speak_v2flushed import SpeakV2FlushedParams + from .speak_v2interrupt import SpeakV2InterruptParams + from .speak_v2interrupt_playback_offset import SpeakV2InterruptPlaybackOffsetParams from .speak_v2session_metadata import SpeakV2SessionMetadataParams from .speak_v2speak import SpeakV2SpeakParams + from .speak_v2speech_interrupted import SpeakV2SpeechInterruptedParams + from .speak_v2speech_interrupted_metadata import SpeakV2SpeechInterruptedMetadataParams + from .speak_v2speech_interrupted_metadata_controls_applied import ( + SpeakV2SpeechInterruptedMetadataControlsAppliedParams, + ) from .speak_v2speech_metadata import SpeakV2SpeechMetadataParams from .speak_v2speech_metadata_controls_applied import SpeakV2SpeechMetadataControlsAppliedParams from .speak_v2speech_started import SpeakV2SpeechStartedParams from .speak_v2warning import SpeakV2WarningParams _dynamic_imports: typing.Dict[str, str] = { "SpeakV2CloseParams": ".speak_v2close", + "SpeakV2ConfigureFailureParams": ".speak_v2configure_failure", + "SpeakV2ConfigureParams": ".speak_v2configure", + "SpeakV2ConfigureSuccessAppliedParams": ".speak_v2configure_success_applied", + "SpeakV2ConfigureSuccessParams": ".speak_v2configure_success", "SpeakV2ConnectedParams": ".speak_v2connected", "SpeakV2ErrorParams": ".speak_v2error", "SpeakV2FlushParams": ".speak_v2flush", "SpeakV2FlushedParams": ".speak_v2flushed", + "SpeakV2InterruptParams": ".speak_v2interrupt", + "SpeakV2InterruptPlaybackOffsetParams": ".speak_v2interrupt_playback_offset", "SpeakV2SessionMetadataParams": ".speak_v2session_metadata", "SpeakV2SpeakParams": ".speak_v2speak", + "SpeakV2SpeechInterruptedMetadataControlsAppliedParams": ".speak_v2speech_interrupted_metadata_controls_applied", + "SpeakV2SpeechInterruptedMetadataParams": ".speak_v2speech_interrupted_metadata", + "SpeakV2SpeechInterruptedParams": ".speak_v2speech_interrupted", "SpeakV2SpeechMetadataControlsAppliedParams": ".speak_v2speech_metadata_controls_applied", "SpeakV2SpeechMetadataParams": ".speak_v2speech_metadata", "SpeakV2SpeechStartedParams": ".speak_v2speech_started", @@ -55,12 +75,21 @@ def __dir__(): __all__ = [ "SpeakV2CloseParams", + "SpeakV2ConfigureFailureParams", + "SpeakV2ConfigureParams", + "SpeakV2ConfigureSuccessAppliedParams", + "SpeakV2ConfigureSuccessParams", "SpeakV2ConnectedParams", "SpeakV2ErrorParams", "SpeakV2FlushParams", "SpeakV2FlushedParams", + "SpeakV2InterruptParams", + "SpeakV2InterruptPlaybackOffsetParams", "SpeakV2SessionMetadataParams", "SpeakV2SpeakParams", + "SpeakV2SpeechInterruptedMetadataControlsAppliedParams", + "SpeakV2SpeechInterruptedMetadataParams", + "SpeakV2SpeechInterruptedParams", "SpeakV2SpeechMetadataControlsAppliedParams", "SpeakV2SpeechMetadataParams", "SpeakV2SpeechStartedParams", diff --git a/src/deepgram/speak/v2/requests/speak_v2configure.py b/src/deepgram/speak/v2/requests/speak_v2configure.py new file mode 100644 index 00000000..4201069f --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2configure.py @@ -0,0 +1,15 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ....types.speak_v2speed import SpeakV2Speed + + +class SpeakV2ConfigureParams(typing_extensions.TypedDict): + type: typing.Literal["Configure"] + """ + Message type identifier + """ + + speed: typing_extensions.NotRequired[SpeakV2Speed] diff --git a/src/deepgram/speak/v2/requests/speak_v2configure_failure.py b/src/deepgram/speak/v2/requests/speak_v2configure_failure.py new file mode 100644 index 00000000..7940a7c4 --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2configure_failure.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..types.speak_v2configure_failure_code import SpeakV2ConfigureFailureCode + + +class SpeakV2ConfigureFailureParams(typing_extensions.TypedDict): + type: typing.Literal["ConfigureFailure"] + """ + Message type identifier + """ + + code: SpeakV2ConfigureFailureCode + """ + Failure code, in `SCREAMING_SNAKE_CASE`. `SPEED_OUT_OF_RANGE`: outside the multipliers the model publishes. `SPEED_INCREMENT_INVALID`: inside the published range but not one of the multipliers. `SPEED_NOT_SUPPORTED`: this model or language has no runtime speed control at all. `INTERNAL_ERROR`: the configuration was acceptable but the server could not apply it — unlike the others, a server-side failure rather than a statement about the request. + """ + + field: typing_extensions.NotRequired[typing.Literal["speed"]] + """ + The configuration field the failure is about. Absent when the failure is not tied to one field. + """ + + value: typing_extensions.NotRequired[float] + """ + The rejected value for `field`. Absent when there is no offending value to echo — `SPEED_NOT_SUPPORTED` names the field but carries no value, because the rejection is a property of the model. + """ + + description: str + """ + A human-readable description of the failure + """ diff --git a/src/deepgram/speak/v2/requests/speak_v2configure_success.py b/src/deepgram/speak/v2/requests/speak_v2configure_success.py new file mode 100644 index 00000000..7862ff30 --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2configure_success.py @@ -0,0 +1,18 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .speak_v2configure_success_applied import SpeakV2ConfigureSuccessAppliedParams + + +class SpeakV2ConfigureSuccessParams(typing_extensions.TypedDict): + type: typing.Literal["ConfigureSuccess"] + """ + Message type identifier + """ + + applied: SpeakV2ConfigureSuccessAppliedParams + """ + Synthesis configuration. A field is present only when it has been set on this session. + """ diff --git a/src/deepgram/speak/v2/requests/speak_v2configure_success_applied.py b/src/deepgram/speak/v2/requests/speak_v2configure_success_applied.py new file mode 100644 index 00000000..e4e91665 --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2configure_success_applied.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ....types.speak_v2speed import SpeakV2Speed + + +class SpeakV2ConfigureSuccessAppliedParams(typing_extensions.TypedDict): + """ + Synthesis configuration. A field is present only when it has been set on this session. + """ + + speed: typing_extensions.NotRequired[SpeakV2Speed] diff --git a/src/deepgram/speak/v2/requests/speak_v2interrupt.py b/src/deepgram/speak/v2/requests/speak_v2interrupt.py new file mode 100644 index 00000000..56fe699a --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2interrupt.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .speak_v2interrupt_playback_offset import SpeakV2InterruptPlaybackOffsetParams + + +class SpeakV2InterruptParams(typing_extensions.TypedDict): + type: typing.Literal["Interrupt"] + """ + Message type identifier + """ + + playback_offset: typing_extensions.NotRequired[SpeakV2InterruptPlaybackOffsetParams] + """ + How much audio the client had played when the user barged in. Optional: without it the server cannot split the turn's text, so `SpeechInterrupted` omits `text_spoken` and `text_remaining`. + + The offset is cumulative from the start of the *session*, not from the start of the current turn. Each `Interrupt` must advance past the position the previous one established. + """ diff --git a/src/deepgram/speak/v2/requests/speak_v2interrupt_playback_offset.py b/src/deepgram/speak/v2/requests/speak_v2interrupt_playback_offset.py new file mode 100644 index 00000000..4c270daf --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2interrupt_playback_offset.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class SpeakV2InterruptPlaybackOffsetParams(typing_extensions.TypedDict): + """ + How much audio the client had played when the user barged in. Optional: without it the server cannot split the turn's text, so `SpeechInterrupted` omits `text_spoken` and `text_remaining`. + + The offset is cumulative from the start of the *session*, not from the start of the current turn. Each `Interrupt` must advance past the position the previous one established. + """ + + type: typing.Literal["time_ms"] + """ + Offset unit. `time_ms` is the only supported form. + """ + + value: int + """ + Milliseconds of session audio the client played before barging in. + """ diff --git a/src/deepgram/speak/v2/requests/speak_v2session_metadata.py b/src/deepgram/speak/v2/requests/speak_v2session_metadata.py index 9b4b9e03..dd73739d 100644 --- a/src/deepgram/speak/v2/requests/speak_v2session_metadata.py +++ b/src/deepgram/speak/v2/requests/speak_v2session_metadata.py @@ -13,7 +13,7 @@ class SpeakV2SessionMetadataParams(typing_extensions.TypedDict): total_audio_duration_ms: int """ - Cumulative audio duration produced across the session, in milliseconds + Cumulative audio duration produced across the session, in milliseconds. An `Interrupt` rebases this onto the audio the client actually played. """ total_input_character_count: int diff --git a/src/deepgram/speak/v2/requests/speak_v2speak.py b/src/deepgram/speak/v2/requests/speak_v2speak.py index 2112ce06..14665cca 100644 --- a/src/deepgram/speak/v2/requests/speak_v2speak.py +++ b/src/deepgram/speak/v2/requests/speak_v2speak.py @@ -13,5 +13,5 @@ class SpeakV2SpeakParams(typing_extensions.TypedDict): text: str """ - The input text to synthesize + The input text to synthesize. Inline pause and pronunciation controls are not yet applied; they are stripped from the text before synthesis. """ diff --git a/src/deepgram/speak/v2/requests/speak_v2speech_interrupted.py b/src/deepgram/speak/v2/requests/speak_v2speech_interrupted.py new file mode 100644 index 00000000..3571c0e2 --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2speech_interrupted.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from .speak_v2speech_interrupted_metadata import SpeakV2SpeechInterruptedMetadataParams + + +class SpeakV2SpeechInterruptedParams(typing_extensions.TypedDict): + type: typing.Literal["SpeechInterrupted"] + """ + Message type identifier + """ + + audio_played_ms: int + """ + How much audio the client had played when the interrupt landed, in milliseconds from the start of the session. Echoes the `Interrupt`'s `playback_offset` when one was supplied. Otherwise it is the server's own total, representing the audio that has been generated so far. A client that sends its first `Interrupt` without an offset can use this value as the baseline the next one must advance past. + """ + + text_spoken: typing_extensions.NotRequired[str] + """ + The portion of the turn's text the user heard. Omitted when the `Interrupt` carried no `playback_offset`. + """ + + text_remaining: typing_extensions.NotRequired[str] + """ + The portion of the turn's text the user did not hear. Omitted when the `Interrupt` carried no `playback_offset`. + """ + + metadata: SpeakV2SpeechInterruptedMetadataParams + """ + Billing and timing for a single turn. + """ diff --git a/src/deepgram/speak/v2/requests/speak_v2speech_interrupted_metadata.py b/src/deepgram/speak/v2/requests/speak_v2speech_interrupted_metadata.py new file mode 100644 index 00000000..431919c7 --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2speech_interrupted_metadata.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from .speak_v2speech_interrupted_metadata_controls_applied import SpeakV2SpeechInterruptedMetadataControlsAppliedParams + + +class SpeakV2SpeechInterruptedMetadataParams(typing_extensions.TypedDict): + """ + Billing and timing for a single turn. + """ + + speech_id: str + """ + Server-assigned turn identifier + """ + + audio_duration_ms: int + """ + Audio duration produced for this turn, in milliseconds + """ + + input_character_count: int + """ + Raw input character count for this turn, before text normalization + """ + + billable_character_count: int + """ + Billable character count for this turn — the input character count with stripped control characters removed. Always less than or equal to `input_character_count`. + """ + + controls_applied: SpeakV2SpeechInterruptedMetadataControlsAppliedParams + """ + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. + """ diff --git a/src/deepgram/speak/v2/requests/speak_v2speech_interrupted_metadata_controls_applied.py b/src/deepgram/speak/v2/requests/speak_v2speech_interrupted_metadata_controls_applied.py new file mode 100644 index 00000000..dd47def1 --- /dev/null +++ b/src/deepgram/speak/v2/requests/speak_v2speech_interrupted_metadata_controls_applied.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class SpeakV2SpeechInterruptedMetadataControlsAppliedParams(typing_extensions.TypedDict): + """ + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. + """ + + pronunciations_applied: int + """ + Pronunciation overrides successfully applied. Mirrors the Aura-2 `dg-pronunciations-applied` REST header. Currently always `0`. + """ + + breaks_applied: int + """ + Pause (break) controls successfully applied. Mirrors the Aura-2 `dg-breaks-applied` REST header. Currently always `0`. + """ + + pronunciation_warnings: int + """ + Pronunciation entries that triggered a warning (invalid IPA, word too long). Mirrors the Aura-2 `dg-pronunciation-warnings` REST header. Currently always `0`. + """ diff --git a/src/deepgram/speak/v2/requests/speak_v2speech_metadata.py b/src/deepgram/speak/v2/requests/speak_v2speech_metadata.py index 23b20488..2848762d 100644 --- a/src/deepgram/speak/v2/requests/speak_v2speech_metadata.py +++ b/src/deepgram/speak/v2/requests/speak_v2speech_metadata.py @@ -34,5 +34,5 @@ class SpeakV2SpeechMetadataParams(typing_extensions.TypedDict): controls_applied: SpeakV2SpeechMetadataControlsAppliedParams """ - Controls applied during the turn. Inline pronunciation and pause controls are not available during Early Access, so every count is currently `0`. + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. """ diff --git a/src/deepgram/speak/v2/requests/speak_v2speech_metadata_controls_applied.py b/src/deepgram/speak/v2/requests/speak_v2speech_metadata_controls_applied.py index 561318ac..ace2e3ef 100644 --- a/src/deepgram/speak/v2/requests/speak_v2speech_metadata_controls_applied.py +++ b/src/deepgram/speak/v2/requests/speak_v2speech_metadata_controls_applied.py @@ -5,15 +5,20 @@ class SpeakV2SpeechMetadataControlsAppliedParams(typing_extensions.TypedDict): """ - Controls applied during the turn. Inline pronunciation and pause controls are not available during Early Access, so every count is currently `0`. + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. """ pronunciations_applied: int """ - Pronunciation overrides successfully applied. Mirrors the Aura-2 `dg-pronunciations-applied` REST header. Always `0` during Early Access. + Pronunciation overrides successfully applied. Mirrors the Aura-2 `dg-pronunciations-applied` REST header. Currently always `0`. + """ + + breaks_applied: int + """ + Pause (break) controls successfully applied. Mirrors the Aura-2 `dg-breaks-applied` REST header. Currently always `0`. """ pronunciation_warnings: int """ - Pronunciation entries that triggered a warning (invalid IPA, word too long). Mirrors the Aura-2 `dg-pronunciation-warnings` REST header. Always `0` during Early Access. + Pronunciation entries that triggered a warning (invalid IPA, word too long). Mirrors the Aura-2 `dg-pronunciation-warnings` REST header. Currently always `0`. """ diff --git a/src/deepgram/speak/v2/requests/speak_v2warning.py b/src/deepgram/speak/v2/requests/speak_v2warning.py index 1cbeb836..fb5df32b 100644 --- a/src/deepgram/speak/v2/requests/speak_v2warning.py +++ b/src/deepgram/speak/v2/requests/speak_v2warning.py @@ -13,7 +13,13 @@ class SpeakV2WarningParams(typing_extensions.TypedDict): code: str """ - Warning code identifying the condition, in `SCREAMING_SNAKE_CASE`. Early Access codes are `NO_ACTIVE_SPEECH` (a speech-scoped message arrived with no active turn) and `SYNTHESIS_RETRYING` (a synthesis request failed and is being retried). + Warning code identifying the condition, in `SCREAMING_SNAKE_CASE`. + + Turn-scoped codes: `NO_ACTIVE_SPEECH` (a speech-scoped message arrived with no active turn), `NO_SYNTHESIZABLE_TEXT` (the turn's text was entirely whitespace or punctuation, so it produced no audio and is completed with a zero-duration `SpeechMetadata`).`SYNTHESIS_RETRYING` (a synthesis request failed and is being retried). + + Inline-control codes are reserved and not currently emitted, because inline pause and pronunciation controls are not yet applied: `BREAKS_LIMIT_EXCEEDED` (too many pause controls, or two pauses with no intervening text), `BREAK_TOKENS_OUT_OF_RANGE` (pause durations outside the range the model supports), `BREAK_TOKENS_WITH_INVALID_INCREMENTS` (pause durations off the model's supported increment), `PRONUNCIATION_WARNINGS` (a pronunciation override contained invalid IPA), `PRONUNCIATION_TOO_LONG` (an IPA string exceeded the length limit), `PRONUNCIATIONS_LIMIT_EXCEEDED` (too many pronunciation controls in one turn). + + Interrupt-scoped codes, each meaning the `Interrupt` was ignored: `NO_AUDIO_GENERATED` (the session has produced no audio yet, so there is nothing to interrupt), `INTERRUPT_IN_PROGRESS` (an earlier `Interrupt` is still being processed — at most one is handled at a time), `INVALID_INTERRUPT_OFFSET` (the `playback_offset` did not advance past the position a prior interrupt established). """ description: str diff --git a/src/deepgram/speak/v2/socket_client.py b/src/deepgram/speak/v2/socket_client.py index a2896a45..c6729c32 100644 --- a/src/deepgram/speak/v2/socket_client.py +++ b/src/deepgram/speak/v2/socket_client.py @@ -8,12 +8,17 @@ from ...core.events import EventEmitterMixin, EventType from ...core.unchecked_base_model import construct_type from .types.speak_v2close import SpeakV2Close +from .types.speak_v2configure import SpeakV2Configure +from .types.speak_v2configure_failure import SpeakV2ConfigureFailure +from .types.speak_v2configure_success import SpeakV2ConfigureSuccess from .types.speak_v2connected import SpeakV2Connected from .types.speak_v2error import SpeakV2Error from .types.speak_v2flush import SpeakV2Flush from .types.speak_v2flushed import SpeakV2Flushed +from .types.speak_v2interrupt import SpeakV2Interrupt from .types.speak_v2session_metadata import SpeakV2SessionMetadata from .types.speak_v2speak import SpeakV2Speak +from .types.speak_v2speech_interrupted import SpeakV2SpeechInterrupted from .types.speak_v2speech_metadata import SpeakV2SpeechMetadata from .types.speak_v2speech_started import SpeakV2SpeechStarted from .types.speak_v2warning import SpeakV2Warning @@ -29,8 +34,11 @@ SpeakV2Connected, SpeakV2SpeechStarted, SpeakV2SpeechMetadata, + SpeakV2SpeechInterrupted, SpeakV2Flushed, SpeakV2SessionMetadata, + SpeakV2ConfigureSuccess, + SpeakV2ConfigureFailure, SpeakV2Warning, SpeakV2Error, ] @@ -98,6 +106,20 @@ async def send_flush(self, message: typing.Optional[SpeakV2Flush] = None) -> Non """ await self._send_model(message or SpeakV2Flush(type="Flush")) + async def send_interrupt(self, message: typing.Optional[SpeakV2Interrupt] = None) -> None: + """ + Send a message to the websocket connection. + The message will be sent as a SpeakV2Interrupt. + """ + await self._send_model(message or SpeakV2Interrupt(type="Interrupt")) + + async def send_configure(self, message: SpeakV2Configure) -> None: + """ + Send a message to the websocket connection. + The message will be sent as a SpeakV2Configure. + """ + await self._send_model(message) + async def send_close(self, message: typing.Optional[SpeakV2Close] = None) -> None: """ Send a message to the websocket connection. @@ -196,6 +218,20 @@ def send_flush(self, message: typing.Optional[SpeakV2Flush] = None) -> None: """ self._send_model(message or SpeakV2Flush(type="Flush")) + def send_interrupt(self, message: typing.Optional[SpeakV2Interrupt] = None) -> None: + """ + Send a message to the websocket connection. + The message will be sent as a SpeakV2Interrupt. + """ + self._send_model(message or SpeakV2Interrupt(type="Interrupt")) + + def send_configure(self, message: SpeakV2Configure) -> None: + """ + Send a message to the websocket connection. + The message will be sent as a SpeakV2Configure. + """ + self._send_model(message) + def send_close(self, message: typing.Optional[SpeakV2Close] = None) -> None: """ Send a message to the websocket connection. diff --git a/src/deepgram/speak/v2/types/__init__.py b/src/deepgram/speak/v2/types/__init__.py index b08407f0..f056a8f0 100644 --- a/src/deepgram/speak/v2/types/__init__.py +++ b/src/deepgram/speak/v2/types/__init__.py @@ -7,26 +7,46 @@ if typing.TYPE_CHECKING: from .speak_v2close import SpeakV2Close + from .speak_v2configure import SpeakV2Configure + from .speak_v2configure_failure import SpeakV2ConfigureFailure + from .speak_v2configure_failure_code import SpeakV2ConfigureFailureCode + from .speak_v2configure_success import SpeakV2ConfigureSuccess + from .speak_v2configure_success_applied import SpeakV2ConfigureSuccessApplied from .speak_v2connected import SpeakV2Connected from .speak_v2error import SpeakV2Error from .speak_v2error_code import SpeakV2ErrorCode from .speak_v2flush import SpeakV2Flush from .speak_v2flushed import SpeakV2Flushed + from .speak_v2interrupt import SpeakV2Interrupt + from .speak_v2interrupt_playback_offset import SpeakV2InterruptPlaybackOffset from .speak_v2session_metadata import SpeakV2SessionMetadata from .speak_v2speak import SpeakV2Speak + from .speak_v2speech_interrupted import SpeakV2SpeechInterrupted + from .speak_v2speech_interrupted_metadata import SpeakV2SpeechInterruptedMetadata + from .speak_v2speech_interrupted_metadata_controls_applied import SpeakV2SpeechInterruptedMetadataControlsApplied from .speak_v2speech_metadata import SpeakV2SpeechMetadata from .speak_v2speech_metadata_controls_applied import SpeakV2SpeechMetadataControlsApplied from .speak_v2speech_started import SpeakV2SpeechStarted from .speak_v2warning import SpeakV2Warning _dynamic_imports: typing.Dict[str, str] = { "SpeakV2Close": ".speak_v2close", + "SpeakV2Configure": ".speak_v2configure", + "SpeakV2ConfigureFailure": ".speak_v2configure_failure", + "SpeakV2ConfigureFailureCode": ".speak_v2configure_failure_code", + "SpeakV2ConfigureSuccess": ".speak_v2configure_success", + "SpeakV2ConfigureSuccessApplied": ".speak_v2configure_success_applied", "SpeakV2Connected": ".speak_v2connected", "SpeakV2Error": ".speak_v2error", "SpeakV2ErrorCode": ".speak_v2error_code", "SpeakV2Flush": ".speak_v2flush", "SpeakV2Flushed": ".speak_v2flushed", + "SpeakV2Interrupt": ".speak_v2interrupt", + "SpeakV2InterruptPlaybackOffset": ".speak_v2interrupt_playback_offset", "SpeakV2SessionMetadata": ".speak_v2session_metadata", "SpeakV2Speak": ".speak_v2speak", + "SpeakV2SpeechInterrupted": ".speak_v2speech_interrupted", + "SpeakV2SpeechInterruptedMetadata": ".speak_v2speech_interrupted_metadata", + "SpeakV2SpeechInterruptedMetadataControlsApplied": ".speak_v2speech_interrupted_metadata_controls_applied", "SpeakV2SpeechMetadata": ".speak_v2speech_metadata", "SpeakV2SpeechMetadataControlsApplied": ".speak_v2speech_metadata_controls_applied", "SpeakV2SpeechStarted": ".speak_v2speech_started", @@ -57,13 +77,23 @@ def __dir__(): __all__ = [ "SpeakV2Close", + "SpeakV2Configure", + "SpeakV2ConfigureFailure", + "SpeakV2ConfigureFailureCode", + "SpeakV2ConfigureSuccess", + "SpeakV2ConfigureSuccessApplied", "SpeakV2Connected", "SpeakV2Error", "SpeakV2ErrorCode", "SpeakV2Flush", "SpeakV2Flushed", + "SpeakV2Interrupt", + "SpeakV2InterruptPlaybackOffset", "SpeakV2SessionMetadata", "SpeakV2Speak", + "SpeakV2SpeechInterrupted", + "SpeakV2SpeechInterruptedMetadata", + "SpeakV2SpeechInterruptedMetadataControlsApplied", "SpeakV2SpeechMetadata", "SpeakV2SpeechMetadataControlsApplied", "SpeakV2SpeechStarted", diff --git a/src/deepgram/speak/v2/types/speak_v2configure.py b/src/deepgram/speak/v2/types/speak_v2configure.py new file mode 100644 index 00000000..013a1f05 --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2configure.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel +from ....types.speak_v2speed import SpeakV2Speed + + +class SpeakV2Configure(UncheckedBaseModel): + type: typing.Literal["Configure"] = pydantic.Field(default="Configure") + """ + Message type identifier + """ + + speed: typing.Optional[SpeakV2Speed] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2configure_failure.py b/src/deepgram/speak/v2/types/speak_v2configure_failure.py new file mode 100644 index 00000000..a3801ea4 --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2configure_failure.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel +from .speak_v2configure_failure_code import SpeakV2ConfigureFailureCode + + +class SpeakV2ConfigureFailure(UncheckedBaseModel): + type: typing.Literal["ConfigureFailure"] = pydantic.Field(default="ConfigureFailure") + """ + Message type identifier + """ + + code: SpeakV2ConfigureFailureCode = pydantic.Field() + """ + Failure code, in `SCREAMING_SNAKE_CASE`. `SPEED_OUT_OF_RANGE`: outside the multipliers the model publishes. `SPEED_INCREMENT_INVALID`: inside the published range but not one of the multipliers. `SPEED_NOT_SUPPORTED`: this model or language has no runtime speed control at all. `INTERNAL_ERROR`: the configuration was acceptable but the server could not apply it — unlike the others, a server-side failure rather than a statement about the request. + """ + + field: typing.Optional[typing.Literal["speed"]] = pydantic.Field(default=None) + """ + The configuration field the failure is about. Absent when the failure is not tied to one field. + """ + + value: typing.Optional[float] = pydantic.Field(default=None) + """ + The rejected value for `field`. Absent when there is no offending value to echo — `SPEED_NOT_SUPPORTED` names the field but carries no value, because the rejection is a property of the model. + """ + + description: str = pydantic.Field() + """ + A human-readable description of the failure + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2configure_failure_code.py b/src/deepgram/speak/v2/types/speak_v2configure_failure_code.py new file mode 100644 index 00000000..80a46f9c --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2configure_failure_code.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SpeakV2ConfigureFailureCode = typing.Union[ + typing.Literal["SPEED_OUT_OF_RANGE", "SPEED_INCREMENT_INVALID", "SPEED_NOT_SUPPORTED", "INTERNAL_ERROR"], typing.Any +] diff --git a/src/deepgram/speak/v2/types/speak_v2configure_success.py b/src/deepgram/speak/v2/types/speak_v2configure_success.py new file mode 100644 index 00000000..84be96b0 --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2configure_success.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel +from .speak_v2configure_success_applied import SpeakV2ConfigureSuccessApplied + + +class SpeakV2ConfigureSuccess(UncheckedBaseModel): + type: typing.Literal["ConfigureSuccess"] = pydantic.Field(default="ConfigureSuccess") + """ + Message type identifier + """ + + applied: SpeakV2ConfigureSuccessApplied = pydantic.Field() + """ + Synthesis configuration. A field is present only when it has been set on this session. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2configure_success_applied.py b/src/deepgram/speak/v2/types/speak_v2configure_success_applied.py new file mode 100644 index 00000000..507d9ea9 --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2configure_success_applied.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel +from ....types.speak_v2speed import SpeakV2Speed + + +class SpeakV2ConfigureSuccessApplied(UncheckedBaseModel): + """ + Synthesis configuration. A field is present only when it has been set on this session. + """ + + speed: typing.Optional[SpeakV2Speed] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2error_code.py b/src/deepgram/speak/v2/types/speak_v2error_code.py index 8d9e1df1..d693c7e5 100644 --- a/src/deepgram/speak/v2/types/speak_v2error_code.py +++ b/src/deepgram/speak/v2/types/speak_v2error_code.py @@ -3,6 +3,8 @@ import typing SpeakV2ErrorCode = typing.Union[ - typing.Literal["MESSAGE-0000", "DATA-0000", "BIG-0000", "NET-0000", "NET-0001", "NET-0002", "NET-0003", "NET-0004"], + typing.Literal[ + "MESSAGE-0000", "DATA-0000", "DATA-0002", "BIG-0000", "NET-0000", "NET-0001", "NET-0002", "NET-0003", "NET-0004" + ], typing.Any, ] diff --git a/src/deepgram/speak/v2/types/speak_v2interrupt.py b/src/deepgram/speak/v2/types/speak_v2interrupt.py new file mode 100644 index 00000000..62f729af --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2interrupt.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel +from .speak_v2interrupt_playback_offset import SpeakV2InterruptPlaybackOffset + + +class SpeakV2Interrupt(UncheckedBaseModel): + type: typing.Literal["Interrupt"] = pydantic.Field(default="Interrupt") + """ + Message type identifier + """ + + playback_offset: typing.Optional[SpeakV2InterruptPlaybackOffset] = pydantic.Field(default=None) + """ + How much audio the client had played when the user barged in. Optional: without it the server cannot split the turn's text, so `SpeechInterrupted` omits `text_spoken` and `text_remaining`. + + The offset is cumulative from the start of the *session*, not from the start of the current turn. Each `Interrupt` must advance past the position the previous one established. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2interrupt_playback_offset.py b/src/deepgram/speak/v2/types/speak_v2interrupt_playback_offset.py new file mode 100644 index 00000000..dabe735e --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2interrupt_playback_offset.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel + + +class SpeakV2InterruptPlaybackOffset(UncheckedBaseModel): + """ + How much audio the client had played when the user barged in. Optional: without it the server cannot split the turn's text, so `SpeechInterrupted` omits `text_spoken` and `text_remaining`. + + The offset is cumulative from the start of the *session*, not from the start of the current turn. Each `Interrupt` must advance past the position the previous one established. + """ + + type: typing.Literal["time_ms"] = pydantic.Field(default="time_ms") + """ + Offset unit. `time_ms` is the only supported form. + """ + + value: int = pydantic.Field() + """ + Milliseconds of session audio the client played before barging in. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2session_metadata.py b/src/deepgram/speak/v2/types/speak_v2session_metadata.py index cf96e019..0e5b8c63 100644 --- a/src/deepgram/speak/v2/types/speak_v2session_metadata.py +++ b/src/deepgram/speak/v2/types/speak_v2session_metadata.py @@ -15,7 +15,7 @@ class SpeakV2SessionMetadata(UncheckedBaseModel): total_audio_duration_ms: int = pydantic.Field() """ - Cumulative audio duration produced across the session, in milliseconds + Cumulative audio duration produced across the session, in milliseconds. An `Interrupt` rebases this onto the audio the client actually played. """ total_input_character_count: int = pydantic.Field() diff --git a/src/deepgram/speak/v2/types/speak_v2speak.py b/src/deepgram/speak/v2/types/speak_v2speak.py index 6d327781..dc180777 100644 --- a/src/deepgram/speak/v2/types/speak_v2speak.py +++ b/src/deepgram/speak/v2/types/speak_v2speak.py @@ -15,7 +15,7 @@ class SpeakV2Speak(UncheckedBaseModel): text: str = pydantic.Field() """ - The input text to synthesize + The input text to synthesize. Inline pause and pronunciation controls are not yet applied; they are stripped from the text before synthesis. """ if IS_PYDANTIC_V2: diff --git a/src/deepgram/speak/v2/types/speak_v2speech_interrupted.py b/src/deepgram/speak/v2/types/speak_v2speech_interrupted.py new file mode 100644 index 00000000..7cfbb1ef --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2speech_interrupted.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel +from .speak_v2speech_interrupted_metadata import SpeakV2SpeechInterruptedMetadata + + +class SpeakV2SpeechInterrupted(UncheckedBaseModel): + type: typing.Literal["SpeechInterrupted"] = pydantic.Field(default="SpeechInterrupted") + """ + Message type identifier + """ + + audio_played_ms: int = pydantic.Field() + """ + How much audio the client had played when the interrupt landed, in milliseconds from the start of the session. Echoes the `Interrupt`'s `playback_offset` when one was supplied. Otherwise it is the server's own total, representing the audio that has been generated so far. A client that sends its first `Interrupt` without an offset can use this value as the baseline the next one must advance past. + """ + + text_spoken: typing.Optional[str] = pydantic.Field(default=None) + """ + The portion of the turn's text the user heard. Omitted when the `Interrupt` carried no `playback_offset`. + """ + + text_remaining: typing.Optional[str] = pydantic.Field(default=None) + """ + The portion of the turn's text the user did not hear. Omitted when the `Interrupt` carried no `playback_offset`. + """ + + metadata: SpeakV2SpeechInterruptedMetadata = pydantic.Field() + """ + Billing and timing for a single turn. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2speech_interrupted_metadata.py b/src/deepgram/speak/v2/types/speak_v2speech_interrupted_metadata.py new file mode 100644 index 00000000..45787d7c --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2speech_interrupted_metadata.py @@ -0,0 +1,48 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel +from .speak_v2speech_interrupted_metadata_controls_applied import SpeakV2SpeechInterruptedMetadataControlsApplied + + +class SpeakV2SpeechInterruptedMetadata(UncheckedBaseModel): + """ + Billing and timing for a single turn. + """ + + speech_id: str = pydantic.Field() + """ + Server-assigned turn identifier + """ + + audio_duration_ms: int = pydantic.Field() + """ + Audio duration produced for this turn, in milliseconds + """ + + input_character_count: int = pydantic.Field() + """ + Raw input character count for this turn, before text normalization + """ + + billable_character_count: int = pydantic.Field() + """ + Billable character count for this turn — the input character count with stripped control characters removed. Always less than or equal to `input_character_count`. + """ + + controls_applied: SpeakV2SpeechInterruptedMetadataControlsApplied = pydantic.Field() + """ + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2speech_interrupted_metadata_controls_applied.py b/src/deepgram/speak/v2/types/speak_v2speech_interrupted_metadata_controls_applied.py new file mode 100644 index 00000000..1d6776f3 --- /dev/null +++ b/src/deepgram/speak/v2/types/speak_v2speech_interrupted_metadata_controls_applied.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ....core.pydantic_utilities import IS_PYDANTIC_V2 +from ....core.unchecked_base_model import UncheckedBaseModel + + +class SpeakV2SpeechInterruptedMetadataControlsApplied(UncheckedBaseModel): + """ + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. + """ + + pronunciations_applied: int = pydantic.Field() + """ + Pronunciation overrides successfully applied. Mirrors the Aura-2 `dg-pronunciations-applied` REST header. Currently always `0`. + """ + + breaks_applied: int = pydantic.Field() + """ + Pause (break) controls successfully applied. Mirrors the Aura-2 `dg-breaks-applied` REST header. Currently always `0`. + """ + + pronunciation_warnings: int = pydantic.Field() + """ + Pronunciation entries that triggered a warning (invalid IPA, word too long). Mirrors the Aura-2 `dg-pronunciation-warnings` REST header. Currently always `0`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/deepgram/speak/v2/types/speak_v2speech_metadata.py b/src/deepgram/speak/v2/types/speak_v2speech_metadata.py index 9df85f0e..23aabd74 100644 --- a/src/deepgram/speak/v2/types/speak_v2speech_metadata.py +++ b/src/deepgram/speak/v2/types/speak_v2speech_metadata.py @@ -36,7 +36,7 @@ class SpeakV2SpeechMetadata(UncheckedBaseModel): controls_applied: SpeakV2SpeechMetadataControlsApplied = pydantic.Field() """ - Controls applied during the turn. Inline pronunciation and pause controls are not available during Early Access, so every count is currently `0`. + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. """ if IS_PYDANTIC_V2: diff --git a/src/deepgram/speak/v2/types/speak_v2speech_metadata_controls_applied.py b/src/deepgram/speak/v2/types/speak_v2speech_metadata_controls_applied.py index e24d03cd..124099af 100644 --- a/src/deepgram/speak/v2/types/speak_v2speech_metadata_controls_applied.py +++ b/src/deepgram/speak/v2/types/speak_v2speech_metadata_controls_applied.py @@ -9,17 +9,22 @@ class SpeakV2SpeechMetadataControlsApplied(UncheckedBaseModel): """ - Controls applied during the turn. Inline pronunciation and pause controls are not available during Early Access, so every count is currently `0`. + Counts of the inline controls the server acted on during the turn. Inline pause and pronunciation controls are not applied at launch — support is coming soon — so every count is currently `0`. """ pronunciations_applied: int = pydantic.Field() """ - Pronunciation overrides successfully applied. Mirrors the Aura-2 `dg-pronunciations-applied` REST header. Always `0` during Early Access. + Pronunciation overrides successfully applied. Mirrors the Aura-2 `dg-pronunciations-applied` REST header. Currently always `0`. + """ + + breaks_applied: int = pydantic.Field() + """ + Pause (break) controls successfully applied. Mirrors the Aura-2 `dg-breaks-applied` REST header. Currently always `0`. """ pronunciation_warnings: int = pydantic.Field() """ - Pronunciation entries that triggered a warning (invalid IPA, word too long). Mirrors the Aura-2 `dg-pronunciation-warnings` REST header. Always `0` during Early Access. + Pronunciation entries that triggered a warning (invalid IPA, word too long). Mirrors the Aura-2 `dg-pronunciation-warnings` REST header. Currently always `0`. """ if IS_PYDANTIC_V2: diff --git a/src/deepgram/speak/v2/types/speak_v2warning.py b/src/deepgram/speak/v2/types/speak_v2warning.py index 967b2279..1097e63a 100644 --- a/src/deepgram/speak/v2/types/speak_v2warning.py +++ b/src/deepgram/speak/v2/types/speak_v2warning.py @@ -15,7 +15,13 @@ class SpeakV2Warning(UncheckedBaseModel): code: str = pydantic.Field() """ - Warning code identifying the condition, in `SCREAMING_SNAKE_CASE`. Early Access codes are `NO_ACTIVE_SPEECH` (a speech-scoped message arrived with no active turn) and `SYNTHESIS_RETRYING` (a synthesis request failed and is being retried). + Warning code identifying the condition, in `SCREAMING_SNAKE_CASE`. + + Turn-scoped codes: `NO_ACTIVE_SPEECH` (a speech-scoped message arrived with no active turn), `NO_SYNTHESIZABLE_TEXT` (the turn's text was entirely whitespace or punctuation, so it produced no audio and is completed with a zero-duration `SpeechMetadata`).`SYNTHESIS_RETRYING` (a synthesis request failed and is being retried). + + Inline-control codes are reserved and not currently emitted, because inline pause and pronunciation controls are not yet applied: `BREAKS_LIMIT_EXCEEDED` (too many pause controls, or two pauses with no intervening text), `BREAK_TOKENS_OUT_OF_RANGE` (pause durations outside the range the model supports), `BREAK_TOKENS_WITH_INVALID_INCREMENTS` (pause durations off the model's supported increment), `PRONUNCIATION_WARNINGS` (a pronunciation override contained invalid IPA), `PRONUNCIATION_TOO_LONG` (an IPA string exceeded the length limit), `PRONUNCIATIONS_LIMIT_EXCEEDED` (too many pronunciation controls in one turn). + + Interrupt-scoped codes, each meaning the `Interrupt` was ignored: `NO_AUDIO_GENERATED` (the session has produced no audio yet, so there is nothing to interrupt), `INTERRUPT_IN_PROGRESS` (an earlier `Interrupt` is still being processed — at most one is handled at a time), `INVALID_INTERRUPT_OFFSET` (the `playback_offset` did not advance past the position a prior interrupt established). """ description: str = pydantic.Field() diff --git a/src/deepgram/types/__init__.py b/src/deepgram/types/__init__.py index fa49f377..d4a9b67d 100644 --- a/src/deepgram/types/__init__.py +++ b/src/deepgram/types/__init__.py @@ -6,6 +6,10 @@ from importlib import import_module if typing.TYPE_CHECKING: + from .google_think_provider_version import GoogleThinkProviderVersion + from .listen_v2redact import ListenV2Redact + from .speak_v2expressivity import SpeakV2Expressivity + from .speak_v2speed import SpeakV2Speed from .agent_configuration_v1 import AgentConfigurationV1 from .agent_think_models_v1response import AgentThinkModelsV1Response from .agent_think_models_v1response_models_item import AgentThinkModelsV1ResponseModelsItem @@ -289,6 +293,10 @@ from .usage_v1response import UsageV1Response from .usage_v1response_resolution import UsageV1ResponseResolution _dynamic_imports: typing.Dict[str, str] = { + "GoogleThinkProviderVersion": ".google_think_provider_version", + "ListenV2Redact": ".listen_v2redact", + "SpeakV2Expressivity": ".speak_v2expressivity", + "SpeakV2Speed": ".speak_v2speed", "AgentConfigurationV1": ".agent_configuration_v1", "AgentThinkModelsV1Response": ".agent_think_models_v1response", "AgentThinkModelsV1ResponseModelsItem": ".agent_think_models_v1response_models_item", @@ -558,6 +566,10 @@ def __dir__(): __all__ = [ + "GoogleThinkProviderVersion", + "ListenV2Redact", + "SpeakV2Expressivity", + "SpeakV2Speed", "AgentConfigurationV1", "AgentThinkModelsV1Response", "AgentThinkModelsV1ResponseModelsItem", diff --git a/src/deepgram/types/deepgram.py b/src/deepgram/types/deepgram.py index 88579177..ba3c095f 100644 --- a/src/deepgram/types/deepgram.py +++ b/src/deepgram/types/deepgram.py @@ -9,15 +9,19 @@ class Deepgram(UncheckedBaseModel): + """ + Deepgram text-to-speech provider. Aura models use version v1 (default); Flux TTS uses version v2 and a flux-* model. + """ + type: typing.Literal["deepgram"] = "deepgram" - version: typing.Optional[typing.Literal["v1"]] = pydantic.Field(default=None) + version: typing.Optional[str] = pydantic.Field(default=None) """ - The REST API version for the Deepgram text-to-speech API + The Deepgram text-to-speech model family. Accepted values: `v1` (Aura, the default) and `v2` (Flux TTS). Use `v1` with an aura-* model and `v2` with a flux-* model. Defaults to `v1` when omitted. """ model: DeepgramSpeakProviderModel = pydantic.Field() """ - Deepgram TTS model + Deepgram TTS model. Aura models (version v1) use the aura-* voices; Flux TTS (version v2) uses the flux-{voice}-{language} voices (e.g. flux-alexis-en). """ speed: typing.Optional[float] = pydantic.Field(default=None) diff --git a/src/deepgram/types/deepgram_speak_provider_model.py b/src/deepgram/types/deepgram_speak_provider_model.py index 72bcae5c..a4530678 100644 --- a/src/deepgram/types/deepgram_speak_provider_model.py +++ b/src/deepgram/types/deepgram_speak_provider_model.py @@ -67,6 +67,18 @@ "aura-2-selena-es", "aura-2-estrella-es", "aura-2-javier-es", + "flux-haley-en", + "flux-heather-en", + "flux-cole-en", + "flux-alexis-en", + "flux-priya-en", + "flux-jack-en", + "flux-bruce-en", + "flux-rufus-en", + "flux-drew-en", + "flux-renee-en", + "flux-marcus-en", + "flux-sharon-en", ], typing.Any, ] diff --git a/src/deepgram/types/google.py b/src/deepgram/types/google.py index 56e86997..463bbc52 100644 --- a/src/deepgram/types/google.py +++ b/src/deepgram/types/google.py @@ -6,13 +6,14 @@ from ..core.pydantic_utilities import IS_PYDANTIC_V2 from ..core.unchecked_base_model import UncheckedBaseModel from .google_think_provider_model import GoogleThinkProviderModel +from .google_think_provider_version import GoogleThinkProviderVersion class Google(UncheckedBaseModel): type: typing.Literal["google"] = "google" - version: typing.Optional[typing.Literal["v1beta"]] = pydantic.Field(default=None) + version: typing.Optional[GoogleThinkProviderVersion] = pydantic.Field(default=None) """ - The REST API version for the Google generative language API + The Google API used for the request: ai-studio-v1beta for the AI Studio API, or gemini-enterprise-agent-v1 for the Gemini Enterprise Agent (GEA) API. v1beta is accepted as an alias for ai-studio-v1beta. Defaults based on the Deepgram Voice Agent endpoint you connect to. """ model: GoogleThinkProviderModel = pydantic.Field() diff --git a/src/deepgram/types/google_think_provider_version.py b/src/deepgram/types/google_think_provider_version.py new file mode 100644 index 00000000..bc5e8997 --- /dev/null +++ b/src/deepgram/types/google_think_provider_version.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +GoogleThinkProviderVersion = typing.Union[ + typing.Literal["ai-studio-v1beta", "gemini-enterprise-agent-v1", "v1beta"], typing.Any +] diff --git a/src/deepgram/types/listen_v1keyterm.py b/src/deepgram/types/listen_v1keyterm.py index 67bd49f0..85e7d587 100644 --- a/src/deepgram/types/listen_v1keyterm.py +++ b/src/deepgram/types/listen_v1keyterm.py @@ -4,5 +4,9 @@ ListenV1Keyterm = typing.Any """ -Key term prompting can boost specialized terminology and brands. Only compatible with Nova-3 +Key term prompting improves recognition of specialized terminology and brands. Only compatible with Nova-3. + +`keyterm` accepts plain terms only. Unlike the legacy `keywords` feature, it does not support weights or intensifiers. Appending one (for example, `keyterm=term:0.15`) is not rejected—the weight is silently ignored and the entire value is treated as a literal keyterm. + +To boost multiple separate keyterms, repeat the `keyterm` parameter (for example, `keyterm=term1&keyterm=term2`). To boost one multi-word phrase as a single keyterm, join the words with `%20` or `+` (for example, `keyterm=customer%20service`). Do not separate keyterms with commas, semicolons, or line breaks. """ diff --git a/src/deepgram/types/listen_v2eot_timeout_ms.py b/src/deepgram/types/listen_v2eot_timeout_ms.py index 9f4c05c0..e47d48ad 100644 --- a/src/deepgram/types/listen_v2eot_timeout_ms.py +++ b/src/deepgram/types/listen_v2eot_timeout_ms.py @@ -5,5 +5,5 @@ ListenV2EotTimeoutMs = typing.Any """ A turn will be finished when this much time has passed after speech, -regardless of EOT confidence. +regardless of EOT confidence. Valid Values 500 - 60000. """ diff --git a/src/deepgram/types/listen_v2redact.py b/src/deepgram/types/listen_v2redact.py new file mode 100644 index 00000000..5484d329 --- /dev/null +++ b/src/deepgram/types/listen_v2redact.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ListenV2Redact = typing.Union[typing.Literal["numbers", "aggressive_numbers"], typing.Any] diff --git a/src/deepgram/types/speak_settings_v1provider.py b/src/deepgram/types/speak_settings_v1provider.py index 409122ed..5c696e33 100644 --- a/src/deepgram/types/speak_settings_v1provider.py +++ b/src/deepgram/types/speak_settings_v1provider.py @@ -21,7 +21,7 @@ class SpeakSettingsV1Provider_Deepgram(UncheckedBaseModel): type: typing.Literal["deepgram"] = "deepgram" - version: typing.Optional[typing.Literal["v1"]] = None + version: typing.Optional[str] = None model: DeepgramSpeakProviderModel speed: typing.Optional[float] = None diff --git a/src/deepgram/types/speak_v2expressivity.py b/src/deepgram/types/speak_v2expressivity.py new file mode 100644 index 00000000..5b6b14b9 --- /dev/null +++ b/src/deepgram/types/speak_v2expressivity.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +SpeakV2Expressivity = int +""" +Expressive range of the generated speech. Accepted values: `-2`, `-1`, `0`, `1`, `2`. `0` is the voice's nominal delivery; negative values are flatter and more restrained, positive values more animated. +""" diff --git a/src/deepgram/types/speak_v2speed.py b/src/deepgram/types/speak_v2speed.py new file mode 100644 index 00000000..de5837f3 --- /dev/null +++ b/src/deepgram/types/speak_v2speed.py @@ -0,0 +1,6 @@ +# This file was auto-generated by Fern from our API Definition. + +SpeakV2Speed = float +""" +Speech-rate multiplier. `1.0` is the model's nominal rate; lower is slower. Accepted values: `0.85`, `0.90`, `0.95`, `1.00`, `1.05`, `1.10`, `1.15`. Any other value is rejected with `SPEED_OUT_OF_RANGE`, and a value off the `0.05` increment with `SPEED_INCREMENT_INVALID`. +""" diff --git a/src/deepgram/types/think_settings_v1provider.py b/src/deepgram/types/think_settings_v1provider.py index 756dc5ab..8d08f092 100644 --- a/src/deepgram/types/think_settings_v1provider.py +++ b/src/deepgram/types/think_settings_v1provider.py @@ -12,6 +12,7 @@ from .aws_bedrock_think_provider_credentials import AwsBedrockThinkProviderCredentials from .aws_bedrock_think_provider_model import AwsBedrockThinkProviderModel from .google_think_provider_model import GoogleThinkProviderModel +from .google_think_provider_version import GoogleThinkProviderVersion from .groq_think_provider_reasoning_mode import GroqThinkProviderReasoningMode from .open_ai_think_provider_model import OpenAiThinkProviderModel from .open_ai_think_provider_reasoning_mode import OpenAiThinkProviderReasoningMode @@ -68,7 +69,7 @@ class Config: class ThinkSettingsV1Provider_Google(UncheckedBaseModel): type: typing.Literal["google"] = "google" - version: typing.Optional[typing.Literal["v1beta"]] = None + version: typing.Optional[GoogleThinkProviderVersion] = None model: GoogleThinkProviderModel temperature: typing.Optional[float] = None diff --git a/tests/custom/test_agent_update_listen.py b/tests/custom/test_agent_update_listen.py index 667f7623..8ce00062 100644 --- a/tests/custom/test_agent_update_listen.py +++ b/tests/custom/test_agent_update_listen.py @@ -16,7 +16,12 @@ from deepgram.agent.v1.types.agent_v1listen_updated import AgentV1ListenUpdated from deepgram.agent.v1.types.agent_v1update_listen import AgentV1UpdateListen from deepgram.agent.v1.types.agent_v1update_listen_listen import AgentV1UpdateListenListen +from deepgram.agent.v1.types.agent_v1update_listen_listen_provider import ( + AgentV1UpdateListenListenProvider_V1, + AgentV1UpdateListenListenProvider_V2, +) from deepgram.core.unchecked_base_model import construct_type +from deepgram.types.deepgram_listen_provider_v1 import DeepgramListenProviderV1 from deepgram.types.deepgram_listen_provider_v2 import DeepgramListenProviderV2 @@ -46,6 +51,115 @@ def test_serializes_provider_on_the_wire(self): assert sent["type"] == "UpdateListen" assert sent["listen"]["provider"]["type"] == "deepgram" assert sent["listen"]["provider"]["model"] == "flux-general-en" + # The legacy (unversioned) provider must be coerced to the v2 member of + # the AgentV1UpdateListenListenProvider union added in the 2026-07-31 + # regen -- that coercion is the frozen patch on + # agent_v1update_listen_listen.py. + assert sent["listen"]["provider"]["version"] == "v2" + + def test_legacy_v1_provider_is_coerced_to_v1_member(self): + ws = _FakeWebSocket() + message = AgentV1UpdateListen( + listen=AgentV1UpdateListenListen(provider=DeepgramListenProviderV1(model="nova-3")) + ) + V1SocketClient(websocket=ws).send_update_listen(message) + + provider = _sent_json(ws)["listen"]["provider"] + assert provider["version"] == "v1" + assert provider["model"] == "nova-3" + + def test_legacy_dict_provider_is_coerced(self): + ws = _FakeWebSocket() + message = AgentV1UpdateListen( + listen=AgentV1UpdateListenListen(provider={"type": "deepgram", "model": "flux-general-en"}) + ) + V1SocketClient(websocket=ws).send_update_listen(message) + + provider = _sent_json(ws)["listen"]["provider"] + assert provider["version"] == "v2" + assert provider["model"] == "flux-general-en" + + def test_legacy_dict_language_hint_is_remapped(self): + # The API uses deny_unknown_fields, so the deprecated singular + # `language_hint` must be remapped to `language_hints` and dropped -- + # on the raw-dict path as well as the model-instance path. Before this + # was fixed the dict path shipped the dead singular key and the server + # rejected the whole UpdateListen with no client-side signal. + ws = _FakeWebSocket() + message = AgentV1UpdateListen( + listen=AgentV1UpdateListenListen( + provider={"type": "deepgram", "model": "flux-general-multi", "language_hint": "en"} + ) + ) + V1SocketClient(websocket=ws).send_update_listen(message) + + provider = _sent_json(ws)["listen"]["provider"] + assert provider["language_hints"] == ["en"] + assert "language_hint" not in provider + assert provider["version"] == "v2" + + def test_legacy_dict_version_is_inferred_from_shape(self): + # A dict carrying V1-only fields must not be stamped "v2": that would + # declare a Flux/v2 provider while carrying v1-only keys. + ws = _FakeWebSocket() + V1SocketClient(websocket=ws).send_update_listen( + AgentV1UpdateListen( + listen=AgentV1UpdateListenListen( + provider={ + "type": "deepgram", + "model": "nova-3", + "language": "en", + "smart_format": True, + } + ) + ) + ) + provider = _sent_json(ws)["listen"]["provider"] + assert provider["version"] == "v1" + assert provider["language"] == "en" + assert provider["smart_format"] is True + + def test_coercion_never_raises_on_a_malformed_dict(self): + # The coercion is a compat shim; it must not become a new source of + # exceptions for input the pre-regen code passed through untouched. + from deepgram.agent.v1.types.agent_v1update_listen_listen import ( + _coerce_legacy_update_listen_provider, + ) + + out = _coerce_legacy_update_listen_provider({"provider": {"type": "deepgram"}}) + assert out["provider"]["version"] == "v2" + + def test_native_union_v2_provider_round_trips(self): + # The generator-native form must keep working alongside the legacy shim. + ws = _FakeWebSocket() + message = AgentV1UpdateListen( + listen=AgentV1UpdateListenListen( + provider=AgentV1UpdateListenListenProvider_V2( + model="flux-general-en", language_hints=["en"], eot_threshold=0.8 + ) + ) + ) + V1SocketClient(websocket=ws).send_update_listen(message) + + provider = _sent_json(ws)["listen"]["provider"] + assert provider["version"] == "v2" + assert provider["model"] == "flux-general-en" + assert provider["language_hints"] == ["en"] + assert provider["eot_threshold"] == 0.8 + + def test_native_union_v1_provider_round_trips(self): + ws = _FakeWebSocket() + message = AgentV1UpdateListen( + listen=AgentV1UpdateListenListen( + provider=AgentV1UpdateListenListenProvider_V1(model="nova-3", smart_format=True) + ) + ) + V1SocketClient(websocket=ws).send_update_listen(message) + + provider = _sent_json(ws)["listen"]["provider"] + assert provider["version"] == "v1" + assert provider["model"] == "nova-3" + assert provider["smart_format"] is True class TestListenUpdatedResponse: diff --git a/tests/custom/test_api_error_redaction.py b/tests/custom/test_api_error_redaction.py new file mode 100644 index 00000000..5bba78e9 --- /dev/null +++ b/tests/custom/test_api_error_redaction.py @@ -0,0 +1,80 @@ +""" +The SDK's error types must never carry a credential. + +Every websocket ``connect()`` path raises ``ApiError(headers=dict(headers), ...)`` +with the *full* request headers, and both error types stringify their headers +dict. Before this was fixed, a failed connect produced: + + headers: {'Authorization': 'Token ba06ae87...', ...}, status_code: 400, ... + +so any application that logged the exception — ``print(e)``, a traceback, a log +aggregator, an error tracker — wrote the customer's API key in clear text. This +is the same threat ``_secure_logging.py`` already covers for the ``websockets`` +DEBUG handshake logs; the exception path is the other route to it. + +Redaction happens at construction rather than only in ``__str__`` so it also +covers ``repr()`` and attribute serialisation (an error tracker sends both). +Non-sensitive headers are deliberately preserved: ``dg-request-id`` is the main +reason to inspect them at all. + +Hand-written and frozen in ``.fernignore`` alongside the two patched files. +""" + +import pytest + +from deepgram.core.api_error import ApiError +from deepgram.core.parse_error import ParsingError + +_SECRET = "abcd1234abcd1234abcd1234abcd1234abcd1234" + +_HEADERS = { + "Authorization": f"Token {_SECRET}", + "Proxy-Authorization": f"bearer {_SECRET}", + "dg-request-id": "req-abc-123", + "User-Agent": "deepgram-sdk/9.9.9", +} + + +def _build(cls): + kwargs = {"cause": ValueError("boom")} if cls is ParsingError else {} + return cls( + headers=dict(_HEADERS), + status_code=400, + body="Unexpected error when initializing websocket connection.", + **kwargs, + ) + + +@pytest.mark.parametrize("cls", [ApiError, ParsingError]) +class TestCredentialRedaction: + def test_secret_absent_from_str(self, cls): + assert _SECRET not in str(_build(cls)) + + def test_secret_absent_from_repr(self, cls): + # An error tracker serialises repr/attributes, not just the message. + assert _SECRET not in repr(_build(cls)) + + def test_secret_absent_from_headers_attribute(self, cls): + err = _build(cls) + assert not any(_SECRET in str(v) for v in (err.headers or {}).values()) + + def test_auth_scheme_is_preserved(self, cls): + # Keep the scheme so "was I sending a Token or a bearer?" stays answerable. + err = _build(cls) + assert err.headers["Authorization"] == "Token [REDACTED]" + assert err.headers["Proxy-Authorization"] == "bearer [REDACTED]" + + def test_non_sensitive_headers_survive(self, cls): + err = _build(cls) + assert err.headers["dg-request-id"] == "req-abc-123" + assert err.headers["User-Agent"] == "deepgram-sdk/9.9.9" + + def test_none_headers_stay_none(self, cls): + kwargs = {"cause": None} if cls is ParsingError else {} + assert cls(headers=None, status_code=500, body="x", **kwargs).headers is None + + def test_redaction_never_raises_on_odd_headers(self, cls): + # Redaction must not be the reason an error path fails. + kwargs = {"cause": None} if cls is ParsingError else {} + err = cls(headers="not-a-mapping", status_code=500, body="x", **kwargs) # type: ignore[arg-type] + assert err.headers is None diff --git a/tests/custom/test_listen_v2_connect_wire.py b/tests/custom/test_listen_v2_connect_wire.py index d1ec2d64..1f914438 100644 --- a/tests/custom/test_listen_v2_connect_wire.py +++ b/tests/custom/test_listen_v2_connect_wire.py @@ -74,6 +74,34 @@ def test_sync_connect_targets_v2_listen_and_serializes_numerals(): assert query["numerals"] == ["true"] +def test_sync_connect_serializes_redact(): + # `redact` (ListenV2Redact) was added in the 2026-08-11 regen; pin its + # query-string shape so a future regen cannot silently drop it. + capture = _CaptureConnect() + with patch.object(listen_v2_client.websockets_sync_client, "connect", capture): + with DeepgramClient(api_key="test_api_key").listen.v2.connect( + model="flux-general-en", + redact="aggressive_numbers", + ): + pass + + _, query = _path_and_query(capture.url) + assert query["redact"] == ["aggressive_numbers"] + + +async def test_async_connect_serializes_redact(): + capture = _CaptureConnect() + with patch.object(listen_v2_client, "websockets_client_connect", capture): + async with AsyncDeepgramClient(api_key="test_api_key").listen.v2.connect( + model="flux-general-en", + redact="numbers", + ): + pass + + _, query = _path_and_query(capture.url) + assert query["redact"] == ["numbers"] + + def test_sync_connect_omits_numerals_when_absent(): capture = _CaptureConnect() with patch.object(listen_v2_client.websockets_sync_client, "connect", capture): diff --git a/tests/custom/test_secure_logging.py b/tests/custom/test_secure_logging.py index e895c0a8..097b6998 100644 --- a/tests/custom/test_secure_logging.py +++ b/tests/custom/test_secure_logging.py @@ -15,6 +15,7 @@ RedactCredentialsFilter, _mask_value, install_websocket_log_redaction, + redact_sensitive_headers, uninstall_websocket_log_redaction, ) @@ -66,6 +67,34 @@ def test_non_sensitive_headers_are_untouched(caplog): assert msg == "> Sec-WebSocket-Version: 13" +def test_sec_websocket_protocol_is_redacted(caplog): + """The Sec-WebSocket-Protocol header carries the API key in the browser auth + pattern the connect() docstrings recommend (``"token, "``).""" + install_websocket_log_redaction() + msg = _emit_header(caplog, "websockets.client", "Sec-WebSocket-Protocol", "token, sk_live_SECRET123") + assert "sk_live_SECRET123" not in msg + assert "token, [REDACTED]" in msg # protocol marker kept, key masked + + +def test_redact_sensitive_headers_masks_sec_websocket_protocol(): + """redact_sensitive_headers (used by ApiError/ParsingError) must mask the key + carried in Sec-WebSocket-Protocol, not just Authorization.""" + masked = redact_sensitive_headers( + {"Sec-WebSocket-Protocol": "token, sk_live_SECRET123", "dg-request-id": "abc"} + ) + assert masked is not None + assert "sk_live_SECRET123" not in str(masked) + assert masked["Sec-WebSocket-Protocol"] == "token, [REDACTED]" + assert masked["dg-request-id"] == "abc" # non-sensitive header preserved + + +def test_redact_sensitive_headers_masks_authorization(): + masked = redact_sensitive_headers({"Authorization": "Token sk_live_SECRET123"}) + assert masked is not None + assert "sk_live_SECRET123" not in str(masked) + assert masked["Authorization"] == "Token [REDACTED]" + + def test_redaction_is_case_insensitive_on_header_name(caplog): install_websocket_log_redaction() msg = _emit_header(caplog, "websockets.client", "authorization", "Token abc123") diff --git a/tests/custom/test_speak_v2_interrupt_configure.py b/tests/custom/test_speak_v2_interrupt_configure.py new file mode 100644 index 00000000..57eda85c --- /dev/null +++ b/tests/custom/test_speak_v2_interrupt_configure.py @@ -0,0 +1,273 @@ +""" +Coverage for the Speak V2 (Flux TTS) barge-in and mid-stream reconfiguration +surface added in the 2026-08-11 regen. ``test_speak_v2_socket.py`` pins the +speak/flush/close surface and forward compatibility; this file pins what that +regen introduced, which shipped with no automated coverage: + + * ``send_interrupt`` — barge-in, with and without a ``playback_offset``. + * ``send_configure`` — mid-stream ``speed`` changes. + * response parsing — ``SpeechInterrupted`` (including nested metadata and the + ``controls_applied`` counters), plus the ``ConfigureSuccess`` / + ``ConfigureFailure`` acknowledgements. + * connect query — the ``speed`` and ``expressivity`` parameters. + +Both ``send_*`` methods here are generator-owned (they take a required message +model, unlike the patched no-payload ``send_flush``/``send_close``), so these +tests guard the wire shape a future regen must not silently change. + +Note on ``_declared_fields``: these models are ``extra="allow"``, so asserting +``msg.`` alone passes even when the field is *not declared* — the value +just arrives as an undeclared extra. Where the point is to pin the schema (not +just the value), assert against the declared field set instead. + +Driven with a fake websocket / a capture shim — no network. +""" + +import json +import urllib.parse +from unittest.mock import patch + +import deepgram.speak.v2.client as speak_v2_client +from deepgram import AsyncDeepgramClient, DeepgramClient +from deepgram.core.pydantic_utilities import IS_PYDANTIC_V2 +from deepgram.speak.v2.socket_client import AsyncV2SocketClient, V2SocketClient +from deepgram.speak.v2.types.speak_v2configure import SpeakV2Configure +from deepgram.speak.v2.types.speak_v2configure_failure import SpeakV2ConfigureFailure +from deepgram.speak.v2.types.speak_v2configure_success import SpeakV2ConfigureSuccess +from deepgram.speak.v2.types.speak_v2interrupt import SpeakV2Interrupt +from deepgram.speak.v2.types.speak_v2interrupt_playback_offset import SpeakV2InterruptPlaybackOffset +from deepgram.speak.v2.types.speak_v2speech_interrupted import SpeakV2SpeechInterrupted +from deepgram.speak.v2.types.speak_v2speech_interrupted_metadata import SpeakV2SpeechInterruptedMetadata +from deepgram.speak.v2.types.speak_v2speech_interrupted_metadata_controls_applied import ( + SpeakV2SpeechInterruptedMetadataControlsApplied, +) + +# A full server-emitted SpeechInterrupted frame. Every field on the nested +# metadata is required by the model, so this doubles as a shape assertion. +_SPEECH_INTERRUPTED = { + "type": "SpeechInterrupted", + "audio_played_ms": 1200, + "text_spoken": "Hello there", + "text_remaining": "how are you?", + "metadata": { + "speech_id": "speech-1", + "audio_duration_ms": 4000, + "input_character_count": 24, + "billable_character_count": 24, + "controls_applied": { + "pronunciations_applied": 2, + "breaks_applied": 1, + "pronunciation_warnings": 0, + }, + }, +} + + +def _declared_fields(model_cls): + """The model's *declared* field names (works on pydantic v1 and v2).""" + return set(model_cls.model_fields if IS_PYDANTIC_V2 else model_cls.__fields__) + + +class _FakeWebSocket: + """Captures ``.send()`` payloads and replays ``incoming`` on recv.""" + + def __init__(self, incoming=None): + self.sent = [] + self._incoming = list(incoming or []) + + def send(self, data): + self.sent.append(data) + + def recv(self): + return self._incoming.pop(0) + + def __iter__(self): + yield from self._incoming + + +class _FakeAsyncWebSocket: + """Async analogue of ``_FakeWebSocket``.""" + + def __init__(self, incoming=None): + self.sent = [] + self._incoming = list(incoming or []) + + async def send(self, data): + self.sent.append(data) + + async def recv(self): + return self._incoming.pop(0) + + +def _sent_json(ws): + assert len(ws.sent) == 1 + payload = ws.sent[0] + return json.loads(payload) if isinstance(payload, str) else payload + + +class TestSpeakV2Interrupt: + def test_send_interrupt_serializes_bare(self): + ws = _FakeWebSocket() + V2SocketClient(websocket=ws).send_interrupt(SpeakV2Interrupt()) + assert _sent_json(ws) == {"type": "Interrupt"} + + def test_send_interrupt_no_arg_emits_default(self): + # Interrupt carries no required payload, so it is callable with no + # argument like the sibling send_flush()/send_close() controls + # (hand-applied shim, frozen in .fernignore). + ws = _FakeWebSocket() + V2SocketClient(websocket=ws).send_interrupt() + assert _sent_json(ws) == {"type": "Interrupt"} + + async def test_async_send_interrupt_no_arg_emits_default(self): + ws = _FakeAsyncWebSocket() + await AsyncV2SocketClient(websocket=ws).send_interrupt() + assert _sent_json(ws) == {"type": "Interrupt"} + + def test_send_interrupt_serializes_playback_offset(self): + ws = _FakeWebSocket() + V2SocketClient(websocket=ws).send_interrupt( + SpeakV2Interrupt(playback_offset=SpeakV2InterruptPlaybackOffset(value=1500)) + ) + assert _sent_json(ws) == { + "type": "Interrupt", + "playback_offset": {"type": "time_ms", "value": 1500}, + } + + async def test_async_send_interrupt_serializes(self): + ws = _FakeAsyncWebSocket() + await AsyncV2SocketClient(websocket=ws).send_interrupt(SpeakV2Interrupt()) + assert _sent_json(ws) == {"type": "Interrupt"} + + def test_recv_parses_speech_interrupted(self): + ws = _FakeWebSocket(incoming=[json.dumps(_SPEECH_INTERRUPTED)]) + msg = V2SocketClient(websocket=ws).recv() + + assert isinstance(msg, SpeakV2SpeechInterrupted) + assert msg.audio_played_ms == 1200 + assert msg.text_spoken == "Hello there" + assert msg.text_remaining == "how are you?" + assert msg.metadata.speech_id == "speech-1" + assert msg.metadata.billable_character_count == 24 + assert msg.metadata.controls_applied.breaks_applied == 1 + assert msg.metadata.controls_applied.pronunciations_applied == 2 + + def test_speech_interrupted_metadata_schema_is_declared(self): + # extra="allow" means value assertions alone can't detect a dropped + # field, so pin the declared schema explicitly. + assert { + "speech_id", + "audio_duration_ms", + "input_character_count", + "billable_character_count", + "controls_applied", + } <= _declared_fields(SpeakV2SpeechInterruptedMetadata) + assert { + "pronunciations_applied", + "breaks_applied", + "pronunciation_warnings", + } <= _declared_fields(SpeakV2SpeechInterruptedMetadataControlsApplied) + + async def test_async_recv_parses_speech_interrupted(self): + ws = _FakeAsyncWebSocket(incoming=[json.dumps(_SPEECH_INTERRUPTED)]) + msg = await AsyncV2SocketClient(websocket=ws).recv() + assert isinstance(msg, SpeakV2SpeechInterrupted) + assert msg.metadata.controls_applied.breaks_applied == 1 + + +class TestSpeakV2Configure: + def test_send_configure_serializes_speed(self): + ws = _FakeWebSocket() + V2SocketClient(websocket=ws).send_configure(SpeakV2Configure(speed=1.25)) + assert _sent_json(ws) == {"type": "Configure", "speed": 1.25} + + async def test_async_send_configure_serializes_speed(self): + ws = _FakeAsyncWebSocket() + await AsyncV2SocketClient(websocket=ws).send_configure(SpeakV2Configure(speed=0.8)) + assert _sent_json(ws) == {"type": "Configure", "speed": 0.8} + + def test_recv_parses_configure_success(self): + ws = _FakeWebSocket( + incoming=[json.dumps({"type": "ConfigureSuccess", "applied": {"speed": 1.25}})] + ) + msg = V2SocketClient(websocket=ws).recv() + assert isinstance(msg, SpeakV2ConfigureSuccess) + assert msg.applied.speed == 1.25 + + def test_recv_parses_configure_failure(self): + ws = _FakeWebSocket( + incoming=[ + json.dumps( + { + "type": "ConfigureFailure", + "code": "SPEED_OUT_OF_RANGE", + "field": "speed", + "value": 9.0, + "description": "speed must be between 0.5 and 2.0", + } + ) + ] + ) + msg = V2SocketClient(websocket=ws).recv() + assert isinstance(msg, SpeakV2ConfigureFailure) + assert msg.code == "SPEED_OUT_OF_RANGE" + assert msg.field == "speed" + assert msg.value == 9.0 + assert "0.5" in msg.description + + +class _CaptureConnect: + """Stands in for ``websockets(.sync).connect``: records the URL it is given.""" + + def __init__(self): + self.url = None + + def __call__(self, url, **kwargs): + self.url = url + return self + + def __enter__(self): + return object() + + def __exit__(self, *exc): + return False + + async def __aenter__(self): + return object() + + async def __aexit__(self, *exc): + return False + + +def _query(url): + return urllib.parse.parse_qs(urllib.parse.urlsplit(url).query) + + +def test_sync_connect_serializes_speed_and_expressivity(): + capture = _CaptureConnect() + with patch.object(speak_v2_client.websockets_sync_client, "connect", capture): + with DeepgramClient(api_key="test_api_key").speak.v2.connect( + model="flux-alexis-en", + speed=1.25, + expressivity=3, + ): + pass + + query = _query(capture.url) + assert query["speed"] == ["1.25"] + assert query["expressivity"] == ["3"] + + +async def test_async_connect_serializes_speed_and_expressivity(): + capture = _CaptureConnect() + with patch.object(speak_v2_client, "websockets_client_connect", capture): + async with AsyncDeepgramClient(api_key="test_api_key").speak.v2.connect( + model="flux-alexis-en", + speed=0.75, + expressivity=1, + ): + pass + + query = _query(capture.url) + assert query["speed"] == ["0.75"] + assert query["expressivity"] == ["1"] diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py index 9bcc5235..8a8f2387 100644 --- a/tests/utils/test_http_client.py +++ b/tests/utils/test_http_client.py @@ -1,6 +1,6 @@ # This file was auto-generated by Fern from our API Definition. -from typing import Any, Dict +from typing import Any, Dict, Tuple, cast from unittest.mock import AsyncMock, MagicMock, patch import httpx @@ -692,3 +692,70 @@ def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None: def test_should_retry_599_upper_boundary() -> None: """Legacy mode retries on >= 500, which includes 599.""" assert _should_retry(_make_response(599)) is True + + +# --------------------------------------------------------------------------- +# RequestOptions timeout resolution tests (timeout / deprecated timeout_in_seconds) +# --------------------------------------------------------------------------- + + +def _sync_client_with_base_timeout(base_timeout: Any) -> Tuple[HttpClient, _DummySyncClient]: + dummy_client = _DummySyncClient() + http_client = HttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: base_timeout, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + ) + return http_client, dummy_client + + +def test_sync_request_options_timeout_used() -> None: + """The new `timeout` request option is passed through to httpx (in seconds).""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options={"timeout": 30}) + assert dummy_client.last_request_kwargs["timeout"] == 30 + + +def test_sync_request_options_timeout_in_seconds_still_works() -> None: + """The deprecated `timeout_in_seconds` request option remains backwards compatible.""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options={"timeout_in_seconds": 45}) + assert dummy_client.last_request_kwargs["timeout"] == 45 + + +def test_sync_request_options_timeout_takes_precedence() -> None: + """When both are set, `timeout` wins over `timeout_in_seconds` (same seconds unit).""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45}) + assert dummy_client.last_request_kwargs["timeout"] == 30 + + +def test_sync_request_options_timeout_falls_back_to_base() -> None: + """When neither key is set, the client-level base timeout is used.""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + http_client.request(path="/test", method="GET", request_options=None) + assert dummy_client.last_request_kwargs["timeout"] == 60 + + +def test_sync_request_options_timeout_none_falls_back_to_deprecated() -> None: + """An explicit `timeout=None` (dynamic caller) falls back to the deprecated `timeout_in_seconds`.""" + http_client, dummy_client = _sync_client_with_base_timeout(60) + request_options = cast(RequestOptions, {"timeout": None, "timeout_in_seconds": 45}) + http_client.request(path="/test", method="GET", request_options=request_options) + assert dummy_client.last_request_kwargs["timeout"] == 45 + + +@pytest.mark.asyncio +async def test_async_request_options_timeout_takes_precedence() -> None: + """Async: `timeout` wins over the deprecated `timeout_in_seconds`.""" + dummy_client = _DummyAsyncClient() + http_client = AsyncHttpClient( + httpx_client=dummy_client, # type: ignore[arg-type] + base_timeout=lambda: 60, + base_headers=lambda: {}, + base_url=lambda: "https://example.com", + async_base_headers=None, + ) + await http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45}) + assert dummy_client.last_request_kwargs["timeout"] == 30 diff --git a/tests/wire/test_manage_v1_projects_requests.py b/tests/wire/test_manage_v1_projects_requests.py index fd361042..acf577b0 100644 --- a/tests/wire/test_manage_v1_projects_requests.py +++ b/tests/wire/test_manage_v1_projects_requests.py @@ -8,7 +8,24 @@ def test_manage_v1_projects_requests_list_() -> None: test_id = "manage.v1.projects.requests.list_.0" client = get_client(test_id) client.manage.v1.projects.requests.list( - project_id="123456-7890-1234-5678-901234", + project_id="12345678-90ab-cdef-1234-567890abcdef", + ) + verify_request_count(test_id, "GET", "/v1/projects/12345678-90ab-cdef-1234-567890abcdef/requests", None, 1) + + +def test_manage_v1_projects_requests_list_serializes_all_query_params() -> None: + """All ten optional query params must reach the wire, with datetime encoded as ISO-8601 Z. + + The 2026-08-11 regen simplified the upstream spec *example* for this endpoint, + and Fern derives the wire test from that example -- so the generated test + dropped every query parameter, leaving their serialization unverified while + the client signature still forwards all ten. This restores that coverage; the + file is frozen in .fernignore so a future regen cannot silently drop it again. + """ + test_id = "manage.v1.projects.requests.list_.query_params" + client = get_client(test_id) + client.manage.v1.projects.requests.list( + project_id="12345678-90ab-cdef-1234-567890abcdef", start=datetime.datetime.fromisoformat("2024-01-15T09:30:00+00:00"), end=datetime.datetime.fromisoformat("2024-01-15T09:30:00+00:00"), limit=1.1, @@ -23,7 +40,7 @@ def test_manage_v1_projects_requests_list_() -> None: verify_request_count( test_id, "GET", - "/v1/projects/123456-7890-1234-5678-901234/requests", + "/v1/projects/12345678-90ab-cdef-1234-567890abcdef/requests", { "start": "2024-01-15T09:30:00Z", "end": "2024-01-15T09:30:00Z", @@ -45,9 +62,13 @@ def test_manage_v1_projects_requests_get() -> None: test_id = "manage.v1.projects.requests.get.0" client = get_client(test_id) client.manage.v1.projects.requests.get( - project_id="123456-7890-1234-5678-901234", - request_id="123456-7890-1234-5678-901234", + project_id="12345678-90ab-cdef-1234-567890abcdef", + request_id="a3f1c9d2-4b7e-4f9a-8c3d-2e5f7b9a1c0d", ) verify_request_count( - test_id, "GET", "/v1/projects/123456-7890-1234-5678-901234/requests/123456-7890-1234-5678-901234", None, 1 + test_id, + "GET", + "/v1/projects/12345678-90ab-cdef-1234-567890abcdef/requests/a3f1c9d2-4b7e-4f9a-8c3d-2e5f7b9a1c0d", + None, + 1, ) diff --git a/wiremock/wiremock-mappings.json b/wiremock/wiremock-mappings.json index 67ae5cc3..8d49c170 100644 --- a/wiremock/wiremock-mappings.json +++ b/wiremock/wiremock-mappings.json @@ -774,8 +774,8 @@ } }, { - "id": "d6a14959-05fa-4aec-9f0c-ba2a817c66e5", - "name": "List Project Requests - default", + "id": "1aa49f93-05d3-41e6-a9b6-b87abe58f653", + "name": "List Project Requests - List Project Requests", "request": { "urlPathTemplate": "/v1/projects/{project_id}/requests", "method": "GET", @@ -786,50 +786,18 @@ }, "pathParameters": { "project_id": { - "equalTo": "123456-7890-1234-5678-901234" - } - }, - "queryParameters": { - "start": { - "equalTo": "2024-01-15T09:30:00Z" - }, - "end": { - "equalTo": "2024-01-15T09:30:00Z" - }, - "limit": { - "equalTo": "1.1" - }, - "page": { - "equalTo": "1.1" - }, - "accessor": { - "equalTo": "12345678-1234-1234-1234-123456789012" - }, - "request_id": { - "equalTo": "12345678-1234-1234-1234-123456789012" - }, - "deployment": { - "equalTo": "hosted" - }, - "endpoint": { - "equalTo": "listen" - }, - "method": { - "equalTo": "sync" - }, - "status": { - "equalTo": "succeeded" + "equalTo": "12345678-90ab-cdef-1234-567890abcdef" } } }, "response": { "status": 200, - "body": "{\n \"page\": 1.1,\n \"limit\": 1.1,\n \"requests\": [\n {\n \"request_id\": \"request_id\",\n \"project_uuid\": \"project_uuid\",\n \"created\": \"2024-01-15T09:30:00Z\",\n \"path\": \"path\",\n \"api_key_id\": \"api_key_id\",\n \"response\": {\n \"key\": \"value\"\n },\n \"code\": 1.1,\n \"deployment\": \"deployment\",\n \"callback\": \"callback\"\n }\n ]\n}", + "body": "{\n \"page\": 0,\n \"limit\": 10,\n \"requests\": [\n {\n \"request_id\": \"a3f1c9d2-4b7e-4f9a-8c3d-2e5f7b9a1c0d\",\n \"project_uuid\": \"12345678-90ab-cdef-1234-567890abcdef\",\n \"created\": \"2024-01-15T09:48:20.000Z\",\n \"path\": \"/v1/listen?\",\n \"api_key_id\": \"b1e2c3d4-5678-90ab-cdef-1234567890ab\",\n \"response\": {\n \"details\": {\n \"usd\": 0.0075,\n \"duration\": 30,\n \"total_audio\": 30,\n \"channels\": 1,\n \"streams\": 1,\n \"tier\": \"base\",\n \"metadata\": {},\n \"models\": [\n \"1a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d\"\n ],\n \"method\": \"sync\",\n \"tags\": [],\n \"features\": [],\n \"config\": {}\n },\n \"token_details\": [],\n \"code\": 200,\n \"completed\": \"2024-01-15T09:48:21.000Z\",\n \"deployment\": \"hosted:us\"\n },\n \"callback\": null\n }\n ]\n}", "headers": { "Content-Type": "application/json" } }, - "uuid": "d6a14959-05fa-4aec-9f0c-ba2a817c66e5", + "uuid": "1aa49f93-05d3-41e6-a9b6-b87abe58f653", "persistent": true, "priority": 3, "metadata": { @@ -842,8 +810,8 @@ } }, { - "id": "733e39aa-d3ef-4ea7-8062-af080c6288c4", - "name": "Get a Project Request - default", + "id": "0c5ca54d-c4a7-42fd-b595-b0c6d34b82f9", + "name": "Get a Project Request - Get a Project Request", "request": { "urlPathTemplate": "/v1/projects/{project_id}/requests/{request_id}", "method": "GET", @@ -854,21 +822,21 @@ }, "pathParameters": { "project_id": { - "equalTo": "123456-7890-1234-5678-901234" + "equalTo": "12345678-90ab-cdef-1234-567890abcdef" }, "request_id": { - "equalTo": "123456-7890-1234-5678-901234" + "equalTo": "a3f1c9d2-4b7e-4f9a-8c3d-2e5f7b9a1c0d" } } }, "response": { "status": 200, - "body": "{\n \"request\": {\n \"request_id\": \"request_id\",\n \"project_uuid\": \"project_uuid\",\n \"created\": \"2024-01-15T09:30:00Z\",\n \"path\": \"path\",\n \"api_key_id\": \"api_key_id\",\n \"response\": {\n \"key\": \"value\"\n },\n \"code\": 1.1,\n \"deployment\": \"deployment\",\n \"callback\": \"callback\"\n }\n}", + "body": "{\n \"request\": {\n \"request_id\": \"a3f1c9d2-4b7e-4f9a-8c3d-2e5f7b9a1c0d\",\n \"project_uuid\": \"12345678-90ab-cdef-1234-567890abcdef\",\n \"created\": \"2024-01-15T09:48:20.000Z\",\n \"path\": \"/v1/listen?\",\n \"api_key_id\": \"b1e2c3d4-5678-90ab-cdef-1234567890ab\",\n \"response\": {\n \"details\": {\n \"usd\": 0.0075,\n \"duration\": 30,\n \"total_audio\": 30,\n \"channels\": 1,\n \"streams\": 1,\n \"tier\": \"base\",\n \"metadata\": {},\n \"models\": [\n \"1a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d\"\n ],\n \"method\": \"sync\",\n \"tags\": [],\n \"features\": [],\n \"config\": {}\n },\n \"token_details\": [],\n \"code\": 200,\n \"completed\": \"2024-01-15T09:48:21.000Z\",\n \"deployment\": \"hosted:us\"\n },\n \"callback\": null\n }\n}", "headers": { "Content-Type": "application/json" } }, - "uuid": "733e39aa-d3ef-4ea7-8062-af080c6288c4", + "uuid": "0c5ca54d-c4a7-42fd-b595-b0c6d34b82f9", "persistent": true, "priority": 3, "metadata": {