Add chapter detection visualizer with thumbnails
Allows visual verification of detected scene changes before accepting. Features: - Extracts thumbnail at each detected chapter timestamp - Displays first 24 chapters in scrollable grid (4 columns) - Shows timestamp below each thumbnail (160x90px previews) - Accept/Reject buttons to confirm or discard detection - Progress indicator during thumbnail generation Implementation: - showChapterPreview() function creates preview dialog - extractChapterThumbnail() uses FFmpeg to extract frame - Scales to 320x180, saves as JPEG in temp dir - Thumbnails generated in background, dialog updated when ready Performance: - Limits to 24 chapters for UI responsiveness - Shows "(showing first 24)" if more detected - Temp files stored in videotools-chapter-thumbs/ User workflow: 1. Adjust sensitivity slider 2. Click "Detect Scenes" 3. Review thumbnails to verify detection quality 4. Accept to use chapters, or Reject to try different sensitivity
This commit is contained in:
parent
d781ce2d58
commit
0d1235d867
123
author_module.go
123
author_module.go
|
|
@ -414,10 +414,15 @@ func buildChaptersTab(state *appState) fyne.CanvasObject {
|
||||||
dialog.ShowInformation("Scene Detection", "No scene changes detected at the current sensitivity.", state.window)
|
dialog.ShowInformation("Scene Detection", "No scene changes detected at the current sensitivity.", state.window)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.authorChapters = chapters
|
// Show chapter preview dialog for visual verification
|
||||||
state.authorChapterSource = "scenes"
|
state.showChapterPreview(targetPath, chapters, func(accepted bool) {
|
||||||
state.updateAuthorSummary()
|
if accepted {
|
||||||
refreshChapters()
|
state.authorChapters = chapters
|
||||||
|
state.authorChapterSource = "scenes"
|
||||||
|
state.updateAuthorSummary()
|
||||||
|
refreshChapters()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
|
@ -2276,6 +2281,116 @@ func runCommand(name string, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *appState) showChapterPreview(videoPath string, chapters []authorChapter, callback func(bool)) {
|
||||||
|
dlg := dialog.NewCustom("Chapter Preview", "Close", container.NewVBox(
|
||||||
|
widget.NewLabel(fmt.Sprintf("Detected %d chapters - generating thumbnails...", len(chapters))),
|
||||||
|
widget.NewProgressBarInfinite(),
|
||||||
|
), s.window)
|
||||||
|
dlg.Resize(fyne.NewSize(800, 600))
|
||||||
|
dlg.Show()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Limit preview to first 24 chapters for performance
|
||||||
|
previewCount := len(chapters)
|
||||||
|
if previewCount > 24 {
|
||||||
|
previewCount = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnails := make([]fyne.CanvasObject, 0, previewCount)
|
||||||
|
for i := 0; i < previewCount; i++ {
|
||||||
|
ch := chapters[i]
|
||||||
|
thumbPath, err := extractChapterThumbnail(videoPath, ch.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "failed to extract thumbnail at %.2f: %v", ch.Timestamp, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
img := canvas.NewImageFromFile(thumbPath)
|
||||||
|
img.FillMode = canvas.ImageFillContain
|
||||||
|
img.SetMinSize(fyne.NewSize(160, 90))
|
||||||
|
|
||||||
|
timeLabel := widget.NewLabel(fmt.Sprintf("%.2fs", ch.Timestamp))
|
||||||
|
timeLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
|
thumbCard := container.NewVBox(
|
||||||
|
container.NewMax(img),
|
||||||
|
timeLabel,
|
||||||
|
)
|
||||||
|
thumbnails = append(thumbnails, thumbCard)
|
||||||
|
}
|
||||||
|
|
||||||
|
runOnUI(func() {
|
||||||
|
dlg.Hide()
|
||||||
|
|
||||||
|
if len(thumbnails) == 0 {
|
||||||
|
dialog.ShowError(fmt.Errorf("failed to generate chapter thumbnails"), s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
grid := container.NewGridWrap(fyne.NewSize(170, 120), thumbnails...)
|
||||||
|
scroll := container.NewVScroll(grid)
|
||||||
|
scroll.SetMinSize(fyne.NewSize(780, 500))
|
||||||
|
|
||||||
|
infoText := fmt.Sprintf("Found %d chapters", len(chapters))
|
||||||
|
if len(chapters) > previewCount {
|
||||||
|
infoText += fmt.Sprintf(" (showing first %d)", previewCount)
|
||||||
|
}
|
||||||
|
info := widget.NewLabel(infoText)
|
||||||
|
info.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
var previewDlg *dialog.CustomDialog
|
||||||
|
acceptBtn := widget.NewButton("Accept Chapters", func() {
|
||||||
|
previewDlg.Hide()
|
||||||
|
callback(true)
|
||||||
|
})
|
||||||
|
acceptBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
rejectBtn := widget.NewButton("Reject", func() {
|
||||||
|
previewDlg.Hide()
|
||||||
|
callback(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
content := container.NewBorder(
|
||||||
|
container.NewVBox(info, widget.NewSeparator()),
|
||||||
|
container.NewHBox(rejectBtn, acceptBtn),
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
scroll,
|
||||||
|
)
|
||||||
|
|
||||||
|
previewDlg = dialog.NewCustom("Chapter Preview", "Close", content, s.window)
|
||||||
|
previewDlg.Resize(fyne.NewSize(800, 600))
|
||||||
|
previewDlg.Show()
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractChapterThumbnail(videoPath string, timestamp float64) (string, error) {
|
||||||
|
tmpDir := filepath.Join(os.TempDir(), "videotools-chapter-thumbs")
|
||||||
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputPath := filepath.Join(tmpDir, fmt.Sprintf("thumb_%.2f.jpg", timestamp))
|
||||||
|
args := []string{
|
||||||
|
"-ss", fmt.Sprintf("%.2f", timestamp),
|
||||||
|
"-i", videoPath,
|
||||||
|
"-frames:v", "1",
|
||||||
|
"-q:v", "2",
|
||||||
|
"-vf", "scale=320:180",
|
||||||
|
"-y",
|
||||||
|
outputPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(platformConfig.FFmpegPath, args...)
|
||||||
|
utils.ApplyNoWindow(cmd)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
func runOnUI(fn func()) {
|
func runOnUI(fn func()) {
|
||||||
fn()
|
fn()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user