Communitygithub.com

wjs-transcribing-audio

Audio/Video → SRT in der Ausgangssprache (Whisper oder Volcano ASR). Schritt 1 der jianshuo-Lokalisierungssuite. 131★.

Was ist wjs-transcribing-audio?

Diese Fähigkeit (Skill) transkribiert Audio- oder Videodateien in eine SRT-Datei (Untertitel) in der gesprochenen Ausgangssprache. Sie ist der erste Schritt der jianshuo-Lokalisierungssuite und erzeugt eine zeitgestempelte Textspur, die dann in anderen Sprachen übersetzt werden kann. Der Skill unterstützt sowohl die OpenAI Whisper API (für die meisten Sprachen) als auch die Volcano (豆包) ASR-Engine von ByteDance (bevorzugt für Chinesisch aufgrund der besseren Genauigkeit). Er vermeidet bekannte Fallstricke von Whisper wie 30-Sekunden-Blob-Cues oder Endlosschleifen-Halluzinationen, indem er Wort-für-Wort-Zeitstempel anfordert und die Cues selbstständig zusammenbaut. Für den Fall, dass keine API verfügbar ist, wird eine lokale Whisper-Installation als letzter Ausweg verwendet. Nach der ASR-Transkription folgt für chinesische Inhalte ein optionaler KI-Korrekturdurchlauf (Claude), um häufige Homophonfehler zu beheben.

Funktioniert mit✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/jianshuo/claude-skills/tree/main/wjs-transcribing-audio

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

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-subtitles springen.
  • 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

AusgangsspracheStandard-EngineBegründung
Chinesisch (zh-CN, zh-HK, zh-TW)Volcano (豆包) ASRWesentlich 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ätWhisper ist mehrsprachig stark; Wort-Zeitstempel ermöglichen eigenständige Cue-Erstellung
Offline / kein API-ZugangLokales 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:

  1. 30-Sekunden-Blob-Cues. Bei langen Monologen gibt whisper-1 mit response_format=srt einen einzigen Cue aus, der das gesamte 30s condition_on_previous_text-Fenster abdeckt. Das Transkript ist in Ordnung; das Timing ist für die Bildschirmanzeige unbrauchbar.
  2. Schleifen-Halluzination bei leisen Enden. Gieriges temperature=0 bei 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 mit set -a; source ~/code/.env; set +a laden.
  • SOCKS-Proxy auf diesem Rechner: httpx benötigt das socksio-Extra – uvx --with httpx --with socksio python ... verwenden (ohne erhält man ImportError: 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=2 ist zuverlässiger als 4.
  • Wiederholung: Jeder API-Aufruf ist mit 5-fachem exponentiellem Backoff umschlossen (time.sleep(min(2**n, 30))) – RemoteProtocolError: Server disconnected ist 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/end jedes Wortes hinzufügen, bevor die Cues assembliert werden.
  • Schleifenschutz (Gürtel und Hosenträger): Selbst mit temperature=0.2 schleift 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=srt nicht 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=0 nicht verwenden bei potenziell leiser Audiospur (Yoga, spirituelle Inhalte, Podcast-Enden). Gierige Dekodierungsschleifen. 0.2 aktiviert 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=zh festlegen).

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}}. NICHT result_type:"single" setzen – das gibt nur den letzten Satz pro Frame zurück; der Standardmodus sammelt alle Äußerungen an.
  • Ergebnisform: result.utterances[], jedes mit text (mit Zeichensetzung) + words[] (Token + ms start_time/end_time). Lateinische Token wie "AI" kommen mit start=end=0 zurück – build_srt_from_asr.py fü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-min Zeichen; 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-chars erhö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_APPID und VOLC_ASR_ACCESS_TOKEN/VOLC_TOKEN; FFMPEG_BIN optional.
    • Die 火山 ASR und TTS dieses Benutzers teilen EINEN Anmeldedatensatz – dieselbe App-ID + Zugriffstoken funktionieren für beide Dienste. Sie sind in ~/code/.env als VOLC_TTS_APPID / VOLC_TTS_ACCESS_TOKEN gespeichert (plus VOLC_APPID in ~/.zshrc), mit angehängten Aliasen VOLC_ASR_APPID / VOLC_ASR_ACCESS_TOKEN, die auf dieselben Werte zeigen. Also set -a; source ~/code/.env; set +a ist 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).
  • 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

Individual skills in this repo

This repo contains 5 individual skills — each has its own dedicated page.

Verwandte Skills