From a35ca6f40de73e93372ade6f2d74c231389799e2 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 20 Apr 2026 00:02:07 +0300 Subject: Fix task 25: keep comic generation single-language and single-style --- internal/comic/artist.go | 12 ++++- internal/comic/comic_test.go | 103 +++++++++++++++++++++++------------- internal/comic/generator.go | 44 ++++++++++++++- internal/comic/localization.go | 11 ++++ internal/comic/localization_test.go | 42 +++++++++++++++ internal/comic/narrator.go | 7 ++- internal/comic/text_validation.go | 72 +++++++++++++++++++++++++ internal/comic/types.go | 10 ++-- internal/config/config.go | 4 +- internal/config/config_test.go | 12 ++--- internal/image/gemini.go | 2 +- internal/image/gemini_test.go | 28 ++++++++++ prompts/bible_system.md | 2 +- prompts/blurb_system.md | 2 +- prompts/story_full_prompt.md | 18 +++---- prompts/story_prompt.md | 2 +- 16 files changed, 301 insertions(+), 70 deletions(-) create mode 100644 internal/comic/localization.go create mode 100644 internal/comic/localization_test.go create mode 100644 internal/comic/text_validation.go 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}} -- cgit v1.2.3