fix: xử lý phân quyền tải file từ lệnh AI và hỗ trợ nén MP3/OGG qua API máy chủ

This commit is contained in:
2026-07-22 18:23:20 +07:00
parent d39b3f74ff
commit 7136cbe904
3 changed files with 334 additions and 68 deletions
+159 -28
View File
@@ -5089,13 +5089,15 @@ const App = () => {
}, [tracks, selectedTrackId, selLeft, selRight]); }, [tracks, selectedTrackId, selLeft, selRight]);
// Toast helper // Toast helper
const showToast = (text, type = 'info') => { const showToast = (text, type = 'info', actionText = null, onActionClick = null) => {
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current); if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
setToastMessage({ setToastMessage({
text, text,
type type,
actionText,
onActionClick
}); });
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), 3500); toastTimeoutRef.current = setTimeout(() => setToastMessage(null), actionText ? 8000 : 3500);
}; };
// Server-side upload // Server-side upload
@@ -6605,12 +6607,63 @@ const App = () => {
type: 'audio/wav' type: 'audio/wav'
}); });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
a.href = url; setIsExporting(true);
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`; showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
a.click(); try {
const file = new File([blob], `session_export.wav`, { type: 'audio/wav' });
const formData = new FormData();
formData.append('file', file);
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
method: 'POST',
body: formData
});
if (!uploadResp.ok) throw new Error("Upload to transcode server failed");
const uploadData = await uploadResp.json();
const fileId = uploadData.file_id;
const exportResp = await fetch(`${API_AUDIO}/export`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file_id: fileId,
format: exportSettings.format,
sample_rate: targetRate,
bit_depth: bitDepth
})
});
if (!exportResp.ok) throw new Error("Transcode request failed");
const exportData = await exportResp.json();
const result = await pollTaskResult(exportData.task_id, 20);
if (result.success) {
const outId = result.output_file_id;
const downloadUrl = `${API_AUDIO}/download/${outId}`;
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;
a.click();
showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`, "success");
} else {
throw new Error(result.error || 'Server encoding failed');
}
} catch (transcodeErr) {
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải dạng WAV thay thế.`, "warning");
const a = document.createElement('a');
a.href = url;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
a.click();
}
} else {
if (exportSettings.format !== 'wav') {
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
}
const a = document.createElement('a');
a.href = url;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
a.click();
showToast("Xuất bản âm thanh hoàn tất!", "success");
}
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showToast("Xuất bản âm thanh hoàn tất!", "success");
} catch (err) { } catch (err) {
showToast("Lỗi xuất âm thanh: " + err.message, "error"); showToast("Lỗi xuất âm thanh: " + err.message, "error");
} finally { } finally {
@@ -7239,19 +7292,28 @@ const App = () => {
} }
if (hasCalls) { if (hasCalls) {
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi ${result.functionCalls.length} lệnh...`, time: Date.now() }]); setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi ${result.functionCalls.length} lệnh...`, time: Date.now() }]);
for (const fc of result.functionCalls) { if (window.DAWCommandDispatcher) {
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]); window.DAWCommandDispatcher.isExecutingAI = true;
const cmdName = fc.name.toUpperCase(); }
if (window.DAWCommandDispatcher) { try {
try { for (const fc of result.functionCalls) {
let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments); setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult; const cmdName = fc.name.toUpperCase();
setAiActionLog(prev => [...prev, { type: 'status', text: `${fc.name}: ${cmdResult && cmdResult.success ? 'thành công' : 'thất bại: ' + ((cmdResult && cmdResult.error) || 'unknown')}`, time: Date.now() }]); if (window.DAWCommandDispatcher) {
} catch (cmdErr) { try {
setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: ${cmdErr.message}`, time: Date.now() }]); let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult;
setAiActionLog(prev => [...prev, { type: 'status', text: `${fc.name}: ${cmdResult && cmdResult.success ? 'thành công' : 'thất bại: ' + ((cmdResult && cmdResult.error) || 'unknown')}`, time: Date.now() }]);
} catch (cmdErr) {
setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: ${cmdErr.message}`, time: Date.now() }]);
}
} else {
setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: DAWCommandDispatcher not available`, time: Date.now() }]);
} }
} else { }
setAiActionLog(prev => [...prev, { type: 'error', text: `${fc.name}: DAWCommandDispatcher not available`, time: Date.now() }]); } finally {
if (window.DAWCommandDispatcher) {
window.DAWCommandDispatcher.isExecutingAI = false;
} }
} }
} }
@@ -7782,13 +7844,76 @@ const App = () => {
ofs += bps; ofs += bps;
} }
} }
const blob = new Blob([fileBuf], { type: 'audio/' + fmt }); const blob = new Blob([fileBuf], { type: 'audio/wav' });
const url = URL.createObjectURL(blob); const localUrl = URL.createObjectURL(blob);
const a = document.createElement('a'); const targetFilename = `export_${Date.now()}.${fmt}`;
a.href = url;
a.download = `export_${Date.now()}.${fmt}`; const triggerDownload = (downloadUrl, finalFilename) => {
a.click(); if (window.DAWCommandDispatcher?.isExecutingAI) {
URL.revokeObjectURL(url); showToast(`Xuất nhạc thành công (${fmt.toUpperCase()})!`, "success", "Tải về", () => {
const a = document.createElement('a');
a.href = downloadUrl;
a.download = finalFilename;
a.click();
if (downloadUrl.startsWith('blob:')) {
URL.revokeObjectURL(downloadUrl);
}
});
} else {
const a = document.createElement('a');
a.href = downloadUrl;
a.download = finalFilename;
a.click();
showToast("Xuất bản âm thanh hoàn tất!", "success");
if (downloadUrl.startsWith('blob:')) {
URL.revokeObjectURL(downloadUrl);
}
}
};
if ((fmt === 'mp3' || fmt === 'ogg') && serverStatus === 'connected') {
try {
const file = new File([blob], `export_ai.wav`, { type: 'audio/wav' });
const formData = new FormData();
formData.append('file', file);
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
method: 'POST',
body: formData
});
if (!uploadResp.ok) throw new Error("Upload failed");
const uploadData = await uploadResp.json();
const uploadId = uploadData.file_id;
const exportResp = await fetch(`${API_AUDIO}/export`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file_id: uploadId,
format: fmt,
sample_rate: sr,
bit_depth: bd
})
});
if (!exportResp.ok) throw new Error("Export failed");
const exportData = await exportResp.json();
const result = await pollTaskResult(exportData.task_id, 20);
if (result.success) {
const downloadUrl = `${API_AUDIO}/download/${result.output_file_id}`;
triggerDownload(downloadUrl, targetFilename);
} else {
throw new Error(result.error || 'Server encoding failed');
}
} catch (transcodeErr) {
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải về dạng WAV thay thế.`, "warning");
triggerDownload(localUrl, `export_${Date.now()}.wav`);
}
} else {
const finalFilename = fmt === 'wav' ? `export_${Date.now()}.wav` : `export_${Date.now()}.wav`;
if (fmt !== 'wav') {
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
}
triggerDownload(localUrl, finalFilename);
}
return { success: true, trackId: tid, range: parseFloat((rawEnd - rawStart).toFixed(3)) + 's', format: fmt, channels: numCh === 1 ? 'mono' : 'stereo' }; return { success: true, trackId: tid, range: parseFloat((rawEnd - rawStart).toFixed(3)) + 's', format: fmt, channels: numCh === 1 ? 'mono' : 'stereo' };
}, },
selectItem: (args) => { selectItem: (args) => {
@@ -10461,7 +10586,13 @@ const App = () => {
}, /*#__PURE__*/React.createElement("i", { }, /*#__PURE__*/React.createElement("i", {
"data-lucide": "bell", "data-lucide": "bell",
className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}` className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}`
})), toastMessage.text), /*#__PURE__*/React.createElement(AuthModal, { })), toastMessage.text, toastMessage.onActionClick && /*#__PURE__*/React.createElement("button", {
onClick: () => {
toastMessage.onActionClick();
setToastMessage(null);
},
className: "ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"
}, toastMessage.actionText || 'Tải về')), /*#__PURE__*/React.createElement(AuthModal, {
isOpen: authModalOpen, isOpen: authModalOpen,
mode: authMode, mode: authMode,
forceMandatory: isMandatoryLogin, forceMandatory: isMandatoryLogin,
+175 -40
View File
@@ -5144,13 +5144,15 @@ const App = () => {
}, [tracks, selectedTrackId, selLeft, selRight]); }, [tracks, selectedTrackId, selLeft, selRight]);
// ── Toast helper ── // ── Toast helper ──
const showToast = (text, type = 'info') => { const showToast = (text, type = 'info', actionText = null, onActionClick = null) => {
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current); if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
setToastMessage({ setToastMessage({
text, text,
type type,
actionText,
onActionClick
}); });
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), 3500); toastTimeoutRef.current = setTimeout(() => setToastMessage(null), actionText ? 8000 : 3500);
}; };
// ── Server-side upload ── // ── Server-side upload ──
@@ -6738,12 +6740,66 @@ const App = () => {
type: 'audio/wav' type: 'audio/wav'
}); });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
a.href = url; setIsExporting(true);
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`; showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
a.click(); try {
const file = new File([blob], `session_export.wav`, {
type: 'audio/wav'
});
const formData = new FormData();
formData.append('file', file);
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
method: 'POST',
body: formData
});
if (!uploadResp.ok) throw new Error("Upload to transcode server failed");
const uploadData = await uploadResp.json();
const fileId = uploadData.file_id;
const exportResp = await fetch(`${API_AUDIO}/export`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
file_id: fileId,
format: exportSettings.format,
sample_rate: targetRate,
bit_depth: bitDepth
})
});
if (!exportResp.ok) throw new Error("Transcode request failed");
const exportData = await exportResp.json();
const result = await pollTaskResult(exportData.task_id, 20);
if (result.success) {
const outId = result.output_file_id;
const downloadUrl = `${API_AUDIO}/download/${outId}`;
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;
a.click();
showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`, "success");
} else {
throw new Error(result.error || 'Server encoding failed');
}
} catch (transcodeErr) {
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải dạng WAV thay thế.`, "warning");
const a = document.createElement('a');
a.href = url;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
a.click();
}
} else {
if (exportSettings.format !== 'wav') {
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
}
const a = document.createElement('a');
a.href = url;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
a.click();
showToast("Xuất bản âm thanh hoàn tất!", "success");
}
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
showToast("Xuất bản âm thanh hoàn tất!", "success");
} catch (err) { } catch (err) {
showToast("Lỗi xuất âm thanh: " + err.message, "error"); showToast("Lỗi xuất âm thanh: " + err.message, "error");
} finally { } finally {
@@ -7396,35 +7452,44 @@ const App = () => {
text: ` Gọi ${result.functionCalls.length} lệnh...`, text: ` Gọi ${result.functionCalls.length} lệnh...`,
time: Date.now() time: Date.now()
}]); }]);
for (const fc of result.functionCalls) { if (window.DAWCommandDispatcher) {
setAiActionLog(prev => [...prev, { window.DAWCommandDispatcher.isExecutingAI = true;
type: 'info', }
text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, try {
time: Date.now() for (const fc of result.functionCalls) {
}]); setAiActionLog(prev => [...prev, {
const cmdName = fc.name.toUpperCase(); type: 'info',
if (window.DAWCommandDispatcher) { text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`,
try { time: Date.now()
let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments); }]);
if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult; const cmdName = fc.name.toUpperCase();
setAiActionLog(prev => [...prev, { if (window.DAWCommandDispatcher) {
type: 'status', try {
text: `${fc.name}: ${cmdResult && cmdResult.success ? 'thành công' : 'thất bại: ' + (cmdResult && cmdResult.error || 'unknown')}`, let cmdResult = window.DAWCommandDispatcher.execute(cmdName, fc.arguments);
time: Date.now() if (cmdResult && typeof cmdResult.then === 'function') cmdResult = await cmdResult;
}]); setAiActionLog(prev => [...prev, {
} catch (cmdErr) { type: 'status',
text: `${fc.name}: ${cmdResult && cmdResult.success ? 'thành công' : 'thất bại: ' + (cmdResult && cmdResult.error || 'unknown')}`,
time: Date.now()
}]);
} catch (cmdErr) {
setAiActionLog(prev => [...prev, {
type: 'error',
text: `${fc.name}: ${cmdErr.message}`,
time: Date.now()
}]);
}
} else {
setAiActionLog(prev => [...prev, { setAiActionLog(prev => [...prev, {
type: 'error', type: 'error',
text: `${fc.name}: ${cmdErr.message}`, text: `${fc.name}: DAWCommandDispatcher not available`,
time: Date.now() time: Date.now()
}]); }]);
} }
} else { }
setAiActionLog(prev => [...prev, { } finally {
type: 'error', if (window.DAWCommandDispatcher) {
text: `${fc.name}: DAWCommandDispatcher not available`, window.DAWCommandDispatcher.isExecutingAI = false;
time: Date.now()
}]);
} }
} }
} }
@@ -8110,14 +8175,78 @@ const App = () => {
} }
} }
const blob = new Blob([fileBuf], { const blob = new Blob([fileBuf], {
type: 'audio/' + fmt type: 'audio/wav'
}); });
const url = URL.createObjectURL(blob); const localUrl = URL.createObjectURL(blob);
const a = document.createElement('a'); const targetFilename = `export_${Date.now()}.${fmt}`;
a.href = url; const triggerDownload = (downloadUrl, finalFilename) => {
a.download = `export_${Date.now()}.${fmt}`; if (window.DAWCommandDispatcher?.isExecutingAI) {
a.click(); showToast(`Xuất nhạc thành công (${fmt.toUpperCase()})!`, "success", "Tải về", () => {
URL.revokeObjectURL(url); const a = document.createElement('a');
a.href = downloadUrl;
a.download = finalFilename;
a.click();
if (downloadUrl.startsWith('blob:')) {
URL.revokeObjectURL(downloadUrl);
}
});
} else {
const a = document.createElement('a');
a.href = downloadUrl;
a.download = finalFilename;
a.click();
showToast("Xuất bản âm thanh hoàn tất!", "success");
if (downloadUrl.startsWith('blob:')) {
URL.revokeObjectURL(downloadUrl);
}
}
};
if ((fmt === 'mp3' || fmt === 'ogg') && serverStatus === 'connected') {
try {
const file = new File([blob], `export_ai.wav`, {
type: 'audio/wav'
});
const formData = new FormData();
formData.append('file', file);
const uploadResp = await fetch(`${API_AUDIO}/upload`, {
method: 'POST',
body: formData
});
if (!uploadResp.ok) throw new Error("Upload failed");
const uploadData = await uploadResp.json();
const uploadId = uploadData.file_id;
const exportResp = await fetch(`${API_AUDIO}/export`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
file_id: uploadId,
format: fmt,
sample_rate: sr,
bit_depth: bd
})
});
if (!exportResp.ok) throw new Error("Export failed");
const exportData = await exportResp.json();
const result = await pollTaskResult(exportData.task_id, 20);
if (result.success) {
const downloadUrl = `${API_AUDIO}/download/${result.output_file_id}`;
triggerDownload(downloadUrl, targetFilename);
} else {
throw new Error(result.error || 'Server encoding failed');
}
} catch (transcodeErr) {
showToast(`Lỗi chuyển đổi: ${transcodeErr.message}. Tải về dạng WAV thay thế.`, "warning");
triggerDownload(localUrl, `export_${Date.now()}.wav`);
}
} else {
const finalFilename = fmt === 'wav' ? `export_${Date.now()}.wav` : `export_${Date.now()}.wav`;
if (fmt !== 'wav') {
showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.", "warning");
}
triggerDownload(localUrl, finalFilename);
}
return { return {
success: true, success: true,
trackId: tid, trackId: tid,
@@ -10972,7 +11101,13 @@ const App = () => {
}, /*#__PURE__*/React.createElement("i", { }, /*#__PURE__*/React.createElement("i", {
"data-lucide": "bell", "data-lucide": "bell",
className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}` className: `w-4 h-4 ${toastMessage.type === 'success' ? 'text-emerald-400' : toastMessage.type === 'error' ? 'text-rose-400' : toastMessage.type === 'warning' ? 'text-amber-400' : 'text-cyan-400'}`
})), toastMessage.text), /*#__PURE__*/React.createElement(AuthModal, { })), toastMessage.text, toastMessage.onActionClick && /*#__PURE__*/React.createElement("button", {
onClick: () => {
toastMessage.onActionClick();
setToastMessage(null);
},
className: "ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"
}, toastMessage.actionText || 'Tải về')), /*#__PURE__*/React.createElement(AuthModal, {
isOpen: authModalOpen, isOpen: authModalOpen,
mode: authMode, mode: authMode,
forceMandatory: isMandatoryLogin, forceMandatory: isMandatoryLogin,
Binary file not shown.