ppt-web / src /landppt /web /route_modules /speech_script_routes.py
26fwyzpz6f-max
Clean deploy without binary files
6aecb2e
Raw
History Blame Contribute Delete
41.3 kB
"""
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 # "single", "multi", "full"
slide_indices: Optional[List[int]] = None # For single and multi generation
customization: Dict[str, Any] = {} # Customization options
language: str = "zh"
class SpeechScriptExportRequest(BaseModel):
export_format: str # "docx", "markdown"
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
# Generate task ID for progress tracking
task_id = str(uuid.uuid4())
# Get project
project = await ppt_service.project_manager.get_project(project_id, user_id=user.id)
if not project:
return {
"success": False,
"error": "Project not found"
}
# Check if slides data exists
if not project.slides_data or len(project.slides_data) == 0:
return {
"success": False,
"error": "No slides data available"
}
# Import speech script service
from ...services.speech_script_service import SpeechScriptService, SpeechScriptCustomization
from ...services.speech_script_service import SpeechTone, TargetAudience, LanguageComplexity
# Initialize service with user_id for database AI config lookup
speech_service = SpeechScriptService(user_id=user.id)
await speech_service.initialize_async()
# Parse customization options
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')
)
# Validate request parameters
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"
}
# Create progress task immediately (Valkey-backed) to avoid first-poll "Task not found" in multi-worker setups.
if request.generation_type == "full":
slide_indices = list(range(total_project_slides))
else:
slide_indices = normalized_slide_indices
# Check credits before scheduling any AI work (only billable for LandPPT provider).
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,
)
# Start async generation task
async def generate_async():
try:
logger.info(f"Starting async generation for task {task_id}")
# Generate scripts based on type
if request.generation_type == "single":
# Use multi_slide_scripts_with_retry for single slide to get progress tracking
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
)
# Save scripts to database if successful
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}")
# Ensure all changes are committed before closing
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}")
# NOW mark the task as completed after database save
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))
# Start the async task
asyncio.create_task(generate_async())
# Return immediately with task_id
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:
# Get project for title
project = await ppt_service.project_manager.get_project(project_id, user_id=user.id)
if not project:
return {
"success": False,
"error": "Project not found"
}
# Import exporter
from ...services.speech_script_exporter import get_speech_script_exporter
from ...services.speech_script_service import SlideScriptData
exporter = get_speech_script_exporter()
# Validate scripts data
if not request.scripts_data or len(request.scripts_data) == 0:
return {
"success": False,
"error": "No speech scripts data provided"
}
# Convert scripts data to SlideScriptData objects
scripts = []
for script_data in request.scripts_data:
# Validate required fields
if not script_data.get('script_content'):
continue # Skip empty scripts
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)
# Check if we have any valid scripts after filtering
if not scripts:
return {
"success": False,
"error": "No valid speech scripts found"
}
# Prepare metadata
metadata = {}
if request.include_metadata:
# Calculate total estimated duration from all scripts
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': {}
}
# Export based on format
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
)
# Return file response
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
)
# Return file response
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()
# Expire all objects to ensure fresh data from database
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}")
# 转换为JSON格式
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"
}
# 获取进度信息(多worker下从Valkey读取)
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)
# 转换为API格式
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"
}
# 获取进度信息(多worker下从Valkey读取)
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)
}