echogarden 1.0.0 → 1.0.2
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/schemas/options.json +3 -3
- package/dist/alignment/SpeechAlignment.d.ts +7 -1
- package/dist/alignment/SpeechAlignment.js +67 -55
- package/dist/alignment/SpeechAlignment.js.map +1 -1
- package/dist/api/Alignment.js +16 -8
- package/dist/api/Alignment.js.map +1 -1
- package/dist/dsp/FFT.js +1 -1
- package/dist/dsp/FFT.js.map +1 -1
- package/dist/nlp/Segmentation.d.ts +2 -0
- package/dist/nlp/Segmentation.js +10 -5
- package/dist/nlp/Segmentation.js.map +1 -1
- package/dist/recognition/WhisperCppSTT.js +3 -2
- package/dist/recognition/WhisperCppSTT.js.map +1 -1
- package/dist/recognition/WhisperSTT.d.ts +2 -2
- package/dist/recognition/WhisperSTT.js +38 -16
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/subtitles/Subtitles.js +2 -2
- package/dist/subtitles/Subtitles.js.map +1 -1
- package/dist/tests/Test.js +13 -0
- package/dist/tests/Test.js.map +1 -1
- package/docs/CLI.md +7 -0
- package/docs/Development.md +1 -3
- package/docs/Engines.md +1 -1
- package/docs/Options.md +5 -11
- package/docs/Releases.md +41 -30
- package/docs/Tasklist.md +7 -3
- package/package.json +4 -4
- package/src/alignment/SpeechAlignment.ts +88 -63
- package/src/api/Alignment.ts +20 -15
- package/src/dsp/FFT.ts +1 -1
- package/src/nlp/Segmentation.ts +13 -5
- package/src/recognition/WhisperCppSTT.ts +3 -2
- package/src/recognition/WhisperSTT.ts +46 -18
- package/src/subtitles/Subtitles.ts +1 -1
- package/src/tests/Test.ts +17 -1
package/src/api/Alignment.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { WhisperOptions } from '../recognition/WhisperSTT.js'
|
|
|
11
11
|
import chalk from 'chalk'
|
|
12
12
|
import { DtwGranularity, createAlignmentReferenceUsingEspeak } from '../alignment/SpeechAlignment.js'
|
|
13
13
|
import { SubtitlesConfig, defaultSubtitlesBaseConfig } from '../subtitles/Subtitles.js'
|
|
14
|
-
|
|
14
|
+
import { EspeakOptions, defaultEspeakOptions } from '../synthesis/EspeakTTS.js'
|
|
15
|
+
import { isWord } from '../nlp/Segmentation.js'
|
|
15
16
|
|
|
16
17
|
const log = logToStderr
|
|
17
18
|
|
|
@@ -145,7 +146,12 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
145
146
|
logger.end()
|
|
146
147
|
|
|
147
148
|
// Recognize source audio
|
|
148
|
-
|
|
149
|
+
let { wordTimeline: recognitionTimeline } = await API.recognize(sourceRawAudio, recognitionOptions)
|
|
150
|
+
|
|
151
|
+
logger.log('')
|
|
152
|
+
|
|
153
|
+
// Remove non-word entries from recognition timeline
|
|
154
|
+
recognitionTimeline = recognitionTimeline.filter(entry => isWord(entry.text))
|
|
149
155
|
|
|
150
156
|
// Synthesize the ground-truth transcript and get its timeline
|
|
151
157
|
logger.start('Synthesize ground-truth transcript with eSpeak')
|
|
@@ -153,15 +159,8 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
153
159
|
const {
|
|
154
160
|
referenceRawAudio,
|
|
155
161
|
referenceTimeline,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Synthesize the recognized transcript and get its timeline
|
|
159
|
-
logger.start('Synthesize recognized transcript with eSpeak')
|
|
160
|
-
|
|
161
|
-
const {
|
|
162
|
-
referenceRawAudio: synthesizedRecognizedTranscriptRawAudio,
|
|
163
|
-
referenceTimeline: synthesizedRecognitionTimeline
|
|
164
|
-
} = await createAlignmentReferenceUsingEspeak(recognizedTranscript, language, undefined, undefined, true)
|
|
162
|
+
espeakVoice,
|
|
163
|
+
} = await createAlignmentReferenceUsingEspeak(transcript, language, options.plainText, options.customLexiconPaths, false)
|
|
165
164
|
|
|
166
165
|
logger.end()
|
|
167
166
|
|
|
@@ -169,6 +168,13 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
169
168
|
|
|
170
169
|
const phoneAlignmentMethod = options.dtw!.phoneAlignmentMethod!
|
|
171
170
|
|
|
171
|
+
const espeakOptions: EspeakOptions = {
|
|
172
|
+
...defaultEspeakOptions,
|
|
173
|
+
voice: espeakVoice,
|
|
174
|
+
useKlatt: false,
|
|
175
|
+
insertSeparators: true
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
// Align the ground-truth transcript and the recognized transcript
|
|
173
179
|
mappedTimeline = await alignUsingDtwWithRecognitionReference(
|
|
174
180
|
sourceRawAudio,
|
|
@@ -176,11 +182,10 @@ export async function align(input: AudioSourceParam, transcript: string, options
|
|
|
176
182
|
referenceTimeline,
|
|
177
183
|
|
|
178
184
|
recognitionTimeline,
|
|
179
|
-
synthesizedRecognizedTranscriptRawAudio,
|
|
180
|
-
synthesizedRecognitionTimeline,
|
|
181
185
|
|
|
182
186
|
granularities,
|
|
183
187
|
windowDurations,
|
|
188
|
+
espeakOptions,
|
|
184
189
|
phoneAlignmentMethod)
|
|
185
190
|
|
|
186
191
|
break
|
|
@@ -346,10 +351,10 @@ export const defaultAlignmentOptions: AlignmentOptions = {
|
|
|
346
351
|
topCandidateCount: 5,
|
|
347
352
|
punctuationThreshold: 0.2,
|
|
348
353
|
maxTokensPerPart: 250,
|
|
349
|
-
autoPromptParts:
|
|
354
|
+
autoPromptParts: false,
|
|
350
355
|
suppressRepetition: true,
|
|
356
|
+
decodeTimestampTokens: true,
|
|
351
357
|
seed: undefined,
|
|
352
|
-
decodeTimestampTokens: false,
|
|
353
358
|
}
|
|
354
359
|
},
|
|
355
360
|
|
package/src/dsp/FFT.ts
CHANGED
|
@@ -122,7 +122,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
|
|
|
122
122
|
|
|
123
123
|
wasmMemory.freeAll()
|
|
124
124
|
|
|
125
|
-
// Divide by sum of
|
|
125
|
+
// Divide each output sample by the sum of squared weights
|
|
126
126
|
for (let i = 0; i < outSamples.length; i++) {
|
|
127
127
|
outSamples[i] /= sumOfSquaredWeightsForSample[i] + 1e-8
|
|
128
128
|
}
|
package/src/nlp/Segmentation.ts
CHANGED
|
@@ -8,8 +8,9 @@ import { ParagraphBreakType, WhitespaceProcessing } from '../api/Common.js'
|
|
|
8
8
|
|
|
9
9
|
const log = logToStderr
|
|
10
10
|
|
|
11
|
-
export const wordCharacterPattern = /[\p{Letter}\p{Number}]
|
|
12
|
-
export const punctuationPattern = /[\p{Punctuation}]
|
|
11
|
+
export const wordCharacterPattern = /[\p{Letter}\p{Number}]+/u
|
|
12
|
+
export const punctuationPattern = /[\p{Punctuation}]+/u
|
|
13
|
+
|
|
13
14
|
export const phraseSeparators = [',', ';', ':']
|
|
14
15
|
export const sentenceSeparators = ['.', '?', '!']
|
|
15
16
|
export const symbolWords = ['$', '€', '¢', '£', '¥', '©', '®', '™', '%', '&', '#', '~', '@', '+', '±', '÷', '/', '*', '=', '¼', '½', '¾']
|
|
@@ -18,13 +19,20 @@ export function isWordOrSymbolWord(str: string) {
|
|
|
18
19
|
return isWord(str) || symbolWords.includes(str)
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
export function isSymbolWord(str: string) {
|
|
23
|
+
return symbolWords.includes(str.trim())
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export function isWord(str: string) {
|
|
22
|
-
|
|
23
|
-
return wordCharacterPattern.test(str) || symbolWords.includes(str)
|
|
27
|
+
return wordCharacterPattern.test(str.trim())
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export function isPunctuation(str: string) {
|
|
27
|
-
return punctuationPattern.test(str)
|
|
31
|
+
return punctuationPattern.test(str.trim())
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isWhitespace(str: string) {
|
|
35
|
+
return str.trim().length === 0
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
export class Sentence {
|
|
@@ -288,13 +288,14 @@ async function parseResultObject(resultObject: WhisperCppVerboseResult, modelNam
|
|
|
288
288
|
|
|
289
289
|
const allTokenIds = tokenTimeline.map(entry => entry.id!)
|
|
290
290
|
const transcript = whisper.tokensToText(allTokenIds).trim()
|
|
291
|
+
const language = resultObject.result.language
|
|
291
292
|
|
|
292
|
-
|
|
293
|
+
const timeline = whisper.tokenTimelineToWordTimeline(tokenTimeline, language)
|
|
293
294
|
|
|
294
295
|
return {
|
|
295
296
|
transcript,
|
|
296
297
|
timeline,
|
|
297
|
-
language
|
|
298
|
+
language
|
|
298
299
|
}
|
|
299
300
|
}
|
|
300
301
|
|
|
@@ -19,6 +19,7 @@ import chalk from 'chalk'
|
|
|
19
19
|
import { XorShift32RNG } from '../utilities/RandomGenerator.js'
|
|
20
20
|
import { detectSpeechLanguageByParts } from '../api/LanguageDetection.js'
|
|
21
21
|
import { type Tiktoken } from 'tiktoken/lite'
|
|
22
|
+
import { isPunctuation, isWhitespace } from '../nlp/Segmentation.js'
|
|
22
23
|
|
|
23
24
|
export async function recognize(sourceRawAudio: RawAudio, modelName: WhisperModelName, modelDir: string, task: WhisperTask, sourceLanguage: string, options: WhisperOptions) {
|
|
24
25
|
if (sourceRawAudio.sampleRate != 16000) {
|
|
@@ -398,7 +399,7 @@ export class Whisper {
|
|
|
398
399
|
logger.end()
|
|
399
400
|
}
|
|
400
401
|
|
|
401
|
-
timeline = this.tokenTimelineToWordTimeline(timeline)
|
|
402
|
+
timeline = this.tokenTimelineToWordTimeline(timeline, language)
|
|
402
403
|
|
|
403
404
|
const transcript = this.tokensToText(allDecodedTokens).trim()
|
|
404
405
|
|
|
@@ -438,7 +439,7 @@ export class Whisper {
|
|
|
438
439
|
const alignmentPath = await this.findAlignmentPathFromQKs(crossAttentionQKs, tokens, 0, audioFrameCount)//, this.getAlignmentHeadIndexes())
|
|
439
440
|
let timeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, tokens, 0, audioDuration)
|
|
440
441
|
|
|
441
|
-
timeline = this.tokenTimelineToWordTimeline(timeline)
|
|
442
|
+
timeline = this.tokenTimelineToWordTimeline(timeline, language)
|
|
442
443
|
|
|
443
444
|
logger.end()
|
|
444
445
|
|
|
@@ -1022,33 +1023,40 @@ export class Whisper {
|
|
|
1022
1023
|
}
|
|
1023
1024
|
}
|
|
1024
1025
|
|
|
1025
|
-
tokenTimelineToWordTimeline(tokenTimeline: Timeline) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1026
|
+
tokenTimelineToWordTimeline(tokenTimeline: Timeline, language: string): Timeline {
|
|
1027
|
+
function isSeparatorCharacter(char: string) {
|
|
1028
|
+
const nonSeparatingPunctuation = [`'`, `-`, `.`, `·`, `•`]
|
|
1028
1029
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1030
|
+
if (nonSeparatingPunctuation.includes(char)) {
|
|
1031
|
+
return false
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return isWhitespace(char) || isPunctuation(char)
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function startsWithSeparatorCharacter(text: string) {
|
|
1038
|
+
return isSeparatorCharacter(text[0])
|
|
1031
1039
|
}
|
|
1032
1040
|
|
|
1033
|
-
function
|
|
1034
|
-
return
|
|
1041
|
+
function endsWithSeparatorCharacter(text: string) {
|
|
1042
|
+
return isSeparatorCharacter(text[text.length - 1])
|
|
1035
1043
|
}
|
|
1036
1044
|
|
|
1037
1045
|
const resultTimeline: Timeline = []
|
|
1038
1046
|
|
|
1039
|
-
|
|
1047
|
+
let groups: TimelineEntry[][] = []
|
|
1040
1048
|
|
|
1041
|
-
for (let
|
|
1042
|
-
const entry = tokenTimeline[
|
|
1043
|
-
const previousEntry =
|
|
1049
|
+
for (let tokenIndex = 0; tokenIndex < tokenTimeline.length; tokenIndex++) {
|
|
1050
|
+
const entry = tokenTimeline[tokenIndex]
|
|
1051
|
+
const previousEntry = tokenIndex > 0 ? tokenTimeline[tokenIndex - 1] : undefined
|
|
1044
1052
|
|
|
1045
1053
|
const text = entry.text
|
|
1046
1054
|
const previousEntryText = previousEntry?.text
|
|
1047
1055
|
|
|
1048
1056
|
if (groups.length == 0 ||
|
|
1049
1057
|
text === '' ||
|
|
1050
|
-
|
|
1051
|
-
(previousEntryText != null &&
|
|
1058
|
+
startsWithSeparatorCharacter(text) ||
|
|
1059
|
+
(previousEntryText != null && endsWithSeparatorCharacter(previousEntryText))) {
|
|
1052
1060
|
|
|
1053
1061
|
groups.push([entry])
|
|
1054
1062
|
} else {
|
|
@@ -1056,8 +1064,28 @@ export class Whisper {
|
|
|
1056
1064
|
}
|
|
1057
1065
|
}
|
|
1058
1066
|
|
|
1067
|
+
const newGroups: TimelineEntry[][] = []
|
|
1068
|
+
|
|
1069
|
+
for (let groupIndex = 0; groupIndex < groups.length - 1; groupIndex++) {
|
|
1070
|
+
const group = groups[groupIndex]
|
|
1071
|
+
const nextGroup = groups[groupIndex + 1]
|
|
1072
|
+
|
|
1073
|
+
if (
|
|
1074
|
+
group.length > 1 &&
|
|
1075
|
+
group[group.length - 1].text === '.' &&
|
|
1076
|
+
[' ', '['].includes(nextGroup[0].text[0])) {
|
|
1077
|
+
|
|
1078
|
+
newGroups.push(group.slice(0, group.length - 1))
|
|
1079
|
+
newGroups.push(group.slice(group.length - 1))
|
|
1080
|
+
} else {
|
|
1081
|
+
newGroups.push(group)
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
groups = newGroups
|
|
1086
|
+
|
|
1059
1087
|
for (const group of groups) {
|
|
1060
|
-
|
|
1088
|
+
let groupText = this.tokensToText(group.map(entry => entry.id!))
|
|
1061
1089
|
|
|
1062
1090
|
if (groupText === '') {
|
|
1063
1091
|
continue
|
|
@@ -1755,8 +1783,8 @@ export interface WhisperOptions {
|
|
|
1755
1783
|
autoPromptParts?: boolean
|
|
1756
1784
|
maxTokensPerPart?: number
|
|
1757
1785
|
suppressRepetition?: boolean
|
|
1758
|
-
seed?: number
|
|
1759
1786
|
decodeTimestampTokens?: boolean
|
|
1787
|
+
seed?: number
|
|
1760
1788
|
}
|
|
1761
1789
|
|
|
1762
1790
|
export const defaultWhisperOptions: WhisperOptions = {
|
|
@@ -1768,6 +1796,6 @@ export const defaultWhisperOptions: WhisperOptions = {
|
|
|
1768
1796
|
autoPromptParts: true,
|
|
1769
1797
|
maxTokensPerPart: 250,
|
|
1770
1798
|
suppressRepetition: true,
|
|
1799
|
+
decodeTimestampTokens: true,
|
|
1771
1800
|
seed: undefined,
|
|
1772
|
-
decodeTimestampTokens: false,
|
|
1773
1801
|
}
|
|
@@ -200,7 +200,7 @@ function getCuesFromTimeline_IsolateSegmentSentence(timeline: Timeline, config:
|
|
|
200
200
|
continue
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
const wordTimeline = entry.timeline!.filter(entry =>
|
|
203
|
+
const wordTimeline = entry.timeline!.filter(entry => isWordOrSymbolWord(entry.text))
|
|
204
204
|
|
|
205
205
|
// First, add word start and end offsets for all word entries
|
|
206
206
|
let lastWordEndOffset = 0
|
package/src/tests/Test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { getRepetitionScoreRelativeToFirstSubstring, logToStderr, setupProgramTerminationListeners } from '../utilities/Utilities.js'
|
|
1
|
+
import { getRepetitionScoreRelativeToFirstSubstring, logToStderr, setupProgramTerminationListeners, writeToStderr } from '../utilities/Utilities.js'
|
|
2
2
|
import { makeTarballsForInstalledPackages } from '../utilities/TarballMaker.js'
|
|
3
3
|
import { testEspeakSynthesisWithPrePhonemizedInputs, testKirshenbaumPhonemization } from '../synthesis/EspeakTTS.js'
|
|
4
|
+
import { isPunctuation } from '../nlp/Segmentation.js'
|
|
4
5
|
|
|
5
6
|
const log = logToStderr
|
|
6
7
|
|
|
@@ -21,4 +22,19 @@ setupProgramTerminationListeners()
|
|
|
21
22
|
//getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'a', 'c', 'a', 'b', 'a', 'c', 'a'])
|
|
22
23
|
//getRepetitionScoreRelativeToFirstSubstring(['a', 'a', 'a', 'b', 'b', 'a', 'a', 'a', 'b'])
|
|
23
24
|
|
|
25
|
+
/*
|
|
26
|
+
const allPunctuationChars: string[] = []
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < 65536; i++) {
|
|
29
|
+
const char = String.fromCodePoint(i)
|
|
30
|
+
|
|
31
|
+
if (isPunctuation(char)) {
|
|
32
|
+
allPunctuationChars.push(char)
|
|
33
|
+
|
|
34
|
+
writeToStderr(`${char} `)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
*/
|
|
38
|
+
|
|
24
39
|
process.exit(0)
|
|
40
|
+
|