scriptivox 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.
- scriptivox/__init__.py +145 -0
- scriptivox/_async_client.py +428 -0
- scriptivox/_client.py +437 -0
- scriptivox/_common.py +293 -0
- scriptivox/_error_codes.py +190 -0
- scriptivox/_exceptions.py +196 -0
- scriptivox/_types.py +473 -0
- scriptivox/_version.py +1 -0
- scriptivox/_webhooks.py +108 -0
- scriptivox/py.typed +0 -0
- scriptivox-0.1.0.dist-info/METADATA +235 -0
- scriptivox-0.1.0.dist-info/RECORD +14 -0
- scriptivox-0.1.0.dist-info/WHEEL +4 -0
- scriptivox-0.1.0.dist-info/licenses/LICENSE +21 -0
scriptivox/_types.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
# GENERATED by scripts/generate.py from openapi/openapi.json — do not edit by hand.
|
|
2
|
+
# OpenAPI 1.0.0 · sha256 489f83bddaa9893f
|
|
3
|
+
"""Response and request shapes of the Scriptivox API, as TypedDicts (plain dicts at runtime)."""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Literal, Optional, TypedDict, Union
|
|
7
|
+
|
|
8
|
+
class _ErrorDetailRequired(TypedDict):
|
|
9
|
+
#: Machine-readable error code.
|
|
10
|
+
code: Literal["INVALID_REQUEST", "INVALID_FILENAME", "INVALID_MEDIA_FORMAT", "FILE_NOT_UPLOADED", "FILE_TOO_LARGE", "UPLOAD_ALREADY_USED", "UPLOAD_EXPIRED", "INVALID_API_KEY", "API_KEY_REVOKED", "INSUFFICIENT_BALANCE", "ZERO_BALANCE", "UPLOAD_NOT_FOUND", "TRANSCRIPTION_NOT_FOUND", "NOT_FOUND", "FORBIDDEN", "METHOD_NOT_ALLOWED", "CONFLICT", "PAYLOAD_TOO_LARGE", "UNSUPPORTED_MEDIA_TYPE", "IDEMPOTENCY_KEY_LOCKED", "IDEMPOTENCY_KEY_CONFLICT", "RATE_LIMIT_EXCEEDED", "INTERNAL_ERROR", "MAINTENANCE", "ENDPOINT_REMOVED", "ISSUER_UNCONFIGURED"]
|
|
11
|
+
#: Human-readable description of what went wrong.
|
|
12
|
+
message: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ErrorDetail(_ErrorDetailRequired, total=False):
|
|
16
|
+
"""The error envelope. Always present on a failed request."""
|
|
17
|
+
#: Documentation for this error code.
|
|
18
|
+
docs_url: str
|
|
19
|
+
#: The id of this request, the same value as the `X-Request-Id` response header. Include it when you contact support.
|
|
20
|
+
request_id: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _ErrorRequired(TypedDict):
|
|
24
|
+
#: The error envelope. Always present on a failed request.
|
|
25
|
+
error: ErrorDetail
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Error(_ErrorRequired, total=False):
|
|
29
|
+
"""Every failed request returns this shape. `code` is stable and machine-readable; `message` is a sanitized, customer-safe string and never a raw stack trace; `request_id` identifies the request for support."""
|
|
30
|
+
#: Seconds to wait before retrying. Present on 429 responses alongside the Retry-After header.
|
|
31
|
+
retry_after: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CreateUploadRequest(TypedDict):
|
|
35
|
+
#: Name of the file being uploaded, e.g. "meeting.mp3". ASCII only, no path separators, and it must carry a supported extension.
|
|
36
|
+
filename: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Upload(TypedDict):
|
|
40
|
+
#: Pass this to POST /v1/transcribe once the PUT succeeds.
|
|
41
|
+
upload_id: str
|
|
42
|
+
#: Presigned URL to PUT the file bytes to.
|
|
43
|
+
upload_url: str
|
|
44
|
+
#: Seconds until upload_url expires. Currently 3600.
|
|
45
|
+
expires_in: int
|
|
46
|
+
#: HTTP method to use against upload_url.
|
|
47
|
+
method: Literal["PUT"]
|
|
48
|
+
#: Headers that must be sent with the PUT request.
|
|
49
|
+
headers: Dict[str, str]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _CreateTranscriptionFromUrlRequired(TypedDict):
|
|
53
|
+
#: Anonymously accessible http or https URL. An anonymous GET must return the raw file bytes, not an HTML preview, login form or password page. Direct and presigned URLs (S3, GCS, Azure SAS) always work. Share links work only when set to "anyone with the link can view": Google Drive, Dropbox files (not /scl/fo/ folder links), and OneDrive personal /redir or onedrive.live.com links. OneDrive 1drv.ms/v/c/ and /p/c/ photo shares cannot be used — Microsoft routes them through SharePoint Photos, which requires sign-in.
|
|
54
|
+
url: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CreateTranscriptionFromUrl(_CreateTranscriptionFromUrlRequired, total=False):
|
|
58
|
+
"""Transcribe a file we download from a public URL."""
|
|
59
|
+
#: ISO 639-1 language code (with BCP-47 fallbacks such as `yue` and `kea`), e.g. "en", "es", "fr". Omit or pass null for auto-detection. Passing the language explicitly is recommended: it is faster (the model skips its detection pass) and avoids known auto-detect failure modes on short clips, code-switched audio, and files that open with music. Forcing a language the audio is not in can produce a translation rather than a transcription. Invalid codes return 400 INVALID_REQUEST.
|
|
60
|
+
language: Optional[str]
|
|
61
|
+
#: Identify who said what and attach a speaker label to every utterance. Implies align: true, because speaker assignment needs word-level alignment.
|
|
62
|
+
diarize: bool
|
|
63
|
+
#: Expected number of speakers. Requires diarize: true; passing it without diarize returns 400 INVALID_REQUEST. This is a soft prior, not a hard ceiling — the result may contain slightly more or fewer labels. Strongly recommended whenever the count is known: it measurably reduces over- and under-segmentation.
|
|
64
|
+
speaker_count: int
|
|
65
|
+
#: Produce word-level start/end timestamps plus per-word confidence where the alignment model supports it. Confidence is language-dependent and may be null. Ignored (treated as true) when diarize is true.
|
|
66
|
+
align: bool
|
|
67
|
+
#: URL to receive the job's webhooks (processing, then completed or failed). Must be https://. Signed with the key's webhook signing secret and retried with backoff until your server answers 2xx — see https://platform.scriptivox.com/docs/webhooks.
|
|
68
|
+
webhook_url: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _CreateTranscriptionFromUploadRequired(TypedDict):
|
|
72
|
+
#: The upload_id returned by POST /v1/upload.
|
|
73
|
+
upload_id: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class CreateTranscriptionFromUpload(_CreateTranscriptionFromUploadRequired, total=False):
|
|
77
|
+
"""Transcribe a file previously sent to a presigned upload URL."""
|
|
78
|
+
#: ISO 639-1 language code (with BCP-47 fallbacks such as `yue` and `kea`), e.g. "en", "es", "fr". Omit or pass null for auto-detection. Passing the language explicitly is recommended: it is faster (the model skips its detection pass) and avoids known auto-detect failure modes on short clips, code-switched audio, and files that open with music. Forcing a language the audio is not in can produce a translation rather than a transcription. Invalid codes return 400 INVALID_REQUEST.
|
|
79
|
+
language: Optional[str]
|
|
80
|
+
#: Identify who said what and attach a speaker label to every utterance. Implies align: true, because speaker assignment needs word-level alignment.
|
|
81
|
+
diarize: bool
|
|
82
|
+
#: Expected number of speakers. Requires diarize: true; passing it without diarize returns 400 INVALID_REQUEST. This is a soft prior, not a hard ceiling — the result may contain slightly more or fewer labels. Strongly recommended whenever the count is known: it measurably reduces over- and under-segmentation.
|
|
83
|
+
speaker_count: int
|
|
84
|
+
#: Produce word-level start/end timestamps plus per-word confidence where the alignment model supports it. Confidence is language-dependent and may be null. Ignored (treated as true) when diarize is true.
|
|
85
|
+
align: bool
|
|
86
|
+
#: URL to receive the job's webhooks (processing, then completed or failed). Must be https://. Signed with the key's webhook signing secret and retried with backoff until your server answers 2xx — see https://platform.scriptivox.com/docs/webhooks.
|
|
87
|
+
webhook_url: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Supply exactly one of `url` (Scriptivox downloads the file) or `upload_id` (returned by POST /v1/upload). Every other property is optional and shared by both forms.
|
|
91
|
+
CreateTranscriptionRequest = Union[CreateTranscriptionFromUrl, CreateTranscriptionFromUpload]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _TranscriptionCreatedRequired(TypedDict):
|
|
95
|
+
#: Transcription id. Pass it to GET /v1/transcribe/{id} to poll for the result.
|
|
96
|
+
id: str
|
|
97
|
+
#: Always "created" here. The file has been accepted but not yet downloaded or validated, so this is not a statement that the input is usable.
|
|
98
|
+
status: Literal["created"]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TranscriptionCreated(_TranscriptionCreatedRequired, total=False):
|
|
102
|
+
#: Human-readable confirmation. Not machine-readable; do not branch on it.
|
|
103
|
+
message: str
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TranscriptionError(TypedDict):
|
|
107
|
+
"""Present when status is "failed". These codes surface only on GET /v1/transcribe/{id}, never on the submit call. Failed jobs are free — the reservation is released."""
|
|
108
|
+
#: Stable machine-readable reason the job failed. Branch on this, never on message.
|
|
109
|
+
code: Literal["URL_NOT_ACCESSIBLE", "DOWNLOAD_FAILED", "INVALID_MEDIA_FORMAT", "FILE_TOO_LARGE", "DURATION_TOO_LONG", "INSUFFICIENT_BALANCE", "PROCESSING_ERROR", "CREATED_TIMEOUT", "DOWNLOAD_TIMEOUT", "PROCESSING_TIMEOUT", "BILLING_ERROR", "INTERNAL_ERROR", "CANCELLED"]
|
|
110
|
+
#: Human-readable explanation. Wording may change between releases.
|
|
111
|
+
message: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _TranscriptionCancelledRequired(TypedDict):
|
|
115
|
+
#: The transcription that was cancelled.
|
|
116
|
+
id: str
|
|
117
|
+
#: A cancelled job is reported as "failed" with error.code CANCELLED — there is no separate "cancelled" status.
|
|
118
|
+
status: Literal["failed"]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TranscriptionCancelled(_TranscriptionCancelledRequired, total=False):
|
|
122
|
+
error: TranscriptionError
|
|
123
|
+
#: Kept for compatibility, always 0: nothing is reserved before transcription starts, and a job can only be cancelled before then.
|
|
124
|
+
released_cents: float
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# created: accepted, about to download or validate. downloading: URL flow only. processing: being transcribed. completed: result is populated. failed: inspect error.code.
|
|
128
|
+
TranscriptionStatus = Literal["created", "downloading", "pending", "processing", "completed", "failed"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Word(TypedDict, total=False):
|
|
132
|
+
"""Present only when align is effectively true. Leading whitespace is stripped — reconstruct spacing from utterance.text if you need it exactly."""
|
|
133
|
+
#: The token as transcribed, including any trailing punctuation.
|
|
134
|
+
word: str
|
|
135
|
+
#: Seconds from the start of the audio.
|
|
136
|
+
start: float
|
|
137
|
+
#: Seconds from the start of the audio.
|
|
138
|
+
end: float
|
|
139
|
+
#: Null for languages whose alignment model does not report confidence. Handle null defensively.
|
|
140
|
+
confidence: Optional[float]
|
|
141
|
+
#: Absent when diarize is false.
|
|
142
|
+
speaker: str
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Utterance(TypedDict, total=False):
|
|
146
|
+
#: Seconds from the start of the audio to the first word.
|
|
147
|
+
start: float
|
|
148
|
+
#: Seconds from the start of the audio to the last word.
|
|
149
|
+
end: float
|
|
150
|
+
#: The utterance as a single string, with original spacing.
|
|
151
|
+
text: str
|
|
152
|
+
#: Labels are "SPEAKER 1", "SPEAKER 2", ... (space, 1-indexed). Null when diarize is false.
|
|
153
|
+
speaker: Optional[str]
|
|
154
|
+
#: Mean word confidence for this utterance. Null for languages whose alignment model does not report it.
|
|
155
|
+
confidence: Optional[float]
|
|
156
|
+
#: Empty array when align is false.
|
|
157
|
+
words: List[Word]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TranscriptionResult(TypedDict, total=False):
|
|
161
|
+
"""Populated once status is "completed"."""
|
|
162
|
+
#: The whole transcript as one string, utterances joined in order.
|
|
163
|
+
full_transcript: str
|
|
164
|
+
#: The language actually used, detected or supplied.
|
|
165
|
+
language: str
|
|
166
|
+
#: Length of the decoded audio in seconds.
|
|
167
|
+
duration_seconds: float
|
|
168
|
+
#: Null when diarize is false.
|
|
169
|
+
speakers: Optional[List[str]]
|
|
170
|
+
#: Speech segments in chronological order. This is the structure to iterate; full_transcript is a convenience.
|
|
171
|
+
utterances: List[Utterance]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _TranscriptionSummaryRequired(TypedDict):
|
|
175
|
+
#: Transcription id, as returned by POST /v1/transcribe.
|
|
176
|
+
id: str
|
|
177
|
+
status: TranscriptionStatus
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TranscriptionSummary(_TranscriptionSummaryRequired, total=False):
|
|
181
|
+
"""A transcription without its transcript. This is what GET /v1/transcriptions returns per item."""
|
|
182
|
+
#: Rounded to the nearest whole second. Null until the file has been probed.
|
|
183
|
+
audio_duration_seconds: Optional[int]
|
|
184
|
+
#: Size of the source file. Null until it has been downloaded.
|
|
185
|
+
file_size_bytes: Optional[int]
|
|
186
|
+
#: ISO code of the language used. Null until detection has run.
|
|
187
|
+
language: Optional[str]
|
|
188
|
+
#: Whether speaker diarization was requested for this job.
|
|
189
|
+
diarize: bool
|
|
190
|
+
#: The speaker count supplied on the request, not a detected one. Null when it was not supplied.
|
|
191
|
+
speaker_count: Optional[int]
|
|
192
|
+
#: The effective value. Always true when diarize is true, even if the request passed align: false.
|
|
193
|
+
align: bool
|
|
194
|
+
#: Fractional and exact, e.g. 0.061111 for an 11-second clip at $0.20/hour. The balance is debited against this precise value, not a rounded one.
|
|
195
|
+
cost_cents: Optional[float]
|
|
196
|
+
#: Present only on URL-flow transcriptions.
|
|
197
|
+
source_url: str
|
|
198
|
+
#: Free-text stage label while processing, e.g. "downloading". For display only — the machine-readable state is status.
|
|
199
|
+
progress: Optional[str]
|
|
200
|
+
#: When the job was accepted. RFC 3339, UTC.
|
|
201
|
+
created_at: str
|
|
202
|
+
#: When transcription began. Null while the job is still queued.
|
|
203
|
+
started_at: Optional[str]
|
|
204
|
+
#: When the job reached a terminal state. Null until then.
|
|
205
|
+
completed_at: Optional[str]
|
|
206
|
+
error: TranscriptionError
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Transcription(TranscriptionSummary, total=False):
|
|
210
|
+
"""A transcription including its result once complete. Some result fields depend on the flags the job was created with: result.speakers and utterances[].speaker are null when diarize is false, and utterances[].words is empty when align is false."""
|
|
211
|
+
#: Null until status is "completed".
|
|
212
|
+
result: Optional[TranscriptionResult]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TranscriptionPage(TypedDict):
|
|
216
|
+
#: This page of transcriptions, newest first unless order says otherwise. Each item omits the heavy result object.
|
|
217
|
+
items: List[TranscriptionSummary]
|
|
218
|
+
#: Pass back as the cursor query parameter for the next page. Null on the last page.
|
|
219
|
+
next_cursor: Optional[str]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class ServiceDiscoveryEndpointsItem(TypedDict, total=False):
|
|
223
|
+
#: HTTP method.
|
|
224
|
+
method: str
|
|
225
|
+
#: Path, including the /v1 prefix.
|
|
226
|
+
path: str
|
|
227
|
+
#: What the operation does.
|
|
228
|
+
summary: str
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class _ServiceDiscoveryRequired(TypedDict):
|
|
232
|
+
#: Human-readable name of the API.
|
|
233
|
+
service: str
|
|
234
|
+
#: The API version this host serves, matching the URL prefix.
|
|
235
|
+
version: str
|
|
236
|
+
#: Prefix every path in `endpoints` with this.
|
|
237
|
+
base_url: str
|
|
238
|
+
#: Human-readable documentation index.
|
|
239
|
+
documentation: str
|
|
240
|
+
#: This document.
|
|
241
|
+
openapi: str
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class ServiceDiscovery(_ServiceDiscoveryRequired, total=False):
|
|
245
|
+
"""What GET /v1 returns. Deliberately loose: it is a signpost for an agent that has just found this host, not a contract to generate a client from. The authoritative description of every operation is this document."""
|
|
246
|
+
#: One sentence on what the API does.
|
|
247
|
+
description: str
|
|
248
|
+
#: What the service is for, and when not to use it.
|
|
249
|
+
llms_txt: str
|
|
250
|
+
#: Model Context Protocol manifest, for agents that call tools natively.
|
|
251
|
+
mcp: str
|
|
252
|
+
#: Live uptime and incident history.
|
|
253
|
+
status: str
|
|
254
|
+
#: How to authenticate every other operation: the scheme, the accepted headers, the key format, and where a human gets one.
|
|
255
|
+
authentication: Dict[str, Any]
|
|
256
|
+
#: One entry per operation, with method, path and a one-line summary.
|
|
257
|
+
endpoints: List[ServiceDiscoveryEndpointsItem]
|
|
258
|
+
#: Per-IP and per-key ceilings, the header names that carry them, and where they are documented.
|
|
259
|
+
rate_limits: Dict[str, Any]
|
|
260
|
+
#: Versioning scheme, current version, policy URL, and the headers a deprecation would arrive on.
|
|
261
|
+
versioning: Dict[str, Any]
|
|
262
|
+
#: The error envelope, and where the code list lives.
|
|
263
|
+
errors: Dict[str, Any]
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class FeedbackRequest(TypedDict, total=False):
|
|
267
|
+
#: How good the transcript is, 1 (poor) to 5 (excellent). Completed jobs only.
|
|
268
|
+
rating: int
|
|
269
|
+
#: What the rating is about.
|
|
270
|
+
topics: List[Literal["accuracy", "speakers", "timing", "language", "formatting", "other"]]
|
|
271
|
+
#: A short comment on the transcript.
|
|
272
|
+
comment: str
|
|
273
|
+
#: Your end user's own words, unchanged, if you have them.
|
|
274
|
+
their_words: str
|
|
275
|
+
#: true to report a problem with this job (failed, empty or wrong). Needs what_happened.
|
|
276
|
+
problem: bool
|
|
277
|
+
#: What went wrong, for a problem report.
|
|
278
|
+
what_happened: str
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class FeedbackResultProblemReport(TypedDict, total=False):
|
|
282
|
+
"""The problem report opened or added to, or null."""
|
|
283
|
+
#: The report id (null if a parallel call opened it a moment earlier).
|
|
284
|
+
id: Optional[str]
|
|
285
|
+
#: Open until the team resolves it (within 24 hours).
|
|
286
|
+
status: Literal["open"]
|
|
287
|
+
#: True when this was added to a report on the same job that is still open.
|
|
288
|
+
added_to_open_report: bool
|
|
289
|
+
#: True when a rating of 1 or 2 with words opened it.
|
|
290
|
+
opened_by_rating: bool
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class _FeedbackResultRequired(TypedDict):
|
|
294
|
+
#: The job the feedback is about.
|
|
295
|
+
transcription_id: str
|
|
296
|
+
#: True when a rating was sent and saved.
|
|
297
|
+
rating_saved: bool
|
|
298
|
+
#: The problem report opened or added to, or null.
|
|
299
|
+
problem_report: Optional[FeedbackResultProblemReport]
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class FeedbackResult(_FeedbackResultRequired, total=False):
|
|
303
|
+
#: Set when the rating was saved but the problem report was not (for example the daily limit).
|
|
304
|
+
problem_report_error: str
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class WebhookTestRequest(TypedDict, total=False):
|
|
308
|
+
#: Where to send the test event. https:// only. Without it, the account's default webhook URL is used.
|
|
309
|
+
url: str
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class _WebhookSendResultRequired(TypedDict):
|
|
313
|
+
#: The X-Scriptivox-Delivery id of this event.
|
|
314
|
+
delivery_id: str
|
|
315
|
+
#: The event sent.
|
|
316
|
+
event: str
|
|
317
|
+
#: The X-Scriptivox-Attempt value of this try.
|
|
318
|
+
attempt: int
|
|
319
|
+
#: True when your server answered with a 2xx status.
|
|
320
|
+
delivered: bool
|
|
321
|
+
#: The HTTP status your server answered, or null when there was no answer.
|
|
322
|
+
response_code: Optional[int]
|
|
323
|
+
#: Why it was not delivered, e.g. "HTTP 500" or "No answer within 10 s.".
|
|
324
|
+
error: Optional[str]
|
|
325
|
+
#: How long the try took.
|
|
326
|
+
duration_ms: int
|
|
327
|
+
#: The event after this try: pending (automatic retries continue), succeeded, failed (no tries left, or 410), superseded.
|
|
328
|
+
status: Literal["pending", "succeeded", "failed", "superseded", "unknown"]
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class WebhookSendResult(_WebhookSendResultRequired, total=False):
|
|
332
|
+
"""What happened to one try of a webhook made on request."""
|
|
333
|
+
#: The job (resend only).
|
|
334
|
+
transcription_id: str
|
|
335
|
+
#: Where the test event went (test only).
|
|
336
|
+
url: str
|
|
337
|
+
#: When the next automatic retry is due, if any.
|
|
338
|
+
next_attempt_at: Optional[str]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
class _WebhookTranscriptionProcessingRequired(TypedDict):
|
|
342
|
+
#: The event name.
|
|
343
|
+
event: Literal["transcription.processing"]
|
|
344
|
+
#: The job this event is about.
|
|
345
|
+
transcription_id: str
|
|
346
|
+
#: The job status when the event was made.
|
|
347
|
+
status: Literal["processing"]
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class WebhookTranscriptionProcessing(_WebhookTranscriptionProcessingRequired, total=False):
|
|
351
|
+
#: Measured length of the audio.
|
|
352
|
+
duration_seconds: float
|
|
353
|
+
#: Reserved for this job, in cents; charged when it completes.
|
|
354
|
+
cost_cents: float
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class _WebhookTranscriptionCompletedRequired(TypedDict):
|
|
358
|
+
#: The event name.
|
|
359
|
+
event: Literal["transcription.completed"]
|
|
360
|
+
#: The job this event is about.
|
|
361
|
+
transcription_id: str
|
|
362
|
+
#: The job status when the event was made.
|
|
363
|
+
status: Literal["completed"]
|
|
364
|
+
#: The transcript, the same as GET /v1/transcribe/{id} returns.
|
|
365
|
+
result: TranscriptionResult
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class WebhookTranscriptionCompleted(_WebhookTranscriptionCompletedRequired, total=False):
|
|
369
|
+
#: Billed length of the audio.
|
|
370
|
+
duration_seconds: float
|
|
371
|
+
#: Charged for this job, in cents.
|
|
372
|
+
cost_cents: float
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class WebhookTranscriptionFailed(TypedDict):
|
|
376
|
+
#: The event name.
|
|
377
|
+
event: Literal["transcription.failed"]
|
|
378
|
+
#: The job this event is about.
|
|
379
|
+
transcription_id: str
|
|
380
|
+
#: The job status when the event was made.
|
|
381
|
+
status: Literal["failed"]
|
|
382
|
+
#: Why the job failed. Branch on error.code.
|
|
383
|
+
error: TranscriptionError
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class WebhookTranscriptionDownloading(TypedDict):
|
|
387
|
+
#: The event name.
|
|
388
|
+
event: Literal["transcription.downloading"]
|
|
389
|
+
#: The job this event is about.
|
|
390
|
+
transcription_id: str
|
|
391
|
+
#: The job status when the event was made.
|
|
392
|
+
status: Literal["downloading"]
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class _WebhookTestRequired(TypedDict):
|
|
396
|
+
#: The event name.
|
|
397
|
+
event: Literal["webhook.test"]
|
|
398
|
+
#: Always true: this is not about a real job.
|
|
399
|
+
test: Literal[True]
|
|
400
|
+
#: A fixed sentence saying this is a test.
|
|
401
|
+
message: str
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class WebhookTest(_WebhookTestRequired, total=False):
|
|
405
|
+
#: When the test was requested.
|
|
406
|
+
created_at: str
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class AutoTopup(TypedDict):
|
|
410
|
+
"""Auto top-up, set on the Billing page of the API dashboard. When on and available_cents falls below threshold_cents, the saved card is charged amount_cents, up to the monthly limit set there. A top-up shows on a later call, not this one."""
|
|
411
|
+
#: Whether auto top-up is on.
|
|
412
|
+
enabled: bool
|
|
413
|
+
#: Top up when available_cents falls below this, in cents.
|
|
414
|
+
threshold_cents: int
|
|
415
|
+
#: How much each top-up adds, in cents.
|
|
416
|
+
amount_cents: int
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class _BalanceRequired(TypedDict):
|
|
420
|
+
#: Total balance, in cents. Can be below zero after a refunded deposit.
|
|
421
|
+
balance_cents: float
|
|
422
|
+
#: Held against in-progress transcriptions, in cents.
|
|
423
|
+
reserved_cents: float
|
|
424
|
+
#: balance_cents minus reserved_cents. Must be above zero to start a job.
|
|
425
|
+
available_cents: float
|
|
426
|
+
#: Current price per hour of audio, in cents.
|
|
427
|
+
price_per_hour_cents: float
|
|
428
|
+
#: Auto top-up, set on the Billing page of the API dashboard. When on and available_cents falls below threshold_cents, the saved card is charged amount_cents, up to the monthly limit set there. A top-up shows on a later call, not this one.
|
|
429
|
+
auto_topup: AutoTopup
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class Balance(_BalanceRequired, total=False):
|
|
433
|
+
#: available_cents divided by price_per_hour_cents.
|
|
434
|
+
estimated_hours_available: float
|
|
435
|
+
#: Where a human can add funds. Send a user here on a 402.
|
|
436
|
+
deposit_url: str
|
|
437
|
+
#: When the balance last changed. RFC 3339, UTC.
|
|
438
|
+
updated_at: str
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
__all__ = [
|
|
442
|
+
"AutoTopup",
|
|
443
|
+
"Balance",
|
|
444
|
+
"CreateTranscriptionFromUpload",
|
|
445
|
+
"CreateTranscriptionFromUrl",
|
|
446
|
+
"CreateTranscriptionRequest",
|
|
447
|
+
"CreateUploadRequest",
|
|
448
|
+
"Error",
|
|
449
|
+
"ErrorDetail",
|
|
450
|
+
"FeedbackRequest",
|
|
451
|
+
"FeedbackResult",
|
|
452
|
+
"FeedbackResultProblemReport",
|
|
453
|
+
"ServiceDiscovery",
|
|
454
|
+
"ServiceDiscoveryEndpointsItem",
|
|
455
|
+
"Transcription",
|
|
456
|
+
"TranscriptionCancelled",
|
|
457
|
+
"TranscriptionCreated",
|
|
458
|
+
"TranscriptionError",
|
|
459
|
+
"TranscriptionPage",
|
|
460
|
+
"TranscriptionResult",
|
|
461
|
+
"TranscriptionStatus",
|
|
462
|
+
"TranscriptionSummary",
|
|
463
|
+
"Upload",
|
|
464
|
+
"Utterance",
|
|
465
|
+
"WebhookSendResult",
|
|
466
|
+
"WebhookTest",
|
|
467
|
+
"WebhookTestRequest",
|
|
468
|
+
"WebhookTranscriptionCompleted",
|
|
469
|
+
"WebhookTranscriptionDownloading",
|
|
470
|
+
"WebhookTranscriptionFailed",
|
|
471
|
+
"WebhookTranscriptionProcessing",
|
|
472
|
+
"Word",
|
|
473
|
+
]
|
scriptivox/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
scriptivox/_webhooks.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Webhook signature check. The signing formula (platform.scriptivox.com/docs/webhooks)::
|
|
2
|
+
|
|
3
|
+
signature = hex(HMAC-SHA256(secret, f"{X-Scriptivox-Timestamp}.{raw body}"))
|
|
4
|
+
|
|
5
|
+
where ``secret`` is the key's whole ``whsec_…`` string. A timestamp more than 5 minutes from now is refused.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import hmac
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Iterable, Mapping, Optional, Tuple, Union, cast
|
|
14
|
+
|
|
15
|
+
from ._exceptions import WebhookVerificationError
|
|
16
|
+
from ._types import WebhookTest, WebhookTranscriptionCompleted, WebhookTranscriptionDownloading, WebhookTranscriptionFailed, WebhookTranscriptionProcessing
|
|
17
|
+
|
|
18
|
+
SIGNATURE_HEADER = "X-Scriptivox-Signature"
|
|
19
|
+
TIMESTAMP_HEADER = "X-Scriptivox-Timestamp"
|
|
20
|
+
DELIVERY_HEADER = "X-Scriptivox-Delivery"
|
|
21
|
+
ATTEMPT_HEADER = "X-Scriptivox-Attempt"
|
|
22
|
+
#: Seconds a webhook's timestamp may be away from now.
|
|
23
|
+
DEFAULT_TOLERANCE = 300
|
|
24
|
+
|
|
25
|
+
#: Every event body a webhook can carry; switch on ``event``. Answer 2xx to event types you do not know.
|
|
26
|
+
WebhookEvent = Union[
|
|
27
|
+
WebhookTranscriptionProcessing,
|
|
28
|
+
WebhookTranscriptionCompleted,
|
|
29
|
+
WebhookTranscriptionFailed,
|
|
30
|
+
WebhookTranscriptionDownloading,
|
|
31
|
+
WebhookTest,
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
HeadersLike = Union[Mapping[str, Any], Iterable[Tuple[str, str]]]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_header(headers: HeadersLike, name: str) -> Optional[str]:
|
|
38
|
+
"""Case-insensitive lookup in a dict, httpx/Starlette/Django/Flask headers, or a list of pairs."""
|
|
39
|
+
wanted = name.lower()
|
|
40
|
+
items = headers.items() if hasattr(headers, "items") else headers
|
|
41
|
+
for key, value in cast(Iterable[Tuple[str, Any]], items):
|
|
42
|
+
if isinstance(key, bytes):
|
|
43
|
+
key = key.decode("latin-1")
|
|
44
|
+
if key.lower() != wanted or value is None:
|
|
45
|
+
continue
|
|
46
|
+
if isinstance(value, (list, tuple)):
|
|
47
|
+
value = value[0] if value else None
|
|
48
|
+
if isinstance(value, bytes):
|
|
49
|
+
value = value.decode("latin-1")
|
|
50
|
+
return None if value is None else str(value)
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _body_bytes(raw_body: Union[bytes, bytearray, memoryview, str]) -> bytes:
|
|
55
|
+
if isinstance(raw_body, str):
|
|
56
|
+
return raw_body.encode("utf-8")
|
|
57
|
+
if isinstance(raw_body, (bytes, bytearray, memoryview)):
|
|
58
|
+
return bytes(raw_body)
|
|
59
|
+
raise TypeError(
|
|
60
|
+
"verify_webhook needs the raw request body (bytes or str), not parsed JSON: "
|
|
61
|
+
"re-serialising changes the bytes and the signature."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compute_signature(raw_body: Union[bytes, bytearray, memoryview, str], timestamp: str, secret: str) -> str:
|
|
66
|
+
"""The signature the API would send for this body and timestamp."""
|
|
67
|
+
mac = hmac.new(secret.encode("utf-8"), f"{timestamp}.".encode("utf-8"), hashlib.sha256)
|
|
68
|
+
mac.update(_body_bytes(raw_body))
|
|
69
|
+
return mac.hexdigest()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def verify_webhook(
|
|
73
|
+
raw_body: Union[bytes, bytearray, memoryview, str],
|
|
74
|
+
headers: HeadersLike,
|
|
75
|
+
secret: str,
|
|
76
|
+
*,
|
|
77
|
+
tolerance: int = DEFAULT_TOLERANCE,
|
|
78
|
+
now: Optional[float] = None,
|
|
79
|
+
) -> WebhookEvent:
|
|
80
|
+
"""Check that a webhook request came from Scriptivox and is fresh, then return its parsed event.
|
|
81
|
+
|
|
82
|
+
Pass the body exactly as received (``await request.body()``, ``request.get_data()``,
|
|
83
|
+
``request.body``), never re-serialised JSON. Raises WebhookVerificationError; answer such a
|
|
84
|
+
request with 400. Deduplicate on the X-Scriptivox-Delivery header: an event can arrive twice.
|
|
85
|
+
"""
|
|
86
|
+
body = _body_bytes(raw_body)
|
|
87
|
+
if not isinstance(secret, str) or not secret:
|
|
88
|
+
raise TypeError("verify_webhook needs the key's webhook signing secret (whsec_…).")
|
|
89
|
+
signature = read_header(headers, SIGNATURE_HEADER)
|
|
90
|
+
timestamp = read_header(headers, TIMESTAMP_HEADER)
|
|
91
|
+
if not signature or not timestamp:
|
|
92
|
+
raise WebhookVerificationError("missing_header", "Missing X-Scriptivox-Signature or X-Scriptivox-Timestamp header.")
|
|
93
|
+
timestamp = timestamp.strip()
|
|
94
|
+
if not timestamp.isdigit():
|
|
95
|
+
raise WebhookVerificationError("invalid_timestamp", "X-Scriptivox-Timestamp is not a Unix time in seconds.")
|
|
96
|
+
current = time.time() if now is None else now
|
|
97
|
+
if abs(int(current) - int(timestamp)) > tolerance:
|
|
98
|
+
raise WebhookVerificationError(
|
|
99
|
+
"timestamp_outside_tolerance",
|
|
100
|
+
f"The webhook's timestamp is more than {tolerance} s from now: refused as a possible replay.",
|
|
101
|
+
)
|
|
102
|
+
expected = compute_signature(body, timestamp, secret)
|
|
103
|
+
if not hmac.compare_digest(expected.encode("ascii"), signature.strip().lower().encode("utf-8")):
|
|
104
|
+
raise WebhookVerificationError("signature_mismatch", "The webhook signature does not match: wrong secret, or the body was changed.")
|
|
105
|
+
try:
|
|
106
|
+
return cast(WebhookEvent, json.loads(body.decode("utf-8")))
|
|
107
|
+
except (ValueError, UnicodeDecodeError):
|
|
108
|
+
raise WebhookVerificationError("invalid_json", "The webhook body is not JSON.") from None
|
scriptivox/py.typed
ADDED
|
File without changes
|