v0.0.2
Openai-Whisper-Talk est un exemple d'application de conversation vocale alimentée par des technologies Openai telles que Whisper, un système automatique de reconnaissance vocale (ASR), les compléments de chat, une interface qui simule la conversation avec un modèle qui joue le rôle d'assistant, d'incorporation, de convertit du texte en données vectorielles qui peuvent être utilisées dans des tasks tels que la recherche sémantique et le dernier texte à une personne, qui transforme le texte de la vie du texte ito-like. L'application est construite à l'aide de Nuxt, un framework JavaScript basé sur Vue.js.
L'application a deux nouvelles fonctionnalités: la gestion des horaires et la mémoire à long terme. Avec la gestion des calendriers, vous pouvez commander le chatbot pour ajouter, modifier, supprimer et récupérer les événements planifiés. La fonction de mémoire à long terme vous permet de stocker des extraits d'informations dont le chatbot se souviendra pour une référence future. Vous pouvez intégrer de manière transparente les deux fonctions dans vos conversations simplement en interagissant avec le chatbot.
Mise à jour: mis à jour le module OpenAI vers la dernière version et remplacé le modèle d'incorporation de
text-embedding-ada-002au nouveau modèle V3text-embedding-3-small.
Openai-Whisper-Talk は、 Whisper (自動音声認識 (ASR) システム) 、 、 CHAT ACHELIONS (アシスタントの役割を果たすモデルとの会話をシミュレートするインターフェース) 、 、 、 、 (セマンティック検索などのタスクで使用できるベクターデータにテキストを変換する)) 、そして最新の Text-to-disseops
このアプリケーションには、「スケジュール管理」と「永続メモリ」の 2 つの新機能があります。スケジュール管理を使用すると、チャットボットにスケジュールイベントの追加、変更、削除、取得を指示できます。永続メモリ機能を使用すると、将来の参照のためにチャットボットが覚えておく情報のスニペットを保存できます。これらの機能をチャットボットとの対話を通じてシームレスに統合することができます。将来的に、いくつかの機能強化、たとえばメールやメッセージング機能を追加することで、完全な個人アシスタントになるかもしれません。

À partir de la page principale, vous pouvez choisir avec quel chatbot pour s'engager. Chaque chatbot a une personnalité distincte, parle une langue différente et possède une voix unique. Vous pouvez modifier le nom et la personnalité de n'importe quel chatbot en cliquant sur le bouton Modifier adjacent à son nom. Actuellement, l'interface utilisateur ne prend pas directement en charge l'ajout de nouveaux chatbots; Cependant, vous pouvez ajouter manuellement un chatbot et personnaliser les paramètres vocaux et linguistiques pour chacun en modifiant le fichier actif / contacts.json.

De plus, vous pouvez personnaliser votre profil en cliquant sur l'icône Avatar dans le coin supérieur droit. Cela vous permet d'entrer votre nom et de partager des détails sur vous-même, permettant au chatbot d'interagir avec vous de manière plus personnalisée.


Les données audio sont automatiquement enregistrées si le son est détecté. Un paramètre de seuil est disponible pour empêcher le bruit de fond de déclencher la capture audio. Par défaut, cela est défini sur -45 dB (avec -10 dB représentant la coupure du son la plus bruyante). Vous pouvez ajuster ce seuil en modifiant la variable MIN_DECIBELS 1 en fonction de vos besoins.
Lorsque l'enregistrement est activé et qu'aucun son n'est détecté pendant 3 secondes , les données audio sont téléchargées et envoyées au backend pour la transcription. Il convient de noter que dans les conversations typiques, l'écart moyen entre chaque virage est d'environ 2 secondes. De même, la pause entre les phrases lors de la parole est à peu près la même. Par conséquent, j'ai choisi une durée assez longue pour signifier attendre une réponse. Vous pouvez ajuster cette durée en modifiant la variable MAX_COUNT 1 .
Le système peut enregistrer en continu les données audio jusqu'à ce qu'une réponse soit reçue. Une fois la réponse audio du chatbot reçu et lu, l'enregistrement audio est désactivé pour empêcher l'enregistrement par inadvertance de la propre réponse du chatbot.
Une entrée de texte est également fournie si vous souhaitez écrire vos messages.
Toutes les données audio enregistrées sont téléchargées dans le répertoire public/upload au format de fichier WebM. Avant de soumettre le fichier audio à l'API Whisper, il est nécessaire de supprimer tous les segments silencieux. Cette étape aide à prévenir le problème bien connu des hallucinations générées par Whisper. Pour cette même raison, il est recommandé de définir la valeur MIN_DECIBELS aussi élevée que possible, garantissant que seule la parole est enregistrée.
Pour éliminer les pièces silencieuses de l'audio, nous utiliserons ffmpeg . Assurez-vous de l'installer.
ffmpeg -i sourceFile -af silenceremove=stop_periods=-1:stop_duration=1:stop_threshold=-50dB outputFileDans cette commande:
-1 sourceFile Spécifie le fichier d'entrée.-af silenceremove applique le filtre silencerremove .stop_periods=-1 supprime toutes les périodes de silence.stop_duration=1 définit toute période de silence de plus de 1 seconde en tant que silence.stop_threshold=-50dB définit tout niveau de bruit inférieur à -50 dB en silence.outputFile le fichier de sortie. Pour invoquer cette commande shell dans notre itinéraire API, nous utiliserons exec à partir du module child-process .
Après avoir retiré les pièces silencieuses, la taille du fichier doit être vérifiée. La taille du fichier final est généralement beaucoup plus petite que l'original. Au cours de ce chèque, les fichiers inférieurs à 16 Ko sont ignorés, en supposant qu'un fichier de cette taille, avec une profondeur de 16 bits, équivaut à environ une seconde d'audio. Tout ce qui est plus court est probablement inaudible.
// simple file size formula
const audio_file_size = duration_in_seconds * bitrateL'ensemble du processus, de l'élimination des pièces silencieuses à la vérification de la taille du fichier, est conçu pour garantir que seules les données audio viables sont envoyées à l'API Whisper. Notre objectif est d'éviter les hallucinations et la transmission inutile des données.
Après avoir confirmé la viabilité de nos données audio, nous procédons ensuite à l'appel de l'API Whisper.
const transcription = await openai . audio . transcriptions . create ( {
file : fs . createReadStream ( filename ) ,
language : lang ,
response_format : 'text' ,
temperature : 0 ,
} ) où lang est le code ISO 639-1 de la langue spécifiée de notre chatbot. Veuillez vérifier la liste de la langue actuellement prise en charge.
Nous optons pour un format text simple car les horodatages ne sont pas nécessaires, et nous définissons le paramètre temperature à zéro pour atteindre une sortie déterministe.
Après avoir reçu la transcription de Whisper, nous procédons à la soumettre à l'API de complétion de chat avec l'appel de fonction. Nous utilisons le dernier module Openai Node.js (version 4) qui a été publié hier (2023/11/07), qui comprend un format d'appelant à la fonction mis à jour. Cette nouvelle itération permet l'invocation de plusieurs fonctions dans une seule demande.
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 }
]
} )Dissolons les différentes composantes de cet appel dans les sections suivantes.
L'invite du système joue un rôle crucial en donnant vie à notre chatbot. C'est ici où nous établissons son nom et son personnage, sur la base du chatbot choisi par l'utilisateur. Nous lui fournissons des instructions spécifiques sur la façon de répondre, ainsi qu'une liste de fonctions qu'il peut exécuter. Nous l'informons également sur l'identité de l'utilisateur et certaines détails personnels. Enfin, nous définissons la date et l'heure actuelles, ce qui est essentiel pour activer les fonctions de calendrier.
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` Tous les messages seront stockés dans MongoDB .
Pour gérer les jetons et éviter de dépasser la limite maximale du modèle, nous n'enverrons que les 20 dernières interactions. La fonction trim_array est conçue pour couper l'historique des messages si elle dépasse 20 tours. Ce seuil peut être ajusté pour répondre à vos besoins spécifiques.
// allow less than or equal to 15 turns
const history_context = trim_array ( all_history , 15 ) Depuis l'écran principal, vous avez la possibilité d'effacer l'historique précédent pour chaque chatbot.

Veuillez noter que nous avons séparé la gestion des appels de fonction (fonction_call.js) de l'appel de l'API de complétion de chat principal (transccrire.php). Cette distinction est faite pour aborder les instances lorsque le contenu texte est présent lorsqu'un appel de fonction est invoqué, ce qui est généralement nul. Cette séparation permet à l'application d'afficher le texte tout en traitant simultanément l'appel de fonction. J'ai également joint le processus dans une boucle dans le cas où un deuxième appel API entraîne un autre appel de fonction. Cela pourrait probablement être géré plus élégamment en mettant en œuvre le streaming , mais je n'ai pas encore appris à utiliser le streaming avec Nuxt.
Nos fonctions sont classées dans deux nouvelles fonctionnalités: gestion des programmes et mémoire à long terme .
Examinons d'abord comment nous gérons les appels de fonction avec le nouveau format. Nous devons utiliser le paramètre des nouveaux outils au lieu du paramètre de fonctions désormais déprécié pour activer l'invocation de plusieurs appels de fonction.
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 }
]
} ) Vérifiez le schéma JSON de chaque fonctions de lib/ Directory. Voici pour 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" ]
}
}Pour Dicuss comment tout cela fonctionne, passons à la section suivante.
Pour la gestion des horaires , nous avons les fonctions suivantes:
Toutes les entrées de calendrier seront stockées à MongoDB. Veuillez noter que toutes les entrées seront accessibles à tous les chatbots.
Jetons un coup d'œil à un exemple de conversation de chat pour montrer comment ces éléments interagissent:
Utilisateur: Bonjour, Jeeves. Quel est mon emploi du temps pour aujourd'hui?
Fonction Calling (Invoquez get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [ {
id : 'call_4cCtmNlgYN5o4foVOQM9MIdA' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-10"}' }
} ]
}Réponse de la fonction:
[
{
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' +
'}'
}
]Résumé du 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.'
}Utilisateur: Il semble redondant d'avoir une entrée séparée pour le même événement. Pouvez-vous modifier l'événement avec Anna pour inclure la rencontre avec Tony et supprimer l'autre événement?
Fonction appelle (invoqué edit_calendar_entry et 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"}'
}
}
]
}Réponse de la fonction:
[ {
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}'
}
]Résumé du 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.'
}Utilisateur: D'accord, juste pour être sûr, pouvez-vous me dire à nouveau mon horaire pour aujourd'hui?
Fonction Calling (Invoquez get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_liqFn24Ds4qp0cy2V5NBg62v' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-10"}' }
}
]
}Réponse de la fonction:
[ {
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' +
'}'
}
]Résumé du 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.'
}Utilisateur: Ah, vous avez oublié d'ajouter l'emplacement du dîner. Désolé mais pouvez-vous le mettre à jour à nouveau?
Fonction appelle (invoqué 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."}'
}
}
]
}Réponse de la fonction:
{
tool_call_id : 'call_eRTFMcz2l8qHO7nhp3msI3v8' ,
role : 'tool' ,
name : 'edit_calendar_entry' ,
content : '{n "message": "Entry edited",n "name": "Dinner with Anna"n}'
}Résumé du chat:
{ role : 'assistant' ,
content : 'The event "Dinner with Anna" has been updated to include the meeting with Tony at the Electric Sheep Bar.'
}Utilisateur: D'accord, je veux le confirmer à nouveau, pouvez-vous me dire mon horaire pour aujourd'hui?
Fonction Call (invoqué get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_HYlAj4BTeWcu2CxSp4zBYPuC' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-10"}' }
}
]
}Réponse de la fonction:
[ {
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' +
'}'
}
]Résumé du 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.'
}Pour la mémoire à long terme , nous avons les fonctions suivantes
Toutes les fonctions précédentes se sont principalement concentrées sur les tâches de récupération des données typiques et de définition. Mais pour la récupération de la mémoire à long terme, nous ne pouvons pas simplement faire la recherche de mots clés. Nous devons considérer le contexte de la requête. Nous devons faire une recherche sémantique. Et à cet égard, nous utiliserons l' API Embeddings . Passons à la section suivante pour en discuter davantage.
MISE À JOUR : J'ai remplacé
text-embedding-ada-002avectext-embedding-3-small. Sur la base de mes tests, ce dernier fonctionne assez bien. Les scores de similitude des cosinus entre les réponses étroitement liés et les réponses non pertinentes sont nettement distinctes. De plus, la performance du coût du modèle V3, au prix de 0,00002 $ / 1k, est une évidence ?. Cependant, vous devez convertir vos données vectorielles d'ADA en V3 Small car elles ne sont pas compatibles. Notez que nous avons également abaissé le seuil de 0,72 à 0,3.
Pour le dire simplement, les intégres mesurent la parenté des chaînes de texte. Si nous appelons l'API, il nous donnera des données vectorielles de numéros flottants associés au texte d'entrée.
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" ,
} )Pour l'utiliser dans notre application, nous implémenterons ce qu'ils appellent une génération auprès de la récupération ou un chiffon simple.
Initialement, lorsque de nouvelles données sont reçues de la fonction SAVE_NEW_MEMORY, nous appelons l'API Embeddings pour générer sa représentation vectorielle. Ces données vectorielles sont ensuite stockées dans MongoDB pour une utilisation future.
Par la suite, lorsqu'un utilisateur soumet une requête qui nécessite une récupération de mémoire, la fonction get_info_from_memory est déclenchée. Nous appelons l'API Embeddings pour les paramètres de recherche et les compare aux données vectorielles stockées en utilisant une simple similitude de cosinus. Cette comparaison donne généralement plusieurs matchs avec des scores variables. Nous avons fixé notre seuil sur un score de 0,3 (0,72 pour le modèle ADA) ou plus, et nous limitons les résultats à un maximum de 10.
Les résultats sont ensuite transmis à l'API Final Chat Completion, qui détermine la réponse la plus appropriée à la requête de l'utilisateur. L'IA a la capacité de sélectionner un ou plusieurs résultats comme base de sa réponse, en fonction de la nature de la requête. C'est là que le véritable pouvoir de l'IA est démontré. Plutôt que de simplement régurgiter toutes les informations qu'il reçoit, l'IA analyse les données et formule une réponse appropriée. Si le résultat du RAG est jugé suffisant, il générera une réponse de texte positive.
Pour faciliter le stockage des données vectorielles et d'autres opérations vectorielles dans MongoDB , j'utilise le module Mongoose. Il s'agit de loin de la solution la plus indolore pour gérer le stockage et la récupération des données vectorielles.
Tout d'abord, nous définissons notre schéma:
const memoryVectorsSchema = new mongoose . Schema ( {
chunks : [ {
embedding : [ Number ] ,
text : String
} ]
} ) Ensuite, nous attachons la fonction getScore qui exécute la similitude des cosinus entre le modèle de document et la chaîne de requête intégrée:
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 }
} )
}Ensuite, nous compilons notre schéma dans un modèle:
const MemoryVectors = mongoose . model ( 'MemoryVectors' , memoryVectorsSchema )Pour enregistrer des données vectorielles:
const newVector = new MemoryVectors ( {
chunks
} )
newVector . save ( )Pour rechercher les données vectorielles stockées avec les intérêts de la requête utilisateur
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 ) : ''C'est ça. Soigné, hein?
Pour démontrer comment tout cela fonctionne, vérifions un exemple de conversation.
Utilisateur: Hé, dis-moi, quel est ton hamburger préféré? fromage? lard? champignon?
BOT: Oh, c'est sûr! Je suis tout au sujet de ce burger Bacon, tu sais? Le bacon croustillant l'emmène à un tout nouveau niveau. Et toi?
Utilisateur: champignon! avec du fromage! Oh, j'adore ça.
BOT: Totalement! Les champignons et le fromage sont un match fait au paradis du burger. Avez-vous essayé de faire votre propre hamburger aux champignons et au fromage à la maison? Ça vaut tellement la peine!
Utilisateur: pas encore. Avez-vous essayé l'avocat?
Bot: Oh, totalement! L'avocat sur un hamburger est comme la cerise sur le dessus, vous savez? Cela ajoute cette bonté crémeuse et beurrée qui fait passer le tout au niveau supérieur. L'avez-vous essayé?
Utilisateur: Oui. Il y a en fait une histoire drôle qui y est liée. J'ai donc déjà eu un hamburger d'avocat. Puis une fois, mes amis et moi sommes allés faire du ski. En cours de route, nous nous sommes arrêtés dans une boutique de hamburgers. Pendant que je parcourais le menu, j'ai vu Avocado et je l'ai pointé lorsque j'ai commandé. Savez-vous ce qui s'est passé ensuite?
BOT: Pas question! Ce qui s'est passé? Vous ont-ils donné un hamburger avec, comme, tout un avocat dessus? Ce serait hilarant!
Utilisateur: non. mais près. Ils m'ont donné un hamburger avec une tranche d'avocat. rien d'autre. Il s'avère que celui que j'ai choisi dans le menu est pour l'option végétalienne!
Bot: Oh mon Dieu, c'est trop drôle! Vous vous êtes donc retrouvé avec un hamburger d'avocat ordinaire au lieu de celui que vous attendiez. C'est comme un mélange tout droit sorti d'un film de comédie! Avez-vous fini par profiter du hamburger à l'avocat?
Utilisateur: Je l'ai bien mangé mais je n'étais pas satisfait, bien sûr. Hé, pouvez-vous enregistrer ce drôle de mémoire, ce sera bon de se souvenir parfois
Call de fonction (invoqué 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"]}'
}
}
]
}À ce stade, nous appelons l'API Embeddings et obtenons les données vectorielles pour l'ensemble des arguments et l'enregistrez avec le texte sur MongoDB.
Réponse de la fonction:
[ {
tool_call_id : 'call_2TIYJOo9WJcprD7ozQsERoo4' ,
role : 'tool' ,
name : 'save_new_memory' ,
content : '{n "message": "New memory saved"n}'
}
]Bot: Mémoire enregistrée! Maintenant, vous avez cette histoire de mélange amusante à regarder chaque fois que vous avez besoin d'un bon rire. C'est comme une petite tranche de vie, tu sais?
Maintenant, pour récupérer les informations de la mémoire
Utilisateur: Soit dit en passant, connaissez-vous le hamburger qui se ferme ce mois-ci? Celui à Shibuya? Quel était le nom?
Call de fonction (invoqué get_calendar_entry):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_3PNs56LMrmDViZUnrxdBScGz' ,
type : 'function' ,
function : { name : 'get_calendar_entry' , arguments : '{"date":"2023-11-30"}' }
}
]
}Réponse de la fonction:
[ {
tool_call_id : 'call_3PNs56LMrmDViZUnrxdBScGz' ,
role : 'tool' ,
name : 'get_calendar_entry' ,
content : '{n "message": "No entries found"n}'
}
]BOT: Je n'ai trouvé aucune information sur une fermeture de hamburger à Shibuya ce mois-ci. Peut-être que c'est juste une rumeur, tu sais? Mais si vous découvrez le nom, faites-le moi savoir!
Utilisateur: pouvez-vous vérifier votre mémoire, je pense que je vous aurais peut-être dit qu'avant
Call de fonction (invoqué 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"]}'
}
}
]
} Ici, nous obtenons les intérêts pour les termes de recherche au lieu de la requête utilisateur réelle. Ensuite, en utilisant les données vectorielles résultantes, nous appelons mongoDb.searchEntry pour rechercher les données vectorielles stockées dans MongoDB.
Réponse de la fonction:
[ {
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` +
'}'
}
]Je ne montre que le résultat partiel du contenu, mais il contient plusieurs coups sûrs de la base de données liés aux termes de recherche. Nous envoyons tous les résultats à l'API de complétion de chat et laissons l'IA décider quoi faire.
Bot: Oh, c'est vrai! Le hamburger qui se ferme à Shibuya est le Triki Burger. Leur emplacement Shibuya s'arrête le 20 de ce mois. C'est triste de les voir partir, tu sais?
Si vous l'avez remarqué, le résultat partiel indiqué est en japonais! Donc, certainement, cela montre que nous ne faisons pas seulement une recherche de mots clés simples ici. Vous pouvez ajouter des données de langage mixte et interroger la mémoire en utilisant une langue et des chances différentes si vous obtiendrez toujours la réponse. Essayons d'utiliser notre bot russe, Vitaly . J'utiliserai Google Translate car je ne parle pas russe.
Utilisateur: Виталий, можеш Â Всоousse (Vitaly, pouvez-vous vous souvenir du nom de mes croustilles préférées?)
Fonction Calling (invoqué get_info_from_memory):
{ role : 'assistant' ,
content : null ,
tool_calls : [
{
id : 'call_8fXETNlDMuKEXsitUuqt5PlE' ,
type : 'function' ,
function : {
name : 'get_info_from_memory' ,
arguments : '{"search":["картофельные чипсы"]}'
}
}
]
}Donc, encore une fois, nous obtenons les intérêts pour le terme de recherche et les utilisons sur chaque données vectorielles stockées dans MongoDB.
Résultat de la fonction:
[ {
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` +
'}'
}
]Vous pouvez voir que le résultat des données présenté est mitigé, avec des textes japonais et anglais.
BOT: твои ююбимые картофельные чиbalы - croustilles de pommes de terre de bacon croustillantes. (Vos croustilles préférées sont des croustilles de bacon croustillantes.)
D'accord, passons à la dernière étape.
En recevant la réponse textuelle des étapes précédentes, nous appellerons maintenant l'API de texte vocal pour permettre à notre chatbot de vocaliser la réponse
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
} Étant donné que le DOC ne fournit pas de descriptions spécifiques pour chaque voix, j'ai donc demandé à Chatgpt de faire des suppositions éduquées en fonction de leurs noms:
ALLIAG (F): polyvalent et peut convenir au contenu technique ou pédagogique, tels que des tutoriels, du matériel éducatif ou tout contenu où la clarté et la précision sont essentielles.
Echo (M): pourrait être approprié pour la narration, les livres audio ou le contenu qui nécessite un ton plus dramatique ou narratif.
Fable (F): pourrait être idéal pour les histoires des enfants, le contenu fantastique ou tout récit qui nécessite un ton plus ludique ou imaginatif.
ONYX (M): Peut être adapté à la lecture de la littérature sérieuse, à la livraison du contenu d'actualités ou à tout matériel qui nécessite une livraison plus sérieuse ou formelle.
Nova (F): Convient aux publicités, au contenu de motivation ou à tout matériel qui nécessite une livraison positive et enthousiaste.
Shimmer (F): Convient pour le contenu de style de vie, les podcasts ou tout ce qui nécessite un ton engageant, amical et accueillant.
Les données audio générées seront ensuite enregistrées dans le répertoire public/upload au format de fichier MP3. Nous enverrons ensuite le lien vers le côté client où un HTMLAudioElement dynamique se chargera et le jouera.
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 ( )
}Veuillez noter que l'enregistrement de l'audio sera désactivé pendant que la réponse audio est en lecture. Cela garantit que l'application n'enregistre pas la réponse du bot.
FFMPEG est utilisé pour supprimer les pièces silencieuses dans le fichier audio.
Pour installer l'outil de ligne de commande 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 sera utilisé pour stocker les entrées de calendrier et les données vectorielles pour la fonction de mémoire à long terme.
Pour installer MongoDB Community Edition, veuillez consulter cette page. Vous pouvez également installer le shell MongoDB pour afficher la base de données.
Tout d'abord, assurez-vous que FFMPEG et MONGODB sont installés dans votre système.
Pour cloner le référentiel du projet et installer les dépendances
$ git clone https://github.com/supershaneski/openai-whisper-talk.git myproject
$ cd myproject
$ npm install Copiez le fichier .env.example et renommez-vous vers .env , puis ouvrez-le et modifiez les éléments avec des valeurs réelles. Pour les éléments MongoDB, vous n'avez probablement pas besoin de les modifier à moins que vous ayez une configuration différente.
NUXT_OPENAI_API_KEY=your-openai-api-key
NUXT_MONGODB_HOST_NAME=localhost
NUXT_MONGODB_PORT=27017
NUXT_MONGODB_DB_NAME=embeddingvectorsdb
Puis pour exécuter l'application
$ npm run dev Ouvrez votre navigateur à http://localhost:5000/ (le numéro de port dépend de la disponibilité) pour charger la page de demande.
Remarque: je n'ai pas encore testé cela avec la dernière mise à jour
Vous voudrez peut-être exécuter cette application à l'aide du protocole https . Cela est nécessaire pour permettre la capture audio à l'aide d'un appareil séparé comme un smartphone.
Pour ce faire, préparez le certificate et les fichiers key appropriés et modifiez server.mjs au répertoire racine.
Puis Boud le projet
$ npm run buildEnfin, exécutez l'application
$ node server.mjs Maintenant, ouvrez votre navigateur à https://localhost:3000/ (le numéro de port dépend de la disponibilité) ou utilisez votre adresse IP locale pour charger la page.
Vous pouvez trouver ces variables en pages / talk / [id] .vue fichier. ↩ ↩ 2
Si par date, l'article ne doit être qu'un en vertu de la date, sinon une erreur sera renvoyée. ↩