mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-19 06:40:38 -07:00
feat(capture): dictation, personalities, 0.5.0
Ships the Capture release end to end. Global-hotkey dictation with synthetic paste into the focused app on macOS and Windows, an on-screen pill across recording / transcribing / refining, customizable push-to- talk and toggle chords, and an accessibility-permission prompt scoped to Settings → Captures with inline re-check feedback. Voice profiles gain optional personalities that power compose / rewrite / respond actions via a local Qwen3 LLM — shared with refinement, so there is one local LLM in the app, not two. Refinement hardened with deterministic Whisper-loop collapse before the LLM sees the transcript, per-capture flag snapshots for re-runs, and a ten-transcript evaluation harness across every bundled refinement size. Version bump 0.4.5 → 0.5.0. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
ed2eec591a
commit
87c582ad54
@@ -20,6 +20,7 @@ class VoiceProfileCreate(BaseModel):
|
||||
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):
|
||||
@@ -36,6 +37,7 @@ class VoiceProfileResponse(BaseModel):
|
||||
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
|
||||
@@ -107,6 +109,7 @@ class GenerationResponse(BaseModel):
|
||||
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
|
||||
@@ -170,6 +173,196 @@ class TranscriptionResponse(BaseModel):
|
||||
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
|
||||
chord_push_to_talk_keys: List[str] = Field(default_factory=lambda: ["MetaRight", "AltGr"])
|
||||
chord_toggle_to_talk_keys: List[str] = Field(
|
||||
default_factory=lambda: ["MetaRight", "AltGr", "Space"]
|
||||
)
|
||||
|
||||
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
|
||||
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 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 endpoints ─────────────────────────────────────
|
||||
# compose / rewrite / respond return raw text; /speak chains LLM → TTS
|
||||
# and either persists as a generation (persist=true) or streams audio
|
||||
# back transiently.
|
||||
|
||||
|
||||
class PersonalityTextRequest(BaseModel):
|
||||
"""Body for ``/profiles/{id}/rewrite`` and ``/profiles/{id}/respond``."""
|
||||
|
||||
text: str = Field(..., min_length=1, max_length=10000)
|
||||
|
||||
|
||||
class PersonalityTextResponse(BaseModel):
|
||||
"""Response returned by compose / rewrite / respond endpoints."""
|
||||
|
||||
text: str
|
||||
model_size: str
|
||||
|
||||
|
||||
class PersonalitySpeakRequest(BaseModel):
|
||||
"""Body for ``/profiles/{id}/speak`` — LLM transform then TTS."""
|
||||
|
||||
text: str = Field(..., min_length=1, max_length=10000)
|
||||
# When true, the generated audio is persisted as a regular row in the
|
||||
# generations table (tagged with ``source="personality_speak"``) and
|
||||
# the response returns a GenerationResponse the client polls like any
|
||||
# other generation. When false, the LLM output is fed to a synchronous
|
||||
# TTS call and the wav bytes stream back directly.
|
||||
persist: bool = True
|
||||
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)$",
|
||||
)
|
||||
engine: Optional[str] = Field(
|
||||
None,
|
||||
pattern="^(qwen|qwen_custom_voice|luxtts|chatterbox|chatterbox_turbo|tada|kokoro)$",
|
||||
)
|
||||
# ``respond`` is the default because this endpoint is designed for
|
||||
# conversational / agent-style callers. Override to ``rewrite`` to
|
||||
# speak the user's text in character verbatim, or ``compose`` to
|
||||
# speak an utterance the character would come up with on its own
|
||||
# (in which case ``text`` is treated as a topical hint, not content).
|
||||
intent: str = Field(
|
||||
default="respond", pattern="^(respond|rewrite|compose)$"
|
||||
)
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
"""Response model for health check."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user