summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/comic/helpers.go28
-rw-r--r--internal/comic/runner.go61
-rw-r--r--internal/comic/types.go1
-rw-r--r--internal/config/config.go10
-rw-r--r--internal/config/config_test.go27
5 files changed, 121 insertions, 6 deletions
diff --git a/internal/comic/helpers.go b/internal/comic/helpers.go
index 6f5b5a8..1d51597 100644
--- a/internal/comic/helpers.go
+++ b/internal/comic/helpers.go
@@ -4,6 +4,8 @@ import (
"strings"
)
+const maxPromptSlugLength = 48
+
// slugify converts a comic title into a safe file-name component.
func slugify(title string) string {
title = strings.ToLower(strings.TrimSpace(title))
@@ -29,6 +31,32 @@ func slugify(title string) string {
return slug
}
+func shortSlug(title, fallback string) string {
+ slug := slugify(title)
+ if slug == "comic" {
+ slug = fallback
+ }
+ if slug == "" {
+ slug = fallback
+ }
+ if len(slug) <= maxPromptSlugLength {
+ return slug
+ }
+ cut := strings.LastIndex(slug[:maxPromptSlugLength+1], "-")
+ if cut < 12 {
+ cut = maxPromptSlugLength
+ }
+ slug = strings.TrimRight(slug[:cut], "-")
+ if slug == "" {
+ return fallback
+ }
+ return slug
+}
+
+func shortPromptSlugFromText(text string) string {
+ return shortSlug(text, "manual-prompt")
+}
+
// splitIntoSections divides text into n sections, preferring paragraph boundaries.
func splitIntoSections(text string, n int) []string {
paragraphs := splitParagraphs(text)
diff --git a/internal/comic/runner.go b/internal/comic/runner.go
index a5d89e6..a9aa3ef 100644
--- a/internal/comic/runner.go
+++ b/internal/comic/runner.go
@@ -212,12 +212,9 @@ func (r *Runner) RunPrompt(ctx context.Context, prompt string) error {
}
dir := orDefault(r.config.OutputDir, ".")
- slug := strings.TrimSpace(r.config.Slug)
- if slug == "" {
- slug = slugify(prompt)
- if slug == "" || slug == "comic" {
- slug = "manual-prompt"
- }
+ slug, err := r.manualPromptSlug(ctx, prompt)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: could not generate prompt title: %v\n", err)
}
assetsDir := comicAssetsDir(dir, slug)
@@ -234,6 +231,58 @@ func (r *Runner) RunPrompt(ctx context.Context, prompt string) error {
return nil
}
+func (r *Runner) manualPromptSlug(ctx context.Context, prompt string) (string, error) {
+ if forced := strings.TrimSpace(r.config.Slug); forced != "" {
+ return forced, nil
+ }
+ fallback := shortPromptSlugFromText(prompt)
+ if r.config.TextProvider == nil || r.config.Prompts == nil {
+ return fallback, nil
+ }
+ title, err := r.generateManualPromptTitle(ctx, prompt)
+ if err != nil {
+ return fallback, err
+ }
+ if titleSlug := shortPromptSlugFromText(title); titleSlug != "manual-prompt" {
+ fmt.Printf(" Prompt title: %q (slug: %s)\n", title, titleSlug)
+ return titleSlug, nil
+ }
+ return fallback, nil
+}
+
+func (r *Runner) generateManualPromptTitle(ctx context.Context, prompt string) (string, error) {
+ renderedPrompt, err := r.config.Prompts.RenderPrompt(manualTitlePromptTemplate, map[string]any{
+ "Prompt": prompt,
+ })
+ if err != nil {
+ return "", fmt.Errorf("render manual title prompt: %w", err)
+ }
+ callCtx, cancel := withTimeout(ctx, helperTimeout)
+ defer cancel()
+ text, err := r.config.TextProvider.GenerateText(callCtx, renderedPrompt)
+ if err != nil {
+ return "", fmt.Errorf("generate manual title: %w", err)
+ }
+ title := cleanManualPromptTitle(text)
+ if title == "" {
+ return "", fmt.Errorf("manual title was empty")
+ }
+ if err := validateNoPromptLeakage("manual prompt title", title); err != nil {
+ return "", err
+ }
+ return title, nil
+}
+
+func cleanManualPromptTitle(text string) string {
+ text = strings.TrimSpace(text)
+ if idx := strings.IndexByte(text, '\n'); idx >= 0 {
+ text = text[:idx]
+ }
+ text = strings.TrimSpace(text)
+ text = strings.Trim(text, `"'“”‘’.:;`)
+ return strings.Join(strings.Fields(text), " ")
+}
+
func comicsRootDir(outputRoot string) string {
root := orDefault(outputRoot, ".")
if filepath.Base(filepath.Clean(root)) == "comics" {
diff --git a/internal/comic/types.go b/internal/comic/types.go
index f6c8bcd..0c94a85 100644
--- a/internal/comic/types.go
+++ b/internal/comic/types.go
@@ -17,6 +17,7 @@ const (
storyPromptTemplate = "story_prompt.md"
storyFullPromptTemplate = "story_full_prompt.md"
manualPromptTemplate = "manual_prompt.md"
+ manualTitlePromptTemplate = "manual_title_prompt.md"
coverPromptTemplate = "cover_prompt.md"
storyPagePromptTemplate = "story_page_prompt.md"
galleryPagePromptTemplate = "gallery_page_prompt.md"
diff --git a/internal/config/config.go b/internal/config/config.go
index f778637..754c816 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -197,6 +197,7 @@ func Load(configPath string) (*Config, error) {
return nil, fmt.Errorf("decode config: %w", err)
}
+ applyEnvFallbacks(cfg)
cfg.normalize()
if err := cfg.validate(); err != nil {
return nil, err
@@ -393,3 +394,12 @@ func setDefaults(v *viper.Viper, cfg *Config) {
v.SetDefault("prompts_dir", cfg.PromptsDir)
}
+
+func applyEnvFallbacks(cfg *Config) {
+ if cfg == nil {
+ return
+ }
+ if cfg.API.GoogleAPIKey == "" {
+ cfg.API.GoogleAPIKey = os.Getenv("GOOGLE_API_KEY")
+ }
+}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index f825f58..1a53486 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -101,6 +101,33 @@ narration:
}
}
+func TestLoadFallsBackToGoogleAPIKeyEnv(t *testing.T) {
+ t.Setenv("GOOGLE_API_KEY", "fallback-key")
+
+ cfg, err := Load("")
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ if got, want := cfg.GoogleAPIKey(), "fallback-key"; got != want {
+ t.Fatalf("GoogleAPIKey() = %q, want %q", got, want)
+ }
+}
+
+func TestLoadPrefersComicForgeGoogleAPIKeyEnv(t *testing.T) {
+ t.Setenv("GOOGLE_API_KEY", "fallback-key")
+ t.Setenv("COMICFORGE_API_GOOGLE_API_KEY", "comicforge-key")
+
+ cfg, err := Load("")
+ if err != nil {
+ t.Fatalf("Load() error = %v", err)
+ }
+
+ if got, want := cfg.GoogleAPIKey(), "comicforge-key"; got != want {
+ t.Fatalf("GoogleAPIKey() = %q, want %q", got, want)
+ }
+}
+
func TestRenderPrompt(t *testing.T) {
tmpDir := t.TempDir()
cfg := DefaultConfig()