summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/config/config.go22
-rw-r--r--internal/config/config_test.go105
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")