imgenx-mcp 0.4.0__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.
- imgenx/factory.py +50 -0
- imgenx/main.py +54 -0
- imgenx/operator.py +151 -0
- imgenx/oss_service.py +198 -0
- imgenx/predictor/base/base_image_analyzer.py +13 -0
- imgenx/predictor/base/base_image_generator.py +17 -0
- imgenx/predictor/base/base_video_generator.py +18 -0
- imgenx/predictor/generators/doubao_image_analyzer.py +65 -0
- imgenx/predictor/generators/doubao_image_generator.py +77 -0
- imgenx/predictor/generators/doubao_video_generator.py +122 -0
- imgenx/script.py +103 -0
- imgenx/server.py +378 -0
- imgenx_mcp-0.4.0.dist-info/METADATA +392 -0
- imgenx_mcp-0.4.0.dist-info/RECORD +17 -0
- imgenx_mcp-0.4.0.dist-info/WHEEL +4 -0
- imgenx_mcp-0.4.0.dist-info/entry_points.txt +2 -0
- imgenx_mcp-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
sys.path.insert(0, '../../..')
|
|
3
|
+
import base64
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict
|
|
7
|
+
from volcenginesdkarkruntime import Ark
|
|
8
|
+
|
|
9
|
+
from imgenx.predictor.base.base_video_generator import BaseVideoGenerator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DoubaoVideoGenerator(BaseVideoGenerator):
|
|
13
|
+
|
|
14
|
+
def __init__(self, model: str, api_key: str):
|
|
15
|
+
self.model = model
|
|
16
|
+
self.client = Ark(
|
|
17
|
+
base_url='https://ark.cn-beijing.volces.com/api/v3',
|
|
18
|
+
api_key=api_key,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def text_to_video(self, prompt: str, resolution: str = '720p', ratio: str = '16:9', duration: int = 5) -> str:
|
|
22
|
+
create_result = self.client.content_generation.tasks.create(
|
|
23
|
+
model=self.model,
|
|
24
|
+
content=[
|
|
25
|
+
{
|
|
26
|
+
'type': 'text',
|
|
27
|
+
'text': prompt.strip()
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
extra_body={
|
|
31
|
+
'resolution': resolution,
|
|
32
|
+
'ratio': ratio,
|
|
33
|
+
'duration': duration,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
task_id = create_result.id
|
|
37
|
+
|
|
38
|
+
while True:
|
|
39
|
+
get_result = self.client.content_generation.tasks.get(task_id=task_id)
|
|
40
|
+
status = get_result.status
|
|
41
|
+
if status == "succeeded":
|
|
42
|
+
return get_result.content.video_url
|
|
43
|
+
elif status == "failed":
|
|
44
|
+
raise Exception(str(get_result.error))
|
|
45
|
+
else:
|
|
46
|
+
time.sleep(3)
|
|
47
|
+
|
|
48
|
+
def image_to_video(self, prompt: str, first_frame: str, last_frame: str|None = None,
|
|
49
|
+
resolution: str = '720p', ratio: str = '16:9', duration: int = 5) -> str:
|
|
50
|
+
if not first_frame.startswith('http'):
|
|
51
|
+
first_frame = self._image_to_base64(first_frame)
|
|
52
|
+
|
|
53
|
+
if last_frame is not None and not last_frame.startswith('http'):
|
|
54
|
+
last_frame = self._image_to_base64(last_frame)
|
|
55
|
+
|
|
56
|
+
content = [
|
|
57
|
+
{
|
|
58
|
+
'type': 'text',
|
|
59
|
+
'text': prompt.strip()
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
'type': 'image_url',
|
|
63
|
+
'image_url': {
|
|
64
|
+
'url': first_frame
|
|
65
|
+
},
|
|
66
|
+
'role': 'first_frame'
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
if last_frame is not None:
|
|
71
|
+
content.append({
|
|
72
|
+
'type': 'image_url',
|
|
73
|
+
'image_url': {
|
|
74
|
+
'url': last_frame
|
|
75
|
+
},
|
|
76
|
+
'role': 'last_frame'
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
create_result = self.client.content_generation.tasks.create(
|
|
80
|
+
model=self.model,
|
|
81
|
+
content=content,
|
|
82
|
+
extra_body={
|
|
83
|
+
'resolution': resolution,
|
|
84
|
+
'ratio': ratio,
|
|
85
|
+
'duration': duration,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
task_id = create_result.id
|
|
89
|
+
|
|
90
|
+
while True:
|
|
91
|
+
get_result = self.client.content_generation.tasks.get(task_id=task_id)
|
|
92
|
+
status = get_result.status
|
|
93
|
+
if status == "succeeded":
|
|
94
|
+
return get_result.content.video_url
|
|
95
|
+
elif status == "failed":
|
|
96
|
+
raise Exception(str(get_result.error))
|
|
97
|
+
else:
|
|
98
|
+
time.sleep(3)
|
|
99
|
+
|
|
100
|
+
def _image_to_base64(self, image_path: str) -> str:
|
|
101
|
+
image_path = Path(image_path)
|
|
102
|
+
|
|
103
|
+
with open(image_path, 'rb') as image_file:
|
|
104
|
+
base64_image = base64.b64encode(image_file.read()).decode('utf-8')
|
|
105
|
+
base64_image = f'data:image/{image_path.suffix.strip(".")};base64,{base64_image}'
|
|
106
|
+
|
|
107
|
+
return base64_image
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == '__main__':
|
|
111
|
+
import os
|
|
112
|
+
from dotenv import load_dotenv
|
|
113
|
+
|
|
114
|
+
load_dotenv()
|
|
115
|
+
|
|
116
|
+
api_key = os.getenv('IMGENX_API_KEY')
|
|
117
|
+
model = 'doubao-seedance-1-0-pro-fast-251015'
|
|
118
|
+
|
|
119
|
+
generator = DoubaoVideoGenerator(model, api_key)
|
|
120
|
+
image = '/Volumes/DATA/个人/project/imgenx-mcp-server/logo.jpg'
|
|
121
|
+
result = generator.image_to_video('一个人在运动', resolution='720p', ratio='16:9', first_frame=image, duration=5)
|
|
122
|
+
print(result)
|
imgenx/script.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List, Dict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
|
|
9
|
+
from imgenx import factory
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def text_to_image(model: str, api_key: str, prompt: str, size: str) -> List[Dict[str, str]]:
|
|
13
|
+
generator = factory.create_image_generator(model, api_key)
|
|
14
|
+
url_list = generator.text_to_image(prompt, size)
|
|
15
|
+
return url_list
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def image_to_image(model: str, api_key: str, prompt: str, images: List[str], size: str) -> List[Dict[str, str]]:
|
|
19
|
+
generator = factory.create_image_generator(model, api_key)
|
|
20
|
+
url_list = generator.image_to_image(prompt, images, size)
|
|
21
|
+
return url_list
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def text_to_video(model: str, api_key: str, prompt: str,
|
|
25
|
+
resolution: str = '720p', ratio: str = '16:9', duration: int = 5) -> str:
|
|
26
|
+
generator = factory.create_video_generator(model, api_key)
|
|
27
|
+
url = generator.text_to_video(prompt, resolution, ratio, duration)
|
|
28
|
+
return url
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def image_to_video(model: str, api_key: str, prompt: str, first_frame: str, last_frame: str|None = None,
|
|
32
|
+
resolution: str = '720p', ratio: str = '16:9', duration: int = 5) -> str:
|
|
33
|
+
generator = factory.create_video_generator(model, api_key)
|
|
34
|
+
url = generator.image_to_video(prompt, first_frame, last_frame, resolution, ratio, duration)
|
|
35
|
+
return url
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def gen_image(prompt: str, size: str, output: str, images: List[str] = None):
|
|
39
|
+
print('Generate images...')
|
|
40
|
+
|
|
41
|
+
load_dotenv()
|
|
42
|
+
model = os.getenv('IMGENX_IMAGE_MODEL')
|
|
43
|
+
api_key = os.getenv('IMGENX_API_KEY')
|
|
44
|
+
|
|
45
|
+
if model is None:
|
|
46
|
+
raise ValueError('Envrioment variable IMGENX_IMAGE_MODEL is empty.')
|
|
47
|
+
|
|
48
|
+
if api_key is None:
|
|
49
|
+
raise ValueError('Envrioment variable IMGENX_API_KEY is empty.')
|
|
50
|
+
|
|
51
|
+
output = Path(output)
|
|
52
|
+
|
|
53
|
+
if output.exists() and output.is_file():
|
|
54
|
+
raise ValueError(f'Output path {output} already exists.')
|
|
55
|
+
|
|
56
|
+
if images is not None and len(images) > 0:
|
|
57
|
+
url_list = image_to_image(model, api_key, prompt, images, size)
|
|
58
|
+
else:
|
|
59
|
+
url_list = text_to_image(model, api_key, prompt, size)
|
|
60
|
+
|
|
61
|
+
if output.is_dir():
|
|
62
|
+
path_list = [f'{output}/{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}_{i + 1}.png' for i in range(len(url_list))]
|
|
63
|
+
elif len(url_list) == 1:
|
|
64
|
+
path_list = [output]
|
|
65
|
+
else:
|
|
66
|
+
path_list = [f'{output.parent}/{output.stem}_{i + 1}{output.suffix if output.suffix else ".jpg"}' for i in range(len(url_list))]
|
|
67
|
+
|
|
68
|
+
for url_item, path in zip(url_list, path_list):
|
|
69
|
+
response = requests.get(url_item['url'])
|
|
70
|
+
Path(path).write_bytes(response.content)
|
|
71
|
+
print(f'Save image to {path}')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def gen_video(prompt: str, first_frame: str|None = None, last_frame: str|None = None,
|
|
75
|
+
resolution: str = '720p', ratio: str = '16:9', duration: int = 5, output: str = None):
|
|
76
|
+
print('Generate video...')
|
|
77
|
+
|
|
78
|
+
load_dotenv()
|
|
79
|
+
model = os.getenv('IMGENX_VIDEO_MODEL')
|
|
80
|
+
api_key = os.getenv('IMGENX_API_KEY')
|
|
81
|
+
|
|
82
|
+
if model is None:
|
|
83
|
+
raise ValueError('Envrioment variable IMGENX_VIDEO_MODEL is empty.')
|
|
84
|
+
|
|
85
|
+
if api_key is None:
|
|
86
|
+
raise ValueError('Envrioment variable IMGENX_API_KEY is empty.')
|
|
87
|
+
|
|
88
|
+
if output is None:
|
|
89
|
+
output = f'{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}.mp4'
|
|
90
|
+
else:
|
|
91
|
+
output = Path(output).with_name(f'{Path(output).stem}_{datetime.now().strftime("%Y-%m-%d_%H:%M:%S")}{Path(output).suffix}')
|
|
92
|
+
|
|
93
|
+
if output.exists() and output.is_file():
|
|
94
|
+
raise ValueError(f'Output path {output} already exists.')
|
|
95
|
+
|
|
96
|
+
if first_frame is None and last_frame is None:
|
|
97
|
+
url = text_to_video(model, api_key, prompt, resolution, ratio, duration)
|
|
98
|
+
else:
|
|
99
|
+
url = image_to_video(model, api_key, prompt, first_frame, last_frame, resolution, ratio, duration)
|
|
100
|
+
|
|
101
|
+
response = requests.get(url)
|
|
102
|
+
Path(output).write_bytes(response.content)
|
|
103
|
+
print(f'Save video to {output}')
|
imgenx/server.py
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Dict, Tuple
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
from fastmcp import FastMCP
|
|
10
|
+
from fastmcp.exceptions import ToolError
|
|
11
|
+
from fastmcp.server.dependencies import get_http_headers
|
|
12
|
+
|
|
13
|
+
from imgenx import factory
|
|
14
|
+
from imgenx import operator
|
|
15
|
+
from imgenx.oss_service import get_oss_service
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
load_dotenv()
|
|
19
|
+
|
|
20
|
+
mcp = FastMCP(
|
|
21
|
+
name='imgenx-mcp-server',
|
|
22
|
+
instructions='图片视频生成工具,自动上传到 OSS 并返回永久 CDN URL',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _upload_url_to_oss(url: str, business_dir: str = 'images') -> Dict[str, str]:
|
|
27
|
+
"""内部函数:下载 URL 并上传到 OSS"""
|
|
28
|
+
try:
|
|
29
|
+
# 下载文件
|
|
30
|
+
response = requests.get(url, timeout=60)
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
|
|
33
|
+
# 从 URL 提取文件扩展名
|
|
34
|
+
path = urlparse(url).path
|
|
35
|
+
ext = Path(path).suffix or '.jpg'
|
|
36
|
+
filename = f'generated{ext}'
|
|
37
|
+
|
|
38
|
+
# 上传到 OSS
|
|
39
|
+
oss_service = get_oss_service()
|
|
40
|
+
result = oss_service.upload_bytes(response.content, filename, business_dir=business_dir)
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise ToolError(f'上传到 OSS 失败: {e}')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@mcp.tool
|
|
48
|
+
def text_to_image(prompt: str, size: str = '2K') -> List[Dict[str, str]]:
|
|
49
|
+
'''根据输入的提示词生成图片,自动上传到 OSS 并返回永久 CDN URL。
|
|
50
|
+
确保用 Markdown 格式输出图片,例如:
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
prompt (str): 生成图片的提示词
|
|
54
|
+
size (str): 生成图像的分辨率或宽高像素值
|
|
55
|
+
分辨率可选值:'1K'、'2K', '4K'
|
|
56
|
+
宽高像素可选值:2048x2048、2304x1728、1728x2304、2560x1440、1440x2560、2496x1664、1664x2496、3024x1296
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
List[Dict[str, str]]: 包含 cdn_url(CDN地址)、oss_url(OSS地址)、object_key(存储路径)
|
|
60
|
+
'''
|
|
61
|
+
headers = get_http_headers(include_all=True)
|
|
62
|
+
model = headers.get('imgenx_image_model', os.getenv('IMGENX_IMAGE_MODEL'))
|
|
63
|
+
api_key = headers.get('imgenx_api_key', os.getenv('IMGENX_API_KEY'))
|
|
64
|
+
|
|
65
|
+
if model is None:
|
|
66
|
+
raise ToolError('IMGENX_IMAGE_MODEL is None')
|
|
67
|
+
|
|
68
|
+
if api_key is None:
|
|
69
|
+
raise ToolError('IMGENX_API_KEY is None')
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# 生成图片
|
|
73
|
+
generator = factory.create_image_generator(model, api_key)
|
|
74
|
+
temp_url_list = generator.text_to_image(prompt, size)
|
|
75
|
+
|
|
76
|
+
# 自动上传到 OSS
|
|
77
|
+
result_list = []
|
|
78
|
+
for item in temp_url_list:
|
|
79
|
+
temp_url = item.get('url')
|
|
80
|
+
oss_result = _upload_url_to_oss(temp_url, business_dir='images')
|
|
81
|
+
result_list.append(oss_result)
|
|
82
|
+
|
|
83
|
+
return result_list
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise ToolError(f'Error: {e}')
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp.tool
|
|
89
|
+
def image_to_image(prompt: str, images: List[str], size: str = '2K') -> List[Dict[str, str]]:
|
|
90
|
+
'''根据输入的提示词和图片生成新图片,自动上传到 OSS 并返回永久 CDN URL。
|
|
91
|
+
确保用 Markdown 格式输出图片,例如:
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
prompt (str): 生成图片的提示词
|
|
95
|
+
images (List[str]): 输入图片 url 列表或文件路径列表
|
|
96
|
+
size (str): 生成图像的分辨率或宽高像素值
|
|
97
|
+
分辨率可选值:'1K'、'2K', '4K'
|
|
98
|
+
宽高像素可选值:2048x2048、2304x1728、1728x2304、2560x1440、1440x2560、2496x1664、1664x2496、3024x1296
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List[Dict[str, str]]: 包含 cdn_url(CDN地址)、oss_url(OSS地址)、object_key(存储路径)
|
|
102
|
+
'''
|
|
103
|
+
headers = get_http_headers(include_all=True)
|
|
104
|
+
model = headers.get('imgenx_image_model', os.getenv('IMGENX_IMAGE_MODEL'))
|
|
105
|
+
api_key = headers.get('imgenx_api_key', os.getenv('IMGENX_API_KEY'))
|
|
106
|
+
|
|
107
|
+
if model is None:
|
|
108
|
+
raise ToolError('IMGENX_IMAGE_MODEL is None')
|
|
109
|
+
|
|
110
|
+
if api_key is None:
|
|
111
|
+
raise ToolError('IMGENX_API_KEY is None')
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
# 生成图片
|
|
115
|
+
generator = factory.create_image_generator(model, api_key)
|
|
116
|
+
temp_url_list = generator.image_to_image(prompt, images, size)
|
|
117
|
+
|
|
118
|
+
# 自动上传到 OSS
|
|
119
|
+
result_list = []
|
|
120
|
+
for item in temp_url_list:
|
|
121
|
+
temp_url = item.get('url')
|
|
122
|
+
oss_result = _upload_url_to_oss(temp_url, business_dir='images')
|
|
123
|
+
result_list.append(oss_result)
|
|
124
|
+
|
|
125
|
+
return result_list
|
|
126
|
+
except Exception as e:
|
|
127
|
+
raise ToolError(f'Error: {e}')
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@mcp.tool
|
|
131
|
+
def text_to_video(prompt: str, resolution: str = '720p', ratio: str = '16:9', duration: int = 5) -> Dict[str, str]:
|
|
132
|
+
'''根据输入的提示词生成视频,自动上传到 OSS 并返回永久 CDN URL。
|
|
133
|
+
确保用 Markdown 格式输出视频,例如:[title](cdn_url)
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
prompt (str): 生成视频的提示词
|
|
137
|
+
resolution (str): 生成视频的分辨率:480p、720p、1080p
|
|
138
|
+
ratio (str): 生成视频的比例:16:9、4:3、1:1、3:4、9:16、21:9
|
|
139
|
+
duration (int): 生成视频的时长,单位秒,支持 2~12 秒
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dict[str, str]: 包含 cdn_url(CDN地址)、oss_url(OSS地址)、object_key(存储路径)
|
|
143
|
+
'''
|
|
144
|
+
headers = get_http_headers(include_all=True)
|
|
145
|
+
model = headers.get('imgenx_video_model', os.getenv('IMGENX_VIDEO_MODEL'))
|
|
146
|
+
api_key = headers.get('imgenx_api_key', os.getenv('IMGENX_API_KEY'))
|
|
147
|
+
|
|
148
|
+
if model is None:
|
|
149
|
+
raise ToolError('IMGENX_VIDEO_MODEL is None')
|
|
150
|
+
|
|
151
|
+
if api_key is None:
|
|
152
|
+
raise ToolError('IMGENX_API_KEY is None')
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# 生成视频
|
|
156
|
+
generator = factory.create_video_generator(model, api_key)
|
|
157
|
+
temp_url = generator.text_to_video(prompt, resolution, ratio, duration)
|
|
158
|
+
|
|
159
|
+
# 自动上传到 OSS
|
|
160
|
+
result = _upload_url_to_oss(temp_url, business_dir='videos')
|
|
161
|
+
|
|
162
|
+
return result
|
|
163
|
+
except Exception as e:
|
|
164
|
+
raise ToolError(f'Error: {e}')
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@mcp.tool
|
|
168
|
+
def image_to_video(prompt: str, first_frame: str, last_frame: str|None = None,
|
|
169
|
+
resolution: str = '720p', ratio: str = '16:9', duration: int = 5) -> Dict[str, str]:
|
|
170
|
+
'''根据输入的提示词和视频首尾帧图片生成视频,自动上传到 OSS 并返回永久 CDN URL。
|
|
171
|
+
确保用 Markdown 格式输出视频,例如:[title](cdn_url)
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
prompt (str): 生成视频的提示词
|
|
175
|
+
first_frame (str): 视频的首帧图片 url 或文件路径
|
|
176
|
+
last_frame (str|None): 视频的尾图片 url 或文件路径,默认 None
|
|
177
|
+
resolution (str): 生成视频的分辨率:480p、720p、1080p
|
|
178
|
+
ratio (str): 生成视频的比例:16:9、4:3、1:1、3:4、9:16、21:9
|
|
179
|
+
duration (int): 生成视频的时长,单位秒,支持 2~12 秒
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Dict[str, str]: 包含 cdn_url(CDN地址)、oss_url(OSS地址)、object_key(存储路径)
|
|
183
|
+
'''
|
|
184
|
+
headers = get_http_headers(include_all=True)
|
|
185
|
+
model = headers.get('imgenx_video_model', os.getenv('IMGENX_VIDEO_MODEL'))
|
|
186
|
+
api_key = headers.get('imgenx_api_key', os.getenv('IMGENX_API_KEY'))
|
|
187
|
+
|
|
188
|
+
if model is None:
|
|
189
|
+
raise ToolError('IMGENX_VIDEO_MODEL is None')
|
|
190
|
+
|
|
191
|
+
if api_key is None:
|
|
192
|
+
raise ToolError('IMGENX_API_KEY is None')
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
# 生成视频
|
|
196
|
+
generator = factory.create_video_generator(model, api_key)
|
|
197
|
+
temp_url = generator.image_to_video(prompt, first_frame, last_frame, resolution, ratio, duration)
|
|
198
|
+
|
|
199
|
+
# 自动上传到 OSS
|
|
200
|
+
result = _upload_url_to_oss(temp_url, business_dir='videos')
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise ToolError(f'Error: {e}')
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@mcp.tool
|
|
208
|
+
def analyze_image(prompt: str, image: str) -> str:
|
|
209
|
+
'''分析图片获取精确的信息,确保用户需要分析、编辑、裁剪图片时先调用此工具。
|
|
210
|
+
确保尽量用精确数字描述图片信息。
|
|
211
|
+
输出图片裁剪区域时,确保给出精确**小数比例坐标**,坐标为左上角和右下角:x1(left), y1(upper), x2(right), y2(lower)
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
prompt (str): 分析图片的提示词
|
|
215
|
+
image (str): 图片路径或 URL
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
str: 图片分析结果
|
|
219
|
+
'''
|
|
220
|
+
headers = get_http_headers(include_all=True)
|
|
221
|
+
model = headers.get('imgenx_analyzer_model', os.getenv('IMGENX_ANALYZER_MODEL'))
|
|
222
|
+
api_key = headers.get('imgenx_api_key', os.getenv('IMGENX_API_KEY'))
|
|
223
|
+
|
|
224
|
+
if model is None:
|
|
225
|
+
raise ToolError('IMGENX_ANALYZER_MODEL is None')
|
|
226
|
+
|
|
227
|
+
if api_key is None:
|
|
228
|
+
raise ToolError('IMGENX_API_KEY is None')
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
info = operator.get_image_info(image)
|
|
232
|
+
prompt = f'image info: {info}\n\n{prompt}'
|
|
233
|
+
|
|
234
|
+
analyzer = factory.create_image_analyzer(model, api_key)
|
|
235
|
+
result = analyzer.analyze(prompt, image)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
raise ToolError(f'Error: {e}')
|
|
238
|
+
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@mcp.tool
|
|
243
|
+
def get_image_info(image: str) -> Dict[str, str]:
|
|
244
|
+
'''获取图片分辨率和类型信息,确保用户需要获取图片分辨率和类型信息时调用此工具。
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
image (str): 图片路径或 URL
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dict[str,str]: 分辨率和类型信息
|
|
251
|
+
'''
|
|
252
|
+
try:
|
|
253
|
+
info = operator.get_image_info(image)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
raise ToolError(f'Error: {e}')
|
|
256
|
+
|
|
257
|
+
return info
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@mcp.tool
|
|
261
|
+
def crop_image(image: str, box: str, output: str) -> Dict[str, str]:
|
|
262
|
+
'''框裁剪图片,确保用户需要裁剪图片时调用此工具。
|
|
263
|
+
Args:
|
|
264
|
+
image (str): 图片路径或 URL
|
|
265
|
+
box (str): 小数比例坐标,x1(left), y1(upper), x2(right), y2(lower)
|
|
266
|
+
output (str): 输出文件路径(后缀决定格式)
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Dict[str,str]: 生成图片的 path
|
|
270
|
+
'''
|
|
271
|
+
try:
|
|
272
|
+
operator.crop_image(image, box, output)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
raise ToolError(f'Error: {e}')
|
|
275
|
+
|
|
276
|
+
p = Path(output).resolve()
|
|
277
|
+
return {'title': p.name, 'path': str(p)}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@mcp.tool
|
|
281
|
+
def resize_image(image: str, size: str, output: str, keep_aspect: bool = True) -> Dict[str, str]:
|
|
282
|
+
'''调整图片尺寸,确保用户需要调整图片尺寸时调用此工具。
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
image (str): 图片路径或 URL
|
|
286
|
+
size (str): "WIDTHxHEIGHT"
|
|
287
|
+
output (str): 输出文件路径
|
|
288
|
+
keep_aspect (bool): 是否保持比例(True 为等比不超过目标尺寸)
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Dict[str,str]: 生成图片的 path
|
|
292
|
+
'''
|
|
293
|
+
try:
|
|
294
|
+
operator.resize_image(image, size, output, keep_aspect=keep_aspect)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
raise ToolError(f'Error: {e}')
|
|
297
|
+
|
|
298
|
+
p = Path(output).resolve()
|
|
299
|
+
return {'title': p.name, 'path': str(p)}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@mcp.tool
|
|
303
|
+
def convert_image(image: str, format: str, output: str, quality: int = 90) -> Dict[str, str]:
|
|
304
|
+
'''格式转换,确保用户需要转换图片格式时调用此工具。
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
image (str): 图片路径或 URL
|
|
308
|
+
format (str): 目标格式:PNG/JPEG/JPG/WEBP
|
|
309
|
+
output (str): 输出文件路径
|
|
310
|
+
quality (int): 压缩质量(针对有损格式)
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
Dict[str,str]: 生成图片的 path
|
|
314
|
+
'''
|
|
315
|
+
try:
|
|
316
|
+
operator.convert_image(image, format, output, quality=quality)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
raise ToolError(f'Error: {e}')
|
|
319
|
+
|
|
320
|
+
p = Path(output).resolve()
|
|
321
|
+
return {'title': p.name, 'path': str(p)}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
@mcp.tool
|
|
325
|
+
def adjust_image(image: str, output: str, brightness: float = 1.0, contrast: float = 1.0, saturation: float = 1.0) -> Dict[str, str]:
|
|
326
|
+
'''基础图像调整:亮度/对比度/饱和度,确保用户需要调整图片时调用此工具。
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
image (str): 图片路径或 URL
|
|
330
|
+
output (str): 输出文件路径
|
|
331
|
+
brightness (float): 亮度,默认 1.0
|
|
332
|
+
contrast (float): 对比度,默认 1.0
|
|
333
|
+
saturation (float): 饱和度,默认 1.0
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Dict[str,str]: 生成图片的 path
|
|
337
|
+
'''
|
|
338
|
+
try:
|
|
339
|
+
operator.adjust_image(image, output, brightness=brightness, contrast=contrast, saturation=saturation)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
raise ToolError(f'Error: {e}')
|
|
342
|
+
|
|
343
|
+
p = Path(output).resolve()
|
|
344
|
+
return {'title': p.name, 'path': str(p)}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@mcp.tool
|
|
348
|
+
def paste_image(front_image: str, background_image: str, output: str, position: Tuple[int, int]) -> Dict[str, str]:
|
|
349
|
+
'''将图片粘贴到背景图片上,确保用户需要粘贴图片时调用此工具。
|
|
350
|
+
粘贴图片前,需要用 `resize_image` 工具调整 `front_image` 到适合的尺寸。
|
|
351
|
+
调用 `analyze_image` 工具确定粘贴位置。
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
front_image (str): 图片路径或 URL
|
|
355
|
+
background_image (str): 背景图片路径或 URL
|
|
356
|
+
output (str): 输出文件路径
|
|
357
|
+
position (Tuple[int, int]): 粘贴位置的 (x, y),背景图片的左上角像素坐标
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Dict[str,str]: 生成图片的 path
|
|
361
|
+
'''
|
|
362
|
+
try:
|
|
363
|
+
operator.paste_image(front_image, background_image, position, output)
|
|
364
|
+
except Exception as e:
|
|
365
|
+
raise ToolError(f'Error: {e}')
|
|
366
|
+
|
|
367
|
+
p = Path(output).resolve()
|
|
368
|
+
return {'title': p.name, 'path': str(p)}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@mcp.custom_route('/health', methods=['GET'])
|
|
372
|
+
def health() -> str:
|
|
373
|
+
return 'success'
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@mcp.custom_route('/healthy', methods=['GET'])
|
|
377
|
+
def healthy() -> str:
|
|
378
|
+
return 'success'
|