diff --git a/api/__pycache__/main.cpython-312.pyc b/api/__pycache__/main.cpython-312.pyc index a154140..834e7c3 100644 Binary files a/api/__pycache__/main.cpython-312.pyc and b/api/__pycache__/main.cpython-312.pyc differ diff --git a/api/main.py b/api/main.py index b000300..aba7b57 100644 --- a/api/main.py +++ b/api/main.py @@ -56,12 +56,11 @@ app.add_middleware( ) # 导入路由 -from api.routers import tweet, poster, poster_unified, prompt, document, data, integration, content_integration +from api.routers import tweet, poster, prompt, document, data, integration, content_integration # 包含路由 app.include_router(tweet.router, prefix="/api/v1/tweet", tags=["tweet"]) app.include_router(poster.router, prefix="/api/v1/poster", tags=["poster"]) -app.include_router(poster_unified.router, prefix="/api/v2/poster", tags=["poster-unified"]) app.include_router(prompt.router, prefix="/api/v1/prompt", tags=["prompt"]) app.include_router(document.router, prefix="/api/v1/document", tags=["document"]) app.include_router(data.router, prefix="/api/v1", tags=["data"]) diff --git a/api/models/__pycache__/poster.cpython-312.pyc b/api/models/__pycache__/poster.cpython-312.pyc index dfee064..325008f 100644 Binary files a/api/models/__pycache__/poster.cpython-312.pyc and b/api/models/__pycache__/poster.cpython-312.pyc differ diff --git a/api/models/poster.py b/api/models/poster.py index 17afb23..7b7cc31 100644 --- a/api/models/poster.py +++ b/api/models/poster.py @@ -2,101 +2,122 @@ # -*- coding: utf-8 -*- """ -海报API模型定义 +海报API模型定义 - 简化版本 +只保留核心功能,重点优化图片ID使用追踪 """ -from typing import List, Dict, Any, Optional, Tuple +from typing import List, Dict, Any, Optional from pydantic import BaseModel, Field -class PosterRequest(BaseModel): +class PosterGenerateRequest(BaseModel): """海报生成请求模型""" - content: Dict[str, Any] = Field(..., description="内容数据,包含标题、正文等") - topic_index: str = Field(..., description="主题索引,用于文件命名") - template_name: Optional[str] = Field(None, description="模板名称,如果为None则根据配置选择") + content_id: Optional[int] = Field(None, description="内容ID") + product_id: Optional[int] = Field(None, description="产品ID") + scenic_spot_id: Optional[int] = Field(None, description="景区ID") + image_ids: Optional[List[int]] = Field(None, description="图像ID列表") + generate_collage: bool = Field(False, description="是否生成拼图") class Config: schema_extra = { "example": { - "content": { - "title": "【北京故宫】避开人潮的秘密路线,90%的人都不知道!", - "content": "故宫,作为中国最著名的文化遗产之一...", - "tag": ["北京旅游", "故宫", "旅游攻略", "避暑胜地"] - }, - "topic_index": "1", - "template_name": "vibrant" + "content_id": 1, + "product_id": 2, + "scenic_spot_id": 3, + "image_ids": [1, 2, 3], + "generate_collage": True } } -class PosterResponse(BaseModel): +class ImageUsageInfo(BaseModel): + """图像使用信息模型 - 重点追踪图片使用情况""" + image_id: int = Field(..., description="图像ID") + usage_count: int = Field(..., description="使用次数") + first_used_at: str = Field(..., description="首次使用时间") + last_used_at: str = Field(..., description="最后使用时间") + usage_context: List[str] = Field(default_factory=list, description="使用场景列表") + + +class PosterGenerateResponse(BaseModel): """海报生成响应模型""" request_id: str = Field(..., description="请求ID") - topic_index: str = Field(..., description="主题索引") - poster_path: str = Field(..., description="生成的海报文件路径") - template_name: str = Field(..., description="使用的模板名称") + poster_base64: str = Field(..., description="海报图像的base64编码") + content_info: Optional[Dict[str, Any]] = Field(None, description="内容信息") + product_info: Optional[Dict[str, Any]] = Field(None, description="产品信息") + scenic_spot_info: Optional[Dict[str, Any]] = Field(None, description="景区信息") + used_image_ids: List[int] = Field(default_factory=list, description="使用的图像ID列表") + image_usage_info: List[ImageUsageInfo] = Field(default_factory=list, description="图像使用详情") + collage_base64: Optional[str] = Field(None, description="拼图的base64编码") + metadata: Dict[str, Any] = Field(default_factory=dict, description="处理元数据") class Config: schema_extra = { "example": { "request_id": "poster-20240715-123456-a1b2c3d4", - "topic_index": "1", - "poster_path": "/result/run_20230715_123456/topic_1/poster_vibrant.png", - "template_name": "vibrant" - } - } - - -class TemplateListResponse(BaseModel): - """模板列表响应模型""" - templates: List[str] = Field(..., description="可用的模板列表") - default_template: str = Field(..., description="默认模板") - - class Config: - schema_extra = { - "example": { - "templates": ["vibrant", "business", "collage"], - "default_template": "vibrant" - } - } - - -class PosterTextRequest(BaseModel): - """海报文案生成请求模型""" - system_prompt: str = Field(..., description="系统提示词") - user_prompt: str = Field(..., description="用户提示词") - context_data: Optional[Dict[str, Any]] = Field(None, description="上下文数据,用于填充提示词中的占位符") - temperature: Optional[float] = Field(0.3, description="生成温度参数") - top_p: Optional[float] = Field(0.4, description="top_p参数") - - class Config: - schema_extra = { - "example": { - "system_prompt": "你是一位专业的旅游海报文案撰写专家...", - "user_prompt": "请为{location}的{attraction}创作一段简短有力的海报文案...", - "context_data": { - "location": "北京", - "attraction": "故宫" + "poster_base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...", + "content_info": { + "id": 1, + "title": "【北京故宫】避开人潮的秘密路线", + "content": "故宫,作为中国最著名的文化遗产之一...", + "tag": "北京旅游,故宫,旅游攻略" }, - "temperature": 0.3, - "top_p": 0.4 + "used_image_ids": [1, 2, 3], + "image_usage_info": [ + { + "image_id": 1, + "usage_count": 5, + "first_used_at": "2024-07-15 10:30:00", + "last_used_at": "2024-07-15 10:30:00", + "usage_context": ["poster_generation", "collage_creation"] + } + ], + "collage_base64": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ...", + "metadata": { + "total_images_used": 3, + "has_collage": True, + "processing_time": "2.5s" + } } } -class PosterTextResponse(BaseModel): - """海报文案生成响应模型""" - request_id: str = Field(..., description="请求ID") - text_content: Dict[str, Any] = Field(..., description="生成的文案内容") +class ImageUsageRequest(BaseModel): + """图像使用查询请求模型""" + image_ids: List[int] = Field(..., description="要查询的图像ID列表") class Config: schema_extra = { "example": { - "request_id": "text-20240715-123456-a1b2c3d4", - "text_content": { - "title": "紫禁城的秘密", - "subtitle": "600年历史,等你探索", - "description": "穿越时光,触摸历史,感受帝王的荣耀与辉煌" + "image_ids": [1, 2, 3, 4, 5] + } + } + + +class ImageUsageResponse(BaseModel): + """图像使用情况响应模型""" + request_id: str = Field(..., description="请求ID") + image_usage_info: List[ImageUsageInfo] = Field(..., description="图像使用详情") + summary: Dict[str, Any] = Field(..., description="使用情况汇总") + + class Config: + schema_extra = { + "example": { + "request_id": "usage-20240715-123456-a1b2c3d4", + "image_usage_info": [ + { + "image_id": 1, + "usage_count": 5, + "first_used_at": "2024-07-15 10:30:00", + "last_used_at": "2024-07-15 10:30:00", + "usage_context": ["poster_generation", "collage_creation"] + } + ], + "summary": { + "total_images": 5, + "total_usage_count": 15, + "most_used_image_id": 1, + "least_used_image_id": 5 } } } \ No newline at end of file diff --git a/api/routers/__pycache__/poster.cpython-312.pyc b/api/routers/__pycache__/poster.cpython-312.pyc index 1bd8334..2924d04 100644 Binary files a/api/routers/__pycache__/poster.cpython-312.pyc and b/api/routers/__pycache__/poster.cpython-312.pyc differ diff --git a/api/routers/poster.py b/api/routers/poster.py index 829d2c6..7f53f97 100644 --- a/api/routers/poster.py +++ b/api/routers/poster.py @@ -2,7 +2,8 @@ # -*- coding: utf-8 -*- """ -海报API路由 +海报API路由 - 简化版本 +只保留核心功能,重点优化图片使用追踪 """ import logging @@ -14,9 +15,8 @@ from core.ai import AIAgent from utils.file_io import OutputManager from api.services.poster import PosterService from api.models.poster import ( - PosterRequest, PosterResponse, - TemplateListResponse, - PosterTextRequest, PosterTextResponse + PosterGenerateRequest, PosterGenerateResponse, + ImageUsageRequest, ImageUsageResponse ) # 从依赖注入模块导入依赖 @@ -37,80 +37,51 @@ def get_poster_service( return PosterService(ai_agent, config_manager, output_manager) -@router.post("/generate", response_model=PosterResponse, summary="生成海报") +@router.post("/generate", response_model=PosterGenerateResponse, summary="生成海报") async def generate_poster( - request: PosterRequest, + request: PosterGenerateRequest, poster_service: PosterService = Depends(get_poster_service) ): """ 生成海报 - - **content**: 内容数据,包含标题、正文等 - - **topic_index**: 主题索引,用于文件命名 - - **template_name**: 模板名称,如果为None则根据配置选择 + - **content_id**: 内容ID(可选) + - **product_id**: 产品ID(可选) + - **scenic_spot_id**: 景区ID(可选) + - **image_ids**: 图像ID列表(可选) + - **generate_collage**: 是否生成拼图 """ try: - request_id, topic_index, poster_path, template_name = poster_service.generate_poster( - content=request.content, - topic_index=request.topic_index, - template_name=request.template_name + result = poster_service.generate_poster_simplified( + content_id=request.content_id, + product_id=request.product_id, + scenic_spot_id=request.scenic_spot_id, + image_ids=request.image_ids, + generate_collage=request.generate_collage ) - return PosterResponse( - request_id=request_id, - topic_index=topic_index, - poster_path=poster_path, - template_name=template_name - ) + return PosterGenerateResponse(**result) + except ValueError as e: + logger.error(f"参数错误: {e}") + raise HTTPException(status_code=400, detail=str(e)) except Exception as e: logger.error(f"生成海报失败: {e}", exc_info=True) raise HTTPException(status_code=500, detail=f"生成海报失败: {str(e)}") -@router.get("/templates", response_model=TemplateListResponse, summary="获取可用模板列表") -async def get_templates( - poster_service: PosterService = Depends(get_poster_service) -): - """获取可用的海报模板列表""" - try: - templates, default_template = poster_service.get_available_templates() - - return TemplateListResponse( - templates=templates, - default_template=default_template - ) - except Exception as e: - logger.error(f"获取模板列表失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"获取模板列表失败: {str(e)}") - - -@router.post("/text", response_model=PosterTextResponse, summary="生成海报文案") -async def generate_poster_text( - request: PosterTextRequest, +@router.post("/image-usage", response_model=ImageUsageResponse, summary="查询图像使用情况") +async def get_image_usage( + request: ImageUsageRequest, poster_service: PosterService = Depends(get_poster_service) ): """ - 生成海报文案 + 查询图像使用情况 - - **system_prompt**: 系统提示词 - - **user_prompt**: 用户提示词 - - **context_data**: 上下文数据,用于填充提示词中的占位符 - - **temperature**: 生成温度参数 - - **top_p**: top_p参数 + - **image_ids**: 要查询的图像ID列表 """ try: - request_id, text_content = await poster_service.generate_poster_text( - system_prompt=request.system_prompt, - user_prompt=request.user_prompt, - context_data=request.context_data, - temperature=request.temperature, - top_p=request.top_p - ) - - return PosterTextResponse( - request_id=request_id, - text_content=text_content - ) + result = poster_service.get_image_usage_info(request.image_ids) + return ImageUsageResponse(**result) except Exception as e: - logger.error(f"生成海报文案失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"生成海报文案失败: {str(e)}") \ No newline at end of file + logger.error(f"查询图像使用情况失败: {e}", exc_info=True) + raise HTTPException(status_code=500, detail=f"查询图像使用情况失败: {str(e)}") \ No newline at end of file diff --git a/api/routers/poster_unified.py b/api/routers/poster_unified.py deleted file mode 100644 index 86c87c4..0000000 --- a/api/routers/poster_unified.py +++ /dev/null @@ -1,314 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -统一海报API路由 -支持多种模板类型的海报生成,配置化管理 -""" - -import logging -import uuid -from datetime import datetime, timezone -from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks -from typing import Dict, Any, List - -from core.ai import AIAgent -from api.services.poster_service import UnifiedPosterService -from api.models.vibrant_poster import ( - PosterGenerationRequest, PosterGenerationResponse, - ContentGenerationRequest, ContentGenerationResponse, - TemplateListResponse, TemplateInfo, - BaseAPIResponse -) - -# 从依赖注入模块导入依赖 -from api.dependencies import get_ai_agent - -logger = logging.getLogger(__name__) - -# 创建路由 -router = APIRouter() - -# 依赖注入函数 -def get_unified_poster_service( - ai_agent: AIAgent = Depends(get_ai_agent) -) -> UnifiedPosterService: - """获取统一海报服务""" - return UnifiedPosterService(ai_agent) - - -def create_response(success: bool, message: str, data: Any = None, request_id: str = None) -> Dict[str, Any]: - """创建标准响应""" - if request_id is None: - request_id = f"req-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - - return { - "success": success, - "message": message, - "request_id": request_id, - "timestamp": datetime.now(timezone.utc).isoformat(), - "data": data - } - - -@router.get("/templates", response_model=TemplateListResponse, summary="获取所有可用模板") -async def get_templates( - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 获取所有可用的海报模板列表 - - 返回每个模板的详细信息,包括: - - 模板ID和名称 - - 模板描述 - - 模板尺寸 - - 必填字段和可选字段 - """ - try: - templates = service.get_available_templates() - response_data = create_response( - success=True, - message="获取模板列表成功", - data=templates - ) - return TemplateListResponse(**response_data) - - except Exception as e: - logger.error(f"获取模板列表失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"获取模板列表失败: {str(e)}") - - -@router.get("/templates/{template_id}", response_model=BaseAPIResponse, summary="获取指定模板信息") -async def get_template_info( - template_id: str, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 获取指定模板的详细信息 - - 参数: - - **template_id**: 模板ID - """ - try: - template_info = service.get_template_info(template_id) - if not template_info: - raise HTTPException(status_code=404, detail=f"模板 {template_id} 不存在") - - response_data = create_response( - success=True, - message="获取模板信息成功", - data=template_info.dict() - ) - return BaseAPIResponse(**response_data) - - except HTTPException: - raise - except Exception as e: - logger.error(f"获取模板信息失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"获取模板信息失败: {str(e)}") - - -@router.post("/content/generate", response_model=ContentGenerationResponse, summary="生成海报内容") -async def generate_content( - request: ContentGenerationRequest, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 根据源数据生成海报内容,不生成实际图片 - - 用于: - 1. 预览生成的内容 - 2. 调试和测试内容生成 - 3. 分步骤生成(先生成内容,再生成图片) - - 参数: - - **template_id**: 模板ID - - **source_data**: 源数据,用于AI生成内容 - - **temperature**: AI生成温度参数 - """ - try: - content = await service.generate_content( - template_id=request.template_id, - source_data=request.source_data, - temperature=request.temperature - ) - - response_data = create_response( - success=True, - message="内容生成成功", - data={ - "template_id": request.template_id, - "content": content, - "metadata": { - "generation_method": "ai_generated", - "temperature": request.temperature - } - } - ) - return ContentGenerationResponse(**response_data) - - except Exception as e: - logger.error(f"生成内容失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"生成内容失败: {str(e)}") - - -@router.post("/generate", response_model=PosterGenerationResponse, summary="生成海报") -async def generate_poster( - request: PosterGenerationRequest, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 生成海报图片 - - 支持两种模式: - 1. 直接提供内容(content字段) - 2. 提供源数据让AI生成内容(source_data字段) - - 参数: - - **template_id**: 模板ID - - **content**: 直接提供的海报内容(可选) - - **source_data**: 源数据,用于AI生成内容(可选) - - **topic_name**: 主题名称,用于文件命名 - - **image_path**: 指定图片路径(可选) - - **image_dir**: 图片目录(可选) - - **output_dir**: 输出目录(可选) - - **temperature**: AI生成温度参数 - """ - try: - result = await service.generate_poster( - template_id=request.template_id, - content=request.content, - source_data=request.source_data, - topic_name=request.topic_name, - image_path=request.image_path, - image_dir=request.image_dir, - output_dir=request.output_dir, - temperature=request.temperature - ) - - response_data = create_response( - success=True, - message="海报生成成功", - data=result, - request_id=result["request_id"] - ) - return PosterGenerationResponse(**response_data) - - except Exception as e: - logger.error(f"生成海报失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"生成海报失败: {str(e)}") - - -@router.post("/batch", response_model=BaseAPIResponse, summary="批量生成海报") -async def batch_generate_posters( - template_id: str, - base_path: str, - image_dir: str = None, - source_files: Dict[str, str] = None, - output_base: str = "result/posters", - parallel_count: int = 3, - temperature: float = 0.7, - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 批量生成海报 - - 自动扫描指定目录下的topic文件夹,为每个topic生成海报。 - - 参数: - - **template_id**: 模板ID - - **base_path**: 包含多个topic目录的基础路径 - - **image_dir**: 图片目录(可选) - - **source_files**: 源文件配置字典(可选) - - **output_base**: 输出基础目录 - - **parallel_count**: 并发处理数量 - - **temperature**: AI生成温度参数 - """ - try: - result = await service.batch_generate_posters( - template_id=template_id, - base_path=base_path, - image_dir=image_dir, - source_files=source_files or {}, - output_base=output_base, - parallel_count=parallel_count, - temperature=temperature - ) - - response_data = create_response( - success=True, - message=f"批量生成完成,成功: {result['successful_count']}, 失败: {result['failed_count']}", - data=result, - request_id=result["request_id"] - ) - return BaseAPIResponse(**response_data) - - except Exception as e: - logger.error(f"批量生成海报失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"批量生成海报失败: {str(e)}") - - -@router.post("/config/reload", response_model=BaseAPIResponse, summary="重新加载配置") -async def reload_config( - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """ - 重新加载海报配置 - - 用于在不重启服务的情况下更新配置,包括: - - 提示词模板 - - 模板配置 - - 默认参数 - """ - try: - service.reload_config() - response_data = create_response( - success=True, - message="配置重新加载成功" - ) - return BaseAPIResponse(**response_data) - - except Exception as e: - logger.error(f"重新加载配置失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"重新加载配置失败: {str(e)}") - - -@router.get("/health", summary="健康检查") -async def health_check(): - """服务健康检查""" - return create_response( - success=True, - message="统一海报服务运行正常", - data={ - "service": "unified_poster", - "status": "healthy", - "version": "2.0.0" - } - ) - - -@router.get("/config", summary="获取服务配置") -async def get_service_config( - service: UnifiedPosterService = Depends(get_unified_poster_service) -): - """获取服务配置信息""" - try: - config_info = { - "default_image_dir": service.config_manager.get_default_config("image_dir"), - "default_output_dir": service.config_manager.get_default_config("output_dir"), - "default_font_dir": service.config_manager.get_default_config("font_dir"), - "default_template": service.config_manager.get_default_config("template"), - "supported_image_formats": ["png", "jpg", "jpeg", "webp"], - "available_templates": len(service.get_available_templates()) - } - - response_data = create_response( - success=True, - message="获取配置成功", - data=config_info - ) - return BaseAPIResponse(**response_data) - - except Exception as e: - logger.error(f"获取配置失败: {e}", exc_info=True) - raise HTTPException(status_code=500, detail=f"获取配置失败: {str(e)}") \ No newline at end of file diff --git a/api/services/__pycache__/database_service.cpython-312.pyc b/api/services/__pycache__/database_service.cpython-312.pyc index 7f6e3a4..a77b98d 100644 Binary files a/api/services/__pycache__/database_service.cpython-312.pyc and b/api/services/__pycache__/database_service.cpython-312.pyc differ diff --git a/api/services/__pycache__/poster.cpython-312.pyc b/api/services/__pycache__/poster.cpython-312.pyc index fc910e4..01a4145 100644 Binary files a/api/services/__pycache__/poster.cpython-312.pyc and b/api/services/__pycache__/poster.cpython-312.pyc differ diff --git a/api/services/__pycache__/prompt_service.cpython-312.pyc b/api/services/__pycache__/prompt_service.cpython-312.pyc index 6b2c7c2..79230af 100644 Binary files a/api/services/__pycache__/prompt_service.cpython-312.pyc and b/api/services/__pycache__/prompt_service.cpython-312.pyc differ diff --git a/api/services/database_service.py b/api/services/database_service.py index c13301d..a8785eb 100644 --- a/api/services/database_service.py +++ b/api/services/database_service.py @@ -632,7 +632,7 @@ class DatabaseService: conn = self.db_pool.get_connection() cursor = conn.cursor(dictionary=True) cursor.execute( - "SELECT id FROM product WHERE productName = %s AND isDelete = 0", + "SELECT id FROM product WHERE name = %s AND isDelete = 0", (product_name,) ) result = cursor.fetchone() @@ -647,4 +647,259 @@ class DatabaseService: except Exception as e: logger.error(f"查询产品ID失败: {e}") - return None \ No newline at end of file + return None + + + + def get_image_by_id(self, image_id: int) -> Optional[Dict[str, Any]]: + """ + 根据ID获取图像信息 + + Args: + image_id: 图像ID + + Returns: + 图像信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM material WHERE id = %s AND materialType = 'image' AND isDelete = 0", + (image_id,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到图像信息: ID={image_id}, 名称={result['materialName']}") + return result + else: + logger.warning(f"未找到图像信息: ID={image_id}") + return None + + except Exception as e: + logger.error(f"查询图像信息失败: {e}") + return None + + def get_images_by_ids(self, image_ids: List[int]) -> List[Dict[str, Any]]: + """ + 根据ID列表批量获取图像信息 + + Args: + image_ids: 图像ID列表 + + Returns: + 图像信息列表 + """ + if not self.db_pool or not image_ids: + return [] + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + + # 构建IN查询 + placeholders = ','.join(['%s'] * len(image_ids)) + query = f"SELECT * FROM material WHERE id IN ({placeholders}) AND materialType = 'image' AND isDelete = 0" + cursor.execute(query, image_ids) + results = cursor.fetchall() + cursor.close() + conn.close() + + logger.info(f"批量查询图像信息: 请求{len(image_ids)}个,找到{len(results)}个") + return results + + except Exception as e: + logger.error(f"批量查询图像信息失败: {e}") + return [] + + def get_content_by_id(self, content_id: int) -> Optional[Dict[str, Any]]: + """ + 根据ID获取内容信息 + + Args: + content_id: 内容ID + + Returns: + 内容信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM content WHERE id = %s AND isDelete = 0", + (content_id,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到内容信息: ID={content_id}, 标题={result['title']}") + return result + else: + logger.warning(f"未找到内容信息: ID={content_id}") + return None + + except Exception as e: + logger.error(f"查询内容信息失败: {e}") + return None + + def get_content_by_topic_index(self, topic_index: str) -> Optional[Dict[str, Any]]: + """ + 根据主题索引获取内容信息 + + Args: + topic_index: 主题索引 + + Returns: + 内容信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM content WHERE topicIndex = %s AND isDelete = 0 ORDER BY createTime DESC LIMIT 1", + (topic_index,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到内容信息: topicIndex={topic_index}, 标题={result['title']}") + return result + else: + logger.warning(f"未找到内容信息: topicIndex={topic_index}") + return None + + except Exception as e: + logger.error(f"查询内容信息失败: {e}") + return None + + def get_images_by_folder_id(self, folder_id: int) -> List[Dict[str, Any]]: + """ + 根据文件夹ID获取图像列表 + + Args: + folder_id: 文件夹ID + + Returns: + 图像信息列表 + """ + if not self.db_pool: + return [] + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM material WHERE folderId = %s AND materialType = 'image' AND isDelete = 0 ORDER BY createTime DESC", + (folder_id,) + ) + results = cursor.fetchall() + cursor.close() + conn.close() + + logger.info(f"根据文件夹ID获取图像: folderId={folder_id}, 找到{len(results)}个图像") + return results + + except Exception as e: + logger.error(f"根据文件夹ID获取图像失败: {e}") + return [] + + def get_folder_by_id(self, folder_id: int) -> Optional[Dict[str, Any]]: + """ + 根据ID获取文件夹信息 + + Args: + folder_id: 文件夹ID + + Returns: + 文件夹信息字典,如果未找到则返回None + """ + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute( + "SELECT * FROM material_folder WHERE id = %s AND isDelete = 0", + (folder_id,) + ) + result = cursor.fetchone() + cursor.close() + conn.close() + + if result: + logger.info(f"找到文件夹信息: ID={folder_id}, 名称={result['folderName']}") + return result + else: + logger.warning(f"未找到文件夹信息: ID={folder_id}") + return None + + except Exception as e: + logger.error(f"查询文件夹信息失败: {e}") + return None + + def get_related_images_for_content(self, content_id: int, limit: int = 10) -> List[Dict[str, Any]]: + """ + 获取与内容相关的图像列表 + + Args: + content_id: 内容ID + limit: 限制数量 + + Returns: + 相关图像列表 + """ + if not self.db_pool: + return [] + + try: + conn = self.db_pool.get_connection() + cursor = conn.cursor(dictionary=True) + + # 获取内容信息 + cursor.execute( + "SELECT * FROM content WHERE id = %s AND isDelete = 0", + (content_id,) + ) + content = cursor.fetchone() + + if not content: + cursor.close() + conn.close() + return [] + + # 获取相关的图像(这里可以根据业务逻辑调整查询条件) + cursor.execute( + "SELECT * FROM material WHERE materialType = 'image' AND isDelete = 0 ORDER BY RAND() LIMIT %s", + (limit,) + ) + results = cursor.fetchall() + cursor.close() + conn.close() + + logger.info(f"获取相关内容图像: contentId={content_id}, 找到{len(results)}个图像") + return results + + except Exception as e: + logger.error(f"获取相关内容图像失败: {e}") + return [] \ No newline at end of file diff --git a/api/services/poster.py b/api/services/poster.py index 709264d..55fb6fc 100644 --- a/api/services/poster.py +++ b/api/services/poster.py @@ -2,20 +2,22 @@ # -*- coding: utf-8 -*- """ -海报服务层 -封装现有功能,提供API调用 +海报服务层 - 简化版本 +封装核心功能,重点优化图片使用追踪 """ import logging import uuid -from typing import List, Dict, Any, Optional, Tuple +import time +from typing import List, Dict, Any, Optional from datetime import datetime from core.config import ConfigManager, PosterConfig from core.ai import AIAgent from utils.file_io import OutputManager +from utils.image_processor import ImageProcessor from poster.poster_generator import PosterGenerator -from poster.text_generator import PosterContentGenerator +from api.services.database_service import DatabaseService logger = logging.getLogger(__name__) @@ -38,86 +40,291 @@ class PosterService: # 初始化各个组件 self.poster_generator = PosterGenerator(config_manager, output_manager) - self.text_generator = PosterContentGenerator(ai_agent) - def generate_poster(self, content: Dict[str, Any], topic_index: str, - template_name: Optional[str] = None) -> Tuple[str, str, str, str]: + # 初始化数据库服务 + self.db_service = DatabaseService(config_manager) + + # 图片使用追踪存储(实际应用中应该使用数据库) + self._image_usage_tracker = {} + + def generate_poster_simplified(self, content_id: Optional[int] = None, + product_id: Optional[int] = None, + scenic_spot_id: Optional[int] = None, + image_ids: Optional[List[int]] = None, + generate_collage: bool = False) -> Dict[str, Any]: """ - 生成海报 + 简化的海报生成方法 Args: - content: 内容数据,包含标题、正文等 - topic_index: 主题索引,用于文件命名 - template_name: 模板名称,如果为None则根据配置选择 + content_id: 内容ID + product_id: 产品ID + scenic_spot_id: 景区ID + image_ids: 图像ID列表 + generate_collage: 是否生成拼图 Returns: - 请求ID、主题索引、生成的海报文件路径和使用的模板名称 + 包含base64图像数据和图片使用信息的字典 """ - logger.info(f"开始为主题 {topic_index} 生成海报") + start_time = time.time() + logger.info(f"开始生成海报: content_id={content_id}, product_id={product_id}, scenic_spot_id={scenic_spot_id}") - # 生成海报 - poster_path = self.poster_generator.generate_poster(content, topic_index, template_name) + result = { + "request_id": f"poster-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}", + "poster_base64": "", + "content_info": None, + "product_info": None, + "scenic_spot_info": None, + "used_image_ids": [], + "image_usage_info": [], + "collage_base64": None, + "metadata": {} + } - # 获取使用的模板名称 - if template_name is None: - template_name = self.poster_generator._select_template() - - # 生成请求ID - request_id = f"poster-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - - logger.info(f"海报生成完成,请求ID: {request_id}, 主题索引: {topic_index}, 模板: {template_name}") - return request_id, topic_index, poster_path, template_name - - def get_available_templates(self) -> Tuple[List[str], str]: - """ - 获取可用的模板列表 - - Returns: - 可用的模板列表和默认模板 - """ - # 获取配置 - poster_config = self.config_manager.get_config('poster', PosterConfig) - - # 获取可用模板 - available_templates = poster_config.available_templates - - # 获取默认模板 - default_template = poster_config.template_selection - if default_template == "random" and available_templates: - default_template = available_templates[0] + try: + # 1. 获取内容信息 + if content_id: + content_info = self.db_service.get_content_by_id(content_id) + if content_info: + result["content_info"] = self._build_content_info(content_info) - return available_templates, default_template + # 2. 获取产品信息 + if product_id: + product_info = self.db_service.get_product_by_id(product_id) + if product_info: + result["product_info"] = self._build_product_info(product_info) + + # 3. 获取景区信息 + if scenic_spot_id: + scenic_spot_info = self.db_service.get_scenic_spot_by_id(scenic_spot_id) + if scenic_spot_info: + result["scenic_spot_info"] = self._build_scenic_spot_info(scenic_spot_info) + + # 4. 处理图像信息并追踪使用情况 + image_paths = [] + if image_ids: + images = self.db_service.get_images_by_ids(image_ids) + for img in images: + image_paths.append(img['filePath']) + result["used_image_ids"].append(img['id']) + # 更新图片使用追踪 + self._update_image_usage(img['id'], "poster_generation") + + # 5. 构建海报内容 + poster_content = self._build_poster_content_from_info( + result["content_info"], result["product_info"], result["scenic_spot_info"] + ) + + # 6. 生成海报(只使用vibrant模板) + poster_path = self.poster_generator.generate_poster( + poster_content, + str(content_id or product_id or scenic_spot_id or "unknown"), + "vibrant" + ) + + if poster_path: + # 转换为base64 + result["poster_base64"] = ImageProcessor.image_to_base64(poster_path) + + # 7. 处理拼图 + if generate_collage and len(image_paths) > 1: + collage_result = ImageProcessor.process_images_for_poster( + image_paths, + target_size=(900, 1200), + create_collage=True + ) + + if collage_result.get("collage_image"): + result["collage_base64"] = collage_result["collage_image"]["base64"] + # 更新拼图中使用的图片使用追踪 + for img_id in result["used_image_ids"]: + self._update_image_usage(img_id, "collage_creation") + + # 8. 构建图片使用信息 + result["image_usage_info"] = self._get_image_usage_info(result["used_image_ids"]) + + # 9. 添加元数据 + processing_time = time.time() - start_time + result["metadata"] = { + "total_images_used": len(result["used_image_ids"]), + "has_collage": result["collage_base64"] is not None, + "processing_time": f"{processing_time:.2f}s", + "template_used": "vibrant" + } + + logger.info(f"海报生成完成,处理时间: {processing_time:.2f}s") + return result + + except Exception as e: + logger.error(f"生成海报失败: {e}", exc_info=True) + result["metadata"]["error"] = str(e) + return result - async def generate_poster_text(self, system_prompt: str, user_prompt: str, - context_data: Optional[Dict[str, Any]] = None, - temperature: Optional[float] = None, - top_p: Optional[float] = None) -> Tuple[str, Dict[str, Any]]: + def get_image_usage_info(self, image_ids: List[int]) -> Dict[str, Any]: """ - 生成海报文案 + 获取图像使用情况信息 Args: - system_prompt: 系统提示词 - user_prompt: 用户提示词 - context_data: 上下文数据,用于填充提示词中的占位符 - temperature: 生成温度参数 - top_p: top_p参数 + image_ids: 图像ID列表 Returns: - 请求ID和生成的文案内容 + 图像使用情况信息 """ - logger.info("开始生成海报文案") + request_id = f"usage-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - # 生成文案 - text_content = await self.text_generator.generate_text_for_poster( - system_prompt=system_prompt, - user_prompt=user_prompt, - context_data=context_data, - temperature=temperature, - top_p=top_p - ) + image_usage_info = [] + total_usage_count = 0 + usage_counts = [] - # 生成请求ID - request_id = f"text-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" + for img_id in image_ids: + usage_info = self._get_single_image_usage_info(img_id) + if usage_info: + image_usage_info.append(usage_info) + total_usage_count += usage_info["usage_count"] + usage_counts.append(usage_info["usage_count"]) - logger.info(f"海报文案生成完成,请求ID: {request_id}") - return request_id, text_content \ No newline at end of file + # 计算汇总信息 + summary = { + "total_images": len(image_ids), + "total_usage_count": total_usage_count, + "most_used_image_id": None, + "least_used_image_id": None + } + + if usage_counts: + max_usage = max(usage_counts) + min_usage = min(usage_counts) + summary["most_used_image_id"] = next( + (info["image_id"] for info in image_usage_info if info["usage_count"] == max_usage), + None + ) + summary["least_used_image_id"] = next( + (info["image_id"] for info in image_usage_info if info["usage_count"] == min_usage), + None + ) + + return { + "request_id": request_id, + "image_usage_info": image_usage_info, + "summary": summary + } + + def _update_image_usage(self, image_id: int, context: str): + """更新图片使用追踪""" + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + if image_id not in self._image_usage_tracker: + self._image_usage_tracker[image_id] = { + "usage_count": 0, + "first_used_at": current_time, + "last_used_at": current_time, + "usage_context": [] + } + + tracker = self._image_usage_tracker[image_id] + tracker["usage_count"] += 1 + tracker["last_used_at"] = current_time + if context not in tracker["usage_context"]: + tracker["usage_context"].append(context) + + def _get_single_image_usage_info(self, image_id: int) -> Optional[Dict[str, Any]]: + """获取单个图片的使用信息""" + if image_id in self._image_usage_tracker: + tracker = self._image_usage_tracker[image_id] + return { + "image_id": image_id, + "usage_count": tracker["usage_count"], + "first_used_at": tracker["first_used_at"], + "last_used_at": tracker["last_used_at"], + "usage_context": tracker["usage_context"] + } + else: + # 如果图片从未被使用过,返回默认信息 + return { + "image_id": image_id, + "usage_count": 0, + "first_used_at": "", + "last_used_at": "", + "usage_context": [] + } + + def _get_image_usage_info(self, image_ids: List[int]) -> List[Dict[str, Any]]: + """获取图片使用信息列表""" + usage_info = [] + for img_id in image_ids: + info = self._get_single_image_usage_info(img_id) + if info: + usage_info.append(info) + return usage_info + + def _build_content_info(self, content_data: Dict[str, Any]) -> Dict[str, Any]: + """构建内容信息""" + return { + "id": content_data["id"], + "title": content_data["title"], + "content": content_data["content"], + "tag": content_data["tag"] + } + + def _build_product_info(self, product_data: Dict[str, Any]) -> Dict[str, Any]: + """构建产品信息""" + return { + "id": product_data["id"], + "name": product_data["name"], + "description": product_data.get("description"), + "real_price": float(product_data["realPrice"]) if product_data.get("realPrice") else None, + "origin_price": float(product_data["originPrice"]) if product_data.get("originPrice") else None + } + + def _build_scenic_spot_info(self, scenic_data: Dict[str, Any]) -> Dict[str, Any]: + """构建景区信息""" + return { + "id": scenic_data["id"], + "name": scenic_data["name"], + "description": scenic_data.get("description"), + "address": scenic_data.get("address") + } + + def _build_poster_content_from_info(self, content_info: Optional[Dict[str, Any]], + product_info: Optional[Dict[str, Any]], + scenic_spot_info: Optional[Dict[str, Any]]) -> Dict[str, Any]: + """从信息构建海报内容""" + title = "" + content_parts = [] + tags = [] + + # 构建标题 + if content_info: + title = content_info["title"] + if content_info.get("content"): + content_parts.append(content_info["content"]) + if content_info.get("tag"): + tags.extend(content_info["tag"].split(",")) + else: + # 如果没有内容信息,使用景区和产品信息构建标题 + if scenic_spot_info and product_info: + title = f"{scenic_spot_info['name']} - {product_info['name']}" + elif scenic_spot_info: + title = scenic_spot_info['name'] + elif product_info: + title = product_info['name'] + + # 添加景区信息 + if scenic_spot_info and scenic_spot_info.get("description"): + content_parts.append(f"景区介绍: {scenic_spot_info['description']}") + tags.append(scenic_spot_info["name"]) + + # 添加产品信息 + if product_info: + if product_info.get("description"): + content_parts.append(f"产品介绍: {product_info['description']}") + if product_info.get("real_price"): + content_parts.append(f"价格: ¥{product_info['real_price']}") + tags.append(product_info["name"]) + + content = "\n\n".join(content_parts) if content_parts else "暂无详细内容" + + return { + "title": title, + "content": content, + "tag": tags + } \ No newline at end of file diff --git a/api/services/prompt_service.py b/api/services/prompt_service.py index 5fae265..04b5376 100644 --- a/api/services/prompt_service.py +++ b/api/services/prompt_service.py @@ -51,7 +51,7 @@ class PromptService: self.db_pool = self._init_db_pool() # 创建必要的目录结构 - self._create_resource_directories() + # self._create_resource_directories() def _create_resource_directories(self): pass