Files
groot 47c523dd98
All checks were successful
release-tag / release-image (push) Successful in 3m51s
RC-14
2026-08-14 06:17:30 +02:00

722 lines
24 KiB
Go

package artifact
import (
"bytes"
"context"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"neuralhunt/internal/settings"
)
type Worker struct {
db *sql.DB
dir string
publicBase string
settings *settings.Manager
http *http.Client
wake chan struct{}
budgetHoldMu sync.Mutex
budgetHoldUntil time.Time
}
func New(db *sql.DB, dir string, sm *settings.Manager) (*Worker, error) {
if dir == "" {
dir = "./data/artifacts"
}
abs, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
if err := os.MkdirAll(abs, 0o750); err != nil {
return nil, err
}
return &Worker{
db: db,
dir: abs,
publicBase: strings.TrimRight(os.Getenv("ARTIFACT_PUBLIC_BASE_URL"), "/"),
settings: sm,
http: &http.Client{Timeout: envDuration("ARTIFACT_HTTP_TIMEOUT", 4*time.Minute)},
wake: make(chan struct{}, 1),
}, nil
}
func envDuration(k string, d time.Duration) time.Duration {
if raw := strings.TrimSpace(os.Getenv(k)); raw != "" {
if v, err := time.ParseDuration(raw); err == nil {
return v
}
}
return d
}
type win struct {
ID, Seed, Winner, ProvenanceWinner, Worker, Signature, Guess string
Origin string
RarityOverride string
Rarity string
DisplayName string
RangeBits int
Completed time.Time
PublicJWK json.RawMessage
PromptInstructions string
NegativePrompt string
StyleReference string
BeaconPath string
BeaconBoostedPath string
BeaconRound uint64
}
func (w *Worker) Run(ctx context.Context) {
t := time.NewTicker(3 * time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
case <-w.wake:
}
if err := w.one(ctx); err != nil {
log.Printf("artifact worker: %v", err)
}
}
}
// Notify wakes the artifact worker after an admin action has queued work. The
// channel is deliberately coalescing: one wake-up is enough even if an admin
// creates several drops at once. The normal ticker remains as a safety net.
func (w *Worker) Notify() {
if w == nil || w.wake == nil {
return
}
select {
case w.wake <- struct{}{}:
default:
}
}
func (w *Worker) claim(ctx context.Context) (win, error) {
tx, err := w.db.BeginTx(ctx, nil)
if err != nil {
return win{}, err
}
defer tx.Rollback()
var x win
var completedMS int64
var raw string
err = tx.QueryRowContext(ctx, `SELECT t.id,t.public_seed,COALESCE(t.artifact_owner_client_id,t.winner_client_id),COALESCE(t.winner_client_id,''),COALESCE(t.winner_worker_client_id,t.winner_client_id,t.artifact_owner_client_id),COALESCE(t.winner_signature,''),COALESCE(t.winning_guess,''),t.display_name,t.range_bits,t.completed_at,c.public_jwk,t.nft_prompt_instructions,t.nft_negative_prompt,t.nft_style_reference,t.winner_beacon_path,t.winner_beacon_boosted_path,t.winner_beacon_round,t.artifact_origin,COALESCE(t.artifact_rarity_override,''),COALESCE(t.artifact_rarity,'')
FROM tasks t JOIN clients c ON c.id=COALESCE(t.winner_worker_client_id,t.winner_client_id,t.artifact_owner_client_id)
WHERE t.artifact_status='pending' AND COALESCE(t.artifact_owner_client_id,t.winner_client_id) IS NOT NULL ORDER BY t.completed_at LIMIT 1`).Scan(&x.ID, &x.Seed, &x.Winner, &x.ProvenanceWinner, &x.Worker, &x.Signature, &x.Guess, &x.DisplayName, &x.RangeBits, &completedMS, &raw, &x.PromptInstructions, &x.NegativePrompt, &x.StyleReference, &x.BeaconPath, &x.BeaconBoostedPath, &x.BeaconRound, &x.Origin, &x.RarityOverride, &x.Rarity)
if err != nil {
return win{}, err
}
x.Completed = time.UnixMilli(completedMS).UTC()
x.PublicJWK = json.RawMessage(raw)
if _, err = tx.ExecContext(ctx, `UPDATE tasks SET artifact_status='generating',artifact_error=NULL WHERE id=? AND artifact_status='pending'`, x.ID); err != nil {
return win{}, err
}
if err = tx.Commit(); err != nil {
return win{}, err
}
return x, nil
}
func (w *Worker) ownerTraitUsage(ctx context.Context, ownerClientID, excludeTaskID string) (*traitUsage, error) {
ownerClientID = strings.TrimSpace(ownerClientID)
if ownerClientID == "" {
return nil, nil
}
rows, err := w.db.QueryContext(ctx, `SELECT id,public_seed,COALESCE(artifact_owner_client_id,winner_client_id),COALESCE(winning_guess,''),COALESCE(artifact_origin,'win')
FROM tasks
WHERE COALESCE(artifact_owner_client_id,winner_client_id)=? AND id<>? AND status='completed' AND artifact_status='ready'
ORDER BY COALESCE(completed_at,created_at) DESC LIMIT 128`, ownerClientID, excludeTaskID)
if err != nil {
return nil, err
}
defer rows.Close()
usage := newTraitUsage()
for rows.Next() {
var prior win
if err := rows.Scan(&prior.ID, &prior.Seed, &prior.Winner, &prior.Guess, &prior.Origin); err != nil {
return nil, err
}
traits := deriveCollectionTraits(prior)
usage.Themes[traits.ThemeName]++
usage.Poses[traits.Pose]++
usage.Atmospheres[traits.Atmosphere]++
usage.Cameras[traits.Camera]++
usage.Palettes[traits.PaletteStory]++
usage.SceneFocuses[traits.SceneFocus]++
}
if err := rows.Err(); err != nil {
return nil, err
}
return usage, nil
}
type imageResult struct {
Bytes []byte
Ext string
Provider string
Meta map[string]any
Usage *imageUsage
EstimatedCostUSD *float64
PricingBasis string
}
func (w *Worker) budgetHoldActive() bool {
w.budgetHoldMu.Lock()
defer w.budgetHoldMu.Unlock()
return time.Now().Before(w.budgetHoldUntil)
}
func (w *Worker) holdBudgetFor(d time.Duration) {
if d < time.Second {
d = time.Minute
}
w.budgetHoldMu.Lock()
until := time.Now().Add(d)
if until.After(w.budgetHoldUntil) {
w.budgetHoldUntil = until
}
w.budgetHoldMu.Unlock()
}
func (w *Worker) deferBudget(ctx context.Context, taskID string, err error) {
msg := err.Error()
_, _ = w.db.ExecContext(ctx, `UPDATE tasks SET artifact_status='pending',artifact_error=? WHERE id=? AND artifact_status='generating'`, msg, taskID)
}
func (w *Worker) one(ctx context.Context) error {
if w.budgetHoldActive() {
return nil
}
x, err := w.claim(ctx)
if errors.Is(err, sql.ErrNoRows) {
return nil
}
if err != nil {
return err
}
cfg := w.settings.Get()
preset := strings.ToLower(strings.TrimSpace(cfg.ArtifactPreset))
rarityDist := rarityDistributionFromPercentages(
cfg.ArtifactRarityCommonPct,
cfg.ArtifactRarityUncommonPct,
cfg.ArtifactRarityRarePct,
cfg.ArtifactRarityUltraRarePct,
cfg.ArtifactRaritySpecialIllustrationPct,
)
effectiveRarity := x.Rarity
if strings.TrimSpace(effectiveRarity) == "" {
effectiveRarity = x.RarityOverride
}
traits := deriveCollectionTraitsConfigured(x, nil, rarityDist, effectiveRarity)
if preset == collectionPresetRaccoon && strings.EqualFold(strings.TrimSpace(x.Origin), "admin_drop") {
if usage, usageErr := w.ownerTraitUsage(ctx, x.Winner, x.ID); usageErr != nil {
log.Printf("artifact worker task %s owner trait usage unavailable: %v", x.ID, usageErr)
} else if usage != nil {
traits = deriveCollectionTraitsConfigured(x, usage, rarityDist, effectiveRarity)
}
}
if preset == collectionPresetRaccoon && strings.TrimSpace(x.Rarity) == "" {
if _, rarityErr := w.db.ExecContext(ctx, `UPDATE tasks SET artifact_rarity=? WHERE id=? AND artifact_rarity=''`, traits.Rarity, x.ID); rarityErr != nil {
w.fail(ctx, x.ID, rarityErr)
return fmt.Errorf("task %s: persist rarity: %w", x.ID, rarityErr)
}
x.Rarity = traits.Rarity
}
prompt := ""
negativePrompt := ""
if preset == collectionPresetRaccoon {
prompt = buildCollectionPrompt(x, traits)
negativePrompt = collectionNegativePrompt(x)
} else {
prompt = buildPrompt(cfg.ArtifactPrompt, x.PromptInstructions, x)
negativePrompt = joinPrompt(cfg.ArtifactNegativePrompt, x.NegativePrompt)
}
img, err := w.generate(ctx, cfg, x, prompt, negativePrompt)
if err != nil {
var budgetErr *OpenAIBudgetError
if errors.As(err, &budgetErr) {
w.deferBudget(ctx, x.ID, err)
w.holdBudgetFor(budgetErr.RetryAfter)
log.Printf("artifact worker task %s deferred by OpenAI circuit breaker: %v", x.ID, err)
return nil
}
w.fail(ctx, x.ID, err)
return fmt.Errorf("task %s: %w", x.ID, err)
}
if len(img.Bytes) == 0 {
err := errors.New("image provider returned empty output")
w.fail(ctx, x.ID, err)
return fmt.Errorf("task %s: %w", x.ID, err)
}
if img.Ext == "" {
img.Ext = "png"
}
idSum := sha256.Sum256([]byte(x.ID + "|" + x.Winner + "|" + x.Signature))
artifactID := "artifact_" + hex.EncodeToString(idSum[:12])
promptSum := sha256.Sum256([]byte(prompt))
rawArtSum := sha256.Sum256(img.Bytes)
finalBytes := img.Bytes
finalExt := strings.TrimPrefix(strings.ToLower(img.Ext), ".")
if preset == collectionPresetRaccoon {
finalBytes = renderCardSVG(img.Bytes, img.Ext, x, traits)
finalExt = "svg"
}
finalSum := sha256.Sum256(finalBytes)
manifest := map[string]any{
"artifact_id": artifactID,
"artifact_preset": preset,
"task_id": x.ID,
"task_display_name": x.DisplayName,
"task_range_bits": x.RangeBits,
"artifact_origin": x.Origin,
"artifact_rarity_override": x.RarityOverride,
"artifact_rarity": x.Rarity,
"artifact_owner_client_id": x.Winner,
"winner_client_id": x.ProvenanceWinner,
"winner_worker_client_id": x.Worker,
"winning_beacon_path": x.BeaconPath,
"winning_beacon_boosted_path": x.BeaconBoostedPath,
"winning_beacon_round": x.BeaconRound,
"winning_worker_public_jwk": json.RawMessage(x.PublicJWK),
"winning_guess": x.Guess,
"winner_guess_signature": x.Signature,
"completed_at": x.Completed,
"image_sha256": hex.EncodeToString(finalSum[:]),
"raw_art_sha256": hex.EncodeToString(rawArtSum[:]),
"prompt_sha256": hex.EncodeToString(promptSum[:]),
"task_prompt_instructions": x.PromptInstructions,
"task_style_reference": x.StyleReference,
"provider": img.Provider,
"provider_meta": img.Meta,
"note": "artifact_owner_client_id is current ownership at mint time; winner fields preserve game provenance when present; image_sha256 binds the final card",
}
if preset == collectionPresetRaccoon {
manifest["collection_character"] = "RIFT"
manifest["collection_traits"] = traits
manifest["layout"] = map[string]any{
"format": "svg",
"width": 1024,
"height": 1536,
"mode": "programmatic-full-art-card-v1",
}
}
mb, _ := json.MarshalIndent(manifest, "", " ")
relDir := filepath.Join("artifacts", artifactID)
outDir := filepath.Join(w.dir, artifactID)
if err := os.MkdirAll(outDir, 0o750); err != nil {
w.fail(ctx, x.ID, err)
return fmt.Errorf("task %s: %w", x.ID, err)
}
if preset == collectionPresetRaccoon {
rawName := "art." + strings.TrimPrefix(strings.ToLower(img.Ext), ".")
if err := atomicWrite(filepath.Join(outDir, rawName), img.Bytes, 0o640); err != nil {
w.fail(ctx, x.ID, err)
return err
}
}
imageName := "image." + finalExt
if err := atomicWrite(filepath.Join(outDir, imageName), finalBytes, 0o640); err != nil {
w.fail(ctx, x.ID, err)
return fmt.Errorf("task %s: %w", x.ID, err)
}
if err := atomicWrite(filepath.Join(outDir, "manifest.json"), mb, 0o640); err != nil {
w.fail(ctx, x.ID, err)
return fmt.Errorf("task %s: %w", x.ID, err)
}
imgURI := w.uri(filepath.ToSlash(filepath.Join(relDir, imageName)))
manURI := w.uri(filepath.ToSlash(filepath.Join(relDir, "manifest.json")))
_, err = w.db.ExecContext(ctx, `UPDATE tasks SET artifact_status='ready',artifact_uri=?,artifact_manifest_uri=?,artifact_error=NULL WHERE id=?`, imgURI, manURI, x.ID)
if err != nil {
w.fail(ctx, x.ID, err)
return fmt.Errorf("task %s: finalize artifact: %w", x.ID, err)
}
log.Printf("artifact worker task %s ready: %s", x.ID, imgURI)
return nil
}
func joinPrompt(parts ...string) string {
out := make([]string, 0, len(parts))
for _, p := range parts {
if p = strings.TrimSpace(p); p != "" {
out = append(out, p)
}
}
return strings.Join(out, "\n")
}
func buildPrompt(base, taskInstructions string, x win) string {
winner := x.Winner
if len(winner) > 24 {
winner = winner[:24]
}
creative := joinPrompt(base, taskInstructions)
return creative + fmt.Sprintf("\nTask fingerprint: %s. Winner fingerprint: %s. Difficulty: %d-bit probability space. Public seed fingerprint: %s. Treat these values only as deterministic creative seeds; do not render them as readable text.", x.ID, winner, x.RangeBits, shortHash(x.Seed))
}
func shortHash(s string) string {
h := sha256.Sum256([]byte(s))
return hex.EncodeToString(h[:8])
}
func (w *Worker) generate(ctx context.Context, cfg settings.Runtime, x win, prompt, negativePrompt string) (imageResult, error) {
provider := strings.ToLower(strings.TrimSpace(cfg.ArtifactProvider))
switch provider {
case "local":
return imageResult{Bytes: procedural(x), Ext: "svg", Provider: "local-procedural", Meta: map[string]any{"model": "deterministic-svg"}}, nil
case "openai":
return w.openAI(ctx, cfg, x, prompt)
case "comfyui":
return w.comfyUI(ctx, cfg, x, prompt, negativePrompt)
case "a1111":
return w.a1111(ctx, cfg, x, prompt, negativePrompt)
case "auto":
var errs []string
if strings.TrimSpace(os.Getenv("OPENAI_API_KEY")) != "" {
if r, err := w.openAI(ctx, cfg, x, prompt); err == nil {
return r, nil
} else {
errs = append(errs, "openai: "+err.Error())
}
}
if strings.TrimSpace(os.Getenv("COMFYUI_URL")) != "" && strings.TrimSpace(os.Getenv("COMFYUI_WORKFLOW_PATH")) != "" {
if r, err := w.comfyUI(ctx, cfg, x, prompt, negativePrompt); err == nil {
return r, nil
} else {
errs = append(errs, "comfyui: "+err.Error())
}
}
if strings.TrimSpace(os.Getenv("A1111_URL")) != "" {
if r, err := w.a1111(ctx, cfg, x, prompt, negativePrompt); err == nil {
return r, nil
} else {
errs = append(errs, "a1111: "+err.Error())
}
}
meta := map[string]any{"model": "deterministic-svg"}
if len(errs) > 0 {
meta["fallback_errors"] = errs
}
return imageResult{Bytes: procedural(x), Ext: "svg", Provider: "local-procedural-fallback", Meta: meta}, nil
default:
return imageResult{}, fmt.Errorf("unknown artifact provider %q", provider)
}
}
func (w *Worker) comfyUI(ctx context.Context, cfg settings.Runtime, x win, prompt, negativePrompt string) (imageResult, error) {
base := strings.TrimRight(strings.TrimSpace(os.Getenv("COMFYUI_URL")), "/")
workflowPath := strings.TrimSpace(os.Getenv("COMFYUI_WORKFLOW_PATH"))
if base == "" || workflowPath == "" {
return imageResult{}, errors.New("COMFYUI_URL and COMFYUI_WORKFLOW_PATH are required")
}
raw, err := os.ReadFile(workflowPath)
if err != nil {
return imageResult{}, fmt.Errorf("read ComfyUI workflow: %w", err)
}
var workflow any
if err := json.Unmarshal(raw, &workflow); err != nil {
return imageResult{}, fmt.Errorf("parse ComfyUI workflow: %w", err)
}
seed := deterministicSeed(x)
replacements := map[string]string{
"{{PROMPT}}": prompt,
"{{NEGATIVE_PROMPT}}": negativePrompt,
"{{SEED}}": strconv.FormatInt(seed, 10),
"{{WIDTH}}": strconv.Itoa(cfg.ArtifactWidth),
"{{HEIGHT}}": strconv.Itoa(cfg.ArtifactHeight),
"{{STEPS}}": strconv.Itoa(cfg.ArtifactSteps),
"{{MODEL}}": cfg.ArtifactModel,
}
workflow = replaceJSON(workflow, replacements)
clientID := "neuralhunt-" + shortHash(x.ID)
payload := map[string]any{"prompt": workflow, "client_id": clientID}
var queued struct {
PromptID string `json:"prompt_id"`
Error any `json:"error"`
Nodes map[string]any `json:"node_errors"`
}
if err := w.doJSON(ctx, http.MethodPost, base+"/prompt", payload, &queued, "", ""); err != nil {
return imageResult{}, err
}
if queued.PromptID == "" {
return imageResult{}, fmt.Errorf("ComfyUI rejected workflow: error=%v node_errors=%v", queued.Error, queued.Nodes)
}
deadline := time.Now().Add(envDuration("COMFYUI_POLL_TIMEOUT", 4*time.Minute))
for time.Now().Before(deadline) {
select {
case <-ctx.Done():
return imageResult{}, ctx.Err()
case <-time.After(900 * time.Millisecond):
}
resp, err := w.http.Get(base + "/history/" + url.PathEscape(queued.PromptID))
if err != nil {
continue
}
b, readErr := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
resp.Body.Close()
if readErr != nil || resp.StatusCode/100 != 2 {
continue
}
var history map[string]any
if json.Unmarshal(b, &history) != nil {
continue
}
entry, ok := history[queued.PromptID].(map[string]any)
if !ok {
continue
}
ref, ok := findComfyImage(entry["outputs"])
if !ok {
continue
}
q := url.Values{}
q.Set("filename", ref.Filename)
q.Set("subfolder", ref.Subfolder)
q.Set("type", ref.Type)
imgResp, err := w.http.Get(base + "/view?" + q.Encode())
if err != nil {
return imageResult{}, err
}
img, err := io.ReadAll(io.LimitReader(imgResp.Body, 64<<20))
imgResp.Body.Close()
if err != nil {
return imageResult{}, err
}
if imgResp.StatusCode/100 != 2 {
return imageResult{}, fmt.Errorf("ComfyUI /view HTTP %d", imgResp.StatusCode)
}
ext := strings.TrimPrefix(strings.ToLower(filepath.Ext(ref.Filename)), ".")
if ext == "" {
ext = "png"
}
return imageResult{Bytes: img, Ext: ext, Provider: "comfyui", Meta: map[string]any{"prompt_id": queued.PromptID, "model": cfg.ArtifactModel, "seed": seed}}, nil
}
return imageResult{}, errors.New("ComfyUI generation timed out")
}
type comfyImageRef struct{ Filename, Subfolder, Type string }
func findComfyImage(v any) (comfyImageRef, bool) {
switch x := v.(type) {
case map[string]any:
if fn, ok := x["filename"].(string); ok && fn != "" {
sub, _ := x["subfolder"].(string)
typ, _ := x["type"].(string)
if typ == "" {
typ = "output"
}
return comfyImageRef{fn, sub, typ}, true
}
for _, child := range x {
if r, ok := findComfyImage(child); ok {
return r, true
}
}
case []any:
for _, child := range x {
if r, ok := findComfyImage(child); ok {
return r, true
}
}
}
return comfyImageRef{}, false
}
func replaceJSON(v any, repl map[string]string) any {
switch x := v.(type) {
case map[string]any:
for k, child := range x {
x[k] = replaceJSON(child, repl)
}
return x
case []any:
for i := range x {
x[i] = replaceJSON(x[i], repl)
}
return x
case string:
original := x
for from, to := range repl {
x = strings.ReplaceAll(x, from, to)
}
// Exact numeric placeholders become JSON numbers where possible.
if original == "{{SEED}}" || original == "{{WIDTH}}" || original == "{{HEIGHT}}" || original == "{{STEPS}}" {
if n, err := strconv.ParseInt(x, 10, 64); err == nil {
return n
}
}
return x
default:
return v
}
}
func (w *Worker) a1111(ctx context.Context, cfg settings.Runtime, x win, prompt, negativePrompt string) (imageResult, error) {
base := strings.TrimRight(strings.TrimSpace(os.Getenv("A1111_URL")), "/")
if base == "" {
return imageResult{}, errors.New("A1111_URL is not configured")
}
seed := deterministicSeed(x)
payload := map[string]any{
"prompt": prompt,
"negative_prompt": negativePrompt,
"steps": cfg.ArtifactSteps,
"width": cfg.ArtifactWidth,
"height": cfg.ArtifactHeight,
"seed": seed,
"cfg_scale": envFloat("A1111_CFG_SCALE", 7.0),
}
if sampler := strings.TrimSpace(os.Getenv("A1111_SAMPLER")); sampler != "" {
payload["sampler_name"] = sampler
}
if model := strings.TrimSpace(cfg.ArtifactModel); model != "" && !strings.HasPrefix(strings.ToLower(model), "gpt-image") {
payload["override_settings"] = map[string]any{"sd_model_checkpoint": model}
}
var out struct {
Images []string `json:"images"`
Info string `json:"info"`
}
if err := w.doJSON(ctx, http.MethodPost, base+"/sdapi/v1/txt2img", payload, &out, os.Getenv("A1111_USER"), os.Getenv("A1111_PASSWORD")); err != nil {
return imageResult{}, err
}
if len(out.Images) == 0 {
return imageResult{}, errors.New("A1111 returned no images")
}
b64 := out.Images[0]
if i := strings.Index(b64, ","); strings.HasPrefix(b64, "data:") && i >= 0 {
b64 = b64[i+1:]
}
img, err := base64.StdEncoding.DecodeString(b64)
if err != nil {
return imageResult{}, err
}
return imageResult{Bytes: img, Ext: "png", Provider: "a1111", Meta: map[string]any{"seed": seed, "steps": cfg.ArtifactSteps, "model": cfg.ArtifactModel}}, nil
}
func (w *Worker) doJSON(ctx context.Context, method, endpoint string, payload any, out any, user, pass string) error {
body, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, method, endpoint, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if strings.TrimSpace(user) != "" {
req.SetBasicAuth(user, pass)
}
resp, err := w.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 32<<20))
if err != nil {
return err
}
if resp.StatusCode/100 != 2 {
return fmt.Errorf("HTTP %d from %s: %s", resp.StatusCode, endpoint, truncate(string(raw), 1200))
}
if out == nil {
return nil
}
if err := json.Unmarshal(raw, out); err != nil {
return fmt.Errorf("decode %s: %w", endpoint, err)
}
return nil
}
func envFloat(k string, d float64) float64 {
if raw := strings.TrimSpace(os.Getenv(k)); raw != "" {
if v, err := strconv.ParseFloat(raw, 64); err == nil {
return v
}
}
return d
}
func deterministicSeed(x win) int64 {
h := sha256.Sum256([]byte(x.ID + "|" + x.Winner + "|" + x.Seed))
var n uint64
for i := 0; i < 8; i++ {
n = (n << 8) | uint64(h[i])
}
return int64(n & 0x7fffffffffffffff)
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "…"
}
func (w *Worker) uri(rel string) string {
p := "/" + strings.TrimLeft(rel, "/")
if w.publicBase == "" {
return p
}
return w.publicBase + p
}
func (w *Worker) fail(ctx context.Context, taskID string, err error) {
_, _ = w.db.ExecContext(ctx, `UPDATE tasks SET artifact_status='error',artifact_error=? WHERE id=?`, err.Error(), taskID)
}
func atomicWrite(path string, data []byte, mode os.FileMode) error {
tmp := path + ".tmp"
if err := os.WriteFile(tmp, data, mode); err != nil {
return err
}
_ = os.Remove(path)
return os.Rename(tmp, path)
}
func procedural(x win) []byte {
h := sha256.Sum256([]byte(x.ID + x.Winner + x.Seed))
a := int(h[0]) % 360
b := int(h[1]) % 360
c := int(h[2]) % 360
svg := fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024"><defs><radialGradient id="g"><stop stop-color="hsl(%d 100%% 70%%)"/><stop offset=".55" stop-color="hsl(%d 85%% 45%%)"/><stop offset="1" stop-color="#04111f"/></radialGradient><filter id="gl"><feGaussianBlur stdDeviation="8"/></filter></defs><rect width="1024" height="1024" fill="#04111f"/><circle cx="512" cy="512" r="390" fill="none" stroke="#6574ff" opacity=".18" stroke-width="2"/><circle cx="512" cy="512" r="290" fill="url(#g)" opacity=".22" filter="url(#gl)"/><g fill="hsl(%d 100%% 75%%)">`, a, b, c)
for i := 0; i < 480; i++ {
v := int(h[i%32])
xv := 512 + ((i*73 + v*11) % 620) - 310
yv := 512 + ((i*97 + v*7) % 620) - 310
r := 1 + (v % 5)
svg += fmt.Sprintf(`<circle cx="%d" cy="%d" r="%d" opacity=".65"/>`, xv, yv, r)
}
svg += fmt.Sprintf(`</g><text x="52" y="900" fill="#d9f7ff" font-family="monospace" font-size="28">%s</text><text x="52" y="940" fill="#7fe7ff" font-family="monospace" font-size="18">winner %s</text></svg>`, x.ID, x.Winner[:min(20, len(x.Winner))])
return []byte(svg)
}
func min(a, b int) int {
if a < b {
return a
}
return b
}