anovo-api / routers /document.py
rushabh13's picture
Deploy document workspace and CoWriter API
ad25fb2
Raw
History Blame Contribute Delete
4.64 kB
import io
import logging
from docx import Document
from docx.shared import Pt
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from services.humanize_service import humanize as _humanize
from services.paraphrase_service import paraphrase as _paraphrase
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["document"])
class DocProcessResponse(BaseModel):
original_text: str
processed_text: str
mode: str
filename: str
class DocExtractResponse(BaseModel):
text: str
filename: str
character_count: int
class DocDownloadRequest(BaseModel):
processed_text: str
filename: str
def _extract_document_text(filename: str, content: bytes) -> str:
suffix = filename.lower().rsplit(".", 1)[-1] if "." in filename else ""
if suffix == "docx":
try:
doc = Document(io.BytesIO(content))
except Exception as exc:
raise HTTPException(status_code=400, detail=f"Could not read .docx file: {exc}")
text = "\n\n".join(paragraph.text.strip() for paragraph in doc.paragraphs if paragraph.text.strip())
elif suffix == "txt":
try:
text = content.decode("utf-8-sig").strip()
except UnicodeDecodeError as exc:
raise HTTPException(status_code=400, detail="Text files must use UTF-8 encoding") from exc
else:
raise HTTPException(status_code=400, detail="Upload a .docx or .txt document")
if not text:
raise HTTPException(status_code=400, detail="Document contains no readable text")
if len(text) > 50000:
raise HTTPException(
status_code=400,
detail="Document too large. Maximum 50,000 characters supported.",
)
return text
@router.post(
"/documents/extract",
response_model=DocExtractResponse,
summary="Extract editable text from a document",
)
async def extract_document(file: UploadFile = File(...)) -> DocExtractResponse:
"""Load a document into the unified writing workspace without processing it."""
filename = file.filename or "document.docx"
text = _extract_document_text(filename, await file.read())
return DocExtractResponse(text=text, filename=filename, character_count=len(text))
@router.post(
"/upload-doc",
response_model=DocProcessResponse,
summary="Upload a Word doc and humanize or paraphrase it",
)
async def upload_doc(
file: UploadFile = File(...),
mode: str = Form("humanize"),
):
"""
Upload a .docx file and process it through humanize or paraphrase.
Returns JSON with original and processed text for side-by-side comparison.
Use `/api/upload-doc/download` to get the result as a .docx file.
"""
if not file.filename or not file.filename.lower().endswith(".docx"):
raise HTTPException(status_code=400, detail="Only .docx files are supported by this legacy endpoint")
if mode not in ("humanize", "paraphrase"):
raise HTTPException(status_code=400, detail="Mode must be 'humanize' or 'paraphrase'")
full_text = _extract_document_text(file.filename, await file.read())
try:
if mode == "humanize":
result = _humanize(full_text)
processed_text = result["humanized"]
else:
processed_text, _ = _paraphrase(full_text, intensity=3)
except Exception as exc:
logger.exception("Document processing failed")
raise HTTPException(status_code=500, detail=f"Processing failed: {exc}")
return DocProcessResponse(
original_text=full_text,
processed_text=processed_text,
mode=mode,
filename=file.filename or "document.docx",
)
@router.post("/upload-doc/download", summary="Download processed text as .docx")
def download_doc(request: DocDownloadRequest):
"""Convert processed text back into a downloadable .docx file."""
out_doc = Document()
style = out_doc.styles["Normal"]
style.font.size = Pt(11)
style.font.name = "Calibri"
for paragraph in request.processed_text.split("\n\n"):
paragraph = paragraph.strip()
if paragraph:
out_doc.add_paragraph(paragraph)
buf = io.BytesIO()
out_doc.save(buf)
buf.seek(0)
base_name = request.filename.rsplit(".", 1)[0]
out_name = f"{base_name}_processed.docx"
return StreamingResponse(
buf,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
headers={"Content-Disposition": f'attachment; filename="{out_name}"'},
)