@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/CHANGELOG.md +31 -0
- package/README.md +83 -70
- package/describe.json +2 -27
- package/dist/webui.css +1 -0
- package/dist/webui.js +1 -0
- package/package.json +33 -6
- package/src/ENV_PROMPT.md +6 -7
- package/src/audio/queue.ts +1 -1
- package/src/config.ts +60 -117
- package/src/definition.ts +3 -6
- package/src/models.ts +66 -0
- package/src/panel.ts +34 -0
- package/src/service.ts +371 -0
- package/src/speech/style.ts +2 -14
- package/src/speech/tags.ts +8 -6
- package/src/speech/text.ts +4 -1
- package/src/tools.ts +1 -16
- package/src/types.ts +46 -49
- package/src/world.ts +219 -267
- package/src/backends/audiocpp.ts +0 -98
- package/src/backends/custom.ts +0 -132
- package/src/backends/http.ts +0 -91
- package/src/backends/index.ts +0 -21
- package/src/backends/openai.ts +0 -51
- package/src/http.ts +0 -205
- package/src/options.ts +0 -15
- package/src/sing.ts +0 -73
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
|
-
/**
|
|
8
|
-
export const
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
model: '
|
|
16
|
-
voice: '
|
|
17
|
-
|
|
13
|
+
serviceUrl: 'http://127.0.0.1:9883',
|
|
14
|
+
serviceTokenSecret: '',
|
|
15
|
+
model: '',
|
|
16
|
+
voice: '',
|
|
17
|
+
modelOptions: {},
|
|
18
18
|
format: 'wav',
|
|
19
|
-
timeoutMs:
|
|
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
|
-
|
|
56
|
+
lampService: '语音服务',
|
|
72
57
|
lampQueue: '朗读队列',
|
|
73
|
-
|
|
74
|
-
|
|
58
|
+
servicePage: '服务设置',
|
|
59
|
+
modelPanel: '模型',
|
|
60
|
+
modelSaveShape: '保存失败:参数格式不对。',
|
|
61
|
+
modelSaveModel: '保存失败:模型 id 不能为空。',
|
|
62
|
+
modelSaveVoice: '保存失败:音色必须是字符串。',
|
|
63
|
+
modelSaveOptions: '保存失败:模型参数必须是对象。',
|
|
64
|
+
modelUnknownMethod: '未知的面板方法。',
|
|
75
65
|
connection: '连接',
|
|
76
|
-
connectionDesc: '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
76
|
+
emotionModeDesc: 'instruct 折成自然语言指令;categorical 只发情绪标签;basic 只发语速;tags 只用正文标签;none 不发。',
|
|
89
77
|
defaultEmotion: '默认情绪',
|
|
90
78
|
defaultInstruct: '默认表演指令',
|
|
91
79
|
speed: '语速',
|
|
92
80
|
paralinguistic: '副语言标签',
|
|
93
|
-
paralinguisticDesc: 'auto
|
|
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: '
|
|
117
|
+
label: 'Kmch AI · 语音',
|
|
146
118
|
envTitle: 'Voice · environment prompt',
|
|
147
|
-
|
|
119
|
+
lampService: 'Speech service',
|
|
148
120
|
lampQueue: 'Speech queue',
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
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
|
|
170
|
-
triggers: 'Triggers
|
|
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
|
|
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}
|
|
242
|
-
[`${p}
|
|
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,
|
|
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:
|
|
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
|
+
}
|