echogarden 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/lexicons/heteronyms.en.json +6 -2
- package/data/lexicons/words.en.json +379 -19
- package/dist/alignment/SemanticTextAlignment.js +6 -7
- package/dist/alignment/SemanticTextAlignment.js.map +1 -1
- package/dist/api/Alignment.js +2 -2
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Recognition.js +2 -2
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/SpeechTranslation.js +2 -2
- package/dist/api/SpeechTranslation.js.map +1 -1
- package/dist/api/Synthesis.js +7 -7
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/TimelineTranslationAlignment.js +2 -2
- package/dist/api/TimelineTranslationAlignment.js.map +1 -1
- package/dist/api/TranslationAlignment.js +2 -2
- package/dist/api/TranslationAlignment.js.map +1 -1
- package/dist/audio/AudioPlayer.js +2 -2
- package/dist/audio/AudioPlayer.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +1 -1
- package/dist/cli/CLI.js +4 -4
- package/dist/cli/CLI.js.map +1 -1
- package/dist/denoising/RNNoise.js +5 -5
- package/dist/denoising/RNNoise.js.map +1 -1
- package/dist/dsp/FFT.js +13 -19
- package/dist/dsp/FFT.js.map +1 -1
- package/dist/dsp/Rubberband.js +5 -5
- package/dist/dsp/Rubberband.js.map +1 -1
- package/dist/dsp/Sonic.js +6 -6
- package/dist/dsp/Sonic.js.map +1 -1
- package/dist/dsp/SpeexResampler.js +11 -11
- package/dist/dsp/SpeexResampler.js.map +1 -1
- package/dist/nlp/EspeakPhonemizer.js +1 -1
- package/dist/nlp/EspeakPhonemizer.js.map +1 -1
- package/dist/nlp/Segmentation.d.ts +13 -10
- package/dist/nlp/Segmentation.js +129 -106
- package/dist/nlp/Segmentation.js.map +1 -1
- package/dist/nlp/TextNormalizer.js +74 -42
- package/dist/nlp/TextNormalizer.js.map +1 -1
- package/dist/recognition/AmazonTranscribeSTT.js +2 -2
- package/dist/recognition/AmazonTranscribeSTT.js.map +1 -1
- package/dist/recognition/SileroSTT.js +2 -2
- package/dist/recognition/SileroSTT.js.map +1 -1
- package/dist/recognition/WhisperSTT.js +4 -16
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/subtitles/Subtitles.js +52 -7
- package/dist/subtitles/Subtitles.js.map +1 -1
- package/dist/synthesis/ElevenLabsTTS.js +11 -10
- package/dist/synthesis/ElevenLabsTTS.js.map +1 -1
- package/dist/synthesis/EspeakTTS.js +8 -8
- package/dist/synthesis/EspeakTTS.js.map +1 -1
- package/dist/synthesis/KokoroTTS.js +21 -10
- package/dist/synthesis/KokoroTTS.js.map +1 -1
- package/dist/synthesis/SvoxPicoTTS.js +24 -23
- package/dist/synthesis/SvoxPicoTTS.js.map +1 -1
- package/dist/text-translation/DeepLTextTranslation.js +4 -3
- package/dist/text-translation/DeepLTextTranslation.js.map +1 -1
- package/dist/text-translation/NLLBTextTranslation.js +7 -6
- package/dist/text-translation/NLLBTextTranslation.js.map +1 -1
- package/dist/utilities/StringUtilities.d.ts +1 -1
- package/dist/utilities/StringUtilities.js +5 -5
- package/dist/utilities/StringUtilities.js.map +1 -1
- package/dist/utilities/Timeline.d.ts +2 -3
- package/dist/utilities/Timeline.js +96 -147
- package/dist/utilities/Timeline.js.map +1 -1
- package/dist/utilities/WikipediaReader.js +3 -3
- package/dist/utilities/WikipediaReader.js.map +1 -1
- package/dist/voice-activity-detection/WebRtcVAD.js +4 -4
- package/dist/voice-activity-detection/WebRtcVAD.js.map +1 -1
- package/docs/Tasklist.md +1 -6
- package/package.json +13 -11
- package/src/alignment/SemanticTextAlignment.ts +6 -7
- package/src/api/Alignment.ts +2 -2
- package/src/api/Recognition.ts +2 -2
- package/src/api/SpeechTranslation.ts +2 -2
- package/src/api/Synthesis.ts +7 -7
- package/src/api/TimelineTranslationAlignment.ts +2 -2
- package/src/api/TranslationAlignment.ts +2 -2
- package/src/audio/AudioPlayer.ts +2 -2
- package/src/cli/CLI.ts +4 -4
- package/src/denoising/RNNoise.ts +5 -5
- package/src/dsp/FFT.ts +22 -19
- package/src/dsp/Rubberband.ts +5 -5
- package/src/dsp/Sonic.ts +6 -6
- package/src/dsp/SpeexResampler.ts +11 -11
- package/src/nlp/EspeakPhonemizer.ts +1 -1
- package/src/nlp/Segmentation.ts +146 -111
- package/src/nlp/TextNormalizer.ts +94 -52
- package/src/recognition/AmazonTranscribeSTT.ts +2 -2
- package/src/recognition/SileroSTT.ts +2 -2
- package/src/recognition/WhisperSTT.ts +4 -18
- package/src/subtitles/Subtitles.ts +69 -8
- package/src/synthesis/ElevenLabsTTS.ts +12 -11
- package/src/synthesis/EspeakTTS.ts +9 -8
- package/src/synthesis/KokoroTTS.ts +24 -12
- package/src/synthesis/SvoxPicoTTS.ts +26 -23
- package/src/text-translation/DeepLTextTranslation.ts +4 -3
- package/src/text-translation/NLLBTextTranslation.ts +8 -6
- package/src/typings/Fillers.d.ts +0 -16
- package/src/utilities/StringUtilities.ts +5 -6
- package/src/utilities/Timeline.ts +100 -170
- package/src/utilities/WikipediaReader.ts +3 -3
- package/src/voice-activity-detection/WebRtcVAD.ts +5 -4
- package/dist/nlp/CompromiseNLP.d.ts +0 -15
- package/dist/nlp/CompromiseNLP.js +0 -79
- package/dist/nlp/CompromiseNLP.js.map +0 -1
- package/dist/utilities/WasmMemoryManager.d.ts +0 -153
- package/dist/utilities/WasmMemoryManager.js +0 -440
- package/dist/utilities/WasmMemoryManager.js.map +0 -1
- package/src/nlp/CompromiseNLP.ts +0 -119
- package/src/utilities/WasmMemoryManager.ts +0 -566
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echogarden",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "An easy-to-use speech toolset. Includes tools for synthesis, recognition, alignment, speech translation, language detection, source separation and more.",
|
|
5
5
|
"author": "Rotem Dan",
|
|
6
6
|
"license": "GPL-3.0",
|
|
@@ -52,27 +52,27 @@
|
|
|
52
52
|
"echogarden": "./dist/cli/CLILauncher.js"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@aws-sdk/client-polly": "^3.
|
|
56
|
-
"@aws-sdk/client-transcribe-streaming": "^3.
|
|
57
|
-
"@echogarden/audio-io": "^0.
|
|
55
|
+
"@aws-sdk/client-polly": "^3.810.0",
|
|
56
|
+
"@aws-sdk/client-transcribe-streaming": "^3.810.0",
|
|
57
|
+
"@echogarden/audio-io": "^0.4.1",
|
|
58
58
|
"@echogarden/espeak-ng-emscripten": "^0.3.3",
|
|
59
59
|
"@echogarden/fasttext-wasm": "^0.1.0",
|
|
60
60
|
"@echogarden/flite-wasi": "^0.1.1",
|
|
61
61
|
"@echogarden/fvad-wasm": "^0.2.0",
|
|
62
62
|
"@echogarden/gnuspeech-wasm": "^0.2.1",
|
|
63
|
+
"@echogarden/icu-segmentation-wasm": "^0.2.1",
|
|
63
64
|
"@echogarden/pffft-wasm": "^0.4.2",
|
|
64
65
|
"@echogarden/rnnoise-wasm": "^0.2.0",
|
|
65
66
|
"@echogarden/rubberband-wasm": "^0.2.0",
|
|
66
67
|
"@echogarden/sonic-wasm": "^0.2.0",
|
|
67
68
|
"@echogarden/speex-resampler-wasm": "^0.3.0",
|
|
68
69
|
"@echogarden/svoxpico-wasm": "^0.2.0",
|
|
69
|
-
"@echogarden/
|
|
70
|
-
"@echogarden/
|
|
70
|
+
"@echogarden/text-segmentation": "^0.3.1",
|
|
71
|
+
"@echogarden/transformers-nodejs-lite": "^2.17.1-lite.4",
|
|
72
|
+
"@echogarden/wave-codec": "^0.3.3",
|
|
71
73
|
"@mozilla/readability": "^0.6.0",
|
|
72
74
|
"chalk": "^5.4.1",
|
|
73
|
-
"cldr-segmentation": "^2.2.1",
|
|
74
75
|
"command-exists": "^1.2.9",
|
|
75
|
-
"compromise": "^14.14.4",
|
|
76
76
|
"fs-extra": "^11.3.0",
|
|
77
77
|
"gaxios": "^6.7.1",
|
|
78
78
|
"graceful-fs": "^4.2.11",
|
|
@@ -84,14 +84,16 @@
|
|
|
84
84
|
"kuromoji": "^0.1.2",
|
|
85
85
|
"microsoft-cognitiveservices-speech-sdk": "^1.43.1",
|
|
86
86
|
"msgpack-lite": "^0.1.26",
|
|
87
|
-
"onnxruntime-node": "^1.
|
|
88
|
-
"openai": "^4.
|
|
87
|
+
"onnxruntime-node": "^1.22.0",
|
|
88
|
+
"openai": "^4.98.0",
|
|
89
|
+
"regexp-composer": "^0.3.0",
|
|
89
90
|
"sam-js": "^0.3.1",
|
|
90
91
|
"strip-ansi": "^7.1.0",
|
|
91
92
|
"tar": "^7.4.3",
|
|
92
93
|
"tiktoken": "^1.0.21",
|
|
93
94
|
"tinyld": "^1.3.4",
|
|
94
95
|
"wasm-feature-detect": "^1.8.0",
|
|
96
|
+
"wasm-heap-manager": "^0.2.2",
|
|
95
97
|
"ws": "^8.18.2",
|
|
96
98
|
"wtf_wikipedia": "^10.4.0"
|
|
97
99
|
},
|
|
@@ -112,7 +114,7 @@
|
|
|
112
114
|
"@types/graceful-fs": "^4.1.9",
|
|
113
115
|
"@types/jsdom": "^21.1.7",
|
|
114
116
|
"@types/msgpack-lite": "^0.1.11",
|
|
115
|
-
"@types/node": "^22.15.
|
|
117
|
+
"@types/node": "^22.15.18",
|
|
116
118
|
"@types/tar": "^6.1.13",
|
|
117
119
|
"@types/ws": "^8.18.1",
|
|
118
120
|
"ts-json-schema-generator": "^2.4.0",
|
|
@@ -3,7 +3,7 @@ import { Logger } from '../utilities/Logger.js'
|
|
|
3
3
|
import { loadPackage } from '../utilities/PackageManager.js'
|
|
4
4
|
import { alignDTWWindowed } from './DTWSequenceAlignmentWindowed.js'
|
|
5
5
|
import { cosineDistance } from '../math/VectorMath.js'
|
|
6
|
-
import {
|
|
6
|
+
import { includesPunctuation, isWord, parseText } from '../nlp/Segmentation.js'
|
|
7
7
|
import { Timeline, extractEntries } from '../utilities/Timeline.js'
|
|
8
8
|
|
|
9
9
|
export async function alignTimelineToTextSemantically(timeline: Timeline, text: string, textLangCode: string) {
|
|
@@ -26,13 +26,12 @@ export async function alignTimelineToTextSemantically(timeline: Timeline, text:
|
|
|
26
26
|
|
|
27
27
|
const timelineWordEntriesFiltered = timelineWordEntryGroups.flat()
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const segmentedText = await parseText(text, textLangCode)
|
|
30
30
|
|
|
31
31
|
const textWordGroups: string[][] = []
|
|
32
32
|
|
|
33
|
-
for (const
|
|
34
|
-
|
|
35
|
-
wordGroup = wordGroup.filter(word => isWord(word))
|
|
33
|
+
for (const sentenceEntry of segmentedText.sentences) {
|
|
34
|
+
const wordGroup = sentenceEntry.words.nonPunctuationWords
|
|
36
35
|
|
|
37
36
|
textWordGroups.push(wordGroup)
|
|
38
37
|
}
|
|
@@ -197,8 +196,8 @@ export async function alignWordsToWordsSemantically(wordsGroups1: string[][], wo
|
|
|
197
196
|
|
|
198
197
|
// Align
|
|
199
198
|
function costFunction(a: TokenEmbeddingData, b: TokenEmbeddingData) {
|
|
200
|
-
const aIsPunctuation =
|
|
201
|
-
const bIsPunctuation =
|
|
199
|
+
const aIsPunctuation = includesPunctuation(a.text)
|
|
200
|
+
const bIsPunctuation = includesPunctuation(b.text)
|
|
202
201
|
|
|
203
202
|
if (aIsPunctuation === bIsPunctuation) {
|
|
204
203
|
return cosineDistance(a.embeddingVector, b.embeddingVector)
|
package/src/api/Alignment.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { AudioSourceParam, RawAudio, ensureRawAudio, getRawAudioDuration, normal
|
|
|
5
5
|
import { Logger } from '../utilities/Logger.js'
|
|
6
6
|
|
|
7
7
|
import * as API from './API.js'
|
|
8
|
-
import { Timeline, addTimeOffsetToTimeline,
|
|
8
|
+
import { Timeline, addTimeOffsetToTimeline, addWordTextOffsetsToTimelineInPlace, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
|
|
9
9
|
import { formatLanguageCodeWithName, getDefaultDialectForLanguageCodeIfPossible, getShortLanguageCode, parseLangIdentifier } from '../utilities/Locale.js'
|
|
10
10
|
import { type WhisperAlignmentOptions } from '../recognition/WhisperSTT.js'
|
|
11
11
|
import chalk from 'chalk'
|
|
@@ -329,7 +329,7 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
// Add text offsets
|
|
332
|
-
|
|
332
|
+
addWordTextOffsetsToTimelineInPlace(mappedTimeline, transcript)
|
|
333
333
|
|
|
334
334
|
// Make segment timeline
|
|
335
335
|
const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(mappedTimeline, transcript, language, options.plainText?.paragraphBreaks, options.plainText?.whitespace)
|
package/src/api/Recognition.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace,
|
|
|
5
5
|
import { Logger } from '../utilities/Logger.js'
|
|
6
6
|
|
|
7
7
|
import * as API from './API.js'
|
|
8
|
-
import { Timeline,
|
|
8
|
+
import { Timeline, addWordTextOffsetsToTimelineInPlace, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
|
|
9
9
|
import { formatLanguageCodeWithName, parseLangIdentifier } from '../utilities/Locale.js'
|
|
10
10
|
import { loadPackage } from '../utilities/PackageManager.js'
|
|
11
11
|
import chalk from 'chalk'
|
|
@@ -312,7 +312,7 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
// Add text offsets
|
|
315
|
-
|
|
315
|
+
addWordTextOffsetsToTimelineInPlace(timeline, transcript)
|
|
316
316
|
|
|
317
317
|
// Make segment timeline
|
|
318
318
|
const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(timeline, transcript, languageCode, 'single', 'preserve')
|
|
@@ -4,7 +4,7 @@ import { logToStderr } from '../utilities/Utilities.js'
|
|
|
4
4
|
import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace, trimAudioEnd } from '../audio/AudioUtilities.js'
|
|
5
5
|
import { Logger } from '../utilities/Logger.js'
|
|
6
6
|
|
|
7
|
-
import { Timeline,
|
|
7
|
+
import { Timeline, addWordTextOffsetsToTimelineInPlace, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
|
|
8
8
|
import { type WhisperOptions } from '../recognition/WhisperSTT.js'
|
|
9
9
|
import { formatLanguageCodeWithName, getShortLanguageCode, normalizeIdentifierToLanguageCode, parseLangIdentifier } from '../utilities/Locale.js'
|
|
10
10
|
import { EngineMetadata } from './Common.js'
|
|
@@ -201,7 +201,7 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
if (wordTimeline) {
|
|
204
|
-
|
|
204
|
+
addWordTextOffsetsToTimelineInPlace(wordTimeline, transcript)
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
if (!segmentTimeline) {
|
package/src/api/Synthesis.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { clip, sha256AsHex, stringifyAndFormatJson, logToStderr, yieldToEventLoo
|
|
|
6
6
|
import { RawAudio, concatAudioSegments, downmixToMono, encodeRawAudioToWave, getSamplePeakDecibels, getEmptyRawAudio, getRawAudioDuration, trimAudioEnd, trimAudioStart, attenuateIfClippingInPlace, normalizeAudioLevelInPlace } from '../audio/AudioUtilities.js'
|
|
7
7
|
import { Logger } from '../utilities/Logger.js'
|
|
8
8
|
|
|
9
|
-
import { isWordOrSymbolWord,
|
|
9
|
+
import { isWordOrSymbolWord, parseText, splitToParagraphs } from '../nlp/Segmentation.js'
|
|
10
10
|
import { type RubberbandOptions } from '../dsp/Rubberband.js'
|
|
11
11
|
import { loadLexiconsForLanguage } from '../nlp/Lexicon.js'
|
|
12
12
|
|
|
@@ -116,9 +116,9 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
|
|
|
116
116
|
let timeOffset = 0
|
|
117
117
|
|
|
118
118
|
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
|
|
119
|
-
const segmentText = segments[segmentIndex]
|
|
119
|
+
const segmentText = segments[segmentIndex]
|
|
120
120
|
|
|
121
|
-
logger.log(`\n${chalk.magentaBright(`Synthesizing segment ${segmentIndex + 1}/${segments.length}`)}: '${segmentText}'`)
|
|
121
|
+
logger.log(`\n${chalk.magentaBright(`Synthesizing segment ${segmentIndex + 1}/${segments.length}`)}: '${segmentText.trim()}'`)
|
|
122
122
|
|
|
123
123
|
const segmentStartTime = timeOffset
|
|
124
124
|
|
|
@@ -132,9 +132,9 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
|
|
|
132
132
|
|
|
133
133
|
let sentences: string[]
|
|
134
134
|
|
|
135
|
-
if ((options.splitToSentences || options.engine
|
|
136
|
-
|
|
137
|
-
sentences = sentences.
|
|
135
|
+
if ((options.splitToSentences || options.engine === 'vits' || options.engine === 'kokoro') && !options.ssml) {
|
|
136
|
+
const parsedText = await parseText(segmentText, options.language!)
|
|
137
|
+
sentences = parsedText.sentences.map(sentenceEntry => sentenceEntry.text)
|
|
138
138
|
|
|
139
139
|
if (sentences.length == 0) {
|
|
140
140
|
sentences = ['']
|
|
@@ -156,7 +156,7 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
|
|
|
156
156
|
|
|
157
157
|
const sentenceText = sentences[sentenceIndex].trim()
|
|
158
158
|
|
|
159
|
-
logger.log(`\n${chalk.magentaBright(`Synthesizing sentence ${sentenceIndex + 1}/${sentences.length}`)}: "${sentenceText}"`)
|
|
159
|
+
logger.log(`\n${chalk.magentaBright(`Synthesizing sentence ${sentenceIndex + 1}/${sentences.length}`)}: "${sentenceText.trim()}"`)
|
|
160
160
|
|
|
161
161
|
const sentenceStartTime = timeOffset
|
|
162
162
|
|
|
@@ -4,7 +4,7 @@ import { SubtitlesConfig } from '../subtitles/Subtitles.js'
|
|
|
4
4
|
import { formatLanguageCodeWithName, getShortLanguageCode, parseLangIdentifier } from '../utilities/Locale.js'
|
|
5
5
|
import { Logger } from '../utilities/Logger.js'
|
|
6
6
|
import { extendDeep } from '../utilities/ObjectUtilities.js'
|
|
7
|
-
import { Timeline,
|
|
7
|
+
import { Timeline, addWordTextOffsetsToTimelineInPlace, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
|
|
8
8
|
import * as API from './API.js'
|
|
9
9
|
|
|
10
10
|
export async function alignTimelineTranslation(inputTimeline: Timeline, translatedTranscript: string, options: TimelineTranslationAlignmentOptions): Promise<TimelineTranslationAlignmentResult> {
|
|
@@ -95,7 +95,7 @@ export async function alignTimelineTranslation(inputTimeline: Timeline, translat
|
|
|
95
95
|
|
|
96
96
|
logger.start(`Postprocess timeline`)
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
addWordTextOffsetsToTimelineInPlace(mappedWordTimeline, translatedTranscript)
|
|
99
99
|
|
|
100
100
|
const { segmentTimeline: mappedTimeline } = await wordTimelineToSegmentSentenceTimeline(mappedWordTimeline, translatedTranscript, targetLanguage)
|
|
101
101
|
|
|
@@ -5,7 +5,7 @@ import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace,
|
|
|
5
5
|
import { Logger } from '../utilities/Logger.js'
|
|
6
6
|
|
|
7
7
|
import * as API from './API.js'
|
|
8
|
-
import { Timeline,
|
|
8
|
+
import { Timeline, addWordTextOffsetsToTimelineInPlace, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
|
|
9
9
|
import { formatLanguageCodeWithName, getShortLanguageCode, normalizeIdentifierToLanguageCode, parseLangIdentifier } from '../utilities/Locale.js'
|
|
10
10
|
import { type WhisperAlignmentOptions } from '../recognition/WhisperSTT.js'
|
|
11
11
|
import chalk from 'chalk'
|
|
@@ -124,7 +124,7 @@ export async function alignTranslation(input: AudioSourceParam, translatedTransc
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// Add text offsets
|
|
127
|
-
|
|
127
|
+
addWordTextOffsetsToTimelineInPlace(mappedTimeline, translatedTranscript)
|
|
128
128
|
|
|
129
129
|
// Make segment timeline
|
|
130
130
|
const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(mappedTimeline, translatedTranscript, sourceLanguage, options.plainText?.paragraphBreaks, options.plainText?.whitespace)
|
package/src/audio/AudioPlayer.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { Timer } from '../utilities/Timer.js'
|
|
|
9
9
|
import { clip, getRandomHexString, waitTimeout, writeToStderr } from '../utilities/Utilities.js'
|
|
10
10
|
import { encodeToAudioBuffer, float32ToInt16Pcm, interleaveChannels } from './AudioBufferConversion.js'
|
|
11
11
|
import { OpenPromise } from '../utilities/OpenPromise.js'
|
|
12
|
-
import { Timeline,
|
|
12
|
+
import { Timeline, addWordTextOffsetsToTimelineInPlace } from '../utilities/Timeline.js'
|
|
13
13
|
import { readAndParseJsonFile, readFileAsUtf8, remove, writeFile } from '../utilities/FileSystem.js'
|
|
14
14
|
import { tryResolvingSoxPath } from './SoxPath.js'
|
|
15
15
|
import { SignalChannel } from '../utilities/SignalChannel.js'
|
|
@@ -38,7 +38,7 @@ export async function playAudioWithWordTimeline(rawAudio: RawAudio, wordTimeline
|
|
|
38
38
|
|
|
39
39
|
wordTimeline = deepClone(wordTimeline)
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
addWordTextOffsetsToTimelineInPlace(wordTimeline, transcript)
|
|
42
42
|
|
|
43
43
|
let timelineEntryIndex = 0
|
|
44
44
|
let transcriptOffset = 0
|
package/src/cli/CLI.ts
CHANGED
|
@@ -10,10 +10,10 @@ import { SubtitlesConfig, subtitlesToText, timelineToSubtitles } from '../subtit
|
|
|
10
10
|
import { Logger, resetActiveLogger } from '../utilities/Logger.js'
|
|
11
11
|
import { isMainThread, parentPort } from 'node:worker_threads'
|
|
12
12
|
import { encodeFromChannels, getDefaultFFMpegOptionsForSpeech } from '../codecs/FFMpegTranscoder.js'
|
|
13
|
-
import { splitToParagraphs, splitToWords
|
|
13
|
+
import { splitToParagraphs, splitToWords } from '../nlp/Segmentation.js'
|
|
14
14
|
import { playAudioSamplesWithKeyboardControls, playAudioWithWordTimeline } from '../audio/AudioPlayer.js'
|
|
15
15
|
import { extendDeep } from '../utilities/ObjectUtilities.js'
|
|
16
|
-
import { Timeline, TimelineEntry, addTimeOffsetToTimeline,
|
|
16
|
+
import { Timeline, TimelineEntry, addTimeOffsetToTimeline, addWordTextOffsetsToTimelineInPlace, roundTimelineProperties } from '../utilities/Timeline.js'
|
|
17
17
|
import { ensureDir, existsSync, readAndParseJsonFile, readdir, readFileAsUtf8, writeFileSafe } from '../utilities/FileSystem.js'
|
|
18
18
|
import { formatLanguageCodeWithName, getShortLanguageCode } from '../utilities/Locale.js'
|
|
19
19
|
import { APIOptions } from '../api/APIOptions.js'
|
|
@@ -509,7 +509,7 @@ export async function speak(operationData: CLIOperationData) {
|
|
|
509
509
|
const { audio: synthesizedAudio, timeline } = await API.synthesize(textSegments, options, onSegment, undefined)
|
|
510
510
|
|
|
511
511
|
if (plainText) {
|
|
512
|
-
|
|
512
|
+
addWordTextOffsetsToTimelineInPlace(timeline, plainText)
|
|
513
513
|
}
|
|
514
514
|
|
|
515
515
|
if (outputFilenames.length > 0) {
|
|
@@ -1884,7 +1884,7 @@ async function checkOutputFilenames(outputFilenames: string[], acceptMediaOutput
|
|
|
1884
1884
|
async function writeOutputFilesForSegment(outputFilenames: string[], index: number, total: number, audio: RawAudio, timeline: Timeline, text: string, language: string, allowOverwrite: boolean) {
|
|
1885
1885
|
const digitCount = Math.max((total + 1).toString().length, 2)
|
|
1886
1886
|
|
|
1887
|
-
const segmentWords = (await splitToWords(text, language)).
|
|
1887
|
+
const segmentWords = (await splitToWords(text, language)).nonPunctuationWords
|
|
1888
1888
|
|
|
1889
1889
|
const segmentJoinedWords = segmentWords.join(' ').trim()
|
|
1890
1890
|
|
package/src/denoising/RNNoise.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { float32ToInt16Pcm } from '../audio/AudioBufferConversion.js'
|
|
2
2
|
import { concatFloat32Arrays } from '../utilities/Utilities.js'
|
|
3
|
-
import {
|
|
3
|
+
import { wrapEmscriptenModuleHeap } from 'wasm-heap-manager'
|
|
4
4
|
import { Logger } from '../utilities/Logger.js'
|
|
5
5
|
import { RawAudio, cloneRawAudio } from '../audio/AudioUtilities.js'
|
|
6
6
|
|
|
@@ -24,15 +24,15 @@ export async function denoiseAudio(rawAudio: RawAudio) {
|
|
|
24
24
|
const m = await getRnnoiseInstance()
|
|
25
25
|
|
|
26
26
|
logger.start('Process with RNNoise')
|
|
27
|
-
const
|
|
27
|
+
const wasmHeap = wrapEmscriptenModuleHeap(m)
|
|
28
28
|
|
|
29
29
|
const stateSize = m._rnnoise_get_size()
|
|
30
30
|
const frameSize = m._rnnoise_get_frame_size()
|
|
31
31
|
|
|
32
32
|
const denoiseState = m._rnnoise_create(0)
|
|
33
33
|
|
|
34
|
-
const inputRef =
|
|
35
|
-
const outputRef =
|
|
34
|
+
const inputRef = wasmHeap.allocFloat32Array(frameSize)
|
|
35
|
+
const outputRef = wasmHeap.allocFloat32Array(frameSize)
|
|
36
36
|
|
|
37
37
|
const floatSamples = rawAudio.audioChannels[0]
|
|
38
38
|
const int16Samples = float32ToInt16Pcm(floatSamples)
|
|
@@ -70,7 +70,7 @@ export async function denoiseAudio(rawAudio: RawAudio) {
|
|
|
70
70
|
outputNewFrame(outputRef.view.slice(), lastFrameVadProbability)
|
|
71
71
|
|
|
72
72
|
m._rnnoise_destroy(denoiseState)
|
|
73
|
-
|
|
73
|
+
wasmHeap.freeAll()
|
|
74
74
|
|
|
75
75
|
const int16DenoisedSamplesAsFloats = concatFloat32Arrays(processedFrames)
|
|
76
76
|
|
package/src/dsp/FFT.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ComplexNumber } from '../math/VectorMath.js'
|
|
2
2
|
import { concatFloat32Arrays, isWasmSimdSupported } from '../utilities/Utilities.js'
|
|
3
|
-
import {
|
|
3
|
+
import { createWasmHeapManager } from 'wasm-heap-manager'
|
|
4
4
|
|
|
5
5
|
// Compute short-term Fourier transform (real-valued)
|
|
6
6
|
export async function stftr(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
|
|
@@ -15,7 +15,7 @@ export async function stftr(samples: Float32Array, fftOrder: number, windowSize:
|
|
|
15
15
|
|
|
16
16
|
// Incrementally generate short-term Fourier transform frames (real-valued)
|
|
17
17
|
export async function* stftrGenerator(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
|
|
18
|
-
if (fftOrder % 2
|
|
18
|
+
if (fftOrder % 2 !== 0 || windowSize % 2 !== 0) {
|
|
19
19
|
throw new Error('FFT order and window size must be multiples of 2')
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -35,17 +35,19 @@ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, w
|
|
|
35
35
|
const windowWeights = getWindowWeights(windowType, windowSize)
|
|
36
36
|
|
|
37
37
|
const m = await getPFFFTInstance(await isPffftSimdSupportedForFFTOrder(fftOrder))
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
|
|
39
|
+
const wasmHeap = createWasmHeapManager(
|
|
40
|
+
() => m.HEAPU8.buffer,
|
|
41
|
+
m._pffft_aligned_malloc,
|
|
42
|
+
m._pffft_aligned_free,
|
|
43
|
+
)
|
|
42
44
|
|
|
43
45
|
const statePtr = m._pffft_new_setup(fftOrder, 0)
|
|
44
46
|
|
|
45
47
|
const sampleCount = samples.length
|
|
46
|
-
const frameBufferRef =
|
|
47
|
-
const binsBufferRef =
|
|
48
|
-
const workBufferRef =
|
|
48
|
+
const frameBufferRef = wasmHeap.allocFloat32Array(fftOrder)
|
|
49
|
+
const binsBufferRef = wasmHeap.allocFloat32Array(fftOrder * 2)
|
|
50
|
+
const workBufferRef = wasmHeap.allocFloat32Array(fftOrder * 2)
|
|
49
51
|
|
|
50
52
|
for (let offset = 0; offset < sampleCount; offset += hopSize) {
|
|
51
53
|
const windowSamples = samples.subarray(offset, offset + windowSize)
|
|
@@ -68,12 +70,12 @@ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, w
|
|
|
68
70
|
|
|
69
71
|
m._pffft_destroy_setup(statePtr)
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
wasmHeap.freeAll()
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// Compute short-term inverse Fourier transform (real-valued)
|
|
75
77
|
export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType, expectedOutputLength?: number) {
|
|
76
|
-
if (fftOrder % 2
|
|
78
|
+
if (fftOrder % 2 !== 0 || windowSize % 2 !== 0) {
|
|
77
79
|
throw new Error('FFT order and window size must multiples of 2')
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -101,16 +103,17 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
|
|
|
101
103
|
|
|
102
104
|
const m = await getPFFFTInstance(await isPffftSimdSupportedForFFTOrder(fftOrder))
|
|
103
105
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const wasmHeap = createWasmHeapManager(
|
|
107
|
+
() => m.HEAPU8.buffer,
|
|
108
|
+
m._pffft_aligned_malloc,
|
|
109
|
+
m._pffft_aligned_free,
|
|
110
|
+
)
|
|
108
111
|
|
|
109
112
|
const statePtr = m._pffft_new_setup(fftOrder, 0)
|
|
110
113
|
|
|
111
|
-
const frameBufferRef =
|
|
112
|
-
const binsRef =
|
|
113
|
-
const workBufferRef =
|
|
114
|
+
const frameBufferRef = wasmHeap.allocFloat32Array(fftOrder)
|
|
115
|
+
const binsRef = wasmHeap.allocFloat32Array(fftOrder * 2)
|
|
116
|
+
const workBufferRef = wasmHeap.allocFloat32Array(fftOrder * 2)
|
|
114
117
|
|
|
115
118
|
const sumOfSquaredWeightsForSample = new Float32Array(outSampleCount)
|
|
116
119
|
|
|
@@ -140,7 +143,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
|
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
m._pffft_destroy_setup(statePtr)
|
|
143
|
-
|
|
146
|
+
wasmHeap.freeAll()
|
|
144
147
|
|
|
145
148
|
// Divide each output sample by the sum of squared weights
|
|
146
149
|
for (let i = 0; i < outSamples.length; i++) {
|
package/src/dsp/Rubberband.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RawAudio } from '../audio/AudioUtilities.js'
|
|
2
2
|
import { extendDeep } from '../utilities/ObjectUtilities.js'
|
|
3
3
|
import { concatFloat32Arrays } from '../utilities/Utilities.js'
|
|
4
|
-
import { Float32ArrayRef,
|
|
4
|
+
import { Float32ArrayRef, wrapEmscriptenModuleHeap } from 'wasm-heap-manager'
|
|
5
5
|
|
|
6
6
|
let rubberbandInstance: any
|
|
7
7
|
|
|
@@ -14,7 +14,7 @@ export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchS
|
|
|
14
14
|
const sampleRate = rawAudio.sampleRate
|
|
15
15
|
|
|
16
16
|
const m = await getRubberbandInstance()
|
|
17
|
-
const
|
|
17
|
+
const wasmHeap = wrapEmscriptenModuleHeap(m)
|
|
18
18
|
|
|
19
19
|
const optionFlags = rubberBandOptionsToFlags(options)
|
|
20
20
|
|
|
@@ -25,11 +25,11 @@ export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchS
|
|
|
25
25
|
const samplesRequired = m._rubberband_get_samples_required(statePtr)
|
|
26
26
|
const bufferSize = Math.min(samplesRequired, sampleCount)
|
|
27
27
|
|
|
28
|
-
const bufferChannelPtrsRef =
|
|
28
|
+
const bufferChannelPtrsRef = wasmHeap.allocUint32Array(bufferSize)
|
|
29
29
|
const bufferChannelRefs: Float32ArrayRef[] = []
|
|
30
30
|
|
|
31
31
|
for (let i = 0; i < channelCount; i++) {
|
|
32
|
-
const bufferChannelRef =
|
|
32
|
+
const bufferChannelRef = wasmHeap.allocFloat32Array(bufferSize)
|
|
33
33
|
bufferChannelPtrsRef.view[i] = bufferChannelRef.address
|
|
34
34
|
|
|
35
35
|
bufferChannelRefs.push(bufferChannelRef)
|
|
@@ -102,7 +102,7 @@ export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchS
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
m._rubberband_delete(statePtr)
|
|
105
|
-
|
|
105
|
+
wasmHeap.freeAll()
|
|
106
106
|
|
|
107
107
|
const outputAudioChannels = outputAudioChannelChunks.map(chunks => concatFloat32Arrays(chunks))
|
|
108
108
|
const outputRawAudio: RawAudio = { audioChannels: outputAudioChannels, sampleRate }
|
package/src/dsp/Sonic.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RawAudio } from '../audio/AudioUtilities.js'
|
|
2
|
-
import {
|
|
2
|
+
import { wrapEmscriptenModuleHeap } from 'wasm-heap-manager'
|
|
3
3
|
|
|
4
4
|
let sonicInstance: any
|
|
5
5
|
|
|
@@ -10,13 +10,13 @@ export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchS
|
|
|
10
10
|
const inputSampleCount = rawAudio.audioChannels[0].length
|
|
11
11
|
|
|
12
12
|
const m = await getSonicInstance()
|
|
13
|
-
const
|
|
13
|
+
const wasmHeap = wrapEmscriptenModuleHeap(m)
|
|
14
14
|
|
|
15
15
|
const streamPtr = m._sonicCreateStream(sampleRate, channelCount)
|
|
16
16
|
m._sonicSetSpeed(streamPtr, speed)
|
|
17
17
|
m._sonicSetPitch(streamPtr, pitchScale)
|
|
18
18
|
|
|
19
|
-
const inputSamplesRef =
|
|
19
|
+
const inputSamplesRef = wasmHeap.allocFloat32Array(inputSampleCount)
|
|
20
20
|
inputSamplesRef.view.set(inputSamples)
|
|
21
21
|
|
|
22
22
|
const writeSuccess = m._sonicWriteFloatToStream(streamPtr, inputSamplesRef.address, inputSampleCount)
|
|
@@ -33,9 +33,9 @@ export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchS
|
|
|
33
33
|
|
|
34
34
|
const samplesAvailable = m._sonicSamplesAvailable(streamPtr)
|
|
35
35
|
|
|
36
|
-
const outputSamplesRef =
|
|
36
|
+
const outputSamplesRef = wasmHeap.allocFloat32Array(samplesAvailable)
|
|
37
37
|
|
|
38
|
-
const samplesRead = m._sonicReadFloatFromStream(streamPtr, outputSamplesRef.address, outputSamplesRef.
|
|
38
|
+
const samplesRead = m._sonicReadFloatFromStream(streamPtr, outputSamplesRef.address, outputSamplesRef.allocatedByteCount)
|
|
39
39
|
|
|
40
40
|
const outputSamples = outputSamplesRef.view.slice(0, samplesRead)
|
|
41
41
|
|
|
@@ -43,7 +43,7 @@ export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchS
|
|
|
43
43
|
|
|
44
44
|
m._sonicDestroyStream(streamPtr)
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
wasmHeap.freeAll()
|
|
47
47
|
|
|
48
48
|
return resultAudio
|
|
49
49
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RawAudio, cloneRawAudio } from '../audio/AudioUtilities.js'
|
|
2
2
|
import { concatFloat32Arrays, isWasmSimdSupported } from '../utilities/Utilities.js'
|
|
3
|
-
import {
|
|
3
|
+
import { wrapEmscriptenModuleHeap } from 'wasm-heap-manager'
|
|
4
4
|
|
|
5
5
|
let speexResamplerInstance: any
|
|
6
6
|
|
|
@@ -23,17 +23,17 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const m = await getSpeexResamplerInstance()
|
|
26
|
-
const
|
|
26
|
+
const wasmHeap = wrapEmscriptenModuleHeap(m)
|
|
27
27
|
|
|
28
28
|
function speexResultCodeToString(resultCode: number) {
|
|
29
29
|
const errorStrPtr = m._speex_resampler_strerror(resultCode)
|
|
30
|
-
const
|
|
31
|
-
const message =
|
|
30
|
+
const messageRef = wasmHeap.wrapNullTerminatedUtf8String(errorStrPtr)
|
|
31
|
+
const message = messageRef.value
|
|
32
32
|
|
|
33
33
|
return message
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const initErrRef =
|
|
36
|
+
const initErrRef = wasmHeap.allocInt32()
|
|
37
37
|
const resamplerStateAddress = m._speex_resampler_init(channelCount, inSampleRate, outSampleRate, quality, initErrRef.address)
|
|
38
38
|
let resultCode = initErrRef.value
|
|
39
39
|
|
|
@@ -46,11 +46,11 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
|
|
|
46
46
|
|
|
47
47
|
const maxChunkSize = 2 ** 20
|
|
48
48
|
|
|
49
|
-
const inputChunkSampleCountRef =
|
|
50
|
-
const outputChunkSampleCountRef =
|
|
49
|
+
const inputChunkSampleCountRef = wasmHeap.allocInt32()
|
|
50
|
+
const outputChunkSampleCountRef = wasmHeap.allocInt32()
|
|
51
51
|
|
|
52
|
-
const inputChunkSamplesRef =
|
|
53
|
-
const outputChunkSamplesRef =
|
|
52
|
+
const inputChunkSamplesRef = wasmHeap.allocFloat32Array(maxChunkSize * 2)
|
|
53
|
+
const outputChunkSamplesRef = wasmHeap.allocFloat32Array(Math.floor(maxChunkSize * sampleRateRatio) * 2)
|
|
54
54
|
|
|
55
55
|
const resampledAudioChunksForChannels: Float32Array[][] = []
|
|
56
56
|
|
|
@@ -65,7 +65,7 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
|
|
|
65
65
|
const inputPaddingSize = isLastChunk ? inputLatency : 0
|
|
66
66
|
const maxSamplesToRead = Math.min(maxChunkSize, totalSampleCount - readOffset) + inputPaddingSize
|
|
67
67
|
|
|
68
|
-
const maxSamplesToWrite = outputChunkSamplesRef.
|
|
68
|
+
const maxSamplesToWrite = outputChunkSamplesRef.elementCount
|
|
69
69
|
|
|
70
70
|
const inputChunkSamplesForChannel = rawAudio.audioChannels[channelIndex].slice(readOffset, readOffset + maxSamplesToRead)
|
|
71
71
|
|
|
@@ -91,7 +91,7 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
m._speex_resampler_destroy(resamplerStateAddress)
|
|
94
|
-
|
|
94
|
+
wasmHeap.freeAll()
|
|
95
95
|
|
|
96
96
|
const resampledAudio: RawAudio = {
|
|
97
97
|
audioChannels: [],
|
|
@@ -57,7 +57,7 @@ export async function phonemizeText(text: string, voice: string, substitutionMap
|
|
|
57
57
|
.replaceAll('«', ', ')
|
|
58
58
|
.replaceAll('»', ', ')
|
|
59
59
|
|
|
60
|
-
const segmentedText = await Segmentation.
|
|
60
|
+
const segmentedText = await Segmentation.parseTextAndConvertToFragmentObjects(text, voice)
|
|
61
61
|
const preparedPhrases: string[] = []
|
|
62
62
|
const phraseBreakers: string[] = []
|
|
63
63
|
|