@kmchai/cortico-world-voice 0.3.0 → 0.3.2

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 CHANGED
@@ -1,3 +1,12 @@
1
+ ## 0.3.2
2
+
3
+ - IndexTTS2 的模型提示词明确:`emotion` / `instruct` 传自然语言,由服务映射到 `emotion_text`;不要在正文写 `emotion_text` 或方括号标签。服务 0.4.2 起这两个 options 不再由模型页覆盖。
4
+
5
+ ## 0.3.1
6
+
7
+ - 自动朗读不再把 `end_turn` 当成实质工具调用。模型说完正文再调 `end_turn` 结束这一轮时,正文照常按概率朗读;仍会压掉与 `web_search` 等实际工具同轮出现的中间正文。
8
+ - 网关 0.1.1 已把 World 的 `outputTap` 扇进主 session;此前网关 bot 里概率朗读拿不到正文。
9
+
1
10
  ## 0.3.0
2
11
 
3
12
  - 系统 Config tab 移除「音色与情绪」组;音色、情绪控制、默认情绪、默认表演指令、语速与副语言标签移到「模型」页。
package/README.md CHANGED
@@ -77,7 +77,7 @@ World 页的「模型」tab 选模型与音色,并按服务 `/v1/models` 返回
77
77
 
78
78
  只想响应工具与显式请求、不要随机朗读:保留 `autoSpeak: true`,把 `autoProbability` 设为 `0`。把 `autoSpeak` 整个关掉会让 tap 不再挂载,显式关键词也就看不到正文了;这时只保留 `voice_speak` 工具触发。
79
79
 
80
- 自动朗读依赖 Persona 把已挂载 World 的 `outputTap()` 扇进主 session;仓内 `cormini` 与 Coopanion 都这样做。只想要工具触发时,把 `autoSpeak` 关掉即可。
80
+ 自动朗读依赖 Persona 把已挂载 World 的 `outputTap()` 扇进主 session;`cormini`、Coopanion 与 `Kmch AI · 网关` 0.1.1 起都这样做。只想要工具触发时,把 `autoSpeak` 关掉即可。模型说完正文再调 `end_turn` 不影响朗读;与搜索等实际工具同轮的中间正文仍不念。
81
81
 
82
82
  ## 情绪、语速与标签
83
83
 
@@ -91,7 +91,7 @@ World 页的「模型」tab 选模型与音色,并按服务 `/v1/models` 返回
91
91
  | `tags` | 不发情绪参数,只用正文标签。 | 正文标签。 |
92
92
  | `none` | 都不发。 | — |
93
93
 
94
- OpenAI 兼容条目把 `instructions` / `emotion` / `speed` 直接放进请求。其他模型可在服务面板里覆盖 profile 的字段映射,语音不用改配置。
94
+ OpenAI 兼容条目把 `instructions` / `emotion` / `speed` 直接放进请求。IndexTTS2 把 `instructions` 或 `emotion` 映射成 `use_emotion_text: true` 与 `emotion_text`;这两个参数由语音的「默认情绪 / 默认表演指令」或单句 `emotion` / `instruct` 驱动,不在模型参数面板里单独出现。其他模型可在服务面板里覆盖 profile 的字段映射,语音不用改配置。
95
95
 
96
96
  `response_format` 每次都带;`model` 与 `voice` 配了才带,工具参数里的音色优先于配置默认值。
97
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmchai/cortico-world-voice",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Cortico World: voice tools, probabilistic reading, emotion and paralinguistic tags, text filtering and local playback; speech is synthesized by the Kmch AI service over HTTP",
6
6
  "author": "kmchai",
package/src/models.ts CHANGED
@@ -28,7 +28,7 @@ const INDEXTTS2: ModelInfo = {
28
28
  match: ['index-tts2', 'index_tts2', 'index-tts2.5', 'index_tts2_5', '2.5'],
29
29
  label: 'IndexTTS2',
30
30
  tags: false,
31
- prompt: '情绪用自然语言(emotion_text),不要写 [Laughter] 这类方括号标签;采样/时长参数在语音模型页配置,不要写进正文.',
31
+ prompt: '情绪用 voice_speak 的 emotion 或 instruct 传自然语言,服务会映射到 emotion_text;不要在正文写 emotion_text 或 [Laughter] 这类方括号标签。采样/时长参数在语音模型页配置,不要写进正文.',
32
32
  };
33
33
 
34
34
  const GENERIC: ModelInfo = {
package/src/triggers.ts CHANGED
@@ -8,6 +8,8 @@ import type { VoiceConfigSection } from './types.ts';
8
8
 
9
9
  /** 工具参数里可能放正文的字段,按顺序取第一个字符串。 */
10
10
  const TEXT_FIELDS = ['script', 'question', 'text', 'content', 'message', 'body'] as const;
11
+ /** 只结束这一轮的控制工具,不算「这一轮做了别的事」。 */
12
+ const FLOW_TOOLS = new Set(['end_turn']);
11
13
 
12
14
  export function splitList(raw: string): string[] {
13
15
  return raw
@@ -60,15 +62,27 @@ interface StreamLike {
60
62
 
61
63
  export class VoiceTap implements OutputTap {
62
64
  private text = '';
63
- private calls = 0;
64
- private readonly names = new Map<number, string>();
65
+ /** 这一轮出现过的函数调用:output_index -> name;名字未知时存空串。 */
66
+ private readonly callNames = new Map<number, string>();
67
+ /** 没有 output_index 的函数调用;无法判断是不是 end_turn,按实质调用处理。 */
68
+ private orphanCall = false;
65
69
  private readonly emitted = new Set<number>();
66
70
 
67
71
  constructor(private readonly opts: TapOptions) {}
68
72
 
73
+ private noteCall(index: number, name: string): void {
74
+ if (index < 0) {
75
+ if (name && FLOW_TOOLS.has(name)) return;
76
+ this.orphanCall = true;
77
+ return;
78
+ }
79
+ if (name) this.callNames.set(index, name);
80
+ else if (!this.callNames.has(index)) this.callNames.set(index, '');
81
+ }
82
+
69
83
  /** 工具调用完成:同一个 output_index 只交出去一次。 */
70
84
  private takeCall(index: number, name: string, args: string): void {
71
- if (!name || name.startsWith('voice_')) return;
85
+ if (!name || name.startsWith('voice_') || FLOW_TOOLS.has(name)) return;
72
86
  if (this.emitted.has(index)) return;
73
87
  if (!toolNames(this.opts.cfg()).has(name)) return;
74
88
  const text = textOfToolArgs(args);
@@ -85,38 +99,39 @@ export class VoiceTap implements OutputTap {
85
99
  return;
86
100
  }
87
101
  if (e.type === 'response.output_item.added' && e.item?.type === 'function_call') {
88
- this.calls += 1;
89
- if (e.item.name) this.names.set(index, e.item.name);
102
+ this.noteCall(index, e.item.name ?? '');
90
103
  return;
91
104
  }
92
105
  // 有的传输只给参数结束事件,不带完整 item;名字从 added 里取。
93
106
  if (e.type === 'response.function_call_arguments.done') {
94
- const name = this.names.get(index) ?? '';
107
+ const name = this.callNames.get(index) ?? '';
108
+ this.noteCall(index, name);
95
109
  if (name) this.takeCall(index, name, e.arguments ?? '');
96
110
  return;
97
111
  }
98
112
  if (e.type === 'response.output_item.done' && e.item?.type === 'function_call') {
99
- this.calls += 1;
100
- const name = e.item.name ?? this.names.get(index) ?? '';
113
+ const name = e.item.name ?? this.callNames.get(index) ?? '';
114
+ this.noteCall(index, name);
101
115
  if (name) this.takeCall(index, name, e.item.arguments ?? '');
102
- this.names.delete(index);
103
116
  }
104
117
  }
105
118
 
106
119
  onRoundEnd(): void {
107
120
  const text = this.text;
108
121
  this.text = '';
109
- const pureText = this.calls === 0;
110
- this.calls = 0;
111
- this.names.clear();
122
+ // 只有结束轮次的调用不算实质动作;纯文本回复照样可以自动朗读。
123
+ const nonFlow = this.orphanCall
124
+ || [...this.callNames.values()].some((name) => name === '' || !FLOW_TOOLS.has(name));
125
+ this.callNames.clear();
126
+ this.orphanCall = false;
112
127
  this.emitted.clear();
113
- if (pureText && text.trim()) this.opts.onText(text, 'text');
128
+ if (!nonFlow && text.trim()) this.opts.onText(text, 'text');
114
129
  }
115
130
 
116
131
  onAbort(): void {
117
132
  this.text = '';
118
- this.calls = 0;
119
- this.names.clear();
133
+ this.callNames.clear();
134
+ this.orphanCall = false;
120
135
  this.emitted.clear();
121
136
  this.opts.onAbort?.();
122
137
  }