diff --git a/.fern/metadata.json b/.fern/metadata.json index 585ba06..69b204f 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,5 +1,5 @@ { - "cliVersion": "5.50.1", + "cliVersion": "5.50.4", "generatorName": "fernapi/fern-python-sdk", "generatorVersion": "5.14.13", "generatorConfig": { @@ -8,10 +8,10 @@ "enabled": true } }, - "originGitCommit": "fbaef4c66e97232edfaacb20d23d476aa286ecdd", + "originGitCommit": "e2c477c4583f8f9e7d0d1a5b5591c1aa8cabef10", "originGitCommitIsDirty": true, "invokedBy": "ci", "requestedVersion": "AUTO", "ciProvider": "unknown", - "sdkVersion": "16.3.0" + "sdkVersion": "16.4.0" } \ No newline at end of file diff --git a/.fern/replay.lock b/.fern/replay.lock index 56c4194..ae7ed39 100644 --- a/.fern/replay.lock +++ b/.fern/replay.lock @@ -54,19 +54,25 @@ generations: cli_version: unknown generator_versions: fernapi/fern-python-sdk: 5.14.13 -current_generation: ac1f834fe3b978f13cb7dba910fc81dcd35abe2c + - commit_sha: e0932f75557c87fc1914f6c54c5917b59231d3a1 + tree_hash: ff00b350d3683c15ac372d560f5d168778226aa9 + timestamp: 2026-06-23T15:04:35.943Z + cli_version: unknown + generator_versions: + fernapi/fern-python-sdk: 5.14.13 +current_generation: e0932f75557c87fc1914f6c54c5917b59231d3a1 patches: - id: patch-6516695e - content_hash: sha256:07505f8175d78f0dc3cab8f1bc99e9588c1c24f0c43427439a4c685d56635932 + content_hash: sha256:d2b3264c983a6bb7ce6db1b48d80d28aa93b1f5c838f654cd489a6e8569bee20 original_commit: 6516695ecaba47ae4bcc8119acca86a1113adeeb original_message: "Release 15.0.2: restore bundled openapi.json packaging (#169)" original_author: Gavin Sharp - base_generation: ac1f834fe3b978f13cb7dba910fc81dcd35abe2c + base_generation: e0932f75557c87fc1914f6c54c5917b59231d3a1 files: - pyproject.toml patch_content: | diff --git a/pyproject.toml b/pyproject.toml - index cc86d42..678bcb9 100644 + index cc86d42..6c649b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] @@ -74,7 +80,7 @@ patches: [tool.poetry] name = "phenoml" -version = "0.0.0.dev0" - +version = "16.3.0" + +version = "16.4.0" description = "" readme = "README.md" authors = [] @@ -96,7 +102,7 @@ patches: [tool.poetry] name = "phenoml" - version = "16.3.0" + version = "16.4.0" description = "" readme = "README.md" authors = [] diff --git a/changelog.md b/changelog.md index fb5e474..dfb6bc0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +## [16.4.0] - 2026-06-23 +### Added +- **`client.voice.voice.transcribe(...)`** — new sync and async method that accepts raw audio bytes (WAV, FLAC, MP3, OGG/WebM Opus) and returns a `TranscribeResponse` with the full transcript, supporting up to ~5 minutes of audio per request. +- **`RawVoiceClient`** and **`AsyncRawVoiceClient`** — new clients under `phenoml.voice.voice` exposing `transcribe(...)` with support for `bytes`, `Iterator[bytes]`, or `AsyncIterator[bytes]` input and an optional BCP-47 `language` hint. +- **`phenoml.voice.TranscribeResponse`** — new response type with a `transcript` field returned by the transcription endpoint. +- **`phenoml.voice.errors`** — new typed error classes (`BadRequestError`, `UnauthorizedError`, `PaymentRequiredError`, `ContentTooLargeError`, `BadGatewayError`, `ServiceUnavailableError`, `GatewayTimeoutError`) raised by the voice service. + ## [16.3.0] - 2026-06-18 ### Added - **`phenoml.agent.errors.ConflictError`** — new `ApiError` subclass raised by `client.agent.chat.send(...)` and `client.agent.chat.stream(...)` for HTTP 409 responses when a session already has an active turn. diff --git a/code-examples.json b/code-examples.json index 29025a1..a84c5de 100644 --- a/code-examples.json +++ b/code-examples.json @@ -2,8 +2,8 @@ "metadata": { "language": "python", "packageName": "phenoml", - "sdkVersion": "16.3.0", - "specCommit": "fbaef4c66e97232edfaacb20d23d476aa286ecdd", + "sdkVersion": "16.4.0", + "specCommit": "e2c477c4583f8f9e7d0d1a5b5591c1aa8cabef10", "generatorName": "fernapi/fern-python-sdk" }, "renderRules": { @@ -3986,6 +3986,37 @@ ] } }, + "POST /transcribe": { + "httpMethod": "POST", + "httpPath": "/transcribe", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.voice.voice.transcribe({{__body__}})", + "params": [], + "body": { + "fieldSeparator": ", ", + "fields": [ + { + "jsonKey": "language", + "fieldTemplate": "language={{value}}", + "kind": "list", + "required": false, + "items": { + "jsonKey": "", + "fieldTemplate": "{{value}}", + "kind": "string", + "required": true + } + } + ] + } + } + }, "GET /workflows": { "httpMethod": "GET", "httpPath": "/workflows", diff --git a/poetry.lock b/poetry.lock index 2811db4..2250508 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1245,14 +1245,14 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "9.1.0" +version = "9.1.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32"}, - {file = "pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c"}, + {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, + {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index 678bcb9..6c649b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "phenoml" -version = "16.3.0" +version = "16.4.0" description = "" readme = "README.md" authors = [] diff --git a/reference.md b/reference.md index 81797cd..22ffc52 100644 --- a/reference.md +++ b/reference.md @@ -6448,6 +6448,85 @@ client.tools.mcp_tools.delete( + + + + +## Voice +
client.voice.voice.transcribe(...) -> TranscribeResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Transcribes an uploaded audio recording and returns the transcript. +Send the raw audio bytes as the request body; the audio format is +detected automatically (WAV, FLAC, MP3, OGG/WebM Opus). + +Supports up to ~5 minutes of audio per request. This limit is on audio +duration regardless of file size or format, so a compressed recording +within the size limit can still be rejected for being too long. Pair the +transcript with a downstream text step (e.g. `POST /lang2fhir/create`) +to turn it into a FHIR resource. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +client.voice.voice.transcribe(...) +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]` — Raw audio bytes (WAV, FLAC, MP3, or OGG/WebM Opus). + +
+
+ +
+
+ +**language:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — BCP-47 language tag, repeatable for up to 4 candidate languages. Defaults to `en-US`. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/src/phenoml/__init__.py b/src/phenoml/__init__.py index 423b783..f998705 100644 --- a/src/phenoml/__init__.py +++ b/src/phenoml/__init__.py @@ -17,6 +17,7 @@ lang2fhir, summary, tools, + voice, workflows, ) from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient @@ -40,6 +41,7 @@ "lang2fhir": ".lang2fhir", "summary": ".summary", "tools": ".tools", + "voice": ".voice", "workflows": ".workflows", } @@ -82,5 +84,6 @@ def __dir__(): "lang2fhir", "summary", "tools", + "voice", "workflows", ] diff --git a/src/phenoml/client.py b/src/phenoml/client.py index 2988357..126a68e 100644 --- a/src/phenoml/client.py +++ b/src/phenoml/client.py @@ -23,6 +23,7 @@ from .lang2fhir.client import AsyncLang2FhirClient, Lang2FhirClient from .summary.client import AsyncSummaryClient, SummaryClient from .tools.client import AsyncToolsClient, ToolsClient + from .voice.client import AsyncVoiceClient, VoiceClient from .workflows.client import AsyncWorkflowsClient, WorkflowsClient @@ -206,6 +207,7 @@ def __init__( self._lang2fhir: typing.Optional[Lang2FhirClient] = None self._summary: typing.Optional[SummaryClient] = None self._tools: typing.Optional[ToolsClient] = None + self._voice: typing.Optional[VoiceClient] = None self._workflows: typing.Optional[WorkflowsClient] = None @property @@ -288,6 +290,14 @@ def tools(self): self._tools = ToolsClient(client_wrapper=self._client_wrapper) return self._tools + @property + def voice(self): + if self._voice is None: + from .voice.client import VoiceClient # noqa: E402 + + self._voice = VoiceClient(client_wrapper=self._client_wrapper) + return self._voice + @property def workflows(self): if self._workflows is None: @@ -492,6 +502,7 @@ def __init__( self._lang2fhir: typing.Optional[AsyncLang2FhirClient] = None self._summary: typing.Optional[AsyncSummaryClient] = None self._tools: typing.Optional[AsyncToolsClient] = None + self._voice: typing.Optional[AsyncVoiceClient] = None self._workflows: typing.Optional[AsyncWorkflowsClient] = None @property @@ -574,6 +585,14 @@ def tools(self): self._tools = AsyncToolsClient(client_wrapper=self._client_wrapper) return self._tools + @property + def voice(self): + if self._voice is None: + from .voice.client import AsyncVoiceClient # noqa: E402 + + self._voice = AsyncVoiceClient(client_wrapper=self._client_wrapper) + return self._voice + @property def workflows(self): if self._workflows is None: diff --git a/src/phenoml/core/client_wrapper.py b/src/phenoml/core/client_wrapper.py index be7351c..536cb52 100644 --- a/src/phenoml/core/client_wrapper.py +++ b/src/phenoml/core/client_wrapper.py @@ -29,12 +29,12 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "phenoml/16.3.0", + "User-Agent": "phenoml/16.4.0", "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": "phenoml", - "X-Fern-SDK-Version": "16.3.0", + "X-Fern-SDK-Version": "16.4.0", **(self.get_custom_headers() or {}), } token = self._get_token() diff --git a/src/phenoml/openapi/openapi.json b/src/phenoml/openapi/openapi.json index 20e6e8a..979abe8 100644 --- a/src/phenoml/openapi/openapi.json +++ b/src/phenoml/openapi/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Phenoml API", - "version": "fbaef4c66e97232edfaacb20d23d476aa286ecdd" + "version": "e2c477c4583f8f9e7d0d1a5b5591c1aa8cabef10" }, "x-services": [ { @@ -67,6 +67,13 @@ "description": "FHIR server operations including resource CRUD, search, and batch operations.", "iconHint": "proxy", "status": "ga" + }, + { + "id": "voice", + "name": "Voice", + "description": "Transcribe audio recordings into text with speech-to-text. Pair the transcript with Lang2FHIR to turn spoken clinical notes into structured FHIR resources.", + "iconHint": "voice", + "status": "alpha" } ], "paths": { @@ -6409,6 +6416,80 @@ "x-service": "tools" } }, + "/transcribe": { + "post": { + "tags": [ + "Voice / Voice" + ], + "operationId": "voice_transcribe", + "summary": "Transcribe audio", + "description": "Transcribes an uploaded audio recording and returns the transcript.\nSend the raw audio bytes as the request body; the audio format is\ndetected automatically (WAV, FLAC, MP3, OGG/WebM Opus).\n\nSupports up to ~5 minutes of audio per request. This limit is on audio\nduration regardless of file size or format, so a compressed recording\nwithin the size limit can still be rejected for being too long. Pair the\ntranscript with a downstream text step (e.g. `POST /lang2fhir/create`)\nto turn it into a FHIR resource.\n", + "parameters": [ + { + "name": "language", + "in": "query", + "description": "BCP-47 language tag, repeatable for up to 4 candidate languages. Defaults to `en-US`.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "requestBody": { + "required": true, + "description": "Raw audio bytes (WAV, FLAC, MP3, or OGG/WebM Opus).", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "Transcription succeeded.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/voice_TranscribeResponse" + } + } + } + }, + "400": { + "description": "Invalid request (empty body, too many languages, no transcript produced, or audio that is too long or in an unsupported format)" + }, + "401": { + "description": "Unauthorized" + }, + "402": { + "description": "Payment required (credits exhausted or subscription inactive)" + }, + "413": { + "description": "Audio exceeds the maximum size" + }, + "502": { + "description": "Speech recognition failed" + }, + "503": { + "description": "Transcription temporarily unavailable" + }, + "504": { + "description": "Transcription timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "voice" + } + }, "/workflows": { "get": { "operationId": "workflows_list", @@ -11044,6 +11125,18 @@ } } }, + "voice_TranscribeResponse": { + "type": "object", + "required": [ + "transcript" + ], + "properties": { + "transcript": { + "type": "string", + "description": "The full transcript of the audio." + } + } + }, "workflows_CreateWorkflowRequest": { "type": "object", "required": [ @@ -11832,6 +11925,10 @@ { "name": "Tools / MCP Tools", "description": "List and manage individual tools exposed by an MCP server." + }, + { + "name": "Voice / Voice", + "description": "Speech-to-text transcription of audio recordings." } ] } diff --git a/src/phenoml/voice/__init__.py b/src/phenoml/voice/__init__.py new file mode 100644 index 0000000..281fff2 --- /dev/null +++ b/src/phenoml/voice/__init__.py @@ -0,0 +1,64 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import TranscribeResponse + from .errors import ( + BadGatewayError, + BadRequestError, + ContentTooLargeError, + GatewayTimeoutError, + PaymentRequiredError, + ServiceUnavailableError, + UnauthorizedError, + ) + from . import voice +_dynamic_imports: typing.Dict[str, str] = { + "BadGatewayError": ".errors", + "BadRequestError": ".errors", + "ContentTooLargeError": ".errors", + "GatewayTimeoutError": ".errors", + "PaymentRequiredError": ".errors", + "ServiceUnavailableError": ".errors", + "TranscribeResponse": ".types", + "UnauthorizedError": ".errors", + "voice": ".voice", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadGatewayError", + "BadRequestError", + "ContentTooLargeError", + "GatewayTimeoutError", + "PaymentRequiredError", + "ServiceUnavailableError", + "TranscribeResponse", + "UnauthorizedError", + "voice", +] diff --git a/src/phenoml/voice/client.py b/src/phenoml/voice/client.py new file mode 100644 index 0000000..f2194af --- /dev/null +++ b/src/phenoml/voice/client.py @@ -0,0 +1,64 @@ +# This file was auto-generated by Fern from our API Definition. + +from __future__ import annotations + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .raw_client import AsyncRawVoiceClient, RawVoiceClient + +if typing.TYPE_CHECKING: + from .voice.client import AsyncVoiceClient as voice_voice_client_AsyncVoiceClient + from .voice.client import VoiceClient as voice_voice_client_VoiceClient + + +class VoiceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawVoiceClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._voice: typing.Optional[voice_voice_client_VoiceClient] = None + + @property + def with_raw_response(self) -> RawVoiceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawVoiceClient + """ + return self._raw_client + + @property + def voice(self): + if self._voice is None: + from .voice.client import VoiceClient as voice_voice_client_VoiceClient # noqa: E402 + + self._voice = voice_voice_client_VoiceClient(client_wrapper=self._client_wrapper) + return self._voice + + +class AsyncVoiceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawVoiceClient(client_wrapper=client_wrapper) + self._client_wrapper = client_wrapper + self._voice: typing.Optional[voice_voice_client_AsyncVoiceClient] = None + + @property + def with_raw_response(self) -> AsyncRawVoiceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawVoiceClient + """ + return self._raw_client + + @property + def voice(self): + if self._voice is None: + from .voice.client import AsyncVoiceClient as voice_voice_client_AsyncVoiceClient # noqa: E402 + + self._voice = voice_voice_client_AsyncVoiceClient(client_wrapper=self._client_wrapper) + return self._voice diff --git a/src/phenoml/voice/errors/__init__.py b/src/phenoml/voice/errors/__init__.py new file mode 100644 index 0000000..d163027 --- /dev/null +++ b/src/phenoml/voice/errors/__init__.py @@ -0,0 +1,56 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_gateway_error import BadGatewayError + from .bad_request_error import BadRequestError + from .content_too_large_error import ContentTooLargeError + from .gateway_timeout_error import GatewayTimeoutError + from .payment_required_error import PaymentRequiredError + from .service_unavailable_error import ServiceUnavailableError + from .unauthorized_error import UnauthorizedError +_dynamic_imports: typing.Dict[str, str] = { + "BadGatewayError": ".bad_gateway_error", + "BadRequestError": ".bad_request_error", + "ContentTooLargeError": ".content_too_large_error", + "GatewayTimeoutError": ".gateway_timeout_error", + "PaymentRequiredError": ".payment_required_error", + "ServiceUnavailableError": ".service_unavailable_error", + "UnauthorizedError": ".unauthorized_error", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadGatewayError", + "BadRequestError", + "ContentTooLargeError", + "GatewayTimeoutError", + "PaymentRequiredError", + "ServiceUnavailableError", + "UnauthorizedError", +] diff --git a/src/phenoml/voice/errors/bad_gateway_error.py b/src/phenoml/voice/errors/bad_gateway_error.py new file mode 100644 index 0000000..dc20174 --- /dev/null +++ b/src/phenoml/voice/errors/bad_gateway_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class BadGatewayError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=502, headers=headers, body=body) diff --git a/src/phenoml/voice/errors/bad_request_error.py b/src/phenoml/voice/errors/bad_request_error.py new file mode 100644 index 0000000..412bae7 --- /dev/null +++ b/src/phenoml/voice/errors/bad_request_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class BadRequestError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/src/phenoml/voice/errors/content_too_large_error.py b/src/phenoml/voice/errors/content_too_large_error.py new file mode 100644 index 0000000..5aa39e3 --- /dev/null +++ b/src/phenoml/voice/errors/content_too_large_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class ContentTooLargeError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=413, headers=headers, body=body) diff --git a/src/phenoml/voice/errors/gateway_timeout_error.py b/src/phenoml/voice/errors/gateway_timeout_error.py new file mode 100644 index 0000000..19acf1d --- /dev/null +++ b/src/phenoml/voice/errors/gateway_timeout_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class GatewayTimeoutError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=504, headers=headers, body=body) diff --git a/src/phenoml/voice/errors/payment_required_error.py b/src/phenoml/voice/errors/payment_required_error.py new file mode 100644 index 0000000..d971b1b --- /dev/null +++ b/src/phenoml/voice/errors/payment_required_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class PaymentRequiredError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=402, headers=headers, body=body) diff --git a/src/phenoml/voice/errors/service_unavailable_error.py b/src/phenoml/voice/errors/service_unavailable_error.py new file mode 100644 index 0000000..1e7c99e --- /dev/null +++ b/src/phenoml/voice/errors/service_unavailable_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class ServiceUnavailableError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=503, headers=headers, body=body) diff --git a/src/phenoml/voice/errors/unauthorized_error.py b/src/phenoml/voice/errors/unauthorized_error.py new file mode 100644 index 0000000..c990122 --- /dev/null +++ b/src/phenoml/voice/errors/unauthorized_error.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.api_error import ApiError + + +class UnauthorizedError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/src/phenoml/voice/raw_client.py b/src/phenoml/voice/raw_client.py new file mode 100644 index 0000000..1be3d83 --- /dev/null +++ b/src/phenoml/voice/raw_client.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper + + +class RawVoiceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + +class AsyncRawVoiceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper diff --git a/src/phenoml/voice/types/__init__.py b/src/phenoml/voice/types/__init__.py new file mode 100644 index 0000000..526b95b --- /dev/null +++ b/src/phenoml/voice/types/__init__.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .transcribe_response import TranscribeResponse +_dynamic_imports: typing.Dict[str, str] = {"TranscribeResponse": ".transcribe_response"} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = ["TranscribeResponse"] diff --git a/src/phenoml/voice/types/transcribe_response.py b/src/phenoml/voice/types/transcribe_response.py new file mode 100644 index 0000000..df00073 --- /dev/null +++ b/src/phenoml/voice/types/transcribe_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class TranscribeResponse(UniversalBaseModel): + transcript: str = pydantic.Field() + """ + The full transcript of the audio. + """ + + 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/phenoml/voice/voice/__init__.py b/src/phenoml/voice/voice/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/phenoml/voice/voice/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/phenoml/voice/voice/client.py b/src/phenoml/voice/voice/client.py new file mode 100644 index 0000000..e4522c0 --- /dev/null +++ b/src/phenoml/voice/voice/client.py @@ -0,0 +1,117 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ..types.transcribe_response import TranscribeResponse +from .raw_client import AsyncRawVoiceClient, RawVoiceClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class VoiceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawVoiceClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawVoiceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawVoiceClient + """ + return self._raw_client + + def transcribe( + self, + *, + request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]], + language: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TranscribeResponse: + """ + Transcribes an uploaded audio recording and returns the transcript. + Send the raw audio bytes as the request body; the audio format is + detected automatically (WAV, FLAC, MP3, OGG/WebM Opus). + + Supports up to ~5 minutes of audio per request. This limit is on audio + duration regardless of file size or format, so a compressed recording + within the size limit can still be rejected for being too long. Pair the + transcript with a downstream text step (e.g. `POST /lang2fhir/create`) + to turn it into a FHIR resource. + + Parameters + ---------- + request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] + + language : typing.Optional[typing.Union[str, typing.Sequence[str]]] + BCP-47 language tag, repeatable for up to 4 candidate languages. Defaults to `en-US`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TranscribeResponse + Transcription succeeded. + """ + _response = self._raw_client.transcribe(request=request, language=language, request_options=request_options) + return _response.data + + +class AsyncVoiceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawVoiceClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawVoiceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawVoiceClient + """ + return self._raw_client + + async def transcribe( + self, + *, + request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]], + language: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> TranscribeResponse: + """ + Transcribes an uploaded audio recording and returns the transcript. + Send the raw audio bytes as the request body; the audio format is + detected automatically (WAV, FLAC, MP3, OGG/WebM Opus). + + Supports up to ~5 minutes of audio per request. This limit is on audio + duration regardless of file size or format, so a compressed recording + within the size limit can still be rejected for being too long. Pair the + transcript with a downstream text step (e.g. `POST /lang2fhir/create`) + to turn it into a FHIR resource. + + Parameters + ---------- + request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] + + language : typing.Optional[typing.Union[str, typing.Sequence[str]]] + BCP-47 language tag, repeatable for up to 4 candidate languages. Defaults to `en-US`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + TranscribeResponse + Transcription succeeded. + """ + _response = await self._raw_client.transcribe( + request=request, language=language, request_options=request_options + ) + return _response.data diff --git a/src/phenoml/voice/voice/raw_client.py b/src/phenoml/voice/voice/raw_client.py new file mode 100644 index 0000000..a7a90f7 --- /dev/null +++ b/src/phenoml/voice/voice/raw_client.py @@ -0,0 +1,317 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.parse_error import ParsingError +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +from ..errors.bad_gateway_error import BadGatewayError +from ..errors.bad_request_error import BadRequestError +from ..errors.content_too_large_error import ContentTooLargeError +from ..errors.gateway_timeout_error import GatewayTimeoutError +from ..errors.payment_required_error import PaymentRequiredError +from ..errors.service_unavailable_error import ServiceUnavailableError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.transcribe_response import TranscribeResponse +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawVoiceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def transcribe( + self, + *, + request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]], + language: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[TranscribeResponse]: + """ + Transcribes an uploaded audio recording and returns the transcript. + Send the raw audio bytes as the request body; the audio format is + detected automatically (WAV, FLAC, MP3, OGG/WebM Opus). + + Supports up to ~5 minutes of audio per request. This limit is on audio + duration regardless of file size or format, so a compressed recording + within the size limit can still be rejected for being too long. Pair the + transcript with a downstream text step (e.g. `POST /lang2fhir/create`) + to turn it into a FHIR resource. + + Parameters + ---------- + request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] + + language : typing.Optional[typing.Union[str, typing.Sequence[str]]] + BCP-47 language tag, repeatable for up to 4 candidate languages. Defaults to `en-US`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[TranscribeResponse] + Transcription succeeded. + """ + _response = self._client_wrapper.httpx_client.request( + "transcribe", + method="POST", + params={ + "language": language, + }, + content=request, + headers={ + "content-type": "application/octet-stream", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TranscribeResponse, + parse_obj_as( + type_=TranscribeResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawVoiceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def transcribe( + self, + *, + request: typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]], + language: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[TranscribeResponse]: + """ + Transcribes an uploaded audio recording and returns the transcript. + Send the raw audio bytes as the request body; the audio format is + detected automatically (WAV, FLAC, MP3, OGG/WebM Opus). + + Supports up to ~5 minutes of audio per request. This limit is on audio + duration regardless of file size or format, so a compressed recording + within the size limit can still be rejected for being too long. Pair the + transcript with a downstream text step (e.g. `POST /lang2fhir/create`) + to turn it into a FHIR resource. + + Parameters + ---------- + request : typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]] + + language : typing.Optional[typing.Union[str, typing.Sequence[str]]] + BCP-47 language tag, repeatable for up to 4 candidate languages. Defaults to `en-US`. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[TranscribeResponse] + Transcription succeeded. + """ + _response = await self._client_wrapper.httpx_client.request( + "transcribe", + method="POST", + params={ + "language": language, + }, + content=request, + headers={ + "content-type": "application/octet-stream", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + TranscribeResponse, + parse_obj_as( + type_=TranscribeResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 402: + raise PaymentRequiredError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 502: + raise BadGatewayError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 503: + raise ServiceUnavailableError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)