local-coze 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- local_coze/__init__.py +110 -0
- local_coze/cli/__init__.py +3 -0
- local_coze/cli/chat.py +126 -0
- local_coze/cli/cli.py +34 -0
- local_coze/cli/constants.py +7 -0
- local_coze/cli/db.py +81 -0
- local_coze/cli/embedding.py +193 -0
- local_coze/cli/image.py +162 -0
- local_coze/cli/knowledge.py +195 -0
- local_coze/cli/search.py +198 -0
- local_coze/cli/utils.py +41 -0
- local_coze/cli/video.py +191 -0
- local_coze/cli/video_edit.py +888 -0
- local_coze/cli/voice.py +351 -0
- local_coze/core/__init__.py +25 -0
- local_coze/core/client.py +253 -0
- local_coze/core/config.py +58 -0
- local_coze/core/exceptions.py +67 -0
- local_coze/database/__init__.py +29 -0
- local_coze/database/client.py +170 -0
- local_coze/database/migration.py +342 -0
- local_coze/embedding/__init__.py +31 -0
- local_coze/embedding/client.py +350 -0
- local_coze/embedding/models.py +130 -0
- local_coze/image/__init__.py +19 -0
- local_coze/image/client.py +110 -0
- local_coze/image/models.py +163 -0
- local_coze/knowledge/__init__.py +19 -0
- local_coze/knowledge/client.py +148 -0
- local_coze/knowledge/models.py +45 -0
- local_coze/llm/__init__.py +25 -0
- local_coze/llm/client.py +317 -0
- local_coze/llm/models.py +48 -0
- local_coze/memory/__init__.py +14 -0
- local_coze/memory/client.py +176 -0
- local_coze/s3/__init__.py +12 -0
- local_coze/s3/client.py +580 -0
- local_coze/s3/models.py +18 -0
- local_coze/search/__init__.py +19 -0
- local_coze/search/client.py +183 -0
- local_coze/search/models.py +57 -0
- local_coze/video/__init__.py +17 -0
- local_coze/video/client.py +347 -0
- local_coze/video/models.py +39 -0
- local_coze/video_edit/__init__.py +23 -0
- local_coze/video_edit/examples.py +340 -0
- local_coze/video_edit/frame_extractor.py +176 -0
- local_coze/video_edit/models.py +362 -0
- local_coze/video_edit/video_edit.py +631 -0
- local_coze/voice/__init__.py +17 -0
- local_coze/voice/asr.py +82 -0
- local_coze/voice/models.py +86 -0
- local_coze/voice/tts.py +94 -0
- local_coze-0.0.1.dist-info/METADATA +636 -0
- local_coze-0.0.1.dist-info/RECORD +58 -0
- local_coze-0.0.1.dist-info/WHEEL +4 -0
- local_coze-0.0.1.dist-info/entry_points.txt +3 -0
- local_coze-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from coze_coding_utils.runtime_ctx.context import Context
|
|
4
|
+
from cozeloop.decorator import observe
|
|
5
|
+
|
|
6
|
+
from ..core.client import BaseClient
|
|
7
|
+
from ..core.config import Config
|
|
8
|
+
from ..core.exceptions import APIError
|
|
9
|
+
from .models import ImageItem, SearchFilter, SearchRequest, SearchResponse, WebItem
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _convert_to_api_format(data: dict) -> dict:
|
|
13
|
+
result = {}
|
|
14
|
+
for key, value in data.items():
|
|
15
|
+
if value is None:
|
|
16
|
+
continue
|
|
17
|
+
if key == "query":
|
|
18
|
+
result["Query"] = value
|
|
19
|
+
elif key == "search_type":
|
|
20
|
+
result["SearchType"] = value
|
|
21
|
+
elif key == "count":
|
|
22
|
+
result["Count"] = value
|
|
23
|
+
elif key == "need_summary":
|
|
24
|
+
result["NeedSummary"] = value
|
|
25
|
+
elif key == "time_range":
|
|
26
|
+
result["TimeRange"] = value
|
|
27
|
+
elif key == "filter" and value:
|
|
28
|
+
result["Filter"] = {
|
|
29
|
+
"NeedContent": value.get("need_content", False),
|
|
30
|
+
"NeedUrl": value.get("need_url", False),
|
|
31
|
+
"Sites": value.get("sites"),
|
|
32
|
+
"BlockHosts": value.get("block_hosts"),
|
|
33
|
+
}
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _convert_from_api_format(item: dict) -> dict:
|
|
38
|
+
result = {}
|
|
39
|
+
for key, value in item.items():
|
|
40
|
+
if key == "Id":
|
|
41
|
+
result["id"] = value
|
|
42
|
+
elif key == "SortId":
|
|
43
|
+
result["sort_id"] = value
|
|
44
|
+
elif key == "Title":
|
|
45
|
+
result["title"] = value
|
|
46
|
+
elif key == "SiteName":
|
|
47
|
+
result["site_name"] = value
|
|
48
|
+
elif key == "Url":
|
|
49
|
+
result["url"] = value
|
|
50
|
+
elif key == "Snippet":
|
|
51
|
+
result["snippet"] = value
|
|
52
|
+
elif key == "Summary":
|
|
53
|
+
result["summary"] = value
|
|
54
|
+
elif key == "Content":
|
|
55
|
+
result["content"] = value
|
|
56
|
+
elif key == "PublishTime":
|
|
57
|
+
result["publish_time"] = value
|
|
58
|
+
elif key == "LogoUrl":
|
|
59
|
+
result["logo_url"] = value
|
|
60
|
+
elif key == "RankScore":
|
|
61
|
+
result["rank_score"] = value
|
|
62
|
+
elif key == "AuthInfoDes":
|
|
63
|
+
result["auth_info_des"] = value
|
|
64
|
+
elif key == "AuthInfoLevel":
|
|
65
|
+
result["auth_info_level"] = value
|
|
66
|
+
elif key == "Image":
|
|
67
|
+
result["image"] = {
|
|
68
|
+
"url": value.get("Url"),
|
|
69
|
+
"width": value.get("Width"),
|
|
70
|
+
"height": value.get("Height"),
|
|
71
|
+
"shape": value.get("Shape"),
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SearchClient(BaseClient):
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
config: Optional[Config] = None,
|
|
80
|
+
ctx: Optional[Context] = None,
|
|
81
|
+
custom_headers: Optional[Dict[str, str]] = None,
|
|
82
|
+
verbose: bool = False,
|
|
83
|
+
):
|
|
84
|
+
super().__init__(config, ctx, custom_headers, verbose)
|
|
85
|
+
self.base_url = self.config.base_url
|
|
86
|
+
|
|
87
|
+
@observe(name="web_search")
|
|
88
|
+
def search(
|
|
89
|
+
self,
|
|
90
|
+
query: str,
|
|
91
|
+
search_type: str = "web",
|
|
92
|
+
count: Optional[int] = 10,
|
|
93
|
+
need_content: Optional[bool] = False,
|
|
94
|
+
need_url: Optional[bool] = False,
|
|
95
|
+
sites: Optional[str] = None,
|
|
96
|
+
block_hosts: Optional[str] = None,
|
|
97
|
+
need_summary: Optional[bool] = True,
|
|
98
|
+
time_range: Optional[str] = None,
|
|
99
|
+
) -> SearchResponse:
|
|
100
|
+
search_filter = SearchFilter(
|
|
101
|
+
need_content=need_content,
|
|
102
|
+
need_url=need_url,
|
|
103
|
+
sites=sites,
|
|
104
|
+
block_hosts=block_hosts,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
request = SearchRequest(
|
|
108
|
+
query=query,
|
|
109
|
+
search_type=search_type,
|
|
110
|
+
count=count,
|
|
111
|
+
filter=search_filter,
|
|
112
|
+
need_summary=need_summary,
|
|
113
|
+
time_range=time_range,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
api_request = _convert_to_api_format(request.model_dump(exclude_none=True))
|
|
117
|
+
|
|
118
|
+
response = self._request(
|
|
119
|
+
method="POST",
|
|
120
|
+
url=f"{self.base_url}/api/search_api/web_search",
|
|
121
|
+
json=api_request,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
response_metadata = response.get("ResponseMetadata", {})
|
|
125
|
+
if response_metadata.get("Error"):
|
|
126
|
+
raise APIError(f"Search failed: {response_metadata.get('Error')}")
|
|
127
|
+
|
|
128
|
+
result = response.get("Result", {})
|
|
129
|
+
|
|
130
|
+
web_items = []
|
|
131
|
+
if result.get("WebResults"):
|
|
132
|
+
web_items = [
|
|
133
|
+
WebItem(**_convert_from_api_format(item))
|
|
134
|
+
for item in result.get("WebResults", [])
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
image_items = []
|
|
138
|
+
if result.get("ImageResults"):
|
|
139
|
+
image_items = [
|
|
140
|
+
ImageItem(**_convert_from_api_format(item))
|
|
141
|
+
for item in result.get("ImageResults", [])
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
summary = None
|
|
145
|
+
if result.get("Choices"):
|
|
146
|
+
summary = (
|
|
147
|
+
result.get("Choices", [{}])[0].get("Message", {}).get("Content", "")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return SearchResponse(
|
|
151
|
+
web_items=web_items, image_items=image_items, summary=summary
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@observe(name="web_search_simple")
|
|
155
|
+
def web_search(
|
|
156
|
+
self,
|
|
157
|
+
query: str,
|
|
158
|
+
count: Optional[int] = 10,
|
|
159
|
+
need_summary: Optional[bool] = True,
|
|
160
|
+
) -> SearchResponse:
|
|
161
|
+
return self.search(
|
|
162
|
+
query=query, search_type="web", count=count, need_summary=need_summary
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@observe(name="web_search_with_summary")
|
|
166
|
+
def web_search_with_summary(
|
|
167
|
+
self,
|
|
168
|
+
query: str,
|
|
169
|
+
count: Optional[int] = 10,
|
|
170
|
+
) -> SearchResponse:
|
|
171
|
+
return self.search(
|
|
172
|
+
query=query, search_type="web_summary", count=count, need_summary=True
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@observe(name="image_search")
|
|
176
|
+
def image_search(
|
|
177
|
+
self,
|
|
178
|
+
query: str,
|
|
179
|
+
count: Optional[int] = 10,
|
|
180
|
+
) -> SearchResponse:
|
|
181
|
+
return self.search(
|
|
182
|
+
query=query, search_type="image", count=count, need_summary=False
|
|
183
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import Optional, List, Literal
|
|
2
|
+
from pydantic import BaseModel, Field
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ImageInfo(BaseModel):
|
|
6
|
+
url: str = Field(..., description="图片链接")
|
|
7
|
+
width: Optional[int] = Field(None, description="宽")
|
|
8
|
+
height: Optional[int] = Field(None, description="高")
|
|
9
|
+
shape: str = Field(..., description="横长方形/竖长方形/方形")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class WebItem(BaseModel):
|
|
13
|
+
id: str = Field(..., description="结果Id")
|
|
14
|
+
sort_id: int = Field(..., description="排序Id")
|
|
15
|
+
title: str = Field(..., description="标题")
|
|
16
|
+
site_name: Optional[str] = Field(None, description="站点名")
|
|
17
|
+
url: Optional[str] = Field(None, description="落地页")
|
|
18
|
+
snippet: str = Field(..., description="普通摘要")
|
|
19
|
+
summary: Optional[str] = Field(None, description="精准摘要")
|
|
20
|
+
content: Optional[str] = Field(None, description="正文")
|
|
21
|
+
publish_time: Optional[str] = Field(None, description="发布时间")
|
|
22
|
+
logo_url: Optional[str] = Field(None, description="落地页IconUrl链接")
|
|
23
|
+
rank_score: Optional[float] = Field(None, description="得分")
|
|
24
|
+
auth_info_des: str = Field(..., description="权威度描述")
|
|
25
|
+
auth_info_level: int = Field(..., description="权威度评级")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ImageItem(BaseModel):
|
|
29
|
+
id: str = Field(..., description="结果Id")
|
|
30
|
+
sort_id: int = Field(..., description="排序Id")
|
|
31
|
+
title: Optional[str] = Field(None, description="标题")
|
|
32
|
+
site_name: Optional[str] = Field(None, description="站点名")
|
|
33
|
+
url: Optional[str] = Field(None, description="落地页")
|
|
34
|
+
publish_time: Optional[str] = Field(None, description="发布时间")
|
|
35
|
+
image: ImageInfo = Field(..., description="图片详情")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SearchFilter(BaseModel):
|
|
39
|
+
need_content: Optional[bool] = Field(False, description="是否仅返回有正文的结果")
|
|
40
|
+
need_url: Optional[bool] = Field(False, description="是否仅返回原文链接的结果")
|
|
41
|
+
sites: Optional[str] = Field(None, description="指定搜索的Site范围")
|
|
42
|
+
block_hosts: Optional[str] = Field(None, description="指定屏蔽的搜索Site")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SearchRequest(BaseModel):
|
|
46
|
+
query: str = Field(..., description="用户搜索query")
|
|
47
|
+
search_type: Literal["web", "web_summary", "image"] = Field("web", description="搜索类型")
|
|
48
|
+
count: Optional[int] = Field(10, description="返回条数")
|
|
49
|
+
filter: Optional[SearchFilter] = None
|
|
50
|
+
need_summary: Optional[bool] = Field(True, description="是否需要精准摘要")
|
|
51
|
+
time_range: Optional[str] = Field(None, description="指定搜索的发文时间")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SearchResponse(BaseModel):
|
|
55
|
+
web_items: List[WebItem] = Field(default_factory=list, description="Web搜索结果")
|
|
56
|
+
image_items: List[ImageItem] = Field(default_factory=list, description="图片搜索结果")
|
|
57
|
+
summary: Optional[str] = Field(None, description="搜索结果摘要")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .client import VideoGenerationClient
|
|
2
|
+
from .models import (
|
|
3
|
+
VideoGenerationRequest,
|
|
4
|
+
VideoGenerationTask,
|
|
5
|
+
TextContent,
|
|
6
|
+
ImageURLContent,
|
|
7
|
+
ImageURL
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"VideoGenerationClient",
|
|
12
|
+
"VideoGenerationRequest",
|
|
13
|
+
"VideoGenerationTask",
|
|
14
|
+
"TextContent",
|
|
15
|
+
"ImageURLContent",
|
|
16
|
+
"ImageURL"
|
|
17
|
+
]
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Dict, List, Optional, Tuple, Union
|
|
5
|
+
|
|
6
|
+
from coze_coding_utils.runtime_ctx.context import Context
|
|
7
|
+
from cozeloop.decorator import observe
|
|
8
|
+
|
|
9
|
+
from ..core.client import BaseClient
|
|
10
|
+
from ..core.config import Config
|
|
11
|
+
from ..core.exceptions import APIError
|
|
12
|
+
from .models import (
|
|
13
|
+
ImageURLContent,
|
|
14
|
+
TextContent,
|
|
15
|
+
VideoGenerationRequest,
|
|
16
|
+
VideoGenerationTask,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class VideoGenerationClient(BaseClient):
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
config: Optional[Config] = None,
|
|
26
|
+
ctx: Optional[Context] = None,
|
|
27
|
+
custom_headers: Optional[Dict[str, str]] = None,
|
|
28
|
+
verbose: bool = False,
|
|
29
|
+
):
|
|
30
|
+
super().__init__(config, ctx, custom_headers, verbose)
|
|
31
|
+
self.base_url = self.config.base_url
|
|
32
|
+
|
|
33
|
+
def _create_task(
|
|
34
|
+
self,
|
|
35
|
+
model: str,
|
|
36
|
+
content: List[Union[TextContent, ImageURLContent]],
|
|
37
|
+
resolution: Optional[str] = "720p",
|
|
38
|
+
ratio: Optional[str] = "16:9",
|
|
39
|
+
duration: Optional[int] = 5,
|
|
40
|
+
watermark: Optional[bool] = True,
|
|
41
|
+
seed: Optional[int] = None,
|
|
42
|
+
camerafixed: Optional[bool] = False,
|
|
43
|
+
generate_audio: Optional[bool] = True,
|
|
44
|
+
) -> str:
|
|
45
|
+
request = VideoGenerationRequest(
|
|
46
|
+
model=model,
|
|
47
|
+
content=content,
|
|
48
|
+
resolution=resolution,
|
|
49
|
+
ratio=ratio,
|
|
50
|
+
duration=duration,
|
|
51
|
+
watermark=watermark,
|
|
52
|
+
seed=seed,
|
|
53
|
+
camerafixed=camerafixed,
|
|
54
|
+
generate_audio=generate_audio,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
response = self._request(
|
|
58
|
+
method="POST",
|
|
59
|
+
url=f"{self.base_url}/api/v3/contents/generations/tasks",
|
|
60
|
+
json=request.model_dump(exclude_none=True),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return response.get("id")
|
|
64
|
+
|
|
65
|
+
def _get_task_status(self, task_id: str) -> VideoGenerationTask:
|
|
66
|
+
response = self._request(
|
|
67
|
+
method="GET",
|
|
68
|
+
url=f"{self.base_url}/api/v3/contents/generations/tasks/{task_id}",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return VideoGenerationTask(**response)
|
|
72
|
+
|
|
73
|
+
@observe(name="video_generation")
|
|
74
|
+
def video_generation(
|
|
75
|
+
self,
|
|
76
|
+
content_items: List[Union[TextContent, ImageURLContent]],
|
|
77
|
+
callback_url: Optional[str] = None,
|
|
78
|
+
return_last_frame: Optional[bool] = False,
|
|
79
|
+
model: str = "doubao-seedance-1-5-pro-251215",
|
|
80
|
+
max_wait_time: int = 900,
|
|
81
|
+
resolution: Optional[str] = "720p",
|
|
82
|
+
ratio: Optional[str] = "16:9",
|
|
83
|
+
duration: Optional[int] = 5,
|
|
84
|
+
watermark: Optional[bool] = True,
|
|
85
|
+
seed: Optional[int] = None,
|
|
86
|
+
camerafixed: Optional[bool] = False,
|
|
87
|
+
generate_audio: Optional[bool] = True,
|
|
88
|
+
) -> Tuple[Optional[str], Dict, str]:
|
|
89
|
+
"""
|
|
90
|
+
视频生成函数(同步)。调用生视频模型,非大语言模型,仅用于生成视频
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
content_items: 输入给模型,生成视频的信息,支持文本信息和图片信息。
|
|
94
|
+
callback_url: 可选,填写本次生成任务结果的回调通知地址。当视频生成任务有状态变化时,方舟将向此地址推送 POST 请求。
|
|
95
|
+
return_last_frame: 可选,返回生成视频的尾帧图像。设置为 true 后,可获取视频的尾帧图像。
|
|
96
|
+
model: 模型名称,默认使用 doubao-seedance-1-5-pro-251215
|
|
97
|
+
max_wait_time: 最大等待时间(秒),默认 900 秒
|
|
98
|
+
resolution: 视频分辨率,可选 "480p" 或 "720p",默认 "720p"
|
|
99
|
+
ratio: 视频比例,可选 "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "adaptive",默认 "16:9"
|
|
100
|
+
duration: 视频时长(秒),范围 4 到 12,默认 5,若是 -1,则由模型决定长度
|
|
101
|
+
watermark: 是否添加水印,默认 True
|
|
102
|
+
seed: 随机种子,用于复现生成结果
|
|
103
|
+
camerafixed: 是否固定摄像机位置,默认 False
|
|
104
|
+
generate_audio: 是否生成音频,默认 True
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Tuple[Optional[str], Dict, str]: (视频URL, 完整响应数据字典, 尾帧图像URL)
|
|
108
|
+
完整响应数据示例:
|
|
109
|
+
{
|
|
110
|
+
"id": "cgt-2025******-****",
|
|
111
|
+
"model": "doubao-seedance-1-5-pro-251215",
|
|
112
|
+
"status": "succeeded",
|
|
113
|
+
"content": {
|
|
114
|
+
"video_url": "https://ark-content-generation-cn-beijing.tos-cn-beijing.volces.com/..."
|
|
115
|
+
},
|
|
116
|
+
"seed": 10,
|
|
117
|
+
"resolution": "720p",
|
|
118
|
+
"ratio": "16:9",
|
|
119
|
+
"duration": 5,
|
|
120
|
+
"framespersecond": 24,
|
|
121
|
+
"usage": {
|
|
122
|
+
"completion_tokens": 108900,
|
|
123
|
+
"total_tokens": 108900
|
|
124
|
+
},
|
|
125
|
+
"created_at": 1743414619,
|
|
126
|
+
"updated_at": 1743414673
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
APIError: 当视频生成失败、超时或出现其他错误时
|
|
131
|
+
"""
|
|
132
|
+
poll_interval = 5
|
|
133
|
+
|
|
134
|
+
request_data = {
|
|
135
|
+
"model": model,
|
|
136
|
+
"content": [item.model_dump() for item in content_items],
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if callback_url:
|
|
140
|
+
request_data["callback_url"] = callback_url
|
|
141
|
+
if return_last_frame:
|
|
142
|
+
request_data["return_last_frame"] = return_last_frame
|
|
143
|
+
|
|
144
|
+
if resolution is not None:
|
|
145
|
+
request_data["resolution"] = resolution
|
|
146
|
+
if ratio is not None:
|
|
147
|
+
request_data["ratio"] = ratio
|
|
148
|
+
if duration is not None:
|
|
149
|
+
if duration < 4 or duration > 12:
|
|
150
|
+
# 兜底策略
|
|
151
|
+
duration = -1
|
|
152
|
+
request_data["duration"] = duration
|
|
153
|
+
if watermark is not None:
|
|
154
|
+
request_data["watermark"] = watermark
|
|
155
|
+
if seed is not None:
|
|
156
|
+
request_data["seed"] = seed
|
|
157
|
+
if camerafixed is not None:
|
|
158
|
+
request_data["camerafixed"] = camerafixed
|
|
159
|
+
if generate_audio is not None:
|
|
160
|
+
request_data["generate_audio"] = generate_audio
|
|
161
|
+
|
|
162
|
+
task_id = None
|
|
163
|
+
retry_count = 0
|
|
164
|
+
max_retries = 3
|
|
165
|
+
|
|
166
|
+
while retry_count < max_retries:
|
|
167
|
+
try:
|
|
168
|
+
response = self._request(
|
|
169
|
+
method="POST",
|
|
170
|
+
url=f"{self.base_url}/api/v3/contents/generations/tasks",
|
|
171
|
+
json=request_data,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
task_id = response.get("id")
|
|
175
|
+
if not task_id:
|
|
176
|
+
raise APIError("创建视频生成任务失败:响应中缺少任务ID")
|
|
177
|
+
|
|
178
|
+
logger.info(f"视频生成任务创建成功,任务ID: {task_id}")
|
|
179
|
+
break
|
|
180
|
+
|
|
181
|
+
except APIError as e:
|
|
182
|
+
retry_count += 1
|
|
183
|
+
error_msg = str(e)
|
|
184
|
+
logger.error(
|
|
185
|
+
f"创建视频生成任务失败(尝试 {retry_count}/{max_retries}): {error_msg}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if retry_count >= max_retries:
|
|
189
|
+
raise APIError(
|
|
190
|
+
f"创建视频生成任务失败,已重试{max_retries}次: {error_msg}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if "rate limit" in error_msg.lower() or "429" in error_msg:
|
|
194
|
+
wait_time = min(2**retry_count, 10)
|
|
195
|
+
logger.warning(f"遇到速率限制,等待 {wait_time} 秒后重试...")
|
|
196
|
+
time.sleep(wait_time)
|
|
197
|
+
elif "timeout" in error_msg.lower():
|
|
198
|
+
logger.warning(f"请求超时,等待 2 秒后重试...")
|
|
199
|
+
time.sleep(2)
|
|
200
|
+
else:
|
|
201
|
+
raise
|
|
202
|
+
|
|
203
|
+
start_time = time.time()
|
|
204
|
+
poll_count = 0
|
|
205
|
+
|
|
206
|
+
while time.time() - start_time < max_wait_time:
|
|
207
|
+
poll_count += 1
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
response = self._request(
|
|
211
|
+
method="GET",
|
|
212
|
+
url=f"{self.base_url}/api/v3/contents/generations/tasks/{task_id}",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
status = response.get("status")
|
|
216
|
+
logger.debug(f"任务 {task_id} 状态检查 #{poll_count}: {status}")
|
|
217
|
+
|
|
218
|
+
if status == "succeeded":
|
|
219
|
+
video_url = response.get("content", {}).get("video_url")
|
|
220
|
+
last_frame_url = response.get("content", {}).get(
|
|
221
|
+
"last_frame_url", ""
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if not video_url:
|
|
225
|
+
raise APIError(
|
|
226
|
+
f"视频生成成功但响应中缺少视频URL,任务ID: {task_id}"
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
logger.info(
|
|
230
|
+
f"视频生成成功,任务ID: {task_id}, 视频URL: {video_url}"
|
|
231
|
+
)
|
|
232
|
+
return video_url, response, last_frame_url
|
|
233
|
+
|
|
234
|
+
elif status == "failed":
|
|
235
|
+
error_message = response.get("error_message", "未知错误")
|
|
236
|
+
logger.error(
|
|
237
|
+
f"视频生成失败,任务ID: {task_id}, 错误: {error_message}"
|
|
238
|
+
)
|
|
239
|
+
raise APIError(f"视频生成失败: {error_message}")
|
|
240
|
+
|
|
241
|
+
elif status == "cancelled":
|
|
242
|
+
logger.warning(f"视频生成任务被取消,任务ID: {task_id}")
|
|
243
|
+
return None, response, ""
|
|
244
|
+
|
|
245
|
+
elif status in ["queued", "running"]:
|
|
246
|
+
time.sleep(poll_interval)
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
else:
|
|
250
|
+
logger.warning(f"未知的任务状态: {status},任务ID: {task_id}")
|
|
251
|
+
time.sleep(poll_interval)
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
except APIError as e:
|
|
255
|
+
error_msg = str(e)
|
|
256
|
+
logger.error(f"查询任务状态失败,任务ID: {task_id}, 错误: {error_msg}")
|
|
257
|
+
|
|
258
|
+
if "not found" in error_msg.lower() or "404" in error_msg:
|
|
259
|
+
raise APIError(f"任务不存在或已过期,任务ID: {task_id}")
|
|
260
|
+
|
|
261
|
+
if time.time() - start_time >= max_wait_time:
|
|
262
|
+
raise
|
|
263
|
+
|
|
264
|
+
time.sleep(poll_interval)
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
elapsed_time = int(time.time() - start_time)
|
|
268
|
+
logger.error(f"视频生成超时,任务ID: {task_id}, 已等待: {elapsed_time}秒")
|
|
269
|
+
raise APIError(f"视频生成超时,已等待 {elapsed_time} 秒,任务ID: {task_id}")
|
|
270
|
+
|
|
271
|
+
async def video_generation_async(
|
|
272
|
+
self,
|
|
273
|
+
content_items: List[Union[TextContent, ImageURLContent]],
|
|
274
|
+
callback_url: Optional[str] = None,
|
|
275
|
+
return_last_frame: Optional[bool] = False,
|
|
276
|
+
model: str = "doubao-seedance-1-5-pro-251215",
|
|
277
|
+
max_wait_time: int = 900,
|
|
278
|
+
resolution: Optional[str] = "720p",
|
|
279
|
+
ratio: Optional[str] = "16:9",
|
|
280
|
+
duration: Optional[int] = 5,
|
|
281
|
+
watermark: Optional[bool] = True,
|
|
282
|
+
seed: Optional[int] = None,
|
|
283
|
+
camerafixed: Optional[bool] = False,
|
|
284
|
+
generate_audio: Optional[bool] = True,
|
|
285
|
+
) -> Tuple[Optional[str], Dict, str]:
|
|
286
|
+
"""
|
|
287
|
+
视频生成函数(异步)。调用生视频模型,非大语言模型,仅用于生成视频
|
|
288
|
+
|
|
289
|
+
适用于批量生成视频的场景,可以并发执行多个视频生成任务。
|
|
290
|
+
|
|
291
|
+
Example:
|
|
292
|
+
```python
|
|
293
|
+
import asyncio
|
|
294
|
+
|
|
295
|
+
async def batch_generate():
|
|
296
|
+
prompts = ["小猫玩球", "日落海滩", "城市夜景"]
|
|
297
|
+
tasks = [
|
|
298
|
+
client.video_generation_async(
|
|
299
|
+
content_items=[TextContent(text=prompt)]
|
|
300
|
+
)
|
|
301
|
+
for prompt in prompts
|
|
302
|
+
]
|
|
303
|
+
results = await asyncio.gather(*tasks)
|
|
304
|
+
return results
|
|
305
|
+
|
|
306
|
+
results = asyncio.run(batch_generate())
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
content_items: 输入给模型,生成视频的信息,支持文本信息和图片信息。
|
|
311
|
+
callback_url: 可选,填写本次生成任务结果的回调通知地址。
|
|
312
|
+
return_last_frame: 可选,返回生成视频的尾帧图像。
|
|
313
|
+
model: 模型名称,默认使用 doubao-seedance-1-5-pro-251215
|
|
314
|
+
max_wait_time: 最大等待时间(秒),默认 900 秒
|
|
315
|
+
resolution: 视频分辨率,可选 "480p" 或 "720p",默认 "720p"
|
|
316
|
+
ratio: 视频比例,可选 "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "adaptive",默认 "16:9"
|
|
317
|
+
duration: 视频时长(秒),范围 -1 到 12,默认 5
|
|
318
|
+
watermark: 是否添加水印,默认 True
|
|
319
|
+
seed: 随机种子,用于复现生成结果
|
|
320
|
+
camerafixed: 是否固定摄像机位置,默认 False
|
|
321
|
+
generate_audio: 是否生成音频,默认 True
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Tuple[Optional[str], Dict, str]: (视频URL, 完整响应数据字典, 尾帧图像URL)
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
APIError: 当视频生成失败、超时或出现其他错误时
|
|
328
|
+
"""
|
|
329
|
+
loop = asyncio.get_event_loop()
|
|
330
|
+
result = await loop.run_in_executor(
|
|
331
|
+
None,
|
|
332
|
+
lambda: self.video_generation(
|
|
333
|
+
content_items=content_items,
|
|
334
|
+
callback_url=callback_url,
|
|
335
|
+
return_last_frame=return_last_frame,
|
|
336
|
+
model=model,
|
|
337
|
+
max_wait_time=max_wait_time,
|
|
338
|
+
resolution=resolution,
|
|
339
|
+
ratio=ratio,
|
|
340
|
+
duration=duration,
|
|
341
|
+
watermark=watermark,
|
|
342
|
+
seed=seed,
|
|
343
|
+
camerafixed=camerafixed,
|
|
344
|
+
generate_audio=generate_audio,
|
|
345
|
+
),
|
|
346
|
+
)
|
|
347
|
+
return result
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import List, Literal, Optional, Union
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ImageURL(BaseModel):
|
|
7
|
+
url: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ImageURLContent(BaseModel):
|
|
11
|
+
type: Literal["image_url"] = "image_url"
|
|
12
|
+
image_url: ImageURL
|
|
13
|
+
role: Optional[Literal["first_frame", "last_frame", "reference_image"]] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TextContent(BaseModel):
|
|
17
|
+
type: Literal["text"] = "text"
|
|
18
|
+
text: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VideoGenerationRequest(BaseModel):
|
|
22
|
+
model: str
|
|
23
|
+
content: List[Union[TextContent, ImageURLContent]]
|
|
24
|
+
resolution: Optional[Literal["480p", "720p", "1080p"]] = "720p"
|
|
25
|
+
ratio: Optional[
|
|
26
|
+
Literal["16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "adaptive"]
|
|
27
|
+
] = "16:9"
|
|
28
|
+
duration: Optional[int] = Field(default=5, ge=-1, le=12)
|
|
29
|
+
watermark: Optional[bool] = True
|
|
30
|
+
seed: Optional[int] = None
|
|
31
|
+
camerafixed: Optional[bool] = False
|
|
32
|
+
generate_audio: Optional[bool] = True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VideoGenerationTask(BaseModel):
|
|
36
|
+
id: str
|
|
37
|
+
status: Literal["processing", "completed", "failed"]
|
|
38
|
+
video_url: Optional[str] = None
|
|
39
|
+
error_message: Optional[str] = None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .frame_extractor import FrameExtractorClient
|
|
2
|
+
from .video_edit import VideoEditClient
|
|
3
|
+
from .models import (
|
|
4
|
+
FrameExtractorResponse,
|
|
5
|
+
FrameChunk,
|
|
6
|
+
VideoEditResponse,
|
|
7
|
+
SubtitleConfig,
|
|
8
|
+
FontPosConfig,
|
|
9
|
+
TextItem,
|
|
10
|
+
OutputSync,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FrameExtractorClient",
|
|
15
|
+
"VideoEditClient",
|
|
16
|
+
"FrameExtractorResponse",
|
|
17
|
+
"FrameChunk",
|
|
18
|
+
"VideoEditResponse",
|
|
19
|
+
"SubtitleConfig",
|
|
20
|
+
"FontPosConfig",
|
|
21
|
+
"TextItem",
|
|
22
|
+
"OutputSync",
|
|
23
|
+
]
|