vibesurf 0.1.20__py3-none-any.whl → 0.1.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of vibesurf might be problematic. Click here for more details.

Files changed (43) hide show
  1. vibe_surf/_version.py +2 -2
  2. vibe_surf/agents/browser_use_agent.py +1 -1
  3. vibe_surf/agents/prompts/vibe_surf_prompt.py +1 -0
  4. vibe_surf/backend/api/task.py +1 -1
  5. vibe_surf/backend/api/voices.py +481 -0
  6. vibe_surf/backend/database/migrations/v004_add_voice_profiles.sql +35 -0
  7. vibe_surf/backend/database/models.py +38 -1
  8. vibe_surf/backend/database/queries.py +189 -1
  9. vibe_surf/backend/main.py +2 -0
  10. vibe_surf/backend/shared_state.py +1 -1
  11. vibe_surf/backend/voice_model_config.py +25 -0
  12. vibe_surf/browser/agen_browser_profile.py +2 -0
  13. vibe_surf/browser/agent_browser_session.py +5 -4
  14. vibe_surf/chrome_extension/background.js +271 -25
  15. vibe_surf/chrome_extension/content.js +147 -0
  16. vibe_surf/chrome_extension/permission-iframe.html +38 -0
  17. vibe_surf/chrome_extension/permission-request.html +104 -0
  18. vibe_surf/chrome_extension/scripts/api-client.js +61 -0
  19. vibe_surf/chrome_extension/scripts/file-manager.js +53 -12
  20. vibe_surf/chrome_extension/scripts/main.js +53 -12
  21. vibe_surf/chrome_extension/scripts/permission-iframe-request.js +188 -0
  22. vibe_surf/chrome_extension/scripts/permission-request.js +118 -0
  23. vibe_surf/chrome_extension/scripts/session-manager.js +30 -4
  24. vibe_surf/chrome_extension/scripts/settings-manager.js +690 -3
  25. vibe_surf/chrome_extension/scripts/ui-manager.js +961 -147
  26. vibe_surf/chrome_extension/scripts/user-settings-storage.js +422 -0
  27. vibe_surf/chrome_extension/scripts/voice-recorder.js +514 -0
  28. vibe_surf/chrome_extension/sidepanel.html +106 -29
  29. vibe_surf/chrome_extension/styles/components.css +35 -0
  30. vibe_surf/chrome_extension/styles/input.css +164 -1
  31. vibe_surf/chrome_extension/styles/layout.css +1 -1
  32. vibe_surf/chrome_extension/styles/settings-environment.css +138 -0
  33. vibe_surf/chrome_extension/styles/settings-forms.css +7 -7
  34. vibe_surf/chrome_extension/styles/variables.css +51 -0
  35. vibe_surf/tools/voice_asr.py +79 -8
  36. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/METADATA +9 -13
  37. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/RECORD +41 -34
  38. vibe_surf/chrome_extension/icons/convert-svg.js +0 -33
  39. vibe_surf/chrome_extension/icons/logo-preview.html +0 -187
  40. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/WHEEL +0 -0
  41. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/entry_points.txt +0 -0
  42. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/licenses/LICENSE +0 -0
  43. {vibesurf-0.1.20.dist-info → vibesurf-0.1.22.dist-info}/top_level.txt +0 -0
vibe_surf/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.20'
32
- __version_tuple__ = version_tuple = (0, 1, 20)
31
+ __version__ = version = '0.1.22'
32
+ __version_tuple__ = version_tuple = (0, 1, 22)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -525,7 +525,7 @@ class BrowserUseAgent(Agent):
525
525
  signal_handler.register()
526
526
 
527
527
  try:
528
- self._log_agent_run()
528
+ await self._log_agent_run()
529
529
 
530
530
  self.logger.debug(
531
531
  f'🔧 Agent setup: Task ID {self.task_id[-4:]}, Session ID {self.session_id[-4:]}, Browser Session ID {self.browser_session.id[-4:] if self.browser_session else "None"}'
@@ -86,4 +86,5 @@ EXTEND_BU_SYSTEM_PROMPT = """
86
86
  * Regarding file operations, please note that you need the full relative path (including subfolders), not just the file name.
87
87
  * Especially when a file operation reports an error, please reflect whether the file path is not written correctly, such as the subfolder is not written.
88
88
  * If you are operating on files in the filesystem, be sure to use relative paths (relative to the workspace dir) instead of absolute paths.
89
+ * If you are typing in the search box, please use Enter key to search instead of clicking.
89
90
  """
@@ -287,7 +287,7 @@ async def stop_task(control_request: TaskControlRequest):
287
287
  active_task["end_time"] = datetime.now()
288
288
 
289
289
  # Clear active task
290
- clear_active_task()
290
+ # clear_active_task()
291
291
 
292
292
  return {
293
293
  "success": True,
@@ -0,0 +1,481 @@
1
+ """
2
+ Tools API endpoints for VibeSurf Backend
3
+
4
+ Handles voice recognition and other tool-related operations.
5
+ """
6
+ import pdb
7
+
8
+ from fastapi import APIRouter, HTTPException, Depends, UploadFile, File
9
+ from fastapi.responses import JSONResponse
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+ from typing import Dict, List, Optional, Any
12
+ from pydantic import BaseModel
13
+ import os
14
+ import logging
15
+ from datetime import datetime
16
+
17
+ from vibe_surf.tools.voice_asr import QwenASR, OpenAIASR, GeminiASR
18
+
19
+ from ..database.manager import get_db_session
20
+ from ..database.queries import VoiceProfileQueries
21
+ from ..voice_model_config import VOICE_MODELS
22
+
23
+
24
+ router = APIRouter(prefix="/voices", tags=["voices"])
25
+
26
+ from vibe_surf.logger import get_logger
27
+
28
+ logger = get_logger(__name__)
29
+
30
+
31
+ # Pydantic models for request validation
32
+ class VoiceProfileCreate(BaseModel):
33
+ voice_profile_name: str
34
+ voice_model_type: str # "asr" or "tts"
35
+ voice_model_name: str
36
+ api_key: Optional[str] = None
37
+ voice_meta_params: Optional[Dict[str, Any]] = None
38
+ description: Optional[str] = None
39
+
40
+ class VoiceProfileUpdate(BaseModel):
41
+ voice_model_type: Optional[str] = None
42
+ voice_model_name: Optional[str] = None
43
+ api_key: Optional[str] = None
44
+ voice_meta_params: Optional[Dict[str, Any]] = None
45
+ description: Optional[str] = None
46
+ is_active: Optional[bool] = None
47
+
48
+
49
+ @router.post("/voice-profiles")
50
+ async def create_voice_profile(
51
+ profile_data: VoiceProfileCreate,
52
+ db: AsyncSession = Depends(get_db_session)
53
+ ):
54
+ """Create a new voice profile"""
55
+ try:
56
+ # Validate voice_model_type
57
+ if profile_data.voice_model_type not in ["asr", "tts"]:
58
+ raise HTTPException(
59
+ status_code=400,
60
+ detail="voice_model_type must be 'asr' or 'tts'"
61
+ )
62
+
63
+ # Check if profile name already exists
64
+ existing_profile = await VoiceProfileQueries.get_profile(db, profile_data.voice_profile_name)
65
+ if existing_profile:
66
+ raise HTTPException(
67
+ status_code=400,
68
+ detail=f"Voice profile '{profile_data.voice_profile_name}' already exists"
69
+ )
70
+
71
+ # Create the profile
72
+ created_profile = await VoiceProfileQueries.create_profile(
73
+ db=db,
74
+ voice_profile_name=profile_data.voice_profile_name,
75
+ voice_model_type=profile_data.voice_model_type,
76
+ voice_model_name=profile_data.voice_model_name,
77
+ api_key=profile_data.api_key,
78
+ voice_meta_params=profile_data.voice_meta_params,
79
+ description=profile_data.description
80
+ )
81
+
82
+ await db.commit()
83
+
84
+ return {
85
+ "success": True,
86
+ "message": f"Voice profile '{profile_data.voice_profile_name}' created successfully",
87
+ "profile": created_profile
88
+ }
89
+
90
+ except HTTPException:
91
+ raise
92
+ except Exception as e:
93
+ await db.rollback()
94
+ logger.error(f"Failed to create voice profile: {e}")
95
+ raise HTTPException(
96
+ status_code=500,
97
+ detail=f"Failed to create voice profile: {str(e)}"
98
+ )
99
+
100
+
101
+ @router.put("/voice-profiles/{voice_profile_name}")
102
+ async def update_voice_profile(
103
+ voice_profile_name: str,
104
+ profile_data: VoiceProfileUpdate,
105
+ db: AsyncSession = Depends(get_db_session)
106
+ ):
107
+ """Update an existing voice profile"""
108
+ try:
109
+ # Check if profile exists
110
+ existing_profile = await VoiceProfileQueries.get_profile(db, voice_profile_name)
111
+ if not existing_profile:
112
+ raise HTTPException(
113
+ status_code=404,
114
+ detail=f"Voice profile '{voice_profile_name}' not found"
115
+ )
116
+
117
+ # Validate voice_model_type if provided
118
+ if profile_data.voice_model_type and profile_data.voice_model_type not in ["asr", "tts"]:
119
+ raise HTTPException(
120
+ status_code=400,
121
+ detail="voice_model_type must be 'asr' or 'tts'"
122
+ )
123
+
124
+ # Prepare update data (exclude None values)
125
+ update_data = {}
126
+ for field, value in profile_data.dict(exclude_unset=True).items():
127
+ if value is not None:
128
+ update_data[field] = value
129
+
130
+ if not update_data:
131
+ raise HTTPException(
132
+ status_code=400,
133
+ detail="No valid fields provided for update"
134
+ )
135
+
136
+ # Update the profile
137
+ success = await VoiceProfileQueries.update_profile(
138
+ db=db,
139
+ voice_profile_name=voice_profile_name,
140
+ updates=update_data
141
+ )
142
+
143
+ if not success:
144
+ raise HTTPException(
145
+ status_code=500,
146
+ detail="Failed to update voice profile"
147
+ )
148
+
149
+ await db.commit()
150
+
151
+ # Get updated profile
152
+ updated_profile = await VoiceProfileQueries.get_profile(db, voice_profile_name)
153
+
154
+ return {
155
+ "success": True,
156
+ "message": f"Voice profile '{voice_profile_name}' updated successfully",
157
+ "profile": {
158
+ "profile_id": updated_profile.profile_id,
159
+ "voice_profile_name": updated_profile.voice_profile_name,
160
+ "voice_model_type": updated_profile.voice_model_type.value,
161
+ "voice_model_name": updated_profile.voice_model_name,
162
+ "voice_meta_params": updated_profile.voice_meta_params,
163
+ "description": updated_profile.description,
164
+ "is_active": updated_profile.is_active,
165
+ "created_at": updated_profile.created_at,
166
+ "updated_at": updated_profile.updated_at,
167
+ "last_used_at": updated_profile.last_used_at
168
+ }
169
+ }
170
+
171
+ except HTTPException:
172
+ raise
173
+ except Exception as e:
174
+ await db.rollback()
175
+ logger.error(f"Failed to update voice profile: {e}")
176
+ raise HTTPException(
177
+ status_code=500,
178
+ detail=f"Failed to update voice profile: {str(e)}"
179
+ )
180
+
181
+
182
+ @router.delete("/voice-profiles/{voice_profile_name}")
183
+ async def delete_voice_profile(
184
+ voice_profile_name: str,
185
+ db: AsyncSession = Depends(get_db_session)
186
+ ):
187
+ """Delete a voice profile"""
188
+ try:
189
+ # Check if profile exists
190
+ existing_profile = await VoiceProfileQueries.get_profile(db, voice_profile_name)
191
+ if not existing_profile:
192
+ raise HTTPException(
193
+ status_code=404,
194
+ detail=f"Voice profile '{voice_profile_name}' not found"
195
+ )
196
+
197
+ # Delete the profile
198
+ success = await VoiceProfileQueries.delete_profile(db, voice_profile_name)
199
+
200
+ if not success:
201
+ raise HTTPException(
202
+ status_code=500,
203
+ detail="Failed to delete voice profile"
204
+ )
205
+
206
+ await db.commit()
207
+
208
+ return {
209
+ "success": True,
210
+ "message": f"Voice profile '{voice_profile_name}' deleted successfully"
211
+ }
212
+
213
+ except HTTPException:
214
+ raise
215
+ except Exception as e:
216
+ await db.rollback()
217
+ logger.error(f"Failed to delete voice profile: {e}")
218
+ raise HTTPException(
219
+ status_code=500,
220
+ detail=f"Failed to delete voice profile: {str(e)}"
221
+ )
222
+
223
+
224
+ @router.post("/asr")
225
+ async def voice_recognition(
226
+ audio_file: UploadFile = File(...),
227
+ voice_profile_name: str = None,
228
+ db: AsyncSession = Depends(get_db_session)
229
+ ):
230
+ """
231
+ Voice recognition using specified voice profile
232
+
233
+ Args:
234
+ audio_file: Audio file to transcribe
235
+ voice_profile_name: Name of the voice profile to use (required)
236
+ db: Database session
237
+
238
+ Returns:
239
+ Dict with recognized text
240
+ """
241
+ from .. import shared_state
242
+ try:
243
+ # Validate required parameters
244
+ if not voice_profile_name:
245
+ raise HTTPException(
246
+ status_code=400,
247
+ detail="voice_profile_name parameter is required"
248
+ )
249
+
250
+ if not audio_file or not audio_file.filename:
251
+ raise HTTPException(
252
+ status_code=400,
253
+ detail="audio_file is required and must have a filename"
254
+ )
255
+
256
+ # Log the incoming request for debugging
257
+ logger.info(f"ASR request: voice_profile_name='{voice_profile_name}', audio_file='{audio_file.filename}', size={audio_file.size if hasattr(audio_file, 'size') else 'unknown'}")
258
+
259
+ # Get voice profile with decrypted API key
260
+ profile_data = await VoiceProfileQueries.get_profile_with_decrypted_key(db, voice_profile_name)
261
+ if not profile_data:
262
+ raise HTTPException(
263
+ status_code=404,
264
+ detail=f"Voice profile '{voice_profile_name}' not found"
265
+ )
266
+
267
+ # Check if profile is active
268
+ if not profile_data.get("is_active"):
269
+ raise HTTPException(
270
+ status_code=400,
271
+ detail=f"Voice profile '{voice_profile_name}' is inactive"
272
+ )
273
+
274
+ # Check if profile is for ASR
275
+ if profile_data.get("voice_model_type") != "asr":
276
+ raise HTTPException(
277
+ status_code=400,
278
+ detail=f"Voice profile '{voice_profile_name}' is not an ASR profile"
279
+ )
280
+
281
+ # Get model configuration
282
+ voice_model_name = profile_data.get("voice_model_name")
283
+ model_config = VOICE_MODELS.get(voice_model_name)
284
+ if not model_config:
285
+ raise HTTPException(
286
+ status_code=400,
287
+ detail=f"Voice model '{voice_model_name}' is not supported"
288
+ )
289
+
290
+ # Save uploaded file permanently in workspace_dir/audios/
291
+ saved_file_path = None
292
+ try:
293
+ # Get workspace directory
294
+ workspace_dir = shared_state.workspace_dir
295
+ if not workspace_dir:
296
+ raise HTTPException(
297
+ status_code=500,
298
+ detail="Workspace directory not configured"
299
+ )
300
+
301
+ # Create audios directory if it doesn't exist
302
+ audios_dir = os.path.join(workspace_dir, "audios")
303
+ os.makedirs(audios_dir, exist_ok=True)
304
+
305
+ # Generate timestamp-based filename
306
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3] # microseconds to milliseconds
307
+ file_extension = ".wav" # Default to wav
308
+ if audio_file.filename:
309
+ original_ext = os.path.splitext(audio_file.filename)[1]
310
+ if original_ext:
311
+ file_extension = original_ext
312
+
313
+ saved_filename = f"asr-{timestamp}{file_extension}"
314
+ saved_file_path = os.path.join(audios_dir, saved_filename)
315
+
316
+ # Save the audio file
317
+ content = await audio_file.read()
318
+ with open(saved_file_path, "wb") as f:
319
+ f.write(content)
320
+
321
+ # Initialize ASR
322
+ api_key = profile_data.get("api_key")
323
+ voice_meta_params = profile_data.get("voice_meta_params", {})
324
+ asr_model_name = voice_meta_params.get("asr_model_name", "")
325
+ recognized_text = ""
326
+ if voice_model_name == "qwen-asr":
327
+ asr = QwenASR(model=asr_model_name, api_key=api_key)
328
+ recognized_text = asr.asr(wav_url=saved_file_path)
329
+ elif voice_model_name == "openai-asr":
330
+ # Support custom base_url for OpenAI
331
+ base_url = voice_meta_params.get("base_url")
332
+ asr = OpenAIASR(model=asr_model_name, api_key=api_key, base_url=base_url)
333
+ recognized_text = asr.asr(wav_url=saved_file_path)
334
+ elif voice_model_name == "gemini-asr":
335
+ asr = GeminiASR(model=asr_model_name, api_key=api_key)
336
+ recognized_text = asr.asr(wav_url=saved_file_path)
337
+ else:
338
+ raise HTTPException(
339
+ status_code=400,
340
+ detail=f"Voice model '{voice_model_name}' is not supported"
341
+ )
342
+ logger.debug(f"Recognized text: {recognized_text}")
343
+ # Update last used timestamp
344
+ await VoiceProfileQueries.update_last_used(db, voice_profile_name)
345
+ await db.commit()
346
+
347
+ return {
348
+ "success": True,
349
+ "voice_profile_name": voice_profile_name,
350
+ "voice_model_name": voice_model_name,
351
+ "recognized_text": recognized_text,
352
+ "filename": audio_file.filename,
353
+ "saved_audio_path": saved_file_path
354
+ }
355
+
356
+ except Exception as e:
357
+ # If there's an error, we might want to clean up the saved file
358
+ if saved_file_path and os.path.exists(saved_file_path):
359
+ try:
360
+ os.unlink(saved_file_path)
361
+ except:
362
+ pass # Ignore cleanup errors
363
+ raise e
364
+
365
+ except HTTPException:
366
+ raise
367
+ except Exception as e:
368
+ logger.error(f"Failed to perform voice recognition: {e}")
369
+ raise HTTPException(
370
+ status_code=500,
371
+ detail=f"Voice recognition failed: {str(e)}"
372
+ )
373
+
374
+
375
+ @router.get("/voice-profiles")
376
+ async def list_voice_profiles(
377
+ voice_model_type: Optional[str] = None,
378
+ active_only: bool = True,
379
+ limit: int = 50,
380
+ offset: int = 0,
381
+ db: AsyncSession = Depends(get_db_session)
382
+ ):
383
+ """List voice profiles"""
384
+ try:
385
+ profiles = await VoiceProfileQueries.list_profiles(
386
+ db=db,
387
+ voice_model_type=voice_model_type,
388
+ active_only=active_only,
389
+ limit=limit,
390
+ offset=offset
391
+ )
392
+
393
+ profile_list = []
394
+ for profile in profiles:
395
+ profile_data = {
396
+ "profile_id": profile.profile_id,
397
+ "voice_profile_name": profile.voice_profile_name,
398
+ "voice_model_type": profile.voice_model_type.value,
399
+ "voice_model_name": profile.voice_model_name,
400
+ "voice_meta_params": profile.voice_meta_params,
401
+ "description": profile.description,
402
+ "is_active": profile.is_active,
403
+ "created_at": profile.created_at,
404
+ "updated_at": profile.updated_at,
405
+ "last_used_at": profile.last_used_at
406
+ }
407
+ profile_list.append(profile_data)
408
+
409
+ return {
410
+ "profiles": profile_list,
411
+ "total": len(profile_list),
412
+ "voice_model_type": voice_model_type,
413
+ "active_only": active_only
414
+ }
415
+
416
+ except Exception as e:
417
+ logger.error(f"Failed to list voice profiles: {e}")
418
+ raise HTTPException(
419
+ status_code=500,
420
+ detail=f"Failed to list voice profiles: {str(e)}"
421
+ )
422
+
423
+
424
+ @router.get("/models")
425
+ async def get_available_voice_models(model_type: Optional[str] = None):
426
+ """Get list of all available voice models"""
427
+ models = []
428
+ for model_name, config in VOICE_MODELS.items():
429
+ # Filter by model_type if provided
430
+ config_model_type = config.get("model_type", "asr")
431
+ if model_type and config_model_type != model_type:
432
+ continue
433
+
434
+ model_info = {
435
+ "model_name": model_name,
436
+ "model_type": config_model_type,
437
+ "requires_api_key": config.get("requires_api_key", True)
438
+ }
439
+ models.append(model_info)
440
+
441
+ return {
442
+ "models": models,
443
+ "total_models": len(models)
444
+ }
445
+
446
+
447
+ @router.get("/{voice_profile_name}")
448
+ async def get_voice_profile(
449
+ voice_profile_name: str,
450
+ db: AsyncSession = Depends(get_db_session)
451
+ ):
452
+ """Get specific voice profile by name (without API key)"""
453
+ try:
454
+ profile = await VoiceProfileQueries.get_profile(db, voice_profile_name)
455
+ if not profile:
456
+ raise HTTPException(
457
+ status_code=404,
458
+ detail=f"Voice profile '{voice_profile_name}' not found"
459
+ )
460
+
461
+ return {
462
+ "profile_id": profile.profile_id,
463
+ "voice_profile_name": profile.voice_profile_name,
464
+ "voice_model_type": profile.voice_model_type.value,
465
+ "voice_model_name": profile.voice_model_name,
466
+ "voice_meta_params": profile.voice_meta_params,
467
+ "description": profile.description,
468
+ "is_active": profile.is_active,
469
+ "created_at": profile.created_at,
470
+ "updated_at": profile.updated_at,
471
+ "last_used_at": profile.last_used_at
472
+ }
473
+
474
+ except HTTPException:
475
+ raise
476
+ except Exception as e:
477
+ logger.error(f"Failed to get voice profile: {e}")
478
+ raise HTTPException(
479
+ status_code=500,
480
+ detail=f"Failed to get voice profile: {str(e)}"
481
+ )
@@ -0,0 +1,35 @@
1
+ -- Migration: v004_add_voice_profiles.sql
2
+ -- Description: Add voice_profiles table for voice model management
3
+ -- Version: 0.0.4
4
+
5
+ -- Enable foreign keys
6
+ PRAGMA foreign_keys = ON;
7
+
8
+ -- Create Voice Profiles table
9
+ CREATE TABLE IF NOT EXISTS voice_profiles (
10
+ profile_id VARCHAR(36) NOT NULL PRIMARY KEY,
11
+ voice_profile_name VARCHAR(100) NOT NULL UNIQUE,
12
+ voice_model_type VARCHAR(3) NOT NULL,
13
+ voice_model_name VARCHAR(100) NOT NULL,
14
+ encrypted_api_key TEXT,
15
+ voice_meta_params JSON,
16
+ description TEXT,
17
+ is_active BOOLEAN NOT NULL DEFAULT 1,
18
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
20
+ last_used_at DATETIME,
21
+ CHECK (voice_model_type IN ('asr', 'tts'))
22
+ );
23
+
24
+ -- Create indexes for voice profiles
25
+ CREATE INDEX IF NOT EXISTS idx_voice_profiles_name ON voice_profiles(voice_profile_name);
26
+ CREATE INDEX IF NOT EXISTS idx_voice_profiles_type ON voice_profiles(voice_model_type);
27
+ CREATE INDEX IF NOT EXISTS idx_voice_profiles_active ON voice_profiles(is_active);
28
+
29
+ -- Create trigger for automatic timestamp updates
30
+ CREATE TRIGGER IF NOT EXISTS update_voice_profiles_updated_at
31
+ AFTER UPDATE ON voice_profiles
32
+ FOR EACH ROW
33
+ BEGIN
34
+ UPDATE voice_profiles SET updated_at = CURRENT_TIMESTAMP WHERE profile_id = OLD.profile_id;
35
+ END;
@@ -22,6 +22,38 @@ class TaskStatus(enum.Enum):
22
22
  FAILED = "failed"
23
23
  STOPPED = "stopped"
24
24
 
25
+ class VoiceModelType(enum.Enum):
26
+ ASR = "asr"
27
+ TTS = "tts"
28
+
29
+ class VoiceProfile(Base):
30
+ """Voice Profile model for managing voice model configurations with encrypted API keys"""
31
+ __tablename__ = 'voice_profiles'
32
+
33
+ # Primary identifier
34
+ profile_id = Column(String(36), primary_key=True, default=lambda: str(uuid4()))
35
+ voice_profile_name = Column(String(100), nullable=False, unique=True) # User-defined unique name
36
+
37
+ # Voice Model Configuration
38
+ voice_model_type = Column(Enum(VoiceModelType, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # asr or tts
39
+ voice_model_name = Column(String(100), nullable=False)
40
+ encrypted_api_key = Column(Text, nullable=True) # Encrypted API key using MAC address
41
+
42
+ # Voice model parameters (stored as JSON to allow flexibility)
43
+ voice_meta_params = Column(JSON, nullable=True) # Model-specific parameters
44
+
45
+ # Profile metadata
46
+ description = Column(Text, nullable=True)
47
+ is_active = Column(Boolean, default=True, nullable=False)
48
+
49
+ # Timestamps
50
+ created_at = Column(DateTime, nullable=False, default=func.now())
51
+ updated_at = Column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
52
+ last_used_at = Column(DateTime, nullable=True)
53
+
54
+ def __repr__(self):
55
+ return f"<VoiceProfile(voice_profile_name={self.voice_profile_name}, voice_model_name={self.voice_model_name}, type={self.voice_model_type.value})>"
56
+
25
57
  class LLMProfile(Base):
26
58
  """LLM Profile model for managing LLM configurations with encrypted API keys"""
27
59
  __tablename__ = 'llm_profiles'
@@ -164,4 +196,9 @@ Index('idx_uploaded_files_filename', UploadedFile.original_filename)
164
196
  # MCP Profile indexes
165
197
  Index('idx_mcp_profiles_display_name', McpProfile.display_name)
166
198
  Index('idx_mcp_profiles_server_name', McpProfile.mcp_server_name)
167
- Index('idx_mcp_profiles_active', McpProfile.is_active)
199
+ Index('idx_mcp_profiles_active', McpProfile.is_active)
200
+
201
+ # Voice Profile indexes
202
+ Index('idx_voice_profiles_name', VoiceProfile.voice_profile_name)
203
+ Index('idx_voice_profiles_type', VoiceProfile.voice_model_type)
204
+ Index('idx_voice_profiles_active', VoiceProfile.is_active)