增加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 ""
|
||||
|
||||
@@ -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 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) ----
|
||||
const ann = reactive({ content: '' })
|
||||
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) }
|
||||
}
|
||||
|
||||
onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnouncement(); loadPay(); loadProxy(); loadLogs(); loadMedia() })
|
||||
onMounted(() => { loadSite(); loadReg(); loadSmtp(); loadCredits(); loadAnnouncement(); loadPay(); loadProxy(); loadLogs(); loadMedia(); loadDeai() })
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 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 (公告) -->
|
||||
<div class="card p-5">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
|
||||
@@ -28,6 +28,7 @@ const prompt = ref(draft.prompt || '')
|
||||
const ratio = ref(draft.ratio || '')
|
||||
const resolution = ref(draft.resolution || '')
|
||||
const duration = ref(draft.duration || '')
|
||||
const deai = ref(draft.deai || false)
|
||||
|
||||
watch(mode, (v) => { draft.mode = v })
|
||||
watch(modelId, (v) => { draft.modelId = v })
|
||||
@@ -35,6 +36,7 @@ watch(prompt, (v) => { draft.prompt = v })
|
||||
watch(ratio, (v) => { draft.ratio = v })
|
||||
watch(resolution, (v) => { draft.resolution = v })
|
||||
watch(duration, (v) => { draft.duration = v })
|
||||
watch(deai, (v) => { draft.deai = v })
|
||||
|
||||
const refImages = ref([]) // [{ name, dataUrl }]
|
||||
const fileInput = ref(null)
|
||||
@@ -136,6 +138,12 @@ function tierPrice(normalMap, agentMap, key) {
|
||||
}
|
||||
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(() => {
|
||||
if (!model.value) return null
|
||||
const m = model.value
|
||||
@@ -145,7 +153,9 @@ const price = computed(() => {
|
||||
if (rp == null || dp == null) return null
|
||||
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 canAfford = computed(() => price.value == null || credits.value >= price.value)
|
||||
@@ -392,6 +402,7 @@ async function fireOne() {
|
||||
ratio: ratio.value,
|
||||
resolution: resolution.value,
|
||||
duration: mode.value === 'video' ? duration.value : '',
|
||||
deai: mode.value === 'image' ? deai.value : false,
|
||||
status: 'pending',
|
||||
url: '',
|
||||
error: '',
|
||||
@@ -414,6 +425,7 @@ async function fireOne() {
|
||||
model: task.model, prompt: task.prompt, ratio: task.ratio, resolution: task.resolution,
|
||||
}
|
||||
if (task.kind === 'video') payload.duration = task.duration
|
||||
if (task.kind === 'image' && task.deai) payload.deai = true
|
||||
if (refsSnapshot.length) {
|
||||
const refs = await Promise.all(refsSnapshot.map(refToBase64))
|
||||
payload.reference_images = refs.filter(Boolean)
|
||||
@@ -574,9 +586,10 @@ function onKey(e) { if (e.key === 'Escape') lightbox.value = null }
|
||||
|
||||
onMounted(async () => {
|
||||
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 || []
|
||||
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
|
||||
// page's example cards to seed the form in one click.
|
||||
const qPrompt = String(route.query.prompt || '')
|
||||
@@ -683,6 +696,20 @@ onUnmounted(() => {
|
||||
</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">
|
||||
<label class="block text-xs font-medium text-slate-500 mb-1.5">时长</label>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user