fix: sửa UI của bars và lưu thông tin vào profile

This commit is contained in:
2026-07-22 10:54:24 +07:00
parent 58089f40f9
commit b78193dfad
5 changed files with 180 additions and 71 deletions
+47 -51
View File
@@ -1,12 +1,31 @@
import json import json, os
from fastapi import APIRouter, HTTPException, Header, Depends from fastapi import APIRouter, HTTPException, Header, Depends
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
import time import time
from app.core.auth import decode_token from app.core.auth import decode_token
from app.config import settings
router = APIRouter() router = APIRouter()
DATA_FILE = os.path.join(settings.PROCESSED_DIR, "user_configs.json")
def _load_all():
if not os.path.exists(DATA_FILE):
return {"ai_configs": {}, "preferences": {}}
try:
with open(DATA_FILE, "r") as f:
return json.load(f)
except: return {"ai_configs": {}, "preferences": {}}
def _save_all(ai_configs=None, preferences=None):
data = _load_all()
if ai_configs is not None: data["ai_configs"] = ai_configs
if preferences is not None: data["preferences"] = preferences
os.makedirs(os.path.dirname(DATA_FILE), exist_ok=True)
with open(DATA_FILE, "w") as f:
json.dump(data, f, indent=2)
def _get_user_id(authorization): def _get_user_id(authorization):
if not authorization or not authorization.startswith("Bearer "): if not authorization or not authorization.startswith("Bearer "):
return "anonymous" return "anonymous"
@@ -16,9 +35,21 @@ def _get_user_id(authorization):
return "anonymous" return "anonymous"
return payload.get("user_id", "anonymous") return payload.get("user_id", "anonymous")
# In-memory / per-user AI provider configurations storage dictionary def _load_ai_configs():
USER_AI_CONFIGS = {} data = _load_all()
USER_PREFERENCES = {} return data.get("ai_configs", {})
def _load_preferences():
data = _load_all()
return data.get("preferences", {})
def _get_default_providers():
return [
{"id": "openai_default", "name": "OpenAI Official", "provider_type": "openai", "api_base_url": "https://api.openai.com/v1", "api_key": "", "model_name": "gpt-4o", "temperature": 0.7, "is_active": True},
{"id": "openai_compat_default", "name": "OpenAI Compatible (Ollama/LocalAI/DeepSeek)", "provider_type": "openai_compatible", "api_base_url": "http://localhost:11434/v1", "api_key": "ollama", "model_name": "deepseek-r1", "temperature": 0.7, "is_active": False},
{"id": "anthropic_default", "name": "Anthropic Claude", "provider_type": "anthropic", "api_base_url": "https://api.anthropic.com/v1", "api_key": "", "model_name": "claude-3-5-sonnet", "temperature": 0.7, "is_active": False},
{"id": "gemini_default", "name": "Google Gemini", "provider_type": "gemini", "api_base_url": "https://generativelanguage.googleapis.com", "api_key": "", "model_name": "gemini-1.5-pro", "temperature": 0.7, "is_active": False}
]
class AIProviderSetting(BaseModel): class AIProviderSetting(BaseModel):
id: str id: str
@@ -39,69 +70,34 @@ class SavePreferencesRequest(BaseModel):
@router.get("/preferences") @router.get("/preferences")
async def get_user_preferences(authorization: Optional[str] = Header(None)): async def get_user_preferences(authorization: Optional[str] = Header(None)):
uid = _get_user_id(authorization) uid = _get_user_id(authorization)
return {"success": True, "preferences": USER_PREFERENCES.get(uid, {})} prefs = _load_preferences()
return {"success": True, "preferences": prefs.get(uid, {})}
@router.post("/preferences") @router.post("/preferences")
async def save_user_preferences(req: SavePreferencesRequest, authorization: Optional[str] = Header(None)): async def save_user_preferences(req: SavePreferencesRequest, authorization: Optional[str] = Header(None)):
uid = _get_user_id(authorization) uid = _get_user_id(authorization)
USER_PREFERENCES[uid] = req.preferences prefs = _load_preferences()
prefs[uid] = req.preferences
_save_all(preferences=prefs)
return {"success": True, "message": "Đã lưu cấu hình người dùng."} 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(authorization: Optional[str] = Header(None)): async def get_user_ai_config(authorization: Optional[str] = Header(None)):
uid = _get_user_id(authorization) uid = _get_user_id(authorization)
if uid not in USER_AI_CONFIGS: configs = _load_ai_configs()
USER_AI_CONFIGS[uid] = [ if uid not in configs:
{ configs[uid] = _get_default_providers()
"id": "openai_default",
"name": "OpenAI Official",
"provider_type": "openai",
"api_base_url": "https://api.openai.com/v1",
"api_key": "",
"model_name": "gpt-4o",
"temperature": 0.7,
"is_active": True
},
{
"id": "openai_compat_default",
"name": "OpenAI Compatible (Ollama/LocalAI/DeepSeek)",
"provider_type": "openai_compatible",
"api_base_url": "http://localhost:11434/v1",
"api_key": "ollama",
"model_name": "deepseek-r1",
"temperature": 0.7,
"is_active": False
},
{
"id": "anthropic_default",
"name": "Anthropic Claude",
"provider_type": "anthropic",
"api_base_url": "https://api.anthropic.com/v1",
"api_key": "",
"model_name": "claude-3-5-sonnet",
"temperature": 0.7,
"is_active": False
},
{
"id": "gemini_default",
"name": "Google Gemini",
"provider_type": "gemini",
"api_base_url": "https://generativelanguage.googleapis.com",
"api_key": "",
"model_name": "gemini-1.5-pro",
"temperature": 0.7,
"is_active": False
}
]
return { return {
"success": True, "success": True,
"providers": USER_AI_CONFIGS[uid] "providers": configs[uid]
} }
@router.post("/config/ai") @router.post("/config/ai")
async def save_user_ai_config(req: SaveAIConfigRequest, authorization: Optional[str] = Header(None)): async def save_user_ai_config(req: SaveAIConfigRequest, authorization: Optional[str] = Header(None)):
uid = _get_user_id(authorization) uid = _get_user_id(authorization)
USER_AI_CONFIGS[uid] = [p.dict() for p in req.providers] configs = _load_ai_configs()
configs[uid] = [p.dict() for p in req.providers]
_save_all(ai_configs=configs)
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!"
+42 -10
View File
@@ -751,6 +751,21 @@ const TempoTrackLane = ({
ctx.font = 'bold 9px Inter, sans-serif'; ctx.font = 'bold 9px Inter, sans-serif';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillText(`${Math.ceil(beatNum / 4)}`, localX + 3, 11); ctx.fillText(`${Math.ceil(beatNum / 4)}`, localX + 3, 11);
} else if (zoom >= 2) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(localX, 0);
ctx.lineTo(localX, height);
ctx.stroke();
if (zoom >= 5) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.font = '8px Inter, sans-serif';
ctx.textAlign = 'left';
const bar = Math.ceil(beatNum / 4);
const beat = ((beatNum - 1) % 4) + 1;
ctx.fillText(`${bar}:${beat}`, localX + 2, 9);
}
} else { } else {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
ctx.lineWidth = 1; ctx.lineWidth = 1;
@@ -2590,7 +2605,9 @@ const AIConfigModal = ({
className: "text-xs uppercase font-bold text-slate-400 block mb-2" className: "text-xs uppercase font-bold text-slate-400 block mb-2"
}, "Providers"), providers.map(p => /*#__PURE__*/React.createElement("button", { }, "Providers"), providers.map(p => /*#__PURE__*/React.createElement("button", {
key: p.id, key: p.id,
tabIndex: 0,
onClick: () => setSelectedId(p.id), onClick: () => setSelectedId(p.id),
onKeyDown: e => { if (e.key === ' ') { e.preventDefault(); updateProviderField(p.id, 'is_active', !p.is_active); } },
className: `w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId === p.id ? 'bg-cyan-600 text-white shadow' : 'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}` className: `w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId === p.id ? 'bg-cyan-600 text-white shadow' : 'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}`
}, /*#__PURE__*/React.createElement("span", { }, /*#__PURE__*/React.createElement("span", {
className: "truncate" className: "truncate"
@@ -2627,7 +2644,15 @@ const AIConfigModal = ({
} }
}, },
className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded" className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"
}, "Xóa"))), activeProvider && /*#__PURE__*/React.createElement("form", { }, "Xóa"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
onClick: function(e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos > 0) { var tmp = provs[pos]; provs[pos] = provs[pos-1]; provs[pos-1] = tmp; setProviders(provs); } },
className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
title: "Di chuyển lên"
}, "▲"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
onClick: function(e) { e.stopPropagation(); var provs = providers.slice(); var pos = -1; for (var pi = 0; pi < provs.length; pi++) { if (provs[pi].id === selectedId) { pos = pi; break; } } if (pos < provs.length - 1) { var tmp = provs[pos]; provs[pos] = provs[pos+1]; provs[pos+1] = tmp; setProviders(provs); } },
className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
title: "Di chuyển xuống"
}, "▼"))), activeProvider && /*#__PURE__*/React.createElement("form", {
onSubmit: handleSave, onSubmit: handleSave,
className: "col-span-2 space-y-3.5" className: "col-span-2 space-y-3.5"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", { }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
@@ -2993,6 +3018,8 @@ const App = () => {
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [selectionStart, setSelectionStart] = useState(null); const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null); const [selectionEnd, setSelectionEnd] = useState(null);
const selectionRef = useRef({ start: null, end: null });
selectionRef.current = { start: selectionStart, end: selectionEnd };
const [selectionMode, setSelectionMode] = useState(null); // 'global' (from ruler) | 'local' (from track) const [selectionMode, setSelectionMode] = useState(null); // 'global' (from ruler) | 'local' (from track)
const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null); const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
const [localSelectionStart, setLocalSelectionStart] = useState(null); const [localSelectionStart, setLocalSelectionStart] = useState(null);
@@ -7050,7 +7077,7 @@ const App = () => {
if (window.DAWCommandDispatcher) { if (window.DAWCommandDispatcher) {
try { try {
const cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments); const cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
setAiActionLog(prev => [...prev, { type: 'status', text: `${fc.name}: ${cmdResult.success ? 'thành công' : 'thất bại'}`, time: Date.now() }]); setAiActionLog(prev => [...prev, { type: 'status', text: `${fc.name}: ${cmdResult.success ? 'thành công' : 'thất bại: ' + (cmdResult.error || 'unknown')}`, time: Date.now() }]);
} catch (cmdErr) { } catch (cmdErr) {
setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: ${cmdErr.message}`, time: Date.now() }]); setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: ${cmdErr.message}`, time: Date.now() }]);
} }
@@ -7372,6 +7399,7 @@ const App = () => {
setSelectionMode('global'); setSelectionMode('global');
setSelectionStart(start); setSelectionStart(start);
setSelectionEnd(end); setSelectionEnd(end);
selectionRef.current = { start, 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) => { cutAudio: (args) => {
@@ -7380,15 +7408,18 @@ const App = () => {
if (!track) return { success: false, error: 'Track not found' }; if (!track) return { success: false, error: 'Track not found' };
if (!track.buffer) return { success: false, error: 'Track has no audio buffer' }; if (!track.buffer) return { success: false, error: 'Track has no audio buffer' };
const barDur = 60 / parseInt(bpm || 120) * 4; const barDur = 60 / parseInt(bpm || 120) * 4;
const sel = selectionRef.current;
let rawStart, rawEnd; let rawStart, rawEnd;
if (args.start_time !== undefined) rawStart = args.start_time; if (args.start_time !== undefined && args.start_time !== null) rawStart = args.start_time;
else if (args.start_bar !== undefined) rawStart = args.start_bar * barDur; else if (args.start_bar !== undefined && args.start_bar !== null) rawStart = args.start_bar * barDur;
else if (selectionStart !== null) rawStart = selectionStart; else if (sel.start !== null) rawStart = sel.start;
else return { success: false, error: 'No selection or start position provided. Use set_selection first or provide start_time/start_bar.' }; else rawStart = currentTime;
if (args.end_time !== undefined) rawEnd = args.end_time; if (args.end_time !== undefined && args.end_time !== null) rawEnd = args.end_time;
else if (args.length_bars !== undefined) rawEnd = rawStart + args.length_bars * barDur; else if (args.end_bar !== undefined && args.end_bar !== null) rawEnd = args.end_bar * barDur;
else if (selectionEnd !== null && selectionEnd > rawStart) rawEnd = selectionEnd; else if (args.length_bars !== undefined && args.length_bars !== null) rawEnd = rawStart + args.length_bars * barDur;
else return { success: false, error: 'No end position provided. Use set_selection first or provide end_time/length_bars.' }; else if (sel.end !== null && sel.end > rawStart) rawEnd = sel.end;
else return { success: false, error: 'No end position provided. Provide end_time, end_bar, or length_bars.' };
if (rawEnd <= rawStart) return { success: false, error: 'End position must be after start position.' };
const buffer = track.buffer; const buffer = track.buffer;
const ctx = getAudioContext(); const ctx = getAudioContext();
const snap = args.snap_silence !== false; const snap = args.snap_silence !== false;
@@ -7424,6 +7455,7 @@ const App = () => {
return updated; return updated;
}); });
setSelectedTrackId(newId); setSelectedTrackId(newId);
selectionRef.current = { start: 0, end: slicedBuffer.duration };
clearLocalSelection(); clearLocalSelection();
setSelectionMode('global'); setSelectionMode('global');
setSelectionStart(0); setSelectionStart(0);
+86 -6
View File
@@ -751,6 +751,21 @@ const TempoTrackLane = ({
ctx.font = 'bold 9px Inter, sans-serif'; ctx.font = 'bold 9px Inter, sans-serif';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillText(`${Math.ceil(beatNum / 4)}`, localX + 3, 11); ctx.fillText(`${Math.ceil(beatNum / 4)}`, localX + 3, 11);
} else if (zoom >= 2) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(localX, 0);
ctx.lineTo(localX, height);
ctx.stroke();
if (zoom >= 5) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
ctx.font = '8px Inter, sans-serif';
ctx.textAlign = 'left';
const bar = Math.ceil(beatNum / 4);
const beat = (beatNum - 1) % 4 + 1;
ctx.fillText(`${bar}:${beat}`, localX + 2, 9);
}
} else { } else {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'; ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
ctx.lineWidth = 1; ctx.lineWidth = 1;
@@ -2590,7 +2605,14 @@ const AIConfigModal = ({
className: "text-xs uppercase font-bold text-slate-400 block mb-2" className: "text-xs uppercase font-bold text-slate-400 block mb-2"
}, "Providers"), providers.map(p => /*#__PURE__*/React.createElement("button", { }, "Providers"), providers.map(p => /*#__PURE__*/React.createElement("button", {
key: p.id, key: p.id,
tabIndex: 0,
onClick: () => setSelectedId(p.id), onClick: () => setSelectedId(p.id),
onKeyDown: e => {
if (e.key === ' ') {
e.preventDefault();
updateProviderField(p.id, 'is_active', !p.is_active);
}
},
className: `w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId === p.id ? 'bg-cyan-600 text-white shadow' : 'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}` className: `w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId === p.id ? 'bg-cyan-600 text-white shadow' : 'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}`
}, /*#__PURE__*/React.createElement("span", { }, /*#__PURE__*/React.createElement("span", {
className: "truncate" className: "truncate"
@@ -2627,7 +2649,47 @@ const AIConfigModal = ({
} }
}, },
className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded" className: "px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"
}, "Xóa"))), activeProvider && /*#__PURE__*/React.createElement("form", { }, "Xóa"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
onClick: function (e) {
e.stopPropagation();
var provs = providers.slice();
var pos = -1;
for (var pi = 0; pi < provs.length; pi++) {
if (provs[pi].id === selectedId) {
pos = pi;
break;
}
}
if (pos > 0) {
var tmp = provs[pos];
provs[pos] = provs[pos - 1];
provs[pos - 1] = tmp;
setProviders(provs);
}
},
className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
title: "Di chuyển lên"
}, "▲"), providers.length > 1 && /*#__PURE__*/React.createElement("button", {
onClick: function (e) {
e.stopPropagation();
var provs = providers.slice();
var pos = -1;
for (var pi = 0; pi < provs.length; pi++) {
if (provs[pi].id === selectedId) {
pos = pi;
break;
}
}
if (pos < provs.length - 1) {
var tmp = provs[pos];
provs[pos] = provs[pos + 1];
provs[pos + 1] = tmp;
setProviders(provs);
}
},
className: "px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",
title: "Di chuyển xuống"
}, "▼"))), activeProvider && /*#__PURE__*/React.createElement("form", {
onSubmit: handleSave, onSubmit: handleSave,
className: "col-span-2 space-y-3.5" className: "col-span-2 space-y-3.5"
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", { }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
@@ -2993,6 +3055,14 @@ const App = () => {
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [selectionStart, setSelectionStart] = useState(null); const [selectionStart, setSelectionStart] = useState(null);
const [selectionEnd, setSelectionEnd] = useState(null); const [selectionEnd, setSelectionEnd] = useState(null);
const selectionRef = useRef({
start: null,
end: null
});
selectionRef.current = {
start: selectionStart,
end: selectionEnd
};
const [selectionMode, setSelectionMode] = useState(null); // 'global' (from ruler) | 'local' (from track) const [selectionMode, setSelectionMode] = useState(null); // 'global' (from ruler) | 'local' (from track)
const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null); const [localSelectionTrackId, setLocalSelectionTrackId] = useState(null);
const [localSelectionStart, setLocalSelectionStart] = useState(null); const [localSelectionStart, setLocalSelectionStart] = useState(null);
@@ -7117,7 +7187,7 @@ const App = () => {
const cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments); const cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
setAiActionLog(prev => [...prev, { setAiActionLog(prev => [...prev, {
type: 'status', type: 'status',
text: `${fc.name}: ${cmdResult.success ? 'thành công' : 'thất bại'}`, text: `${fc.name}: ${cmdResult.success ? 'thành công' : 'thất bại: ' + (cmdResult.error || 'unknown')}`,
time: Date.now() time: Date.now()
}]); }]);
} catch (cmdErr) { } catch (cmdErr) {
@@ -7536,6 +7606,10 @@ const App = () => {
setSelectionMode('global'); setSelectionMode('global');
setSelectionStart(start); setSelectionStart(start);
setSelectionEnd(end); setSelectionEnd(end);
selectionRef.current = {
start,
end
};
return { return {
success: true, success: true,
start: parseFloat(start.toFixed(3)), start: parseFloat(start.toFixed(3)),
@@ -7555,14 +7629,16 @@ const App = () => {
error: 'Track has no audio buffer' error: 'Track has no audio buffer'
}; };
const barDur = 60 / parseInt(bpm || 120) * 4; const barDur = 60 / parseInt(bpm || 120) * 4;
const sel = selectionRef.current;
let rawStart, rawEnd; 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 { if (args.start_time !== undefined && args.start_time !== null) rawStart = args.start_time;else if (args.start_bar !== undefined && args.start_bar !== null) rawStart = args.start_bar * barDur;else if (sel.start !== null) rawStart = sel.start;else rawStart = currentTime;
if (args.end_time !== undefined && args.end_time !== null) rawEnd = args.end_time;else if (args.end_bar !== undefined && args.end_bar !== null) rawEnd = args.end_bar * barDur;else if (args.length_bars !== undefined && args.length_bars !== null) rawEnd = rawStart + args.length_bars * barDur;else if (sel.end !== null && sel.end > rawStart) rawEnd = sel.end;else return {
success: false, success: false,
error: 'No selection or start position provided. Use set_selection first or provide start_time/start_bar.' error: 'No end position provided. Provide end_time, end_bar, or length_bars.'
}; };
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 { if (rawEnd <= rawStart) return {
success: false, success: false,
error: 'No end position provided. Use set_selection first or provide end_time/length_bars.' error: 'End position must be after start position.'
}; };
const buffer = track.buffer; const buffer = track.buffer;
const ctx = getAudioContext(); const ctx = getAudioContext();
@@ -7615,6 +7691,10 @@ const App = () => {
return updated; return updated;
}); });
setSelectedTrackId(newId); setSelectedTrackId(newId);
selectionRef.current = {
start: 0,
end: slicedBuffer.duration
};
clearLocalSelection(); clearLocalSelection();
setSelectionMode('global'); setSelectionMode('global');
setSelectionStart(0); setSelectionStart(0);
+5 -4
View File
@@ -179,16 +179,17 @@ const AIGateway = (function() {
} }
}, { }, {
name: 'cut_audio', 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', description: 'Cắt một đoạn audio từ track nguồn. Tự động tạo selection, snap zero-crossing, tạo track mới chứa đoạn cắt. Lệnh DUY NHẤT cho thao tác cắt - không cần gọi set_selection trước.',
parameters: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
track_id: { type: 'string', description: 'ID của track nguồn' }, 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.' }, start_time: { type: 'number', description: 'Vị trí bắt đầu cắt (giây). Mặc định: dùng playhead 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.' }, 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.' }, 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.' }, end_bar: { type: 'number', description: 'Bar kết thúc cắt. VD: end_bar=12 cắt đến bar 12. Dùng thay cho end_time.' },
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)' }, length_bars: { type: 'number', description: 'Độ dài cắt (bar). Dùng cùng start_bar thay cho end_bar.' },
snap_silence: { type: 'boolean', description: 'Tự động snap vào điểm zero-crossing 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>"' } new_track_name: { type: 'string', description: 'Tên cho track mới. Mặc định: "Cut_<tên_track_gốc>"' }
} }
} }
Binary file not shown.