@goodandready/dsh-tts 0.4.26 → 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.
@@ -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 > 0
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
- if (activeDownloads.has(engineId)) {
136
- throw new Error(`Download already in progress for ${engineId}`)
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
- try {
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
- const targetFile = path.join(dir, f.name)
166
- const tmpFile = path.join(dir, `${f.name}.tmp`)
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
- const res = await customFetch(f.url, { signal: controller.signal })
169
- if (!res.ok) {
170
- throw new Error(`Failed to download ${f.name}: HTTP ${res.status}`)
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
- ws.once('drain', resolve)
183
- } else {
184
- process.nextTick(resolve)
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
- } finally {
196
- await new Promise((resolve) => ws.end(resolve))
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
- await fs.promises.rename(tmpFile, targetFile)
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
- downloadState.progress = 100
216
- activeDownloads.delete(engineId)
217
- lastErrors.delete(engineId)
218
- return await getStatus(engineId)
219
- } catch (err) {
220
- // Clean up tmp files
221
- for (const f of spec.files) {
222
- const tmpFile = path.join(dir, `${f.name}.tmp`)
223
- await fs.promises.rm(tmpFile, { force: true }).catch(() => {})
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) {