Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _public/static/admin/js/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const LOCALE_MAP = {

"video": {
"label": "视频配置",
"enable_public_asset": { title: "公开资产链接", desc: "是否开启生成结束后创建 Public 资产。" },
"concurrent": { title: "并发上限", desc: "Reverse 接口并发上限。" },
"timeout": { title: "请求超时", desc: "Reverse 接口超时时间(秒)。" },
"stream_timeout": { title: "流空闲超时", desc: "流式空闲超时时间(秒)。" },
Expand Down
1 change: 1 addition & 0 deletions _public/static/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
"stream_timeout": {"title": "Stream Idle Timeout", "desc": "Streaming idle timeout (seconds)."}
},
"video": {
"enable_public_asset": {"title": "Public Asset Link", "desc": "Whether to create a public asset link after video generation finishes."},
"concurrent": {"title": "Concurrency Limit", "desc": "Reverse API concurrency limit."},
"timeout": {"title": "Request Timeout", "desc": "Reverse API timeout (seconds)."},
"stream_timeout": {"title": "Stream Idle Timeout", "desc": "Streaming idle timeout (seconds)."},
Expand Down
1 change: 1 addition & 0 deletions _public/static/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
"stream_timeout": {"title": "流空闲超时", "desc": "流式空闲超时时间(秒)。"}
},
"video": {
"enable_public_asset": {"title": "公开资产链接", "desc": "是否开启生成结束后创建 Public 资产。"},
"concurrent": {"title": "并发上限", "desc": "Reverse 接口并发上限。"},
"timeout": {"title": "请求超时", "desc": "Reverse 接口超时时间(秒)。"},
"stream_timeout": {"title": "流空闲超时", "desc": "流式空闲超时时间(秒)。"},
Expand Down
50 changes: 50 additions & 0 deletions app/services/grok/services/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from app.services.grok.utils.stream import wrap_stream_with_usage
from app.services.reverse.app_chat import AppChatReverse
from app.services.reverse.media_post import MediaPostReverse
from app.services.reverse.media_post_link import MediaPostLinkReverse
from app.services.reverse.utils.session import ResettableSession
from app.services.reverse.video_upscale import VideoUpscaleReverse
from app.services.token import EffortType, get_token_manager
Expand Down Expand Up @@ -88,6 +89,37 @@ def _extract_video_id(video_url: str) -> str:
return ""


def _public_asset_enabled() -> bool:
return bool(get_config("video.enable_public_asset", False))


async def _create_public_video_link(token: str, video_url: str) -> str:
if not video_url or not _public_asset_enabled():
return video_url

video_id = _extract_video_id(video_url)
if not video_id:
logger.warning("Video public link skipped: unable to extract video id")
return video_url

try:
async with _new_session() as session:
response = await MediaPostLinkReverse.request(session, token, video_id)
payload = response.json() if response is not None else {}
share_link = _pick_str(payload.get("shareLink")) if isinstance(payload, dict) else ""
if share_link:
if share_link.endswith(".mp4"):
logger.info(f"Video public link created: {share_link}")
return share_link
public_url = f"https://imagine-public.x.ai/imagine-public/share-videos/{video_id}.mp4?cache=1"
logger.info(f"Video public link created: {public_url}")
return public_url
except Exception as e:
logger.warning(f"Video public link failed: {e}")

return video_url


def _build_mode_flag(preset: str) -> str:
mode_map = {
"fun": "--mode=extremely-crazy",
Expand Down Expand Up @@ -950,6 +982,11 @@ async def _stream_chain() -> AsyncGenerator[str, None]:
if not upscaled:
logger.warning("Video upscale failed, fallback to 480p result")

if _public_asset_enabled():
for chunk in writer.emit_note("正在生成可公开访问链接\n"):
yield chunk
final_video_url = await _create_public_video_link(token, final_video_url)

dl_service = DownloadService()
try:
rendered = await dl_service.render_video(
Expand Down Expand Up @@ -1026,6 +1063,9 @@ async def _collect_chain() -> Dict[str, Any]:
if not upscaled:
logger.warning("Video upscale failed, fallback to 480p result")

if _public_asset_enabled():
final_video_url = await _create_public_video_link(token, final_video_url)

dl_service = DownloadService()
try:
content = await dl_service.render_video(
Expand Down Expand Up @@ -1092,6 +1132,7 @@ def __init__(
self.token = token
self.show_think = bool(show_think)
self.upscale_on_finish = bool(upscale_on_finish)
self.enable_public_asset = _public_asset_enabled()
self.round_index = max(1, int(round_index or 1))
self.round_total = max(self.round_index, int(round_total or self.round_index))

Expand Down Expand Up @@ -1157,6 +1198,11 @@ async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, N
if not upscaled:
logger.warning("Video upscale failed, fallback to 480p result")

if self.enable_public_asset:
for chunk in self.writer.emit_note("正在生成可公开访问链接\n"):
yield chunk
final_video_url = await _create_public_video_link(self.token, final_video_url)

rendered = await self._get_dl().render_video(
final_video_url,
self.token,
Expand Down Expand Up @@ -1187,6 +1233,7 @@ def __init__(
self.model = model
self.token = token
self.upscale_on_finish = bool(upscale_on_finish)
self.enable_public_asset = _public_asset_enabled()
self.round_index = max(1, int(round_index or 1))
self.round_total = max(self.round_index, int(round_total or self.round_index))
self._dl_service: Optional[DownloadService] = None
Expand Down Expand Up @@ -1222,6 +1269,9 @@ async def process(self, response: AsyncIterable[bytes]) -> Dict[str, Any]:
if not upscaled:
logger.warning("Video upscale failed, fallback to 480p result")

if self.enable_public_asset:
final_video_url = await _create_public_video_link(self.token, final_video_url)

content = await self._get_dl().render_video(
final_video_url,
self.token,
Expand Down
6 changes: 5 additions & 1 deletion app/services/grok/utils/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async def resolve_url(
) -> str:
asset_url = path_or_url
path = path_or_url
parsed = None
if path_or_url.startswith("http"):
parsed = urlparse(path_or_url)
path = parsed.path or ""
Expand All @@ -68,6 +69,8 @@ async def resolve_url(

app_url = get_config("app.app_url")
if app_url:
if parsed and parsed.netloc and parsed.netloc != "assets.grok.com":
return asset_url
await self.download_file(asset_url, token, media_type)
return f"{app_url.rstrip('/')}/v1/files/{media_type}{path}"
return asset_url
Expand Down Expand Up @@ -192,7 +195,8 @@ async def download_file(self, file_path: str, token: str, media_type: str = "ima
async with _get_download_semaphore():
file_path = self._normalize_path(file_path)
cache_dir = self.image_dir if media_type == "image" else self.video_dir
filename = file_path.lstrip("/").replace("/", "-")
cache_key = urlparse(file_path).path or file_path
filename = cache_key.lstrip("/").replace("/", "-")
cache_path = cache_dir / filename

lock_name = (
Expand Down
26 changes: 19 additions & 7 deletions app/services/reverse/assets_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import urllib.parse
from typing import Any
from pathlib import Path
from urllib.parse import urlparse
from curl_cffi.requests import AsyncSession

from app.core.logger import logger
Expand Down Expand Up @@ -42,10 +43,21 @@ async def request(session: AsyncSession, token: str, file_path: str) -> Any:
Any: The response from the request.
"""
try:
# Normalize path
if not file_path.startswith("/"):
file_path = f"/{file_path}"
url = f"{DOWNLOAD_API}{file_path}"
parsed = urlparse(file_path)
origin = "https://assets.grok.com"
referer = "https://grok.com/"
if parsed.scheme and parsed.netloc:
url = file_path
request_path = parsed.path or "/"
if parsed.query:
request_path = f"{request_path}?{parsed.query}"
origin = f"{parsed.scheme}://{parsed.netloc}"
referer = f"{origin}/"
else:
if not file_path.startswith("/"):
file_path = f"/{file_path}"
request_path = file_path
url = f"{DOWNLOAD_API}{file_path}"

# Get proxies
base_proxy = get_config("proxy.base_proxy_url") or ""
Expand All @@ -58,14 +70,14 @@ async def request(session: AsyncSession, token: str, file_path: str) -> Any:
proxies = None

# Guess content type by extension for Accept/Sec-Fetch-Dest
content_type = _CONTENT_TYPES.get(Path(urllib.parse.urlparse(file_path).path).suffix.lower())
content_type = _CONTENT_TYPES.get(Path(urllib.parse.urlparse(request_path).path).suffix.lower())

# Build headers
headers = build_headers(
cookie_token=token,
content_type=content_type,
origin="https://assets.grok.com",
referer="https://grok.com/",
origin=origin,
referer=referer,
)
## Align with browser download navigation headers
headers["Cache-Control"] = "no-cache"
Expand Down
107 changes: 107 additions & 0 deletions app/services/reverse/media_post_link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""
Reverse interface: media post create link.
"""

import orjson
from typing import Any
from curl_cffi.requests import AsyncSession

from app.core.logger import logger
from app.core.config import get_config
from app.core.exceptions import UpstreamException
from app.services.token.service import TokenService
from app.services.reverse.utils.headers import build_headers
from app.services.reverse.utils.retry import retry_on_status

MEDIA_POST_LINK_API = "https://grok.com/rest/media/post/create-link"


class MediaPostLinkReverse:
"""/rest/media/post/create-link reverse interface."""

@staticmethod
async def request(
session: AsyncSession,
token: str,
post_id: str,
) -> Any:
try:
# Get proxies
base_proxy = get_config("proxy.base_proxy_url")
proxies = {"http": base_proxy, "https": base_proxy} if base_proxy else None

# Build headers
headers = build_headers(
cookie_token=token,
content_type="application/json",
origin="https://grok.com",
referer="https://grok.com",
)

# Build payload
payload = {
"postId": post_id,
"source": "post-page",
"platform": "web"
}

# Curl Config
timeout = get_config("video.timeout")
browser = get_config("proxy.browser")

async def _do_request():
response = await session.post(
MEDIA_POST_LINK_API,
headers=headers,
data=orjson.dumps(payload),
timeout=timeout,
proxies=proxies,
impersonate=browser,
)

if response.status_code != 200:
content = ""
try:
content = await response.text()
except Exception:
pass
logger.error(
f"MediaPostLinkReverse: Media post create link failed, {response.status_code}",
extra={"error_type": "UpstreamException"},
)
raise UpstreamException(
message=f"MediaPostLinkReverse: Media post create link failed, {response.status_code}",
details={"status": response.status_code, "body": content},
)

return response

return await retry_on_status(_do_request)

except Exception as e:
# Handle upstream exception
if isinstance(e, UpstreamException):
status = None
if e.details and "status" in e.details:
status = e.details["status"]
else:
status = getattr(e, "status_code", None)
if status == 401:
try:
await TokenService.record_fail(token, status, "media_post_link_auth_failed")
except Exception:
pass
raise

# Handle other non-upstream exceptions
logger.error(
f"MediaPostLinkReverse: Media post create link failed, {str(e)}",
extra={"error_type": type(e).__name__},
)
raise UpstreamException(
message=f"MediaPostLinkReverse: Media post create link failed, {str(e)}",
details={"status": 502, "error": str(e)},
)


__all__ = ["MediaPostLinkReverse"]
2 changes: 2 additions & 0 deletions config.defaults.toml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ response_format = "url"

# ==================== 视频配置 ====================
[video]
# 是否开启生成结束后 Public 资产
enable_public_asset = false
# Reverse 接口并发上限
concurrent = 100
# Reverse 接口超时时间(秒)
Expand Down
Loading