Files
popiart-server/internal/server/server.go
T
wtgoku d0318292c1 Make artifact and media metadata first-class server records
Artifact and media metadata were previously reconstructed from job result
refs and JSON sidecar files. This change regularizes metadata into SQLite,
keeps filesystem blobs in place, and preserves backward compatibility via
lazy fallback and backfill from existing job refs and JSON metadata.

Constraint: Blob storage remains on the local filesystem in this phase
Rejected: Migrate blobs into SQLite | larger scope and worse operational profile for current media sizes
Rejected: Hard cutover without fallback | unsafe for historical data already on the test server
Confidence: medium
Scope-risk: moderate
Directive: Treat SQLite as the metadata source of truth; JSON sidecars are compatibility fallback only
Tested: go test ./...; deployed to test server 101.42.99.35; verified /v1/artifacts, /v1/artifacts/:id, signed media URL 200, unsigned content 401
Not-tested: Full historical backfill sweep over all existing artifact rows under production-sized data volume
2026-04-18 23:06:34 +08:00

2009 lines
55 KiB
Go

package server
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type Server struct {
store *store
mux *http.ServeMux
cfg Config
newapi *newAPIClient
}
func New() *Server {
svc, err := NewWithConfig(ConfigFromEnv())
if err != nil {
panic(err)
}
return svc
}
func NewWithConfig(cfg Config) (*Server, error) {
cfg = normalizeConfig(cfg)
store, err := newStore(cfg)
if err != nil {
return nil, err
}
s := &Server{
store: store,
mux: http.NewServeMux(),
cfg: cfg,
newapi: newNewAPIClient(cfg),
}
s.routes()
return s, nil
}
func (s *Server) Handler() http.Handler {
return s.mux
}
func (s *Server) routes() {
s.mux.HandleFunc("/health", s.handleHealth)
s.mux.HandleFunc("/v1/auth/login", s.handleAuthLogin)
s.mux.HandleFunc("/v1/auth/me", s.handleAuthMe)
s.mux.HandleFunc("/v1/auth/logout", s.handleAuthLogout)
s.mux.HandleFunc("/v1/auth/token/rotate", s.handleAuthTokenRotate)
s.mux.HandleFunc("/v1/skills", s.handleSkills)
s.mux.HandleFunc("/v1/skills/", s.handleSkill)
s.mux.HandleFunc("/v1/jobs", s.handleJobs)
s.mux.HandleFunc("/v1/jobs/", s.handleJob)
s.mux.HandleFunc("/v1/media/upload", s.handleMediaUpload)
s.mux.HandleFunc("/v1/media/", s.handleMedia)
s.mux.HandleFunc("/v1/artifacts", s.handleArtifacts)
s.mux.HandleFunc("/v1/artifacts/upload", s.handleArtifactUpload)
s.mux.HandleFunc("/v1/artifacts/", s.handleArtifact)
s.mux.HandleFunc("/v1/budget", s.handleBudget)
s.mux.HandleFunc("/v1/budget/usage", s.handleBudgetUsage)
s.mux.HandleFunc("/v1/budget/limits", s.handleBudgetLimits)
s.mux.HandleFunc("/v1/projects", s.handleProjects)
s.mux.HandleFunc("/v1/projects/", s.handleProject)
s.mux.HandleFunc("/v1/models", s.handleModels)
s.mux.HandleFunc("/v1/models/routes", s.handleModelRoutes)
s.mux.HandleFunc("/v1/models/routes/overrides", s.handleModelRouteOverrides)
s.mux.HandleFunc("/v1/models/routes/overrides/unset", s.handleModelRouteOverrideUnset)
s.mux.HandleFunc("/v1/models/infer", s.handleModelsInfer)
}
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"service": "popiart-server-dev",
"version": "0.2.0",
"time": time.Now().UTC().Format(time.RFC3339),
})
}
func (s *Server) handleAuthLogin(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w)
return
}
var req struct {
Key string `json:"key"`
}
if !decodeJSON(w, r, &req) {
return
}
req.Key = strings.TrimSpace(req.Key)
if req.Key == "" {
writeError(w, http.StatusUnauthorized, "UNAUTHENTICATED", "key is required", nil)
return
}
if s.newapi == nil || !s.newapi.enabled() {
writeError(w, http.StatusInternalServerError, "SERVER_ERROR", "PopiNewAPI base URL is not configured", nil)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
if err := s.newapi.verifyKey(ctx, req.Key); err != nil {
writeError(w, http.StatusUnauthorized, "UNAUTHENTICATED", "key invalid or expired", map[string]any{
"details": err.Error(),
})
return
}
key, u, ok, err := s.store.createSession(req.Key)
if err != nil {
writeInternalError(w, "failed to create session", err)
return
}
if !ok {
writeError(w, http.StatusUnauthorized, "UNAUTHENTICATED", "key invalid or expired", nil)
return
}
writeData(w, http.StatusOK, map[string]any{
"key": key,
"token": key,
"user": u,
})
}
func (s *Server) handleAuthMe(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
writeData(w, http.StatusOK, authSessionView{
User: current.User,
SessionKey: current.Token,
UpstreamKeyMasked: maskSecret(current.UpstreamKey),
})
}
func (s *Server) handleAuthLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w)
return
}
token, ok := bearerToken(r)
if ok {
_ = s.store.deleteSession(token)
}
writeData(w, http.StatusOK, map[string]any{"logged_out": true})
}
func (s *Server) handleAuthTokenRotate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w)
return
}
token, ok := bearerToken(r)
if !ok {
writeError(w, http.StatusUnauthorized, "UNAUTHENTICATED", "missing bearer key", nil)
return
}
newToken, u, exists, err := s.store.rotateSession(token)
if err != nil {
writeInternalError(w, "failed to rotate session", err)
return
}
if !exists {
writeError(w, http.StatusUnauthorized, "UNAUTHENTICATED", "key invalid or expired", nil)
return
}
writeData(w, http.StatusOK, map[string]any{
"key": newToken,
"token": newToken,
"user": u,
})
}
func (s *Server) handleSkills(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
tag := r.URL.Query().Get("tag")
search := r.URL.Query().Get("search")
limit := intQuery(r, "limit", 50)
offset := intQuery(r, "offset", 0)
items, total := s.store.listSkills(tag, search, limit, offset)
writeData(w, http.StatusOK, map[string]any{
"items": items,
"total": total,
"limit": limit,
"offset": offset,
})
}
func (s *Server) handleSkill(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
path := strings.TrimPrefix(r.URL.Path, "/v1/skills/")
if path == "" {
notFound(w)
return
}
if strings.HasSuffix(path, "/schema") {
id := strings.TrimSuffix(strings.TrimSuffix(path, "/schema"), "/")
item, exists := s.store.getSkill(id)
if !exists {
notFound(w)
return
}
writeData(w, http.StatusOK, map[string]any{
"input_schema": item.InputSchema,
"output_schema": item.OutputSchema,
})
return
}
item, exists := s.store.getSkill(strings.Trim(path, "/"))
if !exists {
notFound(w)
return
}
writeData(w, http.StatusOK, item)
}
func (s *Server) handleJobs(w http.ResponseWriter, r *http.Request) {
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
switch r.Method {
case http.MethodPost:
var req struct {
SkillID string `json:"skill_id"`
Input map[string]any `json:"input"`
ProjectID string `json:"project_id"`
Priority string `json:"priority"`
IdempotencyKey string `json:"idempotency_key"`
}
if !decodeJSON(w, r, &req) {
return
}
if req.SkillID == "" {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "skill_id is required", nil)
return
}
item, exists := s.store.getSkill(req.SkillID)
if !exists {
writeError(w, http.StatusNotFound, "NOT_FOUND", "skill not found", nil)
return
}
routeKey := normalizeRouteKey(item.RouteKey)
modelID := s.resolveModelID(routeKey, req.ProjectID)
record, statusCode, err := s.store.createJob(
req.SkillID,
routeKey,
modelID,
routeExecMode(routeKey),
req.Input,
req.ProjectID,
defaultString(req.Priority, "normal"),
req.IdempotencyKey,
current,
)
if err != nil {
writeInternalError(w, "failed to create job", err)
return
}
if statusCode == http.StatusAccepted {
go s.dispatchJob(record)
}
writeData(w, statusCode, map[string]any{
"job_id": record.JobID,
"status": record.Status,
"created_at": record.CreatedAt,
})
case http.MethodGet:
status := r.URL.Query().Get("status")
skillID := r.URL.Query().Get("skill_id")
projectID := r.URL.Query().Get("project_id")
limit := intQuery(r, "limit", 20)
offset := intQuery(r, "offset", 0)
items, total, err := s.store.listJobs(current.User.ID, status, skillID, projectID, limit, offset)
if err != nil {
writeInternalError(w, "failed to list jobs", err)
return
}
writeData(w, http.StatusOK, map[string]any{
"items": items,
"total": total,
"limit": limit,
"offset": offset,
})
default:
methodNotAllowed(w)
}
}
func (s *Server) handleJob(w http.ResponseWriter, r *http.Request) {
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
path := strings.TrimPrefix(r.URL.Path, "/v1/jobs/")
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) == 0 || parts[0] == "" {
notFound(w)
return
}
jobID := parts[0]
if len(parts) == 1 && r.Method == http.MethodGet {
record, exists, err := s.store.getJob(current.User.ID, jobID)
if err != nil {
writeInternalError(w, "failed to load job", err)
return
}
if !exists {
notFound(w)
return
}
writeData(w, http.StatusOK, record)
return
}
if len(parts) == 2 && parts[1] == "cancel" && r.Method == http.MethodPost {
record, exists, conflict, err := s.store.cancelJob(current.User.ID, jobID)
if err != nil {
writeInternalError(w, "failed to cancel job", err)
return
}
if !exists {
notFound(w)
return
}
if conflict {
writeError(w, http.StatusConflict, "CONFLICT", "job already in terminal state", nil)
return
}
writeData(w, http.StatusOK, record)
return
}
if len(parts) == 2 && parts[1] == "logs" && r.Method == http.MethodGet {
logs, exists, err := s.store.jobLogs(current.User.ID, jobID)
if err != nil {
writeInternalError(w, "failed to load job logs", err)
return
}
if !exists {
notFound(w)
return
}
if strings.Contains(r.Header.Get("Accept"), "text/event-stream") {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
flusher, _ := w.(http.Flusher)
for _, item := range logs {
fmt.Fprintf(w, "data: %s\n\n", mustJSON(item))
if flusher != nil {
flusher.Flush()
}
time.Sleep(150 * time.Millisecond)
}
return
}
writeData(w, http.StatusOK, logs)
return
}
if len(parts) == 2 && parts[1] == "artifacts" && r.Method == http.MethodGet {
items, exists, err := s.store.artifactsForJob(current.User.ID, jobID)
if err != nil {
writeInternalError(w, "failed to list job artifacts", err)
return
}
if !exists {
notFound(w)
return
}
writeData(w, http.StatusOK, map[string]any{"items": items})
return
}
notFound(w)
}
func (s *Server) handleArtifacts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
projectID := strings.TrimSpace(r.URL.Query().Get("project_id"))
jobID := strings.TrimSpace(r.URL.Query().Get("job_id"))
limit := intQuery(r, "limit", 20)
offset := intQuery(r, "offset", 0)
items, total, err := s.store.listArtifacts(current.User.ID, projectID, jobID, limit, offset)
if err != nil {
writeInternalError(w, "failed to list artifacts", err)
return
}
writeData(w, http.StatusOK, map[string]any{
"items": artifactViews(s.cfg, items, time.Now().UTC()),
"total": total,
"limit": limit,
"offset": offset,
})
}
func (s *Server) handleArtifactUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w)
return
}
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
r.Body = http.MaxBytesReader(w, r.Body, 20<<20)
if err := r.ParseMultipartForm(20 << 20); err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "failed to parse multipart form", map[string]any{
"details": err.Error(),
})
return
}
file, header, err := r.FormFile("file")
if err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "file is required", nil)
return
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "failed to read upload file", map[string]any{
"details": err.Error(),
})
return
}
if len(content) == 0 {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "uploaded file is empty", nil)
return
}
var metadata any
metadataJSON := strings.TrimSpace(r.FormValue("metadata_json"))
if metadataJSON != "" {
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "metadata_json must be valid JSON", map[string]any{
"details": err.Error(),
})
return
}
}
filename := strings.TrimSpace(r.FormValue("filename"))
if filename == "" {
filename = strings.TrimSpace(header.Filename)
}
contentType := strings.TrimSpace(r.FormValue("content_type"))
if contentType == "" {
contentType = strings.TrimSpace(header.Header.Get("Content-Type"))
}
if contentType == "" || contentType == "application/octet-stream" {
contentType = http.DetectContentType(content)
}
if filename == "" {
filename = "artifact" + extensionFromContentType(contentType)
}
filename = sanitizeFilename(filename)
visibility := strings.TrimSpace(r.FormValue("visibility"))
input := map[string]any{
"filename": filename,
"content_type": contentType,
"size_bytes": len(content),
"role": strings.TrimSpace(r.FormValue("role")),
}
projectID := strings.TrimSpace(r.FormValue("project_id"))
if projectID != "" {
input["project_id"] = projectID
}
if metadata != nil {
input["metadata"] = metadata
}
record, _, err := s.store.createJob(
"popiskill-artifact-upload-local-v1",
"artifact.upload",
"",
"local_upload",
input,
projectID,
"normal",
"",
current,
)
if err != nil {
writeInternalError(w, "failed to create upload job", err)
return
}
refMedia, err := s.store.persistMediaContent(
current.User.ID,
projectID,
buildArtifactID(record.JobID, 0),
filename,
contentType,
visibility,
content,
"",
)
if err != nil {
writeInternalError(w, "failed to persist upload media", err)
return
}
ref := resultRef{
Kind: "local_path",
URL: refMedia.URL,
LocalPath: refMedia.LocalPath,
MediaID: refMedia.ID,
Filename: refMedia.Filename,
ContentType: refMedia.ContentType,
SizeBytes: refMedia.SizeBytes,
Visibility: refMedia.Visibility,
SHA256: refMedia.SHA256,
StorageStatus: "ready",
}
if err := s.store.completeJobWithResults(record.JobID, []resultRef{ref}, nil); err != nil {
writeInternalError(w, "failed to persist upload artifact", err)
return
}
item, _, exists, err := s.store.artifactRef(current.User.ID, buildArtifactID(record.JobID, 0))
if err != nil {
writeInternalError(w, "failed to load uploaded artifact", err)
return
}
if !exists || item == nil {
writeInternalError(w, "uploaded artifact missing after persistence", errors.New("artifact not found"))
return
}
writeData(w, http.StatusCreated, artifactView(s.cfg, *item, time.Now().UTC()))
}
func (s *Server) handleMediaUpload(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w)
return
}
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
if err := r.ParseMultipartForm(32 << 20); err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "failed to parse multipart form", map[string]any{
"details": err.Error(),
})
return
}
file, header, err := r.FormFile("file")
if err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "file is required", nil)
return
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "failed to read upload file", map[string]any{
"details": err.Error(),
})
return
}
if len(content) == 0 {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "uploaded file is empty", nil)
return
}
metadataJSON := strings.TrimSpace(r.FormValue("metadata_json"))
if metadataJSON != "" {
var metadata any
if err := json.Unmarshal([]byte(metadataJSON), &metadata); err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "metadata_json must be valid JSON", map[string]any{
"details": err.Error(),
})
return
}
}
filename := strings.TrimSpace(r.FormValue("filename"))
if filename == "" {
filename = strings.TrimSpace(header.Filename)
}
contentType := strings.TrimSpace(r.FormValue("content_type"))
if contentType == "" {
contentType = strings.TrimSpace(header.Header.Get("Content-Type"))
}
if contentType == "" || contentType == "application/octet-stream" {
contentType = http.DetectContentType(content)
}
if filename == "" {
filename = "media" + extensionFromContentType(contentType)
}
filename = sanitizeFilename(filename)
record, err := s.store.persistMediaContent(
current.User.ID,
strings.TrimSpace(r.FormValue("project_id")),
"",
filename,
contentType,
strings.TrimSpace(r.FormValue("visibility")),
content,
"",
)
if err != nil {
writeInternalError(w, "failed to persist media", err)
return
}
writeData(w, http.StatusCreated, mediaView(s.cfg, record, time.Now().UTC()))
}
func (s *Server) handleMedia(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/v1/media/")
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) == 0 || parts[0] == "" {
notFound(w)
return
}
record, exists, err := s.store.getMedia(parts[0])
if err != nil {
writeInternalError(w, "failed to load media", err)
return
}
if !exists || record == nil {
notFound(w)
return
}
if len(parts) == 2 && parts[1] == "content" && r.Method == http.MethodGet {
if !validateSignedMediaAccess(s.cfg, record.ID, r.URL.Query().Get("exp"), r.URL.Query().Get("sig"), time.Now().UTC()) {
current, ok, err := s.sessionFromRequest(r)
if err != nil {
writeInternalError(w, "failed to load session", err)
return
}
if !ok {
writeError(w, http.StatusUnauthorized, "UNAUTHENTICATED", "missing bearer key or signed media url", nil)
return
}
if current.User.ID != record.UserID {
notFound(w)
return
}
}
file, err := os.Open(record.LocalPath)
if err != nil {
writeInternalError(w, "failed to read media content", err)
return
}
defer file.Close()
w.Header().Set("Content-Type", defaultString(record.ContentType, "application/octet-stream"))
if record.SizeBytes > 0 {
w.Header().Set("Content-Length", strconv.FormatInt(record.SizeBytes, 10))
}
w.WriteHeader(http.StatusOK)
if _, err := io.Copy(w, file); err != nil {
log.Printf("popiartServer: streaming media %s failed: %v", record.ID, err)
}
return
}
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
if current.User.ID != record.UserID {
notFound(w)
return
}
if len(parts) == 1 && r.Method == http.MethodGet {
writeData(w, http.StatusOK, mediaView(s.cfg, *record, time.Now().UTC()))
return
}
notFound(w)
}
func (s *Server) handleArtifact(w http.ResponseWriter, r *http.Request) {
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
path := strings.TrimPrefix(r.URL.Path, "/v1/artifacts/")
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) == 0 || parts[0] == "" {
notFound(w)
return
}
artifactID := parts[0]
item, ref, exists, err := s.store.artifactRef(current.User.ID, artifactID)
if err != nil {
writeInternalError(w, "failed to load artifact", err)
return
}
if !exists {
notFound(w)
return
}
if len(parts) == 1 && r.Method == http.MethodGet {
writeData(w, http.StatusOK, artifactView(s.cfg, *item, time.Now().UTC()))
return
}
if len(parts) == 2 && parts[1] == "content" && r.Method == http.MethodGet {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Minute)
defer cancel()
contentType, sizeBytes, reader, err := s.newapi.openResultRef(ctx, current.UpstreamKey, ref)
if err != nil {
writeInternalError(w, "failed to read artifact content", err)
return
}
defer reader.Close()
w.Header().Set("Content-Type", defaultString(contentType, item.ContentType))
if sizeBytes > 0 {
w.Header().Set("Content-Length", strconv.FormatInt(sizeBytes, 10))
}
w.WriteHeader(http.StatusOK)
if _, err := io.Copy(w, reader); err != nil {
log.Printf("popiartServer: streaming artifact %s failed: %v", artifactID, err)
}
return
}
notFound(w)
}
func (s *Server) handleBudget(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
writeData(w, http.StatusOK, map[string]any{
"period": map[string]any{
"start": time.Now().UTC().AddDate(0, 0, -30).Format(time.RFC3339),
"end": time.Now().UTC().Format(time.RFC3339),
},
"used": map[string]any{
"tokens": 12000,
"cost_usd": 4.62,
},
"limit": map[string]any{
"monthly_tokens": 1000000,
"monthly_cost_usd": 200,
},
"remaining": map[string]any{
"tokens": 988000,
"cost_usd": 195.38,
},
})
}
func (s *Server) handleBudgetUsage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
writeData(w, http.StatusOK, map[string]any{
"rows": []map[string]any{
{"dimension": "popiskill-image-text2image-basic-v1", "tokens_used": 3000, "cost_usd": 1.2, "job_count": 3},
{"dimension": "popiskill-video-image2video-basic-v1", "tokens_used": 9000, "cost_usd": 3.42, "job_count": 2},
},
"total": map[string]any{"tokens_used": 12000, "cost_usd": 4.62, "job_count": 5},
})
}
func (s *Server) handleBudgetLimits(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
writeData(w, http.StatusOK, map[string]any{
"rate_limits": []map[string]any{
{"window": "1m", "limit": 60, "remaining": 59, "reset_at": time.Now().UTC().Add(time.Minute).Format(time.RFC3339)},
},
"quota": map[string]any{"monthly_tokens": 1000000, "monthly_cost_usd": 200},
})
}
func (s *Server) handleProjects(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
writeData(w, http.StatusOK, map[string]any{
"items": s.store.projects,
"total": len(s.store.projects),
"limit": intQuery(r, "limit", 20),
"offset": 0,
})
}
func (s *Server) handleProject(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
path := strings.TrimPrefix(r.URL.Path, "/v1/projects/")
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) == 0 || parts[0] == "" {
notFound(w)
return
}
projectID := parts[0]
var found *project
for _, item := range s.store.projects {
if item.ID == projectID {
copyItem := item
found = &copyItem
break
}
}
if found == nil {
notFound(w)
return
}
if len(parts) == 1 {
writeData(w, http.StatusOK, found)
return
}
if len(parts) == 2 && parts[1] == "context" {
routes, err := s.store.routesForProject(projectID)
if err != nil {
writeInternalError(w, "failed to load project routes", err)
return
}
writeData(w, http.StatusOK, map[string]any{
"project": found,
"budget": map[string]any{
"used": map[string]any{"tokens": 12000, "cost_usd": 4.62},
"limit": map[string]any{"monthly_tokens": 1000000, "monthly_cost_usd": 200},
"remaining": map[string]any{"tokens": 988000, "cost_usd": 195.38},
},
"route_overrides": routes,
"available_skills": s.store.skills,
})
return
}
notFound(w)
}
func (s *Server) handleModels(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/models" {
notFound(w)
return
}
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
if s.newapi == nil || !s.newapi.enabled() {
writeError(w, http.StatusServiceUnavailable, "MODEL_LIST_UNAVAILABLE", "PopiNewAPI is not configured for popiartServer", nil)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
items, err := s.newapi.listModels(ctx, current.UpstreamKey)
if err != nil {
writeError(w, http.StatusBadGateway, "MODEL_LIST_FAILED", "failed to load models from PopiNewAPI", map[string]any{
"details": err.Error(),
})
return
}
modelType := r.URL.Query().Get("type")
provider := r.URL.Query().Get("provider")
filtered := make([]model, 0, len(items))
for _, item := range items {
if modelType != "" && item.Type != modelType {
continue
}
if provider != "" && item.Provider != provider {
continue
}
filtered = append(filtered, item)
}
writeData(w, http.StatusOK, map[string]any{"items": filtered})
}
func (s *Server) handleModelRoutes(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
projectID := r.URL.Query().Get("project_id")
routes, err := s.store.routesForProject(projectID)
if err != nil {
writeInternalError(w, "failed to load route overrides", err)
return
}
writeData(w, http.StatusOK, map[string]any{
"global": map[string]any{
"image": s.cfg.DefaultImageModel,
"video": s.cfg.DefaultVideoModel,
"audio": "",
},
"overrides": map[string]any{
projectID: routes,
},
})
}
func (s *Server) handleModelRouteOverrides(w http.ResponseWriter, r *http.Request) {
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
switch r.Method {
case http.MethodGet:
projectID := r.URL.Query().Get("project_id")
routes, err := s.store.routesForProject(projectID)
if err != nil {
writeInternalError(w, "failed to load route overrides", err)
return
}
writeData(w, http.StatusOK, map[string]any{
"project_id": projectID,
"items": routes,
})
case http.MethodPost:
var req struct {
ProjectID string `json:"project_id"`
SkillType string `json:"skill_type"`
ModelID string `json:"model_id"`
}
if !decodeJSON(w, r, &req) {
return
}
if req.ProjectID == "" || req.SkillType == "" || req.ModelID == "" {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "project_id, skill_type, and model_id are required", nil)
return
}
routeKey := normalizeRouteKey(req.SkillType)
if err := s.store.setRouteOverride(req.ProjectID, routeKey, req.ModelID); err != nil {
writeInternalError(w, "failed to save route override", err)
return
}
writeData(w, http.StatusOK, map[string]any{
"project_id": req.ProjectID,
"skill_type": req.SkillType,
"route_key": routeKey,
"model_id": req.ModelID,
})
default:
methodNotAllowed(w)
}
}
func (s *Server) handleModelRouteOverrideUnset(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w)
return
}
_, ok := s.authenticateSession(w, r)
if !ok {
return
}
var req struct {
ProjectID string `json:"project_id"`
SkillType string `json:"skill_type"`
}
if !decodeJSON(w, r, &req) {
return
}
if req.ProjectID == "" || req.SkillType == "" {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "project_id and skill_type are required", nil)
return
}
routeKey := normalizeRouteKey(req.SkillType)
if err := s.store.unsetRouteOverride(req.ProjectID, routeKey); err != nil {
writeInternalError(w, "failed to delete route override", err)
return
}
writeData(w, http.StatusOK, map[string]any{
"project_id": req.ProjectID,
"skill_type": req.SkillType,
"route_key": routeKey,
"unset": true,
})
}
func (s *Server) handleModelsInfer(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w)
return
}
current, ok := s.authenticateSession(w, r)
if !ok {
return
}
var req struct {
ModelID string `json:"model_id"`
Input map[string]any `json:"input"`
ProjectID string `json:"project_id"`
Priority string `json:"priority"`
IdempotencyKey string `json:"idempotency_key"`
}
if !decodeJSON(w, r, &req) {
return
}
if req.ModelID == "" {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "model_id is required", nil)
return
}
routeKey := inferRouteKeyForModel(req.ModelID, req.Input)
record, statusCode, err := s.store.createJob(
"",
routeKey,
req.ModelID,
routeExecMode(routeKey),
req.Input,
req.ProjectID,
defaultString(req.Priority, "normal"),
req.IdempotencyKey,
current,
)
if err != nil {
writeInternalError(w, "failed to create model job", err)
return
}
if statusCode == http.StatusAccepted {
go s.dispatchJob(record)
}
writeData(w, statusCode, map[string]any{
"job_id": record.JobID,
"status": record.Status,
"created_at": record.CreatedAt,
})
}
func (s *Server) dispatchJob(record *job) {
if record == nil {
return
}
switch normalizeRouteKey(record.RouteKey) {
case "image.text2image":
s.executeTextToImageJob(record)
case "image.img2img":
s.executeImageToImageJob(record)
case "video.image2video":
s.executeImageToVideoJob(record)
default:
s.executeUnsupportedSkill(record)
}
}
func (s *Server) executeTextToImageJob(record *job) {
if _, _, err := s.store.startJob(record.JobID); err != nil {
log.Printf("popiartServer: start job %s failed: %v", record.JobID, err)
}
if s.newapi == nil || !s.newapi.enabled() {
if err := s.store.failJob(record.JobID, "NEWAPI_NOT_CONFIGURED", "PopiNewAPI is not configured for popiartServer", nil); err != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, err)
}
return
}
modelID := strings.TrimSpace(record.ModelID)
if modelID == "" {
modelID = s.resolveModelID(record.RouteKey, record.ProjectID)
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
refs, usage, err := s.newapi.generateImageRefs(ctx, record.UpstreamKey, modelID, record.Input)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MODEL_REQUEST_FAILED", err.Error(), map[string]any{
"model_id": modelID,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
refs, err = s.persistResultRefs(ctx, record, refs)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MEDIA_PERSIST_FAILED", "failed to persist generated media", map[string]any{
"details": err.Error(),
"model_id": modelID,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
if err := s.store.completeJobWithResults(record.JobID, refs, usage); err != nil {
log.Printf("popiartServer: complete job %s failed: %v", record.JobID, err)
if failErr := s.store.failJob(record.JobID, "RESULT_REF_PERSIST_FAILED", "failed to persist job result refs", map[string]any{
"details": err.Error(),
}); failErr != nil {
log.Printf("popiartServer: fail job %s after result ref error failed: %v", record.JobID, failErr)
}
}
}
func (s *Server) executeImageToImageJob(record *job) {
if _, _, err := s.store.startJob(record.JobID); err != nil {
log.Printf("popiartServer: start job %s failed: %v", record.JobID, err)
}
if s.newapi == nil || !s.newapi.enabled() {
if err := s.store.failJob(record.JobID, "NEWAPI_NOT_CONFIGURED", "PopiNewAPI is not configured for popiartServer", nil); err != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, err)
}
return
}
modelID := strings.TrimSpace(record.ModelID)
if modelID == "" {
modelID = s.resolveModelID(record.RouteKey, record.ProjectID)
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
input := buildImageToImageInput(record)
imageRefs, err := s.resolveImageToImageReferences(ctx, record, input)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
"skill_id": record.SkillID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
log.Printf(
"popiartServer: image job %s using model=%s refs=%s preserve_composition=%t strength=%v",
record.JobID,
modelID,
summarizeImageEditReferenceRoles(imageRefs),
boolValue(input["preserve_composition"]),
input["strength"],
)
var (
resultRefs []resultRef
usage map[string]any
)
if useMiniMaxImageGenerations(modelID) {
resultRefs, usage, err = s.newapi.generateMiniMaxImageRefs(ctx, record.UpstreamKey, modelID, input, imageRefs)
} else {
resultRefs, usage, err = s.newapi.generateEditedImageRefs(ctx, record.UpstreamKey, modelID, input, imageRefs)
}
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MODEL_REQUEST_FAILED", err.Error(), map[string]any{
"model_id": modelID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
resultRefs, err = s.persistResultRefs(ctx, record, resultRefs)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MEDIA_PERSIST_FAILED", "failed to persist generated media", map[string]any{
"details": err.Error(),
"model_id": modelID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
if err := s.store.completeJobWithResults(record.JobID, resultRefs, usage); err != nil {
log.Printf("popiartServer: complete job %s failed: %v", record.JobID, err)
if failErr := s.store.failJob(record.JobID, "RESULT_REF_PERSIST_FAILED", "failed to persist job result refs", map[string]any{
"details": err.Error(),
}); failErr != nil {
log.Printf("popiartServer: fail job %s after result ref error failed: %v", record.JobID, failErr)
}
}
}
func (s *Server) executeImageToVideoJob(record *job) {
if _, _, err := s.store.startJob(record.JobID); err != nil {
log.Printf("popiartServer: start job %s failed: %v", record.JobID, err)
}
if s.newapi == nil || !s.newapi.enabled() {
if err := s.store.failJob(record.JobID, "NEWAPI_NOT_CONFIGURED", "PopiNewAPI is not configured for popiartServer", nil); err != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, err)
}
return
}
modelID := strings.TrimSpace(record.ModelID)
if modelID == "" {
modelID = s.resolveModelID(record.RouteKey, record.ProjectID)
}
input := buildImageToVideoInput(record)
submitCtx, cancelSubmit := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancelSubmit()
var refsForMiniMax []imageEditReference
var ref imageEditReference
var err error
if useMiniMaxVideoGenerations(modelID) {
if hasVideoReferenceInput(input) {
refsForMiniMax, err = s.resolveVideoReferences(submitCtx, record, input)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
"skill_id": record.SkillID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
}
} else {
ref, err = s.resolveImageToImageReference(submitCtx, record, input)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "REFERENCE_IMAGE_RESOLUTION_FAILED", err.Error(), map[string]any{
"skill_id": record.SkillID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
}
var upstreamTaskID string
if useMiniMaxVideoGenerations(modelID) {
upstreamTaskID, err = s.newapi.submitMiniMaxVideoTask(submitCtx, record.UpstreamKey, modelID, input, refsForMiniMax)
} else {
upstreamTaskID, err = s.newapi.submitImageToVideoTask(submitCtx, record.UpstreamKey, modelID, input, ref)
}
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MODEL_REQUEST_FAILED", err.Error(), map[string]any{
"model_id": modelID,
"route_key": record.RouteKey,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
if err := s.store.linkUpstreamTask(record.JobID, upstreamTaskID); err != nil {
if repoErr := s.store.failJob(record.JobID, "TASK_LINK_PERSIST_FAILED", "failed to persist upstream task id", map[string]any{
"details": err.Error(),
"newapi_task_id": upstreamTaskID,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
pollCtx, cancelPoll := context.WithTimeout(context.Background(), 20*time.Minute)
defer cancelPoll()
taskResult, err := s.waitForVideoTask(pollCtx, record.UpstreamKey, upstreamTaskID)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "TASK_POLL_FAILED", err.Error(), map[string]any{
"model_id": modelID,
"route_key": record.RouteKey,
"newapi_task_id": upstreamTaskID,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
refs := []resultRef{buildVideoResultRef(s.newapi.baseURL, upstreamTaskID, modelID, taskResult)}
usage := map[string]any{
"newapi_task_id": upstreamTaskID,
"status": taskResult.Status,
}
if strings.TrimSpace(taskResult.Format) != "" {
usage["format"] = taskResult.Format
}
persistCtx, cancelPersist := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancelPersist()
refs, err = s.persistResultRefs(persistCtx, record, refs)
if err != nil {
if repoErr := s.store.failJob(record.JobID, "MEDIA_PERSIST_FAILED", "failed to persist generated media", map[string]any{
"details": err.Error(),
"model_id": modelID,
"route_key": record.RouteKey,
"newapi_task_id": upstreamTaskID,
}); repoErr != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, repoErr)
}
return
}
if err := s.store.completeJobWithResults(record.JobID, refs, usage); err != nil {
log.Printf("popiartServer: complete job %s failed: %v", record.JobID, err)
if failErr := s.store.failJob(record.JobID, "RESULT_REF_PERSIST_FAILED", "failed to persist job result refs", map[string]any{
"details": err.Error(),
}); failErr != nil {
log.Printf("popiartServer: fail job %s after result ref error failed: %v", record.JobID, failErr)
}
}
}
func hasVideoReferenceInput(input map[string]any) bool {
if input == nil {
return false
}
return strings.TrimSpace(stringValue(
input["source_artifact_id"],
input["image"],
input["image_url"],
input["reference_image_url"],
)) != "" || len(extractStringValues(input["images"])) > 0
}
func (s *Server) executeUnsupportedSkill(record *job) {
if _, _, err := s.store.startJob(record.JobID); err != nil {
log.Printf("popiartServer: start job %s failed: %v", record.JobID, err)
}
time.Sleep(300 * time.Millisecond)
if err := s.store.failJob(record.JobID, "NOT_IMPLEMENTED", "skill runtime is not connected yet", map[string]any{
"skill_id": record.SkillID,
"route_key": record.RouteKey,
"exec_mode": record.ExecMode,
}); err != nil {
log.Printf("popiartServer: fail job %s failed: %v", record.JobID, err)
}
}
func (s *Server) resolveModelID(routeKey, projectID string) string {
routes, err := s.store.routesForProject(projectID)
if err == nil {
if override := routes[normalizeRouteKey(routeKey)]; override != "" {
return override
}
}
switch normalizeRouteKey(routeKey) {
case "video.image2video":
return s.cfg.DefaultVideoModel
default:
return s.cfg.DefaultImageModel
}
}
func inferRouteKeyForModel(modelID string, input map[string]any) string {
if modelType, _ := classifyModelIDFallback(modelID); modelType == "video" {
return "video.image2video"
}
if input != nil {
if ref := strings.TrimSpace(stringValue(
input["source_artifact_id"],
input["image"],
input["reference_image_url"],
input["image_url"],
)); ref != "" {
return "image.img2img"
}
}
return "image.text2image"
}
func (s *Server) resolveImageToImageReferences(ctx context.Context, record *job, input map[string]any) ([]imageEditReference, error) {
if record == nil {
return nil, fmt.Errorf("job record is required")
}
if artifactID := strings.TrimSpace(stringValue(input["source_artifact_id"])); artifactID != "" {
sourceRef, err := s.resolveArtifactImageReference(ctx, record, artifactID, "source")
if err != nil {
return nil, err
}
refs := []imageEditReference{sourceRef}
seen := map[string]struct{}{artifactID: {}}
var appendArtifactRefs = func(role string, values []string) error {
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
ref, err := s.resolveArtifactImageReference(ctx, record, value, role)
if err != nil {
return err
}
seen[value] = struct{}{}
refs = append(refs, ref)
}
return nil
}
if err := appendArtifactRefs("identity", stringSliceValue(input["identity_reference_artifact_ids"])); err != nil {
return nil, err
}
if err := appendArtifactRefs("style", stringSliceValue(input["style_reference_artifact_ids"])); err != nil {
return nil, err
}
if err := appendArtifactRefs("reference", stringSliceValue(input["reference_artifact_ids"])); err != nil {
return nil, err
}
return refs, nil
}
refURL := strings.TrimSpace(stringValue(
input["image"],
input["reference_image_url"],
input["image_url"],
))
if refURL == "" && strings.Contains(record.SkillID, "popistudio-alice-showcase") {
refURL = defaultAliceReferenceURL
}
if refURL == "" {
return nil, fmt.Errorf("reference image is required")
}
ref, err := s.downloadReferenceImage(ctx, record.SessionID, refURL)
if err != nil {
return nil, err
}
ref.Role = "source"
return []imageEditReference{ref}, nil
}
func (s *Server) resolveImageToImageReference(ctx context.Context, record *job, input map[string]any) (imageEditReference, error) {
refs, err := s.resolveImageToImageReferences(ctx, record, input)
if err != nil {
return imageEditReference{}, err
}
if len(refs) == 0 {
return imageEditReference{}, fmt.Errorf("reference image is required")
}
return refs[0], nil
}
func (s *Server) resolveVideoReferences(ctx context.Context, record *job, input map[string]any) ([]imageEditReference, error) {
if record == nil {
return nil, fmt.Errorf("job record is required")
}
if artifactID := strings.TrimSpace(stringValue(input["source_artifact_id"])); artifactID != "" {
ref, err := s.resolveImageToImageReference(ctx, record, map[string]any{
"source_artifact_id": artifactID,
})
if err != nil {
return nil, err
}
return []imageEditReference{ref}, nil
}
urls := extractStringValues(input["images"])
if len(urls) == 0 {
if refURL := strings.TrimSpace(stringValue(
input["image"],
input["reference_image_url"],
input["image_url"],
)); refURL != "" {
urls = []string{refURL}
}
}
if len(urls) == 0 {
return nil, fmt.Errorf("reference image is required")
}
refs := make([]imageEditReference, 0, len(urls))
for _, rawURL := range urls {
ref, err := s.downloadReferenceImage(ctx, record.SessionID, rawURL)
if err != nil {
return nil, err
}
refs = append(refs, ref)
}
return refs, nil
}
func extractStringValues(value any) []string {
switch typed := value.(type) {
case []string:
out := make([]string, 0, len(typed))
for _, item := range typed {
if text := strings.TrimSpace(item); text != "" {
out = append(out, text)
}
}
return out
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
if text := strings.TrimSpace(stringValue(item)); text != "" {
out = append(out, text)
}
}
return out
default:
return nil
}
}
func (s *Server) waitForVideoTask(ctx context.Context, token, taskID string) (*videoTaskResult, error) {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
result, err := s.newapi.fetchVideoTask(ctx, token, taskID)
if err != nil {
return nil, err
}
switch result.Status {
case "completed":
return result, nil
case "failed":
message := defaultString(strings.TrimSpace(result.ErrorReason), "video task failed")
return nil, errors.New(message)
case "queued", "in_progress", "":
default:
if strings.TrimSpace(result.Status) != "" {
return nil, fmt.Errorf("unexpected video task status: %s", result.Status)
}
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
}
}
}
func (s *Server) downloadReferenceImage(ctx context.Context, sessionToken, rawURL string) (imageEditReference, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
if err != nil {
return imageEditReference{}, err
}
if sessionToken = strings.TrimSpace(sessionToken); sessionToken != "" && shouldAttachAuthHeader(publicBaseURL(s.cfg), rawURL) {
req.Header.Set("Authorization", "Bearer "+sessionToken)
}
resp, err := s.newapi.httpClient.Do(req)
if err != nil {
return imageEditReference{}, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return imageEditReference{}, fmt.Errorf("download reference image failed: %s", strings.TrimSpace(string(body)))
}
content, err := io.ReadAll(resp.Body)
if err != nil {
return imageEditReference{}, err
}
contentType := defaultString(strings.TrimSpace(resp.Header.Get("Content-Type")), http.DetectContentType(content))
return imageEditReference{
Filename: filenameFromURL(rawURL, "reference"+extensionFromContentType(contentType)),
ContentType: contentType,
Content: content,
URL: strings.TrimSpace(rawURL),
}, nil
}
func buildImageToImageInput(record *job) map[string]any {
input := cloneMap(record.Input)
if input == nil {
input = map[string]any{}
}
prompt := strings.TrimSpace(stringValue(
input["prompt"],
input["scene_prompt"],
))
if strings.Contains(record.SkillID, "popistudio-alice-showcase") {
prompt = buildAliceShowcasePrompt(input, prompt)
} else {
prompt = buildGenericImageToImagePrompt(input, prompt)
}
input["prompt"] = prompt
delete(input, "scene_prompt")
return input
}
func buildImageToVideoInput(record *job) map[string]any {
input := cloneMap(record.Input)
if input == nil {
input = map[string]any{}
}
prompt := strings.TrimSpace(stringValue(
input["prompt"],
input["motion_prompt"],
input["scene_prompt"],
))
parts := make([]string, 0, 6)
if prompt != "" {
parts = append(parts, prompt)
}
if cameraMotion := strings.TrimSpace(stringValue(input["camera_motion"])); cameraMotion != "" {
parts = append(parts, "camera motion "+cameraMotion)
}
if shotType := strings.TrimSpace(stringValue(input["shot_type"])); shotType != "" {
parts = append(parts, shotType)
}
if mood := strings.TrimSpace(stringValue(input["mood"])); mood != "" {
parts = append(parts, mood)
}
if aspectRatio := strings.TrimSpace(stringValue(input["aspect_ratio"])); aspectRatio != "" {
parts = append(parts, "aspect ratio "+aspectRatio)
}
if len(parts) == 0 {
parts = append(parts, "Generate a short polished cinematic motion clip that preserves the reference image subject identity.")
}
input["prompt"] = strings.Join(parts, ", ")
delete(input, "motion_prompt")
delete(input, "scene_prompt")
return input
}
func buildAliceShowcasePrompt(input map[string]any, scenePrompt string) string {
parts := []string{
"PopiStudio Alice as the same fixed main protagonist from the canonical Alice reference image",
"preserve the same anime facial structure, recognizability, hairstyle, hair color, clothing language, and main palette",
"Alice remains the visual center of the frame",
}
if scenePrompt != "" {
parts = append(parts, scenePrompt)
}
if shotType := strings.TrimSpace(stringValue(input["shot_type"])); shotType != "" {
parts = append(parts, shotType)
}
if camera := strings.TrimSpace(stringValue(input["camera"])); camera != "" {
parts = append(parts, camera)
}
if mood := strings.TrimSpace(stringValue(input["mood"])); mood != "" {
parts = append(parts, mood)
}
if aspectRatio := strings.TrimSpace(stringValue(input["aspect_ratio"])); aspectRatio != "" {
parts = append(parts, "aspect ratio "+aspectRatio)
}
parts = append(parts,
"real-world modern Chinese living scene",
"Sony photo realism",
"natural light",
"warm realistic tone",
"high detail",
"clean composition",
)
return strings.Join(parts, ", ")
}
func (s *Server) resolveArtifactImageReference(ctx context.Context, record *job, artifactID, role string) (imageEditReference, error) {
item, ref, exists, err := s.store.artifactRef(record.UserID, artifactID)
if err != nil {
return imageEditReference{}, err
}
if !exists {
return imageEditReference{}, fmt.Errorf("%s artifact not found: %s", defaultString(role, "reference"), artifactID)
}
contentType, _, reader, err := s.newapi.openResultRef(ctx, record.UpstreamKey, ref)
if err != nil {
return imageEditReference{}, err
}
defer reader.Close()
content, err := io.ReadAll(reader)
if err != nil {
return imageEditReference{}, err
}
return imageEditReference{
Role: role,
Filename: item.Filename,
ContentType: defaultString(contentType, item.ContentType),
Content: content,
URL: artifactView(s.cfg, *item, time.Now().UTC()).URL,
}, nil
}
func buildGenericImageToImagePrompt(input map[string]any, prompt string) string {
parts := make([]string, 0, 6)
if prompt != "" {
parts = append(parts, prompt)
}
if refURL := strings.TrimSpace(stringValue(input["image"], input["reference_image_url"], input["image_url"])); refURL != "" {
parts = append(parts, "preserve the same main subject identity and key visual traits from the reference image")
}
if aspectRatio := strings.TrimSpace(stringValue(input["aspect_ratio"])); aspectRatio != "" {
parts = append(parts, "aspect ratio "+aspectRatio)
}
if len(parts) == 0 {
return "preserve the same main subject identity and render a polished high-detail image"
}
return strings.Join(parts, ", ")
}
func summarizeImageEditReferenceRoles(refs []imageEditReference) string {
if len(refs) == 0 {
return "none"
}
parts := make([]string, 0, len(refs))
for _, ref := range refs {
role := strings.TrimSpace(ref.Role)
if role == "" {
role = "reference"
}
parts = append(parts, role)
}
return strings.Join(parts, ",")
}
func stringValue(values ...any) string {
for _, value := range values {
if text, ok := value.(string); ok && strings.TrimSpace(text) != "" {
return text
}
}
return ""
}
func boolValue(value any) bool {
switch typed := value.(type) {
case bool:
return typed
default:
return false
}
}
func stringSliceValue(value any) []string {
switch typed := value.(type) {
case []string:
return cleanedStringValues(typed)
case []any:
items := make([]string, 0, len(typed))
for _, item := range typed {
items = append(items, strings.TrimSpace(fmt.Sprint(item)))
}
return cleanedStringValues(items)
default:
return nil
}
}
func cleanedStringValues(values []string) []string {
items := make([]string, 0, len(values))
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
items = append(items, trimmed)
}
}
return items
}
func buildVideoResultRef(baseURL, taskID, modelID string, task *videoTaskResult) resultRef {
urlValue := strings.TrimSpace(task.URL)
if urlValue == "" {
urlValue = strings.TrimRight(baseURL, "/") + "/v1/videos/" + taskID + "/content"
}
contentType := "video/mp4"
ext := ".mp4"
if format := strings.TrimSpace(task.Format); format != "" {
switch strings.ToLower(strings.TrimPrefix(format, ".")) {
case "mov", "quicktime":
contentType = "video/quicktime"
ext = ".mov"
case "webm":
contentType = "video/webm"
ext = ".webm"
case "mp4":
contentType = "video/mp4"
ext = ".mp4"
}
}
filename := filenameFromURL(urlValue, "")
if filename == "" || filename == "content" || !strings.Contains(filepath.Base(filename), ".") {
filename = sanitizeFilename(defaultString(modelID, "video")) + "-" + sanitizeFilename(taskID) + ext
}
if strings.HasPrefix(urlValue, "data:") {
return resultRef{
Kind: "data_url",
DataURL: urlValue,
Filename: filename,
ContentType: contentType,
}
}
return resultRef{
Kind: "url",
URL: urlValue,
Filename: filename,
ContentType: contentType,
}
}
const defaultAliceReferenceURL = "http://8.136.121.101:8790/media/Character_id_card/alice.jpg"
func (s *Server) authenticateSession(w http.ResponseWriter, r *http.Request) (session, bool) {
current, ok, err := s.sessionFromRequest(r)
if err != nil {
writeInternalError(w, "failed to load session", err)
return session{}, false
}
if !ok {
writeError(w, http.StatusUnauthorized, "UNAUTHENTICATED", "missing bearer key", nil)
return session{}, false
}
return current, true
}
func (s *Server) sessionFromRequest(r *http.Request) (session, bool, error) {
token, ok := bearerToken(r)
if !ok {
return session{}, false, nil
}
current, exists, err := s.store.session(token)
if err != nil {
return session{}, false, err
}
if !exists {
return session{}, false, nil
}
return current, true, nil
}
func bearerToken(r *http.Request) (string, bool) {
header := strings.TrimSpace(r.Header.Get("Authorization"))
if header == "" || !strings.HasPrefix(header, "Bearer ") {
return "", false
}
return strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")), true
}
func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
writeError(w, http.StatusBadRequest, "BAD_REQUEST", "failed to read request body", nil)
return false
}
if len(strings.TrimSpace(string(body))) == 0 {
body = []byte(`{}`)
}
if err := json.Unmarshal(body, dst); err != nil {
writeError(w, http.StatusBadRequest, "VALIDATION_ERROR", "invalid JSON body", map[string]any{
"details": err.Error(),
})
return false
}
return true
}
func intQuery(r *http.Request, key string, fallback int) int {
value := r.URL.Query().Get(key)
if value == "" {
return fallback
}
n, err := strconv.Atoi(value)
if err != nil || n < 0 {
return fallback
}
return n
}
func writeData(w http.ResponseWriter, status int, data any) {
writeJSON(w, status, map[string]any{
"ok": true,
"data": data,
})
}
func buildDataURL(contentType string, content []byte) string {
return "data:" + defaultString(strings.TrimSpace(contentType), "application/octet-stream") + ";base64," + base64.StdEncoding.EncodeToString(content)
}
func writeError(w http.ResponseWriter, status int, code, message string, details map[string]any) {
errBody := map[string]any{
"code": code,
"message": message,
}
for key, value := range details {
errBody[key] = value
}
writeJSON(w, status, map[string]any{
"ok": false,
"error": errBody,
})
}
func writeInternalError(w http.ResponseWriter, message string, err error) {
details := map[string]any{}
if err != nil {
details["details"] = err.Error()
}
writeError(w, http.StatusInternalServerError, "SERVER_ERROR", message, details)
}
func writeJSON(w http.ResponseWriter, status int, payload any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(payload)
}
func notFound(w http.ResponseWriter) {
writeError(w, http.StatusNotFound, "NOT_FOUND", "resource not found", nil)
}
func methodNotAllowed(w http.ResponseWriter) {
writeError(w, http.StatusMethodNotAllowed, "BAD_REQUEST", "method not allowed", nil)
}
func mustJSON(value any) string {
data, err := json.Marshal(value)
if err != nil {
return `{"level":"error","message":"marshal failed"}`
}
return string(data)
}
func defaultString(value, fallback string) string {
if value != "" {
return value
}
return fallback
}