nixamp 0.73.0 → 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/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/web/dist/assets/{ar-0IqzB31c.js → ar-Bsgrtf2h.js} +1 -1
- package/web/dist/assets/{cs-DCfCI0eZ.js → cs-ZsF7vMVh.js} +1 -1
- package/web/dist/assets/{da-Bss0fAE3.js → da-381kzPn7.js} +1 -1
- package/web/dist/assets/{de-rv67Ab7H.js → de-DizoHpaG.js} +1 -1
- package/web/dist/assets/{es-DV-6B3BH.js → es-YAdydD7d.js} +1 -1
- package/web/dist/assets/{fi-DQS8All1.js → fi-BUHThgUk.js} +1 -1
- package/web/dist/assets/{fr-CCl8kuVS.js → fr-CKa1Nkl9.js} +1 -1
- package/web/dist/assets/{hi-DjRXm_AW.js → hi-Cbv0ZU2J.js} +1 -1
- package/web/dist/assets/{hu-BnY80Y25.js → hu-25AxyySz.js} +1 -1
- package/web/dist/assets/{id-a14Arova.js → id-rYanwc8o.js} +1 -1
- package/web/dist/assets/{it-Clku_76_.js → it-eebB-mQw.js} +1 -1
- package/web/dist/assets/{main-39LJVPMZ.js → main-CFt8ILSw.js} +2 -2
- package/web/dist/assets/{nl-xnrEs6a0.js → nl-D1EYp0XC.js} +1 -1
- package/web/dist/assets/{ru-BiwJsMjn.js → ru-DpxLY1H_.js} +1 -1
- package/web/dist/assets/{sv-CflBqeQ8.js → sv--koGVKUA.js} +1 -1
- package/web/dist/assets/{uk-9vC_u0Y6.js → uk-oI3mSYqs.js} +1 -1
- package/web/dist/assets/{vi-CulXW-FL.js → vi-bt6nXnTa.js} +1 -1
- package/web/dist/assets/{zh-CwaS2ltZ.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/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;
|
package/src/ai-text.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface AskOptions {
|
|
|
19
19
|
timeoutMs?: number;
|
|
20
20
|
/** The longest answer, in tokens (default 1024): a song's words need more than a title does. */
|
|
21
21
|
maxTokens?: number;
|
|
22
|
+
/** The OpenAI-compatible model to ask instead of NIXAMP_OPENAI_MODEL (a slower, better writer for song words). */
|
|
23
|
+
model?: string;
|
|
22
24
|
fetcher?: typeof fetch;
|
|
23
25
|
env?: Record<string, string | undefined>;
|
|
24
26
|
}
|
|
@@ -118,7 +120,7 @@ export async function askJson(options: AskOptions): Promise<Answer> {
|
|
|
118
120
|
// Workers AI's /ai/v1, Ollama's /v1): NIXAMP_OPENAI_URL is that base.
|
|
119
121
|
const baseUrl = (aiKey("NIXAMP_OPENAI_URL", env) || "https://api.openai.com/v1").replace(/\/+$/, "");
|
|
120
122
|
const official = new URL(baseUrl).host === "api.openai.com";
|
|
121
|
-
const model = aiKey("NIXAMP_OPENAI_MODEL", env) || "gpt-4.1-mini";
|
|
123
|
+
const model = options.model || aiKey("NIXAMP_OPENAI_MODEL", env) || "gpt-4.1-mini";
|
|
122
124
|
const response = await send(`${baseUrl}/chat/completions`, {
|
|
123
125
|
method: "POST",
|
|
124
126
|
signal: AbortSignal.timeout(options.timeoutMs ?? 30_000),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./main-
|
|
1
|
+
import{t as e}from"./main-CFt8ILSw.js";var t={...e,"No parties live right now.":`لا توجد جلسات مباشرة الآن.`,"Could not refresh parties.":`تعذر تحديث الجلسات.`,"Pick a server in the directory, or paste its link here.":`اختر خادمًا من الدليل أو الصق رابطه هنا.`,"Interface language":`لغة الواجهة`,Language:`اللغة`,"Translation language":`لغة الترجمة`,"Original (auto-detect)":`الأصل (اكتشاف تلقائي)`,Play:`تشغيل`,Pause:`إيقاف مؤقت`,Stop:`إيقاف`,Next:`التالي`,Previous:`السابق`,Select:`اختيار`,"Next/Prev":`التالي/السابق`,Quit:`خروج`,"Now Playing":`قيد التشغيل`,"Spectrum Analyser":`محلل الطيف`,Playlist:`قائمة التشغيل`,Files:`الملفات`,"Open files":`فتح ملفات`,"Open folder":`فتح مجلد`,"Add to library":`إضافة إلى المكتبة`,"Replace playlist":`استبدال قائمة التشغيل`,Filter:`تصفية`,Search:`بحث`,"Nothing loaded.":`لم يُحمّل أي محتوى.`,"Empty.":`فارغ.`,"Loading…":`جارٍ التحميل…`,"Connecting…":`جارٍ الاتصال…`,Connected:`متصل`,"Not connected":`غير متصل`,"No source":`لا يوجد مصدر`,Playing:`يعمل`,Stopped:`متوقف`,Volume:`مستوى الصوت`,Seek:`موضع التشغيل`,"Full screen":`ملء الشاشة`,Download:`تنزيل`,Share:`مشاركة`,Copy:`نسخ`,"Copy link":`نسخ الرابط`,Copied:`تم النسخ`,Save:`حفظ`,Cancel:`إلغاء`,Close:`إغلاق`,Remove:`إزالة`,Delete:`حذف`,Edit:`تعديل`,Add:`إضافة`,Send:`إرسال`,Retry:`إعادة المحاولة`,Back:`رجوع`,"Next page":`الصفحة التالية`,"Previous page":`الصفحة السابقة`,"Show more":`عرض المزيد`,Panels:`اللوحات`,"Reset layout":`إعادة ضبط التخطيط`,Show:`إظهار`,Hide:`إخفاء`,Collapse:`طي`,Expand:`توسيع`,"Move up":`نقل لأعلى`,"Move down":`نقل لأسفل`,Install:`تثبيت`,"TV mode":`وضع التلفاز`,On:`مفعّل`,Off:`معطّل`,Account:`الحساب`,"Sign in":`تسجيل الدخول`,"Sign out":`تسجيل الخروج`,"Create account":`إنشاء حساب`,Email:`البريد الإلكتروني`,Password:`كلمة المرور`,"Forgot password?":`نسيت كلمة المرور؟`,"Welcome back":`مرحبًا بعودتك`,Profile:`الملف الشخصي`,Handle:`اسم المستخدم`,"Your servers":`خوادمك`,Server:`الخادم`,Connect:`اتصال`,Disconnect:`قطع الاتصال`,Directory:`الدليل`,Favourites:`المفضلة`,Follow:`متابعة`,Unfollow:`إلغاء المتابعة`,Notifications:`الإشعارات`,"Phone number":`رقم الهاتف`,"Save number":`حفظ الرقم`,Transcript:`النص المفرغ`,Captions:`الترجمة النصية`,"Translate audio":`ترجمة الصوت`,"Audio language":`لغة الصوت`,"Audio options":`خيارات الصوت`,"Keep background sound":`الاحتفاظ بأصوات الخلفية`,"Background level":`مستوى صوت الخلفية`,"Speaker voices":`أصوات المتحدثين`,"Any voice":`أي صوت`,"Male voice":`صوت رجالي`,"Female voice":`صوت نسائي`,"Original audio.":`الصوت الأصلي.`,"Buy translated audio":`شراء الصوت المترجم`,"Audio credit":`رصيد الصوت`,"Buy pass":`شراء تصريح`,Pass:`تصريح`,"Pay with":`الدفع باستخدام`,"Check payment":`التحقق من الدفع`,"Open checkout":`الانتقال للدفع`,"Free sessions":`جلسات مجانية`,"Usage rates and expiry":`الأسعار ومدة الصلاحية`,Catalogs:`الكتالوجات`,"Add catalog":`إضافة كتالوج`,"Catalog name":`اسم الكتالوج`,Link:`الرابط`,"Play link":`تشغيل الرابط`,"Go live":`بدء بث مباشر`,Publish:`نشر`,Listen:`استماع`,"Live now":`مباشر الآن`,Upcoming:`قريبًا`,Parties:`حفلات المشاهدة`,"Join party":`الانضمام للحفلة`,"Invite code":`رمز الدعوة`,Invite:`دعوة`,Chat:`الدردشة`,"Chat message":`رسالة دردشة`,People:`الأشخاص`,Speaker:`متحدث`,Speakers:`المتحدثون`,Listener:`مستمع`,Moderator:`مشرف`,"Raise hand":`رفع اليد`,"Hand raises":`الأيدي المرفوعة`,Questions:`الأسئلة`,Resources:`الموارد`,Replay:`إعادة العرض`,Schedule:`الجدول`,Event:`الفعالية`,Host:`المضيف`,About:`حول`,Stage:`المنصة`,Title:`العنوان`,Description:`الوصف`,Topic:`الموضوع`,Public:`عام`,"Anyone with the link":`أي شخص لديه الرابط`,"Invited people":`الأشخاص المدعوون`,"Who can join":`من يمكنه الانضمام`,"Date and time":`التاريخ والوقت`,"Expected length":`المدة المتوقعة`,Repeat:`تكرار`,"Does not repeat":`لا يتكرر`,"Every day":`كل يوم`,"Every week":`كل أسبوع`,Homepage:`الموقع الإلكتروني`,"Host name":`اسم المضيف`,"Profile photo URL":`رابط صورة الملف الشخصي`,"Broadcast link":`رابط البث`,"Edit event":`تعديل الفعالية`,"Save changes":`حفظ التغييرات`,"Create live":`إنشاء بث مباشر`,"Schedule class":`جدولة درس`,"Start class":`بدء الدرس`,"End event":`إنهاء الفعالية`,"End session":`إنهاء الجلسة`,"Session ended":`انتهت الجلسة`,"Event ended":`انتهت الفعالية`,Cancelled:`ملغاة`,"Replay available":`إعادة العرض متاحة`,Join:`انضمام`,"Create invite":`إنشاء دعوة`,"Invite as":`الدعوة بصفة`,"Watch now":`شاهد الآن`,"Start a room":`إنشاء غرفة`,"Recent replays":`إعادات العرض الأخيرة`,"Your account":`حسابك`,"Write with AI":`الكتابة بالذكاء الاصطناعي`,"Suggested title":`العنوان المقترح`,"Suggested description":`الوصف المقترح`,"Suggested topic":`الموضوع المقترح`,"Use these details":`استخدام هذه التفاصيل`,"Learn something live.":`تعلّم شيئًا مباشرة.`,"Screen share":`مشاركة الشاشة`,"Upload and broadcast":`رفع وبث`,Help:`مساعدة`,"Reload Player":`إعادة تحميل المشغل`,"Copy Bundled CLI Path":`نسخ مسار أداة سطر الأوامر المضمنة`,"Developer tools":`أدوات المطور`,Undo:`تراجع`,Redo:`إعادة`,Cut:`قص`,Paste:`لصق`,"Select all":`تحديد الكل`,Status:`الحالة`,"Activity log":`سجل النشاط`,Admin:`الإدارة`,"Playback controls":`عناصر التحكم بالتشغيل`,"Tracks and folders":`المقاطع والمجلدات`,"Original-language captions.":`ترجمة نصية باللغة الأصلية.`,"Skip to playback controls":`الانتقال إلى عناصر التحكم بالتشغيل`,"Skip to playlist":`الانتقال إلى قائمة التشغيل`,"Skip to transcript and translated audio":`الانتقال إلى النص المفرغ والصوت المترجم`};export{t as default};
|