@ericdisero/aurora-shared 0.1.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/README.md +9 -0
- package/dist/audio/ffmpeg.d.ts +21 -0
- package/dist/audio/ffmpeg.js +112 -0
- package/dist/audio/wav.d.ts +15 -0
- package/dist/audio/wav.js +159 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +50 -0
- package/dist/db.d.ts +3 -0
- package/dist/db.js +121 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/jobs.d.ts +45 -0
- package/dist/jobs.js +220 -0
- package/dist/operations/index.d.ts +12 -0
- package/dist/operations/index.js +848 -0
- package/dist/paths.d.ts +17 -0
- package/dist/paths.js +79 -0
- package/dist/providers/mvsep.d.ts +27 -0
- package/dist/providers/mvsep.js +112 -0
- package/dist/providers/suno.d.ts +89 -0
- package/dist/providers/suno.js +309 -0
- package/dist/sidecars.d.ts +20 -0
- package/dist/sidecars.js +109 -0
- package/dist/skills/content.d.ts +1 -0
- package/dist/skills/content.js +9 -0
- package/dist/split.d.ts +24 -0
- package/dist/split.js +162 -0
- package/dist/stack.d.ts +19 -0
- package/dist/stack.js +139 -0
- package/dist/storage/assets.d.ts +30 -0
- package/dist/storage/assets.js +103 -0
- package/dist/storage/projects.d.ts +12 -0
- package/dist/storage/projects.js +85 -0
- package/dist/storage/references.d.ts +10 -0
- package/dist/storage/references.js +54 -0
- package/dist/storage/stems.d.ts +13 -0
- package/dist/storage/stems.js +41 -0
- package/dist/types.d.ts +72 -0
- package/dist/types.js +5 -0
- package/package.json +51 -0
- package/skills/aurora-cost-discipline.md +31 -0
- package/skills/aurora-music-production.md +43 -0
- package/skills/aurora-split-and-stems.md +33 -0
- package/skills/aurora-suno-prompting.md +35 -0
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AppSettings } from './types.js';
|
|
2
|
+
/** Aurora's userData directory. AURORA_USER_DATA overrides (also how the test
|
|
3
|
+
* harness isolates); otherwise the per-OS Electron location, preferring the
|
|
4
|
+
* casing variant that already holds aurora.db. */
|
|
5
|
+
export declare function getUserDataDir(): string;
|
|
6
|
+
export declare function getDbPath(): string;
|
|
7
|
+
/** Read the app's settings.json, merging saved values over defaults (the app's
|
|
8
|
+
* own semantics — missing keys fall to default). Read-only here: the MCP never
|
|
9
|
+
* writes settings. */
|
|
10
|
+
export declare function getSettings(): AppSettings;
|
|
11
|
+
/** Active projects root — the custom setting if set, else userData/projects. */
|
|
12
|
+
export declare function getProjectsDirectory(): string;
|
|
13
|
+
/** Global reference library dir (userData/references/<refId>/ holds the copied
|
|
14
|
+
* audio + cached curve files). */
|
|
15
|
+
export declare function getReferencesDir(): string;
|
|
16
|
+
/** Where MCP background-job manifests live. */
|
|
17
|
+
export declare function getJobsDir(): string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Aurora userData + project-root resolution WITHOUT Electron. The desktop app
|
|
2
|
+
// resolves these via app.getPath('userData'); we hardcode-resolve the same
|
|
3
|
+
// locations per OS (the handoff plan's blessed approach). The app's Electron
|
|
4
|
+
// name is `aurora` (package.json name) in dev and productName `Aurora` when
|
|
5
|
+
// packaged — same directory on the case-insensitive default filesystems of
|
|
6
|
+
// Windows/macOS; on Linux we probe both casings and prefer the one holding
|
|
7
|
+
// aurora.db.
|
|
8
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
const SETTINGS_DEFAULTS = {
|
|
12
|
+
projectsDirectory: '',
|
|
13
|
+
outputDirectory: '',
|
|
14
|
+
defaultGenModel: 'V5',
|
|
15
|
+
defaultSmoothing: 0.5,
|
|
16
|
+
defaultBitDepth: 24
|
|
17
|
+
};
|
|
18
|
+
function userDataCandidates() {
|
|
19
|
+
const home = homedir();
|
|
20
|
+
if (process.platform === 'win32') {
|
|
21
|
+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
22
|
+
return [join(appData, 'aurora'), join(appData, 'Aurora')];
|
|
23
|
+
}
|
|
24
|
+
if (process.platform === 'darwin') {
|
|
25
|
+
const base = join(home, 'Library', 'Application Support');
|
|
26
|
+
return [join(base, 'aurora'), join(base, 'Aurora')];
|
|
27
|
+
}
|
|
28
|
+
const base = process.env.XDG_CONFIG_HOME || join(home, '.config');
|
|
29
|
+
return [join(base, 'aurora'), join(base, 'Aurora')];
|
|
30
|
+
}
|
|
31
|
+
/** Aurora's userData directory. AURORA_USER_DATA overrides (also how the test
|
|
32
|
+
* harness isolates); otherwise the per-OS Electron location, preferring the
|
|
33
|
+
* casing variant that already holds aurora.db. */
|
|
34
|
+
export function getUserDataDir() {
|
|
35
|
+
if (process.env.AURORA_USER_DATA)
|
|
36
|
+
return process.env.AURORA_USER_DATA;
|
|
37
|
+
const candidates = userDataCandidates();
|
|
38
|
+
for (const c of candidates) {
|
|
39
|
+
if (existsSync(join(c, 'aurora.db')))
|
|
40
|
+
return c;
|
|
41
|
+
}
|
|
42
|
+
for (const c of candidates) {
|
|
43
|
+
if (existsSync(c))
|
|
44
|
+
return c;
|
|
45
|
+
}
|
|
46
|
+
return candidates[0];
|
|
47
|
+
}
|
|
48
|
+
export function getDbPath() {
|
|
49
|
+
return join(getUserDataDir(), 'aurora.db');
|
|
50
|
+
}
|
|
51
|
+
/** Read the app's settings.json, merging saved values over defaults (the app's
|
|
52
|
+
* own semantics — missing keys fall to default). Read-only here: the MCP never
|
|
53
|
+
* writes settings. */
|
|
54
|
+
export function getSettings() {
|
|
55
|
+
const path = join(getUserDataDir(), 'settings.json');
|
|
56
|
+
if (!existsSync(path))
|
|
57
|
+
return { ...SETTINGS_DEFAULTS };
|
|
58
|
+
try {
|
|
59
|
+
const saved = JSON.parse(readFileSync(path, 'utf-8'));
|
|
60
|
+
return { ...SETTINGS_DEFAULTS, ...saved };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return { ...SETTINGS_DEFAULTS };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Active projects root — the custom setting if set, else userData/projects. */
|
|
67
|
+
export function getProjectsDirectory() {
|
|
68
|
+
return getSettings().projectsDirectory || join(getUserDataDir(), 'projects');
|
|
69
|
+
}
|
|
70
|
+
/** Global reference library dir (userData/references/<refId>/ holds the copied
|
|
71
|
+
* audio + cached curve files). */
|
|
72
|
+
export function getReferencesDir() {
|
|
73
|
+
return join(getUserDataDir(), 'references');
|
|
74
|
+
}
|
|
75
|
+
/** Where MCP background-job manifests live. */
|
|
76
|
+
export function getJobsDir() {
|
|
77
|
+
return join(getUserDataDir(), 'agent-jobs');
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MvsepJobSpec, SeparationResultFile } from '../types.js';
|
|
2
|
+
export declare const MVSEP_BASE_URL = "https://mvsep.com";
|
|
3
|
+
/** Submit one separation job (multipart, field MUST be "audiofile"). */
|
|
4
|
+
export declare function createSeparationJob(audio: Buffer, spec: MvsepJobSpec): Promise<{
|
|
5
|
+
hash: string;
|
|
6
|
+
}>;
|
|
7
|
+
export interface SeparationStatus {
|
|
8
|
+
/** waiting | processing | distributing | merging | done | failed | not_found */
|
|
9
|
+
status: string;
|
|
10
|
+
files?: SeparationResultFile[];
|
|
11
|
+
message?: string;
|
|
12
|
+
}
|
|
13
|
+
/** ONE status fetch (no waiting) — the background-job model's poll unit. */
|
|
14
|
+
export declare function fetchSeparationStatus(hash: string): Promise<SeparationStatus>;
|
|
15
|
+
/** Interpret a status: returns files when done, null while in flight, throws on
|
|
16
|
+
* terminal failure. */
|
|
17
|
+
export declare function resolveSeparationStatus(hash: string, s: SeparationStatus): SeparationResultFile[] | null;
|
|
18
|
+
/** Blocking poll until the job is done; returns the result files. */
|
|
19
|
+
export declare function awaitSeparationResult(handle: {
|
|
20
|
+
hash: string;
|
|
21
|
+
}, onPoll?: (status: string) => void): Promise<SeparationResultFile[]>;
|
|
22
|
+
export interface MvsepUserInfo {
|
|
23
|
+
premiumMinutes: number | null;
|
|
24
|
+
premiumEnabled: boolean | null;
|
|
25
|
+
}
|
|
26
|
+
/** Free balance call — premium minutes + premium flag from /api/app/user. */
|
|
27
|
+
export declare function getMvsepUserInfo(): Promise<MvsepUserInfo>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// MVSEP client — port of aurora's verified implementation (src/main/providers/
|
|
2
|
+
// separation/dev-direct.ts), re-verified against live mvsep.com docs +
|
|
3
|
+
// GET /api/app/algorithms on 2026-06-10. Adds a single-shot status fetch for
|
|
4
|
+
// the background-job model and the free user-info balance call.
|
|
5
|
+
// Contract: aurora/docs/build-specs/mvsep-separation-contract.md.
|
|
6
|
+
import { requireMvsepKey } from '../config.js';
|
|
7
|
+
export const MVSEP_BASE_URL = 'https://mvsep.com';
|
|
8
|
+
const CREATE_URL = `${MVSEP_BASE_URL}/api/separation/create`;
|
|
9
|
+
const GET_URL = `${MVSEP_BASE_URL}/api/separation/get`;
|
|
10
|
+
const USER_URL = `${MVSEP_BASE_URL}/api/app/user`;
|
|
11
|
+
const MAX_RETRIES = 5;
|
|
12
|
+
const BASE_RETRY_DELAY_MS = 3000; // exp backoff: 3 * 2**(r-1) seconds
|
|
13
|
+
const POLL_DELAY_MS = 5000;
|
|
14
|
+
const MAX_POLL_ATTEMPTS = 120; // ~10 min ceiling
|
|
15
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
16
|
+
/** Submit one separation job (multipart, field MUST be "audiofile"). */
|
|
17
|
+
export async function createSeparationJob(audio, spec) {
|
|
18
|
+
let lastError;
|
|
19
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
const form = new FormData();
|
|
22
|
+
form.append('api_token', requireMvsepKey());
|
|
23
|
+
form.append('sep_type', spec.sep_type);
|
|
24
|
+
form.append('output_format', spec.output_format);
|
|
25
|
+
form.append('is_demo', spec.is_demo);
|
|
26
|
+
if (spec.add_opt1 != null)
|
|
27
|
+
form.append('add_opt1', spec.add_opt1);
|
|
28
|
+
if (spec.add_opt2 != null)
|
|
29
|
+
form.append('add_opt2', spec.add_opt2);
|
|
30
|
+
const blob = new Blob([new Uint8Array(audio)], { type: 'audio/wav' });
|
|
31
|
+
form.append('audiofile', blob, 'input.wav');
|
|
32
|
+
const res = await fetch(CREATE_URL, { method: 'POST', body: form });
|
|
33
|
+
const body = (await res.json());
|
|
34
|
+
if (!res.ok || !body.success || !body.data?.hash) {
|
|
35
|
+
throw new Error(`MVSEP create failed (HTTP ${res.status}): ${body.data?.message || 'unknown error'}`);
|
|
36
|
+
}
|
|
37
|
+
return { hash: body.data.hash };
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
lastError = err;
|
|
41
|
+
if (attempt < MAX_RETRIES) {
|
|
42
|
+
await sleep(BASE_RETRY_DELAY_MS * 2 ** (attempt - 1));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`MVSEP create failed after ${MAX_RETRIES} attempts: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
47
|
+
}
|
|
48
|
+
/** ONE status fetch (no waiting) — the background-job model's poll unit. */
|
|
49
|
+
export async function fetchSeparationStatus(hash) {
|
|
50
|
+
const res = await fetch(`${GET_URL}?hash=${encodeURIComponent(hash)}`);
|
|
51
|
+
const body = (await res.json());
|
|
52
|
+
const status = body.status ?? 'unknown';
|
|
53
|
+
return {
|
|
54
|
+
status,
|
|
55
|
+
files: body.data?.files?.map((f) => ({ url: f.url, filename: f.download })),
|
|
56
|
+
message: body.data?.message
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Interpret a status: returns files when done, null while in flight, throws on
|
|
60
|
+
* terminal failure. */
|
|
61
|
+
export function resolveSeparationStatus(hash, s) {
|
|
62
|
+
switch (s.status) {
|
|
63
|
+
case 'done':
|
|
64
|
+
if (!s.files || s.files.length === 0) {
|
|
65
|
+
throw new Error(`MVSEP job ${hash} is done but returned no result files — results are deleted server-side ` +
|
|
66
|
+
'after a retention window; the job likely expired before download. Re-run the split.');
|
|
67
|
+
}
|
|
68
|
+
return s.files;
|
|
69
|
+
case 'failed':
|
|
70
|
+
throw new Error(`MVSEP job failed: ${s.message || 'no detail'}`);
|
|
71
|
+
case 'not_found':
|
|
72
|
+
throw new Error(`MVSEP job not found (hash invalid, expired, or result already deleted): ${hash}`);
|
|
73
|
+
case 'waiting':
|
|
74
|
+
case 'processing':
|
|
75
|
+
case 'distributing':
|
|
76
|
+
case 'merging':
|
|
77
|
+
return null;
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`MVSEP unknown job status: ${s.status}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Blocking poll until the job is done; returns the result files. */
|
|
83
|
+
export async function awaitSeparationResult(handle, onPoll) {
|
|
84
|
+
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
85
|
+
await sleep(POLL_DELAY_MS);
|
|
86
|
+
let s;
|
|
87
|
+
try {
|
|
88
|
+
s = await fetchSeparationStatus(handle.hash);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
continue; // transient network blip — keep polling
|
|
92
|
+
}
|
|
93
|
+
onPoll?.(s.status);
|
|
94
|
+
const files = resolveSeparationStatus(handle.hash, s);
|
|
95
|
+
if (files)
|
|
96
|
+
return files;
|
|
97
|
+
}
|
|
98
|
+
throw new Error('MVSEP job timed out (exceeded ~10 min poll ceiling)');
|
|
99
|
+
}
|
|
100
|
+
/** Free balance call — premium minutes + premium flag from /api/app/user. */
|
|
101
|
+
export async function getMvsepUserInfo() {
|
|
102
|
+
const res = await fetch(`${USER_URL}?api_token=${encodeURIComponent(requireMvsepKey())}`);
|
|
103
|
+
const body = (await res.json());
|
|
104
|
+
if (!res.ok || body.success === false) {
|
|
105
|
+
throw new Error(`MVSEP user info failed (HTTP ${res.status})`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
premiumMinutes: body.data?.premium_minutes ?? null,
|
|
109
|
+
premiumEnabled: body.data?.premium_enabled != null ? body.data.premium_enabled === 1 : null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=mvsep.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export declare const SUNOAPI_BASE_URL = "https://api.sunoapi.org";
|
|
2
|
+
export declare const KIE_BASE_URL = "https://api.kie.ai";
|
|
3
|
+
export interface SunoProvider {
|
|
4
|
+
label: string;
|
|
5
|
+
baseUrl: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
uploadStreamUrls: string[];
|
|
8
|
+
}
|
|
9
|
+
/** Resolve the active provider. Base URL: override → sunoapi.org when its key
|
|
10
|
+
* is set → kie.ai. Key: SUNO_API_KEY falling back to KIE_API_KEY. */
|
|
11
|
+
export declare function getProvider(): SunoProvider;
|
|
12
|
+
export declare const host: () => string;
|
|
13
|
+
export interface GenerationParams {
|
|
14
|
+
prompt: string;
|
|
15
|
+
style?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
instrumental: boolean;
|
|
18
|
+
customMode: boolean;
|
|
19
|
+
model: string;
|
|
20
|
+
vocalGender?: 'male' | 'female';
|
|
21
|
+
/** Comma-separated styles to exclude (the docs type this as ONE string). */
|
|
22
|
+
negativeTags?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Submit a generation task. Returns the provider taskId. */
|
|
25
|
+
export declare function createGeneration(params: GenerationParams): Promise<string>;
|
|
26
|
+
export interface SoundsParams {
|
|
27
|
+
/** Max 500 chars per docs.sunoapi.org/suno-api/generate-sounds. */
|
|
28
|
+
prompt: string;
|
|
29
|
+
/** Pitch lock, e.g. 'C', 'Cm', 'F#'. Default 'Any'. */
|
|
30
|
+
soundKey?: string;
|
|
31
|
+
/** BPM 1-300; omit for auto. */
|
|
32
|
+
soundTempo?: number;
|
|
33
|
+
soundLoop?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Submit a Sounds Generation task. sunoapi.org ONLY — kie.ai does not expose
|
|
36
|
+
* the endpoint. Model locked to V5 by the docs. */
|
|
37
|
+
export declare function createSoundsGeneration(params: SoundsParams): Promise<string>;
|
|
38
|
+
/** Upload a local audio file via the provider's File Upload API. Returns the
|
|
39
|
+
* hosted URL for use as a cover reference (auto-deletes after ~3 days). */
|
|
40
|
+
export declare function uploadAudioFile(filePath: string): Promise<string>;
|
|
41
|
+
export interface CoverParams {
|
|
42
|
+
/** Hosted reference-audio URL from uploadAudioFile. */
|
|
43
|
+
uploadUrl: string;
|
|
44
|
+
prompt: string;
|
|
45
|
+
style?: string;
|
|
46
|
+
title?: string;
|
|
47
|
+
instrumental: boolean;
|
|
48
|
+
customMode: boolean;
|
|
49
|
+
model: string;
|
|
50
|
+
vocalGender?: 'male' | 'female';
|
|
51
|
+
negativeTags?: string;
|
|
52
|
+
/** 0..1 — how strongly the output hews to the input audio. */
|
|
53
|
+
audioWeight?: number;
|
|
54
|
+
}
|
|
55
|
+
/** Submit an upload-and-cover (style transform) task. Poll the returned taskId
|
|
56
|
+
* with the generation record fetchers (same record-info endpoint). */
|
|
57
|
+
export declare function createCover(params: CoverParams): Promise<string>;
|
|
58
|
+
export interface PolledVariation {
|
|
59
|
+
/** Per-variation audio id — the WAV-conversion stage needs it. */
|
|
60
|
+
id?: string;
|
|
61
|
+
audioUrl?: string;
|
|
62
|
+
/** Listenable mid-generation, before audioUrl exists. Expires server-side —
|
|
63
|
+
* never persist; use for instant preview only. */
|
|
64
|
+
streamAudioUrl?: string;
|
|
65
|
+
title?: string;
|
|
66
|
+
duration?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface GenerationRecord {
|
|
69
|
+
/** PENDING / TEXT_SUCCESS / FIRST_SUCCESS / SUCCESS / *_FAILED /
|
|
70
|
+
* CALLBACK_EXCEPTION / SENSITIVE_WORD_ERROR */
|
|
71
|
+
status: string;
|
|
72
|
+
variations: PolledVariation[];
|
|
73
|
+
}
|
|
74
|
+
/** ONE record-info fetch (no waiting). The background-job model's poll unit. */
|
|
75
|
+
export declare function fetchGenerationRecord(taskId: string): Promise<GenerationRecord>;
|
|
76
|
+
export declare function isGenerationFailure(status: string): boolean;
|
|
77
|
+
/** Blocking poll until the variations are ready (mirrors the app's pattern,
|
|
78
|
+
* including the CALLBACK_EXCEPTION grace window). */
|
|
79
|
+
export declare function pollGenerationTask(taskId: string, onStatus?: (status: string) => void): Promise<PolledVariation[]>;
|
|
80
|
+
/** Submit a WAV conversion for one variation. Returns the WAV task's own taskId. */
|
|
81
|
+
export declare function createWavConversion(taskId: string, audioId: string): Promise<string>;
|
|
82
|
+
/** Blocking poll of wav/record-info until the WAV URL appears. Tolerant of
|
|
83
|
+
* shape drift (camel/snake, nested/flat). */
|
|
84
|
+
export declare function pollWavConversion(wavTaskId: string): Promise<string>;
|
|
85
|
+
/** Download a URL to disk. Provider URLs expire server-side — always persist
|
|
86
|
+
* immediately. */
|
|
87
|
+
export declare function downloadTo(url: string, destPath: string): Promise<void>;
|
|
88
|
+
/** Remaining credit balance — free call, doubles as the auth smoke test. */
|
|
89
|
+
export declare function getRemainingCredits(): Promise<number>;
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// Suno-provider client — sunoapi.org PRIMARY, kie.ai fallback (schema-identical
|
|
2
|
+
// /api/v1/* surfaces). Merged port of aurora's two proven implementations:
|
|
3
|
+
// src/main/providers/generation/suno-client.ts (app) + tools/bridge/lib/kie.ts
|
|
4
|
+
// (the reference impl, real-call verified 2026-06-10). Adds single-shot record
|
|
5
|
+
// fetchers so the background-job model can poll without blocking, and surfaces
|
|
6
|
+
// streamAudioUrl (listenable mid-generation, before the final file exists).
|
|
7
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { basename, extname } from 'node:path';
|
|
9
|
+
import { getKieKey, getSunoBaseUrlOverride, getSunoKey } from '../config.js';
|
|
10
|
+
export const SUNOAPI_BASE_URL = 'https://api.sunoapi.org';
|
|
11
|
+
export const KIE_BASE_URL = 'https://api.kie.ai';
|
|
12
|
+
/** Resolve the active provider. Base URL: override → sunoapi.org when its key
|
|
13
|
+
* is set → kie.ai. Key: SUNO_API_KEY falling back to KIE_API_KEY. */
|
|
14
|
+
export function getProvider() {
|
|
15
|
+
const sunoKey = getSunoKey();
|
|
16
|
+
const kieKey = getKieKey();
|
|
17
|
+
const apiKey = sunoKey || kieKey;
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
throw new Error('No Suno provider key configured. Set SUNO_API_KEY (api.sunoapi.org, primary) or ' +
|
|
20
|
+
'KIE_API_KEY (api.kie.ai, fallback) in your environment / MCP config env block, ' +
|
|
21
|
+
'or run: aurora keys set --suno-api-key <key>');
|
|
22
|
+
}
|
|
23
|
+
const baseUrl = getSunoBaseUrlOverride() || (sunoKey ? SUNOAPI_BASE_URL : KIE_BASE_URL);
|
|
24
|
+
const isSunoApiOrg = baseUrl.includes('sunoapi.org');
|
|
25
|
+
return {
|
|
26
|
+
label: isSunoApiOrg ? 'sunoapi.org' : new URL(baseUrl).host,
|
|
27
|
+
baseUrl,
|
|
28
|
+
apiKey,
|
|
29
|
+
uploadStreamUrls: isSunoApiOrg
|
|
30
|
+
? ['https://sunoapiorg.redpandaai.co/api/file-stream-upload', `${baseUrl}/api/file-stream-upload`]
|
|
31
|
+
: ['https://kieai.redpandaai.co/api/file-stream-upload', `${baseUrl}/api/file-stream-upload`]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function authHeaders() {
|
|
35
|
+
return {
|
|
36
|
+
'Content-Type': 'application/json',
|
|
37
|
+
Authorization: `Bearer ${getProvider().apiKey}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const api = (path) => `${getProvider().baseUrl}${path}`;
|
|
41
|
+
export const host = () => getProvider().label;
|
|
42
|
+
// Placeholder used only if the provider rejects a create that omits callBackUrl.
|
|
43
|
+
// We never receive this callback — completion is detected by polling.
|
|
44
|
+
const CALLBACK_PLACEHOLDER = 'https://example.com/aurora-mcp-callback';
|
|
45
|
+
const POLL_DELAY_MS = 5000;
|
|
46
|
+
const MAX_POLL_ATTEMPTS = 120; // ~10 min ceiling
|
|
47
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
48
|
+
const wireVocalGender = (v) => (v === 'male' ? 'm' : 'f');
|
|
49
|
+
/** Submit a generation task. Returns the provider taskId. */
|
|
50
|
+
export async function createGeneration(params) {
|
|
51
|
+
const body = {
|
|
52
|
+
prompt: params.prompt,
|
|
53
|
+
customMode: params.customMode,
|
|
54
|
+
instrumental: params.instrumental,
|
|
55
|
+
model: params.model
|
|
56
|
+
};
|
|
57
|
+
if (params.style)
|
|
58
|
+
body.style = params.style;
|
|
59
|
+
if (params.title)
|
|
60
|
+
body.title = params.title;
|
|
61
|
+
if (params.vocalGender)
|
|
62
|
+
body.vocalGender = wireVocalGender(params.vocalGender);
|
|
63
|
+
if (params.negativeTags)
|
|
64
|
+
body.negativeTags = params.negativeTags;
|
|
65
|
+
const res = await fetch(api('/api/v1/generate'), {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: authHeaders(),
|
|
68
|
+
body: JSON.stringify(body)
|
|
69
|
+
});
|
|
70
|
+
const json = (await res.json());
|
|
71
|
+
const taskId = json.data?.taskId;
|
|
72
|
+
if (!res.ok || !taskId) {
|
|
73
|
+
throw new Error(`${host()} generate failed (HTTP ${res.status}): ${json.msg || 'no taskId returned'}`);
|
|
74
|
+
}
|
|
75
|
+
return taskId;
|
|
76
|
+
}
|
|
77
|
+
/** Submit a Sounds Generation task. sunoapi.org ONLY — kie.ai does not expose
|
|
78
|
+
* the endpoint. Model locked to V5 by the docs. */
|
|
79
|
+
export async function createSoundsGeneration(params) {
|
|
80
|
+
const body = { prompt: params.prompt, model: 'V5' };
|
|
81
|
+
if (params.soundKey)
|
|
82
|
+
body.soundKey = params.soundKey;
|
|
83
|
+
if (params.soundTempo !== undefined)
|
|
84
|
+
body.soundTempo = params.soundTempo;
|
|
85
|
+
if (params.soundLoop)
|
|
86
|
+
body.soundLoop = true;
|
|
87
|
+
const res = await fetch(api('/api/v1/generate/sounds'), {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: authHeaders(),
|
|
90
|
+
body: JSON.stringify(body)
|
|
91
|
+
});
|
|
92
|
+
const json = (await res.json());
|
|
93
|
+
const taskId = json.data?.taskId ?? json.data?.task_id;
|
|
94
|
+
if (!res.ok || (json.code !== undefined && json.code !== 200) || !taskId) {
|
|
95
|
+
throw new Error(`${host()} generate/sounds failed (HTTP ${res.status}, code ${json.code ?? 'n/a'}): ${json.msg || 'no taskId returned'}`);
|
|
96
|
+
}
|
|
97
|
+
return taskId;
|
|
98
|
+
}
|
|
99
|
+
// ── Upload + cover (style transform) ────────────────────────────
|
|
100
|
+
const MIME_BY_EXT = {
|
|
101
|
+
'.wav': 'audio/wav',
|
|
102
|
+
'.mp3': 'audio/mpeg',
|
|
103
|
+
'.flac': 'audio/flac',
|
|
104
|
+
'.aiff': 'audio/aiff',
|
|
105
|
+
'.aif': 'audio/aiff'
|
|
106
|
+
};
|
|
107
|
+
const UPLOAD_DIR = 'aurora-mcp';
|
|
108
|
+
/** Upload a local audio file via the provider's File Upload API. Returns the
|
|
109
|
+
* hosted URL for use as a cover reference (auto-deletes after ~3 days). */
|
|
110
|
+
export async function uploadAudioFile(filePath) {
|
|
111
|
+
const { apiKey, uploadStreamUrls } = getProvider();
|
|
112
|
+
const bytes = await readFile(filePath);
|
|
113
|
+
const fileName = basename(filePath);
|
|
114
|
+
const mime = MIME_BY_EXT[extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
115
|
+
let lastError;
|
|
116
|
+
for (const url of uploadStreamUrls) {
|
|
117
|
+
try {
|
|
118
|
+
const form = new FormData();
|
|
119
|
+
form.append('uploadPath', UPLOAD_DIR);
|
|
120
|
+
form.append('fileName', fileName);
|
|
121
|
+
form.append('file', new Blob([new Uint8Array(bytes)], { type: mime }), fileName);
|
|
122
|
+
const res = await fetch(url, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
// No Content-Type — fetch sets the multipart boundary itself.
|
|
125
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
126
|
+
body: form
|
|
127
|
+
});
|
|
128
|
+
const json = (await res.json());
|
|
129
|
+
const hosted = json.data?.downloadUrl ?? json.data?.download_url ?? json.data?.fileUrl ?? json.data?.file_url;
|
|
130
|
+
if (!res.ok || json.success === false || (json.code !== undefined && json.code !== 200) || !hosted) {
|
|
131
|
+
throw new Error(`${host()} file upload failed (HTTP ${res.status}, code ${json.code ?? 'n/a'}): ${json.msg || 'no downloadUrl returned'}`);
|
|
132
|
+
}
|
|
133
|
+
return hosted;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
lastError = err; // try the fallback host
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`${host()} file upload failed on all hosts: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
140
|
+
}
|
|
141
|
+
/** Submit an upload-and-cover (style transform) task. Poll the returned taskId
|
|
142
|
+
* with the generation record fetchers (same record-info endpoint). */
|
|
143
|
+
export async function createCover(params) {
|
|
144
|
+
const attempt = async (withCallback) => {
|
|
145
|
+
const body = {
|
|
146
|
+
uploadUrl: params.uploadUrl,
|
|
147
|
+
prompt: params.prompt,
|
|
148
|
+
customMode: params.customMode,
|
|
149
|
+
instrumental: params.instrumental,
|
|
150
|
+
model: params.model
|
|
151
|
+
};
|
|
152
|
+
if (params.style)
|
|
153
|
+
body.style = params.style;
|
|
154
|
+
if (params.title)
|
|
155
|
+
body.title = params.title;
|
|
156
|
+
if (params.vocalGender)
|
|
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;
|
|
162
|
+
if (withCallback)
|
|
163
|
+
body.callBackUrl = CALLBACK_PLACEHOLDER;
|
|
164
|
+
const res = await fetch(api('/api/v1/generate/upload-cover'), {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: authHeaders(),
|
|
167
|
+
body: JSON.stringify(body)
|
|
168
|
+
});
|
|
169
|
+
const json = (await res.json());
|
|
170
|
+
const taskId = json.data?.taskId ?? json.data?.task_id;
|
|
171
|
+
if (!res.ok || (json.code !== undefined && json.code !== 200) || !taskId) {
|
|
172
|
+
throw new Error(`${host()} upload-cover failed (HTTP ${res.status}, code ${json.code ?? 'n/a'}): ${json.msg || 'no taskId returned'}`);
|
|
173
|
+
}
|
|
174
|
+
return taskId;
|
|
175
|
+
};
|
|
176
|
+
try {
|
|
177
|
+
return await attempt(false);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return attempt(true);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** ONE record-info fetch (no waiting). The background-job model's poll unit. */
|
|
184
|
+
export async function fetchGenerationRecord(taskId) {
|
|
185
|
+
const res = await fetch(`${api('/api/v1/generate/record-info')}?taskId=${encodeURIComponent(taskId)}`, { headers: authHeaders() });
|
|
186
|
+
const info = (await res.json());
|
|
187
|
+
const status = info.data?.status ?? 'PENDING';
|
|
188
|
+
const variations = (info.data?.response?.sunoData ?? []).map((s) => ({
|
|
189
|
+
id: s.id,
|
|
190
|
+
audioUrl: s.audioUrl,
|
|
191
|
+
streamAudioUrl: s.streamAudioUrl,
|
|
192
|
+
title: s.title,
|
|
193
|
+
duration: s.duration
|
|
194
|
+
}));
|
|
195
|
+
return { status, variations };
|
|
196
|
+
}
|
|
197
|
+
export function isGenerationFailure(status) {
|
|
198
|
+
return status.endsWith('FAILED') || status === 'CREATE_TASK_FAILED' || status === 'SENSITIVE_WORD_ERROR';
|
|
199
|
+
}
|
|
200
|
+
/** Blocking poll until the variations are ready (mirrors the app's pattern,
|
|
201
|
+
* including the CALLBACK_EXCEPTION grace window). */
|
|
202
|
+
export async function pollGenerationTask(taskId, onStatus) {
|
|
203
|
+
let terminalWithoutAudio = 0;
|
|
204
|
+
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
205
|
+
await sleep(POLL_DELAY_MS);
|
|
206
|
+
let record;
|
|
207
|
+
try {
|
|
208
|
+
record = await fetchGenerationRecord(taskId);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
continue; // transient — keep polling
|
|
212
|
+
}
|
|
213
|
+
onStatus?.(record.status);
|
|
214
|
+
if (record.status === 'SUCCESS' || record.status === 'CALLBACK_EXCEPTION') {
|
|
215
|
+
const ready = record.variations.filter((v) => v.audioUrl);
|
|
216
|
+
if (ready.length > 0)
|
|
217
|
+
return ready;
|
|
218
|
+
if (record.status === 'SUCCESS') {
|
|
219
|
+
throw new Error(`${host()} reported SUCCESS but returned no audio URLs`);
|
|
220
|
+
}
|
|
221
|
+
terminalWithoutAudio++;
|
|
222
|
+
if (terminalWithoutAudio >= 3) {
|
|
223
|
+
throw new Error(`${host()} reported CALLBACK_EXCEPTION but returned no audio URLs`);
|
|
224
|
+
}
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (isGenerationFailure(record.status)) {
|
|
228
|
+
throw new Error(`${host()} generation failed with status: ${record.status}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
throw new Error(`${host()} generation timed out (exceeded ~10 min poll ceiling)`);
|
|
232
|
+
}
|
|
233
|
+
// ── WAV conversion ──────────────────────────────────────────────
|
|
234
|
+
/** Submit a WAV conversion for one variation. Returns the WAV task's own taskId. */
|
|
235
|
+
export async function createWavConversion(taskId, audioId) {
|
|
236
|
+
const attempt = async (withCallback) => {
|
|
237
|
+
const body = { taskId, audioId };
|
|
238
|
+
if (withCallback)
|
|
239
|
+
body.callBackUrl = CALLBACK_PLACEHOLDER;
|
|
240
|
+
const res = await fetch(api('/api/v1/wav/generate'), {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
headers: authHeaders(),
|
|
243
|
+
body: JSON.stringify(body)
|
|
244
|
+
});
|
|
245
|
+
const json = (await res.json());
|
|
246
|
+
const wavTaskId = json.data?.taskId ?? json.data?.task_id;
|
|
247
|
+
if (!res.ok || (json.code !== undefined && json.code !== 200) || !wavTaskId) {
|
|
248
|
+
throw new Error(`${host()} wav/generate failed (HTTP ${res.status}, code ${json.code ?? 'n/a'}): ${json.msg || 'no taskId returned'}`);
|
|
249
|
+
}
|
|
250
|
+
return wavTaskId;
|
|
251
|
+
};
|
|
252
|
+
try {
|
|
253
|
+
return await attempt(false);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return attempt(true);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Blocking poll of wav/record-info until the WAV URL appears. Tolerant of
|
|
260
|
+
* shape drift (camel/snake, nested/flat). */
|
|
261
|
+
export async function pollWavConversion(wavTaskId) {
|
|
262
|
+
let terminalWithoutUrl = 0;
|
|
263
|
+
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
|
264
|
+
await sleep(POLL_DELAY_MS);
|
|
265
|
+
let json;
|
|
266
|
+
try {
|
|
267
|
+
const res = await fetch(`${api('/api/v1/wav/record-info')}?taskId=${encodeURIComponent(wavTaskId)}`, { headers: authHeaders() });
|
|
268
|
+
json = (await res.json());
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const d = json.data ?? {};
|
|
274
|
+
const url = d.response?.audioWavUrl ?? d.response?.audio_wav_url ?? d.audioWavUrl ?? d.audio_wav_url;
|
|
275
|
+
if (typeof url === 'string' && url.length > 0)
|
|
276
|
+
return url;
|
|
277
|
+
const flag = String(d.successFlag ?? d.status ?? 'PENDING');
|
|
278
|
+
if (flag.endsWith('FAILED')) {
|
|
279
|
+
throw new Error(`${host()} WAV conversion failed (${flag}): ${d.errorMessage || d.errorCode || 'no detail'}`);
|
|
280
|
+
}
|
|
281
|
+
if (flag === 'SUCCESS' || flag === 'CALLBACK_EXCEPTION') {
|
|
282
|
+
terminalWithoutUrl++;
|
|
283
|
+
if (terminalWithoutUrl >= 3) {
|
|
284
|
+
throw new Error(`${host()} WAV task reports ${flag} but no audioWavUrl returned`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
throw new Error(`${host()} WAV conversion timed out (exceeded ~10 min poll ceiling)`);
|
|
289
|
+
}
|
|
290
|
+
// ── Misc ────────────────────────────────────────────────────────
|
|
291
|
+
/** Download a URL to disk. Provider URLs expire server-side — always persist
|
|
292
|
+
* immediately. */
|
|
293
|
+
export async function downloadTo(url, destPath) {
|
|
294
|
+
const res = await fetch(url);
|
|
295
|
+
if (!res.ok)
|
|
296
|
+
throw new Error(`Download failed (HTTP ${res.status}): ${url}`);
|
|
297
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
298
|
+
await writeFile(destPath, buf);
|
|
299
|
+
}
|
|
300
|
+
/** Remaining credit balance — free call, doubles as the auth smoke test. */
|
|
301
|
+
export async function getRemainingCredits() {
|
|
302
|
+
const res = await fetch(api('/api/v1/generate/credit'), { headers: authHeaders() });
|
|
303
|
+
const json = (await res.json());
|
|
304
|
+
if (!res.ok || (json.code !== undefined && json.code !== 200) || typeof json.data !== 'number') {
|
|
305
|
+
throw new Error(`${host()} credit check failed (HTTP ${res.status}, code ${json.code ?? 'n/a'}): ${json.msg || 'no numeric data field'}`);
|
|
306
|
+
}
|
|
307
|
+
return json.data;
|
|
308
|
+
}
|
|
309
|
+
//# sourceMappingURL=suno.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface RvcUpscaleParams {
|
|
2
|
+
/** Input vocal WAV (typically the split's vocals stem). */
|
|
3
|
+
inputPath: string;
|
|
4
|
+
/** Output WAV path. */
|
|
5
|
+
outputPath: string;
|
|
6
|
+
/** Voice model: 'jb' (default) or 'purposeaudacity' (A/B alternate). */
|
|
7
|
+
model?: string;
|
|
8
|
+
/** Optional pitch shift in semitones. */
|
|
9
|
+
f0UpKey?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function runRvcUpscale(params: RvcUpscaleParams): Promise<string>;
|
|
12
|
+
export type MidiMode = 'poly' | 'mono' | 'auto';
|
|
13
|
+
export interface RipMidiParams {
|
|
14
|
+
inputPath: string;
|
|
15
|
+
outputPath: string;
|
|
16
|
+
/** Force a transcription path, or auto-route from `instrument`. */
|
|
17
|
+
mode: MidiMode;
|
|
18
|
+
instrument?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function runRipMidi(params: RipMidiParams): Promise<string>;
|