LLM: streaming + rich context + 32×32 frames + proxy log
- Streaming: requestSongTweak now uses streamObject when a progress
callback is provided (via handleLlmTweak), emitting partial {rationale,
edits} and updating toast/console as text arrives; falls back to
generateObject if gateway doesn't support streaming. Models always
called via provider.chat(modelId) / /v1/chat/completions, the correct
path for meta/muse-spark-1.3-contributor via gateway.
- Rich context: spec.js gains FULL control wording (any amount/arrangement/
bpm allowed), and detailed payload — trackSummary with bandBalance,
tempo, normalization (per-feature lo/hi), sectionsDetailed (energy/
flux/centroid/flatness/width/slope/endEnergy/duration), stats
(kinds, avg/max energy, bars). LLM told You have FULL control.
- Low-res vision: new src/llm/thumbs.js — captureThumbnails(show, {8,32})
samples section midpoints, warms up state, renders via Show, box-averages
down to 32×32 JPEG data URLs, restores timeline. main.js now captures 8
frames with two animation frames of pause, then passes them as {type:
image, image: dataUrl} parts in the user message (llmTweak.js
attachFramesToMessages).
- Proxy logging (no bearer): vite.config.js llmGatewayProxy now logs
sanitized requests/responses to logs/llm-proxy.log (JSON lines, 1MB
rotation, redacts apiKey/authorization from path/body/headers) so the
coding agent can inspect prompts without re-calling. .gitignore adds
logs/.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+144
-20
@@ -69,15 +69,8 @@ export function clampEdits(edits) {
|
||||
* Kept compatible with the old `{spec,track,look,hint}` shape by accepting
|
||||
* either `look` (no-op here) or `lookDigest`.
|
||||
*/
|
||||
export function buildTweakPrompt({ spec, track, look, lookDigest = null, hint = '' } = {}) {
|
||||
export function buildTweakPrompt({ spec, track, look, lookDigest = null, hint = '', frames = [] } = {}) {
|
||||
const summary = track ? track.summary : null;
|
||||
const sections = track ? (track.sections || []).map((s) => ({
|
||||
kind: s.kind,
|
||||
start: Number(s.startSec != null ? s.startSec : s.start).toFixed(1),
|
||||
end: Number(s.endSec != null ? s.endSec : s.end).toFixed(1),
|
||||
energy: Number((s.energy ?? 0).toFixed(3)),
|
||||
})) : [];
|
||||
|
||||
const digest = lookDigest ?? (look ? look : null);
|
||||
|
||||
const specForPrompt = spec ? {
|
||||
@@ -92,36 +85,167 @@ export function buildTweakPrompt({ spec, track, look, lookDigest = null, hint =
|
||||
seed: spec.seed ?? null,
|
||||
} : null;
|
||||
|
||||
// --- helpers for richer context ---
|
||||
const r2 = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
|
||||
};
|
||||
const r3 = (v) => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? Math.round(n * 1000) / 1000 : 0;
|
||||
};
|
||||
|
||||
const sectionsDetailed = track ? (track.sections || []).map((s, i) => ({
|
||||
index: Number.isFinite(s.index) ? s.index : i,
|
||||
kind: s.kind || 'unknown',
|
||||
start: r2(s.startSec != null ? s.startSec : (s.start ?? 0)),
|
||||
end: r2(s.endSec != null ? s.endSec : (s.end ?? 0)),
|
||||
duration: r2(s.duration ?? ((s.endSec != null ? s.endSec : (s.end ?? 0)) - (s.startSec != null ? s.startSec : (s.start ?? 0)))),
|
||||
energy: r3(s.energy ?? 0),
|
||||
flux: r3(s.flux ?? 0),
|
||||
centroid: r3(s.centroid ?? 0),
|
||||
flatness: r3(s.flatness ?? 0),
|
||||
width: r3(s.width ?? 0),
|
||||
slope: r3(s.slope ?? 0),
|
||||
endEnergy: r3(s.endEnergy ?? 0),
|
||||
})) : [];
|
||||
|
||||
// trackSummary — extended
|
||||
let trackSummary = null;
|
||||
if (summary) {
|
||||
trackSummary = {
|
||||
bpm: summary.bpm ?? track?.tempo?.bpm ?? null,
|
||||
meanCentroid: summary.meanCentroid != null ? r3(summary.meanCentroid) : null,
|
||||
meanFlatness: summary.meanFlatness != null ? r3(summary.meanFlatness) : null,
|
||||
meanWidth: summary.meanWidth != null ? r3(summary.meanWidth) : undefined,
|
||||
meanLoudness: summary.meanLoudness != null ? r3(summary.meanLoudness) : null,
|
||||
dynamicRange: summary.dynamicRange != null ? r3(summary.dynamicRange) : null,
|
||||
tempoConfidence: summary.tempoConfidence != null ? r3(summary.tempoConfidence) : (track?.tempo?.confidence != null ? r3(track.tempo.confidence) : null),
|
||||
frameCount: track?.frameCount ?? summary.frameCount ?? null,
|
||||
fps: track?.fps ?? summary.fps ?? null,
|
||||
duration: r2(track?.duration ?? summary.duration ?? 0),
|
||||
sections: sectionsDetailed,
|
||||
};
|
||||
if (summary.bandBalance) {
|
||||
trackSummary.bandBalance = {
|
||||
sub: r3(summary.bandBalance.sub ?? 0),
|
||||
low: r3(summary.bandBalance.low ?? 0),
|
||||
mid: r3(summary.bandBalance.mid ?? 0),
|
||||
high: r3(summary.bandBalance.high ?? 0),
|
||||
air: r3(summary.bandBalance.air ?? 0),
|
||||
};
|
||||
}
|
||||
// remove undefined meanWidth if not present to keep payload compact
|
||||
if (trackSummary.meanWidth === undefined) delete trackSummary.meanWidth;
|
||||
} else if (sectionsDetailed.length || track) {
|
||||
// fallback when summary missing but track exists (e.g. synthetic test track)
|
||||
trackSummary = {
|
||||
bpm: track?.tempo?.bpm ?? null,
|
||||
frameCount: track?.frameCount ?? null,
|
||||
fps: track?.fps ?? null,
|
||||
duration: r2(track?.duration ?? 0),
|
||||
sections: sectionsDetailed,
|
||||
};
|
||||
if (track?.summary?.bandBalance) {
|
||||
trackSummary.bandBalance = {
|
||||
sub: r3(track.summary.bandBalance.sub ?? 0),
|
||||
low: r3(track.summary.bandBalance.low ?? 0),
|
||||
mid: r3(track.summary.bandBalance.mid ?? 0),
|
||||
high: r3(track.summary.bandBalance.high ?? 0),
|
||||
air: r3(track.summary.bandBalance.air ?? 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// tempo
|
||||
let tempo = null;
|
||||
if (track?.tempo) {
|
||||
tempo = {
|
||||
bpm: r2(track.tempo.bpm ?? summary?.bpm ?? 0),
|
||||
confidence: r3(track.tempo.confidence ?? 0),
|
||||
beatsPerBar: track.tempo.beatsPerBar ?? 4,
|
||||
downbeats: Array.isArray(track.tempo.downbeats) ? track.tempo.downbeats.length : 0,
|
||||
};
|
||||
} else if (summary?.bpm != null) {
|
||||
tempo = { bpm: r2(summary.bpm), confidence: r3(summary.tempoConfidence ?? 0), beatsPerBar: 4, downbeats: 0 };
|
||||
}
|
||||
|
||||
// normalization
|
||||
let normalization = null;
|
||||
if (track?.normalization && typeof track.normalization === 'object') {
|
||||
normalization = {};
|
||||
for (const [k, v] of Object.entries(track.normalization)) {
|
||||
if (v && typeof v === 'object' && v.lo != null && v.hi != null) {
|
||||
normalization[k] = { lo: r3(v.lo), hi: r3(v.hi) };
|
||||
}
|
||||
}
|
||||
if (!Object.keys(normalization).length) normalization = null;
|
||||
}
|
||||
|
||||
// stats — derived
|
||||
let stats = null;
|
||||
if (sectionsDetailed.length || tempo || track) {
|
||||
const kinds = [...new Set(sectionsDetailed.map((s) => s.kind))];
|
||||
const avgEnergy = sectionsDetailed.length
|
||||
? r3(sectionsDetailed.reduce((a, s) => a + s.energy, 0) / sectionsDetailed.length)
|
||||
: 0;
|
||||
let maxSec = null;
|
||||
let maxE = -Infinity;
|
||||
for (const s of sectionsDetailed) if (s.energy > maxE) { maxE = s.energy; maxSec = s; }
|
||||
const duration = track?.duration ?? summary?.duration ?? 0;
|
||||
const bpmVal = tempo?.bpm ?? summary?.bpm ?? 120;
|
||||
const bpb = tempo?.beatsPerBar ?? 4;
|
||||
const bars = bpmVal > 0 && duration > 0 ? Math.round(duration / ((60 / bpmVal) * bpb)) : 0;
|
||||
stats = {
|
||||
totalSections: sectionsDetailed.length,
|
||||
kinds,
|
||||
avgEnergy,
|
||||
maxEnergySection: maxSec ? { index: maxSec.index, kind: maxSec.kind, energy: maxSec.energy } : null,
|
||||
bpm: tempo?.bpm ?? null,
|
||||
beatsPerBar: tempo?.beatsPerBar ?? null,
|
||||
downbeats: tempo?.downbeats ?? 0,
|
||||
bars,
|
||||
duration: r2(duration),
|
||||
fps: track?.fps ?? summary?.fps ?? null,
|
||||
frameCount: track?.frameCount ?? summary?.frameCount ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
const hintBlock = hint && String(hint).trim()
|
||||
? `User hint: "${String(hint).trim().slice(0, 300)}"`
|
||||
: 'No user hint.';
|
||||
|
||||
const frameNote = Array.isArray(frames) && frames.length
|
||||
? `You are also given ${frames.length} low-res thumbnails (32×32) sampled across the song in order — use them to judge color, structure, and motion.`
|
||||
: null;
|
||||
|
||||
const system = [
|
||||
'You are a producer for a generative music-video system called flow-state.',
|
||||
'You suggest SMALL tweaks to the *song synthesis parameters* so the next generation looks better.',
|
||||
'You suggest tweaks to the *song synthesis parameters* so the next generation looks better.',
|
||||
'You do NOT edit the visualization directly — only the 7 song knobs below. The system re-synthesizes audio and regenerates the look.',
|
||||
'',
|
||||
'Rules:',
|
||||
'- Change at most 2-3 knobs, each by ≤0.15 (brightness/noise/dynamics/transition/noiseColour) or bpm ±8, or one arrangement step.',
|
||||
'- Prefer lowering agitation for breakdowns/intros; keep section count ≥3 if possible.',
|
||||
'You have FULL control — change any subset of the 7 knobs by any amount within valid ranges, including large jumps, arrangement swaps, or bpm shifts. Prefer impactful changes that clearly improve visuals; keep duration fixed.',
|
||||
'- Valid bpm 60-180 int, brightness/noise/dynamics/transition/noiseColour 0-1, arrangement one of: ' + ARRANGEMENT_VALUES.join(', ') + '.',
|
||||
'- noiseColour may be null (follows brightness) or 0-1; leave it null unless you have a reason.',
|
||||
'- All 7 edit fields MUST be present in edits — use null for unchanged (not omitted). Example: { bpm: null, brightness: 0.6, noise: null, noiseColour: null, dynamics: null, transition: null, arrangement: null }',
|
||||
'- Explain your reasoning in 1-2 sentences (rationale).',
|
||||
'- If the look is already good, return all-null edits.',
|
||||
hintBlock,
|
||||
].join('\n');
|
||||
frameNote,
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
const user = JSON.stringify({
|
||||
const userPayload = {
|
||||
songSpec: specForPrompt,
|
||||
trackSummary: summary ? {
|
||||
bpm: summary.bpm, meanCentroid: summary.meanCentroid, meanFlatness: summary.meanFlatness,
|
||||
dynamicRange: summary.dynamicRange, meanLoudness: summary.meanLoudness,
|
||||
tempoConfidence: summary.tempoConfidence,
|
||||
} : null,
|
||||
sections,
|
||||
trackSummary,
|
||||
sections: sectionsDetailed,
|
||||
tempo,
|
||||
normalization,
|
||||
lookDigest: digest,
|
||||
}, null, 2);
|
||||
stats,
|
||||
};
|
||||
|
||||
const user = JSON.stringify(userPayload, null, 2);
|
||||
|
||||
return [
|
||||
{ role: 'system', content: system },
|
||||
|
||||
Reference in New Issue
Block a user