From fad83533bfb51032b455b8aa4d903392941ad3fe Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Wed, 24 Dec 2025 19:39:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=99=AE=E5=8F=B7=E5=A2=9E=E5=8A=A025s?= =?UTF-8?q?=E6=99=AE=E9=80=9A=E8=A7=86=E9=A2=91=E3=80=81=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?pro=E7=B3=BB=E5=88=97=E9=AB=98=E6=B8=85=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E3=80=81=E7=BB=9F=E4=B8=80=E6=A8=A1=E5=9E=8B=E5=90=8D=E5=AD=97?= =?UTF-8?q?=E3=80=81=E8=BF=87=E8=BD=BD=E4=B8=8D=E5=86=8D=E8=AE=A1=E5=85=A5?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E7=A6=81=E7=94=A8=E8=AE=A1=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 92 +++++++++++------ config/setting.toml | 2 +- config/setting_warp.toml | 2 +- src/core/config.py | 2 +- src/core/database.py | 12 +-- src/services/generation_handler.py | 155 ++++++++++++++++++++++++----- src/services/load_balancer.py | 10 +- src/services/sora_client.py | 20 +++- 8 files changed, 226 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index c1a765c..d127c9e 100644 --- a/README.md +++ b/README.md @@ -110,14 +110,15 @@ python main.py | 功能 | 模型 | 说明 | |------|------|------| -| 文生图 | `sora-image*` | 使用 `content` 为字符串 | -| 图生图 | `sora-image*` | 使用 `content` 数组 + `image_url` | -| 文生视频 | `sora-video*` | 使用 `content` 为字符串 | -| 图生视频 | `sora-video*` | 使用 `content` 数组 + `image_url` | -| 创建角色 | `sora-video*` | 使用 `content` 数组 + `video_url` | -| 角色生成视频 | `sora-video*` | 使用 `content` 数组 + `video_url` + 文本 | -| Remix | `sora-video*` | 在 `content` 中包含 Remix ID | -| 视频分镜 | `sora-video*` | 在 `content` 中使用```[时长s]提示词```格式触发 | +| 文生图 | `gpt-image*` | 使用 `content` 为字符串 | +| 图生图 | `gpt-image*` | 使用 `content` 数组 + `image_url` | +| 文生视频 | `sora2-*` | 使用 `content` 为字符串 | +| 图生视频 | `sora2-*` | 使用 `content` 数组 + `image_url` | +| 视频风格 | `sora2-*` | 在提示词中使用 `{风格ID}` 格式,如 `{anime}提示词` | +| 创建角色 | `sora2-*` | 使用 `content` 数组 + `video_url` | +| 角色生成视频 | `sora2-*` | 使用 `content` 数组 + `video_url` + 文本 | +| Remix | `sora2-*` | 在 `content` 中包含 Remix ID | +| 视频分镜 | `sora2-*` | 在 `content` 中使用```[时长s]提示词```格式触发 | --- @@ -135,20 +136,44 @@ python main.py | 模型 | 说明 | 尺寸 | |------|------|------| -| `sora-image` | 文生图(默认) | 360×360 | -| `sora-image-landscape` | 文生图(横屏) | 540×360 | -| `sora-image-portrait` | 文生图(竖屏) | 360×540 | +| `gpt-image` | 文生图(正方形) | 360×360 | +| `gpt-image-landscape` | 文生图(横屏) | 540×360 | +| `gpt-image-portrait` | 文生图(竖屏) | 360×540 | **视频模型** +**标准版(Sora2)** + | 模型 | 时长 | 方向 | 说明 | |------|------|------|------| -| `sora-video-10s` | 10秒 | 横屏 | 文生视频/图生视频 | -| `sora-video-15s` | 15秒 | 横屏 | 文生视频/图生视频 | -| `sora-video-landscape-10s` | 10秒 | 横屏 | 文生视频/图生视频 | -| `sora-video-landscape-15s` | 15秒 | 横屏 | 文生视频/图生视频 | -| `sora-video-portrait-10s` | 10秒 | 竖屏 | 文生视频/图生视频 | -| `sora-video-portrait-15s` | 15秒 | 竖屏 | 文生视频/图生视频 | +| `sora2-landscape-10s` | 10秒 | 横屏 | 文生视频/图生视频 | +| `sora2-landscape-15s` | 15秒 | 横屏 | 文生视频/图生视频 | +| `sora2-landscape-25s` | 25秒 | 横屏 | 文生视频/图生视频 | +| `sora2-portrait-10s` | 10秒 | 竖屏 | 文生视频/图生视频 | +| `sora2-portrait-15s` | 15秒 | 竖屏 | 文生视频/图生视频 | +| `sora2-portrait-25s` | 25秒 | 竖屏 | 文生视频/图生视频 | + +**Pro 版(需要 ChatGPT Pro 订阅)** + +| 模型 | 时长 | 方向 | 说明 | +|------|------|------|------| +| `sora2pro-landscape-10s` | 10秒 | 横屏 | Pro 质量文生视频/图生视频 | +| `sora2pro-landscape-15s` | 15秒 | 横屏 | Pro 质量文生视频/图生视频 | +| `sora2pro-landscape-25s` | 25秒 | 横屏 | Pro 质量文生视频/图生视频 | +| `sora2pro-portrait-10s` | 10秒 | 竖屏 | Pro 质量文生视频/图生视频 | +| `sora2pro-portrait-15s` | 15秒 | 竖屏 | Pro 质量文生视频/图生视频 | +| `sora2pro-portrait-25s` | 25秒 | 竖屏 | Pro 质量文生视频/图生视频 | + +**Pro HD 版(需要 ChatGPT Pro 订阅,高清质量)** + +| 模型 | 时长 | 方向 | 说明 | +|------|------|------|------| +| `sora2pro-hd-landscape-10s` | 10秒 | 横屏 | Pro 高清文生视频/图生视频 | +| `sora2pro-hd-landscape-15s` | 15秒 | 横屏 | Pro 高清文生视频/图生视频 | +| `sora2pro-hd-portrait-10s` | 10秒 | 竖屏 | Pro 高清文生视频/图生视频 | +| `sora2pro-hd-portrait-15s` | 15秒 | 竖屏 | Pro 高清文生视频/图生视频 | + +> **注意:** Pro 系列模型需要 ChatGPT Pro 订阅(`plan_type: "chatgpt_pro"`)。如果没有 Pro 账号,请求这些模型会返回错误。 #### 请求示例 @@ -159,13 +184,14 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-image", + "model": "gpt-image", "messages": [ { "role": "user", "content": "一只可爱的小猫咪" } - ] + ], + "stream": true }' ``` @@ -176,7 +202,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-image", + "model": "gpt-image", "messages": [ { "role": "user", @@ -205,7 +231,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", @@ -223,7 +249,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", @@ -254,13 +280,14 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", "content": "https://sora.chatgpt.com/p/s_68e3a06dcd888191b150971da152c1f5改成水墨画风格" } - ] + ], + "stream": true }' ``` @@ -280,13 +307,14 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", "content": "[5.0s]猫猫从飞机上跳伞 [5.0s]猫猫降落 [10.0s]猫猫在田野奔跑" } - ] + ], + "stream": true }' ``` @@ -322,7 +350,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", @@ -340,7 +368,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", @@ -358,7 +386,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", @@ -394,7 +422,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", @@ -421,7 +449,7 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Authorization: Bearer han1234" \ -H "Content-Type: application/json" \ -d '{ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", @@ -461,7 +489,7 @@ response = requests.post( "Content-Type": "application/json" }, json={ - "model": "sora-video-landscape-10s", + "model": "sora2-landscape-10s", "messages": [ { "role": "user", diff --git a/config/setting.toml b/config/setting.toml index 577ae07..a08349b 100644 --- a/config/setting.toml +++ b/config/setting.toml @@ -27,7 +27,7 @@ base_url = "http://127.0.0.1:8000" [generation] image_timeout = 300 -video_timeout = 1500 +video_timeout = 3000 [admin] error_ban_threshold = 3 diff --git a/config/setting_warp.toml b/config/setting_warp.toml index ef331d5..cc3fa48 100644 --- a/config/setting_warp.toml +++ b/config/setting_warp.toml @@ -27,7 +27,7 @@ base_url = "http://127.0.0.1:8000" [generation] image_timeout = 300 -video_timeout = 1500 +video_timeout = 3000 [admin] error_ban_threshold = 3 diff --git a/src/core/config.py b/src/core/config.py index 0160b4a..4a9d72d 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -163,7 +163,7 @@ class Config: @property def video_timeout(self) -> int: """Get video generation timeout in seconds""" - return self._config.get("generation", {}).get("video_timeout", 1500) + return self._config.get("generation", {}).get("video_timeout", 3000) def set_video_timeout(self, timeout: int): """Set video generation timeout in seconds""" diff --git a/src/core/database.py b/src/core/database.py index fd570dc..ff22259 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -144,12 +144,12 @@ class Database: if count[0] == 0: # Get generation config from config_dict if provided, otherwise use defaults image_timeout = 300 - video_timeout = 1500 + video_timeout = 3000 if config_dict: generation_config = config_dict.get("generation", {}) image_timeout = generation_config.get("image_timeout", 300) - video_timeout = generation_config.get("video_timeout", 1500) + video_timeout = generation_config.get("video_timeout", 3000) await db.execute(""" INSERT INTO generation_config (id, image_timeout, video_timeout) @@ -401,7 +401,7 @@ class Database: CREATE TABLE IF NOT EXISTS generation_config ( id INTEGER PRIMARY KEY DEFAULT 1, image_timeout INTEGER DEFAULT 300, - video_timeout INTEGER DEFAULT 1500, + video_timeout INTEGER DEFAULT 3000, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) @@ -1013,7 +1013,7 @@ class Database: return GenerationConfig(**dict(row)) # If no row exists, return a default config # This should not happen in normal operation as _ensure_config_rows should create it - return GenerationConfig(image_timeout=300, video_timeout=1500) + return GenerationConfig(image_timeout=300, video_timeout=3000) async def update_generation_config(self, image_timeout: int = None, video_timeout: int = None): """Update generation configuration""" @@ -1027,10 +1027,10 @@ class Database: current = dict(row) # Update only provided fields new_image_timeout = image_timeout if image_timeout is not None else current.get("image_timeout", 300) - new_video_timeout = video_timeout if video_timeout is not None else current.get("video_timeout", 1500) + new_video_timeout = video_timeout if video_timeout is not None else current.get("video_timeout", 3000) else: new_image_timeout = image_timeout if image_timeout is not None else 300 - new_video_timeout = video_timeout if video_timeout is not None else 1500 + new_video_timeout = video_timeout if video_timeout is not None else 3000 await db.execute(""" UPDATE generation_config diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index bf3a86f..28b90b2 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -19,52 +19,139 @@ from ..core.logger import debug_logger # Model configuration MODEL_CONFIG = { - "sora-image": { + "gpt-image": { "type": "image", "width": 360, "height": 360 }, - "sora-image-landscape": { + "gpt-image-landscape": { "type": "image", "width": 540, "height": 360 }, - "sora-image-portrait": { + "gpt-image-portrait": { "type": "image", "width": 360, "height": 540 }, # Video models with 10s duration (300 frames) - "sora-video-10s": { + "sora2-landscape-10s": { "type": "video", "orientation": "landscape", "n_frames": 300 }, - "sora-video-landscape-10s": { - "type": "video", - "orientation": "landscape", - "n_frames": 300 - }, - "sora-video-portrait-10s": { + "sora2-portrait-10s": { "type": "video", "orientation": "portrait", "n_frames": 300 }, # Video models with 15s duration (450 frames) - "sora-video-15s": { + "sora2-landscape-15s": { "type": "video", "orientation": "landscape", "n_frames": 450 }, - "sora-video-landscape-15s": { - "type": "video", - "orientation": "landscape", - "n_frames": 450 - }, - "sora-video-portrait-15s": { + "sora2-portrait-15s": { "type": "video", "orientation": "portrait", "n_frames": 450 + }, + # Video models with 25s duration (750 frames) + "sora2-landscape-25s": { + "type": "video", + "orientation": "landscape", + "n_frames": 750, + "model": "sy_8", + "size": "small" + }, + "sora2-portrait-25s": { + "type": "video", + "orientation": "portrait", + "n_frames": 750, + "model": "sy_8", + "size": "small" + }, + # Pro video models (require Pro subscription) + "sora2pro-landscape-10s": { + "type": "video", + "orientation": "landscape", + "n_frames": 300, + "model": "sy_ore", + "size": "small", + "require_pro": True + }, + "sora2pro-portrait-10s": { + "type": "video", + "orientation": "portrait", + "n_frames": 300, + "model": "sy_ore", + "size": "small", + "require_pro": True + }, + "sora2pro-landscape-15s": { + "type": "video", + "orientation": "landscape", + "n_frames": 450, + "model": "sy_ore", + "size": "small", + "require_pro": True + }, + "sora2pro-portrait-15s": { + "type": "video", + "orientation": "portrait", + "n_frames": 450, + "model": "sy_ore", + "size": "small", + "require_pro": True + }, + "sora2pro-landscape-25s": { + "type": "video", + "orientation": "landscape", + "n_frames": 750, + "model": "sy_ore", + "size": "small", + "require_pro": True + }, + "sora2pro-portrait-25s": { + "type": "video", + "orientation": "portrait", + "n_frames": 750, + "model": "sy_ore", + "size": "small", + "require_pro": True + }, + # Pro HD video models (require Pro subscription, high quality) + "sora2pro-hd-landscape-10s": { + "type": "video", + "orientation": "landscape", + "n_frames": 300, + "model": "sy_ore", + "size": "large", + "require_pro": True + }, + "sora2pro-hd-portrait-10s": { + "type": "video", + "orientation": "portrait", + "n_frames": 300, + "model": "sy_ore", + "size": "large", + "require_pro": True + }, + "sora2pro-hd-landscape-15s": { + "type": "video", + "orientation": "landscape", + "n_frames": 450, + "model": "sy_ore", + "size": "large", + "require_pro": True + }, + "sora2pro-hd-portrait-15s": { + "type": "video", + "orientation": "portrait", + "n_frames": 450, + "model": "sy_ore", + "size": "large", + "require_pro": True } } @@ -294,10 +381,20 @@ class GenerationHandler: return # Streaming mode: proceed with actual generation + # Check if model requires Pro subscription + require_pro = model_config.get("require_pro", False) + # Select token (with lock for image generation, Sora2 quota check for video generation) - token_obj = await self.load_balancer.select_token(for_image_generation=is_image, for_video_generation=is_video) + # If Pro is required, filter for Pro tokens only + token_obj = await self.load_balancer.select_token( + for_image_generation=is_image, + for_video_generation=is_video, + require_pro=require_pro + ) if not token_obj: - if is_image: + if require_pro: + raise Exception("No available Pro tokens. Pro models require a ChatGPT Pro subscription.") + elif is_image: raise Exception("No available tokens for image generation. All tokens are either disabled, cooling down, locked, or expired.") else: raise Exception("No available tokens for video generation. All tokens are either disabled, cooling down, Sora2 quota exhausted, don't support Sora2, or expired.") @@ -383,12 +480,18 @@ class GenerationHandler: ) else: # Normal video generation + # Get model and size from config (default to sy_8 and small for backward compatibility) + sora_model = model_config.get("model", "sy_8") + video_size = model_config.get("size", "small") + task_id = await self.sora_client.generate_video( clean_prompt, token_obj.token, orientation=model_config["orientation"], media_id=media_id, n_frames=n_frames, - style_id=style_id + style_id=style_id, + model=sora_model, + size=video_size ) else: task_id = await self.sora_client.generate_image( @@ -1271,10 +1374,16 @@ class GenerationHandler: # Get n_frames from model configuration n_frames = model_config.get("n_frames", 300) # Default to 300 frames (10s) + # Get model and size from config (default to sy_8 and small for backward compatibility) + sora_model = model_config.get("model", "sy_8") + video_size = model_config.get("size", "small") + task_id = await self.sora_client.generate_video( full_prompt, token_obj.token, orientation=model_config["orientation"], - n_frames=n_frames + n_frames=n_frames, + model=sora_model, + size=video_size ) debug_logger.log_info(f"Video generation started, task_id: {task_id}") @@ -1282,7 +1391,7 @@ class GenerationHandler: task = Task( task_id=task_id, token_id=token_obj.id, - model=f"sora-video-{model_config['orientation']}", + model=f"sora2-video-{model_config['orientation']}", prompt=full_prompt, status="processing", progress=0.0 @@ -1375,7 +1484,7 @@ class GenerationHandler: task = Task( task_id=task_id, token_id=token_obj.id, - model=f"sora-video-{model_config['orientation']}", + model=f"sora2-video-{model_config['orientation']}", prompt=f"remix:{remix_target_id} {clean_prompt}", status="processing", progress=0.0 diff --git a/src/services/load_balancer.py b/src/services/load_balancer.py index 69e9b80..1bed9d9 100644 --- a/src/services/load_balancer.py +++ b/src/services/load_balancer.py @@ -17,13 +17,14 @@ class LoadBalancer: # Use image timeout from config as lock timeout self.token_lock = TokenLock(lock_timeout=config.image_timeout) - async def select_token(self, for_image_generation: bool = False, for_video_generation: bool = False) -> Optional[Token]: + async def select_token(self, for_image_generation: bool = False, for_video_generation: bool = False, require_pro: bool = False) -> Optional[Token]: """ Select a token using random load balancing Args: for_image_generation: If True, only select tokens that are not locked for image generation and have image_enabled=True for_video_generation: If True, filter out tokens with Sora2 quota exhausted (sora2_cooldown_until not expired), tokens that don't support Sora2, and tokens with video_enabled=False + require_pro: If True, only select tokens with ChatGPT Pro subscription (plan_type="chatgpt_pro") Returns: Selected token or None if no available tokens @@ -56,6 +57,13 @@ class LoadBalancer: if not active_tokens: return None + # Filter for Pro tokens if required + if require_pro: + pro_tokens = [token for token in active_tokens if token.plan_type == "chatgpt_pro"] + if not pro_tokens: + return None + active_tokens = pro_tokens + # If for video generation, filter out tokens with Sora2 quota exhausted and tokens without Sora2 support if for_video_generation: from datetime import datetime diff --git a/src/services/sora_client.py b/src/services/sora_client.py index fbaebdd..a77e7ec 100644 --- a/src/services/sora_client.py +++ b/src/services/sora_client.py @@ -254,8 +254,20 @@ class SoraClient: return result["id"] async def generate_video(self, prompt: str, token: str, orientation: str = "landscape", - media_id: Optional[str] = None, n_frames: int = 450, style_id: Optional[str] = None) -> str: - """Generate video (text-to-video or image-to-video)""" + media_id: Optional[str] = None, n_frames: int = 450, style_id: Optional[str] = None, + model: str = "sy_8", size: str = "small") -> str: + """Generate video (text-to-video or image-to-video) + + Args: + prompt: Video generation prompt + token: Access token + orientation: Video orientation (landscape/portrait) + media_id: Optional image media_id for image-to-video + n_frames: Number of frames (300/450/750) + style_id: Optional style ID + model: Model to use (sy_8 for standard, sy_ore for pro) + size: Video size (small for standard, large for HD) + """ inpaint_items = [] if media_id: inpaint_items = [{ @@ -267,9 +279,9 @@ class SoraClient: "kind": "video", "prompt": prompt, "orientation": orientation, - "size": "small", + "size": size, "n_frames": n_frames, - "model": "sy_8", + "model": model, "inpaint_items": inpaint_items, "style_id": style_id }