Fuuga.Image.cpu
1.0.15
dotnet add package Fuuga.Image.cpu --version 1.0.15
NuGet\Install-Package Fuuga.Image.cpu -Version 1.0.15
<PackageReference Include="Fuuga.Image.cpu" Version="1.0.15" />
<PackageVersion Include="Fuuga.Image.cpu" Version="1.0.15" />
<PackageReference Include="Fuuga.Image.cpu" />
paket add Fuuga.Image.cpu --version 1.0.15
#r "nuget: Fuuga.Image.cpu, 1.0.15"
#:package Fuuga.Image.cpu@1.0.15
#addin nuget:?package=Fuuga.Image.cpu&version=1.0.15
#tool nuget:?package=Fuuga.Image.cpu&version=1.0.15
Fuuga.Image
This is image generation MCP client for Fuuga (or other agents) to make the agent capable of producing images via other (Stable Diffusion) models from for example HuggingFace.
Standalone CLI tool for image generation and captioning, built on stable-diffusion.cpp and Phi-3.5-vision.
CPU vs CUDA
The compute backend is decided by which package you install, not by a runtime flag:
Fuuga.Image(NuGet) — CUDA natives (CUDA 12.8, win-x64; needs an NVIDIA GPU).Fuuga.Image.cpu(NuGet) — CPU natives, runs anywhere.
Building from source, the TorchBackend MSBuild property makes the same choice: dotnet build -p:TorchBackend=cuda for CUDA, plain dotnet build for the CPU default. The main library follows the identical split (Fuuga / Fuuga.cpu).
This applies to every subcommand: diffusion generation (stable-diffusion.cpp natives), captioning (ONNX Runtime GenAI natives), and the from-scratch pipeline (libtorch natives) all use CUDA in the CUDA package when a GPU is present, and CPU otherwise — no flags involved.
Commands
fuuga-image txt2img Generate an image from a text prompt
fuuga-image img2img Transform an image guided by a text prompt
fuuga-image txt2video Generate a video from a text prompt (video models only)
fuuga-image img2video Animate a still image into a video (video models only)
fuuga-image vid2vid Edit/continue a video guided by text (VACE; video models only)
fuuga-image caption Generate a text description of an image
fuuga-image scratch Generate with the from-scratch SD 1.5 implementation (see below)
fuuga-image info Display model metadata
Quick Start
dotnet build
# Text-to-image
dotnet run -- txt2img --model path/to/model.ckpt --prompt "a cat sitting on a windowsill" --steps 20
# Image-to-image
dotnet run -- img2img --model path/to/model.ckpt --source input.png --prompt "in watercolor style" --strength 0.75
# Text-to-video (requires a video model, e.g. a Wan2.1/2.2 T2V GGUF)
dotnet run -- txt2video --model path/to/wan-t2v.gguf --prompt "a wave crashing on rocks" --frames 16 --fps 8 --format mp4
# Image-to-video (animate a still)
dotnet run -- img2video --model path/to/wan-i2v.gguf --source photo.png --prompt "gentle zoom" --frames 24 --format gif
# Video-to-video (edit/continue a video guided by text; --source is a video file or a frames dir)
dotnet run -- vid2vid --model path/to/wan-vace.gguf --source clip.mp4 --prompt "make it snowy" --format mp4
# Attach an audio track to a generated mp4 (models don't make audio; this muxes a provided file)
dotnet run -- txt2video --model path/to/wan-t2v.gguf --prompt "a marching band" --format mp4 --audio track.mp3
# Caption an image
dotnet run -- caption --model path/to/vision-model/ --image photo.jpg --detail standard
# Caption a video (samples frames and describes them; --video is a file or a frames dir)
dotnet run -- caption --model path/to/vision-model/ --video clip.mp4 --frame-samples 8 --detail detailed
# Transcribe/describe audio (needs an audio-capable model, e.g. Phi-4-multimodal — same runtime)
dotnet run -- caption --model path/to/phi-4-multimodal/ --audio speech.wav
# Model info
dotnet run -- info --model path/to/model.ckpt
Common Options
| Option | Description | Default |
|---|---|---|
--model <path> |
Path to model file or directory | (required) |
--output <dir> |
Output directory | output |
--format <fmt> |
Output format: png or jpeg |
png |
--verbose |
Enable detailed logging | off |
txt2img Options
| Option | Description | Default |
|---|---|---|
--prompt <text> |
Text description of desired image | (required) |
--negative <text> |
What to avoid in the image | none |
--width <int> |
Image width in pixels (multiple of 8) | 512 |
--height <int> |
Image height in pixels (multiple of 8) | 512 |
--steps <int> |
Denoising steps | 20 |
--cfg <float> |
Classifier-free guidance scale | 7.0 |
--seed <int> |
Random seed (-1 for random) | -1 |
--sampler <name> |
euler, euler_a, heun, dpm2, dpm++2s, lcm |
euler_a |
--batch <int> |
Number of images to generate | 1 |
--vae <path> |
External VAE model | none |
--lora <path> |
LoRA weights | none |
--quantization <q> |
f32, f16, q8_0, q5_1, q4_0 |
f16 |
img2img Options
Same as txt2img, plus:
| Option | Description | Default |
|---|---|---|
--source <path> |
Source image to transform | (required) |
--strength <float> |
Denoising strength 0.0-1.0 | 0.75 |
caption Options
Captioning uses whatever multimodal model directory you pass to --model. Provide one of
--image (a still), --video (a video file decoded via ffmpeg, or a directory of frames, sampled
to --frame-samples), or --audio (transcribe/describe an audio clip). Audio needs an
audio-capable model such as Phi-4-multimodal — it runs on the same ONNX-Runtime-GenAI runtime
already bundled (no extra dependency), you just point --model at it.
| Option | Description | Default |
|---|---|---|
--image <path> |
Image to caption | (one of image/video/audio) |
--video <path> |
Video file or frames directory to describe | (one of image/video/audio) |
--audio <path> |
Audio file to transcribe/describe (audio-capable model) | (one of image/video/audio) |
--frame-samples <N> |
Frames sampled from the video | 8 |
--detail <level> |
brief, standard, detailed |
standard |
--max-tokens <int> |
Maximum tokens to generate | 256 |
--prompt <text> |
Custom prompt for image captioning (must contain <\|image_1\|>) |
none |
txt2video / img2video / vid2vid Options
Video is generated only when the loaded model supports it (e.g. Wan2.1/2.2 text-to-video,
image-to-video, or VACE GGUF models); the tool checks the model's capability and errors clearly
otherwise. img2video additionally takes --source <path> (the starting image); vid2vid takes
--source <path> (a video file or a directory of frame images) plus --vace-strength <f>.
Video models produce no audio. --audio <path> muxes a user-provided audio file into an
mp4/webp output via ffmpeg (ignored for gif / image sequences).
| Option | Description | Default |
|---|---|---|
--frames <int> |
Number of frames to generate | 16 |
--fps <int> |
Frames per second of the output | 8 |
--width / --height |
Frame size | 512 |
--steps / --cfg / --seed / --sampler |
Same as txt2img | |
--format <fmt> |
png (image sequence, default), mp4, gif, webp |
png |
Output formats. png writes an image sequence (frame_0000.png …) into a per-run
subdirectory — this is dependency-free and always available. mp4, gif, and webp are
encoded from those frames with ffmpeg, which is an optional external tool (see below). If a
single-file format is requested but ffmpeg is not found, the tool falls back to an image
sequence and prints a note.
Optional: ffmpeg for mp4 / gif / webp
ffmpeg is not a .NET dependency and is not bundled. To enable single-file video output, install it
and make sure it is on PATH (or point FUUGA_FFMPEG at the executable):
- Windows:
winget install Gyan.FFmpeg(orchoco install ffmpeg), or download from https://ffmpeg.org/download.html and add thebinfolder toPATH. - macOS:
brew install ffmpeg - Linux:
sudo apt install ffmpeg(Debian/Ubuntu) or your distro's package manager. - Explicit path: set
FUUGA_FFMPEG=/full/path/to/ffmpegto use a specific build.
Without ffmpeg, --format png (image sequence) still works and can be turned into a video later
with any tool.
From-Scratch Stable Diffusion (scratch)
The scratch subcommand runs a complete Stable Diffusion 1.5 implementation written
from first principles in F# on TorchSharp — no stable-diffusion.cpp, no Python. It
implements the CLIP text encoder, the VAE encoder/decoder, the latent-diffusion UNet
(with self/cross attention), CLIP's BPE tokenizer, and DDIM/DDPM samplers with
classifier-free guidance, and loads the standard SD v1.x single-file safetensors
checkpoint directly. It exists to be read, hacked, and extended (in the spirit of
"stable diffusion from scratch" tutorials) — use txt2img/img2img for fast
production generation.
You need two downloads:
- The checkpoint — an SD v1.x single-file
.safetensors, e.g.v1-5-pruned-emaonly-fp16.safetensors(2.1 GB; the fp32v1-5-pruned-emaonly.safetensorsalso works). - The CLIP tokenizer tables —
vocab.json+merges.txtfromopenai/clip-vit-large-patch14into one directory.
# Text-to-image
dotnet run -- scratch --model models/v1-5-pruned-emaonly-fp16.safetensors \
--clip-tokenizer models/clip-tokenizer \
--prompt "a photograph of an astronaut riding a horse" \
--steps 30 --cfg 7.5 --width 512 --height 512 --seed 42
# Image-to-image
dotnet run -- scratch --model models/v1-5-pruned-emaonly-fp16.safetensors \
--clip-tokenizer models/clip-tokenizer \
--prompt "in watercolor style" --image input.png --strength 0.7
| Option | Description | Default |
|---|---|---|
--model <path> |
SD v1.x checkpoint: single-file .safetensors or a stable-diffusion.cpp .gguf (F32/F16/Q4_0/Q8_0, dequantized on load) — the same GGUF txt2img uses works here |
(required) |
--clip-tokenizer <dir> |
Directory with CLIP vocab.json + merges.txt |
(required) |
--prompt / --negative |
Prompt / negative prompt | "" |
--steps <int> |
DDPM denoising steps | 30 |
--cfg <float> |
Classifier-free guidance scale | 7.5 |
--seed <int> |
Random seed | 42 |
--width / --height |
Output size (multiples of 8) | 512 |
--image <path> |
img2img source image | none (txt2img) |
--strength <float> |
img2img noise strength 0–1 | 0.8 |
--sampler <name> |
ddim (deterministic, converges in ~10–20 steps) or ddpm (ancestral; needs ~30+ steps) |
ddim |
--preview-every <N> |
Progressive rendering: write an approximate preview PNG every N steps (fast latent→RGB projection, no VAE decode) |
off |
Notes:
- The device follows the installed backend automatically: a CUDA build/package with a GPU present runs on CUDA, otherwise CPU. CPU generation is slow (the UNet runs in float32); a CUDA build is strongly recommended for full-size images. Small sizes are fine for CPU experiments — 64×64 runs in ~10 s and is ideal for pipeline smoke tests; 256×256 takes ~1–2 min.
- Any width/height that is a multiple of 8 works, including non-square aspect
ratios (e.g.
--width 128 --height 192). Quality is best at the model's native 512×512; far below it images become abstract, far above it content repeats. - Progressive rendering: pass
--preview-every Nto write intermediate preview PNGs during denoising, or consumeScratchProgress.PreviewPngfrom thegeneratecallback in library use. - Also exposed as the
fuuga_image_scratchMCP tool (via--mcp-server) and from the main Fuuga CLI asfuuga image --scratch <prompt> --model <safetensors> --clip-tokenizer <dir>. - Library usage from F# scripts: see
examples/scratch-sd-demo.fsx. - Fine-tuning:
ScratchSdPipeline.diffusionTrainingLossimplements the standard epsilon-prediction objective — noise VAE-encoded images to a random timestep and MSE-score the UNet's noise prediction. Pair it withtorch.optim.AdamWoverunet.parameters()to retrain, then export — train slow here, serve fast there. RL-style preference tuning (DDPO / reward-weighted) would build on the same loss and is not included. - Export:
ScratchSdWeights.exportSd15writes a single-file F16 safetensors;exportSd15Ggufwrites a stable-diffusion.cpp-compatible GGUF (F16 or Q8_0, ~1 byte/weight). Both are loadable byfuuga-image txt2img, ComfyUI, A1111, and back into this module (verified: sd.cpp generates from a scratch-exported Q8_0 GGUF). Formats deliberately NOT supported:.ckpt(Python pickle — unsafe to parse outside Python), diffusers multi-folder layout (different key naming; convert to single-file with any standard tool), and ONNX (TorchSharp has no graph tracer; ONNX SD serving is a different backend, not an export target). - GroupNorm is implemented from primitive ops (
ScratchGroupNorm): the fused TorchSharp/libtorch kernel on win-x64 CPU can return garbage in forward and silently zero input-gradients in backward, which would break training entirely. - Module names deliberately mirror the checkpoint key layout
(
model.diffusion_model.*,first_stage_model.*,cond_stage_model.transformer.*), so weight loading is a prefix strip instead of a mapping table — and the key names are unit-tested against the real SD 1.5 layout. - Source layout:
ScratchSdTokenizer.fs(CLIP BPE) →ScratchSdModels.fs(CLIP/VAE/UNet) →ScratchSdWeights.fs(safetensors reader + loader) →ScratchSdPipeline.fs(DDPM sampler + txt2img/img2img pipeline).
MCP Server Mode
Fuuga.Image can run as an MCP tool server over stdio, enabling integration with Fuuga LLM via the MCP protocol:
dotnet run -- --mcp-server
This exposes these tools via JSON-RPC 2.0 over stdin/stdout:
fuuga_image_txt2img-- text-to-image generationfuuga_image_img2img-- image-to-image transformationfuuga_image_txt2video-- text-to-video generation (video models only)fuuga_image_img2video-- image-to-video animation (video models only)fuuga_image_caption-- image captioning
See architecture.md Section 18 for the full integration design.
F# Script Demo
An interactive demo script shows how to use Fuuga.Image as an F# library — calling the modules directly with record-based configuration, validation, and I/O:
dotnet build Fuuga.Image
dotnet fsi examples/image-demo.fsx
The script runs without models (dry-run mode), exercising types, validation, image I/O, and prompt templates. Uncomment Section 5 and set your model paths for actual generation. See examples/image-demo.fsx for the full source.
Project Structure
Fuuga.Image/
Types.fs Layer 0: Domain types, error handling (ImageError DU)
Config.fs Layer 1: CLI argument parsing
ImageIO.fs Layer 2: Image load/save, format conversion, validation
VideoIO.fs Layer 2b: Video frame I/O (ffmpeg optional)
ScratchSdTokenizer.fs Layer 2c: CLIP BPE tokenizer (from-scratch SD)
ScratchSdModels.fs Layer 2c: CLIP/VAE/UNet architecture in TorchSharp
ScratchSdWeights.fs Layer 2c: safetensors reader + checkpoint loading
ScratchSdPipeline.fs Layer 2c: DDPM sampler + txt2img/img2img pipeline
Diffusion.fs Layer 3: Stable Diffusion model wrapper (txt2img, img2img)
Caption.fs Layer 4: Phi-3.5-vision captioning (ONNX Runtime GenAI)
McpServer.fs Layer 5: MCP JSON-RPC 2.0 server over stdio
Program.fs Layer 6: Entry point, subcommand routing, error handling
Architecture
- Image Extension Architecture -- standalone design
- Main Architecture Section 18 -- Fuuga LLM integration
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- FSharp.Core (>= 10.1.301)
- HPPH.SkiaSharp (>= 1.0.0)
- libtorch-cpu (>= 2.10.0)
- Microsoft.ML.OnnxRuntimeGenAI (>= 0.14.1)
- StableDiffusion.NET (>= 7.0.0)
- StableDiffusion.NET.Backend.Cpu (>= 7.0.0)
- TorchSharp (>= 0.107.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Backend-selection release. The compute backend is now decided entirely by which package you install — Fuuga.Image (CUDA) vs Fuuga.Image.cpu (CPU) — across every subcommand: diffusion generation (stable-diffusion.cpp), captioning (ONNX Runtime GenAI), and the from-scratch pipeline (libtorch). The CUDA package now ships Microsoft.ML.OnnxRuntimeGenAI.Cuda so captioning genuinely uses the GPU when present. The misleading per-command --gpu flag has been removed from all help text and docs (it is still parsed for script compatibility but has no effect for diffusion; the scratch and caption paths auto-select CUDA when the CUDA build detects a GPU).