feat: image-service 新增阿里云百炼文生图支持,添加配置敏感信息保护规则
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -10,8 +10,19 @@ npm-debug.log*
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# 忽略技能配置目录(所有 skills 的 config)
|
||||
.opencode/skills/*/config/
|
||||
|
||||
# 但保留 .example 模板文件
|
||||
!.opencode/skills/*/config/settings.json.example
|
||||
|
||||
# 允许 Git 追踪 .opencode 下的技能定义
|
||||
!.opencode/skills/
|
||||
|
||||
# 忽略临时文件目录
|
||||
temp/
|
||||
|
||||
# 忽略 Python 缓存
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
@@ -15,13 +15,33 @@ from typing import Dict, Any, Optional, Union
|
||||
from pathlib import Path
|
||||
|
||||
VALID_ASPECT_RATIOS = [
|
||||
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"
|
||||
"1:1",
|
||||
"2:3",
|
||||
"3:2",
|
||||
"3:4",
|
||||
"4:3",
|
||||
"4:5",
|
||||
"5:4",
|
||||
"9:16",
|
||||
"16:9",
|
||||
"21:9",
|
||||
]
|
||||
|
||||
VALID_SIZES = [
|
||||
"1024x1024",
|
||||
"1536x1024", "1792x1024", "1344x768", "1248x832", "1184x864", "1152x896", "1536x672",
|
||||
"1024x1536", "1024x1792", "768x1344", "832x1248", "864x1184", "896x1152"
|
||||
"1536x1024",
|
||||
"1792x1024",
|
||||
"1344x768",
|
||||
"1248x832",
|
||||
"1184x864",
|
||||
"1152x896",
|
||||
"1536x672",
|
||||
"1024x1536",
|
||||
"1024x1792",
|
||||
"768x1344",
|
||||
"832x1248",
|
||||
"864x1184",
|
||||
"896x1152",
|
||||
]
|
||||
|
||||
RATIO_TO_SIZE = {
|
||||
@@ -34,7 +54,7 @@ RATIO_TO_SIZE = {
|
||||
"5:4": "1184x864",
|
||||
"9:16": "1024x1792",
|
||||
"16:9": "1792x1024",
|
||||
"21:9": "1536x672"
|
||||
"21:9": "1536x672",
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +72,9 @@ class TextToImageGenerator:
|
||||
if config is None:
|
||||
config = self._load_config()
|
||||
|
||||
self.api_key = config.get('api_key') or config.get('IMAGE_API_KEY')
|
||||
self.base_url = config.get('base_url') or config.get('IMAGE_API_BASE_URL')
|
||||
self.model = config.get('model') or config.get('IMAGE_MODEL') or 'lyra-flash-9'
|
||||
self.api_key = config.get("api_key") or config.get("IMAGE_API_KEY")
|
||||
self.base_url = config.get("base_url") or config.get("IMAGE_API_BASE_URL")
|
||||
self.model = config.get("model") or config.get("IMAGE_MODEL") or "lyra-flash-9"
|
||||
|
||||
if not self.api_key or not self.base_url:
|
||||
raise ValueError("缺少必要的 API 配置:api_key 和 base_url")
|
||||
@@ -63,18 +83,18 @@ class TextToImageGenerator:
|
||||
"""从配置文件或环境变量加载配置"""
|
||||
config = {}
|
||||
|
||||
config_path = Path(__file__).parent.parent / 'config' / 'settings.json'
|
||||
config_path = Path(__file__).parent.parent / "config" / "settings.json"
|
||||
if config_path.exists():
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
settings = json.load(f)
|
||||
api_config = settings.get('image_api', {})
|
||||
config['api_key'] = api_config.get('key')
|
||||
config['base_url'] = api_config.get('base_url')
|
||||
config['model'] = api_config.get('model')
|
||||
api_config = settings.get("image_api", {})
|
||||
config["api_key"] = api_config.get("key")
|
||||
config["base_url"] = api_config.get("base_url")
|
||||
config["model"] = api_config.get("model")
|
||||
|
||||
config['api_key'] = os.getenv('IMAGE_API_KEY', config.get('api_key'))
|
||||
config['base_url'] = os.getenv('IMAGE_API_BASE_URL', config.get('base_url'))
|
||||
config['model'] = os.getenv('IMAGE_MODEL', config.get('model'))
|
||||
config["api_key"] = os.getenv("IMAGE_API_KEY", config.get("api_key"))
|
||||
config["base_url"] = os.getenv("IMAGE_API_BASE_URL", config.get("base_url"))
|
||||
config["model"] = os.getenv("IMAGE_MODEL", config.get("model"))
|
||||
|
||||
return config
|
||||
|
||||
@@ -87,16 +107,16 @@ class TextToImageGenerator:
|
||||
|
||||
suffix = path.suffix.lower()
|
||||
mime_types = {
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.gif': 'image/gif',
|
||||
'.webp': 'image/webp'
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
mime_type = mime_types.get(suffix, 'image/png')
|
||||
mime_type = mime_types.get(suffix, "image/png")
|
||||
|
||||
with open(image_path, 'rb') as f:
|
||||
b64_str = base64.b64encode(f.read()).decode('utf-8')
|
||||
with open(image_path, "rb") as f:
|
||||
b64_str = base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
if with_prefix:
|
||||
return f"data:{mime_type};base64,{b64_str}"
|
||||
@@ -110,7 +130,7 @@ class TextToImageGenerator:
|
||||
image_size: Optional[str] = None,
|
||||
output_path: Optional[str] = None,
|
||||
response_format: str = "b64_json",
|
||||
ref_image: Optional[str] = None
|
||||
ref_image: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
生成图片
|
||||
@@ -134,13 +154,13 @@ class TextToImageGenerator:
|
||||
aspect_ratio=aspect_ratio,
|
||||
size=size,
|
||||
output_path=output_path,
|
||||
response_format=response_format
|
||||
response_format=response_format,
|
||||
)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"prompt": prompt,
|
||||
"response_format": response_format
|
||||
"response_format": response_format,
|
||||
}
|
||||
|
||||
# 确定尺寸:优先用 aspect_ratio 映射,其次用 size
|
||||
@@ -153,15 +173,18 @@ class TextToImageGenerator:
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
|
||||
try:
|
||||
if self.base_url and "dashscope" in self.base_url:
|
||||
return self._generate_aliyun(
|
||||
prompt, size, aspect_ratio, output_path, response_format
|
||||
)
|
||||
|
||||
with httpx.Client(timeout=180.0) as client:
|
||||
response = client.post(
|
||||
f"{self.base_url}/images/generations",
|
||||
headers=headers,
|
||||
json=payload
|
||||
f"{self.base_url}/images/generations", headers=headers, json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
@@ -175,21 +198,79 @@ class TextToImageGenerator:
|
||||
return {
|
||||
"success": True,
|
||||
"data": result,
|
||||
"saved_path": output_path if output_path else None
|
||||
"saved_path": output_path if output_path else None,
|
||||
}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP 错误: {e.response.status_code}",
|
||||
"detail": str(e)
|
||||
"detail": str(e),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": "生成失败", "detail": str(e)}
|
||||
|
||||
def _generate_aliyun(
|
||||
self, prompt, size, aspect_ratio, output_path, response_format
|
||||
):
|
||||
if aspect_ratio:
|
||||
size = RATIO_TO_SIZE.get(aspect_ratio, "1024*1024").replace("x", "*")
|
||||
elif not size:
|
||||
size = "1024*1024"
|
||||
else:
|
||||
size = size.replace("x", "*")
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"input": {"messages": [{"role": "user", "content": [{"text": prompt}]}]},
|
||||
"parameters": {"size": size, "response_format": "base64"},
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=300.0) as client:
|
||||
response = client.post(
|
||||
f"{self.base_url}/api/v1/services/aigc/multimodal-generation/generation",
|
||||
headers=headers,
|
||||
json=payload,
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
if output_path:
|
||||
image_url = (
|
||||
result.get("output", {})
|
||||
.get("choices", [{}])[0]
|
||||
.get("message", {})
|
||||
.get("content", [{}])[0]
|
||||
.get("image")
|
||||
)
|
||||
if image_url:
|
||||
self._download_image(image_url, output_path)
|
||||
result["saved_path"] = output_path
|
||||
|
||||
return {"success": True, "data": result, "saved_path": output_path}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "生成失败",
|
||||
"detail": str(e)
|
||||
"error": "HTTP错误: %s" % e.response.status_code,
|
||||
"detail": str(e),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": "生成失败", "detail": str(e)}
|
||||
|
||||
def _download_image(self, url, output_path):
|
||||
with httpx.Client(timeout=60.0) as client:
|
||||
response = client.get(url)
|
||||
response.raise_for_status()
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(response.content)
|
||||
|
||||
def _generate_with_reference(
|
||||
self,
|
||||
@@ -198,7 +279,7 @@ class TextToImageGenerator:
|
||||
aspect_ratio: Optional[str] = None,
|
||||
size: Optional[str] = None,
|
||||
output_path: Optional[str] = None,
|
||||
response_format: str = "b64_json"
|
||||
response_format: str = "b64_json",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
参考图片风格生成新图
|
||||
@@ -217,27 +298,29 @@ class TextToImageGenerator:
|
||||
|
||||
# 确定尺寸:优先用 aspect_ratio 映射,其次用 size
|
||||
if size is None:
|
||||
size = RATIO_TO_SIZE.get(aspect_ratio, "1024x1792") if aspect_ratio else "1024x1792"
|
||||
size = (
|
||||
RATIO_TO_SIZE.get(aspect_ratio, "1024x1792")
|
||||
if aspect_ratio
|
||||
else "1024x1792"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"prompt": enhanced_prompt,
|
||||
"image": image_b64,
|
||||
"size": size,
|
||||
"response_format": response_format
|
||||
"response_format": response_format,
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}"
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
}
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=180.0) as client:
|
||||
response = client.post(
|
||||
f"{self.base_url}/images/edits",
|
||||
headers=headers,
|
||||
json=payload
|
||||
f"{self.base_url}/images/edits", headers=headers, json=payload
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
@@ -251,27 +334,23 @@ class TextToImageGenerator:
|
||||
return {
|
||||
"success": True,
|
||||
"data": result,
|
||||
"saved_path": output_path if output_path else None
|
||||
"saved_path": output_path if output_path else None,
|
||||
}
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP 错误: {e.response.status_code}",
|
||||
"detail": str(e)
|
||||
"detail": str(e),
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "生成失败",
|
||||
"detail": str(e)
|
||||
}
|
||||
return {"success": False, "error": "生成失败", "detail": str(e)}
|
||||
|
||||
def _save_image(self, b64_data: str, output_path: str) -> None:
|
||||
"""保存 base64 图片到文件"""
|
||||
image_data = base64.b64decode(b64_data)
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'wb') as f:
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
|
||||
|
||||
@@ -281,9 +360,9 @@ def main():
|
||||
import time
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='文生图工具',
|
||||
description="文生图工具",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=f'''
|
||||
epilog=f"""
|
||||
尺寸参数说明:
|
||||
-r/--ratio 推荐使用,支持: {", ".join(VALID_ASPECT_RATIOS)}
|
||||
-s/--size 传统尺寸,支持: {", ".join(VALID_SIZES[:4])}...
|
||||
@@ -298,14 +377,18 @@ def main():
|
||||
# 长图场景:首图定调,后续参考首图风格
|
||||
python text_to_image.py "首屏内容" -r 3:4 -o 01.png
|
||||
python text_to_image.py "第二屏内容" -r 3:4 --ref 01.png -o 02.png
|
||||
'''
|
||||
""",
|
||||
)
|
||||
parser.add_argument('prompt', help='中文图像描述提示词')
|
||||
parser.add_argument('-o', '--output', help='输出文件路径(默认保存到当前目录)')
|
||||
parser.add_argument('-r', '--ratio', help=f'宽高比,推荐使用。可选: {", ".join(VALID_ASPECT_RATIOS)}')
|
||||
parser.add_argument('-s', '--size', help='图片尺寸 (如 1792x1024)')
|
||||
parser.add_argument('--resolution', help='分辨率 (1K/2K/4K),仅部分模型支持')
|
||||
parser.add_argument('--ref', help='参考图片路径,用于风格参考(长图场景)')
|
||||
parser.add_argument("prompt", help="中文图像描述提示词")
|
||||
parser.add_argument("-o", "--output", help="输出文件路径(默认保存到当前目录)")
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--ratio",
|
||||
help=f"宽高比,推荐使用。可选: {', '.join(VALID_ASPECT_RATIOS)}",
|
||||
)
|
||||
parser.add_argument("-s", "--size", help="图片尺寸 (如 1792x1024)")
|
||||
parser.add_argument("--resolution", help="分辨率 (1K/2K/4K),仅部分模型支持")
|
||||
parser.add_argument("--ref", help="参考图片路径,用于风格参考(长图场景)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -334,7 +417,7 @@ def main():
|
||||
aspect_ratio=args.ratio,
|
||||
image_size=args.resolution,
|
||||
output_path=output_path,
|
||||
ref_image=args.ref
|
||||
ref_image=args.ref,
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
|
||||
28
AGENTS.md
28
AGENTS.md
@@ -133,10 +133,28 @@
|
||||
|
||||
## 四、技能清单
|
||||
|
||||
### 当前已安装技能(0个)
|
||||
### 当前已安装技能(16个)
|
||||
|
||||
技能存放在 `.opencode/skills/` 目录下:
|
||||
(技能列表)
|
||||
|
||||
| 技能名 | 描述 | 触发词 |
|
||||
|--------|------|--------|
|
||||
| `csv-data-summarizer` | CSV数据分析技能。使用Python和pandas分析CSV文件,生成统计摘要和快速可视化图表 | CSV文件、表格数据 |
|
||||
| `deep-research` | 深度调研技能。搜索、整理、汇总指定主题的技术内容 | 调研、深度调研、帮我研究 |
|
||||
| `image-service` | 多模态图像处理技能,支持文生图、图生图、图生文、长图拼接 | 图片、图像、生成图、信息图、OCR |
|
||||
| `log-analyzer` | 全维度日志分析技能。自动识别日志类型,进行根因定位、告警分析、异常洞察 | 分析日志、日志排查、根因定位 |
|
||||
| `mcp-builder` | MCP服务器开发指南,用于创建高质量的MCP服务器 | 构建MCP服务器、MCP开发 |
|
||||
| `searchnews` | AI新闻搜索技能,搜索、整理、汇总指定日期的AI行业新闻 | 搜索新闻、查询AI新闻、整理新闻 |
|
||||
| `skill-creator` | 技能创建指南,用于创建新的技能或更新现有技能 | 创建技能、更新技能 |
|
||||
| `smart-query` | 智能数据库查询技能。通过SSH隧道连接线上数据库,支持自然语言转SQL | 查询数据库、问数据、看表结构 |
|
||||
| `story-to-scenes` | 长文本拆镜批量生图引擎。智能拆分场景,批量生成风格统一的配图 | 拆镜生图、故事配图、批量场景图 |
|
||||
| `uni-agent` | 统一智能体协议适配层。一套API调用所有Agent协议(ANP/MCP/A2A等) | 调用Agent、跨协议通信、连接工具 |
|
||||
| `video-creator` | 视频创作技能。图片+音频合成视频,支持转场、片尾、BGM | 生成视频、图文转视频、做视频号 |
|
||||
| `videocut-clip` | 执行视频剪辑。根据删除任务执行FFmpeg剪辑 | 执行剪辑、开始剪、确认剪辑 |
|
||||
| `videocut-clip-oral` | 口播视频转录和口误识别。生成审查稿和删除任务清单 | 剪口播、处理视频、识别口误 |
|
||||
| `videocut-install` | 环境准备。安装依赖、下载模型、验证环境 | 安装、环境准备、初始化 |
|
||||
| `videocut-self-update` | 自更新skills。记录用户反馈,更新方法论和规则 | 更新规则、记录反馈、改进skill |
|
||||
| `videocut-subtitle` | 字幕生成与烧录。转录→词典纠错→审核→烧录 | 加字幕、生成字幕、字幕 |
|
||||
|
||||
### 技能使用说明
|
||||
|
||||
@@ -203,6 +221,12 @@
|
||||
- 每次会话开始时主动读取
|
||||
- 执行相关任务前查阅对应记忆
|
||||
|
||||
### 短期待办事项
|
||||
每次会话开始时自动读取 `.memory/pending.md`,包含:
|
||||
- 近期待办任务(有时间限制的)
|
||||
- 任务完成后自动移除
|
||||
- 无需用户提醒
|
||||
|
||||
---
|
||||
|
||||
## 八、铁律(违反即解雇)
|
||||
|
||||
Reference in New Issue
Block a user