Comportement souhaité
Comportement réel
Ce que j'ai essayé
Étapes à reproduire
Recherche
Comportement souhaité
Canalisez plusieurs flux lisibles, reçus de plusieurs demandes d'api, vers un seul flux inscriptible.
Les réponses api proviennent de la méthode textToSpeech.synthesize () d'ibm-watson.
La raison pour laquelle plusieurs demandes sont requises est que le service a une limite de 5KB
Sur la saisie de texte.
Par conséquent, une chaîne de 18KB
, Par exemple, nécessite quatre requêtes.
Comportement réel
Le fichier de flux inscriptible est incomplet et tronqué.
L'application semble "se bloquer".
Lorsque j'essaie d'ouvrir le fichier .mp3
Incomplet dans un lecteur audio, il indique qu'il est corrompu.
Le processus d'ouverture et de fermeture du fichier semble augmenter sa taille - comme ouvrir le fichier incite en quelque sorte plus de données à y circuler.
Un comportement indésirable est plus apparent avec des entrées plus grandes, par exemple quatre chaînes de 4000 octets ou moins.
Ce que j'ai essayé
J'ai essayé plusieurs méthodes pour diriger les flux lisibles vers un seul flux inscriptible ou plusieurs flux inscriptibles à l'aide des packages npm combiné-flux , combiné-flux2 , multiflux et archiveur et ils entraînent tous des fichiers incomplets. Ma dernière tentative n'utilise aucun package et est indiquée dans la section Steps To Reproduce
Ci-dessous.
Je remets donc en question chaque partie de ma logique d'application:
01. Quel est le type de réponse d'une demande d'API de texte à discours watson?
Les documents de synthèse vocale , disons que le type de réponse api est:
Response type: NodeJS.ReadableStream|FileObject|Buffer
Je suis confus que le type de réponse est l'une des trois choses possibles.
Dans toutes mes tentatives, j'ai supposé qu'il s'agissait d'un readable stream
.
02. Puis-je faire plusieurs demandes d'api dans une fonction de carte?
03. Puis-je encapsuler chaque requête dans une
promise()
et résoudre leresponse
?04. Puis-je affecter le tableau résultant à une variable
promises
?05. Puis-je déclarer
var audio_files = await Promise.all(promises)
?06. Après cette déclaration, toutes les réponses sont-elles "terminées"?
07. Comment canaliser correctement chaque réponse vers un flux inscriptible?
08. Comment puis-je détecter lorsque tous les canaux sont terminés, afin de pouvoir renvoyer le fichier au client?
Pour les questions 2 à 6, je suppose que la réponse est "OUI".
Je pense que mes échecs concernent les questions 7 et 8.
Étapes à reproduire
Vous pouvez tester ce code avec un tableau de quatre chaînes de texte générées aléatoirement avec une taille d'octet respective de 3975
, 3863
, 3974
Et 3629
Octets - - voici un Pastebin de ce tablea .
// route handler
app.route("/api/:api_version/tts")
.get(api_tts_get);
// route handler middleware
const api_tts_get = async (req, res) => {
var query_parameters = req.query;
var file_name = query_parameters.file_name;
var text_string_array = text_string_array; // eg: https://Pastebin.com/raw/JkK8ehwV
var absolute_path = path.join(__dirname, "/src/temp_audio/", file_name);
var relative_path = path.join("./src/temp_audio/", file_name); // path relative to server root
// for each string in an array, send it to the watson api
var promises = text_string_array.map(text_string => {
return new Promise((resolve, reject) => {
// credentials
var textToSpeech = new TextToSpeechV1({
iam_apikey: iam_apikey,
url: tts_service_url
});
// params
var synthesizeParams = {
text: text_string,
accept: 'audio/mp3',
voice: 'en-US_AllisonV3Voice'
};
// make request
textToSpeech.synthesize(synthesizeParams, (err, audio) => {
if (err) {
console.log("synthesize - an error occurred: ");
return reject(err);
}
resolve(audio);
});
});
});
try {
// wait for all responses
var audio_files = await Promise.all(promises);
var audio_files_length = audio_files.length;
var write_stream = fs.createWriteStream(`${relative_path}.mp3`);
audio_files.forEach((audio, index) => {
// if this is the last value in the array,
// pipe it to write_stream,
// when finished, the readable stream will emit 'end'
// then the .end() method will be called on write_stream
// which will trigger the 'finished' event on the write_stream
if (index == audio_files_length - 1) {
audio.pipe(write_stream);
}
// if not the last value in the array,
// pipe to write_stream and leave open
else {
audio.pipe(write_stream, { end: false });
}
});
write_stream.on('finish', function() {
// download the file (using absolute_path)
res.download(`${absolute_path}.mp3`, (err) => {
if (err) {
console.log(err);
}
// delete the file (using relative_path)
fs.unlink(`${relative_path}.mp3`, (err) => {
if (err) {
console.log(err);
}
});
});
});
} catch (err) {
console.log("there was an error getting tts");
console.log(err);
}
}
exemple officiel montre:
textToSpeech.synthesize(synthesizeParams)
.then(audio => {
audio.pipe(fs.createWriteStream('hello_world.mp3'));
})
.catch(err => {
console.log('error:', err);
});
ce qui semble bien fonctionner pour les demandes uniques, mais pas pour les demandes multiples, pour autant que je sache.
Recherche
concernant les flux lisibles et inscriptibles, les modes de flux lisibles (circulant et en pause), les événements 'data', 'end', 'drain' et 'finish', pipe (), fs.createReadStream () et fs.createWriteStream ( )
Presque toutes les applications Node.js, aussi simples soient-elles, utilisent les flux d'une manière ou d'une autre ...
const server = http.createServer((req, res) => {
// `req` is an http.IncomingMessage, which is a Readable Stream
// `res` is an http.ServerResponse, which is a Writable Stream
let body = '';
// get the data as utf8 strings.
// if an encoding is not set, Buffer objects will be received.
req.setEncoding('utf8');
// readable streams emit 'data' events once a listener is added
req.on('data', (chunk) => {
body += chunk;
});
// the 'end' event indicates that the entire body has been received
req.on('end', () => {
try {
const data = JSON.parse(body);
// write back something interesting to the user:
res.write(typeof data);
res.end();
} catch (er) {
// uh oh! bad json!
res.statusCode = 400;
return res.end(`error: ${er.message}`);
}
});
});
https://nodejs.org/api/stream.html#stream_api_for_stream_consumers
Les flux lisibles ont deux modes principaux qui affectent la façon dont nous pouvons les consommer ... ils peuvent être soit en mode
paused
soit en modeflowing
. Tous les flux lisibles démarrent par défaut en mode pause, mais ils peuvent être facilement basculés versflowing
et revenir àpaused
si nécessaire ... en ajoutant simplement un gestionnaire d'événementsdata
, le flux suspendu en modeflowing
et la suppression du gestionnaire d'événementsdata
remet le flux en modepaused
.
https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be9
Voici une liste des événements et fonctions importants pouvant être utilisés avec des flux lisibles et inscriptibles
Les événements les plus importants sur un flux lisible sont:
L'événement
data
, qui est émis chaque fois que le flux transmet un bloc de données au consommateur L'événementend
, qui est émis lorsqu'il n'y a plus de données à consommer à partir du flux.Les événements les plus importants d'un flux inscriptible sont:
L'événement
drain
, qui indique que le flux inscriptible peut recevoir plus de données. L'événementfinish
, qui est émis lorsque toutes les données ont été vidées sur le système sous-jacent.
https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be9
.pipe()
s'occupe d'écouter les événements 'data' et 'end' defs.createReadStream()
.
https://github.com/substack/stream-handbook#why-you-should-use-streams
.pipe()
est juste une fonction qui prend un flux source lisible src et accroche la sortie à un flux inscriptible de destinationdst
https://github.com/substack/stream-handbook#pipe
La valeur de retour de la méthode
pipe()
est le flux de destination
https://flaviocopes.com/nodejs-streams/#pipe
Par défaut, stream.end () est appelé sur le flux de destination
Writable
lorsque le flux sourceReadable
émet'end'
, De sorte que la destination est n'est plus accessible en écriture. Pour désactiver ce comportement par défaut, l'optionend
peut être passée en tant quefalse
, ce qui rend le flux de destination ouvert:
https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
L'événement
'finish'
Est émis après l'appel de la méthodestream.end()
et toutes les données ont été vidées vers le système sous-jacent.
const writer = getWritableStreamSomehow();
for (let i = 0; i < 100; i++) {
writer.write(`hello, #${i}!\n`);
}
writer.end('This is the end\n');
writer.on('finish', () => {
console.log('All writes are now complete.');
});
https://nodejs.org/api/stream.html#stream_event_finish
Si vous essayez de lire plusieurs fichiers et de les diriger vers un flux inscriptible, vous devez les diriger chacun vers le flux inscriptible et passer
end: false
Lorsque vous le faites, car par défaut, un flux lisible termine l'écriture diffuser lorsqu'il n'y a plus de données à lire. Voici un exemple:
var ws = fs.createWriteStream('output.pdf');
fs.createReadStream('pdf-sample1.pdf').pipe(ws, { end: false });
fs.createReadStream('pdf-sample2.pdf').pipe(ws, { end: false });
fs.createReadStream('pdf-sample3.pdf').pipe(ws);
https://stackoverflow.com/a/30916248
Vous souhaitez ajouter la deuxième lecture dans un écouteur d'événements pour que la première lecture se termine ...
var a = fs.createReadStream('a');
var b = fs.createReadStream('b');
var c = fs.createWriteStream('c');
a.pipe(c, {end:false});
a.on('end', function() {
b.pipe(c)
}
https://stackoverflow.com/a/28033554
Un bref historique de Node Streams - partie n et deux .
Recherche Google associée:
comment diriger plusieurs flux lisibles vers un seul flux inscriptible? nodejs
Questions couvrant le même sujet ou un sujet similaire, sans réponses faisant autorité (ou pouvant être "obsolètes"):
Comment diriger plusieurs ReadableStreams vers un seul WriteStream?
Piping vers le même flux inscriptible deux fois via différents flux lisibles
WebRTC serait une bonne option pour le problème ci-dessus. Parce que votre une fois que votre fichier a été généré, je vais laisser le client écouter.
Voici deux solutions.
Solution 01
Bluebird.mapSeries
Il utilise Bluebird.mapSeries
De BM answer mais au lieu de simplement mapper les réponses, les requêtes et sont traitées dans la fonction de mappage. En outre, il résout les promesses sur l'événement de flux inscriptible finish
, plutôt que sur l'événement de flux lisible end
. Bluebird
est utile en ce qu'il pauses
itération dans une fonction de carte jusqu'à ce qu'une réponse ait été reçue et gérée, puis passe à l'itération suivante.
Étant donné que la fonction de carte Bluebird
produit des fichiers audio propres, plutôt que de compresser les fichiers, vous pourriez utiliser une solution comme celle de Terry Lennox réponse pour combiner plusieurs fichiers audio en un seul fichier audio. Ma première tentative de cette solution, en utilisant Bluebird
et fluent-ffmpeg
, A produit un seul fichier, mais sa qualité était légèrement inférieure - sans doute cela pourrait être modifié dans les paramètres ffmpeg
, mais je n'ai pas eu le temps de faire ça.
// route handler
app.route("/api/:api_version/tts")
.get(api_tts_get);
// route handler middleware
const api_tts_get = async (req, res) => {
var query_parameters = req.query;
var file_name = query_parameters.file_name;
var text_string_array = text_string_array; // eg: https://Pastebin.com/raw/JkK8ehwV
var absolute_path = path.join(__dirname, "/src/temp_audio/", file_name);
var relative_path = path.join("./src/temp_audio/", file_name); // path relative to server root
// set up archiver
var archive = archiver('Zip', {
zlib: { level: 9 } // sets the compression level
});
var Zip_write_stream = fs.createWriteStream(`${relative_path}.Zip`);
archive.pipe(Zip_write_stream);
await Bluebird.mapSeries(text_chunk_array, async function(text_chunk, index) {
// check if last value of array
const isLastIndex = index === text_chunk_array.length - 1;
return new Promise((resolve, reject) => {
var textToSpeech = new TextToSpeechV1({
iam_apikey: iam_apikey,
url: tts_service_url
});
var synthesizeParams = {
text: text_chunk,
accept: 'audio/mp3',
voice: 'en-US_AllisonV3Voice'
};
textToSpeech.synthesize(synthesizeParams, (err, audio) => {
if (err) {
console.log("synthesize - an error occurred: ");
return reject(err);
}
// write individual files to disk
var file_name = `${relative_path}_${index}.mp3`;
var write_stream = fs.createWriteStream(`${file_name}`);
audio.pipe(write_stream);
// on finish event of individual file write
write_stream.on('finish', function() {
// add file to archive
archive.file(file_name, { name: `audio_${index}.mp3` });
// if not the last value of the array
if (isLastIndex === false) {
resolve();
}
// if the last value of the array
else if (isLastIndex === true) {
resolve();
// when Zip file has finished writing,
// send it back to client, and delete temp files from server
Zip_write_stream.on('close', function() {
// download the Zip file (using absolute_path)
res.download(`${absolute_path}.Zip`, (err) => {
if (err) {
console.log(err);
}
// delete each audio file (using relative_path)
for (let i = 0; i < text_chunk_array.length; i++) {
fs.unlink(`${relative_path}_${i}.mp3`, (err) => {
if (err) {
console.log(err);
}
console.log(`AUDIO FILE ${i} REMOVED!`);
});
}
// delete the Zip file
fs.unlink(`${relative_path}.Zip`, (err) => {
if (err) {
console.log(err);
}
console.log(`Zip FILE REMOVED!`);
});
});
});
// from archiver readme examples
archive.on('warning', function(err) {
if (err.code === 'ENOENT') {
// log warning
} else {
// throw error
throw err;
}
});
// from archiver readme examples
archive.on('error', function(err) {
throw err;
});
// from archiver readme examples
archive.finalize();
}
});
});
});
});
}
Solution 02
J'étais désireux de trouver une solution qui n'utilisait pas de bibliothèque pour "faire une pause" dans l'itération map()
, donc j'ai:
map()
contre une pour la boucleawait
avant l'appel api, plutôt que de le mettre dans une promesse, etreturn new Promise()
pour contenir la gestion des réponses, j'ai utilisé await new Promise()
(glané de cette réponse ) Ce dernier changement, comme par magie, a interrompu la boucle jusqu'à ce que les opérations archive.file()
et audio.pipe(writestream)
soient terminées - je voudrais mieux comprendre comment cela fonctionne.
// route handler
app.route("/api/:api_version/tts")
.get(api_tts_get);
// route handler middleware
const api_tts_get = async (req, res) => {
var query_parameters = req.query;
var file_name = query_parameters.file_name;
var text_string_array = text_string_array; // eg: https://Pastebin.com/raw/JkK8ehwV
var absolute_path = path.join(__dirname, "/src/temp_audio/", file_name);
var relative_path = path.join("./src/temp_audio/", file_name); // path relative to server root
// set up archiver
var archive = archiver('Zip', {
zlib: { level: 9 } // sets the compression level
});
var Zip_write_stream = fs.createWriteStream(`${relative_path}.Zip`);
archive.pipe(Zip_write_stream);
for (const [index, text_chunk] of text_chunk_array.entries()) {
// check if last value of array
const isLastIndex = index === text_chunk_array.length - 1;
var textToSpeech = new TextToSpeechV1({
iam_apikey: iam_apikey,
url: tts_service_url
});
var synthesizeParams = {
text: text_chunk,
accept: 'audio/mp3',
voice: 'en-US_AllisonV3Voice'
};
try {
var audio_readable_stream = await textToSpeech.synthesize(synthesizeParams);
await new Promise(function(resolve, reject) {
// write individual files to disk
var file_name = `${relative_path}_${index}.mp3`;
var write_stream = fs.createWriteStream(`${file_name}`);
audio_readable_stream.pipe(write_stream);
// on finish event of individual file write
write_stream.on('finish', function() {
// add file to archive
archive.file(file_name, { name: `audio_${index}.mp3` });
// if not the last value of the array
if (isLastIndex === false) {
resolve();
}
// if the last value of the array
else if (isLastIndex === true) {
resolve();
// when Zip file has finished writing,
// send it back to client, and delete temp files from server
Zip_write_stream.on('close', function() {
// download the Zip file (using absolute_path)
res.download(`${absolute_path}.Zip`, (err) => {
if (err) {
console.log(err);
}
// delete each audio file (using relative_path)
for (let i = 0; i < text_chunk_array.length; i++) {
fs.unlink(`${relative_path}_${i}.mp3`, (err) => {
if (err) {
console.log(err);
}
console.log(`AUDIO FILE ${i} REMOVED!`);
});
}
// delete the Zip file
fs.unlink(`${relative_path}.Zip`, (err) => {
if (err) {
console.log(err);
}
console.log(`Zip FILE REMOVED!`);
});
});
});
// from archiver readme examples
archive.on('warning', function(err) {
if (err.code === 'ENOENT') {
// log warning
} else {
// throw error
throw err;
}
});
// from archiver readme examples
archive.on('error', function(err) {
throw err;
});
// from archiver readme examples
archive.finalize();
}
});
});
} catch (err) {
console.log("oh dear, there was an error: ");
console.log(err);
}
}
}
Expériences d'apprentissage
D'autres problèmes survenus au cours de ce processus sont documentés ci-dessous:
Les demandes longues expirent lors de l'utilisation du nœud (et renvoient la demande) ...
// solution
req.connection.setTimeout( 1000 * 60 * 10 ); // ten minutes
Voir: https://github.com/expressjs/express/issues/2512
400 erreurs causées par la taille maximale de l'en-tête du nœud de 8 Ko (la chaîne de requête est incluse dans la taille de l'en-tête) ...
// solution (although probably not recommended - better to get text_string_array from server, rather than client)
node --max-http-header-size 80000 app.js