├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TODO.md ├── audio ├── PAUSE.mp3 ├── README.md ├── en │ └── README.md ├── fr │ └── README.md └── it │ └── README.md ├── bin └── jointts.js ├── com ├── duration ├── pause └── play ├── config ├── README.md ├── en │ └── characters.json └── it │ └── characters.json ├── doc ├── API.md ├── CLI.md ├── config.md ├── multilanguage.md └── segmentation.md ├── examples ├── README.md ├── en │ ├── CSQU3054383.mp3 │ ├── JL1349-76_[45A_slash_MU4].mp3 │ └── RAIU_690011_4_25_U1.mp3 └── it │ ├── CSQU3054383.mp3 │ ├── CSQU3054383_long.mp3 │ ├── JL1349-76 [45AslashMU4].mp3 │ └── RAIU 690011 4 25 U1.mp3 ├── index.js ├── lib ├── audioFilenameFromText.js ├── buildCharactersAudio.js ├── buildConfigJson.js ├── characterSetIt.js ├── characters.js ├── charbychar.js ├── concatAudioFiles.js ├── convertAudioFormat.js ├── elapsed.js ├── fileHelpers.js ├── getArgs.js ├── googleTranslateLanguages.js ├── googleTranslateTTS.js ├── info.js ├── sanitizeFilename.js ├── sentenceTokenizer.js ├── serialize.js ├── sleep.js ├── spawn.js └── ttsfile.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # vim session data 2 | Session.vim 3 | 4 | # Dependency directories 5 | node_modules/ 6 | 7 | audio/it/opus 8 | audio/it/*.webm 9 | audio/it/*.mp3 10 | audio/it/*.wav 11 | audio/it/*.ogg 12 | audio/it/*.pcm 13 | 14 | audio/en/opus 15 | audio/en/*.webm 16 | audio/en/*.mp3 17 | audio/en/*.wav 18 | audio/en/*.ogg 19 | audio/en/*.pcm 20 | 21 | # temporary files 22 | tmp/ 23 | 24 | # Logs 25 | log/ 26 | *.log 27 | 28 | # Optional npm cache directory 29 | .npm 30 | 31 | # Optional eslint cache 32 | .eslintcache 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # Output of 'npm pack' 38 | *.tgz 39 | 40 | # dotenv environment variables file 41 | .env 42 | .env.test 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## Version 0.13.4 6 | 7 | - added config/README.md 8 | - added config/en/characters.json 9 | - added examples audio file in: examples/it examples/en 10 | 11 | 12 | ## Version 0.12.0 13 | 14 | - modified config/it/characters.json 15 | - modified com/pause 16 | - modified com/play 17 | - modified doc/segmentation.md 18 | - modified lib/buildCharactersAudio.js 19 | - modified lib/buildConfigJson.js 20 | - spelling (charByChar) function renamed: lib/charbychar.js 21 | - added examples/ containing spelling examples 22 | 23 | ## Version 0.11.1 24 | 25 | - google-tts-api updated to version "^2.0.1" 26 | - lib/buildCharactersAudio.js minor issue fixed 27 | 28 | ## Version 0.10.0 29 | 30 | Major new feature: 31 | - spelling (charByChar) function 32 | - com/pause 33 | 34 | modified files: 35 | - README.md 36 | - TODO.md 37 | - config/it/characters.json 38 | - lib/audioFilenameFromText.js 39 | - lib/buildCharactersAudio.js 40 | - lib/sanitizeFilename.js 41 | 42 | new files: 43 | - doc/config.md 44 | - lib/buildConfigJson.js 45 | - lib/spelling.js 46 | - lib/characters.js 47 | 48 | ## Version 0.8.0 49 | 50 | modified files: 51 | - bin/jointts.js 52 | - lib/convertAudioFormat.js 53 | - lib/googleTranslateLanguages.js 54 | - lib/googleTranslateTTS.js 55 | 56 | new files: 57 | - TODO.md 58 | - lib/buildCharactersAudio.js 59 | - lib/characterSet.js 60 | - lib/info.js 61 | 62 | ## Version 0.7.0 63 | 64 | - added subcommand `jointts convert` 65 | - modified bin/jointts.js 66 | - modified lib/convertAudioFormat.js 67 | - new index.js 68 | 69 | ## Version 0.6.7 70 | - added disclaimer section in README.md 71 | - documentation rework, creating multiple files in doc/ 72 | 73 | ## Version 0.5.0 74 | - better explanation in README.md 75 | - lib/characterSetIt.js contains Italian language character spelling 76 | - lib/buildConfigJsonIt.js build Italian language grammar config/it/config.json 77 | - lib/audioFilenameFromText.js updated 78 | 79 | ## Version 0.4.0 80 | - new 81 | - config/it/ 82 | - lib/characterSetIt.js 83 | - modified 84 | - config/characters.json 85 | - lib/audioFilenameFromText.js 86 | - lib/concatAudioFiles.js 87 | - lib/fileHelpers.js 88 | 89 | ## Version 0.3.3 90 | - added command line utility `jointts download googletranslate` 91 | - command line utility `jointts` 92 | - ISO langauge codes are validate through googleTranslateLanguages.js 93 | 94 | ## Version 0.1.0 95 | - [oncatAudioFiles.js](lib/concatAudioFiles.js): concat files with same codec, using ffmpeg. 96 | - [convertAudioFormats.js](lib/convertAudioFormats.js): convert audio files codecs/formats, using ffmpeg. 97 | - helpers bash scripts (using ffmpeg): `script/duration`, `script/play`. 98 | - [googleTranslateTTS.js](lib/googleTranslateTTS.js): downloads audio base files, using [Google Translate Speech library](https://github.com/zlargon/google-tts). 99 | 100 | ## Version 0.0.7 101 | - lib/googleTranslateTTS download teh TTS MP3 file from googleTranslate 102 | - lib/convertcodec change codec to WAV/OPUS format, from input audio files 103 | 104 | ## Version 0.0.1 - initial commit 105 | - edited README.md 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Giorgio Robino 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JoinTTS 2 | 3 | Brainless concatenative text to speech. 4 | 5 | JoinTTS is a simple off-line (on-premise) concatenative TTS nodejs API. 6 | 7 | > `jointts` is the formal name of this project, 8 | > but you can call it simply `joint`, 9 | > that's also the command line program alias. 10 | > Ah! There’s a funny double meaning in the name.🙄 11 | 12 | ## Introduction 13 | 14 | The goal is to build a super simple efficient concatenative speech synthesis 15 | that at run-time concatenates prerecorded local audio files, without any cloud access. 16 | 17 | The system is suitable for applications with a small grammar 18 | (a limited set of sentences/words) for a semi-static speech generation. 19 | 20 | An example of application could be an embedded system TTS made by mainly fixed output sentences, 21 | but containing a small amount of variable/dynamic parts, as entities (codes, names) in template literals. 22 | 23 | The target environment is so any sort of embedded system (on-premise/off-line), with poor CPU resources, 24 | but the need of a real-time responsive speech output. 25 | 26 | The speech is produced by concatenating prepared audio files sources, 27 | for letters, words, template literals, entire phrases. 28 | All audio files "chunks" needed are prepared offline, to be available afterward, 29 | at run-time, for a fast concatenative audio generation. 30 | 31 | Text-to-speech output are audio files 32 | or in-memory binary blobs (nodejs buffers) 33 | in a specific audio codec as PCM or OPUS. 34 | 35 | Audio recordings could be realized/sourced in two ways, using in alternative: 36 | 37 | - Real human voices (by voice actors) recordings 38 | 39 | This is specially useful by example in language education apps, 40 | for special purposes, as syllables pronunciation. 41 | 42 | - Synthetic voices recording 43 | 44 | You can by example use Google Translate TTS, or any TTS of your choice to prepare speech files/buffers) 45 | 46 | > 💡 Note that using a cloud-based TTS to generate audio chunks 47 | > is more a test system to workaround the availability of real human voice recording. 48 | > Please read [disclaimer](#discalimer) section for details. 49 | 50 | 51 | ### Multi language 52 | 53 | Speech generation is language-dependent. 54 | 55 | JoinTTS can be configured to manage many natural languages. 56 | See [Multi-language](doc/multilanguage.md) doc. 57 | 58 | ### Segmentation 59 | 60 | Input texts could be managed as characters, words, phrases. 61 | 62 | - Static phrases 63 | - Words concatenation 64 | - Character-by-character spelling 65 | - Template literals 66 | 67 | See [Text segmentation](doc/segmentation.md) doc. 68 | 69 | 70 | ## How it works? 71 | 72 | ### Step 1 - Build "language model" configurations 73 | 74 | All audio files required are generated following configuration files settings, 75 | with user voice recordings or with any (synthetic voices) third party sources to be downloaded. 76 | 77 | Configurations files are: 78 | 79 | - `characters.json` 80 | - `words.json` 81 | - `phrases.json` 82 | - `templates.json` 83 | 84 | They specify which file has to be used for the target concatenation. 85 | 86 | Configuration files are language-dependent: 87 | 88 | - `config/it/*.json` 89 | - `config/en/*.json` 90 | - `config/de/*.json` 91 | - ... 92 | 93 | ``` 94 | +-------------------+ 95 | | | 96 | | joinTTS CLI | 97 | | | 98 | +---------+---------+ 99 | | 100 | +---------v---------+ 101 | | | 102 | | language grammar | 103 | | config generator | 104 | | | 105 | +---------+---------+ 106 | | 107 | v 108 | config/it/*.json 109 | config/en/*.json 110 | config/de/*.json 111 | | 112 | v 113 | ``` 114 | 115 | ### Step 2 - Build speech audio files 116 | 117 | Audio source files can be made in 2 different ways: 118 | 119 | - 🎙 Human voice recordings 120 | 121 | For a personalized voice experience, 122 | a voice actor can record all required audio files. 123 | 124 | 🛠 TOCOMPLETE 125 | 126 | - 🩹 Synthetic voices files 127 | 128 | Audio files are generated by any cloud-based TTS and downloaded as files. 129 | A synthetic voice file can be made using any cloud-based TTS 130 | as Amazon Polly, Google Cloud Platform Text-to-Speech, etc. 131 | 132 | joinTTS use, for example only, the [Google Translate Speech library](https://github.com/zlargon/google-tts). 133 | Whit `jointts` (or `joint`) command line utility, speech MP3 files 134 | (containing the Google Translate synthetic voice) can be generated from texts: 135 | 136 | ```bash 137 | $ jointts download gt 138 | ``` 139 | ``` 140 | +------------------+ 141 | | | 142 | | joinTTS CLI | 143 | | | 144 | +---------+--------+ 145 | config/it/*.json | 146 | config/en/*.json | 147 | config/de/*.json | 148 | | | +---------v--------+ 149 | | +----------> | 150 | | | audio files | 151 | | | production | 152 | | | | 153 | | +--------+---------+ 154 | | | 155 | | v 156 | | audio/it/a.mp3 157 | | audio/it/b.mp3 158 | | audio/it/c.mp3 159 | | ... 160 | | | 161 | v v 162 | ``` 163 | 164 | ### Step 3 - run-time usage 165 | 166 | At run-time the main program call joints run-time engine 167 | that generates on the fly audio speech files, 168 | concatenating available audio chunks. 169 | 170 | ``` 171 | config/it/*.json 172 | config/en/*.json 173 | config/de/*.json 174 | | audio/it/a.mp3 175 | | audio/it/b.mp3 176 | | audio/it/c.mp3 177 | | ... 178 | | | 179 | +---------v---------------------v----------+ 180 | | | 181 | text --> | joinTTS run-time API | --> audio file 182 | 'ABC123' | | ABC123.mp3 183 | +------------------------------------------+ 184 | | ffmpeg | 185 | +------------------------------------------+ 186 | ``` 187 | 188 | See functions documentation: 189 | 190 | - function calls [API](doc/API.md) 191 | - command line program usage [`jointts`](doc/CLI.md) 192 | 193 | 194 | ## Installation 195 | 196 | 1. 📦 Install `ffmpeg` 197 | 198 | [ffmpeg](https://ffmpeg.org/) is used acid backend engine for all audio files conversions, 199 | audio play, audio concatenations. 200 | 201 | ```bash 202 | sudo apt install ffmpeg 203 | ``` 204 | Optionally, to use OPUS codecs: 205 | 206 | ```bash 207 | sudo apt install libopus0 opus-tools 208 | ``` 209 | 210 | 2. 📦 Install `jointts` 211 | 212 | The package contains command line program `jointts`, 213 | so you must install the npm package as global: 214 | 215 | Download this github repo: 216 | 217 | ```bash 218 | $ git clone https://github.com/solyarisoftware/jointts 219 | $ cd jointts && npm link 220 | ``` 221 | 222 | Or use npm package manager repo 223 | 224 | ```bash 225 | $ npm install -g jointts 226 | ``` 227 | 228 | ## 👂 Listen audio rendering examples 229 | 230 | Listen [here](examples/README.md) examples of spelling audio rendering for alphanumeric codes. 231 | 232 | ## 🛠 Status 233 | 234 | WORK-IN-PROGRESS / DRAFT. 235 | 236 | So far, the project is a proof-of-concept, 237 | in pre-alfa stage, with 60% of features implemented. 238 | Smart high-level usage has to be defined. 239 | 240 | 241 | ## ⚠️ Disclaimer 242 | 243 | JointTTS run-time usage is intended to basically run on a private environment. 244 | You are in charge to manage privacy, permissions, licenses, of all your files. 245 | 246 | If you use cloud-based TTS platforms (as Amazon Polly, Google TTS, etc.) 247 | to download synthetic voice files in the preparation step, 248 | it’s your responsibility to not break any license or copyright. 249 | 250 | In the same way, if you use voice recordings of other people, 251 | please assure to have permissions to do it. 252 | 253 | 254 | ## License 255 | 256 | [MIT](LICENSE) (c) Giorgio Robino 257 | 258 | --- 259 | 260 | [top](#) 261 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To do 2 | 3 | ## Issues related to google-tts package 4 | 5 | opened issue: https://github.com/zlargon/google-tts/issues/33 6 | 7 | - [x] node-fetch vulnerability found in package google-tts 8 | 9 | closed issue: https://github.com/zlargon/google-tts/issues/31 10 | 11 | - [ ] get key failed from google 12 | ``` 13 | $ jointts download gt mi chiamo Giorgio --language=it 14 | 15 | sentence : mi chiamo Giorgio 16 | language : it 17 | speed : normal 18 | 19 | Error: get key failed from google 20 | at /home/giorgio/jointts/node_modules/google-tts-api/lib/key.js:30:32 21 | at processTicksAndRejections (internal/process/task_queues.js:93:5) 22 | ``` 23 | 24 | -------------------------------------------------------------------------------- /audio/PAUSE.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/audio/PAUSE.mp3 -------------------------------------------------------------------------------- /audio/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/audio/README.md -------------------------------------------------------------------------------- /audio/en/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/audio/en/README.md -------------------------------------------------------------------------------- /audio/fr/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/audio/fr/README.md -------------------------------------------------------------------------------- /audio/it/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/audio/it/README.md -------------------------------------------------------------------------------- /bin/jointts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { info } = require('../lib/info') 4 | const { getArgs } = require('../lib/getArgs') 5 | const { downloadGoogleTransalteMP3 } = require('../lib/googleTranslateTTS') 6 | const { printLanguages } = require('../lib/googleTranslateLanguages') 7 | const { convertAudioFormat } = require('../lib/convertAudioFormat') 8 | 9 | const programNamePathItems = process.argv[1].split('/') 10 | const programName = programNamePathItems[programNamePathItems.length-1] 11 | 12 | // get command line args and commands 13 | const { args, commands } = getArgs() 14 | 15 | const command = commands[0] ? commands[0].toLowerCase() : undefined 16 | const subCommand = commands[1] ? commands[1].toLowerCase() : undefined 17 | 18 | switch ( command ) { 19 | 20 | case 'download': 21 | 22 | switch ( subCommand ) { 23 | 24 | case 'gt': 25 | case 'googletranslate': 26 | console.log( info() ) 27 | downloadGoogleTransalteMP3(commands.slice(2), args, `${programName} ${command} ${subCommand}`) 28 | break 29 | 30 | default: 31 | console.log( info() ) 32 | console.log( usage() ) 33 | break 34 | 35 | } 36 | break 37 | 38 | case 'isocodes': 39 | case 'languages': 40 | console.log( info() ) 41 | printLanguages() 42 | break 43 | 44 | case 'convert': 45 | console.log( info() ) 46 | convertAudioFormat(commands.slice(1), args, `${programName} ${command}`) 47 | .then( result => { 48 | 49 | if (result.exit == 0) 50 | console.log( result ) 51 | else 52 | console.error( `command ${result.fullcmd} failed with exit code: ${result.exit}` ) 53 | 54 | }) 55 | .catch( data => console.log( `command failed with error. See: ${data}` )) 56 | break 57 | 58 | default: 59 | console.log( info() ) 60 | console.log( usage() ) 61 | break 62 | 63 | } 64 | 65 | 66 | function usage() { 67 | return ( 68 | 'Usage:\n\n' + 69 | ` ${programName} download gt download Google Translate TTS MP3 file\n` + 70 | ` ${programName} languages list of ISO-639-1 language codes (in Google Translate)\n` + 71 | ` ${programName} convert convert audio file codec\n` + 72 | '\n' 73 | ) 74 | } 75 | 76 | -------------------------------------------------------------------------------- /com/duration: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # calculate duration in seconds of a webm file 5 | # converting it in a wav temporary file 6 | # usage: com/duration audiofiles/audiofile.webm 7 | # see: https://stackoverflow.com/questions/62093480/how-to-find-duration-of-a-webm-opus-audio-file-in-seconds-in-a-js-program/62102888#62102888 8 | # 9 | if [ $# -eq 0 ] 10 | then 11 | echo 12 | echo "calculate duration in seconds of an audio file" 13 | echo "usage: $0 " 14 | echo 15 | exit 16 | fi 17 | 18 | INPUT_FILE=$1 19 | TMP_FILE=$1.temporary.wav 20 | 21 | ffmpeg -loglevel panic -i $INPUT_FILE -y $TMP_FILE 22 | ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 $TMP_FILE 23 | rm $TMP_FILE 24 | -------------------------------------------------------------------------------- /com/pause: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # https://www.mankier.com/1/ffmpeg-utils#Syntax-Time_duration 5 | # 6 | if [[ $# -ne 2 ]] 7 | then 8 | echo 9 | echo "create a silent file (pause), durating specified milliseconds" 10 | echo "usage: $0 " 11 | echo 12 | exit 13 | fi 14 | 15 | CODEC='mp3' 16 | BITRATE=24000 17 | 18 | ffmpeg -y -f lavfi -i anullsrc=r=$BITRATE:cl=mono -t $2ms -acodec $CODEC $1 19 | -------------------------------------------------------------------------------- /com/play: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # play an audio file 5 | # 6 | # cvlc --play-and-exit --loop --verbose -1 /home/giorgio/concatts/audio/mi_chiamo_giorgio.mp3 7 | # ffplay -nodisp -autoexit -hide_banner -loglevel panic audio/mi_chiamo_giorgio.mp3.opus 8 | # opusdec --force-wav --quiet audio/mi_chiamo_giorgio.mp3.opus - | aplay 9 | # 10 | if [ $# -eq 0 ] 11 | then 12 | echo 13 | echo "play an audio file" 14 | echo "usage: $0 " 15 | echo 16 | exit 17 | fi 18 | 19 | ffplay -nodisp -autoexit -hide_banner -loglevel panic "$1" 20 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Characters configuration (DRAFT) 2 | 3 | Why and how to build configuration files for characters spelling. 4 | 5 | ## Characters pronunciation 6 | 7 | With the goal of doing word spelling, 8 | for each natural language, you need to associate each letter (character) with a spelling pronunciation. 9 | 10 | For example consider the original input text: 11 | ``` 12 | CSQU3054383 13 | ``` 14 | you want to spell the word as sequence of single characters (separated by brief pauses). 15 | ``` 16 | C S Q U 3 0 5 4 3 8 3 17 | ``` 18 | A basic TTS for English language could be the pronunciation of each character spelling: 19 | ``` 20 | sii es kiu iu three zero five four three eight three 21 | ``` 22 | Better, you probably want to spell each character using the pseudo-phonetic spelling, that's related to the: 23 | 24 | - each specific language 25 | - a specific [spelling alphabet](https://en.wikipedia.org/wiki/Spelling_alphabet). 26 | 27 | By example, using the [NATO phonetic alphabet](https://en.wikipedia.org/wiki/NATO_phonetic_alphabet), 28 | the code will be spelled like: 29 | ``` 30 | Charli for sii 31 | Sierra for es 32 | Quebec for kiu 33 | Uniform for iu 34 | three 35 | zero 36 | five 37 | four 38 | three 39 | eight 40 | three 41 | ``` 42 | 43 | ## Characters pronunciation configuration files 44 | 45 | For each language, 46 | you want to associate a character part of a that language character set, 47 | to an audio file that contains the spelling. 48 | By example: 49 | 50 | ``` 51 | { 52 | "a": { 53 | "speech": "alfa for a", 54 | "file": "a.mp3" 55 | }, 56 | "b": { 57 | "speech": "bravo for b", 58 | "file": "b.mp3" 59 | }, 60 | "c": { 61 | "speech": "charlie for c", 62 | "file": "c.mp3" 63 | }, 64 | } 65 | ``` 66 | 67 | jointts use JSON configuration files, containing for each language: 68 | - all the letters of the alphabet, 69 | - digits, 70 | - special (punctuation) characters 71 | 72 | Currently jointts foresee Italian and English languages and related characters configuration: 73 | - `config/it/characters.json` 74 | - `config/en/characters.json` 75 | 76 | 77 | # Build characters speech files 78 | 79 | When a character configuration file is prepared for your target language, 80 | you have to create corresponding speech audio files. 81 | 82 | You can use Google Translate Speech TTS to build character spelling audio files, 83 | by example for Italian and English, with commands: 84 | 85 | ```bash 86 | node lib/buildCharactersAudio.js --language=it 87 | node lib/buildCharactersAudio.js --language=en 88 | ``` 89 | --- 90 | 91 | [top](#) | [home](../README.md) 92 | -------------------------------------------------------------------------------- /config/en/characters.json: -------------------------------------------------------------------------------- 1 | { 2 | "homedirectory": "audio", 3 | "language": "en", 4 | "characters": { 5 | "0": { 6 | "speech": "zero", 7 | "file": "0.mp3" 8 | }, 9 | "1": { 10 | "speech": "one", 11 | "file": "1.mp3" 12 | }, 13 | "2": { 14 | "speech": "two", 15 | "file": "2.mp3" 16 | }, 17 | "3": { 18 | "speech": "three", 19 | "file": "3.mp3" 20 | }, 21 | "4": { 22 | "speech": "four", 23 | "file": "4.mp3" 24 | }, 25 | "5": { 26 | "speech": "five", 27 | "file": "5.mp3" 28 | }, 29 | "6": { 30 | "speech": "six", 31 | "file": "6.mp3" 32 | }, 33 | "7": { 34 | "speech": "seven", 35 | "file": "7.mp3" 36 | }, 37 | "8": { 38 | "speech": "eight", 39 | "file": "8.mp3" 40 | }, 41 | "9": { 42 | "speech": "nine", 43 | "file": "9.mp3" 44 | }, 45 | "a": { 46 | "speech": "alfa for a", 47 | "file": "a.mp3" 48 | }, 49 | "b": { 50 | "speech": "bravo for b", 51 | "file": "b.mp3" 52 | }, 53 | "c": { 54 | "speech": "charlie for c", 55 | "file": "c.mp3" 56 | }, 57 | "d": { 58 | "speech": "delta for d", 59 | "file": "d.mp3" 60 | }, 61 | "e": { 62 | "speech": "echo for e", 63 | "file": "e.mp3" 64 | }, 65 | "f": { 66 | "speech": "foxtrot for f", 67 | "file": "f.mp3" 68 | }, 69 | "g": { 70 | "speech": "golf for g", 71 | "file": "g.mp3" 72 | }, 73 | "h": { 74 | "speech": "hotel for h", 75 | "file": "h.mp3" 76 | }, 77 | "i": { 78 | "speech": "india for i", 79 | "file": "i.mp3" 80 | }, 81 | "j": { 82 | "speech": "juliet for j", 83 | "file": "j.mp3" 84 | }, 85 | "k": { 86 | "speech": "kilo for k", 87 | "file": "k.mp3" 88 | }, 89 | "l": { 90 | "speech": "lima for l", 91 | "file": "l.mp3" 92 | }, 93 | "m": { 94 | "speech": "mike for m", 95 | "file": "m.mp3" 96 | }, 97 | "n": { 98 | "speech": "november for n", 99 | "file": "n.mp3" 100 | }, 101 | "o": { 102 | "speech": "oscar for o", 103 | "file": "o.mp3" 104 | }, 105 | "p": { 106 | "speech": "papa for p", 107 | "file": "p.mp3" 108 | }, 109 | "q": { 110 | "speech": "quebec for q", 111 | "file": "q.mp3" 112 | }, 113 | "r": { 114 | "speech": "romeo for r", 115 | "file": "r.mp3" 116 | }, 117 | "s": { 118 | "speech": "sierra for s", 119 | "file": "s.mp3" 120 | }, 121 | "t": { 122 | "speech": "tango for t", 123 | "file": "t.mp3" 124 | }, 125 | "u": { 126 | "speech": "uniform for u", 127 | "file": "u.mp3" 128 | }, 129 | "v": { 130 | "speech": "victor for v", 131 | "file": "v.mp3" 132 | }, 133 | "w": { 134 | "speech": "whiskey for w", 135 | "file": "w.mp3" 136 | }, 137 | "x": { 138 | "speech": "x-ray for x", 139 | "file": "x.mp3" 140 | }, 141 | "y": { 142 | "speech": "yankee for y", 143 | "file": "y.mp3" 144 | }, 145 | "z": { 146 | "speech": "zulu for z", 147 | "file": "z.mp3" 148 | }, 149 | " ": { 150 | "speech": "space", 151 | "file": "space.mp3" 152 | }, 153 | "\t": { 154 | "speech": "tab", 155 | "file": "tabulation.mp3" 156 | }, 157 | ".": { 158 | "speech": "point", 159 | "file": "point.mp3" 160 | }, 161 | ",": { 162 | "speech": "comma", 163 | "file": "comma.mp3" 164 | }, 165 | ";": { 166 | "speech": "semicolon", 167 | "file": "semicolon.mp3" 168 | }, 169 | ":": { 170 | "speech": "colon", 171 | "file": "colon.mp3" 172 | }, 173 | "!": { 174 | "speech": "exclamation mark", 175 | "file": "exclamation_mark.mp3" 176 | }, 177 | "?": { 178 | "speech": "question mark", 179 | "file": "question_mark.mp3" 180 | }, 181 | "’": { 182 | "speech": "tick", 183 | "file": "tick.mp3" 184 | }, 185 | "‘": { 186 | "speech": "backtick", 187 | "file": "backtick.mp3" 188 | }, 189 | "\"": { 190 | "speech": "quotation mark", 191 | "file": "quotation marks.mp3" 192 | }, 193 | "'": { 194 | "speech": "apostrophe", 195 | "file": "apostrophe.mp3" 196 | }, 197 | "´": { 198 | "speech": "acute accent", 199 | "file": "accent_acute.mp3" 200 | }, 201 | "`": { 202 | "speech": "grave accent", 203 | "file": "accent_grave.mp3" 204 | }, 205 | "”": { 206 | "speech": "closing_inclined_quotes", 207 | "file": "closing_inclined_quotes.mp3" 208 | }, 209 | "“": { 210 | "speech": "opening_inclined_quotes", 211 | "file": "opening_inclined_quotes.mp3" 212 | }, 213 | "«": { 214 | "speech": "opening double quotes", 215 | "file": "opening_double_quotes.mp3" 216 | }, 217 | "»": { 218 | "speech": "closing double quotes", 219 | "file": "closing_double_quotes.mp3" 220 | }, 221 | "(": { 222 | "speech": "opening round bracket", 223 | "file": "opening_round_bracket.mp3" 224 | }, 225 | ")": { 226 | "speech": "closing round bracket", 227 | "file": "closing_round_bracket.mp3" 228 | }, 229 | "[": { 230 | "speech": "opening square bracket", 231 | "file": "opening_square_bracket.mp3" 232 | }, 233 | "]": { 234 | "speech": "closing square bracket", 235 | "file": "closing_square_bracket.mp3" 236 | }, 237 | "{": { 238 | "speech": "opening curly bracket", 239 | "file": "opening_curly_bracket.mp3" 240 | }, 241 | "}": { 242 | "speech": "closing curly bracket", 243 | "file": "closing_curly_bracket.mp3" 244 | }, 245 | "@": { 246 | "speech": "at sign", 247 | "file": "at_sign.mp3" 248 | }, 249 | "*": { 250 | "speech": "asterisk_symbol", 251 | "file": "symbol_asterisk.mp3" 252 | }, 253 | "#": { 254 | "speech": "hash", 255 | "file": "hash.mp3" 256 | }, 257 | "%": { 258 | "speech": "percent sign", 259 | "file": "percent_sign.mp3" 260 | }, 261 | "|": { 262 | "speech": "vertical_bar", 263 | "file": "vertical_bar.mp3" 264 | }, 265 | "/": { 266 | "speech": "slash", 267 | "file": "slash.mp3" 268 | }, 269 | "\\": { 270 | "speech": "backslash", 271 | "file": "backslash.mp3" 272 | }, 273 | "£": { 274 | "speech": "lira sign", 275 | "file": "lira_sign.mp3" 276 | }, 277 | "$": { 278 | "speech": "dollar sign", 279 | "file": "dollar_sign.mp3" 280 | }, 281 | "&": { 282 | "speech": "ampersand", 283 | "file": "ampersand.mp3" 284 | }, 285 | "^": { 286 | "speech": "caret", 287 | "file": "caret.mp3" 288 | }, 289 | "=": { 290 | "speech": "equal sign", 291 | "file": "equal_sign.mp3" 292 | }, 293 | "-": { 294 | "speech": "dash", 295 | "file": "dash.mp3" 296 | }, 297 | "+": { 298 | "speech": "plus sign", 299 | "file": "plus sign.mp3" 300 | }, 301 | ">": { 302 | "speech": "grater-than sign", 303 | "file": "grater_than_sign.mp3" 304 | }, 305 | "<": { 306 | "speech": "less-then sign", 307 | "file": "less_then_sign.mp3" 308 | }, 309 | "~": { 310 | "speech": "tilde", 311 | "file": "tilde.mp3" 312 | }, 313 | "_": { 314 | "speech": "underscore", 315 | "file": "underscore.mp3" 316 | }, 317 | "¢": { 318 | "speech": "penny sign", 319 | "file": "penny_sign.mp3" 320 | }, 321 | "©": { 322 | "speech": "copyright sign", 323 | "file": "copyright_sign.mp3" 324 | }, 325 | "÷": { 326 | "speech": "division sign", 327 | "file": "division_sign.mp3" 328 | }, 329 | "µ": { 330 | "speech": "micron sign", 331 | "file": "micron_sign.mp3" 332 | }, 333 | "¶": { 334 | "speech": "paragraph delimiter", 335 | "file": "paragraph_delimiter.mp3" 336 | }, 337 | "±": { 338 | "speech": "more or less sign", 339 | "file": "more_or_less_sign.mp3" 340 | }, 341 | "®": { 342 | "speech": "trademark symbol", 343 | "file": "registered_brand_symbol.mp3" 344 | }, 345 | "§": { 346 | "speech": "section delimiter", 347 | "file": "section_delimiter.mp3" 348 | }, 349 | "™": { 350 | "speech": "trademark sign", 351 | "file": "trademark_sign.mp3" 352 | }, 353 | "¥": { 354 | "speech": "japanese yen sign", 355 | "file": "japanese_yen_sign.mp3" 356 | }, 357 | "¿": { 358 | "speech": "inverted question mark", 359 | "file": "inverted_question_mark.mp3" 360 | }, 361 | "¡": { 362 | "speech": "inverted exclamation mark", 363 | "file": "inverted_exclamation_mark.mp3" 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /config/it/characters.json: -------------------------------------------------------------------------------- 1 | { 2 | "homedirectory": "audio", 3 | "language": "it", 4 | "characters": { 5 | "0": { 6 | "speech": "zero", 7 | "file": "0.mp3" 8 | }, 9 | "1": { 10 | "speech": "uno", 11 | "file": "1.mp3" 12 | }, 13 | "2": { 14 | "speech": "due", 15 | "file": "2.mp3" 16 | }, 17 | "3": { 18 | "speech": "tre", 19 | "file": "3.mp3" 20 | }, 21 | "4": { 22 | "speech": "quattro", 23 | "file": "4.mp3" 24 | }, 25 | "5": { 26 | "speech": "cinque", 27 | "file": "5.mp3" 28 | }, 29 | "6": { 30 | "speech": "sei", 31 | "file": "6.mp3" 32 | }, 33 | "7": { 34 | "speech": "sette", 35 | "file": "7.mp3" 36 | }, 37 | "8": { 38 | "speech": "otto", 39 | "file": "8.mp3" 40 | }, 41 | "9": { 42 | "speech": "nove", 43 | "file": "9.mp3" 44 | }, 45 | "a": { 46 | "speech": "a come Ancona", 47 | "file": "a.mp3" 48 | }, 49 | "b": { 50 | "speech": "bi come Bologna", 51 | "file": "b.mp3" 52 | }, 53 | "c": { 54 | "speech": "ci come Cagliari", 55 | "file": "c.mp3" 56 | }, 57 | "d": { 58 | "speech": "di come Domodossola", 59 | "file": "d.mp3" 60 | }, 61 | "e": { 62 | "speech": "é come Empoli", 63 | "file": "e.mp3" 64 | }, 65 | "f": { 66 | "speech": "èffè come Firenze", 67 | "file": "f.mp3" 68 | }, 69 | "g": { 70 | "speech": "gi come Genova", 71 | "file": "g.mp3" 72 | }, 73 | "h": { 74 | "speech": "àcca come Hotel", 75 | "file": "h.mp3" 76 | }, 77 | "i": { 78 | "speech": "i come Imperia", 79 | "file": "i.mp3" 80 | }, 81 | "j": { 82 | "speech": "i lùnga come Jolly", 83 | "file": "j.mp3" 84 | }, 85 | "k": { 86 | "speech": "càppa come kursaal", 87 | "file": "k.mp3" 88 | }, 89 | "l": { 90 | "speech": "èllè come Livorno", 91 | "file": "l.mp3" 92 | }, 93 | "m": { 94 | "speech": "èmmè come Milano", 95 | "file": "m.mp3" 96 | }, 97 | "n": { 98 | "speech": "ènnè come Napoli", 99 | "file": "n.mp3" 100 | }, 101 | "o": { 102 | "speech": "ò come Otranto", 103 | "file": "o.mp3" 104 | }, 105 | "p": { 106 | "speech": "pi come Palermo", 107 | "file": "p.mp3" 108 | }, 109 | "q": { 110 | "speech": "cu come Quarto", 111 | "file": "q.mp3" 112 | }, 113 | "r": { 114 | "speech": "èrrè come Roma", 115 | "file": "r.mp3" 116 | }, 117 | "s": { 118 | "speech": "èssè come Savona", 119 | "file": "s.mp3" 120 | }, 121 | "t": { 122 | "speech": "ti come Torino", 123 | "file": "t.mp3" 124 | }, 125 | "u": { 126 | "speech": "u come Udine", 127 | "file": "u.mp3" 128 | }, 129 | "v": { 130 | "speech": "vu come Venezia", 131 | "file": "v.mp3" 132 | }, 133 | "w": { 134 | "speech": "vu dóppia come Washington", 135 | "file": "w.mp3" 136 | }, 137 | "x": { 138 | "speech": "ics come Xilofono", 139 | "file": "x.mp3" 140 | }, 141 | "y": { 142 | "speech": "ìpsilon", 143 | "file": "y.mp3" 144 | }, 145 | "z": { 146 | "speech": "zèta come Zara", 147 | "file": "z.mp3" 148 | }, 149 | "á": { 150 | "speech": "á con accento acuto", 151 | "file": "a_con_accento_acuto.mp3" 152 | }, 153 | "à": { 154 | "speech": "à con accento grave", 155 | "file": "a_con_accento_grave.mp3" 156 | }, 157 | "é": { 158 | "speech": "é con accento acuto", 159 | "file": "e_con_accento_acuto.mp3" 160 | }, 161 | "è": { 162 | "speech": "è con accento grave", 163 | "file": "e_con_accento_grave.mp3" 164 | }, 165 | "í": { 166 | "speech": "í con accento acuto", 167 | "file": "i_con_accento_acuto.mp3" 168 | }, 169 | "ì": { 170 | "speech": "ì con accento grave", 171 | "file": "i_con_accento_grave.mp3" 172 | }, 173 | "ó": { 174 | "speech": "ó con accento acuto", 175 | "file": "o_con_accento_acuto.mp3" 176 | }, 177 | "ò": { 178 | "speech": "ò con accento grave", 179 | "file": "o_con_accento_grave.mp3" 180 | }, 181 | "ú": { 182 | "speech": "ú con accento acuto", 183 | "file": "u_con_accento_acuto.mp3" 184 | }, 185 | "ù": { 186 | "speech": "ù con accento grave", 187 | "file": "u_con_accento_grave.mp3" 188 | }, 189 | " ": { 190 | "speech": "spazio", 191 | "file": "spazio.mp3" 192 | }, 193 | "\t": { 194 | "speech": "tabulazione", 195 | "file": "tabulazione.mp3" 196 | }, 197 | ".": { 198 | "speech": "punto", 199 | "file": "punto.mp3" 200 | }, 201 | "·": { 202 | "speech": "punto centrale", 203 | "file": "punto_centrale.mp3" 204 | }, 205 | ",": { 206 | "speech": "virgola", 207 | "file": "virgola.mp3" 208 | }, 209 | ";": { 210 | "speech": "punto e virgola", 211 | "file": "punto_e_virgola.mp3" 212 | }, 213 | ":": { 214 | "speech": "due punti", 215 | "file": "due_punti.mp3" 216 | }, 217 | "!": { 218 | "speech": "punto esclamativo", 219 | "file": "punto_esclamativo.mp3" 220 | }, 221 | "?": { 222 | "speech": "punto interrogativo", 223 | "file": "punto_interrogativo.mp3" 224 | }, 225 | "’": { 226 | "speech": "virgoletta destra inclinata", 227 | "file": "virgoletta_destra_inclinata.mp3" 228 | }, 229 | "‘": { 230 | "speech": "virgoletta sinistra inclinata", 231 | "file": "virgoletta_sinistra_inclinata.mp3" 232 | }, 233 | "\"": { 234 | "speech": "virgolette", 235 | "file": "virgolette.mp3" 236 | }, 237 | "'": { 238 | "speech": "apostrofo", 239 | "file": "apostrofo.mp3" 240 | }, 241 | "´": { 242 | "speech": "accento acuto", 243 | "file": "accento_acuto.mp3" 244 | }, 245 | "`": { 246 | "speech": "accento grave", 247 | "file": "accento_grave.mp3" 248 | }, 249 | "”": { 250 | "speech": "virgolette destre inclinate", 251 | "file": "virgolette_destre_inclinate.mp3" 252 | }, 253 | "“": { 254 | "speech": "virgolette sinistre inclinate", 255 | "file": "virgolette_sinistre_inclinate.mp3" 256 | }, 257 | "«": { 258 | "speech": "virgolette doppie aperte", 259 | "file": "virgolette_doppie_aperte.mp3" 260 | }, 261 | "»": { 262 | "speech": "virgolette doppie chiuse", 263 | "file": "virgolette_doppie_chiuse.mp3" 264 | }, 265 | "(": { 266 | "speech": "parentesi tonda aperta", 267 | "file": "parentesi_tonda_aperta.mp3" 268 | }, 269 | ")": { 270 | "speech": "parentesi tonda chiusa", 271 | "file": "parentesi_tonda_chiusa.mp3" 272 | }, 273 | "[": { 274 | "speech": "parentesi quadra aperta", 275 | "file": "parentesi_quadra_aperta.mp3" 276 | }, 277 | "]": { 278 | "speech": "parentesi quadra chiusa", 279 | "file": "parentesi_quadra_chiusa.mp3" 280 | }, 281 | "{": { 282 | "speech": "parentesi graffa aperta", 283 | "file": "parentesi_graffa_aperta.mp3" 284 | }, 285 | "}": { 286 | "speech": "parentesi graffa chiusa", 287 | "file": "parentesi_graffa_chiusa.mp3" 288 | }, 289 | "@": { 290 | "speech": "chiocciola", 291 | "file": "chiocciola.mp3" 292 | }, 293 | "*": { 294 | "speech": "simbolo asterisco", 295 | "file": "simbolo_asterisco.mp3" 296 | }, 297 | "#": { 298 | "speech": "simbolo cancelletto", 299 | "file": "simbolo_cancelletto.mp3" 300 | }, 301 | "%": { 302 | "speech": "simbolo percento", 303 | "file": "simbolo_percento.mp3" 304 | }, 305 | "|": { 306 | "speech": "barra verticale", 307 | "file": "barra_verticale.mp3" 308 | }, 309 | "/": { 310 | "speech": "barra", 311 | "file": "barra.mp3" 312 | }, 313 | "\\": { 314 | "speech": "barra retroversa", 315 | "file": "barra_retroversa.mp3" 316 | }, 317 | "£": { 318 | "speech": "simbolo valuta lira", 319 | "file": "simbolo_valuta_lira.mp3" 320 | }, 321 | "$": { 322 | "speech": "simbolo valuta dollaro", 323 | "file": "simbolo_valuta_dollaro.mp3" 324 | }, 325 | "&": { 326 | "speech": "simbolo e commerciale", 327 | "file": "simbolo_e_commerciale.mp3" 328 | }, 329 | "^": { 330 | "speech": "simbolo cappelletto", 331 | "file": "simbolo_cappelletto.mp3" 332 | }, 333 | "=": { 334 | "speech": "simbolo uguale", 335 | "file": "simbolo_uguale.mp3" 336 | }, 337 | "-": { 338 | "speech": "trattino", 339 | "file": "trattino.mp3" 340 | }, 341 | "+": { 342 | "speech": "simbolo più", 343 | "file": "simbolo_più.mp3" 344 | }, 345 | ">": { 346 | "speech": "simbolo maggiore", 347 | "file": "simbolo_maggiore.mp3" 348 | }, 349 | "<": { 350 | "speech": "simbolo minore", 351 | "file": "simbolo_minore.mp3" 352 | }, 353 | "~": { 354 | "speech": "tilde", 355 | "file": "tilde.mp3" 356 | }, 357 | "—": { 358 | "speech": "trattino lungo", 359 | "file": "trattino_lungo.mp3" 360 | }, 361 | "_": { 362 | "speech": "sottolineato", 363 | "file": "sottolineato.mp3" 364 | }, 365 | "¢": { 366 | "speech": "simbolo di centesimo", 367 | "file": "simbolo_di_centesimo.mp3" 368 | }, 369 | "©": { 370 | "speech": "simbolo di copyright", 371 | "file": "simbolo_di_copyright.mp3" 372 | }, 373 | "÷": { 374 | "speech": "simbolo di divisione", 375 | "file": "simbolo_di_divisione.mp3" 376 | }, 377 | "µ": { 378 | "speech": "simbolo micron", 379 | "file": "simbolo_micron.mp3" 380 | }, 381 | "¶": { 382 | "speech": "delimitatore di paragrafo", 383 | "file": "delimitatore_di_paragrafo.mp3" 384 | }, 385 | "±": { 386 | "speech": "simbolo più o meno", 387 | "file": "simbolo_più_o_meno.mp3" 388 | }, 389 | "®": { 390 | "speech": "simbolo di marchio registrato", 391 | "file": "simbolo_di_marchio_registrato.mp3" 392 | }, 393 | "§": { 394 | "speech": "delimitatore di sezione", 395 | "file": "delimitatore_di_sezione.mp3" 396 | }, 397 | "™": { 398 | "speech": "simbolo trademark", 399 | "file": "simbolo_trademark.mp3" 400 | }, 401 | "¥": { 402 | "speech": "simbolo valuta Yen Giapponese", 403 | "file": "simbolo_valuta_yen_giapponese.mp3" 404 | }, 405 | "¿": { 406 | "speech": "punto di domanda invertito", 407 | "file": "punto_di_domanda_invertito.mp3" 408 | }, 409 | "¡": { 410 | "speech": "punto esclamativo invertito", 411 | "file": "punto_esclamativo_invertito.mp3" 412 | } 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | # API functions (DRAFT) 2 | 3 | ## `setup` 4 | 5 | ```javascript 6 | /** 7 | * setup 8 | * 9 | * @param {string} language language code ('en-us', 'it', ) 10 | * @param {String} codec audio coding format (wav/ogg/etc.) 11 | * @return {String} spelling character-by-character spelling mode (''/'nato'/etc.) 12 | */ 13 | ``` 14 | 15 | Usage: 16 | ```javascript 17 | const {setup} = require('jointts') 18 | setup('it', 'ogg', 'nato') 19 | ``` 20 | 21 | ### `ttsfile` 22 | The returned object is an audio file, lossless (e.g. `wav`) 23 | or in a compressed lossy compression format 24 | (e.g. [`ogg`](https://en.wikipedia.org/wiki/Opus_(audio_format))) 25 | 26 | ```javascript 27 | /** 28 | * ttsfile 29 | * 30 | * @param {String} text sentence to be spoken 31 | * @param {string} language language_code ('en-us', 'it', ) 32 | * @param {String} codec audio coding format (wav/ogg/etc.) 33 | * @return {String} filename name of audio file 34 | */ 35 | ``` 36 | 37 | Usage: 38 | ```javascript 39 | const {ttsfile} = require('jointts') 40 | const fileName = ttsfile('Container JL1349-76 has been cleared for pick-up.', 'en', 'ogg') 41 | ``` 42 | 43 | ## `ttsbuf` 44 | The returned object is a memory buffer in the above specified format. 45 | 46 | ```javascript 47 | /** 48 | * ttsbuf 49 | * 50 | * @param {String} text sentence to be spoken 51 | * @param {string} language language_code ('en-us', 'it', ) 52 | * @param {String} codec audio coding format (wav/ogg/etc.) 53 | * @return {buffer} 54 | */ 55 | ``` 56 | 57 | Usage: 58 | ```javascript 59 | const {ttsbuf} = require('jointts') 60 | const buffer = ttsbuf('Il container JL1349-76 è pronto per il ritiro.', 'it', 'ogg') 61 | ``` 62 | 63 | ## Run-time concatenation backend 64 | 65 | - File-level concatenation 66 | 67 | The quick&dirty approach is to use `ffmpeg` or `sox`, 68 | as a background process that create dynamic concatenations. 69 | 70 | See: 71 | 72 | - audio files concatenation using `ffmpeg`: 73 | - https://trac.ffmpeg.org/wiki/Concatenate 74 | - https://superuser.com/questions/587511/concatenate-multiple-wav-files-using-single-command-without-extra-file/1307384#1307384 75 | 76 | - audio files concatenation using `sox`: 77 | - https://superuser.com/questions/571463/how-do-i-append-a-bunch-of-wav-files-while-retaining-not-zero-padded-numeric 78 | - https://superuser.com/questions/64164/linux-command-to-concatenate-audio-files-and-output-them-to-ogg 79 | - https://stackoverflow.com/questions/10721089/combine-two-audio-files-with-a-command-line-tool 80 | - https://askubuntu.com/questions/20507/concatenating-several-mp3-files-into-one-mp3 81 | - http://sox.sourceforge.net/Docs/Documentation 82 | - http://sox.sourceforge.net/sox.html#EFFECTS 83 | 84 | - In-memory concatenation of audio buffers: 85 | 86 | that's the correct / fastest solution: working in-memory. 87 | It require having audio chunks as `PCM`, see: 88 | - https://github.com/benmangold/audio-concatenation 89 | - https://github.com/streamich/memfs 90 | 91 | --- 92 | 93 | [top](#) | [home](../README.md) 94 | -------------------------------------------------------------------------------- /doc/CLI.md: -------------------------------------------------------------------------------- 1 | # `jointts` command line program 2 | 3 | ```bash 4 | $ jointts 5 | 6 | jointts, brainless off-line concatenative text to speech 7 | v. 0.9.0, (C) Giorgio Robino 8 | 9 | Usage: 10 | 11 | jointts download gt download Google Translate TTS MP3 file 12 | jointts languages list of ISO-639-1 language codes (in Google Translate) 13 | jointts convert convert audio file codec 14 | ``` 15 | 16 | ## `jointts download gt` 17 | 18 | ```bash 19 | $ jointts download gt 20 | 21 | jointts, brainless off-line concatenative text to speech 22 | v. 0.9.0, (C) Giorgio Robino 23 | 24 | usage: 25 | 26 | jointts download gt \ 27 | --language= \ 28 | [--directory=] \ 29 | [--speed=] 30 | 31 | where: 32 | : language ISO code (it/en/etc.) 33 | see https://cloud.google.com/speech-to-text/docs/languages 34 | : path to audio files home directory. Default: your/path/audio 35 | : 1 = normal (default), 0.24 = slow, 0 = very slow 36 | 37 | examples: 38 | 39 | jointts download gt mi chiamo Giorgio --language=it 40 | jointts download gt mi chiamo Giorgio Robino e sono nato a Genova, in Italia. --language=it-IT --speed=0.2 41 | jointts download gt "my name is Giorgio Robino and I'm born in Genoa, Italy." --language=en 42 | jointts download gt "my name is Giorgio Robino" --language=en --directory=/home/giorgio/myproject/audio 43 | 44 | ``` 45 | 46 | ## `jointts languages` 47 | 48 | ```bash 49 | $ jointts languages 50 | 51 | jointts, brainless off-line concatenative text to speech 52 | v. 0.9.0, (C) Giorgio Robino 53 | 54 | CODE LANGUAGE 55 | ISO-639-1 NAME 56 | --------- ------------------- 57 | af Afrikaans 58 | sq Albanian 59 | am Amharic 60 | ar Arabic 61 | hy Armenian 62 | az Azerbaijani 63 | eu Basque 64 | be Belarusian 65 | bn Bengali 66 | .. ... 67 | ``` 68 | 69 | 70 | ## `jointts convert` 71 | 72 | ```bash 73 | $ jointts convert 74 | 75 | jointts, brainless off-line concatenative text to speech 76 | v. 0.9.0, (C) Giorgio Robino 77 | 78 | usage: 79 | 80 | jointts convert \ 81 | --format= \ 82 | 83 | where: 84 | : pcm | wav | opus | ogg | webm 85 | 86 | example: 87 | 88 | jointts convert /home/myproject/audio/mi_chiamo_Giorgio.mp3 --format=pcm 89 | -> /home/myproject/audio/mi_chiamo_Giorgio.mp3.pcm 90 | ``` 91 | 92 | --- 93 | 94 | [top](#) | [home](../README.md) 95 | -------------------------------------------------------------------------------- /doc/config.md: -------------------------------------------------------------------------------- 1 | # Characters 2 | 3 | 4 | ## Generate configuration file 5 | 6 | ```bash 7 | $ node lib/buildConfigJson.js 8 | ``` 9 | 10 | ``` 11 | config/ 12 | └── it 13 | ├── characters.json 14 | └── README.md 15 | ``` 16 | 17 | ## Generate audio files 18 | 19 | ```bash 20 | $ node lib/buildCharactersAudio.js 21 | ``` 22 | 23 | ``` 24 | audio/it/ 25 | ├── àcca.mp3 26 | ├── accento_acuto.mp3 27 | ├── accento_grave.mp3 28 | ├── á_con_accento_acuto.mp3 29 | ├── à_con_accento_grave.mp3 30 | ├── a.mp3 31 | ├── apostrofo.mp3 32 | ├── barra.mp3 33 | ├── barra_retroversa.mp3 34 | ├── barra_verticale.mp3 35 | ├── bi.mp3 36 | ├── càppa.mp3 37 | ├── chiocciola.mp3 38 | ├── ci.mp3 39 | ├── cinque.mp3 40 | ├── cu.mp3 41 | ├── delimitatore_di_paragrafo.mp3 42 | ├── delimitatore_di_sezione.mp3 43 | ├── di.mp3 44 | ├── dóppia_vu.mp3 45 | ├── due.mp3 46 | ├── due_punti.mp3 47 | ├── é_con_accento_acuto.mp3 48 | ├── è_con_accento_grave.mp3 49 | ├── èffe.mp3 50 | ├── èlle.mp3 51 | ├── èmme.mp3 52 | ├── é.mp3 53 | ├── ènne.mp3 54 | ├── èrre.mp3 55 | ├── èsse.mp3 56 | ├── gi.mp3 57 | ├── giorgio_robino_.mp3.opus 58 | ├── í_con_accento_acuto.mp3 59 | ├── ì_con_accento_grave.mp3 60 | ├── ics.mp3 61 | ├── i_lùnga.mp3 62 | ├── i.mp3 63 | ├── ìpsilon.mp3 64 | ├── mi_chiamo_giorgio_robino.mp3.ogg 65 | ├── mi_chiamo_giorgio_robino.mp3.opus 66 | ├── mi_chiamo_giorgio_robino.mp3.pcm 67 | ├── mi_chiamo_giorgio_robino.mp3.wav 68 | ├── mi_chiamo_giorgio_robino.mp3.webm 69 | ├── nove.mp3 70 | ├── ó_con_accento_acuto.mp3 71 | ├── ò_con_accento_grave.mp3 72 | ├── ò.mp3 73 | ├── otto.mp3 74 | ├── parentesi_graffa_aperta.mp3 75 | ├── parentesi_graffa_chiusa.mp3 76 | ├── parentesi_quadra_aperta.mp3 77 | ├── parentesi_quadra_chiusa.mp3 78 | ├── parentesi_tonda_aperta.mp3 79 | ├── parentesi_tonda_chiusa.mp3 80 | ├── pi.mp3 81 | ├── punto_centrale.mp3 82 | ├── punto_di_domanda_invertito.mp3 83 | ├── punto_esclamativo_invertito.mp3 84 | ├── punto_esclamativo.mp3 85 | ├── punto_e_virgola.mp3 86 | ├── punto_interrogativo.mp3 87 | ├── punto.mp3 88 | ├── quattro.mp3 89 | ├── README.md 90 | ├── sei.mp3 91 | ├── sette.mp3 92 | ├── simbolo_asterisco.mp3 93 | ├── simbolo_cancelletto.mp3 94 | ├── simbolo_cappelletto.mp3 95 | ├── simbolo_di_centesimo.mp3 96 | ├── simbolo_di_copyright.mp3 97 | ├── simbolo_di_divisione.mp3 98 | ├── simbolo_di_marchio_registrato.mp3 99 | ├── simbolo_e_commerciale.mp3 100 | ├── simbolo_maggiore.mp3 101 | ├── simbolo_micron.mp3 102 | ├── simbolo_minore.mp3 103 | ├── simbolo_percento.mp3 104 | ├── simbolo_più.mp3 105 | ├── simbolo_più_o_meno.mp3 106 | ├── simbolo_trademark.mp3 107 | ├── simbolo_uguale.mp3 108 | ├── simbolo_valuta_dollaro.mp3 109 | ├── simbolo_valuta_lira.mp3 110 | ├── simbolo_valuta_yen_giapponese.mp3 111 | ├── sottolineato.mp3 112 | ├── spazio.mp3 113 | ├── tabulazione.mp3 114 | ├── tilde.mp3 115 | ├── ti.mp3 116 | ├── trattino_lungo.mp3 117 | ├── trattino.mp3 118 | ├── tre.mp3 119 | ├── ú_con_accento_acuto.mp3 120 | ├── ù_con_accento_grave.mp3 121 | ├── u.mp3 122 | ├── uno.mp3 123 | ├── virgola.mp3 124 | ├── virgoletta_destra_inclinata.mp3 125 | ├── virgoletta_sinistra_inclinata.mp3 126 | ├── virgolette_destre_inclinate.mp3 127 | ├── virgolette_doppie_aperte.mp3 128 | ├── virgolette_doppie_chiuse.mp3 129 | ├── virgolette.mp3 130 | ├── virgolette_sinistre_inclinate.mp3 131 | ├── vu.mp3 132 | ├── zero.mp3 133 | └── zèta.mp3 134 | ``` 135 | 136 | --- 137 | 138 | [top](#) | [home](../README.md) 139 | -------------------------------------------------------------------------------- /doc/multilanguage.md: -------------------------------------------------------------------------------- 1 | # Multi-language 2 | 3 | Speech generation is language-dependent. 4 | The speech translation of any text depends on a specific natural language of reference. 5 | 6 | By example `123.45` is pronounced 7 | 8 | - in Italian: `uno due tre punto quattro cinque` (or: `centoventitré punto quarantacinque`) 9 | - in English: `one two three point four five` 10 | 11 | We use [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes 12 | to organize texts to be processed. 13 | 14 | Maybe with a function call: 15 | 16 | ```javascript 17 | const {ttsfile} = require('jointts') 18 | 19 | // generate the speech for a text in Italian language 20 | ttsfile('123.45', 'it') 21 | // -> '/some/path/it/uno_due_tre_punto_quattro_cinque.ogg' 22 | 23 | // the English language equivalent: 24 | ttsfile('123.45', 'en') 25 | // -> '/some/path/en/one_two_three_point_four_five.ogg' 26 | ``` 27 | 28 | Files are organized with a directory for each language: 29 | 30 | ``` 31 | ├── some/path/audio 32 | │   ├── it 33 | │   │   ├── uno_due_tre_punto_quattro_cinque.ogg 34 | │   │   ├── uno_due_tre_punto_quattro_sei.ogg 35 | │   │   └─── ti_amo.ogg 36 | │   ├── en 37 | │   │   ├── one_two_three_point_four_five.ogg 38 | │   │   ├── one_two_three_point_four_six.ogg 39 | │   │   └─── i_love_you.ogg 40 | │   ├── de 41 | │   │   ├── ...ogg 42 | │   │   ├── ...ogg 43 | 44 | ``` 45 | --- 46 | 47 | [top](#) | [home](../README.md) 48 | -------------------------------------------------------------------------------- /doc/segmentation.md: -------------------------------------------------------------------------------- 1 | # Text segmentation 2 | 3 | Input texts could be "segmented" (and configured) in different parts: characters, words, phrases. 4 | 5 | - Static phrases 6 | - Template literals 7 | - Word-by-word concatenation 8 | - Character-by-character spelling 9 | 10 | ## Static phrases 11 | 12 | It's the simplest scenario: you have a list of static, immutable, ready done, phrases. By example: 13 | ``` 14 | Looks like her company has three containers set to sail for tonight. 15 | ``` 16 | The above sentence corresponds to: 17 | 18 | - an audio (speech) file with some naming convention 19 | 20 | by example: `your/path/speech/en/looks_like_her_company_has_three_containers_set_to_sail_for_tonight.ogg` 21 | 22 | - or with a obscure name/UUID (maybe made with [nanonid](https://github.com/ai/nanoidu)) 23 | 24 | by example: `your/path/speech/en/V1StGXR8_Z5jdHi6B-myT.ogg`. 25 | 26 | ```javascript 27 | const {ttsfile} = require('jointts') 28 | const fileName = ttsfile('Looks like her company has three containers set to sail for tonight', 'en') 29 | // -> 'your/path/speech/en/looks_like_her_company_has_three_containers_set_to_sail_for_tonight.ogg' 30 | ``` 31 | 32 | ## Template literals 33 | 34 | Template literals are string literals allowing embedded expressions. 35 | They are static phrases containing also variable parts (entities) to be resolved at run-time. 36 | By example: 37 | ``` 38 | Container JL1349-76 has been cleared for pick-up. 39 | ``` 40 | 41 | in the sentence up here, the entity `JL1349-76` is a domain specific code 42 | (a shipping container code), to be spelled as a `{alphanumericCode}`. 43 | At the configuration level, a template literal could have a syntax like: 44 | ``` 45 | Container {alphanumericCode} has been cleared for pick-up. 46 | ``` 47 | 48 | That "template literal" be a concatenation of 3 strings component parts: 49 | - `Container `, a static string 50 | - `{alphanumericCode}`, an alphanumeric code to be spelled char-by-char 51 | - ` has been cleared for pick-up.`, a static string 52 | 53 | At run-time, the TTS translation function must recognize the template literal, 54 | concatenating the sequences. 55 | 56 | A possible API could be somthing like: 57 | 58 | ```javascript 59 | const {ttsfile} = require('jointts') 60 | const fileName = ttsfile( 61 | 'Container {alphanumericCode} has been cleared for pick-up.', 62 | 'en', 63 | { alphanumericCode: 'JL1349-76' } 64 | ) 65 | // -> 'Container JL1349-76 has been cleared for pick-up..ogg' 66 | ``` 67 | 68 | ## Words concatenation 69 | 70 | Phrases are built concatenating words and or letters 71 | (that is the general case of concatenative text-to-speech). 72 | Apparently it's a "worst case" because 73 | the system has to preset all the words of the grammar. 74 | 75 | In the example: 76 | ``` 77 | Container JL1349-76 has been cleared for pick-up. 78 | ``` 79 | 80 | The sentence could split in blank separated word tokens: 81 | ``` 82 | Container 83 | JL1349-76 84 | has 85 | been 86 | cleared 87 | for 88 | pick-up. 89 | ``` 90 | 91 | The downside of this approach is that having a file for each word, 92 | implies a run time concatenation, with possible unnatural sounding play, 93 | due to the static file sequencing. 94 | 95 | 96 | ## Character-by-character spelling 97 | 98 | joinTTS foresee a ready done letters/symbols set of audio files, 99 | for spoken spelling of single chars, acronyms, numbers, or alphanumeric codes, 100 | unknown words (not included in custom grammar): 101 | 102 | Examples (alphanumeric codes): 103 | ``` 104 | RAIU 690011 4 25 U1 105 | CSQU3054383 106 | 1006.760 107 | ``` 108 | 109 | > BTW, the two initial lines are container codes with ISO6346 format 110 | > (see my npm package [iso6347](https://github.com/solyarisoftware/iso6346)). 111 | 112 | In the case of an alphanumeric sequence of chars, 113 | the required spoken spelling is the concatenation of letter-by-letter speech. 114 | The original input text: 115 | ``` 116 | CSQU3054383 117 | ``` 118 | 119 | is spelled as sequence of single characters (separated by brief pauses): 120 | ``` 121 | C S Q U 3 0 5 4 3 8 3 122 | ``` 123 | 124 | Where the spelling of each character corresponds to the pseudo-phonetic spelling audio, depending on 125 | 126 | - the language 127 | - the specific [spelling alphabet](https://en.wikipedia.org/wiki/Spelling_alphabet). 128 | 129 | A basic TTS for English language could be 130 | ``` 131 | sii es kiu iu three zero five four three eight three 132 | ``` 133 | 134 | Whereas, using the NATO phonetic alphabet: 135 | ``` 136 | Charli for sii 137 | Sierra for es 138 | Quebec for kiu 139 | Uniform for iu 140 | three 141 | zero 142 | five 143 | four 144 | three 145 | eight 146 | three 147 | ``` 148 | 149 | or simply: 150 | ``` 151 | Charli 152 | Sierra 153 | Quebec 154 | Uniform 155 | three 156 | zero 157 | five 158 | four 159 | three 160 | eight 161 | three 162 | ``` 163 | 164 | --- 165 | 166 | [top](#) | [home](../README.md) 167 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Listen examples 2 | 3 | ## Alphanumeric codes spelling 4 | 5 | Here below some alphanumeric codes spelling examples, in Italian language.
6 | See [Text segmentation](doc/segmentation.md) doc. 7 | 8 | ### 🛠 Steps to run examples (WORK IN PROGRESS) 9 | 10 | 1. Edit your characters configuration file in your language, by example in Italian, in English: 11 | ```bash 12 | config/it/characters.json 13 | config/en/characters.json 14 | ``` 15 | - Alphabet in json file is no case sensitive. 16 | - Long spelling: each letter is spelled with the phonetic word code [Alfabetico telefonico Italiano](https://it.wikipedia.org/wiki/Alfabeto_telefonico_italiano). 17 | 18 | 2. Prepare a inter-char pause file using com/pause utility, e.g. 19 | ```bash 20 | com/pause PAUSE.mp3 300 21 | ``` 22 | The above script create the file `audio/PAUSE.mp3` as an inter-char pause of ~350 msecs. 23 | Note that MP3 codec doesn't allow a perfect timing in milliseconds, adding ~50msecs. 24 | 25 | 3. Build characters speech files 26 | ```bash 27 | node lib/buildCharactersAudio.js --language=it 28 | node lib/buildCharactersAudio.js --language=en 29 | ``` 30 | 31 | 4. Run the run-time concatenation to produce the TTS spelling for a desired word/sentence, 32 | by examples: 33 | 34 | ```bash 35 | node lib/charByChar --language=it --text=CSQU3054383 36 | node lib/charByChar --language=en --text=CSQU3054383 37 | 38 | node lib/charbychar --text='RAIU 690011 4 25 U1' --language=it 39 | node lib/charbychar --text='RAIU 690011 4 25 U1' --language=en 40 | 41 | node lib/charbychar --text='JL1349-76 [45A/MU4]' --language=it 42 | node lib/charbychar --text='JL1349-76 [45A/MU4]' --language=en 43 | ``` 44 | 45 | ### 👂 Listen rendered audio files 46 | 47 | - 🎧 🇮🇹 [`CSQU3054383`](it/CSQU3054383.mp3) 48 | 49 | - Italian language 50 | - The alphanumeric code (a shipping container code in a partial ISO6346 format) contains letters and digits 51 | - Short spelling: each letter is spelled shortly 52 | - No case sensitive 53 | - To listen from CLI with command: 54 | ```bash 55 | com/play examples/CSQU3054383.mp3 56 | ``` 57 | 58 | - 🎧 🇮🇹 [`CSQU3054383`](it/CSQU3054383_long.mp3) 59 | 60 | - Italian language 61 | - The alphanumeric code (a shipping container code in a partial ISO6346 format) contains letters and digits 62 | - Long spelling 63 | - No case sensitive 64 | - To listen from CLI with command: 65 | ```bash 66 | com/play examples/it/CSQU3054383_long.mp3 67 | ``` 68 | 69 | - 🎧 🇬🇧 [`CSQU3054383`](en/CSQU3054383.mp3) 70 | 71 | - English language 72 | - The alphanumeric code (a shipping container code in a partial ISO6346 format) contains letters and digits 73 | - Long spelling 74 | - No case sensitive 75 | - To listen from CLI with command: 76 | ```bash 77 | com/play examples/en/CSQU3054383.mp3 78 | ``` 79 | 80 | - 🎧 🇮🇹 [`RAIU 690011 4 25 U1`](it/RAIU%20690011%204%2025%20U1.mp3) 81 | 82 | - Italian language 83 | - The alphanumeric code (a shipping container code in a complete ISO6346 format) contains letters: 84 | `a` `A` `B` `b` etc., digits: `1` `2` etc., blanks: ` ` 85 | - Long spelling 86 | - No case sensitive 87 | - To listen from CLI with command: 88 | ```bash 89 | com/play 'examples/it/RAIU 690011 4 25 U1.mp3' 90 | ``` 91 | 92 | - 🎧 🇬🇧 [`RAIU 690011 4 25 U1`](en/RAIU_690011_4_25_U1.mp3) 93 | 94 | - English language 95 | - The alphanumeric code (a shipping container code in a complete ISO6346 format) contains letters: 96 | `a` `A` `B` `b` etc., digits: `1` `2` etc., blanks: ` ` 97 | - Long spelling 98 | - No case sensitive 99 | - To listen from CLI with command: 100 | ```bash 101 | com/play 'examples/en/RAIU_690011_4_25_U1.mp3' 102 | ``` 103 | 104 | - 🎧 🇮🇹 [`JL1349-76 [45A/MU4]`](it/JL1349-76%20%5B45AslashMU4%5D.mp3) 105 | 106 | - Italian language 107 | - The alphanumeric code contains letters, digits, blanks and special symbols as: `-` `/` `[` `]` 108 | - No case sensitive 109 | - To listen from CLI with command: 110 | ```bash 111 | com/play 'examples/it/JL1349-76 [45AslashMU4].mp3' 112 | ``` 113 | 114 | - 🎧 🇬🇧 [`JL1349-76 [45A/MU4]`](en/JL1349-76_%5B45A_slash_MU4%5D.mp3) 115 | 116 | - English language 117 | - The alphanumeric code contains letters, digits, blanks and special symbols as: `-` `/` `[` `]` 118 | - No case sensitive 119 | - To listen from CLI with command: 120 | ```bash 121 | com/play 'examples/en/JL1349-76_[45AslashMU4].mp3' 122 | ``` 123 | 124 | --- 125 | 126 | [top](#) | [home](../README.md) 127 | -------------------------------------------------------------------------------- /examples/en/CSQU3054383.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/examples/en/CSQU3054383.mp3 -------------------------------------------------------------------------------- /examples/en/JL1349-76_[45A_slash_MU4].mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/examples/en/JL1349-76_[45A_slash_MU4].mp3 -------------------------------------------------------------------------------- /examples/en/RAIU_690011_4_25_U1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/examples/en/RAIU_690011_4_25_U1.mp3 -------------------------------------------------------------------------------- /examples/it/CSQU3054383.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/examples/it/CSQU3054383.mp3 -------------------------------------------------------------------------------- /examples/it/CSQU3054383_long.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/examples/it/CSQU3054383_long.mp3 -------------------------------------------------------------------------------- /examples/it/JL1349-76 [45AslashMU4].mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/examples/it/JL1349-76 [45AslashMU4].mp3 -------------------------------------------------------------------------------- /examples/it/RAIU 690011 4 25 U1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solyarisoftware/jointts/de74ff2a1b0e1712d39a0a72cc6bc3dbecadc18b/examples/it/RAIU 690011 4 25 U1.mp3 -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * setup 3 | * 4 | * @param {string} language language code ('en-us', 'it', ) 5 | * @param {String} codec audio coding format (wav/ogg/etc.) 6 | * @return {String} spelling character-by-character spelling mode (''/'nato'/etc.) 7 | * 8 | * @example 9 | * const {setup} = require('jointts') 10 | * setup('it', 'ogg', 'nato') 11 | * 12 | */ 13 | function setup( language, codec, spelling ) { 14 | } 15 | 16 | /** 17 | * ttsfile 18 | * 19 | * The returned object is an audio file, lossless (e.g. `wav`) 20 | * or in a compressed lossy compression format 21 | * 22 | * @param {String} text sentence to be spoken 23 | * @param {string} language language_code ('en-us', 'it', ) 24 | * @param {String} codec audio coding format (wav/ogg/etc.) 25 | * 26 | * @return {String} filename name of audio file 27 | * 28 | * @example 29 | * const {ttsfile} = require('jointts') 30 | * const fileName = ttsfile('Container JL1349-76 has been cleared for pick-up.', 'en', 'ogg') 31 | */ 32 | function ttsFile( text, language, codec, filename ) { 33 | } 34 | 35 | 36 | /** 37 | * ttsbuf 38 | * WARNING 39 | * TODO 40 | * 41 | * The returned object is a memory buffer in the above specified format. 42 | * 43 | * @param {String} text sentence to be spoken 44 | * @param {string} language language_code ('en-us', 'it', ) 45 | * @param {String} codec audio coding format (wav/ogg/etc.) 46 | * 47 | * @return {Buffer} 48 | * 49 | * @example 50 | * const {ttsbuf} = require('jointts') 51 | * const buffer = ttsbuf('Il container JL1349-76 è pronto per il ritiro.', 'it', 'ogg') 52 | */ 53 | function ttsBuffer( text, language, codec ) { 54 | } 55 | 56 | 57 | module.exports = { 58 | setup, 59 | ttsFile, 60 | ttsBuffer 61 | 62 | } 63 | -------------------------------------------------------------------------------- /lib/audioFilenameFromText.js: -------------------------------------------------------------------------------- 1 | //const path = require('path') 2 | const { sanitizeFilename } = require('./sanitizeFilename') 3 | 4 | //const AUDIO_FILES_DIRECTORY = `${path.resolve('.')}/audio` 5 | const AUDIO_FILES_DIRECTORY = `${__dirname}/audio` 6 | const AUDIO_FILES_FORMAT = 'mp3' 7 | const AUDIO_FILES_LANGUAGE = 'it' 8 | 9 | const DEFAULT_CONFIG = { 10 | homedirectory: undefined, 11 | suffix: AUDIO_FILES_FORMAT, 12 | language: undefined 13 | } 14 | 15 | /** 16 | * audioFilename 17 | * 18 | * return the audio file name corresponding to a text 19 | * 20 | * @param {String} text 21 | * @param {String} directory home directory of all audio files 22 | * @param {String} suffix of the file. Identify the audio format 23 | * @param {String} language ISO code 24 | * 25 | */ 26 | function audioFilename( text, configVariant ) { 27 | 28 | const config = { 29 | ...DEFAULT_CONFIG, 30 | ...configVariant 31 | } 32 | 33 | // 34 | // TODO 35 | // validate attributes 36 | // 37 | // validate chars in text. 38 | // Considering linux filesystems rules, any char is valid except NULL and SLASH. 39 | // See: https://unix.stackexchange.com/questions/230291/what-characters-are-valid-to-use-in-filenames 40 | // 41 | //const sanitizedText = text 42 | // .replaceAll('\0', 'NULL') 43 | // .replaceAll('/', 'SLASH') 44 | 45 | // build filename fullpath 46 | const homedirectory = config.homedirectory ? config.homedirectory + '/' : '' 47 | const language = config.language ? config.language + '/' : '' 48 | const name = `${sanitizeFilename(text)}.${config.suffix}` 49 | 50 | return `${homedirectory}${language}${name}` 51 | 52 | } 53 | 54 | 55 | /** 56 | * unit test main 57 | */ 58 | function test() { 59 | 60 | const texts = [ 61 | ' ', 62 | '\0', 63 | '/a', 64 | 'á con accento acuto', 65 | 'à con accento grave', 66 | '89/56 [ABC43@RT]', 67 | 'giorgio robino' 68 | ] 69 | 70 | 71 | console.log( 'texts:' ) 72 | console.log( texts ) 73 | console.log() 74 | 75 | console.log( 'audio files names options:' ) 76 | 77 | console.log( texts.map(text => audioFilename(text)) ) 78 | 79 | console.log( texts.map(text => audioFilename(text, {homedirectory: '', language: 'it'})) ) 80 | 81 | console.log( texts.map(text => audioFilename(text, { 82 | homedirectory: AUDIO_FILES_DIRECTORY, 83 | language: AUDIO_FILES_LANGUAGE, 84 | suffix: AUDIO_FILES_FORMAT 85 | })) ) 86 | 87 | console.log() 88 | } 89 | 90 | 91 | if (require.main === module) 92 | test() 93 | 94 | // exports public function 95 | module.exports = { audioFilename } 96 | 97 | -------------------------------------------------------------------------------- /lib/buildCharactersAudio.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const { googleTranslateTTS } = require('./googleTranslateTTS') 5 | const { validIsoCode } = require('./googleTranslateLanguages') 6 | const { sleep } = require('./sleep') 7 | const { getArgs } = require('./getArgs') 8 | 9 | /** 10 | * buildCharsAudio 11 | * 12 | * build TTS audio files for each character in configuration file: 13 | * your/home/path/config/ 14 | * using Google Translate TTS API, 15 | * 16 | * Configuration file: 17 | * config 18 | * └── it 19 | * └── characters.json 20 | * 21 | * Audio files created: 22 | * audio/it 23 | * ├── àcca.mp3 24 | * ├── accento_acuto.mp3 25 | * ├── accento_grave.mp3 26 | * ├── à_con_accento_grave.mp3 27 | * ├── a.mp3 28 | * ├── apostrofo.mp3 29 | * ├── barra.mp3 30 | * ├── barra_retroversa.mp3 31 | * ├── barra_verticale.mp3 32 | * ├── bi.mp3 33 | * ├── ... 34 | * ├── ... 35 | * 36 | * @param {String} language ISO code 37 | * 38 | */ 39 | async function buildCharsAudio(language) { 40 | 41 | if ( !validIsoCode(language) ) 42 | return console.error(`ERROR: ${language} is not a valid Google Translate ISO code.`) 43 | 44 | // 45 | // read characters.json config file 46 | // 47 | const configHomeDirectory = `${path.resolve(__dirname, '..')}/config` 48 | const configFilePath = `${configHomeDirectory}/${language}/characters.json` 49 | const characterSet = JSON.parse(fs.readFileSync(configFilePath)) 50 | 51 | const characters = characterSet.characters 52 | const audioFilePath = `${characterSet.homedirectory}/${characterSet.language}` 53 | 54 | //console.log(characters) 55 | console.log('Configuration: ' + configFilePath) 56 | 57 | // 58 | // produce an audio file for each character, 59 | // following configuration settings 60 | // 61 | for (const char in characters) { 62 | 63 | const audioFileName = `${audioFilePath}/${characters[char].file}` 64 | const speech = characters[char].speech 65 | 66 | console.info() 67 | console.info(`char : ${char}`) 68 | console.info(`speech: ${speech}`) 69 | console.info(`file : ${audioFileName}`) 70 | 71 | for (;;) { 72 | try { 73 | // call translate TTS API 74 | const result = await googleTranslateTTS(speech, audioFileName, language) 75 | 76 | console.info(`created file ${audioFileName} in ${result.time}`) 77 | break 78 | } 79 | catch(error) { 80 | console.error(`ERROR generating googleTransalteTTS for character ${char}: ${error}`) 81 | // sleep for a second before retrying 82 | await sleep(900) 83 | } 84 | } 85 | 86 | // sleep for a while before next character 87 | await sleep(900) 88 | } 89 | 90 | } 91 | 92 | /** 93 | * checkArgs 94 | * command line parsing 95 | * 96 | * @param {String} args 97 | * @param {String} programName 98 | * 99 | * @returns {SentenceAndAttributes} 100 | * @typedef {Object} SentenceAndAttributes 101 | * @property {String} language 102 | * 103 | */ 104 | function checkArgs(args, programName) { 105 | 106 | const language = args.language 107 | 108 | if ( !language ) 109 | helpAndExit(programName) 110 | 111 | return { language } 112 | } 113 | 114 | 115 | function helpAndExit(programName) { 116 | console.log() 117 | console.log('usage:') 118 | console.log() 119 | console.log(` ${programName} --language=`) 120 | console.log() 121 | console.log(' where:') 122 | console.log(' : language ISO code (it/en/etc.)') 123 | console.log(' see https://cloud.google.com/speech-to-text/docs/languages') 124 | console.log() 125 | console.log('example:') 126 | console.log() 127 | console.log(` ${programName} --language=it`) 128 | console.log() 129 | process.exit(1) 130 | } 131 | 132 | 133 | /** 134 | * unit test 135 | */ 136 | async function main() { 137 | 138 | const { args } = getArgs() 139 | const { language } = checkArgs(args, 'node lib/buildCharactersAudio') 140 | 141 | await buildCharsAudio(language) 142 | 143 | } 144 | 145 | if (require.main === module) 146 | main() 147 | 148 | module.exports = { buildCharsAudio } 149 | 150 | -------------------------------------------------------------------------------- /lib/buildConfigJson.js: -------------------------------------------------------------------------------- 1 | // italian character set 2 | const { CHARACTER_SIMPLIFIED } = require('./characterSetIt') 3 | const { audioFilename } = require('./audioFilenameFromText') 4 | 5 | const AUDIO_FILES_FORMAT = 'mp3' 6 | 7 | /** 8 | * buildConfigJson 9 | * 10 | * build configuration JSON file 11 | * 12 | * @param {String} homedirectory 13 | * @param {language} language two letters ISO code 14 | * @param {Object} characterSet 15 | * @param {String} file suffix 16 | * 17 | */ 18 | function buildConfigJson(homedirectory, language, characterSet, suffix) { 19 | 20 | const characters = {} 21 | 22 | for (const item in characterSet) { 23 | 24 | const speech = characterSet[item] 25 | 26 | characters[item] = { 27 | speech, 28 | file: audioFilename( speech, { suffix } ) 29 | //file: audioFilename(speech, { homedirectory: 'audio', language: 'it'} ) 30 | } 31 | 32 | } 33 | 34 | const config = { 35 | 36 | homedirectory, 37 | language, 38 | characters 39 | //words: {}, 40 | //phrases: {} 41 | 42 | } 43 | 44 | console.log( JSON.stringify(config, null, 4) ) 45 | 46 | } 47 | 48 | 49 | function main() { 50 | console.log() 51 | console.log( 'node lib/buildConfigJsonIt > config/it/characters.json' ) 52 | console.log() 53 | console.log('JSON file:') 54 | console.log() 55 | buildConfigJson('audio', 'it', CHARACTER_SIMPLIFIED, AUDIO_FILES_FORMAT ) 56 | } 57 | 58 | 59 | if (require.main === module) 60 | main() 61 | 62 | module.exports = { 63 | buildConfigJson 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /lib/characterSetIt.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * ITALIAN LANGUAGE ALPHABET 4 | * 5 | * @see 6 | * https://it.wikipedia.org/wiki/Alfabeto_telefonico_italiano 7 | * https://it.wikipedia.org/wiki/Alfabeto_fonetico_NATO 8 | * https://corsidia.com/materia/web-design/caratterispecialihtml 9 | * https://it.wikipedia.org/wiki/Compitazione 10 | * 11 | */ 12 | 13 | const LETTER_SEMPLIFIED = { 14 | 'a': 'a', 15 | 'b': 'bi', 16 | 'c': 'ci', 17 | 'd': 'di', 18 | 'e': 'é', 19 | 'f': 'èffe', 20 | 'g': 'gi', 21 | 'h': 'àcca', 22 | 'i': 'i', 23 | 'j': 'i lùnga', 24 | 'k': 'càppa', 25 | 'l': 'èlle', 26 | 'm': 'èmme', 27 | 'n': 'ènne', 28 | 'o': 'ò', 29 | 'p': 'pi', 30 | 'q': 'cu', 31 | 'r': 'èrre', 32 | 's': 'èsse', 33 | 't': 'ti', 34 | 'u': 'u', 35 | 'v': 'vu', 36 | 'w': 'dóppia vu', 37 | 'x': 'ics', 38 | 'y': 'ìpsilon', 39 | 'z': 'zèta', 40 | } 41 | 42 | const LETTER_SPELLING_ITALIAN_PHONE_ALPHABET = { 43 | 'a': 'a come Ancona', 44 | 'b': 'bi come Bari', //'Bologna', 45 | 'c': 'ci come Como', 46 | 'd': 'di come Domodossola', 47 | 'e': 'é come Empoli', 48 | 'f': 'èffe come Firenze', 49 | 'g': 'gi come Genova', 50 | 'h': 'àcca come hotel', 51 | 'i': 'i come Imola', 52 | 'j': 'i lùnga come Jolly', // Jesolo 53 | 'k': 'càppa come kursaal', 54 | 'l': 'èlle come Livorno', 55 | 'm': 'èmme come Milano', 56 | 'n': 'ènne come Napoli', 57 | 'o': 'ò come Otranto', 58 | 'p': 'pi come Palermo', //'Padova', 59 | 'q': 'cu come Quarto', //'Québec', 60 | 'r': 'èrre come Roma', 61 | 's': 'èsse come Savona', //'Salerno', 62 | 't': 'ti come Torino', //'Taranto', 63 | 'u': 'u come Udine', 64 | 'v': 'vu come Venezia',//'vi', 65 | 'w': 'dóppia vu come Washington', //'vi doppia', 'doppia vu', 'doppia vi', 66 | 'x': 'ics come xilofono', //'xeres'] ], 67 | 'y': 'ìpsilon come yogurt', //'York', 'yacht'] ],, //'i greca'], 68 | 'z': 'zèta come Zara', 69 | } 70 | 71 | const DIACRITIC_MARK_SIMPLIFIED = { 72 | 'á': 'á con accento acuto', 73 | 'à': 'à con accento grave', 74 | 'é': 'é con accento acuto', 75 | 'è': 'è con accento grave', 76 | 'í': 'í con accento acuto', 77 | 'ì': 'ì con accento grave', 78 | 'ó': 'ó con accento acuto', 79 | 'ò': 'ò con accento grave', 80 | 'ú': 'ú con accento acuto', 81 | 'ù': 'ù con accento grave', 82 | } 83 | 84 | const DIGIT = { 85 | '1': 'uno', 86 | '2': 'due', 87 | '3': 'tre', 88 | '4': 'quattro', 89 | '5': 'cinque', 90 | '6': 'sei', 91 | '7': 'sette', 92 | '8': 'otto', 93 | '9': 'nove', 94 | '0': 'zero', 95 | } 96 | 97 | const DIACRITIC_MARK_COMPLETE = { 98 | 'á': 'a minuscola con accento acuto', 99 | 'Á': 'A maiuscola con accento acuto', 100 | 'à': 'a minuscola con accento grave', 101 | 'À': 'A maiuscola con accento grave', 102 | 'â': 'a minuscola con accento circonflesso', 103 | 'Â': 'A maiuscola con accento circonflesso', 104 | 'å': 'a minuscola con anello o occhiello', 105 | 'Å': 'A maiuscola con anello o occhiello', 106 | 'ã': 'a minuscola con tilde', 107 | 'Ã': 'A maiuscola con tilde', 108 | 'ä': 'a minuscola con dieresi', 109 | 'Ä': 'A maiuscola con dieresi', 110 | 'æ': 'ae minuscola con legatura fonetica', 111 | 'Æ': 'AE maiuscola con legatura fonetica', 112 | 'ç': 'c minuscola con cediglia', 113 | 'Ç': 'C maiuscola con cediglia', 114 | 'é': 'e minuscola con accento acuto', 115 | 'É': 'E maiuscola con accento acuto', 116 | 'è': 'e minuscola con accento grave', 117 | 'È': 'E maiuscola con accento grave', 118 | 'ê': 'e minuscola con accento circonflesso', 119 | 'Ê': 'E maiuscola con accento circonflesso', 120 | 'ë': 'e minuscola con dieresi ', 121 | 'Ë': 'E maiuscola con dieresi ', 122 | 'í': 'I minuscola con accento acuto', 123 | 'Í': 'I maiuscola con accento acuto', 124 | 'ì': 'i minuscola con accento grave', 125 | 'Ì': 'I maiuscola con accento grave', 126 | 'î': 'i minuscola con accento circonflesso', 127 | 'Î': 'I maiuscola con accento circonflesso', 128 | 'ï': 'i minuscola con diaeresis', 129 | 'Ï': 'I maiuscola con diaeresis', 130 | 'ñ': 'n minuscola con tilde', 131 | 'Ñ': 'N maiuscola con tilde', 132 | 'ó': 'o minuscola con accento acuto', 133 | 'Ó': 'O maiuscola con accento acuto', 134 | 'ò': 'o minuscola con accento grave', 135 | 'Ò': 'O maiuscola con accento grave', 136 | 'ô': 'o minuscola con accento circonflesso', 137 | 'Ô': 'O maiuscola con accento circonflesso', 138 | 'ø': 'o minuscola barrata', 139 | 'Ø': 'O maiuscola barrata', 140 | 'õ': 'o minuscola con tilde', 141 | 'Õ': 'O maiuscola con tilde', 142 | 'ö': 'o minuscola con dieresi', 143 | 'Ö': 'O maiuscola con dieresi', 144 | 'ú': 'u minuscola con accento acuto', 145 | 'Ú': 'U maiuscola con accento acuto', 146 | 'ù': 'u minuscola con accento grave', 147 | 'Ù': 'U maiuscola con accento grave', 148 | 'û': 'u minuscola con accento circonflesso', 149 | 'Û': 'U maiuscola con accento circonflesso', 150 | 'ü': 'u minuscola con dieresi', 151 | 'Ü': 'U maiuscola con dieresi', 152 | 'ß': 'simbolo beta', 153 | 'ÿ': 'y minuscola con dieresi', 154 | } 155 | 156 | const PUNTUACTIONS_SYMBOLS = { 157 | ' ': 'spazio', 158 | '\t': 'tabulazione', 159 | '.': 'punto', 160 | '·': 'punto centrale', 161 | ',': 'virgola', 162 | ';': 'punto e virgola', 163 | ':': 'due punti', 164 | '!': 'punto esclamativo', 165 | '?': 'punto interrogativo', 166 | 167 | '’': 'virgoletta destra inclinata', 168 | '‘': 'virgoletta sinistra inclinata', 169 | 170 | '"': 'virgolette', // 'virgolette doppie'], 171 | '\'': 'apostrofo', // 'virgolette singole'], 172 | '´': 'accento acuto', // apostrofo senza lettera 173 | '`': 'accento grave', // 'apostro retroverso', apostropo inverso senza lettera 174 | 175 | '”': 'virgolette destre inclinate', 176 | '“': 'virgolette sinistre inclinate', 177 | 178 | '«': 'virgolette doppie aperte', // virgolette sinistre in stile europeo 179 | '»': 'virgolette doppie chiuse', // virgolette destre in stile europeo 180 | 181 | '(': 'parentesi tonda aperta', 182 | ')': 'parentesi tonda chiusa', 183 | 184 | '[': 'parentesi quadra aperta', 185 | ']': 'parentesi quadra chiusa', 186 | 187 | '{': 'parentesi graffa aperta', 188 | '}': 'parentesi graffa chiusa', 189 | 190 | '@': 'chiocciola', 191 | '*': 'simbolo asterisco', 192 | '#': 'simbolo cancelletto', 193 | '%': 'simbolo percento', 194 | 195 | '|': 'barra verticale', 196 | '/': 'barra', 197 | '\\': 'barra retroversa', 198 | 199 | '£': 'simbolo valuta lira', 200 | '$': 'simbolo valuta dollaro', 201 | '&': 'simbolo e commerciale', 202 | '^': 'simbolo cappelletto', 203 | '=': 'simbolo uguale', 204 | '-': 'trattino', // 'lineetta', 'meno'], 205 | '+': 'simbolo più', 206 | '>': 'simbolo maggiore', 207 | '<': 'simbolo minore', 208 | '~': 'tilde', 209 | 210 | '—': 'trattino lungo', 211 | '_': 'sottolineato', // 'sottolineatura', 'trattino basso'], 212 | '¢': 'simbolo di centesimo', 213 | '©': 'simbolo di copyright', 214 | '÷': 'simbolo di divisione', 215 | 'µ': 'simbolo micron', 216 | '¶': 'delimitatore di paragrafo', 217 | '±': 'simbolo più o meno', 218 | '®': 'simbolo di marchio registrato', 219 | '§': 'delimitatore di sezione', 220 | '™': 'simbolo trademark', 221 | '¥': 'simbolo valuta Yen Giapponese', 222 | '¿': 'punto di domanda invertito', 223 | '¡': 'punto esclamativo invertito', 224 | } 225 | 226 | 227 | const LETTER_COMPLETE = { 228 | // 229 | // Letters 230 | // 231 | 'a': 'a minuscola', //'Ancona'], 232 | 'b': 'b minuscola', //'bi', //['Bari', 'Bologna']], 233 | 'c': 'c minuscola', //'ci', //'Como'], 234 | 'd': 'd minuscola', //'di', //'Domodossola'], 235 | 'e': 'e minuscola', //'e', //'Empoli'], 236 | 'f': 'f minuscola', //'effe', //'Firenze'], 237 | 'g': 'g minuscola', //'gi', //'Genova'], 238 | 'h': 'h minuscola', //'acca', //'hotel'], 239 | 'i': 'i minuscola', //'i', //'Imola'], 240 | 'j': 'j minuscola', //'i lunga', //'Imola'], 241 | 'k': 'k minuscola', //'cappa', //'kursaal'], 242 | 'l': 'l minuscola', //'elle', //'Livorno'], 243 | 'm': 'm minuscola', //'emme', //'Milano'], 244 | 'n': 'n minuscola', //'enne', //'Napoli'], 245 | 'o': 'o minuscola', //'o', //'Otranto'], 246 | 'p': 'p minuscola', //'pi', //['Palermo', 'Padova'] ], 247 | 'q': 'q minuscola', //'cu', //['Quarto', 'Québec'] ], 248 | 'r': 'r minuscola', //'erre', //'Roma'], 249 | 's': 's minuscola', //'esse', //['Savona', 'Salerno'] ], 250 | 't': 'ti minuscola', //'ti', //['Torino', 'Taranto'] ], 251 | 'u': 'u minuscola', //'u', //'Udine' ], 252 | 'v': 'v minuscola', //'vu', //'vi'], 'Venezia' ], 253 | 'w': 'w minuscola', //'vu doppia', //'vi doppia', 'doppia vu', 'doppia vi'], 'Washington' ], 254 | 'x': 'x minuscola', //'ics', //['xilofono', 'xeres'] ], 255 | 'y': 'y minuscola', //'ipsilon', //'i greca'], ['yogurt', 'York', 'yacht'] ], 256 | 'z': 'z minuscola', //'zeta', //'Zara'], 257 | 258 | 'A': 'A maiuscola', //'Ancona'], 259 | 'B': 'B maiuscola', //'bi', //['Bari', 'Bologna']], 260 | 'C': 'C maiuscola', //'ci', //'Como'], 261 | 'D': 'D maiuscola', //'di', //'Domodossola'], 262 | 'E': 'E maiuscola', //'e', //'Empoli'], 263 | 'F': 'F maiuscola', //'effe', //'Firenze'], 264 | 'G': 'G maiuscola', //'gi', //'Genova'], 265 | 'H': 'H maiuscola', //'acca', //'hotel'], 266 | 'I': 'I maiuscola', //'i', //'Imola'], 267 | 'J': 'J maiuscola', //'i lunga', //'Imola'], 268 | 'K': 'K maiuscola', //'cappa', //'kursaal'], 269 | 'L': 'L maiuscola', //'elle', //'Livorno'], 270 | 'M': 'M maiuscola', //'emme', //'Milano'], 271 | 'N': 'N maiuscola', //'enne', //'Napoli'], 272 | 'O': 'O maiuscola', //'o', //'Otranto'], 273 | 'P': 'P maiuscola', //'pi', //['Palermo', 'Padova'] ], 274 | 'Q': 'Q maiuscola', //'cu', //['Quarto', 'Québec'] ], 275 | 'R': 'R maiuscola', //'erre', //'Roma'], 276 | 'S': 'S maiuscola', //'esse', //['Savona', 'Salerno'] ], 277 | 'T': 'Ti maiuscola', //'ti', //['Torino', 'Taranto'] ], 278 | 'U': 'U maiuscola', //'u', //'Udine' ], 279 | 'V': 'V maiuscola', //'vu', //'vi'], 'Venezia' ], 280 | 'W': 'W maiuscola', //'vu doppia', //'vi doppia', 'doppia vu', 'doppia vi'], 'Washington' ], 281 | 'X': 'X maiuscola', //'ics', //['xilofono', 'xeres'] ], 282 | 'Y': 'Y maiuscola', //'ipsilon', //'i greca'], ['yogurt', 'York', 'yacht'] ], 283 | 'Z': 'Z maiuscola', //'zeta', //'Zara'], 284 | } 285 | 286 | const CHARACTER_SIMPLIFIED = { 287 | ...LETTER_SEMPLIFIED, 288 | ...DIACRITIC_MARK_SIMPLIFIED, 289 | ...DIGIT, 290 | ...PUNTUACTIONS_SYMBOLS 291 | } 292 | 293 | 294 | const CHARACTER_COMPLETE = { 295 | ...LETTER_COMPLETE, 296 | ...DIACRITIC_MARK_COMPLETE, 297 | ...DIGIT, 298 | ...PUNTUACTIONS_SYMBOLS 299 | } 300 | 301 | 302 | 303 | function lookupDescription(char, simplifiedMode) { 304 | 305 | let description 306 | 307 | if (simplifiedMode) { 308 | description = CHARACTER_SIMPLIFIED[char] 309 | if (description) 310 | return description 311 | } 312 | 313 | description = CHARACTER_COMPLETE[char] 314 | if (simplifiedMode) 315 | return description 316 | else 317 | return char // null / char + ': carattere non riconosciuto' 318 | } 319 | 320 | 321 | 322 | /* 323 | * text to speech with spelling 324 | * 325 | * @public 326 | * @param {String} word 327 | * @param {Boolean} fromASR 328 | */ 329 | function spelling(word, simplified=true) { 330 | 331 | const charSet = simplified ? CHARACTER_SIMPLIFIED : CHARACTER_COMPLETE 332 | 333 | const arrayOfChars = word.toLowerCase().split('') 334 | 335 | return arrayOfChars 336 | .map( char => charSet[char] ) 337 | .join(' ') 338 | } 339 | 340 | 341 | /* 342 | * for unit test 343 | */ 344 | function main() { 345 | 346 | let sentence = 'alfaBetO%DF' 347 | 348 | const sentenceArray = sentence.split('') 349 | 350 | console.log() 351 | console.log(sentence) 352 | console.log() 353 | 354 | for (const char of sentenceArray) 355 | console.log( lookupDescription(char, false) ) 356 | 357 | console.log() 358 | 359 | for (const char of sentenceArray) 360 | console.log( lookupDescription(char, true) ) 361 | 362 | console.log() 363 | 364 | sentence = '1006.760' 365 | console.log( sentence ) 366 | console.log( spelling(sentence) ) 367 | 368 | sentence = 'CSQU3054383' 369 | console.log( sentence ) 370 | console.log( spelling(sentence) ) 371 | 372 | sentence = 'JL1349-76' 373 | console.log( sentence ) 374 | console.log( spelling(sentence) ) 375 | 376 | sentence = 'RAIU 690011 4 25 U1' 377 | console.log( sentence ) 378 | console.log( spelling(sentence) ) 379 | 380 | console.log() 381 | } 382 | 383 | if (require.main === module) 384 | main() 385 | 386 | module.exports = { 387 | CHARACTER_SIMPLIFIED, 388 | CHARACTER_COMPLETE 389 | } 390 | 391 | 392 | -------------------------------------------------------------------------------- /lib/characters.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // 5 | // set the configuration home directory path 6 | // 7 | const configHomeDirectory = `${path.resolve(__dirname, '..')}/config` 8 | 9 | let CHARACTERSET = {} 10 | 11 | 12 | /** 13 | * loadConfig 14 | * 15 | * load JSON file in memory 16 | * 17 | * @param {String} language ISO code 18 | * @return {Object} characters.json as js object 19 | * 20 | */ 21 | function loadConfig(language) { 22 | 23 | const configFilePath = `${configHomeDirectory}/${language}/characters.json` 24 | 25 | return (CHARACTERSET = JSON.parse(fs.readFileSync(configFilePath))) 26 | 27 | } 28 | 29 | 30 | /** 31 | * audioFilePath 32 | * 33 | * returns the filename of a character speech file, reading the dictionary in memory. 34 | * does not check if the file exists. 35 | * 36 | * @param {String} char 37 | * @param {String} language 38 | * @param {String} dictionary configuration dictionary TODO 39 | * @return {String} filename fullpath 40 | * 41 | */ 42 | function audioFilePath (char, language, dictionary=CHARACTERSET) { 43 | 44 | // verify that language code JSON attribute corresponds to that requested 45 | if (language !== dictionary.language) { 46 | console.error(`ERROR in audioFilePath: language ${language} doesn't match dictionary language attrribute ${dictionary.language}`) 47 | return null 48 | } 49 | 50 | // TODO 51 | // validate if char exists in the dictionary. 52 | if ( !dictionary.characters[char] ) { 53 | console.error(`ERROR in audioFilePath: char ${char} not found in ${language} dictionary`) 54 | return null 55 | } 56 | 57 | const audioFilePath = `${dictionary.homedirectory}/${dictionary.language}` 58 | const filename = `${audioFilePath}/${dictionary.characters[char].file}` 59 | return filename 60 | 61 | } 62 | 63 | /** 64 | * unit test 65 | */ 66 | function main() { 67 | console.log( loadConfig('it') ) 68 | console.log() 69 | console.log( audioFilePath('昨', 'it') ) 70 | console.log( audioFilePath('@', 'it') ) 71 | console.log( audioFilePath('z', 'it') ) 72 | } 73 | 74 | if (require.main === module) 75 | main() 76 | 77 | module.exports = { 78 | loadConfig, 79 | audioFilePath 80 | } 81 | -------------------------------------------------------------------------------- /lib/charbychar.js: -------------------------------------------------------------------------------- 1 | const { concatAudiofiles } = require('./concatAudioFiles') 2 | const { loadConfig, audioFilePath } = require('./characters.js') 3 | const { sanitizeFilename } = require('./sanitizeFilename') 4 | const { getArgs } = require('./getArgs') 5 | 6 | 7 | /** 8 | * charByChar 9 | * 10 | * the text is spelled, pronouncing each char in sequence. 11 | * The returned object is an audio file. 12 | * 13 | * @param {String} text sentence to be spoken 14 | * @param {string} language language_code ('en-us', 'it', ) 15 | * @param {String} filename name of audio file 16 | * @param {Number} interCharPause blank to be inserted between chars 17 | * 18 | * @return {String} filename name of audio file 19 | * 20 | */ 21 | function charByChar( text, language, filename, interCharPauseFile ) { 22 | 23 | const inputFilenames = text 24 | // create and array iof characters from the string 25 | .toLowerCase() 26 | .split('') 27 | 28 | // assign a filename for each character (the spelling speech) 29 | .map( char => audioFilePath(char, language) ) 30 | 31 | // insert a pause file after each character spelling speech 32 | .map( i => [i, interCharPauseFile] ) 33 | .flat() 34 | 35 | // concatenate all files 36 | return concatAudiofiles( inputFilenames, filename ) 37 | } 38 | 39 | 40 | function helpAndExit(programName) { 41 | console.log() 42 | console.log('usage:') 43 | console.log() 44 | console.log(` ${programName} \\ `) 45 | console.log(' --language= \\ ') 46 | console.log(' --text= \\ ') 47 | console.log(' [--pause=]') 48 | console.log() 49 | console.log(' where:') 50 | console.log(' : language ISO code (it/en/etc.)') 51 | console.log(' see https://cloud.google.com/speech-to-text/docs/languages') 52 | console.log(' : text you want to render in speech') 53 | console.log(' : silence file for the inter char pause') 54 | console.log() 55 | console.log('example:') 56 | console.log() 57 | console.log(` ${programName} --text=CQH-7865 --language=it`) 58 | console.log(' -> audio/it/CQH-7865.mp3') 59 | console.log() 60 | process.exit(1) 61 | } 62 | 63 | 64 | /** 65 | * checkArgs 66 | * command line parsing 67 | * 68 | * @param {String} args 69 | * @param {String} programName 70 | * 71 | * @returns {SentenceAndAttributes} 72 | * @typedef {Object} SentenceAndAttributes 73 | * @property {String} language 74 | * 75 | */ 76 | function checkArgs(args, programName) { 77 | 78 | const language = args.language 79 | const text = args.text 80 | let pause = args.pause 81 | const interCharPauseFileDefault = 'audio/PAUSE.mp3' 82 | 83 | if ( !language ) 84 | helpAndExit(programName) 85 | 86 | if ( !text ) 87 | helpAndExit(programName) 88 | 89 | if ( !pause ) { 90 | console.log(`pause file not supplied. I'll Use: ${interCharPauseFileDefault}`) 91 | pause = interCharPauseFileDefault 92 | } 93 | 94 | return { language, text, pause } 95 | } 96 | 97 | 98 | /** 99 | * unit test 100 | */ 101 | async function main() { 102 | 103 | //const language = 'en' 104 | //const text = 'CSQU3054383' 105 | //const text = 'RAIU 690011 4 25 U1' 106 | //const text = 'JL1349-76 [45A/MU4]' 107 | //const interCharPauseFile = 'audio/PAUSE.mp3' 108 | 109 | const { args } = getArgs() 110 | const { language, text, pause } = checkArgs(args, 'node lib/charByChar') 111 | 112 | const filename = sanitizeFilename(text) 113 | const fullPathFileName = `audio/${language}/${filename}.mp3` 114 | 115 | loadConfig(language) 116 | 117 | charByChar( text, language, fullPathFileName, pause ) 118 | .then( result => { 119 | 120 | if (result.exit == 0) { 121 | console.info() 122 | console.info( `text : ${text}` ) 123 | console.info( `pause : ${pause}` ) 124 | console.info( `language : ${language}` ) 125 | console.info( `filename : ${fullPathFileName}` ) 126 | console.info( result ) 127 | } 128 | else 129 | console.error( `ERROR charByChar.concatAudiofiles: ${JSON.stringify(result, null, 2)}` ) 130 | 131 | }) 132 | .catch( error => console.error(error) ) 133 | 134 | } 135 | 136 | if (require.main === module) 137 | main() 138 | 139 | module.exports = { 140 | charByChar, 141 | spelling: charByChar 142 | } 143 | 144 | -------------------------------------------------------------------------------- /lib/concatAudioFiles.js: -------------------------------------------------------------------------------- 1 | const { spawnAsync } = require('./spawn') 2 | const { fileExtension, fileNameConcat } = require('./fileHelpers') 3 | 4 | const DEBUG = false 5 | 6 | /* 7 | * - File-level concatenation 8 | * 9 | * The quick&dirty approach is to use `ffmpeg` or `sox`, 10 | * as a background process that create dynamic concatenations. 11 | * 12 | * - audio files concatenation using `ffmpeg`: 13 | * - https://trac.ffmpeg.org/wiki/Concatenate 14 | * - https://superuser.com/questions/587511/concatenate-multiple-wav-files-using-single-command-without-extra-file/1307384#1307384 15 | * 16 | * - audio files concatenation using `sox`: 17 | * - https://superuser.com/questions/571463/how-do-i-append-a-bunch-of-wav-files-while-retaining-not-zero-padded-numeric 18 | * - https://superuser.com/questions/64164/linux-command-to-concatenate-audio-files-and-output-them-to-ogg 19 | * - https://stackoverflow.com/questions/10721089/combine-two-audio-files-with-a-command-line-tool 20 | * - https://askubuntu.com/questions/20507/concatenating-several-mp3-files-into-one-mp3 21 | * - http://sox.sourceforge.net/Docs/Documentation 22 | * - http://sox.sourceforge.net/sox.html#EFFECTS 23 | * 24 | * - In-memory concatenation of audio buffers: 25 | * 26 | * that's the correct / fastest solution: working in-memory. 27 | * It require having audio chunks as `PCM`, see: 28 | * - https://github.com/benmangold/audio-concatenation 29 | * - https://github.com/streamich/memfs 30 | */ 31 | 32 | 33 | /** 34 | * concatAudiofiles 35 | * 36 | * basic files concatenation, using ffmpeg "concat", 37 | * applicable with audio files having the same codec. 38 | * 39 | * The concat protocol works at the file level. 40 | * Certain files (MPEG-2 transport streams, possibly others) can be concatenated. 41 | * This is analogous to using cat on UNIX-like systems. 42 | * 43 | * All MPEG codecs (MPEG-4 Part 10 / AVC, MPEG-4 Part 2, MPEG-2 Video, 44 | * MPEG-1 Audio Layer II, MPEG-2 Audio Layer III (MP3), MPEG-4 Part III (AAC)) 45 | * are supported in the MPEG-TS container format. 46 | * 47 | * @see https://trac.ffmpeg.org/wiki/Concatenate 48 | * @example 49 | * ffmpeg -loglevel panic \ 50 | * -i "concat:audio/mi_chiamo_.mp3|audio/giorgio_robino_.mp3" \ 51 | * -c copy audio/concatenationresult.mp3 -y 52 | * 53 | * @param {string[]} inputFilenames array of filenames 54 | * @param {string} outputFilename 55 | * @return {Promise} object returned by the ffmpeg spawned process, plus the attribute outputFilename 56 | */ 57 | async function concatAudiofiles( inputFilenames, outputFilename=fileNameConcat(inputFilenames) ) { 58 | 59 | if (DEBUG) { 60 | console.log() 61 | console.log( `input files: ${inputFilenames.join(', ')}` ) 62 | console.log( 'output file: ' + outputFilename ) 63 | } 64 | 65 | // TODO 66 | // VALIDATION of inputFilenames: need a check verifying if filenames have the same codecs (at least the same suffix) 67 | 68 | const firstFile = inputFilenames[0] 69 | const suffix = fileExtension(firstFile) 70 | 71 | // verify if all file externsions are equal 72 | if( ! inputFilenames.every( filename => fileExtension(filename) === suffix ) ) 73 | return Promise.reject(`ERROR on concatAudiofiles: inputFilenames (${inputFilenames.join(', ')}) have NOT the same suffix.`) 74 | 75 | // WARNING 76 | // ffmpeg error handling weird behaviour: 77 | // ffmpeg do not riase any error if 78 | // - one or more files to be concatenated are not in the same codec 79 | // - or if the files exists! 80 | 81 | const args = [ 82 | '-loglevel', 'panic', 83 | '-i', `concat:${inputFilenames.join('|')}`, 84 | '-c', 'copy', 85 | outputFilename, 86 | '-y' 87 | ] 88 | 89 | const spawnResult = await spawnAsync('ffmpeg', args) 90 | 91 | const result = {...spawnResult, ...{outputFilename}} 92 | 93 | //console.info( result ) 94 | 95 | // add attribute outputFilename to the object returned by spawnAsync 96 | //return await Promise.resolve( result ) 97 | return result 98 | } 99 | 100 | 101 | /** 102 | * unit test main 103 | */ 104 | async function main() { 105 | 106 | // 107 | // to generate input files: 108 | // node lib/googleTranslateTTS.js "mi chiamo " --language=it 109 | // node lib/googleTranslateTTS.js "Giorgio Robino " --language=it 110 | // 111 | const inputs = [ 112 | 'audio/it/mi_chiamo_.mp3', 113 | 'audio/it/Giorgio_Robino_.mp3' 114 | ] 115 | 116 | 117 | try { 118 | const result = await concatAudiofiles( inputs /*, 'audio/it/concatexample.mp3'*/ ) 119 | 120 | // console.info( result ) 121 | 122 | if (result.exit === 0) 123 | console.info( result ) 124 | else 125 | console.error( `ERROR (concatAudiofiles): ${result}` ) 126 | } 127 | catch(error) { 128 | console.error(error) 129 | } 130 | 131 | } 132 | 133 | 134 | if (require.main === module) 135 | main() 136 | 137 | // exports public function 138 | module.exports = { concatAudiofiles } 139 | 140 | -------------------------------------------------------------------------------- /lib/convertAudioFormat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module convertcodecs 3 | * convert an audio file to PCM audio codec, using ffmpeg 4 | * convert an audio file to WAV audio codec, using ffmpeg 5 | * convert an audio file to WEBM audio codec, using ffmpeg 6 | * convert an audio file to OGG audio codec, using ffmpeg 7 | * convert an audio file to OPUS audio codec, using ffmpeg 8 | * 9 | * @author giorgio.robino@gmail.com 10 | * 11 | * @see 12 | * https://www.stefaanlippens.net/audio_conversion_cheat_sheet/ 13 | * 14 | * @install sudo apt-get install libopus0 opus-tools ffmpeg 15 | */ 16 | const fs = require('fs') 17 | const { spawnAsync } = require('./spawn') 18 | const { sleep } = require('./sleep') 19 | const { getArgs } = require('./getArgs') 20 | 21 | /** 22 | * toOpus 23 | * Convert an input audio/video file to OPUS audio codec, using ffmpeg 24 | * 25 | * @see https://opus-codec.org/ 26 | * @see https://en.wikipedia.org/wiki/WebM 27 | * @public 28 | * 29 | * @param {String} inputFile filename 30 | * @param {String} [suffix=opus] file suxxix, default: opus, also used: opus 31 | * @returns {Promise} status of ffmpeg spawned process 32 | */ 33 | function toOpus(inputFile, suffix='opus') { 34 | 35 | // add a suffic to the OPUS audio output filename 36 | const opusFile = `${inputFile}.${suffix}` 37 | 38 | /** 39 | * Basic conversion to OPUS format (using defaults) 40 | * ffmpeg -loglevel panic -i $AUDIO_FILE -c libopus $WAV_FILE -y 41 | * 42 | * Converstion to OPUS format, with some tunings for voice 43 | * https://stackoverflow.com/questions/38185598/how-to-convert-an-mp3-file-to-an-ogg-opus-file 44 | * ffmpeg -loglevel panic -i $AUDIO_FILE -c:a libopus -compression_level 10 -frame_duration 60 -vbr -application voip $OGG_AUDIO_FILE -y 45 | */ 46 | const ARGS = [ 47 | '-loglevel', 'panic', 48 | '-i', inputFile, 49 | 50 | '-c:a', 'libopus', 51 | 52 | // 16 Khz 53 | '-ar', '16000', 54 | 55 | '-compression_level', '10', 56 | '-frame_duration', '60', 57 | '-vbr', 'on', 58 | '-application', 'voip', 59 | 60 | opusFile, 61 | '-y' 62 | ] 63 | 64 | return spawnAsync('ffmpeg', ARGS) 65 | } 66 | 67 | 68 | /** 69 | * toPcm 70 | * Convert an input audio/video file to PCM format, using ffmpeg 71 | * 72 | * @public 73 | * @param {String} inputFile filename 74 | * @returns {Promise} status of ffmpeg spawned process 75 | */ 76 | function toPcm(inputFile) { 77 | 78 | // wav audio output filename 79 | const pcmFile = inputFile + '.pcm' 80 | 81 | // ffmpeg -i wav/one.wav -acodec pcm_s16le -f s16le -ac 1 -ar 44100 pcm/one.pcm 82 | const args = [ 83 | '-loglevel', 'panic', 84 | 85 | '-i', inputFile, 86 | 87 | '-ac', '1', 88 | 89 | '-acodec', 'pcm_s16le', 90 | 91 | '-f', 's16le', 92 | 93 | '-ar', '16000', 94 | 95 | pcmFile, 96 | '-y' 97 | ] 98 | 99 | return spawnAsync('ffmpeg', args) 100 | } 101 | 102 | /** 103 | * toWav 104 | * Convert an nput audio/video file to wav format, using ffmpeg 105 | * 106 | * @public 107 | * @param {String} inputFile filename 108 | * @param {Number} [mode=16] values: 8 -> ARGS_ or 16 (default) i-> ARGS_16 109 | * @returns {Promise} status of ffmpeg spawned process 110 | */ 111 | function toWav(inputFile, mode=16) { 112 | 113 | // wav audio output filename 114 | const wavFile = inputFile + '.wav' 115 | 116 | /** 117 | * ARGS_8: 8 bit 8KHz 118 | * 119 | * ffmpeg -loglevel panic -i $AUDIO_FILE -ac 1 -acodec pcm_u8 -ar 8000 $WAV_FILE -y 120 | */ 121 | const ARGS_8 = [ 122 | '-loglevel', 'panic', 123 | '-i', inputFile, 124 | 125 | // loudness normalization 126 | '-filter:a', 'loudnorm', 127 | 128 | // mono 129 | '-ac', '1', 130 | 131 | // 8 bits 132 | '-acodec', 'pcm_u8', 133 | 134 | // 8 KHz 135 | '-ar', '8000', 136 | 137 | wavFile, 138 | '-y' 139 | ] 140 | 141 | /** 142 | * ARGS_16: 16 bit 16KHz 143 | * 144 | * ffmpeg -loglevel panic -i $AUDIO_FILE -ac 1 -ar 16000 $WAV_FILE -y 145 | */ 146 | const ARGS_16 = [ 147 | '-loglevel', 'panic', 148 | '-i', inputFile, 149 | 150 | // loudness normalization 151 | //'-filter:a', 'loudnorm', 152 | // https://developers.google.com/actions/tools/audio-loudness#using_ffmpeg 153 | //'-af', 'loudnorm=I=-19:dual_mono=true:TP=-1.5:LRA=11', 154 | 155 | // mono 156 | '-ac', '1', 157 | 158 | // 16 bits 159 | // https://stackoverflow.com/a/19073622/1786393 160 | '-acodec', 'pcm_s16le', 161 | 162 | // 16 Khz 163 | '-ar', '16000', 164 | 165 | wavFile, 166 | '-y' 167 | ] 168 | 169 | // default mode is ARGS_16 170 | const args = (mode == 8) ? ARGS_8 : ARGS_16 171 | 172 | //const result = await spawnAsync('ffmpeg', args) 173 | return spawnAsync('ffmpeg', args) 174 | } 175 | 176 | function helpAndExit(programName) { 177 | console.log('usage:') 178 | console.log() 179 | console.log(` ${programName} \\ `) 180 | console.log(' --format= \\ ') 181 | console.log() 182 | console.log(' where:') 183 | console.log(' : pcm | wav | opus | ogg | webm') 184 | console.log() 185 | console.log('example:') 186 | console.log() 187 | console.log(` ${programName} /home/myproject/audio/mi_chiamo_Giorgio.mp3 --format=pcm`) 188 | console.log(' -> /home/myproject/audio/mi_chiamo_Giorgio.mp3.pcm') 189 | console.log() 190 | process.exit(1) 191 | } 192 | 193 | 194 | /** 195 | * checkArgs 196 | * command line parsing 197 | * 198 | * @returns {FileAndAttributes} 199 | * @typedef {Object} FileAndAttributes 200 | * @property {String} filename 201 | * @property {String} format 202 | * 203 | */ 204 | function checkArgs(commands, args, programName) { 205 | 206 | const filename = commands[0] 207 | const suffix = args.format 208 | 209 | if ( !suffix && !commands.length ) 210 | helpAndExit(programName) 211 | 212 | if ( !fs.existsSync(filename) ) { 213 | console.error(`\nERROR: input filename: ${filename} doesn't exist.`) 214 | process.exit(1) 215 | } 216 | 217 | if (! ['pcm', 'wav', 'opus', 'ogg', 'webm'].includes(suffix) ) { 218 | console.error(`\nERROR on convertAudioFormat: suffix ${suffix} uncovered. Allowed: pcm, wav, opus, ogg, webm.`) 219 | process.exit(1) 220 | } 221 | 222 | // return validated values 223 | return { filename, suffix } 224 | 225 | } 226 | 227 | 228 | /** 229 | * convertAudioFormat 230 | * Convert an input audio/video file to using ffmpeg 231 | * 232 | * @public 233 | * @param {String} commands 234 | * @param {String} args 235 | * @param {String} programName 236 | * @returns {Promise} status of ffmpeg spawned process 237 | */ 238 | function convertAudioFormat(commands, args, programName) { 239 | 240 | const { filename, suffix } = checkArgs(commands, args, programName) 241 | 242 | switch (suffix) { 243 | case 'pcm': 244 | return toPcm(filename) 245 | case 'wav': 246 | return toWav(filename) 247 | case 'opus': 248 | return toOpus(filename, 'opus') 249 | case 'ogg': 250 | return toOpus(filename, 'ogg') 251 | case 'webm': 252 | return toOpus(filename, 'webm') 253 | default: 254 | console.error(`ERROR on convertAudioFormat: suffix ${suffix} invalid.`) 255 | } 256 | 257 | } 258 | 259 | /** 260 | * for unit test 261 | * @private 262 | */ 263 | async function testSequential() { 264 | 265 | // 266 | // generate an input MP3 audio file: 267 | // node lib/googleTranslateTTS mi chiamo Giorgio --language=it 268 | // 269 | const MP3_AUDIO_FILE = 'audio/it/mi_chiamo_giorgio_robino.mp3' 270 | 271 | console.log ( `\ntoOpus (from ${MP3_AUDIO_FILE} to ${MP3_AUDIO_FILE}.opus)` ) 272 | 273 | toOpus(MP3_AUDIO_FILE, 'opus') 274 | .then( result => { 275 | 276 | if (result.exit == 0) 277 | console.log( result ) 278 | else 279 | console.error( `command ${result.fullcmd} failed with exit code: ${result.exit}` ) 280 | 281 | }) 282 | .catch( data => console.log( `command failed with error. See: ${data}` )) 283 | 284 | await sleep(2000) 285 | 286 | console.log ( `\ntoOpus (from ${MP3_AUDIO_FILE} to ${MP3_AUDIO_FILE}.ogg)` ) 287 | toOpus(MP3_AUDIO_FILE, 'ogg') 288 | .then( result => { 289 | 290 | if (result.exit == 0) 291 | console.log( result ) 292 | else 293 | console.error( `command ${result.fullcmd} failed with exit code: ${result.exit}` ) 294 | 295 | }) 296 | .catch( data => console.log( `command failed with error. See: ${data}` )) 297 | 298 | await sleep(2000) 299 | 300 | console.log ( `\ntoOpus (from ${MP3_AUDIO_FILE} to ${MP3_AUDIO_FILE}.webm)` ) 301 | toOpus(MP3_AUDIO_FILE, 'webm') 302 | .then( result => { 303 | 304 | if (result.exit == 0) 305 | console.log( result ) 306 | else 307 | console.error( `command ${result.fullcmd} failed with exit code: ${result.exit}` ) 308 | 309 | }) 310 | .catch( data => console.log( `command failed with error. See: ${data}` )) 311 | 312 | await sleep(2000) 313 | 314 | console.log ( `\ntoWav (from ${MP3_AUDIO_FILE} to ${MP3_AUDIO_FILE}.wav)` ) 315 | 316 | toWav(MP3_AUDIO_FILE, 'wav') 317 | .then( result => { 318 | 319 | if (result.exit == 0) 320 | console.log( result ) 321 | else 322 | console.error( `command ${result.fullcmd} failed with exit code: ${result.exit}` ) 323 | 324 | }) 325 | .catch( data => console.log( `command failed with error. See: ${data}` )) 326 | 327 | await sleep(2000) 328 | 329 | console.log ( `\ntoPcm (from ${MP3_AUDIO_FILE} to ${MP3_AUDIO_FILE}.pcm)` ) 330 | 331 | toPcm(MP3_AUDIO_FILE, 'pcm') 332 | .then( result => { 333 | 334 | if (result.exit == 0) 335 | console.log( result ) 336 | else 337 | console.error( `command ${result.fullcmd} failed with exit code: ${result.exit}` ) 338 | 339 | }) 340 | .catch( data => console.log( `command failed with error. See: ${data}` )) 341 | } 342 | 343 | /** 344 | * unit test main 345 | */ 346 | function test() { 347 | 348 | // get command line args and commands 349 | const { commands, args } = getArgs() 350 | 351 | convertAudioFormat(commands, args, 'node convertAudioFormat') 352 | .then( result => { 353 | 354 | if (result.exit == 0) 355 | console.log( result ) 356 | else 357 | console.error( `command ${result.fullcmd} failed with exit code: ${result.exit}` ) 358 | 359 | }) 360 | .catch( data => console.log( `command failed with error. See: ${data}` )) 361 | 362 | } 363 | 364 | 365 | if (require.main === module) 366 | test() 367 | 368 | module.exports = { 369 | toPcm, 370 | toWav, 371 | toOpus, 372 | convertAudioFormat 373 | } 374 | 375 | -------------------------------------------------------------------------------- /lib/elapsed.js: -------------------------------------------------------------------------------- 1 | //https://nodejs.org/api/process.html#process_process_hrtime_time 2 | const {sleep} = require('./sleep') 3 | 4 | class Elapsed { 5 | 6 | /** 7 | * @returns {this} 8 | */ 9 | constructor() { 10 | this.hrbegin = process.hrtime() 11 | } 12 | 13 | /** 14 | * @returns {Array} 15 | */ 16 | begin() { 17 | this.hrbegin = process.hrtime() 18 | } 19 | 20 | /** 21 | * @returns {Array} 22 | */ 23 | stop() { 24 | return process.hrtime(this.hrbegin) 25 | } 26 | 27 | /** 28 | * elapsed 29 | * in seconds plus milliseconds 30 | * 31 | * @returns {String} 32 | */ 33 | elapsedInSecsPlusMsecs() { 34 | const hrend = this.stop() 35 | 36 | const sec = hrend[0] 37 | const msec = Math.round( hrend[1] / 1000000 ) //.toFixed(3) 38 | 39 | return `${sec}s ${msec}ms` 40 | } 41 | 42 | /** 43 | * elapsed 44 | * in milliseconds and approximation in seconds 45 | * 46 | * @returns {String} 47 | */ 48 | elapsed() { 49 | const hrend = this.stop() 50 | 51 | const secsInMsecs = hrend[0] * 1000 52 | const msecs = Math.round( hrend[1] / 1000000 ) //.toFixed(3) 53 | 54 | // duration in milliseconds 55 | const duration = secsInMsecs + msecs 56 | 57 | return `${Math.round(duration)}ms~${Math.round(duration/1000)}s` 58 | } 59 | 60 | } 61 | 62 | 63 | 64 | /** 65 | * for unit test 66 | * @private 67 | */ 68 | async function main() { 69 | const time = new Elapsed() 70 | 71 | await sleep(1042) 72 | 73 | //console.log(time.str()) 74 | console.log( time.elapsed() ) 75 | } 76 | 77 | 78 | /** 79 | * unit test 80 | */ 81 | if (require.main === module) main() 82 | 83 | module.exports = Elapsed 84 | 85 | -------------------------------------------------------------------------------- /lib/fileHelpers.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | 3 | // https://stackoverflow.com/a/12900504/1786393 4 | function fileExtension(path) { 5 | 6 | const basename = path.split(/[\\/]/).pop() // extract file name from full path ... 7 | // (supports `\\` and `/` separators) 8 | const pos = basename.lastIndexOf('.') // get last position of `.` 9 | 10 | if (basename === '' || pos < 1) // if file name is empty or ... 11 | return '' // `.` not found (-1) or comes first (0) 12 | 13 | return basename.slice(pos + 1) // extract extension ignoring `.` 14 | 15 | } 16 | 17 | 18 | //Extract the filename: 19 | function fileBasename(fullPathName) { 20 | return path.basename(fullPathName) 21 | } 22 | 23 | //Extract the filename: 24 | function fileName(fullPathName) { 25 | return path.basename(fullPathName).split('.')[0] 26 | } 27 | 28 | 29 | function filePath(fullPathName) { 30 | return path.dirname(fullPathName) 31 | } 32 | 33 | /** 34 | * fileNameConcat 35 | * outputFilename is made joining names of inputFilenames 36 | * 37 | * @param {string[]} inputFilenames array of filenames 38 | * @returns {string} status of ffmpeg spawned process 39 | */ 40 | function fileNameConcat(inputFilenames) { 41 | 42 | const firstFile = inputFilenames[0] 43 | const suffix = fileExtension(firstFile) 44 | 45 | const path = filePath(firstFile) 46 | const name = inputFilenames 47 | .map( fname => fileName(fname) ) 48 | .join('') 49 | 50 | return `${path}/${name}.${suffix}` 51 | 52 | } 53 | 54 | function main() { 55 | 56 | console.log( '\'' + fileExtension('/path/to/name') + '\'' ) 57 | console.log( '\'' + fileExtension('/path/to/file.mp3') + '\'' ) 58 | console.log( '\'' + fileExtension('/path/to/name.with.many.dots.mp3.opus') + '\'' ) 59 | console.log() 60 | console.log( filePath('/path/to/name.with.many.dots.mp3.opus') ) 61 | console.log( fileBasename('/path/to/name.with.many.dots.mp3.opus') ) 62 | console.log( fileName('/path/to/name.with.many.dots.mp3.opus') ) 63 | console.log() 64 | console.log( fileNameConcat(['/path/en/first.mp3.opus', '/path/en/second.mp3.opus']) ) 65 | } 66 | 67 | 68 | /** 69 | * unit test 70 | */ 71 | if (require.main === module) main() 72 | 73 | module.exports = { 74 | fileExtension, 75 | fileName, 76 | fileNameConcat, 77 | fileBasename, 78 | filePath 79 | } 80 | -------------------------------------------------------------------------------- /lib/getArgs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * get command line arguments 3 | * 4 | * @see https://stackoverflow.com/a/54098693/1786393 5 | * 6 | */ 7 | function getArgs () { 8 | const commands = [] 9 | const args = {} 10 | process.argv 11 | .slice(2, process.argv.length) 12 | .forEach( arg => { 13 | // long arg 14 | if (arg.slice(0,2) === '--') { 15 | const longArg = arg.split('=') 16 | const longArgFlag = longArg[0].slice(2,longArg[0].length) 17 | const longArgValue = longArg.length > 1 ? longArg[1] : true 18 | args[longArgFlag] = longArgValue 19 | } 20 | // flags 21 | else if (arg[0] === '-') { 22 | const flags = arg.slice(1,arg.length).split('') 23 | flags.forEach(flag => { 24 | args[flag] = true 25 | }) 26 | } 27 | else 28 | commands.push(arg) 29 | }) 30 | return { args, commands } 31 | } 32 | 33 | 34 | // test 35 | if (require.main === module) { 36 | 37 | // node getArgs test --dir=examples/getUserName --start=getUserName.askName 38 | console.log( getArgs() ) 39 | } 40 | 41 | module.exports = { getArgs } 42 | -------------------------------------------------------------------------------- /lib/googleTranslateLanguages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Google language codes: 3 | * @see https://cloud.google.com/speech-to-text/docs/languages 4 | */ 5 | const GoogleTranslateLanguages = { 6 | 'af': 'Afrikaans', 7 | 'sq': 'Albanian', 8 | 'am': 'Amharic', 9 | 'ar': 'Arabic', 10 | 'hy': 'Armenian', 11 | 'az': 'Azerbaijani', 12 | 'eu': 'Basque', 13 | 'be': 'Belarusian', 14 | 'bn': 'Bengali', 15 | 'bs': 'Bosnian', 16 | 'bg': 'Bulgarian', 17 | 'ca': 'Catalan', 18 | 'ceb': 'Cebuano',// (ISO-639-2) 19 | 'zh-CN': 'Chinese (Simplified)', 20 | 'zh': 'Chinese (Traditional)', // (BCP-47) 21 | 'zh-TW': 'Chinese (Traditional)', // (BCP-47) 22 | 'co': 'Corsican', 23 | 'hr': 'Croatian', 24 | 'cs': 'Czech', 25 | 'da': 'Danish', 26 | 'nl': 'Dutch', 27 | 'en': 'English', 28 | 'eo': 'Esperanto', 29 | 'et': 'Estonian', 30 | 'fi': 'Finnish', 31 | 'fr': 'French', 32 | 'fy': 'Frisian', 33 | 'gl': 'Galician', 34 | 'ka': 'Georgian', 35 | 'de': 'German', 36 | 'el': 'Greek', 37 | 'gu': 'Gujarati', 38 | 'ht': 'Haitian Creole', 39 | 'ha': 'Hausa', 40 | 'haw': 'Hawaiian', // (ISO-639-2) 41 | 'he': 'Hebrew', 42 | 'iw': 'Hebrew', 43 | 'hi': 'Hindi', 44 | 'hmn': 'Hmong', // (ISO-639-2) 45 | 'hu': 'Hungarian', 46 | 'is': 'Icelandic', 47 | 'ig': 'Igbo', 48 | 'id': 'Indonesian', 49 | 'ga': 'Irish', 50 | 'it': 'Italian', 51 | 'ja': 'Japanese', 52 | 'jv': 'Javanese', 53 | 'kn': 'Kannada', 54 | 'kk': 'Kazakh', 55 | 'km': 'Khmer', 56 | 'rw': 'Kinyarwanda', 57 | 'ko': 'Korean', 58 | 'ku': 'Kurdish', 59 | 'ky': 'Kyrgyz', 60 | 'lo': 'Lao', 61 | 'la': 'Latin', 62 | 'lv': 'Latvian', 63 | 'lt': 'Lithuanian', 64 | 'lb': 'Luxembourgish', 65 | 'mk': 'Macedonian', 66 | 'mg': 'Malagasy', 67 | 'ms': 'Malay', 68 | 'ml': 'Malayalam', 69 | 'mt': 'Maltese', 70 | 'mi': 'Maori', 71 | 'mr': 'Marathi', 72 | 'mn': 'Mongolian', 73 | 'my': 'Myanmar (Burmese)', 74 | 'ne': 'Nepali', 75 | 'no': 'Norwegian', 76 | 'ny': 'Nyanja (Chichewa)', 77 | 'or': 'Odia (Oriya)', 78 | 'ps': 'Pashto', 79 | 'fa': 'Persian', 80 | 'pl': 'Polish', 81 | 'pt': 'Portuguese (Portugal, Brazil)', 82 | 'pa': 'Punjabi', 83 | 'ro': 'Romanian', 84 | 'ru': 'Russian', 85 | 'sm': 'Samoan', 86 | 'gd': 'Scots Gaelic', 87 | 'sr': 'Serbian', 88 | 'st': 'Sesotho', 89 | 'sn': 'Shona', 90 | 'sd': 'Sindhi', 91 | 'si': 'Sinhala (Sinhalese)', 92 | 'sk': 'Slovak', 93 | 'sl': 'Slovenian', 94 | 'so': 'Somali', 95 | 'es': 'Spanish', 96 | 'su': 'Sundanese', 97 | 'sw': 'Swahili', 98 | 'sv': 'Swedish', 99 | 'tl': 'Tagalog (Filipino)', 100 | 'tg': 'Tajik', 101 | 'ta': 'Tamil', 102 | 'tt': 'Tatar', 103 | 'te': 'Telugu', 104 | 'th': 'Thai', 105 | 'tr': 'Turkish', 106 | 'tk': 'Turkmen', 107 | 'uk': 'Ukrainian', 108 | 'ur': 'Urdu', 109 | 'ug': 'Uyghur', 110 | 'uz': 'Uzbek', 111 | 'vi': 'Vietnamese', 112 | 'cy': 'Welsh', 113 | 'xh': 'Xhosa', 114 | 'yi': 'Yiddish', 115 | 'yo': 'Yoruba', 116 | 'zu': 'Zulu', 117 | } 118 | 119 | 120 | const ISOCodes = Object.keys(GoogleTranslateLanguages) 121 | 122 | 123 | function printLanguages () { 124 | 125 | console.log('CODE LANGUAGE') 126 | console.log('ISO-639-1 NAME') 127 | console.log('--------- -------------------') 128 | 129 | for ( const isoCode in GoogleTranslateLanguages) { 130 | console.log(`${(' ' + isoCode).slice(-9)} ${GoogleTranslateLanguages[isoCode]}`) 131 | } 132 | 133 | console.log() 134 | 135 | } 136 | 137 | function validIsoCode(code) { 138 | return ISOCodes.includes(code) 139 | } 140 | 141 | 142 | /** 143 | * unit test main 144 | */ 145 | function test() { 146 | 147 | printLanguages() 148 | console.log() 149 | console.log( validIsoCode('ww')) 150 | console.log( validIsoCode('it')) 151 | 152 | } 153 | 154 | 155 | if (require.main === module) 156 | test() 157 | 158 | module.exports = { 159 | GoogleTranslateLanguages, 160 | printLanguages, 161 | validIsoCode 162 | } 163 | 164 | -------------------------------------------------------------------------------- /lib/googleTranslateTTS.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const https = require('https') 4 | const googleTTS = require('google-tts-api') 5 | 6 | const Elapsed = require('./elapsed') 7 | const { sanitizeFilename } = require('./sanitizeFilename') 8 | const { getArgs } = require('./getArgs') 9 | const { validIsoCode } = require('./googleTranslateLanguages') 10 | 11 | 12 | const AUDIO_FILES_HOME = `${path.resolve('.')}/audio` 13 | 14 | /** 15 | * @desc true if argument is a number 16 | * 17 | * @param {Number} 18 | * @returns {Boolean} 19 | * 20 | */ 21 | function isNumber(num) { 22 | return !isNaN(parseFloat(num)) && isFinite(num) 23 | } 24 | 25 | 26 | /** 27 | * googleTranslateTTS 28 | * download Google Translate TTS MP3 file 29 | * @public 30 | * 31 | * Google TTS (Text-To-Speech) for node.js 32 | * @see https://github.com/zlargon/google-tts 33 | * @see https://cloud.google.com/speech-to-text/docs/languages 34 | * 35 | * @param {String} sentence 36 | * @param {String} filename 37 | * @param {String} lang 38 | * @param {Boolean} slow 39 | * 40 | * @returns {Promise.} 41 | * @returns {Promise.} 42 | * @typedef resolveObj 43 | * @property {String} url - url returned by googleTTS 44 | * @property {String} time - execution elapsed time 45 | */ 46 | function googleTranslateTTS(sentence, filename, lang, slow=false) { 47 | return new Promise( (resolve, reject) => { 48 | 49 | if ( ! validIsoCode(lang) ) 50 | reject( `ERROR: on function googleTranslateTTS. Invalid language code: ${lang}` ) 51 | 52 | const elapsedTime = new Elapsed() 53 | const url = googleTTS.getAudioUrl( sentence, { lang, slow } ) 54 | 55 | // download the MP3 audio file from specified url 56 | https.get( url, response => { 57 | response 58 | .pipe(fs.createWriteStream(filename)) 59 | .on('finish', () => 60 | resolve( { url, time: elapsedTime.elapsed() } )) 61 | }) 62 | 63 | }) 64 | } 65 | 66 | 67 | function helpAndExit(programName) { 68 | console.log('usage:') 69 | console.log() 70 | console.log(` ${programName} \\ `) 71 | console.log(' --language= \\ ') 72 | console.log(' [--directory=] \\ ') 73 | console.log(' [--speed=]') 74 | console.log() 75 | console.log(' where:') 76 | console.log(' : language ISO code (it/en/etc.)') 77 | console.log(' see https://cloud.google.com/speech-to-text/docs/languages') 78 | console.log(' : path to audio files home directory. Default: your/path/audio') 79 | console.log(' : 1 = normal (default), 0 = slow') 80 | console.log() 81 | console.log('examples:') 82 | console.log() 83 | console.log(` ${programName} mi chiamo Giorgio --language=it`) 84 | console.log(` ${programName} mi chiamo Giorgio Robino e sono nato a Genova, in Italia. --language=it-IT --speed=1`) 85 | console.log(` ${programName} "my name is Giorgio Robino and I'm born in Genoa, Italy." --language=en `) 86 | console.log(` ${programName} "my name is Giorgio Robino" --language=en --directory=/home/giorgio/myproject/audio`) 87 | console.log() 88 | process.exit(1) 89 | } 90 | 91 | /** 92 | * checkArgs 93 | * command line parsing 94 | * 95 | * @param {String} commands 96 | * @param {String} args 97 | * @param {String} programName 98 | * 99 | * @returns {SentenceAndAttributes} 100 | * @typedef {Object} SentenceAndAttributes 101 | * @property {String} sentence 102 | * @property {String} directory 103 | * @property {String} language 104 | * @property {Number} speed 105 | * 106 | */ 107 | function checkArgs(commands, args, programName) { 108 | 109 | const language = args.language 110 | const speed = isNumber(args.speed) ? +args.speed : 1 111 | let directory = args.directory 112 | 113 | if (!directory) 114 | directory = `${process.cwd()}/audio` 115 | 116 | if ( !language && !commands.length ) 117 | helpAndExit(programName) 118 | 119 | // create audio home directory if not exists 120 | if ( !fs.existsSync(directory) ) { 121 | fs.mkdirSync(directory) 122 | console.log(`created directory: ${directory}`) 123 | } 124 | 125 | // create the sentence text from command line words 126 | const sentence = commands.join(' ') 127 | 128 | if (sentence.length > 200) { 129 | console.error('sentence length must be max 200 characters') 130 | process.exit(1) 131 | } 132 | 133 | // retrun validate values 134 | return { sentence, directory, language, speed } 135 | } 136 | 137 | /** 138 | * @public 139 | * @param {String} commands 140 | * @param {String} args 141 | * @param {String} programName 142 | * 143 | */ 144 | function downloadGoogleTransalteMP3(commands, args, programName) { 145 | 146 | const { sentence, directory, language, speed } = checkArgs(commands, args, programName) 147 | let slow 148 | 149 | const languageDirectory = `${directory}/${language}` 150 | 151 | // create language directory if not exists 152 | if ( !fs.existsSync(languageDirectory) ) { 153 | fs.mkdirSync(languageDirectory) 154 | console.log(`created directory: ${languageDirectory}`) 155 | } 156 | 157 | const filename = `${AUDIO_FILES_HOME}/${language}/${sanitizeFilename(sentence)}.mp3` 158 | 159 | console.log() 160 | console.log(`sentence : ${sentence}`) 161 | console.log(`language : ${language}`) 162 | console.log(`speed : ${speed < 1 ? slow = true : slow = false}`) 163 | console.log() 164 | 165 | 166 | // call translate TTS API 167 | googleTranslateTTS(sentence, filename, language, slow) 168 | .then( result => { 169 | console.info(`url : ${result.url}`) 170 | console.log(`file name : ${filename}` ) 171 | console.info(`elapsed time : ${result.time}`) 172 | }) 173 | .catch( err => console.error(err) ) 174 | 175 | } 176 | 177 | 178 | /** 179 | * unit test main 180 | */ 181 | function test() { 182 | 183 | // get command line args and commands 184 | const { commands, args } = getArgs() 185 | 186 | downloadGoogleTransalteMP3(commands, args, 'node googleTranslateTTS') 187 | 188 | } 189 | 190 | 191 | if (require.main === module) 192 | test() 193 | 194 | module.exports = { 195 | googleTranslateTTS, 196 | downloadGoogleTransalteMP3 197 | } 198 | 199 | -------------------------------------------------------------------------------- /lib/info.js: -------------------------------------------------------------------------------- 1 | const { name, version, author, description } = require('../package') 2 | 3 | function info() { 4 | return ( 5 | '\n' + 6 | `${name}, ${description}\n` + 7 | `v. ${version}, (C) ${author}\n` 8 | ) 9 | } 10 | 11 | module.exports = { info } 12 | -------------------------------------------------------------------------------- /lib/sanitizeFilename.js: -------------------------------------------------------------------------------- 1 | const ALLOWED_CHARACTERS = 'a-z0-9àèéìòù' 2 | 3 | /** 4 | * sanitizeFileName 5 | * transaltate a sentence in a valid filename 6 | * old version restrictive (allow just a set of chars) 7 | * 8 | * @param {String} 9 | * @return {String} 10 | * 11 | * @see https://stackoverflow.com/a/8485137/1786393 12 | * @see https://stackoverflow.com/questions/42068/how-do-i-handle-newlines-in-json 13 | */ 14 | function sanitizeFilenameOLD(text, allowedCharacters=ALLOWED_CHARACTERS) { 15 | 16 | const sanitizeRegex = new RegExp(`[^${allowedCharacters}]`, 'gi') 17 | 18 | return text 19 | .toLowerCase() // lowercase all 20 | .replace(sanitizeRegex, '_') // sanitize 21 | .replace(/_{2,}/g, '_') // remove duplicate underscores 22 | } 23 | 24 | 25 | /** 26 | * sanitizeFileName 27 | * transaltate a sentence in a valid filename 28 | * 29 | * @param {String} text 30 | * @param {Object} options 31 | * @return {String} 32 | * 33 | * @see 34 | * https://serverfault.com/questions/348482/how-to-remove-invalid-characters-from-filenames 35 | * https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names/1976131#:~:text=Under%20Linux%20and%20other%20Unix,path%20name%2C%20separating%20directory%20components. 36 | * https://unix.stackexchange.com/questions/230291/what-characters-are-valid-to-use-in-filenames 37 | * https://stackoverflow.com/a/8485137/1786393 38 | * https://stackoverflow.com/questions/42068/how-do-i-handle-newlines-in-json 39 | */ 40 | function sanitizeFilename(text, options={toLowerCase: false, noBlanks: true, noDots:false}) { 41 | 42 | // validate chars in text: 43 | 44 | // https://flaviocopes.com/un\x00/ 45 | // The first 32 characters, U+0000-U+001F (0-31) are called Control Codes. 46 | if ( /[\u0000-\u001F]/.test(text) ) { 47 | console.error(`ERROR: text contains invalid characters: [${text}]`) 48 | } 49 | 50 | // Considering linux filesystems rules, any char is valid except NULL and SLASH. 51 | // See: https://unix.stackexchange.com/questions/230291/what-characters-are-valid-to-use-in-filenames 52 | 53 | if ( options.toLowerCase ) 54 | text = text.toLowerCase() 55 | 56 | if ( options.noBlanks ) 57 | text = text.replace(/[ ]/gi, '_') 58 | 59 | if ( options.noDots ) 60 | text = text.replace(/[.]/gi, '_dot_') 61 | 62 | return text 63 | .replace(/[/]/gi, '_slash_') 64 | .replace(/[\u0000-\u001F]/gi, '_') 65 | } 66 | 67 | 68 | /** 69 | * fullFileName 70 | * transaltate a sentence in a valid full filename (path, suffi. etc.) 71 | * 72 | * @param {String} text 73 | * @param {Object} options 74 | * @param {Object} sanitizeOptions 75 | * 76 | * @return {String} 77 | * 78 | */ 79 | function fullFilename( text, options={path: 'audio', language: 'it', suffix: 'mp3'}, sanitizeOptions ) { 80 | 81 | // TODO 82 | // validate attributes loss 83 | return `${options.path}/${options.language}/${sanitizeFilename(text, sanitizeOptions)}.${options.suffix}` 84 | 85 | } 86 | 87 | 88 | function main() { 89 | 90 | let sentence 91 | 92 | sentence = 'perché\0 non ce l\'hai detto, però?' 93 | console.log() 94 | console.log(sentence) 95 | console.log(sanitizeFilename(sentence)) 96 | 97 | sentence = 'http://convcomp.it' 98 | console.log() 99 | console.log(sentence) 100 | console.log(sanitizeFilename(sentence, {noDots: true})) 101 | 102 | sentence = 'Ha detto: "Non è vero". E tu digli che é un\'idiota!"' 103 | console.log() 104 | console.log(sentence) 105 | console.log(sanitizeFilename(sentence)) 106 | 107 | sentence = 'á con accento acuto' 108 | console.log() 109 | console.log(sentence) 110 | console.log(sanitizeFilename(sentence)) 111 | 112 | sentence = 'à con accento grave' 113 | console.log() 114 | console.log(sentence) 115 | console.log(sanitizeFilename(sentence)) 116 | 117 | sentence = '.' 118 | console.log() 119 | console.log(sentence) 120 | console.log(sanitizeFilename(sentence)) 121 | 122 | sentence = 'RAIU 690011 4 25 U1' 123 | console.log() 124 | console.log(sentence) 125 | console.log(sanitizeFilename(sentence)) 126 | console.log(fullFilename(sentence)) 127 | 128 | sentence = 'JL1349-76 [45A/MU4]' 129 | console.log() 130 | console.log(sentence) 131 | console.log(sanitizeFilename(sentence)) 132 | console.log(fullFilename(sentence)) 133 | 134 | console.log() 135 | 136 | } 137 | 138 | 139 | if (require.main === module) 140 | main() 141 | 142 | 143 | // exports public function 144 | module.exports = { 145 | sanitizeFilenameOLD, 146 | sanitizeFilename, 147 | fullFilename 148 | } 149 | 150 | -------------------------------------------------------------------------------- /lib/sentenceTokenizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Split multi sentence text in an array of sentences. 3 | * Each sentence have a max number of chars 4 | * @public 5 | * @param {String} text 6 | * @param {Integer} limit maximum number of characters for each sentence 7 | * @return {String[]} array of strings 8 | * 9 | * @see https://stackoverflow.com/questions/5454235/shorten-string-without-cutting-words-in-javascript 10 | */ 11 | function sentenceTokenizer(text, limit) { 12 | 13 | const sentences = sentenceTokenizerRaw(text) 14 | const shortenedSentences = [] 15 | sentences.forEach( sentence => shortenedSentences.push( breakSentence(sentence,limit) ) ) 16 | 17 | return shortenedSentences.flat(Infinity) 18 | } 19 | 20 | 21 | /** 22 | * Split multi sentence text in an array of sentences. 23 | * Each sentence length could be arbitrary long. 24 | * 25 | * @param {String} text 26 | * @return {String[]} array of strings 27 | * 28 | * @see https://stackoverflow.com/questions/18914629/split-string-into-sentences-in-javascript 29 | * @see https://github.com/zlargon/google-tts/blob/master/example/long-english-characters.js 30 | */ 31 | function sentenceTokenizerRaw(text) { 32 | // Opinable/TODO 33 | // sentence stop chars: . , ; ? ! \n 34 | //const arrayOfSentences = text.replace(/([\n.,;:?!])\s*(?=[\S])/g, '$1\r').split('\r') 35 | //const arrayOfSentences = text.replace(/([()\[\]{}".,;:?!])\s*(?=[\S])/g, '$1\r').split('\r') 36 | const arrayOfSentences = text.replace(/([\n.;:?!"])\s*(?=[\S])/g, '$1\r').split('\r') 37 | return arrayOfSentences 38 | } 39 | 40 | 41 | /** 42 | * Split a sentence in a numer of subsentence of specified max length 43 | * 44 | * @param {String} sentence 45 | * @param {Integer} limit maximum number of characters to extract 46 | * 47 | * @return {String[]} array of strings 48 | * 49 | * @see https://stackoverflow.com/questions/5454235/shorten-string-without-cutting-words-in-javascript 50 | */ 51 | function breakSentence(sentence, limit) { 52 | const queue = sentence.split(' ') 53 | const sentences = [] 54 | 55 | while (queue.length) { 56 | const word = queue.shift() 57 | 58 | if (word.length >= limit) { 59 | sentences.push(word) 60 | } 61 | else { 62 | let words = word 63 | 64 | for(;;) { 65 | if (!queue.length || 66 | words.length > limit || 67 | words.length + queue[0].length + 1 > limit) { 68 | break 69 | } 70 | 71 | words += ' ' + queue.shift() 72 | } 73 | 74 | sentences.push(words) 75 | } 76 | } 77 | 78 | return sentences 79 | } 80 | 81 | // https://stackoverflow.com/a/36247412/1786393 82 | //const leftPad = (s, c, n) =>{ s = s.toString(); c = c.toString(); return s.length > n ? s : c.repeat(n - s.length) + s; } 83 | 84 | 85 | const NULL_REGEXP = /(?!)/ // regexp always fails 86 | 87 | /** 88 | * split a sentence in multiple phrases, if the sentence contains ',' or '.' 89 | * split a sentence in multiple words, separated by blanks 90 | * 91 | * @example 92 | * wordOrPhraseTokenier('uno due') // return ['uno', 'due'] 93 | * wordOrPhraseTokenier('uno, due , tre e quattro ') // return ['uno', 'due', 'tre e quattro'] 94 | * 95 | * @param {String} anyCaseSentence 96 | * @return {String[]} 97 | * 98 | */ 99 | const wordOrPhraseTokenier = (anyCaseSentence) => { 100 | 101 | const PHRASES_SEPARATOR = /[,.]/ 102 | const WORD_SEPARATOR = ' ' 103 | //const PUNTUACTION_REGEXP = /[|'"£%&/\\()=§*+°#@\[\]{}\]-_><\^]/g 104 | const PUNTUACTION_REGEXP = /[|"£%&/\\()=§*+°#@\[\]{}\]-_><\^]/g 105 | const SEPARATORS_REGEXP = /[?!;:]/g 106 | 107 | // sentence preprocessing 108 | const sentence = anyCaseSentence 109 | 110 | // convert to lower case to avoid Google TTS anomalies 111 | .toLowerCase() 112 | 113 | // convert 'virgola' and 'punto' (from ASR) to corresponding chars 114 | .replace(/virgola/g, ',') 115 | .replace(/punto/g, '.') 116 | 117 | .replace(SEPARATORS_REGEXP, '.') 118 | 119 | // italian keyboard puntuaction chars are converted to blank char 120 | .replace(PUNTUACTION_REGEXP, ' ') 121 | 122 | // convert duplicate tabs or blanks in a single blank 123 | .replace(/\s+/g,' ') 124 | 125 | //const tokens = sentence.indexOf(PHRASES_SEPARATOR) > -1 ? 126 | const tokens = sentence.match(PHRASES_SEPARATOR) ? 127 | sentence.split(PHRASES_SEPARATOR) : 128 | sentence.split(WORD_SEPARATOR) 129 | 130 | return tokens 131 | // remove blanks at the begin and the end 132 | .map(token => token.trim()) 133 | // remove void phrases 134 | .filter(token => token.length > 0) 135 | } 136 | 137 | 138 | const simplePhraseTokenier = (anyCaseSentence) => { 139 | 140 | const PHRASES_SEPARATOR = /[.]/ 141 | const WORD_SEPARATOR = ' ' 142 | 143 | // sentence preprocessing 144 | const sentence = anyCaseSentence 145 | 146 | // convert 'virgola' and 'punto' (from ASR) to corresponding chars 147 | .replace(/punto/g, '.') 148 | 149 | // convert duplicate tabs or blanks in a single blank 150 | .replace(/\s+/g,' ') 151 | 152 | //const tokens = sentence.indexOf(PHRASES_SEPARATOR) > -1 ? 153 | const tokens = sentence.split(PHRASES_SEPARATOR) 154 | 155 | return tokens 156 | // remove blanks at the begin and the end 157 | .map(token => token.trim()) 158 | // remove void phrases 159 | .filter(token => token.length > 0) 160 | } 161 | 162 | function unitTest() { 163 | //const yourString = 'Buongiorno Maestra! io mi chiamo Layla, sono egiziana.' //replace with your string. 164 | //const yourString = "Lo Sportello APRE Liguria è lieto di invitarla al workshop sul "TRATTAMENTO DEGLI ASPETTI ETICI NEI PROGETTI DI RICERCA E INNOVAZIONE IN H2020" organizzato in collaborazione con APRE e che si terrà il giorno 15 Aprile p.v. ore 14.00 presso la Sala Conferenze del Di.M.I. (Viale Benedetto XV, 6). 165 | 166 | const maxLength = 140 167 | console.log('max sentence length is: ', maxLength) 168 | console.log() 169 | console.log() 170 | 171 | //const longString = 'Il workshop intende fornire gli strumenti per il trattamento degli aspetti etici nella presentazione della proposta (con particolare riferimento alla compilazione del questionario etico obbligatorio) e durante la gestione del progetto.' 172 | //console.log( breakSentence(longString, maxLength) ) 173 | 174 | console.log('Demo text: ') 175 | console.log() 176 | 177 | const string = `Per tutte le attività finanziate dall'Unione Europea, gli aspetti etici sono parte integrante della ricerca dall'inizio alla fine e la conformità etica è considerata fondamentale per garantire l'eccellenza della ricerca 178 | 179 | Pertanto, fin dall'elaborazione dell'idea progettuale, è necessario effettuare una valutazione etica approfondita non solo per "rispettare il quadro giuridico", ma anche per migliorare la qualità della proposta, aumentandone così le possibilità di successo 180 | 181 | La condotta di ricerca etica implica l'applicazione dei principi etici e della legislazione fondamentali alla ricerca scientifica in tutti i "possibili ambiti" di ricerca. Il workshop intende fornire gli strumenti per il trattamento degli aspetti etici nella presentazione della proposta (con particolare riferimento alla compilazione del questionario etico obbligatorio) e durante la gestione del progetto. Sarà inoltre previsto un focus specifico sulle misure di conformità per previste dal Protocollo di Nagoya relativo kall'accesso alle risorse genetiche (EU Access and Benefit Sharing Regulation).` 182 | 183 | console.log(string) 184 | console.log() 185 | console.log('Split in sentences: ' ) 186 | console.log() 187 | 188 | sentenceTokenizer(string, maxLength) 189 | //.forEach( sentence => console.log(`${leftPad(sentence.length, ' ', 3)} ${sentence}`)) 190 | .forEach( sentence => console.log(`${(''+sentence.length).padStart(3, ' ')} ${sentence}`)) 191 | 192 | 193 | console.log( breakSentence(string, 80) ) 194 | console.log() 195 | 196 | console.log() 197 | console.log('---------------------------') 198 | console.log('wordOrPhraseTokenier tests') 199 | console.log('---------------------------') 200 | console.log() 201 | 202 | let sentence 203 | 204 | sentence = 'UNO due' 205 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 206 | 207 | sentence = 'mela.' 208 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 209 | 210 | sentence = 'PERA, la MEla, la mela è VERDE' 211 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 212 | 213 | sentence = 'mela. la mela, la mela è verde' 214 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 215 | 216 | sentence = 'mela punto la mela virgola la mela è verde punto' 217 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 218 | 219 | sentence = 'la cameriera è carina!' 220 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 221 | 222 | sentence = 'la banana è gialla?' 223 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 224 | 225 | sentence = 'la merenda@: è davvero buona ?!' 226 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 227 | 228 | sentence = 'chi l\'ha visto? Io non l\'ho visto!' 229 | console.log( sentence, wordOrPhraseTokenier(sentence) ) 230 | 231 | sentence = 'Chi è stato?. Io non l\'ho mica fatto!.Va bene.' 232 | console.log( sentence, simplePhraseTokenier(sentence) ) 233 | } 234 | 235 | 236 | if (require.main === module) 237 | unitTest() 238 | 239 | module.exports = { 240 | breakSentence, 241 | sentenceTokenizer, 242 | wordOrPhraseTokenier, 243 | simplePhraseTokenier 244 | } 245 | 246 | -------------------------------------------------------------------------------- /lib/serialize.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * resolves Promises sequentially. 4 | * 5 | * @example 6 | * const urls = ['/url1', '/url2', '/url3'] 7 | * const funcs = urls.map(url => () => $.ajax(url)) 8 | * 9 | * serialize(funcs) 10 | * .then(console.log) 11 | * .catch(console.error) 12 | * 13 | * @see https://hackernoon.com/functional-javascript-resolving-promises-sequentially-7aac18c4431e 14 | * @see https://stackoverflow.com/questions/24586110/resolve-promises-one-after-another-i-e-in-sequence 15 | */ 16 | const serialize = funcs => 17 | funcs.reduce((promise, func) => 18 | promise.then(result => func().then(Array.prototype.concat.bind(result))), 19 | Promise.resolve([])) 20 | 21 | 22 | module.exports = { serialize } 23 | 24 | -------------------------------------------------------------------------------- /lib/sleep.js: -------------------------------------------------------------------------------- 1 | /* 2 | * sleep for a number of milliseconds 3 | * 4 | * @param {Integer} ms milliseconds to sleep 5 | * @return Promise 6 | */ 7 | function sleep(ms) { 8 | return new Promise(resolve => setTimeout(resolve, ms)) 9 | } 10 | 11 | module.exports = { sleep } 12 | 13 | -------------------------------------------------------------------------------- /lib/spawn.js: -------------------------------------------------------------------------------- 1 | 2 | const { spawn } = require('child_process') 3 | const Elapsed = require('./elapsed') 4 | 5 | const DEBUG = false 6 | 7 | 8 | /** 9 | * spawn a process 10 | * 11 | * @param {String} command 12 | * @param {String[]} args - command arguments 13 | * 14 | * @typedef SpawnedProcess 15 | * @property {number} exit - exit code 16 | * @property {String} cmd - full command 17 | * @property {String} execution - execution elapsed time 18 | * 19 | * @returns {Promise} status of spawned process 20 | * 21 | * @see https://stackoverflow.com/questions/14332721/node-js-spawn-child-process-and-get-terminal-output-live 22 | */ 23 | function spawnAsync (command, args) { 24 | 25 | const fullcmd = `${command} ${args.join(' ')}` 26 | let stdout = '' 27 | let stderr = '' 28 | 29 | if(DEBUG) 30 | console.log('spawnAsync>command>', fullcmd) 31 | 32 | return new Promise((resolve, reject) => { 33 | 34 | const elapsedTime = new Elapsed() 35 | 36 | const child = spawn(command, args) 37 | 38 | child.stdout.setEncoding('utf8') 39 | child.stderr.setEncoding('utf8') 40 | 41 | child.stdout.on('data', (data) => { 42 | stdout += data.toString() 43 | 44 | if(DEBUG) 45 | console.log(`spawnAsync>stdout> ${command}: ${data}`) 46 | }) 47 | 48 | child.stderr.on('data', (data) => { 49 | stderr += data.toString() 50 | 51 | if(DEBUG) 52 | console.log(`spawnAsync>stderr> ${command}: ${data}`) 53 | }) 54 | 55 | child.on('error', (error) => { 56 | console.error(`spawnAsync> error ${error}`) 57 | reject(error) 58 | }) 59 | 60 | /* 61 | child.on('exit', (code, signal) => { 62 | if (code !== 0) { 63 | console.error(`spawnAsync>exit> code ${code} and signal ${signal}`) 64 | reject(code, signal) 65 | } 66 | }) 67 | */ 68 | 69 | child.on('close', (code) => { 70 | if (stderr !== '') { 71 | console.error(`spawnAsync>stderr> ${command}: ${stderr} exit code: ${code}`) 72 | reject(stderr) 73 | } 74 | else 75 | resolve({ 76 | exit: code, 77 | fullcmd, 78 | stdout, 79 | stderr, 80 | execution: elapsedTime.elapsed() 81 | }) 82 | }) 83 | }) 84 | } 85 | 86 | 87 | /* 88 | * for unit test 89 | * @private 90 | */ 91 | async function main() { 92 | 93 | if ( process.argv.length < 3 ) { 94 | console.log() 95 | console.log('usage : node spawn ') 96 | console.log('example: node spawn ls -l') 97 | console.log() 98 | process.exit(1) 99 | } 100 | 101 | const fullCommand = process.argv.slice(2).join(' ') 102 | 103 | const [command, ...args] = fullCommand.split(' ') 104 | 105 | spawnAsync(command, args) 106 | .then( result => { 107 | 108 | //if (result.code === 0) 109 | console.log( result /*.stdout*/ ) // print the attribute stdout of resolve object 110 | //else 111 | // console.error( `command ${result.fullcmd} failed with exit code: ${result.code}` ) 112 | 113 | }) 114 | .catch( data => console.log( `command failed. See: ${data}` )) 115 | } 116 | 117 | if (require.main === module) 118 | main() 119 | 120 | 121 | module.exports = { spawnAsync } 122 | 123 | -------------------------------------------------------------------------------- /lib/ttsfile.js: -------------------------------------------------------------------------------- 1 | const { concatAudiofiles } = require('./concatAudioFiles') 2 | const { sanitizeFilename, fullFilename } = require('./sanitizeFilename') 3 | const fs = require('fs') 4 | 5 | 6 | /** 7 | * 8 | * ttsFile 9 | * 10 | * The returned object is an audio file, lossless (e.g. `wav`) 11 | * or in a compressed lossy compression format (e.g. 'ogg') 12 | * 13 | * @param [{String}] text sentence to be spoken, splitted in an array of tokens 14 | * @param {string} path home directory path, where input files are located 15 | * @param {string} language language_code ('en-us', 'it', ) 16 | * @param {String} suffix file suffix / audio coding format ('mp3','wav'/'ogg'/etc.) 17 | * @return {String} outputfilename name of audio file 18 | */ 19 | 20 | async function ttsFile(texts, path, language, suffix, outputFilename) { 21 | 22 | // outputFilename=fileNameConcat(inputFilenames) 23 | 24 | // build the list of fullpath input filenames 25 | const inputFilenames = [] 26 | const outputFilenameAsConcat = [] 27 | 28 | for (const text of texts) { 29 | 30 | const filename = fullFilename(text, {path, language, suffix}) 31 | 32 | // validate file existence, async 33 | fs.access(filename, (err) => { 34 | if (err) 35 | throw new Error(`${err}: file ${filename} does not exist.`) 36 | }) 37 | 38 | // create the list of input full filenames 39 | inputFilenames.push( filename ) 40 | 41 | // prepare the list of input filenames string, to be attached 42 | outputFilenameAsConcat.push( sanitizeFilename(text) ) 43 | 44 | } 45 | 46 | // if outputfilename has not specificed as parameter, 47 | // it's build concatenating input filenames 48 | if ( ! outputFilename ) 49 | outputFilename = fullFilename( outputFilenameAsConcat.join(''), {path, language, suffix} ) 50 | 51 | //console.log(inputFilenames) 52 | //console.log(outputFilename) 53 | 54 | // concatenate all files 55 | try { 56 | const result = await concatAudiofiles( inputFilenames, outputFilename ) 57 | 58 | if (result.exit === 0) { 59 | //console.info( result ) 60 | return result 61 | } 62 | else 63 | console.error( `ERROR (concatAudiofiles): ${result}` ) 64 | } 65 | catch(error) { 66 | console.error(error) 67 | } 68 | } 69 | 70 | 71 | /** 72 | * unit test main 73 | */ 74 | async function main() { 75 | 76 | // 77 | // to generate input files: 78 | // node lib/googleTranslateTTS.js "mi chiamo " --language=it 79 | // node lib/googleTranslateTTS.js "Giorgio Robino " --language=it 80 | // 81 | const texts = [ 82 | 'mi chiamo ', 83 | 'Giorgio Robino ' 84 | ] 85 | 86 | try { 87 | const result = await ttsFile(texts, 'audio', 'it', 'mp3') 88 | console.info( result ) 89 | } 90 | catch(error) { 91 | console.error(error) 92 | } 93 | 94 | } 95 | 96 | 97 | if (require.main === module) 98 | main() 99 | 100 | 101 | module.exports = { 102 | ttsFile 103 | } 104 | 105 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jointts", 3 | "version": "0.11.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "0.11.0", 9 | "license": "MIT", 10 | "dependencies": { 11 | "google-tts-api": "^2.0.1" 12 | }, 13 | "bin": { 14 | "joint": "bin/jointts.js", 15 | "jointts": "bin/jointts.js" 16 | } 17 | }, 18 | "node_modules/axios": { 19 | "version": "0.21.1", 20 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", 21 | "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", 22 | "dependencies": { 23 | "follow-redirects": "^1.10.0" 24 | } 25 | }, 26 | "node_modules/follow-redirects": { 27 | "version": "1.13.1", 28 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", 29 | "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==", 30 | "funding": [ 31 | { 32 | "type": "individual", 33 | "url": "https://github.com/sponsors/RubenVerborgh" 34 | } 35 | ], 36 | "engines": { 37 | "node": ">=4.0" 38 | }, 39 | "peerDependenciesMeta": { 40 | "debug": { 41 | "optional": true 42 | } 43 | } 44 | }, 45 | "node_modules/google-tts-api": { 46 | "version": "2.0.1", 47 | "resolved": "https://registry.npmjs.org/google-tts-api/-/google-tts-api-2.0.1.tgz", 48 | "integrity": "sha512-gFYSMusgxU7ad6+lTjG7xuFajIK6LxlXXkFMKQCZTzzEbDwXz4n3Ljc57c4PCxP/PvOZ4tJCgRGWiPJaAyToLg==", 49 | "dependencies": { 50 | "axios": "^0.21.0" 51 | } 52 | } 53 | }, 54 | "dependencies": { 55 | "axios": { 56 | "version": "0.21.1", 57 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", 58 | "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", 59 | "requires": { 60 | "follow-redirects": "^1.10.0" 61 | } 62 | }, 63 | "follow-redirects": { 64 | "version": "1.13.1", 65 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", 66 | "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" 67 | }, 68 | "google-tts-api": { 69 | "version": "2.0.1", 70 | "resolved": "https://registry.npmjs.org/google-tts-api/-/google-tts-api-2.0.1.tgz", 71 | "integrity": "sha512-gFYSMusgxU7ad6+lTjG7xuFajIK6LxlXXkFMKQCZTzzEbDwXz4n3Ljc57c4PCxP/PvOZ4tJCgRGWiPJaAyToLg==", 72 | "requires": { 73 | "axios": "^0.21.0" 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jointts", 3 | "version": "0.13.6", 4 | "description": "brainless off-line concatenative text to speech", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "bin": { 13 | "joint": "bin/jointts.js", 14 | "jointts": "bin/jointts.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/solyarisoftware/concatts.git" 19 | }, 20 | "keywords": [ 21 | "TTS", 22 | "Text To Speech", 23 | "on-premise concatenative TTS" 24 | ], 25 | "author": "Giorgio Robino ", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/solyarisoftware/concatts/issues" 29 | }, 30 | "homepage": "https://github.com/solyarisoftware/concatts#readme", 31 | "dependencies": { 32 | "google-tts-api": "^2.0.1" 33 | } 34 | } 35 | --------------------------------------------------------------------------------