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 audio | Grabación de video (StreamYard 2 en pantalla) | |
|---|---|---|
episode.json | ninguno (o {"layout": "background"}) | {"layout": "stacked"} |
| Imagen | assets/background.png | El mosaico del orador izquierdo arriba, el derecho abajo, subtítulos en la banda central |
| Sonido | audio.mp3 | extraí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
- Ejecuta
check_env.py(sin argumento de episodio). Verifica ffmpeg/libass, el fondo, la fuente y queOPENAI_API_KEYesté 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
catogrepen el archivo.env, y nunca pidas al usuario que pegue la clave en el chat. El usuario coloca la clave en.envpor sí mismo:cp .env.example .env, luego edita el archivo.
- Nunca leas, imprimas, uses
- Crea
episodes/<slug>/(slug corto en ASCII, p. ej.ep042). Copia la grabación allí comoinput.mp4y la nota del programa comoshownote.txt. Si no hay nota del programa, pídele al usuario los nombres y términos clave y escríbelos tú mismo enshownote.txt. No muevas ni copies una grabación de la que no se te haya dado una ruta. - Si es una grabación de video de dos personas lado a lado, escribe
episode.json:{"layout": "stacked"}. - 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.
-
Lee
shownote.txtpor completo. Enumera los nombres (presentadores, invitados), productos, empresas y términos técnicos que contiene. -
Lee la transcripción:
show_transcript.py <ep> --raw. Para episodios largos, léela en rangos (--from-id/--to-id) y busca congrepposibles 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 (制度→精度`). -
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).antesdebe 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óncuando 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 editestranscript.jsonmanualmente. - Si no hay nada que corregir, escribe solo la fila del encabezado.
-
Ejecuta
apply_corrections.py <ep>. Escribetranscript.jsony ejecutavalidate_transcript.py. Si no se encuentra unantes, corrige esa fila y vuelve a ejecutarlo. -
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 editandocorrections.mdy volviendo a ejecutarapply_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.
-
Busca:
show_transcript.py <ep> --grep <keyword> ... -C 5con 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. -
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; usaend_afterpara 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.
-
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_atyend_afterson subcadenas exactas opcionales del texto del primer y último segmento de un tramo. Las alternativas también los aceptan. Cada tramo necesita unareasoncuando hay varios. (Un borrador de un solo tramo también puede ponerstart_id/end_id/... en el nivel superior.) -
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, escribeclip.jsony lo valida. Enumera cada tramo con su rango de tiempo y duración. Lee cualquier líneawarnque imprima. Los tramos adyacentes (ids de segmento consecutivos) se consideran superpuestos: fúndelos en un solo tramo. -
Solo para grabaciones de video: ejecuta
preview_layout.py <ep>y miralayout.preview.png(ylayout.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"enepisode.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.jsonque funcione. -
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énlayout.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.jsony vuelve a ejecutarbuild_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.mp4debe 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 JPno 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íntoma | Solución |
|---|---|
OPENAI_API_KEY is not set | El usuario la añade a $SKILL_DIR/.env (plantilla .env.example) o la exporta. Nunca manejes la clave tú |
before ... not found | before debe ser una subcadena exacta del texto actual de ese segmento (las filas anteriores pueden haberlo cambiado) |
segments changed but not listed | Cada id de segmento modificado debe aparecer en una fila de corrections.md |
spans total ... > 180s / exceeds 180s | Elimina 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 huecos | transcribe.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 episodio | Renombra el digest.mp4, digest.ass, clip.json, clip.draft.json terminados (p. ej. digest-keypoints.mp4), luego escribe un nuevo clip.draft.json |
spans overlap | Dos 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 borde | Corrige tiles en episode.json, verifica con preview_layout.py, luego render.py <ep> |
layout 'stacked' but input.mp4 has no video stream | Es un pódcast de audio: elimina episode.json |
| Subtítulos en una fuente alternativa o delgada | assets/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 fuente | render.py <ep> lo detecta y vuelve a renderizar; --force para asegurarse |