fix: thêm và sửa các tools của AI

This commit is contained in:
2026-07-22 09:51:27 +07:00
parent 4791bb22d9
commit 108943ae81
7 changed files with 285 additions and 13 deletions
+36 -10
View File
@@ -1,12 +1,24 @@
from fastapi import APIRouter, HTTPException, Depends import json
from fastapi import APIRouter, HTTPException, Header, Depends
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List from typing import Optional, List, Dict, Any
import time import time
from app.core.auth import decode_token
router = APIRouter() router = APIRouter()
def _get_user_id(authorization):
if not authorization or not authorization.startswith("Bearer "):
return "anonymous"
token = authorization.split(" ")[1]
payload = decode_token(token)
if not payload:
return "anonymous"
return payload.get("user_id", "anonymous")
# In-memory / per-user AI provider configurations storage dictionary # In-memory / per-user AI provider configurations storage dictionary
USER_AI_CONFIGS = {} USER_AI_CONFIGS = {}
USER_PREFERENCES = {}
class AIProviderSetting(BaseModel): class AIProviderSetting(BaseModel):
id: str id: str
@@ -21,11 +33,25 @@ class AIProviderSetting(BaseModel):
class SaveAIConfigRequest(BaseModel): class SaveAIConfigRequest(BaseModel):
providers: List[AIProviderSetting] providers: List[AIProviderSetting]
class SavePreferencesRequest(BaseModel):
preferences: Dict[str, Any]
@router.get("/preferences")
async def get_user_preferences(authorization: Optional[str] = Header(None)):
uid = _get_user_id(authorization)
return {"success": True, "preferences": USER_PREFERENCES.get(uid, {})}
@router.post("/preferences")
async def save_user_preferences(req: SavePreferencesRequest, authorization: Optional[str] = Header(None)):
uid = _get_user_id(authorization)
USER_PREFERENCES[uid] = req.preferences
return {"success": True, "message": "Đã lưu cấu hình người dùng."}
@router.get("/config/ai") @router.get("/config/ai")
async def get_user_ai_config(): async def get_user_ai_config(authorization: Optional[str] = Header(None)):
"""Fetch user's AI provider configurations.""" uid = _get_user_id(authorization)
if "default_user" not in USER_AI_CONFIGS: if uid not in USER_AI_CONFIGS:
USER_AI_CONFIGS["default_user"] = [ USER_AI_CONFIGS[uid] = [
{ {
"id": "openai_default", "id": "openai_default",
"name": "OpenAI Official", "name": "OpenAI Official",
@@ -69,13 +95,13 @@ async def get_user_ai_config():
] ]
return { return {
"success": True, "success": True,
"providers": USER_AI_CONFIGS["default_user"] "providers": USER_AI_CONFIGS[uid]
} }
@router.post("/config/ai") @router.post("/config/ai")
async def save_user_ai_config(req: SaveAIConfigRequest): async def save_user_ai_config(req: SaveAIConfigRequest, authorization: Optional[str] = Header(None)):
"""Save user's AI provider configurations.""" uid = _get_user_id(authorization)
USER_AI_CONFIGS["default_user"] = [p.dict() for p in req.providers] USER_AI_CONFIGS[uid] = [p.dict() for p in req.providers]
return { return {
"success": True, "success": True,
"message": "Đã lưu cấu hình AI Providers thành công!" "message": "Đã lưu cấu hình AI Providers thành công!"
+98 -1
View File
@@ -3240,6 +3240,25 @@ const App = () => {
setIsMandatoryLogin(false); setIsMandatoryLogin(false);
setAuthModalOpen(false); setAuthModalOpen(false);
loadPendingSfsProject(); loadPendingSfsProject();
(async () => {
try {
const data = await window.SonicAPI.getPreferences();
if (data && data.preferences) {
const p = data.preferences;
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
if (p.panelPositions) setPanelPositions(p.panelPositions);
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
}
} catch (e) { /* preferences not available */ }
})();
} }
}; };
const handleLogout = () => { const handleLogout = () => {
@@ -7333,6 +7352,67 @@ const App = () => {
setSelectionEnd(end); setSelectionEnd(end);
return { success: true, start: parseFloat(start.toFixed(3)), end: parseFloat(end.toFixed(3)), length: parseFloat((end - start).toFixed(3)) }; return { success: true, start: parseFloat(start.toFixed(3)), end: parseFloat(end.toFixed(3)), length: parseFloat((end - start).toFixed(3)) };
}, },
cutAudio: (args) => {
const tid = args.track_id || selectedTrackId;
const track = tracks.find(t => t.id === tid);
if (!track) return { success: false, error: 'Track not found' };
if (!track.buffer) return { success: false, error: 'Track has no audio buffer' };
const barDur = 60 / parseInt(bpm || 120) * 4;
let rawStart, rawEnd;
if (args.start_time !== undefined) rawStart = args.start_time;
else if (args.start_bar !== undefined) rawStart = args.start_bar * barDur;
else if (selectionStart !== null) rawStart = selectionStart;
else return { success: false, error: 'No selection or start position provided. Use set_selection first or provide start_time/start_bar.' };
if (args.end_time !== undefined) rawEnd = args.end_time;
else if (args.length_bars !== undefined) rawEnd = rawStart + args.length_bars * barDur;
else if (selectionEnd !== null && selectionEnd > rawStart) rawEnd = selectionEnd;
else return { success: false, error: 'No end position provided. Use set_selection first or provide end_time/length_bars.' };
const buffer = track.buffer;
const ctx = getAudioContext();
const snap = args.snap_silence !== false;
const loopStart = snap ? findZeroCrossing(buffer, rawStart) : rawStart;
const loopEnd = snap ? findZeroCrossing(buffer, rawEnd) : rawEnd;
const sampleRate = buffer.sampleRate;
const startSample = Math.max(0, Math.min(buffer.length - 1, Math.floor(loopStart * sampleRate)));
const endSample = Math.max(0, Math.min(buffer.length, Math.floor(loopEnd * sampleRate)));
const sliceLength = endSample - startSample;
if (sliceLength <= 100) return { success: false, error: 'Selection too short or invalid' };
const numChannels = buffer.numberOfChannels || 1;
const slicedBuffer = ctx.createBuffer(numChannels, sliceLength, sampleRate);
for (let c = 0; c < numChannels; c++) {
const src = buffer.getChannelData(c);
const dst = slicedBuffer.getChannelData(c);
dst.set(src.subarray(startSample, endSample));
}
const newId = 'track_cut_' + Date.now();
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
const color = colors[tracks.length % colors.length];
const cutName = args.new_track_name || `Cut_${track.name}`;
const newTrack = {
id: newId, name: cutName, buffer: slicedBuffer,
startTime: 0, height: 128, volumeDb: 0, pan: 0,
muted: false, solo: false, color, markers: [],
clips: [{ id: 'clip_' + newId, buffer: slicedBuffer, startTime: 0, name: cutName }],
serverFileId: null
};
setTracks(prev => {
const idx = prev.findIndex(t => t.id === tid);
const updated = [...prev];
updated.splice(idx + 1, 0, newTrack);
return updated;
});
setSelectedTrackId(newId);
clearLocalSelection();
setSelectionMode('global');
setSelectionStart(0);
setSelectionEnd(slicedBuffer.duration);
setTimeout(() => lucide.createIcons(), 200);
return {
success: true, trackId: newId, trackName: cutName,
cutStart: parseFloat(loopStart.toFixed(3)), cutEnd: parseFloat(loopEnd.toFixed(3)),
duration: parseFloat(slicedBuffer.duration.toFixed(3))
};
},
scanTrack: (args) => { scanTrack: (args) => {
const tid = args.track_id || selectedTrackId; const tid = args.track_id || selectedTrackId;
const track = tracks.find(t => t.id === tid); const track = tracks.find(t => t.id === tid);
@@ -7411,6 +7491,23 @@ const App = () => {
localStorage.setItem('ai_api_key', aiConfig.apiKey); localStorage.setItem('ai_api_key', aiConfig.apiKey);
localStorage.setItem('ai_model', aiConfig.model); localStorage.setItem('ai_model', aiConfig.model);
}, [aiConfig]); }, [aiConfig]);
// Auto-save user preferences (panel state, provider)
const prefsRef = useRef({});
prefsRef.current = {
showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
showMediaExplorer, showFxRack, showMidiEvents,
panelPositions, rightSidebarWidth, mediaExplorerHeight, selectedProviderId
};
useEffect(() => {
if (!currentUser) return;
const timer = setTimeout(async () => {
try { await window.SonicAPI.savePreferences(prefsRef.current); } catch (e) {}
}, 2000);
return () => clearTimeout(timer);
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel,
showMediaExplorer, showFxRack, showMidiEvents, panelPositions,
rightSidebarWidth, mediaExplorerHeight, selectedProviderId, currentUser]);
return /*#__PURE__*/React.createElement("div", { return /*#__PURE__*/React.createElement("div", {
className: "h-full w-full flex flex-col bg-[#1e1e1e]" className: "h-full w-full flex flex-col bg-[#1e1e1e]"
}, /*#__PURE__*/React.createElement("header", { }, /*#__PURE__*/React.createElement("header", {
@@ -8296,7 +8393,7 @@ const App = () => {
key: p.id, key: p.id,
value: p.id value: p.id
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", { }, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1" className: "border-t border-zinc-800 pt-1.5 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("div", { }, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1" className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
}, /*#__PURE__*/React.createElement("span", { }, /*#__PURE__*/React.createElement("span", {
+131 -1
View File
@@ -3240,6 +3240,25 @@ const App = () => {
setIsMandatoryLogin(false); setIsMandatoryLogin(false);
setAuthModalOpen(false); setAuthModalOpen(false);
loadPendingSfsProject(); loadPendingSfsProject();
(async () => {
try {
const data = await window.SonicAPI.getPreferences();
if (data && data.preferences) {
const p = data.preferences;
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
if (p.showExportPanel !== undefined) setShowExportPanel(p.showExportPanel);
if (p.showSelectionPanel !== undefined) setShowSelectionPanel(p.showSelectionPanel);
if (p.showPythonToolsPanel !== undefined) setShowPythonToolsPanel(p.showPythonToolsPanel);
if (p.showMediaExplorer !== undefined) setShowMediaExplorer(p.showMediaExplorer);
if (p.showFxRack !== undefined) setShowFxRack(p.showFxRack);
if (p.showMidiEvents !== undefined) setShowMidiEvents(p.showMidiEvents);
if (p.panelPositions) setPanelPositions(p.panelPositions);
if (p.rightSidebarWidth) setRightSidebarWidth(p.rightSidebarWidth);
if (p.mediaExplorerHeight) setMediaExplorerHeight(p.mediaExplorerHeight);
if (p.selectedProviderId) setSelectedProviderId(p.selectedProviderId);
}
} catch (e) {/* preferences not available */}
})();
} }
}; };
const handleLogout = () => { const handleLogout = () => {
@@ -7460,6 +7479,92 @@ const App = () => {
length: parseFloat((end - start).toFixed(3)) length: parseFloat((end - start).toFixed(3))
}; };
}, },
cutAudio: args => {
const tid = args.track_id || selectedTrackId;
const track = tracks.find(t => t.id === tid);
if (!track) return {
success: false,
error: 'Track not found'
};
if (!track.buffer) return {
success: false,
error: 'Track has no audio buffer'
};
const barDur = 60 / parseInt(bpm || 120) * 4;
let rawStart, rawEnd;
if (args.start_time !== undefined) rawStart = args.start_time;else if (args.start_bar !== undefined) rawStart = args.start_bar * barDur;else if (selectionStart !== null) rawStart = selectionStart;else return {
success: false,
error: 'No selection or start position provided. Use set_selection first or provide start_time/start_bar.'
};
if (args.end_time !== undefined) rawEnd = args.end_time;else if (args.length_bars !== undefined) rawEnd = rawStart + args.length_bars * barDur;else if (selectionEnd !== null && selectionEnd > rawStart) rawEnd = selectionEnd;else return {
success: false,
error: 'No end position provided. Use set_selection first or provide end_time/length_bars.'
};
const buffer = track.buffer;
const ctx = getAudioContext();
const snap = args.snap_silence !== false;
const loopStart = snap ? findZeroCrossing(buffer, rawStart) : rawStart;
const loopEnd = snap ? findZeroCrossing(buffer, rawEnd) : rawEnd;
const sampleRate = buffer.sampleRate;
const startSample = Math.max(0, Math.min(buffer.length - 1, Math.floor(loopStart * sampleRate)));
const endSample = Math.max(0, Math.min(buffer.length, Math.floor(loopEnd * sampleRate)));
const sliceLength = endSample - startSample;
if (sliceLength <= 100) return {
success: false,
error: 'Selection too short or invalid'
};
const numChannels = buffer.numberOfChannels || 1;
const slicedBuffer = ctx.createBuffer(numChannels, sliceLength, sampleRate);
for (let c = 0; c < numChannels; c++) {
const src = buffer.getChannelData(c);
const dst = slicedBuffer.getChannelData(c);
dst.set(src.subarray(startSample, endSample));
}
const newId = 'track_cut_' + Date.now();
const colors = ['#0f766e', '#1d4ed8', '#701a75', '#a21caf', '#b45309'];
const color = colors[tracks.length % colors.length];
const cutName = args.new_track_name || `Cut_${track.name}`;
const newTrack = {
id: newId,
name: cutName,
buffer: slicedBuffer,
startTime: 0,
height: 128,
volumeDb: 0,
pan: 0,
muted: false,
solo: false,
color,
markers: [],
clips: [{
id: 'clip_' + newId,
buffer: slicedBuffer,
startTime: 0,
name: cutName
}],
serverFileId: null
};
setTracks(prev => {
const idx = prev.findIndex(t => t.id === tid);
const updated = [...prev];
updated.splice(idx + 1, 0, newTrack);
return updated;
});
setSelectedTrackId(newId);
clearLocalSelection();
setSelectionMode('global');
setSelectionStart(0);
setSelectionEnd(slicedBuffer.duration);
setTimeout(() => lucide.createIcons(), 200);
return {
success: true,
trackId: newId,
trackName: cutName,
cutStart: parseFloat(loopStart.toFixed(3)),
cutEnd: parseFloat(loopEnd.toFixed(3)),
duration: parseFloat(slicedBuffer.duration.toFixed(3))
};
},
scanTrack: args => { scanTrack: args => {
const tid = args.track_id || selectedTrackId; const tid = args.track_id || selectedTrackId;
const track = tracks.find(t => t.id === tid); const track = tracks.find(t => t.id === tid);
@@ -7565,6 +7670,31 @@ const App = () => {
localStorage.setItem('ai_api_key', aiConfig.apiKey); localStorage.setItem('ai_api_key', aiConfig.apiKey);
localStorage.setItem('ai_model', aiConfig.model); localStorage.setItem('ai_model', aiConfig.model);
}, [aiConfig]); }, [aiConfig]);
// ── Auto-save user preferences (panel state, provider) ──
const prefsRef = useRef({});
prefsRef.current = {
showAIPanel,
showExportPanel,
showSelectionPanel,
showPythonToolsPanel,
showMediaExplorer,
showFxRack,
showMidiEvents,
panelPositions,
rightSidebarWidth,
mediaExplorerHeight,
selectedProviderId
};
useEffect(() => {
if (!currentUser) return;
const timer = setTimeout(async () => {
try {
await window.SonicAPI.savePreferences(prefsRef.current);
} catch (e) {}
}, 2000);
return () => clearTimeout(timer);
}, [showAIPanel, showExportPanel, showSelectionPanel, showPythonToolsPanel, showMediaExplorer, showFxRack, showMidiEvents, panelPositions, rightSidebarWidth, mediaExplorerHeight, selectedProviderId, currentUser]);
return /*#__PURE__*/React.createElement("div", { return /*#__PURE__*/React.createElement("div", {
className: "h-full w-full flex flex-col bg-[#1e1e1e]" className: "h-full w-full flex flex-col bg-[#1e1e1e]"
}, /*#__PURE__*/React.createElement("header", { }, /*#__PURE__*/React.createElement("header", {
@@ -8444,7 +8574,7 @@ const App = () => {
key: p.id, key: p.id,
value: p.id value: p.id
}, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", { }, p.name, p.is_active ? '' : ' (inactive)')))), /*#__PURE__*/React.createElement("div", {
className: "border-t border-zinc-800 pt-1.5 mt-1" className: "border-t border-zinc-800 pt-1.5 mt-1 shrink-0"
}, /*#__PURE__*/React.createElement("div", { }, /*#__PURE__*/React.createElement("div", {
className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1" className: "text-xs font-bold text-zinc-400 uppercase mb-1 flex items-center gap-1"
}, /*#__PURE__*/React.createElement("span", { }, /*#__PURE__*/React.createElement("span", {
+15
View File
@@ -166,6 +166,21 @@ const AIGateway = (function() {
length_bars: { type: 'number', description: 'Độ dài vùng chọn (bar). Dùng cùng start_bar thay cho end_bar.' } length_bars: { type: 'number', description: 'Độ dài vùng chọn (bar). Dùng cùng start_bar thay cho end_bar.' }
} }
} }
}, {
name: 'cut_audio',
description: 'Cắt đoạn audio đang được chọn từ track nguồn, snap zero-crossing, tạo track mới chứa đoạn cắt',
parameters: {
type: 'object',
properties: {
track_id: { type: 'string', description: 'ID của track nguồn' },
start_time: { type: 'number', description: 'Vị trí bắt đầu cắt (giây). Mặc định: dùng selection hiện tại.' },
end_time: { type: 'number', description: 'Vị trí kết thúc cắt (giây). Mặc định: dùng selection hiện tại.' },
start_bar: { type: 'number', description: 'Bar bắt đầu cắt (0 = bar đầu). Dùng thay cho start_time.' },
length_bars: { type: 'number', description: 'Độ dài cắt (bar). Dùng cùng start_bar.' },
snap_silence: { type: 'boolean', description: 'Tự động snap vào điểm silence/gần silence gần nhất ở hai đầu (mặc định: true)' },
new_track_name: { type: 'string', description: 'Tên cho track mới. Mặc định: "Cut_<tên_track_gốc>"' }
}
}
}, { }, {
name: 'scan_track', name: 'scan_track',
description: 'Quét và phân tích track âm thanh: phát hiện BPM (tempo) và tự động cập nhật tempo hệ thống, sample rate, số kênh (mono/stereo), duration', description: 'Quét và phân tích track âm thanh: phát hiện BPM (tempo) và tự động cập nhật tempo hệ thống, sample rate, số kênh (mono/stereo), duration',
+4 -1
View File
@@ -52,6 +52,9 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
runPythonTool: (toolType, trackId, fileId, timePos = 0.0, freq = 440.0, duration = 2.0, waveType = "sine") => apiRequest('/api/v1/audio/python-tool', { method: 'POST', body: JSON.stringify({ tool_type: toolType, track_id: trackId, file_id: fileId, time_pos: timePos, freq: freq, duration: duration, wave_type: waveType }) }), runPythonTool: (toolType, trackId, fileId, timePos = 0.0, freq = 440.0, duration = 2.0, waveType = "sine") => apiRequest('/api/v1/audio/python-tool', { method: 'POST', body: JSON.stringify({ tool_type: toolType, track_id: trackId, file_id: fileId, time_pos: timePos, freq: freq, duration: duration, wave_type: waveType }) }),
getAIConfigs: () => apiRequest('/api/v1/user/config/ai', { method: 'GET' }), getAIConfigs: () => apiRequest('/api/v1/user/config/ai', { method: 'GET' }),
saveAIConfigs: (providers) => apiRequest('/api/v1/user/config/ai', { method: 'POST', body: JSON.stringify({ providers }) }) saveAIConfigs: (providers) => apiRequest('/api/v1/user/config/ai', { method: 'POST', body: JSON.stringify({ providers }) }),
getPreferences: () => apiRequest('/api/v1/user/preferences', { method: 'GET' }),
savePreferences: (prefs) => apiRequest('/api/v1/user/preferences', { method: 'POST', body: JSON.stringify({ preferences: prefs }) })
}; };
})(); })();
@@ -59,6 +59,7 @@ const DAWCommandDispatcher = (function() {
register('PROCESS_AUDIO_DSP', (args) => api.processAudioDsp(args)); register('PROCESS_AUDIO_DSP', (args) => api.processAudioDsp(args));
register('RENAME_TRACK', (args) => api.renameTrack(args)); register('RENAME_TRACK', (args) => api.renameTrack(args));
register('SCAN_TRACK', (args) => api.scanTrack(args)); register('SCAN_TRACK', (args) => api.scanTrack(args));
register('CUT_AUDIO', (args) => api.cutAudio(args));
register('SET_SELECTION', (args) => api.setSelection(args)); register('SET_SELECTION', (args) => api.setSelection(args));
register('SET_BPM', (args) => api.setBpm(args)); register('SET_BPM', (args) => api.setBpm(args));
register('SET_PLAYHEAD', (args) => api.setPlayhead(args)); register('SET_PLAYHEAD', (args) => api.setPlayhead(args));
Binary file not shown.