@ericdisero/aurora-shared 0.1.0 → 0.2.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/db.js +25 -2
- package/dist/extract-catalog.d.ts +81 -0
- package/dist/extract-catalog.js +351 -0
- package/dist/extract.d.ts +35 -0
- package/dist/extract.js +133 -0
- package/dist/jobs.d.ts +4 -1
- package/dist/jobs.js +56 -0
- package/dist/key-detect.d.ts +3 -0
- package/dist/key-detect.js +154 -0
- package/dist/operations/index.js +422 -45
- package/dist/providers/suno.d.ts +57 -0
- package/dist/providers/suno.js +89 -10
- package/dist/skills/content.js +2 -2
- package/dist/storage/assets.d.ts +3 -0
- package/dist/storage/assets.js +7 -0
- package/dist/storage/extractions.d.ts +11 -0
- package/dist/storage/extractions.js +42 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/skills/aurora-cost-discipline.md +4 -2
- package/skills/aurora-suno-prompting.md +47 -16
package/dist/providers/suno.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ export interface GenerationParams {
|
|
|
20
20
|
vocalGender?: 'male' | 'female';
|
|
21
21
|
/** Comma-separated styles to exclude (the docs type this as ONE string). */
|
|
22
22
|
negativeTags?: string;
|
|
23
|
+
/** 0..1 — style guidance intensity. */
|
|
24
|
+
styleWeight?: number;
|
|
25
|
+
/** 0..1 — creative deviation / novelty. */
|
|
26
|
+
weirdnessConstraint?: number;
|
|
27
|
+
/** 0..1 — input-audio influence (audio-conditioned tasks). */
|
|
28
|
+
audioWeight?: number;
|
|
29
|
+
/** Persona id (Generate Persona) or a Suno Voice voiceId. Custom mode only. */
|
|
30
|
+
personaId?: string;
|
|
31
|
+
/** 'style_persona' (default) | 'voice_persona' (voiceId on V5/V5_5). */
|
|
32
|
+
personaModel?: 'style_persona' | 'voice_persona';
|
|
23
33
|
}
|
|
24
34
|
/** Submit a generation task. Returns the provider taskId. */
|
|
25
35
|
export declare function createGeneration(params: GenerationParams): Promise<string>;
|
|
@@ -31,6 +41,8 @@ export interface SoundsParams {
|
|
|
31
41
|
/** BPM 1-300; omit for auto. */
|
|
32
42
|
soundTempo?: number;
|
|
33
43
|
soundLoop?: boolean;
|
|
44
|
+
/** Capture lyric subtitles alongside the audio. */
|
|
45
|
+
grabLyrics?: boolean;
|
|
34
46
|
}
|
|
35
47
|
/** Submit a Sounds Generation task. sunoapi.org ONLY — kie.ai does not expose
|
|
36
48
|
* the endpoint. Model locked to V5 by the docs. */
|
|
@@ -51,10 +63,55 @@ export interface CoverParams {
|
|
|
51
63
|
negativeTags?: string;
|
|
52
64
|
/** 0..1 — how strongly the output hews to the input audio. */
|
|
53
65
|
audioWeight?: number;
|
|
66
|
+
styleWeight?: number;
|
|
67
|
+
weirdnessConstraint?: number;
|
|
68
|
+
personaId?: string;
|
|
69
|
+
personaModel?: 'style_persona' | 'voice_persona';
|
|
54
70
|
}
|
|
55
71
|
/** Submit an upload-and-cover (style transform) task. Poll the returned taskId
|
|
56
72
|
* with the generation record fetchers (same record-info endpoint). */
|
|
57
73
|
export declare function createCover(params: CoverParams): Promise<string>;
|
|
74
|
+
export interface AddVocalsParams {
|
|
75
|
+
/** Hosted instrumental URL from uploadAudioFile. */
|
|
76
|
+
uploadUrl: string;
|
|
77
|
+
/** Vocal content + stylistic direction (lyrics-style text works). */
|
|
78
|
+
prompt: string;
|
|
79
|
+
/** Genre / vocal approach, e.g. 'epic film choir, massed choral harmonies'. */
|
|
80
|
+
style: string;
|
|
81
|
+
/** Track title (≤100 chars). */
|
|
82
|
+
title: string;
|
|
83
|
+
/** Vocal styles to exclude, ONE comma-separated string (required by the docs). */
|
|
84
|
+
negativeTags: string;
|
|
85
|
+
vocalGender?: 'male' | 'female';
|
|
86
|
+
styleWeight?: number;
|
|
87
|
+
weirdnessConstraint?: number;
|
|
88
|
+
audioWeight?: number;
|
|
89
|
+
/** V4_5PLUS (default) | V5 | V5_5 — this endpoint supports only these three. */
|
|
90
|
+
model?: string;
|
|
91
|
+
}
|
|
92
|
+
/** Submit an add-vocals task: generates vocals over the uploaded instrumental,
|
|
93
|
+
* harmonized with it. Poll with the generation record fetchers. */
|
|
94
|
+
export declare function createAddVocals(params: AddVocalsParams): Promise<string>;
|
|
95
|
+
export interface AddInstrumentalParams {
|
|
96
|
+
/** Hosted audio URL (usually vocals or a stem) from uploadAudioFile. */
|
|
97
|
+
uploadUrl: string;
|
|
98
|
+
/** Track title (≤100 chars). */
|
|
99
|
+
title: string;
|
|
100
|
+
/** Desired instrumental style/mood/instruments — this endpoint names the field
|
|
101
|
+
* `tags`, NOT `style`. */
|
|
102
|
+
tags: string;
|
|
103
|
+
/** Styles/instruments to exclude, ONE comma-separated string. */
|
|
104
|
+
negativeTags: string;
|
|
105
|
+
vocalGender?: 'male' | 'female';
|
|
106
|
+
styleWeight?: number;
|
|
107
|
+
weirdnessConstraint?: number;
|
|
108
|
+
audioWeight?: number;
|
|
109
|
+
/** V4_5PLUS (default) | V5 | V5_5. */
|
|
110
|
+
model?: string;
|
|
111
|
+
}
|
|
112
|
+
/** Submit an add-instrumental task: generates backing instrumentation
|
|
113
|
+
* complementary to the uploaded audio. Poll with the generation fetchers. */
|
|
114
|
+
export declare function createAddInstrumental(params: AddInstrumentalParams): Promise<string>;
|
|
58
115
|
export interface PolledVariation {
|
|
59
116
|
/** Per-variation audio id — the WAV-conversion stage needs it. */
|
|
60
117
|
id?: string;
|
package/dist/providers/suno.js
CHANGED
|
@@ -46,6 +46,24 @@ const POLL_DELAY_MS = 5000;
|
|
|
46
46
|
const MAX_POLL_ATTEMPTS = 120; // ~10 min ceiling
|
|
47
47
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
48
48
|
const wireVocalGender = (v) => (v === 'male' ? 'm' : 'f');
|
|
49
|
+
/** Append the shared optional knobs (full wire surface, param-table contract:
|
|
50
|
+
* docs/suno-param-surface.md) onto a create body. */
|
|
51
|
+
function applySharedKnobs(body, p) {
|
|
52
|
+
if (p.vocalGender)
|
|
53
|
+
body.vocalGender = wireVocalGender(p.vocalGender);
|
|
54
|
+
if (p.negativeTags)
|
|
55
|
+
body.negativeTags = p.negativeTags;
|
|
56
|
+
if (p.styleWeight !== undefined)
|
|
57
|
+
body.styleWeight = p.styleWeight;
|
|
58
|
+
if (p.weirdnessConstraint !== undefined)
|
|
59
|
+
body.weirdnessConstraint = p.weirdnessConstraint;
|
|
60
|
+
if (p.audioWeight !== undefined)
|
|
61
|
+
body.audioWeight = p.audioWeight;
|
|
62
|
+
if (p.personaId) {
|
|
63
|
+
body.personaId = p.personaId;
|
|
64
|
+
body.personaModel = p.personaModel ?? 'style_persona';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
49
67
|
/** Submit a generation task. Returns the provider taskId. */
|
|
50
68
|
export async function createGeneration(params) {
|
|
51
69
|
const body = {
|
|
@@ -58,10 +76,7 @@ export async function createGeneration(params) {
|
|
|
58
76
|
body.style = params.style;
|
|
59
77
|
if (params.title)
|
|
60
78
|
body.title = params.title;
|
|
61
|
-
|
|
62
|
-
body.vocalGender = wireVocalGender(params.vocalGender);
|
|
63
|
-
if (params.negativeTags)
|
|
64
|
-
body.negativeTags = params.negativeTags;
|
|
79
|
+
applySharedKnobs(body, params);
|
|
65
80
|
const res = await fetch(api('/api/v1/generate'), {
|
|
66
81
|
method: 'POST',
|
|
67
82
|
headers: authHeaders(),
|
|
@@ -84,6 +99,8 @@ export async function createSoundsGeneration(params) {
|
|
|
84
99
|
body.soundTempo = params.soundTempo;
|
|
85
100
|
if (params.soundLoop)
|
|
86
101
|
body.soundLoop = true;
|
|
102
|
+
if (params.grabLyrics)
|
|
103
|
+
body.grabLyrics = true;
|
|
87
104
|
const res = await fetch(api('/api/v1/generate/sounds'), {
|
|
88
105
|
method: 'POST',
|
|
89
106
|
headers: authHeaders(),
|
|
@@ -153,12 +170,7 @@ export async function createCover(params) {
|
|
|
153
170
|
body.style = params.style;
|
|
154
171
|
if (params.title)
|
|
155
172
|
body.title = params.title;
|
|
156
|
-
|
|
157
|
-
body.vocalGender = wireVocalGender(params.vocalGender);
|
|
158
|
-
if (params.negativeTags)
|
|
159
|
-
body.negativeTags = params.negativeTags;
|
|
160
|
-
if (params.audioWeight !== undefined)
|
|
161
|
-
body.audioWeight = params.audioWeight;
|
|
173
|
+
applySharedKnobs(body, params);
|
|
162
174
|
if (withCallback)
|
|
163
175
|
body.callBackUrl = CALLBACK_PLACEHOLDER;
|
|
164
176
|
const res = await fetch(api('/api/v1/generate/upload-cover'), {
|
|
@@ -180,6 +192,73 @@ export async function createCover(params) {
|
|
|
180
192
|
return attempt(true);
|
|
181
193
|
}
|
|
182
194
|
}
|
|
195
|
+
/** Submit an add-vocals task: generates vocals over the uploaded instrumental,
|
|
196
|
+
* harmonized with it. Poll with the generation record fetchers. */
|
|
197
|
+
export async function createAddVocals(params) {
|
|
198
|
+
const attempt = async (withCallback) => {
|
|
199
|
+
const body = {
|
|
200
|
+
uploadUrl: params.uploadUrl,
|
|
201
|
+
prompt: params.prompt,
|
|
202
|
+
style: params.style,
|
|
203
|
+
title: params.title,
|
|
204
|
+
negativeTags: params.negativeTags,
|
|
205
|
+
model: params.model ?? 'V4_5PLUS'
|
|
206
|
+
};
|
|
207
|
+
applySharedKnobs(body, { ...params, negativeTags: undefined });
|
|
208
|
+
if (withCallback)
|
|
209
|
+
body.callBackUrl = CALLBACK_PLACEHOLDER;
|
|
210
|
+
const res = await fetch(api('/api/v1/generate/add-vocals'), {
|
|
211
|
+
method: 'POST',
|
|
212
|
+
headers: authHeaders(),
|
|
213
|
+
body: JSON.stringify(body)
|
|
214
|
+
});
|
|
215
|
+
const json = (await res.json());
|
|
216
|
+
const taskId = json.data?.taskId ?? json.data?.task_id;
|
|
217
|
+
if (!res.ok || (json.code !== undefined && json.code !== 200) || !taskId) {
|
|
218
|
+
throw new Error(`${host()} add-vocals failed (HTTP ${res.status}, code ${json.code ?? 'n/a'}): ${json.msg || 'no taskId returned'}`);
|
|
219
|
+
}
|
|
220
|
+
return taskId;
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
return await attempt(false);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return attempt(true);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Submit an add-instrumental task: generates backing instrumentation
|
|
230
|
+
* complementary to the uploaded audio. Poll with the generation fetchers. */
|
|
231
|
+
export async function createAddInstrumental(params) {
|
|
232
|
+
const attempt = async (withCallback) => {
|
|
233
|
+
const body = {
|
|
234
|
+
uploadUrl: params.uploadUrl,
|
|
235
|
+
title: params.title,
|
|
236
|
+
tags: params.tags,
|
|
237
|
+
negativeTags: params.negativeTags,
|
|
238
|
+
model: params.model ?? 'V4_5PLUS'
|
|
239
|
+
};
|
|
240
|
+
applySharedKnobs(body, { ...params, negativeTags: undefined });
|
|
241
|
+
if (withCallback)
|
|
242
|
+
body.callBackUrl = CALLBACK_PLACEHOLDER;
|
|
243
|
+
const res = await fetch(api('/api/v1/generate/add-instrumental'), {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: authHeaders(),
|
|
246
|
+
body: JSON.stringify(body)
|
|
247
|
+
});
|
|
248
|
+
const json = (await res.json());
|
|
249
|
+
const taskId = json.data?.taskId ?? json.data?.task_id;
|
|
250
|
+
if (!res.ok || (json.code !== undefined && json.code !== 200) || !taskId) {
|
|
251
|
+
throw new Error(`${host()} add-instrumental failed (HTTP ${res.status}, code ${json.code ?? 'n/a'}): ${json.msg || 'no taskId returned'}`);
|
|
252
|
+
}
|
|
253
|
+
return taskId;
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
return await attempt(false);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return attempt(true);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
183
262
|
/** ONE record-info fetch (no waiting). The background-job model's poll unit. */
|
|
184
263
|
export async function fetchGenerationRecord(taskId) {
|
|
185
264
|
const res = await fetch(`${api('/api/v1/generate/record-info')}?taskId=${encodeURIComponent(taskId)}`, { headers: authHeaders() });
|
package/dist/skills/content.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// GENERATED — do not edit. Source: packages/shared/skills/*.md
|
|
2
2
|
// Regenerated by scripts/embed-skills.mjs on every build.
|
|
3
3
|
export const SKILLS = {
|
|
4
|
-
"aurora-cost-discipline": "---\nname: aurora-cost-discipline\ndescription: Credit and spend discipline for every paid Aurora operation (Suno generation/cover/sounds/WAV, MVSEP splits). Fires before any aurora_generate, aurora_cover, aurora_sounds, aurora_split, or aurora_fetch_wav call.\n---\n\n# Aurora Cost Discipline\n\nTwo metered providers sit behind Aurora's cloud ops. Spend is real money. The rules:\n\n## Always\n\n1. **`aurora_get_credits` BEFORE the first paid call of a session** — and after a batch, to log actual spend.\n2. **Never re-split.** `aurora_split` burns real MVSEP credits; the op refuses when 7 stems already exist — don't work around it. Check `aurora_list_assets` first.\n3. **Batch authorization, not per-call nagging.** When the user approves a multi-generation plan (\"make me 4 braams and a riser\"), that approval covers the enumerated batch — don't re-confirm each call. NEW spend beyond the approved batch needs a fresh ask.\n\n## Known costs (sunoapi.org credits, measured 2026-06-10)\n\n| Op | Cost |\n|---|---|\n| `aurora_sounds` | ~2.5 credits (~$0.0125) — the cheap verification + layer tool |\n| `aurora_cover` | ~12 credits + ~0.4 per WAV fetch |\n| `aurora_fetch_wav` | ~0.4 credits per conversion |\n| `aurora_generate` | not yet measured — check credits before/after and report the delta |\n| `aurora_split` | MVSEP credits, priced by audio duration (separate balance) |\n| `aurora_get_credits`, all local ffmpeg/stack/project ops | FREE |\n\n## Cheap-first ladder\n\n- Verifying a pipeline or experimenting? `aurora_sounds` first (2.5 credits), full `aurora_generate` only when the user wants a track.\n- Audition MP3s before paying for WAV upgrades; `aurora_fetch_wav` only the keepers.\n- Local ops (`aurora_pitch_shift`, `aurora_convert`, stack everything) cost nothing — prefer them over regenerating.\n",
|
|
4
|
+
"aurora-cost-discipline": "---\nname: aurora-cost-discipline\ndescription: Credit and spend discipline for every paid Aurora operation (Suno generation/cover/sounds/layering/WAV, MVSEP splits and extractions). Fires before any aurora_generate, aurora_cover, aurora_add_vocals, aurora_add_instrumental, aurora_sounds, aurora_split, aurora_extract, or aurora_fetch_wav call.\n---\n\n# Aurora Cost Discipline\n\nTwo metered providers sit behind Aurora's cloud ops. Spend is real money. The rules:\n\n## Always\n\n1. **`aurora_get_credits` BEFORE the first paid call of a session** — and after a batch, to log actual spend.\n2. **Never re-split.** `aurora_split` burns real MVSEP credits; the op refuses when 7 stems already exist — don't work around it. Check `aurora_list_assets` first.\n3. **Batch authorization, not per-call nagging.** When the user approves a multi-generation plan (\"make me 4 braams and a riser\"), that approval covers the enumerated batch — don't re-confirm each call. NEW spend beyond the approved batch needs a fresh ask.\n\n## Known costs (sunoapi.org credits, measured 2026-06-10)\n\n| Op | Cost |\n|---|---|\n| `aurora_sounds` | ~2.5 credits (~$0.0125) — the cheap verification + layer tool |\n| `aurora_cover` | ~12 credits + ~0.4 per WAV fetch |\n| `aurora_fetch_wav` | ~0.4 credits per conversion |\n| `aurora_generate` | not yet measured — check credits before/after and report the delta |\n| `aurora_add_vocals` / `aurora_add_instrumental` | not yet measured — same generation family; check the delta and report it |\n| `aurora_split` | MVSEP credits, priced by audio duration (separate balance); ALWAYS 3 MVSEP calls |\n| `aurora_extract` | MVSEP credits, VARIABLE by selection — the op's response includes the call plan; bundles count once however many of their stems you pick. Read the estimate before confirming a big catalog run |\n| `aurora_get_credits`, all local ffmpeg/stack/project ops | FREE |\n\n## Cheap-first ladder\n\n- Verifying a pipeline or experimenting? `aurora_sounds` first (2.5 credits), full `aurora_generate` only when the user wants a track.\n- Audition MP3s before paying for WAV upgrades; `aurora_fetch_wav` only the keepers.\n- Local ops (`aurora_pitch_shift`, `aurora_convert`, stack everything) cost nothing — prefer them over regenerating.\n",
|
|
5
5
|
"aurora-music-production": "---\nname: aurora-music-production\ndescription: End-to-end Aurora workflow — create a project, generate or cover tracks, manufacture sounds, split into 7 stems, layer in the stack, export aligned WAVs for the DAW. Use when driving Aurora (the AI audio workbench) for any music production task.\n---\n\n# Aurora Music Production Workflow\n\nAurora is the desktop layer between AI music generation and a real DAW: generate AI music, split anything into stems, keep it all organized. Files on disk ARE the product — everything you create lands in a real project folder the user can open, play, and drag into their DAW.\n\n## Session start\n\n1. `aurora_get_workspace_state` — projects list, key status, folder locations. Once per session.\n2. `aurora_get_credits` — Suno credits + MVSEP minutes. ALWAYS before paid calls (see aurora-cost-discipline).\n\n## The verbs\n\n- **Generate** (`aurora_generate`) — full track from a prompt. 2 variations land as assets. 1-3 min.\n- **Cover** (`aurora_cover`) — style-transform an existing asset or file: same musical content, new style. `audioWeight` is the dial: 0 = new style dominates, 1 = stay close to the source.\n- **Sounds** (`aurora_sounds`) — samples, one-shots, loops with key/tempo lock. Fast (~20-30s), cheap (~2.5 credits). The layer-manufacturing tool: braams, booms, transitions, textures.\n- **Split** (`aurora_split`) — ANY asset → 7 stems (vocals, kick, snare, toms, hats, bass, everything-else). REAL MVSEP credits; never re-split (the op refuses if 7 stems exist).\n- **Stack** (`aurora_stack_*`) — layer assets/stems on lanes with offsets and gain, then `aurora_stack_export` for a sample-aligned multi-WAV bundle (drop at time zero in any DAW).\n\n## Long-op discipline\n\nGeneration and splits take minutes. Prefer `background: true` + `aurora_get_job_status` polling every 10-20s:\n\n- Status responses include `streamUrls` while a generation is still cooking — give the user the link, they can LISTEN ~30-45s in, minutes before files land.\n- Split stems land PROGRESSIVELY: vocals/kick/snare/toms/hats/bass appear as each MVSEP job finishes; everything-else (ee) lands last.\n- Jobs survive restarts — `aurora_list_jobs` recovers anything in flight.\n\n## Files + organization\n\n- Project folder: `generations/ covers/ imports/ references/ stems/<asset>/ masters/ stack-export/ stack.json`.\n- MP3 lands first; `aurora_fetch_wav` upgrades a generation/cover to provider WAV (~0.4 credits).\n- `aurora_pitch_shift` and `aurora_convert` are FREE local ffmpeg ops.\n- Mastering (analyze → mix → export) lives in the Aurora app window — point the user there once stems exist; it is not agent-drivable yet.\n\n## Suno prompting quick rules\n\n- Custom mode = set `style` AND `title` together; then `prompt` carries the LYRICS.\n- Non-custom mode: `prompt` is a track description.\n- `negativeTags` is ONE comma-separated string (\"Heavy Metal, Upbeat Drums\").\n- Sounds prompts: concrete and physical (\"huge cinematic braam, dark low brass, trailer hit\"), max 500 chars, lock `soundKey`/`tempo` when the track they'll sit in is known.\n",
|
|
6
6
|
"aurora-split-and-stems": "---\nname: aurora-split-and-stems\ndescription: How Aurora's 7-stem split works (3 MVSEP jobs + phase cancellation), what the stems are, progressive landing, cost rules, and where stems live on disk. Use when calling aurora_split or working with split stems.\n---\n\n# Aurora Split & Stems\n\n## The 7 stems\n\n`vocals, kick, snare, toms, hats, bass, ee` (everything-else). Only 5 come from MVSEP; **hats** and **ee** are synthesized locally by phase cancellation (hats = drums − kick − snare − toms; ee = original − vocals − drums − bass). This is why ee always lands LAST.\n\n## How a split runs\n\n3 parallel MVSEP jobs (vocals model, drum separation, bass model) on one standardized 44.1kHz float32 WAV. Stems land **progressively** as each job finishes:\n\n- vocals job → `vocals`\n- drums job → `kick`, `snare`, `toms`, `hats`\n- bass job → `bass`\n- all three done → `ee`\n\nWith `background: true`, `aurora_get_job_status` shows the per-job landing state — the user can start auditioning early stems while the rest cook. Typical total: 3-5 minutes (longer if the MVSEP queue is busy — free-tier keys run 1 concurrent job, so the 3 jobs may serialize).\n\n## Cost rules\n\n- REAL MVSEP credits, priced by audio duration. Check `aurora_get_credits` (mvsepPremiumMinutes) first.\n- **Never re-split**: the op returns existing stems instead of spending again when a full set exists.\n- Any asset kind splits: generations, covers, imports, AND references (split-a-reference is a first-class loop for studying an arrangement).\n\n## On disk\n\nStems live at `<project>/stems/<asset-slug>-<id6>/*.wav` — 32-bit float, sample-aligned by construction. They are DAW-ready files: stack them (`aurora_stack_add_lane` with `stemType`), pitch them (`aurora_pitch_shift`), rip MIDI from them (`aurora_rip_midi`), or point the user at the folder.\n\nMastering against a reference (analyze → mix → export) happens in the Aurora app window from any split set — not agent-drivable yet.\n",
|
|
7
|
-
"aurora-suno-prompting": "---\nname: aurora-suno-prompting\ndescription: Prompting guide for Aurora's Suno-backed generation ops — generate (full tracks), cover (style transforms,
|
|
7
|
+
"aurora-suno-prompting": "---\nname: aurora-suno-prompting\ndescription: Prompting guide for Aurora's Suno-backed generation ops — generate (ultra-custom full tracks), cover (style transforms + single-layer covers), add_vocals/add_instrumental (layering over existing audio), and sounds (samples/loops with key+tempo lock). Use when writing prompts for aurora_generate, aurora_cover, aurora_add_vocals, aurora_add_instrumental, or aurora_sounds.\n---\n\n# Suno Prompting for Aurora\n\nFull wire-param reference: `docs/suno-param-surface.md` in this repo. Research receipts behind the layering recipes: second-brain `business/projects/aurora-docs/suno-layering-playbook-2026-06.md`.\n\n## The one principle that prevents the classic failure\n\n**Only show Suno the material you want performed.** Covers and audio-conditioned ops re-render EVERYTHING in the reference. Feed a full mix and ask for \"solo choir\" and you get a choir performing the drums. Feed one stem and you get that line re-performed.\n\n## aurora_generate — full tracks (ultra-custom is the default posture)\n\n- **Custom mode** (`customMode: true`, style + title required): `prompt` is the EXACT lyrics, sung as written (≤5000 chars on V4_5+). Use section metatags to steer arrangement: `[Verse]`, `[Chorus]`, `[Choir]`, `[Harmony]`, `[Guitar Solo]`, `[Instrumental]`. This is the default posture — the agent surface exists for full control.\n- **Description mode** (`customMode: false`): `prompt` ≤500 chars describing the track; Suno writes its own lyrics. Use only when the user genuinely wants a surprise.\n- Knobs: `styleWeight` (style adherence), `weirdnessConstraint` (low = predictable, high = surprises), `negativeTags` (ONE comma-separated string — more reliable than \"no X\" inside the style text), `vocalGender`, `personaId`/`personaModel` (consistent vocal character across generations; carries timbre, never melodies).\n- Models: V5/V5_5 have the best prompt + negative-tag adherence on record. V4 caps style at 200 chars and lyrics at 3000.\n\n### Isolated / solo material from scratch (a cappella choir, solo instrument)\n\nPure isolation is a coin flip on every model — stack the odds, expect 2-4 takes, and budget an aurora_split pass on keepers:\n\n1. Style field = ALL voice/instrument descriptors: `epic cinematic choir, a cappella, sacred choral, massed voices, no instruments` (under 200 chars, max 2-3 \"no X\" exclusions).\n2. `negativeTags: \"drums, percussion, orchestra, strings, piano, synthesizer, instruments\"`.\n3. Give the voices a job: Latin or invented syllables in the lyrics with `[Choir]`/`[Harmony]` tags — sung text occupies the slot that otherwise gets filled imitating instruments.\n4. Solo instrument: `instrumental: true` (hard mode, reliable) + single-instrument style + negativeTags for everything else.\n5. Key/BPM in the style text (\"120 BPM, D minor\") is approximate guidance, never a lock. For layering-grade sync, condition on audio instead (below) or conform the take in a DAW.\n\n## aurora_cover — style transforms AND single-layer covers\n\nThe source's musical content is kept; the style is replaced — for the WHOLE input.\n\n- **Whole-track transform** (same song, new genre): `audioWeight` 0.5-0.7.\n- **Single-layer cover** (the layering move — e.g. turn a string melody into a choir line): the reference must be ONLY the line to perform — one stem, a bare MIDI render, even a hummed take. Settings that lock structure while swapping timbre: `audioWeight` 0.7-0.85, `styleWeight` 0.55-0.75, `weirdnessConstraint` 0.2-0.4. Do not upload a dense master and expect surgical obedience. Change one knob at a time between takes.\n- Custom mode rules same as generate (style + title together). Source cap 8 minutes (V4_5ALL: 1 minute).\n\n## aurora_add_vocals — vocals/choir over an existing production\n\nThe designed-for-layering endpoint: upload an instrumental, get vocals performed against its tempo, key, and changes. THE recipe for \"add an epic choir to my finished arrangement\":\n\n1. Feed a SIMPLIFIED bounce — harmonic skeleton + the melody the choir should relate to. Strip drums and dense ornamentation first; dense masters degrade conditioning.\n2. `style: \"epic film choir, massed choral harmonies, latin chant\"`, `negativeTags: \"lead singer, pop vocal, rap, spoken word, autotune\"`, `audioWeight` 0.7-0.85, prompt = Latin/invented syllables with `[Choir]`/`[Harmony]` tags.\n3. The output is a full mix. **Discard Suno's backing**: aurora_split the result, keep ONLY the vocals stem, and lay it over the real production. This makes it irrelevant whether the endpoint preserved or re-rendered the upload.\n\nModels: V4_5PLUS (default) / V5 / V5_5 only.\n\n## aurora_add_instrumental — backing built around an upload\n\nInverse of add_vocals (input usually a vocal or melodic stem; output full mix with new instrumentation). Field name is `tags`, not `style`. Same split-and-keep-the-new-layer closer.\n\n## aurora_sounds — samples, loops, textures\n\nThe layer-manufacturing tool. Prompts are short (max 500 chars), physical, and concrete:\n\n- Name the sound type: braam, boom, riser, downer, whoosh, impact, drone, texture, loop.\n- Describe the material: \"dark low brass\", \"metallic scrape\", \"sub-heavy 808\", \"airy granular pad\".\n- Lock `soundKey` (e.g. \"Cm\", \"F#\") and `tempo` (BPM) when the destination track is known — this is the point of the tool. Sharps only at the wire (no flats).\n- `loop: true` for loopable textures/grooves; `grabLyrics: true` to capture lyric subtitles when the sound has voices.\n\n## When Suno is the wrong tool — say so\n\nFor \"a choir/vocalist singing the EXACT lines the user wrote, in tune with their track,\" no Suno path is note-precise. Recommend MIDI-native singing synths (Synthesizer V choir collections, ACE Studio choir mode) or sample libraries, and keep Suno for texture, surprise, and layer-covers where exact notes don't matter. The hybrid worth offering: write the line as MIDI, render it with anything, and aurora_cover THAT render with choir tags.\n\nEach generation-family call returns 2 variations — audition both before generating more.\n",
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=content.js.map
|
package/dist/storage/assets.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export declare function getAsset(id: string): ProjectAsset | null;
|
|
|
5
5
|
export declare function ensureKindDir(projectId: string, kind: AssetKind): Promise<string>;
|
|
6
6
|
/** Where an asset's split stems land: <project>/stems/<asset-slug>-<shortid>/. */
|
|
7
7
|
export declare function getAssetStemsDir(asset: ProjectAsset): string;
|
|
8
|
+
/** Where an asset's Sample Extractor results land:
|
|
9
|
+
* <project>/extracts/<asset-slug>-<shortid>/. Same naming discipline as stems. */
|
|
10
|
+
export declare function getAssetExtractsDir(asset: ProjectAsset): string;
|
|
8
11
|
/** Non-clobbering destination filename inside a kind dir. */
|
|
9
12
|
export declare function uniqueDestPath(dir: string, fileName: string): string;
|
|
10
13
|
export declare function insertAsset(params: {
|
package/dist/storage/assets.js
CHANGED
|
@@ -47,6 +47,11 @@ export async function ensureKindDir(projectId, kind) {
|
|
|
47
47
|
export function getAssetStemsDir(asset) {
|
|
48
48
|
return join(getProjectDirectory(asset.projectId), 'stems', `${slugify(asset.name, 40)}-${asset.id.slice(0, 6)}`);
|
|
49
49
|
}
|
|
50
|
+
/** Where an asset's Sample Extractor results land:
|
|
51
|
+
* <project>/extracts/<asset-slug>-<shortid>/. Same naming discipline as stems. */
|
|
52
|
+
export function getAssetExtractsDir(asset) {
|
|
53
|
+
return join(getProjectDirectory(asset.projectId), 'extracts', `${slugify(asset.name, 40)}-${asset.id.slice(0, 6)}`);
|
|
54
|
+
}
|
|
50
55
|
/** Non-clobbering destination filename inside a kind dir. */
|
|
51
56
|
export function uniqueDestPath(dir, fileName) {
|
|
52
57
|
const ext = extname(fileName);
|
|
@@ -93,9 +98,11 @@ export async function deleteAsset(id) {
|
|
|
93
98
|
return;
|
|
94
99
|
const db = getDb();
|
|
95
100
|
db.prepare('DELETE FROM project_stems WHERE asset_id = ?').run(id);
|
|
101
|
+
db.prepare('DELETE FROM extraction_stems WHERE asset_id = ?').run(id);
|
|
96
102
|
db.prepare('DELETE FROM project_assets WHERE id = ?').run(id);
|
|
97
103
|
await rm(asset.path, { force: true }).catch(() => { });
|
|
98
104
|
await rm(getAssetStemsDir(asset), { recursive: true, force: true }).catch(() => { });
|
|
105
|
+
await rm(getAssetExtractsDir(asset), { recursive: true, force: true }).catch(() => { });
|
|
99
106
|
if (asset.refId)
|
|
100
107
|
await deleteReference(asset.refId).catch(() => { });
|
|
101
108
|
touchProject(asset.projectId);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ExtractionStem } from '../types.js';
|
|
2
|
+
export declare function getExtractionStems(assetId: string): ExtractionStem[];
|
|
3
|
+
export declare function getProjectExtractionStems(projectId: string): ExtractionStem[];
|
|
4
|
+
export declare function upsertExtractionStem(params: {
|
|
5
|
+
projectId: string;
|
|
6
|
+
assetId: string;
|
|
7
|
+
stemId: string;
|
|
8
|
+
path: string;
|
|
9
|
+
detectedKey: string | null;
|
|
10
|
+
}): ExtractionStem;
|
|
11
|
+
export declare function deleteExtractionStemsForAsset(assetId: string): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { getDb } from '../db.js';
|
|
3
|
+
function rowToStem(row) {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
projectId: row.project_id,
|
|
7
|
+
assetId: row.asset_id,
|
|
8
|
+
stemId: row.stem_id,
|
|
9
|
+
path: row.path,
|
|
10
|
+
detectedKey: row.detected_key,
|
|
11
|
+
createdAt: row.created_at
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function getExtractionStems(assetId) {
|
|
15
|
+
const rows = getDb()
|
|
16
|
+
.prepare('SELECT * FROM extraction_stems WHERE asset_id = ? ORDER BY stem_id')
|
|
17
|
+
.all(assetId);
|
|
18
|
+
return rows.map(rowToStem);
|
|
19
|
+
}
|
|
20
|
+
export function getProjectExtractionStems(projectId) {
|
|
21
|
+
const rows = getDb()
|
|
22
|
+
.prepare('SELECT * FROM extraction_stems WHERE project_id = ? ORDER BY asset_id, stem_id')
|
|
23
|
+
.all(projectId);
|
|
24
|
+
return rows.map(rowToStem);
|
|
25
|
+
}
|
|
26
|
+
export function upsertExtractionStem(params) {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
const id = randomUUID();
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
db.prepare(`INSERT INTO extraction_stems (id, project_id, asset_id, stem_id, path, detected_key, created_at)
|
|
31
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
32
|
+
ON CONFLICT(asset_id, stem_id)
|
|
33
|
+
DO UPDATE SET path = excluded.path, detected_key = excluded.detected_key, created_at = excluded.created_at`).run(id, params.projectId, params.assetId, params.stemId, params.path, params.detectedKey, now);
|
|
34
|
+
const row = db
|
|
35
|
+
.prepare('SELECT * FROM extraction_stems WHERE asset_id = ? AND stem_id = ?')
|
|
36
|
+
.get(params.assetId, params.stemId);
|
|
37
|
+
return rowToStem(row);
|
|
38
|
+
}
|
|
39
|
+
export function deleteExtractionStemsForAsset(assetId) {
|
|
40
|
+
getDb().prepare('DELETE FROM extraction_stems WHERE asset_id = ?').run(assetId);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=extractions.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -30,6 +30,18 @@ export interface ProjectStem {
|
|
|
30
30
|
path: string;
|
|
31
31
|
origin: 'mvsep' | 'synthesized';
|
|
32
32
|
}
|
|
33
|
+
/** A Sample Extractor result stem (schema v2 extraction_stems). stemId is a
|
|
34
|
+
* catalog id from extract-catalog.ts (piano / vocal_lead / drum_kick / ee…). */
|
|
35
|
+
export interface ExtractionStem {
|
|
36
|
+
id: string;
|
|
37
|
+
projectId: string;
|
|
38
|
+
assetId: string;
|
|
39
|
+
stemId: string;
|
|
40
|
+
path: string;
|
|
41
|
+
/** Krumhansl-Schmuckler result, e.g. "C major / A minor" (null = not detected). */
|
|
42
|
+
detectedKey: string | null;
|
|
43
|
+
createdAt: number;
|
|
44
|
+
}
|
|
33
45
|
export interface ReferenceTrack {
|
|
34
46
|
id: string;
|
|
35
47
|
name: string;
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Aurora domain types — mirrored from aurora/src/shared/types/index.ts (the
|
|
2
2
|
// locked contract). The MCP works against the SAME DB + project folders as the
|
|
3
|
-
// app, so these shapes must stay in lockstep with the app's schema
|
|
3
|
+
// app, so these shapes must stay in lockstep with the app's schema v2.
|
|
4
4
|
export const STEM_TYPES = ['vocals', 'kick', 'snare', 'toms', 'hats', 'bass', 'ee'];
|
|
5
5
|
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ericdisero/aurora-shared",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shared operations layer for the Aurora MCP server and CLI: storage, Suno/MVSEP provider clients, background jobs, and the single tool surface both consume. Most users want @ericdisero/aurora-mcp-server or @ericdisero/aurora-cli instead.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aurora-cost-discipline
|
|
3
|
-
description: Credit and spend discipline for every paid Aurora operation (Suno generation/cover/sounds/WAV, MVSEP splits). Fires before any aurora_generate, aurora_cover, aurora_sounds, aurora_split, or aurora_fetch_wav call.
|
|
3
|
+
description: Credit and spend discipline for every paid Aurora operation (Suno generation/cover/sounds/layering/WAV, MVSEP splits and extractions). Fires before any aurora_generate, aurora_cover, aurora_add_vocals, aurora_add_instrumental, aurora_sounds, aurora_split, aurora_extract, or aurora_fetch_wav call.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Aurora Cost Discipline
|
|
@@ -21,7 +21,9 @@ Two metered providers sit behind Aurora's cloud ops. Spend is real money. The ru
|
|
|
21
21
|
| `aurora_cover` | ~12 credits + ~0.4 per WAV fetch |
|
|
22
22
|
| `aurora_fetch_wav` | ~0.4 credits per conversion |
|
|
23
23
|
| `aurora_generate` | not yet measured — check credits before/after and report the delta |
|
|
24
|
-
| `
|
|
24
|
+
| `aurora_add_vocals` / `aurora_add_instrumental` | not yet measured — same generation family; check the delta and report it |
|
|
25
|
+
| `aurora_split` | MVSEP credits, priced by audio duration (separate balance); ALWAYS 3 MVSEP calls |
|
|
26
|
+
| `aurora_extract` | MVSEP credits, VARIABLE by selection — the op's response includes the call plan; bundles count once however many of their stems you pick. Read the estimate before confirming a big catalog run |
|
|
25
27
|
| `aurora_get_credits`, all local ffmpeg/stack/project ops | FREE |
|
|
26
28
|
|
|
27
29
|
## Cheap-first ladder
|
|
@@ -1,35 +1,66 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aurora-suno-prompting
|
|
3
|
-
description: Prompting guide for Aurora's Suno-backed generation ops — generate (full tracks), cover (style transforms,
|
|
3
|
+
description: Prompting guide for Aurora's Suno-backed generation ops — generate (ultra-custom full tracks), cover (style transforms + single-layer covers), add_vocals/add_instrumental (layering over existing audio), and sounds (samples/loops with key+tempo lock). Use when writing prompts for aurora_generate, aurora_cover, aurora_add_vocals, aurora_add_instrumental, or aurora_sounds.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Suno Prompting for Aurora
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Full wire-param reference: `docs/suno-param-surface.md` in this repo. Research receipts behind the layering recipes: second-brain `business/projects/aurora-docs/suno-layering-playbook-2026-06.md`.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## The one principle that prevents the classic failure
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
- **Custom mode** (`style` AND `title` set): `prompt` carries the LYRICS; `style` carries the genre/production language. The provider requires BOTH style and title together.
|
|
12
|
+
**Only show Suno the material you want performed.** Covers and audio-conditioned ops re-render EVERYTHING in the reference. Feed a full mix and ask for "solo choir" and you get a choir performing the drums. Feed one stem and you get that line re-performed.
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
## aurora_generate — full tracks (ultra-custom is the default posture)
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
- **Custom mode** (`customMode: true`, style + title required): `prompt` is the EXACT lyrics, sung as written (≤5000 chars on V4_5+). Use section metatags to steer arrangement: `[Verse]`, `[Chorus]`, `[Choir]`, `[Harmony]`, `[Guitar Solo]`, `[Instrumental]`. This is the default posture — the agent surface exists for full control.
|
|
17
|
+
- **Description mode** (`customMode: false`): `prompt` ≤500 chars describing the track; Suno writes its own lyrics. Use only when the user genuinely wants a surprise.
|
|
18
|
+
- Knobs: `styleWeight` (style adherence), `weirdnessConstraint` (low = predictable, high = surprises), `negativeTags` (ONE comma-separated string — more reliable than "no X" inside the style text), `vocalGender`, `personaId`/`personaModel` (consistent vocal character across generations; carries timbre, never melodies).
|
|
19
|
+
- Models: V5/V5_5 have the best prompt + negative-tag adherence on record. V4 caps style at 200 chars and lyrics at 3000.
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
### Isolated / solo material from scratch (a cappella choir, solo instrument)
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
- Custom mode rules are the same: `style` + `title` together, prompt describes the transformation target.
|
|
23
|
-
- Source cap: 8 minutes. Project asset (`sourceAssetId`) or external file (`sourcePath`).
|
|
23
|
+
Pure isolation is a coin flip on every model — stack the odds, expect 2-4 takes, and budget an aurora_split pass on keepers:
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
1. Style field = ALL voice/instrument descriptors: `epic cinematic choir, a cappella, sacred choral, massed voices, no instruments` (under 200 chars, max 2-3 "no X" exclusions).
|
|
26
|
+
2. `negativeTags: "drums, percussion, orchestra, strings, piano, synthesizer, instruments"`.
|
|
27
|
+
3. Give the voices a job: Latin or invented syllables in the lyrics with `[Choir]`/`[Harmony]` tags — sung text occupies the slot that otherwise gets filled imitating instruments.
|
|
28
|
+
4. Solo instrument: `instrumental: true` (hard mode, reliable) + single-instrument style + negativeTags for everything else.
|
|
29
|
+
5. Key/BPM in the style text ("120 BPM, D minor") is approximate guidance, never a lock. For layering-grade sync, condition on audio instead (below) or conform the take in a DAW.
|
|
30
|
+
|
|
31
|
+
## aurora_cover — style transforms AND single-layer covers
|
|
32
|
+
|
|
33
|
+
The source's musical content is kept; the style is replaced — for the WHOLE input.
|
|
34
|
+
|
|
35
|
+
- **Whole-track transform** (same song, new genre): `audioWeight` 0.5-0.7.
|
|
36
|
+
- **Single-layer cover** (the layering move — e.g. turn a string melody into a choir line): the reference must be ONLY the line to perform — one stem, a bare MIDI render, even a hummed take. Settings that lock structure while swapping timbre: `audioWeight` 0.7-0.85, `styleWeight` 0.55-0.75, `weirdnessConstraint` 0.2-0.4. Do not upload a dense master and expect surgical obedience. Change one knob at a time between takes.
|
|
37
|
+
- Custom mode rules same as generate (style + title together). Source cap 8 minutes (V4_5ALL: 1 minute).
|
|
38
|
+
|
|
39
|
+
## aurora_add_vocals — vocals/choir over an existing production
|
|
40
|
+
|
|
41
|
+
The designed-for-layering endpoint: upload an instrumental, get vocals performed against its tempo, key, and changes. THE recipe for "add an epic choir to my finished arrangement":
|
|
42
|
+
|
|
43
|
+
1. Feed a SIMPLIFIED bounce — harmonic skeleton + the melody the choir should relate to. Strip drums and dense ornamentation first; dense masters degrade conditioning.
|
|
44
|
+
2. `style: "epic film choir, massed choral harmonies, latin chant"`, `negativeTags: "lead singer, pop vocal, rap, spoken word, autotune"`, `audioWeight` 0.7-0.85, prompt = Latin/invented syllables with `[Choir]`/`[Harmony]` tags.
|
|
45
|
+
3. The output is a full mix. **Discard Suno's backing**: aurora_split the result, keep ONLY the vocals stem, and lay it over the real production. This makes it irrelevant whether the endpoint preserved or re-rendered the upload.
|
|
46
|
+
|
|
47
|
+
Models: V4_5PLUS (default) / V5 / V5_5 only.
|
|
48
|
+
|
|
49
|
+
## aurora_add_instrumental — backing built around an upload
|
|
50
|
+
|
|
51
|
+
Inverse of add_vocals (input usually a vocal or melodic stem; output full mix with new instrumentation). Field name is `tags`, not `style`. Same split-and-keep-the-new-layer closer.
|
|
52
|
+
|
|
53
|
+
## aurora_sounds — samples, loops, textures
|
|
26
54
|
|
|
27
55
|
The layer-manufacturing tool. Prompts are short (max 500 chars), physical, and concrete:
|
|
28
56
|
|
|
29
57
|
- Name the sound type: braam, boom, riser, downer, whoosh, impact, drone, texture, loop.
|
|
30
58
|
- Describe the material: "dark low brass", "metallic scrape", "sub-heavy 808", "airy granular pad".
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
59
|
+
- Lock `soundKey` (e.g. "Cm", "F#") and `tempo` (BPM) when the destination track is known — this is the point of the tool. Sharps only at the wire (no flats).
|
|
60
|
+
- `loop: true` for loopable textures/grooves; `grabLyrics: true` to capture lyric subtitles when the sound has voices.
|
|
61
|
+
|
|
62
|
+
## When Suno is the wrong tool — say so
|
|
63
|
+
|
|
64
|
+
For "a choir/vocalist singing the EXACT lines the user wrote, in tune with their track," no Suno path is note-precise. Recommend MIDI-native singing synths (Synthesizer V choir collections, ACE Studio choir mode) or sample libraries, and keep Suno for texture, surprise, and layer-covers where exact notes don't matter. The hybrid worth offering: write the line as MIDI, render it with anything, and aurora_cover THAT render with choir tags.
|
|
34
65
|
|
|
35
|
-
Each call returns 2 variations — audition both before generating more.
|
|
66
|
+
Each generation-family call returns 2 variations — audition both before generating more.
|