feat: add waveform voice messages end-to-end
All checks were successful
CI / test (push) Successful in 23s

This commit is contained in:
2026-03-08 12:40:49 +03:00
parent 3b82b5e558
commit 30169a3a27
13 changed files with 361 additions and 19 deletions

View File

@@ -1,4 +1,4 @@
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy import ForeignKey, Integer, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database.base import Base
@@ -12,5 +12,6 @@ class Attachment(Base):
file_url: Mapped[str] = mapped_column(String(1024), nullable=False)
file_type: Mapped[str] = mapped_column(String(64), nullable=False)
file_size: Mapped[int] = mapped_column(Integer, nullable=False)
waveform_data: Mapped[str | None] = mapped_column(Text, nullable=True)
message = relationship("Message", back_populates="attachments")

View File

@@ -12,12 +12,14 @@ async def create_attachment(
file_url: str,
file_type: str,
file_size: int,
waveform_data: str | None = None,
) -> Attachment:
attachment = Attachment(
message_id=message_id,
file_url=file_url,
file_type=file_type,
file_size=file_size,
waveform_data=waveform_data,
)
db.add(attachment)
await db.flush()
@@ -46,3 +48,15 @@ async def list_chat_attachments(
stmt = stmt.where(Attachment.id < before_id)
result = await db.execute(stmt)
return [(row[0], row[1]) for row in result.all()]
async def list_attachments_by_message_ids(
db: AsyncSession,
*,
message_ids: list[int],
) -> list[Attachment]:
if not message_ids:
return []
stmt = select(Attachment).where(Attachment.message_id.in_(message_ids))
result = await db.execute(stmt)
return list(result.scalars().all())

View File

@@ -21,6 +21,7 @@ class AttachmentCreateRequest(BaseModel):
file_url: str = Field(min_length=1, max_length=1024)
file_type: str = Field(min_length=1, max_length=64)
file_size: int = Field(gt=0)
waveform_points: list[int] | None = Field(default=None, min_length=8, max_length=256)
class AttachmentRead(BaseModel):
@@ -31,6 +32,7 @@ class AttachmentRead(BaseModel):
file_url: str
file_type: str
file_size: int
waveform_points: list[int] | None = None
class ChatAttachmentRead(BaseModel):
@@ -42,3 +44,4 @@ class ChatAttachmentRead(BaseModel):
file_url: str
file_type: str
file_size: int
waveform_points: list[int] | None = None

View File

@@ -1,4 +1,5 @@
import mimetypes
import json
from urllib.parse import quote
from uuid import uuid4
@@ -29,6 +30,35 @@ ALLOWED_MIME_TYPES = {
"text/plain",
}
def _normalize_waveform(points: list[int] | None) -> list[int] | None:
if points is None:
return None
normalized = [max(0, min(31, int(value))) for value in points]
if len(normalized) < 8:
return None
return normalized
def _decode_waveform(raw: str | None) -> list[int] | None:
if not raw:
return None
try:
parsed = json.loads(raw)
except json.JSONDecodeError:
return None
if not isinstance(parsed, list):
return None
result: list[int] = []
for value in parsed[:256]:
if isinstance(value, int):
result.append(max(0, min(31, value)))
elif isinstance(value, float):
result.append(max(0, min(31, int(value))))
else:
return None
return result or None
def _normalize_mime(file_type: str) -> str:
return file_type.split(";", maxsplit=1)[0].strip().lower()
@@ -125,16 +155,27 @@ async def store_attachment_metadata(
detail="Only the message sender can attach files",
)
normalized_waveform = _normalize_waveform(payload.waveform_points)
attachment = await repository.create_attachment(
db,
message_id=payload.message_id,
file_url=payload.file_url,
file_type=payload.file_type,
file_size=payload.file_size,
waveform_data=json.dumps(normalized_waveform, ensure_ascii=True) if normalized_waveform else None,
)
await db.commit()
await db.refresh(attachment)
return AttachmentRead.model_validate(attachment)
return AttachmentRead.model_validate(
{
"id": attachment.id,
"message_id": attachment.message_id,
"file_url": attachment.file_url,
"file_type": attachment.file_type,
"file_size": attachment.file_size,
"waveform_points": _decode_waveform(attachment.waveform_data),
}
)
async def list_attachments_for_chat(
@@ -162,6 +203,7 @@ async def list_attachments_for_chat(
file_url=attachment.file_url,
file_type=attachment.file_type,
file_size=attachment.file_size,
waveform_points=_decode_waveform(attachment.waveform_data),
)
for attachment, message in rows
]