From 2c2fd44b6af13ed64f923e5d3824c6f497caa3a1 Mon Sep 17 00:00:00 2001 From: TheSmallHanCat Date: Wed, 24 Dec 2025 10:12:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E5=8A=9F=E8=83=BD=20close=20#32?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 84 ++++++++++++++++++++++++++++++ src/services/generation_handler.py | 42 ++++++++++++--- src/services/sora_client.py | 16 +++--- 3 files changed, 130 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index d8cf7e4..c1a765c 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,90 @@ curl -X POST "http://localhost:8000/v1/chat/completions" \ }' ``` +### 视频风格功能 + +Sora2API 支持**视频风格**功能,可以为生成的视频应用预设风格。 + +#### 使用方法 + +在提示词中使用 `{风格ID}` 格式指定风格,系统会自动提取并应用该风格。 + +#### 支持的风格 + +| 风格ID | 显示名称 | 说明 | +|--------|----------|------| +| `festive` | Festive | 节日风格 | +| `kakalaka` | 🪭👺 | 混沌风格 | +| `news` | News | 新闻风格 | +| `selfie` | Selfie | 自拍风格 | +| `handheld` | Handheld | 手持风格 | +| `golden` | Golden | 金色风格 | +| `anime` | Anime | 动漫风格 | +| `retro` | Retro | 复古风格 | +| `nostalgic` | Vintage | 怀旧风格 | +| `comic` | Comic | 漫画风格 | + +#### 示例 + +**使用动漫风格生成视频** + +```bash +curl -X POST "http://localhost:8000/v1/chat/completions" \ + -H "Authorization: Bearer han1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "sora-video-landscape-10s", + "messages": [ + { + "role": "user", + "content": "{anime}一只小猫在草地上奔跑" + } + ], + "stream": true + }' +``` + +**使用复古风格生成视频** + +```bash +curl -X POST "http://localhost:8000/v1/chat/completions" \ + -H "Authorization: Bearer han1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "sora-video-landscape-10s", + "messages": [ + { + "role": "user", + "content": "{retro}城市街道夜景" + } + ], + "stream": true + }' +``` + +**在Remix中使用风格** + +```bash +curl -X POST "http://localhost:8000/v1/chat/completions" \ + -H "Authorization: Bearer han1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "sora-video-landscape-10s", + "messages": [ + { + "role": "user", + "content": "{comic}https://sora.chatgpt.com/p/s_68e3a06dcd888191b150971da152c1f5改成漫画风格" + } + ], + "stream": true + }' +``` + +**注意事项** +- 风格标记 `{风格ID}` 可以放在提示词的任意位置 +- 系统会自动提取风格ID并从提示词中移除风格标记 +- 如果不指定风格,将使用默认风格生成 + ### 视频角色功能 Sora2API 支持**视频角色生成**功能。 diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index ca484ae..bf3a86f 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -166,6 +166,27 @@ class GenerationHandler: return cleaned + def _extract_style(self, prompt: str) -> tuple[str, Optional[str]]: + """Extract style from prompt + + Args: + prompt: Original prompt + + Returns: + Tuple of (cleaned_prompt, style_id) + """ + # Extract {style} pattern + match = re.search(r'\{([^}]+)\}', prompt) + if match: + style_id = match.group(1).strip() + # Remove {style} from prompt + cleaned_prompt = re.sub(r'\{[^}]+\}', '', prompt).strip() + # Clean up extra whitespace + cleaned_prompt = ' '.join(cleaned_prompt.split()) + debug_logger.log_info(f"Extracted style: '{style_id}' from prompt: '{prompt}'") + return cleaned_prompt, style_id + return prompt, None + async def _download_file(self, url: str) -> bytes: """Download file from URL @@ -339,30 +360,35 @@ class GenerationHandler: # Get n_frames from model configuration n_frames = model_config.get("n_frames", 300) # Default to 300 frames (10s) + # Extract style from prompt + clean_prompt, style_id = self._extract_style(prompt) + # Check if prompt is in storyboard format - if self.sora_client.is_storyboard_prompt(prompt): + if self.sora_client.is_storyboard_prompt(clean_prompt): # Storyboard mode if stream: yield self._format_stream_chunk( reasoning_content="Detected storyboard format. Converting to storyboard API format...\n" ) - formatted_prompt = self.sora_client.format_storyboard_prompt(prompt) + formatted_prompt = self.sora_client.format_storyboard_prompt(clean_prompt) debug_logger.log_info(f"Storyboard mode detected. Formatted prompt: {formatted_prompt}") task_id = await self.sora_client.generate_storyboard( formatted_prompt, token_obj.token, orientation=model_config["orientation"], media_id=media_id, - n_frames=n_frames + n_frames=n_frames, + style_id=style_id ) else: # Normal video generation task_id = await self.sora_client.generate_video( - prompt, token_obj.token, + clean_prompt, token_obj.token, orientation=model_config["orientation"], media_id=media_id, - n_frames=n_frames + n_frames=n_frames, + style_id=style_id ) else: task_id = await self.sora_client.generate_image( @@ -1325,6 +1351,9 @@ class GenerationHandler: # Clean remix link from prompt to avoid duplication clean_prompt = self._clean_remix_link_from_prompt(prompt) + # Extract style from prompt + clean_prompt, style_id = self._extract_style(clean_prompt) + # Get n_frames from model configuration n_frames = model_config.get("n_frames", 300) # Default to 300 frames (10s) @@ -1337,7 +1366,8 @@ class GenerationHandler: prompt=clean_prompt, token=token_obj.token, orientation=model_config["orientation"], - n_frames=n_frames + n_frames=n_frames, + style_id=style_id ) debug_logger.log_info(f"Remix generation started, task_id: {task_id}") diff --git a/src/services/sora_client.py b/src/services/sora_client.py index 0068b35..fbaebdd 100644 --- a/src/services/sora_client.py +++ b/src/services/sora_client.py @@ -254,7 +254,7 @@ 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) -> str: + media_id: Optional[str] = None, n_frames: int = 450, style_id: Optional[str] = None) -> str: """Generate video (text-to-video or image-to-video)""" inpaint_items = [] if media_id: @@ -270,7 +270,8 @@ class SoraClient: "size": "small", "n_frames": n_frames, "model": "sy_8", - "inpaint_items": inpaint_items + "inpaint_items": inpaint_items, + "style_id": style_id } # 生成请求需要添加 sentinel token @@ -648,7 +649,7 @@ class SoraClient: return True async def remix_video(self, remix_target_id: str, prompt: str, token: str, - orientation: str = "portrait", n_frames: int = 450) -> str: + orientation: str = "portrait", n_frames: int = 450, style_id: Optional[str] = None) -> str: """Generate video using remix (based on existing video) Args: @@ -657,6 +658,7 @@ class SoraClient: token: Access token orientation: Video orientation (portrait/landscape) n_frames: Number of frames + style_id: Optional style ID Returns: task_id @@ -670,14 +672,15 @@ class SoraClient: "cameo_replacements": {}, "model": "sy_8", "orientation": orientation, - "n_frames": n_frames + "n_frames": n_frames, + "style_id": style_id } result = await self._make_request("POST", "/nf/create", token, json_data=json_data, add_sentinel_token=True) return result.get("id") async def generate_storyboard(self, prompt: str, token: str, orientation: str = "landscape", - media_id: Optional[str] = None, n_frames: int = 450) -> str: + media_id: Optional[str] = None, n_frames: int = 450, style_id: Optional[str] = None) -> str: """Generate video using storyboard mode Args: @@ -686,6 +689,7 @@ class SoraClient: orientation: Video orientation (portrait/landscape) media_id: Optional image media_id for image-to-video n_frames: Number of frames + style_id: Optional style ID Returns: task_id @@ -709,7 +713,7 @@ class SoraClient: "remix_target_id": None, "model": "sy_8", "metadata": None, - "style_id": None, + "style_id": style_id, "cameo_ids": None, "cameo_replacements": None, "audio_caption": None,