diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f7223c9..56ed524 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ type WsMsg = | { type: 'segments'; segments: { start: number; end: number }[]; duration: number } | { type: 'progress'; percent: number } | { type: 'done'; message: string } + | { type: 'timeline'; segments: { start: number; end: number }[]; duration: number } | { type: 'error'; message: string } type Phase = 'idle' | 'uploading' | 'analyzing' | 'ready' | 'exporting' | 'done' @@ -66,6 +67,13 @@ export default function App() { return { ...prev, progress: msg.percent } case 'done': return { ...prev, phase: 'done', outputFile: msg.message, progress: 100 } + case 'timeline': + return { + ...prev, + phase: 'ready', + duration: msg.duration, + segments: msg.segments.map((s) => ({ ...s, kept: true })), + } case 'error': return { ...prev, phase: 'ready', error: msg.message } default: @@ -86,7 +94,7 @@ export default function App() { minSilence: number, padding: number, ) => { - setState((prev) => ({ ...prev, phase: 'analyzing', segments: [], error: null })) + setState((prev) => ({ ...prev, phase: 'analyzing', segments: [], outputFile: null, error: null })) try { const res = await fetch('/analyze', { method: 'POST', @@ -136,7 +144,7 @@ export default function App() { const res = await fetch('/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename: state.filename, segments: kept }), + body: JSON.stringify({ filename: state.filename, segments: kept, duration: state.duration }), }) if (!res.ok) throw new Error(await res.text()) } catch (e) { @@ -254,7 +262,7 @@ export default function App() { )} - {state.phase === 'done' && state.outputFile && ( + {state.outputFile && (

Export complete!

diff --git a/handlers/export.go b/handlers/export.go index ff51a00..c9c8dad 100644 --- a/handlers/export.go +++ b/handlers/export.go @@ -14,6 +14,13 @@ import ( type exportRequest struct { Filename string `json:"filename"` Segments []transcode.Segment `json:"segments"` + Duration float64 `json:"duration"` +} + +type updateTimeLine struct { + Type string `json:"type"` + Segments []transcode.Segment `json:"segments"` + Duration float64 `json:"duration"` } func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc { @@ -28,6 +35,7 @@ func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc { http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest) return } + if len(req.Segments) == 0 { http.Error(w, "no segments", http.StatusBadRequest) return @@ -50,15 +58,30 @@ func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc { return } - msg, _ := json.Marshal(map[string]string{ + doneMsg, _ := json.Marshal(map[string]string{ "type": "done", "message": outputKey, }) - hub.Broadcast(msg) + hub.Broadcast(doneMsg) + + // Build normalized segments for the exported video: timestamps start at 0 + // with no gaps, so the timeline reflects the exported file's layout. + var normalized []transcode.Segment + cursor := 0.0 + for _, seg := range req.Segments { + dur := seg.End - seg.Start + normalized = append(normalized, transcode.Segment{Start: cursor, End: cursor + dur}) + cursor += dur + } + timelineMsg, _ := json.Marshal(updateTimeLine{ + Type: "timeline", + Segments: normalized, + Duration: cursor, + }) + hub.Broadcast(timelineMsg) }() w.WriteHeader(http.StatusAccepted) - log.Println("Export handler works") } } diff --git a/handlers/zoom.go b/handlers/zoom.go index 3e5b686..102184b 100644 --- a/handlers/zoom.go +++ b/handlers/zoom.go @@ -27,8 +27,7 @@ func ZoomHandler(hub *ws.Hub) http.HandlerFunc { return } - h := transcode.HandlerZoom{Center: req.Center} - result := h.TimelineZoom(req.Zoom, req.Duration) + result := transcode.TimelineZoom(req.Center, req.Zoom, req.Duration) data, _ := json.Marshal(result) hub.Broadcast(data) diff --git a/transcode/timeline.go b/transcode/timeline.go index 7a7f51d..ef613d7 100644 --- a/transcode/timeline.go +++ b/transcode/timeline.go @@ -1,10 +1,5 @@ package transcode -// HandlerZoom holds the current scroll center for timeline zoom calculations. -type HandlerZoom struct { - Center float64 // center of the visible window in seconds -} - // ZoomResult is the result of a TimelineZoom calculation. type ZoomResult struct { Type string `json:"type"` @@ -14,13 +9,13 @@ type ZoomResult struct { } // TimelineZoom computes the visible time window for the given zoomPercentage -// (1–100, where 100 = full duration visible) centered on h.Center. -func (h *HandlerZoom) TimelineZoom(zoomPercentage, duration float64) *ZoomResult { +// (1–100, where 100 = full duration visible) centered on zoomCenter. +func TimelineZoom(zoomCenter, zoomPercentage, duration float64) *ZoomResult { visibleDuration := duration * (zoomPercentage / 100) half := visibleDuration / 2 - viewStart := h.Center - half - viewEnd := h.Center + half + viewStart := zoomCenter - half + viewEnd := zoomCenter + half // Clamp to [0, duration], shifting the window rather than just truncating // so the visible span stays the same size when near the edges.