Files
doc-translator/src/worker/app.py
T
anker-na-20 b6dd80e749 First commit
This is base for automatization translating
2026-05-21 10:45:27 +03:00

205 lines
6.5 KiB
Python

from fastapi import FastAPI
from pydantic import BaseModel
import fitz # PyMuPDF
import os
import psycopg2
from datetime import datetime
import json
DB_DSN = os.getenv("DB_DSN", "postgresql://doc_user:doc_password@db:5432/doc_translator")
app = FastAPI()
class ProcessRequest(BaseModel):
document_id: int
original_path: str
def get_db_conn():
return psycopg2.connect(DB_DSN)
@app.post("/process-document")
def process_document(req: ProcessRequest):
doc_id = req.document_id
path = req.original_path
if not os.path.exists(path):
_set_status(doc_id, "failed", f"File not found: {path}")
return {"ok": False, "error": "file_not_found"}
try:
_set_status(doc_id, "processing", None)
# 1. Открываем PDF
pdf = fitz.open(path)
page_count = pdf.page_count
# Обновим page_count в documents
_update_document_page_count(doc_id, page_count)
# 2. Проходим по страницам и блокам текста
translations = []
for page_index in range(page_count):
page = pdf[page_index]
blocks = page.get_text("blocks") # список блоков: (x0, y0, x1, y1, text, block_no, block_type, ...)
segment_index = 0
for b in blocks:
x0, y0, x1, y1, text, *_rest = b
if not text.strip():
continue
segment_index += 1
# TODO: здесь вызвать DeepSeek через Ollama
translated = _translate_text(text)
bbox = {"x0": x0, "y0": y0, "x1": x1, "y1": y1}
translations.append({
"document_id": doc_id,
"page_number": page_index + 1,
"segment_index": segment_index,
"source_text": text,
"translated_text": translated,
"bbox": bbox,
})
# 3. Сохраняем сегменты в таблицу translations
_insert_translations(translations)
# 4. Собираем новый PDF с русским текстом
output_path = _build_translated_pdf(path, pdf, translations, doc_id)
# 5. Обновляем запись в documents
_set_status(doc_id, "completed", None, output_path=output_path)
return {"ok": True, "document_id": doc_id, "output_path": output_path}
except Exception as e:
_set_status(doc_id, "failed", str(e))
return {"ok": False, "error": str(e)}
def _set_status(doc_id: int, status: str, error: str | None, output_path: str | None = None):
conn = get_db_conn()
cur = conn.cursor()
cur.execute(
"""
UPDATE documents
SET status = %s,
error_message = %s,
output_path = COALESCE(%s, output_path),
updated_at = %s
WHERE id = %s
""",
(status, error, output_path, datetime.now(), doc_id),
)
conn.commit()
cur.close()
conn.close()
def _update_document_page_count(doc_id: int, page_count: int):
conn = get_db_conn()
cur = conn.cursor()
cur.execute(
"UPDATE documents SET page_count = %s WHERE id = %s",
(page_count, doc_id),
)
conn.commit()
cur.close()
conn.close()
def _translate_text(text: str) -> str:
"""
Заглушка перевода.
Здесь надо:
- взять активный промпт из таблицы prompts;
- вызвать Ollama (DeepSeek) HTTP-запросом;
- вернуть перевод.
Пока просто возвращаем исходный текст.
"""
return text # TODO: заменить на реальный вызов LLM
def _insert_translations(rows: list[dict]):
conn = get_db_conn()
cur = conn.cursor()
for r in rows:
cur.execute(
"""
INSERT INTO translations
(document_id, page_number, segment_index, source_text, translated_text, bbox, model_name, created_at)
VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s, %s)
""",
(
r["document_id"],
r["page_number"],
r["segment_index"],
r["source_text"],
r["translated_text"],
json.dumps(r["bbox"]),
"deepseek-r1:7b",
datetime.now(),
),
)
conn.commit()
cur.close()
conn.close()
def _build_translated_pdf(original_path: str, pdf: fitz.Document, segments: list[dict], doc_id: int) -> str:
"""
Строим новый PDF:
- берем исходную страницу как фон;
- поверх в тех же bbox'ах рисуем русский текст.
Это упрощенный вариант: не идеальная типографика, но структура и картинки сохраняются.
"""
import pathlib
import json
# Индексируем переводы по (page, segment_index) или лучше по bbox
by_page: dict[int, list[dict]] = {}
for s in segments:
by_page.setdefault(s["page_number"], []).append(s)
output_dir = os.getenv("OUTPUT_DIR", "/data/output")
os.makedirs(output_dir, exist_ok=True)
basename = os.path.basename(original_path)
name, _ext = os.path.splitext(basename)
output_path = os.path.join(output_dir, f"{name}_ru.pdf")
# Создаем новый PDF
out_doc = fitz.open()
for page_index in range(pdf.page_count):
src_page = pdf[page_index]
rect = src_page.rect
new_page = out_doc.new_page(width=rect.width, height=rect.height)
# Рендерим исходную страницу как картинку (фон)
pix = src_page.get_pixmap(dpi=150)
img_rect = fitz.Rect(0, 0, rect.width, rect.height)
new_page.insert_image(img_rect, pixmap=pix)
# Наносим переведенный текст поверх
page_number = page_index + 1
for seg in by_page.get(page_number, []):
bbox = seg["bbox"]
r = fitz.Rect(bbox["x0"], bbox["y0"], bbox["x1"], bbox["y1"])
new_page.insert_textbox(
r,
seg["translated_text"],
fontsize=8,
fontname="helv",
align=fitz.TEXT_ALIGN_LEFT,
)
out_doc.save(output_path)
out_doc.close()
pdf.close()
return output_path