增加ai去特征

This commit is contained in:
2026-07-09 23:48:58 +08:00
parent 21cca6513b
commit 6d6bd2a638
9 changed files with 290 additions and 6 deletions
+46
View File
@@ -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 {
+95
View File
@@ -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, 212px).
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
+43 -3
View File
@@ -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 ""