dsh-audiogen 0.2.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/src/protocol.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  export const AUDIOGEN_SETTINGS_NAMESPACE = 'dsh-audiogen'
9
9
 
10
10
  /** Published package version shared by the host updater and the client UI. */
11
- export const PLUGIN_VERSION = '0.2.0'
11
+ export const PLUGIN_VERSION = '0.3.2'
12
12
 
13
13
  /** Same-origin route family (loopback-only, mirroring dsh-imagegen). */
14
14
  export const SETTINGS_API = {
@@ -45,7 +45,7 @@ export const HISTORY_API = {
45
45
  export const HISTORY_MAX = 50
46
46
 
47
47
  /** Audio generation modes. */
48
- export type AudioMode = 'tts' | 'music' | 'sfx'
48
+ export type AudioMode = 'tts' | 'music' | 'sfx' | 'voice_design'
49
49
 
50
50
  /** The capability category of an audio model/voice. */
51
51
  export type AudioModelCategory =
@@ -94,6 +94,8 @@ export interface PresetProviderView {
94
94
  name: string
95
95
  apiUrl: string
96
96
  hint: string
97
+ /** Official vendor website, shown as a link in the channel editor. */
98
+ site?: string
97
99
  models: ModelMapping[]
98
100
  }
99
101
 
@@ -106,6 +108,8 @@ export interface GenerateAudioRequest {
106
108
  prompt: string
107
109
  /** Optional voice alias for TTS. */
108
110
  voice?: string
111
+ /** Optional preview text for voice-design APIs. */
112
+ previewText?: string
109
113
  /** Optional speaking rate / speed multiplier. */
110
114
  speed?: number
111
115
  /** Requested duration in seconds (music/sfx). */
@@ -118,6 +122,38 @@ export interface GenerateAudioRequest {
118
122
  channel?: string
119
123
  /** Upstream model id actually sent (host-filled from alias mapping). */
120
124
  upstream?: string
125
+
126
+ // ---- MiniMax TTS 专属字段(其他厂商渠道忽略)——————
127
+ /** MiniMax 音色情绪,如 happy/sad/angry/nervous/fearful/bored;默认按音色自身。 */
128
+ emotion?: string
129
+ /** MiniMax 音量,范围 0-10,默认 1。 */
130
+ vol?: number
131
+ /** MiniMax 音调偏移(半音),范围 -12~12,默认 0。 */
132
+ pitch?: number
133
+ /** MiniMax 文本归一化处理开关(默认 true)。 */
134
+ textNormalization?: boolean
135
+ /** MiniMax 数学公式朗读开关(默认 false)。 */
136
+ latexRead?: boolean
137
+ /** MiniMax 发音词典 tone 条目,元素形如 "处理/(chu3)(li3)" 或 "危险/dangerous"。 */
138
+ pronunciationTone?: string[]
139
+ /** MiniMax 采样率:16000/24000/32000/44100/48000,默认 32000。 */
140
+ sampleRate?: number
141
+ /** MiniMax 码率(bps):64000-320000,默认 128000。 */
142
+ bitrate?: number
143
+ /** MiniMax 声道数:1 或 2,默认 1。 */
144
+ audioChannel?: number
145
+ /** MiniMax 强制 CBR 编码(avoid VBR),默认 false。 */
146
+ forceCbr?: boolean
147
+ /** MiniMax 字幕开关:true 时响应携带字幕内容/文件。 */
148
+ subtitleEnable?: boolean
149
+ /** MiniMax AIGC 水印开关。 */
150
+ aigcWatermark?: boolean
151
+ /** MiniMax 语言增强(language_boost),如"中英混读",按模型支持情况。 */
152
+ languageBoost?: string
153
+ /** MiniMax 变声参数(voice_modify,speech-2.8 等支持)。 */
154
+ voiceModify?: { pitch?: number; intensity?: number; timbre?: number; soundEffects?: string }
155
+ /** MiniMax 双音色混合权重(timbre_weights)。 */
156
+ timbreWeights?: Array<{ voiceId: string; weight: number }>
121
157
  }
122
158
 
123
159
  /** One generated audio, normalized host-side to base64. */
@@ -134,6 +170,8 @@ export interface GeneratedAudio {
134
170
  url: string
135
171
  /** Stable audio id / file name. */
136
172
  id: string
173
+ /** Optional voice id returned by a voice-design API. */
174
+ voiceId?: string
137
175
  }
138
176
 
139
177
  /** Successful generate outcome. */
@@ -153,6 +191,8 @@ export interface HistoryAudioRef {
153
191
  mime: string
154
192
  /** Duration in seconds when known. */
155
193
  duration?: number
194
+ /** Optional generated voice id. */
195
+ voiceId?: string
156
196
  }
157
197
 
158
198
  /** A saved generation as the browser consumes it. */
@@ -163,6 +203,7 @@ export interface HistoryEntry {
163
203
  model: string
164
204
  prompt: string
165
205
  voice?: string
206
+ voiceId?: string
166
207
  speed?: number
167
208
  duration?: number
168
209
  format?: string
@@ -179,6 +220,7 @@ export interface HistoryEntryInput {
179
220
  model: string
180
221
  prompt: string
181
222
  voice?: string
223
+ voiceId?: string
182
224
  speed?: number
183
225
  duration?: number
184
226
  format?: string
package/src/routes.ts CHANGED
@@ -90,18 +90,56 @@ function messageOf(error: unknown): string {
90
90
  }
91
91
 
92
92
  function parseGenerateRequest(body: Record<string, unknown>): GenerateAudioRequest | undefined {
93
- const mode = body.mode === 'music' ? 'music' : body.mode === 'sfx' ? 'sfx' : 'tts'
93
+ const mode = body.mode === 'music' ? 'music' : body.mode === 'sfx' ? 'sfx' : body.mode === 'voice_design' ? 'voice_design' : 'tts'
94
94
  const prompt = typeof body.prompt === 'string' ? body.prompt.trim() : ''
95
95
  if (prompt === '') return undefined
96
+ const num = (value: unknown): number | undefined => typeof value === 'number' && Number.isFinite(value) ? value : undefined
97
+ const str = (value: unknown): string | undefined => typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
98
+ const flag = (value: unknown): boolean | undefined => typeof value === 'boolean' ? value : undefined
99
+ const tone = Array.isArray(body.pronunciationTone)
100
+ ? body.pronunciationTone.filter((item): item is string => typeof item === 'string' && item.trim() !== '').map(item => item.trim())
101
+ : undefined
102
+ const voiceModifyRaw = body.voiceModify
103
+ const voiceModify = typeof voiceModifyRaw === 'object' && voiceModifyRaw !== null
104
+ ? {
105
+ ...(num((voiceModifyRaw as Record<string, unknown>).pitch) !== undefined ? { pitch: num((voiceModifyRaw as Record<string, unknown>).pitch)! } : {}),
106
+ ...(num((voiceModifyRaw as Record<string, unknown>).intensity) !== undefined ? { intensity: num((voiceModifyRaw as Record<string, unknown>).intensity)! } : {}),
107
+ ...(num((voiceModifyRaw as Record<string, unknown>).timbre) !== undefined ? { timbre: num((voiceModifyRaw as Record<string, unknown>).timbre)! } : {}),
108
+ ...(str((voiceModifyRaw as Record<string, unknown>).soundEffects) !== undefined ? { soundEffects: str((voiceModifyRaw as Record<string, unknown>).soundEffects)! } : {}),
109
+ }
110
+ : undefined
111
+ const timbreWeights = Array.isArray(body.timbreWeights)
112
+ ? body.timbreWeights
113
+ .filter((item): item is Record<string, unknown> => typeof item === 'object' && item !== null && typeof (item as Record<string, unknown>).voiceId === 'string' && typeof (item as Record<string, unknown>).weight === 'number')
114
+ .map(item => ({ voiceId: (item.voiceId as string).trim(), weight: item.weight as number }))
115
+ .filter(item => item.voiceId !== '')
116
+ : undefined
96
117
  return {
97
118
  mode,
98
119
  model: typeof body.model === 'string' ? body.model.trim() : '',
99
120
  prompt,
100
121
  ...(typeof body.voice === 'string' && body.voice.trim() !== '' ? { voice: body.voice.trim() } : {}),
101
- ...(typeof body.speed === 'number' ? { speed: body.speed } : {}),
102
- ...(typeof body.duration === 'number' ? { duration: body.duration } : {}),
122
+ ...(typeof body.previewText === 'string' && body.previewText.trim() !== '' ? { previewText: body.previewText.trim() } : {}),
123
+ ...(num(body.speed) !== undefined ? { speed: num(body.speed)! } : {}),
124
+ ...(num(body.duration) !== undefined ? { duration: num(body.duration)! } : {}),
103
125
  ...(typeof body.format === 'string' && body.format.trim() !== '' ? { format: body.format.trim() } : {}),
104
126
  ...(typeof body.channelId === 'string' && body.channelId !== '' ? { channelId: body.channelId } : {}),
127
+ // ---- MiniMax TTS 专属字段(其他厂商忽略) ----
128
+ ...(str(body.emotion) !== undefined ? { emotion: str(body.emotion)! } : {}),
129
+ ...(num(body.vol) !== undefined ? { vol: num(body.vol)! } : {}),
130
+ ...(num(body.pitch) !== undefined ? { pitch: num(body.pitch)! } : {}),
131
+ ...(flag(body.textNormalization) !== undefined ? { textNormalization: flag(body.textNormalization)! } : {}),
132
+ ...(flag(body.latexRead) !== undefined ? { latexRead: flag(body.latexRead)! } : {}),
133
+ ...(tone !== undefined && tone.length > 0 ? { pronunciationTone: tone } : {}),
134
+ ...(num(body.sampleRate) !== undefined ? { sampleRate: num(body.sampleRate)! } : {}),
135
+ ...(num(body.bitrate) !== undefined ? { bitrate: num(body.bitrate)! } : {}),
136
+ ...(num(body.audioChannel) !== undefined ? { audioChannel: num(body.audioChannel)! } : {}),
137
+ ...(flag(body.forceCbr) !== undefined ? { forceCbr: flag(body.forceCbr)! } : {}),
138
+ ...(flag(body.subtitleEnable) !== undefined ? { subtitleEnable: flag(body.subtitleEnable)! } : {}),
139
+ ...(flag(body.aigcWatermark) !== undefined ? { aigcWatermark: flag(body.aigcWatermark)! } : {}),
140
+ ...(str(body.languageBoost) !== undefined ? { languageBoost: str(body.languageBoost)! } : {}),
141
+ ...(voiceModify !== undefined ? { voiceModify } : {}),
142
+ ...(timbreWeights !== undefined && timbreWeights.length > 0 ? { timbreWeights } : {}),
105
143
  }
106
144
  }
107
145
 
@@ -141,6 +179,10 @@ function resolveChannelRequest(
141
179
  const target = explicit ?? defaults
142
180
  const asked = request.model.trim()
143
181
  if (asked === '') {
182
+ if (request.mode === 'voice_design') {
183
+ if (target === undefined) return { ok: false, code: 'no-channels', message: '尚未配置任何渠道' }
184
+ return { ok: true, request: { ...request, channelId: target.id, channel: target.name } }
185
+ }
144
186
  const alias = target?.models[0]?.alias ?? ''
145
187
  if (alias === '') {
146
188
  return { ok: false, code: 'no-models', message: `渠道「${target?.name ?? ''}」尚未配置模型/音色,请先在设置中添加` }
@@ -207,7 +249,9 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
207
249
  ?? view.channels[0]
208
250
  const channel: AudioChannel = {
209
251
  id: stored?.id ?? 'preview',
210
- preset: stored?.preset ?? '',
252
+ // The draft's own preset wins (even when empty = custom): discovery
253
+ // must follow the vendor being configured, not the default channel.
254
+ preset: typeof body?.preset === 'string' ? body.preset.trim() : (stored?.preset ?? ''),
211
255
  name: stored?.name ?? '',
212
256
  apiUrl: typeof body?.apiUrl === 'string' && body.apiUrl.trim() !== '' ? body.apiUrl.trim() : (stored?.apiUrl ?? ''),
213
257
  apiKey: typeof body?.apiKey === 'string' && body.apiKey.trim() !== '' ? body.apiKey.trim() : (stored?.apiKey ?? ''),
@@ -300,6 +344,7 @@ export function makeRoutes(deps: AudiogenRoutesDeps): WebRoute[] {
300
344
  mime: saved.mime,
301
345
  bytes: saved.bytes,
302
346
  url: `${AUDIO_API.file}/${encodeURIComponent(saved.file)}`,
347
+ ...(output.voiceId === undefined ? {} : { voiceId: output.voiceId }),
303
348
  })
304
349
  }
305
350
  let history