feat(plagiarism): показывать текст работы с подсветкой совпадений
All checks were successful
Deploy / test (push) Successful in 3m49s
Deploy / deploy (push) Successful in 22s

Добавлен GET /tasks/{public_id}/text, читает извлечённый текст из
staged_works.text_key (тот же текст, по которому считались
position_start/position_end) — тот же MinIO-объект, что уже читает
админка для превью. Фронтенд: сворачиваемый блок с текстом работы,
где заимствования/цитаты/тематически близкие фрагменты подсвечены
разными цветами с тултипом на источник.
This commit is contained in:
jze9
2026-08-25 14:32:16 +05:00
parent 53d0de3d71
commit d02596cedb
5 changed files with 195 additions and 2 deletions

View File

@@ -10,8 +10,10 @@ from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.minio_client import get_minio
from app.core.security import get_current_user
from app.database import get_db
from app.models.admin import StagedWork
from app.models.task import Task
from app.models.user import User
from app.schemas.tasks import TaskResponse
@@ -63,6 +65,42 @@ async def get_task(
return TaskResponse.model_validate(task)
@router.get("/{public_id}/text")
async def get_task_text(
public_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""Полный текст проверенного документа — для подсветки совпадений в интерфейсе.
Тот же текст, по которому worker-gpu считал position_start/position_end в
отчёте о плагиате (см. StagedWork.text_key — пишется в _stage_work при
извлечении, worker-indexer/app/tasks/index.py). Запись туда — best-effort
(сбой не валит саму проверку), поэтому текст доступен не для всех задач.
"""
result = await db.execute(
select(Task).where(Task.public_id == public_id, Task.user_id == current_user.id)
)
task = result.scalar_one_or_none()
if task is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Задача не найдена")
staged = (
await db.execute(select(StagedWork).where(StagedWork.task_id == task.id))
).scalar_one_or_none()
if staged is None or not staged.text_key:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Текст работы недоступен")
try:
obj = get_minio().get_object("staging", staged.text_key)
text = obj.read().decode("utf-8", errors="replace")
except Exception as e:
logger.warning(f"Не удалось прочитать текст задачи {public_id!r}: {e}")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Текст работы недоступен") from e
return {"text": text}
@router.delete("/{public_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_task(
public_id: str,