dreamloop 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {dreamloop-0.1.1 → dreamloop-0.1.2}/CHANGELOG.md +12 -0
  2. {dreamloop-0.1.1 → dreamloop-0.1.2}/PKG-INFO +28 -3
  3. {dreamloop-0.1.1 → dreamloop-0.1.2}/README.md +27 -2
  4. {dreamloop-0.1.1 → dreamloop-0.1.2}/README.zh-CN.md +25 -2
  5. {dreamloop-0.1.1 → dreamloop-0.1.2}/pyproject.toml +1 -1
  6. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/__init__.py +1 -1
  7. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/analysis.py +5 -1
  8. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/cli.py +25 -0
  9. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/core.py +166 -0
  10. dreamloop-0.1.2/src/dreamloop/images.py +201 -0
  11. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/static/style.css +78 -14
  12. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/templates/detail.html +13 -2
  13. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/templates/index.html +49 -8
  14. {dreamloop-0.1.1 → dreamloop-0.1.2}/src/dreamloop/web.py +87 -2
  15. {dreamloop-0.1.1 → dreamloop-0.1.2}/tests/test_cli.py +11 -0
  16. {dreamloop-0.1.1 → dreamloop-0.1.2}/tests/test_core_workflow.py +125 -0
  17. {dreamloop-0.1.1 → dreamloop-0.1.2}/tests/test_readme_positioning.py +9 -0
  18. {dreamloop-0.1.1 → dreamloop-0.1.2}/tests/test_web_api.py +88 -1
  19. {dreamloop-0.1.1 → dreamloop-0.1.2}/uv.lock +1 -1
  20. {dreamloop-0.1.1 → dreamloop-0.1.2}/.github/workflows/ci.yml +0 -0
  21. {dreamloop-0.1.1 → dreamloop-0.1.2}/.github/workflows/publish.yml +0 -0
  22. {dreamloop-0.1.1 → dreamloop-0.1.2}/.gitignore +0 -0
  23. {dreamloop-0.1.1 → dreamloop-0.1.2}/LICENSE +0 -0
  24. {dreamloop-0.1.1 → dreamloop-0.1.2}/docs/assets/dashboard-placeholder.svg +0 -0
  25. {dreamloop-0.1.1 → dreamloop-0.1.2}/docs/assets/dashboard-screenshot.png +0 -0
  26. {dreamloop-0.1.1 → dreamloop-0.1.2}/docs/assets/hero-dashboard.svg +0 -0
  27. {dreamloop-0.1.1 → dreamloop-0.1.2}/docs/assets/social-preview.png +0 -0
  28. {dreamloop-0.1.1 → dreamloop-0.1.2}/docs/demo-recording.md +0 -0
  29. {dreamloop-0.1.1 → dreamloop-0.1.2}/scripts/start-dreamloop.cmd +0 -0
  30. {dreamloop-0.1.1 → dreamloop-0.1.2}/tests/test_ai_config.py +0 -0
  31. {dreamloop-0.1.1 → dreamloop-0.1.2}/tests/test_packaging_release.py +0 -0
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.1.2 - 2026-06-14
4
+
5
+ DreamLoop's second patch tightens first-screen quality and introduces an explicit image-generation path:
6
+
7
+ - Reduced Dashboard hero title weight and spacing so the screenshot screen no longer visually overflows.
8
+ - Added subtle page-enter transitions and sidebar active-state motion, with `prefers-reduced-motion` support.
9
+ - Fixed the language toggle mojibake so Chinese renders as `中文`.
10
+ - Added optional image provider configuration: local visual cards by default, ComfyUI readiness checks, and a custom OpenAI-compatible cloud image endpoint.
11
+ - Added `dream_images` storage and `.dreamloop/assets/images/` output for generated image files.
12
+ - Added dream image web/API routes and `dreamloop image test`.
13
+ - Gallery now prefers real generated images and falls back to local visual memory cards.
14
+
3
15
  ## v0.1.1 - 2026-06-14
4
16
 
5
17
  This patch focuses on first-impression reliability and the trust loop around interpretation quality:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dreamloop
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A local-first AI dream journal for people who want to notice patterns in their dreams.
5
5
  Project-URL: Homepage, https://github.com/saime428/DreamLoop
6
6
  Project-URL: Repository, https://github.com/saime428/DreamLoop
@@ -38,6 +38,7 @@ Description-Content-Type: text/markdown
38
38
  - Free with Ollama. Optional DeepSeek/OpenAI or custom OpenAI-compatible endpoints.
39
39
  - CLI-first, forkable, and built for Obsidian-minded knowledge workers.
40
40
  - Detailed dream analysis that favors emotions, real-life context, and multiple verifiable interpretations.
41
+ - Optional dream images are explicit opt-in: local cards by default, OpenAI-compatible cloud endpoints only when configured, and local ComfyUI reserved for workflow-backed generation.
41
42
 
42
43
  ```bash
43
44
  git clone https://github.com/saime428/DreamLoop.git
@@ -162,6 +163,19 @@ Provider defaults:
162
163
  - `custom`: any OpenAI-compatible `/v1` endpoint, including local gateways
163
164
  - `none`: capture-only local journal mode
164
165
 
166
+ ## Image Providers
167
+
168
+ DreamLoop does not call an image API by default. Detail pages always support a local visual-memory card, and real image generation is opt-in. In v0.1.2 the cloud OpenAI-compatible path can generate files; the local ComfyUI option is a configuration and connectivity checkpoint until a workflow is attached.
169
+
170
+ ```bash
171
+ dreamloop image use local_card
172
+ dreamloop image use local_comfyui --base-url http://127.0.0.1:8188
173
+ dreamloop image use cloud_openai_compatible --model image-model --base-url https://images.example/v1
174
+ dreamloop image test
175
+ ```
176
+
177
+ Generated image files are stored under `.dreamloop/assets/images/`; the database stores local metadata such as provider, model, prompt, path, status, and error. ComfyUI readiness can be checked with `dreamloop image test`, but prompt submission stays disabled until a workflow is configured.
178
+
165
179
  ## Web App Loop
166
180
 
167
181
  The FastAPI/Jinja dashboard is intentionally lightweight:
@@ -170,8 +184,8 @@ The FastAPI/Jinja dashboard is intentionally lightweight:
170
184
  - Log: draft-first dream capture, optional reflection prompts, and AI analysis before saving
171
185
  - Detail: original dream text, detailed interpretation, reality-grounded questions, raw JSON, and an opt-in visual-memory entry point
172
186
  - Patterns: clickable calendar, recurring symbols, theme trends, and filters back into Log
173
- - Gallery: v0.1 local visual cards derived from saved dreams; image generation remains opt-in
174
- - Settings: provider selection, launch notes, local data directory, and privacy status
187
+ - Gallery: real generated images when configured, otherwise local visual cards derived from saved dreams
188
+ - Settings: AI provider, image provider, launch notes, local data directory, and privacy status
175
189
 
176
190
  DreamLoop is currently launched with `uv run dreamloop web` from a checkout or `dreamloop web` after package install. A native desktop app is a later packaging task; v0.1 stays lightweight so the local-first core remains easy to inspect and fork.
177
191
 
@@ -183,7 +197,9 @@ The same app exposes JSON endpoints:
183
197
  - `GET /api/dreams/{id}/similar`
184
198
  - `POST /api/analyze/pending`
185
199
  - `POST /api/dreams/{id}/feedback`
200
+ - `POST /api/dreams/{id}/image`
186
201
  - `GET /api/feedback/summary`
202
+ - `GET /api/images/status`
187
203
  - `POST /api/import/ics`
188
204
  - `POST /api/weather/sync`
189
205
  - `GET /api/insights/heatmap`
@@ -196,6 +212,7 @@ The same app exposes JSON endpoints:
196
212
  dreamloop.sqlite3
197
213
  config.json
198
214
  secrets.env
215
+ assets/images/
199
216
  chroma/
200
217
  exports/
201
218
  imports/
@@ -243,6 +260,14 @@ dreamloop web
243
260
  - Local feedback buttons on interpretations: resonates, not accurate, unsure.
244
261
  - Patterns summary for high-resonance themes.
245
262
 
263
+ ### v0.1.2
264
+
265
+ - Dashboard first-screen polish for screenshot quality.
266
+ - Subtle page transitions with reduced-motion support.
267
+ - Optional image provider settings for local visual cards, ComfyUI readiness checks, and custom cloud image endpoints.
268
+ - Real generated image storage in `.dreamloop/assets/images/`.
269
+ - Gallery prefers real dream images and falls back to local visual cards.
270
+
246
271
  ### v0.2
247
272
 
248
273
  - Markdown export.
@@ -12,6 +12,7 @@
12
12
  - Free with Ollama. Optional DeepSeek/OpenAI or custom OpenAI-compatible endpoints.
13
13
  - CLI-first, forkable, and built for Obsidian-minded knowledge workers.
14
14
  - Detailed dream analysis that favors emotions, real-life context, and multiple verifiable interpretations.
15
+ - Optional dream images are explicit opt-in: local cards by default, OpenAI-compatible cloud endpoints only when configured, and local ComfyUI reserved for workflow-backed generation.
15
16
 
16
17
  ```bash
17
18
  git clone https://github.com/saime428/DreamLoop.git
@@ -136,6 +137,19 @@ Provider defaults:
136
137
  - `custom`: any OpenAI-compatible `/v1` endpoint, including local gateways
137
138
  - `none`: capture-only local journal mode
138
139
 
140
+ ## Image Providers
141
+
142
+ DreamLoop does not call an image API by default. Detail pages always support a local visual-memory card, and real image generation is opt-in. In v0.1.2 the cloud OpenAI-compatible path can generate files; the local ComfyUI option is a configuration and connectivity checkpoint until a workflow is attached.
143
+
144
+ ```bash
145
+ dreamloop image use local_card
146
+ dreamloop image use local_comfyui --base-url http://127.0.0.1:8188
147
+ dreamloop image use cloud_openai_compatible --model image-model --base-url https://images.example/v1
148
+ dreamloop image test
149
+ ```
150
+
151
+ Generated image files are stored under `.dreamloop/assets/images/`; the database stores local metadata such as provider, model, prompt, path, status, and error. ComfyUI readiness can be checked with `dreamloop image test`, but prompt submission stays disabled until a workflow is configured.
152
+
139
153
  ## Web App Loop
140
154
 
141
155
  The FastAPI/Jinja dashboard is intentionally lightweight:
@@ -144,8 +158,8 @@ The FastAPI/Jinja dashboard is intentionally lightweight:
144
158
  - Log: draft-first dream capture, optional reflection prompts, and AI analysis before saving
145
159
  - Detail: original dream text, detailed interpretation, reality-grounded questions, raw JSON, and an opt-in visual-memory entry point
146
160
  - Patterns: clickable calendar, recurring symbols, theme trends, and filters back into Log
147
- - Gallery: v0.1 local visual cards derived from saved dreams; image generation remains opt-in
148
- - Settings: provider selection, launch notes, local data directory, and privacy status
161
+ - Gallery: real generated images when configured, otherwise local visual cards derived from saved dreams
162
+ - Settings: AI provider, image provider, launch notes, local data directory, and privacy status
149
163
 
150
164
  DreamLoop is currently launched with `uv run dreamloop web` from a checkout or `dreamloop web` after package install. A native desktop app is a later packaging task; v0.1 stays lightweight so the local-first core remains easy to inspect and fork.
151
165
 
@@ -157,7 +171,9 @@ The same app exposes JSON endpoints:
157
171
  - `GET /api/dreams/{id}/similar`
158
172
  - `POST /api/analyze/pending`
159
173
  - `POST /api/dreams/{id}/feedback`
174
+ - `POST /api/dreams/{id}/image`
160
175
  - `GET /api/feedback/summary`
176
+ - `GET /api/images/status`
161
177
  - `POST /api/import/ics`
162
178
  - `POST /api/weather/sync`
163
179
  - `GET /api/insights/heatmap`
@@ -170,6 +186,7 @@ The same app exposes JSON endpoints:
170
186
  dreamloop.sqlite3
171
187
  config.json
172
188
  secrets.env
189
+ assets/images/
173
190
  chroma/
174
191
  exports/
175
192
  imports/
@@ -217,6 +234,14 @@ dreamloop web
217
234
  - Local feedback buttons on interpretations: resonates, not accurate, unsure.
218
235
  - Patterns summary for high-resonance themes.
219
236
 
237
+ ### v0.1.2
238
+
239
+ - Dashboard first-screen polish for screenshot quality.
240
+ - Subtle page transitions with reduced-motion support.
241
+ - Optional image provider settings for local visual cards, ComfyUI readiness checks, and custom cloud image endpoints.
242
+ - Real generated image storage in `.dreamloop/assets/images/`.
243
+ - Gallery prefers real dream images and falls back to local visual cards.
244
+
220
245
  ### v0.2
221
246
 
222
247
  - Markdown export.
@@ -12,6 +12,7 @@
12
12
  - 支持 Ollama 零成本运行;需要云模型时再显式配置 DeepSeek、OpenAI 或 Custom OpenAI-compatible。
13
13
  - CLI 优先、易 fork,适合开发者和 Obsidian 式知识工作流。
14
14
  - 解梦更重视情绪、现实处境、多种可验证解释,而不是只给一两句玄学摘要。
15
+ - 梦境图像默认不调用外部 API;先生成本地视觉卡片,需要时再显式接入 OpenAI-compatible 云端图像接口,ComfyUI 暂作为工作流生图的配置预留。
15
16
 
16
17
  ```bash
17
18
  git clone https://github.com/saime428/DreamLoop.git
@@ -83,8 +84,8 @@ DreamLoop v0.1 的产品逻辑是一个完整闭环:
83
84
  - Log 录入:高频输入页,先写梦境,也可补充醒来感受、现实关联和个人联想,再点击 AI 分析。
84
85
  - Detail 分析:查看梦境原文、详细解梦、现实问题、自我验证问题、原始 JSON,并能给每条解释打反馈。
85
86
  - Patterns 规律:可点击梦境日历、符号趋势、主题趋势和高共鸣主题,图表能跳回 Log 过滤记录。
86
- - Gallery 记忆:v0.1 先展示本地视觉卡片;真实图像生成是 opt-in 路线,不默认调用付费 API。
87
- - Settings 信任托底:配置模型提供方、查看本地数据目录、确认密钥不回显,并阅读隐私审计说明。
87
+ - Gallery 记忆:优先展示真实生成图像;没有配置图像 provider 时展示本地视觉卡片。
88
+ - Settings 信任托底:配置 AI 提供方、图像提供方、查看本地数据目录、确认密钥不回显,并阅读隐私审计说明。
88
89
 
89
90
  ## 无 AI 模式也能用
90
91
 
@@ -125,6 +126,19 @@ Provider 说明:
125
126
  - `custom`:任意 OpenAI-compatible `/v1` 端点,包括本地网关。
126
127
  - `none`:只记录和浏览,不做 AI 分析。
127
128
 
129
+ ## 图像 Provider
130
+
131
+ DreamLoop 默认不会调用图像 API。详情页始终可以生成本地视觉记忆卡片;真实图像生成需要你显式配置。v0.1.2 中,OpenAI-compatible 云端接口可以生成文件;本地 ComfyUI 先作为配置和连通性检测入口,等绑定 workflow 后再提交生图任务。
132
+
133
+ ```bash
134
+ dreamloop image use local_card
135
+ dreamloop image use local_comfyui --base-url http://127.0.0.1:8188
136
+ dreamloop image use cloud_openai_compatible --model image-model --base-url https://images.example/v1
137
+ dreamloop image test
138
+ ```
139
+
140
+ 真实图像文件会写入 `.dreamloop/assets/images/`。数据库只保存 provider、model、prompt、路径、状态和错误信息等本地元数据。ComfyUI 可以用 `dreamloop image test` 检查服务是否在线,但在配置 workflow 前不会启用真实生图按钮。
141
+
128
142
  ## 常见排查
129
143
 
130
144
  - 页面没有分析:先运行 `dreamloop doctor`,确认 provider 是否 ready。
@@ -156,6 +170,7 @@ Obsidian 路线图:
156
170
  dreamloop.sqlite3
157
171
  config.json
158
172
  secrets.env
173
+ assets/images/
159
174
  chroma/
160
175
  exports/
161
176
  imports/
@@ -184,6 +199,14 @@ SQLite 保存梦境、分析结果、用户反馈、日历事件和天气摘要
184
199
  - Patterns 展示高共鸣主题。
185
200
  - Settings 增加隐私审计说明。
186
201
 
202
+ ### v0.1.2
203
+
204
+ - 修复 Dashboard 首屏视觉撑爆。
205
+ - 增加轻量页面切换动效,并支持 reduced motion。
206
+ - 增加本地视觉卡片、ComfyUI 连通性检查和自定义云端图像接口配置。
207
+ - 真实图像保存到 `.dreamloop/assets/images/`。
208
+ - Gallery 优先展示真实梦境图像,没有图像时回退到本地视觉卡片。
209
+
187
210
  ### v0.2
188
211
 
189
212
  - Markdown/Obsidian 导出。
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dreamloop"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "A local-first AI dream journal for people who want to notice patterns in their dreams."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """DreamLoop local-first dream journal."""
2
2
 
3
- __version__ = "0.1.1"
3
+ __version__ = "0.1.2"
@@ -203,7 +203,11 @@ def save_ai_config(
203
203
  config["base_url"] = base_url
204
204
  path = dreamloop_dir(root) / "config.json"
205
205
  path.parent.mkdir(parents=True, exist_ok=True)
206
- path.write_text(json.dumps({"ai": config}, ensure_ascii=False, indent=2), encoding="utf-8")
206
+ payload = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
207
+ if not isinstance(payload, dict):
208
+ payload = {}
209
+ payload["ai"] = config
210
+ path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
207
211
  ensure_gitignore(root)
208
212
  return path
209
213
 
@@ -10,14 +10,17 @@ import typer
10
10
 
11
11
  from .analysis import ai_status, save_ai_config, test_provider_connection
12
12
  from .core import DreamLoop
13
+ from .images import image_status, save_image_config, test_image_provider_connection
13
14
 
14
15
  app = typer.Typer(help="Local-first AI dream journal.")
15
16
  import_app = typer.Typer(help="Import local data.")
16
17
  weather_app = typer.Typer(help="Sync local context such as weather.")
17
18
  ai_app = typer.Typer(help="Configure local and optional cloud AI providers.")
19
+ image_app = typer.Typer(help="Configure optional dream image generation.")
18
20
  app.add_typer(import_app, name="import")
19
21
  app.add_typer(weather_app, name="weather")
20
22
  app.add_typer(ai_app, name="ai")
23
+ app.add_typer(image_app, name="image")
21
24
 
22
25
 
23
26
  @app.command()
@@ -83,6 +86,12 @@ def doctor() -> None:
83
86
  typer.echo(f"connection: {test_provider_connection(loop.root)}")
84
87
  if status.warning:
85
88
  typer.echo(f"warning: {status.warning}")
89
+ image = image_status(loop.root)
90
+ typer.echo(f"image_provider: {image.provider}")
91
+ typer.echo(f"image_model: {image.model or 'none'}")
92
+ typer.echo(f"image_ready: {image.ready}")
93
+ if image.warning:
94
+ typer.echo(f"image_warning: {image.warning}")
86
95
  typer.echo("secrets: hidden")
87
96
 
88
97
 
@@ -163,5 +172,21 @@ def ai_test() -> None:
163
172
  typer.echo(test_provider_connection())
164
173
 
165
174
 
175
+ @image_app.command("use")
176
+ def image_use(
177
+ provider: str,
178
+ model: Annotated[str | None, typer.Option("--model")] = None,
179
+ base_url: Annotated[str | None, typer.Option("--base-url")] = None,
180
+ ) -> None:
181
+ save_image_config(provider=provider, model=model, base_url=base_url)
182
+ status = image_status()
183
+ typer.echo(f"Image provider set to {status.provider} ({status.model or 'local card'}).")
184
+
185
+
186
+ @image_app.command("test")
187
+ def image_test() -> None:
188
+ typer.echo(test_image_provider_connection())
189
+
190
+
166
191
  def main() -> None:
167
192
  app()
@@ -19,6 +19,7 @@ from .analysis import (
19
19
  normalize_report_payload,
20
20
  normalize_text_list,
21
21
  )
22
+ from .images import ImageGenerator, build_image_generator, image_status
22
23
 
23
24
  FEEDBACK_RATINGS = {"resonates", "not_accurate", "unsure"}
24
25
 
@@ -28,11 +29,13 @@ class DreamLoop:
28
29
  self.root = Path(root or Path.cwd())
29
30
  self.data_dir = self.root / ".dreamloop"
30
31
  self.db_path = self.data_dir / "dreamloop.sqlite3"
32
+ self.images_dir = self.data_dir / "assets" / "images"
31
33
 
32
34
  def init(self) -> None:
33
35
  self.data_dir.mkdir(parents=True, exist_ok=True)
34
36
  for name in ("chroma", "exports", "imports"):
35
37
  (self.data_dir / name).mkdir(parents=True, exist_ok=True)
38
+ self.images_dir.mkdir(parents=True, exist_ok=True)
36
39
  self._ensure_gitignore()
37
40
  self._migrate()
38
41
 
@@ -118,6 +121,15 @@ class DreamLoop:
118
121
  "SELECT * FROM dream_analyses WHERE dream_id = ? AND language = ?",
119
122
  (dream_id, normalize_language(language)),
120
123
  ).fetchone()
124
+ image = db.execute(
125
+ """
126
+ SELECT * FROM dream_images
127
+ WHERE dream_id = ? AND language = ?
128
+ ORDER BY CASE WHEN status = 'complete' THEN 0 ELSE 1 END, id DESC
129
+ LIMIT 1
130
+ """,
131
+ (dream_id, normalize_language(language)),
132
+ ).fetchone()
121
133
  if analysis:
122
134
  raw_report = parse_json_any(analysis["raw_json"])
123
135
  report = normalize_report_payload(raw_report) if isinstance(raw_report, dict) else {}
@@ -133,6 +145,7 @@ class DreamLoop:
133
145
  }
134
146
  else:
135
147
  dream["analysis"] = None
148
+ dream["image"] = image_from_row(image) if image else None
136
149
  return dream
137
150
 
138
151
  def analyze_pending(
@@ -180,6 +193,7 @@ class DreamLoop:
180
193
  self.init()
181
194
  with self._connect() as db:
182
195
  db.execute("DELETE FROM user_feedback WHERE dream_id = ?", (dream_id,))
196
+ db.execute("DELETE FROM dream_images WHERE dream_id = ?", (dream_id,))
183
197
  db.execute("DELETE FROM dream_analyses WHERE dream_id = ?", (dream_id,))
184
198
  cursor = db.execute("DELETE FROM dreams WHERE id = ?", (dream_id,))
185
199
  return cursor.rowcount > 0
@@ -304,6 +318,74 @@ class DreamLoop:
304
318
  )
305
319
  return visual
306
320
 
321
+ def generate_dream_image(
322
+ self,
323
+ dream_id: int,
324
+ *,
325
+ language: str = "en",
326
+ generator: ImageGenerator | None = None,
327
+ ) -> dict[str, Any]:
328
+ self.init()
329
+ dream = self.get_dream(dream_id, language=language)
330
+ prompt = build_dream_image_prompt(dream)
331
+ status = image_status(self.root)
332
+ generator = generator or build_image_generator(self.root)
333
+ provider = getattr(generator, "provider", status.provider) if generator else status.provider
334
+ model = getattr(generator, "model", status.model) if generator else status.model
335
+ created_at = datetime.now().isoformat(timespec="seconds")
336
+ if generator is None:
337
+ error = status.warning or "Image provider is not ready."
338
+ with self._connect() as db:
339
+ image_id = self._store_image_record(
340
+ db,
341
+ dream_id,
342
+ language=language,
343
+ provider=provider,
344
+ model=model or "",
345
+ prompt=prompt,
346
+ image_path="",
347
+ status="error",
348
+ error=error,
349
+ created_at=created_at,
350
+ )
351
+ raise RuntimeError(f"{error} (image record #{image_id})")
352
+ try:
353
+ image_bytes = generator.generate(prompt)
354
+ except Exception as exc:
355
+ with self._connect() as db:
356
+ self._store_image_record(
357
+ db,
358
+ dream_id,
359
+ language=language,
360
+ provider=provider,
361
+ model=model or "",
362
+ prompt=prompt,
363
+ image_path="",
364
+ status="error",
365
+ error=str(exc),
366
+ created_at=created_at,
367
+ )
368
+ raise
369
+ filename = f"dream-{dream_id}-{normalize_language(language)}-{datetime.now().strftime('%Y%m%d%H%M%S%f')}.png"
370
+ image_path = self.images_dir / filename
371
+ image_path.write_bytes(image_bytes)
372
+ relative_path = f"assets/images/{filename}"
373
+ with self._connect() as db:
374
+ image_id = self._store_image_record(
375
+ db,
376
+ dream_id,
377
+ language=language,
378
+ provider=provider,
379
+ model=model or "",
380
+ prompt=prompt,
381
+ image_path=relative_path,
382
+ status="complete",
383
+ error="",
384
+ created_at=created_at,
385
+ )
386
+ row = db.execute("SELECT * FROM dream_images WHERE id = ?", (image_id,)).fetchone()
387
+ return image_from_row(row)
388
+
307
389
  def import_ics(self, path: str | Path) -> int:
308
390
  self.init()
309
391
  events = parse_ics(Path(path).read_text(encoding="utf-8"))
@@ -481,6 +563,40 @@ class DreamLoop:
481
563
  )
482
564
  db.execute("UPDATE dreams SET analysis_status = 'analyzed' WHERE id = ?", (dream_id,))
483
565
 
566
+ def _store_image_record(
567
+ self,
568
+ db: sqlite3.Connection,
569
+ dream_id: int,
570
+ *,
571
+ language: str,
572
+ provider: str,
573
+ model: str,
574
+ prompt: str,
575
+ image_path: str,
576
+ status: str,
577
+ error: str,
578
+ created_at: str,
579
+ ) -> int:
580
+ cursor = db.execute(
581
+ """
582
+ INSERT INTO dream_images (
583
+ dream_id, language, provider, model, prompt, image_path, status, error, created_at
584
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
585
+ """,
586
+ (
587
+ dream_id,
588
+ normalize_language(language),
589
+ provider,
590
+ model,
591
+ prompt,
592
+ image_path,
593
+ status,
594
+ error,
595
+ created_at,
596
+ ),
597
+ )
598
+ return int(cursor.lastrowid)
599
+
484
600
  def _migrate(self) -> None:
485
601
  with self._connect() as db:
486
602
  db.executescript(
@@ -526,6 +642,19 @@ class DreamLoop:
526
642
  reason TEXT NOT NULL DEFAULT '',
527
643
  created_at TEXT NOT NULL
528
644
  );
645
+
646
+ CREATE TABLE IF NOT EXISTS dream_images (
647
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
648
+ dream_id INTEGER NOT NULL REFERENCES dreams(id) ON DELETE CASCADE,
649
+ language TEXT NOT NULL DEFAULT 'en',
650
+ provider TEXT NOT NULL,
651
+ model TEXT NOT NULL DEFAULT '',
652
+ prompt TEXT NOT NULL,
653
+ image_path TEXT NOT NULL DEFAULT '',
654
+ status TEXT NOT NULL,
655
+ error TEXT NOT NULL DEFAULT '',
656
+ created_at TEXT NOT NULL
657
+ );
529
658
  """
530
659
  )
531
660
  self._migrate_dreams_table(db)
@@ -667,6 +796,43 @@ def normalize_visual_memory(payload: dict[str, Any]) -> dict[str, Any]:
667
796
  return visual
668
797
 
669
798
 
799
+ def image_from_row(row: sqlite3.Row | None) -> dict[str, Any] | None:
800
+ if row is None:
801
+ return None
802
+ image = dict(row)
803
+ image["image_url"] = image_url(image.get("image_path", ""))
804
+ return image
805
+
806
+
807
+ def image_url(image_path: str) -> str:
808
+ if not image_path:
809
+ return ""
810
+ if image_path.startswith("assets/"):
811
+ return f"/dreamloop-assets/{image_path.removeprefix('assets/')}"
812
+ return f"/dreamloop-assets/{image_path.lstrip('/')}"
813
+
814
+
815
+ def build_dream_image_prompt(dream: dict[str, Any]) -> str:
816
+ analysis = dream.get("analysis") or {}
817
+ report = analysis.get("report") or {}
818
+ parts = [
819
+ "Create a cinematic, dreamlike illustration based on this dream.",
820
+ f"Dream text: {dream.get('content', '')}",
821
+ ]
822
+ if analysis.get("summary"):
823
+ parts.append(f"Interpretive summary: {analysis['summary']}")
824
+ if analysis.get("emotional_tone"):
825
+ parts.append(f"Core emotion: {analysis['emotional_tone']}")
826
+ symbols = normalize_text_list(analysis.get("symbols"))
827
+ if symbols:
828
+ parts.append(f"Important people, objects, and scenes: {', '.join(symbols[:6])}")
829
+ details = normalize_text_list(report.get("dream_details"))
830
+ if details:
831
+ parts.append(f"Concrete dream details to preserve: {', '.join(details[:4])}")
832
+ parts.append("Avoid text, logos, UI, gore, and literal JSON. Keep it atmospheric but specific.")
833
+ return " ".join(part for part in parts if part.strip())
834
+
835
+
670
836
  def demo_samples() -> list[dict[str, Any]]:
671
837
  return [
672
838
  {