Spaces:
Running
on
Zero
Running
on
Zero
import gradio as gr | |
import torch | |
from PIL import Image | |
from transformers import AutoModel, AutoTokenizer | |
import warnings | |
import os | |
import spaces | |
# 禁用警告信息 | |
warnings.filterwarnings("ignore") | |
# 全局变量存储模型 | |
model = None | |
tokenizer = None | |
def load_model(): | |
"""加载MiniCPM-o模型""" | |
global model, tokenizer | |
if model is None: | |
print("正在加载MiniCPM-o模型...") | |
device = "cuda" if torch.cuda.is_available() else "cpu" | |
model = AutoModel.from_pretrained( | |
'openbmb/MiniCPM-o-2_6', | |
trust_remote_code=True, | |
torch_dtype=torch.bfloat16 if device == "cuda" else torch.float32, | |
device_map="auto" if device == "cuda" else None, | |
init_vision=True, | |
init_audio=False, | |
init_tts=False | |
) | |
model = model.eval().to(device) | |
tokenizer = AutoTokenizer.from_pretrained('openbmb/MiniCPM-o-2_6', trust_remote_code=True) | |
print("模型加载完成") | |
return model, tokenizer | |
def clean_markdown_output(text): | |
"""清理输出文本,只保留markdown表格""" | |
lines = text.strip().split('\n') | |
markdown_lines = [] | |
# 查找markdown表格的开始和结束 | |
in_table = False | |
for line in lines: | |
line = line.strip() | |
# 检查是否是表格行(包含|符号) | |
if '|' in line and not line.startswith('```'): | |
in_table = True | |
markdown_lines.append(line) | |
elif in_table and line == '': | |
# 空行可能表示表格结束 | |
break | |
elif in_table and not line.startswith('```'): | |
# 继续收集表格相关行 | |
markdown_lines.append(line) | |
# 如果没有找到表格,返回原始清理后的文本 | |
if not markdown_lines: | |
# 移除代码块标记和多余的说明文字 | |
cleaned_text = text.replace('```markdown', '').replace('```', '').strip() | |
# 移除常见的解释性文字 | |
lines = cleaned_text.split('\n') | |
result_lines = [] | |
for line in lines: | |
line = line.strip() | |
if line and not line.startswith('这个表格') and not line.startswith('该表格') and not line.startswith('表格显示'): | |
result_lines.append(line) | |
return '\n'.join(result_lines) | |
return '\n'.join(markdown_lines) | |
def clean_formula_output(text): | |
"""清理输出文本,只保留LaTeX公式""" | |
lines = text.strip().split('\n') | |
formula_lines = [] | |
for line in lines: | |
line = line.strip() | |
# 跳过解释性文字 | |
if line and not any(line.startswith(prefix) for prefix in [ | |
'这个公式', '该公式', '公式表示', '根据图片', '图片中的', '识别结果' | |
]): | |
# 保留包含LaTeX语法的行 | |
if any(symbol in line for symbol in ['$', '\\', '{', '}', '^', '_']) or '=' in line: | |
formula_lines.append(line) | |
# 或者保留纯数学表达式 | |
elif any(char.isdigit() or char in '+-*/=()[]{}^_' for char in line): | |
formula_lines.append(line) | |
# 如果没有找到公式,返回原始清理后的文本 | |
if not formula_lines: | |
cleaned_text = text.replace('```latex', '').replace('```', '').strip() | |
lines = cleaned_text.split('\n') | |
result_lines = [] | |
for line in lines: | |
line = line.strip() | |
if line and not any(line.startswith(prefix) for prefix in [ | |
'这个公式', '该公式', '公式表示', '根据图片', '图片中的' | |
]): | |
result_lines.append(line) | |
return '\n'.join(result_lines) | |
return '\n'.join(formula_lines) | |
def clean_text_output(text): | |
"""清理输出文本,只保留识别的文字内容""" | |
# 移除代码块标记 | |
cleaned_text = text.replace('```text', '').replace('```', '').strip() | |
lines = cleaned_text.split('\n') | |
text_lines = [] | |
for line in lines: | |
line = line.strip() | |
# 跳过解释性文字和标签信息 | |
if line and not any(line.startswith(prefix) for prefix in [ | |
'图片中的文字', '识别结果', '文字内容', '根据图片', '这张图片', '该图片', | |
'标题:', '正文:', '内容:', '文本:', '题目:', '段落:', '文字:' | |
]): | |
# 移除行首的标签格式(如 "标题:内容" -> "内容") | |
if ':' in line: | |
# 检查是否是标签格式 | |
parts = line.split(':', 1) | |
if len(parts) == 2 and len(parts[0]) <= 10: # 标签通常很短 | |
# 可能的标签词 | |
label_keywords = ['标题', '正文', '内容', '文本', '题目', '段落', '文字', '主题', '副标题'] | |
if any(keyword in parts[0] for keyword in label_keywords): | |
# 只保留标签后的内容 | |
text_lines.append(parts[1].strip()) | |
else: | |
# 不是标签格式,保留整行 | |
text_lines.append(line) | |
else: | |
text_lines.append(line) | |
else: | |
text_lines.append(line) | |
return '\n'.join(text_lines) | |
def parse_image(image, parse_type): | |
"""解析图片内容为指定格式""" | |
try: | |
# 确保模型已加载 | |
model, tokenizer = load_model() | |
if image is None: | |
return "请上传一张图片", "" | |
# 转换图片格式 | |
if isinstance(image, str): | |
image = Image.open(image).convert('RGB') | |
elif hasattr(image, 'convert'): | |
image = image.convert('RGB') | |
# 根据解析类型设置不同的提示词 | |
questions = { | |
"表格解析": "解析一下这个表格为markdown格式,不需要任何解释和思考,直接输出markdown格式", | |
"公式解析": "识别并提取图片中的数学公式,用LaTeX格式输出,不需要任何解释,直接输出公式", | |
"文本解析": "识别并提取图片中的所有文字内容,保持原有格式,不需要任何解释,直接输出文字内容" | |
} | |
question = questions.get(parse_type, questions["表格解析"]) | |
msgs = [{'role': 'user', 'content': [image, question]}] | |
# 使用流式输出获取结果 | |
res = model.chat( | |
msgs=msgs, | |
tokenizer=tokenizer, | |
sampling=True, | |
stream=True | |
) | |
# 收集所有输出文本 | |
generated_text = "" | |
for new_text in res: | |
generated_text += new_text | |
# 根据类型清理输出 | |
if parse_type == "表格解析": | |
result = clean_markdown_output(generated_text) | |
output_format = "Markdown" | |
elif parse_type == "公式解析": | |
result = clean_formula_output(generated_text) | |
output_format = "LaTeX" | |
elif parse_type == "文本解析": | |
result = clean_text_output(generated_text) | |
output_format = "纯文本" | |
else: | |
result = generated_text.strip() | |
output_format = "原始输出" | |
return result, f"解析完成 - 输出格式: {output_format}" | |
except Exception as e: | |
return f"解析失败: {str(e)}", "错误" | |
def create_interface(): | |
"""创建Gradio界面""" | |
# 自定义CSS样式 | |
css = """ | |
.gradio-container { | |
font-family: 'Helvetica Neue', Arial, sans-serif; | |
} | |
.output-text { | |
font-family: 'Courier New', monospace; | |
font-size: 14px; | |
} | |
""" | |
with gr.Blocks(css=css, title="MiniCPM 多模态内容解析工具", analytics_enabled=False) as interface: | |
gr.Markdown(""" | |
# 🚀 MiniCPM 多模态内容解析工具 | |
基于MiniCPM-o多模态模型的智能图片内容解析工具,支持表格、公式、文本三种解析模式。 | |
## 📋 使用说明 | |
1. **上传图片**: 支持 PNG、JPG、JPEG 等格式 | |
2. **选择解析类型**: 根据图片内容选择相应的解析模式 | |
3. **获取结果**: 自动清理输出,获得纯净的解析结果 | |
## 🎯 解析类型说明 | |
- **📊 表格解析**: 将表格图片转换为Markdown格式 | |
- **🧮 公式解析**: 识别数学公式并输出LaTeX格式 | |
- **📝 文本解析**: 提取图片中的所有文字内容 | |
""") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
# 输入组件 | |
image_input = gr.Image( | |
label="📷 上传图片", | |
type="pil", | |
height=400 | |
) | |
parse_type = gr.Radio( | |
choices=["表格解析", "公式解析", "文本解析"], | |
value="表格解析", | |
label="🎛️ 选择解析类型", | |
info="根据图片内容选择合适的解析模式" | |
) | |
parse_button = gr.Button( | |
"🔍 开始解析", | |
variant="primary", | |
size="lg" | |
) | |
with gr.Column(scale=1): | |
# 输出组件 | |
status_output = gr.Textbox( | |
label="📊 解析状态", | |
value="等待上传图片...", | |
interactive=False | |
) | |
result_output = gr.Textbox( | |
label="📄 解析结果", | |
lines=20, | |
max_lines=30, | |
show_copy_button=True, | |
elem_classes=["output-text"], | |
placeholder="解析结果将在这里显示...", | |
interactive=True | |
) | |
# 示例图片 | |
gr.Markdown("## 📖 示例图片") | |
with gr.Row(): | |
gr.Examples( | |
examples=[ | |
["./table.png", "表格解析"], | |
["./formulas.png", "公式解析"], | |
["./text.png", "文本解析"] | |
], | |
inputs=[image_input, parse_type], | |
label="点击示例快速体验", | |
cache_examples=False | |
) | |
# 绑定事件 | |
parse_button.click( | |
fn=parse_image, | |
inputs=[image_input, parse_type], | |
outputs=[result_output, status_output] | |
) | |
# 添加页脚信息 | |
gr.Markdown(""" | |
--- | |
### 💡 使用提示 | |
- 确保图片清晰,内容结构明显 | |
- 复杂表格建议分段处理 | |
- 公式图片建议使用高分辨率 | |
- 文字图片避免模糊、倾斜或光线不足 | |
### 🔧 技术支持 | |
- 模型: MiniCPM-o-2.6 | |
- 框架: Gradio + Transformers | |
- GPU: CUDA加速推理 | |
""") | |
return interface | |
if __name__ == "__main__": | |
# 在ZeroGPU环境中不预加载模型,按需加载以节省资源 | |
print("🚀 启动MiniCPM多模态内容解析工具") | |
print("📝 模型将在首次使用时自动加载") | |
# 创建并启动界面 | |
interface = create_interface() | |
interface.launch( | |
server_name="0.0.0.0", # 允许外部访问 | |
server_port=7860, # Hugging Face Spaces默认端口 | |
share=False, # 在Hugging Face上部署时设为False | |
show_error=True, # 显示详细错误信息 | |
quiet=False, # 显示启动信息 | |
debug=False, # 关闭调试模式 | |
) |