Was macht wjs-transcribing-audio?
Gesprochenes Audio → zeitgestempelte SRT in derselben Sprache. Dieser Skill endet mit der SRT in der Ausgangssprache. Die Übersetzung in eine andere Sprache erfolgt im nächsten Skill (/wjs-translating-subtitles).
Wann verwenden
- Der Benutzer stellt eine Video- oder Audiodatei bereit und wünscht ein Transkript / SRT in der Ausgangssprache.
- Der Benutzer hat bereits eine übersetzte SRT, aber die SRT in der Ausgangssprache fehlt.
- Der Benutzer fragt nach "做 SRT" / "Untertitel erstellen" / "出逐字稿" ohne dass bereits ein Übersetzungsschritt angefragt wurde.
Wann NICHT verwenden
- SRT in der Ausgangssprache existiert bereits → direkt zu
/wjs-translating-subtitlesspringen. - Der Benutzer möchte das Transkript in einer anderen Sprache als der gesprochenen → zuerst diesen Skill ausführen, dann
/wjs-translating-subtitles. - Der Benutzer wünscht nur die Synchronisation oder das Einbrennen → wenn SRT existiert, überspringen; ansonsten zuerst diesen Skill ausführen.
Routing: Welche Engine
| Ausgangssprache | Standard-Engine | Begründung |
|---|---|---|
| Chinesisch (zh-CN, zh-HK, zh-TW) | Volcano (豆包) ASR | Wesentlich bessere Genauigkeit als Whisper für Chinesisch – ständige Präferenz des Benutzers |
| Alle anderen (es, en, pt, fr, it, ja, ko, …) | OpenAI Whisper API mit Wort-Granularität | Whisper ist mehrsprachig stark; Wort-Zeitstempel ermöglichen eigenständige Cue-Erstellung |
| Offline / kein API-Zugang | Lokales openai-whisper (medium) | Qualitätsminimum; gleiche Schleifen-/Blob-Fehlermodi gelten |
Für Chinesisch nicht standardmäßig auf Whisper zurückgreifen, es sei denn, der Benutzer fordert es explizit an oder Volcano ist nicht verfügbar. Dies ist eine bewusste Routing-Entscheidung – siehe Benutzer-Speicher zur Priorität der chinesischen ASR.
OpenAI Whisper API-Pfad (Nicht-Chinesisch und Chinesisch als Fallback)
Das Kernprinzip: response_format=srt nicht anfordern. Die Whisper-Cue-Segmentierung versagt bei langen Monologen (30-Sekunden-Blob-Cues) und leisen Passagen (Schleifen-Halluzinationen). Fordere Wort-für-Wort-Zeitstempel an und baue die Cues selbst zusammen – die Nachbearbeitung ist deterministisch und kostenlos.
Warum nicht response_format=srt
Zwei Fehlermodi, die die whisper-1 SRT-Ausgabe bei langen Inhalten zerstören:
- 30-Sekunden-Blob-Cues. Bei langen Monologen gibt
whisper-1mitresponse_format=srteinen einzigen Cue aus, der das gesamte 30scondition_on_previous_text-Fenster abdeckt. Das Transkript ist in Ordnung; das Timing ist für die Bildschirmanzeige unbrauchbar. - Schleifen-Halluzination bei leisen Enden. Gieriges
temperature=0bei niederenergetischem Audio erzeugt "你如果不把拥抱浪费写在这上面,你很难的" 50 Mal wiederholt.
Beide Probleme entstehen, wenn Whisper die Cue-Grenzen bestimmen darf. Lösung: Wort-für-Wort-Zeitstempel + eigener, zeichensetzungsbewusster Assembler.
API aufrufen
# 1. Für den Upload komprimieren – 64kbps Mono MP3 ist für Sprache völlig ausreichend.
# OpenAI-Limit ist 25 MB pro Anfrage; in 10-Minuten-Stücke aufteilen
# (≈4,5 MB bei 64kbps) für Stabilität unter unzuverlässigen Proxys.
ffmpeg -hide_banner -loglevel error -y \
-ss <start> -t 600 -i input.mp4 \
-vn -ac 1 -ar 16000 -c:a libmp3lame -b:a 64k chunk.mp3
# 2. Wort-für-Wort-Zeitstempel anfordern. NICHT response_format=srt anfordern.
import httpx, os
data = {
"model": "whisper-1",
"language": "es", # Ausgangssprache festlegen; niemals automatisch erkennen
"response_format": "verbose_json",
"timestamp_granularities[]": "word", # ← das entscheidende Flag
"temperature": "0.2", # Fallback-Kette aktivieren (Anti-Schleife)
}
with open("chunk.mp3", "rb") as f:
r = httpx.post(
"https://api.openai.com/v1/audio/transcriptions",
headers={"Authorization": f"Bearer {os.environ['OPENAI_API_KEY']}"},
data=data,
files={"file": ("chunk.mp3", f, "audio/mpeg")},
timeout=600.0,
)
r.raise_for_status()
j = r.json()
words = j["words"] # [{"word": "hola", "start": 0.12, "end": 0.34}, ...]
segments = j["segments"] # siehe Überraschung unten
Überraschung: words[] hat keine Zeichensetzung, segments[] ist inkonsistent
Whispers words[]-Array enthält normalerweise keine Zeichensetzung in word["word"] – jeder Eintrag ist ein reiner Token wie "做", "个", "测", "试". Zeichensetzung, wenn vorhanden, lebt nur im text-Feld von segments[].
Schlimmer noch: Der segments[]-Text ist inkonsistent mit Zeichensetzung über verschiedene Chunks derselben Datei hinweg: Chunk 0 eines 79-minütigen Podcasts könnte 285 reine Segmente ausgeben ("做个测试" "你在" "呵呵") mit jeweils 1-2s ohne Zeichensetzung; Chunk 7 könnte 34 Segmente mit 14-30s Länge mit Zeichensetzung ausgeben. Beide Verhaltensweisen treten in derselben API-Antwort auf.
Die richtige Rezeptur kombiniert daher beide: Verwende segments[] für natürliche Pausengrenzen (bereits am Atem ausgerichtet), behandle sie aber als Rohdaten für deinen eigenen Cue-Assembler, der Wort-Zeitstempel verwendet, um sie überall dort zu teilen, wo die Segmente zu lang sind.
Cue-Assemblierungsrezept
TARGET_DUR = 3.0 # versuche, Cues so lang zu machen
MAX_CUE_DUR = 5.0 # niemals überschreiten
MAX_CHARS = 18 # ~eine Zeile bei Schriftgröße 14 auf 1080 breit, vertikal
MAX_GAP = 1.0 # Stille-Schwelle → Cue-Grenze erzwingen
MIN_PIECE = 0.3 # darunter mit Nachbarn zusammenführen
SPLIT_PUNCT = set(",。!?;,.;!?")
# Schritt A: Kurze segments[] in Richtung TARGET_DUR zusammenführen (segmente,
# nicht Wörter verwenden – Whispers Segmentgrenzen sind bereits
# pausenausgerichtet).
def assemble(segments, offset):
cues, buf = [], []
def flush():
if buf:
cues.append((buf[0]["start"]+offset, buf[-1]["end"]+offset,
"".join(s["text"].strip() for s in buf)))
buf.clear()
for s in segments:
dur = s["end"] - s["start"]
# Einzelnes langes Segment MIT interner Zeichensetzung → eigenständig teilen
if dur > MAX_CUE_DUR and any(c in s["text"] for c in SPLIT_PUNCT):
flush(); cues.extend(split_long_segment(s, offset)); continue
if not buf: buf.append(s); continue
if (s["start"] - buf[-1]["end"]) >= MAX_GAP \
or (buf[-1]["end"] - buf[0]["start"]) >= TARGET_DUR \
or (s["end"] - buf[0]["start"]) > MAX_CUE_DUR:
flush()
buf.append(s)
flush(); return cues
# Schritt B: Abschlussdurchlauf – jedes interne Komma/jeden Punkt in einen eigenen Cue teilen
# (proportionale Zeitstempel nach Zeichenposition). Stücke kürzer als MIN_PIECE
# nach vorne zusammenführen.
# Schritt C: Jeder Cue, der immer noch > MAX_CHARS ist, wird an der größten
# Wortlücke mit words[]-Zeitstempeln geteilt. Rekursiv bis unter die Grenze.
TARGET_DUR und MAX_CHARS an den Leserythmus der Plattform anpassen. Die 18-Zeichen-Grenze ist wichtig fürs Einbrennen auf vertikalen 1080×1920 bei Schriftgröße=14 – längere Zeilen würden zu mehreren unleserlichen Zeilen umgebrochen.
Betriebliche Details
- Authentifizierung: Anmeldedaten liegen in
~/code/.env. Vor dem Aufruf mitset -a; source ~/code/.env; set +aladen. - SOCKS-Proxy auf diesem Rechner:
httpxbenötigt dassocksio-Extra –uvx --with httpx --with socksio python ...verwenden (ohne erhält manImportError: Using SOCKS proxy, but the 'socksio' package is not installed). - Chunking: 10-Minuten-Stücke mit 64kbps Mono MP3 (~4,5 MB pro Stück) sind der Sweet Spot für Zuverlässigkeit. 20-Minuten-Stücke (~9 MB) verursachen manchmal einen RST unter unzuverlässigen Proxys. Gleichzeitigkeit mit
max_workers=2ist zuverlässiger als4. - Wiederholung: Jeder API-Aufruf ist mit 5-fachem exponentiellem Backoff umschlossen (
time.sleep(min(2**n, 30))) –RemoteProtocolError: Server disconnectedist häufig und vorübergehend. - Offset-Zusammenfügung: Die Wörter jedes Chunks kommen mit Zeitstempeln relativ zu diesem Chunk zurück. Beim Zusammenführen den absoluten Start-Offset des Chunks zu
start/endjedes Wortes hinzufügen, bevor die Cues assembliert werden. - Schleifenschutz (Gürtel und Hosenträger): Selbst mit
temperature=0.2schleift gelegentlich ein Sub-Chunk. Nach der Assemblierung einen Schleifendetektor auf den Text jedes Cues anwenden – wenn eine Phrase von 8–40 Zeichen Länge 3+ Mal hintereinander wiederholt wird, den Cue verwerfen.
Anti-Patterns (nicht tun)
- ❌
response_format=srtnicht anfordern für Inhalte länger als ~2 Minuten. - ❌ Schlechte Cues nicht mit einem zweiten API-Aufruf "reparieren". Wenn du Blob-Cues oder Schleifen-Halluzinationen von deinem ersten Aufruf erhalten hast, wiederhole es einmal mit Wort-Granularität – transkribiere nicht nur den defekten Unterbereich neu.
- ❌
temperature=0nicht verwenden bei potenziell leiser Audiospur (Yoga, spirituelle Inhalte, Podcast-Enden). Gierige Dekodierungsschleifen.0.2aktiviert die Fallback-Kette. - ❌
language=...nicht überspringen. Die automatische Erkennung wechselt gelegentlich Chinesisch→Japanisch oder Spanisch→Portugiesisch auf den ersten 30 Sekunden, und das gesamte Transkript ist dann falsch.
Volcano (豆包) ASR-Pfad – bevorzugt für Chinesisch
Volcano ASR übertrifft Whisper routinemäßig bei der Mandarin-Genauigkeit (Erkennungsrate, Zeichensetzung, benannte Entitäten). Dies als Standard für zh-*-Ausgangssprache verwenden.
⛔ NIEMALS 飞书妙记 / lark-minutes für ASR verwenden. Es gibt nur Turn-Level (Sprecherwechsel)-Zeitstempel, keine Wort-für-Wort-Zeitangaben – darauf basierende Untertitel driften um Sekunden ab und brechen an unnatürlichen Stellen. Die ständige Regel des Benutzers: "以后不要用飞书妙记来做ASR,不能作为SRT使用,记住". Es gibt keinen 飞书妙记-Fallback. Wenn Volcano nicht verfügbar ist, auf den OpenAI Whisper-Wort-für-Wort-Pfad oben zurückfallen (Sprache =
language=zhfestlegen).
Die STREAMING WebSocket-API verwenden – pusht Bytes, benötigt KEINE öffentliche URL
Die File / 录音文件识别 / MediaKit-APIs benötigen alle eine öffentlich erreichbare HTTP(S)-Audio-URL. Der Benutzer hat das Hosten von URLs abgelehnt ("不要再用什么从服务器端去 download 的这 mp3 这样的模式" – Tunnel scheitern in seinem Hotspot). Die 大模型流式语音识别 (Bigmodel Streaming)-API umgeht die URL vollständig, indem sie rohe PCM-Bytes über einen WebSocket pusht. Dies ist der funktionierende Pfad. Sie gibt Wort-für-Wort-Millisekunden-Zeitstempel zurück.
Gebündelte Skripte (diese verwenden – sie sind der verifizierte funktionierende Pfad):
# 1. Transkribieren: Pusht 16k Mono PCM-Bytes über WebSocket → ASR-JSON
# (dekodiert jede Eingabe über ffmpeg; .pcm wird direkt durchgereicht)
export VOLC_ASR_APPID=… VOLC_ASR_ACCESS_TOKEN=… # Anmeldedaten liegen beim Benutzer
python3 scripts/volc_asr_stream.py <clip.mp4|wav|mp3|pcm> <out.asr.json>
# 2. Baue eine saubere, wortgetakte SRT aus dem ASR-JSON
python3 scripts/build_srt_from_asr.py <out.asr.json> <out.srt> [max_chars=18]
# Segmentierungsknöpfe (optional):
# --max-chars N erhöhen, um Cues ganz zu halten / an Zeichensetzung zu teilen, nicht mitten im Satz
# --soft-min N min Zeichen vor einem ,、; einen Cue auslöst (Standard 8)
# --strip-punct ALLE Zeichensetzung aus dem angezeigten Text entfernen (sauberes Untertitel-Aussehen)
- Endpunkt:
wss://openspeech.bytedance.com/api/v3/sauc/bigmodel - Header:
X-Api-App-Key:{appid},X-Api-Access-Key:{token},X-Api-Resource-Id:volc.bigasr.sauc.duration,X-Api-Connect-Id:{uuid} - Binäres v3-Framing: 4-Byte-Header
[(ver<<4)|hdrsize, (msgtype<<4)|flags, (ser<<4)|comp, 0]. Full-client(0x1)+POS_SEQ sendet gzipptes JSON-Konfig; audio(0x2)-Pakete senden gzippte PCM-Blöcke (200ms = 6400 Bytes @16k Mono s16le); letztes Paket verwendet NEG_WITH_SEQ(0x3) mit negativer Sequenznummer. Server-Full-Response(0x9) gibt gzipptes JSON zurück. - Konfig-JSON:
{user:{uid}, audio:{format:"pcm",rate:16000,bits:16,channel:1,codec:"raw"}, request:{model_name:"bigmodel", enable_punc:true, enable_itn:true, show_utterances:true}}. NICHTresult_type:"single"setzen – das gibt nur den letzten Satz pro Frame zurück; der Standardmodus sammelt alle Äußerungen an. - Ergebnisform:
result.utterances[], jedes mittext(mit Zeichensetzung) +words[](Token + msstart_time/end_time). Lateinische Token wie "AI" kommen mitstart=end=0zurück –build_srt_from_asr.pyfüllt vorwärts/rückwärts von Nachbarn auf. - build_srt_from_asr.py macht auch: Teilt Cues an Zeichensetzung (HART
。!?leert; WEICH,、;leert nach--soft-minZeichen; harte Obergrenze--max-chars), entfernt optional alle Zeichensetzung aus der Anzeige (--strip-punct), verwirft 呃/嗯/唉-Füllwörter und kollabiert unmittelbare doppelte kurze Token (才才→才). Jeder Cue ist durch sein erstes/letztes Wort getaktet, sodass er exakt auf dem gesprochenen Audio sitzt – kein Drift. Um Unterbrechungen mitten im Satz zu vermeiden,--max-charserhöhen, sodass die Grenzen auf Zeichensetzung und nicht auf die Zeichenobergrenze fallen. - Transkription pro Clip: Transkribiere das eigene Audio jedes Clips (16k Mono PCM), sodass die SRT-Zeitstempel clip-relativ sind.
- Anmeldedaten: Liegen beim Benutzer (豆包语音引擎). Von den Skripten akzeptierte Umgebungsvariablen:
VOLC_ASR_APPID/VOLC_APPIDundVOLC_ASR_ACCESS_TOKEN/VOLC_TOKEN;FFMPEG_BINoptional.- Die 火山 ASR und TTS dieses Benutzers teilen EINEN Anmeldedatensatz – dieselbe App-ID + Zugriffstoken funktionieren für beide Dienste. Sie sind in
~/code/.envalsVOLC_TTS_APPID/VOLC_TTS_ACCESS_TOKENgespeichert (plusVOLC_APPIDin~/.zshrc), mit angehängten AliasenVOLC_ASR_APPID/VOLC_ASR_ACCESS_TOKEN, die auf dieselben Werte zeigen. Alsoset -a; source ~/code/.env; set +aist genug – kein separates ASR-Token, nach dem gesucht werden muss. Wenn die Aliase jemals fehlen, lege sie aus den TTS-Werten neu an (es ist dasselbe Geheimnis).
- Die 火山 ASR und TTS dieses Benutzers teilen EINEN Anmeldedatensatz – dieselbe App-ID + Zugriffstoken funktionieren für beide Dienste. Sie sind in
- Sackgassen (nicht erneut versuchen): MediaKit asr-subtitles (Dokumentation 6448/2381968) benötigt immer noch eine öffentliche URL und einen separaten MediaKit-API-Schlüssel. Die 录音文件识别 Datei-API benötigt eine öffentliche URL.
Lokales Whisper als letzter Ausweg
Nur wenn offline, das API-Kontingent erschöpft ist oder für extrem günstige Rohentwürfe. Die Qualität ist für Chinesisch deutlich geringer; dieselben Blob-/Schleifen-Fehlermodi gelten; lokales Whisper gibt keine Wort-für-Wort-Zeitstempel über die CLI aus, sodass die prinzipielle Lösung nicht verfügbar ist.
ffmpeg -i input.mp4 -vn -ac 1 -ar 16000 -c:a pcm_s16le _audio.wav -y
uvx --from openai-whisper whisper _audio.wav \
--language zh --task transcribe \
--model medium --output_format srt --output_dir .
rm _audio.wav
medium ist die praktische Untergrenze für die chinesische Genauigkeit; small ist nur für sauberes Studio-Englisch in Ordnung. Whisper schreibt . Millisekunden; die Datei ist immer noch gültiges SRT. Wenn die SRT neu generiert wird, immer , ms ausgeben.
KI-Korrekturdurchgang – offensichtliche 错别字 (Tippfehler) korrigieren (letzter Schritt, immer für Chinesisch ausführen)
Selbst Volcano ASR liefert klare Homophonfehler, die auf dem Bildschirm falsch gelesen werden: beobachtet – 「总数」→「意数」, 「需求」→「虚求」, 「程序员」→「成员」. Roher ASR-Text ist das Minimum, nicht das Maximum. Nachdem die SRT assembliert ist, einen Claude-Korrekturdurchgang über die gesamte SRT durchführen, um offensichtliche Fehler unter Verwendung des Satzkontexts zu korrigieren.
Dieser Durchgang wird innerhalb der Sitzung von Claude durch Lesen der SRT durchgeführt – kein externer API-Aufruf. Die .srt-Datei an Ort und Stelle umschreiben (oder als <stem>.polished.srt).
Harte Regeln – dies ist Korrektur, kein Editieren:
- ✅ Korrigiere nur eindeutige Homophon-/ 错别字-Fehler, bei denen das beabsichtigte Wort aus dem Kontext eindeutig ist (意数→总数, 虚求→需求).
- ⛔ **NIEMALS Zeitstempel, Cue