Improve benchmark results sorting and cancel flow

This commit is contained in:
Stu Leak 2025-12-20 12:05:19 -05:00
parent 73e527048a
commit 5b76da0fdf
4 changed files with 234 additions and 47 deletions

55
DONE.md
View File

@ -2,9 +2,59 @@
This file tracks completed features, fixes, and milestones.
## Version 0.1.0-dev19 (2025-12-18) - Convert Module Cleanup & UX Polish
## Version 0.1.0-dev19 (2025-12-18 to 2025-12-20) - Convert Module Cleanup & UX Polish
### Features
### Features (2025-12-20 Session)
- ✅ **History Sidebar - In Progress Tab**
- Added "In Progress" tab to history sidebar
- Shows running and pending jobs without opening queue
- Animated striped progress bars per module color
- Real-time progress updates (0-100%)
- No delete button on active jobs (only completed/failed)
- Dynamic status text ("Running..." or "Pending")
- ✅ **Benchmark System Overhaul**
- **Hardware Detection Module** (`internal/sysinfo/sysinfo.go`)
- Cross-platform CPU detection (model, cores, clock speed)
- GPU detection with driver version (NVIDIA via nvidia-smi)
- RAM detection with human-readable formatting
- Linux, Windows, macOS support
- **Hardware Info Display**
- Shown immediately in benchmark progress view (before tests run)
- Displayed in benchmark results view
- Saved with each benchmark run for history
- **Settings Persistence**
- Hardware acceleration settings saved with benchmarks
- Settings persist between sessions via config file
- GPU automatically detected and used
- **UI Polish**
- "Run Benchmark" button highlighted (HighImportance) on first run
- Returns to normal styling after initial benchmark
- Guides new users to run initial benchmark
- ✅ **Bitrate Preset Simplification**
- Reduced from 13 confusing options to 6 clear presets
- Removed resolution references (no more "1440p" confusion)
- Codec-agnostic (presets don't change selected codec)
- Quality-based naming: Low/Medium/Good/High/Very High Quality
- Focused on common use cases (1.5-8 Mbps range)
- Presets only set bitrate and switch to CBR mode
- User codec choice (H.264, VP9, AV1, etc.) preserved
- ✅ **Quality Preset Codec Compatibility**
- "Lossless" quality option only available for H.265 and AV1
- Dynamic quality dropdown based on selected codec
- Automatic fallback to "Near-Lossless" when switching to non-lossless codec
- Lossless + Target Size bitrate mode now supported for H.265/AV1
- Prevents invalid codec/quality combinations
- ✅ **App Icon Improvements**
- Regenerated VT_Icon.ico with transparent background
- Updated LoadAppIcon() to search PNG first (better Linux support)
- Searches both current directory and executable directory
- Added debug logging for icon loading troubleshooting
### Features (2025-12-18 Session)
- ✅ **History Sidebar Enhancements**
- Delete button ("×") on each history entry
- Remove individual entries from history
@ -715,6 +765,7 @@ This file tracks completed features, fixes, and milestones.
### Recent Fixes
- ✅ Fixed aspect ratio default from 16:9 to Source (dev7)
- ✅ Ranked benchmark results by score and added cancel confirmation
- ✅ Stabilized video seeking and embedded rendering
- ✅ Improved player window positioning
- ✅ Fixed clear video functionality

29
TODO.md
View File

@ -2,10 +2,10 @@
This file tracks upcoming features, improvements, and known issues.
## Current Focus: dev19 - Convert Module Cleanup & Polish
## Current Focus: dev20+ - Feature Development
### In Progress
- [ ] **AI Frame Interpolation Support**
- [ ] **AI Frame Interpolation Support** (Deferred to dev20+)
- RIFE (Real-Time Intermediate Flow Estimation) - https://github.com/hzwer/ECCV2022-RIFE
- FILM (Frame Interpolation for Large Motion) - https://github.com/google-research/frame-interpolation
- DAIN (Depth-Aware Video Frame Interpolation) - https://github.com/baowenbo/DAIN
@ -14,11 +14,34 @@ This file tracks upcoming features, improvements, and known issues.
- Model download/management system
- UI controls for model selection
- [ ] **Color Space Preservation**
- [ ] **Color Space Preservation** (Deferred to dev20+)
- Fix color space preservation in upscale module
- Ensure all conversions preserve color metadata (color_space, color_primaries, color_trc, color_range)
- Test with HDR content
### Completed in dev19 (2025-12-20)
- [x] **History Sidebar - In Progress Tab** ✅ COMPLETED
- Shows running/pending jobs without opening full queue
- Animated progress bars per module color
- Real-time progress updates
- [x] **Benchmark System Overhaul** ✅ COMPLETED
- Hardware detection module (CPU, GPU, RAM, drivers)
- Hardware info displayed in progress and results views
- Settings persistence across sessions
- First-run button highlighting
- Results ranked by score with cancel confirmation
- [x] **Bitrate Preset Simplification** ✅ COMPLETED
- Codec-agnostic quality-based presets
- Removed confusing resolution references
- 6 clear presets: Manual, Low, Medium, Good, High, Very High
- [x] **Quality Preset Codec Compatibility** ✅ COMPLETED
- Lossless option only for H.265/AV1
- Dynamic dropdown based on codec
- Lossless + Target Size mode support
## Priority Features for dev20+
### Quality & Polish Improvements

View File

@ -3,20 +3,24 @@ package ui
import (
"fmt"
"image/color"
"sort"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/benchmark"
"git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo"
)
// BuildBenchmarkProgressView creates the benchmark progress UI
func BuildBenchmarkProgressView(
hwInfo sysinfo.HardwareInfo,
onCancel func(),
titleColor, bgColor, textColor color.Color,
) *BenchmarkProgressView {
view := &BenchmarkProgressView{
hwInfo: hwInfo,
titleColor: titleColor,
bgColor: bgColor,
textColor: textColor,
@ -28,6 +32,7 @@ func BuildBenchmarkProgressView(
// BenchmarkProgressView shows real-time benchmark progress
type BenchmarkProgressView struct {
hwInfo sysinfo.HardwareInfo
titleColor color.Color
bgColor color.Color
textColor color.Color
@ -57,6 +62,37 @@ func (v *BenchmarkProgressView) build() {
container.NewCenter(title),
)
// Hardware info section
hwInfoTitle := widget.NewLabel("System Hardware")
hwInfoTitle.TextStyle = fyne.TextStyle{Bold: true}
hwInfoTitle.Alignment = fyne.TextAlignCenter
cpuLabel := widget.NewLabel(fmt.Sprintf("CPU: %s (%d cores @ %s)", v.hwInfo.CPU, v.hwInfo.CPUCores, v.hwInfo.CPUMHz))
cpuLabel.Wrapping = fyne.TextWrapWord
gpuLabel := widget.NewLabel(fmt.Sprintf("GPU: %s", v.hwInfo.GPU))
gpuLabel.Wrapping = fyne.TextWrapWord
ramLabel := widget.NewLabel(fmt.Sprintf("RAM: %s", v.hwInfo.RAM))
driverLabel := widget.NewLabel(fmt.Sprintf("Driver: %s", v.hwInfo.GPUDriver))
driverLabel.Wrapping = fyne.TextWrapWord
hwCard := canvas.NewRectangle(color.RGBA{R: 34, G: 38, B: 48, A: 255})
hwCard.CornerRadius = 8
hwContent := container.NewVBox(
hwInfoTitle,
cpuLabel,
gpuLabel,
ramLabel,
driverLabel,
)
hwInfoSection := container.NewPadded(
container.NewMax(hwCard, hwContent),
)
// Status section
v.statusLabel = widget.NewLabel("Initializing benchmark...")
v.statusLabel.TextStyle = fyne.TextStyle{Bold: true}
@ -96,6 +132,8 @@ func (v *BenchmarkProgressView) build() {
header,
nil, nil, nil,
container.NewVBox(
hwInfoSection,
widget.NewSeparator(),
statusSection,
widget.NewSeparator(),
resultsSection,
@ -188,6 +226,7 @@ func (v *BenchmarkProgressView) SetComplete() {
func BuildBenchmarkResultsView(
results []benchmark.Result,
recommendation benchmark.Result,
hwInfo sysinfo.HardwareInfo,
onApply func(),
onClose func(),
titleColor, bgColor, textColor color.Color,
@ -207,6 +246,37 @@ func BuildBenchmarkResultsView(
container.NewCenter(title),
)
// Hardware info section
hwInfoTitle := widget.NewLabel("System Hardware")
hwInfoTitle.TextStyle = fyne.TextStyle{Bold: true}
hwInfoTitle.Alignment = fyne.TextAlignCenter
cpuLabel := widget.NewLabel(fmt.Sprintf("CPU: %s (%d cores @ %s)", hwInfo.CPU, hwInfo.CPUCores, hwInfo.CPUMHz))
cpuLabel.Wrapping = fyne.TextWrapWord
gpuLabel := widget.NewLabel(fmt.Sprintf("GPU: %s", hwInfo.GPU))
gpuLabel.Wrapping = fyne.TextWrapWord
ramLabel := widget.NewLabel(fmt.Sprintf("RAM: %s", hwInfo.RAM))
driverLabel := widget.NewLabel(fmt.Sprintf("Driver: %s", hwInfo.GPUDriver))
driverLabel.Wrapping = fyne.TextWrapWord
hwCard := canvas.NewRectangle(color.RGBA{R: 34, G: 38, B: 48, A: 255})
hwCard.CornerRadius = 8
hwContent := container.NewVBox(
hwInfoTitle,
cpuLabel,
gpuLabel,
ramLabel,
driverLabel,
)
hwInfoSection := container.NewPadded(
container.NewMax(hwCard, hwContent),
)
// Recommendation section
if recommendation.Encoder != "" {
recTitle := widget.NewLabel("RECOMMENDED ENCODER")
@ -243,12 +313,19 @@ func BuildBenchmarkResultsView(
topResultsTitle.TextStyle = fyne.TextStyle{Bold: true}
topResultsTitle.Alignment = fyne.TextAlignCenter
var resultItems []fyne.CanvasObject
for i, result := range results {
if result.Error != "" {
continue
var filtered []benchmark.Result
for _, result := range results {
if result.Error == "" {
filtered = append(filtered, result)
}
}
sort.Slice(filtered, func(i, j int) bool {
return filtered[i].Score > filtered[j].Score
})
var resultItems []fyne.CanvasObject
for i, result := range filtered {
rankLabel := widget.NewLabel(fmt.Sprintf("#%d", i+1))
rankLabel.TextStyle = fyne.TextStyle{Bold: true}
@ -290,6 +367,8 @@ func BuildBenchmarkResultsView(
header,
nil, nil, nil,
container.NewVBox(
hwInfoSection,
widget.NewSeparator(),
recommendationSection,
widget.NewSeparator(),
resultsSection,

110
main.go
View File

@ -44,6 +44,7 @@ import (
"git.leaktechnologies.dev/stu/VideoTools/internal/modules"
"git.leaktechnologies.dev/stu/VideoTools/internal/player"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
"git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo"
"git.leaktechnologies.dev/stu/VideoTools/internal/thumbnail"
"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
@ -81,16 +82,17 @@ var (
nvencRuntimeOK bool
modulesList = []Module{
{"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet
{"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue
{"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
{"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red
{"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal
{"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet
{"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue
{"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
{"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red
{"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal
}
// Platform-specific configuration
@ -557,12 +559,13 @@ func savePersistedConvertConfig(cfg convertConfig) error {
// benchmarkRun represents a single benchmark test run
type benchmarkRun struct {
Timestamp time.Time `json:"timestamp"`
Results []benchmark.Result `json:"results"`
RecommendedEncoder string `json:"recommended_encoder"`
RecommendedPreset string `json:"recommended_preset"`
RecommendedHWAccel string `json:"recommended_hwaccel"`
RecommendedFPS float64 `json:"recommended_fps"`
Timestamp time.Time `json:"timestamp"`
Results []benchmark.Result `json:"results"`
RecommendedEncoder string `json:"recommended_encoder"`
RecommendedPreset string `json:"recommended_preset"`
RecommendedHWAccel string `json:"recommended_hwaccel"`
RecommendedFPS float64 `json:"recommended_fps"`
HardwareInfo sysinfo.HardwareInfo `json:"hardware_info"`
}
// benchmarkConfig holds benchmark history
@ -1355,7 +1358,7 @@ func (s *appState) showMainMenu() {
Label: m.Label,
Color: m.Color,
Category: m.Category,
Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge" || m.ID == "thumb" || m.ID == "player" || m.ID == "filters" || m.ID == "upscale", // Enabled modules
Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge" || m.ID == "thumb" || m.ID == "player" || m.ID == "filters" || m.ID == "upscale", // Enabled modules (subtitles placeholder stays disabled)
})
}
@ -1408,11 +1411,17 @@ func (s *appState) showMainMenu() {
)
}
// Check if benchmark has been run
hasBenchmark := false
if cfg, err := loadBenchmarkConfig(); err == nil && len(cfg.History) > 0 {
hasBenchmark = true
}
menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, nil, s.showBenchmark, s.showBenchmarkHistory, func() {
// Toggle sidebar
s.sidebarVisible = !s.sidebarVisible
s.showMainMenu()
}, s.sidebarVisible, sidebar, titleColor, queueColor, textColor, queueCompleted, queueTotal)
}, s.sidebarVisible, sidebar, titleColor, queueColor, textColor, queueCompleted, queueTotal, hasBenchmark)
// Update stats bar
s.updateStatsBar()
@ -1733,17 +1742,35 @@ func (s *appState) showBenchmark() {
s.stopPlayer()
s.active = "benchmark"
// Detect hardware info upfront
hwInfo := sysinfo.Detect()
logging.Debug(logging.CatSystem, "detected hardware for benchmark: %s", hwInfo.Summary())
// Create benchmark suite
tmpDir := filepath.Join(os.TempDir(), "videotools-benchmark")
_ = os.MkdirAll(tmpDir, 0o755)
suite := benchmark.NewSuite(platformConfig.FFmpegPath, tmpDir)
// Build progress view
benchComplete := atomic.Bool{}
ctx, cancel := context.WithCancel(context.Background())
// Build progress view with hardware info
view := ui.BuildBenchmarkProgressView(
hwInfo,
func() {
// Cancel benchmark
s.showMainMenu()
if benchComplete.Load() {
s.showMainMenu()
return
}
dialog.ShowConfirm("Cancel Benchmark?", "The benchmark is still running. Cancel it now?", func(ok bool) {
if !ok {
return
}
cancel()
s.showMainMenu()
}, s.window)
},
utils.MustHex("#4CE870"),
utils.MustHex("#1E1E1E"),
@ -1754,12 +1781,13 @@ func (s *appState) showBenchmark() {
// Run benchmark in background
go func() {
ctx := context.Background()
// Generate test video
view.UpdateProgress(0, 100, "Generating test video", "")
testPath, err := suite.GenerateTestVideo(ctx, 30)
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
logging.Debug(logging.CatSystem, "failed to generate test video: %v", err)
dialog.ShowError(fmt.Errorf("failed to generate test video: %w", err), s.window)
s.showMainMenu()
@ -1780,6 +1808,9 @@ func (s *appState) showBenchmark() {
// Run benchmark suite
err = suite.RunFullSuite(ctx, availableEncoders)
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
logging.Debug(logging.CatSystem, "benchmark failed: %v", err)
dialog.ShowError(fmt.Errorf("benchmark failed: %w", err), s.window)
s.showMainMenu()
@ -1793,6 +1824,7 @@ func (s *appState) showBenchmark() {
// Mark complete
view.SetComplete()
benchComplete.Store(true)
// Get recommendation
encoder, preset, rec := suite.GetRecommendation()
@ -1807,10 +1839,13 @@ func (s *appState) showBenchmark() {
// Show results dialog with option to apply
go func() {
// Detect hardware info for display
hwInfo := sysinfo.Detect()
allResults := suite.Results // Show all results, not just top 10
resultsView := ui.BuildBenchmarkResultsView(
allResults,
rec,
hwInfo,
func() {
// Apply recommended settings
s.applyBenchmarkRecommendation(encoder, preset)
@ -1876,6 +1911,10 @@ func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset
hwAccel = "none"
}
// Detect hardware info
hwInfo := sysinfo.Detect()
logging.Debug(logging.CatSystem, "detected hardware: %s", hwInfo.Summary())
// Load existing config
cfg, err := loadBenchmarkConfig()
if err != nil {
@ -1891,6 +1930,7 @@ func (s *appState) saveBenchmarkRun(results []benchmark.Result, encoder, preset
RecommendedPreset: preset,
RecommendedHWAccel: hwAccel,
RecommendedFPS: fps,
HardwareInfo: hwInfo,
}
// Add to history (keep last 10 runs)
@ -1991,6 +2031,7 @@ func (s *appState) showBenchmarkHistory() {
resultsView := ui.BuildBenchmarkResultsView(
run.Results,
rec,
run.HardwareInfo,
func() {
// Apply this recommendation
s.applyBenchmarkRecommendation(run.RecommendedEncoder, run.RecommendedPreset)
@ -5786,9 +5827,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
"Target Size (Calculate from file size)",
}
bitrateModeMap := map[string]string{
"CRF (Constant Rate Factor)": "CRF",
"CBR (Constant Bitrate)": "CBR",
"VBR (Variable Bitrate)": "VBR",
"CRF (Constant Rate Factor)": "CRF",
"CBR (Constant Bitrate)": "CBR",
"VBR (Variable Bitrate)": "VBR",
"Target Size (Calculate from file size)": "Target Size",
}
reverseMap := map[string]string{
@ -5857,18 +5898,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
presets := []bitratePreset{
{Label: "Manual", Bitrate: "", Codec: ""},
{Label: "AV1 1080p - 1200k (smallest)", Bitrate: "1200k", Codec: "AV1"},
{Label: "AV1 1080p - 1400k (sweet spot)", Bitrate: "1400k", Codec: "AV1"},
{Label: "AV1 1080p - 1800k (headroom)", Bitrate: "1800k", Codec: "AV1"},
{Label: "H.265 1080p - 2000k (balanced)", Bitrate: "2000k", Codec: "H.265"},
{Label: "H.265 1080p - 2400k (noisy sources)", Bitrate: "2400k", Codec: "H.265"},
{Label: "AV1 1440p - 2600k (balanced)", Bitrate: "2600k", Codec: "AV1"},
{Label: "H.265 1440p - 3200k (balanced)", Bitrate: "3200k", Codec: "H.265"},
{Label: "H.265 1440p - 4000k (noisy sources)", Bitrate: "4000k", Codec: "H.265"},
{Label: "AV1 4K - 5M (balanced)", Bitrate: "5000k", Codec: "AV1"},
{Label: "H.265 4K - 6M (balanced)", Bitrate: "6000k", Codec: "H.265"},
{Label: "AV1 4K - 7M (archive)", Bitrate: "7000k", Codec: "AV1"},
{Label: "H.265 4K - 9M (fast/Topaz)", Bitrate: "9000k", Codec: "H.265"},
{Label: "1.5 Mbps - Low Quality", Bitrate: "1500k", Codec: ""},
{Label: "2.5 Mbps - Medium Quality", Bitrate: "2500k", Codec: ""},
{Label: "4.0 Mbps - Good Quality", Bitrate: "4000k", Codec: ""},
{Label: "6.0 Mbps - High Quality", Bitrate: "6000k", Codec: ""},
{Label: "8.0 Mbps - Very High Quality", Bitrate: "8000k", Codec: ""},
}
bitratePresetLookup := make(map[string]bitratePreset)