增加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})
|
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) {
|
func (h *AppSettingsHandler) LogsGet(c *gin.Context) {
|
||||||
data, err := h.settings.Logs(c.Request.Context())
|
data, err := h.settings.Logs(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
|
|||||||
Resolution string `json:"resolution"`
|
Resolution string `json:"resolution"`
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
ReferenceImages []string `json:"reference_images"`
|
ReferenceImages []string `json:"reference_images"`
|
||||||
|
DeAI bool `json:"deai"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil {
|
if err := c.ShouldBindJSON(&body); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
c.JSON(http.StatusBadRequest, gin.H{"detail": "invalid request body"})
|
||||||
@@ -81,6 +82,7 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
|
|||||||
Resolution: body.Resolution,
|
Resolution: body.Resolution,
|
||||||
Duration: body.Duration,
|
Duration: body.Duration,
|
||||||
ReferenceImages: body.ReferenceImages,
|
ReferenceImages: body.ReferenceImages,
|
||||||
|
DeAI: body.DeAI,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch {
|
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("/video-presets", handlers.UserGen.VideoPresets)
|
||||||
publicAdmin.GET("/catalog", handlers.UserGen.Catalog)
|
publicAdmin.GET("/catalog", handlers.UserGen.Catalog)
|
||||||
publicAdmin.GET("/models", handlers.UserGen.Models)
|
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.
|
// 易支付 async notify — called server-to-server by the pay platform, no auth.
|
||||||
publicAdmin.GET("/pay/notify", handlers.Payment.Notify)
|
publicAdmin.GET("/pay/notify", handlers.Payment.Notify)
|
||||||
publicAdmin.POST("/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.PUT("/credits", handlers.AppSettings.CreditsPut)
|
||||||
settings.GET("/logs", handlers.AppSettings.LogsGet)
|
settings.GET("/logs", handlers.AppSettings.LogsGet)
|
||||||
settings.PUT("/logs", handlers.AppSettings.LogsPut)
|
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.GET("/media", handlers.AppSettings.MediaGet)
|
||||||
settings.PUT("/media", handlers.AppSettings.MediaPut)
|
settings.PUT("/media", handlers.AppSettings.MediaPut)
|
||||||
settings.GET("/announcement", handlers.Announcement.AdminGet)
|
settings.GET("/announcement", handlers.Announcement.AdminGet)
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ type CreditSettings struct {
|
|||||||
CDKRedeemEnabled bool `json:"cdk_redeem_enabled"`
|
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 {
|
type ProxySettings struct {
|
||||||
Proxy string `json:"proxy"`
|
Proxy string `json:"proxy"`
|
||||||
}
|
}
|
||||||
@@ -334,6 +342,44 @@ func (s *AppSettingsService) TestProxy(ctx context.Context, proxy string) (map[s
|
|||||||
}, nil
|
}, 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) {
|
func (s *AppSettingsService) Credits(ctx context.Context) (*CreditSettings, error) {
|
||||||
checkinEnabledRaw, err := s.settings.GetValue(ctx, "credits.checkin_enabled")
|
checkinEnabledRaw, err := s.settings.GetValue(ctx, "credits.checkin_enabled")
|
||||||
if err != nil {
|
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
|
Resolution string
|
||||||
Duration string
|
Duration string
|
||||||
ReferenceImages []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 pins an admin test to one specific provider account (账号生图测试).
|
||||||
AccountID string
|
AccountID string
|
||||||
}
|
}
|
||||||
@@ -75,6 +78,7 @@ func (s *UserGenerationService) Generate(ctx context.Context, user *model.User,
|
|||||||
AspectRatio: in.Ratio,
|
AspectRatio: in.Ratio,
|
||||||
Resolution: in.Resolution,
|
Resolution: in.Resolution,
|
||||||
ReferenceImages: in.ReferenceImages,
|
ReferenceImages: in.ReferenceImages,
|
||||||
|
DeAI: in.DeAI,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -199,6 +199,10 @@ type V1ImageRequest struct {
|
|||||||
Resolution string
|
Resolution string
|
||||||
N int
|
N int
|
||||||
ReferenceImages []string
|
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"),
|
// BaseURL is the scheme+host of the inbound request (e.g. "https://host"),
|
||||||
// used to build absolute, directly-downloadable output URLs. Empty falls
|
// used to build absolute, directly-downloadable output URLs. Empty falls
|
||||||
// back to a relative "/images/..." path.
|
// 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)
|
_ = s.events.UpdateStatus(ctx, eventID, "failed", "provider not implemented", 0)
|
||||||
return nil, fmt.Errorf("%w: %s", ErrProviderUnsupported, modelItem.Provider)
|
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 {
|
if !noStore {
|
||||||
// Upload to RustFS. On failure the generation fails and credits are
|
// Upload to RustFS. On failure the generation fails and credits are
|
||||||
// refunded — we never fall back to local disk.
|
// refunded — we never fall back to local disk.
|
||||||
@@ -1104,7 +1115,11 @@ func (s *V1Service) prepareImage(ctx context.Context, principal *APIPrincipal, i
|
|||||||
resolution = fb
|
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 {
|
if err != nil {
|
||||||
return nil, "", "", 0, err
|
return nil, "", "", 0, err
|
||||||
}
|
}
|
||||||
@@ -1172,14 +1187,14 @@ func (s *V1Service) prepareVideo(ctx context.Context, principal *APIPrincipal, i
|
|||||||
if resolution == "" {
|
if resolution == "" {
|
||||||
resolution = "720p"
|
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 {
|
if err != nil {
|
||||||
return nil, "", "", "", 0, err
|
return nil, "", "", "", 0, err
|
||||||
}
|
}
|
||||||
return modelItem, resolution, aspectRatio, duration, price, nil
|
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 即将被扣费的
|
// 代理用户走代理价(某档未设代理价则回退普通价)。principal.User 即将被扣费的
|
||||||
// 用户,无论画图台还是 key 调用都从这里取,所以一处即覆盖所有路径。
|
// 用户,无论画图台还是 key 调用都从这里取,所以一处即覆盖所有路径。
|
||||||
agent := principal != nil && principal.User != nil && principal.User.Role == "agent"
|
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 {
|
if !ok {
|
||||||
return 0, ErrUnsupportedParams
|
return 0, ErrUnsupportedParams
|
||||||
}
|
}
|
||||||
|
price += surcharge
|
||||||
if !charge || principal == nil || principal.User == nil {
|
if !charge || principal == nil || principal.User == nil {
|
||||||
return 0, 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
|
// 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
|
// order), or "" if none is priced. Used to rescue a request whose resolution
|
||||||
// the model doesn't support.
|
// 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 {
|
func firstPricedResolution(item *model.ModelConfig) string {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -120,6 +120,24 @@ const smtpBusy = ref(false); const smtpSaved = ref(false)
|
|||||||
const credits = reactive({ checkin_enabled: true, checkin_reward: 3, invite_enabled: true, invite_reward: 3, cdk_redeem_enabled: true })
|
const credits = reactive({ checkin_enabled: true, checkin_reward: 3, invite_enabled: true, invite_reward: 3, cdk_redeem_enabled: true })
|
||||||
const credBusy = ref(false); const credSaved = ref(false)
|
const credBusy = ref(false); const credSaved = ref(false)
|
||||||
|
|
||||||
|
// ---- deai (去AI特征 附加价格) ----
|
||||||
|
const deaiCfg = reactive({ price_1k: 1, price_2k: 2, price_4k: 3 })
|
||||||
|
const deaiBusy = ref(false); const deaiSaved = ref(false)
|
||||||
|
async function loadDeai() {
|
||||||
|
const r = await api('/settings/deai')
|
||||||
|
if (r.ok && r.data) Object.assign(deaiCfg, r.data)
|
||||||
|
}
|
||||||
|
async function saveDeai() {
|
||||||
|
deaiBusy.value = true; deaiSaved.value = false
|
||||||
|
const r = await api('/settings/deai', jsonBody('PUT', {
|
||||||
|
price_1k: Number(deaiCfg.price_1k) || 0,
|
||||||
|
price_2k: Number(deaiCfg.price_2k) || 0,
|
||||||
|
price_4k: Number(deaiCfg.price_4k) || 0,
|
||||||
|
}))
|
||||||
|
deaiBusy.value = false
|
||||||
|
if (r.ok) { deaiSaved.value = true; setTimeout(() => (deaiSaved.value = false), 2000) }
|
||||||
|
}
|
||||||
|
|
||||||
// ---- announcement (公告, markdown; re-pops for users who haven't seen edits) ----
|
// ---- announcement (公告, markdown; re-pops for users who haven't seen edits) ----
|
||||||
const ann = reactive({ content: '' })
|
const ann = reactive({ content: '' })
|
||||||
const annBusy = ref(false); const annSaved = ref(false)
|
const annBusy = ref(false); const annSaved = ref(false)
|
||||||
@@ -269,7 +287,7 @@ async function saveCredits() {
|
|||||||
if (r.ok) { credSaved.value = true; setTimeout(() => (credSaved.value = false), 2000) }
|
if (r.ok) { credSaved.value = true; setTimeout(() => (credSaved.value = false), 2000) }
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnouncement(); loadPay(); loadProxy(); loadLogs(); loadMedia() })
|
onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnouncement(); loadPay(); loadProxy(); loadLogs(); loadMedia(); loadDeai() })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -462,6 +480,30 @@ onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnounce
|
|||||||
<div class="mt-4"><button @click="saveCredits" :disabled="credBusy" class="btn-primary">{{ credBusy ? '保存中…' : '保存设置' }}</button></div>
|
<div class="mt-4"><button @click="saveCredits" :disabled="credBusy" class="btn-primary">{{ credBusy ? '保存中…' : '保存设置' }}</button></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- deai (去AI特征) -->
|
||||||
|
<div class="card p-5">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-sm font-semibold">去AI特征</h2>
|
||||||
|
<span v-if="deaiSaved" class="text-xs text-emerald-300">已保存 ✓</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-400 mb-4">画图台开启「去AI特征」后,按画质档位在模型价格之上额外扣除的积分。仅对图片生成生效。</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="row">
|
||||||
|
<span><span class="lbl">1K 附加价格</span><span class="hint">默认 1 积分。</span></span>
|
||||||
|
<input type="number" min="0" v-model.number="deaiCfg.price_1k" class="num" />
|
||||||
|
</label>
|
||||||
|
<label class="row">
|
||||||
|
<span><span class="lbl">2K 附加价格</span><span class="hint">默认 2 积分。</span></span>
|
||||||
|
<input type="number" min="0" v-model.number="deaiCfg.price_2k" class="num" />
|
||||||
|
</label>
|
||||||
|
<label class="row">
|
||||||
|
<span><span class="lbl">4K 附加价格</span><span class="hint">默认 3 积分。</span></span>
|
||||||
|
<input type="number" min="0" v-model.number="deaiCfg.price_4k" class="num" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4"><button @click="saveDeai" :disabled="deaiBusy" class="btn-primary">{{ deaiBusy ? '保存中…' : '保存设置' }}</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- announcement (公告) -->
|
<!-- announcement (公告) -->
|
||||||
<div class="card p-5">
|
<div class="card p-5">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const prompt = ref(draft.prompt || '')
|
|||||||
const ratio = ref(draft.ratio || '')
|
const ratio = ref(draft.ratio || '')
|
||||||
const resolution = ref(draft.resolution || '')
|
const resolution = ref(draft.resolution || '')
|
||||||
const duration = ref(draft.duration || '')
|
const duration = ref(draft.duration || '')
|
||||||
|
const deai = ref(draft.deai || false)
|
||||||
|
|
||||||
watch(mode, (v) => { draft.mode = v })
|
watch(mode, (v) => { draft.mode = v })
|
||||||
watch(modelId, (v) => { draft.modelId = v })
|
watch(modelId, (v) => { draft.modelId = v })
|
||||||
@@ -35,6 +36,7 @@ watch(prompt, (v) => { draft.prompt = v })
|
|||||||
watch(ratio, (v) => { draft.ratio = v })
|
watch(ratio, (v) => { draft.ratio = v })
|
||||||
watch(resolution, (v) => { draft.resolution = v })
|
watch(resolution, (v) => { draft.resolution = v })
|
||||||
watch(duration, (v) => { draft.duration = v })
|
watch(duration, (v) => { draft.duration = v })
|
||||||
|
watch(deai, (v) => { draft.deai = v })
|
||||||
|
|
||||||
const refImages = ref([]) // [{ name, dataUrl }]
|
const refImages = ref([]) // [{ name, dataUrl }]
|
||||||
const fileInput = ref(null)
|
const fileInput = ref(null)
|
||||||
@@ -136,6 +138,12 @@ function tierPrice(normalMap, agentMap, key) {
|
|||||||
}
|
}
|
||||||
return Number(n)
|
return Number(n)
|
||||||
}
|
}
|
||||||
|
// 去AI特征 per-tier surcharge (loaded from /deai-pricing; defaults 1/2/3 分).
|
||||||
|
const deaiPricing = ref({ price_1k: 1, price_2k: 2, price_4k: 3 })
|
||||||
|
const deaiSurcharge = computed(() => {
|
||||||
|
const key = { '1K': 'price_1k', '2K': 'price_2k', '4K': 'price_4k' }[resolution.value] || 'price_1k'
|
||||||
|
return Number(deaiPricing.value[key] ?? 0)
|
||||||
|
})
|
||||||
const price = computed(() => {
|
const price = computed(() => {
|
||||||
if (!model.value) return null
|
if (!model.value) return null
|
||||||
const m = model.value
|
const m = model.value
|
||||||
@@ -145,7 +153,9 @@ const price = computed(() => {
|
|||||||
if (rp == null || dp == null) return null
|
if (rp == null || dp == null) return null
|
||||||
return rp + dp
|
return rp + dp
|
||||||
}
|
}
|
||||||
return tierPrice(m.prices, m.prices_agent, resolution.value)
|
const base = tierPrice(m.prices, m.prices_agent, resolution.value)
|
||||||
|
if (base == null) return null
|
||||||
|
return base + (deai.value ? deaiSurcharge.value : 0)
|
||||||
})
|
})
|
||||||
const priceLabel = computed(() => price.value == null ? '—' : pointsLabel(price.value))
|
const priceLabel = computed(() => price.value == null ? '—' : pointsLabel(price.value))
|
||||||
const canAfford = computed(() => price.value == null || credits.value >= price.value)
|
const canAfford = computed(() => price.value == null || credits.value >= price.value)
|
||||||
@@ -392,6 +402,7 @@ async function fireOne() {
|
|||||||
ratio: ratio.value,
|
ratio: ratio.value,
|
||||||
resolution: resolution.value,
|
resolution: resolution.value,
|
||||||
duration: mode.value === 'video' ? duration.value : '',
|
duration: mode.value === 'video' ? duration.value : '',
|
||||||
|
deai: mode.value === 'image' ? deai.value : false,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
url: '',
|
url: '',
|
||||||
error: '',
|
error: '',
|
||||||
@@ -414,6 +425,7 @@ async function fireOne() {
|
|||||||
model: task.model, prompt: task.prompt, ratio: task.ratio, resolution: task.resolution,
|
model: task.model, prompt: task.prompt, ratio: task.ratio, resolution: task.resolution,
|
||||||
}
|
}
|
||||||
if (task.kind === 'video') payload.duration = task.duration
|
if (task.kind === 'video') payload.duration = task.duration
|
||||||
|
if (task.kind === 'image' && task.deai) payload.deai = true
|
||||||
if (refsSnapshot.length) {
|
if (refsSnapshot.length) {
|
||||||
const refs = await Promise.all(refsSnapshot.map(refToBase64))
|
const refs = await Promise.all(refsSnapshot.map(refToBase64))
|
||||||
payload.reference_images = refs.filter(Boolean)
|
payload.reference_images = refs.filter(Boolean)
|
||||||
@@ -574,9 +586,10 @@ function onKey(e) { if (e.key === 'Escape') lightbox.value = null }
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
refreshMe() // pull the latest real balance
|
refreshMe() // pull the latest real balance
|
||||||
const [mm, pp] = await Promise.all([api('/managed-models'), api('/video-presets')])
|
const [mm, pp, dp] = await Promise.all([api('/managed-models'), api('/video-presets'), api('/deai-pricing')])
|
||||||
allModels.value = mm.data?.data || []
|
allModels.value = mm.data?.data || []
|
||||||
presets.value = pp.data?.data || []
|
presets.value = pp.data?.data || []
|
||||||
|
if (dp.ok && dp.data) deaiPricing.value = dp.data
|
||||||
// Pre-fill from query string (?prompt=...&model=...) — used by the home
|
// Pre-fill from query string (?prompt=...&model=...) — used by the home
|
||||||
// page's example cards to seed the form in one click.
|
// page's example cards to seed the form in one click.
|
||||||
const qPrompt = String(route.query.prompt || '')
|
const qPrompt = String(route.query.prompt || '')
|
||||||
@@ -683,6 +696,20 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 去AI特征 (image only): opt-in post-processing with a per-tier surcharge -->
|
||||||
|
<div v-if="mode === 'image'" class="flex items-center justify-between">
|
||||||
|
<label class="text-xs font-medium text-slate-500">
|
||||||
|
去AI特征
|
||||||
|
<span class="text-slate-400 font-normal">(+{{ deaiSurcharge }} 积分)</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" role="switch" :aria-checked="deai" @click="deai = !deai"
|
||||||
|
class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors"
|
||||||
|
:class="deai ? 'bg-slate-900' : 'bg-slate-200'">
|
||||||
|
<span class="inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform"
|
||||||
|
:class="deai ? 'translate-x-[18px]' : 'translate-x-0.5'"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="mode === 'video' && durations.length > 0">
|
<div v-if="mode === 'video' && durations.length > 0">
|
||||||
<label class="block text-xs font-medium text-slate-500 mb-1.5">时长</label>
|
<label class="block text-xs font-medium text-slate-500 mb-1.5">时长</label>
|
||||||
<div class="flex flex-wrap gap-1.5">
|
<div class="flex flex-wrap gap-1.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user