summaryrefslogtreecommitdiff
path: root/internal/cli
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-06 11:25:37 +0300
committerPaul Buetow <paul@buetow.org>2026-04-06 11:25:37 +0300
commitef1f3d03ac3f117fecd999fc4fa96f557bfa8fe4 (patch)
treef1d3507db5699d86045090e64db99781611c99b7 /internal/cli
parent8f906ac9efc703db4f15c39115394ca8cf01c119 (diff)
feat: add --video CLI flag to control Veo video prompt after story generation
Add VideoEnabled bool (default true) to Flags, register --video bool flag in command.go, export PromptForGalleryVideos/GenerateSelectedVideos, and wire them into main.go via runStoryVideos — skipped when --video=false. Also update findGalleryPages to walk subdirectories recursively so gallery PNGs inside comics/<slug>/ are discovered from the CWD search root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/cli')
-rw-r--r--internal/cli/command.go2
-rw-r--r--internal/cli/flags.go2
-rw-r--r--internal/cli/prompts.go39
-rw-r--r--internal/cli/video_runner.go6
4 files changed, 36 insertions, 13 deletions
diff --git a/internal/cli/command.go b/internal/cli/command.go
index 090b2d1..c9dd889 100644
--- a/internal/cli/command.go
+++ b/internal/cli/command.go
@@ -75,6 +75,8 @@ func setupFlags(cmd *cobra.Command, flags *Flags) {
cmd.Flags().StringVar(&flags.NarratorVoice, "narrator-voice", "",
"Gemini voice for cinematic story narration (default: random from cinematic pool). "+
"Valid values: Charon, Fenrir, Enceladus, Algieba, Aoede, Schedar")
+ cmd.Flags().BoolVar(&flags.VideoEnabled, "video", flags.VideoEnabled,
+ "Prompt to generate Veo videos after comic generation (default true; use --video=false to skip)")
cmd.Flags().BoolVar(&flags.SkipAudio, "skip-audio", false, "Skip audio generation")
cmd.Flags().BoolVar(&flags.SkipImages, "skip-images", false, "Skip image download")
cmd.Flags().BoolVar(&flags.GenerateAnki, "anki", false, "Generate Anki import file (APKG format by default, use --anki-csv for legacy CSV)")
diff --git a/internal/cli/flags.go b/internal/cli/flags.go
index df493a4..678b8ab 100644
--- a/internal/cli/flags.go
+++ b/internal/cli/flags.go
@@ -26,6 +26,7 @@ type Flags struct {
StoryNoUltraRealistic bool // --no-ultra-realistic: disable photorealistic rendering requirement
StorySlug string // --story-slug: force a specific output slug/directory (empty = auto from title)
NarratorVoice string // --narrator-voice: Gemini voice for cinematic narration (empty = random)
+ VideoEnabled bool // --video: whether to prompt for Veo video generation after --story completes
SkipAudio bool
SkipImages bool
GenerateAnki bool
@@ -72,6 +73,7 @@ func NewFlags() *Flags {
AudioFormat: defaults.OutputFormat,
AudioProvider: defaults.Provider,
ImageAPI: "nanobanana",
+ VideoEnabled: true,
DeckName: "Bulgarian Vocabulary",
OpenAIModel: "gpt-4o-mini-tts",
OpenAISpeed: 0.9,
diff --git a/internal/cli/prompts.go b/internal/cli/prompts.go
index ff17767..b43bb5d 100644
--- a/internal/cli/prompts.go
+++ b/internal/cli/prompts.go
@@ -3,6 +3,7 @@ package cli
import (
"bufio"
"fmt"
+ "io/fs"
"os"
"path/filepath"
"sort"
@@ -10,14 +11,14 @@ import (
"strings"
)
-// promptForGalleryVideos lists all *_gallery_*.png files found in outputDir,
-// shows them to the user, and asks whether they want to generate videos.
-// If the user agrees, it asks which pages to generate (e.g. "1,3,5" or "all")
-// and returns the parsed list of page numbers.
+// PromptForGalleryVideos lists all *_gallery_*.png files found under outputDir
+// (searching recursively), shows them to the user, and asks whether they want
+// to generate videos. If the user agrees, it asks which pages to generate
+// (e.g. "1,3,5" or "all") and returns the parsed list of page numbers.
//
// Returns an empty slice when the user declines or enters nothing.
// Returns an error only on unexpected I/O or parse failures.
-func promptForGalleryVideos(outputDir string) ([]int, error) {
+func PromptForGalleryVideos(outputDir string) ([]int, error) {
pages, pngPaths, err := findGalleryPages(outputDir)
if err != nil {
return nil, err
@@ -41,13 +42,31 @@ func promptForGalleryVideos(outputDir string) ([]int, error) {
return askPageSelection(pages)
}
-// findGalleryPages globs for *_gallery_*.png files in outputDir and returns
-// the sorted list of page numbers and matching file paths.
+// findGalleryPages walks outputDir recursively looking for *_gallery_*.png
+// files and returns the sorted list of unique page numbers and matching paths.
+// Walking recursively is necessary because the story runner places gallery
+// images in a per-comic subdirectory (comics/<slug>/) rather than directly
+// in the top-level output directory.
func findGalleryPages(outputDir string) ([]int, []string, error) {
- pattern := filepath.Join(outputDir, "*_gallery_*.png")
- matches, err := filepath.Glob(pattern)
+ var matches []string
+
+ err := filepath.WalkDir(outputDir, func(path string, d fs.DirEntry, walkErr error) error {
+ if walkErr != nil {
+ // Skip unreadable directories rather than aborting the whole walk.
+ return nil
+ }
+ if d.IsDir() {
+ return nil
+ }
+ base := filepath.Base(path)
+ // Match files that follow the *_gallery_N.png naming convention.
+ if strings.Contains(base, "_gallery_") && strings.HasSuffix(base, ".png") {
+ matches = append(matches, path)
+ }
+ return nil
+ })
if err != nil {
- return nil, nil, fmt.Errorf("cli: glob gallery files in %s: %w", outputDir, err)
+ return nil, nil, fmt.Errorf("cli: walking gallery files in %s: %w", outputDir, err)
}
sort.Strings(matches)
diff --git a/internal/cli/video_runner.go b/internal/cli/video_runner.go
index 40a1fdd..2679a26 100644
--- a/internal/cli/video_runner.go
+++ b/internal/cli/video_runner.go
@@ -7,19 +7,19 @@ import (
"codeberg.org/snonux/totalrecall/internal/video"
)
-// generateSelectedVideos is the CLI runner that animates gallery PNG files
+// 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).
//
// apiKey is the Google/Gemini API key passed by the caller.
-// selected is the list of gallery page numbers to process (from promptForGalleryVideos).
+// selected is the list of gallery page numbers to process (from PromptForGalleryVideos).
// outputDir is both the directory that contains the gallery PNGs and the
// destination for the resulting MP4 files (written next to the PNGs).
//
// Each page prints a "Generating…" line before the API call and a "Video saved:"
// line with the output path on success. The function stops and returns on the
// first error so callers can log it without silently skipping pages.
-func generateSelectedVideos(apiKey string, selected []int, outputDir string) error {
+func GenerateSelectedVideos(apiKey string, selected []int, outputDir string) error {
if len(selected) == 0 {
return nil
}