Classroom TTS fails silently under provider rate limits: 116/133 narration clips lost while the job still reports succeeded
Summary
generateTTSForClassroom() issues TTS requests back-to-back with no throttling and no retry. When the provider enforces a per-minute request cap, every call after the cap is hit fails instantly; the exception is caught and downgraded to log.warn, and the generation job still finishes as succeeded. The classroom ships with most of its narration missing and nothing in the API response or UI signals it.
Environment
- OpenMAIC v1.0.3, commit
2cbd011 - Local
pnpm install && pnpm build && pnpm start - Server-side TTS: MiniMax
speech-2.8-hd/female-yujie(TTS_MINIMAX_API_KEY),tts: truereported byGET /api/health
Reproduction
- Configure a TTS provider whose account has a modest RPM cap.
- Submit a classroom large enough to exceed that cap within the first minute:
POST /api/generate-classroomwith{"requirement": "...", "pdfContent": {"text": "...", "images": []}, "enableTTS": true} - Poll
GET /api/generate-classroom/{jobId}until it reportssucceeded. - Inspect the persisted classroom and its
audio/directory.
Observed
- Job result:
{"status":"succeeded","step":"completed","progress":100,"scenesGenerated":21,"totalScenes":21} - Persisted classroom: 133 speech actions, only 17 with
audioId/audioUrl(12.8%) data/classrooms/{id}/audio/: 17 files- Server log, one line per lost clip:
[WARN] [ClassroomMedia] TTS generation failed for action action_pqJ3xATU: Error: MiniMax TTS error:
No audio returned. Response: {"base_resp":{"status_code":1002,"status_msg":"rate limit exceeded(RPM)"}}Note that the TTS stage reported progress for 133 actions and finished in ~60 seconds: the 116 failures returned instantly instead of being paced or retried.
Root cause
lib/server/classroom-media-generation.ts — the action loop awaits generateTTS() with no delay and no retry, and swallows failures:
for (const action of scene.actions) {
// ...
try {
const result = await generateTTS({ /* ... */ }, speechAction.text);
// ...
} catch (err) {
log.warn(`TTS generation failed for action ${action.id}:`, err);
}
}Two aggravating details:
- MiniMax reports the rate limit as HTTP 200 with
base_resp.status_code = 1002, not 429, so it never becomes aTTSRateLimitError—lib/audio/tts-providers.tsthrows a genericError("MiniMax TTS error: No audio returned. …")instead. - Because failures are non-fatal by design, the job result is indistinguishable from a fully narrated classroom.
Suggested fix
I patched this locally and verified it — happy to open a PR if that is useful:
- Pace the calls (
TTS_MIN_INTERVAL_MS, default 1000ms) and double the interval on every rate-limit pushback (capped at 15s) rather than giving up. - Detect rate limits in both shapes: typed
TTSRateLimitError, or a message matching/rate limit|too many requests|\b429\b|\b1002\b/i. - Report coverage with a single loud summary instead of only per-action warnings (
TTS generation complete: N clips written/TTS generation INCOMPLETE: N written, M speech actions left silent (…)).
Verified after the patch, same key, 9-scene course:
TTS rate limited for tts_s2_action_Pq0e4hkE; widening spacing to 2000ms (retry 1/5)
TTS rate limited for tts_s2_action_xpFYEEHz; widening spacing to 4000ms (retry 1/5)
TTS rate limited for tts_s3_action_CUpA6cpk; widening spacing to 8000ms (retry 1/5)
TTS generation complete: 63 clips written→ 63/63 speech actions got audio, 0 failures (previously 17/133).
A secondary improvement: surface partial TTS coverage in the job result (or at least a warning field) so a silent classroom can never be reported as a clean success.
Source: THU-MAIC/OpenMAIC