Refactor web build process and update documentation

- Removed Node.js build artifacts from .gitignore and adjusted Makefile to reflect changes in web UI build process, now using server-rendered Go templates instead of React.
- Updated README to clarify the new web UI architecture and output formats, emphasizing the removal of the Node.js build step.
- Added a command to set the number of frames per render task in manager configuration, enhancing user control over rendering settings.
- Improved Gitea workflow by removing unnecessary npm install step, streamlining the CI process.
This commit is contained in:
2026-03-12 19:44:40 -05:00
parent d3c5ee0dba
commit 2deb47e5ad
78 changed files with 3895 additions and 12499 deletions

View File

@@ -12,6 +12,7 @@ import (
"regexp"
"sort"
"strings"
"sync"
"jiggablend/internal/runner/encoding"
)
@@ -26,6 +27,10 @@ func NewEncodeProcessor() *EncodeProcessor {
// Process executes an encode task.
func (p *EncodeProcessor) Process(ctx *Context) error {
if err := ctx.CheckCancelled(); err != nil {
return err
}
ctx.Info(fmt.Sprintf("Starting encode task: job %d", ctx.JobID))
log.Printf("Processing encode task %d for job %d", ctx.TaskID, ctx.JobID)
@@ -64,23 +69,18 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
ctx.Info(fmt.Sprintf("File: %s (type: %s, size: %d)", file.FileName, file.FileType, file.FileSize))
}
// Determine source format based on output format
sourceFormat := "exr"
// Encode from EXR frames only
fileExt := ".exr"
// Find and deduplicate frame files (EXR or PNG)
frameFileSet := make(map[string]bool)
var frameFilesList []string
for _, file := range files {
if file.FileType == "output" && strings.HasSuffix(strings.ToLower(file.FileName), fileExt) {
// Deduplicate by filename
if !frameFileSet[file.FileName] {
frameFileSet[file.FileName] = true
frameFilesList = append(frameFilesList, file.FileName)
}
}
}
if len(frameFilesList) == 0 {
// Log why no files matched (deduplicate for error reporting)
outputFileSet := make(map[string]bool)
@@ -103,37 +103,61 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
}
}
}
ctx.Error(fmt.Sprintf("no %s frame files found for encode: found %d total files, %d unique output files, %d unique %s files (with other types)", strings.ToUpper(fileExt[1:]), len(files), len(outputFiles), len(frameFilesOtherType), strings.ToUpper(fileExt[1:])))
ctx.Error(fmt.Sprintf("no EXR frame files found for encode: found %d total files, %d unique output files, %d unique EXR files (with other types)", len(files), len(outputFiles), len(frameFilesOtherType)))
if len(outputFiles) > 0 {
ctx.Error(fmt.Sprintf("Output files found: %v", outputFiles))
}
if len(frameFilesOtherType) > 0 {
ctx.Error(fmt.Sprintf("%s files with wrong type: %v", strings.ToUpper(fileExt[1:]), frameFilesOtherType))
ctx.Error(fmt.Sprintf("EXR files with wrong type: %v", frameFilesOtherType))
}
err := fmt.Errorf("no %s frame files found for encode", strings.ToUpper(fileExt[1:]))
err := fmt.Errorf("no EXR frame files found for encode")
return err
}
ctx.Info(fmt.Sprintf("Found %d %s frames for encode", len(frameFilesList), strings.ToUpper(fileExt[1:])))
ctx.Info(fmt.Sprintf("Found %d EXR frames for encode", len(frameFilesList)))
// Download frames
ctx.Info(fmt.Sprintf("Downloading %d %s frames for encode...", len(frameFilesList), strings.ToUpper(fileExt[1:])))
// Download frames with bounded parallelism (8 concurrent downloads)
const downloadWorkers = 8
ctx.Info(fmt.Sprintf("Downloading %d EXR frames for encode...", len(frameFilesList)))
type result struct {
path string
err error
}
results := make([]result, len(frameFilesList))
var wg sync.WaitGroup
sem := make(chan struct{}, downloadWorkers)
for i, fileName := range frameFilesList {
wg.Add(1)
go func(i int, fileName string) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
framePath := filepath.Join(workDir, fileName)
err := ctx.Manager.DownloadFrame(ctx.JobID, fileName, framePath)
if err != nil {
ctx.Error(fmt.Sprintf("Failed to download EXR frame %s: %v", fileName, err))
log.Printf("Failed to download EXR frame for encode %s: %v", fileName, err)
results[i] = result{"", err}
return
}
results[i] = result{framePath, nil}
}(i, fileName)
}
wg.Wait()
var frameFiles []string
for i, fileName := range frameFilesList {
ctx.Info(fmt.Sprintf("Downloading frame %d/%d: %s", i+1, len(frameFilesList), fileName))
framePath := filepath.Join(workDir, fileName)
if err := ctx.Manager.DownloadFrame(ctx.JobID, fileName, framePath); err != nil {
ctx.Error(fmt.Sprintf("Failed to download %s frame %s: %v", strings.ToUpper(fileExt[1:]), fileName, err))
log.Printf("Failed to download %s frame for encode %s: %v", strings.ToUpper(fileExt[1:]), fileName, err)
continue
for _, r := range results {
if r.err == nil && r.path != "" {
frameFiles = append(frameFiles, r.path)
}
ctx.Info(fmt.Sprintf("Successfully downloaded frame %d/%d: %s", i+1, len(frameFilesList), fileName))
frameFiles = append(frameFiles, framePath)
}
if err := ctx.CheckCancelled(); err != nil {
return err
}
if len(frameFiles) == 0 {
err := fmt.Errorf("failed to download any %s frames for encode", strings.ToUpper(fileExt[1:]))
err := fmt.Errorf("failed to download any EXR frames for encode")
ctx.Error(err.Error())
return err
}
@@ -141,11 +165,9 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
sort.Strings(frameFiles)
ctx.Info(fmt.Sprintf("Downloaded %d frames", len(frameFiles)))
// Check if EXR files have alpha channel and HDR content (only for EXR source format)
// Check if EXR files have alpha channel (for encode decision)
hasAlpha := false
hasHDR := false
if sourceFormat == "exr" {
// Check first frame for alpha channel and HDR using ffprobe
{
firstFrame := frameFiles[0]
hasAlpha = detectAlphaChannel(ctx, firstFrame)
if hasAlpha {
@@ -153,45 +175,28 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
} else {
ctx.Info("No alpha channel detected in EXR files")
}
hasHDR = detectHDR(ctx, firstFrame)
if hasHDR {
ctx.Info("Detected HDR content in EXR files")
} else {
ctx.Info("No HDR content detected in EXR files (SDR range)")
}
}
// Generate video
// Use alpha if:
// 1. User explicitly enabled it OR source has alpha channel AND
// 2. Codec supports alpha (AV1 or VP9)
preserveAlpha := ctx.ShouldPreserveAlpha()
useAlpha := (preserveAlpha || hasAlpha) && (outputFormat == "EXR_AV1_MP4" || outputFormat == "EXR_VP9_WEBM")
if (preserveAlpha || hasAlpha) && outputFormat == "EXR_264_MP4" {
ctx.Warn("Alpha channel requested/detected but H.264 does not support alpha. Consider using EXR_AV1_MP4 or EXR_VP9_WEBM to preserve alpha.")
}
if preserveAlpha && !hasAlpha {
ctx.Warn("Alpha preservation requested but no alpha channel detected in EXR files.")
// Use alpha when source EXR has alpha and codec supports it (AV1 or VP9). H.264 does not support alpha.
useAlpha := hasAlpha && (outputFormat == "EXR_AV1_MP4" || outputFormat == "EXR_VP9_WEBM")
if hasAlpha && outputFormat == "EXR_264_MP4" {
ctx.Warn("Alpha channel detected in EXR but H.264 does not support alpha. Use EXR_AV1_MP4 or EXR_VP9_WEBM to preserve alpha in video.")
}
if useAlpha {
if preserveAlpha && hasAlpha {
ctx.Info("Alpha preservation enabled: Using alpha channel encoding")
} else if hasAlpha {
ctx.Info("Alpha channel detected - automatically enabling alpha encoding")
}
ctx.Info("Alpha channel detected - encoding with alpha (AV1/VP9)")
}
var outputExt string
switch outputFormat {
case "EXR_VP9_WEBM":
outputExt = "webm"
ctx.Info("Encoding WebM video with VP9 codec (with alpha channel and HDR support)...")
ctx.Info("Encoding WebM video with VP9 codec (alpha, HDR)...")
case "EXR_AV1_MP4":
outputExt = "mp4"
ctx.Info("Encoding MP4 video with AV1 codec (with alpha channel)...")
ctx.Info("Encoding MP4 video with AV1 codec (alpha, HDR)...")
default:
outputExt = "mp4"
ctx.Info("Encoding MP4 video with H.264 codec...")
ctx.Info("Encoding MP4 video with H.264 codec (HDR, HLG)...")
}
outputVideo := filepath.Join(workDir, fmt.Sprintf("output_%d.%s", ctx.JobID, outputExt))
@@ -231,11 +236,6 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
// Pass 1
ctx.Info("Pass 1/2: Analyzing content for optimal encode...")
softEncoder := encoder.(*encoding.SoftwareEncoder)
// Use HDR if: user explicitly enabled it OR HDR content was detected
preserveHDR := (ctx.ShouldPreserveHDR() || hasHDR) && sourceFormat == "exr"
if hasHDR && !ctx.ShouldPreserveHDR() {
ctx.Info("HDR content detected - automatically enabling HDR preservation")
}
pass1Cmd := softEncoder.BuildPass1Command(&encoding.EncodeConfig{
InputPattern: patternPath,
OutputPath: outputVideo,
@@ -244,8 +244,6 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
WorkDir: workDir,
UseAlpha: useAlpha,
TwoPass: true,
SourceFormat: sourceFormat,
PreserveHDR: preserveHDR,
})
if err := pass1Cmd.Run(); err != nil {
ctx.Warn(fmt.Sprintf("Pass 1 completed (warnings expected): %v", err))
@@ -254,15 +252,6 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
// Pass 2
ctx.Info("Pass 2/2: Encoding with optimal quality...")
preserveHDR = (ctx.ShouldPreserveHDR() || hasHDR) && sourceFormat == "exr"
if preserveHDR {
if hasHDR && !ctx.ShouldPreserveHDR() {
ctx.Info("HDR preservation enabled (auto-detected): Using HLG transfer with bt709 primaries")
} else {
ctx.Info("HDR preservation enabled: Using HLG transfer with bt709 primaries")
}
}
config := &encoding.EncodeConfig{
InputPattern: patternPath,
OutputPath: outputVideo,
@@ -271,8 +260,6 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
WorkDir: workDir,
UseAlpha: useAlpha,
TwoPass: true, // Software encoding always uses 2-pass for quality
SourceFormat: sourceFormat,
PreserveHDR: preserveHDR,
}
cmd := encoder.BuildCommand(config)
@@ -294,6 +281,8 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start encode command: %w", err)
}
stopMonitor := ctx.StartCancellationMonitor(cmd, "encode")
defer stopMonitor()
ctx.Processes.Track(ctx.TaskID, cmd)
defer ctx.Processes.Untrack(ctx.TaskID)
@@ -329,6 +318,9 @@ func (p *EncodeProcessor) Process(ctx *Context) error {
<-stderrDone
if err != nil {
if cancelled, checkErr := ctx.IsJobCancelled(); checkErr == nil && cancelled {
return ErrJobCancelled
}
var errMsg string
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 137 {