feat: add waveform voice messages end-to-end
All checks were successful
CI / test (push) Successful in 23s
All checks were successful
CI / test (push) Successful in 23s
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user