From 7136cbe9049d601bf7a706d55517636718d5b9dd Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 22 Jul 2026 18:23:20 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20x=E1=BB=AD=20l=C3=BD=20ph=C3=A2n=20quy?= =?UTF-8?q?=E1=BB=81n=20t=E1=BA=A3i=20file=20t=E1=BB=AB=20l=E1=BB=87nh=20A?= =?UTF-8?q?I=20v=C3=A0=20h=E1=BB=97=20tr=E1=BB=A3=20n=C3=A9n=20MP3/OGG=20q?= =?UTF-8?q?ua=20API=20m=C3=A1y=20ch=E1=BB=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 187 +++++++++++++++++++++++---- app/static/js/app.precompiled.js | 215 +++++++++++++++++++++++++------ app/storage/sonicforge.db | Bin 45056 -> 45056 bytes 3 files changed, 334 insertions(+), 68 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index c582d62..0476150 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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, diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 33827ba..b9bb597 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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, diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 03dba3f13f8a1d59c49dba5287ac4fb8086e0eb3..ea6ca2e2c7223cf2ea0d8e240936f0a3f26659da 100644 GIT binary patch delta 227 zcmZp8z|`=7X@WH4oQX2djB_?72*@*XOfHmnaxgSCOf@&PG)XhHFf%tuGE6nLG`2`G zPBStxwoEZMNKLggGe}D|GqOxJOiM{JG)zlMPBAtxurNxsFfccHO9Gq=tES&M7_TB80_m%GxN-QeL&(BkEPAo_*Dpv5x&o7wVRbe+-p;~tG zlZtu`OG85gLkn{y9i_a)+*BniCFjzTcxODC9#y`YTv^S}#kAM))(p+Flf1rco>ZM@ F0sto4OpO2l delta 277 zcmZp8z|`=7X@WH4l!-FVj8iry2*@*XPA-&par1nj6lRXk?h2m^OKOjoIYNYJM)}M8{h*G(Nh`n!0&Xb)E?Tx-MM?