refactor: rename canto-backend → backend, canto-frontend → frontend
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
0
backend/db/__init__.py
Normal file
0
backend/db/__init__.py
Normal file
769
backend/db/crud.py
Normal file
769
backend/db/crud.py
Normal file
@@ -0,0 +1,769 @@
|
||||
import json
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from db.models import User, Job, VoiceCache, SystemSettings, VoiceDesign, AudiobookProject, AudiobookChapter, AudiobookCharacter, AudiobookSegment, UsageLog
|
||||
|
||||
def get_user_by_username(db: Session, username: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.username == username).first()
|
||||
|
||||
def get_user_by_email(db: Session, email: str) -> Optional[User]:
|
||||
return db.query(User).filter(User.email == email).first()
|
||||
|
||||
def count_users(db: Session) -> int:
|
||||
return db.query(User).count()
|
||||
|
||||
def create_user(db: Session, username: str, email: str, hashed_password: str) -> User:
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
hashed_password=hashed_password
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
def create_user_by_admin(
|
||||
db: Session,
|
||||
username: str,
|
||||
email: str,
|
||||
hashed_password: str,
|
||||
is_superuser: bool = False,
|
||||
can_use_local_model: bool = False,
|
||||
can_use_nsfw: bool = False
|
||||
) -> User:
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
hashed_password=hashed_password,
|
||||
is_superuser=is_superuser,
|
||||
can_use_local_model=can_use_local_model,
|
||||
can_use_nsfw=can_use_nsfw
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
def get_user_by_id(db: Session, user_id: int) -> Optional[User]:
|
||||
return db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
def list_users(db: Session, skip: int = 0, limit: int = 100) -> tuple[List[User], int]:
|
||||
total = db.query(User).count()
|
||||
users = db.query(User).order_by(User.created_at.desc()).offset(skip).limit(limit).all()
|
||||
return users, total
|
||||
|
||||
def update_user(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
username: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
hashed_password: Optional[str] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
is_superuser: Optional[bool] = None,
|
||||
can_use_local_model: Optional[bool] = None,
|
||||
can_use_nsfw: Optional[bool] = None
|
||||
) -> Optional[User]:
|
||||
user = get_user_by_id(db, user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
if username is not None:
|
||||
user.username = username
|
||||
if email is not None:
|
||||
user.email = email
|
||||
if hashed_password is not None:
|
||||
user.hashed_password = hashed_password
|
||||
if is_active is not None:
|
||||
user.is_active = is_active
|
||||
if is_superuser is not None:
|
||||
user.is_superuser = is_superuser
|
||||
if can_use_local_model is not None:
|
||||
user.can_use_local_model = can_use_local_model
|
||||
if can_use_nsfw is not None:
|
||||
user.can_use_nsfw = can_use_nsfw
|
||||
|
||||
user.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
def delete_user(db: Session, user_id: int) -> bool:
|
||||
user = get_user_by_id(db, user_id)
|
||||
if not user:
|
||||
return False
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def change_user_password(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
new_hashed_password: str
|
||||
) -> Optional[User]:
|
||||
user = get_user_by_id(db, user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
user.hashed_password = new_hashed_password
|
||||
user.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
def create_job(db: Session, user_id: int, job_type: str, input_data: Dict[str, Any]) -> Job:
|
||||
job = Job(
|
||||
user_id=user_id,
|
||||
job_type=job_type,
|
||||
input_data=json.dumps(input_data),
|
||||
status="pending"
|
||||
)
|
||||
db.add(job)
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
return job
|
||||
|
||||
def get_job(db: Session, job_id: int, user_id: int) -> Optional[Job]:
|
||||
return db.query(Job).filter(Job.id == job_id, Job.user_id == user_id).first()
|
||||
|
||||
def list_jobs(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
status: Optional[str] = None
|
||||
) -> List[Job]:
|
||||
query = db.query(Job).filter(Job.user_id == user_id)
|
||||
if status:
|
||||
query = query.filter(Job.status == status)
|
||||
return query.order_by(Job.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
def update_job_status(
|
||||
db: Session,
|
||||
job_id: int,
|
||||
user_id: int,
|
||||
status: str,
|
||||
output_path: Optional[str] = None,
|
||||
error_message: Optional[str] = None
|
||||
) -> Optional[Job]:
|
||||
job = get_job(db, job_id, user_id)
|
||||
if not job:
|
||||
return None
|
||||
|
||||
job.status = status
|
||||
if output_path:
|
||||
job.output_path = output_path
|
||||
if error_message:
|
||||
job.error_message = error_message
|
||||
if status in ["completed", "failed"]:
|
||||
job.completed_at = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
db.refresh(job)
|
||||
return job
|
||||
|
||||
def delete_job(db: Session, job_id: int, user_id: int) -> bool:
|
||||
job = get_job(db, job_id, user_id)
|
||||
if not job:
|
||||
return False
|
||||
db.delete(job)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def create_cache_entry(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
ref_audio_hash: str,
|
||||
cache_path: str,
|
||||
meta_data: Optional[Dict[str, Any]] = None
|
||||
) -> VoiceCache:
|
||||
cache = VoiceCache(
|
||||
user_id=user_id,
|
||||
ref_audio_hash=ref_audio_hash,
|
||||
cache_path=cache_path,
|
||||
meta_data=json.dumps(meta_data) if meta_data else None
|
||||
)
|
||||
db.add(cache)
|
||||
db.commit()
|
||||
db.refresh(cache)
|
||||
return cache
|
||||
|
||||
def get_cache_entry(db: Session, user_id: int, ref_audio_hash: str) -> Optional[VoiceCache]:
|
||||
cache = db.query(VoiceCache).filter(
|
||||
VoiceCache.user_id == user_id,
|
||||
VoiceCache.ref_audio_hash == ref_audio_hash
|
||||
).first()
|
||||
|
||||
if cache:
|
||||
cache.last_accessed = datetime.utcnow()
|
||||
cache.access_count += 1
|
||||
db.commit()
|
||||
db.refresh(cache)
|
||||
|
||||
return cache
|
||||
|
||||
def list_cache_entries(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[VoiceCache]:
|
||||
return db.query(VoiceCache).filter(
|
||||
VoiceCache.user_id == user_id
|
||||
).order_by(VoiceCache.last_accessed.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
def delete_cache_entry(db: Session, cache_id: int, user_id: int) -> bool:
|
||||
cache = db.query(VoiceCache).filter(
|
||||
VoiceCache.id == cache_id,
|
||||
VoiceCache.user_id == user_id
|
||||
).first()
|
||||
if not cache:
|
||||
return False
|
||||
db.delete(cache)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def get_user_preferences(db: Session, user_id: int) -> dict:
|
||||
user = get_user_by_id(db, user_id)
|
||||
if not user or not user.user_preferences:
|
||||
return {"default_backend": "local", "onboarding_completed": False}
|
||||
prefs = dict(user.user_preferences)
|
||||
if prefs.get("default_backend") == "aliyun":
|
||||
prefs["default_backend"] = "local"
|
||||
return prefs
|
||||
|
||||
def update_user_preferences(db: Session, user_id: int, preferences: dict) -> Optional[User]:
|
||||
user = get_user_by_id(db, user_id)
|
||||
if not user:
|
||||
return None
|
||||
user.user_preferences = preferences
|
||||
user.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
def get_system_setting(db: Session, key: str) -> Optional[dict]:
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
if not setting:
|
||||
return None
|
||||
return setting.value
|
||||
|
||||
def update_system_setting(db: Session, key: str, value: dict) -> SystemSettings:
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
if setting:
|
||||
setting.value = value
|
||||
setting.updated_at = datetime.utcnow()
|
||||
else:
|
||||
setting = SystemSettings(key=key, value=value, updated_at=datetime.utcnow())
|
||||
db.add(setting)
|
||||
db.commit()
|
||||
db.refresh(setting)
|
||||
return setting
|
||||
|
||||
def can_user_use_local_model(user: User) -> bool:
|
||||
return True
|
||||
|
||||
def can_user_use_nsfw(user: User) -> bool:
|
||||
return user.is_superuser or user.can_use_nsfw
|
||||
|
||||
def create_voice_design(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
name: str,
|
||||
instruct: str,
|
||||
meta_data: Optional[Dict[str, Any]] = None,
|
||||
preview_text: Optional[str] = None,
|
||||
voice_cache_id: Optional[int] = None,
|
||||
ref_audio_path: Optional[str] = None,
|
||||
ref_text: Optional[str] = None,
|
||||
) -> VoiceDesign:
|
||||
design = VoiceDesign(
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
instruct=instruct,
|
||||
meta_data=meta_data,
|
||||
preview_text=preview_text,
|
||||
voice_cache_id=voice_cache_id,
|
||||
ref_audio_path=ref_audio_path,
|
||||
ref_text=ref_text,
|
||||
created_at=datetime.utcnow(),
|
||||
last_used=datetime.utcnow()
|
||||
)
|
||||
db.add(design)
|
||||
db.commit()
|
||||
db.refresh(design)
|
||||
return design
|
||||
|
||||
def get_voice_design(db: Session, design_id: int, user_id: int) -> Optional[VoiceDesign]:
|
||||
return db.query(VoiceDesign).filter(
|
||||
VoiceDesign.id == design_id,
|
||||
VoiceDesign.user_id == user_id,
|
||||
VoiceDesign.is_active == True
|
||||
).first()
|
||||
|
||||
def list_voice_designs(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
backend_type: Optional[str] = None,
|
||||
skip: int = 0,
|
||||
limit: int = 100
|
||||
) -> List[VoiceDesign]:
|
||||
query = db.query(VoiceDesign).filter(
|
||||
VoiceDesign.user_id == user_id,
|
||||
VoiceDesign.is_active == True
|
||||
)
|
||||
return query.order_by(VoiceDesign.last_used.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
def count_voice_designs(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
backend_type: Optional[str] = None
|
||||
) -> int:
|
||||
return db.query(VoiceDesign).filter(
|
||||
VoiceDesign.user_id == user_id,
|
||||
VoiceDesign.is_active == True
|
||||
).count()
|
||||
|
||||
def delete_voice_design(db: Session, design_id: int, user_id: int) -> bool:
|
||||
design = get_voice_design(db, design_id, user_id)
|
||||
if not design:
|
||||
return False
|
||||
db.delete(design)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def update_voice_design_usage(db: Session, design_id: int, user_id: int) -> Optional[VoiceDesign]:
|
||||
design = get_voice_design(db, design_id, user_id)
|
||||
if design:
|
||||
design.last_used = datetime.utcnow()
|
||||
design.use_count += 1
|
||||
db.commit()
|
||||
db.refresh(design)
|
||||
return design
|
||||
|
||||
|
||||
def update_user_llm_config(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
llm_api_key: Optional[str] = None,
|
||||
llm_base_url: Optional[str] = None,
|
||||
llm_model: Optional[str] = None,
|
||||
clear: bool = False
|
||||
) -> Optional[User]:
|
||||
user = get_user_by_id(db, user_id)
|
||||
if not user:
|
||||
return None
|
||||
if clear:
|
||||
user.llm_api_key = None
|
||||
user.llm_base_url = None
|
||||
user.llm_model = None
|
||||
else:
|
||||
if llm_api_key is not None:
|
||||
user.llm_api_key = llm_api_key
|
||||
if llm_base_url is not None:
|
||||
user.llm_base_url = llm_base_url
|
||||
if llm_model is not None:
|
||||
user.llm_model = llm_model
|
||||
user.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
def get_system_setting(db: Session, key: str):
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
return setting.value if setting else None
|
||||
|
||||
|
||||
def set_system_setting(db: Session, key: str, value) -> SystemSettings:
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
if setting:
|
||||
setting.value = value
|
||||
setting.updated_at = datetime.utcnow()
|
||||
else:
|
||||
setting = SystemSettings(key=key, value=value)
|
||||
db.add(setting)
|
||||
db.commit()
|
||||
db.refresh(setting)
|
||||
return setting
|
||||
|
||||
|
||||
def delete_system_setting(db: Session, key: str) -> bool:
|
||||
setting = db.query(SystemSettings).filter(SystemSettings.key == key).first()
|
||||
if setting:
|
||||
db.delete(setting)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def create_audiobook_project(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
title: str,
|
||||
source_type: str,
|
||||
source_text: Optional[str] = None,
|
||||
source_path: Optional[str] = None,
|
||||
llm_model: Optional[str] = None,
|
||||
script_config: Optional[Dict[str, Any]] = None,
|
||||
) -> AudiobookProject:
|
||||
project = AudiobookProject(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
source_type=source_type,
|
||||
source_text=source_text,
|
||||
source_path=source_path,
|
||||
llm_model=llm_model,
|
||||
script_config=script_config,
|
||||
status="pending",
|
||||
)
|
||||
db.add(project)
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
def get_audiobook_project(db: Session, project_id: int, user_id: int) -> Optional[AudiobookProject]:
|
||||
return db.query(AudiobookProject).filter(
|
||||
AudiobookProject.id == project_id,
|
||||
AudiobookProject.user_id == user_id
|
||||
).first()
|
||||
|
||||
|
||||
def list_audiobook_projects(db: Session, user_id: int, skip: int = 0, limit: int = 50) -> List[AudiobookProject]:
|
||||
return db.query(AudiobookProject).filter(
|
||||
AudiobookProject.user_id == user_id
|
||||
).order_by(AudiobookProject.created_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
def update_audiobook_project_status(
|
||||
db: Session,
|
||||
project_id: int,
|
||||
status: str,
|
||||
error_message: Optional[str] = None
|
||||
) -> Optional[AudiobookProject]:
|
||||
project = db.query(AudiobookProject).filter(AudiobookProject.id == project_id).first()
|
||||
if not project:
|
||||
return None
|
||||
project.status = status
|
||||
if error_message is not None:
|
||||
project.error_message = error_message
|
||||
project.updated_at = datetime.utcnow()
|
||||
db.commit()
|
||||
db.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
def delete_audiobook_project(db: Session, project_id: int, user_id: int) -> bool:
|
||||
project = get_audiobook_project(db, project_id, user_id)
|
||||
if not project:
|
||||
return False
|
||||
db.delete(project)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
def create_audiobook_chapter(
|
||||
db: Session,
|
||||
project_id: int,
|
||||
chapter_index: int,
|
||||
source_text: str,
|
||||
title: Optional[str] = None,
|
||||
) -> AudiobookChapter:
|
||||
chapter = AudiobookChapter(
|
||||
project_id=project_id,
|
||||
chapter_index=chapter_index,
|
||||
source_text=source_text,
|
||||
title=title,
|
||||
status="pending",
|
||||
)
|
||||
db.add(chapter)
|
||||
db.commit()
|
||||
db.refresh(chapter)
|
||||
return chapter
|
||||
|
||||
|
||||
def get_audiobook_chapter(db: Session, chapter_id: int) -> Optional[AudiobookChapter]:
|
||||
return db.query(AudiobookChapter).filter(AudiobookChapter.id == chapter_id).first()
|
||||
|
||||
|
||||
def get_audiobook_chapter_by_index(db: Session, project_id: int, chapter_index: int) -> Optional[AudiobookChapter]:
|
||||
return db.query(AudiobookChapter).filter(
|
||||
AudiobookChapter.project_id == project_id,
|
||||
AudiobookChapter.chapter_index == chapter_index,
|
||||
).first()
|
||||
|
||||
|
||||
def list_audiobook_chapters(db: Session, project_id: int) -> List[AudiobookChapter]:
|
||||
return db.query(AudiobookChapter).filter(
|
||||
AudiobookChapter.project_id == project_id
|
||||
).order_by(AudiobookChapter.chapter_index).all()
|
||||
|
||||
|
||||
def update_audiobook_chapter_status(
|
||||
db: Session,
|
||||
chapter_id: int,
|
||||
status: str,
|
||||
error_message: Optional[str] = None,
|
||||
) -> Optional[AudiobookChapter]:
|
||||
chapter = db.query(AudiobookChapter).filter(AudiobookChapter.id == chapter_id).first()
|
||||
if not chapter:
|
||||
return None
|
||||
chapter.status = status
|
||||
if error_message is not None:
|
||||
chapter.error_message = error_message
|
||||
db.commit()
|
||||
db.refresh(chapter)
|
||||
return chapter
|
||||
|
||||
|
||||
def delete_audiobook_chapters(db: Session, project_id: int) -> None:
|
||||
db.query(AudiobookChapter).filter(AudiobookChapter.project_id == project_id).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
def delete_audiobook_segments_for_chapter(db: Session, project_id: int, chapter_index: int) -> None:
|
||||
db.query(AudiobookSegment).filter(
|
||||
AudiobookSegment.project_id == project_id,
|
||||
AudiobookSegment.chapter_index == chapter_index,
|
||||
).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
def create_audiobook_character(
|
||||
db: Session,
|
||||
project_id: int,
|
||||
name: str,
|
||||
gender: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
instruct: Optional[str] = None,
|
||||
voice_design_id: Optional[int] = None,
|
||||
) -> AudiobookCharacter:
|
||||
char = AudiobookCharacter(
|
||||
project_id=project_id,
|
||||
name=name,
|
||||
gender=gender,
|
||||
description=description,
|
||||
instruct=instruct,
|
||||
voice_design_id=voice_design_id,
|
||||
)
|
||||
db.add(char)
|
||||
db.commit()
|
||||
db.refresh(char)
|
||||
return char
|
||||
|
||||
|
||||
def get_audiobook_character(db: Session, char_id: int) -> Optional[AudiobookCharacter]:
|
||||
return db.query(AudiobookCharacter).filter(AudiobookCharacter.id == char_id).first()
|
||||
|
||||
|
||||
def list_audiobook_characters(db: Session, project_id: int) -> List[AudiobookCharacter]:
|
||||
return db.query(AudiobookCharacter).filter(
|
||||
AudiobookCharacter.project_id == project_id
|
||||
).all()
|
||||
|
||||
|
||||
def update_audiobook_character_voice(
|
||||
db: Session,
|
||||
char_id: int,
|
||||
voice_design_id: int
|
||||
) -> Optional[AudiobookCharacter]:
|
||||
char = db.query(AudiobookCharacter).filter(AudiobookCharacter.id == char_id).first()
|
||||
if not char:
|
||||
return None
|
||||
char.voice_design_id = voice_design_id
|
||||
db.commit()
|
||||
db.refresh(char)
|
||||
return char
|
||||
|
||||
|
||||
def update_audiobook_character(
|
||||
db: Session,
|
||||
char_id: int,
|
||||
name: Optional[str] = None,
|
||||
gender: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
instruct: Optional[str] = None,
|
||||
voice_design_id: Optional[int] = None,
|
||||
) -> Optional[AudiobookCharacter]:
|
||||
char = db.query(AudiobookCharacter).filter(AudiobookCharacter.id == char_id).first()
|
||||
if not char:
|
||||
return None
|
||||
if name is not None:
|
||||
char.name = name
|
||||
if gender is not None:
|
||||
char.gender = gender
|
||||
if description is not None:
|
||||
char.description = description
|
||||
if instruct is not None:
|
||||
char.instruct = instruct
|
||||
if voice_design_id is not None:
|
||||
char.voice_design_id = voice_design_id
|
||||
db.commit()
|
||||
db.refresh(char)
|
||||
return char
|
||||
|
||||
|
||||
def create_audiobook_segment(
|
||||
db: Session,
|
||||
project_id: int,
|
||||
character_id: int,
|
||||
text: str,
|
||||
chapter_index: int = 0,
|
||||
segment_index: int = 0,
|
||||
emo_text: Optional[str] = None,
|
||||
emo_alpha: Optional[float] = None,
|
||||
) -> AudiobookSegment:
|
||||
seg = AudiobookSegment(
|
||||
project_id=project_id,
|
||||
character_id=character_id,
|
||||
text=text,
|
||||
chapter_index=chapter_index,
|
||||
segment_index=segment_index,
|
||||
emo_text=emo_text or None,
|
||||
emo_alpha=emo_alpha,
|
||||
status="pending",
|
||||
)
|
||||
db.add(seg)
|
||||
db.commit()
|
||||
db.refresh(seg)
|
||||
return seg
|
||||
|
||||
|
||||
def list_audiobook_segments(
|
||||
db: Session,
|
||||
project_id: int,
|
||||
chapter_index: Optional[int] = None
|
||||
) -> List[AudiobookSegment]:
|
||||
query = db.query(AudiobookSegment).filter(AudiobookSegment.project_id == project_id)
|
||||
if chapter_index is not None:
|
||||
query = query.filter(AudiobookSegment.chapter_index == chapter_index)
|
||||
return query.order_by(AudiobookSegment.chapter_index, AudiobookSegment.segment_index).all()
|
||||
|
||||
|
||||
def update_audiobook_segment_status(
|
||||
db: Session,
|
||||
segment_id: int,
|
||||
status: str,
|
||||
audio_path: Optional[str] = None
|
||||
) -> Optional[AudiobookSegment]:
|
||||
seg = db.query(AudiobookSegment).filter(AudiobookSegment.id == segment_id).first()
|
||||
if not seg:
|
||||
return None
|
||||
seg.status = status
|
||||
if audio_path is not None:
|
||||
seg.audio_path = audio_path
|
||||
db.commit()
|
||||
db.refresh(seg)
|
||||
return seg
|
||||
|
||||
|
||||
def update_audiobook_segment(
|
||||
db: Session,
|
||||
segment_id: int,
|
||||
text: str,
|
||||
emo_text: Optional[str],
|
||||
emo_alpha: Optional[float],
|
||||
) -> Optional[AudiobookSegment]:
|
||||
seg = db.query(AudiobookSegment).filter(AudiobookSegment.id == segment_id).first()
|
||||
if not seg:
|
||||
return None
|
||||
seg.text = text
|
||||
seg.emo_text = emo_text or None
|
||||
seg.emo_alpha = emo_alpha
|
||||
seg.status = "pending"
|
||||
seg.audio_path = None
|
||||
db.commit()
|
||||
db.refresh(seg)
|
||||
return seg
|
||||
|
||||
|
||||
def delete_audiobook_segments(db: Session, project_id: int) -> None:
|
||||
db.query(AudiobookSegment).filter(AudiobookSegment.project_id == project_id).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
def delete_audiobook_characters(db: Session, project_id: int) -> None:
|
||||
db.query(AudiobookCharacter).filter(AudiobookCharacter.project_id == project_id).delete()
|
||||
db.commit()
|
||||
|
||||
|
||||
def create_usage_log(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
prompt_tokens: int,
|
||||
completion_tokens: int,
|
||||
model: Optional[str] = None,
|
||||
context: Optional[str] = None,
|
||||
) -> UsageLog:
|
||||
log = UsageLog(
|
||||
user_id=user_id,
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
model=model,
|
||||
context=context,
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
return log
|
||||
|
||||
|
||||
def get_usage_stats(
|
||||
db: Session,
|
||||
date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None,
|
||||
) -> List[Dict]:
|
||||
llm_query = db.query(
|
||||
UsageLog.user_id,
|
||||
func.sum(UsageLog.prompt_tokens).label("prompt_tokens"),
|
||||
func.sum(UsageLog.completion_tokens).label("completion_tokens"),
|
||||
).group_by(UsageLog.user_id)
|
||||
if date_from:
|
||||
llm_query = llm_query.filter(UsageLog.created_at >= date_from)
|
||||
if date_to:
|
||||
llm_query = llm_query.filter(UsageLog.created_at <= date_to)
|
||||
llm_rows = llm_query.all()
|
||||
llm_map: Dict[int, Dict] = {
|
||||
r.user_id: {"prompt_tokens": r.prompt_tokens or 0, "completion_tokens": r.completion_tokens or 0}
|
||||
for r in llm_rows
|
||||
}
|
||||
|
||||
tts_query = db.query(
|
||||
Job.user_id,
|
||||
Job.backend_type,
|
||||
func.count(Job.id).label("job_count"),
|
||||
func.sum(func.coalesce(func.length(Job.input_data), 0)).label("char_count"),
|
||||
).filter(Job.status == "completed").group_by(Job.user_id, Job.backend_type)
|
||||
if date_from:
|
||||
tts_query = tts_query.filter(Job.created_at >= date_from)
|
||||
if date_to:
|
||||
tts_query = tts_query.filter(Job.created_at <= date_to)
|
||||
tts_rows = tts_query.all()
|
||||
tts_map: Dict[int, List] = {}
|
||||
for r in tts_rows:
|
||||
tts_map.setdefault(r.user_id, []).append({
|
||||
"backend_type": r.backend_type,
|
||||
"job_count": r.job_count,
|
||||
"char_count": r.char_count or 0,
|
||||
})
|
||||
|
||||
user_ids = set(llm_map.keys()) | set(tts_map.keys())
|
||||
users = db.query(User).filter(User.id.in_(user_ids)).all() if user_ids else []
|
||||
user_info = {u.id: {"username": u.username, "email": u.email} for u in users}
|
||||
|
||||
result = []
|
||||
for uid in sorted(user_ids):
|
||||
info = user_info.get(uid, {"username": f"user_{uid}", "email": ""})
|
||||
llm = llm_map.get(uid, {"prompt_tokens": 0, "completion_tokens": 0})
|
||||
result.append({
|
||||
"user_id": uid,
|
||||
"username": info["username"],
|
||||
"email": info["email"],
|
||||
"llm_prompt_tokens": llm["prompt_tokens"],
|
||||
"llm_completion_tokens": llm["completion_tokens"],
|
||||
"tts_backends": tts_map.get(uid, []),
|
||||
})
|
||||
return result
|
||||
53
backend/db/database.py
Normal file
53
backend/db/database.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy.orm import sessionmaker, declarative_base
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
from config import settings
|
||||
|
||||
_is_sqlite = "sqlite" in settings.DATABASE_URL
|
||||
|
||||
engine = create_engine(
|
||||
settings.DATABASE_URL,
|
||||
connect_args={"check_same_thread": False} if _is_sqlite else {},
|
||||
poolclass=NullPool if _is_sqlite else None,
|
||||
)
|
||||
|
||||
if _is_sqlite:
|
||||
@event.listens_for(engine, "connect")
|
||||
def _set_wal(dbapi_conn, _):
|
||||
dbapi_conn.execute("PRAGMA journal_mode=WAL")
|
||||
dbapi_conn.execute("PRAGMA synchronous=NORMAL")
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
if "sqlite" in str(engine.url):
|
||||
with engine.connect() as conn:
|
||||
try:
|
||||
conn.execute(__import__("sqlalchemy").text(
|
||||
"ALTER TABLE audiobook_characters ADD COLUMN gender VARCHAR(20)"
|
||||
))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
for col_def in [
|
||||
"ALTER TABLE audiobook_segments ADD COLUMN emo_text VARCHAR(20)",
|
||||
"ALTER TABLE audiobook_segments ADD COLUMN emo_alpha REAL",
|
||||
"ALTER TABLE audiobook_projects ADD COLUMN script_config JSON",
|
||||
"ALTER TABLE users ADD COLUMN can_use_nsfw BOOLEAN DEFAULT FALSE NOT NULL",
|
||||
]:
|
||||
try:
|
||||
conn.execute(__import__("sqlalchemy").text(col_def))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
pass
|
||||
17
backend/db/migrate_add_local_permission.py
Normal file
17
backend/db/migrate_add_local_permission.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import create_engine, text
|
||||
from core.config import settings
|
||||
|
||||
def migrate():
|
||||
engine = create_engine(settings.DATABASE_URL)
|
||||
with engine.connect() as conn:
|
||||
conn.execute(text(
|
||||
"ALTER TABLE users ADD COLUMN can_use_local_model BOOLEAN DEFAULT 0 NOT NULL"
|
||||
))
|
||||
conn.execute(text(
|
||||
"UPDATE users SET can_use_local_model = 1 WHERE is_superuser = 1"
|
||||
))
|
||||
conn.commit()
|
||||
print("Migration completed: Added can_use_local_model column")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate()
|
||||
213
backend/db/models.py
Normal file
213
backend/db/models.py
Normal file
@@ -0,0 +1,213 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Float, ForeignKey, Text, Index, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from db.database import Base
|
||||
|
||||
class JobStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
class AudiobookStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
ANALYZING = "analyzing"
|
||||
READY = "ready"
|
||||
GENERATING = "generating"
|
||||
DONE = "done"
|
||||
ERROR = "error"
|
||||
|
||||
class SegmentStatus(str, Enum):
|
||||
PENDING = "pending"
|
||||
GENERATING = "generating"
|
||||
DONE = "done"
|
||||
ERROR = "error"
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
username = Column(String(50), unique=True, nullable=False, index=True)
|
||||
email = Column(String(255), unique=True, nullable=False, index=True)
|
||||
hashed_password = Column(String(255), nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_superuser = Column(Boolean, default=False, nullable=False)
|
||||
llm_api_key = Column(Text, nullable=True)
|
||||
llm_base_url = Column(String(500), nullable=True)
|
||||
llm_model = Column(String(200), nullable=True)
|
||||
can_use_local_model = Column(Boolean, default=False, nullable=False)
|
||||
can_use_nsfw = Column(Boolean, default=False, nullable=False)
|
||||
user_preferences = Column(JSON, nullable=True, default=lambda: {"default_backend": "local", "onboarding_completed": False})
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
jobs = relationship("Job", back_populates="user", cascade="all, delete-orphan")
|
||||
voice_caches = relationship("VoiceCache", back_populates="user", cascade="all, delete-orphan")
|
||||
voice_designs = relationship("VoiceDesign", back_populates="user", cascade="all, delete-orphan")
|
||||
audiobook_projects = relationship("AudiobookProject", back_populates="user", cascade="all, delete-orphan")
|
||||
usage_logs = relationship("UsageLog", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
class Job(Base):
|
||||
__tablename__ = "jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
job_type = Column(String(50), nullable=False)
|
||||
status = Column(String(50), default="pending", nullable=False, index=True)
|
||||
backend_type = Column(String(20), default="local", nullable=False)
|
||||
input_data = Column(Text, nullable=True)
|
||||
input_params = Column(JSON, nullable=True)
|
||||
output_path = Column(String(500), nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
|
||||
user = relationship("User", back_populates="jobs")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_user_status', 'user_id', 'status'),
|
||||
Index('idx_user_created', 'user_id', 'created_at'),
|
||||
)
|
||||
|
||||
class VoiceCache(Base):
|
||||
__tablename__ = "voice_caches"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
ref_audio_hash = Column(String(64), nullable=False, index=True)
|
||||
cache_path = Column(String(500), nullable=False)
|
||||
meta_data = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
last_accessed = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
access_count = Column(Integer, default=0, nullable=False)
|
||||
|
||||
user = relationship("User", back_populates="voice_caches")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_user_hash', 'user_id', 'ref_audio_hash'),
|
||||
)
|
||||
|
||||
class SystemSettings(Base):
|
||||
__tablename__ = "system_settings"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String(100), unique=True, nullable=False, index=True)
|
||||
value = Column(JSON, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
class VoiceDesign(Base):
|
||||
__tablename__ = "voice_designs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
instruct = Column(Text, nullable=False)
|
||||
meta_data = Column(JSON, nullable=True)
|
||||
preview_text = Column(Text, nullable=True)
|
||||
ref_audio_path = Column(String(500), nullable=True)
|
||||
ref_text = Column(Text, nullable=True)
|
||||
voice_cache_id = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
last_used = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
use_count = Column(Integer, default=0, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
user = relationship("User", back_populates="voice_designs")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_user_active', 'user_id', 'is_active'),
|
||||
)
|
||||
|
||||
|
||||
class AudiobookProject(Base):
|
||||
__tablename__ = "audiobook_projects"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
title = Column(String(500), nullable=False)
|
||||
source_type = Column(String(20), nullable=False)
|
||||
script_config = Column(JSON, nullable=True)
|
||||
source_path = Column(String(500), nullable=True)
|
||||
source_text = Column(Text, nullable=True)
|
||||
status = Column(String(20), default="pending", nullable=False, index=True)
|
||||
llm_model = Column(String(200), nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
|
||||
user = relationship("User", back_populates="audiobook_projects")
|
||||
characters = relationship("AudiobookCharacter", back_populates="project", cascade="all, delete-orphan")
|
||||
chapters = relationship("AudiobookChapter", back_populates="project", cascade="all, delete-orphan", order_by="AudiobookChapter.chapter_index")
|
||||
segments = relationship("AudiobookSegment", back_populates="project", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class AudiobookChapter(Base):
|
||||
__tablename__ = "audiobook_chapters"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
project_id = Column(Integer, ForeignKey("audiobook_projects.id"), nullable=False, index=True)
|
||||
chapter_index = Column(Integer, nullable=False)
|
||||
title = Column(String(500), nullable=True)
|
||||
source_text = Column(Text, nullable=False)
|
||||
status = Column(String(20), default="pending", nullable=False)
|
||||
error_message = Column(Text, nullable=True)
|
||||
|
||||
project = relationship("AudiobookProject", back_populates="chapters")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_chapter_project_idx', 'project_id', 'chapter_index'),
|
||||
)
|
||||
|
||||
|
||||
class AudiobookCharacter(Base):
|
||||
__tablename__ = "audiobook_characters"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
project_id = Column(Integer, ForeignKey("audiobook_projects.id"), nullable=False, index=True)
|
||||
name = Column(String(200), nullable=False)
|
||||
gender = Column(String(20), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
instruct = Column(Text, nullable=True)
|
||||
voice_design_id = Column(Integer, ForeignKey("voice_designs.id"), nullable=True)
|
||||
project = relationship("AudiobookProject", back_populates="characters")
|
||||
voice_design = relationship("VoiceDesign")
|
||||
segments = relationship("AudiobookSegment", back_populates="character")
|
||||
|
||||
|
||||
class AudiobookSegment(Base):
|
||||
__tablename__ = "audiobook_segments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
project_id = Column(Integer, ForeignKey("audiobook_projects.id"), nullable=False, index=True)
|
||||
chapter_index = Column(Integer, nullable=False, default=0)
|
||||
segment_index = Column(Integer, nullable=False)
|
||||
character_id = Column(Integer, ForeignKey("audiobook_characters.id"), nullable=False)
|
||||
text = Column(Text, nullable=False)
|
||||
emo_text = Column(String(100), nullable=True)
|
||||
emo_alpha = Column(Float, nullable=True)
|
||||
audio_path = Column(String(500), nullable=True)
|
||||
status = Column(String(20), default="pending", nullable=False)
|
||||
|
||||
project = relationship("AudiobookProject", back_populates="segments")
|
||||
character = relationship("AudiobookCharacter", back_populates="segments")
|
||||
|
||||
__table_args__ = (
|
||||
Index('idx_project_chapter', 'project_id', 'chapter_index', 'segment_index'),
|
||||
)
|
||||
|
||||
|
||||
class UsageLog(Base):
|
||||
__tablename__ = "usage_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
prompt_tokens = Column(Integer, nullable=False, default=0)
|
||||
completion_tokens = Column(Integer, nullable=False, default=0)
|
||||
model = Column(String(200), nullable=True)
|
||||
context = Column(String(100), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||
|
||||
user = relationship("User", back_populates="usage_logs")
|
||||
Reference in New Issue
Block a user