diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/comic/helpers.go | 28 | ||||
| -rw-r--r-- | internal/comic/runner.go | 61 | ||||
| -rw-r--r-- | internal/comic/types.go | 1 | ||||
| -rw-r--r-- | internal/config/config.go | 10 | ||||
| -rw-r--r-- | internal/config/config_test.go | 27 |
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() |
