erin.dev

Back to home

๐Ÿ’ญ DreamInk

A two-stage LLM pipeline that transforms raw morning dream notes into illustrated journal entries using GPT-4 and DALL-E 3

Source Code

Dreams are vivid experiences that fade within minutes of waking. The fragmentary notes you jot down at 6am โ€” "underwater kitchen, mom was stained glass, fish in the cabinets" โ€” are barely coherent, and fed directly to an image generator, they produce generic "dreamy" results.

DreamInk solves this with a multi-model pipeline: GPT-4 first reconstructs a rich visual scene from raw notes while preserving the surreal, non-rational dream logic, then DALL-E 3 generates artwork in one of 5 configurable artistic styles. Entries accumulate into a browsable journal with tag analytics that surface recurring dream motifs over time.

The core insight: an intermediate expansion step โ€” where GPT-4 adds lighting, color, texture, and spatial detail without rationalizing impossible elements โ€” is the difference between a generic "dreamy underwater scene" and a specific illustration of your childhood kitchen with fish in the cabinets and a stained-glass mother refracting waterlight across the floor.

119 tests across 10 modules, all fully mocked with zero API calls. ~$0.11 per entry ($1.65/month at 15 entries).

๐Ÿ› ๏ธ Tech Stack

Python 3.11OpenAI SDK v1.x (GPT-4 + DALL-E 3)Pydantic v2ClickPillowhttpxTOML (tomllib)pytest

๐Ÿš€ Features

  • Two-stage LLM pipeline โ€” GPT-4 expands fragmentary dream notes into 3-4 sentence visual scenes (temp 0.9), then a second call extracts colors and mood as structured JSON (temp 0.3). Temperature is dynamically tuned based on input length.

  • 5 art style presets defined in TOML config โ€” surrealist, watercolor, film still, manga, and mixed-media collage โ€” each with carefully crafted DALL-E 3 prompt suffixes that control composition, not just aesthetic.

  • Automatic tag extraction โ€” GPT-4 classifies 3-5 thematic motifs per dream (e.g. water, flight, transformation) with a three-layer parser: JSON โ†’ regex quoted strings โ†’ regex comma-separated, so the pipeline never crashes on malformed LLM output.

  • Content policy resilience โ€” when DALL-E rejects a scene, GPT-4 automatically sanitizes it (removing objectionable elements while preserving dream structure) and retries once. A boolean flag on the recursive call prevents infinite loops.

  • 6 CLI commands via Click โ€” add, generate, compare, tags, journal, export โ€” with piped and interactive input, styled terminal output, and per-entry cost tracking.

  • Style comparison โ€” generate the same dream across all 5 styles side-by-side, with idempotent skipping of already-generated styles to avoid wasted API calls.

  • Tag analytics โ€” ASCII bar charts for frequency, O(nยทkยฒ) co-occurrence analysis, date filtering with --since, and cost projections from actual entry frequency.

  • Self-contained HTML export โ€” base64-encoded thumbnails, inlined CSS, responsive layout. Zero external dependencies โ€” the file works on a USB drive.

  • Atomic persistence โ€” flat JSON database with write-to-temp-then-os.replace() pattern for crash-safe writes. No external database needed for <500 entries/year.

๐Ÿง—โ€โ™‚๏ธ Challenges

  • Preserving surreal dream logic โ€” Without explicit prompting, GPT-4 rationalizes impossible elements: "mother made of stained glass" becomes "mother standing near a stained glass window." The most impactful prompt improvement was a single instruction telling the model not to make things make sense. This required iterative prompt engineering โ€” treating prompts as code with targeted edits for specific failure modes.

  • Two-call expansion design โ€” Combining creative scene expansion and structured metadata extraction into a single GPT-4 call produced less reliable JSON output. Separating them into two calls with different temperatures (0.9 creative, 0.3 structured) made both more reliable โ€” the creative call focuses purely on vivid description without worrying about JSON formatting.

  • DALL-E 3 prompt rewriting โ€” DALL-E 3 internally rewrites every prompt, sometimes dropping surreal elements entirely. Capturing and persisting the revised_prompt was essential for debugging. For one dream about "a forest made of clocks," DALL-E 3 rewrote it to remove the surreal element. Logging intermediate state across multi-model pipelines is critical.

  • Choosing watercolor as default โ€” Photorealistic rendering produced uncanny results because DALL-E couldn't resolve the inherent ambiguity in dream imagery. Watercolor's soft edges and bleeding colors are features, not bugs, when the source material is fuzzy memory. The lesson: aesthetic framing of AI output can matter as much as its technical quality.

  • Structured output parsing from LLMs โ€” GPT-4 doesn't always return valid JSON. The tag parser uses a three-layer fallback strategy: json.loads() โ†’ regex extraction of quoted strings โ†’ regex extraction with stopword filtering. Each layer is simpler and less precise, but the pipeline never crashes. This "parse optimistically, degrade gracefully" pattern is reusable for any LLM-structured-output task.

๐Ÿง  Learnings

  • Intermediate reasoning steps dramatically improve multi-model pipelines. Direct dream-notes-to-image produced generic results. The GPT-4 expansion step was the difference between "a dreamy underwater scene" and a specific childhood kitchen with fish in the cabinets. This principle โ€” that an intermediate expansion/reasoning step before generation improves output quality โ€” applies broadly across LLM pipeline design.

  • Temperature is a precision dial, not just a creativity slider. Scene expansion (creative, temp 0.9), metadata extraction (structured, temp 0.3), tag classification (deterministic, temp 0.3), and content sanitization (moderate rewrite, temp 0.5) each use different temperatures matched to the task. Short inputs (<10 words) get a lower temperature to prevent hallucination on sparse signal.

  • Dependency injection makes LLM code testable without mocking frameworks. Every API-calling function accepts optional client and config parameters โ€” production code uses defaults, tests pass in MagicMock(). MagicMock's auto-nested attributes naturally handle client.chat.completions.create(). No @patch decorators needed, and 119 tests run in under a second with zero API calls.

  • No LangChain was a deliberate choice, not an oversight. The entire pipeline orchestration is ~50 lines of Python. Using a framework for a two-call pipeline would signal reflexive dependency adoption rather than thoughtful tool selection. The hand-rolled retry logic (exponential backoff, ~30 lines) is easier to read when retry conditions are domain-specific (retry 429/5xx, don't retry 400/401).

  • Dream tags revealed patterns I didn't consciously notice. After 30+ entries, the tag frequency chart showed water appeared in 40% of my dreams. Even simple metadata extraction on personal data can surface surprising insights โ€” an accidental self-analysis feature that became the most personally valuable outcome.