Download src/landppt/api/global_master_template_api.py from biaoqing/ppt-web: direct link, hf CLI and curl.
- Browser
- Download file 25.5 kB
-
https://huggingface.co/spaces/biaoqing/ppt-web/resolve/main/src/landppt/api/global_master_template_api.py
- Command line
-
hf download hf://spaces/biaoqing/ppt-web/src/landppt/api/global_master_template_api.py
-
curl -L -o global_master_template_api.py https://huggingface.co/spaces/biaoqing/ppt-web/resolve/main/src/landppt/api/global_master_template_api.py
25.5 kB
| """ | |
| Global Master Template API endpoints | |
| """ | |
| import logging | |
| from typing import List, Optional | |
| from fastapi import APIRouter, HTTPException, Depends, Query, Request | |
| from fastapi.responses import JSONResponse | |
| from .models import ( | |
| GlobalMasterTemplateCreate, GlobalMasterTemplateUpdate, GlobalMasterTemplateResponse, | |
| GlobalMasterTemplateDetailResponse, GlobalMasterTemplateGenerateRequest, | |
| TemplateSelectionRequest, TemplateSelectionResponse | |
| ) | |
| from ..services.template.global_master_template_service import GlobalMasterTemplateService | |
| from ..auth.middleware import get_current_user_required | |
| from ..core.config import app_config | |
| from ..database.database import AsyncSessionLocal | |
| from ..services.credits_service import CreditsService | |
| logger = logging.getLogger(__name__) | |
| # Create router | |
| router = APIRouter(prefix="/api/global-master-templates", tags=["Global Master Templates"]) | |
| def _template_service_for_user( | |
| user, | |
| *, | |
| allow_system_template_write: bool = False, | |
| ) -> GlobalMasterTemplateService: | |
| """Create a user-scoped template service instance.""" | |
| return GlobalMasterTemplateService( | |
| user_id=user.id, | |
| allow_system_template_write=bool( | |
| allow_system_template_write and getattr(user, "is_admin", False) | |
| ), | |
| ) | |
| async def create_template( | |
| template_data: GlobalMasterTemplateCreate, | |
| user=Depends(get_current_user_required), | |
| ): | |
| """Create a new global master template""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| payload = template_data.model_dump() | |
| if (payload.get("created_by") or "").strip().lower() == "user": | |
| payload["created_by"] = f"user:{user.id}" | |
| result = await template_service.create_template(payload) | |
| return GlobalMasterTemplateResponse(**result) | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except Exception as e: | |
| logger.error(f"Failed to create template: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to create template") | |
| async def get_all_templates( | |
| active_only: bool = Query(True, description="Only return active templates"), | |
| tags: Optional[str] = Query(None, description="Filter by tags (comma-separated)"), | |
| page: int = Query(1, ge=1, description="Page number (1-based)"), | |
| page_size: int = Query(6, ge=1, le=100, description="Number of items per page"), | |
| search: Optional[str] = Query(None, description="Search in template name and description"), | |
| user=Depends(get_current_user_required), | |
| ): | |
| """Get all global master templates with pagination""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| if tags: | |
| tag_list = [tag.strip() for tag in tags.split(",")] | |
| result = await template_service.get_templates_by_tags_paginated( | |
| tag_list, active_only, page, page_size, search | |
| ) | |
| else: | |
| result = await template_service.get_all_templates_paginated( | |
| active_only, page, page_size, search | |
| ) | |
| return { | |
| "templates": [GlobalMasterTemplateResponse(**template) for template in result["templates"]], | |
| "pagination": result["pagination"] | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to get templates: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to get templates") | |
| async def update_template( | |
| template_id: int, | |
| update_data: GlobalMasterTemplateUpdate, | |
| user=Depends(get_current_user_required), | |
| ): | |
| """Update a global master template""" | |
| try: | |
| template_service = _template_service_for_user( | |
| user, | |
| allow_system_template_write=True, | |
| ) | |
| # Filter out None values | |
| update_dict = {k: v for k, v in update_data.model_dump().items() if v is not None} | |
| if not update_dict: | |
| raise HTTPException(status_code=400, detail="No update data provided") | |
| success = await template_service.update_template(template_id, update_dict) | |
| if not success: | |
| raise HTTPException(status_code=404, detail="Template not found") | |
| return {"success": True, "message": "Template updated successfully"} | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to update template {template_id}: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to update template") | |
| async def delete_template(template_id: int, user=Depends(get_current_user_required)): | |
| """Delete a global master template""" | |
| try: | |
| template_service = _template_service_for_user( | |
| user, | |
| allow_system_template_write=True, | |
| ) | |
| success = await template_service.delete_template(template_id) | |
| if not success: | |
| raise HTTPException(status_code=404, detail="Template not found") | |
| return {"success": True, "message": "Template deleted successfully"} | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to delete template {template_id}: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to delete template") | |
| async def set_default_template(template_id: int, user=Depends(get_current_user_required)): | |
| """Set a template as the default template""" | |
| try: | |
| template_service = _template_service_for_user( | |
| user, | |
| allow_system_template_write=True, | |
| ) | |
| success = await template_service.set_default_template(template_id) | |
| if not success: | |
| raise HTTPException(status_code=404, detail="Template not found") | |
| return {"success": True, "message": "Default template set successfully"} | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to set default template {template_id}: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to set default template") | |
| async def get_default_template(user=Depends(get_current_user_required)): | |
| """Get the default global master template""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| template = await template_service.get_default_template() | |
| if not template: | |
| raise HTTPException(status_code=404, detail="No default template found") | |
| return GlobalMasterTemplateDetailResponse(**template) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to get default template: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to get default template") | |
| async def get_template_by_id(template_id: int, user=Depends(get_current_user_required)): | |
| """Get a global master template by ID""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| template = await template_service.get_template_by_id(template_id) | |
| if not template: | |
| raise HTTPException(status_code=404, detail="Template not found") | |
| return GlobalMasterTemplateDetailResponse(**template) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to get template {template_id}: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to get template") | |
| async def generate_template_with_ai( | |
| request: GlobalMasterTemplateGenerateRequest, | |
| user = Depends(get_current_user_required) | |
| ): | |
| """Generate a new template using AI (does not save to database)""" | |
| try: | |
| # Create service with user_id for proper landppt config handling | |
| user_template_service = GlobalMasterTemplateService(user_id=user.id) | |
| # Credits: only bill LandPPT official provider. | |
| provider_name = None | |
| if app_config.enable_credits_system: | |
| try: | |
| _, template_settings = await user_template_service._get_template_role_provider_async() | |
| provider_name = template_settings.get("provider") | |
| except Exception as e: | |
| logger.warning(f"Failed to resolve template provider for credits check: {e}") | |
| if (provider_name or "").strip().lower() == "landppt": | |
| async with AsyncSessionLocal() as session: | |
| credits_service = CreditsService(session) | |
| required = credits_service.get_operation_cost("template_generation", 1) | |
| balance = await credits_service.get_balance(user.id) | |
| if balance < required: | |
| return { | |
| "success": False, | |
| "message": f"积分不足,模板生成需要{required}积分,当前余额{balance}积分", | |
| "required": required, | |
| "balance": balance, | |
| } | |
| # 准备参考图片数据(支持多张) | |
| reference_images_data: list = [] | |
| if request.reference_images: | |
| for img in request.reference_images: | |
| reference_images_data.append({ | |
| "filename": img.filename, | |
| "data": img.data, | |
| "size": img.size, | |
| "type": img.type, | |
| }) | |
| # Backward compat: single reference_image → list | |
| reference_image_data = None | |
| if request.reference_image: | |
| reference_image_data = { | |
| "filename": request.reference_image.filename, | |
| "data": request.reference_image.data, | |
| "size": request.reference_image.size, | |
| "type": request.reference_image.type, | |
| } | |
| if not reference_images_data: | |
| reference_images_data = [reference_image_data] | |
| reference_pptx_data = None | |
| if getattr(request, "reference_pptx", None): | |
| reference_pptx_data = { | |
| "filename": request.reference_pptx.filename, | |
| "data": request.reference_pptx.data, | |
| "size": request.reference_pptx.size, | |
| "type": request.reference_pptx.type, | |
| } | |
| # 使用AI生成服务(不保存到数据库) | |
| result = await user_template_service.generate_template_with_ai( | |
| prompt=request.prompt, | |
| template_name=request.template_name, | |
| description=request.description, | |
| tags=request.tags, | |
| generation_mode=request.generation_mode, | |
| reference_image=reference_image_data, | |
| reference_images=reference_images_data or None, | |
| reference_pptx=reference_pptx_data, | |
| ) | |
| # Deduct credits after successful generation. | |
| if app_config.enable_credits_system and (provider_name or "").strip().lower() == "landppt": | |
| try: | |
| async with AsyncSessionLocal() as session: | |
| credits_service = CreditsService(session) | |
| await credits_service.consume_credits( | |
| user_id=user.id, | |
| operation_type="template_generation", | |
| quantity=1, | |
| description="AI模板生成", | |
| reference_id=f"global_master_template:{request.template_name or ''}".rstrip(":"), | |
| ) | |
| except Exception as e: | |
| logger.error(f"Failed to consume credits for template generation: {e}") | |
| return { | |
| "success": True, | |
| "message": "模板生成完成!", | |
| "data": result | |
| } | |
| except Exception as e: | |
| logger.error(f"Failed to generate template with AI: {e}") | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def save_generated_template( | |
| request: dict, | |
| user=Depends(get_current_user_required), | |
| ): | |
| """Save a generated template after user confirmation""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| # Extract template data from request | |
| template_data = { | |
| 'template_name': request.get('template_name'), | |
| 'description': request.get('description', ''), | |
| 'html_template': request.get('html_template'), | |
| 'tags': request.get('tags', []), | |
| 'created_by': 'AI' | |
| } | |
| # Add timestamp to avoid name conflicts | |
| import time | |
| timestamp = int(time.time()) | |
| template_data['template_name'] = f"{template_data['template_name']}_{timestamp}" | |
| result = await template_service.create_template(template_data) | |
| return GlobalMasterTemplateResponse(**result) | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except Exception as e: | |
| logger.error(f"Failed to save generated template: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to save template") | |
| async def adjust_template( | |
| payload: dict, | |
| user=Depends(get_current_user_required), | |
| http_request: Request = None, | |
| ): | |
| """Adjust a generated template based on user feedback""" | |
| from fastapi.responses import StreamingResponse | |
| import json | |
| user_template_service = GlobalMasterTemplateService(user_id=user.id) | |
| template_data = payload.get("template_data") or {} | |
| current_html = ( | |
| payload.get("html_template") | |
| or template_data.get("html_template") | |
| or template_data.get("html") | |
| ) | |
| adjustment_request = ( | |
| payload.get("adjustment_request") | |
| or payload.get("adjustment") | |
| or template_data.get("adjustment_request") | |
| ) | |
| template_name = ( | |
| payload.get("template_name") | |
| or template_data.get("template_name") | |
| or "模板" | |
| ) | |
| # Decide response mode: | |
| # - SSE when the client explicitly asks (`Accept: text/event-stream`) or `stream=true`. | |
| accept = ((http_request.headers.get("accept") if http_request else "") or "").lower() | |
| stream_flag = payload.get("stream") | |
| want_stream = ("text/event-stream" in accept) or bool(stream_flag) | |
| if not (isinstance(current_html, str) and current_html.strip()): | |
| if want_stream: | |
| async def _err(): | |
| yield f"data: {json.dumps({'type': 'error', 'message': 'html_template is required'})}\n\n" | |
| return StreamingResponse(_err(), media_type="text/event-stream", headers={"Cache-Control": "no-cache"}) | |
| raise HTTPException(status_code=400, detail="html_template is required") | |
| if not (isinstance(adjustment_request, str) and adjustment_request.strip()): | |
| if want_stream: | |
| async def _err(): | |
| yield f"data: {json.dumps({'type': 'error', 'message': 'adjustment_request is required'})}\n\n" | |
| return StreamingResponse(_err(), media_type="text/event-stream", headers={"Cache-Control": "no-cache"}) | |
| raise HTTPException(status_code=400, detail="adjustment_request is required") | |
| provider_name = None | |
| required = 0 | |
| balance = 0 | |
| if app_config.enable_credits_system: | |
| try: | |
| _, template_settings = await user_template_service._get_template_role_provider_async() | |
| provider_name = template_settings.get("provider") | |
| except Exception as e: | |
| logger.warning(f"Failed to resolve template provider for credits check: {e}") | |
| if (provider_name or "").strip().lower() == "landppt": | |
| async with AsyncSessionLocal() as session: | |
| credits_service = CreditsService(session) | |
| required = credits_service.get_operation_cost("template_generation", 1) | |
| balance = await credits_service.get_balance(user.id) | |
| if balance < required: | |
| message = f"积分不足,模板调整需要{required}积分,当前余额{balance}积分" | |
| if want_stream: | |
| async def _err(): | |
| yield f"data: {json.dumps({'type': 'error', 'message': message, 'required': required, 'balance': balance})}\n\n" | |
| return StreamingResponse(_err(), media_type="text/event-stream", headers={"Cache-Control": "no-cache"}) | |
| return {"success": False, "message": message, "required": required, "balance": balance} | |
| if want_stream: | |
| async def adjust_stream(): | |
| try: | |
| # Send initial status | |
| yield f"data: {json.dumps({'type': 'status', 'message': '正在分析调整需求...'})}\n\n" | |
| credits_consumed = False | |
| async for chunk in user_template_service.adjust_template_with_ai_stream( | |
| current_html=current_html, | |
| adjustment_request=adjustment_request, | |
| template_name=template_name, | |
| ): | |
| if ( | |
| app_config.enable_credits_system | |
| and (provider_name or "").strip().lower() == "landppt" | |
| and (chunk or {}).get("type") == "complete" | |
| and not credits_consumed | |
| ): | |
| try: | |
| async with AsyncSessionLocal() as session: | |
| credits_service = CreditsService(session) | |
| await credits_service.consume_credits( | |
| user_id=user.id, | |
| operation_type="template_generation", | |
| quantity=1, | |
| description="AI模板调整", | |
| reference_id=f"global_master_template_adjust:{template_name}".rstrip(":"), | |
| ) | |
| credits_consumed = True | |
| except Exception as e: | |
| logger.error(f"Failed to consume credits for template adjustment: {e}") | |
| yield f"data: {json.dumps(chunk)}\n\n" | |
| except Exception as e: | |
| logger.error(f"Failed to adjust template: {e}") | |
| yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n" | |
| return StreamingResponse( | |
| adjust_stream(), | |
| media_type="text/event-stream", | |
| headers={ | |
| "Cache-Control": "no-cache", | |
| "Connection": "keep-alive", | |
| }, | |
| ) | |
| # Default: return JSON (more compatible with `apiClient` callers). | |
| complete_event = None | |
| async for chunk in user_template_service.adjust_template_with_ai_stream( | |
| current_html=current_html, | |
| adjustment_request=adjustment_request, | |
| template_name=template_name, | |
| ): | |
| if (chunk or {}).get("type") == "error": | |
| raise HTTPException(status_code=500, detail=(chunk or {}).get("message") or "Template adjustment failed") | |
| if (chunk or {}).get("type") == "complete" and (chunk or {}).get("html_template"): | |
| complete_event = chunk | |
| break | |
| if not complete_event: | |
| raise HTTPException(status_code=500, detail="Template adjustment failed") | |
| if app_config.enable_credits_system and (provider_name or "").strip().lower() == "landppt": | |
| try: | |
| async with AsyncSessionLocal() as session: | |
| credits_service = CreditsService(session) | |
| await credits_service.consume_credits( | |
| user_id=user.id, | |
| operation_type="template_generation", | |
| quantity=1, | |
| description="AI模板调整", | |
| reference_id=f"global_master_template_adjust:{template_name}".rstrip(":"), | |
| ) | |
| except Exception as e: | |
| logger.error(f"Failed to consume credits for template adjustment: {e}") | |
| return {"success": True, "data": complete_event} | |
| async def select_template_for_project( | |
| request: TemplateSelectionRequest, | |
| user=Depends(get_current_user_required), | |
| ): | |
| """Select a template for PPT generation""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| if request.selected_template_id: | |
| # Get the selected template | |
| template = await template_service.get_template_by_id(request.selected_template_id) | |
| if not template: | |
| raise HTTPException(status_code=404, detail="Selected template not found") | |
| # Increment usage count | |
| await template_service.increment_template_usage(request.selected_template_id) | |
| return TemplateSelectionResponse( | |
| success=True, | |
| message="Template selected successfully", | |
| selected_template=GlobalMasterTemplateResponse(**template) | |
| ) | |
| else: | |
| # Use default template | |
| template = await template_service.get_default_template() | |
| if not template: | |
| raise HTTPException(status_code=404, detail="No default template found") | |
| # Increment usage count | |
| await template_service.increment_template_usage(template['id']) | |
| return TemplateSelectionResponse( | |
| success=True, | |
| message="Default template selected", | |
| selected_template=GlobalMasterTemplateResponse(**template) | |
| ) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to select template: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to select template") | |
| async def duplicate_template( | |
| template_id: int, | |
| new_name: str = Query(..., description="New template name"), | |
| user=Depends(get_current_user_required), | |
| ): | |
| """Duplicate an existing template""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| # Get the original template | |
| original = await template_service.get_template_by_id(template_id) | |
| if not original: | |
| raise HTTPException(status_code=404, detail="Template not found") | |
| # Create duplicate data | |
| duplicate_data = { | |
| 'template_name': new_name, | |
| 'description': f"复制自: {original['template_name']}", | |
| 'html_template': original['html_template'], | |
| 'tags': original['tags'] + ['复制'], | |
| 'created_by': 'duplicate' | |
| } | |
| result = await template_service.create_template(duplicate_data) | |
| return GlobalMasterTemplateResponse(**result) | |
| except ValueError as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to duplicate template {template_id}: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to duplicate template") | |
| async def get_template_preview(template_id: int, user=Depends(get_current_user_required)): | |
| """Get template preview data""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| template = await template_service.get_template_by_id(template_id) | |
| if not template: | |
| raise HTTPException(status_code=404, detail="Template not found") | |
| return { | |
| "id": template['id'], | |
| "template_name": template['template_name'], | |
| "preview_image": template['preview_image'], | |
| "html_template": template['html_template'] | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to get template preview {template_id}: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to get template preview") | |
| # Add increment usage endpoint for internal use | |
| async def increment_template_usage(template_id: int, user=Depends(get_current_user_required)): | |
| """Increment template usage count (internal use)""" | |
| try: | |
| template_service = _template_service_for_user(user) | |
| success = await template_service.increment_template_usage(template_id) | |
| if not success: | |
| raise HTTPException(status_code=404, detail="Template not found") | |
| return {"success": True, "message": "Usage count incremented"} | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"Failed to increment usage for template {template_id}: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to increment usage count") | |