Compare commits

...

4 Commits

Author SHA1 Message Date
7a82542f91 Rewrite Author module docs for accessibility 2025-12-26 19:33:51 -05:00
230523c737 Add comprehensive Author module documentation
Created AUTHOR_MODULE.md covering all features in detail:

Chapter Detection:
- How scene detection works (FFmpeg filter threshold)
- Sensitivity slider guide (0.1-0.9 range explained)
- Visual preview feature with thumbnails
- Use cases for different video types
- Technical implementation details

DVD Timestamp Correction:
- SCR backwards error explanation
- Automatic remux solution (-fflags +genpts)
- Why it's needed and how it works
- Performance impact (negligible)

Authoring Log Viewer:
- Tail behavior (last 100 lines)
- Performance optimizations explained
- Copy Log and View Full Log buttons
- Memory usage improvements (O(1) vs O(n))

Complete Workflows:
- Single movie to DVD
- TV series multi-title disc
- Concert with manual chapters

Troubleshooting:
- SCR errors
- Too many/few chapters detected
- UI lag issues (now fixed)
- ISO burning problems
- Playback stuttering

Technical Details:
- Full encoding pipeline with commands
- Chapter XML format
- Temporary file locations
- Dependencies and installation
- File size estimates for NTSC/PAL

Also updated DVD_USER_GUIDE.md:
- Removed branding footer
- Added reference to AUTHOR_MODULE.md for technical details
2025-12-26 19:33:51 -05:00
0d1235d867 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
2025-12-26 19:33:51 -05:00
d781ce2d58 Optimize author log viewer performance with tail behavior
Problem: Author log was causing severe UI lag and memory issues
- Unbounded string growth (entire log kept in RAM)
- Full widget rebuild on every line append
- O(n²) string concatenation performance

Solutions implemented:
1. Tail behavior - Keep only last 100 lines in UI
   - Uses circular buffer (authorLogLines slice)
   - Prevents unbounded memory growth
   - Rebuilds text from buffer on each append

2. Copy Log button
   - Copies full log from file (accurate)
   - Falls back to in-memory log if file unavailable

3. View Full Log button
   - Opens full log in dedicated log viewer
   - No UI lag from large logs

4. Track log file path
   - authorLogFilePath stored when log created
   - Used by copy and view buttons

Performance improvements:
- Memory: O(1) instead of O(n) - fixed 100 line buffer
- CPU: One SetText() per line instead of concatenation
- UI responsiveness: Dramatically improved with tail behavior

UI changes:
- Label shows "Authoring Log (last 100 lines)"
- Copy/View buttons for accessing full log
2025-12-26 19:33:51 -05:00
4 changed files with 437 additions and 8 deletions

View File

@ -414,10 +414,15 @@ func buildChaptersTab(state *appState) fyne.CanvasObject {
dialog.ShowInformation("Scene Detection", "No scene changes detected at the current sensitivity.", state.window)
return
}
state.authorChapters = chapters
state.authorChapterSource = "scenes"
state.updateAuthorSummary()
refreshChapters()
// Show chapter preview dialog for visual verification
state.showChapterPreview(targetPath, chapters, func(accepted bool) {
if accepted {
state.authorChapters = chapters
state.authorChapterSource = "scenes"
state.updateAuthorSummary()
refreshChapters()
}
})
})
}()
})
@ -750,6 +755,42 @@ func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
logScroll.SetMinSize(fyne.NewSize(0, 200))
state.authorLogScroll = logScroll
// Log control buttons
copyLogBtn := widget.NewButton("Copy Log", func() {
if state.authorLogFilePath != "" {
// Copy from file for accuracy
if data, err := os.ReadFile(state.authorLogFilePath); err == nil {
state.window.Clipboard().SetContent(string(data))
dialog.ShowInformation("Copied", "Full authoring log copied to clipboard", state.window)
return
}
}
// Fallback to in-memory log
state.window.Clipboard().SetContent(state.authorLogText)
dialog.ShowInformation("Copied", "Authoring log copied to clipboard", state.window)
})
copyLogBtn.Importance = widget.LowImportance
viewFullLogBtn := widget.NewButton("View Full Log", func() {
if state.authorLogFilePath == "" || state.authorLogFilePath == "-" {
dialog.ShowInformation("No Log File", "No log file available to view", state.window)
return
}
if _, err := os.Stat(state.authorLogFilePath); err != nil {
dialog.ShowError(fmt.Errorf("log file not found: %w", err), state.window)
return
}
state.openLogViewer("Authoring Log", state.authorLogFilePath, false)
})
viewFullLogBtn.Importance = widget.LowImportance
logControls := container.NewHBox(
widget.NewLabel("Authoring Log (last 100 lines):"),
layout.NewSpacer(),
copyLogBtn,
viewFullLogBtn,
)
controls := container.NewVBox(
widget.NewLabel("Generate DVD/ISO:"),
widget.NewSeparator(),
@ -759,7 +800,7 @@ func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
statusLabel,
progressBar,
widget.NewSeparator(),
widget.NewLabel("Authoring Log:"),
logControls,
logScroll,
widget.NewSeparator(),
generateBtn,
@ -1162,6 +1203,8 @@ func concatDVDMpg(inputs []string, output string) error {
func (s *appState) resetAuthorLog() {
s.authorLogText = ""
s.authorLogLines = nil
s.authorLogFilePath = ""
if s.authorLogEntry != nil {
s.authorLogEntry.SetText("")
}
@ -1174,7 +1217,17 @@ func (s *appState) appendAuthorLog(line string) {
if strings.TrimSpace(line) == "" {
return
}
s.authorLogText += line + "\n"
// Keep only last 100 lines for UI display (tail behavior)
const maxLines = 100
s.authorLogLines = append(s.authorLogLines, line)
if len(s.authorLogLines) > maxLines {
s.authorLogLines = s.authorLogLines[len(s.authorLogLines)-maxLines:]
}
// Rebuild text from buffer
s.authorLogText = strings.Join(s.authorLogLines, "\n")
if s.authorLogEntry != nil {
s.authorLogEntry.SetText(s.authorLogText)
}
@ -1726,6 +1779,7 @@ func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progres
logging.Debug(logging.CatSystem, "author log open failed: %v", logErr)
} else {
job.LogPath = logPath
s.authorLogFilePath = logPath // Store for UI access
defer logFile.Close()
}
@ -1828,6 +1882,7 @@ func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progres
logging.Debug(logging.CatSystem, "author log open failed: %v", logErr)
} else {
job.LogPath = logPath
s.authorLogFilePath = logPath // Store for UI access
defer logFile.Close()
}
@ -2226,6 +2281,116 @@ func runCommand(name string, args []string) error {
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()) {
fn()
}

263
docs/AUTHOR_MODULE.md Normal file
View File

@ -0,0 +1,263 @@
# Author Module Guide
## What Does This Do?
The Author module turns your video files into DVDs that'll play in any DVD player - the kind you'd hook up to a TV. It handles all the technical stuff so you don't have to worry about it.
---
## Getting Started
### Making a Single DVD
1. Click **Author** from the main menu
2. **Files Tab** → Click "Select File" → Pick your video
3. **Settings Tab**:
- DVD or Blu-ray (pick DVD for now)
- NTSC or PAL - pick NTSC if you're in the US
- 16:9 or 4:3 - pick 16:9 for widescreen
4. **Generate Tab** → Click "Generate DVD/ISO"
5. Wait for it to finish, then burn the .iso file to a DVD-R
That's it. The DVD will play in any player.
---
## Scene Detection - Finding Chapter Points Automatically
### What Are Chapters?
You know how DVDs let you skip to different parts of the movie? Those are chapters. The Author module can find these automatically by detecting when scenes change.
### How to Use It
1. Load your video (Files or Clips tab)
2. Go to **Chapters Tab**
3. Move the "Detection Sensitivity" slider:
- Move it **left** for more chapters (catches small changes)
- Move it **right** for fewer chapters (only big changes)
4. Click "Detect Scenes"
5. Look at the thumbnails that pop up - these show where chapters will be
6. If it looks good, click "Accept." If not, click "Reject" and try a different sensitivity
### What Sensitivity Should I Use?
It depends on your video:
- **Movies**: Use 0.5 - 0.6 (only major scene changes)
- **TV shows**: Use 0.3 - 0.4 (catches scene changes between commercial breaks)
- **Music videos**: Use 0.2 - 0.3 (lots of quick cuts)
- **Your phone videos**: Use 0.4 - 0.5 (depends on how much you moved around)
Don't stress about getting it perfect. Just adjust the slider and click "Detect Scenes" again until the preview looks right.
### The Preview Window
After detection runs, you'll see a grid of thumbnails. Each thumbnail is a freeze-frame from where a chapter starts. This lets you actually see if the detection makes sense - way better than just seeing a list of timestamps.
The preview shows the first 24 chapters. If more were detected, you'll see a message like "Found 152 chapters (showing first 24)". That's a sign you should increase the sensitivity slider.
---
## Understanding the Settings
### Output Type
**DVD** - Standard DVD format. Works everywhere.
**Blu-ray** - Not ready yet. Stick with DVD.
### Region
**NTSC** - US, Canada, Japan. Videos play at 30 frames per second.
**PAL** - Europe, Australia, most of the world. Videos play at 25 frames per second.
Pick based on where you live. If you're not sure, pick NTSC.
### Aspect Ratio
**16:9** - Widescreen. Use this for videos from phones, cameras, YouTube.
**4:3** - Old TV shape. Only use if your video is actually in this format (rare now).
**AUTO** - Let the software decide. Safe choice.
When in doubt, use 16:9.
### Disc Size
**DVD5** - Holds 4.7 GB. Standard blank DVDs you buy at the store.
**DVD9** - Holds 8.5 GB. Dual-layer discs (more expensive).
Use DVD5 unless you're making a really long video (over 2 hours).
---
## Common Scenarios
### Scenario 1: Burning Home Videos to DVD
You filmed stuff on your phone and want to give it to relatives who don't use computers much.
1. **Files Tab** → Select your phone video
2. **Chapters Tab** → Detect scenes with sensitivity around 0.4
3. Check the preview - should show major moments (birthday, cake, opening presents, etc.)
4. **Settings Tab**:
- Output Type: DVD
- Region: NTSC
- Aspect Ratio: 16:9
5. **Generate Tab**:
- Title: "Birthday 2024"
- Pick where to save it
- Click Generate
6. When done, burn the .iso file to a DVD-R
7. Hand it to grandma - it'll just work in her DVD player
### Scenario 2: Multiple Episodes on One Disc
You downloaded 3 episodes of a show and want them on one disc with a menu.
1. **Clips Tab** → Click "Add Video" for each episode
2. Leave "Treat as Chapters" OFF - this keeps them as separate titles
3. **Settings Tab**:
- Output Type: DVD
- Region: NTSC
- Create Menu: YES (important!)
4. **Generate Tab** → Generate the disc
5. The DVD will have a menu where you can pick which episode to watch
### Scenario 3: Concert Video with Song Chapters
You recorded a concert and want to skip to specific songs.
Option A - Automatic:
1. Load the concert video
2. **Chapters Tab** → Try sensitivity 0.3 first
3. Look at preview - if chapters line up with songs, you're done
4. If not, adjust sensitivity and try again
Option B - Manual:
1. Play through the video and note the times when songs start
2. **Chapters Tab** → Click "+ Add Chapter" for each song
3. Enter the time (like 3:45 for 3 minutes 45 seconds)
4. Name it (Song 1, Song 2, etc.)
---
## What's Happening Behind the Scenes?
You don't need to know this to use the software, but if you're curious:
### The Encoding Process
When you click Generate:
1. **Encoding**: Your video gets converted to MPEG-2 format (the DVD standard)
2. **Timestamp Fix**: The software makes sure the timestamps are perfectly sequential (DVDs are picky about this)
3. **Structure Creation**: It builds the VIDEO_TS folder structure that DVD players expect
4. **ISO Creation**: If you picked ISO, everything gets packed into one burnable file
### Why Does It Take So Long?
Converting video to MPEG-2 is CPU-intensive. A 90-minute video might take 30-60 minutes to encode, depending on your computer. You can queue multiple jobs and let it run overnight.
### The Timestamp Fix Thing
Some videos, especially .avi files, have timestamps that go slightly backwards occasionally. DVD players hate this and will error out. The software automatically fixes it by running the encoded video through a "remux" step - think of it like reformatting a document to fix the page numbers. Takes a few extra seconds but ensures the DVD actually works.
---
## Troubleshooting
### "I got 200 chapters, that's way too many"
Your sensitivity is too low. Move the slider right to 0.5 or higher and try again.
### "It only found 3 chapters in a 2-hour movie"
Sensitivity is too high. Move the slider left to 0.3 or 0.4.
### "The program is really slow when generating"
That's normal. Encoding video is slow. The good news is you can:
- Queue multiple jobs and walk away
- Work on other stuff - the encoding happens in the background
- Check the log to see progress
### "The authoring log is making everything lag"
This was a bug that's now fixed. The log only shows the last 100 lines. If you want to see everything, click "View Full Log" and it opens in a separate window.
### "My ISO file won't fit on a DVD-R"
Your video is too long or the quality is too high. Options:
- Use a dual-layer DVD-R (DVD9) instead
- Split into 2 discs
- Check if you accidentally loaded multiple long videos
### "The DVD plays but skips or stutters"
This is usually because your original video had variable frame rate (VFR) - phone videos often do this. The software will warn you if it detects this. Solution:
- Try generating again (sometimes it just works)
- Convert the source video to constant frame rate first using the Convert module
- Check if the source video itself plays smoothly
---
## File Size Reference
Here's roughly how much video fits on each disc type:
**DVD5 (4.7 GB)**
- About 2 hours of video at standard quality
- Most movies fit comfortably
**DVD9 (8.5 GB)**
- About 4 hours of video
- Good for director's cuts or multiple episodes
If you're over these limits, split your content across multiple discs.
---
## The Output Files Explained
### VIDEO_TS Folder
This is what DVD players actually read. It contains:
- .IFO files - the "table of contents"
- .VOB files - the actual video data
You can copy this folder to a USB drive and some DVD players can read it directly.
### ISO File
Think of this as a zip file of the VIDEO_TS folder, formatted specifically for burning to disc. When you burn an ISO to a DVD-R, it extracts everything into the right structure automatically.
---
## Tips
**Test Before Making Multiple Copies**
Make one disc, test it in a DVD player, make sure everything works. Then make more copies.
**Name Your Files Clearly**
Use names like "vacation_2024.iso" not "output.iso". Future you will thank you.
**Keep the Source Files**
Don't delete your original videos after making DVDs. Hard drives are cheap, memories aren't.
**Preview the Chapters**
Always check that chapter preview before accepting. It takes 10 seconds and prevents surprises.
**Use the Queue**
Got 5 videos to convert? Add them all to the queue and start it before bed. They'll all be done by morning.
---
## Related Guides
- **DVD_USER_GUIDE.md** - How to use the Convert module for DVD encoding
- **QUEUE_SYSTEM_GUIDE.md** - Managing multiple jobs
- **MODULES.md** - What all the other modules do
---
That's everything. Load a video, adjust some settings, click Generate. The software handles the complicated parts.

View File

@ -328,5 +328,4 @@ Happy encoding! 📀
---
*Generated with Claude Code*
*For support, check the comprehensive guides in the project repository*
For technical details on DVD authoring with chapters, see AUTHOR_MODULE.md

View File

@ -962,6 +962,8 @@ type appState struct {
authorChaptersRefresh func() // Refresh hook for chapter list UI
authorDiscSize string // "DVD5" or "DVD9"
authorLogText string
authorLogLines []string // Circular buffer for last N lines
authorLogFilePath string // Path to log file for full viewing
authorLogEntry *widget.Entry
authorLogScroll *container.Scroll
authorProgress float64