feat: initial microservices project structure
Services: - api: FastAPI gateway with JWT auth, async endpoints, WebSocket - worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM - worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction - worker-notifier: SMTP email notifications - worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting Infrastructure: - docker-compose.yml (production) + docker-compose.dev.yml (hot reload) - Nginx reverse proxy + WebSocket support - PostgreSQL 16 with Alembic migrations - Elasticsearch 8 with Russian/English analyzers - MinIO, RabbitMQ, Redis, Ollama Frontend: - React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5 - 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register Scripts: - Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
services/worker-gost/app/formatters/__init__.py
Normal file
0
services/worker-gost/app/formatters/__init__.py
Normal file
57
services/worker-gost/app/formatters/gost_7_0_5.py
Normal file
57
services/worker-gost/app/formatters/gost_7_0_5.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""ГОСТ Р 7.0.5-2008 — Краткая библиографическая ссылка.
|
||||
|
||||
Используется для ссылок внутри текста: [Автор, год]
|
||||
"""
|
||||
|
||||
|
||||
class GOST705Formatter:
|
||||
"""Форматтер кратких библиографических ссылок по ГОСТ Р 7.0.5-2008."""
|
||||
|
||||
def format_short(self, doc: dict) -> str:
|
||||
"""
|
||||
Форматировать краткую ссылку вида [Автор, год].
|
||||
|
||||
Args:
|
||||
doc: Словарь с метаданными документа
|
||||
|
||||
Returns:
|
||||
Форматированная краткая ссылка, например [Иванов, 2023]
|
||||
"""
|
||||
authors = doc.get("authors", [])
|
||||
year = doc.get("year", "б. г.")
|
||||
|
||||
if authors:
|
||||
first_author = authors[0]
|
||||
last_name = first_author.get("last_name", "")
|
||||
if last_name:
|
||||
return f"[{last_name}, {year}]"
|
||||
|
||||
# Если нет авторов — используем первые слова названия
|
||||
title = doc.get("title", "")
|
||||
if title:
|
||||
short_title = " ".join(title.split()[:3])
|
||||
return f"[{short_title}..., {year}]"
|
||||
|
||||
return f"[{year}]"
|
||||
|
||||
def format_inline(self, doc: dict, page: str | None = None) -> str:
|
||||
"""
|
||||
Форматировать ссылку для включения в текст с опциональным номером страницы.
|
||||
|
||||
Args:
|
||||
doc: Словарь с метаданными
|
||||
page: Номер страницы (опционально)
|
||||
|
||||
Returns:
|
||||
Например: [Иванов, 2023, с. 15] или [Иванов, 2023]
|
||||
"""
|
||||
base = self.format_short(doc)
|
||||
if page:
|
||||
# Вставить номер страницы перед закрывающей скобкой
|
||||
return base[:-1] + f", с. {page}]"
|
||||
return base
|
||||
|
||||
|
||||
def format_short(doc: dict) -> str:
|
||||
"""Удобная функция для быстрого форматирования."""
|
||||
return GOST705Formatter().format_short(doc)
|
||||
267
services/worker-gost/app/formatters/gost_7_1.py
Normal file
267
services/worker-gost/app/formatters/gost_7_1.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""ГОСТ 7.1-2003 — Библиографическая запись. Библиографическое описание.
|
||||
|
||||
Полная запись для списка литературы.
|
||||
|
||||
Формат для статьи в журнале:
|
||||
Автор(ы). Название // Журнал. — Год. — Т. X, № X. — С. X–X.
|
||||
|
||||
Формат для книги:
|
||||
Автор(ы). Название. — Город : Издательство, Год. — X с.
|
||||
|
||||
Правила по ГОСТ 7.1-2003:
|
||||
- До 3 авторов: перечислить всех
|
||||
- 4+ авторов: первые 3 + "и др." (рус.) / "et al." (англ.)
|
||||
- Разделитель смысловых частей: " — "
|
||||
- Разделитель авторов: ", "
|
||||
- Место публикации через "/"
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
|
||||
def _format_author(author: dict) -> str:
|
||||
"""
|
||||
Форматировать одного автора.
|
||||
|
||||
Args:
|
||||
author: dict с полями last_name, first_name, initials (опционально)
|
||||
|
||||
Returns:
|
||||
Строка вида "Иванов И. И." или "Иванов И."
|
||||
"""
|
||||
last_name = author.get("last_name", "").strip()
|
||||
initials = author.get("initials", "").strip()
|
||||
first_name = author.get("first_name", "").strip()
|
||||
|
||||
if not last_name:
|
||||
return ""
|
||||
|
||||
if initials:
|
||||
# Нормализовать инициалы: обеспечить точки
|
||||
if not initials.endswith("."):
|
||||
initials += "."
|
||||
return f"{last_name} {initials}"
|
||||
elif first_name:
|
||||
# Извлечь инициалы из полного имени
|
||||
parts = first_name.split()
|
||||
inits = "".join(p[0].upper() + "." for p in parts if p)
|
||||
return f"{last_name} {inits}"
|
||||
else:
|
||||
return last_name
|
||||
|
||||
|
||||
def _format_authors(authors: list[dict], lang: str = "ru") -> str:
|
||||
"""
|
||||
Форматировать список авторов по правилам ГОСТ 7.1-2003.
|
||||
|
||||
Args:
|
||||
authors: Список словарей с авторами
|
||||
lang: Язык для "и др." / "et al."
|
||||
|
||||
Returns:
|
||||
Строка с отформатированными авторами
|
||||
"""
|
||||
if not authors:
|
||||
return ""
|
||||
|
||||
et_al = "и др." if lang == "ru" else "et al."
|
||||
formatted = [_format_author(a) for a in authors if a.get("last_name")]
|
||||
formatted = [f for f in formatted if f]
|
||||
|
||||
if not formatted:
|
||||
return ""
|
||||
|
||||
if len(formatted) <= 3:
|
||||
return ", ".join(formatted)
|
||||
else:
|
||||
return ", ".join(formatted[:3]) + f", {et_al}"
|
||||
|
||||
|
||||
class GOST71Formatter:
|
||||
"""Форматтер полных библиографических записей по ГОСТ 7.1-2003."""
|
||||
|
||||
def format_full(self, doc: dict) -> str:
|
||||
"""
|
||||
Форматировать полную библиографическую запись.
|
||||
|
||||
Args:
|
||||
doc: Словарь с полями документа
|
||||
|
||||
Returns:
|
||||
Отформатированная строка библиографической записи
|
||||
"""
|
||||
doc_type = self._detect_type(doc)
|
||||
|
||||
if doc_type == "article":
|
||||
return self._format_article(doc)
|
||||
elif doc_type == "book":
|
||||
return self._format_book(doc)
|
||||
elif doc_type == "web":
|
||||
return self._format_web(doc)
|
||||
else:
|
||||
return self._format_generic(doc)
|
||||
|
||||
def _detect_type(self, doc: dict) -> str:
|
||||
"""Определить тип документа."""
|
||||
if doc.get("journal"):
|
||||
return "article"
|
||||
if doc.get("url") and not doc.get("journal"):
|
||||
return "web"
|
||||
return "generic"
|
||||
|
||||
def _format_article(self, doc: dict) -> str:
|
||||
"""
|
||||
Форматировать статью в журнале.
|
||||
|
||||
Формат: Автор(ы). Название // Журнал. — Год. — Т. X, № X. — С. X–X.
|
||||
"""
|
||||
lang = doc.get("lang", "ru") or "ru"
|
||||
authors = doc.get("authors", [])
|
||||
author_str = _format_authors(authors, lang=lang)
|
||||
|
||||
title = (doc.get("title") or "").strip()
|
||||
journal = (doc.get("journal") or "").strip()
|
||||
year = doc.get("year", "")
|
||||
volume = doc.get("volume", "")
|
||||
issue = doc.get("issue", "")
|
||||
pages = doc.get("pages", "")
|
||||
doi = doc.get("doi", "")
|
||||
|
||||
parts = []
|
||||
|
||||
# Авторы и название
|
||||
if author_str:
|
||||
parts.append(f"{author_str}. {title}")
|
||||
else:
|
||||
parts.append(title)
|
||||
|
||||
# Место публикации
|
||||
location = f"// {journal}"
|
||||
if year:
|
||||
location += f". — {year}"
|
||||
if volume:
|
||||
location += f". — Т. {volume}"
|
||||
if issue:
|
||||
location += f", № {issue}"
|
||||
if pages:
|
||||
location += f". — С. {pages}"
|
||||
|
||||
parts.append(location)
|
||||
|
||||
result = " ".join(parts)
|
||||
|
||||
# DOI
|
||||
if doi:
|
||||
result += f". — DOI: {doi}"
|
||||
|
||||
return result.strip()
|
||||
|
||||
def _format_book(self, doc: dict) -> str:
|
||||
"""
|
||||
Форматировать книгу / монографию.
|
||||
|
||||
Формат: Автор(ы). Название. — Город : Издательство, Год. — X с.
|
||||
"""
|
||||
lang = doc.get("lang", "ru") or "ru"
|
||||
authors = doc.get("authors", [])
|
||||
author_str = _format_authors(authors, lang=lang)
|
||||
|
||||
title = (doc.get("title") or "").strip()
|
||||
year = doc.get("year", "б. г.")
|
||||
pages = doc.get("pages", "")
|
||||
|
||||
parts = []
|
||||
|
||||
if author_str:
|
||||
parts.append(f"{author_str}.")
|
||||
|
||||
parts.append(f"{title}.")
|
||||
parts.append(f"— {year}.")
|
||||
|
||||
if pages:
|
||||
# Предполагаем, что pages содержит количество страниц
|
||||
parts.append(f"— {pages} с.")
|
||||
|
||||
return " ".join(parts).strip()
|
||||
|
||||
def _format_web(self, doc: dict) -> str:
|
||||
"""
|
||||
Форматировать электронный ресурс.
|
||||
|
||||
Формат: Автор(ы). Название [Электронный ресурс]. — URL: ... (дата обращения: ...)
|
||||
"""
|
||||
lang = doc.get("lang", "ru") or "ru"
|
||||
authors = doc.get("authors", [])
|
||||
author_str = _format_authors(authors, lang=lang)
|
||||
|
||||
title = (doc.get("title") or "").strip()
|
||||
url = (doc.get("url") or "").strip()
|
||||
year = doc.get("year", "")
|
||||
|
||||
parts = []
|
||||
|
||||
if author_str:
|
||||
parts.append(f"{author_str}.")
|
||||
|
||||
# Для ГОСТ — обозначение электронного ресурса
|
||||
parts.append(f"{title} [Электронный ресурс].")
|
||||
|
||||
if year:
|
||||
parts.append(f"— {year}.")
|
||||
|
||||
if url:
|
||||
parts.append(f"— URL: {url}")
|
||||
|
||||
return " ".join(parts).strip()
|
||||
|
||||
def _format_generic(self, doc: dict) -> str:
|
||||
"""Базовый форматтер для неопределённых типов."""
|
||||
lang = doc.get("lang", "ru") or "ru"
|
||||
author_str = _format_authors(doc.get("authors", []), lang=lang)
|
||||
title = (doc.get("title") or "").strip()
|
||||
year = doc.get("year", "")
|
||||
|
||||
result = ""
|
||||
if author_str:
|
||||
result += f"{author_str}. "
|
||||
result += title
|
||||
if year:
|
||||
result += f". — {year}"
|
||||
|
||||
return result.strip()
|
||||
|
||||
|
||||
def _get_sort_key(doc: dict) -> str:
|
||||
"""
|
||||
Получить ключ сортировки для документа.
|
||||
|
||||
Кириллица идёт перед латиницей (для русских традиций):
|
||||
сначала русскоязычные источники, затем иностранные.
|
||||
|
||||
Args:
|
||||
doc: Словарь с метаданными
|
||||
|
||||
Returns:
|
||||
Строка для сортировки
|
||||
"""
|
||||
authors = doc.get("authors", [])
|
||||
if authors:
|
||||
last_name = authors[0].get("last_name", "")
|
||||
else:
|
||||
last_name = doc.get("title", "")
|
||||
|
||||
if not last_name:
|
||||
return "яяя" # В конец
|
||||
|
||||
# Кириллица < латиница (традиция: русские источники первыми)
|
||||
first_char = last_name[0].lower()
|
||||
is_latin = ord(first_char) < 256 and first_char.isalpha()
|
||||
|
||||
# Префикс для сортировки: 0 для кириллицы, 1 для латиницы
|
||||
prefix = "1" if is_latin else "0"
|
||||
return f"{prefix}{last_name.lower()}"
|
||||
|
||||
|
||||
def format_full(doc: dict) -> str:
|
||||
"""Удобная функция для быстрого форматирования."""
|
||||
return GOST71Formatter().format_full(doc)
|
||||
Reference in New Issue
Block a user