fix: thêm và sửa các tools của AI
This commit is contained in:
+36
-10
@@ -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
@@ -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", {
|
||||||
|
|||||||
@@ -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", {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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.
Reference in New Issue
Block a user