@leiboss/dsh-tts 0.0.0-stage → 0.2.1
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 +74 -0
- package/LICENSE +21 -0
- package/README.md +352 -2
- package/cordis.patch.yml +4 -0
- package/dist/audio-route.d.ts +116 -0
- package/dist/audio-route.d.ts.map +1 -0
- package/dist/audio-route.js +406 -0
- package/dist/audio-route.js.map +1 -0
- package/dist/cache.d.ts +41 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +105 -0
- package/dist/cache.js.map +1 -0
- package/dist/chunked-tts.d.ts +34 -0
- package/dist/chunked-tts.d.ts.map +1 -0
- package/dist/chunked-tts.js +121 -0
- package/dist/chunked-tts.js.map +1 -0
- package/dist/chunker.d.ts +19 -0
- package/dist/chunker.d.ts.map +1 -0
- package/dist/chunker.js +98 -0
- package/dist/chunker.js.map +1 -0
- package/dist/client.js +90 -0
- package/dist/concurrency.d.ts +33 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +95 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.d.ts +133 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +143 -0
- package/dist/config.js.map +1 -0
- package/dist/edge-tts.d.ts +74 -0
- package/dist/edge-tts.d.ts.map +1 -0
- package/dist/edge-tts.js +154 -0
- package/dist/edge-tts.js.map +1 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +68 -0
- package/dist/errors.js.map +1 -0
- package/dist/fallback.d.ts +55 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +79 -0
- package/dist/fallback.js.map +1 -0
- package/dist/index.d.ts +69 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +617 -0
- package/dist/index.js.map +1 -0
- package/dist/probe.d.ts +42 -0
- package/dist/probe.d.ts.map +1 -0
- package/dist/probe.js +63 -0
- package/dist/probe.js.map +1 -0
- package/dist/provider.d.ts +33 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +7 -0
- package/dist/provider.js.map +1 -0
- package/dist/sapi-tts.d.ts +29 -0
- package/dist/sapi-tts.d.ts.map +1 -0
- package/dist/sapi-tts.js +131 -0
- package/dist/sapi-tts.js.map +1 -0
- package/dist/self-check.d.ts +24 -0
- package/dist/self-check.d.ts.map +1 -0
- package/dist/self-check.js +72 -0
- package/dist/self-check.js.map +1 -0
- package/dist/voices.d.ts +21 -0
- package/dist/voices.d.ts.map +1 -0
- package/dist/voices.js +74 -0
- package/dist/voices.js.map +1 -0
- package/icon.svg +6 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +88 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serial chunked synthesis for long texts.
|
|
3
|
+
*
|
|
4
|
+
* Every chunk goes through the existing Edge-TTS path into a private temp
|
|
5
|
+
* directory, in order, one subprocess at a time. The finished MP3 files are
|
|
6
|
+
* concatenated byte-for-byte (MP3 frame streams allow this) and the result is
|
|
7
|
+
* published atomically at the normal cache path, so the cache key remains the
|
|
8
|
+
* hash of the full original text.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { AUDIO_FORMAT, outputFilePath } from "./cache.js";
|
|
14
|
+
import { DEFAULT_TOTAL_TIMEOUT_MS } from "./config.js";
|
|
15
|
+
import { splitText } from "./chunker.js";
|
|
16
|
+
import { synthesize, } from "./edge-tts.js";
|
|
17
|
+
import { ErrorCode, TtsError } from "./errors.js";
|
|
18
|
+
/** Pause between chunk subprocesses so Edge-TTS is not rate-limited. */
|
|
19
|
+
export const DEFAULT_INTER_CHUNK_DELAY_MS = 200;
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
function chunkFailure(index, total, error) {
|
|
24
|
+
if (error instanceof TtsError) {
|
|
25
|
+
return new TtsError(error.code, `Chunk ${index + 1}/${total} failed: ${error.detail}`, error.suggestion, { cause: error });
|
|
26
|
+
}
|
|
27
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return new TtsError(ErrorCode.EdgeTtsFailed, `Chunk ${index + 1}/${total} failed: ${detail}`, 'Retry the call; if it persists, run `tts_self_check` and inspect Edge-TTS output.', { cause: error });
|
|
29
|
+
}
|
|
30
|
+
function publishFailure(target, error) {
|
|
31
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
32
|
+
return new TtsError(ErrorCode.OutputDirNotWritable, `Cannot write the concatenated audio file ${target}: ${detail}`, 'Check the DSH sandbox file policy and the output directory permissions.', { cause: error });
|
|
33
|
+
}
|
|
34
|
+
function overallTimeout(totalTimeoutMs, index, total) {
|
|
35
|
+
return new TtsError(ErrorCode.SynthesisTimeout, `Overall synthesis deadline of ${totalTimeoutMs} ms was exceeded before chunk ${index + 1}/${total}.`, 'Increase `totalTimeoutMs`, shorten the text, or raise `chunkSize`.');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Synthesize `text` chunk by chunk and return one concatenated MP3. Callers
|
|
39
|
+
* only use this above the chunk threshold; calling it with one chunk simply
|
|
40
|
+
* delegates to the one-shot path.
|
|
41
|
+
*/
|
|
42
|
+
export async function synthesizeChunked(text, options) {
|
|
43
|
+
const chunks = splitText(text, options.chunkSize);
|
|
44
|
+
const base = {
|
|
45
|
+
voice: options.voice,
|
|
46
|
+
rate: options.rate,
|
|
47
|
+
pitch: options.pitch,
|
|
48
|
+
volume: options.volume,
|
|
49
|
+
proxy: options.proxy,
|
|
50
|
+
binary: options.binary,
|
|
51
|
+
timeoutMs: options.timeoutMs,
|
|
52
|
+
signal: options.signal,
|
|
53
|
+
spawnImpl: options.spawnImpl,
|
|
54
|
+
};
|
|
55
|
+
if (chunks.length <= 1) {
|
|
56
|
+
return synthesize(text, {
|
|
57
|
+
...base,
|
|
58
|
+
outputDir: options.outputDir,
|
|
59
|
+
hash: options.hash,
|
|
60
|
+
now: options.now,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const delayMs = options.interChunkDelayMs ?? DEFAULT_INTER_CHUNK_DELAY_MS;
|
|
64
|
+
const delay = options.delay ?? sleep;
|
|
65
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'dsh-tts-chunks-'));
|
|
66
|
+
const baseTime = (options.now ?? new Date()).getTime();
|
|
67
|
+
const totalTimeoutMs = options.totalTimeoutMs ?? DEFAULT_TOTAL_TIMEOUT_MS;
|
|
68
|
+
const deadline = Date.now() + totalTimeoutMs;
|
|
69
|
+
try {
|
|
70
|
+
const buffers = [];
|
|
71
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
72
|
+
if (options.signal?.aborted) {
|
|
73
|
+
throw new TtsError(ErrorCode.Aborted, 'Chunked synthesis was aborted.');
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() >= deadline) {
|
|
76
|
+
throw overallTimeout(totalTimeoutMs, index, chunks.length);
|
|
77
|
+
}
|
|
78
|
+
if (index > 0 && delayMs > 0)
|
|
79
|
+
await delay(delayMs);
|
|
80
|
+
if (Date.now() >= deadline) {
|
|
81
|
+
throw overallTimeout(totalTimeoutMs, index, chunks.length);
|
|
82
|
+
}
|
|
83
|
+
const remaining = deadline - Date.now();
|
|
84
|
+
try {
|
|
85
|
+
const part = await synthesize(chunks[index] ?? '', {
|
|
86
|
+
...base,
|
|
87
|
+
timeoutMs: Math.max(1, Math.min(options.timeoutMs, remaining)),
|
|
88
|
+
outputDir: tempDir,
|
|
89
|
+
hash: `part-${index}`,
|
|
90
|
+
now: new Date(baseTime + index),
|
|
91
|
+
});
|
|
92
|
+
buffers.push(await readFile(part.filePath));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (Date.now() >= deadline) {
|
|
96
|
+
throw overallTimeout(totalTimeoutMs, index, chunks.length);
|
|
97
|
+
}
|
|
98
|
+
throw chunkFailure(index, chunks.length, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const bytes = Buffer.concat(buffers);
|
|
102
|
+
if (bytes.length === 0) {
|
|
103
|
+
throw new TtsError(ErrorCode.EmptyAudio, 'Chunked synthesis produced no audio bytes.', 'Retry the call; if it persists, run `tts_self_check`.');
|
|
104
|
+
}
|
|
105
|
+
const target = outputFilePath(options.outputDir, options.hash, options.now);
|
|
106
|
+
try {
|
|
107
|
+
await mkdir(dirname(target), { recursive: true });
|
|
108
|
+
const partial = `${target}.partial`;
|
|
109
|
+
await writeFile(partial, bytes);
|
|
110
|
+
await rename(partial, target);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw publishFailure(target, error);
|
|
114
|
+
}
|
|
115
|
+
return { filePath: target, format: AUDIO_FORMAT };
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=chunked-tts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunked-tts.js","sourceRoot":"","sources":["../src/chunked-tts.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text chunking for long synthesis requests.
|
|
3
|
+
*
|
|
4
|
+
* Splitting happens on code points, so a surrogate pair is never cut in half,
|
|
5
|
+
* and the cut point prefers sentence punctuation, then newlines, then softer
|
|
6
|
+
* separators such as commas and spaces. The module has no I/O and no
|
|
7
|
+
* subprocess dependency, which keeps it directly testable.
|
|
8
|
+
*/
|
|
9
|
+
/** Target characters per chunk; also the one-shot threshold. */
|
|
10
|
+
export declare const DEFAULT_CHUNK_SIZE = 1200;
|
|
11
|
+
/** Count Unicode code points rather than UTF-16 code units. */
|
|
12
|
+
export declare function countCharacters(text: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* Split `text` into chunks of at most `chunkSize` characters, cutting at the
|
|
15
|
+
* latest sentence boundary in each window when one exists. Whitespace at a
|
|
16
|
+
* chunk edge is trimmed; otherwise every character is preserved.
|
|
17
|
+
*/
|
|
18
|
+
export declare function splitText(text: string, chunkSize?: number): string[];
|
|
19
|
+
//# sourceMappingURL=chunker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunker.d.ts","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,OAAQ,CAAA;AAiBvC,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAeD;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAA2B,GACrC,MAAM,EAAE,CAgDV"}
|
package/dist/chunker.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure text chunking for long synthesis requests.
|
|
3
|
+
*
|
|
4
|
+
* Splitting happens on code points, so a surrogate pair is never cut in half,
|
|
5
|
+
* and the cut point prefers sentence punctuation, then newlines, then softer
|
|
6
|
+
* separators such as commas and spaces. The module has no I/O and no
|
|
7
|
+
* subprocess dependency, which keeps it directly testable.
|
|
8
|
+
*/
|
|
9
|
+
/** Target characters per chunk; also the one-shot threshold. */
|
|
10
|
+
export const DEFAULT_CHUNK_SIZE = 1_200;
|
|
11
|
+
/** A cut is only taken before this fraction of the window when possible. */
|
|
12
|
+
const MIN_FILL_RATIO = 0.5;
|
|
13
|
+
const HARD_SENTENCE = new Set(['。', '!', '?', ';', '!', '?', ';']);
|
|
14
|
+
const SOFT_BREAK = new Set([
|
|
15
|
+
',',
|
|
16
|
+
'、',
|
|
17
|
+
':',
|
|
18
|
+
',',
|
|
19
|
+
':',
|
|
20
|
+
' ',
|
|
21
|
+
'\t',
|
|
22
|
+
'\u3000',
|
|
23
|
+
]);
|
|
24
|
+
/** Count Unicode code points rather than UTF-16 code units. */
|
|
25
|
+
export function countCharacters(text) {
|
|
26
|
+
return Array.from(text).length;
|
|
27
|
+
}
|
|
28
|
+
function isSentenceBreak(chars, index) {
|
|
29
|
+
const char = chars[index];
|
|
30
|
+
if (char === undefined)
|
|
31
|
+
return false;
|
|
32
|
+
if (char === '\n' || HARD_SENTENCE.has(char))
|
|
33
|
+
return true;
|
|
34
|
+
// A full stop only ends a sentence when followed by whitespace or the end;
|
|
35
|
+
// this keeps `3.14` and file names together.
|
|
36
|
+
if (char === '.') {
|
|
37
|
+
const next = chars[index + 1];
|
|
38
|
+
return next === undefined || /\s/.test(next);
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Split `text` into chunks of at most `chunkSize` characters, cutting at the
|
|
44
|
+
* latest sentence boundary in each window when one exists. Whitespace at a
|
|
45
|
+
* chunk edge is trimmed; otherwise every character is preserved.
|
|
46
|
+
*/
|
|
47
|
+
export function splitText(text, chunkSize = DEFAULT_CHUNK_SIZE) {
|
|
48
|
+
if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
|
|
49
|
+
throw new RangeError(`chunkSize must be a positive integer, got ${chunkSize}`);
|
|
50
|
+
}
|
|
51
|
+
const chars = Array.from(text);
|
|
52
|
+
if (chars.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
if (chars.length <= chunkSize)
|
|
55
|
+
return [text];
|
|
56
|
+
const minFill = Math.max(1, Math.floor(chunkSize * MIN_FILL_RATIO));
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let start = 0;
|
|
59
|
+
while (start < chars.length) {
|
|
60
|
+
if (chars.length - start <= chunkSize) {
|
|
61
|
+
const tail = chars.slice(start).join('').trim();
|
|
62
|
+
if (tail.length > 0)
|
|
63
|
+
chunks.push(tail);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const limit = start + chunkSize;
|
|
67
|
+
let sentence = -1;
|
|
68
|
+
let soft = -1;
|
|
69
|
+
for (let index = start; index < limit; index += 1) {
|
|
70
|
+
if (isSentenceBreak(chars, index))
|
|
71
|
+
sentence = index + 1;
|
|
72
|
+
else if (SOFT_BREAK.has(chars[index] ?? ''))
|
|
73
|
+
soft = index + 1;
|
|
74
|
+
}
|
|
75
|
+
let cut;
|
|
76
|
+
if (sentence >= start + minFill) {
|
|
77
|
+
cut = sentence;
|
|
78
|
+
}
|
|
79
|
+
else if (soft >= start + minFill) {
|
|
80
|
+
cut = soft;
|
|
81
|
+
}
|
|
82
|
+
else if (sentence > start) {
|
|
83
|
+
cut = sentence;
|
|
84
|
+
}
|
|
85
|
+
else if (soft > start) {
|
|
86
|
+
cut = soft;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
cut = limit;
|
|
90
|
+
}
|
|
91
|
+
const piece = chars.slice(start, cut).join('').trim();
|
|
92
|
+
if (piece.length > 0)
|
|
93
|
+
chunks.push(piece);
|
|
94
|
+
start = cut;
|
|
95
|
+
}
|
|
96
|
+
return chunks;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=chunker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chunker.js","sourceRoot":"","sources":["../src/chunker.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half of the plugin: a keyed `tool.call.toolview` for `tts_generate`.
|
|
3
|
+
*
|
|
4
|
+
* The Host serves this file as the `exports["./client"]` bundle, so it must stay
|
|
5
|
+
* in the lazy-CJS module-loader format: executing the script only registers the
|
|
6
|
+
* factory, and the factory runs (with the shared `require`) on first import.
|
|
7
|
+
*
|
|
8
|
+
* `block` is either the running call node or the settled `ToolResultNode`. The
|
|
9
|
+
* result node exposes the model-facing `content` blocks and an optional host
|
|
10
|
+
* `meta`, not the structured tool return value, so the playback URL is taken
|
|
11
|
+
* from the rendered text that `tts_generate` always emits.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: '@leiboss/dsh-tts',
|
|
16
|
+
factory: (require) => {
|
|
17
|
+
var module = { exports: {} }
|
|
18
|
+
var exports = module.exports
|
|
19
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
20
|
+
|
|
21
|
+
const react = require('react')
|
|
22
|
+
const h = react.createElement
|
|
23
|
+
|
|
24
|
+
/** Same-origin playback path; stops at markup, quotes and whitespace. */
|
|
25
|
+
const AUDIO_URL = /\/dsh-tts\/audio\/[^\s)"'<>`\]]+/g
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Collect the playback URLs rendered into one tool result.
|
|
29
|
+
* @param {readonly { text?: string }[] | undefined} blocks
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
function extractAudioUrls(blocks) {
|
|
33
|
+
/** @type {string[]} */
|
|
34
|
+
const urls = []
|
|
35
|
+
for (const block of blocks ?? []) {
|
|
36
|
+
if (typeof block !== 'object' || block === null) continue
|
|
37
|
+
if (typeof block.text !== 'string') continue
|
|
38
|
+
const matches = block.text.match(AUDIO_URL)
|
|
39
|
+
if (matches === null) continue
|
|
40
|
+
for (const url of matches) if (!urls.includes(url)) urls.push(url)
|
|
41
|
+
}
|
|
42
|
+
return urls
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Inline player for the settled result; renders nothing while the call is
|
|
47
|
+
* still running or when no playback URL was produced (non-Web hosts,
|
|
48
|
+
* speechSynthesis fallback, route registration skipped).
|
|
49
|
+
* @param {{ block?: unknown }} props
|
|
50
|
+
*/
|
|
51
|
+
function TtsAudioView(props) {
|
|
52
|
+
const block = props?.block
|
|
53
|
+
if (typeof block !== 'object' || block === null) return null
|
|
54
|
+
if ('name' in block) return null
|
|
55
|
+
const urls = extractAudioUrls(
|
|
56
|
+
/** @type {{ content?: { text?: string }[] }} */ (block).content,
|
|
57
|
+
)
|
|
58
|
+
if (urls.length === 0) return null
|
|
59
|
+
return h(
|
|
60
|
+
'div',
|
|
61
|
+
{ style: { width: '100%' } },
|
|
62
|
+
urls.map((url) =>
|
|
63
|
+
h('audio', {
|
|
64
|
+
key: url,
|
|
65
|
+
controls: true,
|
|
66
|
+
preload: 'none',
|
|
67
|
+
src: url,
|
|
68
|
+
'aria-label': '播放',
|
|
69
|
+
style: { width: '100%' },
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function apply(ctx) {
|
|
76
|
+
ctx.slots.inject('tool.call.toolview', function* () {
|
|
77
|
+
yield ctx.slots.register(
|
|
78
|
+
{ name: 'tool.call.toolview', key: 'tts_generate' },
|
|
79
|
+
TtsAudioView,
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
exports.apply = apply
|
|
85
|
+
exports.inject = ['slots']
|
|
86
|
+
exports.extractAudioUrls = extractAudioUrls
|
|
87
|
+
exports.TtsAudioView = TtsAudioView
|
|
88
|
+
return module.exports
|
|
89
|
+
},
|
|
90
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Limits resolved from plugin config. */
|
|
2
|
+
export interface ConcurrencyLimits {
|
|
3
|
+
maxConcurrent: number;
|
|
4
|
+
queueSize: number;
|
|
5
|
+
queueTimeoutMs: number;
|
|
6
|
+
}
|
|
7
|
+
/** Observable capacity, surfaced in TTS_BUSY messages. */
|
|
8
|
+
export interface LimiterStats {
|
|
9
|
+
running: number;
|
|
10
|
+
queued: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Counting semaphore with a bounded FIFO wait queue. Every mutation happens
|
|
14
|
+
* synchronously, so slot hand-off cannot leak capacity between tasks.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ConcurrencyLimiter {
|
|
17
|
+
private running;
|
|
18
|
+
private readonly waiters;
|
|
19
|
+
private readonly limits;
|
|
20
|
+
constructor(limits: ConcurrencyLimits);
|
|
21
|
+
/** Current capacity snapshot. */
|
|
22
|
+
get stats(): LimiterStats;
|
|
23
|
+
/** Run `task` in a slot, waiting for capacity up to `queueTimeoutMs`. */
|
|
24
|
+
run<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
25
|
+
private acquire;
|
|
26
|
+
/** Remove a waiter and release its timer/listener; safe to call twice. */
|
|
27
|
+
private remove;
|
|
28
|
+
/** Free a slot and hand it directly to the oldest waiter. */
|
|
29
|
+
private release;
|
|
30
|
+
/** Structured capacity error; never includes call arguments. */
|
|
31
|
+
private busy;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=concurrency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAUA,0CAA0C;AAC1C,MAAM,WAAW,iBAAiB;IAChC,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,0DAA0D;AAC1D,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAUD;;;GAGG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;gBAE9B,MAAM,EAAE,iBAAiB;IAIrC,iCAAiC;IACjC,IAAI,KAAK,IAAI,YAAY,CAExB;IAED,yEAAyE;IACnE,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC;IAStE,OAAO,CAAC,OAAO;IA8Cf,0EAA0E;IAC1E,OAAO,CAAC,MAAM;IASd,6DAA6D;IAC7D,OAAO,CAAC,OAAO;IAYf,gEAAgE;IAChE,OAAO,CAAC,IAAI;CAQb"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight bulkhead for synthesis tasks.
|
|
3
|
+
*
|
|
4
|
+
* A slot wraps one complete request: cache lookup, chunked serial synthesis,
|
|
5
|
+
* and any fallback engine. Chunks share their caller's slot, so a long text
|
|
6
|
+
* never consumes more than one slot. Cache hits are filtered before `run`, so
|
|
7
|
+
* they never queue.
|
|
8
|
+
*/
|
|
9
|
+
import { ErrorCode, TtsError } from "./errors.js";
|
|
10
|
+
/**
|
|
11
|
+
* Counting semaphore with a bounded FIFO wait queue. Every mutation happens
|
|
12
|
+
* synchronously, so slot hand-off cannot leak capacity between tasks.
|
|
13
|
+
*/
|
|
14
|
+
export class ConcurrencyLimiter {
|
|
15
|
+
running = 0;
|
|
16
|
+
waiters = [];
|
|
17
|
+
limits;
|
|
18
|
+
constructor(limits) {
|
|
19
|
+
this.limits = limits;
|
|
20
|
+
}
|
|
21
|
+
/** Current capacity snapshot. */
|
|
22
|
+
get stats() {
|
|
23
|
+
return { running: this.running, queued: this.waiters.length };
|
|
24
|
+
}
|
|
25
|
+
/** Run `task` in a slot, waiting for capacity up to `queueTimeoutMs`. */
|
|
26
|
+
async run(task, signal) {
|
|
27
|
+
await this.acquire(signal);
|
|
28
|
+
try {
|
|
29
|
+
return await task();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
this.release();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
acquire(signal) {
|
|
36
|
+
if (signal?.aborted) {
|
|
37
|
+
return Promise.reject(new TtsError(ErrorCode.Aborted, 'Synthesis was aborted before it started.'));
|
|
38
|
+
}
|
|
39
|
+
if (this.running < this.limits.maxConcurrent) {
|
|
40
|
+
this.running += 1;
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
if (this.waiters.length >= this.limits.queueSize) {
|
|
44
|
+
return Promise.reject(this.busy('the wait queue is full'));
|
|
45
|
+
}
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const waiter = {
|
|
48
|
+
resolve,
|
|
49
|
+
reject,
|
|
50
|
+
timer: setTimeout(() => {
|
|
51
|
+
this.remove(waiter);
|
|
52
|
+
reject(this.busy('the queue wait timed out'));
|
|
53
|
+
}, this.limits.queueTimeoutMs),
|
|
54
|
+
};
|
|
55
|
+
if (signal) {
|
|
56
|
+
waiter.signal = signal;
|
|
57
|
+
waiter.onAbort = () => {
|
|
58
|
+
this.remove(waiter);
|
|
59
|
+
reject(new TtsError(ErrorCode.Aborted, 'Synthesis was aborted while waiting for a slot.'));
|
|
60
|
+
};
|
|
61
|
+
signal.addEventListener('abort', waiter.onAbort, { once: true });
|
|
62
|
+
}
|
|
63
|
+
this.waiters.push(waiter);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Remove a waiter and release its timer/listener; safe to call twice. */
|
|
67
|
+
remove(waiter) {
|
|
68
|
+
clearTimeout(waiter.timer);
|
|
69
|
+
if (waiter.onAbort) {
|
|
70
|
+
waiter.signal?.removeEventListener('abort', waiter.onAbort);
|
|
71
|
+
}
|
|
72
|
+
const index = this.waiters.indexOf(waiter);
|
|
73
|
+
if (index !== -1)
|
|
74
|
+
this.waiters.splice(index, 1);
|
|
75
|
+
}
|
|
76
|
+
/** Free a slot and hand it directly to the oldest waiter. */
|
|
77
|
+
release() {
|
|
78
|
+
this.running -= 1;
|
|
79
|
+
const next = this.waiters.shift();
|
|
80
|
+
if (!next)
|
|
81
|
+
return;
|
|
82
|
+
clearTimeout(next.timer);
|
|
83
|
+
if (next.onAbort) {
|
|
84
|
+
next.signal?.removeEventListener('abort', next.onAbort);
|
|
85
|
+
}
|
|
86
|
+
this.running += 1;
|
|
87
|
+
next.resolve();
|
|
88
|
+
}
|
|
89
|
+
/** Structured capacity error; never includes call arguments. */
|
|
90
|
+
busy(detail) {
|
|
91
|
+
const { running, queued } = this.stats;
|
|
92
|
+
return new TtsError(ErrorCode.TtsBusy, `TTS synthesis is busy: running=${running}, queued=${queued}, maxConcurrent=${this.limits.maxConcurrent}, queueSize=${this.limits.queueSize} (${detail}).`, 'Retry after the current syntheses finish, or raise `maxConcurrent` / `queueSize` in the plugin config.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAuBjD;;;GAGG;AACH,MAAM,OAAO,kBAAkB;IACrB,OAAO,GAAG,CAAC,CAAA;IACF,OAAO,GAAa,EAAE,CAAA;IAEtB,MAAM,CAAmB;IAE1C,YAAY,MAAyB;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;IAED,iCAAiC;IACjC,IAAI,KAAK;QACP,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAA;IAC/D,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,GAAG,CAAI,IAAsB,EAAE,MAAoB;QACvD,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAC1B,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,EAAE,CAAA;QACrB,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,OAAO,EAAE,CAAA;QAChB,CAAC;IACH,CAAC;IAEO,OAAO,CAAC,MAAoB;QAClC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,QAAQ,CAAC,SAAS,CAAC,OAAO,EAAE,0CAA0C,CAAC,CAC5E,CAAA;QACH,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YAC7C,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;YACjB,OAAO,OAAO,CAAC,OAAO,EAAE,CAAA;QAC1B,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACjD,OAAO,OAAO,CAAC,MAAM,CACnB,IAAI,CAAC,IAAI,CAAC,wBAAwB,CAAC,CACpC,CAAA;QACH,CAAC;QAED,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC3C,MAAM,MAAM,GAAW;gBACrB,OAAO;gBACP,MAAM;gBACN,KAAK,EAAE,UAAU,CAAC,GAAG,EAAE;oBACrB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;oBACnB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC,CAAA;gBAC/C,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;aAC/B,CAAA;YAED,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,MAAM,GAAG,MAAM,CAAA;gBACtB,MAAM,CAAC,OAAO,GAAG,GAAG,EAAE;oBACpB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;oBACnB,MAAM,CACJ,IAAI,QAAQ,CACV,SAAS,CAAC,OAAO,EACjB,iDAAiD,CAClD,CACF,CAAA;gBACH,CAAC,CAAA;gBACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YAClE,CAAC;YAED,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC3B,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,0EAA0E;IAClE,MAAM,CAAC,MAAc;QAC3B,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC1B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAA;QAC7D,CAAC;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QAC1C,IAAI,KAAK,KAAK,CAAC,CAAC;YAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IACjD,CAAC;IAED,6DAA6D;IACrD,OAAO;QACb,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;QACjB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;QACjC,IAAI,CAAC,IAAI;YAAE,OAAM;QACjB,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACxB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,CAAA;QACzD,CAAC;QACD,IAAI,CAAC,OAAO,IAAI,CAAC,CAAA;QACjB,IAAI,CAAC,OAAO,EAAE,CAAA;IAChB,CAAC;IAED,gEAAgE;IACxD,IAAI,CAAC,MAAc;QACzB,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;QACtC,OAAO,IAAI,QAAQ,CACjB,SAAS,CAAC,OAAO,EACjB,kCAAkC,OAAO,YAAY,MAAM,mBAAmB,IAAI,CAAC,MAAM,CAAC,aAAa,eAAe,IAAI,CAAC,MAAM,CAAC,SAAS,KAAK,MAAM,IAAI,EAC1J,wGAAwG,CACzG,CAAA;IACH,CAAC;CACF"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
/** Voice Edge-TTS uses when neither config nor the call specifies one. */
|
|
3
|
+
export declare const DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural";
|
|
4
|
+
/** Rate Edge-TTS uses when the call omits one. */
|
|
5
|
+
export declare const DEFAULT_RATE = "+0%";
|
|
6
|
+
/** Per-synthesis subprocess deadline. */
|
|
7
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
8
|
+
/** CLI name resolved through PATH unless overridden. */
|
|
9
|
+
export declare const DEFAULT_EDGE_TTS_BINARY = "edge-tts";
|
|
10
|
+
/** Subdirectory of `DSH_HOME` that owns every generated audio file. */
|
|
11
|
+
export declare const OUTPUT_SUBDIR = "tts-output";
|
|
12
|
+
/** Target characters per chunk, and the one-shot threshold. */
|
|
13
|
+
export declare const DEFAULT_CHUNK_SIZE = 1200;
|
|
14
|
+
/** Long texts are chunked by default. */
|
|
15
|
+
export declare const DEFAULT_CHUNKING_ENABLED = true;
|
|
16
|
+
/** Engines the fallback chain understands, in default priority order. */
|
|
17
|
+
export declare const DEFAULT_FALLBACK_ORDER: readonly ["edge-tts", "sapi", "speechSynthesis"];
|
|
18
|
+
/** Fallback is enabled unless configured otherwise. */
|
|
19
|
+
export declare const DEFAULT_FALLBACK_ENABLED = true;
|
|
20
|
+
/** Syntheses allowed to run at the same time. */
|
|
21
|
+
export declare const DEFAULT_MAX_CONCURRENT = 2;
|
|
22
|
+
/** Callers allowed to wait for a synthesis slot. */
|
|
23
|
+
export declare const DEFAULT_QUEUE_SIZE = 4;
|
|
24
|
+
/** How long a caller may wait for a slot before TTS_BUSY. */
|
|
25
|
+
export declare const DEFAULT_QUEUE_TIMEOUT_MS = 10000;
|
|
26
|
+
/** Upper bound for one complete `tts_generate` call, across all chunks. */
|
|
27
|
+
export declare const DEFAULT_TOTAL_TIMEOUT_MS = 180000;
|
|
28
|
+
/** Extra Host names accepted by the audio route (reverse proxies). */
|
|
29
|
+
export declare const DEFAULT_TRUSTED_HOSTS: readonly string[];
|
|
30
|
+
/** One engine in the fallback chain. */
|
|
31
|
+
export type TtsEngine = (typeof DEFAULT_FALLBACK_ORDER)[number];
|
|
32
|
+
/** Narrow an arbitrary config string to a known engine. */
|
|
33
|
+
export declare function isTtsEngine(value: string): value is TtsEngine;
|
|
34
|
+
/**
|
|
35
|
+
* Config schema exposed to the DSH configuration layer. Defaults live here (not
|
|
36
|
+
* in call sites) so the settings surface and runtime behaviour agree.
|
|
37
|
+
*/
|
|
38
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
39
|
+
outputDir: z<string, string, "plain">;
|
|
40
|
+
defaultVoice: z<string, string, "defined">;
|
|
41
|
+
timeoutMs: z<number, number, "defined">;
|
|
42
|
+
edgeTtsBinary: z<string, string, "defined">;
|
|
43
|
+
chunkSize: z<number, number, "defined">;
|
|
44
|
+
chunkingEnabled: z<boolean, boolean, "defined">;
|
|
45
|
+
fallbackEnabled: z<boolean, boolean, "defined">;
|
|
46
|
+
fallbackOrder: z<string[], string[], "defined">;
|
|
47
|
+
maxConcurrent: z<number, number, "defined">;
|
|
48
|
+
queueSize: z<number, number, "defined">;
|
|
49
|
+
queueTimeoutMs: z<number, number, "defined">;
|
|
50
|
+
totalTimeoutMs: z<number, number, "defined">;
|
|
51
|
+
trustedHosts: z<string[], string[], "defined">;
|
|
52
|
+
proxy: z<string, string, "plain">;
|
|
53
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
54
|
+
outputDir: z<string, string, "plain">;
|
|
55
|
+
defaultVoice: z<string, string, "defined">;
|
|
56
|
+
timeoutMs: z<number, number, "defined">;
|
|
57
|
+
edgeTtsBinary: z<string, string, "defined">;
|
|
58
|
+
chunkSize: z<number, number, "defined">;
|
|
59
|
+
chunkingEnabled: z<boolean, boolean, "defined">;
|
|
60
|
+
fallbackEnabled: z<boolean, boolean, "defined">;
|
|
61
|
+
fallbackOrder: z<string[], string[], "defined">;
|
|
62
|
+
maxConcurrent: z<number, number, "defined">;
|
|
63
|
+
queueSize: z<number, number, "defined">;
|
|
64
|
+
queueTimeoutMs: z<number, number, "defined">;
|
|
65
|
+
totalTimeoutMs: z<number, number, "defined">;
|
|
66
|
+
trustedHosts: z<string[], string[], "defined">;
|
|
67
|
+
proxy: z<string, string, "plain">;
|
|
68
|
+
}>>, "plain">;
|
|
69
|
+
/** Config accepted by {@link apply}; every field is optional and defaulted. */
|
|
70
|
+
export interface TtsConfig {
|
|
71
|
+
outputDir?: string;
|
|
72
|
+
defaultVoice?: string;
|
|
73
|
+
timeoutMs?: number;
|
|
74
|
+
edgeTtsBinary?: string;
|
|
75
|
+
chunkSize?: number;
|
|
76
|
+
chunkingEnabled?: boolean;
|
|
77
|
+
fallbackEnabled?: boolean;
|
|
78
|
+
fallbackOrder?: string[];
|
|
79
|
+
maxConcurrent?: number;
|
|
80
|
+
queueSize?: number;
|
|
81
|
+
queueTimeoutMs?: number;
|
|
82
|
+
totalTimeoutMs?: number;
|
|
83
|
+
trustedHosts?: string[];
|
|
84
|
+
/** HTTP proxy forwarded to edge-tts as `--proxy`; no env fallback. */
|
|
85
|
+
proxy?: string;
|
|
86
|
+
}
|
|
87
|
+
/** Effective, path-resolved plugin settings. */
|
|
88
|
+
export interface ResolvedConfig {
|
|
89
|
+
/** Harness home from `DSH_HOME`; empty when unset. */
|
|
90
|
+
dshHome: string;
|
|
91
|
+
/** Absolute output root; falls back to `~/.dsh/tts-output`. */
|
|
92
|
+
outputDir: string;
|
|
93
|
+
defaultVoice: string;
|
|
94
|
+
timeoutMs: number;
|
|
95
|
+
edgeTtsBinary: string;
|
|
96
|
+
chunkSize: number;
|
|
97
|
+
chunkingEnabled: boolean;
|
|
98
|
+
fallbackEnabled: boolean;
|
|
99
|
+
/** Unknown engine names are dropped; order is preserved. */
|
|
100
|
+
fallbackOrder: TtsEngine[];
|
|
101
|
+
maxConcurrent: number;
|
|
102
|
+
queueSize: number;
|
|
103
|
+
queueTimeoutMs: number;
|
|
104
|
+
totalTimeoutMs: number;
|
|
105
|
+
trustedHosts: string[];
|
|
106
|
+
/** Undefined when unset; deliberately never read from the environment. */
|
|
107
|
+
proxy?: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve config defaults without touching the filesystem. A missing
|
|
111
|
+
* `DSH_HOME` is not fatal here: explicit `outputDir` still works, and the
|
|
112
|
+
* self-check needs to report the missing value instead of failing to load.
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolveConfig(raw?: Partial<TtsConfig>, env?: NodeJS.ProcessEnv): ResolvedConfig;
|
|
115
|
+
/**
|
|
116
|
+
* Validate the settings a synthesis actually needs. Kept separate from
|
|
117
|
+
* {@link resolveConfig} so loading the plugin never fails at mount time.
|
|
118
|
+
*/
|
|
119
|
+
export declare function requireSynthesisConfig(config: ResolvedConfig): {
|
|
120
|
+
outputDir: string;
|
|
121
|
+
timeoutMs: number;
|
|
122
|
+
chunkSize: number;
|
|
123
|
+
chunkingEnabled: boolean;
|
|
124
|
+
fallbackEnabled: boolean;
|
|
125
|
+
fallbackOrder: TtsEngine[];
|
|
126
|
+
maxConcurrent: number;
|
|
127
|
+
queueSize: number;
|
|
128
|
+
queueTimeoutMs: number;
|
|
129
|
+
totalTimeoutMs: number;
|
|
130
|
+
trustedHosts: string[];
|
|
131
|
+
proxy?: string;
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|