Communitygithub.com

podcast-digest

Convierte episodios de pódcast en japonés (audio con notas del programa o grabaciones de videollamadas) en clips verticales 9:16 de hasta 180 segundos con subtítulos en japonés, extrayendo los puntos destacados seleccionados manualmente.

¿Qué es podcast-digest?

Esta habilidad permite a un agente de IA convertir un episodio de pódcast en japonés o una grabación de videollamada (por ejemplo, de StreamYard) en un video corto vertical de formato 9:16 con subtítulos en japonés incrustados. El proceso es semiautomático: los scripts se encargan de la transcripción, el cálculo de tiempos y el renderizado, mientras que el agente (o el usuario humano) debe intervenir en dos puntos clave: corregir la transcripción (corrigiendo errores de reconocimiento de nombres y términos técnicos) y seleccionar los fragmentos destacados a incluir. La salida es un video de hasta 180 segundos que une uno o varios fragmentos del episodio original en orden cronológico, mediante cortes directos, con transiciones de audio suaves. Está diseñado para ser usado con pódcast de audio o grabaciones de video de dos personas en pantalla dividida (modo apilado). La habilidad incluye validaciones y puntos de control humanos para garantizar la calidad del resultado final. Es ideal para creadores de contenido que quieran generar clips promocionales o resúmenes de sus episodios.

Compatible con✓Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/tomoima525/podcast-digest-skill/tree/HEAD

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Pódcast / grabación → video destacado

El trabajo determinista (audio, llamadas API, cálculos de tiempo, renderizado) lo hacen los scripts. Tú realizas los dos pasos de juicio: corregir la transcripción (1b) y elegir los destacados (2). Hay dos puntos de control humanos. Nunca los omitas.

SKILL_DIR a continuación significa el directorio que contiene este archivo. Ejecuta los scripts con uv, que usa el entorno bloqueado de esta habilidad desde cualquier directorio de trabajo: uv run --project "$SKILL_DIR" "$SKILL_DIR/scripts/<name>.py" <episode_dir>. En los pasos siguientes, <name>.py <ep> es la abreviatura de ese comando. Cada script imprime lo que creó o saltó. Volver a ejecutar siempre es seguro: los pasos terminados se saltan y los archivos derivados se reconstruyen cuando cambian sus entradas. --force rehace un paso.

Estructura del episodio (un directorio por episodio)

episodes/<slug>/
  input.mp4                        proporcionado por el usuario (audio de pódcast o grabación de video)
  shownote.txt                     proporcionado por el usuario (recomendado: nombres, productos, temas)
  episode.json                     lo escribes tú para grabaciones de video: {"layout": "stacked"}
  audio.mp3, transcript.raw.json   paso 1a  (transcribe.py)
  corrections.md                   paso 1b  (lo escribes tú)
  transcript.json                  paso 1b  (apply_corrections.py)
  clip.draft.json                  paso 2   (lo escribes tú)
  clip.json                        paso 2   (build_clip.py)
  layout.preview.png               paso 2   (preview_layout.py, grabaciones de video)
  digest.ass, digest.mp4           paso 3   (render.py)

Dos tipos de episodio, un mismo flujo

Pódcast de audioGrabación de video (StreamYard 2 en pantalla)
episode.jsonninguno (o {"layout": "background"}){"layout": "stacked"}
Imagenassets/background.pngEl mosaico del orador izquierdo arriba, el derecho abajo, subtítulos en la banda central
Sonidoaudio.mp3extraído de input.mp4, sincronizado con la imagen

Todo lo demás (transcripción, correcciones, selección de destacados, subtítulos, volumen) es igual. Una grabación de 50 minutos y ~1.5 GB está bien: solo se sube su audio y el renderizado busca directamente cada fragmento destacado.

Paso 0: Configuración

  1. Ejecuta check_env.py (sin argumento de episodio). Verifica ffmpeg/libass, el fondo, la fuente y que OPENAI_API_KEY esté configurada en el shell o en $SKILL_DIR/.env. Si sale con código 1, detente y dirige al usuario al README → Configuración.
    • Nunca leas, imprimas, uses cat o grep en el archivo .env, y nunca pidas al usuario que pegue la clave en el chat. El usuario coloca la clave en .env por sí mismo: cp .env.example .env, luego edita el archivo.
  2. Crea episodes/<slug>/ (slug corto en ASCII, p. ej. ep042). Copia la grabación allí como input.mp4 y la nota del programa como shownote.txt. Si no hay nota del programa, pídele al usuario los nombres y términos clave y escríbelos tú mismo en shownote.txt. No muevas ni copies una grabación de la que no se te haya dado una ruta.
  3. Si es una grabación de video de dos personas lado a lado, escribe episode.json: {"layout": "stacked"}.
  4. Ejecuta run_all.py episodes/<slug>. Ejecuta el paso 1a y se detiene en el paso 1b con código de salida 10. Esa pausa es esperada. El código de salida 1 significa un error: léelo.

Paso 1a: Transcribir (script)

run_all.py ya ejecutó transcribe.py. Extrae audio mono de 16 kHz y llama a whisper-1 con marcas de tiempo por palabra, language="ja" y un mensaje (prompt) de vocabulario construido a partir de la nota del programa. Los archivos largos se dividen automáticamente en fragmentos superpuestos. whisper-1 a veces se salta un tramo de habla: las pausas largas entre palabras reconocidas que aún contienen sonido se reenvían por separado y los segmentos recuperados se insertan (líneas gap ... recovered). Para ver el mensaje, ejecuta transcribe.py <ep> --print-prompt. Para sobrescribirlo, coloca el texto en <ep>/prompt.txt y vuelve a ejecutar con --force. La reejecución cuesta dinero de la API, así que pregunta primero al usuario.

Paso 1b: Corregir la transcripción (tú)

Objetivo: corregir palabras mal reconocidas en el text de cada segmento, usando shownote.txt como fuente de verdad para nombres, productos y términos.

  1. Lee shownote.txt por completo. Enumera los nombres (presentadores, invitados), productos, empresas y términos técnicos que contiene.

  2. Lee la transcripción: show_transcript.py <ep> --raw. Para episodios largos, léela en rangos (--from-id/--to-id) y busca con grep posibles errores de reconocimiento de cada término (--grep クロード --grep claude). Whisper suele escribir los nombres en inglés en katakana, los escribe mal (Wisper), omite letras (NTHROPIC) o elige el homófono kanji incorrecto (制度→精度`).

  3. Escribe corrections.md: una tabla en markdown. Cada fila reemplaza una subcadena exacta en el/los segmento(s) listado(s):

    | id | antes | después | razón |
    |----|--------|----------|-------|
    | 6 | クロットコード | Claude Code | nombre del producto en la nota del programa |
    | 7, 8, 12 | エーデント | エージェント | error de reconocimiento de エージェント |
    
    • id: ids de segmento, separados por comas, rangos permitidos (10-12). antes debe aparecer en el texto actual de ese segmento. Las filas se aplican en orden.
    • Corrige solo errores de reconocimiento. No reformules, resumas, elimines muletillas o "mejores" el japonés. Corrige un homófono obvio por contexto y di "(contexto)" en razón cuando la nota del programa no lo aclare.
    • Si un segmento es una alucinación de whisper (por ejemplo, 「ご視聴ありがとうございました」 en silencio), ponlo vacío: | 41 | ご視聴ありがとうございました | | alucinación |.
    • Nunca fusiones, dividas, reordenes o cambies los tiempos de los segmentos. Los scripts lo hacen imposible siempre que pases por corrections.md. No edites transcript.json manualmente.
    • Si no hay nada que corregir, escribe solo la fila del encabezado.
  4. Ejecuta apply_corrections.py <ep>. Escribe transcript.json y ejecuta validate_transcript.py. Si no se encuentra un antes, corrige esa fila y vuelve a ejecutarlo.

  5. PUNTO DE CONTROL 1. Muéstrale al humano la tabla de correcciones (o un resumen de ella, más las filas marcadas como "(contexto)") y pídele que revise corrections.md. Aplica sus cambios editando corrections.md y volviendo a ejecutar apply_corrections.py. Continúa solo después de que lo aprueben.

Paso 2: Elegir los destacados (tú)

Este paso requiere la instrucción del humano: ya sea un tema (「料金の話をしているところ」) o una solicitud de las mejores partes (「大事なポイントをまとめて」). Si no te ha dado una, pídela. No adivines un tema.

Un clip son uno o más tramos (partes contiguas del episodio) reproducidos en orden cronológico y unidos con cortes directos. Total: máximo 180 s.

  1. Busca: show_transcript.py <ep> --grep <keyword> ... -C 5 con las palabras clave de la instrucción más sinónimos y ortografías probables. Luego lee generosamente alrededor de cada acierto (--from-id/--to-id) para ver dónde empieza y termina realmente cada discusión. Para "los puntos importantes", lee toda la transcripción en rangos primero y enumera los momentos candidatos con sus ids antes de elegir.

  2. Elige los tramos:

    • Un tramo cuando la instrucción nombra un tema y su discusión cabe en 180 s.
    • Varios tramos (normalmente 2–4) cuando la instrucción pide destacados, o cuando un tema está disperso y la charla intermedia no es relevante. Cada tramo debe tener sentido por sí mismo y justo después del anterior: un corte directo no da contexto.
    • Para cada tramo: empieza donde se introduce el punto, no a mitad de un pensamiento o con una referencia anterior (「さっきの」「前回」「それで」). Recorta una muletilla o transición inicial (「では本題です。」「で、」) con start_at. Termina en una oración completa; usa end_after para detenerte dentro de un segmento.
    • Prefiere menos tramos completos a muchos fragmentos; un tramo de menos de ~8 s rara vez se sostiene por sí solo.
    • Si el material es más largo de 180 s, conserva las partes más sólidas y menciónalo en razón.
    • Añade hasta 2 alternatives (otros tramos sólidos) con sus razones.
    • Si nada coincide, detente e informa al humano qué buscaste y qué cubre realmente el episodio. No escribas un borrador.
  3. Escribe clip.draft.json. Tú eliges los ids de segmento. El script hace los tiempos:

    {
      "instruction": "<la instrucción del humano, textual>",
      "title": "スキル機能と文字起こしの要点",
      "reason": "Por qué estas partes, en su conjunto.",
      "spans": [
        { "start_id": 6, "end_id": 8, "start_at": "今日は、Claude Codeのスキル機能について",
          "title": "スキル機能とは", "reason": "Introduce el tema." },
        { "start_id": 12, "end_id": 12, "title": "分担が大事", "reason": "El punto central." }
      ],
      "alternatives": [
        { "start_id": 120, "end_id": 131, "title": "...", "reason": "..." }
      ]
    }
    

    start_at y end_after son subcadenas exactas opcionales del texto del primer y último segmento de un tramo. Las alternativas también los aceptan. Cada tramo necesita una reason cuando hay varios. (Un borrador de un solo tramo también puede poner start_id/end_id/... en el nivel superior.)

  4. Ejecuta build_clip.py <ep>. Ajusta cada límite a las marcas de tiempo de las palabras, añade unos 0.3 s de relleno (nunca más allá de la palabra vecina), ordena los tramos, verifica que no se superpongan y que el total sea ≤ 180 s, escribe clip.json y lo valida. Enumera cada tramo con su rango de tiempo y duración. Lee cualquier línea warn que imprima. Los tramos adyacentes (ids de segmento consecutivos) se consideran superpuestos: fúndelos en un solo tramo.

  5. Solo para grabaciones de video: ejecuta preview_layout.py <ep> y mira layout.preview.png (y layout.source.png, donde los mosaicos están delineados en rojo = arriba, azul = abajo). Cada panel debe mostrar a una persona, centrada, sin bordes de marco ni mosaico vecino. Si no, establece "tiles" en episode.json (fracciones del fotograma original) y vuelve a ejecutar:

    {"layout": "stacked",
     "tiles": [{"x": 0.0, "y": 0.14, "w": 0.497, "h": 0.70},
               {"x": 0.504, "y": 0.14, "w": 0.496, "h": 0.70}]}
    

    El primer mosaico va arriba. Intercambia las dos entradas para intercambiar los oradores. Las grabaciones de la misma configuración de estudio comparten los mismos mosaicos, así que reutiliza un episode.json que funcione.

  6. PUNTO DE CONTROL 2. Muéstrale al humano el plan: el título general y la duración total, luego cada tramo en orden con título, mm:ss–mm:ss, duración, razón, y su primera y última oración (show_transcript.py <ep> --from-id A --to-id B), luego las alternativas. Para video, muestra también layout.preview.png. Pídele que apruebe, intercambie, añada o elimine tramos.

    • build_clip.py <ep> --promote N: con un tramo, intercambia la alternativa N; con varios, añade la alternativa N como otro tramo (vigila el total de 180 s).
    • Cualquier otro cambio: edita clip.draft.json y vuelve a ejecutar build_clip.py.
    • Continúa solo después de que lo aprueben.

Paso 3: Renderizar (script)

Ejecuta run_all.py <ep> --render (o render.py <ep>). Salida: digest.mp4, 1080x1920, 30 fps, H.264 + AAC, normalizado a -14 LUFS, con subtítulos incrustados desde digest.ass. Los tramos se unen con cortes directos. Cada corte tiene un fundido de audio de 50 ms para que no haga clic.

Luego verifica el resultado:

  • ffprobe -v error -show_entries format=duration -of csv=p=0 <ep>/digest.mp4 debe coincidir con el total que imprimió build_clip.py.
  • Extrae un fotograma de cada tramo y míralos, para saber que los subtítulos se renderizan, no son cajas tofu y (video) los paneles están encuadrados correctamente: ffmpeg -y -v error -ss 5 -i <ep>/digest.mp4 -frames:v 1 /tmp/frame.png.
  • Si el renderizado advierte que la fuente Noto Sans JP no se encontró, dile al usuario (README → Configuración).

Informa la ruta a digest.mp4, el título y el rango de tiempo de origen de cada tramo.

Solución de problemas

SíntomaSolución
OPENAI_API_KEY is not setEl usuario la añade a $SKILL_DIR/.env (plantilla .env.example) o la exporta. Nunca manejes la clave tú
before ... not foundbefore debe ser una subcadena exacta del texto actual de ese segmento (las filas anteriores pueden haberlo cambiado)
segments changed but not listedCada id de segmento modificado debe aparecer en una fila de corrections.md
spans total ... > 180s / exceeds 180sElimina un tramo, elige otros más cortos o recorta con start_at / end_after
Falta discurso en la transcripción (un salto largo en los tiempos donde la gente habla) en una transcripción hecha antes de que existiera la recuperación de huecostranscribe.py <ep> --fill-gaps reenvía solo los huecos. Imprime cómo cambian los ids: renumerar corrections.md / clip.draft.json para que coincida, luego apply_corrections.py <ep>. Pon en blanco cualquier 「ご視聴ありがとうございました」 recuperado como alucinación
Un segundo clip del mismo episodioRenombra el digest.mp4, digest.ass, clip.json, clip.draft.json terminados (p. ej. digest-keypoints.mp4), luego escribe un nuevo clip.draft.json
spans overlapDos tramos comparten o tocan segmentos: fúndelos en un solo tramo
opens with 'さっき' (warn)Empieza más tarde o recorta con start_at para que el tramo se sostenga por sí solo
Los paneles muestran la zona incorrecta / un bordeCorrige tiles en episode.json, verifica con preview_layout.py, luego render.py <ep>
layout 'stacked' but input.mp4 has no video streamEs un pódcast de audio: elimina episode.json
Subtítulos en una fuente alternativa o delgadaassets/fonts/NotoSansJP-Bold.ttf debe existir (un archivo Bold estático, no la fuente variable); consulta README → Configuración
Se cambiaron background.png / la configuración de la fuenterender.py <ep> lo detecta y vuelve a renderizar; --force para asegurarse

Skills relacionados