diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-08 09:57:25 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-08 09:57:25 +0300 |
| commit | ef1eefce9a1515a17490d6624ef88f2ef41330e1 (patch) | |
| tree | 42c9b979b04f4b2ca3ec9f778e57233171738f76 /internal | |
| parent | f486e9c677c72a409df8696104661847354286df (diff) | |
refactor: slim cmd composition root, move story video flow to internal/video
- Add newProcessor() and newProcessorConfig in processor_config.go; newStoryRunner
and storyUltraRealistic in story.go; main.go focuses on runCommand wiring.
- Move gallery prompts, GenerateSelectedVideos, and RunStoryVideos into
internal/video; cli.GenerateSelectedVideos delegates for GUI compatibility.
- Remove t.Parallel from Veo tests that patch newGenaiClient (race with globals).
Made-with: Cursor
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/cli/video_runner.go | 35 | ||||
| -rw-r--r-- | internal/video/gallery_prompt.go (renamed from internal/cli/prompts.go) | 12 | ||||
| -rw-r--r-- | internal/video/gallery_prompt_test.go (renamed from internal/cli/prompts_test.go) | 2 | ||||
| -rw-r--r-- | internal/video/generate_selected.go | 45 | ||||
| -rw-r--r-- | internal/video/story_run.go | 30 | ||||
| -rw-r--r-- | internal/video/veo.go | 6 | ||||
| -rw-r--r-- | internal/video/veo_test.go | 4 |
7 files changed, 92 insertions, 42 deletions
diff --git a/internal/cli/video_runner.go b/internal/cli/video_runner.go index aab23ae..021a6bf 100644 --- a/internal/cli/video_runner.go +++ b/internal/cli/video_runner.go @@ -1,47 +1,22 @@ package cli import ( - "context" - "fmt" - "codeberg.org/snonux/totalrecall/internal/video" ) // GenerateSelectedVideos is the CLI runner that animates gallery PNG files -// into MP4 clips using Google's Veo model. It processes pages sequentially -// (Veo generation is slow and API quotas make parallelism impractical). +// into MP4 clips using Google's Veo model. It delegates to the video package +// so GUI and tests can keep using the cli entry point without importing video +// directly. // // apiKey is the Google/Gemini API key passed by the caller. // selectedPaths contains the absolute (or relative) paths of the gallery PNGs -// to animate — typically returned by PromptForGalleryVideos. +// to animate — typically returned by video.PromptForGalleryVideos. // // Each page prints a "Generating…" line before the API call and a "Video saved:" // line with the output path on success. The MP4 is written next to its source // PNG so that gallery images and their videos stay in the same directory. // The function stops and returns on the first error so the caller can log it. func GenerateSelectedVideos(apiKey string, selectedPaths []string) error { - if len(selectedPaths) == 0 { - return nil - } - - gen, err := video.NewVeoGenerator(apiKey) - if err != nil { - return fmt.Errorf("cli: initialising Veo generator: %w", err) - } - - ctx := context.Background() - - for _, imgPath := range selectedPaths { - fmt.Printf("Generating video for: %s\n", imgPath) - - // GenerateVideoFromPath applies an operation-level deadline when ctx has none. - mp4Path, err := gen.GenerateVideoFromPath(ctx, imgPath) - if err != nil { - return fmt.Errorf("cli: generating video for %s: %w", imgPath, err) - } - - fmt.Printf("Video saved: %s\n", mp4Path) - } - - return nil + return video.GenerateSelectedVideos(apiKey, selectedPaths) } diff --git a/internal/cli/prompts.go b/internal/video/gallery_prompt.go index d7190c9..013f9b2 100644 --- a/internal/cli/prompts.go +++ b/internal/video/gallery_prompt.go @@ -1,4 +1,4 @@ -package cli +package video import ( "bufio" @@ -95,7 +95,7 @@ func findGalleryPages(outputDir string) ([]int, []string, error) { return nil }) if err != nil { - return nil, nil, fmt.Errorf("cli: walking gallery files in %s: %w", outputDir, err) + return nil, nil, fmt.Errorf("video: walking gallery files in %s: %w", outputDir, err) } sort.Strings(matches) @@ -153,7 +153,7 @@ func askYesNo(prompt string) (bool, error) { reader := bufio.NewReader(os.Stdin) line, err := reader.ReadString('\n') if err != nil { - return false, fmt.Errorf("cli: reading user input: %w", err) + return false, fmt.Errorf("video: reading user input: %w", err) } answer := strings.TrimSpace(strings.ToLower(line)) return answer == "y", nil @@ -172,7 +172,7 @@ func askPageSelection(availablePages []int) ([]int, error) { reader := bufio.NewReader(os.Stdin) line, err := reader.ReadString('\n') if err != nil { - return nil, fmt.Errorf("cli: reading page selection: %w", err) + return nil, fmt.Errorf("video: reading page selection: %w", err) } input := strings.TrimSpace(line) @@ -229,10 +229,10 @@ func parseSelection(input string, max int) ([]int, error) { } n, err := strconv.Atoi(tok) if err != nil { - return nil, fmt.Errorf("cli: invalid page number %q: %w", tok, err) + return nil, fmt.Errorf("video: invalid page number %q: %w", tok, err) } if n <= 0 { - return nil, fmt.Errorf("cli: page numbers must be positive, got %d", n) + return nil, fmt.Errorf("video: page numbers must be positive, got %d", n) } seen[n] = struct{}{} } diff --git a/internal/cli/prompts_test.go b/internal/video/gallery_prompt_test.go index 7eb6344..869d708 100644 --- a/internal/cli/prompts_test.go +++ b/internal/video/gallery_prompt_test.go @@ -1,4 +1,4 @@ -package cli +package video import ( "os" diff --git a/internal/video/generate_selected.go b/internal/video/generate_selected.go new file mode 100644 index 0000000..4407aaa --- /dev/null +++ b/internal/video/generate_selected.go @@ -0,0 +1,45 @@ +package video + +import ( + "context" + "fmt" +) + +// GenerateSelectedVideos animates gallery PNG files into MP4 clips using +// Google's Veo model. It processes pages sequentially (Veo generation is slow +// and API quotas make parallelism impractical). +// +// apiKey is the Google/Gemini API key passed by the caller. +// selectedPaths contains the absolute (or relative) paths of the gallery PNGs +// to animate — typically returned by PromptForGalleryVideos. +// +// Each page prints a "Generating…" line before the API call and a "Video saved:" +// line with the output path on success. The MP4 is written next to its source +// PNG so that gallery images and their videos stay in the same directory. +// The function stops and returns on the first error so the caller can log it. +func GenerateSelectedVideos(apiKey string, selectedPaths []string) error { + if len(selectedPaths) == 0 { + return nil + } + + gen, err := NewVeoGenerator(apiKey) + if err != nil { + return fmt.Errorf("video: initialising Veo generator: %w", err) + } + + ctx := context.Background() + + for _, imgPath := range selectedPaths { + fmt.Printf("Generating video for: %s\n", imgPath) + + // GenerateVideoFromPath applies an operation-level deadline when ctx has none. + mp4Path, err := gen.GenerateVideoFromPath(ctx, imgPath) + if err != nil { + return fmt.Errorf("video: generating video for %s: %w", imgPath, err) + } + + fmt.Printf("Video saved: %s\n", mp4Path) + } + + return nil +} diff --git a/internal/video/story_run.go b/internal/video/story_run.go new file mode 100644 index 0000000..e3c1951 --- /dev/null +++ b/internal/video/story_run.go @@ -0,0 +1,30 @@ +package video + +import ( + "fmt" + "os" +) + +// RunStoryVideos runs the post-story gallery prompt and Veo generation when +// videoEnabled is true. When false, it returns immediately (e.g. --video=false). +// +// Video generation failures are intentionally non-fatal: the comic, PDF, and +// narration are already on disk, so a Veo API error should not invalidate those +// outputs. Errors are printed as warnings and the function returns nil. +func RunStoryVideos(videoEnabled bool, outputDir string, apiKey string) error { + if !videoEnabled { + return nil + } + + selectedPaths, err := PromptForGalleryVideos(outputDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: video prompt failed: %v\n", err) + return nil + } + + if err := GenerateSelectedVideos(apiKey, selectedPaths); err != nil { + fmt.Fprintf(os.Stderr, "Warning: video generation failed: %v\n", err) + } + + return nil +} diff --git a/internal/video/veo.go b/internal/video/veo.go index 05fd0f3..34d0871 100644 --- a/internal/video/veo.go +++ b/internal/video/veo.go @@ -1,6 +1,6 @@ -// Package video provides video generation capabilities using Google's Veo model. -// It reads existing gallery images (comic-style flashcard panels) and animates -// them into short MP4 clips via the Veo API's long-running operation pattern. +// Package video provides Veo-based MP4 generation from comic gallery PNGs, +// interactive gallery page selection after --story runs, and the combined +// post-story flow (RunStoryVideos). package video import ( diff --git a/internal/video/veo_test.go b/internal/video/veo_test.go index 7facd43..ce653e3 100644 --- a/internal/video/veo_test.go +++ b/internal/video/veo_test.go @@ -37,7 +37,7 @@ func TestNewVeoGenerator_WhitespaceKey(t *testing.T) { // TestNewVeoGenerator_ClientInitFailure verifies that a genai client // initialisation error propagates as a wrapped error. func TestNewVeoGenerator_ClientInitFailure(t *testing.T) { - t.Parallel() + // Do not use t.Parallel: this test replaces the package-global newGenaiClient hook. // Temporarily replace the genai client constructor with one that always fails. orig := newGenaiClient @@ -251,7 +251,7 @@ func TestLoadGalleryImage_MultipleMatchesUsesFirst(t *testing.T) { // TestNewVeoGenerator_WithMockClient verifies that NewVeoGenerator succeeds // when the genai client factory does not return an error. func TestNewVeoGenerator_WithMockClient(t *testing.T) { - t.Parallel() + // Do not use t.Parallel: this test replaces the package-global newGenaiClient hook. orig := newGenaiClient newGenaiClient = func(_ context.Context, _ *genai.ClientConfig) (*genai.Client, error) { |
