Drive Stanzabar from your own code
Everything the board asks the model is available over HTTP. A natural use is a songbook kept as data - a worship team's set, a teacher's class songs, a band's demos in ChordPro - where a script sends one song's arrangement, asks for lines that fit the metre, revises the lines a check flagged, or asks for a producer's critique.
One thing to be clear about first: the model is checked, not trusted. The caller sends the board's own syllable count and rhyme letter for every line; the reply carries the model's count and letter for every line it writes, and the board recounts both before anything is offered. Section ids, line numbers and chord positions must exist, and a chord substitution must be diatonic, borrowed or a secondary dominant in the song's key. The board's logic ships as plain scripts you can load in node — see the end of the examples.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"stanzabar"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object: never wrap it as {"input": {…}} — that hides every field from the model, and /estimate warns unknown field 'input'.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the model lanes are metered and need a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
One system prompt serves three tasks. task picks the contract; a missing or unknown task is answered as critique, named in the reply's task.
| task | When | What targets holds |
|---|---|---|
lyrics | Write lyric lines for chosen sections, in each section's metre and rhyme scheme, to the brief. | [{"section":"S4","lines":4,"keep_existing":false}] |
revise | Rewrite only the marked lines; everything else is context. | [{"section":"S2","n":[4]}] |
critique | Critique the arrangement: hook placement, section verdicts with lengths, moves, one chord substitution per section. | [] |
Every run body is a JSON object of string fields (the declared input schema is scalar, so object-valued fields are JSON-encoded strings; task, title, song, targets and facts are required, and /estimate warns when one is missing).
| Field | What it holds |
|---|---|
task | "lyrics", "revise" or "critique". |
title | The song title. |
brief | JSON string: {"theme","mood","audience","pov"} - any may be empty. |
song | JSON string: {"key":"G","key_label":"G major","tempo":104,"meter":"4/4","sections":[{"id":"S2","type":"verse","label":"Verse 1","start_bar":5,"bars":8,"metre":"8","scheme":"ABAB","chords":[["1.1","G","I","diatonic"],["2.1","C","IV","diatonic"]],"lines":[{"n":1,"text":"...","syllables":8,"rhyme":"A","target":8}]}]}. chords entries are [bar.beat, symbol, roman, category]; metre is a number, a dotted hymn metre (8.6.8.6) or CM / LM / SM; scheme cycles over the lines, X = unrhymed. A line may carry range ("8-9") and marked. A long non-target section may arrive as its first and last line plus {"n":2,"omitted":k}. |
targets | JSON string, per task (table above). |
facts | JSON string: the board's free checks - total_bars, duration, beats_per_bar, the key's diatonic and borrowed chords, repeated lines (hooks) and check findings (checks). |
why | revise only, optional: what the person wants changed. |
clipped | Optional: what was left out of the input. |
retry_note | Optional: sent once when a previous reply was not valid JSON. |
The reply text is one JSON object:
// task "lyrics" (and "revise", which returns only the marked line numbers)
{ "task": "lyrics",
"sections": [{ "section": "S4", "lines": [{ "n": 1, "text": "...", "syllables": 8, "rhyme": "A", "note": "..." }] }],
"hook": { "text": "...", "where": "S3.1" },
"notes": ["..."] }
// task "critique"
{ "task": "critique", "summary": "...",
"hook": { "section": "S4", "n": 1, "advice": "..." },
"sections": [{ "section": "S1", "verdict": "keep|trim|extend|cut|move|rewrite", "bars": 4, "advice": "..." }],
"moves": [{ "section": "S4", "to": 3, "reason": "..." }],
"chords": [{ "section": "S2", "bar": 3, "beat": 1, "from": "Em", "to": "C", "reason": "..." }],
"notes": ["..."] }
Check before you apply: recount every line (the board's estimator is Lyric.lineSyl), re-derive rhyme letters from the section as it would read after the change (Lyric.scheme), refuse lines for sections or line numbers that were not targets, refuse a from chord that is not on that bar, and refuse a to chord that is chromatic in the key (Theory.analyze(chord, key).category).
Worked examples
These are the bundled examples on the page: the exact run body the page builds and a reply produced against the shipped prompt (the page replays the same replies for free).
lyrics: a 90th-birthday singalong - write verse 2 and the bridge
Input (targets names the two empty sections):
{
"task": "lyrics",
"title": "Ninety Candles",
"brief": "(a JSON string - decoded below)",
"song": "(a JSON string - decoded below)",
"targets": "(a JSON string - decoded below)",
"facts": "(a JSON string - decoded below)"
}
brief, decoded:
{
"theme": "Grandma Rose turns ninety: her rose garden by the gate, her terrible jokes, the kitchen radio she sings over",
"mood": "warm, funny, singalong",
"audience": "family party - three generations sing the chorus",
"pov": "we"
}
song, decoded:
{
"key": "G",
"key_label": "G major",
"tempo": 104,
"meter": "4/4",
"sections": [
{
"id": "S1",
"type": "intro",
"label": "Intro",
"start_bar": 1,
"bars": 4,
"chords": [
[
"1.1",
"G",
"I",
"diatonic"
],
[
"2.1",
"C",
"IV",
"diatonic"
],
[
"3.1",
"G",
"I",
"diatonic"
],
[
"4.1",
"D",
"V",
"diatonic"
]
],
"lines": []
},
{
"id": "S2",
"type": "verse",
"label": "Verse 1",
"start_bar": 5,
"bars": 8,
"metre": "8",
"scheme": "ABAB",
"chords": [
[
"1.1",
"G",
"I",
"diatonic"
],
[
"2.1",
"C",
"IV",
"diatonic"
],
[
"3.1",
"Em",
"vi",
"diatonic"
],
[
"4.1",
"D",
"V",
"diatonic"
],
[
"5.1",
"G",
"I",
"diatonic"
],
[
"6.1",
"C",
"IV",
"diatonic"
],
[
"7.1",
"Em",
"vi",
"diatonic"
],
[
"8.1",
"D",
"V",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "She planted roses by the gate",
"syllables": 8,
"rhyme": "A",
"target": 8
},
{
"n": 2,
"text": "Before the rest of us were born",
"syllables": 8,
"rhyme": "B",
"target": 8
},
{
"n": 3,
"text": "She never let a supper wait",
"syllables": 8,
"rhyme": "A",
"target": 8
},
{
"n": 4,
"text": "And sang along to every storm",
"syllables": 9,
"rhyme": "B",
"range": "8-9",
"target": 8
}
]
},
{
"id": "S3",
"type": "chorus",
"label": "Chorus",
"start_bar": 13,
"bars": 8,
"metre": "7",
"scheme": "AABB",
"chords": [
[
"1.1",
"G",
"I",
"diatonic"
],
[
"2.1",
"D",
"V",
"diatonic"
],
[
"3.1",
"Em",
"vi",
"diatonic"
],
[
"4.1",
"C",
"IV",
"diatonic"
],
[
"5.1",
"G",
"I",
"diatonic"
],
[
"6.1",
"D",
"V",
"diatonic"
],
[
"7.1",
"Em",
"vi",
"diatonic"
],
[
"8.1",
"C",
"IV",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "Ninety candles, Grandma Rose",
"syllables": 7,
"rhyme": "A",
"target": 7
},
{
"n": 2,
"text": "Light them up from head to toes",
"syllables": 7,
"rhyme": "A",
"target": 7
},
{
"n": 3,
"text": "Every year the garden grows",
"syllables": 8,
"rhyme": "A",
"range": "7-8",
"target": 7
},
{
"n": 4,
"text": "Happy birthday, Grandma Rose",
"syllables": 7,
"rhyme": "A",
"target": 7
}
]
},
{
"id": "S4",
"type": "verse",
"label": "Verse 2",
"start_bar": 21,
"bars": 8,
"metre": "8",
"scheme": "ABAB",
"chords": [
[
"1.1",
"G",
"I",
"diatonic"
],
[
"2.1",
"C",
"IV",
"diatonic"
],
[
"3.1",
"Em",
"vi",
"diat
... (shortened here)
targets, decoded:
[
{
"section": "S4",
"lines": 4,
"keep_existing": false
},
{
"section": "S6",
"lines": 4,
"keep_existing": false
}
]
facts, decoded:
{
"total_bars": 56,
"duration": "2:09",
"beats_per_bar": 4,
"diatonic": [
"G",
"Am",
"Bm",
"C",
"D",
"Em",
"F#dim"
],
"borrowed": [
"Cm",
"Eb",
"F",
"Bb",
"Dm",
"Adim",
"A7",
"E7",
"B7"
],
"hooks": [
{
"text": "Ninety candles, Grandma Rose",
"at": [
"S3.1",
"S5.1",
"S7.1"
]
},
{
"text": "Light them up from head to toes",
"at": [
"S3.2",
"S5.2",
"S7.2"
]
},
{
"text": "Every year the garden grows",
"at": [
"S3.3",
"S5.3",
"S7.3"
]
},
{
"text": "Happy birthday, Grandma Rose",
"at": [
"S3.4",
"S5.4",
"S7.4"
]
}
],
"checks": []
}
Reply:
{
"task": "lyrics",
"sections": [
{
"section": "S4",
"lines": [
{
"n": 1,
"text": "She tells the one about the crow",
"syllables": 8,
"rhyme": "A",
"note": "brings in her terrible jokes"
},
{
"n": 2,
"text": "We groan the way we groaned before",
"syllables": 8,
"rhyme": "B",
"note": "the family's running reaction, a little comedy"
},
{
"n": 3,
"text": "Then sings above the radio",
"syllables": 8,
"rhyme": "A",
"note": "the kitchen radio she sings over"
},
{
"n": 4,
"text": "And spins us round the kitchen floor",
"syllables": 8,
"rhyme": "B",
"note": "ends on action, keeps chorus words out"
}
]
},
{
"section": "S6",
"lines": [
{
"n": 1,
"text": "So pass the spade and pass the seeds",
"syllables": 8,
"rhyme": "A",
"note": "turn: the family takes over the garden"
},
{
"n": 2,
"text": "We'll keep her roses free of weeds",
"syllables": 8,
"rhyme": "A",
"note": "shifts to future tense, a promise"
},
{
"n": 3,
"text": "And when the grandkids roll their eyes",
"syllables": 8,
"rhyme": "B",
"note": "the next generation enters"
},
{
"n": 4,
"text": "We'll tell her jokes and act surprised",
"syllables": 8,
"rhyme": "B",
"note": "we inherit the bad jokes; near rhyme"
}
]
}
],
"notes": [
"The bridge turns from describing Grandma Rose to the family promising to carry on her garden and her jokes.",
"S6 line 4 is a near rhyme (eyes/surprised) on purpose."
]
}
revise: a worship song - three lines marked that break common metre
Input (the marked lines carry "marked": true; targets lists them):
{
"task": "revise",
"title": "Morning Light",
"brief": "(a JSON string - decoded below)",
"song": "(a JSON string - decoded below)",
"targets": "(a JSON string - decoded below)",
"facts": "(a JSON string - decoded below)",
"why": "Keep the plain worship words; make each marked line sing in its metre and keep the rhyme."
}
brief, decoded:
{
"theme": "God's steady faithfulness from morning to evening; plain words a congregation can sing on first hearing",
"mood": "calm, grateful, building in the bridge",
"audience": "Sunday worship team and congregation",
"pov": "I / you (to God)"
}
song, decoded:
{
"key": "D",
"key_label": "D major",
"tempo": 76,
"meter": "4/4",
"sections": [
{
"id": "S1",
"type": "intro",
"label": "Intro",
"start_bar": 1,
"bars": 4,
"chords": [
[
"1.1",
"D",
"I",
"diatonic"
],
[
"2.1",
"G",
"IV",
"diatonic"
],
[
"3.1",
"D",
"I",
"diatonic"
],
[
"4.1",
"A",
"V",
"diatonic"
]
],
"lines": []
},
{
"id": "S2",
"type": "verse",
"label": "Verse 1",
"start_bar": 5,
"bars": 16,
"metre": "CM",
"scheme": "ABAB",
"chords": [
[
"1.1",
"D",
"I",
"diatonic"
],
[
"3.1",
"G",
"IV",
"diatonic"
],
[
"5.1",
"D",
"I",
"diatonic"
],
[
"7.1",
"A",
"V",
"diatonic"
],
[
"9.1",
"Bm",
"vi",
"diatonic"
],
[
"11.1",
"G",
"IV",
"diatonic"
],
[
"13.1",
"A",
"V",
"diatonic"
],
[
"15.1",
"D",
"I",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "When morning breaks across the hills",
"syllables": 8,
"rhyme": "A",
"target": 8
},
{
"n": 2,
"text": "And mist lifts off the lake",
"syllables": 6,
"rhyme": "B",
"target": 6
},
{
"n": 3,
"text": "Your quiet mercy gently fills",
"syllables": 8,
"rhyme": "A",
"target": 8
},
{
"n": 4,
"text": "Every single breath that I take",
"syllables": 9,
"rhyme": "B",
"range": "8-9",
"target": 6,
"marked": true
}
]
},
{
"id": "S3",
"type": "chorus",
"label": "Chorus",
"start_bar": 21,
"bars": 8,
"metre": "7",
"scheme": "AABB",
"chords": [
[
"1.1",
"G",
"IV",
"diatonic"
],
[
"2.1",
"D",
"I",
"diatonic"
],
[
"3.1",
"A",
"V",
"diatonic"
],
[
"4.1",
"Bm",
"vi",
"diatonic"
],
[
"5.1",
"G",
"IV",
"diatonic"
],
[
"6.1",
"D",
"I",
"diatonic"
],
[
"7.1",
"A",
"V",
"diatonic"
],
[
"8.1",
"Bm",
"vi",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "Morning light, you call my name",
"syllables": 7,
"rhyme": "A",
"target": 7
},
{
"n": 2,
"text": "Every day you stay the same",
"syllables": 8,
"rhyme": "A",
"range": "7-8",
"target": 7
},
{
"n": 3,
"text": "Hold me when the shadows fall",
"syllables": 7,
"rhyme": "B",
"target": 7
},
{
"n": 4,
"text": "You are faithful through it all",
"syllables": 7,
"rhyme": "B",
"target": 7
}
]
},
{
"id": "S4",
"type": "verse",
"label": "Verse 2",
"start_bar": 29,
"bars": 16,
"metre": "CM",
"scheme": "ABAB",
"chords": [
[
"1.1",
"D",
"I",
"diatonic"
],
[
"3.1",
"G",
"IV",
"diatonic"
],
[
"5.
... (shortened here)
targets, decoded:
[
{
"section": "S2",
"n": [
4
]
},
{
"section": "S4",
"n": [
4
]
},
{
"section": "S6",
"n": [
3
]
}
]
facts, decoded:
{
"total_bars": 68,
"duration": "3:35",
"beats_per_bar": 4,
"diatonic": [
"D",
"Em",
"F#m",
"G",
"A",
"Bm",
"C#dim"
],
"borrowed": [
"Gm",
"Bb",
"C",
"F",
"Am",
"Edim",
"E7",
"B7",
"F#7"
],
"hooks": [
{
"text": "Morning light, you call my name",
"at": [
"S3.1",
"S5.1",
"S7.1"
]
},
{
"text": "Every day you stay the same",
"at": [
"S3.2",
"S5.2",
"S7.2"
]
},
{
"text": "Hold me when the shadows fall",
"at": [
"S3.3",
"S5.3",
"S7.3"
]
},
{
"text": "You are faithful through it all",
"at": [
"S3.4",
"S5.4",
"S7.4"
]
}
],
"checks": [
"S2.4 metre: Verse 1 line 4: 9 (8-9) syllables against a target of 6.",
"S4.4 metre: Verse 2 line 4: 9 syllables against a target of 6.",
"S4.2 orphan: Verse 2 line 2 should rhyme by the ABAB scheme but has no partner (“low”).",
"S4.4 orphan: Verse 2 line 4 should rhyme by the ABAB scheme but has no partner (“now”).",
"S6.3 metre: Bridge line 3: 13 (12-13) syllables against a target of 8.",
"S6 echo: Bridge gives away the chorus: “morning”, “light” also open the chorus."
]
}
Reply:
{
"task": "revise",
"sections": [
{
"section": "S2",
"lines": [
{
"n": 4,
"text": "In every breath I take",
"syllables": 6,
"rhyme": "B",
"note": "was 9 syllables against 6; keeps the lake/take rhyme"
}
]
},
{
"section": "S4",
"lines": [
{
"n": 4,
"text": "And at your feet let go",
"syllables": 6,
"rhyme": "B",
"note": "was 9 with filler now; go now rhymes with low"
}
]
},
{
"section": "S6",
"lines": [
{
"n": 3,
"text": "Nothing I do could earn this love",
"syllables": 8,
"rhyme": "B",
"note": "was 13 syllables against 8; same thought, unrhymed X line"
}
]
}
],
"notes": [
"Bridge lines 1, 2 and 4 sit at 7 against 8; a held note covers them, so they were left alone.",
"The bridge's last line still echoes the chorus with morning light; mark it if you want that changed."
]
}
critique: a band demo with a late chorus, a long verse and a chromatic chord
Input:
{
"task": "critique",
"title": "Night Bus",
"brief": "(a JSON string - decoded below)",
"song": "(a JSON string - decoded below)",
"targets": "(a JSON string - decoded below)",
"facts": "(a JSON string - decoded below)"
}
brief, decoded:
{
"theme": "riding the last bus home after a breakup, watching other passengers, not ready to arrive",
"mood": "moody indie, restrained then lifting",
"audience": "a four-piece band's demo for a battle of the bands",
"pov": "I"
}
song, decoded:
{
"key": "Am",
"key_label": "A minor",
"tempo": 92,
"meter": "4/4",
"sections": [
{
"id": "S1",
"type": "intro",
"label": "Intro",
"start_bar": 1,
"bars": 8,
"chords": [
[
"1.1",
"Am",
"i",
"diatonic"
],
[
"2.1",
"F",
"VI",
"diatonic"
],
[
"3.1",
"C",
"III",
"diatonic"
],
[
"4.1",
"G",
"VII",
"diatonic"
],
[
"5.1",
"Am",
"i",
"diatonic"
],
[
"6.1",
"F",
"VI",
"diatonic"
],
[
"7.1",
"C",
"III",
"diatonic"
],
[
"8.1",
"G",
"VII",
"diatonic"
]
],
"lines": []
},
{
"id": "S2",
"type": "verse",
"label": "Verse 1",
"start_bar": 9,
"bars": 8,
"metre": "8",
"scheme": "ABAB",
"chords": [
[
"1.1",
"Am",
"i",
"diatonic"
],
[
"2.1",
"F",
"VI",
"diatonic"
],
[
"3.1",
"C",
"III",
"diatonic"
],
[
"4.1",
"G",
"VII",
"diatonic"
],
[
"5.1",
"Am",
"i",
"diatonic"
],
[
"6.1",
"F",
"VI",
"diatonic"
],
[
"7.1",
"C",
"III",
"diatonic"
],
[
"8.1",
"G",
"VII",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "The ninety-four is running late",
"syllables": 8,
"rhyme": "A",
"target": 8
},
{
"n": 2,
"text": "The windows sweat with city air",
"syllables": 8,
"rhyme": "B",
"target": 8
},
{
"n": 3,
"text": "I trace your name and watch the gate",
"syllables": 8,
"rhyme": "A",
"target": 8
},
{
"n": 4,
"text": "Of every stop that isn't there",
"syllables": 9,
"rhyme": "B",
"range": "8-9",
"target": 8
}
]
},
{
"id": "S3",
"type": "verse",
"label": "Verse 2",
"start_bar": 17,
"bars": 16,
"metre": "8",
"scheme": "ABAB",
"chords": [
[
"1.1",
"Am",
"i",
"diatonic"
],
[
"2.1",
"F",
"VI",
"diatonic"
],
[
"3.1",
"C",
"III",
"diatonic"
],
[
"4.1",
"G",
"VII",
"diatonic"
],
[
"5.1",
"Am",
"i",
"diatonic"
],
[
"6.1",
"F",
"VI",
"diatonic"
],
[
"7.1",
"C",
"III",
"diatonic"
],
[
"8.1",
"G",
"VII",
"diatonic"
],
[
"9.1",
"Am",
"i",
"diatonic"
],
[
"10.1",
"F",
"VI",
"diatonic"
],
[
"11.1",
"C",
"III",
"diatonic"
],
[
"12.1",
"G",
"VII",
"diatonic"
],
[
"13.1",
"Am",
"i",
"diatonic"
],
[
"14.1",
"F",
"VI",
"diatonic"
],
[
"15.1",
"C",
"III",
"diatonic"
],
[
"16.1",
"G",
"VII",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "A man in orange sleeps it off",
"syllable
... (shortened here)
targets, decoded:
[]
facts, decoded:
{
"total_bars": 72,
"duration": "3:08",
"beats_per_bar": 4,
"diatonic": [
"Am",
"Bdim",
"C",
"Dm",
"Em",
"F",
"G"
],
"borrowed": [
"D",
"E",
"G#dim",
"A",
"Bm",
"B7",
"C7"
],
"hooks": [
{
"text": "Night bus, carry me home",
"at": [
"S4.1",
"S7.1"
]
},
{
"text": "Through the orange and the chrome",
"at": [
"S4.2",
"S7.2"
]
},
{
"text": "Every light a little flame",
"at": [
"S4.3",
"S7.3"
]
},
{
"text": "Every stop without your name",
"at": [
"S4.4",
"S7.4"
]
}
],
"checks": [
"S6 chromatic: Bridge bar 2: Eb is outside A minor (neither diatonic nor borrowed).",
"S6 chromatic: Bridge bar 6: Eb is outside A minor (neither diatonic nor borrowed).",
"S3 echo: Verse 2 gives away the chorus: “night”, “bus” also open the chorus."
]
}
Reply:
{
"task": "critique",
"summary": "The chorus is a real hook - \"Night bus, carry me home\" is the title, it opens the chorus and the Dm-Eb-E bridge gives the song a genuine harmonic turn. The biggest problem is the wait: an 8-bar intro, Verse 1 and a 16-bar Verse 2 back to back put the first chorus at bar 33, about 1:23 at 92 bpm, far too late for a battle-of-the-bands set.",
"hook": {
"section": "S4",
"n": 1,
"advice": "The hook sits in the right place (S4 line 1, repeated at S7.1), but it only lands twice and the first time is at bar 33, roughly 1:23 in. Move the first chorus straight after Verse 1 and trim the intro, and it arrives at bar 13 (about 0:31). At 6 syllables against a 7 target, let the band hold \"home\" for the extra beat rather than padding the line."
},
"sections": [
{
"section": "S1",
"verdict": "trim",
"bars": 4,
"advice": "8 bars of Am-F-C-G before a word is sung is the same loop the verse uses, so nothing is lost by halving it. Four bars, one pass of the progression, then the vocal."
},
{
"section": "S2",
"verdict": "keep",
"advice": "Strong, concrete opening - the late ninety-four and sweating windows set the scene. Line 4 \"Of every stop that isn't there\" can sing as 9; sing \"ev'ry\" to keep it on 8."
},
{
"section": "S3",
"verdict": "trim",
"bars": 8,
"advice": "16 bars for four 8-syllable lines means each line is stretched over four bars or the progression loops twice with half of it empty. Cut to 8 bars like Verse 1. Also rewrite line 4: \"And still the night bus rolls away\" gives away the chorus's opening words right before it - try an image of the bus itself (the doors folding shut, the route sign) without \"night\" or \"bus\"."
},
{
"section": "S4",
"verdict": "keep",
"advice": "Good hook and a clean F-G-Am lift. Lines 3 and 4 run 7-8 against 7; sing \"ev'ry\" to keep both on 7 and the hook line punchier."
},
{
"section": "S5",
"verdict": "keep",
"advice": "Keep the solo for the battle-of-the-bands showcase, but after the reorder it follows Verse 2, so build it: half-time drums for 4 bars, full band for the last 4, so it climbs into the bridge instead of idling on the verse loop."
},
{
"section": "S6",
"verdict": "keep",
"advice": "This is the best section on the board: the lyric turns the question back on the singer and the Dm-Eb-Am-E move changes the harmonic colour completely. Drop the band to bass and voice for the first 4 bars so the final chorus has somewhere to rise from."
},
{
"section": "S7",
"verdict": "extend",
"bars": 16,
"advice": "The final chorus should be the peak, but it is identical to the first. Double it to 16 bars - the second pass with the full band, a higher harmony on \"carry me home\" and the last line repeated."
},
{
"section": "S8",
"verdict": "trim",
"bars": 4,
"advice": "Another 8 bars of the verse loop after the peak drains the energy. Four bars, ending on Am, or a single repeat of the hook line sung alone over the last chord - the bus pulling away."
}
],
"moves": [
{
"section": "S4",
"to": 3,
"reason": "Verse 1 and Verse 2 run back to back with no chorus between them, 24 bars of the same Am-F-C-G loop. Put the first chorus after Verse 1 so the hook arrives near 0:31 and Verse 2 has something to return from."
}
],
"chords": [
{
"section": "S2",
"bar": 8,
"beat": 1,
"from": "G",
"to": "E",
"reason": "With the chorus now following Verse 1, an E (V) on the last bar pulls hard toward A minor and then side-steps to the chorus's F - a deceptive lift that announces the hook."
},
{
"section": "S4",
"bar": 8,
"beat": 1,
"from": "Am",
"to": "E",
"reason": "Two bars of Am close the chorus flat; an E on bar 8 creates tension that carries the band into Verse 2 instead of stopping."
},
{
"section": "S6",
"bar": 6,
"beat": 1,
"from": "Eb",
"to": "E",
"reason": "Keep the first Eb at bar 2 - it is the bridge's jolt, the moment the song looks out the window. On the repeat, E (V) makes a real cadence into Am on bar 7 and resolves the doubt."
}
],
"notes": [
"The Eb in S
... (shortened here; the page shows it whole)
lyrics: a 12-bar blues school song - every AAB verse still to write
Input:
{
"task": "lyrics",
"title": "Sort It Out",
"brief": "(a JSON string - decoded below)",
"song": "(a JSON string - decoded below)",
"targets": "(a JSON string - decoded below)",
"facts": "(a JSON string - decoded below)"
}
brief, decoded:
{
"theme": "recycling week at school: paper, cans, bottles and the compost bin - sorting rubbish is a superpower",
"mood": "bouncy, call and response, a little silly",
"audience": "children aged 7-8 singing at a school assembly",
"pov": "we"
}
song, decoded:
{
"key": "E",
"key_label": "E major",
"tempo": 116,
"meter": "4/4",
"sections": [
{
"id": "S1",
"type": "intro",
"label": "Intro",
"start_bar": 1,
"bars": 4,
"chords": [
[
"1.1",
"E7",
"I7",
"borrowed"
],
[
"2.1",
"A7",
"IV7",
"borrowed"
],
[
"3.1",
"E7",
"I7",
"borrowed"
],
[
"4.1",
"B7",
"V7",
"diatonic"
]
],
"lines": []
},
{
"id": "S2",
"type": "verse",
"label": "Verse 1",
"start_bar": 5,
"bars": 12,
"metre": "10",
"scheme": "AAB",
"chords": [
[
"1.1",
"E7",
"I7",
"borrowed"
],
[
"2.1",
"E7",
"I7",
"borrowed"
],
[
"3.1",
"E7",
"I7",
"borrowed"
],
[
"4.1",
"E7",
"I7",
"borrowed"
],
[
"5.1",
"A7",
"IV7",
"borrowed"
],
[
"6.1",
"A7",
"IV7",
"borrowed"
],
[
"7.1",
"E7",
"I7",
"borrowed"
],
[
"8.1",
"E7",
"I7",
"borrowed"
],
[
"9.1",
"B7",
"V7",
"diatonic"
],
[
"10.1",
"A7",
"IV7",
"borrowed"
],
[
"11.1",
"E7",
"I7",
"borrowed"
],
[
"12.1",
"B7",
"V7",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "",
"syllables": 0,
"rhyme": "",
"target": 10
},
{
"n": 2,
"text": "",
"syllables": 0,
"rhyme": "",
"target": 10
},
{
"n": 3,
"text": "",
"syllables": 0,
"rhyme": "",
"target": 10
}
]
},
{
"id": "S3",
"type": "verse",
"label": "Verse 2",
"start_bar": 17,
"bars": 12,
"metre": "10",
"scheme": "AAB",
"chords": [
[
"1.1",
"E7",
"I7",
"borrowed"
],
[
"2.1",
"E7",
"I7",
"borrowed"
],
[
"3.1",
"E7",
"I7",
"borrowed"
],
[
"4.1",
"E7",
"I7",
"borrowed"
],
[
"5.1",
"A7",
"IV7",
"borrowed"
],
[
"6.1",
"A7",
"IV7",
"borrowed"
],
[
"7.1",
"E7",
"I7",
"borrowed"
],
[
"8.1",
"E7",
"I7",
"borrowed"
],
[
"9.1",
"B7",
"V7",
"diatonic"
],
[
"10.1",
"A7",
"IV7",
"borrowed"
],
[
"11.1",
"E7",
"I7",
"borrowed"
],
[
"12.1",
"B7",
"V7",
"diatonic"
]
],
"lines": [
{
"n": 1,
"text": "",
"syllables": 0,
"rhyme": "",
"target": 10
},
{
"n": 2,
"text": "",
"syllables": 0,
"rhyme": "",
"target": 10
},
{
"n": 3,
"text": "",
"syllables": 0,
"rhyme": "",
"target": 10
}
]
},
{
"id": "S4",
"type": "solo",
"label": "Kazoo solo",
"start_bar": 29,
"bars": 12,
"chords": [
[
"1.1",
"E7",
"I7",
"borrowed"
],
[
"2.1",
"E7",
"I7",
"borrowed"
],
[
... (shortened here)
targets, decoded:
[
{
"section": "S2",
"lines": 3,
"keep_existing": false
},
{
"section": "S3",
"lines": 3,
"keep_existing": false
},
{
"section": "S5",
"lines": 3,
"keep_existing": false
}
]
facts, decoded:
{
"total_bars": 56,
"duration": "1:56",
"beats_per_bar": 4,
"diatonic": [
"E",
"F#m",
"G#m",
"A",
"B",
"C#m",
"D#dim"
],
"borrowed": [
"Am",
"C",
"D",
"G",
"Bm",
"F#dim",
"F#7",
"C#7",
"G#7"
],
"hooks": [],
"checks": []
}
Reply:
{
"task": "lyrics",
"sections": [
{
"section": "S2",
"lines": [
{
"n": 1,
"text": "Paper in the paper bin, nice and flat",
"syllables": 10,
"rhyme": "A",
"note": "leader's call: the first job, paper"
},
{
"n": 2,
"text": "We said paper, paper bin, nice and flat",
"syllables": 10,
"rhyme": "A",
"note": "everyone answers, blues repeat of line 1"
},
{
"n": 3,
"text": "Sorting is our superpower, hooray!",
"syllables": 10,
"rhyme": "B",
"note": "answer line states the big idea"
}
]
},
{
"section": "S3",
"lines": [
{
"n": 1,
"text": "Stamp on the empty cans and hear them crunch",
"syllables": 10,
"rhyme": "A",
"note": "call: a stomping action for the cans"
},
{
"n": 2,
"text": "Squish the fizzy cans from yesterday's lunch",
"syllables": 10,
"rhyme": "A",
"note": "varied repeat with a silly lunch detail"
},
{
"n": 3,
"text": "Bottles go clink-clink-clink, then in they go",
"syllables": 10,
"rhyme": "B",
"note": "answer moves on to bottles, sound effect"
}
]
},
{
"section": "S5",
"lines": [
{
"n": 1,
"text": "Apple cores and peels in the compost bin",
"syllables": 10,
"rhyme": "A",
"note": "call: last stop, the compost bin"
},
{
"n": 2,
"text": "Banana skins, the wriggly worms dig in",
"syllables": 10,
"rhyme": "A",
"note": "varied repeat, worms make it silly"
},
{
"n": 3,
"text": "Sort it out, and the whole school takes a bow",
"syllables": 10,
"rhyme": "B",
"note": "title lands in the final answer line"
}
]
}
],
"hook": {
"text": "Sort it out, and the whole school takes a bow",
"where": "S5.3"
},
"notes": [
"No chorus on the board, so each verse ends on its own answer line; the title closes Verse 3.",
"Line 1 of each verse suits a leader or one class, line 2 the whole hall."
]
}
The board counts and checks; the API does not
Over the API you get the model's lines and advice as JSON. The counting, the rhyme letters and the key checks are yours to run, and the board's own code is plain scripts you can load in node after global.window = {}, in this order: theory.js (Theory.analyze(chord, key), Theory.transpose(chord, from, to), Theory.diatonic(key)), lyric.js (Lyric.lineSyl(text), Lyric.scheme(lines), Lyric.rhymes(a, b)), song.js (Song.buildInput(song, {task, targets, why}) for this exact run body, Song.toChordPro(song), Song.parseChordPro(text)) and reconcile.js (Recon.normalize(Recon.parseResult(text), task) then Recon.reconcile(result, song, {task, targets}) for the proposals and their flags).
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://stanzabar.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://stanzabar.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// stanzabar.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://stanzabar.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://stanzabar.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// Stanzabar.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class Stanzabar {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://stanzabar.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://stanzabar.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://stanzabar.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://stanzabar.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"stanzabar"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "stanzabar"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "stanzabar" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "stanzabar"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "stanzabar"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "stanzabar" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "stanzabar"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "stanzabar" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "lyrics", "title": "...", "brief": "{...}", "song": "{...}", "targets": "[...]", "facts": "{...}"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "lyrics", "title": "...", "brief": "{...}", "song": "{...}", "targets": "[...]", "facts": "{...}"}
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task, title, brief, song, targets, facts}
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses stanzabar:<task>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="stanzabar:lyrics:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "stanzabar:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the lane's JSON object
print(j["charged_credits"], j.get("truncated"), len(reply.get("sections", [])))
import { createHash } from "node:crypto";
const key = `stanzabar:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.sections && reply.sections.length);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "stanzabar:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "stanzabar:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "stanzabar:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
$key = "stanzabar:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"stanzabar:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: stanzabar:lyrics:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "lyrics", "title": "...", "brief": "{...}", "song": "{...}", "targets": "[...]", "facts": "{...}"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "stanzabar:lyrics:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `stanzabar:lyrics:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "stanzabar:lyrics:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "stanzabar:lyrics:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "stanzabar:lyrics:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: stanzabar:lyrics:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"stanzabar:lyrics:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);