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 03dba3f..ea6ca2e 100644 Binary files a/app/storage/sonicforge.db and b/app/storage/sonicforge.db differ