@goodandready/dsh-tts 0.4.27 → 0.4.29
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/CHANGELOG.md +68 -0
- package/README.md +18 -13
- package/README.ru.md +11 -10
- package/README.zh.md +5 -5
- package/lib/cache.js +70 -40
- package/lib/client.js +338 -166
- package/lib/engines/manager.js +137 -75
- package/lib/index.js +345 -64
- package/lib/local.js +1 -1
- package/lib/providers/cloud.js +44 -5
- package/lib/providers/constants.js +4 -4
- package/lib/providers/local.js +1 -1
- package/lib/providers.js +11 -2
- package/lib/routes.js +107 -36
- package/lib/stream-hub.js +55 -8
- package/lib/text.js +25 -2
- package/package.json +1 -1
package/lib/engines/manager.js
CHANGED
|
@@ -11,11 +11,13 @@ export const MODEL_SPECS = {
|
|
|
11
11
|
files: [
|
|
12
12
|
{
|
|
13
13
|
name: 'kokoro-v0_19.onnx',
|
|
14
|
-
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/
|
|
14
|
+
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/f905c3175b9f7a77d4c1b9be894ff99ff7e9a8f2/kokoro-v0_19.onnx',
|
|
15
|
+
minBytes: 1024,
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
name: 'voices.json',
|
|
18
|
-
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/
|
|
19
|
+
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/f905c3175b9f7a77d4c1b9be894ff99ff7e9a8f2/voices.json',
|
|
20
|
+
minBytes: 1024,
|
|
19
21
|
},
|
|
20
22
|
],
|
|
21
23
|
},
|
|
@@ -28,6 +30,7 @@ export const MODEL_SPECS = {
|
|
|
28
30
|
{
|
|
29
31
|
name: 'model_1200000.safetensors',
|
|
30
32
|
url: 'https://huggingface.co/SWivid/F5-TTS/resolve/main/F5TTS_Base/model_1200000.safetensors',
|
|
33
|
+
minBytes: 1024,
|
|
31
34
|
},
|
|
32
35
|
],
|
|
33
36
|
},
|
|
@@ -42,13 +45,26 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
42
45
|
return path.join(baseDir, engineId)
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
async function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
async function validateFileIntegrity(filePath, minBytes = 1024) {
|
|
49
|
+
try {
|
|
50
|
+
const st = await fs.promises.stat(filePath)
|
|
51
|
+
if (!st.isFile() || st.size < minBytes) return false
|
|
52
|
+
const fd = await fs.promises.open(filePath, 'r')
|
|
53
|
+
const buf = Buffer.alloc(Math.min(st.size, 1024))
|
|
54
|
+
await fd.read(buf, 0, buf.length, 0)
|
|
55
|
+
await fd.close()
|
|
56
|
+
const str = buf.toString('utf8').trim().toLowerCase()
|
|
57
|
+
if (str.startsWith('<!doctype') || str.startsWith('<html') || str.startsWith('<head') || str.includes('<html')) {
|
|
50
58
|
return false
|
|
51
59
|
}
|
|
60
|
+
return true
|
|
61
|
+
} catch (_err) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function checkFileExists(filePath, minBytes = 1024) {
|
|
67
|
+
return await validateFileIntegrity(filePath, minBytes)
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
async function getDirSize(dirPath) {
|
|
@@ -98,7 +114,7 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
98
114
|
let allFilesExist = true
|
|
99
115
|
for (const f of spec.files) {
|
|
100
116
|
const p = path.join(dir, f.name)
|
|
101
|
-
if (!(await checkFileExists(p))) {
|
|
117
|
+
if (!(await checkFileExists(p, f.minBytes || 1024))) {
|
|
102
118
|
allFilesExist = false
|
|
103
119
|
break
|
|
104
120
|
}
|
|
@@ -132,14 +148,14 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
132
148
|
const spec = MODEL_SPECS[engineId]
|
|
133
149
|
if (!spec) throw new Error(`Unknown engine: ${engineId}`)
|
|
134
150
|
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
const existing = activeDownloads.get(engineId)
|
|
152
|
+
if (existing && existing.promise) {
|
|
153
|
+
return await existing.promise
|
|
137
154
|
}
|
|
138
155
|
lastErrors.delete(engineId)
|
|
139
156
|
|
|
140
157
|
const customFetch = options.fetchImpl || fetchImpl
|
|
141
158
|
const dir = getModelPath(engineId)
|
|
142
|
-
await fs.promises.mkdir(dir, { recursive: true })
|
|
143
159
|
|
|
144
160
|
const controller = new AbortController()
|
|
145
161
|
if (options.signal) {
|
|
@@ -152,90 +168,136 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
152
168
|
downloadedBytes: 0,
|
|
153
169
|
totalBytes: spec.approxMb * 1024 * 1024,
|
|
154
170
|
controller,
|
|
171
|
+
promise: null,
|
|
155
172
|
}
|
|
156
|
-
activeDownloads.set(engineId, downloadState)
|
|
157
|
-
let totalDownloaded = 0
|
|
158
173
|
|
|
159
|
-
|
|
160
|
-
for (const f of spec.files) {
|
|
161
|
-
if (options.signal?.aborted || controller.signal.aborted) {
|
|
162
|
-
throw new Error('Download aborted')
|
|
163
|
-
}
|
|
174
|
+
const createdTmpFiles = []
|
|
164
175
|
|
|
165
|
-
|
|
166
|
-
|
|
176
|
+
const execute = async () => {
|
|
177
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
178
|
+
let totalDownloaded = 0
|
|
179
|
+
try {
|
|
180
|
+
for (const f of spec.files) {
|
|
181
|
+
if (options.signal?.aborted || controller.signal.aborted) {
|
|
182
|
+
throw new Error('Download aborted')
|
|
183
|
+
}
|
|
167
184
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
185
|
+
const targetFile = path.join(dir, f.name)
|
|
186
|
+
const nonce = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
187
|
+
const tmpFile = path.join(dir, `${f.name}.${nonce}.tmp`)
|
|
188
|
+
createdTmpFiles.push(tmpFile)
|
|
189
|
+
|
|
190
|
+
const res = await customFetch(f.url, { signal: controller.signal })
|
|
191
|
+
if (!res.ok) {
|
|
192
|
+
throw new Error(`Failed to download ${f.name}: HTTP ${res.status}`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (res.body && typeof res.body.getReader === 'function') {
|
|
196
|
+
const reader = res.body.getReader()
|
|
197
|
+
const ws = fs.createWriteStream(tmpFile)
|
|
198
|
+
let streamError = null
|
|
199
|
+
const onStreamError = (err) => { streamError = err }
|
|
200
|
+
ws.once('error', onStreamError)
|
|
201
|
+
try {
|
|
202
|
+
while (true) {
|
|
203
|
+
if (streamError) throw streamError
|
|
204
|
+
const { done, value } = await reader.read()
|
|
205
|
+
if (done) break
|
|
206
|
+
if (streamError) throw streamError
|
|
172
207
|
|
|
173
|
-
if (res.body && typeof res.body.getReader === 'function') {
|
|
174
|
-
const reader = res.body.getReader()
|
|
175
|
-
const ws = fs.createWriteStream(tmpFile)
|
|
176
|
-
try {
|
|
177
|
-
while (true) {
|
|
178
|
-
const { done, value } = await reader.read()
|
|
179
|
-
if (done) break
|
|
180
|
-
await new Promise((resolve, reject) => {
|
|
181
208
|
if (!ws.write(value)) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
209
|
+
await new Promise((resolve, reject) => {
|
|
210
|
+
const onDrain = () => { cleanup(); resolve() }
|
|
211
|
+
const onError = (e) => { cleanup(); reject(e) }
|
|
212
|
+
const cleanup = () => {
|
|
213
|
+
ws.removeListener('drain', onDrain)
|
|
214
|
+
ws.removeListener('error', onError)
|
|
215
|
+
}
|
|
216
|
+
ws.once('drain', onDrain)
|
|
217
|
+
ws.once('error', onError)
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
totalDownloaded += value.length
|
|
221
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
222
|
+
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
223
|
+
if (typeof options.onProgress === 'function') {
|
|
224
|
+
options.onProgress(downloadState)
|
|
185
225
|
}
|
|
186
|
-
ws.once('error', reject)
|
|
187
|
-
})
|
|
188
|
-
totalDownloaded += value.length
|
|
189
|
-
downloadState.downloadedBytes = totalDownloaded
|
|
190
|
-
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
191
|
-
if (typeof options.onProgress === 'function') {
|
|
192
|
-
options.onProgress(downloadState)
|
|
193
226
|
}
|
|
227
|
+
} finally {
|
|
228
|
+
ws.removeListener('error', onStreamError)
|
|
229
|
+
await new Promise((resolve, reject) => {
|
|
230
|
+
ws.end((err) => (err ? reject(err) : resolve()))
|
|
231
|
+
})
|
|
194
232
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
233
|
+
} else if (res.arrayBuffer) {
|
|
234
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
235
|
+
totalDownloaded += buf.length
|
|
236
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
237
|
+
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
238
|
+
await fs.promises.writeFile(tmpFile, buf)
|
|
239
|
+
} else if (typeof res.text === 'function') {
|
|
240
|
+
const txt = await res.text()
|
|
241
|
+
const buf = Buffer.from(txt, 'utf8')
|
|
242
|
+
totalDownloaded += buf.length
|
|
243
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
244
|
+
await fs.promises.writeFile(tmpFile, buf)
|
|
197
245
|
}
|
|
198
|
-
} else if (res.arrayBuffer) {
|
|
199
|
-
const buf = Buffer.from(await res.arrayBuffer())
|
|
200
|
-
totalDownloaded += buf.length
|
|
201
|
-
downloadState.downloadedBytes = totalDownloaded
|
|
202
|
-
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
203
|
-
await fs.promises.writeFile(tmpFile, buf)
|
|
204
|
-
} else if (typeof res.text === 'function') {
|
|
205
|
-
const txt = await res.text()
|
|
206
|
-
const buf = Buffer.from(txt, 'utf8')
|
|
207
|
-
totalDownloaded += buf.length
|
|
208
|
-
downloadState.downloadedBytes = totalDownloaded
|
|
209
|
-
await fs.promises.writeFile(tmpFile, buf)
|
|
210
|
-
}
|
|
211
246
|
|
|
212
|
-
|
|
213
|
-
|
|
247
|
+
const minExpected = f.minBytes || 1024
|
|
248
|
+
const st = await fs.promises.stat(tmpFile)
|
|
249
|
+
if (st.size < minExpected) {
|
|
250
|
+
throw new Error(`File ${f.name} truncated: got ${st.size} bytes, expected at least ${minExpected}`)
|
|
251
|
+
}
|
|
252
|
+
const valid = await validateFileIntegrity(tmpFile, minExpected)
|
|
253
|
+
if (!valid) {
|
|
254
|
+
throw new Error(`File ${f.name} failed integrity check: invalid format or HTML error`)
|
|
255
|
+
}
|
|
214
256
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await
|
|
257
|
+
await fs.promises.rename(tmpFile, targetFile)
|
|
258
|
+
const idx = createdTmpFiles.indexOf(tmpFile)
|
|
259
|
+
if (idx !== -1) createdTmpFiles.splice(idx, 1)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
downloadState.progress = 100
|
|
263
|
+
lastErrors.delete(engineId)
|
|
264
|
+
activeDownloads.delete(engineId)
|
|
265
|
+
return await getStatus(engineId)
|
|
266
|
+
} catch (err) {
|
|
267
|
+
for (const tmp of createdTmpFiles) {
|
|
268
|
+
await fs.promises.rm(tmp, { force: true }).catch(() => {})
|
|
269
|
+
}
|
|
270
|
+
const rawMsg = String(err?.message || err || 'Installation failed')
|
|
271
|
+
const sanitized = rawMsg.replace(/https?:\/\/[^\s]+/g, '[url]')
|
|
272
|
+
lastErrors.set(engineId, { message: sanitized, failedAt: Date.now() })
|
|
273
|
+
throw err
|
|
274
|
+
} finally {
|
|
275
|
+
if (activeDownloads.get(engineId) === downloadState) {
|
|
276
|
+
activeDownloads.delete(engineId)
|
|
277
|
+
}
|
|
224
278
|
}
|
|
225
|
-
const rawMsg = String(err?.message || err || 'Installation failed')
|
|
226
|
-
const sanitized = rawMsg.replace(/https?:\/\/[^\s]+/g, '[url]')
|
|
227
|
-
lastErrors.set(engineId, { message: sanitized, failedAt: Date.now() })
|
|
228
|
-
throw err
|
|
229
|
-
} finally {
|
|
230
|
-
activeDownloads.delete(engineId)
|
|
231
279
|
}
|
|
280
|
+
|
|
281
|
+
const promise = execute()
|
|
282
|
+
downloadState.promise = promise
|
|
283
|
+
activeDownloads.set(engineId, downloadState)
|
|
284
|
+
return await promise
|
|
232
285
|
}
|
|
233
286
|
|
|
234
287
|
async function cancelDownload(engineId) {
|
|
235
288
|
const active = activeDownloads.get(engineId)
|
|
236
289
|
if (active && active.controller) {
|
|
237
290
|
active.controller.abort()
|
|
238
|
-
|
|
291
|
+
if (active.promise) {
|
|
292
|
+
try {
|
|
293
|
+
await active.promise
|
|
294
|
+
} catch (_err) {
|
|
295
|
+
// aborted download rejected as expected
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (activeDownloads.get(engineId) === active) {
|
|
299
|
+
activeDownloads.delete(engineId)
|
|
300
|
+
}
|
|
239
301
|
lastErrors.delete(engineId)
|
|
240
302
|
return true
|
|
241
303
|
}
|