mirror of
https://github.com/sligter/LandPPT.git
synced 2026-08-31 01:43:15 +08:00
feat(outline): implement caching for existing outlines to optimize generation process
This commit is contained in:
@@ -4,6 +4,14 @@ This repository includes reusable Codex skills under `skills/`.
|
||||
|
||||
## Available
|
||||
|
||||
- `landppt-skills`
|
||||
- Path: `skills/landppt-skills/SKILL.md`
|
||||
- Purpose: Independent LandPPT-style workflow for native editable PPTX generation, speaker scripts, narration audio, and explainer video export.
|
||||
- Includes:
|
||||
- Requirement, outline, prompt, and native PPTX guidance
|
||||
- Self-contained local scripts for outline validation, reference PPTX analysis, native template/PPTX generation, narration, and video export
|
||||
- No dependency on the LandPPT web service, database, protected routes, commercial conversion services, or HTML slide rendering
|
||||
|
||||
- `landppt-ppt-generation`
|
||||
- Path: `skills/landppt-ppt-generation/SKILL.md`
|
||||
- Purpose: End-to-end LandPPT PPT generation and post-edit operations via user API key.
|
||||
|
||||
@@ -300,6 +300,20 @@ class ProjectOutlineStreamingService:
|
||||
from ..file_outline_utils import extract_saved_file_outline, should_force_file_outline_regeneration
|
||||
force_file_outline_regeneration = should_force_file_outline_regeneration(project.confirmed_requirements or {})
|
||||
ignore_saved_outline = bool(force_regenerate or force_file_outline_regeneration)
|
||||
existing_outline = project.outline if isinstance(project.outline, dict) else None
|
||||
existing_slides = existing_outline.get('slides', []) if existing_outline else []
|
||||
if existing_slides and not ignore_saved_outline:
|
||||
import json
|
||||
logger.info(
|
||||
'Project %s already has an outline with %s slides, streaming saved outline',
|
||||
project_id,
|
||||
len(existing_slides),
|
||||
)
|
||||
await self._update_outline_generation_stage(project_id, existing_outline)
|
||||
yield f"data: {json.dumps({'status': {'step': 'cached', 'message': '已加载已有大纲', 'progress': 1.0}}, ensure_ascii=False)}\n\n"
|
||||
yield f"data: {json.dumps({'outline': existing_outline}, ensure_ascii=False)}\n\n"
|
||||
yield f"data: {json.dumps({'done': True, 'llm_call_count': 0})}\n\n"
|
||||
return
|
||||
if ignore_saved_outline:
|
||||
logger.info(
|
||||
'Project %s requested fresh outline generation, skipping saved outline cache',
|
||||
|
||||
@@ -42,6 +42,38 @@ async def stream_outline_generation(
|
||||
raise HTTPException(status_code=404, detail="Project not found")
|
||||
|
||||
user_ppt_service = get_ppt_service_for_user(user.id)
|
||||
confirmed_requirements = project.confirmed_requirements or {}
|
||||
force_file_outline_regeneration = bool(
|
||||
confirmed_requirements.get("force_file_outline_regeneration")
|
||||
)
|
||||
existing_outline = project.outline if isinstance(project.outline, dict) else None
|
||||
existing_slides = existing_outline.get("slides", []) if existing_outline else []
|
||||
|
||||
if existing_slides and not force_regenerate and not force_file_outline_regeneration:
|
||||
async def generate_saved_outline():
|
||||
import json
|
||||
try:
|
||||
await user_ppt_service._update_outline_generation_stage(project_id, existing_outline)
|
||||
except Exception as stage_error:
|
||||
logger.warning(
|
||||
"Failed to mark saved outline stage completed for project %s: %s",
|
||||
project_id,
|
||||
stage_error,
|
||||
)
|
||||
yield f"data: {json.dumps({'status': {'step': 'cached', 'message': '已加载已有大纲', 'progress': 1.0}}, ensure_ascii=False)}\n\n"
|
||||
yield f"data: {json.dumps({'outline': existing_outline}, ensure_ascii=False)}\n\n"
|
||||
yield f"data: {json.dumps({'done': True, 'llm_call_count': 0})}\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
generate_saved_outline(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no",
|
||||
},
|
||||
)
|
||||
|
||||
_, outline_settings = await user_ppt_service.get_role_provider_async("outline")
|
||||
outline_provider_name = outline_settings.get("provider")
|
||||
|
||||
@@ -67,11 +99,7 @@ async def stream_outline_generation(
|
||||
|
||||
async def generate():
|
||||
billed = False
|
||||
confirmed_requirements = project.confirmed_requirements or {}
|
||||
content_source = confirmed_requirements.get("content_source")
|
||||
force_file_outline_regeneration = bool(
|
||||
confirmed_requirements.get("force_file_outline_regeneration")
|
||||
)
|
||||
force_fresh_generation = bool(force_regenerate or force_file_outline_regeneration)
|
||||
if force_fresh_generation:
|
||||
logger.info(
|
||||
|
||||
@@ -771,6 +771,12 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasInitialOutlineSlides()) {
|
||||
hydrateOutlineSectionFromProjectState();
|
||||
console.log('Saved outline exists, skipping auto-start outline generation.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if outline section is visible and outline generation should start
|
||||
const outlineSection = document.getElementById('outline-section');
|
||||
if (outlineSection && outlineSection.style.display !== 'none') {
|
||||
@@ -1152,6 +1158,12 @@
|
||||
lastOutlineErrorRetryHandler = 'regenerateOutlineNew';
|
||||
const forceRegenerate = options && options.forceRegenerate === true;
|
||||
|
||||
if (!forceRegenerate && hasInitialOutlineSlides()) {
|
||||
console.log('Saved outline exists, hydrating instead of starting outline generation.');
|
||||
hydrateOutlineSectionFromProjectState();
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记为已开始
|
||||
outlineGenerationStarted = true;
|
||||
|
||||
@@ -3158,6 +3170,12 @@
|
||||
const requirementsCompleted = requirementsStage?.querySelector('.stage-status-icon')?.textContent === '✓';
|
||||
const outlineStatus = outlineStage?.querySelector('.stage-status-icon')?.textContent;
|
||||
|
||||
if (hasInitialOutlineSlides()) {
|
||||
hydrateOutlineSectionFromProjectState();
|
||||
console.log('Saved outline exists, skipping workflow auto-start.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-start outline generation if requirements just confirmed or if requirements completed and outline pending
|
||||
if ((fromRequirements || requirementsCompleted) && outlineStatus === '⏳') {
|
||||
// Requirements confirmed, automatically start outline generation (second step)
|
||||
|
||||
@@ -134,3 +134,11 @@ async def test_database_service_filters_projects_by_effective_status_after_conve
|
||||
|
||||
assert response.total == 1
|
||||
assert [project.project_id for project in response.projects] == ["derived-in-progress"]
|
||||
|
||||
|
||||
def test_todo_board_preserves_saved_outline_before_auto_starting_generation():
|
||||
script = _read("src/landppt/web/templates/components/project/todo_board/extra_js_1.html")
|
||||
|
||||
assert "Saved outline exists, skipping auto-start outline generation." in script
|
||||
assert "Saved outline exists, hydrating instead of starting outline generation." in script
|
||||
assert "Saved outline exists, skipping workflow auto-start." in script
|
||||
|
||||
@@ -388,3 +388,41 @@ async def test_generate_outline_streaming_force_regenerate_skips_saved_outline(m
|
||||
assert stub.stage_updates
|
||||
assert project.outline["title"] == "fresh"
|
||||
assert any('"done": true' in chunk for chunk in events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_outline_streaming_replays_saved_outline_without_regeneration(monkeypatch):
|
||||
project = SimpleNamespace(
|
||||
topic="cached topic",
|
||||
outline={
|
||||
"title": "cached",
|
||||
"slides": [{"page_number": 1, "title": "cached"}],
|
||||
"metadata": {},
|
||||
},
|
||||
confirmed_requirements={"content_source": "manual"},
|
||||
project_metadata={},
|
||||
todo_board=None,
|
||||
updated_at=0,
|
||||
)
|
||||
stub = _OutlineStreamingFreshGenerationStubService(project)
|
||||
service = ProjectOutlineStreamingService(stub)
|
||||
|
||||
async def _unexpected_run_streaming_outline_research(*args, **kwargs):
|
||||
raise AssertionError("saved outline should not trigger fresh generation")
|
||||
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_run_streaming_outline_research",
|
||||
_unexpected_run_streaming_outline_research,
|
||||
raising=False,
|
||||
)
|
||||
|
||||
events = []
|
||||
async for chunk in service.generate_outline_streaming("project-1"):
|
||||
events.append(chunk)
|
||||
|
||||
assert stub.project_manager.status_updates == []
|
||||
assert stub.stage_updates == [("project-1", project.outline)]
|
||||
assert any('"step": "cached"' in chunk for chunk in events)
|
||||
assert any('"outline"' in chunk and '"cached"' in chunk for chunk in events)
|
||||
assert any('"llm_call_count": 0' in chunk for chunk in events)
|
||||
|
||||
Reference in New Issue
Block a user