Movie Musical Generator / API
Your token

Driving it from your own code

Everything the page does is an ordinary HTTP call against https://api.skillsafe.ai/v1/app-api. Pick a language once with the tabs and every sample on the page follows it.

The envelope

Every response is {"data": ...} on success and {"error": {...}} on failure. Read data, not the top level. The most common mistake against this API is reading one level too shallow: the model's text lives at data.output.output, not at data.output.

CodeMeansWhat to do
400The body did not validateCheck the field names against the table below. Note that /estimate validates almost nothing, so a body it accepts can still be wrong.
401No token, or a stale oneMint a guest token. A cold 401 on a first visit is the correct answer, not a fault.
402Not enough creditsCompare hold_credits against /me before submitting.
403A guest tried to runRuns need a signed-in token. Free calls do not.
404Unknown job, record or collectionCheck the id. A guest sees only rows its own subject wrote.
429Rate limitedBack off. Similarity search is 30/min per IP, tighter than the other endpoints.

1. A tiny client, and a token

The slug goes in the body of /guest. An X-App-Slug header returns 400 slug is required on that endpoint, whatever other docs say.

2. Who am I

/me returns exactly three fields: subject_type, subject_id and credits. There is no username and no email, so the signed-in test is subject_type === "user" and nothing else.

3. What a run will cost

/estimate is free, creates no job and returns the model binding along with the hold. hold_credits is what gets reserved: it prices the full output cap, and the settled charge is usually far lower.

A warning worth more than the sample. This endpoint performs no body validation. A bare string, a number and null all come back ok with a well-formed estimate and the correct model. So a clean estimate proves the model binding and says nothing whatever about whether your input shape is right. Check the field table yourself.

4. Run it, then poll

Pass an Idempotency-Key header on every run. A retry after a network blip must reuse the same key or you pay twice; two different lanes over the same show must use different keys or the second returns the first one's result.

5. Streaming, and what actually arrives

/run-stream is server-sent events. The frame format is a named event line, a data line, then a blank line - not a {"type":"delta"} object, which does not exist on this API and which a surprising number of published samples describe.

In a browser you will not see deltas at all. Measured on this deployment: a page receives tick heartbeats and one final done, while curl receives the full delta stream. That is why this app polls a job rather than building a live preview - a streaming preview on a web page would be dead code.

6. Saved shows

The app declares one collection, shows, readable only by its owner. Records nest their fields under doc.

The input contract

Taken from the code that builds the request, not from intent. Route on task first - it selects which contract the model writes to, and the remaining fields differ by lane. All fields are scalars; there is no nesting anywhere in the input.

Every request

FieldTypeWhat it is
taskstringtreatment or number. Missing or unrecognised falls back to treatment and the reply says so.
stancestringAn id: earnest, wry, operatic, procedural, melancholy, raucous.
stance_wantsstringOne sentence saying what the stance is for. This is the authority on the stance, not the id.
stance_overridesstringComma-separated house-rule names the stance may break. Empty means none.
idiomstringAn id for the score's idiom: golden-age, brass-and-marches, folk-americana, jazz-standard, synth, gospel-soul, chamber-indie, music-hall.
idiom_conventionsstringInstrumentation and song form, in a sentence or two. Described by convention, never by artist.
idiom_overridesstringAs above, for the idiom.
audiencestringall-ages or grown-up.
avoidstringFree text naming things the writer should not use. A constraint on the writing.

task: treatment

FieldTypeWhat it is
premisestringThe premise. Clipped from the middle at 2400 characters, with the cut announced in-band.
actsnumber2 or 3.
numbersnumberHow many songs the list must hold: 6, 8, 10 or 12.

task: number

FieldTypeWhat it is
show_contextstringA plain-text digest of the treatment: title, logline, world, cast, acts, song list. Treated as established fact.
number_briefstringThe one song-list row to write, as labelled lines: SLOT, ACT, TITLE, SINGER, FORM, FUNCTION, CHANGE.
lengthstringshort (three to five sections) or full (five to eight).

A number request in full

{
  "task": "number",
  "show_context": "TITLE: ...\nLOGLINE: ...\nACT I: ...\nSONG 5 (act II): ...",
  "number_brief": "SLOT: 5\nACT: II\nTITLE: ...\nSINGER: ...\nFORM: ...\nFUNCTION: ...\nCHANGE: ...",
  "length": "full",
  "stance": "wry",
  "stance_wants": "...",
  "stance_overrides": "no-narrator-of-record",
  "idiom": "brass-and-marches",
  "idiom_conventions": "...",
  "idiom_overrides": "",
  "audience": "all-ages",
  "avoid": ""
}

The output contract

Plain labelled lines, not JSON. A label in capitals, a colon, then the value. Multi-part values are separated by a vertical bar and the part order is fixed. Labelled lines are used because each one completes on its own: a reply cut off at sixty per cent still yields sixty per cent of a show, where a truncated JSON object yields nothing.

task: treatment

TITLE: <the show's title>
TAGLINE: <one clause>
LOGLINE: <one or two sentences>
WORLD: <two or three sentences>
SCORE: <what the score sounds like>
CAST: <name> | <who they are> | <what they want> | <what they will not admit>
ACT: <roman numeral> | <act title> | <what changes across it>
BEAT: <act> | <beat title> | <what happens>
SONG: <slot> | <act> | <title> | <singer> | <form> | <function> | <what changes>
NOTE: <a short paragraph>

CAST repeats three to five times, ACT exactly acts times, BEAT three to five times per act, and SONG exactly numbers times with slots running continuously from 1 across act breaks.

task: number

NUMBER: <the song title, unchanged from the brief>
PLACEMENT: <act> | <where in the act> | <who is on screen>
FORM: <the musical form>
JOB: <what this number has to accomplish>
SECTION: <a section label>
LYRIC: <one line of the lyric>
CRAFT: rhyme | <the scheme, and where it departs>
CRAFT: meter | <the syllable shape>
CRAFT: turn | <where the number changes>
CRAFT: staging | <what the camera and the room are doing>
NOTE: <a short paragraph>

SECTION and LYRIC interleave: each section label is followed by its own lyric lines, one line per LYRIC. All four CRAFT keys are emitted, in that order.

The refusal

REFUSED: <one sentence naming what could not be done and what to ask instead>

Emitted alone, with no other labels, and only when nothing is left of the request after the content rules have been applied. The far more common outcome is a full result plus a NOTE: clause saying what was changed - an original riff instead of an adaptation, a fictional character instead of a named living person, a genre instead of a named writer's voice.

Parsing it

Split on newlines. A line matching ^([A-Z][A-Z0-9_]{1,14})\s*:\s*(.*)$ is a labelled line; anything else is a continuation of the line above it. Split multi-part values on | and trim. If the reply was truncated, drop only a trailing line that is demonstrably unfinished - running that repair on a complete reply deletes its last line, which in a lyric is the whole point of the song.

What it will not write

The app writes original material only. It will not reproduce the words of an existing song in any form, will not hand back a real film or musical's plot, characters or song titles under a new name, will not attribute anything to a real writer, composer or performer, and will not build a show on a living person's private life. Those rules bind every field of every reply, and a request that cannot be served without breaking one comes back as the version that can be written, with the change named on the NOTE: line.