mirror of
https://github.com/sligter/LandPPT.git
synced 2026-08-29 07:56:41 +08:00
feat: enhance outline generation with custom audience and description parameters
This commit is contained in:
@@ -900,6 +900,9 @@ async def upload_file_and_generate_outline(
|
||||
focus_content: Optional[str] = Form(None),
|
||||
tech_highlights: Optional[str] = Form(None),
|
||||
target_audience: Optional[str] = Form(None),
|
||||
custom_audience: Optional[str] = Form(None),
|
||||
requirements: Optional[str] = Form(None),
|
||||
description: Optional[str] = Form(None),
|
||||
network_mode: bool = Form(False), # 是否启用联网搜索(与项目创建保持一致)
|
||||
language: str = Form("zh") # 语言参数
|
||||
):
|
||||
@@ -947,9 +950,11 @@ async def upload_file_and_generate_outline(
|
||||
context = {
|
||||
'scenario': scenario,
|
||||
'target_audience': target_audience or '普通大众',
|
||||
'requirements': '',
|
||||
'custom_audience': custom_audience or '',
|
||||
'requirements': requirements or '',
|
||||
'ppt_style': ppt_style,
|
||||
'description': f'文件数量: {len(files)}',
|
||||
'description': description or '',
|
||||
'source_summary': f'文件数量: {len(files)}',
|
||||
'file_processing_mode': file_processing_mode,
|
||||
}
|
||||
|
||||
@@ -991,7 +996,10 @@ async def upload_file_and_generate_outline(
|
||||
filename=final_filename,
|
||||
topic=topic,
|
||||
scenario=scenario,
|
||||
requirements="", # API调用暂时没有requirements参数
|
||||
requirements=requirements,
|
||||
target_audience=target_audience,
|
||||
custom_audience=custom_audience,
|
||||
description=description,
|
||||
page_count_mode=page_count_mode,
|
||||
min_pages=min_pages,
|
||||
max_pages=max_pages,
|
||||
@@ -1000,7 +1008,6 @@ async def upload_file_and_generate_outline(
|
||||
custom_style_prompt=custom_style_prompt,
|
||||
file_processing_mode=file_processing_mode,
|
||||
content_analysis_depth=content_analysis_depth,
|
||||
target_audience=target_audience,
|
||||
language=language
|
||||
)
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ class PPTGenerationRequest(BaseModel):
|
||||
user_id: Optional[int] = Field(None, description="User ID for project ownership")
|
||||
# 目标受众和风格相关参数
|
||||
target_audience: Optional[str] = Field(None, description="Target audience for the PPT")
|
||||
custom_audience: Optional[str] = Field(None, description="Custom audience details")
|
||||
ppt_style: str = Field("general", description="PPT style: 'general', 'conference', 'custom'")
|
||||
custom_style_prompt: Optional[str] = Field(None, description="Custom style prompt")
|
||||
include_transition_pages: bool = Field(False, description="Whether to add transition slides between major sections")
|
||||
@@ -201,6 +202,8 @@ class FileOutlineGenerationRequest(BaseModel):
|
||||
scenario: str = Field("general", description="PPT scenario type")
|
||||
requirements: Optional[str] = Field(None, description="Specific requirements from user")
|
||||
target_audience: Optional[str] = Field(None, description="Target audience for the PPT")
|
||||
custom_audience: Optional[str] = Field(None, description="Custom audience details")
|
||||
description: Optional[str] = Field(None, description="Additional description or requirements")
|
||||
language: str = Field("zh", description="Language for the PPT content: 'zh' for Chinese, 'en' for English")
|
||||
page_count_mode: str = Field("ai_decide", description="Page count mode: 'ai_decide', 'custom_range', 'fixed'")
|
||||
min_pages: Optional[int] = Field(8, description="Minimum pages for custom_range mode")
|
||||
|
||||
@@ -16,6 +16,7 @@ from ...utils.thread_pool import run_blocking_io
|
||||
from .outline_workflow_support import (
|
||||
build_file_info,
|
||||
build_processing_stats,
|
||||
build_project_requirements_text,
|
||||
build_transition_page_requirement_text,
|
||||
build_validation_requirements,
|
||||
create_outline_from_file_content,
|
||||
@@ -83,7 +84,7 @@ class OutlineWorkflowService:
|
||||
try:
|
||||
logger.info("Streaming file outline generation for %s", request.filename)
|
||||
project_requirements = (
|
||||
(getattr(request, "requirements", "") or "")
|
||||
build_project_requirements_text(request)
|
||||
+ build_transition_page_requirement_text(request)
|
||||
)
|
||||
try:
|
||||
@@ -99,7 +100,7 @@ class OutlineWorkflowService:
|
||||
project_scenario=request.scenario or "general",
|
||||
project_requirements=project_requirements,
|
||||
target_audience=getattr(request, "target_audience", "General audience"),
|
||||
custom_audience="",
|
||||
custom_audience=getattr(request, "custom_audience", "") or "",
|
||||
ppt_style=getattr(request, "ppt_style", "general"),
|
||||
custom_style_prompt=getattr(request, "custom_style_prompt", ""),
|
||||
page_count_mode=getattr(request, "page_count_mode", "ai_decide"),
|
||||
@@ -182,7 +183,7 @@ class OutlineWorkflowService:
|
||||
try:
|
||||
logger.info("Generating file outline for %s", request.filename)
|
||||
project_requirements = (
|
||||
(getattr(request, "requirements", "") or "")
|
||||
build_project_requirements_text(request)
|
||||
+ build_transition_page_requirement_text(request)
|
||||
)
|
||||
try:
|
||||
@@ -198,7 +199,7 @@ class OutlineWorkflowService:
|
||||
project_scenario=request.scenario or "general",
|
||||
project_requirements=project_requirements,
|
||||
target_audience=getattr(request, "target_audience", "General audience"),
|
||||
custom_audience="",
|
||||
custom_audience=getattr(request, "custom_audience", "") or "",
|
||||
ppt_style=getattr(request, "ppt_style", "general"),
|
||||
custom_style_prompt=getattr(request, "custom_style_prompt", ""),
|
||||
page_count_mode=getattr(request, "page_count_mode", "ai_decide"),
|
||||
|
||||
@@ -15,6 +15,9 @@ def build_validation_requirements(request: Any, outline_title: str) -> Dict[str,
|
||||
return {
|
||||
"topic": getattr(request, "topic", None) or outline_title or "Document Presentation",
|
||||
"target_audience": getattr(request, "target_audience", None) or "General audience",
|
||||
"custom_audience": getattr(request, "custom_audience", None) or "",
|
||||
"requirements": getattr(request, "requirements", None) or "",
|
||||
"description": getattr(request, "description", None) or "",
|
||||
"focus_content": list(getattr(request, "focus_content", []) or []),
|
||||
"tech_highlights": list(getattr(request, "tech_highlights", []) or []),
|
||||
"page_count_settings": {
|
||||
@@ -27,6 +30,20 @@ def build_validation_requirements(request: Any, outline_title: str) -> Dict[str,
|
||||
}
|
||||
|
||||
|
||||
def build_project_requirements_text(request: Any) -> str:
|
||||
parts = []
|
||||
|
||||
requirements = (getattr(request, "requirements", None) or "").strip()
|
||||
if requirements:
|
||||
parts.append(requirements)
|
||||
|
||||
description = (getattr(request, "description", None) or "").strip()
|
||||
if description and description not in requirements:
|
||||
parts.append(f"Additional user instructions:\n{description}")
|
||||
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def build_transition_page_requirement_text(request: Any) -> str:
|
||||
if not bool(getattr(request, "include_transition_pages", False)):
|
||||
return ""
|
||||
|
||||
@@ -48,6 +48,39 @@ class ProjectOutlineResearchService:
|
||||
def __getattr__(self, name: str):
|
||||
return getattr(self._service, name)
|
||||
|
||||
@staticmethod
|
||||
def _build_user_context_markdown(topic: str, context: Optional[Dict[str, Any]]) -> str:
|
||||
context = context or {}
|
||||
lines = ["## User Requirements\n"]
|
||||
|
||||
if topic:
|
||||
lines.append(f"- Topic: {topic}")
|
||||
for label, key in (
|
||||
("Scenario", "scenario"),
|
||||
("Target audience", "target_audience"),
|
||||
("Custom audience", "custom_audience"),
|
||||
("PPT style", "ppt_style"),
|
||||
("Custom style", "custom_style_prompt"),
|
||||
("Source summary", "source_summary"),
|
||||
):
|
||||
raw_value = context.get(key)
|
||||
value = raw_value.strip() if isinstance(raw_value, str) else raw_value
|
||||
if value:
|
||||
lines.append(f"- {label}: {value}")
|
||||
|
||||
requirements = (context.get("requirements") or "").strip()
|
||||
if requirements:
|
||||
lines.append("\n### Specific Requirements\n")
|
||||
lines.append(requirements)
|
||||
|
||||
description = (context.get("description") or "").strip()
|
||||
if description and description not in requirements:
|
||||
lines.append("\n### Additional Instructions\n")
|
||||
lines.append(description)
|
||||
|
||||
lines.append("\n---\n\n")
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _generate_outline_from_research_runtime(self, request: PPTGenerationRequest, page_count_settings: Dict[str, Any]=None) -> Optional[PPTOutline]:
|
||||
if not getattr(self, 'enhanced_research_service', None) and not getattr(self, 'research_service', None):
|
||||
try:
|
||||
@@ -68,6 +101,7 @@ class ProjectOutlineResearchService:
|
||||
research_input_context = {
|
||||
'scenario': request.scenario,
|
||||
'target_audience': getattr(request, 'target_audience', '普通大众'),
|
||||
'custom_audience': getattr(request, 'custom_audience', ''),
|
||||
'requirements': request.requirements,
|
||||
'ppt_style': getattr(request, 'ppt_style', 'general'),
|
||||
'description': getattr(request, 'description', ''),
|
||||
@@ -135,7 +169,7 @@ class ProjectOutlineResearchService:
|
||||
logger.info('Using %s research report file for outline generation: %s', provider, report_path)
|
||||
try:
|
||||
from ...api.models import FileOutlineGenerationRequest
|
||||
file_request = FileOutlineGenerationRequest(file_path=report_path, filename=Path(report_path).name, topic=request.topic, scenario=request.scenario, requirements=request.requirements, target_audience=getattr(request, 'target_audience', '普通大众'), ppt_style=getattr(request, 'ppt_style', 'general'), custom_style_prompt=getattr(request, 'custom_style_prompt', ''), include_transition_pages=bool(getattr(request, 'include_transition_pages', False)), page_count_mode=page_count_settings.get('mode', 'ai_decide') if page_count_settings else 'ai_decide', min_pages=page_count_settings.get('min_pages') if page_count_settings else None, max_pages=page_count_settings.get('max_pages') if page_count_settings else None, fixed_pages=page_count_settings.get('fixed_pages') if page_count_settings else None, language=request.language)
|
||||
file_request = FileOutlineGenerationRequest(file_path=report_path, filename=Path(report_path).name, topic=request.topic, scenario=request.scenario, requirements=request.requirements, target_audience=getattr(request, 'target_audience', '普通大众'), custom_audience=getattr(request, 'custom_audience', ''), description=getattr(request, 'description', ''), ppt_style=getattr(request, 'ppt_style', 'general'), custom_style_prompt=getattr(request, 'custom_style_prompt', ''), include_transition_pages=bool(getattr(request, 'include_transition_pages', False)), page_count_mode=page_count_settings.get('mode', 'ai_decide') if page_count_settings else 'ai_decide', min_pages=page_count_settings.get('min_pages') if page_count_settings else None, max_pages=page_count_settings.get('max_pages') if page_count_settings else None, fixed_pages=page_count_settings.get('fixed_pages') if page_count_settings else None, language=request.language)
|
||||
file_outline_result = await self.generate_outline_from_file(file_request)
|
||||
if file_outline_result.success and file_outline_result.outline:
|
||||
outline_data = file_outline_result.outline
|
||||
@@ -325,6 +359,7 @@ class ProjectOutlineResearchService:
|
||||
merged_content_parts.append(f'# {topic}\n')
|
||||
merged_content_parts.append(f"*整合文档 - 生成时间: {time.strftime('%Y-%m-%d %H:%M:%S')}*\n")
|
||||
merged_content_parts.append('---\n\n')
|
||||
merged_content_parts.append(self._build_user_context_markdown(topic, context))
|
||||
if research_markdown and os.path.exists(research_markdown):
|
||||
merged_content_parts.append('## 📡 联网搜索结果\n\n')
|
||||
with open(research_markdown, 'r', encoding='utf-8') as f:
|
||||
|
||||
@@ -119,7 +119,7 @@ class ProjectOutlineStreamingService:
|
||||
yield await self._build_streaming_research_status_event('research_skip', '已启用联网模式,但研究服务不可用,改为直接生成大纲...', 0.02)
|
||||
return
|
||||
|
||||
research_context_data = {'scenario': project.scenario, 'target_audience': confirmed_requirements.get('target_audience', '普通大众'), 'requirements': project.requirements, 'ppt_style': confirmed_requirements.get('ppt_style', 'general'), 'description': confirmed_requirements.get('description', '')}
|
||||
research_context_data = {'scenario': project.scenario, 'target_audience': confirmed_requirements.get('target_audience', '普通大众'), 'custom_audience': confirmed_requirements.get('custom_audience', ''), 'requirements': confirmed_requirements.get('requirements', project.requirements), 'ppt_style': confirmed_requirements.get('ppt_style', 'general'), 'description': confirmed_requirements.get('description', '')}
|
||||
research_language = 'zh'
|
||||
if project.project_metadata and isinstance(project.project_metadata, dict):
|
||||
research_language = project.project_metadata.get('language', 'zh')
|
||||
@@ -251,7 +251,7 @@ class ProjectOutlineStreamingService:
|
||||
language = 'zh'
|
||||
if project.project_metadata and isinstance(project.project_metadata, dict):
|
||||
language = project.project_metadata.get('language', 'zh')
|
||||
file_request = FileOutlineGenerationRequest(file_path=report_path, filename=Path(report_path).name, topic=confirmed_requirements.get('topic', project.topic), scenario=confirmed_requirements.get('type', project.scenario), requirements=confirmed_requirements.get('requirements', project.requirements), language=language, page_count_mode=confirmed_requirements.get('page_count_settings', {}).get('mode', 'ai_decide'), min_pages=confirmed_requirements.get('page_count_settings', {}).get('min_pages', 8), max_pages=confirmed_requirements.get('page_count_settings', {}).get('max_pages', 15), fixed_pages=confirmed_requirements.get('page_count_settings', {}).get('fixed_pages', 10), ppt_style=confirmed_requirements.get('ppt_style', 'general'), custom_style_prompt=confirmed_requirements.get('custom_style_prompt'), include_transition_pages=bool(confirmed_requirements.get('include_transition_pages', False)), target_audience=confirmed_requirements.get('target_audience', '普通大众'), custom_audience=confirmed_requirements.get('custom_audience'), file_processing_mode='markitdown', content_analysis_depth='fast')
|
||||
file_request = FileOutlineGenerationRequest(file_path=report_path, filename=Path(report_path).name, topic=confirmed_requirements.get('topic', project.topic), scenario=confirmed_requirements.get('type', project.scenario), requirements=confirmed_requirements.get('requirements', project.requirements), description=confirmed_requirements.get('description'), language=language, page_count_mode=confirmed_requirements.get('page_count_settings', {}).get('mode', 'ai_decide'), min_pages=confirmed_requirements.get('page_count_settings', {}).get('min_pages', 8), max_pages=confirmed_requirements.get('page_count_settings', {}).get('max_pages', 15), fixed_pages=confirmed_requirements.get('page_count_settings', {}).get('fixed_pages', 10), ppt_style=confirmed_requirements.get('ppt_style', 'general'), custom_style_prompt=confirmed_requirements.get('custom_style_prompt'), include_transition_pages=bool(confirmed_requirements.get('include_transition_pages', False)), target_audience=confirmed_requirements.get('target_audience', '普通大众'), custom_audience=confirmed_requirements.get('custom_audience'), file_processing_mode='markitdown', content_analysis_depth='fast')
|
||||
structured_outline = None
|
||||
llm_call_count = 0
|
||||
last_ping_at = time.time()
|
||||
|
||||
@@ -233,6 +233,7 @@ async def generate_outline(
|
||||
language=language,
|
||||
network_mode=network_mode,
|
||||
target_audience=confirmed_requirements.get('target_audience', '普通大众'),
|
||||
custom_audience=confirmed_requirements.get('custom_audience'),
|
||||
ppt_style=confirmed_requirements.get('ppt_style', 'general'),
|
||||
custom_style_prompt=confirmed_requirements.get('custom_style_prompt'),
|
||||
include_transition_pages=bool(confirmed_requirements.get('include_transition_pages', False)),
|
||||
@@ -339,6 +340,7 @@ async def regenerate_outline(
|
||||
language=language,
|
||||
network_mode=network_mode,
|
||||
target_audience=confirmed_requirements.get('target_audience', '普通大众'),
|
||||
custom_audience=confirmed_requirements.get('custom_audience'),
|
||||
ppt_style=confirmed_requirements.get('ppt_style', 'general'),
|
||||
custom_style_prompt=confirmed_requirements.get('custom_style_prompt'),
|
||||
include_transition_pages=bool(confirmed_requirements.get('include_transition_pages', False)),
|
||||
@@ -441,6 +443,8 @@ async def regenerate_outline(
|
||||
scenario=project_request.scenario,
|
||||
requirements=confirmed_requirements.get('requirements', ''),
|
||||
target_audience=confirmed_requirements.get('target_audience', '普通大众'),
|
||||
custom_audience=confirmed_requirements.get('custom_audience'),
|
||||
description=confirmed_requirements.get('description'),
|
||||
language=language,
|
||||
page_count_mode=page_count_settings.get('mode', 'ai_decide'),
|
||||
min_pages=page_count_settings.get('min_pages', 5),
|
||||
@@ -681,6 +685,8 @@ async def generate_file_outline(
|
||||
scenario='general',
|
||||
requirements=final_reqs,
|
||||
target_audience=confirmed_requirements.get('target_audience', '普通大众'),
|
||||
custom_audience=confirmed_requirements.get('custom_audience'),
|
||||
description=confirmed_requirements.get('description'),
|
||||
language=language,
|
||||
page_count_mode=confirmed_requirements.get('page_count_settings', {}).get('mode', 'ai_decide'),
|
||||
min_pages=confirmed_requirements.get('page_count_settings', {}).get('min_pages', 8),
|
||||
|
||||
@@ -78,6 +78,72 @@ def _get_project_network_mode(project: PPTProject) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _build_source_outline_context(
|
||||
*,
|
||||
scenario: str,
|
||||
target_audience: str,
|
||||
requirements_text: str,
|
||||
ppt_style: str,
|
||||
custom_style_prompt: Optional[str],
|
||||
confirmed_requirements: Dict[str, Any],
|
||||
source_summary: str,
|
||||
file_processing_mode: str,
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"scenario": scenario,
|
||||
"target_audience": target_audience or "General audience",
|
||||
"custom_audience": confirmed_requirements.get("custom_audience") or "",
|
||||
"requirements": requirements_text or "",
|
||||
"ppt_style": ppt_style,
|
||||
"custom_style_prompt": custom_style_prompt or "",
|
||||
"description": confirmed_requirements.get("description") or "",
|
||||
"source_summary": source_summary,
|
||||
"file_processing_mode": file_processing_mode,
|
||||
}
|
||||
|
||||
|
||||
def _build_source_outline_user_brief_markdown(
|
||||
*,
|
||||
topic: str,
|
||||
scenario: str,
|
||||
target_audience: str,
|
||||
requirements: Optional[str],
|
||||
description: Optional[str],
|
||||
custom_audience: Optional[str],
|
||||
ppt_style: str,
|
||||
custom_style_prompt: Optional[str],
|
||||
source_summary: str,
|
||||
) -> str:
|
||||
lines = ["\n## User Requirements\n"]
|
||||
if topic and topic.strip():
|
||||
lines.append(f"- Topic: {topic.strip()}")
|
||||
if scenario:
|
||||
lines.append(f"- Scenario: {scenario}")
|
||||
if target_audience:
|
||||
lines.append(f"- Target audience: {target_audience}")
|
||||
if custom_audience:
|
||||
lines.append(f"- Custom audience: {custom_audience}")
|
||||
if ppt_style:
|
||||
lines.append(f"- PPT style: {ppt_style}")
|
||||
if custom_style_prompt:
|
||||
lines.append(f"- Custom style: {custom_style_prompt}")
|
||||
if source_summary:
|
||||
lines.append(f"- Source summary: {source_summary}")
|
||||
|
||||
requirements_text = (requirements or "").strip()
|
||||
if requirements_text:
|
||||
lines.append("\n### Specific Requirements\n")
|
||||
lines.append(requirements_text)
|
||||
|
||||
description_text = (description or "").strip()
|
||||
if description_text and description_text not in requirements_text:
|
||||
lines.append("\n### Additional Instructions\n")
|
||||
lines.append(description_text)
|
||||
|
||||
lines.append("\n---\n")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
async def _save_uploaded_files_for_confirmed_requirements(file_uploads: List[UploadFile]) -> Dict[str, Any]:
|
||||
from ...services.file_processor import FileProcessor
|
||||
|
||||
@@ -216,14 +282,16 @@ async def _prepare_uploaded_source_outline_request(
|
||||
filename_for_request = primary_filename or file_entries[0].get("filename") or "uploaded_file"
|
||||
|
||||
if network_mode and topic:
|
||||
context = {
|
||||
"scenario": scenario,
|
||||
"target_audience": target_audience or "普通观众",
|
||||
"requirements": requirements_text or "",
|
||||
"ppt_style": ppt_style,
|
||||
"description": f"文件数量: {len(file_entries)}",
|
||||
"file_processing_mode": file_processing_mode,
|
||||
}
|
||||
context = _build_source_outline_context(
|
||||
scenario=scenario,
|
||||
target_audience=target_audience,
|
||||
requirements_text=requirements_text,
|
||||
ppt_style=ppt_style,
|
||||
custom_style_prompt=custom_style_prompt,
|
||||
confirmed_requirements=confirmed_requirements,
|
||||
source_summary=f"File count: {len(file_entries)}",
|
||||
file_processing_mode=file_processing_mode,
|
||||
)
|
||||
merged_file_path = await user_ppt_service.conduct_research_and_merge_with_files(
|
||||
topic=topic,
|
||||
language=language,
|
||||
@@ -270,6 +338,8 @@ async def _prepare_uploaded_source_outline_request(
|
||||
scenario=scenario,
|
||||
requirements=requirements_text,
|
||||
target_audience=target_audience,
|
||||
custom_audience=confirmed_requirements.get("custom_audience"),
|
||||
description=confirmed_requirements.get("description"),
|
||||
language=language,
|
||||
page_count_mode=page_count_mode,
|
||||
min_pages=min_pages,
|
||||
@@ -352,6 +422,9 @@ async def _generate_outline_from_confirmed_sources(
|
||||
file_processing_mode=file_processing_mode,
|
||||
content_analysis_depth=content_analysis_depth,
|
||||
requirements=requirements_text,
|
||||
description=confirmed_requirements.get("description"),
|
||||
custom_audience=confirmed_requirements.get("custom_audience"),
|
||||
include_transition_pages=bool(confirmed_requirements.get("include_transition_pages", False)),
|
||||
scenario=scenario,
|
||||
language=language,
|
||||
user_id=user_id,
|
||||
@@ -385,14 +458,16 @@ async def _generate_outline_from_confirmed_sources(
|
||||
filename_for_request = primary_filename or file_entries[0].get("filename") or "uploaded_file"
|
||||
|
||||
if network_mode and topic:
|
||||
context = {
|
||||
"scenario": scenario,
|
||||
"target_audience": target_audience or "普通观众",
|
||||
"requirements": requirements_text or "",
|
||||
"ppt_style": ppt_style,
|
||||
"description": f"文件数量: {len(file_entries)}",
|
||||
"file_processing_mode": file_processing_mode,
|
||||
}
|
||||
context = _build_source_outline_context(
|
||||
scenario=scenario,
|
||||
target_audience=target_audience,
|
||||
requirements_text=requirements_text,
|
||||
ppt_style=ppt_style,
|
||||
custom_style_prompt=custom_style_prompt,
|
||||
confirmed_requirements=confirmed_requirements,
|
||||
source_summary=f"File count: {len(file_entries)}",
|
||||
file_processing_mode=file_processing_mode,
|
||||
)
|
||||
merged_file_path = await user_ppt_service.conduct_research_and_merge_with_files(
|
||||
topic=topic,
|
||||
language=language,
|
||||
@@ -438,6 +513,8 @@ async def _generate_outline_from_confirmed_sources(
|
||||
scenario=scenario,
|
||||
requirements=requirements_text,
|
||||
target_audience=target_audience,
|
||||
custom_audience=confirmed_requirements.get("custom_audience"),
|
||||
description=confirmed_requirements.get("description"),
|
||||
language=language,
|
||||
page_count_mode=page_count_mode,
|
||||
min_pages=min_pages,
|
||||
@@ -903,6 +980,9 @@ async def _process_url_sources_for_outline(
|
||||
file_processing_mode: str,
|
||||
content_analysis_depth: str,
|
||||
requirements: str = None,
|
||||
description: str = None,
|
||||
custom_audience: str = None,
|
||||
include_transition_pages: bool = False,
|
||||
scenario: str = "general",
|
||||
language: str = "zh",
|
||||
user_id: int = None,
|
||||
@@ -1017,6 +1097,19 @@ async def _process_url_sources_for_outline(
|
||||
markdown_parts.append("# URL Source Content\n")
|
||||
if topic and topic.strip():
|
||||
markdown_parts.append(f"\nTopic: {topic.strip()}\n")
|
||||
markdown_parts.append(
|
||||
_build_source_outline_user_brief_markdown(
|
||||
topic=topic,
|
||||
scenario=scenario,
|
||||
target_audience=target_audience,
|
||||
requirements=requirements,
|
||||
description=description,
|
||||
custom_audience=custom_audience,
|
||||
ppt_style=ppt_style,
|
||||
custom_style_prompt=custom_style_prompt,
|
||||
source_summary=f"URL count: {len(source_urls)}",
|
||||
)
|
||||
)
|
||||
|
||||
if processed_file_sources:
|
||||
markdown_parts.append("\n## Downloaded File Sources\n")
|
||||
@@ -1060,6 +1153,8 @@ async def _process_url_sources_for_outline(
|
||||
scenario=scenario,
|
||||
requirements=requirements,
|
||||
target_audience=target_audience,
|
||||
custom_audience=custom_audience,
|
||||
description=description,
|
||||
language=language,
|
||||
page_count_mode=page_count_mode,
|
||||
min_pages=min_pages,
|
||||
@@ -1067,7 +1162,7 @@ async def _process_url_sources_for_outline(
|
||||
fixed_pages=fixed_pages,
|
||||
ppt_style=ppt_style,
|
||||
custom_style_prompt=custom_style_prompt,
|
||||
include_transition_pages=False,
|
||||
include_transition_pages=include_transition_pages,
|
||||
file_processing_mode=file_processing_mode,
|
||||
content_analysis_depth=content_analysis_depth,
|
||||
)
|
||||
@@ -1168,6 +1263,8 @@ async def _process_uploaded_files_for_outline(
|
||||
file_processing_mode: str,
|
||||
content_analysis_depth: str,
|
||||
requirements: str = None,
|
||||
description: str = None,
|
||||
custom_audience: str = None,
|
||||
enable_web_search: bool = False, # 新增参数
|
||||
scenario: str = "general", # 新增参数
|
||||
language: str = "zh", # 新增参数
|
||||
@@ -1231,9 +1328,11 @@ async def _process_uploaded_files_for_outline(
|
||||
context = {
|
||||
'scenario': scenario,
|
||||
'target_audience': target_audience or '普通大众',
|
||||
'custom_audience': custom_audience or '',
|
||||
'requirements': requirements or '',
|
||||
'ppt_style': ppt_style,
|
||||
'description': f'文件数量: {len(files)}',
|
||||
'description': description or '',
|
||||
'source_summary': f'文件数量: {len(files)}',
|
||||
'file_processing_mode': file_processing_mode,
|
||||
}
|
||||
|
||||
@@ -1273,6 +1372,8 @@ async def _process_uploaded_files_for_outline(
|
||||
scenario=scenario,
|
||||
requirements=requirements,
|
||||
target_audience=target_audience,
|
||||
custom_audience=custom_audience,
|
||||
description=description,
|
||||
language=language,
|
||||
page_count_mode=page_count_mode,
|
||||
min_pages=min_pages,
|
||||
|
||||
@@ -177,6 +177,7 @@ async def start_project_workflow(
|
||||
language=language,
|
||||
network_mode=network_mode,
|
||||
target_audience=confirmed_requirements.get("target_audience", "普通大众"),
|
||||
custom_audience=confirmed_requirements.get("custom_audience"),
|
||||
ppt_style=confirmed_requirements.get("ppt_style", "general"),
|
||||
custom_style_prompt=confirmed_requirements.get("custom_style_prompt"),
|
||||
description=confirmed_requirements.get("description"),
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
const initialProjectSlides = {{ (project.slides_data if project and project.slides_data else []) | tojson }};
|
||||
const hasInitialSlidesHtml = {{ 'true' if project and project.slides_html else 'false' }};
|
||||
const hasConfirmedRequirements = {{ 'true' if project and project.confirmed_requirements else 'false' }};
|
||||
{% set outline_stage = todo_board.stages | selectattr('id', 'equalto', 'outline_generation') | first %}
|
||||
const initialOutlineStageStatus = {{ (outline_stage.status if outline_stage else '') | tojson }};
|
||||
let outlineResumeAutoStartScheduled = false;
|
||||
let outlineHasGeneratedInSession = false;
|
||||
const hasProjectLanguage = (
|
||||
projectMetadata &&
|
||||
typeof projectMetadata === 'object' &&
|
||||
@@ -607,6 +611,10 @@
|
||||
);
|
||||
}
|
||||
|
||||
function hasAnyOutlineSlides() {
|
||||
return hasInitialOutlineSlides() || outlineHasGeneratedInSession;
|
||||
}
|
||||
|
||||
function hasInitialPPTOutput() {
|
||||
return Boolean(
|
||||
(Array.isArray(initialProjectSlides) && initialProjectSlides.length > 0) ||
|
||||
@@ -615,13 +623,38 @@
|
||||
}
|
||||
|
||||
function shouldExposePPTStartButton(pptStageStatus = null) {
|
||||
if (!hasInitialOutlineSlides() || hasInitialPPTOutput()) {
|
||||
if (!hasAnyOutlineSlides() || hasInitialPPTOutput()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !pptStageStatus || ['pending', 'failed'].includes(pptStageStatus);
|
||||
}
|
||||
|
||||
function shouldResumeOutlineGenerationOnLoad() {
|
||||
if (!hasConfirmedRequirements || hasAnyOutlineSlides() || hasInitialPPTOutput()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ['pending', 'running', ''].includes(initialOutlineStageStatus);
|
||||
}
|
||||
|
||||
function scheduleOutlineResumeAutoStart(delayMs = 500) {
|
||||
if (outlineResumeAutoStartScheduled || outlineGenerationStarted || hasAnyOutlineSlides()) {
|
||||
return;
|
||||
}
|
||||
|
||||
outlineResumeAutoStartScheduled = true;
|
||||
window.setTimeout(() => {
|
||||
outlineResumeAutoStartScheduled = false;
|
||||
if (outlineGenerationStarted || hasAnyOutlineSlides()) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Resuming outline generation from saved todo state.');
|
||||
startOutlineGenerationNew();
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
function hydrateOutlineSectionFromProjectState() {
|
||||
if (!shouldRenderInitialOutlineSection()) {
|
||||
return false;
|
||||
@@ -771,12 +804,17 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasInitialOutlineSlides()) {
|
||||
if (hasAnyOutlineSlides()) {
|
||||
hydrateOutlineSectionFromProjectState();
|
||||
console.log('Saved outline exists, skipping auto-start outline generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldResumeOutlineGenerationOnLoad()) {
|
||||
scheduleOutlineResumeAutoStart();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if outline section is visible and outline generation should start
|
||||
const outlineSection = document.getElementById('outline-section');
|
||||
if (outlineSection && outlineSection.style.display !== 'none') {
|
||||
@@ -1158,7 +1196,7 @@
|
||||
lastOutlineErrorRetryHandler = 'regenerateOutlineNew';
|
||||
const forceRegenerate = options && options.forceRegenerate === true;
|
||||
|
||||
if (!forceRegenerate && hasInitialOutlineSlides()) {
|
||||
if (!forceRegenerate && hasAnyOutlineSlides()) {
|
||||
console.log('Saved outline exists, hydrating instead of starting outline generation.');
|
||||
hydrateOutlineSectionFromProjectState();
|
||||
return;
|
||||
@@ -3170,7 +3208,7 @@
|
||||
const requirementsCompleted = requirementsStage?.querySelector('.stage-status-icon')?.textContent === '✓';
|
||||
const outlineStatus = outlineStage?.querySelector('.stage-status-icon')?.textContent;
|
||||
|
||||
if (hasInitialOutlineSlides()) {
|
||||
if (hasAnyOutlineSlides()) {
|
||||
hydrateOutlineSectionFromProjectState();
|
||||
console.log('Saved outline exists, skipping workflow auto-start.');
|
||||
return;
|
||||
@@ -4131,6 +4169,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
outlineHasGeneratedInSession = true;
|
||||
renderOutlineContentNew(parsedOutline, outlineContainer);
|
||||
|
||||
if (loadingDiv) loadingDiv.style.display = 'none';
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
if "bs4" not in sys.modules:
|
||||
sys.modules["bs4"] = types.SimpleNamespace(BeautifulSoup=object, Comment=object)
|
||||
|
||||
if "tavily" not in sys.modules:
|
||||
sys.modules["tavily"] = types.SimpleNamespace(TavilyClient=object)
|
||||
|
||||
if "langchain_core.documents" not in sys.modules:
|
||||
langchain_core_module = sys.modules.setdefault(
|
||||
"langchain_core",
|
||||
types.ModuleType("langchain_core"),
|
||||
)
|
||||
documents_module = types.ModuleType("langchain_core.documents")
|
||||
documents_module.Document = object
|
||||
sys.modules["langchain_core.documents"] = documents_module
|
||||
setattr(langchain_core_module, "documents", documents_module)
|
||||
|
||||
from landppt.services.outline.project_outline_research_service import ProjectOutlineResearchService
|
||||
|
||||
|
||||
class _OutlineResearchStubService:
|
||||
enhanced_research_service = None
|
||||
enhanced_report_generator = None
|
||||
|
||||
def _initialize_research_services(self):
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_research_file_merge_preserves_user_requirements(monkeypatch, tmp_path):
|
||||
service = ProjectOutlineResearchService(_OutlineResearchStubService())
|
||||
|
||||
fake_module = types.ModuleType("landppt.services.file_processor")
|
||||
|
||||
class _FakeFileProcessor:
|
||||
async def process_file(self, file_path, filename, file_processing_mode=None):
|
||||
return SimpleNamespace(
|
||||
processed_content=f"{filename}:{file_processing_mode or 'default'}:{file_path}"
|
||||
)
|
||||
|
||||
fake_module.FileProcessor = _FakeFileProcessor
|
||||
monkeypatch.setitem(sys.modules, "landppt.services.file_processor", fake_module)
|
||||
monkeypatch.setattr("tempfile.gettempdir", lambda: str(tmp_path))
|
||||
|
||||
merged_path = await service.conduct_research_and_merge_with_files(
|
||||
topic="Test Topic",
|
||||
language="zh",
|
||||
file_paths=["dummy.txt"],
|
||||
context={
|
||||
"file_processing_mode": "markitdown",
|
||||
"requirements": "Highlight operational risk",
|
||||
"description": "Prioritize executive action items",
|
||||
"custom_audience": "CFO office",
|
||||
"source_summary": "File count: 1",
|
||||
},
|
||||
)
|
||||
|
||||
merged_content = Path(merged_path).read_text(encoding="utf-8")
|
||||
assert "## User Requirements" in merged_content
|
||||
assert "Highlight operational risk" in merged_content
|
||||
assert "Prioritize executive action items" in merged_content
|
||||
assert "CFO office" in merged_content
|
||||
assert merged_content.index("## User Requirements") < merged_content.index("dummy.txt")
|
||||
assert "markitdown" in merged_content
|
||||
assert str(tmp_path) in str(Path(merged_path).parent.parent)
|
||||
|
||||
|
||||
def test_url_source_outline_user_brief_preserves_requirements():
|
||||
from landppt.web.route_modules.outline_support import (
|
||||
_build_source_outline_user_brief_markdown,
|
||||
)
|
||||
|
||||
brief = _build_source_outline_user_brief_markdown(
|
||||
topic="AI Strategy",
|
||||
scenario="business",
|
||||
target_audience="Executives",
|
||||
requirements="Use an investment committee perspective",
|
||||
description="Avoid generic market overview",
|
||||
custom_audience="Investment committee",
|
||||
ppt_style="general",
|
||||
custom_style_prompt="Concise and evidence-led",
|
||||
source_summary="URL count: 2",
|
||||
)
|
||||
|
||||
assert "## User Requirements" in brief
|
||||
assert "Use an investment committee perspective" in brief
|
||||
assert "Avoid generic market overview" in brief
|
||||
assert "Investment committee" in brief
|
||||
assert "URL count: 2" in brief
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
|
||||
from landppt.services.outline.outline_workflow_service import OutlineWorkflowService
|
||||
from landppt.services.outline.outline_workflow_support import (
|
||||
build_project_requirements_text,
|
||||
create_outline_from_file_content,
|
||||
get_chunk_size_from_request,
|
||||
get_slides_range_from_request,
|
||||
@@ -20,6 +21,8 @@ def _build_request(**overrides):
|
||||
"scenario": "general",
|
||||
"requirements": "Keep it concise",
|
||||
"target_audience": "Leadership",
|
||||
"custom_audience": "",
|
||||
"description": "",
|
||||
"language": "zh",
|
||||
"page_count_mode": "ai_decide",
|
||||
"min_pages": 8,
|
||||
@@ -79,6 +82,18 @@ def test_outline_workflow_support_builds_fixed_page_outline():
|
||||
assert outline["slides"][-1]["page_number"] == 4
|
||||
|
||||
|
||||
def test_outline_workflow_support_combines_requirements_and_description():
|
||||
request = _build_request(
|
||||
requirements="Highlight security risks",
|
||||
description="Prioritize mitigation roadmap",
|
||||
)
|
||||
|
||||
project_requirements = build_project_requirements_text(request)
|
||||
|
||||
assert "Highlight security risks" in project_requirements
|
||||
assert "Prioritize mitigation roadmap" in project_requirements
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outline_workflow_service_falls_back_when_generator_is_unavailable(tmp_path):
|
||||
source_file = tmp_path / "fallback.md"
|
||||
@@ -120,3 +135,72 @@ async def test_outline_workflow_service_falls_back_when_generator_is_unavailable
|
||||
assert result.file_info["used_summeryanyfile"] is False
|
||||
assert result.outline["slides"][0]["title"] == "Quarterly Review"
|
||||
assert workflow._service.validation_requirements["topic"] == "Quarterly Review"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outline_workflow_service_passes_user_context_to_summeryanyfile(tmp_path):
|
||||
source_file = tmp_path / "source.md"
|
||||
source_file.write_text("# Market\nContext", encoding="utf-8")
|
||||
|
||||
request = _build_request(
|
||||
file_path=str(source_file),
|
||||
filename=source_file.name,
|
||||
requirements="Highlight security risks",
|
||||
description="Prioritize mitigation roadmap",
|
||||
custom_audience="Board audit committee",
|
||||
)
|
||||
|
||||
class FakeOutline:
|
||||
total_pages = 1
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"title": "Quarterly Review",
|
||||
"slides": [
|
||||
{
|
||||
"page_number": 1,
|
||||
"title": "Quarterly Review",
|
||||
"content_points": ["Context"],
|
||||
"slide_type": "title",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
class FakeGenerator:
|
||||
def __init__(self):
|
||||
self.kwargs = None
|
||||
|
||||
async def generate_from_file(self, file_path, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
return FakeOutline()
|
||||
|
||||
class DummyService:
|
||||
def __init__(self):
|
||||
self.validation_requirements = None
|
||||
|
||||
def _standardize_summeryfile_outline(self, outline):
|
||||
return outline
|
||||
|
||||
async def _validate_and_repair_outline_json(self, outline, requirements):
|
||||
self.validation_requirements = requirements
|
||||
return outline
|
||||
|
||||
def _extract_summeryanyfile_llm_call_count(self, _generator):
|
||||
return 1
|
||||
|
||||
fake_generator = FakeGenerator()
|
||||
workflow = OutlineWorkflowService(DummyService())
|
||||
|
||||
async def _create_fake_generator(_request):
|
||||
return fake_generator, tmp_path
|
||||
|
||||
workflow._create_outline_generator = _create_fake_generator
|
||||
|
||||
result = await workflow.generate_outline_from_file(request)
|
||||
|
||||
assert result.success is True
|
||||
assert "Highlight security risks" in fake_generator.kwargs["project_requirements"]
|
||||
assert "Prioritize mitigation roadmap" in fake_generator.kwargs["project_requirements"]
|
||||
assert fake_generator.kwargs["custom_audience"] == "Board audit committee"
|
||||
assert workflow._service.validation_requirements["custom_audience"] == "Board audit committee"
|
||||
assert workflow._service.validation_requirements["description"] == "Prioritize mitigation roadmap"
|
||||
|
||||
@@ -197,6 +197,41 @@ def test_todo_board_preserves_saved_outline_before_auto_starting_generation():
|
||||
assert "Saved outline exists, skipping workflow auto-start." in script
|
||||
|
||||
|
||||
def test_todo_board_resumes_interrupted_outline_generation_on_reload():
|
||||
script = _read("src/landppt/web/templates/components/project/todo_board/extra_js_1.html")
|
||||
|
||||
assert "initialOutlineStageStatus" in script
|
||||
assert "shouldResumeOutlineGenerationOnLoad" in script
|
||||
assert "['pending', 'running', ''].includes(initialOutlineStageStatus)" in script
|
||||
assert "Resuming outline generation from saved todo state." in script
|
||||
|
||||
|
||||
def test_todo_board_template_injects_outline_stage_status():
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
env = Environment(loader=FileSystemLoader(ROOT / "src/landppt/web/templates"))
|
||||
rendered = env.get_template("components/project/todo_board/extra_js_1.html").render(
|
||||
todo_board={
|
||||
"task_id": "project-1",
|
||||
"stages": [
|
||||
{"id": "requirements_confirmation", "status": "completed"},
|
||||
{"id": "outline_generation", "status": "running"},
|
||||
{"id": "ppt_creation", "status": "pending"},
|
||||
],
|
||||
},
|
||||
project={
|
||||
"project_metadata": {},
|
||||
"outline": None,
|
||||
"slides_data": [],
|
||||
"slides_html": "",
|
||||
"confirmed_requirements": {"topic": "Demo"},
|
||||
},
|
||||
)
|
||||
|
||||
assert 'const initialOutlineStageStatus = "running";' in rendered
|
||||
assert "const hasConfirmedRequirements = true;" in rendered
|
||||
|
||||
|
||||
def test_slide_record_from_payload_preserves_outline_metadata():
|
||||
record = DatabaseService._slide_record_from_payload(
|
||||
"project-1",
|
||||
|
||||
Reference in New Issue
Block a user