@gobing-ai/knowledge-kit 0.0.6 → 0.0.8
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/dist/index.js +21643 -11933
- package/package.json +4 -1
- package/plugins/generations/content-gen/package.json +2 -1
- package/plugins/generations/content-gen/src/index.ts +9 -8
- package/plugins/generations/content-gen/src/storm.ts +12 -4
- package/plugins/generations/voice-gen/package.json +17 -0
- package/plugins/generations/voice-gen/plugin.json +6 -0
- package/plugins/generations/voice-gen/src/concat.ts +218 -0
- package/plugins/generations/voice-gen/src/index.ts +213 -0
- package/plugins/generations/voice-gen/src/voicebox-client.ts +223 -0
- package/plugins/generations/voice-gen/src/voicescript.ts +365 -0
- package/plugins/generations/voice-gen/tsconfig.json +8 -0
- package/plugins/ingestions/karakeep-local/package.json +17 -0
- package/plugins/ingestions/karakeep-local/src/index.ts +31 -26
- package/plugins/ingestions/karakeep-local/tsconfig.json +4 -0
- package/plugins/ingestions/web-search/package.json +4 -1
- package/plugins/ingestions/web-search/src/index.ts +299 -17
- package/plugins/kk/README.md +12 -1
- package/plugins/kk/commands/workflow-run.md +202 -0
- package/plugins/kk/config.example.yaml +34 -0
- package/plugins/kk/scripts/render-md.ts +150 -0
- package/plugins/kk/skills/{kk-judge → content-judge}/SKILL.md +13 -14
- package/plugins/kk/skills/{kk-judge → content-judge}/references/rubrics.md +2 -2
- package/plugins/kk/skills/{kk-judge → content-judge}/references/workflow-integration.md +17 -15
- package/plugins/kk/skills/itc-generating/SKILL.md +147 -0
- package/plugins/kk/skills/itc-generating/references/generic-craft.md +80 -0
- package/plugins/kk/skills/itc-generating/references/platform-english.md +72 -0
- package/plugins/kk/skills/itc-generating/references/platform-wechat.md +60 -0
- package/plugins/kk/skills/itc-generating/references/skill-authoring.md +62 -0
- package/plugins/kk/skills/storm-research/SKILL.md +173 -0
- package/plugins/kk/skills/{kk-topic → topic}/SKILL.md +4 -4
- package/plugins/kk/workflows/judge-gated-publish-example.yaml +101 -0
- package/plugins/kk/workflows/kk-ingest-generate-publish.yaml +72 -0
- package/plugins/kk/workflows/kk-itc.yaml +285 -0
- package/plugins/kk/workflows/kk-solo-podcast.yaml +374 -0
- package/plugins/kk/workflows/kk-storm-research.yaml +184 -0
- package/plugins/kk/workflows/validate-voicescript.ts +226 -0
- package/plugins/publishings/emdash-pub/package.json +17 -0
- package/plugins/publishings/emdash-pub/plugin.json +7 -0
- package/plugins/publishings/emdash-pub/src/index.ts +450 -0
- package/plugins/publishings/emdash-pub/tsconfig.json +4 -0
- package/plugins/publishings/qiita-pub/package.json +2 -1
- package/plugins/publishings/qiita-pub/src/index.ts +9 -9
- package/plugins/publishings/surfdash-pub/package.json +2 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +16 -11
- package/plugins/publishings/zenn-pub/package.json +2 -1
- package/plugins/publishings/zenn-pub/src/index.ts +11 -11
- package/plugins/kk/agents/kk-judge-compliance.md +0 -37
- package/plugins/kk/agents/kk-judge-tech.md +0 -35
- package/plugins/kk/agents/kk-judge-tone.md +0 -37
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
export interface VoiceboxGenerateBody {
|
|
2
|
+
profile_id: string;
|
|
3
|
+
text: string;
|
|
4
|
+
language: string;
|
|
5
|
+
engine?: string;
|
|
6
|
+
instruct?: string;
|
|
7
|
+
max_chunk_chars: number;
|
|
8
|
+
crossfade_ms: number;
|
|
9
|
+
personality: false;
|
|
10
|
+
normalize: true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface VoiceboxProfile {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
engine?: string;
|
|
18
|
+
language?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface VoiceboxHistory {
|
|
22
|
+
id: string;
|
|
23
|
+
status: 'pending' | 'generating' | 'completed' | 'failed' | string;
|
|
24
|
+
duration?: number;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface VoiceboxClient {
|
|
29
|
+
readonly url: string;
|
|
30
|
+
health(): Promise<void>;
|
|
31
|
+
resolveProfile(nameOrId: string): Promise<VoiceboxProfile>;
|
|
32
|
+
generate(body: VoiceboxGenerateBody): Promise<{ id: string }>;
|
|
33
|
+
waitUntilDone(
|
|
34
|
+
id: string,
|
|
35
|
+
timeoutMs?: number,
|
|
36
|
+
pollMs?: number,
|
|
37
|
+
): Promise<{ status: 'completed' | 'failed'; duration?: number; error?: string }>;
|
|
38
|
+
downloadAudio(id: string): Promise<Uint8Array>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface VoiceboxClientOptions {
|
|
42
|
+
url?: string;
|
|
43
|
+
fetch?: typeof fetch;
|
|
44
|
+
sleep?: (ms: number) => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a client for the Voicebox local REST API.
|
|
49
|
+
*/
|
|
50
|
+
export function createVoiceboxClient(options: VoiceboxClientOptions = {}): VoiceboxClient {
|
|
51
|
+
const rawUrl = options.url || process.env.VOICEBOX_URL || 'http://127.0.0.1:17493';
|
|
52
|
+
const url = rawUrl.replace(/\/+$/, '');
|
|
53
|
+
const customFetch = options.fetch ?? globalThis.fetch;
|
|
54
|
+
const sleep = options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
url,
|
|
58
|
+
|
|
59
|
+
async health(): Promise<void> {
|
|
60
|
+
try {
|
|
61
|
+
const res = await customFetch(`${url}/health`);
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
67
|
+
throw new Error(`Voicebox health check failed at ${url}: ${reason}`);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async resolveProfile(nameOrId: string): Promise<VoiceboxProfile> {
|
|
72
|
+
let res: Response;
|
|
73
|
+
try {
|
|
74
|
+
res = await customFetch(`${url}/profiles`);
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
77
|
+
throw new Error(`Voicebox /profiles request failed at ${url}: ${reason}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
throw new Error(`Voicebox /profiles request failed at ${url}: HTTP ${res.status} ${res.statusText}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let profiles: VoiceboxProfile[];
|
|
85
|
+
try {
|
|
86
|
+
profiles = (await res.json()) as VoiceboxProfile[];
|
|
87
|
+
} catch (err: unknown) {
|
|
88
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
89
|
+
throw new Error(`Voicebox /profiles returned invalid JSON at ${url}: ${reason}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!Array.isArray(profiles)) {
|
|
93
|
+
throw new Error(`Voicebox /profiles returned non-array at ${url}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const targetLower = nameOrId.toLowerCase();
|
|
97
|
+
const matched = profiles.find((p) => p.id === nameOrId || (p.name && p.name.toLowerCase() === targetLower));
|
|
98
|
+
|
|
99
|
+
if (!matched) {
|
|
100
|
+
throw new Error(`Voicebox profile "${nameOrId}" not found at ${url}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return matched;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async generate(body: VoiceboxGenerateBody): Promise<{ id: string }> {
|
|
107
|
+
let res: Response;
|
|
108
|
+
try {
|
|
109
|
+
res = await customFetch(`${url}/generate`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
headers: {
|
|
112
|
+
'Content-Type': 'application/json',
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(body),
|
|
115
|
+
});
|
|
116
|
+
} catch (err: unknown) {
|
|
117
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
118
|
+
throw new Error(`Voicebox /generate request failed at ${url}: ${reason}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
let errorDetails = '';
|
|
123
|
+
try {
|
|
124
|
+
errorDetails = await res.text();
|
|
125
|
+
} catch {
|
|
126
|
+
// ignore
|
|
127
|
+
}
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Voicebox /generate failed at ${url}: HTTP ${res.status} ${res.statusText} ${errorDetails}`.trim(),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let data: { id: string };
|
|
134
|
+
try {
|
|
135
|
+
data = (await res.json()) as { id: string };
|
|
136
|
+
} catch (err: unknown) {
|
|
137
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
138
|
+
throw new Error(`Voicebox /generate returned invalid JSON at ${url}: ${reason}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!data || typeof data.id !== 'string') {
|
|
142
|
+
throw new Error(`Voicebox /generate response missing id at ${url}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { id: data.id };
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
async waitUntilDone(
|
|
149
|
+
id: string,
|
|
150
|
+
timeoutMs?: number,
|
|
151
|
+
pollMs?: number,
|
|
152
|
+
): Promise<{ status: 'completed' | 'failed'; duration?: number; error?: string }> {
|
|
153
|
+
const timeout =
|
|
154
|
+
timeoutMs ?? (process.env.VOICEBOX_TIMEOUT_MS ? parseInt(process.env.VOICEBOX_TIMEOUT_MS, 10) : 600000);
|
|
155
|
+
const poll = pollMs ?? (process.env.VOICEBOX_POLL_MS ? parseInt(process.env.VOICEBOX_POLL_MS, 10) : 1000);
|
|
156
|
+
|
|
157
|
+
const startTime = Date.now();
|
|
158
|
+
|
|
159
|
+
while (true) {
|
|
160
|
+
let res: Response;
|
|
161
|
+
try {
|
|
162
|
+
res = await customFetch(`${url}/history/${id}`);
|
|
163
|
+
} catch (err: unknown) {
|
|
164
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
165
|
+
throw new Error(`Voicebox /history/${id} request failed at ${url}: ${reason}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Voicebox /history/${id} request failed at ${url}: HTTP ${res.status} ${res.statusText}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let history: VoiceboxHistory;
|
|
175
|
+
try {
|
|
176
|
+
history = (await res.json()) as VoiceboxHistory;
|
|
177
|
+
} catch (err: unknown) {
|
|
178
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
179
|
+
throw new Error(`Voicebox /history/${id} returned invalid JSON at ${url}: ${reason}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (history.status === 'completed') {
|
|
183
|
+
return { status: 'completed', duration: history.duration };
|
|
184
|
+
}
|
|
185
|
+
if (history.status === 'failed') {
|
|
186
|
+
return {
|
|
187
|
+
status: 'failed',
|
|
188
|
+
duration: history.duration,
|
|
189
|
+
error: history.error || 'Generation status failed',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (Date.now() - startTime >= timeout) {
|
|
194
|
+
throw new Error(`Voicebox generation timed out after ${timeout}ms for id: ${id} at ${url}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await sleep(poll);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
async downloadAudio(id: string): Promise<Uint8Array> {
|
|
202
|
+
let res: Response;
|
|
203
|
+
try {
|
|
204
|
+
res = await customFetch(`${url}/audio/${id}`);
|
|
205
|
+
} catch (err: unknown) {
|
|
206
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
207
|
+
throw new Error(`Voicebox /audio/${id} request failed at ${url}: ${reason}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
throw new Error(`Voicebox /audio/${id} request failed at ${url}: HTTP ${res.status} ${res.statusText}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const arrayBuf = await res.arrayBuffer();
|
|
216
|
+
return new Uint8Array(arrayBuf);
|
|
217
|
+
} catch (err: unknown) {
|
|
218
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
219
|
+
throw new Error(`Voicebox /audio/${id} failed to read audio stream at ${url}: ${reason}`);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import type { Doc } from '@gobing-ai/kk-core';
|
|
2
|
+
|
|
3
|
+
export const VOICEBOX_TEXT_MAX = 50_000;
|
|
4
|
+
export const VOICEBOX_INSTRUCT_MAX = 500;
|
|
5
|
+
export const VOICEBOX_CHUNK_CHARS_MIN = 100;
|
|
6
|
+
export const VOICEBOX_CHUNK_CHARS_MAX = 5_000;
|
|
7
|
+
export const VOICEBOX_CHUNK_CHARS_DEFAULT = 800;
|
|
8
|
+
export const VOICEBOX_CROSSFADE_MS_MIN = 0;
|
|
9
|
+
export const VOICEBOX_CROSSFADE_MS_MAX = 500;
|
|
10
|
+
export const VOICEBOX_CROSSFADE_MS_DEFAULT = 50;
|
|
11
|
+
|
|
12
|
+
export const VOICEBOX_ENGINES = [
|
|
13
|
+
'qwen',
|
|
14
|
+
'qwen_custom_voice',
|
|
15
|
+
'luxtts',
|
|
16
|
+
'chatterbox',
|
|
17
|
+
'chatterbox_turbo',
|
|
18
|
+
'tada',
|
|
19
|
+
'kokoro',
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export const VOICEBOX_LANGUAGES = [
|
|
23
|
+
'zh',
|
|
24
|
+
'en',
|
|
25
|
+
'ja',
|
|
26
|
+
'ko',
|
|
27
|
+
'de',
|
|
28
|
+
'fr',
|
|
29
|
+
'ru',
|
|
30
|
+
'pt',
|
|
31
|
+
'es',
|
|
32
|
+
'it',
|
|
33
|
+
'he',
|
|
34
|
+
'ar',
|
|
35
|
+
'da',
|
|
36
|
+
'el',
|
|
37
|
+
'fi',
|
|
38
|
+
'hi',
|
|
39
|
+
'ms',
|
|
40
|
+
'nl',
|
|
41
|
+
'no',
|
|
42
|
+
'pl',
|
|
43
|
+
'sv',
|
|
44
|
+
'sw',
|
|
45
|
+
'tr',
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const ENGINE_SET = new Set<string>(VOICEBOX_ENGINES);
|
|
49
|
+
const LANGUAGE_SET = new Set<string>(VOICEBOX_LANGUAGES);
|
|
50
|
+
|
|
51
|
+
function assertEngine(value: string | undefined, label: string): void {
|
|
52
|
+
if (value !== undefined && !ENGINE_SET.has(value)) {
|
|
53
|
+
throw new Error(`${label} engine must be one of ${VOICEBOX_ENGINES.join('|')} (got "${value}")`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertLanguage(value: string | undefined, label: string): void {
|
|
58
|
+
if (value !== undefined && !LANGUAGE_SET.has(value)) {
|
|
59
|
+
throw new Error(`${label} language must be a Voicebox language code (got "${value}")`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface VoiceSpeaker {
|
|
64
|
+
profile: string;
|
|
65
|
+
engine?: string;
|
|
66
|
+
language?: string;
|
|
67
|
+
instruct?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface VoiceSegment {
|
|
71
|
+
speaker?: string;
|
|
72
|
+
profile?: string;
|
|
73
|
+
text: string;
|
|
74
|
+
instruct?: string;
|
|
75
|
+
engine?: string;
|
|
76
|
+
language?: string;
|
|
77
|
+
gap_ms?: number;
|
|
78
|
+
max_chunk_chars?: number;
|
|
79
|
+
crossfade_ms?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface VoiceScript {
|
|
83
|
+
title?: string;
|
|
84
|
+
language?: string;
|
|
85
|
+
default_engine?: string;
|
|
86
|
+
default_profile?: string;
|
|
87
|
+
max_chunk_chars?: number;
|
|
88
|
+
crossfade_ms?: number;
|
|
89
|
+
speakers?: Record<string, VoiceSpeaker>;
|
|
90
|
+
segments: VoiceSegment[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const VOICESCRIPT_KEYS = new Set(['segments', 'speakers', 'default_profile', 'default_engine']);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse a Doc into a VoiceScript structure.
|
|
97
|
+
* - JSON object starting with '{' -> VoiceScript path (even without segments -> validation fails loud).
|
|
98
|
+
* - Body starting with '[' -> Plain text path (e.g. Chatterbox [laugh] tag).
|
|
99
|
+
* - YAML mapping with any of segments/speakers/default_profile/default_engine -> VoiceScript path.
|
|
100
|
+
* - Otherwise -> Plain text single-segment script.
|
|
101
|
+
*/
|
|
102
|
+
export function parseDocToVoiceScript(doc: Doc, envDefaultProfile?: string): VoiceScript {
|
|
103
|
+
const trimmed = (doc.body || '').trim();
|
|
104
|
+
|
|
105
|
+
if (trimmed.startsWith('{')) {
|
|
106
|
+
let parsed: unknown;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(trimmed);
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error('Invalid VoiceScript JSON structure: failed to parse JSON object');
|
|
111
|
+
}
|
|
112
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
113
|
+
throw new Error('Invalid VoiceScript JSON structure: root must be a JSON object');
|
|
114
|
+
}
|
|
115
|
+
return parsed as VoiceScript;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (trimmed.startsWith('[')) {
|
|
119
|
+
return {
|
|
120
|
+
segments: [{ text: trimmed }],
|
|
121
|
+
default_profile:
|
|
122
|
+
envDefaultProfile ??
|
|
123
|
+
(typeof doc.metadata?.voiceProfile === 'string' ? doc.metadata.voiceProfile : undefined),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const parsed = Bun.YAML.parse(trimmed);
|
|
129
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
130
|
+
const hasKey = Object.keys(parsed).some((k) => VOICESCRIPT_KEYS.has(k));
|
|
131
|
+
if (hasKey) {
|
|
132
|
+
return parsed as VoiceScript;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Fall through to plain text
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
segments: [{ text: trimmed }],
|
|
141
|
+
default_profile:
|
|
142
|
+
envDefaultProfile ??
|
|
143
|
+
(typeof doc.metadata?.voiceProfile === 'string' ? doc.metadata.voiceProfile : undefined),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Merge multiple Doc-level VoiceScripts into a single combined VoiceScript.
|
|
149
|
+
*/
|
|
150
|
+
export function mergeVoiceScripts(scripts: VoiceScript[], docs: Doc[], envDefaultProfile?: string): VoiceScript {
|
|
151
|
+
if (scripts.length === 0) {
|
|
152
|
+
return { segments: [] };
|
|
153
|
+
}
|
|
154
|
+
const firstScript = scripts[0];
|
|
155
|
+
if (scripts.length === 1 && firstScript) {
|
|
156
|
+
return firstScript;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const combinedSpeakers: Record<string, VoiceSpeaker> = {};
|
|
160
|
+
const combinedSegments: VoiceSegment[] = [];
|
|
161
|
+
|
|
162
|
+
let title: string | undefined;
|
|
163
|
+
let defaultProfile: string | undefined = envDefaultProfile;
|
|
164
|
+
let defaultEngine: string | undefined;
|
|
165
|
+
let language: string | undefined;
|
|
166
|
+
let maxChunkChars: number | undefined;
|
|
167
|
+
let crossfadeMs: number | undefined;
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < scripts.length; i++) {
|
|
170
|
+
const script = scripts[i];
|
|
171
|
+
if (!script) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const doc = docs[i];
|
|
175
|
+
|
|
176
|
+
if (!title) {
|
|
177
|
+
title = script.title || doc?.title;
|
|
178
|
+
}
|
|
179
|
+
if (script.default_profile && defaultProfile === envDefaultProfile) {
|
|
180
|
+
defaultProfile = script.default_profile;
|
|
181
|
+
}
|
|
182
|
+
if (!defaultProfile && typeof doc?.metadata?.voiceProfile === 'string') {
|
|
183
|
+
defaultProfile = doc.metadata.voiceProfile;
|
|
184
|
+
}
|
|
185
|
+
if (!defaultEngine && script.default_engine) {
|
|
186
|
+
defaultEngine = script.default_engine;
|
|
187
|
+
}
|
|
188
|
+
if (!language && script.language) {
|
|
189
|
+
language = script.language;
|
|
190
|
+
}
|
|
191
|
+
if (maxChunkChars === undefined && script.max_chunk_chars !== undefined) {
|
|
192
|
+
maxChunkChars = script.max_chunk_chars;
|
|
193
|
+
}
|
|
194
|
+
if (crossfadeMs === undefined && script.crossfade_ms !== undefined) {
|
|
195
|
+
crossfadeMs = script.crossfade_ms;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (script.speakers) {
|
|
199
|
+
for (const [key, speaker] of Object.entries(script.speakers)) {
|
|
200
|
+
if (key in combinedSpeakers) {
|
|
201
|
+
throw new Error(`Duplicate speaker key across documents: "${key}"`);
|
|
202
|
+
}
|
|
203
|
+
combinedSpeakers[key] = speaker;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (Array.isArray(script.segments)) {
|
|
208
|
+
combinedSegments.push(...script.segments);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const result: VoiceScript = {
|
|
213
|
+
title: title || docs[0]?.title || 'Generated voice',
|
|
214
|
+
segments: combinedSegments,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
if (Object.keys(combinedSpeakers).length > 0) {
|
|
218
|
+
result.speakers = combinedSpeakers;
|
|
219
|
+
}
|
|
220
|
+
if (defaultProfile) {
|
|
221
|
+
result.default_profile = defaultProfile;
|
|
222
|
+
}
|
|
223
|
+
if (defaultEngine) {
|
|
224
|
+
result.default_engine = defaultEngine;
|
|
225
|
+
}
|
|
226
|
+
if (language) {
|
|
227
|
+
result.language = language;
|
|
228
|
+
}
|
|
229
|
+
if (maxChunkChars !== undefined) {
|
|
230
|
+
result.max_chunk_chars = maxChunkChars;
|
|
231
|
+
}
|
|
232
|
+
if (crossfadeMs !== undefined) {
|
|
233
|
+
result.crossfade_ms = crossfadeMs;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Validate that a VoiceScript satisfies all Voicebox and plugin constraints.
|
|
241
|
+
*/
|
|
242
|
+
export function validateVoiceScript(script: VoiceScript): void {
|
|
243
|
+
if (!script || typeof script !== 'object') {
|
|
244
|
+
throw new Error('VoiceScript must be an object');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!Array.isArray(script.segments) || script.segments.length === 0) {
|
|
248
|
+
throw new Error('VoiceScript must have a non-empty segments array');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (script.max_chunk_chars !== undefined) {
|
|
252
|
+
if (
|
|
253
|
+
typeof script.max_chunk_chars !== 'number' ||
|
|
254
|
+
!Number.isInteger(script.max_chunk_chars) ||
|
|
255
|
+
script.max_chunk_chars < VOICEBOX_CHUNK_CHARS_MIN ||
|
|
256
|
+
script.max_chunk_chars > VOICEBOX_CHUNK_CHARS_MAX
|
|
257
|
+
) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`max_chunk_chars must be between ${VOICEBOX_CHUNK_CHARS_MIN} and ${VOICEBOX_CHUNK_CHARS_MAX}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (script.crossfade_ms !== undefined) {
|
|
265
|
+
if (
|
|
266
|
+
typeof script.crossfade_ms !== 'number' ||
|
|
267
|
+
!Number.isInteger(script.crossfade_ms) ||
|
|
268
|
+
script.crossfade_ms < VOICEBOX_CROSSFADE_MS_MIN ||
|
|
269
|
+
script.crossfade_ms > VOICEBOX_CROSSFADE_MS_MAX
|
|
270
|
+
) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`crossfade_ms must be between ${VOICEBOX_CROSSFADE_MS_MIN} and ${VOICEBOX_CROSSFADE_MS_MAX}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
assertEngine(script.default_engine, 'VoiceScript default_engine');
|
|
278
|
+
assertLanguage(script.language, 'VoiceScript');
|
|
279
|
+
|
|
280
|
+
if (script.speakers) {
|
|
281
|
+
for (const [name, speaker] of Object.entries(script.speakers)) {
|
|
282
|
+
if (!speaker || typeof speaker !== 'object') {
|
|
283
|
+
throw new Error(`Speaker "${name}" definition must be an object`);
|
|
284
|
+
}
|
|
285
|
+
if (!speaker.profile || typeof speaker.profile !== 'string') {
|
|
286
|
+
throw new Error(`Speaker "${name}" must have a profile specified`);
|
|
287
|
+
}
|
|
288
|
+
if (
|
|
289
|
+
speaker.instruct &&
|
|
290
|
+
typeof speaker.instruct === 'string' &&
|
|
291
|
+
speaker.instruct.length > VOICEBOX_INSTRUCT_MAX
|
|
292
|
+
) {
|
|
293
|
+
throw new Error(`Speaker "${name}" instruct exceeds 500 characters (${speaker.instruct.length} chars)`);
|
|
294
|
+
}
|
|
295
|
+
assertEngine(speaker.engine, `Speaker "${name}"`);
|
|
296
|
+
assertLanguage(speaker.language, `Speaker "${name}"`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
for (let idx = 0; idx < script.segments.length; idx++) {
|
|
301
|
+
const segment = script.segments[idx];
|
|
302
|
+
if (!segment || typeof segment !== 'object') {
|
|
303
|
+
throw new Error(`Segment at index ${idx} must be an object`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (typeof segment.text !== 'string' || segment.text.length === 0) {
|
|
307
|
+
throw new Error(`Segment at index ${idx} text must not be empty`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (segment.text.length > VOICEBOX_TEXT_MAX) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`Segment text exceeds the 50,000-character Voicebox /generate cap (${segment.text.length} chars)`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (
|
|
317
|
+
segment.instruct &&
|
|
318
|
+
typeof segment.instruct === 'string' &&
|
|
319
|
+
segment.instruct.length > VOICEBOX_INSTRUCT_MAX
|
|
320
|
+
) {
|
|
321
|
+
throw new Error(`Segment instruct exceeds 500 characters (${segment.instruct.length} chars)`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (segment.speaker) {
|
|
325
|
+
if (!script.speakers || !(segment.speaker in script.speakers)) {
|
|
326
|
+
throw new Error(`Unknown speaker: "${segment.speaker}"`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (segment.max_chunk_chars !== undefined) {
|
|
331
|
+
if (
|
|
332
|
+
typeof segment.max_chunk_chars !== 'number' ||
|
|
333
|
+
!Number.isInteger(segment.max_chunk_chars) ||
|
|
334
|
+
segment.max_chunk_chars < VOICEBOX_CHUNK_CHARS_MIN ||
|
|
335
|
+
segment.max_chunk_chars > VOICEBOX_CHUNK_CHARS_MAX
|
|
336
|
+
) {
|
|
337
|
+
throw new Error(
|
|
338
|
+
`max_chunk_chars must be between ${VOICEBOX_CHUNK_CHARS_MIN} and ${VOICEBOX_CHUNK_CHARS_MAX}`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (segment.crossfade_ms !== undefined) {
|
|
344
|
+
if (
|
|
345
|
+
typeof segment.crossfade_ms !== 'number' ||
|
|
346
|
+
!Number.isInteger(segment.crossfade_ms) ||
|
|
347
|
+
segment.crossfade_ms < VOICEBOX_CROSSFADE_MS_MIN ||
|
|
348
|
+
segment.crossfade_ms > VOICEBOX_CROSSFADE_MS_MAX
|
|
349
|
+
) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`crossfade_ms must be between ${VOICEBOX_CROSSFADE_MS_MIN} and ${VOICEBOX_CROSSFADE_MS_MAX}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (segment.gap_ms !== undefined) {
|
|
357
|
+
if (typeof segment.gap_ms !== 'number' || segment.gap_ms < 0) {
|
|
358
|
+
throw new Error('gap_ms must be non-negative');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
assertEngine(segment.engine, `Segment at index ${idx}`);
|
|
363
|
+
assertLanguage(segment.language, `Segment at index ${idx}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gobing-ai/karakeep-local",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"typecheck": "tsc --noEmit"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@gobing-ai/kk-core": "workspace:*",
|
|
10
|
+
"@gobing-ai/ts-infra": "catalog:",
|
|
11
|
+
"@gobing-ai/ts-runtime": "catalog:",
|
|
12
|
+
"@gobing-ai/ts-utils": "catalog:"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.14"
|
|
16
|
+
}
|
|
17
|
+
}
|