@laitszkin/apollo-toolkit 4.1.3 → 5.0.0
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 +45 -0
- package/bin/apollo-toolkit.ts +4 -0
- package/dist/bin/apollo-toolkit.js +4 -0
- package/package.json +7 -2
- package/packages/cli/dist/help-text-builder.d.ts +23 -0
- package/packages/cli/dist/help-text-builder.js +166 -0
- package/packages/cli/dist/index.d.ts +6 -17
- package/packages/cli/dist/index.js +52 -246
- package/packages/cli/dist/installer.d.ts +1 -0
- package/packages/cli/dist/installer.js +20 -7
- package/packages/cli/dist/parsers/install-parser.d.ts +15 -0
- package/packages/cli/dist/parsers/install-parser.js +87 -0
- package/packages/cli/dist/parsers/parser-utils.d.ts +9 -0
- package/packages/cli/dist/parsers/parser-utils.js +16 -0
- package/packages/cli/dist/parsers/tool-parser.d.ts +16 -0
- package/packages/cli/dist/parsers/tool-parser.js +58 -0
- package/packages/cli/dist/parsers/types.d.ts +50 -0
- package/packages/cli/dist/parsers/types.js +1 -0
- package/packages/cli/dist/parsers/uninstall-parser.d.ts +15 -0
- package/packages/cli/dist/parsers/uninstall-parser.js +67 -0
- package/packages/cli/dist/tool-registration.d.ts +2 -0
- package/packages/cli/dist/tool-registration.js +2 -0
- package/packages/cli/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/cli/dist/types.d.ts +3 -1
- package/packages/cli/dist/updater.js +11 -5
- package/packages/cli/help-text-builder.ts +180 -0
- package/packages/cli/index.ts +59 -251
- package/packages/cli/installer.ts +19 -7
- package/packages/cli/package.json +6 -3
- package/packages/cli/parsers/install-parser.ts +94 -0
- package/packages/cli/parsers/parser-utils.ts +17 -0
- package/packages/cli/parsers/tool-parser.ts +65 -0
- package/packages/cli/parsers/types.ts +56 -0
- package/packages/cli/parsers/uninstall-parser.ts +75 -0
- package/packages/cli/tool-registration.ts +3 -0
- package/packages/cli/types.ts +6 -1
- package/packages/cli/updater.ts +11 -5
- package/packages/tool-registry/dist/registry.js +3 -4
- package/packages/tool-registry/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tool-registry/dist/types.d.ts +2 -9
- package/packages/tool-registry/package.json +3 -3
- package/packages/tool-registry/registry.ts +3 -4
- package/packages/tool-registry/tsconfig.json +6 -2
- package/packages/tool-registry/types.ts +3 -9
- package/packages/tool-utils/app-error.ts +97 -0
- package/packages/tool-utils/dist/app-error.d.ts +49 -0
- package/packages/tool-utils/dist/app-error.js +80 -0
- package/packages/tool-utils/dist/index.d.ts +5 -0
- package/packages/tool-utils/dist/index.js +3 -0
- package/packages/tool-utils/dist/platform-adapter.d.ts +48 -0
- package/packages/tool-utils/dist/platform-adapter.js +73 -0
- package/packages/tool-utils/dist/schema.d.ts +68 -0
- package/packages/tool-utils/dist/schema.js +67 -0
- package/packages/tool-utils/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tool-utils/index.ts +12 -0
- package/packages/tool-utils/package.json +3 -3
- package/packages/tool-utils/platform-adapter.ts +112 -0
- package/packages/tool-utils/schema.ts +122 -0
- package/packages/tools/architecture/dist/index.d.ts +13 -0
- package/packages/tools/architecture/dist/index.js +55 -57
- package/packages/tools/architecture/dist/index.test.js +17 -4
- package/packages/tools/architecture/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/architecture/index.test.ts +27 -14
- package/packages/tools/architecture/index.ts +85 -88
- package/packages/tools/architecture/package.json +3 -3
- package/packages/tools/codegraph/dist/index.js +21 -17
- package/packages/tools/codegraph/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/codegraph/index.ts +21 -17
- package/packages/tools/codegraph/package.json +3 -3
- package/packages/tools/create-review-report/dist/index.d.ts +1 -2
- package/packages/tools/create-review-report/dist/index.js +46 -77
- package/packages/tools/create-review-report/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/create-review-report/index.ts +52 -81
- package/packages/tools/create-review-report/package.json +3 -3
- package/packages/tools/create-specs/dist/index.d.ts +1 -2
- package/packages/tools/create-specs/dist/index.js +70 -123
- package/packages/tools/create-specs/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/create-specs/index.ts +82 -128
- package/packages/tools/create-specs/package.json +3 -3
- package/packages/tools/docs-to-voice/dist/index.d.ts +1 -2
- package/packages/tools/docs-to-voice/dist/index.js +116 -219
- package/packages/tools/docs-to-voice/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/docs-to-voice/index.ts +265 -385
- package/packages/tools/docs-to-voice/package.json +3 -3
- package/packages/tools/enforce-video-aspect-ratio/dist/index.d.ts +1 -2
- package/packages/tools/enforce-video-aspect-ratio/dist/index.js +77 -154
- package/packages/tools/enforce-video-aspect-ratio/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/enforce-video-aspect-ratio/index.ts +87 -172
- package/packages/tools/enforce-video-aspect-ratio/package.json +3 -3
- package/packages/tools/eval/dist/index.js +7 -0
- package/packages/tools/eval/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/eval/index.ts +8 -0
- package/packages/tools/eval/package.json +3 -3
- package/packages/tools/extract-conversations/dist/index.d.ts +1 -2
- package/packages/tools/extract-conversations/dist/index.js +31 -29
- package/packages/tools/extract-conversations/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/extract-conversations/index.ts +37 -30
- package/packages/tools/extract-conversations/package.json +3 -3
- package/packages/tools/extract-pdf-text/dist/index.d.ts +1 -2
- package/packages/tools/extract-pdf-text/dist/index.js +44 -65
- package/packages/tools/extract-pdf-text/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/extract-pdf-text/index.ts +55 -74
- package/packages/tools/extract-pdf-text/package.json +3 -3
- package/packages/tools/filter-logs/dist/index.js +60 -84
- package/packages/tools/filter-logs/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/filter-logs/index.ts +67 -97
- package/packages/tools/filter-logs/package.json +3 -3
- package/packages/tools/find-github-issues/dist/index.d.ts +10 -0
- package/packages/tools/find-github-issues/dist/index.js +34 -5
- package/packages/tools/find-github-issues/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/find-github-issues/index.ts +37 -5
- package/packages/tools/find-github-issues/package.json +3 -3
- package/packages/tools/generate-storyboard-images/dist/index.d.ts +1 -2
- package/packages/tools/generate-storyboard-images/dist/index.js +98 -173
- package/packages/tools/generate-storyboard-images/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/generate-storyboard-images/index.ts +100 -188
- package/packages/tools/generate-storyboard-images/package.json +3 -3
- package/packages/tools/open-github-issue/dist/index.d.ts +13 -0
- package/packages/tools/open-github-issue/dist/index.js +67 -68
- package/packages/tools/open-github-issue/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/open-github-issue/index.ts +71 -72
- package/packages/tools/open-github-issue/package.json +3 -3
- package/packages/tools/read-github-issue/dist/index.d.ts +16 -1
- package/packages/tools/read-github-issue/dist/index.js +32 -40
- package/packages/tools/read-github-issue/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/read-github-issue/index.ts +32 -45
- package/packages/tools/read-github-issue/package.json +3 -3
- package/packages/tools/render-error-book/dist/index.d.ts +1 -2
- package/packages/tools/render-error-book/dist/index.js +74 -95
- package/packages/tools/render-error-book/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/render-error-book/index.ts +88 -103
- package/packages/tools/render-error-book/package.json +3 -3
- package/packages/tools/render-katex/dist/index.d.ts +1 -2
- package/packages/tools/render-katex/dist/index.js +70 -157
- package/packages/tools/render-katex/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/render-katex/index.ts +138 -222
- package/packages/tools/render-katex/package.json +3 -3
- package/packages/tools/review-threads/dist/index.d.ts +12 -0
- package/packages/tools/review-threads/dist/index.js +83 -86
- package/packages/tools/review-threads/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/review-threads/index.ts +90 -84
- package/packages/tools/review-threads/package.json +3 -3
- package/packages/tools/search-logs/dist/index.js +100 -136
- package/packages/tools/search-logs/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/search-logs/index.ts +113 -145
- package/packages/tools/search-logs/package.json +3 -3
- package/packages/tools/sync-memory-index/dist/index.js +34 -28
- package/packages/tools/sync-memory-index/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/sync-memory-index/index.ts +37 -28
- package/packages/tools/sync-memory-index/package.json +3 -3
- package/packages/tools/validate-openai-agent-config/dist/index.js +13 -7
- package/packages/tools/validate-openai-agent-config/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/validate-openai-agent-config/index.ts +13 -7
- package/packages/tools/validate-openai-agent-config/package.json +3 -3
- package/packages/tools/validate-skill-frontmatter/dist/index.js +12 -6
- package/packages/tools/validate-skill-frontmatter/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tools/validate-skill-frontmatter/index.ts +12 -6
- package/packages/tools/validate-skill-frontmatter/package.json +3 -3
- package/packages/tui/dist/index.d.ts +2 -1
- package/packages/tui/dist/index.js +1 -0
- package/packages/tui/dist/stdio-adapter.d.ts +36 -0
- package/packages/tui/dist/stdio-adapter.js +69 -0
- package/packages/tui/dist/terminal.js +3 -1
- package/packages/tui/dist/tsconfig.tsbuildinfo +1 -1
- package/packages/tui/dist/types.d.ts +17 -0
- package/packages/tui/index.ts +2 -1
- package/packages/tui/package.json +6 -5
- package/packages/tui/stdio-adapter.ts +85 -0
- package/packages/tui/terminal.ts +3 -1
- package/packages/tui/tsconfig.json +5 -2
- package/packages/tui/types.ts +19 -0
- package/resources/project-architecture/assets/architecture.css +2 -1
- package/resources/project-architecture/atlas/atlas.history.log +1 -0
- package/resources/project-architecture/atlas/atlas.history.undo.json +13 -2
- package/resources/project-architecture/atlas/atlas.history.undo.stack.json +610 -0
- package/resources/project-architecture/atlas/atlas.index.yaml +81 -5
- package/resources/project-architecture/atlas/features/cli-dispatch.yaml +43 -0
- package/resources/project-architecture/atlas/features/terminal-ui.yaml +29 -0
- package/resources/project-architecture/atlas/features/tool-registry.yaml +22 -0
- package/resources/project-architecture/atlas/features/tool-utils.yaml +22 -0
- package/resources/project-architecture/features/cli-dispatch/arg-parser.html +40 -0
- package/resources/project-architecture/features/cli-dispatch/help-builder.html +40 -0
- package/resources/project-architecture/features/cli-dispatch/index.html +64 -0
- package/resources/project-architecture/features/cli-dispatch/installer-core.html +40 -0
- package/resources/project-architecture/features/cli-dispatch/tool-discovery.html +40 -0
- package/resources/project-architecture/features/cli-dispatch/update-checker.html +40 -0
- package/resources/project-architecture/features/terminal-ui/banner-display.html +40 -0
- package/resources/project-architecture/features/terminal-ui/index.html +50 -0
- package/resources/project-architecture/features/terminal-ui/interactive-prompts.html +40 -0
- package/resources/project-architecture/features/terminal-ui/terminal-detection.html +40 -0
- package/resources/project-architecture/features/tool-registry/formatter.html +40 -0
- package/resources/project-architecture/features/tool-registry/index.html +43 -0
- package/resources/project-architecture/features/tool-registry/registry-core.html +40 -0
- package/resources/project-architecture/features/tool-utils/index.html +43 -0
- package/resources/project-architecture/features/tool-utils/log-utils.html +40 -0
- package/resources/project-architecture/features/tool-utils/skill-discovery.html +40 -0
- package/resources/project-architecture/index.html +365 -121
- package/scripts/rewrite-imports.mjs +2 -2
- package/scripts/test.sh +144 -8
- package/skills/design/SKILL.md +57 -64
- package/skills/design/assets/templates/DESIGN.md +12 -0
- package/skills/design/references/code-smells.md +94 -0
- package/skills/design/references/module-boundary-adjustment.md +126 -0
- package/skills/design/references/module-internal-restructuring.md +132 -0
- package/skills/design/references/module-internal-simplification.md +164 -0
|
@@ -4,32 +4,13 @@ import path from 'node:path';
|
|
|
4
4
|
import https from 'node:https';
|
|
5
5
|
import http from 'node:http';
|
|
6
6
|
import type { ToolDefinition, ToolContext } from '@laitszkin/tool-registry';
|
|
7
|
+
import { SystemError, UserInputError, createToolRunner } from '@laitszkin/tool-utils';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_API_ENDPOINT =
|
|
9
10
|
'https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation';
|
|
10
11
|
const DEFAULT_API_MODEL = 'qwen3-tts';
|
|
11
12
|
const DEFAULT_API_VOICE = 'Cherry';
|
|
12
13
|
|
|
13
|
-
interface DocsToVoiceArgs {
|
|
14
|
-
inputText: string | null;
|
|
15
|
-
inputFile: string | null;
|
|
16
|
-
projectDir: string;
|
|
17
|
-
projectName: string | null;
|
|
18
|
-
outputName: string | null;
|
|
19
|
-
mode: string;
|
|
20
|
-
voice: string | null;
|
|
21
|
-
rate: string | null;
|
|
22
|
-
speechRate: string | null;
|
|
23
|
-
apiEndpoint: string;
|
|
24
|
-
apiModel: string;
|
|
25
|
-
apiVoice: string;
|
|
26
|
-
apiKey: string | null;
|
|
27
|
-
maxChars: string | null;
|
|
28
|
-
noAutoProsody: boolean;
|
|
29
|
-
force: boolean;
|
|
30
|
-
help: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
14
|
interface TimelineEntry {
|
|
34
15
|
index: number;
|
|
35
16
|
text: string;
|
|
@@ -39,113 +20,15 @@ interface TimelineEntry {
|
|
|
39
20
|
endMs: number;
|
|
40
21
|
}
|
|
41
22
|
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
inputFile: null,
|
|
46
|
-
projectDir: '.',
|
|
47
|
-
projectName: null,
|
|
48
|
-
outputName: null,
|
|
49
|
-
mode: 'say',
|
|
50
|
-
voice: null,
|
|
51
|
-
rate: null,
|
|
52
|
-
speechRate: null,
|
|
53
|
-
apiEndpoint: DEFAULT_API_ENDPOINT,
|
|
54
|
-
apiModel: DEFAULT_API_MODEL,
|
|
55
|
-
apiVoice: DEFAULT_API_VOICE,
|
|
56
|
-
apiKey: null,
|
|
57
|
-
maxChars: null,
|
|
58
|
-
noAutoProsody: false,
|
|
59
|
-
force: false,
|
|
60
|
-
help: false,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
for (let i = 0; i < args.length; i++) {
|
|
64
|
-
const arg = args[i];
|
|
65
|
-
if (arg === '--help' || arg === '-h') {
|
|
66
|
-
parsed.help = true;
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
if (arg.startsWith('--')) {
|
|
70
|
-
const eqIndex = arg.indexOf('=');
|
|
71
|
-
let key: string;
|
|
72
|
-
let value: string;
|
|
73
|
-
|
|
74
|
-
if (eqIndex !== -1) {
|
|
75
|
-
key = arg.slice(2, eqIndex);
|
|
76
|
-
value = arg.slice(eqIndex + 1);
|
|
77
|
-
} else {
|
|
78
|
-
key = arg.slice(2);
|
|
79
|
-
value = args[++i] || '';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
switch (key) {
|
|
83
|
-
case 'text':
|
|
84
|
-
parsed.inputText = value;
|
|
85
|
-
break;
|
|
86
|
-
case 'input':
|
|
87
|
-
case 'input-file':
|
|
88
|
-
parsed.inputFile = value;
|
|
89
|
-
break;
|
|
90
|
-
case 'project-dir':
|
|
91
|
-
parsed.projectDir = value;
|
|
92
|
-
break;
|
|
93
|
-
case 'project-name':
|
|
94
|
-
parsed.projectName = value;
|
|
95
|
-
break;
|
|
96
|
-
case 'output-name':
|
|
97
|
-
parsed.outputName = value;
|
|
98
|
-
break;
|
|
99
|
-
case 'engine':
|
|
100
|
-
case 'mode':
|
|
101
|
-
parsed.mode = value.toLowerCase();
|
|
102
|
-
break;
|
|
103
|
-
case 'voice':
|
|
104
|
-
parsed.voice = value;
|
|
105
|
-
break;
|
|
106
|
-
case 'rate':
|
|
107
|
-
parsed.rate = value;
|
|
108
|
-
break;
|
|
109
|
-
case 'speech-rate':
|
|
110
|
-
parsed.speechRate = value;
|
|
111
|
-
break;
|
|
112
|
-
case 'api-endpoint':
|
|
113
|
-
parsed.apiEndpoint = value;
|
|
114
|
-
break;
|
|
115
|
-
case 'api-model':
|
|
116
|
-
parsed.apiModel = value;
|
|
117
|
-
break;
|
|
118
|
-
case 'api-voice':
|
|
119
|
-
parsed.apiVoice = value;
|
|
120
|
-
break;
|
|
121
|
-
case 'api-key':
|
|
122
|
-
parsed.apiKey = value;
|
|
123
|
-
break;
|
|
124
|
-
case 'max-chars':
|
|
125
|
-
parsed.maxChars = value;
|
|
126
|
-
break;
|
|
127
|
-
case 'no-auto-prosody':
|
|
128
|
-
parsed.noAutoProsody = true;
|
|
129
|
-
break;
|
|
130
|
-
case 'force':
|
|
131
|
-
parsed.force = true;
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return parsed;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function readInputText(opts: DocsToVoiceArgs): string {
|
|
141
|
-
if (opts.inputFile) {
|
|
142
|
-
const inputPath = path.resolve(opts.inputFile);
|
|
23
|
+
function readInputText(inputFile: string | null, inputText: string | null): string {
|
|
24
|
+
if (inputFile) {
|
|
25
|
+
const inputPath = path.resolve(inputFile);
|
|
143
26
|
if (!fs.existsSync(inputPath)) {
|
|
144
|
-
throw new
|
|
27
|
+
throw new UserInputError(`Input file not found: ${inputPath}`);
|
|
145
28
|
}
|
|
146
29
|
return fs.readFileSync(inputPath, 'utf-8');
|
|
147
30
|
}
|
|
148
|
-
return
|
|
31
|
+
return inputText || '';
|
|
149
32
|
}
|
|
150
33
|
|
|
151
34
|
function splitSentences(rawText: string): string[] {
|
|
@@ -349,8 +232,10 @@ function applySpeechRateToAudio(outputPath: string, speechRate: number): void {
|
|
|
349
232
|
}
|
|
350
233
|
} catch (err: unknown) {
|
|
351
234
|
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
352
|
-
throw new
|
|
235
|
+
throw new SystemError(
|
|
353
236
|
`ffmpeg failed while applying --speech-rate: ${err instanceof Error ? err.message : 'unknown error'}`,
|
|
237
|
+
undefined,
|
|
238
|
+
{ cause: err },
|
|
354
239
|
);
|
|
355
240
|
}
|
|
356
241
|
}
|
|
@@ -400,7 +285,7 @@ function splitTextForTts(text: string, maxChars: number | null): string[] {
|
|
|
400
285
|
|
|
401
286
|
function concatAudioFiles(partPaths: string[], outputPath: string): void {
|
|
402
287
|
if (partPaths.length === 0) {
|
|
403
|
-
throw new
|
|
288
|
+
throw new SystemError('No chunk audio generated for concatenation.');
|
|
404
289
|
}
|
|
405
290
|
if (partPaths.length === 1) {
|
|
406
291
|
fs.copyFileSync(partPaths[0], outputPath);
|
|
@@ -419,8 +304,10 @@ function concatAudioFiles(partPaths: string[], outputPath: string): void {
|
|
|
419
304
|
{ stdio: 'ignore', timeout: 120000 },
|
|
420
305
|
);
|
|
421
306
|
} catch (err: unknown) {
|
|
422
|
-
throw new
|
|
307
|
+
throw new SystemError(
|
|
423
308
|
`ffmpeg concat failed: ${err instanceof Error ? err.message : 'unknown error'}`,
|
|
309
|
+
undefined,
|
|
310
|
+
{ cause: err },
|
|
424
311
|
);
|
|
425
312
|
} finally {
|
|
426
313
|
try { fs.unlinkSync(listFile); fs.rmdirSync(path.dirname(listFile)); } catch { /* ignore */ }
|
|
@@ -484,323 +371,316 @@ function requestAlibabaCloudTTS(
|
|
|
484
371
|
const audioFormat = audio.format || audio.mime_type || '';
|
|
485
372
|
|
|
486
373
|
if (!audioUrl && !audioData) {
|
|
487
|
-
reject(new
|
|
374
|
+
reject(new SystemError('API response does not contain output.audio.url or output.audio.data'));
|
|
488
375
|
return;
|
|
489
376
|
}
|
|
490
377
|
|
|
491
378
|
resolve({ audioUrl, audioData, audioFormat });
|
|
492
379
|
} catch {
|
|
493
|
-
reject(new
|
|
380
|
+
reject(new SystemError('API response is not valid JSON.'));
|
|
494
381
|
}
|
|
495
382
|
});
|
|
496
383
|
res.on('error', reject);
|
|
497
384
|
});
|
|
498
385
|
|
|
499
386
|
req.on('error', reject);
|
|
500
|
-
req.on('timeout', () => { req.destroy(); reject(new
|
|
387
|
+
req.on('timeout', () => { req.destroy(); reject(new SystemError('API request timed out')); });
|
|
501
388
|
req.write(payload);
|
|
502
389
|
req.end();
|
|
503
390
|
});
|
|
504
391
|
}
|
|
505
392
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const outputDir = path.join(projectDir, 'audio', projectName);
|
|
559
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
560
|
-
|
|
561
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
562
|
-
const outputName = opts.outputName || `voice-${timestamp}`;
|
|
563
|
-
const hasExtension = outputName.includes('.');
|
|
564
|
-
|
|
565
|
-
if (opts.mode === 'say') {
|
|
566
|
-
// macOS say mode
|
|
567
|
-
const textChunks = splitTextForTts(sourceText, opts.maxChars ? parseInt(opts.maxChars, 10) || null : null);
|
|
568
|
-
if (textChunks.length === 0) {
|
|
569
|
-
stderr.write('Error: No text content found for conversion.\n');
|
|
570
|
-
return 1;
|
|
393
|
+
const schema = {
|
|
394
|
+
options: {
|
|
395
|
+
'text': { type: 'string' as const },
|
|
396
|
+
'input': { type: 'string' as const },
|
|
397
|
+
'input-file': { type: 'string' as const },
|
|
398
|
+
'project-dir': { type: 'string' as const, default: '.' },
|
|
399
|
+
'project-name': { type: 'string' as const },
|
|
400
|
+
'output-name': { type: 'string' as const },
|
|
401
|
+
'engine': { type: 'string' as const },
|
|
402
|
+
'mode': { type: 'string' as const, default: 'say' },
|
|
403
|
+
'voice': { type: 'string' as const },
|
|
404
|
+
'rate': { type: 'string' as const },
|
|
405
|
+
'speech-rate': { type: 'string' as const },
|
|
406
|
+
'api-endpoint': { type: 'string' as const, default: DEFAULT_API_ENDPOINT },
|
|
407
|
+
'api-model': { type: 'string' as const, default: DEFAULT_API_MODEL },
|
|
408
|
+
'api-voice': { type: 'string' as const, default: DEFAULT_API_VOICE },
|
|
409
|
+
'api-key': { type: 'string' as const },
|
|
410
|
+
'max-chars': { type: 'string' as const },
|
|
411
|
+
'no-auto-prosody': { type: 'boolean' as const, default: false },
|
|
412
|
+
'force': { type: 'boolean' as const, default: false },
|
|
413
|
+
},
|
|
414
|
+
allowPositionals: true,
|
|
415
|
+
usage: 'apltk docs-to-voice [options]',
|
|
416
|
+
description: 'Convert text into audio and sentence timelines.',
|
|
417
|
+
handler: async (
|
|
418
|
+
values: Record<string, unknown>,
|
|
419
|
+
_positionals: string[],
|
|
420
|
+
context: ToolContext,
|
|
421
|
+
): Promise<number> => {
|
|
422
|
+
const stdout = context.stdout ?? process.stdout;
|
|
423
|
+
const stderr = context.stderr ?? process.stderr;
|
|
424
|
+
|
|
425
|
+
// Resolve args (previously handled by parseCliArgs)
|
|
426
|
+
const inputText: string | null = (values['text'] as string | undefined) ?? null;
|
|
427
|
+
const inputFile: string | null = (values['input'] as string | undefined) || (values['input-file'] as string | undefined) || null;
|
|
428
|
+
const projectDir: string = (values['project-dir'] as string) || '.';
|
|
429
|
+
const projectName: string | null = (values['project-name'] as string | undefined) ?? null;
|
|
430
|
+
const outputName: string | null = (values['output-name'] as string | undefined) ?? null;
|
|
431
|
+
const mode: string = ((values['mode'] as string | undefined) || (values['engine'] as string | undefined) || 'say').toLowerCase();
|
|
432
|
+
const voice: string | null = (values['voice'] as string | undefined) ?? null;
|
|
433
|
+
const rate: string | null = (values['rate'] as string | undefined) ?? null;
|
|
434
|
+
const speechRate: string | null = (values['speech-rate'] as string | undefined) ?? null;
|
|
435
|
+
const apiEndpoint: string = (values['api-endpoint'] as string) || DEFAULT_API_ENDPOINT;
|
|
436
|
+
const apiModel: string = (values['api-model'] as string) || DEFAULT_API_MODEL;
|
|
437
|
+
const apiVoice: string = (values['api-voice'] as string) || DEFAULT_API_VOICE;
|
|
438
|
+
const apiKey: string | null = (values['api-key'] as string | undefined) ?? null;
|
|
439
|
+
const maxChars: string | null = (values['max-chars'] as string | undefined) ?? null;
|
|
440
|
+
const noAutoProsody: boolean = !!values['no-auto-prosody'];
|
|
441
|
+
const force: boolean = !!values['force'];
|
|
442
|
+
|
|
443
|
+
if (mode !== 'say' && mode !== 'api') {
|
|
444
|
+
throw new UserInputError('--mode must be one of: say, api');
|
|
571
445
|
}
|
|
572
446
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
} catch {
|
|
577
|
-
stderr.write("Error: macOS 'say' command not found.\n");
|
|
578
|
-
return 1;
|
|
447
|
+
const sourceText = readInputText(inputFile, inputText);
|
|
448
|
+
if (!sourceText.trim()) {
|
|
449
|
+
throw new UserInputError('No text content found for conversion.');
|
|
579
450
|
}
|
|
580
451
|
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
return 1;
|
|
452
|
+
// Resolve output directory
|
|
453
|
+
const resolvedProjectDir = path.resolve(projectDir);
|
|
454
|
+
const resolvedProjectName = projectName || path.basename(resolvedProjectDir);
|
|
455
|
+
if (!resolvedProjectName) {
|
|
456
|
+
throw new UserInputError('Unable to determine project name.');
|
|
587
457
|
}
|
|
588
458
|
|
|
589
|
-
|
|
590
|
-
|
|
459
|
+
const outputDir = path.join(resolvedProjectDir, 'audio', resolvedProjectName);
|
|
460
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
591
461
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (opts.voice) sayArgs.push('-v', opts.voice);
|
|
596
|
-
if (opts.rate) sayArgs.push('-r', opts.rate);
|
|
462
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
463
|
+
const outName = outputName || `voice-${timestamp}`;
|
|
464
|
+
const hasExtension = outName.includes('.');
|
|
597
465
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
sayArgs.push('-f', tmpFile);
|
|
466
|
+
if (mode === 'say') {
|
|
467
|
+
const finalOutputName = hasExtension ? outName : `${outName}.aiff`;
|
|
468
|
+
const outputPath = path.join(outputDir, finalOutputName);
|
|
602
469
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
stdio: 'ignore',
|
|
606
|
-
timeout: 300000,
|
|
607
|
-
});
|
|
608
|
-
} catch (err: unknown) {
|
|
609
|
-
const msg = err instanceof Error ? err.message : 'unknown error';
|
|
610
|
-
throw new Error(`say mode failed: ${msg}`);
|
|
611
|
-
} finally {
|
|
612
|
-
try { fs.unlinkSync(tmpFile); fs.rmdirSync(path.dirname(tmpFile)); } catch { /* ignore */ }
|
|
470
|
+
if (fs.existsSync(outputPath) && !force) {
|
|
471
|
+
throw new UserInputError(`Output already exists: ${outputPath}. Use --force to overwrite.`);
|
|
613
472
|
}
|
|
614
|
-
} else {
|
|
615
|
-
// Multiple chunks: generate then concat
|
|
616
|
-
const tempDir = fs.mkdtempSync('docs-to-voice-say-');
|
|
617
|
-
const partPaths: string[] = [];
|
|
618
|
-
const partExt = path.extname(outputPath) || '.aiff';
|
|
619
473
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
474
|
+
// Build prosody-enhanced text
|
|
475
|
+
const textChunks = splitTextForTts(sourceText, maxChars ? parseInt(maxChars, 10) || null : null);
|
|
476
|
+
if (textChunks.length === 0) {
|
|
477
|
+
throw new UserInputError('No text content found for conversion.');
|
|
478
|
+
}
|
|
479
|
+
const chunks = noAutoProsody ? textChunks : textChunks.map(buildAutoProsodyText);
|
|
480
|
+
|
|
481
|
+
if (chunks.length === 1) {
|
|
482
|
+
// Single say command
|
|
483
|
+
const sayArgs = ['-o', outputPath];
|
|
484
|
+
if (voice) sayArgs.push('-v', voice);
|
|
485
|
+
if (rate) sayArgs.push('-r', rate);
|
|
486
|
+
|
|
487
|
+
const tmpFile = path.join(fs.mkdtempSync('docs-to-voice-'), 'input.txt');
|
|
488
|
+
fs.mkdirSync(path.dirname(tmpFile), { recursive: true });
|
|
489
|
+
fs.writeFileSync(tmpFile, chunks[0], 'utf-8');
|
|
490
|
+
sayArgs.push('-f', tmpFile);
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
execSync(`say ${sayArgs.map((a) => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`, {
|
|
494
|
+
stdio: 'ignore',
|
|
495
|
+
timeout: 300000,
|
|
496
|
+
});
|
|
497
|
+
} catch (err: unknown) {
|
|
498
|
+
const msg = err instanceof Error ? err.message : 'unknown error';
|
|
499
|
+
throw new SystemError(`say mode failed: ${msg}`);
|
|
500
|
+
} finally {
|
|
501
|
+
try { fs.unlinkSync(tmpFile); fs.rmdirSync(path.dirname(tmpFile)); } catch { /* ignore */ }
|
|
636
502
|
}
|
|
503
|
+
} else {
|
|
504
|
+
// Multiple chunks: generate then concat
|
|
505
|
+
const tempDir = fs.mkdtempSync('docs-to-voice-say-');
|
|
506
|
+
const partPaths: string[] = [];
|
|
507
|
+
const partExt = path.extname(outputPath) || '.aiff';
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
511
|
+
const partPath = path.join(tempDir, `part-${String(i + 1).padStart(4, '0')}${partExt}`);
|
|
512
|
+
const sArgs = ['-o', partPath];
|
|
513
|
+
if (voice) sArgs.push('-v', voice);
|
|
514
|
+
if (rate) sArgs.push('-r', rate);
|
|
515
|
+
|
|
516
|
+
const tFile = path.join(tempDir, `chunk-${i}.txt`);
|
|
517
|
+
fs.writeFileSync(tFile, chunks[i], 'utf-8');
|
|
518
|
+
sArgs.push('-f', tFile);
|
|
519
|
+
|
|
520
|
+
execSync(
|
|
521
|
+
`say ${sArgs.map((a) => (a.includes(' ') ? `"${a}"` : a)).join(' ')}`,
|
|
522
|
+
{ stdio: 'ignore', timeout: 300000 },
|
|
523
|
+
);
|
|
524
|
+
partPaths.push(partPath);
|
|
525
|
+
}
|
|
637
526
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
527
|
+
concatAudioFiles(partPaths, outputPath);
|
|
528
|
+
} finally {
|
|
529
|
+
try { fs.rmSync(tempDir, { recursive: true }); } catch { /* ignore */ }
|
|
530
|
+
}
|
|
641
531
|
}
|
|
642
|
-
}
|
|
643
532
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
533
|
+
// Apply speech rate if requested
|
|
534
|
+
if (speechRate) {
|
|
535
|
+
const rateVal = parseFloat(speechRate);
|
|
536
|
+
if (rateVal > 0) applySpeechRateToAudio(outputPath, rateVal);
|
|
537
|
+
}
|
|
649
538
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
return 1;
|
|
659
|
-
}
|
|
539
|
+
// Write timeline files
|
|
540
|
+
writeTimelineFiles(sourceText, outputPath, null);
|
|
541
|
+
stdout.write(`${outputPath}\n`);
|
|
542
|
+
} else {
|
|
543
|
+
// API mode
|
|
544
|
+
if (!apiKey) {
|
|
545
|
+
throw new UserInputError('--api-key is required for api mode.');
|
|
546
|
+
}
|
|
660
547
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
548
|
+
const sentences = splitSentences(sourceText);
|
|
549
|
+
if (sentences.length === 0) {
|
|
550
|
+
throw new UserInputError('No text content found for conversion.');
|
|
551
|
+
}
|
|
666
552
|
|
|
667
|
-
|
|
553
|
+
const maxCharsNum = maxChars ? parseInt(maxChars, 10) || null : null;
|
|
668
554
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
555
|
+
// Build request items from sentences
|
|
556
|
+
interface RequestItem {
|
|
557
|
+
sentenceIndex: number;
|
|
558
|
+
text: string;
|
|
559
|
+
}
|
|
560
|
+
const requestItems: RequestItem[] = [];
|
|
561
|
+
for (let si = 0; si < sentences.length; si++) {
|
|
562
|
+
const sentence = sentences[si];
|
|
563
|
+
if (maxCharsNum && sentence.length > maxCharsNum) {
|
|
564
|
+
for (let i = 0; i < sentence.length; i += maxCharsNum) {
|
|
565
|
+
requestItems.push({ sentenceIndex: si, text: sentence.slice(i, i + maxCharsNum) });
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
requestItems.push({ sentenceIndex: si, text: sentence });
|
|
680
569
|
}
|
|
681
|
-
} else {
|
|
682
|
-
requestItems.push({ sentenceIndex: si, text: sentence });
|
|
683
570
|
}
|
|
684
|
-
}
|
|
685
571
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
572
|
+
if (requestItems.length === 0) {
|
|
573
|
+
throw new UserInputError('No text content found for conversion.');
|
|
574
|
+
}
|
|
690
575
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
576
|
+
const tempDir = fs.mkdtempSync('docs-to-voice-api-');
|
|
577
|
+
const partPaths: string[] = [];
|
|
578
|
+
let partExt = '';
|
|
579
|
+
const sentenceDurations = new Array(sentences.length).fill(0);
|
|
580
|
+
const sentenceDurationKnown = new Array(sentences.length).fill(true);
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
for (let i = 0; i < requestItems.length; i++) {
|
|
584
|
+
const item = requestItems[i];
|
|
585
|
+
const apiResult = await requestAlibabaCloudTTS(
|
|
586
|
+
apiEndpoint,
|
|
587
|
+
apiKey,
|
|
588
|
+
apiModel,
|
|
589
|
+
apiVoice,
|
|
590
|
+
item.text,
|
|
591
|
+
);
|
|
592
|
+
|
|
593
|
+
const currentExt = apiResult.audioFormat || 'wav';
|
|
594
|
+
if (!partExt) partExt = currentExt;
|
|
595
|
+
|
|
596
|
+
const partPath = path.join(tempDir, `part-${String(i + 1).padStart(4, '0')}.${currentExt}`);
|
|
597
|
+
if (apiResult.audioUrl) {
|
|
598
|
+
await downloadBinary(apiResult.audioUrl, partPath);
|
|
599
|
+
} else if (apiResult.audioData) {
|
|
600
|
+
fs.writeFileSync(partPath, Buffer.from(apiResult.audioData, 'base64'));
|
|
601
|
+
} else {
|
|
602
|
+
throw new SystemError('No audio data in API response.');
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (!fs.existsSync(partPath) || fs.statSync(partPath).size === 0) {
|
|
606
|
+
throw new SystemError(`Failed to generate audio chunk ${i + 1}.`);
|
|
607
|
+
}
|
|
608
|
+
partPaths.push(partPath);
|
|
719
609
|
|
|
720
|
-
|
|
721
|
-
|
|
610
|
+
const partDuration = readDurationSeconds(partPath);
|
|
611
|
+
if (partDuration === null || partDuration <= 0) {
|
|
612
|
+
sentenceDurationKnown[item.sentenceIndex] = false;
|
|
613
|
+
} else {
|
|
614
|
+
sentenceDurations[item.sentenceIndex] += partDuration;
|
|
615
|
+
}
|
|
722
616
|
}
|
|
723
|
-
partPaths.push(partPath);
|
|
724
617
|
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
618
|
+
const finalOutputName = hasExtension
|
|
619
|
+
? outName
|
|
620
|
+
: `${outName}.${partExt || 'wav'}`;
|
|
621
|
+
const outputPath = path.join(outputDir, finalOutputName);
|
|
622
|
+
|
|
623
|
+
if (fs.existsSync(outputPath) && !force) {
|
|
624
|
+
throw new UserInputError(`Output already exists: ${outputPath}. Use --force to overwrite.`);
|
|
730
625
|
}
|
|
731
|
-
}
|
|
732
626
|
|
|
733
|
-
|
|
734
|
-
? outputName
|
|
735
|
-
: `${outputName}.${partExt || 'wav'}`;
|
|
736
|
-
const outputPath = path.join(outputDir, finalOutputName);
|
|
627
|
+
concatAudioFiles(partPaths, outputPath);
|
|
737
628
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
629
|
+
// Build timeline durations
|
|
630
|
+
let timelineDurations: number[] | null = null;
|
|
631
|
+
const unknownIndexes = sentenceDurationKnown
|
|
632
|
+
.map((known, idx) => (known ? -1 : idx))
|
|
633
|
+
.filter((idx) => idx >= 0);
|
|
634
|
+
|
|
635
|
+
if (unknownIndexes.length === 0 && sentenceDurations.reduce((a, b) => a + b, 0) > 0) {
|
|
636
|
+
timelineDurations = sentenceDurations;
|
|
637
|
+
} else if (unknownIndexes.length > 0) {
|
|
638
|
+
const outputDuration = readDurationSeconds(outputPath);
|
|
639
|
+
const knownTotal = sentenceDurations.reduce(
|
|
640
|
+
(sum, val, idx) => (sentenceDurationKnown[idx] ? sum + val : sum),
|
|
641
|
+
0,
|
|
642
|
+
);
|
|
742
643
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
const outputDuration = readDurationSeconds(outputPath);
|
|
755
|
-
const knownTotal = sentenceDurations.reduce(
|
|
756
|
-
(sum, val, idx) => (sentenceDurationKnown[idx] ? sum + val : sum),
|
|
757
|
-
0,
|
|
758
|
-
);
|
|
759
|
-
|
|
760
|
-
if (outputDuration && outputDuration > knownTotal) {
|
|
761
|
-
const remaining = outputDuration - knownTotal;
|
|
762
|
-
const unknownWeights = unknownIndexes.map((idx) => sentenceWeight(sentences[idx]));
|
|
763
|
-
const totalUnknownWeight = unknownWeights.reduce((a, b) => a + b, 0);
|
|
764
|
-
|
|
765
|
-
if (totalUnknownWeight > 0) {
|
|
766
|
-
for (let wi = 0; wi < unknownIndexes.length; wi++) {
|
|
767
|
-
sentenceDurations[unknownIndexes[wi]] +=
|
|
768
|
-
remaining * (unknownWeights[wi] / totalUnknownWeight);
|
|
644
|
+
if (outputDuration && outputDuration > knownTotal) {
|
|
645
|
+
const remaining = outputDuration - knownTotal;
|
|
646
|
+
const unknownWeights = unknownIndexes.map((idx) => sentenceWeight(sentences[idx]));
|
|
647
|
+
const totalUnknownWeight = unknownWeights.reduce((a, b) => a + b, 0);
|
|
648
|
+
|
|
649
|
+
if (totalUnknownWeight > 0) {
|
|
650
|
+
for (let wi = 0; wi < unknownIndexes.length; wi++) {
|
|
651
|
+
sentenceDurations[unknownIndexes[wi]] +=
|
|
652
|
+
remaining * (unknownWeights[wi] / totalUnknownWeight);
|
|
653
|
+
}
|
|
654
|
+
timelineDurations = sentenceDurations;
|
|
769
655
|
}
|
|
770
|
-
timelineDurations = sentenceDurations;
|
|
771
656
|
}
|
|
772
657
|
}
|
|
773
|
-
}
|
|
774
658
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
659
|
+
// Apply speech rate if requested
|
|
660
|
+
if (speechRate) {
|
|
661
|
+
const rateVal = parseFloat(speechRate);
|
|
662
|
+
if (rateVal > 0) {
|
|
663
|
+
applySpeechRateToAudio(outputPath, rateVal);
|
|
664
|
+
if (timelineDurations) {
|
|
665
|
+
timelineDurations = timelineDurations.map((d) => d / rateVal);
|
|
666
|
+
}
|
|
782
667
|
}
|
|
783
668
|
}
|
|
784
|
-
}
|
|
785
669
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
670
|
+
writeTimelineFiles(sourceText, outputPath, timelineDurations);
|
|
671
|
+
stdout.write(`${outputPath}\n`);
|
|
672
|
+
} finally {
|
|
673
|
+
try { fs.rmSync(tempDir, { recursive: true }); } catch { /* ignore */ }
|
|
674
|
+
}
|
|
790
675
|
}
|
|
791
|
-
}
|
|
792
676
|
|
|
793
|
-
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
stderr.write(`Error: ${msg}\n`);
|
|
797
|
-
return 1;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
677
|
+
return 0;
|
|
678
|
+
},
|
|
679
|
+
};
|
|
800
680
|
|
|
801
681
|
export const tool: ToolDefinition = {
|
|
802
682
|
name: 'docs-to-voice',
|
|
803
683
|
category: 'media',
|
|
804
684
|
description: 'Convert text into audio and sentence timelines.',
|
|
805
|
-
handler:
|
|
685
|
+
handler: createToolRunner(schema),
|
|
806
686
|
};
|