Skip to content
Open
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
55 changes: 55 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 @@ -1092,6 +1093,7 @@ def __init__(
self.token = token
self.show_think = bool(show_think)
self.upscale_on_finish = bool(upscale_on_finish)
self.enable_public_asset = bool(get_config("video.enable_public_asset"))
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 @@ -1124,6 +1126,28 @@ async def close(self):
await self._dl_service.close()
self._dl_service = None

async def _create_public_link(self, video_url: str) -> str:
if not video_url or not self.enable_public_asset:
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, self.token, video_id
)
payload = response.json() if response is not None else {}
share_link = payload.get("shareLink") if isinstance(payload, dict) else None
if 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

async def process(self, response: AsyncIterable[bytes]) -> AsyncGenerator[str, None]:
result = VideoRoundResult()
try:
Expand Down Expand Up @@ -1157,6 +1181,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 self._create_public_link(final_video_url)

rendered = await self._get_dl().render_video(
final_video_url,
self.token,
Expand Down Expand Up @@ -1187,6 +1216,7 @@ def __init__(
self.model = model
self.token = token
self.upscale_on_finish = bool(upscale_on_finish)
self.enable_public_asset = bool(get_config("video.enable_public_asset"))
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 All @@ -1201,6 +1231,28 @@ async def close(self):
await self._dl_service.close()
self._dl_service = None

async def _create_public_link(self, video_url: str) -> str:
if not video_url or not self.enable_public_asset:
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, self.token, video_id
)
payload = response.json() if response is not None else {}
share_link = payload.get("shareLink") if isinstance(payload, dict) else None
if 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

async def process(self, response: AsyncIterable[bytes]) -> Dict[str, Any]:
try:
result = await _collect_round_result(
Expand All @@ -1222,6 +1274,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 self._create_public_link(final_video_url)

content = await self._get_dl().render_video(
final_video_url,
self.token,
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 = true
# Reverse 接口并发上限
concurrent = 100
# Reverse 接口超时时间(秒)
Expand Down