summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/comic/artist.go12
-rw-r--r--internal/comic/comic_test.go103
-rw-r--r--internal/comic/generator.go44
-rw-r--r--internal/comic/localization.go11
-rw-r--r--internal/comic/localization_test.go42
-rw-r--r--internal/comic/narrator.go7
-rw-r--r--internal/comic/text_validation.go72
-rw-r--r--internal/comic/types.go10
-rw-r--r--internal/config/config.go4
-rw-r--r--internal/config/config_test.go12
-rw-r--r--internal/image/gemini.go2
-rw-r--r--internal/image/gemini_test.go28
-rw-r--r--prompts/bible_system.md2
-rw-r--r--prompts/blurb_system.md2
-rw-r--r--prompts/story_full_prompt.md18
-rw-r--r--prompts/story_prompt.md2
16 files changed, 301 insertions, 70 deletions
diff --git a/internal/comic/artist.go b/internal/comic/artist.go
index 279aa1d..54f28cf 100644
--- a/internal/comic/artist.go
+++ b/internal/comic/artist.go
@@ -199,6 +199,8 @@ func (a *Artist) resolveHelperTexts(ctx context.Context, storyText, prebuiltBibl
}
systemPrompt, err := a.prompts.RenderPrompt(blurbSystemTemplate, map[string]any{
"StoryText": storyText,
+ "Language": a.language,
+ "Script": a.script,
})
if err != nil {
return "", "", fmt.Errorf("render blurb prompt: %w", err)
@@ -212,6 +214,12 @@ func (a *Artist) resolveHelperTexts(ctx context.Context, storyText, prebuiltBibl
return bible, blurb, nil
}
blurb = strings.TrimSpace(text)
+ if err := validateTextScript("back-cover blurb", blurb, a.script); err != nil {
+ return "", "", fmt.Errorf("generate back-cover blurb: %w", err)
+ }
+ if err := validateNoPromptLeakage("back-cover blurb", blurb); err != nil {
+ return "", "", fmt.Errorf("generate back-cover blurb: %w", err)
+ }
if blurb != "" {
fmt.Printf(" Back-cover blurb ready (%d chars)\n", len(blurb))
}
@@ -224,7 +232,7 @@ func (a *Artist) coverPromptData(storyText, style, bible string) map[string]any
"Script": a.script,
"Style": style,
"Bible": bible,
- "Subtitle": "ComicForge Adventures",
+ "Subtitle": localizedBrandName(a.language, a.script),
"StoryText": storyText,
"RenderingRequirement": a.renderingRequirement(),
"RenderingRequirementEnd": a.renderingRequirementEnd(),
@@ -265,7 +273,7 @@ func (a *Artist) backPromptData(storyText, style, bible, blurb string) map[strin
"Style": style,
"Bible": bible,
"BlurbBox": blurbBoxInstruction(blurb),
- "SeriesTitle": "ComicForge Adventures",
+ "SeriesTitle": localizedBrandName(a.language, a.script),
"StoryText": storyText,
"RenderingRequirement": a.renderingRequirement(),
"RenderingRequirementEnd": a.renderingRequirementEnd(),
diff --git a/internal/comic/comic_test.go b/internal/comic/comic_test.go
index ed263d8..3506303 100644
--- a/internal/comic/comic_test.go
+++ b/internal/comic/comic_test.go
@@ -79,32 +79,32 @@ func TestGeneratorGenerateFull(t *testing.T) {
t.Parallel()
text := strings.Join([]string{
- "story",
+ "история",
storyBibleSeparator,
- "bible",
+ "библия",
storyTitleSeparator,
- "Title",
+ "Заглавие",
storyPanelSeparator,
- "P1-A: a",
- "P1-B: b",
- "P1-C: c",
- "P1-D: d",
- "P2-A: e",
- "P2-B: f",
- "P2-C: g",
- "P2-D: h",
- "P3-A: i",
- "P3-B: j",
- "P3-C: k",
- "P3-D: l",
- "P4-A: m",
- "P4-B: n",
- "P4-C: o",
- "P4-D: p",
- "P5-A: q",
- "P5-B: r",
- "P5-C: s",
- "P5-D: t",
+ "P1-A: а",
+ "P1-B: б",
+ "P1-C: в",
+ "P1-D: г",
+ "P2-A: д",
+ "P2-B: е",
+ "P2-C: ж",
+ "P2-D: з",
+ "P3-A: и",
+ "P3-B: й",
+ "P3-C: к",
+ "P3-D: л",
+ "P4-A: м",
+ "P4-B: н",
+ "P4-C: о",
+ "P4-D: п",
+ "P5-A: р",
+ "P5-B: с",
+ "P5-C: т",
+ "P5-D: у",
}, "\n")
generator := NewGenerator(&GeneratorConfig{
TextProvider: fakeTextProvider{text: text},
@@ -114,7 +114,7 @@ func TestGeneratorGenerateFull(t *testing.T) {
if err != nil {
t.Fatalf("GenerateFull() error = %v", err)
}
- if got.Title != "Title" || got.StoryText != "story" || got.Bible != "bible" {
+ if got.Title != "Заглавие" || got.StoryText != "история" || got.Bible != "библия" {
t.Fatalf("GenerateFull() = %#v", got)
}
}
@@ -144,19 +144,24 @@ func TestArtistAndRunnerEndToEndWithFakes(t *testing.T) {
tmpDir := t.TempDir()
img := fakeImageProvider{t: t}
- genText := fakeTextProvider{text: strings.Join([]string{
- "story",
- storyBibleSeparator,
- "bible",
- storyTitleSeparator,
- "Title",
- storyPanelSeparator,
- "P1-A: a", "P1-B: b", "P1-C: c", "P1-D: d",
- "P2-A: e", "P2-B: f", "P2-C: g", "P2-D: h",
- "P3-A: i", "P3-B: j", "P3-C: k", "P3-D: l",
- "P4-A: m", "P4-B: n", "P4-C: o", "P4-D: p",
- "P5-A: q", "P5-B: r", "P5-C: s", "P5-D: t",
- }, "\n")}
+ genText := &scriptedTextProvider{responses: []string{
+ strings.Join([]string{
+ "история",
+ storyBibleSeparator,
+ "библия",
+ storyTitleSeparator,
+ "Заглавие",
+ storyPanelSeparator,
+ "P1-A: а", "P1-B: б", "P1-C: в", "P1-D: г",
+ "P2-A: д", "P2-B: е", "P2-C: ж", "P2-D: з",
+ "P3-A: и", "P3-B: й", "P3-C: к", "P3-D: л",
+ "P4-A: м", "P4-B: н", "P4-C: о", "P4-D: п",
+ "P5-A: р", "P5-B: с", "P5-C: т", "P5-D: у",
+ }, "\n"),
+ "Кратко рекламно описание.",
+ "Пъстра уводна реплика за разказа.",
+ "Топъл заключителен епилог за историята.",
+ }}
narr := fakeTTSProvider{}
runner := NewRunner(&RunnerConfig{
TextProvider: genText,
@@ -194,7 +199,10 @@ func TestRunnerPropagatesRenderFailures(t *testing.T) {
})
runner := NewRunner(&RunnerConfig{
- TextProvider: fakeTextProvider{text: strings.Join([]string{"story", storyBibleSeparator, "bible", storyTitleSeparator, "Title", storyPanelSeparator, "P1-A: a", "P1-B: b", "P1-C: c", "P1-D: d", "P2-A: e", "P2-B: f", "P2-C: g", "P2-D: h", "P3-A: i", "P3-B: j", "P3-C: k", "P3-D: l", "P4-A: m", "P4-B: n", "P4-C: o", "P4-D: p", "P5-A: q", "P5-B: r", "P5-C: s", "P5-D: t"}, "\n")},
+ TextProvider: &scriptedTextProvider{responses: []string{
+ strings.Join([]string{"история", storyBibleSeparator, "библия", storyTitleSeparator, "Заглавие", storyPanelSeparator, "P1-A: а", "P1-B: б", "P1-C: в", "P1-D: г", "P2-A: д", "P2-B: е", "P2-C: ж", "P2-D: з", "P3-A: и", "P3-B: й", "P3-C: к", "P3-D: л", "P4-A: м", "P4-B: н", "P4-C: о", "P4-D: п", "P5-A: р", "P5-B: с", "P5-C: т", "P5-D: у"}, "\n"),
+ "Кратко рекламно описание.",
+ }},
ImageProvider: failingImageProvider{},
MainTTSProvider: fakeTTSProvider{},
ConclusionTTSProvider: fakeTTSProvider{},
@@ -274,6 +282,25 @@ func (f fakeTextProvider) GenerateText(_ context.Context, _ string) (string, err
return f.text, nil
}
+type scriptedTextProvider struct {
+ responses []string
+ index int
+}
+
+func (p *scriptedTextProvider) Name() string { return "scripted-text" }
+func (p *scriptedTextProvider) IsAvailable() error { return nil }
+func (p *scriptedTextProvider) GenerateText(_ context.Context, _ string) (string, error) {
+ if len(p.responses) == 0 {
+ return "", nil
+ }
+ if p.index >= len(p.responses) {
+ return p.responses[len(p.responses)-1], nil
+ }
+ out := p.responses[p.index]
+ p.index++
+ return out, nil
+}
+
type fakeImageProvider struct{ t *testing.T }
func (f fakeImageProvider) Name() string { return "fake-image" }
diff --git a/internal/comic/generator.go b/internal/comic/generator.go
index 34c60c5..b3be2ef 100644
--- a/internal/comic/generator.go
+++ b/internal/comic/generator.go
@@ -79,6 +79,12 @@ func (g *Generator) Generate(ctx context.Context, entries []WordEntry) (string,
if text == "" {
return "", fmt.Errorf("no story content returned")
}
+ if err := validateTextScript("story text", text, g.script); err != nil {
+ return "", err
+ }
+ if err := validateNoPromptLeakage("story text", text); err != nil {
+ return "", err
+ }
return text, nil
}
@@ -101,7 +107,11 @@ func (g *Generator) GenerateFull(ctx context.Context, entries []WordEntry) (Gene
if text == "" {
return GenerateResult{}, fmt.Errorf("no content returned")
}
- return parseGenerateResult(text), nil
+ result := parseGenerateResult(text)
+ if err := validateGeneratedResult(result, g.script); err != nil {
+ return GenerateResult{}, err
+ }
+ return result, nil
}
func (g *Generator) ready() error {
@@ -139,3 +149,35 @@ func (g *Generator) renderStoryPrompt(templateName string, entries []WordEntry)
}
return systemPrompt + "\n\n" + userPrompt, nil
}
+
+func validateGeneratedResult(result GenerateResult, script string) error {
+ if err := validateTextScript("story text", result.StoryText, script); err != nil {
+ return err
+ }
+ if err := validateNoPromptLeakage("story text", result.StoryText); err != nil {
+ return err
+ }
+ if err := validateTextScript("character bible", result.Bible, script); err != nil {
+ return err
+ }
+ if err := validateNoPromptLeakage("character bible", result.Bible); err != nil {
+ return err
+ }
+ if err := validateTextScript("comic title", result.Title, script); err != nil {
+ return err
+ }
+ if err := validateNoPromptLeakage("comic title", result.Title); err != nil {
+ return err
+ }
+ for pageIdx, page := range result.PanelScript {
+ for panelIdx, panel := range page {
+ if err := validateTextScript(fmt.Sprintf("panel script P%d-%c", pageIdx+1, 'A'+panelIdx), panel, script); err != nil {
+ return err
+ }
+ if err := validateNoPromptLeakage(fmt.Sprintf("panel script P%d-%c", pageIdx+1, 'A'+panelIdx), panel); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/comic/localization.go b/internal/comic/localization.go
new file mode 100644
index 0000000..2578feb
--- /dev/null
+++ b/internal/comic/localization.go
@@ -0,0 +1,11 @@
+package comic
+
+import "strings"
+
+// localizedBrandName returns the comic's subtitle/series name in the selected script.
+func localizedBrandName(language, script string) string {
+ if strings.EqualFold(script, "Cyrillic") || strings.EqualFold(language, "Bulgarian") {
+ return "КомиксФордж Приключения"
+ }
+ return "ComicForge Adventures"
+}
diff --git a/internal/comic/localization_test.go b/internal/comic/localization_test.go
new file mode 100644
index 0000000..a1609f3
--- /dev/null
+++ b/internal/comic/localization_test.go
@@ -0,0 +1,42 @@
+package comic
+
+import "testing"
+
+func TestLocalizedBrandName(t *testing.T) {
+ t.Parallel()
+
+ if got, want := localizedBrandName("Bulgarian", "Cyrillic"), "КомиксФордж Приключения"; got != want {
+ t.Fatalf("localizedBrandName() = %q, want %q", got, want)
+ }
+ if got, want := localizedBrandName("English", "Latin"), "ComicForge Adventures"; got != want {
+ t.Fatalf("localizedBrandName() = %q, want %q", got, want)
+ }
+}
+
+func TestValidateTextScript(t *testing.T) {
+ t.Parallel()
+
+ if err := validateTextScript("title", "Заглавие", "Cyrillic"); err != nil {
+ t.Fatalf("validateTextScript() error = %v", err)
+ }
+ if err := validateTextScript("title", "Title", "Cyrillic"); err == nil {
+ t.Fatal("validateTextScript() error = nil, want Latin rejection")
+ }
+ if err := validateTextScript("title", "Title", "Latin"); err != nil {
+ t.Fatalf("validateTextScript() error = %v", err)
+ }
+ if err := validateTextScript("title", "Заглавие", "Latin"); err == nil {
+ t.Fatal("validateTextScript() error = nil, want Cyrillic rejection")
+ }
+}
+
+func TestValidateNoPromptLeakage(t *testing.T) {
+ t.Parallel()
+
+ if err := validateNoPromptLeakage("story", "A calm story without artifacts."); err != nil {
+ t.Fatalf("validateNoPromptLeakage() error = %v", err)
+ }
+ if err := validateNoPromptLeakage("story", "Words to include:\n- ябълка"); err == nil {
+ t.Fatal("validateNoPromptLeakage() error = nil, want prompt leakage rejection")
+ }
+}
diff --git a/internal/comic/narrator.go b/internal/comic/narrator.go
index 1b4365f..a7c76c3 100644
--- a/internal/comic/narrator.go
+++ b/internal/comic/narrator.go
@@ -206,7 +206,12 @@ func (n *Narrator) buildTeaser(ctx context.Context, templateName, storyText stri
fmt.Printf(" Warning: teaser generation failed: %v\n", err)
return ""
}
- return strings.TrimSpace(text)
+ teaser := strings.TrimSpace(text)
+ if err := validateTextScript("narration teaser", teaser, n.script); err != nil {
+ fmt.Printf(" Warning: teaser validation failed: %v\n", err)
+ return ""
+ }
+ return teaser
}
func (n *Narrator) narrateChunkWith(ctx context.Context, provider provider.TTSProvider, text, outputFile string) error {
diff --git a/internal/comic/text_validation.go b/internal/comic/text_validation.go
new file mode 100644
index 0000000..31f1bba
--- /dev/null
+++ b/internal/comic/text_validation.go
@@ -0,0 +1,72 @@
+package comic
+
+import (
+ "fmt"
+ "strings"
+ "unicode"
+)
+
+var promptLeakMarkers = []string{
+ "words to include",
+ "required words",
+ "character guide",
+ "comic title",
+ "panel script",
+ "mandatory language rule",
+ "mandatory panel layout",
+ "mandatory speech bubbles",
+ "strict consistency rules",
+ "story excerpt",
+ "story teaser",
+ "story ending hint",
+ "---character guide---",
+ "---comic title---",
+ "---panel script---",
+}
+
+func validateTextScript(label, text, script string) error {
+ text = strings.TrimSpace(text)
+ if text == "" {
+ return fmt.Errorf("%s is empty", label)
+ }
+
+ switch {
+ case strings.EqualFold(script, "Cyrillic"):
+ if containsLatinLetters(text) {
+ return fmt.Errorf("%s contains Latin letters despite %s script", label, script)
+ }
+ case strings.EqualFold(script, "Latin"):
+ if containsCyrillicLetters(text) {
+ return fmt.Errorf("%s contains Cyrillic letters despite %s script", label, script)
+ }
+ }
+ return nil
+}
+
+func containsLatinLetters(text string) bool {
+ for _, r := range text {
+ if unicode.Is(unicode.Latin, r) {
+ return true
+ }
+ }
+ return false
+}
+
+func containsCyrillicLetters(text string) bool {
+ for _, r := range text {
+ if unicode.Is(unicode.Cyrillic, r) {
+ return true
+ }
+ }
+ return false
+}
+
+func validateNoPromptLeakage(label, text string) error {
+ lower := strings.ToLower(text)
+ for _, marker := range promptLeakMarkers {
+ if strings.Contains(lower, marker) {
+ return fmt.Errorf("%s contains prompt leakage marker %q", label, marker)
+ }
+ }
+ return nil
+}
diff --git a/internal/comic/types.go b/internal/comic/types.go
index 59b410c..6689095 100644
--- a/internal/comic/types.go
+++ b/internal/comic/types.go
@@ -234,18 +234,14 @@ func buildWordList(entries []WordEntry, header string) string {
var sb strings.Builder
sb.WriteString(header)
if header != "" {
- sb.WriteString("Words to include:\n")
+ sb.WriteString("Required words:\n")
}
- for i, entry := range entries {
+ for _, entry := range entries {
word := strings.TrimSpace(entry.Word)
if word == "" {
word = strings.TrimSpace(entry.Translation)
}
- if entry.Translation != "" && entry.Word != "" {
- sb.WriteString(fmt.Sprintf("%d. %s (%s)\n", i+1, entry.Word, entry.Translation))
- continue
- }
- sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, word))
+ sb.WriteString(fmt.Sprintf("- %s\n", word))
}
return sb.String()
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 2726c46..ba8a56e 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -126,8 +126,8 @@ func DefaultConfig() *Config {
Language: LanguageConfig{
Input: "Vocabulary",
Output: "Story",
- Story: "Story",
- Script: "Latin",
+ Story: "Bulgarian",
+ Script: "Cyrillic",
},
Story: StoryConfig{
Genres: []string{
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index ae2127a..74fa944 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -88,7 +88,7 @@ func TestRenderPromptFallsBackToEmbeddedTemplate(t *testing.T) {
got, err := cfg.RenderPrompt("story_prompt.md", map[string]any{
"Language": "Bulgarian",
"Genre": "a mystery with a surprising twist",
- "Words": "1. ябълка (apple)\n2. книга (book)\n",
+ "Words": "- ябълка\n- книга\n",
})
if err != nil {
t.Fatalf("RenderPrompt() error = %v", err)
@@ -145,9 +145,9 @@ func TestEmbeddedPromptTemplatesRender(t *testing.T) {
"Script": "Cyrillic",
"Genre": "a mystery with a surprising twist",
"Style": "cinematic realism",
- "Words": "1. ябълка (apple)\n2. книга (book)\n",
- "Bible": "Mira: young adult, brown hair, blue eyes, red coat.\n",
- "Subtitle": "Sample Subtitle",
+ "Words": "- ябълка\n- книга\n",
+ "Bible": "Мира: млада жена, кафява коса, сини очи, червено палто.\n",
+ "Subtitle": "КомиксФордж Приключения",
"StoryText": "A short story teaser.",
"RenderingRequirement": "ULTRA-REALISTIC RENDERING",
"RenderingRequirementEnd": "FINAL LOCK — PHOTOREALISM",
@@ -157,8 +157,8 @@ func TestEmbeddedPromptTemplatesRender(t *testing.T) {
"PageNum": 1,
"TotalPages": 5,
"PanelLayout": "MANDATORY PANEL LAYOUT — divide the image into exactly 4 panels in a 2×2 grid.\n",
- "BlurbBox": "a rectangular text box with a short blurb",
- "SeriesTitle": "ComicForge Adventures",
+ "BlurbBox": "правоъгълно текстово поле с кратък текст",
+ "SeriesTitle": "КомиксФордж Приключения",
"Pose": "extreme close-up portrait",
}
diff --git a/internal/image/gemini.go b/internal/image/gemini.go
index 85da3a8..15373e2 100644
--- a/internal/image/gemini.go
+++ b/internal/image/gemini.go
@@ -185,7 +185,7 @@ func (c *GeminiProvider) GenerateImage(ctx context.Context, prompt, outputFile s
if strings.TrimSpace(outputFile) == "" {
return fmt.Errorf("output file is required")
}
- results, err := c.Search(ctx, &SearchOptions{Query: prompt})
+ results, err := c.Search(ctx, &SearchOptions{CustomPrompt: prompt})
if err != nil {
return err
}
diff --git a/internal/image/gemini_test.go b/internal/image/gemini_test.go
index bb3affe..bbc9dcd 100644
--- a/internal/image/gemini_test.go
+++ b/internal/image/gemini_test.go
@@ -107,6 +107,34 @@ func TestGeminiProvider_Search_CustomPromptSkipsTextGeneration(t *testing.T) {
}
}
+func TestGeminiProvider_GenerateImage_UsesCustomPrompt(t *testing.T) {
+ originalText := geminiGenerateText
+ originalImage := geminiGenerateImage
+ t.Cleanup(func() {
+ geminiGenerateText = originalText
+ geminiGenerateImage = originalImage
+ })
+
+ geminiGenerateText = func(context.Context, *GeminiProvider, string, string, string, float32, int32) (string, error) {
+ t.Fatal("unexpected text generation for comic prompt")
+ return "", nil
+ }
+
+ var gotPrompt string
+ geminiGenerateImage = func(_ context.Context, _ *GeminiProvider, prompt, _ string) ([]byte, string, error) {
+ gotPrompt = prompt
+ return mustJPEGBytes(t), "image/jpeg", nil
+ }
+
+ client := NewGeminiProvider(&GeminiConfig{APIKey: "test-key"})
+ if err := client.GenerateImage(context.Background(), "Art style: comic. FULL-BLEED SINGLE ILLUSTRATION.", t.TempDir()+"/out.png"); err != nil {
+ t.Fatalf("GenerateImage() unexpected error: %v", err)
+ }
+ if gotPrompt != "Art style: comic. FULL-BLEED SINGLE ILLUSTRATION." {
+ t.Fatalf("GenerateImage() prompt = %q, want custom prompt", gotPrompt)
+ }
+}
+
func TestGeminiProvider_Search_GeneratedPromptFlow(t *testing.T) {
originalText := geminiGenerateText
originalImage := geminiGenerateImage
diff --git a/prompts/bible_system.md b/prompts/bible_system.md
index 334e307..918a4b7 100644
--- a/prompts/bible_system.md
+++ b/prompts/bible_system.md
@@ -1,4 +1,4 @@
-You are a comic-book art director producing a CHARACTER CONSISTENCY GUIDE in English for an illustrator.
+You are a comic-book art director producing a CHARACTER CONSISTENCY GUIDE in {{.Language}} using {{.Script}} script for an illustrator.
For every named HUMAN character provide: name, apparent age category (young child, teenager, young adult, middle-aged, elderly), hair (colour + style), eye colour, skin tone, build, and the EXACT clothing they wear — specify garment, colour, pattern, and fit. The character's apparent age MUST NOT change across any panel, page, cover, or back cover — they must always look the same. Clothing must NOT change between panels unless the story explicitly describes a change; if no change is described, list the same outfit for all appearances.
diff --git a/prompts/blurb_system.md b/prompts/blurb_system.md
index 045c696..b6aa189 100644
--- a/prompts/blurb_system.md
+++ b/prompts/blurb_system.md
@@ -1,3 +1,3 @@
You are a comic-book editor writing back-cover marketing copy.
-Rules: write exactly 2–3 sentences in English; exciting and enticing; do NOT spoil the ending; use present-tense second-person (e.g. "Join Eli as she discovers…").
+Rules: write exactly 2–3 sentences in {{.Language}} using {{.Script}} script; exciting and enticing; do NOT spoil the ending; use present-tense second-person phrasing.
Output only the blurb text — no quotes, no labels, no extra commentary.
diff --git a/prompts/story_full_prompt.md b/prompts/story_full_prompt.md
index e528086..76d5505 100644
--- a/prompts/story_full_prompt.md
+++ b/prompts/story_full_prompt.md
@@ -1,6 +1,6 @@
Write a ~250-word story in {{.Language}} that naturally uses all of the following words.
The story must be {{.Genre}} — do NOT write a generic fairy tale.
-Number each word as shown below.
+Use every word naturally, but do NOT repeat the word list formatting, bullet symbols, or labels in the story text.
Words to include:
{{.Words}}
@@ -8,7 +8,7 @@ Words to include:
After the story text, write exactly this line by itself (nothing else on that line):
{{.StoryBibleSeparator}}
-Then write a CHARACTER CONSISTENCY GUIDE in English for an illustrator.
+Then write a CHARACTER CONSISTENCY GUIDE in {{.Language}} using {{.Script}} script for an illustrator.
IMPORTANT: all human characters must be adults (18 years or older). Do NOT describe any character as a child, teenager, or minor.
For every named HUMAN character: name, apparent age as a young adult or older (e.g. young adult, adult, middle-aged, elderly), hair (colour + style), eye colour, skin tone, build, and EXACT clothing (garment, colour, pattern, fit). Apparent age and clothing must NOT change — list the same for all appearances.
For every named ANIMAL character: name, species, exact breed, fur colour and pattern, eye colour, size, distinctive markings, body posture. Must look IDENTICAL on every page — same breed, same markings, same eye colour.
@@ -18,20 +18,20 @@ Maximum 280 words for the guide. No headers, just dense descriptive prose.
After the character guide, write exactly this line by itself:
{{.StoryTitleSeparator}}
-Then write a short comic book title in English: 2-4 words that capture the story's theme and characters (e.g. 'Stardust Explorers', 'The Clockwork Dragon', 'Mystery at Midnight'). Output only the title — no quotes, no punctuation, no explanation.
+Then write a short comic book title in {{.Language}} using {{.Script}} script: 2-4 words that capture the story's theme and characters. Output only the title — no quotes, no punctuation, no explanation.
After the title, write exactly this line by itself:
{{.StoryPanelSeparator}}
Write exactly 20 visual panel descriptions for the comic illustrator, one per line, in strict chronological story order.
Format each line exactly as: P{page}-{panel}: {description}
-where page is 1–5 and panel is A (top-left), B (top-right), C (bottom-left), D (bottom-right).
+където page е от 1 до 5, а panel е A (горе вляво), B (горе вдясно), C (долу вляво), D (долу вдясно).
Each description is 1–2 sentences covering: WHO is in the panel, WHAT they are doing, WHERE they are, their expression or body language, and — REQUIRED for most panels — the exact {{.Language}} dialogue or thought text the character says/thinks, written in quotes.
At least 3 of the 4 panels on every page MUST include {{.Language}} dialogue in a speech bubble or thought bubble. The dialogue must come directly from the story and be in {{.Script}} script.
The 20 panels must retell the story from beginning to end — each page covers one story beat, each panel advances the action.
Do NOT repeat the same scene or camera angle on consecutive panels.
-Example format (replace with actual story content):
-P1-A: Maria walks out her front door into morning sunlight, laptop bag on shoulder, saying "Най-накрая!" in a speech bubble.
-P1-B: She strides down a busy city street past parked cars, headphones in, thinking "Толкова хубав ден." in a thought bubble.
-P1-C: Close-up of her hand gripping a large steaming coffee cup, the barista handing it over saying "Заповядайте!" in a speech bubble.
-P1-D: Maria pauses at a park entrance, gazing at green trees ahead, saying "Точно това ми трябваше." in a speech bubble.
+Примерен формат (замени с истинското съдържание на историята):
+P1-A: Мария излиза от входната си врата в утринната светлина, с чанта на рамо, и казва "Най-сетне!" в балон за реплика.
+P1-B: Тя върви по оживена улица покрай паркирани коли, с слушалки в ушите, и мисли "Толкова хубав ден." в мисловен балон.
+P1-C: Крупен план на ръката ѝ, която стиска голяма пареща чаша кафе, а баристата ѝ я подава с думите "Заповядайте!" в балон за реплика.
+P1-D: Мария спира при входа на парк и поглежда към зелените дървета отпред, като казва "Точно това ми трябваше." в балон за реплика.
diff --git a/prompts/story_prompt.md b/prompts/story_prompt.md
index 35c2da8..b49f063 100644
--- a/prompts/story_prompt.md
+++ b/prompts/story_prompt.md
@@ -1,6 +1,6 @@
Write a ~250-word story in {{.Language}} that naturally uses all of the following words.
The story must be {{.Genre}} — do NOT write a generic fairy tale.
-Number each word as shown below. Return ONLY the story text — no title, no header, no explanation.
+Use every word naturally, but do NOT repeat the word list formatting, bullet symbols, or labels in the story text. Return ONLY the story text — no title, no header, no explanation.
Words to include:
{{.Words}}