190 lines
5.8 KiB
Python
190 lines
5.8 KiB
Python
"""Users API routes for VoIdea."""
|
|
|
|
from typing import Annotated, Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.dependencies import get_current_user, get_db
|
|
from app.models.user import User
|
|
from app.schemas.user import SubscriptionInfo, UserResponse, UserUpdate, VoiceSettingsResponse, VoiceSettingsUpdate
|
|
from app.services.tariff_service import TariffService
|
|
from app.services.user_service import UserService
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _user_to_response(user: User) -> UserResponse:
|
|
return UserResponse(
|
|
id=str(user.id),
|
|
email=user.email,
|
|
display_name=user.display_name,
|
|
avatar_url=user.avatar_url,
|
|
is_active=user.is_active,
|
|
is_superuser=user.is_superuser,
|
|
oauth_provider=user.oauth_provider,
|
|
created_at=user.created_at,
|
|
updated_at=user.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/me", response_model=UserResponse)
|
|
async def get_me(user: Annotated[User, Depends(get_current_user)]):
|
|
return _user_to_response(user)
|
|
|
|
|
|
@router.patch("/me", response_model=UserResponse)
|
|
async def update_me(
|
|
body: UserUpdate,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = UserService(db)
|
|
result = await service.update_profile(
|
|
str(user.id),
|
|
display_name=body.display_name,
|
|
avatar_url=body.avatar_url,
|
|
)
|
|
if not result:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
|
return _user_to_response(result)
|
|
|
|
|
|
@router.delete("/me", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_me(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
service = UserService(db)
|
|
await service.delete(str(user.id))
|
|
|
|
|
|
VOICE_PRESETS: dict[str, dict[str, Any]] = {
|
|
"standard": {
|
|
"vad_noise_threshold": 0.3,
|
|
"vad_silence_timeout_ms": 1500,
|
|
"confidence_verified": 80,
|
|
"confidence_warning": 50,
|
|
"semantic_mode": "fast",
|
|
"tts_enabled": True,
|
|
"continuous_listening": False,
|
|
},
|
|
"precise": {
|
|
"vad_noise_threshold": 0.2,
|
|
"vad_silence_timeout_ms": 2000,
|
|
"confidence_verified": 85,
|
|
"confidence_warning": 60,
|
|
"semantic_mode": "full",
|
|
"tts_enabled": True,
|
|
"continuous_listening": False,
|
|
},
|
|
"fast": {
|
|
"vad_noise_threshold": 0.4,
|
|
"vad_silence_timeout_ms": 1000,
|
|
"confidence_verified": 70,
|
|
"confidence_warning": 40,
|
|
"semantic_mode": "fast",
|
|
"tts_enabled": False,
|
|
"continuous_listening": True,
|
|
},
|
|
"quiet": {
|
|
"vad_noise_threshold": 0.6,
|
|
"vad_silence_timeout_ms": 3000,
|
|
"confidence_verified": 80,
|
|
"confidence_warning": 50,
|
|
"semantic_mode": "fast",
|
|
"tts_enabled": True,
|
|
"continuous_listening": False,
|
|
},
|
|
"expert": {
|
|
"vad_noise_threshold": 0.15,
|
|
"vad_silence_timeout_ms": 1000,
|
|
"confidence_verified": 90,
|
|
"confidence_warning": 70,
|
|
"semantic_mode": "full",
|
|
"tts_enabled": True,
|
|
"continuous_listening": True,
|
|
},
|
|
}
|
|
|
|
|
|
@router.get("/me/voice-settings", response_model=VoiceSettingsResponse)
|
|
async def get_voice_settings(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
):
|
|
tuning = user.pipeline_tuning or {}
|
|
preset = tuning.get("preset", "standard")
|
|
overrides = tuning.get("overrides", {})
|
|
return VoiceSettingsResponse(
|
|
preset=preset,
|
|
overrides=overrides,
|
|
available_presets=list(VOICE_PRESETS.keys()),
|
|
preset_values=VOICE_PRESETS.get(preset, VOICE_PRESETS["standard"]),
|
|
)
|
|
|
|
|
|
@router.patch("/me/voice-settings", response_model=VoiceSettingsResponse)
|
|
async def update_voice_settings(
|
|
body: VoiceSettingsUpdate,
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
from sqlalchemy import select
|
|
|
|
tuning = user.pipeline_tuning or {}
|
|
|
|
if body.reset:
|
|
tuning = {"preset": "standard", "overrides": {}}
|
|
else:
|
|
if body.preset is not None:
|
|
if body.preset not in VOICE_PRESETS:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Unknown preset: {body.preset}. Available: {', '.join(VOICE_PRESETS.keys())}",
|
|
)
|
|
tuning["preset"] = body.preset
|
|
|
|
if body.overrides is not None:
|
|
existing_overrides = tuning.get("overrides", {})
|
|
existing_overrides.update(body.overrides)
|
|
tuning["overrides"] = existing_overrides
|
|
|
|
result = await db.execute(select(User).where(User.id == user.id))
|
|
db_user = result.scalar_one()
|
|
db_user.pipeline_tuning = tuning
|
|
await db.commit()
|
|
|
|
preset = tuning.get("preset", "standard")
|
|
preset_vals = VOICE_PRESETS.get(preset, VOICE_PRESETS["standard"])
|
|
overrides = tuning.get("overrides", {})
|
|
merged = {**preset_vals, **overrides}
|
|
|
|
return VoiceSettingsResponse(
|
|
preset=preset,
|
|
overrides=overrides,
|
|
available_presets=list(VOICE_PRESETS.keys()),
|
|
preset_values=merged,
|
|
)
|
|
|
|
|
|
@router.get("/me/subscription", response_model=SubscriptionInfo)
|
|
async def get_my_subscription(
|
|
user: Annotated[User, Depends(get_current_user)],
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
svc = TariffService(db)
|
|
sub = await svc.get_user_subscription(user.id)
|
|
if not sub:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Subscription not found"
|
|
)
|
|
plan = await svc.get_plan_by_id(sub.plan_id)
|
|
return SubscriptionInfo(
|
|
plan_name=plan.name if plan else "Бесплатно",
|
|
plan_code=plan.code if plan else "free",
|
|
status=sub.status,
|
|
expires_at=sub.current_period_end,
|
|
features=plan.features if plan else None,
|
|
)
|