feat: add multimodal vision expert for image analysis and code generation

- VisionExpert class: image analysis + code generation from images
- Anthropic Messages API integration (mimo-v2-omni model)
- Gate classifier: new 'vision' expert type
- Orchestrator: vision pipeline with image_paths support
- CLI: /paste (clipboard) and /image (file) commands
- Optional deps: pip install kwcode[multimodal] (Pillow + pyperclip)
- Architecture diagram in docs/
This commit is contained in:
dashitongzhi
2026-04-30 11:46:49 +08:00
parent a88e0c18b3
commit b2e9d33164
8 changed files with 540 additions and 6 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ build/
.pytest_cache/
test_project/
.eggs/
.DS_Store

View File

@@ -211,6 +211,14 @@ Prompt Optimizer可选需 Anthropic API key
### Office 文档
- Excel / PPT / Word 生成
### 多模态图片处理
- `/paste` 命令:从剪贴板粘贴图片
- `/image <path>` 命令:添加图片文件
- 图片分析代码截图、UI设计图、文档表格
- 基于图片的代码生成UI截图→HTML/CSS、错误截图→修复代码
- 支持格式PNG、JPG、JPEG、GIF、WebP、BMP
- 安装:`pip install kwcode[multimodal]`
### 价值可见
- `kwcode stats`:完成任务数、节省时间估算
- 飞轮通知:专家投产时弹出
@@ -355,6 +363,8 @@ kwcode --plan "重构数据库连接层"
/memory 查看项目记忆
/init 初始化项目规则文件
/cd <路径> 切换项目目录
/paste 从剪贴板粘贴图片
/image <路径> 添加图片文件
/help 显示帮助
```
@@ -418,7 +428,8 @@ kaiwu/
│ ├── verifier.py # [元专家] 语法检查 + pytest
│ ├── debug_subagent.py # [元专家] 运行时调试sys.settrace
│ ├── reviewer.py # [元专家] 需求对齐审查
── search_augmentor.py # 搜索增强 + BM25 + CE 重排
── search_augmentor.py # 搜索增强 + BM25 + CE 重排
│ └── vision_expert.py # [元专家] 多模态图片处理
├── search/
│ ├── reranker.py # Cross-Encoder 可选重排
│ ├── duckduckgo.py # SearXNG + DDG 并行搜索

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -161,6 +161,10 @@ def _build_pipeline(model_path, ollama_url, ollama_model, project_root, verbose)
from kaiwu.experts.chat_expert import ChatExpert
chat_expert = ChatExpert(llm=llm, search_augmentor=search)
# Vision Expert (多模态图片处理)
from kaiwu.experts.vision_expert import VisionExpert
vision_expert = VisionExpert(llm=llm, tool_executor=tools)
# Debug Subagent (问题1修复实例化并注入)
from kaiwu.experts.debug_subagent import DebugSubagent
debug_subagent = DebugSubagent(llm, tools)
@@ -183,6 +187,7 @@ def _build_pipeline(model_path, ollama_url, ollama_model, project_root, verbose)
ab_tester=ab_tester,
chat_expert=chat_expert,
debug_subagent=debug_subagent,
vision_expert=vision_expert,
)
# Wire circular reference: ABTester needs orchestrator for backtest
ab_tester.orchestrator = orchestrator
@@ -192,17 +197,26 @@ def _build_pipeline(model_path, ollama_url, ollama_model, project_root, verbose)
# ── Single task execution ─────────────────────────────────────
def _run_task(task, gate, orchestrator, memory, project_root, verbose, plan=False, no_search=False):
def _run_task(task, gate, orchestrator, memory, project_root, verbose, plan=False, no_search=False, image_paths=None):
"""Execute a single task through the pipeline. Returns success bool."""
from kaiwu.core.orchestrator import EXPERT_SEQUENCES
from rich.progress import Progress, SpinnerColumn, TextColumn
# 处理图片上下文
if image_paths:
logger.info(f"[main] 任务包含 {len(image_paths)} 张图片")
# 将图片路径添加到任务描述中
image_context = "\n".join([f"[图片: {img}]" for img in image_paths])
task_with_images = f"{task}\n\n{image_context}"
else:
task_with_images = task
# Gate (with spinner)
with Progress(SpinnerColumn(), TextColumn("{task.description}"),
transient=True, console=console) as progress:
spin = progress.add_task("分析任务...", total=None)
try:
gate_result = gate.classify(task, memory_context=memory.load(project_root))
gate_result = gate.classify(task_with_images, memory_context=memory.load(project_root))
except Exception as e:
progress.stop()
console.print(f"\n [red]模型调用失败:{e}[/red]")
@@ -298,12 +312,13 @@ def _run_task(task, gate, orchestrator, memory, project_root, verbose, plan=Fals
logger.debug("[main] 预搜索失败: %s", e)
result = orchestrator.run(
user_input=task,
user_input=task_with_images,
gate_result=gate_result,
project_root=project_root,
on_status=_status_fn,
no_search=no_search,
pre_search_results=pre_search,
image_paths=image_paths,
)
# 保存最后结果供conversation_history使用问题7
orchestrator._last_result = result
@@ -393,6 +408,8 @@ REPL_COMMANDS = {
"/plan": "计划模式 (用法: /plan <任务> 或 /plan 后输入任务)",
"/multi": "多任务模式 (用法: /multi 后按提示输入多个任务)",
"/api": "API配置 (用法: /api show | /api temp <url> | /api default <url>)",
"/paste": "从剪贴板粘贴图片",
"/image": "添加图片文件 (用法: /image <path>)",
"/exit": "退出",
}
@@ -642,6 +659,35 @@ def _repl(model_path, ollama_url, ollama_model, project_root, verbose):
elif cmd == "/multi":
_handle_multi_command(arg, gate, orchestrator, project_root, console)
elif cmd == "/paste":
from kaiwu.experts.vision_expert import save_clipboard_image
image_path = save_clipboard_image()
if image_path:
console.print(f" [green]图片已从剪贴板保存: {image_path}[/green]")
console.print(" [dim]现在可以输入任务描述,图片将作为上下文[/dim]")
# 存储图片路径供后续任务使用
if not hasattr(session, '_pending_images'):
session._pending_images = []
session._pending_images.append(image_path)
else:
console.print(" [yellow]剪贴板中没有图片[/yellow]")
elif cmd == "/image":
if not arg:
console.print(" [yellow]用法: /image <图片路径>[/yellow]")
else:
from kaiwu.experts.vision_expert import validate_image_path
image_path = arg.strip()
if validate_image_path(image_path):
console.print(f" [green]图片已添加: {image_path}[/green]")
console.print(" [dim]现在可以输入任务描述,图片将作为上下文[/dim]")
# 存储图片路径供后续任务使用
if not hasattr(session, '_pending_images'):
session._pending_images = []
session._pending_images.append(image_path)
else:
console.print(f" [red]图片文件不存在或格式不支持: {image_path}[/red]")
elif cmd == "/api":
api_parts = user_input.split()
result = _handle_api_command(api_parts, ollama_url, ollama_model)
@@ -677,6 +723,13 @@ def _repl(model_path, ollama_url, ollama_model, project_root, verbose):
f"耗时{pruner._last_compress_ms:.1f}ms[/dim]"
)
# 处理待处理的图片
pending_images = getattr(session, '_pending_images', [])
if pending_images:
console.print(f" [cyan]图片上下文: {len(pending_images)} 张图片[/cyan]")
for img_path in pending_images:
console.print(f" - {img_path}")
t0 = time.perf_counter()
# P2: Small model forces plan mode (问题6修复用户可通过 no_search 间接控制)
effective_plan = plan_next or (model_strategy.force_plan_mode and not no_search)
@@ -689,7 +742,13 @@ def _repl(model_path, ollama_url, ollama_model, project_root, verbose):
verbose=verbose,
plan=effective_plan,
no_search=False,
image_paths=pending_images if pending_images else None,
)
# 清除已使用的图片
if pending_images:
session._pending_images = []
elapsed = time.perf_counter() - t0
plan_next = False # Reset plan flag

View File

@@ -27,6 +27,7 @@ expert_type选项
- doc写注释、文档、README仅限代码相关文档用户明确提到已有文件+docstring/注释)
- office仅限生成Excel(.xlsx)/Word(.docx)/PPT(.pptx)办公文档,不包括代码文件
- chat问候、闲聊、非编码问题、询问天气、询问知识
- vision图片分析、图片代码生成、UI截图分析、设计图实现用户上传了图片或提到图片相关内容
difficulty选项easy | hard
hard条件满足任意一条
@@ -48,6 +49,7 @@ subtask_hint仅difficulty=hard时填写逗号分隔的子任务简述。
- "修复src/xxx.py" → locator_repair
- "重构src/xxx.py" → refactor
- 不确定时优先选codegen或locator_repair不要选office
- 图片相关任务(上传图片、分析截图、根据设计图生成代码) → vision
示例:
- "你好"{{"expert_type":"chat","task_summary":"问候","difficulty":"easy","needs_search":false,"subtask_hint":""}}
@@ -57,6 +59,8 @@ subtask_hint仅difficulty=hard时填写逗号分隔的子任务简述。
- "生成一个Excel报表"{{"expert_type":"office","task_summary":"Excel报表","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "根据内容生成项目介绍PPT"{{"expert_type":"office","task_summary":"项目PPT","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "做个幻灯片汇报"{{"expert_type":"office","task_summary":"汇报PPT","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "分析这张截图"{{"expert_type":"vision","task_summary":"图片分析","difficulty":"easy","needs_search":false,"subtask_hint":""}}
- "根据UI设计图生成HTML代码"{{"expert_type":"vision","task_summary":"UI代码生成","difficulty":"easy","needs_search":false,"subtask_hint":""}}
格式:{{"expert_type":"...","task_summary":"10字内","difficulty":"...","needs_search":false,"subtask_hint":""}}
@@ -66,14 +70,14 @@ subtask_hint仅difficulty=hard时填写逗号分隔的子任务简述。
GATE_GRAMMAR = r'''
root ::= "{" ws expert-type "," ws task-summary "," ws difficulty "}" ws
expert-type ::= "\"expert_type\"" ws ":" ws "\"" expert-val "\""
expert-val ::= "locator_repair" | "codegen" | "refactor" | "doc" | "office" | "chat"
expert-val ::= "locator_repair" | "codegen" | "refactor" | "doc" | "office" | "chat" | "vision"
task-summary ::= "\"task_summary\"" ws ":" ws string
difficulty ::= "\"difficulty\"" ws ":" ws ("\"easy\"" | "\"hard\"")
string ::= "\"" [^"]* "\""
ws ::= [ \t\n]*
'''
VALID_EXPERT_TYPES = {"locator_repair", "codegen", "refactor", "doc", "office", "chat"}
VALID_EXPERT_TYPES = {"locator_repair", "codegen", "refactor", "doc", "office", "chat", "vision"}
VALID_DIFFICULTIES = {"easy", "hard"}

View File

@@ -36,6 +36,7 @@ EXPERT_SEQUENCES = {
"doc": ["locator", "generator"],
"office": ["office"],
"chat": ["chat"],
"vision": ["vision"],
}
@@ -59,6 +60,7 @@ class PipelineOrchestrator:
ab_tester: ABTester | None = None,
chat_expert: ChatExpert | None = None,
debug_subagent=None,
vision_expert=None,
):
self.locator = locator
self.generator = generator
@@ -66,6 +68,7 @@ class PipelineOrchestrator:
self.search_augmentor = search_augmentor
self.office_handler = office_handler
self.chat_expert = chat_expert
self.vision_expert = vision_expert
self.tools = tool_executor
self.memory = memory
self.registry = registry
@@ -85,6 +88,7 @@ class PipelineOrchestrator:
no_search: bool = False,
skip_checkpoint: bool = False,
pre_search_results: str = "",
image_paths: list = None,
) -> dict:
"""
Execute the expert pipeline.
@@ -104,6 +108,11 @@ class PipelineOrchestrator:
expert_system_prompt=gate_result.get("system_prompt", ""),
)
# 处理图片路径
if image_paths:
ctx.image_paths = image_paths
logger.info(f"[orchestrator] 任务包含 {len(image_paths)} 张图片")
expert_type = gate_result.get("expert_type", "locator_repair")
# ── Pre-search results injection (P1-B) ──
@@ -140,6 +149,30 @@ class PipelineOrchestrator:
"elapsed": elapsed,
}
# vision类型图片处理任务
if expert_type == "vision":
self._emit(on_status, "vision", "图片处理模式")
if self.vision_expert and ctx.image_paths:
# 使用第一个图片进行处理
ctx.image_path = ctx.image_paths[0]
result = self.vision_expert.run(ctx)
elapsed = time.time() - start_time
return {
"success": result.get("success", False),
"context": ctx,
"error": result.get("error"),
"elapsed": elapsed,
}
else:
ctx.generator_output = {"explanation": "图片处理功能需要配置Vision专家", "patches": []}
elapsed = time.time() - start_time
return {
"success": False,
"context": ctx,
"error": "Vision专家未配置或未提供图片",
"elapsed": elapsed,
}
# Gate 3: AB test — check if a candidate expert should be used for this task
ab_candidate_name = None
ab_used_new = False

View File

@@ -0,0 +1,425 @@
"""
VisionExpert: 多模态图片处理专家
支持图片上传、剪贴板粘贴、图片分析和基于图片的代码生成。
Pipeline: 图片输入 → 图片分析 → 任务路由 → 代码生成/图片描述
"""
import base64
import logging
import os
import tempfile
from pathlib import Path
from typing import Optional, Union
from kaiwu.core.context import TaskContext
logger = logging.getLogger(__name__)
# ── Vision system prompts ──────────────────────────────────────────
VISION_ANALYSIS_SYSTEM = """\
你是KWCode的多模态视觉专家。用户上传了一张图片请根据图片内容提供有用的分析。
## 分析类型
1. **代码截图分析**:如果图片包含代码、错误信息、终端输出
- 识别代码语言和功能
- 分析错误信息并提供修复建议
- 如果用户有相关任务,提供代码修改建议
2. **UI/设计图分析**如果图片是UI设计、网页截图、界面原型
- 描述布局和设计元素
- 提供实现建议HTML/CSS/框架)
- 识别可复用的组件
3. **文档/表格分析**:如果图片是文档、表格、图表
- 提取关键信息
- 结构化数据
- 提供数据处理建议
4. **通用图片分析**:其他类型图片
- 详细描述图片内容
- 识别关键元素
- 提供相关建议
## 输出格式
- 简洁明了2-5句话
- 如果是代码相关,提供具体的修改建议
- 如果是UI设计提供实现方向
- 使用中文回复"""
VISION_CODEGEN_SYSTEM = """\
你是KWCode的多模态代码生成专家。用户上传了图片并要求生成代码。
## 代码生成规则
1. **UI截图 → HTML/CSS**
- 使用Tailwind CSS
- 响应式设计
- 保持视觉一致性
2. **错误截图 → 修复代码**
- 分析错误信息
- 定位问题根源
- 提供最小化修复
3. **设计图 → 组件代码**
- 选择合适的框架React/Vue/原生)
- 模块化组件设计
- 可复用性优先
## 输出要求
- 只输出完整可执行的代码
- 不要解释不要markdown代码块标记
- 代码末尾添加使用说明注释"""
class VisionExpert:
"""多模态图片处理专家:分析图片内容,生成代码或提供分析"""
def __init__(self, llm, tool_executor=None):
self.llm = llm
self.tools = tool_executor
self._temp_dir = Path(tempfile.mkdtemp(prefix="kwcode_vision_"))
def run(self, ctx: TaskContext) -> dict:
"""
处理图片输入任务
Args:
ctx: 任务上下文,包含 user_input 和 image_path
Returns:
dict: 包含 success, output, metadata
"""
image_path = getattr(ctx, 'image_path', None)
if not image_path:
return {
"success": False,
"output": "错误:未提供图片路径",
"metadata": {"error": "no_image_path"}
}
# 验证图片文件
if not self._validate_image(image_path):
return {
"success": False,
"output": f"错误:图片文件不存在或格式不支持: {image_path}",
"metadata": {"error": "invalid_image"}
}
# 分析用户意图
user_input = ctx.user_input.strip()
is_codegen_task = self._is_codegen_task(user_input)
try:
# 编码图片
image_base64 = self._encode_image(image_path)
if is_codegen_task:
return self._run_codegen(ctx, image_base64, image_path)
else:
return self._run_analysis(ctx, image_base64, image_path)
except Exception as e:
logger.error(f"VisionExpert error: {e}")
return {
"success": False,
"output": f"图片处理失败: {str(e)}",
"metadata": {"error": str(e)}
}
def _validate_image(self, image_path: str) -> bool:
"""验证图片文件是否存在且格式支持"""
path = Path(image_path)
if not path.exists():
return False
# 支持的图片格式
supported_formats = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'}
return path.suffix.lower() in supported_formats
def _encode_image(self, image_path: str) -> str:
"""将图片编码为base64"""
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode('utf-8')
def _is_codegen_task(self, user_input: str) -> bool:
"""判断是否为代码生成任务"""
codegen_keywords = [
"生成代码", "写代码", "实现", "创建", "生成",
"代码", "函数", "", "组件", "页面",
"generate", "create", "implement", "code",
"html", "css", "javascript", "python", "react", "vue"
]
# 检查是否包含代码相关关键词
user_input_lower = user_input.lower()
return any(kw in user_input_lower for kw in codegen_keywords)
def _run_analysis(self, ctx: TaskContext, image_base64: str, image_path: str) -> dict:
"""运行图片分析"""
logger.info(f"[vision] 分析图片: {image_path}")
# 构建提示词
user_input = ctx.user_input.strip()
if user_input:
prompt = f"用户上传了一张图片并说:{user_input}\n\n请分析这张图片。"
else:
prompt = "用户上传了一张图片,请分析其内容。"
# 调用Vision LLM
response = self._call_vision_llm(
system_prompt=VISION_ANALYSIS_SYSTEM,
user_prompt=prompt,
image_base64=image_base64
)
return {
"success": True,
"output": response,
"metadata": {
"type": "vision_analysis",
"image_path": image_path,
"has_user_task": bool(user_input)
}
}
def _run_codegen(self, ctx: TaskContext, image_base64: str, image_path: str) -> dict:
"""运行基于图片的代码生成"""
logger.info(f"[vision] 基于图片生成代码: {image_path}")
user_input = ctx.user_input.strip()
# 构建提示词
prompt = f"用户上传了一张图片并要求:{user_input}\n\n请根据图片内容生成代码。"
# 调用Vision LLM
response = self._call_vision_llm(
system_prompt=VISION_CODEGEN_SYSTEM,
user_prompt=prompt,
image_base64=image_base64
)
# 尝试执行生成的代码(如果用户要求)
if self.tools and self._should_execute_code(user_input):
execution_result = self._execute_generated_code(response)
if execution_result:
response += f"\n\n--- 执行结果 ---\n{execution_result}"
return {
"success": True,
"output": response,
"metadata": {
"type": "vision_codegen",
"image_path": image_path,
"task": user_input
}
}
def _call_vision_llm(self, system_prompt: str, user_prompt: str, image_base64: str) -> str:
"""调用支持Vision的LLM (Anthropic Messages API 格式)
优先使用 self.llm (如果支持 vision),否则回退到环境变量配置的 API
KWCODE_VISION_API_URL - API endpoint (默认 Anthropic 格式)
KWCODE_VISION_API_KEY - API key
KWCODE_VISION_MODEL - 模型名 (默认 mimo-v2-omni)
"""
# 尝试通过 self.llm 直接调用(如果后端支持多模态)
if self.llm is not None:
try:
return self._try_llm_vision(system_prompt, user_prompt, image_base64)
except (AttributeError, TypeError, Exception) as e:
logger.debug(f"[vision] self.llm 不支持 vision回退到 API: {e}")
# 回退:直接调用 Anthropic Messages API
return self._call_anthropic_vision(system_prompt, user_prompt, image_base64)
def _try_llm_vision(self, system_prompt: str, user_prompt: str, image_base64: str) -> str:
"""尝试通过 self.llm 的 chat 接口发送多模态请求"""
messages = [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": [
{"type": "text", "text": user_prompt},
{
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{image_base64}"},
},
],
},
]
return self.llm.chat(messages, max_tokens=2048)
def _call_anthropic_vision(self, system_prompt: str, user_prompt: str, image_base64: str) -> str:
"""直接调用 Anthropic Messages API兼容 xiaomimimo 等代理)"""
import json as _json
import urllib.request
import urllib.error
api_url = os.environ.get(
"KWCODE_VISION_API_URL",
"https://token-plan-cn.xiaomimimo.com/anthropic/v1/messages",
)
api_key = os.environ.get("KWCODE_VISION_API_KEY", "")
model = os.environ.get("KWCODE_VISION_MODEL", "mimo-v2-omni")
# 检测图片格式(从 base64 开头字节判断)
media_type = "image/png"
raw_sample = image_base64[:20]
if raw_sample.startswith("/9j"):
media_type = "image/jpeg"
elif raw_sample.startswith("R0lGOD"):
media_type = "image/gif"
elif raw_sample.startswith("UklGR"):
media_type = "image/webp"
payload = {
"model": model,
"max_tokens": 2048,
"system": system_prompt,
"messages": [
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": image_base64,
},
},
{"type": "text", "text": user_prompt},
],
}
],
}
headers = {
"Content-Type": "application/json",
"anthropic-version": "2023-06-01",
}
if api_key:
headers["x-api-key"] = api_key
logger.info(f"[vision] 调用 {api_url} model={model}")
data_bytes = _json.dumps(payload).encode("utf-8")
req = urllib.request.Request(api_url, data=data_bytes, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=90) as resp:
result = _json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")[:500]
raise RuntimeError(f"Vision API HTTP {e.code}: {body}") from e
# 提取文本
# 检查 API 级别错误
if result.get("type") == "error" or result.get("isError"):
error_msg = result.get("error", {}).get("message", str(result))
raise RuntimeError(f"Vision API error: {error_msg}")
stop_reason = result.get("stop_reason", "")
if stop_reason == "max_tokens":
logger.warning("[vision] 输出被截断 (max_tokens reached)")
text_parts = []
for block in result.get("content", []):
if block.get("type") == "text":
text_parts.append(block["text"])
usage = result.get("usage", {})
logger.info(
f"[vision] 完成: {usage.get('input_tokens', '?')} in / "
f"{usage.get('output_tokens', '?')} out tokens"
)
return "\n".join(text_parts) if text_parts else "[VisionExpert] 模型未返回文本内容"
def _should_execute_code(self, user_input: str) -> bool:
"""判断是否应该执行生成的代码"""
execute_keywords = ["执行", "运行", "测试", "run", "execute", "test"]
return any(kw in user_input.lower() for kw in execute_keywords)
def _execute_generated_code(self, code: str) -> Optional[str]:
"""执行生成的代码"""
if not self.tools:
return None
try:
# 保存代码到临时文件
temp_file = self._temp_dir / "generated_code.py"
temp_file.write_text(code, encoding='utf-8')
# 执行代码
result = self.tools.run_bash(f"python {temp_file}")
return result.get("output", "")
except Exception as e:
return f"执行失败: {str(e)}"
def cleanup(self):
"""清理临时文件"""
import shutil
if self._temp_dir.exists():
shutil.rmtree(self._temp_dir)
def save_clipboard_image() -> Optional[str]:
"""
从剪贴板保存图片
Returns:
str: 保存的图片路径如果剪贴板没有图片则返回None
"""
try:
from PIL import Image, ImageGrab
image = ImageGrab.grabclipboard()
if isinstance(image, Image.Image):
# 生成临时文件路径
temp_dir = Path(tempfile.mkdtemp(prefix="kwcode_clipboard_"))
temp_path = temp_dir / "clipboard_image.png"
# 保存图片
image.save(temp_path, "PNG")
logger.info(f"[vision] 剪贴板图片已保存: {temp_path}")
return str(temp_path)
else:
logger.debug("[vision] 剪贴板中没有图片")
return None
except ImportError:
logger.warning("[vision] Pillow未安装无法处理剪贴板图片")
return None
except Exception as e:
logger.error(f"[vision] 处理剪贴板图片失败: {e}")
return None
def validate_image_path(path: str) -> bool:
"""验证图片路径是否有效"""
path_obj = Path(path)
if not path_obj.exists():
return False
supported_formats = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'}
return path_obj.suffix.lower() in supported_formats
def get_image_info(image_path: str) -> dict:
"""获取图片基本信息"""
try:
from PIL import Image
with Image.open(image_path) as img:
return {
"format": img.format,
"size": img.size,
"mode": img.mode,
"file_size": os.path.getsize(image_path)
}
except Exception as e:
return {"error": str(e)}

View File

@@ -30,6 +30,7 @@ dependencies = [
[project.optional-dependencies]
rerank = ["sentence-transformers>=2.7.0"]
multimodal = ["Pillow>=10.0", "pyperclip>=1.8"]
[project.scripts]
kwcode = "kaiwu.cli.main:app"