修复grok视频参考图
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
package grok
|
package grok
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
|
|
||||||
http "github.com/bogdanfinn/fhttp"
|
http "github.com/bogdanfinn/fhttp"
|
||||||
tlsclient "github.com/bogdanfinn/tls-client"
|
tlsclient "github.com/bogdanfinn/tls-client"
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
// assetBase is where generated media artifacts live (the stream returns a
|
// assetBase is where generated media artifacts live (the stream returns a
|
||||||
@@ -166,17 +169,11 @@ func (c *Client) GenerateVideo(ctx context.Context, token, prompt, aspectRatio,
|
|||||||
return data, meta, nil
|
return data, meta, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// uploadImage uploads one reference frame via /rest/app-chat/upload-file (JSON
|
// uploadImage uploads one reference frame via /http/upload-file-v2/direct and
|
||||||
// with base64 content) and returns its asset content URL for imageReferences.
|
// returns its asset content URL for imageReferences. Cloudflare's bot score is
|
||||||
// Cloudflare's bot score is per-request, so a big base64 upload can hit a
|
// per-request, so retry transient failures with backoff instead of failing the
|
||||||
// "Just a moment…" 403 intermittently while identical requests pass — retry
|
// whole task.
|
||||||
// transient failures with backoff instead of failing the whole task.
|
|
||||||
func (c *Client) uploadImage(ctx context.Context, client tlsclient.HttpClient, token string, img []byte) (string, error) {
|
func (c *Client) uploadImage(ctx context.Context, client tlsclient.HttpClient, token string, img []byte) (string, error) {
|
||||||
body := map[string]any{
|
|
||||||
"fileName": "ref.png",
|
|
||||||
"fileMimeType": "image/png",
|
|
||||||
"content": base64.StdEncoding.EncodeToString(img),
|
|
||||||
}
|
|
||||||
var res map[string]any
|
var res map[string]any
|
||||||
var err error
|
var err error
|
||||||
backoffs := []time.Duration{0, 2 * time.Second, 5 * time.Second, 10 * time.Second}
|
backoffs := []time.Duration{0, 2 * time.Second, 5 * time.Second, 10 * time.Second}
|
||||||
@@ -188,7 +185,7 @@ func (c *Client) uploadImage(ctx context.Context, client tlsclient.HttpClient, t
|
|||||||
case <-time.After(wait):
|
case <-time.After(wait):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res, err = c.postJSON(ctx, client, token, "/rest/app-chat/upload-file", body)
|
res, err = c.uploadFileV2(ctx, client, token, img)
|
||||||
if err == nil || !errors.Is(err, ErrTemporaryUpstream) {
|
if err == nil || !errors.Is(err, ErrTemporaryUpstream) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -196,7 +193,11 @@ func (c *Client) uploadImage(ctx context.Context, client tlsclient.HttpClient, t
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
fileURI := strings.TrimSpace(stringValue(res["fileUri"]))
|
meta, _ := res["fileMetadata"].(map[string]any)
|
||||||
|
fileURI := ""
|
||||||
|
if meta != nil {
|
||||||
|
fileURI = strings.TrimSpace(stringValue(meta["fileUri"]))
|
||||||
|
}
|
||||||
if fileURI == "" {
|
if fileURI == "" {
|
||||||
return "", fmt.Errorf("%w: upload missing fileUri", ErrTemporaryUpstream)
|
return "", fmt.Errorf("%w: upload missing fileUri", ErrTemporaryUpstream)
|
||||||
}
|
}
|
||||||
@@ -206,6 +207,52 @@ func (c *Client) uploadImage(ctx context.Context, client tlsclient.HttpClient, t
|
|||||||
return assetBase + strings.TrimPrefix(fileURI, "/"), nil
|
return assetBase + strings.TrimPrefix(fileURI, "/"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) uploadFileV2(ctx context.Context, client tlsclient.HttpClient, token string, img []byte) (map[string]any, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
mw := multipart.NewWriter(&buf)
|
||||||
|
partHeader := textproto.MIMEHeader{}
|
||||||
|
partHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file"; filename="%s"`, uuid.NewString()+".png"))
|
||||||
|
partHeader.Set("Content-Type", "image/png")
|
||||||
|
part, err := mw.CreatePart(partHeader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := part.Write(img); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := mw.WriteField("file_source", "IMAGINE_SELF_UPLOAD_FILE_SOURCE"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := mw.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, apiBase+"/http/upload-file-v2/direct", bytes.NewReader(buf.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
c.applyHeaders(req, token, map[string]string{"content-type": mw.FormDataContentType()})
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
raw, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrTemporaryUpstream, err)
|
||||||
|
}
|
||||||
|
if e := mapStatus("/http/upload-file-v2/direct", resp.StatusCode, raw); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
var out map[string]any
|
||||||
|
if err := json.Unmarshal(raw, &out); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: upload non-json: %s", ErrTemporaryUpstream, clip(raw, 120))
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// createPost registers a video media post and returns its id (parentPostId).
|
// createPost registers a video media post and returns its id (parentPostId).
|
||||||
func (c *Client) createPost(ctx context.Context, client tlsclient.HttpClient, token, prompt string) (string, error) {
|
func (c *Client) createPost(ctx context.Context, client tlsclient.HttpClient, token, prompt string) (string, error) {
|
||||||
res, err := c.postJSON(ctx, client, token, "/rest/media/post/create", map[string]any{
|
res, err := c.postJSON(ctx, client, token, "/rest/media/post/create", map[string]any{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SelectMenu from '../components/SelectMenu.vue'
|
|||||||
import MediaLightbox from '../components/MediaLightbox.vue'
|
import MediaLightbox from '../components/MediaLightbox.vue'
|
||||||
import { pointsLabel } from '../credits'
|
import { pointsLabel } from '../credits'
|
||||||
import { sortResolutions } from '../utils/format'
|
import { sortResolutions } from '../utils/format'
|
||||||
|
import { copyText } from '../utils/clipboard'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -265,6 +266,12 @@ function flash(msg) {
|
|||||||
toastTimer = setTimeout(() => (toast.value = ''), 1800)
|
toastTimer = setTimeout(() => (toast.value = ''), 1800)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyPrompt(item) {
|
||||||
|
const text = (item && item.prompt) || ''
|
||||||
|
if (!text.trim()) return
|
||||||
|
flash(await copyText(text) ? '指令已复制' : '复制失败')
|
||||||
|
}
|
||||||
|
|
||||||
// ---- generate (concurrent — no lock) ----
|
// ---- generate (concurrent — no lock) ----
|
||||||
// 生图 can request 1–4 images at once: each is an independent task/charge.
|
// 生图 can request 1–4 images at once: each is an independent task/charge.
|
||||||
const count = ref(1)
|
const count = ref(1)
|
||||||
@@ -691,7 +698,9 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-x-0 bottom-0 p-2.5 pointer-events-none">
|
<div class="absolute inset-x-0 bottom-0 p-2.5 pointer-events-none">
|
||||||
<div class="pg-cap text-[11px] leading-tight font-medium line-clamp-2" :title="item.prompt">{{ item.prompt }}</div>
|
<div class="pg-cap text-[11px] leading-tight font-medium line-clamp-2 transition-colors"
|
||||||
|
:class="item.prompt ? 'pointer-events-auto cursor-pointer' : ''"
|
||||||
|
:title="item.prompt ? '点击复制提示词' : ''" @click.stop="copyPrompt(item)">{{ item.prompt }}</div>
|
||||||
<div class="pg-cap-sub text-[9px] mt-0.5 font-mono truncate">{{ item.model }}<span v-if="item.elapsed_ms"> · {{ (item.elapsed_ms / 1000).toFixed(1) }}s</span></div>
|
<div class="pg-cap-sub text-[9px] mt-0.5 font-mono truncate">{{ item.model }}<span v-if="item.elapsed_ms"> · {{ (item.elapsed_ms / 1000).toFixed(1) }}s</span></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -744,5 +753,6 @@ onUnmounted(() => {
|
|||||||
The global `.theme-text` remap would otherwise darken them (it turns
|
The global `.theme-text` remap would otherwise darken them (it turns
|
||||||
over-image whites dark for the marketing pages), making them unreadable here. */
|
over-image whites dark for the marketing pages), making them unreadable here. */
|
||||||
.pg-cap { color: #fff !important; }
|
.pg-cap { color: #fff !important; }
|
||||||
|
.pg-cap.cursor-pointer:hover { color: rgb(255 255 255 / 0.75) !important; }
|
||||||
.pg-cap-sub { color: rgb(255 255 255 / 0.62) !important; }
|
.pg-cap-sub { color: rgb(255 255 255 / 0.62) !important; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user