summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-06 16:15:53 +0300
committerPaul Buetow <paul@buetow.org>2026-04-06 16:15:53 +0300
commitdf87a6af7bf1beea8dcc8bbeb5f299025cb2e8f4 (patch)
treeb21627409c7778a6e70a60f3f2b312f68df44a47
parent85fd2a50049ae9e7efad6ef0b45d3f2a754fe340 (diff)
feat: --narrate opt-in narration, panel variety mandate, v0.23.0v0.23.0
- Add --narrate flag (default false); narration is now opt-in to save TTS quota - Add VARIETY MANDATE to story page prompts: each panel must differ in camera angle, pose, location, lighting, and foreground — prevents repeated compositions - Wire NarrateEnabled through RunnerConfig and main.go - Update README: --narrate flag, updated output file table, narrator voice examples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--README.md29
-rw-r--r--cmd/totalrecall/main.go1
-rw-r--r--internal/cli/command.go3
-rw-r--r--internal/cli/flags.go1
-rw-r--r--internal/story/artist.go4
-rw-r--r--internal/story/runner.go9
-rw-r--r--internal/version.go2
7 files changed, 37 insertions, 12 deletions
diff --git a/README.md b/README.md
index d53777e..304a5d3 100644
--- a/README.md
+++ b/README.md
@@ -46,8 +46,9 @@ It has mainly been vibe coded using Claude Code CLI.
- Art style override: `--story-style` replaces the random pick for both modes
- **Iterative character consistency**: each page is generated with the cover + previous page as pixel references so characters stay visually consistent
- **Character bible**: Gemini generates a detailed visual guide (age, clothing, colours) used in every prompt
- - **Cinematic narration** via Gemini TTS (`<slug>_narration.mp3`) — Bulgarian phonology, dramatic pacing, random voice from a curated pool; override with `--narrator-voice`
- - Intro teaser (15 s, different voice) + main story chunks (~100 words each) + epilogue with ambient music
+ - **Cinematic narration** via Gemini TTS (opt-in with `--narrate`; off by default to save quota)
+ - Output: `<slug>_narration.mp3` — Bulgarian phonology, dramatic pacing, random voice from a curated pool
+ - Override voice with `--narrator-voice`; intro teaser + main story chunks (~100 words each) + epilogue
- Falls back to `<slug>_tts_todo.txt` if narration fails
- **Vocabulary learning file** (`<slug>_comic_vocabulary.txt`) — word list with translations + full story text
- **Theme file** (`<slug>_theme.txt`) — records the `--story-theme` used for easy reproduction
@@ -189,10 +190,10 @@ Key features:
- `<slug>_gallery_1.png` … `<slug>_gallery_5.png` — five close-up character gallery pages
- `<slug>_back.png` — back cover with blurb
- `<slug>.pdf` — all pages assembled into a single PDF
- - `<slug>_narration.mp3` — cinematic Gemini TTS narration with intro, story chunks, and epilogue
+ - `<slug>_narration.mp3` — cinematic Gemini TTS narration (only when `--narrate` is passed)
+ - `<slug>_tts_todo.txt` — written if `--narrate` was given but narration failed
- `<slug>_comic_vocabulary.txt` — vocabulary words + full story text for learning
- `<slug>_theme.txt` — records `--story-theme` for easy reproduction
- - `<slug>_tts_todo.txt` — written instead of MP3 only if narration fails
Customise the story:
```bash
@@ -206,13 +207,19 @@ Key features:
# Force standard comic style (default is random 50/50 between ultra-realistic and standard)
totalrecall --story words.txt --no-ultra-realistic
- # Choose narrator voice (default: random from pool)
- totalrecall --story words.txt --narrator-voice Charon # deep, authoritative
- totalrecall --story words.txt --narrator-voice Fenrir # strong, resonant
- totalrecall --story words.txt --narrator-voice Enceladus # breathy, intimate
- totalrecall --story words.txt --narrator-voice Algieba # smooth, warm
- totalrecall --story words.txt --narrator-voice Aoede # breezy, expressive
- totalrecall --story words.txt --narrator-voice Schedar # steady, grounded
+ # Enable cinematic narration (default: off — use --narrate to opt in)
+ totalrecall --story words.txt --narrate
+
+ # Choose narrator voice (default: random from pool; only relevant with --narrate)
+ totalrecall --story words.txt --narrate --narrator-voice Charon # deep, authoritative
+ totalrecall --story words.txt --narrate --narrator-voice Fenrir # strong, resonant
+ totalrecall --story words.txt --narrate --narrator-voice Enceladus # breathy, intimate
+ totalrecall --story words.txt --narrate --narrator-voice Algieba # smooth, warm
+ totalrecall --story words.txt --narrate --narrator-voice Aoede # breezy, expressive
+ totalrecall --story words.txt --narrate --narrator-voice Schedar # steady, grounded
+
+ # Repair a partial run (skip existing pages, regenerate missing ones)
+ totalrecall --story words.txt --story-slug my-comic-slug
```
#### Optional Gallery Videos (Veo)
diff --git a/cmd/totalrecall/main.go b/cmd/totalrecall/main.go
index 13fa1bd..c991378 100644
--- a/cmd/totalrecall/main.go
+++ b/cmd/totalrecall/main.go
@@ -115,6 +115,7 @@ func runCommand(cmd *cobra.Command, args []string, flags *cli.Flags) error {
Theme: flags.StoryTheme,
UltraRealistic: storyUltraRealistic(flags.StoryNoUltraRealistic),
NarratorVoice: flags.NarratorVoice,
+ NarrateEnabled: flags.NarrateEnabled,
Slug: flags.StorySlug,
})
if err := runner.Run(flags.StoryFile); err != nil {
diff --git a/internal/cli/command.go b/internal/cli/command.go
index c9dd889..ad62272 100644
--- a/internal/cli/command.go
+++ b/internal/cli/command.go
@@ -75,6 +75,9 @@ 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.NarrateEnabled, "narrate", false,
+ "Generate a cinematic MP3 narration of the story after --story completes (default false). "+
+ "Requires GOOGLE_API_KEY. Use --narrator-voice to pick a specific voice.")
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")
diff --git a/internal/cli/flags.go b/internal/cli/flags.go
index 678b8ab..7e1cdbc 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)
+ NarrateEnabled bool // --narrate: generate cinematic MP3 narration after --story (default false)
VideoEnabled bool // --video: whether to prompt for Veo video generation after --story completes
SkipAudio bool
SkipImages bool
diff --git a/internal/story/artist.go b/internal/story/artist.go
index 3e6b191..e3a0fb6 100644
--- a/internal/story/artist.go
+++ b/internal/story/artist.go
@@ -550,6 +550,10 @@ func buildStoryPagePrompt(section string, pageNum, totalPages int, style, bible
"Each panel is separated by a thin black gutter line. "+
"All 4 panels must be clearly distinct scenes — NOT one continuous image. "+
"The full image area must be covered by the 4 panels with no empty space.\n"+
+ "VARIETY MANDATE — every panel MUST differ from the others in at least 3 of these dimensions: "+
+ "camera angle (close-up, medium shot, wide shot, bird's-eye, low angle), "+
+ "character pose or action, location or background detail, lighting or time-of-day, "+
+ "and foreground objects. Repeating the same angle or composition across panels is FORBIDDEN.\n"+
renderReq+
"STRICT CONSISTENCY RULES — apply to every single panel:\n"+
" • Human characters: identical face, AGE APPEARANCE, hair colour/style, and clothing "+
diff --git a/internal/story/runner.go b/internal/story/runner.go
index 3399bb4..e5e9aa5 100644
--- a/internal/story/runner.go
+++ b/internal/story/runner.go
@@ -50,6 +50,9 @@ type RunnerConfig struct {
// uses this value directly. Combine with loadOrGenerate's skip-existing logic
// to repair a partial comic run without regenerating already-present pages.
Slug string
+ // NarrateEnabled controls whether a cinematic MP3 narration is generated
+ // after comic creation. Defaults to false — narration is opt-in via --narrate.
+ NarrateEnabled bool
}
// Runner orchestrates the full pipeline: text → image → narration.
@@ -172,6 +175,12 @@ func (r *Runner) Run(batchFile string) error {
r.drawComicPages(result.StoryText, result.Bible, slug, entries)
+ // Narration is opt-in (--narrate flag). Skip entirely when not requested
+ // so runs complete faster and don't consume TTS quota unnecessarily.
+ if r.config != nil && !r.config.NarrateEnabled {
+ fmt.Println("Narration skipped (use --narrate to enable).")
+ return nil
+ }
return r.handleNarration(result.StoryText, slug, comicsDir)
}
diff --git a/internal/version.go b/internal/version.go
index 70d8b6e..51e50c3 100644
--- a/internal/version.go
+++ b/internal/version.go
@@ -1,3 +1,3 @@
package internal
-const Version = "0.22.0"
+const Version = "0.23.0"