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.
@@ -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
- const { transcript: recognizedTranscript, wordTimeline: recognitionTimeline } = await API.recognize(sourceRawAudio, recognitionOptions)
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
- } = await createAlignmentReferenceUsingEspeak(transcript, language, options.plainText, options.customLexiconPaths, true)
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: true,
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 weight squares for each samples
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
  }
@@ -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}]/u
12
- export const punctuationPattern = /[\p{Punctuation}]/u
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
- str = str.trim()
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
- let timeline = whisper.tokenTimelineToWordTimeline(tokenTimeline)
293
+ const timeline = whisper.tokenTimelineToWordTimeline(tokenTimeline, language)
293
294
 
294
295
  return {
295
296
  transcript,
296
297
  timeline,
297
- language: resultObject.result.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
- const separatorChars =
1027
- [' ', '–', '一', ',', '、', '|', '/', '\\', ';', '"', '“', '”', '…', '(', ')', '[', ']', '{', '}']
1026
+ tokenTimelineToWordTimeline(tokenTimeline: Timeline, language: string): Timeline {
1027
+ function isSeparatorCharacter(char: string) {
1028
+ const nonSeparatingPunctuation = [`'`, `-`, `.`, `·`, `•`]
1028
1029
 
1029
- function startsWithSeparatingPunctuation(text: string) {
1030
- return separatorChars.some(char => text.startsWith(char))
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 isSeparatorPunctuation(text: string) {
1034
- return separatorChars.includes(text)
1041
+ function endsWithSeparatorCharacter(text: string) {
1042
+ return isSeparatorCharacter(text[text.length - 1])
1035
1043
  }
1036
1044
 
1037
1045
  const resultTimeline: Timeline = []
1038
1046
 
1039
- const groups: TimelineEntry[][] = []
1047
+ let groups: TimelineEntry[][] = []
1040
1048
 
1041
- for (let i = 0; i < tokenTimeline.length; i++) {
1042
- const entry = tokenTimeline[i]
1043
- const previousEntry = i > 0 ? tokenTimeline[i - 1] : undefined
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
- startsWithSeparatingPunctuation(text) ||
1051
- (previousEntryText != null && isSeparatorPunctuation(previousEntryText))) {
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
- const groupText = this.tokensToText(group.map(entry => entry.id!))
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 => isWord(entry.text))
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
+