summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-08 09:57:25 +0300
committerPaul Buetow <paul@buetow.org>2026-04-08 09:57:25 +0300
commitef1eefce9a1515a17490d6624ef88f2ef41330e1 (patch)
tree42c9b979b04f4b2ca3ec9f778e57233171738f76 /internal
parentf486e9c677c72a409df8696104661847354286df (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.go35
-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.go45
-rw-r--r--internal/video/story_run.go30
-rw-r--r--internal/video/veo.go6
-rw-r--r--internal/video/veo_test.go4
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) {