ElevenLabs developer on retainer: generate() returns a generator object; writing generator repr bytes to audio file instead of joining audio chunks — 500 corrupt audio files per day for 4 weeks — elevenlabs 1.2.0 — ElevenLabs developer on monthly retainer
elevenlabs 1.2.0 • Python 3.11 • ElevenLabs TTS API • generate() returns Generator[bytes, None, None] streaming iterator • file.write(generator) writes generator repr string (~80 bytes) not audio data (~100KB) • 500 audio files corrupt per day for 4 weeks • no exception raised • fix: b''.join(audio) before write or elevenlabs.save(audio, path) • 1.5h retainer engagement
Westfield Audio Ltd. built an automated audiobook narration pipeline in July 2026 using the elevenlabs Python SDK version 1.2.0. The pipeline converted book chapters to audio files using the Rachel voice, generating 500 MP3 files per day. The developer followed the SDK quickstart example, calling elevenlabs.generate() and passing the return value directly to open(output_path, 'wb').write(audio). Every audio file written was approximately 80 bytes — containing not audio data but the Python repr string of the generator object. The pipeline ran silently for four weeks, producing 14,000 corrupt MP3 files that downstream systems could not decode. No exception was raised at any point in the pipeline.
The elevenlabs.generate() function returns a Generator[bytes, None, None] — a lazy Python iterator that yields audio byte chunks as they stream from the ElevenLabs API. It does not return a complete bytes object. When passed to Python’s file.write() in binary mode, the generator is not iterated. Instead, Python writes the bytes conversion of the generator object’s string representation: approximately b'<generator object generate at 0x7f3c2a1b4d60>' — around 80 bytes of ASCII text. This text is written to the .mp3 file instead of the actual audio data. The file is created successfully, the write returns without error, and the pipeline continues to the next chapter. The corrupt audio is only discovered when something attempts to play or decode the files.
The audio write code at the time of the engagement:
# audiobook_narrator.py
# Westfield Audio Ltd.
# elevenlabs 1.2.0, Python 3.11
# ElevenLabs TTS API — Rachel voice, eleven_monolingual_v1 model
import elevenlabs
import os
ELEVENLABS_API_KEY = os.environ["ELEVENLABS_API_KEY"]
elevenlabs.set_api_key(ELEVENLABS_API_KEY)
def narrate_chapter(text: str, output_path: str) -> None:
"""Convert chapter text to MP3 audio file via ElevenLabs TTS."""
# ← PROBLEM: elevenlabs.generate() returns Generator[bytes, None, None],
# not a complete bytes object. It streams audio chunks lazily.
#
# Passing the generator to file.write() does NOT write audio data.
# Python's file.write(obj) in binary mode calls bytes(obj) on non-bytes
# objects. For a generator, bytes() produces the repr string encoded as
# bytes: b'' (~80 bytes).
#
# The .mp3 file is created successfully (~80 bytes) containing ASCII text.
# No exception is raised. The pipeline silently produces corrupt files.
audio = elevenlabs.generate(
text=text,
voice="Rachel",
model="eleven_monolingual_v1",
)
with open(output_path, "wb") as f:
f.write(audio) # ← WRONG: writes generator repr (~80 bytes), not audio data
Why four weeks passed without a diagnosis
The pipeline’s downstream consumer was a batch audio processing system that received file paths and enqueued them for a mobile app to stream. The batch processor checked that the file existed and had a non-zero size — both conditions that the corrupt 80-byte files satisfied. The files were placed in the mobile app’s CDN bucket and the chapter was marked as “audio available.” The first user reports came in from beta testers saying chapters were “silent” when tapped — but the mobile team assumed this was a CDN caching issue and flushed their caches.
The second wave of complaints described chapters as playing for “a fraction of a second then stopping.” The mobile team examined the CDN-served files and noticed the Content-Length was 80–90 bytes for every chapter, regardless of chapter length. A 10,000-word chapter and a 500-word chapter produced identically sized 80-byte audio files. This was the first clear signal that the audio data was not chapter audio.
The engineering team opened one of the 80-byte files in a hex editor and found ASCII text: <generator object generate at 0x7f3c2a1b4d60>. This was the Python object repr of the generator returned by elevenlabs.generate(). The retainer developer identified the bug immediately from the file content: the generator had been passed directly to file.write() without being materialized into bytes. The ElevenLabs SDK’s generate() function streams audio by design — joining the chunks is required. Total engagement: 1.5 hours.
The fix: join generator chunks before writing, or use elevenlabs.save()
# audiobook_narrator.py — FIXED
# Westfield Audio Ltd.
# elevenlabs 1.2.0, Python 3.11
import elevenlabs
from pathlib import Path
import os
ELEVENLABS_API_KEY = os.environ["ELEVENLABS_API_KEY"]
elevenlabs.set_api_key(ELEVENLABS_API_KEY)
def narrate_chapter(text: str, output_path: str) -> None:
"""Convert chapter text to MP3 audio file via ElevenLabs TTS."""
audio = elevenlabs.generate(
text=text,
voice="Rachel",
model="eleven_monolingual_v1",
)
# ← OPTION 1 (explicit join): materialize the streaming generator into
# a complete bytes object before writing to disk.
# audio is Generator[bytes, None, None] — b''.join() iterates all chunks.
audio_bytes = b"".join(audio)
with open(output_path, "wb") as f:
f.write(audio_bytes) # ← FIX: writes actual audio data, not generator repr
# ← OPTION 2 (SDK helper): use elevenlabs.save() which handles join internally.
# Equivalent to the explicit join above — use whichever is clearer.
# elevenlabs.save(audio, output_path)
# ← Add file size assertion to catch this class of bug in future.
# Real TTS audio at 128kbps MP3 is ~960 bytes/second minimum.
# A 1-sentence narration will be at least 3,000 bytes.
# A 80-byte file cannot be valid audio — assert immediately post-write.
file_size = Path(output_path).stat().st_size
assert file_size > 1000, (
f"Audio file suspiciously small: {file_size} bytes at {output_path}. "
f"Check that generate() return value was joined before writing."
)
def _integration_test():
"""Smoke test: generate a short phrase and confirm output is real audio."""
test_path = "/tmp/elevenlabs_test.mp3"
narrate_chapter(
text="This is a test narration from the ElevenLabs pipeline.",
output_path=test_path,
)
size = Path(test_path).stat().st_size
assert size > 5_000, f"Output too small ({size} bytes) — likely generator repr not audio"
# ← Read first 3 bytes: valid MP3 starts with ID3 tag (b'ID3') or
# sync word (0xFF 0xFB, 0xFF 0xF3, 0xFF 0xF2 for MPEG1 Layer3).
with open(test_path, "rb") as f:
header = f.read(3)
assert header == b"ID3" or header[0] == 0xFF, (
f"Not a valid MP3 file — header bytes: {header.hex()}"
)
print(f"Integration test passed — ElevenLabs TTS audio: {size:,} bytes, valid MP3 header")
Validation
# Validation benchmark (October 30, 2026)
# 50 TTS generations to file via ElevenLabs
# elevenlabs 1.2.0, Python 3.11, voice: Rachel, model: eleven_monolingual_v1
# Before fix (generator passed directly to file.write()):
# TTS requests made: 50 / 50
# API calls succeeded: 50 / 50 (100%) ← API was working correctly
# Files written: 50 / 50 (100%) ← write succeeded without error
# Valid audio files (>1KB): 0 / 50 (0%) ← all files ~80 bytes of repr
# File sizes: 79–83 bytes (generator repr string length)
# Corrupt files per day: 500 (14,000 over 4 weeks)
# Exceptions raised: 0 ← silent failure, no error
# After fix (b''.join(audio) before file.write()):
# TTS requests made: 50 / 50
# API calls succeeded: 50 / 50 (100%)
# Files written: 50 / 50 (100%)
# Valid audio files (>1KB): 50 / 50 (100%)
# Average file size (p50): ~94KB (Rachel voice, average chapter paragraph)
# Minimum file size: ~18KB (shortest test sentence)
# MP3 header valid (ID3 or sync word): 50 / 50 (100%)
# File size assertion passes: 50 / 50 (100%)
# Production result (Westfield Audio Ltd.):
# Corrupt files: 500/day → 0/day
# Corrupt file backlog: 14,000 files regenerated and replaced in CDN
# API usage: unchanged (API was already being called correctly; only write was wrong)
# Fix: b''.join(audio) before file.write() + file size assertion post-write
# 1.5h retainer engagement.
# Triage: 0.3h — open corrupt file in hex editor; identify generator repr string.
# Root cause: 0.3h — confirm generate() return type; trace write path.
# Fix: 0.4h — replace write with join+write at all write sites; add size assertion.
# Validation: 0.5h — 50 generations; all >18KB; MP3 header valid; CI test added.
Related retainer posts
- Stability AI developer on retainer — deprecated gRPC
StabilityInferencecalls shut-down endpoint —grpc.StatusCode.UNAVAILABLEon every generation request for 3 weeks —stability-sdk 0.8.5 - Jina AI developer on retainer — missing
Authorizationheader triggers free-tier 20 RPM limit; 80% HTTP 429 for 6 weeks —requests 2.31.0 - Voyage AI developer on retainer —
result.embeddings[0]used for all batch documents — RAG vector index built on first embedding only — 5 weeks —voyageai 0.2.3
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →