copilot-liku-cli 0.0.4 → 0.0.9
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/QUICKSTART.md +24 -0
- package/README.md +85 -33
- package/package.json +23 -14
- package/scripts/postinstall.js +63 -0
- package/src/cli/commands/window.js +66 -0
- package/src/main/agents/base-agent.js +15 -7
- package/src/main/agents/builder.js +211 -0
- package/src/main/agents/index.js +12 -4
- package/src/main/agents/orchestrator.js +40 -0
- package/src/main/agents/producer.js +891 -0
- package/src/main/agents/researcher.js +78 -0
- package/src/main/agents/state-manager.js +134 -2
- package/src/main/agents/trace-writer.js +83 -0
- package/src/main/agents/verifier.js +201 -0
- package/src/main/ai-service.js +673 -66
- package/src/main/index.js +682 -110
- package/src/main/inspect-service.js +24 -1
- package/src/main/python-bridge.js +395 -0
- package/src/main/system-automation.js +934 -133
- package/src/main/ui-automation/core/ui-provider.js +99 -0
- package/src/main/ui-automation/core/uia-host.js +214 -0
- package/src/main/ui-automation/index.js +30 -0
- package/src/main/ui-automation/interactions/element-click.js +6 -6
- package/src/main/ui-automation/interactions/high-level.js +28 -6
- package/src/main/ui-automation/interactions/index.js +21 -0
- package/src/main/ui-automation/interactions/pattern-actions.js +236 -0
- package/src/main/ui-automation/window/index.js +6 -0
- package/src/main/ui-automation/window/manager.js +173 -26
- package/src/main/ui-watcher.js +420 -56
- package/src/main/visual-awareness.js +18 -1
- package/src/native/windows-uia/Program.cs +89 -0
- package/src/native/windows-uia/build.ps1 +24 -0
- package/src/native/windows-uia-dotnet/Program.cs +920 -0
- package/src/native/windows-uia-dotnet/WindowsUIA.csproj +11 -0
- package/src/native/windows-uia-dotnet/build.ps1 +24 -0
- package/src/renderer/chat/chat.js +943 -671
- package/src/renderer/chat/index.html +39 -4
- package/src/renderer/chat/preload.js +8 -1
- package/src/renderer/overlay/overlay.js +157 -8
- package/src/renderer/overlay/preload.js +4 -0
- package/src/shared/inspect-types.js +82 -6
- package/ARCHITECTURE.md +0 -411
- package/CONFIGURATION.md +0 -302
- package/CONTRIBUTING.md +0 -225
- package/ELECTRON_README.md +0 -121
- package/PROJECT_STATUS.md +0 -229
- package/TESTING.md +0 -274
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Producer Agent
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates "agentic producer" flow:
|
|
5
|
+
* 1) Draft Score Plan from prompt (schema-guided).
|
|
6
|
+
* 2) Generate music via JSON-RPC gateway.
|
|
7
|
+
* 3) Run critics to quality-gate the result.
|
|
8
|
+
* 4) Refine the plan and retry (bounded attempts).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { BaseAgent, AgentRole, AgentCapabilities } = require('./base-agent');
|
|
12
|
+
const { PythonBridge } = require('../python-bridge');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_MAX_ITERATIONS = 2;
|
|
17
|
+
const DEFAULT_BPM = 90;
|
|
18
|
+
const DEFAULT_KEY = 'C';
|
|
19
|
+
const DEFAULT_MODE = 'minor';
|
|
20
|
+
const DEFAULT_TIME_SIGNATURE = [4, 4];
|
|
21
|
+
const DEFAULT_DIRECTOR_MODEL = 'claude-sonnet-4.5';
|
|
22
|
+
const DEFAULT_PRODUCER_MODEL = 'gpt-4.1';
|
|
23
|
+
const DEFAULT_VERIFIER_MODEL = 'claude-sonnet-4.5';
|
|
24
|
+
|
|
25
|
+
class ProducerAgent extends BaseAgent {
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
super({
|
|
28
|
+
...options,
|
|
29
|
+
role: AgentRole.PRODUCER,
|
|
30
|
+
name: options.name || 'producer',
|
|
31
|
+
description: 'Creates score plans, generates music, and runs quality critics',
|
|
32
|
+
capabilities: [
|
|
33
|
+
AgentCapabilities.SEARCH,
|
|
34
|
+
AgentCapabilities.READ,
|
|
35
|
+
AgentCapabilities.EXECUTE,
|
|
36
|
+
AgentCapabilities.TODO,
|
|
37
|
+
AgentCapabilities.HANDOFF
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.pythonBridge = null;
|
|
42
|
+
this._scorePlanSchemaCache = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getSystemPrompt() {
|
|
46
|
+
return `You are the PRODUCER agent in a multi-agent music system.
|
|
47
|
+
|
|
48
|
+
# ROLE
|
|
49
|
+
- Generate a valid Score Plan (score_plan_v1) for MUSE.
|
|
50
|
+
- Keep plans musically coherent and production-aware.
|
|
51
|
+
- Return JSON only (no markdown) when asked to output a plan.
|
|
52
|
+
|
|
53
|
+
# QUALITY
|
|
54
|
+
- Prefer clear section structures and instrument roles.
|
|
55
|
+
- Use musically sensible BPM, key, mode, and arrangement.
|
|
56
|
+
|
|
57
|
+
# SAFETY
|
|
58
|
+
- Do not remove features or disable existing behavior.
|
|
59
|
+
- Keep outputs deterministic and schema-compliant.`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async process(task, context = {}) {
|
|
63
|
+
const prompt = this._extractPrompt(task);
|
|
64
|
+
const maxIterations = Number(context.maxIterations || DEFAULT_MAX_ITERATIONS);
|
|
65
|
+
const allowCriticGateFailure = Boolean(
|
|
66
|
+
context.allowCriticGateFailure ||
|
|
67
|
+
context.generationOnlySuccess ||
|
|
68
|
+
context.allowQualityGateBypass
|
|
69
|
+
);
|
|
70
|
+
const referenceInput = this._resolveReferenceInput(prompt, context);
|
|
71
|
+
const modelPolicy = this._resolveModelPolicy(context);
|
|
72
|
+
|
|
73
|
+
const builder = this.orchestrator?.getBuilder?.();
|
|
74
|
+
const verifier = this.orchestrator?.getVerifier?.();
|
|
75
|
+
if (!builder) {
|
|
76
|
+
return { success: false, error: 'Producer requires Builder agent access' };
|
|
77
|
+
}
|
|
78
|
+
if (!verifier) {
|
|
79
|
+
return { success: false, error: 'Producer requires Verifier agent access' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const referenceProfile = await this._analyzeReference(referenceInput);
|
|
83
|
+
let scorePlan = await this._createScorePlan(prompt, referenceProfile, modelPolicy);
|
|
84
|
+
|
|
85
|
+
const planningTelemetry = {
|
|
86
|
+
roleModels: {
|
|
87
|
+
director: modelPolicy.director,
|
|
88
|
+
producer: modelPolicy.producer,
|
|
89
|
+
verifier: modelPolicy.verifier
|
|
90
|
+
},
|
|
91
|
+
referenceUsed: !!referenceProfile,
|
|
92
|
+
referenceSource: referenceInput || null,
|
|
93
|
+
timestamp: new Date().toISOString()
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
this.log('info', 'Producer model policy selected', planningTelemetry);
|
|
97
|
+
const phaseStates = [];
|
|
98
|
+
this._pushPhaseState(phaseStates, 'producer_start', 0.02, 'Producer orchestration started');
|
|
99
|
+
|
|
100
|
+
const validationTelemetry = [];
|
|
101
|
+
|
|
102
|
+
const initialValidation = this._prepareValidatedScorePlan(scorePlan, prompt, 'initial');
|
|
103
|
+
scorePlan = initialValidation.plan;
|
|
104
|
+
validationTelemetry.push(initialValidation);
|
|
105
|
+
this._pushPhaseState(phaseStates, 'score_plan_validation', 0.12, initialValidation.validBefore ? 'Initial score plan validated' : 'Initial score plan required fallback');
|
|
106
|
+
|
|
107
|
+
scorePlan = this._normalizeScorePlan(scorePlan, prompt);
|
|
108
|
+
|
|
109
|
+
let lastResult = null;
|
|
110
|
+
let lastCritics = null;
|
|
111
|
+
let lastOutputAnalysis = null;
|
|
112
|
+
const preflightTelemetry = [];
|
|
113
|
+
|
|
114
|
+
for (let attempt = 1; attempt <= maxIterations; attempt++) {
|
|
115
|
+
this.log('info', 'Producer attempt starting', { attempt, maxIterations });
|
|
116
|
+
this._pushPhaseState(phaseStates, `attempt_${attempt}_start`, 0.15 + ((attempt - 1) * (0.7 / Math.max(1, maxIterations))), `Attempt ${attempt}/${maxIterations} started`);
|
|
117
|
+
|
|
118
|
+
const attemptValidation = this._prepareValidatedScorePlan(scorePlan, prompt, `attempt_${attempt}`);
|
|
119
|
+
scorePlan = attemptValidation.plan;
|
|
120
|
+
validationTelemetry.push(attemptValidation);
|
|
121
|
+
this._pushPhaseState(phaseStates, `attempt_${attempt}_validation`, 0.2 + ((attempt - 1) * (0.7 / Math.max(1, maxIterations))), attemptValidation.validBefore ? 'Attempt plan validated' : 'Attempt plan fallback applied');
|
|
122
|
+
|
|
123
|
+
const preflight = await verifier.preflightScorePlanGate(scorePlan, {
|
|
124
|
+
prompt,
|
|
125
|
+
model: modelPolicy.verifier
|
|
126
|
+
});
|
|
127
|
+
preflightTelemetry.push({ attempt, ...preflight });
|
|
128
|
+
this._pushPhaseState(phaseStates, `attempt_${attempt}_preflight`, 0.25 + ((attempt - 1) * (0.7 / Math.max(1, maxIterations))), preflight.passed ? 'Preflight gate passed' : 'Preflight gate failed');
|
|
129
|
+
|
|
130
|
+
if (!preflight.passed) {
|
|
131
|
+
this.log('warn', 'Preflight gate failed before generation', {
|
|
132
|
+
attempt,
|
|
133
|
+
issues: preflight.issues
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (attempt < maxIterations) {
|
|
137
|
+
const syntheticCritic = {
|
|
138
|
+
report: {
|
|
139
|
+
summary: `Preflight gate failed: ${(preflight.issues || []).slice(0, 5).join('; ')}`
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
scorePlan = await this._refineScorePlan(prompt, scorePlan, syntheticCritic, referenceProfile, modelPolicy);
|
|
143
|
+
scorePlan = this._normalizeScorePlan(scorePlan, prompt);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
terminalOutcome: 'PRECHECK_FAILED',
|
|
150
|
+
response: this._formatFailureResponse(scorePlan, lastResult, lastCritics, maxIterations, {
|
|
151
|
+
preflight,
|
|
152
|
+
outputAnalysis: lastOutputAnalysis
|
|
153
|
+
}),
|
|
154
|
+
scorePlan,
|
|
155
|
+
generation: lastResult,
|
|
156
|
+
critics: lastCritics,
|
|
157
|
+
outputAnalysis: lastOutputAnalysis,
|
|
158
|
+
planningTelemetry,
|
|
159
|
+
validationTelemetry,
|
|
160
|
+
preflightTelemetry,
|
|
161
|
+
phaseStates
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lastResult = await builder.generateMusicFromScorePlan(scorePlan, {
|
|
166
|
+
prompt,
|
|
167
|
+
trackProgress: true
|
|
168
|
+
});
|
|
169
|
+
this._pushPhaseState(phaseStates, `attempt_${attempt}_generation`, 0.55 + ((attempt - 1) * (0.35 / Math.max(1, maxIterations))), 'Generation run completed');
|
|
170
|
+
|
|
171
|
+
if (!lastResult || !lastResult.midi_path) {
|
|
172
|
+
this.log('error', 'Music generation failed', { attempt, result: lastResult });
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
terminalOutcome: 'GENERATION_FAILED',
|
|
176
|
+
error: 'Generation failed or missing midi_path',
|
|
177
|
+
attempt,
|
|
178
|
+
result: lastResult,
|
|
179
|
+
planningTelemetry,
|
|
180
|
+
validationTelemetry,
|
|
181
|
+
preflightTelemetry,
|
|
182
|
+
phaseStates
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
lastCritics = await verifier.runMusicCritics(lastResult.midi_path, scorePlan.genre);
|
|
187
|
+
this._pushPhaseState(phaseStates, `attempt_${attempt}_critics`, 0.72 + ((attempt - 1) * (0.2 / Math.max(1, maxIterations))), lastCritics?.passed ? 'Critics passed' : 'Critics failed');
|
|
188
|
+
|
|
189
|
+
if (lastResult.audio_path) {
|
|
190
|
+
try {
|
|
191
|
+
lastOutputAnalysis = await verifier.analyzeRenderedOutput(
|
|
192
|
+
lastResult.audio_path,
|
|
193
|
+
scorePlan.genre || 'pop'
|
|
194
|
+
);
|
|
195
|
+
this._pushPhaseState(phaseStates, `attempt_${attempt}_output_analysis`, 0.82 + ((attempt - 1) * (0.16 / Math.max(1, maxIterations))), 'Output analysis complete');
|
|
196
|
+
} catch (error) {
|
|
197
|
+
lastOutputAnalysis = {
|
|
198
|
+
passed: false,
|
|
199
|
+
error: error.message
|
|
200
|
+
};
|
|
201
|
+
this._pushPhaseState(phaseStates, `attempt_${attempt}_output_analysis`, 0.82 + ((attempt - 1) * (0.16 / Math.max(1, maxIterations))), `Output analysis failed: ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (lastCritics.passed) {
|
|
206
|
+
this._pushPhaseState(phaseStates, 'producer_complete', 1.0, 'Producer completed successfully');
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
terminalOutcome: 'COMPLETED_SUCCESS',
|
|
210
|
+
response: this._formatSuccessResponse(scorePlan, lastResult, lastCritics, attempt, {
|
|
211
|
+
outputAnalysis: lastOutputAnalysis,
|
|
212
|
+
preflight: preflightTelemetry[preflightTelemetry.length - 1] || null
|
|
213
|
+
}),
|
|
214
|
+
scorePlan,
|
|
215
|
+
generation: lastResult,
|
|
216
|
+
critics: lastCritics,
|
|
217
|
+
outputAnalysis: lastOutputAnalysis,
|
|
218
|
+
planningTelemetry,
|
|
219
|
+
validationTelemetry,
|
|
220
|
+
preflightTelemetry,
|
|
221
|
+
phaseStates
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (allowCriticGateFailure && lastResult && lastResult.midi_path) {
|
|
226
|
+
this._pushPhaseState(phaseStates, 'producer_complete', 1.0, 'Producer completed with critic-gate bypass');
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
terminalOutcome: 'COMPLETED_WITH_CRITIC_FAIL_ACCEPTED',
|
|
230
|
+
response: this._formatSuccessResponse(scorePlan, lastResult, lastCritics, attempt, {
|
|
231
|
+
outputAnalysis: lastOutputAnalysis,
|
|
232
|
+
preflight: preflightTelemetry[preflightTelemetry.length - 1] || null,
|
|
233
|
+
criticGateBypassed: true
|
|
234
|
+
}),
|
|
235
|
+
scorePlan,
|
|
236
|
+
generation: lastResult,
|
|
237
|
+
critics: lastCritics,
|
|
238
|
+
outputAnalysis: lastOutputAnalysis,
|
|
239
|
+
planningTelemetry,
|
|
240
|
+
validationTelemetry,
|
|
241
|
+
preflightTelemetry,
|
|
242
|
+
phaseStates
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (attempt < maxIterations) {
|
|
247
|
+
scorePlan = await this._refineScorePlan(prompt, scorePlan, lastCritics, referenceProfile, modelPolicy);
|
|
248
|
+
scorePlan = this._normalizeScorePlan(scorePlan, prompt);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
success: false,
|
|
254
|
+
terminalOutcome: 'COMPLETED_WITH_CRITIC_FAIL',
|
|
255
|
+
response: this._formatFailureResponse(scorePlan, lastResult, lastCritics, maxIterations, {
|
|
256
|
+
preflight: preflightTelemetry[preflightTelemetry.length - 1] || null,
|
|
257
|
+
outputAnalysis: lastOutputAnalysis,
|
|
258
|
+
suggestBypass: true
|
|
259
|
+
}),
|
|
260
|
+
scorePlan,
|
|
261
|
+
generation: lastResult,
|
|
262
|
+
critics: lastCritics,
|
|
263
|
+
outputAnalysis: lastOutputAnalysis,
|
|
264
|
+
planningTelemetry,
|
|
265
|
+
validationTelemetry,
|
|
266
|
+
preflightTelemetry,
|
|
267
|
+
phaseStates
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_pushPhaseState(target, step, percent, message, extra = {}) {
|
|
272
|
+
target.push({
|
|
273
|
+
step,
|
|
274
|
+
percent: Math.max(0, Math.min(1, Number(percent) || 0)),
|
|
275
|
+
message,
|
|
276
|
+
timestamp: new Date().toISOString(),
|
|
277
|
+
...extra
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async ensurePythonBridge() {
|
|
282
|
+
if (!this.pythonBridge) {
|
|
283
|
+
this.pythonBridge = PythonBridge.getShared();
|
|
284
|
+
}
|
|
285
|
+
if (!this.pythonBridge.isRunning) {
|
|
286
|
+
await this.pythonBridge.start();
|
|
287
|
+
}
|
|
288
|
+
return this.pythonBridge;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
_extractPrompt(task) {
|
|
292
|
+
if (!task) return '';
|
|
293
|
+
if (typeof task === 'string') return task.trim();
|
|
294
|
+
if (typeof task.prompt === 'string') return task.prompt.trim();
|
|
295
|
+
if (typeof task.description === 'string') return task.description.trim();
|
|
296
|
+
return '';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_schemaPath() {
|
|
300
|
+
return path.resolve(__dirname, '..', '..', '..', '..', 'MUSE', 'docs', 'muse-specs', 'schemas', 'score_plan.v1.schema.json');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_loadSchema() {
|
|
304
|
+
try {
|
|
305
|
+
const schemaPath = this._schemaPath();
|
|
306
|
+
return fs.readFileSync(schemaPath, 'utf-8');
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.log('warn', 'Failed to load score plan schema', { error: error.message });
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_loadScorePlanSchema() {
|
|
314
|
+
if (this._scorePlanSchemaCache) {
|
|
315
|
+
return this._scorePlanSchemaCache;
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const schemaText = this._loadSchema();
|
|
319
|
+
if (!schemaText) return null;
|
|
320
|
+
this._scorePlanSchemaCache = JSON.parse(schemaText);
|
|
321
|
+
return this._scorePlanSchemaCache;
|
|
322
|
+
} catch (error) {
|
|
323
|
+
this.log('warn', 'Failed to parse score plan schema JSON', { error: error.message });
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async _createScorePlan(prompt, referenceProfile = null, modelPolicy = null) {
|
|
329
|
+
const schemaText = this._loadSchema();
|
|
330
|
+
const referenceContext = this._formatReferenceContext(referenceProfile);
|
|
331
|
+
const policy = modelPolicy || { director: DEFAULT_DIRECTOR_MODEL, producer: DEFAULT_PRODUCER_MODEL };
|
|
332
|
+
|
|
333
|
+
const directorGuidance = await this._draftDirectorGuidance(prompt, referenceProfile, policy.director);
|
|
334
|
+
|
|
335
|
+
const baseInstruction = `Create a score_plan_v1 JSON for this prompt.
|
|
336
|
+
Prompt: ${prompt}
|
|
337
|
+
|
|
338
|
+
${referenceContext}
|
|
339
|
+
|
|
340
|
+
Director guidance (creative intent):
|
|
341
|
+
${directorGuidance}
|
|
342
|
+
|
|
343
|
+
Rules:
|
|
344
|
+
- Output JSON ONLY (no markdown).
|
|
345
|
+
- Must satisfy required fields in the schema.
|
|
346
|
+
- Keep instruments realistic and varied.
|
|
347
|
+
`;
|
|
348
|
+
|
|
349
|
+
const promptWithSchema = schemaText
|
|
350
|
+
? `${baseInstruction}\nSchema:\n${schemaText}`
|
|
351
|
+
: baseInstruction;
|
|
352
|
+
|
|
353
|
+
const response = await this.chat(promptWithSchema, { model: policy.producer });
|
|
354
|
+
const jsonText = this._extractJson(response.text);
|
|
355
|
+
if (!jsonText) {
|
|
356
|
+
this.log('warn', 'Failed to parse score plan JSON, falling back');
|
|
357
|
+
return {};
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
return JSON.parse(jsonText);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
this.log('warn', 'Score plan JSON parse error', { error: error.message });
|
|
363
|
+
return {};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async _refineScorePlan(prompt, previousPlan, critics, referenceProfile = null, modelPolicy = null) {
|
|
368
|
+
const schemaText = this._loadSchema();
|
|
369
|
+
const criticSummary = critics?.report?.summary || 'Critics failed without a summary.';
|
|
370
|
+
const referenceContext = this._formatReferenceContext(referenceProfile);
|
|
371
|
+
const policy = modelPolicy || { director: DEFAULT_DIRECTOR_MODEL, producer: DEFAULT_PRODUCER_MODEL };
|
|
372
|
+
const baseInstruction = `Refine the previous score_plan_v1 JSON to address critics.
|
|
373
|
+
Prompt: ${prompt}
|
|
374
|
+
Critic summary: ${criticSummary}
|
|
375
|
+
|
|
376
|
+
${referenceContext}
|
|
377
|
+
|
|
378
|
+
Rules:
|
|
379
|
+
- Output JSON ONLY (no markdown).
|
|
380
|
+
- Preserve the prompt and keep schema validity.
|
|
381
|
+
`;
|
|
382
|
+
|
|
383
|
+
const promptWithSchema = schemaText
|
|
384
|
+
? `${baseInstruction}\nPrevious plan:\n${JSON.stringify(previousPlan, null, 2)}\nSchema:\n${schemaText}`
|
|
385
|
+
: `${baseInstruction}\nPrevious plan:\n${JSON.stringify(previousPlan, null, 2)}`;
|
|
386
|
+
|
|
387
|
+
const response = await this.chat(promptWithSchema, { model: policy.producer });
|
|
388
|
+
const jsonText = this._extractJson(response.text);
|
|
389
|
+
if (!jsonText) {
|
|
390
|
+
return previousPlan;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
return JSON.parse(jsonText);
|
|
394
|
+
} catch (_error) {
|
|
395
|
+
return previousPlan;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
_normalizeScorePlan(plan, prompt) {
|
|
400
|
+
const normalized = (plan && typeof plan === 'object') ? { ...plan } : {};
|
|
401
|
+
normalized.schema_version = 'score_plan_v1';
|
|
402
|
+
normalized.prompt = (normalized.prompt && String(normalized.prompt).trim()) || prompt || 'Music generation';
|
|
403
|
+
|
|
404
|
+
const bpm = Number(normalized.bpm);
|
|
405
|
+
normalized.bpm = Number.isFinite(bpm) ? Math.min(220, Math.max(30, bpm)) : DEFAULT_BPM;
|
|
406
|
+
|
|
407
|
+
const key = typeof normalized.key === 'string' ? normalized.key.trim() : DEFAULT_KEY;
|
|
408
|
+
normalized.key = /^[A-G](#|b)?$/.test(key) ? key : DEFAULT_KEY;
|
|
409
|
+
|
|
410
|
+
const mode = typeof normalized.mode === 'string' ? normalized.mode : DEFAULT_MODE;
|
|
411
|
+
const allowedModes = new Set(['major', 'minor', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'locrian']);
|
|
412
|
+
normalized.mode = allowedModes.has(mode) ? mode : DEFAULT_MODE;
|
|
413
|
+
|
|
414
|
+
if (!Array.isArray(normalized.time_signature) || normalized.time_signature.length !== 2) {
|
|
415
|
+
normalized.time_signature = DEFAULT_TIME_SIGNATURE;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!Array.isArray(normalized.sections) || normalized.sections.length === 0) {
|
|
419
|
+
normalized.sections = [
|
|
420
|
+
{ name: 'Intro', type: 'intro', bars: 8, energy: 0.2, tension: 0.2 },
|
|
421
|
+
{ name: 'Verse', type: 'verse', bars: 16, energy: 0.35, tension: 0.3 },
|
|
422
|
+
{ name: 'Chorus', type: 'chorus', bars: 16, energy: 0.6, tension: 0.5 },
|
|
423
|
+
{ name: 'Outro', type: 'outro', bars: 8, energy: 0.2, tension: 0.2 }
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!Array.isArray(normalized.tracks) || normalized.tracks.length === 0) {
|
|
428
|
+
normalized.tracks = [
|
|
429
|
+
{ role: 'pad', instrument: 'Atmospheric Pad', density: 0.7 },
|
|
430
|
+
{ role: 'strings', instrument: 'Warm Strings', density: 0.5 },
|
|
431
|
+
{ role: 'keys', instrument: 'Soft Piano', density: 0.4 },
|
|
432
|
+
{ role: 'bass', instrument: 'Sub Bass', density: 0.3 },
|
|
433
|
+
{ role: 'fx', instrument: 'Drone FX', density: 0.2 }
|
|
434
|
+
];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return normalized;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
_prepareValidatedScorePlan(plan, prompt, stage = 'unknown') {
|
|
441
|
+
const normalized = this._normalizeScorePlan(plan, prompt);
|
|
442
|
+
const schema = this._loadScorePlanSchema();
|
|
443
|
+
const sanitized = this._sanitizeScorePlanToSchemaSubset(normalized, schema);
|
|
444
|
+
const before = this._validateScorePlanStrict(sanitized);
|
|
445
|
+
|
|
446
|
+
if (before.valid) {
|
|
447
|
+
return {
|
|
448
|
+
stage,
|
|
449
|
+
validBefore: true,
|
|
450
|
+
validAfter: true,
|
|
451
|
+
fallbackApplied: false,
|
|
452
|
+
errorsBefore: [],
|
|
453
|
+
errorsAfter: [],
|
|
454
|
+
plan: sanitized
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const fallbackPlan = this._buildFallbackScorePlan(prompt, sanitized);
|
|
459
|
+
const fallbackSanitized = this._sanitizeScorePlanToSchemaSubset(fallbackPlan, schema);
|
|
460
|
+
const after = this._validateScorePlanStrict(fallbackSanitized);
|
|
461
|
+
|
|
462
|
+
if (!after.valid) {
|
|
463
|
+
this.log('warn', 'Fallback score plan still failed strict validation', {
|
|
464
|
+
stage,
|
|
465
|
+
errors: after.errors
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
stage,
|
|
471
|
+
validBefore: false,
|
|
472
|
+
validAfter: after.valid,
|
|
473
|
+
fallbackApplied: true,
|
|
474
|
+
errorsBefore: before.errors,
|
|
475
|
+
errorsAfter: after.errors,
|
|
476
|
+
plan: fallbackSanitized
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
_sanitizeScorePlanToSchemaSubset(plan, _schema = null) {
|
|
481
|
+
const src = (plan && typeof plan === 'object') ? plan : {};
|
|
482
|
+
|
|
483
|
+
const topAllowed = new Set([
|
|
484
|
+
'schema_version', 'request_id', 'prompt', 'bpm', 'key', 'mode',
|
|
485
|
+
'time_signature', 'genre', 'mood', 'influences', 'seed', 'duration_bars',
|
|
486
|
+
'sections', 'chord_map', 'tension_curve', 'cue_points', 'tracks', 'constraints'
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
const out = {};
|
|
490
|
+
for (const [key, value] of Object.entries(src)) {
|
|
491
|
+
if (topAllowed.has(key)) out[key] = value;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (Array.isArray(out.time_signature)) {
|
|
495
|
+
out.time_signature = out.time_signature.slice(0, 2).map(v => Number(v));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (Array.isArray(out.sections)) {
|
|
499
|
+
out.sections = out.sections
|
|
500
|
+
.filter(s => s && typeof s === 'object')
|
|
501
|
+
.map(s => ({
|
|
502
|
+
name: s.name,
|
|
503
|
+
type: s.type,
|
|
504
|
+
bars: Number(s.bars),
|
|
505
|
+
energy: s.energy !== undefined ? Number(s.energy) : undefined,
|
|
506
|
+
tension: s.tension !== undefined ? Number(s.tension) : undefined
|
|
507
|
+
}));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (Array.isArray(out.tracks)) {
|
|
511
|
+
out.tracks = out.tracks
|
|
512
|
+
.filter(t => t && typeof t === 'object')
|
|
513
|
+
.map(t => ({
|
|
514
|
+
role: t.role,
|
|
515
|
+
instrument: t.instrument,
|
|
516
|
+
pattern_hint: t.pattern_hint,
|
|
517
|
+
octave: t.octave !== undefined ? Number(t.octave) : undefined,
|
|
518
|
+
density: t.density !== undefined ? Number(t.density) : undefined,
|
|
519
|
+
activation: Array.isArray(t.activation)
|
|
520
|
+
? t.activation
|
|
521
|
+
.filter(a => a && typeof a === 'object')
|
|
522
|
+
.map(a => ({ section: a.section, active: !!a.active }))
|
|
523
|
+
: undefined
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (Array.isArray(out.chord_map)) {
|
|
528
|
+
out.chord_map = out.chord_map
|
|
529
|
+
.filter(c => c && typeof c === 'object')
|
|
530
|
+
.map(c => ({ bar: Number(c.bar), chord: c.chord }));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (Array.isArray(out.cue_points)) {
|
|
534
|
+
out.cue_points = out.cue_points
|
|
535
|
+
.filter(c => c && typeof c === 'object')
|
|
536
|
+
.map(c => ({
|
|
537
|
+
bar: Number(c.bar),
|
|
538
|
+
type: c.type,
|
|
539
|
+
intensity: c.intensity !== undefined ? Number(c.intensity) : undefined
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (out.constraints && typeof out.constraints === 'object') {
|
|
544
|
+
out.constraints = {
|
|
545
|
+
avoid_instruments: Array.isArray(out.constraints.avoid_instruments) ? out.constraints.avoid_instruments : undefined,
|
|
546
|
+
avoid_drums: Array.isArray(out.constraints.avoid_drums) ? out.constraints.avoid_drums : undefined,
|
|
547
|
+
max_polyphony: out.constraints.max_polyphony !== undefined ? Number(out.constraints.max_polyphony) : undefined
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const pruneUndefined = (obj) => {
|
|
552
|
+
if (Array.isArray(obj)) return obj.map(pruneUndefined);
|
|
553
|
+
if (obj && typeof obj === 'object') {
|
|
554
|
+
const cleaned = {};
|
|
555
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
556
|
+
if (v !== undefined) cleaned[k] = pruneUndefined(v);
|
|
557
|
+
}
|
|
558
|
+
return cleaned;
|
|
559
|
+
}
|
|
560
|
+
return obj;
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
return pruneUndefined(out);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
_validateScorePlanStrict(plan) {
|
|
567
|
+
const errors = [];
|
|
568
|
+
const allowedModes = new Set(['major', 'minor', 'dorian', 'phrygian', 'lydian', 'mixolydian', 'locrian']);
|
|
569
|
+
const allowedSectionTypes = new Set(['intro', 'verse', 'pre_chorus', 'chorus', 'drop', 'bridge', 'breakdown', 'outro']);
|
|
570
|
+
const allowedTrackRoles = new Set(['drums', 'bass', 'keys', 'lead', 'strings', 'fx', 'pad']);
|
|
571
|
+
const allowedCueTypes = new Set(['fill', 'build', 'drop', 'breakdown']);
|
|
572
|
+
|
|
573
|
+
const required = ['schema_version', 'prompt', 'bpm', 'key', 'mode', 'sections', 'tracks'];
|
|
574
|
+
for (const key of required) {
|
|
575
|
+
if (plan[key] === undefined || plan[key] === null) {
|
|
576
|
+
errors.push(`Missing required field: ${key}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (plan.schema_version !== 'score_plan_v1') {
|
|
581
|
+
errors.push('schema_version must be score_plan_v1');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (typeof plan.prompt !== 'string' || !plan.prompt.trim()) {
|
|
585
|
+
errors.push('prompt must be a non-empty string');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (typeof plan.bpm !== 'number' || Number.isNaN(plan.bpm) || plan.bpm < 30 || plan.bpm > 220) {
|
|
589
|
+
errors.push('bpm must be a number in [30,220]');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (typeof plan.key !== 'string' || !/^[A-G](#|b)?$/.test(plan.key)) {
|
|
593
|
+
errors.push('key must match ^[A-G](#|b)?$');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!allowedModes.has(plan.mode)) {
|
|
597
|
+
errors.push('mode must be one of the allowed modes');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (plan.time_signature !== undefined) {
|
|
601
|
+
const ts = plan.time_signature;
|
|
602
|
+
if (!Array.isArray(ts) || ts.length !== 2 || !Number.isInteger(ts[0]) || !Number.isInteger(ts[1]) || ts[0] < 1 || ts[1] < 1) {
|
|
603
|
+
errors.push('time_signature must be [int>=1, int>=1]');
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (!Array.isArray(plan.sections) || plan.sections.length < 1) {
|
|
608
|
+
errors.push('sections must be a non-empty array');
|
|
609
|
+
} else {
|
|
610
|
+
plan.sections.forEach((s, i) => {
|
|
611
|
+
if (!s || typeof s !== 'object') {
|
|
612
|
+
errors.push(`sections[${i}] must be an object`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (typeof s.name !== 'string' || !s.name) errors.push(`sections[${i}].name required`);
|
|
616
|
+
if (!allowedSectionTypes.has(s.type)) errors.push(`sections[${i}].type invalid`);
|
|
617
|
+
if (!Number.isInteger(s.bars) || s.bars < 1) errors.push(`sections[${i}].bars must be int>=1`);
|
|
618
|
+
if (s.energy !== undefined && (typeof s.energy !== 'number' || s.energy < 0 || s.energy > 1)) {
|
|
619
|
+
errors.push(`sections[${i}].energy must be in [0,1]`);
|
|
620
|
+
}
|
|
621
|
+
if (s.tension !== undefined && (typeof s.tension !== 'number' || s.tension < 0 || s.tension > 1)) {
|
|
622
|
+
errors.push(`sections[${i}].tension must be in [0,1]`);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!Array.isArray(plan.tracks) || plan.tracks.length < 1) {
|
|
628
|
+
errors.push('tracks must be a non-empty array');
|
|
629
|
+
} else {
|
|
630
|
+
plan.tracks.forEach((t, i) => {
|
|
631
|
+
if (!t || typeof t !== 'object') {
|
|
632
|
+
errors.push(`tracks[${i}] must be an object`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (!allowedTrackRoles.has(t.role)) errors.push(`tracks[${i}].role invalid`);
|
|
636
|
+
if (typeof t.instrument !== 'string' || !t.instrument) errors.push(`tracks[${i}].instrument required`);
|
|
637
|
+
if (t.density !== undefined && (typeof t.density !== 'number' || t.density < 0 || t.density > 1)) {
|
|
638
|
+
errors.push(`tracks[${i}].density must be in [0,1]`);
|
|
639
|
+
}
|
|
640
|
+
if (t.activation !== undefined) {
|
|
641
|
+
if (!Array.isArray(t.activation)) {
|
|
642
|
+
errors.push(`tracks[${i}].activation must be an array`);
|
|
643
|
+
} else {
|
|
644
|
+
t.activation.forEach((a, j) => {
|
|
645
|
+
if (!a || typeof a !== 'object') {
|
|
646
|
+
errors.push(`tracks[${i}].activation[${j}] must be object`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (typeof a.section !== 'string' || !a.section) errors.push(`tracks[${i}].activation[${j}].section required`);
|
|
650
|
+
if (typeof a.active !== 'boolean') errors.push(`tracks[${i}].activation[${j}].active must be boolean`);
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (plan.chord_map !== undefined) {
|
|
658
|
+
if (!Array.isArray(plan.chord_map)) {
|
|
659
|
+
errors.push('chord_map must be an array');
|
|
660
|
+
} else {
|
|
661
|
+
plan.chord_map.forEach((c, i) => {
|
|
662
|
+
if (!c || typeof c !== 'object') {
|
|
663
|
+
errors.push(`chord_map[${i}] must be object`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (!Number.isInteger(c.bar) || c.bar < 1) errors.push(`chord_map[${i}].bar must be int>=1`);
|
|
667
|
+
if (typeof c.chord !== 'string' || !c.chord) errors.push(`chord_map[${i}].chord required`);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (plan.cue_points !== undefined) {
|
|
673
|
+
if (!Array.isArray(plan.cue_points)) {
|
|
674
|
+
errors.push('cue_points must be an array');
|
|
675
|
+
} else {
|
|
676
|
+
plan.cue_points.forEach((c, i) => {
|
|
677
|
+
if (!c || typeof c !== 'object') {
|
|
678
|
+
errors.push(`cue_points[${i}] must be object`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (!Number.isInteger(c.bar) || c.bar < 1) errors.push(`cue_points[${i}].bar must be int>=1`);
|
|
682
|
+
if (!allowedCueTypes.has(c.type)) errors.push(`cue_points[${i}].type invalid`);
|
|
683
|
+
if (c.intensity !== undefined && (typeof c.intensity !== 'number' || c.intensity < 0 || c.intensity > 1)) {
|
|
684
|
+
errors.push(`cue_points[${i}].intensity must be in [0,1]`);
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (plan.constraints !== undefined) {
|
|
691
|
+
const c = plan.constraints;
|
|
692
|
+
if (!c || typeof c !== 'object' || Array.isArray(c)) {
|
|
693
|
+
errors.push('constraints must be an object');
|
|
694
|
+
} else if (c.max_polyphony !== undefined && (!Number.isInteger(c.max_polyphony) || c.max_polyphony < 1)) {
|
|
695
|
+
errors.push('constraints.max_polyphony must be int>=1');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return { valid: errors.length === 0, errors };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
_buildFallbackScorePlan(prompt, candidate = {}) {
|
|
703
|
+
const safePrompt = (candidate.prompt && String(candidate.prompt).trim()) || prompt || 'Music generation';
|
|
704
|
+
return {
|
|
705
|
+
schema_version: 'score_plan_v1',
|
|
706
|
+
prompt: safePrompt,
|
|
707
|
+
bpm: DEFAULT_BPM,
|
|
708
|
+
key: DEFAULT_KEY,
|
|
709
|
+
mode: DEFAULT_MODE,
|
|
710
|
+
time_signature: DEFAULT_TIME_SIGNATURE,
|
|
711
|
+
genre: typeof candidate.genre === 'string' ? candidate.genre : undefined,
|
|
712
|
+
mood: typeof candidate.mood === 'string' ? candidate.mood : undefined,
|
|
713
|
+
sections: [
|
|
714
|
+
{ name: 'Intro', type: 'intro', bars: 8, energy: 0.2, tension: 0.2 },
|
|
715
|
+
{ name: 'Verse', type: 'verse', bars: 16, energy: 0.35, tension: 0.3 },
|
|
716
|
+
{ name: 'Chorus', type: 'chorus', bars: 16, energy: 0.6, tension: 0.5 },
|
|
717
|
+
{ name: 'Outro', type: 'outro', bars: 8, energy: 0.2, tension: 0.2 }
|
|
718
|
+
],
|
|
719
|
+
tracks: [
|
|
720
|
+
{ role: 'pad', instrument: 'Atmospheric Pad', density: 0.7 },
|
|
721
|
+
{ role: 'strings', instrument: 'Warm Strings', density: 0.5 },
|
|
722
|
+
{ role: 'keys', instrument: 'Soft Piano', density: 0.4 },
|
|
723
|
+
{ role: 'bass', instrument: 'Sub Bass', density: 0.3 },
|
|
724
|
+
{ role: 'fx', instrument: 'Drone FX', density: 0.2 }
|
|
725
|
+
]
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
_extractJson(text) {
|
|
730
|
+
if (!text || typeof text !== 'string') return null;
|
|
731
|
+
const stripped = text.trim().replace(/^```json/i, '').replace(/^```/i, '').replace(/```$/i, '').trim();
|
|
732
|
+
if (stripped.startsWith('{') && stripped.endsWith('}')) {
|
|
733
|
+
return stripped;
|
|
734
|
+
}
|
|
735
|
+
const start = stripped.indexOf('{');
|
|
736
|
+
if (start === -1) return null;
|
|
737
|
+
let depth = 0;
|
|
738
|
+
for (let i = start; i < stripped.length; i++) {
|
|
739
|
+
const ch = stripped[i];
|
|
740
|
+
if (ch === '{') depth += 1;
|
|
741
|
+
if (ch === '}') {
|
|
742
|
+
depth -= 1;
|
|
743
|
+
if (depth === 0) {
|
|
744
|
+
return stripped.slice(start, i + 1);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
_resolveReferenceInput(prompt, context = {}) {
|
|
752
|
+
if (context.referenceUrl && typeof context.referenceUrl === 'string') {
|
|
753
|
+
return context.referenceUrl.trim();
|
|
754
|
+
}
|
|
755
|
+
if (context.referencePath && typeof context.referencePath === 'string') {
|
|
756
|
+
return context.referencePath.trim();
|
|
757
|
+
}
|
|
758
|
+
if (context.reference && typeof context.reference === 'string') {
|
|
759
|
+
return context.reference.trim();
|
|
760
|
+
}
|
|
761
|
+
return this._extractFirstUrl(prompt);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
_resolveModelPolicy(context = {}) {
|
|
765
|
+
const policy = context.modelPolicy && typeof context.modelPolicy === 'object'
|
|
766
|
+
? context.modelPolicy
|
|
767
|
+
: {};
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
director: policy.director || context.directorModel || DEFAULT_DIRECTOR_MODEL,
|
|
771
|
+
producer: policy.producer || context.producerModel || DEFAULT_PRODUCER_MODEL,
|
|
772
|
+
verifier: policy.verifier || context.verifierModel || DEFAULT_VERIFIER_MODEL
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
_extractFirstUrl(text) {
|
|
777
|
+
if (!text || typeof text !== 'string') return null;
|
|
778
|
+
const match = text.match(/https?:\/\/[^\s)]+/i);
|
|
779
|
+
return match ? match[0] : null;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async _analyzeReference(referenceInput) {
|
|
783
|
+
if (!referenceInput) return null;
|
|
784
|
+
try {
|
|
785
|
+
const bridge = await this.ensurePythonBridge();
|
|
786
|
+
const key = /^https?:\/\//i.test(referenceInput) ? 'url' : 'file_path';
|
|
787
|
+
const profile = await bridge.call('analyze_reference', {
|
|
788
|
+
[key]: referenceInput,
|
|
789
|
+
include_genre_in_hints: false
|
|
790
|
+
}, 120000);
|
|
791
|
+
this.log('info', 'Reference analysis complete', {
|
|
792
|
+
source: referenceInput,
|
|
793
|
+
bpm: profile?.bpm,
|
|
794
|
+
key: profile?.key,
|
|
795
|
+
mode: profile?.mode
|
|
796
|
+
});
|
|
797
|
+
return profile;
|
|
798
|
+
} catch (error) {
|
|
799
|
+
this.log('warn', 'Reference analysis failed; continuing without it', {
|
|
800
|
+
source: referenceInput,
|
|
801
|
+
error: error.message
|
|
802
|
+
});
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async _draftDirectorGuidance(prompt, referenceProfile, directorModel) {
|
|
808
|
+
const referenceContext = this._formatReferenceContext(referenceProfile);
|
|
809
|
+
const instruction = `You are the Director role. Produce concise creative direction for song planning (not JSON).
|
|
810
|
+
Prompt: ${prompt}
|
|
811
|
+
|
|
812
|
+
${referenceContext}
|
|
813
|
+
|
|
814
|
+
Return 6-10 bullet points covering: form, energy arc, rhythm feel, harmony color, instrumentation priorities, and mix aesthetic.`;
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const response = await this.chat(instruction, { model: directorModel });
|
|
818
|
+
return response?.text || 'No director guidance available.';
|
|
819
|
+
} catch (error) {
|
|
820
|
+
this.log('warn', 'Director guidance failed; fallback to prompt-only planning', {
|
|
821
|
+
model: directorModel,
|
|
822
|
+
error: error.message
|
|
823
|
+
});
|
|
824
|
+
return 'Director guidance unavailable; use prompt and reference profile only.';
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
_formatReferenceContext(profile) {
|
|
829
|
+
if (!profile || typeof profile !== 'object') {
|
|
830
|
+
return 'Reference profile: none.';
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const compact = {
|
|
834
|
+
source: profile.source,
|
|
835
|
+
title: profile.title,
|
|
836
|
+
bpm: profile.bpm,
|
|
837
|
+
key: profile.key,
|
|
838
|
+
mode: profile.mode,
|
|
839
|
+
estimated_genre: profile.estimated_genre,
|
|
840
|
+
style_tags: profile.style_tags,
|
|
841
|
+
prompt_hints: profile.prompt_hints,
|
|
842
|
+
generation_params: profile.generation_params
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
return `Reference profile (ground truth from Python audio analysis):\n${JSON.stringify(compact, null, 2)}\nUse it to guide tempo/key/feel, but keep the final score plan coherent with the user prompt.`;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
_formatSuccessResponse(plan, generation, critics, attempt, extras = {}) {
|
|
849
|
+
const title = generation.title || generation.output_name || generation.output_filename || 'Generated track';
|
|
850
|
+
const midiPath = generation.midi_path || 'unknown';
|
|
851
|
+
const audioPath = generation.audio_path || generation.wav_path || 'unknown';
|
|
852
|
+
const criticsSummary = critics?.report?.summary || 'Critics passed.';
|
|
853
|
+
const preflightStatus = extras?.preflight?.passed === false ? 'FAIL' : 'PASS';
|
|
854
|
+
const outputScore = extras?.outputAnalysis && typeof extras.outputAnalysis.genre_match_score !== 'undefined'
|
|
855
|
+
? extras.outputAnalysis.genre_match_score
|
|
856
|
+
: 'n/a';
|
|
857
|
+
const outputPass = extras?.outputAnalysis && typeof extras.outputAnalysis.passed !== 'undefined'
|
|
858
|
+
? extras.outputAnalysis.passed
|
|
859
|
+
: 'n/a';
|
|
860
|
+
const criticBypassLine = extras?.criticGateBypassed ? '\nCritic Gate Bypass: enabled (generation accepted despite critic failure).' : '';
|
|
861
|
+
return `Producer completed in ${attempt} attempt(s).
|
|
862
|
+
Title: ${title}
|
|
863
|
+
Prompt: ${plan.prompt}
|
|
864
|
+
Key/Mode: ${plan.key} ${plan.mode}
|
|
865
|
+
BPM: ${plan.bpm}
|
|
866
|
+
MIDI: ${midiPath}
|
|
867
|
+
Audio: ${audioPath}
|
|
868
|
+
Preflight Gate: ${preflightStatus}
|
|
869
|
+
Critics: ${criticsSummary}
|
|
870
|
+
Output Analysis: passed=${outputPass}, genre_match_score=${outputScore}${criticBypassLine}`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
_formatFailureResponse(plan, generation, critics, attempts, extras = {}) {
|
|
874
|
+
const criticsSummary = critics?.report?.summary || 'Critics failed.';
|
|
875
|
+
const preflightStatus = extras?.preflight?.passed === false ? 'FAIL' : 'n/a';
|
|
876
|
+
const outputScore = extras?.outputAnalysis && typeof extras.outputAnalysis.genre_match_score !== 'undefined'
|
|
877
|
+
? extras.outputAnalysis.genre_match_score
|
|
878
|
+
: 'n/a';
|
|
879
|
+
const bypassHint = extras?.suggestBypass
|
|
880
|
+
? '\nTip: Use /produce --accept-generation <prompt> to accept generated output even when critics fail.'
|
|
881
|
+
: '';
|
|
882
|
+
return `Producer failed after ${attempts} attempt(s).
|
|
883
|
+
Prompt: ${plan?.prompt || 'unknown'}
|
|
884
|
+
Last result: ${generation?.midi_path || 'no midi'}
|
|
885
|
+
Preflight Gate: ${preflightStatus}
|
|
886
|
+
Critics: ${criticsSummary}
|
|
887
|
+
Output Analysis Score: ${outputScore}${bypassHint}`;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
module.exports = { ProducerAgent };
|