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]>
65 lines
2.1 KiB
Go
65 lines
2.1 KiB
Go
// mesh_convert.go implements meshgen.ConverterProvider against the
|
|
// mediautils shim's POST /v1/convert_mesh (multipart file + target),
|
|
// reached through the /upstream passthrough (ADR-0020). Exists because
|
|
// Hunyuan3D's api_server always returns GLB — STL for the printer
|
|
// pipeline is produced by this conversion hop.
|
|
package llamaswap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/llm"
|
|
"gitea.stevedudenhoeffer.com/steve/majordomo/meshgen"
|
|
)
|
|
|
|
// MeshConverter implements meshgen.ConverterProvider.
|
|
func (p *Provider) MeshConverter(id string) (meshgen.Converter, error) {
|
|
if err := p.requireBaseURL(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &meshConverter{p: p, id: id}, nil
|
|
}
|
|
|
|
type meshConverter struct {
|
|
p *Provider
|
|
id string
|
|
}
|
|
|
|
// Convert implements meshgen.Converter.
|
|
func (m *meshConverter) Convert(ctx context.Context, mesh meshgen.Mesh, format string) (*meshgen.Result, error) {
|
|
if len(mesh.Data) == 0 {
|
|
return nil, fmt.Errorf("%w: mesh conversion requires mesh bytes", llm.ErrUnsupported)
|
|
}
|
|
format = strings.ToLower(strings.TrimSpace(format))
|
|
mimeType, ok := meshFormats[format]
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: unsupported mesh format %q (want glb, stl, or obj)", llm.ErrUnsupported, format)
|
|
}
|
|
path, err := upstreamPath(m.id, "/v1/convert_mesh")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
filename := "mesh." + mesh.Format
|
|
if mesh.Format == "" {
|
|
filename = "mesh"
|
|
}
|
|
body, contentType, err := buildMultipart("build mesh-convert form",
|
|
filePart{field: "file", filename: filename, data: mesh.Data},
|
|
[]formField{{"target", format, true}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw, _, err := m.p.doRaw(ctx, http.MethodPost, path, m.id, contentType, body, maxMeshResponseBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(raw) == 0 {
|
|
return nil, &llm.APIError{Provider: m.p.name, Model: m.id, Message: "mesh conversion returned no data"}
|
|
}
|
|
actualFormat, actualMIME := sniffMeshFormat(raw, format, mimeType)
|
|
return &meshgen.Result{Mesh: meshgen.Mesh{Data: raw, Format: actualFormat, MIME: actualMIME}}, nil
|
|
}
|