Update voice libary key form
This commit is contained in:
@@ -28,6 +28,8 @@ class Voice(Base):
|
|||||||
description: Mapped[str] = mapped_column(String(255), nullable=False)
|
description: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||||
model: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商语音模型标识
|
model: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商语音模型标识
|
||||||
voice_key: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商voice_key
|
voice_key: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商voice_key
|
||||||
|
api_key: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) # 每个声音独立 API key
|
||||||
|
base_url: Mapped[Optional[str]] = mapped_column(String(512), nullable=True) # 每个声音独立 OpenAI-compatible base_url
|
||||||
speed: Mapped[float] = mapped_column(Float, default=1.0)
|
speed: Mapped[float] = mapped_column(Float, default=1.0)
|
||||||
gain: Mapped[int] = mapped_column(Integer, default=0)
|
gain: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
pitch: Mapped[int] = mapped_column(Integer, default=0)
|
pitch: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
@@ -38,17 +40,6 @@ class Voice(Base):
|
|||||||
user = relationship("User", foreign_keys=[user_id])
|
user = relationship("User", foreign_keys=[user_id])
|
||||||
|
|
||||||
|
|
||||||
class VendorCredential(Base):
|
|
||||||
__tablename__ = "vendor_credentials"
|
|
||||||
|
|
||||||
vendor_key: Mapped[str] = mapped_column(String(64), primary_key=True)
|
|
||||||
vendor_name: Mapped[str] = mapped_column(String(128), nullable=False)
|
|
||||||
api_key: Mapped[str] = mapped_column(String(512), nullable=False)
|
|
||||||
base_url: Mapped[Optional[str]] = mapped_column(String(512), nullable=True)
|
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
||||||
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
||||||
|
|
||||||
|
|
||||||
# ============ LLM Model ============
|
# ============ LLM Model ============
|
||||||
class LLMModel(Base):
|
class LLMModel(Base):
|
||||||
__tablename__ = "llm_models"
|
__tablename__ = "llm_models"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -9,16 +8,8 @@ from fastapi import APIRouter, Depends, HTTPException
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..db import get_db
|
from ..db import get_db
|
||||||
from ..models import VendorCredential, Voice
|
from ..models import Voice
|
||||||
from ..schemas import (
|
from ..schemas import VoiceCreate, VoiceOut, VoicePreviewRequest, VoicePreviewResponse, VoiceUpdate
|
||||||
VendorCredentialOut,
|
|
||||||
VendorCredentialUpsert,
|
|
||||||
VoiceCreate,
|
|
||||||
VoiceOut,
|
|
||||||
VoicePreviewRequest,
|
|
||||||
VoicePreviewResponse,
|
|
||||||
VoiceUpdate,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/voices", tags=["Voices"])
|
router = APIRouter(prefix="/voices", tags=["Voices"])
|
||||||
|
|
||||||
@@ -29,28 +20,10 @@ def _is_siliconflow_vendor(vendor: str) -> bool:
|
|||||||
return vendor.strip().lower() in {"siliconflow", "硅基流动"}
|
return vendor.strip().lower() in {"siliconflow", "硅基流动"}
|
||||||
|
|
||||||
|
|
||||||
def _canonical_vendor_key(vendor: str) -> str:
|
def _default_base_url(vendor: str) -> Optional[str]:
|
||||||
normalized = vendor.strip().lower()
|
if _is_siliconflow_vendor(vendor):
|
||||||
alias_map = {
|
return "https://api.siliconflow.cn/v1"
|
||||||
"硅基流动": "siliconflow",
|
return None
|
||||||
"siliconflow": "siliconflow",
|
|
||||||
"ali": "ali",
|
|
||||||
"volcano": "volcano",
|
|
||||||
"minimax": "minimax",
|
|
||||||
}
|
|
||||||
return alias_map.get(normalized, normalized)
|
|
||||||
|
|
||||||
|
|
||||||
def _default_tts_base_url(vendor_key: str) -> Optional[str]:
|
|
||||||
defaults = {
|
|
||||||
"siliconflow": "https://api.siliconflow.cn/v1",
|
|
||||||
}
|
|
||||||
return defaults.get(vendor_key)
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_vendor_credential(db: Session, vendor: str) -> Optional[VendorCredential]:
|
|
||||||
vendor_key = _canonical_vendor_key(vendor)
|
|
||||||
return db.query(VendorCredential).filter(VendorCredential.vendor_key == vendor_key).first()
|
|
||||||
|
|
||||||
|
|
||||||
def _build_siliconflow_voice_key(voice: Voice, model: str) -> str:
|
def _build_siliconflow_voice_key(voice: Voice, model: str) -> str:
|
||||||
@@ -108,6 +81,8 @@ def create_voice(data: VoiceCreate, db: Session = Depends(get_db)):
|
|||||||
description=data.description,
|
description=data.description,
|
||||||
model=model,
|
model=model,
|
||||||
voice_key=voice_key,
|
voice_key=voice_key,
|
||||||
|
api_key=data.api_key,
|
||||||
|
base_url=data.base_url,
|
||||||
speed=data.speed,
|
speed=data.speed,
|
||||||
gain=data.gain,
|
gain=data.gain,
|
||||||
pitch=data.pitch,
|
pitch=data.pitch,
|
||||||
@@ -165,56 +140,6 @@ def delete_voice(id: str, db: Session = Depends(get_db)):
|
|||||||
return {"message": "Deleted successfully"}
|
return {"message": "Deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/vendors/credentials")
|
|
||||||
def list_vendor_credentials(db: Session = Depends(get_db)):
|
|
||||||
items = db.query(VendorCredential).order_by(VendorCredential.updated_at.desc()).all()
|
|
||||||
return {"list": items, "total": len(items)}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/vendors/credentials/{vendor_key}", response_model=VendorCredentialOut)
|
|
||||||
def get_vendor_credential(vendor_key: str, db: Session = Depends(get_db)):
|
|
||||||
key = _canonical_vendor_key(vendor_key)
|
|
||||||
item = db.query(VendorCredential).filter(VendorCredential.vendor_key == key).first()
|
|
||||||
if not item:
|
|
||||||
raise HTTPException(status_code=404, detail="Vendor credential not found")
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/vendors/credentials/{vendor_key}", response_model=VendorCredentialOut)
|
|
||||||
def upsert_vendor_credential(vendor_key: str, data: VendorCredentialUpsert, db: Session = Depends(get_db)):
|
|
||||||
key = _canonical_vendor_key(vendor_key)
|
|
||||||
item = db.query(VendorCredential).filter(VendorCredential.vendor_key == key).first()
|
|
||||||
|
|
||||||
if item:
|
|
||||||
item.vendor_name = data.vendor_name or item.vendor_name
|
|
||||||
item.api_key = data.api_key
|
|
||||||
item.base_url = data.base_url
|
|
||||||
item.updated_at = datetime.utcnow()
|
|
||||||
else:
|
|
||||||
item = VendorCredential(
|
|
||||||
vendor_key=key,
|
|
||||||
vendor_name=data.vendor_name or vendor_key,
|
|
||||||
api_key=data.api_key,
|
|
||||||
base_url=data.base_url,
|
|
||||||
)
|
|
||||||
db.add(item)
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
db.refresh(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/vendors/credentials/{vendor_key}")
|
|
||||||
def delete_vendor_credential(vendor_key: str, db: Session = Depends(get_db)):
|
|
||||||
key = _canonical_vendor_key(vendor_key)
|
|
||||||
item = db.query(VendorCredential).filter(VendorCredential.vendor_key == key).first()
|
|
||||||
if not item:
|
|
||||||
raise HTTPException(status_code=404, detail="Vendor credential not found")
|
|
||||||
db.delete(item)
|
|
||||||
db.commit()
|
|
||||||
return {"message": "Deleted successfully"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{id}/preview", response_model=VoicePreviewResponse)
|
@router.post("/{id}/preview", response_model=VoicePreviewResponse)
|
||||||
def preview_voice(id: str, data: VoicePreviewRequest, db: Session = Depends(get_db)):
|
def preview_voice(id: str, data: VoicePreviewRequest, db: Session = Depends(get_db)):
|
||||||
"""试听指定声音,基于 OpenAI-compatible /audio/speech 接口。"""
|
"""试听指定声音,基于 OpenAI-compatible /audio/speech 接口。"""
|
||||||
@@ -226,22 +151,17 @@ def preview_voice(id: str, data: VoicePreviewRequest, db: Session = Depends(get_
|
|||||||
if not text:
|
if not text:
|
||||||
raise HTTPException(status_code=400, detail="Preview text cannot be empty")
|
raise HTTPException(status_code=400, detail="Preview text cannot be empty")
|
||||||
|
|
||||||
credential = _resolve_vendor_credential(db, voice.vendor)
|
api_key = (data.api_key or "").strip() or (voice.api_key or "").strip()
|
||||||
api_key = (data.api_key or "").strip()
|
if not api_key and _is_siliconflow_vendor(voice.vendor):
|
||||||
if not api_key and credential:
|
api_key = os.getenv("SILICONFLOW_API_KEY", "").strip()
|
||||||
api_key = credential.api_key
|
|
||||||
if not api_key:
|
if not api_key:
|
||||||
api_key = os.getenv("SILICONFLOW_API_KEY") if _is_siliconflow_vendor(voice.vendor) else ""
|
raise HTTPException(status_code=400, detail=f"API key is required for voice: {voice.name}")
|
||||||
if not api_key:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Vendor API key is required for {voice.vendor}")
|
base_url = (voice.base_url or "").strip() or (_default_base_url(voice.vendor) or "")
|
||||||
|
if not base_url:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Base URL is required for voice: {voice.name}")
|
||||||
|
|
||||||
model = voice.model or SILICONFLOW_DEFAULT_MODEL
|
model = voice.model or SILICONFLOW_DEFAULT_MODEL
|
||||||
vendor_key = _canonical_vendor_key(voice.vendor)
|
|
||||||
base_url = (credential.base_url.strip() if credential and credential.base_url else "") or _default_tts_base_url(vendor_key)
|
|
||||||
if not base_url:
|
|
||||||
raise HTTPException(status_code=400, detail=f"Vendor base_url is required for {voice.vendor}")
|
|
||||||
tts_api_url = f"{base_url.rstrip('/')}/audio/speech"
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"input": text,
|
"input": text,
|
||||||
@@ -253,7 +173,7 @@ def preview_voice(id: str, data: VoicePreviewRequest, db: Session = Depends(get_
|
|||||||
try:
|
try:
|
||||||
with httpx.Client(timeout=45.0) as client:
|
with httpx.Client(timeout=45.0) as client:
|
||||||
response = client.post(
|
response = client.post(
|
||||||
tts_api_url,
|
f"{base_url.rstrip('/')}/audio/speech",
|
||||||
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
||||||
json=payload,
|
json=payload,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ class VoiceCreate(VoiceBase):
|
|||||||
id: Optional[str] = None
|
id: Optional[str] = None
|
||||||
model: Optional[str] = None # 厂商语音模型标识
|
model: Optional[str] = None # 厂商语音模型标识
|
||||||
voice_key: Optional[str] = None # 厂商voice_key
|
voice_key: Optional[str] = None # 厂商voice_key
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
base_url: Optional[str] = None
|
||||||
speed: float = 1.0
|
speed: float = 1.0
|
||||||
gain: int = 0
|
gain: int = 0
|
||||||
pitch: int = 0
|
pitch: int = 0
|
||||||
@@ -67,6 +69,8 @@ class VoiceUpdate(BaseModel):
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
voice_key: Optional[str] = None
|
voice_key: Optional[str] = None
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
base_url: Optional[str] = None
|
||||||
speed: Optional[float] = None
|
speed: Optional[float] = None
|
||||||
gain: Optional[int] = None
|
gain: Optional[int] = None
|
||||||
pitch: Optional[int] = None
|
pitch: Optional[int] = None
|
||||||
@@ -78,6 +82,8 @@ class VoiceOut(VoiceBase):
|
|||||||
user_id: Optional[int] = None
|
user_id: Optional[int] = None
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
voice_key: Optional[str] = None
|
voice_key: Optional[str] = None
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
base_url: Optional[str] = None
|
||||||
speed: float = 1.0
|
speed: float = 1.0
|
||||||
gain: int = 0
|
gain: int = 0
|
||||||
pitch: int = 0
|
pitch: int = 0
|
||||||
@@ -104,24 +110,6 @@ class VoicePreviewResponse(BaseModel):
|
|||||||
error: Optional[str] = None
|
error: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class VendorCredentialUpsert(BaseModel):
|
|
||||||
vendor_name: Optional[str] = None
|
|
||||||
api_key: str
|
|
||||||
base_url: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class VendorCredentialOut(BaseModel):
|
|
||||||
vendor_key: str
|
|
||||||
vendor_name: str
|
|
||||||
api_key: str
|
|
||||||
base_url: Optional[str] = None
|
|
||||||
created_at: Optional[datetime] = None
|
|
||||||
updated_at: Optional[datetime] = None
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
# ============ LLM Model ============
|
# ============ LLM Model ============
|
||||||
class LLMModelBase(BaseModel):
|
class LLMModelBase(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
|
|||||||
@@ -180,11 +180,12 @@ class TestVoiceAPI:
|
|||||||
encoded = payload["audio_url"].split(",", 1)[1]
|
encoded = payload["audio_url"].split(",", 1)[1]
|
||||||
assert base64.b64decode(encoded) == b"fake-mp3-bytes"
|
assert base64.b64decode(encoded) == b"fake-mp3-bytes"
|
||||||
|
|
||||||
def test_vendor_credential_persist_and_preview_use_db_key(self, client, monkeypatch):
|
def test_voice_credential_persist_and_preview_use_voice_key(self, client, monkeypatch):
|
||||||
"""Test vendor credential persisted in DB and used by preview endpoint"""
|
"""Test per-voice api_key/base_url persisted and used by preview endpoint"""
|
||||||
from app.routers import voices as voice_router
|
from app.routers import voices as voice_router
|
||||||
|
|
||||||
captured_auth = {"value": ""}
|
captured_auth = {"value": ""}
|
||||||
|
captured_url = {"value": ""}
|
||||||
|
|
||||||
class DummyResponse:
|
class DummyResponse:
|
||||||
status_code = 200
|
status_code = 200
|
||||||
@@ -207,22 +208,13 @@ class TestVoiceAPI:
|
|||||||
def post(self, *args, **kwargs):
|
def post(self, *args, **kwargs):
|
||||||
headers = kwargs.get("headers", {})
|
headers = kwargs.get("headers", {})
|
||||||
captured_auth["value"] = headers.get("Authorization", "")
|
captured_auth["value"] = headers.get("Authorization", "")
|
||||||
|
if args:
|
||||||
|
captured_url["value"] = args[0]
|
||||||
return DummyResponse()
|
return DummyResponse()
|
||||||
|
|
||||||
monkeypatch.delenv("SILICONFLOW_API_KEY", raising=False)
|
monkeypatch.delenv("SILICONFLOW_API_KEY", raising=False)
|
||||||
monkeypatch.setattr(voice_router.httpx, "Client", DummyClient)
|
monkeypatch.setattr(voice_router.httpx, "Client", DummyClient)
|
||||||
|
|
||||||
save_cred = client.put(
|
|
||||||
"/api/voices/vendors/credentials/siliconflow",
|
|
||||||
json={
|
|
||||||
"vendor_name": "SiliconFlow",
|
|
||||||
"api_key": "db-key-123",
|
|
||||||
"base_url": "https://api.siliconflow.cn/v1"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
assert save_cred.status_code == 200
|
|
||||||
assert save_cred.json()["vendor_key"] == "siliconflow"
|
|
||||||
|
|
||||||
create_resp = client.post("/api/voices", json={
|
create_resp = client.post("/api/voices", json={
|
||||||
"id": "anna2",
|
"id": "anna2",
|
||||||
"name": "Anna 2",
|
"name": "Anna 2",
|
||||||
@@ -231,10 +223,13 @@ class TestVoiceAPI:
|
|||||||
"language": "zh",
|
"language": "zh",
|
||||||
"description": "voice",
|
"description": "voice",
|
||||||
"model": "FunAudioLLM/CosyVoice2-0.5B",
|
"model": "FunAudioLLM/CosyVoice2-0.5B",
|
||||||
"voice_key": "FunAudioLLM/CosyVoice2-0.5B:anna"
|
"voice_key": "FunAudioLLM/CosyVoice2-0.5B:anna",
|
||||||
|
"api_key": "voice-key-123",
|
||||||
|
"base_url": "https://api.siliconflow.cn/v1"
|
||||||
})
|
})
|
||||||
assert create_resp.status_code == 200
|
assert create_resp.status_code == 200
|
||||||
|
|
||||||
preview_resp = client.post("/api/voices/anna2/preview", json={"text": "hello"})
|
preview_resp = client.post("/api/voices/anna2/preview", json={"text": "hello"})
|
||||||
assert preview_resp.status_code == 200
|
assert preview_resp.status_code == 200
|
||||||
assert captured_auth["value"] == "Bearer db-key-123"
|
assert captured_auth["value"] == "Bearer voice-key-123"
|
||||||
|
assert captured_url["value"] == "https://api.siliconflow.cn/v1/audio/speech"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { Assistant, CallLog, InteractionDetail, KnowledgeBase, KnowledgeDocument, VendorCredential, Voice, Workflow, WorkflowEdge, WorkflowNode } from '../types';
|
import { Assistant, CallLog, InteractionDetail, KnowledgeBase, KnowledgeDocument, Voice, Workflow, WorkflowEdge, WorkflowNode } from '../types';
|
||||||
import { apiRequest } from './apiClient';
|
import { apiRequest } from './apiClient';
|
||||||
|
|
||||||
type AnyRecord = Record<string, any>;
|
type AnyRecord = Record<string, any>;
|
||||||
@@ -55,6 +55,8 @@ const mapVoice = (raw: AnyRecord): Voice => ({
|
|||||||
description: readField(raw, ['description'], ''),
|
description: readField(raw, ['description'], ''),
|
||||||
model: readField(raw, ['model'], ''),
|
model: readField(raw, ['model'], ''),
|
||||||
voiceKey: readField(raw, ['voiceKey', 'voice_key'], ''),
|
voiceKey: readField(raw, ['voiceKey', 'voice_key'], ''),
|
||||||
|
apiKey: readField(raw, ['apiKey', 'api_key'], ''),
|
||||||
|
baseUrl: readField(raw, ['baseUrl', 'base_url'], ''),
|
||||||
speed: Number(readField(raw, ['speed'], 1)),
|
speed: Number(readField(raw, ['speed'], 1)),
|
||||||
gain: Number(readField(raw, ['gain'], 0)),
|
gain: Number(readField(raw, ['gain'], 0)),
|
||||||
pitch: Number(readField(raw, ['pitch'], 0)),
|
pitch: Number(readField(raw, ['pitch'], 0)),
|
||||||
@@ -62,13 +64,6 @@ const mapVoice = (raw: AnyRecord): Voice => ({
|
|||||||
isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)),
|
isSystem: Boolean(readField(raw, ['isSystem', 'is_system'], false)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapVendorCredential = (raw: AnyRecord): VendorCredential => ({
|
|
||||||
vendorKey: String(readField(raw, ['vendorKey', 'vendor_key'], '')),
|
|
||||||
vendorName: readField(raw, ['vendorName', 'vendor_name'], ''),
|
|
||||||
apiKey: readField(raw, ['apiKey', 'api_key'], ''),
|
|
||||||
baseUrl: readField(raw, ['baseUrl', 'base_url'], ''),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapWorkflowNode = (raw: AnyRecord): WorkflowNode => ({
|
const mapWorkflowNode = (raw: AnyRecord): WorkflowNode => ({
|
||||||
name: readField(raw, ['name'], ''),
|
name: readField(raw, ['name'], ''),
|
||||||
type: readField(raw, ['type'], 'conversation') as 'conversation' | 'tool' | 'human' | 'end',
|
type: readField(raw, ['type'], 'conversation') as 'conversation' | 'tool' | 'human' | 'end',
|
||||||
@@ -205,6 +200,8 @@ export const createVoice = async (data: Partial<Voice>): Promise<Voice> => {
|
|||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
model: data.model || undefined,
|
model: data.model || undefined,
|
||||||
voice_key: data.voiceKey || undefined,
|
voice_key: data.voiceKey || undefined,
|
||||||
|
api_key: data.apiKey || undefined,
|
||||||
|
base_url: data.baseUrl || undefined,
|
||||||
speed: data.speed ?? 1,
|
speed: data.speed ?? 1,
|
||||||
gain: data.gain ?? 0,
|
gain: data.gain ?? 0,
|
||||||
pitch: data.pitch ?? 0,
|
pitch: data.pitch ?? 0,
|
||||||
@@ -223,6 +220,8 @@ export const updateVoice = async (id: string, data: Partial<Voice>): Promise<Voi
|
|||||||
description: data.description,
|
description: data.description,
|
||||||
model: data.model,
|
model: data.model,
|
||||||
voice_key: data.voiceKey,
|
voice_key: data.voiceKey,
|
||||||
|
api_key: data.apiKey,
|
||||||
|
base_url: data.baseUrl,
|
||||||
speed: data.speed,
|
speed: data.speed,
|
||||||
gain: data.gain,
|
gain: data.gain,
|
||||||
pitch: data.pitch,
|
pitch: data.pitch,
|
||||||
@@ -247,24 +246,6 @@ export const previewVoice = async (id: string, text: string, speed?: number, api
|
|||||||
return response.audio_url;
|
return response.audio_url;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchVendorCredentials = async (): Promise<VendorCredential[]> => {
|
|
||||||
const response = await apiRequest<{ list?: AnyRecord[] }>('/voices/vendors/credentials');
|
|
||||||
const list = response.list || [];
|
|
||||||
return list.map((item) => mapVendorCredential(item));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveVendorCredential = async (vendorKey: string, data: { vendorName: string; apiKey: string; baseUrl?: string }): Promise<VendorCredential> => {
|
|
||||||
const response = await apiRequest<AnyRecord>(`/voices/vendors/credentials/${vendorKey}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: {
|
|
||||||
vendor_name: data.vendorName,
|
|
||||||
api_key: data.apiKey,
|
|
||||||
base_url: data.baseUrl || undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return mapVendorCredential(response);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchWorkflows = async (): Promise<Workflow[]> => {
|
export const fetchWorkflows = async (): Promise<Workflow[]> => {
|
||||||
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/workflows');
|
const response = await apiRequest<{ list?: AnyRecord[] } | AnyRecord[]>('/workflows');
|
||||||
const list = Array.isArray(response) ? response : (response.list || []);
|
const list = Array.isArray(response) ? response : (response.list || []);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export interface Voice {
|
|||||||
description: string;
|
description: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
voiceKey?: string;
|
voiceKey?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl?: string;
|
||||||
speed?: number;
|
speed?: number;
|
||||||
gain?: number;
|
gain?: number;
|
||||||
pitch?: number;
|
pitch?: number;
|
||||||
@@ -37,13 +39,6 @@ export interface Voice {
|
|||||||
isSystem?: boolean;
|
isSystem?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VendorCredential {
|
|
||||||
vendorKey: string;
|
|
||||||
vendorName: string;
|
|
||||||
apiKey: string;
|
|
||||||
baseUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KnowledgeBase {
|
export interface KnowledgeBase {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user