doocus first ver

This commit is contained in:
Mariano Gabriel
2026-07-05 10:08:42 -03:00
parent e214b17c55
commit ca8b3a784d
57 changed files with 4167 additions and 56 deletions

111
doocus/workflow.py Normal file
View File

@@ -0,0 +1,111 @@
"""
Orchestrate document extraction: dispatch → extract → write sidecars → manifest.
Follows the meetus WorkflowConfig / ProcessingWorkflow shape
(meetus/workflow.py) so the two pipelines feel the same.
"""
from pathlib import Path
from datetime import datetime
import logging
from typing import Dict, Any
from . import registry
from .naming import sha256_file
from .output_manager import DocOutputManager
logger = logging.getLogger(__name__)
class DocConfig:
"""Configuration for a single document extraction."""
def __init__(self, **kwargs):
self.source_path = Path(kwargs["source"])
self.output_dir = kwargs.get("output_dir", "docs-output")
self.no_cache = kwargs.get("no_cache", False)
self.render = kwargs.get("render", False) # optional page/slide images
self.ocr = kwargs.get("ocr", False) # OCR images / scanned pdf
def options(self) -> Dict[str, Any]:
return {"render": self.render, "ocr": self.ocr}
def to_dict(self) -> Dict[str, Any]:
return {"render": self.render, "ocr": self.ocr, "no_cache": self.no_cache}
class DocWorkflow:
"""Extract one document into a run folder."""
def __init__(self, config: DocConfig):
self.config = config
self.out = DocOutputManager(
config.source_path,
config.output_dir,
use_cache=not config.no_cache,
)
def run(self) -> Dict[str, Any]:
src = self.config.source_path
if not src.exists():
raise FileNotFoundError(f"source not found: {src}")
ext = registry.ext_of(src)
family = registry.family_for(src) or "unhandled"
logger.info("Extracting %s (family: %s)", src.name, family)
result = registry.extract(src, self.config.options())
content_path = self.out.write_content(result.content)
sha = sha256_file(src)
meta = self._build_meta(src, ext, family, sha, result)
self.out.write_meta(meta)
thumb_written = False
if result.thumb:
self.out.write_thumb(result.thumb)
thumb_written = True
outputs = {
"content": content_path.name,
"meta": "meta.json",
"thumb": "thumb.jpg" if thumb_written else None,
}
self.out.save_manifest(self.config.to_dict(), outputs, sha)
if result.warnings:
for w in result.warnings:
logger.warning(" %s", w)
logger.info("%s%s", src.name, self.out.output_dir)
return {
"output_dir": str(self.out.output_dir),
"content": str(content_path),
"meta": str(self.out.get_path("meta.json")),
"family": family,
"extractor": result.extractor,
"warnings": result.warnings,
}
def _build_meta(self, src: Path, ext: str, family: str, sha: str, result) -> Dict[str, Any]:
stat = src.stat()
meta = {
"source": {
"name": src.name,
"path": str(src.absolute()),
"sha256": sha,
"bytes": stat.st_size,
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
},
"type": ext,
"family": family,
"extractor": result.extractor,
"extracted_at": datetime.now().isoformat(),
"word_count": len(result.content.split()),
"warnings": result.warnings,
}
# Type-specific fields (title, author, dates, pages, structure, ...) sit
# at the top level alongside the core fields.
for k, v in result.metadata.items():
if k not in meta:
meta[k] = v
return meta