upd pdf service
This commit is contained in:
parent
1ca7efe4d1
commit
02469f89d5
@ -27,6 +27,9 @@ class Settings(BaseSettings):
|
|||||||
openai_model: str = "gpt-5-mini"
|
openai_model: str = "gpt-5-mini"
|
||||||
openai_embeddings_model: str = "text-embedding-3-small"
|
openai_embeddings_model: str = "text-embedding-3-small"
|
||||||
|
|
||||||
|
# PDF Generation API Keys
|
||||||
|
cloudmersive_api_key: str | None = None
|
||||||
|
|
||||||
# AI Agent API Keys (for voice interviewer)
|
# AI Agent API Keys (for voice interviewer)
|
||||||
deepgram_api_key: str | None = None
|
deepgram_api_key: str | None = None
|
||||||
cartesia_api_key: str | None = None
|
cartesia_api_key: str | None = None
|
||||||
|
@ -1,161 +1,18 @@
|
|||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import quote
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from playwright.async_api import async_playwright
|
|
||||||
|
|
||||||
from app.core.s3 import s3_service
|
from app.core.s3 import s3_service
|
||||||
from app.models.interview_report import InterviewReport, RecommendationType
|
from app.models.interview_report import InterviewReport, RecommendationType
|
||||||
|
|
||||||
|
|
||||||
class PDFReportService:
|
class PDFReportService:
|
||||||
"""Сервис для генерации PDF отчетов по интервью на основе HTML шаблона"""
|
"""Сервис для генерации и загрузки PDF отчетов в S3 хранилище"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.template_path = "templates/interview_report.html"
|
self.template_path = "templates/interview_report.html"
|
||||||
self._setup_fonts()
|
|
||||||
|
|
||||||
def _download_font(self, url: str, dest_path: str) -> str:
|
|
||||||
"""Скачивает шрифт по URL в dest_path (перезаписывает если нужно)."""
|
|
||||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
|
||||||
try:
|
|
||||||
resp = requests.get(url, stream=True, timeout=15)
|
|
||||||
resp.raise_for_status()
|
|
||||||
with open(dest_path, "wb") as f:
|
|
||||||
shutil.copyfileobj(resp.raw, f)
|
|
||||||
print(f"[OK] Downloaded font {url} -> {dest_path}")
|
|
||||||
return dest_path
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Failed to download font {url}: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _register_local_fonts(self, regular_path: str, bold_path: str):
|
|
||||||
"""Регистрирует шрифты в ReportLab, чтобы xhtml2pdf мог ими пользоваться."""
|
|
||||||
try:
|
|
||||||
from reportlab.lib.fonts import addMapping
|
|
||||||
from reportlab.pdfbase import pdfmetrics
|
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
|
|
||||||
pdfmetrics.registerFont(TTFont("DejaVuSans", regular_path))
|
|
||||||
pdfmetrics.registerFont(TTFont("DejaVuSans-Bold", bold_path))
|
|
||||||
# mapping: family, bold(1)/normal(0), italic(1)/normal(0), fontkey
|
|
||||||
addMapping("DejaVuSans", 0, 0, "DejaVuSans")
|
|
||||||
addMapping("DejaVuSans", 1, 0, "DejaVuSans-Bold")
|
|
||||||
|
|
||||||
self.available_fonts = ["DejaVuSans", "DejaVuSans-Bold"]
|
|
||||||
print("[OK] Registered DejaVu fonts in ReportLab")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Register fonts failed: {e}")
|
|
||||||
self.available_fonts = []
|
|
||||||
|
|
||||||
def _setup_fonts(self):
|
|
||||||
"""Настройка русских шрифтов для xhtml2pdf"""
|
|
||||||
self.available_fonts = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
from reportlab.lib.fonts import addMapping
|
|
||||||
from reportlab.pdfbase import pdfmetrics
|
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
|
||||||
|
|
||||||
# Используем скачанные DejaVu шрифты
|
|
||||||
fonts_dir = "static/fonts"
|
|
||||||
font_paths = [
|
|
||||||
(os.path.join(fonts_dir, "DejaVuSans.ttf"), "DejaVu", False, False),
|
|
||||||
(os.path.join(fonts_dir, "DejaVuSans-Bold.ttf"), "DejaVu", True, False),
|
|
||||||
]
|
|
||||||
|
|
||||||
for font_path, font_name, is_bold, is_italic in font_paths:
|
|
||||||
if os.path.exists(font_path):
|
|
||||||
try:
|
|
||||||
font_key = f"{font_name}"
|
|
||||||
if is_bold:
|
|
||||||
font_key += "-Bold"
|
|
||||||
if is_italic:
|
|
||||||
font_key += "-Italic"
|
|
||||||
|
|
||||||
# Проверяем, что шрифт можно загрузить
|
|
||||||
test_font = TTFont(font_key, font_path)
|
|
||||||
pdfmetrics.registerFont(test_font)
|
|
||||||
addMapping(font_name, is_bold, is_italic, font_key)
|
|
||||||
|
|
||||||
self.available_fonts.append(font_key)
|
|
||||||
print(f"[OK] Successfully registered font: {font_key}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Failed to register font {font_path}: {e}")
|
|
||||||
else:
|
|
||||||
print(f"[ERROR] Font file not found: {font_path}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Font setup failed: {e}")
|
|
||||||
|
|
||||||
print(f"Available fonts: {self.available_fonts}")
|
|
||||||
|
|
||||||
def _get_font_css(self) -> str:
|
|
||||||
"""Возвращает CSS с подключением локальных шрифтов (скачивает при необходимости)."""
|
|
||||||
# paths локальные
|
|
||||||
fonts_dir = os.path.abspath("static/fonts").replace("\\", "/")
|
|
||||||
regular_local = os.path.join(fonts_dir, "DejaVuSans.ttf").replace("\\", "/")
|
|
||||||
bold_local = os.path.join(fonts_dir, "DejaVuSans-Bold.ttf").replace("\\", "/")
|
|
||||||
|
|
||||||
# твои удалённые URL (используй свои)
|
|
||||||
remote_regular = (
|
|
||||||
"https://d8d88bee-afd2-4266-8332-538389e25f52.selstorage.ru/DejaVuSans.ttf"
|
|
||||||
)
|
|
||||||
remote_bold = "https://d8d88bee-afd2-4266-8332-538389e25f52.selstorage.ru/DejaVuSans-Bold.ttf"
|
|
||||||
|
|
||||||
# скачиваем если локально нет
|
|
||||||
try:
|
|
||||||
if not os.path.exists(regular_local) or os.path.getsize(regular_local) == 0:
|
|
||||||
self._download_font(remote_regular, regular_local)
|
|
||||||
if not os.path.exists(bold_local) or os.path.getsize(bold_local) == 0:
|
|
||||||
self._download_font(remote_bold, bold_local)
|
|
||||||
except Exception as e:
|
|
||||||
print("[WARNING] Failed to ensure local fonts:", e)
|
|
||||||
|
|
||||||
# регистрируем в ReportLab (чтобы гарантировать поддержку кириллицы)
|
|
||||||
try:
|
|
||||||
self._register_local_fonts(regular_local, bold_local)
|
|
||||||
except Exception as e:
|
|
||||||
print("[WARNING] Font registration error:", e)
|
|
||||||
|
|
||||||
# используем file:/// абсолютный путь в src и УБИРАЕМ format('...') — это важно
|
|
||||||
# url-энкодим путь на случай пробелов
|
|
||||||
reg_quoted = quote(regular_local)
|
|
||||||
bold_quoted = quote(bold_local)
|
|
||||||
|
|
||||||
font_css = f"""
|
|
||||||
<style>
|
|
||||||
@font-face {{
|
|
||||||
font-family: 'DejaVuSans';
|
|
||||||
src: url('file:///{reg_quoted}');
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
}}
|
|
||||||
@font-face {{
|
|
||||||
font-family: 'DejaVuSans';
|
|
||||||
src: url('file:///{bold_quoted}');
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal;
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* Применяем семейство — без !important, чтобы не ломать шаблон */
|
|
||||||
body, * {{
|
|
||||||
font-family: 'DejaVuSans', Arial, sans-serif;
|
|
||||||
}}
|
|
||||||
|
|
||||||
@page {{
|
|
||||||
size: A4;
|
|
||||||
margin: 0.75in;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
"""
|
|
||||||
return font_css
|
|
||||||
|
|
||||||
def _load_html_template(self) -> str:
|
def _load_html_template(self) -> str:
|
||||||
"""Загружает HTML шаблон из файла"""
|
"""Загружает HTML шаблон из файла"""
|
||||||
@ -194,7 +51,7 @@ class PDFReportService:
|
|||||||
if score >= 90:
|
if score >= 90:
|
||||||
return "score-green" # STRONGLY_RECOMMEND
|
return "score-green" # STRONGLY_RECOMMEND
|
||||||
elif score >= 75:
|
elif score >= 75:
|
||||||
return "score-light-green" # RECOMMEND
|
return "score-green" # RECOMMEND
|
||||||
elif score >= 60:
|
elif score >= 60:
|
||||||
return "score-orange" # CONSIDER
|
return "score-orange" # CONSIDER
|
||||||
else:
|
else:
|
||||||
@ -211,48 +68,24 @@ class PDFReportService:
|
|||||||
else: # REJECT
|
else: # REJECT
|
||||||
return ("Не рекомендуем", "reject-button")
|
return ("Не рекомендуем", "reject-button")
|
||||||
|
|
||||||
def link_callback(self, uri, rel):
|
async def generate_html_report(
|
||||||
"""Скачивает удалённый ресурс в temp файл и возвращает путь (для xhtml2pdf)."""
|
|
||||||
# remote -> сохранить во временный файл и вернуть путь
|
|
||||||
if uri.startswith("http://") or uri.startswith("https://"):
|
|
||||||
try:
|
|
||||||
r = requests.get(uri, stream=True, timeout=15)
|
|
||||||
r.raise_for_status()
|
|
||||||
fd, tmp_path = tempfile.mkstemp(suffix=os.path.basename(uri))
|
|
||||||
with os.fdopen(fd, "wb") as f:
|
|
||||||
for chunk in r.iter_content(8192):
|
|
||||||
f.write(chunk)
|
|
||||||
return tmp_path
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"Не удалось скачать ресурс {uri}: {e}")
|
|
||||||
# file:///path -> без префикса
|
|
||||||
if uri.startswith("file:///"):
|
|
||||||
return uri[7:]
|
|
||||||
# локальные относительные пути
|
|
||||||
if os.path.isfile(uri):
|
|
||||||
return uri
|
|
||||||
# fallback — возвращаем как есть (pisa попробует обработать)
|
|
||||||
return uri
|
|
||||||
|
|
||||||
def fetch_resources(self, uri, rel):
|
|
||||||
# Разрешаем xhtml2pdf скачивать https
|
|
||||||
return self.link_callback(uri, rel)
|
|
||||||
|
|
||||||
async def generate_pdf_report(
|
|
||||||
self,
|
self,
|
||||||
interview_report: InterviewReport,
|
interview_report: InterviewReport,
|
||||||
candidate_name: str = None,
|
candidate_name: str = None,
|
||||||
position: str = None,
|
position: str = None,
|
||||||
resume_file_url: str = None,
|
resume_file_url: str = None,
|
||||||
) -> bytes:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Генерирует PDF отчет на основе HTML шаблона
|
Генерирует HTML отчет на основе Jinja2 шаблона
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
interview_report: Данные отчета по интервью
|
interview_report: Данные отчета по интервью
|
||||||
|
candidate_name: Имя кандидата
|
||||||
|
position: Позиция
|
||||||
|
resume_file_url: URL резюме
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bytes: PDF файл в виде байтов
|
str: HTML содержимое отчета
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Загружаем HTML шаблон
|
# Загружаем HTML шаблон
|
||||||
@ -270,46 +103,14 @@ class PDFReportService:
|
|||||||
template = Template(html_template)
|
template = Template(html_template)
|
||||||
rendered_html = template.render(**template_data)
|
rendered_html = template.render(**template_data)
|
||||||
|
|
||||||
# Получаем CSS с проверенными шрифтами
|
# Сохраняем debug.html для отладки
|
||||||
font_css = self._get_font_css()
|
|
||||||
|
|
||||||
# Вставляем стили
|
|
||||||
if "<head>" in rendered_html:
|
|
||||||
rendered_html = rendered_html.replace("<head>", f"<head>{font_css}")
|
|
||||||
else:
|
|
||||||
rendered_html = font_css + rendered_html
|
|
||||||
|
|
||||||
with open("debug.html", "w", encoding="utf-8") as f:
|
with open("debug.html", "w", encoding="utf-8") as f:
|
||||||
f.write(rendered_html)
|
f.write(rendered_html)
|
||||||
|
|
||||||
# Генерируем PDF из debug.html с помощью Playwright
|
return rendered_html
|
||||||
print("[OK] Using Playwright to generate PDF from debug.html")
|
|
||||||
|
|
||||||
async def generate_pdf():
|
|
||||||
async with async_playwright() as p:
|
|
||||||
browser = await p.chromium.launch()
|
|
||||||
page = await browser.new_page()
|
|
||||||
await page.goto(f"file://{os.path.abspath('debug.html')}")
|
|
||||||
await page.wait_for_load_state("networkidle")
|
|
||||||
pdf_bytes = await page.pdf(
|
|
||||||
format="A4",
|
|
||||||
margin={
|
|
||||||
"top": "0.75in",
|
|
||||||
"bottom": "0.75in",
|
|
||||||
"left": "0.75in",
|
|
||||||
"right": "0.75in",
|
|
||||||
},
|
|
||||||
print_background=True,
|
|
||||||
)
|
|
||||||
await browser.close()
|
|
||||||
return pdf_bytes
|
|
||||||
|
|
||||||
pdf_bytes = await generate_pdf()
|
|
||||||
|
|
||||||
return pdf_bytes
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Ошибка при генерации PDF: {str(e)}")
|
raise Exception(f"Ошибка при генерации HTML отчета: {str(e)}")
|
||||||
|
|
||||||
def _prepare_template_data(
|
def _prepare_template_data(
|
||||||
self,
|
self,
|
||||||
@ -320,8 +121,8 @@ class PDFReportService:
|
|||||||
) -> dict:
|
) -> dict:
|
||||||
"""Подготавливает данные для HTML шаблона"""
|
"""Подготавливает данные для HTML шаблона"""
|
||||||
|
|
||||||
# Используем переданные параметры как в старой версии
|
# Используем переданные параметры
|
||||||
resume_url = resume_file_url # Пока оставим заглушку для ссылки на резюме
|
resume_url = resume_file_url or "#"
|
||||||
|
|
||||||
# Форматируем дату интервью
|
# Форматируем дату интервью
|
||||||
interview_date = "Не указана"
|
interview_date = "Не указана"
|
||||||
@ -341,7 +142,7 @@ class PDFReportService:
|
|||||||
interview_report.recommendation
|
interview_report.recommendation
|
||||||
)
|
)
|
||||||
|
|
||||||
# Сильные стороны и области развития (используем правильные поля модели)
|
# Сильные стороны и области развития
|
||||||
strengths = (
|
strengths = (
|
||||||
self._format_list_field(interview_report.strengths)
|
self._format_list_field(interview_report.strengths)
|
||||||
if interview_report.strengths
|
if interview_report.strengths
|
||||||
@ -353,7 +154,7 @@ class PDFReportService:
|
|||||||
else "Не указаны"
|
else "Не указаны"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Детальная оценка - всегда все критерии, как в старой версии
|
# Детальная оценка - всегда все критерии
|
||||||
evaluation_criteria = [
|
evaluation_criteria = [
|
||||||
{
|
{
|
||||||
"name": "Технические навыки",
|
"name": "Технические навыки",
|
||||||
@ -413,11 +214,9 @@ class PDFReportService:
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Красные флаги - используем поле модели напрямую
|
# Красные флаги
|
||||||
red_flags = interview_report.red_flags or []
|
red_flags = interview_report.red_flags or []
|
||||||
|
|
||||||
# Ссылка на резюме (уже определена выше)
|
|
||||||
|
|
||||||
# ID отчета
|
# ID отчета
|
||||||
report_id = f"#{interview_report.id}" if interview_report.id else "#0"
|
report_id = f"#{interview_report.id}" if interview_report.id else "#0"
|
||||||
|
|
||||||
@ -464,49 +263,6 @@ class PDFReportService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Ошибка при загрузке PDF в S3: {str(e)}")
|
raise Exception(f"Ошибка при загрузке PDF в S3: {str(e)}")
|
||||||
|
|
||||||
async def generate_and_upload_pdf(
|
|
||||||
self,
|
|
||||||
report: InterviewReport,
|
|
||||||
candidate_name: str = None,
|
|
||||||
position: str = None,
|
|
||||||
resume_file_url: str = None,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Генерирует PDF отчет и загружает его в S3 (метод обратной совместимости)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
report: Отчет по интервью
|
|
||||||
candidate_name: Имя кандидата (не используется, берется из отчета)
|
|
||||||
position: Позиция (не используется, берется из отчета)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: Публичная ссылка на PDF файл
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Генерируем PDF
|
|
||||||
pdf_bytes = await self.generate_pdf_report(
|
|
||||||
report, candidate_name, position, resume_file_url
|
|
||||||
)
|
|
||||||
|
|
||||||
# Создаем имя файла - используем переданный параметр как в старой версии
|
|
||||||
safe_name = (
|
|
||||||
candidate_name
|
|
||||||
if candidate_name and candidate_name != "Не указано"
|
|
||||||
else "candidate"
|
|
||||||
)
|
|
||||||
|
|
||||||
safe_name = "".join(
|
|
||||||
c for c in safe_name if c.isalnum() or c in (" ", "-", "_")
|
|
||||||
).strip()
|
|
||||||
filename = f"interview_report_{safe_name}_{report.id}.pdf"
|
|
||||||
|
|
||||||
# Загружаем в S3
|
|
||||||
pdf_url = await self.upload_pdf_to_s3(pdf_bytes, filename)
|
|
||||||
|
|
||||||
return pdf_url
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise Exception(f"Ошибка при генерации и загрузке PDF: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
# Экземпляр сервиса
|
# Экземпляр сервиса
|
||||||
|
@ -780,14 +780,14 @@ def parse_vacancy_task(
|
|||||||
|
|
||||||
@celery_app.task(bind=True)
|
@celery_app.task(bind=True)
|
||||||
def generate_pdf_report_task(
|
def generate_pdf_report_task(
|
||||||
self,
|
self,
|
||||||
report_data: dict,
|
report_data: dict,
|
||||||
candidate_name: str = None,
|
candidate_name: str = None,
|
||||||
position: str = None,
|
position: str = None,
|
||||||
resume_file_url: str = None,
|
resume_file_url: str = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Асинхронная задача для генерации PDF отчета по интервью
|
Асинхронная задача для генерации PDF отчета по интервью с использованием PDFShift API
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
report_data: Словарь с данными отчета InterviewReport
|
report_data: Словарь с данными отчета InterviewReport
|
||||||
@ -797,9 +797,9 @@ def generate_pdf_report_task(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import requests
|
||||||
from app.models.interview_report import InterviewReport
|
from datetime import datetime
|
||||||
from app.services.pdf_report_service import pdf_report_service
|
from app.core.config import settings
|
||||||
from celery_worker.database import (
|
from celery_worker.database import (
|
||||||
SyncInterviewReportRepository,
|
SyncInterviewReportRepository,
|
||||||
get_sync_session,
|
get_sync_session,
|
||||||
@ -811,41 +811,43 @@ def generate_pdf_report_task(
|
|||||||
meta={"status": "Начинаем генерацию PDF отчета...", "progress": 10},
|
meta={"status": "Начинаем генерацию PDF отчета...", "progress": 10},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Создаем объект InterviewReport из переданных данных
|
# Генерируем HTML контент отчета из Jinja шаблона
|
||||||
self.update_state(
|
self.update_state(
|
||||||
state="PROGRESS",
|
state="PROGRESS",
|
||||||
meta={"status": "Подготавливаем данные отчета...", "progress": 20},
|
meta={"status": "Подготавливаем HTML отчета...", "progress": 20},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Подготавливаем данные для создания объекта
|
# Создаем объект InterviewReport для использования в сервисе
|
||||||
clean_report_data = report_data.copy()
|
clean_report_data = report_data.copy()
|
||||||
|
|
||||||
# Обрабатываем datetime поля - убираем их, так как они не нужны для создания mock объекта
|
|
||||||
clean_report_data.pop('created_at', None)
|
clean_report_data.pop('created_at', None)
|
||||||
clean_report_data.pop('updated_at', None)
|
clean_report_data.pop('updated_at', None)
|
||||||
|
|
||||||
# Создаем объект InterviewReport с обработанными данными
|
from app.models.interview_report import InterviewReport
|
||||||
mock_report = InterviewReport(**clean_report_data)
|
mock_report = InterviewReport(**clean_report_data)
|
||||||
|
|
||||||
# Генерируем PDF
|
# Генерируем HTML через сервис
|
||||||
self.update_state(
|
def run_html_generation():
|
||||||
state="PROGRESS", meta={"status": "Генерируем PDF отчет...", "progress": 40}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Запускаем асинхронную функцию в новом цикле событий
|
|
||||||
def run_pdf_generation():
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
|
from app.services.pdf_report_service import pdf_report_service
|
||||||
return loop.run_until_complete(
|
return loop.run_until_complete(
|
||||||
pdf_report_service.generate_pdf_report(
|
pdf_report_service.generate_html_report(
|
||||||
mock_report, candidate_name, position, resume_file_url
|
mock_report, candidate_name, position, resume_file_url
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
|
|
||||||
pdf_bytes = run_pdf_generation()
|
html_content = run_html_generation()
|
||||||
|
|
||||||
|
# Генерируем PDF через Cloudmersive API
|
||||||
|
self.update_state(
|
||||||
|
state="PROGRESS",
|
||||||
|
meta={"status": "Конвертируем HTML в PDF через Cloudmersive...", "progress": 50}
|
||||||
|
)
|
||||||
|
|
||||||
|
pdf_bytes = _convert_html_to_pdf_cloudmersive(html_content)
|
||||||
|
|
||||||
# Загружаем в S3
|
# Загружаем в S3
|
||||||
self.update_state(
|
self.update_state(
|
||||||
@ -857,6 +859,8 @@ def generate_pdf_report_task(
|
|||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
try:
|
try:
|
||||||
|
from app.services.pdf_report_service import pdf_report_service
|
||||||
|
|
||||||
# Создаем имя файла
|
# Создаем имя файла
|
||||||
safe_name = (
|
safe_name = (
|
||||||
candidate_name
|
candidate_name
|
||||||
@ -918,3 +922,63 @@ def generate_pdf_report_task(
|
|||||||
)
|
)
|
||||||
|
|
||||||
raise Exception(f"Ошибка при генерации PDF: {str(e)}")
|
raise Exception(f"Ошибка при генерации PDF: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_html_to_pdf_cloudmersive(html_content: str) -> bytes:
|
||||||
|
"""
|
||||||
|
Конвертирует HTML в PDF используя Cloudmersive API через HTTP requests
|
||||||
|
|
||||||
|
Args:
|
||||||
|
html_content: HTML содержимое для конвертации
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bytes: PDF файл в виде байтов
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
# Проверяем наличие API ключа
|
||||||
|
if not hasattr(settings, 'cloudmersive_api_key') or not settings.cloudmersive_api_key:
|
||||||
|
raise Exception("Cloudmersive API ключ не настроен в settings.cloudmersive_api_key")
|
||||||
|
|
||||||
|
# Проверяем HTML на корректность
|
||||||
|
if not html_content or len(html_content.strip()) == 0:
|
||||||
|
raise Exception("HTML содержимое пустое")
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"[Cloudmersive] Starting HTML to PDF conversion via HTTP...")
|
||||||
|
print(f"[Cloudmersive] HTML length: {len(html_content)} characters")
|
||||||
|
|
||||||
|
# Отправляем HTTP запрос с HTML как raw данные - правильный эндпоинт
|
||||||
|
url = "https://api.cloudmersive.com/convert/html/to/pdf"
|
||||||
|
headers = {
|
||||||
|
'Apikey': settings.cloudmersive_api_key,
|
||||||
|
'Content-Type': 'text/html'
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"[Cloudmersive] Sending HTTP request to {url}")
|
||||||
|
print(f"[Cloudmersive] Sending HTML as raw data...")
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
headers=headers,
|
||||||
|
data=html_content.encode('utf-8'),
|
||||||
|
timeout=60
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[Cloudmersive] Response status: {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
pdf_bytes = response.content
|
||||||
|
print(f"[Cloudmersive] Conversion successful, PDF size: {len(pdf_bytes)} bytes")
|
||||||
|
return pdf_bytes
|
||||||
|
else:
|
||||||
|
print(f"[Cloudmersive] Error response: {response.text}")
|
||||||
|
raise Exception(f"Cloudmersive API returned {response.status_code}: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"[Cloudmersive] HTTP Error: {e}")
|
||||||
|
raise Exception(f"Ошибка HTTP запроса к Cloudmersive: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Cloudmersive] Unexpected error: {e}")
|
||||||
|
raise Exception(f"Неожиданная ошибка при генерации PDF: {str(e)}")
|
||||||
|
@ -34,9 +34,7 @@ dependencies = [
|
|||||||
"pdfkit>=1.0.0",
|
"pdfkit>=1.0.0",
|
||||||
"jinja2>=3.1.6",
|
"jinja2>=3.1.6",
|
||||||
"greenlet>=3.2.4",
|
"greenlet>=3.2.4",
|
||||||
"xhtml2pdf>=0.2.17",
|
"requests>=2.31.0",
|
||||||
"playwright>=1.55.0",
|
|
||||||
"celery-types==0.23.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
@ -9,6 +9,8 @@ class RagSettings(BaseSettings):
|
|||||||
milvus_uri: str = "milvus_uri"
|
milvus_uri: str = "milvus_uri"
|
||||||
milvus_collection: str = "candidate_profiles"
|
milvus_collection: str = "candidate_profiles"
|
||||||
|
|
||||||
|
cloudmersive_api_key: str = "cloudmersive_api_key"
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
redis_cache_url: str = "localhost"
|
redis_cache_url: str = "localhost"
|
||||||
redis_cache_port: int = 6379
|
redis_cache_port: int = 6379
|
||||||
|
Loading…
Reference in New Issue
Block a user