Skip to content

Commit 06e0498

Browse files
fix(speech): serialize pronunciation dictionary correctly
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 5f13ef5 commit 06e0498

3 files changed

Lines changed: 184 additions & 5 deletions

File tree

src/commands/speech/synthesize.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,17 @@ export default defineCommand({
9292
if (flags.subtitles) body.subtitle_enable = true; // Correct API parameter name
9393

9494
if (flags.pronunciation) {
95-
body.pronunciation_dict = (flags.pronunciation as string[]).map(p => {
96-
const [from, to] = p.split('/');
97-
return { tone: to || from!, text: from! };
98-
});
95+
const tones = flags.pronunciation as string[];
96+
for (const p of tones) {
97+
if (!p.includes('/')) {
98+
throw new CLIError(
99+
`Invalid --pronunciation value ${JSON.stringify(p)}: expected the form word/(pronunciation), e.g. 重/(chong2).`,
100+
ExitCode.USAGE,
101+
'mmx speech synthesize --text "我要去重庆出差。" --pronunciation "重/(chong2)"',
102+
);
103+
}
104+
}
105+
body.pronunciation_dict = { tone: tones };
99106
}
100107

101108
if (dryRun(config, body)) return;

src/types/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface SpeechRequest {
105105
channel?: number;
106106
};
107107
language_boost?: string;
108-
pronunciation_dict?: Array<{ tone: string; text: string }>;
108+
pronunciation_dict?: { tone: string[] };
109109
output_format?: 'url' | 'hex';
110110
stream?: boolean;
111111
subtitle_enable?: boolean; // Correct API parameter name (not 'subtitle')

test/commands/speech/synthesize.test.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,178 @@ describe('speech synthesize command', () => {
202202
console.log = originalLog;
203203
}
204204
});
205+
206+
it('--pronunciation uses the API-compatible request shape', async () => {
207+
const config = {
208+
apiKey: 'test-key',
209+
region: 'global' as const,
210+
baseUrl: 'https://api.mmx.io',
211+
output: 'json' as const,
212+
timeout: 10,
213+
verbose: false,
214+
quiet: false,
215+
noColor: true,
216+
yes: false,
217+
dryRun: true,
218+
nonInteractive: true,
219+
async: false,
220+
};
221+
222+
const originalLog = console.log;
223+
let output = '';
224+
console.log = (msg: string) => { output += msg; };
225+
226+
try {
227+
await synthesizeCommand.execute(config, {
228+
text: '我要去重庆出差。',
229+
pronunciation: [
230+
'重/(chong2)',
231+
'行长/(hang2)(zhang3)',
232+
],
233+
quiet: false,
234+
verbose: false,
235+
noColor: true,
236+
yes: false,
237+
dryRun: true,
238+
help: false,
239+
nonInteractive: true,
240+
async: false,
241+
});
242+
243+
const parsed = JSON.parse(output);
244+
245+
expect(parsed.request.pronunciation_dict).toEqual({
246+
tone: [
247+
'重/(chong2)',
248+
'行长/(hang2)(zhang3)',
249+
],
250+
});
251+
} finally {
252+
console.log = originalLog;
253+
}
254+
});
255+
256+
it('--pronunciation with a single value serializes under tone', async () => {
257+
const config = {
258+
apiKey: 'test-key',
259+
region: 'global' as const,
260+
baseUrl: 'https://api.mmx.io',
261+
output: 'json' as const,
262+
timeout: 10,
263+
verbose: false,
264+
quiet: false,
265+
noColor: true,
266+
yes: false,
267+
dryRun: true,
268+
nonInteractive: true,
269+
async: false,
270+
};
271+
272+
const originalLog = console.log;
273+
let output = '';
274+
console.log = (msg: string) => { output += msg; };
275+
276+
try {
277+
await synthesizeCommand.execute(config, {
278+
text: '我要去重庆出差。',
279+
pronunciation: ['重/(chong2)'],
280+
quiet: false,
281+
verbose: false,
282+
noColor: true,
283+
yes: false,
284+
dryRun: true,
285+
help: false,
286+
nonInteractive: true,
287+
async: false,
288+
});
289+
290+
const parsed = JSON.parse(output);
291+
292+
expect(parsed.request.pronunciation_dict).toEqual({
293+
tone: ['重/(chong2)'],
294+
});
295+
// Must NOT be the old array-of-objects shape.
296+
expect(Array.isArray(parsed.request.pronunciation_dict)).toBe(false);
297+
} finally {
298+
console.log = originalLog;
299+
}
300+
});
301+
302+
it('--pronunciation preserves multi-character word values verbatim', async () => {
303+
const config = {
304+
apiKey: 'test-key',
305+
region: 'global' as const,
306+
baseUrl: 'https://api.mmx.io',
307+
output: 'json' as const,
308+
timeout: 10,
309+
verbose: false,
310+
quiet: false,
311+
noColor: true,
312+
yes: false,
313+
dryRun: true,
314+
nonInteractive: true,
315+
async: false,
316+
};
317+
318+
const originalLog = console.log;
319+
let output = '';
320+
console.log = (msg: string) => { output += msg; };
321+
322+
try {
323+
await synthesizeCommand.execute(config, {
324+
text: '行长',
325+
pronunciation: ['行长/(hang2)(zhang3)'],
326+
quiet: false,
327+
verbose: false,
328+
noColor: true,
329+
yes: false,
330+
dryRun: true,
331+
help: false,
332+
nonInteractive: true,
333+
async: false,
334+
});
335+
336+
const parsed = JSON.parse(output);
337+
338+
expect(parsed.request.pronunciation_dict.tone).toEqual([
339+
'行长/(hang2)(zhang3)',
340+
]);
341+
} finally {
342+
console.log = originalLog;
343+
}
344+
});
345+
346+
it('--pronunciation rejects malformed values without a separator', async () => {
347+
const config = {
348+
apiKey: 'test-key',
349+
region: 'global' as const,
350+
baseUrl: 'https://api.mmx.io',
351+
output: 'json' as const,
352+
timeout: 10,
353+
verbose: false,
354+
quiet: false,
355+
noColor: true,
356+
yes: false,
357+
dryRun: true,
358+
nonInteractive: true,
359+
async: false,
360+
};
361+
362+
await expect(
363+
synthesizeCommand.execute(config, {
364+
text: 'Hello',
365+
pronunciation: ['chong2'],
366+
quiet: false,
367+
verbose: false,
368+
noColor: true,
369+
yes: false,
370+
dryRun: true,
371+
help: false,
372+
nonInteractive: true,
373+
async: false,
374+
}),
375+
).rejects.toThrow('Invalid --pronunciation value "chong2"');
376+
});
205377
});
206378

207379
describe('speech synthesize format validation', () => {

0 commit comments

Comments
 (0)