Cuando una transcripción ya tiene marcas de tiempo por palabra, encontrar candidatos a clips cortos en una grabación larga no necesita ningún modelo. Una ventana de longitud fija, que empieza en cada palabra y se puntúa según cuántas palabras caben dentro, encuentra los tramos donde más se dice. El editor de vídeo de la aplicación de escritorio de osFoundry incluye justo esto como botón Suggest clips, escrito en Rust (find_highlights en canvas_transcript.rs). Aquí se reconstruye en Python y se ejecuta sobre una entrevista real de 5:07, con mediciones de lo que hace cada regla, cómo cambia el resultado según la longitud de la ventana, cuándo deja de ser óptima la selección de «el mejor primero» y cómo hacer lineal la pasada de puntuación.
La entrada
La grabación es una entrevista de la NASA a la astronauta Christina Koch. Su transcripción de Whisper tiene 1.007 palabras en 306,97 segundos, 3,28 palabras por segundo de media. En este modo Whisper devuelve las palabras sin puntuación, y lo único que necesita el algoritmo son los tiempos:
{"words": [
{"index": 0, "text": "And", "start_ms": 0, "end_ms": 160},
{"index": 1, "text": "we'll", "start_ms": 160, "end_ms": 280},
{"index": 2, "text": "get", "start_ms": 280, "end_ms": 380},
...
], "duration_ms": 306970}
Una ventana por palabra
Cada palabra abre una ventana candidata. Una palabra cuenta si termina antes que la ventana, y el recorrido se detiene en la primera palabra que no lo cumple:
def candidates(words, window_ms):
out = []
for i, w in enumerate(words):
start = w["start_ms"]
end = start + window_ms
n = 0
for x in words[i:]:
if x["end_ms"] > end:
break
n += 1
if n:
out.append((start, end, n))
return out
Al empezar las ventanas en palabras y no en una cuadrícula fija (cada segundo, por ejemplo), un candidato siempre arranca al principio de una palabra, nunca a mitad. La puntuación se divide por la longitud de la ventana, no por el tiempo realmente hablado, así que las 44 ventanas que empiezan en los últimos 15 segundos del clip puntúan bajo por sí solas, sin ningún caso especial.
Para esta entrevista salen 1.007 candidatos. La mediana tiene 49 palabras y la mejor, 67.
El mejor primero, sin solapes
Los candidatos se ordenan por puntuación, con los empates resueltos por posición para que la lista salga igual en cada ejecución, y se toman desde arriba mientras no se solapen con nada de lo ya elegido:
def find_highlights(words, window_ms=15_000, count=3):
ranked = sorted(candidates(words, window_ms), key=lambda c: (-c[2], c[0]))
chosen = []
for c in ranked:
if len(chosen) >= count:
break
if any(c[0] < k[1] and k[0] < c[1] for k in chosen):
continue # overlaps a better window
chosen.append(c)
return sorted(chosen)
Con los ajustes de la aplicación, 15 segundos y tres clips, la versión en Python devuelve los mismos tres rangos que muestra la aplicación:
| clip | palabras | palabras/s |
|---|---|---|
| 0:45.1 – 1:00.1 | 64 | 4,27 |
| 2:03.4 – 2:18.4 | 67 | 4,47 |
| 2:31.5 – 2:46.5 | 65 | 4,33 |
La regla de solapamiento hace casi todo el trabajo. Sin ella, los tres primeros son 2:03.4, 2:03.6 y 2:00.2: el mismo pasaje tres veces, todos a menos de 3,4 segundos unos de otros. El habla densa sigue siendo densa durante un rato, así que las mejores ventanas se agrupan: los 19 mejores candidatos empiezan todos entre 1:59.7 y 2:04.4. La segunda y la tercera elección son los candidatos número 20 y 21 del ranking, y para llegar a ellos se descartaron 18 ventanas con mejor puntuación. Las dos empatadas a 67 (2:03.4 y 2:03.6) explican por qué importa desempatar por posición.
La longitud de la ventana cambia el resultado
La misma transcripción con otras longitudes de ventana:
| ventana | elegidos (inicio, palabras/s) |
|---|---|
| 10 s | 0:51.2 (4,5), 2:08.5 (4,7), 3:15.3 (4,5) |
| 15 s | 0:45.1 (4,27), 2:03.4 (4,47), 2:31.5 (4,33) |
| 30 s | 0:25.1 (3,8), 1:48.5 (3,93), 2:31.5 (3,93) |
| 60 s | 0:00.0 (3,57), 1:23.7 (3,48), 2:31.0 (3,77) |
Las ventanas cortas premian una sola ráfaga rápida y dan picos más altos. Las largas promedian pausas y preguntas y tienden hacia la media global de 3,28. El tramo en torno a 2:31 sobrevive con cualquier longitud de 15 a 60 segundos, lo que es una señal bastante buena de que es una respuesta sostenida y no una frase rápida suelta. Probar dos longitudes y buscar dónde coinciden es una forma barata de ordenar los candidatos por algo más que un único número.
¿Es óptimo elegir el mejor primero?
Quedarse primero con la mejor ventana es una elección voraz (greedy). Para comprobarla, la respuesta exacta sale de un pequeño programa dinámico sobre los candidatos ordenados por inicio: para cada candidato, o se salta, o se toma y se salta al primer candidato que empieza después de que este termine.
import bisect, functools
def best_total(words, window_ms, count):
cands = sorted(candidates(words, window_ms))
starts = [c[0] for c in cands]
nxt = [bisect.bisect_left(starts, c[1]) for c in cands]
@functools.lru_cache(None)
def best(i, k):
if k == 0 or i >= len(cands):
return 0
return max(best(i + 1, k), cands[i][2] + best(nxt[i], k - 1))
return best(0, count)
Total de palabras en las ventanas elegidas, voraz frente a exacto:
| ventana × clips | el mejor primero | exacto |
|---|---|---|
| 15 s × 3 | 196 palabras, 3 clips | 196 |
| 15 s × 10 | 595 palabras, 10 clips | 595 |
| 30 s × 5 | 563 palabras, 5 clips | 565 |
| 30 s × 10 | 764 palabras, 8 clips | 989 |
| 60 s × 5 | 849 palabras, 5 clips | 991 |
Para una lista corta de ventanas cortas, el voraz no pierde nada aquí: con 15 segundos igualó el total exacto para 3, 5 y 10 clips. Se viene abajo cuando las ventanas empiezan a llenar la grabación. Diez ventanas de 30 segundos caben en 307 segundos, pero el mejor primero coloca sus primeras elecciones donde están los picos, los huecos que quedan entre ellas miden menos de 30 segundos, y devuelve 8 clips donde la respuesta exacta mete 10. Lo mismo cuesta 142 palabras con 60 s × 5. Así que la versión voraz es la herramienta adecuada para «dame tres candidatos» y la equivocada para «divide esta charla en capítulos», donde el DP solo ocupa unas líneas más.
Hacer lineal la pasada de puntuación
El recorrido repetido de candidates lee cada palabra de cada ventana, así que su coste crece con la longitud de la ventana por el número de palabras. Como los tiempos de inicio y de fin solo avanzan, la última palabra de la ventana también solo avanza, y basta con un puntero:
def candidates_two_pointer(words, window_ms):
out, j = [], 0
for i, w in enumerate(words):
j = max(j, i)
end = w["start_ms"] + window_ms
while j < len(words) and words[j]["end_ms"] <= end:
j += 1
if j > i:
out.append((w["start_ms"], end, j - i))
return out
Las dos versiones devuelven candidatos idénticos sobre esta transcripción. Medido con Python 3.9 en un Apple M1, mediana de 7 ejecuciones, solo la puntuación; el caso de 1 hora se construye repitiendo la entrevista 12 veces:
| transcripción | palabras | recorrido repetido | dos punteros |
|---|---|---|---|
| 5 min | 1.007 | 2,9 ms | 0,4 ms |
| 1 h | 12.084 | 190,5 ms | 5,4 ms |
El bucle en Rust de la aplicación es la versión de recorrido repetido. En Python, con un pódcast largo o con varias longitudes de ventana a la vez, la versión de dos punteros es la que conviene quedarse. Eso sí, depende de que los tiempos de fin estén ordenados, algo que se cumplió para todas las palabras aquí, pero que vale la pena comprobar con un assert en transcripciones combinadas de varias fuentes.
Lo que la puntuación no sabe
Las palabras por segundo miden cuánto se dice, no si merece la pena escucharlo. El comentario sobre find_highlights en el código de la aplicación lo llama heurística y marca el límite a propósito: un modelo que elija los momentos destacados sería otra funcionalidad, una que cuesta dinero y necesita una clave, y mejorar esta en silencio convertiría una operación gratuita en un cargo que el usuario nunca aprobó. El panel de la aplicación le dice lo mismo al usuario: «a starting point, not a verdict» (un punto de partida, no un veredicto). En la práctica, las tres ventanas indican por dónde empezar a escuchar, y los puntos de corte se siguen decidiendo de oído.
El script completo, con todas las mediciones de arriba, son unas 120 líneas de Python con solo la biblioteca estándar. En la aplicación de escritorio de osFoundry la misma función está detrás del botón Suggest clips del panel de transcripción del editor de vídeo, junto a un paso de transcripción que funciona con tu propia clave de API en la nube. La aplicación es gratuita desde la página de descarga de osFoundry.

Top comments (0)