diff --git a/.github/workflows/DiscordVirtualAssistant.yml b/.github/workflows/DiscordVirtualAssistant.yml index 59eabcd..4aa2df2 100644 --- a/.github/workflows/DiscordVirtualAssistant.yml +++ b/.github/workflows/DiscordVirtualAssistant.yml @@ -33,3 +33,4 @@ jobs: env: PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} CLIENT_EMAIL: ${{ secrets.CLIENT_EMAIL }} + DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} diff --git a/.gitignore b/.gitignore index 50e6dc0..feaeedc 100644 --- a/.gitignore +++ b/.gitignore @@ -137,4 +137,6 @@ node_modules/ response.mp3 config.json recordings/ -GoogleServiceAccount.json \ No newline at end of file +GoogleServiceAccount.json +token.txt +scripts/figures/*.png diff --git a/README.md b/README.md index d4164ca..1507a16 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,171 @@ # Discord-Virtual-Assistant + +![javascript](https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black) +![Node.js](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white) +![Mariadb](https://img.shields.io/badge/MariaDB-4EA94B?style=for-the-badge&logo=mariadb&logoColor=white) +![Markdown](https://img.shields.io/badge/Markdown-000000?style=for-the-badge&logo=markdown&logoColor=white) + +![Maintained](https://img.shields.io/badge/Maintained%3F-yes-green.svg) +![Licence](https://img.shields.io/github/license/plop91/Discord-Virtual-Assistant.svg) +![Website](https://img.shields.io/website-up-down-green-red/https/plop91.github.io/Discord-Virtual-Assistant/) +![Downloads](https://img.shields.io/github/downloads/plop91/Discord-Virtual-Assistant/total.svg) + ## Authors -* Ian Sodersjerna - @plop91 -* Jacob Austin - @jacob-austin -* Jonathan Tucker - @JTucker2000 -* Nick Miceli - @nickmiceli4 + +| Author | Link | +|---------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Ian Sodersjerna](https://github.com/plop91) | [![Follow](https://img.shields.io/github/followers/plop91.svg?style=social&label=Follow&maxAge=2592000)](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fplop91) | +| [Jacob Austin](https://github.com/jacob-austin) | [![Follow](https://img.shields.io/github/followers/jacob-austin.svg?style=social&label=Follow&maxAge=2592000)](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2FJTucker2000) | +| [Jonathan Tucker](https://github.com/JTucker2000) | [![Follow](https://img.shields.io/github/followers/JTucker2000.svg?style=social&label=Follow&maxAge=2592000)](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fjacob-austin) | +| [Nick Miceli](https://github.com/nickmiceli4) | [![Follow](https://img.shields.io/github/followers/nickmiceli4.svg?style=social&label=Follow&maxAge=2592000)](https://github.com/login?return_to=https%3A%2F%2Fgithub.com%2Fnickmiceli4) | + ## Instructions ### Installation: -Discord Virtual Assistant requires node.js in order to run, if you do not have node.js installed follow [these instructions](https://nodejs.org/en/download/) first then complete these steps -1. Download code to "DISCORD_VIRTUAL_ASSISTANT_DIR" (replace "DISCORD_VIRTUAL_ASSISTANT_DIR" with desired folder name) -```shell +Discord Virtual Assistant requires several external components to install and run including a node.js, ffmpeg and a maria database in order to run. + +* if you do not have node.js it can be installed from [these instructions](https://nodejs.org/en/download/) first then complete these steps +* if you do not have ffmpeg it can be installed from [their website](https://ffmpeg.org/download.html) +* if you do not have mariadb installed follow [these instructions](https://mariadb.com/get-started-with-mariadb/) or contact databases@sodersjerna.com for access to existing database. +* instructions for creating a discord bot token can be found [here](https://www.writebots.com/discord-bot-token/) +* instructions for getting a Google Cloud service account authentication JSON can be found [here](https://cloud.google.com/docs/authentication/production) under "Creating a Service Account" + + +1. Download code to "DISCORD\_VIRTUAL\_ASSISTANT\_DIR" (replace "DISCORD\_VIRTUAL\_ASSISTANT\_DIR" with desired folder name) + +``` git clone https://github.com/plop91/Discord-Virtual-Assistant.git DISCORD_VIRTUAL_ASSISTANT_DIR ``` + 2. Change to downloaded directory -```shell + +``` cd DISCORD_VIRTUAL_ASSISTANT_DIR ``` + 3. Install dependencies -```shell + +``` +npm update -g npm npm install ``` -4. Create a Discord bot token and set it as an environmental variable, instructions for creating a discord bot token can be found [here](https://www.writebots.com/discord-bot-token/) -```shell + +4. Set your Discord bot token as an environmental variable. + +``` export DISCORD_TOKEN=YOUR_DISCORD_TOKEN # commands may vary per operating system ``` -5. Download and install MariaDB using [these instructions](https://mariadb.com/get-started-with-mariadb/) or contact databases@sodersjerna.com for access to existing database. -```shell + +5. Set your maria db credentials as an environmental variable. + +``` export DVA_DATABASE_HOST=hostname/ip export DVA_DATABASE_USER=username export DVA_DATABASE_PASSWORD=password ``` +6. Set your Google Cloud service account credentials as an environmental variable. Open your service account JSON file and copy the fields named 'client_email' and 'private_key'. Use single quotes around the private key to account for spaces. + +``` +export CLIENT_EMAIL=ClientEmail +export PRIVATE_KEY='PrivateKey' +``` ### Usage: + 1. Change to downloaded directory -```shell + +``` cd DISCORD_VIRTUAL_ASSISTANT_DIR ``` + 2. Start the node application -```shell + +``` npm start ``` +3. Join a voice channel in a server that the bot is a member of and type 'join' in a text channel to have the bot join your audio channel (instructions to invite a bot can be found [here](https://discordpy.readthedocs.io/en/stable/discord.html)) +4. type 'record' to make the bot begin recording audio. +5. after you have finished speaking the bot will respond in the audio channel. + ### Testing: + 1. Change to downloaded directory -```shell + +``` cd DISCORD_VIRTUAL_ASSISTANT_DIR ``` + 2. Start the tests for the node application -```shell + +``` npm test ``` ## Objectives -This project seeks to solve that Discord currently lacks any built-in virtual assistant to help users with their daily interactions with the app; therefore, the objective for Discord Virtual Assistant is to create a Discord bot that users can interact with similarly to how people interact with Google Home or Amazon Alexa. Users of our Discord Virtual Assistant will communicate with it through a voice channel within a Discord Server. The bot will perform a task based on what is asked of it and reply back through the voice channel with either an answer to a question or acknowledgement of a command. -In regards to the features our Discord Virtual Assistant will support, it will support typical features that other virtual assistants can such as stating the time, weather, and starting a timer. On top of these features, we will also be supporting more Discord-specific features such as messaging other members in a server and recording audio in a voice channel. +This project seeks to solve that Discord currently lacks any built-in virtual assistant to help users with their daily interactions with the app; therefore, the objective for Discord Virtual Assistant is to create a Discord bot that users can interact with similarly to how people interact with Google Home or Amazon Alexa. Users of our Discord Virtual Assistant will communicate with it through a voice channel within a Discord Server. The bot will perform a task based on what is asked of it and reply back through the voice channel with either an answer to a question or acknowledgement of a command. In regards to the features our Discord Virtual Assistant will support, it will support typical features that other virtual assistants can such as stating the time, weather, and starting a timer. On top of these features, we will also be supporting more Discord-specific features such as messaging other members in a server and recording audio in a voice channel. ## Current Practice -Currently, the main alternative to Discord Virtual Assistant would be the typical virtual assistants on the market such as Google assistant, Amazon Alexa, or Siri. While these are all viable options, the features they offer do not interact with Discord. With Discord being a common tool used for both gaming and school, a virtual assistant that can help users streamline the way in which they interact with it would be a welcomed addition to the market. -In the realm of Discord bots, there are some virtual assistants out there such as Google Assistant Discord bot or SUSI.AI. However, they are limited in their abilities. Mainly, users interact with these bots using messages in a channel rather than being able to use voice commands to control the bot like Discord Virtual Assistant aims to do. Also, the features that they support are akin to traditional virtual assistants (Google Assistant, Alexa, etc.) in that they do not help users interact with Discord. They are virtual assistants within Discord, which our Assistant will build off of by being a virtual assistant for Discord. +Currently, the main alternative to Discord Virtual Assistant would be the typical virtual assistants on the market such as Google assistant, Amazon Alexa, or Siri. While these are all viable options, the features they offer do not interact with Discord. With Discord being a common tool used for both gaming and school, a virtual assistant that can help users streamline the way in which they interact with it would be a welcomed addition to the market. In the realm of Discord bots, there are some virtual assistants out there such as Google Assistant Discord bot or SUSI.AI. However, they are limited in their abilities. Mainly, users interact with these bots using messages in a channel rather than being able to use voice commands to control the bot like Discord Virtual Assistant aims to do. Also, the features that they support are akin to traditional virtual assistants (Google Assistant, Alexa, etc.) in that they do not help users interact with Discord. They are virtual assistants within Discord, which our Assistant will build off of by being a virtual assistant for Discord. ## Our approach -The idea of a virtual assistant is nothing new, however, we are applying it to a new platform which has never seen any kind of virtual assistant support in the past. The approach relies on layering several API’s in order to get the desired virtual assistant. Firstly, and most importantly, the discord.js API allows us to easily interact with the discord platform. The discord.js API gives us the ability to send and receive messages and audio while simultaneously granting full administration over the Discord server with functions such as banning users, inviting users, or creating webhooks. Once we have our interface with Discord setup, the next step will be to receive and process the audio clips returned from Discord. This should be straightforward as discord.js returns audio clips separated by “silence” (or when no one is speaking in the server) which works perfectly for this application. Once we have our audio clip we will upload it to the Google Speech-to-Text API which will return a transcript of the audio clip. -Once we have the transcript of the audio we must parse the text looking for commands and their arguments. The parser will need to understand all potential commands and may in some cases need to wait for multiple audio files to be processed before it can perform an action. From here the bot begins performing an action which will usually begin with an audio response to the user. The audio response will be generated by the Google Text-to-Speech API using the text provided by the action. Once a response is rendered the virtual assistant will perform any additional functions contained in the action, such as sending a message, or banning a user. +The idea of a virtual assistant is nothing new, however, we are applying it to a new platform which has never seen any kind of virtual assistant support in the past. The approach relies on layering several API’s in order to get the desired virtual assistant. Firstly, and most importantly, the discord.js API allows us to easily interact with the discord platform. The discord.js API gives us the ability to send and receive messages and audio while simultaneously granting full administration over the Discord server with functions such as banning users, inviting users, or creating webhooks. Once we have our interface with Discord setup, the next step will be to receive and process the audio clips returned from Discord. This should be straightforward as discord.js returns audio clips separated by “silence” (or when no one is speaking in the server) which works perfectly for this application. Once we have our audio clip we will upload it to the Google Speech-to-Text API which will return a transcript of the audio clip. Once we have the transcript of the audio we must parse the text looking for commands and their arguments. The parser will need to understand all potential commands and may in some cases need to wait for multiple audio files to be processed before it can perform an action. From here the bot begins performing an action which will usually begin with an audio response to the user. The audio response will be generated by the Google Text-to-Speech API using the text provided by the action. Once a response is rendered the virtual assistant will perform any additional functions contained in the action, such as sending a message, or banning a user. # Interface -Our software will not have its own graphical user interface. Rather, users will interact with Discord’s GUI to use the bot. In order to interface with our bot specifically, users will join a Discord voice channel where the bot is present and they will speak commands in the channel directly to the bot. -The way this works relies on the discord.js API, which provides functionality to record the audio in the voice channel as an audio file. This audio file can then be transcribed to text via any number of speech-to-text APIs, the current plan is to use Google Speech-to-Text. Once the audio is transcribed to text, the bot will have a parser which will scan through the text to look out for keywords and phrases that correspond to the commands the bot supports; then, the bot will act on the command it receives. If the command is to do something within Discord itself (send a message, ban a user, etc.), the bot will use discord.js to execute the requested task, but if the command is for something not Discord related (ask for the weather, time, etc) then the bot will execute it using other tools such as a weather API for weather data or JavaScript’s Date class for the time. -Once the command has been executed the bot will put together a string to respond to the user. It will either contain the information the user requested or simply an acknowledgement that the command has executed if no information is requested. Once the string is constructed, Google Text-to-Speech will be used to get an audio file of the response being spoken and discord.js will be used to have the audio file played in the voice channel back to the user. + +Our software will not have its own graphical user interface. Rather, users will interact with Discord’s GUI to use the bot. In order to interface with our bot specifically, users will join a Discord voice channel where the bot is present and they will speak commands in the channel directly to the bot. The way this works relies on the discord.js API, which provides functionality to record the audio in the voice channel as an audio file. This audio file can then be transcribed to text via any number of speech-to-text APIs, the current plan is to use Google Speech-to-Text. Once the audio is transcribed to text, the bot will have a parser which will scan through the text to look out for keywords and phrases that correspond to the commands the bot supports; then, the bot will act on the command it receives. If the command is to do something within Discord itself (send a message, ban a user, etc.), the bot will use discord.js to execute the requested task, but if the command is for something not Discord related (ask for the weather, time, etc) then the bot will execute it using other tools such as a weather API for weather data or JavaScript’s Date class for the time. Once the command has been executed the bot will put together a string to respond to the user. It will either contain the information the user requested or simply an acknowledgement that the command has executed if no information is requested. Once the string is constructed, Google Text-to-Speech will be used to get an audio file of the response being spoken and discord.js will be used to have the audio file played in the voice channel back to the user. # Who cares? -Users of Discord would find our virtual assistant useful, since no alternatives exist that can directly interact with Discord. It will be able to have much more functionality within Discord than other virtual assistants. It could do things like message other Discord users, play music within a voice channel, or record the audio within a voice channel. The virtual assistant will take the form of a Discord bot, so it will also be able to take in voice commands from every user in the voice channel. -Discord server administrators may also find the virtual assistant useful. You could map administrator commands to specific voice commands to save time. For example, you might say “Hey Discord bot, add a voice channel named general” or “Hey Discord bot, ban user1234”. The usefulness of the virtual assistant would depend on the administrator and what they do, but it certainly has potential as a time saving tool. +Users of Discord would find our virtual assistant useful, since no alternatives exist that can directly interact with Discord. It will be able to have much more functionality within Discord than other virtual assistants. It could do things like message other Discord users, play music within a voice channel, or record the audio within a voice channel. The virtual assistant will take the form of a Discord bot, so it will also be able to take in voice commands from every user in the voice channel. Discord server administrators may also find the virtual assistant useful. You could map administrator commands to specific voice commands to save time. For example, you might say “Hey Discord bot, add a voice channel named general” or “Hey Discord bot, ban user1234”. The usefulness of the virtual assistant would depend on the administrator and what they do, but it certainly has potential as a time saving tool. ## Potential success -For the project to be considered a success, it must be able to process voice commands and provide some basic functionality through those commands. If the project ends up being successful in this way, it not only means we have built a decent bot; it also means we have created a great platform to build off of in the future. There are many features not included in our objective that could be added to a virtual assistant like this one. -For example, we could add interaction between different apps and hardware. You may be able to say something like “Hey Discord bot, turn all of the lights in the bedroom green” or “Hey Discord bot, launch a random game on Steam”. If our project managed to be this successful, the Discord virtual assistant would be one of the best Discord bots available. It might even be a viable alternative to other virtual assistants like Siri or Google Assistant. + +For the project to be considered a success, it must be able to process voice commands and provide some basic functionality through those commands. If the project ends up being successful in this way, it not only means we have built a decent bot; it also means we have created a great platform to build off of in the future. There are many features not included in our objective that could be added to a virtual assistant like this one. For example, we could add interaction between different apps and hardware. You may be able to say something like “Hey Discord bot, turn all of the lights in the bedroom green” or “Hey Discord bot, launch a random game on Steam”. If our project managed to be this successful, the Discord virtual assistant would be one of the best Discord bots available. It might even be a viable alternative to other virtual assistants like Siri or Google Assistant. ## Risks and payoffs -The main risk of this project has to do with its scope. There are endless possibilities for possible features to add, so it is important the team gets the core system functioning well before getting caught up in adding features, or else the risk could be a piece of software that has many things it should do but few things it actually does do. Sticking to the schedule and list of core features is essential to prevent feature creep. -Another risk is the pricing of speech-to-text APIs. While the estimated cost is not extreme and it is unlikely that significantly more API calls are needed than what is estimated, all use of APIs must be documented and shared with the team to prevent excessive costs from being incurred. -One more risk is that we are using several different APIs for this project, so there is always a risk that different APIs do not work well together and need extra development time to be properly modular. At the start of the project it would likely be wise to try several different speech-to-text APIs to see how they function with the necessary Discord API to prevent deadlock later in the project. +The main risk of this project has to do with its scope. There are endless possibilities for possible features to add, so it is important the team gets the core system functioning well before getting caught up in adding features, or else the risk could be a piece of software that has many things it should do but few things it actually does do. Sticking to the schedule and list of core features is essential to prevent feature creep. Another risk is the pricing of speech-to-text APIs. While the estimated cost is not extreme and it is unlikely that significantly more API calls are needed than what is estimated, all use of APIs must be documented and shared with the team to prevent excessive costs from being incurred. One more risk is that we are using several different APIs for this project, so there is always a risk that different APIs do not work well together and need extra development time to be properly modular. At the start of the project it would likely be wise to try several different speech-to-text APIs to see how they function with the necessary Discord API to prevent deadlock later in the project. ## Development costs -Most of the development cost for this project will come from the price of using pre-developed APIs. While the Discord API has a global rate limit for requests, there is no cost associated with use. There are many Speech-to-text APIs available for public use, with differing costs and pricing schemes. For most, a length of time is free either weekly or monthly, and additional time must be purchased. -To create a cost estimate for this project, we will make a few assumptions and assertions: -There are four (4) members of the development team -The project will take three (3) months of development -Each member will commit an average of ten (10) hours per week to the project -An average of 25% of the work time will be used for API calls -Assertions 1 and 2 will remain constant, but assertions 3 and 4 are estimated slightly above what will likely be needed to avoid underestimating cost. Using these numbers, we can estimate the total project cost for each API here: -We can see the models vary in pricing strategy and costs. Google tops the pricing by a good margin, but boasts top-of-the-line accuracy, although the accuracy numbers of these models aren’t made public. IBM Watson also supports several languages and includes 500 monthly free minutes, with middle of the pack pricing for the researched models. AssemblyAI brings the cheapest per-hour pricing with three free hours monthly, but only supports English at the time of writing. Use of IBM Watson or AssemblyAI would help keep costs to around $100 total for the project, and no other expenses should be necessary. +Most of the development cost for this project will come from the price of using pre-developed APIs. While the Discord API has a global rate limit for requests, there is no cost associated with use. There are many Speech-to-text APIs available for public use, with differing costs and pricing schemes. For most, a length of time is free either weekly or monthly, and additional time must be purchased. To create a cost estimate for this project, we will make a few assumptions and assertions: There are four (4) members of the development team The project will take three (3) months of development Each member will commit an average of ten (10) hours per week to the project An average of 25% of the work time will be used for API calls Assertions 1 and 2 will remain constant, but assertions 3 and 4 are estimated slightly above what will likely be needed to avoid underestimating cost. Using these numbers, we can estimate the total project cost for each API here: We can see the models vary in pricing strategy and costs. Google tops the pricing by a good margin, but boasts top-of-the-line accuracy, although the accuracy numbers of these models aren’t made public. IBM Watson also supports several languages and includes 500 monthly free minutes, with middle of the pack pricing for the researched models. AssemblyAI brings the cheapest per-hour pricing with three free hours monthly, but only supports English at the time of writing. Use of IBM Watson or AssemblyAI would help keep costs to around $100 total for the project, and no other expenses should be necessary. ## Measurements of success -As our project is so large in size there will need to be many specific goals set and met in order to complete the project within this semester, a sorted list follows showing checkpoints we must meet in the order they must be completed to have a working virtual assistant. -Create a javascript application capable of interfacing with all required functions of Discord -Send and receive messages from discord -Send and receive audio from discord -Administer the discord server -Create interface between JavaScript application and the Google Speech-to-Text API -(MIDTERM CHECKPOINT) - to include testing and documentation of currently written code -Create a text parser with the ability to decipher and execute commands -Create an interface between javascript application and Google Text-to-Speech API -(FINAL CHECKPOINT) - to include testing,documentation and deployment of virtual assistant. + +As our project is so large in size there will need to be many specific goals set and met in order to complete the project within this semester, a sorted list follows showing checkpoints we must meet in the order they must be completed to have a working virtual assistant. Create a javascript application capable of interfacing with all required functions of Discord Send and receive messages from discord Send and receive audio from discord Administer the discord server Create interface between JavaScript application and the Google Speech-to-Text API (MIDTERM CHECKPOINT) - to include testing and documentation of currently written code Create a text parser with the ability to decipher and execute commands Create an interface between javascript application and Google Text-to-Speech API (FINAL CHECKPOINT) - to include testing,documentation and deployment of virtual assistant. This list is subject to change, we will likely need to add additional steps however this is a good outline of the steps necessary to creating a Virtual assistant. ## Architecture Diagram + Insert Architecture Diagram here. ## Technologies + Insert Technologies here ## Roles + Insert who will be responsible for what ## Week-by-week schedule -Schedule is subject to change in the case of unknown circumstances. - -* February 14 - February 18 -Work on implementation and architecture plan -Research JavaScript and Discord.js to figure out interaction between script and Discord (send and receive audio and text, administrate the server) - - -* February 21 - February 25 -Finish implementation and architecture plan -Begin writing and testing JavaScript code to get audio and messages from the server. - -* February 28 - March 4 -Finish implementation of JS to receive commands from Discord. -Submit ‘Repository and begin implementation’ assignment -Begin to interface between JS and google speech to text if time permits - -* March 7 - March 11 (Spring Break) -Interface between JS and google speech to text -Prepare mid term presentation and practice if group can meet over break - -* March 14 - March 18 -Have JS and speech to text interface completed and tested. -Practice and deliver presentation if time was not found over break -Have testing and documentation for all code written thus far - -* March 21 - March 25 -Code parser to interpret speech to text results -Add some possible commands for bot to act on -Write code for text to speech to reply to user - -* March 28 - April 1 -Write JS to send response back to Discord Voice chat -Continue adding features that users can command bot with - -* April 4 - April 8 -Polish Code (extra time in case of delays) -Begin drafting final report - -* April 11 - April 15 -Submit final report draft - -* April 18 - April 22 -Prepare and practice final presentation -Edit final report based on feedback from draft -* April 25 - April 29 -Submit Finalized report -Deliver presentation +Schedule is subject to change in the case of unknown circumstances. + +* February 14 - February 18 Work on implementation and architecture plan Research JavaScript and Discord.js to figure out interaction between script and Discord (send and receive audio and text, administrate the server) +* February 21 - February 25 Finish implementation and architecture plan Begin writing and testing JavaScript code to get audio and messages from the server. +* February 28 - March 4 Finish implementation of JS to receive commands from Discord. Submit ‘Repository and begin implementation’ assignment Begin to interface between JS and google speech to text if time permits +* March 7 - March 11 (Spring Break) Interface between JS and google speech to text Prepare mid term presentation and practice if group can meet over break +* March 14 - March 18 Have JS and speech to text interface completed and tested. Practice and deliver presentation if time was not found over break Have testing and documentation for all code written thus far +* March 21 - March 25 Code parser to interpret speech to text results Add some possible commands for bot to act on Write code for text to speech to reply to user +* March 28 - April 1 Write JS to send response back to Discord Voice chat Continue adding features that users can command bot with +* April 4 - April 8 Polish Code (extra time in case of delays) Begin drafting final report +* April 11 - April 15 Submit final report draft +* April 18 - April 22 Prepare and practice final presentation Edit final report based on feedback from draft +* April 25 - April 29 Submit Finalized report Deliver presentation diff --git a/index.js b/index.js index 08eab8c..e539fbb 100644 --- a/index.js +++ b/index.js @@ -6,21 +6,26 @@ @Description: Main file for program. @Changelog: - 3/19/2022 IS:Convert main loop into async function and add sleep function. - 2/25/2022 IS:Added import statement for config file and import for environmental variables as needed. - 2/19/2022 IS:Added import statements and basic operation. + 3/19/2022 IS: Add audio conversion with ffmpeg. + 3/19/2022 IS: Convert main loop into async function and add sleep function. + 2/25/2022 IS: Added import statement for config file and import for environmental variables as needed. + 2/19/2022 IS: Added import statements and basic operation. */ +// Discord handler const DiscordHandler = require('./src/discord'); const Parser = require('./src/parser'); const S2T = require('./src/s2t'); +const T2S = require('./src/t2s'); +const { execSync } = require('child_process'); -const discordclient = new DiscordHandler(); -const parser = new Parser(discordclient); +const discord_client = new DiscordHandler(); +const parser = new Parser(discord_client); const speech2text = new S2T(); +const text2speech = new T2S(); /** * Helper function, sleeps for x ms. - * @param ms miliseconds to sleep + * @param ms {int} Milliseconds to sleep */ function sleep(ms) { return new Promise((resolve) => { @@ -28,24 +33,78 @@ function sleep(ms) { }); } +/** + * Use ffmpeg to convert audio from signed-16 bit, little endian, 48000hz, 2 channel audio to WAV + * @param audio {string} File name of audio file + * @returns {string} New audio filename + */ +function convert_audio(audio) { + const new_filename = audio.slice(0, -4) + '.wav'; + console.log('audio conversion has started'); + // use ffmpeg to convert signed-16 bit, little endian, 48000hz, 2 channel audio to WAV + const command = 'ffmpeg -f s16le -ar 48k -ac 2 -i ' + audio + ' ' + new_filename; + console.log(command); + execSync(command); + console.log('audio conversion has ended'); + return new_filename; +} -discordclient.login().then(async () => { - let cont = true; +discord_client.login().then(async () => { + const cont = true; while (cont) { // if the discord client has audio ready to process - if (discordclient.audio_ready) { + if (discord_client.audio_status) { + let audio = await discord_client.audio_clip; + await sleep(1); + audio = convert_audio(audio); + // transcribe the audio using the speech to text module - const transcript = await speech2text.transcribe(discordclient.audio_clip); + const transcript = await speech2text.transcribe(audio); + + console.log('Transcript:' + transcript); + // Parse the transcript and preform actions const status = parser.parse(transcript); - if (!status) { - cont = false; + let test = 'none'; + switch (status) { + case 'Played file': + test = 'play'; + break; + case 'Stated time': + test = 'time'; + break; + case 'Stated weather': + test = 'weather'; + break; + case 'Banned user': + test = 'ban'; + break; + case 'DMed user': + test = 'dm'; + break; + case 'no command found': + test = 'none'; + break; + case 'echoed command': + test = 'echo'; + break; } + + await discord_client.pool.getConnection() + .then (conn => { + conn.query('USE dva'); + conn.query('REPLACE INTO command_usage VALUES (?)', [test]); + return conn.release(); + }); + + // await text2speech.convert(status); + + await discord_client.play('response.mp3'); } await sleep(100); } }, ).finally(() => { - discordclient.logout().then(() => console.log('Bot shutdown successfully')); + discord_client.logout().then(() => console.log('Bot shutdown successfully')); }); diff --git a/scripts/figures/README.md b/scripts/figures/README.md new file mode 100644 index 0000000..d40cdf7 --- /dev/null +++ b/scripts/figures/README.md @@ -0,0 +1,17 @@ +# Figures + +## Instructions + +### Installation +Figure generation is handled by python if you do not have a python interpreter or the pip package manager they can be downloaded [here](https://www.python.org/downloads/) +1. install required python packages +```shell +python -m pip install -r requirements.txt +``` +2. Figure generation requires database access, if you haven't setup database environmental variables please refer to the main README for instructions +### Usage +To run figure generation: +```shell +python generate_figures.py +``` +This will save and display a graph which represents the usage of the record function over time. \ No newline at end of file diff --git a/scripts/figures/generate_figures.py b/scripts/figures/generate_figures.py new file mode 100644 index 0000000..b94a24b --- /dev/null +++ b/scripts/figures/generate_figures.py @@ -0,0 +1,110 @@ +import mariadb +import sys +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +import os + +""" + @Project: Discord Virtual Assistant + @Title: generate_figures.py + @Authors: Ian Sodersjerna, Jacob Austin, Jonathan Tucker, Nick Miceli + @Created: 2/19/2022 + @Description: Creates figures to track usage of DVA + + @Changelog: + 3/29/2022 IS: Basic database connection, data processing, and figure generation +""" + +try: + conn = mariadb.connect( + user=os.environ.get('DVA_DATABASE_USER'), + password=os.environ.get('DVA_DATABASE_PASSWORD'), + host=os.environ.get('DVA_DATABASE_HOST'), + database="dva" + + ) +except mariadb.Error as e: + print(f"Error connecting to MariaDB Platform: {e}") + sys.exit(1) + +# Get Cursor +cur = conn.cursor() + +cur.execute("SELECT time FROM record_usage") + +dates = [] +for t in cur: + dates.append(t[0].date()) + +usage_per_day = [] +while len(dates) > 0: + count = 0 + d0 = dates[0] + + # count occurrences of d0 + for d in dates: + if d == d0: + count += 1 + + # remove all occurrences of d0 + dates = [i for i in dates if i != d0] + + usage_per_day.append((d0, count)) + +usage_per_day.sort() + +day, usage = zip(*usage_per_day) + +plt.bar(day, usage) +plt.title("record command usage") +plt.xlabel('Days') +plt.xticks(rotation='vertical') + +plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%m/%d/%Y')) +plt.gca().xaxis.set_major_locator(mdates.DayLocator()) + +plt.ylabel('Usage') + +plt.autoscale() +plt.subplots_adjust(bottom=0.25) + +plt.savefig("record_usage_figure.png") +plt.show() + +cur.execute("SELECT command FROM command_usage") + +commands = [] +for command in cur: + commands.append(command[0]) +commands.sort() + +commands_usage = [] +while len(commands) > 0: + count = 0 + c0 = commands[0] + + # count occurrences of d0 + for d in commands: + if d == c0: + count += 1 + + # remove all occurrences of d0 + commands = [i for i in commands if i != c0] + + commands_usage.append((c0, count)) + +command, usage = zip(*commands_usage) + +plt.bar(command, usage) +plt.title("voice command usage") +plt.xlabel('command') + + +plt.ylabel('Usage') + +plt.autoscale() +# plt.subplots_adjust(bottom=0.25) + +plt.savefig("command_usage_figure.png") +plt.show() diff --git a/scripts/figures/requirements.txt b/scripts/figures/requirements.txt new file mode 100644 index 0000000..b305ccd --- /dev/null +++ b/scripts/figures/requirements.txt @@ -0,0 +1,3 @@ +matplotlib +pandas +mariadb \ No newline at end of file diff --git a/src/discord.js b/src/discord.js index 9963453..a177da1 100644 --- a/src/discord.js +++ b/src/discord.js @@ -6,6 +6,8 @@ @Description: Interface with discord API @Changelog: + 3/25/2022 IS: create on_message function. + 3/24/2022 IS: add play function 3/19/2022 IS: Upgrade to async functions, update token env name to DISCORD_TOKEN 3/16/2022 IS: Update database to mariaDB 2/08/2022 IS: Add sqlite3 database with temp test function. @@ -24,19 +26,15 @@ require('dotenv').config(); class DiscordHandlerGeneric { /** - * Generic constructor, used to set discord token, prepare the audio queue, and create a mariadb pool to access database. + * Generic constructor, used to set discord token, prepare the audio queue. */ - constructor(timeout) { - this.LOGIN_TIMEOUT = timeout; - + constructor() { // Discord token this.token = process.env.DISCORD_TOKEN; // Voice recording. this.audio_ready = false; this.audio_queue = []; - - } /** @@ -59,6 +57,10 @@ class DiscordHandlerGeneric { return clip; } + /** + * create a mariadb pool to access database and check database. + * @returns {Promise} + */ async login() { this.pool = mariadb.createPool({ // process.env.TOKEN @@ -68,22 +70,34 @@ class DiscordHandlerGeneric { connectionLimit: 10, }); - await this.pool.getConnection() + const success = await this.pool.getConnection() .then (conn => { - // create the database if it does not exist should only occur when changing databases. - conn.query('CREATE DATABASE IF NOT EXISTS dva'); - // change into dva database - conn.query('USE dva'); - // create last_seen table if it does not exist - return conn.query('CREATE TABLE IF NOT EXISTS last_seen (username CHAR(100) PRIMARY KEY, server CHAR(20))'); - }) - .then((res) => { - console.log(res); - }).catch(err => { - console.log(err); + try { + // create the database if it does not exist should only occur when changing databases. + conn.query('CREATE DATABASE IF NOT EXISTS dva'); + // change into dva database + conn.query('USE dva'); + // create last_seen table if it does not exist + conn.query('CREATE TABLE IF NOT EXISTS last_seen (username CHAR(100) PRIMARY KEY, server CHAR(20))'); + // create record_usage + conn.query('CREATE TABLE IF NOT EXISTS record_usage (time DATETIME)'); + // create command usage + conn.query('CREATE TABLE IF NOT EXISTS command_usage (command CHAR(100))'); + return conn.release(); + } + catch (e) { + return false; + } }); + if (success !== false) { + console.log('INFO:Database creation complete'); + } } + /** + * Release database resources. + * @returns {Promise} + */ async logout() { if (this.pool) { await this.pool.end(); @@ -97,130 +111,219 @@ class DiscordHandlerGeneric { * Discord Bot Handler class */ class DiscordHandler extends DiscordHandlerGeneric { + /** - * Base constructor, sets up bot, on message functions and logs the bot in. + * Base constructor, sets up bot, and event handlers. */ constructor() { - super(20000); + super(); // Discord API client object. this.client = new Discord.Client(); // Voice connection defaulted to null indicating bot is not connected to voice. this.connection = null; - this.connected = false; - // Message handling function. this.client.on('message', async message => { + await this.on_message(message); + }); - console.log(message.author.username + ': ' + message.content); - // If the bot sent the message we can ignore the rest. - if (message.author.bot) return; + // Function that runs once at startup. + this.client.once('ready', () => { + console.log('INFO:Discord Bot Ready!'); + }); - this.pool.getConnection() - .then (conn => { - conn.query('USE dva'); - return conn.query('REPLACE INTO last_seen VALUES (?, ?)', [message.author.username, message.guild.id]); - }) - .then((res) => { - console.log(res); - }) - .catch(err => { - console.log(err); - }); - - if (message.content === 'join') { - // Join the same voice channel of the author of the message - if (message.member.voice.channel) { - this.connection = await message.member.voice.channel.join(); - } - else { - message.channel.send('You are not connected to a server'); - } - } - else if (message.content === 'record') { - message.channel.send('Recording starting now'); - if (!this.connection) { - if (message.member.voice.channel) { - this.connection = await message.member.voice.channel.join(); - } - else { - message.channel.send('You are not connected to a server'); - return; - } - } - // create a recorder object - const audio = this.connection.receiver.createStream(message.author, { mode: 'opus' }); - // save audio stream, refer to https://v12.discordjs.guide/voice/receiving-audio.html#basic-usage for playback information - if (!fs.existsSync('recordings')) { - fs.mkdirSync('recordings'); - } - const writer = fs.createWriteStream('recordings/' + message.id + '.ogg'); - audio.pipe(writer); - await writer.on('finish', () => { - // this.connection.play(fs.createReadStream('recordings/' + message.id + '.ogg'), { type: 'ogg/opus' }); - this.audio_queue.push('recordings/' + message.id + '.ogg'); - this.audio_ready = true; - message.channel.send('Recording finished'); - }); + } - } - else if (message.content === 'test') { - await this.pool.getConnection() - .then (conn => { - conn.query('USE dva'); - return conn.query('SELECT * FROM last_seen'); - }) - .then((res) => { - console.log(res); - }) - .catch(err => { - console.log(err); - }); - } + /** + * Function that handles a message event. + * + * @param message message object. + * @returns {Promise} + */ + async on_message(message) { + + console.log(message.author.username + ': ' + message.content); + // If the bot sent the message we can ignore the rest. + if (message.author.bot) return; + + await this.pool.getConnection() + .then (conn => { + conn.query('USE dva'); + conn.query('REPLACE INTO last_seen VALUES (?, ?)', [message.author.username, message.guild.id]); + return conn.release(); + }); + + if (message.content === 'join') { + await this.on_join(message); + } + else if (message.content.startsWith('play')) { + await this.on_play(message); + } + else if (message.content === 'record') { + await this.on_record(message); + } + else if (message.content === 'test') { + await this.on_test(message); + } + } + + /** + * Function called when 'join' message is sent + * @param message message that called this function + * @returns {Promise} + */ + async on_join(message) { + // Join the same voice channel of the author of the message + if (message.member.voice.channel) { + this.connection = await message.member.voice.channel.join(); + } + else { + await message.channel.send('You are not connected to a server'); + } + + } + + /** + * Function called when 'play' message is sent + * @param message message that called this function + * @returns {Promise} + */ + async on_play(message) { + const args = message.content.split(' '); + + if (!args.length > 1) { + await message.channel.send('must provide an argument'); + return; + } + + if (!fs.existsSync(args[1])) { + return; + } + const dispatcher = this.connection.play(args[1]); + // const dispatcher = this.connection.play(fs.createReadStream('recordings/957639346616406076.ogg'), { type: 'ogg/opus' }); + + dispatcher.on('start', () => { + console.log('INFO:audio.mp3 is now playing!'); }); - // Function that runs once at startup. - this.client.once('ready', () => { - // console.log('Discord Bot Ready!'); - this.connected = true; + dispatcher.on('finish', () => { + console.log('INFO:audio.mp3 has finished playing!'); + }); + + // Always remember to handle errors appropriately! + dispatcher.on('error', console.error); + + } + + /** + * Function called when 'record' message is sent + * @param message message that called this function + * @returns {Promise} + */ + async on_record(message) { + await this.pool.getConnection() + .then (conn => { + const t = new Date(); + const dateString = t.getUTCFullYear() + '-' + (t.getUTCMonth() + 1) + '-' + t.getUTCDate() + ' ' + t.getUTCHours() + ':' + t.getUTCMinutes() + ':' + t.getUTCSeconds(); + conn.query('USE dva'); + return conn.query('INSERT INTO record_usage VALUES (?)', [dateString]); + }); + if (!this.connection) { + if (message.member.voice.channel) { + this.connection = await message.member.voice.channel.join(); + } + else { + message.channel.send('You are not connected to a server'); + return; + } + } + message.channel.send('Recording starting now'); + // create a recorder object + const audio = this.connection.receiver.createStream(message.author, { mode: 'pcm' }); + // save audio stream, refer to https://v12.discordjs.guide/voice/receiving-audio.html#basic-usage for playback information + if (!fs.existsSync('recordings')) { + fs.mkdirSync('recordings'); + } + const writer = fs.createWriteStream('recordings/' + message.id + '.pcm'); + audio.pipe(writer); + await writer.on('finish', () => { + // this.connection.play(fs.createReadStream('recordings/' + message.id + '.ogg'), { type: 'ogg/opus' }); + this.audio_queue.push('recordings/' + message.id + '.pcm'); + this.audio_ready = true; + message.channel.send('Recording finished'); }); + } + /** + * Function called when 'record' message is sent + * @param message message that called this function + * @returns {Promise} + */ + async on_test(message) { + const res = await this.pool.getConnection() + .then (conn => { + conn.query('USE dva'); + const q_res = conn.query('SELECT * FROM command_usage'); + conn.release(); + return q_res; + }); + console.log('INFO:'); + console.log(res); + await message.channel.send(res.toString()); } + /** + * Log the bot into the discord api and setup resources. + * + * @returns {Promise} + */ async login() { - class Login_error extends Error {} try { - const start = Date.now(); await super.login(); - // Log the bot in. await this.client.login(this.token); - while (!this.connected) { - const now = Date.now(); - if (now - start > this.LOGIN_TIMEOUT) { - throw new Login_error('login timer exceeded'); - } - } return true; } catch (error) { - console.log(error); + console.log('ERROR:' + error); return false; } } + /** + * Log the bot out of the discord api and release resources. + * + * @returns {Promise} + */ async logout() { try { await super.logout(); await this.client.destroy(); + delete this.client; return true; } catch (error) { - console.log(error); + console.log('ERROR:' + error); return false; } } + /** + * Play an audio file through the voice connection. + * + * @param filename + * @returns {Promise} + */ + async play(filename) { + try { + if (this.connection) { + this.connection.play(filename); + } + } + catch (e) { + console.log('ERROR:' + e); + } + } } /** @@ -228,8 +331,11 @@ class DiscordHandler extends DiscordHandlerGeneric { */ // eslint-disable-next-line no-unused-vars class DiscordHandlerTest extends DiscordHandlerGeneric { + /** + * + */ constructor() { - super(30000); + super(); } diff --git a/src/parser.js b/src/parser.js index 9101aba..676f2ef 100644 --- a/src/parser.js +++ b/src/parser.js @@ -6,6 +6,7 @@ @Description: Responsible for parsing user input @Changelog: + 3/29/2022 JA: Added a function to parse the text and return the command 3/16/2022 JA: Added shell functionality for testing 2/19/2022 IS: Added basic structure */ @@ -29,18 +30,42 @@ class Parser extends ParserGeneric { // if first word is 'discord' then strip from string if (text.toLowerCase().startsWith('discord')) { text = text.substring(8); - } - - // TODO: implement checking if text is an included command - - // basic shell for testing, only covers one case - if (text.toLowerCase() === 'what time is it') { - this.text2speech.convert('It is ' + new Date().toLocaleTimeString()); - return 'time found'; + // check next word for possible commands + const sentence = text.split(' '); + switch (sentence[0].toLowerCase()) { + case 'play': + this.discord.play(sentence[1]); + return 'Played file'; + case 'time': + this.text2speech.convert(new Date().toLocaleTimeString()); + this.discord.play('response.mp3'); + return 'Stated time'; + case 'weather': + this.text2speech.convert('Feature coming soon!'); + this.discord.play('response.mp3'); + return 'Stated weather'; + case 'ban': + // const user = sentence[1]; + // this.discord.ban(user); + this.text2speech.convert('Feature coming soon!'); + this.discord.play('response.mp3'); + return 'Banned user'; + case 'dm': + // const user = sentence[1]; + // const message = sentence[2:]; + // this.discord.dm(user, message); + this.text2speech.convert('Feature coming soon!'); + this.discord.play('response.mp3'); + return 'DMed user'; + case 'echo': + sentence.shift(); + this.text2speech.convert(sentence.join(' ')); + this.discord.play('response.mp3'); + return 'echoed command'; + } } return 'no command found'; } - } class ParserTest extends ParserGeneric { constructor(discord) { diff --git a/src/s2t.js b/src/s2t.js index ef3eac9..483c864 100644 --- a/src/s2t.js +++ b/src/s2t.js @@ -6,6 +6,7 @@ @Description: Interface with Speech-to-Text API @Changelog: + 3/27/2022 IS: Changed encoding to WAV and added channel count and separate channel recognition to support dual channel audio. 3/21/2022 JT: finished first working version 3/20/2022 JT: changed encoding to OGG_OPUS 3/14/2022 JT: added Google Speech-To-Text and began work on transcription @@ -28,7 +29,7 @@ class S2T { } /** - * Function takes in a path to an audio file in OGG_OPUS format and uses + * Function takes in a path to an audio file in WAV format and uses * Google's speech-to-text API to create a transcript. The transcript * is then returned as a string. * @param filepath: the path to the audio file @@ -36,15 +37,13 @@ class S2T { */ async transcribe(filepath) { // Set up information for transcription request. - const encoding = 'OGG_OPUS'; - const sampleRateHertz = 48000; const languageCode = 'en-US'; const config = { - encoding: encoding, - sampleRateHertz: sampleRateHertz, - languageCode: languageCode - }; + languageCode: languageCode, + audioChannelCount: 2, + enableSeparateRecognitionPerChannel: true, + }; const audio = { content: fs.readFileSync(filepath).toString('base64') @@ -54,16 +53,15 @@ class S2T { config: config, audio: audio }; - + // Send request and receive response. const [operation] = await this.s2tClient.longRunningRecognize(request); const [response] = await operation.promise(); // Return the transcription. const transcription = response.results - .map(result => result.alternatives[0].transcript) - .join('\n'); - return transcription; + .map(result => result.alternatives[0].transcript); + return transcription[0]; } } diff --git a/src/t2s.js b/src/t2s.js index f0a1b2e..985981e 100644 --- a/src/t2s.js +++ b/src/t2s.js @@ -51,7 +51,7 @@ class T2S { const [response] = await this.t2sClient.synthesizeSpeech(request); const writeFile = util.promisify(fs.writeFile); // Remember the audio content before converting to .MP3 (for testing) - this.lastAudioContent = response.audioContent; + this.lastAudioContent = response.audioContent.toString('hex'); // Write file and return promise so the file can we 'awaited' return await writeFile(fileToWrite, response.audioContent, 'binary'); } diff --git a/tests/discord.test.js b/tests/discord.test.js index c98d62c..736dbad 100644 --- a/tests/discord.test.js +++ b/tests/discord.test.js @@ -56,32 +56,56 @@ describe('Base Discord', () => { // eslint-disable-next-line no-undef expect(discord.audio_queue.length).toEqual(0); }); + + // eslint-disable-next-line no-undef + test('Discord audio_queue dequeue first item in queue', async () => { + discord.audio_queue.push('test1'); + discord.audio_queue.push('test2'); + discord.audio_queue.push('test3'); + // eslint-disable-next-line no-undef + expect(discord.audio_clip).toEqual('test1'); + // eslint-disable-next-line no-undef + expect(discord.audio_clip).toEqual('test2'); + // eslint-disable-next-line no-undef + expect(discord.audio_clip).toEqual('test3'); + }); + // eslint-disable-next-line no-undef + test('discord token environmental variable', async () => { + // eslint-disable-next-line no-undef + expect(discord.token).toBeDefined(); + }); + }); -// // eslint-disable-next-line no-undef +// eslint-disable-next-line no-undef // describe('login/logout discord', () => { // +// let discord = null; +// +// async function setup() { +// discord = new DiscordHandler(); +// } +// +// // eslint-disable-next-line no-undef +// beforeEach(async () => { +// await setup(); +// }); +// // // // eslint-disable-next-line no-undef // test('Discord login', async () => { -// const discordclient = new DiscordHandler(); -// // eslint-disable-next-line no-undef -// expect(discordclient).toBeDefined(); -// const status = await discordclient.login(); +// const status = await discord.login(); +// await sleep(100); // // eslint-disable-next-line no-undef // expect(status).toBeTruthy(); +// await discord.logout(); // }); // // // eslint-disable-next-line no-undef // test('Discord logout', async () => { -// const discordclient = new DiscordHandler(); -// // eslint-disable-next-line no-undef -// expect(discordclient).toBeDefined(); -// await discordclient.login(); -// -// // eslint-disable-next-line no-undef -// expect(discordclient).toBeDefined(); -// const status = await discordclient.logout(); +// await discord.login(); +// await sleep(100); +// const status = await discord.logout(); // // eslint-disable-next-line no-undef // expect(status).toBeTruthy(); // }); diff --git a/tests/parser.test.js b/tests/parser.test.js index 86c3208..4c01cf6 100644 --- a/tests/parser.test.js +++ b/tests/parser.test.js @@ -21,11 +21,39 @@ test('Discord constructor', () => { expect(parser).toBeDefined(); }); -test('Test default command parsing', () => { +test('Test parsing time command', () => { const discordClient = new DiscordHandler(); const parser = new Parser(discordClient); - const result = parser.parse('discord what time is it'); - expect(result).toBe('time found'); + const result = parser.parse('discord time'); + expect(result).toBe('Stated time'); +}); + +test('Test parsing play command', () => { + const discordClient = new DiscordHandler(); + const parser = new Parser(discordClient); + const result = parser.parse('discord play response.mp3'); + expect(result).toBe('Played file'); +}); + +test('Test parsing weather command', () => { + const discordClient = new DiscordHandler(); + const parser = new Parser(discordClient); + const result = parser.parse('discord weather'); + expect(result).toBe('Stated weather'); +}); + +test('Test parsing ban command', () => { + const discordClient = new DiscordHandler(); + const parser = new Parser(discordClient); + const result = parser.parse('discord ban'); + expect(result).toBe('Banned user'); +}); + +test('Test parsing dm command', () => { + const discordClient = new DiscordHandler(); + const parser = new Parser(discordClient); + const result = parser.parse('discord dm'); + expect(result).toBe('DMed user'); }); test('Test no command found', () => { diff --git a/tests/resources/s2tTestAudio1.opus b/tests/resources/s2tTestAudio1.opus deleted file mode 100644 index aa4f496..0000000 Binary files a/tests/resources/s2tTestAudio1.opus and /dev/null differ diff --git a/tests/resources/s2tTestAudio1.wav b/tests/resources/s2tTestAudio1.wav new file mode 100644 index 0000000..a13228e Binary files /dev/null and b/tests/resources/s2tTestAudio1.wav differ diff --git a/tests/resources/s2tTestAudio2.opus b/tests/resources/s2tTestAudio2.opus deleted file mode 100644 index 336c0c4..0000000 Binary files a/tests/resources/s2tTestAudio2.opus and /dev/null differ diff --git a/tests/resources/s2tTestAudio2.wav b/tests/resources/s2tTestAudio2.wav new file mode 100644 index 0000000..beba0ee Binary files /dev/null and b/tests/resources/s2tTestAudio2.wav differ diff --git a/tests/resources/s2tTestAudio3.opus b/tests/resources/s2tTestAudio3.opus deleted file mode 100644 index 80b59ba..0000000 Binary files a/tests/resources/s2tTestAudio3.opus and /dev/null differ diff --git a/tests/resources/s2tTestAudio3.wav b/tests/resources/s2tTestAudio3.wav new file mode 100644 index 0000000..b4af2e2 Binary files /dev/null and b/tests/resources/s2tTestAudio3.wav differ diff --git a/tests/resources/s2tTestAudio4.wav b/tests/resources/s2tTestAudio4.wav new file mode 100644 index 0000000..5286255 Binary files /dev/null and b/tests/resources/s2tTestAudio4.wav differ diff --git a/tests/resources/s2tTestAudio5.wav b/tests/resources/s2tTestAudio5.wav new file mode 100644 index 0000000..a1a46ae Binary files /dev/null and b/tests/resources/s2tTestAudio5.wav differ diff --git a/tests/resources/s2tTestAudio6.wav b/tests/resources/s2tTestAudio6.wav new file mode 100644 index 0000000..b32d205 Binary files /dev/null and b/tests/resources/s2tTestAudio6.wav differ diff --git a/tests/resources/testAudioContent1.txt b/tests/resources/testAudioContent1.txt index 2a9c2f3..e969d96 100644 Binary files a/tests/resources/testAudioContent1.txt and b/tests/resources/testAudioContent1.txt differ diff --git a/tests/resources/testAudioContent2.txt b/tests/resources/testAudioContent2.txt new file mode 100644 index 0000000..fdd1faa --- /dev/null +++ b/tests/resources/testAudioContent2.txt @@ -0,0 +1 @@ +fff344c400112a453c01411001f00013002318c6c86390000fffffffffc8da9cf9de427ffc953bc849084a9ce740e774210845211a739dfff39de42108a0022387b41f808ffcfc00c00e879f9bfffeff0042dcc0262db54b762637fb3ffb9a83fff344c40e12132a88018a3800dff8ea1e5d7ffdbb1ffffb18c9309fffddf93306804c4a1009024021fffbffc01c7898bd81e0880e05e37733ffffffff160ddd8f5263860dc6e4ea0e25bf4ffffffffff51f5fff5ffffffffff15ff57ff11bbdfff344c41817b306a401c24001adf528e2f7096242cfdc4511cf1288e0d03c907818342e72c9640c0682e61b0438dc3d676a173c8e68b938526c53f46495e1295ad6ee6d1d070d1f029d9f2ee3caa4af1ae9179e02fffffffffffffffeffffebfff344c40c13cad6b8000850b9f8ffadfabefbaaa79528ecc638714a305cf1a7160fc1a1208830a4520f1b62f1441653a0d6b7df622f1cedd5bd967ddba4c2dba13191035f85c68c4129e48608811672552fffffffffffffffe52ffffffeff73fff344c40f14530ac0000850b93dcc42accecb7060f2183f19301f070342c298645c5c5984292463e2af06cad314c2a37b6a5b38c1d4e3c8aa8b19573751a44a3a5d4b67db78dd2d62a9d9aeec7d04d58782a0989fa914c9d43cdb17bdf5fbb6fff344c410164aa6bc00488ab9ec7f86fa5ffff460a41c8e44df9af2f9665cd397d0e54300cb7014103d720b0c09394cb95a83454a4123183b1618715b3a19d59473a3299ccac8eed2a0b28acab44dd84857e81a16392ab52d9a6aa6c035e879fff344c409147922b000c61670951310582b99da04d442069b38aae927cca69eca3bb41456ee599e904f59017104db83972e349eae3675356fbe7b961c345cecba9471d1626d24c1031e05102d5a9d17ec23fa6839d1c7f25e0355b14f9847a5fff344c40a138956b400c60c94b2e1401eb2189a211089c2db62dcdaf46a6250ad907d665684b6d6963ed6e13b95ce4bbbabb4ddcedef1c33671e99a1d8ac4d012b2ae632a6f67eb4ba4ecc88b1568cfc6aab9f613d89937e0c2ee2ceeb7531afff344c40e14a906b000c61271262d0c8417a8a4bd34030b430c2020bcaf2bb00c205c0d4e26dcdad4f9704cca4a18152c8194548d2d6db61b46db58a3914a588ad3c41619b17a9414182a76aeeb7fff7addcb07acc19566e71b269a9a0d02a2fff344c40e1390e6a800ce52700e561d111c731effbfa5c3545065308846d9e64e72d932e4f70482a1cce9d87f444f90f0e120a1b230d965013344b860a9edfa85c96edfded00b85e9fbbfffc515ddc51f3031fb6e86422114e034f542e4805ffff344c4121288c2a400c65e4c3de44d66dc1a7a60c792312356e8c011932882001806a8e30a189931a9cbd97d7a7e31986771a2d65bd20cf0a0e0401c1a3ffffe8ffffff52ab334fc0dbb196ec2d7cf2c18b517e00860e301642b5000183094fff344c41a1268bea000ce9e4ca9973931c5b07ea70be10f3b0e708e00c4ae00183d4fc0c83d4c20138b4924449d566bc44c1a4ffffebb2effffffe955cb4adc4e2ca82830b0574bda3c8b551f3c99b2f4c76cd9b95bc1db8af62f7adbfd1996fff344c4231190caa400ce3070406a02dea99871636091ab141e0812ee74d3b1e470e46fe3f19d4e0e26affff619475ad5fb22358fa5df00b20548f39b23209987f4c913f589977176c0eef02f79b34b7d46ab8b3224bd1291613d4031168085fff344c42f1510e6a800c3de70912410268580300b7c2278ca73ad4779042c4c4611fe80402c117943260f8da93fffa942e695b91d0bdd6abd25bf622cd0beec2c0c9a1b0f25d351cc5630d5afefddd1dc644c2a8d140581a1583053ee6e119dfff344c42d123962b400c29e9531d648b792255e7cd3c3de35f3bfadf79979abef57870299c02bf936c6989a90a49f78a3829a5ad9890a2d3167cb579dadbff7b6ee64315440062b15f8aa19f9e3db1fdaac565a65c765b79f7636d75daaac43fff344c43711b182b000c15895c4895c4abaf07d5a8ed0d8d67d994aeec24a2655d1d3d70d39964b205535ed855c358674193ffbf24190086b2213480e1f3e1f79c8e2051f3c0c5350989d93ee7b685944b333551112aa14557669255309257ffff344c44311597ea800c8529544b515b53a1b6484002c4d2547865e092d1f823b7312c0ca5ffff473246a1e65c7812483d0fc92ec4342ec6b99f31daa361dae5ecf5a7e66def254eb61952b683045e499ffffffffa16b822c027eb3cce87596fff344c45011717aa400c858941088c4821b4c664152877dfd73f9fffffffffffffffc7711562e1ea04a48fc81658a284c40955ccb81956f1adc34ed230e2affe8fffffe852347a530ee08a81630203c541dcbe61c2018c8820a0d03409bd940fff344c45d113166980158400079a197c8bddfe2501d85410944a2281eb4cd0c8c8d9fe81a14cb62974923121a202fc38f26010e08a0d98502b3011d13278bdf954a25d27c67459a4f991b56625d5ff270f16513cb746c92cc4baafff45eb312fff344c46b20a2ae68019c8800abad1304d2f52924bfffd34546689b1b999e5247cc8cf9555f46581c5080e6751694d2b82e543d1ab529b3acb7fcad6bf1e65d3b0f0c6155a937b52ffa4cdb33555552804040d512ae2cfe57f2bdda8f54fd47fff344c43b10310a0401d81800bffffaacffad00b819223838c9a1b6709d47ea25794e084d03cc3cb87f3468a030330f8b8796a191ab2820551cbec967ab05040818262a2a2848d0322a2cdfac545b50bb3ff50b0ab162a28dd7fff1754c414dfff344c44d126105680079867045332e31303055555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555554c414d45332efff344c45600000348000000003130305555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555ff030d14c116fff344c4a900000348000000001efffb2781100c817ff138bb2f50ee45c73c89862b06e405f4f1720f045c66011000ca7037181567c9c34302284e01842326169e3244dfe6e6068c43c898b987241bbc1b9e3ac338fe44d5759ba619001ba80dfff344c4ac00000348000000008806c603208da0bffff7409f531a66e23e1720c9243c87484447b20ffffa65f72719327cde811c20008d0c09b0e905a47389f16a16013a7fffe8668b7e68b4f3a89509b31489921481b944cc992c2afd97d088fff344c4ac0000034801400000a3260f27f1006b94b6e5bcff2e3b9eaca867c5ac3d716303a001c8288b2f9b9e10607fc4ee2e016505fc06f80d8fa6ea4188228b88ac97291e22e49a6f7b93874ae897c9e2a13e40c9c1cf2d9710a69d01d859fff344c4ff29cb29f401989800598170ba5a1981942284d1001cc20fefe8217520bcfb1bb9922b76e9bfedda920876ea40e2079ccffc9b0a040e6b0f3eb3756344454da4895f425342b84522790f151f458e9061400f00ce34dc2d858cdb0198fff344c4aa221a629c0198900031ae1642b12c8139cbd0749e087a2c876e29be7ba74e73a22bf09115063a4180fa142dc895a567865b110ae928ce862da392eac53b8a099191e4288e136eb3c29e4935021e234d3e66d2016a0d019b20646793fff344c4741fd97eb001987800f10022dea0a1b76867edb10afde6a603cb8537219122451cf9668490a525ad552a5a6e435bb5da5ed9e3bfd5c6de799d79e39649ab09c351a8f45b1a0d4b2f9e9b45771ebe5cf5cc20dd1d1ea39bfd4decbda6fff344c447153966b801d860006f699976d32aebbdcbffef50bbd1ace897b0a9381cad01c5da2e58840308240a689d9a61aac33330d7a5d5648d7a1ecb3ab4bc99edc5cf4e9d2e7ae4906ab5301611688622b0b9a25094b6268fbe4e449317aefff344c44517e98298015a6000727aeef5be66666df336b4cb2b5decad7b175bc79d117ffc1a7f4a19001efaf931caa3037331157f149aa31e1287b03e6c0dfa7b100882426160df2738905b1646e4827aaf73060220580b00b84e0551ffe70afff344c4381f5b2a980199500082c13919391908538d4268dbf8f0ccca09e311f1104805815bfe492024a33b0923c12828c5c278d4c2fffffe6363d2e424c48c518a1097ffffa7fff2057319911090c20346f5ee38494041426d404b06748739fff344c40d15912ab401d96000119018052fccf0a091ffe577bfbd4533c715f4947f120700c2fe4c0ccce25cf22475c4c8d9eb27b7a52e41d56af6b565c94060882e58734f1b60d51e913c1a14ffeb9790ffeaa774ba55fe7280ce0ef7764a28fff344c40911b156b400cace9478d76f37337557b78362e9cc4755f0090dd001c0d3a619002e91287a6dd59037e84b92ea3cec60402d35c7ef99d3741d3aa51f6d9e3effffa8f93fd9ffa9e6555db133a1579b9857140e9d898165048cfdd034fff344c41511c946ac00ce50708505eef359721ddc650d0edfc424b85a341400b50c30e8f43b4a6f855d17fa6641c2e229e2eda1f135cc1d6260d7fffffdf4aac263021ac6ab86a70a87a171745f61d44352b9549d811a5177e901c4ceda4883fff344c42012711a9c00cbd270a91c933607f3b7ed43625aa226ad63a66336a37571778dab24f3edd6225ccacdc43a050ed6759458dfa69250b9c0eb01a04e34800480a09db596ef870f01438198be03f7624ebda4d0a33cdb1e397b416a3bbffff344c42910f9729800cb0695d4bfd2f98aee6672ccb3a5a741002988d15ce139be5b9a162dcf8fdfecbdebf7188a45515651c286adea080c065fc48578b01a6d980f1c858026d18020697f5bf8c3b7a647339bfc7ddeffffdacbc5a25d1826fff344c438120172b800c31294bb6ba56f5e4e8ace46bbe1e98b03b397fe570b25e7e500b268bc908bfa9aa88ea9da432bf7b3e12b4ea39bca734f04800d97581d8acf947ef723adf80952ef57fccdc89a3c129252408d0a9d8883a69c2c741dfff344c443129186b800c3ce940f78acbd4a75c595fdc904107ce9db224dc63a55425dc5e5e8a06bf6cc6f2baae30a1d79597d0d81d509b0b91c397ef37992bf33d735eab3ddf08f2f3294c0f2f283d9cd726d1d13a9caa5d9c5dee35d4ecaaafff344c44b128986b800c3d6949afa7397b6ad580170f16e4b382284b8cefae559a06b5fba992a4f59aa5d9af4163f001811203e54960aabf36de672c8f76dcc774d2a93804584949557adf5b3dabf3b417e2cda9fbb69695662de9b7b1048a8fff344c4531241629c00ca4c9593bf281001de0bac5c8fb528905c1ff61d5adeaf6aef7ebcf661445bd98c6cb0991c76d6dd636d2f29ba0590c4ed30c88bfdcd1aa16030785a97ae10e73ace42faccf3b1cda05623e303129a226f621f974d74fff344c45d1130f288015a4800addd596633c444a8553eb453260b8533133270bce70c0ae390440650e5683155130281344e10c21c2e02e94d3459989d525c72c698e0006c1bbc85f52a9bdeca1e85c8789b19719b3cedfdeff999b914228542fff344c46b217b1a88019a80012e6f4ffffffd374169a08569d7fffffffe666e8a6e6881a17dd58124297ecb0b08792946b650349a167075816b13b506a72ab6b9d6fc6a55f99c2b61bab9e7ba07f72a092b4e90c0f18a78cb067f63b157d888fff344c4381f61c6ac0198c000adb372646fc5d9651c9f527f9752ef98486d76d77976c4f677a9ac634dbdd9a0d52ea376a96c737632c39de77e9bf5ddebbff7f8126d151024d654ba3f6b05d08054b33dd175c62294224f4340041589e734e5fff344c40d143946ac01d978010c2932978c3b05d265cb58d0ff6e65f7f1cdb102f05fb8a916c9cae86e0488c84593d34d7662ae5b12cfb5366dade7ebfc6b3ffffe7ffebaf89c28a620a71a99437bebc8bad905262ea6786142c50842809f3ffff344c40f15e8e6a000d3de70a2a21b6907ed68e5b80fb0c3aa46dc38d8cea1c91df2e5485dc18a6a84a8bc224b25313818c9e4ec0543e815bcf93e203079a2082680212130e1e12110082e642f681d0fffffffff5dcdc013a511065319765afff344c40a11d8fab800784c704f9c3857f7fffffda64a850e392ee47ba31998663402041072c73850ba87a44c8139fa09b50241004006f5a25ff5ca38bbc3e9a519f3820fe8327ec6f28a9341b48a0951475fffffff9f8b19720c9722b29008fff344c415178ac2c0006950b90287438a71e737856e1f30f228628701288e20160de33d485ee4b20b3c5054471d502ef796f75336b337f5b22ddd3ad457bef53ab5d455e896933d24a5df1571318b9e439dbcedfd92d5830edffffffffff7fffff344c40911b9ded0001046997872850ce69dfcfc8e21b31aa660642f223001b3a5bb818340438b48624512081a0279f043d9511c6534e9b0f74f5f58d75fd442d171bdc9e89ceb99c3b4983ffffffffffffff72a95bf5f4ca9355844c462a3fff344c41510d202cc0141280118d5162c6b049c780ca7514132230f1f72a1594b112ca276917d6f106de3614b5d5d73b2de895cec093bc6dbdee56a01b0f81a58430c17920b0ddbfb7fe8534e8a9dbef4ddab6c9bffa149138644d3c83765a2fff344c4241a2aa688018a680082d3a771cc3847e01cc2d8bc0ea0911791a6a4594923b9a31e4e98c309facac662ea448b98178eb2eb44e19ad5becbf6e3d495323631244952f2491352323ea0a983cfe95dd9c5b14a962b4c5560627a6e75b5fff344c40e1601f6b801894000ffff749ffffff1fb5d429ff094895e934d8d074737089ee7d532d0e37fd88ed2df1a7f15142a2289a9f4ceb4529e7cb3ec069878386634267ab167ad430e4cf2b321a065640ec858b3abad5a240a0c8f4c087afff344c409127172b801d8480092a692069a90963c0da6795dde7877f7dff9ff59fafefb7f113b43c0d2014161c80c1e9b028cbb7d324c69754de452cb9ff59fdeff3fe7eb9be90010ed4fffd45ddf3c38d47c66699b6b101853e6751ed424bafff344c41212017ebc00c1da948371324a99b68f90cd2f41ace101e83c15a6ae2c8724a8d5a4c54e8dae62278aa679a79aa42b5660eb9e4569bad6798bd162dfffe891ffa83c9e5749784e51244ba9068eae8902632a03b5476fa3e7a11ee0d5fff344c41d11796ec000c1589433a87c1b3e763b8c74e106f757d78c7d8fcffaedc5978e77fa72b3b04f52b4a332a0504365abfffa94fdca839f6aeac2c3f84169e57780a66e456fb31779afc5f17ea7a9f2e9d197aa5f5a840b5d0e547b26dffff344c42a11e95ebc00c2d894c6b5b080b9f29bdb8b77ebfd914cfccfc52d1f2ad357a6c5bd3ffdab78a5f82ac4faf408547ca75055070038cc1018474c676981b548d696f9e7fd3398a4bc65428158b2002034c2a2a5a1b49219488f210454fff344c43511f926bc007b52709483a680660a8b8880e64545d2fd42edfffa92cd8d9044e8ba068c56c274e4a70931626302c4ab98264891a8312adcf51f51f72f728c5a55264542c0d017872281d95949c723185a47de7bd245a769efe5e479fff344c440109956c0007a5095453f75d4942ab48178d26a89f75dd66389126e973268e6936086d8cf0d5d35615375ce332a8e56f4b334898e13e32782a2969965f26f2a752d81d872bdc383c2f3fbcc8e05091c6f4c6ffff42aeba04fda5202fff344c450124912b400c3d270cadbbbb955adb39910dd2426817a3facc6e2852e154f1f45c46cdb18dc676a4a9ea88cc85d830aba552a6b6572daf5b8ecc6d9d94ea2aac44444254d071096bbe9ae396250b961166beb7630e0c8dbd9803a14fff344c45911c12aac00c3d270011282195529e9d98ae868d5762c9c79ceb49c5054539f7cfda86d9fb6ccadb7a482e8728e687c4c81548ab05bff5fffffffffa75dbc2bd2922a9c99eaf61e84b72861fd670658d04b9f0967b090e6fd67c1d6fff344c46511a916a400c30c70f7ac7c6de65f0d52f028248d634e4c6cfc996aee6d59122e59d2ae0ecf48acc1a47d7fffffffffd44109e19a4210f90310c59b0ca228453ec0cac30728002e0347010d718c6fa6abcba03efd676aed54d9edc8fff344c47111790e9400cbcc700050520a42a4d3c55c7573acad2922a15bade9e9964fc136c33f30334173103831d0f604a2482530d02705e41502665715993e6f2affff33dd49fb4ff47b8151ea9ae8293ef518cb0f889ffbfaeb5abf47fffffff344c47e1000fe6c00d31070d9ffa630d303851f0cbb30e030c1e47a5320483028296796f618745f98c52cba55976f72fe5bada323dbd71a7150b620ca8accf22943a53284fa59f78cbd16fffff77bb776ffd95725399a56f01ee0ea109a92fff344c4911030ee6000da4c703198dbd0b08fb44a72f5c70dca7f9970ca5b32364bacea4b1f5689752aeb70b58cb11b187299342a2a311ebf91cf0e71d04bc73d24c9c6c63df7208f73e481817070191b25322495ff659a299468c91917b4aafff344c4a31178ea5c015b2800491fff240943034648b8d2e1a298bcba4b992e97ffff44bee8199b88e19fda491d91dc0732749ab1458f47154d8bf2386f12e4d2990a4ec77eac7798fe9af9fa3d9586b30ada2ab52c0a87c8254378563f1ad6fff344c4b02002d26c01996801b6afb4cae4e22974b2b12936278e59f7737b335db5accad8977fd9ede6d6f7fed2b35efcdacf74cf7e32ecd9afac1f699d9edcb4dbe72db96a7fe7da56a14aea62ff8d04155343498dfedefcd1f2aa0cd21a69fff344c4831f8a620001cf60012890e8ef54c7321acaa520a4b5b1452eb42a609950a8010cc6b50a18c089f15a5522a192542156c11404c42c3c52848812049bc5b7c6d95898880448951a8f9233e9a67ca38e682a5e8925da9dd896b1224912fff344c4572012b1d4007a4cb992395fd6e4a33339eab3d3cf7367cccff394c976d99f3b39fb57fffef33873258bd051b0bcde0dc9f0aa4c414d45332e313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c4290000034800000000313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c47c0000034800000000313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c4ac0000034800000000313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c4ac0000034800000000313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c4ac0000034800000000313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c4ac0000034800000000313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c4ac0000034800000000313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa08b005821213fff344c4ac0000034800000000602c032c06b08008012854b496c5496f4bbc802b7a88336e7bd76d455b204162872fa99394125a024b47b59f21e7aa204d1eae8f60a08a0660666128b4e18410e75c2080822ff5f7fb00018b8fffff109dcd13fff344c4ac00000348000000004115cd2992573082038be8b3444a2e0b91394000804e160c063cfc227f862a1a541671cc899a915430c1442025d286afe65af13f6cb652cf9cb7605069a71d405d03f484e1f210f13980fa342e690a4090c984fff344c4ac000003480000000097259634cef92b4b3f085034ac115c8d9824d701c2493c6d51a86bb24592373ee3b546e39d6440934a7efeddeb9b0e76f7a7cc3ad553de12a365b5e12faf3091276f34cb4510ae3e7aaaaaf33e1920f03a6589fff344c4ff1dd26dd0007a46b804910eea08c9dbc7b8190d084b983402181a1a220b0900a14340c8aac90548991732305a90934d766b61917fe6853afc5ffc51b8ab750bb1230599fc55bd7c594c414d45332e31303055555555555555555555fff344c4da21ba89e400ca4cb95555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555fff344c4a60ff041b800c246245555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555 \ No newline at end of file diff --git a/tests/resources/testAudioContent3.txt b/tests/resources/testAudioContent3.txt new file mode 100644 index 0000000..821a528 --- /dev/null +++ b/tests/resources/testAudioContent3.txt @@ -0,0 +1 @@ +fff344c40011680210014310002808060988cf820086080625cffe5c1c040100c410d60e1cf89c1f07010744e080625c1c04010ac103994043f940c7e2707010040e789c1c040100401007c1f07c1f070100c2f6b03a68cee8bccdc7fd99327cfff344c40d15a3029401893801e34b9c597cbaa4f213ba30e1e383c46281141335be6abb99169c87b2ff6dfa1d7647ffa354f5ee8382924362c50b7ffe89eb78dca4b9a60e0d0b8f9c67fffefbedfd1068374a5c3118fabe2e51ffbd63ffe9f9fff344c40914cb12ac01c128015e664aa32d767ffb7edfffe7ee8cfb3d5c849875d3554617566bac4545d144dc877287c2660e104070f898e1ed1a2e3038283459cae65721c405068ba072e2ea7171e24388478d3b8852d52fff7fffe8bebf6ffff344c408147b0eb000084cb9e5ffffffffbe7dfffffb6fffffffeffd6fc6dc696dd7ad7c767f55b35f2a321729fd5653ecc492100b64972513593d625419328dd5a00a868141f785172588c41756664264611873b310ffb03fffdcffffd17ffff344c40913731aa800004cbdf97fffff7fffe667fffcff7375befdffcc5537fdbb97d1acbca33a1884b97d2245943747a6ee68bf9046ccf153a0920c66a27923c2a6e1692cd308f6482c1933891d82cd5e9bcc63cf7ffc0abc8bfffffffffffff344c40e131312b000004ab9f44a25de966d377aa21288b664244f288888b28c1a384c0e1b89888e3098329dd08c348e0e3c54e6181e11161e41a2434503c61630c3a2948343a3d105436ad44a2e02180602c0ac82a83fd470d704e14a85e4fff344c414146a76b0007a84b87a02e291e22837887963c400882031c8058273ea7dd27dfbefebfff90dfffff4a53e5ab7ffa3f318cbe652cc6746e5dea560c3e7a54354895c5437c1ac8de65d8e085c1919dee82b5572eec3ab2ceaf12ed34cfff344c41514c186ac00c60694a020f401254d461cb2de37d3cdd0b58afdf9a04e40501e7c2d76373bf9d3dac72d525290bcdcff39f1294080009d6ba1d723288ada3c7aee103c2caffd08e7ea842e97b72cd9032ab78f4a8387a963de0e1b1dfff344c41511b0beb400c6304d973e82211e06dad3c8844c6da5f4eca0b66bf6e4dbd8e35afc272d7e74999a1d7f37f5816c0425c2a7c9b10dcf6470682235d7c2ec1c8e7e003b16750d69a575fc61b2ebb16c5c34660205ed4ab308ca8e120bfff344c4211220feb4007e1e70002dc0620e2468624e710b6ab948c978af2243a437f6ae6fbd6e9abead4cda1b98efaafa87b4f1656c9805d348b407ba6ca143a8748a50b2c2a666d1f5b014da14f585188b4f2d68a8c4066b405e667990d833fff344c42b1220eab000861e7089e213170acbfc2b2259bd4724f0d5f0abe0413a2ae53ffa5292e0528125e7708b1307b51029cda47dc2af66b298b0aed48c8191953411291af30b99004e3a13c0094c17502863815971869706e0b0c4564786fff344c4351228daac00c61e70b85855463fcb19e31f505b9cb91a7fffd6c77f5ac7542217b4debc0883075285e619283dc031a2492a7834b6ab579274ad776ba37a9ada656d3a32cfa2067872a6448c9f27cbd8fc26c762253c8a390f96c8d1fff344c43f12a8d6ac00c65e70a6b20ab7fffea343dff3e095ffb810c9fc54689a73d0401c8c5e381670e206c69a8c869a208d34b5174342c64ade559626442a54f40512e548910c981217a515a1e97380d0a45952d2ec1e53bffff4288fae0bfff344c44711c8d2a800c670702affc90af2d20c2dea67781d786a52112568b571befcddfa2ca0879ad62f0cedc8222bdaee546b8fba74c56b22fadd988e0d1a0c7e12d2197fe9e070e810205cf97fffd82134cef2ef062ae6a0d6e9ba884e96fff344c4521250d6b000c63070da8048adaa50f4babe5c7a4bfdda3f3da3dae948a67d1564beb7b4ea3755ad2a880290aaa209c024439c1de2646c1340bea6e392663c1945ccc1ab7d3d6fb6ae9505b181a1b3999b2993653565fa967c11d9d5fff344c45b1689cab400c31a95d6f49f386242382fadd06bf9c10bc68e309088a540848e14630ec69e43ef8954e1cd3d59b3590956323b16878ca5a3f6f4ba81e42467c38de249abe2f7dc7bdebbf0714adfc93d203e70ab639d205a262d13eafff344c4531679d2b400c15e95d1310a26170048ffd9105fc201526e96031b1a3734b6a45597c83b38c2f6106728ff4f8be57dae6899283ab16181d8aa6048fd3138162de8f4101c3c3f52ca83c5984f5f772f32adbd76ff68f656de35dcd698fff344c44c1691deb000ca1899c377cd18611cf42e66476f67b576aaffae1c191d42a84a374ad545babff0809a7bc29aa0232090bd49985e867df4ca4fedd49fb717d2c447a2a0f120a8b82042150096a854610b244c2c8528c6a7294ed9c6fdfff344c44414d1a6ac00c15295494458f8226c931022a65e2c4154feaaadc05c09c60f283781121813e0f7c9f718e4103869a669979b87fc5fbbe7f77c3ff7f2ea9459d9f2666a2421f248f87ca8b48761a96af0e6d731baf977d44edaa45b6cfff344c4431269a6b0015258004436c20d63d5551c007408960d1ad1aaa868431a09e66f41a530645386110505a65f85d18cb779d2c23f4b6a1885ad435a7ab8572b9ebb6a966839d6a5ac68ca6bc4c3d73dfafede9d0508c21d619a933480a0fff344c44c21122280019a7800498e8202b959b2ba2aa8b1162728b16b3a189fb5a6cd589de27ae3dacdc9243a57b17f816837adfdbc0c7b6bebfada7ffffffa622c125d45954513a4b0c105283750802780010e91840aa13247294828f48f49fff344c41a12098e4801cf2801a4aadc156ab55acaf448c6ca56cc6ffe5294b294a56529656431943a00879e52888743a1d34a56ffcc61236105052a8c6c3274b69cee8b0341b141f2c461e9a0ec4930ec420a6ced21320e14d0c41af434325bfff344c4241058c1fc0062464c822619201d11b8e0ad44269a82cda1ef7fb74ab4a5df4b52b5c559fffd152ea2e24252aa6348b92895d942204834f42cd2b92449551248a060a49e71b7d57aaad9392a582b834f82c1d054ec8e543654efac15fff344c4351140c1e4007a4c4c82a77f582bf5c1977ea3dfcb3cb7d41d38ca7ff346dd050100c2601030c2e2b0b8ad1d2347b0cb9ea82b27d505008060c170001888ac1315a3d511b7481065cfce7be10842e10f39facf0865cd1a39d236f60afff344c44300c0040000e800002083803ffe2191c3da0f6800460efff33a3ff3c72c66c2d000e10de235038334dc06905fe5f26c8981b890075ac01d23de9a69a20704e866c7601952ff7ec0664980a1b0240067c52fffe1b60ec2f882825019fff344c49300000348000000008feff62147206604ee41dc8bffffe39e44c6418dc8a1386897fffff970b845d3ffffffc5d5b8b8b3404dc144404a58bbdb9cb54c92599f3944456e05684e84e22610b5d2126e0661734c745e8400a72a73281bfff344c4e614c1618801494801dacd61f4ac522a3951ba343944aa798a388de43b2b6952c5b3470ba9729b0a086aaddc72cacc72fd5bd45310cc865b8e367716c28b2c31c28e57ab3acee7d9ad95cffcbf3b7a9fdff7fbcb16efdddb4837d465fff344c4e6191252980193a0001eb5ffface9299e3fadaf99fe65127395a6cee322836256f729692b68a4d6921f8b21c9024932be46d7b7c164e369c2f39c249bff45feeb395b4eb8689483c3b9ecca824d1280ea5ff385d802a6f8766e01414fff344c4d420a1d6b80198c800516d906b83d640bc2850249fa965d1e96f626765964f7489e6a27003874cb8c42edd2a3c3f9afd0ae7c553cf46426f2bf2de736844d84cfce6f925deb1dd39f82b81d6c5ac184cafdd5a6eb217e65bffa7d9fafff344c4a411612ec401d85800bbcfe4d371717eaabf2a4d928c0f08e1ed8722c2c7134cd736b4b17b5cd3126c35d120a9612c911f2c78af0eaa2d09b5cab0334c236c460e246ac46682845a31c135e48204d25eabf1fa5d92609e9932914403fff344c4b111619ebc007c1494700bc2022ccbf004c18a30650976a36ef24c9720072c65bbf47f001f836c86a2e1a2fbffc39e3e0f013c05604a04cfabfff25cdc2a022039e501d83001b9fffffe66a2794cb8b73451892e4bffffffff8ec180fff344c4be11094ab0015840000be126ec3bc7989992e6e6eb37894425228268aa94ee090e5b81a9c75ca5aa184624e44096ac554438e2f20a5eb1715bff359ce1d75aa4172bed9ddedae1388b1cbf1bbee9accf7f1d8f34f197e7b564a44687fff344c4cc21832a6c019b68000871efba4263b4f88f2b7a8e9845c0717274d767dbb4da7b8830614267ccbb9f10a243819a52fbcd27c35daface338f5fbffffffffda1cb4bfada32a31f8f79f7ed5fe6a9a24d687d766d1ac1da2f30f8410ebfff344c49921ca96b001997801fb6351160c1c6457bbdcb130e170f75c1c6cfbd790b1c37fa95162c0fe0c6b4227ccf1f3aeba51ef3fb6ac40a6b2c29557bf875993d2b3c7df8358f7be6b6d7ce6ac4ea1e74a58ef339cd757a5f39927a06016fff344c4641b59caac01d97800167183ceffffffffc0aaf50fb06d4499a5906e880c8104d5ad042121a84a6f10ae37818eabd76d6842e1ef29d425bb31f323132c2d71a352ea9366c55588eaa0934ec02b0d0b32e4a46460418c15144bd14c8afff344c44912b18ab80093c694aaa6f7602360f9d4c5f2b1740a90281b957b59e1110416c7e31d475acaab111a41dc2b670cc422f9f3f2ca9a939ffa0f1d3a18e9f5ffacc6722314a2238f5a1504dce34440a204bbe5a9cd6ee41a9842e3a29afff344c45111c192a8009e0a95ab4d2575814d9bce7755e1942a4097fef5296b235d6a739fabb9300bd87e1baf2f9cd7fd468a7d857f1bf43fe241dbea51cfe1d0536c7e9fd0fffffffffe8ad6b1944b2307b7835f2e98b3eb0ca0daab2d160bfff344c45d1221829c00d60a94f7b97198a2ab6bac3b3286590e87ee71f74aceb4f38ad4cd5dbb0051770197cb0a809076f2a127435d58b3e484cd151fbc92cfdb338abd685b04dc8e043e969b96e40b1e47f8990f0260e275ca16450a340891fff344c46710f8be900159600055e7121cb3c82b0fde3eefce152859ead79b5febf258b2c505c6a61e8689a7ffaff383c070161668b9e590304a0d438473e3fffffd8508a8dcf1718610c48c16252c991534affffffff1a7878a7888f17f7dfdfff344c4761f4b0aa401984001956305896096392acff95f3dc3a95cfa2a2c59cc23a99a117c16c482a9f2e0b613853b84a28281e0f34b2c5c788c4da9623854e7337adffafffbff9bbf99b7885121a29984cc2686aea3865edfeaf234130dd7fff344c44b11814ec001cf400058a143ecb55203d6b38db792e1faead72ee5f553ab40462ab7986933a9f46cc8a78b6f71c49166eabf7fe2ffa1fa286751000282dc9e1fc590cc20714c1023b947b77e8afeff56003632dca91f747666f4f4f4fff344c4581221a2c00083c694e0e01ad7cca3bc1fae3bf0cbcd3fecd36ff6d3aafacaec912ab7aa2959b397cdebeadc83f40e0062ba880b76152e82256c4af10d6fabd5cf3a06180cd8d3d41c1ac3bbe9c22e68d14d0a9dfdbcfd57edd765f2fff344c462117996b800c3ca94a6eeecd016743c625aa3d23350e38550211f131a822adfff3bccf3a6aa0f8b9af5fa7cef535aa46598b7d00a890c21c22de9b145614039a0916643059a8c1c6721b02818220f18300a9769045a95395647bd18fff344c46f11b9aea001595000424620c280ada227b1376dd9b1617e506a8883af1c92d1f4c2865b59f3f6f57393dcbd21fc078a1f58ef4f4ccfde66764b0be4c0be3b9bfb698413cb46ec3fcf2679df8a797d1f000c4ff777731ad815617fdbfff344c47b1f39ca7c019c6000e8d18bc0f385a483043c7fa0ab080069a679d3d2340b31b6dce9b1d410c501a0a1163ee9aa9c37334d6d01d04b4153c1a131ac330463a92cbb4ea1268cd110248cf0ee69ab0bcb0759207d2e04669d1e607e11fff344c4512182c2b0019858008e361d0d520603ac9868c3f7d8dc351590a852527976b639e7677ef3fcdbebedac96d46d9ffffff7db29f2f7d339baf749b38988a9de3aa61530536ae5a4d24640f408b3ad7e8586a2d5ae66cc934ecf35ee78fff344c41e12418aa801d86800f2d618c73209a43180bb29523306d949059342b479be4d47a46cf524b6ffabefd4bf649ee566c9c577ca8fdef2a5503079a85472d43e6a483b59f66014033bfd1740296fee192dfaf8426faaed1012ade69287fff344c4281241c6a400d3d495625f5034480151aa79196453cf2dd9bd39efff53b986ec84a2d3ae4dede6133db2e658df1cc8b7b3c674b054b97959c480485af100018e9f322a12dbf22a16de94d550d11cc52a432ab23518d64e0d666b8516fff344c43211a10aa400c21270a79f14c76fa83a321ca140071b7d4bfffbbff29e851af28088cec223a897f258efd5d4eda7fb4a605f3ba3d73c94ff7aecfebfa7fbcc4b21253eaef729d1893a3124ba3ccdc8c1dc9915e08a80dd4e2d6a8421fff344c43e137b0eb4002884b908463b94f06ea7244116f20772b885802effe7f29084454711fffb5fff3ffffbb22ffb7feda1eb30963259fdfb29119508e80a72216960877040220cac8519ca112c3488553329d43143988110c31905b99420fff344c44311f316b8000844b9003472c79efafffff8516bffe85f97fffff2ffffed7feada37356bfc992a75716311cc43b8c132a905455040144c3eac8eaaea281e21c2c69c3a3105c699460b14463ea1e2a875da2ec39227fffffffffffffffff344c44e11731eb400084abcfffffffffd5a56f95feffd4a52ca5694add444551ca550ea0783ce86120e8a890b143a2b50180c3c443a2a81e314a56432b0934a52950cafd03c6120083ad5e93b330d3264fc44299f4c9839067f0482c44e8dfff344c45b119b229c01412800bfe3ac77978ec3636366ffe68c40d0e6e54a4117ffde6e7cd0d18e364a1cefffefde6e7c986e3b5151a916ffffe6870dcfd9b8ef3e6f2f350f8682006f00a25ffffffb984c93e718cb6513c820fa4088138685fff344c46721b32aa0018c580003bc9c7d06ffffffffbf4cfd9f8a395ecaf04e1d83d998d46c7876a66a7d28455eb09805c916278a2530108734912f386201b0b73c4f97159c2a68afe377e8db08e4994c158bef9fe7f757aa1421387c282c57fff344c4331141ceb801d138014576d0c6e8dc71cea0fcc352aba958a36dfaff190700f3c6ee4c0525d5be009328e3404f4a2d4c799aea316d6925ad345aa2f9aa3e200290f686837100eb965f97dbd17e4f416682078a930c7e748a1076c882fff344c44111a14ec0007b5095884c369e49c5ea069c1a72ce1b84083cbd65305a4d4a0dc0da7a6531eb90c3faca6dcef9c4b79b936a5829935b47c7796ff3fbcf9dfc836f92c7f103f95b7e62fdf677e933b5d9e3bfaae8165dc5c0b5aaffdcfff344c44d1289bab800941694c1032559ea695f49bb8575b08ede1bc9346f0d4e8746cc0609fe295c7fe96f4a66bf362339cc3d2d726af9f6bf67cc8c4524ecc0393dd383878a84e616c74ea169ea65c52affc30036d176a0cc2c316c6e0c13fff344c455119152c000c3cc948685f3603d6abe2216aec17ce6b685e9a52ab62a75b773ab5a669a6dc452fcd37fb98ed6f9edf258b05c473526d52545fdb769d5b128ff8d10cabc013c4f54ec0e418d189f95e3d28496f451ba81258a55e4b9fff344c46111015ebc007b0c94c6d6f12e9789670ad9aa99c5fd77aa4b82781c5e6ed94b72fadf9d0a42b0c04034306527569fa377ddeadbff00a04b60982133411858c589ac4884f22b597f274fd4665aa58d0c52ab158e99946119f6ce4555fff344c470120152b8007bc6950f48b02648597346711980342a8377bd34ac1e44f5bff9ebb7ddeadeeed59c483acc87ff70bcf5b3f1f622008aa5f262e381b1d3027d890506984c79193a4655272a4670ec430b1f1f934b41687bccf97f61f2fff344c47b1148d6bc007bd271f6effddbd94d24a6bccb1265fc753599afbf152a113d4a824dd2c091145965bf4634bb0c9a02d1410a0bc12461c5b815ca80d42d829631c07201402609c07a304b53bd3680b0e416843c4a0f900e674356558cfff344c48811d14ac4014c4801d12b61861762908b47d667d49e0eadfff05ce23b60890d425c74cb8fb81253fc271513a722b3b8c8ae7919dfdd778b6a7fbffa7d9e551c7877f1f9d77fe09a152121b20d78c5e1d2a54969c9805ac40e618982fff344c4931fd1e6a4019878008715575dcd08b7a1050990c909117e0f41cc27c0e608709a1744113c2083e0fc216779caad55a210d4f28a293717c2c0702c1b2660e550ab6f78b0f4dcaf7f79ababde0e1f3e8c7313adbe9e13c7065897dc08fff344c46621e9e29c01987800a729a2d4f9f6b67eb748cb1f35bbc80c6ff7ad6bccf612b98ab162e1a2fe2537e0d2a5a500361b1c19161e543172d44a5ed3975dfd3fb0ec130f4b9de8d44afd69fdd35942cc2d650e91c3d458603c3098618cfff344c43112499e9801d84001cd71ffffff3335d5c7922a2ab5fcfaf3f7ff33d9e7602af2d580d9398560d1c423c9c8ba6152c9ce1600448d55992baefc52f23f6fb4bf313f2ebd5d65ed4b3904c8008061c060c08a31c9f4fe8ce8aec42ba7fff344c43a11a1b284015a2801ffffd54d0894303ae31f2a75c53113934a1f32a633c36e141c32e7b26853140a3a21d3b43712511087243b70168b481261b5d2410d8d5106c703238e68a1852c689b9f1b24d8e411451b2913c35ca84e2c4fe0fff344c44620d22e60019b88000702e18425268d06788724ef659828c8f9e22a458b4b74968cf7b476958f22fa9dbf3a444f2fd0382b73e448d5d2a972ea6ab3569adbf5d89b323fab05550a952e9d154bfeba60084d5ff752178d5ff0562302fff344c4151721ea7c01984000c104054057f0bf4fc92dfe582f0443b1f6494713ff9f08f75aaca8afff8a19962e787850b0b4d14bfffd58a0280f06078607e2a2bca942d7ffffe7e5013adfe223df9007cd7e56b50309009aa05c2b668e2dd4fff344c40b11012a0401c618006b49c925245a4d966a686197b9628aeb4054bc98fe1faaf3abc68182902a0a82a742848eca8a34f02cbbf919ee46d5074aab9a7c97c8ff5c1a050c021280511848e2d02424c59115048d81402087010630a0a1fff344c41a10e08dc80031864c071303e12029105888b0159fb40a44266424f48548848d1e1e44615246bfff8488ba909a7f8a3ffff8c5618718718718714245091424509142959595959959595959959582860ac1828606081820608103040cfff344c4291b1921d40018c671103040c10206081820e8e1218181818006060606018181818006062c915555554596596440c0c0c0a121212120c0c0c0c0a1212569255555574d34d35555555d2060606a0b2cb14303068fd8a081834bf14103fff344c40f10e9fd940018c698a9fcb2d2fe597ffeccffff36b3ff66b3feb359ff581597e2820552fc5040aa5eca08159f650505a2c2c2b58a8a36a16662acfff8b54c414d45332e3130305555555555555555555555555555554c414d45332efff344c41e000003480000000031303055555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555554c414d45332efff344c471000003480000000031303055555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555554c414d45332efff344c4ac000003480000000031303055555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555554c414d45332efff344c4ac000003480000000031303055555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555554c414d45332efff344c4ac000003480000000031303055555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555553b3269073819c05b03fff344c4ac0000034800000000388792f76a43a0b99a687abd5ec62b1593923193468dba402b04c364faa0a050180da39facf73843fe888e71000000110febbbbbeeeee7a239e8811111dfff44773e22168810421cbb50261fe08063ca020083fff344c4ac0000034800000000c40185838083b8801039c4e1f5829d2da4145b47a4849d2e30585962c8ae0481204852cac2a0b0242a264518ff294a5ea489122452d8a244880853757d5557fffffacccccccccbf1999b8cccdd50a0d035feb0fff344c4af00c0040000e8000054153bea0681a7f1282a1dc4a0b0352ae82a0aaa4c414d45332e313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4c414d45332efff344c4ff1961c5d8007a4694313030aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafff344c4ec16498ddc007a4694aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaafff344c4ac0000034800000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ No newline at end of file diff --git a/tests/s2t.test.js b/tests/s2t.test.js index 222164d..9422e5a 100644 --- a/tests/s2t.test.js +++ b/tests/s2t.test.js @@ -6,6 +6,8 @@ @Description: Unit testing for Speech-to-Text interface. @Changelog: + 3/29/2022 JT:Added more tests + 3/29/2022 JT:Changed tests to wav format 3/21/2022 JT:Added await to some tests 3/20/2022 JT:Changed tests to opus format 3/16/2022 JT:Added audio tests @@ -23,18 +25,36 @@ test('S2T constructor', () => { test('S2T "Hey Discord bot"', async () => { const speech2text = new S2T(); - const result = await speech2text.transcribe("./tests/resources/s2tTestAudio1.opus"); - expect(result).toBe("hey Discord bot"); + const result = await speech2text.transcribe("./tests/resources/s2tTestAudio1.wav"); + expect(result).toEqual("hey Discord bot"); }); test('S2T "Whats the weather like today"', async () => { const speech2text = new S2T(); - const result = await speech2text.transcribe("./tests/resources/s2tTestAudio2.opus"); - expect(result).toBe("what's the weather like today"); + const result = await speech2text.transcribe("./tests/resources/s2tTestAudio2.wav"); + expect(result).toEqual("what's the weather like today"); }); test('S2T "What time is the basketball game today"', async () => { const speech2text = new S2T(); - const result = await speech2text.transcribe("./tests/resources/s2tTestAudio3.opus"); - expect(result).toBe("what time is the basketball game today"); + const result = await speech2text.transcribe("./tests/resources/s2tTestAudio3.wav"); + expect(result).toEqual("what time is the basketball game today"); +}); + +test('S2T "What restaurants are near me"', async () => { + const speech2text = new S2T(); + const result = await speech2text.transcribe("./tests/resources/s2tTestAudio4.wav"); + expect(result).toEqual("what restaurants are near me"); +}); + +test('S2T "Hey Discord bot, play some music"', async () => { + const speech2text = new S2T(); + const result = await speech2text.transcribe("./tests/resources/s2tTestAudio5.wav"); + expect(result).toEqual("hey Discord bot play some music"); +}); + +test('S2T "Hey Discord bot, are there any sporting events near me today"', async () => { + const speech2text = new S2T(); + const result = await speech2text.transcribe("./tests/resources/s2tTestAudio6.wav"); + expect(result).toEqual("hey Discord bot are there any sporting events near me today"); }); \ No newline at end of file diff --git a/tests/t2s.test.js b/tests/t2s.test.js index ef385bd..86020e4 100644 --- a/tests/t2s.test.js +++ b/tests/t2s.test.js @@ -6,6 +6,7 @@ @Description: Unit testing for Text-to-Speech interface. @Changelog: + 3/29/2022 NM:Added test for empty text and female audio content 3/17/2022 NM:Added tests for matching and not matching audio 2/19/2022 IS:Added constructor test */ @@ -13,31 +14,61 @@ const T2S = require('../src/t2s'); const fs = require('fs'); // eslint-disable-next-line no-undef jest.useFakeTimers('legacy'); +let text2Speech = undefined; +// eslint-disable-next-line no-undef +beforeAll(() => { + text2Speech = new T2S(); +}); + +// eslint-disable-next-line no-undef +test('T2S constructor', () => { + // eslint-disable-next-line no-undef + expect(text2Speech).toBeDefined(); +}); // eslint-disable-next-line no-undef -test('Synthesize speech match', async () => { - const text2Speech = new T2S(); +test('Sythesize speech empty text', async () => { + await text2Speech.convert(''); + const testString = text2Speech.lastAudioContent; + // The buffer should be empty + // eslint-disable-next-line no-undef + expect(testString.length).toBe(0); +}); + +// eslint-disable-next-line no-undef +test('Synthesize speech match neutral voice', async () => { await text2Speech.convert('Sorry, I don\'t understand.'); - const resBuffer = fs.readFileSync('tests/resources/testAudioContent1.txt'); - const testBuffer = text2Speech.lastAudioContent; + // These files contain the two possible audio contents of 'Sorry, I don't understand.' + // that Google may return (being spoken in a neutral voice). + const resString1 = fs.readFileSync('tests/resources/testAudioContent1.txt').toString(); + const resString2 = fs.readFileSync('tests/resources/testAudioContent2.txt').toString(); + const testString = text2Speech.lastAudioContent; + // The audio content from google should match one of the known correct audio contents // eslint-disable-next-line no-undef - expect(Buffer.compare(resBuffer, testBuffer) == 0).toBe(true); + expect(resString1.localeCompare(testString) === 0 || resString2.localeCompare(testString) === 0).toBe(true); }); // eslint-disable-next-line no-undef -test('Synthesize speech no match', async () => { - const text2Speech = new T2S(); +test('Synthesize speech no match neutral voice', async () => { await text2Speech.convert('Message sent.'); - const resBuffer = fs.readFileSync('tests/resources/testAudioContent1.txt'); - const testBuffer = text2Speech.lastAudioContent; + // These files contain the two possible audio contents of 'Sorry, I don't understand.' + // that Google may return (being spoken in a neutral voice). + const resString1 = fs.readFileSync('tests/resources/testAudioContent1.txt').toString(); + const resString2 = fs.readFileSync('tests/resources/testAudioContent2.txt').toString(); + const testString = text2Speech.lastAudioContent; + // The audio contents should not match (Convert was passed a different string) // eslint-disable-next-line no-undef - expect(Buffer.compare(resBuffer, testBuffer) == 0).toBe(false); + expect(resString1.localeCompare(testString) === 0 || resString2.localeCompare(testString) === 0).toBe(false); }); // eslint-disable-next-line no-undef -test('T2S constructor', () => { - const text2speech = new T2S(); +test('Synthesize speech no match female voice', async () => { + await text2Speech.convert('Sorry, I don\'t understand.'); + // This file contains audio content of 'Sorry, I don't understand.' (being spoken in a female voice) + const resString = fs.readFileSync('tests/resources/testAudioContent3.txt').toString(); + const testString = text2Speech.lastAudioContent; + // The audio contents should not match (Convert uses neutral voice) // eslint-disable-next-line no-undef - expect(text2speech).toBeDefined(); + expect(resString.localeCompare(testString) === 0).toBe(false); }); \ No newline at end of file