web-dev-qa-db-fra.com

Comment diriger plusieurs flux lisibles, depuis plusieurs requêtes API, vers un seul flux accessible en écriture?

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 le response?

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 mode flowing. Tous les flux lisibles démarrent par défaut en mode pause, mais ils peuvent être facilement basculés vers flowing et revenir à paused si nécessaire ... en ajoutant simplement un gestionnaire d'événements data, le flux suspendu en mode flowing et la suppression du gestionnaire d'événements data remet le flux en mode paused.

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

enter image description here

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énement end, 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énement finish, 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' de fs.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 destination dst

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 source Readable émet 'end', De sorte que la destination est n'est plus accessible en écriture. Pour désactiver ce comportement par défaut, l'option end peut être passée en tant que false, 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éthode stream.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

Pipe plusieurs fichiers vers une seule réponse

Création d'un flux Node.js à partir de deux flux canalisés

11
user1063287

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.

https://www.npmjs.com/package/simple-peer

0
Hamid Raza Noori

Voici deux solutions.

Solution 01

  • utilise Bluebird.mapSeries
  • écrit des réponses individuelles dans des fichiers temporaires
  • les place dans un fichier Zip (en utilisant archiveur )
  • envoie le fichier Zip au client pour l'enregistrer
  • supprime les fichiers temporaires

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:

  • échangé la fonction map() contre une pour la boucle
  • utilisé await avant l'appel api, plutôt que de le mettre dans une promesse, et
  • au lieu d'utiliser return 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

Voir: https://github.com/nodejs/node/issues/24692

0
user1063287