Auto-Cut - Recorte medido de silencios y peso muerto
Principios
- Cortar ANTES de construir escenas/subtítulos. Cortar después desvía todas las marcas de tiempo (subtítulos, zooms, SFX). Salida de este paso:
assets/<source>.cut.mp4+assets/transcript.cut.json- cada paso posterior utiliza la versión recortada. - Cortar es OBLIGATORIO cuando el briefing habilita autoCut - no es una sugerencia. Si no se puede cortar nada, debes indicar una razón concreta (el video ya estaba ajustado) en el informe.
- Los umbrales residen en el servidor, no en tu cabeza. NO ejecutes
silencedetecttú mismo ni elijas un umbral en dB por intuición. Dos hechos comprobados acabaron con ese flujo de trabajo:- El umbral correcto es una propiedad del ARCHIVO, no una constante. En un archivo real, -40dB encontró 0 silencios, -30dB encontró 13, -25dB encontró 21. Un número fijo es inútil o se come el habla.
- La sonoridad por sí sola no puede distinguir entre "pausa" y "hablar en voz baja". En el archivo fuente medido,
silencedetecta -30dB informó 48.6s de "silencio" pero solo 18.2s de ellos estaban en un espacio real entre palabras; los otros 30.4s estaban dentro de palabras (pausas de sílabas, finales sordos en vietnamita c/t/p/ch). Cortar solo por nivel de sonido se traga el habla. El servidor realiza la medición (autoTrim.ts) y la generación de candidatos (deadWeight.ts). Tú haces la revisión. Esa división es el objetivo principal.
- Se cortan dos tipos de peso muerto: silencios (medidos por la máquina, protegidos por la transcripción) y peso muerto de contenido (muletillas, tartamudeos, tomas reformuladas - propuestas por el servidor, aprobadas por ti; además de PUNTOS repetidos, que solo tú puedes encontrar - ver Paso 3).
Paso 0 - Requisito previo: una transcripción con marcas de tiempo de palabras
Toda la protección depende de ello. El servidor busca assets/transcript.raw.json, luego assets/transcript.json. Sin una, el análisis aún se ejecuta pero devuelve guarded: false, la lista de peso muerto está vacía y los números son conjeturas en ambas direcciones (puede pasar un mal corte y fallar uno bueno). Transcribe primero - nunca recortes una cabeza parlante vietnamita sin protección.
Paso 1 - Analizar (gratuito, repetible, sin codificación)
POST http://localhost:6869/api/projects/<id>/auto-trim/analyze
body: {} # o { "source": "assets/face.mp4", "level": "tight" }
level es natural | default | tight y por defecto es brief.autoCutLevel. Respuesta:
silence-durationSec, elthresholdDbelegido +thresholdNote(por qué ganó ese umbral),noiseFloorDbmedido, lossilencesque se cortarán,keepRanges,removedSec, la tabla completasweep, ywordGuard(cuántos segundos vetó la transcripción).deadWeight-candidates[]conkind(filler | stutter | repeat-take | hesitation),start/end,text,confidence,reason,context; además detotalSecybyKind.guarded-truesolo cuando se encontró una transcripción. Si esfalse, arréglalo antes de cortar.
No se codifica nada, así que llámalo tantas veces como quieras (un fuente de 217s se analiza en aproximadamente un segundo).
Paso 2 - REVISAR cada candidato (tu trabajo, no el de la máquina)
La lista es determinista: misma transcripción, mismos candidatos, misma confianza. Lo que no puede hacer es entender el significado. Revísalos uno por uno y conserva solo aquellos que defenderías:
- Confianza baja significa "lee el contexto primero", no "probablemente esté bien". Las muletillas en vietnamita casi siempre chocan con palabras reales.
đó,ấy,thế,mà,làson tanto partículas de relleno como demostrativos/conjunciones reales. - Las frases conectoras (
hoặc là,tức là,bởi vì là,với lại là) son la trampa. Medido en una transcripción real: "…có thể là ok ứng dụng nó / Hoặc là / Tham khảo để tìm cách…" es una alternativa genuina - cortarlo destruye una rama de la oración. Pero "Còn trường hợp mà mọi người có thể nghe / Hoặc là / Còn trường hợp mà người AI không ứng dụng được…" es una oración abandonada y debería eliminarse. La forma superficial es idéntica; solo el significado las separa. Es exactamente por eso que estos candidatos vuelven con una confianza base baja y una razón que dice LEE EL CONTEXTO. - Candidatos
repeat-take: comprueba que la toma posterior realmente sea la más completa antes de aprobar. - Los candidatos
hesitationson silencio puro entre palabras - generalmente seguros, pero verifica que no estás eliminando una pausa deliberada antes de un remate.
Rechaza libremente. Una muletilla dejada cuesta 0.4s; una palabra real cortada hace que la oración no tenga sentido y el espectador lo nota de inmediato.
Paso 3 - PUNTOS REPETIDOS (solo tú puedes encontrarlos - obligatorio)
Ningún detector captura esto, y es el peso muerto del que los usuarios se quejan más. Lee la transcripción como un discurso y analízala semánticamente:
- Agrupa oraciones que expresan el MISMO PUNTO - las palabras no tienen por qué coincidir, solo el contenido. Un hablante a menudo reformula un punto 2-3 veces: primero corto/titubeante, luego más completo. Considera también oraciones NO adyacentes (expone el punto A, divaga, vuelve al punto A con más detalle).
- Conserva exactamente UNA versión por grupo - la MÁS COMPLETA:
- Versión corta anterior, reformulación posterior más completa -> conserva la posterior, corta la anterior.
- La oración posterior es solo un eco corto ("đúng vậy, như tôi nói…") -> conserva la anterior, corta el eco.
- Dos versiones equivalentes -> conserva la entrega más fluida (menos titubeos, sin muletillas).
- Verifica el flujo después de cortar: vuelve a leer el texto conservado de principio a fin. Si una oración conservada hace referencia hacia atrás ("như vừa nói") a una que cortaste, o conserva ambas o elige la versión sin la referencia hacia atrás.
- Convierte cada decisión en un rango
{start, end}y envíalo con los candidatos aprobados en el Paso 4. - Construye una tabla antes de cortar (inclúyela en el informe):
marca de tiempo | oración cortada | razón | oración conservada- para que el usuario pueda revisar cada decisión.
Paso 4 - Aplicar (pasa por la cola de renderizado)
POST http://localhost:6869/api/projects/<id>/auto-trim/apply
body: { "cutCandidates": [{ "start": 51.81, "end": 52.25 }, …] } # SOLO lo que aprobaste
-> 202 { job }
Envía una lista vacía si no aprobaste nada - los silencios medidos aún se cortarán. Luego consulta GET /api/jobs/<jobId> hasta que termine. El trabajo:
- Vuelve a analizar con la protección de la transcripción, fusiona los rangos de silencio con tus rangos aprobados (las superposiciones se fusionan, por lo que nada se cuenta dos veces).
- Descarta cualquier rango aprobado que se trague un punto medio de palabra - una última red de seguridad sobre tu revisión, y registra cada rango que rechaza. Los rangos rechazados en el registro significan que aprobaste algo que se encuentra sobre habla real; vuelve atrás y relee ese punto.
- Corta en UNA pasada de ffmpeg (
trim/atrim + setpts/asetpts + concat), escribiendoassets/<stem>.cut.mp4. - Reasigna cada marca de tiempo a la línea de tiempo recortada ->
assets/transcript.cut.json. - Verifica la SALIDA con las palabras reasignadas y escribe
assets/auto-trim-report.json.
Si no hay nada que valga la pena cortar, el trabajo deliberadamente NO produce un .cut.mp4 (una copia idéntica recodificada solo pierde calidad) - output en el informe es null y sigues usando el fuente.
Paso 5 - Leer el informe (el trabajo no está terminado hasta que lo hagas)
assets/auto-trim-report.json contiene: duración antes/después, removed.silenceSec vs removed.approvedSec, el umbral elegido y por qué, candidatos rechazados y verification.
verdict: "pass"- el resultado cumple con el perfil para ese nivel.verdict: "fail"- el trabajo aún terminó y el archivo es utilizable, pero NO cumplió con el perfil. El registro lo indica explícitamente. Aprueba más peso muerto (Paso 2/3) y ejecuta apply nuevamente, o indica en el informe final por qué lo estás aceptando. Nunca informes el corte como realizado mientras ignoras un fallo.
Los criterios de aprobación no son la vieja regla "sin silencio > 0.8s" - la medición mostró que esa regla es demasiado laxa. Un archivo real la aprobó mientras llevaba 13 silencios residuales de 0.45-0.67s totalizando 6.7s (4.2% del tiempo de ejecución), lo que suena obviamente lento. Los perfiles ahora limitan TANTO el silencio individual más largo como la relación total (default: 0.5s / 3%).
Paso 6 - Usar la versión recortada en todas partes
A partir de aquí, los subtítulos, la disposición de las teclas, la sincronización de SFX y los zooms leen assets/transcript.cut.json y el .cut.mp4. La comprobación de control de calidad dead-air vuelve a medir el video ensamblado con la transcripción y FALLA cuando brief.autoCut está activado y el silencio muerto aún supera el perfil - eso es el respaldo, no el plan.
El informe final debe incluir: la tabla de corte del Paso 3, los segundos eliminados divididos en silencio vs peso muerto aprobado, y el veredicto de verificación. Toma los números de auto-trim-report.json.
Problemas conocidos
- Usar la transcripción ANTIGUA después de cortar -> los subtítulos se desincronizan progresivamente hacia el final. Cambia siempre a
transcript.cut.json. - Cortar solo silencios e ignorar los puntos repetidos - lo que los usuarios llaman "peso muerto" es principalmente el Paso 3.
- Cortar el borrador renderizado en lugar del fuente -> la calidad disminuye por una segunda codificación. Corta siempre desde el fuente (el servidor se niega a sobrescribir el fuente y omite los archivos ya nombrados
*.cut.*al elegir un valor predeterminado). - Analizar un
.cut.mp4mientras la única transcripción es la original -> la protección está alineada con la línea de tiempo incorrecta. Empareja el archivo fuente con la transcripción que lo describe. - Archivos SFX/narración con su propio silencio inicial -> usa
data-media-startpara cortar dentro de HyperFrames (consulta "Sonoridad SFX y silencio inicial" en el skill noti-tiktok-vn), no recodifiques un archivo de audio solo por 0.3s de silencio inicial. - Unir segmentos cortados sin un fundido -> un clic en cada unión. Corregido en
autoTrim.ts; la regla está a continuación y se aplica a cualquier código nuevo que corte o concatenate audio.
Audio: fundido de 30ms en cada borde de corte (regla estricta)
Un corte cae en medio de una forma de onda. La última muestra de un segmento y la primera muestra del siguiente no están relacionadas, por lo que la unión es un escalón vertical, y un escalón es un clic. La causa no es el codificador ni el fuente - es la unión misma, por lo que sobrevive a cualquier recodificación posterior.
La importancia depende de DÓNDE cae el corte, y la diferencia es grande. Medido sin pérdida en un video real de 278s de una persona hablando, comparando el mismo corte con y sin el fundido (el escalón muestra a muestra más grande en la unión; cualquier cosa por encima de ~0.02 es audible):
| Tipo de unión | Antes | Después |
|---|---|---|
Corte de silencio, 40 uniones (balanced) | mediana 0.0040, peor 0.0113 - ninguno audible | mediana 0.0000 |
| En medio del habla, 4 uniones (peso muerto / puntos repetidos) | 0.1195, 0.0744, 0.0232, 0.0064 - 3 de 4 audibles | todos ≤ 0.0008 |
A modo de escala, el escalón más fuerte en cualquier parte del habla normal en esos archivos fue de 0.06-0.13. Por lo tanto, una unión en medio del habla sin fundido puede ser un salto mayor que cualquier cosa que el contenido mismo produzca, mientras que una unión de silencio no es ni remotamente audible.
No te saltes el fundido basándote en la primera fila. El recorte de silencio simple es el caso en el que no importa; el Paso 3 (puntos repetidos) y la extracción de clips cortan directamente a través del habla, y esas son las uniones que hacen clic.
Solución: funde ambos bordes de cada segmento durante 30ms. Suficientemente largo para eliminar el escalón, demasiado corto para escucharse como un fundido. Usa audioCutFade() en apps/server/src/util.ts; no implementes el filtro manualmente.
[0:a]atrim=start=S:end=E,asetpts=PTS-STARTPTS,afade=t=in:st=0:d=0.030,afade=t=out:st=D-0.030:d=0.030[aN]
Tres cosas que hacen que funcione silenciosamente sin hacer nada:
- Orden.
afadedebe ir DESPUÉS deasetpts=PTS-STARTPTS.afadeleestdel reloj propio del flujo; con marcas de tiempo originalesst=0está en el pasado y el fundido de entrada nunca se activa. - Longitud.
Des la duración del segmento, por lo que el inicio del fundido de salida esD - 0.030. Limita el fundido aD/2o los dos fundidos se superponen en un segmento corto y lo engullen. - Extraer un clip, no solo unir. Un clip extraído de un video más largo tiene los mismos dos bordes en bruto aunque no se una nada -
reframe.tslos funde mediante-af.
Una nota sobre cómo medir esto tú mismo: no compares dos codificaciones AAC. La recodificación perturba las muestras en todas partes, lo que enterró las 40 uniones reales entre 294 regiones de diferencia espurias y puso un desplazamiento de 54 ms entre los dos archivos. Renderiza ambas variantes directamente a pcm_s16le - entonces las dos están alineadas por muestra y las uniones se asientan exactamente en las longitudes acumulativas de los segmentos.