fix: MIDI Rearrange — selected item triggers dedicated tool flow
- midiExtractor.js: extractSelectedMIDIContext with note name conversion - aiGateway.js: REARRANGE_TOOL_SPEC + buildRearrangeMessage - dawCommandDispatcher.js: register REARRANGE_MIDI_MELODY command - app.jsx: handleAISend detects selected MIDI item → rearrange flow - rearrangeMidiMelody handler places A/B track below source track
This commit is contained in:
@@ -13718,6 +13718,92 @@ const App = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── MIDI Rearrange Flow: selected MIDI item → use rearrange tool ──
|
||||
if (selectedItemIds && selectedItemIds.size === 1) {
|
||||
const selId = selectedItemIds.values().next().value;
|
||||
let isMidiItem = false;
|
||||
let sourceTrackId = null;
|
||||
let sourceItemName = null;
|
||||
for (const t of activeTracks || []) {
|
||||
const found = (t.midiItems || []).find(m => m.id === selId);
|
||||
if (found) { isMidiItem = true; sourceTrackId = t.id; sourceItemName = found.name; break; }
|
||||
}
|
||||
if (isMidiItem && window.SonicMidiExtractor) {
|
||||
try {
|
||||
setAiProcessing(true);
|
||||
setAiActionLog(prev => [...prev, { type: 'status', text: ` 🎯 Phát hiện MIDI item được chọn — chuyển sang chế độ Rearrange...`, time: Date.now() }]);
|
||||
|
||||
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: ` Rearrange Provider: ${provider.name || 'default'} | Model: ${model}`, time: Date.now() }]);
|
||||
|
||||
const srcContext = window.SonicMidiExtractor.extractSelectedMIDIContext(activeTracks || tracks, selId, bpm);
|
||||
setAiActionLog(prev => [...prev, { type: 'info', text: ` 📋 Trích xuất ${srcContext.total_notes} notes từ "${srcContext.item_name}" (${srcContext.track_name})`, time: Date.now() }]);
|
||||
|
||||
if (window.DAWCommandDispatcher) {
|
||||
window.DAWCommandDispatcher.rearrangeSourceTrackId = sourceTrackId;
|
||||
window.DAWCommandDispatcher.rearrangeSourceItemName = sourceItemName;
|
||||
}
|
||||
|
||||
const messages = window.AIGateway.buildRearrangeMessage(prompt, srcContext);
|
||||
const completion = await window.AIGateway.callLLM({
|
||||
provider: provider.name || 'default',
|
||||
model,
|
||||
apiKey,
|
||||
baseUrl: baseUrl.replace(/\/chat\/completions$/, '').replace(/\/$/, ''),
|
||||
messages,
|
||||
tools: [window.AIGateway.REARRANGE_TOOL_SPEC],
|
||||
toolChoice: 'auto'
|
||||
});
|
||||
|
||||
const functionCalls = window.AIGateway.extractFunctionCalls(completion);
|
||||
if (functionCalls && functionCalls.length > 0) {
|
||||
for (const fc of functionCalls) {
|
||||
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
|
||||
const cmdName = fc.name.toUpperCase();
|
||||
if (window.DAWCommandDispatcher) {
|
||||
try {
|
||||
let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
|
||||
if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult;
|
||||
setAiActionLog(prev => [...prev, { type: 'status', text: ` ✅ ${fc.name}: thành công — ${fc.arguments.rearrange_title || ''}`, time: Date.now() }]);
|
||||
} catch (cmdErr) {
|
||||
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ ${fc.name}: ${cmdErr.message}`, time: Date.now() }]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setAiActionLog(prev => [...prev, { type: 'error', text: ` ❌ AI không trả về lệnh rearrange hợp lệ.`, time: Date.now() }]);
|
||||
}
|
||||
|
||||
const textResp = functionCalls.length === 0 && completion.choices && completion.choices[0] && completion.choices[0].message && completion.choices[0].message.content;
|
||||
if (textResp) {
|
||||
setAiActionLog(prev => [...prev, { type: 'status', text: ` AI: ${textResp.slice(0, 500)}`, time: Date.now() }]);
|
||||
}
|
||||
if (prompt) {
|
||||
promptHistRef.current = [...promptHistRef.current.slice(-49), prompt];
|
||||
setPromptHistory(promptHistRef.current);
|
||||
}
|
||||
setPromptHistIdx(-1);
|
||||
setAiActionLog(prev => [...prev, { type: 'status', text: ` Hoàn tất Rearrange.`, time: Date.now() }]);
|
||||
setAiPrompt('');
|
||||
setTimeout(() => lucide.createIcons(), 200);
|
||||
setAiProcessing(false);
|
||||
return;
|
||||
} catch (err) {
|
||||
setAiActionLog(prev => [...prev, { type: 'error', text: ` Lỗi Rearrange: ${err.message}`, time: Date.now() }]);
|
||||
showToast(`AI Rearrange Error: ${err.message}`, 'error');
|
||||
setAiProcessing(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (window.DAWCommandDispatcher) {
|
||||
window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId;
|
||||
window.DAWCommandDispatcher.currentTracks = activeTracks;
|
||||
@@ -14670,6 +14756,77 @@ const App = () => {
|
||||
showToast(`Đã nạp ${aiTracks.length} tracks MIDI thế hệ AI!`, 'success');
|
||||
return { success: true };
|
||||
},
|
||||
rearrangeMidiMelody: (args) => {
|
||||
const { rearrange_title, rearranged_notes, soundfont_id, soundfont_bank, soundfont_program } = args;
|
||||
if (!rearranged_notes || rearranged_notes.length === 0) {
|
||||
return { success: false, error: 'No rearranged notes provided' };
|
||||
}
|
||||
const sourceTrackId = window.DAWCommandDispatcher?.rearrangeSourceTrackId || selectedTrackId;
|
||||
const sourceItemName = window.DAWCommandDispatcher?.rearrangeSourceItemName || 'Source';
|
||||
const bpmVal = parseInt(bpm) || 120;
|
||||
const secondsPerBeat = 60.0 / bpmVal;
|
||||
const secondsPerBar = secondsPerBeat * 4;
|
||||
|
||||
const totalBeats = rearranged_notes.reduce((max, n) => Math.max(max, (n.start_beat || 0) + (n.duration_beats || 1)), 0);
|
||||
const totalBars = Math.max(1, Math.ceil(totalBeats / 4));
|
||||
const durationSec = totalBars * secondsPerBar;
|
||||
|
||||
updateActiveTracks(prev => {
|
||||
let updatedTracks = [...prev];
|
||||
// Find source track index to insert new track right after it
|
||||
const sourceIdx = sourceTrackId ? updatedTracks.findIndex(t => t.id === sourceTrackId || t.id === 'track_' + sourceTrackId) : -1;
|
||||
const insertIdx = sourceIdx >= 0 ? sourceIdx + 1 : updatedTracks.length;
|
||||
const newId = (updatedTracks.length + 1).toString();
|
||||
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
|
||||
const newTrack = {
|
||||
id: newId,
|
||||
name: `[AI Rearrange] ${rearrange_title || 'Variation'}`,
|
||||
type: 'MIDI',
|
||||
volumeDb: 0,
|
||||
pan: 0,
|
||||
muted: false,
|
||||
solo: false,
|
||||
color: colors[insertIdx % colors.length],
|
||||
markers: [],
|
||||
serverFileId: null,
|
||||
clips: [],
|
||||
sections: [],
|
||||
midiItems: [{
|
||||
id: 'item_rearr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5),
|
||||
name: `[AI] ${rearrange_title || 'Rearranged'} - var`,
|
||||
parent_track_id: newId,
|
||||
startTime: currentTime,
|
||||
duration: durationSec,
|
||||
length_bars: totalBars,
|
||||
notes: rearranged_notes.map((n, i) => ({
|
||||
id: `note_rearr_${Date.now()}_${i}`,
|
||||
pitch: Math.max(0, Math.min(127, 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
|
||||
}))
|
||||
}]
|
||||
};
|
||||
if (soundfont_id || (soundfont_bank !== undefined && soundfont_program !== undefined)) {
|
||||
newTrack.soundfont_id = soundfont_id || '';
|
||||
newTrack.soundfont_bank = soundfont_bank ?? 0;
|
||||
newTrack.soundfont_program = soundfont_program ?? 0;
|
||||
newTrack.synth_engine = {
|
||||
type: 'soundfont',
|
||||
plugin_id: soundfont_id ? 'sf_' + soundfont_id : null,
|
||||
soundfont_bank: soundfont_bank ?? 0,
|
||||
soundfont_program: soundfont_program ?? 0,
|
||||
soundfont_id: soundfont_id || ''
|
||||
};
|
||||
}
|
||||
updatedTracks.splice(insertIdx, 0, newTrack);
|
||||
return updatedTracks;
|
||||
});
|
||||
|
||||
showToast(`✅ AI Rearrange: "${rearrange_title}" — ${rearranged_notes.length} notes`, 'success');
|
||||
return { success: true, trackId: newId, totalBars };
|
||||
},
|
||||
createMidiItem: (args) => {
|
||||
const trackId = args.track_id || selectedTrackId;
|
||||
if (!trackId) return { success: false, error: 'No track_id provided' };
|
||||
|
||||
Reference in New Issue
Block a user