fix: sửa UI của bars và lưu thông tin vào profile
This commit is contained in:
+47
-51
@@ -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
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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, và 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.
Reference in New Issue
Block a user