@mulingai-npm/redis 3.1.1 → 3.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.
@@ -1,382 +1,377 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MulingstreamChunkManager = void 0;
4
- const uuid_1 = require("uuid");
5
- /* -------------------------------------------------------------------------- */
6
- /* Constants */
7
- /* -------------------------------------------------------------------------- */
8
- const EXPIRATION = 12 * 60 * 60; // 12 h
9
- const ROOM_ARRAY_LENGTH = 50; // keep last 50 chunks
10
- /* -------------------------------------------------------------------------- */
11
- /* Main class */
12
- /* -------------------------------------------------------------------------- */
13
- class MulingstreamChunkManager {
14
- constructor(redisClient) {
15
- this.redisClient = redisClient;
16
- }
17
- /* ----------------------------- key helpers ----------------------------- */
18
- roomZsetKey(roomId) {
19
- return `room:${roomId}:chunks`;
20
- }
21
- chunkHashKey(chunkId) {
22
- return `chunk:${chunkId}`;
23
- }
24
- generateChunkId(roomId, chunkNumber) {
25
- return `[${roomId}]-[${chunkNumber}]-[${(0, uuid_1.v4)()}]`;
26
- }
27
- /* --------------------------- (de)serialization -------------------------- */
28
- serialize(value) {
29
- return JSON.stringify(value);
30
- }
31
- deserialize(v) {
32
- return v ? JSON.parse(v) : undefined;
33
- }
34
- hashToChunk(h) {
35
- return {
36
- chunkId: h.chunkId,
37
- roomId: h.roomId,
38
- chunkNumber: parseInt(h.chunkNumber, 10),
39
- language: h.language,
40
- sttProviders: this.deserialize(h.sttProviders),
41
- targetLanguages: this.deserialize(h.targetLanguages),
42
- shortCodeTargetLanguages: this.deserialize(h.shortCodeTargetLanguages),
43
- finalTranscription: h.finalTranscription,
44
- sttStatus: h.sttStatus,
45
- createdAt: parseInt(h.createdAt, 10),
46
- audioChunk: this.deserialize(h.audioChunk),
47
- stt: this.deserialize(h.stt),
48
- translation: this.deserialize(h.translation),
49
- tts: this.deserialize(h.tts)
50
- };
51
- }
52
- /* ------------------------------ timeouts ------------------------------- */
53
- getTimeout(start, end) {
54
- // const len = end - start;
55
- // if (len < 1_000) return 1_500;
56
- // if (len < 3_000) return 3_000;
57
- // if (len < 5_000) return 7_000;
58
- // if (len < 8_000) return 10_000;
59
- // if (len < 12_000) return 15_000;
60
- // if (len < 20_000) return 20_000;
61
- return 30000;
62
- }
63
- /* ---------------------------------------------------------------------- */
64
- /* Public API (same) */
65
- /* ---------------------------------------------------------------------- */
66
- /* Room helpers ---------------------------------------------------------- */
67
- async initRoom(roomId) {
68
- const exists = await this.redisClient.exists(this.roomZsetKey(roomId));
69
- if (exists)
70
- return false;
71
- await this.redisClient.expire(this.roomZsetKey(roomId), EXPIRATION);
72
- return true;
73
- }
74
- async getRooms() {
75
- const keys = await this.redisClient.keys('room:*:chunks');
76
- return keys.map((k) => k.replace(/^room:(.*):chunks$/, '$1'));
77
- }
78
- async getMulingstreamChunksByRoom(roomId) {
79
- const ids = await this.redisClient.zrange(this.roomZsetKey(roomId), 0, -1);
80
- if (!ids.length)
81
- return null;
82
- const pipe = this.redisClient.pipeline();
83
- ids.forEach((cid) => pipe.hgetall(this.chunkHashKey(cid)));
84
- const res = await pipe.exec();
85
- if (!res)
86
- return null;
87
- return res === null || res === void 0 ? void 0 : res.map(([, d]) => this.hashToChunk(d));
88
- }
89
- getRoomById(roomId) {
90
- return this.getMulingstreamChunksByRoom(roomId);
91
- }
92
- /* ------------------------- chunk insertion ---------------------------- */
93
- async addMulingstreamChunk(params) {
94
- var _a, _b;
95
- const { roomId, chunkNumber, language, start, end, duration, isFirst, isLast, theme, sttProviders, targetLanguages, shortCodeTargetLanguages } = params;
96
- /* ---------- reset room on first chunk ---------- */
97
- if (chunkNumber === 1) {
98
- const old = await this.redisClient.zrange(this.roomZsetKey(roomId), 0, -1);
99
- if (old.length) {
100
- const p = this.redisClient.pipeline();
101
- old.forEach((cid) => p.unlink(this.chunkHashKey(cid)));
102
- p.del(this.roomZsetKey(roomId));
103
- await p.exec();
104
- }
105
- }
106
- else {
107
- // remove older duplicate with same chunkNumber
108
- const dup = await this.redisClient.zrangebyscore(this.roomZsetKey(roomId), chunkNumber, chunkNumber);
109
- if (dup.length) {
110
- const p = this.redisClient.pipeline();
111
- dup.forEach((cid) => p.unlink(this.chunkHashKey(cid)));
112
- p.zrem(this.roomZsetKey(roomId), ...dup);
113
- await p.exec();
114
- }
115
- }
116
- /* ---------- build new chunk ---------- */
117
- const chunkId = this.generateChunkId(roomId, chunkNumber);
118
- const audioChunk = { start, end, duration, isFirst, isLast, theme, processingStart: Date.now() };
119
- const stt = {};
120
- sttProviders.forEach((p) => (stt[p] = { transcription: '', status: 'INIT' }));
121
- const translation = {};
122
- const tts = {};
123
- shortCodeTargetLanguages.forEach((l) => {
124
- translation[l] = { translation: '', status: 'INIT' };
125
- tts[l] = { ttsAudioPath: '', status: 'INIT', isEmitted: false, totalCheck: 0 };
126
- });
127
- const hash = {
128
- chunkId,
129
- roomId,
130
- chunkNumber: String(chunkNumber),
131
- language,
132
- sttProviders: this.serialize(sttProviders),
133
- targetLanguages: this.serialize(targetLanguages),
134
- shortCodeTargetLanguages: this.serialize(shortCodeTargetLanguages),
135
- finalTranscription: '',
136
- sttStatus: 'INIT',
137
- createdAt: String(Date.now()),
138
- audioChunk: this.serialize(audioChunk),
139
- stt: this.serialize(stt),
140
- translation: this.serialize(translation),
141
- tts: this.serialize(tts)
142
- };
143
- /* ---------- write + trim (pipeline) ---------- */
144
- const pipe = this.redisClient.pipeline();
145
- pipe.hset(this.chunkHashKey(chunkId), hash);
146
- pipe.expire(this.chunkHashKey(chunkId), EXPIRATION);
147
- pipe.zadd(this.roomZsetKey(roomId), chunkNumber, chunkId);
148
- pipe.expire(this.roomZsetKey(roomId), EXPIRATION);
149
- pipe.zrange(this.roomZsetKey(roomId), 0, -ROOM_ARRAY_LENGTH - 1, 'WITHSCORES');
150
- const execResults = await pipe.exec();
151
- /* execResults can be null or shorter than expected — fall back to an empty list */
152
- const oldPairs = ((_b = (_a = execResults === null || execResults === void 0 ? void 0 : execResults[4]) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : []); // [id1, score1, id2, score2, ...]
153
- if (Array.isArray(oldPairs) && oldPairs.length) {
154
- const oldIds = [];
155
- for (let i = 0; i < oldPairs.length; i += 2)
156
- oldIds.push(oldPairs[i]);
157
- const trim = this.redisClient.pipeline();
158
- trim.zrem(this.roomZsetKey(roomId), ...oldIds);
159
- oldIds.forEach((cid) => trim.unlink(this.chunkHashKey(cid)));
160
- await trim.exec();
161
- }
162
- }
163
- /* --------------------------- helper lookup ---------------------------- */
164
- async getChunkId(roomId, n) {
165
- const ids = await this.redisClient.zrangebyscore(this.roomZsetKey(roomId), n, n);
166
- return ids.length ? ids[0] : null;
167
- }
168
- async getMulingstreamChunkById(roomId, n) {
169
- const cid = await this.getChunkId(roomId, n);
170
- if (!cid)
171
- return null;
172
- const raw = await this.redisClient.hgetall(this.chunkHashKey(cid));
173
- return raw.chunkId ? this.hashToChunk(raw) : null;
174
- }
175
- /* ------------------------------ Updaters ------------------------------ */
176
- /* helper to fetch-mutate-save one chunk */
177
- async withChunk(roomId, n, fn) {
178
- const cid = await this.getChunkId(roomId, n);
179
- if (!cid)
180
- return null;
181
- const key = this.chunkHashKey(cid);
182
- const raw = await this.redisClient.hgetall(key);
183
- if (!raw.chunkId)
184
- return null;
185
- const chunk = this.hashToChunk(raw);
186
- const out = await fn(chunk);
187
- const p = this.redisClient.pipeline();
188
- p.hset(key, {
189
- finalTranscription: chunk.finalTranscription,
190
- sttStatus: chunk.sttStatus,
191
- stt: this.serialize(chunk.stt),
192
- translation: this.serialize(chunk.translation),
193
- tts: this.serialize(chunk.tts)
194
- });
195
- p.expire(key, EXPIRATION);
196
- await p.exec();
197
- return out;
198
- }
199
- /* ---- specific update methods (same signatures as before) ---- */
200
- async updateStt(roomId, n, provider, opt) {
201
- return ((await this.withChunk(roomId, n, (c) => {
202
- if (!c.stt[provider])
203
- return false;
204
- if (opt.transcription !== undefined)
205
- c.stt[provider].transcription = opt.transcription;
206
- if (opt.sttStatus !== undefined)
207
- c.stt[provider].status = opt.sttStatus;
208
- return true;
209
- })) === true);
210
- }
211
- async updateSttObject(roomId, n, newStt) {
212
- return ((await this.withChunk(roomId, n, (c) => {
213
- c.stt = newStt;
214
- return true;
215
- })) === true);
216
- }
217
- async discardStt(roomId, n) {
218
- return ((await this.withChunk(roomId, n, (c) => {
219
- Object.keys(c.stt).forEach((p) => {
220
- c.stt[p].transcription = '';
221
- c.stt[p].status = 'DISCARDED';
222
- });
223
- c.finalTranscription = '';
224
- c.sttStatus = 'DISCARDED';
225
- return true;
226
- })) === true);
227
- }
228
- async updateFinalTranscription(roomId, n, opt) {
229
- return this.withChunk(roomId, n, (c) => {
230
- if (opt.transcription !== undefined)
231
- c.finalTranscription = opt.transcription;
232
- if (opt.sttStatus !== undefined)
233
- c.sttStatus = opt.sttStatus;
234
- return c;
235
- });
236
- }
237
- async discardPostStt(roomId, n) {
238
- return ((await this.withChunk(roomId, n, (c) => {
239
- Object.values(c.translation).forEach((t) => (t.status = 'DISCARDED'));
240
- Object.values(c.tts).forEach((t) => (t.status = 'DISCARDED'));
241
- return true;
242
- })) === true);
243
- }
244
- async discardLanguage(roomId, n, lang) {
245
- return ((await this.withChunk(roomId, n, (c) => {
246
- if (c.translation[lang])
247
- c.translation[lang].status = 'DISCARDED';
248
- if (c.tts[lang])
249
- c.tts[lang].status = 'DISCARDED';
250
- return true;
251
- })) === true);
252
- }
253
- async discardLanguages(roomId, n, opt) {
254
- return this.withChunk(roomId, n, (c) => {
255
- var _a, _b;
256
- (_a = opt.translation) === null || _a === void 0 ? void 0 : _a.forEach((l) => {
257
- const e = c.translation[l];
258
- if (e && e.status === 'INIT')
259
- e.status = 'DISCARDED';
260
- });
261
- (_b = opt.tts) === null || _b === void 0 ? void 0 : _b.forEach((l) => {
262
- const e = c.tts[l];
263
- if (e && e.status === 'INIT')
264
- e.status = 'DISCARDED';
265
- });
266
- return c;
267
- });
268
- }
269
- async updateTranslation(roomId, n, lang, opt) {
270
- return this.withChunk(roomId, n, (c) => {
271
- const e = c.translation[lang];
272
- if (!e)
273
- return null;
274
- if (opt.translation !== undefined)
275
- e.translation = opt.translation;
276
- if (opt.status !== undefined)
277
- e.status = opt.status;
278
- return c;
279
- });
280
- }
281
- async updateTranslationInBulk(roomId, n, dict, status = 'READY') {
282
- return this.withChunk(roomId, n, (c) => {
283
- Object.entries(dict).forEach(([l, txt]) => {
284
- if (!c.translation[l])
285
- return;
286
- c.translation[l].translation = txt;
287
- c.translation[l].status = status;
288
- });
289
- return c;
290
- });
291
- }
292
- async updateTts(roomId, n, lang, opt) {
293
- return this.withChunk(roomId, n, (c) => {
294
- const e = c.tts[lang];
295
- if (!e)
296
- return null;
297
- if (opt.ttsAudioPath !== undefined)
298
- e.ttsAudioPath = opt.ttsAudioPath;
299
- if (opt.status !== undefined)
300
- e.status = opt.status;
301
- if (opt.isEmitted !== undefined)
302
- e.isEmitted = opt.isEmitted;
303
- if (opt.totalCheck !== undefined)
304
- e.totalCheck = opt.totalCheck;
305
- return c;
306
- });
307
- }
308
- async increaseTotalCheck(roomId, n, lang) {
309
- return this.withChunk(roomId, n, (c) => {
310
- if (!c.tts[lang])
311
- return null;
312
- c.tts[lang].totalCheck += 1;
313
- return c;
314
- });
315
- }
316
- async areTranslationsProcessed(roomId, n) {
317
- const c = await this.getMulingstreamChunkById(roomId, n);
318
- return !!c && Object.values(c.translation).every((t) => t.status !== 'INIT');
319
- }
320
- /* -------------------------- READY-TTS sequence ------------------------- */
321
- async getAllReadyTts(roomId, lang) {
322
- var _a;
323
- const chunks = (_a = (await this.getMulingstreamChunksByRoom(roomId))) !== null && _a !== void 0 ? _a : [];
324
- if (!chunks.length)
325
- return [];
326
- chunks.sort((a, b) => a.chunkNumber - b.chunkNumber);
327
- /* isolate window where lang exists */
328
- let lastIdx = -1;
329
- for (let i = chunks.length - 1; i >= 0; i--)
330
- if (chunks[i].tts[lang]) {
331
- lastIdx = i;
332
- break;
333
- }
334
- if (lastIdx === -1)
335
- return [];
336
- let firstIdx = lastIdx;
337
- for (let i = lastIdx - 1; i >= 0; i--) {
338
- if (!chunks[i].tts[lang])
339
- break;
340
- firstIdx = i;
341
- }
342
- const window = chunks.slice(firstIdx, lastIdx + 1);
343
- /* find last USED/DISCARDED */
344
- let lastDone = -1;
345
- for (let i = window.length - 1; i >= 0; i--) {
346
- const s = window[i].tts[lang].status;
347
- if (s === 'USED' || s === 'DISCARDED') {
348
- lastDone = i;
349
- break;
350
- }
351
- }
352
- const ready = [];
353
- let blocked = false;
354
- const now = Date.now();
355
- for (let i = lastDone + 1; i < window.length; i++) {
356
- const entry = window[i].tts[lang];
357
- const audio = window[i].audioChunk;
358
- if (entry.status === 'INIT') {
359
- if (now - window[i].createdAt > this.getTimeout(audio.start, audio.end))
360
- entry.status = 'DISCARDED';
361
- else
362
- blocked = true;
363
- continue;
364
- }
365
- if (entry.status === 'READY') {
366
- if (blocked)
367
- break;
368
- ready.push(window[i]);
369
- continue;
370
- }
371
- break; // got USED/DISCARDED unexpectedly
372
- }
373
- /* persist DISCARDED changes (if any) */
374
- if (!ready.length || blocked) {
375
- const p = this.redisClient.pipeline();
376
- window.forEach((c) => p.hset(this.chunkHashKey(c.chunkId), { tts: this.serialize(c.tts) }));
377
- await p.exec();
378
- }
379
- return ready;
380
- }
381
- }
382
- exports.MulingstreamChunkManager = MulingstreamChunkManager;
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MulingstreamChunkManager = void 0;
4
+ const uuid_1 = require("uuid");
5
+ /* -------------------------------------------------------------------------- */
6
+ /* Constants */
7
+ /* -------------------------------------------------------------------------- */
8
+ const EXPIRATION = 12 * 60 * 60; // 12 h
9
+ const ROOM_ARRAY_LENGTH = 50; // keep last 50 chunks
10
+ /* -------------------------------------------------------------------------- */
11
+ /* Main class */
12
+ /* -------------------------------------------------------------------------- */
13
+ class MulingstreamChunkManager {
14
+ constructor(redisClient) {
15
+ this.redisClient = redisClient;
16
+ }
17
+ /* ----------------------------- key helpers ----------------------------- */
18
+ roomZsetKey(roomId) {
19
+ return `room:${roomId}:chunks`;
20
+ }
21
+ chunkHashKey(chunkId) {
22
+ return `chunk:${chunkId}`;
23
+ }
24
+ generateChunkId(roomId, chunkNumber) {
25
+ return `[${roomId}]-[${chunkNumber}]-[${(0, uuid_1.v4)()}]`;
26
+ }
27
+ /* --------------------------- (de)serialization -------------------------- */
28
+ serialize(value) {
29
+ return JSON.stringify(value);
30
+ }
31
+ deserialize(v) {
32
+ return v ? JSON.parse(v) : undefined;
33
+ }
34
+ hashToChunk(h) {
35
+ return {
36
+ chunkId: h.chunkId,
37
+ roomId: h.roomId,
38
+ chunkNumber: parseInt(h.chunkNumber, 10),
39
+ language: h.language,
40
+ sttProviders: this.deserialize(h.sttProviders),
41
+ targetLanguages: this.deserialize(h.targetLanguages),
42
+ shortCodeTargetLanguages: this.deserialize(h.shortCodeTargetLanguages),
43
+ finalTranscription: h.finalTranscription,
44
+ sttStatus: h.sttStatus,
45
+ createdAt: parseInt(h.createdAt, 10),
46
+ audioChunk: this.deserialize(h.audioChunk),
47
+ stt: this.deserialize(h.stt),
48
+ translation: this.deserialize(h.translation),
49
+ tts: this.deserialize(h.tts)
50
+ };
51
+ }
52
+ /* ------------------------------ timeouts ------------------------------- */
53
+ getTimeout(start, end) {
54
+ // const len = end - start;
55
+ // if (len < 1_000) return 1_500;
56
+ // if (len < 3_000) return 3_000;
57
+ // if (len < 5_000) return 7_000;
58
+ // if (len < 8_000) return 10_000;
59
+ // if (len < 12_000) return 15_000;
60
+ // if (len < 20_000) return 20_000;
61
+ return 30000;
62
+ }
63
+ /* ---------------------------------------------------------------------- */
64
+ /* Public API (same) */
65
+ /* ---------------------------------------------------------------------- */
66
+ /* Room helpers ---------------------------------------------------------- */
67
+ async initRoom(roomId) {
68
+ const exists = await this.redisClient.exists(this.roomZsetKey(roomId));
69
+ if (exists)
70
+ return false;
71
+ await this.redisClient.expire(this.roomZsetKey(roomId), EXPIRATION);
72
+ return true;
73
+ }
74
+ async getRooms() {
75
+ const keys = await this.redisClient.keys('room:*:chunks');
76
+ return keys.map((k) => k.replace(/^room:(.*):chunks$/, '$1'));
77
+ }
78
+ async getMulingstreamChunksByRoom(roomId) {
79
+ const ids = await this.redisClient.zrange(this.roomZsetKey(roomId), 0, -1);
80
+ if (!ids.length)
81
+ return null;
82
+ const pipe = this.redisClient.pipeline();
83
+ ids.forEach((cid) => pipe.hgetall(this.chunkHashKey(cid)));
84
+ const res = await pipe.exec();
85
+ if (!res)
86
+ return null;
87
+ return res === null || res === void 0 ? void 0 : res.map(([, d]) => this.hashToChunk(d));
88
+ }
89
+ getRoomById(roomId) {
90
+ return this.getMulingstreamChunksByRoom(roomId);
91
+ }
92
+ /* ------------------------- chunk insertion ---------------------------- */
93
+ async addMulingstreamChunk(params) {
94
+ var _a, _b;
95
+ const { roomId, chunkNumber, language, start, end, duration, isFirst, isLast, theme, sttProviders, targetLanguages, shortCodeTargetLanguages } = params;
96
+ if (chunkNumber === 1) {
97
+ const old = await this.redisClient.zrange(this.roomZsetKey(roomId), 0, -1);
98
+ if (old.length) {
99
+ const p = this.redisClient.pipeline();
100
+ old.forEach((cid) => p.unlink(this.chunkHashKey(cid)));
101
+ p.del(this.roomZsetKey(roomId));
102
+ await p.exec();
103
+ }
104
+ }
105
+ else {
106
+ const dup = await this.redisClient.zrangebyscore(this.roomZsetKey(roomId), chunkNumber, chunkNumber);
107
+ if (dup.length) {
108
+ const p = this.redisClient.pipeline();
109
+ dup.forEach((cid) => p.unlink(this.chunkHashKey(cid)));
110
+ p.zrem(this.roomZsetKey(roomId), ...dup);
111
+ await p.exec();
112
+ }
113
+ }
114
+ const chunkId = this.generateChunkId(roomId, chunkNumber);
115
+ const audioChunk = { start, end, duration, isFirst, isLast, theme, processingStart: Date.now() };
116
+ const stt = {};
117
+ sttProviders.forEach((p) => (stt[p] = { transcription: '', apiType: '', status: 'INIT' }));
118
+ const translation = {};
119
+ const tts = {};
120
+ shortCodeTargetLanguages.forEach((l) => {
121
+ translation[l] = { translation: '', status: 'INIT' };
122
+ tts[l] = { ttsAudioPath: '', status: 'INIT', isEmitted: false, totalCheck: 0 };
123
+ });
124
+ const hash = {
125
+ chunkId,
126
+ roomId,
127
+ chunkNumber: String(chunkNumber),
128
+ language,
129
+ sttProviders: this.serialize(sttProviders),
130
+ targetLanguages: this.serialize(targetLanguages),
131
+ shortCodeTargetLanguages: this.serialize(shortCodeTargetLanguages),
132
+ finalTranscription: '',
133
+ sttStatus: 'INIT',
134
+ createdAt: String(Date.now()),
135
+ audioChunk: this.serialize(audioChunk),
136
+ stt: this.serialize(stt),
137
+ translation: this.serialize(translation),
138
+ tts: this.serialize(tts)
139
+ };
140
+ const pipe = this.redisClient.pipeline();
141
+ pipe.hset(this.chunkHashKey(chunkId), hash);
142
+ pipe.expire(this.chunkHashKey(chunkId), EXPIRATION);
143
+ pipe.zadd(this.roomZsetKey(roomId), chunkNumber, chunkId);
144
+ pipe.expire(this.roomZsetKey(roomId), EXPIRATION);
145
+ pipe.zrange(this.roomZsetKey(roomId), 0, -ROOM_ARRAY_LENGTH - 1, 'WITHSCORES');
146
+ const execResults = await pipe.exec();
147
+ const oldPairs = ((_b = (_a = execResults === null || execResults === void 0 ? void 0 : execResults[4]) === null || _a === void 0 ? void 0 : _a[1]) !== null && _b !== void 0 ? _b : []);
148
+ if (Array.isArray(oldPairs) && oldPairs.length) {
149
+ const oldIds = [];
150
+ for (let i = 0; i < oldPairs.length; i += 2)
151
+ oldIds.push(oldPairs[i]);
152
+ const trim = this.redisClient.pipeline();
153
+ trim.zrem(this.roomZsetKey(roomId), ...oldIds);
154
+ oldIds.forEach((cid) => trim.unlink(this.chunkHashKey(cid)));
155
+ await trim.exec();
156
+ }
157
+ }
158
+ /* --------------------------- helper lookup ---------------------------- */
159
+ async getChunkId(roomId, n) {
160
+ const ids = await this.redisClient.zrangebyscore(this.roomZsetKey(roomId), n, n);
161
+ return ids.length ? ids[0] : null;
162
+ }
163
+ async getMulingstreamChunkById(roomId, n) {
164
+ const cid = await this.getChunkId(roomId, n);
165
+ if (!cid)
166
+ return null;
167
+ const raw = await this.redisClient.hgetall(this.chunkHashKey(cid));
168
+ return raw.chunkId ? this.hashToChunk(raw) : null;
169
+ }
170
+ /* ------------------------------ Updaters ------------------------------ */
171
+ /* helper to fetch-mutate-save one chunk */
172
+ async withChunk(roomId, n, fn) {
173
+ const cid = await this.getChunkId(roomId, n);
174
+ if (!cid)
175
+ return null;
176
+ const key = this.chunkHashKey(cid);
177
+ const raw = await this.redisClient.hgetall(key);
178
+ if (!raw.chunkId)
179
+ return null;
180
+ const chunk = this.hashToChunk(raw);
181
+ const out = await fn(chunk);
182
+ const p = this.redisClient.pipeline();
183
+ p.hset(key, {
184
+ finalTranscription: chunk.finalTranscription,
185
+ sttStatus: chunk.sttStatus,
186
+ stt: this.serialize(chunk.stt),
187
+ translation: this.serialize(chunk.translation),
188
+ tts: this.serialize(chunk.tts)
189
+ });
190
+ p.expire(key, EXPIRATION);
191
+ await p.exec();
192
+ return out;
193
+ }
194
+ /* ---- specific update methods (same signatures as before) ---- */
195
+ async updateStt(roomId, n, provider, opt) {
196
+ return ((await this.withChunk(roomId, n, (c) => {
197
+ if (!c.stt[provider])
198
+ return false;
199
+ if (opt.transcription !== undefined)
200
+ c.stt[provider].transcription = opt.transcription;
201
+ if (opt.sttStatus !== undefined)
202
+ c.stt[provider].status = opt.sttStatus;
203
+ return true;
204
+ })) === true);
205
+ }
206
+ async updateSttObject(roomId, n, newStt) {
207
+ return ((await this.withChunk(roomId, n, (c) => {
208
+ c.stt = newStt;
209
+ return true;
210
+ })) === true);
211
+ }
212
+ async discardStt(roomId, n) {
213
+ return ((await this.withChunk(roomId, n, (c) => {
214
+ Object.keys(c.stt).forEach((p) => {
215
+ c.stt[p].transcription = '';
216
+ c.stt[p].status = 'DISCARDED';
217
+ });
218
+ c.finalTranscription = '';
219
+ c.sttStatus = 'DISCARDED';
220
+ return true;
221
+ })) === true);
222
+ }
223
+ async updateFinalTranscription(roomId, n, opt) {
224
+ return this.withChunk(roomId, n, (c) => {
225
+ if (opt.transcription !== undefined)
226
+ c.finalTranscription = opt.transcription;
227
+ if (opt.sttStatus !== undefined)
228
+ c.sttStatus = opt.sttStatus;
229
+ return c;
230
+ });
231
+ }
232
+ async discardPostStt(roomId, n) {
233
+ return ((await this.withChunk(roomId, n, (c) => {
234
+ Object.values(c.translation).forEach((t) => (t.status = 'DISCARDED'));
235
+ Object.values(c.tts).forEach((t) => (t.status = 'DISCARDED'));
236
+ return true;
237
+ })) === true);
238
+ }
239
+ async discardLanguage(roomId, n, lang) {
240
+ return ((await this.withChunk(roomId, n, (c) => {
241
+ if (c.translation[lang])
242
+ c.translation[lang].status = 'DISCARDED';
243
+ if (c.tts[lang])
244
+ c.tts[lang].status = 'DISCARDED';
245
+ return true;
246
+ })) === true);
247
+ }
248
+ async discardLanguages(roomId, n, opt) {
249
+ return this.withChunk(roomId, n, (c) => {
250
+ var _a, _b;
251
+ (_a = opt.translation) === null || _a === void 0 ? void 0 : _a.forEach((l) => {
252
+ const e = c.translation[l];
253
+ if (e && e.status === 'INIT')
254
+ e.status = 'DISCARDED';
255
+ });
256
+ (_b = opt.tts) === null || _b === void 0 ? void 0 : _b.forEach((l) => {
257
+ const e = c.tts[l];
258
+ if (e && e.status === 'INIT')
259
+ e.status = 'DISCARDED';
260
+ });
261
+ return c;
262
+ });
263
+ }
264
+ async updateTranslation(roomId, n, lang, opt) {
265
+ return this.withChunk(roomId, n, (c) => {
266
+ const e = c.translation[lang];
267
+ if (!e)
268
+ return null;
269
+ if (opt.translation !== undefined)
270
+ e.translation = opt.translation;
271
+ if (opt.status !== undefined)
272
+ e.status = opt.status;
273
+ return c;
274
+ });
275
+ }
276
+ async updateTranslationInBulk(roomId, n, dict, status = 'READY') {
277
+ return this.withChunk(roomId, n, (c) => {
278
+ Object.entries(dict).forEach(([l, txt]) => {
279
+ if (!c.translation[l])
280
+ return;
281
+ c.translation[l].translation = txt;
282
+ c.translation[l].status = status;
283
+ });
284
+ return c;
285
+ });
286
+ }
287
+ async updateTts(roomId, n, lang, opt) {
288
+ return this.withChunk(roomId, n, (c) => {
289
+ const e = c.tts[lang];
290
+ if (!e)
291
+ return null;
292
+ if (opt.ttsAudioPath !== undefined)
293
+ e.ttsAudioPath = opt.ttsAudioPath;
294
+ if (opt.status !== undefined)
295
+ e.status = opt.status;
296
+ if (opt.isEmitted !== undefined)
297
+ e.isEmitted = opt.isEmitted;
298
+ if (opt.totalCheck !== undefined)
299
+ e.totalCheck = opt.totalCheck;
300
+ return c;
301
+ });
302
+ }
303
+ async increaseTotalCheck(roomId, n, lang) {
304
+ return this.withChunk(roomId, n, (c) => {
305
+ if (!c.tts[lang])
306
+ return null;
307
+ c.tts[lang].totalCheck += 1;
308
+ return c;
309
+ });
310
+ }
311
+ async areTranslationsProcessed(roomId, n) {
312
+ const c = await this.getMulingstreamChunkById(roomId, n);
313
+ return !!c && Object.values(c.translation).every((t) => t.status !== 'INIT');
314
+ }
315
+ /* -------------------------- READY-TTS sequence ------------------------- */
316
+ async getAllReadyTts(roomId, lang) {
317
+ var _a;
318
+ const chunks = (_a = (await this.getMulingstreamChunksByRoom(roomId))) !== null && _a !== void 0 ? _a : [];
319
+ if (!chunks.length)
320
+ return [];
321
+ chunks.sort((a, b) => a.chunkNumber - b.chunkNumber);
322
+ /* isolate window where lang exists */
323
+ let lastIdx = -1;
324
+ for (let i = chunks.length - 1; i >= 0; i--)
325
+ if (chunks[i].tts[lang]) {
326
+ lastIdx = i;
327
+ break;
328
+ }
329
+ if (lastIdx === -1)
330
+ return [];
331
+ let firstIdx = lastIdx;
332
+ for (let i = lastIdx - 1; i >= 0; i--) {
333
+ if (!chunks[i].tts[lang])
334
+ break;
335
+ firstIdx = i;
336
+ }
337
+ const window = chunks.slice(firstIdx, lastIdx + 1);
338
+ /* find last USED/DISCARDED */
339
+ let lastDone = -1;
340
+ for (let i = window.length - 1; i >= 0; i--) {
341
+ const s = window[i].tts[lang].status;
342
+ if (s === 'USED' || s === 'DISCARDED') {
343
+ lastDone = i;
344
+ break;
345
+ }
346
+ }
347
+ const ready = [];
348
+ let blocked = false;
349
+ const now = Date.now();
350
+ for (let i = lastDone + 1; i < window.length; i++) {
351
+ const entry = window[i].tts[lang];
352
+ const audio = window[i].audioChunk;
353
+ if (entry.status === 'INIT') {
354
+ if (now - window[i].createdAt > this.getTimeout(audio.start, audio.end))
355
+ entry.status = 'DISCARDED';
356
+ else
357
+ blocked = true;
358
+ continue;
359
+ }
360
+ if (entry.status === 'READY') {
361
+ if (blocked)
362
+ break;
363
+ ready.push(window[i]);
364
+ continue;
365
+ }
366
+ break; // got USED/DISCARDED unexpectedly
367
+ }
368
+ /* persist DISCARDED changes (if any) */
369
+ if (!ready.length || blocked) {
370
+ const p = this.redisClient.pipeline();
371
+ window.forEach((c) => p.hset(this.chunkHashKey(c.chunkId), { tts: this.serialize(c.tts) }));
372
+ await p.exec();
373
+ }
374
+ return ready;
375
+ }
376
+ }
377
+ exports.MulingstreamChunkManager = MulingstreamChunkManager;