@downcity/plugins 1.0.56 → 1.0.57

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.
Files changed (201) hide show
  1. package/bin/BuiltinPlugins.d.ts.map +1 -1
  2. package/bin/BuiltinPlugins.js +0 -4
  3. package/bin/BuiltinPlugins.js.map +1 -1
  4. package/bin/asr/Plugin.d.ts +67 -7
  5. package/bin/asr/Plugin.d.ts.map +1 -1
  6. package/bin/asr/Plugin.js +229 -461
  7. package/bin/asr/Plugin.js.map +1 -1
  8. package/bin/asr/types/AsrPlugin.d.ts +114 -0
  9. package/bin/asr/types/AsrPlugin.d.ts.map +1 -0
  10. package/bin/asr/types/AsrPlugin.js +10 -0
  11. package/bin/asr/types/AsrPlugin.js.map +1 -0
  12. package/bin/image/ImagePlugin.d.ts +1 -1
  13. package/bin/image/ImagePlugin.d.ts.map +1 -1
  14. package/bin/image/ImagePlugin.js +23 -5
  15. package/bin/image/ImagePlugin.js.map +1 -1
  16. package/bin/index.d.ts +2 -0
  17. package/bin/index.d.ts.map +1 -1
  18. package/bin/tts/Plugin.d.ts +53 -6
  19. package/bin/tts/Plugin.d.ts.map +1 -1
  20. package/bin/tts/Plugin.js +197 -474
  21. package/bin/tts/Plugin.js.map +1 -1
  22. package/bin/tts/types/TtsPlugin.d.ts +63 -102
  23. package/bin/tts/types/TtsPlugin.d.ts.map +1 -1
  24. package/bin/tts/types/TtsPlugin.js +4 -3
  25. package/bin/tts/types/TtsPlugin.js.map +1 -1
  26. package/bin/web/PROMPT.d.ts +1 -1
  27. package/bin/web/PROMPT.d.ts.map +1 -1
  28. package/bin/web/PROMPT.js +1 -1
  29. package/bin/web/PROMPT.js.map +1 -1
  30. package/bin/web/Plugin.d.ts +66 -5
  31. package/bin/web/Plugin.d.ts.map +1 -1
  32. package/bin/web/Plugin.js +126 -450
  33. package/bin/web/Plugin.js.map +1 -1
  34. package/bin/web/WebPromptAssets.d.ts +1 -9
  35. package/bin/web/WebPromptAssets.d.ts.map +1 -1
  36. package/bin/web/WebPromptAssets.js +1 -11
  37. package/bin/web/WebPromptAssets.js.map +1 -1
  38. package/bin/web/runtime/Install.d.ts +19 -0
  39. package/bin/web/runtime/Install.d.ts.map +1 -0
  40. package/bin/web/runtime/Install.js +178 -0
  41. package/bin/web/runtime/Install.js.map +1 -0
  42. package/bin/web/types/WebPlugin.d.ts +37 -109
  43. package/bin/web/types/WebPlugin.d.ts.map +1 -1
  44. package/bin/web/types/WebPlugin.js +5 -7
  45. package/bin/web/types/WebPlugin.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/BuiltinPlugins.ts +0 -4
  48. package/src/asr/Plugin.ts +264 -483
  49. package/src/asr/types/AsrPlugin.ts +118 -0
  50. package/src/image/ImagePlugin.ts +23 -5
  51. package/src/index.ts +12 -0
  52. package/src/tts/Plugin.ts +225 -492
  53. package/src/tts/types/TtsPlugin.ts +67 -102
  54. package/src/web/PROMPT.ts +1 -1
  55. package/src/web/PROMPT.ts.txt +32 -6
  56. package/src/web/Plugin.ts +119 -453
  57. package/src/web/WebPromptAssets.ts +1 -13
  58. package/src/web/runtime/Install.ts +241 -0
  59. package/src/web/types/WebPlugin.ts +37 -113
  60. package/bin/asr/Config.d.ts +0 -43
  61. package/bin/asr/Config.d.ts.map +0 -1
  62. package/bin/asr/Config.js +0 -107
  63. package/bin/asr/Config.js.map +0 -1
  64. package/bin/asr/Dependency.d.ts +0 -77
  65. package/bin/asr/Dependency.d.ts.map +0 -1
  66. package/bin/asr/Dependency.js +0 -238
  67. package/bin/asr/Dependency.js.map +0 -1
  68. package/bin/asr/InboundAugment.d.ts +0 -17
  69. package/bin/asr/InboundAugment.d.ts.map +0 -1
  70. package/bin/asr/InboundAugment.js +0 -47
  71. package/bin/asr/InboundAugment.js.map +0 -1
  72. package/bin/asr/ModelCatalog.d.ts +0 -29
  73. package/bin/asr/ModelCatalog.d.ts.map +0 -1
  74. package/bin/asr/ModelCatalog.js +0 -25
  75. package/bin/asr/ModelCatalog.js.map +0 -1
  76. package/bin/tts/Dependency.d.ts +0 -90
  77. package/bin/tts/Dependency.d.ts.map +0 -1
  78. package/bin/tts/Dependency.js +0 -344
  79. package/bin/tts/Dependency.js.map +0 -1
  80. package/bin/tts/PluginSupport.d.ts +0 -25
  81. package/bin/tts/PluginSupport.d.ts.map +0 -1
  82. package/bin/tts/PluginSupport.js +0 -72
  83. package/bin/tts/PluginSupport.js.map +0 -1
  84. package/bin/tts/runtime/Catalog.d.ts +0 -21
  85. package/bin/tts/runtime/Catalog.d.ts.map +0 -1
  86. package/bin/tts/runtime/Catalog.js +0 -90
  87. package/bin/tts/runtime/Catalog.js.map +0 -1
  88. package/bin/tts/runtime/DependencyInstaller.d.ts +0 -143
  89. package/bin/tts/runtime/DependencyInstaller.d.ts.map +0 -1
  90. package/bin/tts/runtime/DependencyInstaller.js +0 -261
  91. package/bin/tts/runtime/DependencyInstaller.js.map +0 -1
  92. package/bin/tts/runtime/Installer.d.ts +0 -89
  93. package/bin/tts/runtime/Installer.d.ts.map +0 -1
  94. package/bin/tts/runtime/Installer.js +0 -188
  95. package/bin/tts/runtime/Installer.js.map +0 -1
  96. package/bin/tts/runtime/Paths.d.ts +0 -20
  97. package/bin/tts/runtime/Paths.d.ts.map +0 -1
  98. package/bin/tts/runtime/Paths.js +0 -32
  99. package/bin/tts/runtime/Paths.js.map +0 -1
  100. package/bin/tts/runtime/Synthesizer.d.ts +0 -44
  101. package/bin/tts/runtime/Synthesizer.d.ts.map +0 -1
  102. package/bin/tts/runtime/Synthesizer.js +0 -363
  103. package/bin/tts/runtime/Synthesizer.js.map +0 -1
  104. package/bin/tts/types/Tts.d.ts +0 -91
  105. package/bin/tts/types/Tts.d.ts.map +0 -1
  106. package/bin/tts/types/Tts.js +0 -9
  107. package/bin/tts/types/Tts.js.map +0 -1
  108. package/bin/voice/Config.d.ts +0 -43
  109. package/bin/voice/Config.d.ts.map +0 -1
  110. package/bin/voice/Config.js +0 -104
  111. package/bin/voice/Config.js.map +0 -1
  112. package/bin/voice/Dependency.d.ts +0 -77
  113. package/bin/voice/Dependency.d.ts.map +0 -1
  114. package/bin/voice/Dependency.js +0 -237
  115. package/bin/voice/Dependency.js.map +0 -1
  116. package/bin/voice/InboundAugment.d.ts +0 -17
  117. package/bin/voice/InboundAugment.d.ts.map +0 -1
  118. package/bin/voice/InboundAugment.js +0 -47
  119. package/bin/voice/InboundAugment.js.map +0 -1
  120. package/bin/voice/ModelCatalog.d.ts +0 -29
  121. package/bin/voice/ModelCatalog.d.ts.map +0 -1
  122. package/bin/voice/ModelCatalog.js +0 -25
  123. package/bin/voice/ModelCatalog.js.map +0 -1
  124. package/bin/voice/runtime/Catalog.d.ts +0 -18
  125. package/bin/voice/runtime/Catalog.d.ts.map +0 -1
  126. package/bin/voice/runtime/Catalog.js +0 -61
  127. package/bin/voice/runtime/Catalog.js.map +0 -1
  128. package/bin/voice/runtime/DependencyInstaller.d.ts +0 -145
  129. package/bin/voice/runtime/DependencyInstaller.d.ts.map +0 -1
  130. package/bin/voice/runtime/DependencyInstaller.js +0 -309
  131. package/bin/voice/runtime/DependencyInstaller.js.map +0 -1
  132. package/bin/voice/runtime/Installer.d.ts +0 -94
  133. package/bin/voice/runtime/Installer.d.ts.map +0 -1
  134. package/bin/voice/runtime/Installer.js +0 -200
  135. package/bin/voice/runtime/Installer.js.map +0 -1
  136. package/bin/voice/runtime/Paths.d.ts +0 -8
  137. package/bin/voice/runtime/Paths.d.ts.map +0 -1
  138. package/bin/voice/runtime/Paths.js +0 -26
  139. package/bin/voice/runtime/Paths.js.map +0 -1
  140. package/bin/voice/runtime/Transcriber.d.ts +0 -57
  141. package/bin/voice/runtime/Transcriber.d.ts.map +0 -1
  142. package/bin/voice/runtime/Transcriber.js +0 -329
  143. package/bin/voice/runtime/Transcriber.js.map +0 -1
  144. package/bin/voice/types/Voice.d.ts +0 -58
  145. package/bin/voice/types/Voice.d.ts.map +0 -1
  146. package/bin/voice/types/Voice.js +0 -9
  147. package/bin/voice/types/Voice.js.map +0 -1
  148. package/bin/voice/types/VoicePlugin.d.ts +0 -190
  149. package/bin/voice/types/VoicePlugin.d.ts.map +0 -1
  150. package/bin/voice/types/VoicePlugin.js +0 -9
  151. package/bin/voice/types/VoicePlugin.js.map +0 -1
  152. package/bin/web/Dependency.d.ts +0 -10
  153. package/bin/web/Dependency.d.ts.map +0 -1
  154. package/bin/web/Dependency.js +0 -10
  155. package/bin/web/Dependency.js.map +0 -1
  156. package/bin/web/PROMPT.agent-browser.d.ts +0 -7
  157. package/bin/web/PROMPT.agent-browser.d.ts.map +0 -1
  158. package/bin/web/PROMPT.agent-browser.js +0 -8
  159. package/bin/web/PROMPT.agent-browser.js.map +0 -1
  160. package/bin/web/PROMPT.web-access.d.ts +0 -7
  161. package/bin/web/PROMPT.web-access.d.ts.map +0 -1
  162. package/bin/web/PROMPT.web-access.js +0 -8
  163. package/bin/web/PROMPT.web-access.js.map +0 -1
  164. package/bin/web/runtime/Config.d.ts +0 -21
  165. package/bin/web/runtime/Config.d.ts.map +0 -1
  166. package/bin/web/runtime/Config.js +0 -79
  167. package/bin/web/runtime/Config.js.map +0 -1
  168. package/bin/web/runtime/Source.d.ts +0 -29
  169. package/bin/web/runtime/Source.d.ts.map +0 -1
  170. package/bin/web/runtime/Source.js +0 -209
  171. package/bin/web/runtime/Source.js.map +0 -1
  172. package/src/asr/Config.ts +0 -138
  173. package/src/asr/Dependency.ts +0 -336
  174. package/src/asr/InboundAugment.ts +0 -59
  175. package/src/asr/ModelCatalog.ts +0 -43
  176. package/src/tts/Dependency.ts +0 -473
  177. package/src/tts/PluginSupport.ts +0 -85
  178. package/src/tts/runtime/Catalog.ts +0 -97
  179. package/src/tts/runtime/DependencyInstaller.ts +0 -436
  180. package/src/tts/runtime/Installer.ts +0 -297
  181. package/src/tts/runtime/Paths.ts +0 -39
  182. package/src/tts/runtime/Synthesizer.ts +0 -480
  183. package/src/tts/types/Tts.ts +0 -99
  184. package/src/voice/Config.ts +0 -135
  185. package/src/voice/Dependency.ts +0 -329
  186. package/src/voice/InboundAugment.ts +0 -59
  187. package/src/voice/ModelCatalog.ts +0 -43
  188. package/src/voice/runtime/Catalog.ts +0 -68
  189. package/src/voice/runtime/DependencyInstaller.ts +0 -505
  190. package/src/voice/runtime/Installer.ts +0 -324
  191. package/src/voice/runtime/Paths.ts +0 -26
  192. package/src/voice/runtime/Transcriber.ts +0 -467
  193. package/src/voice/types/Voice.ts +0 -68
  194. package/src/voice/types/VoicePlugin.ts +0 -194
  195. package/src/web/Dependency.ts +0 -17
  196. package/src/web/PROMPT.agent-browser.ts +0 -9
  197. package/src/web/PROMPT.agent-browser.ts.txt +0 -17
  198. package/src/web/PROMPT.web-access.ts +0 -9
  199. package/src/web/PROMPT.web-access.ts.txt +0 -13
  200. package/src/web/runtime/Config.ts +0 -105
  201. package/src/web/runtime/Source.ts +0 -257
package/src/tts/Plugin.ts CHANGED
@@ -1,517 +1,250 @@
1
1
  /**
2
- * TTS Plugin。
2
+ * TtsPlugin:文本转语音插件。
3
3
  *
4
4
  * 关键点(中文)
5
- * - TTS Plugin 现在自己内聚本地模型与 Python 依赖,不再依赖 console 模型池。
6
- * - Console 只通过 setup / action 与插件交互,保持极简统一。
5
+ * - TTS 能力通过 constructor 注入,推荐传入 `city.ai.tts`。
6
+ * - plugin 不负责本地模型、Python 依赖、provider 或项目配置。
7
+ * - action 返回 AI SDK UIMessage,音频 file part 会由 plugin tool bridge 统一落盘到资源目录。
7
8
  */
8
9
 
9
10
  import { BasePlugin } from "@downcity/agent/internal/plugin/core/BasePlugin.js";
10
- import type { Plugin } from "@downcity/agent/internal/plugin/types/Plugin.js";
11
+ import type { AgentContext } from "@downcity/agent/internal/types/runtime/agent/AgentContext.js";
12
+ import type {
13
+ JsonObject,
14
+ JsonValue,
15
+ } from "@downcity/agent/internal/types/common/Json.js";
11
16
  import type {
12
- TtsInstallInput,
13
- TtsPluginConfig,
14
- TtsSynthesizeInput,
17
+ TtsPluginInput,
18
+ TtsPluginOptions,
19
+ TtsPluginResult,
20
+ TtsPluginSimpleAudioResult,
21
+ TtsPluginUiMessageResult,
15
22
  } from "@/tts/types/TtsPlugin.js";
16
- import { isPluginEnabled } from "@downcity/agent/internal/plugin/core/Activation.js";
17
- import { writeProjectPluginEnabled } from "@downcity/agent/internal/plugin/core/ProjectConfigStore.js";
18
- import {
19
- checkTtsSynthesizer,
20
- installTtsSynthesizer,
21
- listTtsModelOptions,
22
- readTtsPluginConfig,
23
- writeTtsPluginConfig,
24
- } from "@/tts/Dependency.js";
25
- import { resolveTtsModelId } from "@/tts/runtime/Catalog.js";
26
- import { synthesizeSpeechFile } from "@/tts/runtime/Synthesizer.js";
27
- import {
28
- getBooleanOpt,
29
- getNumberOpt,
30
- getStringOpt,
31
- toJsonObject,
32
- } from "@/tts/PluginSupport.js";
33
23
 
34
- function createTtsPluginDefinition(plugin: Plugin): Plugin {
24
+ const DEFAULT_TTS_PLUGIN_NAME = "tts";
25
+ const DEFAULT_TTS_PLUGIN_TITLE = "TTS";
26
+ const DEFAULT_TTS_PLUGIN_DESCRIPTION =
27
+ "Synthesize speech through an injected TTS function and return assistant audio file parts.";
28
+ const DEFAULT_MEDIA_TYPE = "audio/wav";
29
+ const DEFAULT_FILENAME = "speech.wav";
30
+
31
+ /**
32
+ * 判断值是否为普通对象。
33
+ */
34
+ function to_record(value: unknown): Record<string, unknown> | null {
35
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
36
+ return value as Record<string, unknown>;
37
+ }
38
+
39
+ /**
40
+ * 归一化 action payload。
41
+ */
42
+ function normalize_tts_payload(payload: JsonValue | undefined): TtsPluginInput {
43
+ const record = to_record(payload ?? {});
44
+ if (!record) {
45
+ throw new TypeError("TtsPlugin.synthesize payload must be an object");
46
+ }
47
+ const text = typeof record.text === "string" ? record.text.trim() : "";
48
+ if (!text) {
49
+ throw new Error("TtsPlugin.synthesize requires text");
50
+ }
35
51
  return {
36
- name: "tts",
37
- title: "Text To Speech",
38
- description:
39
- "Generates local speech audio files from plain text through an installed TTS model, then returns a reusable audio file tag for downstream sending.",
40
- config: {
41
- plugin: "tts",
42
- scope: "project",
43
- defaultValue: {
44
- provider: "local",
45
- modelId: "qwen3-tts-0.6b",
46
- format: "wav",
47
- speed: 1,
48
- },
49
- },
50
- setup: {
51
- mode: "install-configure",
52
- title: "安装语音合成",
53
- description: "选择模型后直接完成模型下载、独立运行环境准备与配置写入。",
54
- fields: [
55
- {
56
- key: "modelId",
57
- label: "模型",
58
- type: "select",
59
- required: true,
60
- sourceAction: "models",
61
- },
62
- {
63
- key: "format",
64
- label: "输出格式",
65
- type: "select",
66
- required: true,
67
- options: [
68
- { label: "WAV", value: "wav" },
69
- { label: "FLAC", value: "flac" },
70
- ],
71
- },
72
- ],
73
- primaryAction: "install",
74
- statusAction: "status",
75
- },
76
- usage: {
77
- title: "配置语音合成",
78
- description: "设置当前 agent 默认使用的语音模型与输出参数。",
79
- fields: [
80
- {
81
- key: "modelId",
82
- label: "默认模型",
83
- type: "select",
84
- required: true,
85
- sourceAction: "models",
86
- },
87
- {
88
- key: "format",
89
- label: "输出格式",
90
- type: "select",
91
- required: true,
92
- options: [
93
- { label: "WAV", value: "wav", description: "兼容性最好,适合绝大多数渠道。" },
94
- { label: "FLAC", value: "flac", description: "体积更小,适合存档或传输。" },
95
- ],
96
- },
97
- {
98
- key: "speed",
99
- label: "默认语速",
100
- type: "number",
101
- placeholder: "1",
102
- description: "1 为正常语速,可按需要设置为 0.8、1.2 等。",
103
- },
104
- {
105
- key: "language",
106
- label: "默认语言提示",
107
- type: "string",
108
- placeholder: "auto / zh / en",
109
- },
52
+ ...(record as TtsPluginInput),
53
+ text,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * 判断返回值是否已经是 AI SDK UIMessage。
59
+ */
60
+ function is_ui_message_result(value: TtsPluginResult): value is TtsPluginUiMessageResult {
61
+ const record = to_record(value);
62
+ return Boolean(record && Array.isArray(record.parts));
63
+ }
64
+
65
+ /**
66
+ * 读取简单音频结果中的 URL。
67
+ */
68
+ function read_audio_url(record: Record<string, unknown>): string {
69
+ const url =
70
+ typeof record.url === "string"
71
+ ? record.url.trim()
72
+ : typeof record.data_url === "string"
73
+ ? record.data_url.trim()
74
+ : typeof record.audio_path === "string"
75
+ ? record.audio_path.trim()
76
+ : "";
77
+ if (!url) {
78
+ throw new TypeError(
79
+ "TtsPlugin tts function must return a UIMessage or { url | data_url | audio_path }",
80
+ );
81
+ }
82
+ return url;
83
+ }
84
+
85
+ /**
86
+ * 把简单音频结果归一为 UIMessage。
87
+ */
88
+ function simple_audio_to_ui_message(
89
+ result: TtsPluginSimpleAudioResult,
90
+ ): TtsPluginUiMessageResult {
91
+ const record = to_record(result);
92
+ if (!record) {
93
+ throw new TypeError("TtsPlugin tts function returned an invalid result");
94
+ }
95
+ const url = read_audio_url(record);
96
+ const media_type =
97
+ typeof record.media_type === "string" && record.media_type.trim()
98
+ ? record.media_type.trim()
99
+ : DEFAULT_MEDIA_TYPE;
100
+ const filename =
101
+ typeof record.filename === "string" && record.filename.trim()
102
+ ? record.filename.trim()
103
+ : DEFAULT_FILENAME;
104
+ const text = typeof record.text === "string" ? record.text.trim() : "";
105
+
106
+ return {
107
+ id: `tts:${Date.now()}`,
108
+ role: "assistant",
109
+ parts: [
110
+ ...(text ? [{ type: "text" as const, text }] : []),
110
111
  {
111
- key: "voice",
112
- label: "默认音色",
113
- type: "string",
114
- placeholder: "可选音色 ID",
112
+ type: "file" as const,
113
+ mediaType: media_type,
114
+ filename,
115
+ url,
115
116
  },
116
117
  ],
117
- saveAction: "configure",
118
- statusAction: "status",
119
- },
120
- async availability(context) {
121
- if (!isPluginEnabled({ plugin, context })) {
122
- return {
123
- enabled: false,
124
- available: false,
125
- reasons: ["tts plugin disabled in project config"],
126
- };
118
+ };
119
+ }
120
+
121
+ /**
122
+ * 校验并归一化 TTS 返回结果。
123
+ */
124
+ function normalize_tts_result(result: TtsPluginResult): TtsPluginUiMessageResult {
125
+ if (is_ui_message_result(result)) {
126
+ return result;
127
+ }
128
+ return simple_audio_to_ui_message(result);
129
+ }
130
+
131
+ /**
132
+ * Agent TTS 插件。
133
+ */
134
+ export class TtsPlugin extends BasePlugin {
135
+ /**
136
+ * 当前 plugin 稳定名称。
137
+ */
138
+ readonly name: string;
139
+
140
+ /**
141
+ * 插件标题。
142
+ */
143
+ readonly title: string;
144
+
145
+ /**
146
+ * 插件说明。
147
+ */
148
+ readonly description: string;
149
+
150
+ private readonly tts: TtsPluginOptions["tts"];
151
+ private readonly language?: string;
152
+ private readonly voice?: string;
153
+ private readonly format?: string;
154
+
155
+ constructor(options: TtsPluginOptions) {
156
+ super();
157
+ const name = String(options.name || DEFAULT_TTS_PLUGIN_NAME).trim();
158
+ if (!name) {
159
+ throw new Error("TtsPlugin requires a non-empty name");
127
160
  }
128
- const dependencyStatus = await checkTtsSynthesizer(context);
129
- return {
130
- enabled: true,
131
- available: dependencyStatus.available,
132
- reasons: dependencyStatus.reasons,
133
- };
134
- },
135
- actions: {
136
- status: {
137
- allowWhenDisabled: true,
138
- command: {
139
- description: "查看 tts plugin 当前状态",
140
- mapInput() {
141
- return {};
142
- },
143
- },
144
- execute: async ({ context }) => {
145
- const config = readTtsPluginConfig(context);
146
- const availability = await plugin.availability!(context);
147
- const synthesizer = await checkTtsSynthesizer(context);
148
- return {
149
- success: true,
150
- data: {
151
- plugin: toJsonObject(config) || {},
152
- availability: {
153
- enabled: availability.enabled,
154
- available: availability.available,
155
- reasons: availability.reasons,
156
- },
157
- synthesizer: {
158
- available: synthesizer.available,
159
- reasons: synthesizer.reasons,
160
- details: synthesizer.details || null,
161
- },
162
- },
163
- };
164
- },
165
- },
166
- doctor: {
167
- allowWhenDisabled: true,
168
- command: {
169
- description: "检查 tts plugin 依赖状态",
170
- mapInput() {
171
- return {};
172
- },
173
- },
174
- execute: async ({ context }) => {
175
- const result = await checkTtsSynthesizer(context);
176
- return {
177
- success: true,
178
- data: {
179
- available: result.available,
180
- reasons: result.reasons,
181
- details: result.details || null,
182
- },
183
- };
184
- },
185
- },
186
- models: {
187
- allowWhenDisabled: true,
188
- command: {
189
- description: "列出可用于 tts 的本地模型",
190
- mapInput() {
191
- return {};
192
- },
193
- },
194
- execute: async () => {
195
- return {
196
- success: true,
197
- data: {
198
- options: listTtsModelOptions(),
199
- },
200
- };
201
- },
202
- },
203
- install: {
204
- allowWhenDisabled: true,
205
- command: {
206
- description: "安装 tts 语音合成依赖",
207
- configure(command) {
208
- command
209
- .argument("[models...]")
210
- .option("--active-model <modelId>", "安装完成后设为当前模型")
211
- .option("--models-dir <path>", "模型目录(可选)")
212
- .option("--python <bin>", "Python 可执行文件(默认 python3)")
213
- .option("--no-install-deps", "跳过依赖安装")
214
- .option("--force", "强制覆盖已存在资源")
215
- .option("--hf-token <token>", "HuggingFace token(可选)")
216
- .option("--format <format>", "默认输出格式(wav/flac)");
217
- },
218
- mapInput({ args, opts }) {
219
- return {
220
- modelIds: args,
221
- ...(getStringOpt(opts, "activeModel")
222
- ? { activeModel: getStringOpt(opts, "activeModel") }
223
- : {}),
224
- ...(getStringOpt(opts, "modelsDir")
225
- ? { modelsDir: getStringOpt(opts, "modelsDir") }
226
- : {}),
227
- ...(getStringOpt(opts, "python")
228
- ? { pythonBin: getStringOpt(opts, "python") }
229
- : {}),
230
- installDeps: getBooleanOpt(opts, "installDeps", true),
231
- force: getBooleanOpt(opts, "force", false),
232
- ...(getStringOpt(opts, "hfToken")
233
- ? { hfToken: getStringOpt(opts, "hfToken") }
234
- : {}),
235
- ...(getStringOpt(opts, "format")
236
- ? { format: getStringOpt(opts, "format") }
237
- : {}),
238
- };
239
- },
240
- },
241
- execute: async ({ context, payload }) => {
242
- const result = await installTtsSynthesizer({
243
- context,
244
- input:
245
- payload && typeof payload === "object" && !Array.isArray(payload)
246
- ? (payload as TtsInstallInput)
247
- : undefined,
248
- });
249
- return {
250
- success: result.success,
251
- ...(result.message ? { message: result.message } : {}),
252
- ...(result.details !== undefined ? { data: result.details } : {}),
253
- ...(result.success ? {} : { error: result.message || "install failed" }),
254
- };
255
- },
256
- },
257
- configure: {
258
- allowWhenDisabled: true,
259
- execute: async ({ context, payload }) => {
260
- const payloadObject =
261
- payload && typeof payload === "object" && !Array.isArray(payload)
262
- ? (payload as Partial<TtsPluginConfig> & Record<string, unknown>)
263
- : {};
264
- const { enabled: _ignoredEnabled, ...patch } = payloadObject;
265
- const current = readTtsPluginConfig(context);
266
- const next = {
267
- ...current,
268
- ...(patch as Partial<TtsPluginConfig>),
269
- } satisfies TtsPluginConfig;
270
- await writeTtsPluginConfig({
271
- context,
272
- value: next,
273
- });
274
- return {
275
- success: true,
276
- data: {
277
- plugin: toJsonObject(next) || {},
278
- },
279
- };
280
- },
281
- },
282
- on: {
283
- allowWhenDisabled: true,
284
- command: {
285
- description: "启用 tts plugin,并可选安装本地依赖",
286
- configure(command) {
287
- command
288
- .argument("[models...]")
289
- .option("--active-model <modelId>", "安装完成后设为当前模型")
290
- .option("--models-dir <path>", "模型目录(可选)")
291
- .option("--python <bin>", "Python 可执行文件(默认 python3)")
292
- .option("--no-install", "仅启用 plugin,不安装依赖")
293
- .option("--no-install-deps", "跳过依赖安装")
294
- .option("--force", "强制覆盖已存在资源")
295
- .option("--hf-token <token>", "HuggingFace token(可选)")
296
- .option("--format <format>", "默认输出格式(wav/flac)");
297
- },
298
- mapInput({ args, opts }) {
299
- return {
300
- modelIds: args,
301
- ...(getStringOpt(opts, "activeModel")
302
- ? { activeModel: getStringOpt(opts, "activeModel") }
303
- : {}),
304
- ...(getStringOpt(opts, "modelsDir")
305
- ? { modelsDir: getStringOpt(opts, "modelsDir") }
306
- : {}),
307
- ...(getStringOpt(opts, "python")
308
- ? { pythonBin: getStringOpt(opts, "python") }
309
- : {}),
310
- install: getBooleanOpt(opts, "install", true),
311
- installDeps: getBooleanOpt(opts, "installDeps", true),
312
- force: getBooleanOpt(opts, "force", false),
313
- ...(getStringOpt(opts, "hfToken")
314
- ? { hfToken: getStringOpt(opts, "hfToken") }
315
- : {}),
316
- ...(getStringOpt(opts, "format")
317
- ? { format: getStringOpt(opts, "format") }
318
- : {}),
319
- };
320
- },
321
- },
322
- execute: async ({ context, payload }) => {
323
- await writeProjectPluginEnabled({
324
- pluginName: "tts",
325
- enabled: true,
326
- context,
327
- });
328
- if ((payload as { install?: unknown }).install !== false) {
329
- const installResult = await installTtsSynthesizer({
330
- context,
331
- input:
332
- payload && typeof payload === "object" && !Array.isArray(payload)
333
- ? (payload as TtsInstallInput)
334
- : undefined,
335
- });
336
- if (!installResult.success) {
337
- return {
338
- success: false,
339
- error: installResult.message || "tts dependency install failed",
340
- message: installResult.message || "tts dependency install failed",
341
- };
342
- }
343
- }
344
- return {
345
- success: true,
346
- data: {
347
- plugin: toJsonObject(readTtsPluginConfig(context) as Record<string, unknown>) || {},
348
- },
349
- };
350
- },
351
- },
352
- off: {
353
- command: {
354
- description: "关闭 tts plugin",
355
- mapInput() {
356
- return {};
357
- },
358
- },
359
- execute: async ({ context }) => {
360
- await writeProjectPluginEnabled({
361
- pluginName: "tts",
362
- enabled: false,
363
- context,
364
- });
365
- return {
366
- success: true,
367
- data: {
368
- plugin: toJsonObject(readTtsPluginConfig(context) as Record<string, unknown>) || {},
369
- },
370
- };
371
- },
372
- },
373
- use: {
374
- allowWhenDisabled: true,
375
- command: {
376
- description: "切换 tts 当前模型",
377
- configure(command) {
378
- command.argument("<modelId>");
379
- },
380
- mapInput({ args }) {
381
- const modelId = String(args[0] || "").trim();
382
- if (!modelId) {
383
- throw new Error("modelId is required");
384
- }
385
- return {
386
- modelId,
387
- };
388
- },
389
- },
390
- execute: async ({ context, payload }) => {
391
- const modelId = String((payload as { modelId?: unknown }).modelId || "").trim();
392
- const resolvedModelId = resolveTtsModelId(modelId);
393
- if (!resolvedModelId) {
394
- return {
395
- success: false,
396
- error: `Unsupported tts model: ${modelId}`,
397
- message: `Unsupported tts model: ${modelId}`,
398
- };
399
- }
400
- const nextConfig = await writeTtsPluginConfig({
401
- context,
402
- value: {
403
- ...readTtsPluginConfig(context),
404
- modelId: resolvedModelId,
405
- },
406
- });
407
- return {
408
- success: true,
409
- data: {
410
- plugin: toJsonObject(nextConfig as Record<string, unknown>) || {},
411
- },
412
- };
413
- },
414
- },
161
+ if (typeof options.tts !== "function") {
162
+ throw new Error("TtsPlugin requires a tts function");
163
+ }
164
+ this.name = name;
165
+ this.title = String(options.title || DEFAULT_TTS_PLUGIN_TITLE).trim();
166
+ this.description = String(
167
+ options.description || DEFAULT_TTS_PLUGIN_DESCRIPTION,
168
+ ).trim();
169
+ this.tts = options.tts;
170
+ this.language =
171
+ typeof options.language === "string" && options.language.trim()
172
+ ? options.language.trim()
173
+ : undefined;
174
+ this.voice =
175
+ typeof options.voice === "string" && options.voice.trim()
176
+ ? options.voice.trim()
177
+ : undefined;
178
+ this.format =
179
+ typeof options.format === "string" && options.format.trim()
180
+ ? options.format.trim()
181
+ : undefined;
182
+ }
183
+
184
+ /**
185
+ * TTS 插件给模型的使用说明。
186
+ */
187
+ system(_context: AgentContext): string {
188
+ return [
189
+ "# TTS Plugin",
190
+ "",
191
+ "Use this plugin when the user asks to create spoken audio, voice output, narration, or a reusable audio file.",
192
+ "Do not call it for ordinary text replies unless the user explicitly wants audio.",
193
+ "",
194
+ "Call through `plugin_call`:",
195
+ "",
196
+ "```ts",
197
+ "plugin_call({",
198
+ ` plugin: "${this.name}",`,
199
+ ' action: "synthesize",',
200
+ " payload: {",
201
+ ' text: "...",',
202
+ " },",
203
+ "});",
204
+ "```",
205
+ "",
206
+ "Payload rules:",
207
+ "- `text` is required.",
208
+ "- Optional fields: `language`, `voice`, `format`, `speed`, `provider_options`.",
209
+ "- The returned audio file part is saved under project `.downcity/resources` and attached to the final assistant message automatically.",
210
+ ].join("\n");
211
+ }
212
+
213
+ /**
214
+ * 执行一次 TTS 合成。
215
+ */
216
+ private async synthesize(input: TtsPluginInput): Promise<TtsPluginUiMessageResult> {
217
+ const result = await this.tts({
218
+ ...(this.language ? { language: this.language } : {}),
219
+ ...(this.voice ? { voice: this.voice } : {}),
220
+ ...(this.format ? { format: this.format } : {}),
221
+ ...input,
222
+ });
223
+ return normalize_tts_result(result);
224
+ }
225
+
226
+ /**
227
+ * 显式 action 集合。
228
+ */
229
+ readonly actions = {
415
230
  synthesize: {
416
- command: {
417
- description: "将文本生成语音文件",
418
- configure(command) {
419
- command
420
- .argument("<text>")
421
- .option("--model <modelId>", "语音模型 ID")
422
- .option("--language <language>", "语言提示(可选,例如 zh / en)")
423
- .option("--voice <voice>", "音色 ID(可选)")
424
- .option("--format <format>", "输出格式(wav/flac)")
425
- .option("--speed <speed>", "语速倍率", Number)
426
- .option("--output <path>", "输出文件路径或目录(可选)");
427
- },
428
- mapInput({ args, opts }) {
429
- const text = String(args[0] || "").trim();
430
- if (!text) {
431
- throw new Error("text is required");
432
- }
231
+ execute: async ({ payload }: { payload: JsonValue }) => {
232
+ try {
233
+ const input = normalize_tts_payload(payload);
234
+ const message = await this.synthesize(input);
433
235
  return {
434
- text,
435
- ...(getStringOpt(opts, "model")
436
- ? { modelId: getStringOpt(opts, "model") }
437
- : {}),
438
- ...(getStringOpt(opts, "language")
439
- ? { language: getStringOpt(opts, "language") }
440
- : {}),
441
- ...(getStringOpt(opts, "voice")
442
- ? { voice: getStringOpt(opts, "voice") }
443
- : {}),
444
- ...(getStringOpt(opts, "format")
445
- ? { format: getStringOpt(opts, "format") }
446
- : {}),
447
- ...(typeof getNumberOpt(opts, "speed") === "number"
448
- ? { speed: getNumberOpt(opts, "speed") }
449
- : {}),
450
- ...(getStringOpt(opts, "output")
451
- ? { output: getStringOpt(opts, "output") }
452
- : {}),
236
+ success: true,
237
+ data: message as unknown as JsonObject,
238
+ message: "speech synthesized",
453
239
  };
454
- },
455
- },
456
- execute: async ({ context, payload }) => {
457
- const pluginStatus = await plugin.availability!(context);
458
- if (!pluginStatus.enabled || !pluginStatus.available) {
240
+ } catch (error) {
459
241
  return {
460
242
  success: false,
461
- error: pluginStatus.reasons[0] || "tts plugin unavailable",
462
- message: pluginStatus.reasons[0] || "tts plugin unavailable",
243
+ error: String(error),
244
+ message: String(error),
463
245
  };
464
246
  }
465
-
466
- const input =
467
- payload && typeof payload === "object" && !Array.isArray(payload)
468
- ? (payload as TtsSynthesizeInput)
469
- : {};
470
- const result = await synthesizeSpeechFile({
471
- context,
472
- config: readTtsPluginConfig(context),
473
- input,
474
- });
475
- return {
476
- success: true,
477
- data: {
478
- outputPath: result.outputPath,
479
- fileTag: result.fileTag,
480
- bytes: result.bytes,
481
- ...(result.stderrSummary ? { stderr: result.stderrSummary } : {}),
482
- },
483
- };
484
247
  },
485
248
  },
486
- },
487
- system(context) {
488
- if (!isPluginEnabled({ plugin, context })) {
489
- return "";
490
- }
491
- return [
492
- "# TTS Plugin",
493
- "The agent can call the tts plugin to synthesize speech audio from plain text.",
494
- "Typical usage flow:",
495
- "1. Check availability with the `status` action when you need to confirm whether the plugin and model are ready.",
496
- "2. Generate audio with the `synthesize` action.",
497
- "3. Optionally override synthesis parameters in the action payload: `voice`, `language`, `format`, `speed`, and `output`.",
498
- "Use the `tts.synthesize` action when the user asks to generate spoken audio or a reusable audio file tag.",
499
- "A successful synthesis returns a local output path and a reusable `<file type=\"audio\">...</file>` tag for downstream sending.",
500
- "If the Python runner prints non-fatal stderr, the command still succeeds and returns that stderr summary as extra context.",
501
- "Example: `plugin_call({ plugin: \"tts\", action: \"synthesize\", payload: { text: \"你好,欢迎来到 Downcity\", format: \"wav\" } })`",
502
- ].join("\n");
503
- },
504
249
  };
505
250
  }
506
-
507
- /**
508
- * TtsPlugin:文本转语音插件。
509
- */
510
- export class TtsPlugin extends BasePlugin {
511
- readonly name = "tts";
512
-
513
- constructor() {
514
- super();
515
- Object.assign(this, createTtsPluginDefinition(this));
516
- }
517
- }