Jaipong: A Two-Stage Text-to-Song Studio and the Case for a Line-Oriented Plan Format

Romi Nur Ismanto
Independent AI Research Lab, Jakarta, Indonesia
hello@rominur.com
August 2026

Abstract

Jaipong, deployed at jaipong.rominur.com, turns one Indonesian sentence into a complete sung song: a language model writes the title, style direction and lyrics, and a music generation model performs them as real audio. The two models have different latencies, different failure modes and — decisively — different economics, since the music model is priced per song rather than per token. The application is therefore built as two serverless endpoints rather than one, each finishing well inside its time budget, with the expensive quota enforced at the endpoint where the money is actually spent. This paper describes three design decisions that carry the system. First, the song plan is written in a line-oriented format instead of JSON: each field occupies one line, so the title reaches the screen the moment its line completes, every lyric line follows as it is written, and a generation truncated mid-stream degrades to the last whole line instead of to a parse error — there is no malformed JSON to repair because there is no JSON. Second, generated audio travels to the browser as base64 chunks over server-sent events, split on four-character boundaries so every chunk decodes independently, and the file format is identified by sniffing its first bytes rather than trusting the request parameters — the music model returns MP3 even when asked for WAV. Third, the server keeps nothing: song metadata lives in the browser's localStorage and the audio itself in IndexedDB, while the API key and model names exist only on the server. The result is a studio in which regional Indonesian traditions — jaipongan, dangdut koplo, keroncong, gamelan — sit beside pop and EDM as one-tap presets, and in which the worst possible day of abuse has a known price ceiling.

Keywords: text-to-song generation, music generation models, streaming parsers, server-sent events, Web Audio API, IndexedDB, cost governance, rate limiting, Indonesian music, Next.js

1. Introduction

The application is named after jaipongan, the kendang-driven dance music of West Java — a tradition that was itself a deliberate act of synthesis, assembled in the 1970s from older Sundanese forms into something new and popular. The name states the ambition: not a generic music toy with an Indonesian translation layer, but a studio in which an Indonesian speaker describes a song in their own language and receives one back in it, with jaipongan, dangdut koplo, keroncong and gamelan offered as first-class styles rather than exotic afterthoughts.

Getting there requires two different models. A language model is good at titles, structure and lyrics — and at following instructions such as “use these lyrics exactly as written.” A music generation model is good at sound — singing included — but is expensive in a categorically different way: it is priced per song, not per token, at roughly $0.08 per generation. The system's architecture follows from taking that difference seriously rather than hiding both models behind one endpoint.

Section 2 describes the two-stage pipeline and why it is two endpoints. Section 3 describes the plan format, which is the paper's central argument. Section 4 covers moving audio over SSE. Section 5 covers playback. Section 6 covers client-owned storage and the server's deliberate statelessness. Section 7 covers cost governance. Section 8 states the limits.

2. Two Stages, Two Endpoints

Composition runs in two stages, both through OpenRouter, each behind its own serverless endpoint.

Table 1. The two stages of song composition.
StageEndpointModel (default)ProducesPriced
1. Plan/api/composeGemini FlashTitle, genre, style paragraph, lyrics — streamed line by linePer token
2. Audio/api/renderLyria 3 ProFinished stereo audio plus time-stamped lyricsPer song

The split is not decorative. Each stage finishes well inside a serverless function's time budget, where a single combined endpoint would have to survive the sum of both models' worst cases. More important, the split lets each endpoint enforce the quota appropriate to its cost: the plan stage is cheap and rate-limited loosely; the render stage is where an actual per-song charge occurs, so the burst, daily and global limits of Section 7 are checked there — at the door where the money leaves.

sentence → /api/compose (LLM writes the plan, streamed) → user sees lyrics as they are written → /api/render (music model sings it) → base64 audio over SSE → IndexedDB → Web Audio playback

The user's explicit choices survive both stages: a vocal selection (female, male, instrumental) and a supplied title override whatever the model guessed, and lyrics the user provides are passed to the music model with the instruction to sing them verbatim — the model is asked not to edit, extend or translate them. The form also accepts free-style tags, and twelve one-tap genre presets fill them in — Pop Indonesia, Dangdut Koplo, Jaipong Sunda, Keroncong, Gamelan Ambient, Balada Galau, Rock Anthem, Lo-fi, EDM, Reggae, Jazz Kafe and Cinematic — with target durations from 45 seconds to 4 minutes.

3. The Plan Format, or: No JSON to Repair

The natural implementation of stage one asks the language model for a JSON object. Jaipong deliberately does not. The plan is a line-oriented text format, one field per line, lyrics as plain lines under section labels:

JUDUL: Goyang Panen Rampak
GENRE: Jaipong Sunda
TAG: jaipong, kendang sunda, suling, ceria
VOKAL: wanita
GAYA: Jaipong Sunda bertempo lincah sekitar 120 BPM, kendang rapat…
LIRIK:
[Verse 1]
Pare koneng di sawah
Hate bungah sumringah

Two properties fall out of this choice, and both are user-visible. The first is streamability: a parser fed the model's output token by token can act on every completed line immediately. The implementation is a small incremental parser that buffers until each newline; when the JUDUL line completes, the title is already on the user's screen — while the model is still writing the first verse — and every lyric line follows the moment it exists. Waiting for a JSON object to close before parsing it forfeits exactly this.

The second property is graceful truncation. A generation cut off mid-stream — by a token limit, a network fault, or an upstream hiccup — leaves a prefix of whole lines, and a prefix of whole lines is a valid, usable plan. Truncated JSON, by contrast, is a syntax error, and the standard remedies (retry the call, or attempt mechanical repair of a dangling brace) either spend money again or risk silently corrupting the content. In this format the failure mode is “the song has one verse fewer,” not “the song does not parse.”

The parser is deliberately forgiving in the same spirit: section labels such as [Chorus] are recognised and withheld from the lyric stream, a style paragraph that wraps across lines is folded back together, and the vocal field accepts natural-language variants and normalises them. The format asks the model for the smallest amount of discipline it reliably has — put each field on its own line — and no more.

4. Audio over SSE, and Trusting the File

Stage two streams the finished audio back through the same server-sent-events channel used for status and lyrics. The audio arrives from the upstream model as base64; the server re-chunks it on four-character boundaries before forwarding, because base64 decodes in quanta of four characters — a chunk split anywhere else is undecodable on its own. Aligned chunks let the browser decode and accumulate the file incrementally instead of holding a single giant string in memory.

The first chunk carries one more obligation: identifying what the file actually is. The request asks the model for WAV; the model sends MP3 anyway. Rather than trusting the request parameter — or the response's claimed format — the server sniffs the first bytes: a RIFF header means WAV, an ID3 tag or an MP3 frame-sync means MP3, and the sniffed answer sets the MIME type the browser will use. The principle generalises and is worth stating plainly: at a model boundary, believe the bytes, not the negotiation.

A final guard closes the stage: if the accumulated audio is implausibly small — under roughly 30 KB, the size of no real song — the render is declared failed rather than saved, so the library never accumulates silent, empty tracks that a user would discover only on pressing play.

Alongside the audio, the music model returns the lyrics it actually sang, time-stamped per line ([12.0:] Pare koneng di sawah). The parser for these is again forgiving — lines without timestamps are accepted as untimed lyrics — so a format drift upstream degrades the karaoke display rather than emptying it.

5. Playback: Web Audio, Not the Audio Element

The obvious way to play an MP3 is an <audio> element. Jaipong instead decodes the full file into an AudioBuffer and plays it through the Web Audio API, for a reason that only appears in testing: autoplay policy. A media element's play() is judged by the browser's user-activation rules at each call, and a play started programmatically — say, when generation finishes a minute after the user's last click — can simply be refused. An AudioContext, once unlocked by any user gesture, stays unlocked; playback through it is governed by a decision the user already made.

The choice has a second dividend. The application inherited an analyser-driven spectrum visualizer from its own earlier synthesizer engine (Section 8); a buffer-source player plugs into the same analyser node, so the visualizer, the volume control and the seek logic serve both players unchanged. Time-stamped lyrics drive a three-tier karaoke panel — past lines dimmed, the active line lit and slightly enlarged, coming lines waiting — with the active line held centred in its own panel rather than scrolling the page. Transport is also on the keyboard: space toggles play, arrow keys seek.

6. The Browser Owns the Audio

The server generates songs; it does not keep them. Library metadata — titles, genres, tags, timed lyrics — lives in localStorage, and the audio files themselves live in IndexedDB, which is built for blobs of this size. A song evicted from the library takes its audio with it. There is no server-side database, no account system, and no stored copy of anything a user has made: the operator could not produce a user's songs if asked, because they exist only in that user's browser.

The same boundary is drawn around configuration. Every environment variable is read server-side only — none carries the NEXT_PUBLIC_ prefix that Next.js would inline into client bundles — so the OpenRouter key and even the model names never reach the browser. A health endpoint reports whether the service is configured without disclosing what it is configured with.

7. Paying per Song: Cost Governance

An open website with no login attached to a model priced per song is an invitation to an unbounded bill, and the mitigation has to be architectural rather than hopeful. Jaipong's quotas are layered where Section 2 put them — at the render endpoint, where the charge occurs:

Table 2. Default quota layers on song generation.
LayerDefaultBounds
Burst, per IP6 songs / 5 minutesScripted rapid-fire from one address
Daily, per IP40 songs / dayOne address's total consumption
Daily, global150 songs / dayThe operator's worst possible day: ≈$12

The global layer is the one that matters, because it converts an open-ended risk into a number the operator chose. At $0.08 per song, 150 songs is about $12; the ceiling is set in one environment variable and can follow the budget. The per-IP counters live in process memory by default, which on serverless means each warm instance counts separately — the limits are softer than their numbers. The README says so rather than pretending otherwise, and an optional Upstash Redis backing makes the counters exact across instances when precision is worth a dependency. The global cap is the backstop either way.

8. Compatibility: The Old Synthesizer Stays

Jaipong's first incarnation did not call a music model at all: the language model wrote a full score — notes, chords, drum grooves — and a hand-built Web Audio synthesizer engine performed it in the browser, with music-theory, instrument and groove modules and a WAV exporter. The music-model rebuild made that engine obsolete for new songs and deleted none of it: score-era songs in a user's library still play through the old engine and still export to WAV, while new tracks play through the buffer player of Section 5, both feeding the same analyser. Keeping a superseded engine alive purely so that nobody's saved songs die is unglamorous, and it is what a library feature owes its users.

9. Limits

Duration is a request, not a contract. The target duration is passed to the music model as an instruction; the model decides where the song actually ends.

Lyric fidelity is likewise asked, not enforced. The music model is instructed to sing supplied lyrics verbatim, and mostly does; nothing downstream can force it. The timed lyrics it returns describe what it sang, which is the honest record.

Per-IP quotas are approximate on serverless. In-memory counters reset with instances; only the Redis option makes them exact. The global cap, checked the same way, shares the imprecision but bounds the damage.

The library is as durable as the browser's storage. IndexedDB survives normal use but yields to a cleared profile or storage pressure; there is no server copy to restore from — the price of Section 6, stated plainly.

Generated audio cannot be edited. A song that is almost right is regenerated, not tweaked; the system offers no stems, no remix, no partial re-render.

10. Conclusion

Jaipong's useful lessons are mostly about seams. Between the user and the language model, a line-oriented plan format buys streaming and truncation-tolerance that JSON structurally cannot offer, at the cost of a forty-line parser. Between the language model and the music model, a plain-text plan is the entire interface, so either model can be swapped by environment variable. Between the music model and the browser, format-sniffing the first bytes — rather than believing the request parameters — absorbs an upstream that sends MP3 when asked for WAV. And between the operator and an open internet, a per-song-priced model is made deployable by placing the quota at the endpoint where the money moves and capping the worst day at a chosen number. None of these is specific to music; all of them recur wherever generative models with unequal economics are composed into one product.

References

  1. Manuel, P., and Baier, R. “Jaipongan: Indigenous Popular Music of West Java.” Asian Music 18(1), 1986.
  2. Agostinelli, A., et al. “MusicLM: Generating Music from Text.” arXiv:2301.11325, 2023.
  3. Google DeepMind. “Lyria: High-Fidelity Music Generation.” Model documentation, 2026.
  4. OpenRouter. “API Reference: Streaming and Multimodal Outputs.” 2026.
  5. WHATWG. “HTML Living Standard — Server-Sent Events.” 2026.
  6. WHATWG. “HTML Living Standard — Media Elements and User Activation.” 2026.
  7. W3C. “Web Audio API.” W3C Recommendation, 2021.
  8. W3C. “Indexed Database API 3.0.” W3C Working Draft, 2026.
  9. Josefsson, S. “The Base16, Base32, and Base64 Data Encodings.” RFC 4648, 2006.
  10. Ismanto, R. N. “Somat: A Single-Box Indonesian AI Chat for Text, Images and Documents.” 2026.