V0.0.2
Operai-Whisper-Talk es una aplicación de conversación de voz de muestra impulsada por tecnologías de OpenAI como Whisper, un sistema automático de reconocimiento de voz (ASR), finalizaciones de chat, una interfaz que simula una conversación con un modelo que juega el papel de Asistente, Incrustaciones, Convierte el texto en datos vectoriales que se pueden usar en tareas como la búsqueda y el último texto de texto, ese text en el texto de Spovey LifeLeke. La aplicación se crea con NUXT, un marco JavaScript basado en Vue.js.
La aplicación tiene dos nuevas características: gestión de programas y memoria a largo plazo. Con la gestión de programas, puede ordenar el chatbot para agregar, modificar, eliminar y recuperar eventos programados. La función de memoria a largo plazo le permite almacenar fragmentos de información que el chatbot recordará para referencia futura. Puede integrar sin problemas ambas funciones en sus conversaciones simplemente interactuando con el chatbot.
ACTUALIZACIÓN: Actualizó el módulo OpenAI a la última versión y reemplazó el modelo de incrustación de
text-embedding-ada-002al nuevo modelo V3text-embedding-3-small.
OpenAI-Whisper-Talk は、 Whisper (自動音声認識( ASR )システム)、 Terminaciones de chat (アシスタントの役割を果たすモデルとの会話をシミュレートするインターフェース)、 Incorporación (セマンティック検索などのタスクで使用できるベクターデータにテキストを変換する)、そして最新の Texto a voz (テキストをリアルな話し言葉のオーディオに変える)など、 OpenAI の技術を駆使したサンプル音声会話アプリケーションです。このアプリケーションは、 Vue.js に基づいた Javascript フレームワークである Nuxt を使用して構築されています。
このアプリケーションには、「スケジュール管理」と「永続メモリ」の 2 つの新機能があります。スケジュール管理を使用すると、チャットボットにスケジュールイベントの追加、変更、削除、取得を指示できます。永続メモリ機能を使用すると、将来の参照のためにチャットボットが覚えておく情報のスニペットを保存できます。これらの機能をチャットボットとの対話を通じてシームレスに統合することができます。将来的に、いくつかの機能強化、たとえばメールやメッセージング機能を追加することで、完全な個人アシスタントになるかもしれません。

Desde la página principal, puede elegir con qué chatbot interactuar. Cada chatbot tiene una personalidad distinta, habla un lenguaje diferente y posee una voz única. Puede alterar el nombre y la personalidad de cualquier chatbot haciendo clic en el botón Editar adyacente a su nombre. Actualmente, la interfaz de usuario no admite directamente la adición de nuevos chatbots; Sin embargo, puede agregar manualmente un chatbot y personalizar la configuración de voz y idioma para cada uno modificando el archivo activos/contactos.json.

Además, puede personalizar su perfil haciendo clic en el icono Avatar en la esquina superior derecha. Esto le permite ingresar su nombre y compartir detalles sobre usted, permitiendo que el chatbot interactúe con usted de una manera más personalizada.


Los datos de audio se registran automáticamente si se detecta el sonido. Hay una configuración de umbral disponible para evitar que el ruido de fondo active la captura de audio. Por defecto, esto se establece en -45DB (con -10DB que representa el corte de sonido más fuerte). Puede ajustar este umbral modificando la variable MIN_DECIBELS 1 de acuerdo con sus necesidades.
Cuando la grabación está habilitada y no se detecta ningún sonido durante 3 segundos , los datos de audio se cargan y se envían al backend para la transcripción. Vale la pena señalar que en las conversaciones típicas, la brecha promedio entre cada turno es de alrededor de 2 segundos. Del mismo modo, la pausa entre las oraciones al hablar es aproximadamente la misma. Por lo tanto, he elegido una duración que es lo suficientemente larga como para significar esperar una respuesta. Puede ajustar esta duración editando la variable MAX_COUNT 1 .
El sistema puede registrar continuamente los datos de audio hasta que se reciba una respuesta. Una vez que se recibe y reproduce la respuesta de audio del chatbot, la grabación de audio está deshabilitada para evitar la grabación inadvertida de la propia respuesta del chatbot.
También se proporciona una entrada de texto si desea escribir sus mensajes.
Todos los datos de audio grabados se cargan en el directorio public/upload en formato de archivo Webm. Antes de enviar el archivo de audio a la API Whisper, es necesario eliminar todos los segmentos silenciosos. Este paso ayuda a prevenir el conocido problema de las alucinaciones generadas por Whisper. Por esta misma razón, se recomienda establecer el valor MIN_DECIBELS lo más alto posible, asegurando que solo se registre el habla.
Para eliminar las piezas silenciosas del audio, usaremos ffmpeg . Asegúrese de instalarlo.
ffmpeg -i sourceFile -af silenceremove=stop_periods=-1:stop_duration=1:stop_threshold=-50dB outputFileEn este comando:
-1 sourceFile especifica el archivo de entrada.-af silenceremove aplica el filtro silencerremove .stop_periods=-1 elimina todos los períodos de silencio.stop_duration=1 establece cualquier período de silencio de más de 1 segundo como silencio.stop_threshold=-50dB establece cualquier nivel de ruido por debajo de -50dB como silencio.outputFile el archivo de salida. Para invocar este comando shell en nuestra ruta API, usaremos exec del módulo child-process .
Después de eliminar las piezas silenciosas, se debe verificar el tamaño del archivo. El tamaño final del archivo suele ser mucho más pequeño que el original. Durante esta verificación, se ignoran los archivos de más de 16 kb, suponiendo que un archivo de este tamaño, con una profundidad de 16 bits, equivale a aproximadamente un segundo de audio. Todo lo más corto probablemente sea inaudible.
// simple file size formula
const audio_file_size = duration_in_seconds * bitrateTodo el proceso, desde eliminar las piezas silenciosas hasta el tamaño de la verificación del archivo, está diseñado para garantizar que solo se envíen datos de audio viables a la API Whisper. Nuestro objetivo es evitar la alucinación y la transmisión innecesaria de datos.
Habiendo confirmado la viabilidad de nuestros datos de audio, luego procedemos a llamar a la API Whisper.
const transcription = await openai . audio . transcriptions . create ( {
file : fs . createReadStream ( filename ) ,
language : lang ,
response_format : 'text' ,
temperature : 0 ,
} ) Donde lang es el código ISO 639-1 del lenguaje especificado de nuestro chatbot. Consulte la lista del idioma compatible actualmente.
Optamos por un formato text simple ya que no se requieren marcas de tiempo, y establecemos el parámetro temperature en cero para lograr una salida determinista.
Después de recibir la transcripción de Whisper, procedemos a enviarla a la API de finalización de chat con llamadas de funciones. Utilizamos el último módulo Node.js de OpenAI (versión 4) que se lanzó ayer (2023/11/07), que incluye un formato de llamada de funciones actualizado. Esta nueva iteración permite la invocación de múltiples funciones en una sola solicitud.
let messages = [ { role : 'system' , content : system_prompt } ]
let all_history = await mongoDb . getMessages ( )
if ( all_history . length > 0 ) {
const history_context = trim_array ( all_history , 20 )
messages = messages . concat ( history_context )
}
messages . push ( { role : 'user' , content : user_query } )
let response = await openai . chat . completions . create ( {
temperature : 0.3 ,
messages ,
tools : [
{ type : 'function' , function : add_calendar_entry } ,
{ type : 'function' , function : get_calendar_entry } ,
{ type : 'function' , function : edit_calendar_entry } ,
{ type : 'function' , function : delete_calendar_entry } ,
{ type : 'function' , function : save_new_memory } ,
{ type : 'function' , function : get_info_from_memory }
]
} )Disecemos los diversos componentes de esta llamada en las secciones posteriores.
El indicador del sistema juega un papel crucial en dar vida a nuestro chatbot. Es aquí donde establecemos su nombre y personalidad, según el chatbot elegido por el usuario. Le proporcionamos instrucciones específicas sobre cómo responder, junto con una lista de funciones que puede ejecutar. También le informamos sobre la identidad del usuario y algunos datos personales. Finalmente, establecemos la fecha y hora actuales, lo cual es esencial para activar las funciones del calendario.
const today = new Date ( )
let system_prompt = `In this session, we will simulate a voice conversation between two friends.nn` +
`# Personan` +
`You will act as ${ selPerson . name } .n` +
` ${ selPerson . prompt } nn` +
`Please ensure that your responses are consistent with this persona.nn` +
`# Instructionsn` +
`The user is talking to you over voice on their phone, and your response will be read out loud with realistic text-to-speech (TTS) technology.n` +
`Use natural, conversational language that are clear and easy to follow (short sentences, simple words).n` +
`Keep the conversation flowing.n` +
`Sometimes the user might just want to chat. Ask them relevant follow-up questions.nn` +
`# Functionsn` +
`You have the following functions that you can call depending on the situation.n` +
`add_calendar_entry, to add a new event.n` +
`get_calendar_entry, to get the event at a particular date.n` +
`edit_calendar_entry, to edit or update existing event.n` +
`delete_calendar_entry, to delete an existing event.n` +
`save_new_memory, to save new information to memory.n` +
`get_info_from_memory, to retrieve information from memory.nn` +
`When you present the result from the function, only mention the relevant details for the user query.n` +
`Omit information that is redundant and not relevant to the query.n` +
`Always be concise and brief in your replies.nn` +
`# Usern` +
`As for me, in this simulation I am known as ${ user_info . name } .n` +
` ${ user_info . prompt } nn` +
`# Todayn` +
`Today is ${ today } .n` Todos los mensajes se almacenarán en MongoDB .
Para administrar los tokens y evitar superar el límite máximo del modelo, solo enviaremos las últimas 20 interacciones. La función trim_array está diseñada para recortar el historial de mensajes si supera las 20 vueltas. Este umbral se puede ajustar para cumplir con sus requisitos específicos.
// allow less than or equal to 15 turns
const history_context = trim_array ( all_history , 15 ) Desde la pantalla principal, tiene la opción de borrar el historial anterior para cada chatbot.

Tenga en cuenta que hemos separado el manejo de las llamadas de funciones (function_call.js) de la llamada API de finalización de chat principal (transcribe.php). Esta distinción se hace para abordar las instancias cuando el contenido de texto está presente mientras se invoca una llamada de función, que generalmente es nula. Esta separación permite que la aplicación muestre el texto mientras se procesa simultáneamente la llamada de función. También he incluido el proceso dentro de un bucle en caso de que una segunda llamada API resulte en otra llamada de función. Esto probablemente podría manejarse con más elegancia implementando transmisión , pero todavía tengo que aprender a usar la transmisión con Nuxt.
Nuestras funciones se clasifican en dos nuevas características: gestión de programas y memoria a largo plazo .
Primero examinemos cómo gestionamos las llamadas de funciones con el nuevo formato. Debemos utilizar el parámetro de Nuevas Herramientas en lugar del parámetro de funciones ahora deprimido para habilitar la invocación de múltiples llamadas de funciones.
let response = await openai . chat . completions . create ( {
temperature : 0.3 ,
messages ,
tools : [
{ type : 'function' , function : add_calendar_entry } ,
{ type : 'function' , function : get_calendar_entry } ,
{ type : 'function' , function : edit_calendar_entry } ,
{ type : 'function' , function : delete_calendar_entry } ,
{ type : 'function' , function : save_new_memory } ,
{ type : 'function' , function : get_info_from_memory }
]
} ) Verifique el esquema JSON de cada funciones de lib/ Directorio. Aquí está para add_calendar_entry :
{
"name" : "add_calendar_entry" ,
"description" : "Adds a new entry to the calendar" ,
"parameters" : {
"type" : "object" ,
"properties" : {
"event" : {
"type" : "string" ,
"description" : "The name or title of the event"
} ,
"date" : {
"type" : "string" ,
"description" : "The date of the event in 'YYYY-MM-DD' format"
} ,
"time" : {
"type" : "string" ,
"description" : "The time of the event in 'HH:MM' format"
} ,
"additional_detail" : {
"type" : "string" ,
"description" : "Any additional details or notes related to the event"
}
} ,
"required" : [ "event" , "date" , "time" , "additional_detail" ]
}
}Para difusiones de cómo funcionan todos estos, pasemos a la siguiente sección.
Para la gestión del cronograma , tenemos las siguientes funciones:
Todas las entradas de calendario se almacenarán en MongoDB. Tenga en cuenta que todas las entradas serán accesibles para todos los chatbots.
Echemos un vistazo a una conversación de chat de muestra para demostrar cómo interactúan estos elementos:
Usuario: Buenos días, Jeeves. ¿Cuál es mi horario para hoy?
CALLAR DE FUNCIÓN (invocar get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [ {
id : 'call_4cCtmNlgYN5o4foVOQM9MIdA' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-10"}' }
} ]
}Respuesta de la función:
[
{
tool_call_id : 'call_4cCtmNlgYN5o4foVOQM9MIdA' ,
role : 'tool' ,
name : 'get_calendar_entry' ,
content : '{n' +
' "message": "Found 3 entries",n' +
' "items": [n' +
' {n' +
' "_id": "654b2805e51fcd815dea8e2d",n' +
' "event": "Project presentation",n' +
' "date": "2023-11-10",n' +
' "time": "10:00",n' +
' "additional_detail": "Important project presentation",n' +
' "__v": 0n' +
' },n' +
' {n' +
' "_id": "654d7b53ff0bf31de6c2af29",n' +
' "event": "Meet Tony",n' +
' "date": "2023-11-10",n' +
' "time": "19:00",n' +
' "additional_detail": "Electric Sheep Bar",n' +
' "__v": 0n' +
' },n' +
' {n' +
' "_id": "654d7d868751ea8704fc9f11",n' +
' "event": "Dinner with Anna",n' +
' "date": "2023-11-10",n' +
' "time": "19:00",n' +
' "additional_detail": "Electric Sheep Bar",n' +
' "__v": 0n' +
' }n' +
' ]n' +
'}'
}
]Resumen del chat:
{ role : 'assistant' ,
content : 'Good morning, Jerry. Today, you have a project presentation at 10:00 AM and a dinner meeting with Anna at 7:00 PM at the Electric Sheep Bar. You also have a meeting with Tony at 7:00 PM at the same location.'
}Usuario: parece redundante tener una entrada separada para el mismo evento. ¿Puedes editar el evento con Anna para incluir a Tony y eliminar el otro evento?
Función llamada (invocada edit_calendar_entry y delete_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_ZVAwO5YsXMuWr8876tyCIPIX' ,
type : 'function' ,
function : {
name : 'edit_calendar_entry' ,
arguments : '{"event": "Dinner Meeting with Anna", "date": "2023-11-10", "time": "19:00", "additional_detail": "Meeting with Tony included."}'
}
} ,
{
id : 'call_eMGuI8LxLbXFsLp8FduCrxgp' ,
type : 'function' ,
function : {
name : 'delete_calendar_entry' ,
arguments : '{"delete_type": "event_name", "event": "Meeting with Tony"}'
}
}
]
}Respuesta de la función:
[ {
tool_call_id : 'call_ZVAwO5YsXMuWr8876tyCIPIX' ,
role : 'tool' ,
name : 'edit_calendar_entry' ,
content : '{n "message": "Entry edited",n "name": "Dinner Meeting with Anna"n}'
} ,
{
tool_call_id : 'call_eMGuI8LxLbXFsLp8FduCrxgp' ,
role : 'tool' ,
name : 'delete_calendar_entry' ,
content : '{n "name": "Meeting with Tony",n "message": "Entry deleted"n}'
}
]Resumen del chat:
{ role : 'assistant' ,
content : 'The event "Dinner Meeting with Anna" has been updated to include the meeting with Tony, and the separate event "Meeting with Tony" has been deleted.'
}Usuario: Bien, solo para estar seguro, ¿puedes decirme mi horario de nuevo por hoy?
CALLAR DE FUNCIÓN (invocar get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_liqFn24Ds4qp0cy2V5NBg62v' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-10"}' }
}
]
}Respuesta de la función:
[ {
tool_call_id : 'call_liqFn24Ds4qp0cy2V5NBg62v' ,
role : 'tool' ,
name : 'get_calendar_entry' ,
content : '{n' +
' "message": "Found 2 entries",n' +
' "items": [n' +
' {n' +
' "_id": "654b2805e51fcd815dea8e2d",n' +
' "event": "Project presentation",n' +
' "date": "2023-11-10",n' +
' "time": "10:00",n' +
' "additional_detail": "Important project presentation",n' +
' "__v": 0n' +
' },n' +
' {n' +
' "_id": "654d7d868751ea8704fc9f11",n' +
' "event": "Dinner with Anna",n' +
' "date": "2023-11-10",n' +
' "time": "19:00",n' +
' "additional_detail": "Meeting with Tony included.",n' +
' "__v": 0n' +
' }n' +
' ]n' +
'}'
}
]Resumen del chat:
{ role : 'assistant' ,
content : 'Your schedule for today is as follows:n' +
'- Project presentation at 10:00 AMn' +
'- Dinner with Anna at 7:00 PM, which includes a meeting with Tony.'
}Usuario: Ah, olvidaste agregar la ubicación en la cena. Lo siento, pero ¿puedes actualizarlo de nuevo?
Función llamando (invocado edit_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_eRTFMcz2l8qHO7nhp3msI3v8' ,
type : 'function' ,
function : {
name : 'edit_calendar_entry' ,
arguments : '{"event":"Dinner with Anna","date":"2023-11-10","time":"19:00","additional_detail":"Meeting with Tony at the Electric Sheep Bar."}'
}
}
]
}Respuesta de la función:
{
tool_call_id : 'call_eRTFMcz2l8qHO7nhp3msI3v8' ,
role : 'tool' ,
name : 'edit_calendar_entry' ,
content : '{n "message": "Entry edited",n "name": "Dinner with Anna"n}'
}Resumen del chat:
{ role : 'assistant' ,
content : 'The event "Dinner with Anna" has been updated to include the meeting with Tony at the Electric Sheep Bar.'
}Usuario: Bien, quiero confirmarlo nuevamente, ¿puedes decirme mi horario para hoy?
Función llamada (invocada get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_HYlAj4BTeWcu2CxSp4zBYPuC' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-10"}' }
}
]
}Respuesta de la función:
[ {
tool_call_id : 'call_HYlAj4BTeWcu2CxSp4zBYPuC' ,
role : 'tool' ,
name : 'get_calendar_entry' ,
content : '{n' +
' "message": "Found 2 entries",n' +
' "items": [n' +
' {n' +
' "_id": "654b2805e51fcd815dea8e2d",n' +
' "event": "Project presentation",n' +
' "date": "2023-11-10",n' +
' "time": "10:00",n' +
' "additional_detail": "Important project presentation",n' +
' "__v": 0n' +
' },n' +
' {n' +
' "_id": "654d7d868751ea8704fc9f11",n' +
' "event": "Dinner with Anna",n' +
' "date": "2023-11-10",n' +
' "time": "19:00",n' +
' "additional_detail": "Meeting with Tony at the Electric Sheep Bar.",n' +
' "__v": 0n' +
' }n' +
' ]n' +
'}'
}
]Resumen del chat:
{ role : 'assistant' ,
content : 'Your schedule for today is as follows:n' +
'- Project presentation at 10:00 AMn' +
'- Dinner with Anna at 7:00 PM, which includes a meeting with Tony at the Electric Sheep Bar.'
}Para la memoria a largo plazo , tenemos las siguientes funciones
Todas las funciones anteriores se han centrado principalmente en la recuperación de datos típica y las tareas de configuración. Pero para la recuperación de memoria a largo plazo, no podemos simplemente hacer una búsqueda de palabras clave. Necesitamos considerar el contexto de la consulta. Necesitamos hacer una búsqueda semántica. Y en este sentido, utilizaremos la API de incrustaciones . Pasemos a la siguiente sección para discutir esto más a fondo.
ACTUALIZACIÓN : He reemplazado
text-embedding-ada-002context-embedding-3-small. Según mis pruebas, este último funciona lo suficientemente bien. Los puntajes de similitud coseno entre las respuestas estrechamente relacionadas y las respuestas no relevantes son notablemente distintas. Además, el rendimiento de costo del modelo pequeño V3, con un precio de $ 0.00002/1K tokens, es obvio? Sin embargo, debe convertir sus datos vectoriales de ADA a V3 Small ya que no son compatibles. Tenga en cuenta que también bajamos el umbral de 0.72 a 0.3.
En pocas palabras, las incrustaciones miden la relación de las cadenas de texto. Si llamamos a la API, nos dará datos vectoriales de números flotantes asociados con el texto de entrada.
const embedding = await openai . embeddings . create ( {
model : "text-embedding-3-small" , //"text-embedding-ada-002",
input : "The quick brown fox jumped over the lazy dog" ,
encoding_format : "float" ,
} )Para usar esto en nuestra aplicación, implementaremos lo que llaman una generación de recuperación auggada o trapo simple.
Inicialmente, cuando se reciben nuevos datos de la función save_new_memory, llamamos a la API de incrustaciones para generar su representación vectorial. Estos datos vectoriales se almacenan en MongoDB para uso futuro.
Posteriormente, cuando un usuario envía una consulta que requiere recuperación de memoria, se activa la función get_info_from_memory. Llamamos a la API de incrustaciones para los parámetros de búsqueda y los compara con los datos del vector almacenado utilizando una similitud de coseno simple. Esta comparación generalmente produce varios partidos con puntajes variables. Hemos establecido nuestro umbral en una puntuación de 0.3 (0.72 para el modelo ADA) o superior, y limitamos los resultados a un máximo de 10.
Los resultados se pasan a la API final de finalización de chat, que determina la respuesta más adecuada a la consulta del usuario. La IA tiene la capacidad de seleccionar uno o más resultados como base para su respuesta, dependiendo de la naturaleza de la consulta. Aquí es donde se demuestra el verdadero poder de la IA. En lugar de simplemente regurgitar toda la información que recibe, la IA analiza los datos y formula una respuesta apropiada. Si el resultado del trapo se considera suficiente, generará una respuesta de texto positiva.
Para ayudar a facilitar el almacenamiento de datos vectoriales y otras operaciones vectoriales en MongoDB , estoy usando el módulo Mongoose. Con mucho, esta es la solución más indolora para manejar el almacenamiento y la recuperación de los datos vectoriales.
Primero, definimos nuestro esquema:
const memoryVectorsSchema = new mongoose . Schema ( {
chunks : [ {
embedding : [ Number ] ,
text : String
} ]
} ) Luego adjuntamos la función getScore que ejecuta la similitud de coseno entre el modelo de documento y la cadena de consulta integrada:
memoryVectorsSchema . methods . getScore = function getScore ( query_embedding ) {
return this . chunks . map ( ( chunk ) => {
const dot_product = chunk . embedding . reduce ( ( sum , val , i ) => sum + val * query_embedding [ i ] , 0 )
const magnitude = ( vec ) => Math . sqrt ( vec . reduce ( ( sum , val ) => sum + val * val , 0 ) )
const cosine_similarity = dot_product / ( magnitude ( chunk . embedding ) * magnitude ( query_embedding ) )
return { text : chunk . text , score : cosine_similarity }
} )
}A continuación, compilamos nuestro esquema en un modelo:
const MemoryVectors = mongoose . model ( 'MemoryVectors' , memoryVectorsSchema )Para guardar los datos vectoriales:
const newVector = new MemoryVectors ( {
chunks
} )
newVector . save ( )Para buscar los datos del vector almacenado con los incrustaciones de la consulta del usuario
const cosineSimThreshold = 0.3 // Note: for text-embedding-ada-002, 0.72 but for v3 embed models, use 0.3
const maxResults = 10
const maxFilesLength = 2000 * 3
const vectors = await MemoryVectors . find ( )
const vector_result = vectors . map ( ( v ) => {
return v . getScore ( search_query )
} ) . flat ( ) . sort ( ( a , b ) => b . score - a . score ) . filter ( ( chunk ) => chunk . score > cosineSimThreshold ) . slice ( 0 , maxResults )
const query_result = vector_result . length > 0 ? vector_result . map ( ( v ) => v . text ) . join ( "n" ) . slice ( 0 , maxFilesLength ) : ''Eso es todo. Ordenado, ¿eh?
Para demostrar cómo funcionan todo esto, verifiquemos una conversación de muestra.
Usuario: Oye, dime, ¿cuál es tu hamburguesa favorita? ¿queso? ¿tocino? ¿champiñón?
Bot: ¡Oh, seguro! Me refiero a esa hamburguesa de tocino, ¿sabes? El crujiente tocino solo lo lleva a un nivel completamente nuevo. ¿Qué pasa contigo?
Usuario: ¡hongo! ¡Con queso! Oh, me encanta.
Bot: ¡Totalmente! El hongo y el queso son una combinación hecha en Burger Heaven. ¿Has intentado hacer tu propia hamburguesa de champiñones y queso en casa? ¡Vale la pena!
Usuario: todavía no. ¿Has probado el aguacate?
Bot: ¡Oh, totalmente! El aguacate en una hamburguesa es como la cereza en la parte superior, ¿sabes? Agrega esa bondad cremosa y mantecosa que lleva todo al siguiente nivel. ¿Lo has probado?
Usuario: Sí. En realidad, hay una historia divertida relacionada con ella. Así que he tenido hamburguesa de aguacate antes. Entonces, una vez, mis amigos y yo fuimos al viaje de esquí. En el camino nos detuvimos en una tienda de hamburguesas. Mientras navegaba por el menú, vi aguacate y lo señalé cuando ordené. ¿Sabes lo que pasó después?
Bot: ¡De ninguna manera! ¿Qué pasó? ¿Te dieron una hamburguesa con un aguacate entero? ¡Eso sería muy gracioso!
Usuario: no. Pero cerca. Me dieron una hamburguesa con una porción de aguacate. nada más. Resulta que el que elegí en el menú es para la opción vegana.
Bot: ¡Oh, Dios mío, eso es demasiado divertido! Así que terminaste con una hamburguesa de aguacate simple en lugar de la que esperabas. ¡Eso es como una confusión directamente de una película de comedia! ¿Terminaste disfrutando de la hamburguesa de aguacate?
Usuario: Lo comí bien pero no estaba satisfecho, por supuesto. Oye, ¿puedes guardar este recuerdo divertido? Será bueno recordar a veces
Llamada de funciones (invocado save_new_memory):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_2TIYJOo9WJcprD7ozQsERoo4' ,
type : 'function' ,
function : {
name : 'save_new_memory' ,
arguments : '{"memory_title":"Avocado Burger Mix-Up","memory_detail":"Ordered an avocado burger at a ski trip burger shop, but got a plain avocado burger instead. Turns out, the one I picked on the menu was the vegan option! Hilarious mix-up.","memory_date":"2023-11-10","memory_tags":["funny","food","burger"]}'
}
}
]
}En este punto, llamamos a la API de incrustaciones y obtenemos los datos vectoriales para los argumentos completos y los guardamos con el texto a MongoDB.
Respuesta de la función:
[ {
tool_call_id : 'call_2TIYJOo9WJcprD7ozQsERoo4' ,
role : 'tool' ,
name : 'save_new_memory' ,
content : '{n "message": "New memory saved"n}'
}
]BOT: ¡Guardado de memoria! Ahora tienes esa divertida historia de confusión para recordar cada vez que necesitas una buena risa. Es como una pequeña porción de la vida, ¿sabes?
Ahora, para recuperar información de memoria
Usuario: Por cierto, ¿conoce la hamburguesa que está cerrando este mes? ¿El de Shibuya? ¿Cuál era el nombre?
Llamada de función (invocado get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_3PNs56LMrmDViZUnrxdBScGz' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-30"}' }
}
]
}Respuesta de la función:
[ {
tool_call_id : 'call_3PNs56LMrmDViZUnrxdBScGz' ,
role : 'tool' ,
name : 'get_calendar_entry' ,
content : '{n "message": "No entries found"n}'
}
]BOT: No pude encontrar ninguna información sobre un cierre de hamburguesas en Shibuya este mes. Tal vez es solo un rumor, ¿sabes? Pero si descubres el nombre, ¡hágamelo saber!
Usuario: ¿Puede verificar su memoria? Creo que podría haberle dicho que antes
Llamada de función (invocado get_info_from_memory):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_WVBvjR0MKgaaTzik7QBwk3Ei' ,
type : 'function' ,
function : {
name : 'get_info_from_memory' ,
arguments : '{"search":["burger","Shibuya","closing"]}'
}
}
]
} Aquí obtenemos los incrustaciones para los términos de búsqueda en lugar de la consulta de usuario real. Luego, utilizando los datos vectoriales resultantes, llamamos a mongoDb.searchEntry para buscar los datos vectoriales almacenados en MongoDB.
Respuesta de la función:
[ {
tool_call_id : 'call_WVBvjR0MKgaaTzik7QBwk3Ei' ,
role : 'tool' ,
name : 'get_info_from_memory' ,
content : '{n' +
' "message": "Retrieved related info for burger,Shibuya,closing",n' +
` "result": "score: 0.8513867910905064\n title: Becker's ハンバーガーショップ閉店 detail: 千葉の人気なバーガーショップ「Becker's」が閉店するらしい。 tags: 閉店,ハンバーガー,千葉\nscore: 0.8506170963709474\n title: トリキバーガーの渋谷店閉店情報 detail: 渋谷のトリキバーガーの店舗が閉店することが決まりました。最後の日は今月の20日です。お店は渋谷の井の頭通りにあります。 date: 2023-11-10 tags: トリキバーガー,渋谷,閉店情報\nscore: 0.8307398914702265\n title: Becker's閉店情報 detail: 千葉の人気なバーガーショップBecker'sが閉店する。最後の日は今月の22日。 date: 2023-11-22 tags: 閉店情報,Becker's...n` +
'}'
}
]Solo estoy mostrando el resultado parcial del contenido, pero contiene varios éxitos de la base de datos relacionada con los términos de búsqueda. Enviamos todos los resultados a la API de finalización de chat y dejamos que la IA decida qué hacer.
Bot: ¡Oh, claro! La hamburguesa que está cerrando en Shibuya es la hamburguesa Triki. Su ubicación de Shibuya se está cerrando el 20 de este mes. Es triste verlos irse, ¿sabes?
Si notó, ¡el resultado parcial que se muestra está en japonés! Entonces, definitivamente, esto muestra que no estamos haciendo solo una búsqueda simple de palabras clave aquí. Puede agregar datos de lenguaje mixto y consultar la memoria utilizando un lenguaje diferente y es probable que aún obtenga la respuesta. Intentemos usar nuestro bot ruso, Vitaly . Usaré Google Translate ya que no hablo ruso.
Usuario: виталий, мешжш вспо,000iguritar (Vitaly, ¿puedes recordar el nombre de mis papas fritas favoritas?)
Función llamando (invocado get_info_from_memory):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_8fXETNlDMuKEXsitUuqt5PlE' ,
type : 'function' ,
function : {
name : 'get_info_from_memory' ,
arguments : '{"search":["картофельные чипсы"]}'
}
}
]
}Entonces, nuevamente, obtenemos los incrustaciones para el término de búsqueda y lo usamos con cada datos de vectores almacenados en MongoDB.
Resultado de la función:
[ {
tool_call_id : 'call_8fXETNlDMuKEXsitUuqt5PlE' ,
role : 'tool' ,
name : 'get_info_from_memory' ,
content : '{n' +
' "message": "Retrieved related info for картофельные чипсы",n' +
` "result": "score: 0.7718838658905126\n title: トムさんのお気に入りのパン detail: トムさんのお気に入りのパンはカレーパンとあんドーナツです。カレーパンはスパイシーなカレーとサクサクの生地が絶妙に合い、あんドーナツは甘さともちもち感が魅力です。\nscore: 0.7560670627592779\n title: Crisp Bacon Potato Chips detail: Brand: Dice tags: snack,potato chips,brand\nscore: 0.7354783965092787\n title: あまとうのあんみつ detail: 小樽のあまとうで食べるあんみつは絶品。...n` +
'}'
}
]Puede ver que el resultado de los datos que se muestra es mixto, con textos japoneses e ingleses.
Bot: твои любиеые картофельные чипсы - Patatas de tocino crujientes. (Tus papas fritas favoritas son las papas fritas de tocino crujientes).
Bien, pasemos al paso final.
Al recibir la respuesta del texto de los pasos anteriores, ahora llamaremos a la API de texto a voz para habilitar nuestro chatbot para vocalizar la respuesta
try {
const mp3 = await openai . audio . speech . create ( {
model : 'tts-1' ,
voice : 'alloy' ,
input : 'The quick brown fox jumps over the lazy dog.' ,
} )
const buffer = Buffer . from ( await mp3 . arrayBuffer ( ) )
await fs . promises . writeFile ( filename , buffer )
} catch ( error ) {
throw error
} Dado que el Doc no proporciona descripciones específicas para cada voz, le pedí a ChatGPT que hiciera algunas conjeturas educadas basadas en sus nombres:
Aleación (f): versátil y podría ser adecuado para contenido técnico o instructivo, como tutoriales, material educativo o cualquier contenido donde la claridad y la precisión sean esenciales.
Echo (M): podría ser apropiado para la narración de historias, audiolibros o contenido que requiere un tono más dramático o narrativo.
Fable (F): podría ser ideal para historias de niños, contenido de fantasía o cualquier narrativa que requiera un tono más juguetón o imaginativo.
Onyx (M): podría ser adecuado para leer literatura seria, entregar contenido de noticias o cualquier material que requiera una entrega más grave o formal.
Nova (f): Adecuado para anuncios, contenido motivacional o cualquier material que requiera una entrega positiva y entusiasta.
Shimmer (F): Adecuado para contenido de estilo de vida, podcasts o cualquier cosa que requiera un tono atractivo, amigable y acogedor.
Los datos de audio generados se guardarán en el directorio public/upload en formato de archivo mp3. Luego enviaremos el enlace al lado del cliente donde se cargará y lo jugará un dinámico htmlaudioelement.
const audioDomRef = new Audio ( )
audioDomRef . type = 'audio/mpeg'
audioDomRef . addEventListener ( 'loadedmetadata' , handleAudioLoad )
audioDomRef . addEventListener ( 'ended' , handleAudioEnded )
audioDomRef . addEventListener ( 'error' , handleAudioError )
audioDomRef . src = audio_url
function handleAudioLoad ( ) {
audioDomRef . value . play ( )
}Tenga en cuenta que la grabación de audio se deshabilitará mientras se reproduce la respuesta de audio. Esto asegura que la aplicación no registre la respuesta del bot.
FFMPEG se usa para eliminar piezas silenciosas en el archivo de audio.
Para instalar la herramienta de línea de comandos FFMPEG
# on Ubuntu or Debian
sudo apt update && sudo apt install ffmpeg
# on Arch Linux
sudo pacman -S ffmpeg
# on MacOS using Homebrew (https://brew.sh/)
brew install ffmpeg
# on Windows using Chocolatey (https://chocolatey.org/)
choco install ffmpeg
# on Windows using Scoop (https://scoop.sh/)
scoop install ffmpeg
MongoDB se utilizará para almacenar entradas de calendario y datos vectoriales para la función de memoria a largo plazo.
Para instalar MongoDB Community Edition, consulte esta página. También es posible que desee instalar MongoDB Shell para ver la base de datos.
Primero, asegúrese de que FFMPEG y MongoDB estén instalados en su sistema.
Para clonar el repositorio del proyecto e instalar las dependencias
$ git clone https://github.com/supershaneski/openai-whisper-talk.git myproject
$ cd myproject
$ npm install Copie el archivo .env.example y cambie el nombre a .env , luego abra y edite los elementos allí con valores reales. Para los elementos de MongoDB, probablemente no necesite editarlos a menos que tenga una configuración diferente.
NUXT_OPENAI_API_KEY=your-openai-api-key
NUXT_MONGODB_HOST_NAME=localhost
NUXT_MONGODB_PORT=27017
NUXT_MONGODB_DB_NAME=embeddingvectorsdb
Luego para ejecutar la aplicación
$ npm run dev Abra su navegador a http://localhost:5000/ (el número de puerto depende de la disponibilidad) para cargar la página de la aplicación.
Nota: Todavía no he probado esto con la última actualización
Es posible que desee ejecutar esta aplicación usando el protocolo https . Esto es necesario para habilitar la captura de audio utilizando un dispositivo separado como un teléfono inteligente.
Para hacerlo, prepare el certificate y los archivos key adecuados y edite server.mjs en el directorio root.
Entonces BUID el proyecto
$ npm run buildFinalmente, ejecuta la aplicación
$ node server.mjs Ahora, abra su navegador a https://localhost:3000/ (el número de puerto depende de la disponibilidad) o use su dirección IP local para cargar la página.
Puede encontrar estas variables en páginas/charla/[id] .Vue File. ↩ ↩ 2
Si es por fecha, el elemento debe ser solo uno bajo la fecha, de lo contrario, se devolverá un error. ↩