Count TTS characters first: Sume counts spaces and punctuation
Sume TTS bills every character including spaces and punctuation, up to 20,000 per request. A runnable Python counter prices a script on Sume and MAI.

Sume TTS counts every character of the transcript, spaces and punctuation included, at $0.0475 per 1,000 characters with a 1-cent minimum and a 20,000-character maximum per request, so count characters with len() rather than estimating from words. The 57-character sample line below holds 9 words, which is 5.70 characters per word; your scripts will differ, and the ratio shifts the price.
Counting and pricing in 10 lines
This script counts a string and prices it at the Sume rate with its 1-cent minimum and at MAI-Voice-2.1 and Flash. It prints whether the text fits one Sume request. It makes no network calls.
script = "Fresh pears, picked this morning. Two for $3, today only!"
chars = len(script)
words = len(script.split())
rates = {"Sume TTS": 47.5, "MAI-Voice-2.1": 22.0, "MAI-Voice-2.1-Flash": 15.0}
print("characters:", chars, "words:", words, "chars/word: %.2f" % (chars / words))
for name, per_million in rates.items():
cost = chars * per_million / 1_000_000
if name == "Sume TTS":
cost = max(0.01, cost)
print("%-22s $%.6f" % (name, cost))
print("fits one Sume request:", chars <= 20000)Why the count must be exact
At a 20,000-character request cap, a script that is 20,001 characters needs two jobs, and if the second job is one character the second job still costs the 1-cent minimum. Smart quotes, long dashes and emoji count as characters, and a line break counts too if it is in the string you send. If you strip markup or stage directions before sending, count after stripping, since the catalog counts the transcript field.
The catalog's pricing_basis says the reservation is computed from the transcript character count, so a request with extra whitespace reserves more than the same text trimmed.
| Measure | Value |
|---|---|
| Characters | 57 |
| Words | 10 |
| Characters per word | 5.70 |
| Words in a 20,000-character request at this ratio | 3,509 |
| Sume cost (1-cent minimum applies) | $0.01 |
| MAI-Voice-2.1 cost | $0.001254 |
Splitting a long script
For a script over 20,000 characters, split on a paragraph or sentence boundary below the cap, send each part as its own job with its own idempotency key, and join the results with Timeline audio concat at $0.01 per job, up to 20 parts. The join is sample-domain, so there is no re-synthesis and no silence at the seam.
Pre-flight checks worth adding
Three small checks save money. First, assert that the count is at most 20,000 before you submit, since a longer script is refused. Second, flag lines under 211 characters, because each bills the 1-cent minimum and may be cheaper to merge into a neighbor. Third, compare the count of the text you send with the count of the text you approved; if you strip markup in between, the approved script and the billed script are different strings.
For a 12-line script of 180 characters per line, those checks tell you immediately that the lines are under the floor, that the total of 2,160 characters would cost $0.1026 as one request, and that the same text sent as twelve requests would bill 12 cents. That is the same trade-off the cost post on single-line fixes works through: one file is cheaper and per-line files are easier to edit.
A last detail is cheap to verify: count with the same string you send. Windows line endings add a character per line, and trailing spaces count as well, so normalize the text first and count after that.
Sources
Related posts
More in Developers
- curl -d to Sume /v1/videos gives 415: add the JSON Content-Type
A POST /v1/videos with a body that is not application/json gets 415 unsupported_media_type, with the content type you sent in details. The curl fix.
- Dart: submit a 30-second Wan 3.0 clip with package:http
Server-side Dart with package:http: POST wan-3.0 for 30 seconds, poll until completed and write the MP4. Keep the key out of the Flutter app.
- Deno 2.9.7: scope permissions to a Sume key check on GET /v1/me
Run a Sume API key check in Deno 2.9.7 with allow-net limited to api.sume.com and allow-env limited to the key, and see what the permission error looks like.
- Deno 2.9.7 traceparent fix: tag a Sume job id in a Deno.serve log
Deno 2.9.7 extracts traceparent from Deno.serve regardless of header case. In a Sume webhook handler, log the job_id with the trace so a render is traceable.
Written by Sume