fix(synthesize): validate clone-path instruct client-side so non-EN/ZH prose can't 400 (#612) (#658)
A Vietnamese user typed a free-form Vietnamese description into the voice style
(instruct) field and got "400 Bad Request: Unsupported instruct items found in
quảng cáo, sôi nổi và thu hút". The instruct field is a fixed EN/ZH style-tag
vocabulary (the model's trained tokens: gender/age/pitch/accent/dialect/whisper);
the backend _resolve_instruct deliberately *raises* on unknown items.
The design path already guarded this: it runs the free-text through
buildDesignInstruct(), keeping valid tags, dropping the rest, and surfacing a
localized warning toast (#115/#114). But the *clone* path
(defineMethod === 'audio') appended the raw `instruct` string straight to the
request — so a clone + free-text style in any non-EN/ZH language round-tripped to
a 400 instead of being handled locally.
Fix (localized client-side guard, the chosen approach): route the clone path's
free-text through the same buildDesignInstruct({}, instruct) guard. Valid style
tags survive (a clone can still ask for "whisper"); unsupported items drop with
the existing localized `tts_errors.ignored_unsupported` toast; synthesis proceeds
in the user's language without style control instead of failing outright. No
backend/engine change — the model genuinely can't honor non-EN/ZH instructs, so
this makes the failure graceful and understandable rather than a raw 400.
Test: two cases in voiceInstruct.test.js pin the clone scenario — a fully
Vietnamese instruct yields "" + all items in the unsupported bucket, and a mixed
"whisper, sôi nổi" keeps "whisper" while flagging the prose.
Closes #612
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
mergetest
Claude Opus 4.8
parent
e87c13e919
commit
10b9d6950d
@@ -121,7 +121,22 @@ export default function useTTS({ selectedProfile, setSelectedProfile, loadHistor
|
||||
formData.append("ref_audio", safeBlob, refAudio.name || "audio.wav");
|
||||
formData.append("ref_text", refText);
|
||||
}
|
||||
if (instruct) formData.append("instruct", instruct);
|
||||
// #612: a clone's free-text style field also passes through the backend
|
||||
// instruct whitelist, so raw prose (e.g. a non-EN/ZH description like the
|
||||
// Vietnamese report) 400s with "Unsupported instruct items". Apply the
|
||||
// SAME validator-safe guard the design path uses: keep valid style tags,
|
||||
// drop the rest, and surface a localized warning toast — never round-trip
|
||||
// a 400. vdStates is empty here (clone has no design sliders).
|
||||
if (instruct) {
|
||||
const { instruct: safeInstruct, unsupported, duplicates } = buildDesignInstruct({}, instruct);
|
||||
if (unsupported.length) {
|
||||
toast(t('tts_errors.ignored_unsupported', { items: unsupported.join(', ') }), { icon: '⚠️' });
|
||||
}
|
||||
if (duplicates.length) {
|
||||
toast(t('tts_errors.ignored_duplicate', { items: duplicates.join(', ') }), { icon: '⚠️' });
|
||||
}
|
||||
if (safeInstruct) formData.append("instruct", safeInstruct);
|
||||
}
|
||||
} else {
|
||||
// #526: reuse the pinned seed when "keep this seed" is on (stable
|
||||
// tweaks), else roll a fresh one. The backend echoes the seed it used
|
||||
|
||||
@@ -27,6 +27,22 @@ describe('buildDesignInstruct', () => {
|
||||
expect(duplicates).toEqual([]);
|
||||
});
|
||||
|
||||
it('clone path (#612): non-EN/ZH free-text yields no instruct, all items flagged unsupported', () => {
|
||||
// The clone synthesize path runs free-text through buildDesignInstruct({}, …)
|
||||
// exactly like this. A Vietnamese description must NOT reach the backend (it
|
||||
// 400s with "Unsupported instruct items"); it drops to "" + a warn bucket so
|
||||
// the UI shows a localized toast and synthesis still proceeds (no style).
|
||||
const { instruct, unsupported } = buildDesignInstruct({}, 'quảng cáo, sôi nổi và thu hút');
|
||||
expect(instruct).toBe('');
|
||||
expect(unsupported).toEqual(['quảng cáo', 'sôi nổi và thu hút']);
|
||||
});
|
||||
|
||||
it('clone path keeps valid style tags while dropping prose in the same field', () => {
|
||||
const { instruct, unsupported } = buildDesignInstruct({}, 'whisper, sôi nổi');
|
||||
expect(instruct).toBe('whisper');
|
||||
expect(unsupported).toEqual(['sôi nổi']);
|
||||
});
|
||||
|
||||
it('buckets a valid tag outranked by a dropdown as a duplicate, not unsupported (#114)', () => {
|
||||
const { instruct, unsupported, duplicates } = buildDesignInstruct({ Pitch: 'low pitch' }, 'high pitch');
|
||||
expect(instruct).toBe('low pitch'); // dropdown wins the category
|
||||
|
||||
Reference in New Issue
Block a user