增加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
@@ -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 {
+3
View File
@@ -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)
+46
View File
@@ -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 {
+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 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
+43 -3
View File
@@ -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 ""
+43 -1
View File
@@ -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">
+29 -2
View File
@@ -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">