@mulingai-npm/redis 3.40.40 → 3.40.43
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.
|
@@ -181,8 +181,21 @@ export declare class MulingstreamChunkManager {
|
|
|
181
181
|
*/
|
|
182
182
|
updateTranslationInBulk(roomId: string, n: number, dict: Record<string, string>, status?: StepStatus, hintsPerLanguage?: Record<string, TranslationHint[]>): Promise<MulingstreamChunkData | null>;
|
|
183
183
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
184
|
+
* Atomic per-language merge into the serialized `tts` hash field.
|
|
185
|
+
*
|
|
186
|
+
* The `tts` field is ONE JSON blob holding every target language's TTS state. A naive
|
|
187
|
+
* hget -> modify -> hset (the previous implementation) races with concurrent updateTts calls
|
|
188
|
+
* for OTHER languages on the SAME chunk: with fast TTS (ElevenLabs Flash ~150ms) all target
|
|
189
|
+
* languages of a chunk finish within a few ms of each other, so two handlers read the same
|
|
190
|
+
* snapshot and the second hset clobbers the first language's freshly-written READY back to its
|
|
191
|
+
* old value (INIT). The sequencer then reads INIT and drops the audio
|
|
192
|
+
* ('[audio-drop] reason=not-ready'), leaving the listener with text but no audio even though
|
|
193
|
+
* TTS succeeded — the faster the provider, the tighter the window and the more clips are lost.
|
|
194
|
+
*
|
|
195
|
+
* Running the read-modify-write inside a Lua script makes it atomic (Redis executes a script to
|
|
196
|
+
* completion single-threaded), so sibling-language writes can no longer interleave. WATCH/MULTI
|
|
197
|
+
* is NOT usable here: RedisClient shares a single ioredis connection across all callers, and
|
|
198
|
+
* concurrent commands on that connection would break optimistic-lock semantics.
|
|
186
199
|
*/
|
|
187
200
|
updateTts(roomId: string, n: number, lang: string, opt: {
|
|
188
201
|
ttsAudioPath?: string;
|
|
@@ -4,6 +4,27 @@ exports.MulingstreamChunkManager = void 0;
|
|
|
4
4
|
const uuid_1 = require("uuid");
|
|
5
5
|
const EXPIRATION = 12 * 60 * 60;
|
|
6
6
|
const ROOM_ARRAY_LENGTH = 50;
|
|
7
|
+
/**
|
|
8
|
+
* Atomic merge of a partial update into one language's entry inside the serialized `tts` JSON
|
|
9
|
+
* hash field. Runs entirely inside Redis so concurrent per-language writes to the same chunk can
|
|
10
|
+
* never clobber each other (see MulingstreamChunkManager.updateTts for the race this prevents).
|
|
11
|
+
* KEYS[1] = chunk hash key
|
|
12
|
+
* ARGV[1] = language code ARGV[2] = JSON of the partial patch ARGV[3] = expiry seconds
|
|
13
|
+
* Returns 1 on merge, -1 if the language entry is absent (no-op), 0 if there is no tts field yet.
|
|
14
|
+
*/
|
|
15
|
+
const TTS_MERGE_LUA = `
|
|
16
|
+
local raw = redis.call('HGET', KEYS[1], 'tts')
|
|
17
|
+
if not raw or raw == '' then return 0 end
|
|
18
|
+
local ok, tts = pcall(cjson.decode, raw)
|
|
19
|
+
if not ok or type(tts) ~= 'table' then return 0 end
|
|
20
|
+
local entry = tts[ARGV[1]]
|
|
21
|
+
if entry == nil then return -1 end
|
|
22
|
+
local patch = cjson.decode(ARGV[2])
|
|
23
|
+
for k, v in pairs(patch) do entry[k] = v end
|
|
24
|
+
redis.call('HSET', KEYS[1], 'tts', cjson.encode(tts))
|
|
25
|
+
redis.call('EXPIRE', KEYS[1], tonumber(ARGV[3]))
|
|
26
|
+
return 1
|
|
27
|
+
`;
|
|
7
28
|
class MulingstreamChunkManager {
|
|
8
29
|
constructor(redisClient) {
|
|
9
30
|
this.redisClient = redisClient;
|
|
@@ -413,32 +434,38 @@ class MulingstreamChunkManager {
|
|
|
413
434
|
return this.getMulingstreamChunkById(roomId, n);
|
|
414
435
|
}
|
|
415
436
|
/**
|
|
416
|
-
*
|
|
417
|
-
*
|
|
437
|
+
* Atomic per-language merge into the serialized `tts` hash field.
|
|
438
|
+
*
|
|
439
|
+
* The `tts` field is ONE JSON blob holding every target language's TTS state. A naive
|
|
440
|
+
* hget -> modify -> hset (the previous implementation) races with concurrent updateTts calls
|
|
441
|
+
* for OTHER languages on the SAME chunk: with fast TTS (ElevenLabs Flash ~150ms) all target
|
|
442
|
+
* languages of a chunk finish within a few ms of each other, so two handlers read the same
|
|
443
|
+
* snapshot and the second hset clobbers the first language's freshly-written READY back to its
|
|
444
|
+
* old value (INIT). The sequencer then reads INIT and drops the audio
|
|
445
|
+
* ('[audio-drop] reason=not-ready'), leaving the listener with text but no audio even though
|
|
446
|
+
* TTS succeeded — the faster the provider, the tighter the window and the more clips are lost.
|
|
447
|
+
*
|
|
448
|
+
* Running the read-modify-write inside a Lua script makes it atomic (Redis executes a script to
|
|
449
|
+
* completion single-threaded), so sibling-language writes can no longer interleave. WATCH/MULTI
|
|
450
|
+
* is NOT usable here: RedisClient shares a single ioredis connection across all callers, and
|
|
451
|
+
* concurrent commands on that connection would break optimistic-lock semantics.
|
|
418
452
|
*/
|
|
419
453
|
async updateTts(roomId, n, lang, opt) {
|
|
420
|
-
var _a;
|
|
421
454
|
const cid = await this.getChunkId(roomId, n);
|
|
422
455
|
if (!cid)
|
|
423
456
|
return null;
|
|
424
457
|
const key = this.chunkHashKey(cid);
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
const e = tts[lang];
|
|
428
|
-
if (!e)
|
|
429
|
-
return this.getMulingstreamChunkById(roomId, n);
|
|
458
|
+
// Only the explicitly-provided fields are merged; absent fields are left untouched.
|
|
459
|
+
const patch = {};
|
|
430
460
|
if (opt.ttsAudioPath !== undefined)
|
|
431
|
-
|
|
461
|
+
patch.ttsAudioPath = opt.ttsAudioPath;
|
|
432
462
|
if (opt.status !== undefined)
|
|
433
|
-
|
|
463
|
+
patch.status = opt.status;
|
|
434
464
|
if (opt.isEmitted !== undefined)
|
|
435
|
-
|
|
465
|
+
patch.isEmitted = opt.isEmitted;
|
|
436
466
|
if (opt.duration !== undefined)
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
pipe.hset(key, { tts: this.serialize(tts) });
|
|
440
|
-
pipe.expire(key, EXPIRATION);
|
|
441
|
-
await pipe.exec();
|
|
467
|
+
patch.duration = opt.duration;
|
|
468
|
+
await this.redisClient.eval(TTS_MERGE_LUA, [key], [lang, JSON.stringify(patch), EXPIRATION]);
|
|
442
469
|
return this.getMulingstreamChunkById(roomId, n);
|
|
443
470
|
}
|
|
444
471
|
/**
|
package/dist/redis-client.d.ts
CHANGED
|
@@ -44,6 +44,13 @@ export declare class RedisClient {
|
|
|
44
44
|
lrange(key: string, start: number, stop: number): Promise<string[]>;
|
|
45
45
|
ltrim(key: string, start: number, stop: number): Promise<string>;
|
|
46
46
|
pipeline(): ReturnType<IORedis['pipeline']>;
|
|
47
|
+
/**
|
|
48
|
+
* Run a Lua script atomically (Redis executes each script to completion single-threaded, so a
|
|
49
|
+
* read-modify-write inside one script cannot be interleaved by another writer). Used for
|
|
50
|
+
* lost-update-safe merges into serialized JSON hash fields — see MulingstreamChunkManager.updateTts.
|
|
51
|
+
* Args are stringified because every Redis command argument is a bulk string; use tonumber() in Lua.
|
|
52
|
+
*/
|
|
53
|
+
eval(script: string, keys: string[], args: (string | number)[]): Promise<unknown>;
|
|
47
54
|
unlink(...keys: string[]): Promise<number>;
|
|
48
55
|
jsonSet<T>(key: string, path: string, value: T): Promise<string>;
|
|
49
56
|
jsonGet<T>(key: string, path: string): Promise<T | null>;
|
package/dist/redis-client.js
CHANGED
|
@@ -126,6 +126,15 @@ class RedisClient {
|
|
|
126
126
|
pipeline() {
|
|
127
127
|
return this.client.pipeline();
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Run a Lua script atomically (Redis executes each script to completion single-threaded, so a
|
|
131
|
+
* read-modify-write inside one script cannot be interleaved by another writer). Used for
|
|
132
|
+
* lost-update-safe merges into serialized JSON hash fields — see MulingstreamChunkManager.updateTts.
|
|
133
|
+
* Args are stringified because every Redis command argument is a bulk string; use tonumber() in Lua.
|
|
134
|
+
*/
|
|
135
|
+
async eval(script, keys, args) {
|
|
136
|
+
return this.client.call('EVAL', script, String(keys.length), ...keys, ...args.map(String));
|
|
137
|
+
}
|
|
129
138
|
async unlink(...keys) {
|
|
130
139
|
// unlink exists on Redis ≥4 and is supported by ioredis but not typed in
|
|
131
140
|
// older versions – we cast to any.
|