mirror of
https://github.com/TheSmallHanCat/sora2api.git
synced 2026-02-24 02:04:41 +08:00
84
README.md
84
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 支持**视频角色生成**功能。
|
Sora2API 支持**视频角色生成**功能。
|
||||||
|
|||||||
@@ -166,6 +166,27 @@ class GenerationHandler:
|
|||||||
|
|
||||||
return cleaned
|
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:
|
async def _download_file(self, url: str) -> bytes:
|
||||||
"""Download file from URL
|
"""Download file from URL
|
||||||
|
|
||||||
@@ -339,30 +360,35 @@ class GenerationHandler:
|
|||||||
# Get n_frames from model configuration
|
# Get n_frames from model configuration
|
||||||
n_frames = model_config.get("n_frames", 300) # Default to 300 frames (10s)
|
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
|
# 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
|
# Storyboard mode
|
||||||
if stream:
|
if stream:
|
||||||
yield self._format_stream_chunk(
|
yield self._format_stream_chunk(
|
||||||
reasoning_content="Detected storyboard format. Converting to storyboard API format...\n"
|
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}")
|
debug_logger.log_info(f"Storyboard mode detected. Formatted prompt: {formatted_prompt}")
|
||||||
|
|
||||||
task_id = await self.sora_client.generate_storyboard(
|
task_id = await self.sora_client.generate_storyboard(
|
||||||
formatted_prompt, token_obj.token,
|
formatted_prompt, token_obj.token,
|
||||||
orientation=model_config["orientation"],
|
orientation=model_config["orientation"],
|
||||||
media_id=media_id,
|
media_id=media_id,
|
||||||
n_frames=n_frames
|
n_frames=n_frames,
|
||||||
|
style_id=style_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Normal video generation
|
# Normal video generation
|
||||||
task_id = await self.sora_client.generate_video(
|
task_id = await self.sora_client.generate_video(
|
||||||
prompt, token_obj.token,
|
clean_prompt, token_obj.token,
|
||||||
orientation=model_config["orientation"],
|
orientation=model_config["orientation"],
|
||||||
media_id=media_id,
|
media_id=media_id,
|
||||||
n_frames=n_frames
|
n_frames=n_frames,
|
||||||
|
style_id=style_id
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
task_id = await self.sora_client.generate_image(
|
task_id = await self.sora_client.generate_image(
|
||||||
@@ -1325,6 +1351,9 @@ class GenerationHandler:
|
|||||||
# Clean remix link from prompt to avoid duplication
|
# Clean remix link from prompt to avoid duplication
|
||||||
clean_prompt = self._clean_remix_link_from_prompt(prompt)
|
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
|
# Get n_frames from model configuration
|
||||||
n_frames = model_config.get("n_frames", 300) # Default to 300 frames (10s)
|
n_frames = model_config.get("n_frames", 300) # Default to 300 frames (10s)
|
||||||
|
|
||||||
@@ -1337,7 +1366,8 @@ class GenerationHandler:
|
|||||||
prompt=clean_prompt,
|
prompt=clean_prompt,
|
||||||
token=token_obj.token,
|
token=token_obj.token,
|
||||||
orientation=model_config["orientation"],
|
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}")
|
debug_logger.log_info(f"Remix generation started, task_id: {task_id}")
|
||||||
|
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ class SoraClient:
|
|||||||
return result["id"]
|
return result["id"]
|
||||||
|
|
||||||
async def generate_video(self, prompt: str, token: str, orientation: str = "landscape",
|
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)"""
|
"""Generate video (text-to-video or image-to-video)"""
|
||||||
inpaint_items = []
|
inpaint_items = []
|
||||||
if media_id:
|
if media_id:
|
||||||
@@ -270,7 +270,8 @@ class SoraClient:
|
|||||||
"size": "small",
|
"size": "small",
|
||||||
"n_frames": n_frames,
|
"n_frames": n_frames,
|
||||||
"model": "sy_8",
|
"model": "sy_8",
|
||||||
"inpaint_items": inpaint_items
|
"inpaint_items": inpaint_items,
|
||||||
|
"style_id": style_id
|
||||||
}
|
}
|
||||||
|
|
||||||
# 生成请求需要添加 sentinel token
|
# 生成请求需要添加 sentinel token
|
||||||
@@ -648,7 +649,7 @@ class SoraClient:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def remix_video(self, remix_target_id: str, prompt: str, token: str,
|
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)
|
"""Generate video using remix (based on existing video)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -657,6 +658,7 @@ class SoraClient:
|
|||||||
token: Access token
|
token: Access token
|
||||||
orientation: Video orientation (portrait/landscape)
|
orientation: Video orientation (portrait/landscape)
|
||||||
n_frames: Number of frames
|
n_frames: Number of frames
|
||||||
|
style_id: Optional style ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
task_id
|
task_id
|
||||||
@@ -670,14 +672,15 @@ class SoraClient:
|
|||||||
"cameo_replacements": {},
|
"cameo_replacements": {},
|
||||||
"model": "sy_8",
|
"model": "sy_8",
|
||||||
"orientation": orientation,
|
"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)
|
result = await self._make_request("POST", "/nf/create", token, json_data=json_data, add_sentinel_token=True)
|
||||||
return result.get("id")
|
return result.get("id")
|
||||||
|
|
||||||
async def generate_storyboard(self, prompt: str, token: str, orientation: str = "landscape",
|
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
|
"""Generate video using storyboard mode
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -686,6 +689,7 @@ class SoraClient:
|
|||||||
orientation: Video orientation (portrait/landscape)
|
orientation: Video orientation (portrait/landscape)
|
||||||
media_id: Optional image media_id for image-to-video
|
media_id: Optional image media_id for image-to-video
|
||||||
n_frames: Number of frames
|
n_frames: Number of frames
|
||||||
|
style_id: Optional style ID
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
task_id
|
task_id
|
||||||
@@ -709,7 +713,7 @@ class SoraClient:
|
|||||||
"remix_target_id": None,
|
"remix_target_id": None,
|
||||||
"model": "sy_8",
|
"model": "sy_8",
|
||||||
"metadata": None,
|
"metadata": None,
|
||||||
"style_id": None,
|
"style_id": style_id,
|
||||||
"cameo_ids": None,
|
"cameo_ids": None,
|
||||||
"cameo_replacements": None,
|
"cameo_replacements": None,
|
||||||
"audio_caption": None,
|
"audio_caption": None,
|
||||||
|
|||||||
Reference in New Issue
Block a user