Repository navigation
feat: add native audio input encoding to provider formats #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,7 +9,7 @@ | |
| from collections.abc import Iterable, Mapping | ||
| from typing import Any | ||
|
|
||
| from republic._content import Image, Message, ProviderData, Text, Tool, ToolResult, Video, _Media | ||
| from republic._content import Audio, Image, Message, ProviderData, Text, Tool, ToolResult, Video, _Media | ||
| from republic._options import ReasoningEffort, ToolChoice | ||
| from republic._response import BuiltinToolCall, Citation, FinishReason | ||
| from republic.errors import APIResponseError, UnsupportedFeatureError | ||
|
|
@@ -31,6 +31,10 @@ | |
| unsupported_tool, | ||
| ) | ||
|
|
||
| # The standard library reports the legacy aliases for `.wav` and `.aiff` files, and Google | ||
| # accepts only the canonical names they stand for. | ||
| _CANONICAL_MIME_TYPES = {"audio/x-wav": "audio/wav", "audio/x-aiff": "audio/aiff"} | ||
|
|
||
| # Gemini only sends call ids for some models. Keep the original so a generated | ||
| # placeholder id is never sent back. | ||
| _CALL_ID = "gemini_call_id" | ||
|
|
@@ -254,7 +258,7 @@ def _content(message: Message) -> dict[str, Any]: | |
| if message.role == "tool": | ||
| return {"role": "user", "parts": [_function_response(result) for result in message.tool_results]} | ||
| parts: list[Mapping[str, Any]] = provider_payloads(message, GeminiFormat.name) | ||
| parts.extend(_part(part) for part in message.parts if isinstance(part, Text | Image | Video)) | ||
| parts.extend(_part(part) for part in message.parts if isinstance(part, Text | Image | Audio | Video)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Audio is admitted for Gemini here, but
Reproduction (candidate 449debd, repository test double)import republic
from tests.conftest import FakeService
service = FakeService()
service.reply_json({"candidates": [{"content": {"parts": [{"text": "heard"}]}, "finishReason": "STOP"}]})
model = republic.get_model("google:test", http_client=service.client())
await model.chat(republic.user("Listen", republic.audio("voice.wav"))) # real .wav file on disk
service.body()["contents"][0]["parts"]
# [{'text': 'Listen'}, {'inlineData': {'mimeType': 'audio/x-wav', 'data': 'UklGRi4uLi5XQVZFZm10IA=='}}]
# the same file through the chat encoder
# [{'type': 'text', 'text': 'Listen'}, {'type': 'input_audio', 'input_audio': {'data': '...', 'format': 'wav'}}]No live model call was made (the review environment has no credentials); what is verified is the encoded request body, the stdlib inference, and the provider's documented MIME set. Repair direction: canonicalize the legacy aliases before Gemini encoding ( |
||
| for call in message.tool_calls: | ||
| function_call: dict[str, Any] = {"name": call.name, "args": call.args} | ||
| if call_id := call.metadata.get(_CALL_ID): | ||
|
|
@@ -279,6 +283,7 @@ def _function_response(result: ToolResult) -> dict[str, Any]: | |
| def _part(part: Text | _Media) -> dict[str, Any]: | ||
| if isinstance(part, Text): | ||
| return {"text": part.text} | ||
| media_type = _CANONICAL_MIME_TYPES.get(part.media_type, part.media_type) | ||
| if part.url is not None: | ||
| return {"fileData": {"mimeType": part.media_type, "fileUri": part.url}} | ||
| return {"inlineData": {"mimeType": part.media_type, "data": part.base64_data}} | ||
| return {"fileData": {"mimeType": media_type, "fileUri": part.url}} | ||
| return {"inlineData": {"mimeType": media_type, "data": part.base64_data}} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor, non-blocking: this extraction is a no-op on the supported Pythons, and its only observable effect is a small regression.
MimeTypes.guess_typealready parses its argument as a URL and usesp.path(checked in the 3.11–3.14 stdlib sources and on the 3.12 runtime used here), so query strings and fragments were ignored before this change;tests/test_audio.py::test_signed_media_urls_reach_the_providerstill passes with this line reverted.The one input that behaves differently now is a URL whose path carries
;parameters:mimetypes.guess_type("https://cdn.example/a.wav;v=2")returnedaudio/x-wav, whileurlsplit(...).pathisa.wav;v=2and yieldsNone, sorepublic.image/audio/video(...)raisesValueError: Cannot guess the media type ... pass media_type explicitlyfor such URLs.Keeping the extraction for explicitness is fine — just noting it adds no coverage, and dropping it preserves the behaviour documented above the loader.