Skip to content

Commit 2e5cc47

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

3 files changed

Lines changed: 141 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: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,135 @@ 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+
// Object with a tone string array (not the old array-of-objects shape),
246+
// preserving each value verbatim — including multi-character words.
247+
expect(Array.isArray(parsed.request.pronunciation_dict)).toBe(false);
248+
expect(parsed.request.pronunciation_dict).toEqual({
249+
tone: [
250+
'重/(chong2)',
251+
'行长/(hang2)(zhang3)',
252+
],
253+
});
254+
} finally {
255+
console.log = originalLog;
256+
}
257+
});
258+
259+
it('--pronunciation with a single value serializes under tone', async () => {
260+
const config = {
261+
apiKey: 'test-key',
262+
region: 'global' as const,
263+
baseUrl: 'https://api.mmx.io',
264+
output: 'json' as const,
265+
timeout: 10,
266+
verbose: false,
267+
quiet: false,
268+
noColor: true,
269+
yes: false,
270+
dryRun: true,
271+
nonInteractive: true,
272+
async: false,
273+
};
274+
275+
const originalLog = console.log;
276+
let output = '';
277+
console.log = (msg: string) => { output += msg; };
278+
279+
try {
280+
await synthesizeCommand.execute(config, {
281+
text: '我要去重庆出差。',
282+
pronunciation: ['重/(chong2)'],
283+
quiet: false,
284+
verbose: false,
285+
noColor: true,
286+
yes: false,
287+
dryRun: true,
288+
help: false,
289+
nonInteractive: true,
290+
async: false,
291+
});
292+
293+
const parsed = JSON.parse(output);
294+
295+
expect(parsed.request.pronunciation_dict).toEqual({
296+
tone: ['重/(chong2)'],
297+
});
298+
} finally {
299+
console.log = originalLog;
300+
}
301+
});
302+
303+
it('--pronunciation rejects malformed values without a separator', async () => {
304+
const config = {
305+
apiKey: 'test-key',
306+
region: 'global' as const,
307+
baseUrl: 'https://api.mmx.io',
308+
output: 'json' as const,
309+
timeout: 10,
310+
verbose: false,
311+
quiet: false,
312+
noColor: true,
313+
yes: false,
314+
dryRun: true,
315+
nonInteractive: true,
316+
async: false,
317+
};
318+
319+
await expect(
320+
synthesizeCommand.execute(config, {
321+
text: 'Hello',
322+
pronunciation: ['chong2'],
323+
quiet: false,
324+
verbose: false,
325+
noColor: true,
326+
yes: false,
327+
dryRun: true,
328+
help: false,
329+
nonInteractive: true,
330+
async: false,
331+
}),
332+
).rejects.toThrow('Invalid --pronunciation value "chong2"');
333+
});
205334
});
206335

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

0 commit comments

Comments
 (0)