nixamp 0.72.1 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-keys.d.ts +1 -1
- package/dist/ai-keys.js +2 -0
- package/dist/ai-label.d.ts +22 -0
- package/dist/ai-label.js +156 -12
- package/dist/ai-text.d.ts +2 -0
- package/dist/ai-text.js +1 -1
- package/dist/guide-vods.d.ts +2 -0
- package/dist/guide-vods.js +5 -2
- package/dist/library-usage.d.ts +1 -1
- package/dist/library-usage.js +2 -2
- package/package.json +1 -1
- package/src/ai-keys.ts +2 -0
- package/src/ai-label.ts +178 -12
- package/src/ai-text.ts +3 -1
- package/src/guide-vods.ts +5 -2
- package/src/library-usage.ts +2 -2
- package/web/dist/assets/{ar-eyxh1zc7.js → ar-Bsgrtf2h.js} +1 -1
- package/web/dist/assets/{cs-B90TGfUS.js → cs-ZsF7vMVh.js} +1 -1
- package/web/dist/assets/{da-DeALlwS0.js → da-381kzPn7.js} +1 -1
- package/web/dist/assets/{de-Bh0WOnki.js → de-DizoHpaG.js} +1 -1
- package/web/dist/assets/{es-DBpX7st3.js → es-YAdydD7d.js} +1 -1
- package/web/dist/assets/{fi-D6UNXw4X.js → fi-BUHThgUk.js} +1 -1
- package/web/dist/assets/{fr-BMJjqZmD.js → fr-CKa1Nkl9.js} +1 -1
- package/web/dist/assets/{hi-DpKm0Tn3.js → hi-Cbv0ZU2J.js} +1 -1
- package/web/dist/assets/{hu-CrQnpajp.js → hu-25AxyySz.js} +1 -1
- package/web/dist/assets/{id-CsoO6zNU.js → id-rYanwc8o.js} +1 -1
- package/web/dist/assets/{it-CDtrb8CW.js → it-eebB-mQw.js} +1 -1
- package/web/dist/assets/{main-7tQ30Sxs.js → main-CFt8ILSw.js} +2 -2
- package/web/dist/assets/{nl-B1dym6rM.js → nl-D1EYp0XC.js} +1 -1
- package/web/dist/assets/{ru-BWEBAfPH.js → ru-DpxLY1H_.js} +1 -1
- package/web/dist/assets/{sv-Briwmc7a.js → sv--koGVKUA.js} +1 -1
- package/web/dist/assets/{uk-Cr4aHUYR.js → uk-oI3mSYqs.js} +1 -1
- package/web/dist/assets/{vi-D99lOoJC.js → vi-bt6nXnTa.js} +1 -1
- package/web/dist/assets/{zh-Di4VXEll.js → zh-Cs3h-m_j.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +19 -19
package/dist/ai-keys.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const AI_KEY_NAMES: readonly ["ELEVENLABS_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "NIXAMP_OPENAI_URL", "NIXAMP_OPENAI_MODEL", "NIXAMP_MUSIC_MODEL", "NIXAMP_LYRICS_KEY"];
|
|
1
|
+
export declare const AI_KEY_NAMES: readonly ["ELEVENLABS_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "NIXAMP_OPENAI_URL", "NIXAMP_OPENAI_MODEL", "NIXAMP_LABEL_MODEL", "NIXAMP_MUSIC_MODEL", "NIXAMP_LYRICS_KEY"];
|
|
2
2
|
export type AiKeyName = (typeof AI_KEY_NAMES)[number];
|
|
3
3
|
export declare function aiKeysFile(): string;
|
|
4
4
|
/** The key, from the environment or ai.json; "" when neither has it. */
|
package/dist/ai-keys.js
CHANGED
|
@@ -13,6 +13,8 @@ export const AI_KEY_NAMES = [
|
|
|
13
13
|
"ELEVENLABS_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
|
|
14
14
|
// Any OpenAI-compatible endpoint in OpenAI's place (Cloudflare Workers AI, Ollama, ...) and its model.
|
|
15
15
|
"NIXAMP_OPENAI_URL", "NIXAMP_OPENAI_MODEL",
|
|
16
|
+
// The label's songwriter on that endpoint (album plans, lyrics, new artists), e.g. @cf/openai/gpt-oss-120b; NIXAMP_OPENAI_MODEL when unset.
|
|
17
|
+
"NIXAMP_LABEL_MODEL",
|
|
16
18
|
// Which ElevenLabs music model composes: music_v1, music_v2 or music_v2_5 (the default).
|
|
17
19
|
"NIXAMP_MUSIC_MODEL",
|
|
18
20
|
// nichedb.dev's key for publishing our own songs' words (lyrics-publish.ts).
|
package/dist/ai-label.d.ts
CHANGED
|
@@ -128,6 +128,28 @@ export type ArtistPreset = Pick<LabelArtist, "name" | "genre" | "sound" | "voice
|
|
|
128
128
|
* `nixamp station label artist add --preset KEY`, or all of them with `--roster`.
|
|
129
129
|
*/
|
|
130
130
|
export declare const ROSTER: Record<string, ArtistPreset>;
|
|
131
|
+
/**
|
|
132
|
+
* How a genre is written and performed: the album plan, the words and the
|
|
133
|
+
* music model's brief all read it, so a rapper raps and a punk band does not
|
|
134
|
+
* write a ballad. A genre without one gets the general rules only.
|
|
135
|
+
*/
|
|
136
|
+
export interface GenreCraft {
|
|
137
|
+
/** Rapped verses (hooks may be chanted or sung) or sung throughout. */
|
|
138
|
+
delivery: "rapped" | "sung";
|
|
139
|
+
/** How the words are built: form, rhyme, point of view, what makes it the genre. */
|
|
140
|
+
lyrics: string;
|
|
141
|
+
/** What the record sounds like beyond the artist's own sound. */
|
|
142
|
+
production: string;
|
|
143
|
+
/** What the music model is told to leave out. */
|
|
144
|
+
avoid: string;
|
|
145
|
+
}
|
|
146
|
+
/** The genre's craft notes, or none for a genre the label has no notes on. */
|
|
147
|
+
export declare function craftFor(genre: string): GenreCraft | undefined;
|
|
148
|
+
/**
|
|
149
|
+
* The music model's brief for one song: the sound, the song's own style, the
|
|
150
|
+
* voice, how it is delivered and what to leave out.
|
|
151
|
+
*/
|
|
152
|
+
export declare function composePrompt(artist: Pick<LabelArtist, "genre" | "sound" | "voice">, song: Pick<LabelSong, "style">): string;
|
|
131
153
|
export declare const DEFAULTS: Omit<LabelSettings, "pausedReason">;
|
|
132
154
|
/** A name as a folder or file name: what a filesystem or a zip refuses is dropped. */
|
|
133
155
|
export declare function fileName(text: string): string;
|
package/dist/ai-label.js
CHANGED
|
@@ -258,7 +258,147 @@ export const ROSTER = {
|
|
|
258
258
|
bio: "Glitter on the outside, journal entries on the inside.",
|
|
259
259
|
themes: "first nights out, confidence, crushes, friendship, dancing it off",
|
|
260
260
|
},
|
|
261
|
+
"lowrider-gospel": {
|
|
262
|
+
name: "Lowrider Gospel", genre: "Gangsta Rap",
|
|
263
|
+
sound: "West Coast G-funk, whining portamento synth lead, rubbery Moog bassline, talkbox hook, laid-back swung drums with fat claps, sun-bleached Rhodes, 90-96 BPM",
|
|
264
|
+
voice: "unhurried male baritone with a slow West Coast drawl, rapping a hair behind the beat, cool even when the story turns violent",
|
|
265
|
+
bio: "Raps like he is giving you directions to a block you should not visit after dark.",
|
|
266
|
+
themes: "cruising on Sundays, set loyalty, cousins doing time, the cop car in the mirror, money that came too fast, mothers who pray",
|
|
267
|
+
},
|
|
268
|
+
"cutlass-deacon": {
|
|
269
|
+
name: "Cutlass Deacon", genre: "Gangsta Rap",
|
|
270
|
+
sound: "dark Southern street rap, Memphis horror-movie piano loop, cowbell, rattling TR-808 hats in triplets, distorted sub bass, chanted hooks, 68-75 BPM half-time",
|
|
271
|
+
voice: "menacing low male voice, hushed and close to the mic, rapid triplet flow that snaps into chanted call-and-response hooks",
|
|
272
|
+
bio: "Grew up between a church pew and a trap house and still quotes both.",
|
|
273
|
+
themes: "paranoia, debts collected, a city that eats its young, faith and guilt, the price of loyalty, never sleeping",
|
|
274
|
+
},
|
|
275
|
+
"duchess-ransom": {
|
|
276
|
+
name: "Duchess Ransom", genre: "Gangsta Rap",
|
|
277
|
+
sound: "East Coast mafioso rap, cinematic orchestral sample with strings and brass stabs, hard dusty boom-bap drums, crackling vinyl, sparse menacing piano, 86-90 BPM",
|
|
278
|
+
voice: "cold, precise female MC in a low register, dense multisyllabic rhymes delivered conversationally, never raises her voice",
|
|
279
|
+
bio: "Runs her verses like a ledger: every name, every debt, every date.",
|
|
280
|
+
themes: "the come-up as a business, informants, crooked lawyers, fur and funerals, trust as currency, the cost of the crown",
|
|
281
|
+
},
|
|
261
282
|
};
|
|
283
|
+
const RAP_FORM = [
|
|
284
|
+
"Rap structure: [Intro] of 1-2 ad-lib lines, [Verse 1] of 12-16 bars (one bar per line), [Hook] of 4 lines, [Verse 2] of 12-16 bars, [Hook] again, optional [Verse 3] or [Outro].",
|
|
285
|
+
"No pre-chorus, no bridge. Every verse line is a full bar of 9-15 syllables with internal and multisyllabic rhymes (slant rhymes welcome) chained across lines, not just end rhymes on simple words.",
|
|
286
|
+
"Verses tell a story in first person with specific detail: a street, a car model year, a sum of money, a time of night, what someone said. Punchlines and wordplay land at the end of every 4 bars.",
|
|
287
|
+
"The hook is short, chantable and rhythmic, repeated phrases, not a sung pop melody.",
|
|
288
|
+
].join(" ");
|
|
289
|
+
const CRAFT = {
|
|
290
|
+
"gangsta-rap": {
|
|
291
|
+
delivery: "rapped",
|
|
292
|
+
lyrics: `${RAP_FORM} Gangsta rap is street reportage: hard, cinematic first-person crime stories with consequences (arrests, funerals, betrayal, paranoia), bravado earned by detail, slang that fits the region, dark humour. Mild profanity is fine; no slurs, no real names of people, gangs or places' crews. Never preachy, never a happy ending tied in a bow.`,
|
|
293
|
+
production: "Hard-hitting mix, loud punchy drums and heavy bass up front, the rap vocal dry and on top of the beat, ad-libs and hype doubles at line ends.",
|
|
294
|
+
avoid: "singing in the verses, autotune pop melodies, acoustic guitar, EDM drops, uplifting choirs, cheerful major-key pop",
|
|
295
|
+
},
|
|
296
|
+
"hip-hop": {
|
|
297
|
+
delivery: "rapped",
|
|
298
|
+
lyrics: `${RAP_FORM} Hip-hop with real bars: confident, witty, specific, a clear point of view, no motivational-poster lines.`,
|
|
299
|
+
production: "Punchy drums and bass up front, the rap vocal dry and clear on top, ad-libs at line ends.",
|
|
300
|
+
avoid: "singing in the verses, cheesy pop melodies, acoustic strumming, EDM drops",
|
|
301
|
+
},
|
|
302
|
+
punk: {
|
|
303
|
+
delivery: "sung",
|
|
304
|
+
lyrics: "Punk: short, fast, angry and funny. Two-line or four-line verses that spit a specific grievance (a landlord, a boss, a bus that never comes), a shout-along chorus of 2-4 words repeated, no bridge or a 2-line breakdown. Blunt plain words, no poetry-class metaphors.",
|
|
305
|
+
production: "Raw live-room energy, loud distorted guitars, shouted gang vocals on the chorus.",
|
|
306
|
+
avoid: "polished pop vocals, synths, slow ballad sections, autotune",
|
|
307
|
+
},
|
|
308
|
+
blues: {
|
|
309
|
+
delivery: "sung",
|
|
310
|
+
lyrics: "Blues: AAB verses (a line, the same line again with a small twist, then a line that answers it), 3-5 verses, call-and-response with the guitar, earthy concrete detail (rent, rain, the 5:15 train, a woman or man who left), wry humour inside the hurt.",
|
|
311
|
+
production: "Live band in a small room, expressive guitar fills between the vocal lines.",
|
|
312
|
+
avoid: "pop chorus hooks, synths, autotune, polished modern production",
|
|
313
|
+
},
|
|
314
|
+
country: {
|
|
315
|
+
delivery: "sung",
|
|
316
|
+
lyrics: "Country: a story song with a twist in the last chorus, names, places and objects (a pawned guitar, a county road number, a diner waitress), a turn of phrase in the hook that means two things. Plain-spoken, conversational lines.",
|
|
317
|
+
production: "Warm organic band, upfront vocal with natural twang.",
|
|
318
|
+
avoid: "EDM drums, trap hi-hats, autotune, generic stadium pop",
|
|
319
|
+
},
|
|
320
|
+
jazz: {
|
|
321
|
+
delivery: "sung",
|
|
322
|
+
lyrics: "Jazz standard form: AABA, 32 bars, sophisticated adult wit, internal rhymes, a clever title phrase turned three ways, late-night city detail.",
|
|
323
|
+
production: "Acoustic small group, natural room sound, solos between choruses.",
|
|
324
|
+
avoid: "pop drums, synths, autotune, rap",
|
|
325
|
+
},
|
|
326
|
+
metal: {
|
|
327
|
+
delivery: "sung",
|
|
328
|
+
lyrics: "Metal: dark, mythic or apocalyptic imagery made concrete, short punchy lines that fit riffs, a chanted chorus, a breakdown section of 2-4 lines. No self-help language.",
|
|
329
|
+
production: "Crushing tight guitars, powerful drums, huge low end.",
|
|
330
|
+
avoid: "pop vocals, acoustic softness, synth-pop, rap",
|
|
331
|
+
},
|
|
332
|
+
"classic-rock": {
|
|
333
|
+
delivery: "sung",
|
|
334
|
+
lyrics: "Classic rock: swaggering, sensual or road-worn stories, a riff-shaped chorus with a memorable title phrase, characters and places, a little danger.",
|
|
335
|
+
production: "Analog 1970s band sound, warm tube amps, live drums.",
|
|
336
|
+
avoid: "modern pop gloss, trap drums, autotune, synths",
|
|
337
|
+
},
|
|
338
|
+
rock: {
|
|
339
|
+
delivery: "sung",
|
|
340
|
+
lyrics: "Rock: concrete images and a story, a chorus built on a strong title phrase, verses that move the story on.",
|
|
341
|
+
production: "Big live band, guitars and drums up front.",
|
|
342
|
+
avoid: "autotune, trap drums, EDM drops",
|
|
343
|
+
},
|
|
344
|
+
pop: {
|
|
345
|
+
delivery: "sung",
|
|
346
|
+
lyrics: "Pop: a single sharp title idea, conversational verses with specific moments (a text message, a dress, a parking lot at 2 a.m.), a chorus that is instantly singable and repeats the title.",
|
|
347
|
+
production: "Polished modern mix, vocals bright and forward.",
|
|
348
|
+
avoid: "muddy lo-fi, out-of-tune vocals, rap verses",
|
|
349
|
+
},
|
|
350
|
+
rnb: {
|
|
351
|
+
delivery: "sung",
|
|
352
|
+
lyrics: "R&B: intimate, sensual, adult, specific (a hotel key, a voicemail, a late shift), melismatic hook phrases, a pre-chorus that builds tension.",
|
|
353
|
+
production: "Smooth warm low end, lush stacked harmonies, ad-libs.",
|
|
354
|
+
avoid: "rock guitars, EDM drops, stiff robotic vocals",
|
|
355
|
+
},
|
|
356
|
+
synthwave: {
|
|
357
|
+
delivery: "sung",
|
|
358
|
+
lyrics: "Synthwave: cinematic, nocturnal, a movie told in images, short evocative lines.",
|
|
359
|
+
production: "Retro analog synths, gated reverb drums, wide stereo.",
|
|
360
|
+
avoid: "acoustic instruments, trap hi-hats, modern pop vocal effects",
|
|
361
|
+
},
|
|
362
|
+
"yacht-rock": {
|
|
363
|
+
delivery: "sung",
|
|
364
|
+
lyrics: "Yacht rock: smooth, wry adult romance, marinas and late lunches, sophisticated chords mean sophisticated words, a falsetto-ready chorus.",
|
|
365
|
+
production: "Glossy late-1970s West Coast studio sound, Rhodes, clean guitar, tight session drums.",
|
|
366
|
+
avoid: "distortion, rap, EDM, trap drums",
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
/** The genre's craft notes, or none for a genre the label has no notes on. */
|
|
370
|
+
export function craftFor(genre) {
|
|
371
|
+
return CRAFT[genreSlug(genre)];
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* How the label's songwriter is asked: NIXAMP_LABEL_MODEL when set (a reasoning
|
|
375
|
+
* model like gpt-oss-120b writes far better words than the quick default, but
|
|
376
|
+
* thinks first, so it gets room and time).
|
|
377
|
+
*/
|
|
378
|
+
function writer() {
|
|
379
|
+
const model = aiKey("NIXAMP_LABEL_MODEL");
|
|
380
|
+
return { ...(model ? { model } : {}), maxTokens: 9000, timeoutMs: 240_000 };
|
|
381
|
+
}
|
|
382
|
+
/** What every lyric avoids: the stock lines models reach for first. */
|
|
383
|
+
const CLICHES = "neon, fire inside, rise above, shine so bright, heart of gold, chains, echoes, shadows, the night is young, we'll never back down, stand tall, break free, feel alive, against all odds, journey, destiny, soul on fire, light the way, never give up";
|
|
384
|
+
/**
|
|
385
|
+
* The music model's brief for one song: the sound, the song's own style, the
|
|
386
|
+
* voice, how it is delivered and what to leave out.
|
|
387
|
+
*/
|
|
388
|
+
export function composePrompt(artist, song) {
|
|
389
|
+
const craft = craftFor(artist.genre);
|
|
390
|
+
const delivery = craft?.delivery === "rapped"
|
|
391
|
+
? "Lead vocal raps the verses in English: rhythmic, percussive flow locked to the beat, not sung; the hook is chanted or half-sung."
|
|
392
|
+
: "Sung in English.";
|
|
393
|
+
return [
|
|
394
|
+
`${artist.sound}.`,
|
|
395
|
+
`${song.style}.`,
|
|
396
|
+
`Lead vocal: ${artist.voice}.`,
|
|
397
|
+
delivery,
|
|
398
|
+
craft?.production ?? "",
|
|
399
|
+
craft?.avoid ? `Avoid: ${craft.avoid}.` : "",
|
|
400
|
+
].filter(Boolean).join(" ").slice(0, 1500);
|
|
401
|
+
}
|
|
262
402
|
export const DEFAULTS = {
|
|
263
403
|
holder: "Profullstack, Inc.",
|
|
264
404
|
enabled: true,
|
|
@@ -317,12 +457,16 @@ const PLAN_SYSTEM = [
|
|
|
317
457
|
"Plan one album. Answer as JSON: {\"title\": \"1-6 words\", \"concept\": \"what the album is about, at most 300 characters\",",
|
|
318
458
|
"\"songs\": [{\"title\": \"1-6 words\", \"style\": \"the production brief for this song, at most 300 characters: tempo in BPM, mood, key or mode, featured instruments, arrangement\"}]}.",
|
|
319
459
|
"Vary tempo, mood and arrangement across the running order, within the artist's sound; open strong, end memorable.",
|
|
460
|
+
"Titles are specific and surprising (an object, a place, a phrase someone says), never motivational slogans or abstract words like Rise, Empowered, Shine, Journey.",
|
|
461
|
+
"Each song's style names its own story in a few words (who, where, what happens) so no two songs are about the same thing.",
|
|
320
462
|
].join(" ");
|
|
321
463
|
const LYRICS_SYSTEM = [
|
|
322
|
-
"You
|
|
323
|
-
"
|
|
324
|
-
|
|
325
|
-
"
|
|
464
|
+
"You are a professional songwriter writing original English lyrics for an AI performer. Never quote or imitate an existing song and never name a real artist.",
|
|
465
|
+
"Write like a working pro, not a greeting card: concrete nouns, sensory detail, a point of view, a story that moves, lines a real artist would be proud of. Show, never tell how anyone feels.",
|
|
466
|
+
`Never use these stock words or phrases: ${CLICHES}. No rhyming a word with itself, no filler like "oh yeah" to finish a line.`,
|
|
467
|
+
"Put each section tag on its own line, e.g. [Intro], [Verse 1], [Pre-Chorus], [Chorus], [Hook], [Verse 2], [Bridge], [Outro]. Never just one line under a section.",
|
|
468
|
+
"Follow the genre's craft notes when given; without them: two verses of four to eight lines, a pre-chorus of two lines, a four-line chorus sung two or three times, a bridge of two to four lines.",
|
|
469
|
+
"Only the words that are performed, no stage directions or notes. Answer as JSON: {\"lyrics\": \"the lyrics, lines separated by \\n\"}.",
|
|
326
470
|
].join(" ");
|
|
327
471
|
export async function planAlbum(artist, songs, ask = askJson) {
|
|
328
472
|
const before = artist.albums.slice(-4).map((one) => `"${one.title}" (${one.songs.map((song) => song.title).join("; ")})`);
|
|
@@ -336,11 +480,11 @@ export async function planAlbum(artist, songs, ask = askJson) {
|
|
|
336
480
|
artist.bio ? `Who they are: ${artist.bio}` : "",
|
|
337
481
|
artist.themes ? `What they sing about: ${artist.themes}` : "",
|
|
338
482
|
artist.cast ? `The band, as they are (write for these people): ${artist.cast}` : "",
|
|
483
|
+
craftFor(artist.genre) ? `Genre craft: ${craftFor(artist.genre).lyrics}` : "",
|
|
339
484
|
before.length ? `Albums already out (do not reuse their titles or song titles; take the story somewhere new): ${before.join(" | ")}` : "This is the artist's debut album.",
|
|
340
485
|
`The album has exactly ${songs} songs.`,
|
|
341
486
|
].filter(Boolean).join("\n"),
|
|
342
|
-
|
|
343
|
-
timeoutMs: 90_000,
|
|
487
|
+
...writer(),
|
|
344
488
|
});
|
|
345
489
|
const title = clean(answer.value["title"], 80).replace(/"/g, "");
|
|
346
490
|
const concept = clean(answer.value["concept"], 400);
|
|
@@ -365,8 +509,7 @@ export async function inventArtist(genre, taken, ask = askJson) {
|
|
|
365
509
|
const answer = await ask({
|
|
366
510
|
system: ARTIST_SYSTEM,
|
|
367
511
|
prompt: `Genre: ${genre}\nAlready on the label (do not reuse or echo these names or sounds): ${taken.slice(-40).join(", ") || "nobody yet"}`,
|
|
368
|
-
|
|
369
|
-
timeoutMs: 60_000,
|
|
512
|
+
...writer(),
|
|
370
513
|
});
|
|
371
514
|
const name = clean(answer.value["name"], 60).replace(/"/g, "");
|
|
372
515
|
const sound = clean(answer.value["sound"], 400);
|
|
@@ -377,6 +520,7 @@ export async function inventArtist(genre, taken, ask = askJson) {
|
|
|
377
520
|
return { name, genre, sound, voice: clean(answer.value["voice"], 200) || "expressive lead vocal", bio: clean(answer.value["bio"], 300), themes: clean(answer.value["themes"], 200) };
|
|
378
521
|
}
|
|
379
522
|
export async function writeLyrics(artist, album, song, ask = askJson) {
|
|
523
|
+
const craft = craftFor(artist.genre);
|
|
380
524
|
const answer = await ask({
|
|
381
525
|
system: LYRICS_SYSTEM,
|
|
382
526
|
prompt: [
|
|
@@ -385,10 +529,10 @@ export async function writeLyrics(artist, album, song, ask = askJson) {
|
|
|
385
529
|
artist.cast ? `Who sings and plays it (their voice and personality shape the words): ${artist.cast}` : "",
|
|
386
530
|
`Album: "${album.title}"${album.concept ? `: ${album.concept}` : ""}`,
|
|
387
531
|
`Song: "${song.title}": ${song.style}`,
|
|
388
|
-
`
|
|
532
|
+
craft ? `Genre craft (follow it): ${craft.lyrics}` : "",
|
|
533
|
+
`The song lasts about ${song.seconds} seconds: keep the words under ${Math.max(600, Math.round(song.seconds * (craft?.delivery === "rapped" ? 16 : 10)))} characters.`,
|
|
389
534
|
].filter(Boolean).join("\n"),
|
|
390
|
-
|
|
391
|
-
timeoutMs: 90_000,
|
|
535
|
+
...writer(),
|
|
392
536
|
});
|
|
393
537
|
const lyrics = cleanLyrics(String(answer.value["lyrics"] ?? ""));
|
|
394
538
|
if (lyrics.replace(/\[[^\]]*\]/g, "").trim().length < 40)
|
|
@@ -1776,7 +1920,7 @@ export class AiLabel {
|
|
|
1776
1920
|
}
|
|
1777
1921
|
await this.ensureCover(artist, album);
|
|
1778
1922
|
this.log(`composing "${song.title}" (${artist.name}, ${album.title}, ${song.seconds} s, ${composer.name})`);
|
|
1779
|
-
const prompt =
|
|
1923
|
+
const prompt = composePrompt(artist, song);
|
|
1780
1924
|
await composer.compose({ prompt, seconds: song.seconds, vocals: true, out: raw, lyrics: song.lyrics });
|
|
1781
1925
|
const month = this.period();
|
|
1782
1926
|
this.usage[month] = (this.usage[month] ?? 0) + song.seconds;
|
package/dist/ai-text.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface AskOptions {
|
|
|
7
7
|
timeoutMs?: number;
|
|
8
8
|
/** The longest answer, in tokens (default 1024): a song's words need more than a title does. */
|
|
9
9
|
maxTokens?: number;
|
|
10
|
+
/** The OpenAI-compatible model to ask instead of NIXAMP_OPENAI_MODEL (a slower, better writer for song words). */
|
|
11
|
+
model?: string;
|
|
10
12
|
fetcher?: typeof fetch;
|
|
11
13
|
env?: Record<string, string | undefined>;
|
|
12
14
|
}
|
package/dist/ai-text.js
CHANGED
|
@@ -117,7 +117,7 @@ export async function askJson(options) {
|
|
|
117
117
|
// Workers AI's /ai/v1, Ollama's /v1): NIXAMP_OPENAI_URL is that base.
|
|
118
118
|
const baseUrl = (aiKey("NIXAMP_OPENAI_URL", env) || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
119
119
|
const official = new URL(baseUrl).host === "api.openai.com";
|
|
120
|
-
const model = aiKey("NIXAMP_OPENAI_MODEL", env) || "gpt-4.1-mini";
|
|
120
|
+
const model = options.model || aiKey("NIXAMP_OPENAI_MODEL", env) || "gpt-4.1-mini";
|
|
121
121
|
const response = await send(`${baseUrl}/chat/completions`, {
|
|
122
122
|
method: "POST",
|
|
123
123
|
signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
|
package/dist/guide-vods.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface GuideVod {
|
|
|
19
19
|
}
|
|
20
20
|
/** The bases a price may be put on: what the owner may sell, not just share. */
|
|
21
21
|
export declare const SELLABLE_BASES: readonly RoomBasis[];
|
|
22
|
+
/** What a new video of the owner's costs to download unless it says otherwise (Anthony: "$1 per video"). */
|
|
23
|
+
export declare const VOD_DEFAULT_PRICE_CENTS = 100;
|
|
22
24
|
/** $100: a price above that is a typo. */
|
|
23
25
|
export declare const VOD_PRICE_MAX_CENTS = 10000;
|
|
24
26
|
/** Files one member may have on the guide at once, and files on it in all. */
|
package/dist/guide-vods.js
CHANGED
|
@@ -24,6 +24,8 @@ import { randomBytes } from "node:crypto";
|
|
|
24
24
|
import { ROOM_BASES } from "./guide-rooms.js";
|
|
25
25
|
/** The bases a price may be put on: what the owner may sell, not just share. */
|
|
26
26
|
export const SELLABLE_BASES = ["owned", "public-domain"];
|
|
27
|
+
/** What a new video of the owner's costs to download unless it says otherwise (Anthony: "$1 per video"). */
|
|
28
|
+
export const VOD_DEFAULT_PRICE_CENTS = 100;
|
|
27
29
|
/** $100: a price above that is a typo. */
|
|
28
30
|
export const VOD_PRICE_MAX_CENTS = 10_000;
|
|
29
31
|
/** Files one member may have on the guide at once, and files on it in all. */
|
|
@@ -273,8 +275,9 @@ export async function handleVods(request, response, path, ctx) {
|
|
|
273
275
|
const title = typeof body?.title === "string" ? body.title.trim().slice(0, 160) : "";
|
|
274
276
|
const note = typeof body?.note === "string" ? body.note.trim().slice(0, 300) : "";
|
|
275
277
|
const owner = taken?.by ?? by;
|
|
276
|
-
// A price, in dollars: only the owner, only on its own, only what it may sell. Left out, the old one stays
|
|
277
|
-
|
|
278
|
+
// A price, in dollars: only the owner, only on its own, only what it may sell. Left out, the old one stays,
|
|
279
|
+
// and a new one of the owner's that it may sell costs the default.
|
|
280
|
+
let priceCents = taken ? (taken.priceCents ?? 0) : owner === "" && ctx.canControl && SELLABLE_BASES.includes(basis) ? VOD_DEFAULT_PRICE_CENTS : 0;
|
|
278
281
|
if (body?.price !== undefined && body.price !== null) {
|
|
279
282
|
const cents = Math.round(Number(body.price) * 100);
|
|
280
283
|
if (!ctx.canControl || owner !== "") {
|
package/dist/library-usage.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** `nixamp library` subcommands and usage, apart so help does not load the client. */
|
|
2
2
|
export declare const LIBRARY_COMMANDS: Set<string>;
|
|
3
|
-
export declare const LIBRARY_USAGE = "nixamp library \u2014 what this server has, indexed\n\n nixamp library PATH the folder the daemon serves (set once)\n nixamp library status items by state, sources, last scans\n nixamp library sources\n nixamp library add PATH index another folder (inside your home, not hidden)\n nixamp library scan [SOURCE] [--force] [--wait]\n nixamp library search WORDS [--artist A] [--album A] [--state failed] [--limit 20]\n nixamp library show ITEM\n nixamp library fix ITEM --title T [--artist A] [--album A] [--genre G] [--track N] [--year N]\n nixamp library remove SOURCE forget a folder; its files are not touched\n nixamp library playlists\n nixamp library playlist new NAME [ITEM...]\n nixamp library playlist show|delete PLAYLIST\n nixamp library playlist add PLAYLIST ITEM...\n nixamp library publish ITEM... | FOLDER | WORDS --basis B [--title T] [--note N] [--dry-run]\n on demand on live.streaming: a link that plays each from the start,\n from any of your servers (needs nixamp login); --here for this server's own guide\n (B: owned, permission, licensed, creative-commons, public-domain)\n --price USD:
|
|
3
|
+
export declare const LIBRARY_USAGE = "nixamp library \u2014 what this server has, indexed\n\n nixamp library PATH the folder the daemon serves (set once)\n nixamp library status items by state, sources, last scans\n nixamp library sources\n nixamp library add PATH index another folder (inside your home, not hidden)\n nixamp library scan [SOURCE] [--force] [--wait]\n nixamp library search WORDS [--artist A] [--album A] [--state failed] [--limit 20]\n nixamp library show ITEM\n nixamp library fix ITEM --title T [--artist A] [--album A] [--genre G] [--track N] [--year N]\n nixamp library remove SOURCE forget a folder; its files are not touched\n nixamp library playlists\n nixamp library playlist new NAME [ITEM...]\n nixamp library playlist show|delete PLAYLIST\n nixamp library playlist add PLAYLIST ITEM...\n nixamp library publish ITEM... | FOLDER | WORDS --basis B [--title T] [--note N] [--dry-run]\n on demand on live.streaming: a link that plays each from the start,\n from any of your servers (needs nixamp login); --here for this server's own guide\n (B: owned, permission, licensed, creative-commons, public-domain)\n --price USD: its download price on nixamp.com (guide owner, owned or\n public-domain only; new ones default to $1, --price 0 for free)\n nixamp library vods what is published on demand, with each link\n nixamp library unpublish VOD take one off\n\n --json machine-readable output\n --url, --key another server, as with `nixamp admin`";
|
package/dist/library-usage.js
CHANGED
|
@@ -19,8 +19,8 @@ export const LIBRARY_USAGE = `nixamp library — what this server has, indexed
|
|
|
19
19
|
on demand on live.streaming: a link that plays each from the start,
|
|
20
20
|
from any of your servers (needs nixamp login); --here for this server's own guide
|
|
21
21
|
(B: owned, permission, licensed, creative-commons, public-domain)
|
|
22
|
-
--price USD:
|
|
23
|
-
public-domain only;
|
|
22
|
+
--price USD: its download price on nixamp.com (guide owner, owned or
|
|
23
|
+
public-domain only; new ones default to $1, --price 0 for free)
|
|
24
24
|
nixamp library vods what is published on demand, with each link
|
|
25
25
|
nixamp library unpublish VOD take one off
|
|
26
26
|
|
package/package.json
CHANGED
package/src/ai-keys.ts
CHANGED
|
@@ -14,6 +14,8 @@ export const AI_KEY_NAMES = [
|
|
|
14
14
|
"ELEVENLABS_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY",
|
|
15
15
|
// Any OpenAI-compatible endpoint in OpenAI's place (Cloudflare Workers AI, Ollama, ...) and its model.
|
|
16
16
|
"NIXAMP_OPENAI_URL", "NIXAMP_OPENAI_MODEL",
|
|
17
|
+
// The label's songwriter on that endpoint (album plans, lyrics, new artists), e.g. @cf/openai/gpt-oss-120b; NIXAMP_OPENAI_MODEL when unset.
|
|
18
|
+
"NIXAMP_LABEL_MODEL",
|
|
17
19
|
// Which ElevenLabs music model composes: music_v1, music_v2 or music_v2_5 (the default).
|
|
18
20
|
"NIXAMP_MUSIC_MODEL",
|
|
19
21
|
// nichedb.dev's key for publishing our own songs' words (lyrics-publish.ts).
|
package/src/ai-label.ts
CHANGED
|
@@ -384,8 +384,170 @@ export const ROSTER: Record<string, ArtistPreset> = {
|
|
|
384
384
|
bio: "Glitter on the outside, journal entries on the inside.",
|
|
385
385
|
themes: "first nights out, confidence, crushes, friendship, dancing it off",
|
|
386
386
|
},
|
|
387
|
+
"lowrider-gospel": {
|
|
388
|
+
name: "Lowrider Gospel", genre: "Gangsta Rap",
|
|
389
|
+
sound: "West Coast G-funk, whining portamento synth lead, rubbery Moog bassline, talkbox hook, laid-back swung drums with fat claps, sun-bleached Rhodes, 90-96 BPM",
|
|
390
|
+
voice: "unhurried male baritone with a slow West Coast drawl, rapping a hair behind the beat, cool even when the story turns violent",
|
|
391
|
+
bio: "Raps like he is giving you directions to a block you should not visit after dark.",
|
|
392
|
+
themes: "cruising on Sundays, set loyalty, cousins doing time, the cop car in the mirror, money that came too fast, mothers who pray",
|
|
393
|
+
},
|
|
394
|
+
"cutlass-deacon": {
|
|
395
|
+
name: "Cutlass Deacon", genre: "Gangsta Rap",
|
|
396
|
+
sound: "dark Southern street rap, Memphis horror-movie piano loop, cowbell, rattling TR-808 hats in triplets, distorted sub bass, chanted hooks, 68-75 BPM half-time",
|
|
397
|
+
voice: "menacing low male voice, hushed and close to the mic, rapid triplet flow that snaps into chanted call-and-response hooks",
|
|
398
|
+
bio: "Grew up between a church pew and a trap house and still quotes both.",
|
|
399
|
+
themes: "paranoia, debts collected, a city that eats its young, faith and guilt, the price of loyalty, never sleeping",
|
|
400
|
+
},
|
|
401
|
+
"duchess-ransom": {
|
|
402
|
+
name: "Duchess Ransom", genre: "Gangsta Rap",
|
|
403
|
+
sound: "East Coast mafioso rap, cinematic orchestral sample with strings and brass stabs, hard dusty boom-bap drums, crackling vinyl, sparse menacing piano, 86-90 BPM",
|
|
404
|
+
voice: "cold, precise female MC in a low register, dense multisyllabic rhymes delivered conversationally, never raises her voice",
|
|
405
|
+
bio: "Runs her verses like a ledger: every name, every debt, every date.",
|
|
406
|
+
themes: "the come-up as a business, informants, crooked lawyers, fur and funerals, trust as currency, the cost of the crown",
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* How a genre is written and performed: the album plan, the words and the
|
|
412
|
+
* music model's brief all read it, so a rapper raps and a punk band does not
|
|
413
|
+
* write a ballad. A genre without one gets the general rules only.
|
|
414
|
+
*/
|
|
415
|
+
export interface GenreCraft {
|
|
416
|
+
/** Rapped verses (hooks may be chanted or sung) or sung throughout. */
|
|
417
|
+
delivery: "rapped" | "sung";
|
|
418
|
+
/** How the words are built: form, rhyme, point of view, what makes it the genre. */
|
|
419
|
+
lyrics: string;
|
|
420
|
+
/** What the record sounds like beyond the artist's own sound. */
|
|
421
|
+
production: string;
|
|
422
|
+
/** What the music model is told to leave out. */
|
|
423
|
+
avoid: string;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const RAP_FORM = [
|
|
427
|
+
"Rap structure: [Intro] of 1-2 ad-lib lines, [Verse 1] of 12-16 bars (one bar per line), [Hook] of 4 lines, [Verse 2] of 12-16 bars, [Hook] again, optional [Verse 3] or [Outro].",
|
|
428
|
+
"No pre-chorus, no bridge. Every verse line is a full bar of 9-15 syllables with internal and multisyllabic rhymes (slant rhymes welcome) chained across lines, not just end rhymes on simple words.",
|
|
429
|
+
"Verses tell a story in first person with specific detail: a street, a car model year, a sum of money, a time of night, what someone said. Punchlines and wordplay land at the end of every 4 bars.",
|
|
430
|
+
"The hook is short, chantable and rhythmic, repeated phrases, not a sung pop melody.",
|
|
431
|
+
].join(" ");
|
|
432
|
+
|
|
433
|
+
const CRAFT: Record<string, GenreCraft> = {
|
|
434
|
+
"gangsta-rap": {
|
|
435
|
+
delivery: "rapped",
|
|
436
|
+
lyrics: `${RAP_FORM} Gangsta rap is street reportage: hard, cinematic first-person crime stories with consequences (arrests, funerals, betrayal, paranoia), bravado earned by detail, slang that fits the region, dark humour. Mild profanity is fine; no slurs, no real names of people, gangs or places' crews. Never preachy, never a happy ending tied in a bow.`,
|
|
437
|
+
production: "Hard-hitting mix, loud punchy drums and heavy bass up front, the rap vocal dry and on top of the beat, ad-libs and hype doubles at line ends.",
|
|
438
|
+
avoid: "singing in the verses, autotune pop melodies, acoustic guitar, EDM drops, uplifting choirs, cheerful major-key pop",
|
|
439
|
+
},
|
|
440
|
+
"hip-hop": {
|
|
441
|
+
delivery: "rapped",
|
|
442
|
+
lyrics: `${RAP_FORM} Hip-hop with real bars: confident, witty, specific, a clear point of view, no motivational-poster lines.`,
|
|
443
|
+
production: "Punchy drums and bass up front, the rap vocal dry and clear on top, ad-libs at line ends.",
|
|
444
|
+
avoid: "singing in the verses, cheesy pop melodies, acoustic strumming, EDM drops",
|
|
445
|
+
},
|
|
446
|
+
punk: {
|
|
447
|
+
delivery: "sung",
|
|
448
|
+
lyrics: "Punk: short, fast, angry and funny. Two-line or four-line verses that spit a specific grievance (a landlord, a boss, a bus that never comes), a shout-along chorus of 2-4 words repeated, no bridge or a 2-line breakdown. Blunt plain words, no poetry-class metaphors.",
|
|
449
|
+
production: "Raw live-room energy, loud distorted guitars, shouted gang vocals on the chorus.",
|
|
450
|
+
avoid: "polished pop vocals, synths, slow ballad sections, autotune",
|
|
451
|
+
},
|
|
452
|
+
blues: {
|
|
453
|
+
delivery: "sung",
|
|
454
|
+
lyrics: "Blues: AAB verses (a line, the same line again with a small twist, then a line that answers it), 3-5 verses, call-and-response with the guitar, earthy concrete detail (rent, rain, the 5:15 train, a woman or man who left), wry humour inside the hurt.",
|
|
455
|
+
production: "Live band in a small room, expressive guitar fills between the vocal lines.",
|
|
456
|
+
avoid: "pop chorus hooks, synths, autotune, polished modern production",
|
|
457
|
+
},
|
|
458
|
+
country: {
|
|
459
|
+
delivery: "sung",
|
|
460
|
+
lyrics: "Country: a story song with a twist in the last chorus, names, places and objects (a pawned guitar, a county road number, a diner waitress), a turn of phrase in the hook that means two things. Plain-spoken, conversational lines.",
|
|
461
|
+
production: "Warm organic band, upfront vocal with natural twang.",
|
|
462
|
+
avoid: "EDM drums, trap hi-hats, autotune, generic stadium pop",
|
|
463
|
+
},
|
|
464
|
+
jazz: {
|
|
465
|
+
delivery: "sung",
|
|
466
|
+
lyrics: "Jazz standard form: AABA, 32 bars, sophisticated adult wit, internal rhymes, a clever title phrase turned three ways, late-night city detail.",
|
|
467
|
+
production: "Acoustic small group, natural room sound, solos between choruses.",
|
|
468
|
+
avoid: "pop drums, synths, autotune, rap",
|
|
469
|
+
},
|
|
470
|
+
metal: {
|
|
471
|
+
delivery: "sung",
|
|
472
|
+
lyrics: "Metal: dark, mythic or apocalyptic imagery made concrete, short punchy lines that fit riffs, a chanted chorus, a breakdown section of 2-4 lines. No self-help language.",
|
|
473
|
+
production: "Crushing tight guitars, powerful drums, huge low end.",
|
|
474
|
+
avoid: "pop vocals, acoustic softness, synth-pop, rap",
|
|
475
|
+
},
|
|
476
|
+
"classic-rock": {
|
|
477
|
+
delivery: "sung",
|
|
478
|
+
lyrics: "Classic rock: swaggering, sensual or road-worn stories, a riff-shaped chorus with a memorable title phrase, characters and places, a little danger.",
|
|
479
|
+
production: "Analog 1970s band sound, warm tube amps, live drums.",
|
|
480
|
+
avoid: "modern pop gloss, trap drums, autotune, synths",
|
|
481
|
+
},
|
|
482
|
+
rock: {
|
|
483
|
+
delivery: "sung",
|
|
484
|
+
lyrics: "Rock: concrete images and a story, a chorus built on a strong title phrase, verses that move the story on.",
|
|
485
|
+
production: "Big live band, guitars and drums up front.",
|
|
486
|
+
avoid: "autotune, trap drums, EDM drops",
|
|
487
|
+
},
|
|
488
|
+
pop: {
|
|
489
|
+
delivery: "sung",
|
|
490
|
+
lyrics: "Pop: a single sharp title idea, conversational verses with specific moments (a text message, a dress, a parking lot at 2 a.m.), a chorus that is instantly singable and repeats the title.",
|
|
491
|
+
production: "Polished modern mix, vocals bright and forward.",
|
|
492
|
+
avoid: "muddy lo-fi, out-of-tune vocals, rap verses",
|
|
493
|
+
},
|
|
494
|
+
rnb: {
|
|
495
|
+
delivery: "sung",
|
|
496
|
+
lyrics: "R&B: intimate, sensual, adult, specific (a hotel key, a voicemail, a late shift), melismatic hook phrases, a pre-chorus that builds tension.",
|
|
497
|
+
production: "Smooth warm low end, lush stacked harmonies, ad-libs.",
|
|
498
|
+
avoid: "rock guitars, EDM drops, stiff robotic vocals",
|
|
499
|
+
},
|
|
500
|
+
synthwave: {
|
|
501
|
+
delivery: "sung",
|
|
502
|
+
lyrics: "Synthwave: cinematic, nocturnal, a movie told in images, short evocative lines.",
|
|
503
|
+
production: "Retro analog synths, gated reverb drums, wide stereo.",
|
|
504
|
+
avoid: "acoustic instruments, trap hi-hats, modern pop vocal effects",
|
|
505
|
+
},
|
|
506
|
+
"yacht-rock": {
|
|
507
|
+
delivery: "sung",
|
|
508
|
+
lyrics: "Yacht rock: smooth, wry adult romance, marinas and late lunches, sophisticated chords mean sophisticated words, a falsetto-ready chorus.",
|
|
509
|
+
production: "Glossy late-1970s West Coast studio sound, Rhodes, clean guitar, tight session drums.",
|
|
510
|
+
avoid: "distortion, rap, EDM, trap drums",
|
|
511
|
+
},
|
|
387
512
|
};
|
|
388
513
|
|
|
514
|
+
/** The genre's craft notes, or none for a genre the label has no notes on. */
|
|
515
|
+
export function craftFor(genre: string): GenreCraft | undefined {
|
|
516
|
+
return CRAFT[genreSlug(genre)];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* How the label's songwriter is asked: NIXAMP_LABEL_MODEL when set (a reasoning
|
|
521
|
+
* model like gpt-oss-120b writes far better words than the quick default, but
|
|
522
|
+
* thinks first, so it gets room and time).
|
|
523
|
+
*/
|
|
524
|
+
function writer(): { model?: string; maxTokens: number; timeoutMs: number } {
|
|
525
|
+
const model = aiKey("NIXAMP_LABEL_MODEL");
|
|
526
|
+
return { ...(model ? { model } : {}), maxTokens: 9000, timeoutMs: 240_000 };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** What every lyric avoids: the stock lines models reach for first. */
|
|
530
|
+
const CLICHES = "neon, fire inside, rise above, shine so bright, heart of gold, chains, echoes, shadows, the night is young, we'll never back down, stand tall, break free, feel alive, against all odds, journey, destiny, soul on fire, light the way, never give up";
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* The music model's brief for one song: the sound, the song's own style, the
|
|
534
|
+
* voice, how it is delivered and what to leave out.
|
|
535
|
+
*/
|
|
536
|
+
export function composePrompt(artist: Pick<LabelArtist, "genre" | "sound" | "voice">, song: Pick<LabelSong, "style">): string {
|
|
537
|
+
const craft = craftFor(artist.genre);
|
|
538
|
+
const delivery = craft?.delivery === "rapped"
|
|
539
|
+
? "Lead vocal raps the verses in English: rhythmic, percussive flow locked to the beat, not sung; the hook is chanted or half-sung."
|
|
540
|
+
: "Sung in English.";
|
|
541
|
+
return [
|
|
542
|
+
`${artist.sound}.`,
|
|
543
|
+
`${song.style}.`,
|
|
544
|
+
`Lead vocal: ${artist.voice}.`,
|
|
545
|
+
delivery,
|
|
546
|
+
craft?.production ?? "",
|
|
547
|
+
craft?.avoid ? `Avoid: ${craft.avoid}.` : "",
|
|
548
|
+
].filter(Boolean).join(" ").slice(0, 1500);
|
|
549
|
+
}
|
|
550
|
+
|
|
389
551
|
export const DEFAULTS: Omit<LabelSettings, "pausedReason"> = {
|
|
390
552
|
holder: "Profullstack, Inc.",
|
|
391
553
|
enabled: true,
|
|
@@ -451,13 +613,17 @@ const PLAN_SYSTEM = [
|
|
|
451
613
|
"Plan one album. Answer as JSON: {\"title\": \"1-6 words\", \"concept\": \"what the album is about, at most 300 characters\",",
|
|
452
614
|
"\"songs\": [{\"title\": \"1-6 words\", \"style\": \"the production brief for this song, at most 300 characters: tempo in BPM, mood, key or mode, featured instruments, arrangement\"}]}.",
|
|
453
615
|
"Vary tempo, mood and arrangement across the running order, within the artist's sound; open strong, end memorable.",
|
|
616
|
+
"Titles are specific and surprising (an object, a place, a phrase someone says), never motivational slogans or abstract words like Rise, Empowered, Shine, Journey.",
|
|
617
|
+
"Each song's style names its own story in a few words (who, where, what happens) so no two songs are about the same thing.",
|
|
454
618
|
].join(" ");
|
|
455
619
|
|
|
456
620
|
const LYRICS_SYSTEM = [
|
|
457
|
-
"You
|
|
458
|
-
"
|
|
459
|
-
|
|
460
|
-
"
|
|
621
|
+
"You are a professional songwriter writing original English lyrics for an AI performer. Never quote or imitate an existing song and never name a real artist.",
|
|
622
|
+
"Write like a working pro, not a greeting card: concrete nouns, sensory detail, a point of view, a story that moves, lines a real artist would be proud of. Show, never tell how anyone feels.",
|
|
623
|
+
`Never use these stock words or phrases: ${CLICHES}. No rhyming a word with itself, no filler like "oh yeah" to finish a line.`,
|
|
624
|
+
"Put each section tag on its own line, e.g. [Intro], [Verse 1], [Pre-Chorus], [Chorus], [Hook], [Verse 2], [Bridge], [Outro]. Never just one line under a section.",
|
|
625
|
+
"Follow the genre's craft notes when given; without them: two verses of four to eight lines, a pre-chorus of two lines, a four-line chorus sung two or three times, a bridge of two to four lines.",
|
|
626
|
+
"Only the words that are performed, no stage directions or notes. Answer as JSON: {\"lyrics\": \"the lyrics, lines separated by \\n\"}.",
|
|
461
627
|
].join(" ");
|
|
462
628
|
|
|
463
629
|
export async function planAlbum(
|
|
@@ -476,11 +642,11 @@ export async function planAlbum(
|
|
|
476
642
|
artist.bio ? `Who they are: ${artist.bio}` : "",
|
|
477
643
|
artist.themes ? `What they sing about: ${artist.themes}` : "",
|
|
478
644
|
artist.cast ? `The band, as they are (write for these people): ${artist.cast}` : "",
|
|
645
|
+
craftFor(artist.genre) ? `Genre craft: ${craftFor(artist.genre)!.lyrics}` : "",
|
|
479
646
|
before.length ? `Albums already out (do not reuse their titles or song titles; take the story somewhere new): ${before.join(" | ")}` : "This is the artist's debut album.",
|
|
480
647
|
`The album has exactly ${songs} songs.`,
|
|
481
648
|
].filter(Boolean).join("\n"),
|
|
482
|
-
|
|
483
|
-
timeoutMs: 90_000,
|
|
649
|
+
...writer(),
|
|
484
650
|
});
|
|
485
651
|
const title = clean(answer.value["title"], 80).replace(/"/g, "");
|
|
486
652
|
const concept = clean(answer.value["concept"], 400);
|
|
@@ -506,8 +672,7 @@ export async function inventArtist(genre: string, taken: string[], ask: typeof a
|
|
|
506
672
|
const answer = await ask({
|
|
507
673
|
system: ARTIST_SYSTEM,
|
|
508
674
|
prompt: `Genre: ${genre}\nAlready on the label (do not reuse or echo these names or sounds): ${taken.slice(-40).join(", ") || "nobody yet"}`,
|
|
509
|
-
|
|
510
|
-
timeoutMs: 60_000,
|
|
675
|
+
...writer(),
|
|
511
676
|
});
|
|
512
677
|
const name = clean(answer.value["name"], 60).replace(/"/g, "");
|
|
513
678
|
const sound = clean(answer.value["sound"], 400);
|
|
@@ -522,6 +687,7 @@ export async function writeLyrics(
|
|
|
522
687
|
song: Pick<LabelSong, "title" | "style" | "seconds">,
|
|
523
688
|
ask: typeof askJson = askJson,
|
|
524
689
|
): Promise<{ lyrics: string; model: string }> {
|
|
690
|
+
const craft = craftFor(artist.genre);
|
|
525
691
|
const answer = await ask({
|
|
526
692
|
system: LYRICS_SYSTEM,
|
|
527
693
|
prompt: [
|
|
@@ -530,10 +696,10 @@ export async function writeLyrics(
|
|
|
530
696
|
artist.cast ? `Who sings and plays it (their voice and personality shape the words): ${artist.cast}` : "",
|
|
531
697
|
`Album: "${album.title}"${album.concept ? `: ${album.concept}` : ""}`,
|
|
532
698
|
`Song: "${song.title}": ${song.style}`,
|
|
533
|
-
`
|
|
699
|
+
craft ? `Genre craft (follow it): ${craft.lyrics}` : "",
|
|
700
|
+
`The song lasts about ${song.seconds} seconds: keep the words under ${Math.max(600, Math.round(song.seconds * (craft?.delivery === "rapped" ? 16 : 10)))} characters.`,
|
|
534
701
|
].filter(Boolean).join("\n"),
|
|
535
|
-
|
|
536
|
-
timeoutMs: 90_000,
|
|
702
|
+
...writer(),
|
|
537
703
|
});
|
|
538
704
|
const lyrics = cleanLyrics(String(answer.value["lyrics"] ?? ""));
|
|
539
705
|
if (lyrics.replace(/\[[^\]]*\]/g, "").trim().length < 40) throw new Error("the model wrote no lyrics");
|
|
@@ -1889,7 +2055,7 @@ export class AiLabel {
|
|
|
1889
2055
|
}
|
|
1890
2056
|
await this.ensureCover(artist, album);
|
|
1891
2057
|
this.log(`composing "${song.title}" (${artist.name}, ${album.title}, ${song.seconds} s, ${composer.name})`);
|
|
1892
|
-
const prompt =
|
|
2058
|
+
const prompt = composePrompt(artist, song);
|
|
1893
2059
|
await composer.compose({ prompt, seconds: song.seconds, vocals: true, out: raw, lyrics: song.lyrics });
|
|
1894
2060
|
const month = this.period();
|
|
1895
2061
|
this.usage[month] = (this.usage[month] ?? 0) + song.seconds;
|