M7:形仪与语音——真形锚点、候选定相、声音档案与克隆授权、ASR 接入
- 形仪域 domains/xingyi:form_anchors(稳定辨识特征/年龄表现/许可禁忌/可变范围——生成内容不可静默覆盖锚点);forms(主仪/化仪/文字形仪,candidate→adopt 定相治理:未定相不进呈现位、主仪唯一、新主仪自动退役旧主仪);voice_profiles(三来源,授权克隆必填用途与传播范围,revoke-grant 撤回立即生效,assert-usable 合成前置校验拒绝撤回/过期) - ASR:元炁 SenseVoice /v1/audio/transcriptions 透传(Whisper 兼容 multipart);YUANQI_BASE_URL 未配置时 503 不降级不伪造——多模态关闭纪律 - 前端形仪工坊(人物详情页):真形锚点编辑(特征逐行解析)/候选登记定相废弃/声音档案建立与授权撤回;头像呈现位改走定相主仪(forms/primary);xingyi api 库 + mock + 组件 4 用例 - 退役:旧 avatar 模块(services/endpoints/avatar、avatar_dir 配置、avatar_url 字段、前端直传组件与测试) - 验收(test_m7_xingyi 三用例):未定相不成为正式形仪(含主仪唯一与旧主仪 retired 断言);克隆授权可撤回(缺用途拒/撤回即拒/过期同理/预设不受约束);关闭全部多模态后文字相伴完整可用(ASR 503 时镜树缘契百工照常——回归) - 四套件:后端 376、前端 203、BFF 4、神识 5 包全绿(2 个预存环境失败不变);迁移 008 净库+±1 干净
This commit is contained in:
1 parent
7e23d6fa84
commit
b419e0afde
30 files changed
+1876
-468
No files matched your search
@@ -0,0 +1,157 @@
|
||||
"""形仪 API:真形锚点 / 形仪候选定相 / 声音档案与克隆授权 / ASR 转写。"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, File, UploadFile, status
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.api.v1.deps import get_current_user
|
||||
from app.domains.xingyi import service
|
||||
from app.schemas.auth import CurrentUser
|
||||
|
||||
form_router = APIRouter(prefix="/characters/{character_id}", tags=["xingyi"])
|
||||
|
||||
|
||||
class AnchorUpsertRequest(BaseModel):
|
||||
features: dict = Field(default_factory=dict, description="稳定辨识特征")
|
||||
age_presentation: str = "adult"
|
||||
permissions: dict = Field(default_factory=dict)
|
||||
mutable_scope: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FormCreateRequest(BaseModel):
|
||||
kind: str = "costume"
|
||||
source: str = "upload"
|
||||
description: str = Field(min_length=1, max_length=4000)
|
||||
media_ref: str | None = None
|
||||
live2d: dict | None = None
|
||||
|
||||
|
||||
class VoiceCreateRequest(BaseModel):
|
||||
source: str
|
||||
name: str = Field(min_length=1, max_length=120)
|
||||
params: dict = Field(default_factory=dict)
|
||||
grant_purpose: str | None = None
|
||||
grant_expires_at: datetime | None = None
|
||||
grant_distribution: str | None = None
|
||||
|
||||
|
||||
# ── 真形锚点 ──
|
||||
|
||||
|
||||
@form_router.get("/form-anchor")
|
||||
async def get_anchor(
|
||||
character_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await service.get_anchor(character_id, user.owner_scope)
|
||||
|
||||
|
||||
@form_router.put("/form-anchor")
|
||||
async def upsert_anchor(
|
||||
character_id: str, payload: AnchorUpsertRequest, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await service.upsert_anchor(
|
||||
character_id, user.owner_scope,
|
||||
features=payload.features, age_presentation=payload.age_presentation,
|
||||
permissions=payload.permissions, mutable_scope=payload.mutable_scope,
|
||||
)
|
||||
|
||||
|
||||
# ── 形仪候选与定相 ──
|
||||
|
||||
|
||||
@form_router.get("/forms", status_code=status.HTTP_200_OK)
|
||||
async def list_forms_api(
|
||||
character_id: str, user: CurrentUser = Depends(get_current_user), status_filter: str | None = None
|
||||
) -> list[dict]:
|
||||
return await service.list_forms(character_id, user.owner_scope, status=status_filter)
|
||||
|
||||
|
||||
@form_router.post("/forms", status_code=status.HTTP_201_CREATED)
|
||||
async def create_form_api(
|
||||
character_id: str, payload: FormCreateRequest, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await service.create_form(
|
||||
character_id, user.owner_scope,
|
||||
kind=payload.kind, source=payload.source, description=payload.description,
|
||||
media_ref=payload.media_ref, live2d=payload.live2d,
|
||||
)
|
||||
|
||||
|
||||
@form_router.get("/forms/primary")
|
||||
async def current_primary_api(
|
||||
character_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict | None:
|
||||
return await service.current_primary(character_id, user.owner_scope)
|
||||
|
||||
|
||||
@form_router.post("/forms/{form_id}/adopt")
|
||||
async def adopt_form_api(
|
||||
character_id: str, form_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await service.adopt_form(form_id, user.owner_scope)
|
||||
|
||||
|
||||
@form_router.post("/forms/{form_id}/discard")
|
||||
async def discard_form_api(
|
||||
character_id: str, form_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await service.discard_form(form_id, user.owner_scope)
|
||||
|
||||
|
||||
# ── 声音档案 ──
|
||||
|
||||
|
||||
@form_router.get("/voices")
|
||||
async def list_voices_api(
|
||||
character_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> list[dict]:
|
||||
return await service.list_voices(character_id, user.owner_scope)
|
||||
|
||||
|
||||
@form_router.post("/voices", status_code=status.HTTP_201_CREATED)
|
||||
async def create_voice_api(
|
||||
character_id: str, payload: VoiceCreateRequest, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await service.create_voice(
|
||||
character_id, user.owner_scope,
|
||||
source=payload.source, name=payload.name, params=payload.params,
|
||||
grant_purpose=payload.grant_purpose, grant_expires_at=payload.grant_expires_at,
|
||||
grant_distribution=payload.grant_distribution,
|
||||
)
|
||||
|
||||
|
||||
@form_router.post("/voices/{voice_id}/revoke-grant")
|
||||
async def revoke_voice_grant_api(
|
||||
character_id: str, voice_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
return await service.revoke_voice_grant(voice_id, user.owner_scope)
|
||||
|
||||
|
||||
@form_router.post("/voices/{voice_id}/assert-usable")
|
||||
async def assert_voice_usable_api(
|
||||
character_id: str, voice_id: str, user: CurrentUser = Depends(get_current_user)
|
||||
) -> dict:
|
||||
"""合成前置校验(授权克隆:未撤回且未过期才可用)。"""
|
||||
return await service.assert_voice_usable(voice_id, user.owner_scope)
|
||||
|
||||
|
||||
# ── ASR 转写(元炁 /v1/audio/transcriptions 透传) ──
|
||||
|
||||
|
||||
@form_router.post("/voice/transcriptions")
|
||||
async def transcribe_audio_api(
|
||||
character_id: str,
|
||||
user: CurrentUser = Depends(get_current_user),
|
||||
audio: UploadFile = File(...),
|
||||
) -> dict:
|
||||
"""语音转文本(元炁 SenseVoice)。转写文本由前端作为镜树问节内容提交。
|
||||
|
||||
多模态关闭(YUANQI 未启用)时本端点 503——文字相伴链路不受影响(回归由
|
||||
测试钉死:关闭全部多模态后镜树/缘契照常)。
|
||||
"""
|
||||
from app.domains.xingyi.asr import transcribe
|
||||
|
||||
await service._owned(character_id, user.owner_scope)
|
||||
content = await audio.read()
|
||||
return await transcribe(content, audio.filename or "audio.webm")
|
||||
Reference in new issue
Block a user