feat: optimize task file generation and add visualization download
Backend changes: - Disable PP-Structure debug file generation by default - Separate raw_ocr_regions.json generation from debug flag (critical file) - Add visualization folder download endpoint as ZIP - Add has_visualization field to TaskDetailResponse - Stop generating Markdown files - Save translated PDFs to task folder with caching Frontend changes: - Replace JSON/MD download buttons with PDF buttons in TaskHistoryPage - Add visualization download button in TaskDetailPage - Fix Processing page task switching issue (reset isNotFound) Archives two OpenSpec proposals: - optimize-task-files-and-visualization - simplify-frontend-add-billing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1510,14 +1510,25 @@ class OCRService:
|
||||
'height': ocr_height
|
||||
}]
|
||||
|
||||
# Generate PP-StructureV3 debug outputs if enabled
|
||||
# Always save raw_ocr_regions.json (required for PDF generation and translation)
|
||||
if output_dir:
|
||||
try:
|
||||
import json
|
||||
ocr_json_path = output_dir / f"{image_path.stem}_raw_ocr_regions.json"
|
||||
with open(ocr_json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(text_regions, f, ensure_ascii=False, indent=2)
|
||||
logger.info(f"Saved raw OCR regions to {ocr_json_path}")
|
||||
except Exception as ocr_save_error:
|
||||
logger.warning(f"Failed to save raw OCR regions: {ocr_save_error}")
|
||||
|
||||
# Generate PP-StructureV3 debug outputs if enabled (debug files only)
|
||||
if settings.pp_structure_debug_enabled and output_dir:
|
||||
try:
|
||||
from app.services.pp_structure_debug import PPStructureDebug
|
||||
debug_service = PPStructureDebug(output_dir)
|
||||
|
||||
# Save raw results as JSON
|
||||
debug_service.save_raw_results(
|
||||
# Save PP-Structure raw results and summary (debug only)
|
||||
debug_service.save_debug_results(
|
||||
pp_structure_results={
|
||||
'elements': layout_data.get('elements', []),
|
||||
'total_elements': layout_data.get('total_elements', 0),
|
||||
@@ -2536,7 +2547,7 @@ class OCRService:
|
||||
source_file_path: Optional[Path] = None
|
||||
) -> Tuple[Optional[Path], Optional[Path], Optional[Path]]:
|
||||
"""
|
||||
Save OCR results to JSON, Markdown, and layout-preserving PDF files
|
||||
Save OCR results to JSON and layout-preserving PDF files
|
||||
|
||||
Args:
|
||||
result: OCR result (UnifiedDocument or dictionary)
|
||||
@@ -2546,9 +2557,11 @@ class OCRService:
|
||||
|
||||
Returns:
|
||||
Tuple of (json_path, markdown_path, pdf_path)
|
||||
Note: markdown_path is always None (Markdown generation removed)
|
||||
"""
|
||||
try:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
markdown_path = None # Markdown generation removed
|
||||
|
||||
# Use UnifiedDocumentExporter for standardized export
|
||||
if isinstance(result, UnifiedDocument) and UnifiedDocumentExporter is not None:
|
||||
@@ -2560,31 +2573,16 @@ class OCRService:
|
||||
include_metadata=True,
|
||||
include_statistics=True
|
||||
)
|
||||
|
||||
markdown_path = output_dir / f"{file_id}_output.md"
|
||||
UnifiedDocumentExporter.export_to_markdown(
|
||||
result,
|
||||
markdown_path,
|
||||
include_metadata_header=False # Keep output clean
|
||||
)
|
||||
|
||||
markdown_content = result.extract_all_text()
|
||||
else:
|
||||
# Legacy path for dict results
|
||||
result_dict = result if isinstance(result, dict) else result.to_dict()
|
||||
markdown_content = result.get('markdown_content', '') if isinstance(result, dict) else ''
|
||||
|
||||
# Save JSON
|
||||
json_path = output_dir / f"{file_id}_result.json"
|
||||
with open(json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(result_dict, f, ensure_ascii=False, indent=2)
|
||||
|
||||
# Save Markdown
|
||||
markdown_path = output_dir / f"{file_id}_output.md"
|
||||
with open(markdown_path, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
logger.info(f"Results saved: {json_path.name}, {markdown_path.name}")
|
||||
logger.info(f"Results saved: {json_path.name}")
|
||||
|
||||
# Generate layout-preserving PDF
|
||||
pdf_path = None
|
||||
|
||||
@@ -107,6 +107,50 @@ class PPStructureDebug:
|
||||
|
||||
return saved_files
|
||||
|
||||
def save_debug_results(
|
||||
self,
|
||||
pp_structure_results: Dict[str, Any],
|
||||
raw_ocr_regions: List[Dict[str, Any]],
|
||||
filename_prefix: str = "debug"
|
||||
) -> Dict[str, Path]:
|
||||
"""
|
||||
Save debug-only files (PP-Structure raw results and summary).
|
||||
Does NOT save raw_ocr_regions.json (that's handled separately).
|
||||
|
||||
Args:
|
||||
pp_structure_results: Raw PP-StructureV3 analysis results
|
||||
raw_ocr_regions: Raw OCR text regions (for summary generation only)
|
||||
filename_prefix: Prefix for output files
|
||||
|
||||
Returns:
|
||||
Dictionary with paths to saved files
|
||||
"""
|
||||
saved_files = {}
|
||||
|
||||
# Save PP-StructureV3 results
|
||||
pp_json_path = self.output_dir / f"{filename_prefix}_pp_structure_raw.json"
|
||||
try:
|
||||
serializable_results = self._make_serializable(pp_structure_results)
|
||||
with open(pp_json_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(serializable_results, f, ensure_ascii=False, indent=2)
|
||||
saved_files['pp_structure'] = pp_json_path
|
||||
logger.info(f"Saved PP-StructureV3 raw results to {pp_json_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save PP-StructureV3 results: {e}")
|
||||
|
||||
# Save summary comparison
|
||||
summary_path = self.output_dir / f"{filename_prefix}_debug_summary.json"
|
||||
try:
|
||||
summary = self._generate_summary(pp_structure_results, raw_ocr_regions)
|
||||
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(summary, f, ensure_ascii=False, indent=2)
|
||||
saved_files['summary'] = summary_path
|
||||
logger.info(f"Saved debug summary to {summary_path}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save debug summary: {e}")
|
||||
|
||||
return saved_files
|
||||
|
||||
def generate_visualization(
|
||||
self,
|
||||
image_path: Path,
|
||||
|
||||
@@ -255,14 +255,8 @@ class UnifiedDocumentExporter:
|
||||
logger.error(f"Failed to export JSON: {e}")
|
||||
results['json'] = None
|
||||
|
||||
# Export Markdown
|
||||
try:
|
||||
md_path = output_dir / f"{file_id}_output.md"
|
||||
UnifiedDocumentExporter.export_to_markdown(document, md_path)
|
||||
results['markdown'] = md_path
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export Markdown: {e}")
|
||||
results['markdown'] = None
|
||||
# Markdown export removed - no longer generating _output.md files
|
||||
results['markdown'] = None
|
||||
|
||||
# Export plain text
|
||||
try:
|
||||
@@ -469,13 +463,13 @@ def save_unified_document(
|
||||
document: The UnifiedDocument to save
|
||||
output_dir: Output directory
|
||||
file_id: Base filename
|
||||
formats: List of formats to export (default: ['json', 'markdown'])
|
||||
formats: List of formats to export (default: ['json'])
|
||||
|
||||
Returns:
|
||||
Dictionary mapping format names to output paths
|
||||
"""
|
||||
if formats is None:
|
||||
formats = ['json', 'markdown']
|
||||
formats = ['json']
|
||||
|
||||
results = {}
|
||||
output_dir = Path(output_dir)
|
||||
@@ -488,9 +482,9 @@ def save_unified_document(
|
||||
UnifiedDocumentExporter.export_to_json(document, path)
|
||||
results['json'] = path
|
||||
elif fmt == 'markdown':
|
||||
path = output_dir / f"{file_id}_output.md"
|
||||
UnifiedDocumentExporter.export_to_markdown(document, path)
|
||||
results['markdown'] = path
|
||||
# Markdown export removed - skip silently
|
||||
results['markdown'] = None
|
||||
continue
|
||||
elif fmt == 'text':
|
||||
path = output_dir / f"{file_id}_text.txt"
|
||||
UnifiedDocumentExporter.export_to_text(document, path)
|
||||
|
||||
Reference in New Issue
Block a user