summaryrefslogtreecommitdiff
AgeCommit message (Collapse)Author
2026-04-06feat: mandatory speech bubbles in story panels, v0.26.0v0.26.0Paul Buetow
Two-part fix for missing dialogue bubbles: 1. Panel script prompt: each panel description must include quoted Bulgarian dialogue for the character to speak/think. At least 3 of 4 panels per page must have speech or thought text in the description. 2. Image prompt: added MANDATORY SPEECH BUBBLES block requiring at least 3 of 4 panels to contain a bubble, describing exact white-background/black-outline style for speech and cloud-shape for thought bubbles, all text Bulgarian Cyrillic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: Gemini panel script drives comic panels in narrative order, v0.25.0v0.25.0Paul Buetow
Previously story text was split mechanically into 5 sections and each panel was guessed from raw prose — causing repeated compositions and incoherent plots. Now GenerateFull requests a 20-entry panel visual script (P1-A…P5-D) from Gemini in the same API call as the story and bible. Each panel gets an explicit 1-2 sentence description of WHO/WHAT/WHERE/expression, written in chronological story order. buildStoryPagePrompt now uses these descriptions to drive the image model directly ("Draw each panel EXACTLY as described") instead of "scene N from the excerpt". Falls back to excerpt-driven prompts when the script is absent or incomplete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: add --ultra-realistic flag to force photorealistic rendering, v0.24.0v0.24.0Paul Buetow
Previously only --no-ultra-realistic existed (to force standard comic style). --ultra-realistic now forces photorealistic panels instead of the random 50/50 pick. storyUltraRealistic() updated to handle three states: forced-on, forced-off, random. README updated with --ultra-realistic example. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: --narrate opt-in narration, panel variety mandate, v0.23.0v0.23.0Paul Buetow
- Add --narrate flag (default false); narration is now opt-in to save TTS quota - Add VARIETY MANDATE to story page prompts: each panel must differ in camera angle, pose, location, lighting, and foreground — prevents repeated compositions - Wire NarrateEnabled through RunnerConfig and main.go - Update README: --narrate flag, updated output file table, narrator voice examples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06chore: bump version to 0.22.0v0.22.0Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06fix: surface Veo API error field (content policy / geographic restriction) ↵Paul Buetow
with clear message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06chore: bump version to 0.21.0, add maxPollAttempts constant and comments to ↵Paul Buetow
veo.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06test: add missing unit tests for Veo video feature (task 016)Paul Buetow
- internal/video/veo_test.go: add pageNumFromPath table test, loadGalleryImage multiple-matches edge case, saveMP4 fallback name, and NewVeoGenerator success path with mock client injection - internal/cli/prompts_test.go: add parseSelection edge cases — zero max with "all", page number exceeding max, and whitespace-only input - internal/cli/video_runner_test.go: new file covering GenerateSelectedVideos for empty/nil paths (early return) and empty/whitespace API key (error path) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06docs: document Optional Gallery Videos (Veo) feature in READMEPaul Buetow
Add a new subsection under the --story CLI section explaining the interactive Veo video-generation prompt that runs after comic creation, including the model name (veo-2.0-generate-001), MP4 output paths, the billing/cost note, and the --video=false skip flag. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: add Fyne GUI dialog for gallery video generationPaul Buetow
Adds showGalleryVideoDialog in internal/gui/dialogs.go: a multi-checkbox list (all pre-selected) lets the user choose which gallery PNGs to animate, then calls cli.GenerateSelectedVideos in a goroutine while a progress dialog indicates background work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06fix: correct video integration after comic generationPaul Buetow
PromptForGalleryVideos now returns []string paths (from recursive walk) instead of []int page numbers, so GenerateSelectedVideos always has the exact path to each gallery PNG regardless of which comics/<slug>/ subdirectory it lives in. Previously, GenerateSelectedVideos passed "." as galleryPath and called loadGalleryImage with a non-recursive filepath.Glob that could not find PNGs in subdirectories, causing video generation to always fail with "no gallery image found". Additional changes: - Add VeoGenerator.GenerateVideoFromPath that accepts a full image path and writes the MP4 next to the source PNG - Make runStoryVideos non-fatal: video errors print a warning and return nil so comic/PDF/narration outputs are never invalidated by Veo errors - Add filterPathsByPages helper and tests for the new behaviour - Add TestFindGalleryPages_Recursive to cover the comics/<slug>/ layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: add --video CLI flag to control Veo video prompt after story generationPaul Buetow
Add VideoEnabled bool (default true) to Flags, register --video bool flag in command.go, export PromptForGalleryVideos/GenerateSelectedVideos, and wire them into main.go via runStoryVideos — skipped when --video=false. Also update findGalleryPages to walk subdirectories recursively so gallery PNGs inside comics/<slug>/ are discovered from the CWD search root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: add generateSelectedVideos CLI runner for Veo video generationPaul Buetow
Implements the shared CLI runner that calls video.VeoGenerator for each selected gallery page number sequentially, printing progress and the saved MP4 path per page. Lives in internal/cli/video_runner.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: add CLI prompt helper for gallery video generationPaul Buetow
Implements promptForGalleryVideos(outputDir) which lists *_gallery_*.png files, asks the user y/n, then prompts for page selection (1,3,5 or all), returning a sorted slice of chosen page numbers. Also adds the pure helper parseSelection(input, max) with full unit tests covering all branches. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06feat: add Veo video generator package for comic gallery animationPaul Buetow
Introduces internal/video/veo.go with NewVeoGenerator and GenerateVideoFromGallery. Reads gallery PNG pages, submits them to the Veo API (veo-2.0-generate-001) as image-to-video with an 8-second 16:9 clip request, polls the long-running operation every 15 s, then saves the resulting MP4 to the output directory. Unit tests in veo_test.go cover key error paths and helper functions using mocks — no real API calls required. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06fix: track all GUI goroutines with WaitGroup and ctx.Done() (Go Mistake #62)Paul Buetow
Fire-and-forget goroutines in the tooltip setup, word-change handler, and audio playback could write to freed Fyne widgets after the window was closed. All four patterns are now fixed: - setupTooltips() and the secondary-toolbar tooltip block: replaced time.AfterFunc(500ms) with wg-tracked goroutines using select/ctx.Done(). - handleWordChange(): replaced time.AfterFunc(100ms) with the same pattern. - onWindowClosed(): added wordChangeTimer.Stop() to prevent its AfterFunc callback from firing after context cancellation. - AudioPlayer: added ctx context.Context field + SetContext(); the post-playback goroutine now guards fyne.Do with ctx.Err() == nil, and the auto-play AfterFunc is replaced with a ctx-aware goroutine. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06refactor: extract shared CardStore into internal/store to eliminate duplicationPaul Buetow
FindCardDirectory, FindOrCreateCardDirectory, GenerateCardID and the ScanWords helper previously existed in both internal/utils.go (as standalone functions) and were partially duplicated in internal/gui/card_service.go (readWordFromDir, ScanExistingWords). Introduce internal/store.CardStore as the single source of truth for all on-disk card-directory operations. Both internal/processor and internal/gui now hold a *store.CardStore field and delegate to it, removing the last copy of the directory-scanning loop from card_service.go. internal/utils.go keeps thin forwarding wrappers for callers that import the root internal package. Also adds table-driven unit tests for the new package covering FindCardDirectory (including legacy _word.txt fallback), FindOrCreateCardDirectory, and CardStore.ScanWords. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06refactor: consolidate provider factory test seams into shared named typesPaul Buetow
Define audio.ProviderFactory, image.PromptAwareClient, image.OpenAIClientFactory, image.NanoBananaClientFactory, and image.ClientFactories as the single source of truth for the three injectable factory signatures that were previously duplicated across processor.Processor, gui.Application, and gui.GenerationOrchestrator. Replace all three separate function-type fields with imageFactories image.ClientFactories + newAudioProvider audio.ProviderFactory, eliminating the parallel field declarations and the local promptAwareImageClient interface in gui/generator.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06refactor: remove Viper coupling from processor package (DIP)Paul Buetow
Resolve all Viper config values once at startup in cmd/totalrecall/main.go via the new newProcessorConfig() helper. The exported processor.Config struct replaces the internal viperConfig type, and NewProcessor now accepts *Config instead of querying Viper internally. The processor package no longer imports Viper at all. Tests are updated to pass Config values directly, eliminating the viper.Set/Reset boilerplate that coupled tests to the global singleton. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06refactor: decompose gui.Application god object into focused servicesPaul Buetow
Extract CardService (file I/O, persistence, card directory management) and GenerationOrchestrator (audio/image/phonetics generation) from the 2852-line Application struct. Application is now thin UI event-wiring. Also split all functions over 50 lines into focused helpers throughout app.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06refactor: decompose Processor god object into focused files (SRP)Paul Buetow
Extract audio coordination (voice selection, config assembly, attribution writing) into audio_coordinator.go, card directory management into card_store.go, and image downloading/searcher construction into image_downloader.go. processor.go shrinks from ~1119 to ~575 lines, each file now has a single clear responsibility. Also apply go fmt to all touched files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06chore: bump version to 0.20.0v0.20.0Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04chore: bump version to 0.19.0v0.19.0Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: progressive retry backoff for rate-limited image generationPaul Buetow
Replace fixed 10s retry pause with pageRetryBase × attempt (15s→30s→45s→60s) and increase max retries from 3 to 5. Extracted shared retryPage() helper used by both generateStoryPage and generatePageWithRetry to keep retry logic DRY. Gallery pages that fail due to Gemini rate limits now recover automatically instead of being silently skipped after just 30 seconds of retries. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: skip existing pages on re-run, add --story-slug for targeted repairv0.18.0Paul Buetow
- loadOrGenerate() in artist.go skips API calls for pages already on disk, loading bytes for the reference chain. Re-runs now only generate missing pages instead of regenerating the whole comic. - --story-slug flag forces the output directory slug so a repair run writes to the same directory as the original (e.g. --story-slug ai-jungle-quest). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04fix: remove per-page location lock to allow scene transitions in panelsPaul Buetow
The strict LOCATION rule (all 4 panels must show the exact same location) prevented natural scene progression within a page's story excerpt. Removed so characters can move through the environment across panels on a single page. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: use photography-only style pool for ultra-realistic mode, update READMEv0.17.0Paul Buetow
Ultra-realistic mode now picks from a dedicated realisticStyles pool (DSLR photography, cinematic stills, hyper-realistic) instead of the comicStyles pool. This prevents "comic strip" language from dominating the model's output when photorealistic rendering is requested. README updated: 12-page comic count (5 gallery pages), rendering mode description clarified to show both pools and the 50/50 random default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04fix: enforce per-page location from story excerpt, bump to 0.16.0v0.16.0Paul Buetow
- Add explicit LOCATION rule to story page prompt: all panels must show only the location described in that page's excerpt, not any other story location - Strengthen excerpt label so model illustrates only the current excerpt - Bump version to 0.16.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04fix: gender-neutral gallery/cover prompts, support multiple main charactersv0.15.0Paul Buetow
- Replace all "heroine/her" references in gallery poses with gender-neutral "main character/them" - Explicitly instruct model to preserve character gender, not invent new characters - Support multiple main characters in gallery page instruction - Fix cover and back cover orientation: remove "portrait orientation" (canvas is 16:9 landscape) - Bump version to 0.15.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: 5 gallery pages with distinct poses, fix single-image layoutv0.14.0Paul Buetow
- Increase galleryPageCount from 3 to 5 (12 total pages per comic) - Add 2 new gallery poses: profile close-up and power stance full-body - Fix gallery prompt: explicitly state single 16:9 landscape image, no grid/panels Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: random ultra-realistic/comic style, --no-ultra-realistic flag, updated ↵v0.13.0Paul Buetow
README - Random 50/50 rendering mode per run (ultra-realistic or standard comic style) - --no-ultra-realistic flag forces standard comic style when set - renderingRequirement const centralised and applied to all image prompts via DRY renderReq() method - Log rendering mode chosen at generation time - Update README: correct page count (10), output filenames, all --story flags documented - Bump version to 0.13.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: photorealistic rendering requirement in all image prompts, ↵v0.12.0Paul Buetow
vocabulary/theme files - Add renderingRequirement const applied to all prompts (cover, story pages, back cover, gallery) - Write <slug>_comic_vocabulary.txt with vocabulary words and story text for learning - Write <slug>_theme.txt recording the --story-theme used for easy reproduction - Enforce adult (18+) characters in generator to avoid content policy blocks on revealing clothing - Add cover/back cover retry logic (same pageMaxRetries as story pages) - Bump version to 0.12.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04feat: 5 story pages, 3 gallery pages, cover retry, adult characters, shorter ↵v0.11.0Paul Buetow
TTS chunks - Increase story pages from 3 to 5 (10 total with cover/gallery/back) - Add 3 text-free gallery pages with close-up heroine poses before back cover - Add retry logic to cover and back cover generation (same as story pages) - Enforce adult (18+) characters in generator prompt to avoid content policy blocks - Reduce TTS chunk size from 200 to 100 words to stay under 1-minute quality threshold - Increase TTS timeout from 2 to 3 minutes to avoid spurious deadline exceeded errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03fixPaul Buetow
2026-04-03feat: comic pipeline v0.10.0 — iterative chaining, title slugs, comics ↵Paul Buetow
subfolder, 16:9 - Iterative image chaining: each page receives cover + prev page as pixel-level reference (multimodal) so the model copies appearance directly, not from text alone - --story-theme flag overrides random genre pick per run - Gemini now generates a short comic title (---COMIC TITLE--- separator) used as the output slug; all files named <slug>_cover.png, <slug>_page_1.png, etc. - All comic output stored under comics/<slug>/ subdirectory - Aspect ratio changed to 16:9 (ThinkPad X1 Gen 9 screen fit; 16:10 not supported) - Bulgarian language rule moved to top of every prompt for stronger compliance - Dynamic story-specific cover masthead + publisher logo badge matching story genre - --story-theme "a thrilling space..." forces genre without touching random pool Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: append Gemini-generated cinematic epilogue at end of narrationPaul Buetow
- buildConclusion() calls gemini-2.5-flash (same SystemInstruction pattern) to write a 3-4 sentence Bulgarian cinematic epilogue (~40-60 words, ~15-30s) that reflects the story's emotion rather than recapping the plot - narrateConclusion() narrates it as a separate TTS chunk and appends it to the chunk list before final concatenation; failure is non-fatal - Narrator struct now stores apiKey for the text-generation call - Single-chunk path simplified: Rename instead of unnecessary concatenation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03fix: split long story narration into chunks to preserve voice qualityPaul Buetow
Gemini TTS voice quality degrades noticeably over long single-call texts. Split the story into ~200-word paragraph-aligned chunks, narrate each separately, then concatenate with ffmpeg's concat demuxer (copy codec, no re-encoding). Short stories (single chunk) still use one call. Temp files are written to a OS temp dir and cleaned up automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: lock age appearance across all comic pages, covers, and back coverPaul Buetow
- Bible prompt now requires EXACT age for each character (e.g. "8-year-old girl") and explicitly states age must not change across any page, cover, or back cover - Story page consistency rules now call out AGE APPEARANCE alongside face and clothing - Cover prompt: "Do NOT invent new characters or use generic stand-ins" — only story characters with matching age/face/clothing may appear - Back cover prompt: same restriction, plus "same age, same face, same clothing, same animals as in the interior pages" - Both covers updated with: "a child must never look older or younger than defined" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: traditional comic covers, animal consistency, back-cover blurb, PDF outputPaul Buetow
Cover/back improvements: - Front cover: MANDATORY TITLE instruction makes 'BULGARIAN VOCABULARY ADVENTURE' dominate the top 20% of the cover in bold masthead lettering - Back cover: Gemini generates a 2-3 sentence English marketing blurb that is embedded verbatim in the blurb-box instruction (italic display type) - Both covers now specify Silver-Age / Bronze-Age comic production layout (price box, barcode strip, cover lines) Animal consistency: - Character bible prompt now treats animals as named characters — specifies exact breed, fur colour/pattern, markings, and eye colour; instructs model never to substitute a generic animal or change markings between pages - Story page prompts add explicit animal consistency rule alongside human rules Bible/blurb reliability: - Switch helper calls to same SystemInstruction + user-content pattern that the story generator uses successfully with gemini-2.5-flash - Increase MaxOutputTokens to 8192 and timeout to 90s to match story generator - Retry pause extended to 15s for free-tier RPM recovery between rapid calls - Removed deprecated gemini-2.0-flash model references PDF generation (new internal/story/pdf.go): - AssembleComicPDF() combines all 5 images into comic.pdf via ImageMagick convert - Pages are in reading order: cover → story pages → back cover - Non-fatal: warns if ImageMagick is not installed, never blocks the pipeline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: 5-page comic with cover/back cover, 9-panel grid, and character biblePaul Buetow
- Add comic_cover.png and comic_back.png flanking the 3 story pages - Switch story pages to 3:4 portrait aspect ratio for standard comic proportions - Request 9-panel (3×3) grid layout in every story page prompt - Generate a Gemini character bible before image generation to lock characters, clothing, and setting across all 5 pages — clothing only changes if the story explicitly directs it - Fix nil-writer panic in cover warning path - Fix undefined comicPageCount reference in runner.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: character bible for consistent comic panel charactersPaul Buetow
- Generate a Gemini character bible before panel image generation - Bible describes each character (name, age, hair, eyes, clothing) and setting; it is prepended to every panel prompt to lock visual consistency - Revert to 3 separate comic_page_N.png files (4:3 aspect ratio) - bibleMaxTokens=2048 to avoid Gemini 2.5 Flash thinking-token starvation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: single tall comic strip with consistent characters and more dramatic ↵Paul Buetow
narration - Replace 3 separate comic pages with one 9:16 tall comic_strip.png - Prompt explicitly requests 3 vertically stacked panels with consistent characters - Add AspectRatio field to SearchOptions; NanoBanana uses it when set - Make cinematic narration instruction more dramatic (movie trailer narrator style) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03feat: add --story cinematic narration and comic strip generationPaul Buetow
- Generates a ~500-word Bulgarian vocabulary story from a batch file - Produces 3 comic pages via NanoBanana (90% ultra-realistic style) - Art style chosen randomly per run; override with --story-style - Cinematic Gemini TTS narration saved as story_narration.mp3 - Random voice from curated pool (Charon, Fenrir, Enceladus, Algieba, Aoede, Schedar) - Override narrator voice with --narrator-voice - Falls back to story_tts_todo.txt if narration fails - No new API key required — reuses existing GOOGLE_API_KEY - Bump version to 0.9.4 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03chore: bump version to 0.9.2Paul Buetow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03fix: disable Gemini thinking for phonetic IPA lookupPaul Buetow
Gemini 2.5 Flash runs internal reasoning by default. Those thinking tokens count against MaxOutputTokens, leaving almost no budget for the actual IPA output — hence the truncated [ˈɡlɛdɐm with no closing bracket. Set ThinkingBudget=0 in ThinkingConfig so all 200 output tokens are reserved for the IPA transcription itself. A simple dictionary-style lookup needs no chain-of-thought reasoning. Also included: audio_player layout fix (NewBorder center + TextWrapWord) so long IPA strings are not clipped in the GUI bar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03fix: show full phonetic IPA in audio player barPaul Buetow
Two issues prevented the IPA from being fully displayed: 1. audio_player.go: switched the bottom bar from NewHBox to NewBorder so the phonetic label fills the centre column instead of being squeezed to its minimum width between the stop button and the status label. Also enabled TextWrapWord so very long IPA strings wrap rather than being clipped. 2. phonetic/fetcher.go: raised MaxOutputTokens from 50 to 200 so Gemini 2.5 Flash has enough budget to emit the full IPA bracket pair without truncation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03fix: increase phonetic MaxOutputTokens from 50 to 200Paul Buetow
Gemini 2.5 Flash emits internal reasoning tokens before the IPA bracket pair. With the 50-token cap the output was silently truncated to just [ˈ, losing the rest of the transcription. 200 tokens is ample headroom for any Bulgarian word or short phrase while keeping the response focused. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03fix: resolve all golangci-lint issuesPaul Buetow
- audio/fallbacks.go: lowercase error string per Go convention - gui/app.go: remove empty else branch in keyboard shortcut handler - audio/provider_test.go: remove unused mockProvider type - update test assertions in voices_test.go and processor_test.go to match the corrected lowercase error string Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03task 00a: track background goroutines in WaitGroup and respect ctx.Done()Paul Buetow
- startFileCheckTicker: add wg.Add(1)/wg.Done() so the app shutdown handler waits for the ticker goroutine to exit (it already uses ctx.Done() to stop). - post-delete cleanup goroutine: add wg.Add(1)/wg.Done() and a ctx.Done() case so a shutdown during the 5-second polling window terminates cleanly instead of leaking the goroutine. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03task 00o: split ImageSearcher into ImageSearcher + AttributionProvider (ISP)Paul Buetow
Introduce AttributionProvider interface with GetAttribution() separate from ImageSearcher so callers only needing search/download don't carry attribution. ImageClient composes both. Downloader.searcher and NewDownloader now take ImageClient since attribution is needed when saving downloaded files. Compile-time assertions updated to ImageClient. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>