Communitygithub.com

wjs-transcrição-áudio

Áudio/vídeo → SRT no idioma de origem (Whisper). Passo 1 do conjunto de localização jianshuo.

O que é wjs-transcrição-áudio?

Este skill de agente de IA (wjs-transcribing-audio) é o primeiro passo do conjunto de localização jianshuo, projetado para transcrever áudio ou vídeo em legendas SRT no mesmo idioma da fala, sem tradução. Ele suporta múltiplas plataformas de IA, como Claude Code, Cursor e Codex, e é ideal para criadores de conteúdo, editores de vídeo e tradutores que precisam de transcrições precisas no idioma original antes de qualquer etapa de tradução. O skill implementa uma rota inteligente baseada no idioma de origem: para chinês, usa a API Volcano ASR (豆包), que oferece precisão superior; para outros idiomas (espanhol, inglês, português, etc.), utiliza a OpenAI Whisper API com timestamps no nível de palavra para evitar os problemas comuns do formato SRT, como 'blobs' de 30 segundos e alucinações em loop. Em ambientes offline ou sem acesso a APIs, recorre ao Whisper local (modelo médio) como alternativa. O processo de pós-processamento é meticuloso: monta as legendas a partir de timestamps de palavra, dividindo em blocos de ~3 segundos (máximo 5 segundos) com base em pontuação e pausas. Para saídas em chinês, inclui uma etapa de correção ortográfica usando o próprio Claude, que corrige erros homofônicos óbvios sem alterar timestamps ou quebrar legendas existentes. A saída final é um arquivo SRT limpo e sincronizado, pronto para uso ou tradução subsequente.

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

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que wjs-transcrição-áudio faz?

Áudio falado → SRT com timestamps no mesmo idioma. Este skill para no SRT do idioma de origem. A tradução para outro idioma é o próximo skill (/wjs-translating-subtitles).

Quando usar

  • O usuário fornece um arquivo de vídeo ou áudio e deseja uma transcrição / SRT no idioma de origem.
  • O usuário já tem um SRT traduzido, mas o SRT original está faltando.
  • O usuário pede "fazer SRT" / "criar legendas" / "出逐字稿" sem solicitar tradução.

Quando NÃO usar

  • O SRT no idioma de origem já existe → pule direto para /wjs-translating-subtitles.
  • O usuário quer a transcrição em um idioma diferente do falado → execute este skill primeiro, depois /wjs-translating-subtitles.
  • O usuário quer apenas dublagem ou queima de legenda → se o SRT existir, pule; caso contrário, execute este primeiro.

Roteamento: qual mecanismo

Idioma de origemMecanismo padrãoPor quê
Chinês (zh-CN, zh-HK, zh-TW)Volcano (豆包) ASRPrecisão materialmente melhor que Whisper para chinês — preferência constante do usuário
Qualquer outro (es, en, pt, fr, it, ja, ko, …)API OpenAI Whisper com granularidade de palavraWhisper multilíngue é robusto; timestamps de palavras permitem montar as legendas manualmente
Offline / sem acesso a APIopenai-whisper local (medium)Qualidade mínima; mesmos modos de falha de blob/loop se aplicam

Para chinês, não use Whisper como padrão, a menos que o usuário peça explicitamente ou Volcano esteja indisponível. Esta é uma decisão de roteamento deliberada — veja a memória do usuário sobre prioridade de ASR chinês.

Caminho da API OpenAI Whisper (não-chinês e fallback chinês)

O princípio fundamental: não solicite response_format=srt. A segmentação de legendas do Whisper falha em monólogos longos (legendas blob de 30 segundos) e em trechos silenciosos (alucinações em loop). Solicite timestamps no nível de palavra e monte as legendas você mesmo — o pós-processamento é determinístico e gratuito.

Por que não usar response_format=srt

Dois modos de falha que prejudicam a saída SRT do whisper-1 em conteúdo longo:

  1. Legendas blob de 30 segundos. Em monólogos longos, whisper-1 com response_format=srt emite uma única legenda cobrindo toda a janela de 30s de condition_on_previous_text. A transcrição está correta; o tempo é inutilizável para leitura na tela.
  2. Alucinação em loop em caudas silenciosas. temperature=0 ganancioso em áudio de baixa energia produz "você não pode colocar esse abraço desperdiçado aqui, é muito difícil" repetido 50 vezes.

Ambos decorrem de deixar o Whisper decidir os limites das legendas. Solução: timestamps no nível de palavra + seu próprio montador com reconhecimento de pontuação.

Chamando a API

# 1. Comprimir para upload — MP3 mono 64 kbps é suficiente para fala.
#    Limite da OpenAI é 25MB por requisição; divida em pedaços de 10 min
#    (≈4.5MB a 64kbps) para resiliência sob proxies instáveis.
ffmpeg -hide_banner -loglevel error -y \
  -ss <inicio> -t 600 -i input.mp4 \
  -vn -ac 1 -ar 16000 -c:a libmp3lame -b:a 64k chunk.mp3
# 2. Solicitar timestamps no nível de palavra. NÃO solicite response_format=srt.
import httpx, os
data = {
    "model": "whisper-1",
    "language": "es",                        # fixe o idioma de origem; nunca auto-detectar
    "response_format": "verbose_json",
    "timestamp_granularities[]": "word",     # ← a flag crítica
    "temperature": "0.2",                    # habilita cadeia de fallback (anti-loop)
}
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": "olá", "start": 0.12, "end": 0.34}, ...]
segments = j["segments"]   # veja surpresa abaixo

Surpresa: words[] não tem pontuação, segments[] é inconsistente

O array words[] do Whisper tipicamente não tem pontuação em word["word"] — cada entrada é um token puro como "fazer", "um", "teste". Pontuação, quando presente, vive apenas no campo text de segments[].

Pior, o texto de segments[] é inconsistentemente pontuado entre pedaços do mesmo arquivo: o pedaço 0 de um podcast de 79 min pode emitir 285 segmentos nus ("fazendo um teste" "você" "hehe") de 1-2s cada sem pontuação; o pedaço 7 pode emitir 34 segmentos de 14-30s cada com pontuação. Ambos os comportamentos são enviados na mesma resposta da API.

Portanto, a receita correta combina ambos: use segments[] para limites naturais de pausa (já alinhados à respiração), mas trate-os como entrada bruta para seu próprio montador de legendas, que usa timestamps de palavra para dividir onde os segmentos são muito longos.

Receita de montagem de legendas

DUR_ALVO = 3.0   # tente fazer as legendas deste tamanho
DUR_MAX_LEGENDA = 5.0  # nunca exceda
MAX_CARACT = 18     # ~uma linha na Fonte 14 em 1080-wide vertical
MAX_LACUNA = 1.0      # limite de silêncio → força limite de legenda
MIN_PECA = 0.3    # abaixo disso, mesclar com vizinho
PONT_DIVISAO = set(",。!?;,.;!?")

# Passo A: mesclar segments[] curtos em direção a DUR_ALVO (use segments,
#         não words — os limites de segmento do Whisper já estão
#         alinhados a pausas).
def montar(segments, offset):
    legendas, buf = [], []
    def descarregar():
        if buf:
            legendas.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"]
        # Segmento único longo COM pontuação interna → dividir isoladamente
        if dur > DUR_MAX_LEGENDA and any(c in s["text"] for c in PONT_DIVISAO):
            descarregar(); legendas.extend(quebrar_segmento_longo(s, offset)); continue
        if not buf: buf.append(s); continue
        if (s["start"] - buf[-1]["end"]) >= MAX_LACUNA \
           or (buf[-1]["end"] - buf[0]["start"]) >= DUR_ALVO \
           or (s["end"] - buf[0]["start"]) > DUR_MAX_LEGENDA:
            descarregar()
        buf.append(s)
    descarregar(); return legendas

# Passo B: passada final — dividir cada vírgula/ponto interno em sua própria legenda
#         (temporizações proporcionais por posição do caractere). Juntar peças
#         mais curtas que MIN_PECA para frente.

# Passo C: qualquer legenda ainda > MAX_CARACT é dividida no maior espaço
#         entre palavras usando timestamps de words[]. Recursivo até abaixo do limite.

Ajuste DUR_ALVO e MAX_CARACT para o ritmo de leitura da plataforma. O limite de 18 caracteres é importante para queima de legenda em vertical 1080×1920 em Fontsize=14 — mais longo quebra em múltiplas linhas ilegíveis.

Detalhes operacionais

  • Autenticação: credenciais estão em ~/code/.env. Carregue com set -a; source ~/code/.env; set +a antes de invocar.
  • Proxy SOCKS nesta máquina: httpx precisa do extra socksio — use uvx --with httpx --with socksio python ... (sem isso você obtém ImportError: Usando proxy SOCKS, mas o pacote 'socksio' não está instalado).
  • Divisão em pedaços: pedaços de 10 min a 64kbps MP3 mono (~4.5MB cada) são o ponto ideal de confiabilidade. Pedaços de 20 min (~9MB) às vezes sofrem RST sob proxies instáveis. Concorrência max_workers=2 é mais confiável que 4.
  • Repetição: toda chamada de API envolta em backoff exponencial de 5× (time.sleep(min(2**n, 30))) — RemoteProtocolError: Servidor desconectado é comum e transitório.
  • Junção de deslocamento: as palavras de cada pedaço vêm com timestamps relativos àquele pedaço. Ao mesclar, adicione o deslocamento inicial absoluto do pedaço a cada start/end da palavra antes de montar as legendas.
  • Proteção de loop (cinto + suspensórios): mesmo com temperature=0.2, ocasionalmente um sub-pedaço ainda entra em loop. Após a montagem, execute um detector de loop no texto de cada legenda — se qualquer frase de comprimento 8–40 caracteres repetir 3+ vezes consecutivamente, descarte a legenda.

Anti-padrões (não faça)

  • ❌ Não solicite response_format=srt para conteúdo com mais de ~2 minutos.
  • ❌ Não "corrija" legendas ruins com uma segunda chamada de API. Se você obteve legendas blob ou alucinações em loop da sua primeira chamada, refaça com granularidade de palavra uma vez — não retranscreva apenas o sub-intervalo quebrado.
  • ❌ Não use temperature=0 em áudio potencialmente silencioso (yoga, conteúdo espiritual, encerramentos de podcast). Decodificação gananciosa entra em loop. 0.2 habilita a cadeia de fallback.
  • ❌ Não pule language=.... A detecção automática ocasionalmente troca Chinês→Japonês ou Espanhol→Português nos primeiros 30 segundos e toda a transcrição fica errada.

Caminho Volcano (豆包) ASR — preferido para chinês

O Volcano ASR rotineiramente supera o Whisper na precisão do Mandarim (taxa de reconhecimento, pontuação, entidades nomeadas). Use este como padrão para fonte zh-*.

⛔ NUNCA use 飞书妙记 / lark-minutes para ASR. Ele fornece apenas timestamps em nível de turno (fala do falante), não timing por palavra — legendas construídas a partir dele flutuam por segundos e quebram em lugares não naturais. A regra permanente do usuário: "以后不要用飞书妙记来做ASR,不能作为SRT使用,记住". Não há fallback de 飞书妙记. Se Volcano estiver indisponível, caia para o caminho OpenAI Whisper de nível de palavra acima (fixe language=zh).

Use a API WebSocket STREAMING — envia bytes, NÃO precisa de URL pública

As APIs de arquivo / 录音文件识别 / MediaKit todas exigem uma URL HTTP(S) de áudio publicamente acessível. O usuário rejeitou hospedagem de URL ("não use mais esse modelo de baixar mp3 do servidor" — túneis falham no ponto de acesso deles). A API 大模型流式语音识别 (streaming bigmodel) contorna a URL completamente enviando bytes PCM brutos por um WebSocket. Este é o caminho funcional. Ela retorna timestamps de palavra em ms.

Scripts inclusos (use estes — são o caminho funcional verificado):

# 1. Transcrever: envia bytes PCM mono de 16k por WebSocket → JSON ASR
#    (decodifica qualquer entrada via ffmpeg; .pcm passa direto)
export VOLC_ASR_APPID=…  VOLC_ASR_ACCESS_TOKEN=…   # credenciais estão com o usuário
python3 scripts/volc_asr_stream.py <clip.mp4|wav|mp3|pcm> <out.asr.json>

# 2. Construir um SRT limpo com timing de palavra a partir do JSON ASR
python3 scripts/build_srt_from_asr.py <out.asr.json> <out.srt> [max_chars=18]
#    Parâmetros de segmentação (opcionais):
#      --max-chars N   aumente para manter legendas inteiras / quebrar na pontuação, não no meio da frase
#      --soft-min N    min caracteres antes de uma ,、; descarregar uma legenda (padrão 8)
#      --strip-punct   remover TODA pontuação do texto exibido (aparência limpa de legenda)
  • Endpoint: wss://openspeech.bytedance.com/api/v3/sauc/bigmodel
  • Headers: X-Api-App-Key:{appid}, X-Api-Access-Key:{token}, X-Api-Resource-Id:volc.bigasr.sauc.duration, X-Api-Connect-Id:{uuid}
  • Frame v3 binário: cabeçalho de 4 bytes [(ver<<4)|hdrsize, (msgtype<<4)|flags, (ser<<4)|comp, 0]. Full-client(0x1)+POS_SEQ envia JSON config gzipado; pacotes de áudio(0x2) enviam chunks PCM gzipados (200ms = 6400 bytes @16k mono s16le); último pacote usa NEG_WITH_SEQ(0x3) com seq negativo. Full-response(0x9) do servidor retorna JSON gzipado.
  • JSON de configuração: {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}}. NÃO defina result_type:"single" — isso retorna apenas a última frase a cada quadro; o modo padrão acumula todas as falas.
  • Forma do resultado: result.utterances[], cada um com text (pontuado) + words[] (token + start_time/end_time em ms). Tokens latinos como "AI" vêm com start=end=0 — build_srt_from_asr.py preenche para frente/trás a partir dos vizinhos.
  • build_srt_from_asr.py também: divide legendas na pontuação (DESCARREGAMENTO FORTE em 。!?; DESCARREGAMENTO SUAVE em ,、; após --soft-min caracteres; limite máximo --max-chars), opcionalmente remove toda a pontuação da exibição (--strip-punct), descarta preenchimentos 呃/嗯/唉, e colapsa tokens curtos duplicados imediatos (才才→才). Cada legenda é temporizada pela sua primeira/última palavra para que se assente exatamente no áudio falado — sem flutuação. Para evitar quebras no meio da frase, aumente --max-chars para que os limites caiam na pontuação em vez do limite de caracteres.
  • Transcrição por clipe: transcreva o próprio áudio de cada clipe (PCM mono de 16k) para que os timestamps SRT saiam relativos ao clipe.
  • Credenciais: estão com o usuário (豆包语音引擎). Nomes de env aceitos pelo script: VOLC_ASR_APPID/VOLC_APPID e VOLC_ASR_ACCESS_TOKEN/VOLC_TOKEN; FFMPEG_BIN opcional.
    • O ASR e TTS 火山 deste usuário COMPARTILHAM UM conjunto de credenciais — o mesmo appid + access token funcionam para ambos os serviços. Eles estão armazenados em ~/code/.env como VOLC_TTS_APPID / VOLC_TTS_ACCESS_TOKEN (mais VOLC_APPID em ~/.zshrc), com aliases VOLC_ASR_APPID / VOLC_ASR_ACCESS_TOKEN anexados apontando para os mesmos valores. Então set -a; source ~/code/.env; set +a é suficiente — nenhum token ASR separado para procurar. Se os aliases estiverem faltando, recrie-os a partir dos valores TTS (eles são o mesmo segredo).
  • Caminhos sem saída (não tente novamente): MediaKit asr-subtitles (doc 6448/2381968) ainda precisa de uma URL pública e uma chave de API MediaKit separada. A API de arquivo 录音文件识别 precisa de uma URL pública.

Whisper Local como último recurso

Apenas quando offline, a cota da API estiver esgotada, ou para rascunhos ultra-baratos. Qualidade é materialmente menor para chinês; os mesmos modos de falha de blob/loop se aplicam; Whisper local não expõe timestamps no nível de palavra via CLI, portanto a correção baseada em princípios não está disponível.

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 é o piso prático para precisão em chinês; small é aceitável apenas para inglês limpo de estúdio. Whisper escreve . milissegundos; o arquivo ainda é SRT válido. Se você regenerar o SRT, sempre emita , ms.

Passo de polimento com IA — corrigir erros óbvios de digitação (etapa final, sempre executar para chinês)

Mesmo o Volcano ASR produz erros homofônicos claros que são lidos incorretamente na tela — observados: 「总数」→「意数」, 「需求」→「虚求」, 「程序员」→「成员」. O texto ASR bruto é o piso, não o teto. Após o SRT ser montado, faça uma passada de polimento com Claude sobre o SRT completo para corrigir erros óbvios usando o contexto da frase.

Esta passada é feita na sessão por Claude lendo o SRT — nenhuma chamada de API externa. Reescreva o .srt no local (ou para <nome>.polished.srt).

Regras rígidas — isso é correção, não edição:

  • ✅ Corrija apenas erros claros de homófono / digitação onde a palavra pretendida é inequívoca pelo contexto (意数→总数, 虚求→需求).
  • ⛔ **NUNCA mude timestamps,

Individual skills in this repo

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

Habilidades Relacionadas