Files
apuc 04a7b31a75 Веб-приложение диагностики по видео (ФПГ GigaDoc)
- Загрузка/сканирование видео, извлечение кадров, анализ через SDK ФПГ
- Понятный диагноз с сохранением в SQLite и share-ссылками
- Экспорт PDF/DOCX, скачивание записанного видео
- Камерное сканирование на мобильных (canvas + MediaRecorder)
- Секреты вынесены в переменные окружения
2026-09-07 19:01:06 +03:00

268 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
Генерация PDF и DOCX отчётов из сохранённого результата диагностики.
"""
import io
import json
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib import colors
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
# --- Шрифты для кириллицы в PDF ---
FONT_PATHS = {
"DejaVuSans": "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"DejaVuSans-Bold": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"DejaVuSans-Oblique": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf",
}
for name, path in FONT_PATHS.items():
try:
pdfmetrics.registerFont(TTFont(name, path))
except Exception:
pass
STATUS_TEXT = {
"ok": "В норме",
"warning": "Обратить внимание",
"danger": "Отклонение",
"info": "Информация",
}
STATUS_COLORS = {
"ok": colors.HexColor("#155724"),
"warning": colors.HexColor("#856404"),
"danger": colors.HexColor("#721c24"),
"info": colors.HexColor("#0c5460"),
}
STATUS_BG = {
"ok": colors.HexColor("#d4edda"),
"warning": colors.HexColor("#fff3cd"),
"danger": colors.HexColor("#f8d7da"),
"info": colors.HexColor("#d1ecf1"),
}
def _load_result(share_id):
"""Загрузить результат из БД (импорт на месте, чтобы не было циклических зависимостей)."""
import storage
return storage.get_result(share_id)
def _parse_json_list(raw):
"""Вернуть список из БД (get_result уже возвращает распарсенные списки)."""
if isinstance(raw, list):
return raw
try:
return json.loads(raw) if raw else []
except Exception:
return []
# ============================ PDF ============================
def generate_pdf(share_id) -> bytes:
"""Сгенерировать PDF-отчёт. Возвращает байты."""
result = _load_result(share_id)
if result is None:
raise ValueError("Результат не найден")
buf = io.BytesIO()
doc = SimpleDocTemplate(
buf, pagesize=A4,
leftMargin=20 * mm, rightMargin=20 * mm,
topMargin=18 * mm, bottomMargin=18 * mm,
title=f"Результат диагностики #{share_id}",
author="med.my-dpr.ru",
)
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(
name="TitleRu", fontName="DejaVuSans-Bold", fontSize=18,
leading=24, alignment=TA_CENTER, spaceAfter=4,
))
styles.add(ParagraphStyle(
name="SubtitleRu", fontName="DejaVuSans", fontSize=10,
leading=14, alignment=TA_CENTER, textColor=colors.grey, spaceAfter=14,
))
styles.add(ParagraphStyle(
name="H2Ru", fontName="DejaVuSans-Bold", fontSize=13,
leading=18, spaceBefore=12, spaceAfter=6,
))
styles.add(ParagraphStyle(
name="BodyRu", fontName="DejaVuSans", fontSize=10.5,
leading=16, spaceAfter=6,
))
styles.add(ParagraphStyle(
name="SmallRu", fontName="DejaVuSans", fontSize=8.5,
leading=12, textColor=colors.grey,
))
story = []
# Заголовок
story.append(Paragraph("🩺 Диагностика по видео", styles["TitleRu"]))
story.append(Paragraph(
f"Результат #{result['share_id']}{result['created_at'][:10]} {result['created_at'][11:16]}",
styles["SubtitleRu"],
))
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor("#667eea"), spaceAfter=12))
# Сводка
story.append(Paragraph("Сводка", styles["H2Ru"]))
story.append(Paragraph(result["summary"], styles["BodyRu"]))
# Предупреждения
warnings = _parse_json_list(result["warnings"])
if warnings:
story.append(Paragraph("⚠️ Требует внимания", styles["H2Ru"]))
for w in warnings:
story.append(Paragraph(f"• {w}", styles["BodyRu"]))
# Показатели
metrics = _parse_json_list(result["metrics"])
if metrics:
story.append(Paragraph("Показатели", styles["H2Ru"]))
data = [["Показатель", "Значение", "Оценка"]]
for m in metrics:
value = m.get("value", "")
comment = m.get("comment", "")
if comment:
value = f"{value} ({comment})"
status = m.get("status", "info")
data.append([
m.get("name", ""),
value,
STATUS_TEXT.get(status, status),
])
table = Table(data, colWidths=[55 * mm, 75 * mm, 50 * mm])
table.setStyle(TableStyle([
("FONTNAME", (0, 0), (-1, 0), "DejaVuSans-Bold"),
("FONTNAME", (0, 1), (-1, -1), "DejaVuSans"),
("FONTSIZE", (0, 0), (-1, -1), 9.5),
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#667eea")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f5f6ff")]),
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#cccccc")),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("LEFTPADDING", (0, 0), (-1, -1), 6),
("RIGHTPADDING", (0, 0), (-1, -1), 6),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
]))
story.append(table)
# Рекомендации
advice = _parse_json_list(result["advice"])
if advice:
story.append(Paragraph("💡 Рекомендации", styles["H2Ru"]))
for a in advice:
story.append(Paragraph(f"• {a}", styles["BodyRu"]))
story.append(Spacer(1, 10))
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#dddddd")))
story.append(Spacer(1, 6))
story.append(Paragraph(
"⚠️ Результат получен автоматически по анализу видео и не является медицинским диагнозом. "
"Для точной оценки обратитесь к врачу.",
styles["SmallRu"],
))
doc.build(story)
return buf.getvalue()
# ============================ DOCX ============================
def generate_docx(share_id) -> bytes:
"""Сгенерировать DOCX-отчёт. Возвращает байты."""
result = _load_result(share_id)
if result is None:
raise ValueError("Результат не найден")
doc = Document()
# Стили
style = doc.styles["Normal"]
style.font.name = "Calibri"
style.font.size = Pt(11)
# Заголовок
title = doc.add_heading("🩺 Диагностика по видео", level=0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
for run in title.runs:
run.font.color.rgb = RGBColor(0x66, 0x7E, 0xEA)
sub = doc.add_paragraph()
sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = sub.add_run(
f"Результат #{result['share_id']}{result['created_at'][:10]} {result['created_at'][11:16]}"
)
run.font.size = Pt(10)
run.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
# Сводка
doc.add_heading("Сводка", level=1)
doc.add_paragraph(result["summary"])
# Предупреждения
warnings = _parse_json_list(result["warnings"])
if warnings:
doc.add_heading("⚠️ Требует внимания", level=1)
for w in warnings:
doc.add_paragraph(w, style="List Bullet")
# Показатели
metrics = _parse_json_list(result["metrics"])
if metrics:
doc.add_heading("Показатели", level=1)
table = doc.add_table(rows=1, cols=3)
table.style = "Light Grid Accent 1"
hdr = table.rows[0].cells
for i, h in enumerate(["Показатель", "Значение", "Оценка"]):
hdr[i].text = h
for p in hdr[i].paragraphs:
for r in p.runs:
r.font.bold = True
for m in metrics:
row = table.add_row().cells
value = m.get("value", "")
comment = m.get("comment", "")
if comment:
value = f"{value} ({comment})"
row[0].text = m.get("name", "")
row[1].text = value
row[2].text = STATUS_TEXT.get(m.get("status", "info"), m.get("status", ""))
# Рекомендации
advice = _parse_json_list(result["advice"])
if advice:
doc.add_heading("💡 Рекомендации", level=1)
for a in advice:
doc.add_paragraph(a, style="List Bullet")
# Дисклеймер
doc.add_paragraph()
disc = doc.add_paragraph()
run = disc.add_run(
"⚠️ Результат получен автоматически по анализу видео и не является медицинским диагнозом. "
"Для точной оценки обратитесь к врачу."
)
run.font.size = Pt(9)
run.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
buf = io.BytesIO()
doc.save(buf)
return buf.getvalue()