How to make Text to speech queue Node.js - javascript

This might be a very simple thing to do for the pros like some of you, I hope you can help me, I will really appreciate your time, thanks.
I have this TTS discord bot, and it works! But I can't figure out how to queue extra incoming TTS requests.
When current TTS is playing and a new request is submitted, current TTS will stop and start executing next request without letting the current TTS finish.
What I want to do is queue all requests so every single one plays after each finishes.
Some one told me to use this package but I just can't figure it out.
I'm a noob with very limited knowledge, so can someone please add the extra lines that are needed for queues? Or provide a good guide?
I'm sorry for being too picky I know I shouldn't ask for too much, but I've been dealing with this issue for weeks now and I'm desperate.
Here is my code:
const { getAudioUrl } = require('google-tts-api');
module.exports = {
name: 'say',
aliases: ['s'],
cooldown: 3,
description: "tts",
execute: async (message, args, cmd, client, Discord) => {
console.log('Say command executed');
if (!args[0])
return message.channel.send('you gotta include a message!');
const string = args.join(' ');
if (string.length > 200)
return message.channel.send('the message cant be more than 200 letters!');
const voiceChannel = message.member.voice.channel;
if (!voiceChannel)
return message.reply('You have to be in a voice channel to send a message!');
const audioURL = getAudioUrl(string, {
lang: 'en',
slow: false,
host: 'https://translate.google.com',
timeout: 10000,
});
try {
message.channel.startTyping();
setTimeout(function () {
message.channel.send('Speaking your msg...');
message.channel.stopTyping();
console.log('Now starting to talk');
}, 1000);
voiceChannel.join().then(connection => {
const dispatcher = connection.play(audioURL);
dispatcher.on('finish', () => {
console.log('Done talking');
});
});
}
catch (e) {
message.channel.send('Bot error, please try again or try later');
console.error(e);
}
setTimeout(function () {
voiceChannel.leave();
}, 240000);
}
}

With queue package you "push" your execute logic wrapped in a function. The function will be then executed by the queue. The tricky part here is that you need to end the execution inside the dispatcher's finish event handler. You can introduce a new promise which will be then resolved from inside the event handler.
Here's a rough sketch of the solution:
const { getAudioUrl } = require('google-tts-api');
const queue = require('queue');
// Create the queue where we will push our jobs
const q = queue({ concurrency: 1, autostart: true });
module.exports = {
name: 'say',
aliases: ['s'],
cooldown: 3,
description: 'tts',
execute: (message, args, cmd, client, Discord) => {
// Enqueue task
q.push(async () => {
console.log('Say command executed');
if (!args[0]) {
return message.channel.send('you gotta include a message!');
}
const string = args.join(' ');
if (string.length > 200) {
return message.channel.send(
'the message cant be more than 200 letters!'
);
}
const voiceChannel = message.member.voice.channel;
if (!voiceChannel) {
return message.reply(
'You have to be in a voice channel to send a message!'
);
}
const audioURL = getAudioUrl(string, {
lang: 'en',
slow: false,
host: 'https://translate.google.com',
timeout: 10000,
});
try {
message.channel.startTyping();
setTimeout(function () {
message.channel.send('Speaking your msg...');
message.channel.stopTyping();
console.log('Now starting to talk');
}, 1000);
const connection = await voiceChannel.join();
const dispatcher = connection.play(audioURL);
// Here we need to handle the promise resolution from the nested event handler
return Promise.new((resolve, reject) => {
dispatcher.on('finish', () => {
console.log('Done talking');
// voiceChannel.leave();
resolve();
});
dispatcher.on('error', (err) => {
console.log('Some error in dispatcher happenned!', err);
reject(err);
});
});
} catch (e) {
message.channel.send('Bot error, please try again or try later');
console.error(e);
}
// This code won't be called
setTimeout(function () {
voiceChannel.leave();
}, 240000);
});
},
};
Take this solution with a grain of salt. I didn't figure out what version of Discord.js you are targeting. Also note that timeouts and possible other error conditions are not handled.

Related

jest timeouts when calling a database query

I have a test file like this.
const { silBastan } = require("../database.js");
const axios = require('axios').default;
describe("authentication", () => {
describe("when data schema is valid", () => {
test("returns 201 response code if the user doesnt already exists", async () => {
await silBastan();
const response = await axios.post('http://localhost:8000/auth/register', {
email: "my_email",
password: "1234"
});
expect(response.status).toBe(201);
});
});
});
And silBastan is defined here like this
const pg = require("pg");
const client = new pg.Client();
async function silBastan() {
return await client.query(`DELETE FROM account`);
}
Of course i made sure the server started and connected to the database before running the tests.
I wondered if there is something wrong with silBastan and tested it inside a express route handler like this
router.post('/register', async (req, res) => {
const { email, password } = req.body;
await silBastan();
try {
await db.createAccount(email, password);
res.sendStatus(201);
} catch (e) {
res.status(400).json({ err: "Already exists" });
}
});
and there was no timeout. And after this i returned another promise from silBastan like this:
async function silBastan() {
// return await client.query(`DELETE FROM account`);
return new Promise((resolve) => setTimeout(() => resolve(), 1000));
}
And again there is no timeout. I tried couple of other variations as well like these:
function silBastan() {
return client.query(`DELETE FROM account`);
}
async function silBastan() {
await client.query(`DELETE FROM account`);
}
Nothing worked i always get this message:
thrown: "Exceeded timeout of 5000 ms for a test.
Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."
I don't think the problem is with the function because i would get the same behavior in the route handler too.

Adding a global timer for tmi.js integrated discord.js

I'm trying to do a discord bot that listens to multiple twitch chat for commands, then run them on discord, using both tmi.js and discord.js. Currently it is working, but I can't seem to add a global cooldown on the commands itself to prevent spamming. At first I've tried adding a cd timer to each command, but I'm unable to get it working, hence the decision to try to make a global cd but still to no avail. Am I doing something wrongly?
twitch.on('message', (channel, tags, message, self) => {
if(!message.startsWith(prefix) || self) return;
const args = (message.slice(prefix.length).trim().split(/ +/));
const commandName = args.shift().toLowerCase();
if (!twitch.commands.has(commandName)) return;
const command = twitch.commands.get(commandName);
}
try {
command.execute(bot, botChannel, vcChannel, isReady);
} catch (error){
console.error(error);
}
});
just to update, I basically took a leaflet from async await functions here: https://stackoverflow.com/a/54772517/14637034
Then, I modified the code as such:
const setAsyncTimeout = (cb, timeout = 0) => new Promise(resolve => {
setTimeout(() => {
cb();
resolve();
}, timeout);
});
const doStuffAsync = async () => {
await setAsyncTimeout(() => {
isReady = true;
console.log(isReady);
}, 10000);};
twitch.on('message', (channel, tags, message, self) => {
if(!message.startsWith(prefix) || self) return;
const args = (message.slice(prefix.length).trim().split(/ +/));
const commandName = args.shift().toLowerCase();
if (!twitch.commands.has(commandName)) return;
if (isReady){
const command = twitch.commands.get(commandName);
isReady = false;
try {
command.execute(bot, botChannel, vcChannel);
} catch (error){
console.error(error);
}
doStuffAsync();
}
});
Seemed to work for now, as 10s is a long enough time for bot to leave discord properly without causing timeout. I'm still open to better suggestions for optimization though!

How to run async functions sequentially on a React-Native app, using Expo

UPDATE
Combining both solutions below, I wrote:
const startMusic = async () => {
let currentSong
let songPath
const songArray = [
{ path: require("../assets/sounds/Katsu.mp3"), song: mainTheme },
{ path: require("../assets/sounds/MainTheme2.mp3"), song: mainTheme2 },
{ path: require("../assets/sounds/MainTheme3.mp3"), song: mainTheme3 },
]
for (var i = 0; i < songArray.length; i++) {
currentSong = songArray[i].song
songPath = songArray[i].path
try {
await currentSong.loadAsync(songPath)
await currentSong.playAsync()
// setSoundObject(currentSong)
console.log("Music will start")
return new Promise(resolve => {
currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log("Just finished playing")
resolve()
}
})
})
} catch (error) {
console.log(`Error: ${error}`)
return
}
}
}
This actually plays the song, and the console logs occur on time ("Just finished playing" happens exactly when the song ends)
I'm trying to figure out how to play the next song.. and how will it know when it has reached the final song?
return new Promise(resolve => {
currentSong.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log("Just finished playing")
resolve()
}
})
}).then(() => console.log("Next song?"))
Figured how where to put the .then to get it to console log right after "Just finished playing" I'm just trying to see how to actually put the next song there
(then of course, telling it when to go back to the first song in the array)
Original Post
Working on an assignment for a react native app using expo-av library for Sound files.
Right now, the app has a startMusic function set in a Context file that is responsible for playing the app's background music. It only has one song for now:
const startMusic = async () => {
try {
await mainTheme.loadAsync(require("../assets/sounds/Katsu.mp3"))
await mainTheme.playAsync()
setSoundObject(mainTheme)
console.log("The first song is playing! Enjoy!")
} catch (error) {
console.log(`Couldnt load main theme: ${error}`)
return
}
}
It is used in the homescreen component's file like so:
const { startMusic } = useContext(MusicContext)
useEffect(() => {
startMusic()
}, [])
For the second song, I wrote another const in the MusicContext file:
const secondSong = async () => {
try {
await mainTheme2.loadAsync(require("../assets/sounds/MainTheme2.mp3"))
await mainTheme2.playAsync()
setSoundObject(mainTheme2)
console.log("Now playing the second track. Enjoy!")
} catch (error) {
console.log(`Could not play the second song: ${error}`)
return
}
}
Annnnnd… here is where my trouble lies. I know this wasn't gonna work but I wrote this in the component file to try to get the second song playing after the first song
useEffect(() => {
startMusic()
.then(secondSong())
}, [])
I know there's more to it than that but I'm having trouble.
Problem with your code is not just running one function after another (that would be as simple as startMusic().then(() => secondSong()) but still won't solve the problem), but the fact that your functions actually don't wait for a song to finish playing before resolving
You expect this line await mainTheme.playAsync() to pause function execution until the song has finished, but what it in fact does according to docs https://docs.expo.io/versions/latest/sdk/av/ is exactly only starting the playback (without waiting for it to finish)
With that being said, you need to determine the moment your playback finishes, then create a Promise that will only resolve after the playback is finished so that your second song can only start after the first
In the simplest form without error handling and such, it can look like this
const startAndWaitForCompletion = async () => {
try {
await mainTheme.loadAsync(require('../assets/sounds/Katsu.mp3'))
await mainTheme.playAsync()
console.log('will start playing soon')
return new Promise((resolve) => {
mainTheme.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log('finished playing')
resolve()
}
}
})
} catch (error) {
console.log('error', error)
}
}
the trick is of course the .setOnPlaybackStatusUpdate listener that will be called every so often with playback status, and by analyzing the status you can tell the song has finished playing. If you scroll to the bottom of the page I linked you will find other examples with status update
updated
const startAndWaitForCompletion = async (playbackObject, file) => {
try {
await playbackObject.loadAsync(file)
await playbackObject.playAsync()
console.log('will start playing soon')
return new Promise((resolve) => {
playbackObject.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log('finished playing')
resolve()
}
}
})
} catch (error) {
console.log('error', error)
}
}
////
const songs = [
{ path: require('../assets/sounds/Katsu.mp3'), song: mainTheme },
{ path: require('../assets/sounds/MainTheme2.mp3'), song: mainTheme2 },
{ path: require('../assets/sounds/MainTheme3.mp3'), song: mainTheme3 },
]
useEffect(() => {
(async () => {
for (let i = 0; i < songs.length; i++) {
await startAndWaitForCompletion(songs[i].song, songs[i].path)
}
})()
}, [])
I think you need to rethink this problem/solution to be more abstract.
Instead of making a new const and promise for every single song you want to play (which, as you said, isn't workable, and isn't scalable, like say if you wanted 10 songs instead of 2), make "startMusic" a function that plays an array of songs (each array index being a filepath to an MP3, like in your example), and
then resolve/reject the promise as needed.
A quick "startMusic" rewrite:
const startMusic(songArray) = async () => {
for (var songIndex in songArray) {
try {
await mainTheme.loadAsync(require(songArray[songIndex]))
await mainTheme.playAsync()
setSoundObject(mainTheme)
console.log("Song #", songIndex, "of ", songArray.length " is playing. Enjoy!")
} catch (error) {
console.log(`Couldnt load song: ${error}`)
return
}
}
}
A "Promise.all" chain could be useful here, too, if the above for-try-catch structure doesn't work: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Not familiar with the expo-av library, so there might be some specific quirks to look out for there, but I think re-writing "startMusic" to be an abstract function that plays an array of "N" songs is a much more optimal approach, and will minimize/eliminate your problems.
.then() accepts a function but you've provided the result of function execution by calling secondSong.
Do:
useEffect(() => {
startMusic()
.then(() => secondSong())
}, [])
Or just get rid of () after secondSong:
useEffect(() => {
startMusic()
.then(secondSong)
}, [])

Stopping an active Testcafe run

I try to add a stop function to the Testcafe run. I start Testcafe with:
let testcafe = null;
let testcafeprom = null;
testcafeprom = createTestCafe('localhost', 1337, 1338)
.then(tc => {
testcafe = tc;
const runner = testcafe.createRunner();
return runner
.src([__basedir + '/tests/temp.js'])
.browsers(myBrowser)
//.browsers('browserstack:Chrome')
.screenshots(__basedir +'/allure/screenshots/', true)
.reporter(['uistatusreporter', {name: 'allure',output: 'test/report.json'}])
.run();
})
.then(failedCount => {
testcafe.close();
startReportGenerator();
capcon.stopCapture(process.stdout);
console.log("Testcafe Ende");
if(failedCount>0){
res.sendStatus(400);
console.log('Tests failed: ' + failedCount);
//res.statusCode = 400; //BadRequest 400
/*
res.json({
success: 'failed',
fails: failedCount
});
*/
}else{
//res.statusCode = 200; //BadRequest 400
res.sendStatus(200);
console.log('All success');
/*
res.json({
success: 'ok',
fails: failedCount
});
*/
}
})
.catch(error => {
testcafe.close();
console.log('Tests failed: Testcafe Error');
console.log(error);
res.sendStatus(401);
});
Then I added a function to stop the run:
router.get('/stopit', async (req, res) => {
testcafeprom.cancel();
res.sendStatus(200);
});
As I understand is that createTestCafe will return a promise and in all to stop the promise I call testcafeprom.cancel(); or testcafeprom.stop();
But the browser is running and running. A simple testcafe.close(); will stop Testcafe complete. But I want to stop it and not shoot it down.
Any suggestion to stop it a better way?
Update:
I have also tested the way to make the runner as promise:
createTestCafe('localhost', 1337, 1338)
.then(tc => {
testcafe = tc;
const runner = testcafe.createRunner();
testcafeprom = runner
.src([__basedir + '/tests/temp.js'])
.browsers(myBrowser)
//.browsers('browserstack:Chrome')
.screenshots(__basedir +'/allure/screenshots/', true)
.reporter(['uistatusreporter', {name: 'allure',output: 'test/report.json'}])
.run();
return testcafeprom;
})
Adding also
await testcafeprom.cancel();
This will have exact the same result as testCafe.close(), means everything is shoot down without any response. Iam confused.
And finally I tried:
let runner = null;
createTestCafe('localhost', 1337, 1338, void 0, true)
.then(testcafe => {
runner = testcafe.createRunner();
})
.then(() => {
return runner
.src([__basedir + '/tests/temp.js'])
.browsers(myBrowser)
//.browsers('browserstack:Chrome')
.screenshots(__basedir +'/allure/screenshots/', true)
.reporter(['uistatusreporter', {name: 'allure',output: 'test/report.json'}])
.run()
.then(failedCount => {
//testcafe.close();
startReportGenerator();
capcon.stopCapture(process.stdout);
console.log(`Finished. Count failed tests:${failedCount}`);
//process.exit(failedCount);
res.sendStatus(200);
});
})
.catch(error => {
startReportGenerator();
capcon.stopCapture(process.stdout);
console.log(error);
//process.exit(1);
res.sendStatus(401);
});
But here is the same. If I call await runner.stop() it looks like that the command will kill the whole process and nothing comes back to the promise.
Is this such a secret how to stop a running TestCafe instance or is the secret that the whole process is shoot down?
It's difficult to say precisely why you face an issue since I cannot debug your project. However, you are correct when you use cancel to stop test execution. The cancel method stops tests execution and closes the browser, but it does not stop TestCafe. This means that you can use the run method again, and it will start test execution and open browsers again.
I created an example to demonstrate that this approach works.
Test code:
fixture `fixture`
.page `http://example.com`;
for (let i = 0; i < 50; i++) {
test(`test A ${i}`, async t => {
await t.click('h1');
});
}
Testcafe start code:
const createTestCafe = require('testcafe');
(async () => {
const testCafe = await createTestCafe();
let timeout;
const runner = testCafe.createRunner();
const runPromise = runner
.src('test.js')
.browsers('chrome')
.run();
const cancelPromise = new Promise(resolve => {
timeout = setTimeout(() => {
runPromise.cancel();
resolve('canceled');
}, 20000);
});
let value = await Promise.race([runPromise, cancelPromise]);
if (value === 'canceled')
console.log('test execution was canceled')
value = await runner.run();
console.log(`${value} failed tests`);
await testCafe.close();
})();

ES7 promises and awaiting async function which loops forever in background

This might be a special case:
I want to read from a queue (AWS SQS), which is done by making a call which waits a few secs for messages, and then resolves - and call again and again in a loop as long as you want to process that queue (it checks a flag every time).
This means that I have a consume function which is running as long as the app is active, or the queue is unflagged.
And I also have a subscribe function used for subscribing to a queue - which is supposed to resolve as soon as it knows that the consumer is able to connect to the queue. Even though this functions calls the consumer which keeps running and does not return until the queue is unflagged.
It gives me some challenges - do you have any tips on how to solve this with modern JS and async/await promises? I keep in mind this code is running in a React web app, not in node.js.
I basically just want the await subscribe(QUEUE) call (which comes from the GUI) to resolve as soon as it's sure that it can read from that queue. But if it cannot, I want it to throw an error which is propagated to the origin of the subscribe call - which means that I have to await consume(QUEUE), right?
Update:
Some untested draft code has been added (I don't want to spend more time making it work if I'm not doing the right approach) - I thought about sending success and failure callback to the consuming function, so it can report a success as soon as it gets the first valid (but possibly empty) response from the queue, which makes it store the queue url as a subscription - and unsubscribe if as the queue poll fails.
Since I'm setting up several queue consumers they should not be blocking anything but just work in the background
let subscribedQueueURLs = []
async function consumeQueue(
url: QueueURL,
success: () => mixed,
failure: (error: Error) => mixed
) {
const sqs = new AWS.SQS()
const params = {
QueueUrl: url,
WaitTimeSeconds: 20,
}
try {
do {
// eslint-disable-next-line no-await-in-loop
const receivedData = await sqs.receiveMessage(params).promise()
if (!subscribedQueueURLs.includes(url)) {
success()
}
// eslint-disable-next-line no-restricted-syntax
for (const message of receivedData.Messages) {
console.log({ message })
// eslint-disable-next-line no-await-in-loop
eventHandler && (await eventHandler.message(message, url))
const deleteParams = {
QueueUrl: url,
ReceiptHandle: message.ReceiptHandle,
}
// eslint-disable-next-line no-await-in-loop
const deleteResult = await sqs.deleteMessage(deleteParams).promise()
console.log({ deleteResult })
}
} while (subscribedQueueURLs.includes(url))
} catch (error) {
failure(error)
}
}
export const subscribe = async (entityType: EntityType, entityId: EntityId) => {
const url = generateQueueURL(entityType, entityId)
consumeQueue(
url,
() => {
subscribedQueueURLs.push(url)
eventHandler && eventHandler.subscribe(url)
},
error => {
console.error(error)
unsubscribe(entityType, entityId)
}
)
}
I ended up solving it like this - maybe not the most elegant solution though...
let eventHandler: ?EventHandler
let awsOptions: ?AWSOptions
let subscribedQueueUrls = []
let sqs = null
let sns = null
export function setup(handler: EventHandler) {
eventHandler = handler
}
export async function login(
{ awsKey, awsSecret, awsRegion }: AWSCredentials,
autoReconnect: boolean
) {
const credentials = new AWS.Credentials(awsKey, awsSecret)
AWS.config.update({ region: awsRegion, credentials })
sqs = new AWS.SQS({ apiVersion: '2012-11-05' })
sns = new AWS.SNS({ apiVersion: '2010-03-31' })
const sts = new AWS.STS({ apiVersion: '2011-06-15' })
const { Account } = await sts.getCallerIdentity().promise()
awsOptions = { accountId: Account, region: awsRegion }
eventHandler && eventHandler.login({ awsRegion, awsKey, awsSecret }, autoReconnect)
}
async function handleQueueMessages(messages, queueUrl) {
if (!sqs) {
throw new Error(
'Attempt to subscribe before SQS client is ready (i.e. authenticated).'
)
}
// eslint-disable-next-line no-restricted-syntax
for (const message of messages) {
if (!eventHandler) {
return
}
// eslint-disable-next-line no-await-in-loop
await eventHandler.message({
content: message,
queueUrl,
timestamp: new Date().toISOString(),
})
const deleteParams = {
QueueUrl: queueUrl,
ReceiptHandle: message.ReceiptHandle,
}
// eslint-disable-next-line no-await-in-loop
await sqs.deleteMessage(deleteParams).promise()
}
}
export async function subscribe(queueUrl: QueueUrl) {
if (!sqs) {
throw new Error(
'Attempt to subscribe before SQS client is ready (i.e. authenticated).'
)
}
const initialParams = {
QueueUrl: queueUrl,
WaitTimeSeconds: 0,
MessageAttributeNames: ['All'],
AttributeNames: ['All'],
}
const longPollParams = {
...initialParams,
WaitTimeSeconds: 20,
}
// Attempt to consume the queue, and handle any pending messages.
const firstResponse = await sqs.receiveMessage(initialParams).promise()
if (!subscribedQueueUrls.includes(queueUrl)) {
subscribedQueueUrls.push(queueUrl)
eventHandler && eventHandler.subscribe(queueUrl)
}
handleQueueMessages(firstResponse.Messages, queueUrl)
// Keep on polling the queue afterwards.
setImmediate(async () => {
if (!sqs) {
throw new Error(
'Attempt to subscribe before SQS client is ready (i.e. authenticated).'
)
}
try {
do {
// eslint-disable-next-line no-await-in-loop
const received = await sqs.receiveMessage(longPollParams).promise()
handleQueueMessages(received.Messages, queueUrl)
} while (sqs && subscribedQueueUrls.includes(queueUrl))
} catch (error) {
eventHandler && eventHandler.disconnect()
throw error
}
})
}

Categories