Silent timeline removed when hitting export

This commit is contained in:
2026-02-27 00:23:51 -08:00
parent c82ac37358
commit 96e99d2029
4 changed files with 42 additions and 17 deletions

View File

@@ -10,6 +10,7 @@ type WsMsg =
| { type: 'segments'; segments: { start: number; end: number }[]; duration: number } | { type: 'segments'; segments: { start: number; end: number }[]; duration: number }
| { type: 'progress'; percent: number } | { type: 'progress'; percent: number }
| { type: 'done'; message: string } | { type: 'done'; message: string }
| { type: 'timeline'; segments: { start: number; end: number }[]; duration: number }
| { type: 'error'; message: string } | { type: 'error'; message: string }
type Phase = 'idle' | 'uploading' | 'analyzing' | 'ready' | 'exporting' | 'done' type Phase = 'idle' | 'uploading' | 'analyzing' | 'ready' | 'exporting' | 'done'
@@ -66,6 +67,13 @@ export default function App() {
return { ...prev, progress: msg.percent } return { ...prev, progress: msg.percent }
case 'done': case 'done':
return { ...prev, phase: 'done', outputFile: msg.message, progress: 100 } 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': case 'error':
return { ...prev, phase: 'ready', error: msg.message } return { ...prev, phase: 'ready', error: msg.message }
default: default:
@@ -86,7 +94,7 @@ export default function App() {
minSilence: number, minSilence: number,
padding: number, padding: number,
) => { ) => {
setState((prev) => ({ ...prev, phase: 'analyzing', segments: [], error: null })) setState((prev) => ({ ...prev, phase: 'analyzing', segments: [], outputFile: null, error: null }))
try { try {
const res = await fetch('/analyze', { const res = await fetch('/analyze', {
method: 'POST', method: 'POST',
@@ -136,7 +144,7 @@ export default function App() {
const res = await fetch('/export', { const res = await fetch('/export', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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()) if (!res.ok) throw new Error(await res.text())
} catch (e) { } catch (e) {
@@ -254,7 +262,7 @@ export default function App() {
</div> </div>
)} )}
{state.phase === 'done' && state.outputFile && ( {state.outputFile && (
<div className="done-panel"> <div className="done-panel">
<span className="done-check"></span> <span className="done-check"></span>
<p>Export complete!</p> <p>Export complete!</p>

View File

@@ -14,6 +14,13 @@ import (
type exportRequest struct { type exportRequest struct {
Filename string `json:"filename"` Filename string `json:"filename"`
Segments []transcode.Segment `json:"segments"` 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 { 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) http.Error(w, "bad JSON: "+err.Error(), http.StatusBadRequest)
return return
} }
if len(req.Segments) == 0 { if len(req.Segments) == 0 {
http.Error(w, "no segments", http.StatusBadRequest) http.Error(w, "no segments", http.StatusBadRequest)
return return
@@ -50,15 +58,30 @@ func ExportHandler(st *store.Store, hub *ws.Hub) http.HandlerFunc {
return return
} }
msg, _ := json.Marshal(map[string]string{ doneMsg, _ := json.Marshal(map[string]string{
"type": "done", "type": "done",
"message": outputKey, "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) w.WriteHeader(http.StatusAccepted)
log.Println("Export handler works") log.Println("Export handler works")
} }
} }

View File

@@ -27,8 +27,7 @@ func ZoomHandler(hub *ws.Hub) http.HandlerFunc {
return return
} }
h := transcode.HandlerZoom{Center: req.Center} result := transcode.TimelineZoom(req.Center, req.Zoom, req.Duration)
result := h.TimelineZoom(req.Zoom, req.Duration)
data, _ := json.Marshal(result) data, _ := json.Marshal(result)
hub.Broadcast(data) hub.Broadcast(data)

View File

@@ -1,10 +1,5 @@
package transcode 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. // ZoomResult is the result of a TimelineZoom calculation.
type ZoomResult struct { type ZoomResult struct {
Type string `json:"type"` Type string `json:"type"`
@@ -14,13 +9,13 @@ type ZoomResult struct {
} }
// TimelineZoom computes the visible time window for the given zoomPercentage // TimelineZoom computes the visible time window for the given zoomPercentage
// (1100, where 100 = full duration visible) centered on h.Center. // (1100, where 100 = full duration visible) centered on zoomCenter.
func (h *HandlerZoom) TimelineZoom(zoomPercentage, duration float64) *ZoomResult { func TimelineZoom(zoomCenter, zoomPercentage, duration float64) *ZoomResult {
visibleDuration := duration * (zoomPercentage / 100) visibleDuration := duration * (zoomPercentage / 100)
half := visibleDuration / 2 half := visibleDuration / 2
viewStart := h.Center - half viewStart := zoomCenter - half
viewEnd := h.Center + half viewEnd := zoomCenter + half
// Clamp to [0, duration], shifting the window rather than just truncating // Clamp to [0, duration], shifting the window rather than just truncating
// so the visible span stays the same size when near the edges. // so the visible span stays the same size when near the edges.