@goodandready/dsh-tts 0.4.27 → 0.4.28
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 +39 -0
- package/README.md +9 -5
- package/lib/cache.js +70 -40
- package/lib/client.js +257 -160
- package/lib/engines/manager.js +103 -69
- package/lib/index.js +277 -54
- package/lib/local.js +1 -1
- package/lib/providers/cloud.js +36 -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 +102 -36
- package/lib/stream-hub.js +51 -7
- package/lib/text.js +18 -1
- package/package.json +1 -1
package/lib/engines/manager.js
CHANGED
|
@@ -12,10 +12,12 @@ export const MODEL_SPECS = {
|
|
|
12
12
|
{
|
|
13
13
|
name: 'kokoro-v0_19.onnx',
|
|
14
14
|
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/main/kokoro-v0_19.onnx',
|
|
15
|
+
minBytes: 1024,
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
name: 'voices.json',
|
|
18
19
|
url: 'https://huggingface.co/hexgrad/Kokoro-82M/resolve/main/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,10 +45,10 @@ 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 checkFileExists(filePath) {
|
|
48
|
+
async function checkFileExists(filePath, minBytes = 1024) {
|
|
46
49
|
try {
|
|
47
50
|
const st = await fs.promises.stat(filePath)
|
|
48
|
-
return st.isFile() && st.size
|
|
51
|
+
return st.isFile() && st.size >= minBytes
|
|
49
52
|
} catch {
|
|
50
53
|
return false
|
|
51
54
|
}
|
|
@@ -98,7 +101,7 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
98
101
|
let allFilesExist = true
|
|
99
102
|
for (const f of spec.files) {
|
|
100
103
|
const p = path.join(dir, f.name)
|
|
101
|
-
if (!(await checkFileExists(p))) {
|
|
104
|
+
if (!(await checkFileExists(p, f.minBytes || 1024))) {
|
|
102
105
|
allFilesExist = false
|
|
103
106
|
break
|
|
104
107
|
}
|
|
@@ -132,14 +135,14 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
132
135
|
const spec = MODEL_SPECS[engineId]
|
|
133
136
|
if (!spec) throw new Error(`Unknown engine: ${engineId}`)
|
|
134
137
|
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
const existing = activeDownloads.get(engineId)
|
|
139
|
+
if (existing && existing.promise) {
|
|
140
|
+
return await existing.promise
|
|
137
141
|
}
|
|
138
142
|
lastErrors.delete(engineId)
|
|
139
143
|
|
|
140
144
|
const customFetch = options.fetchImpl || fetchImpl
|
|
141
145
|
const dir = getModelPath(engineId)
|
|
142
|
-
await fs.promises.mkdir(dir, { recursive: true })
|
|
143
146
|
|
|
144
147
|
const controller = new AbortController()
|
|
145
148
|
if (options.signal) {
|
|
@@ -152,83 +155,114 @@ export function createModelManager({ root = process.env.DSH_HOME || path.join(os
|
|
|
152
155
|
downloadedBytes: 0,
|
|
153
156
|
totalBytes: spec.approxMb * 1024 * 1024,
|
|
154
157
|
controller,
|
|
158
|
+
promise: null,
|
|
155
159
|
}
|
|
156
|
-
activeDownloads.set(engineId, downloadState)
|
|
157
|
-
let totalDownloaded = 0
|
|
158
160
|
|
|
159
|
-
|
|
160
|
-
for (const f of spec.files) {
|
|
161
|
-
if (options.signal?.aborted || controller.signal.aborted) {
|
|
162
|
-
throw new Error('Download aborted')
|
|
163
|
-
}
|
|
161
|
+
const createdTmpFiles = []
|
|
164
162
|
|
|
165
|
-
|
|
166
|
-
|
|
163
|
+
const execute = async () => {
|
|
164
|
+
await fs.promises.mkdir(dir, { recursive: true })
|
|
165
|
+
let totalDownloaded = 0
|
|
166
|
+
try {
|
|
167
|
+
for (const f of spec.files) {
|
|
168
|
+
if (options.signal?.aborted || controller.signal.aborted) {
|
|
169
|
+
throw new Error('Download aborted')
|
|
170
|
+
}
|
|
167
171
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
const targetFile = path.join(dir, f.name)
|
|
173
|
+
const nonce = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
|
|
174
|
+
const tmpFile = path.join(dir, `${f.name}.${nonce}.tmp`)
|
|
175
|
+
createdTmpFiles.push(tmpFile)
|
|
176
|
+
|
|
177
|
+
const res = await customFetch(f.url, { signal: controller.signal })
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
throw new Error(`Failed to download ${f.name}: HTTP ${res.status}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (res.body && typeof res.body.getReader === 'function') {
|
|
183
|
+
const reader = res.body.getReader()
|
|
184
|
+
const ws = fs.createWriteStream(tmpFile)
|
|
185
|
+
let streamError = null
|
|
186
|
+
const onStreamError = (err) => { streamError = err }
|
|
187
|
+
ws.once('error', onStreamError)
|
|
188
|
+
try {
|
|
189
|
+
while (true) {
|
|
190
|
+
if (streamError) throw streamError
|
|
191
|
+
const { done, value } = await reader.read()
|
|
192
|
+
if (done) break
|
|
193
|
+
if (streamError) throw streamError
|
|
172
194
|
|
|
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
195
|
if (!ws.write(value)) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
await new Promise((resolve, reject) => {
|
|
197
|
+
const onDrain = () => { cleanup(); resolve() }
|
|
198
|
+
const onError = (e) => { cleanup(); reject(e) }
|
|
199
|
+
const cleanup = () => {
|
|
200
|
+
ws.removeListener('drain', onDrain)
|
|
201
|
+
ws.removeListener('error', onError)
|
|
202
|
+
}
|
|
203
|
+
ws.once('drain', onDrain)
|
|
204
|
+
ws.once('error', onError)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
totalDownloaded += value.length
|
|
208
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
209
|
+
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
210
|
+
if (typeof options.onProgress === 'function') {
|
|
211
|
+
options.onProgress(downloadState)
|
|
185
212
|
}
|
|
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
213
|
}
|
|
214
|
+
} finally {
|
|
215
|
+
ws.removeListener('error', onStreamError)
|
|
216
|
+
await new Promise((resolve, reject) => {
|
|
217
|
+
ws.end((err) => (err ? reject(err) : resolve()))
|
|
218
|
+
})
|
|
194
219
|
}
|
|
195
|
-
}
|
|
196
|
-
|
|
220
|
+
} else if (res.arrayBuffer) {
|
|
221
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
222
|
+
totalDownloaded += buf.length
|
|
223
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
224
|
+
downloadState.progress = Math.min(100, Math.round((totalDownloaded / (downloadState.totalBytes || 1)) * 100))
|
|
225
|
+
await fs.promises.writeFile(tmpFile, buf)
|
|
226
|
+
} else if (typeof res.text === 'function') {
|
|
227
|
+
const txt = await res.text()
|
|
228
|
+
const buf = Buffer.from(txt, 'utf8')
|
|
229
|
+
totalDownloaded += buf.length
|
|
230
|
+
downloadState.downloadedBytes = totalDownloaded
|
|
231
|
+
await fs.promises.writeFile(tmpFile, buf)
|
|
197
232
|
}
|
|
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
233
|
|
|
212
|
-
|
|
213
|
-
|
|
234
|
+
const minExpected = f.minBytes || 1024
|
|
235
|
+
const st = await fs.promises.stat(tmpFile)
|
|
236
|
+
if (st.size < minExpected) {
|
|
237
|
+
throw new Error(`File ${f.name} truncated: got ${st.size} bytes, expected at least ${minExpected}`)
|
|
238
|
+
}
|
|
214
239
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await
|
|
240
|
+
await fs.promises.rename(tmpFile, targetFile)
|
|
241
|
+
const idx = createdTmpFiles.indexOf(tmpFile)
|
|
242
|
+
if (idx !== -1) createdTmpFiles.splice(idx, 1)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
downloadState.progress = 100
|
|
246
|
+
lastErrors.delete(engineId)
|
|
247
|
+
activeDownloads.delete(engineId)
|
|
248
|
+
return await getStatus(engineId)
|
|
249
|
+
} catch (err) {
|
|
250
|
+
for (const tmp of createdTmpFiles) {
|
|
251
|
+
await fs.promises.rm(tmp, { force: true }).catch(() => {})
|
|
252
|
+
}
|
|
253
|
+
const rawMsg = String(err?.message || err || 'Installation failed')
|
|
254
|
+
const sanitized = rawMsg.replace(/https?:\/\/[^\s]+/g, '[url]')
|
|
255
|
+
lastErrors.set(engineId, { message: sanitized, failedAt: Date.now() })
|
|
256
|
+
throw err
|
|
257
|
+
} finally {
|
|
258
|
+
activeDownloads.delete(engineId)
|
|
224
259
|
}
|
|
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
260
|
}
|
|
261
|
+
|
|
262
|
+
const promise = execute()
|
|
263
|
+
downloadState.promise = promise
|
|
264
|
+
activeDownloads.set(engineId, downloadState)
|
|
265
|
+
return await promise
|
|
232
266
|
}
|
|
233
267
|
|
|
234
268
|
async function cancelDownload(engineId) {
|