UNPKG

9.58 kBJavaScriptView Raw
1const ansiEscapes = require('ansi-escapes');
2const chalk = require('chalk')
3const Table = require('cli-table3');
4
5const docs = chalk`
6You can use the {blue wait:1234} modifier to add a custom delay (in milliseconds) between specific words.
7e.g. {cyan $ vox echo go wait:1200 helium}
8`
9
10function run() {
11 const prefix = chalk`{bgYellow.black λ } `
12 const voicepacks = require('fs').readdirSync(require('path').resolve(__dirname, 'assets', 'voicepacks'))
13 if (!voicepacks || !voicepacks.length) {
14 console.log(chalk`${prefix}{bgRed.black ERROR} No voicepacks found`)
15 return
16 }
17 // check autocomplete
18 // if handled the process will exit
19 const program = require('commander')
20 program
21 .version('0.1.0')
22 .addOption(new program.Option('-v, --voice <voice>', 'Select voice').default(voicepacks[0]).choices(voicepacks))
23 .addOption(new program.Option('-l, --list [letter]', 'List words'))
24 .addOption(new program.Option('-s, --search <search>', 'Search in available words'))
25 .addOption(new program.Option('-c, --compact', 'Compact result'))
26 .addOption(new program.Option('-r, --repeat <n>', 'Repeat n times'))
27 .addOption(new program.Option('-p, --pause <n>', 'Pause between repeats').default(1400))
28 .addOption(new program.Option('-d, --delay <n>', 'Delay between words').default(350))
29 .addOption(new program.Option('-x, --random [n]', 'Pick random (n) words'))
30 .arguments('[words...]')
31 .description(prefix + 'Speech synthensizing from the cli using Half-Life and Black Mesa VOX soundpacks', {
32 words: 'words to say'
33 })
34 .addHelpText('after', docs)
35 .action((words, options, ...rest) => {
36
37 const voice = options.voice
38 const getFilepath = (n) => './assets/voicepacks/' + voice + '/' + n + '.wav'
39
40 if (options.list || options.search) {
41 var table = new Table({
42 head: options.compact ? ['Letter', 'Words'] : ['Letter', 'Word', 'filename'],
43 colWidths: options.compact ? [8, 40] : [8, 18],
44 wordWrap:true
45 });
46 const letters = {}
47
48 const files = require('fs').readdirSync(require('path').resolve(__dirname, 'assets', 'voicepacks', voice))
49 const search = (options.search || typeof options.list === 'string') && new RegExp(options.search ? options.search : `^${options.list}`, 'gi')
50 const wordsList = files
51 .filter(f => f.match(/\.wav$/)).map(f => f.replace(/\.wav$/, '')).sort()
52 .filter(word => !search || word.match(search))
53
54
55 wordsList.map(word => letters[word[0]] = [...letters[word[0]] || [], word])
56 Object.keys(letters).sort().map(letter => {
57 const letterWords = letters[letter]
58 const compact = options.compact && letterWords.join(', ')
59 table.push([
60 ...compact
61 ? [letter.toUpperCase(), { content: compact }]
62 : [
63 {
64 rowSpan: letterWords.length,
65 content: letter.toUpperCase()
66 },
67 letterWords[0], getFilepath(letterWords[0])]
68 ])
69
70 if (!compact)
71 letterWords.map((word, i) => i && table.push([word, getFilepath(word)]))
72 })
73 table.push([{ colSpan: options.compact ? 2 : 3, content: `Total${search ? ' results' : ''}: ${wordsList.length}` }])
74
75 process.stdout.write(table.toString());
76 process.stdout.write('\n');
77 } else {
78 if (!words || !words.length) {
79 if (options.random) {
80
81 const files = require('fs').readdirSync(require('path').resolve(__dirname, 'assets', voice))
82 const wordsList = files
83 .filter(f => f.match(/\.wav$/)).map(f => f.replace(/\.wav$/, '')).sort(() => Math.random() > 0.5 ? 1 : -1)
84 const wordsCount = +options.random > 1 ? options.random : Math.floor(Math.random() * 12)
85 for (let i = 0; i < wordsCount; ++i) words.push(wordsList[i])
86 } else {
87 words = Math.random() > 0.7 ? 'bloop wait:1200 no'.split(' ') : 'bloop must command'.split(' ')
88 }
89 }
90
91 const player = require('node-wav-player');
92 var wavFileInfo = require('wav-file-info');
93
94 let times = 0
95 let word
96 const pause = options.pause
97
98 const started = Date.now()
99 const compact = options.compact
100
101 !compact && console.log(chalk`${prefix}Synthesizing speech {dim — using {green ${options.voice}} voicepack}`)
102 !compact && options.repeat && line(chalk`\n${prefix}{dim Repeating ${options.repeat} times}`)
103 !compact && line(`${prefix}${words.join(' ')}`)
104
105 const playNext = async () => {
106 if (!word && word !== 0) word = 0
107 else word++
108
109 !compact && clear()
110
111 const timerDisplay = async (time, interval = 50, fn) => {
112 process.stdout.write(ansiEscapes.eraseLines(2))
113 fn()
114 await new Promise(r => setTimeout(r, interval))
115 if (time - (interval * 5) > 0) timerDisplay(time - interval, interval, fn)
116 }
117
118 const done = async () => {
119 if (options.repeat && ++times < options.repeat) {
120 word = false
121 !compact && clear()
122 !compact && options.repeat && line(chalk`{dim ${prefix}Cycle ${times} of ${options.repeat} complete in ${Date.now() - started}ms. (CTRL+C to stop)}`)
123
124 const then = Date.now()
125 !compact && line(chalk`${prefix}{dim.green "${words.join(' ')}"}`)
126 !compact && line(chalk`${prefix}{dim Repeating in ${options.pause}ms}`)
127 !compact && await timerDisplay(options.pause, 50, () => {
128 console.log(chalk`${prefix}{dim Repeating in ${options.pause - (Date.now() - then)}ms...}`)
129 })
130 await new Promise(r => setTimeout(r, options.pause))
131 playNext()
132 } else {
133 !compact && clear()
134 line(chalk`${prefix}{green "${words.join(' ')}}"`)
135 options.repeat
136 ? line(chalk`${prefix}{dim Completed ${options.repeat} cycles in ${Date.now() - started}ms.}`)
137 : line(chalk`${prefix}{dim Complete in ${Date.now() - started}ms.}`)
138 }
139 }
140
141 !compact && options.repeat && line(chalk`${prefix}{dim Cycle ${times + 1} of ${options.repeat}... (CTRL+C to stop)}`)
142 !compact && line(chalk`${prefix}{dim "${words.map((w, i) => i === word ? chalk.reset.blue.underline(w) : i < word ? chalk.dim.green(w) : chalk.dim(w)).join(' ')}}{dim "}`);
143
144 const sayWord = words[word]
145 if (!sayWord) done()
146 else {
147
148 const filepath = require('path').resolve(__dirname, getFilepath(sayWord))
149 const delay = +options.delay
150
151 const [, action, params] = sayWord.match(/^(.*):(.*)$/i) || []
152
153 if (action) {
154 if (action === 'wait') {
155 // process.stdout.write(chalk`\n${prefix}{dim Waiting ${+params}ms...}\n`);
156 !compact && line(chalk`${prefix}{dim Waiting ${+params}ms...}`);
157 await new Promise(r => setTimeout(r, +params))
158 playNext()
159 }
160 } else
161 try {
162 wavFileInfo
163 .infoByFilename(filepath, async (err, info) => {
164 if (err) {
165 throw err;
166 }
167
168 const duration = Math.floor(info.duration * 1000)
169 const waitFor = Math.floor(info.duration * 1000) - 350
170
171 !compact && line(chalk`${prefix}{dim Playing "${sayWord}" — ${duration}ms...}`);
172
173 const start = Date.now()
174 player.play({ path: filepath })
175 .then(async () => {
176 if (word < words.length - 1) {
177 const end = Date.now()
178 const diff = end - start
179
180 await new Promise((r) => setTimeout(r, waitFor - diff))
181 delay && await new Promise((r) => setTimeout(r, delay))
182 playNext()
183 } else {
184 done()
185 }
186 })
187 });
188 } catch (error) {
189 !compact && clear()
190 line(chalk`${prefix}{dim "${words.map((w, i) => i === word ? chalk.yellow.underline(w) : i < word ? chalk.dim.green(w) : chalk.dim(w)).join(' ')}"}`)
191 line(chalk`${prefix}{bgRed.black ERROR} "${sayWord}" ${error}`);
192 }
193 }
194
195 }
196
197 playNext()
198 }
199 });
200 program.parse(process.argv);
201
202}
203module.exports = {
204 run
205 // run: () => myRun()
206}
207
208const state = {}
209
210const line = (input) => {
211 state.lines = (state.lines || 0) + input.split('\n').length
212 process.stdout.write(input + '\n')
213}
214
215const overline = (input) => {
216 state.lines = ((state.lines || 0) + input.split('\n').length) - 1
217 process.stdout.write(ansiEscapes.eraseLines(2))
218 process.stdout.write(input + '\n')
219}
220
221const clear = () => {
222 if (state.lines)
223 process.stdout.write(ansiEscapes.eraseLines(state.lines + 1))
224 state.lines = 0
225}
226
227const myRun = async () => {
228 var table = new Table({
229 style:{head:[],border:[]},
230 colWidths: [14, 18],
231 wordWrap:true
232 });
233
234 table.push(['playing', 'wav', './assets/vox/wav.wav', 12.32 + 'ms'])
235 line(table.toString())
236 // await new Promise(r => setTimeout(r, 1000))
237 // clear()
238}