|
|
import gradio as gr |
|
|
import os |
|
|
import base64 |
|
|
import json |
|
|
import time |
|
|
os.system("pip install openai") |
|
|
from openai import OpenAI, APITimeoutError, APIError |
|
|
import io |
|
|
import logging |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
from PIL import Image |
|
|
PIL_AVAILABLE = True |
|
|
except ImportError: |
|
|
PIL_AVAILABLE = False |
|
|
logging.warning("未找到pillow库,图片处理功能将不可用") |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
API_KEY = os.getenv('baidu_api_key') |
|
|
|
|
|
client = OpenAI( |
|
|
base_url='https://qianfan.baidubce.com/v2', |
|
|
api_key=API_KEY |
|
|
) |
|
|
|
|
|
|
|
|
def read_base64_from_file(file_path): |
|
|
"""从指定文本文件读取Base64编码(处理空文件、路径错误等异常)""" |
|
|
try: |
|
|
|
|
|
if not os.path.exists(file_path): |
|
|
logging.warning(f"Base64文件不存在:{file_path}") |
|
|
return None |
|
|
|
|
|
|
|
|
with open(file_path, 'r', encoding='utf-8') as f: |
|
|
base64_str = f.read().strip() |
|
|
|
|
|
|
|
|
if len(base64_str) % 4 != 0: |
|
|
logging.error(f"{file_path} 中Base64编码无效(长度非4的倍数)") |
|
|
return None |
|
|
|
|
|
return base64_str |
|
|
except Exception as e: |
|
|
logging.error(f"读取{file_path}失败:{str(e)}") |
|
|
return None |
|
|
|
|
|
def base64_to_pil_image(base64_str): |
|
|
"""将Base64字符串转换为PIL Image对象(用于Gradio渲染)""" |
|
|
try: |
|
|
if not base64_str: |
|
|
return None |
|
|
|
|
|
|
|
|
image_bytes = base64.b64decode(base64_str) |
|
|
|
|
|
image = Image.open(io.BytesIO(image_bytes)) |
|
|
return image |
|
|
except Exception as e: |
|
|
logging.error(f"Base64转图片失败:{str(e)}") |
|
|
return None |
|
|
|
|
|
|
|
|
def compress_image(image_path, max_size=(1024, 1024)): |
|
|
"""图片压缩处理,增加pillow可用性检查""" |
|
|
if not PIL_AVAILABLE: |
|
|
raise Exception("未安装pillow库,请执行 `pip install pillow` 安装后重试") |
|
|
|
|
|
try: |
|
|
with Image.open(image_path) as img: |
|
|
|
|
|
if img.mode in ('RGBA', 'LA'): |
|
|
background = Image.new(img.mode[:-1], img.size, (255, 255, 255)) |
|
|
background.paste(img, img.split()[-1]) |
|
|
img = background |
|
|
|
|
|
img.thumbnail(max_size, Image.Resampling.LANCZOS) |
|
|
|
|
|
img_byte_arr = io.BytesIO() |
|
|
img.save(img_byte_arr, format='JPEG', quality=80) |
|
|
return base64.b64encode(img_byte_arr.getvalue()).decode('utf-8') |
|
|
except Exception as e: |
|
|
logger.error(f"图片处理失败: {str(e)}") |
|
|
raise Exception(f"图片处理失败: {str(e)}") |
|
|
|
|
|
def clean_json_response(raw_content): |
|
|
"""清理模型返回的响应,移除代码块标记,确保JSON可解析""" |
|
|
|
|
|
if raw_content.startswith('```json') or raw_content.startswith('```JSON'): |
|
|
raw_content = raw_content[7:] |
|
|
|
|
|
if raw_content.endswith('```'): |
|
|
raw_content = raw_content[:-3] |
|
|
|
|
|
return raw_content.strip() |
|
|
|
|
|
def format_analysis_result(result_json): |
|
|
"""将JSON结果格式化为结构化的Markdown文本,用于页面展示""" |
|
|
if not isinstance(result_json, dict): |
|
|
return "⚠️ 分析结果格式异常,无法结构化展示" |
|
|
|
|
|
|
|
|
md_content = "# 智能体截图分析报告\n\n" |
|
|
|
|
|
|
|
|
scoring = result_json.get("页面评分", {}) |
|
|
if scoring: |
|
|
md_content += "## 一、页面评分(1-10分)\n" |
|
|
md_content += "| 评价维度 | 得分 | 评价说明 |\n" |
|
|
md_content += "|----------------|------|---------------------------|\n" |
|
|
|
|
|
|
|
|
score_items = ["overall", "design", "usability", "functionality", "responsiveness"] |
|
|
item_names = { |
|
|
"overall": "整体评价", |
|
|
"design": "设计美感", |
|
|
"usability": "易用性", |
|
|
"functionality": "功能完整性", |
|
|
"responsiveness": "响应式设计" |
|
|
} |
|
|
|
|
|
for item in score_items: |
|
|
score = scoring.get(item, "N/A") |
|
|
comment = scoring.get(f"{item}_comment", "无") |
|
|
md_content += f"| {item_names[item]} | {score} | {comment} |\n" |
|
|
md_content += "\n" |
|
|
|
|
|
|
|
|
capabilities = result_json.get("智能体能力拆解", {}) |
|
|
if capabilities: |
|
|
md_content += "## 二、智能体能力拆解\n" |
|
|
|
|
|
|
|
|
core_funcs = capabilities.get("core_functions", []) |
|
|
if core_funcs: |
|
|
md_content += "### 1. 核心功能\n" |
|
|
for i, func in enumerate(core_funcs, 1): |
|
|
md_content += f"- **{i}.** {func}\n" |
|
|
md_content += "\n" |
|
|
|
|
|
|
|
|
strengths = capabilities.get("strengths", []) |
|
|
if strengths: |
|
|
md_content += "### 2. 优势\n" |
|
|
for i, strength in enumerate(strengths, 1): |
|
|
md_content += f"- **{i}.** {strength}\n" |
|
|
md_content += "\n" |
|
|
|
|
|
|
|
|
weaknesses = capabilities.get("weaknesses", []) |
|
|
if weaknesses: |
|
|
md_content += "### 3. 劣势\n" |
|
|
for i, weakness in enumerate(weaknesses, 1): |
|
|
md_content += f"- **{i}.** {weakness}\n" |
|
|
md_content += "\n" |
|
|
|
|
|
|
|
|
potential_uses = capabilities.get("potential_uses", []) |
|
|
if potential_uses: |
|
|
md_content += "### 4. 潜在用途\n" |
|
|
for i, use in enumerate(potential_uses, 1): |
|
|
md_content += f"- **{i}.** {use}\n" |
|
|
md_content += "\n" |
|
|
|
|
|
|
|
|
improvement_areas = capabilities.get("improvement_areas", []) |
|
|
if improvement_areas: |
|
|
md_content += "### 5. 改进方向\n" |
|
|
for i, area in enumerate(improvement_areas, 1): |
|
|
md_content += f"- **{i}.** {area}\n" |
|
|
md_content += "\n" |
|
|
|
|
|
|
|
|
detailed_analysis = capabilities.get("detailed_analysis", "无") |
|
|
if detailed_analysis != "无": |
|
|
md_content += "### 6. 详细分析\n" |
|
|
md_content += f">{detailed_analysis}\n\n" |
|
|
|
|
|
|
|
|
if not scoring and not capabilities: |
|
|
md_content += "⚠️ 未获取到有效分析结果,请检查图片内容或重试" |
|
|
|
|
|
return md_content |
|
|
|
|
|
def analyze_image(input_image): |
|
|
"""AI分析核心函数:返回【结构化Markdown结果】+【原始JSON】+【状态】""" |
|
|
|
|
|
structured_result = "" |
|
|
raw_json = "" |
|
|
status = "就绪" |
|
|
|
|
|
|
|
|
if not PIL_AVAILABLE: |
|
|
status = "❌ 缺少依赖" |
|
|
structured_result = "未安装pillow库,请关闭应用并执行 `pip install pillow` 安装后重试" |
|
|
yield structured_result, raw_json, status |
|
|
return |
|
|
|
|
|
|
|
|
if not input_image: |
|
|
status = "请先上传图片" |
|
|
structured_result = "请点击左侧「上传截图」区域,选择JPG/PNG格式的智能体网页截图" |
|
|
yield structured_result, raw_json, status |
|
|
return |
|
|
|
|
|
try: |
|
|
|
|
|
status = "正在准备图片..." |
|
|
structured_result = "🔄 正在读取并准备图片文件..." |
|
|
yield structured_result, raw_json, status |
|
|
time.sleep(0.1) |
|
|
|
|
|
|
|
|
status = "正在压缩图片..." |
|
|
structured_result = "🔄 正在压缩图片(确保分析效率,不影响质量)..." |
|
|
yield structured_result, raw_json, status |
|
|
image_base64 = compress_image(input_image) |
|
|
time.sleep(0.1) |
|
|
|
|
|
|
|
|
status = "正在发送分析请求..." |
|
|
structured_result = "🔄 正在向AI模型发送分析请求(约3-10秒)..." |
|
|
yield structured_result, raw_json, status |
|
|
|
|
|
|
|
|
messages = [ |
|
|
{ |
|
|
"role": "user", |
|
|
"content": [ |
|
|
{ |
|
|
"type": "text", |
|
|
"text": "请分析这张智能体网页的截图,并严格按照以下格式返回JSON:\n\ |
|
|
{\n\ |
|
|
\"页面评分\": {\n\ |
|
|
\"overall\": 分数(1-10),\n\ |
|
|
\"overall_comment\": \"简短评价\",\n\ |
|
|
\"design\": 分数(1-10),\n\ |
|
|
\"design_comment\": \"简短评价\",\n\ |
|
|
\"usability\": 分数(1-10),\n\ |
|
|
\"usability_comment\": \"简短评价\",\n\ |
|
|
\"functionality\": 分数(1-10),\n\ |
|
|
\"functionality_comment\": \"简短评价\",\n\ |
|
|
\"responsiveness\": 分数(1-10),\n\ |
|
|
\"responsiveness_comment\": \"简短评价\"\n\ |
|
|
},\n\ |
|
|
\"智能体能力拆解\": {\n\ |
|
|
\"core_functions\": [\"功能1\", \"功能2\", ...],\n\ |
|
|
\"strengths\": [\"优势1\", \"优势2\", ...],\n\ |
|
|
\"weaknesses\": [\"劣势1\", \"劣势2\", ...],\n\ |
|
|
\"potential_uses\": [\"用途1\", \"用途2\", ...],\n\ |
|
|
\"improvement_areas\": [\"改进1\", \"改进2\", ...],\n\ |
|
|
\"detailed_analysis\": \"详细综合分析文本\"\n\ |
|
|
}\n\ |
|
|
}\n\ |
|
|
【注意】:仅返回纯JSON,不要包含代码块标记(如```json)、解释文本等额外内容!" |
|
|
}, |
|
|
{ |
|
|
"type": "image_url", |
|
|
"image_url": {"url": f"data:image/jpeg;base64,{image_base64}"} |
|
|
} |
|
|
] |
|
|
} |
|
|
] |
|
|
|
|
|
|
|
|
status = "AI正在分析(约3-10秒)..." |
|
|
structured_result = f"🔄 AI正在深度分析图片内容...\n\n当前进度:\n- 图片已上传:✅\n- 模型已接收:✅\n- 分析中:⌛" |
|
|
yield structured_result, raw_json, status |
|
|
|
|
|
|
|
|
response = client.chat.completions.create( |
|
|
model="ernie-4.5-turbo-vl-32k", |
|
|
messages=messages, |
|
|
temperature=0.2, |
|
|
top_p=0.8, |
|
|
extra_body={"penalty_score": 1}, |
|
|
timeout=30 |
|
|
) |
|
|
|
|
|
|
|
|
status = "正在整理分析结果..." |
|
|
structured_result = "🔄 AI分析完成,正在整理结构化报告..." |
|
|
yield structured_result, raw_json, status |
|
|
time.sleep(0.1) |
|
|
|
|
|
|
|
|
raw_content = response.choices[0].message.content |
|
|
cleaned_content = clean_json_response(raw_content) |
|
|
raw_json = json.dumps(json.loads(cleaned_content), ensure_ascii=False, indent=2) |
|
|
|
|
|
|
|
|
result_json = json.loads(cleaned_content) |
|
|
structured_result = format_analysis_result(result_json) |
|
|
status = "✅ 分析完成!" |
|
|
yield structured_result, raw_json, status |
|
|
|
|
|
|
|
|
except APITimeoutError: |
|
|
status = "❌ 分析超时" |
|
|
structured_result = "⚠️ API调用超时(超过30秒)\n\n可能原因:\n1. 网络不稳定\n2. 模型负载较高\n建议:检查网络后重试" |
|
|
yield structured_result, raw_json, status |
|
|
except APIError as e: |
|
|
status = "❌ API错误" |
|
|
structured_result = f"⚠️ 百度千帆API错误\n\n错误详情:\n{str(e)}\n\n建议:检查API密钥是否正确,或前往百度智能云控制台确认服务状态" |
|
|
yield structured_result, raw_json, status |
|
|
except json.JSONDecodeError as e: |
|
|
status = "❌ 格式错误" |
|
|
structured_result = f"⚠️ AI返回结果非标准JSON\n\n原始内容:\n{cleaned_content[:500]}...\n\n错误详情:{str(e)}" |
|
|
yield structured_result, cleaned_content, status |
|
|
except Exception as e: |
|
|
status = "❌ 分析失败" |
|
|
structured_result = f"⚠️ 分析过程出错\n\n错误详情:\n{str(e)}\n\n建议:检查图片格式(仅支持JPG/PNG)或重试" |
|
|
yield structured_result, raw_json, status |
|
|
|
|
|
|
|
|
with gr.Blocks(title="智能体截图分析工具", theme=gr.themes.Soft()) as demo: |
|
|
|
|
|
raw_json_store = gr.State("") |
|
|
|
|
|
|
|
|
logo_base64 = read_base64_from_file("logo.txt") |
|
|
demo_base64 = read_base64_from_file("demo.txt") |
|
|
logo_image = base64_to_pil_image(logo_base64) |
|
|
demo_image = base64_to_pil_image(demo_base64) |
|
|
|
|
|
|
|
|
with gr.Row(elem_id="logo_row", visible=logo_image is not None): |
|
|
gr.Image( |
|
|
value=logo_image, |
|
|
height=50, width=165, |
|
|
interactive=False, |
|
|
show_label=False, |
|
|
show_download_button=False |
|
|
) |
|
|
if logo_image is None: |
|
|
gr.Markdown("# 智能体截图分析工具", elem_id="fallback_title") |
|
|
|
|
|
|
|
|
gr.Markdown("### 功能说明", visible=logo_image is not None) |
|
|
gr.Markdown(""" |
|
|
上传智能体网页截图(JPG/PNG),系统将自动完成: |
|
|
1. 页面质量评分(整体评价、设计美感等5个维度) |
|
|
2. 智能体能力拆解(核心功能、优势、潜在用途等) |
|
|
3. 生成结构化报告,支持下载原始JSON结果 |
|
|
""", elem_id="description") |
|
|
|
|
|
|
|
|
with gr.Row(elem_id="main_content", variant="panel"): |
|
|
|
|
|
with gr.Column(scale=1, elem_id="upload_col"): |
|
|
input_image = gr.Image( |
|
|
type="filepath", |
|
|
label="上传截图", |
|
|
height=300, |
|
|
show_label=True, |
|
|
elem_id="image_upload" |
|
|
) |
|
|
|
|
|
|
|
|
example_images = [demo_image] if demo_image is not None else [] |
|
|
if example_images: |
|
|
gr.Examples( |
|
|
examples=example_images, |
|
|
inputs=input_image, |
|
|
label="示例截图(点击可直接使用)", |
|
|
elem_id="examples_box" |
|
|
) |
|
|
|
|
|
analyze_btn = gr.Button( |
|
|
"开始分析", |
|
|
variant="primary", |
|
|
size="lg", |
|
|
elem_id="analyze_btn", |
|
|
interactive=PIL_AVAILABLE |
|
|
) |
|
|
|
|
|
if not PIL_AVAILABLE: |
|
|
gr.Markdown("⚠️ 未安装pillow库,无法处理图片\n请执行 `pip install pillow` 后重启应用", elem_id="dependency_warning") |
|
|
|
|
|
|
|
|
with gr.Column(scale=2, elem_id="result_col", variant="panel"): |
|
|
|
|
|
status_display = gr.Textbox( |
|
|
label="当前状态", |
|
|
interactive=False, |
|
|
value="就绪:请上传截图并点击「开始分析」", |
|
|
elem_id="status_box", |
|
|
max_lines=2 |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tabs(elem_id="result_tabs"): |
|
|
|
|
|
with gr.Tab("结构化分析报告", elem_id="structured_tab"): |
|
|
structured_result = gr.Markdown( |
|
|
value=""" |
|
|
# 等待分析... |
|
|
|
|
|
## 操作指引 |
|
|
1. 点击左侧「上传截图」区域,选择智能体网页的JPG/PNG图片 |
|
|
2. 点击「开始分析」按钮,等待3-10秒 |
|
|
3. 分析完成后,此处将显示结构化报告 |
|
|
|
|
|
## 示例图说明 |
|
|
若左侧有示例图片,可直接点击示例快速测试 |
|
|
""", |
|
|
elem_id="structured_result" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Tab("原始JSON结果", elem_id="raw_json_tab"): |
|
|
raw_json_result = gr.Textbox( |
|
|
label=None, |
|
|
lines=20, |
|
|
placeholder="分析完成后,此处将显示格式化的原始JSON结果...", |
|
|
elem_id="raw_json_box", |
|
|
container=True |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(elem_id="action_buttons"): |
|
|
download_btn = gr.DownloadButton( |
|
|
"下载JSON结果", |
|
|
label=None, |
|
|
elem_id="download_btn", |
|
|
visible=False |
|
|
) |
|
|
clear_btn = gr.Button( |
|
|
"清除结果", |
|
|
variant="secondary", |
|
|
size="sm", |
|
|
elem_id="clear_btn" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
analyze_btn.click( |
|
|
fn=analyze_image, |
|
|
inputs=input_image, |
|
|
outputs=[structured_result, raw_json_result, status_display] |
|
|
).then( |
|
|
|
|
|
fn=lambda raw_json, status: ( |
|
|
raw_json, |
|
|
gr.update(visible=status.startswith("✅")) |
|
|
), |
|
|
inputs=[raw_json_result, status_display], |
|
|
outputs=[raw_json_store, download_btn] |
|
|
) |
|
|
|
|
|
|
|
|
download_btn.click( |
|
|
fn=lambda result: (result, "analysis_result.json"), |
|
|
inputs=raw_json_store, |
|
|
outputs=download_btn, |
|
|
show_progress=False |
|
|
) |
|
|
|
|
|
|
|
|
clear_btn.click( |
|
|
fn=lambda: ( |
|
|
""" |
|
|
# 等待分析... |
|
|
|
|
|
## 操作指引 |
|
|
1. 点击左侧「上传截图」区域,选择智能体网页的JPG/PNG图片 |
|
|
2. 点击「开始分析」按钮,等待3-10秒 |
|
|
3. 分析完成后,此处将显示结构化报告 |
|
|
|
|
|
## 示例图说明 |
|
|
若左侧有示例图片,可直接点击示例快速测试 |
|
|
""", |
|
|
"", |
|
|
"就绪:请上传截图并点击「开始分析」", |
|
|
gr.update(visible=False), |
|
|
"" |
|
|
), |
|
|
outputs=[structured_result, raw_json_result, status_display, download_btn, raw_json_store] |
|
|
) |
|
|
|
|
|
|
|
|
demo.load( |
|
|
None, |
|
|
None, |
|
|
None, |
|
|
js="""() => { |
|
|
// 结果区整体样式 |
|
|
const resultCol = document.getElementById('result_col'); |
|
|
if (resultCol) { |
|
|
resultCol.style.padding = '20px'; |
|
|
resultCol.style.borderRadius = '8px'; |
|
|
} |
|
|
|
|
|
// 状态框样式和动态颜色 |
|
|
const statusBox = document.getElementById('status_box'); |
|
|
if (statusBox) { |
|
|
statusBox.style.marginBottom = '15px'; |
|
|
statusBox.style.padding = '8px'; |
|
|
|
|
|
// 定时检查状态文本并更新颜色 |
|
|
setInterval(() => { |
|
|
const statusText = statusBox.value || ''; |
|
|
if (typeof statusText === 'string') { |
|
|
if (statusText.startsWith('✅')) statusBox.style.color = '#27ae60'; |
|
|
else if (statusText.startsWith('❌')) statusBox.style.color = '#e74c3c'; |
|
|
else if (statusText.includes('分析中') || statusText.includes('🔄')) statusBox.style.color = '#3498db'; |
|
|
else statusBox.style.color = '#34495e'; |
|
|
} |
|
|
}, 100); |
|
|
} |
|
|
|
|
|
// 按钮区样式 |
|
|
const actionButtons = document.getElementById('action_buttons'); |
|
|
if (actionButtons) { |
|
|
actionButtons.style.marginTop = '15px'; |
|
|
actionButtons.style.gap = '10px'; |
|
|
} |
|
|
|
|
|
// 结构化报告样式优化 |
|
|
const structuredResult = document.getElementById('structured_result'); |
|
|
if (structuredResult) { |
|
|
structuredResult.style.padding = '10px 0'; |
|
|
} |
|
|
}""" |
|
|
) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
try: |
|
|
|
|
|
required_imports = { |
|
|
"gradio": "gradio", |
|
|
"openai": "openai", |
|
|
"PIL": "pillow", |
|
|
"requests": "requests" |
|
|
} |
|
|
|
|
|
for import_name, package_name in required_imports.items(): |
|
|
try: |
|
|
__import__(import_name) |
|
|
except ImportError: |
|
|
raise ImportError(package_name) |
|
|
|
|
|
|
|
|
demo.launch( |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
) |
|
|
except ImportError as e: |
|
|
logger.critical(f"缺少依赖包:请执行 `pip install {e.args[0]}` 安装") |
|
|
except Exception as e: |
|
|
logger.critical(f"应用启动失败:{str(e)}") |
|
|
|