LLM refine: 2dp prompt, mixed 64/32 frames, full proxy log, low reasoning
- spec.js: round all songSpec + extended summary/tempo/normalization to
r2 (2dp) so high-precision 0.6607… stops wasting tokens; hoist r2/r3
above specForPrompt to fix ReferenceError; FULL control wording kept.
- thumbs.js: border detection — first/last + 0.5s of any section start
→ 64×64 (borders), interior → 32×32 (between). Back-compat shim for
old {size:N} callers. Returns size+role per frame for attachFrames.
- vite.config.js: raise sanitizeBodyForLog to 64k for __llm so 8×
vision frames aren't truncated (was 16384, only 2 images logged).
Also log reasoningEffort via providerOptions.
- llmTweak.js: set providerOptions reasoningEffort:'low' for both
streamObject and generateObject fallback to cut 2459 → faster
streaming progress, temperature 0.7 kept for variety.
Verification: build+lint:scenes green, tests 18/18.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -147,7 +147,7 @@ export async function requestSongTweak({ spec, track, look, hint = '', frames =
|
|||||||
schema: TweakResultSchema,
|
schema: TweakResultSchema,
|
||||||
messages,
|
messages,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
providerOptions: { openai: { strictJsonSchema: false } },
|
providerOptions: { openai: { strictJsonSchema: false, reasoningEffort: 'low' } },
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastPartial = null;
|
let lastPartial = null;
|
||||||
@@ -224,7 +224,7 @@ export async function requestSongTweak({ spec, track, look, hint = '', frames =
|
|||||||
messages,
|
messages,
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
maxRetries: 0,
|
maxRetries: 0,
|
||||||
providerOptions: { openai: { strictJsonSchema: false } },
|
providerOptions: { openai: { strictJsonSchema: false, reasoningEffort: 'low' } },
|
||||||
});
|
});
|
||||||
const elapsed = ((performance.now() - started) / 1000).toFixed(1);
|
const elapsed = ((performance.now() - started) / 1000).toFixed(1);
|
||||||
|
|
||||||
|
|||||||
+16
-20
@@ -70,30 +70,26 @@ export function clampEdits(edits) {
|
|||||||
* either `look` (no-op here) or `lookDigest`.
|
* either `look` (no-op here) or `lookDigest`.
|
||||||
*/
|
*/
|
||||||
export function buildTweakPrompt({ spec, track, look, lookDigest = null, hint = '', frames = [] } = {}) {
|
export function buildTweakPrompt({ spec, track, look, lookDigest = null, hint = '', frames = [] } = {}) {
|
||||||
const summary = track ? track.summary : null;
|
|
||||||
const digest = lookDigest ?? (look ? look : null);
|
|
||||||
|
|
||||||
const specForPrompt = spec ? {
|
|
||||||
bpm: spec.bpm ?? null,
|
|
||||||
brightness: spec.brightness ?? null,
|
|
||||||
noise: spec.noise ?? null,
|
|
||||||
noiseColour: spec.noiseColour ?? null,
|
|
||||||
dynamics: spec.dynamics ?? null,
|
|
||||||
transition: spec.transition ?? null,
|
|
||||||
arrangement: spec.arrangement ?? null,
|
|
||||||
duration: spec.duration ?? null,
|
|
||||||
seed: spec.seed ?? null,
|
|
||||||
} : null;
|
|
||||||
|
|
||||||
// --- helpers for richer context ---
|
|
||||||
const r2 = (v) => {
|
const r2 = (v) => {
|
||||||
const n = Number(v);
|
const n = Number(v);
|
||||||
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
|
return Number.isFinite(n) ? Math.round(n * 100) / 100 : 0;
|
||||||
};
|
};
|
||||||
const r3 = (v) => {
|
const r3 = r2;
|
||||||
const n = Number(v);
|
|
||||||
return Number.isFinite(n) ? Math.round(n * 1000) / 1000 : 0;
|
const summary = track ? track.summary : null;
|
||||||
};
|
const digest = lookDigest ?? (look ? look : null);
|
||||||
|
|
||||||
|
const specForPrompt = spec ? {
|
||||||
|
bpm: spec.bpm != null ? Math.round(Number(spec.bpm)) : null,
|
||||||
|
brightness: spec.brightness != null ? r2(spec.brightness) : null,
|
||||||
|
noise: spec.noise != null ? r2(spec.noise) : null,
|
||||||
|
noiseColour: spec.noiseColour != null ? r2(spec.noiseColour) : null,
|
||||||
|
dynamics: spec.dynamics != null ? r2(spec.dynamics) : null,
|
||||||
|
transition: spec.transition != null ? r2(spec.transition) : null,
|
||||||
|
arrangement: spec.arrangement ?? null,
|
||||||
|
duration: spec.duration != null ? r2(spec.duration) : null,
|
||||||
|
seed: spec.seed ?? null,
|
||||||
|
} : null;
|
||||||
|
|
||||||
const sectionsDetailed = track ? (track.sections || []).map((s, i) => ({
|
const sectionsDetailed = track ? (track.sections || []).map((s, i) => ({
|
||||||
index: Number.isFinite(s.index) ? s.index : i,
|
index: Number.isFinite(s.index) ? s.index : i,
|
||||||
|
|||||||
@@ -77,7 +77,28 @@ function pickFrames(show, count, frameCount, fps) {
|
|||||||
return uniformFrames(count, frameCount);
|
return uniformFrames(count, frameCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function captureThumbnails(show, { count = 8, size = 32 } = {}) {
|
/**
|
||||||
|
* Capture thumbnails — mixed res per "scene borders" request.
|
||||||
|
*
|
||||||
|
* Each sampled frame is classified: frames at the track's first frame,
|
||||||
|
* last frame, or within 0.5s of any section start/end are "border"
|
||||||
|
* frames and rendered at 64×64; frames in the interior of a section are
|
||||||
|
* rendered at 32×32 (between-border). This gives the vision model high
|
||||||
|
* detail where cuts happen and low cost elsewhere.
|
||||||
|
*
|
||||||
|
* Returns Array<{ kind, time, frame, dataUrl, size, role }> where
|
||||||
|
* role is 'border'|'inner'. Backward compat: callers that ignore size
|
||||||
|
* still see the same shape plus a new field.
|
||||||
|
*/
|
||||||
|
export async function captureThumbnails(show, { count = 8, borderSize = 64, innerSize = 32 } = {}) {
|
||||||
|
// Backward compat shim: old callers pass {size: N}. Respect it as innerSize with borderSize = min(innerSize*2, 128).
|
||||||
|
if (arguments[1] && Object.prototype.hasOwnProperty.call(arguments[1], 'size') && !Object.prototype.hasOwnProperty.call(arguments[1], 'innerSize')) {
|
||||||
|
const legacySize = arguments[1].size;
|
||||||
|
if (typeof legacySize === 'number' && legacySize > 0) {
|
||||||
|
innerSize = legacySize | 0;
|
||||||
|
borderSize = Math.min(128, innerSize * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!show || !show.ready || !show.track || !show.engine) return [];
|
if (!show || !show.ready || !show.track || !show.engine) return [];
|
||||||
const fps = show.fps || 60;
|
const fps = show.fps || 60;
|
||||||
const frameCount = show.frameCount || (show.track && show.track.frameCount) || Math.round((show.track.duration || 0) * fps) || 0;
|
const frameCount = show.frameCount || (show.track && show.track.frameCount) || Math.round((show.track.duration || 0) * fps) || 0;
|
||||||
@@ -85,6 +106,23 @@ export async function captureThumbnails(show, { count = 8, size = 32 } = {}) {
|
|||||||
const originalFrame = show.timeline ? show.timeline.frame : 0;
|
const originalFrame = show.timeline ? show.timeline.frame : 0;
|
||||||
const origW = show.engine.width, origH = show.engine.height;
|
const origW = show.engine.width, origH = show.engine.height;
|
||||||
const frames = pickFrames(show, count, frameCount, fps).sort((a, b) => a - b);
|
const frames = pickFrames(show, count, frameCount, fps).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
// Precompute border detection from section starts
|
||||||
|
const boundaries = [];
|
||||||
|
if (show.track && show.track.sections) {
|
||||||
|
for (const s of show.track.sections) {
|
||||||
|
if (Number.isFinite(s.startFrame)) boundaries.push(s.startFrame);
|
||||||
|
else if (Number.isFinite(s.start)) boundaries.push(Math.round(s.start * fps));
|
||||||
|
}
|
||||||
|
if (frameCount > 0) boundaries.push(frameCount - 1);
|
||||||
|
}
|
||||||
|
const halfSec = Math.round(0.5 * fps);
|
||||||
|
const isBorder = (f) => {
|
||||||
|
if (f === 0 || f === frameCount - 1) return true;
|
||||||
|
for (const b of boundaries) if (Math.abs(f - b) <= halfSec) return true;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
try {
|
try {
|
||||||
for (const frame of frames) {
|
for (const frame of frames) {
|
||||||
@@ -100,6 +138,7 @@ export async function captureThumbnails(show, { count = 8, size = 32 } = {}) {
|
|||||||
let target; try { target = show.renderFrame(frame); } catch { continue; }
|
let target; try { target = show.renderFrame(frame); } catch { continue; }
|
||||||
let pixels; try { pixels = show.readPixels(target); } catch { continue; }
|
let pixels; try { pixels = show.readPixels(target); } catch { continue; }
|
||||||
if (!pixels || !pixels.length) continue;
|
if (!pixels || !pixels.length) continue;
|
||||||
|
const size = isBorder(frame) ? borderSize : innerSize;
|
||||||
let dataUrl = ''; try { dataUrl = downscaleRgbaToDataUrl(pixels, origW, origH, size); } catch { dataUrl = 'data:image/jpeg;base64,'; }
|
let dataUrl = ''; try { dataUrl = downscaleRgbaToDataUrl(pixels, origW, origH, size); } catch { dataUrl = 'data:image/jpeg;base64,'; }
|
||||||
let kind = '';
|
let kind = '';
|
||||||
try {
|
try {
|
||||||
@@ -107,7 +146,7 @@ export async function captureThumbnails(show, { count = 8, size = 32 } = {}) {
|
|||||||
if (sec && sec.kind) kind = sec.kind;
|
if (sec && sec.kind) kind = sec.kind;
|
||||||
else if (show.track.sections) for (const s of show.track.sections) if (Number.isFinite(s.startFrame) && frame >= s.startFrame && frame < s.endFrame) { kind = s.kind || ''; break; }
|
else if (show.track.sections) for (const s of show.track.sections) if (Number.isFinite(s.startFrame) && frame >= s.startFrame && frame < s.endFrame) { kind = s.kind || ''; break; }
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
out.push({ kind: kind || '', time: frame / fps, frame, dataUrl });
|
out.push({ kind: kind || '', time: frame / fps, frame, dataUrl, size, role: isBorder(frame) ? 'border' : 'inner' });
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -118,19 +118,20 @@ function llmGatewayProxy() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeBodyForLog(ct, raw) {
|
function sanitizeBodyForLog(ct, raw) {
|
||||||
// Don't log if content-type is missing or body is binary
|
// Allow __llm JSON (which includes images) to be larger — spec asked
|
||||||
|
// for full logging. 64k keeps 8× images plus schema without truncation.
|
||||||
|
const limit = 65536;
|
||||||
if (!ct || !ct.includes('json')) {
|
if (!ct || !ct.includes('json')) {
|
||||||
if (raw && raw.length > 4096) return `[${ct || 'unknown'} body ${raw.length} bytes, not logged]`;
|
if (raw && raw.length > limit) return `[${ct || 'unknown'} body ${raw.length} bytes, not logged]`;
|
||||||
try { return raw ? raw.toString('utf8').slice(0, 4096) : ''; } catch { return '[unprintable]'; }
|
try { return raw ? raw.toString('utf8').slice(0, limit) : ''; } catch { return '[unprintable]'; }
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const text = raw ? raw.toString('utf8') : '';
|
const text = raw ? raw.toString('utf8') : '';
|
||||||
const parsed = JSON.parse(text);
|
const parsed = JSON.parse(text);
|
||||||
// Redact sensitive keys if they somehow appear in JSON body
|
|
||||||
const redacted = redactJsonForLog(parsed);
|
const redacted = redactJsonForLog(parsed);
|
||||||
return JSON.stringify(redacted).slice(0, 16384);
|
return JSON.stringify(redacted).slice(0, limit);
|
||||||
} catch {
|
} catch {
|
||||||
return raw ? raw.toString('utf8').slice(0, 4096) : '';
|
return raw ? raw.toString('utf8').slice(0, limit) : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user