Files
majordomo/meshgen/meshgen.go
T
steveandClaude Fable 5 fd8d56c3c1
Gadfly review (reusable) / review (pull_request) Successful in 15s
Adversarial Review (Gadfly) / review (pull_request) Successful in 15s
CI / Tidy (pull_request) Successful in 9m26s
CI / Build & Test (pull_request) Successful in 10m0s
fix: live-API corrections — music result shapes, mesh format honesty, mesh conversion
Round 2 from live smokes on netherstorm (2026-07-14):

- ACE-Step result blob is an ARRAY of objects and carries RAW control
  characters inside string values (literal newlines) — strict JSON
  rejected it. parseMusicResult sanitizes control chars (only legal
  inside string values in the double-encoded blob) and accepts array or
  object shapes. Regression test uses the live payload shape.
- Hunyuan3D GenerationRequest has NO output-format field (the documented
  type param is fiction) — it always returns GLB. Results are now
  labelled by sniffed magic bytes, never by the requested format.
- NEW meshgen.Converter/ConverterProvider optional surface + llamaswap
  impl over the mediautils shim POST /v1/convert_mesh — the STL hop for
  the printer pipeline.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-14 12:22:06 -04:00

173 lines
5.8 KiB
Go

// Package meshgen is majordomo's canonical image-to-3D surface. Like
// imagegen/audio/videogen, it is a deliberately separate leaf contract from
// the llm package: mesh generation shares none of the chat machinery, so it
// gets its own small Provider/Model interfaces (ADR-0020, following the
// ADR-0016→0019 lineage: functional options, zero values = backend default,
// bytes-only I/O, Raw escape hatch).
//
// The first implementation is provider/llamaswap, which targets a
// Hunyuan3D-2.1-style api_server reached through the /upstream passthrough.
// The surface is image-to-3D only: text-to-3D is the composition
// imagegen.Generate → meshgen.Generate, owned by the caller.
package meshgen
import (
"context"
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
)
// Image is the conditioning input (bytes + MIME), aliased to llm.ImagePart so
// chat- or imagegen-sourced images feed mesh generation without conversion.
type Image = llm.ImagePart
// Mesh is one generated 3D asset: raw encoded bytes plus its format.
type Mesh struct {
// Data is the encoded mesh (binary GLB/STL/OBJ container).
Data []byte
// Format is the lowercase container name ("glb", "stl", "obj").
Format string
// MIME is the corresponding MIME type, e.g. "model/gltf-binary",
// "model/stl".
MIME string
}
// Request is an image-to-3D generation request. Zero values mean "backend
// default".
type Request struct {
// Image is the source image the mesh is reconstructed from. Required.
Image Image
// Format is the requested output container ("glb", "stl", "obj");
// "" = backend default (glb).
Format string
// Texture asks the backend to also synthesize surface textures. Much
// slower and much more VRAM-hungry than shape-only on the reference
// backend; leave false for print-pipeline geometry.
Texture bool
// RemoveBackground lets the backend cut the subject out first; nil =
// backend default (true on Hunyuan3D). Set explicitly to false when the
// input is already a clean cutout.
RemoveBackground *bool
// OctreeResolution is the shape-decoder grid resolution; 0 = backend
// default.
OctreeResolution int
// Steps is the number of diffusion steps; nil = backend default.
Steps *int
// GuidanceScale is the guidance strength; nil = backend default.
GuidanceScale *float64
// Seed fixes the RNG seed for reproducible output; nil = backend default.
Seed *int64
// FaceCount caps the output mesh's face count; 0 = backend default.
FaceCount int
}
// Result is the canonical mesh-generation result.
type Result struct {
// Mesh is the generated asset.
Mesh Mesh
// Raw is the provider-native response object. May be nil.
Raw any
}
// Option mutates a Request before it is sent. Options passed to Generate are
// applied to a copy of the request, so a Request value can be reused.
type Option func(*Request)
// WithFormat sets the output container ("glb", "stl", "obj").
func WithFormat(f string) Option { return func(r *Request) { r.Format = f } }
// WithTexture enables texture synthesis.
func WithTexture() Option { return func(r *Request) { r.Texture = true } }
// WithRemoveBackground sets the backend's background-removal preprocessing.
func WithRemoveBackground(v bool) Option {
return func(r *Request) { r.RemoveBackground = &v }
}
// WithOctreeResolution sets the shape-decoder grid resolution.
func WithOctreeResolution(n int) Option { return func(r *Request) { r.OctreeResolution = n } }
// WithSteps overrides the number of diffusion steps.
func WithSteps(n int) Option { return func(r *Request) { r.Steps = &n } }
// WithGuidanceScale overrides the guidance strength.
func WithGuidanceScale(s float64) Option { return func(r *Request) { r.GuidanceScale = &s } }
// WithSeed fixes the RNG seed.
func WithSeed(seed int64) Option { return func(r *Request) { r.Seed = &seed } }
// WithFaceCount caps the output mesh's face count.
func WithFaceCount(n int) Option { return func(r *Request) { r.FaceCount = n } }
// Apply returns a copy of the request with all options applied. Providers
// call this once at the top of Generate.
func (r Request) Apply(opts ...Option) Request {
for _, opt := range opts {
opt(&r)
}
return r
}
// Model generates 3D meshes from images.
type Model interface {
// Generate reconstructs a mesh from the request's image.
Generate(ctx context.Context, req Request, opts ...Option) (*Result, error)
}
// ModelOption configures a Model at construction time. Reserved for future
// per-model settings.
type ModelOption func(*ModelConfig)
// ModelConfig carries per-model construction settings.
type ModelConfig struct{}
// ApplyModelOptions folds options into a config.
func ApplyModelOptions(opts []ModelOption) ModelConfig {
var cfg ModelConfig
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
// Provider mints mesh models bound to one backend.
type Provider interface {
// Name is the registry identifier for the provider.
Name() string
// MeshModel returns a Model bound to the given id (passed through to the
// backend verbatim; no catalog validation).
MeshModel(id string, opts ...ModelOption) (Model, error)
}
// Converter re-encodes a mesh between containers (GLB/STL/OBJ). A separate
// optional surface because conversion typically runs on a DIFFERENT backend
// than generation (the reference host converts on its mediautils shim —
// Hunyuan3D's live server always emits GLB regardless of the requested
// format).
type Converter interface {
// Convert returns the mesh re-encoded in the given format.
Convert(ctx context.Context, mesh Mesh, format string) (*Result, error)
}
// ConverterProvider mints Converters bound to one backend.
type ConverterProvider interface {
// Name is the registry identifier for the provider.
Name() string
// MeshConverter returns a Converter bound to the given id (passed
// through to the backend verbatim; no catalog validation).
MeshConverter(id string) (Converter, error)
}