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.
Files changed (283) hide show
  1. tau/__init__.py +0 -0
  2. tau/agent/__init__.py +11 -0
  3. tau/agent/prompt/__init__.py +10 -0
  4. tau/agent/prompt/builder.py +302 -0
  5. tau/agent/prompt/types.py +33 -0
  6. tau/agent/service.py +369 -0
  7. tau/agent/types.py +61 -0
  8. tau/auth/manager.py +247 -0
  9. tau/auth/storage.py +82 -0
  10. tau/auth/types.py +41 -0
  11. tau/builtins/__init__.py +4 -0
  12. tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
  13. tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
  14. tau/builtins/commands/__init__.py +41 -0
  15. tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  16. tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  17. tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
  18. tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
  19. tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
  20. tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
  21. tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
  22. tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
  23. tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
  24. tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
  25. tau/builtins/commands/clear.py +16 -0
  26. tau/builtins/commands/compact.py +28 -0
  27. tau/builtins/commands/reload.py +27 -0
  28. tau/builtins/commands/session.py +19 -0
  29. tau/builtins/extensions/footer/__init__.py +76 -0
  30. tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
  31. tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
  32. tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
  33. tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
  34. tau/builtins/extensions/footer/git.py +26 -0
  35. tau/builtins/extensions/footer/model.py +69 -0
  36. tau/builtins/extensions/footer/utils.py +44 -0
  37. tau/builtins/extensions/header/__init__.py +18 -0
  38. tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
  39. tau/builtins/models/__init__.py +0 -0
  40. tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
  41. tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
  42. tau/builtins/models/audio.py +43 -0
  43. tau/builtins/models/image.py +43 -0
  44. tau/builtins/models/text.py +482 -0
  45. tau/builtins/models/video.py +40 -0
  46. tau/builtins/prompts/commit.md +7 -0
  47. tau/builtins/prompts/docs.md +7 -0
  48. tau/builtins/prompts/explain.md +7 -0
  49. tau/builtins/prompts/fix.md +7 -0
  50. tau/builtins/prompts/refactor.md +7 -0
  51. tau/builtins/prompts/review.md +7 -0
  52. tau/builtins/prompts/test.md +7 -0
  53. tau/builtins/providers/__init__.py +0 -0
  54. tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  55. tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
  56. tau/builtins/providers/audio.py +10 -0
  57. tau/builtins/providers/image.py +9 -0
  58. tau/builtins/providers/text.py +33 -0
  59. tau/builtins/providers/video.py +6 -0
  60. tau/builtins/skills/code-review/SKILL.md +4 -0
  61. tau/builtins/skills/debug/SKILL.md +4 -0
  62. tau/builtins/skills/git-commit/SKILL.md +4 -0
  63. tau/builtins/themes/dark.yaml +1 -0
  64. tau/builtins/themes/light.yaml +46 -0
  65. tau/builtins/tools/__init__.py +73 -0
  66. tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  67. tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  68. tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
  69. tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
  70. tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
  71. tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
  72. tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
  73. tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
  74. tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
  75. tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
  76. tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
  77. tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
  78. tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
  79. tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
  80. tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
  81. tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
  82. tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
  83. tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
  84. tau/builtins/tools/edit.py +215 -0
  85. tau/builtins/tools/glob.py +112 -0
  86. tau/builtins/tools/grep.py +146 -0
  87. tau/builtins/tools/ls.py +135 -0
  88. tau/builtins/tools/read.py +122 -0
  89. tau/builtins/tools/terminal.py +150 -0
  90. tau/builtins/tools/write.py +105 -0
  91. tau/commands/__init__.py +10 -0
  92. tau/commands/registry.py +71 -0
  93. tau/commands/types.py +33 -0
  94. tau/console/__init__.py +0 -0
  95. tau/console/cli.py +266 -0
  96. tau/console/commands/__init__.py +0 -0
  97. tau/console/commands/auth.py +193 -0
  98. tau/console/commands/packages.py +104 -0
  99. tau/console/commands/update.py +76 -0
  100. tau/core/__init__.py +0 -0
  101. tau/core/registry.py +102 -0
  102. tau/engine/__init__.py +47 -0
  103. tau/engine/service.py +768 -0
  104. tau/engine/types.py +163 -0
  105. tau/extensions/__init__.py +28 -0
  106. tau/extensions/api.py +928 -0
  107. tau/extensions/context.py +462 -0
  108. tau/extensions/events.py +70 -0
  109. tau/extensions/loader.py +386 -0
  110. tau/extensions/runtime.py +184 -0
  111. tau/extensions/settings.py +137 -0
  112. tau/hooks/__init__.py +112 -0
  113. tau/hooks/engine.py +237 -0
  114. tau/hooks/inference.py +21 -0
  115. tau/hooks/runtime.py +126 -0
  116. tau/hooks/service.py +121 -0
  117. tau/hooks/session.py +117 -0
  118. tau/hooks/tui.py +61 -0
  119. tau/hooks/types.py +72 -0
  120. tau/inference/__init__.py +180 -0
  121. tau/inference/api/__init__.py +0 -0
  122. tau/inference/api/audio/__init__.py +0 -0
  123. tau/inference/api/audio/base.py +29 -0
  124. tau/inference/api/audio/builtins.py +15 -0
  125. tau/inference/api/audio/elevenlabs_audio.py +183 -0
  126. tau/inference/api/audio/gemini_audio.py +95 -0
  127. tau/inference/api/audio/openai_audio.py +159 -0
  128. tau/inference/api/audio/registry.py +15 -0
  129. tau/inference/api/audio/sarvam_audio.py +163 -0
  130. tau/inference/api/audio/service.py +103 -0
  131. tau/inference/api/audio/utils.py +47 -0
  132. tau/inference/api/image/__init__.py +0 -0
  133. tau/inference/api/image/base.py +17 -0
  134. tau/inference/api/image/builtins.py +8 -0
  135. tau/inference/api/image/gemini_image.py +77 -0
  136. tau/inference/api/image/openai_image.py +103 -0
  137. tau/inference/api/image/openrouter.py +144 -0
  138. tau/inference/api/image/registry.py +15 -0
  139. tau/inference/api/image/service.py +71 -0
  140. tau/inference/api/registry.py +82 -0
  141. tau/inference/api/text/__init__.py +0 -0
  142. tau/inference/api/text/anthropic_claude_code.py +222 -0
  143. tau/inference/api/text/anthropic_messages.py +196 -0
  144. tau/inference/api/text/base.py +40 -0
  145. tau/inference/api/text/builtins.py +19 -0
  146. tau/inference/api/text/gemini_generate.py +234 -0
  147. tau/inference/api/text/github_copilot_chat.py +172 -0
  148. tau/inference/api/text/google_antigravity.py +522 -0
  149. tau/inference/api/text/mistral_chat.py +284 -0
  150. tau/inference/api/text/ollama_chat.py +200 -0
  151. tau/inference/api/text/openai_codex_responses.py +497 -0
  152. tau/inference/api/text/openai_completions.py +227 -0
  153. tau/inference/api/text/openai_responses.py +235 -0
  154. tau/inference/api/text/registry.py +50 -0
  155. tau/inference/api/text/service.py +297 -0
  156. tau/inference/api/text/types.py +7 -0
  157. tau/inference/api/text/utils.py +228 -0
  158. tau/inference/api/video/__init__.py +0 -0
  159. tau/inference/api/video/base.py +26 -0
  160. tau/inference/api/video/builtins.py +7 -0
  161. tau/inference/api/video/fal_video.py +119 -0
  162. tau/inference/api/video/openrouter_video.py +142 -0
  163. tau/inference/api/video/registry.py +15 -0
  164. tau/inference/api/video/service.py +72 -0
  165. tau/inference/model/__init__.py +0 -0
  166. tau/inference/model/registry.py +102 -0
  167. tau/inference/model/types.py +65 -0
  168. tau/inference/provider/__init__.py +0 -0
  169. tau/inference/provider/oauth/__init__.py +35 -0
  170. tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
  171. tau/inference/provider/oauth/github_copilot.py +333 -0
  172. tau/inference/provider/oauth/google_antigravity.py +258 -0
  173. tau/inference/provider/oauth/openai_codex.py +309 -0
  174. tau/inference/provider/oauth/pkce.py +14 -0
  175. tau/inference/provider/oauth/types.py +46 -0
  176. tau/inference/provider/oauth/utils.py +154 -0
  177. tau/inference/provider/registry.py +141 -0
  178. tau/inference/provider/types.py +114 -0
  179. tau/inference/types.py +549 -0
  180. tau/inference/utils.py +219 -0
  181. tau/message/__init__.py +0 -0
  182. tau/message/types.py +482 -0
  183. tau/message/utils.py +178 -0
  184. tau/packages/__init__.py +11 -0
  185. tau/packages/manager.py +190 -0
  186. tau/packages/types.py +20 -0
  187. tau/packages/utils.py +67 -0
  188. tau/prompts/expand.py +58 -0
  189. tau/prompts/loader.py +69 -0
  190. tau/prompts/registry.py +45 -0
  191. tau/prompts/types.py +24 -0
  192. tau/rpc/__init__.py +8 -0
  193. tau/rpc/mode.py +783 -0
  194. tau/rpc/types.py +252 -0
  195. tau/runtime/service.py +759 -0
  196. tau/runtime/types.py +303 -0
  197. tau/session/branch_summarization.py +312 -0
  198. tau/session/compaction.py +646 -0
  199. tau/session/manager.py +652 -0
  200. tau/session/types.py +188 -0
  201. tau/session/utils.py +233 -0
  202. tau/settings/manager.py +1077 -0
  203. tau/settings/paths.py +150 -0
  204. tau/settings/storage.py +63 -0
  205. tau/settings/types.py +173 -0
  206. tau/settings/utils.py +25 -0
  207. tau/skills/loader.py +91 -0
  208. tau/skills/registry.py +70 -0
  209. tau/skills/types.py +25 -0
  210. tau/themes/loader.py +238 -0
  211. tau/themes/registry.py +108 -0
  212. tau/themes/types.py +19 -0
  213. tau/tool/__init__.py +3 -0
  214. tau/tool/registry.py +117 -0
  215. tau/tool/render.py +21 -0
  216. tau/tool/types.py +244 -0
  217. tau/trust/__init__.py +13 -0
  218. tau/trust/manager.py +80 -0
  219. tau/trust/types.py +14 -0
  220. tau/trust/utils.py +72 -0
  221. tau/tui/__init__.py +54 -0
  222. tau/tui/agent_hooks.py +346 -0
  223. tau/tui/ansi.py +330 -0
  224. tau/tui/app.py +540 -0
  225. tau/tui/autocomplete.py +33 -0
  226. tau/tui/capabilities.py +119 -0
  227. tau/tui/commands/__init__.py +3 -0
  228. tau/tui/commands/appearance.py +498 -0
  229. tau/tui/commands/auth.py +232 -0
  230. tau/tui/commands/context.py +38 -0
  231. tau/tui/commands/misc.py +82 -0
  232. tau/tui/commands/model.py +118 -0
  233. tau/tui/commands/session.py +464 -0
  234. tau/tui/component.py +268 -0
  235. tau/tui/components/__init__.py +0 -0
  236. tau/tui/components/autocomplete_manager.py +267 -0
  237. tau/tui/components/autocomplete_picker.py +143 -0
  238. tau/tui/components/box.py +90 -0
  239. tau/tui/components/command_palette.py +144 -0
  240. tau/tui/components/dynamic_border.py +19 -0
  241. tau/tui/components/file_picker.py +233 -0
  242. tau/tui/components/image.py +181 -0
  243. tau/tui/components/inline_selector.py +71 -0
  244. tau/tui/components/layout.py +1194 -0
  245. tau/tui/components/message_list.py +692 -0
  246. tau/tui/components/modal.py +97 -0
  247. tau/tui/components/model_palette.py +204 -0
  248. tau/tui/components/picker_overlay.py +174 -0
  249. tau/tui/components/prompt_overlay.py +236 -0
  250. tau/tui/components/resume_modal.py +372 -0
  251. tau/tui/components/select_list.py +222 -0
  252. tau/tui/components/settings_modal.py +274 -0
  253. tau/tui/components/settings_schema.py +203 -0
  254. tau/tui/components/spinner.py +119 -0
  255. tau/tui/components/text_input.py +396 -0
  256. tau/tui/components/text_prompt.py +82 -0
  257. tau/tui/components/tree_select_list.py +580 -0
  258. tau/tui/components/trust_screen.py +97 -0
  259. tau/tui/diff.py +114 -0
  260. tau/tui/fuzzy.py +99 -0
  261. tau/tui/input.py +496 -0
  262. tau/tui/input_handler.py +716 -0
  263. tau/tui/keybindings.py +87 -0
  264. tau/tui/markdown.py +286 -0
  265. tau/tui/message_renderers.py +31 -0
  266. tau/tui/overlay.py +326 -0
  267. tau/tui/renderer.py +378 -0
  268. tau/tui/terminal.py +499 -0
  269. tau/tui/theme.py +148 -0
  270. tau/tui/tui.py +544 -0
  271. tau/tui/ui_context.py +768 -0
  272. tau/tui/utils.py +20 -0
  273. tau/utils/__init__.py +0 -0
  274. tau/utils/http_proxy.py +221 -0
  275. tau/utils/image_processing.py +172 -0
  276. tau/utils/secrets.py +59 -0
  277. tau/utils/version_check.py +60 -0
  278. tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
  279. tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
  280. tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
  281. tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  282. tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  283. 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
+ )