Compare commits
3 Commits
6a1bbb9a08
...
33215d33cb
| Author | SHA1 | Date |
|---|---|---|
|
|
33215d33cb | |
|
|
90331a7036 | |
|
|
3baab00f80 |
|
|
@ -1 +1 @@
|
|||
cpython-3.13.5-linux-x86_64-gnu
|
||||
cpython-3.13.12-linux-x86_64-gnu
|
||||
2
Makefile
2
Makefile
|
|
@ -14,7 +14,7 @@ format:
|
|||
uv run ruff format
|
||||
|
||||
lint:
|
||||
uv run mypy ./ --explicit-package-bases;
|
||||
uv run mypy ./server --explicit-package-bases;
|
||||
ruff check --fix
|
||||
|
||||
pipinstall:
|
||||
|
|
|
|||
|
|
@ -35,4 +35,5 @@ class ResultsView(ModelView):
|
|||
"result",
|
||||
"path",
|
||||
"beerd",
|
||||
"probability"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,11 +3,13 @@ from dataclasses import dataclass
|
|||
from typing import Annotated
|
||||
|
||||
import inject
|
||||
from litestar import Controller, Request, Response, post
|
||||
from litestar import Controller, Request, Response, get, post
|
||||
from litestar.datastructures import UploadFile
|
||||
from litestar.enums import RequestEncodingType
|
||||
from litestar.params import Body
|
||||
from litestar.response import Template
|
||||
|
||||
from server.modules.attachments import AtachmentService
|
||||
from server.modules.recognizer import RecognizerService
|
||||
|
||||
|
||||
|
|
@ -49,3 +51,18 @@ class BreedsController(Controller):
|
|||
body = await data.read()
|
||||
result = await recognizer_service.predict_cat_image(body)
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
"""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 ###
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
from server.modules.descriptions.repository.repository import (
|
||||
ACharactersRepository,
|
||||
Breed,
|
||||
CharactersRepository,
|
||||
PGCharactersRepository,
|
||||
)
|
||||
|
||||
__all__ = ("CharactersRepository", "ACharactersRepository", "PGCharactersRepository")
|
||||
__all__ = ("CharactersRepository", "ACharactersRepository", "PGCharactersRepository", "Breed")
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ from sqlalchemy.orm import relationship
|
|||
|
||||
from server.config import get_app_config
|
||||
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.descriptions.repository.models import Beerds
|
||||
from server.modules.rate import domain
|
||||
|
||||
|
||||
@mapper_registry.mapped
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
from server.modules.recognizer.repository.repository import (
|
||||
ARecognizerRepository,
|
||||
RecognizerRepository,
|
||||
)
|
||||
from server.modules.recognizer.repository.repository import ARecognizerRepository, RecognizerRepository, ResultBeerds
|
||||
|
||||
__all__ = ("RecognizerRepository", "ARecognizerRepository")
|
||||
__all__ = ("RecognizerRepository", "ARecognizerRepository", "ResultBeerds")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from sqlalchemy import (
|
|||
Column,
|
||||
DateTime,
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
|
|
@ -62,3 +63,4 @@ class ResultBeerds(UJsonMixin):
|
|||
id: str = field(metadata={"sa": Column(String(), primary_key=True, nullable=False)})
|
||||
recognizer_results_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)})
|
||||
|
|
|
|||
|
|
@ -17,6 +17,12 @@ class ResultWithBeerds:
|
|||
beerds: list[rm.ResultBeerds]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResultBeerds:
|
||||
beerd_id: str
|
||||
probability: float
|
||||
|
||||
|
||||
class ARecognizerRepository(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
async def images_dogs(self) -> dict:
|
||||
|
|
@ -39,7 +45,7 @@ class ARecognizerRepository(metaclass=ABCMeta):
|
|||
"""Получить **все** результаты (кэшируется)."""
|
||||
|
||||
@abstractmethod
|
||||
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[str]) -> None:
|
||||
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[ResultBeerds]) -> None:
|
||||
"""
|
||||
Создать новый результат и сразу же вставить связанные `ResultBeerds`.
|
||||
|
||||
|
|
@ -74,7 +80,7 @@ class RecognizerRepository(ARecognizerRepository):
|
|||
data_labels = f.read()
|
||||
return ujson.loads(data_labels)
|
||||
|
||||
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[str]) -> None:
|
||||
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[ResultBeerds]) -> None:
|
||||
"""
|
||||
Создаёт запись в ``recognizer_results`` и сразу же добавляет
|
||||
одну запись в ``recognizer_results_beerds`` (если передан список
|
||||
|
|
@ -101,7 +107,8 @@ class RecognizerRepository(ARecognizerRepository):
|
|||
{
|
||||
"id": str(uuid4()),
|
||||
"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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import torch
|
|||
from torchvision import transforms # type: ignore
|
||||
|
||||
from server.modules.attachments.domains.attachments import Attachment
|
||||
from server.modules.descriptions.repository import ACharactersRepository
|
||||
from server.modules.recognizer.repository import ARecognizerRepository, models
|
||||
from server.modules.descriptions.repository import ACharactersRepository, Breed
|
||||
from server.modules.recognizer.repository import ARecognizerRepository, ResultBeerds, models
|
||||
|
||||
TorchModel = NewType("TorchModel", torch.nn.Module)
|
||||
|
||||
|
|
@ -47,6 +47,27 @@ class RecognizerResult(UJsonMixin):
|
|||
images: list
|
||||
description: dict[str, list] | 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:
|
||||
|
|
@ -68,18 +89,47 @@ class RecognizerService:
|
|||
async def images_dogs(self) -> dict:
|
||||
return await self._repository.images_dogs()
|
||||
|
||||
async def create_result(self, attachment: Attachment, user_id: str, device_id: str, beerd_names: list[str]):
|
||||
async def create_result(
|
||||
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()
|
||||
share_id = str(uuid4())
|
||||
await self._repository.create_result_with_beerds(
|
||||
models.Results(
|
||||
id=str(uuid4()),
|
||||
id=share_id,
|
||||
attachment_id=attachment.id,
|
||||
user_id=user_id,
|
||||
device_id=device_id,
|
||||
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:
|
||||
if device_id is None:
|
||||
|
|
@ -101,9 +151,20 @@ class RecognizerService:
|
|||
)
|
||||
description.setdefault(name, []).append(f"/dogs-characteristics/{name.replace(' ', '_')}")
|
||||
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(
|
||||
results=results, images=images, description=description, uploaded_attach_id=attachment.id
|
||||
results=results,
|
||||
images=images,
|
||||
description=description,
|
||||
uploaded_attach_id=attachment.id,
|
||||
share_id=share_id
|
||||
)
|
||||
|
||||
async def predict_cat_image(self, image: bytes) -> RecognizerResult:
|
||||
|
|
@ -122,7 +183,13 @@ class RecognizerService:
|
|||
)
|
||||
)
|
||||
results[probabilities] = name
|
||||
return RecognizerResult(results=results, images=images, description=None, uploaded_attach_id=attachment.id)
|
||||
return RecognizerResult(
|
||||
results=results,
|
||||
images=images,
|
||||
description=None,
|
||||
uploaded_attach_id=attachment.id,
|
||||
share_id=None
|
||||
)
|
||||
|
||||
def _predict(self, image: bytes, model, device="cpu") -> list[Any]:
|
||||
img_size = (224, 224)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,23 @@ async function SavePhoto(self) {
|
|||
if (response.ok) {
|
||||
let json = await response.json();
|
||||
currentAttachmentID = json.uploaded_attach_id;
|
||||
|
||||
let text = "<h3 class='image-results'>Результаты</h3>";
|
||||
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 uniqChecker = {};
|
||||
|
||||
// Функция для создания HTML галереи
|
||||
|
|
@ -199,7 +214,10 @@ function closeModal() {
|
|||
/* ────────────────────────────────────────────────────── */
|
||||
(() => {
|
||||
const ratingContainer = document.querySelector('.star-rating');
|
||||
const stars = Array.from(ratingContainer.querySelectorAll('.star'));
|
||||
let stars = []
|
||||
if (ratingContainer) {
|
||||
stars = Array.from(ratingContainer.querySelectorAll('.star'));
|
||||
}
|
||||
const feedback = document.getElementById('feedback');
|
||||
|
||||
let currentRating = 0; // 0 = пока не выбрано
|
||||
|
|
@ -277,4 +295,36 @@ function closeModal() {
|
|||
|
||||
// По умолчанию не показываем выбранную оценку
|
||||
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);
|
||||
}
|
||||
|
|
@ -369,4 +369,45 @@ input[type="text"]:hover {
|
|||
.star:focus-visible { /* обводка для клавиатурного фокуса */
|
||||
outline: 2px solid #333;
|
||||
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); }
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
{% block meta %}{% endblock %}
|
||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/styles.css?v=5">
|
||||
<link rel="stylesheet" href="/static/styles.css?v=6">
|
||||
<!-- Yandex.Metrika counter -->
|
||||
<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)};
|
||||
|
|
@ -45,6 +45,6 @@
|
|||
{% block form %}{% endblock %}
|
||||
</body>
|
||||
</section>
|
||||
<script src="/static/scripts.js?v=8"></script>
|
||||
<script src="/static/scripts.js?v=9"></script>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
{% 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 %}
|
||||
Loading…
Reference in New Issue