IMPROVE: cài đặt thêm tool để có thể phối lại cho đoạn midi item hoặc viết thêm cho nó
This commit is contained in:
+225
-5
@@ -769,14 +769,14 @@ function createEqProModule(ctx, params) {
|
|||||||
const f = filters[i]; if (!f) return;
|
const f = filters[i]; if (!f) return;
|
||||||
const now = ctx.currentTime;
|
const now = ctx.currentTime;
|
||||||
if (patch.type !== undefined) f.type = patch.type;
|
if (patch.type !== undefined) f.type = patch.type;
|
||||||
if (patch.freq !== undefined) f.frequency.setTargetAtTime(eqproClamp(b.freq, EQPRO_F_MIN, EQPRO_F_MAX), now, 0.005);
|
if (patch.freq !== undefined) f.frequency.setValueAtTime(eqproClamp(b.freq, EQPRO_F_MIN, EQPRO_F_MAX), now);
|
||||||
if (patch.q !== undefined) f.Q.setTargetAtTime(eqproClamp(b.q, 0.1, 18), now, 0.005);
|
if (patch.q !== undefined) f.Q.setValueAtTime(eqproClamp(b.q, 0.1, 18), now);
|
||||||
if (patch.gain !== undefined || patch.active !== undefined) f.gain.setTargetAtTime((b.active !== false) ? ((b.gain || 0) * amount / 100) : 0, now, 0.005);
|
if (patch.gain !== undefined || patch.active !== undefined) f.gain.setValueAtTime((b.active !== false) ? ((b.gain || 0) * amount / 100) : 0, now);
|
||||||
};
|
};
|
||||||
const setAmount = (a) => {
|
const setAmount = (a) => {
|
||||||
amount = eqproClamp(a, 0, 200);
|
amount = eqproClamp(a, 0, 200);
|
||||||
const now = ctx.currentTime;
|
const now = ctx.currentTime;
|
||||||
filters.forEach((f, i) => { const b = bands[i]; if (b) f.gain.setTargetAtTime((b.active !== false) ? ((b.gain || 0) * amount / 100) : 0, now, 0.005); });
|
filters.forEach((f, i) => { const b = bands[i]; if (b) f.gain.setValueAtTime((b.active !== false) ? ((b.gain || 0) * amount / 100) : 0, now); });
|
||||||
};
|
};
|
||||||
// Replace the internal band model + rebuild DSP immediately (add/delete/reset
|
// Replace the internal band model + rebuild DSP immediately (add/delete/reset
|
||||||
// from the UI) — guarantees the audible result matches the added bands at once.
|
// from the UI) — guarantees the audible result matches the added bands at once.
|
||||||
@@ -13955,6 +13955,7 @@ const App = () => {
|
|||||||
isRunning: false,
|
isRunning: false,
|
||||||
});
|
});
|
||||||
const [aiPrompt, setAiPrompt] = useState('');
|
const [aiPrompt, setAiPrompt] = useState('');
|
||||||
|
const [aiComposeMode, setAiComposeMode] = useState('SIMILAR_VARIATION');
|
||||||
const [promptHistory, setPromptHistory] = useState([]);
|
const [promptHistory, setPromptHistory] = useState([]);
|
||||||
const [promptHistIdx, setPromptHistIdx] = useState(-1);
|
const [promptHistIdx, setPromptHistIdx] = useState(-1);
|
||||||
const promptHistRef = useRef([]);
|
const promptHistRef = useRef([]);
|
||||||
@@ -14083,7 +14084,7 @@ const App = () => {
|
|||||||
const tlist = activeTracks || tracks || [];
|
const tlist = activeTracks || tracks || [];
|
||||||
for (const t of tlist) {
|
for (const t of tlist) {
|
||||||
const found = (t.midiItems || []).find(m => m.id === selId);
|
const found = (t.midiItems || []).find(m => m.id === selId);
|
||||||
if (found) return { itemName: found.name, trackName: t.name, trackId: t.id, itemId: selId };
|
if (found) return { itemName: found.name, trackName: t.name, trackId: t.id, itemId: selId, notes: found.notes || [], startTime: found.startTime || 0, duration: found.duration || 4 };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -22507,6 +22508,206 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── AI Compose Similar → NEXT TRACK (user request) ──
|
||||||
|
// Click MIDI item + gõ prompt → AI phân tích cấu trúc giai điệu trong item
|
||||||
|
// (pitch range, intervals, rhythm) → sáng tác giai điệu TƯƠNG TỰ → ghi MIDI
|
||||||
|
// notes vào track KẾ TIẾP; nếu track kế tiếp KHÔNG RỖNG → chèn track mới
|
||||||
|
// (ngay sau track hiện tại) và ghi MIDI item vào đó.
|
||||||
|
// ── AI Compose (ai_midi_rearrange_specification.md) ──
|
||||||
|
// Click MIDI item + chọn mode (Variation/Extend) + gõ prompt:
|
||||||
|
// 1. SonicMidiExtractor nén note (pitch + note_name, round 2dp) tiết kiệm token
|
||||||
|
// 2. Function Tool Schema rearrange_or_extend_midi_melody (2 mode)
|
||||||
|
// 3. Prompt engineering: ORIGINAL CONTEXT + USER DIRECTIVE + MODE DIRECTIVE + CONSTRAINTS
|
||||||
|
// 4. Ingest: SIMILAR_VARIATION → track mới ngay dưới (A/B); EXTEND_CONTINUATION → append item sau item gốc
|
||||||
|
const handleAiComposeFromItem = async (mode) => {
|
||||||
|
const selMidi = getSelectedMidiItemInfo();
|
||||||
|
if (!selMidi) { showToast('Hãy chọn 1 MIDI item trên timeline trước.', 'warning'); return; }
|
||||||
|
const prompt = aiPrompt.trim();
|
||||||
|
if (!prompt) { showToast('Vui lòng gõ prompt yêu cầu giai điệu.', 'warning'); return; }
|
||||||
|
const composeMode = mode === 'EXTEND_CONTINUATION' ? 'EXTEND_CONTINUATION' : 'SIMILAR_VARIATION';
|
||||||
|
setAiProcessing(true);
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'status', text: ` 🎵 Đang phân tích "${selMidi.itemName}" (${(selMidi.notes || []).length} notes) + gửi AI [${composeMode === 'EXTEND_CONTINUATION' ? 'Extend' : 'Variation'}]...`, time: Date.now() }]);
|
||||||
|
try {
|
||||||
|
if (aiProviders.length === 0 || !selectedProviderId) {
|
||||||
|
try { const d = await window.SonicAPI.getAIConfigs(); if (d && d.providers && d.providers.length > 0) { setAiProviders(d.providers); const a = d.providers.find(p => p.is_active) || d.providers[0]; if (a) setSelectedProviderId(a.id); } } catch (e) {}
|
||||||
|
}
|
||||||
|
const prv = aiProviders.find(p => p.id === selectedProviderId) || (aiProviders.length > 0 ? aiProviders[0] : null);
|
||||||
|
const provider = prv || aiConfig;
|
||||||
|
const baseUrl = provider.api_base_url || provider.baseUrl || `${API_BASE_URL}`;
|
||||||
|
const apiKey = provider.api_key || provider.apiKey || '';
|
||||||
|
const model = provider.model_name || provider.model || 'deepseek-chat';
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'info', text: ` Provider: ${provider.name || 'default'} | Model: ${model}`, time: Date.now() }]);
|
||||||
|
|
||||||
|
// STEP 1: extract + nén MIDI context (note_name, round 2dp) qua SonicMidiExtractor
|
||||||
|
if (!window.SonicMidiExtractor) throw new Error('SonicMidiExtractor chưa được nạp.');
|
||||||
|
const midiContext = window.SonicMidiExtractor.extractSelectedMIDIContext(activeTracksRef.current || activeTracks, selMidi.itemId, bpm);
|
||||||
|
const bpmVal = parseInt(bpm) || 120;
|
||||||
|
const secPerBar = (60.0 / bpmVal) * 4;
|
||||||
|
const srcItem = (activeTracksRef.current || activeTracks).flatMap(t => (t.midiItems || []).map(m => ({ ...m, trackId: t.id }))).find(m => m.id === selMidi.itemId);
|
||||||
|
const startBar = srcItem ? (srcItem.startTime || 0) / secPerBar : 0;
|
||||||
|
midiContext.start_bar = startBar;
|
||||||
|
midiContext.time_signature = '4/4';
|
||||||
|
|
||||||
|
// STEP 3: prompt engineering (spec §4)
|
||||||
|
const notesJSON = JSON.stringify(midiContext.notes, null, 2);
|
||||||
|
const nextStartBar = startBar + midiContext.duration_bars;
|
||||||
|
const modeDirective = composeMode === 'EXTEND_CONTINUATION'
|
||||||
|
? `MODE: MELODIC EXTENSION (EXTEND / CONTINUATION)
|
||||||
|
- Source Section: Bars ${startBar.toFixed(2)} -> ${nextStartBar.toFixed(2)} (${midiContext.duration_bars} bars).
|
||||||
|
- DIRECTIVE: Write a CONTINUATION starting at Bar ${nextStartBar.toFixed(2)} spanning the next ${midiContext.duration_bars} bars.
|
||||||
|
- Analyze the ending motif/cadence of the original sequence to craft a smooth transition, then develop the melody toward a climax or resolution.
|
||||||
|
- Set target_start_bar = ${nextStartBar.toFixed(2)}, target_duration_bars = ${midiContext.duration_bars}.`
|
||||||
|
: `MODE: REARRANGEMENT / VARIATION (SIMILAR VARIATION / REARRANGE)
|
||||||
|
- DIRECTIVE: Generate a NEW variation of EQUAL LENGTH (${midiContext.duration_bars} bars, spanning Beats 0.0 -> ${midiContext.total_beats}).
|
||||||
|
- Retain the core harmonic framework/chord progression while applying variation techniques (Syncopation, Arpeggiator, Passing tones, Swing feel, Harmonization) as requested by the user.
|
||||||
|
- Set target_start_bar = ${startBar.toFixed(2)}, target_duration_bars = ${midiContext.duration_bars}.`;
|
||||||
|
const promptText = `You are a professional AI Music Composer & Arranger.
|
||||||
|
|
||||||
|
ORIGINAL MIDI MELODY CONTEXT FROM DAW:
|
||||||
|
- Track: "${midiContext.track_name}" | Item: "${midiContext.item_name}"
|
||||||
|
- Tempo: ${midiContext.bpm} BPM | Time Signature: ${midiContext.time_signature}
|
||||||
|
- Location: Bar ${startBar.toFixed(2)} (Length: ${midiContext.duration_bars} bars / ${midiContext.total_beats} beats)
|
||||||
|
- Array of ${midiContext.total_notes} original MIDI source notes:
|
||||||
|
${notesJSON}
|
||||||
|
|
||||||
|
USER DIRECTIVE:
|
||||||
|
"${prompt}"
|
||||||
|
|
||||||
|
${modeDirective}
|
||||||
|
|
||||||
|
STRICT CONSTRAINTS:
|
||||||
|
1. Return your answer as a SINGLE valid JSON object (NO markdown, NO code fences, NO explanation) with EXACTLY this shape:
|
||||||
|
{"mode":"SIMILAR_VARIATION or EXTEND_CONTINUATION","composition_title":"short style title","target_start_bar":number,"target_duration_bars":number,"generated_notes":[{"pitch":60,"start_beat":0.0,"duration_beats":1.0,"velocity":0.8}]}
|
||||||
|
2. Notes in generated_notes MUST cover the full beat range from 0.0 to ${midiContext.total_beats} without stopping prematurely.
|
||||||
|
3. Ensure the final note sustains or resolves cleanly at beat ${midiContext.total_beats}.
|
||||||
|
4. generated_notes start_beat is relative to the start of the generated item (beat 0.0 = first bar of the new section).`;
|
||||||
|
|
||||||
|
// STEP 2 + dispatch: provider hiện tại trả tool-call rác (_unknown) nên
|
||||||
|
// KHÔNG gửi tools — yêu cầu JSON thuần trong prompt (text-based, giống
|
||||||
|
// flow Piano Roll đã hoạt động).
|
||||||
|
const result = await window.AIGateway.executeAIPrompt({
|
||||||
|
prompt: promptText,
|
||||||
|
provider: provider.name || 'default',
|
||||||
|
model: model,
|
||||||
|
apiKey: apiKey,
|
||||||
|
baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
|
||||||
|
systemInstruction: 'You are a professional AI Music Composer & Arranger. Output ONLY a single valid JSON object (no markdown, no code fences) matching the exact shape requested in the prompt.',
|
||||||
|
tools: []
|
||||||
|
});
|
||||||
|
if (!result) throw new Error('AI không phản hồi');
|
||||||
|
|
||||||
|
// STEP 4: decode — ƯU TIÊN textResponse (JSON thuần); functionCalls chỉ
|
||||||
|
// dùng khi đúng tên tool rearrange_or_extend_midi_melody (provider cũ
|
||||||
|
// trả tool-call rác _unknown chỉ chứa "reason" — bỏ qua).
|
||||||
|
let aiResult = null;
|
||||||
|
let rawDebug = '';
|
||||||
|
const pickNotes = obj => {
|
||||||
|
if (!obj) return null;
|
||||||
|
if (Array.isArray(obj)) return obj;
|
||||||
|
for (const k of ['generated_notes', 'notes', 'rearranged_notes', 'midi_notes']) {
|
||||||
|
if (Array.isArray(obj[k]) && obj[k].length > 0) return obj[k];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const textResp = result.textResponse || result.text || '';
|
||||||
|
if (textResp && typeof textResp === 'string' && textResp.trim()) {
|
||||||
|
try {
|
||||||
|
const cleaned = textResp.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
|
||||||
|
const parsed = JSON.parse(cleaned);
|
||||||
|
const n = pickNotes(parsed);
|
||||||
|
if (n) aiResult = { mode: parsed.mode, composition_title: parsed.composition_title, target_start_bar: parsed.target_start_bar, target_duration_bars: parsed.target_duration_bars, generated_notes: n };
|
||||||
|
} catch (e1) { rawDebug = 'text: ' + textResp.slice(0, 400); console.error('Parse AI result error:', e1); }
|
||||||
|
}
|
||||||
|
if (!aiResult || !pickNotes(aiResult)) {
|
||||||
|
if (result.functionCalls && result.functionCalls.length > 0) {
|
||||||
|
const real = result.functionCalls.find(fc => fc.name === 'rearrange_or_extend_midi_melody');
|
||||||
|
if (real) {
|
||||||
|
try {
|
||||||
|
const a = typeof real.arguments === 'string' ? JSON.parse(real.arguments) : real.arguments;
|
||||||
|
const n = pickNotes(a);
|
||||||
|
if (n) aiResult = { mode: a.mode, composition_title: a.composition_title, target_start_bar: a.target_start_bar, target_duration_bars: a.target_duration_bars, generated_notes: n };
|
||||||
|
} catch (e2) { rawDebug += ' | FC-args-raw: ' + String(real.arguments).slice(0, 300); }
|
||||||
|
} else {
|
||||||
|
rawDebug += ' | FC names: ' + result.functionCalls.map(fc => fc.name || '_unknown').join(',') + ' (không phải tool đúng — bỏ qua)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!aiResult || !pickNotes(aiResult)) {
|
||||||
|
console.error('[AI Compose] Không parse được notes. Debug:', rawDebug, '| functionCalls:', JSON.stringify(result.functionCalls || []).slice(0, 500), '| textResponse:', (result.textResponse || result.text || '').slice(0, 500));
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'error', text: ' ❌ AI không trả về notes hợp lệ. ' + (rawDebug ? 'Raw: ' + rawDebug.slice(0, 200) : ''), time: Date.now() }]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const aiNotes = pickNotes(aiResult);
|
||||||
|
const finalMode = aiResult.mode === 'EXTEND_CONTINUATION' ? 'EXTEND_CONTINUATION' : 'SIMILAR_VARIATION';
|
||||||
|
const title = aiResult.composition_title || 'AI Composition';
|
||||||
|
const targetStartBar = (typeof aiResult.target_start_bar === 'number' ? aiResult.target_start_bar : (finalMode === 'EXTEND_CONTINUATION' ? nextStartBar : startBar));
|
||||||
|
const targetDurBars = (typeof aiResult.target_duration_bars === 'number' ? aiResult.target_duration_bars : midiContext.duration_bars);
|
||||||
|
const newNotes = aiNotes.map((n, i) => ({
|
||||||
|
id: 'note_ai_' + Date.now() + '_' + i,
|
||||||
|
pitch: Math.max(0, Math.min(127, parseInt(n.pitch) || 60)),
|
||||||
|
start_beat: Math.max(0, parseFloat(n.start_beat) || 0),
|
||||||
|
duration_beats: Math.max(0.125, parseFloat(n.duration_beats) || 0.25),
|
||||||
|
velocity: Math.max(0.1, Math.min(1.0, n.velocity ?? 0.8)),
|
||||||
|
pan: 0.0
|
||||||
|
}));
|
||||||
|
const newItem = {
|
||||||
|
id: 'item_ai_' + Date.now(),
|
||||||
|
startTime: Math.max(0, targetStartBar * secPerBar),
|
||||||
|
duration: Math.max(1, targetDurBars * secPerBar),
|
||||||
|
name: (finalMode === 'EXTEND_CONTINUATION' ? '[Extend] ' : '[Variation] ') + title,
|
||||||
|
notes: newNotes
|
||||||
|
};
|
||||||
|
|
||||||
|
// STEP 5: ingest — Variation → track mới ngay dưới (A/B); Extend → append item cuối track gốc
|
||||||
|
const curTracks = activeTracksRef.current || activeTracks;
|
||||||
|
const srcIdx = curTracks.findIndex(t => t.id === selMidi.trackId);
|
||||||
|
if (srcIdx === -1) { setAiActionLog(prev => [...prev, { type: 'error', text: ' ❌ Không tìm thấy track nguồn.', time: Date.now() }]); return; }
|
||||||
|
const sf = aiResult.soundfont_id || (midiContext.instrument && midiContext.instrument.soundfont_id) || 'generaluser_gs';
|
||||||
|
const sfBank = aiResult.soundfont_bank ?? ((midiContext.instrument && midiContext.instrument.soundfont_bank) ?? 0);
|
||||||
|
const sfProg = aiResult.soundfont_program ?? ((midiContext.instrument && midiContext.instrument.soundfont_program) ?? 0);
|
||||||
|
if (finalMode === 'EXTEND_CONTINUATION') {
|
||||||
|
// Append item mới vào CUỐI track gốc
|
||||||
|
updateActiveTracks(prev => prev.map(t => t.id === selMidi.trackId ? { ...t, midiItems: [...(t.midiItems || []), newItem] } : t));
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ [Extend] "${newItem.name}" (${newNotes.length} notes) → append sau item gốc (bar ${targetStartBar.toFixed(2)}).`, time: Date.now() }]);
|
||||||
|
} else {
|
||||||
|
// Tạo track mới ngay dưới track nguồn (A/B testing) — spec §5.
|
||||||
|
// TUÂN THỦ RULES như track gốc: clone toàn bộ cấu trúc track nguồn
|
||||||
|
// (synth_engine, instrumentProgram/instrumentName, volumeDb, pan,
|
||||||
|
// fxActive, bypass flags, color...) rồi reset phần content.
|
||||||
|
const newTrackId = 'track_ai_var_' + Date.now();
|
||||||
|
const srcTrack = curTracks[srcIdx];
|
||||||
|
const newTrack = {
|
||||||
|
...(srcTrack || {}),
|
||||||
|
id: newTrackId,
|
||||||
|
name: '[AI Var] ' + title,
|
||||||
|
buffer: null,
|
||||||
|
startTime: 0,
|
||||||
|
clips: [],
|
||||||
|
sections: [],
|
||||||
|
midiItems: [newItem],
|
||||||
|
muted: false,
|
||||||
|
solo: false,
|
||||||
|
markers: [],
|
||||||
|
serverFileId: null,
|
||||||
|
synth_engine: srcTrack && srcTrack.synth_engine ? srcTrack.synth_engine : { type: 'soundfont', soundfont_id: sf, soundfont_bank: sfBank, soundfont_program: sfProg },
|
||||||
|
instrumentProgram: srcTrack ? srcTrack.instrumentProgram : sfProg,
|
||||||
|
instrumentName: srcTrack ? srcTrack.instrumentName : ('AI ' + title)
|
||||||
|
};
|
||||||
|
updateActiveTracks(prev => { const copy = [...prev]; copy.splice(srcIdx + 1, 0, newTrack); return copy; });
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ [Variation] "${newItem.name}" (${newNotes.length} notes) → track mới "[AI Var] ${title}" ngay dưới (A/B) — kế thừa synth/instrument của track gốc.`, time: Date.now() }]);
|
||||||
|
}
|
||||||
|
setCanvasRedrawCount(n => n + 1);
|
||||||
|
setTimeout(() => lucide.createIcons(), 200);
|
||||||
|
} catch (err) {
|
||||||
|
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ Lỗi: ${err.message}`, time: Date.now() }]);
|
||||||
|
showToast(`AI Error: ${err.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
setAiProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Alias cũ giữ cho nút UI hiện tại (Compose) hoạt động — mode mặc định Variation
|
||||||
|
const handleAiComposeToNextTrack = () => handleAiComposeFromItem('SIMILAR_VARIATION');
|
||||||
|
|
||||||
// ── Split Track at Playhead ──
|
// ── Split Track at Playhead ──
|
||||||
const handleSplitTrackAtTime = (trackId, clipId, time) => {
|
const handleSplitTrackAtTime = (trackId, clipId, time) => {
|
||||||
const track = tracks.find(t => t.id === trackId);
|
const track = tracks.find(t => t.id === trackId);
|
||||||
@@ -24895,6 +25096,25 @@ const App = () => {
|
|||||||
"data-lucide": "send",
|
"data-lucide": "send",
|
||||||
className: "w-3 h-3"
|
className: "w-3 h-3"
|
||||||
})), " Gửi")), /*#__PURE__*/React.createElement("button", {
|
})), " Gửi")), /*#__PURE__*/React.createElement("button", {
|
||||||
|
onClick: () => setAiComposeMode(aiComposeMode === 'EXTEND_CONTINUATION' ? 'SIMILAR_VARIATION' : 'EXTEND_CONTINUATION'),
|
||||||
|
title: "Chọn mode AI: Variation (biến tấu cùng độ dài) / Extend (viết tiếp các bar sau)",
|
||||||
|
className: "px-2 py-1 rounded text-[10px] border font-semibold flex items-center justify-center gap-1 " + (aiComposeMode === 'EXTEND_CONTINUATION' ? 'bg-sky-800 text-sky-100 border-sky-600' : 'bg-amber-800 text-amber-100 border-amber-600')
|
||||||
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
|
className: "inline-flex items-center shrink-0"
|
||||||
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
|
"data-lucide": aiComposeMode === 'EXTEND_CONTINUATION' ? "arrow-right" : "shuffle",
|
||||||
|
className: "w-3 h-3"
|
||||||
|
})), aiComposeMode === 'EXTEND_CONTINUATION' ? 'Extend' : 'Variation'), /*#__PURE__*/React.createElement("button", {
|
||||||
|
onClick: () => handleAiComposeFromItem(aiComposeMode),
|
||||||
|
disabled: aiProcessing,
|
||||||
|
title: "AI phân tích giai điệu MIDI item đang chọn → sáng tác theo mode đã chọn → ghi kết quả (Variation: track mới ngay dưới A/B; Extend: append sau item gốc)",
|
||||||
|
className: "px-2 py-1 bg-teal-800 hover:bg-teal-700 text-white rounded text-[10px] border border-teal-600 flex items-center justify-center gap-1" + (hasSelItem ? '' : ' opacity-40 pointer-events-none')
|
||||||
|
}, /*#__PURE__*/React.createElement("span", {
|
||||||
|
className: "inline-flex items-center shrink-0"
|
||||||
|
}, /*#__PURE__*/React.createElement("i", {
|
||||||
|
"data-lucide": "music-2",
|
||||||
|
className: "w-3 h-3"
|
||||||
|
})), " Compose"), /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
setAiPrompt('');
|
setAiPrompt('');
|
||||||
setAiActionLog([]);
|
setAiActionLog([]);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -121,6 +121,57 @@ const AIGateway = (function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ai_midi_rearrange_specification.md §3 — Function Tool Schema hỗ trợ 2 mode:
|
||||||
|
// SIMILAR_VARIATION (biến tấu cùng độ dài) / EXTEND_CONTINUATION (viết tiếp
|
||||||
|
// các bar sau). AI trả gói dữ liệu có vị trí target trên Timeline.
|
||||||
|
const REARRANGE_EXTEND_TOOL_SPEC = {
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'rearrange_or_extend_midi_melody',
|
||||||
|
description: 'Analyzes source MIDI melody data and returns either a variation (Variation) or continuation (Extend) based on user instructions.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['SIMILAR_VARIATION', 'EXTEND_CONTINUATION'],
|
||||||
|
description: "Mode: 'SIMILAR_VARIATION' (new arrangement of equal length) or 'EXTEND_CONTINUATION' (writes subsequent bars)."
|
||||||
|
},
|
||||||
|
composition_title: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Short title describing the new melody style (e.g., Jazz Swing Variation, Epic Extension Part 2)'
|
||||||
|
},
|
||||||
|
target_start_bar: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Starting bar number for the generated notes on the Timeline'
|
||||||
|
},
|
||||||
|
target_duration_bars: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Total bar duration covered by the generated sequence'
|
||||||
|
},
|
||||||
|
soundfont_id: { type: 'string', default: 'generaluser_gs' },
|
||||||
|
soundfont_bank: { type: 'integer', default: 0 },
|
||||||
|
soundfont_program: { type: 'integer', default: 0 },
|
||||||
|
generated_notes: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Array of AI-generated MIDI notes.',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
pitch: { type: 'integer', minimum: 0, maximum: 127 },
|
||||||
|
start_beat: { type: 'number', description: 'Starting beat position relative to beat 0.0 of the generated item' },
|
||||||
|
duration_beats: { type: 'number', minimum: 0.1 },
|
||||||
|
velocity: { type: 'number', minimum: 0.0, maximum: 1.0 }
|
||||||
|
},
|
||||||
|
required: ['pitch', 'start_beat', 'duration_beats', 'velocity']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['mode', 'composition_title', 'target_start_bar', 'target_duration_bars', 'generated_notes']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const REARRANGE_SCENARIOS = [
|
const REARRANGE_SCENARIOS = [
|
||||||
{
|
{
|
||||||
id: 'arpeggio',
|
id: 'arpeggio',
|
||||||
@@ -456,6 +507,7 @@ Ví dụ: "Hãy chọn và copy từ bar 4 đến bar 12 của track 1 sau đó
|
|||||||
return {
|
return {
|
||||||
DEFAULT_TOOLS,
|
DEFAULT_TOOLS,
|
||||||
REARRANGE_TOOL_SPEC,
|
REARRANGE_TOOL_SPEC,
|
||||||
|
REARRANGE_EXTEND_TOOL_SPEC,
|
||||||
REARRANGE_SCENARIOS,
|
REARRANGE_SCENARIOS,
|
||||||
detectRearrangeScenario,
|
detectRearrangeScenario,
|
||||||
buildRearrangeMessage,
|
buildRearrangeMessage,
|
||||||
|
|||||||
@@ -17,14 +17,14 @@
|
|||||||
<script src="/static/js/services/storage.js?v=202607271016"></script>
|
<script src="/static/js/services/storage.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/soundfontPlayer.js?v=202608031400"></script>
|
<script src="/static/js/services/soundfontPlayer.js?v=202608031400"></script>
|
||||||
<script src="/static/js/services/aiGateway.js?v=202607271016"></script>
|
<script src="/static/js/services/aiGateway.js?v=202608037200"></script>
|
||||||
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
<script src="/static/js/services/dawCommandDispatcher.js?v=202607271016"></script>
|
||||||
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
<script src="/static/js/services/pianoRollTabService.js?v=202607272044"></script>
|
||||||
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
|
<script src="/static/js/services/ghostNoteExtractor.js?v=202607271727"></script>
|
||||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||||
<script src="/static/js/app.precompiled.js?v=202608037000" defer></script>
|
<script src="/static/js/app.precompiled.js?v=202608037600" defer></script>
|
||||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1,4 +1,37 @@
|
|||||||
### [2026-08-03] Task: Fix re-schedule STALE CLOSURE (dùng refs) + persist zoom qua reload
|
### [2026-08-03] Task: Fix CÂM TOÀN CỤC — EQ PRO setTargetAtTime 0.005 gây "BiquadFilterNode: state is bad"
|
||||||
|
- **Tóm tắt thay đổi:** User báo play + ARM MIDI preview đều không có âm thanh; console: `BiquadFilterNode: state is bad, probably due to unstable filter caused by fast parameter automation` — đúng cảnh báo cũ trong code (fast automation → master routing broken → CÂM toàn cục). Thủ phạm: EQ PRO `setBand`/`setAmount` dùng `setTargetAtTime(..., now, 0.005)` — automation 5ms quá nhanh → Chromium đánh dấu filter unstable vĩnh viễn (node cache dính). **Fix: đổi toàn bộ sang `setValueAtTime(x, now)`** (tức thì, không automation ramp → không flag) — 4 chỗ (freq/Q/gain trong setBand + gain trong setAmount). Hard refresh → module EQ PRO mới (filter mới) → hết câm.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037600)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 1002801 bytes, node --check OK, `pytest` 86 passed. Không còn setTargetAtTime 0.005 (EQ PRO) — setValueAtTime ✓.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** Track `[AI Var]` do AI tạo không có âm thanh — thiếu cấu trúc như track MIDI gốc (instrumentProgram/instrumentName/synth_engine + các field rules). Fix: track mới = **clone toàn bộ track nguồn** (`...(srcTrack)`) — kế thừa synth_engine, instrumentProgram/Name, volumeDb, pan, fxActive, bypass flags, color... — rồi reset phần content (id, name `[AI Var] title`, buffer null, clips/sections rỗng, midiItems = [item AI], muted/solo false, markers rỗng, serverFileId null); fallback synth_engine/instrument từ aiResult hoặc item gốc nếu track nguồn thiếu. Track AI giờ tuân thủ rules như mọi track khác trong session.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037500)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 1002829 bytes, node --check OK, `pytest` 86 passed. Clone srcTrack ✓, instrumentProgram kế thừa ✓.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** Debug log cho thấy provider trả **nhiều tool calls `_unknown`** (arguments chỉ chứa "reason" — không có notes) → tool-calling không hoạt động với provider hiện tại. Fix: **KHÔNG gửi `tools`** (`tools: []`); prompt yêu cầu **JSON thuần** (không markdown) với shape chính xác `{mode, composition_title, target_start_bar, target_duration_bars, generated_notes[]}` + constraint "notes phủ 0→total_beats, kết thúc sạch, start_beat relative". Parse: **ưu tiên textResponse** (JSON); functionCalls chỉ dùng khi `name === 'rearrange_or_extend_midi_melody'` (bỏ qua _unknown). Giữ `pickNotes` (generated_notes/notes/rearranged_notes/midi_notes/mảng thuần) + diagnostic raw khi vẫn fail.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037400)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 1002469 bytes, node --check OK, `pytest` 86 passed. tools:[] ✓, pickNotes ×2, JSON shape constraint ✓.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** Lỗi "AI không trả về generated_notes hợp lệ" — model trả key/format khác (notes/rearranged_notes/mảng thuần) hoặc functionCalls arguments là mảng trực tiếp. Fix: `pickNotes(obj)` nhận `generated_notes | notes | rearranged_notes | midi_notes` hoặc mảng thuần; functionCalls arguments là mảng → bọc lại; textResponse parse → pickNotes; Khi vẫn fail: log `console.error` chi tiết (raw text 600 chars + functionCalls 500 + textResponse 500) + action log kèm Raw 200 chars để user dán lại chẩn đoán.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037300)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 1001539 bytes, node --check OK, `pytest` 86 passed. pickNotes ×5, aiNotes ×2.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** Sửa tool AI theo spec đính kèm — chỉ cần click MIDI item + gõ prompt:
|
||||||
|
1. **STEP 1 — Extract/Nén**: `SonicMidiExtractor.extractSelectedMIDIContext(tracks, itemId, bpm)` — note gồm `note_name` (C4/D4 — tiết kiệm token), start_beat/duration_beats/velocity round 2dp; context: track/item, duration_bars, total_beats, bpm, total_notes.
|
||||||
|
2. **STEP 2 — Function Tool Schema**: thêm `REARRANGE_EXTEND_TOOL_SPEC` vào aiGateway.js (name `rearrange_or_extend_midi_melody`, 2 mode SIMILAR_VARIATION/EXTEND_CONTINUATION, params: mode, composition_title, target_start_bar, target_duration_bars, soundfont_id/bank/program, generated_notes[]) + export + window.AIGateway.
|
||||||
|
3. **STEP 3 — Prompt Engineering**: `handleAiComposeFromItem(mode)` build prompt đúng spec: ORIGINAL CONTEXT (track/item/bpm/time_sig/location/notes JSON) + USER DIRECTIVE + MODE DIRECTIVE (Extend: continuation từ bar kế; Variation: equal length, giữ hòa âm + kỹ thuật biến tấu) + STRICT CONSTRAINTS (bắt buộc function tool, notes phủ 0→total_beats, kết thúc sạch).
|
||||||
|
4. **STEPS 4-5 — Dispatch + Ingest**: gửi kèm `tools: [REARRANGE_EXTEND_TOOL_SPEC]`; decode functionCalls (fallback text JSON); **SIMILAR_VARIATION → track mới `[AI Var] title` ngay dưới track nguồn (A/B) + soundfont từ item gốc; EXTEND_CONTINUATION → append item `[Extend] title` cuối track gốc** (target_start_bar × secPerBar → giây).
|
||||||
|
5. **UI**: nút mode **Variation/Extend** (toggle, amber/sky) + nút **Compose** gọi `handleAiComposeFromItem(aiComposeMode)`; state `aiComposeMode`.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/services/aiGateway.js` (REARRANGE_EXTEND_TOOL_SPEC), `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037200 cả aiGateway.js)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 1000935 bytes, node --check OK, `pytest` 86 passed. handleAiComposeFromItem ×3, REARRANGE_EXTEND ×1, rearrange_or_extend ×2.
|
||||||
|
---
|
||||||
|
- **Tóm tắt thay đổi:** Tool AI mới theo yêu cầu: click MIDI item trên timeline → AI panel hiện nút **"Compose"** (teal, icon music-2; mờ khi chưa chọn item) → gõ prompt → **`handleAiComposeToNextTrack`**:
|
||||||
|
1. Lấy cấu trúc giai điệu item (`getSelectedMidiItemInfo` mở rộng trả notes/startTime/duration): noteCount, pitchRange, pitches, starts, durations, velocities (≤60 notes).
|
||||||
|
2. Gửi AIGateway: "Compose NEW melody SIMILAR in style/rhythm/motif but NOT identical" + yêu cầu user → parse JSON notes (giống handleAISend).
|
||||||
|
3. **Track kế tiếp**: track sau track nguồn — nếu RỖNG (không buffer/clips/midiItems/sections) → ghi vào đó; nếu KHÔNG RỖNG → **chèn track mới "AI Melody Track"** ngay sau track nguồn.
|
||||||
|
4. Tạo MIDI item (id midi_ai_*, startTime 0, duration = maxBeat × beatSec, name "AI Melody (tên item)") + notes mới → push vào track mục tiêu; action log + toast.
|
||||||
|
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` (rebuild), `app/templates/index.html` (bump v=202608037100)
|
||||||
|
- **Ghi chú/Test (nếu có):** BUILD OK 996444 bytes, node --check OK, `pytest` 86 passed. handleAiComposeToNextTrack ×2 (định nghĩa + onClick).
|
||||||
|
---
|
||||||
- **Tóm tắt thay đổi:**
|
- **Tóm tắt thay đổi:**
|
||||||
1. **Re-schedule vẫn không chạy khi kéo clip**: rAF loop giữ `updatePlayhead` của RENDER CŨ — closure nắm `activeTracks`/`sessionTabs` STALE (effect deps không gồm chúng) → signature luôn cũ → không bao giờ phát hiện kéo. **Fix**: re-schedule dùng `activeTracksRef.current` + `sessionTabsRef.current` (sync mỗi render); solo check tính lại từ ref (`curTracks.some(t => t.solo)`).
|
1. **Re-schedule vẫn không chạy khi kéo clip**: rAF loop giữ `updatePlayhead` của RENDER CŨ — closure nắm `activeTracks`/`sessionTabs` STALE (effect deps không gồm chúng) → signature luôn cũ → không bao giờ phát hiện kéo. **Fix**: re-schedule dùng `activeTracksRef.current` + `sessionTabsRef.current` (sync mỗi render); solo check tính lại từ ref (`curTracks.some(t => t.solo)`).
|
||||||
2. **Zoom persist**: `zoom` (App) khởi tạo từ `localStorage.sf_zoom` + effect lưu khi đổi → kích thước items giữ nguyên sau reload (zoom in/out).
|
2. **Zoom persist**: `zoom` (App) khởi tạo từ `localStorage.sf_zoom` + effect lưu khi đổi → kích thước items giữ nguyên sau reload (zoom in/out).
|
||||||
|
|||||||
Reference in New Issue
Block a user