Automatice la creación de capítulos de vídeo con LLM y TF-IDF | de Yann-Aël Le Borgne | septiembre de 2024

Automatice la creación de capítulos de vídeo con LLM y TF-IDF | de Yann-Aël Le Borgne | septiembre de 2024

Los pasos clave en el flujo de trabajo implican estructurar la transcripción en párrafos (paso 2) antes de agrupar los párrafos en capítulos de los cuales se deriva una tabla de contenido (paso 4). Tenga en cuenta que estos dos pasos pueden depender de diferentes LLM: un LLM rápido y económico como LLama 3 8B para la tarea simple de edición de texto e identificación de párrafos, y un LLM más sofisticado como GPT-4o-mini para generar la tabla de contenido. . Entre los dos, TF-IDF se utiliza para agregar información de marca de tiempo a párrafos estructurados.

El resto del artículo describe cada paso con más detalle.

Consulte los artículos adjuntos. Repositorio de Github y cuaderno de Colab ¡Para explorar por ti mismo!

Tomemos como ejemplo la primera conferencia del curso “MIT 6.S191: Introducción al aprendizaje profundo” (Introducción al aprendizaje profundo.com) por Alexander Amini y Ava Amini (bajo licencia MIT).

Captura de pantalla de la página de YouTube del curso. El material del curso tiene la licencia MIT.

Tenga en cuenta que los capítulos ya se proporcionan en la descripción del video.

Los capítulos están disponibles en la descripción de YouTube.

Esto nos proporciona una base para comparar cualitativamente nuestros capítulos más adelante en este artículo.

API de transcripción de YouTube

Para los vídeos de YouTube, YouTube suele poner a disposición una transcripción generada automáticamente. Una manera conveniente de recuperar esta transcripción es llamar obtener_transcripción método pitón youtube_transcript_api biblioteca. El método toma YouTube. id_video biblioteca como argumento:

# https://www.youtube.com/watch?v=ErnWZxJovaM
video_id = "ErnWZxJovaM" # MIT Introduction to Deep Learning - 2024

# Retrieve transcript with the youtube_transcript_api library
from youtube_transcript_api import YouTubeTranscriptApi
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=["en"])

Esto devuelve la transcripción como una lista de pares clave-valor de texto y marca de tiempo:

[{'text': '[Music]', 'start': 1.17}, 
{'text': 'good afternoon everyone and welcome to', 'start': 10.28},
{'text': 'MIT sus1 191 my name is Alexander amini', 'start': 12.88},
{'text': "and I'll be one of your instructors for", 'start': 16.84},
...]

Sin embargo, la transcripción está mal formateada: carece de puntuación y contiene errores tipográficos (“MIT sus1 191” en lugar de “MIT 6.S191”, o “amini” en lugar de “Amini”).

Convertir voz en texto con Whisper

Alternativamente, se puede utilizar una biblioteca de voz a texto para inferir la transcripción de un archivo de vídeo o audio. Recomendamos usar susurrar más rápidoque es una implementación rápida de código abierto de última generación susurro modelo.

Los modelos están disponibles en diferentes tamaños. El más preciso es el “large-v3”, que es capaz de transcribir aproximadamente 15 minutos de audio por minuto en una GPU T4 (disponible de forma gratuita en Google Colab).

from faster_whisper import WhisperModel

# Load Whisper model
whisper_model = WhisperModel("large-v3",
device="cuda" if torch.cuda.is_available() else "cpu",
compute_type="float16",
)

# Call the Whisper transcribe function on the audio file
initial_prompt = "Use punctuation, like this."
segments, transcript_info = whisper_model.transcribe(audio_file, initial_prompt=initial_prompt, language="en")

El resultado de la transcripción se proporciona en forma de segmentos que se pueden convertir fácilmente en una lista de texto y marcas de tiempo como ocurre con el youtube_transcript_api biblioteca.

Consejo: a veces, susurrar puede no incluye puntuación. EL aviso_inicial El argumento se puede utilizar para solicitar al modelo que agregue puntuación proporcionando una oración corta que contenga puntuación.

A continuación se muestra un extracto de la transcripción de nuestro vídeo de ejemplo con Whisper Large-v3:

[{'start': 0.0, 'text': ' Good afternoon, everyone, and welcome to MIT Success 191.'},
{'start': 15.28, 'text': " My name is Alexander Amini, and I'll be one of your instructors for the course this year"},
{'start': 19.32, 'duration': 2.08, 'text': ' along with Ava.'}
...]

Tenga en cuenta que, en comparación con la transcripción de YouTube, se ha agregado puntuación. Sin embargo, persisten algunos errores de transcripción («MIT Success 191» en lugar de «MIT 6.S191»).

Una vez que la transcripción esté disponible, el segundo paso es editarla y estructurarla en párrafos.

La edición de una transcripción se refiere a los cambios realizados para mejorar la legibilidad. Esto implica, por ejemplo, añadir puntuación si falta, corregir errores gramaticales, eliminar tics verbales, etc.

La estructuración en párrafos también mejora la legibilidad y además sirve como paso de preprocesamiento para la identificación de capítulos en el paso 4, ya que los capítulos se formarán agrupando párrafos.

La edición y estructuración de párrafos se puede realizar en una sola operación, utilizando un LLM. A continuación ilustramos el resultado esperado de este paso:

Izquierda: transcripción sin procesar. Derecha: Transcripción editada y estructurada.

Esta tarea no requiere un LLM muy sofisticado ya que implica principalmente reformular el contenido. Al momento de escribir este artículo, se podían obtener resultados decentes con, por ejemplo, GPT-4o-mini o Llama 3 8B, y el siguiente mensaje del sistema:

Eres un asistente útil.

Su tarea es mejorar la legibilidad de la entrada del usuario: agregue puntuación cuando sea necesario y elimine los tics verbales, y estructure el texto en párrafos separados por «\n\n».

Mantenga la redacción lo más cercana posible al texto original.

Coloca tu respuesta entre etiquetas .

Contamos con API de finalización de chat compatible con OpenAI para la llamada LLM, con mensajes que tienen los roles de «sistema», «usuario» o «asistente». El siguiente código ilustra la creación de instancias de un cliente LLM con Groqusando Llama 3 8B:

# Connect to Groq with a Groq API key
llm_client = Groq(api_key=api_key)
model = "llama-8b-8192"

# Extract text from transcript
transcript_text = ' '.join([s['text'] for s in transcript])

# Call LLM
response = client.chat.completions.create(
messages=[
{
"role": "system",
"content": system_prompt
},
{
"role": "user",
"content": transcript_text
}
],
model=model,
temperature=0,
seed=42
)

Dado un fragmento de «texto_transcripción» sin procesar como entrada, esto devuelve un fragmento de texto editado en etiquetas. :

response_content=response.choices[0].message.content

print(response_content)
"""
<answer>
Good afternoon, everyone, and welcome to MIT 6.S191. My name is Alexander Amini, and I'll be one of your instructors for the course this year, along with Ava. We're really excited to welcome you to this incredible course.

This is a fast-paced and intense one-week course that we're about to go through together. We'll be covering the foundations of a rapidly changing field, and a field that has been revolutionizing many areas of science, mathematics, physics, and more.

Over the past decade, AI and deep learning have been rapidly advancing and solving problems that we didn't think were solvable in our lifetimes. Today, AI is solving problems beyond human performance, and each year, this lecture is getting harder and harder to teach because it's supposed to cover the foundations of the field.
</answer>
"""

Luego extraigamos el texto editado de las etiquetas. dividámoslo en párrafos y estructuremos los resultados como un diccionario JSON compuesto por números de párrafo y fragmentos de texto:

import re
pattern = re.compile(r'<answer>(.*?)</answer>', re.DOTALL)
response_content_edited = pattern.findall(response_content)
paragraphs = response_content_edited.strip().split('\n\n')
paragraphs_dict = [{'paragraph_number': i, 'paragraph_text': paragraph} for i, paragraph in enumerate(paragraphs)

print(paragraph_dict)

[{'paragraph_number': 0,
'paragraph_text': "Good afternoon, everyone, and welcome to MIT 6.S191. My name is Alexander Amini, and I'll be one of your instructors for the course this year, along with Ava. We're really excited to welcome you to this incredible course."},
{'paragraph_number': 1,
'paragraph_text': "This is a fast-paced and intense one-week course that we're about to go through together. We'll be covering the foundations of a rapidly changing field, and a field that has been revolutionizing many areas of science, mathematics, physics, and more."},
{'paragraph_number': 2,
'paragraph_text': "Over the past decade, AI and deep learning have been rapidly advancing and solving problems that we didn't think were solvable in our lifetimes. Today, AI is solving problems beyond human performance, and each year, this lecture is getting harder and harder to teach because it's supposed to cover the foundations of the field."}]

Tenga en cuenta que la entrada no debe ser demasiado larga, de lo contrario el LLM “olvidará” parte del texto. Para entradas largas, la transcripción debe dividirse en partes para mejorar la confiabilidad. Notamos que GPT-4o-mini maneja bien hasta 5000 caracteres, mientras que Llama 3 8B sólo puede manejar hasta 1500 caracteres. El cuaderno proporciona la función transcripción_en_párrafos que se encarga de dividir la transcripción en pedazos.

La transcripción ahora está estructurada como una lista de párrafos editados, pero las marcas de tiempo se perdieron en el proceso.

El tercer paso es agregar marcas de tiempo, deduciendo qué segmento de la transcripción sin procesar está más cerca de cada párrafo.

TF-IDF se utiliza para encontrar qué segmento de transcripción sin formato (derecha) coincide mejor con el comienzo de los párrafos editados (izquierda).

Para esta tarea nos apoyamos en Métrica TF-IDF. TF-IDF significa frecuencia del término – frecuencia inversa del documento y es una medida de similitud que permite comparar dos partes de un texto. La métrica funciona calculando la cantidad de palabras similares, dando más peso a las palabras que aparecen con menos frecuencia.

Como paso de preprocesamiento, ajustamos los segmentos de la transcripción y los encabezados de párrafo para que contengan la misma cantidad de palabras. Las partes del texto deben ser lo suficientemente largas para que los comienzos de los párrafos puedan asociarse correctamente con un único segmento de transcripción. Descubrimos que utilizar 50 palabras funciona bien en la práctica.


num_words = 50

transcript_num_words = transform_text_segments(transcript, num_words=num_words)
paragraphs_start_text = [{"start": p['paragraph_number'], "text": p['paragraph_text']} for p in paragraphs]
paragraphs_num_words = transform_text_segments(paragraphs_start_text, num_words=num_words)

Entonces nos basamos en el Aprender biblioteca y sus TfidfVectorizador Y coseno_similitud función para ejecutar TF-IDF y calcular las similitudes entre cada inicio de párrafo y segmento de transcripción. A continuación se muestra un código de muestra para encontrar el mejor índice de coincidencia en los segmentos de transcripción del primer párrafo.

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# Paragraph for which to find the timestamp
paragraph_i = 0

# Create a TF-IDF vectorizer
vectorizer = TfidfVectorizer().fit_transform(transcript_num_words + paragraphs_num_words)
# Get the TF-IDF vectors for the transcript and the excerpt
vectors = vectorizer.toarray()
# Extract the TF-IDF vector for the paragraph
paragraph_vector = vectors[len(transcript_num_words) + paragraph_i]

# Calculate the cosine similarity between the paragraph vector and each transcript chunk
similarities = cosine_similarity(vectors[:len(transcript_num_words)], paragraph_vector.reshape(1, -1))
# Find the index of the most similar chunk
best_match_index = int(np.argmax(similarities))

Hemos envuelto el proceso en un agregar marcas de tiempo a los párrafos función que agrega marcas de tiempo a los párrafos, junto con el índice y el texto del segmento correspondiente:

paragraphs = add_timestamps_to_paragraphs(transcript, paragraphs, num_words=50)

#Example of output for the first paragraph:
print(paragraphs[0])

{'paragraph_number': 0,
'paragraph_text': "Good afternoon, everyone, and welcome to MIT 6.S191. My name is Alexander Amini, and I'll be one of your instructors for the course this year, along with Ava. We're really excited to welcome you to this incredible course.",
'matched_index': 1,
'matched_text': 'good afternoon everyone and welcome to',
'start_time': 10}

En el ejemplo anterior, el primer párrafo (numerado 0) corresponde al segmento de transcripción número 1 que comienza en el momento 10 (en segundos).

Luego, el índice se encuentra agrupando párrafos consecutivos en capítulos e identificando los títulos de capítulos importantes. La tarea la realiza principalmente un LLM, que es responsable de transformar una entrada que consta de una lista de párrafos JSON en una salida que consta de una lista de títulos de capítulos JSON con números de párrafo iniciales:

system_prompt_paragraphs_to_toc = """

You are a helpful assistant.

You are given a transcript of a course in JSON format as a list of paragraphs, each containing 'paragraph_number' and 'paragraph_text' keys.

Your task is to group consecutive paragraphs in chapters for the course and identify meaningful chapter titles.

Here are the steps to follow:

1. Read the transcript carefully to understand its general structure and the main topics covered.
2. Look for clues that a new chapter is about to start. This could be a change of topic, a change of time or setting, the introduction of new themes or topics, or the speaker's explicit mention of a new part.
3. For each chapter, keep track of the paragraph number that starts the chapter and identify a meaningful chapter title.
4. Chapters should ideally be equally spaced throughout the transcript, and discuss a specific topic.

Format your result in JSON, with a list dictionaries for chapters, with 'start_paragraph_number':integer and 'title':string as key:value.

Example:
{"chapters":
[{"start_paragraph_number": 0, "title": "Introduction"},
{"start_paragraph_number": 10, "title": "Chapter 1"}
]
}
"""

Una parte importante es solicitar específicamente la salida JSON, lo que aumenta las posibilidades de obtener una salida JSON formateada correctamente que luego se puede volver a cargar en Python.

GPT-4o-mini se utiliza para esta tarea porque es más rentable que el GPT-4o de OpenAI y generalmente proporciona buenos resultados. Las instrucciones se proporcionan a través de la función «sistema» y los párrafos se proporcionan en formato JSON a través de la función «usuario».

# Connect to OpenAI with an OpenAI API key
llm_client_get_toc = OpenAI(api_key=api_key)
model_get_toc = "gpt-4o-mini-2024-07-18"

# Dump JSON paragraphs as text
paragraphs_number_text = [{'paragraph_number': p['paragraph_number'], 'paragraph_text': p['paragraph_text']} for p in paragraphs]
paragraphs_json_dump = json.dumps(paragraphs_number_text)

# Call LLM
response = client_get_toc.chat.completions.create(
messages=[
{
"role": "system",
"content": system_prompt_paragraphs_to_toc
},
{
"role": "user",
"content": paragraphs_json_dump
}
],
model=model_get_toc,
temperature=0,
seed=42
)

¡Y ahí lo tienes! La llamada devuelve la lista de títulos de capítulos así como el número de párrafo inicial en formato JSON:

print(response)

{
"chapters": [
{
"start_paragraph_number": 0,
"title": "Introduction to the Course"
},
{
"start_paragraph_number": 17,
"title": "Foundations of Intelligence and Deep Learning"
},
{
"start_paragraph_number": 24,
"title": "Course Structure and Expectations"
}
....
]
}

Al igual que en el paso 2, el LLM puede tener dificultades con entradas largas y rechazar parte de la entrada. La solución es nuevamente dividir la entrada en partes, lo cual se implementa en el bloc de notas con el párrafos_a_la_tabla_de_contenidos función y tamaño_pieza configuración.

Este último paso combina los párrafos y la tabla de contenido para crear un archivo JSON estructurado con capítulos, un ejemplo del cual se proporciona en el Soporte para el repositorio de Github.

A continuación ilustramos el capítulo resultante (derecha), en comparación con el capítulo base que estaba disponible en la descripción de YouTube (izquierda):