From 610c384bcaf971f77ec9d6e2352a6d76a47b4472 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 22 Jul 2026 17:20:41 +0700 Subject: [PATCH] fix: copilot Multi-tool Calling) --- app/static/js/app.jsx | 109 ++++++++++++++++++ app/static/js/app.precompiled.js | 171 ++++++++++++++++++++++++++++ app/static/js/services/aiGateway.js | 8 +- app/storage/sonicforge.db | Bin 45056 -> 45056 bytes 4 files changed, 287 insertions(+), 1 deletion(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 877a349..84d4c57 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -7729,6 +7729,115 @@ const App = () => { return { ...t, markers: [...(t.markers || []), { id: 'ai_marker_' + Date.now(), time, label: args.label || 'AI Marker' }] }; })); return { success: true, trackId, time }; + }, + fadeIn: (args) => { + const trackIdRaw = args.track_id || selectedTrackId; + const track = tracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); + if (!track) return { success: false, error: `Track ${trackIdRaw} not found` }; + const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []); + if (clips.length === 0) return { success: false, error: 'No clips on track' }; + let clip = null; + if (args.clip_id) { + clip = clips.find(c => c.id === args.clip_id); + } else if (args.clip_index !== undefined) { + const idx = parseInt(args.clip_index); + const realIdx = idx > 0 ? idx - 1 : 0; + clip = clips[realIdx] || clips[0]; + } else { + clip = clips[0]; + } + if (!clip || !clip.buffer) return { success: false, error: 'Clip has no audio buffer' }; + const duration = parseFloat(args.duration_seconds || 3); + const buffer = clip.buffer; + const sr = buffer.sampleRate; + const numChannels = buffer.numberOfChannels; + const length = buffer.length; + const ctx = getAudioContext(); + const newBuffer = ctx.createBuffer(numChannels, length, sr); + for (let c = 0; c < numChannels; c++) { + newBuffer.getChannelData(c).set(buffer.getChannelData(c)); + } + const fadeSamples = Math.min(length, Math.floor(duration * sr)); + for (let c = 0; c < numChannels; c++) { + const data = newBuffer.getChannelData(c); + for (let i = 0; i < fadeSamples; i++) { + const factor = (1 - Math.cos(Math.PI * i / fadeSamples)) / 2; + data[i] *= factor; + } + } + const beforeSnap = captureTrackSnapshot(track.id); + setTracks(prev => prev.map(t => { + if (t.id !== track.id) return t; + const existingClips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []); + const updatedClips = existingClips.map(c => { + if (c.id === clip.id || (clip.id.startsWith('default_') && c.id === 'default')) { + return { ...c, buffer: newBuffer }; + } + return c; + }); + const mainBuffer = updatedClips[0]?.buffer || t.buffer; + return { ...t, clips: updatedClips, buffer: mainBuffer }; + })); + setTimeout(() => { + const afterSnap = captureTrackSnapshot(track.id); + pushAction('AI_FADE_IN', track.id, beforeSnap, afterSnap); + }, 50); + return { success: true, trackId: track.id, clipId: clip.id, duration_seconds: duration }; + }, + fadeOut: (args) => { + const trackIdRaw = args.track_id || selectedTrackId; + const track = tracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); + if (!track) return { success: false, error: `Track ${trackIdRaw} not found` }; + const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []); + if (clips.length === 0) return { success: false, error: 'No clips on track' }; + let clip = null; + if (args.clip_id) { + clip = clips.find(c => c.id === args.clip_id); + } else if (args.clip_index !== undefined) { + const idx = parseInt(args.clip_index); + const realIdx = idx > 0 ? idx - 1 : 0; + clip = clips[realIdx] || clips[0]; + } else { + clip = clips[0]; + } + if (!clip || !clip.buffer) return { success: false, error: 'Clip has no audio buffer' }; + const duration = parseFloat(args.duration_seconds || 3); + const buffer = clip.buffer; + const sr = buffer.sampleRate; + const numChannels = buffer.numberOfChannels; + const length = buffer.length; + const ctx = getAudioContext(); + const newBuffer = ctx.createBuffer(numChannels, length, sr); + for (let c = 0; c < numChannels; c++) { + newBuffer.getChannelData(c).set(buffer.getChannelData(c)); + } + const fadeSamples = Math.min(length, Math.floor(duration * sr)); + for (let c = 0; c < numChannels; c++) { + const data = newBuffer.getChannelData(c); + for (let i = 0; i < fadeSamples; i++) { + const idx = length - fadeSamples + i; + const factor = (1 + Math.cos(Math.PI * i / fadeSamples)) / 2; + data[idx] *= factor; + } + } + const beforeSnap = captureTrackSnapshot(track.id); + setTracks(prev => prev.map(t => { + if (t.id !== track.id) return t; + const existingClips = t.clips && t.clips.length > 0 ? t.clips : (t.buffer ? [{ id: 'default_' + t.id, buffer: t.buffer, startTime: t.startTime || 0, name: t.name }] : []); + const updatedClips = existingClips.map(c => { + if (c.id === clip.id || (clip.id.startsWith('default_') && c.id === 'default')) { + return { ...c, buffer: newBuffer }; + } + return c; + }); + const mainBuffer = updatedClips[0]?.buffer || t.buffer; + return { ...t, clips: updatedClips, buffer: mainBuffer }; + })); + setTimeout(() => { + const afterSnap = captureTrackSnapshot(track.id); + pushAction('AI_FADE_OUT', track.id, beforeSnap, afterSnap); + }, 50); + return { success: true, trackId: track.id, clipId: clip.id, duration_seconds: duration }; } }; window.DAWCommandDispatcher.registerDAWCommands(api); diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 73d0588..59822d9 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -8099,6 +8099,177 @@ const App = () => { trackId, time }; + }, + fadeIn: args => { + const trackIdRaw = args.track_id || selectedTrackId; + const track = tracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); + if (!track) return { + success: false, + error: `Track ${trackIdRaw} not found` + }; + const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ + id: 'default_' + track.id, + buffer: track.buffer, + startTime: track.startTime || 0, + name: track.name + }] : []; + if (clips.length === 0) return { + success: false, + error: 'No clips on track' + }; + let clip = null; + if (args.clip_id) { + clip = clips.find(c => c.id === args.clip_id); + } else if (args.clip_index !== undefined) { + const idx = parseInt(args.clip_index); + const realIdx = idx > 0 ? idx - 1 : 0; + clip = clips[realIdx] || clips[0]; + } else { + clip = clips[0]; + } + if (!clip || !clip.buffer) return { + success: false, + error: 'Clip has no audio buffer' + }; + const duration = parseFloat(args.duration_seconds || 3); + const buffer = clip.buffer; + const sr = buffer.sampleRate; + const numChannels = buffer.numberOfChannels; + const length = buffer.length; + const ctx = getAudioContext(); + const newBuffer = ctx.createBuffer(numChannels, length, sr); + for (let c = 0; c < numChannels; c++) { + newBuffer.getChannelData(c).set(buffer.getChannelData(c)); + } + const fadeSamples = Math.min(length, Math.floor(duration * sr)); + for (let c = 0; c < numChannels; c++) { + const data = newBuffer.getChannelData(c); + for (let i = 0; i < fadeSamples; i++) { + const factor = (1 - Math.cos(Math.PI * i / fadeSamples)) / 2; + data[i] *= factor; + } + } + const beforeSnap = captureTrackSnapshot(track.id); + setTracks(prev => prev.map(t => { + if (t.id !== track.id) return t; + const existingClips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{ + id: 'default_' + t.id, + buffer: t.buffer, + startTime: t.startTime || 0, + name: t.name + }] : []; + const updatedClips = existingClips.map(c => { + if (c.id === clip.id || clip.id.startsWith('default_') && c.id === 'default') { + return { + ...c, + buffer: newBuffer + }; + } + return c; + }); + const mainBuffer = updatedClips[0]?.buffer || t.buffer; + return { + ...t, + clips: updatedClips, + buffer: mainBuffer + }; + })); + setTimeout(() => { + const afterSnap = captureTrackSnapshot(track.id); + pushAction('AI_FADE_IN', track.id, beforeSnap, afterSnap); + }, 50); + return { + success: true, + trackId: track.id, + clipId: clip.id, + duration_seconds: duration + }; + }, + fadeOut: args => { + const trackIdRaw = args.track_id || selectedTrackId; + const track = tracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); + if (!track) return { + success: false, + error: `Track ${trackIdRaw} not found` + }; + const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ + id: 'default_' + track.id, + buffer: track.buffer, + startTime: track.startTime || 0, + name: track.name + }] : []; + if (clips.length === 0) return { + success: false, + error: 'No clips on track' + }; + let clip = null; + if (args.clip_id) { + clip = clips.find(c => c.id === args.clip_id); + } else if (args.clip_index !== undefined) { + const idx = parseInt(args.clip_index); + const realIdx = idx > 0 ? idx - 1 : 0; + clip = clips[realIdx] || clips[0]; + } else { + clip = clips[0]; + } + if (!clip || !clip.buffer) return { + success: false, + error: 'Clip has no audio buffer' + }; + const duration = parseFloat(args.duration_seconds || 3); + const buffer = clip.buffer; + const sr = buffer.sampleRate; + const numChannels = buffer.numberOfChannels; + const length = buffer.length; + const ctx = getAudioContext(); + const newBuffer = ctx.createBuffer(numChannels, length, sr); + for (let c = 0; c < numChannels; c++) { + newBuffer.getChannelData(c).set(buffer.getChannelData(c)); + } + const fadeSamples = Math.min(length, Math.floor(duration * sr)); + for (let c = 0; c < numChannels; c++) { + const data = newBuffer.getChannelData(c); + for (let i = 0; i < fadeSamples; i++) { + const idx = length - fadeSamples + i; + const factor = (1 + Math.cos(Math.PI * i / fadeSamples)) / 2; + data[idx] *= factor; + } + } + const beforeSnap = captureTrackSnapshot(track.id); + setTracks(prev => prev.map(t => { + if (t.id !== track.id) return t; + const existingClips = t.clips && t.clips.length > 0 ? t.clips : t.buffer ? [{ + id: 'default_' + t.id, + buffer: t.buffer, + startTime: t.startTime || 0, + name: t.name + }] : []; + const updatedClips = existingClips.map(c => { + if (c.id === clip.id || clip.id.startsWith('default_') && c.id === 'default') { + return { + ...c, + buffer: newBuffer + }; + } + return c; + }); + const mainBuffer = updatedClips[0]?.buffer || t.buffer; + return { + ...t, + clips: updatedClips, + buffer: mainBuffer + }; + })); + setTimeout(() => { + const afterSnap = captureTrackSnapshot(track.id); + pushAction('AI_FADE_OUT', track.id, beforeSnap, afterSnap); + }, 50); + return { + success: true, + trackId: track.id, + clipId: clip.id, + duration_seconds: duration + }; } }; window.DAWCommandDispatcher.registerDAWCommands(api); diff --git a/app/static/js/services/aiGateway.js b/app/static/js/services/aiGateway.js index eaf9deb..ed380e8 100644 --- a/app/static/js/services/aiGateway.js +++ b/app/static/js/services/aiGateway.js @@ -143,7 +143,13 @@ const AIGateway = (function() { const contextStr = JSON.stringify(context, null, 2); const toolNames = DEFAULT_TOOLS.map(t => ` - ${t.name}: ${t.description}`).join('\n'); return [ - { role: 'system', content: `Bạn là trợ lý DAW. Dùng function calls. Bar 0 = bar đầu. Có thể gọi nhiều function cùng lúc.` }, + { role: 'system', content: `Bạn là trợ lý điều khiển DAW chuyên nghiệp. +Nhiệm vụ của bạn là phân tích yêu cầu của người dùng và chuyển đổi thành danh sách các function calls tương ứng. +QUAN TRỌNG: +1. Bạn đang hoạt động ở chế độ một lượt (one-shot). Hãy trả về TẤT CẢ các function calls cần thiết để thực hiện toàn bộ các bước trong yêu cầu của người dùng trong một phản hồi duy nhất. Đừng thực hiện từng bước qua nhiều lượt chat. +2. Có thể gọi nhiều function cùng một lúc (gọi song song/nối tiếp). Chúng sẽ được thực thi theo thứ tự bạn trả về. +3. Khi một lệnh trước đó (như 'cut_audio') tạo ra một track mới, track đó sẽ tự động được chọn làm active track (selectedTrackId). Do đó, đối với các lệnh tiếp theo áp dụng lên track mới này (như 'fade_in', 'set_track_volume'), bạn hãy để trống tham số 'track_id' (hoặc truyền null) để hệ thống tự động áp dụng lên track vừa được tạo và chọn. +4. Bar 0 đại diện cho bar đầu tiên trên timeline.` }, { role: 'user', content: `Ngữ cảnh DAW hiện tại:\n${contextStr}\n\nYêu cầu người dùng: ${prompt}` } ]; } diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 4fabc1f1c601d08dc5821dcd266fb0a054b498a1..d396b54e8d921c5b79097bf9671b5b93a4461f52 100644 GIT binary patch delta 240 zcmVo=Wp5%nbaHiNEFx@SVr6V1IwDh4MN&mi qB7Kw0e6W+3e-8=fLE4xmC^)E%vyyxD0R#X50w}S8Bm%Rqe^D?v(oLEG delta 157 zcmV;O0Al}u-~xc)0+1U4U6C9^0bQ|R5FY^ulW`wHKruBjWj8fBG-fq8Ha9S0F=aD3 zGdN;1W->A}Ib=65Wo0=wFlJ*mGC5;0W@KV9F=k?8WHT@@I5K58FgG+gX0!7j4p0pM z0009Ek^m2{3z7|ovk?%43$v1Y^#KWlB?JjyRX>JhlR0_4vod^20h4-r4+*S6+L$HW LOAwv2uYXZ6@aHgz