tau-coding-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from tau.inference.api.audio.base import BaseAudioAPI as BaseAPI
|
|
9
|
+
from tau.inference.model.types import Model
|
|
10
|
+
from tau.inference.types import (
|
|
11
|
+
AudioFormat,
|
|
12
|
+
AudioOptions,
|
|
13
|
+
AudioStopReason,
|
|
14
|
+
STTContext,
|
|
15
|
+
SynthesizedAudio,
|
|
16
|
+
TTSContext,
|
|
17
|
+
TimestampGranularity,
|
|
18
|
+
TranscribedAudio,
|
|
19
|
+
WordTimestamp,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_BASE_URL = "https://api.elevenlabs.io"
|
|
23
|
+
|
|
24
|
+
# Maps our AudioFormat to ElevenLabs output_format query param values
|
|
25
|
+
_FORMAT_MAP: dict[AudioFormat, str] = {
|
|
26
|
+
AudioFormat.MP3: "mp3_44100_128",
|
|
27
|
+
AudioFormat.WAV: "pcm_44100",
|
|
28
|
+
AudioFormat.PCM: "pcm_44100",
|
|
29
|
+
AudioFormat.OPUS: "mp3_44100_128",
|
|
30
|
+
AudioFormat.AAC: "mp3_44100_128",
|
|
31
|
+
AudioFormat.FLAC: "mp3_44100_128",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ElevenLabs PCM output_format values are actually raw PCM, not WAV
|
|
35
|
+
_PCM_FORMATS = {"pcm_16000", "pcm_22050", "pcm_24000", "pcm_44100"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ElevenLabsAudioAPI(BaseAPI):
|
|
39
|
+
"""
|
|
40
|
+
ElevenLabs audio API — proprietary REST endpoints.
|
|
41
|
+
TTS: POST /v1/text-to-speech/{voice_id} — voice_id in URL path,
|
|
42
|
+
output_format as query param, returns raw audio bytes.
|
|
43
|
+
STT: POST /v1/speech-to-text — multipart, returns JSON transcript.
|
|
44
|
+
Auth via xi-api-key header.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, options: AudioOptions) -> None:
|
|
48
|
+
super().__init__(options)
|
|
49
|
+
|
|
50
|
+
def _new_client(self) -> httpx.AsyncClient:
|
|
51
|
+
# Per-call client (used inside `async with`) so its connection pool is
|
|
52
|
+
# always closed — no persistent client left unclosed for the GC.
|
|
53
|
+
return httpx.AsyncClient(
|
|
54
|
+
base_url=self.options.base_url or _BASE_URL,
|
|
55
|
+
timeout=self.options.timeout.total_seconds(),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
59
|
+
headers: dict[str, str] = {}
|
|
60
|
+
if self.options.api_key:
|
|
61
|
+
headers["xi-api-key"] = self.options.api_key
|
|
62
|
+
if self.options.headers:
|
|
63
|
+
headers.update(self.options.headers)
|
|
64
|
+
return headers
|
|
65
|
+
|
|
66
|
+
async def synthesize(self, model: Model, context: TTSContext) -> SynthesizedAudio:
|
|
67
|
+
el_format = _FORMAT_MAP.get(context.response_format, "mp3_44100_128")
|
|
68
|
+
resolved_format = (
|
|
69
|
+
AudioFormat.PCM if el_format in _PCM_FORMATS else context.response_format
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
body: dict[str, Any] = {
|
|
73
|
+
"text": context.input,
|
|
74
|
+
"model_id": model.id,
|
|
75
|
+
"voice_settings": {"speed": context.speed},
|
|
76
|
+
}
|
|
77
|
+
if context.language:
|
|
78
|
+
body["language_code"] = context.language
|
|
79
|
+
|
|
80
|
+
payload = {"voice_id": context.voice, "output_format": el_format, "body": body}
|
|
81
|
+
|
|
82
|
+
if self.options.on_payload:
|
|
83
|
+
modified = self.options.on_payload(payload)
|
|
84
|
+
if modified is not None:
|
|
85
|
+
payload = modified
|
|
86
|
+
|
|
87
|
+
voice_id = payload.pop("voice_id")
|
|
88
|
+
output_format = payload.pop("output_format")
|
|
89
|
+
body = payload.pop("body", body)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
async with self._new_client() as http:
|
|
93
|
+
response = await http.post(
|
|
94
|
+
f"/v1/text-to-speech/{voice_id}",
|
|
95
|
+
params={"output_format": output_format},
|
|
96
|
+
json=body,
|
|
97
|
+
headers=self._auth_headers(),
|
|
98
|
+
)
|
|
99
|
+
response.raise_for_status()
|
|
100
|
+
|
|
101
|
+
if self.options.on_response:
|
|
102
|
+
self.options.on_response(response)
|
|
103
|
+
|
|
104
|
+
return SynthesizedAudio(
|
|
105
|
+
model_id=model.id,
|
|
106
|
+
provider=model.provider,
|
|
107
|
+
audio=response.content,
|
|
108
|
+
format=resolved_format,
|
|
109
|
+
stop_reason=AudioStopReason.Stop,
|
|
110
|
+
timestamp=time.time(),
|
|
111
|
+
)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
return SynthesizedAudio(
|
|
114
|
+
model_id=model.id,
|
|
115
|
+
provider=model.provider,
|
|
116
|
+
audio=b"",
|
|
117
|
+
format=context.response_format,
|
|
118
|
+
stop_reason=AudioStopReason.Error,
|
|
119
|
+
error=str(exc),
|
|
120
|
+
timestamp=time.time(),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
async def transcribe(self, model: Model, context: STTContext) -> TranscribedAudio:
|
|
124
|
+
filename = f"audio.{context.format.value}"
|
|
125
|
+
files = {"file": (filename, context.audio, f"audio/{context.format.value}")}
|
|
126
|
+
|
|
127
|
+
data: dict[str, Any] = {"model_id": model.id}
|
|
128
|
+
if context.language:
|
|
129
|
+
data["language_code"] = context.language
|
|
130
|
+
if context.temperature != 0.0:
|
|
131
|
+
data["temperature"] = str(context.temperature)
|
|
132
|
+
if context.timestamp_granularities:
|
|
133
|
+
# ElevenLabs uses a single value: "word" or "character"
|
|
134
|
+
data["timestamps_granularity"] = "word"
|
|
135
|
+
|
|
136
|
+
payload = {**data, "_files": files}
|
|
137
|
+
|
|
138
|
+
if self.options.on_payload:
|
|
139
|
+
modified = self.options.on_payload(payload)
|
|
140
|
+
if modified is not None:
|
|
141
|
+
files = modified.pop("_files", files)
|
|
142
|
+
data = {k: v for k, v in modified.items() if not k.startswith("_")}
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
async with self._new_client() as http:
|
|
146
|
+
response = await http.post(
|
|
147
|
+
"/v1/speech-to-text",
|
|
148
|
+
data=data,
|
|
149
|
+
files=files,
|
|
150
|
+
headers=self._auth_headers(),
|
|
151
|
+
)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
result = response.json()
|
|
154
|
+
|
|
155
|
+
if self.options.on_response:
|
|
156
|
+
self.options.on_response(result)
|
|
157
|
+
|
|
158
|
+
words: list[WordTimestamp] = [
|
|
159
|
+
WordTimestamp(word=w["text"], start=w["start"], end=w["end"])
|
|
160
|
+
for w in result.get("words", [])
|
|
161
|
+
if w.get("type") == "word"
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
return TranscribedAudio(
|
|
165
|
+
model_id=model.id,
|
|
166
|
+
provider=model.provider,
|
|
167
|
+
text=result.get("text", ""),
|
|
168
|
+
language=result.get("language_code"),
|
|
169
|
+
duration=result.get("audio_duration_secs"),
|
|
170
|
+
words=words,
|
|
171
|
+
stop_reason=AudioStopReason.Stop,
|
|
172
|
+
usage={"language_probability": result.get("language_probability")},
|
|
173
|
+
timestamp=time.time(),
|
|
174
|
+
)
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
return TranscribedAudio(
|
|
177
|
+
model_id=model.id,
|
|
178
|
+
provider=model.provider,
|
|
179
|
+
text="",
|
|
180
|
+
stop_reason=AudioStopReason.Error,
|
|
181
|
+
error=str(exc),
|
|
182
|
+
timestamp=time.time(),
|
|
183
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
from google import genai
|
|
6
|
+
from google.genai import types as genai_types
|
|
7
|
+
|
|
8
|
+
from tau.inference.api.audio.base import BaseAudioAPI as BaseAPI
|
|
9
|
+
from tau.inference.model.types import Model
|
|
10
|
+
from tau.inference.types import (
|
|
11
|
+
AudioFormat,
|
|
12
|
+
AudioOptions,
|
|
13
|
+
AudioStopReason,
|
|
14
|
+
STTContext,
|
|
15
|
+
SynthesizedAudio,
|
|
16
|
+
TTSContext,
|
|
17
|
+
TranscribedAudio,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GeminiAudioAPI(BaseAPI):
|
|
22
|
+
"""
|
|
23
|
+
Gemini TTS via generate_content with response_modalities=["AUDIO"].
|
|
24
|
+
Output is always raw PCM (s16le, 24 kHz, mono) regardless of the
|
|
25
|
+
requested format — Gemini does not support format selection.
|
|
26
|
+
STT is not available through the Gemini generative API.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, options: AudioOptions) -> None:
|
|
30
|
+
super().__init__(options)
|
|
31
|
+
self._client: genai.Client | None = None
|
|
32
|
+
if options.api_key:
|
|
33
|
+
self._client = genai.Client(api_key=options.api_key)
|
|
34
|
+
|
|
35
|
+
def _get_client(self) -> genai.Client:
|
|
36
|
+
if self._client is None or self.options.api_key != getattr(self._client._api_client, 'api_key', None):
|
|
37
|
+
self._client = genai.Client(api_key=self.options.api_key)
|
|
38
|
+
return self._client
|
|
39
|
+
|
|
40
|
+
async def synthesize(self, model: Model, context: TTSContext) -> SynthesizedAudio:
|
|
41
|
+
"""Synthesize audio from text using Gemini API."""
|
|
42
|
+
client = self._get_client()
|
|
43
|
+
|
|
44
|
+
config = genai_types.GenerateContentConfig(
|
|
45
|
+
response_modalities=["AUDIO"],
|
|
46
|
+
speech_config=genai_types.SpeechConfig(
|
|
47
|
+
voice_config=genai_types.VoiceConfig(
|
|
48
|
+
prebuilt_voice_config=genai_types.PrebuiltVoiceConfig(
|
|
49
|
+
voice_name=context.voice,
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
payload = {"model": model.id, "contents": context.input, "config": config}
|
|
56
|
+
|
|
57
|
+
if self.options.on_payload:
|
|
58
|
+
modified = self.options.on_payload(payload)
|
|
59
|
+
if modified is not None:
|
|
60
|
+
payload = modified
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
response = await client.aio.models.generate_content(**payload)
|
|
64
|
+
|
|
65
|
+
if self.options.on_response:
|
|
66
|
+
self.options.on_response(response)
|
|
67
|
+
|
|
68
|
+
part = response.candidates[0].content.parts[0]
|
|
69
|
+
audio_bytes: bytes = part.inline_data.data
|
|
70
|
+
|
|
71
|
+
return SynthesizedAudio(
|
|
72
|
+
model_id=model.id,
|
|
73
|
+
provider=model.provider,
|
|
74
|
+
audio=audio_bytes,
|
|
75
|
+
format=AudioFormat.PCM,
|
|
76
|
+
stop_reason=AudioStopReason.Stop,
|
|
77
|
+
timestamp=time.time(),
|
|
78
|
+
)
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
return SynthesizedAudio(
|
|
81
|
+
model_id=model.id,
|
|
82
|
+
provider=model.provider,
|
|
83
|
+
audio=b"",
|
|
84
|
+
format=AudioFormat.PCM,
|
|
85
|
+
stop_reason=AudioStopReason.Error,
|
|
86
|
+
error=str(exc),
|
|
87
|
+
timestamp=time.time(),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def transcribe(self, model: Model, context: STTContext) -> TranscribedAudio:
|
|
91
|
+
"""Transcribe audio to text (not supported by Gemini API)."""
|
|
92
|
+
raise NotImplementedError(
|
|
93
|
+
"Gemini generative API does not support speech-to-text transcription. "
|
|
94
|
+
"Use a Whisper-based model (openai or groq provider) for STT."
|
|
95
|
+
)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from openai import AsyncOpenAI
|
|
8
|
+
|
|
9
|
+
from tau.inference.api.audio.base import BaseAudioAPI as BaseAPI
|
|
10
|
+
from tau.inference.model.types import Model
|
|
11
|
+
from tau.inference.types import (
|
|
12
|
+
AudioFormat,
|
|
13
|
+
AudioOptions,
|
|
14
|
+
AudioStopReason,
|
|
15
|
+
STTContext,
|
|
16
|
+
SegmentTimestamp,
|
|
17
|
+
SynthesizedAudio,
|
|
18
|
+
TTSContext,
|
|
19
|
+
TranscribedAudio,
|
|
20
|
+
WordTimestamp,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_STOP_REASON: dict[str, AudioStopReason] = {
|
|
24
|
+
"stop": AudioStopReason.Stop,
|
|
25
|
+
"error": AudioStopReason.Error,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OpenAIAudioAPI(BaseAPI):
|
|
30
|
+
"""
|
|
31
|
+
OpenAI-compatible audio API — works with OpenAI, Groq, and any provider
|
|
32
|
+
that implements the /v1/audio/speech and /v1/audio/transcriptions endpoints.
|
|
33
|
+
Point at a different provider by setting options.base_url and options.api_key.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, options: AudioOptions) -> None:
|
|
37
|
+
super().__init__(options)
|
|
38
|
+
self._client = AsyncOpenAI(
|
|
39
|
+
api_key=options.api_key or "placeholder",
|
|
40
|
+
base_url=options.base_url,
|
|
41
|
+
default_headers=options.headers,
|
|
42
|
+
max_retries=options.max_retries,
|
|
43
|
+
timeout=options.timeout.total_seconds(),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
async def synthesize(self, model: Model, context: TTSContext) -> SynthesizedAudio:
|
|
47
|
+
"""Synthesize audio from text using OpenAI API."""
|
|
48
|
+
if self.options.api_key:
|
|
49
|
+
self._client.api_key = self.options.api_key
|
|
50
|
+
|
|
51
|
+
params: dict[str, Any] = {
|
|
52
|
+
"model": model.id,
|
|
53
|
+
"input": context.input,
|
|
54
|
+
"voice": context.voice,
|
|
55
|
+
"response_format": context.response_format.value,
|
|
56
|
+
"speed": context.speed,
|
|
57
|
+
}
|
|
58
|
+
if context.instructions:
|
|
59
|
+
params["instructions"] = context.instructions
|
|
60
|
+
|
|
61
|
+
if self.options.on_payload:
|
|
62
|
+
modified = self.options.on_payload(params)
|
|
63
|
+
if modified is not None:
|
|
64
|
+
params = modified
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
response = await self._client.audio.speech.create(**params)
|
|
68
|
+
audio_bytes = response.content
|
|
69
|
+
|
|
70
|
+
if self.options.on_response:
|
|
71
|
+
self.options.on_response(response)
|
|
72
|
+
|
|
73
|
+
return SynthesizedAudio(
|
|
74
|
+
model_id=model.id,
|
|
75
|
+
provider=model.provider,
|
|
76
|
+
audio=audio_bytes,
|
|
77
|
+
format=context.response_format,
|
|
78
|
+
stop_reason=AudioStopReason.Stop,
|
|
79
|
+
timestamp=time.time(),
|
|
80
|
+
)
|
|
81
|
+
except Exception as exc:
|
|
82
|
+
return SynthesizedAudio(
|
|
83
|
+
model_id=model.id,
|
|
84
|
+
provider=model.provider,
|
|
85
|
+
audio=b"",
|
|
86
|
+
format=context.response_format,
|
|
87
|
+
stop_reason=AudioStopReason.Error,
|
|
88
|
+
error=str(exc),
|
|
89
|
+
timestamp=time.time(),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def transcribe(self, model: Model, context: STTContext) -> TranscribedAudio:
|
|
93
|
+
"""Transcribe audio to text using OpenAI API."""
|
|
94
|
+
if self.options.api_key:
|
|
95
|
+
self._client.api_key = self.options.api_key
|
|
96
|
+
|
|
97
|
+
# OpenAI SDK expects a file-like object with a name; wrap raw bytes
|
|
98
|
+
filename = f"audio.{context.format.value}"
|
|
99
|
+
audio_file = (filename, context.audio, f"audio/{context.format.value}")
|
|
100
|
+
|
|
101
|
+
params: dict[str, Any] = {
|
|
102
|
+
"model": model.id,
|
|
103
|
+
"file": audio_file,
|
|
104
|
+
"response_format": "verbose_json",
|
|
105
|
+
}
|
|
106
|
+
if context.language:
|
|
107
|
+
params["language"] = context.language
|
|
108
|
+
if context.temperature != 0.0:
|
|
109
|
+
params["temperature"] = context.temperature
|
|
110
|
+
if context.timestamp_granularities:
|
|
111
|
+
params["timestamp_granularities"] = [g.value for g in context.timestamp_granularities]
|
|
112
|
+
if context.prompt:
|
|
113
|
+
params["prompt"] = context.prompt
|
|
114
|
+
|
|
115
|
+
if self.options.on_payload:
|
|
116
|
+
modified = self.options.on_payload(params)
|
|
117
|
+
if modified is not None:
|
|
118
|
+
params = modified
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
response = await self._client.audio.transcriptions.create(**params)
|
|
122
|
+
|
|
123
|
+
if self.options.on_response:
|
|
124
|
+
self.options.on_response(response)
|
|
125
|
+
|
|
126
|
+
words: list[WordTimestamp] = []
|
|
127
|
+
segments: list[SegmentTimestamp] = []
|
|
128
|
+
|
|
129
|
+
if hasattr(response, "words") and response.words:
|
|
130
|
+
words = [
|
|
131
|
+
WordTimestamp(word=w.word, start=w.start, end=w.end)
|
|
132
|
+
for w in response.words
|
|
133
|
+
]
|
|
134
|
+
if hasattr(response, "segments") and response.segments:
|
|
135
|
+
segments = [
|
|
136
|
+
SegmentTimestamp(id=s.id, text=s.text, start=s.start, end=s.end)
|
|
137
|
+
for s in response.segments
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
return TranscribedAudio(
|
|
141
|
+
model_id=model.id,
|
|
142
|
+
provider=model.provider,
|
|
143
|
+
text=response.text,
|
|
144
|
+
language=getattr(response, "language", None),
|
|
145
|
+
duration=getattr(response, "duration", None),
|
|
146
|
+
words=words,
|
|
147
|
+
segments=segments,
|
|
148
|
+
stop_reason=AudioStopReason.Stop,
|
|
149
|
+
timestamp=time.time(),
|
|
150
|
+
)
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
return TranscribedAudio(
|
|
153
|
+
model_id=model.id,
|
|
154
|
+
provider=model.provider,
|
|
155
|
+
text="",
|
|
156
|
+
stop_reason=AudioStopReason.Error,
|
|
157
|
+
error=str(exc),
|
|
158
|
+
timestamp=time.time(),
|
|
159
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from tau.inference.api.registry import BaseAPIRegistry
|
|
3
|
+
from tau.inference.api.audio.base import BaseAudioAPI
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AudioAPIRegistry(BaseAPIRegistry[BaseAudioAPI]):
|
|
7
|
+
"""Registry for audio API implementations."""
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_builtins(cls) -> AudioAPIRegistry:
|
|
11
|
+
from tau.inference.api.audio.builtins import AUDIO_APIS
|
|
12
|
+
instance = cls()
|
|
13
|
+
for name, api in AUDIO_APIS:
|
|
14
|
+
instance.register(name, api)
|
|
15
|
+
return instance
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from tau.inference.api.audio.base import BaseAudioAPI as BaseAPI
|
|
10
|
+
from tau.inference.model.types import Model
|
|
11
|
+
from tau.inference.types import (
|
|
12
|
+
AudioFormat,
|
|
13
|
+
AudioOptions,
|
|
14
|
+
AudioStopReason,
|
|
15
|
+
STTContext,
|
|
16
|
+
SegmentTimestamp,
|
|
17
|
+
SynthesizedAudio,
|
|
18
|
+
TTSContext,
|
|
19
|
+
TranscribedAudio,
|
|
20
|
+
WordTimestamp,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
_BASE_URL = "https://api.sarvam.ai"
|
|
24
|
+
_TTS_ENDPOINT = "/text-to-speech"
|
|
25
|
+
_STT_ENDPOINT = "/speech-to-text"
|
|
26
|
+
|
|
27
|
+
# Sarvam returns WAV by default inside the base64 payload
|
|
28
|
+
_OUTPUT_FORMAT = AudioFormat.WAV
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SarvamAudioAPI(BaseAPI):
|
|
32
|
+
"""
|
|
33
|
+
Sarvam AI audio API — proprietary REST endpoints for Indian language TTS and STT.
|
|
34
|
+
Auth via custom header: api-subscription-key.
|
|
35
|
+
TTS response: JSON with base64-encoded audio in audios[].
|
|
36
|
+
STT response: JSON with transcript, language_code, language_probability.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, options: AudioOptions) -> None:
|
|
40
|
+
super().__init__(options)
|
|
41
|
+
|
|
42
|
+
def _new_client(self) -> httpx.AsyncClient:
|
|
43
|
+
# Per-call client (used inside `async with`) so its connection pool is
|
|
44
|
+
# always closed — no persistent client left unclosed for the GC.
|
|
45
|
+
return httpx.AsyncClient(
|
|
46
|
+
base_url=self.options.base_url or _BASE_URL,
|
|
47
|
+
timeout=self.options.timeout.total_seconds(),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
51
|
+
headers: dict[str, str] = {}
|
|
52
|
+
if self.options.api_key:
|
|
53
|
+
headers["api-subscription-key"] = self.options.api_key
|
|
54
|
+
if self.options.headers:
|
|
55
|
+
headers.update(self.options.headers)
|
|
56
|
+
return headers
|
|
57
|
+
|
|
58
|
+
async def synthesize(self, model: Model, context: TTSContext) -> SynthesizedAudio:
|
|
59
|
+
payload: dict[str, Any] = {
|
|
60
|
+
"model": model.id,
|
|
61
|
+
"text": context.input,
|
|
62
|
+
"speaker": context.voice,
|
|
63
|
+
"pace": context.speed,
|
|
64
|
+
}
|
|
65
|
+
if context.language:
|
|
66
|
+
payload["target_language_code"] = context.language
|
|
67
|
+
|
|
68
|
+
if self.options.on_payload:
|
|
69
|
+
modified = self.options.on_payload(payload)
|
|
70
|
+
if modified is not None:
|
|
71
|
+
payload = modified
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
async with self._new_client() as http:
|
|
75
|
+
response = await http.post(
|
|
76
|
+
_TTS_ENDPOINT,
|
|
77
|
+
json=payload,
|
|
78
|
+
headers=self._auth_headers(),
|
|
79
|
+
)
|
|
80
|
+
response.raise_for_status()
|
|
81
|
+
data = response.json()
|
|
82
|
+
|
|
83
|
+
if self.options.on_response:
|
|
84
|
+
self.options.on_response(data)
|
|
85
|
+
|
|
86
|
+
audio_bytes = base64.b64decode(data["audios"][0])
|
|
87
|
+
|
|
88
|
+
return SynthesizedAudio(
|
|
89
|
+
model_id=model.id,
|
|
90
|
+
provider=model.provider,
|
|
91
|
+
audio=audio_bytes,
|
|
92
|
+
format=_OUTPUT_FORMAT,
|
|
93
|
+
stop_reason=AudioStopReason.Stop,
|
|
94
|
+
timestamp=time.time(),
|
|
95
|
+
)
|
|
96
|
+
except Exception as exc:
|
|
97
|
+
return SynthesizedAudio(
|
|
98
|
+
model_id=model.id,
|
|
99
|
+
provider=model.provider,
|
|
100
|
+
audio=b"",
|
|
101
|
+
format=_OUTPUT_FORMAT,
|
|
102
|
+
stop_reason=AudioStopReason.Error,
|
|
103
|
+
error=str(exc),
|
|
104
|
+
timestamp=time.time(),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def transcribe(self, model: Model, context: STTContext) -> TranscribedAudio:
|
|
108
|
+
filename = f"audio.{context.format.value}"
|
|
109
|
+
files = {"file": (filename, context.audio, f"audio/{context.format.value}")}
|
|
110
|
+
data: dict[str, Any] = {"model": model.id}
|
|
111
|
+
|
|
112
|
+
if context.language:
|
|
113
|
+
data["language_code"] = context.language
|
|
114
|
+
if context.prompt:
|
|
115
|
+
# prompt field carries Sarvam's mode: transcribe/translate/verbatim/translit/codemix
|
|
116
|
+
data["mode"] = context.prompt
|
|
117
|
+
|
|
118
|
+
if self.options.on_payload:
|
|
119
|
+
modified = self.options.on_payload({**data, "_files": files})
|
|
120
|
+
if modified is not None:
|
|
121
|
+
files = modified.pop("_files", files)
|
|
122
|
+
data = modified
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
async with self._new_client() as http:
|
|
126
|
+
response = await http.post(
|
|
127
|
+
_STT_ENDPOINT,
|
|
128
|
+
data=data,
|
|
129
|
+
files=files,
|
|
130
|
+
headers=self._auth_headers(),
|
|
131
|
+
)
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
result = response.json()
|
|
134
|
+
|
|
135
|
+
if self.options.on_response:
|
|
136
|
+
self.options.on_response(result)
|
|
137
|
+
|
|
138
|
+
words: list[WordTimestamp] = []
|
|
139
|
+
if ts := result.get("timestamps"):
|
|
140
|
+
words = [
|
|
141
|
+
WordTimestamp(word=w["word"], start=w["start"], end=w["end"])
|
|
142
|
+
for w in ts.get("words", [])
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
return TranscribedAudio(
|
|
146
|
+
model_id=model.id,
|
|
147
|
+
provider=model.provider,
|
|
148
|
+
text=result.get("transcript", ""),
|
|
149
|
+
language=result.get("language_code"),
|
|
150
|
+
words=words,
|
|
151
|
+
stop_reason=AudioStopReason.Stop,
|
|
152
|
+
usage={"language_probability": result.get("language_probability")},
|
|
153
|
+
timestamp=time.time(),
|
|
154
|
+
)
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
return TranscribedAudio(
|
|
157
|
+
model_id=model.id,
|
|
158
|
+
provider=model.provider,
|
|
159
|
+
text="",
|
|
160
|
+
stop_reason=AudioStopReason.Error,
|
|
161
|
+
error=str(exc),
|
|
162
|
+
timestamp=time.time(),
|
|
163
|
+
)
|