Changes
diff --git a/designs/design-0-zimage.md b/designs/design-0-zimage.md
new file mode 100644
index 0000000..7d4cb1b
--- /dev/null
+++ b/designs/design-0-zimage.md
@@ -0,0 +1,905 @@
+# Standalone Z-Image Turbo CLI Design
+
+## Purpose
+
+This document describes the first implementation milestone for this
+repository: a standalone Python command line program that generates one
+non-gibberish image from local Z-Image Turbo weights.
+
+The goal is intentionally narrower than full ComfyUI parity. The first
+working version only needs to prove that the project can load the local
+model stack, run text-to-image inference without importing ComfyUI at
+runtime, decode the latent, and write a PNG. LoRA support is explicitly
+out of scope for this milestone.
+
+The ComfyUI workflow is still the reference for model filenames, image
+size, sampler defaults, prompt handling, and generation parameters. The
+runtime program must not depend on ComfyUI as a package or import from
+ComfyUI modules during generation.
+
+## Requirements
+
+The product requirements in `prd.md` say the tool must:
+
+- Be a Python CLI.
+- Generate images using the local Z-Image Turbo model stack referenced
+ by `~/programs/ComfyUI/user/default/workflows/Z-image Turbo.json`.
+- Avoid any runtime dependency on ComfyUI.
+- Use the ComfyUI workflow only as a reference.
+- Avoid downloading model files or other files from the internet.
+- Assume model files are already local.
+- Use `uv` for dependency and virtual environment management.
+- Support prompt, negative prompt, seed, width, height, batch size,
+ steps, CFG, output path, and device options.
+- First prove one image can be generated from local weights without
+ importing ComfyUI.
+
+The current user-level clarification narrows the milestone:
+
+- The first output only needs to be an image that is not gibberish.
+- LoRA support should be skipped for now.
+- ComfyUI source should be used as implementation reference material.
+
+## Reference Workflow
+
+The workflow file is:
+
+`/home/mw/programs/ComfyUI/user/default/workflows/Z-image Turbo.json`
+
+The active path in that graph uses these nodes:
+
+- `UNETLoader`
+- `CLIPLoader`
+- `VAELoader`
+- `ModelSamplingAuraFlow`
+- `CLIPTextEncode`
+- `EmptyLatentImage`
+- `KSampler`
+- `VAEDecode`
+- `SaveImage`
+
+The workflow also contains LoRA nodes, but this design ignores them for
+the first milestone.
+
+### Model Files
+
+ComfyUI resolves models through:
+
+`/home/mw/programs/ComfyUI/extra_model_paths.yaml`
+
+That file maps the default model root to:
+
+`/home/mw/documents/ai/diffusion/models`
+
+For the first milestone, the CLI should use this model root by default
+and allow it to be overridden with a command line option.
+
+Required files:
+
+| Role | Relative Path |
+| --- | --- |
+| Diffusion model | `diffusion_models/z-image_turbo/moodyPornMix_zitV3.safetensors` |
+| Text encoder | `text_encoders/z-image_turbo/qwen_3_4b.safetensors` |
+| VAE | `vae/z-image_turbo/ae.safetensors` |
+
+The workflow also references an all-in-one checkpoint:
+
+`checkpoints/z-image_turbo/redcraftRedzimageUpdatedDEC03_redzimage15AIO.safetensors`
+
+That checkpoint node is not connected to the active generation path, so
+the CLI should not use it in the first milestone.
+
+### Generation Defaults
+
+The first CLI defaults should match the active workflow:
+
+| Setting | Value |
+| --- | --- |
+| Width | `832` |
+| Height | `1248` |
+| Batch size | `1` |
+| Steps | `10` |
+| CFG | `1.0` |
+| Sampler | `seeds_3` |
+| Scheduler | `beta` |
+| Denoise | `1.0` |
+| Model sampling shift | `3.0` |
+| Positive prompt | User supplied |
+| Negative prompt | `text, watermark, full-body` |
+
+## Important ComfyUI Findings
+
+This section records the ComfyUI source behavior that matters for the
+standalone implementation.
+
+### Z-Image Model Configuration
+
+ComfyUI defines `ZImage` in:
+
+`/home/mw/programs/ComfyUI/comfy/supported_models.py`
+
+The relevant behavior is:
+
+- Z-Image is a `Lumina2` model variant.
+- Its `unet_config` contains `image_model = "lumina2"` and `dim = 3840`.
+- Its sampling settings are `multiplier = 1.0` and `shift = 3.0`.
+- Its supported inference dtypes are `bfloat16` and `float32`, with
+ optional `float16` only when ComfyUI detects extended fp16 support.
+- It uses the same latent format as `Lumina2`, which is `Flux`.
+
+The standalone implementation should run on CUDA only for the first
+milestone. It should start with `bfloat16` if the selected CUDA device
+supports it, then fall back to `float32` if needed. This choice is
+conservative because ComfyUI lists bf16/fp32 as supported.
+
+### Text Encoder
+
+ComfyUI defines Z-Image text encoding in:
+
+`/home/mw/programs/ComfyUI/comfy/text_encoders/z_image.py`
+
+The relevant behavior is:
+
+- The tokenizer is based on `Qwen2Tokenizer` from `transformers`.
+- The text encoder model is `Qwen3_4B`.
+- The prompt is wrapped with a chat-style template before tokenization:
+
+```text
+<|im_start|>user
+{prompt}<|im_end|>
+<|im_start|>assistant
+```
+
+- Token weighting is disabled for this tokenizer path.
+- The model returns attention masks.
+- The default selected hidden layer is `-2`.
+
+The standalone implementation must reproduce the prompt template. If it
+does not, the model can receive text embeddings in a distribution it was
+not trained to use, which is a common cause of visually incoherent
+images.
+
+### Latent Format
+
+ComfyUI's `Lumina2` model uses `latent_formats.Flux`.
+
+That means:
+
+- The diffusion latent has 16 channels.
+- The spatial downscale ratio is 8.
+- A latent for `832x1248` should have shape:
+
+```text
+[batch_size, 16, 156, 104]
+```
+
+The order is `[N, C, H, W]`. The latent height is image height divided
+by 8. The latent width is image width divided by 8.
+
+The `Flux` latent format also applies scaling when entering and leaving
+the diffusion model. The implementation must port the exact scale,
+shift, or mean/std logic from ComfyUI's `latent_formats.Flux` class
+rather than assuming vanilla Stable Diffusion VAE scaling.
+
+### Model Sampling
+
+The workflow node `ModelSamplingAuraFlow` calls the same patch method as
+ComfyUI's SD3-style model sampling node, but with:
+
+```text
+shift = 3.0
+multiplier = 1.0
+```
+
+The implementation maps to ComfyUI's `ModelSamplingDiscreteFlow` in:
+
+`/home/mw/programs/ComfyUI/comfy/model_sampling.py`
+
+The important methods are:
+
+```python
+def time_snr_shift(alpha, t):
+ if alpha == 1.0:
+ return t
+ return alpha * t / (1 + (alpha - 1) * t)
+
+def sigma(timestep):
+ return time_snr_shift(shift, timestep / multiplier)
+
+def timestep(sigma):
+ return sigma * multiplier
+```
+
+For this workflow, `multiplier` is `1.0`, so the sigma schedule is a
+flow schedule in the `[0, 1]` range shifted by `shift = 3.0`.
+
+### Scheduler
+
+The workflow scheduler is `beta`.
+
+ComfyUI implements this in:
+
+`/home/mw/programs/ComfyUI/comfy/samplers.py`
+
+Conceptually:
+
+1. Build the model sampling object.
+2. Read its `sigmas` table.
+3. Generate `steps` values using a beta distribution percent point
+ function with alpha `0.6` and beta `0.6`.
+4. Convert those values into timestep indices.
+5. Deduplicate adjacent repeated timesteps.
+6. Append a final zero sigma.
+
+This requires `scipy.stats.beta.ppf`, so `scipy` should be a dependency
+unless the beta PPF is reimplemented. Using SciPy is simpler and less
+risky for the first milestone.
+
+### Sampler
+
+The workflow sampler is `seeds_3`.
+
+ComfyUI implements `sample_seeds_3` in:
+
+`/home/mw/programs/ComfyUI/comfy/k_diffusion/sampling.py`
+
+SEEDS-3 is a three-stage stochastic solver. At each sigma interval, it:
+
+1. Calls the denoising model at the current latent.
+2. Converts sigma to half-log-SNR.
+3. Builds two intermediate points at one-third and two-thirds through
+ the interval.
+4. Calls the denoising model at each intermediate point.
+5. Combines the denoised predictions into the next latent.
+6. Optionally injects stochastic noise when `eta` and `s_noise` are
+ positive.
+
+The sampler depends on model-specific conversions between sigma and
+half-log-SNR. Those conversions need to be ported together with the
+sampler math. Porting only the visible loop is not enough.
+
+For the first milestone, the implementation may expose `--sampler
+seeds_3` as the default but keep the sampler code private and minimal.
+It does not need every sampler ComfyUI supports.
+
+### VAE Decode
+
+ComfyUI's `VAE` wrapper is in:
+
+`/home/mw/programs/ComfyUI/comfy/sd.py`
+
+The active VAE file is:
+
+`vae/z-image_turbo/ae.safetensors`
+
+The VAE loader detects the VAE architecture from state dict keys. For
+Z-Image Turbo, the first implementation should inspect the `ae` state
+dict and choose the exact matching VAE class or a compatible local
+implementation. It should not assume SD 1.x VAE layout unless the keys
+prove that layout.
+
+The decode output should be converted from model output range to image
+range, then written as a PNG:
+
+1. Model output tensor is expected in `[-1, 1]`.
+2. Convert to `[0, 1]` with `(image + 1) / 2`.
+3. Clamp to `[0, 1]`.
+4. Convert to `uint8`.
+5. Save with Pillow.
+
+This is the default ComfyUI postprocess path for image VAEs.
+
+## Architecture
+
+The CLI should be organized as a small Python package. The package
+should isolate command parsing, configuration, model loading, sampling,
+and image saving so that each part can be tested independently.
+
+Proposed file layout:
+
+```text
+diffusion_cli/
+ __init__.py
+ cli.py
+ config.py
+ errors.py
+ image_io.py
+ paths.py
+ sampling.py
+ text_encoder.py
+ zimage_model.py
+ vae.py
+tests/
+ test_paths.py
+ test_sampling.py
+ test_cli.py
+pyproject.toml
+README.md
+```
+
+### `cli.py`
+
+This module owns the command line interface.
+
+It should use Python's standard `argparse` library. `argparse` is part
+of the Python standard library and is designed for command line option
+parsing, so it avoids adding a CLI framework dependency. See the
+[argparse documentation](https://docs.python.org/3/library/argparse.html).
+
+The command should be exposed through a `pyproject.toml` console script:
+
+```toml
+[project.scripts]
+diffusion-cli = "diffusion_cli.cli:main"
+```
+
+The CLI should accept:
+
+| Option | Type | Default | Meaning |
+| --- | --- | --- | --- |
+| `--prompt` | string | required | Positive prompt. |
+| `--negative-prompt` | string | workflow default | Negative prompt. |
+| `--seed` | int | random if absent | Noise seed. |
+| `--width` | int | `832` | Output width. |
+| `--height` | int | `1248` | Output height. |
+| `--batch-size` | int | `1` | Number of images. |
+| `--steps` | int | `10` | Denoising steps. |
+| `--cfg` | float | `1.0` | Classifier-free guidance scale. |
+| `--output` | path | `output.png` | Output file path. |
+| `--device` | string | `cuda` | CUDA device id or alias. |
+| `--model-root` | path | local model root | Model root directory. |
+| `--tokenizer-path` | path | required | Qwen tokenizer directory. |
+| `--dtype` | string | `auto` | `auto`, `bf16`, `fp32`. |
+
+Validation rules:
+
+- `width` and `height` must be positive multiples of 8.
+- `batch-size` must be at least 1.
+- `steps` must be at least 1.
+- `cfg` must be non-negative.
+- `output` parent directory must exist or be creatable.
+- The first milestone is CUDA-only. CPU execution should fail with a
+ clear unsupported-device error rather than attempting a very slow run.
+- `device=cuda` must fail clearly if CUDA is unavailable.
+- `dtype=bf16` must fail clearly if unsupported on the selected device.
+- `tokenizer-path` must exist and contain the tokenizer files required
+ by `transformers`, including `vocab.json`, `merges.txt`, and
+ `tokenizer_config.json`.
+
+### `config.py`
+
+This module should hold immutable configuration objects.
+
+Use dataclasses:
+
+```python
+@dataclass(frozen=True)
+class ModelFiles:
+ diffusion_model: Path
+ text_encoder: Path
+ vae: Path
+
+@dataclass(frozen=True)
+class GenerationConfig:
+ prompt: str
+ negative_prompt: str
+ seed: int
+ width: int
+ height: int
+ batch_size: int
+ steps: int
+ cfg: float
+ device: torch.device
+ dtype: torch.dtype
+ output: Path
+```
+
+The value object should not load models. It only records the user's
+intent after validation. Keeping configuration separate from loading
+makes errors easier to test.
+
+### `paths.py`
+
+This module should resolve model paths.
+
+Default model root:
+
+```text
+/home/mw/documents/ai/diffusion/models
+```
+
+Resolution flow:
+
+1. Read `--model-root` if provided.
+2. Otherwise read an environment variable such as
+ `DIFFUSION_CLI_MODEL_ROOT`.
+3. Otherwise use the default root above.
+4. Join the required relative model paths.
+5. Check each path exists and is a regular file.
+6. Return a `ModelFiles` instance.
+
+The implementation should not parse ComfyUI's YAML config at runtime.
+Parsing it would make the new tool's behavior depend on a ComfyUI
+installation. The default root can be documented and overridden.
+
+### `zimage_model.py`
+
+This module should own the diffusion model architecture and state dict
+loading.
+
+The first milestone has two viable implementation strategies:
+
+#### Strategy A: Minimal Local Port
+
+Port the specific Z-Image/Lumina2 model pieces needed by this workflow:
+
+- Lumina2/NextDiT architecture.
+- Z-Image state dict key mapping if needed.
+- Model forward pass for text-to-image.
+- Flux latent format scaling.
+- DiscreteFlow model sampling.
+
+This gives maximum control and avoids requiring upstream `diffusers`
+support that may not exist or may not match ComfyUI's checkpoint format.
+
+The downside is implementation time. The architecture is not tiny.
+
+#### Strategy B: External Pipeline If Available
+
+Use a library implementation only if it can load the exact local
+safetensors files without downloading and without ComfyUI imports.
+
+This should be investigated during implementation, but the design should
+not rely on it. Current local evidence shows ComfyUI has custom Z-Image
+support, and the public Diffusers documentation does not obviously
+document a Z-Image pipeline.
+
+The first implementation should therefore plan for Strategy A, while
+keeping the loader boundary narrow enough that Strategy B can replace it
+later.
+
+### `text_encoder.py`
+
+This module should load the Qwen3-4B text encoder and produce text
+conditioning tensors.
+
+Required behavior:
+
+1. Load `qwen_3_4b.safetensors` with `safetensors`.
+2. Instantiate the Qwen3-4B architecture.
+3. Use the tokenizer vocabulary expected by ComfyUI's
+ `qwen25_tokenizer`.
+4. Wrap prompts with the Z-Image chat template.
+5. Tokenize without weighted prompt syntax.
+6. Run the text encoder under `torch.inference_mode`.
+7. Select the `-2` hidden layer.
+8. Return hidden states and attention masks in the format expected by
+ the diffusion model.
+
+Using `torch.inference_mode` is appropriate because generation does not
+need gradients. PyTorch documents it as a context manager for inference
+workloads that disables autograd-related overhead:
+[torch.inference_mode](https://docs.pytorch.org/docs/stable/generated/torch.inference_mode.html).
+
+### `sampling.py`
+
+This module should contain only the sampler and scheduler needed for the
+first image.
+
+Components:
+
+- `ModelSamplingDiscreteFlow`
+- `betaScheduler`
+- sigma/log-SNR conversion helpers
+- `sampleSeeds3`
+
+The implementation should use names that follow the project style:
+
+- Public classes in `CapCase`.
+- Functions in `camelCase` when multi-word.
+- Variables in `snake_case`.
+
+The sampling flow should be:
+
+1. Build initial Gaussian noise with shape
+ `[batch_size, 16, height // 8, width // 8]`.
+2. Use a seeded generator to create deterministic initial noise on the
+ selected CUDA device.
+3. Build an empty latent image of zeros with the same shape.
+4. Build the shifted flow sigma schedule with shift `3.0`.
+5. Build beta scheduler sigmas for `steps`.
+6. Run SEEDS-3 over those sigmas.
+7. Return the final latent.
+
+CFG behavior:
+
+- At `cfg = 1.0`, conditional and unconditional guidance should reduce
+ to the positive prediction.
+- Even though CFG is 1.0 by default, the CLI should still compute the
+ negative prompt path unless a later profiling pass proves it can be
+ skipped safely.
+- Keeping the negative path initially makes the code match the workflow
+ structure and avoids special cases.
+
+### `vae.py`
+
+This module should load and decode the VAE.
+
+Required behavior:
+
+1. Load `ae.safetensors`.
+2. Detect the architecture from state dict keys.
+3. Instantiate the matching VAE decoder.
+4. Load weights.
+5. Move to selected device and dtype.
+6. Decode final latent.
+7. Return an image tensor in `[0, 1]`.
+
+The first milestone only needs decoding. Encoding can be omitted.
+
+### `image_io.py`
+
+This module should save image tensors as PNG.
+
+Input tensor:
+
+```text
+[batch_size, 3, height, width]
+```
+
+Save behavior:
+
+- If `batch_size == 1`, write exactly the requested output path.
+- If `batch_size > 1`, append an index before the suffix:
+ `output_0000.png`, `output_0001.png`, and so on.
+- Convert tensor layout from `NCHW` to `NHWC`.
+- Clamp before converting to `uint8`.
+
+## Dependency Plan
+
+Use `uv` as the project manager. The official uv documentation describes
+uv as a Python project and package manager with lockfile support:
+[uv documentation](https://docs.astral.sh/uv/).
+
+Initial dependencies:
+
+- `torch`
+- `safetensors`
+- `transformers`
+- `numpy`
+- `scipy`
+- `pillow`
+- `tqdm`
+
+Potential dependencies:
+
+- `einops`, if the local model port needs shape rearrangement helpers.
+- `accelerate`, only if memory management becomes difficult.
+
+Do not add `diffusers` unless implementation research proves it can load
+the exact local Z-Image Turbo files without downloads and without format
+conversion. Adding a large dependency without using it would obscure the
+actual first milestone.
+
+Use `safetensors` for all three model files. Hugging Face documents
+Safetensors as a safe tensor storage format and shows `safe_open` for
+loading tensors:
+[Safetensors documentation](https://huggingface.co/docs/safetensors/index).
+
+## Licensing
+
+ComfyUI source files inspected for this design include GPL license text.
+For this first milestone, licensing is not a blocker. The project owner
+has explicitly allowed copying ComfyUI code directly if that is the
+fastest way to generate the first image.
+
+The runtime constraint still matters: copied or adapted code may live in
+this repository, but the CLI should not import ComfyUI as an installed
+application or require a working ComfyUI runtime. This keeps the
+milestone aligned with the product requirement that the tool stand
+alone.
+
+## Implementation Plan
+
+### Phase 1: Project Scaffolding
+
+Create:
+
+- `pyproject.toml`
+- `diffusion_cli/`
+- `tests/`
+- minimal `README.md`
+
+Add a console script named `diffusion-cli`.
+
+Success criteria:
+
+- `uv run diffusion-cli --help` works.
+- CLI arguments are visible and have correct defaults.
+- No model loading happens during `--help`.
+
+### Phase 2: Path and Config Validation
+
+Implement model path resolution and config validation.
+
+Success criteria:
+
+- Missing model files produce a direct error listing the missing path.
+- Invalid dimensions produce a direct error.
+- Output path validation catches unwritable or invalid paths early.
+
+### Phase 3: Tensor Loading Probe
+
+Implement safetensors loading utilities and a diagnostic command or
+internal debug function that prints:
+
+- number of tensors,
+- top-level key patterns,
+- detected dtype summary,
+- selected architecture guess.
+
+Success criteria:
+
+- The diffusion model, text encoder, and VAE files can be inspected
+ without running full inference.
+- The program can identify the expected Z-Image/Qwen/VAE key patterns.
+- No download attempt occurs.
+
+### Phase 4: Text Encoder
+
+Implement prompt templating and Qwen3-4B conditioning.
+
+Success criteria:
+
+- A short prompt produces a conditioning tensor.
+- The conditioning tensor has plausible shape.
+- Positive and negative prompt encodings both complete.
+- Encoding runs under `torch.inference_mode`.
+
+### Phase 5: Diffusion Model
+
+Implement or integrate the Z-Image/Lumina2 diffusion model.
+
+Success criteria:
+
+- The model loads the local diffusion state dict.
+- A single forward pass accepts:
+ - latent noise,
+ - sigma/timestep,
+ - text conditioning,
+ - attention mask,
+ - token count.
+- The forward output shape matches the latent shape.
+- No NaN or Inf values appear in the output.
+
+### Phase 6: Scheduler and Sampler
+
+Implement beta scheduler and SEEDS-3.
+
+Success criteria:
+
+- Sigma schedule starts at the expected high-noise value and ends at
+ zero.
+- The sampler runs for 10 steps.
+- The latent remains finite.
+- The implementation can run with `cfg=1.0`.
+
+### Phase 7: VAE Decode and PNG Save
+
+Implement VAE decode and image saving.
+
+Success criteria:
+
+- Final decoded tensor has shape `[1, 3, 1248, 832]`.
+- Pixel range after postprocess is `[0, 1]`.
+- Output PNG exists.
+- The image is not obvious random noise or severe gibberish.
+
+## Runtime Flow
+
+The final command should look like:
+
+```bash
+uv run diffusion-cli \
+ --prompt "a cinematic photo of a red apple on a wooden table" \
+ --output output.png
+```
+
+Detailed execution:
+
+1. Parse CLI arguments.
+2. Validate generation config.
+3. Resolve model file paths.
+4. Select device and dtype.
+5. Load text encoder.
+6. Encode positive prompt.
+7. Encode negative prompt.
+8. Load diffusion model.
+9. Build initial latent noise.
+10. Run sampler.
+11. Load VAE.
+12. Decode latent to image.
+13. Save PNG.
+14. Print the output path.
+
+The implementation may load VAE before sampling if that is simpler, but
+sampling first can reduce peak memory if the text encoder or diffusion
+model can be unloaded before VAE decode. The first milestone should
+favor clarity over aggressive memory management.
+
+## Error Handling
+
+Errors should be direct and actionable.
+
+Examples:
+
+- `Missing diffusion model: /path/to/file.safetensors`
+- `Width must be a positive multiple of 8: got 830`
+- `CUDA requested but torch.cuda.is_available() is false`
+- `bf16 requested but selected device does not support bf16`
+- `Unsupported VAE state dict: no known decoder keys found`
+
+Do not print Python tracebacks for expected user errors. Raise custom
+exceptions internally and catch them in `cli.main`.
+
+Unexpected programming errors may still show tracebacks. That is useful
+during this early milestone.
+
+## Testing Strategy
+
+Unit tests should cover the parts that do not require loading the full
+models:
+
+- CLI parsing.
+- Path resolution.
+- Dimension validation.
+- Batch output naming.
+- Beta scheduler shape and final zero sigma.
+- Deterministic seed noise shape.
+
+Integration tests with the full model should be manual at first because
+the model files are large and local to this machine.
+
+Manual test command:
+
+```bash
+uv run diffusion-cli \
+ --prompt "a simple studio photo of a ceramic mug" \
+ --negative-prompt "text, watermark, full-body" \
+ --seed 1234 \
+ --steps 10 \
+ --cfg 1 \
+ --width 832 \
+ --height 1248 \
+ --output test_output.png
+```
+
+Manual acceptance checklist:
+
+- Program exits with code 0.
+- Output file exists.
+- Output dimensions are `832x1248`.
+- Image has recognizable structure.
+- Image is not pure noise, blank, all black, all white, or tiled garbage.
+- Re-running with the same seed produces the same output on the same
+ device and dtype.
+
+## Known Risks
+
+### Model Architecture Complexity
+
+The Z-Image/Lumina2 model architecture is the biggest risk. The CLI
+surface is easy; model execution is the hard part.
+
+Mitigation:
+
+- Start by loading state dict keys and matching them to ComfyUI's
+ detection path.
+- Port only the architecture pieces needed for this exact model.
+- Validate one forward pass before implementing the full sampler.
+
+### Tokenizer Assets
+
+ComfyUI's tokenizer path is local to its source tree:
+
+`comfy/text_encoders/qwen25_tokenizer`
+
+On this machine that directory contains the tokenizer assets:
+
+- `vocab.json`
+- `merges.txt`
+- `tokenizer_config.json`
+
+The `.safetensors` text encoder file contains model weights, not the
+tokenizer vocabulary. The standalone CLI must therefore receive a
+tokenizer directory separately.
+
+Because the PRD says not to download files, the implementation must not
+fetch tokenizer files from Hugging Face at runtime.
+
+Recommended first approach:
+
+- Add `--tokenizer-path`.
+- Require the user to specify the tokenizer directory for now.
+- Validate that the directory contains `vocab.json`, `merges.txt`, and
+ `tokenizer_config.json`.
+- Later copy the Qwen tokenizer assets out of ComfyUI into this
+ repository so the CLI is more self-contained.
+
+### Memory
+
+The workflow uses a large diffusion model and a Qwen3-4B text encoder.
+Running all components on GPU at once may exceed VRAM.
+
+Mitigation:
+
+- Load and run the text encoder first.
+- Move text encoder back to CPU or delete it before loading diffusion.
+- Decode VAE after sampling.
+- Keep batch size default at 1.
+- Prefer bf16 on CUDA.
+- Do not support CPU execution in the first milestone. If the selected
+ device is not CUDA, fail early with a clear message.
+
+### Exact ComfyUI Parity
+
+The first milestone does not require exact bitwise or visual parity with
+ComfyUI. It only requires a coherent image.
+
+Mitigation:
+
+- Match the highest-impact pieces first:
+ - prompt template,
+ - latent shape,
+ - model sampling shift,
+ - scheduler,
+ - sampler,
+ - VAE decode.
+- Defer LoRA, UI workflow parity, and multi-sampler support.
+
+## Out Of Scope
+
+The first milestone should not include:
+
+- LoRA loading.
+- ComfyUI workflow execution.
+- Image-to-image.
+- ControlNet.
+- Multiple samplers.
+- Multiple schedulers.
+- Model downloads.
+- Web UI.
+- Exact ComfyUI output parity.
+- Prompt weighting syntax.
+
+## Open Questions
+
+No blocking open questions remain for the first milestone.
+
+Resolved decisions:
+
+- Tokenizer assets: require `--tokenizer-path` for now. Later, copy the
+ Qwen tokenizer assets out of ComfyUI into this repository.
+- Licensing: do not block on licensing for the first milestone. Copy
+ ComfyUI code directly if needed.
+- Device support: CUDA-only for the first milestone.
+- Diffusion model: hard-code
+ `diffusion_models/z-image_turbo/moodyPornMix_zitV3.safetensors` for
+ the first image proof.
+- All-in-one checkpoints: out of scope for the first milestone, but the
+ design should leave room to support them later.
+
+## Recommended Next Step
+
+Start with scaffolding and model inspection, not the full sampler.
+
+The first concrete implementation task should create:
+
+- `pyproject.toml`
+- CLI argument parsing
+- path resolution
+- safetensors inspection for the three model files
+
+That gives immediate feedback about local model compatibility and avoids
+spending time on sampler code before confirming the exact state dict
+formats.
diff --git a/prd.md b/prd.md
new file mode 100644
index 0000000..a42b0a0
--- /dev/null
+++ b/prd.md
@@ -0,0 +1,16 @@
+# A standalone command line tool to run Z-Image Turbo
+
+- A Python CLI program that generates images using the local Z-Image
+ Turbo model stack referenced by:
+ `~/programs/ComfyUI/user/default/workflows/Z-image Turbo.json`
+- The program must not depend on ComfyUI at runtime.
+- The ComfyUI workflow is only a reference for model filenames, LoRA
+ configuration, sampler defaults, image size, prompt fields, and
+ generation parameters.
+- Do not download model files or any other files from the internet.
+ Assume all model files are stored locally.
+- Use uv to manage virtual env and dependencies.
+- The CLI should support prompt, negative prompt, seed, width, height,
+ batch size, steps, cfg, output path, and device options.
+- The first implementation milestone is proving a single image can be
+ generated from local weights without importing ComfyUI.