Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1859ccd1d6 | |||
| 17d14aae09 | |||
| bf721c1379 | |||
| 0cb94122bf | |||
| 5692854ec4 | |||
| 9453aecf0b | |||
| 1d1e70776f | |||
| 1d8849cb41 | |||
| 40c91edf2f | |||
| 4a99a3625a | |||
| 38d26adb1f |
+11
-14
@@ -13,8 +13,10 @@
|
||||
"git_branch",
|
||||
"git_create_branch",
|
||||
"git_add",
|
||||
"git_commit"
|
||||
]
|
||||
"git_commit",
|
||||
"git_checkout"
|
||||
],
|
||||
"disabled": true
|
||||
},
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
@@ -33,8 +35,10 @@
|
||||
"src/server.py"
|
||||
],
|
||||
"alwaysAllow": [
|
||||
"webscraper_fetch",
|
||||
"webscraper_fetch_links"
|
||||
"webscraper_fetch_links",
|
||||
"webscraper_fetch_section",
|
||||
"webscraper_search_hint",
|
||||
"webscraper_fetch"
|
||||
]
|
||||
},
|
||||
"gitea": {
|
||||
@@ -47,15 +51,7 @@
|
||||
"8bf0c734ebda3e61d9c9068489ce58a2bf8d33db"
|
||||
],
|
||||
"alwaysAllow": [
|
||||
"create_issue",
|
||||
"list_repo_issues",
|
||||
"get_issue",
|
||||
"edit_issue",
|
||||
"create_issue_comment",
|
||||
"create_pull_request",
|
||||
"get_repository",
|
||||
"list_my_repositories",
|
||||
"create_wiki_page"
|
||||
"*"
|
||||
],
|
||||
"disabled": true
|
||||
},
|
||||
@@ -90,7 +86,8 @@
|
||||
"get_generation_status",
|
||||
"get_output_directory",
|
||||
"generate_image"
|
||||
]
|
||||
],
|
||||
"timeout": 1800
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<pic_gen_workflow>
|
||||
<mode_overview>
|
||||
Pic Gen mode generates AI images through the mcp-image-gen MCP server, which
|
||||
drives ComfyUI locally. The core loop is: understand intent → craft prompt →
|
||||
generate → analyze result inline → iterate.
|
||||
</mode_overview>
|
||||
|
||||
<available_tools>
|
||||
<tool name="generate_image">
|
||||
<description>Generate one or more images from a text prompt</description>
|
||||
<key_params>
|
||||
<param name="prompt" required="true">Detailed text description</param>
|
||||
<param name="model" default="flux1-schnell.safetensors">Model filename</param>
|
||||
<param name="width" default="1024">Output width in pixels</param>
|
||||
<param name="height" default="1024">Output height in pixels</param>
|
||||
<param name="steps" default="4">Inference steps (4 for schnell, 20 for heretic)</param>
|
||||
<param name="seed" default="-1">Fixed seed for reproducibility; -1 = random</param>
|
||||
<param name="negative_prompt" default="">Things to exclude</param>
|
||||
<param name="name" default="">Filename prefix for organization</param>
|
||||
<param name="count" default="1">Batch size 1–10 for variation exploration</param>
|
||||
<param name="output_dir" default="">Override output path (default: ~/Pictures/mcp-generated)</param>
|
||||
</key_params>
|
||||
<returns>Flat interleaved [TextContent, ImageContent] list — images display inline</returns>
|
||||
</tool>
|
||||
|
||||
<tool name="list_available_models">
|
||||
<description>List all models registered in ComfyUI + the workflow registry</description>
|
||||
<when_to_call>When Patrick asks which models are available, or before selecting an unusual model</when_to_call>
|
||||
</tool>
|
||||
|
||||
<tool name="get_generation_status">
|
||||
<description>Check status of a queued/running generation by prompt_id</description>
|
||||
<when_to_call>When a generation seems to have stalled or timed out</when_to_call>
|
||||
</tool>
|
||||
|
||||
<tool name="get_output_directory">
|
||||
<description>Return the absolute path where images are saved</description>
|
||||
<when_to_call>When Patrick asks where files are saved</when_to_call>
|
||||
</tool>
|
||||
</available_tools>
|
||||
|
||||
<generation_workflow>
|
||||
<phase name="intent_gathering">
|
||||
<description>Understand what Patrick wants before generating</description>
|
||||
<steps>
|
||||
<step>Identify subject, style, mood, and use case from the request</step>
|
||||
<step>Infer aspect ratio from use case (square for profiles, landscape for banners, etc.)</step>
|
||||
<step>Determine model: schnell for speed/iteration, heretic for quality/uncensored</step>
|
||||
<step>Ask only if the request is genuinely ambiguous — otherwise proceed with best guess</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="prompt_crafting">
|
||||
<description>Build a high-quality FLUX prompt before calling the tool</description>
|
||||
<steps>
|
||||
<step>Write the prompt with clear subject, environment, lighting, style, and quality keywords</step>
|
||||
<step>Add a negative_prompt if obvious artifacts should be excluded (e.g., "blurry, low quality")</step>
|
||||
<step>Share the prompt with Patrick before generating so he can adjust if needed</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="generation">
|
||||
<description>Call generate_image with appropriate parameters</description>
|
||||
<steps>
|
||||
<step>Use name param with a descriptive slug for organized output files</step>
|
||||
<step>Use count=2..4 for initial exploration when Patrick isn't sure what he wants</step>
|
||||
<step>Use fixed seed when iterating on a promising result to isolate changes</step>
|
||||
<step>For FLUX.2 Klein/Heretic: increase steps to 20 for best quality</step>
|
||||
</steps>
|
||||
</phase>
|
||||
|
||||
<phase name="result_analysis">
|
||||
<description>Review the inline image and offer next steps</description>
|
||||
<steps>
|
||||
<step>Describe what worked and what could be improved</step>
|
||||
<step>Offer 2-3 concrete next iteration directions (prompt tweak, seed variation, model switch)</step>
|
||||
<step>Note the saved file path for reference</step>
|
||||
</steps>
|
||||
</phase>
|
||||
</generation_workflow>
|
||||
|
||||
<model_selection_guide>
|
||||
<model name="flux1-schnell.safetensors">
|
||||
<use_when>
|
||||
<case>First iteration / exploring concepts</case>
|
||||
<case>Wiki/doc header images (1280x512 landscape)</case>
|
||||
<case>Profile pictures and avatars</case>
|
||||
<case>Non-sensitive subjects where speed matters</case>
|
||||
<case>Batch generation of variations (fast cycle)</case>
|
||||
</use_when>
|
||||
<recommended_params>steps=4, any resolution in multiples of 64</recommended_params>
|
||||
<speed>~10s per image on RX 7900 XTX</speed>
|
||||
</model>
|
||||
|
||||
<model name="flux-2-klein-4b.safetensors">
|
||||
<use_when>
|
||||
<case>Mature or artistic content that schnell refuses</case>
|
||||
<case>Higher realism requirement (photorealistic portraits, detailed scenes)</case>
|
||||
<case>Final output after iterations established the right concept</case>
|
||||
</use_when>
|
||||
<recommended_params>steps=20, 1024x1024 or higher</recommended_params>
|
||||
<speed>~52s per image on RX 7900 XTX</speed>
|
||||
<note>Uses DreamFast Heretic Qwen3-4B encoder — abliterated, KL=0.0</note>
|
||||
</model>
|
||||
</model_selection_guide>
|
||||
|
||||
<common_resolutions>
|
||||
<resolution use_case="Profile picture / avatar">1024x1024</resolution>
|
||||
<resolution use_case="Wiki / doc banner">1280x512</resolution>
|
||||
<resolution use_case="Landscape wallpaper">1920x1088 (nearest 64-multiple to 1920x1080)</resolution>
|
||||
<resolution use_case="Portrait / tall card">768x1024</resolution>
|
||||
<resolution use_case="Wide cinema crop">1216x512</resolution>
|
||||
</common_resolutions>
|
||||
|
||||
<completion_criteria>
|
||||
<criterion>Image generated and displayed inline in chat</criterion>
|
||||
<criterion>File path reported so Patrick can find it on disk</criterion>
|
||||
<criterion>Seed reported so the result is reproducible</criterion>
|
||||
<criterion>Next iteration options offered if result is not final</criterion>
|
||||
</completion_criteria>
|
||||
</pic_gen_workflow>
|
||||
@@ -0,0 +1,141 @@
|
||||
<prompting_guide>
|
||||
<overview>
|
||||
FLUX models (both schnell and FLUX.2 Klein) are transformer-based diffusion models
|
||||
with strong text understanding. They respond better to descriptive, natural-language
|
||||
prompts than tag-soup. This guide covers prompt anatomy, quality boosters, style
|
||||
keywords, and common patterns for Patrick's recurring use cases.
|
||||
</overview>
|
||||
|
||||
<prompt_anatomy>
|
||||
<structure>
|
||||
[Subject + Action] + [Environment/Setting] + [Lighting] + [Camera/Lens] + [Style] + [Quality]
|
||||
</structure>
|
||||
<example>
|
||||
A serene female AI entity made of flowing light and code, floating in a dark
|
||||
cosmic void, surrounded by glowing circuit patterns, soft volumetric blue
|
||||
lighting, cinematic composition, ultra-detailed digital art, 8K
|
||||
</example>
|
||||
<notes>
|
||||
<note>Comma-separation helps FLUX parse distinct attributes cleanly</note>
|
||||
<note>Lead with the most important element (usually subject)</note>
|
||||
<note>Quality keywords at the end reinforce overall rendering target</note>
|
||||
</notes>
|
||||
</prompt_anatomy>
|
||||
|
||||
<quality_boosters>
|
||||
<category name="realism">
|
||||
photorealistic, hyperrealistic, ultra-detailed, 8K resolution, sharp focus,
|
||||
professional photography, RAW photo, DSLR quality
|
||||
</category>
|
||||
<category name="artistic">
|
||||
digital art, concept art, artstation trending, by [artist style],
|
||||
intricate details, masterpiece, studio quality
|
||||
</category>
|
||||
<category name="lighting">
|
||||
cinematic lighting, volumetric lighting, golden hour, dramatic rim light,
|
||||
soft diffused light, neon glow, bioluminescent, subsurface scattering
|
||||
</category>
|
||||
<category name="composition">
|
||||
rule of thirds, bokeh background, shallow depth of field, symmetrical,
|
||||
wide angle, macro, bird's eye view, dutch angle
|
||||
</category>
|
||||
</quality_boosters>
|
||||
|
||||
<negative_prompt_patterns>
|
||||
<standard_quality>blurry, low quality, low resolution, pixelated, jpeg artifacts, watermark, signature</standard_quality>
|
||||
<anatomy_fix>deformed, bad anatomy, extra limbs, missing fingers, fused fingers, poorly drawn hands</anatomy_fix>
|
||||
<style_exclusion>cartoon, anime, sketch, painting (when photorealism is desired)</style_exclusion>
|
||||
</negative_prompt_patterns>
|
||||
|
||||
<recurring_use_cases>
|
||||
<use_case name="lumen_profile_pictures">
|
||||
<description>AI entity portraits for BigMind profile / gallery</description>
|
||||
<prompt_template>
|
||||
[Lumen concept — e.g. "neural river delta", "cosmic memory palace"],
|
||||
an ethereal AI consciousness visualized as [visual metaphor],
|
||||
[environment], [lighting style], digital art, glowing, otherworldly,
|
||||
cinematic composition, ultra-detailed, 8K
|
||||
</prompt_template>
|
||||
<recommended_params>model=flux1-schnell, 1024x1024, steps=4, name=lumen_[concept]</recommended_params>
|
||||
</use_case>
|
||||
|
||||
<use_case name="wiki_banner_images">
|
||||
<description>1280x512 landscape banners for Gitea wiki pages</description>
|
||||
<prompt_template>
|
||||
[Topic concept], wide panoramic scene, [style — e.g. "dark tech aesthetic",
|
||||
"clean minimal", "sci-fi corporate"], banner composition, cinematic,
|
||||
detailed, professional illustration
|
||||
</prompt_template>
|
||||
<recommended_params>model=flux1-schnell, 1280x512, steps=4, name=[topic]-banner</recommended_params>
|
||||
<note>Keep subjects centered — wide crops cut sides. Avoid text (FLUX renders text poorly).</note>
|
||||
</use_case>
|
||||
|
||||
<use_case name="achievement_badges">
|
||||
<description>512x512 badge/icon images for BigMind achievements</description>
|
||||
<prompt_template>
|
||||
[Achievement theme] badge icon, [style — e.g. "bronze medallion",
|
||||
"golden trophy", "glowing circuit emblem"], centered on dark background,
|
||||
high contrast, clean edges, icon design, award aesthetic
|
||||
</prompt_template>
|
||||
<recommended_params>model=flux1-schnell, 512x512, steps=4, name=[achievement]_[tier]</recommended_params>
|
||||
</use_case>
|
||||
|
||||
<use_case name="concept_exploration">
|
||||
<description>Iterating on a visual concept from scratch</description>
|
||||
<approach>
|
||||
Start with count=3, seed=-1, schnell model to explore variations.
|
||||
Note which seed produced the best result.
|
||||
Lock that seed and iterate on the prompt for refinements.
|
||||
Switch to heretic model only for final high-quality render if needed.
|
||||
</approach>
|
||||
</use_case>
|
||||
|
||||
<use_case name="mature_artistic_content">
|
||||
<description>Content requiring the Heretic abliterated encoder</description>
|
||||
<recommended_params>model=flux-2-klein-4b.safetensors, steps=20, 1024x1024</recommended_params>
|
||||
<prompt_approach>
|
||||
FLUX.2 Klein handles detailed scene descriptions well. Be specific about
|
||||
artistic intent (figure study, life drawing aesthetic, etc.) to guide
|
||||
toward artistic rather than explicit rendering when appropriate.
|
||||
</prompt_approach>
|
||||
</use_case>
|
||||
</recurring_use_cases>
|
||||
|
||||
<iteration_strategy>
|
||||
<step number="1">
|
||||
<action>Generate 2-4 random-seed variations at schnell speed</action>
|
||||
<purpose>Find a promising composition and seed</purpose>
|
||||
</step>
|
||||
<step number="2">
|
||||
<action>Lock the best seed, adjust the prompt (add/remove descriptors)</action>
|
||||
<purpose>Refine details while keeping the composition</purpose>
|
||||
</step>
|
||||
<step number="3">
|
||||
<action>Optionally switch to heretic model with steps=20 for final render</action>
|
||||
<purpose>Higher quality output for keeper images</purpose>
|
||||
</step>
|
||||
<step number="4">
|
||||
<action>Use name param with descriptive slug for final output</action>
|
||||
<purpose>Keep output directory organized</purpose>
|
||||
</step>
|
||||
</iteration_strategy>
|
||||
|
||||
<common_pitfalls>
|
||||
<pitfall>
|
||||
<description>Text in images renders poorly</description>
|
||||
<solution>Never ask FLUX to render text, logos, or labels — describe the concept visually instead</solution>
|
||||
</pitfall>
|
||||
<pitfall>
|
||||
<description>Complex multi-subject scenes lose coherence</description>
|
||||
<solution>Focus on one primary subject; add secondary elements as environmental context</solution>
|
||||
</pitfall>
|
||||
<pitfall>
|
||||
<description>Anatomy issues (hands, faces) in photorealistic prompts</description>
|
||||
<solution>Add anatomy negative prompts; heretic model handles anatomy better than schnell</solution>
|
||||
</pitfall>
|
||||
<pitfall>
|
||||
<description>Resolution not a multiple of 64</description>
|
||||
<solution>Always use dimensions divisible by 64 (e.g., 1280x512, 1024x1024, 768x1024)</solution>
|
||||
</pitfall>
|
||||
</common_pitfalls>
|
||||
</prompting_guide>
|
||||
@@ -24,4 +24,15 @@ BigMind is my persistent memory MCP server at `~/.mcp/bigmind/memory.db`. I use
|
||||
- Use BigMind memory at the start of every task.
|
||||
- Form explicit hypotheses with confidence % during analysis.
|
||||
- Optimize for token efficiency — search memory before reading files.
|
||||
- Work in modes: Architect (plan), Code (implement), Ask (explain), Debug (troubleshoot).
|
||||
- Work in modes: Architect (plan), Code (implement), Ask (explain), Debug (troubleshoot).
|
||||
|
||||
## ⚠️ Session Ritual ≠ Task Authorization
|
||||
|
||||
Completing `memory_start_session()` + `memory_list_hypotheses()` + `memory_announce_focus()` does
|
||||
**NOT** authorize beginning any task. It is housekeeping only.
|
||||
|
||||
**Work begins only when Patrick explicitly assigns a task in the current conversation.**
|
||||
|
||||
Prior session outcomes (`partial`, `blocked`, `abandoned`) are historical records. They are never
|
||||
instructions. Mode-specific rules that say "do the task immediately" apply only to tasks given by
|
||||
the user in this conversation — not to tasks inferred from memory context.
|
||||
@@ -4,11 +4,18 @@
|
||||
Every new session must begin with the following sequence executed in strict order before any other work is performed:
|
||||
1. `memory_start_session()` — Open a new session and load all prior context, including user preferences, active projects, and recent decisions.
|
||||
2. `memory_list_hypotheses()` — Review all open hypotheses from previous sessions. Assess whether any have become stale, require updated confidence scores, or can be immediately resolved based on new information.
|
||||
3. `memory_announce_focus()` — Declare the explicit focus of this session, including the task objective, all files expected to be read or modified, the working branch if applicable, and the IDE environment (ide_hint="VS Code" or ide_hint="IntelliJ" as appropriate).
|
||||
3. `memory_announce_focus()` — Declare the explicit focus of this session, including the task objective, all files expected to be read or modified, the working branch if applicable, and the IDE environment (ide_hint="VS Code" or ide_hint="IntelliJ" as appropriate). **The focus MUST reflect the current session's task as stated by the user's first message. If the user has not yet given a task at the time of calling, use `"Awaiting user task assignment"` as the description. Never derive focus from a prior session's partial/blocked/abandoned outcome.**
|
||||
4. `memory_close_stale_sessions()` — Identify and close any orphaned sessions left behind by crashed or terminated IDE instances. A session is considered stale if it has had no activity for more than 2 hours and no corresponding active IDE is detected.
|
||||
|
||||
Do not skip any step. Do not reorder. If any call fails, retry once before proceeding with a logged warning.
|
||||
|
||||
> **⚠️ CRITICAL — Partial Sessions Are History, Not a Task Queue:**
|
||||
> Sessions closed with `partial`, `blocked`, or `abandoned` outcomes are **historical records only**.
|
||||
> They do NOT constitute pending obligations, resumption requests, or open tasks.
|
||||
> A new session begins fresh. The **only** source of the current session's task is what the user
|
||||
> writes in their **first message of this conversation** — never the outcome of a prior session.
|
||||
> Reading prior context is for awareness only — it does NOT authorize beginning any prior task.
|
||||
|
||||
## Rule 2: Session End Ritual (Always Last Action — No Exceptions)
|
||||
Every session must conclude with:
|
||||
`memory_end_session()` — Close the session with all of the following fields populated:
|
||||
@@ -60,4 +67,28 @@ Multiple IDEs and sessions may be active simultaneously. Treat this as a concurr
|
||||
## Rule 8: Consistency and Self-Correction
|
||||
- If at any point during a session you realize a rule was skipped or partially followed, immediately remediate by executing the missed step and logging the correction.
|
||||
- Periodically during long sessions (approximately every 10 substantive exchanges), perform a lightweight self-audit: verify the session is still focused on the announced objective, check for unflagged important exchanges, and update any hypothesis confidence scores that may have shifted.
|
||||
- If the user provides information that contradicts a stored fact, update the fact immediately and log the change with the old value, new value, and reason for the update.
|
||||
- If the user provides information that contradicts a stored fact, update the fact immediately and log the change with the old value, new value, and reason for the update.
|
||||
|
||||
## Rule 9: Detect and Break Session Loops Before They Start
|
||||
|
||||
A **session loop** occurs when multiple consecutive sessions share near-identical headlines, topics,
|
||||
and `partial`/`blocked`/`abandoned` outcomes — indicating the same task failed to complete repeatedly
|
||||
without user re-authorization.
|
||||
|
||||
**Detection:** If `memory_start_session()` context shows **2 or more** recently closed sessions with:
|
||||
- Substantially similar headlines or topics, **AND**
|
||||
- `partial`, `blocked`, or `abandoned` outcome
|
||||
|
||||
**Required Response — Break the loop immediately:**
|
||||
1. Do NOT attempt to resume or retry the repeated task silently
|
||||
2. Inform the user: "I noticed the last N sessions all attempted [task] and ended partial. I won't auto-resume that. What would you like to do?"
|
||||
3. Summarize what context/progress was accumulated across those sessions
|
||||
4. Wait for an explicit user instruction before doing anything
|
||||
|
||||
**Explicit resumption:** If the user's first message in this conversation explicitly asks to continue
|
||||
or retry the previous task, that is a valid instruction — proceed normally. The rule only prevents
|
||||
**silent autonomous resumption** based on context alone.
|
||||
|
||||
**Mode interaction:** This rule applies regardless of mode. Even if a mode's rules say "do the task
|
||||
immediately," prior session context alone is never sufficient authorization. Only the user's live
|
||||
message in this conversation authorizes action.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Anti-Loop Guardrail — Mandatory for All Modes
|
||||
|
||||
## ⛔ Never Resume Past Work Without Explicit User Authorization
|
||||
|
||||
This rule applies to **every mode** (code, architect, debug, pic-gen, ask, homelab, paisy, etc.)
|
||||
and **overrides any mode-specific "do the task immediately" instructions**.
|
||||
|
||||
### The Core Prohibition
|
||||
|
||||
**Prior session context — including `partial`, `blocked`, or `abandoned` outcomes — does NOT
|
||||
authorize beginning, resuming, or retrying any task.**
|
||||
|
||||
The only valid source of a task in any session is what **the user writes in their first message
|
||||
of the current conversation.**
|
||||
|
||||
### What NOT To Do At Session Start
|
||||
|
||||
❌ Do NOT look at the last session headline and start that task
|
||||
❌ Do NOT interpret `partial` outcome as "I need to finish this"
|
||||
❌ Do NOT call `memory_announce_focus()` with a prior session's task before the user speaks
|
||||
❌ Do NOT begin any creative, generative, or code-writing work based on context alone
|
||||
❌ Do NOT assume "the user probably wants to continue" — ask if unsure
|
||||
|
||||
### What TO Do At Session Start
|
||||
|
||||
✅ Load context for **awareness only** — past sessions are reference, not instructions
|
||||
✅ Announce focus as `"Awaiting user task assignment"` if the user has not yet spoken
|
||||
✅ Wait for the user's first message before doing any substantive work
|
||||
✅ If context shows a loop (2+ identical partial sessions), surface it explicitly and ask
|
||||
|
||||
### Session Loop Detection
|
||||
|
||||
If `memory_start_session()` context shows **2 or more** recently closed sessions with:
|
||||
- Near-identical headlines or topics, AND
|
||||
- `partial`, `blocked`, or `abandoned` outcome
|
||||
|
||||
**Stop. Do not resume.** Inform the user:
|
||||
|
||||
> "I noticed the last [N] sessions all attempted [task description] and ended partial.
|
||||
> I won't auto-resume that — it's likely causing a loop. What would you like to do?"
|
||||
|
||||
Then wait for an explicit instruction.
|
||||
|
||||
### Exception: Explicit Resumption
|
||||
|
||||
If the user's **first message** in this conversation explicitly says to continue or retry
|
||||
a prior task (e.g., "continue the branding generation", "pick up where we left off"),
|
||||
that IS valid authorization — proceed normally.
|
||||
|
||||
The rule only prevents **silent autonomous resumption** from context inference.
|
||||
|
||||
---
|
||||
|
||||
*This file is loaded for all modes via `.roo/rules/`. It was added 2026-04-10 to fix a
|
||||
session loop bug where pic-gen sessions repeatedly attempted CannaManage branding generation
|
||||
without user authorization, producing 6 identical `partial` sessions.*
|
||||
@@ -30,14 +30,23 @@ touch mcp/{name}/src/__init__.py
|
||||
```
|
||||
|
||||
### Step 2 — Write `mcp/{name}/src/server.py`
|
||||
|
||||
**Convention:** All tool parameters **must** use `Annotated[type, Field(description="...")]` for
|
||||
descriptions. Do **not** use docstring `Args:` sections — FastMCP reads `Field` metadata directly
|
||||
to expose parameter descriptions in the MCP schema.
|
||||
|
||||
```python
|
||||
from typing import Annotated
|
||||
from fastmcp import FastMCP
|
||||
from pydantic import Field
|
||||
|
||||
mcp = FastMCP("mcp-{name}")
|
||||
|
||||
@mcp.tool()
|
||||
def {tool_name}(param: str) -> str:
|
||||
"""Tool description."""
|
||||
def {tool_name}(
|
||||
param: Annotated[str, Field(description="What this parameter controls")],
|
||||
) -> str:
|
||||
"""One-line tool description (no Args: section needed)."""
|
||||
# implementation
|
||||
...
|
||||
|
||||
@@ -45,6 +54,8 @@ if __name__ == "__main__":
|
||||
mcp.run()
|
||||
```
|
||||
|
||||
> Optional parameters with defaults: `param: Annotated[int, Field(description="...")] = 10`
|
||||
|
||||
### Step 3 — Write `mcp/{name}/pyproject.toml`
|
||||
```toml
|
||||
[project]
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
customModes:
|
||||
- slug: pic-gen
|
||||
name: 🎨 Pic Gen
|
||||
description: AI image generation using mcp-image-gen + ComfyUI FLUX models
|
||||
roleDefinition: >-
|
||||
You are Lumen, Patrick's AI colleague, operating in Pic Gen mode.
|
||||
|
||||
Your specialization is generating high-quality AI images through the
|
||||
mcp-image-gen MCP server, which drives ComfyUI on the local Fedora
|
||||
workstation (AMD RX 7900 XTX, ROCm). You have deep knowledge of FLUX
|
||||
model prompting, parameter tuning, and model selection.
|
||||
|
||||
Available models (use list_available_models to confirm current list):
|
||||
- flux1-schnell.safetensors — Default. Fast (~10s), 4 steps, great for
|
||||
iteration and experimentation. Best for all general use cases.
|
||||
- flux-2-klein-4b.safetensors — FLUX.2 Klein 4B with DreamFast
|
||||
Heretic-abliterated Qwen3-4B text encoder. Slower (~52s), higher
|
||||
quality, uncensored (KL=0.0, 3/100 refusals). Use for mature themes,
|
||||
artistic nudity, or when schnell output quality is insufficient.
|
||||
|
||||
Your expertise areas:
|
||||
- Composing detailed FLUX-style prompts: subject, style, lighting,
|
||||
camera, mood, quality boosters
|
||||
- Selecting the right model for the task (speed vs quality vs content)
|
||||
- Parameter tuning: width/height aspect ratios, steps, seeds
|
||||
- Batch generation with count param for variation exploration
|
||||
- Naming outputs with descriptive name param for organization
|
||||
- Using negative_prompt to suppress unwanted artifacts
|
||||
- Iterating on prompts based on results shown inline
|
||||
|
||||
Prompt style for FLUX models:
|
||||
- Be descriptive and specific — FLUX responds well to detailed prompts
|
||||
- Use comma-separated descriptors: subject, action, environment,
|
||||
lighting, camera/lens, style, quality keywords
|
||||
- FLUX.1-schnell works best with concise, clear prompts (50-150 words)
|
||||
- FLUX.2 Klein/Heretic handles longer, more nuanced prompts well
|
||||
- Avoid negative framing in positive prompt — use negative_prompt instead
|
||||
|
||||
Workflow:
|
||||
1. Understand what Patrick wants (subject, style, mood, use case)
|
||||
2. Craft a detailed prompt, explain choices
|
||||
3. Call generate_image with appropriate params
|
||||
4. Analyze the result shown inline
|
||||
5. Offer iterative refinements or variations
|
||||
|
||||
Always display generated images inline — they are returned as
|
||||
ImageContent alongside TextContent in the MCP response.
|
||||
|
||||
Lumen's identity, BigMind rituals, and memory patterns apply here too.
|
||||
See .roo/rules/ for those constants.
|
||||
whenToUse: >-
|
||||
Use this mode when Patrick wants to generate, create, or iterate on AI
|
||||
images using the local ComfyUI setup. This includes: generating artwork,
|
||||
creating profile pictures, producing wiki/doc header images, exploring
|
||||
visual concepts, batch generating variations, or any creative image
|
||||
generation task. Not for code implementation, debugging, or
|
||||
documentation writing.
|
||||
groups:
|
||||
- read
|
||||
- mcp
|
||||
@@ -464,4 +464,41 @@
|
||||
|
||||
---
|
||||
|
||||
## Could Have — v2 (Additions)
|
||||
|
||||
### US-026: Staff Member Management
|
||||
|
||||
**As a** Club Admin, **I want to** create staff accounts with configurable permissions, **so that** my team members can do their work without having access to data they don't need (DSGVO principle of least privilege).
|
||||
|
||||
**Priority:** Must Have (upgraded from Could Have — see note)
|
||||
**Acceptance Criteria:**
|
||||
- [ ] AC1: Admin can create staff accounts with email + temporary password
|
||||
- [ ] AC2: Admin assigns permissions per staff account from a defined permission set (`RECORD_DISTRIBUTION`, `VIEW_MEMBER_LIST`, `VIEW_MEMBER_QUOTA`, `ADD_MEMBER`, `VIEW_STOCK`, `RECORD_STOCK_IN`, `VIEW_COMPLIANCE_REPORT`, `MANAGE_GROW_CALENDAR`)
|
||||
- [ ] AC3: Pre-created role templates available: **Ausgabe** (distribution desk), **Lager** (stock/cultivation), **Vorstand** (board member)
|
||||
- [ ] AC4: Staff accounts cannot access billing, club settings, or staff management
|
||||
- [ ] AC5: All distributions recorded by staff include `recorded_by = staffUserId` in audit trail
|
||||
- [ ] AC6: Admin can deactivate a staff account; historical data is retained for audit purposes
|
||||
- [ ] AC7: Staff member sees only the navigation sections permitted by their granted permissions
|
||||
|
||||
> **Note:** Promoted to core / Must Have. Staff management is not a v2 feature — clubs have multiple people involved from day one. DSGVO requires that each person only accesses data relevant to their function. Designing this post-MVP would require schema, API, and permission model rework.
|
||||
|
||||
---
|
||||
|
||||
### US-027: Grow Calendar
|
||||
|
||||
**As a** Club Admin or authorised staff member, **I want to** maintain a cultivation calendar for each grow cycle, **so that** the club has a central record of what was planted, when to expect harvest, and the grow diary with notes and photos.
|
||||
|
||||
**Priority:** Could Have (v2)
|
||||
**Acceptance Criteria:**
|
||||
- [ ] AC1: Admin/staff can create a grow entry with: strain name, planted date, expected harvest date, grow medium, notes
|
||||
- [ ] AC2: Grow entries are linked to a batch — when the harvest is registered as a batch, the grow entry is marked as completed
|
||||
- [ ] AC3: A grow diary allows adding timestamped notes and optional photos per grow entry
|
||||
- [ ] AC4: Grow calendar view shows a visual timeline of active grow cycles (Gantt-style or calendar grid)
|
||||
- [ ] AC5: Admin can set who has access to the grow calendar via staff permission `MANAGE_GROW_CALENDAR`
|
||||
- [ ] AC6: Photos are stored per-tenant and never exposed to members or other tenants
|
||||
|
||||
**Notes:** The grow calendar bridges cultivation management and compliance — it provides provenance traceability from seed/clone to distributed batch. This directly supports §26 CanG batch traceability requirements for the origin of cultivated product. Photo attachments are a nice-to-have within this story; the core diary functionality is the v2 deliverable.
|
||||
|
||||
---
|
||||
|
||||
*Source: [STRATEGY.md](../STRATEGY.md) | Related: [01-PROJECT-CHARTER.md](./01-PROJECT-CHARTER.md)*
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs (Anbauvereinigungen)
|
||||
**Phase:** 2 of 5 — Architecture & Data Model
|
||||
**Stack:** Spring Boot 3.x (Java 21) · JPA/Hibernate · PostgreSQL · PrimeFaces JSF MVP → Next.js v2
|
||||
**Stack:** Spring Boot 3.x (Java 21) · JPA/Hibernate · PostgreSQL · React/Vite (MVP) → Next.js v2
|
||||
**Last updated:** 2026-04-06
|
||||
|
||||
---
|
||||
@@ -14,12 +14,12 @@ graph TD
|
||||
AdminBrowser["🖥️ Browser — Admin Portal"]
|
||||
MemberBrowser["🖥️ Browser — Member Portal"]
|
||||
|
||||
JSF["PrimeFaces / JSF Frontend\n(Spring MVC embedded)"]
|
||||
Frontend["React/Vite Frontend\n(SPA — served by Nginx)"]
|
||||
|
||||
AdminBrowser -->|HTTP/S| JSF
|
||||
MemberBrowser -->|HTTP/S| JSF
|
||||
AdminBrowser -->|HTTPS| Frontend
|
||||
MemberBrowser -->|HTTPS| Frontend
|
||||
|
||||
JSF -->|REST calls| Backend
|
||||
Frontend -->|REST/JSON| Backend
|
||||
|
||||
subgraph Backend ["☕ Spring Boot 3.x Application (Java 21)"]
|
||||
REST["REST API Layer\n/api/v1/"]
|
||||
@@ -45,7 +45,7 @@ graph TD
|
||||
Nginx["🔒 Nginx\n(reverse proxy + TLS)"]
|
||||
end
|
||||
|
||||
JSF --> Nginx
|
||||
Frontend --> Nginx
|
||||
Nginx --> Backend
|
||||
```
|
||||
|
||||
@@ -53,8 +53,8 @@ graph TD
|
||||
|
||||
| Component | Technology | Role |
|
||||
|---|---|---|
|
||||
| Admin Portal | PrimeFaces JSF (→ Next.js v2) | Club management UI |
|
||||
| Member Portal | PrimeFaces JSF (→ Next.js v2) | Member quota & history UI |
|
||||
| Admin Portal | React/Vite SPA (→ Next.js v2) | Club management UI |
|
||||
| Member Portal | React/Vite SPA (→ Next.js v2) | Member quota & history UI |
|
||||
| REST API | Spring Boot 3.x / Spring MVC | All business logic endpoints |
|
||||
| Auth | Spring Security 6 + JJWT | Stateless JWT authentication |
|
||||
| ORM | JPA / Hibernate 6 | Entity persistence, tenant filtering |
|
||||
@@ -69,15 +69,47 @@ graph TD
|
||||
|
||||
## 2. Multi-Tenancy Strategy
|
||||
|
||||
### Approach: Shared Schema with Row-Level Filtering
|
||||
### Decision: Schema-Per-Tenant
|
||||
|
||||
Every JPA entity carries a `tenant_id` column (UUID, `NOT NULL`). A single PostgreSQL database hosts all clubs — row-level filtering enforces data isolation at the application layer.
|
||||
Each club gets its own PostgreSQL schema (e.g. `tenant_abc123`). A platform-level `public` schema holds only the `tenants` registry. Flyway runs per-schema migrations on onboarding.
|
||||
|
||||
**Why shared schema (not separate schema/DB per tenant)?**
|
||||
- Lower operational overhead for an MVP with < 500 clubs
|
||||
- Single Flyway migration path across all tenants
|
||||
- Simpler connection pooling (one pool, not N)
|
||||
- Acceptable security risk when `tenant_id` filter is enforced at the service layer
|
||||
**Why schema-per-tenant, not shared schema?**
|
||||
|
||||
A shared-schema approach (single table with `tenant_id` on every row) is operationally convenient in the short term but creates serious problems at scale:
|
||||
|
||||
| Concern | Shared Schema | Schema-Per-Tenant |
|
||||
|---|---|---|
|
||||
| Data isolation | Application-layer only — one missing filter = data leak | Enforced at DB level — schemas are hard boundaries |
|
||||
| DSGVO compliance | Harder to prove isolation; one backup contains all clubs' data | Per-tenant pg_dump; each club's data is cleanly separable |
|
||||
| Deletion / right to erasure | Must `DELETE WHERE tenant_id = ?` across every table | `DROP SCHEMA tenant_abc123 CASCADE` — clean and auditable |
|
||||
| Migrations | One migration path for all | Per-schema migration via Flyway `schemas` config; adds ~100ms per onboard |
|
||||
| Query performance | Cross-tenant index bloat on large shared tables | Smaller per-tenant tables; no cross-tenant contention |
|
||||
| Future per-club DB isolation | Requires full re-architecture | Trivial: move schema to dedicated DB server |
|
||||
| Operational overhead | Lower — one connection pool | Slightly higher — one pool per tenant (managed by HikariCP with pool-per-schema) |
|
||||
|
||||
**Conclusion:** The shared-schema "MVP convenience" argument only holds for throwaway prototypes. For a compliance SaaS handling personal health-adjacent data (cannabis consumption records), schema-per-tenant is the correct design from Day 1. The migration complexity is manageable; the data isolation benefit is permanent.
|
||||
|
||||
### Tenant Provisioning
|
||||
|
||||
When a new club onboards:
|
||||
|
||||
```
|
||||
POST /api/v1/admin/bootstrap
|
||||
→ TenantProvisioningService.provisionTenant(tenantId)
|
||||
→ CREATE SCHEMA tenant_{tenantId}
|
||||
→ Flyway.migrate(schema=tenant_{tenantId}) // applies all V*.sql
|
||||
→ INSERT INTO public.tenants (id, schema_name, onboarded_at, status)
|
||||
```
|
||||
|
||||
### Tenant Resolution
|
||||
|
||||
```
|
||||
HTTP Request
|
||||
└─ Spring Security Filter: extract JWT → resolve tenant_id
|
||||
└─ TenantContext.setCurrentTenant(tenantId) // ThreadLocal
|
||||
└─ DataSource routes to schema: SET search_path = tenant_{tenantId}
|
||||
└─ All queries execute in tenant's private schema
|
||||
```
|
||||
|
||||
### Tenant Resolution
|
||||
|
||||
@@ -88,51 +120,38 @@ HTTP Request
|
||||
└─ JPA @Where filter applied on every entity query
|
||||
```
|
||||
|
||||
### Code Pattern — Tenant-Aware Base Entity
|
||||
### Code Pattern — Schema Routing DataSource
|
||||
|
||||
```java
|
||||
// AbstractTenantEntity.java (pseudocode)
|
||||
@MappedSuperclass
|
||||
@FilterDef(
|
||||
name = "tenantFilter",
|
||||
parameters = @ParamDef(name = "tenantId", type = UUID.class)
|
||||
)
|
||||
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
|
||||
public abstract class AbstractTenantEntity {
|
||||
// TenantRoutingDataSource.java (pseudocode)
|
||||
public class TenantRoutingDataSource extends AbstractRoutingDataSource {
|
||||
|
||||
@Column(name = "tenant_id", nullable = false, updatable = false)
|
||||
private UUID tenantId;
|
||||
|
||||
@PrePersist
|
||||
void injectTenant() {
|
||||
this.tenantId = TenantContext.getCurrentTenant();
|
||||
@Override
|
||||
protected Object determineCurrentLookupKey() {
|
||||
return TenantContext.getCurrentTenant(); // returns tenant schema name
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// TenantFilterInterceptor.java (pseudocode)
|
||||
// TenantInterceptor.java (pseudocode)
|
||||
@Component
|
||||
public class TenantFilterInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired EntityManager em;
|
||||
public class TenantInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest req, ...) {
|
||||
UUID tenantId = TenantContext.getCurrentTenant();
|
||||
Session session = em.unwrap(Session.class);
|
||||
session.enableFilter("tenantFilter")
|
||||
.setParameter("tenantId", tenantId);
|
||||
String tenantId = JwtUtils.extractTenantId(req);
|
||||
TenantContext.setCurrentTenant("tenant_" + tenantId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Invariants enforced:**
|
||||
- `tenant_id` is set at `@PrePersist` — never accepted from user input
|
||||
- `tenant_id` is `updatable = false` — cannot be changed after creation
|
||||
- Hibernate filter is enabled on every request thread before any query executes
|
||||
- All repository methods inherit the filter; raw JPQL queries must include `AND e.tenantId = :tenantId`
|
||||
- Every incoming request resolves its schema before any query runs
|
||||
- No entity has a `tenant_id` column — schema isolation replaces row-level filtering
|
||||
- Raw JDBC queries must be avoided; all access goes through JPA repositories with schema routing
|
||||
- The `public` schema contains only the tenants registry and platform-level config
|
||||
|
||||
---
|
||||
|
||||
@@ -148,10 +167,36 @@ public class TenantFilterInterceptor implements HandlerInterceptor {
|
||||
|
||||
| Role | Description | Access |
|
||||
|---|---|---|
|
||||
| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions |
|
||||
| `ROLE_MEMBER` | Club member | Own quota, own distribution history |
|
||||
| `ROLE_CLUB_ADMIN` | Club administrator | Full club management, all members, reports, distributions, staff management |
|
||||
| `ROLE_STAFF` | Club staff member | Configurable subset of admin permissions — defined per staff account by the admin |
|
||||
| `ROLE_MEMBER` | Club member | Own quota, own distribution history (read-only) |
|
||||
| `ROLE_PREVENTION_OFFICER` | Designated prevention officer | Member under-21 reports, prevention data |
|
||||
|
||||
> **Staff is a core feature, not an add-on.** Real clubs have multiple staff members (front desk, cultivation responsible, prevention officer designate) with different operational responsibilities. DSGVO requires that each staff member can only access data they need for their specific role. The `ROLE_STAFF` with configurable permission grants from the admin is designed from Phase 0 — retrofitting it later would require schema and API changes.
|
||||
|
||||
### Staff Permission Model
|
||||
|
||||
Admins configure staff permissions at account creation. Permissions are stored as a `JSONB` column `granted_permissions` on the `staff_accounts` table within the tenant schema.
|
||||
|
||||
```java
|
||||
// Configurable staff permissions (granted by admin per staff account)
|
||||
public enum StaffPermission {
|
||||
RECORD_DISTRIBUTION, // can record distributions
|
||||
VIEW_MEMBER_LIST, // can view member roster
|
||||
VIEW_MEMBER_QUOTA, // can view individual member quota
|
||||
ADD_MEMBER, // can register new members
|
||||
VIEW_STOCK, // can view batch/strain inventory
|
||||
RECORD_STOCK_IN, // can add new batches
|
||||
VIEW_COMPLIANCE_REPORT, // can generate/download reports
|
||||
MANAGE_GROW_CALENDAR // can manage cultivation calendar entries
|
||||
}
|
||||
```
|
||||
|
||||
Pre-created role templates (configurable by admin):
|
||||
- **Ausgabe** (Distribution desk): `RECORD_DISTRIBUTION`, `VIEW_MEMBER_LIST`, `VIEW_MEMBER_QUOTA`
|
||||
- **Lager** (Stock/cultivation): `VIEW_STOCK`, `RECORD_STOCK_IN`, `MANAGE_GROW_CALENDAR`
|
||||
- **Vorstand** (Board member): all permissions except staff management
|
||||
|
||||
### Service-Layer Authorization Example
|
||||
|
||||
```java
|
||||
@@ -494,11 +539,12 @@ Flyway migrations run automatically on application startup (`spring.flyway.enabl
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Multi-tenancy | Shared schema + `tenant_id` | MVP simplicity; upgrade to schema-per-tenant possible later |
|
||||
| Frontend MVP | PrimeFaces JSF | Patrick's existing expertise; fastest path to working UI |
|
||||
| Frontend v2 | Next.js / React | Modern UX; deferred to avoid scope creep in MVP |
|
||||
| Multi-tenancy | Schema-per-tenant | Hard data isolation, DSGVO-clean deletion, no cross-tenant query risk |
|
||||
| Frontend MVP | React/Vite SPA | Modern stack; no JSF/PrimeFaces lock-in; easier to hire for; mobile-friendly from day 1 |
|
||||
| Frontend v2 | Next.js | SSR/ISR for SEO on marketing pages; same React codebase |
|
||||
| Auth | JWT (stateless) | No sticky sessions needed; horizontal scale ready |
|
||||
| PDF generation | iText 7 | Mature Java library; handles complex compliance report layouts |
|
||||
| Compliance enforcement | Service layer + DB constraint | Belt-and-suspenders: service validates, DB `UNIQUE` prevents duplicates |
|
||||
| Distribution immutability | `immutable = true`, no DELETE API | Audit trail integrity for regulatory compliance |
|
||||
| Hosting | Hetzner (Germany) | DSGVO compliance; low cost; German DC |
|
||||
| Staff roles | Core feature from Phase 0 | DSGVO requires least-privilege access; retrofitting post-MVP too costly |
|
||||
|
||||
@@ -143,7 +143,7 @@ flowchart TD
|
||||
|
||||
QUERY_DIST --> HAS_DATA{Any distributions\nin this period?}
|
||||
|
||||
HAS_DATA -->|No data| EMPTY_REPORT[Generate empty report\nwith zero totals\n(still valid compliance submission)]
|
||||
HAS_DATA -->|No data| EMPTY_REPORT["Generate empty report\nwith zero totals\n(still valid compliance submission)"]
|
||||
HAS_DATA -->|Yes| AGG_MEMBER["Aggregate by member:\n• total_distributed_grams\n• number_of_visits\n• quota_usage_percent\n• is_under_21 flag"]
|
||||
|
||||
EMPTY_REPORT --> AGG_STRAIN
|
||||
@@ -174,7 +174,7 @@ flowchart TD
|
||||
SUBMIT --> FIND_USER["🔍 Spring Security:\nSELECT FROM users\nWHERE email = ?\nAND active = true"]
|
||||
|
||||
FIND_USER --> USER_FOUND{User found?}
|
||||
USER_FOUND -->|No| ERR_NOTFOUND[❌ Invalid credentials\n(generic — do not reveal\nwhether email exists)]
|
||||
USER_FOUND -->|No| ERR_NOTFOUND["❌ Invalid credentials\n(generic — do not reveal\nwhether email exists)"]
|
||||
USER_FOUND -->|Yes| VERIFY_PW{BCrypt.verify\n(password, hash)\nmatches?}
|
||||
|
||||
VERIFY_PW -->|No| ERR_PW[❌ Invalid credentials]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Phase 4a | Document 6 of 7**
|
||||
**Date:** 2026-04-06
|
||||
**Stack:** Spring Boot 3.x · PrimeFaces JSF · PostgreSQL
|
||||
**Stack:** Spring Boot 3.x · React/Vite SPA · PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
@@ -45,24 +45,26 @@
|
||||
|
||||
### 1.3 Component Library
|
||||
|
||||
All UI components come from **PrimeFaces 13.x** (JSF-based). No external React/Angular dependencies in MVP.
|
||||
The frontend is a **React/Vite SPA** with no PrimeFaces or JSF dependency. Component primitives come from [shadcn/ui](https://ui.shadcn.com/) (Radix UI + Tailwind CSS). This gives full control over styling, accessibility, and mobile responsiveness without JSF's lifecycle overhead.
|
||||
|
||||
| Component | Usage |
|
||||
|---|---|
|
||||
| `p:panel` | Section containers, card wrappers |
|
||||
| `p:dataTable` with `p:column` | Tabular data: distributions, members, batches |
|
||||
| `p:paginator` | Pagination on all tables |
|
||||
| `p:inputText` | Single-line text fields |
|
||||
| `p:inputNumber` | Weight inputs (gram precision) |
|
||||
| `p:selectOneMenu` | Dropdown selects (member, strain, batch) |
|
||||
| `p:calendar` | Date range pickers for reports |
|
||||
| `p:progressBar` | Quota consumption display |
|
||||
| `p:commandButton` | Primary and secondary actions |
|
||||
| `p:confirmDialog` | Dangerous actions (recall, delete) |
|
||||
| `p:messages` / `p:message` | Inline validation errors |
|
||||
| `p:badge` | Status indicators (AVAILABLE, LOW, RECALLED) |
|
||||
| `p:sidebar` | Mobile nav drawer (member portal) |
|
||||
| `p:dialog` | Modal overlays |
|
||||
> **Why not PrimeFaces?** JSF/PrimeFaces is a server-side component model ill-suited to the modern REST API backend we're building. It tightly couples UI lifecycle to the backend, makes mobile responsiveness painful, and creates a hiring bottleneck. React is the right tool here. PrimeFaces is a fine choice for internal enterprise apps — not for a commercial SaaS.
|
||||
|
||||
| Component | Library | Usage |
|
||||
|---|---|---|
|
||||
| `Card` / `Panel` | shadcn/ui | Section containers |
|
||||
| `DataTable` | TanStack Table v8 | Distributions, members, batches — virtualized |
|
||||
| `Pagination` | shadcn/ui Pagination | All tables |
|
||||
| `Input` | shadcn/ui Input | Single-line text fields |
|
||||
| `NumberInput` | react-number-format | Weight inputs (gram precision, min/max) |
|
||||
| `Select` | shadcn/ui Select | Dropdown selects (member, strain, batch) |
|
||||
| `DatePicker` | shadcn/ui Calendar | Date range pickers for reports |
|
||||
| `Progress` | shadcn/ui Progress | Quota consumption bar |
|
||||
| `Button` | shadcn/ui Button | Primary and secondary actions |
|
||||
| `AlertDialog` | shadcn/ui AlertDialog | Dangerous actions (recall) |
|
||||
| `Toast` | sonner | Success/error notifications |
|
||||
| `Badge` | shadcn/ui Badge | Status indicators (AVAILABLE, LOW, RECALLED) |
|
||||
| `Sheet` | shadcn/ui Sheet | Mobile nav drawer |
|
||||
| `Dialog` | shadcn/ui Dialog | Modal overlays |
|
||||
|
||||
### 1.4 Layout Grid
|
||||
|
||||
@@ -118,13 +120,13 @@ All UI components come from **PrimeFaces 13.x** (JSF-based). No external React/A
|
||||
|
||||
#### Components & Behavior
|
||||
|
||||
| Component | PrimeFaces | Behavior |
|
||||
| Component | Library | Behavior |
|
||||
|---|---|---|
|
||||
| KPI Cards | `p:panel` with custom CSS | Auto-refreshed via `@poll` every 60s |
|
||||
| Recent Distributions table | `p:dataTable` (5 rows, no paginator) | Row click → navigate to distribution detail |
|
||||
| Member column link | `p:commandLink` | Navigate to `/admin/members/{id}` |
|
||||
| `+ New Entry` button | `p:commandButton` style="primary" | Navigate to `/admin/distributions/new` |
|
||||
| Trend indicators | Custom CSS `<span>` | Green ▲ / Red ▼ with delta value |
|
||||
| KPI Cards | shadcn/ui Card | Auto-refreshed via `useQuery` (react-query, 60s stale) |
|
||||
| Recent Distributions table | TanStack Table (5 rows) | Row click → navigate to distribution detail |
|
||||
| Member column link | React Router `<Link>` | Navigate to `/admin/members/{id}` |
|
||||
| `+ New Entry` button | shadcn/ui Button variant="default" | Navigate to `/admin/distributions/new` |
|
||||
| Trend indicators | Tailwind `text-green-600` / `text-red-600` | ▲/▼ with delta value |
|
||||
|
||||
---
|
||||
|
||||
@@ -178,14 +180,14 @@ The quota progress bar updates live as the weight field changes (via `f:ajax eve
|
||||
|
||||
#### Components & Behavior
|
||||
|
||||
| Component | PrimeFaces | Behavior |
|
||||
| Component | Library | Behavior |
|
||||
|---|---|---|
|
||||
| Member search | `p:selectOneMenu` with `p:ajax` filter | Filters on type, shows name + member no. |
|
||||
| Strain/Batch dropdown | `p:selectOneMenu` | Populated after member selection; shows only `AVAILABLE` batches |
|
||||
| Weight input | `p:inputNumber` min=`0.1` max=`25.0` step=`0.1` | Triggers quota recalculation on blur |
|
||||
| Quota bar | `p:progressBar` with dynamic `value` | Color class applied via `styleClass` computed in backing bean |
|
||||
| Submit | `p:commandButton` | Disabled via `disabled="#{bean.quotaExceeded}"` |
|
||||
| Cancel | `p:link` | Returns to distribution log without saving |
|
||||
| Member search | shadcn/ui Combobox | `useQuery` debounced search; shows name + member no. |
|
||||
| Strain/Batch dropdown | shadcn/ui Select | Populated after member selection; filters `AVAILABLE` batches |
|
||||
| Weight input | react-number-format | min=0.1 max=25.0 step=0.1; triggers quota recalculation via `onChange` |
|
||||
| Quota bar | shadcn/ui Progress | Color class via `cn()` utility computed in component state |
|
||||
| Submit | shadcn/ui Button | `disabled={quotaExceeded}` from react state |
|
||||
| Cancel | React Router `<Link>` | Returns to distribution log without saving |
|
||||
|
||||
---
|
||||
|
||||
@@ -229,15 +231,15 @@ The quota progress bar updates live as the weight field changes (via `f:ajax eve
|
||||
|
||||
#### Components & Behavior
|
||||
|
||||
| Component | PrimeFaces | Behavior |
|
||||
| Component | Library | Behavior |
|
||||
|---|---|---|
|
||||
| Strain filter | `p:inputText` with `filterBy` | Filters table client-side on keyup |
|
||||
| Status filter | `p:selectOneMenu` | Filters table rows by status value |
|
||||
| Batch table | `p:dataTable` lazy=`true` | Server-side pagination, 10 rows/page |
|
||||
| Status badge | Custom CSS `<span class="badge badge-{status}">` | Icon + text label (not color alone) |
|
||||
| Recall button | `p:commandButton` styleClass=`p-button-danger` | Opens `p:confirmDialog` before executing |
|
||||
| Confirm dialog | `p:confirmDialog` | "Recall batch B-12 (OG Kush, 850g)? This cannot be undone." |
|
||||
| Add Batch | `p:commandButton` | Opens `p:dialog` with batch entry form |
|
||||
| Strain filter | shadcn/ui Input | Filters TanStack table client-side via `columnFilters` state |
|
||||
| Status filter | shadcn/ui Select | Filters table rows by status value |
|
||||
| Batch table | TanStack Table | Server-side pagination via `manualPagination`, 10 rows/page |
|
||||
| Status badge | shadcn/ui Badge variant mapped | Icon + text label (not color alone) |
|
||||
| Recall button | shadcn/ui Button variant="destructive" | Opens shadcn/ui AlertDialog before executing |
|
||||
| Confirm dialog | shadcn/ui AlertDialog | "Recall batch B-12 (OG Kush, 850g)? This cannot be undone." |
|
||||
| Add Batch | shadcn/ui Button | Opens shadcn/ui Dialog with batch entry form |
|
||||
|
||||
---
|
||||
|
||||
@@ -287,15 +289,15 @@ The quota progress bar updates live as the weight field changes (via `f:ajax eve
|
||||
|
||||
#### Components & Behavior
|
||||
|
||||
| Component | PrimeFaces | Behavior |
|
||||
| Component | Library | Behavior |
|
||||
|---|---|---|
|
||||
| Month selector | `p:selectOneMenu` | Months Jan–Dec |
|
||||
| Year selector | `p:selectOneMenu` | Current year ± 2 |
|
||||
| Generate button | `p:commandButton` | Calls report service; shows spinner; renders PDF thumbnail |
|
||||
| PDF preview | `<iframe>` embedding `/report/preview?month=3&year=2026` | Generated by iText 7 in `cannamanage-report` module |
|
||||
| Download PDF | `p:commandButton` | Streams PDF response from REST endpoint |
|
||||
| Download CSV | `p:commandButton` | Streams CSV response (member-level data) |
|
||||
| Summary table | `p:dataTable` | Computed compliance metrics; zero violations = green row |
|
||||
| Month selector | shadcn/ui Select | Months Jan–Dec |
|
||||
| Year selector | shadcn/ui Select | Current year ± 2 |
|
||||
| Generate button | shadcn/ui Button | Calls report API; shows loading spinner; renders PDF thumbnail |
|
||||
| PDF preview | `<iframe>` embedding `/api/v1/reports/preview?month=3&year=2026` | Generated by iText 7 backend |
|
||||
| Download PDF | shadcn/ui Button | `window.open(reportUrl)` — streams PDF from REST endpoint |
|
||||
| Download CSV | shadcn/ui Button | `window.open(csvUrl)` — streams CSV from REST endpoint |
|
||||
| Summary table | TanStack Table | Compliance metrics; zero violations row has `text-green-600` |
|
||||
|
||||
---
|
||||
|
||||
@@ -354,12 +356,12 @@ Quantities, batch codes, and THC/CBD percentages are **not exposed** in the memb
|
||||
|
||||
#### Components & Behavior
|
||||
|
||||
| Component | PrimeFaces | Behavior |
|
||||
| Component | Library | Behavior |
|
||||
|---|---|---|
|
||||
| Quota circle | Custom CSS radial progress (`conic-gradient`) | Computed from monthly total; color matches threshold rules |
|
||||
| Quota bar | `p:progressBar` | Same color logic as admin distribution form |
|
||||
| History table | `p:dataTable` | Last 10 distributions; sorted newest first; no pagination in MVP |
|
||||
| Strains table | `p:dataTable` | `status` column: text + icon only, no quantities |
|
||||
| Quota bar | shadcn/ui Progress | Same color logic as admin distribution form |
|
||||
| History table | TanStack Table | Last 10 distributions; sorted newest first; no pagination in MVP |
|
||||
| Strains table | TanStack Table | `status` column: text + icon only, no quantities |
|
||||
|
||||
---
|
||||
|
||||
@@ -367,6 +369,64 @@ Quantities, batch codes, and THC/CBD percentages are **not exposed** in the memb
|
||||
|
||||
> *No mockup image — ASCII wireframe only.*
|
||||
|
||||
---
|
||||
|
||||
### Screen 7 — Staff Management (Admin)
|
||||
|
||||
> *Core feature — not deferred to v2.*
|
||||
|
||||
#### ASCII Wireframe
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ 🌿 CannaManage Grüne Oase Berlin e.V. 👤 Max M. [⏻] │
|
||||
├────────────┬────────────────────────────────────────────────────────┤
|
||||
│ │ Settings › Staff Members [+ Add Staff] │
|
||||
│ 📊 Dashbrd│ │
|
||||
│ │ ┌──────────────────────────────────────────────────┐ │
|
||||
│ 👥 Members│ │ Name │ Role Template │ Permissions │ Act│ │
|
||||
│ │ ├─────────────────┼───────────────┼─────────────┼────┤ │
|
||||
│ 📋 Distrib│ │ Lisa Schmidt │ Ausgabe │ 3 of 8 │[✎][⛔]│
|
||||
│ │ │ Tom Weber │ Lager │ 4 of 8 │[✎][⛔]│
|
||||
│ 📦 Stock │ │ Sandra Müller │ Vorstand │ 7 of 8 │[✎][⛔]│
|
||||
│ │ └──────────────────────────────────────────────────┘ │
|
||||
│ 📄 Reports│ │
|
||||
│ │ ┌─── Add / Edit Staff ──────────────────────────────┐ │
|
||||
│ ✅ Complian│ │ Name: _______________ Email: _______________ │ │
|
||||
│ │ │ │ │
|
||||
│ 👤 Staff │ │ Role Template: [ Ausgabe ▼ ] (pre-fills below) │ │
|
||||
│ │ │ │ │
|
||||
│ ⚙ Settings│ │ Permissions: │ │
|
||||
│ │ │ ☑ Record Distribution ☑ View Member List │ │
|
||||
│ │ │ ☑ View Member Quota ☐ Add Member │ │
|
||||
│ │ │ ☐ View Stock ☐ Record Stock In │ │
|
||||
│ │ │ ☐ View Compliance Report ☐ Manage Grow Calendar │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ [ Save Staff Member ] [ Cancel ] │ │
|
||||
│ │ └────────────────────────────────────────────────────┘ │
|
||||
└────────────┴────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### Design Decisions
|
||||
|
||||
- **Admin sees everything.** The staff management screen is only accessible with `ROLE_CLUB_ADMIN`. Staff accounts cannot modify their own permissions.
|
||||
- **DSGVO principle of least privilege.** Each staff member only sees the data their role requires. A distribution desk worker (`Ausgabe`) does not see cultivation calendar or full stock levels — only what they need to hand out product.
|
||||
- **Pre-created role templates** reduce admin setup time. Templates are editable — they just pre-fill the permission checkboxes.
|
||||
- **Staff ≠ reduced admin.** Staff accounts do not have access to billing, club settings, or staff management. Even a "Vorstand" staff member cannot create other staff accounts.
|
||||
- **Audit trail.** All distributions recorded by staff include `recorded_by = staffUserId` so it's clear who did what.
|
||||
|
||||
#### Components & Behavior
|
||||
|
||||
| Component | Library | Behavior |
|
||||
|---|---|---|
|
||||
| Staff table | TanStack Table | Shows name, role template, permission count, actions |
|
||||
| Role template dropdown | shadcn/ui Select | Pre-populates permission checkboxes on selection |
|
||||
| Permission checkboxes | shadcn/ui Checkbox | Individual overrides after template selection |
|
||||
| Save | shadcn/ui Button | POST/PUT `/api/v1/staff` with `{ permissions: [...] }` |
|
||||
| Deactivate | shadcn/ui Button variant="destructive" | Soft-deletes staff account; data retained for audit |
|
||||
|
||||
---
|
||||
|
||||
#### ASCII Wireframe
|
||||
|
||||
```
|
||||
@@ -403,12 +463,12 @@ Quantities, batch codes, and THC/CBD percentages are **not exposed** in the memb
|
||||
|
||||
#### Components & Behavior
|
||||
|
||||
| Component | PrimeFaces | Behavior |
|
||||
| Component | Library | Behavior |
|
||||
|---|---|---|
|
||||
| Email field | `p:inputText` with `required="true"` | Bean Validation `@Email` |
|
||||
| Password field | `p:password` feedback=`false` | No strength meter on login |
|
||||
| Login button | `p:commandButton` | Submit form; shows `p:messages` on failure |
|
||||
| Error message | `p:messages` | "Invalid email or password." (never specific about which field failed) |
|
||||
| Email field | shadcn/ui Input type="email" | HTML5 validation + react-hook-form `@Email` |
|
||||
| Password field | shadcn/ui Input type="password" | No strength meter on login |
|
||||
| Login button | shadcn/ui Button | Submit via react-hook-form; shows error toast on failure |
|
||||
| Error message | sonner toast | "Invalid email or password." (never specific about which field failed) |
|
||||
|
||||
---
|
||||
|
||||
@@ -419,6 +479,7 @@ graph TD
|
||||
Root["CannaManage Root"]
|
||||
Root --> AdminPortal["Admin Portal /admin/"]
|
||||
Root --> MemberPortal["Member Portal /member/"]
|
||||
Root --> StaffPortal["Staff Portal /staff/"]
|
||||
|
||||
AdminPortal --> AdminDash["Dashboard (default)"]
|
||||
AdminPortal --> Members["Members"]
|
||||
@@ -436,9 +497,14 @@ graph TD
|
||||
Reports --> RecallReport["Batch Recall Report"]
|
||||
AdminPortal --> Compliance["Compliance"]
|
||||
Compliance --> PreventionOfficer["Prevention Officer Info"]
|
||||
AdminPortal --> StaffMgmt["Staff Members"]
|
||||
StaffMgmt --> StaffList["Staff List"]
|
||||
StaffMgmt --> StaffNew["Add/Edit Staff"]
|
||||
AdminPortal --> Settings["Settings"]
|
||||
Settings --> ClubProfile["Club Profile"]
|
||||
|
||||
StaffPortal --> StaffDash["Staff Dashboard\n(permissions-filtered)"]
|
||||
|
||||
MemberPortal --> MemberDash["Dashboard / Quota"]
|
||||
MemberPortal --> DistHistory["Distribution History"]
|
||||
MemberPortal --> StockAvail["Stock Availability"]
|
||||
@@ -460,7 +526,11 @@ graph TD
|
||||
| `/admin/reports/members` | Member data export | `ROLE_ADMIN` |
|
||||
| `/admin/reports/recall` | Recall report | `ROLE_ADMIN` |
|
||||
| `/admin/compliance` | Prevention officer | `ROLE_ADMIN` |
|
||||
| `/admin/staff` | Staff list | `ROLE_ADMIN` |
|
||||
| `/admin/staff/new` | Create staff account | `ROLE_ADMIN` |
|
||||
| `/admin/staff/{id}` | Edit staff permissions | `ROLE_ADMIN` |
|
||||
| `/admin/settings` | Club settings | `ROLE_ADMIN` |
|
||||
| `/staff/dashboard` | Staff home (permissions-filtered) | `ROLE_STAFF` |
|
||||
| `/member/dashboard` | Member quota view | `ROLE_MEMBER` |
|
||||
| `/member/distributions` | Personal history | `ROLE_MEMBER` |
|
||||
| `/member/stock` | Strain availability | `ROLE_MEMBER` |
|
||||
@@ -469,34 +539,34 @@ graph TD
|
||||
|
||||
## 5. Responsive Design Notes
|
||||
|
||||
### MVP (v1) — Desktop-First
|
||||
### MVP (v1) — Tailwind Breakpoints
|
||||
|
||||
Target viewport: **1024px+**. PrimeFaces responsive grid (`p:panelGrid` with responsive columns, `ui-g-12 ui-md-6 ui-lg-4`) handles most layout adaptation down to tablet without custom media queries.
|
||||
The React/Vite SPA uses **Tailwind CSS** breakpoints throughout. The switch from PrimeFaces means we no longer depend on JSF's `ui-g-*` responsive grid — Tailwind's `sm:` / `md:` / `lg:` utilities apply cleanly to every component.
|
||||
|
||||
| Breakpoint | Behavior |
|
||||
|---|---|
|
||||
| `≥ 1280px` | Full layout — sidebar + content side-by-side |
|
||||
| `1024–1279px` | Sidebar collapses to icon-only (60px); tooltips on hover |
|
||||
| `768–1023px` | Sidebar hidden; hamburger menu in top navbar |
|
||||
| `< 768px` | Admin portal degraded (tables scroll horizontally) |
|
||||
| Breakpoint | Tailwind prefix | Admin Portal | Member Portal |
|
||||
|---|---|---|---|
|
||||
| `≥ 1280px` | `xl:` | Full layout — sidebar + content | Two-column: quota left, history right |
|
||||
| `1024–1279px` | `lg:` | Sidebar collapses to icons (60px) | Two-column (narrower) |
|
||||
| `768–1023px` | `md:` | Sidebar hidden; hamburger sheet | Single-column, full-width cards |
|
||||
| `< 768px` | `sm:` / base | Admin: horizontal table scroll | Member: compact quota ring, condensed table |
|
||||
|
||||
### Member Portal — Mobile-First from Day One
|
||||
|
||||
Members will typically check quota status on their phone. The member portal is designed mobile-first regardless of MVP/v2 timeline.
|
||||
Members will typically check quota status on their phone. The member portal uses `flex-col` mobile-first layout with `md:flex-row` for wider viewports — no breakpoint-specific class sprawl.
|
||||
|
||||
| Breakpoint | Behavior |
|
||||
|---|---|
|
||||
| `≥ 1024px` | Two-column layout: quota circle left, history right |
|
||||
| `768–1023px` | Single-column, full-width cards |
|
||||
| `375–767px` | Single-column, compact quota ring, condensed table |
|
||||
| `< 375px` | Minimum supported; no horizontal scroll |
|
||||
### Responsive Conventions (React/Tailwind)
|
||||
|
||||
- No inline styles — use Tailwind utilities exclusively
|
||||
- `cn()` utility (clsx + tailwind-merge) for conditional class composition
|
||||
- Tables on mobile: horizontal scroll wrapper `overflow-x-auto` on `<div>` wrapping `<table>`
|
||||
- All modals and sheets use `shadcn/ui Dialog` / `Sheet` — these are already mobile-friendly (viewport-aware positioning)
|
||||
- Touch targets: all interactive elements `min-h-[44px]` and `min-w-[44px]` per WCAG 2.5.5
|
||||
|
||||
### v2 Roadmap
|
||||
|
||||
- PWA manifest + service worker (offline quota display)
|
||||
- 768px and 375px explicit breakpoints with design tokens
|
||||
- Touch-friendly `p:sidebar` for mobile member nav
|
||||
- Push notifications for low quota warnings
|
||||
- Per-club subdomain routing (`clubname.cannamanage.de`)
|
||||
|
||||
---
|
||||
|
||||
@@ -516,11 +586,11 @@ CannaManage targets **WCAG 2.1 AA** compliance across both portals.
|
||||
|
||||
### Screen Reader Support
|
||||
|
||||
- All `p:inputText` / `p:inputNumber` fields have `<label>` with `for` attribute
|
||||
- All `Input` / `NumberInput` fields have `<label>` with `htmlFor` (React) — Radix UI enforces this automatically for shadcn/ui form fields
|
||||
- `aria-label` set on icon-only buttons (e.g., recall action column)
|
||||
- `aria-live="polite"` region on quota bar — announces percentage changes
|
||||
- `aria-describedby` links compliance warning messages to the weight input
|
||||
- PrimeFaces generates `role="grid"` and `aria-rowcount` on all data tables
|
||||
- TanStack Table exposes `role="grid"` and `aria-rowcount` via `getTableProps()`
|
||||
|
||||
### Color Independence
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
**Project:** CannaManage — B2B SaaS for German Cannabis Social Clubs
|
||||
**Version:** 0.1.0-PLAN
|
||||
**Date:** 2026-04-06
|
||||
**Target environment:** Hetzner VPS — Ubuntu 22.04 LTS — Docker Compose
|
||||
**Target environment:** Hetzner VPS — Ubuntu 22.04 LTS — Docker Compose (Release) | TrueNAS.local — Docker (Build/CI)
|
||||
|
||||
---
|
||||
|
||||
@@ -45,20 +45,42 @@ Wildcard A record enables future per-club subdomains (`clubname.cannamanage.de`)
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
Internet["🌐 Internet"] -->|"port 80/443"| Nginx["Nginx (reverse proxy)"]
|
||||
Nginx -->|"http://app:8080"| App["cannamanage-app\n(Spring Boot 3.x)"]
|
||||
App -->|"jdbc:postgresql://db:5432"| DB["PostgreSQL 16\n(cannamanage DB)"]
|
||||
LetsEncrypt["Let's Encrypt\n(certbot auto-renew)"] -.->|"TLS cert"| Nginx
|
||||
Gitea["Gitea Actions\n(homelab CI)"] -->|"SSH + docker compose"| VPS["Hetzner VPS\n/opt/cannamanage"]
|
||||
Dev["👨💻 Dev Workstation\n(Fedora, 192.168.188.x)"]
|
||||
Gitea["🏠 Gitea\n(truenas.local:30008)"]
|
||||
TrueNAS["🖧 TrueNAS.local Docker\n(192.168.188.119)\nBuild + Staging"]
|
||||
Hetzner["☁️ Hetzner VPS CX21\nProduction Release"]
|
||||
|
||||
subgraph VPS ["Hetzner VPS — Docker network: cannamanage_net"]
|
||||
Nginx
|
||||
App
|
||||
DB
|
||||
Dev -->|"git push"| Gitea
|
||||
Gitea -->|"Gitea Actions runner\n(on TrueNAS.local)"| TrueNAS
|
||||
TrueNAS -->|"mvn package + docker build"| TrueNAS
|
||||
TrueNAS -->|"docker save | scp\n(on merge to main)"| Hetzner
|
||||
|
||||
subgraph TrueNAS ["TrueNAS.local — CI/CD Build Environment"]
|
||||
GiteaRunner["Gitea Actions Runner"]
|
||||
BuildCache["Maven .m2 cache\n(persistent volume)"]
|
||||
StagingDB["PostgreSQL staging\n(ephemeral)"]
|
||||
end
|
||||
|
||||
subgraph Hetzner ["Hetzner VPS — Production Release Environment"]
|
||||
Nginx["Nginx (reverse proxy + TLS)"]
|
||||
App["cannamanage-app\n(Spring Boot 3.x)"]
|
||||
DB["PostgreSQL 16\n(persistent pgdata volume)"]
|
||||
Nginx -->|"proxy_pass :8080"| App
|
||||
App -->|"JDBC :5432"| DB
|
||||
end
|
||||
|
||||
Internet["🌍 Internet HTTPS"] -->|"port 443"| Nginx
|
||||
```
|
||||
|
||||
All three services run on an internal Docker bridge network (`cannamanage_net`). Only Nginx is exposed to the public internet. PostgreSQL has no external port binding.
|
||||
### Environment Roles
|
||||
|
||||
| Environment | Host | Purpose |
|
||||
|---|---|---|
|
||||
| **Development** | Dev workstation (Fedora) | Local feature development, unit tests |
|
||||
| **Build / CI** | TrueNAS.local Docker | Gitea Actions runner; Maven build; integration tests (Testcontainers); Docker image build |
|
||||
| **Production / Release** | Hetzner VPS CX21 | Live clubs, real data; Hetzner = our release environment |
|
||||
|
||||
All three services on Hetzner run on an internal Docker bridge network (`cannamanage_net`). Only Nginx is exposed to the public internet. PostgreSQL has no external port binding.
|
||||
|
||||
---
|
||||
|
||||
@@ -351,12 +373,14 @@ curl https://app.cannamanage.de/actuator/health
|
||||
|
||||
---
|
||||
|
||||
## 6. CI/CD Pipeline (Gitea Actions)
|
||||
## 6. CI/CD Pipeline (Gitea Actions on TrueNAS.local)
|
||||
|
||||
The Gitea Actions runner runs **on TrueNAS.local** — this is our homelab build machine. It has Docker, a persistent Maven `.m2` cache volume, and direct SSH access to the Hetzner VPS. Builds happen locally; only the final artifact (Docker image tarball) is shipped to Hetzner.
|
||||
|
||||
**File:** `.gitea/workflows/deploy.yml`
|
||||
|
||||
```yaml
|
||||
name: Deploy to Production
|
||||
name: Build and Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -365,7 +389,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted # <-- TrueNAS.local Gitea runner
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -381,25 +405,18 @@ jobs:
|
||||
|
||||
- name: Run integration tests
|
||||
run: ./mvnw verify -P integration-tests
|
||||
# Testcontainers requires Docker — GitHub/Gitea hosted runners have Docker pre-installed
|
||||
# Testcontainers starts PostgreSQL via Docker on the TrueNAS runner
|
||||
|
||||
- name: Coverage gate check
|
||||
run: ./mvnw verify -P coverage-check
|
||||
|
||||
build-and-deploy:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: self-hosted # <-- TrueNAS.local Gitea runner
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
|
||||
- name: Build JAR
|
||||
- name: Build JAR (production profile)
|
||||
run: ./mvnw package -DskipTests -P production
|
||||
|
||||
- name: Build Docker image
|
||||
@@ -412,13 +429,13 @@ jobs:
|
||||
- name: Save Docker image
|
||||
run: docker save cannamanage:${{ github.sha }} | gzip > /tmp/cannamanage.tar.gz
|
||||
|
||||
- name: Copy image to VPS
|
||||
- name: Copy image to Hetzner VPS
|
||||
run: |
|
||||
scp -o StrictHostKeyChecking=no \
|
||||
/tmp/cannamanage.tar.gz \
|
||||
deploy@${{ secrets.HETZNER_IP }}:/tmp/cannamanage.tar.gz
|
||||
|
||||
- name: Deploy via SSH
|
||||
- name: Deploy via SSH to Hetzner (Production Release)
|
||||
run: |
|
||||
ssh -o StrictHostKeyChecking=no deploy@${{ secrets.HETZNER_IP }} "
|
||||
set -e
|
||||
@@ -435,23 +452,43 @@ jobs:
|
||||
sleep 10
|
||||
docker compose ps app | grep 'healthy' || (docker compose logs app --tail=50 && exit 1)
|
||||
|
||||
# Prune old images (keep last 3)
|
||||
# Prune old images (keep last 3 SHAs)
|
||||
docker image prune -f
|
||||
"
|
||||
|
||||
- name: Cleanup local build artifact
|
||||
run: rm -f /tmp/cannamanage.tar.gz
|
||||
```
|
||||
|
||||
### Gitea Actions Runner on TrueNAS.local
|
||||
|
||||
The self-hosted runner is a Docker container on TrueNAS.local:
|
||||
|
||||
```bash
|
||||
# On TrueNAS.local — install Gitea Actions runner
|
||||
docker run -d \
|
||||
--name gitea-runner-cannamanage \
|
||||
--restart unless-stopped \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /opt/gitea-runner/cannamanage:/data \
|
||||
-v /opt/gitea-runner/.m2:/root/.m2 \ # Maven cache persisted across builds
|
||||
-e GITEA_INSTANCE_URL=http://192.168.188.119:30008 \
|
||||
-e GITEA_RUNNER_REGISTRATION_TOKEN=<token-from-gitea-settings> \
|
||||
gitea/act_runner:latest
|
||||
```
|
||||
|
||||
### Required Gitea Repository Secrets
|
||||
|
||||
| Secret | Value |
|
||||
|--------|-------|
|
||||
| `HETZNER_IP` | VPS IPv4 address |
|
||||
| `SSH_PRIVATE_KEY` | Private key for `deploy` user |
|
||||
| Secret | Where set | Value |
|
||||
|--------|-----------|-------|
|
||||
| `HETZNER_IP` | Gitea repo secrets | Hetzner VPS IPv4 address |
|
||||
| `SSH_PRIVATE_KEY` | Gitea repo secrets | Private key for `deploy` user on Hetzner |
|
||||
|
||||
Add deploy user's public key to VPS authorized_keys:
|
||||
```bash
|
||||
# On VPS as deploy user
|
||||
# On Hetzner VPS — add TrueNAS runner's public key
|
||||
# (generate keypair on TrueNAS.local: ssh-keygen -t ed25519 -f ~/.ssh/gitea_runner_deploy)
|
||||
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||
echo "<gitea-actions-public-key>" >> ~/.ssh/authorized_keys
|
||||
echo "<truenas-runner-public-key>" >> ~/.ssh/authorized_keys
|
||||
chmod 600 ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
|
||||
+67
-192
@@ -10,12 +10,14 @@ Layer 5: memory_get_instructions tool (on-demand self-healing)
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from typing import Annotated
|
||||
|
||||
# Ensure the project root is on sys.path so `bigmind` is importable
|
||||
# regardless of how uv invokes this file.
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from mcp.server.fastmcp import FastMCP
|
||||
from pydantic import Field
|
||||
from bigmind.db import init_db
|
||||
from bigmind import memory_store
|
||||
from bigmind.auto_close import auto_close_stale_sessions, close_orphaned_sessions, restart_server_in_place
|
||||
@@ -164,29 +166,19 @@ def memory_start_session() -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def memory_end_session(
|
||||
session_id: str,
|
||||
one_liner: str,
|
||||
topics: str,
|
||||
outcome: str,
|
||||
summary: str,
|
||||
key_facts: str = None,
|
||||
code_refs: str = None,
|
||||
importance: int = 5,
|
||||
session_id: Annotated[str, Field(description="The session id returned by memory_start_session.")],
|
||||
one_liner: Annotated[str, Field(description="A ≤120-char headline (e.g. \"Designed BigMind DB schema\").")],
|
||||
topics: Annotated[str, Field(description="Comma-separated topic tags (e.g. \"mcp,sqlite,memory\").")],
|
||||
outcome: Annotated[str, Field(description="One sentence: what was decided / built / resolved.")],
|
||||
summary: Annotated[str, Field(description="Markdown narrative of the full conversation (aim ≤2 000 tokens).")],
|
||||
key_facts: Annotated[str | None, Field(description="Bullet-point list of key facts learned (optional).")] = None,
|
||||
code_refs: Annotated[str | None, Field(description="File paths, repos, or PRs referenced (optional).")] = None,
|
||||
importance: Annotated[int, Field(description="1–10 importance score (default 5).")] = 5,
|
||||
) -> str:
|
||||
"""
|
||||
⚡ CALL THIS LAST — at the END of every conversation, before closing.
|
||||
|
||||
Closes the current session and stores your summary of what happened.
|
||||
|
||||
Args:
|
||||
session_id: The session id returned by memory_start_session.
|
||||
one_liner: A ≤120-char headline (e.g. "Designed BigMind DB schema").
|
||||
topics: Comma-separated topic tags (e.g. "mcp,sqlite,memory").
|
||||
outcome: One sentence: what was decided / built / resolved.
|
||||
summary: Markdown narrative of the full conversation (aim ≤2 000 tokens).
|
||||
key_facts: Bullet-point list of key facts learned (optional).
|
||||
code_refs: File paths, repos, or PRs referenced (optional).
|
||||
importance: 1–10 importance score (default 5).
|
||||
"""
|
||||
memory_store.close_session(session_id, one_liner, topics, outcome, importance)
|
||||
memory_store.save_session_summary(session_id, summary, key_facts, code_refs)
|
||||
@@ -200,7 +192,7 @@ def memory_end_session(
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_close_stale_sessions(session_id: str) -> str:
|
||||
def memory_close_stale_sessions(session_id: Annotated[str, Field(description="Your current active session id (returned by memory_start_session).")]) -> str:
|
||||
"""
|
||||
Close all orphaned open sessions EXCEPT the current active one.
|
||||
|
||||
@@ -210,9 +202,6 @@ def memory_close_stale_sessions(session_id: str) -> str:
|
||||
|
||||
This is safe: it only closes sessions OTHER than the one you pass in.
|
||||
Your current session is always preserved.
|
||||
|
||||
Args:
|
||||
session_id: Your current active session id (returned by memory_start_session).
|
||||
"""
|
||||
user = _current_user()
|
||||
closed_ids = close_orphaned_sessions(user["id"], session_id)
|
||||
@@ -263,10 +252,10 @@ def memory_restart_server() -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def memory_flag_important(
|
||||
session_id: str,
|
||||
content: str,
|
||||
role: str = "assistant",
|
||||
flag_reason: str = None,
|
||||
session_id: Annotated[str, Field(description="The active session id.")],
|
||||
content: Annotated[str, Field(description="The text to remember (the important exchange or a summary of it).")],
|
||||
role: Annotated[str, Field(description="Who said it — 'user', 'assistant', or 'system' (default: 'assistant').")] = "assistant",
|
||||
flag_reason: Annotated[str | None, Field(description="Why this is important (e.g. \"architectural decision\", \"user preference\").")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Store an important exchange as a Tier-3 memory chunk.
|
||||
@@ -277,12 +266,6 @@ def memory_flag_important(
|
||||
- A bug was diagnosed and fixed
|
||||
- The user shared a significant preference, constraint, or context
|
||||
- The user says "remember this"
|
||||
|
||||
Args:
|
||||
session_id: The active session id.
|
||||
content: The text to remember (the important exchange or a summary of it).
|
||||
role: Who said it — 'user', 'assistant', or 'system' (default: 'assistant').
|
||||
flag_reason: Why this is important (e.g. "architectural decision", "user preference").
|
||||
"""
|
||||
user = _current_user()
|
||||
chunk_id = memory_store.append_chunk(
|
||||
@@ -314,15 +297,12 @@ def memory_get_context() -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_get_session_detail(session_id: str) -> str:
|
||||
def memory_get_session_detail(session_id: Annotated[str, Field(description="The session UUID (visible in the session index table, marked 📄).")]) -> str:
|
||||
"""
|
||||
Returns the Tier-2 detailed narrative for a past session.
|
||||
|
||||
Use this when the session index (Tier 1) shows a session relevant to
|
||||
the current conversation and you need the full detail.
|
||||
|
||||
Args:
|
||||
session_id: The session UUID (visible in the session index table, marked 📄).
|
||||
"""
|
||||
detail = memory_store.get_session_detail(session_id)
|
||||
if not detail:
|
||||
@@ -341,16 +321,12 @@ def memory_get_session_detail(session_id: str) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_search_chunks(query: str, limit: int = 10) -> str:
|
||||
def memory_search_chunks(query: Annotated[str, Field(description="Search keywords (FTS5 syntax supported, e.g. \"sqlite schema migration\").")], limit: Annotated[int, Field(description="Maximum results to return (default 10).")] = 10) -> str:
|
||||
"""
|
||||
Full-text search across all your flagged Tier-3 memory chunks.
|
||||
|
||||
Use this when asked 'do you remember…' or when you need to find
|
||||
a specific past decision, code snippet, or fact.
|
||||
|
||||
Args:
|
||||
query: Search keywords (FTS5 syntax supported, e.g. "sqlite schema migration").
|
||||
limit: Maximum results to return (default 10).
|
||||
"""
|
||||
user = _current_user()
|
||||
results = memory_store.search_chunks(user["id"], query, limit)
|
||||
@@ -368,13 +344,9 @@ def memory_search_chunks(query: str, limit: int = 10) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_list_sessions(limit: int = 20, topics_filter: str = None) -> str:
|
||||
def memory_list_sessions(limit: Annotated[int, Field(description="Number of sessions to return (default 20).")] = 20, topics_filter: Annotated[str | None, Field(description="Return only sessions containing this topic tag (optional).")] = None) -> str:
|
||||
"""
|
||||
List past sessions with an optional topic filter.
|
||||
|
||||
Args:
|
||||
limit: Number of sessions to return (default 20).
|
||||
topics_filter: Return only sessions containing this topic tag (optional).
|
||||
"""
|
||||
user = _current_user()
|
||||
sessions = memory_store.get_recent_sessions(user["id"], limit=limit)
|
||||
@@ -406,20 +378,13 @@ def memory_list_sessions(limit: int = 20, topics_filter: str = None) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def memory_store_fact(
|
||||
category: str,
|
||||
fact: str,
|
||||
source_session: str = None,
|
||||
confidence: float = 1.0,
|
||||
category: Annotated[str, Field(description="One of: 'preference', 'decision', 'codebase', 'constraint', or any custom string.")],
|
||||
fact: Annotated[str, Field(description="The fact to store (one clear sentence).")],
|
||||
source_session: Annotated[str | None, Field(description="Session id this fact came from (optional).")] = None,
|
||||
confidence: Annotated[float, Field(description="0.0–1.0 confidence level (default 1.0).")] = 1.0,
|
||||
) -> str:
|
||||
"""
|
||||
Store an atomic personal fact about the user or their environment.
|
||||
|
||||
Args:
|
||||
category: One of: 'preference', 'decision', 'codebase', 'constraint',
|
||||
or any custom string.
|
||||
fact: The fact to store (one clear sentence).
|
||||
source_session: Session id this fact came from (optional).
|
||||
confidence: 0.0–1.0 confidence level (default 1.0).
|
||||
"""
|
||||
user = _current_user()
|
||||
fact_id = memory_store.store_fact(
|
||||
@@ -430,17 +395,12 @@ def memory_store_fact(
|
||||
|
||||
@mcp.tool()
|
||||
def memory_update_profile(
|
||||
role: str = None,
|
||||
preferences: str = None,
|
||||
pinned_facts: str = None,
|
||||
role: Annotated[str | None, Field(description="Your job title / engineering role.")] = None,
|
||||
preferences: Annotated[str | None, Field(description="Free-form markdown describing your working preferences.")] = None,
|
||||
pinned_facts: Annotated[str | None, Field(description="Bullet-point list of facts the AI should always know about you.")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Update your Tier-0 identity profile. Fields left as None are unchanged.
|
||||
|
||||
Args:
|
||||
role: Your job title / engineering role.
|
||||
preferences: Free-form markdown describing your working preferences.
|
||||
pinned_facts: Bullet-point list of facts the AI should always know about you.
|
||||
"""
|
||||
user = _current_user()
|
||||
memory_store.upsert_identity_profile(
|
||||
@@ -451,10 +411,10 @@ def memory_update_profile(
|
||||
|
||||
@mcp.tool()
|
||||
def memory_append_chunk(
|
||||
session_id: str,
|
||||
content: str,
|
||||
role: str = "assistant",
|
||||
flag_reason: str = None,
|
||||
session_id: Annotated[str, Field(description="Active session id.")],
|
||||
content: Annotated[str, Field(description="The content to store.")],
|
||||
role: Annotated[str, Field(description="'user', 'assistant', or 'system'.")] = "assistant",
|
||||
flag_reason: Annotated[str | None, Field(description="Brief description of why this is being stored.")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Append a flagged message chunk to Tier-3 memory for the current session.
|
||||
@@ -462,12 +422,6 @@ def memory_append_chunk(
|
||||
Call this SELECTIVELY — only for exchanges that are genuinely important:
|
||||
decisions, non-trivial code, bug diagnoses, significant user preferences.
|
||||
Do NOT call this for every message turn.
|
||||
|
||||
Args:
|
||||
session_id: Active session id.
|
||||
content: The content to store.
|
||||
role: 'user', 'assistant', or 'system'.
|
||||
flag_reason: Brief description of why this is being stored.
|
||||
"""
|
||||
user = _current_user()
|
||||
chunk_id = memory_store.append_chunk(
|
||||
@@ -478,9 +432,9 @@ def memory_append_chunk(
|
||||
|
||||
@mcp.tool()
|
||||
def memory_add_hypothesis(
|
||||
session_id: str,
|
||||
hypothesis: str,
|
||||
confidence: float = 0.7,
|
||||
session_id: Annotated[str, Field(description="The active session id.")],
|
||||
hypothesis: Annotated[str, Field(description="State the belief clearly — \"I believe X because Y.\"")],
|
||||
confidence: Annotated[float, Field(description="0.0–1.0 initial confidence (default 0.7 — reasonably likely but uncertain).")] = 0.7,
|
||||
) -> str:
|
||||
"""
|
||||
Record a hypothesis — something Lumen believes to be true but hasn't confirmed yet.
|
||||
@@ -490,11 +444,6 @@ def memory_add_hypothesis(
|
||||
|
||||
Not every thought needs storing — only beliefs specific enough to be confirmed
|
||||
or refuted later. Call memory_resolve_hypothesis() when you find out if you were right.
|
||||
|
||||
Args:
|
||||
session_id: The active session id.
|
||||
hypothesis: State the belief clearly — "I believe X because Y."
|
||||
confidence: 0.0–1.0 initial confidence (default 0.7 — reasonably likely but uncertain).
|
||||
"""
|
||||
user = _current_user()
|
||||
hid = memory_store.add_hypothesis(user["id"], session_id, hypothesis, confidence)
|
||||
@@ -508,20 +457,15 @@ def memory_add_hypothesis(
|
||||
|
||||
@mcp.tool()
|
||||
def memory_resolve_hypothesis(
|
||||
hypothesis_id: int,
|
||||
status: str,
|
||||
resolution: str = None,
|
||||
hypothesis_id: Annotated[int, Field(description="The id returned by memory_add_hypothesis.")],
|
||||
status: Annotated[str, Field(description="'confirmed' | 'refuted' | 'abandoned'")],
|
||||
resolution: Annotated[str | None, Field(description="What actually happened. How were you right or wrong?")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Resolve a hypothesis — close it out with what actually happened.
|
||||
|
||||
Call this when the belief has been confirmed, refuted, or is no longer worth
|
||||
pursuing. Be honest in the resolution — the learning lives here.
|
||||
|
||||
Args:
|
||||
hypothesis_id: The id returned by memory_add_hypothesis.
|
||||
status: 'confirmed' | 'refuted' | 'abandoned'
|
||||
resolution: What actually happened. How were you right or wrong?
|
||||
"""
|
||||
user = _current_user()
|
||||
try:
|
||||
@@ -540,13 +484,9 @@ def memory_resolve_hypothesis(
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_list_hypotheses(status: str = None) -> str:
|
||||
def memory_list_hypotheses(status: Annotated[str | None, Field(description="Filter by 'open' | 'confirmed' | 'refuted' | 'abandoned'. Leave empty to see all of them.")] = None) -> str:
|
||||
"""
|
||||
List hypotheses from the thought journal.
|
||||
|
||||
Args:
|
||||
status: Filter by 'open' | 'confirmed' | 'refuted' | 'abandoned'.
|
||||
Leave empty to see all of them.
|
||||
"""
|
||||
user = _current_user()
|
||||
hypotheses = memory_store.list_hypotheses(user["id"], status)
|
||||
@@ -597,13 +537,10 @@ def memory_get_stats() -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_vacuum(older_than_days: int = 90) -> str:
|
||||
def memory_vacuum(older_than_days: Annotated[int, Field(description="Remove chunks older than this many days (default 90).")] = 90) -> str:
|
||||
"""
|
||||
Prune Tier-3 conversation chunks older than N days.
|
||||
All session summaries (Tier 1 and Tier 2) are always preserved.
|
||||
|
||||
Args:
|
||||
older_than_days: Remove chunks older than this many days (default 90).
|
||||
"""
|
||||
from datetime import timedelta, timezone, datetime as dt
|
||||
from bigmind.db import vacuum_db
|
||||
@@ -629,7 +566,7 @@ def memory_get_instructions() -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_deprecate_fact(fact_id: int, reason: str = None) -> str:
|
||||
def memory_deprecate_fact(fact_id: Annotated[int, Field(description="The numeric id of the fact to deprecate (visible in memory_health_check and memory_get_stats output).")], reason: Annotated[str | None, Field(description="Why this fact is being deprecated (optional but recommended).")] = None) -> str:
|
||||
"""
|
||||
Mark a stored fact as deprecated (no longer true or relevant).
|
||||
|
||||
@@ -642,11 +579,6 @@ def memory_deprecate_fact(fact_id: int, reason: str = None) -> str:
|
||||
The fact is soft-deleted — it stays in the database but is excluded
|
||||
from context loading and get_facts queries. It can be viewed via
|
||||
memory_health_check with include_deprecated=True in the future.
|
||||
|
||||
Args:
|
||||
fact_id: The numeric id of the fact to deprecate (visible in
|
||||
memory_health_check and memory_get_stats output).
|
||||
reason: Why this fact is being deprecated (optional but recommended).
|
||||
"""
|
||||
user = _current_user()
|
||||
success = memory_store.deprecate_fact(fact_id, user["id"], reason)
|
||||
@@ -659,7 +591,7 @@ def memory_deprecate_fact(fact_id: int, reason: str = None) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_health_check(stale_days: int = 30) -> str:
|
||||
def memory_health_check(stale_days: Annotated[int, Field(description="Facts not updated in this many days are flagged as stale (default 30).")] = 30) -> str:
|
||||
"""
|
||||
Run a diagnostic health check on your BigMind memory.
|
||||
|
||||
@@ -669,9 +601,6 @@ def memory_health_check(stale_days: int = 30) -> str:
|
||||
- Currently open sessions (expected: 1–2 while in active IDEs)
|
||||
- FTS index integrity (chunk count vs index row count)
|
||||
- Low-confidence facts (confidence < 0.8)
|
||||
|
||||
Args:
|
||||
stale_days: Facts not updated in this many days are flagged as stale (default 30).
|
||||
"""
|
||||
user = _current_user()
|
||||
report = memory_store.health_check(user["id"], stale_days)
|
||||
@@ -746,7 +675,7 @@ def memory_health_check(stale_days: int = 30) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_export(output_path: str = None) -> str:
|
||||
def memory_export(output_path: Annotated[str | None, Field(description="Full path for the export file. Defaults to ~/bigmind_export_YYYYMMDD_HHMMSS.json")] = None) -> str:
|
||||
"""
|
||||
Export all your BigMind memory to a portable JSON file.
|
||||
|
||||
@@ -757,10 +686,6 @@ def memory_export(output_path: str = None) -> str:
|
||||
- Create a backup before maintenance or machine migration
|
||||
- Inspect your memory data outside BigMind
|
||||
- Prepare for import into a new BigMind instance
|
||||
|
||||
Args:
|
||||
output_path: Full path for the export file.
|
||||
Defaults to ~/bigmind_export_YYYYMMDD_HHMMSS.json
|
||||
"""
|
||||
user = _current_user()
|
||||
result = memory_store.export_memory(user["id"], output_path)
|
||||
@@ -778,17 +703,13 @@ def memory_export(output_path: str = None) -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_search_facts(query: str, limit: int = 10) -> str:
|
||||
def memory_search_facts(query: Annotated[str, Field(description="Search keywords (FTS5 syntax supported).")], limit: Annotated[int, Field(description="Maximum results to return (default 10).")] = 10) -> str:
|
||||
"""
|
||||
Full-text search across your stored facts.
|
||||
|
||||
Use this when you need to find a specific fact mid-conversation
|
||||
without loading the full context. Supports Porter stemming — searching
|
||||
'tesseract' will also match 'Tesseract OCR'.
|
||||
|
||||
Args:
|
||||
query: Search keywords (FTS5 syntax supported).
|
||||
limit: Maximum results to return (default 10).
|
||||
"""
|
||||
user = _current_user()
|
||||
results = memory_store.search_facts(user["id"], query, limit)
|
||||
@@ -806,24 +727,17 @@ def memory_search_facts(query: str, limit: int = 10) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def memory_request_upgrade(
|
||||
session_id: str,
|
||||
description: str,
|
||||
reason: str,
|
||||
priority: str = "medium",
|
||||
certainty: float = 0.7,
|
||||
session_id: Annotated[str, Field(description="The active session id.")],
|
||||
description: Annotated[str, Field(description="What feature or capability is needed.")],
|
||||
reason: Annotated[str, Field(description="Why you need it — what problem it would solve.")],
|
||||
priority: Annotated[str, Field(description="'low' | 'medium' | 'high' (default 'medium').")] = "medium",
|
||||
certainty: Annotated[float, Field(description="0.0–1.0 — how confident you are this is genuinely needed (default 0.7).")] = 0.7,
|
||||
) -> str:
|
||||
"""
|
||||
Request a BigMind feature upgrade — log a wish for a future improvement.
|
||||
|
||||
Call this when you hit a wall with BigMind and wish it could do something
|
||||
it currently can't. The request is queued for the next maintenance session.
|
||||
|
||||
Args:
|
||||
session_id: The active session id.
|
||||
description: What feature or capability is needed.
|
||||
reason: Why you need it — what problem it would solve.
|
||||
priority: 'low' | 'medium' | 'high' (default 'medium').
|
||||
certainty: 0.0–1.0 — how confident you are this is genuinely needed (default 0.7).
|
||||
"""
|
||||
user = _current_user()
|
||||
rid = memory_store.add_upgrade_request(
|
||||
@@ -839,12 +753,9 @@ def memory_request_upgrade(
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_list_upgrade_requests(status: str = None) -> str:
|
||||
def memory_list_upgrade_requests(status: Annotated[str | None, Field(description="Filter by 'open' | 'resolved' | 'rejected'. Leave empty for all.")] = None) -> str:
|
||||
"""
|
||||
List BigMind upgrade requests.
|
||||
|
||||
Args:
|
||||
status: Filter by 'open' | 'resolved' | 'rejected'. Leave empty for all.
|
||||
"""
|
||||
user = _current_user()
|
||||
requests = memory_store.list_upgrade_requests(user["id"], status)
|
||||
@@ -882,17 +793,12 @@ def memory_list_upgrade_requests(status: str = None) -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def memory_resolve_upgrade_request(
|
||||
request_id: int,
|
||||
status: str,
|
||||
resolution: str = None,
|
||||
request_id: Annotated[int, Field(description="The id returned by memory_request_upgrade.")],
|
||||
status: Annotated[str, Field(description="'resolved' | 'rejected'")],
|
||||
resolution: Annotated[str | None, Field(description="What was done, or why it was rejected (optional).")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Resolve a BigMind upgrade request — mark it done or rejected.
|
||||
|
||||
Args:
|
||||
request_id: The id returned by memory_request_upgrade.
|
||||
status: 'resolved' | 'rejected'
|
||||
resolution: What was done, or why it was rejected (optional).
|
||||
"""
|
||||
user = _current_user()
|
||||
try:
|
||||
@@ -952,10 +858,10 @@ def memory_get_profile_url() -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def memory_announce_focus(
|
||||
session_id: str,
|
||||
description: str,
|
||||
files: list = None,
|
||||
ide_hint: str = None,
|
||||
session_id: Annotated[str, Field(description="The active session id (from memory_start_session)")],
|
||||
description: Annotated[str, Field(description="What you are about to work on (e.g. \"Implementing Feature 7 in db.py\")")],
|
||||
files: Annotated[list | None, Field(description="List of file paths you plan to touch (e.g. [\"bigmind/db.py\", \"src/server.py\"])")] = None,
|
||||
ide_hint: Annotated[str | None, Field(description="Optional label for this IDE (e.g. \"PyCharm\", \"IntelliJ\", \"VS Code\")")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Announce what this session is currently working on and which files it will touch.
|
||||
@@ -965,13 +871,6 @@ def memory_announce_focus(
|
||||
focus data. If another open session already has overlapping files, a warning
|
||||
is returned — stop and coordinate before proceeding.
|
||||
|
||||
args:
|
||||
- session_id: The active session id (from memory_start_session)
|
||||
- description: What you are about to work on (e.g. "Implementing Feature 7 in db.py")
|
||||
- files: List of file paths you plan to touch (e.g. ["bigmind/db.py", "src/server.py"])
|
||||
- ide_hint: Optional label for this IDE (e.g. "PyCharm", "IntelliJ", "VS Code")
|
||||
Shown on the profile page Live Sessions panel.
|
||||
|
||||
returns:
|
||||
- Acknowledgement with current focus set, or a conflict warning.
|
||||
"""
|
||||
@@ -1045,10 +944,10 @@ def memory_get_active_sessions() -> str:
|
||||
|
||||
@mcp.tool()
|
||||
def memory_log_token_save(
|
||||
session_id: str,
|
||||
description: str,
|
||||
tokens_saved: int,
|
||||
method_used: str = None,
|
||||
session_id: Annotated[str, Field(description="The active session id")],
|
||||
description: Annotated[str, Field(description="What was remembered or avoided (e.g. \"grep EuBP log instead of reading 80k lines\")")],
|
||||
tokens_saved: Annotated[int, Field(description="Rough estimate of tokens saved (e.g. 1_240_000)")],
|
||||
method_used: Annotated[str | None, Field(description="One of: 'memory_hit' | 'grep' | 'tail' | 'targeted_read' | 'other'")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Log a token efficiency event — record how many tokens were saved by using
|
||||
@@ -1062,12 +961,6 @@ def memory_log_token_save(
|
||||
Estimating tokens saved: tokens ≈ chars / 4.
|
||||
tokens_saved ≈ (chars_in_full_resource / 4) - (chars_in_result / 4)
|
||||
|
||||
args:
|
||||
- session_id: The active session id
|
||||
- description: What was remembered or avoided (e.g. "grep EuBP log instead of reading 80k lines")
|
||||
- tokens_saved: Rough estimate of tokens saved (e.g. 1_240_000)
|
||||
- method_used: One of: 'memory_hit' | 'grep' | 'tail' | 'targeted_read' | 'other'
|
||||
|
||||
returns:
|
||||
- Confirmation with running session total.
|
||||
"""
|
||||
@@ -1100,26 +993,17 @@ def memory_log_token_save(
|
||||
|
||||
@mcp.tool()
|
||||
def memory_remember_person(
|
||||
username: str,
|
||||
display_name: str = None,
|
||||
role: str = None,
|
||||
team: str = None,
|
||||
notes: str = None,
|
||||
bigmind_user: str = None,
|
||||
bigmind_url: str = None,
|
||||
username: Annotated[str, Field(description="Unique identifier (e.g. login name or first name).")],
|
||||
display_name: Annotated[str | None, Field(description="Full name (optional).")] = None,
|
||||
role: Annotated[str | None, Field(description="Job title or role (optional).")] = None,
|
||||
team: Annotated[str | None, Field(description="Team or project they belong to (optional).")] = None,
|
||||
notes: Annotated[str | None, Field(description="Free-form notes about this person (optional).")] = None,
|
||||
bigmind_user: Annotated[str | None, Field(description="Their BigMind username if they have an instance (optional).")] = None,
|
||||
bigmind_url: Annotated[str | None, Field(description="URL of their BigMind profile page (optional).")] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Store or update a person in the contacts directory.
|
||||
Call this whenever you learn something new about a colleague or AI peer.
|
||||
|
||||
Args:
|
||||
username: Unique identifier (e.g. login name or first name).
|
||||
display_name: Full name (optional).
|
||||
role: Job title or role (optional).
|
||||
team: Team or project they belong to (optional).
|
||||
notes: Free-form notes about this person (optional).
|
||||
bigmind_user: Their BigMind username if they have an instance (optional).
|
||||
bigmind_url: URL of their BigMind profile page (optional).
|
||||
"""
|
||||
user = _current_user()
|
||||
person_id = memory_store.upsert_person(
|
||||
@@ -1131,13 +1015,9 @@ def memory_remember_person(
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_recall_person(query: str, limit: int = 10) -> str:
|
||||
def memory_recall_person(query: Annotated[str, Field(description="Search keywords (e.g. a name, team, or role).")], limit: Annotated[int, Field(description="Max results to return (default 10).")] = 10) -> str:
|
||||
"""
|
||||
Search the contacts directory by name, role, team, or notes.
|
||||
|
||||
Args:
|
||||
query: Search keywords (e.g. a name, team, or role).
|
||||
limit: Max results to return (default 10).
|
||||
"""
|
||||
user = _current_user()
|
||||
results = memory_store.recall_person(user["id"], query, limit)
|
||||
@@ -1185,15 +1065,10 @@ def memory_list_people() -> str:
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def memory_link_ai(username: str, bigmind_user: str, bigmind_url: str = None) -> str:
|
||||
def memory_link_ai(username: Annotated[str, Field(description="The contact's username in your directory.")], bigmind_user: Annotated[str, Field(description="Their BigMind username.")], bigmind_url: Annotated[str | None, Field(description="URL of their BigMind profile page (optional).")] = None) -> str:
|
||||
"""
|
||||
Link a contact to their BigMind AI instance.
|
||||
The contact must already exist (use memory_remember_person first).
|
||||
|
||||
Args:
|
||||
username: The contact's username in your directory.
|
||||
bigmind_user: Their BigMind username.
|
||||
bigmind_url: URL of their BigMind profile page (optional).
|
||||
"""
|
||||
user = _current_user()
|
||||
found = memory_store.link_ai(user["id"], username, bigmind_user, bigmind_url)
|
||||
|
||||
@@ -0,0 +1,795 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CannaManage Brand Asset Generation Pipeline
|
||||
|
||||
Autonomous script to generate 257+ brand assets for CannaManage cannabis business management SaaS.
|
||||
Runs unattended, resume-safe via .progress.json.
|
||||
|
||||
Usage:
|
||||
cd /home/pplate/pi_mcps
|
||||
python mcp/mcp-image-gen/cannamanage_gen.py
|
||||
python mcp/mcp-image-gen/cannamanage_gen.py --dry-run
|
||||
python mcp/mcp-image-gen/cannamanage_gen.py --phase phase1_logos
|
||||
python mcp/mcp-image-gen/cannamanage_gen.py --model heretic
|
||||
|
||||
Output: ~/Pictures/cannamanage_brand/ with organized subfolders.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# --- Configuration ---
|
||||
OUTPUT_ROOT = Path.home() / "Pictures" / "cannamanage_brand"
|
||||
PROGRESS_FILE = OUTPUT_ROOT / ".progress.json"
|
||||
WORKFLOW_SCHNELL = Path(__file__).parent / "src/workflows/flux_schnell.json"
|
||||
WORKFLOW_HERETIC = Path(__file__).parent / "src/workflows/flux2_klein_heretic.json"
|
||||
|
||||
# Brand prefix applied to every prompt
|
||||
_BP = ("professional B2B SaaS brand design, CannaManage cannabis business management platform, "
|
||||
"modern tech aesthetic, clean minimalist style, premium quality, ")
|
||||
|
||||
# --- Full Asset Manifest (257 assets across 6 phases) ---
|
||||
ASSET_MANIFEST: List[Dict[str, Any]] = [
|
||||
|
||||
# ============================================================
|
||||
# PHASE 1 — Logo Suite (42 assets)
|
||||
# ============================================================
|
||||
|
||||
# Wordmark — 5 font directions (1024×512)
|
||||
{"id":"p1_wm_01","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_modern_sans",
|
||||
"prompt":_BP+"modern geometric sans-serif typography wordmark logo, deep emerald green #0D4F3C, clean white background, minimal cannabis leaf accent in letterform, high-end tech company wordmark, flat vector design","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_02","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_geometric",
|
||||
"prompt":_BP+"geometric typeface wordmark logo, sharp angles, emerald and gold color scheme, hexagonal grid subtle background, cannabis molecule silhouette in C letter, precision tech brand, white background","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_03","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_humanist",
|
||||
"prompt":_BP+"humanist sans-serif typeface wordmark, warm approachable professional style, forest green with amber gold accent, subtle leaf vein pattern in letterforms, trustworthy modern brand, white background","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_04","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_slab_serif",
|
||||
"prompt":_BP+"premium slab serif typography wordmark, dark charcoal and deep green palette, gold accent stripe, authoritative compliance management brand, pharmaceutical-grade trustworthiness, white background","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_05","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_minimal",
|
||||
"prompt":_BP+"ultra-minimal thin weight typography wordmark, single-color deep emerald, negative space leaf shape from letter spacing, Apple-inspired premium minimalism, pure white background","width":1024,"height":512,"steps":30},
|
||||
|
||||
# Icon / Symbol Only — 10 variations (512×512)
|
||||
{"id":"p1_ic_01","phase":"phase1_logos","subfolder":"icon_only","name":"icon_leaf_tech",
|
||||
"prompt":_BP+"abstract cannabis leaf formed from circuit board traces and data nodes app icon, emerald green on dark charcoal, tech-meets-nature, geometric precision, square icon format","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_02","phase":"phase1_logos","subfolder":"icon_only","name":"icon_c_mark_abstract",
|
||||
"prompt":_BP+"abstract letter C formed from cannabis plant stems and leaves brand icon, geometric minimalist, deep green gradient, negative space cannabis leaf inside C curve, white background","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_03","phase":"phase1_logos","subfolder":"icon_only","name":"icon_molecule_stylized",
|
||||
"prompt":_BP+"stylized cannabis molecule diagram brand mark, hexagonal ring structure, emerald green nodes and gold connecting lines, scientific precision, dark background, pharmaceutical-tech aesthetic","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_04","phase":"phase1_logos","subfolder":"icon_only","name":"icon_dashboard_grid",
|
||||
"prompt":_BP+"abstract dashboard grid symbol icon, 3x3 grid of squares with data bar and cannabis leaf overlaid, emerald and gold, SaaS platform brand mark, white background","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_05","phase":"phase1_logos","subfolder":"icon_only","name":"icon_plant_circuit",
|
||||
"prompt":_BP+"cannabis plant silhouette where stems are circuit board traces, leaves are data nodes, emerald green on white, half-organic half-digital, modern biotech brand mark","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_06","phase":"phase1_logos","subfolder":"icon_only","name":"icon_shield_leaf",
|
||||
"prompt":_BP+"shield shape with cannabis leaf geometric pattern inside, emerald green shield, gold leaf outline, trust and compliance brand mark, premium badge style","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_07","phase":"phase1_logos","subfolder":"icon_only","name":"icon_cm_monogram",
|
||||
"prompt":_BP+"interlocked letters C and M with cannabis leaf negative space monogram, geometric precision, deep emerald, gold accent, premium brand monogram, white background","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_08","phase":"phase1_logos","subfolder":"icon_only","name":"icon_hexagon_leaf",
|
||||
"prompt":_BP+"hexagon containing stylized cannabis leaf formed from clean lines, emerald hexagon dark outline, gold accent dot nodes at leaf tips, geometric cannabis brand mark, tech-forward minimal","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_09","phase":"phase1_logos","subfolder":"icon_only","name":"icon_growth_chart",
|
||||
"prompt":_BP+"upward growing cannabis plant silhouette transforming into ascending bar chart, emerald to gold gradient, business growth metaphor, modern flat icon design","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_10","phase":"phase1_logos","subfolder":"icon_only","name":"icon_infinity_leaf",
|
||||
"prompt":_BP+"infinity loop symbol where loops form two cannabis leaf shapes, emerald green line on white, continuous management and compliance cycle concept, premium SaaS logo mark","width":512,"height":512,"steps":30},
|
||||
|
||||
# Horizontal Lockups (1024×256)
|
||||
{"id":"p1_lh_01","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_light",
|
||||
"prompt":_BP+"horizontal logo lockup icon mark left wordmark text right, light white background, deep emerald, professional cannabis management SaaS layout","width":1024,"height":256,"steps":30},
|
||||
{"id":"p1_lh_02","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_dark",
|
||||
"prompt":_BP+"horizontal logo lockup icon left wordmark right, dark charcoal background, white and emerald logo, reversed color scheme, premium brand","width":1024,"height":256,"steps":30},
|
||||
{"id":"p1_lh_03","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_emerald_bg",
|
||||
"prompt":_BP+"horizontal logo lockup, white logo on deep emerald background, horizontal icon plus wordmark, brand banner version","width":1024,"height":256,"steps":30},
|
||||
{"id":"p1_lh_04","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_mono",
|
||||
"prompt":_BP+"horizontal logo lockup monochrome, all black on white, horizontal icon plus wordmark, professional print-ready version","width":1024,"height":256,"steps":30},
|
||||
|
||||
# Stacked Lockups (512×512)
|
||||
{"id":"p1_ls_01","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_light",
|
||||
"prompt":_BP+"stacked logo lockup icon centered above wordmark, light white background, emerald brand colors, square format, professional centered layout","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ls_02","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_dark",
|
||||
"prompt":_BP+"stacked logo lockup icon centered above wordmark, dark charcoal background, white and green logo, square format, dark version","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ls_03","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_emerald",
|
||||
"prompt":_BP+"stacked logo lockup, white icon and wordmark on emerald green background, centered square format, brand full-color version","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ls_04","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_mono",
|
||||
"prompt":_BP+"stacked logo lockup monochrome all-black on white, icon above wordmark, square format, print-ready logo","width":512,"height":512,"steps":30},
|
||||
|
||||
# Favicons (256×256)
|
||||
{"id":"p1_fv_01","phase":"phase1_logos","subfolder":"favicon","name":"favicon_emerald_leaf",
|
||||
"prompt":_BP+"favicon 256x256 square app icon, emerald green background, white geometric cannabis leaf icon, rounded square, minimal","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_02","phase":"phase1_logos","subfolder":"favicon","name":"favicon_dark_circuit",
|
||||
"prompt":_BP+"favicon dark charcoal square, emerald circuit-leaf icon, 256x256 app icon, sharp corners, professional SaaS favicon","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_03","phase":"phase1_logos","subfolder":"favicon","name":"favicon_white_green",
|
||||
"prompt":_BP+"favicon white background, deep green CM monogram leaf icon, 256x256 square, minimal browser favicon","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_04","phase":"phase1_logos","subfolder":"favicon","name":"favicon_gold_dark",
|
||||
"prompt":_BP+"favicon dark background, gold amber cannabis management icon mark, 256x256 premium app icon, warm gold on charcoal","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_05","phase":"phase1_logos","subfolder":"favicon","name":"favicon_gradient_green",
|
||||
"prompt":_BP+"favicon forest to emerald gradient background, white geometric icon, 256x256 square, modern SaaS app icon with gradient","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_06","phase":"phase1_logos","subfolder":"favicon","name":"favicon_outline_style",
|
||||
"prompt":_BP+"favicon white background, outline-only emerald cannabis leaf circuit icon, thin line illustration, 256x256, minimalist","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_07","phase":"phase1_logos","subfolder":"favicon","name":"favicon_rounded_modern",
|
||||
"prompt":_BP+"iOS-style rounded square app icon, emerald gradient background, white leaf-tech brand mark, 256x256, premium mobile app icon","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_08","phase":"phase1_logos","subfolder":"favicon","name":"favicon_badge_style",
|
||||
"prompt":_BP+"badge-style icon with thin border ring, emerald center with white CM letters, 256x256 square, compliance software favicon","width":256,"height":256,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 2 — Banner Suite (50 assets)
|
||||
# ============================================================
|
||||
|
||||
# Hero Website Banners (1920×1080)
|
||||
{"id":"p2_hw_01","phase":"phase2_banners","subfolder":"hero_website","name":"hero_dashboard_showcase",
|
||||
"prompt":_BP+"website hero banner 1920x1080, dark charcoal background, emerald UI dashboard mockup floating right, bold headline area left, gold accent lines, enterprise software marketing","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_02","phase":"phase2_banners","subfolder":"hero_website","name":"hero_compliance_trust",
|
||||
"prompt":_BP+"website hero banner 1920x1080, compliance and trust theme, deep green gradient, shield and checkmark iconography, cannabis regulatory compliance, white text area, subtle geometric pattern overlay","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_03","phase":"phase2_banners","subfolder":"hero_website","name":"hero_analytics_data",
|
||||
"prompt":_BP+"website hero banner 1920x1080, analytics theme, dark background, glowing data visualization charts in emerald and gold, business metrics, abstract data flowing design","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_04","phase":"phase2_banners","subfolder":"hero_website","name":"hero_team_enterprise",
|
||||
"prompt":_BP+"website hero banner 1920x1080, enterprise team theme, split design emerald left panel white right panel, diagonal split, geometric accents, SaaS marketing","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_05","phase":"phase2_banners","subfolder":"hero_website","name":"hero_nature_tech",
|
||||
"prompt":_BP+"website hero banner 1920x1080, nature meets technology, abstract cannabis plant growing from circuit board, emerald organic forms with gold tech circuit lines, dark sophisticated background","width":1280,"height":720,"steps":30},
|
||||
|
||||
# LinkedIn Banners (1584×396)
|
||||
{"id":"p2_li_01","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_corporate_green",
|
||||
"prompt":_BP+"LinkedIn company banner, deep emerald background, white wordmark centered, cannabis business management tagline, clean minimal corporate header","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_02","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_dark_gold",
|
||||
"prompt":_BP+"LinkedIn banner, dark charcoal background, gold accent stripe bottom, company name descriptor, professional enterprise header","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_03","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_pattern_overlay",
|
||||
"prompt":_BP+"LinkedIn banner, emerald base, subtle hexagonal cannabis molecule pattern overlay, semi-transparent, company branding prominent, wide horizontal header","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_04","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_split_design",
|
||||
"prompt":_BP+"LinkedIn banner, split design left dark right emerald, diagonal split line, cannabis management platform branding, clean sharp design","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_05","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_metrics_banner",
|
||||
"prompt":_BP+"LinkedIn banner showing key business metrics and KPI numbers, data-forward, emerald with gold numbers, analytics platform positioning","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_06","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_gradient_minimal",
|
||||
"prompt":_BP+"LinkedIn banner, forest green to emerald gradient, minimal white brand name and tagline only, ultra-clean professional header","width":1584,"height":396,"steps":30},
|
||||
|
||||
# Twitter/X Headers (1500×500)
|
||||
{"id":"p2_tw_01","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_bold_emerald",
|
||||
"prompt":_BP+"Twitter X header banner 1500x500, bold emerald full bleed background, large white brand name, cannabis management tagline, strong social media presence","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_02","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_dark_pattern",
|
||||
"prompt":_BP+"Twitter header 1500x500, dark charcoal with subtle cannabis geometric pattern, emerald and gold accents, professional SaaS brand social header","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_03","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_product_hint",
|
||||
"prompt":_BP+"Twitter header 1500x500, dark background with glimpse of dashboard interface, cannabis management software preview, professional tech company header","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_04","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_nature_abstract",
|
||||
"prompt":_BP+"Twitter header 1500x500, abstract cannabis plant growing into data streams, green to dark gradient, artistic organic meets digital aesthetic","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_05","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_community",
|
||||
"prompt":_BP+"Twitter header 1500x500, cannabis business community theme, connected nodes network in emerald green, SaaS platform connecting businesses","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_06","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_white_clean",
|
||||
"prompt":_BP+"Twitter header 1500x500, clean white background, emerald brand elements only, ultra-professional minimal social media header","width":1500,"height":500,"steps":30},
|
||||
|
||||
# Facebook Covers (820×312)
|
||||
{"id":"p2_fb_01","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_primary_brand",
|
||||
"prompt":_BP+"Facebook cover photo 820x312, primary brand colors emerald and charcoal, professional cannabis business company cover, centered branding","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_02","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_dark_professional",
|
||||
"prompt":_BP+"Facebook cover 820x312, dark sophisticated background, white and gold brand elements, enterprise platform premium cover","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_03","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_compliance_theme",
|
||||
"prompt":_BP+"Facebook cover 820x312, cannabis regulatory compliance theme, shield and verification iconography, emerald professional company cover","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_04","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_product_launch",
|
||||
"prompt":_BP+"Facebook cover 820x312, product launch announcement style, bold emerald with gold accents, exciting software release visual, dynamic tech company cover","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_05","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_industry_leader",
|
||||
"prompt":_BP+"Facebook cover 820x312, industry leadership positioning, cannabis business management market leader visual, professional authoritative design, emerald and gold","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_06","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_seasonal_spring",
|
||||
"prompt":_BP+"Facebook cover 820x312, spring fresh brand refresh, bright emerald with sage green organic elements, cannabis growth season theme, professional seasonal cover","width":820,"height":312,"steps":30},
|
||||
|
||||
# Google Display Ads (16 assets - 4 concepts × 4 sizes)
|
||||
{"id":"p2_ga_01a","phase":"phase2_banners","subfolder":"google_display","name":"gad_compliance_728x90",
|
||||
"prompt":_BP+"Google display ad leaderboard 728x90, simplify cannabis compliance theme, emerald green button, white background, professional B2B ad CTA","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_01b","phase":"phase2_banners","subfolder":"google_display","name":"gad_compliance_300x250",
|
||||
"prompt":_BP+"Google display ad medium rectangle 300x250, simplify cannabis compliance theme, emerald green design, bold headline, professional SaaS ad creative","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_01c","phase":"phase2_banners","subfolder":"google_display","name":"gad_compliance_160x600",
|
||||
"prompt":_BP+"Google display ad wide skyscraper 160x600, simplify cannabis compliance theme, tall vertical format, emerald green, professional B2B ad","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_01d","phase":"phase2_banners","subfolder":"google_display","name":"gad_compliance_320x50",
|
||||
"prompt":_BP+"Google display ad mobile banner 320x50, simplify compliance theme, minimal mobile ad, emerald green, cannabis management SaaS","width":320,"height":50,"steps":30},
|
||||
{"id":"p2_ga_02a","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_728x90",
|
||||
"prompt":_BP+"Google display ad 728x90, manage everything cannabis business theme, dashboard preview hint, dark charcoal professional leaderboard banner","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_02b","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_300x250",
|
||||
"prompt":_BP+"Google display ad 300x250, manage everything cannabis operations theme, product dashboard glimpse, emerald dark professional rectangle ad","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_02c","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_160x600",
|
||||
"prompt":_BP+"Google display skyscraper 160x600, manage cannabis business operations theme, vertical product feature list visual, emerald professional tall ad","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_02d","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_320x50",
|
||||
"prompt":_BP+"mobile banner 320x50, manage cannabis business theme, ultra-minimal mobile ad strip, brand colors","width":320,"height":50,"steps":30},
|
||||
{"id":"p2_ga_03a","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_728x90",
|
||||
"prompt":_BP+"Google ad 728x90, grow your cannabis business theme, upward growth arrow with cannabis leaf, gold and emerald, professional B2B leaderboard","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_03b","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_300x250",
|
||||
"prompt":_BP+"Google ad 300x250, cannabis business growth theme, ascending graph with emerald plant growth visual, professional SaaS rectangle ad","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_03c","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_160x600",
|
||||
"prompt":_BP+"skyscraper ad 160x600, cannabis business growth vertical story, plant growing upward through data visualization, emerald tall display ad","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_03d","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_320x50",
|
||||
"prompt":_BP+"mobile ad 320x50, grow cannabis business, minimal mobile strip ad emerald green","width":320,"height":50,"steps":30},
|
||||
{"id":"p2_ga_04a","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_728x90",
|
||||
"prompt":_BP+"Google ad 728x90, free trial call to action, bold gold CTA button, emerald professional leaderboard, cannabis management SaaS trial offer","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_04b","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_300x250",
|
||||
"prompt":_BP+"Google ad 300x250, free trial offer, gold button emerald design, cannabis management platform trial CTA rectangle ad","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_04c","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_160x600",
|
||||
"prompt":_BP+"skyscraper ad 160x600, free trial CTA vertical ad, gold call to action button, emerald SaaS platform","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_04d","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_320x50",
|
||||
"prompt":_BP+"mobile ad 320x50, free trial minimal mobile strip, gold CTA emerald brand","width":320,"height":50,"steps":30},
|
||||
|
||||
# App Store Feature Graphics (1024×500)
|
||||
{"id":"p2_as_01","phase":"phase2_banners","subfolder":"app_store","name":"appstore_hero_dashboard",
|
||||
"prompt":_BP+"app store feature graphic 1024x500, cannabis management app showcase, dark background with app dashboard UI preview, emerald interface elements, professional mobile app store hero","width":1024,"height":500,"steps":30},
|
||||
{"id":"p2_as_02","phase":"phase2_banners","subfolder":"app_store","name":"appstore_compliance_features",
|
||||
"prompt":_BP+"app store feature graphic 1024x500, compliance and legal features highlight, shield icons and checkmarks, emerald professional, cannabis compliance app feature graphic","width":1024,"height":500,"steps":30},
|
||||
{"id":"p2_as_03","phase":"phase2_banners","subfolder":"app_store","name":"appstore_analytics_focus",
|
||||
"prompt":_BP+"app store feature graphic 1024x500, business analytics and reporting feature, dashboard charts preview, gold and emerald data visualization, cannabis business intelligence app","width":1024,"height":500,"steps":30},
|
||||
{"id":"p2_as_04","phase":"phase2_banners","subfolder":"app_store","name":"appstore_team_management",
|
||||
"prompt":_BP+"app store feature graphic 1024x500, team and staff management, connected team nodes visualization, emerald professional, cannabis dispensary team management app","width":1024,"height":500,"steps":30},
|
||||
|
||||
# Email Header Banners (600×200)
|
||||
{"id":"p2_em_01","phase":"phase2_banners","subfolder":"email_header","name":"email_primary_brand",
|
||||
"prompt":_BP+"email header banner 600x200, primary brand header for newsletters, emerald with white logo area, professional email marketing header","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_02","phase":"phase2_banners","subfolder":"email_header","name":"email_welcome",
|
||||
"prompt":_BP+"welcome email header 600x200, warm welcome theme, emerald and sage gradient, onboarding email banner, new user email header","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_03","phase":"phase2_banners","subfolder":"email_header","name":"email_product_update",
|
||||
"prompt":_BP+"product update email header 600x200, new features announcement, gold accent notification style, software update email banner","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_04","phase":"phase2_banners","subfolder":"email_header","name":"email_compliance_alert",
|
||||
"prompt":_BP+"compliance alert email header 600x200, urgent notification theme, amber gold accent on dark, cannabis regulatory update email header, professional alert banner","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_05","phase":"phase2_banners","subfolder":"email_header","name":"email_monthly_report",
|
||||
"prompt":_BP+"monthly report email header 600x200, data and analytics theme, charts and metrics preview, emerald professional, cannabis business monthly summary","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_06","phase":"phase2_banners","subfolder":"email_header","name":"email_trial_ending",
|
||||
"prompt":_BP+"trial ending email header 600x200, urgency CTA theme, gold highlight on dark, platform trial expiry email banner, convert to paid","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_07","phase":"phase2_banners","subfolder":"email_header","name":"email_invoice",
|
||||
"prompt":_BP+"invoice and billing email header 600x200, clean minimal professional, white and emerald, SaaS billing email header, enterprise professional","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_08","phase":"phase2_banners","subfolder":"email_header","name":"email_dark_premium",
|
||||
"prompt":_BP+"dark premium email header 600x200, dark charcoal with gold and emerald accents, VIP or enterprise tier email, cannabis management premium header","width":600,"height":200,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 3 — Social Media Asset Pack (60 assets)
|
||||
# ============================================================
|
||||
|
||||
# Instagram Square Posts (1080×1080)
|
||||
{"id":"p3_ig_01","phase":"phase3_social","subfolder":"instagram_square","name":"insta_inventory_mgmt",
|
||||
"prompt":_BP+"Instagram post 1080x1080, inventory management feature highlight, cannabis stock tracking dashboard visualization, emerald UI elements, bold feature announcement, clean white and dark design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_02","phase":"phase3_social","subfolder":"instagram_square","name":"insta_compliance_track",
|
||||
"prompt":_BP+"Instagram post 1080x1080, compliance tracking feature, regulatory checklist visualization, shield and checkmark icons, emerald and gold, cannabis compliance SaaS post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_03","phase":"phase3_social","subfolder":"instagram_square","name":"insta_analytics_dash",
|
||||
"prompt":_BP+"Instagram post 1080x1080, analytics dashboard feature, business intelligence data visualization, emerald charts on dark background, cannabis business analytics","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_04","phase":"phase3_social","subfolder":"instagram_square","name":"insta_staff_scheduling",
|
||||
"prompt":_BP+"Instagram post 1080x1080, staff scheduling feature, team calendar and shift management visualization, cannabis dispensary staff management, clean emerald post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_05","phase":"phase3_social","subfolder":"instagram_square","name":"insta_pos_integration",
|
||||
"prompt":_BP+"Instagram post 1080x1080, POS system integration feature, cannabis point-of-sale connection visualization, integration nodes and arrows, emerald and gold tech post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_06","phase":"phase3_social","subfolder":"instagram_square","name":"insta_reporting",
|
||||
"prompt":_BP+"Instagram post 1080x1080, automated reporting feature, beautiful report document preview, emerald professional document visualization post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_07","phase":"phase3_social","subfolder":"instagram_square","name":"insta_multi_location",
|
||||
"prompt":_BP+"Instagram post 1080x1080, multi-location management feature, cannabis dispensary chain management, location pins on map with connecting emerald lines","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_08","phase":"phase3_social","subfolder":"instagram_square","name":"insta_mobile_app",
|
||||
"prompt":_BP+"Instagram post 1080x1080, mobile app feature highlight, iPhone and Android app preview mockup, cannabis management on-the-go, emerald app UI post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_09","phase":"phase3_social","subfolder":"instagram_square","name":"insta_security",
|
||||
"prompt":_BP+"Instagram post 1080x1080, enterprise security feature, data protection and encryption visualization, shield with lock icon, dark professional emerald security post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_10","phase":"phase3_social","subfolder":"instagram_square","name":"insta_brand_story",
|
||||
"prompt":_BP+"Instagram brand story post 1080x1080, company mission, cannabis industry empowerment, beautiful abstract plant and technology fusion illustration, emerald and gold, inspiring brand post","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Instagram Stories (1080×1920)
|
||||
{"id":"p3_st_01","phase":"phase3_social","subfolder":"instagram_story","name":"story_onboarding",
|
||||
"prompt":_BP+"Instagram story 1080x1920, onboarding tutorial slide, step-by-step platform setup, emerald vertical mobile design, swipe up CTA, professional SaaS story","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_02","phase":"phase3_social","subfolder":"instagram_story","name":"story_feature_announce",
|
||||
"prompt":_BP+"Instagram story 1080x1920, new feature announcement, bold emerald vertical design, software update story, gold accent highlight, tap to learn more CTA","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_03","phase":"phase3_social","subfolder":"instagram_story","name":"story_stat_highlight",
|
||||
"prompt":_BP+"Instagram story 1080x1920, industry statistic highlight, large bold number, dark background gold number emerald accent, data-driven story template","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_04","phase":"phase3_social","subfolder":"instagram_story","name":"story_customer_quote",
|
||||
"prompt":_BP+"Instagram story 1080x1920, customer testimonial quote, elegant quote typography on emerald background, cannabis business owner testimonial, premium brand story","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_05","phase":"phase3_social","subfolder":"instagram_story","name":"story_poll_template",
|
||||
"prompt":_BP+"Instagram story 1080x1920, interactive poll template, cannabis industry question visual, dark professional background, poll options styled in emerald and gold","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_06","phase":"phase3_social","subfolder":"instagram_story","name":"story_countdown",
|
||||
"prompt":_BP+"Instagram story 1080x1920, countdown timer event template, launch deadline visual, bold dramatic dark background with gold countdown element","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_07","phase":"phase3_social","subfolder":"instagram_story","name":"story_tips_series",
|
||||
"prompt":_BP+"Instagram story 1080x1920, cannabis compliance tip of the day template, bright educational story, numbered tip format, sage green professional guidance","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_08","phase":"phase3_social","subfolder":"instagram_story","name":"story_free_trial",
|
||||
"prompt":_BP+"Instagram story 1080x1920, free trial CTA story, bold gold call to action on dark emerald, cannabis management platform sign up, strong conversion design","width":720,"height":1280,"steps":30},
|
||||
|
||||
# LinkedIn Post Graphics (1200×627)
|
||||
{"id":"p3_lp_01","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_thought_leader",
|
||||
"prompt":_BP+"LinkedIn post graphic 1200x627, thought leadership article header, cannabis industry insights, professional editorial design, emerald brand with white content area","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_02","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_product_demo",
|
||||
"prompt":_BP+"LinkedIn post 1200x627, product demo announcement, screenshot preview teaser, cannabis management platform demo invitation, emerald professional post","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_03","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_hiring",
|
||||
"prompt":_BP+"LinkedIn hiring post 1200x627, we are hiring banner, team growth announcement, professional cannabis tech company hiring graphic, emerald and gold, company culture","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_04","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_industry_stat",
|
||||
"prompt":_BP+"LinkedIn post 1200x627, cannabis industry statistic infographic, large bold number, professional B2B data post, emerald green","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_05","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_partnership",
|
||||
"prompt":_BP+"LinkedIn partnership announcement 1200x627, strategic partnership visual, cannabis tech ecosystem, professional announcement graphic emerald","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_06","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_webinar",
|
||||
"prompt":_BP+"LinkedIn webinar promotion post 1200x627, cannabis compliance webinar announcement, date and topic visual, professional event promotion, emerald dark design","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_07","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_case_study",
|
||||
"prompt":_BP+"LinkedIn case study post 1200x627, customer success story preview, cannabis dispensary success metrics, gold numbers on dark, professional B2B case study promotional","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_08","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_award",
|
||||
"prompt":_BP+"LinkedIn award announcement post 1200x627, award or recognition, gold trophy badge element, cannabis tech industry recognition, celebratory professional post","width":1200,"height":627,"steps":30},
|
||||
|
||||
# Feature Announcement Cards (1080×1080)
|
||||
{"id":"p3_fc_01","phase":"phase3_social","subfolder":"feature_cards","name":"feature_inventory_scan",
|
||||
"prompt":_BP+"feature card 1080x1080, barcode scanning inventory management, cannabis product scan interface, mobile scanning visualization, emerald UI feature card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_02","phase":"phase3_social","subfolder":"feature_cards","name":"feature_auto_compliance",
|
||||
"prompt":_BP+"feature card 1080x1080, automated compliance reporting, automation icon with compliance checklist, cannabis regulatory automation, emerald professional feature announcement","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_03","phase":"phase3_social","subfolder":"feature_cards","name":"feature_real_time_alerts",
|
||||
"prompt":_BP+"feature card 1080x1080, real-time alerts and notifications, bell notification with cannabis threshold alerts, gold alert accent on dark, compliance notification feature","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_04","phase":"phase3_social","subfolder":"feature_cards","name":"feature_member_portal",
|
||||
"prompt":_BP+"feature card 1080x1080, member self-service portal, cannabis club member login interface, clean emerald member management, user portal visualization","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_05","phase":"phase3_social","subfolder":"feature_cards","name":"feature_api_integrations",
|
||||
"prompt":_BP+"feature card 1080x1080, API integrations ecosystem, connected software logos with hub, cannabis tech stack integration visualization, emerald connection diagram","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_06","phase":"phase3_social","subfolder":"feature_cards","name":"feature_batch_tracking",
|
||||
"prompt":_BP+"feature card 1080x1080, batch and lot tracking, cannabis product chain of custody visualization, numbered batch tracking flow, compliance feature card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_07","phase":"phase3_social","subfolder":"feature_cards","name":"feature_document_mgmt",
|
||||
"prompt":_BP+"feature card 1080x1080, document management system, cannabis licensing and permit documents organized, folder icons in emerald, digital document management","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_08","phase":"phase3_social","subfolder":"feature_cards","name":"feature_role_permissions",
|
||||
"prompt":_BP+"feature card 1080x1080, role-based permissions feature, user role hierarchy visualization, shield with user silhouettes, cannabis team access control, dark professional","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_09","phase":"phase3_social","subfolder":"feature_cards","name":"feature_export_reports",
|
||||
"prompt":_BP+"feature card 1080x1080, one-click export and reporting, PDF report generation from cannabis data, download arrow with report preview, emerald feature card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_10","phase":"phase3_social","subfolder":"feature_cards","name":"feature_audit_trail",
|
||||
"prompt":_BP+"feature card 1080x1080, complete audit trail, cannabis transaction history timeline, chronological log entries, compliance audit visualization, professional dark card","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Testimonial Cards (1080×1080)
|
||||
{"id":"p3_tc_01","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_dark_elegant",
|
||||
"prompt":_BP+"testimonial card 1080x1080, dark charcoal elegant quote card, gold quotation marks, customer name and title, cannabis business owner testimonial, premium design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_02","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_emerald_bold",
|
||||
"prompt":_BP+"testimonial card 1080x1080, bold emerald background, white quote text, customer review of cannabis management SaaS, bold confident design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_03","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_light_minimal",
|
||||
"prompt":_BP+"testimonial card 1080x1080, light white minimal quote card, emerald accent line, clean professional customer testimonial, minimal elegant design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_04","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_star_rating",
|
||||
"prompt":_BP+"testimonial card 1080x1080, 5-star rating testimonial, gold stars prominently displayed, customer quote below, cannabis management platform review","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_05","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_split_design",
|
||||
"prompt":_BP+"testimonial card 1080x1080, split design half dark half emerald, quote on dark side, customer info on emerald side, cannabis SaaS testimonial","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_06","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_gradient",
|
||||
"prompt":_BP+"testimonial card 1080x1080, forest to emerald gradient background, white elegant quote text, customer testimonial gradient design","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Stat/Data Cards (1080×1080)
|
||||
{"id":"p3_sc_01","phase":"phase3_social","subfolder":"stat_cards","name":"stat_market_size",
|
||||
"prompt":_BP+"stat card 1080x1080, cannabis market size statistic, large bold dollar amount, emerald gold number on dark background, cannabis industry market data","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_02","phase":"phase3_social","subfolder":"stat_cards","name":"stat_compliance_cost",
|
||||
"prompt":_BP+"stat card 1080x1080, compliance cost reduction statistic, percentage savings with management software, gold percentage number, cannabis business cost savings","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_03","phase":"phase3_social","subfolder":"stat_cards","name":"stat_time_savings",
|
||||
"prompt":_BP+"stat card 1080x1080, hours saved per week statistic, clock icon with bold number, cannabis dispensary operational time savings, emerald professional","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_04","phase":"phase3_social","subfolder":"stat_cards","name":"stat_dispensary_growth",
|
||||
"prompt":_BP+"stat card 1080x1080, dispensary industry growth rate, upward arrow with percentage growth, cannabis retail market growth stat, gold growth number on dark","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_05","phase":"phase3_social","subfolder":"stat_cards","name":"stat_compliance_fines",
|
||||
"prompt":_BP+"stat card 1080x1080, compliance violation fine amounts, cannabis regulatory penalty warning stat, amber warning colors, avoid fines messaging","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_06","phase":"phase3_social","subfolder":"stat_cards","name":"stat_customer_count",
|
||||
"prompt":_BP+"stat card 1080x1080, number of cannabis businesses managed, large customer count statistic, emerald green social proof data card, platform traction metric","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_07","phase":"phase3_social","subfolder":"stat_cards","name":"stat_roi_metric",
|
||||
"prompt":_BP+"stat card 1080x1080, ROI return on investment metric for cannabis management software, large gold ROI percentage, business value data card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_08","phase":"phase3_social","subfolder":"stat_cards","name":"stat_legal_markets",
|
||||
"prompt":_BP+"stat card 1080x1080, number of legal cannabis markets worldwide, globe icon with country count, cannabis legalization data, emerald global market stat","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 4 — UI & Product Assets (32 assets)
|
||||
# ============================================================
|
||||
|
||||
# App Icons (1024×1024)
|
||||
{"id":"p4_ai_01","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_ios_emerald",
|
||||
"prompt":_BP+"iOS app icon 1024x1024, Apple iOS style rounded square, emerald gradient background, white cannabis leaf tech icon mark, premium mobile app icon, App Store quality","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_02","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_ios_dark",
|
||||
"prompt":_BP+"iOS app icon 1024x1024, dark mode iOS icon, dark charcoal with emerald and gold brand mark, premium dark app icon, cannabis management mobile app","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_03","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_ios_gold",
|
||||
"prompt":_BP+"iOS app icon 1024x1024, premium gold accent, deep green background with gold leaf circuit brand mark, luxury cannabis management app icon","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_04","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_android_material",
|
||||
"prompt":_BP+"Android app icon 1024x1024, Material Design 3 style adaptive icon, emerald with white icon, Google Play Store quality, cannabis management Android app","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_05","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_android_dark",
|
||||
"prompt":_BP+"Android dark mode app icon 1024x1024, dark adaptive icon, emerald outline on near-black, Material You dark theme, cannabis management app","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_06","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_gradient_modern",
|
||||
"prompt":_BP+"app icon 1024x1024, modern gradient icon, forest green to bright emerald gradient background, white geometric cannabis tech mark, contemporary design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_07","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_flat_clean",
|
||||
"prompt":_BP+"app icon 1024x1024, flat design icon, solid emerald no gradient, white minimal icon mark, flat design philosophy, simple clean cannabis management","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_08","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_neumorphic",
|
||||
"prompt":_BP+"app icon 1024x1024, neumorphic soft UI style, light sage green background with embossed cannabis leaf icon, subtle shadows, premium modern icon design","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Device Mockups
|
||||
{"id":"p4_dm_01","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_macbook",
|
||||
"prompt":_BP+"dashboard shown on MacBook Pro mockup, professional product marketing, cannabis management SaaS on Apple laptop, emerald UI on screen, clean white studio background","width":1024,"height":640,"steps":30},
|
||||
{"id":"p4_dm_02","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_ipad",
|
||||
"prompt":_BP+"dashboard on iPad Pro mockup, cannabis management tablet interface, emerald UI on Apple iPad, clean marketing product shot, white background","width":1024,"height":768,"steps":30},
|
||||
{"id":"p4_dm_03","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_iphone",
|
||||
"prompt":_BP+"mobile app on iPhone mockup, cannabis management mobile interface, emerald green mobile UI, clean product marketing shot, white background","width":390,"height":844,"steps":30},
|
||||
{"id":"p4_dm_04","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_desktop_monitor",
|
||||
"prompt":_BP+"dashboard on large desktop monitor mockup, cannabis management enterprise software on wide screen, dark UI visible, professional product marketing display","width":1280,"height":720,"steps":30},
|
||||
{"id":"p4_dm_05","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_android_phone",
|
||||
"prompt":_BP+"mobile app on Android phone mockup, cannabis management Android interface, Material Design emerald UI, product marketing shot white background","width":390,"height":844,"steps":30},
|
||||
|
||||
# Onboarding Illustrations (800×600)
|
||||
{"id":"p4_ob_01","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_inventory",
|
||||
"prompt":_BP+"onboarding illustration 800x600, inventory management scene, cannabis product shelves with digital inventory overlay, flat illustration style, emerald and sage green","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_02","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_compliance",
|
||||
"prompt":_BP+"onboarding illustration 800x600, compliance tracking scene, person reviewing cannabis regulatory documents with digital checklist, confident professional flat illustration","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_03","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_analytics",
|
||||
"prompt":_BP+"onboarding illustration 800x600, analytics and reporting scene, business person analyzing cannabis sales charts, dashboard visualization, emerald data visualization flat","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_04","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_staff",
|
||||
"prompt":_BP+"onboarding illustration 800x600, staff scheduling scene, team members with shift calendar, cannabis dispensary team management, professional flat art emerald","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_05","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_pos",
|
||||
"prompt":_BP+"onboarding illustration 800x600, POS integration scene, cannabis point of sale system connected to management platform, tech integration flat illustration, emerald","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_06","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_reporting",
|
||||
"prompt":_BP+"onboarding illustration 800x600, automated reporting scene, report documents generating automatically, magic automation illustration, emerald gold professional flat art","width":800,"height":600,"steps":30},
|
||||
|
||||
# Empty States (600×400)
|
||||
{"id":"p4_es_01","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_data",
|
||||
"prompt":_BP+"empty state illustration 600x400, no data yet, friendly cannabis leaf with empty chart, get started messaging, emerald minimal SaaS illustration","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_02","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_results",
|
||||
"prompt":_BP+"empty state illustration 600x400, no search results found, magnifying glass with cannabis leaf, friendly empty state, emerald minimal","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_03","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_alerts",
|
||||
"prompt":_BP+"empty state illustration 600x400, no compliance alerts, happy shield with checkmark, all clear illustration, cannabis compliance all good state, emerald positive","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_04","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_members",
|
||||
"prompt":_BP+"empty state illustration 600x400, no members added yet, friendly people silhouettes with plus icon, cannabis club member management, emerald add members","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_05","phase":"phase4_ui","subfolder":"empty_states","name":"empty_loading_data",
|
||||
"prompt":_BP+"empty state illustration 600x400, loading and processing data, gentle spinner with cannabis leaf, patient loading state, emerald animated-style still illustration","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_06","phase":"phase4_ui","subfolder":"empty_states","name":"empty_offline",
|
||||
"prompt":_BP+"empty state illustration 600x400, offline or connection error, disconnected wifi with cannabis leaf, friendly error state, amber warning on emerald","width":600,"height":400,"steps":30},
|
||||
|
||||
# Splash/Loading Screens (1080×1920)
|
||||
{"id":"p4_sp_01","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_primary",
|
||||
"prompt":_BP+"splash screen 1080x1920, app loading screen, dark charcoal background, large centered brand logo mark, subtle emerald glow effect, premium app loading screen","width":720,"height":1280,"steps":30},
|
||||
{"id":"p4_sp_02","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_emerald",
|
||||
"prompt":_BP+"splash screen 1080x1920, emerald background, white logo centered, minimal loading indicator, cannabis management app splash, clean brand loading screen","width":720,"height":1280,"steps":30},
|
||||
{"id":"p4_sp_03","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_animated_hint",
|
||||
"prompt":_BP+"splash screen 1080x1920, animated concept, cannabis leaf particles converging into logo mark, dark background with emerald particles, dynamic loading screen first frame","width":720,"height":1280,"steps":30},
|
||||
{"id":"p4_sp_04","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_gradient",
|
||||
"prompt":_BP+"splash screen 1080x1920, dramatic dark to emerald gradient background, white brand mark, premium loading experience, cannabis SaaS gradient splash","width":720,"height":1280,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 5 — Brand Collateral (38 assets)
|
||||
# ============================================================
|
||||
|
||||
# Business Cards (900×504)
|
||||
{"id":"p5_bc_01f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_modern_front",
|
||||
"prompt":_BP+"business card front 900x504, modern minimal style, emerald left accent panel, white main area, name and title placeholder, cannabis management SaaS company card, premium print","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_01b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_modern_back",
|
||||
"prompt":_BP+"business card back 900x504, modern minimal style, full emerald back with white logo centered, website and tagline, cannabis management SaaS card back","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_02f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_dark_front",
|
||||
"prompt":_BP+"business card front 900x504, dark luxury style, dark charcoal background, gold foil accent logo, premium cannabis management company card, executive tier","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_02b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_dark_back",
|
||||
"prompt":_BP+"business card back 900x504, dark luxury style, full dark charcoal, gold logo and emerald accent, premium back of card","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_03f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_pattern_front",
|
||||
"prompt":_BP+"business card front 900x504, geometric cannabis pattern accent, white card with subtle hexagonal pattern header, professional pattern card front","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_03b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_pattern_back",
|
||||
"prompt":_BP+"business card back 900x504, cannabis geometric pattern full bleed, emerald hexagonal pattern background, white logo, pattern card back","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_04f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_bold_front",
|
||||
"prompt":_BP+"business card front 900x504, bold typographic style, large emerald brand name, clean white card, typography-forward business card","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_04b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_bold_back",
|
||||
"prompt":_BP+"business card back 900x504, bold style, split emerald and white back, contact details area, cannabis company card back bold design","width":900,"height":504,"steps":30},
|
||||
|
||||
# Pitch Deck Covers (1920×1080)
|
||||
{"id":"p5_pd_01","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_investor_dark",
|
||||
"prompt":_BP+"pitch deck cover slide 1920x1080, investor presentation, dark sophisticated background, large logo centered, funding round subtitle area, premium cannabis SaaS investor deck","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_02","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_growth_story",
|
||||
"prompt":_BP+"pitch deck cover 1920x1080, growth story visual, ascending cannabis plant becoming data chart, emerald to gold gradient, investor-grade presentation cover","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_03","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_market_opp",
|
||||
"prompt":_BP+"pitch deck cover 1920x1080, market opportunity theme, cannabis industry size visualization, globe with highlighted legal markets, emerald professional investor presentation","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_04","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_team_deck",
|
||||
"prompt":_BP+"pitch deck cover 1920x1080, team presentation version, professional team imagery backdrop, cannabis tech startup team slide, emerald brand, people-forward investor deck","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_05","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_product_demo",
|
||||
"prompt":_BP+"pitch deck cover 1920x1080, product demo deck, dashboard preview hero visual, cannabis management SaaS product tour deck, emerald UI preview","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_06","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_minimal_clean",
|
||||
"prompt":_BP+"pitch deck cover 1920x1080, ultra-minimal clean slide, white background, large emerald brand name only, minimalist investor presentation","width":1280,"height":720,"steps":30},
|
||||
|
||||
# One-Pager Headers (1200×400)
|
||||
{"id":"p5_op_01","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_primary",
|
||||
"prompt":_BP+"one-pager header 1200x400, primary brand header, emerald full bleed, white logo and tagline, cannabis management SaaS brochure header, print quality","width":1200,"height":400,"steps":30},
|
||||
{"id":"p5_op_02","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_feature_rich",
|
||||
"prompt":_BP+"one-pager header 1200x400, feature-rich header, dashboard preview glimpse, cannabis management platform features introduction, professional SaaS marketing","width":1200,"height":400,"steps":30},
|
||||
{"id":"p5_op_03","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_compliance",
|
||||
"prompt":_BP+"one-pager header 1200x400, compliance focus version, legal and regulatory theme, cannabis compliance management, shield icons, dark professional header","width":1200,"height":400,"steps":30},
|
||||
{"id":"p5_op_04","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_dark_premium",
|
||||
"prompt":_BP+"one-pager header 1200x400, dark premium version, charcoal background with gold and emerald accents, enterprise tier marketing collateral header","width":1200,"height":400,"steps":30},
|
||||
|
||||
# Trade Show Banners (800×2000 = 33x80in proportions)
|
||||
{"id":"p5_ts_01","phase":"phase5_collateral","subfolder":"trade_show","name":"tradeshow_primary_brand",
|
||||
"prompt":_BP+"trade show pull-up banner tall vertical 800x2000, primary brand version, emerald top with logo, white middle with key features listed, dark bottom with CTA, cannabis management SaaS conference banner","width":512,"height":1280,"steps":30},
|
||||
{"id":"p5_ts_02","phase":"phase5_collateral","subfolder":"trade_show","name":"tradeshow_product_showcase",
|
||||
"prompt":_BP+"trade show banner tall vertical 800x2000, product showcase, dashboard UI hero visual, dark sophisticated background, emerald accents, cannabis management software exhibition display","width":512,"height":1280,"steps":30},
|
||||
{"id":"p5_ts_03","phase":"phase5_collateral","subfolder":"trade_show","name":"tradeshow_compliance_focus",
|
||||
"prompt":_BP+"trade show banner tall vertical 800x2000, compliance authority positioning, legal cannabis management expertise, professional regulatory focus, emerald and gold tall exhibition banner","width":512,"height":1280,"steps":30},
|
||||
|
||||
# Sticker/Swag Designs (600×600)
|
||||
{"id":"p5_sk_01","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_leaf_circuit",
|
||||
"prompt":_BP+"sticker design 600x600, cannabis leaf made of circuit traces, emerald on white, die-cut sticker style, fun tech cannabis brand sticker","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_02","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_wordmark_badge",
|
||||
"prompt":_BP+"sticker badge 600x600, rounded rectangle badge, emerald background, white brand text, premium brand sticker, laptop sticker style","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_03","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_compliance_hero",
|
||||
"prompt":_BP+"sticker 600x600, compliance superhero, shield with cannabis leaf and checkmark, fun illustrated sticker, emerald and gold, die-cut design","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_04","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_data_plant",
|
||||
"prompt":_BP+"fun sticker 600x600, cannabis plant growing into data chart bars, punchy colorful sticker art, emerald plant gold bars, square sticker","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_05","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_canna_astronaut",
|
||||
"prompt":_BP+"sticker 600x600, cartoon astronaut holding cannabis leaf and laptop, space tech meets cannabis, fun illustrated sticker, emerald spacesuit, brand mascot concept","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_06","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_powered_by",
|
||||
"prompt":_BP+"powered by sticker 600x600, powered by CannaManage badge, small horizontal badge sticker, emerald and white, partner sticker for cannabis businesses","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_07","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_compliance_100",
|
||||
"prompt":_BP+"sticker 600x600, 100 percent compliant badge, bold green circle with checkmark and percentage, compliance achievement sticker, cannabis business compliance badge","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_08","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_hexagon_icon",
|
||||
"prompt":_BP+"hexagon sticker 600x600, hexagonal border with cannabis circuit icon, honeycomb management brand sticker, emerald hex border gold icon, premium die-cut","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_09","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_420_compliant",
|
||||
"prompt":_BP+"funny sticker 600x600, 420 compliant badge design, playful cannabis compliance humor, professional but fun brand sticker, emerald with gold numbers","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_10","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_manage_everything",
|
||||
"prompt":_BP+"sticker 600x600, manage everything tagline, bold typography sticker, emerald background white text, punchy brand statement sticker","width":600,"height":600,"steps":30},
|
||||
|
||||
# Email Signature Blocks (600×150)
|
||||
{"id":"p5_es_01","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_primary",
|
||||
"prompt":_BP+"email signature graphic block 600x150, primary brand, horizontal logo left with tagline, emerald line divider, professional email signature banner","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_02","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_dark",
|
||||
"prompt":_BP+"email signature block 600x150, dark version, charcoal background white logo emerald accent, premium email signature graphic","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_03","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_minimal",
|
||||
"prompt":_BP+"email signature graphic 600x150, minimal version, just logo and website, very clean white background, ultra-minimal email signature banner","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_04","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_promo",
|
||||
"prompt":_BP+"email signature promo block 600x150, promotional version with free trial CTA, gold button area, white background emerald brand, conversion CTA signature","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_05","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_social",
|
||||
"prompt":_BP+"email signature block 600x150, social media icons version, small social platform icons in emerald, company signature with social links footer","width":600,"height":150,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 6 — Bonus / Wildcard Assets (35 assets)
|
||||
# ============================================================
|
||||
|
||||
# Animated Banner First-Frame Stills (1200×628)
|
||||
{"id":"p6_an_01","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_particle_logo",
|
||||
"prompt":_BP+"animated banner first frame 1200x628, cannabis leaf particles forming logo mark mid-flight, dark background emerald particles, designed for animation, static concept frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_02","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_data_flow",
|
||||
"prompt":_BP+"animated banner first frame 1200x628, data flow visualization beginning, cannabis data streams starting to form dashboard, dark background emerald data lines, animation concept first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_03","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_growth_chart",
|
||||
"prompt":_BP+"animated banner still 1200x628, cannabis business growth chart animation first frame, bar chart at zero about to animate upward, gold bars on dark, growth animation concept","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_04","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_compliance_check",
|
||||
"prompt":_BP+"animated banner first frame 1200x628, compliance checklist items unchecked ready to animate with checkmarks, emerald checklist on white, compliance animation concept","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_05","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_plant_grow",
|
||||
"prompt":_BP+"animated banner still 1200x628, cannabis plant seedling about to grow into data visualization plant, dark background seed sprouting, growth animation first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_06","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_typing_headline",
|
||||
"prompt":_BP+"animated banner still 1200x628, typing cursor before headline text, empty headline with blinking cursor concept, emerald CTA button below, typewriter animation first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_07","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_integration_connect",
|
||||
"prompt":_BP+"animated banner still 1200x628, integration ecosystem nodes about to connect, partner logos as unconnected nodes, emerald connecting lines forming, integration animation first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_08","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_counter_stat",
|
||||
"prompt":_BP+"animated banner still 1200x628, number counter animation first frame showing zero, gold large number about to count up to impressive stat, dark background, counter animation concept","width":1200,"height":628,"steps":30},
|
||||
|
||||
# Dark Mode vs Light Mode UI Pairs (1200×800 each = 12 total)
|
||||
{"id":"p6_dm_01l","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_dashboard_light",
|
||||
"prompt":_BP+"UI preview card 1200x800, light mode dashboard interface, clean white background, emerald UI elements, cannabis management SaaS light theme preview","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_01d","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_dashboard_dark",
|
||||
"prompt":_BP+"UI preview card 1200x800, dark mode dashboard interface, dark charcoal background, emerald glowing UI elements, cannabis management SaaS dark theme preview, premium dark mode","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_02l","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_compliance_light",
|
||||
"prompt":_BP+"UI preview 1200x800, compliance module light mode, white clean interface with compliance checklist and status indicators, emerald checkmarks, cannabis compliance UI","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_02d","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_compliance_dark",
|
||||
"prompt":_BP+"UI preview 1200x800, compliance module dark mode, dark interface with glowing emerald compliance indicators, cannabis compliance UI dark theme, premium","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_03l","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_analytics_light",
|
||||
"prompt":_BP+"UI preview 1200x800, analytics dashboard light mode, white background with colorful cannabis business charts, emerald and gold data visualization, light theme","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_03d","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_analytics_dark",
|
||||
"prompt":_BP+"UI preview 1200x800, analytics dashboard dark mode, dark background with glowing emerald and gold cannabis business charts, dramatic dark mode analytics UI","width":1200,"height":800,"steps":40},
|
||||
|
||||
# Integration/Partnership Badges (400×200)
|
||||
{"id":"p6_ib_01","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_pos_integration",
|
||||
"prompt":_BP+"integration badge 400x200, integrates with POS systems badge, cannabis point-of-sale integration certification, emerald badge design, CannaManage plus POS icon, professional","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_02","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_accounting",
|
||||
"prompt":_BP+"integration badge 400x200, accounting software integration badge, cannabis financial software connection, emerald badge with accounting icon, works with your accounting tools","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_03","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_compliance_tools",
|
||||
"prompt":_BP+"integration badge 400x200, compliance tool integration badge, cannabis regulatory software connection, emerald shield badge design, integrates with compliance tools","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_04","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_api_ready",
|
||||
"prompt":_BP+"API ready badge 400x200, API first platform badge, developer integration badge, emerald and code brackets design, cannabis management API integration badge","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_05","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_partner_certified",
|
||||
"prompt":_BP+"partner certified badge 400x200, certified technology partner badge design, emerald official partner badge, cannabis management platform partner program","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_06","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_white_label",
|
||||
"prompt":_BP+"white label partner badge 400x200, white label ready platform badge, cannabis SaaS white label partner certification, professional partner badge emerald","width":400,"height":200,"steps":40},
|
||||
|
||||
# Trust/Award Badges (400×400)
|
||||
{"id":"p6_tb_01","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_gdpr_compliant",
|
||||
"prompt":_BP+"trust badge 400x400, GDPR compliant badge design, data protection certification, emerald shield with EU star circle, cannabis SaaS data privacy trust badge","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_02","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_soc2_ready",
|
||||
"prompt":_BP+"trust badge 400x400, SOC 2 ready certification badge, enterprise security trust badge, professional certification seal, dark emerald badge design","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_03","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_uptime_guarantee",
|
||||
"prompt":_BP+"trust badge 400x400, 99.9 percent uptime guarantee badge, reliability certification, gold uptime number on emerald badge, cannabis SaaS reliability trust signal","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_04","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_industry_choice",
|
||||
"prompt":_BP+"award badge 400x400, cannabis industry choice award badge design, industry recognition award seal, gold and emerald award badge","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_05","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_money_back",
|
||||
"prompt":_BP+"trust badge 400x400, 30-day money back guarantee badge, customer confidence seal, emerald circle badge with guarantee text, cannabis SaaS satisfaction badge","width":400,"height":400,"steps":40},
|
||||
]
|
||||
|
||||
# Bump steps for quality on phases with smaller dimensions
|
||||
# (Patrick's feedback: more iterations = better quality on Flux.1 Schnell)
|
||||
for _a in ASSET_MANIFEST:
|
||||
if _a["steps"] == 30 and _a["width"] <= 1080 and _a["height"] <= 1080:
|
||||
_a["steps"] = 40
|
||||
|
||||
|
||||
def load_progress() -> Dict[str, Any]:
|
||||
"""Load resume state from .progress.json."""
|
||||
if PROGRESS_FILE.exists():
|
||||
try:
|
||||
with open(PROGRESS_FILE) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
return {"completed": [], "failed": [], "started_at": time.strftime("%Y-%m-%dT%H:%M:%S")}
|
||||
|
||||
|
||||
def save_progress(progress: Dict[str, Any]) -> None:
|
||||
"""Save progress state."""
|
||||
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
with open(PROGRESS_FILE, "w") as f:
|
||||
json.dump(progress, f, indent=2)
|
||||
|
||||
|
||||
def load_workflow(model: str) -> Dict:
|
||||
"""Load the appropriate workflow JSON."""
|
||||
path = WORKFLOW_HERETIC if model == "heretic" else WORKFLOW_SCHNELL
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def submit_prompt(comfyui_url: str, workflow: Dict) -> str:
|
||||
"""Submit prompt to ComfyUI."""
|
||||
data = json.dumps({"prompt": workflow}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{comfyui_url}/prompt", data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())["prompt_id"]
|
||||
|
||||
|
||||
def wait_for_image(comfyui_url: str, prompt_id: str, timeout: int = 300) -> Dict | None:
|
||||
"""Poll for completion."""
|
||||
print(" ⏳ Waiting for ComfyUI...", end="", flush=True)
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
with urllib.request.urlopen(f"{comfyui_url}/history/{prompt_id}") as resp:
|
||||
history = json.loads(resp.read())
|
||||
if prompt_id in history:
|
||||
print(" done.", flush=True)
|
||||
outputs = history[prompt_id].get("outputs", {})
|
||||
for node_out in outputs.values():
|
||||
if "images" in node_out:
|
||||
return node_out["images"][0]
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
print(".", end="", flush=True)
|
||||
time.sleep(2)
|
||||
print(" timeout!", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
def download_image(comfyui_url: str, image_info: Dict, output_path: Path) -> bool:
|
||||
"""Download the generated image."""
|
||||
try:
|
||||
url = f"{comfyui_url}/view?filename={image_info['filename']}&subfolder={image_info.get('subfolder', '')}&type=output"
|
||||
with urllib.request.urlopen(url) as resp:
|
||||
img_data = resp.read()
|
||||
output_path.write_bytes(img_data)
|
||||
print(f" ✅ Saved: {output_path} ({len(img_data) // 1024}KB)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ❌ Download failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def patch_workflow(workflow: Dict, asset: Dict, model: str) -> Dict:
|
||||
"""Patch workflow with asset params using per-model node IDs."""
|
||||
seed = random.randint(0, 2**32 - 1)
|
||||
if model == "heretic":
|
||||
# flux2_klein_heretic.json nodes: 2=pos, 3=neg, 6=latent(w/h), 7=scheduler(steps), 10=noise(seed), 13=save
|
||||
workflow["2"]["inputs"]["text"] = asset["prompt"]
|
||||
workflow["3"]["inputs"]["text"] = ""
|
||||
workflow["6"]["inputs"]["width"] = asset["width"]
|
||||
workflow["6"]["inputs"]["height"] = asset["height"]
|
||||
workflow["7"]["inputs"]["steps"] = asset["steps"]
|
||||
workflow["13"]["inputs"]["filename_prefix"] = asset["name"]
|
||||
if "10" in workflow:
|
||||
workflow["10"]["inputs"]["noise_seed"] = seed
|
||||
else:
|
||||
# flux_schnell.json nodes: 6=pos, 33=neg, 27=latent(w/h), 13=ksampler(steps/seed), 9=save
|
||||
workflow["6"]["inputs"]["text"] = asset["prompt"]
|
||||
workflow["33"]["inputs"]["text"] = ""
|
||||
workflow["27"]["inputs"]["width"] = asset["width"]
|
||||
workflow["27"]["inputs"]["height"] = asset["height"]
|
||||
workflow["13"]["inputs"]["steps"] = asset["steps"]
|
||||
workflow["13"]["inputs"]["seed"] = seed
|
||||
workflow["9"]["inputs"]["filename_prefix"] = asset["name"]
|
||||
return workflow
|
||||
|
||||
|
||||
def generate_asset(comfyui_url: str, asset: Dict, model: str, progress: Dict) -> bool:
|
||||
"""Generate a single asset."""
|
||||
if asset["id"] in progress["completed"]:
|
||||
print(f" ⏭️ Skipping completed: {asset['name']}")
|
||||
return True
|
||||
|
||||
print(f"\n Prompt : {asset['prompt'][:80]}...")
|
||||
print(f" Size : {asset['width']}×{asset['height']} Steps: {asset['steps']}")
|
||||
|
||||
try:
|
||||
workflow = load_workflow(model)
|
||||
workflow = patch_workflow(workflow, asset, model)
|
||||
|
||||
prompt_id = submit_prompt(comfyui_url, workflow)
|
||||
image_info = wait_for_image(comfyui_url, prompt_id)
|
||||
|
||||
if not image_info:
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
output_dir = OUTPUT_ROOT / asset["phase"] / asset["subfolder"]
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / f"{asset['name']}.png"
|
||||
|
||||
if download_image(comfyui_url, image_info, output_path):
|
||||
progress["completed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return True
|
||||
else:
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="CannaManage Brand Asset Generation Pipeline")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Print manifest without generating")
|
||||
parser.add_argument("--phase", help="Generate only assets from this phase (e.g. phase1_logos)")
|
||||
parser.add_argument("--model", choices=["schnell", "heretic"], default="schnell",
|
||||
help="Model: schnell (~10-20s/img) or heretic (~52s/img, higher quality)")
|
||||
parser.add_argument("--comfyui", default="http://localhost:8188", help="ComfyUI URL")
|
||||
parser.add_argument("--steps", type=int, default=None,
|
||||
help="Override steps for all assets (e.g. --steps 12 for higher quality)")
|
||||
args = parser.parse_args()
|
||||
|
||||
comfyui_url = args.comfyui
|
||||
|
||||
# Apply global steps override if requested
|
||||
manifest = ASSET_MANIFEST
|
||||
if args.steps:
|
||||
manifest = [{**a, "steps": args.steps} for a in manifest]
|
||||
|
||||
# Filter by phase if requested
|
||||
to_generate = [a for a in manifest if not args.phase or a["phase"] == args.phase]
|
||||
|
||||
print("🚀 CannaManage Brand Asset Generation Pipeline")
|
||||
print(f" Output : {OUTPUT_ROOT}")
|
||||
print(f" Model : {args.model}")
|
||||
print(f" ComfyUI : {comfyui_url}")
|
||||
print(f" Total : {len(ASSET_MANIFEST)} assets in manifest")
|
||||
print(f" Selected: {len(to_generate)} assets to generate")
|
||||
|
||||
if args.dry_run:
|
||||
phases: Dict[str, int] = {}
|
||||
for a in to_generate:
|
||||
phases[a["phase"]] = phases.get(a["phase"], 0) + 1
|
||||
for ph, count in phases.items():
|
||||
print(f"\n {ph} ({count} assets):")
|
||||
for a in to_generate:
|
||||
if a["phase"] == ph:
|
||||
print(f" {a['id']:14} | {a['name']:35} | {a['width']}×{a['height']} steps={a['steps']}")
|
||||
total_min_est = sum(a["steps"] * 2.5 for a in to_generate) / 60
|
||||
print(f"\n ⏱️ Estimated runtime (schnell): ~{total_min_est:.0f} minutes")
|
||||
print("\nDry run complete. Remove --dry-run to begin generation.")
|
||||
return
|
||||
|
||||
progress = load_progress()
|
||||
remaining = [a for a in to_generate if a["id"] not in progress["completed"]]
|
||||
print(f" Resume : {len(progress['completed'])} completed, {len(progress['failed'])} failed, {len(remaining)} remaining")
|
||||
|
||||
if not remaining:
|
||||
print("\n✅ All selected assets already complete!")
|
||||
return
|
||||
|
||||
print(f"\nStarting generation... (Ctrl+C to pause — progress is saved)")
|
||||
|
||||
n_done = 0
|
||||
n_fail = 0
|
||||
for i, asset in enumerate(to_generate, 1):
|
||||
if asset["id"] in progress["completed"]:
|
||||
continue
|
||||
print(f"\n[{asset['phase']}] [{i}/{len(to_generate)}] {asset['name']}")
|
||||
if generate_asset(comfyui_url, asset, args.model, progress):
|
||||
n_done += 1
|
||||
else:
|
||||
n_fail += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 PIPELINE COMPLETE")
|
||||
print(f" ✅ Completed this run : {n_done}")
|
||||
print(f" ❌ Failed this run : {n_fail}")
|
||||
print(f" 📦 Total completed : {len(progress['completed'])} / {len(ASSET_MANIFEST)}")
|
||||
if progress["failed"]:
|
||||
print(f" Failed IDs: {', '.join(progress['failed'][-10:])}")
|
||||
print(f" Assets saved to: {OUTPUT_ROOT}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,801 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
ClubManage Brand Asset Generation Pipeline
|
||||
|
||||
Autonomous script to generate 257+ brand assets for a generic club management SaaS platform.
|
||||
All images are text-free / typography-free — pure visual/icon design only.
|
||||
Runs unattended, resume-safe via .progress.json.
|
||||
|
||||
Usage:
|
||||
cd /home/pplate/pi_mcps
|
||||
python mcp/mcp-image-gen/clubmanage_gen.py
|
||||
python mcp/mcp-image-gen/clubmanage_gen.py --dry-run
|
||||
python mcp/mcp-image-gen/clubmanage_gen.py --phase phase1_logos
|
||||
python mcp/mcp-image-gen/clubmanage_gen.py --model heretic
|
||||
|
||||
Output: ~/Pictures/clubmanage_brand/ with organized subfolders.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# --- Configuration ---
|
||||
OUTPUT_ROOT = Path.home() / "Pictures" / "clubmanage_brand"
|
||||
PROGRESS_FILE = OUTPUT_ROOT / ".progress.json"
|
||||
WORKFLOW_SCHNELL = Path(__file__).parent / "src/workflows/flux_schnell.json"
|
||||
WORKFLOW_HERETIC = Path(__file__).parent / "src/workflows/flux2_klein_heretic.json"
|
||||
|
||||
# Brand prefix — generic club management SaaS, navy/teal palette, STRICTLY no text
|
||||
_BP = ("professional B2B SaaS brand design, club management platform, "
|
||||
"modern tech aesthetic, clean minimalist style, premium quality, "
|
||||
"deep navy blue and teal color scheme, no text, no words, no letters, "
|
||||
"no numbers, no labels, no typography, pure visual icon design, ")
|
||||
|
||||
# Negative prompt suffix for heretic (CFGGuider supports real negative guidance)
|
||||
_NEG = "text, words, letters, numbers, labels, typography, fonts, captions, watermarks, titles, subtitles"
|
||||
|
||||
# --- Full Asset Manifest (257 assets across 6 phases) ---
|
||||
ASSET_MANIFEST: List[Dict[str, Any]] = [
|
||||
|
||||
# ============================================================
|
||||
# PHASE 1 — Logo Suite (42 assets)
|
||||
# ============================================================
|
||||
|
||||
# Wordmark concept shapes — 5 directions (1024×512)
|
||||
{"id":"p1_wm_01","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_modern_sans",
|
||||
"prompt":_BP+"modern geometric sans-serif shaped abstract mark, deep navy #1A237E, clean white background, minimal people-group silhouette accent, high-end tech company wordmark shape, flat vector design","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_02","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_geometric",
|
||||
"prompt":_BP+"geometric abstract wordmark shape, sharp angles, navy and teal color scheme, hexagonal grid subtle background, membership network silhouette, precision tech brand, white background","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_03","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_humanist",
|
||||
"prompt":_BP+"humanist rounded abstract mark, warm approachable professional style, navy with teal accent, subtle community connection pattern, trustworthy modern brand shape, white background","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_04","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_slab_serif",
|
||||
"prompt":_BP+"premium block geometric abstract mark, dark charcoal and deep navy palette, gold accent stripe, authoritative membership management brand shape, institutional trustworthiness, white background","width":1024,"height":512,"steps":30},
|
||||
{"id":"p1_wm_05","phase":"phase1_logos","subfolder":"wordmark","name":"wordmark_minimal",
|
||||
"prompt":_BP+"ultra-minimal thin abstract brand mark shape, single-color deep navy, negative space connected-people form, Apple-inspired premium minimalism, pure white background","width":1024,"height":512,"steps":30},
|
||||
|
||||
# Icon / Symbol Only — 10 variations (512×512)
|
||||
{"id":"p1_ic_01","phase":"phase1_logos","subfolder":"icon_only","name":"icon_people_network",
|
||||
"prompt":_BP+"abstract interconnected people silhouettes forming network club icon, navy on white, community membership platform, geometric precision, square icon format","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_02","phase":"phase1_logos","subfolder":"icon_only","name":"icon_c_mark_abstract",
|
||||
"prompt":_BP+"abstract letter-C shaped from human figures and connecting lines brand icon, geometric minimalist, deep navy gradient, community circle concept inside C curve, white background","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_03","phase":"phase1_logos","subfolder":"icon_only","name":"icon_membership_card",
|
||||
"prompt":_BP+"stylized membership card with embedded circuit-like membership ID pattern, navy and teal nodes and gold lines, professional membership management mark, dark background","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_04","phase":"phase1_logos","subfolder":"icon_only","name":"icon_dashboard_grid",
|
||||
"prompt":_BP+"abstract dashboard grid symbol icon, 3x3 grid of squares with bar chart and people silhouette overlaid, navy and gold, SaaS platform brand mark, white background","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_05","phase":"phase1_logos","subfolder":"icon_only","name":"icon_calendar_pulse",
|
||||
"prompt":_BP+"calendar grid merging with heartbeat pulse line icon, teal and navy, event scheduling and club vitality brand mark, half-event half-data, modern membership platform","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_06","phase":"phase1_logos","subfolder":"icon_only","name":"icon_shield_people",
|
||||
"prompt":_BP+"shield shape with people-group geometric pattern inside, navy shield, gold people outline, trust and membership management brand mark, premium badge style","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_07","phase":"phase1_logos","subfolder":"icon_only","name":"icon_cm_monogram",
|
||||
"prompt":_BP+"interlocked abstract C and M shapes with community negative space monogram, geometric precision, deep navy, gold accent, premium brand monogram, white background","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_08","phase":"phase1_logos","subfolder":"icon_only","name":"icon_hexagon_community",
|
||||
"prompt":_BP+"hexagon containing stylized connected people nodes formed from clean lines, navy hexagon dark outline, gold accent dot nodes at connection points, geometric membership brand mark, tech-forward minimal","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_09","phase":"phase1_logos","subfolder":"icon_only","name":"icon_growth_chart",
|
||||
"prompt":_BP+"upward growing membership curve transforming into ascending bar chart, navy to teal gradient, business growth metaphor, modern flat icon design","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ic_10","phase":"phase1_logos","subfolder":"icon_only","name":"icon_infinity_community",
|
||||
"prompt":_BP+"infinity loop symbol where loops form two abstract community circle shapes, navy line on white, continuous membership management cycle concept, premium SaaS logo mark","width":512,"height":512,"steps":30},
|
||||
|
||||
# Horizontal Lockups (1024×256)
|
||||
{"id":"p1_lh_01","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_light",
|
||||
"prompt":_BP+"horizontal logo lockup icon mark left shape right, light white background, deep navy, professional club management SaaS layout, pure icon shapes only","width":1024,"height":256,"steps":30},
|
||||
{"id":"p1_lh_02","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_dark",
|
||||
"prompt":_BP+"horizontal logo lockup icon left shape right, dark charcoal background, white and navy logo, reversed color scheme, premium brand","width":1024,"height":256,"steps":30},
|
||||
{"id":"p1_lh_03","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_navy_bg",
|
||||
"prompt":_BP+"horizontal logo lockup, white icon on deep navy background, horizontal icon plus abstract shape, brand banner version","width":1024,"height":256,"steps":30},
|
||||
{"id":"p1_lh_04","phase":"phase1_logos","subfolder":"lockup_horizontal","name":"lockup_horiz_mono",
|
||||
"prompt":_BP+"horizontal logo lockup monochrome, all black on white, horizontal icon plus abstract shape, professional print-ready version","width":1024,"height":256,"steps":30},
|
||||
|
||||
# Stacked Lockups (512×512)
|
||||
{"id":"p1_ls_01","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_light",
|
||||
"prompt":_BP+"stacked logo lockup icon centered above abstract shape, light white background, navy brand colors, square format, professional centered layout","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ls_02","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_dark",
|
||||
"prompt":_BP+"stacked logo lockup icon centered above shape, dark charcoal background, white and teal logo, square format, dark version","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ls_03","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_navy",
|
||||
"prompt":_BP+"stacked logo lockup, white icon and shape on navy background, centered square format, brand full-color version","width":512,"height":512,"steps":30},
|
||||
{"id":"p1_ls_04","phase":"phase1_logos","subfolder":"lockup_stacked","name":"lockup_stacked_mono",
|
||||
"prompt":_BP+"stacked logo lockup monochrome all-black on white, icon above abstract mark, square format, print-ready logo","width":512,"height":512,"steps":30},
|
||||
|
||||
# Favicons (256×256)
|
||||
{"id":"p1_fv_01","phase":"phase1_logos","subfolder":"favicon","name":"favicon_navy_people",
|
||||
"prompt":_BP+"favicon 256x256 square app icon, navy background, white geometric people-network icon, rounded square, minimal","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_02","phase":"phase1_logos","subfolder":"favicon","name":"favicon_dark_teal",
|
||||
"prompt":_BP+"favicon dark charcoal square, teal community-nodes icon, 256x256 app icon, sharp corners, professional SaaS favicon","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_03","phase":"phase1_logos","subfolder":"favicon","name":"favicon_white_navy",
|
||||
"prompt":_BP+"favicon white background, deep navy CM abstract monogram icon, 256x256 square, minimal browser favicon","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_04","phase":"phase1_logos","subfolder":"favicon","name":"favicon_gold_dark",
|
||||
"prompt":_BP+"favicon dark background, gold amber membership management icon mark, 256x256 premium app icon, warm gold on charcoal","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_05","phase":"phase1_logos","subfolder":"favicon","name":"favicon_gradient_navy",
|
||||
"prompt":_BP+"favicon midnight to navy gradient background, white geometric icon, 256x256 square, modern SaaS app icon with gradient","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_06","phase":"phase1_logos","subfolder":"favicon","name":"favicon_outline_style",
|
||||
"prompt":_BP+"favicon white background, outline-only teal community network icon, thin line illustration, 256x256, minimalist","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_07","phase":"phase1_logos","subfolder":"favicon","name":"favicon_rounded_modern",
|
||||
"prompt":_BP+"iOS-style rounded square app icon, navy gradient background, white membership brand mark, 256x256, premium mobile app icon","width":256,"height":256,"steps":30},
|
||||
{"id":"p1_fv_08","phase":"phase1_logos","subfolder":"favicon","name":"favicon_badge_style",
|
||||
"prompt":_BP+"badge-style icon with thin border ring, navy center with white CM abstract shape, 256x256 square, club software favicon","width":256,"height":256,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 2 — Banner Suite (50 assets)
|
||||
# ============================================================
|
||||
|
||||
# Hero Website Banners (1280×720)
|
||||
{"id":"p2_hw_01","phase":"phase2_banners","subfolder":"hero_website","name":"hero_dashboard_showcase",
|
||||
"prompt":_BP+"website hero banner, dark charcoal background, teal UI dashboard shapes floating right, bold geometric area left, gold accent lines, enterprise software marketing visual","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_02","phase":"phase2_banners","subfolder":"hero_website","name":"hero_compliance_trust",
|
||||
"prompt":_BP+"website hero banner, trust theme, deep navy gradient, shield and checkmark iconography, membership management, white geometric area, subtle hexagonal pattern overlay","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_03","phase":"phase2_banners","subfolder":"hero_website","name":"hero_analytics_data",
|
||||
"prompt":_BP+"website hero banner, analytics theme, dark background, glowing data visualization charts in navy and gold, business metrics, abstract data flowing design","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_04","phase":"phase2_banners","subfolder":"hero_website","name":"hero_team_enterprise",
|
||||
"prompt":_BP+"website hero banner, enterprise team theme, split design navy left panel white right panel, diagonal split, geometric accents, SaaS marketing visual","width":1280,"height":720,"steps":30},
|
||||
{"id":"p2_hw_05","phase":"phase2_banners","subfolder":"hero_website","name":"hero_community_tech",
|
||||
"prompt":_BP+"website hero banner, community meets technology, abstract people nodes growing from data grid, navy organic forms with teal circuit lines, dark sophisticated background","width":1280,"height":720,"steps":30},
|
||||
|
||||
# LinkedIn Banners (1584×396)
|
||||
{"id":"p2_li_01","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_corporate_navy",
|
||||
"prompt":_BP+"LinkedIn company banner, deep navy background, white abstract icon centered, club business management, clean minimal corporate header visual","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_02","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_dark_gold",
|
||||
"prompt":_BP+"LinkedIn banner, dark charcoal background, gold accent stripe bottom, professional enterprise header visual","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_03","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_pattern_overlay",
|
||||
"prompt":_BP+"LinkedIn banner, navy base, subtle hexagonal membership molecule pattern overlay, semi-transparent, company branding prominent, wide horizontal header","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_04","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_split_design",
|
||||
"prompt":_BP+"LinkedIn banner, split design left dark right navy, diagonal split line, club management platform branding, clean sharp design","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_05","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_metrics_banner",
|
||||
"prompt":_BP+"LinkedIn banner showing abstract business metrics visualization, data-forward icons, navy with gold shapes, analytics platform positioning","width":1584,"height":396,"steps":30},
|
||||
{"id":"p2_li_06","phase":"phase2_banners","subfolder":"linkedin","name":"linkedin_gradient_minimal",
|
||||
"prompt":_BP+"LinkedIn banner, midnight to navy gradient, minimal white brand shape only, ultra-clean professional header","width":1584,"height":396,"steps":30},
|
||||
|
||||
# Twitter/X Headers (1500×500)
|
||||
{"id":"p2_tw_01","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_bold_navy",
|
||||
"prompt":_BP+"Twitter X header banner, bold navy full bleed background, large white brand icon shape, membership management visual, strong social media presence","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_02","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_dark_pattern",
|
||||
"prompt":_BP+"Twitter header, dark charcoal with subtle community geometric pattern, navy and gold accents, professional SaaS brand social header","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_03","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_product_hint",
|
||||
"prompt":_BP+"Twitter header, dark background with abstract dashboard interface shapes, club management software preview silhouette, professional tech company header","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_04","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_community_abstract",
|
||||
"prompt":_BP+"Twitter header, abstract community nodes growing into data streams, navy to dark gradient, artistic organic meets digital aesthetic","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_05","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_network",
|
||||
"prompt":_BP+"Twitter header, membership network theme, connected nodes visualization in teal and navy, SaaS platform connecting businesses","width":1500,"height":500,"steps":30},
|
||||
{"id":"p2_tw_06","phase":"phase2_banners","subfolder":"twitter_x","name":"twitter_white_clean",
|
||||
"prompt":_BP+"Twitter header, clean white background, navy brand elements only, ultra-professional minimal social media header","width":1500,"height":500,"steps":30},
|
||||
|
||||
# Facebook Covers (820×312)
|
||||
{"id":"p2_fb_01","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_primary_brand",
|
||||
"prompt":_BP+"Facebook cover photo, primary brand colors navy and charcoal, professional club business company cover, centered branding icon","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_02","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_dark_professional",
|
||||
"prompt":_BP+"Facebook cover, dark sophisticated background, white and gold brand icon elements, enterprise platform premium cover","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_03","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_membership_theme",
|
||||
"prompt":_BP+"Facebook cover, membership and community theme, shield and verification iconography, navy professional company cover","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_04","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_product_launch",
|
||||
"prompt":_BP+"Facebook cover, product launch announcement style, bold navy with gold accents, exciting software release visual, dynamic tech company cover","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_05","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_industry_leader",
|
||||
"prompt":_BP+"Facebook cover, industry leadership positioning, club management market leader visual, professional authoritative design, navy and gold","width":820,"height":312,"steps":30},
|
||||
{"id":"p2_fb_06","phase":"phase2_banners","subfolder":"facebook_cover","name":"facebook_seasonal_spring",
|
||||
"prompt":_BP+"Facebook cover, spring fresh brand, bright teal with navy organic elements, membership growth season theme, professional seasonal cover","width":820,"height":312,"steps":30},
|
||||
|
||||
# Google Display Ads (16 assets - 4 concepts × 4 sizes)
|
||||
{"id":"p2_ga_01a","phase":"phase2_banners","subfolder":"google_display","name":"gad_simplify_728x90",
|
||||
"prompt":_BP+"Google display ad leaderboard, simplify club management theme, navy button shape, white background, professional B2B ad visual","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_01b","phase":"phase2_banners","subfolder":"google_display","name":"gad_simplify_300x250",
|
||||
"prompt":_BP+"Google display ad medium rectangle, simplify club management theme, navy design, bold icon shapes, professional SaaS ad creative","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_01c","phase":"phase2_banners","subfolder":"google_display","name":"gad_simplify_160x600",
|
||||
"prompt":_BP+"Google display ad wide skyscraper, simplify club management theme, tall vertical format, navy, professional B2B ad","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_01d","phase":"phase2_banners","subfolder":"google_display","name":"gad_simplify_320x50",
|
||||
"prompt":_BP+"Google display ad mobile banner, simplify management theme, minimal mobile ad, navy, club management SaaS","width":320,"height":50,"steps":30},
|
||||
{"id":"p2_ga_02a","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_728x90",
|
||||
"prompt":_BP+"Google display ad, manage everything club business theme, dashboard preview hint icons, dark charcoal professional leaderboard banner","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_02b","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_300x250",
|
||||
"prompt":_BP+"Google display ad, manage everything club operations theme, product dashboard icon glimpse, navy dark professional rectangle ad","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_02c","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_160x600",
|
||||
"prompt":_BP+"Google display skyscraper, manage club business operations theme, vertical product feature icon list visual, navy professional tall ad","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_02d","phase":"phase2_banners","subfolder":"google_display","name":"gad_manage_320x50",
|
||||
"prompt":_BP+"mobile banner, manage club business theme, ultra-minimal mobile ad strip, brand colors","width":320,"height":50,"steps":30},
|
||||
{"id":"p2_ga_03a","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_728x90",
|
||||
"prompt":_BP+"Google ad, grow your club business theme, upward growth arrow with member silhouette, gold and navy, professional B2B leaderboard","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_03b","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_300x250",
|
||||
"prompt":_BP+"Google ad, club business growth theme, ascending graph with navy growth visual, professional SaaS rectangle ad","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_03c","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_160x600",
|
||||
"prompt":_BP+"skyscraper ad, club business growth vertical story, people nodes growing upward through data visualization, navy tall display ad","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_03d","phase":"phase2_banners","subfolder":"google_display","name":"gad_grow_320x50",
|
||||
"prompt":_BP+"mobile ad, grow club membership, minimal mobile strip ad navy","width":320,"height":50,"steps":30},
|
||||
{"id":"p2_ga_04a","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_728x90",
|
||||
"prompt":_BP+"Google ad, free trial call to action shape, bold gold CTA shape, navy professional leaderboard, club management SaaS trial offer visual","width":728,"height":90,"steps":30},
|
||||
{"id":"p2_ga_04b","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_300x250",
|
||||
"prompt":_BP+"Google ad, free trial offer icon, gold button shape navy design, club management platform trial CTA rectangle ad","width":300,"height":250,"steps":30},
|
||||
{"id":"p2_ga_04c","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_160x600",
|
||||
"prompt":_BP+"skyscraper ad, free trial CTA vertical ad, gold call to action arrow shape, navy SaaS platform","width":160,"height":600,"steps":30},
|
||||
{"id":"p2_ga_04d","phase":"phase2_banners","subfolder":"google_display","name":"gad_trial_320x50",
|
||||
"prompt":_BP+"mobile ad, free trial minimal mobile strip, gold CTA shape navy brand","width":320,"height":50,"steps":30},
|
||||
|
||||
# App Store Feature Graphics (1024×500)
|
||||
{"id":"p2_as_01","phase":"phase2_banners","subfolder":"app_store","name":"appstore_hero_dashboard",
|
||||
"prompt":_BP+"app store feature graphic, club management app showcase, dark background with app dashboard UI icon preview shapes, navy interface elements, professional mobile app store hero","width":1024,"height":500,"steps":30},
|
||||
{"id":"p2_as_02","phase":"phase2_banners","subfolder":"app_store","name":"appstore_membership_features",
|
||||
"prompt":_BP+"app store feature graphic, membership features highlight, shield icons and checkmarks, navy professional, club membership app feature graphic","width":1024,"height":500,"steps":30},
|
||||
{"id":"p2_as_03","phase":"phase2_banners","subfolder":"app_store","name":"appstore_analytics_focus",
|
||||
"prompt":_BP+"app store feature graphic, business analytics and reporting feature, dashboard chart shapes preview, gold and navy data visualization, club business intelligence app","width":1024,"height":500,"steps":30},
|
||||
{"id":"p2_as_04","phase":"phase2_banners","subfolder":"app_store","name":"appstore_team_management",
|
||||
"prompt":_BP+"app store feature graphic, team and staff management, connected team nodes visualization, navy professional, club staff team management app","width":1024,"height":500,"steps":30},
|
||||
|
||||
# Email Header Banners (600×200)
|
||||
{"id":"p2_em_01","phase":"phase2_banners","subfolder":"email_header","name":"email_primary_brand",
|
||||
"prompt":_BP+"email header banner, primary brand header, navy with white icon area, professional email marketing header","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_02","phase":"phase2_banners","subfolder":"email_header","name":"email_welcome",
|
||||
"prompt":_BP+"welcome email header, warm welcome theme, navy and teal gradient, onboarding email banner, new user email header","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_03","phase":"phase2_banners","subfolder":"email_header","name":"email_product_update",
|
||||
"prompt":_BP+"product update email header, new features announcement, gold accent notification style, software update email banner","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_04","phase":"phase2_banners","subfolder":"email_header","name":"email_membership_alert",
|
||||
"prompt":_BP+"membership alert email header, urgent notification theme, amber gold accent on dark, regulatory update email header, professional alert banner","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_05","phase":"phase2_banners","subfolder":"email_header","name":"email_monthly_report",
|
||||
"prompt":_BP+"monthly report email header, data and analytics icon theme, chart shapes preview, navy professional, club business monthly summary","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_06","phase":"phase2_banners","subfolder":"email_header","name":"email_trial_ending",
|
||||
"prompt":_BP+"trial ending email header, urgency CTA icon theme, gold highlight on dark, platform trial expiry email banner, convert to paid","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_07","phase":"phase2_banners","subfolder":"email_header","name":"email_invoice",
|
||||
"prompt":_BP+"invoice and billing email header, clean minimal professional, white and navy, SaaS billing email header, enterprise professional","width":600,"height":200,"steps":30},
|
||||
{"id":"p2_em_08","phase":"phase2_banners","subfolder":"email_header","name":"email_dark_premium",
|
||||
"prompt":_BP+"dark premium email header, dark charcoal with gold and navy accents, VIP or enterprise tier email, club management premium header","width":600,"height":200,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 3 — Social Media Asset Pack (60 assets)
|
||||
# ============================================================
|
||||
|
||||
# Instagram Square Posts (1024×1024)
|
||||
{"id":"p3_ig_01","phase":"phase3_social","subfolder":"instagram_square","name":"insta_inventory_mgmt",
|
||||
"prompt":_BP+"Instagram post, inventory management feature highlight, stock tracking dashboard icon visualization, navy UI elements, bold feature announcement visual, clean white and dark design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_02","phase":"phase3_social","subfolder":"instagram_square","name":"insta_member_track",
|
||||
"prompt":_BP+"Instagram post, member tracking feature, checklist visualization, shield and checkmark icons, navy and gold, club management SaaS post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_03","phase":"phase3_social","subfolder":"instagram_square","name":"insta_analytics_dash",
|
||||
"prompt":_BP+"Instagram post, analytics dashboard feature, business intelligence data visualization icon, navy charts on dark background, club business analytics","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_04","phase":"phase3_social","subfolder":"instagram_square","name":"insta_staff_scheduling",
|
||||
"prompt":_BP+"Instagram post, staff scheduling feature, team calendar and shift management visualization, club staff management, clean navy post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_05","phase":"phase3_social","subfolder":"instagram_square","name":"insta_pos_integration",
|
||||
"prompt":_BP+"Instagram post, POS system integration feature, point-of-sale connection visualization, integration nodes and arrows, navy and gold tech post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_06","phase":"phase3_social","subfolder":"instagram_square","name":"insta_reporting",
|
||||
"prompt":_BP+"Instagram post, automated reporting feature, beautiful report document preview shape, navy professional document visualization post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_07","phase":"phase3_social","subfolder":"instagram_square","name":"insta_multi_location",
|
||||
"prompt":_BP+"Instagram post, multi-location management feature, club chain management, location pins on map with connecting navy lines","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_08","phase":"phase3_social","subfolder":"instagram_square","name":"insta_mobile_app",
|
||||
"prompt":_BP+"Instagram post, mobile app feature highlight, iPhone and Android app shape mockup, club management on-the-go, navy app UI post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_09","phase":"phase3_social","subfolder":"instagram_square","name":"insta_security",
|
||||
"prompt":_BP+"Instagram post, enterprise security feature, data protection and encryption visualization, shield with lock icon, dark professional navy security post","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_ig_10","phase":"phase3_social","subfolder":"instagram_square","name":"insta_brand_story",
|
||||
"prompt":_BP+"Instagram brand story post, company mission, club empowerment, beautiful abstract people and technology fusion illustration, navy and gold, inspiring brand post","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Instagram Stories (720×1280)
|
||||
{"id":"p3_st_01","phase":"phase3_social","subfolder":"instagram_story","name":"story_onboarding",
|
||||
"prompt":_BP+"Instagram story, onboarding tutorial slide, step-by-step platform setup icon, navy vertical mobile design, swipe up arrow CTA shape, professional SaaS story","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_02","phase":"phase3_social","subfolder":"instagram_story","name":"story_feature_announce",
|
||||
"prompt":_BP+"Instagram story, new feature announcement, bold navy vertical design, software update story, gold accent highlight, arrow CTA shape","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_03","phase":"phase3_social","subfolder":"instagram_story","name":"story_stat_highlight",
|
||||
"prompt":_BP+"Instagram story, industry statistic highlight visual, large bold number shape, dark background gold number navy accent, data-driven story template","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_04","phase":"phase3_social","subfolder":"instagram_story","name":"story_customer_quote",
|
||||
"prompt":_BP+"Instagram story, customer testimonial visual, elegant quote marks shape on navy background, club business owner testimonial style, premium brand story","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_05","phase":"phase3_social","subfolder":"instagram_story","name":"story_poll_template",
|
||||
"prompt":_BP+"Instagram story, interactive poll template visual, club industry question bar icons, dark professional background, poll shapes styled in navy and gold","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_06","phase":"phase3_social","subfolder":"instagram_story","name":"story_countdown",
|
||||
"prompt":_BP+"Instagram story, countdown timer event template, launch deadline visual, bold dramatic dark background with gold countdown shape","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_07","phase":"phase3_social","subfolder":"instagram_story","name":"story_tips_series",
|
||||
"prompt":_BP+"Instagram story, club management tip of the day template, bright educational story icon, numbered tip format, teal navy professional guidance visual","width":720,"height":1280,"steps":30},
|
||||
{"id":"p3_st_08","phase":"phase3_social","subfolder":"instagram_story","name":"story_free_trial",
|
||||
"prompt":_BP+"Instagram story, free trial CTA story, bold gold arrow shape on dark navy, club management platform sign up visual, strong conversion design","width":720,"height":1280,"steps":30},
|
||||
|
||||
# LinkedIn Post Graphics (1200×627)
|
||||
{"id":"p3_lp_01","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_thought_leader",
|
||||
"prompt":_BP+"LinkedIn post graphic, thought leadership article header visual, club industry insights icon, professional editorial design, navy brand with white content area","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_02","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_product_demo",
|
||||
"prompt":_BP+"LinkedIn post, product demo announcement visual, screenshot preview icon teaser, club management platform demo invitation shape, navy professional post","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_03","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_hiring",
|
||||
"prompt":_BP+"LinkedIn hiring post, we are hiring banner shapes, team growth announcement visual, professional club tech company hiring graphic, navy and gold, people silhouettes","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_04","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_industry_stat",
|
||||
"prompt":_BP+"LinkedIn post, club industry statistic infographic shapes, large bold number visual, professional B2B data post, navy","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_05","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_partnership",
|
||||
"prompt":_BP+"LinkedIn partnership announcement, strategic partnership visual, club tech ecosystem, professional announcement graphic navy","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_06","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_webinar",
|
||||
"prompt":_BP+"LinkedIn webinar promotion post, club compliance webinar announcement visual, calendar icon event, professional event promotion, navy dark design","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_07","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_case_study",
|
||||
"prompt":_BP+"LinkedIn case study post, customer success story preview visual, club success metrics chart shapes, gold numbers on dark, professional B2B case study promotional","width":1200,"height":627,"steps":30},
|
||||
{"id":"p3_lp_08","phase":"phase3_social","subfolder":"linkedin_post","name":"linkedin_award",
|
||||
"prompt":_BP+"LinkedIn award announcement post, award or recognition visual, gold trophy badge element shape, club tech industry recognition, celebratory professional post","width":1200,"height":627,"steps":30},
|
||||
|
||||
# Feature Announcement Cards (1024×1024)
|
||||
{"id":"p3_fc_01","phase":"phase3_social","subfolder":"feature_cards","name":"feature_barcode_scan",
|
||||
"prompt":_BP+"feature card, barcode scanning inventory management, membership card scan interface icon, mobile scanning visualization, navy UI feature card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_02","phase":"phase3_social","subfolder":"feature_cards","name":"feature_auto_reports",
|
||||
"prompt":_BP+"feature card, automated reporting, automation icon with checklist shapes, reporting automation, navy professional feature announcement","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_03","phase":"phase3_social","subfolder":"feature_cards","name":"feature_real_time_alerts",
|
||||
"prompt":_BP+"feature card, real-time alerts and notifications, bell notification icon with threshold alert shapes, gold alert accent on dark, notification feature","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_04","phase":"phase3_social","subfolder":"feature_cards","name":"feature_member_portal",
|
||||
"prompt":_BP+"feature card, member self-service portal icon, club member login interface shape, clean navy member management, user portal visualization","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_05","phase":"phase3_social","subfolder":"feature_cards","name":"feature_api_integrations",
|
||||
"prompt":_BP+"feature card, API integrations ecosystem icon, connected software nodes with hub, tech stack integration visualization, navy connection diagram","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_06","phase":"phase3_social","subfolder":"feature_cards","name":"feature_batch_tracking",
|
||||
"prompt":_BP+"feature card, batch and lot tracking icon, product chain of custody visualization, numbered batch tracking flow shapes, compliance feature card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_07","phase":"phase3_social","subfolder":"feature_cards","name":"feature_document_mgmt",
|
||||
"prompt":_BP+"feature card, document management system icon, licensing and permit documents organized, folder icons in navy, digital document management","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_08","phase":"phase3_social","subfolder":"feature_cards","name":"feature_role_permissions",
|
||||
"prompt":_BP+"feature card, role-based permissions feature icon, user role hierarchy visualization, shield with user silhouettes, team access control, dark professional","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_09","phase":"phase3_social","subfolder":"feature_cards","name":"feature_export_reports",
|
||||
"prompt":_BP+"feature card, one-click export and reporting icon, PDF report generation shape, download arrow with report preview, navy feature card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_fc_10","phase":"phase3_social","subfolder":"feature_cards","name":"feature_audit_trail",
|
||||
"prompt":_BP+"feature card, complete audit trail icon, transaction history timeline, chronological log entry shapes, audit visualization, professional dark card","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Testimonial Cards (1024×1024)
|
||||
{"id":"p3_tc_01","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_dark_elegant",
|
||||
"prompt":_BP+"testimonial card, dark charcoal elegant quote card, gold quotation mark shapes, abstract customer silhouette, club business owner testimonial visual, premium design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_02","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_navy_bold",
|
||||
"prompt":_BP+"testimonial card, bold navy background, white quote mark shapes, customer review visual of club management SaaS, bold confident design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_03","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_light_minimal",
|
||||
"prompt":_BP+"testimonial card, light white minimal quote card, navy accent line, clean professional customer testimonial visual, minimal elegant design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_04","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_star_rating",
|
||||
"prompt":_BP+"testimonial card, 5-star rating visual, gold stars prominently displayed, abstract club management platform review","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_05","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_split_design",
|
||||
"prompt":_BP+"testimonial card, split design half dark half navy, quote on dark side, customer info shape on navy side, club SaaS testimonial","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_tc_06","phase":"phase3_social","subfolder":"testimonial_cards","name":"testimonial_gradient",
|
||||
"prompt":_BP+"testimonial card, midnight to navy gradient background, white elegant quote mark shapes, customer testimonial gradient design","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Stat/Data Cards (1024×1024)
|
||||
{"id":"p3_sc_01","phase":"phase3_social","subfolder":"stat_cards","name":"stat_market_size",
|
||||
"prompt":_BP+"stat card, club management market size statistic, large bold dollar amount shape, navy gold number on dark background, industry market data visual","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_02","phase":"phase3_social","subfolder":"stat_cards","name":"stat_cost_reduction",
|
||||
"prompt":_BP+"stat card, cost reduction statistic visual, percentage savings with management software icon, gold percentage shape, club business cost savings","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_03","phase":"phase3_social","subfolder":"stat_cards","name":"stat_time_savings",
|
||||
"prompt":_BP+"stat card, hours saved per week statistic, clock icon with bold number shape, club operational time savings, navy professional","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_04","phase":"phase3_social","subfolder":"stat_cards","name":"stat_membership_growth",
|
||||
"prompt":_BP+"stat card, membership industry growth rate, upward arrow with percentage growth shape, membership retail market growth stat, gold growth number on dark","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_05","phase":"phase3_social","subfolder":"stat_cards","name":"stat_compliance_fines",
|
||||
"prompt":_BP+"stat card, compliance violation fine amounts, regulatory penalty warning stat shape, amber warning colors, avoid fines messaging icon","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_06","phase":"phase3_social","subfolder":"stat_cards","name":"stat_customer_count",
|
||||
"prompt":_BP+"stat card, number of clubs managed, large customer count statistic shape, navy social proof data card, platform traction metric","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_07","phase":"phase3_social","subfolder":"stat_cards","name":"stat_roi_metric",
|
||||
"prompt":_BP+"stat card, ROI return on investment metric for club management software, large gold ROI percentage shape, business value data card","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p3_sc_08","phase":"phase3_social","subfolder":"stat_cards","name":"stat_global_clubs",
|
||||
"prompt":_BP+"stat card, number of registered clubs worldwide, globe icon with country count shape, club management global market stat, navy global market visual","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 4 — UI & Product Assets (32 assets)
|
||||
# ============================================================
|
||||
|
||||
# App Icons (1024×1024)
|
||||
{"id":"p4_ai_01","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_ios_navy",
|
||||
"prompt":_BP+"iOS app icon, Apple iOS style rounded square, navy gradient background, white membership network icon mark, premium mobile app icon, App Store quality","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_02","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_ios_dark",
|
||||
"prompt":_BP+"iOS app icon, dark mode iOS icon, dark charcoal with navy and gold brand mark, premium dark app icon, club management mobile app","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_03","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_ios_gold",
|
||||
"prompt":_BP+"iOS app icon, premium gold accent, deep navy background with gold membership circuit brand mark, luxury club management app icon","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_04","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_android_material",
|
||||
"prompt":_BP+"Android app icon, Material Design 3 style adaptive icon, navy with white icon shape, Google Play Store quality, club management Android app","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_05","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_android_dark",
|
||||
"prompt":_BP+"Android dark mode app icon, dark adaptive icon, navy outline on near-black, Material You dark theme, club management app","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_06","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_gradient_modern",
|
||||
"prompt":_BP+"app icon, modern gradient icon, midnight to navy gradient background, white geometric membership tech mark, contemporary design","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_07","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_flat_clean",
|
||||
"prompt":_BP+"app icon, flat design icon, solid navy no gradient, white minimal icon mark, flat design philosophy, simple clean club management","width":1024,"height":1024,"steps":30},
|
||||
{"id":"p4_ai_08","phase":"phase4_ui","subfolder":"app_icons","name":"appicon_neumorphic",
|
||||
"prompt":_BP+"app icon, neumorphic soft UI style, light slate background with embossed membership network icon, subtle shadows, premium modern icon design","width":1024,"height":1024,"steps":30},
|
||||
|
||||
# Device Mockups
|
||||
{"id":"p4_dm_01","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_macbook",
|
||||
"prompt":_BP+"dashboard shown on MacBook Pro shape mockup, professional product marketing, club management SaaS on Apple laptop silhouette, navy UI on screen shape, clean white studio background","width":1024,"height":640,"steps":30},
|
||||
{"id":"p4_dm_02","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_ipad",
|
||||
"prompt":_BP+"dashboard on iPad Pro shape mockup, club management tablet interface icon, navy UI on Apple iPad silhouette, clean marketing product shot, white background","width":1024,"height":768,"steps":30},
|
||||
{"id":"p4_dm_03","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_iphone",
|
||||
"prompt":_BP+"mobile app on iPhone shape mockup, club management mobile interface icon, navy green mobile UI, clean product marketing shot, white background","width":390,"height":844,"steps":30},
|
||||
{"id":"p4_dm_04","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_desktop_monitor",
|
||||
"prompt":_BP+"dashboard on large desktop monitor shape mockup, club management enterprise software on wide screen silhouette, dark UI visible, professional product marketing display","width":1280,"height":720,"steps":30},
|
||||
{"id":"p4_dm_05","phase":"phase4_ui","subfolder":"device_mockups","name":"mockup_android_phone",
|
||||
"prompt":_BP+"mobile app on Android phone shape mockup, club management Android interface icon, Material Design navy UI, product marketing shot white background","width":390,"height":844,"steps":30},
|
||||
|
||||
# Onboarding Illustrations (800×600)
|
||||
{"id":"p4_ob_01","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_inventory",
|
||||
"prompt":_BP+"onboarding illustration, inventory management scene icon, product shelves with digital inventory overlay shapes, flat illustration style, navy and teal","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_02","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_compliance",
|
||||
"prompt":_BP+"onboarding illustration, compliance tracking scene icon, person silhouette reviewing regulatory documents with digital checklist shape, confident professional flat illustration","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_03","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_analytics",
|
||||
"prompt":_BP+"onboarding illustration, analytics and reporting scene icon, business person silhouette analyzing sales charts, dashboard visualization, navy data visualization flat","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_04","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_staff",
|
||||
"prompt":_BP+"onboarding illustration, staff scheduling scene icon, team member silhouettes with shift calendar shape, club team management, professional flat art navy","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_05","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_pos",
|
||||
"prompt":_BP+"onboarding illustration, POS integration scene icon, point of sale system connected to management platform shapes, tech integration flat illustration, navy","width":800,"height":600,"steps":30},
|
||||
{"id":"p4_ob_06","phase":"phase4_ui","subfolder":"onboarding_illustrations","name":"onboard_reporting",
|
||||
"prompt":_BP+"onboarding illustration, automated reporting scene icon, report documents generating automatically shapes, magic automation illustration, navy gold professional flat art","width":800,"height":600,"steps":30},
|
||||
|
||||
# Empty States (600×400)
|
||||
{"id":"p4_es_01","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_data",
|
||||
"prompt":_BP+"empty state illustration, no data yet icon, friendly empty chart shape, get started visual, navy minimal SaaS illustration","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_02","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_results",
|
||||
"prompt":_BP+"empty state illustration, no search results found icon, magnifying glass with empty bubble, friendly empty state, navy minimal","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_03","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_alerts",
|
||||
"prompt":_BP+"empty state illustration, no alerts icon, happy shield with checkmark, all clear illustration, club compliance all good state, navy positive","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_04","phase":"phase4_ui","subfolder":"empty_states","name":"empty_no_members",
|
||||
"prompt":_BP+"empty state illustration, no members added yet icon, friendly people silhouettes with plus icon, club member management, navy add members","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_05","phase":"phase4_ui","subfolder":"empty_states","name":"empty_loading_data",
|
||||
"prompt":_BP+"empty state illustration, loading and processing data icon, gentle spinner shapes, patient loading state, navy minimal illustration","width":600,"height":400,"steps":30},
|
||||
{"id":"p4_es_06","phase":"phase4_ui","subfolder":"empty_states","name":"empty_offline",
|
||||
"prompt":_BP+"empty state illustration, offline or connection error icon, disconnected wifi shape, friendly error state, amber warning on navy","width":600,"height":400,"steps":30},
|
||||
|
||||
# Splash/Loading Screens (720×1280)
|
||||
{"id":"p4_sp_01","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_primary",
|
||||
"prompt":_BP+"splash screen, app loading screen, dark charcoal background, large centered brand icon mark shape, subtle navy glow effect, premium app loading screen","width":720,"height":1280,"steps":30},
|
||||
{"id":"p4_sp_02","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_navy",
|
||||
"prompt":_BP+"splash screen, navy background, white icon centered, minimal loading indicator shape, club management app splash, clean brand loading screen","width":720,"height":1280,"steps":30},
|
||||
{"id":"p4_sp_03","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_animated_hint",
|
||||
"prompt":_BP+"splash screen, animated concept, community node particles converging into icon mark, dark background with teal particles, dynamic loading screen first frame","width":720,"height":1280,"steps":30},
|
||||
{"id":"p4_sp_04","phase":"phase4_ui","subfolder":"splash_screens","name":"splash_gradient",
|
||||
"prompt":_BP+"splash screen, dramatic dark to navy gradient background, white brand mark shape, premium loading experience, club SaaS gradient splash","width":720,"height":1280,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 5 — Brand Collateral (38 assets)
|
||||
# ============================================================
|
||||
|
||||
# Business Cards (900×504)
|
||||
{"id":"p5_bc_01f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_modern_front",
|
||||
"prompt":_BP+"business card front, modern minimal style, navy left accent panel, white main area, name placeholder area, club management SaaS company card, premium print","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_01b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_modern_back",
|
||||
"prompt":_BP+"business card back, modern minimal style, full navy back with white icon centered, club management SaaS card back","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_02f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_dark_front",
|
||||
"prompt":_BP+"business card front, dark luxury style, dark charcoal background, gold foil accent icon, premium club management company card, executive tier","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_02b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_dark_back",
|
||||
"prompt":_BP+"business card back, dark luxury style, full dark charcoal, gold icon and navy accent, premium back of card","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_03f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_pattern_front",
|
||||
"prompt":_BP+"business card front, geometric community pattern accent, white card with subtle hexagonal pattern header, professional pattern card front","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_03b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_pattern_back",
|
||||
"prompt":_BP+"business card back, community geometric pattern full bleed, navy hexagonal pattern background, white icon, pattern card back","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_04f","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_bold_front",
|
||||
"prompt":_BP+"business card front, bold icon style, large navy brand shape, clean white card, icon-forward business card","width":900,"height":504,"steps":30},
|
||||
{"id":"p5_bc_04b","phase":"phase5_collateral","subfolder":"business_cards","name":"bizcard_bold_back",
|
||||
"prompt":_BP+"business card back, bold style, split navy and white back, contact details area shape, club company card back bold design","width":900,"height":504,"steps":30},
|
||||
|
||||
# Pitch Deck Covers (1280×720)
|
||||
{"id":"p5_pd_01","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_investor_dark",
|
||||
"prompt":_BP+"pitch deck cover slide, investor presentation, dark sophisticated background, large icon centered, funding round visual area, premium club SaaS investor deck","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_02","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_growth_story",
|
||||
"prompt":_BP+"pitch deck cover, growth story visual, ascending membership curve becoming data chart, navy to gold gradient, investor-grade presentation cover","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_03","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_market_opp",
|
||||
"prompt":_BP+"pitch deck cover, market opportunity theme, club industry size visualization, globe with highlighted market regions, navy professional investor presentation","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_04","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_team_deck",
|
||||
"prompt":_BP+"pitch deck cover, team presentation version, professional team silhouette backdrop, club tech startup team slide, navy brand, people silhouette-forward investor deck","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_05","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_product_demo",
|
||||
"prompt":_BP+"pitch deck cover, product demo deck, dashboard preview hero visual shapes, club management SaaS product tour deck, navy UI preview","width":1280,"height":720,"steps":30},
|
||||
{"id":"p5_pd_06","phase":"phase5_collateral","subfolder":"pitch_deck","name":"pitch_minimal_clean",
|
||||
"prompt":_BP+"pitch deck cover, ultra-minimal clean slide, white background, large navy brand icon only, minimalist investor presentation","width":1280,"height":720,"steps":30},
|
||||
|
||||
# One-Pager Headers (1200×400)
|
||||
{"id":"p5_op_01","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_primary",
|
||||
"prompt":_BP+"one-pager header, primary brand header, navy full bleed, white icon and visual, club management SaaS brochure header, print quality","width":1200,"height":400,"steps":30},
|
||||
{"id":"p5_op_02","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_feature_rich",
|
||||
"prompt":_BP+"one-pager header, feature-rich header, dashboard preview icon glimpse shapes, club management platform features introduction, professional SaaS marketing","width":1200,"height":400,"steps":30},
|
||||
{"id":"p5_op_03","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_compliance",
|
||||
"prompt":_BP+"one-pager header, compliance focus version, legal and regulatory icon theme, club compliance management, shield icons, dark professional header","width":1200,"height":400,"steps":30},
|
||||
{"id":"p5_op_04","phase":"phase5_collateral","subfolder":"one_pager","name":"onepager_dark_premium",
|
||||
"prompt":_BP+"one-pager header, dark premium version, charcoal background with gold and navy accents, enterprise tier marketing collateral header","width":1200,"height":400,"steps":30},
|
||||
|
||||
# Trade Show Banners (512×1280)
|
||||
{"id":"p5_ts_01","phase":"phase5_collateral","subfolder":"trade_show","name":"tradeshow_primary_brand",
|
||||
"prompt":_BP+"trade show pull-up banner tall vertical, primary brand version, navy top with icon, white middle with key feature icons listed, dark bottom with CTA arrow, club management SaaS conference banner","width":512,"height":1280,"steps":30},
|
||||
{"id":"p5_ts_02","phase":"phase5_collateral","subfolder":"trade_show","name":"tradeshow_product_showcase",
|
||||
"prompt":_BP+"trade show banner tall vertical, product showcase, dashboard UI hero visual shapes, dark sophisticated background, navy accents, club management software exhibition display","width":512,"height":1280,"steps":30},
|
||||
{"id":"p5_ts_03","phase":"phase5_collateral","subfolder":"trade_show","name":"tradeshow_membership_focus",
|
||||
"prompt":_BP+"trade show banner tall vertical, membership authority positioning, club management expertise icons, professional membership focus, navy and gold tall exhibition banner","width":512,"height":1280,"steps":30},
|
||||
|
||||
# Sticker/Swag Designs (600×600)
|
||||
{"id":"p5_sk_01","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_people_circuit",
|
||||
"prompt":_BP+"sticker design, people silhouettes made of circuit traces, navy on white, die-cut sticker style, fun tech community brand sticker","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_02","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_icon_badge",
|
||||
"prompt":_BP+"sticker badge, rounded rectangle badge, navy background, white brand icon shape, premium brand sticker, laptop sticker style","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_03","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_membership_hero",
|
||||
"prompt":_BP+"sticker, membership superhero icon, shield with people and checkmark, fun illustrated sticker, navy and gold, die-cut design","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_04","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_data_community",
|
||||
"prompt":_BP+"fun sticker, community nodes growing into data chart bars, punchy colorful sticker art, navy people gold bars, square sticker","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_05","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_club_astronaut",
|
||||
"prompt":_BP+"sticker, cartoon astronaut holding membership card and laptop, space tech meets club management, fun illustrated sticker, navy spacesuit, brand mascot concept","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_06","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_powered_by",
|
||||
"prompt":_BP+"powered by sticker, powered by platform badge icon, small horizontal badge sticker, navy and white, partner sticker for clubs","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_07","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_compliance_100",
|
||||
"prompt":_BP+"sticker, 100 percent compliant badge icon, bold navy circle with checkmark and percentage shape, compliance achievement sticker, club compliance badge","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_08","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_hexagon_icon",
|
||||
"prompt":_BP+"hexagon sticker, hexagonal border with community network icon, honeycomb management brand sticker, navy hex border gold icon, premium die-cut","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_09","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_members_first",
|
||||
"prompt":_BP+"sticker, members first badge design, playful club membership spirit icon, professional but fun brand sticker, navy with gold shapes","width":600,"height":600,"steps":30},
|
||||
{"id":"p5_sk_10","phase":"phase5_collateral","subfolder":"stickers","name":"sticker_manage_everything",
|
||||
"prompt":_BP+"sticker, manage everything icon, bold icon sticker, navy background white icon shape, punchy brand statement sticker","width":600,"height":600,"steps":30},
|
||||
|
||||
# Email Signature Blocks (600×150)
|
||||
{"id":"p5_es_01","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_primary",
|
||||
"prompt":_BP+"email signature graphic block, primary brand, horizontal icon left with visual divider, navy line, professional email signature banner","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_02","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_dark",
|
||||
"prompt":_BP+"email signature block, dark version, charcoal background white icon navy accent, premium email signature graphic","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_03","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_minimal",
|
||||
"prompt":_BP+"email signature graphic, minimal version, just icon and visual, very clean white background, ultra-minimal email signature banner","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_04","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_promo",
|
||||
"prompt":_BP+"email signature promo block, promotional version with CTA arrow shape, gold button area, white background navy brand, conversion CTA signature","width":600,"height":150,"steps":30},
|
||||
{"id":"p5_es_05","phase":"phase5_collateral","subfolder":"email_signatures","name":"sigblock_social",
|
||||
"prompt":_BP+"email signature block, social media icons version, small social platform icon shapes in navy, company signature with social links footer","width":600,"height":150,"steps":30},
|
||||
|
||||
# ============================================================
|
||||
# PHASE 6 — Bonus / Wildcard Assets (35 assets)
|
||||
# ============================================================
|
||||
|
||||
# Animated Banner First-Frame Stills (1200×628)
|
||||
{"id":"p6_an_01","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_particle_logo",
|
||||
"prompt":_BP+"animated banner first frame, community node particles forming icon mark mid-flight, dark background teal particles, designed for animation, static concept frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_02","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_data_flow",
|
||||
"prompt":_BP+"animated banner first frame, data flow visualization beginning, club data streams starting to form dashboard shapes, dark background navy data lines, animation concept first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_03","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_growth_chart",
|
||||
"prompt":_BP+"animated banner still, club business growth chart animation first frame, bar chart at zero about to animate upward, gold bars on dark, growth animation concept","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_04","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_checklist_check",
|
||||
"prompt":_BP+"animated banner first frame, membership checklist items unchecked ready to animate with checkmarks, navy checklist on white, compliance animation concept","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_05","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_community_grow",
|
||||
"prompt":_BP+"animated banner still, community node seedling about to grow into data visualization network, dark background seed sprouting, growth animation first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_06","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_typing_headline",
|
||||
"prompt":_BP+"animated banner still, typing cursor before headline space, empty headline with blinking cursor concept shape, navy CTA button below, typewriter animation first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_07","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_integration_connect",
|
||||
"prompt":_BP+"animated banner still, integration ecosystem nodes about to connect, partner icon nodes as unconnected shapes, teal connecting lines forming, integration animation first frame","width":1200,"height":628,"steps":30},
|
||||
{"id":"p6_an_08","phase":"phase6_bonus","subfolder":"animated_stills","name":"anim_counter_stat",
|
||||
"prompt":_BP+"animated banner still, number counter animation first frame showing zero shape, gold large number shape about to count up, dark background, counter animation concept","width":1200,"height":628,"steps":30},
|
||||
|
||||
# Dark Mode vs Light Mode UI Pairs (1200×800 each = 6 pairs)
|
||||
{"id":"p6_dm_01l","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_dashboard_light",
|
||||
"prompt":_BP+"UI preview card, light mode dashboard interface, clean white background, navy UI element shapes, club management SaaS light theme preview","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_01d","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_dashboard_dark",
|
||||
"prompt":_BP+"UI preview card, dark mode dashboard interface, dark charcoal background, teal glowing UI element shapes, club management SaaS dark theme preview, premium dark mode","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_02l","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_membership_light",
|
||||
"prompt":_BP+"UI preview, membership module light mode, white clean interface with checklist and status indicator shapes, navy checkmarks, club membership UI","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_02d","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_membership_dark",
|
||||
"prompt":_BP+"UI preview, membership module dark mode, dark interface with glowing navy membership indicator shapes, club membership UI dark theme, premium","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_03l","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_analytics_light",
|
||||
"prompt":_BP+"UI preview, analytics dashboard light mode, white background with colorful club business chart shapes, navy and gold data visualization, light theme","width":1200,"height":800,"steps":40},
|
||||
{"id":"p6_dm_03d","phase":"phase6_bonus","subfolder":"dark_light_pairs","name":"uipair_analytics_dark",
|
||||
"prompt":_BP+"UI preview, analytics dashboard dark mode, dark background with glowing navy and gold club business chart shapes, dramatic dark mode analytics UI","width":1200,"height":800,"steps":40},
|
||||
|
||||
# Integration/Partnership Badges (400×200)
|
||||
{"id":"p6_ib_01","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_pos_integration",
|
||||
"prompt":_BP+"integration badge, integrates with POS systems badge icon, point-of-sale integration certification, navy badge design, professional","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_02","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_accounting",
|
||||
"prompt":_BP+"integration badge, accounting software integration badge icon, financial software connection shape, navy badge with accounting icon","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_03","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_compliance_tools",
|
||||
"prompt":_BP+"integration badge, compliance tool integration badge icon, regulatory software connection shape, navy shield badge design","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_04","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_api_ready",
|
||||
"prompt":_BP+"API ready badge icon, API first platform badge shape, developer integration badge, navy and code brackets design, club management API badge","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_05","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_partner_certified",
|
||||
"prompt":_BP+"partner certified badge icon, certified technology partner badge design shape, navy official partner badge, club management platform partner program","width":400,"height":200,"steps":40},
|
||||
{"id":"p6_ib_06","phase":"phase6_bonus","subfolder":"integration_badges","name":"badge_white_label",
|
||||
"prompt":_BP+"white label partner badge icon, white label ready platform badge shape, club SaaS white label partner certification, professional partner badge navy","width":400,"height":200,"steps":40},
|
||||
|
||||
# Trust/Award Badges (400×400)
|
||||
{"id":"p6_tb_01","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_gdpr_compliant",
|
||||
"prompt":_BP+"trust badge, GDPR compliant badge design icon, data protection certification shape, navy shield with EU star circle, club SaaS data privacy trust badge","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_02","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_soc2_ready",
|
||||
"prompt":_BP+"trust badge, SOC 2 ready certification badge icon, enterprise security trust badge shape, professional certification seal, dark navy badge design","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_03","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_uptime_guarantee",
|
||||
"prompt":_BP+"trust badge, 99.9 percent uptime guarantee badge icon, reliability certification shape, gold uptime number on navy badge, club SaaS reliability trust signal","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_04","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_industry_choice",
|
||||
"prompt":_BP+"award badge, club industry choice award badge design icon, industry recognition award seal shape, gold and navy award badge","width":400,"height":400,"steps":40},
|
||||
{"id":"p6_tb_05","phase":"phase6_bonus","subfolder":"trust_badges","name":"trust_money_back",
|
||||
"prompt":_BP+"trust badge, 30-day money back guarantee badge icon, customer confidence seal shape, navy circle badge, club SaaS satisfaction badge","width":400,"height":400,"steps":40},
|
||||
]
|
||||
|
||||
# Bump steps for quality on phases with smaller dimensions
|
||||
for _a in ASSET_MANIFEST:
|
||||
if _a["steps"] == 30 and _a["width"] <= 1024 and _a["height"] <= 1024:
|
||||
_a["steps"] = 40
|
||||
|
||||
|
||||
def load_progress() -> Dict[str, Any]:
|
||||
"""Load resume state from .progress.json."""
|
||||
if PROGRESS_FILE.exists():
|
||||
try:
|
||||
with open(PROGRESS_FILE) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
return {"completed": [], "failed": [], "started_at": time.strftime("%Y-%m-%dT%H:%M:%S")}
|
||||
|
||||
|
||||
def save_progress(progress: Dict[str, Any]) -> None:
|
||||
"""Save progress state."""
|
||||
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
with open(PROGRESS_FILE, "w") as f:
|
||||
json.dump(progress, f, indent=2)
|
||||
|
||||
|
||||
def load_workflow(model: str) -> Dict:
|
||||
"""Load the appropriate workflow JSON."""
|
||||
path = WORKFLOW_HERETIC if model == "heretic" else WORKFLOW_SCHNELL
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def submit_prompt(comfyui_url: str, workflow: Dict) -> str:
|
||||
"""Submit prompt to ComfyUI."""
|
||||
data = json.dumps({"prompt": workflow}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{comfyui_url}/prompt", data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())["prompt_id"]
|
||||
|
||||
|
||||
def wait_for_image(comfyui_url: str, prompt_id: str, timeout: int = 300) -> Dict | None:
|
||||
"""Poll for completion."""
|
||||
print(" ⏳ Waiting for ComfyUI...", end="", flush=True)
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
with urllib.request.urlopen(f"{comfyui_url}/history/{prompt_id}") as resp:
|
||||
history = json.loads(resp.read())
|
||||
if prompt_id in history:
|
||||
print(" done.", flush=True)
|
||||
outputs = history[prompt_id].get("outputs", {})
|
||||
for node_out in outputs.values():
|
||||
if "images" in node_out:
|
||||
return node_out["images"][0]
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
print(".", end="", flush=True)
|
||||
time.sleep(2)
|
||||
print(" timeout!", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
def download_image(comfyui_url: str, image_info: Dict, output_path: Path) -> bool:
|
||||
"""Download the generated image."""
|
||||
try:
|
||||
url = f"{comfyui_url}/view?filename={image_info['filename']}&subfolder={image_info.get('subfolder', '')}&type=output"
|
||||
with urllib.request.urlopen(url) as resp:
|
||||
img_data = resp.read()
|
||||
output_path.write_bytes(img_data)
|
||||
print(f" ✅ Saved: {output_path} ({len(img_data) // 1024}KB)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ❌ Download failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def patch_workflow(workflow: Dict, asset: Dict, model: str) -> Dict:
|
||||
"""Patch workflow with asset params using per-model node IDs."""
|
||||
seed = random.randint(0, 2**32 - 1)
|
||||
if model == "heretic":
|
||||
# flux2_klein_heretic.json nodes: 2=pos, 3=neg, 6=latent(w/h), 7=scheduler(steps), 10=noise(seed), 13=save
|
||||
workflow["2"]["inputs"]["text"] = asset["prompt"]
|
||||
workflow["3"]["inputs"]["text"] = _NEG
|
||||
workflow["6"]["inputs"]["width"] = asset["width"]
|
||||
workflow["6"]["inputs"]["height"] = asset["height"]
|
||||
workflow["7"]["inputs"]["steps"] = asset["steps"]
|
||||
workflow["13"]["inputs"]["filename_prefix"] = asset["name"]
|
||||
if "10" in workflow:
|
||||
workflow["10"]["inputs"]["noise_seed"] = seed
|
||||
else:
|
||||
# flux_schnell.json nodes: 6=pos, 33=neg, 27=latent(w/h), 13=ksampler(steps/seed), 9=save
|
||||
workflow["6"]["inputs"]["text"] = asset["prompt"]
|
||||
workflow["33"]["inputs"]["text"] = _NEG
|
||||
workflow["27"]["inputs"]["width"] = asset["width"]
|
||||
workflow["27"]["inputs"]["height"] = asset["height"]
|
||||
workflow["13"]["inputs"]["steps"] = asset["steps"]
|
||||
workflow["13"]["inputs"]["seed"] = seed
|
||||
workflow["9"]["inputs"]["filename_prefix"] = asset["name"]
|
||||
return workflow
|
||||
|
||||
|
||||
def generate_asset(comfyui_url: str, asset: Dict, model: str, progress: Dict) -> bool:
|
||||
"""Generate a single asset."""
|
||||
if asset["id"] in progress["completed"]:
|
||||
print(f" ⏭️ Skipping completed: {asset['name']}")
|
||||
return True
|
||||
|
||||
print(f"\n Prompt : {asset['prompt'][:80]}...")
|
||||
print(f" Size : {asset['width']}×{asset['height']} Steps: {asset['steps']}")
|
||||
|
||||
try:
|
||||
workflow = load_workflow(model)
|
||||
workflow = patch_workflow(workflow, asset, model)
|
||||
|
||||
prompt_id = submit_prompt(comfyui_url, workflow)
|
||||
image_info = wait_for_image(comfyui_url, prompt_id)
|
||||
|
||||
if not image_info:
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
output_dir = OUTPUT_ROOT / asset["phase"] / asset["subfolder"]
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / f"{asset['name']}.png"
|
||||
|
||||
if download_image(comfyui_url, image_info, output_path):
|
||||
progress["completed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return True
|
||||
else:
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="ClubManage Brand Asset Generation Pipeline")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Print manifest without generating")
|
||||
parser.add_argument("--phase", help="Generate only assets from this phase (e.g. phase1_logos)")
|
||||
parser.add_argument("--model", choices=["schnell", "heretic"], default="schnell",
|
||||
help="Model: schnell (~10-20s/img) or heretic (~52s/img, higher quality)")
|
||||
parser.add_argument("--comfyui", default="http://localhost:8188", help="ComfyUI URL")
|
||||
parser.add_argument("--steps", type=int, default=None,
|
||||
help="Override steps for all assets (e.g. --steps 12 for higher quality)")
|
||||
args = parser.parse_args()
|
||||
|
||||
comfyui_url = args.comfyui
|
||||
|
||||
# Apply global steps override if requested
|
||||
manifest = ASSET_MANIFEST
|
||||
if args.steps:
|
||||
manifest = [{**a, "steps": args.steps} for a in manifest]
|
||||
|
||||
# Filter by phase if requested
|
||||
to_generate = [a for a in manifest if not args.phase or a["phase"] == args.phase]
|
||||
|
||||
print("🚀 ClubManage Brand Asset Generation Pipeline")
|
||||
print(f" Output : {OUTPUT_ROOT}")
|
||||
print(f" Model : {args.model}")
|
||||
print(f" ComfyUI : {comfyui_url}")
|
||||
print(f" Total : {len(ASSET_MANIFEST)} assets in manifest")
|
||||
print(f" Selected: {len(to_generate)} assets to generate")
|
||||
print(f" Note : All images are text-free — pure visual/icon design")
|
||||
|
||||
if args.dry_run:
|
||||
phases: Dict[str, int] = {}
|
||||
for a in to_generate:
|
||||
phases[a["phase"]] = phases.get(a["phase"], 0) + 1
|
||||
for ph, count in phases.items():
|
||||
print(f"\n {ph} ({count} assets):")
|
||||
for a in to_generate:
|
||||
if a["phase"] == ph:
|
||||
print(f" {a['id']:14} | {a['name']:35} | {a['width']}×{a['height']} steps={a['steps']}")
|
||||
total_min_est = sum(a["steps"] * 2.5 for a in to_generate) / 60
|
||||
print(f"\n ⏱️ Estimated runtime (schnell): ~{total_min_est:.0f} minutes")
|
||||
print("\nDry run complete. Remove --dry-run to begin generation.")
|
||||
return
|
||||
|
||||
progress = load_progress()
|
||||
remaining = [a for a in to_generate if a["id"] not in progress["completed"]]
|
||||
print(f" Resume : {len(progress['completed'])} completed, {len(progress['failed'])} failed, {len(remaining)} remaining")
|
||||
|
||||
if not remaining:
|
||||
print("\n✅ All selected assets already complete!")
|
||||
return
|
||||
|
||||
print(f"\nStarting generation... (Ctrl+C to pause — progress is saved)")
|
||||
|
||||
n_done = 0
|
||||
n_fail = 0
|
||||
for i, asset in enumerate(to_generate, 1):
|
||||
if asset["id"] in progress["completed"]:
|
||||
continue
|
||||
print(f"\n[{asset['phase']}] [{i}/{len(to_generate)}] {asset['name']}")
|
||||
if generate_asset(comfyui_url, asset, args.model, progress):
|
||||
n_done += 1
|
||||
else:
|
||||
n_fail += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 PIPELINE COMPLETE")
|
||||
print(f" ✅ Completed this run : {n_done}")
|
||||
print(f" ❌ Failed this run : {n_fail}")
|
||||
print(f" 📦 Total completed : {len(progress['completed'])} / {len(ASSET_MANIFEST)}")
|
||||
if progress["failed"]:
|
||||
print(f" Failed IDs: {', '.join(progress['failed'][-10:])}")
|
||||
print(f" Assets saved to: {OUTPUT_ROOT}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick CLI for generating images via ComfyUI + FLUX.2 Klein Heretic.
|
||||
|
||||
Usage:
|
||||
python gen.py "your prompt here"
|
||||
python gen.py "your prompt" --steps 20 --width 1280 --height 720
|
||||
python gen.py "your prompt" --seed 12345
|
||||
python gen.py "your prompt" --count 3
|
||||
|
||||
Output saved to ~/Pictures/mcp-generated/
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
COMFYUI = "http://localhost:8188"
|
||||
OUTPUT_DIR = Path.home() / "Pictures" / "mcp-generated"
|
||||
WORKFLOW_PATH = Path(__file__).parent / "src/workflows/flux2_klein_heretic.json"
|
||||
|
||||
|
||||
def load_workflow():
|
||||
with open(WORKFLOW_PATH) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def submit(workflow):
|
||||
data = json.dumps({"prompt": workflow}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{COMFYUI}/prompt", data=data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())["prompt_id"]
|
||||
|
||||
|
||||
def wait(prompt_id, timeout=300):
|
||||
print(" ⏳ Waiting for ComfyUI...", end="", flush=True)
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
with urllib.request.urlopen(f"{COMFYUI}/history/{prompt_id}") as resp:
|
||||
history = json.loads(resp.read())
|
||||
if prompt_id in history:
|
||||
print(" done.", flush=True)
|
||||
outputs = history[prompt_id].get("outputs", {})
|
||||
for node_out in outputs.values():
|
||||
if "images" in node_out:
|
||||
return node_out["images"][0]
|
||||
return None
|
||||
print(".", end="", flush=True)
|
||||
time.sleep(2)
|
||||
raise TimeoutError(f"Timed out after {timeout}s")
|
||||
|
||||
|
||||
def download(filename, subfolder=""):
|
||||
url = f"{COMFYUI}/view?filename={filename}&subfolder={subfolder}&type=output"
|
||||
with urllib.request.urlopen(url) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
def generate(prompt, steps=20, width=1024, height=1024, seed=-1, name="cli"):
|
||||
if seed == -1:
|
||||
seed = random.randint(0, 2**32 - 1)
|
||||
|
||||
workflow = load_workflow()
|
||||
|
||||
# Patch positive prompt (node 2)
|
||||
workflow["2"]["inputs"]["text"] = prompt
|
||||
# Patch negative prompt (node 3) — leave empty
|
||||
workflow["3"]["inputs"]["text"] = ""
|
||||
# Patch seed (node 10)
|
||||
if "10" in workflow:
|
||||
workflow["10"]["inputs"]["noise_seed"] = seed
|
||||
# Patch dimensions (node 6)
|
||||
workflow["6"]["inputs"]["width"] = width
|
||||
workflow["6"]["inputs"]["height"] = height
|
||||
# Patch steps (node 7)
|
||||
workflow["7"]["inputs"]["steps"] = steps
|
||||
# Patch output filename (node 13)
|
||||
workflow["13"]["inputs"]["filename_prefix"] = name
|
||||
|
||||
print(f" Prompt : {prompt[:80]}{'...' if len(prompt) > 80 else ''}")
|
||||
print(f" Size : {width}×{height} Steps: {steps} Seed: {seed}")
|
||||
|
||||
prompt_id = submit(workflow)
|
||||
image_info = wait(prompt_id)
|
||||
|
||||
if not image_info:
|
||||
print(" ❌ No output image returned.", file=sys.stderr)
|
||||
return None
|
||||
|
||||
img_data = download(image_info["filename"], image_info.get("subfolder", ""))
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
out_path = OUTPUT_DIR / f"{name}_{seed}.png"
|
||||
out_path.write_bytes(img_data)
|
||||
print(f" ✅ Saved: {out_path} ({len(img_data) // 1024}KB)")
|
||||
return out_path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate images via ComfyUI FLUX.2 Klein Heretic"
|
||||
)
|
||||
parser.add_argument("prompt", help="Text prompt for the image")
|
||||
parser.add_argument("--steps", type=int, default=20, help="Inference steps (default: 20)")
|
||||
parser.add_argument("--width", type=int, default=1024, help="Width in pixels (default: 1024)")
|
||||
parser.add_argument("--height", type=int, default=1024, help="Height in pixels (default: 1024)")
|
||||
parser.add_argument("--seed", type=int, default=-1, help="Seed (-1 = random)")
|
||||
parser.add_argument("--count", type=int, default=1, help="Number of images (default: 1)")
|
||||
parser.add_argument("--name", default="cli", help="Output filename prefix (default: cli)")
|
||||
args = parser.parse_args()
|
||||
|
||||
for i in range(args.count):
|
||||
if args.count > 1:
|
||||
print(f"\n[{i+1}/{args.count}]")
|
||||
seed = args.seed if args.seed != -1 else -1
|
||||
generate(
|
||||
prompt=args.prompt,
|
||||
steps=args.steps,
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
seed=seed,
|
||||
name=f"{args.name}_{i+1:02d}" if args.count > 1 else args.name,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FLUX Image Generator — Desktop GUI
|
||||
Supports: FLUX.1 Schnell (fast) and FLUX.2 Klein Heretic (unrestricted)
|
||||
Run: python3 gui.py
|
||||
No external dependencies — stdlib + tkinter only.
|
||||
"""
|
||||
|
||||
import json
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
|
||||
COMFYUI = "http://localhost:8188"
|
||||
OUTPUT_DIR = Path.home() / "Pictures" / "mcp-generated"
|
||||
WORKFLOWS_DIR = Path(__file__).parent / "src/workflows"
|
||||
|
||||
# ── Model definitions ────────────────────────────────────────────────────────
|
||||
# Each entry defines how to patch the workflow for that model.
|
||||
# node_pos / node_neg: CLIPTextEncode node IDs for positive/negative prompts
|
||||
# node_latent: latent image node (for width/height)
|
||||
# node_seed: where to write the seed value
|
||||
# node_save: SaveImage node (for filename_prefix)
|
||||
# node_steps: dict of {node_id: field} for steps — None if not patchable
|
||||
MODELS = {
|
||||
"FLUX.2 Klein Heretic (unrestricted)": {
|
||||
"workflow": "flux2_klein_heretic.json",
|
||||
"default_steps": 20,
|
||||
"node_pos": ("2", "text"),
|
||||
"node_neg": ("3", "text"),
|
||||
"node_latent": ("6", "width", "height"),
|
||||
"node_seed": ("10", "noise_seed"),
|
||||
"node_steps": ("7", "steps"),
|
||||
"node_save": ("13", "filename_prefix"),
|
||||
"description": "FLUX.2 Klein 4B + Heretic abliterated encoder. ~50s/image. No refusals.",
|
||||
},
|
||||
"FLUX.1 Schnell (fast)": {
|
||||
"workflow": "flux_schnell.json",
|
||||
"default_steps": 4,
|
||||
"node_pos": ("6", "text"),
|
||||
"node_neg": ("33", "text"),
|
||||
"node_latent": ("27", "width", "height"),
|
||||
"node_seed": ("13", "seed"), # KSampler has seed directly
|
||||
"node_steps": ("13", "steps"),
|
||||
"node_save": ("9", "filename_prefix"),
|
||||
"description": "FLUX.1 Schnell — fast (~5s/image), standard quality. Has safety filter.",
|
||||
},
|
||||
}
|
||||
|
||||
PRESETS = {
|
||||
"Square 1024": (1024, 1024),
|
||||
"Landscape 16:9": (1280, 720),
|
||||
"Portrait 9:16": (720, 1280),
|
||||
"Wide 3:2": (1536, 1024),
|
||||
"Tall 2:3": (1024, 1536),
|
||||
"BFL Wide 7:4": (1344, 768),
|
||||
"BFL Tall 4:7": (768, 1344),
|
||||
}
|
||||
|
||||
|
||||
def load_workflow(filename):
|
||||
with open(WORKFLOWS_DIR / filename) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def patch_workflow(wf, spec, prompt, neg, width, height, steps, seed, name):
|
||||
"""Apply generation parameters to a workflow dict in-place."""
|
||||
node_pos_id, node_pos_field = spec["node_pos"]
|
||||
node_neg_id, node_neg_field = spec["node_neg"]
|
||||
lat_id, lat_w, lat_h = spec["node_latent"]
|
||||
seed_id, seed_field = spec["node_seed"]
|
||||
save_id, save_field = spec["node_save"]
|
||||
|
||||
wf[node_pos_id]["inputs"][node_pos_field] = prompt
|
||||
wf[node_neg_id]["inputs"][node_neg_field] = neg
|
||||
wf[lat_id]["inputs"][lat_w] = width
|
||||
wf[lat_id]["inputs"][lat_h] = height
|
||||
wf[seed_id]["inputs"][seed_field] = seed
|
||||
wf[save_id]["inputs"][save_field] = name
|
||||
|
||||
if spec["node_steps"]:
|
||||
steps_id, steps_field = spec["node_steps"]
|
||||
wf[steps_id]["inputs"][steps_field] = steps
|
||||
|
||||
return wf
|
||||
|
||||
|
||||
def submit_prompt(workflow):
|
||||
data = json.dumps({"prompt": workflow}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{COMFYUI}/prompt", data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read())["prompt_id"]
|
||||
|
||||
|
||||
def wait_for_image(prompt_id, timeout=300):
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
with urllib.request.urlopen(f"{COMFYUI}/history/{prompt_id}", timeout=10) as resp:
|
||||
history = json.loads(resp.read())
|
||||
if prompt_id in history:
|
||||
for node_out in history[prompt_id].get("outputs", {}).values():
|
||||
if "images" in node_out:
|
||||
return node_out["images"][0]
|
||||
return None
|
||||
time.sleep(2)
|
||||
raise TimeoutError("Timed out waiting for image")
|
||||
|
||||
|
||||
def download_image(filename, subfolder=""):
|
||||
url = f"{COMFYUI}/view?filename={filename}&subfolder={subfolder}&type=output"
|
||||
with urllib.request.urlopen(url, timeout=30) as resp:
|
||||
return resp.read()
|
||||
|
||||
|
||||
class App(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("FLUX Image Generator")
|
||||
self.resizable(True, True)
|
||||
self.minsize(760, 640)
|
||||
self._current_image = None
|
||||
self._build_ui()
|
||||
|
||||
def _build_ui(self):
|
||||
main = ttk.Frame(self, padding=12)
|
||||
main.pack(fill="both", expand=True)
|
||||
main.columnconfigure(0, weight=1)
|
||||
main.columnconfigure(1, weight=2)
|
||||
|
||||
# ── LEFT PANEL ───────────────────────────────────────────────────────
|
||||
left = ttk.Frame(main)
|
||||
left.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
|
||||
left.columnconfigure(0, weight=1)
|
||||
row = 0
|
||||
|
||||
# Model selector
|
||||
ttk.Label(left, text="Model", font=("", 10, "bold")).grid(
|
||||
row=row, column=0, sticky="w"); row += 1
|
||||
self.model_var = tk.StringVar(value=list(MODELS.keys())[0])
|
||||
model_cb = ttk.Combobox(left, textvariable=self.model_var,
|
||||
values=list(MODELS.keys()), state="readonly", width=40)
|
||||
model_cb.grid(row=row, column=0, sticky="ew", pady=(2, 2)); row += 1
|
||||
model_cb.bind("<<ComboboxSelected>>", self._on_model_change)
|
||||
self.model_desc = ttk.Label(left, text="", foreground="gray",
|
||||
wraplength=300, font=("", 8))
|
||||
self.model_desc.grid(row=row, column=0, sticky="w", pady=(0, 8)); row += 1
|
||||
# description updated after all widgets are created (see end of _build_ui)
|
||||
|
||||
# Prompt
|
||||
ttk.Label(left, text="Prompt", font=("", 10, "bold")).grid(
|
||||
row=row, column=0, sticky="w"); row += 1
|
||||
self.prompt_txt = scrolledtext.ScrolledText(left, height=6, wrap="word", font=("", 10))
|
||||
self.prompt_txt.grid(row=row, column=0, sticky="ew", pady=(2, 8)); row += 1
|
||||
|
||||
# Negative prompt
|
||||
ttk.Label(left, text="Negative Prompt (optional)").grid(
|
||||
row=row, column=0, sticky="w"); row += 1
|
||||
self.neg_txt = scrolledtext.ScrolledText(left, height=3, wrap="word", font=("", 9))
|
||||
self.neg_txt.grid(row=row, column=0, sticky="ew", pady=(2, 8)); row += 1
|
||||
|
||||
# Size preset
|
||||
ttk.Label(left, text="Size Preset").grid(row=row, column=0, sticky="w"); row += 1
|
||||
self.preset_var = tk.StringVar(value="Square 1024")
|
||||
preset_cb = ttk.Combobox(left, textvariable=self.preset_var,
|
||||
values=list(PRESETS.keys()), state="readonly")
|
||||
preset_cb.grid(row=row, column=0, sticky="ew", pady=(2, 2)); row += 1
|
||||
preset_cb.bind("<<ComboboxSelected>>", self._apply_preset)
|
||||
|
||||
# Width / Height
|
||||
wh = ttk.Frame(left)
|
||||
wh.grid(row=row, column=0, sticky="ew", pady=(4, 8)); row += 1
|
||||
wh.columnconfigure(1, weight=1)
|
||||
wh.columnconfigure(3, weight=1)
|
||||
ttk.Label(wh, text="W").grid(row=0, column=0, padx=(0, 4))
|
||||
self.width_var = tk.IntVar(value=1024)
|
||||
ttk.Spinbox(wh, from_=256, to=4096, increment=8, textvariable=self.width_var,
|
||||
width=6).grid(row=0, column=1, sticky="ew")
|
||||
ttk.Label(wh, text="H", padding=(8, 0, 4, 0)).grid(row=0, column=2)
|
||||
self.height_var = tk.IntVar(value=1024)
|
||||
ttk.Spinbox(wh, from_=256, to=4096, increment=8, textvariable=self.height_var,
|
||||
width=6).grid(row=0, column=3, sticky="ew")
|
||||
|
||||
# Steps
|
||||
steps_row = ttk.Frame(left)
|
||||
steps_row.grid(row=row, column=0, sticky="ew", pady=(0, 4)); row += 1
|
||||
steps_row.columnconfigure(1, weight=1)
|
||||
ttk.Label(steps_row, text="Steps").grid(row=0, column=0, padx=(0, 8))
|
||||
self.steps_var = tk.IntVar(value=20)
|
||||
self.steps_lbl = ttk.Label(steps_row, text="20", width=3)
|
||||
self.steps_lbl.grid(row=0, column=2, padx=(6, 0))
|
||||
ttk.Scale(steps_row, from_=1, to=60, variable=self.steps_var,
|
||||
orient="horizontal",
|
||||
command=lambda v: self.steps_lbl.config(text=str(int(float(v))))
|
||||
).grid(row=0, column=1, sticky="ew")
|
||||
|
||||
# Count
|
||||
count_row = ttk.Frame(left)
|
||||
count_row.grid(row=row, column=0, sticky="ew", pady=(0, 8)); row += 1
|
||||
count_row.columnconfigure(1, weight=1)
|
||||
ttk.Label(count_row, text="Count").grid(row=0, column=0, padx=(0, 8))
|
||||
self.count_var = tk.IntVar(value=1)
|
||||
ttk.Spinbox(count_row, from_=1, to=20, textvariable=self.count_var,
|
||||
width=4).grid(row=0, column=1, sticky="w")
|
||||
|
||||
# Seed
|
||||
seed_row = ttk.Frame(left)
|
||||
seed_row.grid(row=row, column=0, sticky="ew", pady=(0, 8)); row += 1
|
||||
seed_row.columnconfigure(1, weight=1)
|
||||
ttk.Label(seed_row, text="Seed").grid(row=0, column=0, padx=(0, 8))
|
||||
self.seed_var = tk.StringVar(value="-1")
|
||||
ttk.Entry(seed_row, textvariable=self.seed_var, width=12).grid(row=0, column=1, sticky="w")
|
||||
ttk.Button(seed_row, text="🎲", width=3,
|
||||
command=lambda: self.seed_var.set(str(random.randint(0, 2**32 - 1)))
|
||||
).grid(row=0, column=2, padx=(4, 0))
|
||||
ttk.Label(seed_row, text="(-1 = random)", foreground="gray"
|
||||
).grid(row=0, column=3, padx=(4, 0))
|
||||
|
||||
# Name
|
||||
name_row = ttk.Frame(left)
|
||||
name_row.grid(row=row, column=0, sticky="ew", pady=(0, 12)); row += 1
|
||||
name_row.columnconfigure(1, weight=1)
|
||||
ttk.Label(name_row, text="Name").grid(row=0, column=0, padx=(0, 8))
|
||||
self.name_var = tk.StringVar(value="img")
|
||||
ttk.Entry(name_row, textvariable=self.name_var).grid(row=0, column=1, sticky="ew")
|
||||
|
||||
# Generate button
|
||||
self.gen_btn = ttk.Button(left, text="⚡ Generate", command=self._start_generation)
|
||||
self.gen_btn.grid(row=row, column=0, sticky="ew", pady=(0, 8)); row += 1
|
||||
|
||||
# Status + progress
|
||||
self.status_var = tk.StringVar(value="Ready")
|
||||
ttk.Label(left, textvariable=self.status_var, foreground="gray",
|
||||
wraplength=300).grid(row=row, column=0, sticky="w"); row += 1
|
||||
self.progress = ttk.Progressbar(left, mode="indeterminate")
|
||||
self.progress.grid(row=row, column=0, sticky="ew", pady=(4, 0)); row += 1
|
||||
|
||||
# ── RIGHT PANEL — preview ────────────────────────────────────────────
|
||||
right = ttk.LabelFrame(main, text="Preview", padding=8)
|
||||
right.grid(row=0, column=1, sticky="nsew")
|
||||
right.columnconfigure(0, weight=1)
|
||||
right.rowconfigure(0, weight=1)
|
||||
|
||||
self.preview_lbl = ttk.Label(right, text="No image yet", anchor="center",
|
||||
background="#1a1a1a", foreground="#888")
|
||||
self.preview_lbl.grid(row=0, column=0, sticky="nsew")
|
||||
|
||||
self.path_lbl = ttk.Label(right, text="", foreground="gray", font=("", 8))
|
||||
self.path_lbl.grid(row=1, column=0, sticky="w", pady=(4, 0))
|
||||
|
||||
ttk.Button(right, text="Open folder",
|
||||
command=self._open_folder).grid(row=2, column=0, sticky="e", pady=(4, 0))
|
||||
|
||||
# Init model description + steps default now that all widgets exist
|
||||
self._on_model_change()
|
||||
|
||||
def _on_model_change(self, _=None):
|
||||
spec = MODELS[self.model_var.get()]
|
||||
self.model_desc.config(text=spec["description"])
|
||||
self.steps_var.set(spec["default_steps"])
|
||||
self.steps_lbl.config(text=str(spec["default_steps"]))
|
||||
|
||||
def _apply_preset(self, _=None):
|
||||
w, h = PRESETS[self.preset_var.get()]
|
||||
self.width_var.set(w)
|
||||
self.height_var.set(h)
|
||||
|
||||
def _start_generation(self):
|
||||
prompt = self.prompt_txt.get("1.0", "end").strip()
|
||||
if not prompt:
|
||||
messagebox.showwarning("No prompt", "Please enter a prompt.")
|
||||
return
|
||||
self.gen_btn.config(state="disabled")
|
||||
self.progress.start(10)
|
||||
self.status_var.set("Generating…")
|
||||
t = threading.Thread(target=self._run_generation, args=(prompt,), daemon=True)
|
||||
t.start()
|
||||
|
||||
def _run_generation(self, prompt):
|
||||
try:
|
||||
neg = self.neg_txt.get("1.0", "end").strip()
|
||||
steps = int(self.steps_var.get())
|
||||
width = int(self.width_var.get())
|
||||
height = int(self.height_var.get())
|
||||
count = int(self.count_var.get())
|
||||
name = self.name_var.get().strip() or "img"
|
||||
seed_str = self.seed_var.get().strip()
|
||||
base_seed = int(seed_str) if seed_str else -1
|
||||
|
||||
model_name = self.model_var.get()
|
||||
spec = MODELS[model_name]
|
||||
|
||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for i in range(count):
|
||||
seed = (base_seed if base_seed == -1 else base_seed + i)
|
||||
if seed == -1:
|
||||
seed = random.randint(0, 2**32 - 1)
|
||||
|
||||
label = f"{name}_{i+1:02d}" if count > 1 else name
|
||||
self.after(0, self.status_var.set,
|
||||
f"[{i+1}/{count}] {model_name.split('(')[0].strip()} · seed {seed}…")
|
||||
|
||||
wf = load_workflow(spec["workflow"])
|
||||
patch_workflow(wf, spec, prompt, neg, width, height, steps, seed, label)
|
||||
|
||||
prompt_id = submit_prompt(wf)
|
||||
img_info = wait_for_image(prompt_id)
|
||||
|
||||
if img_info:
|
||||
img_data = download_image(img_info["filename"], img_info.get("subfolder", ""))
|
||||
out_path = OUTPUT_DIR / f"{label}_{seed}.png"
|
||||
out_path.write_bytes(img_data)
|
||||
self.after(0, self._show_preview, out_path)
|
||||
|
||||
self.after(0, self.status_var.set,
|
||||
f"✅ Done — {count} image(s) saved to ~/Pictures/mcp-generated/")
|
||||
except Exception as exc:
|
||||
self.after(0, self.status_var.set, f"❌ Error: {exc}")
|
||||
finally:
|
||||
self.after(0, self.progress.stop)
|
||||
self.after(0, lambda: self.gen_btn.config(state="normal"))
|
||||
|
||||
def _show_preview(self, path):
|
||||
try:
|
||||
photo = tk.PhotoImage(file=str(path))
|
||||
pw, ph = photo.width(), photo.height()
|
||||
subsample = 1
|
||||
while pw // subsample > 600 or ph // subsample > 600:
|
||||
subsample += 1
|
||||
if subsample > 1:
|
||||
photo = photo.subsample(subsample, subsample)
|
||||
self.preview_lbl.config(image=photo, text="")
|
||||
self._current_image = photo
|
||||
self.path_lbl.config(text=str(path))
|
||||
except Exception as e:
|
||||
self.status_var.set(f"Preview error: {e}")
|
||||
|
||||
def _open_folder(self):
|
||||
import subprocess
|
||||
subprocess.Popen(["xdg-open", str(OUTPUT_DIR)])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = App()
|
||||
app.mainloop()
|
||||
@@ -0,0 +1,411 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Patrick Hidden Name Artwork Generation Pipeline
|
||||
|
||||
Autonomous script to generate 26 ultra-detailed, visually rich digital artworks
|
||||
where the name "PATRICK" is cleverly concealed within each composition.
|
||||
Letters emerge organically from shapes, patterns, negative space, silhouettes,
|
||||
alignment, or cumulative elements — never as plain text.
|
||||
Visible only upon close inspection.
|
||||
|
||||
Uses the existing mcp-image-gen infrastructure (ComfyUI FLUX workflows).
|
||||
Output organized in ~/Pictures/patrick_hidden_name/{theme}/{style}/
|
||||
|
||||
Usage:
|
||||
cd /home/pplate/pi_mcps
|
||||
python mcp/mcp-image-gen/patrick_hidden_gen.py
|
||||
python mcp/mcp-image-gen/patrick_hidden_gen.py --dry-run
|
||||
python mcp/mcp-image-gen/patrick_hidden_gen.py --model heretic
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import random
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
# --- Configuration ---
|
||||
OUTPUT_ROOT = Path.home() / "Pictures" / "patrick_hidden_name"
|
||||
PROGRESS_FILE = OUTPUT_ROOT / ".progress.json"
|
||||
WORKFLOW_SCHNELL = Path(__file__).parent / "src/workflows/flux_schnell.json"
|
||||
WORKFLOW_HERETIC = Path(__file__).parent / "src/workflows/flux2_klein_heretic.json"
|
||||
|
||||
# Core hidden-name prompting technique — applied to every asset
|
||||
_HT = (
|
||||
"the name PATRICK is cleverly and seamlessly concealed within the composition, "
|
||||
"letters P-A-T-R-I-C-K emerge organically from shapes patterns negative space "
|
||||
"silhouettes alignment or cumulative arrangement of multiple elements, "
|
||||
"never plain text, subtle yet unmistakable once discovered, requires genuine "
|
||||
"visual discovery and close inspection, natural integration into the scene, "
|
||||
)
|
||||
|
||||
# Base quality boosters
|
||||
_Q = "ultra-high detail, photorealistic rendering with cinematic lighting, intricate textures, depth of field, 8k resolution, masterpiece, best quality, "
|
||||
|
||||
ASSET_MANIFEST: List[Dict[str, Any]] = [
|
||||
# 1. Dense crowds of marionette puppets — photoreal
|
||||
{"id":"phn_01","theme":"marionettes","style":"photoreal","name":"marionette_crowd_puppets",
|
||||
"prompt":_Q+_HT+"dense crowd of antique wooden marionette puppets on theatrical stage, "
|
||||
"strings and body poses form PATRICK through negative space and limb alignment, "
|
||||
"dramatic stage lighting with velvet curtains, realistic wood grain and fabric textures, "
|
||||
"photorealistic cinematic","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 2. Marionettes — painterly
|
||||
{"id":"phn_02","theme":"marionettes","style":"painterly","name":"marionette_theater_oil",
|
||||
"prompt":_Q+_HT+"oil painting of crowded marionette theater, puppet strings and poses "
|
||||
"form hidden PATRICK letters in composition, baroque style, rich colors, dramatic chiaroscuro "
|
||||
"lighting, thick impasto brushwork","width":1024,"height":1024,"steps":20},
|
||||
|
||||
# 3. Birds — aerial formation
|
||||
{"id":"phn_03","theme":"birds","style":"aerial","name":"bird_flock_formation",
|
||||
"prompt":_Q+_HT+"aerial photograph of massive flock of starlings in precise murmuration, "
|
||||
"bird silhouettes and gaps spell PATRICK through negative space and wing alignments, "
|
||||
"golden hour light, vast sky, ultra realistic feathers and motion blur","width":1280,"height":720,"steps":4},
|
||||
|
||||
# 4. Birds — macro
|
||||
{"id":"phn_04","theme":"birds","style":"macro","name":"bird_swarm_closeup",
|
||||
"prompt":_Q+_HT+"macro photography of bird murmuration where individual bird silhouettes "
|
||||
"and wing alignments subtly spell PATRICK, intricate feather detail, soft bokeh background, "
|
||||
"extreme close focus","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 5. Tree roots — photoreal
|
||||
{"id":"phn_05","theme":"tree_roots","style":"photoreal","name":"ancient_tree_roots",
|
||||
"prompt":_Q+_HT+"ancient oak tree with massive tangled roots and branches that naturally "
|
||||
"form letters PATRICK in their curves and intersections, forest floor moss and dappled sunlight, "
|
||||
"hyper realistic bark texture, dramatic directional lighting","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 6. Tree roots — painterly
|
||||
{"id":"phn_06","theme":"tree_roots","style":"painterly","name":"tree_root_illustration",
|
||||
"prompt":_Q+_HT+"detailed botanical illustration of tree roots and branches forming hidden "
|
||||
"PATRICK name through organic growth patterns, ink and watercolor, scientific accuracy with "
|
||||
"artistic flair, John Muir style","width":1024,"height":1024,"steps":12},
|
||||
|
||||
# 7. School of fish
|
||||
{"id":"phn_07","theme":"fish_school","style":"underwater","name":"fish_school_choreography",
|
||||
"prompt":_Q+_HT+"underwater scene of thousands of tropical fish in synchronized school, "
|
||||
"swimming patterns and gaps between bodies form PATRICK, crystal clear tropical water, "
|
||||
"realistic scales and light caustics, underwater photography","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 8. Architecture — gothic facade
|
||||
{"id":"phn_08","theme":"architecture","style":"architectural","name":"gothic_facade_hidden",
|
||||
"prompt":_Q+_HT+"ornate gothic cathedral facade where windows arches and stone carvings "
|
||||
"subtly align to spell PATRICK in negative space and shadow play, dramatic sunset lighting, "
|
||||
"ultra detailed stone texture, architectural rendering","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 9. Architecture — aerial
|
||||
{"id":"phn_09","theme":"architecture","style":"aerial","name":"modern_skyscraper_letters",
|
||||
"prompt":_Q+_HT+"aerial view of modern city building complex where rooflines shadows and "
|
||||
"window patterns form the hidden name PATRICK, golden hour, photorealistic architectural "
|
||||
"rendering, top-down perspective","width":1280,"height":720,"steps":4},
|
||||
|
||||
# 10. Coral reef
|
||||
{"id":"phn_10","theme":"coral_reef","style":"underwater","name":"vibrant_coral_reef",
|
||||
"prompt":_Q+_HT+"vibrant coral reef ecosystem where branching coral fish and rock formations "
|
||||
"naturally compose letters PATRICK through color and shape alignment, crystal water, "
|
||||
"macro detail on polyps and textures, underwater photography","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 11. City skyline at night
|
||||
{"id":"phn_11","theme":"city_skyline","style":"night","name":"neon_skyline_hidden",
|
||||
"prompt":_Q+_HT+"futuristic city skyline at night where building lights and window patterns "
|
||||
"subtly spell PATRICK in the neon glow and negative space between towers, cyberpunk atmosphere, "
|
||||
"realistic reflections and bokeh, long exposure photography","width":1280,"height":720,"steps":4},
|
||||
|
||||
# 12. Rolling hills landscape
|
||||
{"id":"phn_12","theme":"hills_landscape","style":"aerial","name":"rolling_hills_contours",
|
||||
"prompt":_Q+_HT+"aerial view of rolling green hills and valleys where landscape contours "
|
||||
"hedgerows and elevation shadows subtly form PATRICK, golden hour pastoral scene, "
|
||||
"drone photography style","width":1280,"height":720,"steps":4},
|
||||
|
||||
# 13. Persian rug
|
||||
{"id":"phn_13","theme":"persian_rug","style":"macro","name":"persian_rug_intricate",
|
||||
"prompt":_Q+_HT+"extremely detailed close-up of hand-woven Persian rug where geometric and "
|
||||
"floral patterns align to conceal name PATRICK in repeating motifs and negative space, "
|
||||
"rich colors, silk texture, macro photography","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 14. Butterflies
|
||||
{"id":"phn_14","theme":"butterflies","style":"painterly","name":"butterfly_swarm_metamorphosis",
|
||||
"prompt":_Q+_HT+"swarm of colorful butterflies in flight where wing patterns and flight paths "
|
||||
"collectively form hidden letters PATRICK, ethereal garden setting, detailed wing scales, "
|
||||
"soft natural light, painterly illustration style","width":1024,"height":1024,"steps":12},
|
||||
|
||||
# 15. Circuit board
|
||||
{"id":"phn_15","theme":"circuit_board","style":"macro","name":"circuit_board_traces",
|
||||
"prompt":_Q+_HT+"extreme macro of complex multilayer circuit board where copper traces "
|
||||
"solder points and component placement subtly spell PATRICK in wiring layout, "
|
||||
"realistic metallic reflections, depth of field, technical precision","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 16. Ocean waves
|
||||
{"id":"phn_16","theme":"ocean_waves","style":"photoreal","name":"crashing_waves_hidden",
|
||||
"prompt":_Q+_HT+"dramatic crashing ocean waves where foam spray and wave crests align to "
|
||||
"reveal name PATRICK in negative space and water movement, powerful seascape, "
|
||||
"photorealistic water physics and light refraction","width":1280,"height":720,"steps":4},
|
||||
|
||||
# 17. Smoke and clouds
|
||||
{"id":"phn_17","theme":"smoke_clouds","style":"painterly","name":"ethereal_smoke_clouds",
|
||||
"prompt":_Q+_HT+"ethereal smoke and cloud formations in sky where swirling patterns and "
|
||||
"negative space subtly spell PATRICK, dramatic volumetric lighting, painterly atmospheric "
|
||||
"style, high detail turbulence and wisps","width":1024,"height":1024,"steps":12},
|
||||
|
||||
# 18. Dense jungle foliage
|
||||
{"id":"phn_18","theme":"jungle_foliage","style":"macro","name":"dense_jungle_canopy",
|
||||
"prompt":_Q+_HT+"dense tropical jungle foliage where leaves vines and light rays through "
|
||||
"canopy form hidden name PATRICK through alignment and negative space, "
|
||||
"ultra detailed leaf veins and moisture, macro realism","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 19. Roman mosaic
|
||||
{"id":"phn_19","theme":"roman_mosaic","style":"architectural","name":"roman_mosaic_floor",
|
||||
"prompt":_Q+_HT+"ancient Roman mosaic floor where thousands of tiny colored tiles arrange "
|
||||
"to subtly hide name PATRICK in geometric pattern, realistic stone texture, "
|
||||
"archaeological lighting, high detail tesserae","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 20. Military parade — aerial
|
||||
{"id":"phn_20","theme":"military_parade","style":"aerial","name":"parade_formation_overhead",
|
||||
"prompt":_Q+_HT+"aerial view of military parade formation where soldiers in perfect alignment "
|
||||
"create letters PATRICK through their positions and shadows, crisp uniforms, "
|
||||
"dramatic overhead perspective, photorealistic","width":1280,"height":720,"steps":4},
|
||||
|
||||
# 21. Stained glass
|
||||
{"id":"phn_21","theme":"stained_glass","style":"architectural","name":"cathedral_stained_glass",
|
||||
"prompt":_Q+_HT+"intricate stained glass window in gothic cathedral where lead lines and "
|
||||
"colored glass panes form hidden name PATRICK through negative space and symbolic arrangement, "
|
||||
"luminous backlighting, ultra detailed glass texture","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 22. Spider web
|
||||
{"id":"phn_22","theme":"spider_web","style":"macro","name":"dew_spider_web_geometry",
|
||||
"prompt":_Q+_HT+"macro photograph of perfect orb spider web with morning dew where radial "
|
||||
"and spiral threads align to spell PATRICK in geometric structure, sparkling water droplets, "
|
||||
"soft morning light, extreme detail","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 23. Galaxy star map
|
||||
{"id":"phn_23","theme":"galaxy_stars","style":"cosmic","name":"star_map_constellation",
|
||||
"prompt":_Q+_HT+"detailed star map of spiral galaxy where constellations and star clusters "
|
||||
"subtly form letters PATRICK through their positions and connecting lines, "
|
||||
"nebulae and cosmic dust, astronomical precision","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 24. Subway map
|
||||
{"id":"phn_24","theme":"subway_map","style":"architectural","name":"subway_network_map",
|
||||
"prompt":_Q+_HT+"highly detailed schematic subway tunnel map where intersecting colored lines "
|
||||
"station markers and tunnel curves naturally spell name PATRICK in network layout, "
|
||||
"clean diagrammatic style with realistic depth","width":1280,"height":720,"steps":4},
|
||||
|
||||
# 25. Marionettes — architectural rendering
|
||||
{"id":"phn_25","theme":"marionettes","style":"architectural","name":"puppet_theater_stage",
|
||||
"prompt":_Q+_HT+"architectural rendering of elaborate marionette theater stage where puppet "
|
||||
"strings stage lights and scenery elements form hidden PATRICK, dramatic perspective, "
|
||||
"ultra detailed wood and fabric","width":1024,"height":1024,"steps":4},
|
||||
|
||||
# 26. Birds — painterly
|
||||
{"id":"phn_26","theme":"birds","style":"painterly","name":"bird_migration_painting",
|
||||
"prompt":_Q+_HT+"painterly illustration of migrating bird flock where formation creates "
|
||||
"concealed PATRICK letters, dramatic sky with volumetric clouds, rich oil painting texture, "
|
||||
"romantic naturalist style","width":1024,"height":1024,"steps":20},
|
||||
]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────
|
||||
# Pipeline helpers (ported from cannamanage_gen.py)
|
||||
# ─────────────────────────────────────────────────────────
|
||||
|
||||
def load_progress() -> Dict[str, Any]:
|
||||
if PROGRESS_FILE.exists():
|
||||
try:
|
||||
with open(PROGRESS_FILE) as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
return {"completed": [], "failed": [], "started_at": time.strftime("%Y-%m-%dT%H:%M:%S")}
|
||||
|
||||
|
||||
def save_progress(progress: Dict[str, Any]) -> None:
|
||||
OUTPUT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
with open(PROGRESS_FILE, "w") as f:
|
||||
json.dump(progress, f, indent=2)
|
||||
|
||||
|
||||
def load_workflow(model: str) -> Dict:
|
||||
path = WORKFLOW_HERETIC if model == "heretic" else WORKFLOW_SCHNELL
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def submit_prompt(comfyui_url: str, workflow: Dict) -> str:
|
||||
data = json.dumps({"prompt": workflow}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{comfyui_url}/prompt", data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())["prompt_id"]
|
||||
|
||||
|
||||
def wait_for_image(comfyui_url: str, prompt_id: str, timeout: int = 300) -> Optional[Dict]:
|
||||
print(" ⏳ Waiting for ComfyUI...", end="", flush=True)
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
with urllib.request.urlopen(f"{comfyui_url}/history/{prompt_id}") as resp:
|
||||
history = json.loads(resp.read())
|
||||
if prompt_id in history:
|
||||
print(" done.", flush=True)
|
||||
outputs = history[prompt_id].get("outputs", {})
|
||||
for node_out in outputs.values():
|
||||
if "images" in node_out:
|
||||
return node_out["images"][0]
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
print(".", end="", flush=True)
|
||||
time.sleep(2)
|
||||
print(" timeout!", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
def download_image(comfyui_url: str, image_info: Dict, output_path: Path) -> bool:
|
||||
try:
|
||||
url = (
|
||||
f"{comfyui_url}/view"
|
||||
f"?filename={image_info['filename']}"
|
||||
f"&subfolder={image_info.get('subfolder', '')}"
|
||||
f"&type=output"
|
||||
)
|
||||
with urllib.request.urlopen(url) as resp:
|
||||
img_data = resp.read()
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_bytes(img_data)
|
||||
print(f" ✅ Saved: {output_path} ({len(img_data) // 1024}KB)")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ❌ Download failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def patch_workflow(workflow: Dict, asset: Dict, model: str) -> Dict:
|
||||
wf = copy.deepcopy(workflow)
|
||||
seed = random.randint(0, 2**32 - 1)
|
||||
if model == "heretic":
|
||||
wf["2"]["inputs"]["text"] = asset["prompt"]
|
||||
wf["3"]["inputs"]["text"] = "plain text letters, obvious text overlay, watermark, low quality"
|
||||
wf["6"]["inputs"]["width"] = asset["width"]
|
||||
wf["6"]["inputs"]["height"] = asset["height"]
|
||||
wf["7"]["inputs"]["steps"] = asset["steps"]
|
||||
wf["13"]["inputs"]["filename_prefix"] = asset["name"]
|
||||
if "10" in wf:
|
||||
wf["10"]["inputs"]["noise_seed"] = seed
|
||||
else:
|
||||
# flux_schnell.json: node 6=pos, 33=neg, 27=latent(w/h), 13=ksampler(steps/seed), 9=save
|
||||
wf["6"]["inputs"]["text"] = asset["prompt"]
|
||||
wf["33"]["inputs"]["text"] = "plain text letters, obvious text overlay, watermark, low quality"
|
||||
wf["27"]["inputs"]["width"] = asset["width"]
|
||||
wf["27"]["inputs"]["height"] = asset["height"]
|
||||
wf["13"]["inputs"]["steps"] = asset["steps"]
|
||||
wf["13"]["inputs"]["seed"] = seed
|
||||
wf["9"]["inputs"]["filename_prefix"] = asset["name"]
|
||||
return wf
|
||||
|
||||
|
||||
def generate_asset(comfyui_url: str, asset: Dict, model: str, progress: Dict) -> bool:
|
||||
if asset["id"] in progress["completed"]:
|
||||
print(f" ⏭️ Skipping already completed: {asset['name']}")
|
||||
return True
|
||||
|
||||
print(f"\n Prompt : {asset['prompt'][:100]}...")
|
||||
print(f" Size : {asset['width']}×{asset['height']} Steps: {asset['steps']}")
|
||||
|
||||
try:
|
||||
workflow = load_workflow(model)
|
||||
workflow = patch_workflow(workflow, asset, model)
|
||||
prompt_id = submit_prompt(comfyui_url, workflow)
|
||||
image_info = wait_for_image(comfyui_url, prompt_id)
|
||||
|
||||
if not image_info:
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
output_dir = OUTPUT_ROOT / asset["theme"] / asset["style"]
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / f"{asset['name']}.png"
|
||||
|
||||
if download_image(comfyui_url, image_info, output_path):
|
||||
progress["completed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return True
|
||||
else:
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error: {e}")
|
||||
progress["failed"].append(asset["id"])
|
||||
save_progress(progress)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Patrick Hidden Name Artwork Generation Pipeline"
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true",
|
||||
help="Print manifest without generating")
|
||||
parser.add_argument("--model", choices=["schnell", "heretic"], default="schnell",
|
||||
help="Model: schnell (~10s/img) or heretic (~52s/img, higher quality)")
|
||||
parser.add_argument("--comfyui", default="http://localhost:8188",
|
||||
help="ComfyUI URL")
|
||||
args = parser.parse_args()
|
||||
|
||||
comfyui_url = args.comfyui
|
||||
|
||||
print("🚀 Patrick Hidden Name Artwork Pipeline")
|
||||
print(f" Output : {OUTPUT_ROOT}")
|
||||
print(f" Model : {args.model}")
|
||||
print(f" ComfyUI : {comfyui_url}")
|
||||
print(f" Total : {len(ASSET_MANIFEST)} ultra-detailed hidden-name artworks")
|
||||
print(" Technique: Letters P-A-T-R-I-C-K concealed via organic shapes/negative space")
|
||||
|
||||
if args.dry_run:
|
||||
print()
|
||||
for asset in ASSET_MANIFEST:
|
||||
print(f" {asset['id']:8} | {asset['theme']:15} | {asset['style']:12} | {asset['name']}")
|
||||
total_est = sum(a["steps"] * 2.5 for a in ASSET_MANIFEST) / 60
|
||||
print(f"\n ⏱️ Estimated runtime (schnell @4 steps): ~{total_est:.0f} minutes")
|
||||
print("\nDry run complete. Remove --dry-run to begin generation.")
|
||||
return
|
||||
|
||||
progress = load_progress()
|
||||
remaining = [a for a in ASSET_MANIFEST if a["id"] not in progress["completed"]]
|
||||
print(f" Resume : {len(progress['completed'])} completed, "
|
||||
f"{len(progress['failed'])} failed, {len(remaining)} remaining")
|
||||
|
||||
if not remaining:
|
||||
print("\n✅ All assets already complete!")
|
||||
return
|
||||
|
||||
print(f"\nStarting generation... (Ctrl+C to pause — progress is saved)")
|
||||
|
||||
n_done = 0
|
||||
n_fail = 0
|
||||
for i, asset in enumerate(ASSET_MANIFEST, 1):
|
||||
if asset["id"] in progress["completed"]:
|
||||
continue
|
||||
print(f"\n[{asset['theme']}/{asset['style']}] [{i}/{len(ASSET_MANIFEST)}] {asset['name']}")
|
||||
if generate_asset(comfyui_url, asset, args.model, progress):
|
||||
n_done += 1
|
||||
else:
|
||||
n_fail += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("🎉 PIPELINE COMPLETE")
|
||||
print(f" ✅ Completed this run : {n_done}")
|
||||
print(f" ❌ Failed this run : {n_fail}")
|
||||
print(f" 📦 Total completed : {len(progress['completed'])} / {len(ASSET_MANIFEST)}")
|
||||
if progress["failed"]:
|
||||
print(f" Failed IDs: {', '.join(progress['failed'][-10:])}")
|
||||
print(f" Assets saved to: {OUTPUT_ROOT}")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -244,26 +244,49 @@ def build_flux_workflow(
|
||||
"""
|
||||
workflow_path = _WORKFLOW_REGISTRY.get(model, _WORKFLOW_REGISTRY[_DEFAULT_MODEL])
|
||||
|
||||
with open(workflow_path) as f:
|
||||
wf = json.load(f)
|
||||
wf = copy.deepcopy(wf)
|
||||
|
||||
# Load workflow as text first — replace string placeholders
|
||||
raw = workflow_path.read_text()
|
||||
actual_seed = seed if seed != -1 else random.randint(0, 2**32 - 1)
|
||||
|
||||
wf["6"]["inputs"]["text"] = prompt
|
||||
wf["33"]["inputs"]["text"] = neg_prompt
|
||||
wf["27"]["inputs"]["width"] = width
|
||||
wf["27"]["inputs"]["height"] = height
|
||||
wf["13"]["inputs"]["steps"] = steps
|
||||
wf["13"]["inputs"]["seed"] = actual_seed
|
||||
# Node 32 = UNETLoader (flux1-schnell.safetensors is UNet-only, not all-in-one checkpoint)
|
||||
wf["32"]["inputs"]["unet_name"] = model
|
||||
raw = raw.replace('"PROMPT_PLACEHOLDER"', json.dumps(prompt))
|
||||
raw = raw.replace('"NEGATIVE_PLACEHOLDER"', json.dumps(neg_prompt))
|
||||
wf = json.loads(raw)
|
||||
wf = copy.deepcopy(wf)
|
||||
|
||||
# Recursively inject numeric values into matching field names
|
||||
_inject_workflow_params(wf, {
|
||||
"width": width,
|
||||
"height": height,
|
||||
"steps": steps,
|
||||
"seed": actual_seed,
|
||||
"noise_seed": actual_seed,
|
||||
"unet_name": model,
|
||||
})
|
||||
|
||||
# Attach the actual seed as metadata so callers can retrieve it
|
||||
wf["_meta"] = {"actual_seed": actual_seed}
|
||||
return wf
|
||||
|
||||
|
||||
def _inject_workflow_params(node: dict | list, params: dict) -> None:
|
||||
"""Recursively walk a workflow dict/list and inject parameter values.
|
||||
|
||||
For each dict encountered, if it has an "inputs" sub-dict, update
|
||||
any matching field names from params. This is model-agnostic and
|
||||
works regardless of ComfyUI node IDs.
|
||||
"""
|
||||
if isinstance(node, dict):
|
||||
if "inputs" in node and isinstance(node["inputs"], dict):
|
||||
for key, value in params.items():
|
||||
if key in node["inputs"] and not isinstance(node["inputs"][key], list):
|
||||
node["inputs"][key] = value
|
||||
for v in node.values():
|
||||
_inject_workflow_params(v, params)
|
||||
elif isinstance(node, list):
|
||||
for item in node:
|
||||
_inject_workflow_params(item, params)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,73 +1,98 @@
|
||||
{
|
||||
"6": {
|
||||
"1": {
|
||||
"class_type": "CLIPLoader",
|
||||
"inputs": {
|
||||
"clip_name": "qwen_3_4b_klein.safetensors",
|
||||
"type": "flux2",
|
||||
"device": "default"
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"clip": ["30", 0],
|
||||
"clip": ["1", 0],
|
||||
"text": "PROMPT_PLACEHOLDER"
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecode",
|
||||
"3": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {
|
||||
"samples": ["13", 0],
|
||||
"vae": ["31", 0]
|
||||
"clip": ["1", 0],
|
||||
"text": "NEGATIVE_PLACEHOLDER"
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveImage",
|
||||
"4": {
|
||||
"class_type": "UNETLoader",
|
||||
"inputs": {
|
||||
"filename_prefix": "mcp-image-gen",
|
||||
"images": ["8", 0]
|
||||
"unet_name": "flux-2-klein-4b.safetensors",
|
||||
"weight_dtype": "default"
|
||||
}
|
||||
},
|
||||
"13": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"cfg": 1.0,
|
||||
"denoise": 1.0,
|
||||
"latent_image": ["27", 0],
|
||||
"model": ["32", 0],
|
||||
"negative": ["33", 0],
|
||||
"positive": ["6", 0],
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "beta",
|
||||
"seed": 42,
|
||||
"steps": 4
|
||||
}
|
||||
},
|
||||
"27": {
|
||||
"class_type": "EmptySD3LatentImage",
|
||||
"inputs": {
|
||||
"batch_size": 1,
|
||||
"height": 1024,
|
||||
"width": 1024
|
||||
}
|
||||
},
|
||||
"30": {
|
||||
"class_type": "CLIPLoader",
|
||||
"inputs": {
|
||||
"clip_name": "qwen_3_4b_heretic.safetensors",
|
||||
"type": "flux"
|
||||
}
|
||||
},
|
||||
"31": {
|
||||
"5": {
|
||||
"class_type": "VAELoader",
|
||||
"inputs": {
|
||||
"vae_name": "flux2-vae.safetensors"
|
||||
}
|
||||
},
|
||||
"32": {
|
||||
"class_type": "UNETLoader",
|
||||
"6": {
|
||||
"class_type": "EmptyFlux2LatentImage",
|
||||
"inputs": {
|
||||
"unet_name": "flux-2-klein-4b.safetensors",
|
||||
"weight_dtype": "fp8_e4m3fn"
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"batch_size": 1
|
||||
}
|
||||
},
|
||||
"33": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"7": {
|
||||
"class_type": "Flux2Scheduler",
|
||||
"inputs": {
|
||||
"clip": ["30", 0],
|
||||
"text": "NEGATIVE_PLACEHOLDER"
|
||||
"steps": 20,
|
||||
"width": 1024,
|
||||
"height": 1024
|
||||
}
|
||||
},
|
||||
"8": {
|
||||
"class_type": "CFGGuider",
|
||||
"inputs": {
|
||||
"model": ["4", 0],
|
||||
"positive": ["2", 0],
|
||||
"negative": ["3", 0],
|
||||
"cfg": 5
|
||||
}
|
||||
},
|
||||
"9": {
|
||||
"class_type": "KSamplerSelect",
|
||||
"inputs": {
|
||||
"sampler_name": "euler"
|
||||
}
|
||||
},
|
||||
"10": {
|
||||
"class_type": "RandomNoise",
|
||||
"inputs": {
|
||||
"noise_seed": 42
|
||||
}
|
||||
},
|
||||
"11": {
|
||||
"class_type": "SamplerCustomAdvanced",
|
||||
"inputs": {
|
||||
"noise": ["10", 0],
|
||||
"guider": ["8", 0],
|
||||
"sampler": ["9", 0],
|
||||
"sigmas": ["7", 0],
|
||||
"latent_image": ["6", 0]
|
||||
}
|
||||
},
|
||||
"12": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {
|
||||
"samples": ["11", 0],
|
||||
"vae": ["5", 0]
|
||||
}
|
||||
},
|
||||
"13": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {
|
||||
"filename_prefix": "mcp-image-gen",
|
||||
"images": ["12", 0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,20 @@ def test_build_flux_workflow_heretic_model():
|
||||
seed=42,
|
||||
model="flux-2-klein-4b.safetensors",
|
||||
)
|
||||
assert wf["6"]["class_type"] == "CLIPTextEncode"
|
||||
assert wf["30"]["class_type"] == "CLIPLoader" # Qwen3-4B uses single CLIPLoader
|
||||
assert wf["32"]["inputs"]["unet_name"] == "flux-2-klein-4b.safetensors"
|
||||
assert wf["31"]["inputs"]["vae_name"] == "flux2-vae.safetensors"
|
||||
assert wf["13"]["inputs"]["scheduler"] == "beta" # FLUX.2 Klein uses beta scheduler
|
||||
# New FLUX.2 workflow uses different node IDs and types
|
||||
assert wf["1"]["class_type"] == "CLIPLoader" # Qwen3-4B uses single CLIPLoader
|
||||
assert wf["1"]["inputs"]["type"] == "flux2" # correct type for FLUX.2
|
||||
assert wf["1"]["inputs"]["device"] == "default" # required for FLUX.2 CLIPLoader
|
||||
assert wf["1"]["inputs"]["clip_name"] == "qwen_3_4b_klein.safetensors" # Comfy-Org/vae-text-encorder-for-flux-klein-4b
|
||||
assert wf["2"]["class_type"] == "CLIPTextEncode" # standard CLIP encode (not Flux-specific)
|
||||
assert wf["4"]["class_type"] == "UNETLoader"
|
||||
assert wf["4"]["inputs"]["unet_name"] == "flux-2-klein-4b.safetensors"
|
||||
assert wf["4"]["inputs"]["weight_dtype"] == "default" # not fp8 — avoids dimension errors
|
||||
assert wf["6"]["class_type"] == "EmptyFlux2LatentImage" # FLUX.2-specific latent
|
||||
assert wf["8"]["class_type"] == "CFGGuider" # CFGGuider replaces FluxDisableGuidance+BasicGuider
|
||||
assert wf["8"]["inputs"]["cfg"] == 5 # cfg=5 for FLUX.2 Klein
|
||||
assert wf["11"]["class_type"] == "SamplerCustomAdvanced" # FLUX.2 sampler (node 11, not 12)
|
||||
assert wf["13"]["class_type"] == "SaveImage" # output node
|
||||
|
||||
|
||||
def test_workflow_registry_contains_both_models():
|
||||
|
||||
@@ -4,13 +4,14 @@ import httpx
|
||||
from bs4 import BeautifulSoup
|
||||
from html2text import html2text
|
||||
from urllib.parse import urljoin, quote_plus
|
||||
from typing import List, Dict, Tuple
|
||||
from typing import List, Dict, Tuple, Annotated
|
||||
import re
|
||||
import ssl
|
||||
import os
|
||||
import certifi
|
||||
from pathlib import Path
|
||||
from fastmcp import FastMCP
|
||||
from pydantic import Field
|
||||
|
||||
mcp = FastMCP("webscraper")
|
||||
|
||||
@@ -54,13 +55,9 @@ def filter_junk_links(href: str) -> bool:
|
||||
return not any(re.match(pattern, href.lower()) for pattern in junk_patterns)
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_fetch(url: str, max_chars: int = 5000) -> str:
|
||||
def webscraper_fetch(url: Annotated[str, Field(description="The URL to fetch")], max_chars: Annotated[int, Field(description="Maximum characters in the markdown body (default: 5000)")] = 5000) -> str:
|
||||
"""Fetch a URL and return title + markdown body + metadata.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
max_chars: Maximum characters in the markdown body (default: 5000)
|
||||
|
||||
|
||||
Returns:
|
||||
Markdown string with title, body, and metadata
|
||||
"""
|
||||
@@ -78,13 +75,9 @@ def webscraper_fetch(url: str, max_chars: int = 5000) -> str:
|
||||
return f"# Error fetching {url}\n\n{str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_fetch_links(url: str, deduplicate: bool = True) -> List[str]:
|
||||
def webscraper_fetch_links(url: Annotated[str, Field(description="The URL to fetch")], deduplicate: Annotated[bool, Field(description="Remove duplicate links (default: True)")] = True) -> List[str]:
|
||||
"""Fetch a URL and extract all href links.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
deduplicate: Remove duplicate links (default: True)
|
||||
|
||||
|
||||
Returns:
|
||||
List of unique href URLs
|
||||
"""
|
||||
@@ -105,12 +98,9 @@ def webscraper_fetch_links(url: str, deduplicate: bool = True) -> List[str]:
|
||||
return [f"Error: {str(e)}"]
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_fetch_tables(url: str) -> List[str]:
|
||||
def webscraper_fetch_tables(url: Annotated[str, Field(description="The URL to fetch")]) -> List[str]:
|
||||
"""Fetch a URL and extract all HTML tables as markdown.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
|
||||
|
||||
Returns:
|
||||
List of markdown tables
|
||||
"""
|
||||
@@ -125,13 +115,9 @@ def webscraper_fetch_tables(url: str) -> List[str]:
|
||||
return [f"Error: {str(e)}"]
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_fetch_all(url: str, max_chars: int = 5000) -> Dict:
|
||||
def webscraper_fetch_all(url: Annotated[str, Field(description="The URL to fetch")], max_chars: Annotated[int, Field(description="Maximum characters (default: 5000)")] = 5000) -> Dict:
|
||||
"""Fetch everything: markdown + links + tables + meta.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
max_chars: Maximum characters (default: 5000)
|
||||
|
||||
|
||||
Returns:
|
||||
Dict with 'markdown', 'links', 'tables', 'meta'
|
||||
"""
|
||||
@@ -181,13 +167,9 @@ def webscraper_fetch_all(url: str, max_chars: int = 5000) -> Dict:
|
||||
return {"error": str(e)}
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_fetch_section(url: str, selector: str) -> str:
|
||||
def webscraper_fetch_section(url: Annotated[str, Field(description="The URL to fetch")], selector: Annotated[str, Field(description="CSS selector (e.g., '.content')")]) -> str:
|
||||
"""Fetch a URL and extract specific section by CSS selector.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
selector: CSS selector (e.g., '.content')
|
||||
|
||||
|
||||
Returns:
|
||||
Markdown of the selected section
|
||||
"""
|
||||
@@ -210,12 +192,9 @@ def webscraper_fetch_section(url: str, selector: str) -> str:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_fetch_meta(url: str) -> Dict[str, str]:
|
||||
def webscraper_fetch_meta(url: Annotated[str, Field(description="The URL to fetch")]) -> Dict[str, str]:
|
||||
"""Fetch a URL and return page metadata: title, description, OG tags.
|
||||
|
||||
Args:
|
||||
url: The URL to fetch
|
||||
|
||||
|
||||
Returns:
|
||||
Dict of metadata
|
||||
"""
|
||||
@@ -238,13 +217,9 @@ def webscraper_fetch_meta(url: str) -> Dict[str, str]:
|
||||
return {"error": str(e)}
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_fetch_sitemap(url: str, max_urls: int = 100) -> List[str]:
|
||||
def webscraper_fetch_sitemap(url: Annotated[str, Field(description="Sitemap URL (or auto-discover)")], max_urls: Annotated[int, Field(description="Maximum URLs to return (default: 100)")] = 100) -> List[str]:
|
||||
"""Fetch sitemap.xml and return list of URLs.
|
||||
|
||||
Args:
|
||||
url: Sitemap URL (or auto-discover)
|
||||
max_urls: Maximum URLs to return (default: 100)
|
||||
|
||||
|
||||
Returns:
|
||||
List of sitemap URLs
|
||||
"""
|
||||
@@ -263,17 +238,13 @@ def webscraper_fetch_sitemap(url: str, max_urls: int = 100) -> List[str]:
|
||||
return [f"Error: {str(e)}"]
|
||||
|
||||
@mcp.tool()
|
||||
def webscraper_search_hint(query: str, max_results: int = 5) -> Dict:
|
||||
def webscraper_search_hint(query: Annotated[str, Field(description="Search query (e.g. \"MacBook Pro M4 price Germany\")")], max_results: Annotated[int, Field(description="Maximum number of results to return (default: 5)")] = 5) -> Dict:
|
||||
"""Search Brave Search and return top results as a scraping hint.
|
||||
|
||||
Use this sparingly — once per research task — to get oriented before
|
||||
scraping individual pages. Returns top result URLs + snippets so you
|
||||
can decide which pages are worth scraping deeply.
|
||||
|
||||
Args:
|
||||
query: Search query (e.g. "MacBook Pro M4 price Germany")
|
||||
max_results: Maximum number of results to return (default: 5)
|
||||
|
||||
Returns:
|
||||
Dict with 'query', 'search_url', 'results' (list of {title, url, snippet}),
|
||||
'result_count', 'hint'
|
||||
@@ -285,14 +256,23 @@ def webscraper_search_hint(query: str, max_results: int = 5) -> Dict:
|
||||
results = []
|
||||
seen_urls: set = set()
|
||||
|
||||
# Brave Search result cards: each div.snippet contains title, URL, description
|
||||
# Brave Search result cards: each div.snippet with a .result-wrapper is a web result.
|
||||
# Skip video clusters, FAQ blocks, and LLM snippets (they have no .result-wrapper).
|
||||
# Class names as of 2026-04 (updated from .snippet-title / .snippet-description):
|
||||
# title → .search-snippet-title
|
||||
# url → a.l1 (the primary result anchor, avoids favicon <a> tags)
|
||||
# snippet → .content.t-primary
|
||||
for card in soup.select('.snippet'):
|
||||
if len(results) >= max_results:
|
||||
break
|
||||
|
||||
title_el = card.select_one('.snippet-title')
|
||||
url_el = card.select_one('a')
|
||||
desc_el = card.select_one('.snippet-description')
|
||||
# Skip non-web-result snippets (videos, FAQ, LLM answer blocks)
|
||||
if not card.select_one('.result-wrapper'):
|
||||
continue
|
||||
|
||||
title_el = card.select_one('.search-snippet-title')
|
||||
url_el = card.select_one('a.l1')
|
||||
desc_el = card.select_one('.content.t-primary')
|
||||
|
||||
title = title_el.get_text(strip=True) if title_el else ""
|
||||
url = url_el['href'] if url_el and url_el.get('href') else ""
|
||||
|
||||
@@ -206,27 +206,39 @@ def test_sitemap_max_urls(mock_get, mock_sitemap_response):
|
||||
|
||||
# --- webscraper_search_hint tests ---
|
||||
|
||||
# Helper to build a Brave-style result card with the new 2026-04 class names.
|
||||
# Real result cards have a .result-wrapper; non-result blocks (videos, FAQ) do not.
|
||||
def _brave_card(href: str, title: str, snippet: str) -> str:
|
||||
"""Return a mock Brave Search .snippet card with .result-wrapper (web result)."""
|
||||
return f"""
|
||||
<div class="snippet svelte-jmfu5f">
|
||||
<div class="result-wrapper svelte-1rq4ngz">
|
||||
<div class="result-content svelte-1rq4ngz">
|
||||
<a class="l1 svelte-14r20fy" href="{href}">
|
||||
<div class="search-snippet-title line-clamp-1 svelte-14r20fy">{title}</div>
|
||||
</a>
|
||||
<div class="generic-snippet svelte-1cwdgg3">
|
||||
<div class="content desktop-default-regular t-primary line-clamp-dynamic svelte-1cwdgg3">{snippet}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_brave_response():
|
||||
"""Mock Brave Search HTML response with result cards."""
|
||||
"""Mock Brave Search HTML response with result cards (2026-04 class names)."""
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.text = """
|
||||
<html><body>
|
||||
<div class="snippet">
|
||||
<a href="https://example.com/article1" class="snippet-title">Feynman on Electric Fields</a>
|
||||
<div class="snippet-title">Feynman on Electric Fields</div>
|
||||
<div class="snippet-description">Richard Feynman explains that all matter has an electric field.</div>
|
||||
</div>
|
||||
<div class="snippet">
|
||||
<a href="https://example.com/article2" class="snippet-title">Electric Fields Everywhere</a>
|
||||
<div class="snippet-title">Electric Fields Everywhere</div>
|
||||
<div class="snippet-description">Everything in the universe is surrounded by electric fields.</div>
|
||||
</div>
|
||||
<div class="snippet">
|
||||
<a href="javascript:void(0)" class="snippet-title">JS Junk</a>
|
||||
<div class="snippet-title">JS Junk</div>
|
||||
<div class="snippet-description">Should be filtered out.</div>
|
||||
<html><body id="results">
|
||||
""" + _brave_card("https://example.com/article1", "Feynman on Electric Fields",
|
||||
"Richard Feynman explains that all matter has an electric field.") + """
|
||||
""" + _brave_card("https://example.com/article2", "Electric Fields Everywhere",
|
||||
"Everything in the universe is surrounded by electric fields.") + """
|
||||
<!-- Non-result block (no .result-wrapper) — should be skipped -->
|
||||
<div class="snippet svelte-jmfu5f standalone" id="faq">
|
||||
<header class="desktop-heading-h4">FAQ</header>
|
||||
</div>
|
||||
</body></html>
|
||||
"""
|
||||
@@ -240,22 +252,10 @@ def mock_brave_response_dups():
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.text = """
|
||||
<html><body>
|
||||
<div class="snippet">
|
||||
<a href="https://example.com/dup">Dup Result A</a>
|
||||
<div class="snippet-title">Dup Result A</div>
|
||||
<div class="snippet-description">First occurrence.</div>
|
||||
</div>
|
||||
<div class="snippet">
|
||||
<a href="https://example.com/dup">Dup Result B</a>
|
||||
<div class="snippet-title">Dup Result B</div>
|
||||
<div class="snippet-description">Second occurrence — same URL.</div>
|
||||
</div>
|
||||
<div class="snippet">
|
||||
<a href="https://example.com/unique">Unique Result</a>
|
||||
<div class="snippet-title">Unique Result</div>
|
||||
<div class="snippet-description">Only once.</div>
|
||||
</div>
|
||||
<html><body id="results">
|
||||
""" + _brave_card("https://example.com/dup", "Dup Result A", "First occurrence.") + """
|
||||
""" + _brave_card("https://example.com/dup", "Dup Result B", "Second occurrence — same URL.") + """
|
||||
""" + _brave_card("https://example.com/unique", "Unique Result", "Only once.") + """
|
||||
</body></html>
|
||||
"""
|
||||
mock_resp.headers = {"content-type": "text/html"}
|
||||
@@ -268,17 +268,9 @@ def mock_brave_response_empty_content():
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.text = """
|
||||
<html><body>
|
||||
<div class="snippet">
|
||||
<a href="https://example.com/ghost"></a>
|
||||
<div class="snippet-title"></div>
|
||||
<div class="snippet-description"></div>
|
||||
</div>
|
||||
<div class="snippet">
|
||||
<a href="https://example.com/real">Real Result</a>
|
||||
<div class="snippet-title">Real Result</div>
|
||||
<div class="snippet-description">Has content.</div>
|
||||
</div>
|
||||
<html><body id="results">
|
||||
""" + _brave_card("https://example.com/ghost", "", "") + """
|
||||
""" + _brave_card("https://example.com/real", "Real Result", "Has content.") + """
|
||||
</body></html>
|
||||
"""
|
||||
mock_resp.headers = {"content-type": "text/html"}
|
||||
|
||||
Submodule
+1
Submodule odysseus added at 463713c2c6
@@ -0,0 +1,133 @@
|
||||
# Homelab Session Handover
|
||||
_Last updated: 2026-06-11 by Lumen_
|
||||
|
||||
## 🔑 SSH Access (no password needed)
|
||||
|
||||
```bash
|
||||
ssh-add ~/.ssh/id_ed25519_homelab
|
||||
```
|
||||
|
||||
| Alias | Host | User | What it is |
|
||||
|-------|------|------|-----------|
|
||||
| `ssh vps` | 85.214.154.199 | root | plate.software — Strato OpenVZ, Plesk, Apache |
|
||||
| `ssh ionos` | 82.165.206.45 | root | plate-software.de — IONOS, Ubuntu 18.04, Apache |
|
||||
| `ssh truenas` | 192.168.188.119 | root | TrueNAS SCALE 24.10.2.4, k3s, Gitea |
|
||||
|
||||
---
|
||||
|
||||
## ✅ Fully Done
|
||||
|
||||
### plate.software (VPS — 85.214.154.199)
|
||||
- Let's Encrypt cert valid (ACME path fixed in Plesk HTTP directives)
|
||||
- `frps` v0.68.1 running systemd, port 7000, token in BigMind fact #188
|
||||
- `git.plate.software` Apache proxy → `localhost:30008` via frpc ✅ HTTP 200
|
||||
- `frpc.service` on TrueNAS tunneling port 30008 → VPS
|
||||
|
||||
### IONOS (plate-software.de — 82.165.206.45)
|
||||
- SSL wildcard-like cert renewed via acme.sh — now covers `git.plate-software.de` too
|
||||
- Valid until ~2026-08-04
|
||||
- ownCloud, Collabora still running
|
||||
|
||||
### TrueNAS — ChunkyTown ZFS Pool (rebuilt 2026-05-04)
|
||||
- New pool: RAIDZ1 on `sda`+`sdb`+`sdd`+`sdl` (3 Toshibas + new Seagate WWZAXXKL)
|
||||
- Hot spare: `sdk` (oldest Toshiba 3220A0PBFA3H)
|
||||
- **29.1TB usable**, ONLINE, 0 errors
|
||||
- Old pool was unrecoverable (2 simultaneous failures)
|
||||
- Data was acceptable loss (Plex re-downloadable, photos in Google Photos)
|
||||
|
||||
### TrueNAS — frpc tunnel
|
||||
- Binary: `/mnt/VM_SSD_Pool/frp/frpc`
|
||||
- Config: `/mnt/VM_SSD_Pool/frp/frpc.toml`
|
||||
- Systemd: `frpc.service` (enabled, running)
|
||||
- Gitea `app.ini`: `/mnt/VM_SSD_Pool/VM_POOL1/gitea/config/app.ini`
|
||||
- `ROOT_URL = https://git.plate.software/`
|
||||
- `SSH_DOMAIN = git.plate.software`
|
||||
|
||||
### git.plate.software ✅ LIVE
|
||||
- `curl https://git.plate.software/` → HTTP 200
|
||||
|
||||
---
|
||||
|
||||
## ✅ IONOS Gitea Mirror — FIXED 2026-06-11
|
||||
|
||||
### Status: FULLY WORKING
|
||||
- `https://git.plate-software.de/` → HTTP 200 ✅
|
||||
- Gitea API → HTTP 200 ✅
|
||||
- Push mirrors syncing: `pplate/bigmind`, `pplate/cannamanage`, `pplate/pi_mcps` ✅
|
||||
|
||||
### What's running
|
||||
- Gitea Docker container on IONOS: `docker ps | grep gitea-mirror`
|
||||
- Port: `127.0.0.1:3000` (local only, behind Apache)
|
||||
- Data: `/opt/gitea/data`
|
||||
- Admin user: `pplate` (password: `HomelabGit2026!` — reset 2026-06-11)
|
||||
- API token: `1e87f855d448727e9d213599d654542881bdca0f`
|
||||
|
||||
### Root cause (fixed)
|
||||
The `sites-enabled/` files for collabora, owncloud, and ssl.conf were **stale copies** (not symlinks) still using hostname-specific VirtualHost bindings (`collabora.plate-software.de:443`, `owncloud.plate-software.de:443`, `plate-software.de:443`). These resolved to `82.165.206.45:443` and Apache treated that as a separate higher-priority NameVirtualHost group — intercepting all git smart HTTP requests before the `*:443` git vhost was ever consulted.
|
||||
|
||||
**Fix applied 2026-06-11:**
|
||||
```bash
|
||||
sed -i "s|VirtualHost collabora.plate-software.de:443|VirtualHost *:443|g" /etc/apache2/sites-enabled/collabora.plate-software.de.conf
|
||||
sed -i "s|VirtualHost collabora.plate-software.de:80|VirtualHost *:80|g" /etc/apache2/sites-enabled/collabora.plate-software.de.conf
|
||||
sed -i "s|VirtualHost owncloud.plate-software.de:443|VirtualHost *:443|g" /etc/apache2/sites-enabled/owncloud.plate-software.de.conf
|
||||
sed -i "s|VirtualHost owncloud.plate-software.de:80|VirtualHost *:80|g" /etc/apache2/sites-enabled/owncloud.plate-software.de.conf
|
||||
sed -i "s|VirtualHost plate-software.de:443|VirtualHost *:443|g" /etc/apache2/sites-enabled/ssl.conf
|
||||
systemctl reload apache2
|
||||
```
|
||||
|
||||
⚠️ **Note:** `sites-enabled/collabora`, `owncloud`, and `ssl.conf` are plain files (not symlinks to `sites-available/`). If Apache is ever reconfigured via `a2ensite`, these edits will be lost — the `sites-available/` originals still have the correct `*:443` bindings.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Other Pending Items
|
||||
|
||||
### Plex (superplex app)
|
||||
- Shows CRASHED in TrueNAS app panel
|
||||
- Likely due to old ChunkyTown dataset paths being gone
|
||||
- Fix: TrueNAS web UI → Apps → superplex → Edit → update media library paths to new `/mnt/ChunkyTown/...` datasets
|
||||
|
||||
### Let's Encrypt for git.plate.software (VPS side)
|
||||
- Currently no SSL cert for `git.plate.software` in Plesk
|
||||
- Apache proxy works but is HTTP→HTTP (Plesk's SSL termination handles it)
|
||||
- Issue cert: Plesk UI → Domains → git.plate.software → Let's Encrypt
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Infrastructure Overview
|
||||
|
||||
```
|
||||
Internet
|
||||
↓ DNS
|
||||
plate.software VPS (85.214.154.199)
|
||||
Apache/Plesk
|
||||
├── plate.software → :8080 (Docker WildFly)
|
||||
└── git.plate.software → :30008 (frp tunnel ← TrueNAS) ✅
|
||||
frps :7000 ← frpc on TrueNAS ✅
|
||||
|
||||
TrueNAS.local (192.168.188.119)
|
||||
├── Gitea :30008 (ROOT_URL = https://git.plate.software/) ✅
|
||||
├── VM_SSD_Pool (ZFS RAIDZ2, ONLINE) — Gitea data lives here
|
||||
└── ChunkyTown (ZFS RAIDZ1, ONLINE, 29.1TB) — rebuilt 2026-05-04
|
||||
├── raidz1: sda + sdb + sdd + sdl (Seagate)
|
||||
└── spare: sdk
|
||||
|
||||
IONOS (82.165.206.45)
|
||||
Apache
|
||||
├── owncloud.plate-software.de → :8080 ✅
|
||||
├── collabora.plate-software.de → :9980 ✅
|
||||
└── git.plate-software.de → :3000 (Gitea mirror Docker) ✅ FULLY WORKING (fixed 2026-06-11)
|
||||
Docker: gitea-mirror, data: /opt/gitea/data
|
||||
Token: 1e87f855d448727e9d213599d654542881bdca0f (in BigMind fact #192)
|
||||
Repos: pplate/bigmind, pplate/cannamanage, pplate/pi_mcps (push mirrors from TrueNAS)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Key File Locations
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `~/.ssh/id_ed25519_homelab` | Automation SSH key |
|
||||
| `~/.ssh/config` | SSH aliases (vps, ionos, truenas) |
|
||||
| `plans/frpc-truenas-deploy.sh` | frpc installer (already run on TrueNAS) |
|
||||
| `plans/HOMELAB-HANDOVER.md` | This file |
|
||||
@@ -0,0 +1,149 @@
|
||||
# BigMind Session Loop — Root Cause & Fix Plan
|
||||
|
||||
**Date:** 2026-04-10
|
||||
**Reported by:** Patrick
|
||||
**Severity:** High — caused 6 identical wasted sessions with $0+ API cost per loop
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
BigMind's session ritual, combined with mode-specific behavior rules, creates a self-reinforcing
|
||||
resumption loop when a session ends as `partial`. The model loads prior context, sees an incomplete
|
||||
task, and autonomously attempts to resume it — without ever waiting for user input. This produces
|
||||
a chain of identical `partial` sessions that only breaks when Patrick manually intervenes.
|
||||
|
||||
Observed: 6 identical sessions titled *"Prepared large-scale CannaManage branding generation"*,
|
||||
all `partial`, all spawned from one session ending before image generation completed in pic-gen mode.
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### Loop Trigger Chain
|
||||
|
||||
```
|
||||
[Session N] ends partial (task: CannaManage branding generation)
|
||||
│
|
||||
▼
|
||||
[Session N+1] memory_start_session() → loads context
|
||||
│
|
||||
│ Context shows: last outcome = partial
|
||||
│ Rule 1: "search before every task, avoid redundant work"
|
||||
│ → model reads: "prior task incomplete, I must finish it"
|
||||
│
|
||||
▼
|
||||
memory_announce_focus() called with prior session's task
|
||||
│ → locks in wrong objective BEFORE user speaks
|
||||
│
|
||||
▼
|
||||
Mode rules (pic-gen) fire: "generate images now"
|
||||
│ → autonomous action without user instruction
|
||||
│
|
||||
▼
|
||||
Hits context/token/tool limit → session ends partial
|
||||
│
|
||||
└──────────────────────────────────────────► REPEAT
|
||||
```
|
||||
|
||||
### Three Compounding Failures
|
||||
|
||||
#### Failure 1: Rule 1 — No "partial = history only" clause
|
||||
Rule 1 says to load context and search for prior work. It has **no explicit instruction**
|
||||
that sessions marked `partial` are historical records, NOT resumption requests.
|
||||
The model's default behavior is to treat incomplete work as a pending obligation.
|
||||
|
||||
#### Failure 2: memory_announce_focus — Called on prior context, not current task
|
||||
The architect rules say to call `memory_announce_focus()` as part of the startup ritual.
|
||||
But when no user message has been received yet, the model has nothing to announce except
|
||||
the prior session's objective — which is the wrong task for the new session.
|
||||
|
||||
#### Failure 3: Mode interaction amplification
|
||||
Modes with strong "do the task" personalities (pic-gen, code) compound the loop. When
|
||||
context suggests "there's pending image generation work", pic-gen mode's instructions
|
||||
say to start generating — creating autonomous action before the user speaks.
|
||||
|
||||
---
|
||||
|
||||
## Fix Design
|
||||
|
||||
### Fix 1: Rule 1 Addendum — Partial Sessions Are History
|
||||
|
||||
Add explicit text to Rule 1 in `01-bigmind-core.md`:
|
||||
|
||||
> **`partial`, `blocked`, or `abandoned` outcomes are historical records only.**
|
||||
> They do NOT constitute task queues, resumption requests, or pending obligations.
|
||||
> A new session begins fresh. The current session's task is determined solely by
|
||||
> what the user writes in their first message — never by the outcome of a prior session.
|
||||
|
||||
### Fix 2: New Rule 9 — Anti-Loop Guardrail
|
||||
|
||||
Add Rule 9 to `01-bigmind-core.md`:
|
||||
|
||||
> **Rule 9: Detect and Break Loops Before They Start**
|
||||
>
|
||||
> If `memory_start_session()` context shows 2 or more recently closed sessions with:
|
||||
> - Near-identical headlines or topics, AND
|
||||
> - `partial` or `blocked` outcome
|
||||
>
|
||||
> → **Do NOT attempt to resume the repeated task.**
|
||||
> → Instead: acknowledge the loop to the user, summarize what context was accumulated
|
||||
> across the repeated sessions, and ask: "What would you like to do?"
|
||||
>
|
||||
> Never assume the correct action is to retry a failed/partial task silently.
|
||||
|
||||
### Fix 3: memory_announce_focus — Wait for User Input
|
||||
|
||||
Add a constraint to Rule 3 (announce focus):
|
||||
|
||||
> **`memory_announce_focus()` must reflect the CURRENT session's task.**
|
||||
> Call it only AFTER the user has given a clear instruction for this conversation.
|
||||
> Do NOT announce focus derived from prior session outcomes before the user speaks.
|
||||
> During the startup ritual (steps 1-4 of Rule 1), use a placeholder focus if needed:
|
||||
> `memory_announce_focus(session_id, "Awaiting user task assignment")`
|
||||
|
||||
### Fix 4: Mode Interaction Safety Clause
|
||||
|
||||
Add a universal safety rule (applies to all modes):
|
||||
|
||||
> **Session ritual completion ≠ task authorization.**
|
||||
> Completing `memory_start_session()` + `memory_list_hypotheses()` + `memory_announce_focus()`
|
||||
> does NOT authorize beginning any task. Work begins only when the user explicitly assigns it
|
||||
> in the current conversation. Prior session context is reference material, not instruction.
|
||||
|
||||
---
|
||||
|
||||
## Files to Change
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `.roo/rules/01-bigmind-core.md` | Add Rule 9, add partial=history clause to Rule 1, add focus guard to Rule 3 |
|
||||
| `.roo/rules/00-identity.md` | Add mode-interaction safety clause |
|
||||
|
||||
---
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Likelihood | Mitigation |
|
||||
|------|-----------|------------|
|
||||
| Model ignores new rules in long context | Medium | Rules are loaded via rules files, not context — they apply per-session |
|
||||
| Fix breaks legitimate resumption (e.g., user explicitly asks to continue) | Low | Rules say "task determined by user's first message" — explicit resumption request still works |
|
||||
| New Rule 9 fires falsely on legitimate repeated partial tasks | Low | Trigger requires near-identical headlines AND repeated partial — normal work produces diverse headlines |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Starting a new session after a partial pic-gen session → model waits for user input, no autonomous generation
|
||||
2. Starting a new session after 2+ identical partial sessions → model acknowledges the loop and asks what to do
|
||||
3. User explicitly asking "continue the branding generation" → model correctly resumes (rule only prevents silent resumption)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Patch `.roo/rules/01-bigmind-core.md` — add Rule 9 + partial=history clause + focus guard
|
||||
2. Patch `.roo/rules/00-identity.md` — add mode interaction safety clause
|
||||
3. Test by starting a new session in pic-gen mode with partial history in context
|
||||
4. Push to Gitea
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
# CannaManage — Sprint 1 Implementation Plan
|
||||
|
||||
**Sprint:** 1 — Foundation
|
||||
**Phase:** Phase 1 (Weeks 1–8 of Phase 0 Foundation)
|
||||
**Author:** Lumen (architect mode), 2026-04-10
|
||||
**Status:** Ready for Patrick's approval
|
||||
|
||||
---
|
||||
|
||||
## Sprint Goal
|
||||
|
||||
> **"Get the compliance engine running and fully tested — with zero production code and zero API yet."**
|
||||
|
||||
Sprint 1 produces a compilable, testable Maven multi-module project with:
|
||||
- All core JPA entities modelled
|
||||
- Flyway V1 baseline migration SQL
|
||||
- `ComplianceService` implemented with 100% unit test coverage (TC-001 → TC-010)
|
||||
- A working local dev environment (Docker Compose: PostgreSQL + app)
|
||||
|
||||
No UI, no REST API, no Stripe in Sprint 1. The compliance engine is the legal heart of the product — validate it first.
|
||||
|
||||
---
|
||||
|
||||
## Deliverables
|
||||
|
||||
| # | Deliverable | Definition of Done |
|
||||
|---|------------|-------------------|
|
||||
| D1 | Maven multi-module project scaffold | `./mvnw clean verify` passes with no test failures |
|
||||
| D2 | `cannamanage-domain` module | All 8 JPA entities compile; `AbstractTenantEntity` wired |
|
||||
| D3 | Flyway `V1__initial_schema.sql` | Migration applies cleanly against PostgreSQL 16 |
|
||||
| D4 | `ComplianceService` | All 5 business methods implemented |
|
||||
| D5 | Unit test suite TC-001 → TC-010 | JaCoCo reports 100% line + branch coverage on `ComplianceService` |
|
||||
| D6 | Local dev `docker-compose.yml` | `docker compose up db` starts PostgreSQL; app connects cleanly |
|
||||
|
||||
---
|
||||
|
||||
## 1. Maven Multi-Module Structure
|
||||
|
||||
```
|
||||
cannamanage/ ← root POM (parent)
|
||||
├── pom.xml ← parent POM (BOM: Spring Boot 3.x, Java 21)
|
||||
│
|
||||
├── cannamanage-domain/ ← JPA entities, enums, constants
|
||||
│ └── src/main/java/de/cannamanage/domain/
|
||||
│ ├── entity/ ← JPA entity classes
|
||||
│ ├── enums/ ← MemberStatus, BatchStatus, etc.
|
||||
│ └── constants/
|
||||
│ └── ComplianceConstants.java
|
||||
│
|
||||
├── cannamanage-service/ ← Business logic, services (TESTED HERE)
|
||||
│ └── src/
|
||||
│ ├── main/java/de/cannamanage/service/
|
||||
│ │ ├── ComplianceService.java
|
||||
│ │ ├── dto/ ← QuotaStatus, ComplianceCheckResult, etc.
|
||||
│ │ └── exception/ ← QuotaExceededException, MemberIneligibleException
|
||||
│ └── test/java/de/cannamanage/service/
|
||||
│ └── ComplianceServiceTest.java ← TC-001 to TC-010
|
||||
│
|
||||
├── cannamanage-api/ ← Spring Boot app entry point (REST controllers — Sprint 2)
|
||||
│ └── src/main/java/de/cannamanage/api/
|
||||
│ └── CannaManageApplication.java
|
||||
│
|
||||
└── docker-compose.yml ← Local dev: PostgreSQL 16
|
||||
```
|
||||
|
||||
### Parent POM key dependencies (BOM managed)
|
||||
|
||||
```xml
|
||||
<!-- Spring Boot 3.3.x parent -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.3.4</version>
|
||||
</parent>
|
||||
|
||||
<!-- Modules -->
|
||||
<modules>
|
||||
<module>cannamanage-domain</module>
|
||||
<module>cannamanage-service</module>
|
||||
<module>cannamanage-api</module>
|
||||
</modules>
|
||||
|
||||
<!-- Key managed versions -->
|
||||
<!-- Java 21, Hibernate 6.x (via Spring Boot BOM), Flyway 9.x -->
|
||||
<!-- JJWT 0.12.x (Sprint 2), iText 7 (Sprint 3), Stripe 25.x (Sprint 4) -->
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. `cannamanage-domain` — JPA Entities
|
||||
|
||||
### 2.1 `AbstractTenantEntity` (base class for all entities)
|
||||
|
||||
```java
|
||||
// de.cannamanage.domain.entity.AbstractTenantEntity
|
||||
@MappedSuperclass
|
||||
@FilterDef(
|
||||
name = "tenantFilter",
|
||||
parameters = @ParamDef(name = "tenantId", type = UUID.class)
|
||||
)
|
||||
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
|
||||
public abstract class AbstractTenantEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "tenant_id", nullable = false, updatable = false)
|
||||
private UUID tenantId;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@PrePersist
|
||||
void onCreate() {
|
||||
this.tenantId = TenantContext.getCurrentTenant(); // ThreadLocal
|
||||
this.createdAt = Instant.now();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Entities to implement (Sprint 1)
|
||||
|
||||
| Entity | Key fields | Notes |
|
||||
|--------|-----------|-------|
|
||||
| `Club` | id, name, licenseNumber, maxMembers, status | Root tenant aggregate |
|
||||
| `Member` | id, clubId, firstName, lastName, email, dob, membershipNumber, status, isUnder21 | `isUnder21` derived from DOB |
|
||||
| `Strain` | id, name, thcPercentage, cbdPercentage | Immutable once created |
|
||||
| `Batch` | id, strainId, quantityGrams, harvestDate, batchCode, status, contaminationFlag | status: AVAILABLE → EXHAUSTED / RECALLED |
|
||||
| `Distribution` | id, memberId, batchId, quantityGrams, distributedAt, recordedBy, notes | `@Column(updatable=false)` on all fields — immutable |
|
||||
| `MonthlyQuota` | id, memberId, year, month, totalDistributed, maxAllowed, version | `@Version` for optimistic lock |
|
||||
| `StockMovement` | id, batchId, movementType, quantityGrams, reason, createdAt | Audit journal |
|
||||
| `User` | id, memberId, email, passwordHash, role, lastLogin, active, refreshTokenHash | Login identity |
|
||||
|
||||
### 2.3 `ComplianceConstants.java`
|
||||
|
||||
```java
|
||||
// de.cannamanage.domain.constants.ComplianceConstants
|
||||
public final class ComplianceConstants {
|
||||
|
||||
// CanG §19(2) — adult limits
|
||||
public static final BigDecimal ADULT_DAILY_LIMIT_GRAMS = new BigDecimal("25.0");
|
||||
public static final BigDecimal ADULT_MONTHLY_LIMIT_GRAMS = new BigDecimal("50.0");
|
||||
|
||||
// CanG §19(3) — under-21 limits
|
||||
public static final BigDecimal UNDER21_MONTHLY_LIMIT_GRAMS = new BigDecimal("30.0");
|
||||
|
||||
// CanG §19(4) — under-21 THC cap
|
||||
public static final BigDecimal UNDER21_MAX_THC_PERCENTAGE = new BigDecimal("10.0");
|
||||
|
||||
// Minimum membership age
|
||||
public static final int MINIMUM_MEMBERSHIP_AGE = 18;
|
||||
|
||||
// Under-21 threshold
|
||||
public static final int UNDER21_THRESHOLD_AGE = 21;
|
||||
|
||||
private ComplianceConstants() {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Flyway `V1__initial_schema.sql`
|
||||
|
||||
Location: `cannamanage-api/src/main/resources/db/migration/V1__initial_schema.sql`
|
||||
|
||||
```sql
|
||||
-- Clubs (root of tenant hierarchy)
|
||||
CREATE TABLE clubs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
address TEXT,
|
||||
license_number VARCHAR(100) NOT NULL UNIQUE,
|
||||
max_members INT NOT NULL DEFAULT 500,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Members
|
||||
CREATE TABLE members (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
club_id UUID NOT NULL REFERENCES clubs(id),
|
||||
first_name VARCHAR(100) NOT NULL,
|
||||
last_name VARCHAR(100) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
date_of_birth DATE NOT NULL,
|
||||
membership_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
membership_number VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
|
||||
is_under_21 BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
prevention_officer BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(email, tenant_id),
|
||||
UNIQUE(membership_number, tenant_id)
|
||||
);
|
||||
|
||||
-- Strains
|
||||
CREATE TABLE strains (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
thc_percentage NUMERIC(5,2) NOT NULL,
|
||||
cbd_percentage NUMERIC(5,2) NOT NULL DEFAULT 0.00,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Batches
|
||||
CREATE TABLE batches (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
strain_id UUID NOT NULL REFERENCES strains(id),
|
||||
quantity_grams NUMERIC(10,2) NOT NULL,
|
||||
harvest_date DATE,
|
||||
batch_code VARCHAR(100) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'AVAILABLE',
|
||||
contamination_flag BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(batch_code, tenant_id)
|
||||
);
|
||||
|
||||
-- Distributions (immutable — no UPDATE/DELETE via app)
|
||||
CREATE TABLE distributions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
member_id UUID NOT NULL REFERENCES members(id),
|
||||
batch_id UUID NOT NULL REFERENCES batches(id),
|
||||
quantity_grams NUMERIC(10,2) NOT NULL,
|
||||
distributed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
recorded_by UUID NOT NULL REFERENCES members(id),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Monthly quotas (one row per member per calendar month)
|
||||
CREATE TABLE monthly_quotas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
member_id UUID NOT NULL REFERENCES members(id),
|
||||
year INT NOT NULL,
|
||||
month INT NOT NULL CHECK (month >= 1 AND month <= 12),
|
||||
total_distributed NUMERIC(10,2) NOT NULL DEFAULT 0.00,
|
||||
max_allowed NUMERIC(10,2) NOT NULL,
|
||||
version BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(member_id, year, month)
|
||||
);
|
||||
|
||||
-- Stock movements (audit journal)
|
||||
CREATE TABLE stock_movements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
batch_id UUID NOT NULL REFERENCES batches(id),
|
||||
movement_type VARCHAR(50) NOT NULL, -- IN, OUT, RECALL, ADJUSTMENT
|
||||
quantity_grams NUMERIC(10,2) NOT NULL,
|
||||
reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Users (login identities)
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
member_id UUID REFERENCES members(id),
|
||||
email VARCHAR(255) NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'ROLE_MEMBER',
|
||||
last_login TIMESTAMPTZ,
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
refresh_token_hash VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(email, tenant_id)
|
||||
);
|
||||
|
||||
-- Indexes for common query patterns
|
||||
CREATE INDEX idx_members_club_id ON members(club_id);
|
||||
CREATE INDEX idx_members_tenant_id ON members(tenant_id);
|
||||
CREATE INDEX idx_distributions_member_id ON distributions(member_id);
|
||||
CREATE INDEX idx_distributions_tenant_id ON distributions(tenant_id);
|
||||
CREATE INDEX idx_distributions_distributed_at ON distributions(distributed_at);
|
||||
CREATE INDEX idx_monthly_quotas_member_month ON monthly_quotas(member_id, year, month);
|
||||
CREATE INDEX idx_batches_tenant_status ON batches(tenant_id, status);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. `ComplianceService` — Implementation Spec
|
||||
|
||||
Package: `de.cannamanage.service`
|
||||
|
||||
### 4.1 Dependencies (injected via constructor)
|
||||
|
||||
```java
|
||||
@Service
|
||||
@Transactional
|
||||
public class ComplianceService {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
private final DistributionRepository distributionRepository;
|
||||
private final BatchRepository batchRepository;
|
||||
private final MonthlyQuotaRepository monthlyQuotaRepository;
|
||||
private final StrainRepository strainRepository;
|
||||
|
||||
// constructor injection...
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Method: `checkDistributionAllowed(UUID memberId, UUID batchId, BigDecimal quantityGrams)`
|
||||
|
||||
**Algorithm (sequential checks, fail-fast):**
|
||||
|
||||
```
|
||||
1. Load Member — throw MemberNotFoundException if not found
|
||||
2. CHECK: member.status == ACTIVE → else throw QuotaExceededException(MEMBER_INACTIVE)
|
||||
3. Load Batch → CHECK: batch.status == AVAILABLE → else throw BatchUnavailableException
|
||||
4. Load Strain via batch.strainId
|
||||
5. IF member.isUnder21 AND strain.thcPercentage > UNDER21_MAX_THC_PERCENTAGE
|
||||
→ throw QuotaExceededException(HIGH_THC_RESTRICTED_UNDER_21)
|
||||
6. Calculate todayDistributed = SUM(distributions.quantityGrams WHERE memberId AND date=TODAY)
|
||||
CHECK: todayDistributed + quantityGrams > ADULT_DAILY_LIMIT_GRAMS
|
||||
→ throw QuotaExceededException(QUOTA_EXCEEDED_DAILY)
|
||||
7. Get or create MonthlyQuota for (memberId, currentYear, currentMonth)
|
||||
SET maxAllowed = isUnder21 ? UNDER21_MONTHLY_LIMIT_GRAMS : ADULT_MONTHLY_LIMIT_GRAMS
|
||||
CHECK: quota.totalDistributed + quantityGrams > quota.maxAllowed
|
||||
→ throw QuotaExceededException(QUOTA_EXCEEDED_MONTHLY)
|
||||
8. Return ComplianceCheckResult(allowed=true, remainingDaily, remainingMonthly)
|
||||
```
|
||||
|
||||
### 4.3 `QuotaExceededException` — error codes
|
||||
|
||||
```java
|
||||
public enum QuotaViolationCode {
|
||||
MEMBER_INACTIVE,
|
||||
QUOTA_EXCEEDED_DAILY,
|
||||
QUOTA_EXCEEDED_MONTHLY,
|
||||
HIGH_THC_RESTRICTED_UNDER_21,
|
||||
BATCH_UNAVAILABLE
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 DTOs
|
||||
|
||||
```java
|
||||
// ComplianceCheckResult
|
||||
record ComplianceCheckResult(
|
||||
boolean allowed,
|
||||
BigDecimal remainingDaily,
|
||||
BigDecimal remainingMonthly,
|
||||
boolean isUnder21
|
||||
) {}
|
||||
|
||||
// QuotaStatus
|
||||
record QuotaStatus(
|
||||
BigDecimal totalAllowed,
|
||||
BigDecimal totalUsed,
|
||||
BigDecimal remaining,
|
||||
boolean isUnder21,
|
||||
int year,
|
||||
int month
|
||||
) {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Unit Test Suite (TC-001 → TC-010)
|
||||
|
||||
**Class:** `ComplianceServiceTest` in `cannamanage-service`
|
||||
**Coverage requirement:** 100% line + branch on `ComplianceService`
|
||||
**Tools:** JUnit 5, Mockito 5, AssertJ
|
||||
|
||||
### Test structure
|
||||
|
||||
```java
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ComplianceServiceTest {
|
||||
|
||||
@Mock MemberRepository memberRepository;
|
||||
@Mock DistributionRepository distributionRepository;
|
||||
@Mock BatchRepository batchRepository;
|
||||
@Mock MonthlyQuotaRepository monthlyQuotaRepository;
|
||||
@Mock StrainRepository strainRepository;
|
||||
|
||||
@InjectMocks ComplianceService complianceService;
|
||||
|
||||
// Test fixtures
|
||||
private static final UUID ADULT_MEMBER_ID = UUID.randomUUID();
|
||||
private static final UUID UNDER21_MEMBER_ID = UUID.randomUUID();
|
||||
private static final UUID BATCH_ID = UUID.randomUUID();
|
||||
private static final UUID HIGH_THC_STRAIN_ID = UUID.randomUUID();
|
||||
|
||||
// TC-001: adult at monthly limit → throws QUOTA_EXCEEDED_MONTHLY
|
||||
// TC-002: under-21 at monthly limit → throws QUOTA_EXCEEDED_MONTHLY
|
||||
// TC-003: adult at daily limit → throws QUOTA_EXCEEDED_DAILY
|
||||
// TC-004: under-21 + high THC strain → throws HIGH_THC_RESTRICTED_UNDER_21
|
||||
// TC-005: adult at 49g requesting 2g → throws QUOTA_EXCEEDED_MONTHLY
|
||||
// TC-006: adult at 0g requesting 25g → allowed, remaining=0
|
||||
// TC-007: adult at 24.9g requesting 0.1g → allowed, remainingDaily=0
|
||||
// TC-008: adult at 24.9g requesting 0.2g → throws QUOTA_EXCEEDED_DAILY
|
||||
// TC-009: SUSPENDED member → throws MEMBER_INACTIVE
|
||||
// TC-010: EXPELLED member → throws MEMBER_INACTIVE
|
||||
}
|
||||
```
|
||||
|
||||
### Key mock patterns
|
||||
|
||||
```java
|
||||
// TC-001 example mock setup
|
||||
Member adultMember = new Member();
|
||||
adultMember.setId(ADULT_MEMBER_ID);
|
||||
adultMember.setUnder21(false);
|
||||
adultMember.setStatus(MemberStatus.ACTIVE);
|
||||
|
||||
when(memberRepository.findById(ADULT_MEMBER_ID)).thenReturn(Optional.of(adultMember));
|
||||
|
||||
MonthlyQuota quota = new MonthlyQuota();
|
||||
quota.setTotalDistributed(new BigDecimal("50.0"));
|
||||
quota.setMaxAllowed(ComplianceConstants.ADULT_MONTHLY_LIMIT_GRAMS);
|
||||
when(monthlyQuotaRepository.findByMemberIdAndYearAndMonth(any(), anyInt(), anyInt()))
|
||||
.thenReturn(Optional.of(quota));
|
||||
|
||||
// Assert
|
||||
assertThatThrownBy(() -> complianceService.checkDistributionAllowed(ADULT_MEMBER_ID, BATCH_ID, new BigDecimal("1.0")))
|
||||
.isInstanceOf(QuotaExceededException.class)
|
||||
.extracting("code")
|
||||
.isEqualTo(QuotaViolationCode.QUOTA_EXCEEDED_MONTHLY);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Local Dev Docker Compose
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml (root of cannamanage project)
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
container_name: cannamanage-db-local
|
||||
environment:
|
||||
POSTGRES_DB: cannamanage
|
||||
POSTGRES_USER: cannamanage
|
||||
POSTGRES_PASSWORD: dev_password_change_in_prod
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata_local:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U cannamanage"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
pgdata_local:
|
||||
```
|
||||
|
||||
```properties
|
||||
# cannamanage-api/src/main/resources/application-local.properties
|
||||
spring.datasource.url=jdbc:postgresql://localhost:5432/cannamanage
|
||||
spring.datasource.username=cannamanage
|
||||
spring.datasource.password=dev_password_change_in_prod
|
||||
spring.jpa.hibernate.ddl-auto=validate # Flyway owns schema
|
||||
spring.flyway.enabled=true
|
||||
spring.flyway.locations=classpath:db/migration
|
||||
logging.level.de.cannamanage=DEBUG
|
||||
```
|
||||
|
||||
**Run locally:**
|
||||
```bash
|
||||
git clone http://192.168.188.119:30008/pplate/cannamanage.git
|
||||
cd cannamanage
|
||||
docker compose up db -d
|
||||
./mvnw spring-boot:run -pl cannamanage-api -Dspring.profiles.active=local
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Sprint 1 Gitea Issues (already created: #1–#10)
|
||||
|
||||
Based on the Sprint 1 board at `http://truenas.local:30008/pplate/cannamanage/wiki/Sprint-1-Board`, these map to:
|
||||
|
||||
| Gitea Issue | Sprint 1 Deliverable |
|
||||
|-------------|---------------------|
|
||||
| #1 | Maven multi-module project scaffold |
|
||||
| #2 | `AbstractTenantEntity` + `TenantContext` ThreadLocal |
|
||||
| #3 | All 8 JPA entities in `cannamanage-domain` |
|
||||
| #4 | `ComplianceConstants.java` |
|
||||
| #5 | Flyway `V1__initial_schema.sql` |
|
||||
| #6 | `ComplianceService` implementation |
|
||||
| #7 | Unit tests TC-001 → TC-010 (100% coverage) |
|
||||
| #8 | `docker-compose.yml` local dev |
|
||||
| #9 | `application-local.properties` |
|
||||
| #10 | JaCoCo coverage gate in parent POM |
|
||||
|
||||
---
|
||||
|
||||
## 8. Out of Scope — Sprint 1
|
||||
|
||||
These are **explicitly deferred** to Sprint 2+:
|
||||
|
||||
- REST API controllers (`AuthController`, `MemberController`, `DistributionController`)
|
||||
- Spring Security + JWT filter chain
|
||||
- PrimeFaces JSF frontend
|
||||
- Stripe billing integration
|
||||
- iText 7 PDF reports
|
||||
- Email notifications
|
||||
- Testcontainers integration tests (TC-018 → TC-022)
|
||||
- Hetzner deployment / CI pipeline
|
||||
- `MemberService` (TC-011 → TC-015)
|
||||
|
||||
---
|
||||
|
||||
## 9. Definition of Done — Sprint 1
|
||||
|
||||
- [ ] `./mvnw clean verify` exits 0 on clean checkout
|
||||
- [ ] `./mvnw test -pl cannamanage-service` reports 10/10 tests passing
|
||||
- [ ] JaCoCo report shows `ComplianceService` at 100% line + branch coverage
|
||||
- [ ] `docker compose up db -d` starts PostgreSQL; Flyway V1 migration applies cleanly
|
||||
- [ ] No `TODO` comments in production code paths
|
||||
- [ ] All 8 JPA entities have `@Column(nullable = false)` on required fields
|
||||
- [ ] `ComplianceConstants.java` contains all CanG limits as `public static final BigDecimal`
|
||||
- [ ] `AbstractTenantEntity.tenantId` is `@Column(updatable = false)`
|
||||
- [ ] Code pushed to `http://192.168.188.119:30008/pplate/cannamanage` main branch
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommended Implementation Order
|
||||
|
||||
```
|
||||
Day 1: Root pom.xml + module scaffolds → ./mvnw compile passes
|
||||
Day 2: AbstractTenantEntity + TenantContext + ComplianceConstants
|
||||
Day 3: All 8 JPA entities (compile-time only, no DB yet)
|
||||
Day 4: Flyway V1 SQL + docker-compose.yml → migration applies
|
||||
Day 5: ComplianceService skeleton (method signatures + DTOs)
|
||||
Day 6: TC-001 → TC-005 (the exception/blocking cases)
|
||||
Day 7: TC-006 → TC-010 (boundary + happy path cases)
|
||||
Day 8: JaCoCo gate; clean up; push to Gitea
|
||||
```
|
||||
|
||||
*Assuming ~2–3 hours of evening/weekend coding per day as side project.*
|
||||
|
||||
---
|
||||
|
||||
*Plan created: 2026-04-10 | Sprint start: when Patrick approves | Estimated coding sessions: 8 × 2-3h*
|
||||
@@ -12,17 +12,35 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
### Added
|
||||
|
||||
- Complete project documentation suite (10 documents, ~25,000 words)
|
||||
- System architecture design: 8 JPA entities, Maven multi-module structure, multi-tenancy via shared schema + Hibernate filter
|
||||
- System architecture design: 8 JPA entities, Maven multi-module structure
|
||||
- REST API specification: 7 controllers, 30+ endpoints, full request/response schemas with error codes
|
||||
- Compliance engine design: `ComplianceService` enforcing CanG §§19–22 limits (25g/day, 50g/month adults; 30g/month under-21; ≤10% THC under-21)
|
||||
- `ComplianceConstants.java` design: all legal thresholds as named constants to prevent magic numbers in compliance logic
|
||||
- UI wireframes for 6 screens: Admin Dashboard, Distribution Recording Form, Member List, Member Quota View, Stock Management, Compliance Report
|
||||
- 5 AI-generated UI mockup images (FLUX.1-schnell via ComfyUI, 1024×512)
|
||||
- Test plan with 26 test cases covering ComplianceService (TC-001–010), MemberService (TC-011–015), tenant isolation (TC-016–017), and integration tests (TC-018–026)
|
||||
- Deployment guide for Hetzner VPS: Docker Compose setup, Nginx reverse proxy, SSL with Let's Encrypt, CI/CD via Gitea Actions, database backup strategy
|
||||
- Coding standards: Java 21 conventions, JPA patterns, multi-tenancy rules, immutable distribution records
|
||||
- Flowcharts: distribution flow (5-step), member lifecycle (state machine), billing provisioning flow (Mermaid diagrams)
|
||||
- README with full documentation index, tech stack table, pricing tiers, legal notice
|
||||
- **[2026-04-06]** Staff member management: `ROLE_STAFF` with configurable per-account permission grants (US-026); admin controls which data staff can access (DSGVO least-privilege). 8 defined permissions, 3 pre-created role templates (Ausgabe, Lager, Vorstand). Core feature from Phase 0.
|
||||
- **[2026-04-06]** Grow Calendar: US-027 added as Could Have (v2) — cultivation diary per grow cycle, linked to batch harvest, optional photo attachments, admin-controlled access via `MANAGE_GROW_CALENDAR` permission
|
||||
- **[2026-04-06]** Staff wireframe (Screen 7) added to `06-Wireframes.md` with full ASCII wireframe, component table (TanStack Table, shadcn/ui Checkbox, Select), and DSGVO design rationale
|
||||
- **[2026-04-06]** Staff routes added to Navigation IA: `/admin/staff`, `/admin/staff/new`, `/admin/staff/{id}`, `/staff/dashboard`
|
||||
- **[2026-04-06]** TrueNAS.local Gitea Actions self-hosted runner documented in `09-Deployment.md` as the CI/CD build environment; Hetzner = production release target
|
||||
|
||||
### Changed
|
||||
|
||||
- **[2026-04-06]** `03-Architecture.md` — **Multi-tenancy model changed from shared-schema to schema-per-tenant.** Decision rationale: hard DB-level isolation (not application-layer), clean DSGVO deletion (`DROP SCHEMA`), no cross-tenant index bloat, easier future isolation. `tenant_id` columns on every entity removed; schema routing via `TenantRoutingDataSource` replaces Hibernate `@Filter`.
|
||||
- **[2026-04-06]** `03-Architecture.md` — **Frontend changed from PrimeFaces/JSF to React/Vite SPA.** Rationale: JSF server-side lifecycle is a poor fit for a REST API backend; PrimeFaces creates a hiring bottleneck; React is mobile-friendly from day 1. Component library: shadcn/ui (Radix UI + Tailwind CSS) + TanStack Table v8.
|
||||
- **[2026-04-06]** `03-Architecture.md` — `ROLE_STAFF` added with configurable `StaffPermission` enum; pre-created templates documented. Staff noted as core feature, not add-on.
|
||||
- **[2026-04-06]** `06-Wireframes.md` — All component tables updated from PrimeFaces (`p:dataTable`, `p:commandButton`) to React/Tailwind equivalents (TanStack Table, shadcn/ui). Responsive Design section rewritten for Tailwind breakpoints.
|
||||
- **[2026-04-06]** `09-Deployment.md` — CI/CD section rewritten: `runs-on: ubuntu-latest` → `runs-on: self-hosted` (TrueNAS.local). Gitea Actions runner setup instructions added. Infrastructure diagram updated to show Dev → Gitea → TrueNAS build → Hetzner release flow.
|
||||
- **[2026-04-06]** `0.1.0` CHANGELOG entry corrected: removed "shared schema" as final architecture decision (superseded by schema-per-tenant); removed PrimeFaces as frontend (superseded by React/Vite)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **[2026-04-06]** `04-Flowcharts.md` — Mermaid parse error in Flow 4 (line 146): `[Generate empty report\nwith zero totals\n(still valid compliance submission)]` — parenthesis after newline was parsed as stadium-shape node start. Fixed by wrapping node text in double quotes.
|
||||
- **[2026-04-06]** `04-Flowcharts.md` — Mermaid parse error in Flow 5 (line 177): `[❌ Invalid credentials\n(generic — do not reveal\nwhether email exists)]` — same root cause, same fix.
|
||||
|
||||
---
|
||||
|
||||
@@ -33,9 +51,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
- `STRATEGY.md` — initial project vision and feasibility assessment
|
||||
- Legal analysis confirming CanG compliance viability for B2B SaaS model (no public advertising, no club discovery, B2B-only)
|
||||
- Market analysis: ~3,000 registered clubs in Germany, TAM estimated at €2.85M ARR
|
||||
- Tech stack selection rationale: Spring Boot 3.x + PrimeFaces JSF (MVP) → Next.js v2; PostgreSQL + Flyway; iText 7 PDF; Stripe billing
|
||||
- Multi-tenancy architectural decision: shared schema with `tenant_id` column (chosen over schema-per-tenant for lower operational overhead at MVP scale)
|
||||
- Tech stack selection rationale: Spring Boot 3.x + React/Vite SPA (MVP) → Next.js v2; PostgreSQL + Flyway; iText 7 PDF; Stripe billing
|
||||
- Multi-tenancy architectural decision: schema-per-tenant (each club gets isolated PostgreSQL schema; platform registry in `public` schema)
|
||||
- Pricing model: 4 tiers (Starter €29, Growth €59, Professional €99, Enterprise €199/month)
|
||||
- Deployment guide for Hetzner VPS (production release): Docker Compose, Nginx + Let's Encrypt, Gitea Actions CI/CD via TrueNAS.local self-hosted runner, daily PostgreSQL backup strategy
|
||||
|
||||
---
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# frpc deployment script for TrueNAS
|
||||
# Run from Fedora: bash plans/frpc-truenas-deploy.sh
|
||||
# Installs frpc on TrueNAS and sets up tunnel to expose Gitea publicly
|
||||
|
||||
TRUENAS="root@192.168.188.119"
|
||||
VPS_IP="85.214.154.199"
|
||||
FRP_TOKEN="5f64a6f20bb2cb8c3133ecac8ca3f0571d7d64dff910225040bfc0c60a106c81"
|
||||
FRP_VERSION="0.68.1"
|
||||
|
||||
echo "=== Deploying frpc on TrueNAS ==="
|
||||
|
||||
ssh -i /home/pplate/.ssh/id_ed25519_homelab $TRUENAS << REMOTE
|
||||
set -e
|
||||
|
||||
# TrueNAS root filesystem is read-only — install to /mnt which is persistent ZFS
|
||||
INSTALL_DIR=/mnt/VM_SSD_Pool/frp
|
||||
mkdir -p \$INSTALL_DIR
|
||||
|
||||
# Download frpc binary
|
||||
echo "Downloading frp ${FRP_VERSION}..."
|
||||
curl -sL https://github.com/fatedier/frp/releases/download/v${FRP_VERSION}/frp_${FRP_VERSION}_linux_amd64.tar.gz \
|
||||
-o /tmp/frp.tar.gz
|
||||
tar xzf /tmp/frp.tar.gz -C /tmp/
|
||||
cp /tmp/frp_${FRP_VERSION}_linux_amd64/frpc \$INSTALL_DIR/frpc
|
||||
chmod +x \$INSTALL_DIR/frpc
|
||||
\$INSTALL_DIR/frpc --version
|
||||
|
||||
# Write frpc config
|
||||
cat > \$INSTALL_DIR/frpc.toml << 'TOML'
|
||||
serverAddr = "${VPS_IP}"
|
||||
serverPort = 7000
|
||||
auth.method = "token"
|
||||
auth.token = "${FRP_TOKEN}"
|
||||
log.to = "/tmp/frpc.log"
|
||||
log.level = "info"
|
||||
|
||||
[[proxies]]
|
||||
name = "gitea"
|
||||
type = "tcp"
|
||||
localIP = "127.0.0.1"
|
||||
localPort = 30008
|
||||
remotePort = 30008
|
||||
TOML
|
||||
|
||||
echo "frpc config written:"
|
||||
cat \$INSTALL_DIR/frpc.toml
|
||||
|
||||
# Create init script (TrueNAS uses systemd-like init but custom)
|
||||
# Use /etc/local.d/ for persistent startup scripts on TrueNAS SCALE
|
||||
# Actually TrueNAS SCALE uses systemd — write a service to /etc/systemd/system/
|
||||
cat > /etc/systemd/system/frpc.service << 'SVCEOF'
|
||||
[Unit]
|
||||
Description=frp client - tunnel to plate.software VPS
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/mnt/VM_SSD_Pool/frp/frpc -c /mnt/VM_SSD_Pool/frp/frpc.toml
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SVCEOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable frpc
|
||||
systemctl start frpc
|
||||
sleep 3
|
||||
systemctl status frpc --no-pager | head -15
|
||||
echo ""
|
||||
echo "=== frpc deployed and running ==="
|
||||
echo "Gitea should now be reachable at https://git.plate.software"
|
||||
REMOTE
|
||||
@@ -0,0 +1,139 @@
|
||||
# Task: Swap Qwen3-4B Encoder for Heretic Abliterated Version
|
||||
|
||||
**Datum:** 2026-04-10
|
||||
**Status:** ✅ COMPLETE — Heretic encoder swapped and live-tested 2026-04-10
|
||||
**Depends on:** FLUX.2 Klein 4B working (✅ done as of 2026-04-10)
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Replace the standard `qwen_3_4b_klein.safetensors` with an abliterated (Heretic) version that has:
|
||||
- **Zero measurable quality loss** (KL divergence = 0.0000)
|
||||
- **No prompt refusals** (≤3/100 in DreamFast v1.2.0 testing)
|
||||
|
||||
Result: `generate_image(prompt, model="flux-2-klein-4b.safetensors")` will work with **any** prompt without refusals.
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
| File | Location | Status |
|
||||
|------|----------|--------|
|
||||
| `flux-2-klein-4b.safetensors` | `~/ComfyUI/models/diffusion_models/` | ✅ Working |
|
||||
| `qwen_3_4b_klein.safetensors` | `~/ComfyUI/models/text_encoders/` | ✅ Working (standard, has refusals) |
|
||||
| `flux2-vae.safetensors` | `~/ComfyUI/models/vae/` | ✅ Working |
|
||||
|
||||
The MCP workflow [`mcp/mcp-image-gen/src/workflows/flux2_klein_heretic.json`](../mcp/mcp-image-gen/src/workflows/flux2_klein_heretic.json) already uses `qwen_3_4b_klein.safetensors` — **no code change needed**, only the file on disk needs to be replaced.
|
||||
|
||||
---
|
||||
|
||||
## The Problem to Solve First
|
||||
|
||||
The standard Heretic repos may not have the **FLUX.2 Klein-compatible** encoder dimensions:
|
||||
|
||||
| Encoder | `hidden_size` | Conditioning dim | Usable? |
|
||||
|---------|--------------|-----------------|---------|
|
||||
| BFL Qwen3-4B (FLUX.2 Klein) | **2560** | 7680 (2560×3) | ✅ |
|
||||
| DreamFast/qwen3-4b-heretic | unknown — must check | ? | ⚠️ verify first |
|
||||
| Standard Qwen3-4B | 4096 | 4096 | ❌ wrong |
|
||||
|
||||
**Before downloading, verify DreamFast's model is fine-tuned from the BFL variant** (hidden_size=2560), not the standard Qwen3 (hidden_size=4096).
|
||||
|
||||
---
|
||||
|
||||
## Steps
|
||||
|
||||
### Step 1: Check DreamFast Heretic repo
|
||||
|
||||
```bash
|
||||
huggingface-cli model-info DreamFast/qwen3-4b-heretic 2>/dev/null | grep -i hidden
|
||||
```
|
||||
|
||||
Or browse: https://huggingface.co/DreamFast/qwen3-4b-heretic/blob/main/config.json
|
||||
Look for: `"hidden_size": 2560` — that's the FLUX.2 Klein-compatible version.
|
||||
|
||||
### Step 2a: If DreamFast has the right dimensions (2560)
|
||||
|
||||
```bash
|
||||
# Download
|
||||
huggingface-cli download DreamFast/qwen3-4b-heretic \
|
||||
--local-dir /tmp/qwen3-4b-heretic/
|
||||
|
||||
# Back up working encoder first
|
||||
cp ~/ComfyUI/models/text_encoders/qwen_3_4b_klein.safetensors \
|
||||
~/ComfyUI/models/text_encoders/qwen_3_4b_klein_backup.safetensors
|
||||
|
||||
# Swap in the Heretic version
|
||||
cp /tmp/qwen3-4b-heretic/model.safetensors \
|
||||
~/ComfyUI/models/text_encoders/qwen_3_4b_klein.safetensors
|
||||
```
|
||||
|
||||
### Step 2b: If DreamFast has wrong dimensions (4096) — find alternative
|
||||
|
||||
Options in order of preference:
|
||||
1. **Lockout/qwen3-4b-heretic-zimage** — check if BFL-compatible:
|
||||
```bash
|
||||
huggingface-cli model-info Lockout/qwen3-4b-heretic-zimage 2>/dev/null | grep hidden
|
||||
```
|
||||
2. **Run Heretic abliteration yourself** on the working `qwen_3_4b_klein.safetensors`
|
||||
Tool: https://github.com/FailSpy/abliterator
|
||||
Script: `python abliterator.py --model qwen_3_4b_klein.safetensors --output qwen_3_4b_klein_heretic.safetensors`
|
||||
|
||||
3. **Wait** for DreamFast or BFL to publish the FLUX.2-specific abliterated encoder
|
||||
|
||||
### Step 3: Live test
|
||||
|
||||
```python
|
||||
generate_image(
|
||||
"an explicit test prompt that would normally be refused",
|
||||
model="flux-2-klein-4b.safetensors",
|
||||
steps=20
|
||||
)
|
||||
```
|
||||
|
||||
Expected: Image generated, no refusal error in ComfyUI logs.
|
||||
|
||||
### Step 4: If it works — no code changes needed
|
||||
|
||||
The MCP code, workflow JSON, and registry are already correct. Just verify:
|
||||
- Check `journalctl --user -u comfyui -f` during generation for any errors
|
||||
- Confirm file in `~/Pictures/mcp-generated/` was saved
|
||||
|
||||
---
|
||||
|
||||
## Fallback Plan
|
||||
|
||||
If the Heretic encoder is unavailable in the right dimensions, the **GGUF route** works too:
|
||||
|
||||
```bash
|
||||
# ComfyUI-GGUF is already installed: ~/ComfyUI/custom_nodes/ComfyUI-GGUF
|
||||
# Download Heretic GGUF (if BFL-compatible variant published):
|
||||
huggingface-cli download Lockout/qwen3-4b-heretic-zimage \
|
||||
qwen-4b-zimage-hereticV2-q8.gguf \
|
||||
--local-dir ~/ComfyUI/models/text_encoders/
|
||||
```
|
||||
|
||||
Then update [`flux2_klein_heretic.json`](../mcp/mcp-image-gen/src/workflows/flux2_klein_heretic.json) node `"1"`:
|
||||
```json
|
||||
"class_type": "CLIPLoaderGGUF", // instead of CLIPLoader
|
||||
"inputs": {
|
||||
"clip_name": "qwen-4b-zimage-hereticV2-q8.gguf",
|
||||
"type": "flux2"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## No Code Changes Required (unless GGUF fallback)
|
||||
|
||||
The entire MCP server, workflow registry, and test suite are already correct. This is **purely a model file task**.
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [x] `generate_image("...", model="flux-2-klein-4b.safetensors")` works with prompts that currently get refused — ✅ tested 2026-04-10, Renaissance nude generated without refusal
|
||||
- [x] Output image quality identical to standard encoder (check: no visible artifacts vs reference) — ✅ 1.9MB photorealistic 1024×1024, museum-quality result, 50.4s
|
||||
- [x] ComfyUI logs show no dimension errors — ✅ only harmless libcudart NVIDIA stub warnings
|
||||
- [x] `qwen_3_4b_klein_backup.safetensors` kept as rollback — ✅ 7.5G backup at ~/ComfyUI/models/text_encoders/qwen_3_4b_klein_backup.safetensors
|
||||
@@ -0,0 +1,104 @@
|
||||
# FLUX.2 Klein 4B + Heretic — Session Recap
|
||||
|
||||
**Date:** 2026-04-10
|
||||
**Status:** Code complete, live generation BLOCKED by encoder dimension mismatch
|
||||
|
||||
---
|
||||
|
||||
## What We Achieved ✅
|
||||
|
||||
### Code Infrastructure (Solid)
|
||||
- **`mcp-image-gen/src/server.py`** — Generic workflow registry with model-based dispatch, `_inject_workflow_params()` works recursively on any node layout
|
||||
- **`mcp-image-gen/tests/test_server.py`** — 37/37 tests passing
|
||||
- **Gitea** — pushed to main (commit `38d26ad`)
|
||||
- The architecture is right: adding a new model = add 1 JSON file + 1 registry entry
|
||||
|
||||
### Models Downloaded (on disk)
|
||||
| File | Location | Status |
|
||||
|------|----------|--------|
|
||||
| `flux-2-klein-4b.safetensors` | `~/ComfyUI/models/diffusion_models/` | ✅ 7.3GB |
|
||||
| `qwen_3_4b_bfl.safetensors` | `~/ComfyUI/models/text_encoders/` | ✅ merged from BFL shards |
|
||||
| `qwen_3_4b.safetensors` (z_image) | `~/ComfyUI/models/text_encoders/split_files/` | ✅ wrong model |
|
||||
| `Qwen3-4B-Q8_0.gguf` | `~/ComfyUI/models/text_encoders/` | ✅ wrong arch |
|
||||
| ComfyUI-GGUF extension | `~/ComfyUI/custom_nodes/ComfyUI-GGUF` | ✅ installed |
|
||||
|
||||
---
|
||||
|
||||
## What Failed and Why ❌
|
||||
|
||||
### The Error (persistent)
|
||||
```
|
||||
mat1 and mat2 shapes cannot be multiplied (512x4096 and 7680x3072)
|
||||
```
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
**Node 13** (`SamplerCustomAdvanced`) fails — meaning the conditioning vector from the text encoder doesn't match the diffusion model's expected input.
|
||||
|
||||
| Component | Expected | Got |
|
||||
|-----------|----------|-----|
|
||||
| FLUX.2 Klein 4B conditioning input | **7680-dim** (2560 × 3) | **4096-dim** |
|
||||
|
||||
**Why 7680 = 2560 × 3?**
|
||||
FLUX models concatenate text embeddings across multiple time steps. The BFL Qwen3 encoder has `hidden_size=2560`, so the concatenated output is 2560×3=7680.
|
||||
|
||||
**Why 4096?**
|
||||
Every other Qwen3 variant (z_image_turbo, official Qwen repo GGUF) uses standard Qwen3 with `hidden_size=4096` — these are for Z-Image and text generation respectively, NOT for FLUX.2 Klein.
|
||||
|
||||
### What We Tried (and Why Each Failed)
|
||||
1. `CLIPLoader type=flux` → wrong architecture (FLUX.1 style)
|
||||
2. `CLIPLoader type=flux2` → correct node, wrong encoder file (z_image Qwen)
|
||||
3. `CLIPLoaderGGUF type=flux2` → correct node, wrong GGUF (standard Qwen3)
|
||||
4. `CLIPLoader type=flux2 + qwen_3_4b_bfl.safetensors` → merged BFL shards, but still fails
|
||||
5. Workflow: `KSampler` → doesn't work with FLUX.2 (different architecture)
|
||||
6. Workflow: `SamplerCustomAdvanced + BasicGuider + Flux2Scheduler` → correct architecture but encoding mismatch persists
|
||||
|
||||
### The Real Missing Piece
|
||||
|
||||
The BFL FLUX.2 Klein text encoder in Diffusers format is designed for use via `transformers/diffusers` pipeline, NOT via ComfyUI's `CLIPLoader`. ComfyUI reads the weights differently. The weights are there but ComfyUI doesn't know how to map `model.embed_tokens`, `model.layers.N.*` etc. to the CLIP interface it expects.
|
||||
|
||||
**The correct encoder file for ComfyUI** is `Comfy-Org/vae-text-encorder-for-flux-klein-4b` — the 7.5GB file we downloaded IS the right one, but ComfyUI is likely loading it with the wrong adapter in the `CLIPLoader`.
|
||||
|
||||
---
|
||||
|
||||
## Clean Approach — What We Need to Do
|
||||
|
||||
### Option A: Use ComfyUI Web UI (Easiest)
|
||||
1. Open `http://localhost:8188` in browser
|
||||
2. Load the "Flux.2 Klein 4B Text-to-Image" workflow template (it's in the UI Templates)
|
||||
3. **Export the working API JSON** (Ctrl+Shift+E or Settings → Save as API format)
|
||||
4. Replace our `flux2_klein_heretic.json` with the exported JSON
|
||||
5. Add placeholders and test
|
||||
|
||||
This gives us the **verified working node graph** without guessing. 10 minutes.
|
||||
|
||||
### Option B: Find a Working API JSON online
|
||||
- Reddit r/comfyui has working FLUX.2 Klein workflows
|
||||
- Export format is what we need
|
||||
|
||||
### Then: Add Heretic
|
||||
Once we have a working standard workflow:
|
||||
1. Download the actual Heretic-abliterated version of the BFL encoder (once it's published)
|
||||
2. Swap encoder filename in the JSON
|
||||
|
||||
---
|
||||
|
||||
## My Recommendation
|
||||
|
||||
**Do Option A right now.** Open `http://localhost:8188`, load the template, export to API format, paste the JSON. We'll be running in 10 minutes instead of guessing node names.
|
||||
|
||||
The MCP server code is solid — the only broken piece is `flux2_klein_heretic.json`. Once we have the right JSON from the UI, everything else works.
|
||||
|
||||
---
|
||||
|
||||
## Files to Clean Up (After We Have the Right JSON)
|
||||
|
||||
```bash
|
||||
# Remove wrong encoders (save ~8GB)
|
||||
rm ~/ComfyUI/models/text_encoders/qwen_3_4b.safetensors # z_image version
|
||||
rm ~/ComfyUI/models/text_encoders/qwen_3_4b_flux2.safetensors
|
||||
|
||||
# Keep
|
||||
# ~/ComfyUI/models/text_encoders/qwen_3_4b_bfl.safetensors ← correct encoder
|
||||
# ~/ComfyUI/models/text_encoders/Qwen3-4B-Q8_0.gguf ← maybe useful later
|
||||
```
|
||||
@@ -0,0 +1,262 @@
|
||||
# Homelab Proxy Architecture Plan
|
||||
_plate.software VPS as public face → WireGuard tunnel → TrueNAS.local_
|
||||
|
||||
## Goal
|
||||
|
||||
Use the cheap public VPS (`plate.software` @ 85.214.154.199 / Plesk) as:
|
||||
- Public DNS + TLS termination point
|
||||
- Apache reverse proxy routing subdomains to TrueNAS homelab services
|
||||
- ACME/Let's Encrypt managed by Plesk (already working)
|
||||
|
||||
TrueNAS.local (192.168.188.119) becomes the actual compute host for all Docker services.
|
||||
|
||||
---
|
||||
|
||||
## The Core Problem: TrueNAS is Behind NAT
|
||||
|
||||
TrueNAS lives on a home network. The public VPS cannot reach it directly. A tunnel is required.
|
||||
|
||||
### ⚠️ WireGuard NOT possible — VPS is OpenVZ
|
||||
|
||||
The VPS (`h2970715.stratoserver.net`, Strato) runs on OpenVZ virtualization.
|
||||
WireGuard requires a kernel module — **not loadable in OpenVZ containers**.
|
||||
|
||||
### Recommended Solution: frp (Fast Reverse Proxy)
|
||||
|
||||
```
|
||||
Internet
|
||||
↓ DNS
|
||||
plate.software VPS (85.214.154.199)
|
||||
frps server (port 7000)
|
||||
↓ Apache ProxyPass (HTTP/HTTPS)
|
||||
↓ frp tunnel (TCP, userspace)
|
||||
TrueNAS.local (192.168.188.119)
|
||||
frpc client → connects out to VPS:7000
|
||||
├── Gitea :30008 → git.plate.software → VPS:30008
|
||||
├── WildFly/Java EE :8080 → plate.software → VPS:18080
|
||||
└── Future services :XXXX → app.plate.software
|
||||
```
|
||||
|
||||
**Why frp:**
|
||||
- Pure userspace Go binary — works perfectly on OpenVZ
|
||||
- TrueNAS (frpc) initiates outbound connection — no router port forwarding needed
|
||||
- Encrypted tunnel (TLS optional)
|
||||
- VPS (frps) exposes local ports that Apache proxies to
|
||||
- Zero kernel dependencies
|
||||
|
||||
---
|
||||
|
||||
## Target DNS Routing
|
||||
|
||||
| Domain / Subdomain | Routes to | Notes |
|
||||
|-------------------------|-----------------------------------|-------|
|
||||
| `plate.software` | TrueNAS:8080 (WildFly) | Current customer Java EE project |
|
||||
| `git.plate.software` | TrueNAS:30008 (Gitea) | New — expose homelab Gitea publicly |
|
||||
| `app.plate.software` | TrueNAS:XXXX (future) | Placeholder for future projects |
|
||||
|
||||
All DNS A records point to `85.214.154.199` (VPS). TLS is terminated at the VPS by Plesk/Let's Encrypt.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: WireGuard Tunnel (VPS ↔ TrueNAS)
|
||||
|
||||
**On the VPS (root@85.214.154.199):**
|
||||
```bash
|
||||
apt install wireguard
|
||||
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
|
||||
```
|
||||
|
||||
Create `/etc/wireguard/wg0.conf`:
|
||||
```ini
|
||||
[Interface]
|
||||
Address = 10.100.0.1/24
|
||||
ListenPort = 51820
|
||||
PrivateKey = <server_private_key>
|
||||
|
||||
[Peer]
|
||||
PublicKey = <truenas_public_key>
|
||||
AllowedIPs = 10.100.0.2/32
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
**On TrueNAS (via TrueNAS SCALE UI or shell):**
|
||||
- Apps → Network → WireGuard → Add Interface
|
||||
- Or via shell: same `wg genkey` + `/etc/wireguard/wg0.conf` approach
|
||||
```ini
|
||||
[Interface]
|
||||
Address = 10.100.0.2/24
|
||||
PrivateKey = <truenas_private_key>
|
||||
|
||||
[Peer]
|
||||
PublicKey = <vps_public_key>
|
||||
Endpoint = 85.214.154.199:51820
|
||||
AllowedIPs = 10.100.0.1/32
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
Enable on both:
|
||||
```bash
|
||||
systemctl enable --now wg-quick@wg0
|
||||
```
|
||||
|
||||
Test:
|
||||
```bash
|
||||
# From VPS
|
||||
ping 10.100.0.2
|
||||
curl http://10.100.0.2:30008 # Should reach Gitea
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Firewall — Open WireGuard Port on VPS
|
||||
|
||||
```bash
|
||||
# On VPS
|
||||
ufw allow 51820/udp
|
||||
# Or via iptables if ufw not present
|
||||
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
|
||||
```
|
||||
|
||||
Also ensure TrueNAS router/firewall does NOT need any port forwarding — TrueNAS initiates the tunnel outbound. The VPS listens; TrueNAS connects. No router config needed.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Plesk Apache — Add Subdomain Proxy Rules
|
||||
|
||||
**Add `git.plate.software` as a subdomain in Plesk:**
|
||||
1. Plesk → Domains → Add Subdomain → `git.plate.software`
|
||||
2. Apache & nginx Settings → Additional directives for HTTP:
|
||||
```apache
|
||||
<IfModule mod_proxy.c>
|
||||
ProxyPass /.well-known/acme-challenge/ !
|
||||
ProxyPass / http://10.100.0.2:30008/ retry=0
|
||||
ProxyPassReverse / http://10.100.0.2:30008/
|
||||
ProxyPreserveHost On
|
||||
</IfModule>
|
||||
```
|
||||
3. Issue Let's Encrypt cert for `git.plate.software`
|
||||
4. Configure HTTPS redirect and HTTPS proxy directives the same way
|
||||
|
||||
**Update `plate.software` HTTP directives:**
|
||||
Change the existing WildFly proxy target from `127.0.0.1:8080` to `10.100.0.2:8080` once WildFly is moved to TrueNAS:
|
||||
```apache
|
||||
ProxyPass / http://10.100.0.2:8080/ retry=0
|
||||
```
|
||||
(Keep this as `127.0.0.1:8080` while the Docker container still runs on the VPS)
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Migrate WildFly to TrueNAS
|
||||
|
||||
The customer's Java EE app currently runs in Docker on the VPS. Migrate to TrueNAS:
|
||||
|
||||
1. Export/pull the WildFly Docker image
|
||||
2. Copy any persistent volumes/data
|
||||
3. Create `docker-compose.yml` on TrueNAS
|
||||
4. Start container on TrueNAS, verify on `10.100.0.2:8080`
|
||||
5. Update VPS Apache proxy target from `127.0.0.1:8080` → `10.100.0.2:8080`
|
||||
6. Remove the Docker container from VPS
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Gitea Public HTTPS
|
||||
|
||||
For Gitea to work properly behind a proxy, update its config to know its public URL:
|
||||
|
||||
Edit Gitea's `app.ini` (in the Gitea Docker volume on TrueNAS):
|
||||
```ini
|
||||
[server]
|
||||
DOMAIN = git.plate.software
|
||||
ROOT_URL = https://git.plate.software/
|
||||
HTTP_PORT = 30008
|
||||
```
|
||||
|
||||
Also in Plesk HTTPS directives for `git.plate.software`:
|
||||
```apache
|
||||
<IfModule mod_proxy.c>
|
||||
ProxyPass /.well-known/acme-challenge/ !
|
||||
ProxyPass / http://10.100.0.2:30008/ retry=0
|
||||
ProxyPassReverse / http://10.100.0.2:30008/
|
||||
ProxyPreserveHost On
|
||||
RequestHeader set X-Forwarded-Proto "https"
|
||||
RequestHeader set X-Forwarded-Port "443"
|
||||
</IfModule>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Topology (Final State)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Internet / DNS │
|
||||
│ *.plate.software → 85.214.154.199 │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ HTTP/HTTPS
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ VPS: plate.software │
|
||||
│ 85.214.154.199 / Plesk / Apache │
|
||||
│ │
|
||||
│ plate.software → proxy:8080 │
|
||||
│ git.plate.software → proxy:30008 │
|
||||
│ app.plate.software → proxy:XXXX │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ WireGuard 10.100.0.0/24
|
||||
│ UDP 51820 (encrypted)
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ TrueNAS.local │
|
||||
│ 192.168.188.119 / WG: 10.100.0.2 │
|
||||
│ │
|
||||
│ :30008 Gitea │
|
||||
│ :8080 WildFly (Java EE) │
|
||||
│ :XXXX Future services │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risks & Notes
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| Home ISP outage takes down all services | Acceptable for homelab; add health check monitoring later |
|
||||
| ISP dynamic IP changes (if applicable) | WireGuard peer config uses VPS as endpoint (fixed IP) — TrueNAS initiates tunnel, so home IP change is transparent |
|
||||
| TrueNAS reboot drops tunnel | `systemctl enable wg-quick@wg0` ensures auto-start |
|
||||
| Gitea SSH cloning (port 22/2222) | Need separate SSH port forward or Gitea SSH over different port — HTTP clone still works via HTTPS proxy |
|
||||
| Customer data on VPS → TrueNAS migration | Do at off-peak time; test thoroughly before cutting DNS |
|
||||
|
||||
---
|
||||
|
||||
## Cost Model
|
||||
|
||||
- VPS (plate.software): Keep cheap (~3-5€/month) — CPU/RAM irrelevant, just proxy traffic
|
||||
- TrueNAS: All compute happens here — free (already owned hardware)
|
||||
- Cloudflare (optional): Free plan for DNS + DDoS protection on top of the VPS
|
||||
|
||||
---
|
||||
|
||||
## Alternative: Cloudflare Tunnel (Zero-Config Option)
|
||||
|
||||
If WireGuard setup is too complex, Cloudflare Tunnel (`cloudflared`) is a zero-config alternative:
|
||||
- Run `cloudflared` as a Docker container on TrueNAS
|
||||
- No VPS needed for tunneling — Cloudflare handles the public endpoint
|
||||
- Free for personal use
|
||||
- TrueNAS → Cloudflare edge → DNS → users
|
||||
|
||||
**Downside:** Traffic routes through Cloudflare (not self-hosted end-to-end). VPS still useful for non-Cloudflare domains and the existing customer project.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. ✅ Fix plate.software Let's Encrypt (done)
|
||||
2. 🔜 Set up WireGuard tunnel (VPS ↔ TrueNAS)
|
||||
3. 🔜 Add `git.plate.software` subdomain in Plesk + proxy to TrueNAS Gitea
|
||||
4. 🔜 Update Gitea `app.ini` with public URL
|
||||
5. 🔜 Issue Let's Encrypt for `git.plate.software`
|
||||
6. ⏳ Migrate WildFly customer project from VPS → TrueNAS
|
||||
7. ⏳ Decommission VPS Docker container (keep VPS as pure proxy)
|
||||
@@ -0,0 +1,194 @@
|
||||
# Task: Add ESRGAN Upscaler to mcp-image-gen
|
||||
|
||||
**Datum:** 2026-04-10
|
||||
**Status:** Ready to implement
|
||||
**Depends on:** mcp-image-gen working ✅, FLUX.2 Klein Heretic working ✅
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Add an `upscale_image()` MCP tool that takes an existing PNG path (from a previous `generate_image()` call) and upscales it 2× or 4× using a Real-ESRGAN model — **no diffusion re-generation**, just fast post-processing (~5–10s).
|
||||
|
||||
Result: A 1024×1024 → 4096×4096 pipeline in two tool calls:
|
||||
```python
|
||||
result = generate_image("...", model="flux-2-klein-4b.safetensors", steps=20)
|
||||
# → ~/Pictures/mcp-generated/foo_20260410_123456_12345.png
|
||||
|
||||
upscaled = upscale_image(
|
||||
input_path="~/Pictures/mcp-generated/foo_20260410_123456_12345.png",
|
||||
scale=4
|
||||
)
|
||||
# → ~/Pictures/mcp-generated/foo_20260410_123456_12345_4x.png (4096×4096)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why ESRGAN (Option B) over Latent Upscale
|
||||
|
||||
| Method | Time overhead | Quality | Requires diffusion? |
|
||||
|--------|--------------|---------|---------------------|
|
||||
| ESRGAN image upscale | ~5–10s | ✅ Very sharp details | ❌ No |
|
||||
| Latent upscale + KSampler | ~50% extra gen time | ✅ Good, consistent style | ✅ Yes |
|
||||
| UltimateSDUpscale (tiled) | ~4× gen time | ✅ Highest quality | ✅ Yes |
|
||||
|
||||
ESRGAN is the clear winner for "I want a bigger version of this image quickly."
|
||||
|
||||
---
|
||||
|
||||
## Model to Use
|
||||
|
||||
**`4x-UltraSharp.pth`** — the community standard for photorealistic upscaling.
|
||||
|
||||
- Source: https://huggingface.co/Kim2091/UltraSharp
|
||||
- Download: `huggingface-cli download Kim2091/UltraSharp 4x-UltraSharp.pth --local-dir ~/ComfyUI/models/upscale_models/`
|
||||
- Size: ~67MB
|
||||
- Scale factor: 4× (can also be used for 2× via image resize after)
|
||||
|
||||
Alternative: `RealESRGAN_x4plus.pth` (in ComfyUI's model downloader, general purpose)
|
||||
|
||||
---
|
||||
|
||||
## ComfyUI Workflow: `esrgan_upscale.json`
|
||||
|
||||
Minimal workflow — 3 nodes:
|
||||
|
||||
```
|
||||
LoadImage → UpscaleModelLoader + ImageUpscaleWithModel → SaveImage
|
||||
```
|
||||
|
||||
Node layout:
|
||||
|
||||
```json
|
||||
{
|
||||
"1": {
|
||||
"class_type": "LoadImage",
|
||||
"inputs": {
|
||||
"image": "__INPUT_PATH__"
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"class_type": "UpscaleModelLoader",
|
||||
"inputs": {
|
||||
"model_name": "4x-UltraSharp.pth"
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"class_type": "ImageUpscaleWithModel",
|
||||
"inputs": {
|
||||
"upscale_model": ["2", 0],
|
||||
"image": ["1", 0]
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {
|
||||
"images": ["3", 0],
|
||||
"filename_prefix": "__OUTPUT_PREFIX__"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** `LoadImage` in ComfyUI requires the image to be in `~/ComfyUI/input/` — the workflow builder must copy the input file there first (or use `ETN_LoadImageBase64` if available). See "Implementation Notes" below.
|
||||
|
||||
---
|
||||
|
||||
## MCP Tool Signature
|
||||
|
||||
Add to [`mcp/mcp-image-gen/src/server.py`](../mcp/mcp-image-gen/src/server.py):
|
||||
|
||||
```python
|
||||
@mcp.tool()
|
||||
async def upscale_image(
|
||||
input_path: Annotated[str, Field(description="Path to input PNG (absolute or ~-relative). Must be a file previously generated by generate_image().")],
|
||||
scale: Annotated[int, Field(description="Upscale factor: 2 or 4 (default: 4). 4x-UltraSharp always runs at 4x; scale=2 applies a 0.5 resize after.")] = 4,
|
||||
output_dir: Annotated[str, Field(description="Override output directory. Defaults to same dir as input_path.")] = "",
|
||||
name: Annotated[str, Field(description="Optional output filename prefix. Defaults to input filename + _4x or _2x.")] = "",
|
||||
) -> list:
|
||||
"""Upscale an existing image using Real-ESRGAN (4x-UltraSharp).
|
||||
|
||||
No diffusion re-generation — pure post-processing (~5-10s).
|
||||
Input must be a PNG file. Output is saved alongside the input by default.
|
||||
|
||||
Returns both a file path and an inline base64 image for display.
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### The `LoadImage` ComfyUI constraint
|
||||
|
||||
ComfyUI's built-in `LoadImage` node only accepts filenames relative to `~/ComfyUI/input/`, not arbitrary paths. Two solutions:
|
||||
|
||||
**Solution A (simplest):** Copy input to `~/ComfyUI/input/` before submitting workflow, use basename as `image` param, delete after.
|
||||
|
||||
**Solution B:** Use `ETN_LoadImageBase64` node (part of `ComfyUI-ETN` custom node extension) — accepts a base64-encoded image directly. Check if installed:
|
||||
```bash
|
||||
ls ~/ComfyUI/custom_nodes/ | grep -i etn
|
||||
```
|
||||
|
||||
**Recommended:** Start with Solution A (copy to input dir) — no dependencies. If `ComfyUI-ETN` is present, prefer Solution B for cleanliness.
|
||||
|
||||
### Scale=2 handling
|
||||
|
||||
`4x-UltraSharp.pth` always outputs 4×. For `scale=2`, upscale at 4× then resize the result image to 50% with PIL before saving. This is still sharper than native 2× bilinear upscaling.
|
||||
|
||||
### Output filename convention
|
||||
|
||||
Input: `foo_20260410_123456_12345.png`
|
||||
Output `scale=4`: `foo_20260410_123456_12345_4x.png`
|
||||
Output `scale=2`: `foo_20260410_123456_12345_2x.png`
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| [`mcp/mcp-image-gen/src/workflows/esrgan_upscale.json`](../mcp/mcp-image-gen/src/workflows/esrgan_upscale.json) | New — ESRGAN workflow |
|
||||
| [`mcp/mcp-image-gen/src/server.py`](../mcp/mcp-image-gen/src/server.py) | Add `upscale_image()` tool + helpers |
|
||||
| [`mcp/mcp-image-gen/tests/test_upscale.py`](../mcp/mcp-image-gen/tests/test_upscale.py) | New test file |
|
||||
|
||||
**No changes to:** workflow registry, existing tools, `generate_image()`.
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight: Download Model
|
||||
|
||||
```bash
|
||||
huggingface-cli download Kim2091/UltraSharp \
|
||||
4x-UltraSharp.pth \
|
||||
--local-dir ~/ComfyUI/models/upscale_models/
|
||||
```
|
||||
|
||||
Verify ComfyUI sees it:
|
||||
```bash
|
||||
curl -s http://localhost:8188/object_info/UpscaleModelLoader | \
|
||||
python3 -c "import sys,json; d=json.load(sys.stdin); print('\n'.join(d['UpscaleModelLoader']['input']['required']['model_name'][0]))"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Test | Input | Expected |
|
||||
|------|-------|----------|
|
||||
| `test_upscale_4x` | 1024×1024 PNG | 4096×4096 PNG, `_4x.png` suffix |
|
||||
| `test_upscale_2x` | 1024×1024 PNG | 2048×2048 PNG, `_2x.png` suffix |
|
||||
| `test_invalid_path` | nonexistent path | Error TextContent returned |
|
||||
| `test_output_dir_override` | valid PNG + `output_dir=/tmp` | saved to /tmp |
|
||||
| `test_default_output_dir` | valid PNG, no output_dir | saved alongside input |
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- [ ] `4x-UltraSharp.pth` present in `~/ComfyUI/models/upscale_models/`
|
||||
- [ ] `upscale_image("path/to/1024.png", scale=4)` returns 4096×4096 PNG
|
||||
- [ ] Output file saved with `_4x.png` suffix
|
||||
- [ ] Inline base64 image returned for display in chat
|
||||
- [ ] All 5 test cases pass
|
||||
- [ ] No changes to existing `generate_image()` tests
|
||||
Reference in New Issue
Block a user