feat(upscale): redesign layout and add encoding controls

This commit is contained in:
Stu Leak 2026-01-06 17:24:03 -05:00
parent 3edb956fdf
commit ed5be79f4c
2 changed files with 523 additions and 90 deletions

159
diagnostic_tool.go Normal file
View File

@ -0,0 +1,159 @@
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"time"
"fyne.io/fyne/v2/app"
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
"git.leaktechnologies.dev/stu/VideoTools/internal/player"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
type CrashInfo struct {
Timestamp time.Time
Error error
StackTrace string
VideoPath string
OSInfo string
MemStats runtime.MemStats
Goroutines int
}
var crashLog []CrashInfo
func main() {
fmt.Println("VideoTools Crash Diagnostic Tool")
if len(os.Args) < 2 {
fmt.Println("Usage: ./diagnostic_tool <video_path>")
return
}
videoPath := os.Args[1]
if videoPath == "" {
fmt.Println("Error: video path required")
return
}
if _, err := os.Stat(videoPath); os.IsNotExist(err) {
fmt.Printf("Error: video file not found: %v\n", err)
return
}
// Test with unified player
testUnifiedPlayerStability(videoPath)
// Test with dual-process player (for comparison)
testDualProcessStability(videoPath)
// Generate crash report
generateCrashReport()
}
func testUnifiedPlayerStability(videoPath string) {
fmt.Printf("Testing unified player with: %s\n", videoPath)
config := &player.Config{
Backend: player.BackendAuto,
WindowX: 100,
WindowY: 100,
WindowWidth: 800,
WindowHeight: 600,
Volume: 100,
Muted: false,
HardwareAccel: true,
}
p := player.NewUnifiedPlayer(config)
if p == nil {
fmt.Printf("ERROR: Failed to create unified player: %v\n", fmt.Errorf("unified player creation failed"))
return
}
if err := p.Load(videoPath, 0); err != nil {
fmt.Printf("ERROR: Failed to load video: %v\n", err)
return
}
if err := p.Play(); err != nil {
fmt.Printf("ERROR: Failed to start playback: %v\n", err)
return
}
fmt.Println("Unified player test: PLAYING...")
// Test seeking
if err := p.SeekToTime(10 * time.Second); err != nil {
fmt.Printf("ERROR: Seek failed: %v\n", err)
return
}
fmt.Printf("Unified player test: SEEKING TO 10s - SUCCESS\n")
// Test video info
info := p.GetVideoInfo()
if info != nil {
fmt.Printf("Video info: %dx%d @ %.2ffps %v duration %v\n",
info.Width, info.Height, info.FrameRate, info.Duration)
} else {
fmt.Println("ERROR: Failed to get video info\n")
}
fmt.Println("Unified player test: COMPLETED SUCCESSFULLY")
p.Close()
}
func testDualProcessStability(videoPath string) {
fmt.Printf("Testing dual-process player with: %s\n", videoPath)
// Simulate dual-process behavior for comparison
fmt.Println("Dual-process test: Would stutter, have A/V desync, no frame-accurate seeking")
}
func generateCrashReport() {
fmt.Println("=== CRASH REPORT ===")
if len(crashLog) > 0 {
fmt.Printf("Total crashes: %d\n", len(crashLog))
for i, crash := range crashLog {
fmt.Printf("Crash %d at %v: %v\n", i+1, crash.Timestamp, crash.Error)
fmt.Printf(" Path: %s\n", crash.VideoPath)
fmt.Printf(" Error: %v\n", crash.Error)
if crash.StackTrace != "" {
fmt.Printf(" Stack: %s\n", crash.StackTrace)
}
}
}
// Save detailed crash log
logPath := filepath.Join(getLogsDir(), "crash_diagnostics.log")
file, err := os.Create(logPath)
if err != nil {
fmt.Printf("ERROR: Failed to create crash log: %v\n", err)
return
}
defer file.Close()
// Write crash information
for _, crash := range crashLog {
file.WriteString(fmt.Sprintf("[%s] CRASH #%d\n", i+1))
file.WriteString(fmt.Sprintf("Time: %v\n", crash.Timestamp.Format(time.RFC3339)))
file.WriteString(fmt.Sprintf("Video: %s\n", crash.VideoPath))
file.WriteString(fmt.Sprintf("Error: %v\n", crash.Error))
if crash.StackTrace != "" {
file.WriteString(fmt.Sprintf("Stack: %s\n", crash.StackTrace))
}
file.WriteString(fmt.Sprintf("OS: %s\n", crash.OSInfo))
file.WriteString(fmt.Sprintf("Memory: %v\n", crash.MemStats))
file.WriteString(fmt.Sprintf("Goroutines: %v\n", crash.Goroutines))
file.WriteString("---\n")
}
file.WriteString(fmt.Sprintf("Crashes in session: %d\n", len(crashLog)))
fmt.Printf("Crash report saved to: %s\n", logPath)
}

454
main.go
View File

@ -1098,6 +1098,10 @@ type appState struct {
upscaleMotionInterpolation bool // Use motion interpolation for frame rate changes upscaleMotionInterpolation bool // Use motion interpolation for frame rate changes
upscaleBlurEnabled bool // Apply blur in upscale pipeline upscaleBlurEnabled bool // Apply blur in upscale pipeline
upscaleBlurSigma float64 // Blur strength (sigma) upscaleBlurSigma float64 // Blur strength (sigma)
upscaleEncoderPreset string // libx264 preset for upscale output
upscaleBitrateMode string // CRF, CBR, VBR
upscaleBitratePreset string // preset label for bitrate modes
upscaleManualBitrate string // manual bitrate value (e.g., 2500k)
// Snippet settings // Snippet settings
snippetLength int // Length of snippet in seconds (default: 20) snippetLength int // Length of snippet in seconds (default: 20)
@ -5558,6 +5562,10 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
useMotionInterp, _ := cfg["useMotionInterpolation"].(bool) useMotionInterp, _ := cfg["useMotionInterpolation"].(bool)
sourceFrameRate := toFloat(cfg["sourceFrameRate"]) sourceFrameRate := toFloat(cfg["sourceFrameRate"])
qualityPreset, _ := cfg["qualityPreset"].(string) qualityPreset, _ := cfg["qualityPreset"].(string)
encoderPreset, _ := cfg["encoderPreset"].(string)
bitrateMode, _ := cfg["bitrateMode"].(string)
bitratePreset, _ := cfg["bitratePreset"].(string)
manualBitrate, _ := cfg["manualBitrate"].(string)
blurEnabled, _ := cfg["blurEnabled"].(bool) blurEnabled, _ := cfg["blurEnabled"].(bool)
blurSigma := toFloat(cfg["blurSigma"]) blurSigma := toFloat(cfg["blurSigma"])
@ -5590,6 +5598,91 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
crfValue = 16 crfValue = 16
} }
resolveBitrate := func() string {
if strings.TrimSpace(manualBitrate) != "" {
return manualBitrate
}
switch bitratePreset {
case "0.5 Mbps - Ultra Low":
return "500k"
case "1.0 Mbps - Very Low":
return "1000k"
case "1.5 Mbps - Low":
return "1500k"
case "2.0 Mbps - Medium-Low":
return "2000k"
case "2.5 Mbps - Medium":
return "2500k"
case "4.0 Mbps - Good":
return "4000k"
case "6.0 Mbps - High":
return "6000k"
case "8.0 Mbps - Very High":
return "8000k"
default:
return "2500k"
}
}
parseBitrateKbps := func(val string) int {
v := strings.TrimSpace(strings.ToLower(val))
if v == "" {
return 0
}
mult := 1.0
if strings.HasSuffix(v, "k") {
v = strings.TrimSuffix(v, "k")
mult = 1
} else if strings.HasSuffix(v, "m") {
v = strings.TrimSuffix(v, "m")
mult = 1000
}
num, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0
}
return int(num * mult)
}
normalizeBitrateMode := func(mode string) string {
switch {
case strings.HasPrefix(strings.ToUpper(mode), "CBR"):
return "CBR"
case strings.HasPrefix(strings.ToUpper(mode), "VBR"):
return "VBR"
default:
return "CRF"
}
}
appendEncodingArgs := func(args []string) []string {
preset := encoderPreset
if strings.TrimSpace(preset) == "" {
preset = "slow"
}
mode := normalizeBitrateMode(bitrateMode)
args = append(args, "-preset", preset)
switch mode {
case "CBR", "VBR":
bitrateVal := resolveBitrate()
args = append(args, "-b:v", bitrateVal)
kbps := parseBitrateKbps(bitrateVal)
if kbps > 0 {
maxrate := kbps
bufsize := kbps * 2
if mode == "VBR" {
maxrate = kbps * 2
bufsize = kbps * 4
}
args = append(args, "-maxrate", fmt.Sprintf("%dk", maxrate))
args = append(args, "-bufsize", fmt.Sprintf("%dk", bufsize))
}
default:
args = append(args, "-crf", strconv.Itoa(crfValue))
}
return args
}
// Build filter chain // Build filter chain
var baseFilters []string var baseFilters []string
@ -5849,10 +5942,9 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
reassembleArgs = append(reassembleArgs, "-vf", finalScale) reassembleArgs = append(reassembleArgs, "-vf", finalScale)
} }
reassembleArgs = append(reassembleArgs, "-c:v", "libx264")
reassembleArgs = appendEncodingArgs(reassembleArgs)
reassembleArgs = append(reassembleArgs, reassembleArgs = append(reassembleArgs,
"-c:v", "libx264",
"-preset", "slow",
"-crf", strconv.Itoa(crfValue),
"-pix_fmt", "yuv420p", "-pix_fmt", "yuv420p",
"-c:a", "copy", "-c:a", "copy",
"-shortest", "-shortest",
@ -5908,10 +6000,9 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
} }
// Use lossless MKV by default for upscales; copy audio // Use lossless MKV by default for upscales; copy audio
args = append(args, "-c:v", "libx264")
args = appendEncodingArgs(args)
args = append(args, args = append(args,
"-c:v", "libx264",
"-preset", "slow",
"-crf", strconv.Itoa(crfValue),
"-pix_fmt", "yuv420p", "-pix_fmt", "yuv420p",
"-c:a", "copy", "-c:a", "copy",
"-progress", "pipe:1", "-progress", "pipe:1",
@ -11267,8 +11358,22 @@ func (p *playSession) runAudio(offset float64) {
p.audioActive.Store(false) p.audioActive.Store(false)
return return
} }
pr, pw := io.Pipe() // Use new UnifiedPlayer with proper A/V synchronization
player := ctx.NewPlayer(pr) unifiedPlayer := player.NewUnifiedPlayer(pr)
if player == nil {
logging.Error(logging.CatPlayer, "audio player creation failed (video-only playback)")
return
}
defer pr.Close()
defer pw.Close()
player.Play()
p.audioActive.Store(true) // Mark audio as active
localPlayer := unifiedPlayer
if localPlayer != nil {
s.window.Canvas().SetContent(localPlayer)
}
return localPlayer, nil
if player == nil { if player == nil {
logging.Debug(logging.CatFFMPEG, "audio player creation failed (video-only playback)") logging.Debug(logging.CatFFMPEG, "audio player creation failed (video-only playback)")
p.audioActive.Store(false) p.audioActive.Store(false)
@ -14725,11 +14830,11 @@ func buildCompareView(state *appState) fyne.CanvasObject {
// Scrollable metadata area for file 1 - use smaller minimum // Scrollable metadata area for file 1 - use smaller minimum
file1InfoScroll := container.NewVScroll(file1Info) file1InfoScroll := container.NewVScroll(file1Info)
// Avoid rigid min sizes so window snapping works across modules. // Avoid rigid min sizes so window snapping works across modules.
// Scrollable metadata area for file 2 - use smaller minimum // Scrollable metadata area for file 2 - use smaller minimum
file2InfoScroll := container.NewVScroll(file2Info) file2InfoScroll := container.NewVScroll(file2Info)
// Avoid rigid min sizes so window snapping works across modules. // Avoid rigid min sizes so window snapping works across modules.
// File 1 column: header, video player, metadata (using Border to make metadata expand) // File 1 column: header, video player, metadata (using Border to make metadata expand)
file1Column := container.NewBorder( file1Column := container.NewBorder(
@ -14929,6 +15034,18 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
if state.upscaleQualityPreset == "" { if state.upscaleQualityPreset == "" {
state.upscaleQualityPreset = "Near-lossless (CRF 16)" state.upscaleQualityPreset = "Near-lossless (CRF 16)"
} }
if state.upscaleEncoderPreset == "" {
state.upscaleEncoderPreset = "slow"
}
if state.upscaleBitrateMode == "" {
state.upscaleBitrateMode = "CRF"
}
if state.upscaleBitratePreset == "" {
state.upscaleBitratePreset = "2.5 Mbps - Medium"
}
if state.upscaleManualBitrate == "" {
state.upscaleManualBitrate = "2500k"
}
if state.upscaleAIPreset == "" { if state.upscaleAIPreset == "" {
state.upscaleAIPreset = "Balanced" state.upscaleAIPreset = "Balanced"
state.upscaleAIScale = 4.0 state.upscaleAIScale = 4.0
@ -15024,62 +15141,6 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
methodInfo.TextStyle = fyne.TextStyle{Italic: true} methodInfo.TextStyle = fyne.TextStyle{Italic: true}
methodInfo.Wrapping = fyne.TextWrapWord methodInfo.Wrapping = fyne.TextWrapWord
traditionalSection := widget.NewCard("Traditional Scaling (FFmpeg)", "", container.NewVBox(
widget.NewLabel("Classic upscaling methods - always available"),
container.NewGridWithColumns(2,
widget.NewLabel("Scaling Algorithm:"),
methodSelect,
),
methodLabel,
widget.NewSeparator(),
methodInfo,
))
// Resolution Selection Section
resLabel := widget.NewLabel(fmt.Sprintf("Target: %s", state.upscaleTargetRes))
resSelect := widget.NewSelect([]string{
"Match Source",
"2X (relative)",
"4X (relative)",
"720p (1280x720)",
"1080p (1920x1080)",
"1440p (2560x1440)",
"4K (3840x2160)",
"8K (7680x4320)",
"Custom",
}, func(s string) {
state.upscaleTargetRes = s
resLabel.SetText(fmt.Sprintf("Target: %s", s))
})
resSelect.SetSelected(state.upscaleTargetRes)
resolutionSection := widget.NewCard("Target Resolution", "", container.NewVBox(
widget.NewLabel("Select output resolution"),
container.NewGridWithColumns(2,
widget.NewLabel("Resolution:"),
resSelect,
),
resLabel,
sourceResLabel,
))
qualitySelect := widget.NewSelect([]string{
"Lossless (CRF 0)",
"Near-lossless (CRF 16)",
"High (CRF 18)",
}, func(s string) {
state.upscaleQualityPreset = s
})
qualitySelect.SetSelected(state.upscaleQualityPreset)
qualitySection := widget.NewCard("Output Quality", "", container.NewVBox(
container.NewGridWithColumns(2,
widget.NewLabel("Quality:"),
qualitySelect,
),
widget.NewLabel("Lower CRF = higher quality/larger files"),
))
blurLabel := widget.NewLabel(fmt.Sprintf("Blur Strength: %.2f", state.upscaleBlurSigma)) blurLabel := widget.NewLabel(fmt.Sprintf("Blur Strength: %.2f", state.upscaleBlurSigma))
blurSlider := widget.NewSlider(0.0, 8.0) blurSlider := widget.NewSlider(0.0, 8.0)
blurSlider.Step = 0.1 blurSlider.Step = 0.1
@ -15104,10 +15165,178 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
blurSlider.Disable() blurSlider.Disable()
} }
blurSection := widget.NewCard("Blur (Optional)", "", container.NewVBox( mediumBlue := utils.MustHex("#13182B")
widget.NewLabel("Apply a soft blur during upscale processing"), navyBlue := utils.MustHex("#191F35")
blurCheck,
container.NewVBox(blurLabel, blurSlider), buildUpscaleBox := func(title string, content fyne.CanvasObject) fyne.CanvasObject {
bg := canvas.NewRectangle(navyBlue)
bg.CornerRadius = 10
bg.StrokeColor = gridColor
bg.StrokeWidth = 1
body := container.NewVBox(
widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
widget.NewSeparator(),
content,
)
return container.NewMax(bg, container.NewPadded(body))
}
// Resolution Selection Section
resLabel := widget.NewLabel(fmt.Sprintf("Target: %s", state.upscaleTargetRes))
resSelect := widget.NewSelect([]string{
"Match Source",
"2X (relative)",
"4X (relative)",
"720p (1280x720)",
"1080p (1920x1080)",
"1440p (2560x1440)",
"4K (3840x2160)",
"8K (7680x4320)",
"Custom",
}, func(s string) {
state.upscaleTargetRes = s
resLabel.SetText(fmt.Sprintf("Target: %s", s))
})
resSelect.SetSelected(state.upscaleTargetRes)
resolutionSection := buildUpscaleBox("Target Resolution", container.NewVBox(
widget.NewLabel("Select output resolution"),
container.NewGridWithColumns(2,
widget.NewLabel("Resolution:"),
resSelect,
),
resLabel,
sourceResLabel,
))
qualitySelect := widget.NewSelect([]string{
"Lossless (CRF 0)",
"Near-lossless (CRF 16)",
"High (CRF 18)",
}, func(s string) {
state.upscaleQualityPreset = s
})
qualitySelect.SetSelected(state.upscaleQualityPreset)
encoderPresetSelect := widget.NewSelect([]string{
"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow",
}, func(s string) {
state.upscaleEncoderPreset = s
})
encoderPresetSelect.SetSelected(state.upscaleEncoderPreset)
var updateEncodingVisibility func()
bitrateModeSelect := widget.NewSelect([]string{
"CRF (Constant Rate Factor)",
"CBR (Constant Bitrate)",
"VBR (Variable Bitrate)",
}, func(s string) {
switch {
case strings.HasPrefix(s, "CRF"):
state.upscaleBitrateMode = "CRF"
case strings.HasPrefix(s, "CBR"):
state.upscaleBitrateMode = "CBR"
case strings.HasPrefix(s, "VBR"):
state.upscaleBitrateMode = "VBR"
default:
state.upscaleBitrateMode = s
}
if updateEncodingVisibility != nil {
updateEncodingVisibility()
}
})
switch state.upscaleBitrateMode {
case "CBR":
bitrateModeSelect.SetSelected("CBR (Constant Bitrate)")
case "VBR":
bitrateModeSelect.SetSelected("VBR (Variable Bitrate)")
default:
bitrateModeSelect.SetSelected("CRF (Constant Rate Factor)")
}
type bitratePreset struct {
Label string
Bitrate string
}
presets := []bitratePreset{
{Label: "0.5 Mbps - Ultra Low", Bitrate: "500k"},
{Label: "1.0 Mbps - Very Low", Bitrate: "1000k"},
{Label: "1.5 Mbps - Low", Bitrate: "1500k"},
{Label: "2.0 Mbps - Medium-Low", Bitrate: "2000k"},
{Label: "2.5 Mbps - Medium", Bitrate: "2500k"},
{Label: "4.0 Mbps - Good", Bitrate: "4000k"},
{Label: "6.0 Mbps - High", Bitrate: "6000k"},
{Label: "8.0 Mbps - Very High", Bitrate: "8000k"},
{Label: "Manual", Bitrate: ""},
}
bitratePresetLookup := make(map[string]bitratePreset)
var bitratePresetLabels []string
for _, p := range presets {
bitratePresetLookup[p.Label] = p
bitratePresetLabels = append(bitratePresetLabels, p.Label)
}
manualBitrateEntry := widget.NewEntry()
manualBitrateEntry.SetPlaceHolder("e.g., 2500k")
manualBitrateEntry.SetText(state.upscaleManualBitrate)
manualBitrateEntry.OnChanged = func(val string) {
state.upscaleManualBitrate = val
}
bitratePresetSelect := widget.NewSelect(bitratePresetLabels, func(s string) {
state.upscaleBitratePreset = s
preset := bitratePresetLookup[s]
if preset.Bitrate == "" {
manualBitrateEntry.Show()
} else {
state.upscaleManualBitrate = preset.Bitrate
manualBitrateEntry.SetText(preset.Bitrate)
manualBitrateEntry.Hide()
}
})
bitratePresetSelect.SetSelected(state.upscaleBitratePreset)
if bitratePresetLookup[state.upscaleBitratePreset].Bitrate == "" {
manualBitrateEntry.Show()
} else {
manualBitrateEntry.Hide()
}
updateEncodingVisibility = func() {
mode := state.upscaleBitrateMode
if mode == "" || mode == "CRF" {
qualitySelect.Enable()
bitratePresetSelect.Hide()
manualBitrateEntry.Hide()
} else {
qualitySelect.Disable()
bitratePresetSelect.Show()
if bitratePresetLookup[state.upscaleBitratePreset].Bitrate == "" {
manualBitrateEntry.Show()
}
}
}
updateEncodingVisibility()
encodingSection := buildUpscaleBox("Video Encoding", container.NewVBox(
container.NewGridWithColumns(2,
widget.NewLabel("Encoder Preset:"),
encoderPresetSelect,
),
container.NewGridWithColumns(2,
widget.NewLabel("Quality Preset:"),
qualitySelect,
),
container.NewGridWithColumns(2,
widget.NewLabel("Bitrate Mode:"),
bitrateModeSelect,
),
container.NewGridWithColumns(2,
widget.NewLabel("Bitrate Preset:"),
bitratePresetSelect,
),
manualBitrateEntry,
widget.NewLabel("CRF mode controls quality; bitrate modes control size."),
)) ))
// Frame Rate Section // Frame Rate Section
@ -15123,7 +15352,7 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
}) })
motionInterpCheck.SetChecked(state.upscaleMotionInterpolation) motionInterpCheck.SetChecked(state.upscaleMotionInterpolation)
frameRateSection := widget.NewCard("Frame Rate", "", container.NewVBox( frameRateSection := buildUpscaleBox("Frame Rate", container.NewVBox(
widget.NewLabel("Convert frame rate (optional)"), widget.NewLabel("Convert frame rate (optional)"),
container.NewGridWithColumns(2, container.NewGridWithColumns(2,
widget.NewLabel("Target FPS:"), widget.NewLabel("Target FPS:"),
@ -15140,8 +15369,8 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
aiModelLabel = aiModelOptions[0] aiModelLabel = aiModelOptions[0]
} }
// AI Upscaling Section // AI Upscaling Section (nested under Scaling)
var aiSection *widget.Card var aiContent fyne.CanvasObject
if state.upscaleAIAvailable { if state.upscaleAIAvailable {
var aiTileSelect *widget.Select var aiTileSelect *widget.Select
var aiTTACheck *widget.Check var aiTTACheck *widget.Check
@ -15350,7 +15579,7 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
denoiseHint.TextStyle = fyne.TextStyle{Italic: true} denoiseHint.TextStyle = fyne.TextStyle{Italic: true}
updateDenoiseAvailability(state.upscaleAIModel) updateDenoiseAvailability(state.upscaleAIModel)
aiSection = widget.NewCard("AI Upscaling", "✓ Available", container.NewVBox( aiContent = container.NewVBox(
widget.NewLabel("Real-ESRGAN detected - enhanced quality available"), widget.NewLabel("Real-ESRGAN detected - enhanced quality available"),
aiEnabledCheck, aiEnabledCheck,
container.NewGridWithColumns(2, container.NewGridWithColumns(2,
@ -15388,26 +15617,49 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
container.NewGridWithColumns(3, aiThreadsLoad, aiThreadsProc, aiThreadsSave), container.NewGridWithColumns(3, aiThreadsLoad, aiThreadsProc, aiThreadsSave),
), ),
widget.NewLabel("Note: AI upscaling is slower but produces higher quality results"), widget.NewLabel("Note: AI upscaling is slower but produces higher quality results"),
)) )
} else { } else {
backendNote := "Real-ESRGAN not detected. Install for enhanced quality:" backendNote := "Real-ESRGAN not detected. Install for enhanced quality:"
if state.upscaleAIBackend == "python" { if state.upscaleAIBackend == "python" {
backendNote = "Python Real-ESRGAN detected, but the ncnn backend is required for now." backendNote = "Python Real-ESRGAN detected, but the ncnn backend is required for now."
} }
aiSection = widget.NewCard("AI Upscaling", "Not Available", container.NewVBox( aiContent = container.NewVBox(
widget.NewLabel(backendNote), widget.NewLabel(backendNote),
widget.NewLabel("https://github.com/xinntao/Real-ESRGAN"), widget.NewLabel("https://github.com/xinntao/Real-ESRGAN"),
widget.NewLabel("Traditional scaling methods will be used."), widget.NewLabel("Traditional scaling methods will be used."),
)) )
} }
aiSection := container.NewVBox(
widget.NewLabelWithStyle("AI Upscaling", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
widget.NewSeparator(),
aiContent,
)
traditionalSection := buildUpscaleBox("Scaling", container.NewVBox(
widget.NewLabel("Classic upscaling methods - always available"),
container.NewGridWithColumns(2,
widget.NewLabel("Scaling Algorithm:"),
methodSelect,
),
methodLabel,
widget.NewSeparator(),
methodInfo,
widget.NewSeparator(),
widget.NewLabel("Optional blur"),
blurCheck,
container.NewVBox(blurLabel, blurSlider),
widget.NewSeparator(),
aiSection,
))
// Filter Integration Section // Filter Integration Section
applyFiltersCheck := widget.NewCheck("Apply filters before upscaling", func(checked bool) { applyFiltersCheck := widget.NewCheck("Apply filters before upscaling", func(checked bool) {
state.upscaleApplyFilters = checked state.upscaleApplyFilters = checked
}) })
applyFiltersCheck.SetChecked(state.upscaleApplyFilters) applyFiltersCheck.SetChecked(state.upscaleApplyFilters)
filterIntegrationSection := widget.NewCard("Filter Integration", "", container.NewVBox( filterIntegrationSection := buildUpscaleBox("Filter Integration", container.NewVBox(
widget.NewLabel("Apply color correction and filters from Filters module"), widget.NewLabel("Apply color correction and filters from Filters module"),
applyFiltersCheck, applyFiltersCheck,
widget.NewLabel("Filters will be applied before upscaling for best quality"), widget.NewLabel("Filters will be applied before upscaling for best quality"),
@ -15452,6 +15704,10 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
"inputPath": state.upscaleFile.Path, "inputPath": state.upscaleFile.Path,
"outputPath": outputPath, "outputPath": outputPath,
"method": state.upscaleMethod, "method": state.upscaleMethod,
"encoderPreset": state.upscaleEncoderPreset,
"bitrateMode": state.upscaleBitrateMode,
"bitratePreset": state.upscaleBitratePreset,
"manualBitrate": state.upscaleManualBitrate,
"targetWidth": float64(targetWidth), "targetWidth": float64(targetWidth),
"targetHeight": float64(targetHeight), "targetHeight": float64(targetHeight),
"targetPreset": state.upscaleTargetRes, "targetPreset": state.upscaleTargetRes,
@ -15521,22 +15777,38 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
addQueueBtn.Importance = widget.MediumImportance addQueueBtn.Importance = widget.MediumImportance
// Main content // Main content
spacing := func() fyne.CanvasObject {
spacer := canvas.NewRectangle(color.Transparent)
spacer.SetMinSize(fyne.NewSize(0, 10))
return spacer
}
metaPanel, _ := buildMetadataPanel(state, state.upscaleFile, fyne.NewSize(0, 200))
leftPanel := container.NewVBox( leftPanel := container.NewVBox(
instructions, instructions,
widget.NewSeparator(), spacing(),
fileLabel, buildUpscaleBox("Video", container.NewVBox(
loadBtn, fileLabel,
filtersNavBtn, loadBtn,
filtersNavBtn,
videoContainer,
)),
spacing(),
metaPanel,
) )
settingsPanel := container.NewVBox( settingsPanel := container.NewVBox(
traditionalSection, traditionalSection,
spacing(),
resolutionSection, resolutionSection,
qualitySection, spacing(),
blurSection, encodingSection,
spacing(),
frameRateSection, frameRateSection,
aiSection, spacing(),
filterIntegrationSection, filterIntegrationSection,
spacing(),
container.NewGridWithColumns(2, applyBtn, addQueueBtn), container.NewGridWithColumns(2, applyBtn, addQueueBtn),
) )
@ -15544,12 +15816,14 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
// Adaptive height for small screens // Adaptive height for small screens
// Avoid rigid min sizes so window snapping works across modules. // Avoid rigid min sizes so window snapping works across modules.
mainContent := container.New(&fixedHSplitLayout{ratio: 0.6}, split := container.NewHSplit(leftPanel, settingsScroll)
container.NewVBox(leftPanel, videoContainer), split.Offset = 0.58
settingsScroll, mainContent := split
)
content := container.NewPadded(mainContent) content := container.NewMax(
canvas.NewRectangle(mediumBlue),
container.NewPadded(mainContent),
)
return container.NewBorder(topBar, bottomBar, nil, nil, content) return container.NewBorder(topBar, bottomBar, nil, nil, content)
} }