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]>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user