nixamp 0.73.0 → 0.75.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 +197 -20
- package/dist/ai-text.d.ts +2 -0
- package/dist/ai-text.js +1 -1
- package/package.json +1 -1
- package/src/ai-keys.ts +2 -0
- package/src/ai-label.ts +217 -20
- package/src/ai-text.ts +3 -1
- package/web/dist/assets/{ar-0IqzB31c.js → ar-DWO4AD-h.js} +1 -1
- package/web/dist/assets/{cs-DCfCI0eZ.js → cs-BYkdeLUB.js} +1 -1
- package/web/dist/assets/{da-Bss0fAE3.js → da-D1-4-VhC.js} +1 -1
- package/web/dist/assets/{de-rv67Ab7H.js → de-CaklUWbI.js} +1 -1
- package/web/dist/assets/{es-DV-6B3BH.js → es-CG6nwbN9.js} +1 -1
- package/web/dist/assets/{fi-DQS8All1.js → fi-DWfrod_V.js} +1 -1
- package/web/dist/assets/{fr-CCl8kuVS.js → fr-CpWd4lRG.js} +1 -1
- package/web/dist/assets/{hi-DjRXm_AW.js → hi-BIwcJMqA.js} +1 -1
- package/web/dist/assets/{hu-BnY80Y25.js → hu-yaKuIx_R.js} +1 -1
- package/web/dist/assets/{id-a14Arova.js → id-B20X5Jpx.js} +1 -1
- package/web/dist/assets/{it-Clku_76_.js → it-C_0lQi83.js} +1 -1
- package/web/dist/assets/{main-39LJVPMZ.js → main-reQmYdJ0.js} +2 -2
- package/web/dist/assets/{nl-xnrEs6a0.js → nl-vH0eIRvu.js} +1 -1
- package/web/dist/assets/{ru-BiwJsMjn.js → ru-CyWPMStM.js} +1 -1
- package/web/dist/assets/{sv-CflBqeQ8.js → sv-pXXM1l8a.js} +1 -1
- package/web/dist/assets/{uk-9vC_u0Y6.js → uk-DcNuaLei.js} +1 -1
- package/web/dist/assets/{vi-CulXW-FL.js → vi-QYkJg9a4.js} +1 -1
- package/web/dist/assets/{zh-CwaS2ltZ.js → zh-9sYPz_Vm.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), sung throughout, or no voice at all (no words are written). */
|
|
138
|
+
delivery: "rapped" | "sung" | "instrumental";
|
|
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,174 @@ 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
|
+
},
|
|
282
|
+
"ondine-halloway": {
|
|
283
|
+
name: "Ondine Halloway", genre: "Classical",
|
|
284
|
+
sound: "solo concert grand piano and piano concertos with full orchestra, Romantic and Impressionist, singing melodies over rippling arpeggios, virtuosic cadenzas, rubato, 50-140 BPM",
|
|
285
|
+
voice: "instrumental: a Steinway concert grand in a warm hall",
|
|
286
|
+
bio: "A pianist who plays like the room is empty and the night is long.",
|
|
287
|
+
themes: "nocturnes, études, ballades, preludes and piano concertos; rain on windows, insomnia, letters never sent, the sea",
|
|
288
|
+
},
|
|
289
|
+
"aldermoor-symphony": {
|
|
290
|
+
name: "Aldermoor Symphony Orchestra", genre: "Classical",
|
|
291
|
+
sound: "full late-Romantic symphony orchestra, sweeping strings, French horns, brass chorales, woodwind solos, harp, timpani rolls, symphonies and tone poems, 40-160 BPM",
|
|
292
|
+
voice: "instrumental: eighty players in a large concert hall",
|
|
293
|
+
bio: "An orchestra from a northern town that does not exist, playing symphonies nobody wrote until now.",
|
|
294
|
+
themes: "symphonies in four movements, tone poems and overtures; storms, mountains, harvests, wars remembered, homecomings",
|
|
295
|
+
},
|
|
296
|
+
"ashgrove-ensemble": {
|
|
297
|
+
name: "The Ashgrove Ensemble", genre: "Classical",
|
|
298
|
+
sound: "chamber music for strings and woodwinds: string quartet, oboe, clarinet, flute and bassoon in quartets, quintets and serenades, Classical-era clarity to modern lyricism, 60-150 BPM",
|
|
299
|
+
voice: "instrumental: strings and woodwinds in an intimate wooden hall",
|
|
300
|
+
bio: "Seven players who trade melodies like old friends finishing each other's sentences.",
|
|
301
|
+
themes: "string quartets, clarinet quintets, wind serenades, divertimenti; gardens, conversations, country walks, dances",
|
|
302
|
+
},
|
|
261
303
|
};
|
|
304
|
+
const RAP_FORM = [
|
|
305
|
+
"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].",
|
|
306
|
+
"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.",
|
|
307
|
+
"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.",
|
|
308
|
+
"The hook is short, chantable and rhythmic, repeated phrases, not a sung pop melody.",
|
|
309
|
+
].join(" ");
|
|
310
|
+
const CRAFT = {
|
|
311
|
+
"gangsta-rap": {
|
|
312
|
+
delivery: "rapped",
|
|
313
|
+
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.`,
|
|
314
|
+
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.",
|
|
315
|
+
avoid: "singing in the verses, autotune pop melodies, acoustic guitar, EDM drops, uplifting choirs, cheerful major-key pop",
|
|
316
|
+
},
|
|
317
|
+
"hip-hop": {
|
|
318
|
+
delivery: "rapped",
|
|
319
|
+
lyrics: `${RAP_FORM} Hip-hop with real bars: confident, witty, specific, a clear point of view, no motivational-poster lines.`,
|
|
320
|
+
production: "Punchy drums and bass up front, the rap vocal dry and clear on top, ad-libs at line ends.",
|
|
321
|
+
avoid: "singing in the verses, cheesy pop melodies, acoustic strumming, EDM drops",
|
|
322
|
+
},
|
|
323
|
+
punk: {
|
|
324
|
+
delivery: "sung",
|
|
325
|
+
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.",
|
|
326
|
+
production: "Raw live-room energy, loud distorted guitars, shouted gang vocals on the chorus.",
|
|
327
|
+
avoid: "polished pop vocals, synths, slow ballad sections, autotune",
|
|
328
|
+
},
|
|
329
|
+
blues: {
|
|
330
|
+
delivery: "sung",
|
|
331
|
+
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.",
|
|
332
|
+
production: "Live band in a small room, expressive guitar fills between the vocal lines.",
|
|
333
|
+
avoid: "pop chorus hooks, synths, autotune, polished modern production",
|
|
334
|
+
},
|
|
335
|
+
country: {
|
|
336
|
+
delivery: "sung",
|
|
337
|
+
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.",
|
|
338
|
+
production: "Warm organic band, upfront vocal with natural twang.",
|
|
339
|
+
avoid: "EDM drums, trap hi-hats, autotune, generic stadium pop",
|
|
340
|
+
},
|
|
341
|
+
jazz: {
|
|
342
|
+
delivery: "sung",
|
|
343
|
+
lyrics: "Jazz standard form: AABA, 32 bars, sophisticated adult wit, internal rhymes, a clever title phrase turned three ways, late-night city detail.",
|
|
344
|
+
production: "Acoustic small group, natural room sound, solos between choruses.",
|
|
345
|
+
avoid: "pop drums, synths, autotune, rap",
|
|
346
|
+
},
|
|
347
|
+
metal: {
|
|
348
|
+
delivery: "sung",
|
|
349
|
+
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.",
|
|
350
|
+
production: "Crushing tight guitars, powerful drums, huge low end.",
|
|
351
|
+
avoid: "pop vocals, acoustic softness, synth-pop, rap",
|
|
352
|
+
},
|
|
353
|
+
"classic-rock": {
|
|
354
|
+
delivery: "sung",
|
|
355
|
+
lyrics: "Classic rock: swaggering, sensual or road-worn stories, a riff-shaped chorus with a memorable title phrase, characters and places, a little danger.",
|
|
356
|
+
production: "Analog 1970s band sound, warm tube amps, live drums.",
|
|
357
|
+
avoid: "modern pop gloss, trap drums, autotune, synths",
|
|
358
|
+
},
|
|
359
|
+
rock: {
|
|
360
|
+
delivery: "sung",
|
|
361
|
+
lyrics: "Rock: concrete images and a story, a chorus built on a strong title phrase, verses that move the story on.",
|
|
362
|
+
production: "Big live band, guitars and drums up front.",
|
|
363
|
+
avoid: "autotune, trap drums, EDM drops",
|
|
364
|
+
},
|
|
365
|
+
pop: {
|
|
366
|
+
delivery: "sung",
|
|
367
|
+
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.",
|
|
368
|
+
production: "Polished modern mix, vocals bright and forward.",
|
|
369
|
+
avoid: "muddy lo-fi, out-of-tune vocals, rap verses",
|
|
370
|
+
},
|
|
371
|
+
rnb: {
|
|
372
|
+
delivery: "sung",
|
|
373
|
+
lyrics: "R&B: intimate, sensual, adult, specific (a hotel key, a voicemail, a late shift), melismatic hook phrases, a pre-chorus that builds tension.",
|
|
374
|
+
production: "Smooth warm low end, lush stacked harmonies, ad-libs.",
|
|
375
|
+
avoid: "rock guitars, EDM drops, stiff robotic vocals",
|
|
376
|
+
},
|
|
377
|
+
synthwave: {
|
|
378
|
+
delivery: "sung",
|
|
379
|
+
lyrics: "Synthwave: cinematic, nocturnal, a movie told in images, short evocative lines.",
|
|
380
|
+
production: "Retro analog synths, gated reverb drums, wide stereo.",
|
|
381
|
+
avoid: "acoustic instruments, trap hi-hats, modern pop vocal effects",
|
|
382
|
+
},
|
|
383
|
+
classical: {
|
|
384
|
+
delivery: "instrumental",
|
|
385
|
+
lyrics: "Classical, no words: each track is a complete concert piece or movement with a real form (sonata-allegro, rondo, theme and variations, nocturne, etude, scherzo, adagio, fugue, tone poem). Titles read like a concert programme: the form, a key, and an evocative subtitle, e.g. 'Nocturne in C-sharp minor, \"Harbor Fog\"' or 'Symphony No. 1: II. Adagio'. An album is a recital or a full multi-movement work. Each style names the form, key, tempo marking (Allegro, Andante, Largo...), the instruments that lead and the emotional arc (a quiet opening, the climax, how it resolves).",
|
|
386
|
+
production: "Recorded live in a concert hall, natural acoustic reverb, wide dynamic range from pianissimo to fortissimo, real acoustic instruments only.",
|
|
387
|
+
avoid: "vocals, singing, choir words, drums, drum kit, beats, bass guitar, synthesizers, electronic sounds, pop song structure, modern cinematic trailer hits",
|
|
388
|
+
},
|
|
389
|
+
"yacht-rock": {
|
|
390
|
+
delivery: "sung",
|
|
391
|
+
lyrics: "Yacht rock: smooth, wry adult romance, marinas and late lunches, sophisticated chords mean sophisticated words, a falsetto-ready chorus.",
|
|
392
|
+
production: "Glossy late-1970s West Coast studio sound, Rhodes, clean guitar, tight session drums.",
|
|
393
|
+
avoid: "distortion, rap, EDM, trap drums",
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
/** The genre's craft notes, or none for a genre the label has no notes on. */
|
|
397
|
+
export function craftFor(genre) {
|
|
398
|
+
return CRAFT[genreSlug(genre)];
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* How the label's songwriter is asked: NIXAMP_LABEL_MODEL when set (a reasoning
|
|
402
|
+
* model like gpt-oss-120b writes far better words than the quick default, but
|
|
403
|
+
* thinks first, so it gets room and time).
|
|
404
|
+
*/
|
|
405
|
+
function writer() {
|
|
406
|
+
const model = aiKey("NIXAMP_LABEL_MODEL");
|
|
407
|
+
return { ...(model ? { model } : {}), maxTokens: 9000, timeoutMs: 240_000 };
|
|
408
|
+
}
|
|
409
|
+
/** What every lyric avoids: the stock lines models reach for first. */
|
|
410
|
+
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";
|
|
411
|
+
/**
|
|
412
|
+
* The music model's brief for one song: the sound, the song's own style, the
|
|
413
|
+
* voice, how it is delivered and what to leave out.
|
|
414
|
+
*/
|
|
415
|
+
export function composePrompt(artist, song) {
|
|
416
|
+
const craft = craftFor(artist.genre);
|
|
417
|
+
const delivery = craft?.delivery === "rapped"
|
|
418
|
+
? "Lead vocal raps the verses in English: rhythmic, percussive flow locked to the beat, not sung; the hook is chanted or half-sung."
|
|
419
|
+
: craft?.delivery === "instrumental" ? "Purely instrumental, no voice of any kind." : "Sung in English.";
|
|
420
|
+
return [
|
|
421
|
+
`${artist.sound}.`,
|
|
422
|
+
`${song.style}.`,
|
|
423
|
+
craft?.delivery === "instrumental" ? "" : `Lead vocal: ${artist.voice}.`,
|
|
424
|
+
delivery,
|
|
425
|
+
craft?.production ?? "",
|
|
426
|
+
craft?.avoid ? `Avoid: ${craft.avoid}.` : "",
|
|
427
|
+
].filter(Boolean).join(" ").slice(0, 1500);
|
|
428
|
+
}
|
|
262
429
|
export const DEFAULTS = {
|
|
263
430
|
holder: "Profullstack, Inc.",
|
|
264
431
|
enabled: true,
|
|
@@ -317,12 +484,16 @@ const PLAN_SYSTEM = [
|
|
|
317
484
|
"Plan one album. Answer as JSON: {\"title\": \"1-6 words\", \"concept\": \"what the album is about, at most 300 characters\",",
|
|
318
485
|
"\"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
486
|
"Vary tempo, mood and arrangement across the running order, within the artist's sound; open strong, end memorable.",
|
|
487
|
+
"Titles are specific and surprising (an object, a place, a phrase someone says), never motivational slogans or abstract words like Rise, Empowered, Shine, Journey.",
|
|
488
|
+
"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
489
|
].join(" ");
|
|
321
490
|
const LYRICS_SYSTEM = [
|
|
322
|
-
"You
|
|
323
|
-
"
|
|
324
|
-
|
|
325
|
-
"
|
|
491
|
+
"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.",
|
|
492
|
+
"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.",
|
|
493
|
+
`Never use these stock words or phrases: ${CLICHES}. No rhyming a word with itself, no filler like "oh yeah" to finish a line.`,
|
|
494
|
+
"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.",
|
|
495
|
+
"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.",
|
|
496
|
+
"Only the words that are performed, no stage directions or notes. Answer as JSON: {\"lyrics\": \"the lyrics, lines separated by \\n\"}.",
|
|
326
497
|
].join(" ");
|
|
327
498
|
export async function planAlbum(artist, songs, ask = askJson) {
|
|
328
499
|
const before = artist.albums.slice(-4).map((one) => `"${one.title}" (${one.songs.map((song) => song.title).join("; ")})`);
|
|
@@ -336,11 +507,11 @@ export async function planAlbum(artist, songs, ask = askJson) {
|
|
|
336
507
|
artist.bio ? `Who they are: ${artist.bio}` : "",
|
|
337
508
|
artist.themes ? `What they sing about: ${artist.themes}` : "",
|
|
338
509
|
artist.cast ? `The band, as they are (write for these people): ${artist.cast}` : "",
|
|
510
|
+
craftFor(artist.genre) ? `Genre craft: ${craftFor(artist.genre).lyrics}` : "",
|
|
339
511
|
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
512
|
`The album has exactly ${songs} songs.`,
|
|
341
513
|
].filter(Boolean).join("\n"),
|
|
342
|
-
|
|
343
|
-
timeoutMs: 90_000,
|
|
514
|
+
...writer(),
|
|
344
515
|
});
|
|
345
516
|
const title = clean(answer.value["title"], 80).replace(/"/g, "");
|
|
346
517
|
const concept = clean(answer.value["concept"], 400);
|
|
@@ -365,8 +536,7 @@ export async function inventArtist(genre, taken, ask = askJson) {
|
|
|
365
536
|
const answer = await ask({
|
|
366
537
|
system: ARTIST_SYSTEM,
|
|
367
538
|
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,
|
|
539
|
+
...writer(),
|
|
370
540
|
});
|
|
371
541
|
const name = clean(answer.value["name"], 60).replace(/"/g, "");
|
|
372
542
|
const sound = clean(answer.value["sound"], 400);
|
|
@@ -377,6 +547,7 @@ export async function inventArtist(genre, taken, ask = askJson) {
|
|
|
377
547
|
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
548
|
}
|
|
379
549
|
export async function writeLyrics(artist, album, song, ask = askJson) {
|
|
550
|
+
const craft = craftFor(artist.genre);
|
|
380
551
|
const answer = await ask({
|
|
381
552
|
system: LYRICS_SYSTEM,
|
|
382
553
|
prompt: [
|
|
@@ -385,10 +556,10 @@ export async function writeLyrics(artist, album, song, ask = askJson) {
|
|
|
385
556
|
artist.cast ? `Who sings and plays it (their voice and personality shape the words): ${artist.cast}` : "",
|
|
386
557
|
`Album: "${album.title}"${album.concept ? `: ${album.concept}` : ""}`,
|
|
387
558
|
`Song: "${song.title}": ${song.style}`,
|
|
388
|
-
`
|
|
559
|
+
craft ? `Genre craft (follow it): ${craft.lyrics}` : "",
|
|
560
|
+
`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
561
|
].filter(Boolean).join("\n"),
|
|
390
|
-
|
|
391
|
-
timeoutMs: 90_000,
|
|
562
|
+
...writer(),
|
|
392
563
|
});
|
|
393
564
|
const lyrics = cleanLyrics(String(answer.value["lyrics"] ?? ""));
|
|
394
565
|
if (lyrics.replace(/\[[^\]]*\]/g, "").trim().length < 40)
|
|
@@ -1266,7 +1437,8 @@ export class AiLabel {
|
|
|
1266
1437
|
// Only a single is filmed, and only with Veo footage: a song with an older (slideshow) video is filmed when its turn comes.
|
|
1267
1438
|
// A filmed video whose singer was never filmed (a quota or a refusal took the close-ups) is filmed again while it stays up.
|
|
1268
1439
|
const singerless = (song) => Boolean(this.deps.lipsync) && this.settings.lipsyncShots > 0 && (song.videoVersion ?? 1) >= FILMED && !song.singer;
|
|
1269
|
-
|
|
1440
|
+
// Instrumentals have no singer and no band to cast the way Veo films them: no video.
|
|
1441
|
+
const missing = songs.filter(({ artist, song }) => craftFor(artist.genre)?.delivery !== "instrumental" && this.wantsVeo(song) && ((song.videoVersion ?? 1) < FILMED || singerless(song)) && this.now() - (this.filmTriedAt.get(song.file) ?? 0) > 60 * 60_000);
|
|
1270
1442
|
// A single already on /music is filmed again before new ones: the month's budget finishes one video, not starts several.
|
|
1271
1443
|
missing.sort((a, b) => Number((b.song.videoVersion ?? 1) >= SHOWN && Boolean(b.song.video)) - Number((a.song.videoVersion ?? 1) >= SHOWN && Boolean(a.song.video)));
|
|
1272
1444
|
const untagged = songs.filter(({ song }) => song.video && !song.videoTagged && (song.videoVersion ?? 1) >= SHOWN);
|
|
@@ -1767,7 +1939,8 @@ export class AiLabel {
|
|
|
1767
1939
|
const raw = join(album.folder, `.${randomBytes(4).toString("hex")}.raw.mp3`);
|
|
1768
1940
|
try {
|
|
1769
1941
|
let writer = song.made || album.writer;
|
|
1770
|
-
|
|
1942
|
+
const instrumental = craftFor(artist.genre)?.delivery === "instrumental";
|
|
1943
|
+
if (!song.lyrics && !instrumental) {
|
|
1771
1944
|
const written = await writeLyrics({ ...artist, cast: this.castOf(artist) }, album, song, this.deps.ask ?? askJson);
|
|
1772
1945
|
song.lyrics = written.lyrics;
|
|
1773
1946
|
writer = written.model;
|
|
@@ -1776,14 +1949,16 @@ export class AiLabel {
|
|
|
1776
1949
|
}
|
|
1777
1950
|
await this.ensureCover(artist, album);
|
|
1778
1951
|
this.log(`composing "${song.title}" (${artist.name}, ${album.title}, ${song.seconds} s, ${composer.name})`);
|
|
1779
|
-
const prompt =
|
|
1780
|
-
await composer.compose({ prompt, seconds: song.seconds, vocals:
|
|
1952
|
+
const prompt = composePrompt(artist, song);
|
|
1953
|
+
await composer.compose({ prompt, seconds: song.seconds, vocals: !instrumental, out: raw, ...(instrumental ? {} : { lyrics: song.lyrics }) });
|
|
1781
1954
|
const month = this.period();
|
|
1782
1955
|
this.usage[month] = (this.usage[month] ?? 0) + song.seconds;
|
|
1783
1956
|
const base = songBase(song);
|
|
1784
1957
|
const file = join(album.folder, `${base}.mp3`);
|
|
1785
|
-
|
|
1786
|
-
|
|
1958
|
+
// An instrumental has no words, so no words file.
|
|
1959
|
+
const lyricsFile = instrumental ? "" : join(album.folder, `${base}.lyrics.txt`);
|
|
1960
|
+
if (lyricsFile)
|
|
1961
|
+
writeFileSync(lyricsFile, `${song.lyrics}\n`);
|
|
1787
1962
|
const holder = this.settings.holder;
|
|
1788
1963
|
const tags = {
|
|
1789
1964
|
title: song.title,
|
|
@@ -1795,8 +1970,8 @@ export class AiLabel {
|
|
|
1795
1970
|
date: String(album.year),
|
|
1796
1971
|
publisher: holder,
|
|
1797
1972
|
copyright: `℗ ${album.year} ${holder}`,
|
|
1798
|
-
comment: `Made with AI for ${holder}: music by ${composer.name}, words by ${writer}`,
|
|
1799
|
-
"lyrics-eng": song.lyrics,
|
|
1973
|
+
comment: instrumental ? `Made with AI for ${holder}: music by ${composer.name}, planned by ${writer}` : `Made with AI for ${holder}: music by ${composer.name}, words by ${writer}`,
|
|
1974
|
+
...(instrumental ? {} : { "lyrics-eng": song.lyrics }),
|
|
1800
1975
|
};
|
|
1801
1976
|
await (this.deps.tag ?? ((from, to, values, cover) => tagFile(this.deps.ffmpeg, from, to, values, cover)))(raw, file, tags, album.cover);
|
|
1802
1977
|
song.file = file;
|
|
@@ -1949,7 +2124,9 @@ export class AiLabel {
|
|
|
1949
2124
|
return null;
|
|
1950
2125
|
if (match[2] === "mp4")
|
|
1951
2126
|
return song.video && (song.videoVersion ?? 1) >= SHOWN ? { path: song.video, type: "video/mp4" } : null;
|
|
1952
|
-
|
|
2127
|
+
if (match[2] === "mp3")
|
|
2128
|
+
return { path: song.file, type: "audio/mpeg" };
|
|
2129
|
+
return song.lyricsFile ? { path: song.lyricsFile, type: "text/plain; charset=utf-8" } : null;
|
|
1953
2130
|
}
|
|
1954
2131
|
/** The label song at this path, for the now-playing card's Download buttons. */
|
|
1955
2132
|
songAt(file) {
|
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/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).
|