| """ |
| Generated route module extracted from the legacy web router. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import asyncio |
| import json |
| import logging |
| import os |
| import re |
| import tempfile |
| import time |
| import uuid |
| import urllib.parse |
| import urllib.request |
| from pathlib import Path |
| from typing import Any, Dict, List, Optional |
|
|
| import aiohttp |
| from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, Response, UploadFile |
| from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, StreamingResponse |
| from pydantic import BaseModel |
|
|
| from ...ai import AIMessage, MessageRole, get_ai_provider, get_role_provider |
| from ...api.models import FileOutlineGenerationRequest, PPTGenerationRequest, PPTProject, TodoBoard |
| from ...auth.middleware import get_current_user_optional, get_current_user_required |
| from ...core.config import ai_config, app_config, resolve_timeout_seconds |
| from ...database.database import AsyncSessionLocal, get_db |
| from ...database.models import User |
| from ...services.enhanced_ppt_service import EnhancedPPTService |
| from ...services.pdf_to_pptx_converter import get_pdf_to_pptx_converter |
| from ...services.pyppeteer_pdf_converter import get_pdf_converter |
| from ...utils.thread_pool import run_blocking_io, to_thread |
| from .support import ( |
| _apply_no_store_headers, |
| check_credits_for_operation, |
| consume_credits_for_operation, |
| get_ppt_service_for_user, |
| logger, |
| ppt_service, |
| templates, |
| ) |
|
|
| router = APIRouter() |
|
|
|
|
| def _is_billable_provider(provider_name: str | None) -> bool: |
| return (provider_name or "").strip().lower() == "landppt" |
|
|
|
|
| class SpeechScriptGenerationRequest(BaseModel): |
| generation_type: str |
| slide_indices: Optional[List[int]] = None |
| customization: Dict[str, Any] = {} |
| language: str = "zh" |
|
|
|
|
| class SpeechScriptExportRequest(BaseModel): |
| export_format: str |
| scripts_data: List[Dict[str, Any]] |
| include_metadata: bool = True |
|
|
|
|
| class SpeechScriptHumanizeItem(BaseModel): |
| slide_index: int |
| slide_title: Optional[str] = None |
| script_content: str |
|
|
|
|
| class SpeechScriptHumanizeRequest(BaseModel): |
| scripts: List[SpeechScriptHumanizeItem] |
| language: str = "zh" |
|
|
|
|
| def _safe_speech_enum(enum_cls, raw_value, default_value): |
| try: |
| return enum_cls(raw_value) |
| except Exception: |
| return default_value |
|
|
|
|
| @router.post("/api/projects/{project_id}/speech-script/generate") |
| async def generate_speech_script( |
| project_id: str, |
| request: SpeechScriptGenerationRequest, |
| user: User = Depends(get_current_user_required) |
| ): |
| """Generate speech scripts for presentation slides""" |
| try: |
| import uuid |
| import asyncio |
|
|
| |
| task_id = str(uuid.uuid4()) |
|
|
| |
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| |
| if not project.slides_data or len(project.slides_data) == 0: |
| return { |
| "success": False, |
| "error": "No slides data available" |
| } |
|
|
| |
| from ...services.speech_script_service import SpeechScriptService, SpeechScriptCustomization |
| from ...services.speech_script_service import SpeechTone, TargetAudience, LanguageComplexity |
|
|
| |
| speech_service = SpeechScriptService(user_id=user.id) |
| await speech_service.initialize_async() |
|
|
|
|
| |
| customization_data = request.customization |
| customization = SpeechScriptCustomization( |
| language=(request.language or "zh"), |
| tone=SpeechTone(customization_data.get('tone', 'conversational')), |
| target_audience=TargetAudience(customization_data.get('target_audience', 'general_public')), |
| language_complexity=LanguageComplexity(customization_data.get('language_complexity', 'moderate')), |
| custom_style_prompt=customization_data.get('custom_style_prompt'), |
| include_transitions=customization_data.get('include_transitions', True), |
| include_timing_notes=customization_data.get('include_timing_notes', False), |
| speaking_pace=customization_data.get('speaking_pace', 'normal') |
| ) |
|
|
| |
| total_project_slides = len(project.slides_data) |
| raw_slide_indices = request.slide_indices or [] |
| normalized_slide_indices: List[int] = [] |
| seen_slide_indices = set() |
| for raw_index in raw_slide_indices: |
| try: |
| slide_index = int(raw_index) |
| except (TypeError, ValueError): |
| continue |
| if slide_index < 0 or slide_index >= total_project_slides or slide_index in seen_slide_indices: |
| continue |
| seen_slide_indices.add(slide_index) |
| normalized_slide_indices.append(slide_index) |
|
|
| if request.generation_type == "single": |
| if len(normalized_slide_indices) != 1: |
| return { |
| "success": False, |
| "error": "Single generation requires exactly one valid slide index" |
| } |
| elif request.generation_type == "multi": |
| if not normalized_slide_indices: |
| return { |
| "success": False, |
| "error": "Multi generation requires valid slide indices" |
| } |
| elif request.generation_type != "full": |
| return { |
| "success": False, |
| "error": "Invalid generation type" |
| } |
|
|
| |
| if request.generation_type == "full": |
| slide_indices = list(range(total_project_slides)) |
| else: |
| slide_indices = normalized_slide_indices |
|
|
| |
| speech_provider_name = (speech_service.provider_settings or {}).get("provider") |
| has_credits, required, balance = await check_credits_for_operation( |
| user.id, "ai_other", len(slide_indices), provider_name=speech_provider_name |
| ) |
| if not has_credits: |
| return { |
| "success": False, |
| "error": f"积分不足,演讲稿生成需要 {required} 积分,当前余额 {balance} 积分", |
| } |
|
|
| from ...services.progress_tracker import progress_tracker |
| await progress_tracker.create_task_async( |
| task_id=task_id, |
| project_id=project_id, |
| total_slides=len(slide_indices), |
| overwrite=True, |
| ) |
|
|
| |
| async def generate_async(): |
| try: |
| logger.info(f"Starting async generation for task {task_id}") |
|
|
| |
| if request.generation_type == "single": |
| |
| result = await speech_service.generate_multi_slide_scripts_with_retry( |
| project, slide_indices, customization, task_id=task_id |
| ) |
| elif request.generation_type == "multi": |
| result = await speech_service.generate_multi_slide_scripts_with_retry( |
| project, slide_indices, customization, task_id=task_id |
| ) |
| elif request.generation_type == "full": |
| result = await speech_service.generate_full_presentation_scripts( |
| project, customization, progress_callback=None, task_id=task_id |
| ) |
|
|
| |
| if result.success: |
| logger.info(f"Generation successful for task {task_id}, saving to database") |
| from ...services.speech_script_repository import SpeechScriptRepository |
| repo = SpeechScriptRepository() |
|
|
| generation_params = { |
| 'generation_type': request.generation_type, |
| 'tone': customization.tone.value, |
| 'target_audience': customization.target_audience.value, |
| 'language_complexity': customization.language_complexity.value, |
| 'custom_audience': request.customization.get('custom_audience'), |
| 'custom_style_prompt': customization.custom_style_prompt, |
| 'include_transitions': customization.include_transitions, |
| 'include_timing_notes': customization.include_timing_notes, |
| 'speaking_pace': customization.speaking_pace |
| } |
|
|
| saved_count = 0 |
| for script in result.scripts: |
| await repo.save_speech_script( |
| project_id=project_id, |
| slide_index=script.slide_index, |
| language=(request.language or "zh"), |
| slide_title=script.slide_title, |
| script_content=script.script_content, |
| generation_params=generation_params, |
| estimated_duration=script.estimated_duration |
| ) |
| saved_count += 1 |
| logger.debug(f"Saved script {saved_count}/{len(result.scripts)} for slide {script.slide_index}") |
|
|
| |
| repo.db.commit() |
| repo.close() |
| logger.info(f"All {saved_count} scripts saved and committed to database for task {task_id}") |
|
|
| if saved_count > 0: |
| billed, bill_message = await consume_credits_for_operation( |
| user.id, |
| "ai_other", |
| saved_count, |
| description=f"演讲稿生成: {saved_count}页", |
| reference_id=project_id, |
| provider_name=speech_provider_name, |
| ) |
| if not billed and _is_billable_provider(speech_provider_name): |
| logger.error(f"Speech script billing failed for task {task_id}: {bill_message}") |
|
|
| |
| await progress_tracker.complete_task_async(task_id, f"生成完成!成功 {saved_count} 页") |
| logger.info(f"Task {task_id} marked as completed") |
| else: |
| logger.error(f"Generation failed for task {task_id}: {result.error_message}") |
| await progress_tracker.fail_task_async(task_id, result.error_message or "生成失败") |
|
|
| except Exception as e: |
| logger.error(f"Async speech script generation failed for task {task_id}: {e}") |
| await progress_tracker.fail_task_async(task_id, str(e)) |
|
|
| |
| asyncio.create_task(generate_async()) |
|
|
| |
| return { |
| "success": True, |
| "task_id": task_id, |
| "message": "演讲稿生成已开始,请查看进度" |
| } |
|
|
| except Exception as e: |
| logger.error(f"Speech script generation failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|
|
|
| @router.post("/api/projects/{project_id}/speech-script/export") |
| async def export_speech_script( |
| project_id: str, |
| request: SpeechScriptExportRequest, |
| user: User = Depends(get_current_user_required) |
| ): |
| """Export speech scripts to document format""" |
| try: |
| |
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| |
| from ...services.speech_script_exporter import get_speech_script_exporter |
| from ...services.speech_script_service import SlideScriptData |
|
|
| exporter = get_speech_script_exporter() |
|
|
| |
| if not request.scripts_data or len(request.scripts_data) == 0: |
| return { |
| "success": False, |
| "error": "No speech scripts data provided" |
| } |
|
|
| |
| scripts = [] |
| for script_data in request.scripts_data: |
| |
| if not script_data.get('script_content'): |
| continue |
|
|
| script = SlideScriptData( |
| slide_index=script_data.get('slide_index', 0), |
| slide_title=script_data.get('slide_title', ''), |
| script_content=script_data.get('script_content', ''), |
| estimated_duration=script_data.get('estimated_duration'), |
| speaker_notes=script_data.get('speaker_notes') |
| ) |
| scripts.append(script) |
|
|
| |
| if not scripts: |
| return { |
| "success": False, |
| "error": "No valid speech scripts found" |
| } |
|
|
| |
| metadata = {} |
| if request.include_metadata: |
| |
| total_duration = None |
| if scripts: |
| total_minutes = 0 |
| for script in scripts: |
| if script.estimated_duration and '分钟' in script.estimated_duration: |
| try: |
| minutes = float(script.estimated_duration.replace('分钟', '')) |
| total_minutes += minutes |
| except ValueError: |
| pass |
| if total_minutes > 0: |
| total_duration = f"{total_minutes:.1f}分钟" |
|
|
| metadata = { |
| 'generation_time': time.time(), |
| 'total_estimated_duration': total_duration, |
| 'customization': {} |
| } |
|
|
| |
| if request.export_format == "docx": |
| if not exporter.is_docx_available(): |
| return { |
| "success": False, |
| "error": "DOCX export not available. Please install python-docx." |
| } |
|
|
| docx_content = await exporter.export_to_docx( |
| scripts, project.topic, metadata |
| ) |
|
|
| |
| import urllib.parse |
| filename = f"{project.topic}_演讲稿.docx" |
| safe_filename = urllib.parse.quote(filename, safe='') |
|
|
| from fastapi.responses import Response |
| return Response( |
| content=docx_content, |
| media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", |
| headers={ |
| "Content-Disposition": f"attachment; filename*=UTF-8''{safe_filename}" |
| } |
| ) |
|
|
| elif request.export_format == "markdown": |
| markdown_content = await exporter.export_to_markdown( |
| scripts, project.topic, metadata |
| ) |
|
|
| |
| import urllib.parse |
| filename = f"{project.topic}_演讲稿.md" |
| safe_filename = urllib.parse.quote(filename, safe='') |
|
|
| from fastapi.responses import Response |
| return Response( |
| content=markdown_content.encode('utf-8'), |
| media_type="text/markdown", |
| headers={ |
| "Content-Disposition": f"attachment; filename*=UTF-8''{safe_filename}" |
| } |
| ) |
|
|
| else: |
| return { |
| "success": False, |
| "error": "Unsupported export format" |
| } |
|
|
| except Exception as e: |
| logger.error(f"Speech script export failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|
|
|
| @router.get("/api/projects/{project_id}/speech-scripts") |
| async def get_current_speech_scripts( |
| project_id: str, |
| language: str = "zh", |
| user: User = Depends(get_current_user_required) |
| ): |
| """获取项目的当前演讲稿""" |
| try: |
| from ...services.speech_script_repository import SpeechScriptRepository |
|
|
| |
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| repo = SpeechScriptRepository() |
|
|
| |
| repo.db.expire_all() |
|
|
| |
| scripts = await repo.get_current_speech_scripts_by_project(project_id, language=language) |
| logger.info(f"Found {len(scripts)} speech scripts for project {project_id}") |
|
|
| |
| scripts_data = [] |
| for script in scripts: |
| scripts_data.append({ |
| "id": script.id, |
| "slide_index": script.slide_index, |
| "language": getattr(script, "language", "zh"), |
| "slide_title": script.slide_title, |
| "script_content": script.script_content, |
| "estimated_duration": script.estimated_duration, |
| "speaker_notes": script.speaker_notes, |
| "generation_type": script.generation_type, |
| "tone": script.tone, |
| "target_audience": script.target_audience, |
| "custom_audience": script.custom_audience, |
| "language_complexity": script.language_complexity, |
| "speaking_pace": script.speaking_pace, |
| "custom_style_prompt": script.custom_style_prompt, |
| "include_transitions": script.include_transitions, |
| "include_timing_notes": script.include_timing_notes, |
| "created_at": script.created_at, |
| "updated_at": script.updated_at |
| }) |
|
|
| repo.close() |
|
|
| response = JSONResponse({ |
| "success": True, |
| "scripts": scripts_data |
| }) |
| _apply_no_store_headers(response) |
| return response |
|
|
| except Exception as e: |
| logger.error(f"Get current speech scripts failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|
|
|
| @router.post("/api/projects/{project_id}/speech-scripts/humanize") |
| async def humanize_speech_scripts( |
| project_id: str, |
| request: SpeechScriptHumanizeRequest, |
| user: User = Depends(get_current_user_required) |
| ): |
| """按 Humanizer-zh 风格将演讲稿改写为更自然的人话表达。""" |
| try: |
| from ...services.speech_script_repository import SpeechScriptRepository |
| from ...services.speech_script_service import ( |
| LanguageComplexity, |
| SpeechScriptCustomization, |
| SpeechScriptService, |
| SpeechTone, |
| TargetAudience, |
| ) |
| from ...services.progress_tracker import progress_tracker |
|
|
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| requested_language = ((request.language or "zh").strip().lower() or "zh") |
| normalized_items: List[SpeechScriptHumanizeItem] = [] |
| seen_slide_indices = set() |
| for item in request.scripts or []: |
| content = (item.script_content or "").strip() |
| if not content: |
| continue |
| if item.slide_index in seen_slide_indices: |
| continue |
| seen_slide_indices.add(item.slide_index) |
| normalized_items.append(item) |
|
|
| if not normalized_items: |
| return { |
| "success": False, |
| "error": "没有可人话化的演讲稿内容" |
| } |
|
|
| user_id = user.id |
| speech_service = SpeechScriptService(user_id=user.id) |
| await speech_service.initialize_async() |
|
|
| speech_provider_name = (speech_service.provider_settings or {}).get("provider") |
| has_credits, required, balance = await check_credits_for_operation( |
| user_id, "ai_other", len(normalized_items), provider_name=speech_provider_name |
| ) |
| if not has_credits: |
| return { |
| "success": False, |
| "error": f"积分不足,演讲稿一键人话需要 {required} 积分,当前余额 {balance} 积分", |
| } |
|
|
| task_id = str(uuid.uuid4()) |
| await progress_tracker.create_task_async( |
| task_id=task_id, |
| project_id=project_id, |
| total_slides=len(normalized_items), |
| overwrite=True, |
| ) |
| await progress_tracker.update_progress_async(task_id, message="开始一键人话...") |
|
|
| async def humanize_async(): |
| repo = None |
| try: |
| repo = SpeechScriptRepository() |
| updated_scripts: List[Dict[str, Any]] = [] |
| failed_slides: List[Dict[str, Any]] = [] |
|
|
| for item in normalized_items: |
| fallback_title = (item.slide_title or f"第{item.slide_index + 1}页").strip() |
| await progress_tracker.update_progress_async( |
| task_id, |
| current_slide=item.slide_index, |
| current_slide_title=fallback_title, |
| message=f"正在处理第{item.slide_index + 1}页演讲稿...", |
| ) |
|
|
| existing_script = await repo.get_speech_script_by_slide( |
| project_id, |
| item.slide_index, |
| language=requested_language, |
| ) |
| if not existing_script: |
| missing_error = "演讲稿不存在" |
| failed_slides.append({ |
| "slide_index": item.slide_index, |
| "error": missing_error, |
| }) |
| await progress_tracker.add_slide_failed_async( |
| task_id, |
| item.slide_index, |
| fallback_title, |
| missing_error, |
| ) |
| continue |
|
|
| custom_style_prompt = (getattr(existing_script, "custom_style_prompt", None) or "").strip() |
| custom_audience = (getattr(existing_script, "custom_audience", None) or "").strip() |
| if custom_audience: |
| audience_prompt = f"额外受众要求:{custom_audience}" |
| custom_style_prompt = ( |
| f"{custom_style_prompt}\n{audience_prompt}".strip() |
| if custom_style_prompt |
| else audience_prompt |
| ) |
|
|
| customization = SpeechScriptCustomization( |
| language=requested_language, |
| tone=_safe_speech_enum( |
| SpeechTone, |
| getattr(existing_script, "tone", None), |
| SpeechTone.CONVERSATIONAL, |
| ), |
| target_audience=_safe_speech_enum( |
| TargetAudience, |
| getattr(existing_script, "target_audience", None), |
| TargetAudience.GENERAL_PUBLIC, |
| ), |
| language_complexity=_safe_speech_enum( |
| LanguageComplexity, |
| getattr(existing_script, "language_complexity", None), |
| LanguageComplexity.MODERATE, |
| ), |
| custom_style_prompt=custom_style_prompt or None, |
| include_transitions=bool(getattr(existing_script, "include_transitions", True)), |
| include_timing_notes=bool(getattr(existing_script, "include_timing_notes", False)), |
| speaking_pace=(getattr(existing_script, "speaking_pace", None) or "normal"), |
| ) |
|
|
| try: |
| humanized_content = await speech_service.humanize_script(item.script_content, customization) |
| if not humanized_content: |
| raise ValueError("AI 未返回可用内容") |
|
|
| existing_script.script_content = humanized_content |
| existing_script.slide_title = ( |
| item.slide_title |
| or existing_script.slide_title |
| or f"第{item.slide_index + 1}页" |
| ).strip() |
| existing_script.estimated_duration = speech_service._estimate_speaking_duration(humanized_content) |
| existing_script.updated_at = time.time() |
|
|
| updated_scripts.append({ |
| "id": existing_script.id, |
| "slide_index": existing_script.slide_index, |
| "language": existing_script.language, |
| "slide_title": existing_script.slide_title, |
| "script_content": existing_script.script_content, |
| "estimated_duration": existing_script.estimated_duration, |
| "speaker_notes": existing_script.speaker_notes, |
| "updated_at": existing_script.updated_at, |
| }) |
| await progress_tracker.add_slide_completed_async( |
| task_id, |
| item.slide_index, |
| existing_script.slide_title, |
| ) |
| except Exception as humanize_error: |
| error_text = str(humanize_error) |
| failed_slides.append({ |
| "slide_index": item.slide_index, |
| "error": error_text, |
| }) |
| await progress_tracker.add_slide_failed_async( |
| task_id, |
| item.slide_index, |
| getattr(existing_script, "slide_title", None) or fallback_title, |
| error_text, |
| ) |
|
|
| if not updated_scripts: |
| if repo and repo.db: |
| repo.db.rollback() |
| await progress_tracker.fail_task_async( |
| task_id, |
| failed_slides[0]["error"] if failed_slides else "一键人话失败", |
| ) |
| return |
|
|
| repo.db.commit() |
|
|
| billed, bill_message = await consume_credits_for_operation( |
| user_id, |
| "ai_other", |
| len(updated_scripts), |
| description=f"演讲稿一键人话: {len(updated_scripts)}页", |
| reference_id=project_id, |
| provider_name=speech_provider_name, |
| ) |
| if not billed and _is_billable_provider(speech_provider_name): |
| logger.error( |
| "Speech script humanize billing failed for project %s: %s", |
| project_id, |
| bill_message, |
| ) |
|
|
| message = f"一键人话完成!成功 {len(updated_scripts)} 页" |
| if failed_slides: |
| message += f",失败 {len(failed_slides)} 页" |
| await progress_tracker.complete_task_async(task_id, message) |
| except Exception as e: |
| if repo and repo.db: |
| repo.db.rollback() |
| logger.error(f"Humanize speech scripts async task failed: {e}") |
| await progress_tracker.fail_task_async(task_id, str(e)) |
| finally: |
| if repo: |
| repo.close() |
|
|
| asyncio.create_task(humanize_async()) |
|
|
| return { |
| "success": True, |
| "task_id": task_id, |
| "message": "演讲稿一键人话已开始,请查看进度", |
| } |
|
|
| except Exception as e: |
| logger.error(f"Humanize speech scripts failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|
| @router.delete("/api/projects/{project_id}/speech-scripts/slide/{slide_index}") |
| async def delete_speech_script_by_slide( |
| project_id: str, |
| slide_index: int, |
| language: str = "zh", |
| user: User = Depends(get_current_user_required) |
| ): |
| """删除指定幻灯片的演讲稿""" |
| try: |
| from ...services.speech_script_repository import SpeechScriptRepository |
|
|
| |
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| repo = SpeechScriptRepository() |
|
|
| |
| script = await repo.get_speech_script_by_slide(project_id, slide_index, language=language) |
| if not script: |
| return { |
| "success": False, |
| "error": "Speech script not found" |
| } |
|
|
| success = await repo.delete_speech_script(script.id) |
|
|
| return { |
| "success": success, |
| "message": f"第{slide_index + 1}页演讲稿已删除" if success else "删除演讲稿失败" |
| } |
|
|
| except Exception as e: |
| logger.error(f"Delete speech script failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|
|
|
| @router.get("/api/projects/{project_id}/speech-scripts/result/{task_id}") |
| async def get_speech_script_result( |
| project_id: str, |
| task_id: str, |
| language: str = "zh", |
| user: User = Depends(get_current_user_required) |
| ): |
| """获取演讲稿生成结果""" |
| try: |
| from ...services.progress_tracker import progress_tracker |
| from ...services.speech_script_repository import SpeechScriptRepository |
|
|
| |
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| |
| progress_info = await progress_tracker.get_progress_async(task_id) |
|
|
| if not progress_info: |
| return { |
| "success": False, |
| "error": "Task not found" |
| } |
|
|
| |
| if progress_info.project_id != project_id: |
| return { |
| "success": False, |
| "error": "Access denied" |
| } |
|
|
| |
| if progress_info.status != "completed": |
| return { |
| "success": False, |
| "error": "Task not completed yet", |
| "status": progress_info.status, |
| "progress": progress_info.to_dict() |
| } |
|
|
| |
| repo = SpeechScriptRepository() |
| scripts = await repo.get_current_speech_scripts_by_project(project_id, language=language) |
|
|
| |
| scripts_data = [] |
| total_duration_seconds = 0 |
|
|
| for script in scripts: |
| script_data = { |
| "slide_index": script.slide_index, |
| "slide_title": script.slide_title, |
| "script_content": script.script_content, |
| "estimated_duration": script.estimated_duration, |
| "speaker_notes": getattr(script, 'speaker_notes', None) |
| } |
| scripts_data.append(script_data) |
|
|
| |
| if script.estimated_duration: |
| try: |
| if '分钟' in script.estimated_duration: |
| minutes = float(script.estimated_duration.replace('分钟', '')) |
| total_duration_seconds += minutes * 60 |
| elif '秒' in script.estimated_duration: |
| seconds = float(script.estimated_duration.replace('秒', '')) |
| total_duration_seconds += seconds |
| except: |
| pass |
|
|
| |
| if total_duration_seconds < 60: |
| total_duration = f"{int(total_duration_seconds)}秒" |
| else: |
| minutes = total_duration_seconds / 60 |
| total_duration = f"{minutes:.1f}分钟" |
|
|
| repo.close() |
|
|
| return { |
| "success": True, |
| "scripts": scripts_data, |
| "total_estimated_duration": total_duration, |
| "generation_metadata": { |
| "task_id": task_id, |
| "completed_at": progress_info.last_update, |
| "total_slides": progress_info.total_slides, |
| "completed_slides": progress_info.completed_slides, |
| "failed_slides": progress_info.failed_slides, |
| "skipped_slides": progress_info.skipped_slides |
| } |
| } |
|
|
| except Exception as e: |
| logger.error(f"Get speech script result failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|
|
|
| @router.get("/api/projects/{project_id}/speech-scripts/progress/{task_id}") |
| async def get_speech_script_progress( |
| project_id: str, |
| task_id: str, |
| user: User = Depends(get_current_user_required) |
| ): |
| """获取演讲稿生成进度""" |
| try: |
| from ...services.progress_tracker import progress_tracker |
|
|
| |
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| |
| progress_info = await progress_tracker.get_progress_async(task_id) |
|
|
| if not progress_info: |
| return { |
| "success": False, |
| "error": "Task not found" |
| } |
|
|
| |
| if progress_info.project_id != project_id: |
| return { |
| "success": False, |
| "error": "Access denied" |
| } |
|
|
| return { |
| "success": True, |
| "progress": progress_info.to_dict() |
| } |
|
|
| except Exception as e: |
| logger.error(f"Get speech script progress failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|
|
|
| @router.get("/api/projects/{project_id}/speech-scripts/progress/{task_id}/stream") |
| async def stream_speech_script_progress( |
| project_id: str, |
| task_id: str, |
| request: Request, |
| user: User = Depends(get_current_user_required) |
| ): |
| """Stream speech script generation progress as SSE.""" |
| from ...services.progress_tracker import progress_tracker |
|
|
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| async def _project_not_found(): |
| payload = json.dumps({"success": False, "error": "Project not found"}, ensure_ascii=False) |
| yield f"event: progress_error\ndata: {payload}\n\n" |
|
|
| return StreamingResponse( |
| _project_not_found(), |
| media_type="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "X-Accel-Buffering": "no", |
| }, |
| ) |
|
|
| async def _events(): |
| last_payload: Optional[Dict[str, Any]] = None |
| last_heartbeat = time.time() |
|
|
| while True: |
| if await request.is_disconnected(): |
| break |
|
|
| progress_info = await progress_tracker.get_progress_async(task_id) |
| if not progress_info: |
| payload = {"success": False, "error": "Task not found"} |
| yield f"event: progress_error\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n" |
| break |
|
|
| if progress_info.project_id != project_id: |
| payload = {"success": False, "error": "Access denied"} |
| yield f"event: progress_error\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n" |
| break |
|
|
| payload = {"success": True, "progress": progress_info.to_dict()} |
| if payload != last_payload: |
| event_name = "done" if progress_info.status in {"completed", "failed"} else "progress" |
| yield f"event: {event_name}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n" |
| last_payload = payload |
| last_heartbeat = time.time() |
|
|
| if event_name == "done": |
| break |
| elif time.time() - last_heartbeat >= 15: |
| yield ": keepalive\n\n" |
| last_heartbeat = time.time() |
|
|
| await asyncio.sleep(1) |
|
|
| return StreamingResponse( |
| _events(), |
| media_type="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "X-Accel-Buffering": "no", |
| }, |
| ) |
|
|
|
|
| @router.put("/api/projects/{project_id}/speech-scripts/slide/{slide_index}") |
| async def update_speech_script_content( |
| project_id: str, |
| slide_index: int, |
| request: dict, |
| language: str = "zh", |
| user: User = Depends(get_current_user_required) |
| ): |
| """更新演讲稿内容""" |
| try: |
| from ...services.speech_script_repository import SpeechScriptRepository |
|
|
| |
| project = await ppt_service.project_manager.get_project(project_id, user_id=user.id) |
| if not project: |
| return { |
| "success": False, |
| "error": "Project not found" |
| } |
|
|
| |
| script_content = request.get('script_content', '').strip() |
| slide_title = request.get('slide_title', f'第{slide_index + 1}页') |
| estimated_duration = request.get('estimated_duration') |
| speaker_notes = request.get('speaker_notes') |
|
|
| if not script_content: |
| return { |
| "success": False, |
| "error": "演讲稿内容不能为空" |
| } |
|
|
| repo = SpeechScriptRepository() |
|
|
| |
| existing_script = await repo.get_speech_script_by_slide(project_id, slide_index, language=language) |
| if not existing_script: |
| return { |
| "success": False, |
| "error": "演讲稿不存在" |
| } |
|
|
| |
| existing_script.script_content = script_content |
| existing_script.slide_title = slide_title |
| if estimated_duration: |
| existing_script.estimated_duration = estimated_duration |
| if speaker_notes is not None: |
| existing_script.speaker_notes = speaker_notes |
| existing_script.updated_at = time.time() |
|
|
| repo.db.commit() |
| repo.db.refresh(existing_script) |
| repo.close() |
|
|
| return { |
| "success": True, |
| "message": "演讲稿已更新", |
| "script": { |
| "id": existing_script.id, |
| "slide_index": existing_script.slide_index, |
| "slide_title": existing_script.slide_title, |
| "script_content": existing_script.script_content, |
| "estimated_duration": existing_script.estimated_duration, |
| "speaker_notes": existing_script.speaker_notes, |
| "updated_at": existing_script.updated_at |
| } |
| } |
|
|
| except Exception as e: |
| logger.error(f"Update speech script content failed: {e}") |
| return { |
| "success": False, |
| "error": str(e) |
| } |
|
|