mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
158 lines
6.1 KiB
Python
158 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
from functools import lru_cache
|
|
from typing import Any
|
|
|
|
from app.core.config import get_settings
|
|
from app.core.i18n import _load_properties, message_for, resolve_language
|
|
from app.core.ids import snowflake
|
|
from app.core.redis import JavaRedisCodec, get_redis
|
|
from app.core.security import AuthUser, shanghai_now_naive
|
|
from app.repositories.timbre import TimbreRepository
|
|
from app.schemas.timbre import TimbreBody
|
|
|
|
|
|
def _details(row: dict[str, Any]) -> dict[str, Any]:
|
|
return {
|
|
"id": row.get("id"),
|
|
"languages": row.get("languages"),
|
|
"name": row.get("name"),
|
|
"remark": row.get("remark"),
|
|
"referenceAudio": row.get("reference_audio"),
|
|
"referenceText": row.get("reference_text"),
|
|
# TimbreDetailsVO.sort is primitive long, whose Java serializer always
|
|
# emits a string and whose null conversion default is zero.
|
|
"sort": str(row.get("sort") if row.get("sort") is not None else 0),
|
|
"ttsModelId": row.get("tts_model_id"),
|
|
"ttsVoice": row.get("tts_voice"),
|
|
"voiceDemo": row.get("voice_demo"),
|
|
}
|
|
|
|
|
|
class TimbreService:
|
|
def __init__(self, repository: TimbreRepository):
|
|
self.repository = repository
|
|
|
|
@staticmethod
|
|
def _validate(body: TimbreBody, language: str | None) -> None:
|
|
from app.core.errors import AppError
|
|
|
|
for value, message in (
|
|
(body.languages, "timbre.languages.require"),
|
|
(body.name, "timbre.name.require"),
|
|
(body.tts_model_id, "timbre.ttsModelId.require"),
|
|
(body.tts_voice, "timbre.ttsVoice.require"),
|
|
):
|
|
if value is None or not value.strip():
|
|
# TimbreController invokes ValidatorUtils directly. That
|
|
# utility wraps validation text in RenException(String), whose
|
|
# response code is 500 rather than the global @Valid code 10034.
|
|
raise AppError(500, _validation_message(message, language))
|
|
if body.sort is not None and body.sort < 0:
|
|
raise AppError(500, _validation_message("sort.number", language))
|
|
|
|
async def page(
|
|
self,
|
|
tts_model_id: str | None,
|
|
name: str | None,
|
|
page: str | None,
|
|
limit: str | None,
|
|
language: str | None,
|
|
) -> dict[str, Any]:
|
|
if tts_model_id is None or not tts_model_id.strip():
|
|
from app.core.errors import AppError
|
|
|
|
raise AppError(500, _validation_message("timbre.ttsModelId.require", language))
|
|
current, size = max(int(page or "1"), 1), int(limit or "10")
|
|
rows, total = await self.repository.page(
|
|
tts_model_id=tts_model_id, name=name, offset=(current - 1) * size, limit=size
|
|
)
|
|
return {"total": total, "list": [_details(row) for row in rows]}
|
|
|
|
async def save(self, body: TimbreBody, user: AuthUser, language: str | None) -> None:
|
|
self._validate(body, language)
|
|
values = self._values(body, user, str(snowflake.next_id()))
|
|
async with self.repository.session.begin():
|
|
await self.repository.insert(values)
|
|
|
|
async def update(
|
|
self, timbre_id: str, body: TimbreBody, user: AuthUser, language: str | None
|
|
) -> None:
|
|
self._validate(body, language)
|
|
values = self._values(body, user, timbre_id)
|
|
async with self.repository.session.begin():
|
|
await self.repository.update(values)
|
|
await get_redis().delete(f"timbre:details:{timbre_id}")
|
|
|
|
async def delete(self, ids: list[str]) -> None:
|
|
async with self.repository.session.begin():
|
|
await self.repository.delete(ids)
|
|
|
|
async def voices(self, model_id: str, voice_name: str | None, user: AuthUser, language: str | None) -> Any:
|
|
normal, clones = await self.repository.voices(model_id, voice_name, user.id)
|
|
values = [
|
|
{
|
|
"id": row.get("id"),
|
|
"name": row.get("name"),
|
|
"voiceDemo": row.get("voice_demo"),
|
|
"languages": row.get("languages"),
|
|
"isClone": False,
|
|
}
|
|
for row in normal
|
|
]
|
|
prefix = message_for(10158, language)
|
|
redis = get_redis()
|
|
for row in clones:
|
|
name = prefix + str(row.get("name") or "")
|
|
voice = {
|
|
"id": row.get("id"),
|
|
"name": name,
|
|
"voiceDemo": row.get("voice_demo"),
|
|
"languages": row.get("languages"),
|
|
"isClone": True,
|
|
}
|
|
await redis.set(f"timbre:name:{row['id']}", JavaRedisCodec.encode(name))
|
|
values.insert(0, voice)
|
|
return values or None
|
|
|
|
@staticmethod
|
|
def _values(body: TimbreBody, user: AuthUser, timbre_id: str) -> dict[str, Any]:
|
|
assert body.languages is not None
|
|
assert body.name is not None
|
|
assert body.tts_model_id is not None
|
|
assert body.tts_voice is not None
|
|
return {
|
|
"id": timbre_id,
|
|
"languages": body.languages,
|
|
"name": body.name,
|
|
"remark": body.remark,
|
|
"reference_audio": body.reference_audio,
|
|
"reference_text": body.reference_text,
|
|
"sort": body.sort if body.sort is not None else 0,
|
|
"tts_model_id": body.tts_model_id,
|
|
"tts_voice": body.tts_voice,
|
|
"voice_demo": body.voice_demo,
|
|
"creator": user.id,
|
|
"updater": user.id,
|
|
"now": shanghai_now_naive(),
|
|
}
|
|
|
|
|
|
_VALIDATION_FILES = {
|
|
"zh-CN": "validation_zh_CN.properties",
|
|
"zh-TW": "validation_zh_TW.properties",
|
|
"en-US": "validation_en_US.properties",
|
|
"de-DE": "validation_de_DE.properties",
|
|
"vi-VN": "validation_vi_VN.properties",
|
|
"pt-BR": "validation_pt_BR.properties",
|
|
}
|
|
|
|
|
|
@lru_cache(maxsize=64)
|
|
def _validation_message(key: str, accept_language: str | None) -> str:
|
|
language = resolve_language(accept_language)
|
|
directory = get_settings().i18n_dir
|
|
messages = _load_properties(directory / "validation.properties")
|
|
messages.update(_load_properties(directory / _VALIDATION_FILES[language]))
|
|
return messages.get(key, key)
|