Los pasos clave del flujo de trabajo consisten en estructurar la transcripción en párrafos (paso 2) antes de agrupar los párrafos en capítulos de los que se deriva una tabla de contenidos (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 la generación de la tabla de contenidos. Entretanto, se utiliza TF-IDF para agregar información de marca de tiempo a los párrafos estructurados.
El resto de la publicación describe cada paso con más detalle.
Vea el adjunto Repositorio de Github y cuaderno de Colab ¡Para explorar por tu cuenta!
Tomemos como ejemplo La primera conferencia del curso ‘MIT 6.S191: Introducción al aprendizaje profundo’ (Introducción a Deep Learning.com) por Alexander Amini y Ava Amini (Licenciado bajo la licencia MIT).
Tenga en cuenta que los capítulos ya están proporcionados en la descripción del video.
Esto nos proporciona una base para comparar cualitativamente nuestra división en capítulos más adelante en esta publicación.
API de transcripción de YouTube
En el caso de los vídeos de YouTube, YouTube suele poner a disposición una transcripción generada automáticamente. Una forma cómoda de recuperar esa transcripción es llamar al obtener_transcripción método de Python API de transcripción de YouTube biblioteca. El método toma YouTube id del vídeo 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’).
Conversión de voz a texto con Whisper
Como alternativa, se puede utilizar una biblioteca de conversión de voz a texto para inferir la transcripción de un archivo de video o audio. Recomendamos utilizar susurro más rápidoque es una implementación rápida del código abierto de última generación. susurro modelo.
Los modelos vienen en diferentes tamaños. El más preciso es el ‘large-v3’, que es capaz de transcribir unos 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 en el caso del API de transcripción de YouTube biblioteca.
Consejo: El susurro a veces puede no incluye la puntuación. El aviso inicial El argumento se puede utilizar para impulsar al modelo a agregar puntuación proporcionando una pequeña oración que contenga puntuación.
A continuación se muestra un extracto de la transcripción de nuestro ejemplo de video 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 agregó la puntuación. Sin embargo, aún quedan algunos errores de transcripción («MIT Success 191» en lugar de «MIT 6.S191»).
Una vez disponible la transcripción, la segunda etapa consiste en editar y estructurar la transcripción en párrafos.
La edición de transcripciones se refiere a los cambios que se realizan para mejorar la legibilidad. Esto implica, por ejemplo, agregar 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 identificar capítulos en la etapa 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 esta etapa:
Esta tarea no requiere un LLM muy sofisticado, ya que consiste principalmente en reformular el contenido. En el 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 de sistema:
Eres un asistente útil.
Su tarea es mejorar la legibilidad de la entrada del usuario: agregue puntuación si es necesario y elimine tics verbales, y estructurar el texto en párrafos separados con ‘\n\n’.
Mantenga la redacción lo más fiel posible al texto original.
Coloque su respuesta dentro de las etiquetas
.
Confiamos en API de finalización de chat compatible con OpenAI para llamadas LLM, con mensajes que tienen los roles de ‘sistema’, ‘usuario’ o ‘asistente’. El código a continuación ilustra la instanciación de un cliente LLM con Crecerutilizando 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 ‘transcript_text’ sin procesar como entrada, esto devuelve un fragmento de texto editado dentro de las etiquetas
response_content=response.choices[0].message.contentprint(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
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, ya que de lo contrario el LLM «olvidará» parte del texto. Para entradas largas, la transcripción debe dividirse en fragmentos para mejorar la confiabilidad. Notamos que GPT-4o-mini maneja bien hasta 5000 caracteres, mientras que Llama 3 8B solo puede manejar hasta 1500 caracteres. El cuaderno proporciona la función transcripción_a_párrafos que se encarga de dividir la transcripción en fragmentos.
La transcripción ahora está estructurada como una lista de párrafos editados, pero las marcas de tiempo se han perdido en el proceso.
La tercera etapa consiste en volver a agregar marcas de tiempo, infiriendo qué segmento de la transcripción sin procesar es el más cercano a cada párrafo.
Para esta tarea contamos con la Métrica TF-IDF. TF-IDF significa frecuencia del término–frecuencia inversa del documento y es una medida de similitud para comparar dos fragmentos de texto. La medida funciona calculando la cantidad de palabras similares y dando más peso a las palabras que aparecen con menos frecuencia.
Como paso previo al procesamiento, ajustamos los segmentos de transcripción y los comienzos de párrafos para que contengan la misma cantidad de palabras. Los fragmentos de texto deben ser lo suficientemente largos para que los comienzos de párrafos puedan coincidir correctamente con un segmento de transcripción único. Descubrimos que usar 50 palabras funciona bien en la práctica.
num_words = 50transcript_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)
Luego nos basamos en la aprender biblioteca y su Vectorizador Tfidf y similitud_de_coseno Función para ejecutar TF-IDF y calcular similitudes entre el comienzo de cada párrafo y el segmento de transcripción. A continuación se muestra un ejemplo de código 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))
Envolvimos 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 del segmento y el texto coincidentes:
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, se descubre que el primer párrafo (numerado 0) coincide con el segmento de transcripción número 1 que comienza en el tiempo 10 (en segundos).
El índice se encuentra agrupando párrafos consecutivos en capítulos e identificando títulos de capítulos significativos. La tarea la lleva a cabo principalmente un LLM, al que se le ordena transformar una entrada que consiste en una lista de párrafos JSON en una salida que consiste en una lista de títulos de capítulos JSON con los 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"}
]
}
"""
Un elemento importante es solicitar específicamente una salida JSON, lo que aumenta las posibilidades de obtener una salida JSON con el formato correcto que luego pueda cargarse nuevamente en Python.
Para esta tarea se utiliza GPT-4o-mini, ya que es más rentable que GPT-4o de OpenAI y, en general, ofrece buenos resultados. Las instrucciones se proporcionan a través del rol «sistema» y los párrafos se proporcionan en formato JSON a través del rol «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 listo! La llamada devuelve la lista de títulos de capítulos junto con 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 problemas con entradas largas y descartar parte de la entrada. La solución consiste nuevamente en dividir la entrada en fragmentos, lo que se implementa en el cuaderno con el párrafos_a_tabla_de_contenido función y la tamaño del trozo parámetro.
Esta última etapa combina los párrafos y la tabla de contenidos para crear un archivo JSON estructurado con capítulos, un ejemplo del cual se proporciona en el Repositorio Github adjunto.
A continuación, ilustramos la división en capítulos resultante (derecha), en comparación con la división en capítulos de referencia que estaba disponible en la descripción de YouTube (izquierda):