fix: audio clip items lost after reload — missing serverFileId on clip/track

- set serverFileId on clip + track when upload succeeds
- serialize/deserialize server_file_id in source_data
- guard clip.buffer null in WaveformLane canvas draw
This commit is contained in:
2026-07-30 20:54:23 +07:00
parent 9c487614f1
commit 4005088e89
3 changed files with 22 additions and 9 deletions
+13 -6
View File
@@ -1376,6 +1376,7 @@ const WaveformLane = ({
if (clips.length > 0) { if (clips.length > 0) {
clips.forEach(clip => { clips.forEach(clip => {
if (!clip.buffer) return;
const numChannels = clip.buffer.numberOfChannels || 1; const numChannels = clip.buffer.numberOfChannels || 1;
const dataL = clip.buffer.getChannelData(0); const dataL = clip.buffer.getChannelData(0);
const dataR = numChannels >= 2 ? clip.buffer.getChannelData(1) : dataL; const dataR = numChannels >= 2 ? clip.buffer.getChannelData(1) : dataL;
@@ -7548,6 +7549,7 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
if (trackType === "AUDIO" && t.clips) { if (trackType === "AUDIO" && t.clips) {
t.clips.forEach(c => { t.clips.forEach(c => {
const durationSec = c.buffer ? c.buffer.duration : 4.0; const durationSec = c.buffer ? c.buffer.duration : 4.0;
const clipFileId = c.serverFileId || t.serverFileId;
items.push({ items.push({
id: c.id, id: c.id,
name: c.name || "Audio Clip", name: c.name || "Audio Clip",
@@ -7556,10 +7558,11 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
duration_bars: (durationSec / (c.speed || 1.0)) / secondsPerBar, duration_bars: (durationSec / (c.speed || 1.0)) / secondsPerBar,
clip_start_offset_bars: 0.0, clip_start_offset_bars: 0.0,
source_data: { source_data: {
audio_file_url: t.serverFileId ? `/static/audio/uploads/${t.serverFileId}` : "", audio_file_url: clipFileId ? `/static/audio/uploads/${clipFileId}` : "",
sample_rate: c.buffer ? c.buffer.sampleRate : 44100, sample_rate: c.buffer ? c.buffer.sampleRate : 44100,
channels: c.buffer ? c.buffer.numberOfChannels : 2, channels: c.buffer ? c.buffer.numberOfChannels : 2,
gain: 1.0 gain: 1.0,
server_file_id: clipFileId
} }
}); });
}); });
@@ -7631,11 +7634,13 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
(t.items || []).forEach(item => { (t.items || []).forEach(item => {
if (item.type === "AUDIO_ITEM") { if (item.type === "AUDIO_ITEM") {
const src = item.source_data || {}; const src = item.source_data || {};
const clipFileId = src.server_file_id || null;
clips.push({ clips.push({
id: item.id, id: item.id,
name: item.name, name: item.name,
startTime: item.start_bar * secondsPerBar, startTime: item.start_bar * secondsPerBar,
speed: 1.0 speed: 1.0,
serverFileId: clipFileId
}); });
} else if (item.type === "MIDI_ITEM") { } else if (item.type === "MIDI_ITEM") {
const src = item.source_data || {}; const src = item.source_data || {};
@@ -7683,7 +7688,7 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
startTime: t.start_time || 0, startTime: t.start_time || 0,
height: t.height || 140, height: t.height || 140,
markers: t.markers || [], markers: t.markers || [],
serverFileId: t.server_file_id || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.audio_file_url?.split('/').pop()) || null, serverFileId: t.server_file_id || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.server_file_id) || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.audio_file_url?.split('/').pop()) || null,
channelInfo: t.channel_info || null, channelInfo: t.channel_info || null,
isArmed: t.is_armed || false, isArmed: t.is_armed || false,
monitoringEnabled: t.monitoring_enabled !== false, monitoringEnabled: t.monitoring_enabled !== false,
@@ -15775,13 +15780,15 @@ const App = () => {
let count = 0; let count = 0;
for (const file of files) { for (const file of files) {
try { try {
uploadToServer(file, track.id); const uploadResult = await uploadToServer(file, track.id);
const fileId = uploadResult?.file_id || null;
const { audioBuffer: decodedBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(file); const { audioBuffer: decodedBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(file);
const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`; const clipId = `clip_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
const newClip = { id: clipId, buffer: decodedBuffer, startTime: offset, name: file.name, speed: 1.0 }; const newClip = { id: clipId, buffer: decodedBuffer, startTime: offset, name: file.name, speed: 1.0, serverFileId: fileId };
createClipWithUndo(selectedTrackId, newClip); createClipWithUndo(selectedTrackId, newClip);
updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? { updateActiveTracks(prev => prev.map(t => t.id === selectedTrackId ? {
...t, ...t,
serverFileId: t.serverFileId || fileId,
clips: [...(t.clips || []), newClip] clips: [...(t.clips || []), newClip]
} : t)); } : t));
offset += decodedBuffer.duration; offset += decodedBuffer.duration;
File diff suppressed because one or more lines are too long
+6
View File
@@ -849,6 +849,12 @@
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx` - **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
- **Ghi chú/Test (nếu có):** `npm run build` — build passes. - **Ghi chú/Test (nếu có):** `npm run build` — build passes.
### [2026-07-30 20:52] Task: Fix audio clip items lost after reload
- **Tóm tắt thay đổi:** 3 root causes: (1) `serverFileId` không được lưu vào clip/track khi upload audio — `insertSoundClipAtCursor` gọi `uploadToServer` nhưng chỉ lưu vào global `serverFileIdMap`, không set `t.serverFileId`; (2) Serialize dùng `t.serverFileId` (null) → `audio_file_url` rỗng → Deserialize không lấy được fileId → `loadAudioBuffersForTracks` không load được buffer; (3) WaveformLane canvas `clip.buffer.numberOfChannels` crash khi buffer null → clip items không vẽ được. Fix: set `serverFileId` trên clip + track khi upload; serialize/deserialize `server_file_id` trong `source_data`; guard `if (!clip.buffer) return` trong WaveformLane.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`
- **Ghi chú/Test (nếu có):** `npm run build` pass. Chèn audio clip → lưu project → reload → clip hiển thị + waveform load lại.
---
### [2026-07-30 20:46] Task: Auto-save 5min + Backup 30min + Backup management UI ### [2026-07-30 20:46] Task: Auto-save 5min + Backup 30min + Backup management UI
- **Tóm tắt thay đổi:** (1) DB: thêm bảng `project_backups` + migration cột `is_backup`/`original_id`; (2) Backend: `POST/POST /cloud/{id}/backup`, `GET /cloud/{id}/backups`, `DELETE /cloud/backups/{id}`, `POST /cloud/backups/cleanup` — tự động giới hạn retention (mặc định 10, cấu hình 5-20); (3) Frontend API: 4 methods backup; (4) App: `setInterval` 5 phút auto-save + 30 phút backup; (5) ProfileModal: nút Backup (hiện số lượng) mở rộng danh sách, xóa từng bản, thanh cấu hình retention slider 5-20, nút dọn dẹp. - **Tóm tắt thay đổi:** (1) DB: thêm bảng `project_backups` + migration cột `is_backup`/`original_id`; (2) Backend: `POST/POST /cloud/{id}/backup`, `GET /cloud/{id}/backups`, `DELETE /cloud/backups/{id}`, `POST /cloud/backups/cleanup` — tự động giới hạn retention (mặc định 10, cấu hình 5-20); (3) Frontend API: 4 methods backup; (4) App: `setInterval` 5 phút auto-save + 30 phút backup; (5) ProfileModal: nút Backup (hiện số lượng) mở rộng danh sách, xóa từng bản, thanh cấu hình retention slider 5-20, nút dọn dẹp.
- **Các file ảnh hưởng:** `app/models/user.py`, `app/api/v1/projects.py`, `app/static/js/services/api.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js` - **Các file ảnh hưởng:** `app/models/user.py`, `app/api/v1/projects.py`, `app/static/js/services/api.js`, `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`