@mulingai-npm/redis 3.40.27 → 3.40.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.
|
@@ -122,13 +122,27 @@ export declare class MulingstreamChunkManager {
|
|
|
122
122
|
}): Promise<MulingstreamChunkData>;
|
|
123
123
|
private getChunkId;
|
|
124
124
|
getMulingstreamChunkById(roomId: string, n: number): Promise<MulingstreamChunkData>;
|
|
125
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Partial HSET — touches only `finalTranscription`. See setBufferStatus for the
|
|
127
|
+
* race rationale (whole-chunk read-modify-write would clobber concurrent updates).
|
|
128
|
+
*/
|
|
126
129
|
updateFinalTranscription(roomId: string, n: number, transcription: string): Promise<MulingstreamChunkData | null>;
|
|
127
130
|
/**
|
|
128
131
|
* Update chunk with LLM-sanitized transcription from SmartTranslate.
|
|
129
132
|
* If the sanitized text differs from the original, sets transcriptionSource to 'llm'
|
|
130
133
|
* and updates finalTranscription to the sanitized version.
|
|
131
134
|
* Returns the chunk (caller can compare original vs sanitized to decide on TRANSCRIPTION_CORRECTED).
|
|
135
|
+
*
|
|
136
|
+
* IMPORTANT — partial HSET, NOT withChunk(). Reasoning identical to setBufferStatus:
|
|
137
|
+
* handleTtsService runs in parallel and writes `tts.<lang>.status = READY`. The
|
|
138
|
+
* read-modify-write of withChunk would clobber that READY back to INIT (we read
|
|
139
|
+
* tts before TTS callback fires, then write the entire chunk back later). Result:
|
|
140
|
+
* emitChunk's `if (status !== READY) return;` drops the audio publish, listeners
|
|
141
|
+
* see text but never get the audio URL.
|
|
142
|
+
*
|
|
143
|
+
* This bug surfaced 2026-05-01: same root cause as the earlier setBufferStatus
|
|
144
|
+
* race; updateLlmTranscription wasn't fixed at the same time so it kept producing
|
|
145
|
+
* silent audio for any chunk where wasSanitized=true (i.e. every LLM-routed chunk).
|
|
132
146
|
*/
|
|
133
147
|
updateLlmTranscription(roomId: string, n: number, opts: {
|
|
134
148
|
llmTranscription: string;
|
|
@@ -139,16 +153,37 @@ export declare class MulingstreamChunkManager {
|
|
|
139
153
|
chunk: MulingstreamChunkData | null;
|
|
140
154
|
wasChanged: boolean;
|
|
141
155
|
}>;
|
|
156
|
+
/**
|
|
157
|
+
* Partial HSET — reads only `translation` and `tts` fields, modifies in place,
|
|
158
|
+
* writes back only those two. See setBufferStatus for the race rationale.
|
|
159
|
+
*/
|
|
142
160
|
discardLanguage(roomId: string, n: number, lang: string): Promise<MulingstreamChunkData | null>;
|
|
161
|
+
/**
|
|
162
|
+
* Partial HSET — same race-safe pattern as discardLanguage. Only writes the
|
|
163
|
+
* fields actually mutated by the caller (translation and/or tts).
|
|
164
|
+
*/
|
|
143
165
|
discardLanguages(roomId: string, n: number, opt: {
|
|
144
166
|
translation?: string[];
|
|
145
167
|
tts?: string[];
|
|
146
168
|
}): Promise<MulingstreamChunkData | null>;
|
|
169
|
+
/**
|
|
170
|
+
* Partial HSET — only `translation` is read and rewritten. Concurrent updateTts
|
|
171
|
+
* calls won't be clobbered.
|
|
172
|
+
*/
|
|
147
173
|
updateTranslation(roomId: string, n: number, lang: string, opt: {
|
|
148
174
|
translation?: string;
|
|
149
175
|
status?: StepStatus;
|
|
150
176
|
}): Promise<MulingstreamChunkData | null>;
|
|
177
|
+
/**
|
|
178
|
+
* Partial HSET — only `translation` is read and rewritten. This is the hot path
|
|
179
|
+
* that races with handleTtsService's updateTts and was the root cause of the
|
|
180
|
+
* "audio LISTENER_FEED never arrives" bug fixed 2026-05-01.
|
|
181
|
+
*/
|
|
151
182
|
updateTranslationInBulk(roomId: string, n: number, dict: Record<string, string>, status?: StepStatus, hintsPerLanguage?: Record<string, TranslationHint[]>): Promise<MulingstreamChunkData | null>;
|
|
183
|
+
/**
|
|
184
|
+
* Partial HSET — only `tts` is read and rewritten. Same race-rationale as
|
|
185
|
+
* updateTranslationInBulk: avoids clobbering concurrent translation writes.
|
|
186
|
+
*/
|
|
152
187
|
updateTts(roomId: string, n: number, lang: string, opt: {
|
|
153
188
|
ttsAudioPath?: string;
|
|
154
189
|
status?: StepStatus;
|
|
@@ -159,6 +194,13 @@ export declare class MulingstreamChunkManager {
|
|
|
159
194
|
* Update SmartTranslate buffering metadata on a chunk (Section 22).
|
|
160
195
|
* Writes `bufferStatus`, `pendingText`, and `consumedChunkNumbers` so the chunk row
|
|
161
196
|
* persisted at the end of the pipeline reflects whether it was DEFERRED / USED / EMITTED.
|
|
197
|
+
*
|
|
198
|
+
* IMPORTANT — this is a partial HSET that touches ONLY the buffering fields.
|
|
199
|
+
* It deliberately does NOT use `withChunk()` (which rewrites the whole chunk hash)
|
|
200
|
+
* because handleTtsService can update `tts` concurrently with this call. Using the
|
|
201
|
+
* read-modify-write pattern of withChunk would race with TTS status updates and
|
|
202
|
+
* clobber `tts.<lang>.status = READY` back to INIT — preventing the audio sequencer
|
|
203
|
+
* from emitting the listener feed (audio came through as text-only with audio:"").
|
|
162
204
|
*/
|
|
163
205
|
setBufferStatus(roomId: string, n: number, opt: {
|
|
164
206
|
bufferStatus?: 'EMITTED' | 'DEFERRED' | 'USED';
|
|
@@ -171,11 +213,13 @@ export declare class MulingstreamChunkManager {
|
|
|
171
213
|
*/
|
|
172
214
|
isChunkComplete(roomId: string, n: number): Promise<boolean>;
|
|
173
215
|
/**
|
|
174
|
-
* Mark a chunk as complete (all TTS finished for all target languages)
|
|
216
|
+
* Mark a chunk as complete (all TTS finished for all target languages).
|
|
217
|
+
* Partial HSET — touches only `isComplete`.
|
|
175
218
|
*/
|
|
176
219
|
markChunkComplete(roomId: string, n: number): Promise<MulingstreamChunkData | null>;
|
|
177
220
|
/**
|
|
178
|
-
* Mark a chunk as saved to database
|
|
221
|
+
* Mark a chunk as saved to database.
|
|
222
|
+
* Partial HSET — touches only `isSaved`.
|
|
179
223
|
*/
|
|
180
224
|
markChunkSaved(roomId: string, n: number): Promise<MulingstreamChunkData | null>;
|
|
181
225
|
/**
|
|
@@ -45,7 +45,11 @@ class MulingstreamChunkManager {
|
|
|
45
45
|
llmWords: h.llmWords ? this.deserialize(h.llmWords) : undefined,
|
|
46
46
|
transcriptionSource: h.transcriptionSource || 'stt',
|
|
47
47
|
routeUsed: h.routeUsed || undefined,
|
|
48
|
-
sttHistory: h.sttHistory ? this.deserialize(h.sttHistory) : undefined
|
|
48
|
+
sttHistory: h.sttHistory ? this.deserialize(h.sttHistory) : undefined,
|
|
49
|
+
// SmartTranslate buffering (Section 22)
|
|
50
|
+
bufferStatus: h.bufferStatus || undefined,
|
|
51
|
+
pendingText: h.pendingText || undefined,
|
|
52
|
+
consumedChunkNumbers: h.consumedChunkNumbers ? this.deserialize(h.consumedChunkNumbers) : undefined
|
|
49
53
|
};
|
|
50
54
|
}
|
|
51
55
|
getTimeout() {
|
|
@@ -211,151 +215,266 @@ class MulingstreamChunkManager {
|
|
|
211
215
|
const raw = await this.redisClient.hgetall(this.chunkHashKey(cid));
|
|
212
216
|
return raw.chunkId ? this.hashToChunk(raw) : null;
|
|
213
217
|
}
|
|
214
|
-
|
|
218
|
+
// NOTE — `withChunk` was removed 2026-05-01. Its read-modify-write pattern
|
|
219
|
+
// raced with concurrent updates to other fields and clobbered them on writeback.
|
|
220
|
+
// All mutators now use partial HSETs that read and write only the fields they own.
|
|
221
|
+
// If you need a new mutator, follow the pattern of `updateTts` / `updateTranslationInBulk`.
|
|
222
|
+
/**
|
|
223
|
+
* Partial HSET — touches only `finalTranscription`. See setBufferStatus for the
|
|
224
|
+
* race rationale (whole-chunk read-modify-write would clobber concurrent updates).
|
|
225
|
+
*/
|
|
226
|
+
async updateFinalTranscription(roomId, n, transcription) {
|
|
215
227
|
const cid = await this.getChunkId(roomId, n);
|
|
216
228
|
if (!cid)
|
|
217
229
|
return null;
|
|
218
230
|
const key = this.chunkHashKey(cid);
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const p = this.redisClient.pipeline();
|
|
225
|
-
const updateHash = {
|
|
226
|
-
finalTranscription: chunk.finalTranscription,
|
|
227
|
-
translation: this.serialize(chunk.translation),
|
|
228
|
-
tts: this.serialize(chunk.tts),
|
|
229
|
-
streamingChunk: this.serialize(chunk.streamingChunk),
|
|
230
|
-
isComplete: String(chunk.isComplete),
|
|
231
|
-
isSaved: String(chunk.isSaved),
|
|
232
|
-
transcriptionSource: chunk.transcriptionSource || 'stt'
|
|
233
|
-
};
|
|
234
|
-
// SmartTranslate fields (only set if populated)
|
|
235
|
-
if (chunk.llmTranscription !== undefined)
|
|
236
|
-
updateHash.llmTranscription = chunk.llmTranscription;
|
|
237
|
-
if (chunk.llmWords !== undefined)
|
|
238
|
-
updateHash.llmWords = this.serialize(chunk.llmWords);
|
|
239
|
-
if (chunk.routeUsed !== undefined)
|
|
240
|
-
updateHash.routeUsed = chunk.routeUsed;
|
|
241
|
-
if (chunk.sttHistory !== undefined)
|
|
242
|
-
updateHash.sttHistory = this.serialize(chunk.sttHistory);
|
|
243
|
-
p.hset(key, updateHash);
|
|
244
|
-
p.expire(key, EXPIRATION);
|
|
245
|
-
await p.exec();
|
|
246
|
-
return chunk;
|
|
247
|
-
}
|
|
248
|
-
async updateFinalTranscription(roomId, n, transcription) {
|
|
249
|
-
return this.withChunk(roomId, n, (c) => {
|
|
250
|
-
c.finalTranscription = transcription;
|
|
251
|
-
});
|
|
231
|
+
const pipe = this.redisClient.pipeline();
|
|
232
|
+
pipe.hset(key, { finalTranscription: transcription });
|
|
233
|
+
pipe.expire(key, EXPIRATION);
|
|
234
|
+
await pipe.exec();
|
|
235
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
252
236
|
}
|
|
253
237
|
/**
|
|
254
238
|
* Update chunk with LLM-sanitized transcription from SmartTranslate.
|
|
255
239
|
* If the sanitized text differs from the original, sets transcriptionSource to 'llm'
|
|
256
240
|
* and updates finalTranscription to the sanitized version.
|
|
257
241
|
* Returns the chunk (caller can compare original vs sanitized to decide on TRANSCRIPTION_CORRECTED).
|
|
242
|
+
*
|
|
243
|
+
* IMPORTANT — partial HSET, NOT withChunk(). Reasoning identical to setBufferStatus:
|
|
244
|
+
* handleTtsService runs in parallel and writes `tts.<lang>.status = READY`. The
|
|
245
|
+
* read-modify-write of withChunk would clobber that READY back to INIT (we read
|
|
246
|
+
* tts before TTS callback fires, then write the entire chunk back later). Result:
|
|
247
|
+
* emitChunk's `if (status !== READY) return;` drops the audio publish, listeners
|
|
248
|
+
* see text but never get the audio URL.
|
|
249
|
+
*
|
|
250
|
+
* This bug surfaced 2026-05-01: same root cause as the earlier setBufferStatus
|
|
251
|
+
* race; updateLlmTranscription wasn't fixed at the same time so it kept producing
|
|
252
|
+
* silent audio for any chunk where wasSanitized=true (i.e. every LLM-routed chunk).
|
|
258
253
|
*/
|
|
259
254
|
async updateLlmTranscription(roomId, n, opts) {
|
|
255
|
+
const cid = await this.getChunkId(roomId, n);
|
|
256
|
+
if (!cid)
|
|
257
|
+
return { chunk: null, wasChanged: false };
|
|
258
|
+
const key = this.chunkHashKey(cid);
|
|
259
|
+
// Read just the fields we need to compute the diff/append, NOT the whole chunk.
|
|
260
|
+
// We don't need tts/translation/streamingChunk here — they're untouched by this op.
|
|
261
|
+
const [originalTextRaw, sttHistoryRaw] = await Promise.all([
|
|
262
|
+
this.redisClient.hget(key, 'finalTranscription'),
|
|
263
|
+
this.redisClient.hget(key, 'sttHistory')
|
|
264
|
+
]);
|
|
265
|
+
const originalText = originalTextRaw !== null && originalTextRaw !== void 0 ? originalTextRaw : '';
|
|
266
|
+
const existingSttHistory = sttHistoryRaw ? this.deserialize(sttHistoryRaw) : [];
|
|
267
|
+
const update = {
|
|
268
|
+
llmTranscription: opts.llmTranscription,
|
|
269
|
+
routeUsed: opts.routeUsed
|
|
270
|
+
};
|
|
271
|
+
if (opts.llmWords !== undefined)
|
|
272
|
+
update.llmWords = this.serialize(opts.llmWords);
|
|
260
273
|
let wasChanged = false;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
c.sttHistory = c.sttHistory.slice(-3);
|
|
279
|
-
}
|
|
280
|
-
});
|
|
274
|
+
if (opts.routeUsed === 'LLM' && opts.llmTranscription !== originalText) {
|
|
275
|
+
update.finalTranscription = opts.llmTranscription;
|
|
276
|
+
update.transcriptionSource = 'llm';
|
|
277
|
+
wasChanged = true;
|
|
278
|
+
}
|
|
279
|
+
if (opts.sttHistoryEntry) {
|
|
280
|
+
const next = [...existingSttHistory, opts.sttHistoryEntry];
|
|
281
|
+
if (next.length > 3)
|
|
282
|
+
next.splice(0, next.length - 3);
|
|
283
|
+
update.sttHistory = this.serialize(next);
|
|
284
|
+
}
|
|
285
|
+
const pipe = this.redisClient.pipeline();
|
|
286
|
+
pipe.hset(key, update);
|
|
287
|
+
pipe.expire(key, EXPIRATION);
|
|
288
|
+
await pipe.exec();
|
|
289
|
+
// Re-read the full chunk so callers can use the up-to-date object.
|
|
290
|
+
const chunk = await this.getMulingstreamChunkById(roomId, n);
|
|
281
291
|
return { chunk, wasChanged };
|
|
282
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Partial HSET — reads only `translation` and `tts` fields, modifies in place,
|
|
295
|
+
* writes back only those two. See setBufferStatus for the race rationale.
|
|
296
|
+
*/
|
|
283
297
|
async discardLanguage(roomId, n, lang) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
298
|
+
var _a, _b;
|
|
299
|
+
const cid = await this.getChunkId(roomId, n);
|
|
300
|
+
if (!cid)
|
|
301
|
+
return null;
|
|
302
|
+
const key = this.chunkHashKey(cid);
|
|
303
|
+
const [translationRaw, ttsRaw] = await Promise.all([
|
|
304
|
+
this.redisClient.hget(key, 'translation'),
|
|
305
|
+
this.redisClient.hget(key, 'tts')
|
|
306
|
+
]);
|
|
307
|
+
const translation = (_a = this.deserialize(translationRaw !== null && translationRaw !== void 0 ? translationRaw : '')) !== null && _a !== void 0 ? _a : {};
|
|
308
|
+
const tts = (_b = this.deserialize(ttsRaw !== null && ttsRaw !== void 0 ? ttsRaw : '')) !== null && _b !== void 0 ? _b : {};
|
|
309
|
+
if (translation[lang])
|
|
310
|
+
translation[lang].status = 'DISCARDED';
|
|
311
|
+
if (tts[lang])
|
|
312
|
+
tts[lang].status = 'DISCARDED';
|
|
313
|
+
const pipe = this.redisClient.pipeline();
|
|
314
|
+
pipe.hset(key, { translation: this.serialize(translation), tts: this.serialize(tts) });
|
|
315
|
+
pipe.expire(key, EXPIRATION);
|
|
316
|
+
await pipe.exec();
|
|
317
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
290
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Partial HSET — same race-safe pattern as discardLanguage. Only writes the
|
|
321
|
+
* fields actually mutated by the caller (translation and/or tts).
|
|
322
|
+
*/
|
|
291
323
|
async discardLanguages(roomId, n, opt) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
324
|
+
var _a, _b, _c, _d;
|
|
325
|
+
const cid = await this.getChunkId(roomId, n);
|
|
326
|
+
if (!cid)
|
|
327
|
+
return null;
|
|
328
|
+
const key = this.chunkHashKey(cid);
|
|
329
|
+
const wantTranslation = !!((_a = opt.translation) === null || _a === void 0 ? void 0 : _a.length);
|
|
330
|
+
const wantTts = !!((_b = opt.tts) === null || _b === void 0 ? void 0 : _b.length);
|
|
331
|
+
if (!wantTranslation && !wantTts)
|
|
332
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
333
|
+
const [translationRaw, ttsRaw] = await Promise.all([
|
|
334
|
+
wantTranslation ? this.redisClient.hget(key, 'translation') : Promise.resolve(null),
|
|
335
|
+
wantTts ? this.redisClient.hget(key, 'tts') : Promise.resolve(null)
|
|
336
|
+
]);
|
|
337
|
+
const update = {};
|
|
338
|
+
if (wantTranslation) {
|
|
339
|
+
const translation = (_c = this.deserialize(translationRaw !== null && translationRaw !== void 0 ? translationRaw : '')) !== null && _c !== void 0 ? _c : {};
|
|
340
|
+
opt.translation.forEach((l) => {
|
|
341
|
+
const e = translation[l];
|
|
296
342
|
if (e && e.status === 'INIT')
|
|
297
343
|
e.status = 'DISCARDED';
|
|
298
344
|
});
|
|
299
|
-
|
|
300
|
-
|
|
345
|
+
update.translation = this.serialize(translation);
|
|
346
|
+
}
|
|
347
|
+
if (wantTts) {
|
|
348
|
+
const tts = (_d = this.deserialize(ttsRaw !== null && ttsRaw !== void 0 ? ttsRaw : '')) !== null && _d !== void 0 ? _d : {};
|
|
349
|
+
opt.tts.forEach((l) => {
|
|
350
|
+
const e = tts[l];
|
|
301
351
|
if (e && e.status === 'INIT')
|
|
302
352
|
e.status = 'DISCARDED';
|
|
303
353
|
});
|
|
304
|
-
|
|
354
|
+
update.tts = this.serialize(tts);
|
|
355
|
+
}
|
|
356
|
+
const pipe = this.redisClient.pipeline();
|
|
357
|
+
pipe.hset(key, update);
|
|
358
|
+
pipe.expire(key, EXPIRATION);
|
|
359
|
+
await pipe.exec();
|
|
360
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
305
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Partial HSET — only `translation` is read and rewritten. Concurrent updateTts
|
|
364
|
+
* calls won't be clobbered.
|
|
365
|
+
*/
|
|
306
366
|
async updateTranslation(roomId, n, lang, opt) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
367
|
+
var _a;
|
|
368
|
+
const cid = await this.getChunkId(roomId, n);
|
|
369
|
+
if (!cid)
|
|
370
|
+
return null;
|
|
371
|
+
const key = this.chunkHashKey(cid);
|
|
372
|
+
const translationRaw = await this.redisClient.hget(key, 'translation');
|
|
373
|
+
const translation = (_a = this.deserialize(translationRaw !== null && translationRaw !== void 0 ? translationRaw : '')) !== null && _a !== void 0 ? _a : {};
|
|
374
|
+
const e = translation[lang];
|
|
375
|
+
if (!e)
|
|
376
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
377
|
+
if (opt.translation !== undefined)
|
|
378
|
+
e.translation = opt.translation;
|
|
379
|
+
if (opt.status !== undefined)
|
|
380
|
+
e.status = opt.status;
|
|
381
|
+
const pipe = this.redisClient.pipeline();
|
|
382
|
+
pipe.hset(key, { translation: this.serialize(translation) });
|
|
383
|
+
pipe.expire(key, EXPIRATION);
|
|
384
|
+
await pipe.exec();
|
|
385
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
316
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Partial HSET — only `translation` is read and rewritten. This is the hot path
|
|
389
|
+
* that races with handleTtsService's updateTts and was the root cause of the
|
|
390
|
+
* "audio LISTENER_FEED never arrives" bug fixed 2026-05-01.
|
|
391
|
+
*/
|
|
317
392
|
async updateTranslationInBulk(roomId, n, dict, status = 'READY', hintsPerLanguage) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
393
|
+
var _a;
|
|
394
|
+
const cid = await this.getChunkId(roomId, n);
|
|
395
|
+
if (!cid)
|
|
396
|
+
return null;
|
|
397
|
+
const key = this.chunkHashKey(cid);
|
|
398
|
+
const translationRaw = await this.redisClient.hget(key, 'translation');
|
|
399
|
+
const translation = (_a = this.deserialize(translationRaw !== null && translationRaw !== void 0 ? translationRaw : '')) !== null && _a !== void 0 ? _a : {};
|
|
400
|
+
Object.entries(dict).forEach(([l, txt]) => {
|
|
401
|
+
if (!translation[l])
|
|
402
|
+
return;
|
|
403
|
+
translation[l].translation = txt;
|
|
404
|
+
translation[l].status = status;
|
|
405
|
+
if (hintsPerLanguage && hintsPerLanguage[l]) {
|
|
406
|
+
translation[l].hints = hintsPerLanguage[l];
|
|
407
|
+
}
|
|
328
408
|
});
|
|
409
|
+
const pipe = this.redisClient.pipeline();
|
|
410
|
+
pipe.hset(key, { translation: this.serialize(translation) });
|
|
411
|
+
pipe.expire(key, EXPIRATION);
|
|
412
|
+
await pipe.exec();
|
|
413
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
329
414
|
}
|
|
415
|
+
/**
|
|
416
|
+
* Partial HSET — only `tts` is read and rewritten. Same race-rationale as
|
|
417
|
+
* updateTranslationInBulk: avoids clobbering concurrent translation writes.
|
|
418
|
+
*/
|
|
330
419
|
async updateTts(roomId, n, lang, opt) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
420
|
+
var _a;
|
|
421
|
+
const cid = await this.getChunkId(roomId, n);
|
|
422
|
+
if (!cid)
|
|
423
|
+
return null;
|
|
424
|
+
const key = this.chunkHashKey(cid);
|
|
425
|
+
const ttsRaw = await this.redisClient.hget(key, 'tts');
|
|
426
|
+
const tts = (_a = this.deserialize(ttsRaw !== null && ttsRaw !== void 0 ? ttsRaw : '')) !== null && _a !== void 0 ? _a : {};
|
|
427
|
+
const e = tts[lang];
|
|
428
|
+
if (!e)
|
|
429
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
430
|
+
if (opt.ttsAudioPath !== undefined)
|
|
431
|
+
e.ttsAudioPath = opt.ttsAudioPath;
|
|
432
|
+
if (opt.status !== undefined)
|
|
433
|
+
e.status = opt.status;
|
|
434
|
+
if (opt.isEmitted !== undefined)
|
|
435
|
+
e.isEmitted = opt.isEmitted;
|
|
436
|
+
if (opt.duration !== undefined)
|
|
437
|
+
e.duration = opt.duration;
|
|
438
|
+
const pipe = this.redisClient.pipeline();
|
|
439
|
+
pipe.hset(key, { tts: this.serialize(tts) });
|
|
440
|
+
pipe.expire(key, EXPIRATION);
|
|
441
|
+
await pipe.exec();
|
|
442
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
344
443
|
}
|
|
345
444
|
/**
|
|
346
445
|
* Update SmartTranslate buffering metadata on a chunk (Section 22).
|
|
347
446
|
* Writes `bufferStatus`, `pendingText`, and `consumedChunkNumbers` so the chunk row
|
|
348
447
|
* persisted at the end of the pipeline reflects whether it was DEFERRED / USED / EMITTED.
|
|
448
|
+
*
|
|
449
|
+
* IMPORTANT — this is a partial HSET that touches ONLY the buffering fields.
|
|
450
|
+
* It deliberately does NOT use `withChunk()` (which rewrites the whole chunk hash)
|
|
451
|
+
* because handleTtsService can update `tts` concurrently with this call. Using the
|
|
452
|
+
* read-modify-write pattern of withChunk would race with TTS status updates and
|
|
453
|
+
* clobber `tts.<lang>.status = READY` back to INIT — preventing the audio sequencer
|
|
454
|
+
* from emitting the listener feed (audio came through as text-only with audio:"").
|
|
349
455
|
*/
|
|
350
456
|
async setBufferStatus(roomId, n, opt) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
457
|
+
const cid = await this.getChunkId(roomId, n);
|
|
458
|
+
if (!cid)
|
|
459
|
+
return null;
|
|
460
|
+
const key = this.chunkHashKey(cid);
|
|
461
|
+
const update = {};
|
|
462
|
+
if (opt.bufferStatus !== undefined)
|
|
463
|
+
update.bufferStatus = opt.bufferStatus;
|
|
464
|
+
if (opt.pendingText !== undefined)
|
|
465
|
+
update.pendingText = opt.pendingText;
|
|
466
|
+
if (opt.consumedChunkNumbers !== undefined)
|
|
467
|
+
update.consumedChunkNumbers = this.serialize(opt.consumedChunkNumbers);
|
|
468
|
+
if (Object.keys(update).length === 0) {
|
|
469
|
+
// Nothing to write — read-only fetch for callers that still want the chunk back.
|
|
470
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
471
|
+
}
|
|
472
|
+
const pipe = this.redisClient.pipeline();
|
|
473
|
+
pipe.hset(key, update);
|
|
474
|
+
pipe.expire(key, EXPIRATION);
|
|
475
|
+
await pipe.exec();
|
|
476
|
+
// Return the freshly-read chunk so callers can persist it (e.g. insertChunk to PG).
|
|
477
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
359
478
|
}
|
|
360
479
|
async areTranslationsProcessed(roomId, n) {
|
|
361
480
|
const c = await this.getMulingstreamChunkById(roomId, n);
|
|
@@ -371,20 +490,34 @@ class MulingstreamChunkManager {
|
|
|
371
490
|
return Object.values(c.tts).every((t) => t.status !== 'INIT');
|
|
372
491
|
}
|
|
373
492
|
/**
|
|
374
|
-
* Mark a chunk as complete (all TTS finished for all target languages)
|
|
493
|
+
* Mark a chunk as complete (all TTS finished for all target languages).
|
|
494
|
+
* Partial HSET — touches only `isComplete`.
|
|
375
495
|
*/
|
|
376
496
|
async markChunkComplete(roomId, n) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
497
|
+
const cid = await this.getChunkId(roomId, n);
|
|
498
|
+
if (!cid)
|
|
499
|
+
return null;
|
|
500
|
+
const key = this.chunkHashKey(cid);
|
|
501
|
+
const pipe = this.redisClient.pipeline();
|
|
502
|
+
pipe.hset(key, { isComplete: 'true' });
|
|
503
|
+
pipe.expire(key, EXPIRATION);
|
|
504
|
+
await pipe.exec();
|
|
505
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
380
506
|
}
|
|
381
507
|
/**
|
|
382
|
-
* Mark a chunk as saved to database
|
|
508
|
+
* Mark a chunk as saved to database.
|
|
509
|
+
* Partial HSET — touches only `isSaved`.
|
|
383
510
|
*/
|
|
384
511
|
async markChunkSaved(roomId, n) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
512
|
+
const cid = await this.getChunkId(roomId, n);
|
|
513
|
+
if (!cid)
|
|
514
|
+
return null;
|
|
515
|
+
const key = this.chunkHashKey(cid);
|
|
516
|
+
const pipe = this.redisClient.pipeline();
|
|
517
|
+
pipe.hset(key, { isSaved: 'true' });
|
|
518
|
+
pipe.expire(key, EXPIRATION);
|
|
519
|
+
await pipe.exec();
|
|
520
|
+
return this.getMulingstreamChunkById(roomId, n);
|
|
388
521
|
}
|
|
389
522
|
/**
|
|
390
523
|
* Get all complete but unsaved chunks for a room
|