summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-03 23:36:30 +0300
committerPaul Buetow <paul@buetow.org>2026-04-03 23:36:30 +0300
commit3b26916eed21c5c4c3d2ef40f19a1ef6e47b5662 (patch)
tree9ec98bde2124a7f10f79736d319d2b205e61934f
parent621ef41e416bdc9985a27e9a2f923435676f096e (diff)
fix
-rw-r--r--.gitignore1
-rw-r--r--comic_back_attribution.txt16
-rw-r--r--comic_cover_attribution.txt16
-rw-r--r--comic_page_1_attribution.txt16
-rw-r--r--comic_page_2_attribution.txt16
-rw-r--r--comic_page_3_attribution.txt16
-rw-r--r--comic_strip_attribution.txt15
-rw-r--r--internal/story/artist.go63
-rw-r--r--internal/story/generator.go4
-rw-r--r--internal/story/narrator.go60
-rw-r--r--internal/story/runner.go11
11 files changed, 109 insertions, 125 deletions
diff --git a/.gitignore b/.gitignore
index 0905820..92e1076 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
# Binaries for programs and plugins
+/comics
*.exe
*.exe~
*.dll
diff --git a/comic_back_attribution.txt b/comic_back_attribution.txt
deleted file mode 100644
index fd16f28..0000000
--- a/comic_back_attribution.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-Image generated by Google Gemini Nano Banana
-
-Model: gemini-3.1-flash-image-preview
-Text model: gemini-2.5-flash
-Aspect ratio: 4:3
-Size: 896x1200
-Result: Generated educational image for vocabulary story
-
-Prompt used:
-ЗАДЪЛЖИТЕЛНО / MANDATORY LANGUAGE RULE: This is a BULGARIAN comic book. All visible text (blurb box, labels, banners) MUST be in Bulgarian Cyrillic script. English text anywhere on the back cover is STRICTLY FORBIDDEN.
-
-Art style: ultra realistic comic strip with photographic detail and dramatic lighting.
-CHARACTER & SETTING REFERENCE (back cover — follow exactly, do NOT change clothing):
-Captain Elara, a middle-aged woman (early 40s), has dark brown hair tied back in a neat, practical ponytail, with a few loose strands framing her face. Her eyes are sharp, intelligent hazel, and her skin tone is light olive, slightly tanned. She possesses an athletic, lean, and agile build. Elara wears a functional dark grey (almost black) fitted spacesuit with subtle dark blue piping along the seams and collar. The suit features a high stand-up collar, long sleeves, and zips up the front with the zipper visible. It has no visible pockets, patches, or emblems. Her footwear consists of...
-
-Generated at: 2026-04-03 22:39:29
diff --git a/comic_cover_attribution.txt b/comic_cover_attribution.txt
deleted file mode 100644
index c898574..0000000
--- a/comic_cover_attribution.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-Image generated by Google Gemini Nano Banana
-
-Model: gemini-3.1-flash-image-preview
-Text model: gemini-2.5-flash
-Aspect ratio: 4:3
-Size: 896x1200
-Result: Generated educational image for vocabulary story
-
-Prompt used:
-ЗАДЪЛЖИТЕЛНО / MANDATORY LANGUAGE RULE: This is a BULGARIAN comic book. All text on the cover (cover lines, banners, labels) MUST be in Bulgarian Cyrillic script. The masthead title must also be rendered in a striking comic-book font.
-
-Art style: ultra realistic comic strip with photographic detail and dramatic lighting.
-CHARACTER & SETTING REFERENCE (cover — follow exactly, do NOT change clothing):
-Captain Elara, a middle-aged woman (early 40s), has dark brown hair tied back in a neat, practical ponytail, with a few loose strands framing her face. Her eyes are sharp, intelligent hazel, and her skin tone is light olive, slightly tanned. She possesses an athletic, lean, and agile build. Elara wears a functional dark grey (almost black) fitted spacesuit with subtle dark blue piping along the seams and collar. The suit features a high stand-up collar, long sleeves, and zips up the front with the zipper visible. It has no visible pockets, patches, or emblems. Her footwear ...
-
-Generated at: 2026-04-03 22:38:20
diff --git a/comic_page_1_attribution.txt b/comic_page_1_attribution.txt
deleted file mode 100644
index b75b9e0..0000000
--- a/comic_page_1_attribution.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-Image generated by Google Gemini Nano Banana
-
-Model: gemini-3.1-flash-image-preview
-Text model: gemini-2.5-flash
-Aspect ratio: 4:3
-Size: 896x1200
-Result: Generated educational image for vocabulary story
-
-Prompt used:
-ЗАДЪЛЖИТЕЛНО / MANDATORY LANGUAGE RULE: This is a BULGARIAN comic book. Every word of text inside speech bubbles, thought bubbles, caption boxes, and panel labels MUST be written in Bulgarian Cyrillic script (например: Здравей! Какво правиш? Побързай!). English text anywhere in the panels is STRICTLY FORBIDDEN — use ONLY Bulgarian.
-
-Art style: ultra realistic comic strip with photographic detail and dramatic lighting.
-CHARACTER & SETTING REFERENCE (story page 1 of 3 — follow exactly, do NOT change clothing):
-Captain Elara, a middle-aged woman (early 40s), has dark brown hair tied back in a neat, practical ponytail, with a few loose strands framing her face. Her eyes are sharp, intelligent hazel, and her skin tone is light olive, slightly tanned. She possesses an athletic, lean, and agile build. Elara wears a functional dark grey (almost black) fitted spacesuit with subtle dark blue piping along the seams and collar. The suit features...
-
-Generated at: 2026-04-03 22:38:37
diff --git a/comic_page_2_attribution.txt b/comic_page_2_attribution.txt
deleted file mode 100644
index 6bb716c..0000000
--- a/comic_page_2_attribution.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-Image generated by Google Gemini Nano Banana
-
-Model: gemini-3.1-flash-image-preview
-Text model: gemini-2.5-flash
-Aspect ratio: 4:3
-Size: 896x1200
-Result: Generated educational image for vocabulary story
-
-Prompt used:
-ЗАДЪЛЖИТЕЛНО / MANDATORY LANGUAGE RULE: This is a BULGARIAN comic book. Every word of text inside speech bubbles, thought bubbles, caption boxes, and panel labels MUST be written in Bulgarian Cyrillic script (например: Здравей! Какво правиш? Побързай!). English text anywhere in the panels is STRICTLY FORBIDDEN — use ONLY Bulgarian.
-
-Art style: ultra realistic comic strip with photographic detail and dramatic lighting.
-CHARACTER & SETTING REFERENCE (story page 2 of 3 — follow exactly, do NOT change clothing):
-Captain Elara, a middle-aged woman (early 40s), has dark brown hair tied back in a neat, practical ponytail, with a few loose strands framing her face. Her eyes are sharp, intelligent hazel, and her skin tone is light olive, slightly tanned. She possesses an athletic, lean, and agile build. Elara wears a functional dark grey (almost black) fitted spacesuit with subtle dark blue piping along the seams and collar. The suit features...
-
-Generated at: 2026-04-03 22:38:50
diff --git a/comic_page_3_attribution.txt b/comic_page_3_attribution.txt
deleted file mode 100644
index b3d7ef0..0000000
--- a/comic_page_3_attribution.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-Image generated by Google Gemini Nano Banana
-
-Model: gemini-3.1-flash-image-preview
-Text model: gemini-2.5-flash
-Aspect ratio: 4:3
-Size: 896x1200
-Result: Generated educational image for vocabulary story
-
-Prompt used:
-ЗАДЪЛЖИТЕЛНО / MANDATORY LANGUAGE RULE: This is a BULGARIAN comic book. Every word of text inside speech bubbles, thought bubbles, caption boxes, and panel labels MUST be written in Bulgarian Cyrillic script (например: Здравей! Какво правиш? Побързай!). English text anywhere in the panels is STRICTLY FORBIDDEN — use ONLY Bulgarian.
-
-Art style: ultra realistic comic strip with photographic detail and dramatic lighting.
-CHARACTER & SETTING REFERENCE (story page 3 of 3 — follow exactly, do NOT change clothing):
-Captain Elara, a middle-aged woman (early 40s), has dark brown hair tied back in a neat, practical ponytail, with a few loose strands framing her face. Her eyes are sharp, intelligent hazel, and her skin tone is light olive, slightly tanned. She possesses an athletic, lean, and agile build. Elara wears a functional dark grey (almost black) fitted spacesuit with subtle dark blue piping along the seams and collar. The suit features...
-
-Generated at: 2026-04-03 22:39:11
diff --git a/comic_strip_attribution.txt b/comic_strip_attribution.txt
deleted file mode 100644
index a5c09a5..0000000
--- a/comic_strip_attribution.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-Image generated by Google Gemini Nano Banana
-
-Model: gemini-3.1-flash-image-preview
-Text model: gemini-2.5-flash
-Aspect ratio: 4:3
-Size: 768x1376
-Result: Generated educational image for vocabulary story
-
-Prompt used:
-Art style: ultra realistic comic strip with photographic detail and dramatic lighting.
-A single tall comic strip with 3 vertically stacked panels showing the beginning, middle, and end of the story. Keep all characters visually consistent across panels. Scene based on this Bulgarian vocabulary story:
-
-Всяка сутрин в къщата на баба Елица се събуждаше с едно приятно гъделичкане. Не беше слънцето, което я будеше, а нейната любима 1. котка, Мурка. Мурка обичаше да се катери по завивките, да мърка силно и да бута с глава ръката на Елица, докато момиченцето не отвори очи. "Добро утро, Мурка," прошепна Елица, протягайки се с усмивка. 1. Котето се изви грациозно, разтегна лапи и скочи от легл...
-
-Generated at: 2026-04-03 13:28:31
diff --git a/internal/story/artist.go b/internal/story/artist.go
index e40bb6a..8e7e5c7 100644
--- a/internal/story/artist.go
+++ b/internal/story/artist.go
@@ -10,11 +10,13 @@ import (
"google.golang.org/genai"
+ "codeberg.org/snonux/totalrecall/internal/batch"
"codeberg.org/snonux/totalrecall/internal/image"
)
const (
- // storyPageCount is the number of 9-panel story pages (excluding cover/back).
+ // storyPageCount is the number of story pages (excluding cover/back).
+ // Each page uses a 2×2 grid of 4 panels in landscape (16:9) format.
storyPageCount = 3
// comicPageAspectRatio: 16:9 is the closest supported widescreen ratio for
@@ -134,15 +136,17 @@ func NewArtist(config *ArtistConfig) *Artist {
// DrawComicPages generates 5 images total:
// - <titleSlug>_cover.png — full-bleed cover
-// - <titleSlug>_page_1.png … _3 — 9-panel (3×3) story pages
+// - <titleSlug>_page_1.png … _3 — 4-panel (2×2 grid) landscape story pages
// - <titleSlug>_back.png — back cover
//
// A character bible injected into every prompt keeps characters, clothing, and
// setting consistent across all pages. The bible is produced by GenerateFull in
// the same Gemini call as the story; prebuiltBible is passed in from there.
+// entries are the vocabulary words from input.txt — they are injected into every
+// story page prompt so the image model visually features and labels them in panels.
// titleSlug is used as the file-name prefix; it must already be a safe slug.
// Returns the list of saved image paths in order.
-func (a *Artist) DrawComicPages(storyText, prebuiltBible, titleSlug string) ([]string, error) {
+func (a *Artist) DrawComicPages(storyText, prebuiltBible, titleSlug string, entries []batch.WordEntry) ([]string, error) {
style := a.style
if style == "" {
style = pickStyle()
@@ -168,13 +172,13 @@ func (a *Artist) DrawComicPages(storyText, prebuiltBible, titleSlug string) ([]s
recentRefs = appendRef(recentRefs, coverBytes) // cover becomes the anchor reference
}
- // 2. Story pages (9-panel grids) — each page receives cover + previous page as refs.
+ // 2. Story pages (7-panel landscape: 3+4 rows) — each receives cover + previous page as refs.
sections := splitIntoSections(storyText, storyPageCount)
for i, section := range sections {
pageNum := i + 1
fmt.Printf(" Generating story page %d/%d...\n", pageNum, storyPageCount)
p, pageBytes, err := a.generateSinglePage(
- buildStoryPagePrompt(section, pageNum, storyPageCount, style, bible),
+ buildStoryPagePrompt(section, pageNum, storyPageCount, style, bible, entries),
fmt.Sprintf("%s_page_%d", titleSlug, pageNum),
recentRefs,
)
@@ -356,11 +360,13 @@ func buildCoverPrompt(storyText, style, bible string) string {
)
}
-// buildStoryPagePrompt constructs a 9-panel grid page prompt.
-// The Bulgarian language requirement is placed at the very top — before the
-// character reference and story excerpt — because image models tend to follow
-// early instructions more reliably than late ones buried in a list.
-func buildStoryPagePrompt(section string, pageNum, totalPages int, style, bible string) string {
+// buildStoryPagePrompt constructs a landscape comic page prompt.
+// Layout uses a 2×2 grid of 4 panels optimised for the 16:9 aspect ratio.
+// entries are injected as a vocabulary block so the image model features and
+// labels each word visually inside the panels — making each page a learning tool.
+// The Bulgarian language requirement is placed at the very top so it is processed
+// before all other instructions.
+func buildStoryPagePrompt(section string, pageNum, totalPages int, style, bible string, entries []batch.WordEntry) string {
excerpt := strings.TrimSpace(section)
if len(excerpt) > comicPromptMaxChars {
excerpt = excerpt[:comicPromptMaxChars]
@@ -371,6 +377,7 @@ func buildStoryPagePrompt(section string, pageNum, totalPages int, style, bible
}
bibleBlock := bibleSection(bible, fmt.Sprintf("story page %d of %d", pageNum, totalPages))
+ vocabBlock := buildVocabBlock(entries)
return fmt.Sprintf(
// Lead with the hard language constraint so it is processed first.
"ЗАДЪЛЖИТЕЛНО / MANDATORY LANGUAGE RULE: This is a BULGARIAN comic book. "+
@@ -379,8 +386,16 @@ func buildStoryPagePrompt(section string, pageNum, totalPages int, style, bible
"(например: Здравей! Какво правиш? Побързай!). "+
"English text anywhere in the panels is STRICTLY FORBIDDEN — use ONLY Bulgarian.\n\n"+
"Art style: %s.%s\n"+
- "Comic book story page %d of %d. Layout: a 3×3 grid of 9 panels filling the page, "+
- "each panel showing a distinct moment from the excerpt below.\n"+
+ "%s"+ // vocabulary block
+ "Comic book story page %d of %d. "+
+ "MANDATORY PANEL LAYOUT — divide the image into exactly 4 panels in a 2×2 grid:\n"+
+ " • TOP-LEFT panel: scene 1 from the excerpt\n"+
+ " • TOP-RIGHT panel: scene 2 from the excerpt\n"+
+ " • BOTTOM-LEFT panel: scene 3 from the excerpt\n"+
+ " • BOTTOM-RIGHT panel: scene 4 from the excerpt\n"+
+ "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"+
"STRICT CONSISTENCY RULES — apply to every single panel:\n"+
" • Human characters: identical face, AGE APPEARANCE, hair colour/style, and clothing "+
"to the reference — a child must never look older or younger than defined.\n"+
@@ -389,10 +404,32 @@ func buildStoryPagePrompt(section string, pageNum, totalPages int, style, bible
" • Clothing changes only if this page's excerpt explicitly describes a change.\n"+
" • LANGUAGE: all speech, thought, and caption text — Bulgarian Cyrillic ONLY.\n"+
"Story excerpt:\n\n%s",
- style, bibleBlock, pageNum, totalPages, excerpt,
+ style, bibleBlock, vocabBlock, pageNum, totalPages, excerpt,
)
}
+// buildVocabBlock formats the vocabulary entries as a mandatory visual instruction
+// block. Each word must appear as a clearly labelled object or element in at least
+// one panel — making the comic page a vocabulary learning tool as well as a story page.
+func buildVocabBlock(entries []batch.WordEntry) string {
+ if len(entries) == 0 {
+ return ""
+ }
+ var sb strings.Builder
+ sb.WriteString("VOCABULARY WORDS — each word below MUST appear as a clearly visible, labelled\n")
+ sb.WriteString("object or element in at least one panel. Show the object in the scene and add a\n")
+ sb.WriteString("small Bulgarian label directly on it (bold text, contrasting colour, easy to read):\n")
+ for _, e := range entries {
+ if e.Translation != "" {
+ sb.WriteString(fmt.Sprintf(" • %s (%s)\n", e.Bulgarian, e.Translation))
+ } else {
+ sb.WriteString(fmt.Sprintf(" • %s\n", e.Bulgarian))
+ }
+ }
+ sb.WriteString("\n")
+ return sb.String()
+}
+
// buildBackCoverPrompt constructs the back-cover image prompt.
// blurb is an English marketing summary generated by Gemini; when non-empty it is
// embedded verbatim in the blurb-box instruction so the image model renders it.
diff --git a/internal/story/generator.go b/internal/story/generator.go
index 4f4317e..896782b 100644
--- a/internal/story/generator.go
+++ b/internal/story/generator.go
@@ -244,7 +244,7 @@ func parseGenerateResult(combined string) GenerateResult {
func buildStoryPrompt(entries []batch.WordEntry, theme string) string {
genre := resolveGenre(theme)
header := fmt.Sprintf(
- "Write a ~500-word story in Bulgarian that naturally uses all of the following words.\n"+
+ "Write a ~250-word story in Bulgarian that naturally uses all of the following words.\n"+
"The story must be %s — do NOT write a generic fairy tale.\n"+
"Number each word as shown below. Return ONLY the story text — no title, no header, no explanation.\n\n",
genre,
@@ -259,7 +259,7 @@ func buildStoryPrompt(entries []batch.WordEntry, theme string) string {
func buildStoryPromptFull(entries []batch.WordEntry, theme string) string {
genre := resolveGenre(theme)
var sb strings.Builder
- sb.WriteString("Write a ~500-word story in Bulgarian that naturally uses all of the following words.\n")
+ sb.WriteString("Write a ~250-word story in Bulgarian that naturally uses all of the following words.\n")
sb.WriteString(fmt.Sprintf("The story must be %s — do NOT write a generic fairy tale.\n", genre))
sb.WriteString("Number each word as shown below.\n\n")
sb.WriteString(buildWordList(entries, ""))
diff --git a/internal/story/narrator.go b/internal/story/narrator.go
index 6e9fb70..39fa456 100644
--- a/internal/story/narrator.go
+++ b/internal/story/narrator.go
@@ -29,22 +29,29 @@ const (
// conclusionSystemInstruction directs Gemini to write a short cinematic
// epilogue in Bulgarian — roughly 40–60 words (≈15–30 seconds of narration).
conclusionSystemInstruction = `You are a dramatic cinematic narrator writing a closing epilogue for a Bulgarian story.
-Write a SHORT closing epilogue in Bulgarian: exactly 3–4 sentences, cinematic and poetic,
-with a warm and conclusive tone — like the final voice-over of a film that leaves the audience
-with a sense of wonder and completion. Do NOT summarise the plot; instead reflect on the deeper
-meaning or emotion of the story. Output only the Bulgarian epilogue text, nothing else.`
+Write a SHORT closing epilogue in the BULGARIAN language (NOT Russian — Bulgarian uses Cyrillic but
+is a distinct language with different phonology, vocabulary, and grammar).
+Exactly 3–4 sentences, cinematic and poetic, with a warm and conclusive tone — like the final
+voice-over of a film that leaves the audience with a sense of wonder and completion.
+Do NOT summarise the plot; instead reflect on the deeper meaning or emotion of the story.
+Output only the Bulgarian epilogue text, nothing else.`
// cinematicInstruction is prepended to every chunk before the TTS call.
// Gemini TTS reads style instructions from the user-turn prompt, so embedding
// the directive here (rather than as a SystemInstruction) is the supported way
// to control voice style, pacing, and emotional delivery.
- cinematicInstruction = `You are a dramatic cinematic narrator performing a Bulgarian story.
+ // The language must be stated explicitly: Gemini TTS can confuse Bulgarian with
+ // Russian (both use Cyrillic) and apply Slavic Russian phonology by default.
+ cinematicInstruction = `You are a dramatic cinematic narrator performing a story written in BULGARIAN.
+IMPORTANT: This text is in the BULGARIAN language — NOT Russian, NOT Serbian, NOT any other Slavic language.
+Pronounce every word using authentic BULGARIAN phonology and accent. Bulgarian vowels are clear and distinct;
+do not apply Russian stress patterns or Russian vowel reduction. The letter 'ъ' in Bulgarian is a mid-central
+vowel (like the 'u' in "but"), not the Russian reduced schwa.
Deliver this as a professional movie trailer narrator would: deep, resonant, and commanding.
Use long dramatic pauses before key moments. Build tension with slower, deliberate pacing,
then accelerate through action. Drop your voice low and gravelly for mysterious or serious
passages; let warmth and energy rise for joyful or triumphant ones. Breathe life into every
-sentence — this should sound like an epic film, not a reading exercise. Pronounce all
-Bulgarian words with authentic clarity and expressive intonation.
+sentence — this should sound like an epic Bulgarian film, not a reading exercise.
`
)
@@ -130,11 +137,18 @@ func (n *Narrator) Narrate(storyText, outputFile string) error {
chunkPaths = append(chunkPaths, conclusionPath)
}
+ // Merge all segments into a single file, then widen to stereo.
+ // Gemini TTS produces mono audio; convertToStereo duplicates the channel so
+ // the result plays correctly on headphones without audio only in one ear.
+ combinedPath := filepath.Join(tmpDir, "combined.mp3")
if len(chunkPaths) == 1 {
- // Only one segment (short story, no conclusion) — move directly.
- return os.Rename(chunkPaths[0], outputFile)
+ combinedPath = chunkPaths[0]
+ } else {
+ if err := concatenateMP3s(chunkPaths, combinedPath, tmpDir); err != nil {
+ return err
+ }
}
- return concatenateMP3s(chunkPaths, outputFile, tmpDir)
+ return convertToStereo(combinedPath, outputFile)
}
// narrateConclusion generates a short Bulgarian cinematic epilogue via Gemini text,
@@ -232,6 +246,32 @@ func concatenateMP3s(chunkPaths []string, outputFile, tmpDir string) error {
return nil
}
+// convertToStereo re-encodes a mono MP3 to stereo by duplicating the single
+// channel into both left and right. Gemini TTS always outputs mono; without this
+// step the audio plays only in one ear on headphones.
+func convertToStereo(inputFile, outputFile string) error {
+ ffmpegPath, err := exec.LookPath("ffmpeg")
+ if err != nil {
+ // ffmpeg absent — fall back to a plain copy so narration still saves.
+ fmt.Println(" Warning: ffmpeg not found, narration will be mono")
+ return os.Rename(inputFile, outputFile)
+ }
+
+ cmd := exec.Command(ffmpegPath,
+ "-nostdin", "-hide_banner", "-loglevel", "error",
+ "-y",
+ "-i", inputFile,
+ "-ac", "2", // duplicate mono channel into stereo
+ "-codec:a", "libmp3lame", "-q:a", "2",
+ outputFile,
+ )
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("ffmpeg stereo conversion failed: %w\n%s", err, strings.TrimSpace(string(out)))
+ }
+ return nil
+}
+
// splitIntoNarrationChunks divides text into chunks of at most targetWords words,
// splitting at paragraph boundaries (double newline) whenever possible.
// Each chunk is trimmed and non-empty.
diff --git a/internal/story/runner.go b/internal/story/runner.go
index b2442a1..871cb0e 100644
--- a/internal/story/runner.go
+++ b/internal/story/runner.go
@@ -8,6 +8,8 @@ import (
"codeberg.org/snonux/totalrecall/internal/batch"
)
+
+
// ttsTodoContent is written to story_tts_todo.txt as a fallback when Gemini TTS
// narration fails or no API key is available. It documents the original
// ElevenLabs integration placeholder for reference.
@@ -141,18 +143,17 @@ func (r *Runner) Run(batchFile string) error {
return err
}
- r.drawComicPages(result.StoryText, result.Bible, slug)
+ r.drawComicPages(result.StoryText, result.Bible, slug, entries)
return r.handleNarration(result.StoryText, slug, comicsDir)
}
// drawComicPages generates the 5 comic images and assembles them into a PDF.
-// The pre-built bible (from story generation) is passed to DrawComicPages so
-// no extra Gemini call is needed for character consistency.
+// entries carries the vocabulary words so panels can visually feature and label them.
// Errors are non-fatal — story.txt is always accessible regardless of image failures.
-func (r *Runner) drawComicPages(storyText, bible, titleSlug string) {
+func (r *Runner) drawComicPages(storyText, bible, titleSlug string, entries []batch.WordEntry) {
fmt.Printf("Generating %d comic pages...\n", storyPageCount+2) // 2 = cover + back cover
- paths, err := r.artist.DrawComicPages(storyText, bible, titleSlug)
+ paths, err := r.artist.DrawComicPages(storyText, bible, titleSlug, entries)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: comic page generation failed: %v\n", err)
}