tts_language_script_mismatch: Korean TTS needs a Hangul syllable
Sume TTS returns 400 tts_language_script_mismatch when language is ko but the text has no Hangul syllable. Usual cause: UTF-8 decoded as Latin-1. Fix it.

A Sume text-to-speech request with language: "ko" fails with HTTP 400 tts_language_script_mismatch when the transcript does not contain at least one Hangul syllable (the range U+AC00 to U+D7A3). The error is raised before a job is created, so nothing is queued and nothing is charged. In practice the text is almost never really non-Korean: it is Korean that was decoded with the wrong character set somewhere upstream, and the fix is to repair the text, not the request.
The check is the same on all three TTS entry points: POST /v1/tts-1.0/generate, POST /v1/models/sume/tts-1.0/runs and POST /v1/tts-router/generate.
What exactly does the 400 look like?
The error body carries code and public_reason set to tts_language_script_mismatch, category: validation, retryable: false, next_action: fix_input and details.field: transcript. The message reads along the lines of "Korean TTS requires at least one Hangul syllable in transcript. Check the original text and its encoding before retrying." Because retryable is false, a blind retry with the same body will fail the same way.
| Transcript sent with language ko | Hangul syllable present? | Result |
|---|---|---|
| Korean text decoded as Latin-1 (mojibake) | No | 400 tts_language_script_mismatch |
| "Hello from the live show." | No | 400 tts_language_script_mismatch |
| "123 !?" | No | 400 tts_language_script_mismatch |
| Bare jamo such as ㄱㄴㄷ | No (jamo are not syllables) | 400 tts_language_script_mismatch |
| 오늘 라이브에서 소개합니다. | Yes | Accepted, job queued |
Why is my Korean text suddenly Latin-looking?
The failure that motivated the check came from a production pipeline in which UTF-8 Korean was decoded as Latin-1: each Hangul syllable turns into three accented Latin characters and control characters, which still look like text to a JSON encoder. The usual places this happens are a CSV or spreadsheet export opened with the wrong encoding, a webhook or queue consumer that reads bytes with a default of Latin-1, and a shell or Windows console code page that rewrites the string before curl sends it.
Sending that text to a TTS provider would have produced gibberish audio and a charge. Rejecting it at the door is the cheaper outcome.
How do I repair the text and resend?
If the damage is exactly a UTF-8 to Latin-1 mix-up, it is reversible: encode the string back to Latin-1 bytes and decode those bytes as UTF-8. Check the result for a Hangul syllable before you submit.
- Fix the source: set the reader to UTF-8 where the text first enters your system, instead of patching strings at the end.
- Keep
language: "ko". The Sume contract says never to translate a non-English request into English, and a Korean request is not a reason to drop the field. - If a line is truly not Korean (an English product name on its own), send it with
language: "en", or put it inside a Korean sentence.
import re
original = "오늘 라이브에서 소개합니다."
broken = original.encode("utf-8").decode("latin-1")
fixed = broken.encode("latin-1").decode("utf-8")
print(fixed == original)
print(bool(re.search("[가-힣]", fixed)))Does the check work the other way?
Partly. When language is omitted, Sume infers ko from a transcript that is mostly Hangul and ja from one that is mostly kana, and otherwise the provider default of English applies. That inference is a fallback for those two scripts only. For every other non-English language, set language yourself; the contract describes it as the language the voice speaks the transcript in, and it is the field to set for every non-English transcript.
Separately, a voice whose saved language differs from the requested one can return a 409 or an MCP warning that needs a confirmation. That is a different gate, covered in the related posts below, and it does not involve the script check.
Checklist before you resubmit
Run these four checks in your own code before the request leaves, because the API will not tell you which upstream step damaged the text.
- Print
len(text)and the first 20 characters from the exact value you send, not from your editor. - Assert that
re.search("[가-힣]", text)is true wheneverlanguage == "ko". - Log the HTTP body, not just the status:
public_reasonanddetails.fieldtell you the transcript is the problem. - Remember the cost model: TTS is billed per transcript character at $0.0475 per 1,000 characters, and a 400 like this one never reaches that meter.
Sources
Related posts
More in Developers
- Kotlin 2.4.20 allDistinctBy: unique Idempotency-Keys per batch
Kotlin 2.4.20 adds allDistinctBy. Use it to assert one Idempotency-Key per intent before a batch of Sume image submits, then post with java.net.http.
- Calling the Sume API from Kotlin: no SDK, so write the poll loop
Sume publishes a TypeScript SDK and no Kotlin package. The documented submit, poll, fetch loop works from OkHttp or Ktor; the deadline lives in your client.
- Kubb for the Sume OpenAPI spec: Zod schemas and SWR hooks
Kubb parses an OpenAPI spec once and feeds plugins for TypeScript, Zod and SWR. Here is how that maps to Sume's 3.0.3 spec, jobs and the polling stop rule.
- LangChain4j StreamableHttpMcpTransport: protocol detection for Sume
LangChain4j probes the MCP protocol on connect, which costs one round trip. Pin protocolVersion and set timeouts before pointing it at Sume's hosted MCP server.
Written by Sume