ttgrep 0.5.1__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.
- ttgrep-0.5.1/.gitignore +8 -0
- ttgrep-0.5.1/LICENSE +21 -0
- ttgrep-0.5.1/PKG-INFO +241 -0
- ttgrep-0.5.1/README.md +227 -0
- ttgrep-0.5.1/pyproject.toml +32 -0
- ttgrep-0.5.1/ttgrep/__init__.py +1 -0
- ttgrep-0.5.1/ttgrep/__main__.py +4 -0
- ttgrep-0.5.1/ttgrep/asr.py +120 -0
- ttgrep-0.5.1/ttgrep/cli.py +796 -0
- ttgrep-0.5.1/ttgrep/cost.py +91 -0
- ttgrep-0.5.1/ttgrep/fetch.py +360 -0
- ttgrep-0.5.1/ttgrep/mcp_server.py +128 -0
- ttgrep-0.5.1/ttgrep/store.py +125 -0
ttgrep-0.5.1/.gitignore
ADDED
ttgrep-0.5.1/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.1/PKG-INFO
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ttgrep
|
|
3
|
+
Version: 0.5.1
|
|
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.1/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
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ttgrep"
|
|
7
|
+
version = "0.5.1"
|
|
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"]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.sdist]
|
|
31
|
+
# Ship code, not demo videos or local editor config.
|
|
32
|
+
only-include = ["ttgrep", "README.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.1"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Local speech-to-text for videos that have no TikTok captions.
|
|
2
|
+
|
|
3
|
+
Backend preference: mlx-whisper (Apple Silicon; shells out to the ffmpeg CLI)
|
|
4
|
+
then faster-whisper (portable; decodes audio itself, no ffmpeg needed).
|
|
5
|
+
Models download from Hugging Face on first use (~500MB for `small`) into the
|
|
6
|
+
HF cache and are reused forever after.
|
|
7
|
+
|
|
8
|
+
Everything runs on-device: no API, no cost, no audio leaves the machine.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
|
|
16
|
+
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
|
17
|
+
|
|
18
|
+
MODELS = ("tiny", "base", "small", "medium", "large-v3")
|
|
19
|
+
DEFAULT_MODEL = "small" # smallest model that handles non-English speech well
|
|
20
|
+
|
|
21
|
+
_state: dict = {"checked": False, "kind": None, "why": None}
|
|
22
|
+
_faster_models: dict = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def backend() -> str | None:
|
|
26
|
+
"""'mlx' | 'faster' | None, decided once per process."""
|
|
27
|
+
if not _state["checked"]:
|
|
28
|
+
_state["checked"] = True
|
|
29
|
+
try:
|
|
30
|
+
import mlx_whisper # noqa: F401
|
|
31
|
+
|
|
32
|
+
if shutil.which("ffmpeg"):
|
|
33
|
+
_state["kind"] = "mlx"
|
|
34
|
+
else:
|
|
35
|
+
_state["why"] = "mlx-whisper installed but ffmpeg not on PATH"
|
|
36
|
+
except ImportError:
|
|
37
|
+
pass
|
|
38
|
+
if _state["kind"] is None:
|
|
39
|
+
try:
|
|
40
|
+
import faster_whisper # noqa: F401
|
|
41
|
+
|
|
42
|
+
_state["kind"] = "faster"
|
|
43
|
+
except ImportError:
|
|
44
|
+
_state["why"] = _state["why"] or "no whisper backend installed"
|
|
45
|
+
return _state["kind"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def unavailable_reason() -> str:
|
|
49
|
+
backend()
|
|
50
|
+
return _state["why"] or "unknown"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _mlx_repo(model: str) -> str:
|
|
54
|
+
if "/" in model: # full HF repo id passed through
|
|
55
|
+
return model
|
|
56
|
+
return f"mlx-community/whisper-{model}-mlx"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def transcribe(path, model: str = DEFAULT_MODEL) -> tuple[str | None, list[dict]]:
|
|
60
|
+
"""Transcribe an audio/video file. Returns (language_code, segments).
|
|
61
|
+
|
|
62
|
+
Segments use the same {start, end, text} shape as caption cues. The
|
|
63
|
+
language code is whisper-style ('fr', 'en'), distinct from TikTok's
|
|
64
|
+
caption codes ('fra-FR'), which keeps the two kinds of track apart.
|
|
65
|
+
"""
|
|
66
|
+
kind = backend()
|
|
67
|
+
if kind is None:
|
|
68
|
+
raise RuntimeError(f"no ASR backend: {unavailable_reason()}")
|
|
69
|
+
|
|
70
|
+
if kind == "mlx":
|
|
71
|
+
import mlx_whisper
|
|
72
|
+
|
|
73
|
+
result = mlx_whisper.transcribe(str(path), path_or_hf_repo=_mlx_repo(model), verbose=None)
|
|
74
|
+
lang = result.get("language")
|
|
75
|
+
raw = [(float(s["start"]), float(s["end"]), str(s.get("text", "")),
|
|
76
|
+
s.get("no_speech_prob"), s.get("avg_logprob"), s.get("compression_ratio"))
|
|
77
|
+
for s in result.get("segments", [])]
|
|
78
|
+
else:
|
|
79
|
+
from faster_whisper import WhisperModel
|
|
80
|
+
|
|
81
|
+
m = _faster_models.get(model)
|
|
82
|
+
if m is None:
|
|
83
|
+
m = _faster_models[model] = WhisperModel(model, compute_type="int8")
|
|
84
|
+
segs, info = m.transcribe(str(path), vad_filter=True)
|
|
85
|
+
lang = info.language
|
|
86
|
+
raw = [(s.start, s.end, s.text, s.no_speech_prob, s.avg_logprob, s.compression_ratio)
|
|
87
|
+
for s in segs]
|
|
88
|
+
|
|
89
|
+
segments: list[dict] = []
|
|
90
|
+
for start, end, text, nsp, alp, cr in raw:
|
|
91
|
+
text = text.strip()
|
|
92
|
+
if not text or not _looks_like_speech(nsp, alp, cr):
|
|
93
|
+
continue
|
|
94
|
+
if segments and segments[-1]["text"] == text: # whisper stutter
|
|
95
|
+
segments[-1]["end"] = round(end, 3)
|
|
96
|
+
else:
|
|
97
|
+
segments.append({"start": round(start, 3), "end": round(end, 3), "text": text})
|
|
98
|
+
return lang, segments
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _looks_like_speech(no_speech_prob, avg_logprob, compression_ratio) -> bool:
|
|
102
|
+
"""Reject whisper hallucinations on music/ambient audio.
|
|
103
|
+
|
|
104
|
+
Calibrated on TikTok audio: real speech (even over loud music) scores
|
|
105
|
+
avg_logprob ≳ -0.9; hallucinated noise scores ≲ -3. compression_ratio
|
|
106
|
+
> 2.4 (openai-whisper's own threshold) kills repetition loops ("a little
|
|
107
|
+
bit of a little bit of ..."). The classic silence rule (no_speech > 0.6
|
|
108
|
+
and logprob < -1) is kept as a final net. Accurately-heard song lyrics
|
|
109
|
+
pass all three on purpose: they are real audio content — the caller can
|
|
110
|
+
judge them by the track's whisper provenance.
|
|
111
|
+
"""
|
|
112
|
+
if compression_ratio is not None and compression_ratio > 2.4:
|
|
113
|
+
return False
|
|
114
|
+
if avg_logprob is None:
|
|
115
|
+
return True
|
|
116
|
+
if avg_logprob < -2.0:
|
|
117
|
+
return False
|
|
118
|
+
if no_speech_prob is not None and no_speech_prob > 0.6 and avg_logprob < -1.0:
|
|
119
|
+
return False
|
|
120
|
+
return True
|