diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/comic/artist.go | 16 | ||||
| -rw-r--r-- | internal/comic/comic_test.go | 100 | ||||
| -rw-r--r-- | internal/comic/types.go | 1 | ||||
| -rw-r--r-- | internal/config/config_test.go | 3 |
4 files changed, 119 insertions, 1 deletions
diff --git a/internal/comic/artist.go b/internal/comic/artist.go index 13826ee..7ccf084 100644 --- a/internal/comic/artist.go +++ b/internal/comic/artist.go @@ -214,7 +214,11 @@ func (a *Artist) generateWithRetry(ctx context.Context, prompt, outputFile, labe } func (a *Artist) generatePromptImage(ctx context.Context, prompt, outputFile string) error { - return a.generateWithRetryAndValidation(ctx, prompt, outputFile, "manual prompt image", nil, nil) + renderedPrompt, err := a.prompts.RenderPrompt(manualPromptTemplate, a.manualPromptData(prompt)) + if err != nil { + return fmt.Errorf("render manual prompt: %w", err) + } + return a.generateWithRetryAndValidation(ctx, renderedPrompt, outputFile, "manual prompt image", nil, nil) } type imageOutputValidator func(context.Context, string, string, string) error @@ -379,6 +383,16 @@ func (a *Artist) backPromptData(storyText, style, bible, blurb string) map[strin } } +func (a *Artist) manualPromptData(prompt string) map[string]any { + return map[string]any{ + "Prompt": strings.TrimSpace(prompt), + "Style": localizedStylePrompt(a.style, a.language, a.script), + "Theme": a.theme, + "RenderingRequirement": a.renderingRequirement(), + "RenderingRequirementEnd": a.renderingRequirementEnd(), + } +} + func (a *Artist) renderingRequirement() string { if a.ultraRealistic { text, err := a.prompts.RenderPrompt(renderingRequirementPrompt, nil) diff --git a/internal/comic/comic_test.go b/internal/comic/comic_test.go index 29f2a31..866df64 100644 --- a/internal/comic/comic_test.go +++ b/internal/comic/comic_test.go @@ -3,6 +3,7 @@ package comic import ( "context" "errors" + "fmt" "os" "path/filepath" "strings" @@ -485,6 +486,52 @@ func TestRunnerRunPromptRejectsEmptyPrompt(t *testing.T) { } } +func TestRunnerRunPromptAppliesStyleThemeAndUltraContext(t *testing.T) { + t.Parallel() + + originalLeakValidation := validateImagePromptLeakageFn + validateImagePromptLeakageFn = func(context.Context, string, string, string) error { return nil } + t.Cleanup(func() { + validateImagePromptLeakageFn = originalLeakValidation + }) + + provider := &capturingImageProvider{t: t} + renderer := &recordingPromptRenderer{} + runner := NewRunner(&RunnerConfig{ + ImageProvider: provider, + Prompts: renderer, + OutputDir: t.TempDir(), + Slug: "manual-robot", + Style: "noir", + Theme: "mystery", + UltraRealistic: boolPtr(true), + PageMaxRetries: 1, + PageRetryBase: time.Second, + }) + + if err := runner.RunPrompt(context.Background(), "a robot reading a newspaper"); err != nil { + t.Fatalf("RunPrompt() error = %v", err) + } + if provider.lastPrompt == "" { + t.Fatal("image prompt was not captured") + } + if !strings.Contains(provider.lastPrompt, "Create a single image based on this prompt") { + t.Fatalf("prompt = %q, want manual prompt template", provider.lastPrompt) + } + if !strings.Contains(provider.lastPrompt, "a robot reading a newspaper") { + t.Fatalf("prompt = %q, want raw user prompt", provider.lastPrompt) + } + if !strings.Contains(provider.lastPrompt, "Visual style: noir.") { + t.Fatalf("prompt = %q, want style context", provider.lastPrompt) + } + if !strings.Contains(provider.lastPrompt, "Theme context: mystery.") { + t.Fatalf("prompt = %q, want theme context", provider.lastPrompt) + } + if !strings.Contains(provider.lastPrompt, "FINAL STYLE LOCK — PHOTOREALISM") { + t.Fatalf("prompt = %q, want ultra-realistic context", provider.lastPrompt) + } +} + func TestNewRunnerUsesRealisticWeightWhenUltraRealisticUnset(t *testing.T) { t.Parallel() @@ -847,6 +894,12 @@ func (fakePromptRenderer) RenderPrompt(name string, data any) (string, error) { return "system prompt", nil case storyPromptTemplate, storyFullPromptTemplate: return "prompt", nil + case renderingRequirementPrompt: + return "ULTRA-REALISTIC MODE for this image:\n • The image must look like a real photograph or a frame from a high-budget live-action film.\n • Skin, hair, fabric, metal, and environments must look natural and real, with no drawn or painterly effect.\n • Avoid cartoon, anime, manga, comic-line-art, or obviously illustrated rendering.\n • Text elements, if required by the composition, should look like part of a photographed physical cover, sign, or poster.\n • For gallery images, the whole image must be entirely photographic.\n", nil + case renderingRequirementEndPrompt: + return "FINAL STYLE LOCK — PHOTOREALISM: the entire image must look camera-captured. If anything looks drawn or painted, the result is wrong. Do not drift toward comic art between panels or in gallery images.", nil + case manualPromptTemplate: + return renderManualPromptForTest(data), nil case coverPromptTemplate, storyPagePromptTemplate, galleryPagePromptTemplate, backCoverPromptTemplate: return "image prompt", nil case blurbSystemTemplate, introSystemTemplate, conclusionSystemTemplate: @@ -880,6 +933,12 @@ func (r *recordingPromptRenderer) RenderPrompt(name string, data any) (string, e return "system prompt", nil case storyPromptTemplate, storyFullPromptTemplate: return "prompt", nil + case renderingRequirementPrompt: + return "ULTRA-REALISTIC MODE for this image:\n • The image must look like a real photograph or a frame from a high-budget live-action film.\n • Skin, hair, fabric, metal, and environments must look natural and real, with no drawn or painterly effect.\n • Avoid cartoon, anime, manga, comic-line-art, or obviously illustrated rendering.\n • Text elements, if required by the composition, should look like part of a photographed physical cover, sign, or poster.\n • For gallery images, the whole image must be entirely photographic.\n", nil + case renderingRequirementEndPrompt: + return "FINAL STYLE LOCK — PHOTOREALISM: the entire image must look camera-captured. If anything looks drawn or painted, the result is wrong. Do not drift toward comic art between panels or in gallery images.", nil + case manualPromptTemplate: + return renderManualPromptForTest(data), nil case coverPromptTemplate, storyPagePromptTemplate, galleryPagePromptTemplate, backCoverPromptTemplate: return "image prompt", nil case blurbSystemTemplate, introSystemTemplate, conclusionSystemTemplate: @@ -898,6 +957,32 @@ func isImagePromptTemplate(name string) bool { } } +func renderManualPromptForTest(data any) string { + m, _ := data.(map[string]any) + if m == nil { + return "Create a single image based on this prompt:" + } + var sb strings.Builder + sb.WriteString("Create a single image based on this prompt:\n\n") + sb.WriteString(fmt.Sprint(m["Prompt"])) + sb.WriteString("\n\n") + if style, _ := m["Style"].(string); strings.TrimSpace(style) != "" { + fmt.Fprintf(&sb, "Visual style: %s.\n", style) + } + if theme, _ := m["Theme"].(string); strings.TrimSpace(theme) != "" { + fmt.Fprintf(&sb, "Theme context: %s.\n", theme) + } + if requirement, _ := m["RenderingRequirement"].(string); strings.TrimSpace(requirement) != "" { + sb.WriteString(requirement) + sb.WriteString("\n") + } + sb.WriteString("No text, captions, logos, borders, panels, or UI elements.") + if requirementEnd, _ := m["RenderingRequirementEnd"].(string); strings.TrimSpace(requirementEnd) != "" { + sb.WriteString(requirementEnd) + } + return sb.String() +} + type fakeTextProvider struct{ text string } func (f fakeTextProvider) Name() string { return "fake-text" } @@ -936,6 +1021,21 @@ func (f fakeImageProvider) GenerateImage(_ context.Context, _ string, outputFile return nil } +type capturingImageProvider struct { + t *testing.T + lastPrompt string +} + +func (p *capturingImageProvider) Name() string { return "capturing-image" } +func (p *capturingImageProvider) IsAvailable() error { return nil } +func (p *capturingImageProvider) GenerateImage(_ context.Context, prompt, outputFile string) error { + p.lastPrompt = prompt + if err := os.WriteFile(outputFile, []byte("png"), 0o644); err != nil { + p.t.Fatal(err) + } + return nil +} + type refTrackingImageProvider struct { t *testing.T referenceCounts []int diff --git a/internal/comic/types.go b/internal/comic/types.go index 063d38b..f6c8bcd 100644 --- a/internal/comic/types.go +++ b/internal/comic/types.go @@ -16,6 +16,7 @@ const ( storySystemPromptTemplate = "story_system.md" storyPromptTemplate = "story_prompt.md" storyFullPromptTemplate = "story_full_prompt.md" + manualPromptTemplate = "manual_prompt.md" coverPromptTemplate = "cover_prompt.md" storyPagePromptTemplate = "story_page_prompt.md" galleryPagePromptTemplate = "gallery_page_prompt.md" diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 60152be..f825f58 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -190,6 +190,8 @@ func TestEmbeddedPromptTemplatesRender(t *testing.T) { "ScriptName": "кирилица", "Genre": "a mystery with a surprising twist", "Style": "cinematic realism", + "Theme": "mystery", + "Prompt": "a robot reading a newspaper", "Words": "- ябълка\n- книга\n", "Bible": "Мира: млада жена, кафява коса, сини очи, червено палто.\n", "Subtitle": "КомиксФордж Приключения", @@ -230,6 +232,7 @@ func TestEmbeddedPromptTemplatesRender(t *testing.T) { "narrator_conclusion_system.md", "rendering_requirement.md", "rendering_requirement_end.md", + "manual_prompt.md", } { t.Run(name, func(t *testing.T) { got, err := cfg.RenderPrompt(name, data) |
