feat(grok): 视频改走 Grok Console 并新增图片模型
- grok 视频/图片统一走 console.x.ai:sso 换 DPoP 短期 token,绕开 grok.com 的 statsig 反爬 - 新增 grok-image:不带参考图走 /v1/images/generations,带图自动走 quality 上游 + /v1/images/edits - grok 额度改本地写死 图5/视频2,成功各扣一次,两份归零直接判死,无恢复时间 - 视频渲染预算统一 20 分钟(adobe/runway/grok/custom) - 删除参考图人脸打码(facemask + onnxruntime 依赖 + 前台开关)
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"backend/internal/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -12,12 +14,63 @@ import (
|
||||
type UserGenerationHandler struct {
|
||||
userGen *service.UserGenerationService
|
||||
admin *service.AdminReadService
|
||||
idem *idemStore
|
||||
}
|
||||
|
||||
func NewUserGenerationHandler(userGen *service.UserGenerationService, admin *service.AdminReadService) *UserGenerationHandler {
|
||||
return &UserGenerationHandler{
|
||||
userGen: userGen,
|
||||
admin: admin,
|
||||
idem: &idemStore{m: map[string]*idemEntry{}},
|
||||
}
|
||||
}
|
||||
|
||||
// /generate 是同步长请求(视频要跑好几分钟)。等待期间连接一旦被重置(CDN 回源
|
||||
// 超时 / HTTP2 GOAWAY),浏览器会把还没拿到响应的 POST 透明重发,后端就会再生成
|
||||
// 一次、再扣一次积分。前端为每个任务带一个 Idempotency-Key,同一个 key 只真正
|
||||
// 执行一次:原任务还在跑就直接拒绝,已经跑完就把原结果返回。
|
||||
const idemTTL = 10 * time.Minute
|
||||
|
||||
type idemEntry struct {
|
||||
done bool
|
||||
resp map[string]any
|
||||
at time.Time
|
||||
}
|
||||
|
||||
type idemStore struct {
|
||||
mu sync.Mutex
|
||||
m map[string]*idemEntry
|
||||
}
|
||||
|
||||
// begin 登记一个 key。第二个返回值为 true 表示这个 key 已经在处理或刚处理完,
|
||||
// 返回的 entry 是原来那次的状态。
|
||||
func (s *idemStore) begin(key string) (*idemEntry, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
now := time.Now()
|
||||
for k, e := range s.m {
|
||||
if e.done && now.Sub(e.at) > idemTTL {
|
||||
delete(s.m, k)
|
||||
}
|
||||
}
|
||||
if e, ok := s.m[key]; ok {
|
||||
return e, true
|
||||
}
|
||||
e := &idemEntry{at: now}
|
||||
s.m[key] = e
|
||||
return e, false
|
||||
}
|
||||
|
||||
// finish 记下结果供重发命中;resp 为 nil(本次失败)时直接释放 key,允许用户重试。
|
||||
func (s *idemStore) finish(key string, resp map[string]any) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if resp == nil {
|
||||
delete(s.m, key)
|
||||
return
|
||||
}
|
||||
if e, ok := s.m[key]; ok {
|
||||
e.done, e.resp, e.at = true, resp, time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +129,20 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var generated map[string]any // 成功时的响应,供同 key 的重发直接命中
|
||||
if key := strings.TrimSpace(c.GetHeader("Idempotency-Key")); key != "" {
|
||||
key = user.ID + "|" + key
|
||||
if e, dup := h.idem.begin(key); dup {
|
||||
if e.done {
|
||||
c.JSON(http.StatusOK, e.resp)
|
||||
} else {
|
||||
c.JSON(http.StatusConflict, gin.H{"detail": "该任务已在生成中,已忽略重复提交"})
|
||||
}
|
||||
return
|
||||
}
|
||||
defer func() { h.idem.finish(key, generated) }()
|
||||
}
|
||||
|
||||
resp, err := h.userGen.Generate(c.Request.Context(), user, service.UserGenerateRequest{
|
||||
Model: body.Model,
|
||||
Prompt: body.Prompt,
|
||||
@@ -113,6 +180,7 @@ func (h *UserGenerationHandler) Generate(c *gin.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
generated = resp
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -602,11 +670,22 @@ func (h *UserGenerationHandler) catalogEntries(c *gin.Context) ([]gin.H, error)
|
||||
"type": "video",
|
||||
"ratios": []string{"2:3", "3:2", "1:1", "9:16", "16:9"},
|
||||
"resolutions": []string{"720p"},
|
||||
"durations": []string{"6s", "10s"},
|
||||
"max_reference_images": 6,
|
||||
"reference_mode": "asset",
|
||||
"durations": []string{"6s", "10s", "15s"},
|
||||
"max_reference_images": 1,
|
||||
"reference_mode": "frame",
|
||||
"description": "Grok Imagine video (文/图生视频)",
|
||||
},
|
||||
{
|
||||
"id": "grok-image",
|
||||
"provider": "grok",
|
||||
"type": "image",
|
||||
"ratios": []string{"1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9"},
|
||||
"resolutions": []string{"1K", "2K"},
|
||||
"image_to_image": true,
|
||||
"max_reference_images": 3,
|
||||
"reference_mode": "asset",
|
||||
"description": "Grok Imagine image (文生图 / 图生图)",
|
||||
},
|
||||
{
|
||||
"id": "seedream-4.5",
|
||||
"provider": "leonardo",
|
||||
@@ -801,6 +880,15 @@ func (h *UserGenerationHandler) publicModels() ([]gin.H, error) {
|
||||
"description": "Grok Imagine video",
|
||||
"stub": false,
|
||||
},
|
||||
{
|
||||
"id": "grok-image",
|
||||
"provider": "grok",
|
||||
"kind": "image",
|
||||
"ratios": []string{"1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9"},
|
||||
"resolutions": []string{"1K", "2K"},
|
||||
"description": "Grok Imagine image",
|
||||
"stub": false,
|
||||
},
|
||||
{
|
||||
"id": "seedream-4.5",
|
||||
"provider": "leonardo",
|
||||
|
||||
Reference in New Issue
Block a user