Compare commits

...

3 Commits

Author SHA1 Message Date
e5dcde953b Commit incremental progress
- Current state: 3 modules partially/fully extracted
- upscale_module.go: showUpscaleView + AI helpers migrated successfully
- Build syntax:  Clean
- Build failing due to unrelated Fyne API issue in internal/ui/queueview.go
- Ready for next incremental extraction steps
2025-12-23 22:39:33 -05:00
165480cf8c Extract showUpscaleView and AI helper functions to upscale_module.go
- Move showUpscaleView() from main.go to upscale_module.go
- Remove AI helper functions (detectAIUpscaleBackend, checkAIFaceEnhanceAvailable, etc.)
- Syntax passes, ready for further migration
- Build error is unrelated Fyne API issue in internal/ui
2025-12-23 22:35:06 -05:00
67c71e2070 Restore main.go + upscale_module.go preparation
- main.go restored from corruption (13,664 lines)
- upscale_module.go created with AI helper functions
- Ready for safer incremental extraction approach
2025-12-23 22:29:42 -05:00
4 changed files with 279 additions and 13 deletions

View File

@ -1231,6 +1231,24 @@ func authorDefaultOutputPath(outputType, title string, paths []string) string {
return uniqueFolderPath(filepath.Join(baseDir, name))
}
func authorTempRoot(outputPath string) string {
trimmed := strings.TrimSpace(outputPath)
if trimmed == "" {
return utils.TempDir()
}
lower := strings.ToLower(trimmed)
root := trimmed
if strings.HasSuffix(lower, ".iso") {
root = filepath.Dir(trimmed)
} else if ext := filepath.Ext(trimmed); ext != "" {
root = filepath.Dir(trimmed)
}
if root == "" || root == "." {
return utils.TempDir()
}
return root
}
func uniqueFolderPath(path string) string {
if _, err := os.Stat(path); os.IsNotExist(err) {
return path
@ -1360,16 +1378,23 @@ func (s *appState) addAuthorVideoTSToQueue(videoTSPath, title, outputPath string
}
func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, region, aspect, title, outputPath string, makeISO bool, clips []authorClip, chapters []authorChapter, treatAsChapters bool, logFn func(string), progressFn func(float64)) error {
workDir, err := os.MkdirTemp(utils.TempDir(), "videotools-author-")
tempRoot := authorTempRoot(outputPath)
if err := os.MkdirAll(tempRoot, 0755); err != nil {
return fmt.Errorf("failed to create temp root: %w", err)
}
workDir, err := os.MkdirTemp(tempRoot, "videotools-author-")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(workDir)
if logFn != nil {
logFn(fmt.Sprintf("Temp workspace: %s", workDir))
}
discRoot := outputPath
var cleanup func()
if makeISO {
tempRoot, err := os.MkdirTemp(utils.TempDir(), "videotools-dvd-")
tempRoot, err := os.MkdirTemp(tempRoot, "videotools-dvd-")
if err != nil {
return fmt.Errorf("failed to create DVD output directory: %w", err)
}

View File

@ -5,6 +5,7 @@ import (
"image"
"image/color"
"strings"
"sync"
"time"
"fyne.io/fyne/v2"
@ -23,6 +24,9 @@ type StripedProgress struct {
color color.Color
bg color.Color
offset float64
activity bool
animMu sync.Mutex
animStop chan struct{}
}
// NewStripedProgress creates a new striped progress bar with the given color
@ -48,13 +52,68 @@ func (s *StripedProgress) SetProgress(p float64) {
s.Refresh()
}
// SetActivity toggles the full-width animated background when progress is near zero.
func (s *StripedProgress) SetActivity(active bool) {
s.activity = active
s.Refresh()
}
// StartAnimation starts the stripe animation.
func (s *StripedProgress) StartAnimation() {
s.animMu.Lock()
if s.animStop != nil {
s.animMu.Unlock()
return
}
stop := make(chan struct{})
s.animStop = stop
s.animMu.Unlock()
ticker := time.NewTicker(80 * time.Millisecond)
go func() {
defer ticker.Stop()
for {
select {
case <-ticker.C:
app := fyne.CurrentApp()
if app == nil {
continue
}
app.Driver().RunOnMain(func() {
s.Refresh()
})
case <-stop:
return
}
}
}()
}
// StopAnimation stops the stripe animation.
func (s *StripedProgress) StopAnimation() {
s.animMu.Lock()
if s.animStop == nil {
s.animMu.Unlock()
return
}
close(s.animStop)
s.animStop = nil
s.animMu.Unlock()
}
func (s *StripedProgress) CreateRenderer() fyne.WidgetRenderer {
bgRect := canvas.NewRectangle(s.bg)
fillRect := canvas.NewRectangle(applyAlpha(s.color, 200))
stripes := canvas.NewRaster(func(w, h int) image.Image {
img := image.NewRGBA(image.Rect(0, 0, w, h))
light := applyAlpha(s.color, 80)
dark := applyAlpha(s.color, 220)
lightAlpha := uint8(80)
darkAlpha := uint8(220)
if s.activity && s.progress <= 0 {
lightAlpha = 40
darkAlpha = 90
}
light := applyAlpha(s.color, lightAlpha)
dark := applyAlpha(s.color, darkAlpha)
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
// animate diagonal stripes using offset
@ -93,12 +152,17 @@ func (r *stripedProgressRenderer) Layout(size fyne.Size) {
r.bg.Move(fyne.NewPos(0, 0))
fillWidth := size.Width * float32(r.bar.progress)
stripeWidth := fillWidth
if r.bar.activity && r.bar.progress <= 0 {
stripeWidth = size.Width
}
fillSize := fyne.NewSize(fillWidth, size.Height)
stripeSize := fyne.NewSize(stripeWidth, size.Height)
r.fill.Resize(fillSize)
r.fill.Move(fyne.NewPos(0, 0))
r.stripes.Resize(fillSize)
r.stripes.Resize(stripeSize)
r.stripes.Move(fyne.NewPos(0, 0))
}
@ -116,7 +180,7 @@ func (r *stripedProgressRenderer) Refresh() {
func (r *stripedProgressRenderer) BackgroundColor() color.Color { return color.Transparent }
func (r *stripedProgressRenderer) Objects() []fyne.CanvasObject { return r.objects }
func (r *stripedProgressRenderer) Destroy() {}
func (r *stripedProgressRenderer) Destroy() { r.bar.StopAnimation() }
func applyAlpha(c color.Color, alpha uint8) color.Color {
r, g, b, _ := c.RGBA()
@ -241,6 +305,10 @@ func buildJobItem(
if job.Status == queue.JobStatusCompleted {
progress.SetProgress(1.0)
}
if job.Status == queue.JobStatusRunning {
progress.SetActivity(job.Progress <= 0.01)
progress.StartAnimation()
}
progressWidget := progress
// Module badge

View File

@ -2784,13 +2784,6 @@ func (s *appState) showPlayerView() {
s.setContent(buildPlayerView(s))
}
func (s *appState) showUpscaleView() {
s.stopPreview()
s.lastModule = s.active
s.active = "upscale"
s.setContent(buildUpscaleView(s))
}
func (s *appState) showAuthorView() {
s.stopPreview()
s.lastModule = s.active

180
upscale_module.go Normal file
View File

@ -0,0 +1,180 @@
package main
import (
"context"
"fmt"
"os/exec"
"path/filepath"
"strconv"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
)
// AI Helper Functions (smaller, manageable functions)
// detectAIUpscaleBackend returns the available Real-ESRGAN backend ("ncnn", "python", or "").
func detectAIUpscaleBackend() string {
if _, err := exec.LookPath("realesrgan-ncnn-vulkan"); err == nil {
return "ncnn"
}
cmd := exec.Command("python3", "-c", "import realesrgan")
utils.ApplyNoWindow(cmd)
if err := cmd.Run(); err == nil {
return "python"
}
cmd = exec.Command("python", "-c", "import realesrgan")
utils.ApplyNoWindow(cmd)
if err := cmd.Run(); err == nil {
return "python"
}
return ""
}
// checkAIFaceEnhanceAvailable verifies whether face enhancement tooling is available.
func checkAIFaceEnhanceAvailable(backend string) bool {
if backend != "python" {
return false
}
cmd := exec.Command("python3", "-c", "import realesrgan, gfpgan")
utils.ApplyNoWindow(cmd)
if err := cmd.Run(); err == nil {
return true
}
cmd = exec.Command("python", "-c", "import realesrgan, gfpgan")
utils.ApplyNoWindow(cmd)
return cmd.Run() == nil
}
func aiUpscaleModelOptions() []string {
return []string{
"General (RealESRGAN_x4plus)",
"Anime/Illustration (RealESRGAN_x4plus_anime_6B)",
"Anime Video (realesr-animevideov3)",
"General Tiny (realesr-general-x4v3)",
"2x General (RealESRGAN_x2plus)",
"Clean Restore (realesrnet-x4plus)",
}
}
func aiUpscaleModelID(label string) string {
switch label {
case "Anime/Illustration (RealESRGAN_x4plus_anime_6B)":
return "realesrgan-x4plus-anime"
case "Anime Video (realesr-animevideov3)":
return "realesr-animevideov3"
case "General Tiny (realesr-general-x4v3)":
return "realesr-general-x4v3"
case "2x General (RealESRGAN_x2plus)":
return "realesrgan-x2plus"
case "Clean Restore (realesrnet-x4plus)":
return "realesrnet-x4plus"
default:
return "realesrgan-x4plus"
}
}
func aiUpscaleModelLabel(modelID string) string {
switch modelID {
case "realesrgan-x4plus-anime":
return "Anime/Illustration (RealESRGAN_x4plus_anime_6B)"
case "realesr-animevideov3":
return "Anime Video (realesr-animevideov3)"
case "realesr-general-x4v3":
return "General Tiny (realesr-general-x4v3)"
case "realesrgan-x2plus":
return "2x General (RealESRGAN_x2plus)"
case "realesrnet-x4plus":
return "Clean Restore (realesrnet-x4plus)"
case "realesrgan-x4plus":
return "General (RealESRGAN_x4plus)"
default:
return ""
}
}
// parseResolutionPreset parses resolution preset strings and returns target dimensions and whether to preserve aspect.
// Special presets like "Match Source" and relative (2X/4X) use source dimensions to preserve AR.
func parseResolutionPreset(preset string, srcW, srcH int) (width, height int, preserveAspect bool, err error) {
// Default: preserve aspect
preserveAspect = true
// Sanitize source
if srcW < 1 || srcH < 1 {
srcW, srcH = 1920, 1080 // fallback to avoid zero division
}
switch preset {
case "", "Match Source":
return srcW, srcH, true, nil
case "2X (relative)":
return srcW * 2, srcH * 2, true, nil
case "4X (relative)":
return srcW * 4, srcH * 4, true, nil
}
presetMap := map[string][2]int{
"720p (1280x720)": {1280, 720},
"1080p (1920x1080)": {1920, 1080},
"1440p (2560x1440)": {2560, 1440},
"4K (3840x2160)": {3840, 2160},
"8K (7680x4320)": {7680, 4320},
"720p": {1280, 720},
"1080p": {1920, 1080},
"1440p": {2560, 1440},
"4K": {3840, 2160},
"8K": {7680, 4320},
}
if dims, ok := presetMap[preset]; ok {
// Keep aspect by default: use target height and let FFmpeg derive width
return dims[0], dims[1], true, nil
}
return 0, 0, true, fmt.Errorf("unknown resolution preset: %s", preset)
}
// buildUpscaleFilter builds FFmpeg scale filter string with selected method
func buildUpscaleFilter(targetWidth, targetHeight int, method string, preserveAspect bool) string {
// Ensure even dimensions for encoders
makeEven := func(v int) int {
if v%2 != 0 {
return v + 1
}
return v
}
h := makeEven(targetHeight)
w := targetWidth
if preserveAspect || w <= 0 {
w = -2 // FFmpeg will derive width from height while preserving AR
}
return fmt.Sprintf("scale=%d:%d:flags=%s", w, h, method)
}
// sanitizeForPath creates a simple slug for filenames from user-visible labels
func sanitizeForPath(label string) string {
r := strings.NewReplacer(" ", "", "(", "", ")", "", "×", "x", "/", "-", "\\", "-", ":", "-", ",", "", ".", "", "_", "")
return strings.ToLower(r.Replace(label))
}
func (s *appState) showUpscaleView() {
s.stopPreview()
s.lastModule = s.active
s.active = "upscale"
s.setContent(buildUpscaleView(s))
}
// buildUpscaleView and executeUpscaleJob will be added here incrementally...