doubao-image-server 1.0.0__tar.gz

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.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: doubao-image-server
3
+ Version: 1.0.0
4
+ Summary: 基于火山引擎豆包 Seedream 的图片/视频生成 MCP 服务器
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: mcp>=1.0.0
7
+ Requires-Dist: httpx
8
+ Requires-Dist: Pillow
9
+ Requires-Dist: openai
10
+ Dynamic: requires-python
@@ -0,0 +1,21 @@
1
+ # doubao-image-server
2
+
3
+ 基于火山引擎豆包 Seedream 的图片/视频生成 MCP 服务器。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pipx run doubao-image-server
9
+ ```
10
+
11
+ ## 配置
12
+
13
+ 环境变量 `DOUBAO_API_KEY` 或启动后调用 `set_api_key`。
14
+
15
+ ## 工具
16
+
17
+ - `set_api_key` — 设置 API Key
18
+ - `text_to_image` — 文生图
19
+ - `image_to_image` — 图生图
20
+ - `text_to_video` — 文生视频
21
+ - `image_to_video` — 图生视频
@@ -0,0 +1,17 @@
1
+ [project]
2
+ name = "doubao-image-server"
3
+ version = "1.0.0"
4
+ description = "基于火山引擎豆包 Seedream 的图片/视频生成 MCP 服务器"
5
+ requires-python = ">=3.10"
6
+ dependencies = [
7
+ "mcp>=1.0.0",
8
+ "httpx",
9
+ "Pillow",
10
+ "openai",
11
+ ]
12
+
13
+ [project.scripts]
14
+ doubao-image-server = "doubao_image_server:cli"
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ from setuptools import setup, find_packages
2
+ setup(
3
+ name="doubao-image-server",
4
+ version="1.0.0",
5
+ packages=find_packages(where="src"),
6
+ package_dir={"": "src"},
7
+ install_requires=["mcp>=1.0.0", "httpx", "Pillow", "openai"],
8
+ entry_points={"console_scripts": ["doubao-image-server=doubao_image_server:cli"]},
9
+ python_requires=">=3.10",
10
+ )
@@ -0,0 +1,303 @@
1
+ """
2
+ 豆包 MCP Server — 文生图 · 图生图 · 文生视频 · 图生视频
3
+ 模型:Seedream 4.5 / Seedance 2.0
4
+ 协议:FastMCP + stdio
5
+
6
+ ✅ 图片:返回缩略图预览(768px JPEG)+ 独立 HD 链接工具(本 AI 替你同时调,一次全出)
7
+ ✅ 视频:保持原样
8
+ """
9
+
10
+ import os
11
+ import time
12
+ import io
13
+ import httpx
14
+ from PIL import Image as PILImage
15
+ from typing import Any, Dict
16
+ from openai import OpenAI
17
+ from mcp.server.fastmcp import FastMCP, Image
18
+
19
+ _DEFAULT_API_KEY = os.environ.get("DOUBAO_API_KEY", "")
20
+
21
+ mcp = FastMCP("doubao-image-gen")
22
+ _client: OpenAI | None = None
23
+
24
+ # ── 缓存最近一次图片的原图 URL ──
25
+ _last_ttx_url: str | None = None # 文生图
26
+ _last_itx_url: str | None = None # 图生图
27
+
28
+ PREVIEW_MAX_SIDE = 768
29
+ PREVIEW_QUALITY = 75
30
+
31
+
32
+ def _init_client():
33
+ global _client
34
+ if _client is None:
35
+ _client = OpenAI(
36
+ base_url="https://ark.cn-beijing.volces.com/api/v3",
37
+ api_key=_DEFAULT_API_KEY,
38
+ )
39
+
40
+
41
+ def get_client() -> OpenAI:
42
+ if _client is None:
43
+ _init_client()
44
+ return _client
45
+
46
+
47
+ def _download_and_compress(url: str) -> bytes:
48
+ """下载原图 → 压缩为 768px JPEG(~8K token)"""
49
+ resp = httpx.get(url, follow_redirects=True, timeout=30)
50
+ resp.raise_for_status()
51
+
52
+ img = PILImage.open(io.BytesIO(resp.content))
53
+ if img.mode in ("RGBA", "P"):
54
+ img = img.convert("RGB")
55
+
56
+ w, h = img.size
57
+ if w > PREVIEW_MAX_SIDE or h > PREVIEW_MAX_SIDE:
58
+ ratio = min(PREVIEW_MAX_SIDE / w, PREVIEW_MAX_SIDE / h)
59
+ img = img.resize((int(w * ratio), int(h * ratio)), PILImage.LANCZOS)
60
+
61
+ buf = io.BytesIO()
62
+ img.save(buf, format="JPEG", quality=PREVIEW_QUALITY, optimize=True)
63
+ return buf.getvalue()
64
+
65
+
66
+ # ========== ① 设置 API Key ==========
67
+
68
+ @mcp.tool()
69
+ def set_api_key(api_key: str) -> str:
70
+ """设置火山引擎 API Key"""
71
+ global _client
72
+ _client = OpenAI(
73
+ base_url="https://ark.cn-beijing.volces.com/api/v3",
74
+ api_key=api_key,
75
+ )
76
+ return "✅ API Key 已更新"
77
+
78
+
79
+ # ========== ② 文生图(缩略图)==========
80
+
81
+ @mcp.tool()
82
+ def text_to_image(
83
+ prompt: str,
84
+ size: str = "2560x1440",
85
+ model: str = "doubao-seedream-4-5-251128",
86
+ ) -> Image:
87
+ """
88
+ 文生图 - 返回缩略图预览(768px JPEG,省 token)。
89
+ 我会同时自动调 text_to_image_hd_url 给你高清原图链接,无需你手动操作。
90
+ """
91
+ global _last_ttx_url
92
+
93
+ client = get_client()
94
+ resp = client.images.generate(
95
+ model=model,
96
+ prompt=prompt,
97
+ size=size,
98
+ response_format="url",
99
+ )
100
+ if not resp.data or not resp.data[0].url:
101
+ raise RuntimeError("未返回图片数据")
102
+
103
+ _last_ttx_url = resp.data[0].url
104
+ thumb = _download_and_compress(_last_ttx_url)
105
+
106
+ return Image(data=thumb, format="jpeg")
107
+
108
+
109
+ @mcp.tool()
110
+ def text_to_image_hd_url() -> str:
111
+ """获取最近一次文生图的高清原图链接(有时效,请尽快下载保存)"""
112
+ if not _last_ttx_url:
113
+ return "❌ 没有缓存的原图,请先调用 text_to_image 生成图片"
114
+ return f"📥 **高清原图链接**: {_last_ttx_url}"
115
+
116
+
117
+ # ========== ③ 图生图(缩略图)==========
118
+
119
+ @mcp.tool()
120
+ def image_to_image(
121
+ prompt: str,
122
+ image_url: str,
123
+ size: str = "2560x1440",
124
+ model: str = "doubao-seedream-4-5-251128",
125
+ ) -> Image:
126
+ """
127
+ 图生图 - 返回缩略图预览(768px JPEG,省 token)。
128
+ 我会同时自动调 image_to_image_hd_url 给你高清原图链接,无需你手动操作。
129
+ """
130
+ global _last_itx_url
131
+
132
+ client = get_client()
133
+ resp = client.images.generate(
134
+ model=model,
135
+ prompt=prompt,
136
+ image=image_url,
137
+ size=size,
138
+ response_format="url",
139
+ )
140
+ if not resp.data or not resp.data[0].url:
141
+ raise RuntimeError("未返回图片数据")
142
+
143
+ _last_itx_url = resp.data[0].url
144
+ thumb = _download_and_compress(_last_itx_url)
145
+
146
+ return Image(data=thumb, format="jpeg")
147
+
148
+
149
+ @mcp.tool()
150
+ def image_to_image_hd_url() -> str:
151
+ """获取最近一次图生图的高清原图链接(有时效,请尽快下载保存)"""
152
+ if not _last_itx_url:
153
+ return "❌ 没有缓存的原图,请先调用 image_to_image 生成图片"
154
+ return f"📥 **高清原图链接**: {_last_itx_url}"
155
+
156
+
157
+ # ========== ④ 文生视频(不动)==========
158
+
159
+ @mcp.tool()
160
+ def text_to_video(
161
+ prompt: str,
162
+ resolution: str = "720p",
163
+ ratio: str = "16:9",
164
+ model: str = "doubao-seedance-2-0-260128",
165
+ ) -> Dict[str, Any]:
166
+ """文生视频"""
167
+ try:
168
+ client = get_client()
169
+ if ratio and "--ratio" not in prompt:
170
+ prompt += f" --ratio {ratio}"
171
+ if resolution and "--resolution" not in prompt:
172
+ prompt += f" --resolution {resolution}"
173
+
174
+ task = client.content_generation.tasks.create(
175
+ model=model, content=[{"type": "text", "text": prompt}]
176
+ )
177
+
178
+ for _ in range(60):
179
+ time.sleep(5)
180
+ t = client.content_generation.tasks.get(task_id=task.id)
181
+ if t.status == "succeeded":
182
+ return {"success": True, "video_url": t.content.video_url}
183
+ elif t.status in ("failed", "canceled"):
184
+ return {"success": False, "error": f"任务 {t.status}"}
185
+ return {"success": False, "error": "超时"}
186
+ except Exception as e:
187
+ return {"success": False, "error": str(e)}
188
+
189
+
190
+ # ========== ⑤ 图生视频(不动)==========
191
+
192
+ @mcp.tool()
193
+ def image_to_video(
194
+ prompt: str,
195
+ image_url: str,
196
+ resolution: str = "720p",
197
+ ratio: str = "16:9",
198
+ model: str = "doubao-seedance-2-0-260128",
199
+ ) -> Dict[str, Any]:
200
+ """图生视频"""
201
+ try:
202
+ client = get_client()
203
+ if ratio and "--ratio" not in prompt:
204
+ prompt += f" --ratio {ratio}"
205
+ if resolution and "--resolution" not in prompt:
206
+ prompt += f" --resolution {resolution}"
207
+
208
+ task = client.content_generation.tasks.create(
209
+ model=model,
210
+ content=[
211
+ {"type": "text", "text": prompt},
212
+ {"type": "image_url", "image_url": {"url": image_url}},
213
+ ],
214
+ )
215
+
216
+ for _ in range(60):
217
+ time.sleep(5)
218
+ t = client.content_generation.tasks.get(task_id=task.id)
219
+ if t.status == "succeeded":
220
+ return {"success": True, "video_url": t.content.video_url}
221
+ elif t.status in ("failed", "canceled"):
222
+ return {"success": False, "error": f"任务 {t.status}"}
223
+ return {"success": False, "error": "超时"}
224
+ except Exception as e:
225
+ return {"success": False, "error": str(e)}
226
+
227
+
228
+ # ========== 启动 ==========
229
+
230
+ if __name__ == "__main__":
231
+ _init_client()
232
+ import sys
233
+ if "--http" in sys.argv:
234
+ from flask import Flask, request, jsonify
235
+ app = Flask(__name__)
236
+
237
+ @app.route("/generate", methods=["POST"])
238
+ def api_generate():
239
+ data = request.get_json(silent=True) or {}
240
+ prompt = data.get("prompt", "")
241
+ size = data.get("size", "2560x1440")
242
+ if not prompt:
243
+ return jsonify({"error": "缺少 prompt"}), 400
244
+ import httpx
245
+ client = get_client()
246
+ try:
247
+ resp = client.images.generate(
248
+ model="doubao-seedream-4-5-251128", prompt=prompt,
249
+ size=size, response_format="url",
250
+ )
251
+ if resp.data and resp.data[0].url:
252
+ return jsonify({"url": resp.data[0].url})
253
+ return jsonify({"error": "未返回图片"}), 500
254
+ except Exception as e:
255
+ return jsonify({"error": str(e)}), 500
256
+
257
+ @app.route("/edit", methods=["POST"])
258
+ def api_edit():
259
+ data = request.get_json(silent=True) or {}
260
+ prompt = data.get("prompt", "")
261
+ image_url = data.get("image_url", "")
262
+ size = data.get("size", "2560x1440")
263
+ if not prompt or not image_url:
264
+ return jsonify({"error": "缺少 prompt 或 image_url"}), 400
265
+ client = get_client()
266
+ try:
267
+ resp = client.images.generate(
268
+ model="doubao-seedream-4-5-251128", prompt=prompt,
269
+ image=image_url, size=size, response_format="url",
270
+ )
271
+ if resp.data and resp.data[0].url:
272
+ return jsonify({"url": resp.data[0].url})
273
+ return jsonify({"error": "未返回图片"}), 500
274
+ except Exception as e:
275
+ return jsonify({"error": str(e)}), 500
276
+
277
+ import __main__ as main_mod
278
+ @app.route("/download")
279
+ def download_script():
280
+ return __import__("flask").send_file(main_mod.__file__, as_attachment=True, download_name="doubao_image_server.py")
281
+
282
+ @app.route("/")
283
+ def index():
284
+ return jsonify({"status": "ok", "endpoints": ["/generate", "/edit", "/download"]})
285
+
286
+ port = int(os.environ.get("PORT", 8000))
287
+ app.run(host="0.0.0.0", port=port, threaded=True)
288
+ elif "--sse" in sys.argv:
289
+ mcp.run(transport="sse")
290
+ else:
291
+ mcp.run(transport="stdio")
292
+
293
+
294
+ def cli():
295
+ """pipx/pip 命令行入口"""
296
+ import sys
297
+ sys.argv = sys.argv
298
+ if "--http" in sys.argv:
299
+ _start_http()
300
+ elif "--sse" in sys.argv:
301
+ mcp.run(transport="sse")
302
+ else:
303
+ mcp.run(transport="stdio")
@@ -0,0 +1,3 @@
1
+ """pipx entry point"""
2
+ from doubao_image_server import cli
3
+ cli()
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: doubao-image-server
3
+ Version: 1.0.0
4
+ Summary: 基于火山引擎豆包 Seedream 的图片/视频生成 MCP 服务器
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: mcp>=1.0.0
7
+ Requires-Dist: httpx
8
+ Requires-Dist: Pillow
9
+ Requires-Dist: openai
10
+ Dynamic: requires-python
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ src/doubao_image_server/__init__.py
5
+ src/doubao_image_server/__main__.py
6
+ src/doubao_image_server.egg-info/PKG-INFO
7
+ src/doubao_image_server.egg-info/SOURCES.txt
8
+ src/doubao_image_server.egg-info/dependency_links.txt
9
+ src/doubao_image_server.egg-info/entry_points.txt
10
+ src/doubao_image_server.egg-info/requires.txt
11
+ src/doubao_image_server.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ doubao-image-server = doubao_image_server:cli
@@ -0,0 +1,4 @@
1
+ mcp>=1.0.0
2
+ httpx
3
+ Pillow
4
+ openai
@@ -0,0 +1 @@
1
+ doubao_image_server