feat: 普号增加25s普通视频、增加pro系列高清模型、统一模型名字、过载不再计入错误禁用计数

This commit is contained in:
TheSmallHanCat
2025-12-24 19:39:28 +08:00
parent 2c2fd44b6a
commit fad83533bf
8 changed files with 226 additions and 69 deletions

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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"""

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}