@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.
Files changed (44) hide show
  1. package/README.md +9 -0
  2. package/dist/audio/ffmpeg.d.ts +21 -0
  3. package/dist/audio/ffmpeg.js +112 -0
  4. package/dist/audio/wav.d.ts +15 -0
  5. package/dist/audio/wav.js +159 -0
  6. package/dist/config.d.ts +14 -0
  7. package/dist/config.js +50 -0
  8. package/dist/db.d.ts +3 -0
  9. package/dist/db.js +121 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.js +8 -0
  12. package/dist/jobs.d.ts +45 -0
  13. package/dist/jobs.js +220 -0
  14. package/dist/operations/index.d.ts +12 -0
  15. package/dist/operations/index.js +848 -0
  16. package/dist/paths.d.ts +17 -0
  17. package/dist/paths.js +79 -0
  18. package/dist/providers/mvsep.d.ts +27 -0
  19. package/dist/providers/mvsep.js +112 -0
  20. package/dist/providers/suno.d.ts +89 -0
  21. package/dist/providers/suno.js +309 -0
  22. package/dist/sidecars.d.ts +20 -0
  23. package/dist/sidecars.js +109 -0
  24. package/dist/skills/content.d.ts +1 -0
  25. package/dist/skills/content.js +9 -0
  26. package/dist/split.d.ts +24 -0
  27. package/dist/split.js +162 -0
  28. package/dist/stack.d.ts +19 -0
  29. package/dist/stack.js +139 -0
  30. package/dist/storage/assets.d.ts +30 -0
  31. package/dist/storage/assets.js +103 -0
  32. package/dist/storage/projects.d.ts +12 -0
  33. package/dist/storage/projects.js +85 -0
  34. package/dist/storage/references.d.ts +10 -0
  35. package/dist/storage/references.js +54 -0
  36. package/dist/storage/stems.d.ts +13 -0
  37. package/dist/storage/stems.js +41 -0
  38. package/dist/types.d.ts +72 -0
  39. package/dist/types.js +5 -0
  40. package/package.json +51 -0
  41. package/skills/aurora-cost-discipline.md +31 -0
  42. package/skills/aurora-music-production.md +43 -0
  43. package/skills/aurora-split-and-stems.md +33 -0
  44. package/skills/aurora-suno-prompting.md +35 -0
@@ -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>;