Files
ImaGen/internal/backend/mock.go
mAi 237270b204 mAi: #211 - bootstrap ImaGen framework skeleton
First step of the model-agnostic image-generation framework. Lands the
plumbing other components (skill, ComfyUI/Replicate adapters, agents)
will plug into:

- internal/backend: Backend interface (Request/Result), thread-safe
  Registry with init-time Register, plus a Mock reference adapter that
  emits a deterministic gradient PNG for smoke tests.
- internal/config: YAML loader for ~/.config/imagen.yaml. Framework owns
  default_backend + output settings + a per-backend block; each adapter
  owns the schema below its own block via BackendSpec.Raw.
- internal/output: filename templating ({date}/{time}/{slug}/{seed}/
  {backend}/{ext}), JSON metadata sidecar, --output override path.
- internal/prompt: embedded styles.yaml, style-preset suffix application.
- internal/server: 501 stub — HTTP surface lands in a follow-up issue.
- cmd/imagen: generate / backends / config (init|validate|path) / serve
  / version subcommands. Stdlib-only flag parsing with a small helper to
  honour positional prompt args ahead of flags (matches the issue spec).
- Tests for output (slug, naming template, sidecar), backend (mock PNG
  validity + determinism, registry build + duplicate panic), config
  (round-trip + validation), prompt (style apply + unknown-style error).
- CLAUDE.md, README.md, docs/architecture.md, docs/usage.md, Makefile.

Acceptance criteria from #211:
1. go build ./... — clean
2. imagen backends — lists registered backends, exits 0
3. imagen generate "test prompt" --backend mock --output /tmp/x.png —
   writes a 1024x1024 PNG plus an x.png.json sidecar
4. imagen config init | imagen config validate — round-trips cleanly
5. CLAUDE.md "Adding a new adapter" — six-step recipe
2026-05-08 14:37:05 +02:00

117 lines
2.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package backend
import (
"bytes"
"context"
"crypto/sha256"
"encoding/binary"
"fmt"
"image"
"image/color"
"image/png"
"io"
"math/rand"
"time"
)
// Mock is a deterministic image generator used for tests, smoke checks and as
// the reference implementation of the Backend contract. Same seed + same prompt
// always yields the same PNG. It does not call the network.
type Mock struct {
instance string
}
// NewMock builds a Mock backend. cfg is accepted for symmetry with real
// adapters but is ignored.
func NewMock(name string, _ map[string]any) (Backend, error) {
if name == "" {
name = "mock"
}
return &Mock{instance: name}, nil
}
// Name returns the user-facing instance name.
func (m *Mock) Name() string { return m.instance }
// Generate paints a deterministic gradient sized to req.Width×req.Height and
// returns it as a PNG. Width/Height default to 256 when zero.
func (m *Mock) Generate(ctx context.Context, req Request) (*Result, error) {
w, h := req.Width, req.Height
if w == 0 {
w = 256
}
if h == 0 {
h = 256
}
if w < 1 || h < 1 || w > 8192 || h > 8192 {
return nil, fmt.Errorf("mock: invalid size %dx%d", w, h)
}
seed := req.Seed
if seed == 0 {
seed = derivedSeed(req.Prompt)
}
rng := rand.New(rand.NewSource(seed))
baseR := uint8(rng.Intn(256))
baseG := uint8(rng.Intn(256))
baseB := uint8(rng.Intn(256))
start := time.Now()
img := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
for x := 0; x < w; x++ {
fx := float64(x) / float64(w)
fy := float64(y) / float64(h)
img.Set(x, y, color.RGBA{
R: blend(baseR, fx),
G: blend(baseG, fy),
B: blend(baseB, (fx+fy)/2),
A: 255,
})
}
}
var buf bytes.Buffer
if err := png.Encode(&buf, img); err != nil {
return nil, fmt.Errorf("mock: encode png: %w", err)
}
return &Result{
ImageReader: io.NopCloser(&buf),
MimeType: "image/png",
Metadata: map[string]any{
"backend": m.instance,
"backend_type": "mock",
"seed": seed,
"width": w,
"height": h,
"latency_ms": time.Since(start).Milliseconds(),
},
}, nil
}
func blend(base uint8, f float64) uint8 {
v := float64(base) + (255-float64(base))*f
if v < 0 {
v = 0
}
if v > 255 {
v = 255
}
return uint8(v)
}
// derivedSeed produces a stable int64 seed from a prompt so tests are
// reproducible without forcing the caller to pick one.
func derivedSeed(prompt string) int64 {
sum := sha256.Sum256([]byte(prompt))
return int64(binary.BigEndian.Uint64(sum[:8]) >> 1)
}
func init() {
Register("mock", NewMock)
}