echogarden 2.1.1 → 2.1.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/README.md +18 -15
- package/data/lexicons/heteronyms.en.json +297 -117
- package/data/lexicons/words.en.json +200 -0
- package/dist/nlp/Lexicon.d.ts +2 -0
- package/dist/nlp/Lexicon.js +19 -19
- package/dist/nlp/Lexicon.js.map +1 -1
- package/dist/synthesis/EspeakTTS.js +1 -1
- package/dist/synthesis/EspeakTTS.js.map +1 -1
- package/docs/CLI.md +1 -1
- package/docs/CUDA.md +17 -0
- package/docs/Development.md +9 -1
- package/docs/Engines.md +1 -1
- package/docs/Options.md +15 -14
- package/docs/Tasklist.md +0 -1
- package/docs/Technical.md +18 -11
- package/package.json +3 -3
- package/src/nlp/Lexicon.ts +24 -21
- package/src/synthesis/EspeakTTS.ts +1 -1
package/docs/Technical.md
CHANGED
|
@@ -2,36 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
* Echogarden is written in TypeScript and targets the Node.js platform.
|
|
4
4
|
* It uses ESM modules and latest ECMAScript and TypeScript features.
|
|
5
|
-
* It does not depend on essential binary executables. Instead, all of its engines either use pure JavaScript, WebAssembly, WASI, or the ONNX runtime, with some exceptions: the CLI does invoke a command line `ffmpeg` tool, auto-downloaded
|
|
5
|
+
* It does not depend on essential binary executables. Instead, all of its engines either use pure JavaScript, WebAssembly, WASI, or the ONNX runtime, with some exceptions: the CLI does invoke a command line `ffmpeg` tool, auto-downloaded using its internal package system.
|
|
6
6
|
* It does not depend on essential native node.js modules requiring install-time compilation with `node-gyp`. This greatly simplifies the installation experience for end-users (the ONNX runtime bundles precompiled NAPI modules for all supported platforms - it doesn't require any compilation during its installation).
|
|
7
7
|
|
|
8
8
|
## Package system
|
|
9
9
|
|
|
10
10
|
Echogarden uses its own package system to download and install various components as needed. These components can be TTS voices, STT models, or other types of models and data.
|
|
11
11
|
|
|
12
|
-
Packages are downloaded as `.tar.gz` files, and are extracted to `[data-folder]/packages/[package-
|
|
12
|
+
Packages are downloaded as `.tar.gz` files, and are extracted to `[data-folder]/packages/[package-id-string]`. Each package has its own subdirectory.
|
|
13
13
|
|
|
14
14
|
`[data-folder]` is located at:
|
|
15
15
|
* `%AppData%\Local\echogarden` on Windows
|
|
16
16
|
* `Users/User/Library/Application Support/echogarden` on macOS
|
|
17
17
|
* `/home/user/.local/share/echogarden` on Linux
|
|
18
18
|
|
|
19
|
+
`[package-id-string]` is formatted as:
|
|
20
|
+
```
|
|
21
|
+
[engine name]-[package id]-[date as yyyymmdd]
|
|
22
|
+
```
|
|
23
|
+
|
|
19
24
|
By using downloadable packages, the installed size is made significantly smaller and the installation faster. The total size of all available packages is currently about 50GB (may not be up-to-date).
|
|
20
25
|
|
|
21
|
-
The packages are currently hosted and downloaded from a dedicated
|
|
26
|
+
The packages are currently hosted and downloaded from a [dedicated Hugging Face repository](https://huggingface.co/echogarden/echogarden-packages).
|
|
22
27
|
|
|
23
28
|
## Can the base install size be made smaller?
|
|
24
29
|
|
|
25
|
-
The base installed (uncompressed) size, including dependencies, is around
|
|
30
|
+
The base installed (uncompressed) size, including dependencies, is around 400MB (may not be up-to-date). This is without any models or voices, which are downloaded as needed.
|
|
26
31
|
|
|
27
32
|
Currently, the largest contributors to the size are:
|
|
28
33
|
|
|
29
|
-
* `onnxruntime-node` (NAPI):
|
|
30
|
-
* `kuromoji` (JavaScript) 40MB
|
|
31
|
-
* `
|
|
32
|
-
* `
|
|
34
|
+
* `onnxruntime-node` (core inference runtime, NAPI): 180MB
|
|
35
|
+
* `kuromoji` (Japanese tokenizer, JavaScript) 40MB
|
|
36
|
+
* `espeak-ng-emscripten` (core synthesis and phonemization engine, WASM): 23MB
|
|
37
|
+
* `tiktoken` (tokenizer library, WASM): 22MB
|
|
38
|
+
* `flite-wasi` (synthesis engine, WASI): 20MB
|
|
39
|
+
* `jieba-wasm` (Chinese tokenizer, WASM): 14MB
|
|
33
40
|
|
|
34
|
-
`onnxruntime-node` is large because it bundles pre-compiled binaries for multiple platforms. `kuromoji` is large because of its dictionary files and some unessential test code it bundles. The other
|
|
41
|
+
`onnxruntime-node` is large because it bundles pre-compiled binaries for multiple platforms. `kuromoji` is large because of its dictionary files and some unessential test code it bundles. The other packages include large WASM binaries.
|
|
35
42
|
|
|
36
43
|
So, yes, in the future it may be possible to reduce the core installed size by dynamically installing some of these dependencies, or using modified, "slimmed-down" versions of some packages.
|
|
37
44
|
|
|
@@ -44,7 +51,7 @@ However, it is a lot of work, and only a subset of the engines can be supported
|
|
|
44
51
|
* Significantly slower inference when using CPU for ONNX models
|
|
45
52
|
* No cross-domain network connectivity - can't connect to Google Cloud, Microsoft, Amazon etc. without a proxy
|
|
46
53
|
* Large initial download size would make it too heavy and slow to load as part of a standard web page directly
|
|
47
|
-
* Large memory
|
|
54
|
+
* Large memory requirements for the various ONNX models, starting at about 800MB - 1GB, which is a bit too much for a browser
|
|
48
55
|
* Due to the high code complexity, data size, and memory consumption, it is unlikely that a browser extension, internally bundling some of the models, would be accepted to the Chrome and Firefox web stores
|
|
49
56
|
* Will require a virtual file system to store models and make use of downloadable packages
|
|
50
57
|
* Requires duplicating a lot of prior work, porting many node.js-only APIs, and increasing code complexity
|
|
@@ -71,5 +78,5 @@ Also, `option=value` is more similar to the syntax used in the configuration fil
|
|
|
71
78
|
|
|
72
79
|
* `src`: TypeScript source code
|
|
73
80
|
* `dist`: compiled JavaScript modules
|
|
74
|
-
* `data`: various data files, including phonetic lexicons and language code conversion tables. `data/schemas` stores JSON schemas for all configuration options, auto-generated using [`ts-json-schema-generator`](https://github.com/vega/ts-json-schema-generator) directly from the TypeScript code, and used by the CLI to parse and validate the options provided
|
|
81
|
+
* `data`: various data files, including phonetic lexicons and language code conversion tables. `data/schemas` stores JSON schemas for all configuration options, auto-generated using [`ts-json-schema-generator`](https://github.com/vega/ts-json-schema-generator) directly from the TypeScript code, and used by the CLI to parse and validate the options provided. `data/lexicons` contains phonetic lexicons used in speech synthesis and alignment
|
|
75
82
|
* `docs`: documentation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echogarden",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"microsoft-cognitiveservices-speech-sdk": "^1.42.0",
|
|
86
86
|
"msgpack-lite": "^0.1.26",
|
|
87
87
|
"onnxruntime-node": "^1.20.1",
|
|
88
|
-
"openai": "^4.
|
|
88
|
+
"openai": "^4.85.1",
|
|
89
89
|
"sam-js": "^0.3.1",
|
|
90
90
|
"strip-ansi": "^7.1.0",
|
|
91
91
|
"tar": "^7.4.3",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"@types/graceful-fs": "^4.1.9",
|
|
113
113
|
"@types/jsdom": "^21.1.7",
|
|
114
114
|
"@types/msgpack-lite": "^0.1.11",
|
|
115
|
-
"@types/node": "^22.13.
|
|
115
|
+
"@types/node": "^22.13.4",
|
|
116
116
|
"@types/tar": "^6.1.13",
|
|
117
117
|
"@types/ws": "^8.5.14",
|
|
118
118
|
"ts-json-schema-generator": "^2.3.0",
|
package/src/nlp/Lexicon.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { getShortLanguageCode } from '../utilities/Locale.js'
|
|
|
3
3
|
import { resolveToModuleRootDir } from '../utilities/PathUtilities.js'
|
|
4
4
|
|
|
5
5
|
export function tryGetFirstLexiconSubstitution(sentenceWords: string[], wordIndex: number, lexicons: Lexicon[], languageCode: string) {
|
|
6
|
-
|
|
6
|
+
for (let i = lexicons.length - 1; i >= 0; i--) {
|
|
7
|
+
const lexicon = lexicons[i]
|
|
7
8
|
|
|
8
|
-
for (const lexicon of reversedLexicons) {
|
|
9
9
|
const match = tryGetLexiconSubstitution(sentenceWords, wordIndex, lexicon, languageCode)
|
|
10
10
|
|
|
11
11
|
if (match) {
|
|
@@ -20,51 +20,51 @@ export function tryGetLexiconSubstitution(sentenceWords: string[], wordIndex: nu
|
|
|
20
20
|
let word = sentenceWords[wordIndex]
|
|
21
21
|
|
|
22
22
|
if (!word) {
|
|
23
|
-
return
|
|
23
|
+
return undefined
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const shortLanguageCode = getShortLanguageCode(languageCode)
|
|
27
27
|
const lexiconForLanguage = lexicon[shortLanguageCode]
|
|
28
28
|
|
|
29
29
|
if (!lexiconForLanguage) {
|
|
30
|
-
return
|
|
30
|
+
return undefined
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const lexiconEntry = lexiconForLanguage[word]
|
|
34
34
|
|
|
35
35
|
if (!lexiconEntry) {
|
|
36
|
-
return
|
|
36
|
+
return undefined
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
for (let i = 0; i < lexiconEntry.length; i++) {
|
|
40
|
-
const
|
|
40
|
+
const candidateEntry = lexiconEntry[i]
|
|
41
41
|
|
|
42
|
-
const
|
|
42
|
+
const pronunciationPhonemesText = candidateEntry?.pronunciation?.espeak?.[languageCode]
|
|
43
43
|
|
|
44
|
-
if (!
|
|
44
|
+
if (!pronunciationPhonemesText) {
|
|
45
45
|
continue
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const precedingWord = sentenceWords[wordIndex - 1]
|
|
49
|
-
const
|
|
48
|
+
const precedingWord = sentenceWords[wordIndex - 1] ?? ''
|
|
49
|
+
const followingWord = sentenceWords[wordIndex + 1] ?? ''
|
|
50
50
|
|
|
51
|
-
const precededBy =
|
|
52
|
-
const notPrecededBy =
|
|
51
|
+
const precededBy = candidateEntry?.precededBy ?? []
|
|
52
|
+
const notPrecededBy = candidateEntry?.notPrecededBy ?? []
|
|
53
53
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
54
|
+
const followedBy = candidateEntry?.followedBy ?? candidateEntry?.succeededBy ?? []
|
|
55
|
+
const notFollowedBy = candidateEntry?.notFollowedBy ?? candidateEntry?.notSucceededBy ?? []
|
|
56
56
|
|
|
57
|
-
const hasNegativePattern = notPrecededBy.includes(precedingWord) ||
|
|
58
|
-
const hasPositivePattern = precededBy.includes(precedingWord) ||
|
|
57
|
+
const hasNegativePattern = notPrecededBy.includes(precedingWord) || notFollowedBy.includes(followingWord)
|
|
58
|
+
const hasPositivePattern = precededBy.includes(precedingWord) || followedBy.includes(followingWord)
|
|
59
59
|
|
|
60
|
-
if (i
|
|
61
|
-
const substitutionPhonemes =
|
|
60
|
+
if (i === lexiconEntry.length - 1 || (hasPositivePattern && !hasNegativePattern)) {
|
|
61
|
+
const substitutionPhonemes = pronunciationPhonemesText.split(/ +/g)
|
|
62
62
|
|
|
63
63
|
return substitutionPhonemes
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
return
|
|
67
|
+
return undefined
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export async function loadLexiconFile(jsonFilePath: string): Promise<Lexicon> {
|
|
@@ -115,8 +115,11 @@ export type LexiconEntry = {
|
|
|
115
115
|
precededBy?: string[]
|
|
116
116
|
notPrecededBy?: string[]
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
followedBy?: string[]
|
|
119
|
+
notFollowedBy?: string[]
|
|
120
|
+
|
|
121
|
+
succeededBy?: string[] // Deprecated. Replaced by 'followedBy'
|
|
122
|
+
notSucceededBy?: string[] // Deprecated. Replaced by 'notFollowedBy'
|
|
120
123
|
|
|
121
124
|
example?: string
|
|
122
125
|
}
|
|
@@ -324,7 +324,7 @@ export async function synthesizeFragments(fragments: string[], espeakOptions: Es
|
|
|
324
324
|
throw new Error('Unexpected: token timeline should exist and have at least one token')
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
if (tokenTimeline.length !== 1
|
|
327
|
+
if (tokenTimeline.length !== 1 || tokenTimeline[0].text !== '') {
|
|
328
328
|
continue
|
|
329
329
|
}
|
|
330
330
|
|