tldw-client 0.2.6__tar.gz
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.
- tldw_client-0.2.6/LICENSE +21 -0
- tldw_client-0.2.6/PKG-INFO +265 -0
- tldw_client-0.2.6/README.md +243 -0
- tldw_client-0.2.6/build_wheel.py +170 -0
- tldw_client-0.2.6/pyproject.toml +35 -0
- tldw_client-0.2.6/tests/test_client.py +455 -0
- tldw_client-0.2.6/tests/test_packaging.py +112 -0
- tldw_client-0.2.6/tests/test_source_refresh.py +62 -0
- tldw_client-0.2.6/tests/test_spending.py +85 -0
- tldw_client-0.2.6/tests/test_transcript_catalog.py +135 -0
- tldw_client-0.2.6/tests/test_transcript_extraction.py +174 -0
- tldw_client-0.2.6/tests/test_transport.py +262 -0
- tldw_client-0.2.6/tldw_client/__init__.py +1221 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tldw.sh contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tldw-client
|
|
3
|
+
Version: 0.2.6
|
|
4
|
+
Summary: Dependency-free tldw.sh asynchronous analysis client
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: tldw,video,analysis,agents,mcp,api
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Project-URL: Homepage, https://tldw.sh
|
|
20
|
+
Project-URL: Documentation, https://tldw.sh/docs
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# tldw.sh Python client
|
|
24
|
+
|
|
25
|
+
A synchronous Python 3.10+ client with no runtime dependencies.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
python -m pip install tldw-client
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can instead install the versioned wheel from your operator's `/agents` page.
|
|
32
|
+
Download it, verify it against the SHA-256 checksum shown on that page, then
|
|
33
|
+
install the verified local file:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
curl -fsSLo tldw_client-0.2.6-py3-none-any.whl \
|
|
37
|
+
'https://YOUR_APP_ORIGIN/downloads/tldw_client-0.2.6-py3-none-any.whl' &&
|
|
38
|
+
echo 'SHA256_FROM_YOUR_AGENTS_PAGE tldw_client-0.2.6-py3-none-any.whl' | shasum -a 256 -c - &&
|
|
39
|
+
python -m pip install ./tldw_client-0.2.6-py3-none-any.whl
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package is imported as `tldw_client`. Only `tldw-client` on PyPI and your
|
|
43
|
+
operator's verified download are this client; similarly named projects are not.
|
|
44
|
+
|
|
45
|
+
Use your deployment's API origin and a scoped **tldw API key**, never a model
|
|
46
|
+
provider key. Signup and one-time human verification happen in the web app.
|
|
47
|
+
REST, MCP and plugins share 100 monthly Free caption/text credits, Hobby/Pro
|
|
48
|
+
limits and purchased credits. Polling does not create another analysis or reset your allowance.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import os
|
|
52
|
+
from tldw_client import TldwClient, TldwTimeoutError
|
|
53
|
+
|
|
54
|
+
client = TldwClient(
|
|
55
|
+
api_url=os.environ["TLDW_API_URL"], token=os.environ["TLDW_API_TOKEN"]
|
|
56
|
+
)
|
|
57
|
+
receipt = client.submit_analysis(
|
|
58
|
+
"https://youtu.be/VIDEO_ID",
|
|
59
|
+
instructions="Extract the argument and flag weak evidence.",
|
|
60
|
+
idempotency_key="my-persisted-request-001",
|
|
61
|
+
)
|
|
62
|
+
print({"job_id": receipt["job_id"]}) # Persist before waiting.
|
|
63
|
+
try:
|
|
64
|
+
job = client.wait_for_job(receipt["job_id"], timeout_seconds=180)
|
|
65
|
+
result = client.get_summary(job["summary_id"])
|
|
66
|
+
print(result["content"])
|
|
67
|
+
except TldwTimeoutError as error:
|
|
68
|
+
print({"job_id": error.job_id, "status": "resume-polling-later"})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`analyze(media, instructions=..., idempotency_key=..., timeout_seconds=180)`
|
|
72
|
+
combines submission, polling and result retrieval, returning a dictionary with
|
|
73
|
+
`receipt`, `job` and `summary`. A failure after acceptance retains `job_id` and
|
|
74
|
+
`receipt`; unknown submission errors retain `idempotency_key`. Reuse that key
|
|
75
|
+
with exactly the same source/instructions after an uncertain network outcome.
|
|
76
|
+
|
|
77
|
+
Other methods:
|
|
78
|
+
|
|
79
|
+
- `get_job(job_id)`, `get_summary(summary_id)`, `get_usage()` (`account:read`).
|
|
80
|
+
- `compare_tiktoks(urls, instructions=..., idempotency_key=...)` returns an
|
|
81
|
+
accepted comparison view. Persist its `id` before waiting.
|
|
82
|
+
- `get_comparison(id)`, `wait_for_comparison(id, timeout_seconds=180)` return
|
|
83
|
+
the unwrapped comparison view. Completed output needs `summaries:read`.
|
|
84
|
+
|
|
85
|
+
Polling accepts `on_progress(resource)` and `poll_interval_seconds` (default
|
|
86
|
+
1.5, minimum 0.25). It honors larger server polling hints and Retry-After.
|
|
87
|
+
Waits have a finite one-hour maximum; each HTTP operation has a separate
|
|
88
|
+
30-second budget, configurable through `request_timeout_seconds` (maximum 120).
|
|
89
|
+
Pass a `threading.Event` as `cancel_event` to cooperatively stop waiting. The
|
|
90
|
+
current bounded HTTP read can finish before cancellation is observed. Stopping
|
|
91
|
+
a wait never sends a job cancellation; accepted work remains on the server.
|
|
92
|
+
|
|
93
|
+
`TldwError` exposes `code`, `status`, `request_id`, `retry_after_seconds` and
|
|
94
|
+
durable IDs where known. `TldwJobError` marks failed/canceled work. The SDK never
|
|
95
|
+
follows server-supplied URLs or redirects, ignores environment proxies, validates
|
|
96
|
+
resource UUIDs and limits API responses to 8 MiB. Transient retries preserve the
|
|
97
|
+
same serialized body and idempotency key. Scope, signup and trial denials require
|
|
98
|
+
caller action and are not automatically resubmitted.
|
|
99
|
+
|
|
100
|
+
Credentials must come from your secret manager or private environment, never a
|
|
101
|
+
prompt, checked-in file or command argument. The client does not print keys.
|
|
102
|
+
Anonymous mode requires explicit loopback development access:
|
|
103
|
+
`TldwClient(api_url="http://localhost:8000", allow_anonymous=True)`. It grants no
|
|
104
|
+
admin access. Production uses HTTPS and a scoped key.
|
|
105
|
+
|
|
106
|
+
For maintainers, `python build_wheel.py dist` creates the wheel offline using
|
|
107
|
+
only the standard library. The PEP 517 backend supports both source distributions
|
|
108
|
+
and wheels with no build dependencies. A standard build frontend can run
|
|
109
|
+
`python -m build` to build the source archive and rebuild its wheel in isolation.
|
|
110
|
+
Run `python -m unittest discover -s tests` from this source directory to check
|
|
111
|
+
transport, polling, billing-safe retries and packaging. Releases are published
|
|
112
|
+
from `main` by the monorepo's manual release workflow through PyPI trusted
|
|
113
|
+
publishing; a successful local build publishes nothing.
|
|
114
|
+
|
|
115
|
+
## Extract original source without generated analysis
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
receipt = client.submit_transcript(
|
|
119
|
+
"https://youtu.be/VIDEO_ID",
|
|
120
|
+
transcription="never",
|
|
121
|
+
max_credits="30.00",
|
|
122
|
+
idempotency_key="original-source-001",
|
|
123
|
+
)
|
|
124
|
+
print(receipt["job_id"]) # Persist before waiting.
|
|
125
|
+
job = client.wait_for_job(receipt["job_id"])
|
|
126
|
+
original = client.get_saved_transcript_page(job["transcript_id"], limit=100)
|
|
127
|
+
# Follow original["next_offset"] for more source content.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`submit_transcript` posts to `POST /v1/transcripts` and always returns a durable
|
|
131
|
+
accepted receipt. It never requests a summary or invokes an LLM. Completed
|
|
132
|
+
transcript jobs have `output_mode="transcript"`, a `transcript_id`, and no
|
|
133
|
+
`summary_id`. `extract_transcript(media, ...)` combines submission, polling and
|
|
134
|
+
a full source read, returning `receipt`, `job` and `transcript`; timeout and
|
|
135
|
+
source-permission errors retain the accepted receipt and job ID.
|
|
136
|
+
|
|
137
|
+
Use `submit_transcript(upload_id=...)` for your parsed subtitle upload.
|
|
138
|
+
`preferred_caption_languages` selects original captions. Spending/ASR controls
|
|
139
|
+
and `refresh_source` are supported, but analysis instructions are not.
|
|
140
|
+
Submission needs `jobs:write`, polling `jobs:read`, and source reads
|
|
141
|
+
`summaries:read`; no account-read scope is needed. New transcript-only jobs
|
|
142
|
+
retain normal credit rates and ASR consent/caps. If you already have a saved
|
|
143
|
+
transcript ID, read it directly: saved-source reads start no processing job,
|
|
144
|
+
invoke no LLM and charge no processing credits.
|
|
145
|
+
|
|
146
|
+
## Summary and transcript are separate reads
|
|
147
|
+
|
|
148
|
+
After the job completes, use its `summary_id` to retrieve generated output and
|
|
149
|
+
original source independently. Both calls require `summaries:read` and account
|
|
150
|
+
ownership. Neither starts another analysis, transcription or processing-credit
|
|
151
|
+
charge. The summary response includes transcript metadata, not the full source.
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
summary = client.get_summary(job["summary_id"])
|
|
155
|
+
transcript = client.get_transcript(job["summary_id"])
|
|
156
|
+
print(summary["content"]["answer"])
|
|
157
|
+
original_text = (
|
|
158
|
+
"\n".join(segment["text"] for segment in transcript["segments"])
|
|
159
|
+
if transcript["timed"]
|
|
160
|
+
else transcript["text"]
|
|
161
|
+
)
|
|
162
|
+
print(original_text)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
REST uses `GET /v1/summaries/{id}` and `GET /v1/summaries/{id}/transcript`.
|
|
166
|
+
MCP uses `get_summary` and paginated `get_transcript`. Timed YouTube/TikTok
|
|
167
|
+
sources expose `segments`; text/X sources expose `blocks` and `text`. X video
|
|
168
|
+
caption blocks retain `start_s` / `end_s` alongside separate untimed post context.
|
|
169
|
+
The saved source stays in its original language when requesting a translated summary.
|
|
170
|
+
|
|
171
|
+
## Saved transcription inbox
|
|
172
|
+
|
|
173
|
+
`list_transcripts` returns the latest 50 distinct sources requested by your account,
|
|
174
|
+
including sources saved before an analysis failed. It reads metadata only and does
|
|
175
|
+
not mark anything read. All library, source and read-state calls require `summaries:read`.
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
page = client.list_transcripts(limit=50, unread_only=True)
|
|
179
|
+
for item in page["items"]:
|
|
180
|
+
print(item["id"], item["video"]["title"], item["last_requested_at"])
|
|
181
|
+
source = client.get_saved_transcript_page(item["id"], offset=0, limit=100)
|
|
182
|
+
# Follow source["next_offset"] for additional content. Mark only when ready.
|
|
183
|
+
client.mark_transcript_read(item["id"], read=True)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Pass `cursor=page["next_cursor"]` with the same filters for older entries. `since`
|
|
187
|
+
is inclusive and `until` exclusive, using ISO 8601 timestamps against
|
|
188
|
+
`last_requested_at`. `read=False` marks a source unread again. Reading source
|
|
189
|
+
content through REST/MCP/SDK never implicitly changes its read state.
|
|
190
|
+
Use `get_saved_transcript_metadata(id)` to read one source’s title, dates and
|
|
191
|
+
unread status directly, including sources outside the current catalog page. This
|
|
192
|
+
GET does not load the transcript body or start a processing job.
|
|
193
|
+
|
|
194
|
+
X posts, X video captions and articles reuse an owned source snapshot for 24 hours.
|
|
195
|
+
Use `submit_analysis(..., refresh_source=True)` to retrieve current source content.
|
|
196
|
+
YouTube/TikTok keep reusing saved transcripts; another brief does not redownload
|
|
197
|
+
audio or repeat transcription when a usable source already exists.
|
|
198
|
+
|
|
199
|
+
## X video captions and output language
|
|
200
|
+
|
|
201
|
+
Pass an X post URL directly. Default `scope=auto` detects one attached video and
|
|
202
|
+
analyzes its available captions plus separately labeled post context when the
|
|
203
|
+
deployment enables X video extraction. You do not need a subtitle URL or a
|
|
204
|
+
separate provider key. Use `{"type":"x","url":"YOUR_X_POST_URL","scope":"post"}`
|
|
205
|
+
only for post text alone; thread scope does not recursively analyze its videos.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
receipt = client.submit_analysis(
|
|
209
|
+
"https://x.com/hamptonism/status/2071631335836225751",
|
|
210
|
+
instructions="Summarize the spoken content in Spanish. Keep it concise and avoid repetition.",
|
|
211
|
+
transcription="never",
|
|
212
|
+
idempotency_key="x-video-spanish-001",
|
|
213
|
+
)
|
|
214
|
+
print({"job_id": receipt["job_id"]}) # Persist before waiting.
|
|
215
|
+
job = client.wait_for_job(
|
|
216
|
+
receipt["job_id"],
|
|
217
|
+
on_progress=lambda state: print(
|
|
218
|
+
{"status": state["status"], "progress": state["progress"]}
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
result = client.get_summary(job["summary_id"])
|
|
222
|
+
print(result["content"]["answer"])
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The response is a durable job receipt. Save its ID before waiting; a timeout does
|
|
226
|
+
not cancel the job. Use a fresh stable key for a different brief or language and
|
|
227
|
+
reuse the exact body/key after an uncertain submission. Without instructions,
|
|
228
|
+
the service produces a concise summary. A translated summary is generated text
|
|
229
|
+
inside the same schema-3 result, not dubbed audio, a translated video or an SRT
|
|
230
|
+
translation; the original captions remain saved separately.
|
|
231
|
+
|
|
232
|
+
Captioned X videos use actual video duration at 1 credit/minute under normal
|
|
233
|
+
rounding (128.197 seconds = 2.2 credits), not the post's reading time. Missing
|
|
234
|
+
captions fail explicitly with no customer processing charge; X audio
|
|
235
|
+
transcription and visual analysis are not enabled, even with `if_needed`.
|
|
236
|
+
|
|
237
|
+
## Credit controls
|
|
238
|
+
|
|
239
|
+
Captions, text and existing cached transcripts cost **1 credit per source minute**.
|
|
240
|
+
Fresh audio transcription costs **3 credits per source minute** and requires an
|
|
241
|
+
eligible paid plan or server-confirmed billing exemption, plus explicit `transcription: "if_needed"` (Python:
|
|
242
|
+
`transcription="if_needed"`). The default `never` fails with a clear error when
|
|
243
|
+
new transcription would be necessary. Supplying `forceAsr: true` /
|
|
244
|
+
`force_asr=True` also requires that explicit permission.
|
|
245
|
+
|
|
246
|
+
Use `maxCredits: "30.00"` in JavaScript or `max_credits="30.00"` in Python to cap
|
|
247
|
+
a job. The server validates affordability before new transcription. The cap is
|
|
248
|
+
positive, at most 1,000,000 credits, and has at most two decimal places. It is
|
|
249
|
+
required for ordinary paid accounts when `transcription` is `if_needed`. Preserve
|
|
250
|
+
these controls in the same idempotent request body on retries. Jobs expose
|
|
251
|
+
`credits.estimated`, `reserved`, `final`, the billing class/rate and real source
|
|
252
|
+
duration; polling does not create another processing charge.
|
|
253
|
+
|
|
254
|
+
For `compareTikToks` / `compare_tiktoks`, the cap covers all distinct supplied
|
|
255
|
+
clips: each receives an equal share rounded down to 0.01 credit. Unused shares
|
|
256
|
+
are not redistributed. Comparison aggregation adds no customer processing credits.
|
|
257
|
+
Source minutes round up to 0.1 minute with a 0.5-minute minimum before the rate
|
|
258
|
+
multiplier. Raw text uses estimated reading minutes.
|
|
259
|
+
|
|
260
|
+
Free includes 100 credits per month for captions/text, Hobby costs USD20/month
|
|
261
|
+
with 1,000 credits, and Pro costs USD99/month with 5,000 credits. Included
|
|
262
|
+
allowance is used first, then purchased credits, which do not expire. Reading
|
|
263
|
+
account usage does not buy credits or change plans.
|
|
264
|
+
|
|
265
|
+
Server-confirmed billing exemption: the authenticated server decides whether an account is exempt. Exempt accounts may omit a credit ceiling and do not incur customer-credit charges; explicit transcription permission and resource/provider limits still apply. Ordinary paid accounts allowing fresh transcription must supply a ceiling or receive `TRANSCRIPTION_NOT_ALLOWED` (HTTP 422); `MAX_CREDITS_EXCEEDED` (HTTP 422) means a supplied ceiling is invalid or below the required charge. Clients validate supplied values, then pass requests through without fetching extra account scopes or inferring privileges from names, roles, or key prefixes.
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# tldw.sh Python client
|
|
2
|
+
|
|
3
|
+
A synchronous Python 3.10+ client with no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
python -m pip install tldw-client
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
You can instead install the versioned wheel from your operator's `/agents` page.
|
|
10
|
+
Download it, verify it against the SHA-256 checksum shown on that page, then
|
|
11
|
+
install the verified local file:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
curl -fsSLo tldw_client-0.2.6-py3-none-any.whl \
|
|
15
|
+
'https://YOUR_APP_ORIGIN/downloads/tldw_client-0.2.6-py3-none-any.whl' &&
|
|
16
|
+
echo 'SHA256_FROM_YOUR_AGENTS_PAGE tldw_client-0.2.6-py3-none-any.whl' | shasum -a 256 -c - &&
|
|
17
|
+
python -m pip install ./tldw_client-0.2.6-py3-none-any.whl
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The package is imported as `tldw_client`. Only `tldw-client` on PyPI and your
|
|
21
|
+
operator's verified download are this client; similarly named projects are not.
|
|
22
|
+
|
|
23
|
+
Use your deployment's API origin and a scoped **tldw API key**, never a model
|
|
24
|
+
provider key. Signup and one-time human verification happen in the web app.
|
|
25
|
+
REST, MCP and plugins share 100 monthly Free caption/text credits, Hobby/Pro
|
|
26
|
+
limits and purchased credits. Polling does not create another analysis or reset your allowance.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import os
|
|
30
|
+
from tldw_client import TldwClient, TldwTimeoutError
|
|
31
|
+
|
|
32
|
+
client = TldwClient(
|
|
33
|
+
api_url=os.environ["TLDW_API_URL"], token=os.environ["TLDW_API_TOKEN"]
|
|
34
|
+
)
|
|
35
|
+
receipt = client.submit_analysis(
|
|
36
|
+
"https://youtu.be/VIDEO_ID",
|
|
37
|
+
instructions="Extract the argument and flag weak evidence.",
|
|
38
|
+
idempotency_key="my-persisted-request-001",
|
|
39
|
+
)
|
|
40
|
+
print({"job_id": receipt["job_id"]}) # Persist before waiting.
|
|
41
|
+
try:
|
|
42
|
+
job = client.wait_for_job(receipt["job_id"], timeout_seconds=180)
|
|
43
|
+
result = client.get_summary(job["summary_id"])
|
|
44
|
+
print(result["content"])
|
|
45
|
+
except TldwTimeoutError as error:
|
|
46
|
+
print({"job_id": error.job_id, "status": "resume-polling-later"})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`analyze(media, instructions=..., idempotency_key=..., timeout_seconds=180)`
|
|
50
|
+
combines submission, polling and result retrieval, returning a dictionary with
|
|
51
|
+
`receipt`, `job` and `summary`. A failure after acceptance retains `job_id` and
|
|
52
|
+
`receipt`; unknown submission errors retain `idempotency_key`. Reuse that key
|
|
53
|
+
with exactly the same source/instructions after an uncertain network outcome.
|
|
54
|
+
|
|
55
|
+
Other methods:
|
|
56
|
+
|
|
57
|
+
- `get_job(job_id)`, `get_summary(summary_id)`, `get_usage()` (`account:read`).
|
|
58
|
+
- `compare_tiktoks(urls, instructions=..., idempotency_key=...)` returns an
|
|
59
|
+
accepted comparison view. Persist its `id` before waiting.
|
|
60
|
+
- `get_comparison(id)`, `wait_for_comparison(id, timeout_seconds=180)` return
|
|
61
|
+
the unwrapped comparison view. Completed output needs `summaries:read`.
|
|
62
|
+
|
|
63
|
+
Polling accepts `on_progress(resource)` and `poll_interval_seconds` (default
|
|
64
|
+
1.5, minimum 0.25). It honors larger server polling hints and Retry-After.
|
|
65
|
+
Waits have a finite one-hour maximum; each HTTP operation has a separate
|
|
66
|
+
30-second budget, configurable through `request_timeout_seconds` (maximum 120).
|
|
67
|
+
Pass a `threading.Event` as `cancel_event` to cooperatively stop waiting. The
|
|
68
|
+
current bounded HTTP read can finish before cancellation is observed. Stopping
|
|
69
|
+
a wait never sends a job cancellation; accepted work remains on the server.
|
|
70
|
+
|
|
71
|
+
`TldwError` exposes `code`, `status`, `request_id`, `retry_after_seconds` and
|
|
72
|
+
durable IDs where known. `TldwJobError` marks failed/canceled work. The SDK never
|
|
73
|
+
follows server-supplied URLs or redirects, ignores environment proxies, validates
|
|
74
|
+
resource UUIDs and limits API responses to 8 MiB. Transient retries preserve the
|
|
75
|
+
same serialized body and idempotency key. Scope, signup and trial denials require
|
|
76
|
+
caller action and are not automatically resubmitted.
|
|
77
|
+
|
|
78
|
+
Credentials must come from your secret manager or private environment, never a
|
|
79
|
+
prompt, checked-in file or command argument. The client does not print keys.
|
|
80
|
+
Anonymous mode requires explicit loopback development access:
|
|
81
|
+
`TldwClient(api_url="http://localhost:8000", allow_anonymous=True)`. It grants no
|
|
82
|
+
admin access. Production uses HTTPS and a scoped key.
|
|
83
|
+
|
|
84
|
+
For maintainers, `python build_wheel.py dist` creates the wheel offline using
|
|
85
|
+
only the standard library. The PEP 517 backend supports both source distributions
|
|
86
|
+
and wheels with no build dependencies. A standard build frontend can run
|
|
87
|
+
`python -m build` to build the source archive and rebuild its wheel in isolation.
|
|
88
|
+
Run `python -m unittest discover -s tests` from this source directory to check
|
|
89
|
+
transport, polling, billing-safe retries and packaging. Releases are published
|
|
90
|
+
from `main` by the monorepo's manual release workflow through PyPI trusted
|
|
91
|
+
publishing; a successful local build publishes nothing.
|
|
92
|
+
|
|
93
|
+
## Extract original source without generated analysis
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
receipt = client.submit_transcript(
|
|
97
|
+
"https://youtu.be/VIDEO_ID",
|
|
98
|
+
transcription="never",
|
|
99
|
+
max_credits="30.00",
|
|
100
|
+
idempotency_key="original-source-001",
|
|
101
|
+
)
|
|
102
|
+
print(receipt["job_id"]) # Persist before waiting.
|
|
103
|
+
job = client.wait_for_job(receipt["job_id"])
|
|
104
|
+
original = client.get_saved_transcript_page(job["transcript_id"], limit=100)
|
|
105
|
+
# Follow original["next_offset"] for more source content.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`submit_transcript` posts to `POST /v1/transcripts` and always returns a durable
|
|
109
|
+
accepted receipt. It never requests a summary or invokes an LLM. Completed
|
|
110
|
+
transcript jobs have `output_mode="transcript"`, a `transcript_id`, and no
|
|
111
|
+
`summary_id`. `extract_transcript(media, ...)` combines submission, polling and
|
|
112
|
+
a full source read, returning `receipt`, `job` and `transcript`; timeout and
|
|
113
|
+
source-permission errors retain the accepted receipt and job ID.
|
|
114
|
+
|
|
115
|
+
Use `submit_transcript(upload_id=...)` for your parsed subtitle upload.
|
|
116
|
+
`preferred_caption_languages` selects original captions. Spending/ASR controls
|
|
117
|
+
and `refresh_source` are supported, but analysis instructions are not.
|
|
118
|
+
Submission needs `jobs:write`, polling `jobs:read`, and source reads
|
|
119
|
+
`summaries:read`; no account-read scope is needed. New transcript-only jobs
|
|
120
|
+
retain normal credit rates and ASR consent/caps. If you already have a saved
|
|
121
|
+
transcript ID, read it directly: saved-source reads start no processing job,
|
|
122
|
+
invoke no LLM and charge no processing credits.
|
|
123
|
+
|
|
124
|
+
## Summary and transcript are separate reads
|
|
125
|
+
|
|
126
|
+
After the job completes, use its `summary_id` to retrieve generated output and
|
|
127
|
+
original source independently. Both calls require `summaries:read` and account
|
|
128
|
+
ownership. Neither starts another analysis, transcription or processing-credit
|
|
129
|
+
charge. The summary response includes transcript metadata, not the full source.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
summary = client.get_summary(job["summary_id"])
|
|
133
|
+
transcript = client.get_transcript(job["summary_id"])
|
|
134
|
+
print(summary["content"]["answer"])
|
|
135
|
+
original_text = (
|
|
136
|
+
"\n".join(segment["text"] for segment in transcript["segments"])
|
|
137
|
+
if transcript["timed"]
|
|
138
|
+
else transcript["text"]
|
|
139
|
+
)
|
|
140
|
+
print(original_text)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
REST uses `GET /v1/summaries/{id}` and `GET /v1/summaries/{id}/transcript`.
|
|
144
|
+
MCP uses `get_summary` and paginated `get_transcript`. Timed YouTube/TikTok
|
|
145
|
+
sources expose `segments`; text/X sources expose `blocks` and `text`. X video
|
|
146
|
+
caption blocks retain `start_s` / `end_s` alongside separate untimed post context.
|
|
147
|
+
The saved source stays in its original language when requesting a translated summary.
|
|
148
|
+
|
|
149
|
+
## Saved transcription inbox
|
|
150
|
+
|
|
151
|
+
`list_transcripts` returns the latest 50 distinct sources requested by your account,
|
|
152
|
+
including sources saved before an analysis failed. It reads metadata only and does
|
|
153
|
+
not mark anything read. All library, source and read-state calls require `summaries:read`.
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
page = client.list_transcripts(limit=50, unread_only=True)
|
|
157
|
+
for item in page["items"]:
|
|
158
|
+
print(item["id"], item["video"]["title"], item["last_requested_at"])
|
|
159
|
+
source = client.get_saved_transcript_page(item["id"], offset=0, limit=100)
|
|
160
|
+
# Follow source["next_offset"] for additional content. Mark only when ready.
|
|
161
|
+
client.mark_transcript_read(item["id"], read=True)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Pass `cursor=page["next_cursor"]` with the same filters for older entries. `since`
|
|
165
|
+
is inclusive and `until` exclusive, using ISO 8601 timestamps against
|
|
166
|
+
`last_requested_at`. `read=False` marks a source unread again. Reading source
|
|
167
|
+
content through REST/MCP/SDK never implicitly changes its read state.
|
|
168
|
+
Use `get_saved_transcript_metadata(id)` to read one source’s title, dates and
|
|
169
|
+
unread status directly, including sources outside the current catalog page. This
|
|
170
|
+
GET does not load the transcript body or start a processing job.
|
|
171
|
+
|
|
172
|
+
X posts, X video captions and articles reuse an owned source snapshot for 24 hours.
|
|
173
|
+
Use `submit_analysis(..., refresh_source=True)` to retrieve current source content.
|
|
174
|
+
YouTube/TikTok keep reusing saved transcripts; another brief does not redownload
|
|
175
|
+
audio or repeat transcription when a usable source already exists.
|
|
176
|
+
|
|
177
|
+
## X video captions and output language
|
|
178
|
+
|
|
179
|
+
Pass an X post URL directly. Default `scope=auto` detects one attached video and
|
|
180
|
+
analyzes its available captions plus separately labeled post context when the
|
|
181
|
+
deployment enables X video extraction. You do not need a subtitle URL or a
|
|
182
|
+
separate provider key. Use `{"type":"x","url":"YOUR_X_POST_URL","scope":"post"}`
|
|
183
|
+
only for post text alone; thread scope does not recursively analyze its videos.
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
receipt = client.submit_analysis(
|
|
187
|
+
"https://x.com/hamptonism/status/2071631335836225751",
|
|
188
|
+
instructions="Summarize the spoken content in Spanish. Keep it concise and avoid repetition.",
|
|
189
|
+
transcription="never",
|
|
190
|
+
idempotency_key="x-video-spanish-001",
|
|
191
|
+
)
|
|
192
|
+
print({"job_id": receipt["job_id"]}) # Persist before waiting.
|
|
193
|
+
job = client.wait_for_job(
|
|
194
|
+
receipt["job_id"],
|
|
195
|
+
on_progress=lambda state: print(
|
|
196
|
+
{"status": state["status"], "progress": state["progress"]}
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
result = client.get_summary(job["summary_id"])
|
|
200
|
+
print(result["content"]["answer"])
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The response is a durable job receipt. Save its ID before waiting; a timeout does
|
|
204
|
+
not cancel the job. Use a fresh stable key for a different brief or language and
|
|
205
|
+
reuse the exact body/key after an uncertain submission. Without instructions,
|
|
206
|
+
the service produces a concise summary. A translated summary is generated text
|
|
207
|
+
inside the same schema-3 result, not dubbed audio, a translated video or an SRT
|
|
208
|
+
translation; the original captions remain saved separately.
|
|
209
|
+
|
|
210
|
+
Captioned X videos use actual video duration at 1 credit/minute under normal
|
|
211
|
+
rounding (128.197 seconds = 2.2 credits), not the post's reading time. Missing
|
|
212
|
+
captions fail explicitly with no customer processing charge; X audio
|
|
213
|
+
transcription and visual analysis are not enabled, even with `if_needed`.
|
|
214
|
+
|
|
215
|
+
## Credit controls
|
|
216
|
+
|
|
217
|
+
Captions, text and existing cached transcripts cost **1 credit per source minute**.
|
|
218
|
+
Fresh audio transcription costs **3 credits per source minute** and requires an
|
|
219
|
+
eligible paid plan or server-confirmed billing exemption, plus explicit `transcription: "if_needed"` (Python:
|
|
220
|
+
`transcription="if_needed"`). The default `never` fails with a clear error when
|
|
221
|
+
new transcription would be necessary. Supplying `forceAsr: true` /
|
|
222
|
+
`force_asr=True` also requires that explicit permission.
|
|
223
|
+
|
|
224
|
+
Use `maxCredits: "30.00"` in JavaScript or `max_credits="30.00"` in Python to cap
|
|
225
|
+
a job. The server validates affordability before new transcription. The cap is
|
|
226
|
+
positive, at most 1,000,000 credits, and has at most two decimal places. It is
|
|
227
|
+
required for ordinary paid accounts when `transcription` is `if_needed`. Preserve
|
|
228
|
+
these controls in the same idempotent request body on retries. Jobs expose
|
|
229
|
+
`credits.estimated`, `reserved`, `final`, the billing class/rate and real source
|
|
230
|
+
duration; polling does not create another processing charge.
|
|
231
|
+
|
|
232
|
+
For `compareTikToks` / `compare_tiktoks`, the cap covers all distinct supplied
|
|
233
|
+
clips: each receives an equal share rounded down to 0.01 credit. Unused shares
|
|
234
|
+
are not redistributed. Comparison aggregation adds no customer processing credits.
|
|
235
|
+
Source minutes round up to 0.1 minute with a 0.5-minute minimum before the rate
|
|
236
|
+
multiplier. Raw text uses estimated reading minutes.
|
|
237
|
+
|
|
238
|
+
Free includes 100 credits per month for captions/text, Hobby costs USD20/month
|
|
239
|
+
with 1,000 credits, and Pro costs USD99/month with 5,000 credits. Included
|
|
240
|
+
allowance is used first, then purchased credits, which do not expire. Reading
|
|
241
|
+
account usage does not buy credits or change plans.
|
|
242
|
+
|
|
243
|
+
Server-confirmed billing exemption: the authenticated server decides whether an account is exempt. Exempt accounts may omit a credit ceiling and do not incur customer-credit charges; explicit transcription permission and resource/provider limits still apply. Ordinary paid accounts allowing fresh transcription must supply a ceiling or receive `TRANSCRIPTION_NOT_ALLOWED` (HTTP 422); `MAX_CREDITS_EXCEEDED` (HTTP 422) means a supplied ceiling is invalid or below the required charge. Clients validate supplied values, then pass requests through without fetching extra account scopes or inferring privileges from names, roles, or key prefixes.
|