Drive Lyrics Generator from your own code
Everything the web app does goes through one public surface.
Base URL: https://api.skillsafe.ai/v1/app-api.
Every request carries Authorization: Bearer <token>.
One thing to know before you start: the client-side checking that makes this app what it is -
the syllable counting, the rhyme-scheme derivation, the originality check - happens
in the browser, not on the server. Over the API you get the model's reply and its own
claims about its work. If you want the reply checked, run the same modules yourself:
syllable.js, prosody.js, known.js, origin.js
and songscan.js are all served from this origin and none of them touch the network.
The envelope
Every response is one of these two shapes.
{"ok": true, "data": { ... }, "meta": { "request_id": "req_...", "timestamp": "..." }}
{"ok": false, "error": { "code": "...", "message": "...", "status": 400, "details": {} }}
Error codes
unauthorized (401) | No token, or an expired one. On a cold start this is the correct answer, not a fault. |
|---|---|
insufficient_credits (402) | Balance below min_credits. Estimate first and you will never see it. |
forbidden (403) | The token belongs to another app. |
not_found (404) | Unknown job id or collection. |
validation_error (400) | The body was not shaped as the app expects. See error.details. |
rate_limited (429) | Back off and retry. Never tight-loop. |
The input
task comes first and decides everything else. It is
"write" or "revise".
task | Required. "write" or "revise". |
|---|---|
subject | Required. What the song is about, in your words. |
mood | Required object: id, label, overrides, overrides_rule, wants, brief. overrides names the one house craft rule this mood may break. |
genre | Required object: id, label, overrides, overrides_rule, conventions. |
plan | Required array of {kind, label}, in order. kind is one of intro, verse, prechorus, chorus, bridge, refrain, outro. |
devices | The device glossary, as "id: description" strings. A vocabulary for describing finished work, not a checklist. |
crowding | {count, note} - how many known titles touch this subject. A number, deliberately never a list. |
shape | Optional. {syllables_per_line: [min, max], even: bool}. |
perspective | Optional. Who is speaking, and to whom. |
avoid | Optional. Words, images or moves to stay away from. |
prior | revise only. {title, sections: [{label, kind, lines: []}]}. |
directive | revise only. What you want changed. |
The output
Plain text in blocks, not JSON. A block starts with a marker alone on a line; inside it every
line is either key: value or a lyric line prefixed with a pipe and a space. The
format was chosen because a stream gets cut, and a truncated block list is a shorter block list
while a truncated JSON object is nothing at all.
== SONG == title: <the title> premise: <one sentence on what the song is actually about> == SECTION == kind: verse label: Verse 1 function: <what this section does that no other section does> rhyme: <one letter per line; x means rhymes with nothing> syllables: <one number per line, space separated> | <one line of the lyric> | <one line of the lyric> == HOOK == line: <the hook, character for character as it appears in its section> lands: <which section and line, and why there> why: <what makes it a hook> == BRIDGE == does: <what the bridge does that the verses do not> against: <the verse move it works against> == CRAFT == devices: <glossary ids, comma separated> voice: <who speaks; sentence length; whether it evaluates; line endings> note: <two to five sentences of craft talk> == ORIGINALITY == statement: <that these words are newly written> risk: <where the pull toward something existing was strongest>
One == SECTION == block per plan entry, same order, same labels.
rhyme carries one letter per lyric line and syllables one number per
lyric line - both are the model's claims, and the web app recomputes both and shows
you where they disagree.
1. A tiny client helper
Every call below goes through one function. The envelope is always {"ok":true,"data":{...}} or {"ok":false,"error":{"code","message","status"}}, so unwrapping it once here means never unwrapping it again.
# Every call is the same three things: the base URL, a bearer token, and JSON.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN"
call() { # call METHOD PATH [BODY]
curl -s -X "$1" "$BASE$2" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
${3:+-d "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method=method)
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
payload = json.loads(r.read())
if not payload.get("ok"):
raise RuntimeError(payload["error"]["code"] + ": " + payload["error"]["message"])
return payload["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
async function call(method, path, body) {
const res = await fetch(BASE + path, {
method,
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const payload = await res.json();
if (!payload.ok) throw new Error(payload.error.code + ": " + payload.error.message);
return payload.data;
}
package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(method, path string, body any) (json.RawMessage, error) {
var rdr io.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var e envelope
if err := json.NewDecoder(res.Body).Decode(&e); err != nil {
return nil, err
}
if !e.OK {
return nil, errors.New(e.Error.Code + ": " + e.Error.Message)
}
return e.Data, nil
}
import java.net.URI;
import java.net.http.*;
public class Lyric {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String body) throws Exception {
HttpRequest.BodyPublisher pub = body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body);
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, pub).build();
return HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body();
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
def call(method, path, body = nil)
uri = URI(BASE + path)
klass = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.fetch(method)
req = klass.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload["error"]["code"]}: #{payload["error"]["message"]}" unless payload["ok"]
payload["data"]
end
<?php
$BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN";
function call($method, $path, $body = null) {
global $BASE, $TOKEN;
$ch = curl_init($BASE . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $TOKEN,
"Content-Type: application/json",
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new Exception($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
static class Lyric {
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN";
static readonly HttpClient Http = new HttpClient();
public static async Task<string> Call(HttpMethod method, string path, string body = null) {
var req = new HttpRequestMessage(method, Base + path);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (body != null)
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
return await res.Content.ReadAsStringAsync();
}
}
2. Get a token
Easiest route: open the token page, sign in, and copy the token. For a script, mint a guest token instead - a guest can browse and estimate, and needs credits to run.
curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" \
-d '{"slug":"lyrics-generator"}'
guest = call("POST", "/guest", {"slug": "lyrics-generator"})
TOKEN = guest["token"]
const guest = await call("POST", "/guest", { slug: "lyrics-generator" });
// TOKEN = guest.token
raw, err := call("POST", "/guest", map[string]string{"slug": "lyrics-generator"})
// unmarshal raw into a struct carrying Token
String guest = call("POST", "/guest", "{\"slug\":\"lyrics-generator\"}");
// read .data.token out of guest
guest = call("POST", "/guest", { "slug" => "lyrics-generator" })
TOKEN = guest["token"]
$guest = call("POST", "/guest", ["slug" => "lyrics-generator"]);
// $TOKEN = $guest["token"];
var guest = await Lyric.Call(HttpMethod.Post, "/guest", "{\"slug\":\"lyrics-generator\"}");
// read .data.token out of guest
3. Check who you are and what you have
GET /me returns exactly three fields: subject_type (user or guest), subject_id, and credits. A cold-start 401 here is correct rather than a fault - it means no token has been presented yet.
call GET /me
me = call("GET", "/me")
print(me["subject_type"], me["credits"])
const me = await call("GET", "/me");
console.log(me.subject_type, me.credits);
raw, err := call("GET", "/me", nil)
// {"subject_type":"user","subject_id":"...","credits":12345}
System.out.println(call("GET", "/me", null));
me = call("GET", "/me")
puts me["subject_type"], me["credits"]
$me = call("GET", "/me");
echo $me["subject_type"], " ", $me["credits"];
Console.WriteLine(await Lyric.Call(HttpMethod.Get, "/me"));
4. Price the run before you make it
POST /estimate costs nothing and starts no job. It returns hold_credits (what will be reserved), min_credits (the floor below which the run is refused), and the model binding. It does not validate your input. A bare string, a null and an empty array all come back ok:true with a correct model binding, so check the shape of your own body before you send it - this app's client does, in mustBeObject().
call POST /estimate '{"task":"write","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives, end-stopped, nothing judged out loud","brief":"its chorus may refuse to claim anything at all; the flatness is the position"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line, never the reverse","conventions":"the title line lands at the end of the chorus; specificity in objects"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"},{"kind":"verse","label":"Verse 2"},{"kind":"chorus","label":"Chorus 2"},{"kind":"bridge","label":"Bridge"},{"kind":"chorus","label":"Chorus 3"}],"devices":["catalogue: an accumulation of particulars, unranked","time-stamp: a specific hour, day or season fixes the scene"],"crowding":{"count":4,"note":"4 titles in the reference index touch this subject."}}'
brief = {
"task": "write",
"subject": "the last shift at a petrol station that is closing down",
"mood": {
"id": "deadpan",
"label": "Deadpan",
"overrides": "chorus-claim",
"overrides_rule": "the chorus makes a claim, not a summary",
"wants": "flat declaratives, end-stopped, nothing judged out loud",
"brief": "its chorus may refuse to claim anything at all; the flatness is the position"
},
"genre": {
"id": "country",
"label": "Country",
"overrides": "rhyme-serves",
"overrides_rule": "the rhyme serves the line, never the reverse",
"conventions": "the title line lands at the end of the chorus; specificity in objects"
},
"plan": [
{
"kind": "verse",
"label": "Verse 1"
},
{
"kind": "chorus",
"label": "Chorus 1"
},
{
"kind": "verse",
"label": "Verse 2"
},
{
"kind": "chorus",
"label": "Chorus 2"
},
{
"kind": "bridge",
"label": "Bridge"
},
{
"kind": "chorus",
"label": "Chorus 3"
}
],
"devices": [
"catalogue: an accumulation of particulars, unranked",
"time-stamp: a specific hour, day or season fixes the scene"
],
"crowding": {
"count": 4,
"note": "4 titles in the reference index touch this subject."
}
}
est = call("POST", "/estimate", brief)
print(est["hold_credits"], est["model"], est["model_alias"], est["markup_bps"])
const brief = {"task":"write","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives, end-stopped, nothing judged out loud","brief":"its chorus may refuse to claim anything at all; the flatness is the position"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line, never the reverse","conventions":"the title line lands at the end of the chorus; specificity in objects"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"},{"kind":"verse","label":"Verse 2"},{"kind":"chorus","label":"Chorus 2"},{"kind":"bridge","label":"Bridge"},{"kind":"chorus","label":"Chorus 3"}],"devices":["catalogue: an accumulation of particulars, unranked","time-stamp: a specific hour, day or season fixes the scene"],"crowding":{"count":4,"note":"4 titles in the reference index touch this subject."}};
const est = await call("POST", "/estimate", brief);
console.log(est.hold_credits, est.model, est.model_alias, est.markup_bps);
var brief map[string]any
json.Unmarshal([]byte(`{"task":"write","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives, end-stopped, nothing judged out loud","brief":"its chorus may refuse to claim anything at all; the flatness is the position"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line, never the reverse","conventions":"the title line lands at the end of the chorus; specificity in objects"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"},{"kind":"verse","label":"Verse 2"},{"kind":"chorus","label":"Chorus 2"},{"kind":"bridge","label":"Bridge"},{"kind":"chorus","label":"Chorus 3"}],"devices":["catalogue: an accumulation of particulars, unranked","time-stamp: a specific hour, day or season fixes the scene"],"crowding":{"count":4,"note":"4 titles in the reference index touch this subject."}}`), &brief)
raw, err := call("POST", "/estimate", brief)
// {"hold_credits":...,"min_credits":...,"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000}
String brief = "{\"task\":\"write\",\"subject\":\"the last shift at a petrol station that is closing down\",\"mood\":{\"id\":\"deadpan\",\"label\":\"Deadpan\",\"overrides\":\"chorus-claim\",\"overrides_rule\":\"the chorus makes a claim, not a summary\",\"wants\":\"flat declaratives, end-stopped, nothing judged out loud\",\"brief\":\"its chorus may refuse to claim anything at all; the flatness is the position\"},\"genre\":{\"id\":\"country\",\"label\":\"Country\",\"overrides\":\"rhyme-serves\",\"overrides_rule\":\"the rhyme serves the line, never the reverse\",\"conventions\":\"the title line lands at the end of the chorus; specificity in objects\"},\"plan\":[{\"kind\":\"verse\",\"label\":\"Verse 1\"},{\"kind\":\"chorus\",\"label\":\"Chorus 1\"},{\"kind\":\"verse\",\"label\":\"Verse 2\"},{\"kind\":\"chorus\",\"label\":\"Chorus 2\"},{\"kind\":\"bridge\",\"label\":\"Bridge\"},{\"kind\":\"chorus\",\"label\":\"Chorus 3\"}],\"devices\":[\"catalogue: an accumulation of particulars, unranked\",\"time-stamp: a specific hour, day or season fixes the scene\"],\"crowding\":{\"count\":4,\"note\":\"4 titles in the reference index touch this subject.\"}}";
System.out.println(call("POST", "/estimate", brief));
brief = JSON.parse(%q({"task":"write","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives, end-stopped, nothing judged out loud","brief":"its chorus may refuse to claim anything at all; the flatness is the position"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line, never the reverse","conventions":"the title line lands at the end of the chorus; specificity in objects"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"},{"kind":"verse","label":"Verse 2"},{"kind":"chorus","label":"Chorus 2"},{"kind":"bridge","label":"Bridge"},{"kind":"chorus","label":"Chorus 3"}],"devices":["catalogue: an accumulation of particulars, unranked","time-stamp: a specific hour, day or season fixes the scene"],"crowding":{"count":4,"note":"4 titles in the reference index touch this subject."}}))
est = call("POST", "/estimate", brief)
puts est["hold_credits"], est["model"], est["model_alias"]
$brief = json_decode('{"task":"write","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives, end-stopped, nothing judged out loud","brief":"its chorus may refuse to claim anything at all; the flatness is the position"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line, never the reverse","conventions":"the title line lands at the end of the chorus; specificity in objects"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"},{"kind":"verse","label":"Verse 2"},{"kind":"chorus","label":"Chorus 2"},{"kind":"bridge","label":"Bridge"},{"kind":"chorus","label":"Chorus 3"}],"devices":["catalogue: an accumulation of particulars, unranked","time-stamp: a specific hour, day or season fixes the scene"],"crowding":{"count":4,"note":"4 titles in the reference index touch this subject."}}', true);
$est = call("POST", "/estimate", $brief);
echo $est["hold_credits"], " ", $est["model"];
var brief = @"{""task"":""write"",""subject"":""the last shift at a petrol station that is closing down"",""mood"":{""id"":""deadpan"",""label"":""Deadpan"",""overrides"":""chorus-claim"",""overrides_rule"":""the chorus makes a claim, not a summary"",""wants"":""flat declaratives, end-stopped, nothing judged out loud"",""brief"":""its chorus may refuse to claim anything at all; the flatness is the position""},""genre"":{""id"":""country"",""label"":""Country"",""overrides"":""rhyme-serves"",""overrides_rule"":""the rhyme serves the line, never the reverse"",""conventions"":""the title line lands at the end of the chorus; specificity in objects""},""plan"":[{""kind"":""verse"",""label"":""Verse 1""},{""kind"":""chorus"",""label"":""Chorus 1""},{""kind"":""verse"",""label"":""Verse 2""},{""kind"":""chorus"",""label"":""Chorus 2""},{""kind"":""bridge"",""label"":""Bridge""},{""kind"":""chorus"",""label"":""Chorus 3""}],""devices"":[""catalogue: an accumulation of particulars, unranked"",""time-stamp: a specific hour, day or season fixes the scene""],""crowding"":{""count"":4,""note"":""4 titles in the reference index touch this subject.""}}";
Console.WriteLine(await Lyric.Call(HttpMethod.Post, "/estimate", brief));
5. Run it, and poll
POST /run starts a job and returns immediately with a job_id. Poll GET /jobs/{job_id} until status is done or failed. The reply text is at output.output. Always send an Idempotency-Key header derived from the input: a retry on the same key returns the same job instead of billing a second one.
# Start the job. Idempotency-Key makes a retry free.
call POST /run '{"task":"write","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives, end-stopped, nothing judged out loud","brief":"its chorus may refuse to claim anything at all; the flatness is the position"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line, never the reverse","conventions":"the title line lands at the end of the chorus; specificity in objects"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"},{"kind":"verse","label":"Verse 2"},{"kind":"chorus","label":"Chorus 2"},{"kind":"bridge","label":"Bridge"},{"kind":"chorus","label":"Chorus 3"}],"devices":["catalogue: an accumulation of particulars, unranked","time-stamp: a specific hour, day or season fixes the scene"],"crowding":{"count":4,"note":"4 titles in the reference index touch this subject."}}'
# Then poll until status is done or failed:
curl -s "$BASE/jobs/JOB_ID" -H "Authorization: Bearer $TOKEN"
import time
job = call("POST", "/run", brief)
while job["status"] in ("queued", "running"):
time.sleep(1.5)
job = call("GET", "/jobs/" + job["job_id"])
if job["status"] != "done":
raise RuntimeError(job.get("error", "job failed"))
reply = job["output"]["output"]
let job = await call("POST", "/run", brief);
while (job.status === "queued" || job.status === "running") {
await new Promise(r => setTimeout(r, 1500));
job = await call("GET", "/jobs/" + job.job_id);
}
if (job.status !== "done") throw new Error(job.error || "job failed");
const reply = job.output.output;
raw, err := call("POST", "/run", brief)
// poll GET /jobs/{job_id} until status is "done" or "failed";
// the reply text is at .output.output
String job = call("POST", "/run", brief);
// poll GET /jobs/{job_id} until status is done or failed;
// the reply text is at .data.output.output
job = call("POST", "/run", brief)
while %w[queued running].include?(job["status"])
sleep 1.5
job = call("GET", "/jobs/#{job["job_id"]}")
end
raise job["error"].to_s unless job["status"] == "done"
reply = job["output"]["output"]
$job = call("POST", "/run", $brief);
while (in_array($job["status"], ["queued", "running"])) {
sleep(2);
$job = call("GET", "/jobs/" . $job["job_id"]);
}
if ($job["status"] !== "done") { throw new Exception($job["error"] ?? "job failed"); }
$reply = $job["output"]["output"];
var job = await Lyric.Call(HttpMethod.Post, "/run", brief);
// poll GET /jobs/{job_id} until status is done or failed;
// the reply text is at .data.output.output
6. Or stream it
POST /run-stream returns text/event-stream. The wire format is an event: line, one or more data: lines, then a blank line terminating the frame. Event names are job, delta, done, pending and error. A delta payload carries text; done and pending carry the finished job; error carries code, message and job_id. This is taken from the sdk.js in this very bundle and asserted against it by the build, not copied from another app.
# The wire format is: an event line, a data line, then a BLANK line.
curl -N -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: lyrics-generator-<hash-of-input>" \
-d '{"task":"write","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives, end-stopped, nothing judged out loud","brief":"its chorus may refuse to claim anything at all; the flatness is the position"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line, never the reverse","conventions":"the title line lands at the end of the chorus; specificity in objects"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"},{"kind":"verse","label":"Verse 2"},{"kind":"chorus","label":"Chorus 2"},{"kind":"bridge","label":"Bridge"},{"kind":"chorus","label":"Chorus 3"}],"devices":["catalogue: an accumulation of particulars, unranked","time-stamp: a specific hour, day or season fixes the scene"],"crowding":{"count":4,"note":"4 titles in the reference index touch this subject."}}'
# event: job
# data: {"job_id":"job_..."}
#
# event: delta
# data: {"text":"== SONG ==\ntitle: "}
#
# event: done
# data: {"job_id":"job_...","status":"done","output":{"output":"..."},"charged_credits":812}
import urllib.request, json
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps(brief).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Idempotency-Key", "lyrics-generator-" + str(abs(hash(json.dumps(brief)))))
event, chunks = None, []
with urllib.request.urlopen(req) as r:
for raw in r:
line = raw.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
payload = json.loads(line[5:].strip())
if event == "delta":
chunks.append(payload["text"])
elif event in ("done", "pending"):
result = payload
elif event == "error":
raise RuntimeError(payload["code"] + ": " + payload["message"])
elif line == "":
event = None
reply = "".join(chunks)
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": "lyrics-generator-<hash-of-input>"
},
body: JSON.stringify(brief)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", text = "", result = null;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n\n")) >= 0) {
const frame = buf.slice(0, i);
buf = buf.slice(i + 2);
let name = "message", data = "";
frame.split("\n").forEach(l => {
if (l.startsWith("event:")) name = l.slice(6).trim();
else if (l.startsWith("data:")) data += l.slice(5).trim();
});
if (!data) continue;
const payload = JSON.parse(data);
if (name === "delta") text += payload.text || "";
else if (name === "done" || name === "pending") result = payload;
else if (name === "error") throw new Error(payload.code + ": " + payload.message);
}
}
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(briefJSON))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "lyrics-generator-"+hashOf(briefJSON))
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
var event string
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:"):
// event is one of job, delta, done, pending, error
handle(event, strings.TrimSpace(line[5:]))
case line == "":
event = ""
}
}
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Idempotency-Key", "lyrics-generator-<hash-of-input>")
.POST(HttpRequest.BodyPublishers.ofString(brief)).build();
String event = null;
var lines = HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body();
for (String line : (Iterable<String>) lines::iterator) {
if (line.startsWith("event:")) event = line.substring(6).trim();
else if (line.startsWith("data:")) handle(event, line.substring(5).trim());
else if (line.isEmpty()) event = null;
}
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = "lyrics-generator-#{Digest::SHA256.hexdigest(JSON.dump(brief))[0, 16]}"
req.body = JSON.dump(brief)
event = nil
text = +""
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
line = line.chomp
if line.start_with?("event:") then event = line[6..].strip
elsif line.start_with?("data:")
payload = JSON.parse(line[5..].strip)
text << payload["text"].to_s if event == "delta"
raise payload["message"] if event == "error"
elsif line.empty? then event = nil
end
end
end
end
end
$ch = curl_init($BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($brief));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . $TOKEN,
"Content-Type: application/json",
"Idempotency-Key: lyrics-generator-" . substr(sha1(json_encode($brief)), 0, 16),
]);
$event = null;
$text = "";
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) use (&$event, &$text) {
foreach (explode("\n", $chunk) as $line) {
$line = rtrim($line, "\r");
if (str_starts_with($line, "event:")) { $event = trim(substr($line, 6)); }
elseif (str_starts_with($line, "data:")) {
$payload = json_decode(trim(substr($line, 5)), true);
if ($event === "delta") { $text .= $payload["text"] ?? ""; }
} elseif ($line === "") { $event = null; }
}
return strlen($chunk);
});
curl_exec($ch);
curl_close($ch);
var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.Add("Idempotency-Key", "lyrics-generator-<hash-of-input>");
req.Content = new StringContent(brief, Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
string evt = null, line;
var text = new StringBuilder();
while ((line = await reader.ReadLineAsync()) != null) {
if (line.StartsWith("event:")) evt = line.Substring(6).Trim();
else if (line.StartsWith("data:")) Handle(evt, line.Substring(5).Trim());
else if (line.Length == 0) evt = null;
}
7. Revise a song you already have
Same endpoint, task: "revise", plus two fields: prior (the song, section by section) and directive (what to change). Everything else is identical. The revision changes what you asked about and holds the rest.
call POST /run-stream '{"task":"revise","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives","brief":"its chorus may refuse to claim anything"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line","conventions":"the title line lands last"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"}],"devices":["catalogue: an accumulation of particulars, unranked"],"crowding":{"count":4,"note":"lightly worked ground"},"prior":{"title":"The Numbers Come Down","sections":[{"label":"Verse 1","kind":"verse","lines":["<line>","<line>"]}]},"directive":"the bridge is doing the same job as verse 2 - make it turn on the person"}'
again = {
"task": "revise",
"subject": "the last shift at a petrol station that is closing down",
"mood": {
"id": "deadpan",
"label": "Deadpan",
"overrides": "chorus-claim",
"overrides_rule": "the chorus makes a claim, not a summary",
"wants": "flat declaratives",
"brief": "its chorus may refuse to claim anything"
},
"genre": {
"id": "country",
"label": "Country",
"overrides": "rhyme-serves",
"overrides_rule": "the rhyme serves the line",
"conventions": "the title line lands last"
},
"plan": [
{
"kind": "verse",
"label": "Verse 1"
},
{
"kind": "chorus",
"label": "Chorus 1"
}
],
"devices": [
"catalogue: an accumulation of particulars, unranked"
],
"crowding": {
"count": 4,
"note": "lightly worked ground"
},
"prior": {
"title": "The Numbers Come Down",
"sections": [
{
"label": "Verse 1",
"kind": "verse",
"lines": [
"<line>",
"<line>"
]
}
]
},
"directive": "the bridge is doing the same job as verse 2 - make it turn on the person"
}
# same helper, same stream loop - only the task and two extra fields change
job = call("POST", "/run", again)
const again = {"task":"revise","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives","brief":"its chorus may refuse to claim anything"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line","conventions":"the title line lands last"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"}],"devices":["catalogue: an accumulation of particulars, unranked"],"crowding":{"count":4,"note":"lightly worked ground"},"prior":{"title":"The Numbers Come Down","sections":[{"label":"Verse 1","kind":"verse","lines":["<line>","<line>"]}]},"directive":"the bridge is doing the same job as verse 2 - make it turn on the person"};
// same helper, same stream loop - only task, prior and directive change
const job = await call("POST", "/run", again);
var again map[string]any
json.Unmarshal([]byte(`{"task":"revise","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives","brief":"its chorus may refuse to claim anything"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line","conventions":"the title line lands last"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"}],"devices":["catalogue: an accumulation of particulars, unranked"],"crowding":{"count":4,"note":"lightly worked ground"},"prior":{"title":"The Numbers Come Down","sections":[{"label":"Verse 1","kind":"verse","lines":["<line>","<line>"]}]},"directive":"the bridge is doing the same job as verse 2 - make it turn on the person"}`), &again)
raw, err := call("POST", "/run", again)
String again = "{\"task\":\"revise\",\"subject\":\"the last shift at a petrol station that is closing down\",\"mood\":{\"id\":\"deadpan\",\"label\":\"Deadpan\",\"overrides\":\"chorus-claim\",\"overrides_rule\":\"the chorus makes a claim, not a summary\",\"wants\":\"flat declaratives\",\"brief\":\"its chorus may refuse to claim anything\"},\"genre\":{\"id\":\"country\",\"label\":\"Country\",\"overrides\":\"rhyme-serves\",\"overrides_rule\":\"the rhyme serves the line\",\"conventions\":\"the title line lands last\"},\"plan\":[{\"kind\":\"verse\",\"label\":\"Verse 1\"},{\"kind\":\"chorus\",\"label\":\"Chorus 1\"}],\"devices\":[\"catalogue: an accumulation of particulars, unranked\"],\"crowding\":{\"count\":4,\"note\":\"lightly worked ground\"},\"prior\":{\"title\":\"The Numbers Come Down\",\"sections\":[{\"label\":\"Verse 1\",\"kind\":\"verse\",\"lines\":[\"<line>\",\"<line>\"]}]},\"directive\":\"the bridge is doing the same job as verse 2 - make it turn on the person\"}";
System.out.println(call("POST", "/run", again));
again = JSON.parse(%q({"task":"revise","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives","brief":"its chorus may refuse to claim anything"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line","conventions":"the title line lands last"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"}],"devices":["catalogue: an accumulation of particulars, unranked"],"crowding":{"count":4,"note":"lightly worked ground"},"prior":{"title":"The Numbers Come Down","sections":[{"label":"Verse 1","kind":"verse","lines":["<line>","<line>"]}]},"directive":"the bridge is doing the same job as verse 2 - make it turn on the person"}))
job = call("POST", "/run", again)
$again = json_decode('{"task":"revise","subject":"the last shift at a petrol station that is closing down","mood":{"id":"deadpan","label":"Deadpan","overrides":"chorus-claim","overrides_rule":"the chorus makes a claim, not a summary","wants":"flat declaratives","brief":"its chorus may refuse to claim anything"},"genre":{"id":"country","label":"Country","overrides":"rhyme-serves","overrides_rule":"the rhyme serves the line","conventions":"the title line lands last"},"plan":[{"kind":"verse","label":"Verse 1"},{"kind":"chorus","label":"Chorus 1"}],"devices":["catalogue: an accumulation of particulars, unranked"],"crowding":{"count":4,"note":"lightly worked ground"},"prior":{"title":"The Numbers Come Down","sections":[{"label":"Verse 1","kind":"verse","lines":["<line>","<line>"]}]},"directive":"the bridge is doing the same job as verse 2 - make it turn on the person"}', true);
$job = call("POST", "/run", $again);
var again = @"{""task"":""revise"",""subject"":""the last shift at a petrol station that is closing down"",""mood"":{""id"":""deadpan"",""label"":""Deadpan"",""overrides"":""chorus-claim"",""overrides_rule"":""the chorus makes a claim, not a summary"",""wants"":""flat declaratives"",""brief"":""its chorus may refuse to claim anything""},""genre"":{""id"":""country"",""label"":""Country"",""overrides"":""rhyme-serves"",""overrides_rule"":""the rhyme serves the line"",""conventions"":""the title line lands last""},""plan"":[{""kind"":""verse"",""label"":""Verse 1""},{""kind"":""chorus"",""label"":""Chorus 1""}],""devices"":[""catalogue: an accumulation of particulars, unranked""],""crowding"":{""count"":4,""note"":""lightly worked ground""},""prior"":{""title"":""The Numbers Come Down"",""sections"":[{""label"":""Verse 1"",""kind"":""verse"",""lines"":[""<line>"",""<line>""]}]},""directive"":""the bridge is doing the same job as verse 2 - make it turn on the person""}";
Console.WriteLine(await Lyric.Call(HttpMethod.Post, "/run", again));
What the app does that the API does not
The reply is the model's. Everything this app is actually for happens afterwards, in the browser, and is yours to run or skip:
- Every line recounted for syllables by a three-tier counter that reports a range for words that legitimately count two ways, and the count compared against the model's claim.
- The rhyme scheme derived from the actual line endings by an engine with three verdicts - rhymes, does not rhyme, and cannot be verified - rather than two.
- The named hook located character for character in the sections.
- Every field of the reply run against a title index and one-way hook fingerprints, and against detectors for output presenting words as somebody else's.
- The voice measured - words per line, end-stop rate, person, evaluative density - and compared with what the reply said its voice was.