diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-22 09:51:43 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-22 09:51:43 +0300 |
| commit | 664c7d300c4a9326e5aebbd84a20cee5af829a0b (patch) | |
| tree | f7f13e390002ba935d06c67043395a8167411430 /internal/comic | |
| parent | c3ccee67ab204dab4ad77b44fa789a43d826321a (diff) | |
Improve comic style generation
Diffstat (limited to 'internal/comic')
| -rw-r--r-- | internal/comic/artist.go | 113 | ||||
| -rw-r--r-- | internal/comic/comic_test.go | 128 | ||||
| -rw-r--r-- | internal/comic/runner.go | 59 | ||||
| -rw-r--r-- | internal/comic/types.go | 162 |
4 files changed, 367 insertions, 95 deletions
diff --git a/internal/comic/artist.go b/internal/comic/artist.go index e1717d3..5aeccac 100644 --- a/internal/comic/artist.go +++ b/internal/comic/artist.go @@ -14,47 +14,63 @@ import ( // ArtistConfig configures comic page generation. type ArtistConfig struct { - ImageProvider provider.ImageProvider - TextProvider provider.TextProvider - Prompts PromptRenderer - OutputDir string - Style string - ComicStyles []string - RealisticStyles []string - Theme string - AspectRatio string - Language string - Script string - UltraRealistic bool - StoryPages int - GalleryPages int - PanelsPerPage int - PromptMaxChars int - PageMaxRetries int - PageRetryBase time.Duration + ImageProvider provider.ImageProvider + TextProvider provider.TextProvider + Prompts PromptRenderer + OutputDir string + Style string + StyleMode string + ComicStyles []string + RealisticStyles []string + CartoonStyles []string + Action90sStyles []string + MangaStyles []string + CyberpunkStyles []string + GoldenAgeStyles []string + HorrorStyles []string + WatercolorStyles []string + Theme string + AspectRatio string + Language string + Script string + UltraRealistic bool + StoryPages int + GalleryPages int + PanelsPerPage int + PromptMaxChars int + PageMaxRetries int + PageRetryBase time.Duration } // Artist generates comic-book pages. type Artist struct { - imageProvider provider.ImageProvider - textProvider provider.TextProvider - prompts PromptRenderer - outputDir string - style string - comicStyles []string - realisticStyles []string - theme string - aspectRatio string - language string - script string - ultraRealistic bool - storyPages int - galleryPages int - panelsPerPage int - promptMaxChars int - pageMaxRetries int - pageRetryBase time.Duration - initErr error + imageProvider provider.ImageProvider + textProvider provider.TextProvider + prompts PromptRenderer + outputDir string + style string + styleMode string + comicStyles []string + realisticStyles []string + cartoonStyles []string + action90sStyles []string + mangaStyles []string + cyberpunkStyles []string + goldenAgeStyles []string + horrorStyles []string + watercolorStyles []string + theme string + aspectRatio string + language string + script string + ultraRealistic bool + storyPages int + galleryPages int + panelsPerPage int + promptMaxChars int + pageMaxRetries int + pageRetryBase time.Duration + initErr error } type referenceImageGenerator interface { @@ -88,8 +104,16 @@ func NewArtist(cfg *ArtistConfig) *Artist { a.prompts = cfg.Prompts a.outputDir = orDefault(cfg.OutputDir, a.outputDir) a.style = cfg.Style + a.styleMode = orDefault(cfg.StyleMode, styleModeComic) a.comicStyles = append([]string(nil), cfg.ComicStyles...) a.realisticStyles = append([]string(nil), cfg.RealisticStyles...) + a.cartoonStyles = append([]string(nil), cfg.CartoonStyles...) + a.action90sStyles = append([]string(nil), cfg.Action90sStyles...) + a.mangaStyles = append([]string(nil), cfg.MangaStyles...) + a.cyberpunkStyles = append([]string(nil), cfg.CyberpunkStyles...) + a.goldenAgeStyles = append([]string(nil), cfg.GoldenAgeStyles...) + a.horrorStyles = append([]string(nil), cfg.HorrorStyles...) + a.watercolorStyles = append([]string(nil), cfg.WatercolorStyles...) a.theme = cfg.Theme a.aspectRatio = orDefault(cfg.AspectRatio, comicPageAspectRatio) a.language = orDefault(cfg.Language, a.language) @@ -98,7 +122,7 @@ func NewArtist(cfg *ArtistConfig) *Artist { if cfg.StoryPages > 0 { a.storyPages = cfg.StoryPages } - if cfg.GalleryPages > 0 { + if cfg.GalleryPages >= 0 { a.galleryPages = cfg.GalleryPages } if cfg.PanelsPerPage > 0 { @@ -128,7 +152,18 @@ func (a *Artist) DrawComicPages(ctx context.Context, storyText, bible, titleSlug } style := a.style if style == "" { - style = pickStyle(a.comicStyles, a.realisticStyles, a.ultraRealistic) + style = pickStyle( + a.comicStyles, + a.realisticStyles, + a.cartoonStyles, + a.action90sStyles, + a.mangaStyles, + a.cyberpunkStyles, + a.goldenAgeStyles, + a.horrorStyles, + a.watercolorStyles, + a.styleMode, + ) } fmt.Printf(" Comic style: %s\n", style) diff --git a/internal/comic/comic_test.go b/internal/comic/comic_test.go index 65343e6..336de78 100644 --- a/internal/comic/comic_test.go +++ b/internal/comic/comic_test.go @@ -23,6 +23,23 @@ func TestSlugify(t *testing.T) { } } +func TestShortPromptSlugFromText(t *testing.T) { + t.Parallel() + + longPrompt := "generate a logo for ComicForge which is a comic book generator displaying a superhero fighting a bad guy and speech bubbles" + + got := shortPromptSlugFromText(longPrompt) + if got == "" { + t.Fatal("shortPromptSlugFromText() returned empty slug") + } + if len(got) > maxPromptSlugLength { + t.Fatalf("shortPromptSlugFromText() length = %d, want <= %d (%q)", len(got), maxPromptSlugLength, got) + } + if strings.Contains(got, " ") { + t.Fatalf("shortPromptSlugFromText() = %q, want path-safe slug", got) + } +} + func TestParseGenerateResult(t *testing.T) { t.Parallel() @@ -371,8 +388,6 @@ func TestDrawComicPagesReturnsErrorWhenRenderFails(t *testing.T) { } func TestArtistAndRunnerEndToEndWithFakes(t *testing.T) { - t.Parallel() - originalSleep := sleep sleep = func(time.Duration) {} t.Cleanup(func() { @@ -415,6 +430,7 @@ func TestArtistAndRunnerEndToEndWithFakes(t *testing.T) { OutputDir: tmpDir, Slug: "forced-slug", NarrateEnabled: true, + GalleryPages: 1, }) runner.assemblePDF = func(outputDir, titleSlug string, imagePaths []string) (string, error) { path := filepath.Join(outputDir, titleSlug+".pdf") @@ -444,8 +460,6 @@ func TestArtistAndRunnerEndToEndWithFakes(t *testing.T) { } func TestRunnerRunPromptWritesSingleAsset(t *testing.T) { - t.Parallel() - originalLeakValidation := validateImagePromptLeakageFn validateImagePromptLeakageFn = func(context.Context, string, string, string) error { return nil } t.Cleanup(func() { @@ -455,9 +469,9 @@ func TestRunnerRunPromptWritesSingleAsset(t *testing.T) { tmpDir := t.TempDir() runner := NewRunner(&RunnerConfig{ ImageProvider: fakeImageProvider{t: t}, + TextProvider: fakeTextProvider{text: "ComicForge Superhero Logo"}, Prompts: fakePromptRenderer{}, OutputDir: tmpDir, - Slug: "manual-robot", UltraRealistic: boolPtr(false), PageMaxRetries: 1, PageRetryBase: time.Second, @@ -466,7 +480,7 @@ func TestRunnerRunPromptWritesSingleAsset(t *testing.T) { if err := runner.RunPrompt(context.Background(), "a robot reading a newspaper"); err != nil { t.Fatalf("RunPrompt() error = %v", err) } - if _, err := os.Stat(filepath.Join(tmpDir, "comics", "assets", "manual-robot", "prompt.png")); err != nil { + if _, err := os.Stat(filepath.Join(tmpDir, "comics", "assets", "comicforge-superhero-logo", "prompt.png")); err != nil { t.Fatalf("prompt image missing: %v", err) } if _, err := os.Stat(filepath.Join(tmpDir, "comics", "PDF")); !os.IsNotExist(err) { @@ -477,6 +491,44 @@ func TestRunnerRunPromptWritesSingleAsset(t *testing.T) { } } +func TestRunnerRunPromptUsesShortFallbackSlugWithoutTextProvider(t *testing.T) { + originalLeakValidation := validateImagePromptLeakageFn + validateImagePromptLeakageFn = func(context.Context, string, string, string) error { return nil } + t.Cleanup(func() { + validateImagePromptLeakageFn = originalLeakValidation + }) + + tmpDir := t.TempDir() + runner := NewRunner(&RunnerConfig{ + ImageProvider: fakeImageProvider{t: t}, + Prompts: fakePromptRenderer{}, + OutputDir: tmpDir, + UltraRealistic: boolPtr(false), + PageMaxRetries: 1, + PageRetryBase: time.Second, + }) + + longPrompt := "generate a logo for ComicForge which is a comic book generator displaying a superhero fighting a bad guy and speech bubbles" + if err := runner.RunPrompt(context.Background(), longPrompt); err != nil { + t.Fatalf("RunPrompt() error = %v", err) + } + + entries, err := os.ReadDir(filepath.Join(tmpDir, "comics", "assets")) + if err != nil { + t.Fatalf("read assets dir: %v", err) + } + if got, want := len(entries), 1; got != want { + t.Fatalf("asset dirs = %d, want %d", got, want) + } + slug := entries[0].Name() + if len(slug) > maxPromptSlugLength { + t.Fatalf("prompt slug length = %d, want <= %d (%q)", len(slug), maxPromptSlugLength, slug) + } + if _, err := os.Stat(filepath.Join(tmpDir, "comics", "assets", slug, "prompt.png")); err != nil { + t.Fatalf("prompt image missing: %v", err) + } +} + func TestRunnerRunPromptValidatesImageOutput(t *testing.T) { originalLeakValidation := validateImagePromptLeakageFn defer func() { @@ -603,15 +655,16 @@ func TestRunnerRunPromptAppliesStyleThemeAndUltraContext(t *testing.T) { } } -func TestNewRunnerUsesRealisticWeightWhenUltraRealisticUnset(t *testing.T) { +func TestNewRunnerUsesUltraFlagsForStyleMode(t *testing.T) { t.Parallel() t.Run("comic", func(t *testing.T) { + ultra := false runner := NewRunner(&RunnerConfig{ TextProvider: fakeTextProvider{text: "story"}, ImageProvider: fakeImageProvider{t: t}, Prompts: fakePromptRenderer{}, - RealisticWeight: 0, + UltraRealistic: &ultra, StoryPages: 1, GalleryPages: 0, PanelsPerPage: 1, @@ -627,16 +680,20 @@ func TestNewRunnerUsesRealisticWeightWhenUltraRealisticUnset(t *testing.T) { t.Fatal("artist is nil") } if runner.artist.ultraRealistic { - t.Fatal("ultraRealistic = true, want false when weight is 0") + t.Fatal("ultraRealistic = true, want false when explicitly disabled") + } + if got, want := runner.artist.styleMode, styleModeComic; got != want { + t.Fatalf("styleMode = %q, want %q", got, want) } }) t.Run("realistic", func(t *testing.T) { + ultra := true runner := NewRunner(&RunnerConfig{ TextProvider: fakeTextProvider{text: "story"}, ImageProvider: fakeImageProvider{t: t}, Prompts: fakePromptRenderer{}, - RealisticWeight: 1, + UltraRealistic: &ultra, StoryPages: 1, GalleryPages: 0, PanelsPerPage: 1, @@ -652,7 +709,10 @@ func TestNewRunnerUsesRealisticWeightWhenUltraRealisticUnset(t *testing.T) { t.Fatal("artist is nil") } if !runner.artist.ultraRealistic { - t.Fatal("ultraRealistic = false, want true when weight is 1") + t.Fatal("ultraRealistic = false, want true when explicitly enabled") + } + if got, want := runner.artist.styleMode, styleModeRealistic; got != want { + t.Fatalf("styleMode = %q, want %q", got, want) } }) } @@ -769,27 +829,41 @@ func TestDrawComicPagesUsesConfiguredStylePools(t *testing.T) { }) tests := []struct { - name string - ultraRealistic bool - wantStyle string + name string + styleMode string + wantStyle string }{ - {name: "comic", ultraRealistic: false, wantStyle: "comic-ink"}, - {name: "realistic", ultraRealistic: true, wantStyle: "photo-real"}, + {name: "comic", styleMode: styleModeComic, wantStyle: "comic-ink"}, + {name: "realistic", styleMode: styleModeRealistic, wantStyle: "photo-real"}, + {name: "cartoon", styleMode: styleModeCartoon, wantStyle: "cartoon-ink"}, + {name: "action90s", styleMode: styleModeAction90s, wantStyle: "action-ink"}, + {name: "manga", styleMode: styleModeManga, wantStyle: "manga-ink"}, + {name: "cyberpunk", styleMode: styleModeCyberpunk, wantStyle: "cyberpunk-ink"}, + {name: "goldenAge", styleMode: styleModeGoldenAge, wantStyle: "golden-age-ink"}, + {name: "horror", styleMode: styleModeHorror, wantStyle: "horror-ink"}, + {name: "watercolor", styleMode: styleModeWatercolor, wantStyle: "watercolor-ink"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { renderer := &recordingPromptRenderer{} artist := NewArtist(&ArtistConfig{ - ImageProvider: fakeImageProvider{t: t}, - Prompts: renderer, - OutputDir: t.TempDir(), - UltraRealistic: tt.ultraRealistic, - ComicStyles: []string{"comic-ink"}, - RealisticStyles: []string{"photo-real"}, - Language: "English", - Script: "Latin", - PanelsPerPage: 2, + ImageProvider: fakeImageProvider{t: t}, + Prompts: renderer, + OutputDir: t.TempDir(), + StyleMode: tt.styleMode, + ComicStyles: []string{"comic-ink"}, + RealisticStyles: []string{"photo-real"}, + CartoonStyles: []string{"cartoon-ink"}, + Action90sStyles: []string{"action-ink"}, + MangaStyles: []string{"manga-ink"}, + CyberpunkStyles: []string{"cyberpunk-ink"}, + GoldenAgeStyles: []string{"golden-age-ink"}, + HorrorStyles: []string{"horror-ink"}, + WatercolorStyles: []string{"watercolor-ink"}, + Language: "English", + Script: "Latin", + PanelsPerPage: 2, }) if _, err := artist.DrawComicPages(context.Background(), "story", "bible", "slug", []WordEntry{{Word: "ябълка"}}, nil); err != nil { @@ -971,6 +1045,8 @@ func (fakePromptRenderer) RenderPrompt(name string, data any) (string, error) { 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 manualTitlePromptTemplate: + return "short title prompt", nil case coverPromptTemplate, storyPagePromptTemplate, galleryPagePromptTemplate, backCoverPromptTemplate: return "image prompt", nil case blurbSystemTemplate, introSystemTemplate, conclusionSystemTemplate: @@ -1010,6 +1086,8 @@ func (r *recordingPromptRenderer) RenderPrompt(name string, data any) (string, e 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 manualTitlePromptTemplate: + return "short title prompt", nil case coverPromptTemplate, storyPagePromptTemplate, galleryPagePromptTemplate, backCoverPromptTemplate: return "image prompt", nil case blurbSystemTemplate, introSystemTemplate, conclusionSystemTemplate: diff --git a/internal/comic/runner.go b/internal/comic/runner.go index a9aa3ef..500350c 100644 --- a/internal/comic/runner.go +++ b/internal/comic/runner.go @@ -30,6 +30,7 @@ type RunnerConfig struct { Prompts PromptRenderer OutputDir string Style string + StyleMode string Theme string Language string Script string @@ -40,6 +41,13 @@ type RunnerConfig struct { RealisticWeight float64 ComicStyles []string RealisticStyles []string + CartoonStyles []string + Action90sStyles []string + MangaStyles []string + CyberpunkStyles []string + GoldenAgeStyles []string + HorrorStyles []string + WatercolorStyles []string AspectRatio string PromptMaxChars int PageMaxRetries int @@ -67,10 +75,11 @@ func NewRunner(cfg *RunnerConfig) *Runner { return r } - ultra := pickUltraRealistic(cfg.RealisticWeight) - if cfg.UltraRealistic != nil { - ultra = *cfg.UltraRealistic + styleMode := strings.TrimSpace(cfg.StyleMode) + if styleMode == "" { + styleMode = pickStyleMode(cfg.UltraRealistic) } + ultra := styleMode == styleModeRealistic r.generator = NewGenerator(&GeneratorConfig{ TextProvider: cfg.TextProvider, @@ -82,24 +91,32 @@ func NewRunner(cfg *RunnerConfig) *Runner { PanelsPerPage: cfg.PanelsPerPage, }) r.artist = NewArtist(&ArtistConfig{ - ImageProvider: cfg.ImageProvider, - TextProvider: cfg.TextProvider, - Prompts: cfg.Prompts, - OutputDir: cfg.OutputDir, - Style: cfg.Style, - ComicStyles: cfg.ComicStyles, - RealisticStyles: cfg.RealisticStyles, - Theme: cfg.Theme, - AspectRatio: cfg.AspectRatio, - Language: cfg.Language, - Script: cfg.Script, - UltraRealistic: ultra, - PromptMaxChars: cfg.PromptMaxChars, - PageMaxRetries: cfg.PageMaxRetries, - PageRetryBase: cfg.PageRetryBase, - StoryPages: cfg.StoryPages, - GalleryPages: cfg.GalleryPages, - PanelsPerPage: cfg.PanelsPerPage, + ImageProvider: cfg.ImageProvider, + TextProvider: cfg.TextProvider, + Prompts: cfg.Prompts, + OutputDir: cfg.OutputDir, + Style: cfg.Style, + StyleMode: styleMode, + ComicStyles: cfg.ComicStyles, + RealisticStyles: cfg.RealisticStyles, + CartoonStyles: cfg.CartoonStyles, + Action90sStyles: cfg.Action90sStyles, + MangaStyles: cfg.MangaStyles, + CyberpunkStyles: cfg.CyberpunkStyles, + GoldenAgeStyles: cfg.GoldenAgeStyles, + HorrorStyles: cfg.HorrorStyles, + WatercolorStyles: cfg.WatercolorStyles, + Theme: cfg.Theme, + AspectRatio: cfg.AspectRatio, + Language: cfg.Language, + Script: cfg.Script, + UltraRealistic: ultra, + PromptMaxChars: cfg.PromptMaxChars, + PageMaxRetries: cfg.PageMaxRetries, + PageRetryBase: cfg.PageRetryBase, + StoryPages: cfg.StoryPages, + GalleryPages: cfg.GalleryPages, + PanelsPerPage: cfg.PanelsPerPage, }) r.narrator = NewNarrator(&NarratorConfig{ TextProvider: cfg.TextProvider, diff --git a/internal/comic/types.go b/internal/comic/types.go index 0c94a85..5a3a111 100644 --- a/internal/comic/types.go +++ b/internal/comic/types.go @@ -75,18 +75,64 @@ var ( defaultComicStyles = []string{ "classic American comic book with bold ink outlines, halftone dots, and primary colors", - "Japanese manga with clean linework, expressive eyes, and speed lines", "retro 1960s pop art in the style of Roy Lichtenstein with thick outlines and Ben-Day dots", - "watercolor illustration with soft washes, delicate linework, and pastel tones", "European bande dessinée with detailed backgrounds, clear lines, and rich flat colors", "noir black-and-white graphic novel with heavy shadows and high contrast", "children's picture book with bright, friendly illustrations and thick outlines", "painterly oil-on-canvas comic with loose brushwork and vivid impressionist colors", - "cyberpunk neon art with glowing outlines, dark backgrounds, and electric accent colors", } - realisticStyles = defaultRealisticStyles - comicStyles = defaultComicStyles + defaultCartoonStyles = []string{ + "extremely cartoonish 1930s rubber-hose funny-animal comic style, original mouse-and-duck-era mascot energy, pie-cut eyes, button noses or beaks, white gloves, oversized shoes, noodle arms, squash-and-stretch bodies, round heads, huge expressions, flat candy colors, simple gag staging, absolutely no realism", + "vintage theatrical funny-animal cartoon comic style, redesign people as original anthropomorphic mascot characters with beaks or round snouts, pie-cut eyes, white gloves, oversized shoes, elastic limbs, cheerful slapstick poses, bold clean outlines, bright flat backgrounds, no realistic anatomy", + "old black-and-white short cartoon style translated into comic panels, original rubber-hose mascot characters, pie eyes, rubber limbs, bouncing motion arcs, simple rounded shapes, comic mischief, exaggerated face acting, hand-inked animation cels", + } + + defaultAction90sStyles = []string{ + "1990s superhero comic splash-page style like a bold caped hero punching a masked villain in a city, huge muscles, spandex costumes, flowing cape, explosive impact burst, speed lines, smoke, rubble, halftone print texture, heavy black inks, saturated primary colors", + "classic 90s action superhero cover style, oversized yellow masthead energy, caped spandex hero, armored villain, clenched fists, dramatic foreshortening, city skyline, explosions, speech balloons, thick ink outlines, Ben-Day dots", + "extreme 1990s superhero action comic style, heroic anatomy, masked villain, motion streaks, lightning impact shapes, cross-hatching, smoky city destruction, glossy saturated colors, bold comic-book lettering spaces", + } + + defaultMangaStyles = []string{ + "authentic black-and-white shonen manga page style, right-to-left manga energy, screentone shading, manga speed lines, large expressive eyes, sharp angular hair, sweat drops, impact bursts, Japanese comic panel language, minimal western color", + "high-energy manga action style, monochrome ink, dense screentones, exaggerated emotional close-ups, chibi reaction inset where appropriate, motion blur lines, diagonal panel rhythm, dramatic sound-effect shapes", + "modern manga fantasy comic style, elegant anime-inspired character designs, big eyes, spiky hair silhouettes, black ink linework, grey screentone gradients, explosive action frames, cinematic close-ups", + } + + defaultCyberpunkStyles = []string{ + "cyberpunk neon comic style, rain-slick streets, glowing signage, chrome technology, high contrast shadows, electric cyan and magenta accents", + "tech-noir cyberpunk comic style, dense futuristic cityscapes, holograms, cybernetic details, saturated neon rim lighting", + "futuristic cyberpunk action comic style, laser-lit alleys, augmented heroes, digital glitches, saturated night colors, sharp ink silhouettes", + } + + defaultGoldenAgeStyles = []string{ + "golden age superhero comic style, clean heroic poses, bright primary colors, simple confident linework, optimistic mid-century adventure energy", + "vintage golden age comic book style, bold flat colors, classic caped hero staging, simple dramatic compositions, retro print texture", + "1940s superhero serial comic style, square-jawed heroes, clear action, hand-lettered poster energy, bright optimistic palette", + } + + defaultHorrorStyles = []string{ + "vintage horror comic style, eerie shadows, gothic atmosphere, unsettling monsters, dramatic candlelit contrast, suspenseful panel staging", + "creepy supernatural horror comic style, fog, haunted architecture, anxious expressions, heavy inks, sickly green and crimson accents", + "old anthology horror comic style, graveyard mist, warped shadows, tense faces, scratchy ink texture, ominous colored lighting", + } + + defaultWatercolorStyles = []string{ + "watercolor storybook comic style, soft washes, gentle ink outlines, luminous paper texture, warm hand-painted colors, whimsical atmosphere", + "delicate watercolor children's adventure style, airy backgrounds, expressive simple characters, pastel palette, soft brush edges", + "storybook watercolor fantasy comic style, transparent layered color, soft paper grain, tender expressions, dreamy illustrated settings", + } + + realisticStyles = defaultRealisticStyles + comicStyles = defaultComicStyles + cartoonStyles = defaultCartoonStyles + action90sStyles = defaultAction90sStyles + mangaStyles = defaultMangaStyles + cyberpunkStyles = defaultCyberpunkStyles + goldenAgeStyles = defaultGoldenAgeStyles + horrorStyles = defaultHorrorStyles + watercolorStyles = defaultWatercolorStyles galleryPoses = []string{ "close-up portrait of face and shoulders, dramatic three-quarter lighting, clear gaze toward the viewer, sharp eyes and expressive face", @@ -97,6 +143,18 @@ var ( } ) +const ( + styleModeRealistic = "realistic" + styleModeComic = "comic" + styleModeCartoon = "cartoon" + styleModeAction90s = "action_90s" + styleModeManga = "manga" + styleModeCyberpunk = "cyberpunk" + styleModeGoldenAge = "golden_age" + styleModeHorror = "horror" + styleModeWatercolor = "watercolor" +) + // PromptRenderer renders external or embedded prompt templates. type PromptRenderer interface { RenderPrompt(name string, data any) (string, error) @@ -143,13 +201,41 @@ func resolveGenre(theme string, genres []string) string { return pickStoryGenre(genres) } -func pickUltraRealistic(weight float64) bool { - weight = clampRealisticWeight(weight) - return rand.Float64() < weight +func pickStyleMode(forcedUltra *bool) string { + if forcedUltra != nil { + if *forcedUltra { + return styleModeRealistic + } + return styleModeComic + } + modes := []string{ + styleModeRealistic, + styleModeComic, + styleModeCartoon, + styleModeAction90s, + styleModeManga, + styleModeCyberpunk, + styleModeGoldenAge, + styleModeHorror, + styleModeWatercolor, + } + return modes[rand.IntN(len(modes))] } -func pickStyle(comicStyles, realisticStyles []string, ultraRealistic bool) string { - if ultraRealistic { +func pickStyle( + comicStyles, + realisticStyles, + cartoonStyles, + action90sStyles, + mangaStyles, + cyberpunkStyles, + goldenAgeStyles, + horrorStyles, + watercolorStyles []string, + mode string, +) string { + switch mode { + case styleModeRealistic: if len(realisticStyles) == 0 { realisticStyles = defaultRealisticStyles } @@ -157,6 +243,62 @@ func pickStyle(comicStyles, realisticStyles []string, ultraRealistic bool) strin return "" } return realisticStyles[rand.IntN(len(realisticStyles))] + case styleModeCartoon: + if len(cartoonStyles) == 0 { + cartoonStyles = defaultCartoonStyles + } + if len(cartoonStyles) == 0 { + return "" + } + return cartoonStyles[rand.IntN(len(cartoonStyles))] + case styleModeAction90s: + if len(action90sStyles) == 0 { + action90sStyles = defaultAction90sStyles + } + if len(action90sStyles) == 0 { + return "" + } + return action90sStyles[rand.IntN(len(action90sStyles))] + case styleModeManga: + if len(mangaStyles) == 0 { + mangaStyles = defaultMangaStyles + } + if len(mangaStyles) == 0 { + return "" + } + return mangaStyles[rand.IntN(len(mangaStyles))] + case styleModeCyberpunk: + if len(cyberpunkStyles) == 0 { + cyberpunkStyles = defaultCyberpunkStyles + } + if len(cyberpunkStyles) == 0 { + return "" + } + return cyberpunkStyles[rand.IntN(len(cyberpunkStyles))] + case styleModeGoldenAge: + if len(goldenAgeStyles) == 0 { + goldenAgeStyles = defaultGoldenAgeStyles + } + if len(goldenAgeStyles) == 0 { + return "" + } + return goldenAgeStyles[rand.IntN(len(goldenAgeStyles))] + case styleModeHorror: + if len(horrorStyles) == 0 { + horrorStyles = defaultHorrorStyles + } + if len(horrorStyles) == 0 { + return "" + } + return horrorStyles[rand.IntN(len(horrorStyles))] + case styleModeWatercolor: + if len(watercolorStyles) == 0 { + watercolorStyles = defaultWatercolorStyles + } + if len(watercolorStyles) == 0 { + return "" + } + return watercolorStyles[rand.IntN(len(watercolorStyles))] } if len(comicStyles) == 0 { comicStyles = defaultComicStyles |
