Spaces:
Runtime error
Runtime error
| """ | |
| Pydantic models for request/response validation. | |
| """ | |
| from pydantic import BaseModel, Field | |
| from typing import Optional, List | |
| from datetime import datetime | |
| from .utils.capture_chords import ( | |
| default_push_to_talk_chord, | |
| default_toggle_to_talk_chord, | |
| ) | |
| class VoiceProfileCreate(BaseModel): | |
| """Request model for creating a voice profile.""" | |
| name: str = Field(..., min_length=1, max_length=100) | |
| description: Optional[str] = Field(None, max_length=500) | |
| language: str = Field( | |
| default="en", pattern="^(zh|en|ja|ko|de|fr|ru|pt|es|it|he|ar|da|el|fi|hi|ms|nl|no|pl|sv|sw|tr)$" | |
| ) | |
| voice_type: Optional[str] = Field(default="cloned", pattern="^(cloned|preset|designed)$") | |
| preset_engine: Optional[str] = Field(None, max_length=50) | |
| preset_voice_id: Optional[str] = Field(None, max_length=100) | |
| design_prompt: Optional[str] = Field(None, max_length=2000) | |
| default_engine: Optional[str] = Field(None, max_length=50) | |
| personality: Optional[str] = Field(None, max_length=2000) | |
| class VoiceProfileResponse(BaseModel): | |
| """Response model for voice profile.""" | |
| id: str | |
| name: str | |
| description: Optional[str] | |
| language: str | |
| avatar_path: Optional[str] = None | |
| effects_chain: Optional[List["EffectConfig"]] = None | |
| voice_type: str = "cloned" | |
| preset_engine: Optional[str] = None | |
| preset_voice_id: Optional[str] = None | |
| design_prompt: Optional[str] = None | |
| default_engine: Optional[str] = None | |
| personality: Optional[str] = None | |
| generation_count: int = 0 | |
| sample_count: int = 0 | |
| created_at: datetime | |
| updated_at: datetime | |
| class Config: | |
| from_attributes = True | |
| class ProfileSampleCreate(BaseModel): | |
| """Request model for adding a sample to a profile.""" | |
| reference_text: str = Field(..., min_length=1, max_length=1000) | |
| class ProfileSampleUpdate(BaseModel): | |
| """Request model for updating a profile sample.""" | |
| reference_text: str = Field(..., min_length=1, max_length=1000) | |
| class ProfileSampleResponse(BaseModel): | |
| """Response model for profile sample.""" | |
| id: str | |
| profile_id: str | |
| audio_path: str | |
| reference_text: str | |
| class Config: | |
| from_attributes = True | |
| class GenerationRequest(BaseModel): | |
| """Request model for voice generation.""" | |
| profile_id: str | |
| text: str = Field(..., min_length=1, max_length=50000) | |
| language: str = Field(default="en", pattern="^(zh|en|ja|ko|de|fr|ru|pt|es|it|he|ar|da|el|fi|hi|ms|nl|no|pl|sv|sw|tr)$") | |
| seed: Optional[int] = Field(None, ge=0) | |
| model_size: Optional[str] = Field(default="1.7B", pattern="^(1\\.7B|0\\.6B|1B|3B)$") | |
| instruct: Optional[str] = Field(None, max_length=500) | |
| engine: Optional[str] = Field(default="qwen", pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$") | |
| personality: bool = Field( | |
| default=False, | |
| description="When true and the profile has a personality prompt, the input text is rewritten in-character before TTS.", | |
| ) | |
| max_chunk_chars: int = Field( | |
| default=800, ge=100, le=5000, description="Max characters per chunk for long text splitting" | |
| ) | |
| crossfade_ms: int = Field( | |
| default=50, ge=0, le=500, description="Crossfade duration in ms between chunks (0 for hard cut)" | |
| ) | |
| normalize: bool = Field(default=True, description="Normalize output audio volume") | |
| effects_chain: Optional[List["EffectConfig"]] = Field( | |
| None, description="Effects chain to apply after generation (overrides profile default)" | |
| ) | |
| class GenerationResponse(BaseModel): | |
| """Response model for voice generation.""" | |
| id: str | |
| profile_id: str | |
| text: str | |
| language: str | |
| audio_path: Optional[str] = None | |
| duration: Optional[float] = None | |
| seed: Optional[int] = None | |
| instruct: Optional[str] = None | |
| engine: Optional[str] = "qwen" | |
| model_size: Optional[str] = None | |
| status: str = "completed" | |
| error: Optional[str] = None | |
| is_favorited: bool = False | |
| source: str = "manual" | |
| created_at: datetime | |
| versions: Optional[List["GenerationVersionResponse"]] = None | |
| active_version_id: Optional[str] = None | |
| class Config: | |
| from_attributes = True | |
| class HistoryQuery(BaseModel): | |
| """Query model for generation history.""" | |
| profile_id: Optional[str] = None | |
| search: Optional[str] = None | |
| limit: int = Field(default=50, ge=1, le=100) | |
| offset: int = Field(default=0, ge=0) | |
| class HistoryResponse(BaseModel): | |
| """Response model for history entry (includes profile name).""" | |
| id: str | |
| profile_id: str | |
| profile_name: str | |
| text: str | |
| language: str | |
| audio_path: Optional[str] = None | |
| duration: Optional[float] = None | |
| seed: Optional[int] = None | |
| instruct: Optional[str] = None | |
| engine: Optional[str] = "qwen" | |
| model_size: Optional[str] = None | |
| status: str = "completed" | |
| error: Optional[str] = None | |
| is_favorited: bool = False | |
| created_at: datetime | |
| versions: Optional[List["GenerationVersionResponse"]] = None | |
| active_version_id: Optional[str] = None | |
| class Config: | |
| from_attributes = True | |
| class HistoryListResponse(BaseModel): | |
| """Response model for history list.""" | |
| items: List[HistoryResponse] | |
| total: int | |
| class TranscriptionRequest(BaseModel): | |
| """Request model for audio transcription.""" | |
| language: Optional[str] = Field(None, pattern="^(en|zh|ja|ko|de|fr|ru|pt|es|it)$") | |
| model: Optional[str] = Field(None, pattern="^(base|small|medium|large|turbo)$") | |
| class TranscriptionResponse(BaseModel): | |
| """Response model for transcription.""" | |
| text: str | |
| duration: float | |
| class RefinementFlagsModel(BaseModel): | |
| """Boolean toggles that drive the refinement prompt builder.""" | |
| smart_cleanup: bool = True | |
| self_correction: bool = True | |
| preserve_technical: bool = True | |
| class CaptureResponse(BaseModel): | |
| """Response model for a capture.""" | |
| id: str | |
| audio_path: str | |
| source: str | |
| language: Optional[str] = None | |
| duration_ms: Optional[int] = None | |
| transcript_raw: str | |
| transcript_refined: Optional[str] = None | |
| stt_model: Optional[str] = None | |
| llm_model: Optional[str] = None | |
| refinement_flags: Optional[RefinementFlagsModel] = None | |
| created_at: datetime | |
| class Config: | |
| from_attributes = True | |
| class CaptureListResponse(BaseModel): | |
| """Response model for paginated capture list.""" | |
| items: List[CaptureResponse] | |
| total: int | |
| class CaptureCreateResponse(CaptureResponse): | |
| """ | |
| Response model for ``POST /captures``. | |
| Adds ``auto_refine`` and ``allow_auto_paste`` — the server-side settings | |
| captured at the moment the capture was created. The client reads these to | |
| decide whether to chain a refinement request and whether to fire the | |
| synthetic-paste pipeline, so it doesn't need a synced local copy of the | |
| capture_settings table across sibling Tauri webviews. | |
| """ | |
| auto_refine: bool | |
| allow_auto_paste: bool | |
| class CaptureRefineRequest(BaseModel): | |
| """Request to refine a capture's transcript via the LLM.""" | |
| flags: Optional[RefinementFlagsModel] = None | |
| model_size: Optional[str] = Field(default=None, pattern="^(0\\.6B|1\\.7B|4B)$") | |
| class CaptureRetranscribeRequest(BaseModel): | |
| """Request to re-run STT on a capture's audio with a different model.""" | |
| model: Optional[str] = Field(None, pattern="^(base|small|medium|large|turbo)$") | |
| language: Optional[str] = Field(None, pattern="^(en|zh|ja|ko|de|fr|ru|pt|es|it)$") | |
| class CaptureSettingsResponse(BaseModel): | |
| """Server-persisted defaults for the capture / refine flow.""" | |
| stt_model: str = Field(default="turbo", pattern="^(base|small|medium|large|turbo)$") | |
| language: str = Field(default="auto") | |
| auto_refine: bool = True | |
| llm_model: str = Field(default="0.6B", pattern="^(0\\.6B|1\\.7B|4B)$") | |
| smart_cleanup: bool = True | |
| self_correction: bool = True | |
| preserve_technical: bool = True | |
| allow_auto_paste: bool = True | |
| default_playback_voice_id: Optional[str] = None | |
| hotkey_enabled: bool = False | |
| chord_push_to_talk_keys: List[str] = Field( | |
| default_factory=default_push_to_talk_chord | |
| ) | |
| chord_toggle_to_talk_keys: List[str] = Field( | |
| default_factory=default_toggle_to_talk_chord | |
| ) | |
| class Config: | |
| from_attributes = True | |
| class CaptureSettingsUpdate(BaseModel): | |
| """Partial update for capture settings — every field is optional.""" | |
| stt_model: Optional[str] = Field(default=None, pattern="^(base|small|medium|large|turbo)$") | |
| language: Optional[str] = None | |
| auto_refine: Optional[bool] = None | |
| llm_model: Optional[str] = Field(default=None, pattern="^(0\\.6B|1\\.7B|4B)$") | |
| smart_cleanup: Optional[bool] = None | |
| self_correction: Optional[bool] = None | |
| preserve_technical: Optional[bool] = None | |
| allow_auto_paste: Optional[bool] = None | |
| default_playback_voice_id: Optional[str] = None | |
| hotkey_enabled: Optional[bool] = None | |
| chord_push_to_talk_keys: Optional[List[str]] = Field(default=None, min_length=1, max_length=6) | |
| chord_toggle_to_talk_keys: Optional[List[str]] = Field(default=None, min_length=1, max_length=6) | |
| class GenerationSettingsResponse(BaseModel): | |
| """Server-persisted defaults for the generation flow.""" | |
| max_chunk_chars: int = Field(default=800, ge=100, le=5000) | |
| crossfade_ms: int = Field(default=50, ge=0, le=500) | |
| normalize_audio: bool = True | |
| autoplay_on_generate: bool = True | |
| class Config: | |
| from_attributes = True | |
| class GenerationSettingsUpdate(BaseModel): | |
| """Partial update for generation settings — every field is optional.""" | |
| max_chunk_chars: Optional[int] = Field(default=None, ge=100, le=5000) | |
| crossfade_ms: Optional[int] = Field(default=None, ge=0, le=500) | |
| normalize_audio: Optional[bool] = None | |
| autoplay_on_generate: Optional[bool] = None | |
| class MCPClientBindingResponse(BaseModel): | |
| """Per-MCP-client voice binding — what voice / engine the server should | |
| use when a given client_id calls voicebox.speak without args, plus an | |
| opt-in personality-rewrite default.""" | |
| client_id: str | |
| label: Optional[str] = None | |
| profile_id: Optional[str] = None | |
| default_engine: Optional[str] = Field( | |
| None, | |
| pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$", | |
| ) | |
| default_personality: bool = False | |
| last_seen_at: Optional[datetime] = None | |
| created_at: datetime | |
| updated_at: datetime | |
| class Config: | |
| from_attributes = True | |
| class MCPClientBindingUpsert(BaseModel): | |
| """Create or update a binding. Matched by ``client_id``.""" | |
| client_id: str = Field(..., min_length=1, max_length=64) | |
| label: Optional[str] = Field(None, max_length=128) | |
| profile_id: Optional[str] = None | |
| default_engine: Optional[str] = Field( | |
| None, | |
| pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$", | |
| ) | |
| default_personality: bool = False | |
| class MCPClientBindingListResponse(BaseModel): | |
| items: List[MCPClientBindingResponse] | |
| class SpeakRequest(BaseModel): | |
| """Body for POST /speak — non-MCP REST surface that mirrors voicebox.speak.""" | |
| text: str = Field(..., min_length=1, max_length=10000) | |
| profile: Optional[str] = Field( | |
| None, | |
| description="Voice profile name or id. Falls back to per-client binding, then default.", | |
| ) | |
| engine: Optional[str] = Field( | |
| None, | |
| pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$", | |
| ) | |
| personality: Optional[bool] = Field( | |
| None, | |
| description="When true and the profile has a personality prompt, the input text is rewritten in-character before TTS. When null, the per-client binding's default_personality flag decides.", | |
| ) | |
| language: Optional[str] = Field( | |
| None, | |
| pattern="^(zh|en|ja|ko|de|fr|ru|pt|es|it|he|ar|da|el|fi|hi|ms|nl|no|pl|sv|sw|tr)$", | |
| ) | |
| class LLMGenerateRequest(BaseModel): | |
| """Request model for LLM text generation.""" | |
| prompt: str = Field(..., min_length=1, max_length=50000) | |
| system: Optional[str] = Field(None, max_length=4000) | |
| model_size: Optional[str] = Field(default="0.6B", pattern="^(0\\.6B|1\\.7B|4B)$") | |
| max_tokens: int = Field(default=512, ge=1, le=4096) | |
| temperature: float = Field(default=0.7, ge=0.0, le=2.0) | |
| # Few-shot (user, assistant) pairs prepended as real chat turns. | |
| # Used by the refinement service to pin tricky rules (imperatives | |
| # staying imperatives, technical-term punctuation) that small models | |
| # lose when the examples live inline in the system prompt. | |
| examples: Optional[List[List[str]]] = Field(default=None, max_length=8) | |
| class LLMGenerateResponse(BaseModel): | |
| """Response model for LLM text generation.""" | |
| text: str | |
| model_size: str | |
| # ── Profile personality endpoint ────────────────────────────────────── | |
| # The sole standalone personality endpoint is ``/profiles/{id}/compose``, | |
| # which produces a fresh in-character utterance the UI drops into the | |
| # generate textarea. Rewrite is now reached via ``/generate`` with | |
| # ``personality=true``. | |
| class PersonalityTextResponse(BaseModel): | |
| """Response returned by the ``/profiles/{id}/compose`` endpoint.""" | |
| text: str | |
| model_size: str | |
| class ModelReadiness(BaseModel): | |
| """Per-model entry in the dictation readiness checklist. | |
| ``model_name`` is the canonical id used by ``POST /models/download`` so the | |
| frontend can wire a one-click "Download" button without a second lookup. | |
| ``size`` is the user's chosen variant (e.g. "turbo", "0.6B"); ``display_name`` | |
| is what the checklist row should show ("Whisper Turbo"). | |
| """ | |
| ready: bool | |
| model_name: str | |
| display_name: str | |
| size: str | |
| size_mb: Optional[int] = None | |
| class CaptureReadinessResponse(BaseModel): | |
| """Backend gates that must be green before the global hotkey will fire. | |
| The frontend combines this with its own TCC permission checks (input | |
| monitoring, accessibility) into the full dictation readiness checklist. | |
| Hotkey-enabled is the user's intent toggle and lives outside this struct. | |
| """ | |
| stt: ModelReadiness | |
| llm: ModelReadiness | |
| class HealthResponse(BaseModel): | |
| """Response model for health check.""" | |
| status: str | |
| model_loaded: bool | |
| model_downloaded: Optional[bool] = None # Whether model is cached/downloaded | |
| model_size: Optional[str] = None # Current model size if loaded | |
| gpu_available: bool | |
| gpu_type: Optional[str] = None # GPU type (CUDA, MPS, or None) | |
| vram_used_mb: Optional[float] = None | |
| backend_type: Optional[str] = None # Backend type (mlx or pytorch) | |
| backend_variant: Optional[str] = None # Binary variant (cpu or cuda) | |
| gpu_compatibility_warning: Optional[str] = None # Warning if GPU arch unsupported | |
| class DirectoryCheck(BaseModel): | |
| """Health status for a single directory.""" | |
| path: str | |
| exists: bool | |
| writable: bool | |
| error: Optional[str] = None | |
| class FilesystemHealthResponse(BaseModel): | |
| """Response model for filesystem health check.""" | |
| healthy: bool | |
| disk_free_mb: Optional[float] = None | |
| disk_total_mb: Optional[float] = None | |
| directories: List[DirectoryCheck] | |
| class ModelStatus(BaseModel): | |
| """Response model for model status.""" | |
| model_name: str | |
| display_name: str | |
| hf_repo_id: Optional[str] = None # HuggingFace repository ID | |
| downloaded: bool | |
| downloading: bool = False # True if download is in progress | |
| size_mb: Optional[float] = None | |
| loaded: bool = False | |
| class ModelStatusListResponse(BaseModel): | |
| """Response model for model status list.""" | |
| models: List[ModelStatus] | |
| class ModelDownloadRequest(BaseModel): | |
| """Request model for triggering model download.""" | |
| model_name: str | |
| class ModelMigrateRequest(BaseModel): | |
| """Request model for migrating models to a new directory.""" | |
| destination: str | |
| class ActiveDownloadTask(BaseModel): | |
| """Response model for active download task.""" | |
| model_name: str | |
| status: str | |
| started_at: datetime | |
| error: Optional[str] = None | |
| progress: Optional[float] = None # 0-100 percentage | |
| current: Optional[int] = None # bytes downloaded | |
| total: Optional[int] = None # total bytes | |
| filename: Optional[str] = None # current file being downloaded | |
| class ActiveGenerationTask(BaseModel): | |
| """Response model for active generation task.""" | |
| task_id: str | |
| profile_id: str | |
| text_preview: str | |
| started_at: datetime | |
| class ActiveTasksResponse(BaseModel): | |
| """Response model for active tasks.""" | |
| downloads: List[ActiveDownloadTask] | |
| generations: List[ActiveGenerationTask] | |
| class AudioChannelCreate(BaseModel): | |
| """Request model for creating an audio channel.""" | |
| name: str = Field(..., min_length=1, max_length=100) | |
| device_ids: List[str] = Field(default_factory=list) | |
| class AudioChannelUpdate(BaseModel): | |
| """Request model for updating an audio channel.""" | |
| name: Optional[str] = Field(None, min_length=1, max_length=100) | |
| device_ids: Optional[List[str]] = None | |
| class AudioChannelResponse(BaseModel): | |
| """Response model for audio channel.""" | |
| id: str | |
| name: str | |
| is_default: bool | |
| device_ids: List[str] | |
| created_at: datetime | |
| class Config: | |
| from_attributes = True | |
| class ChannelVoiceAssignment(BaseModel): | |
| """Request model for assigning voices to a channel.""" | |
| profile_ids: List[str] | |
| class ProfileChannelAssignment(BaseModel): | |
| """Request model for assigning channels to a profile.""" | |
| channel_ids: List[str] | |
| class StoryCreate(BaseModel): | |
| """Request model for creating a story.""" | |
| name: str = Field(..., min_length=1, max_length=100) | |
| description: Optional[str] = Field(None, max_length=500) | |
| class StoryResponse(BaseModel): | |
| """Response model for story (list view).""" | |
| id: str | |
| name: str | |
| description: Optional[str] | |
| created_at: datetime | |
| updated_at: datetime | |
| item_count: int = 0 | |
| class Config: | |
| from_attributes = True | |
| class StoryItemDetail(BaseModel): | |
| """Detail model for story item with generation info.""" | |
| id: str | |
| story_id: str | |
| generation_id: str | |
| version_id: Optional[str] = None | |
| start_time_ms: int | |
| track: int = 0 | |
| trim_start_ms: int = 0 | |
| trim_end_ms: int = 0 | |
| created_at: datetime | |
| # Generation details | |
| profile_id: str | |
| profile_name: str | |
| text: str | |
| language: str | |
| audio_path: str | |
| duration: float | |
| seed: Optional[int] | |
| instruct: Optional[str] | |
| engine: Optional[str] = None | |
| volume: float = 1.0 | |
| generation_created_at: datetime | |
| # Versions available for this generation | |
| versions: Optional[List["GenerationVersionResponse"]] = None | |
| active_version_id: Optional[str] = None | |
| class Config: | |
| from_attributes = True | |
| class StoryDetailResponse(BaseModel): | |
| """Response model for story with items.""" | |
| id: str | |
| name: str | |
| description: Optional[str] | |
| created_at: datetime | |
| updated_at: datetime | |
| items: List[StoryItemDetail] = [] | |
| class Config: | |
| from_attributes = True | |
| class StoryItemCreate(BaseModel): | |
| """Request model for adding a generation to a story.""" | |
| generation_id: str | |
| start_time_ms: Optional[int] = None # If not provided, will be calculated automatically | |
| track: Optional[int] = 0 # Track number (0 = main track) | |
| class StoryItemUpdateTime(BaseModel): | |
| """Request model for updating a story item's timecode.""" | |
| generation_id: str | |
| start_time_ms: int = Field(..., ge=0) | |
| class StoryItemBatchUpdate(BaseModel): | |
| """Request model for batch updating story item timecodes.""" | |
| updates: List[StoryItemUpdateTime] | |
| class StoryItemReorder(BaseModel): | |
| """Request model for reordering story items.""" | |
| generation_ids: List[str] = Field(..., min_length=1) | |
| class StoryItemMove(BaseModel): | |
| """Request model for moving a story item (position and/or track).""" | |
| start_time_ms: int = Field(..., ge=0) | |
| track: int = 0 | |
| class StoryItemTrim(BaseModel): | |
| """Request model for trimming a story item.""" | |
| trim_start_ms: int = Field(..., ge=0) | |
| trim_end_ms: int = Field(..., ge=0) | |
| class StoryItemSplit(BaseModel): | |
| """Request model for splitting a story item.""" | |
| split_time_ms: int = Field(..., ge=0) # Time within the clip to split at (relative to clip start) | |
| class StoryItemVersionUpdate(BaseModel): | |
| """Request model for setting a story item's pinned version.""" | |
| version_id: Optional[str] = None # null = use generation default | |
| class StoryItemVolumeUpdate(BaseModel): | |
| """Request model for adjusting a story item's playback volume. | |
| Linear gain. ``1.0`` is the original level, ``0.0`` is silent. Capped | |
| above 1.0 so a too-aggressive boost can't blow out the mix or clip | |
| the export. | |
| """ | |
| volume: float = Field(..., ge=0.0, le=2.0) | |
| class EffectConfig(BaseModel): | |
| """A single effect in an effects chain.""" | |
| type: str | |
| enabled: bool = True | |
| params: dict = Field(default_factory=dict) | |
| class EffectsChain(BaseModel): | |
| """An ordered list of effects to apply.""" | |
| effects: List[EffectConfig] = Field(default_factory=list) | |
| class EffectPresetCreate(BaseModel): | |
| """Request model for creating an effect preset.""" | |
| name: str = Field(..., min_length=1, max_length=100) | |
| description: Optional[str] = Field(None, max_length=500) | |
| effects_chain: List[EffectConfig] | |
| class EffectPresetUpdate(BaseModel): | |
| """Request model for updating an effect preset.""" | |
| name: Optional[str] = Field(None, min_length=1, max_length=100) | |
| description: Optional[str] = None | |
| effects_chain: Optional[List[EffectConfig]] = None | |
| class EffectPresetResponse(BaseModel): | |
| """Response model for effect preset.""" | |
| id: str | |
| name: str | |
| description: Optional[str] = None | |
| effects_chain: List[EffectConfig] | |
| is_builtin: bool = False | |
| created_at: datetime | |
| class Config: | |
| from_attributes = True | |
| class GenerationVersionResponse(BaseModel): | |
| """Response model for a generation version.""" | |
| id: str | |
| generation_id: str | |
| label: str | |
| audio_path: str | |
| effects_chain: Optional[List[EffectConfig]] = None | |
| source_version_id: Optional[str] = None | |
| is_default: bool | |
| created_at: datetime | |
| class Config: | |
| from_attributes = True | |
| class ApplyEffectsRequest(BaseModel): | |
| """Request to apply effects to an existing generation.""" | |
| effects_chain: List[EffectConfig] | |
| source_version_id: Optional[str] = Field( | |
| None, description="Version to use as source audio (defaults to clean/original)" | |
| ) | |
| label: Optional[str] = Field(None, max_length=100, description="Label for this version (auto-generated if omitted)") | |
| set_as_default: bool = Field(default=True, description="Set this version as the default") | |
| class ProfileEffectsUpdate(BaseModel): | |
| """Request to update the default effects chain on a profile.""" | |
| effects_chain: Optional[List[EffectConfig]] = Field(None, description="Effects chain (null to remove)") | |
| class AvailableEffectParam(BaseModel): | |
| """Description of a single effect parameter.""" | |
| default: float | |
| min: float | |
| max: float | |
| step: float | |
| description: str | |
| class AvailableEffect(BaseModel): | |
| """Description of an available effect type.""" | |
| type: str | |
| label: str | |
| description: str | |
| params: dict # param_name -> AvailableEffectParam | |
| class AvailableEffectsResponse(BaseModel): | |
| """Response listing all available effect types.""" | |
| effects: List[AvailableEffect] | |