@kmchai/cortico-world-voice 0.1.0 → 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/src/config.ts CHANGED
@@ -4,19 +4,19 @@ import { pick, type Language, type LanguageTable } from 'cortico/core/language.t
4
4
  import type { VoiceConfigSection } from './types.ts';
5
5
 
6
6
  export const VOICE_ID = 'voice';
7
- /** 配置里 `x-options` 用的动态音色表。 */
8
- export const VOICE_OPTIONS_KIND = 'voice.voices';
7
+ /** World 定义、WebUI 与配置页共用同一个显示名。 */
8
+ export const VOICE_LABEL = 'Kmch AI · 语音';
9
9
 
10
10
  export const VOICE_DEFAULTS: VoiceConfigSection = {
11
11
  enabled: false,
12
12
 
13
- backend: 'audiocpp',
14
- baseUrl: 'http://127.0.0.1:8000',
15
- model: 'ming3-tts',
16
- voice: '4016',
17
- apiKeySecret: '',
13
+ serviceUrl: 'http://127.0.0.1:9883',
14
+ serviceTokenSecret: '',
15
+ model: '',
16
+ voice: '',
17
+ modelOptions: {},
18
18
  format: 'wav',
19
- timeoutMs: 120_000,
19
+ timeoutMs: 180_000,
20
20
 
21
21
  emotionMode: 'instruct',
22
22
  defaultEmotion: '',
@@ -47,51 +47,39 @@ export const VOICE_DEFAULTS: VoiceConfigSection = {
47
47
  chunkMax: 120,
48
48
  gapMs: 120,
49
49
  shareAudio: false,
50
-
51
- httpApi: false,
52
- httpPort: 9883,
53
- httpToken: '',
54
- httpPlay: false,
55
-
56
- singUrl: '',
57
- singToken: '',
58
-
59
- customUrl: '',
60
- customMethod: 'POST',
61
- customHeaders: '',
62
- customBody: '',
63
- customResponse: 'audio',
64
- customAudioPath: 'audio',
65
50
  };
66
51
 
67
52
  const TEXT = {
68
53
  zh: {
69
- label: '语音',
54
+ label: 'Kmch AI · 语音',
70
55
  envTitle: '语音 · 环境提示词',
71
- lampBackend: '语音服务',
56
+ lampService: '语音服务',
72
57
  lampQueue: '朗读队列',
73
- lampApi: '本地接口',
74
- lampSinger: '唱歌',
58
+ servicePage: '服务设置',
59
+ modelPanel: '模型',
60
+ modelSaveShape: '保存失败:参数格式不对。',
61
+ modelSaveModel: '保存失败:模型 id 不能为空。',
62
+ modelSaveVoice: '保存失败:音色必须是字符串。',
63
+ modelSaveOptions: '保存失败:模型参数必须是对象。',
64
+ modelUnknownMethod: '未知的面板方法。',
75
65
  connection: '连接',
76
- connectionDesc: '语音服务地址与协议。',
77
- backend: '协议',
78
- baseUrl: '服务地址',
79
- model: '模型',
80
- voice: '音色',
81
- apiKeySecret: '密钥变量名',
82
- apiKeySecretDesc: '部署 .env 里的变量名,留空表示不需要。',
66
+ connectionDesc: '语音服务的 HTTP 地址与令牌。',
67
+ serviceUrl: '服务地址',
68
+ serviceUrlDesc: 'Kmch AI · 服务 的网关地址。',
69
+ serviceTokenSecret: '服务令牌变量名',
70
+ serviceTokenSecretDesc: '部署 .env 里的变量名,留空表示服务不校验。',
83
71
  format: '音频格式',
84
72
  timeoutMs: '超时(毫秒)',
85
73
  style: '音色与情绪',
86
74
  styleDesc: '',
87
75
  emotionMode: '情绪控制',
88
- emotionModeDesc: 'instruct 折成自然语言;categorical 只发情绪标签;basic 只发语速/音调/音量;tags 只用正文标签;none 不发。',
76
+ emotionModeDesc: 'instruct 折成自然语言指令;categorical 只发情绪标签;basic 只发语速;tags 只用正文标签;none 不发。',
89
77
  defaultEmotion: '默认情绪',
90
78
  defaultInstruct: '默认表演指令',
91
79
  speed: '语速',
92
80
  paralinguistic: '副语言标签',
93
- paralinguisticDesc: 'auto 只在支持标签的后端保留;strip 一律从朗读文本里删掉。',
94
- triggers: '触发与过滤',
81
+ paralinguisticDesc: 'auto 只在当前模型支持标签时保留;keep 一律保留;strip 一律从朗读文本里删掉。',
82
+ triggers: '触发',
95
83
  triggersDesc: '',
96
84
  autoSpeak: '自动朗读',
97
85
  autoProbability: '自动朗读概率',
@@ -106,6 +94,8 @@ const TEXT = {
106
94
  explicitWindowSec: '回看秒数',
107
95
  minChars: '最短字数',
108
96
  maxChars: '最长字数',
97
+ filter: '文本过滤',
98
+ filterDesc: '',
109
99
  filterCode: '过滤代码块',
110
100
  filterUrls: '过滤网址',
111
101
  filterMarkdown: '过滤 Markdown',
@@ -114,7 +104,7 @@ const TEXT = {
114
104
  filterEmoji: '过滤表情与符号',
115
105
  filterSymbols: '额外过滤字符',
116
106
  filterSymbolsDesc: '按字面删除,可用逗号/分号/换行分隔。',
117
- output: '输出与接口',
107
+ output: '输出',
118
108
  outputDesc: '',
119
109
  playback: '播放方式',
120
110
  playbackDesc: 'local 用本机播放器;event 发 tts.voice 事件交给别的插件;none 只合成。',
@@ -122,52 +112,37 @@ const TEXT = {
122
112
  chunkMax: '单块字数',
123
113
  gapMs: '块间停顿(毫秒)',
124
114
  shareAudio: '回执带音频附件',
125
- httpApi: '开放本地语音接口',
126
- httpPort: '接口端口',
127
- httpToken: '接口令牌',
128
- httpTokenDesc: '留空不校验;只监听 127.0.0.1。',
129
- httpPlay: '接口合成也本机播放',
130
- sing: '唱歌',
131
- singDesc: '接到唱歌插件的 HTTP 接口;留空则 voice_sing 回失败。',
132
- singUrl: '唱歌服务地址',
133
- singToken: '唱歌令牌',
134
- custom: 'custom 协议',
135
- customDesc: '自定义 JSON 请求;占位符 {{text}} {{voice}} {{emotion}} {{instruct}} {{model}} {{speed}} {{format}}。',
136
- customUrl: '完整地址',
137
- customMethod: '方法',
138
- customHeaders: '请求头(JSON)',
139
- customHeadersDesc: '值里的 ${KEY} 换成 apiKeySecret 读到的密钥。',
140
- customBody: '请求体模板(JSON)',
141
- customResponse: '音频位置',
142
- customAudioPath: '音频字段路径',
143
115
  },
144
116
  en: {
145
- label: 'Voice',
117
+ label: 'Kmch AI · 语音',
146
118
  envTitle: 'Voice · environment prompt',
147
- lampBackend: 'Speech service',
119
+ lampService: 'Speech service',
148
120
  lampQueue: 'Speech queue',
149
- lampApi: 'Local API',
150
- lampSinger: 'Singing',
121
+ servicePage: 'Service settings',
122
+ modelPanel: '模型',
123
+ modelSaveShape: 'Save failed: invalid arguments.',
124
+ modelSaveModel: 'Save failed: the model id must not be empty.',
125
+ modelSaveVoice: 'Save failed: voice must be a string.',
126
+ modelSaveOptions: 'Save failed: model options must be an object.',
127
+ modelUnknownMethod: 'Unknown panel method.',
151
128
  connection: 'Connection',
152
- connectionDesc: 'Where the speech service is and which protocol it speaks.',
153
- backend: 'Protocol',
154
- baseUrl: 'Base URL',
155
- model: 'Model',
156
- voice: 'Voice',
157
- apiKeySecret: 'Secret variable',
158
- apiKeySecretDesc: 'Name in the deployment .env; empty means none.',
129
+ connectionDesc: 'Where the speech service is and which token it expects.',
130
+ serviceUrl: 'Service URL',
131
+ serviceUrlDesc: 'Gateway address of Kmch AI · Service.',
132
+ serviceTokenSecret: 'Service token variable',
133
+ serviceTokenSecretDesc: 'Name in the deployment .env; empty means the service does not check.',
159
134
  format: 'Audio format',
160
135
  timeoutMs: 'Timeout (ms)',
161
136
  style: 'Voice and emotion',
162
137
  styleDesc: '',
163
138
  emotionMode: 'Emotion control',
164
- emotionModeDesc: 'instruct turns it into natural language; categorical sends a label; basic sends speed/pitch/volume; tags uses inline tags only; none sends nothing.',
139
+ emotionModeDesc: 'instruct turns it into natural language; categorical sends a label; basic sends speed; tags uses inline tags only; none sends nothing.',
165
140
  defaultEmotion: 'Default emotion',
166
141
  defaultInstruct: 'Default direction',
167
142
  speed: 'Speed',
168
143
  paralinguistic: 'Paralinguistic tags',
169
- paralinguisticDesc: 'auto keeps them only on backends that support them; strip removes them from the spoken text.',
170
- triggers: 'Triggers and filters',
144
+ paralinguisticDesc: 'auto keeps them only when the current model supports them; keep always keeps; strip removes them from the spoken text.',
145
+ triggers: 'Triggers',
171
146
  triggersDesc: '',
172
147
  autoSpeak: 'Automatic speech',
173
148
  autoProbability: 'Automatic probability',
@@ -182,6 +157,8 @@ const TEXT = {
182
157
  explicitWindowSec: 'Look-back seconds',
183
158
  minChars: 'Minimum characters',
184
159
  maxChars: 'Maximum characters',
160
+ filter: 'Text filter',
161
+ filterDesc: '',
185
162
  filterCode: 'Filter code blocks',
186
163
  filterUrls: 'Filter URLs',
187
164
  filterMarkdown: 'Filter Markdown',
@@ -190,7 +167,7 @@ const TEXT = {
190
167
  filterEmoji: 'Filter emoji and symbols',
191
168
  filterSymbols: 'Extra filtered characters',
192
169
  filterSymbolsDesc: 'Removed literally; comma/semicolon/newline separated.',
193
- output: 'Output and API',
170
+ output: 'Output',
194
171
  outputDesc: '',
195
172
  playback: 'Playback',
196
173
  playbackDesc: 'local uses the built-in player; event emits tts.voice for another plugin; none only synthesizes.',
@@ -198,24 +175,6 @@ const TEXT = {
198
175
  chunkMax: 'Characters per chunk',
199
176
  gapMs: 'Gap between chunks (ms)',
200
177
  shareAudio: 'Attach audio to tool results',
201
- httpApi: 'Serve the local speech API',
202
- httpPort: 'API port',
203
- httpToken: 'API token',
204
- httpTokenDesc: 'Empty means no check; loopback only.',
205
- httpPlay: 'Play API requests locally too',
206
- sing: 'Singing',
207
- singDesc: 'HTTP endpoint of a singing plugin; empty makes voice_sing fail.',
208
- singUrl: 'Singing service URL',
209
- singToken: 'Singing token',
210
- custom: 'custom protocol',
211
- customDesc: 'Custom JSON request; placeholders {{text}} {{voice}} {{emotion}} {{instruct}} {{model}} {{speed}} {{format}}.',
212
- customUrl: 'Full URL',
213
- customMethod: 'Method',
214
- customHeaders: 'Headers (JSON)',
215
- customHeadersDesc: '${KEY} in a value is replaced with the secret named by apiKeySecret.',
216
- customBody: 'Body template (JSON)',
217
- customResponse: 'Audio location',
218
- customAudioPath: 'Audio field path',
219
178
  },
220
179
  } as const;
221
180
 
@@ -238,11 +197,8 @@ export function voiceConfigGroups(language: Language): ConfigGroup[] {
238
197
  title: t.connection,
239
198
  description: t.connectionDesc,
240
199
  properties: {
241
- [`${p}backend`]: { type: 'string', title: t.backend, enum: ['audiocpp', 'openai', 'custom'], 'x-hot': true },
242
- [`${p}baseUrl`]: { type: 'string', title: t.baseUrl, 'x-hot': true },
243
- [`${p}model`]: { type: 'string', title: t.model, 'x-hot': true },
244
- [`${p}voice`]: { type: 'string', title: t.voice, 'x-options': VOICE_OPTIONS_KIND, 'x-hot': true },
245
- [`${p}apiKeySecret`]: { type: 'string', title: t.apiKeySecret, description: t.apiKeySecretDesc, 'x-hot': false },
200
+ [`${p}serviceUrl`]: { type: 'string', title: t.serviceUrl, description: t.serviceUrlDesc, 'x-hot': true },
201
+ [`${p}serviceTokenSecret`]: { type: 'string', title: t.serviceTokenSecret, description: t.serviceTokenSecretDesc, 'x-hot': true },
246
202
  [`${p}format`]: { type: 'string', title: t.format, enum: ['wav', 'mp3'], 'x-hot': true },
247
203
  [`${p}timeoutMs`]: { type: 'integer', title: t.timeoutMs, minimum: 1000, maximum: 600_000, 'x-hot': true },
248
204
  },
@@ -280,6 +236,16 @@ export function voiceConfigGroups(language: Language): ConfigGroup[] {
280
236
  [`${p}explicitWindowSec`]: { type: 'integer', title: t.explicitWindowSec, minimum: 0, maximum: 3600, 'x-hot': true },
281
237
  [`${p}minChars`]: { type: 'integer', title: t.minChars, minimum: 1, maximum: 100, 'x-hot': true },
282
238
  [`${p}maxChars`]: { type: 'integer', title: t.maxChars, minimum: 20, maximum: 8000, 'x-hot': true },
239
+ },
240
+ },
241
+ },
242
+ {
243
+ id: 'world:voice-filter',
244
+ owner: 'world:voice',
245
+ schema: {
246
+ type: 'object',
247
+ title: t.filter,
248
+ properties: {
283
249
  [`${p}filterCode`]: { type: 'boolean', title: t.filterCode, 'x-hot': true },
284
250
  [`${p}filterUrls`]: { type: 'boolean', title: t.filterUrls, 'x-hot': true },
285
251
  [`${p}filterMarkdown`]: { type: 'boolean', title: t.filterMarkdown, 'x-hot': true },
@@ -301,29 +267,6 @@ export function voiceConfigGroups(language: Language): ConfigGroup[] {
301
267
  [`${p}chunkMax`]: { type: 'integer', title: t.chunkMax, minimum: 20, maximum: 500, 'x-hot': true },
302
268
  [`${p}gapMs`]: { type: 'integer', title: t.gapMs, minimum: 0, maximum: 5000, 'x-hot': true },
303
269
  [`${p}shareAudio`]: { type: 'boolean', title: t.shareAudio, 'x-hot': true },
304
- [`${p}httpApi`]: { type: 'boolean', title: t.httpApi, 'x-hot': false },
305
- [`${p}httpPort`]: { type: 'integer', title: t.httpPort, minimum: 1, maximum: 65535, 'x-hot': false },
306
- [`${p}httpToken`]: { type: 'string', title: t.httpToken, description: t.httpTokenDesc, 'x-hot': false },
307
- [`${p}httpPlay`]: { type: 'boolean', title: t.httpPlay, 'x-hot': true },
308
- [`${p}singUrl`]: { type: 'string', title: t.singUrl, description: t.singDesc, 'x-hot': true },
309
- [`${p}singToken`]: { type: 'string', title: t.singToken, 'x-hot': true },
310
- },
311
- },
312
- },
313
- {
314
- id: 'world:voice-custom',
315
- owner: 'world:voice',
316
- schema: {
317
- type: 'object',
318
- title: t.custom,
319
- description: t.customDesc,
320
- properties: {
321
- [`${p}customUrl`]: { type: 'string', title: t.customUrl, 'x-hot': true },
322
- [`${p}customMethod`]: { type: 'string', title: t.customMethod, enum: ['POST', 'GET'], 'x-hot': true },
323
- [`${p}customHeaders`]: { type: 'string', title: t.customHeaders, description: t.customHeadersDesc, 'x-hot': true },
324
- [`${p}customBody`]: { type: 'string', title: t.customBody, 'x-hot': true },
325
- [`${p}customResponse`]: { type: 'string', title: t.customResponse, enum: ['audio', 'base64', 'url'], 'x-hot': true },
326
- [`${p}customAudioPath`]: { type: 'string', title: t.customAudioPath, 'x-hot': true },
327
270
  },
328
271
  },
329
272
  },
package/src/definition.ts CHANGED
@@ -1,15 +1,13 @@
1
1
  import { describeTools, type WorldDefinition } from 'cortico/world.ts';
2
- import { VOICE_DEFAULTS, VOICE_ID, VOICE_OPTIONS_KIND } from './config.ts';
3
- import { currentVoiceOptions } from './options.ts';
2
+ import { VOICE_DEFAULTS, VOICE_ID, VOICE_LABEL } from './config.ts';
4
3
  import { VOICE_TOOL_DECLS } from './tools.ts';
5
4
  import type { VoiceConfigSection } from './types.ts';
6
5
  import { VoiceWorld } from './world.ts';
7
6
 
8
7
  export const VOICE: WorldDefinition<VoiceConfigSection> = {
9
8
  id: VOICE_ID,
10
- label: 'Voice',
11
- defaults: () => ({ ...VOICE_DEFAULTS }),
12
- configOptions: (kind) => (kind === VOICE_OPTIONS_KIND ? currentVoiceOptions() : []),
9
+ label: VOICE_LABEL,
10
+ defaults: () => ({ ...VOICE_DEFAULTS, modelOptions: {} }),
13
11
  create: (ctx) => new VoiceWorld({
14
12
  cfg: ctx.cfg,
15
13
  timezone: ctx.timezone,
@@ -20,7 +18,6 @@ export const VOICE: WorldDefinition<VoiceConfigSection> = {
20
18
  describe: {
21
19
  tools: describeTools(VOICE_TOOL_DECLS, {
22
20
  voice_speak: { zh: '现在念一句;可指定情绪、表演指令、音色与语速,也可以只合成不出声。', en: 'Say one line now, optionally with an emotion, direction, voice and speed, or synthesize it silently.' },
23
- voice_sing: { zh: '把一句歌词交给唱歌插件唱出来。', en: 'Sing one lyric line through the configured singing service.' },
24
21
  voice_hush: { zh: '立刻停声并清空排队。', en: 'Stop the voice and drop queued lines.' },
25
22
  voice_style: { zh: '读或改自动朗读使用的长期音色与情绪。', en: 'Read or change the standing voice style used by automatic reading.' },
26
23
  }),
package/src/models.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 模型能力与提示词片段。
3
+ *
4
+ * 服务返回的模型 id 决定两件事:正文里能不能写副语言标签,以及环境提示词
5
+ * 该怎么描述文本与情绪。按 id 前缀匹配,大小写不敏感;空模型与未知模型按
6
+ * 通用 OpenAI 兼容模型处理。
7
+ */
8
+
9
+ interface ModelInfo {
10
+ /** 模型 id 的前缀,小写。 */
11
+ readonly match: readonly string[];
12
+ /** 给人看的名字;未知模型为空。 */
13
+ readonly label: string;
14
+ /** 是否支持 `[Laughter]` 这类副语言标签。 */
15
+ readonly tags: boolean;
16
+ /** 环境提示词里的文本与情绪写法。 */
17
+ readonly prompt: string;
18
+ }
19
+
20
+ const MING3: ModelInfo = {
21
+ match: ['ming3', 'ming-3', 'ming_3'],
22
+ label: 'Ming 3.0',
23
+ tags: true,
24
+ prompt: '支持 [Laughter] [Sigh] [Breathing] [Uhm] [Question-ei] 等副语言标签;情绪可用 instruct/emotion.',
25
+ };
26
+
27
+ const INDEXTTS2: ModelInfo = {
28
+ match: ['index-tts2', 'index_tts2', 'index-tts2.5', 'index_tts2_5', '2.5'],
29
+ label: 'IndexTTS2',
30
+ tags: false,
31
+ prompt: '情绪用自然语言(emotion_text),不要写 [Laughter] 这类方括号标签;采样/时长参数在语音模型页配置,不要写进正文.',
32
+ };
33
+
34
+ const GENERIC: ModelInfo = {
35
+ match: [],
36
+ label: '',
37
+ tags: false,
38
+ prompt: '用 instructions 描述语气;不要用方括号标签,除非确认模型支持.',
39
+ };
40
+
41
+ const KNOWN: readonly ModelInfo[] = [MING3, INDEXTTS2];
42
+
43
+ function normalize(model: string): string {
44
+ return typeof model === 'string' ? model.trim().toLowerCase() : '';
45
+ }
46
+
47
+ function profileOf(model: string): ModelInfo | null {
48
+ const id = normalize(model);
49
+ if (!id) return null;
50
+ return KNOWN.find((info) => info.match.some((prefix) => id.startsWith(prefix))) ?? null;
51
+ }
52
+
53
+ /** 当前模型是否支持副语言标签;未知与空模型按通用模型处理,返回 false。 */
54
+ export function modelSupportsTags(model: string): boolean {
55
+ return profileOf(model)?.tags ?? GENERIC.tags;
56
+ }
57
+
58
+ /** 给人看的模型名;未知或空模型返回空串,由调用方退回模型 id。 */
59
+ export function modelLabel(model: string): string {
60
+ return profileOf(model)?.label ?? '';
61
+ }
62
+
63
+ /** 环境提示词里的文本与情绪写法;空模型与未知模型用通用片段。 */
64
+ export function modelPrompt(model: string): string {
65
+ return (profileOf(model) ?? GENERIC).prompt;
66
+ }
package/src/panel.ts ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 「模型」面板的数据形状。服务端经由 `World.webUi().invoke` 构造,浏览器端只
3
+ * `import type` 这些声明;面板不接触服务地址与令牌。
4
+ */
5
+ import type { VoiceModel } from './types.ts';
6
+
7
+ export interface PanelModelState {
8
+ /** `worlds.voice.model`。 */
9
+ model: string;
10
+ /** `worlds.voice.voice`。 */
11
+ voice: string;
12
+ /** 当前模型的环境提示词片段,来自 `models.ts`。 */
13
+ prompt: string;
14
+ /** 当前模型已保存的请求参数。 */
15
+ options: Record<string, unknown>;
16
+ /** 服务目录;服务不可达时是上一次可用的结果。 */
17
+ models: VoiceModel[];
18
+ /** 最近一次读取服务目录失败的原因。 */
19
+ error?: string;
20
+ }
21
+
22
+ /** `invoke('model','save', [save])` 的请求体。 */
23
+ export interface PanelModelSave {
24
+ model: string;
25
+ voice: string;
26
+ options: Record<string, unknown>;
27
+ }
28
+
29
+ /** 只认普通对象:数组、null 与类对象都不算。 */
30
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
31
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
32
+ const proto = Object.getPrototypeOf(value);
33
+ return proto === Object.prototype || proto === null;
34
+ }