Compare commits

..

No commits in common. "33215d33cbb2bd8e7191426a686802be66cf0239" and "6a1bbb9a08076e363dc7bbf468ee363d97fe2c61" have entirely different histories.

15 changed files with 27 additions and 280 deletions

View File

@ -1 +1 @@
cpython-3.13.12-linux-x86_64-gnu cpython-3.13.5-linux-x86_64-gnu

View File

@ -14,7 +14,7 @@ format:
uv run ruff format uv run ruff format
lint: lint:
uv run mypy ./server --explicit-package-bases; uv run mypy ./ --explicit-package-bases;
ruff check --fix ruff check --fix
pipinstall: pipinstall:

View File

@ -35,5 +35,4 @@ class ResultsView(ModelView):
"result", "result",
"path", "path",
"beerd", "beerd",
"probability"
] ]

View File

@ -3,13 +3,11 @@ from dataclasses import dataclass
from typing import Annotated from typing import Annotated
import inject import inject
from litestar import Controller, Request, Response, get, post from litestar import Controller, Request, Response, post
from litestar.datastructures import UploadFile from litestar.datastructures import UploadFile
from litestar.enums import RequestEncodingType from litestar.enums import RequestEncodingType
from litestar.params import Body from litestar.params import Body
from litestar.response import Template
from server.modules.attachments import AtachmentService
from server.modules.recognizer import RecognizerService from server.modules.recognizer import RecognizerService
@ -51,18 +49,3 @@ class BreedsController(Controller):
body = await data.read() body = await data.read()
result = await recognizer_service.predict_cat_image(body) result = await recognizer_service.predict_cat_image(body)
return result.to_serializable() return result.to_serializable()
@get("/dogs/share/{result_id:str}")
async def beerds_share(self, result_id: str) -> Template:
recognizer_service: RecognizerService = inject.instance(RecognizerService)
result = await recognizer_service.get_results(result_id)
attach_service: AtachmentService = inject.instance(AtachmentService)
attachments = await attach_service.get_info_byid(session=None, attach_id=[result.attachment_id])
return Template(
template_name="share.html",
context={
"result": result,
"attachment": attachments[0],
"share_id": result_id,
},
)

View File

@ -1,29 +0,0 @@
"""3baab00
Revision ID: 081eb0827a55
Revises: bebaddef3e8d
Create Date: 2026-02-07 20:46:53.971562
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "081eb0827a55"
down_revision: str | None = "bebaddef3e8d"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("recognizer_results_beerds", sa.Column("probability", sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("recognizer_results_beerds", "probability")
# ### end Alembic commands ###

View File

@ -1,8 +1,7 @@
from server.modules.descriptions.repository.repository import ( from server.modules.descriptions.repository.repository import (
ACharactersRepository, ACharactersRepository,
Breed,
CharactersRepository, CharactersRepository,
PGCharactersRepository, PGCharactersRepository,
) )
__all__ = ("CharactersRepository", "ACharactersRepository", "PGCharactersRepository", "Breed") __all__ = ("CharactersRepository", "ACharactersRepository", "PGCharactersRepository")

View File

@ -13,9 +13,9 @@ from sqlalchemy.orm import relationship
from server.config import get_app_config from server.config import get_app_config
from server.infra.db.db_mapper import mapper_registry from server.infra.db.db_mapper import mapper_registry
from server.modules.rate import domain
from server.modules.attachments.repository.attachments import Attachment from server.modules.attachments.repository.attachments import Attachment
from server.modules.descriptions.repository.models import Beerds from server.modules.descriptions.repository.models import Beerds
from server.modules.rate import domain
@mapper_registry.mapped @mapper_registry.mapped

View File

@ -1,3 +1,6 @@
from server.modules.recognizer.repository.repository import ARecognizerRepository, RecognizerRepository, ResultBeerds from server.modules.recognizer.repository.repository import (
ARecognizerRepository,
RecognizerRepository,
)
__all__ = ("RecognizerRepository", "ARecognizerRepository", "ResultBeerds") __all__ = ("RecognizerRepository", "ARecognizerRepository")

View File

@ -6,7 +6,6 @@ from sqlalchemy import (
Column, Column,
DateTime, DateTime,
ForeignKeyConstraint, ForeignKeyConstraint,
Integer,
String, String,
) )
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@ -63,4 +62,3 @@ class ResultBeerds(UJsonMixin):
id: str = field(metadata={"sa": Column(String(), primary_key=True, nullable=False)}) id: str = field(metadata={"sa": Column(String(), primary_key=True, nullable=False)})
recognizer_results_id: str = field(metadata={"sa": Column(String(), nullable=False)}) recognizer_results_id: str = field(metadata={"sa": Column(String(), nullable=False)})
beerd_id: str = field(metadata={"sa": Column(String(), nullable=False)}) beerd_id: str = field(metadata={"sa": Column(String(), nullable=False)})
probability: int = field(metadata={"sa": Column(Integer(), nullable=True)})

View File

@ -17,12 +17,6 @@ class ResultWithBeerds:
beerds: list[rm.ResultBeerds] beerds: list[rm.ResultBeerds]
@dataclass
class ResultBeerds:
beerd_id: str
probability: float
class ARecognizerRepository(metaclass=ABCMeta): class ARecognizerRepository(metaclass=ABCMeta):
@abstractmethod @abstractmethod
async def images_dogs(self) -> dict: async def images_dogs(self) -> dict:
@ -45,7 +39,7 @@ class ARecognizerRepository(metaclass=ABCMeta):
"""Получить **все** результаты (кэшируется).""" """Получить **все** результаты (кэшируется)."""
@abstractmethod @abstractmethod
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[ResultBeerds]) -> None: async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[str]) -> None:
""" """
Создать новый результат и сразу же вставить связанные `ResultBeerds`. Создать новый результат и сразу же вставить связанные `ResultBeerds`.
@ -80,7 +74,7 @@ class RecognizerRepository(ARecognizerRepository):
data_labels = f.read() data_labels = f.read()
return ujson.loads(data_labels) return ujson.loads(data_labels)
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[ResultBeerds]) -> None: async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[str]) -> None:
""" """
Создаёт запись в ``recognizer_results`` и сразу же добавляет Создаёт запись в ``recognizer_results`` и сразу же добавляет
одну запись в ``recognizer_results_beerds`` (если передан список одну запись в ``recognizer_results_beerds`` (если передан список
@ -107,8 +101,7 @@ class RecognizerRepository(ARecognizerRepository):
{ {
"id": str(uuid4()), "id": str(uuid4()),
"recognizer_results_id": result.id, "recognizer_results_id": result.id,
"beerd_id": beerd_id.beerd_id, "beerd_id": beerd_id,
"probability": beerd_id.probability,
} }
for beerd_id in beerd_ids for beerd_id in beerd_ids
] ]

View File

@ -14,8 +14,8 @@ import torch
from torchvision import transforms # type: ignore from torchvision import transforms # type: ignore
from server.modules.attachments.domains.attachments import Attachment from server.modules.attachments.domains.attachments import Attachment
from server.modules.descriptions.repository import ACharactersRepository, Breed from server.modules.descriptions.repository import ACharactersRepository
from server.modules.recognizer.repository import ARecognizerRepository, ResultBeerds, models from server.modules.recognizer.repository import ARecognizerRepository, models
TorchModel = NewType("TorchModel", torch.nn.Module) TorchModel = NewType("TorchModel", torch.nn.Module)
@ -47,27 +47,6 @@ class RecognizerResult(UJsonMixin):
images: list images: list
description: dict[str, list] | None description: dict[str, list] | None
uploaded_attach_id: str | None uploaded_attach_id: str | None
share_id: str | None
@dataclass
class SharingBeerds(UJsonMixin):
alias: str
name: str
images: list[str]
probability: float
@dataclass
class SharingResult(UJsonMixin):
beerds: list[SharingBeerds]
attachment_id: str
@dataclass
class ResultNameBeerds:
name: str
probability: float
class RecognizerService: class RecognizerService:
@ -89,47 +68,18 @@ class RecognizerService:
async def images_dogs(self) -> dict: async def images_dogs(self) -> dict:
return await self._repository.images_dogs() return await self._repository.images_dogs()
async def create_result( async def create_result(self, attachment: Attachment, user_id: str, device_id: str, beerd_names: list[str]):
self, attachment: Attachment, user_id: str, device_id: str, beerd_results: list[ResultNameBeerds]
) -> str:
beerd_names = {b.name: b for b in beerd_results}
characters = await self._repository_characters.get_characters() characters = await self._repository_characters.get_characters()
share_id = str(uuid4())
await self._repository.create_result_with_beerds( await self._repository.create_result_with_beerds(
models.Results( models.Results(
id=share_id, id=str(uuid4()),
attachment_id=attachment.id, attachment_id=attachment.id,
user_id=user_id, user_id=user_id,
device_id=device_id, device_id=device_id,
created_at=datetime.now(UTC), created_at=datetime.now(UTC),
), ),
[ [ch.id for ch in characters if ch.name in beerd_names],
ResultBeerds(beerd_id=ch.id, probability=beerd_names[ch.name].probability)
for ch in characters
if ch.name in beerd_names
],
) )
return share_id
async def get_results(self, result_id: str) -> SharingResult:
results = await self._repository.get_results()
beerds_store: dict[str, Breed] = {b.id: b for b in await self._repository_characters.get_characters()}
images_dogs = await self._repository.images_dogs()
beers: list[SharingBeerds] = []
for r in results:
if r.result.id != result_id:
continue
for beerd in r.beerds:
name = beerds_store[beerd.beerd_id].name.replace(" ", "_")
beers.append(
SharingBeerds(
alias=f"/dogs-characteristics/{beerds_store[beerd.beerd_id].alias}",
name=beerds_store[beerd.beerd_id].name,
probability=beerd.probability,
images=[f"/static/assets/dog/{name}/{i}" for i in images_dogs[name]],
)
)
return SharingResult(beerds=beers, attachment_id=r.result.attachment_id)
async def predict_dog_image(self, image: bytes, user_id: str, device_id: str | None) -> RecognizerResult: async def predict_dog_image(self, image: bytes, user_id: str, device_id: str | None) -> RecognizerResult:
if device_id is None: if device_id is None:
@ -151,20 +101,9 @@ class RecognizerService:
) )
description.setdefault(name, []).append(f"/dogs-characteristics/{name.replace(' ', '_')}") description.setdefault(name, []).append(f"/dogs-characteristics/{name.replace(' ', '_')}")
results[probabilities] = name results[probabilities] = name
asyncio.create_task(self.create_result(attachment, user_id, device_id, [results[key] for key in results]))
share_id = await self.create_result(
attachment,
user_id,
device_id,
[ResultNameBeerds(name=results[key], probability=key*100) for key in results],
)
return RecognizerResult( return RecognizerResult(
results=results, results=results, images=images, description=description, uploaded_attach_id=attachment.id
images=images,
description=description,
uploaded_attach_id=attachment.id,
share_id=share_id
) )
async def predict_cat_image(self, image: bytes) -> RecognizerResult: async def predict_cat_image(self, image: bytes) -> RecognizerResult:
@ -183,13 +122,7 @@ class RecognizerService:
) )
) )
results[probabilities] = name results[probabilities] = name
return RecognizerResult( return RecognizerResult(results=results, images=images, description=None, uploaded_attach_id=attachment.id)
results=results,
images=images,
description=None,
uploaded_attach_id=attachment.id,
share_id=None
)
def _predict(self, image: bytes, model, device="cpu") -> list[Any]: def _predict(self, image: bytes, model, device="cpu") -> list[Any]:
img_size = (224, 224) img_size = (224, 224)

View File

@ -16,23 +16,8 @@ async function SavePhoto(self) {
if (response.ok) { if (response.ok) {
let json = await response.json(); let json = await response.json();
currentAttachmentID = json.uploaded_attach_id; currentAttachmentID = json.uploaded_attach_id;
let shareID = json.share_id;
let style = "";
if (!shareID) {
style = "style='display:none'"
}
console.log(style, !shareID);
let text = `
<div class = "title-block">
<h3 class='image-results'>Результаты</h3>
<div id = "share-icon" data-link = "https://порода-по-фото.рф/beerds/dogs/share/${shareID}" ${style}>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M6.58900253,18.0753965 C6.5573092,17.8852365 6.54138127,17.692783 6.54138127,17.5 C6.54138127,15.5670034 8.10838464,14 10.0413813,14 L12.5,14 C12.7761424,14 13,14.2238576 13,14.5 L13,17.7267988 C13,17.8027316 13.0270017,17.8761901 13.0761794,17.9340463 C13.190639,18.0687047 13.3925891,18.085079 13.5272475,17.9706194 L20.5626572,11.9905211 C20.6161112,11.9450852 20.6657995,11.8953969 20.7112354,11.8419429 C21.1762277,11.2948932 21.1097069,10.4744711 20.5626572,10.0094789 L13.5272475,4.02938061 C13.4693913,3.98020285 13.3959328,3.95320119 13.32,3.95320119 C13.1432689,3.95320119 13,4.09647007 13,4.27320119 L13,7.5 C13,7.77614237 12.7761424,8 12.5,8 L9.5,8 C5.91014913,8 3,10.9101491 3,14.5 C3,17.3494045 4.26637093,19.0973664 6.88288761,19.8387069 L6.58900253,18.0753965 Z M10.0413813,15 C8.66066939,15 7.54138127,16.1192881 7.54138127,17.5 C7.54138127,17.6377022 7.55275836,17.7751689 7.57539646,17.9109975 L7.99319696,20.4178005 C8.0506764,20.7626772 7.74549866,21.0585465 7.40256734,20.990415 C3.83673227,20.2819767 2,18.0778979 2,14.5 C2,10.3578644 5.35786438,7 9.5,7 L12,7 L12,4.27320119 C12,3.54418532 12.5909841,2.95320119 13.32,2.95320119 C13.6332228,2.95320119 13.9362392,3.06458305 14.1748959,3.26744129 L21.2103057,9.24753957 C22.1781628,10.0702182 22.2958533,11.5217342 21.4731747,12.4895914 C21.3927882,12.5841638 21.3048781,12.6720739 21.2103057,12.7524604 L14.1748959,18.7325587 C13.6194301,19.2047047 12.7863861,19.1371606 12.3142401,18.5816947 C12.1113819,18.343038 12,18.0400216 12,17.7267988 L12,15 L10.0413813,15 Z"/>
</svg>
</div>
</div>
`;
let text = "<h3 class='image-results'>Результаты</h3>";
let uniqChecker = {}; let uniqChecker = {};
// Функция для создания HTML галереи // Функция для создания HTML галереи
@ -214,10 +199,7 @@ function closeModal() {
/* ────────────────────────────────────────────────────── */ /* ────────────────────────────────────────────────────── */
(() => { (() => {
const ratingContainer = document.querySelector('.star-rating'); const ratingContainer = document.querySelector('.star-rating');
let stars = [] const stars = Array.from(ratingContainer.querySelectorAll('.star'));
if (ratingContainer) {
stars = Array.from(ratingContainer.querySelectorAll('.star'));
}
const feedback = document.getElementById('feedback'); const feedback = document.getElementById('feedback');
let currentRating = 0; // 0 = пока не выбрано let currentRating = 0; // 0 = пока не выбрано
@ -296,35 +278,3 @@ function closeModal() {
// По умолчанию не показываем выбранную оценку // По умолчанию не показываем выбранную оценку
setRating(0); setRating(0);
})(); })();
document.addEventListener('click', function (event) {
const shareBtn = event.target.closest('#share-icon');
if (shareBtn) {
const link = shareBtn.getAttribute('data-link');
if (link) {
navigator.clipboard.writeText(link).then(() => {
showTooltip(shareBtn, 'Скопировано!');
});
}
}
});
function showTooltip(parent, text) {
// Проверяем, не висит ли уже тултип
if (parent.querySelector('.tooltip')) return;
const tooltip = document.createElement('span');
tooltip.className = 'tooltip';
tooltip.textContent = text;
parent.appendChild(tooltip);
// Удаляем тултип через 2 секунды
setTimeout(() => {
tooltip.classList.add('fade-out');
setTimeout(() => tooltip.remove(), 300);
}, 2000);
}

View File

@ -370,44 +370,3 @@ input[type="text"]:hover {
outline: 2px solid #333; outline: 2px solid #333;
outline-offset: 2px; outline-offset: 2px;
} }
.title-block {
display: flex;
justify-content: space-around;
width: 164px;
margin: 0 auto;
}
#share-icon {
position: relative; /* Нужно для позиционирования тултипа */
cursor: pointer;
display: inline-flex;
height: 10px;
padding: 20px 0 0 0;
}
.tooltip {
position: absolute;
bottom: 120%; /* Над иконкой */
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: #fff;
margin: 5px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
animation: fadeIn 0.3s ease;
padding: 0 4px 0 4px;
}
.tooltip.fade-out {
opacity: 0;
transition: opacity 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, 10px); }
to { opacity: 1; transform: translate(-50%, 0); }
}

View File

@ -8,7 +8,7 @@
{% block meta %}{% endblock %} {% block meta %}{% endblock %}
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="/static/styles.css?v=6"> <link rel="stylesheet" href="/static/styles.css?v=5">
<!-- Yandex.Metrika counter --> <!-- Yandex.Metrika counter -->
<script type="text/javascript" > <script type="text/javascript" >
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)}; (function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
@ -45,6 +45,6 @@
{% block form %}{% endblock %} {% block form %}{% endblock %}
</body> </body>
</section> </section>
<script src="/static/scripts.js?v=9"></script> <script src="/static/scripts.js?v=8"></script>
</html> </html>

View File

@ -1,41 +0,0 @@
{% extends "base.html" %}
{% block meta %}
<meta name="description" content="Порода по фото - поделиться результатом" />
{% endblock %}
{% block title %}Результат определение породы по фото{% endblock %}
{% block content %}
<h1>Мой результат определения породы по фото</h1>
{% endblock %}
{% block form %}
<div>
<div id="upload-image">
<div id="upload-image-text">Ваше изображение:</div>
<img id="image" style="max-width: 200px;" src="/attachments{{ attachment.path }}.original.jpg">
</div>
<div id="result">
<div class = "title-block">
<h3 class="image-results">Результаты</h3>
<div id = "share-icon" data-link = "https://порода-по-фото.рф/beerds/dogs/share/{{ share_id }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<path d="M6.58900253,18.0753965 C6.5573092,17.8852365 6.54138127,17.692783 6.54138127,17.5 C6.54138127,15.5670034 8.10838464,14 10.0413813,14 L12.5,14 C12.7761424,14 13,14.2238576 13,14.5 L13,17.7267988 C13,17.8027316 13.0270017,17.8761901 13.0761794,17.9340463 C13.190639,18.0687047 13.3925891,18.085079 13.5272475,17.9706194 L20.5626572,11.9905211 C20.6161112,11.9450852 20.6657995,11.8953969 20.7112354,11.8419429 C21.1762277,11.2948932 21.1097069,10.4744711 20.5626572,10.0094789 L13.5272475,4.02938061 C13.4693913,3.98020285 13.3959328,3.95320119 13.32,3.95320119 C13.1432689,3.95320119 13,4.09647007 13,4.27320119 L13,7.5 C13,7.77614237 12.7761424,8 12.5,8 L9.5,8 C5.91014913,8 3,10.9101491 3,14.5 C3,17.3494045 4.26637093,19.0973664 6.88288761,19.8387069 L6.58900253,18.0753965 Z M10.0413813,15 C8.66066939,15 7.54138127,16.1192881 7.54138127,17.5 C7.54138127,17.6377022 7.55275836,17.7751689 7.57539646,17.9109975 L7.99319696,20.4178005 C8.0506764,20.7626772 7.74549866,21.0585465 7.40256734,20.990415 C3.83673227,20.2819767 2,18.0778979 2,14.5 C2,10.3578644 5.35786438,7 9.5,7 L12,7 L12,4.27320119 C12,3.54418532 12.5909841,2.95320119 13.32,2.95320119 C13.6332228,2.95320119 13.9362392,3.06458305 14.1748959,3.26744129 L21.2103057,9.24753957 C22.1781628,10.0702182 22.2958533,11.5217342 21.4731747,12.4895914 C21.3927882,12.5841638 21.3048781,12.6720739 21.2103057,12.7524604 L14.1748959,18.7325587 C13.6194301,19.2047047 12.7863861,19.1371606 12.3142401,18.5816947 C12.1113819,18.343038 12,18.0400216 12,17.7267988 L12,15 L10.0413813,15 Z"/>
</svg>
</div>
</div>
{% for result in result.beerds %}
<div class="image-block"><div class="image-text">{{ result.name }} (вероятность: {{ result.probability }}%) <br><a href="{{ result.alias }}" target="_blank">Описание и фото</a></div>
<div class="gallery-container">
<div class="main-image-container">
<img src="{{ result.images[0] }}" class="main-image"></div>
<div class="thumbnails" style="display:none;">
{% for image in result.images %}
<img src="{{ image }}" class="thumbnail">
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}