diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-04 10:58:19 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-04 10:58:19 +0300 |
| commit | 793d7b3c61a403a8c03f1eb44df741c6951444e8 (patch) | |
| tree | 718ce9b751ed592be80b90fbddfe1eb6e9834eb9 | |
| parent | a34789fd2f7c0e2a7292535dbd3cdc68d3f00675 (diff) | |
feat: photorealistic rendering requirement in all image prompts, vocabulary/theme filesv0.12.0
- Add renderingRequirement const applied to all prompts (cover, story pages, back cover, gallery)
- Write <slug>_comic_vocabulary.txt with vocabulary words and story text for learning
- Write <slug>_theme.txt recording the --story-theme used for easy reproduction
- Enforce adult (18+) characters in generator to avoid content policy blocks on revealing clothing
- Add cover/back cover retry logic (same pageMaxRetries as story pages)
- Bump version to 0.12.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | internal/story/artist.go | 11 | ||||
| -rw-r--r-- | internal/story/runner.go | 54 | ||||
| -rw-r--r-- | internal/version.go | 2 |
3 files changed, 66 insertions, 1 deletions
diff --git a/internal/story/artist.go b/internal/story/artist.go index fa62511..6e69435 100644 --- a/internal/story/artist.go +++ b/internal/story/artist.go @@ -57,6 +57,13 @@ const ( // helperRetryPause waits before retrying when the model returns an empty // response — typically caused by free-tier RPM exhaustion between rapid calls. helperRetryPause = 15 * time.Second + + // renderingRequirement is appended to every image prompt (cover, story pages, + // back cover, gallery) to push the model toward photorealistic output even + // within a comic grid layout. Centralised here so it is easy to tune. + renderingRequirement = "RENDERING REQUIREMENT: every panel and illustration must look " + + "like a real photograph — photorealistic skin texture, fabric detail, lighting, " + + "and environment. NOT a drawing, painting, or illustration. Real-world photo quality.\n" ) // comicStyles is the pool from which the page style is drawn each run. @@ -400,6 +407,7 @@ func buildCoverPrompt(storyText, style, bible string) string { "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.\n\n"+ "Art style: %s.%s\n"+ + renderingRequirement+ "TRADITIONAL COMIC BOOK FRONT COVER — portrait orientation, single full-bleed illustration.\n"+ "NO panel grid. NO speech bubbles.\n"+ "MANDATORY MASTHEAD — the most important visual element on this cover:\n"+ @@ -466,6 +474,7 @@ 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"+ + renderingRequirement+ "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"+ @@ -531,6 +540,7 @@ func buildBackCoverPrompt(storyText, style, bible, blurb string) string { "All visible text (blurb box, labels, banners) MUST be in Bulgarian Cyrillic script. "+ "English text anywhere on the back cover is STRICTLY FORBIDDEN.\n\n"+ "Art style: %s.%s\n"+ + renderingRequirement+ "TRADITIONAL COMIC BOOK BACK COVER — portrait orientation, single full-bleed illustration.\n"+ "NO panel grid. NO speech bubbles.\n"+ "Layout rules (must follow exactly):\n"+ @@ -566,6 +576,7 @@ func buildGalleryPagePrompt(style, bible string, galleryNum int) string { bibleBlock := bibleSection(bible, fmt.Sprintf("gallery page %d", galleryNum)) return fmt.Sprintf( "Art style: %s.%s\n"+ + renderingRequirement+ "FULL-BLEED CHARACTER ART PAGE — portrait orientation, single illustration.\n"+ "NO text of any kind. NO title. NO labels. NO speech bubbles. NO panel borders. NO UI elements.\n"+ "This is a text-free variant cover / gallery page. Pure art only.\n\n"+ diff --git a/internal/story/runner.go b/internal/story/runner.go index f69b2f4..107e584 100644 --- a/internal/story/runner.go +++ b/internal/story/runner.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "codeberg.org/snonux/totalrecall/internal/batch" ) @@ -143,6 +144,16 @@ func (r *Runner) Run(batchFile string) error { return err } + if err := r.saveVocabularyFile(result.StoryText, entries, slug, comicsDir); err != nil { + // Non-fatal: vocabulary file is a learning aid, not required for the comic. + fmt.Fprintf(os.Stderr, "Warning: could not write vocabulary file: %v\n", err) + } + + if err := r.saveThemeFile(slug, comicsDir); err != nil { + // Non-fatal: theme file is a convenience record for reproduction. + fmt.Fprintf(os.Stderr, "Warning: could not write theme file: %v\n", err) + } + r.drawComicPages(result.StoryText, result.Bible, slug, entries) return r.handleNarration(result.StoryText, slug, comicsDir) @@ -203,6 +214,49 @@ func (r *Runner) saveStoryText(text, titleSlug, dir string) error { return nil } +// saveVocabularyFile writes <slug>_comic_vocabulary.txt — a learning aid that +// lists the vocabulary words with translations followed by the full story text, +// so the reader can study the words in context. +func (r *Runner) saveVocabularyFile(storyText string, entries []batch.WordEntry, titleSlug, dir string) error { + path := filepath.Join(dir, titleSlug+"_comic_vocabulary.txt") + var sb strings.Builder + + sb.WriteString("# Vocabulary Words\n\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# Story Text\n\n") + sb.WriteString(strings.TrimSpace(storyText)) + sb.WriteString("\n") + + if err := os.WriteFile(path, []byte(sb.String()), 0644); err != nil { + return fmt.Errorf("failed to write vocabulary file: %w", err) + } + fmt.Printf("Vocabulary saved: %s\n", path) + return nil +} + +// saveThemeFile writes <slug>_theme.txt containing the --story-theme value used +// for this run, so the comic can be reproduced by passing the same theme again. +func (r *Runner) saveThemeFile(titleSlug, dir string) error { + theme := "" + if r.config != nil { + theme = r.config.Theme + } + path := filepath.Join(dir, titleSlug+"_theme.txt") + content := theme + "\n" + if err := os.WriteFile(path, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write theme file: %w", err) + } + fmt.Printf("Theme saved: %s\n", path) + return nil +} + // saveTTSPlaceholder writes <slug>_tts_todo.txt as a fallback when narration // is unavailable or fails. func (r *Runner) saveTTSPlaceholder(titleSlug, dir string) error { diff --git a/internal/version.go b/internal/version.go index 39b6037..e65f021 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,3 +1,3 @@ package internal -const Version = "0.11.0" +const Version = "0.12.0" |
