diff options
Diffstat (limited to 'internal/config')
| -rw-r--r-- | internal/config/config.go | 22 | ||||
| -rw-r--r-- | internal/config/config_test.go | 105 |
2 files changed, 123 insertions, 4 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 9267ff0..2726c46 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,7 @@ package config import ( "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" @@ -13,6 +14,7 @@ import ( "github.com/spf13/viper" "codeberg.org/snonux/comicforge/internal/provider" + "codeberg.org/snonux/comicforge/prompts" ) const ( @@ -228,16 +230,19 @@ func (c *Config) PromptPath(name string) string { func (c *Config) LoadPromptTemplate(name string) (*template.Template, error) { path := c.PromptPath(name) content, err := os.ReadFile(path) - if err != nil { + if err == nil { + return parsePromptTemplate(path, content) + } + if !errors.Is(err, fs.ErrNotExist) { return nil, fmt.Errorf("read prompt %q: %w", path, err) } - tmpl, err := template.New(filepath.Base(path)).Option("missingkey=error").Parse(string(content)) + content, err = prompts.Read(name) if err != nil { - return nil, fmt.Errorf("parse prompt %q: %w", path, err) + return nil, fmt.Errorf("read embedded prompt %q: %w", name, err) } - return tmpl, nil + return parsePromptTemplate(filepath.Base(name), content) } // RenderPrompt executes a prompt template with the provided data. @@ -255,6 +260,15 @@ func (c *Config) RenderPrompt(name string, data any) (string, error) { return builder.String(), nil } +func parsePromptTemplate(name string, content []byte) (*template.Template, error) { + tmpl, err := template.New(filepath.Base(name)).Option("missingkey=error").Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("parse prompt %q: %w", name, err) + } + + return tmpl, nil +} + func (c *Config) normalize() { c.Provider.Text = provider.NormalizeName(c.Provider.Text) c.Provider.Image = provider.NormalizeName(c.Provider.Image) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index fe370f6..ae2127a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -81,6 +81,43 @@ func TestRenderPrompt(t *testing.T) { } } +func TestRenderPromptFallsBackToEmbeddedTemplate(t *testing.T) { + cfg := DefaultConfig() + cfg.PromptsDir = t.TempDir() + + 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", + }) + if err != nil { + t.Fatalf("RenderPrompt() error = %v", err) + } + if !strings.Contains(got, "Write a ~250-word story in Bulgarian") { + t.Fatalf("RenderPrompt() = %q, want embedded story prompt", got) + } +} + +func TestRenderPromptPrefersExternalTemplate(t *testing.T) { + tmpDir := t.TempDir() + cfg := DefaultConfig() + cfg.PromptsDir = tmpDir + + if err := os.WriteFile(filepath.Join(tmpDir, "story_prompt.md"), []byte("external {{.Word}}"), 0o644); err != nil { + t.Fatalf("write prompt: %v", err) + } + + got, err := cfg.RenderPrompt("story_prompt.md", map[string]any{ + "Word": "override", + }) + if err != nil { + t.Fatalf("RenderPrompt() error = %v", err) + } + if got != "external override" { + t.Fatalf("RenderPrompt() = %q, want %q", got, "external override") + } +} + func TestRenderPromptMissingKeyReturnsError(t *testing.T) { tmpDir := t.TempDir() cfg := DefaultConfig() @@ -99,6 +136,74 @@ func TestRenderPromptMissingKeyReturnsError(t *testing.T) { } } +func TestEmbeddedPromptTemplatesRender(t *testing.T) { + cfg := DefaultConfig() + cfg.PromptsDir = t.TempDir() + + data := map[string]any{ + "Language": "Bulgarian", + "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", + "StoryText": "A short story teaser.", + "RenderingRequirement": "ULTRA-REALISTIC RENDERING", + "RenderingRequirementEnd": "FINAL LOCK — PHOTOREALISM", + "StoryBibleSeparator": "---CHARACTER GUIDE---", + "StoryTitleSeparator": "---COMIC TITLE---", + "StoryPanelSeparator": "---PANEL SCRIPT---", + "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", + "Pose": "extreme close-up portrait", + } + + for _, name := range []string{ + "story_system.md", + "story_prompt.md", + "story_full_prompt.md", + "bible_system.md", + "blurb_system.md", + "cover_prompt.md", + "story_page_prompt.md", + "gallery_page_prompt.md", + "back_cover_prompt.md", + "panel_script_prompt.md", + "narrator_cinematic.md", + "narrator_intro_system.md", + "narrator_conclusion_system.md", + "rendering_requirement.md", + "rendering_requirement_end.md", + } { + t.Run(name, func(t *testing.T) { + got, err := cfg.RenderPrompt(name, data) + if err != nil { + t.Fatalf("RenderPrompt() error = %v", err) + } + if strings.TrimSpace(got) == "" { + t.Fatal("RenderPrompt() = empty string, want rendered prompt") + } + }) + } +} + +func TestRenderPromptMissingTemplateReturnsError(t *testing.T) { + cfg := DefaultConfig() + cfg.PromptsDir = t.TempDir() + + _, err := cfg.RenderPrompt("missing_prompt.md", map[string]any{}) + if err == nil { + t.Fatal("RenderPrompt() error = nil, want error") + } + if !strings.Contains(err.Error(), "read embedded prompt") { + t.Fatalf("RenderPrompt() error = %v, want embedded read error", err) + } +} + func TestLoadRejectsUnknownProvider(t *testing.T) { tmpDir := t.TempDir() configPath := filepath.Join(tmpDir, "config.yaml") |
