Add interlacing detection to Inspect module and preview feature
Features added: - Auto-detection in Inspect module: runs QuickAnalyze automatically when video is loaded - Interlacing results display in Inspect metadata panel - Deinterlace preview generation: side-by-side comparison button in Convert view - Analyze button integration in Simple menu deinterlacing section - Auto-apply deinterlacing settings when recommended The Inspect module now automatically analyzes videos for interlacing when loaded via: - Load button - Drag-and-drop to main menu tile - Drag-and-drop within Inspect view Results appear directly in the metadata panel with full detection details.
This commit is contained in:
parent
fb859c470a
commit
067e459de9
243
main.go
243
main.go
|
|
@ -70,7 +70,7 @@ var (
|
||||||
logsDirOnce sync.Once
|
logsDirOnce sync.Once
|
||||||
logsDirPath string
|
logsDirPath string
|
||||||
feedbackBundler = utils.NewFeedbackBundler()
|
feedbackBundler = utils.NewFeedbackBundler()
|
||||||
appVersion = "v0.1.0-dev15"
|
appVersion = "v0.1.0-dev16"
|
||||||
|
|
||||||
hwAccelProbeOnce sync.Once
|
hwAccelProbeOnce sync.Once
|
||||||
hwAccelSupported atomic.Value // map[string]bool
|
hwAccelSupported atomic.Value // map[string]bool
|
||||||
|
|
@ -602,8 +602,10 @@ type appState struct {
|
||||||
queueOffset fyne.Position
|
queueOffset fyne.Position
|
||||||
compareFile1 *videoSource
|
compareFile1 *videoSource
|
||||||
compareFile2 *videoSource
|
compareFile2 *videoSource
|
||||||
inspectFile *videoSource
|
inspectFile *videoSource
|
||||||
autoCompare bool // Auto-load Compare module after conversion
|
inspectInterlaceResult *interlace.DetectionResult
|
||||||
|
inspectInterlaceAnalyzing bool
|
||||||
|
autoCompare bool // Auto-load Compare module after conversion
|
||||||
|
|
||||||
// Merge state
|
// Merge state
|
||||||
mergeClips []mergeClip
|
mergeClips []mergeClip
|
||||||
|
|
@ -1632,8 +1634,31 @@ func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) {
|
||||||
time.Sleep(350 * time.Millisecond)
|
time.Sleep(350 * time.Millisecond)
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
s.inspectFile = src
|
s.inspectFile = src
|
||||||
|
s.inspectInterlaceResult = nil
|
||||||
|
s.inspectInterlaceAnalyzing = true
|
||||||
s.showModule(moduleID)
|
s.showModule(moduleID)
|
||||||
logging.Debug(logging.CatModule, "loaded video for inspect module")
|
logging.Debug(logging.CatModule, "loaded video for inspect module")
|
||||||
|
|
||||||
|
// Auto-run interlacing detection in background
|
||||||
|
go func() {
|
||||||
|
detector := interlace.NewDetector(platformConfig.FFmpegPath, platformConfig.FFprobePath)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result, err := detector.QuickAnalyze(ctx, path)
|
||||||
|
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
s.inspectInterlaceAnalyzing = false
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "auto interlacing analysis failed: %v", err)
|
||||||
|
s.inspectInterlaceResult = nil
|
||||||
|
} else {
|
||||||
|
s.inspectInterlaceResult = result
|
||||||
|
logging.Debug(logging.CatSystem, "auto interlacing analysis complete: %s", result.Status)
|
||||||
|
}
|
||||||
|
s.showInspectView() // Refresh to show results
|
||||||
|
}, false)
|
||||||
|
}()
|
||||||
}, false)
|
}, false)
|
||||||
}()
|
}()
|
||||||
return
|
return
|
||||||
|
|
@ -3872,6 +3897,74 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
inverseCheck.Checked = state.convert.InverseTelecine
|
inverseCheck.Checked = state.convert.InverseTelecine
|
||||||
inverseHint := widget.NewLabel(state.convert.InverseAutoNotes)
|
inverseHint := widget.NewLabel(state.convert.InverseAutoNotes)
|
||||||
|
|
||||||
|
// Interlacing Analysis Button (Simple Menu)
|
||||||
|
var analyzeInterlaceBtn *widget.Button
|
||||||
|
analyzeInterlaceBtn = widget.NewButton("Analyze Interlacing", func() {
|
||||||
|
if src == nil {
|
||||||
|
dialog.ShowInformation("Interlacing Analysis", "Load a video first.", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
analyzeInterlaceBtn.SetText("Analyzing...")
|
||||||
|
analyzeInterlaceBtn.Disable()
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
detector := interlace.NewDetector(platformConfig.FFmpegPath, platformConfig.FFprobePath)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result, err := detector.QuickAnalyze(ctx, src.Path)
|
||||||
|
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
analyzeInterlaceBtn.SetText("Analyze Interlacing")
|
||||||
|
analyzeInterlaceBtn.Enable()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "interlacing analysis failed: %v", err)
|
||||||
|
dialog.ShowError(fmt.Errorf("Analysis failed: %w", err), state.window)
|
||||||
|
} else {
|
||||||
|
state.interlaceResult = result
|
||||||
|
logging.Debug(logging.CatSystem, "interlacing analysis complete: %s", result.Status)
|
||||||
|
|
||||||
|
// Show results dialog
|
||||||
|
resultText := fmt.Sprintf(
|
||||||
|
"Status: %s\n"+
|
||||||
|
"Interlaced Frames: %.1f%%\n"+
|
||||||
|
"Field Order: %s\n"+
|
||||||
|
"Confidence: %s\n\n"+
|
||||||
|
"Recommendation:\n%s\n\n"+
|
||||||
|
"Frame Counts:\n"+
|
||||||
|
"Progressive: %d\n"+
|
||||||
|
"Top Field First: %d\n"+
|
||||||
|
"Bottom Field First: %d\n"+
|
||||||
|
"Undetermined: %d\n"+
|
||||||
|
"Total Analyzed: %d",
|
||||||
|
result.Status,
|
||||||
|
result.InterlacedPercent,
|
||||||
|
result.FieldOrder,
|
||||||
|
result.Confidence,
|
||||||
|
result.Recommendation,
|
||||||
|
result.Progressive,
|
||||||
|
result.TFF,
|
||||||
|
result.BFF,
|
||||||
|
result.Undetermined,
|
||||||
|
result.TotalFrames,
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.ShowInformation("Interlacing Analysis Results", resultText, state.window)
|
||||||
|
|
||||||
|
// Auto-update deinterlace setting
|
||||||
|
if result.SuggestDeinterlace && state.convert.Deinterlace == "Off" {
|
||||||
|
state.convert.Deinterlace = "Auto"
|
||||||
|
inverseCheck.SetChecked(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
analyzeInterlaceBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
// Auto-crop controls
|
// Auto-crop controls
|
||||||
autoCropCheck := widget.NewCheck("Auto-Detect Black Bars", func(checked bool) {
|
autoCropCheck := widget.NewCheck("Auto-Detect Black Bars", func(checked bool) {
|
||||||
state.convert.AutoCrop = checked
|
state.convert.AutoCrop = checked
|
||||||
|
|
@ -4872,6 +4965,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
|
|
||||||
widget.NewLabelWithStyle("═══ DEINTERLACING ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("═══ DEINTERLACING ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||||
|
analyzeInterlaceBtn,
|
||||||
inverseCheck,
|
inverseCheck,
|
||||||
inverseHint,
|
inverseHint,
|
||||||
layout.NewSpacer(),
|
layout.NewSpacer(),
|
||||||
|
|
@ -5543,11 +5637,81 @@ Metadata: %s`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
interlaceSection = container.NewVBox(
|
// Preview button (only show if deinterlacing is recommended)
|
||||||
|
var previewSection fyne.CanvasObject
|
||||||
|
if result.SuggestDeinterlace {
|
||||||
|
previewBtn := widget.NewButton("Generate Deinterlace Preview", func() {
|
||||||
|
if state.source == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
dialog.ShowInformation("Generating Preview", "Creating comparison preview...", state.window)
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
detector := interlace.NewDetector(platformConfig.FFmpegPath, platformConfig.FFprobePath)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Generate preview at 10 seconds into the video
|
||||||
|
previewPath := filepath.Join(os.TempDir(), fmt.Sprintf("deinterlace_preview_%d.png", time.Now().Unix()))
|
||||||
|
err := detector.GenerateComparisonPreview(ctx, state.source.Path, 10.0, previewPath)
|
||||||
|
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "preview generation failed: %v", err)
|
||||||
|
dialog.ShowError(fmt.Errorf("Preview generation failed: %w", err), state.window)
|
||||||
|
} else {
|
||||||
|
// Load and display the preview image
|
||||||
|
img, err := fyne.LoadResourceFromPath(previewPath)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("Failed to load preview: %w", err), state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
previewImg := canvas.NewImageFromResource(img)
|
||||||
|
previewImg.FillMode = canvas.ImageFillContain
|
||||||
|
previewImg.SetMinSize(fyne.NewSize(800, 450))
|
||||||
|
|
||||||
|
infoLabel := widget.NewLabel("Left: Original | Right: Deinterlaced")
|
||||||
|
infoLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
infoLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
|
||||||
|
content := container.NewBorder(
|
||||||
|
infoLabel,
|
||||||
|
nil, nil, nil,
|
||||||
|
container.NewScroll(previewImg),
|
||||||
|
)
|
||||||
|
|
||||||
|
previewDialog := dialog.NewCustom("Deinterlace Preview", "Close", content, state.window)
|
||||||
|
previewDialog.Resize(fyne.NewSize(900, 600))
|
||||||
|
previewDialog.Show()
|
||||||
|
|
||||||
|
// Clean up temp file after dialog closes
|
||||||
|
go func() {
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
os.Remove(previewPath)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
previewBtn.Importance = widget.LowImportance
|
||||||
|
previewSection = previewBtn
|
||||||
|
}
|
||||||
|
|
||||||
|
var sectionItems []fyne.CanvasObject
|
||||||
|
sectionItems = append(sectionItems,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
analyzeBtn,
|
analyzeBtn,
|
||||||
container.NewPadded(container.NewMax(resultCard, resultContent)),
|
container.NewPadded(container.NewMax(resultCard, resultContent)),
|
||||||
)
|
)
|
||||||
|
if previewSection != nil {
|
||||||
|
sectionItems = append(sectionItems, previewSection)
|
||||||
|
}
|
||||||
|
|
||||||
|
interlaceSection = container.NewVBox(sectionItems...)
|
||||||
} else {
|
} else {
|
||||||
interlaceSection = container.NewVBox(
|
interlaceSection = container.NewVBox(
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
|
|
@ -6558,8 +6722,32 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
||||||
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
s.inspectFile = src
|
s.inspectFile = src
|
||||||
|
s.inspectInterlaceResult = nil
|
||||||
|
s.inspectInterlaceAnalyzing = true
|
||||||
s.showInspectView()
|
s.showInspectView()
|
||||||
logging.Debug(logging.CatModule, "loaded video into inspect module")
|
logging.Debug(logging.CatModule, "loaded video into inspect module")
|
||||||
|
|
||||||
|
// Auto-run interlacing detection in background
|
||||||
|
videoPath := videoPaths[0]
|
||||||
|
go func() {
|
||||||
|
detector := interlace.NewDetector(platformConfig.FFmpegPath, platformConfig.FFprobePath)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result, err := detector.QuickAnalyze(ctx, videoPath)
|
||||||
|
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
s.inspectInterlaceAnalyzing = false
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "auto interlacing analysis failed: %v", err)
|
||||||
|
s.inspectInterlaceResult = nil
|
||||||
|
} else {
|
||||||
|
s.inspectInterlaceResult = result
|
||||||
|
logging.Debug(logging.CatSystem, "auto interlacing analysis complete: %s", result.Status)
|
||||||
|
}
|
||||||
|
s.showInspectView() // Refresh to show results
|
||||||
|
}, false)
|
||||||
|
}()
|
||||||
}, false)
|
}, false)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -9097,7 +9285,7 @@ func buildInspectView(state *appState) fyne.CanvasObject {
|
||||||
fileSize = utils.FormatBytes(fi.Size())
|
fileSize = utils.FormatBytes(fi.Size())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf(
|
metadata := fmt.Sprintf(
|
||||||
"━━━ FILE INFO ━━━\n"+
|
"━━━ FILE INFO ━━━\n"+
|
||||||
"Path: %s\n"+
|
"Path: %s\n"+
|
||||||
"File Size: %s\n"+
|
"File Size: %s\n"+
|
||||||
|
|
@ -9145,6 +9333,28 @@ func buildInspectView(state *appState) fyne.CanvasObject {
|
||||||
src.HasChapters,
|
src.HasChapters,
|
||||||
src.HasMetadata,
|
src.HasMetadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Add interlacing detection results if available
|
||||||
|
if state.inspectInterlaceAnalyzing {
|
||||||
|
metadata += "\n\n━━━ INTERLACING DETECTION ━━━\n"
|
||||||
|
metadata += "Analyzing... (first 500 frames)"
|
||||||
|
} else if state.inspectInterlaceResult != nil {
|
||||||
|
result := state.inspectInterlaceResult
|
||||||
|
metadata += "\n\n━━━ INTERLACING DETECTION ━━━\n"
|
||||||
|
metadata += fmt.Sprintf("Status: %s\n", result.Status)
|
||||||
|
metadata += fmt.Sprintf("Interlaced Frames: %.1f%%\n", result.InterlacedPercent)
|
||||||
|
metadata += fmt.Sprintf("Field Order: %s\n", result.FieldOrder)
|
||||||
|
metadata += fmt.Sprintf("Confidence: %s\n", result.Confidence)
|
||||||
|
metadata += fmt.Sprintf("Recommendation: %s\n", result.Recommendation)
|
||||||
|
metadata += fmt.Sprintf("\nFrame Counts:\n")
|
||||||
|
metadata += fmt.Sprintf(" Progressive: %d\n", result.Progressive)
|
||||||
|
metadata += fmt.Sprintf(" Top Field First: %d\n", result.TFF)
|
||||||
|
metadata += fmt.Sprintf(" Bottom Field First: %d\n", result.BFF)
|
||||||
|
metadata += fmt.Sprintf(" Undetermined: %d\n", result.Undetermined)
|
||||||
|
metadata += fmt.Sprintf(" Total Analyzed: %d", result.TotalFrames)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video player container
|
// Video player container
|
||||||
|
|
@ -9200,8 +9410,31 @@ func buildInspectView(state *appState) fyne.CanvasObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
state.inspectFile = src
|
state.inspectFile = src
|
||||||
|
state.inspectInterlaceResult = nil
|
||||||
|
state.inspectInterlaceAnalyzing = true
|
||||||
state.showInspectView()
|
state.showInspectView()
|
||||||
logging.Debug(logging.CatModule, "loaded inspect file: %s", path)
|
logging.Debug(logging.CatModule, "loaded inspect file: %s", path)
|
||||||
|
|
||||||
|
// Auto-run interlacing detection in background
|
||||||
|
go func() {
|
||||||
|
detector := interlace.NewDetector(platformConfig.FFmpegPath, platformConfig.FFprobePath)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
result, err := detector.QuickAnalyze(ctx, path)
|
||||||
|
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
state.inspectInterlaceAnalyzing = false
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "auto interlacing analysis failed: %v", err)
|
||||||
|
state.inspectInterlaceResult = nil
|
||||||
|
} else {
|
||||||
|
state.inspectInterlaceResult = result
|
||||||
|
logging.Debug(logging.CatSystem, "auto interlacing analysis complete: %s", result.Status)
|
||||||
|
}
|
||||||
|
state.showInspectView() // Refresh to show results
|
||||||
|
}, false)
|
||||||
|
}()
|
||||||
}, state.window)
|
}, state.window)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user