152 lines
5.4 KiB
Python
152 lines
5.4 KiB
Python
from fastapi import APIRouter, File, Form, HTTPException, UploadFile
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
from services.character_card import (
|
|
list_characters,
|
|
get_character,
|
|
import_card_file,
|
|
preview_card_file,
|
|
update_character,
|
|
update_appearance_tags,
|
|
)
|
|
|
|
router = APIRouter(prefix="/characters", tags=["characters"])
|
|
|
|
|
|
class CardPatch(BaseModel):
|
|
name: Optional[str] = None
|
|
description: Optional[str] = None
|
|
personality: Optional[str] = None
|
|
scenario: Optional[str] = None
|
|
first_mes: Optional[str] = None
|
|
mes_example: Optional[str] = None
|
|
appearance_tags: Optional[str] = None
|
|
appearance_prose: Optional[str] = None
|
|
lora_name: Optional[str] = None
|
|
lora_weight: Optional[float] = None
|
|
alternate_greetings_json: Optional[str] = None
|
|
|
|
|
|
@router.get("/")
|
|
async def list_all():
|
|
return await list_characters()
|
|
|
|
|
|
@router.get("/{card_id}")
|
|
async def get_one(card_id: str):
|
|
card = await get_character(card_id)
|
|
if not card:
|
|
raise HTTPException(status_code=404, detail="Карточка не найдена")
|
|
return card
|
|
|
|
|
|
@router.post("/preview")
|
|
async def preview_card(file: UploadFile = File(...)):
|
|
content = await file.read()
|
|
try:
|
|
return await preview_card_file(content, file.filename or "card.json")
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.patch("/{card_id}")
|
|
async def patch_card(card_id: str, body: CardPatch):
|
|
card = await get_character(card_id)
|
|
if not card:
|
|
raise HTTPException(status_code=404, detail="Карточка не найдена")
|
|
fields = {k: v for k, v in body.model_dump().items() if v is not None}
|
|
await update_character(card_id, fields)
|
|
from services.personas import update_persona_appearance
|
|
if "appearance_tags" in fields:
|
|
await update_persona_appearance(f"card_{card_id}", fields["appearance_tags"])
|
|
if {"lora_name", "lora_weight"} & fields.keys():
|
|
from services.personas import update_persona_lora
|
|
await update_persona_lora(f"card_{card_id}", fields.get("lora_name"), fields.get("lora_weight"))
|
|
char_fields = {"name", "description", "personality", "scenario", "first_mes", "mes_example"}
|
|
if char_fields & fields.keys():
|
|
updated = await get_character(card_id)
|
|
from services.character_card import build_system_prompt
|
|
from services.personas import update_persona_prompt
|
|
await update_persona_prompt(f"card_{card_id}", build_system_prompt(updated))
|
|
if "first_mes" in fields or "alternate_greetings_json" in fields:
|
|
from services.personas import patch_persona
|
|
sync = {}
|
|
if "first_mes" in fields:
|
|
sync["first_mes"] = fields["first_mes"]
|
|
if "alternate_greetings_json" in fields:
|
|
sync["alternate_greetings_json"] = fields["alternate_greetings_json"]
|
|
await patch_persona(f"card_{card_id}", sync)
|
|
return await get_character(card_id)
|
|
|
|
|
|
@router.post("/{card_id}/avatar")
|
|
async def upload_avatar(card_id: str, file: UploadFile = File(...)):
|
|
card = await get_character(card_id)
|
|
if not card:
|
|
raise HTTPException(status_code=404, detail="Карточка не найдена")
|
|
content = await file.read()
|
|
if not content.startswith(b"\x89PNG"):
|
|
raise HTTPException(status_code=400, detail="Нужен PNG")
|
|
from services.character_card import _save_avatar_bytes
|
|
rel = _save_avatar_bytes(content, f"card_{card_id}")
|
|
await update_character(card_id, {"avatar_path": rel})
|
|
from services.personas import patch_persona
|
|
await patch_persona(f"card_{card_id}", {"avatar_path": rel})
|
|
return {"avatar_path": f"/static/{rel}"}
|
|
|
|
|
|
@router.post("/import")
|
|
async def import_card(
|
|
file: UploadFile = File(...),
|
|
lora_name: str = Form(""),
|
|
lora_weight: float = Form(0.8),
|
|
card_id: str = Form(""),
|
|
name: str = Form(""),
|
|
description: str = Form(""),
|
|
personality: str = Form(""),
|
|
scenario: str = Form(""),
|
|
first_mes: str = Form(""),
|
|
mes_example: str = Form(""),
|
|
appearance_tags: str = Form(""),
|
|
alternate_greetings_json: str = Form("[]"),
|
|
):
|
|
content = await file.read()
|
|
overrides = {
|
|
"name": name or None,
|
|
"description": description or None,
|
|
"personality": personality or None,
|
|
"scenario": scenario or None,
|
|
"first_mes": first_mes or None,
|
|
"mes_example": mes_example or None,
|
|
"appearance_tags": appearance_tags or None,
|
|
"alternate_greetings_json": alternate_greetings_json or "[]",
|
|
}
|
|
try:
|
|
card = await import_card_file(
|
|
content,
|
|
file.filename or "card.json",
|
|
lora_name=lora_name,
|
|
lora_weight=lora_weight,
|
|
overrides=overrides,
|
|
card_id=card_id.strip() or None,
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
return {
|
|
"status": "imported",
|
|
"card_id": card["card_id"],
|
|
"persona_id": f"card_{card['card_id']}",
|
|
"name": card["name"],
|
|
"alternate_greetings": card.get("alternate_greetings", []),
|
|
}
|
|
|
|
|
|
@router.delete("/{card_id}")
|
|
async def remove_card(card_id: str):
|
|
from services.personas import delete_persona
|
|
|
|
if not await delete_persona(f"card_{card_id}"):
|
|
raise HTTPException(status_code=404, detail="Карточка не найдена")
|
|
return {"status": "deleted", "card_id": card_id}
|