| 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}"'}, |
| ) |
|
|