From c3ccee67ab204dab4ad77b44fa789a43d826321a Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 22 Apr 2026 09:33:14 +0300 Subject: docs: add architecture document with system diagrams --- docs/ARCHITECTURE.md | 493 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 493 insertions(+) create mode 100644 docs/ARCHITECTURE.md (limited to 'docs') diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..41e34dc --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,493 @@ +# ComicForge Architecture + +This document describes the architecture of **ComicForge**, a Go CLI application that generates AI-powered comic books from vocabulary word lists or direct prompts. + +## Table of Contents + +1. [High-Level Overview](#high-level-overview) +2. [System Architecture](#system-architecture) +3. [Package Structure](#package-structure) +4. [Data Flow](#data-flow) +5. [Provider Architecture](#provider-architecture) +6. [Configuration System](#configuration-system) +7. [Comic Generation Pipeline](#comic-generation-pipeline) +8. [Key Design Patterns](#key-design-patterns) + +--- + +## High-Level Overview + +ComicForge is a command-line tool that orchestrates multiple AI providers to produce complete comic books. Given a vocabulary file containing words in a target language, it: + +1. Generates a story incorporating those words +2. Creates a character bible for visual consistency +3. Renders comic pages (cover, story pages, gallery, back cover) +4. Assembles a PDF +5. Optionally generates cinematic audio narration + +--- + +## System Architecture + +```mermaid +graph TB + subgraph CLI["Command Layer"] + CMD["cmd/comicforge/main.go
Minimal bootstrap"] + CLI_GO["cmd/comicforge/cli.go
Cobra flags & wiring"] + end + + subgraph Internal["Internal Packages"] + CFG["internal/config
Configuration loading"] + PRV["internal/provider
Provider interfaces"] + COM["internal/comic
Generation pipeline"] + IMG["internal/image
Image providers"] + TXT["internal/text
Text providers"] + TTS["internal/tts
TTS providers"] + VOC["internal/vocab
Vocabulary parsing"] + HTP["internal/httpctx
HTTP/GenAI clients"] + end + + subgraph External["External Services"] + GEMINI["Google Gemini API
(Text, Image, TTS)"] + FS["Local Filesystem
(Output, PDFs, Audio)"] + end + + CMD --> CLI_GO + CLI_GO --> CFG + CLI_GO --> COM + CLI_GO --> PRV + PRV --> TXT + PRV --> IMG + PRV --> TTS + COM --> TXT + COM --> IMG + COM --> TTS + COM --> VOC + TXT --> HTP + IMG --> HTP + TTS --> HTP + HTP --> GEMINI + COM --> FS +``` + +--- + +## Package Structure + +```mermaid +graph LR + subgraph Entry["Entry Point"] + M[main.go] + C[cli.go] + end + + subgraph Config["Configuration"] + CFG[config.go] + HOME[home.go] + end + + subgraph Providers["Provider Layer"] + P[provider.go] + TR[text/registry.go] + IR[image/registry.go] + TTR[tts/registry.go] + TG[text/gemini.go] + IG[image/gemini.go] + TTG[tts/gemini.go] + end + + subgraph Pipeline["Comic Pipeline"] + RUN[runner.go] + GEN[generator.go] + ART[artist.go] + NAR[narrator.go] + TYP[types.go] + end + + subgraph Support["Supporting Packages"] + VOC[vocab/reader.go] + HTP[httpctx/httpctx.go] + CIR[apicircuit/apicircuit.go] + end + + M --> C + C --> CFG + C --> RUN + C --> TR + C --> IR + C --> TTR + RUN --> GEN + RUN --> ART + RUN --> NAR + RUN --> VOC + GEN --> TG + ART --> IG + ART --> TG + NAR --> TTG + NAR --> TG + TR --> P + IR --> P + TTR --> P + TG --> HTP + IG --> HTP + TTG --> HTP +``` + +--- + +## Data Flow + +### Vocabulary Mode (Full Comic) + +```mermaid +sequenceDiagram + participant User + participant CLI as cli.go + participant Run as Runner + participant Gen as Generator + participant Art as Artist + participant Nar as Narrator + participant TP as TextProvider + participant IP as ImageProvider + participant TTS as TTSProvider + participant FS as Filesystem + + User->>CLI: comicforge --vocab words.txt + CLI->>Run: NewRunner(cfg) + Run->>Gen: NewGenerator() + Run->>Art: NewArtist() + Run->>Nar: NewNarrator() + + Run->>Gen: GenerateFull(entries) + Gen->>TP: GenerateText(storyPrompt) + TP-->>Gen: story + bible + title + panelScript + Gen-->>Run: GenerateResult + + Run->>Art: DrawComicPages(story, bible, title, entries, panelScript) + loop For each page + Art->>Art: Render prompt template + Art->>IP: GenerateImageWithReferences(prompt, refs) + IP-->>Art: image.png + Art->>FS: Write page file + end + Art-->>Run: []pagePaths + + Run->>FS: Save story.txt + Run->>FS: Save vocabulary.txt + Run->>FS: Save theme.txt + Run->>FS: Assemble PDF + + opt Narration enabled + Run->>Nar: Narrate(storyText) + Nar->>TP: GenerateText(intro prompt) + Nar->>TTS: GenerateAudio(chunks) + Nar->>FS: Save narration.mp3 + end + + Run-->>CLI: success + CLI-->>User: Done +``` + +### Prompt Mode (Single Image) + +```mermaid +sequenceDiagram + participant User + participant CLI as cli.go + participant Run as Runner + participant Art as Artist + participant TP as TextProvider + participant IP as ImageProvider + participant FS as Filesystem + + User->>CLI: comicforge --prompt "dragon in space" + CLI->>Run: NewRunner(cfg) + Run->>Art: NewArtist() + + Run->>Run: Generate title slug via TP + Run->>Art: generatePromptImage(prompt) + Art->>Art: Render manual prompt template + Art->>IP: GenerateImage(renderedPrompt) + IP-->>Art: image.png + Art->>FS: Write prompt.png + + Run-->>CLI: success + CLI-->>User: Done +``` + +--- + +## Provider Architecture + +ComicForge uses a **registry-based provider system** that abstracts over different AI backends. Currently, only **Google Gemini** is fully implemented; OpenAI is stubbed for future extension. + +```mermaid +classDiagram + class TextProvider { + <> + +Name() string + +IsAvailable() error + +GenerateText(ctx, prompt) (string, error) + } + + class ImageProvider { + <> + +Name() string + +IsAvailable() error + +GenerateImage(ctx, prompt, outputFile) error + } + + class AspectRatioImageProvider { + <> + +GenerateImageWithAspectRatio(ctx, prompt, outputFile, aspectRatio) error + } + + class TTSProvider { + <> + +Name() string + +IsAvailable() error + +GenerateAudio(ctx, text, outputFile) error + } + + class GeminiTextProvider { + +client *genai.Client + +model string + +GenerateText(ctx, prompt) (string, error) + } + + class GeminiImageProvider { + +client *genai.Client + +config *GeminiConfig + +GenerateImage(ctx, prompt, outputFile) error + +GenerateImageWithReferences(ctx, prompt, outputFile, refs) error + } + + class GeminiTTSProvider { + +client *genai.Client + +model string + +voice string + +GenerateAudio(ctx, text, outputFile) error + } + + class Registry~T, C~ { + +Register(name, factory) + +Resolve(name) Factory + +New(name, cfg) T + } + + TextProvider <|.. GeminiTextProvider + ImageProvider <|.. GeminiImageProvider + ImageProvider <|-- AspectRatioImageProvider + AspectRatioImageProvider <|.. GeminiImageProvider + TTSProvider <|.. GeminiTTSProvider + Registry --> TextProvider + Registry --> ImageProvider + Registry --> TTSProvider +``` + +### Provider Registry Flow + +```mermaid +graph LR + A[CLI Flags
--text-provider gemini] --> B[Config.Normalize] + B --> C[Registry.Resolve"gemini"] + C --> D[Factory Function] + D --> E[GeminiProvider] + E --> F[IsAvailable Check] + F --> G[Inject into Runner] +``` + +--- + +## Configuration System + +Configuration is loaded via **Viper** with a three-layer priority: + +1. **Defaults** (code-defined) +2. **Config file** (`config.yaml`) +3. **Environment variables** (`COMICFORGE_*`) +4. **CLI flags** (highest priority) + +```mermaid +graph TD + A[DefaultConfig] --> B[Viper SetDefaults] + C[config.yaml] --> D[Viper ReadInConfig] + E[COMICFORGE_API_GOOGLE_API_KEY] --> F[Viper AutomaticEnv] + G[CLI Flags
--text-model, --style] --> H[Apply Overrides] + + B --> I[Loaded Config] + D --> I + F --> I + H --> I + I --> J[Validate Providers] + J --> K[Return *Config] +``` + +The `Config` struct implements multiple small interfaces so each package only depends on what it needs: + +```mermaid +graph TD + CFG[Config] --> TC[TextConfig] + CFG --> IC[ImageConfig] + CFG --> TTC[TTSConfig] + CFG --> TXTC[text.Config] + CFG --> IMGC[image.Config] + CFG --> TTSC[tts.Config] + + TC --> TXT[internal/text] + IC --> IMG[internal/image] + TTC --> TTS[internal/tts] +``` + +--- + +## Comic Generation Pipeline + +The pipeline is divided into three specialized stages: + +```mermaid +graph LR + subgraph Stage1["Stage 1: Story Generation"] + direction TB + VOC[Vocabulary Words] --> GEN + GEN[Generator] --> STORY[Story Text] + GEN --> BIBLE[Character Bible] + GEN --> TITLE[Comic Title] + GEN --> PANEL[Panel Script] + end + + subgraph Stage2["Stage 2: Visual Art"] + direction TB + STORY --> ART[Artist] + BIBLE --> ART + PANEL --> ART + ART --> COVER[Cover Page] + ART --> PAGES[Story Pages] + ART --> GALLERY[Gallery Pages] + ART --> BACK[Back Cover] + end + + subgraph Stage3["Stage 3: Assembly"] + direction TB + COVER --> PDF[PDF Assembly] + PAGES --> PDF + GALLERY --> PDF + BACK --> PDF + STORY --> NAR[Narrator] + NAR --> AUDIO[MP3 Narration] + end +``` + +### Reference Image Chaining + +To maintain visual consistency across comic pages, the Artist passes previous page images as **reference images** to the image provider: + +```mermaid +graph LR + P1[Page 1] -->|ref| P2[Page 2] + P2 -->|ref| P3[Page 3] + P3 -->|ref| P4[...] + + style P1 fill:#e1f5fe + style P2 fill:#e1f5fe + style P3 fill:#e1f5fe +``` + +The `appendRef` function keeps only the **first and most recent** reference images to avoid exceeding API limits. + +--- + +## Key Design Patterns + +### Dependency Injection + +The CLI uses a `commandDeps` struct to inject provider factories, making the entire command layer testable without real API calls: + +```go +type commandDeps struct { + loadConfig func(string) (*config.Config, error) + newTextProvider func(*config.Config) (provider.TextProvider, error) + newImageProvider func(*config.Config) (provider.ImageProvider, error) + newTTSProvider func(*config.Config, string) (provider.TTSProvider, error) + newRunner func(*comic.RunnerConfig) comic.StoryRunner +} +``` + +### Template-Driven Prompts + +All LLM prompts are **Go text/template files** rendered with structured data. Templates live in `prompts/` and can be overridden at runtime via `--prompts-dir`: + +```mermaid +graph LR + DATA[Template Data
Language, Style, Bible] --> RENDER[config.RenderPrompt] + TPL[Template File
story_full_prompt.md] --> RENDER + RENDER --> PROMPT[Final Prompt] + PROMPT --> LLM[Gemini API] +``` + +### Circuit Breaker + +The `internal/apicircuit` package provides circuit breaker protection for API calls to prevent cascading failures when AI providers are unavailable. + +### Defensive Defaults + +Every component uses the **functional options / config struct** pattern with sensible defaults: + +```mermaid +graph TD + A[User Config] --> B{Field Set?} + B -->|Yes| C[Use User Value] + B -->|No| D[Use Default Value] + C --> E[Initialized Component] + D --> E +``` + +--- + +## File Output Structure + +When running with `--output .` and a comic titled "Space Adventure": + +``` +comics/ +├── assets/ +│ └── space-adventure/ +│ ├── space-adventure_cover.png +│ ├── space-adventure_page_1.png +│ ├── space-adventure_page_2.png +│ ├── space-adventure_gallery_1.png +│ ├── space-adventure_back.png +│ ├── space-adventure_story.txt +│ ├── space-adventure_comic_vocabulary.txt +│ ├── space-adventure_theme.txt +│ └── space-adventure_narration.mp3 +├── PDF/ +│ └── space-adventure.pdf +└── gallery/ + ├── space-adventure_gallery_1.png + └── ... +``` + +--- + +## Technology Stack + +| Component | Technology | +|-----------|------------| +| Language | Go 1.23+ | +| CLI Framework | Cobra | +| Configuration | Viper | +| AI Provider SDK | `google.golang.org/genai` | +| PDF Generation | Custom assembly via `internal/comic/pdf.go` | +| Audio Processing | `ffmpeg` (external dependency) | +| Testing | Go standard testing + test tables | +| Module Path | `codeberg.org/snonux/comicforge` | + +--- + +## Future Extension Points + +- **New AI Providers**: Implement `TextProvider`/`ImageProvider`/`TTSProvider` and register in the respective registries +- **New Style Modes**: Add to `StyleConfig` and update `pickStyle()` in `types.go` +- **Custom Output Formats**: The `assemblePDF` field in `Runner` is injectable for alternative exporters +- **Additional Languages**: Extend `localization.go` with new script/language mappings -- cgit v1.2.3