ttgrep 0.5.0__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.
Potentially problematic release.
This version of ttgrep might be problematic. Click here for more details.
- ttgrep-0.5.0/.github/workflows/publish.yml +25 -0
- ttgrep-0.5.0/.gitignore +8 -0
- ttgrep-0.5.0/LICENSE +21 -0
- ttgrep-0.5.0/PKG-INFO +241 -0
- ttgrep-0.5.0/README.md +227 -0
- ttgrep-0.5.0/docs/agent-demo-2.mp4 +0 -0
- ttgrep-0.5.0/docs/agent-demo-2.tape +20 -0
- ttgrep-0.5.0/docs/agent-demo.gif +0 -0
- ttgrep-0.5.0/docs/agent-demo.mp4 +0 -0
- ttgrep-0.5.0/docs/agent-demo.tape +20 -0
- ttgrep-0.5.0/docs/demo.gif +0 -0
- ttgrep-0.5.0/docs/demo.tape +16 -0
- ttgrep-0.5.0/pyproject.toml +28 -0
- ttgrep-0.5.0/ttgrep/__init__.py +1 -0
- ttgrep-0.5.0/ttgrep/__main__.py +4 -0
- ttgrep-0.5.0/ttgrep/asr.py +120 -0
- ttgrep-0.5.0/ttgrep/cli.py +796 -0
- ttgrep-0.5.0/ttgrep/cost.py +91 -0
- ttgrep-0.5.0/ttgrep/fetch.py +360 -0
- ttgrep-0.5.0/ttgrep/mcp_server.py +128 -0
- ttgrep-0.5.0/ttgrep/store.py +125 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Publishes when you push a version tag (v0.5.0, v0.6.0, ...).
|
|
4
|
+
# Auth is PyPI "trusted publishing" (OIDC) — no token stored anywhere.
|
|
5
|
+
# One-time setup on pypi.org: Account → Publishing → add a pending publisher
|
|
6
|
+
# for project `ttgrep`, this repo, workflow `publish.yml`.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
permissions:
|
|
17
|
+
id-token: write # required for trusted publishing
|
|
18
|
+
contents: read
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: astral-sh/setup-uv@v5
|
|
22
|
+
- name: Build sdist and wheel
|
|
23
|
+
run: uv build
|
|
24
|
+
- name: Publish to PyPI
|
|
25
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
ttgrep-0.5.0/.gitignore
ADDED
ttgrep-0.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Louis de Benoist
|
|
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.
|
ttgrep-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ttgrep
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: grep for TikTok. Your AI agent syncs whole accounts, gets every word as text, and searches it. Free, local, cached forever.
|
|
5
|
+
Project-URL: Repository, https://github.com/Louis2B2G/ttgrep
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: faster-whisper>=1.0.0; sys_platform != 'darwin' or platform_machine != 'arm64'
|
|
10
|
+
Requires-Dist: mcp>=1.2
|
|
11
|
+
Requires-Dist: mlx-whisper>=0.4.0; sys_platform == 'darwin' and platform_machine == 'arm64'
|
|
12
|
+
Requires-Dist: yt-dlp[curl-cffi]>=2026.8.19
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# ttgrep
|
|
16
|
+
|
|
17
|
+
grep for TikTok.
|
|
18
|
+
|
|
19
|
+
TikTok has no public API, no transcripts, and no text search. ttgrep fixes that. It downloads everything an account has posted, turns every video into text, and lets you search all of it. Captions come from TikTok when they exist. When they don't, Whisper runs on your machine. Everything is saved to disk and never fetched twice. Nothing costs money.
|
|
20
|
+
|
|
21
|
+
Here is an AI agent using it. Real recording: one question in, receipts out ([full-speed video](docs/agent-demo.mp4), [second example](docs/agent-demo-2.mp4)):
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
It answers three questions:
|
|
26
|
+
|
|
27
|
+
1. **What has this account posted?** Use `ttgrep videos`.
|
|
28
|
+
2. **What was said in this video?** Use `ttgrep transcript`.
|
|
29
|
+
3. **In which videos do they talk about X?** Use `ttgrep search`.
|
|
30
|
+
|
|
31
|
+
It only retrieves. Deciding what the words mean is your job, or your AI agent's. ttgrep is built for agents: the output is plain text that reads well in a context window, it ships an MCP server (`ttgrep mcp`), and there is a ready-made CLAUDE.md snippet at the bottom of this page so your agent knows when to reach for it.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv tool install ttgrep
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`pipx install ttgrep` works too, and so does installing from source with `uv tool install git+https://github.com/Louis2B2G/ttgrep.git`. You get a `ttgrep` command with everything bundled: a current yt-dlp with `curl_cffi` (TikTok blocks plain clients) and a local Whisper. On Apple Silicon the Whisper backend is `mlx-whisper`, which also needs `ffmpeg` on your PATH (`brew install ffmpeg`). Everywhere else it is `faster-whisper`, which needs nothing extra. Run `ttgrep doctor` to check all of it.
|
|
40
|
+
|
|
41
|
+
## How to use it
|
|
42
|
+
|
|
43
|
+
One slow command, then everything is instant:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ttgrep sync @handle --limit 50 # fetch listing + transcripts (slow once, resumable)
|
|
47
|
+
ttgrep videos @handle # instant, from cache
|
|
48
|
+
ttgrep search @handle "topic" # instant, from cache
|
|
49
|
+
ttgrep transcript <url-or-id> # cached, or fetched on demand
|
|
50
|
+
ttgrep cost # what all of this would have cost elsewhere
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`sync` is the only command that does real network work: one request for the post list, then one page per video (about 2-3 seconds each, politely spaced). You can interrupt it any time. It saves as it goes and picks up where it left off. Run it again later and it only fetches new posts. `--limit N` covers just the N most recent posts; leave it off for full history.
|
|
54
|
+
|
|
55
|
+
### `sync ACCOUNT`
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
$ ttgrep sync @hankgreen1 --limit 40
|
|
59
|
+
listing @hankgreen1 (top 40) ... # progress goes to stderr
|
|
60
|
+
fetching captions for 25 videos ... # 15 of the 40 were already cached
|
|
61
|
+
[1/25] 7653172464536538382 ok eng-US
|
|
62
|
+
[2/25] 7652512813184781581 ok eng-US
|
|
63
|
+
[15/25] 7626519681100188958 none # no captions, whisper handles it below
|
|
64
|
+
...
|
|
65
|
+
transcribing 1 captionless videos locally (whisper-small on mlx; first ever run downloads the model)
|
|
66
|
+
[1/1] 7626519681100188958 asr en (3.6s)
|
|
67
|
+
account: hankgreen1 # summary goes to stdout
|
|
68
|
+
videos: 40
|
|
69
|
+
listing: partial
|
|
70
|
+
transcripts_ok: 39
|
|
71
|
+
transcripts_asr: 1
|
|
72
|
+
no_captions: 0
|
|
73
|
+
pending: 0
|
|
74
|
+
errors: 0
|
|
75
|
+
fetched_this_run: 25
|
|
76
|
+
transcribed_this_run: 1
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Videos without TikTok captions get their audio transcribed on your machine with Whisper (the model downloads once, about 500MB for `small`). If Whisper finds no speech at all, like music-only clips, the video is marked and never tried again. That is an answer, not an error.
|
|
80
|
+
|
|
81
|
+
Flags: `--limit N` (N most recent only), `--sleep SECS` (pause between fetches, default 1.0), `--retry-errors` (retry failed videos), `--listing-only` (post list only), `--no-listing` (skip the list, just fill missing transcripts), `--no-asr` (skip Whisper), `--asr-model MODEL` (default `small`).
|
|
82
|
+
|
|
83
|
+
### `videos ACCOUNT`
|
|
84
|
+
|
|
85
|
+
What they posted. TSV, newest first. The title on TikTok is the post description, hashtags and all.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
$ ttgrep videos @hankgreen1 --limit 3
|
|
89
|
+
id date dur views likes comments transcript title
|
|
90
|
+
7678870202494340383 2026-08-28 173 24700 2182 53 ok Longer video getting into the very very weird details of how the immune system ...
|
|
91
|
+
7678484247363030302 2026-08-26 58 53300 4438 90 ok #stitch with @kyro_frenchbulldogs #greenscreen
|
|
92
|
+
7676649513565703437 2026-08-22 67 15700 337 22 ok How do you get a voter’s attention? Listen to the full conversation wherever y...
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The `transcript` column: `ok` means TikTok captions, `asr` means Whisper, `none` means no captions and no speech, `pending` means not fetched yet, `error` means the fetch failed, `photo` means an image post (no audio exists). Flags: `--limit`, `--since YYYY-MM-DD`, `--full` (don't shorten titles), `--json`.
|
|
96
|
+
|
|
97
|
+
### `transcript VIDEO`
|
|
98
|
+
|
|
99
|
+
What was said in one video. Takes a URL or a bare id. Cached videos print instantly. Unknown videos are fetched on the spot, even from accounts you never synced.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
$ ttgrep transcript 7678484247363030302
|
|
103
|
+
id: 7678484247363030302
|
|
104
|
+
account: hankgreen1
|
|
105
|
+
date: 2026-08-26
|
|
106
|
+
duration: 58
|
|
107
|
+
views: 53300
|
|
108
|
+
url: https://www.tiktok.com/@hankgreen1/video/7678484247363030302
|
|
109
|
+
langs: eng-US
|
|
110
|
+
lang: eng-US
|
|
111
|
+
title: #stitch with @kyro_frenchbulldogs #greenscreen
|
|
112
|
+
---
|
|
113
|
+
The butterflies do this thing called puddling, where they drink water specifically from muddy puddles, because their food source, nectar, doesn't contain a lot ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Flags: `--timestamps` (one `[m:ss]` line per caption), `--lang CODE` (`en` works as a prefix for `eng-US`), `--json` (every language track, with timing), `--no-asr`, `--asr-model`. No captions? It transcribes the audio right there, once, in 5 to 30 seconds. Only a video with no speech at all prints `transcript: none`, and it exits 0, because that is an answer.
|
|
117
|
+
|
|
118
|
+
### `search [ACCOUNT] QUERY`
|
|
119
|
+
|
|
120
|
+
Which videos say X. Searches every cached transcript and title. Case doesn't matter. `--regex` for patterns like `'solar|renewable'`. `--all` searches every account you have cached.
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
$ ttgrep search @hankgreen1 "immune"
|
|
124
|
+
# query='immune' accounts=1 posts=40 transcripts_searched=40 matched=2 skipped: no_captions=0 pending=0 error=0
|
|
125
|
+
== 7678870202494340383 2026-08-28 @hankgreen1 views=24700 lang=eng-US
|
|
126
|
+
Longer video getting into the very very weird details of how the immune system ...
|
|
127
|
+
[0:42] like, wake your immune system up to the fact that you might get this cancer. And that would decrease your risk of getting that cancer
|
|
128
|
+
[2:05] and then training with a vaccine, the immune system, to go after it. Like, be on the lookout for those specific weird proteins.
|
|
129
|
+
...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The first line always tells you how much was searchable, so you know what a "no" means. Matches are quoted with timestamps. Phrases that cross caption boundaries still match. Exit code 1 means "searched fine, found nothing". Check the `pending` count before you claim someone never said something.
|
|
133
|
+
|
|
134
|
+
### `cost`
|
|
135
|
+
|
|
136
|
+
What your cache would have cost if you had bought it.
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
$ ttgrep cost
|
|
142
|
+
your cache: 8 accounts · 2,103 videos · 1,892 transcripts · 35.5 hours of speech
|
|
143
|
+
how: TikTok captions (1,557) + whisper on this machine (335)
|
|
144
|
+
the same thing, bought (published 2026 prices):
|
|
145
|
+
Supadata "TikTok Transcript API" ....... $17/month (Pro plan), metered
|
|
146
|
+
OpenAI transcription, 35.5 h of audio ... $12.77, plus you build the rest
|
|
147
|
+
social listening tools ................. can't search speech at all
|
|
148
|
+
your bill .............................. $0.00
|
|
149
|
+
your next 10,000 searches .............. $0.00
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The last line is the point. When every question costs money, an agent learns to stop asking. When questions are free, it can grep everything, twice.
|
|
153
|
+
|
|
154
|
+
Prices, checked August 2026: [Supadata](https://supadata.ai/pricing) sells TikTok transcripts at 1 credit each ($17/month for 3,000, $47/month for 30,000). [OpenAI transcription](https://platform.openai.com/pricing) is $0.006 per minute. [ScrapeCreators](https://scrapecreators.com/tiktok-transcript-api) is similar ($10 per 5,000). The numbers live in `ttgrep/cost.py`; fix them when they drift. `--per-account` shows a per-account breakdown. `--json` gives raw numbers.
|
|
155
|
+
|
|
156
|
+
### `status`, `doctor`, `mcp`
|
|
157
|
+
|
|
158
|
+
`ttgrep status` shows what you have cached. `ttgrep doctor` checks your install and does one live probe against TikTok. Run it first when something breaks.
|
|
159
|
+
|
|
160
|
+
`ttgrep mcp` runs the whole thing as an MCP server over stdio. Same cache, same output, six tools (`tiktok_sync`, `tiktok_videos`, `tiktok_transcript`, `tiktok_search`, `tiktok_status`, `tiktok_cost`):
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{ "mcpServers": { "ttgrep": { "command": "ttgrep", "args": ["mcp"] } } }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## The test
|
|
167
|
+
|
|
168
|
+
The acceptance test for this tool was not a test suite. It was handing a fresh AI agent one sentence ("ttgrep is installed, figure it out with --help") plus a question about a synced account. No docs, no hints. The agent found the commands, searched, quoted timestamped evidence, ranked its findings, and correctly refused to guess about one music-only video because the coverage line told it there was nothing to search. If your agent can read `--help`, it can use this.
|
|
169
|
+
|
|
170
|
+
That test shaped the output: coverage before results, "no captions" kept separate from "not fetched yet", exit 1 for "found nothing", and stderr kept out of the data.
|
|
171
|
+
|
|
172
|
+
## Output rules
|
|
173
|
+
|
|
174
|
+
- stdout is data. stderr is progress and hints. Pipe stdout safely.
|
|
175
|
+
- Exit codes: 0 ok, 1 search found nothing, 2 real error.
|
|
176
|
+
- `--json` everywhere it matters. Default output is compact text, to save tokens.
|
|
177
|
+
- Dates are UTC `YYYY-MM-DD`. Durations are seconds. Counts are plain integers.
|
|
178
|
+
|
|
179
|
+
## Where things live
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
~/.ttgrep/ # change with $TTGREP_HOME
|
|
183
|
+
accounts/<handle>/index.json # post list + per-video status
|
|
184
|
+
accounts/<handle>/transcripts/<id>.json # all language tracks, with timing
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Plain JSON. You can grep it directly. Writes are atomic, so an interrupted sync never corrupts anything. `ok` and `none` are final and never re-fetched. Videos deleted from a profile stay in your cache, marked `listed: false`. Knowledge is not deleted. Full reset for one account: `rm -r ~/.ttgrep/accounts/<handle>`.
|
|
188
|
+
|
|
189
|
+
## Languages
|
|
190
|
+
|
|
191
|
+
TikTok uses its own caption codes: `eng-US` and `fra-FR`, not `en` and `fr`. Some videos have several tracks (the original plus translations). `sync` stores all of them; caption files are tiny. When you read, ttgrep picks the account's main language (the one on most of their videos). Override with `--lang` anywhere. Whisper tracks use short codes (`en`, `fr`) and carry a `source` field, so you can always tell captions from local transcription.
|
|
192
|
+
|
|
193
|
+
One honest warning: Whisper writes down whatever is audible. On a video with a song and no talking, that can be accurately transcribed song lyrics. Hallucinated noise and repeated-word loops are filtered out using Whisper's own confidence numbers, but real lyrics stay in, on purpose. Check whether an `asr` match is speech before you quote it as something the creator said.
|
|
194
|
+
|
|
195
|
+
## When it breaks
|
|
196
|
+
|
|
197
|
+
TikTok changes things. yt-dlp chases them. If fetching stops working:
|
|
198
|
+
|
|
199
|
+
1. `ttgrep doctor` tells you whether it's your install or TikTok.
|
|
200
|
+
2. `uv tool upgrade ttgrep` pulls the latest yt-dlp, which is the fix most of the time.
|
|
201
|
+
3. Many `error` statuses mid-sync usually means rate limiting. Wait, then re-run with a higher `--sleep` and `--retry-errors`.
|
|
202
|
+
|
|
203
|
+
### Notes for whoever maintains this (learned the hard way)
|
|
204
|
+
|
|
205
|
+
- yt-dlp only fills in `subtitles` when the `writesubtitles` (or `listsubtitles`) option is set. A plain `extract_info()` reports no captions for videos that have them.
|
|
206
|
+
- TikTok captions appear under `subtitles`, not `automatic_captions`, even though they are auto-generated.
|
|
207
|
+
- Fetching caption URLs yourself can get a 403. Going through yt-dlp's own subtitle download machinery works. ttgrep does the latter.
|
|
208
|
+
- One extractor path returns SRT data instead of VTT files. The parser accepts both.
|
|
209
|
+
- TikTok serves many image posts under `/video/` URLs. They fail with "No video formats found". That means "image post", not "error".
|
|
210
|
+
- Whisper on music produces two failure modes: hallucinated text (avg_logprob below about -3, versus above -0.9 for real speech) and repetition loops (compression_ratio over 2.4). Both are filtered in `asr.py`.
|
|
211
|
+
- A flat-playlist extraction of a profile returns full metadata per video. Listing an account never needs per-video fetches.
|
|
212
|
+
- A TikTok video id encodes its post time: `id >> 32` is unix seconds.
|
|
213
|
+
- `tiktok.com/@_/video/<id>` resolves for any bare video id. The handle in the URL is ignored.
|
|
214
|
+
|
|
215
|
+
## CLAUDE.md snippet
|
|
216
|
+
|
|
217
|
+
Paste this into your global `~/.claude/CLAUDE.md` so your agent knows when to use ttgrep:
|
|
218
|
+
|
|
219
|
+
```markdown
|
|
220
|
+
## TikTok (`ttgrep`)
|
|
221
|
+
|
|
222
|
+
When a task involves what a TikTok account posts or says (analyzing a creator,
|
|
223
|
+
checking if or when they covered a topic, quoting a video), use `ttgrep`. Do
|
|
224
|
+
not use ad-hoc yt-dlp or web scraping. Everything is cached in ~/.ttgrep;
|
|
225
|
+
never re-fetch what it already has.
|
|
226
|
+
|
|
227
|
+
- New account: `ttgrep sync @handle --limit 50` first (slow, 2-3s per video,
|
|
228
|
+
resumable). Videos without captions get whisper-transcribed automatically.
|
|
229
|
+
- `ttgrep videos @handle` shows what they posted (TSV; `--json` for full records).
|
|
230
|
+
- `ttgrep transcript <url-or-id>` shows what was said (fetches if uncached).
|
|
231
|
+
`transcript: none` means the video has no speech. That is an answer.
|
|
232
|
+
- `ttgrep search @handle "term"` finds which videos mention it (`--regex 'a|b'`,
|
|
233
|
+
`--all` for every cached account). Exit 1 means no matches. Read the `#`
|
|
234
|
+
coverage line first: if `pending` > 0, sync before claiming they never said it.
|
|
235
|
+
- Caption languages are TikTok codes (`eng-US`); default is the account's main
|
|
236
|
+
language, `--lang en` to override. Whisper tracks on music-only videos can be
|
|
237
|
+
song lyrics. Check before quoting them as the creator's words.
|
|
238
|
+
- If fetching fails: `ttgrep doctor`, then `uv tool upgrade ttgrep`.
|
|
239
|
+
- Missing? `uv tool install ttgrep`
|
|
240
|
+
- MCP clients: `ttgrep mcp` serves the same commands as tools over stdio.
|
|
241
|
+
```
|
ttgrep-0.5.0/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# ttgrep
|
|
2
|
+
|
|
3
|
+
grep for TikTok.
|
|
4
|
+
|
|
5
|
+
TikTok has no public API, no transcripts, and no text search. ttgrep fixes that. It downloads everything an account has posted, turns every video into text, and lets you search all of it. Captions come from TikTok when they exist. When they don't, Whisper runs on your machine. Everything is saved to disk and never fetched twice. Nothing costs money.
|
|
6
|
+
|
|
7
|
+
Here is an AI agent using it. Real recording: one question in, receipts out ([full-speed video](docs/agent-demo.mp4), [second example](docs/agent-demo-2.mp4)):
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
It answers three questions:
|
|
12
|
+
|
|
13
|
+
1. **What has this account posted?** Use `ttgrep videos`.
|
|
14
|
+
2. **What was said in this video?** Use `ttgrep transcript`.
|
|
15
|
+
3. **In which videos do they talk about X?** Use `ttgrep search`.
|
|
16
|
+
|
|
17
|
+
It only retrieves. Deciding what the words mean is your job, or your AI agent's. ttgrep is built for agents: the output is plain text that reads well in a context window, it ships an MCP server (`ttgrep mcp`), and there is a ready-made CLAUDE.md snippet at the bottom of this page so your agent knows when to reach for it.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install ttgrep
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`pipx install ttgrep` works too, and so does installing from source with `uv tool install git+https://github.com/Louis2B2G/ttgrep.git`. You get a `ttgrep` command with everything bundled: a current yt-dlp with `curl_cffi` (TikTok blocks plain clients) and a local Whisper. On Apple Silicon the Whisper backend is `mlx-whisper`, which also needs `ffmpeg` on your PATH (`brew install ffmpeg`). Everywhere else it is `faster-whisper`, which needs nothing extra. Run `ttgrep doctor` to check all of it.
|
|
26
|
+
|
|
27
|
+
## How to use it
|
|
28
|
+
|
|
29
|
+
One slow command, then everything is instant:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ttgrep sync @handle --limit 50 # fetch listing + transcripts (slow once, resumable)
|
|
33
|
+
ttgrep videos @handle # instant, from cache
|
|
34
|
+
ttgrep search @handle "topic" # instant, from cache
|
|
35
|
+
ttgrep transcript <url-or-id> # cached, or fetched on demand
|
|
36
|
+
ttgrep cost # what all of this would have cost elsewhere
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`sync` is the only command that does real network work: one request for the post list, then one page per video (about 2-3 seconds each, politely spaced). You can interrupt it any time. It saves as it goes and picks up where it left off. Run it again later and it only fetches new posts. `--limit N` covers just the N most recent posts; leave it off for full history.
|
|
40
|
+
|
|
41
|
+
### `sync ACCOUNT`
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
$ ttgrep sync @hankgreen1 --limit 40
|
|
45
|
+
listing @hankgreen1 (top 40) ... # progress goes to stderr
|
|
46
|
+
fetching captions for 25 videos ... # 15 of the 40 were already cached
|
|
47
|
+
[1/25] 7653172464536538382 ok eng-US
|
|
48
|
+
[2/25] 7652512813184781581 ok eng-US
|
|
49
|
+
[15/25] 7626519681100188958 none # no captions, whisper handles it below
|
|
50
|
+
...
|
|
51
|
+
transcribing 1 captionless videos locally (whisper-small on mlx; first ever run downloads the model)
|
|
52
|
+
[1/1] 7626519681100188958 asr en (3.6s)
|
|
53
|
+
account: hankgreen1 # summary goes to stdout
|
|
54
|
+
videos: 40
|
|
55
|
+
listing: partial
|
|
56
|
+
transcripts_ok: 39
|
|
57
|
+
transcripts_asr: 1
|
|
58
|
+
no_captions: 0
|
|
59
|
+
pending: 0
|
|
60
|
+
errors: 0
|
|
61
|
+
fetched_this_run: 25
|
|
62
|
+
transcribed_this_run: 1
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Videos without TikTok captions get their audio transcribed on your machine with Whisper (the model downloads once, about 500MB for `small`). If Whisper finds no speech at all, like music-only clips, the video is marked and never tried again. That is an answer, not an error.
|
|
66
|
+
|
|
67
|
+
Flags: `--limit N` (N most recent only), `--sleep SECS` (pause between fetches, default 1.0), `--retry-errors` (retry failed videos), `--listing-only` (post list only), `--no-listing` (skip the list, just fill missing transcripts), `--no-asr` (skip Whisper), `--asr-model MODEL` (default `small`).
|
|
68
|
+
|
|
69
|
+
### `videos ACCOUNT`
|
|
70
|
+
|
|
71
|
+
What they posted. TSV, newest first. The title on TikTok is the post description, hashtags and all.
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
$ ttgrep videos @hankgreen1 --limit 3
|
|
75
|
+
id date dur views likes comments transcript title
|
|
76
|
+
7678870202494340383 2026-08-28 173 24700 2182 53 ok Longer video getting into the very very weird details of how the immune system ...
|
|
77
|
+
7678484247363030302 2026-08-26 58 53300 4438 90 ok #stitch with @kyro_frenchbulldogs #greenscreen
|
|
78
|
+
7676649513565703437 2026-08-22 67 15700 337 22 ok How do you get a voter’s attention? Listen to the full conversation wherever y...
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The `transcript` column: `ok` means TikTok captions, `asr` means Whisper, `none` means no captions and no speech, `pending` means not fetched yet, `error` means the fetch failed, `photo` means an image post (no audio exists). Flags: `--limit`, `--since YYYY-MM-DD`, `--full` (don't shorten titles), `--json`.
|
|
82
|
+
|
|
83
|
+
### `transcript VIDEO`
|
|
84
|
+
|
|
85
|
+
What was said in one video. Takes a URL or a bare id. Cached videos print instantly. Unknown videos are fetched on the spot, even from accounts you never synced.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
$ ttgrep transcript 7678484247363030302
|
|
89
|
+
id: 7678484247363030302
|
|
90
|
+
account: hankgreen1
|
|
91
|
+
date: 2026-08-26
|
|
92
|
+
duration: 58
|
|
93
|
+
views: 53300
|
|
94
|
+
url: https://www.tiktok.com/@hankgreen1/video/7678484247363030302
|
|
95
|
+
langs: eng-US
|
|
96
|
+
lang: eng-US
|
|
97
|
+
title: #stitch with @kyro_frenchbulldogs #greenscreen
|
|
98
|
+
---
|
|
99
|
+
The butterflies do this thing called puddling, where they drink water specifically from muddy puddles, because their food source, nectar, doesn't contain a lot ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Flags: `--timestamps` (one `[m:ss]` line per caption), `--lang CODE` (`en` works as a prefix for `eng-US`), `--json` (every language track, with timing), `--no-asr`, `--asr-model`. No captions? It transcribes the audio right there, once, in 5 to 30 seconds. Only a video with no speech at all prints `transcript: none`, and it exits 0, because that is an answer.
|
|
103
|
+
|
|
104
|
+
### `search [ACCOUNT] QUERY`
|
|
105
|
+
|
|
106
|
+
Which videos say X. Searches every cached transcript and title. Case doesn't matter. `--regex` for patterns like `'solar|renewable'`. `--all` searches every account you have cached.
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
$ ttgrep search @hankgreen1 "immune"
|
|
110
|
+
# query='immune' accounts=1 posts=40 transcripts_searched=40 matched=2 skipped: no_captions=0 pending=0 error=0
|
|
111
|
+
== 7678870202494340383 2026-08-28 @hankgreen1 views=24700 lang=eng-US
|
|
112
|
+
Longer video getting into the very very weird details of how the immune system ...
|
|
113
|
+
[0:42] like, wake your immune system up to the fact that you might get this cancer. And that would decrease your risk of getting that cancer
|
|
114
|
+
[2:05] and then training with a vaccine, the immune system, to go after it. Like, be on the lookout for those specific weird proteins.
|
|
115
|
+
...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The first line always tells you how much was searchable, so you know what a "no" means. Matches are quoted with timestamps. Phrases that cross caption boundaries still match. Exit code 1 means "searched fine, found nothing". Check the `pending` count before you claim someone never said something.
|
|
119
|
+
|
|
120
|
+
### `cost`
|
|
121
|
+
|
|
122
|
+
What your cache would have cost if you had bought it.
|
|
123
|
+
|
|
124
|
+

|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
$ ttgrep cost
|
|
128
|
+
your cache: 8 accounts · 2,103 videos · 1,892 transcripts · 35.5 hours of speech
|
|
129
|
+
how: TikTok captions (1,557) + whisper on this machine (335)
|
|
130
|
+
the same thing, bought (published 2026 prices):
|
|
131
|
+
Supadata "TikTok Transcript API" ....... $17/month (Pro plan), metered
|
|
132
|
+
OpenAI transcription, 35.5 h of audio ... $12.77, plus you build the rest
|
|
133
|
+
social listening tools ................. can't search speech at all
|
|
134
|
+
your bill .............................. $0.00
|
|
135
|
+
your next 10,000 searches .............. $0.00
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The last line is the point. When every question costs money, an agent learns to stop asking. When questions are free, it can grep everything, twice.
|
|
139
|
+
|
|
140
|
+
Prices, checked August 2026: [Supadata](https://supadata.ai/pricing) sells TikTok transcripts at 1 credit each ($17/month for 3,000, $47/month for 30,000). [OpenAI transcription](https://platform.openai.com/pricing) is $0.006 per minute. [ScrapeCreators](https://scrapecreators.com/tiktok-transcript-api) is similar ($10 per 5,000). The numbers live in `ttgrep/cost.py`; fix them when they drift. `--per-account` shows a per-account breakdown. `--json` gives raw numbers.
|
|
141
|
+
|
|
142
|
+
### `status`, `doctor`, `mcp`
|
|
143
|
+
|
|
144
|
+
`ttgrep status` shows what you have cached. `ttgrep doctor` checks your install and does one live probe against TikTok. Run it first when something breaks.
|
|
145
|
+
|
|
146
|
+
`ttgrep mcp` runs the whole thing as an MCP server over stdio. Same cache, same output, six tools (`tiktok_sync`, `tiktok_videos`, `tiktok_transcript`, `tiktok_search`, `tiktok_status`, `tiktok_cost`):
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{ "mcpServers": { "ttgrep": { "command": "ttgrep", "args": ["mcp"] } } }
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## The test
|
|
153
|
+
|
|
154
|
+
The acceptance test for this tool was not a test suite. It was handing a fresh AI agent one sentence ("ttgrep is installed, figure it out with --help") plus a question about a synced account. No docs, no hints. The agent found the commands, searched, quoted timestamped evidence, ranked its findings, and correctly refused to guess about one music-only video because the coverage line told it there was nothing to search. If your agent can read `--help`, it can use this.
|
|
155
|
+
|
|
156
|
+
That test shaped the output: coverage before results, "no captions" kept separate from "not fetched yet", exit 1 for "found nothing", and stderr kept out of the data.
|
|
157
|
+
|
|
158
|
+
## Output rules
|
|
159
|
+
|
|
160
|
+
- stdout is data. stderr is progress and hints. Pipe stdout safely.
|
|
161
|
+
- Exit codes: 0 ok, 1 search found nothing, 2 real error.
|
|
162
|
+
- `--json` everywhere it matters. Default output is compact text, to save tokens.
|
|
163
|
+
- Dates are UTC `YYYY-MM-DD`. Durations are seconds. Counts are plain integers.
|
|
164
|
+
|
|
165
|
+
## Where things live
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
~/.ttgrep/ # change with $TTGREP_HOME
|
|
169
|
+
accounts/<handle>/index.json # post list + per-video status
|
|
170
|
+
accounts/<handle>/transcripts/<id>.json # all language tracks, with timing
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Plain JSON. You can grep it directly. Writes are atomic, so an interrupted sync never corrupts anything. `ok` and `none` are final and never re-fetched. Videos deleted from a profile stay in your cache, marked `listed: false`. Knowledge is not deleted. Full reset for one account: `rm -r ~/.ttgrep/accounts/<handle>`.
|
|
174
|
+
|
|
175
|
+
## Languages
|
|
176
|
+
|
|
177
|
+
TikTok uses its own caption codes: `eng-US` and `fra-FR`, not `en` and `fr`. Some videos have several tracks (the original plus translations). `sync` stores all of them; caption files are tiny. When you read, ttgrep picks the account's main language (the one on most of their videos). Override with `--lang` anywhere. Whisper tracks use short codes (`en`, `fr`) and carry a `source` field, so you can always tell captions from local transcription.
|
|
178
|
+
|
|
179
|
+
One honest warning: Whisper writes down whatever is audible. On a video with a song and no talking, that can be accurately transcribed song lyrics. Hallucinated noise and repeated-word loops are filtered out using Whisper's own confidence numbers, but real lyrics stay in, on purpose. Check whether an `asr` match is speech before you quote it as something the creator said.
|
|
180
|
+
|
|
181
|
+
## When it breaks
|
|
182
|
+
|
|
183
|
+
TikTok changes things. yt-dlp chases them. If fetching stops working:
|
|
184
|
+
|
|
185
|
+
1. `ttgrep doctor` tells you whether it's your install or TikTok.
|
|
186
|
+
2. `uv tool upgrade ttgrep` pulls the latest yt-dlp, which is the fix most of the time.
|
|
187
|
+
3. Many `error` statuses mid-sync usually means rate limiting. Wait, then re-run with a higher `--sleep` and `--retry-errors`.
|
|
188
|
+
|
|
189
|
+
### Notes for whoever maintains this (learned the hard way)
|
|
190
|
+
|
|
191
|
+
- yt-dlp only fills in `subtitles` when the `writesubtitles` (or `listsubtitles`) option is set. A plain `extract_info()` reports no captions for videos that have them.
|
|
192
|
+
- TikTok captions appear under `subtitles`, not `automatic_captions`, even though they are auto-generated.
|
|
193
|
+
- Fetching caption URLs yourself can get a 403. Going through yt-dlp's own subtitle download machinery works. ttgrep does the latter.
|
|
194
|
+
- One extractor path returns SRT data instead of VTT files. The parser accepts both.
|
|
195
|
+
- TikTok serves many image posts under `/video/` URLs. They fail with "No video formats found". That means "image post", not "error".
|
|
196
|
+
- Whisper on music produces two failure modes: hallucinated text (avg_logprob below about -3, versus above -0.9 for real speech) and repetition loops (compression_ratio over 2.4). Both are filtered in `asr.py`.
|
|
197
|
+
- A flat-playlist extraction of a profile returns full metadata per video. Listing an account never needs per-video fetches.
|
|
198
|
+
- A TikTok video id encodes its post time: `id >> 32` is unix seconds.
|
|
199
|
+
- `tiktok.com/@_/video/<id>` resolves for any bare video id. The handle in the URL is ignored.
|
|
200
|
+
|
|
201
|
+
## CLAUDE.md snippet
|
|
202
|
+
|
|
203
|
+
Paste this into your global `~/.claude/CLAUDE.md` so your agent knows when to use ttgrep:
|
|
204
|
+
|
|
205
|
+
```markdown
|
|
206
|
+
## TikTok (`ttgrep`)
|
|
207
|
+
|
|
208
|
+
When a task involves what a TikTok account posts or says (analyzing a creator,
|
|
209
|
+
checking if or when they covered a topic, quoting a video), use `ttgrep`. Do
|
|
210
|
+
not use ad-hoc yt-dlp or web scraping. Everything is cached in ~/.ttgrep;
|
|
211
|
+
never re-fetch what it already has.
|
|
212
|
+
|
|
213
|
+
- New account: `ttgrep sync @handle --limit 50` first (slow, 2-3s per video,
|
|
214
|
+
resumable). Videos without captions get whisper-transcribed automatically.
|
|
215
|
+
- `ttgrep videos @handle` shows what they posted (TSV; `--json` for full records).
|
|
216
|
+
- `ttgrep transcript <url-or-id>` shows what was said (fetches if uncached).
|
|
217
|
+
`transcript: none` means the video has no speech. That is an answer.
|
|
218
|
+
- `ttgrep search @handle "term"` finds which videos mention it (`--regex 'a|b'`,
|
|
219
|
+
`--all` for every cached account). Exit 1 means no matches. Read the `#`
|
|
220
|
+
coverage line first: if `pending` > 0, sync before claiming they never said it.
|
|
221
|
+
- Caption languages are TikTok codes (`eng-US`); default is the account's main
|
|
222
|
+
language, `--lang en` to override. Whisper tracks on music-only videos can be
|
|
223
|
+
song lyrics. Check before quoting them as the creator's words.
|
|
224
|
+
- If fetching fails: `ttgrep doctor`, then `uv tool upgrade ttgrep`.
|
|
225
|
+
- Missing? `uv tool install ttgrep`
|
|
226
|
+
- MCP clients: `ttgrep mcp` serves the same commands as tools over stdio.
|
|
227
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Output agent-demo-2.mp4
|
|
2
|
+
Set FontSize 15
|
|
3
|
+
Set Width 1180
|
|
4
|
+
Set Height 840
|
|
5
|
+
Set Padding 16
|
|
6
|
+
Set TypingSpeed 35ms
|
|
7
|
+
Set Theme "Dracula"
|
|
8
|
+
Set Shell bash
|
|
9
|
+
|
|
10
|
+
Hide
|
|
11
|
+
Type "unset CLAUDE_CODE_CHILD_SESSION CLAUDECODE CLAUDE_CODE_ENTRYPOINT; clear"
|
|
12
|
+
Enter
|
|
13
|
+
Show
|
|
14
|
+
Type "claude"
|
|
15
|
+
Enter
|
|
16
|
+
Sleep 15s
|
|
17
|
+
Type "Has Cleo Abram's take on nuclear energy changed over time? Keep it brief - a few dated quotes."
|
|
18
|
+
Sleep 1s
|
|
19
|
+
Enter
|
|
20
|
+
Sleep 300s
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Output agent-demo.mp4
|
|
2
|
+
Set FontSize 15
|
|
3
|
+
Set Width 1180
|
|
4
|
+
Set Height 840
|
|
5
|
+
Set Padding 16
|
|
6
|
+
Set TypingSpeed 35ms
|
|
7
|
+
Set Theme "Dracula"
|
|
8
|
+
Set Shell bash
|
|
9
|
+
|
|
10
|
+
Hide
|
|
11
|
+
Type "unset CLAUDE_CODE_CHILD_SESSION CLAUDECODE CLAUDE_CODE_ENTRYPOINT; clear"
|
|
12
|
+
Enter
|
|
13
|
+
Show
|
|
14
|
+
Type "claude"
|
|
15
|
+
Enter
|
|
16
|
+
Sleep 15s
|
|
17
|
+
Type "What's the worst product MKBHD has talked about on TikTok? Quote him."
|
|
18
|
+
Sleep 1s
|
|
19
|
+
Enter
|
|
20
|
+
Sleep 175s
|
|
Binary file
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# VHS tape for the README hero GIF — render with: vhs docs/demo.tape
|
|
2
|
+
# https://github.com/charmbracelet/vhs
|
|
3
|
+
|
|
4
|
+
Output docs/demo.gif
|
|
5
|
+
|
|
6
|
+
Set FontSize 16
|
|
7
|
+
Set Width 1060
|
|
8
|
+
Set Height 330
|
|
9
|
+
Set Padding 20
|
|
10
|
+
Set TypingSpeed 40ms
|
|
11
|
+
Set Theme "Dracula"
|
|
12
|
+
|
|
13
|
+
Sleep 500ms
|
|
14
|
+
Type "ttgrep cost"
|
|
15
|
+
Enter
|
|
16
|
+
Sleep 7s
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ttgrep"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "grep for TikTok. Your AI agent syncs whole accounts, gets every word as text, and searches it. Free, local, cached forever."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"yt-dlp[curl-cffi]>=2026.8.19",
|
|
14
|
+
# Local whisper for videos without TikTok captions (free, on-device).
|
|
15
|
+
"mlx-whisper>=0.4.0; sys_platform == 'darwin' and platform_machine == 'arm64'",
|
|
16
|
+
"faster-whisper>=1.0.0; sys_platform != 'darwin' or platform_machine != 'arm64'",
|
|
17
|
+
# `ttgrep mcp` — serve the same commands to MCP clients over stdio.
|
|
18
|
+
"mcp>=1.2",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Repository = "https://github.com/Louis2B2G/ttgrep"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
ttgrep = "ttgrep.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["ttgrep"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|