From e0932f75557c87fc1914f6c54c5917b59231d3a1 Mon Sep 17 00:00:00 2001
From: fern-api <115122769+fern-api[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 15:04:35 +0000
Subject: [PATCH 1/6] [fern-generated] Update SDK
Generated by Fern
CLI Version: unknown
Generators:
- fernapi/fern-python-sdk: 5.14.13
---
.fern/metadata.json | 6 +-
poetry.lock | 6 +-
pyproject.toml | 5 +-
reference.md | 79 +++++
src/phenoml/__init__.py | 3 +
src/phenoml/client.py | 19 ++
src/phenoml/core/client_wrapper.py | 4 +-
src/phenoml/voice/__init__.py | 64 ++++
src/phenoml/voice/client.py | 64 ++++
src/phenoml/voice/errors/__init__.py | 56 ++++
src/phenoml/voice/errors/bad_gateway_error.py | 10 +
src/phenoml/voice/errors/bad_request_error.py | 10 +
.../voice/errors/content_too_large_error.py | 10 +
.../voice/errors/gateway_timeout_error.py | 10 +
.../voice/errors/payment_required_error.py | 10 +
.../voice/errors/service_unavailable_error.py | 10 +
.../voice/errors/unauthorized_error.py | 10 +
src/phenoml/voice/raw_client.py | 13 +
src/phenoml/voice/types/__init__.py | 34 ++
.../voice/types/transcribe_response.py | 22 ++
src/phenoml/voice/voice/__init__.py | 4 +
src/phenoml/voice/voice/client.py | 117 +++++++
src/phenoml/voice/voice/raw_client.py | 317 ++++++++++++++++++
23 files changed, 871 insertions(+), 12 deletions(-)
create mode 100644 src/phenoml/voice/__init__.py
create mode 100644 src/phenoml/voice/client.py
create mode 100644 src/phenoml/voice/errors/__init__.py
create mode 100644 src/phenoml/voice/errors/bad_gateway_error.py
create mode 100644 src/phenoml/voice/errors/bad_request_error.py
create mode 100644 src/phenoml/voice/errors/content_too_large_error.py
create mode 100644 src/phenoml/voice/errors/gateway_timeout_error.py
create mode 100644 src/phenoml/voice/errors/payment_required_error.py
create mode 100644 src/phenoml/voice/errors/service_unavailable_error.py
create mode 100644 src/phenoml/voice/errors/unauthorized_error.py
create mode 100644 src/phenoml/voice/raw_client.py
create mode 100644 src/phenoml/voice/types/__init__.py
create mode 100644 src/phenoml/voice/types/transcribe_response.py
create mode 100644 src/phenoml/voice/voice/__init__.py
create mode 100644 src/phenoml/voice/voice/client.py
create mode 100644 src/phenoml/voice/voice/raw_client.py
diff --git a/.fern/metadata.json b/.fern/metadata.json
index 585ba06..1512f51 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": "0.0.0.dev0"
}
\ No newline at end of file
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..cc86d42 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ dynamic = ["version"]
[tool.poetry]
name = "phenoml"
-version = "16.3.0"
+version = "0.0.0.dev0"
description = ""
readme = "README.md"
authors = []
@@ -31,9 +31,6 @@ classifiers = [
packages = [
{ include = "phenoml", from = "src"}
]
-include = [
- { path = "src/phenoml/openapi/openapi.json", format = ["sdist", "wheel"] }
-]
[tool.poetry.urls]
Repository = 'https://github.com/phenoml/phenoml-python-sdk'
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..3421d69 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/0.0.0.dev0",
"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": "0.0.0.dev0",
**(self.get_custom_headers() or {}),
}
token = self._get_token()
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)
From 9428e5fcbd866cdaee094254dae307feace797ca Mon Sep 17 00:00:00 2001
From: fern-api <115122769+fern-api[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 15:05:08 +0000
Subject: [PATCH 2/6] [fern-autoversion] feat: add voice transcription service
and update fhir2omop/agent docs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduce a new `voice` namespace with a `transcribe` method that accepts
raw audio bytes and returns a `TranscribeResponse` containing the full
transcript. The endpoint auto-detects audio format (WAV, FLAC, MP3,
OGG/WebM Opus) and supports up to ~5 minutes of audio per request.
Also updates OpenAPI descriptions for `fhir2omop` (explicit supported
resource-type list and clarified `dropped` semantics) and `agent.chat`
session parameters (409 Conflict documented for overlapping turns).
Key changes:
- Add `client.voice.voice.transcribe(...)` and `client.voice.voice.transcribe(...)` (async) for audio-to-text transcription
- Add `phenoml.voice.TranscribeResponse` response type with `transcript` field
- Add `phenoml.voice.errors` module with `BadGatewayError`, `BadRequestError`, `ContentTooLargeError`, `GatewayTimeoutError`, `PaymentRequiredError`, `ServiceUnavailableError`, and `UnauthorizedError`
- Update `fhir2omop` OpenAPI description to enumerate supported FHIR resource types and clarify `dropped` field semantics
- Update `agent.chat` `session_id` description to document 409 Conflict for concurrent turns
🌿 Generated with Fern
---
.fern/metadata.json | 2 +-
.fern/replay.lock | 2 +-
changelog.md | 11 +++++++++++
pyproject.toml | 2 +-
src/phenoml/core/client_wrapper.py | 4 ++--
5 files changed, 16 insertions(+), 5 deletions(-)
diff --git a/.fern/metadata.json b/.fern/metadata.json
index 1512f51..69b204f 100644
--- a/.fern/metadata.json
+++ b/.fern/metadata.json
@@ -13,5 +13,5 @@
"invokedBy": "ci",
"requestedVersion": "AUTO",
"ciProvider": "unknown",
- "sdkVersion": "0.0.0.dev0"
+ "sdkVersion": "16.4.0"
}
\ No newline at end of file
diff --git a/.fern/replay.lock b/.fern/replay.lock
index 56c4194..ae0bf12 100644
--- a/.fern/replay.lock
+++ b/.fern/replay.lock
@@ -73,7 +73,7 @@ patches:
[tool.poetry]
name = "phenoml"
- -version = "0.0.0.dev0"
+ -version = "16.4.0"
+version = "16.3.0"
description = ""
readme = "README.md"
diff --git a/changelog.md b/changelog.md
index fb5e474..af146ff 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,14 @@
+## [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.
+
+### Changed
+- **`client.fhir2omop.create(...)` docstring** — now enumerates all supported FHIR resource types and clarifies that unsupported types are silently ignored rather than listed under `dropped`.
+- **`client.agent.chat.send(...)` and `client.agent.chat.stream(...)` `session_id` parameter** — description now explicitly states that only one request may be active per session at a time and that overlapping turns return 409 Conflict.
+
## [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/pyproject.toml b/pyproject.toml
index cc86d42..ff52e00 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ dynamic = ["version"]
[tool.poetry]
name = "phenoml"
-version = "0.0.0.dev0"
+version = "16.4.0"
description = ""
readme = "README.md"
authors = []
diff --git a/src/phenoml/core/client_wrapper.py b/src/phenoml/core/client_wrapper.py
index 3421d69..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/0.0.0.dev0",
+ "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": "0.0.0.dev0",
+ "X-Fern-SDK-Version": "16.4.0",
**(self.get_custom_headers() or {}),
}
token = self._get_token()
From 96cea1ff97e48b7f086bc8da260627448271276c Mon Sep 17 00:00:00 2001
From: fern-api <115122769+fern-api[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 15:05:11 +0000
Subject: [PATCH 3/6] [fern-replay] Applied customizations
Patches with unresolved conflicts (1):
- patch-6516695e: Release 15.0.2: restore bundled openapi.json packaging (#169)
Run `fern-replay resolve` to apply these customizations.
---
.fern/replay.lock | 11 +++++++++--
pyproject.toml | 3 +++
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/.fern/replay.lock b/.fern/replay.lock
index ae0bf12..d7fb638 100644
--- a/.fern/replay.lock
+++ b/.fern/replay.lock
@@ -54,7 +54,13 @@ 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
@@ -73,7 +79,7 @@ patches:
[tool.poetry]
name = "phenoml"
- -version = "16.4.0"
+ -version = "0.0.0.dev0"
+version = "16.3.0"
description = ""
readme = "README.md"
@@ -191,3 +197,4 @@ patches:
[tool.poetry.extras]
aiohttp=["aiohttp", "httpx-aiohttp"]
+ status: unresolved
diff --git a/pyproject.toml b/pyproject.toml
index ff52e00..6c649b6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,9 @@ classifiers = [
packages = [
{ include = "phenoml", from = "src"}
]
+include = [
+ { path = "src/phenoml/openapi/openapi.json", format = ["sdist", "wheel"] }
+]
[tool.poetry.urls]
Repository = 'https://github.com/phenoml/phenoml-python-sdk'
From bd511bb49045ade0c679fafb20595dded09ff1d1 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 15:05:32 +0000
Subject: [PATCH 4/6] chore: sync OpenAPI spec + code-examples for
e2c477c4583f8f9e7d0d1a5b5591c1aa8cabef10 [skip ci]
---
code-examples.json | 35 ++++++++++-
src/phenoml/openapi/openapi.json | 99 +++++++++++++++++++++++++++++++-
2 files changed, 131 insertions(+), 3 deletions(-)
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/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."
}
]
}
From e5d72c0b1632fc6b36862cd97dac95bf7df686f5 Mon Sep 17 00:00:00 2001
From: Kerry Weinberg <10016680+kerbearasaurus@users.noreply.github.com>
Date: Tue, 23 Jun 2026 11:35:04 -0400
Subject: [PATCH 5/6] [fern-replay] Resolved conflicts
Patches replayed:
- patch-6516695e: Release 15.0.2: restore bundled openapi.json packaging (#169)
---
.fern/replay.lock | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/.fern/replay.lock b/.fern/replay.lock
index d7fb638..ae7ed39 100644
--- a/.fern/replay.lock
+++ b/.fern/replay.lock
@@ -63,16 +63,16 @@ generations:
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"]
@@ -80,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 = []
@@ -102,7 +102,7 @@ patches:
[tool.poetry]
name = "phenoml"
- version = "16.3.0"
+ version = "16.4.0"
description = ""
readme = "README.md"
authors = []
@@ -197,4 +197,3 @@ patches:
[tool.poetry.extras]
aiohttp=["aiohttp", "httpx-aiohttp"]
- status: unresolved
From b861c0a1727559fed677f6078897230cb5893471 Mon Sep 17 00:00:00 2001
From: Kerry Weinberg <10016680+kerbearasaurus@users.noreply.github.com>
Date: Tue, 23 Jun 2026 11:37:40 -0400
Subject: [PATCH 6/6] release: 16.4.0 - add voice transcription
---
changelog.md | 4 ----
1 file changed, 4 deletions(-)
diff --git a/changelog.md b/changelog.md
index af146ff..dfb6bc0 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,10 +5,6 @@
- **`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.
-### Changed
-- **`client.fhir2omop.create(...)` docstring** — now enumerates all supported FHIR resource types and clarifies that unsupported types are silently ignored rather than listed under `dropped`.
-- **`client.agent.chat.send(...)` and `client.agent.chat.stream(...)` `session_id` parameter** — description now explicitly states that only one request may be active per session at a time and that overlapping turns return 409 Conflict.
-
## [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.