From ef1f3d03ac3f117fecd999fc4fa96f557bfa8fe4 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 6 Apr 2026 11:25:37 +0300 Subject: feat: add --video CLI flag to control Veo video prompt after story generation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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// are discovered from the CWD search root. Co-Authored-By: Claude Sonnet 4.6 --- internal/cli/command.go | 2 ++ internal/cli/flags.go | 2 ++ internal/cli/prompts.go | 39 +++++++++++++++++++++++++++++---------- internal/cli/video_runner.go | 6 +++--- 4 files changed, 36 insertions(+), 13 deletions(-) (limited to 'internal/cli') 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//) 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 } -- cgit v1.2.3