courier-encode 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.
encode/whisper.py ADDED
@@ -0,0 +1,200 @@
1
+ """whisper() / whisper_async() — multipart transcription and translation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from os import PathLike
7
+ from pathlib import Path
8
+ from typing import Any, Literal
9
+
10
+ from . import _http, errors
11
+ from .responses import WhisperResponse
12
+
13
+ ALLOWED_EXTS = {".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm"}
14
+ MAX_BYTES = 25 * 1024 * 1024 # 25 MB
15
+ ALLOWED_FORMATS = {"json", "verbose_json", "text", "srt", "vtt"}
16
+
17
+
18
+ def _resolve_file(
19
+ file: str | PathLike[str] | bytes | tuple[str, bytes],
20
+ ) -> tuple[str, bytes]:
21
+ if isinstance(file, tuple) and len(file) == 2:
22
+ name, data = file
23
+ if not isinstance(data, (bytes, bytearray)):
24
+ raise errors.InvalidAudioError(
25
+ "file tuple must be (filename, bytes)", code="invalid_audio"
26
+ )
27
+ return name, bytes(data)
28
+ if isinstance(file, (bytes, bytearray)):
29
+ return "audio.wav", bytes(file)
30
+ p = Path(file) # type: ignore[arg-type]
31
+ if not p.exists():
32
+ raise errors.InvalidAudioError(f"file not found: {p}", code="invalid_audio")
33
+ return p.name, p.read_bytes()
34
+
35
+
36
+ def _validate(
37
+ name: str,
38
+ data: bytes,
39
+ response_format: str,
40
+ timestamp_granularities: Sequence[str] | None,
41
+ ) -> None:
42
+ suffix = Path(name).suffix.lower()
43
+ if suffix and suffix not in ALLOWED_EXTS:
44
+ raise errors.InvalidAudioError(
45
+ f"unsupported audio extension '{suffix}'; allowed: {sorted(ALLOWED_EXTS)}",
46
+ code="invalid_audio",
47
+ )
48
+ if len(data) > MAX_BYTES:
49
+ raise errors.InvalidRequestError(
50
+ f"file too large: {len(data)} bytes (max {MAX_BYTES})",
51
+ code="invalid_request_error",
52
+ )
53
+ if response_format not in ALLOWED_FORMATS:
54
+ raise errors.CourierError(
55
+ f"invalid response_format '{response_format}'; allowed: {sorted(ALLOWED_FORMATS)}",
56
+ code="invalid_response_format",
57
+ type="invalid_response_format",
58
+ )
59
+ if timestamp_granularities and response_format != "verbose_json":
60
+ raise errors.InvalidRequestError(
61
+ "timestamp_granularities is only allowed with response_format='verbose_json'",
62
+ code="invalid_request_error",
63
+ )
64
+
65
+
66
+ def _build_form(
67
+ *,
68
+ model: str,
69
+ response_format: str,
70
+ language: str | None,
71
+ timestamp_granularities: Sequence[str] | None,
72
+ prompt: str | None,
73
+ temperature: float | None,
74
+ ) -> list[tuple[str, tuple[None, str]]]:
75
+ form: list[tuple[str, tuple[None, str]]] = [
76
+ ("model", (None, model)),
77
+ ("response_format", (None, response_format)),
78
+ ]
79
+ if language:
80
+ form.append(("language", (None, language)))
81
+ if prompt:
82
+ form.append(("prompt", (None, prompt)))
83
+ if temperature is not None:
84
+ form.append(("temperature", (None, str(temperature))))
85
+ if timestamp_granularities:
86
+ for g in timestamp_granularities:
87
+ form.append(("timestamp_granularities[]", (None, g)))
88
+ return form
89
+
90
+
91
+ def _path_for(mode: str) -> str:
92
+ return "/v1/audio/translations" if mode == "translate" else "/v1/audio/transcriptions"
93
+
94
+
95
+ def _parse_response(body: Any, response_format: str) -> WhisperResponse:
96
+ if response_format in ("text", "srt", "vtt"):
97
+ text = body if isinstance(body, str) else str(body)
98
+ return WhisperResponse(text=text, raw=text)
99
+ if not isinstance(body, dict):
100
+ return WhisperResponse(text=str(body), raw=body)
101
+ return WhisperResponse(
102
+ text=str(body.get("text", "")),
103
+ language=body.get("language"),
104
+ duration=body.get("duration"),
105
+ segments=body.get("segments"),
106
+ words=body.get("words"),
107
+ raw=body,
108
+ )
109
+
110
+
111
+ def whisper(
112
+ *,
113
+ file: str | PathLike[str] | bytes | tuple[str, bytes],
114
+ mode: Literal["transcribe", "translate"] = "transcribe",
115
+ model: str = "whisper-1",
116
+ response_format: Literal["json", "verbose_json", "text", "srt", "vtt"] = "json",
117
+ language: str | None = None,
118
+ timestamp_granularities: Sequence[Literal["word", "segment"]] | None = None,
119
+ prompt: str | None = None,
120
+ temperature: float | None = None,
121
+ client: Any = None,
122
+ api_key: str | None = None,
123
+ base_url: str | None = None,
124
+ timeout: float | None = 120.0,
125
+ ) -> WhisperResponse:
126
+ name, data = _resolve_file(file)
127
+ _validate(name, data, response_format, timestamp_granularities)
128
+
129
+ if client is None:
130
+ from .client import Client, get_default_client
131
+
132
+ if api_key or base_url or (timeout is not None and timeout != 60.0):
133
+ client = Client(api_key=api_key, base_url=base_url, timeout=timeout)
134
+ else:
135
+ client = get_default_client()
136
+
137
+ files = {"file": (name, data)}
138
+ form = _build_form(
139
+ model=model,
140
+ response_format=response_format,
141
+ language=language,
142
+ timestamp_granularities=timestamp_granularities,
143
+ prompt=prompt,
144
+ temperature=temperature,
145
+ )
146
+ resp = _http.request_sync(
147
+ client._http,
148
+ "POST",
149
+ _path_for(mode),
150
+ files=files,
151
+ data=dict(form), # httpx accepts list-of-tuples too via 'data'
152
+ max_retries=client.max_retries,
153
+ )
154
+ return _parse_response(_http.parse_body(resp), response_format)
155
+
156
+
157
+ async def whisper_async(
158
+ *,
159
+ file: str | PathLike[str] | bytes | tuple[str, bytes],
160
+ mode: Literal["transcribe", "translate"] = "transcribe",
161
+ model: str = "whisper-1",
162
+ response_format: Literal["json", "verbose_json", "text", "srt", "vtt"] = "json",
163
+ language: str | None = None,
164
+ timestamp_granularities: Sequence[Literal["word", "segment"]] | None = None,
165
+ prompt: str | None = None,
166
+ temperature: float | None = None,
167
+ client: Any = None,
168
+ api_key: str | None = None,
169
+ base_url: str | None = None,
170
+ timeout: float | None = 120.0,
171
+ ) -> WhisperResponse:
172
+ name, data = _resolve_file(file)
173
+ _validate(name, data, response_format, timestamp_granularities)
174
+
175
+ if client is None:
176
+ from .client import AsyncClient, get_default_async_client
177
+
178
+ if api_key or base_url or (timeout is not None and timeout != 60.0):
179
+ client = AsyncClient(api_key=api_key, base_url=base_url, timeout=timeout)
180
+ else:
181
+ client = get_default_async_client()
182
+
183
+ files = {"file": (name, data)}
184
+ form = _build_form(
185
+ model=model,
186
+ response_format=response_format,
187
+ language=language,
188
+ timestamp_granularities=timestamp_granularities,
189
+ prompt=prompt,
190
+ temperature=temperature,
191
+ )
192
+ resp = await _http.request_async(
193
+ client._http,
194
+ "POST",
195
+ _path_for(mode),
196
+ files=files,
197
+ data=dict(form),
198
+ max_retries=client.max_retries,
199
+ )
200
+ return _parse_response(_http.parse_body(resp), response_format)