@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/db.js
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
// out transient writer locks.
|
|
5
5
|
//
|
|
6
6
|
// SCHEMA LOCKSTEP RULE: this file mirrors aurora/src/main/database/migrations.ts
|
|
7
|
-
// at schema version
|
|
7
|
+
// at schema version 2. If the app migrates past v2, openDb() refuses to write
|
|
8
8
|
// with an "update your aurora-mcp packages" error instead of corrupting newer
|
|
9
9
|
// schema assumptions.
|
|
10
10
|
import Database from 'better-sqlite3';
|
|
11
11
|
import { randomUUID } from 'node:crypto';
|
|
12
12
|
import { mkdirSync } from 'node:fs';
|
|
13
13
|
import { getDbPath, getUserDataDir } from './paths.js';
|
|
14
|
-
const KNOWN_SCHEMA_VERSION =
|
|
14
|
+
const KNOWN_SCHEMA_VERSION = 2;
|
|
15
15
|
let db = null;
|
|
16
16
|
export function getDb() {
|
|
17
17
|
if (db)
|
|
@@ -117,5 +117,28 @@ function runMigrations(database) {
|
|
|
117
117
|
database.pragma('user_version = 1');
|
|
118
118
|
})();
|
|
119
119
|
}
|
|
120
|
+
// v2 — Sample Extractor results (extraction_stems), verbatim mirror of the
|
|
121
|
+
// app's v2 migration. Unique per (asset_id, stem_id): re-runs replace rows.
|
|
122
|
+
if (database.pragma('user_version', { simple: true }) < 2) {
|
|
123
|
+
database.transaction(() => {
|
|
124
|
+
database.exec(`
|
|
125
|
+
CREATE TABLE IF NOT EXISTS extraction_stems (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
project_id TEXT NOT NULL,
|
|
128
|
+
asset_id TEXT NOT NULL,
|
|
129
|
+
stem_id TEXT NOT NULL,
|
|
130
|
+
path TEXT NOT NULL,
|
|
131
|
+
detected_key TEXT,
|
|
132
|
+
created_at INTEGER NOT NULL,
|
|
133
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
134
|
+
);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_extraction_stems_asset ON extraction_stems(asset_id);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_extraction_stems_project ON extraction_stems(project_id);
|
|
137
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_extraction_stems_asset_unique
|
|
138
|
+
ON extraction_stems(asset_id, stem_id);
|
|
139
|
+
`);
|
|
140
|
+
database.pragma('user_version = 2');
|
|
141
|
+
})();
|
|
142
|
+
}
|
|
120
143
|
}
|
|
121
144
|
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export interface ExtractBundleConfig {
|
|
2
|
+
stems: string[];
|
|
3
|
+
sepType: number;
|
|
4
|
+
addOpt1?: number;
|
|
5
|
+
addOpt2?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const EXTRACT_BUNDLES: Record<string, ExtractBundleConfig>;
|
|
8
|
+
export interface IndividualStemConfig {
|
|
9
|
+
sepType: number;
|
|
10
|
+
addOpt1?: number;
|
|
11
|
+
addOpt2?: number;
|
|
12
|
+
/** EXACT filename pattern MVSEP returns (lowercase, hyphens). */
|
|
13
|
+
mvsepName: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const EXTRACT_INDIVIDUAL_STEMS: Record<string, IndividualStemConfig>;
|
|
16
|
+
/** Vocal stems are driven by vocalMode/includeReverb, never by direct selection. */
|
|
17
|
+
export declare const VOCAL_STEM_IDS: Set<string>;
|
|
18
|
+
export type VocalSeparationType = 'lead_back' | 'male_female' | null;
|
|
19
|
+
export interface ExtractSelection {
|
|
20
|
+
/** Non-vocal stem ids (orb selection). Category markers are filtered upstream. */
|
|
21
|
+
stems: string[];
|
|
22
|
+
vocalSeparationType: VocalSeparationType;
|
|
23
|
+
includeReverb: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface PlannedApiCall {
|
|
26
|
+
type: 'dereverb' | 'vocal_bundle' | 'bundle' | 'individual';
|
|
27
|
+
/** bundle/vocal_bundle: EXTRACT_BUNDLES key. individual: the stem id. */
|
|
28
|
+
id: string;
|
|
29
|
+
sepType: number;
|
|
30
|
+
addOpt1?: number;
|
|
31
|
+
addOpt2?: number;
|
|
32
|
+
/** 'original' | 'dry' — dry chains the dereverbed vocal into a vocal bundle. */
|
|
33
|
+
inputSource: 'original' | 'dry';
|
|
34
|
+
/** Identification ruleset for the result files. */
|
|
35
|
+
outputType: string;
|
|
36
|
+
/** dereverb only: deliver vocal_dry as a stem (reverb-only mode, no vocal bundle). */
|
|
37
|
+
deliverVocalDry?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface ExtractPlan {
|
|
40
|
+
calls: PlannedApiCall[];
|
|
41
|
+
/** Stem ids this run delivers (ALWAYS ends with 'ee' — free, local, undeselectable). */
|
|
42
|
+
stemsToDeliver: string[];
|
|
43
|
+
}
|
|
44
|
+
/** Port of sample_worker.plan_api_calls — convert a selection into the
|
|
45
|
+
* optimized MVSEP call plan. EE is always delivered (local phase-cancel). */
|
|
46
|
+
export declare function planApiCalls(selection: ExtractSelection): ExtractPlan;
|
|
47
|
+
/** Port of sample_worker._identify_output_files — map MVSEP result files to
|
|
48
|
+
* stem keys by URL-filename pattern, NEVER by array position. Returns
|
|
49
|
+
* stemKey → url. Throws when nothing matches (clear failure beats silence). */
|
|
50
|
+
export declare function identifyOutputFiles(files: Array<{
|
|
51
|
+
url: string;
|
|
52
|
+
filename: string;
|
|
53
|
+
}>, outputType: string): Record<string, string>;
|
|
54
|
+
export declare const CREDITS_PER_MINUTE = 10;
|
|
55
|
+
/** 0-1 min = 1x, 1-2 min = 2x, … */
|
|
56
|
+
export declare function getMinuteMultiplier(durationSeconds: number): number;
|
|
57
|
+
export interface ExtractCallBreakdown {
|
|
58
|
+
bundles: {
|
|
59
|
+
bundleId: string;
|
|
60
|
+
stems: string[];
|
|
61
|
+
}[];
|
|
62
|
+
individualStems: string[];
|
|
63
|
+
totalCalls: number;
|
|
64
|
+
}
|
|
65
|
+
/** Breakdown for the cost card — derived from the REAL plan, so the count can
|
|
66
|
+
* never drift from what the orchestrator fires. */
|
|
67
|
+
export declare function getCallBreakdown(selection: ExtractSelection): ExtractCallBreakdown;
|
|
68
|
+
export declare function countApiCalls(selection: ExtractSelection): number;
|
|
69
|
+
export interface ExtractCostEstimate {
|
|
70
|
+
totalCalls: number;
|
|
71
|
+
minuteMultiplier: number;
|
|
72
|
+
/** Future pack metering: calls × minutes × CREDITS_PER_MINUTE. */
|
|
73
|
+
credits: number;
|
|
74
|
+
breakdown: ExtractCallBreakdown;
|
|
75
|
+
}
|
|
76
|
+
export declare function estimateExtractCost(selection: ExtractSelection, durationSeconds?: number): ExtractCostEstimate;
|
|
77
|
+
export declare const BUNDLE_DISPLAY_NAMES: Record<string, string>;
|
|
78
|
+
/** Display labels for every extractable stem id (orb names + vocal modes). */
|
|
79
|
+
export declare const EXTRACT_STEM_LABELS: Record<string, string>;
|
|
80
|
+
/** Upload/duration caps — prism's cost protection, adopted. */
|
|
81
|
+
export declare const EXTRACT_MAX_DURATION_SECONDS: number;
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Sample Extractor catalog + call planning — ported VERBATIM from prism's
|
|
2
|
+
// sample_worker.py (STEM_BUNDLES lines 222-247, INDIVIDUAL_STEMS 249-295,
|
|
3
|
+
// plan_api_calls 739-838, _identify_output_files 631-733) and the extraction
|
|
4
|
+
// half of frontend/src/lib/credits-calculator.ts. Pure logic, no IO — shared by
|
|
5
|
+
// the main-process orchestrator AND the renderer cost card.
|
|
6
|
+
//
|
|
7
|
+
// LOCKSTEP: this is the identical copy of aurora's src/shared/extract-catalog.ts
|
|
8
|
+
// (storage-semantics lockstep rule). Behavior changes go into BOTH files or
|
|
9
|
+
// neither.
|
|
10
|
+
// Bundled stems — one MVSEP call returns multiple stems.
|
|
11
|
+
export const EXTRACT_BUNDLES = {
|
|
12
|
+
drumsep: {
|
|
13
|
+
stems: [
|
|
14
|
+
'drum_kick',
|
|
15
|
+
'drum_snare',
|
|
16
|
+
'drum_toms',
|
|
17
|
+
'drum_hihats',
|
|
18
|
+
'drum_cymbals_crash',
|
|
19
|
+
'drum_cymbals_ride'
|
|
20
|
+
],
|
|
21
|
+
sepType: 37,
|
|
22
|
+
addOpt1: 7,
|
|
23
|
+
addOpt2: 0
|
|
24
|
+
},
|
|
25
|
+
// add_opt2=1 for original input, 0 for dry (post-dereverb) input — the
|
|
26
|
+
// planner sets it per run.
|
|
27
|
+
lead_back_vocal: { stems: ['vocal_lead', 'vocal_back'], sepType: 49, addOpt1: 6, addOpt2: 1 },
|
|
28
|
+
male_female_vocal: { stems: ['vocal_male', 'vocal_female'], sepType: 57, addOpt1: 3, addOpt2: 1 },
|
|
29
|
+
lead_rhythm_guitar: { stems: ['guitar_lead', 'guitar_rhythm'], sepType: 101, addOpt1: 0 },
|
|
30
|
+
// Dereverb exists as a "bundle" for call counting (dry + reverb in one call).
|
|
31
|
+
dereverb: { stems: ['vocal_dry', 'vocal_reverb'], sepType: 22, addOpt1: 6, addOpt2: 0 }
|
|
32
|
+
};
|
|
33
|
+
// Individual stems — one MVSEP call per stem.
|
|
34
|
+
export const EXTRACT_INDIVIDUAL_STEMS = {
|
|
35
|
+
// Keys
|
|
36
|
+
piano: { sepType: 29, addOpt1: 5, mvsepName: 'piano' },
|
|
37
|
+
digital_piano: { sepType: 79, mvsepName: 'digital-piano' },
|
|
38
|
+
organ: { sepType: 58, addOpt1: 3, mvsepName: 'organ' },
|
|
39
|
+
accordion: { sepType: 99, mvsepName: 'accordion' },
|
|
40
|
+
harpsichord: { sepType: 91, mvsepName: 'harpsichord' },
|
|
41
|
+
// Wind
|
|
42
|
+
saxophone: { sepType: 61, addOpt1: 3, addOpt2: 1, mvsepName: 'saxophone' },
|
|
43
|
+
flute: { sepType: 67, addOpt1: 1, addOpt2: 1, mvsepName: 'flute' },
|
|
44
|
+
trumpet: { sepType: 71, addOpt2: 1, mvsepName: 'trumpet' },
|
|
45
|
+
trombone: { sepType: 75, addOpt2: 1, mvsepName: 'trombone' },
|
|
46
|
+
french_horn: { sepType: 82, addOpt2: 1, mvsepName: 'french-horn' },
|
|
47
|
+
tuba: { sepType: 92, addOpt2: 1, mvsepName: 'tuba' },
|
|
48
|
+
clarinet: { sepType: 78, addOpt2: 1, mvsepName: 'clarinet' },
|
|
49
|
+
oboe: { sepType: 77, addOpt2: 1, mvsepName: 'oboe' },
|
|
50
|
+
bassoon: { sepType: 93, addOpt2: 1, mvsepName: 'bassoon' },
|
|
51
|
+
harmonica: { sepType: 87, addOpt2: 1, mvsepName: 'harmonica' },
|
|
52
|
+
// Plucked — NOTE: MVSEP uses 'acoustic-guitar', not 'guitar-acoustic'.
|
|
53
|
+
guitar_acoustic: { sepType: 66, addOpt2: 1, mvsepName: 'acoustic-guitar' },
|
|
54
|
+
guitar_electric: { sepType: 81, addOpt2: 1, mvsepName: 'electric-guitar' },
|
|
55
|
+
mandolin: { sepType: 74, mvsepName: 'mandolin' },
|
|
56
|
+
banjo: { sepType: 83, mvsepName: 'banjo' },
|
|
57
|
+
ukulele: { sepType: 96, mvsepName: 'ukulele' },
|
|
58
|
+
harp: { sepType: 72, mvsepName: 'harp' },
|
|
59
|
+
sitar: { sepType: 90, mvsepName: 'sitar' },
|
|
60
|
+
dobro: { sepType: 97, mvsepName: 'dobro' },
|
|
61
|
+
// Bowed
|
|
62
|
+
violin: { sepType: 65, addOpt2: 1, mvsepName: 'violin' },
|
|
63
|
+
viola: { sepType: 69, addOpt2: 1, mvsepName: 'viola' },
|
|
64
|
+
cello: { sepType: 70, addOpt2: 1, mvsepName: 'cello' },
|
|
65
|
+
double_bass: { sepType: 73, addOpt2: 1, mvsepName: 'double-bass' },
|
|
66
|
+
// Percussion
|
|
67
|
+
bells: { sepType: 95, mvsepName: 'bells' },
|
|
68
|
+
congas: { sepType: 94, mvsepName: 'congas' },
|
|
69
|
+
tambourine: { sepType: 76, mvsepName: 'tambourine' },
|
|
70
|
+
marimba: { sepType: 84, mvsepName: 'marimba' },
|
|
71
|
+
glockenspiel: { sepType: 85, mvsepName: 'glockenspiel' },
|
|
72
|
+
timpani: { sepType: 86, mvsepName: 'timpani' },
|
|
73
|
+
triangle: { sepType: 89, mvsepName: 'triangle' },
|
|
74
|
+
wind_chimes: { sepType: 98, mvsepName: 'wind-chimes' },
|
|
75
|
+
// Other
|
|
76
|
+
bass: { sepType: 41, addOpt1: 5, addOpt2: 1, mvsepName: 'bass' },
|
|
77
|
+
synth: { sepType: 88, addOpt1: 1, mvsepName: 'synth' }
|
|
78
|
+
};
|
|
79
|
+
/** Vocal stems are driven by vocalMode/includeReverb, never by direct selection. */
|
|
80
|
+
export const VOCAL_STEM_IDS = new Set([
|
|
81
|
+
'vocal_lead',
|
|
82
|
+
'vocal_back',
|
|
83
|
+
'vocal_male',
|
|
84
|
+
'vocal_female',
|
|
85
|
+
'vocal_dry',
|
|
86
|
+
'vocal_reverb'
|
|
87
|
+
]);
|
|
88
|
+
/** Port of sample_worker.plan_api_calls — convert a selection into the
|
|
89
|
+
* optimized MVSEP call plan. EE is always delivered (local phase-cancel). */
|
|
90
|
+
export function planApiCalls(selection) {
|
|
91
|
+
const calls = [];
|
|
92
|
+
const stemsToDeliver = [];
|
|
93
|
+
let dereverbRan = false;
|
|
94
|
+
// Step 0: dereverb. ALWAYS BSRoformer (add_opt1=6) — MelRoformer is broken
|
|
95
|
+
// via the MVSEP API (works on their web UI, returns wrong files via API).
|
|
96
|
+
if (selection.includeReverb) {
|
|
97
|
+
const deliverVocalDry = selection.vocalSeparationType === null;
|
|
98
|
+
calls.push({
|
|
99
|
+
type: 'dereverb',
|
|
100
|
+
id: 'dereverb',
|
|
101
|
+
sepType: 22,
|
|
102
|
+
addOpt1: 6,
|
|
103
|
+
addOpt2: 0,
|
|
104
|
+
inputSource: 'original',
|
|
105
|
+
outputType: 'dereverb',
|
|
106
|
+
deliverVocalDry
|
|
107
|
+
});
|
|
108
|
+
stemsToDeliver.push('vocal_reverb');
|
|
109
|
+
if (deliverVocalDry)
|
|
110
|
+
stemsToDeliver.push('vocal_dry');
|
|
111
|
+
dereverbRan = true;
|
|
112
|
+
}
|
|
113
|
+
// Step 1: vocal separation bundle.
|
|
114
|
+
if (selection.vocalSeparationType) {
|
|
115
|
+
const bundleId = selection.vocalSeparationType === 'lead_back' ? 'lead_back_vocal' : 'male_female_vocal';
|
|
116
|
+
const bundle = EXTRACT_BUNDLES[bundleId];
|
|
117
|
+
calls.push({
|
|
118
|
+
type: 'vocal_bundle',
|
|
119
|
+
id: bundleId,
|
|
120
|
+
sepType: bundle.sepType,
|
|
121
|
+
addOpt1: bundle.addOpt1,
|
|
122
|
+
// add_opt2: 1 against the original, 0 against the dereverbed dry vocal.
|
|
123
|
+
addOpt2: dereverbRan ? 0 : 1,
|
|
124
|
+
inputSource: dereverbRan ? 'dry' : 'original',
|
|
125
|
+
outputType: selection.vocalSeparationType
|
|
126
|
+
});
|
|
127
|
+
stemsToDeliver.push(...bundle.stems);
|
|
128
|
+
}
|
|
129
|
+
// Step 2: non-vocal bundles (drums, guitars) — any selected member pulls the
|
|
130
|
+
// whole bundle in ONE call.
|
|
131
|
+
const covered = new Set();
|
|
132
|
+
for (const bundleId of ['drumsep', 'lead_rhythm_guitar']) {
|
|
133
|
+
const bundle = EXTRACT_BUNDLES[bundleId];
|
|
134
|
+
const requested = bundle.stems.filter((s) => selection.stems.includes(s));
|
|
135
|
+
if (requested.length > 0) {
|
|
136
|
+
calls.push({
|
|
137
|
+
type: 'bundle',
|
|
138
|
+
id: bundleId,
|
|
139
|
+
sepType: bundle.sepType,
|
|
140
|
+
addOpt1: bundle.addOpt1,
|
|
141
|
+
addOpt2: bundle.addOpt2,
|
|
142
|
+
inputSource: 'original',
|
|
143
|
+
outputType: bundleId,
|
|
144
|
+
// prism delivered the full bundle; we keep that (you paid for the call).
|
|
145
|
+
});
|
|
146
|
+
stemsToDeliver.push(...bundle.stems);
|
|
147
|
+
for (const s of bundle.stems)
|
|
148
|
+
covered.add(s);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Step 3: individual stems.
|
|
152
|
+
for (const stemId of selection.stems) {
|
|
153
|
+
if (!covered.has(stemId) && EXTRACT_INDIVIDUAL_STEMS[stemId]) {
|
|
154
|
+
const cfg = EXTRACT_INDIVIDUAL_STEMS[stemId];
|
|
155
|
+
calls.push({
|
|
156
|
+
type: 'individual',
|
|
157
|
+
id: stemId,
|
|
158
|
+
sepType: cfg.sepType,
|
|
159
|
+
addOpt1: cfg.addOpt1,
|
|
160
|
+
addOpt2: cfg.addOpt2,
|
|
161
|
+
inputSource: 'original',
|
|
162
|
+
outputType: stemId
|
|
163
|
+
});
|
|
164
|
+
stemsToDeliver.push(stemId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// EE is always generated and delivered (free, local).
|
|
168
|
+
stemsToDeliver.push('ee');
|
|
169
|
+
return { calls, stemsToDeliver };
|
|
170
|
+
}
|
|
171
|
+
/** Port of sample_worker._identify_output_files — map MVSEP result files to
|
|
172
|
+
* stem keys by URL-filename pattern, NEVER by array position. Returns
|
|
173
|
+
* stemKey → url. Throws when nothing matches (clear failure beats silence). */
|
|
174
|
+
export function identifyOutputFiles(files, outputType) {
|
|
175
|
+
const results = {};
|
|
176
|
+
for (const f of files) {
|
|
177
|
+
const url = f.url;
|
|
178
|
+
if (!url)
|
|
179
|
+
continue;
|
|
180
|
+
const name = (f.filename || url.split('/').pop() || '').toLowerCase();
|
|
181
|
+
if (outputType === 'drumsep') {
|
|
182
|
+
// MVSEP returns: kick, snare, toms, hh, crash, ride.
|
|
183
|
+
if (name.includes('kick'))
|
|
184
|
+
results.drum_kick = url;
|
|
185
|
+
else if (name.includes('snare'))
|
|
186
|
+
results.drum_snare = url;
|
|
187
|
+
else if (name.includes('toms'))
|
|
188
|
+
results.drum_toms = url;
|
|
189
|
+
else if (name.includes('_hh_') ||
|
|
190
|
+
name.includes('_hh.') ||
|
|
191
|
+
name.includes('-hh_') ||
|
|
192
|
+
name.includes('-hh.'))
|
|
193
|
+
results.drum_hihats = url;
|
|
194
|
+
else if (name.includes('crash'))
|
|
195
|
+
results.drum_cymbals_crash = url;
|
|
196
|
+
else if (name.includes('ride'))
|
|
197
|
+
results.drum_cymbals_ride = url;
|
|
198
|
+
}
|
|
199
|
+
else if (outputType === 'dereverb') {
|
|
200
|
+
// BSRoformer output: noreverb (dry vocal), reverb (tail). 'dereverb'
|
|
201
|
+
// CONTAINS 'reverb', so boundary checks are mandatory. We do NOT use
|
|
202
|
+
// 'instrum' — it is not phase-accurate for summing back to the original.
|
|
203
|
+
if (name.includes('_dry_') || name.endsWith('_dry.wav'))
|
|
204
|
+
results.vocal_dry = url;
|
|
205
|
+
else if (name.includes('_noreverb_') || name.endsWith('_noreverb.wav'))
|
|
206
|
+
results.vocal_dry = url;
|
|
207
|
+
else if (name.includes('_other_') || name.endsWith('_other.wav'))
|
|
208
|
+
results.vocal_reverb = url;
|
|
209
|
+
else if (name.includes('_reverb_[mvsep') || name.endsWith('_reverb.wav'))
|
|
210
|
+
results.vocal_reverb = url;
|
|
211
|
+
}
|
|
212
|
+
else if (outputType === 'lead_back') {
|
|
213
|
+
// Returns: vocals-full, vocals-lead, vocals-back, instrum-only, back-instrum.
|
|
214
|
+
if (name.includes('vocals-lead') || name.includes('vocals_lead'))
|
|
215
|
+
results.vocal_lead = url;
|
|
216
|
+
else if (name.includes('vocals-back') || name.includes('vocals_back'))
|
|
217
|
+
results.vocal_back = url;
|
|
218
|
+
}
|
|
219
|
+
else if (outputType === 'male_female') {
|
|
220
|
+
// Filenames END with _male.wav / _female.wav; exclude instrumental-plus-*.
|
|
221
|
+
if (name.endsWith('_male.wav') && !name.includes('instrumental-plus'))
|
|
222
|
+
results.vocal_male = url;
|
|
223
|
+
else if (name.endsWith('_female.wav') && !name.includes('instrumental-plus'))
|
|
224
|
+
results.vocal_female = url;
|
|
225
|
+
}
|
|
226
|
+
else if (outputType === 'lead_rhythm_guitar') {
|
|
227
|
+
if (name.includes('lead-guitar') || name.includes('lead_guitar'))
|
|
228
|
+
results.guitar_lead = url;
|
|
229
|
+
else if (name.includes('rhythm-guitar') || name.includes('rhythm_guitar'))
|
|
230
|
+
results.guitar_rhythm = url;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// Individual stem — exact mvsepName match, excluding no_*/other files.
|
|
234
|
+
const cfg = EXTRACT_INDIVIDUAL_STEMS[outputType];
|
|
235
|
+
const mvsepName = cfg?.mvsepName ?? outputType.replace(/_/g, '-');
|
|
236
|
+
if (name.includes(mvsepName) && !name.includes('no_') && !name.includes('other')) {
|
|
237
|
+
results[outputType] = url;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (Object.keys(results).length === 0) {
|
|
242
|
+
throw new Error(`Could not identify MVSEP output files for ${outputType}. Files: ${files
|
|
243
|
+
.map((f) => f.filename)
|
|
244
|
+
.join(', ')}`);
|
|
245
|
+
}
|
|
246
|
+
return results;
|
|
247
|
+
}
|
|
248
|
+
// ── Cost math (port of credits-calculator.ts, extraction half) ──────────────
|
|
249
|
+
// Aurora's new credit model meters cloud calls; pack pricing is deferred, so
|
|
250
|
+
// the DISPLAY unit today is real provider units: MVSEP calls × ceil(minutes).
|
|
251
|
+
// The 10-credits/min/call constant survives for the future pack metering.
|
|
252
|
+
export const CREDITS_PER_MINUTE = 10;
|
|
253
|
+
/** 0-1 min = 1x, 1-2 min = 2x, … */
|
|
254
|
+
export function getMinuteMultiplier(durationSeconds) {
|
|
255
|
+
if (durationSeconds <= 0)
|
|
256
|
+
return 1;
|
|
257
|
+
return Math.ceil(durationSeconds / 60);
|
|
258
|
+
}
|
|
259
|
+
/** Breakdown for the cost card — derived from the REAL plan, so the count can
|
|
260
|
+
* never drift from what the orchestrator fires. */
|
|
261
|
+
export function getCallBreakdown(selection) {
|
|
262
|
+
const plan = planApiCalls(selection);
|
|
263
|
+
const bundles = [];
|
|
264
|
+
const individualStems = [];
|
|
265
|
+
for (const call of plan.calls) {
|
|
266
|
+
if (call.type === 'individual')
|
|
267
|
+
individualStems.push(call.id);
|
|
268
|
+
else
|
|
269
|
+
bundles.push({ bundleId: call.id, stems: EXTRACT_BUNDLES[call.id]?.stems ?? [] });
|
|
270
|
+
}
|
|
271
|
+
return { bundles, individualStems, totalCalls: plan.calls.length };
|
|
272
|
+
}
|
|
273
|
+
export function countApiCalls(selection) {
|
|
274
|
+
return planApiCalls(selection).calls.length;
|
|
275
|
+
}
|
|
276
|
+
export function estimateExtractCost(selection, durationSeconds = 60) {
|
|
277
|
+
const breakdown = getCallBreakdown(selection);
|
|
278
|
+
const minuteMultiplier = getMinuteMultiplier(durationSeconds);
|
|
279
|
+
return {
|
|
280
|
+
totalCalls: breakdown.totalCalls,
|
|
281
|
+
minuteMultiplier,
|
|
282
|
+
credits: breakdown.totalCalls * minuteMultiplier * CREDITS_PER_MINUTE,
|
|
283
|
+
breakdown
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Human-readable bundle names for cost cards.
|
|
287
|
+
export const BUNDLE_DISPLAY_NAMES = {
|
|
288
|
+
drumsep: 'Drum Kit',
|
|
289
|
+
dereverb: 'Dry + Reverb',
|
|
290
|
+
lead_back_vocal: 'Lead + Backing Vocals',
|
|
291
|
+
male_female_vocal: 'Male + Female Vocals',
|
|
292
|
+
lead_rhythm_guitar: 'Lead + Rhythm Guitar'
|
|
293
|
+
};
|
|
294
|
+
/** Display labels for every extractable stem id (orb names + vocal modes). */
|
|
295
|
+
export const EXTRACT_STEM_LABELS = {
|
|
296
|
+
vocal_lead: 'Lead Vocal',
|
|
297
|
+
vocal_back: 'Backing Vocals',
|
|
298
|
+
vocal_male: 'Male Vocal',
|
|
299
|
+
vocal_female: 'Female Vocal',
|
|
300
|
+
vocal_dry: 'Dry Vocal',
|
|
301
|
+
vocal_reverb: 'Reverb Tail',
|
|
302
|
+
drum_kick: 'Kick',
|
|
303
|
+
drum_snare: 'Snare',
|
|
304
|
+
drum_toms: 'Toms',
|
|
305
|
+
drum_hihats: 'Hi-Hats',
|
|
306
|
+
drum_cymbals_crash: 'Crash',
|
|
307
|
+
drum_cymbals_ride: 'Ride',
|
|
308
|
+
guitar_lead: 'Lead Guitar',
|
|
309
|
+
guitar_rhythm: 'Rhythm Guitar',
|
|
310
|
+
guitar_acoustic: 'Acoustic Guitar',
|
|
311
|
+
guitar_electric: 'Electric Guitar',
|
|
312
|
+
piano: 'Piano',
|
|
313
|
+
digital_piano: 'Digital Piano',
|
|
314
|
+
organ: 'Organ',
|
|
315
|
+
accordion: 'Accordion',
|
|
316
|
+
harpsichord: 'Harpsichord',
|
|
317
|
+
saxophone: 'Saxophone',
|
|
318
|
+
flute: 'Flute',
|
|
319
|
+
trumpet: 'Trumpet',
|
|
320
|
+
trombone: 'Trombone',
|
|
321
|
+
french_horn: 'French Horn',
|
|
322
|
+
tuba: 'Tuba',
|
|
323
|
+
clarinet: 'Clarinet',
|
|
324
|
+
oboe: 'Oboe',
|
|
325
|
+
bassoon: 'Bassoon',
|
|
326
|
+
harmonica: 'Harmonica',
|
|
327
|
+
mandolin: 'Mandolin',
|
|
328
|
+
banjo: 'Banjo',
|
|
329
|
+
ukulele: 'Ukulele',
|
|
330
|
+
harp: 'Harp',
|
|
331
|
+
sitar: 'Sitar',
|
|
332
|
+
dobro: 'Dobro',
|
|
333
|
+
violin: 'Violin',
|
|
334
|
+
viola: 'Viola',
|
|
335
|
+
cello: 'Cello',
|
|
336
|
+
double_bass: 'Double Bass',
|
|
337
|
+
bells: 'Bells',
|
|
338
|
+
congas: 'Congas',
|
|
339
|
+
tambourine: 'Tambourine',
|
|
340
|
+
marimba: 'Marimba',
|
|
341
|
+
glockenspiel: 'Glockenspiel',
|
|
342
|
+
timpani: 'Timpani',
|
|
343
|
+
triangle: 'Triangle',
|
|
344
|
+
wind_chimes: 'Wind Chimes',
|
|
345
|
+
bass: 'Bass',
|
|
346
|
+
synth: 'Synth',
|
|
347
|
+
ee: 'Everything Else'
|
|
348
|
+
};
|
|
349
|
+
/** Upload/duration caps — prism's cost protection, adopted. */
|
|
350
|
+
export const EXTRACT_MAX_DURATION_SECONDS = 12 * 60;
|
|
351
|
+
//# sourceMappingURL=extract-catalog.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ExtractSelection, type PlannedApiCall } from './extract-catalog.js';
|
|
2
|
+
import type { ExtractionStem, ProjectAsset, SeparationResultFile } from './types.js';
|
|
3
|
+
/** Serialized extract state carried by the job manifest's provider blob. */
|
|
4
|
+
export interface ExtractJobState {
|
|
5
|
+
assetId: string;
|
|
6
|
+
extractDir: string;
|
|
7
|
+
originalPath: string;
|
|
8
|
+
calls: PlannedApiCall[];
|
|
9
|
+
/** Index of the NEXT call to submit (or the in-flight one when hash is set). */
|
|
10
|
+
callIndex: number;
|
|
11
|
+
/** In-flight MVSEP hash for calls[callIndex], if submitted. */
|
|
12
|
+
currentHash: string | null;
|
|
13
|
+
/** Dereverb's dry vocal (chained into the vocal bundle when present). */
|
|
14
|
+
vocalDryPath: string | null;
|
|
15
|
+
/** stemId → local path, accumulated as calls land. */
|
|
16
|
+
extractedFiles: Record<string, string>;
|
|
17
|
+
detectedKey: string | null;
|
|
18
|
+
/** outputType: message, for calls that failed (run continues). */
|
|
19
|
+
failures: string[];
|
|
20
|
+
}
|
|
21
|
+
/** Cap + standardize + key-detect + plan. Runs ONCE at op time, before any
|
|
22
|
+
* MVSEP spend. Returns the job state seed. */
|
|
23
|
+
export declare function prepareExtract(assetId: string, selection: ExtractSelection): Promise<{
|
|
24
|
+
asset: ProjectAsset;
|
|
25
|
+
state: ExtractJobState;
|
|
26
|
+
}>;
|
|
27
|
+
/** Submit the next planned call. Mutates state (currentHash). */
|
|
28
|
+
export declare function submitNextExtractCall(state: ExtractJobState): Promise<void>;
|
|
29
|
+
/** Land a finished call's files. Mutates state (extractedFiles / vocalDryPath),
|
|
30
|
+
* then advances callIndex and clears the hash. */
|
|
31
|
+
export declare function landExtractCall(state: ExtractJobState, files: SeparationResultFile[]): Promise<void>;
|
|
32
|
+
/** Record a failed call and move on (prism behavior: partial results survive). */
|
|
33
|
+
export declare function failExtractCall(state: ExtractJobState, message: string): void;
|
|
34
|
+
/** EE synthesis + DB persistence. Runs once after the last call. */
|
|
35
|
+
export declare function finalizeExtract(asset: ProjectAsset, state: ExtractJobState): Promise<ExtractionStem[]>;
|
package/dist/extract.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Sample Extractor orchestration helpers — port of aurora
|
|
2
|
+
// src/main/extract/orchestrate.ts, restructured for the background-job model:
|
|
3
|
+
// the job advances ONE provider interaction at a time (submit a call, or poll
|
|
4
|
+
// the in-flight one), so aurora_get_job_status drives a sequential MVSEP plan
|
|
5
|
+
// across process restarts. Dereverb chains its dry vocal into the vocal
|
|
6
|
+
// bundle; EE phase-cancels every delivered stem from the standardized
|
|
7
|
+
// original; a single failed call keeps the run alive with partial results.
|
|
8
|
+
//
|
|
9
|
+
// LOCKSTEP: behavior mirrors the app orchestrator — changes go into both.
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { createSeparationJob } from './providers/mvsep.js';
|
|
13
|
+
import { getAsset, getAssetExtractsDir } from './storage/assets.js';
|
|
14
|
+
import { upsertExtractionStem } from './storage/extractions.js';
|
|
15
|
+
import { probeDurationSeconds, standardizeToWav } from './audio/ffmpeg.js';
|
|
16
|
+
import { decodeWavFile, encodeWavFloat32File, subtractWavs } from './audio/wav.js';
|
|
17
|
+
import { detectKey } from './key-detect.js';
|
|
18
|
+
import { EXTRACT_MAX_DURATION_SECONDS, identifyOutputFiles, planApiCalls } from './extract-catalog.js';
|
|
19
|
+
function specFor(call) {
|
|
20
|
+
const spec = {
|
|
21
|
+
sep_type: String(call.sepType),
|
|
22
|
+
output_format: '4', // 32-bit float WAV — phase accuracy for EE
|
|
23
|
+
is_demo: '0'
|
|
24
|
+
};
|
|
25
|
+
if (call.addOpt1 !== undefined)
|
|
26
|
+
spec.add_opt1 = String(call.addOpt1);
|
|
27
|
+
if (call.addOpt2 !== undefined)
|
|
28
|
+
spec.add_opt2 = String(call.addOpt2);
|
|
29
|
+
return spec;
|
|
30
|
+
}
|
|
31
|
+
/** Cap + standardize + key-detect + plan. Runs ONCE at op time, before any
|
|
32
|
+
* MVSEP spend. Returns the job state seed. */
|
|
33
|
+
export async function prepareExtract(assetId, selection) {
|
|
34
|
+
const asset = getAsset(assetId);
|
|
35
|
+
if (!asset)
|
|
36
|
+
throw new Error(`Asset not found: ${assetId}`);
|
|
37
|
+
const plan = planApiCalls(selection);
|
|
38
|
+
if (plan.calls.length === 0) {
|
|
39
|
+
throw new Error('Nothing selected — pick at least one instrument or a vocal mode.');
|
|
40
|
+
}
|
|
41
|
+
const duration = await probeDurationSeconds(asset.path);
|
|
42
|
+
if (duration !== null && duration > EXTRACT_MAX_DURATION_SECONDS) {
|
|
43
|
+
throw new Error(`Track is ${Math.round(duration)}s — extraction caps at 12 minutes. Export a shorter section.`);
|
|
44
|
+
}
|
|
45
|
+
const extractDir = getAssetExtractsDir(asset);
|
|
46
|
+
await mkdir(extractDir, { recursive: true });
|
|
47
|
+
const originalPath = join(extractDir, 'original.wav');
|
|
48
|
+
await standardizeToWav(asset.path, originalPath);
|
|
49
|
+
const detectedKey = await detectKey(originalPath);
|
|
50
|
+
return {
|
|
51
|
+
asset,
|
|
52
|
+
state: {
|
|
53
|
+
assetId,
|
|
54
|
+
extractDir,
|
|
55
|
+
originalPath,
|
|
56
|
+
calls: plan.calls,
|
|
57
|
+
callIndex: 0,
|
|
58
|
+
currentHash: null,
|
|
59
|
+
vocalDryPath: null,
|
|
60
|
+
extractedFiles: {},
|
|
61
|
+
detectedKey,
|
|
62
|
+
failures: []
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Submit the next planned call. Mutates state (currentHash). */
|
|
67
|
+
export async function submitNextExtractCall(state) {
|
|
68
|
+
const call = state.calls[state.callIndex];
|
|
69
|
+
const inputPath = call.inputSource === 'dry' && state.vocalDryPath ? state.vocalDryPath : state.originalPath;
|
|
70
|
+
const audio = await readFile(inputPath);
|
|
71
|
+
const { hash } = await createSeparationJob(audio, specFor(call));
|
|
72
|
+
state.currentHash = hash;
|
|
73
|
+
}
|
|
74
|
+
/** Land a finished call's files. Mutates state (extractedFiles / vocalDryPath),
|
|
75
|
+
* then advances callIndex and clears the hash. */
|
|
76
|
+
export async function landExtractCall(state, files) {
|
|
77
|
+
const call = state.calls[state.callIndex];
|
|
78
|
+
const outputs = identifyOutputFiles(files, call.outputType);
|
|
79
|
+
for (const [stemKey, url] of Object.entries(outputs)) {
|
|
80
|
+
const dest = join(state.extractDir, `${stemKey}.wav`);
|
|
81
|
+
const res = await fetch(url);
|
|
82
|
+
if (!res.ok)
|
|
83
|
+
throw new Error(`Stem download failed (HTTP ${res.status}): ${url}`);
|
|
84
|
+
await writeFile(dest, Buffer.from(await res.arrayBuffer()));
|
|
85
|
+
if (call.type === 'dereverb') {
|
|
86
|
+
if (stemKey === 'vocal_dry') {
|
|
87
|
+
state.vocalDryPath = dest;
|
|
88
|
+
// Deliver vocal_dry only in reverb-only mode (no vocal bundle).
|
|
89
|
+
if (call.deliverVocalDry)
|
|
90
|
+
state.extractedFiles.vocal_dry = dest;
|
|
91
|
+
}
|
|
92
|
+
else if (stemKey === 'vocal_reverb') {
|
|
93
|
+
state.extractedFiles.vocal_reverb = dest;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
state.extractedFiles[stemKey] = dest;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
state.callIndex++;
|
|
101
|
+
state.currentHash = null;
|
|
102
|
+
}
|
|
103
|
+
/** Record a failed call and move on (prism behavior: partial results survive). */
|
|
104
|
+
export function failExtractCall(state, message) {
|
|
105
|
+
const call = state.calls[state.callIndex];
|
|
106
|
+
state.failures.push(`${call?.outputType ?? 'call'}: ${message}`);
|
|
107
|
+
state.callIndex++;
|
|
108
|
+
state.currentHash = null;
|
|
109
|
+
}
|
|
110
|
+
/** EE synthesis + DB persistence. Runs once after the last call. */
|
|
111
|
+
export async function finalizeExtract(asset, state) {
|
|
112
|
+
if (Object.keys(state.extractedFiles).length === 0) {
|
|
113
|
+
throw new Error(`No stems extracted — every separation failed. ${state.failures.join('; ')}`);
|
|
114
|
+
}
|
|
115
|
+
const original = await decodeWavFile(state.originalPath);
|
|
116
|
+
const stems = await Promise.all(Object.values(state.extractedFiles).map((path) => decodeWavFile(path)));
|
|
117
|
+
const ee = subtractWavs(original, ...stems);
|
|
118
|
+
const eePath = join(state.extractDir, 'ee.wav');
|
|
119
|
+
await encodeWavFloat32File(eePath, ee.channels, ee.sampleRate);
|
|
120
|
+
state.extractedFiles.ee = eePath;
|
|
121
|
+
const rows = [];
|
|
122
|
+
for (const [stemId, path] of Object.entries(state.extractedFiles)) {
|
|
123
|
+
rows.push(upsertExtractionStem({
|
|
124
|
+
projectId: asset.projectId,
|
|
125
|
+
assetId: asset.id,
|
|
126
|
+
stemId,
|
|
127
|
+
path,
|
|
128
|
+
detectedKey: state.detectedKey
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=extract.js.map
|
package/dist/jobs.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type SplitJobName } from './split.js';
|
|
2
|
-
|
|
2
|
+
import { type ExtractJobState } from './extract.js';
|
|
3
|
+
export type JobKind = 'generate' | 'sounds' | 'cover' | 'add_vocals' | 'add_instrumental' | 'split' | 'extract';
|
|
3
4
|
export interface JobManifest {
|
|
4
5
|
jobId: string;
|
|
5
6
|
kind: JobKind;
|
|
@@ -20,6 +21,8 @@ export interface JobManifest {
|
|
|
20
21
|
assetId?: string;
|
|
21
22
|
stemsDir?: string;
|
|
22
23
|
hashes?: Record<SplitJobName, string>;
|
|
24
|
+
/** extract — the sequential call-plan state machine (extract.ts). */
|
|
25
|
+
extract?: ExtractJobState;
|
|
23
26
|
};
|
|
24
27
|
/** Per-sub-unit idempotency flags (split job names / 'assets'). */
|
|
25
28
|
landed: Record<string, boolean>;
|