222 lines
7.3 KiB
JavaScript
222 lines
7.3 KiB
JavaScript
// SonicForge Studio - AI Gateway & Function Routing
|
|
// LLM Gateway with Function Calling / Structured Outputs (28_AI_PANEL.md §2)
|
|
|
|
const AIGateway = (function() {
|
|
const DEFAULT_TOOLS = [{
|
|
name: 'create_track',
|
|
description: 'Tạo một track mới trong dự án',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string', description: 'Tên track' },
|
|
type: { type: 'string', enum: ['audio', 'midi'], description: 'Loại track' }
|
|
},
|
|
required: ['name', 'type']
|
|
}
|
|
}, {
|
|
name: 'add_midi_item',
|
|
description: 'Thêm một MIDI item/clip vào track',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
track_id: { type: 'string', description: 'ID của track đích' },
|
|
start_bar: { type: 'number', description: 'Vị trí bắt đầu (tính bằng bar)' },
|
|
length_bars: { type: 'number', description: 'Độ dài item (tính bằng bar)' }
|
|
},
|
|
required: ['track_id', 'start_bar', 'length_bars']
|
|
}
|
|
}, {
|
|
name: 'modify_midi_notes',
|
|
description: 'Thêm, chỉnh sửa hoặc xóa các note MIDI trong item',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
item_id: { type: 'string', description: 'ID của MIDI item' },
|
|
notes: {
|
|
type: 'array',
|
|
description: 'Danh sách các note MIDI',
|
|
items: {
|
|
type: 'object',
|
|
properties: {
|
|
pitch: { type: 'string', description: 'VD: C4, D#3, F5' },
|
|
start_time: { type: 'number', description: 'Thời điểm bắt đầu (bar hoặc giây)' },
|
|
duration: { type: 'number', description: 'Độ dài note' },
|
|
velocity: { type: 'integer', minimum: 0, maximum: 127, description: 'Độ mạnh 0-127' }
|
|
},
|
|
required: ['pitch', 'start_time', 'duration']
|
|
}
|
|
}
|
|
},
|
|
required: ['item_id', 'notes']
|
|
}
|
|
}, {
|
|
name: 'process_audio_dsp',
|
|
description: 'Gửi yêu cầu chỉnh sửa âm thanh sang Python DSP Backend',
|
|
parameters: {
|
|
type: 'object',
|
|
properties: {
|
|
track_id: { type: 'string', description: 'ID của track âm thanh' },
|
|
action: { type: 'string', enum: ['normalize', 'invert_phase', 'gain', 'pitch_shift'], description: 'Loại xử lý DSP' },
|
|
params: { type: 'object', description: 'Tham số bổ sung cho hành động' }
|
|
},
|
|
required: ['track_id', 'action']
|
|
}
|
|
}];
|
|
|
|
async function callLLM({ provider, model, apiKey, baseUrl, messages, tools, toolChoice }) {
|
|
const url = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
...(apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {})
|
|
};
|
|
|
|
const body = {
|
|
model,
|
|
messages,
|
|
...(tools && tools.length > 0 ? { tools: tools.map(t => ({ type: 'function', function: t })) } : {}),
|
|
...(toolChoice ? { tool_choice: toolChoice } : {})
|
|
};
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errText = await response.text();
|
|
throw new Error(`LLM API error ${response.status}: ${errText}`);
|
|
}
|
|
|
|
return await response.json();
|
|
}
|
|
|
|
function extractFunctionCalls(completion) {
|
|
const calls = [];
|
|
const choice = completion.choices && completion.choices[0];
|
|
if (!choice) return calls;
|
|
|
|
const msg = choice.message;
|
|
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
for (const tc of msg.tool_calls) {
|
|
if (tc.type === 'function' && tc.function) {
|
|
let args = {};
|
|
try { args = JSON.parse(tc.function.arguments || '{}'); } catch (e) { args = { raw: tc.function.arguments }; }
|
|
calls.push({
|
|
id: tc.id,
|
|
name: tc.function.name,
|
|
arguments: args
|
|
});
|
|
}
|
|
}
|
|
} else if (msg.function_call) {
|
|
let args = {};
|
|
try { args = JSON.parse(msg.function_call.arguments || '{}'); } catch (e) { args = { raw: msg.function_call.arguments }; }
|
|
calls.push({
|
|
id: 'call_' + Date.now(),
|
|
name: msg.function_call.name,
|
|
arguments: args
|
|
});
|
|
}
|
|
return calls;
|
|
}
|
|
|
|
function buildUserMessage(prompt, context) {
|
|
const contextStr = JSON.stringify(context, null, 2);
|
|
return [
|
|
{ role: 'system', content: 'Bạn là trợ lý AI cho DAW (SonicForge Studio). Hãy phân tích yêu cầu người dùng và phản hồi BẰNG DẠNG FUNCTION CALLS phù hợp. Luôn trả về function call khi có thể thực hiện hành động.' },
|
|
{ role: 'user', content: `Ngữ cảnh DAW hiện tại:\n${contextStr}\n\nYêu cầu người dùng: ${prompt}` }
|
|
];
|
|
}
|
|
|
|
function buildAIPromptContext(dawState) {
|
|
const tracks = (dawState.tracks || []).map(t => ({
|
|
id: t.id,
|
|
name: t.name,
|
|
type: t.buffer ? 'audio' : 'empty',
|
|
hasBuffer: !!t.buffer,
|
|
clipsCount: t.clips ? t.clips.length : (t.buffer ? 1 : 0),
|
|
muted: t.muted,
|
|
solo: t.solo,
|
|
volumeDb: t.volumeDb ?? 0,
|
|
pan: t.pan ?? 0
|
|
}));
|
|
return {
|
|
tempo: dawState.bpm || 120,
|
|
timeSignature: '4/4',
|
|
selectedTrackId: dawState.selectedTrackId || null,
|
|
playheadPosition: dawState.currentTime || 0,
|
|
selection: (dawState.selLeft !== null && dawState.selRight !== null && dawState.selRight > dawState.selLeft) ? {
|
|
start: parseFloat(dawState.selLeft.toFixed(3)),
|
|
end: parseFloat(dawState.selRight.toFixed(3)),
|
|
length: parseFloat((dawState.selRight - dawState.selLeft).toFixed(3))
|
|
} : null,
|
|
tracks
|
|
};
|
|
}
|
|
|
|
async function executePrompt({ prompt, provider, model, apiKey, baseUrl, dawContext, tools }) {
|
|
const messages = buildUserMessage(prompt, dawContext);
|
|
const toolList = tools || DEFAULT_TOOLS;
|
|
|
|
const completion = await callLLM({
|
|
provider,
|
|
model,
|
|
apiKey,
|
|
baseUrl,
|
|
messages,
|
|
tools: toolList,
|
|
toolChoice: 'auto'
|
|
});
|
|
|
|
const functionCalls = extractFunctionCalls(completion);
|
|
const textResponse = completion.choices && completion.choices[0] && completion.choices[0].message && completion.choices[0].message.content
|
|
? completion.choices[0].message.content
|
|
: '';
|
|
|
|
return {
|
|
functionCalls,
|
|
textResponse,
|
|
raw: completion
|
|
};
|
|
}
|
|
|
|
async function createMidiItem(args) {
|
|
return fetch('/api/audio_editor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'add_midi', ...args })
|
|
}).then(r => r.json());
|
|
}
|
|
|
|
async function modifyMidiNotes(args) {
|
|
return fetch('/api/audio_editor', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'modify_midi_notes', ...args })
|
|
}).then(r => r.json());
|
|
}
|
|
|
|
async function processAIDSP(args) {
|
|
return fetch('/api/ai_dsp_engine', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'process_ai_dsp', ...args })
|
|
}).then(r => r.json());
|
|
}
|
|
|
|
return {
|
|
DEFAULT_TOOLS,
|
|
callLLM,
|
|
extractFunctionCalls,
|
|
buildUserMessage,
|
|
buildAIPromptContext,
|
|
executePrompt,
|
|
createMidiItem,
|
|
modifyMidiNotes,
|
|
processAIDSP
|
|
};
|
|
})();
|
|
|
|
window.AIGateway = AIGateway;
|