#1567·OpenMAIC

Classroom TTS fails silently under provider rate limits: 116/133 narration clips lost while the job still reports succeeded

Author: llbz510Created Sep 17, 2026Updated Sep 17, 2026

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: true reported by GET /api/health

Reproduction

  1. Configure a TTS provider whose account has a modest RPM cap.
  2. Submit a classroom large enough to exceed that cap within the first minute: POST /api/generate-classroom with {"requirement": "...", "pdfContent": {"text": "...", "images": []}, "enableTTS": true}
  3. Poll GET /api/generate-classroom/{jobId} until it reports succeeded.
  4. 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:

typescript
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:

  1. MiniMax reports the rate limit as HTTP 200 with base_resp.status_code = 1002, not 429, so it never becomes a TTSRateLimitErrorlib/audio/tts-providers.ts throws a generic Error("MiniMax TTS error: No audio returned. …") instead.
  2. 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:

  1. 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.
  2. Detect rate limits in both shapes: typed TTSRateLimitError, or a message matching /rate limit|too many requests|\b429\b|\b1002\b/i.
  3. 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.