Gunnar

mikronova/kyokkou

docker/agent/main.go

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"sync"
)

const (
	blenderBin = "/opt/blender/blender"
	scenesDir  = "/agent/scenes"
	configPath = "/in/config.json"
)

var scenes = map[string]string{
	"fluid_pipes":       "fluid_pipes.py",
	"god_rays":          "fluid_pipes.py",
	"metal_reflections": "fluid_pipes.py",
	"water_liquid":      "fluid_pipes.py",
	"volumetric_clouds": "fluid_pipes.py",
}

var mu sync.Mutex

func main() {
	http.HandleFunc("/health", health)
	http.HandleFunc("/render", render)
	log.Println("kyokkou agent listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func health(w http.ResponseWriter, _ *http.Request) {
	if _, err := os.Stat(blenderBin); err != nil {
		http.Error(w, "blender not found at "+blenderBin, http.StatusServiceUnavailable)
		return
	}
	fmt.Fprint(w, "ok")
}

func render(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "POST only", http.StatusMethodNotAllowed)
		return
	}

	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	var cfg struct {
		SceneType string `json:"scene_type"`
	}
	if err := json.Unmarshal(body, &cfg); err != nil {
		http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
		return
	}

	script, ok := scenes[cfg.SceneType]
	if !ok {
		http.Error(w, "unknown scene_type: "+cfg.SceneType, http.StatusBadRequest)
		return
	}

	if err := os.WriteFile(configPath, body, 0644); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	mu.Lock()
	defer mu.Unlock()

	cmd := exec.Command(blenderBin,
		"--background",
		"--python", filepath.Join(scenesDir, script),
		"--", "--config", configPath,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		http.Error(w, "blender: "+err.Error(), http.StatusInternalServerError)
		return
	}

	fmt.Fprint(w, "ok")
}