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]);
// Toast helper
const showToast = (text, type = 'info') => {
const showToast = (text, type = 'info', actionText = null, onActionClick = null) => {
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
setToastMessage({
text,
type
type,
actionText,
onActionClick
});
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), 3500);
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), actionText ? 8000 : 3500);
};
// Server-side upload
@@ -6605,12 +6607,63 @@ const App = () => {
type: 'audio/wav'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
a.click();
if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
setIsExporting(true);
showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
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);
showToast("Xuất bản âm thanh hoàn tất!", "success");
} catch (err) {
showToast("Lỗi xuất âm thanh: " + err.message, "error");
} finally {
@@ -7239,19 +7292,28 @@ const App = () => {
}
if (hasCalls) {
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi ${result.functionCalls.length} lệnh...`, time: Date.now() }]);
for (const fc of result.functionCalls) {
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
const cmdName = fc.name.toUpperCase();
if (window.DAWCommandDispatcher) {
try {
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() }]);
if (window.DAWCommandDispatcher) {
window.DAWCommandDispatcher.isExecutingAI = true;
}
try {
for (const fc of result.functionCalls) {
setAiActionLog(prev => [...prev, { type: 'info', text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`, time: Date.now() }]);
const cmdName = fc.name.toUpperCase();
if (window.DAWCommandDispatcher) {
try {
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;
}
}
const blob = new Blob([fileBuf], { type: 'audio/' + fmt });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `export_${Date.now()}.${fmt}`;
a.click();
URL.revokeObjectURL(url);
const blob = new Blob([fileBuf], { type: 'audio/wav' });
const localUrl = URL.createObjectURL(blob);
const targetFilename = `export_${Date.now()}.${fmt}`;
const triggerDownload = (downloadUrl, finalFilename) => {
if (window.DAWCommandDispatcher?.isExecutingAI) {
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' };
},
selectItem: (args) => {
@@ -10461,7 +10586,13 @@ const App = () => {
}, /*#__PURE__*/React.createElement("i", {
"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'}`
})), 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,
mode: authMode,
forceMandatory: isMandatoryLogin,
+175 -40
View File
@@ -5144,13 +5144,15 @@ const App = () => {
}, [tracks, selectedTrackId, selLeft, selRight]);
// ── Toast helper ──
const showToast = (text, type = 'info') => {
const showToast = (text, type = 'info', actionText = null, onActionClick = null) => {
if (toastTimeoutRef.current) clearTimeout(toastTimeoutRef.current);
setToastMessage({
text,
type
type,
actionText,
onActionClick
});
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), 3500);
toastTimeoutRef.current = setTimeout(() => setToastMessage(null), actionText ? 8000 : 3500);
};
// ── Server-side upload ──
@@ -6738,12 +6740,66 @@ const App = () => {
type: 'audio/wav'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;
a.click();
if ((exportSettings.format === 'mp3' || exportSettings.format === 'ogg') && serverStatus === 'connected') {
setIsExporting(true);
showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...", "info");
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);
showToast("Xuất bản âm thanh hoàn tất!", "success");
} catch (err) {
showToast("Lỗi xuất âm thanh: " + err.message, "error");
} finally {
@@ -7396,35 +7452,44 @@ const App = () => {
text: ` Gọi ${result.functionCalls.length} lệnh...`,
time: Date.now()
}]);
for (const fc of result.functionCalls) {
setAiActionLog(prev => [...prev, {
type: 'info',
text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`,
time: Date.now()
}]);
const cmdName = fc.name.toUpperCase();
if (window.DAWCommandDispatcher) {
try {
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) {
if (window.DAWCommandDispatcher) {
window.DAWCommandDispatcher.isExecutingAI = true;
}
try {
for (const fc of result.functionCalls) {
setAiActionLog(prev => [...prev, {
type: 'info',
text: ` Gọi lệnh: ${fc.name}(${JSON.stringify(fc.arguments)})`,
time: Date.now()
}]);
const cmdName = fc.name.toUpperCase();
if (window.DAWCommandDispatcher) {
try {
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}: ${cmdErr.message}`,
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;
}
}
}
@@ -8110,14 +8175,78 @@ const App = () => {
}
}
const blob = new Blob([fileBuf], {
type: 'audio/' + fmt
type: 'audio/wav'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `export_${Date.now()}.${fmt}`;
a.click();
URL.revokeObjectURL(url);
const localUrl = URL.createObjectURL(blob);
const targetFilename = `export_${Date.now()}.${fmt}`;
const triggerDownload = (downloadUrl, finalFilename) => {
if (window.DAWCommandDispatcher?.isExecutingAI) {
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,
@@ -10972,7 +11101,13 @@ const App = () => {
}, /*#__PURE__*/React.createElement("i", {
"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'}`
})), 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,
mode: authMode,
forceMandatory: isMandatoryLogin,
Binary file not shown.