Official documentation verification date:2026-07-27 Basis:https://console.groq.com/docs/speech-to-text、https://console.groq.com/docs/api-reference、https://console.groq.com/docs/rate-limits、https://console.groq.com/docs/models、https://console.groq.com/docs/your-data、https://console.groq.com/docs/billing-faqs、https://console.groq.com/docs/quickstart
🔴 Read this first: the privacy red line (this section is the most important content in this pack)
Everything this pack does sends your audio files to someone else's servers.
It's not the kind of sending where it "disappears after processing", it'sthe entire audio file is uploaded, run on machines in the United States, and then the text is sent back. Whatever your computer's microphone records is what gets sent out.
These three types of recordings should never be uploaded
| ❌ Do not upload | Why |
|---|---|
| Classroom recordings containing students' voices | Students are minors.Whether to let children's voices be uploaded to an overseas cloud is something you cannot consent to on behalf of parents. |
| Parent meetings, parent-teacher communication, phone recordings | The other party usually doesn't know they're being recorded, let alone that it will be uploaded |
| Counseling records, case conferences, private conversations with students | Once this kind of content leaks, the ones who get hurt are the children, and you will bear legal responsibility |
While we're at it, three more things not to do:Roll-call recordings that say students' names、Recordings of students' answers in speaking tests、Any file where you're not sure who is speaking in it。
These can be uploaded
- Recordings of you alone lecturing, preparing lessons, or speaking (only your voice)
- Your own public speeches, workshop sharing
- Narration or voiceover scripts you recorded yourself
- Public videos (news, publicly broadcast teaching materials, videos from your own channel)
The one-sentence criterion for judgment
🔴 "If this audio file appeared online tomorrow, would anyone get hurt? Would it require a report?"
As long as the answer is "yes" or "not sure"——do not upload. No exceptions, no matter how rushed the schedule is.
⚠️ The place where things most easily go wrong: the end of the file
The most common accident with classroom recordings isn't the content, it's "forgetting to press stop".
You think you recorded 40 minutes of your own lecture, but actually the recording kept going until class ended, until students came up to ask questions, until you finished chatting with the teacher next door.
🔴 Rule: before uploading, jump to the last 30 seconds of the file and listen to it once. This step takes 30 seconds and can block 90% of the real risk in this pack.
Groq's official data policy (verified 2026-07-27)
Talk about the red line before talking about policy; the order cannot be reversed——No matter how good the policy is, it will not make the red line disappear.
| Item | Official statement | Source |
|---|---|---|
| Is data from inference requests retained | Not retained by default("By default, Groq does not retain customer data for inference requests.") | Your Data in GroqCloud |
| Will it be used to train models | No. The terms of service explicitly state: without the customer's express consent, inputs/outputs may not be used to train or fine-tune models | Groq Services Agreement |
| Are there exceptions? | Yes. For troubleshooting and abuse investigation, input and output may betemporarilylogged, retained for up to 30 days(possibly longer if legally required) | Your Data in GroqCloud |
| Can it be turned off completely? | Yes. All customers can, in the Console's Data Controls turn on Zero Data Retention(ZDR) | Your Data in GroqCloud |
| Is the free plan worse? | The official documentation does not set separate, looser rules for the free plan; it describes the same set | Same as above |
Before the workshop, it is recommended to do one thing first: log in to the Console → Settings → Data Controls → turn on Zero Data Retention. It takes 10 seconds, and it will apply to all subsequent requests.
🔴 But—once ZDR is on, none of the red lines disappear. The policy isthe vendor's promise, not a technical guarantee that "your files never left your computer." In between there is still the network, still your computer, and still the question of whether you will later send the srt file to a LINE group. There is only one truly safe approach: never upload that file in the first place.
What if you really must process it?
Some content simply cannot go to the cloud, but you really need a transcript. Three paths:
- Have the person involved do it themselves: let the student/parent type it themselves using their phone's voice input; you only receive the text
- Run it locally: run an offline speech recognition model on your own computer (no internet connection, no upload). This pathis outside the scope of this workshop, and also outside this package
- Don't do it: this is a completely reasonable option. The 20 minutes saved are far less important than a single personal data incident
Where this package fits in the workshop: Work ⑤ "Turn an API into your own tool"
Third day, morning. What this package does is an "internal tool"—only you use it on your computer, and it does not touch web pages yet.
The previous packages were about "plugging in things others have already made" (NotebookLM, Obsidian, Supabase). This package is different:
You will take a raw API and make something you can keep using from then on.
This is the first time in the whole workshop that you "call an external service, get results back, and wrap it into a tool." Once you learn this pattern, connecting any service afterward follows the same logic.
🔑 There is only one criterion for whether this package succeeds
| Run it once | Turn it into a tool | |
|---|---|---|
| What you do | Paste a string of commands and see the srt come out | Say one sentence in plain language to OpenCode, and it does it all itself |
| Switch to new material | You have to go back, check the handout, and paste the commands again | Just say the same sentence again |
| Will you still use it next month? | No, because you forgot how to run it | Yes, because you only need to remember that sentence |
| Acceptance for this package | ❌ Not counted as complete | ✅ This counts as complete |
So step six (wrapping it into a Skill) is not a bonus question; it is the main topic of this package. The first five steps are only there to clarify "what that plain-language sentence needs to do behind the scenes."
📌 In the afternoon,
10-netlifywill push this tool one layer further outward:turning it into a web page so others can use it too. This morning, first make "usable for yourself" solid, so in the afternoon there's something to move onto the web.
What this pack will do for you
Two main lines, both using the same key:
| Line | What it does | Input → Output | Services used |
|---|---|---|---|
| A Transcribe subtitles | Turn audio into a subtitle file with timecodes | .mp4 / .mp3 / .m4a → .srt |
Groq's Whisper |
| B Text cleanup | Organize the transcript into a handout you can give students | .srt / .txt → .md |
Groq's text model |
The output of Line A happens to be the raw material for Line B. Together they make:A recording → a handout。
🚨 The four traps in this pack (read them all before you start)
| # | Trap | Symptom | One-line fix |
|---|---|---|---|
| ① | Groq doesn't have response_format=srt |
Follow OpenAI's tutorial and you get back 400 |
Take verbose_json,and build the srt yourself(small program included in Step 3) |
| ② | Free plan has a 25 MB per-file limit | A 40-minute video gets rejected outright | First use ffmpeg to compress to 16k mono; if over 25 MB, split into segments |
| ③ | Whisper's Chinese doesn't distinguish Traditional from Simplified | After all that work, the subtitles come out in Simplified | language=zh can only go to "Chinese"; Traditional needs separate handling (Step 3 A5) |
| ④ | After splitting, you have to add the offset to the timecodes yourself | The second segment's subtitles all start from 00:00, and the whole thing doesn't match the video | Usethe actual length of each segmentand accumulate; don't use the nominal "10 minutes" |
Trap ① is the most time-consuming one—95% of Whisper tutorials online are written for OpenAI, which has srt; Groq doesn't. You'll think you made a typo.
Free plan (no credit card required)
⚠️ The following are figures found by 2026-07-27 .Providers can adjust them at any time; refer to the official page(at the end of this section you'll learn how to check for yourself).
Whether a card is required
| Question | Answer (verified 2026-07-27) |
|---|---|
| Does signing up require a credit card | No. Sign up with Google/GitHub/Email and you can generate a key right away |
| Does free usage require a credit card | No. The free plan is "rate-limited," not "time-limited"; there's no such thing as a trial expiration |
| When is a card actually needed | Only when upgrading to the Developer plan. The official docs state it clearly: upgrading requires "a valid payment method (credit card, US bank account, or SEPA debit account)" |
🔴 Rule for the workshop: at no point in the whole process will any screen ask you to enter a card number. If you see a screen asking for a payment method, it means youhave entered the upgrade flow——click Back to exit, do not enter. This whole main track does not need the Developer plan at all.
Free tier (Whisper speech)
| Item | Free plan | Put in terms a teacher would understand |
|---|---|---|
| Max size per file | 25 MB | After compressing to 16k mono FLAC, about 25–30 minutesof audio |
| Requests per minute | 20 | Running 20 files at once is no problem |
| Requests per day | 2,000 | You won't use it up |
| Audio seconds per hour | 7,200 seconds | You can transcribe 2 hours of audio per hour |
| Audio seconds per day | 28,800 seconds | You can transcribe 8 hours of audio per day |
| Shortest audio | 0.01 seconds | — |
| Minimum billable length | 10 seconds | ⚠️ See below |
⚠️ The "minimum billing of 10 seconds" rule bites. It means:No matter how short the file, it eats at least 10 seconds of quota. So "cut a class into 200 small clips and send each one separately" is the most wasteful approach—200 3-second files will be counted as 2,000 seconds. The right approach: cut into large segments (10 minutes per segment), not small ones.
(The paid Developer plan has a 100 MB per-file limit. This package does not take that route; this is written here just so you won't be confused when someone else's tutorial says 100 MB.)
Free tier (text models, used by track B)
| Model | Requests per minute | Requests per day | Tokens per minute | Tokens per day |
|---|---|---|---|---|
llama-3.3-70b-versatile |
30 | 1,000 | 12,000 | 100,000 |
openai/gpt-oss-120b |
30 | 1,000 | 8,000 | 200,000 |
llama-3.1-8b-instant |
30 | 14,400 | 6,000 | 500,000 |
🚨 Here is a pitfall no one talks about but you will definitely hit:
The "tokens per minute" is astonishingly small. A one-hour Chinese speech transcript is roughly 9,000–12,000 characters, which converts tofar exceeding the 12,000-token per-minute limit.
In other words:You cannot throw an entire transcript at it for cleaning all at once. You will definitely get
429。Solution: send it in segments. The little program in step five already has built-in segmentation (default 1,200 characters per segment) and automatic retries, so just run it as is.
How to look up the latest yourself (models get renamed, quotas change)
| What you want to look up | Where to look it up |
|---|---|
| What models exist now(most reliable) | Run in the terminal:curl.exe https://api.groq.com/openai/v1/models -H "Authorization: Bearer $env:GROQ_API_KEY" |
| Model description page | https://console.groq.com/docs/models |
| My account's actual limits | Console → Settings → Limits(what you see here is yours; the docs are general) |
| Speech file size limit | https://console.groq.com/docs/speech-to-text |
| Data policy | https://console.groq.com/docs/your-data |
📌 This package deliberately does not list any prices. The free plan doesn't need them, and hard-coded prices will only become wrong information six months later. The same goes for model names: if any model name below no longer exists when you run it,first run the line above
/v1/modelsto see what it has been renamed to, don't force-fit this document.
Which Whisper model to choose: the workshop main line uses whisper-large-v3
Note: the author's
audio-to-srtand the youtube-workflow skill default towhisper-large-v3-turbo(for speed; word-level timestamps are still available). Both work; the reasons this package recommends it are below.
There are currently two on Groq (verified 2026-07-27):
| Model | Officially stated accuracy | Speed | Languages |
|---|---|---|---|
whisper-large-v3 |
Word error rate 10.3% | 189x real-time | Multilingual (including Chinese) |
whisper-large-v3-turbo |
Word error rate 12% | 216x real-time | Multilingual (including Chinese) |
🔴 Recommended:whisper-large-v3. The reason is a fact many people haven't noticed—
On the free plan, these two models have exactly the same quota(both are 20 requests/min, 7,200 audio seconds/hour, 28,800 audio seconds/day).
turbo's selling point is that it's "cheaper."But the free plan doesn't cost anything anyway, so that selling point amounts to zero here.
And how much does the speed differ? 189x vs 216x. For a 30-minute recording, one takes 9.5 seconds and the other takes 8.3 seconds.You won't feel it at all.
Conclusion:Free users have no reason whatsoever to choose turbo. Use the accurate one.
When to switch to turbo: when you need to run dozens of files at once and are actually sitting there waiting; or when you later upgrade to paid and start caring about cost.
📌 About Traditional Chinese performance:Groq has not published any Chinese-specific accuracy figures, and Whisper officially only has overall multilingual figures. The basis for this package's recommendation of
whisper-large-v3is "lower overall word error rate" + "same free-plan quota,"notany actual Chinese-language comparison test. In practice, both models are passable for Taiwanese Mandarin; the main problem isn't misheard words, it'sproper nouns(names, place names, textbook terminology) andTraditional vs. Simplified—both of these are solved by the cleanup in Step 5, and switching models doesn't help much.
Which text model to choose: the main line uses llama-3.3-70b-versatile
| Model | Why choose / not choose |
|---|---|
llama-3.3-70b-versatile |
✅ Main line. Chinese quality is more stable among free models, andthe 12,000 tokens-per-minute limit is the largest among free plans—cleaning transcripts is exactly what consumes this |
openai/gpt-oss-120b |
🟡 Backup. Use this when the main line one goes down or is renamed |
llama-3.1-8b-instant |
🟡 Use when volume is high. The daily 500,000 tokens is the most generous, but Chinese quality is noticeably weaker, suitable for "bulk rough cleanup, then review it yourself afterward" |
⚠️ Model nameswill change. Before running, use
/v1/modelsto confirm once (see "How to check the latest yourself" above).
🤔 Wait—why not just have OpenCode do the text cleanup directly?
This is a question someone will definitely ask at the workshop, andit's a very good question。
You're already in 01-connect-model Connected OpenCode to a model. To clean up transcripts, throw it the file and say "help me turn this into lecture notes," and it's done.Why bother applying for a separate Groq API key and writing another program?
The answer is three things:
| Reason | Explanation |
|---|---|
| ① This pack is teaching the "pattern," not "cleaning" | Calling an API to get results is a pattern that works for any service afterward. Cleaning is just the practice exercise |
| ② There's no alternative for the audio path | OpenCode's modelcan't understand audio files. Track A has no choice but to go through the API, and Track B uses the same key and the same approach, so learning it in sequence is the easiest |
| ③ This afternoon it has to become a web page | 10-netlify will put this tool on the internet for others to use.A web page can't call the OpenCode on your computer, it can only call an API. The program you write today can be moved straight up there this afternoon |
💡 So:For everyday personal use, just asking OpenCode to clean it is totally OK, you don't have to go through the API every time. But this pack wants you to go through the API once so that you have something that "can run even outside OpenCode."
Prerequisites
- Completed
00-env-setupand01-connect-model - Completed
02-file-toolkitSection B(need to useffmpegandffprobeto compress, split files, and measure length) - Recommend completing
07-github(keeping keys from leaking is fully explained in that pack) - Have internet
- Have on hand aone that meets the privacy red lines aboveaudio or video file
Verify:
ffmpeg -version; ffprobe -version
Printing both versions = pass.
⚠️
ffprobeis often missing. If you only installed the FFmpeg that comes with yt-dlp,ffprobemay not be on the PATH. Install the system version:winget install --id Gyan.FFmpeg -e(if you don't have admin rights, add--scope user)。 mac:brew install ffmpeg/Linux:sudo apt install -y ffmpeg. If you really can't install it, there's still hope—the little program in step four has a fallback method, see that section.
📁 First create a working folder, use an alphanumeric name
New-Item -ItemType Directory -Force "$HOME\Documents\groq-subtitle"
🚨 Don't use Chinese for the folder and file names at first. Windows command line (PowerShell 5.1) sometimes garbles Chinese filenames when passing them to
ffmpeg、curl, and you'll see "file not found" even though the file is clearly right there. Method: before conversion, first change the source to a name likelesson01.mp4, then change it back to Chinese after everything is done. This one looks trivial, but at every workshop session someone gets stuck here for half an hour.
Step One: Apply for a Groq API Key
🖐️ Do everything manually in the browser.The AI agent should not register on your behalf or fill in any fields.
1-1 Register
- Open https://console.groq.com
- choose Continue with Google or Continue with GitHub(recommend using the
07-githubGitHub account, easier to manage later), you can also use Email - No waitlist, no review,won't ask for your credit card
1-2 Create a Key
Left sidebar menu API Keys → Create API Key → pick a name you can understand (e.g. 研習-字幕工具) → created.
🔴 The key will only be shown in full this one time. After you close the windowyou won't see it a second time, and you can only delete it and generate a new one. Copy it now and paste it into your password manager(not into Notepad, not into LINE sent to yourself).
The key looks like a long string starting with gsk_ .
1-3 Also turn on Zero Data Retention
Settings → Data Controls → turn on Zero Data Retention。
It takes 10 seconds, and it applies to all future requests. (Again:turning it on won't make any of those red lines disappear.)
1-4 Verify: is the key alive
First finish step two and set up the environment variable, then come back and run this line (it's in the verification part of step two).
Step two: where should the key go (🚨 if you get this step wrong, the key will end up on GitHub)
🔴 Three absolute rules
① The key must not be written into any
.mdfile — including the Skill file you're about to make. ② The key must not be written into any file that will begit add. ③ The key must not be pasted into a chat window(including showing it to an AI agent).
Reason: the workshop's config files will be projected on a big screen and photographed; and on GitHub there are bots specifically scanning newly pushed keys,and they scan faster than you can notice。
Main approach: set it as an environment variable (recommended, set it once for the whole computer)
The advantage of environment variables is:the key doesn't exist in any file, so it can never be committed.
Windows(PowerShell)
[System.Environment]::SetEnvironmentVariable("GROQ_API_KEY", "貼上你的金鑰", "User")
After setting ityou must restart the terminal(and completely close and reopen OpenCode too), so the new environment variable takes effect.
macOS / Linux(write it into ~/.zshrc or ~/.bashrc, then source once)
echo 'export GROQ_API_KEY="貼上你的金鑰"' >> ~/.zshrc
source ~/.zshrc
Verify (🔴 this line deliberately doesn't print the key itself, so it's safe to project)
Windows
if ($env:GROQ_API_KEY) { "KEY_SET len=" + $env:GROQ_API_KEY.Length } else { "KEY_MISSING" }
macOS / Linux
if [ -n "$GROQ_API_KEY" ]; then echo "KEY_SET len=${#GROQ_API_KEY}"; else echo "KEY_MISSING"; fi
Seeing KEY_SET len=<某個數字> = pass. Seeing KEY_MISSING → you didn't restart the terminal, or you typed the variable name wrong.
Verify the key is alive
curl.exe https://api.groq.com/openai/v1/models -H "Authorization: Bearer $env:GROQ_API_KEY"
- It returns a big chunk of JSON (in which you can see
whisper-large-v3)→ ✅ the key works, and you can also see which models are currently available - It returns
invalid_api_key→ a chunk was missing when you copied the key, or there's extra whitespace before or after it; copy it again
On Windows you must type
curl.exe(with.exe), otherwise it will call PowerShell's built-in alias, which behaves completely differently.
Project approach:.env + .gitignore(the afternoon's 10-netlify will use it)
If your tool needs to go into a "project folder that will be pushed to GitHub", use .env:
First create .gitignore(🔴 create this first, then create .env; the order can't be reversed)
.env
*.key
credentials.*
desktop.ini
*.tmp
Then create .env
GROQ_API_KEY=貼上你的金鑰
Verify that .env is really being blocked:
git status --short
🔴 If
.envappears in the output, it's a failure— it means git can still see it, and the nextgit add .will send the key to GitHub. Go back and check whether.gitignoreis misspelled, or whether it's in the repo root directory.
Stricter check (agents, run this line):
git check-ignore -v .env
If there is output (it will tell you which .gitignore line blocked it) = ✅ pass. No output = ❌ not blocked.
📌 About Google Drive:
.envplaced in a Google Drive folder will be synced to your cloud account—this is usually acceptable (that is your own account). But it and.gitignoreare two different things, cloud sync will not block git. Both things need to be done.
If you want to, in opencode.json , reference the key
this packagedoes not needthis (we call the API ourselves, not connect Groq as OpenCode's model provider). But just in case you later want to:
"environment": { "GROQ_API_KEY": "{env:GROQ_API_KEY}" }
🔴 write
{env:GROQ_API_KEY}, do not write the actual string. This is00-env-setupappendix "MCP General Rules" Article 5, and this package follows it completely.
🚨 In case the key accidentally leaks
Delete the file, delete the message, clear the git history—none of it works. The key is already in someone else's hands.
The only effective remedy: go back to Console → API Keys → delete that key and generate a new one。
Before this is done, there is nothing else to do. After it is done, remember to also change the environment variable to the new one.
Step 3: Route A — Turn audio into subtitles (
.srt)
A1. First compress the file to make it smaller (🚨 this is where trap ② is handled)
Free plan single-file limit 25 MB. A 30-minute video recorded on a phone easily runs to several hundred MB, and sending it directly will definitely be rejected.
The officially recommended approach: reduce to 16kHz mono, convert to FLAC. Doing thiswill not affect recognition accuracy—Whisper only takes 16kHz anyway, and the extra sample rate and second channel would be discarded when sent.
The command given in Groq's official docs (unchanged):
ffmpeg -i "lesson01.mp4" -ar 16000 -ac 1 -map 0:a -c:a flac "clean.flac"
| Parameter | Meaning |
|---|---|
-ar 16000 |
Reduce sample rate to 16kHz (Whisper's native spec) |
-ac 1 |
Merge to mono |
-map 0:a |
Audio only, discard the video entirely (95% of a video file's size is video) |
-c:a flac |
Convert to FLAC (lossless compression) |
After compressing, check the size:
"{0:N1} MB" -f ((Get-Item "clean.flac").Length / 1MB)
# macOS / Linux
du -h clean.flac
Rule of thumb: 16k mono FLAC is about 0.8–1 MB per minute. So 25 MB ≈ 25–30 minutes. If it exceeds that, split it → skip to Step 4。
🟡 A fallback if you really do not want to split the file: switch to low-bitrate mp3, which compresses one hour to about 14 MB.
ffmpeg -i "lesson01.mp4" -ar 16000 -ac 1 -map 0:a -c:a libmp3lame -b:a 32k "clean.mp3"The cost is lossy compression, and Chinese accuracy willslightlydecrease.This package's main route still uses FLAC + splitting, and this fallback is only used when time is really short.
A2. Call the API
Windows (PowerShell, the whole thing is one line; it being long is normal)
curl.exe https://api.groq.com/openai/v1/audio/transcriptions -H "Authorization: Bearer $env:GROQ_API_KEY" -F "file=@clean.flac" -F "model=whisper-large-v3" -F "language=zh" -F "temperature=0" -F "response_format=verbose_json" -o "clean.json"
macOS / Linux
curl https://api.groq.com/openai/v1/audio/transcriptions \
-H "Authorization: Bearer $GROQ_API_KEY" \
-F "file=@clean.flac" \
-F "model=whisper-large-v3" \
-F "language=zh" \
-F "temperature=0" \
-F "response_format=verbose_json" \
-o "clean.json"
| Parameter | Why |
|---|---|
model=whisper-large-v3 |
See "Which model to choose" above |
language=zh |
Must be added. The official docs state clearly: specifying the language willimprove both accuracy and speed. If you don't add it, it has to spend time guessing the language first, and classroom recordings that mix Chinese and English are easily guessed wrong |
temperature=0 |
You want it to write what it hears, not improvise freely |
response_format=verbose_json |
The timestamps are in here. Using json or text won't get you timestamps |
-o clean.json |
Save it to a file. If you don't save it, a big pile of JSON will just spew onto the screen |
🚨 A3. Pitfall ①: Don't write response_format=srt
OpenAI's Whisper API can directly return .srt. Groq can't.
The official API documentation states it very clearly—response_format 's Allowed values are only json、text、verbose_json。
If you hit srt or vtt you'll get a 400 error. And 95% of Whisper tutorials online are written for OpenAI, so if you copy them over you'll run right into this wall.
🔴 Just remember this sentence: Groq only gives you "timestamps + text",
.srtyou have to assemble this file yourself.It sounds like a hassle, but it's really just reformatting JSON—the little program below solves it in one go, and it's the same program you'll use later for splitting long files into segments.
A4. Turn verbose_json into .srt
Create make_srt.py(in your working folder. This program doesn't need any packages installed; it uses only Python's built-in):
# make_srt.py — 把 Groq 的 verbose_json 組成 .srt
# 用法:uv run python make_srt.py 輸出.srt 第1段.json 第2段.json ...
# 只有一段就只傳一個 json;多段會自動接上時間碼偏移(見步驟四)
import json, os, subprocess, sys
def fmt(t):
"""秒數 → SRT 的 00:00:00,000 格式"""
ms = int(round(t * 1000))
h, ms = divmod(ms, 3600000)
m, ms = divmod(ms, 60000)
s, ms = divmod(ms, 1000)
return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
def real_seconds(path):
"""用 ffprobe 量音訊檔的實際長度(秒)。量不到就回 None"""
try:
out = subprocess.run(
["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0", path],
capture_output=True, text=True, check=True).stdout.strip()
return float(out)
except Exception:
return None
out_path, json_files = sys.argv[1], sys.argv[2:]
if not json_files:
sys.exit("SRT_FAIL 沒有指定任何 json 檔")
offset, idx, blocks = 0.0, 1, []
for jf in json_files:
with open(jf, encoding="utf-8") as f:
data = json.load(f)
for seg in data.get("segments", []):
text = (seg.get("text") or "").strip()
if not text:
continue
blocks.append(
f"{idx}\n{fmt(seg['start'] + offset)} --> {fmt(seg['end'] + offset)}\n{text}\n")
idx += 1
# 🚨 陷阱 ④:偏移量用「這一段的實際長度」累加,不要用名目的 600 秒
audio = os.path.splitext(jf)[0] + ".flac"
dur = real_seconds(audio) if os.path.exists(audio) else None
if dur is None: # ffprobe 不在?改用 API 回傳的長度
dur = float(data.get("duration") or 0.0)
offset += dur
with open(out_path, "w", encoding="utf-8") as f:
f.write("\n".join(blocks))
print(f"SRT_OK segments={idx - 1} files={len(json_files)} out={out_path}")
Run it:
uv run python make_srt.py "lesson01.srt" "clean.json"
(uv comes from 00-env-setup. If you don't have uv , use python make_srt.py .... This program uses only built-in modules, so any Python 3 can run it.)
What success looks like: it prints SRT_OK segments=<某個數字> files=1 out=lesson01.srt, and segments is not 0.
A5. 🚨 Pitfall ③: What to do when subtitles turn into Simplified Chinese
Whisper's language code only has "Chinese (zh)", there's no "Traditional Chinese". It decides on its own whether to output Traditional or Simplified, and running the same file twice can give different results.
Two paths,it's recommended to do both:
Route A (solved in one go, but Windows has an encoding pitfall): use the prompt parameter to hint it
Whisper's prompt parameter is used to "demonstrate style" (limit 224 tokens).If you write this prompt in Traditional Chinese characters, the probability of it outputting Traditional Chinese increases greatly.
First useNotepadto create prompt.txt(when saving, choose encoding UTF-8), with one line of content:
以下是繁體中文的課堂錄音逐字稿。
Then in the A2 commandadd one parameter at the end:
-F "prompt=<prompt.txt"
🚨 Why use a file instead of writing the Chinese directly in the command? Windows PowerShell 5.1, when passing Chinese parameters to
curl.exe,uses the system's cp950 encoding, and what gets sent out often turns into garbled text—and it won't report an error; you'll just feel like "this prompt seems to have no effect".-F "欄位名=<檔名"is curl syntax, meaning "this field's value is read from a file",reads the raw bytes directly, bypassing the whole encoding problem. macOS / Linux doesn't have this problem, so writing Chinese directly is fine too.
📌 This is a common community practice, not an official guarantee. It works but is not 100%. So you also need Route B.
Route B (zero-risk fallback): let OpenCode only change characters, not touch timestamps
The produced .srt to OpenCode and say:
幫我把這個 .srt 檔的字幕文字全部改成繁體中文(台灣用語),
順便修掉明顯的錯字。
🔴 硬規定:
- 時間碼那幾行(00:00:00,000 --> 00:00:03,120)一個字都不准改
- 字幕的行數與編號不准增減
- 不要合併或拆開任何一段
🔴 The last three items must be stated, and stated as if they are rules. If you don't state them, the AI will very often "helpfully" merge sentences that are too short and re-paragraph on its own—and the result is that the subtitles and the picturedon't match at all, and it looks completely normal; you only find out when you play the whole video.
Recommended approach: do Route A first (less hassle), keep Route B as a fallback (and use it to fix typos along the way).
A6. ✅ Route A acceptance: switch to a new piece of material—can the same sentence be run again
This is the acceptance criterion for this packet, not "a srt was produced."
| # | What to do | What passing looks like |
|---|---|---|
| 1 | Usea playerto open the original video and drag the .srt into it |
Subtitlesmatch the lips(not just "as long as there are subtitles") |
| 2 | Jump to the videovery endCheck | the time of the last subtitleis close to the total video length(A big difference = the timecodes are off; see Step 4) |
| 3 | Switch to a completely different recording, and rerun from A1 | It runs through just the same |
| 4 | 🔴 After Step 6 is done: tell OpenCode, "Help me convert <新檔名> into subtitles」 |
It does everything by itself, and you don't paste any more commands ← Only this item is a real pass |
Item 4 still can't be done right now (the Skill isn't packaged yet).Keep it in mind for now—this is the goal you'll achieve later.
Step 4: How to split long audio files and how to connect the timecodes (🚨 This is where you handle pitfall ④)
You only need this step if it's over 25 MB (about 25–30 minutes). For short files, just skip to Step 5.
Official recommendation vs. this packet's approach
Groq officially recommends "splitting intooverlappingsegments, transcribing each separately, and handling the overlaps when merging," and includes a Cookbook example.
This packet does not take the overlapping route, and the reason is very practical: after overlapping, you have to decide "which of the two repeated pieces of text is correct, and where the seam is"—this is the part of the whole process that is easiest to get wrong and hardest to verify. For a teacher, it's not worth it.
This packet's approach: no overlap, fixed-length chunks, and accumulate timecodes using "each chunk's actual length." The cost is that at the seamsa break may fall in the middle of a sentence(a word or two missing, or an awkward sentence break). But thiswill be cleaned up along the way in Step 5, and compared with "the whole set of timecodes being off," this cost is far cheaper.
4-1 Splitting files (compress first, then split—the order cannot be reversed)
# ① 先壓成 16k 單聲道 FLAC(跟 A1 一模一樣)
ffmpeg -i "lesson01.mp4" -ar 16000 -ac 1 -map 0:a -c:a flac "clean.flac"
# ② 再切成每段 10 分鐘(600 秒)
ffmpeg -i "clean.flac" -f segment -segment_time 600 -c:a flac "chunk_%03d.flac"
will produce chunk_000.flac、chunk_001.flac、chunk_002.flac…
Why 10 minutes? A 10-minute 16k mono FLAC is about 8–10 MB, leaving a lot of safety margin from the 25 MB limita lot of safety margin. Splitting too small is wasteful instead (remember the "minimum billing 10 seconds" rule), and splitting too large will hit the limit.10 minutes is the sweet spot.
Why compress first and then split? This way, every segment has exactly the same specifications, so measuring length and joining timecodes won't cause any surprises. If you split first and then compress, each segment will be processed by ffmpeg separately, and the lengths may differ by a few tenths of a second.
4-2 Calling the API segment by segment
Windows(PowerShell)
Get-ChildItem "chunk_*.flac" | Sort-Object Name | ForEach-Object {
$out = $_.BaseName + ".json"
Write-Host "轉換中:$($_.Name)"
curl.exe https://api.groq.com/openai/v1/audio/transcriptions -H "Authorization: Bearer $env:GROQ_API_KEY" -F "file=@$($_.Name)" -F "model=whisper-large-v3" -F "language=zh" -F "temperature=0" -F "response_format=verbose_json" -o $out
Start-Sleep -Seconds 3
}
macOS / Linux
for f in chunk_*.flac; do
echo "轉換中:$f"
curl https://api.groq.com/openai/v1/audio/transcriptions \
-H "Authorization: Bearer $GROQ_API_KEY" \
-F "file=@$f" -F "model=whisper-large-v3" -F "language=zh" \
-F "temperature=0" -F "response_format=verbose_json" \
-o "${f%.flac}.json"
sleep 3
done
💡 The
Start-Sleep 3/sleep 3in between is intentional: the free plan allows 20 times per minute, so running it this way definitely won't hit the limit. A 1-hour recording will be split into 6 segments, and running all of them takes about 1 minute.
Check that every segment came back:
Get-ChildItem "chunk_*.json" | Select-Object Name, Length
🔴 If any
.jsonis only a few hundred bytes → that segment failed, and what's inside is an error message, not subtitles. Open it with Notepad to see what it says, and compare it with the "Common Sticking Points" at the end of this document.Don't just continue from here, or that segment's subtitles will disappear entirely.
4-3 Merging (🚨 This is trap ④)
Why you can't just concatenate them directly
The timecode returned for each segment starts from 00:00:00 .
In other words, the first sentence of the second segment is said by the API to be at 0 minutes 3 seconds—but itactuallyis at 10 minutes 3 seconds in the whole video. If you directly concatenate the 6 segments, you'll get a file where "the first 10 minutes have 6 sets of subtitles, and the remaining 50 minutes are completely empty."
The correct approach: add an offset. Butyou can't hard-calculate using "10 minutes"
🔴 This is the finest point in this package, and the one most people get wrong:
ffmpeg's-segment_time 600is "approximately 600 seconds," not "exactly 600 seconds"—the actual split point will fall on the audio packet boundary. Each segment differs by a few tenths of a second,and accumulated across six segments that becomes several seconds. The subtitles line up very well at the beginning, and drift more and more toward the end—and this kind of error is the hardest to notice, because the first 10 minutes are completely normal.The correct solution: use
ffprobeto measure the actual length of each segment, and accumulate them one by one.make_srt.pyalready has this built in (real_seconds()that function), so you don't need to calculate it yourself.
Run it directly (feed all the json in order at once):
Windows
$files = (Get-ChildItem "chunk_*.json" | Sort-Object Name).Name
uv run python make_srt.py "lesson01.srt" $files
macOS / Linux
uv run python make_srt.py "lesson01.srt" $(ls chunk_*.json | sort)
🔴 The order absolutely must not be wrong.
chunk_010.jsonmust come afterchunk_002.json. The above usedSort-Object Name/sort, and the filenames are zero-padded to three digits (%03d), so alphabetical sorting is the correct order. If you manually list the filenames yourself, be sure to check the order—if the order is wrong, the entire subtitle file will be so scrambled that no pattern can be seen.
📌
ffprobeThe fallback when it really can't be installed:make_srt.pyWhen it can't measure the length, it will automatically switch to thedurationfield returned by the API. That number is calculated by Groq,and is usually also very accurate, it's just not likeffprobewhich directly measures the file. If you can installffprobe, install it.
4-4 Verify whether the timecodes are off (🔴 Do not skip this step)
This is the only way to catch timecode drift:
| # | What to do | What passing looks like |
|---|---|---|
| 1 | Look at the original video'stotal length | Write it down, for example 58:20 |
| 2 | Open with Notepad .srt,Scroll to the last line |
The last timecodeis very close 58:20(within 5 seconds counts as normal) |
| 3 | Drag .srt into the player and jump tothe last 2 minutes of the video |
subtitlesstill match the lips |
| 4 | Jump toevery seam(10 min, 20 min, 30 min…) | the subtitles around there have no obvious drift |
🔴 Checking only the beginning won't catch this bug. The signature of timecode drift is that "the beginning is completely normal." You must look at the very end.
What to do if it's off:
| Symptom | Cause |
|---|---|
The last line only goes to 10:00 or so |
Only one json was fed in, or the file order got scrambled |
| The further back, the bigger the deviation | ffprobe is not installed, and the API's duration is also inaccurate → install the system version of FFmpeg and run make_srt.pyagain (no need to re-transcribe, the json is all still there) |
| An entire middle section of subtitles disappears | A certain section's .json is actually an error message (go back to 4-2 and check the size of each json) |
Step Five: Route B — Cleaning the transcript into lecture notes
5-1 First turn the srt into plain text
The subtitle file has numbers and timecodes, and those don't need to be sent for cleaning (they'll also needlessly eat up precious token quota)。
uv run python -c "import re,sys;t=open('lesson01.srt',encoding='utf-8').read();lines=[l for l in t.splitlines() if l.strip() and not l.strip().isdigit() and '-->' not in l];open('lesson01.txt','w',encoding='utf-8').write('\n'.join(lines))"
or even simpler—just throw .srt to OpenCode and say "remove the timecodes and numbers, keep only the text, save as lesson01.txt"。
5-2 Create the cleaning program
In the same folder, create clean.py(same as beforeno need to install any packages):
# clean.py — 把逐字稿清洗成可讀講義(Groq Chat Completions)
# 用法:uv run python clean.py 逐字稿.txt 講義.md
import json, os, sys, time, urllib.error, urllib.request
API = "https://api.groq.com/openai/v1/chat/completions"
MODEL = "llama-3.3-70b-versatile" # 會改名!先跑 /v1/models 確認
LIMIT = 1200 # 每段送幾個字。免費方案 TPM 很小,不要調大
KEY = os.environ.get("GROQ_API_KEY")
if not KEY:
sys.exit("CLEAN_FAIL 環境變數 GROQ_API_KEY 沒設,回步驟二")
SYSTEM = (
"你是台灣國中小老師的文字編輯助理。把語音辨識出來的逐字稿,"
"整理成可以直接發給學生看的講義段落。規則:"
"① 一律輸出繁體中文,用台灣的慣用詞;"
"② 刪掉贅詞、口頭禪、結巴、重複、以及講者自己更正掉的前半句;"
"③ 補上標點符號,依語意分段;"
"④ 不要新增原文沒有的內容,不要加自己的解釋;"
"⑤ 聽不出來的地方原樣保留,並在後面加上 [?];"
"⑥ 不要寫開場白或結語,直接輸出整理後的內文。"
)
def split_text(raw, limit):
"""依行切段,盡量不切在句子中間"""
parts, buf = [], ""
def flush():
nonlocal buf
if buf.strip():
parts.append(buf)
buf = ""
for line in raw.splitlines(keepends=True):
while len(line) > limit: # 整篇沒換行的長文,只好硬切
flush()
parts.append(line[:limit])
line = line[limit:]
if len(buf) + len(line) > limit:
flush()
buf += line
flush()
return parts
def call(text):
body = json.dumps({
"model": MODEL,
"temperature": 0.2,
"messages": [{"role": "system", "content": SYSTEM},
{"role": "user", "content": text}],
}).encode("utf-8")
req = urllib.request.Request(API, data=body, headers={
"Authorization": "Bearer " + KEY,
"Content-Type": "application/json",
})
with urllib.request.urlopen(req, timeout=180) as r:
return json.load(r)["choices"][0]["message"]["content"].strip()
raw = open(sys.argv[1], encoding="utf-8").read()
parts = split_text(raw, LIMIT)
out = []
for n, p in enumerate(parts, 1):
print(f"清洗第 {n}/{len(parts)} 段…")
for attempt in range(3):
try:
out.append(call(p))
break
except urllib.error.HTTPError as e:
detail = e.read().decode("utf-8", "ignore")[:300]
if e.code == 429 and attempt < 2: # 額度滿了,等一下再試
print(" 額度滿了,等 35 秒再試")
time.sleep(35)
continue
sys.exit(f"CLEAN_FAIL http={e.code} {detail}")
time.sleep(2) # 段與段之間刻意留間隔
open(sys.argv[2], "w", encoding="utf-8").write("\n\n".join(out))
print(f"CLEAN_OK pieces={len(parts)} out={sys.argv[2]}")
Run it:
uv run python clean.py "lesson01.txt" "lesson01-講義.md"
What success looks like: prints the progress section by section, and finally prints CLEAN_OK pieces=<數字> out=lesson01-講義.md。
5-3 Why split into sections, and why wait in between
| Design | Reason |
|---|---|
LIMIT = 1200(1,200 characters per section) |
Free plan llama-3.3-70b-versatile only 12,000 tokens per minute. Sending a one-hour transcript all at once will definitely 429 |
Between sections sleep 2 |
spreads out the per-minute token usage so it doesn't all burn up in the first 10 seconds |
When you hit 429 wait 35 seconds and retry |
The token quota isper minuteand resets, so it'll recover after a bit |
🔴 Don't turn
LIMITup trying to run faster. Turning it up only makes you hit429sooner, and then you have to wait 35 seconds every time,which actually makes it slower overall。
5-4 If you want a different output, just change the SYSTEM section
clean.py The SYSTEM string in it is "what you want it to do." Change that section and this program becomes a different tool:
| You want | SYSTEM change to |
|---|---|
| Lecture notes (default) | The section above |
| Class key points summary | "Turn the verbatim transcript into 5–8 key points, one per line, in Traditional Chinese…" |
| Student self-study version (spoken to written) | "Rewrite the verbatim transcript into an explanatory text suitable for junior high school students to read on their own…" |
| Extract exam question material | "Find concepts from the verbatim transcript that can be used for questions, and list them…" |
💡 This is what "turning an API into a tool" means—the same program, the same key, change one passage and you get a different tool.
5-5 ✅ Track B acceptance: switch to a new piece of material, can the same sentence run again
| # | What to do | What passing looks like |
|---|---|---|
| 1 | Open lesson01-講義.md |
isa fluent Traditional Chinese paragraph, not a long string of characters without punctuation |
| 2 | Compare with the original verbatim transcript | the meaning has not been changed,and no content has been added that was not in the original |
| 3 | Find a passage where you remember you misspoke and then corrected yourself | The half-sentence before the correctionhas been deleted |
| 4 | Switch to a completely different verbatim transcript, and run it again | it still runs to completion |
| 5 | 🔴 After finishing step six: say to OpenCode, "Help me turn <新檔名> into a handout」 |
it finishes on its own, and you did not paste any more instructions ← Only this counts as truly passing |
⚠️ Item 2 must really be checked. Language models occasionally "help you add" things—what they add looks very professional, but you never actually said it. Before handing out the handoutread it through yourself, and this cannot be outsourced.
Step six: package the whole process into a Skill (🔴 this is the main topic of work ⑤)
The previous five steps are all "run it once."Something you run once will definitely be forgotten next month.
Now turn it into a skill: from now onin any folder, say one plain sentence to OpenCode, and it will finish it on its own.
6-1 Create the skill folder
Windows
New-Item -ItemType Directory -Force "$HOME\.config\opencode\skills\audio-notes"
macOS / Linux
mkdir -p ~/.config/opencode/skills/audio-notes
⚠️ The folder name must match the skill's
nameexactly, and the filename must beall uppercaseSKILL.md, so OpenCode can scan it. (Same rule as08-workflow-skills。)
6-2 Move the two small programs in
Copy-Item "$HOME\Documents\groq-subtitle\make_srt.py" "$HOME\.config\opencode\skills\audio-notes\"
Copy-Item "$HOME\Documents\groq-subtitle\clean.py" "$HOME\.config\opencode\skills\audio-notes\"
🔴 Before moving them, first confirm that these two files do not contain a key. They read
os.environ.get("GROQ_API_KEY"), so they never had one to begin with—but if you just hard-coded the key into them "for testing convenience," now is the time to remove it.
6-3 Create SKILL.md
Write the entire block below verbatim into ~/.config/opencode/skills/audio-notes/SKILL.md
(Do notwrite the outermost ``` fence symbols into the file; the first line of the file must be ---):
---
name: audio-notes
description: 把音訊或影片轉成字幕檔(.srt),以及把逐字稿清洗成可讀講義(.md)。當使用者說「轉字幕」、「上字幕」、「做逐字稿」、「這個錄音幫我轉文字」、「把逐字稿整理成講義」、「把這段錄音變成講義」時,請一定要使用此技能。走 Groq 的 Whisper 與文字模型,金鑰讀自環境變數 GROQ_API_KEY。
---
# 音訊 → 字幕 → 講義
## 🔴 執行前必問(不可跳過)
動手之前,先問使用者這一句,**等到回答再繼續**:
> 「這個音檔裡面**有沒有學生、家長的聲音**,或是輔導、個案相關的內容?
> 有的話這條流程不能用——語音會上傳到雲端。」
- 回答「有」或「不確定」 → **停止,不要呼叫任何 API**,把原因說清楚
- 回答「沒有,只有我自己的聲音」 → 繼續
順便提醒一次:**上傳前先聽檔案的最後 30 秒**,課堂錄音常常錄到下課後的對話。
## 金鑰
金鑰一律從環境變數 `GROQ_API_KEY` 讀取。
- **不准**把金鑰寫進本檔案、寫進任何 `.py`、寫進任何會被 commit 的檔案
- **不准**把金鑰印在終端機上、或複述給使用者看
- 環境變數不存在 → 停下來,請使用者依懶人包 `09-groq-api` 步驟二設定,**不要自己想辦法繞過**
## 模式 A:音訊 → 字幕(.srt)
1. **檢查工具**:`ffmpeg -version`、`ffprobe -version`。缺了就停下來告訴使用者(懶人包 `02-file-toolkit` B 段)
2. **檔名先換成英數字**(中文檔名在 Windows 命令列可能亂碼),全部跑完再改回來
3. **壓縮**:
`ffmpeg -i <來源> -ar 16000 -ac 1 -map 0:a -c:a flac clean.flac`
4. **看大小**。25 MB 以內 → 跳到第 6 步;超過 → 第 5 步
5. **切段**(每段 600 秒):
`ffmpeg -i clean.flac -f segment -segment_time 600 -c:a flac chunk_%03d.flac`
6. **逐段呼叫 API**,每次之間停 3 秒:
端點 `https://api.groq.com/openai/v1/audio/transcriptions`
參數 `model=whisper-large-v3`、`language=zh`、`temperature=0`、`response_format=verbose_json`
輸出存成與音訊同名的 `.json`
- 🚨 **`response_format` 不可以填 `srt` 或 `vtt`**,Groq 不支援,會回 400
7. **檢查每個 `.json`**:檔案太小(幾百 bytes)代表那一段失敗了,是錯誤訊息不是字幕。**先處理再往下**
8. **組 srt**(json 一定要照檔名排序傳入):
`uv run python <本技能資料夾>/make_srt.py <輸出>.srt chunk_000.json chunk_001.json ...`
9. **繁體化**:把產出的 srt 逐句改成繁體中文並修錯字。
🔴 **時間碼那幾行一個字都不准改,行數與編號不准增減,不准合併或拆開任何一段。**
10. **回報**:段數、輸出路徑,並提醒使用者「請拖進播放器確認**最後 2 分鐘**的字幕還對得上」
## 模式 B:逐字稿 → 講義(.md)
1. 來源是 `.srt` → 先去掉編號與時間碼,只留文字,存成 `.txt`
2. 執行:`uv run python <本技能資料夾>/clean.py <逐字稿>.txt <輸出>.md`
3. 拿到 `CLEAN_OK` 才算成功
4. 回報時**一定要加這句**:「語言模型偶爾會補充原文沒有的內容,**發給學生前請自己讀一遍**」
## 常見狀況
| 狀況 | 處理 |
|------|------|
| `400` 且訊息提到 `response_format` | 用了 `srt`/`vtt`,改成 `verbose_json` |
| `413` 或檔案太大 | 沒壓縮,或壓完仍超過 25 MB → 回去切段 |
| `429` | 額度滿了,等 35 秒再試,**不要換模型硬闖** |
| `401` / `invalid_api_key` | `GROQ_API_KEY` 沒設或設錯 → 停下來請使用者處理 |
| 模型名稱不存在 | 跑 `https://api.groq.com/openai/v1/models` 看現在有哪些,不要亂猜 |
| 字幕越後面越對不上 | `ffprobe` 沒裝,`make_srt.py` 量不到每段實際長度。補裝 FFmpeg 後**重跑 make_srt.py 就好,不用重轉** |
## 不該做的事
- ❌ 使用者沒回答隱私那一題就開始上傳
- ❌ 把金鑰寫進任何檔案或印出來
- ❌ 修改 srt 的時間碼
- ❌ 為了「快一點」把清洗的分段字數調大(只會更快撞 429)
- ❌ 一次送幾十個很短的小檔(最低計費 10 秒,額度會白白燒光)
## 注意事項
- 所有訊息使用**繁體中文**
- Windows 用 PowerShell,串指令用 `;` 不要用 `&&`
- Windows 呼叫 curl 要打 `curl.exe`(有 `.exe`)
6-4 Allow this skill
Open (or create)~/.config/opencode/opencode.json, and confirm that permission.skill contains this item:
{
"permission": {
"skill": {
"audio-notes": "allow"
}
}
}
⚠️ If
opencode.jsonalready has content (the MCP settings added in earlier packs,08-workflow-skillsthe three skills added in ), do not overwrite the whole file, just add theaudio-notesMerge this line into the existingpermission.skillblock.
6-5 Restart OpenCode
OpenCode only scans the skills directory at startup. For the folder you just added, you need toclose it completely and reopen itfor it to be recognized (closing the window doesn't count, you need to actually end the process).
6-6 ✅ Final acceptance: whether this pack succeeds, look here
| # | What to do | What passing looks like |
|---|---|---|
| 1 | Say to OpenCode, "list the skills you currently have available」 | The list includes audio-notes |
| 2 | Take abrand-new, never-runrecording, and say, "help me convert <檔名> to subtitles」 |
It compresses, calls the API, assembles the srt, and converts to Traditional Chinese on its own,You didn't paste any commands |
| 3 | Check whether it first asks the privacy question | It asks ← If it didn't ask, go back and check whether SKILL.md was pasted incompletely |
| 4 | Take abrand-newtranscript, and say, "help me turn <檔名> into a handout」 |
It finishes on its own |
| 5 | Open SKILL.md、make_srt.py、clean.py and search for gsk_ |
🔴 Not found in any of the three files ← If found, that's a failure; go back to step two |
🔴 Items 2 and 4 are the watershed for this pack.
If you can do it = you have atool. If you can't = you've onlyrun it once。
The difference shows next month: people with a tool will use it again, people who ran it once have long forgotten.
Common blockers
Keys and accounts
| Symptom | Cause / solution |
|---|---|
KEY_MISSING |
The environment variable didn't take effect.Restart the terminal(OpenCode also needs to be fully closed and reopened) |
401 / invalid_api_key |
A chunk was missing when the key was copied, or there was extra whitespace before or after. Go back to the Console and generate a new one |
| You only realized you didn't copy it after the key screen was closed | It can't be retrieved; you can only delete it and generate a new one. This is by design, not broken |
| A credit card is required at signup | You've walked into the upgrade flow. Press the back button to exit; the free plan doesn't need a card |
| The key was accidentally pushed to GitHub | Deleting the file is useless. Immediately go back to the Console, delete that key, and generate a new one. Then update the environment variable |
Track A (subtitle conversion)
| Symptom | Cause / solution |
|---|---|
400 It mentions response_format |
🚨 pitfall ①. You filled in srt/vtt, Groq doesn't support it. Change to verbose_json, and use make_srt.py to assemble |
413 / file too large |
🚨 pitfall ②. The free plan allows 25 MB per file. Go back to A1 to compress; if it's still too large, split it into segments |
| The output is Simplified Chinese | 🚨 pitfall ③. See the two routes in A5 |
.srt No response when dragged into the player |
The filename must match the video'ssame name(lesson01.mp4 ↔ lesson01.srt), or load it manually in the player |
| All subtitles are concentrated in the first 10 minutes | 🚨 Trap ④. Only one json was fed in, or the file order got messed up. Go back to 4-3 |
| The further along, the greater the deviation | ffprobe is not installed. Install FFmpeg as a supplement,rerun make_srt.py and it's fine; no need to reconvert(the json files are all still there) |
| There is an entire stretch in the middle with no subtitles | That stretch's .json is an error message. Go back to 4-2 and check the size of each json |
SRT_OK segments=0 |
In the json returned by the API there is no segments—it is mostly that response_format was not filled in verbose_json |
| The subtitle content repeats the entire stretch | There is long-term silence in the audio file, a known behavior of Whisper. Cut out the silence and convert again |
ffmpeg says it cannot find the file, but the file is right there |
Chinese filename / spaces in the path.First change it to an alphanumeric filename, or wrap the entire path in double quotes |
| Running one file per sentence, and the quota suddenly ran out | Minimum billing is 10 seconds. Change it to split into 10-minute chunks |
Track B (text cleaning)
| Symptom | Cause / Solution |
|---|---|
429 |
The per-minute token quota is full. The program will wait 35 seconds and retry on its own; if it keeps happening, adjust LIMIT and tune itsmaller(for example, 800) |
CLEAN_FAIL http=404 |
The model was renamed. Run /v1/models to see what it is called now, and change clean.py 's MODEL |
| Content I never talked about appears in the handout | The language model "added it for you." Make SYSTEM 's clause ④ even stronger, or switch to a larger model |
| The output is still Simplified Chinese | SYSTEM Clause ① was ignored. In the user messagewrite it once more at the beginning"Please output in Traditional Chinese (Taiwan)" |
| It runs very slowly | Normal. The free plan requires chunking + waiting. A one-hour transcript is about 10 chunks, about 2–3 minutes |
| Proper nouns are always wrong | They were misheard during the speech recognition stage. List the correct spellings in SYSTEM , for example "Proper nouns in this text: equilateral triangle, circumcenter, incenter" |
Skill and environment
| Symptom | Cause / Solution |
|---|---|
| The skill does not appear in the list | ①The filename must beall uppercase SKILL.md ②The folder name must equal name ③The first line of the file must be ---(do not write the fence symbols in) ④OpenCode was not restarted |
The skill can be invoked but cannot find .py |
SKILL.md must usethe full path of the skill folderto call the program, not a relative path |
uv says it cannot find it |
Go back to 00-env-setup. Or directly use python make_srt.py ..., these two programs only use built-in modules |
ffprobe not found |
Only the FFmpeg bundled with yt-dlp is installed.winget install --id Gyan.FFmpeg -e(without admin rights, add --scope user) |
| PowerShell chained-command syntax error | PowerShell 5.1 not supported &&, use ; |
curl behavior is weird |
On Windows you have to type curl.exe(with .exe), otherwise it calls the PowerShell alias |
| Chinese parameters passed to curl become garbled | PowerShell 5.1 encoding issue. Use -F "欄位=<檔名" reading from a file (see A5) |
opencode.json after saving, OpenCode won't start |
JSON format: braces must be paired, quotes must be the ASCII half-width ", Windows path backslashes written as two \\ |
✅ Success signal (machine-readable)
After running, please output the following block in full, so subsequent agents can directly interpret it.
=== OPENCODE_GROQ_API ===
result=PASS # PASS | PARTIAL | FAIL
os=windows # windows | macos | linux
privacy_confirmed=yes # yes | no(使用者已確認素材不含學生/家長聲音)
key_set=yes # yes | no(環境變數 GROQ_API_KEY 存在)
key_in_files=no # no | yes(SKILL.md 與 .py 內是否搜得到 gsk_)
api_ok=yes # yes | no(/v1/models 回傳正常)
ffmpeg=ok # ok | missing
ffprobe=ok # ok | missing(缺了會退用 API 的 duration)
model_stt=whisper-large-v3 # 實際使用的語音模型
model_llm=llama-3.3-70b-versatile # 實際使用的文字模型
srt_single=OK # OK | FAIL | SKIP(單檔轉字幕)
srt_chunked=OK # OK | FAIL | SKIP(長檔切段合併)
srt_tail_aligned=yes # yes | no(影片最後 2 分鐘字幕仍對得上)
clean_ok=OK # OK | FAIL | SKIP
skill_installed=yes # yes | no(~/.config/opencode/skills/audio-notes/)
skill_listed=yes # yes | no(重啟後 OpenCode 掃得到)
skill_rerun_srt=OK # OK | FAIL(換新素材,一句話跑完 A 線)
skill_rerun_clean=OK # OK | FAIL(換新素材,一句話跑完 B 線)
=== END ===
Decision rules (agents should judge by this, do not decide on your own):
| Result | Condition |
|---|---|
PASS |
skill_rerun_srt=OK and skill_rerun_clean=OK and key_in_files=no |
PARTIAL |
Lines A and B can be run manually, but the skill isn't packaged / it doesn't run through when swapping materials |
FAIL |
api_ok=no, or neither line A nor line B runs |
🔴 The two that directly make the whole thing
FAIL:
key_in_files=yes—— a key appears in any file,no matter how good the other items are, alwaysFAIL。privacy_confirmed=no—— uploading without confirming the material content with the user,alwaysFAIL, and it must be clearly stated in the report.These two are not allowed to "continue first, patch later."
Completion report format
## Groq API 工具化結果
### 隱私確認
- 已向使用者確認素材不含學生/家長/輔導內容:是 / 否
- 已提醒「上傳前聽最後 30 秒」:是 / 否
- Zero Data Retention 已開啟:是 / 否 / 未確認
### 金鑰
- 存放方式:環境變數 / 專案 .env
- `.gitignore` 已擋住 `.env`(`git check-ignore -v .env` 有輸出):是 / 否 / 不適用
- SKILL.md 與 .py 內搜尋 `gsk_`:查無 / ⚠️ 有(立刻撤銷重產)
- 金鑰驗證(`/v1/models`):PASS / FAIL
### A 線:轉字幕
- 使用模型:<實際用的>
- 來源檔長度:<分鐘> / 壓縮後大小:<MB>
- 是否切段:否 / 是(<N> 段,每段 600 秒)
- 產出:`<路徑>.srt`(<N> 句)
- 時間碼驗證(看**最後 2 分鐘**):PASS / FAIL
- 繁體化方式:prompt 參數 / OpenCode 改字 / 兩者都做
### B 線:文字清洗
- 使用模型:<實際用的>
- 分段數:<N>
- 產出:`<路徑>.md`
- 已提醒使用者「發給學生前自己讀一遍」:是 / 否
### 工具化(本包正題)
- 技能位置:`~/.config/opencode/skills/audio-notes/`
- 隨附程式:`make_srt.py`、`clean.py`
- `opencode.json` 權限已加:是 / 否
- 已重啟 OpenCode:是 / 否
- 技能出現在清單:是 / 否
- 🔴 **換新素材、一句話跑完 A 線**:PASS / FAIL
- 🔴 **換新素材、一句話跑完 B 線**:PASS / FAIL
### 待處理
- <把 FAIL 的項目與原因列在這裡;全部 PASS 就寫「無」>
If it's broken, how to start over
Say to OpenCode: "The Groq package is broken, help me clean it up and start over.」
| What you want to redo | How to do it |
|---|---|
| Only one segment was converted badly | Delete that segment's .json, just rerun that segment.Other segments don't need reconverting |
| Timestamps are off | Install ffprobe,Rerun make_srt.py. No need to call the API again, the json is all still there |
| The whole subtitle file needs redoing | Delete chunk_*.json and .srt, rerun from 4-2 |
| The skill wasn't picked up | Check whether the filename is SKILL.md(all uppercase), whether the folder name is audio-notes, whether the first line is ---. After fixing,completely close OpenCode and reopen it |
| You don't want the skill at all anymore | Delete the ~/.config/opencode/skills/audio-notes/ folder, and from opencode.json 's permission.skill remove that line |
| Key leaked | 🔴 Console → API Keys → Delete that one, regenerate a new one → update the environment variable.This takes priority over everything else |
⚠️ Before rerunning the API, think about the quota first: the free plan gives 7,200 audio seconds per hour. Rerunning a 1-hour recording three times uses up half of it.First confirm which step broke, don't redo the whole thing.
Next step
🚀 Afternoon: Turn this tool into a web page →
10-netlifyNow you have a tool that "only your computer can use." The afternoon package will put it online——Other teachers can just open the URL and use it, no need to install OpenCode or apply for an API key.
What you wrote this morning
clean.py, and the lesson learned that "keys go in environment variables, not in files," this afternoon willbe used exactly as is. (Especially that key rule: once the web page is online, if the key is put in the wrong place, it'll bevisible to the whole world. Practice this today, and you won't panic this afternoon.)
Other directions:
- Where the materials come from (download videos, extract audio tracks, grab ready-made subtitles) →
02-file-toolkit's section B - The generated handouts should go into your second brain →
04-obsidian - Tools need to be backed up and carried with you when you change computers →
07-github - If you want "subtitle conversion" to also be caught by the start/end-of-work workflow →
08-workflow-skills
📌 One reminder:
~/.config/opencode/skills/The skill underis not in any project's repo. If you switch computers, this skillwon't come along. To carry it with you, put theaudio-notesfolder into your GitHub too (see07-github)——But remember, it should never contain keys.