save result definitions
Gitea Actions Demo / build_and_push (push) Successful in 31m34s
Details
Gitea Actions Demo / build_and_push (push) Successful in 31m34s
Details
This commit is contained in:
parent
ee7739b875
commit
877dde5c18
|
|
@ -7,7 +7,6 @@ from server.modules.attachments import AtachmentService
|
|||
|
||||
|
||||
class AtachmentController(Controller):
|
||||
|
||||
@get("/attachments/{raw_path:path}", media_type="image/jpeg")
|
||||
async def get_file(self, raw_path: str) -> Response:
|
||||
attach_service: AtachmentService = inject.instance(AtachmentService)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class DescriptionController(Controller):
|
|||
return Template(
|
||||
template_name="beers-description.html",
|
||||
context={
|
||||
"text": markdown.markdown(breed.description),
|
||||
"text": markdown.markdown(breed.description or ""),
|
||||
"title": breed.name,
|
||||
"images": [f"/static/assets/dog/{name}/{i}" for i in images[name]],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Annotated
|
||||
|
||||
import inject
|
||||
from litestar import (
|
||||
Controller,
|
||||
post,
|
||||
)
|
||||
from litestar import Controller, Request, Response, post
|
||||
from litestar.datastructures import UploadFile
|
||||
from litestar.enums import RequestEncodingType
|
||||
from litestar.params import Body
|
||||
|
|
@ -12,15 +11,37 @@ from litestar.params import Body
|
|||
from server.modules.recognizer import RecognizerService
|
||||
|
||||
|
||||
@dataclass
|
||||
class BeerdsData:
|
||||
f: UploadFile
|
||||
device_id: str
|
||||
|
||||
|
||||
class BreedsController(Controller):
|
||||
path = "/beerds"
|
||||
|
||||
@post("/dogs")
|
||||
async def beerds_dogs(self, data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)]) -> dict:
|
||||
async def beerds_dogs(
|
||||
self,
|
||||
request: Request,
|
||||
data: Annotated[BeerdsData, Body(media_type=RequestEncodingType.MULTI_PART)],
|
||||
) -> Response:
|
||||
user_id = request.cookies.get("user_id")
|
||||
|
||||
# Cookie, которые нужно добавить в ответ (если пользователь новый)
|
||||
new_cookies: dict[str, str] | None = None
|
||||
if not user_id:
|
||||
user_id = str(uuid.uuid4())
|
||||
new_cookies = {"user_id": user_id}
|
||||
|
||||
recognizer_service: RecognizerService = inject.instance(RecognizerService)
|
||||
body = await data.read()
|
||||
result = await recognizer_service.predict_dog_image(body)
|
||||
return result.to_serializable()
|
||||
body = await data.f.read()
|
||||
result = await recognizer_service.predict_dog_image(body, user_id, data.device_id)
|
||||
return Response(
|
||||
content=result.to_serializable(),
|
||||
media_type="application/json",
|
||||
cookies=new_cookies,
|
||||
)
|
||||
|
||||
@post("/cats")
|
||||
async def beerds_cats(self, data: Annotated[UploadFile, Body(media_type=RequestEncodingType.MULTI_PART)]) -> dict:
|
||||
|
|
|
|||
|
|
@ -31,8 +31,9 @@ def inject_config(binder: inject.Binder):
|
|||
db = AsyncDB(cnf)
|
||||
loop.run_until_complete(db.connect())
|
||||
attach_service = AtachmentService(S3StorageDriver(cnf=cnf), DBAttachmentRepository(db))
|
||||
binder.bind(RecognizerService, RecognizerService(RecognizerRepository(), attach_service))
|
||||
binder.bind(CharactersService, CharactersService(PGCharactersRepository(db)))
|
||||
characters_repository = PGCharactersRepository(db)
|
||||
binder.bind(RecognizerService, RecognizerService(RecognizerRepository(db), attach_service, characters_repository))
|
||||
binder.bind(CharactersService, CharactersService(characters_repository))
|
||||
binder.bind(VotesService, VotesService(PGVoteRepository(db)))
|
||||
binder.bind(AtachmentService, attach_service)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from server.infra.db.db_mapper import mapper_registry
|
|||
from server.modules.attachments.repository.models import *
|
||||
from server.modules.descriptions.repository.models import *
|
||||
from server.modules.rate.repository.models import *
|
||||
from server.modules.recognizer.repository.models import *
|
||||
from sqlalchemy import engine_from_config, pool
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
"""ee7739b
|
||||
|
||||
Revision ID: bebaddef3e8d
|
||||
Revises: 474b572b7fe2
|
||||
Create Date: 2026-01-17 13:16:39.809170
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "bebaddef3e8d"
|
||||
down_revision: str | None = "474b572b7fe2"
|
||||
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.create_table(
|
||||
"recognizer_results",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("attachment_id", sa.String(), nullable=False),
|
||||
sa.Column("user_id", sa.String(), nullable=False),
|
||||
sa.Column("device_id", sa.String(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["attachment_id"], ["attachments.id"], name="votes_attachment_id_fk"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"recognizer_results_beerds",
|
||||
sa.Column("id", sa.String(), nullable=False),
|
||||
sa.Column("recognizer_results_id", sa.String(), nullable=False),
|
||||
sa.Column("beerd_id", sa.String(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["beerd_id"], ["beerds.id"], name="recognizer_results_beerd_id_fk"),
|
||||
sa.ForeignKeyConstraint(["recognizer_results_id"], ["recognizer_results.id"], name="recognizer_results_id_fk"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("recognizer_results_beerds")
|
||||
op.drop_table("recognizer_results")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -6,4 +6,4 @@ class Breed:
|
|||
id: str
|
||||
name: str
|
||||
alias: str
|
||||
description: str
|
||||
description: str | None
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class PGCharactersRepository(ACharactersRepository):
|
|||
id=str(row.id),
|
||||
name=row.name.strip(),
|
||||
alias=row.alias.strip(),
|
||||
description=row.descriptions.strip(),
|
||||
description=None,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from dataclasses_ujson.dataclasses_ujson import UJsonMixin # type: ignore
|
||||
from sqlalchemy import (
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKeyConstraint,
|
||||
String,
|
||||
)
|
||||
|
||||
from server.infra.db.db_mapper import mapper_registry
|
||||
|
||||
|
||||
@mapper_registry.mapped
|
||||
@dataclass
|
||||
class Results(UJsonMixin):
|
||||
__sa_dataclass_metadata_key__ = "sa"
|
||||
__tablename__ = "recognizer_results"
|
||||
__table_args__ = (ForeignKeyConstraint(["attachment_id"], ["attachments.id"], "votes_attachment_id_fk"),)
|
||||
|
||||
id: str = field(metadata={"sa": Column(String(), primary_key=True, nullable=False)})
|
||||
attachment_id: str = field(metadata={"sa": Column(String(), nullable=False)})
|
||||
user_id: str = field(metadata={"sa": Column(String(), nullable=False)})
|
||||
device_id: str = field(metadata={"sa": Column(String(), nullable=False)})
|
||||
created_at: datetime = field(
|
||||
default=datetime.now(UTC),
|
||||
metadata={"sa": Column(DateTime(timezone=True), nullable=False)},
|
||||
)
|
||||
|
||||
|
||||
@mapper_registry.mapped
|
||||
@dataclass
|
||||
class ResultBeerds(UJsonMixin):
|
||||
__sa_dataclass_metadata_key__ = "sa"
|
||||
__tablename__ = "recognizer_results_beerds"
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(["recognizer_results_id"], ["recognizer_results.id"], "recognizer_results_id_fk"),
|
||||
ForeignKeyConstraint(["beerd_id"], ["beerds.id"], "recognizer_results_beerd_id_fk"),
|
||||
)
|
||||
|
||||
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)})
|
||||
|
|
@ -1,8 +1,20 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from uuid import uuid4
|
||||
|
||||
import ujson
|
||||
from aiocache import Cache, cached # type: ignore
|
||||
from sqlalchemy import insert, select
|
||||
|
||||
from server.infra.db import AsyncDB
|
||||
from server.modules.recognizer.repository import models as rm
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResultWithBeerds:
|
||||
result: rm.Results
|
||||
beerds: list[rm.ResultBeerds]
|
||||
|
||||
|
||||
class ARecognizerRepository(metaclass=ABCMeta):
|
||||
|
|
@ -22,10 +34,23 @@ class ARecognizerRepository(metaclass=ABCMeta):
|
|||
def labels_cats(self) -> dict:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_results(self) -> list[ResultWithBeerds]:
|
||||
"""Получить **все** результаты (кэшируется)."""
|
||||
|
||||
@abstractmethod
|
||||
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[str]) -> None:
|
||||
"""
|
||||
Создать новый результат и сразу же вставить связанные `ResultBeerds`.
|
||||
|
||||
`beerd_ids` – список id пород, которые должны быть привязаны к
|
||||
результату. Если список пуст, создаётся только результат.
|
||||
"""
|
||||
|
||||
|
||||
class RecognizerRepository(ARecognizerRepository):
|
||||
def __init__(self):
|
||||
pass
|
||||
def __init__(self, db: AsyncDB):
|
||||
self._db = db
|
||||
|
||||
@cached(ttl=60, cache=Cache.MEMORY)
|
||||
async def images_dogs(self) -> dict:
|
||||
|
|
@ -48,3 +73,65 @@ class RecognizerRepository(ARecognizerRepository):
|
|||
with open("server/modules/recognizer/repository/meta/labels_dogs.json") as f: # noqa: ASYNC230
|
||||
data_labels = f.read()
|
||||
return ujson.loads(data_labels)
|
||||
|
||||
async def create_result_with_beerds(self, result: rm.Results, beerd_ids: list[str]) -> None:
|
||||
"""
|
||||
Создаёт запись в ``recognizer_results`` и сразу же добавляет
|
||||
одну запись в ``recognizer_results_beerds`` (если передан список
|
||||
beerd_id) – все в одном `INSERT`‑запросе и одной транзакции.
|
||||
|
||||
При отсутствии `id` у результата генерируется uuid4.
|
||||
"""
|
||||
# --------------------------------------------------------------------
|
||||
# 1️⃣ Подготовим объект результата
|
||||
# --------------------------------------------------------------------
|
||||
if not result.id:
|
||||
result.id = str(uuid4())
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 2️⃣ Откроем транзакцию и добавим результат
|
||||
# --------------------------------------------------------------------
|
||||
async with self._db.async_session() as session:
|
||||
async with session.begin(): # начинается транзакция
|
||||
session.add(result) # INSERT recognizer_results
|
||||
|
||||
# Если есть связанные beerd, делаем один bulk‑INSERT
|
||||
if beerd_ids:
|
||||
values = [
|
||||
{
|
||||
"id": str(uuid4()),
|
||||
"recognizer_results_id": result.id,
|
||||
"beerd_id": beerd_id,
|
||||
}
|
||||
for beerd_id in beerd_ids
|
||||
]
|
||||
# Один INSERT … VALUES (..), (..), …
|
||||
await session.execute(insert(rm.ResultBeerds).values(values))
|
||||
await session.commit() # завершаем транзакцию
|
||||
|
||||
@cached(ttl=60, cache=Cache.MEMORY)
|
||||
async def get_results(self) -> list[ResultWithBeerds]:
|
||||
async with self._db.async_session() as session:
|
||||
# 1️⃣ Получаем все результаты
|
||||
stmt_res = select(rm.Results)
|
||||
res_res = await session.execute(stmt_res)
|
||||
results = res_res.scalars().all()
|
||||
|
||||
if not results:
|
||||
return []
|
||||
|
||||
# 2️⃣ Получаем все beerds, относящиеся к этим результатам
|
||||
res_ids = [r.id for r in results]
|
||||
stmt_beerds = (
|
||||
select(rm.ResultBeerds).where(rm.ResultBeerds.recognizer_results_id.in_(res_ids)) # type:ignore
|
||||
)
|
||||
res_beerds = await session.execute(stmt_beerds)
|
||||
beerds_list = res_beerds.scalars().all()
|
||||
|
||||
# 3️⃣ Формируем карту id → beerds
|
||||
by_res: dict[str, list[rm.ResultBeerds]] = {}
|
||||
for b in beerds_list:
|
||||
by_res.setdefault(b.recognizer_results_id, []).append(b)
|
||||
|
||||
# 4️⃣ Собираем DTO‑ы
|
||||
return [ResultWithBeerds(result=r, beerds=by_res.get(r.id, [])) for r in results]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import asyncio
|
||||
import io
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, NewType, Protocol
|
||||
from uuid import uuid4
|
||||
|
||||
from dataclasses_ujson.dataclasses_ujson import UJsonMixin # type: ignore
|
||||
from PIL import Image
|
||||
|
|
@ -11,7 +14,8 @@ import torch
|
|||
from torchvision import transforms # type: ignore
|
||||
|
||||
from server.modules.attachments.domains.attachments import Attachment
|
||||
from server.modules.recognizer.repository import ARecognizerRepository
|
||||
from server.modules.descriptions.repository import ACharactersRepository
|
||||
from server.modules.recognizer.repository import ARecognizerRepository, models
|
||||
|
||||
TorchModel = NewType("TorchModel", torch.nn.Module)
|
||||
|
||||
|
|
@ -46,11 +50,17 @@ class RecognizerResult(UJsonMixin):
|
|||
|
||||
|
||||
class RecognizerService:
|
||||
__slots__ = ("_repository", "_attachment_service")
|
||||
__slots__ = ("_repository", "_attachment_service", "_repository_characters")
|
||||
|
||||
def __init__(self, repository: ARecognizerRepository, attachment_service: AttachmentService):
|
||||
def __init__(
|
||||
self,
|
||||
repository: ARecognizerRepository,
|
||||
attachment_service: AttachmentService,
|
||||
repository_characters: ACharactersRepository,
|
||||
):
|
||||
self._repository = repository
|
||||
self._attachment_service = attachment_service
|
||||
self._repository_characters = repository_characters
|
||||
|
||||
async def images_cats(self) -> dict:
|
||||
return await self._repository.images_cats()
|
||||
|
|
@ -58,7 +68,22 @@ class RecognizerService:
|
|||
async def images_dogs(self) -> dict:
|
||||
return await self._repository.images_dogs()
|
||||
|
||||
async def predict_dog_image(self, image: bytes) -> RecognizerResult:
|
||||
async def create_result(self, attachment: Attachment, user_id: str, device_id: str, beerd_names: list[str]):
|
||||
characters = await self._repository_characters.get_characters()
|
||||
await self._repository.create_result_with_beerds(
|
||||
models.Results(
|
||||
id=str(uuid4()),
|
||||
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],
|
||||
)
|
||||
|
||||
async def predict_dog_image(self, image: bytes, user_id: str, device_id: str | None) -> RecognizerResult:
|
||||
if device_id is None:
|
||||
device_id = "mobile"
|
||||
attachment = await self._attachment_service.create(image)
|
||||
predicted_data = self._predict(image, DOG_MODEL)
|
||||
results = {}
|
||||
|
|
@ -76,6 +101,7 @@ 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]))
|
||||
return RecognizerResult(
|
||||
results=results, images=images, description=description, uploaded_attach_id=attachment.id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
|
||||
let urlCreator = window.URL || window.webkitURL;
|
||||
|
||||
let current_attachment_id = 0;
|
||||
let current_beerd_name = [];
|
||||
let currentAttachmentID = 0;
|
||||
let currentBeerdName = [];
|
||||
const deviceID = "web";
|
||||
|
||||
async function SavePhoto(self) {
|
||||
document.getElementById("result").innerHTML = "";
|
||||
let photo = document.getElementById("file-input").files[0];
|
||||
let formData = new FormData();
|
||||
formData.append("f", photo);
|
||||
formData.append('device_id', deviceID);
|
||||
let response = await fetch(self.action, { method: "POST", body: formData });
|
||||
|
||||
if (response.ok) {
|
||||
let json = await response.json();
|
||||
current_attachment_id = json.uploaded_attach_id;
|
||||
currentAttachmentID = json.uploaded_attach_id;
|
||||
|
||||
let text = "<h3 class='image-results'>Результаты</h3>";
|
||||
let uniqChecker = {};
|
||||
|
|
@ -37,7 +39,7 @@ async function SavePhoto(self) {
|
|||
|
||||
// Обработка основных результатов
|
||||
for (let key in json.results) {
|
||||
current_beerd_name.push(json.results[key]);
|
||||
currentBeerdName.push(json.results[key]);
|
||||
if (json.description != undefined) {
|
||||
text += `<div class='image-block'><div class='image-text'>${json.results[key]} (вероятность: ${Math.round(parseFloat(key) * 100)}%) <br/><a href="${json.description[json.results[key]]}" target='_blank'>Описание </a></div>`;
|
||||
} else {
|
||||
|
|
@ -218,8 +220,8 @@ function closeModal() {
|
|||
// Подготовьте объект‑payload. Добавьте id‑токены, если нужны.
|
||||
const payload = {
|
||||
rate: value,
|
||||
attachment_id: current_attachment_id,
|
||||
beerd_name: current_beerd_name[0],
|
||||
attachment_id: currentAttachmentID,
|
||||
beerd_name: currentBeerdName[0],
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,6 @@
|
|||
{% block form %}{% endblock %}
|
||||
</body>
|
||||
</section>
|
||||
<script src="/static/scripts.js?v=6"></script>
|
||||
<script src="/static/scripts.js?v=7"></script>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue