UNPKG

tts-narrator

Version:

Generate narration with Text-To-Speech technology

199 lines (198 loc) 9.82 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ScriptProcessor = void 0; const tslib_1 = require("tslib"); const misc_utils_1 = require("@handy-common-utils/misc-utils"); const multi_integer_range_1 = require("multi-integer-range"); const murmurhash = tslib_1.__importStar(require("murmurhash")); const fs = tslib_1.__importStar(require("node:fs")); const node_path_1 = tslib_1.__importDefault(require("node:path")); const audio_utils_1 = require("./audio-utils"); const azure_tts_service_1 = require("./azure-tts-service"); const narration_script_1 = require("./narration-script"); const tts_service_1 = require("./tts-service"); class ScriptProcessor { constructor(scriptFilePath, flags, cliConsole) { this.scriptFilePath = scriptFilePath; this.flags = flags; this.cliConsole = cliConsole !== null && cliConsole !== void 0 ? cliConsole : (this.chalk ? (0, misc_utils_1.consoleWithColour)(this.flags, this.chalk) : (0, misc_utils_1.consoleWithoutColour)(this.flags)); } /** * prompts function, or null caused by library not available */ get prompts() { if (this._prompts === undefined) { try { // eslint-disable-next-line unicorn/prefer-module this._prompts = require('prompts'); } catch (error) { this._prompts = null; this.cliConsole.info(`Library for prompting user input is not available: ${error}`); } } return this._prompts; } /** * chalk, or null caused by library not available */ get chalk() { if (this._chalk === undefined) { try { // eslint-disable-next-line unicorn/prefer-module this._chalk = require('chalk'); } catch (error) { this._chalk = null; this.cliConsole.debug(`Library for colourising console output is not available: ${error}`); } } return this._prompts; } hash(ssml, _paragraph) { const hashNumber = murmurhash.v3(ssml, 2894); return String(hashNumber); } async loadScriptIfNeeded() { if (!this._script) { this._script = await (0, narration_script_1.loadScript)(this.scriptFilePath); this.cliConsole.debug(`Loaded script from ${this.scriptFilePath}`); } } parseRanges() { const chapterRangeFlag = this.flags.chapters; if (chapterRangeFlag) { try { this.chapterRange = new multi_integer_range_1.MultiRange(chapterRangeFlag); } catch (error) { throw new Error(`Invalid chapter range '${chapterRangeFlag}': ${error}`); } } const sectionRangeFlag = this.flags.sections; if (sectionRangeFlag) { try { this.sectionRange = new multi_integer_range_1.MultiRange(sectionRangeFlag); } catch (error) { throw new Error(`Invalid section range '${sectionRangeFlag}': ${error}`); } } } async initialiseTtsServiceIfNeeded() { var _a, _b; if (!this.ttsService) { const ttsServiceType = (_a = this.flags.service) !== null && _a !== void 0 ? _a : this._script.settings.service; switch (ttsServiceType) { case tts_service_1.TtsServiceType.Azure: { this.ttsService = new azure_tts_service_1.AzureTtsService(); this.audioGenerationOptions = { subscriptionKey: (_b = this.flags['subscription-key']) !== null && _b !== void 0 ? _b : (this.flags['subscription-key-env'] ? process.env[this.flags['subscription-key-env']] : undefined), serviceRegion: this.flags.region, outputFormat: this.flags.outputFormat, }; break; } default: { throw new Error(`Unknown TTS service: ${ttsServiceType}`); } } } } async determineAudioFilePath(ssmlHash, _paragraph) { const audioFileFolder = this._script.scriptFilePath.split('.').slice(0, -1).join('.') + '.tts'; if (!fs.existsSync(audioFileFolder)) { fs.mkdirSync(audioFileFolder, { recursive: true }); } const audioFilePath = node_path_1.default.join(audioFileFolder, `${ssmlHash}.mp3`); return audioFilePath; } async processGeneratedAudioFile(audioFilePath) { return audioFilePath; } async run(reconstructedCommandLine) { try { await this.runWithoutCatch(reconstructedCommandLine); } catch (error) { this.cliConsole.error(error.message); this.cliConsole.debug(error); } } async runWithoutCatch(reconstructedCommandLine) { if (reconstructedCommandLine) { this.cliConsole.debug(`Executing command line: ${reconstructedCommandLine}`); } await this.loadScriptIfNeeded(); // chapter and section ranges this.parseRanges(); // walk through the script for (const chapter of this._script.chapters) { const chapterIndex = chapter.index; if (!this.chapterRange || this.chapterRange.has(chapterIndex)) { this.cliConsole.debug(`Entering chapter [${chapterIndex}] ${chapter.key}`); for (const section of chapter.sections) { const sectionIndex = section.index; if (!this.sectionRange || this.sectionRange.has(sectionIndex)) { this.cliConsole.debug(`Entering section [${chapterIndex}-${sectionIndex}] ${section.key}`); for (const paragraph of section.paragraphs) { const paragraphIndex = paragraph.index; // wait for user key press if needed if (paragraphIndex === 1 && this.flags.interactive && this.prompts) { const response = await this.prompts({ initial: true, message: `Press ENTER to continue or CTRL-C to abort => [${chapterIndex}-${sectionIndex}] ${section.key}`, name: 'r', type: 'confirm', }); if (response.r === undefined) { // CTRL-C return; } } this.cliConsole.debug(`Entering paragraph [${chapterIndex}-${sectionIndex}-${paragraphIndex}] ${paragraph.key}`); this.cliConsole.debug(`Processing: ${paragraph.ssml || paragraph.text}`); // generate SSML and its hash await this.initialiseTtsServiceIfNeeded(); // initialise in first use const ssml = await this.ttsService.generateSSML(paragraph); const ssmlHash = this.hash(ssml, paragraph); if (this.flags.ssml) { this.cliConsole.info(`SSML generated with hash ${ssmlHash}:`); this.cliConsole.info(ssml); } const generatedAudioFilePath = await this.determineAudioFilePath(ssmlHash, paragraph); if (this.flags['dry-run']) { this.cliConsole.debug('No action because of dry-run flag'); } else { // check to see if the .mp3 file already exists if (!this.flags.overwrite && fs.existsSync(generatedAudioFilePath) && fs.statSync(generatedAudioFilePath).size > 0) { this.cliConsole.debug(`Re-using already existing audio file '${generatedAudioFilePath}' for ${chapterIndex}-${sectionIndex}-${paragraphIndex}`); } else { // generate .mp3 file if needed await this.ttsService.generateAudio(ssml, Object.assign(Object.assign({}, this.audioGenerationOptions), { outputFilePath: generatedAudioFilePath })); if (this.cliConsole.isDebug) { const audioDuration = await (0, audio_utils_1.getAudioFileDuration)(generatedAudioFilePath); this.cliConsole.debug(`Generated audio of ${audioDuration / 1000}s: ${generatedAudioFilePath}`); } } // post-processing const audioFilePath = await this.processGeneratedAudioFile(generatedAudioFilePath); paragraph.audioFilePath = audioFilePath; // play .mp3 file if needed if (this.flags.play) { await (0, audio_utils_1.playMp3File)(audioFilePath, this.cliConsole.info); } } } } } } } this.cliConsole.debug(`Finished processing ${this.scriptFilePath}`); } get script() { return this._script; } } exports.ScriptProcessor = ScriptProcessor;