← Ruta Perico
Etapa 11

Segmentar texto corrido

1ª categoría Publicada ~2 h · $3,31
11

En el foro las frases venían marcadas: alguien las había puesto entre comillas, y bastó con extraer lo de dentro. Las transcripciones no dan esa ayuda. Son un chorro de texto corrido donde hablan un entrevistador, dos comentaristas y él, sin una sola marca de quién dice qué.

Así que este paso tiene que resolver tres cosas a la vez, y ninguna es independiente de las otras:

  1. Dónde empieza y dónde acaba una frase, en un texto sin puntuación fiable.
  2. Si la dice él o el que tiene al lado.
  3. Si tiene algo de estilo o es una frase informativa cualquiera.

Ventanas que se solapan

Un vídeo de una hora son 8.000 o 9.000 palabras: demasiadas para una sola petición. Se parte en ventanas — y se solapan a propósito.

segment.py
WINDOW_WORDS = 900
OVERLAP_WORDS = 120


def windows(text, size=WINDOW_WORDS, overlap=OVERLAP_WORDS):
    """Slice the text into overlapping word windows.

    The overlap exists so a phrase sitting on a boundary is whole in at least
    one window. It also means the same phrase can come back twice, which the
    caller dedupes.
    """
    words = text.split()
    step = size - overlap
    for start in range(0, max(len(words) - overlap, 1), step):
        chunk = words[start:start + size]
        if len(chunk) >= 50:      # a scrap at the end is not worth a request
            yield " ".join(chunk)

Sin solape, una frase que cae justo en el corte se parte en dos y las dos mitades se pierden — ninguna se sostiene sola. Con 120 palabras de solape, cualquier frase de las que buscamos cabe entera en al menos una ventana. El precio es que algunas salen dos veces, y eso se arregla después deduplicando con la misma clave normalizada de la etapa 6.

La salida va con esquema declarado, igual que el clasificador de la etapa 7: texto, devices y confidence, con los mismos cinco recursos estilísticos.

El modelo inventa

El prompt pide copia literal, con mayúsculas y todo. Y el modelo obedece casi siempre. Casi.

En la transcripción pone:

«…y de nuevo una curva y el número 13 dio la razón a los supersticiosos»

Lo que dice la transcripción

Y el modelo devolvió:

«llevaba el número 13 y eso me hacía concebir un poco de dudas, ¿no?»

Lo que devolvió el modelo. Nunca lo dijo

Fluida, en su registro, con su coletilla al final. Y falsa. Apretar el prompt subió el literal del 95% al 97%. Nunca llegó al 100%.

Así que el pipeline deja de fiarse. Antes de escribir nada, comprueba cada frase contra su transcripción de origen y tira lo que no encuentra:

verify_verbatim.py
def loose(text):
    """Normalise for comparison: case, accents, punctuation and spacing.

    Deliberately forgiving. We are not checking that the model reproduced the
    transcript's commas, we are checking it did not invent or rewrite words.
    """
    text = unicodedata.normalize("NFD", text.lower())
    text = "".join(c for c in text if unicodedata.category(c) != "Mn")
    text = re.sub(r"[^\w\s]", " ", text)
    return re.sub(r"\s+", " ", text).strip()


verbatim, missing = [], []
for phrase in phrases:
    haystack = sources.get(phrase["video_id"], "")
    (verbatim if loose(phrase["text"]) in haystack else missing).append(phrase)
Un prompt es una petición; un filtro es una garantía

Una instrucción en el prompt pide un comportamiento. Una comprobación en el código lo impone. Cuando la diferencia importa, la instrucción no basta por buena que sea — y aquí importaba.

En la ejecución final el filtro no tiró ni una de las 3.531. El prompt estaba bien. Pero eso no se sabe hasta comprobarlo, y no se puede comprobar leyendo 3.531 frases a mano.

Conviene medir bien el daño, porque exagerarlo lleva a la conclusión equivocada. Esto es un traductor de estilo, no una base de datos de citas: una frase inventada que suena a él no es una catástrofe, nadie va a citarla en un juicio.

El coste real es más sutil. Un corpus con frases inventadas hace que la recuperación devuelva imitaciones, y entonces la app imita una imitación. Frase a frase no se nota; sobre miles, el estilo se va aplanando hacia el español genérico que todos los modelos escriben por defecto — justo lo que este proyecto existe para evitar. El filtro es higiene, no un airbag.

Dos fallos más del prompt, del mismo tipo

Antes de la invención aparecieron otros dos, y los tres son el mismo instinto.

Reordenar. En la transcripción: «apartar a los sprinters … y encontrar en nuestro terreno». El modelo devolvía «encontrar en nuestro terreno... a los sprinters», uniendo dos trozos separados con unos puntos suspensivos.

Arreglar. Terminar la frase que él dejó a medias, quitar el titubeo, poner la coma que falta.

Los dos son el modelo intentando que la cita quede mejor. Y los dos son mortales en un corpus que existe para reproducir cómo habla alguien de verdad, con sus arranques fallidos. De ahí la regla que acabó en el prompt:

segment.py — fragmento del prompt
1. LITERAL Y CONTIGUO. Copia un tramo SEGUIDO del fragmento, palabra por
palabra, empezando y acabando donde tú decidas pero sin saltarte nada de
en medio. Con sus titubeos, sus repeticiones y sus errores.

   Prohibido: corregir la gramática; completar lo que dejó a medias;
arreglar la puntuación; resumir; reordenar; y sobre todo unir dos trozos
separados con puntos suspensivos.

   Si lo retocas deja de ser una cita suya y el corpus entero pierde sentido.

Vídeos donde no habla él

La primera ejecución devolvió cero frases en cinco ventanas seguidas de un vídeo de 22.000 palabras. Antes de tocar nada, mirar el vídeo: una retransmisión de 1991 donde él no es comentarista, es el corredor. Los que hablan son otros, y hablan de él en tercera persona:

«Atención, Pedro Delgado… Que Perico Delgado no vaya más hacia el final»

Retransmisión de 1991: hablan de él, no habla él

O sea que la discriminación de hablante funcionaba perfectamente. Y estaba costando dinero por nada: 28 ventanas de ese vídeo, a unos dos céntimos cada una, para devolver cero.

El arreglo es contar vacías seguidas y abandonar el vídeo:

segment.py
EMPTY_WINDOWS_BEFORE_SKIP = 4

if empty_run >= EMPTY_WINDOWS_BEFORE_SKIP:
    print(f"  {row['video_id']}: {empty_run} empty windows, "
          f"skipping the rest", flush=True)
    skipped += 1
    break

Cuatro y no una: hay retransmisiones que arrancan con una entrevista suya en la salida y luego son tres horas de carrera. Con el umbral en cuatro, esa entrevista se recoge y el resto no se paga. De 102 vídeos, nueve acabaron sin aportar nada.

Lo que se eligió, y qué fijó cada cosa

Las 3.531 frases no son un dato de la naturaleza. Son el resultado de seis decisiones, la mayoría tomadas mientras se escribía el script:

Se eligióY con eso quedó fijado
Ventanas de 900 palabrasCuánto contexto ve el modelo para decidir quién habla. Más corto, peor atribución; más largo, más se le escapan frases
120 palabras de solapeQué frases sobreviven a un corte. Menos solape, se pierden las largas que caen en el borde
4 ventanas vacías antes de abandonarQué vídeos se exploran enteros. Con 1 se habrían perdido las entrevistas que abren una retransmisión
«Ante la duda sobre quién habla, descártala»La dirección del error: se pierden frases suyas antes que colar la voz de otro. Al revés que en la etapa 7, y a propósito — aquí no hay revisión posterior que lo cace
Sonnet 5El único modelo que llegó a ejecutarse sobre el corpus entero
Los cinco valores de devicesQué se puede etiquetar. Un recurso que no esté en la lista no aparece en la cobertura, aunque esté en el corpus

De todas ellas, la segunda fila del prompt es la que más frases cuesta y la más fácil de defender: hay material de sobra, y una frase mal atribuida no se detecta nunca más.

El resultado

terminal
3.531 frases · 93 vídeos · 86.632 palabras
3.531 de 3.531 literales (100%), 0 inventadas
$3,31 · 171 ventanas sobre 102 vídeos

Por recurso estilístico, contando que una frase puede llevar varios:

RecursoFrases
Metáfora1.784
Digresión1.384
Tic1.087
Disparate604
Refrán373

Y por confianza declarada por el modelo: 1.388 alta, 1.959 media, 184 baja. Las frases van de 3 a 159 palabras, con una media de 24.

Que la mayoría caiga en «media» es razonable y no es mala señal: en un chorro de texto sin marcas de hablante, estar seguro del todo es la excepción.

Las estimaciones volvieron a fallar

Antes de ejecutar había dos números estimados. Los dos salieron mal, y por bastante:

EstimadoMedido
Densidad1 frase por cada 300 palabras1 por cada 104
Coste~13 $3,31 $

Tres veces más frases por menos de la cuarta parte del dinero. Es la tercera vez que pasa esto en el proyecto — la etapa 3 estimó el corpus a partir de un solo podcast, la etapa 8 dio por buenas las transcripciones leyendo trozos del medio — y sigue siendo el mismo fallo: extrapolar en vez de contar.

Con un matiz que merece la pena, porque cambia lo que se puede hacer al respecto. Las dos veces anteriores la estimación fue optimista: prometía más de lo que había. Esta ha sido pesimista: había más corpus y más barato. Si el error tuviera siempre el mismo signo se podría corregir a ojo. No lo tiene. Por eso la única salida es medir.

Lo que se lleva uno de esta etapa

  1. Un prompt es una petición; un filtro es una garantía. Si algo tiene que cumplirse siempre, compruébalo en el código. Que el filtro no tire nada es el resultado que quieres, no una señal de que sobraba.
  2. Cuando el modelo devuelve cero, mira los datos antes que el prompt. Aquí no fallaba nada: es que en ese vídeo no hablaba él.
  3. Dile en qué dirección equivocarse, y que dependa de si hay revisión después. Sin revisión posterior, mejor perder material que colarlo.
  4. Solapa las ventanas y deduplica luego. Es más barato pagar una frase dos veces que perderla por caer en un borde.
  5. Extrapolar sale barato y contar sale exacto, y el error no tiene un signo fijo del que fiarse.