feat: 服务化改造STP到GLB转换器

- 🚀 新增FastAPI Web服务支持
-  实现异步任务处理和并发转换
- 📊 添加实时进度追踪(0-100%)
- 🏗️ 重构为模块化架构:core/api/services/utils
- 🔧 完整的任务管理系统和状态追踪
- 📖 自动生成API文档(Swagger/ReDoc)
- 🔄 保持CLI模式100%向后兼容
- 🛡️ 增强错误处理和文件验证
- 📝 更新完整文档(README/CLAUDE.md)

技术栈: FastAPI + uvicorn + pydantic + asyncio
API端点: /health, /api/v1/convert, /api/v1/status, /api/v1/tasks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root 2025-08-02 09:44:43 +08:00
parent 218d3bc474
commit 5102bd57e8
26 changed files with 958 additions and 63 deletions

View File

@ -4,27 +4,64 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 项目概述
这是一个Python工具用于将STP格式的3D模型文件转换为GLB格式。项目实现了STP → STL → GLB的转换流程。
这是一个Python服务用于将STP格式的3D模型文件转换为GLB格式。项目提供CLI和Web API两种使用方式实现了STP → STL → GLB的转换流程。
## 核心架构
- **main.py**: 单文件应用,包含完整的转换逻辑
- `stp2stl()`: 使用pythonocc-core读取STP文件并转换为二进制STL
- `stl2glb()`: 使用trimesh库将STL转换为GLB
- 主流程:处理命令行参数,执行转换,清理临时文件
### 模块化设计
- **main.py**: CLI入口保持向后兼容
- **app.py**: FastAPI服务入口
- **core/**: 核心转换引擎
- `converter.py`: 转换器类(封装原有逻辑)
- `models.py`: 数据模型定义
- **api/**: HTTP接口层
- `routes.py`: API路由定义
- `schemas.py`: API数据模式
- **services/**: 业务服务层
- `task_manager.py`: 异步任务管理
- **utils/**: 工具函数库
## 依赖要求
- pythonocc-core: 用于STP/STEP文件读取和STL写入
- trimesh: 用于STL到GLB格式转换
- fastapi: Web框架
- uvicorn: ASGI服务器
- pydantic: 数据验证
## 常用命令
### 运行转换
### CLI模式转换
```bash
python main.py input.stp output.glb
```
### 启动Web服务
```bash
# 开发模式
python app.py
# 或使用uvicorn
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
# 生产模式
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
```
### API使用示例
```bash
# 健康检查
curl http://localhost:8000/health
# 提交转换任务
curl -X POST http://localhost:8000/api/v1/convert \
-H "Content-Type: application/json" \
-d '{"input_path": "/path/to/input.stp", "output_path": "/path/to/output.glb"}'
# 查询任务状态
curl http://localhost:8000/api/v1/status/{task_id}
```
### 测试转换
```bash
python main.py test.stp test.glb
@ -32,13 +69,27 @@ python main.py test.stp test.glb
## 关键技术细节
1. **转换流程**: STP → STL临时文件 → GLB → 清理STL临时文件
2. **STL设置**: 使用二进制模式线性偏差0.01角度偏差0.1
3. **GLB转换**: 使用trimesh库直接处理STL到GLB转换
4. **自动优化**:
- 智能缩放超过1000单位的模型自动缩放到50单位以内
- 自动居中模型质心移动到原点便于Blender查看
5. **错误处理**: 包含文件存在检查、网格验证和临时文件清理
### 转换流程
1. **STP → STL**: 使用pythonocc-core读取STP文件生成临时STL文件
2. **STL → GLB**: 使用trimesh库将STL转换为GLB格式
3. **自动清理**: 转换完成后自动删除临时STL文件
### 服务化特性
1. **异步处理**: 使用asyncio实现并发转换任务
2. **进度追踪**: 实时更新转换进度(0-100%)
3. **任务管理**: 内存存储任务状态,支持查询和管理
4. **RESTful API**: 标准HTTP接口支持跨语言调用
### 转换配置
- **STL设置**: 二进制模式线性偏差0.01角度偏差0.1
- **自动优化**:
- 智能缩放超过1000单位的模型自动缩放到50单位以内
- 自动居中模型质心移动到原点便于Blender查看
### 错误处理
- 完整的文件验证和网格检查
- 异常捕获和错误信息反馈
- 临时文件自动清理机制
## 重要注意事项

228
README.md Normal file
View File

@ -0,0 +1,228 @@
# STP to GLB Converter Service
一个强大的STP到GLB转换服务提供CLI和Web API两种使用方式特别优化了在Blender中的显示效果。
## 功能特性
- **双模式支持**: CLI命令行工具 + Web API服务
- **异步处理**: 支持并发转换请求,实时进度追踪
- **格式转换**: STP → STL → GLB 完整转换流程
- **智能缩放**: 自动将过大模型缩放到适合3D软件的尺寸
- **自动居中**: 将模型质心移动到原点,便于查看
- **RESTful API**: 标准HTTP接口支持跨语言调用
- **任务管理**: 完整的任务生命周期管理和状态追踪
- **样式兼容**: 正确处理STP文件中的样式信息不影响几何转换
- **Blender优化**: 生成的GLB文件可直接在Blender中正常显示
## 系统要求
- Python 3.7+
- Windows/Linux/macOS
## 安装步骤
### 使用requirements.txt推荐
```bash
# 克隆或下载项目
git clone <项目地址>
cd TellmeStpToGlb
# 安装依赖
pip install -r requirements.txt
```
### 手动安装依赖
```bash
# 安装所有依赖
pip install pythonocc-core trimesh fastapi uvicorn pydantic
```
### 方法三使用conda可选
```bash
# 创建虚拟环境
conda create -n stp2glb python=3.9
conda activate stp2glb
# 安装pythonocc-core
conda install -c conda-forge pythonocc-core
# 安装trimesh
pip install trimesh
```
## 使用方法
### CLI模式命令行
```bash
python main.py input.stp output.glb
```
#### CLI示例
```bash
# 转换单个文件
python main.py model.stp model.glb
# 转换测试文件
python main.py test.stp result.glb
```
### API模式Web服务
#### 启动服务
```bash
# 开发模式
python app.py
# 或使用uvicorn
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
# 生产模式
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
```
#### API接口
**健康检查**
```bash
GET http://localhost:8000/health
```
**提交转换任务**
```bash
POST http://localhost:8000/api/v1/convert
Content-Type: application/json
{
"input_path": "/path/to/input.stp",
"output_path": "/path/to/output.glb",
"options": {
"auto_scale": true,
"auto_center": true
}
}
```
**查询任务状态**
```bash
GET http://localhost:8000/api/v1/status/{task_id}
```
**获取任务列表**
```bash
GET http://localhost:8000/api/v1/tasks
```
#### API文档
服务启动后可访问:
- Swagger UI: `http://localhost:8000/docs`
- ReDoc: `http://localhost:8000/redoc`
## 在Blender中使用
1. 打开Blender
2. 删除默认立方体(可选)
3. 文件 → 导入 → glTF 2.0 (.glb/.gltf)
4. 选择转换生成的GLB文件
5. 模型应该在视口中心正常显示
## 技术细节
### 转换流程
1. **STP读取**: 使用pythonocc-core读取STP文件
2. **STL生成**: 转换为高质量STL网格线性偏差0.01
3. **智能处理**: 自动缩放和居中处理
4. **GLB导出**: 使用trimesh生成最终GLB文件
5. **清理**: 自动删除临时STL文件
### 服务化特性
- **异步处理**: 使用asyncio实现并发转换任务
- **进度追踪**: 实时更新转换进度(0-100%)
- **任务管理**: 内存存储任务状态,支持查询和管理
- **RESTful API**: 标准HTTP接口支持跨语言调用
- **错误处理**: 完整的异常捕获和错误信息反馈
### 智能优化
- **尺寸检测**: 自动检测模型最大尺寸
- **智能缩放**: 超过1000单位的模型自动缩放到50单位以内
- **质心居中**: 模型自动移动到坐标原点
## 故障排除
### 常见问题
**1. 导入错误**
```
ImportError: cannot import name 'xxx' from 'OCC.Core'
```
- 解决方案: 确保安装了正确版本的pythonocc-core
- 使用conda安装: `conda install -c conda-forge pythonocc-core`
**2. 样式警告**
```
OVER_RIDING_STYLED_ITEM warnings
```
- 这是正常现象,不影响几何转换
- 警告信息可以忽略
**3. Blender中看不见模型**
- 本工具已自动处理此问题
- 如仍有问题在Blender中按数字键盘 `.` 键聚焦到对象
**4. 模型太大或太小**
- 工具已内置智能缩放功能
- 大模型会自动缩放到合适尺寸
### 依赖问题
如果遇到依赖安装问题:
```bash
# 升级pip
pip install --upgrade pip
# 清理缓存重新安装
pip cache purge
pip install -r requirements.txt --no-cache-dir
```
## 支持的格式
- **输入**: STP/STEP文件
- **输出**: GLB文件glTF 2.0二进制格式)
## 性能说明
- 转换速度取决于模型复杂度
- 大模型(几万面片)通常需要几秒到几十秒
- 内存使用量与模型大小成正比
## 许可证
本项目使用MIT许可证详见LICENSE文件。
## 贡献
欢迎提交Issue和Pull Request来改进这个工具。
## 更新日志
### v2.0.0
- 🚀 服务化改造支持Web API
- ⚡ 异步处理和并发转换
- 📊 实时进度追踪
- 🔧 任务管理系统
- 📖 自动生成API文档
- 🔄 保持CLI模式向后兼容
### v1.0.0
- 初始版本发布
- 支持STP到GLB转换
- 智能缩放和居中功能
- Blender显示优化

Binary file not shown.

0
api/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

134
api/routes.py Normal file
View File

@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
API路由定义
"""
import os
from fastapi import APIRouter, HTTPException, BackgroundTasks
from api.schemas import (
ConvertRequestSchema,
ConvertResponseSchema,
TaskStatusSchema,
HealthResponseSchema
)
from services.task_manager import task_manager
from core.models import ConvertOptions, TaskStatus
router = APIRouter()
@router.get("/health", response_model=HealthResponseSchema)
async def health_check():
"""健康检查接口"""
return HealthResponseSchema()
@router.post("/api/v1/convert", response_model=ConvertResponseSchema)
async def convert_file(request: ConvertRequestSchema):
"""转换文件接口"""
# 验证输入文件是否存在
if not os.path.isfile(request.input_path):
raise HTTPException(status_code=400, detail=f"输入文件不存在: {request.input_path}")
# 验证输入文件扩展名
if not request.input_path.lower().endswith(('.stp', '.step')):
raise HTTPException(status_code=400, detail="输入文件必须是STP或STEP格式")
# 验证输出文件扩展名
if not request.output_path.lower().endswith('.glb'):
raise HTTPException(status_code=400, detail="输出文件必须是GLB格式")
# 确保输出目录存在
output_dir = os.path.dirname(request.output_path)
if output_dir and not os.path.exists(output_dir):
try:
os.makedirs(output_dir, exist_ok=True)
except Exception as e:
raise HTTPException(status_code=400, detail=f"无法创建输出目录: {str(e)}")
# 创建转换选项
options = ConvertOptions(
auto_scale=request.options.auto_scale,
auto_center=request.options.auto_center
)
# 创建任务
task = task_manager.create_task(
input_path=request.input_path,
output_path=request.output_path,
options=options
)
# 启动任务
success = await task_manager.start_task(task.task_id)
if not success:
raise HTTPException(status_code=500, detail="任务启动失败")
return ConvertResponseSchema(
task_id=task.task_id,
status=task.status.value,
message="任务已创建并开始处理"
)
@router.get("/api/v1/status/{task_id}", response_model=TaskStatusSchema)
async def get_task_status(task_id: str):
"""获取任务状态接口"""
task = task_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
return TaskStatusSchema(
task_id=task.task_id,
status=task.status.value,
progress=task.progress,
message=task.message,
created_at=task.created_at,
started_at=task.started_at,
completed_at=task.completed_at,
error_message=task.error_message
)
@router.delete("/api/v1/task/{task_id}")
async def cancel_task(task_id: str):
"""取消任务接口"""
task = task_manager.get_task(task_id)
if not task:
raise HTTPException(status_code=404, detail="任务不存在")
if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
raise HTTPException(status_code=400, detail="任务已完成,无法取消")
success = task_manager.cancel_task(task_id)
if success:
return {"message": "任务已取消"}
else:
return {"message": "任务未在运行中"}
@router.get("/api/v1/tasks")
async def list_tasks():
"""获取所有任务列表接口"""
tasks = task_manager.get_all_tasks()
task_list = []
for task_id, task in tasks.items():
task_list.append({
"task_id": task.task_id,
"status": task.status.value,
"progress": task.progress,
"input_path": task.input_path,
"output_path": task.output_path,
"created_at": task.created_at,
"message": task.message
})
# 按创建时间倒序排列
task_list.sort(key=lambda x: x["created_at"], reverse=True)
return {"tasks": task_list, "total": len(task_list)}

47
api/schemas.py Normal file
View File

@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""
API数据模式定义
"""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
class ConvertOptionsSchema(BaseModel):
"""转换选项模式"""
auto_scale: bool = Field(default=True, description="是否自动缩放")
auto_center: bool = Field(default=True, description="是否自动居中")
class ConvertRequestSchema(BaseModel):
"""转换请求模式"""
input_path: str = Field(..., description="输入STP文件路径")
output_path: str = Field(..., description="输出GLB文件路径")
options: ConvertOptionsSchema = Field(default_factory=ConvertOptionsSchema, description="转换选项")
class ConvertResponseSchema(BaseModel):
"""转换响应模式"""
task_id: str = Field(..., description="任务ID")
status: str = Field(..., description="任务状态")
message: str = Field(default="", description="状态消息")
class TaskStatusSchema(BaseModel):
"""任务状态模式"""
task_id: str = Field(..., description="任务ID")
status: str = Field(..., description="任务状态")
progress: int = Field(..., description="进度百分比(0-100)")
message: str = Field(default="", description="状态消息")
created_at: datetime = Field(..., description="创建时间")
started_at: Optional[datetime] = Field(default=None, description="开始时间")
completed_at: Optional[datetime] = Field(default=None, description="完成时间")
error_message: Optional[str] = Field(default=None, description="错误信息")
class HealthResponseSchema(BaseModel):
"""健康检查响应模式"""
status: str = Field(default="healthy", description="服务状态")
timestamp: datetime = Field(default_factory=datetime.now, description="检查时间")
version: str = Field(default="1.0.0", description="服务版本")

88
app.py Normal file
View File

@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""
FastAPI应用入口
STP到GLB转换服务
"""
import asyncio
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from api.routes import router
from services.task_manager import task_manager
@asynccontextmanager
async def lifespan(app: FastAPI):
"""应用生命周期管理"""
# 启动时的初始化
print("🚀 STP到GLB转换服务启动中...")
# 启动定期清理任务
cleanup_task = asyncio.create_task(periodic_cleanup())
yield
# 关闭时的清理
print("🛑 STP到GLB转换服务关闭中...")
cleanup_task.cancel()
try:
await cleanup_task
except asyncio.CancelledError:
pass
async def periodic_cleanup():
"""定期清理已完成的任务"""
while True:
try:
await asyncio.sleep(300) # 每5分钟清理一次
task_manager.cleanup_completed_tasks(max_tasks=100)
except asyncio.CancelledError:
break
except Exception as e:
print(f"定期清理任务出错: {e}")
# 创建FastAPI应用
app = FastAPI(
title="STP到GLB转换服务",
description="提供STP格式到GLB格式的3D模型转换服务",
version="1.0.0",
lifespan=lifespan
)
# 配置CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 在生产环境中应该限制具体的域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 注册路由
app.include_router(router)
# 全局异常处理
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
"""全局异常处理器"""
print(f"未处理的异常: {exc}")
return HTTPException(status_code=500, detail="服务器内部错误")
if __name__ == "__main__":
import uvicorn
print("启动STP到GLB转换服务...")
print("API文档地址: http://localhost:8000/docs")
print("健康检查: http://localhost:8000/health")
uvicorn.run(
"app:app",
host="0.0.0.0",
port=8000,
reload=True, # 开发模式生产环境应设为False
log_level="info"
)

0
core/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

98
core/converter.py Normal file
View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
STP到GLB转换器类
封装原有转换逻辑支持进度回调
"""
import os
import trimesh
from typing import Callable, Optional
from OCC.Extend.DataExchange import read_step_file, write_stl_file
class StpToGlbConverter:
"""STP到GLB转换器"""
def __init__(self):
self.progress_callback: Optional[Callable[[int, str], None]] = None
def set_progress_callback(self, callback: Callable[[int, str], None]) -> None:
"""设置进度回调函数"""
self.progress_callback = callback
def _update_progress(self, progress: int, message: str) -> None:
"""更新进度"""
if self.progress_callback:
self.progress_callback(progress, message)
def stp_to_stl(self, stp_path: str, stl_path: str) -> None:
"""pythonocc 读 STP 写二进制 STL"""
self._update_progress(5, "检查输入文件...")
if not os.path.isfile(stp_path):
raise FileNotFoundError(f"找不到文件:{stp_path}")
self._update_progress(20, "读取STP文件...")
shape = read_step_file(stp_path)
if shape is None:
raise ValueError(f"无法读取STP文件{stp_path}")
self._update_progress(40, "转换为STL格式...")
write_stl_file(shape, stl_path, mode="binary", linear_deflection=0.01, angular_deflection=0.1)
self._update_progress(60, "STL文件生成完成")
def stl_to_glb(self, stl_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True) -> None:
"""使用 trimesh 转换 STL 到 GLB并优化用于Blender"""
self._update_progress(65, "检查STL文件...")
if not os.path.isfile(stl_path):
raise FileNotFoundError(f"找不到STL文件{stl_path}")
self._update_progress(70, "加载STL网格...")
mesh = trimesh.load(stl_path)
if mesh.is_empty:
raise ValueError(f"STL文件为空或无效{stl_path}")
if len(mesh.vertices) == 0 or len(mesh.faces) == 0:
raise ValueError("网格数据无效")
self._update_progress(80, "优化网格...")
# 自动缩放和居中
if auto_scale or auto_center:
bounds = mesh.bounds
max_dimension = max(bounds[1] - bounds[0])
if auto_scale and max_dimension > 1000:
scale_factor = 50.0 / max_dimension
mesh.apply_scale(scale_factor)
if auto_center:
mesh.apply_translation(-mesh.centroid)
self._update_progress(90, "导出GLB文件...")
try:
mesh.export(glb_path)
if os.path.getsize(glb_path) == 0:
raise ValueError("生成的GLB文件为空")
except Exception as e:
raise ValueError(f"GLB导出失败{e}")
self._update_progress(100, "转换完成")
def convert(self, stp_path: str, glb_path: str, auto_scale: bool = True, auto_center: bool = True) -> None:
"""完整的STP到GLB转换流程"""
stl_tmp = os.path.splitext(glb_path)[0] + ".tmp.stl"
try:
self._update_progress(0, "开始转换...")
self.stp_to_stl(stp_path, stl_tmp)
self.stl_to_glb(stl_tmp, glb_path, auto_scale, auto_center)
finally:
# 清理临时文件
if os.path.isfile(stl_tmp):
os.remove(stl_tmp)

44
core/models.py Normal file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
核心数据模型定义
"""
from enum import Enum
from dataclasses import dataclass
from typing import Optional
from datetime import datetime
class TaskStatus(Enum):
"""任务状态枚举"""
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"
@dataclass
class ConvertOptions:
"""转换选项"""
auto_scale: bool = True
auto_center: bool = True
@dataclass
class ConvertTask:
"""转换任务"""
task_id: str
input_path: str
output_path: str
options: ConvertOptions
status: TaskStatus = TaskStatus.PENDING
progress: int = 0
message: str = ""
created_at: datetime = None
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
error_message: Optional[str] = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now()

70
main.py
View File

@ -2,67 +2,37 @@
"""
stp2glb.py 一键 STP STL GLB
依赖pythonocc-core + trimesh
用法python stp2glb.py model.stp model.glb
用法python main.py model.stp model.glb
"""
import os
import sys
import trimesh
from OCC.Extend.DataExchange import read_step_file, write_stl_file
from core.converter import StpToGlbConverter
def stp2stl(stp_path, stl_path):
"""pythonocc 读 STP 写二进制 STL"""
if not os.path.isfile(stp_path):
sys.exit(f"❌ 找不到文件:{stp_path}")
shape = read_step_file(stp_path)
if shape is None:
sys.exit(f"❌ 无法读取STP文件{stp_path}")
write_stl_file(shape, stl_path, mode="binary", linear_deflection=0.01, angular_deflection=0.1)
def stl2glb(stl_path, glb_path):
"""使用 trimesh 转换 STL 到 GLB并优化用于Blender"""
if not os.path.isfile(stl_path):
sys.exit(f"❌ 找不到STL文件{stl_path}")
def main():
"""CLI主函数保持向后兼容"""
if len(sys.argv) != 3:
sys.exit("用法: python main.py input.stp output.glb")
mesh = trimesh.load(stl_path)
stp_file, glb_file = sys.argv[1], sys.argv[2]
if mesh.is_empty:
sys.exit(f"❌ STL文件为空或无效{stl_path}")
# 创建转换器实例
converter = StpToGlbConverter()
if len(mesh.vertices) == 0 or len(mesh.faces) == 0:
sys.exit(f"❌ 网格数据无效")
# 设置进度回调CLI模式显示简单进度
def show_progress(progress: int, message: str):
if progress % 20 == 0 or progress == 100: # 每20%显示一次
print(f"[{progress:3d}%] {message}")
# 自动缩放和居中
bounds = mesh.bounds
max_dimension = max(bounds[1] - bounds[0])
if max_dimension > 1000:
scale_factor = 50.0 / max_dimension
mesh.apply_scale(scale_factor)
mesh.apply_translation(-mesh.centroid)
converter.set_progress_callback(show_progress)
try:
mesh.export(glb_path)
if os.path.getsize(glb_path) == 0:
sys.exit(f"❌ 生成的GLB文件为空")
# 执行转换
converter.convert(stp_file, glb_file)
print(f"✅ 完成:{glb_file}")
except Exception as e:
sys.exit(f"❌ GLB导出失败{e}")
sys.exit(f"❌ 转换失败:{e}")
if __name__ == "__main__":
if len(sys.argv) != 3:
sys.exit("用法: python stp2glb.py input.stp output.glb")
stp_file, glb_file = sys.argv[1], sys.argv[2]
stl_tmp = os.path.splitext(glb_file)[0] + ".tmp.stl"
try:
stp2stl(stp_file, stl_tmp)
stl2glb(stl_tmp, glb_file)
finally:
if os.path.isfile(stl_tmp):
os.remove(stl_tmp)
print(f"✅ 完成:{glb_file}")
main()

BIN
output.glb Normal file

Binary file not shown.

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
pythonocc-core>=7.7.0
trimesh>=3.15.0
fastapi>=0.104.0
uvicorn[standard]>=0.24.0
pydantic>=2.5.0

0
services/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

142
services/task_manager.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
任务管理服务
负责任务的创建跟踪和状态管理
"""
import asyncio
import uuid
from datetime import datetime
from typing import Dict, Optional
from core.models import ConvertTask, TaskStatus, ConvertOptions
from core.converter import StpToGlbConverter
class TaskManager:
"""任务管理器"""
def __init__(self):
self.tasks: Dict[str, ConvertTask] = {}
self.running_tasks: Dict[str, asyncio.Task] = {}
def create_task(self, input_path: str, output_path: str, options: ConvertOptions) -> ConvertTask:
"""创建新任务"""
task_id = str(uuid.uuid4())
task = ConvertTask(
task_id=task_id,
input_path=input_path,
output_path=output_path,
options=options
)
self.tasks[task_id] = task
return task
def get_task(self, task_id: str) -> Optional[ConvertTask]:
"""获取任务"""
return self.tasks.get(task_id)
def get_all_tasks(self) -> Dict[str, ConvertTask]:
"""获取所有任务"""
return self.tasks.copy()
async def process_task(self, task_id: str) -> None:
"""处理转换任务"""
task = self.tasks.get(task_id)
if not task:
return
# 更新任务状态
task.status = TaskStatus.PROCESSING
task.started_at = datetime.now()
task.message = "开始处理转换任务"
converter = StpToGlbConverter()
# 设置进度回调
def progress_callback(progress: int, message: str):
task.progress = progress
task.message = message
converter.set_progress_callback(progress_callback)
try:
# 执行转换
await asyncio.get_event_loop().run_in_executor(
None,
converter.convert,
task.input_path,
task.output_path,
task.options.auto_scale,
task.options.auto_center
)
# 标记完成
task.status = TaskStatus.COMPLETED
task.completed_at = datetime.now()
task.progress = 100
task.message = "转换完成"
except Exception as e:
# 标记失败
task.status = TaskStatus.FAILED
task.completed_at = datetime.now()
task.error_message = str(e)
task.message = f"转换失败: {str(e)}"
finally:
# 清理运行任务记录
if task_id in self.running_tasks:
del self.running_tasks[task_id]
async def start_task(self, task_id: str) -> bool:
"""启动任务处理"""
if task_id not in self.tasks:
return False
if task_id in self.running_tasks:
return False # 任务已在运行
# 创建异步任务
asyncio_task = asyncio.create_task(self.process_task(task_id))
self.running_tasks[task_id] = asyncio_task
return True
def cancel_task(self, task_id: str) -> bool:
"""取消任务"""
if task_id in self.running_tasks:
self.running_tasks[task_id].cancel()
del self.running_tasks[task_id]
task = self.tasks.get(task_id)
if task:
task.status = TaskStatus.FAILED
task.error_message = "任务被取消"
task.message = "任务已取消"
task.completed_at = datetime.now()
return True
return False
def cleanup_completed_tasks(self, max_tasks: int = 100) -> None:
"""清理已完成的任务,保持任务数量在限制内"""
if len(self.tasks) <= max_tasks:
return
# 按完成时间排序,保留最新的任务
completed_tasks = [
(task_id, task) for task_id, task in self.tasks.items()
if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED] and task.completed_at
]
if len(completed_tasks) > max_tasks // 2:
# 删除最老的已完成任务
completed_tasks.sort(key=lambda x: x[1].completed_at)
to_remove = completed_tasks[:len(completed_tasks) - max_tasks // 2]
for task_id, _ in to_remove:
del self.tasks[task_id]
# 全局任务管理器实例
task_manager = TaskManager()

0
tests/__init__.py Normal file
View File

0
utils/__init__.py Normal file
View File

88
utils/helpers.py Normal file
View File

@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""
工具函数
提供通用的辅助功能
"""
import os
import uuid
from typing import Optional
from datetime import datetime
def generate_task_id() -> str:
"""生成唯一任务ID"""
return str(uuid.uuid4())
def validate_file_path(file_path: str, must_exist: bool = True) -> bool:
"""验证文件路径"""
if not file_path:
return False
if must_exist:
return os.path.isfile(file_path)
# 检查父目录是否存在或可创建
parent_dir = os.path.dirname(file_path)
if parent_dir and not os.path.exists(parent_dir):
try:
os.makedirs(parent_dir, exist_ok=True)
return True
except Exception:
return False
return True
def get_file_extension(file_path: str) -> str:
"""获取文件扩展名(小写)"""
return os.path.splitext(file_path)[1].lower()
def is_stp_file(file_path: str) -> bool:
"""检查是否为STP文件"""
ext = get_file_extension(file_path)
return ext in ['.stp', '.step']
def is_glb_file(file_path: str) -> bool:
"""检查是否为GLB文件"""
ext = get_file_extension(file_path)
return ext == '.glb'
def format_datetime(dt: Optional[datetime]) -> Optional[str]:
"""格式化日期时间"""
if dt is None:
return None
return dt.strftime("%Y-%m-%d %H:%M:%S")
def get_file_size_mb(file_path: str) -> float:
"""获取文件大小MB"""
if not os.path.isfile(file_path):
return 0.0
size_bytes = os.path.getsize(file_path)
return size_bytes / (1024 * 1024)
def cleanup_temp_files(base_path: str) -> None:
"""清理临时文件"""
temp_extensions = ['.tmp', '.temp', '.tmp.stl']
base_dir = os.path.dirname(base_path)
base_name = os.path.splitext(os.path.basename(base_path))[0]
if not os.path.exists(base_dir):
return
for file_name in os.listdir(base_dir):
if file_name.startswith(base_name):
for ext in temp_extensions:
if file_name.endswith(ext):
temp_file = os.path.join(base_dir, file_name)
try:
os.remove(temp_file)
except Exception:
pass # 忽略删除失败的情况