增加ai去特征
This commit is contained in:
@@ -223,6 +223,31 @@ func (h *AppSettingsHandler) CreditsPut(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "data": data})
|
||||
}
|
||||
|
||||
// DeAIGet returns the 去AI特征 per-tier surcharge. Also mounted publicly (the
|
||||
// 画图台 needs it to show the price next to the toggle) — prices aren't secret.
|
||||
func (h *AppSettingsHandler) DeAIGet(c *gin.Context) {
|
||||
data, err := h.settings.DeAI(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": "failed to load deai settings"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
func (h *AppSettingsHandler) DeAIPut(c *gin.Context) {
|
||||
var body service.DeAISettings
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
||||
return
|
||||
}
|
||||
data, err := h.settings.SaveDeAI(c.Request.Context(), body)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "data": data})
|
||||
}
|
||||
|
||||
func (h *AppSettingsHandler) LogsGet(c *gin.Context) {
|
||||
data, err := h.settings.Logs(c.Request.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -68,6 +68,7 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
|
||||
Resolution string `json:"resolution"`
|
||||
Duration string `json:"duration"`
|
||||
ReferenceImages []string `json:"reference_images"`
|
||||
DeAI bool `json:"deai"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
||||
@@ -81,6 +82,7 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
|
||||
Resolution: body.Resolution,
|
||||
Duration: body.Duration,
|
||||
ReferenceImages: body.ReferenceImages,
|
||||
DeAI: body.DeAI,
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
|
||||
@@ -65,6 +65,7 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
|
||||
publicAdmin.GET("/video-presets", handlers.UserGen.VideoPresets)
|
||||
publicAdmin.GET("/catalog", handlers.UserGen.Catalog)
|
||||
publicAdmin.GET("/models", handlers.UserGen.Models)
|
||||
publicAdmin.GET("/deai-pricing", handlers.AppSettings.DeAIGet)
|
||||
// 易支付 async notify — called server-to-server by the pay platform, no auth.
|
||||
publicAdmin.GET("/pay/notify", handlers.Payment.Notify)
|
||||
publicAdmin.POST("/pay/notify", handlers.Payment.Notify)
|
||||
@@ -177,6 +178,8 @@ func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.
|
||||
settings.PUT("/credits", handlers.AppSettings.CreditsPut)
|
||||
settings.GET("/logs", handlers.AppSettings.LogsGet)
|
||||
settings.PUT("/logs", handlers.AppSettings.LogsPut)
|
||||
settings.GET("/deai", handlers.AppSettings.DeAIGet)
|
||||
settings.PUT("/deai", handlers.AppSettings.DeAIPut)
|
||||
settings.GET("/media", handlers.AppSettings.MediaGet)
|
||||
settings.PUT("/media", handlers.AppSettings.MediaPut)
|
||||
settings.GET("/announcement", handlers.Announcement.AdminGet)
|
||||
|
||||
@@ -48,6 +48,14 @@ type CreditSettings struct {
|
||||
CDKRedeemEnabled bool `json:"cdk_redeem_enabled"`
|
||||
}
|
||||
|
||||
// DeAISettings is the per-tier surcharge (积分) for the 去AI特征 option on the
|
||||
// 画图台 — charged on top of the model's image price when the toggle is on.
|
||||
type DeAISettings struct {
|
||||
Price1K int `json:"price_1k"`
|
||||
Price2K int `json:"price_2k"`
|
||||
Price4K int `json:"price_4k"`
|
||||
}
|
||||
|
||||
type ProxySettings struct {
|
||||
Proxy string `json:"proxy"`
|
||||
}
|
||||
@@ -334,6 +342,44 @@ func (s *AppSettingsService) TestProxy(ctx context.Context, proxy string) (map[s
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AppSettingsService) DeAI(ctx context.Context) (*DeAISettings, error) {
|
||||
p1Raw, err := s.settings.GetValue(ctx, "deai.price_1k")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p2Raw, err := s.settings.GetValue(ctx, "deai.price_2k")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p4Raw, err := s.settings.GetValue(ctx, "deai.price_4k")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &DeAISettings{
|
||||
Price1K: clampNonNegative(parseIntSetting(p1Raw, 1)),
|
||||
Price2K: clampNonNegative(parseIntSetting(p2Raw, 2)),
|
||||
Price4K: clampNonNegative(parseIntSetting(p4Raw, 3)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AppSettingsService) SaveDeAI(ctx context.Context, in DeAISettings) (*DeAISettings, error) {
|
||||
if err := s.settings.UpsertValues(ctx, map[string]string{
|
||||
"deai.price_1k": strconv.Itoa(clampNonNegative(in.Price1K)),
|
||||
"deai.price_2k": strconv.Itoa(clampNonNegative(in.Price2K)),
|
||||
"deai.price_4k": strconv.Itoa(clampNonNegative(in.Price4K)),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.DeAI(ctx)
|
||||
}
|
||||
|
||||
func clampNonNegative(n int) int {
|
||||
if n < 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *AppSettingsService) Credits(ctx context.Context) (*CreditSettings, error) {
|
||||
checkinEnabledRaw, err := s.settings.GetValue(ctx, "credits.checkin_enabled")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
_ "image/gif"
|
||||
|
||||
_ "golang.org/x/image/webp"
|
||||
)
|
||||
|
||||
// applyDeAI post-processes a generated image to weaken AI-detection artifacts:
|
||||
// a small edge crop (kills border fingerprints), per-pixel low-amplitude noise,
|
||||
// a subtle brightness/contrast jitter, and a JPEG re-encode round-trip that
|
||||
// both introduces natural compression statistics and strips any embedded
|
||||
// metadata/watermark chunks. Output is PNG (matching the stored content type).
|
||||
func applyDeAI(b []byte) ([]byte, error) {
|
||||
src, _, err := image.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bounds := src.Bounds()
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
|
||||
// Crop a sliver off each edge (~0.3% of the dimension, 2–12px).
|
||||
cropX := clampInt(w*3/1000, 2, 12)
|
||||
cropY := clampInt(h*3/1000, 2, 12)
|
||||
if w <= cropX*4 || h <= cropY*4 {
|
||||
cropX, cropY = 0, 0
|
||||
}
|
||||
x0, y0 := bounds.Min.X+cropX, bounds.Min.Y+cropY
|
||||
x1, y1 := bounds.Max.X-cropX, bounds.Max.Y-cropY
|
||||
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
// Subtle tone jitter: contrast ±2%, brightness ±2 levels.
|
||||
contrast := 1.0 + (rng.Float64()-0.5)*0.04
|
||||
brightness := (rng.Float64() - 0.5) * 4.0
|
||||
|
||||
out := image.NewRGBA(image.Rect(0, 0, x1-x0, y1-y0))
|
||||
for y := y0; y < y1; y++ {
|
||||
for x := x0; x < x1; x++ {
|
||||
r, g, bl, a := src.At(x, y).RGBA()
|
||||
out.SetRGBA(x-x0, y-y0, color.RGBA{
|
||||
R: jitterChannel(r, contrast, brightness, rng),
|
||||
G: jitterChannel(g, contrast, brightness, rng),
|
||||
B: jitterChannel(bl, contrast, brightness, rng),
|
||||
A: uint8(a >> 8),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// JPEG round-trip: natural compression statistics + strips metadata.
|
||||
var jbuf bytes.Buffer
|
||||
if err := jpeg.Encode(&jbuf, out, &jpeg.Options{Quality: 93}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rt, err := jpeg.Decode(bytes.NewReader(jbuf.Bytes()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pbuf bytes.Buffer
|
||||
if err := png.Encode(&pbuf, rt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pbuf.Bytes(), nil
|
||||
}
|
||||
|
||||
// jitterChannel applies contrast/brightness around mid-gray plus ±2 noise to a
|
||||
// 16-bit color channel, returning the clamped 8-bit value.
|
||||
func jitterChannel(v uint32, contrast, brightness float64, rng *rand.Rand) uint8 {
|
||||
f := float64(v>>8)
|
||||
f = (f-128.0)*contrast + 128.0 + brightness + (rng.Float64()-0.5)*4.0
|
||||
if f < 0 {
|
||||
f = 0
|
||||
}
|
||||
if f > 255 {
|
||||
f = 255
|
||||
}
|
||||
return uint8(f + 0.5)
|
||||
}
|
||||
|
||||
func clampInt(n, lo, hi int) int {
|
||||
if n < lo {
|
||||
return lo
|
||||
}
|
||||
if n > hi {
|
||||
return hi
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -33,6 +33,9 @@ type UserGenerateRequest struct {
|
||||
Resolution string
|
||||
Duration string
|
||||
ReferenceImages []string
|
||||
// DeAI applies 去AI特征 post-processing to the generated image (image only)
|
||||
// and charges the per-tier surcharge on top of the model price.
|
||||
DeAI bool
|
||||
// AccountID pins an admin test to one specific provider account (账号生图测试).
|
||||
AccountID string
|
||||
}
|
||||
@@ -75,6 +78,7 @@ func (s *UserGenerationService) Generate(ctx context.Context, user *model.User,
|
||||
AspectRatio: in.Ratio,
|
||||
Resolution: in.Resolution,
|
||||
ReferenceImages: in.ReferenceImages,
|
||||
DeAI: in.DeAI,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -199,6 +199,10 @@ type V1ImageRequest struct {
|
||||
Resolution string
|
||||
N int
|
||||
ReferenceImages []string
|
||||
// DeAI applies 去AI特征 post-processing (crop / noise / tone jitter +
|
||||
// re-encode) to the output and charges the per-tier surcharge on top of
|
||||
// the model price. Playground-only; the /v1 OpenAI path never sets it.
|
||||
DeAI bool
|
||||
// BaseURL is the scheme+host of the inbound request (e.g. "https://host"),
|
||||
// used to build absolute, directly-downloadable output URLs. Empty falls
|
||||
// back to a relative "/images/..." path.
|
||||
@@ -584,6 +588,13 @@ func (s *V1Service) prepareImageExecution(ctx context.Context, principal *APIPri
|
||||
_ = s.events.UpdateStatus(ctx, eventID, "failed", "provider not implemented", 0)
|
||||
return nil, fmt.Errorf("%w: %s", ErrProviderUnsupported, modelItem.Provider)
|
||||
}
|
||||
// 去AI特征: post-process before storing/returning. Best-effort — a decode
|
||||
// failure keeps the original bytes rather than failing a paid generation.
|
||||
if in.DeAI {
|
||||
if processed, derr := applyDeAI(imageBytes); derr == nil {
|
||||
imageBytes = processed
|
||||
}
|
||||
}
|
||||
if !noStore {
|
||||
// Upload to RustFS. On failure the generation fails and credits are
|
||||
// refunded — we never fall back to local disk.
|
||||
@@ -1104,7 +1115,11 @@ func (s *V1Service) prepareImage(ctx context.Context, principal *APIPrincipal, i
|
||||
resolution = fb
|
||||
}
|
||||
}
|
||||
price, err := s.chargeForModel(ctx, principal, modelItem, "image", resolution, "", charge)
|
||||
var surcharge float64
|
||||
if in.DeAI {
|
||||
surcharge = s.deaiSurcharge(ctx, resolution)
|
||||
}
|
||||
price, err := s.chargeForModel(ctx, principal, modelItem, "image", resolution, "", surcharge, charge)
|
||||
if err != nil {
|
||||
return nil, "", "", 0, err
|
||||
}
|
||||
@@ -1172,14 +1187,14 @@ func (s *V1Service) prepareVideo(ctx context.Context, principal *APIPrincipal, i
|
||||
if resolution == "" {
|
||||
resolution = "720p"
|
||||
}
|
||||
price, err := s.chargeForModel(ctx, principal, modelItem, "video", resolution, duration, charge)
|
||||
price, err := s.chargeForModel(ctx, principal, modelItem, "video", resolution, duration, 0, charge)
|
||||
if err != nil {
|
||||
return nil, "", "", "", 0, err
|
||||
}
|
||||
return modelItem, resolution, aspectRatio, duration, price, nil
|
||||
}
|
||||
|
||||
func (s *V1Service) chargeForModel(ctx context.Context, principal *APIPrincipal, modelItem *model.ModelConfig, kind, resolution, duration string, charge bool) (float64, error) {
|
||||
func (s *V1Service) chargeForModel(ctx context.Context, principal *APIPrincipal, modelItem *model.ModelConfig, kind, resolution, duration string, surcharge float64, charge bool) (float64, error) {
|
||||
// 代理用户走代理价(某档未设代理价则回退普通价)。principal.User 即将被扣费的
|
||||
// 用户,无论画图台还是 key 调用都从这里取,所以一处即覆盖所有路径。
|
||||
agent := principal != nil && principal.User != nil && principal.User.Role == "agent"
|
||||
@@ -1187,6 +1202,7 @@ func (s *V1Service) chargeForModel(ctx context.Context, principal *APIPrincipal,
|
||||
if !ok {
|
||||
return 0, ErrUnsupportedParams
|
||||
}
|
||||
price += surcharge
|
||||
if !charge || principal == nil || principal.User == nil {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -2863,6 +2879,30 @@ func guessRatio(w, h int) string {
|
||||
// firstPricedResolution returns the model's lowest priced image tier (1K/2K/4K
|
||||
// order), or "" if none is priced. Used to rescue a request whose resolution
|
||||
// the model doesn't support.
|
||||
// deaiSurcharge returns the 去AI特征 surcharge (积分) for an image resolution
|
||||
// tier, from site settings (defaults: 1K=1, 2K=2, 4K=3).
|
||||
func (s *V1Service) deaiSurcharge(ctx context.Context, resolution string) float64 {
|
||||
key, def := "deai.price_1k", 1
|
||||
switch strings.ToUpper(strings.TrimSpace(resolution)) {
|
||||
case "2K":
|
||||
key, def = "deai.price_2k", 2
|
||||
case "4K":
|
||||
key, def = "deai.price_4k", 3
|
||||
}
|
||||
if s.settings == nil {
|
||||
return float64(def)
|
||||
}
|
||||
raw, err := s.settings.GetValue(ctx, key)
|
||||
if err != nil {
|
||||
return float64(def)
|
||||
}
|
||||
n := parseIntSetting(raw, def)
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
return float64(n)
|
||||
}
|
||||
|
||||
func firstPricedResolution(item *model.ModelConfig) string {
|
||||
if item == nil {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user