tts-narrator
Version:
Generate narration with Text-To-Speech technology
199 lines (198 loc) • 9.82 kB
JavaScript
"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;