262 lines
9.2 KiB
Go
262 lines
9.2 KiB
Go
package settings
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Runtime struct {
|
|
GuessMinIntervalSec int `json:"guess_min_interval_sec"`
|
|
ClientSubmitIntervalSec int `json:"client_submit_interval_sec"`
|
|
TaskRangeBits int `json:"task_range_bits"`
|
|
ActiveTaskCount int `json:"active_task_count"`
|
|
PresenceTTLSec int `json:"presence_ttl_sec"`
|
|
DefaultMaxNodes int `json:"default_max_nodes"`
|
|
PublicScorePrecision int `json:"public_score_precision"`
|
|
ArtifactPreset string `json:"artifact_preset"`
|
|
ArtifactProvider string `json:"artifact_provider"`
|
|
ArtifactModel string `json:"artifact_model"`
|
|
ArtifactPrompt string `json:"artifact_prompt"`
|
|
ArtifactNegativePrompt string `json:"artifact_negative_prompt"`
|
|
ArtifactWidth int `json:"artifact_width"`
|
|
ArtifactHeight int `json:"artifact_height"`
|
|
ArtifactSteps int `json:"artifact_steps"`
|
|
ArtifactQuality string `json:"artifact_quality"`
|
|
}
|
|
|
|
type Manager struct {
|
|
db *sql.DB
|
|
mu sync.RWMutex
|
|
v Runtime
|
|
}
|
|
|
|
func envInt(k string, def int) int {
|
|
if s := os.Getenv(k); s != "" {
|
|
if n, err := strconv.Atoi(s); err == nil {
|
|
return n
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envString(k, def string) string {
|
|
if s := strings.TrimSpace(os.Getenv(k)); s != "" {
|
|
return s
|
|
}
|
|
return def
|
|
}
|
|
|
|
func Defaults() Runtime {
|
|
return Runtime{
|
|
GuessMinIntervalSec: envInt("DEFAULT_GUESS_MIN_INTERVAL_SEC", 10),
|
|
ClientSubmitIntervalSec: envInt("DEFAULT_CLIENT_SUBMIT_INTERVAL_SEC", 11),
|
|
TaskRangeBits: envInt("DEFAULT_TASK_RANGE_BITS", 28),
|
|
ActiveTaskCount: envInt("DEFAULT_ACTIVE_TASK_COUNT", 1),
|
|
PresenceTTLSec: envInt("DEFAULT_PRESENCE_TTL_SEC", 35),
|
|
DefaultMaxNodes: envInt("DEFAULT_MAX_NODES", 2000),
|
|
PublicScorePrecision: envInt("DEFAULT_PUBLIC_SCORE_PRECISION", 2),
|
|
ArtifactPreset: strings.ToLower(envString("ARTIFACT_PRESET", "raccoon_full_art_v1")),
|
|
ArtifactProvider: strings.ToLower(envString("ARTIFACT_PROVIDER", "openai")),
|
|
ArtifactModel: envString("ARTIFACT_MODEL", "gpt-image-2"),
|
|
ArtifactPrompt: envString("ARTIFACT_PROMPT", "Optional global creative override for legacy artifact providers. The raccoon_full_art_v1 preset uses its built-in structured master prompt and task-specific NFT instructions."),
|
|
ArtifactNegativePrompt: envString("ARTIFACT_NEGATIVE_PROMPT", "text, watermark, logo, signature, low quality, blurry, malformed"),
|
|
ArtifactWidth: envInt("ARTIFACT_WIDTH", 1024),
|
|
ArtifactHeight: envInt("ARTIFACT_HEIGHT", 1536),
|
|
ArtifactSteps: envInt("ARTIFACT_STEPS", 28),
|
|
ArtifactQuality: strings.ToLower(envString("ARTIFACT_QUALITY", "medium")),
|
|
}
|
|
}
|
|
|
|
func New(ctx context.Context, db *sql.DB) (*Manager, error) {
|
|
m := &Manager{db: db, v: Defaults()}
|
|
b, _ := json.Marshal(m.v)
|
|
now := time.Now().UTC().UnixMilli()
|
|
_, err := db.ExecContext(ctx, `INSERT INTO settings(key,value,updated_at) VALUES('runtime',?,?) ON CONFLICT(key) DO NOTHING`, string(b), now)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := m.Refresh(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := m.migrateRIFTMediumDefault(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// migrateRIFTMediumDefault upgrades deployments created by the first RIFT
|
|
// release, where high quality was hard-coded as the collection default. The
|
|
// migration runs once only; after that an operator can still deliberately set
|
|
// high (or low) without it being overwritten on restart.
|
|
func (m *Manager) migrateRIFTMediumDefault(ctx context.Context) error {
|
|
const marker = "migration_rift_medium_quality_v1"
|
|
var n int
|
|
if err := m.db.QueryRowContext(ctx, `SELECT count(*) FROM settings WHERE key=?`, marker).Scan(&n); err != nil {
|
|
return err
|
|
}
|
|
if n > 0 {
|
|
return nil
|
|
}
|
|
// A deployment that explicitly configured ARTIFACT_QUALITY keeps that
|
|
// operator choice. The migration only changes the former implicit RIFT
|
|
// default.
|
|
if strings.TrimSpace(os.Getenv("ARTIFACT_QUALITY")) != "" {
|
|
_, err := m.db.ExecContext(ctx, `INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO NOTHING`, marker, "explicit-env-preserved", time.Now().UTC().UnixMilli())
|
|
return err
|
|
}
|
|
v := m.Get()
|
|
if strings.EqualFold(strings.TrimSpace(v.ArtifactPreset), "raccoon_full_art_v1") && strings.EqualFold(strings.TrimSpace(v.ArtifactQuality), "high") {
|
|
v.ArtifactQuality = "medium"
|
|
b, _ := json.Marshal(v)
|
|
now := time.Now().UTC().UnixMilli()
|
|
if _, err := m.db.ExecContext(ctx, `UPDATE settings SET value=?,updated_at=? WHERE key='runtime'`, string(b), now); err != nil {
|
|
return err
|
|
}
|
|
m.mu.Lock()
|
|
m.v = v
|
|
m.mu.Unlock()
|
|
}
|
|
_, err := m.db.ExecContext(ctx, `INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO NOTHING`, marker, "done", time.Now().UTC().UnixMilli())
|
|
return err
|
|
}
|
|
|
|
func (m *Manager) Get() Runtime {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.v
|
|
}
|
|
|
|
func (m *Manager) Refresh(ctx context.Context) error {
|
|
var raw string
|
|
if err := m.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key='runtime'`).Scan(&raw); err != nil {
|
|
return err
|
|
}
|
|
// Start from current defaults so databases created by older Neural Hunt
|
|
// versions automatically receive newly introduced settings.
|
|
var stored map[string]json.RawMessage
|
|
_ = json.Unmarshal([]byte(raw), &stored)
|
|
_, hadArtifactPreset := stored["artifact_preset"]
|
|
v := Defaults()
|
|
if err := json.Unmarshal([]byte(raw), &v); err != nil {
|
|
return err
|
|
}
|
|
|
|
// One-time upgrade for the bundled pre-collection database: older builds
|
|
// shipped the offline square SVG preset. The new collection is designed to
|
|
// work after only OPENAI_API_KEY + model are configured, so that exact legacy
|
|
// default is migrated to OpenAI portrait generation automatically. Explicit
|
|
// non-local provider choices are preserved.
|
|
if !hadArtifactPreset {
|
|
if strings.EqualFold(v.ArtifactProvider, "local") && v.ArtifactWidth == 1024 && v.ArtifactHeight == 1024 {
|
|
v.ArtifactPreset = "raccoon_full_art_v1"
|
|
v.ArtifactProvider = "openai"
|
|
v.ArtifactWidth = 1024
|
|
v.ArtifactHeight = 1536
|
|
v.ArtifactQuality = "medium"
|
|
} else {
|
|
// Existing deployments that deliberately chose another provider keep
|
|
// their old behavior until they explicitly opt into the collection preset.
|
|
v.ArtifactPreset = "legacy"
|
|
}
|
|
if b, err := json.Marshal(v); err == nil {
|
|
_, _ = m.db.ExecContext(ctx, `UPDATE settings SET value=?,updated_at=? WHERE key='runtime'`, string(b), time.Now().UTC().UnixMilli())
|
|
}
|
|
}
|
|
if err := Validate(v); err != nil {
|
|
return err
|
|
}
|
|
m.mu.Lock()
|
|
m.v = v
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) Update(ctx context.Context, v Runtime) error {
|
|
if err := Validate(v); err != nil {
|
|
return err
|
|
}
|
|
b, _ := json.Marshal(v)
|
|
if _, err := m.db.ExecContext(ctx, `UPDATE settings SET value=?,updated_at=? WHERE key='runtime'`, string(b), time.Now().UTC().UnixMilli()); err != nil {
|
|
return err
|
|
}
|
|
m.mu.Lock()
|
|
m.v = v
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func Validate(v Runtime) error {
|
|
if v.GuessMinIntervalSec < 1 || v.GuessMinIntervalSec > 3600 {
|
|
return fmt.Errorf("guess_min_interval_sec must be 1..3600")
|
|
}
|
|
if v.ClientSubmitIntervalSec <= v.GuessMinIntervalSec || v.ClientSubmitIntervalSec > 7200 {
|
|
return fmt.Errorf("client_submit_interval_sec must be > guess_min_interval_sec and <= 7200")
|
|
}
|
|
if v.TaskRangeBits < 8 || v.TaskRangeBits > 128 {
|
|
return fmt.Errorf("task_range_bits must be 8..128")
|
|
}
|
|
if v.ActiveTaskCount < 1 || v.ActiveTaskCount > 64 {
|
|
return fmt.Errorf("active_task_count must be 1..64")
|
|
}
|
|
if v.PresenceTTLSec < 15 || v.PresenceTTLSec > 600 {
|
|
return fmt.Errorf("presence_ttl_sec must be 15..600")
|
|
}
|
|
if v.DefaultMaxNodes < 50 || v.DefaultMaxNodes > 100000 {
|
|
return fmt.Errorf("default_max_nodes must be 50..100000")
|
|
}
|
|
if v.PublicScorePrecision < 0 || v.PublicScorePrecision > 6 {
|
|
return fmt.Errorf("public_score_precision must be 0..6")
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(v.ArtifactPreset)) {
|
|
case "legacy", "raccoon_full_art_v1":
|
|
default:
|
|
return fmt.Errorf("artifact_preset must be legacy or raccoon_full_art_v1")
|
|
}
|
|
switch strings.ToLower(v.ArtifactProvider) {
|
|
case "local", "openai", "comfyui", "a1111", "auto":
|
|
default:
|
|
return fmt.Errorf("artifact_provider must be local, openai, comfyui, a1111 or auto")
|
|
}
|
|
if strings.TrimSpace(v.ArtifactModel) == "" {
|
|
return fmt.Errorf("artifact_model must not be empty")
|
|
}
|
|
if len(v.ArtifactPrompt) < 10 || len(v.ArtifactPrompt) > 12000 {
|
|
return fmt.Errorf("artifact_prompt length must be 10..12000")
|
|
}
|
|
if len(v.ArtifactNegativePrompt) > 8000 {
|
|
return fmt.Errorf("artifact_negative_prompt too long")
|
|
}
|
|
if v.ArtifactWidth < 256 || v.ArtifactWidth > 4096 || v.ArtifactHeight < 256 || v.ArtifactHeight > 4096 {
|
|
return fmt.Errorf("artifact dimensions must be 256..4096")
|
|
}
|
|
if v.ArtifactSteps < 1 || v.ArtifactSteps > 200 {
|
|
return fmt.Errorf("artifact_steps must be 1..200")
|
|
}
|
|
switch strings.ToLower(v.ArtifactQuality) {
|
|
case "low", "medium", "high", "auto":
|
|
default:
|
|
return fmt.Errorf("artifact_quality must be low, medium, high or auto")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) Run(ctx context.Context) {
|
|
t := time.NewTicker(5 * time.Second)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-t.C:
|
|
_ = m.Refresh(ctx)
|
|
}
|
|
}
|
|
}
|