feat: update README and Settings page for dual backend support and Aliyun API key management

This commit is contained in:
2026-02-03 17:42:40 +08:00
parent 244ff94c6a
commit e7b3700a28
3 changed files with 164 additions and 52 deletions

View File

@@ -9,6 +9,7 @@ A text-to-speech web application based on Qwen3-TTS, supporting custom voice, vo
- Custom Voice: Predefined speaker voices
- Voice Design: Create voices from natural language descriptions
- Voice Cloning: Clone voices from uploaded audio
- Dual Backend Support: Switch between local model and Aliyun TTS API
- JWT auth, async tasks, voice cache, dark mode
## Tech Stack
@@ -26,7 +27,9 @@ python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# Edit .env to configure MODEL_BASE_PATH etc.
# Edit .env to configure MODEL_BASE_PATH and DEFAULT_BACKEND
# For local model: Ensure MODEL_BASE_PATH points to Qwen model directory
# For Aliyun: Set DEFAULT_BACKEND=aliyun and configure API key in web settings
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
@@ -49,6 +52,8 @@ Visit `http://localhost:5173`
## Configuration
### Backend Configuration
Backend `.env` key settings:
```env
@@ -56,26 +61,75 @@ SECRET_KEY=your-secret-key
MODEL_DEVICE=cuda:0
MODEL_BASE_PATH=../Qwen
DATABASE_URL=sqlite:///./qwen_tts.db
DEFAULT_BACKEND=local
ALIYUN_REGION=beijing
ALIYUN_MODEL_FLASH=qwen3-tts-flash-realtime
ALIYUN_MODEL_VC=qwen3-tts-vc-realtime-2026-01-15
ALIYUN_MODEL_VD=qwen3-tts-vd-realtime-2026-01-15
```
**Backend Options:**
- `DEFAULT_BACKEND`: Default TTS backend, options: `local` or `aliyun`
- **Local Mode**: Uses local Qwen3-TTS model (requires `MODEL_BASE_PATH` configuration)
- **Aliyun Mode**: Uses Aliyun TTS API (requires users to configure their API keys in settings)
**Aliyun Configuration:**
- Users need to add their Aliyun API keys in the web interface settings page
- API keys are encrypted and stored securely in the database
- Superuser can enable/disable local model access for all users
- To obtain an Aliyun API key, visit the [Aliyun Console](https://dashscope.console.aliyun.com/)
### Frontend Configuration
Frontend `.env`:
```env
VITE_API_URL=http://localhost:8000
```
## Usage
### Switching Between Backends
1. Log in to the web interface
2. Navigate to Settings page
3. Configure your preferred backend:
- **Local Model**: Select "本地模型" (requires local model to be enabled by superuser)
- **Aliyun API**: Select "阿里云" and add your API key
4. The selected backend will be used for all TTS operations by default
5. You can also specify a different backend per request using the `backend` parameter in the API
### Managing Aliyun API Key
1. In Settings page, find the "阿里云 API 密钥" section
2. Enter your Aliyun API key
3. Click "更新密钥" to save and validate
4. The system will verify the key before saving
5. You can delete the key anytime using the delete button
## API
```
POST /auth/register - Register
POST /auth/token - Login
POST /tts/custom-voice - Custom voice
POST /tts/voice-design - Voice design
POST /tts/voice-clone - Voice cloning
POST /tts/custom-voice - Custom voice (supports backend parameter)
POST /tts/voice-design - Voice design (supports backend parameter)
POST /tts/voice-clone - Voice cloning (supports backend parameter)
GET /jobs - Job list
GET /jobs/{id}/download - Download result
```
**Backend Parameter:**
All TTS endpoints support an optional `backend` parameter to specify the TTS backend:
- `backend: "local"` - Use local Qwen3-TTS model
- `backend: "aliyun"` - Use Aliyun TTS API
- If not specified, uses the user's default backend setting
## License
Apache-2.0 license

View File

@@ -9,6 +9,7 @@
- 自定义语音:预定义说话人语音
- 语音设计:自然语言描述创建语音
- 语音克隆:上传音频克隆语音
- 双后端支持:支持本地模型和阿里云 TTS API 切换
- JWT 认证、异步任务、语音缓存、暗黑模式
## 技术栈
@@ -26,7 +27,9 @@ python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# 编辑 .env 配置 MODEL_BASE_PATH
# 编辑 .env 配置 MODEL_BASE_PATH 和 DEFAULT_BACKEND
# 本地模型:确保 MODEL_BASE_PATH 指向 Qwen 模型目录
# 阿里云:设置 DEFAULT_BACKEND=aliyun 并在 Web 设置页面配置 API 密钥
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```
@@ -49,6 +52,8 @@ npm run dev
## 配置
### 后端配置
后端 `.env` 关键配置:
```env
@@ -56,26 +61,75 @@ SECRET_KEY=your-secret-key
MODEL_DEVICE=cuda:0
MODEL_BASE_PATH=../Qwen
DATABASE_URL=sqlite:///./qwen_tts.db
DEFAULT_BACKEND=local
ALIYUN_REGION=beijing
ALIYUN_MODEL_FLASH=qwen3-tts-flash-realtime
ALIYUN_MODEL_VC=qwen3-tts-vc-realtime-2026-01-15
ALIYUN_MODEL_VD=qwen3-tts-vd-realtime-2026-01-15
```
**后端选项:**
- `DEFAULT_BACKEND`: 默认 TTS 后端,可选值:`local``aliyun`
- **本地模式**: 使用本地 Qwen3-TTS 模型(需要配置 `MODEL_BASE_PATH`
- **阿里云模式**: 使用阿里云 TTS API需要用户在设置页面配置 API 密钥)
**阿里云配置:**
- 用户需要在 Web 界面的设置页面添加阿里云 API 密钥
- API 密钥经过加密后安全存储在数据库中
- 超级管理员可以控制是否为所有用户启用本地模型
- 获取阿里云 API 密钥,请访问 [阿里云控制台](https://dashscope.console.aliyun.com/)
### 前端配置
前端 `.env`
```env
VITE_API_URL=http://localhost:8000
```
## 使用说明
### 切换后端
1. 登录 Web 界面
2. 进入设置页面
3. 配置您偏好的后端:
- **本地模型**:选择"本地模型"(需要超级管理员启用本地模型)
- **阿里云 API**:选择"阿里云"并添加您的 API 密钥
4. 选择的后端将默认用于所有 TTS 操作
5. 也可以通过 API 的 `backend` 参数为单次请求指定不同的后端
### 管理阿里云 API 密钥
1. 在设置页面找到"阿里云 API 密钥"部分
2. 输入您的阿里云 API 密钥
3. 点击"更新密钥"保存并验证
4. 系统会在保存前验证密钥的有效性
5. 可随时使用删除按钮删除密钥
## API
```
POST /auth/register - 注册
POST /auth/token - 登录
POST /tts/custom-voice - 自定义语音
POST /tts/voice-design - 语音设计
POST /tts/voice-clone - 语音克隆
POST /tts/custom-voice - 自定义语音(支持 backend 参数)
POST /tts/voice-design - 语音设计(支持 backend 参数)
POST /tts/voice-clone - 语音克隆(支持 backend 参数)
GET /jobs - 任务列表
GET /jobs/{id}/download - 下载结果
```
**Backend 参数:**
所有 TTS 接口都支持可选的 `backend` 参数来指定使用的 TTS 后端:
- `backend: "local"` - 使用本地 Qwen3-TTS 模型
- `backend: "aliyun"` - 使用阿里云 TTS API
- 如果不指定,则使用用户的默认后端设置
## 许可证
Apache-2.0 license

View File

@@ -154,24 +154,24 @@ export default function Settings() {
<div className="h-screen overflow-hidden flex flex-col bg-background">
<Navbar />
<main className="flex-1 overflow-y-auto container mx-auto p-6 max-w-[800px]">
<div className="space-y-6">
<main className="flex-1 overflow-y-auto container mx-auto p-3 sm:p-6 max-w-[800px]">
<div className="space-y-3 sm:space-y-6">
<div>
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-2"></p>
<h1 className="text-2xl sm:text-3xl font-bold"></h1>
<p className="text-sm sm:text-base text-muted-foreground mt-1 sm:mt-2"></p>
</div>
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription> TTS </CardDescription>
<CardHeader className="p-4 sm:p-6">
<CardTitle className="text-lg sm:text-xl"></CardTitle>
<CardDescription className="text-sm"> TTS </CardDescription>
</CardHeader>
<CardContent>
<CardContent className="p-4 sm:p-6">
<RadioGroup
value={preferences.default_backend}
onValueChange={handleBackendChange}
>
<div className={`flex items-center space-x-3 border rounded-lg p-4 ${
<div className={`flex items-center space-x-2 sm:space-x-3 border rounded-lg p-3 sm:p-4 ${
!isBackendAvailable('local') ? 'opacity-50' : 'hover:bg-accent/50 cursor-pointer'
}`}>
<RadioGroupItem
@@ -180,18 +180,18 @@ export default function Settings() {
disabled={!isBackendAvailable('local')}
/>
<Label htmlFor="backend-local" className="flex-1 cursor-pointer">
<div className="font-medium"></div>
<div className="text-sm text-muted-foreground">
<div className="font-medium text-sm sm:text-base"></div>
<div className="text-xs sm:text-sm text-muted-foreground">
使 Qwen3-TTS
{!isBackendAvailable('local') && ' (管理员未启用)'}
</div>
</Label>
</div>
<div className="flex items-center space-x-3 border rounded-lg p-4 hover:bg-accent/50 cursor-pointer">
<div className="flex items-center space-x-2 sm:space-x-3 border rounded-lg p-3 sm:p-4 hover:bg-accent/50 cursor-pointer">
<RadioGroupItem value="aliyun" id="backend-aliyun" />
<Label htmlFor="backend-aliyun" className="flex-1 cursor-pointer">
<div className="font-medium"> API</div>
<div className="text-sm text-muted-foreground">使 TTS </div>
<div className="font-medium text-sm sm:text-base"> API</div>
<div className="text-xs sm:text-sm text-muted-foreground">使 TTS </div>
</Label>
</div>
</RadioGroup>
@@ -199,21 +199,21 @@ export default function Settings() {
</Card>
<Card>
<CardHeader>
<CardTitle> API </CardTitle>
<CardDescription> API </CardDescription>
<CardHeader className="p-4 sm:p-6">
<CardTitle className="text-lg sm:text-xl"> API </CardTitle>
<CardDescription className="text-sm"> API </CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-2 text-sm">
<CardContent className="space-y-3 sm:space-y-4 p-4 sm:p-6">
<div className="flex items-center gap-2 text-xs sm:text-sm">
<span className="text-muted-foreground">:</span>
{hasAliyunKey ? (
<span className="flex items-center gap-1 text-green-600">
<Check className="h-4 w-4" />
<Check className="h-3 w-3 sm:h-4 sm:w-4" />
</span>
) : (
<span className="flex items-center gap-1 text-muted-foreground">
<X className="h-4 w-4" />
<X className="h-3 w-3 sm:h-4 sm:w-4" />
</span>
)}
@@ -226,7 +226,7 @@ export default function Settings() {
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>API </FormLabel>
<FormLabel className="text-sm sm:text-base">API </FormLabel>
<FormControl>
<div className="flex gap-2">
<div className="relative flex-1">
@@ -257,8 +257,8 @@ export default function Settings() {
)}
/>
<div className="flex gap-2">
<Button type="submit" disabled={isLoading}>
<div className="flex flex-wrap gap-2">
<Button type="submit" disabled={isLoading} className="flex-1 sm:flex-initial">
{isLoading ? '更新中...' : hasAliyunKey ? '更新密钥' : '添加密钥'}
</Button>
{hasAliyunKey && (
@@ -268,6 +268,7 @@ export default function Settings() {
variant="outline"
onClick={handleVerifyKey}
disabled={isLoading}
className="flex-1 sm:flex-initial"
>
</Button>
@@ -276,9 +277,11 @@ export default function Settings() {
variant="destructive"
onClick={handleDeleteKey}
disabled={isLoading}
size="icon"
className="sm:w-auto sm:px-4"
>
<Trash2 className="h-4 w-4 mr-2" />
<Trash2 className="h-4 w-4" />
<span className="hidden sm:inline sm:ml-2"></span>
</Button>
</>
)}
@@ -290,16 +293,16 @@ export default function Settings() {
{user.is_superuser && (
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
<CardHeader className="p-4 sm:p-6">
<CardTitle className="text-lg sm:text-xl"></CardTitle>
<CardDescription className="text-sm"></CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="local-model-toggle"></Label>
<p className="text-sm text-muted-foreground">
<CardContent className="p-4 sm:p-6">
<div className="space-y-3 sm:space-y-4">
<div className="flex items-start sm:items-center justify-between gap-4">
<div className="space-y-0.5 flex-1">
<Label htmlFor="local-model-toggle" className="text-sm sm:text-base"></Label>
<p className="text-xs sm:text-sm text-muted-foreground">
使 Qwen3-TTS
</p>
</div>
@@ -307,6 +310,7 @@ export default function Settings() {
id="local-model-toggle"
checked={localModelEnabled}
onCheckedChange={handleToggleLocalModel}
className="shrink-0"
/>
</div>
</div>
@@ -315,21 +319,21 @@ export default function Settings() {
)}
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardDescription></CardDescription>
<CardHeader className="p-4 sm:p-6">
<CardTitle className="text-lg sm:text-xl"></CardTitle>
<CardDescription className="text-sm"></CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2">
<Label></Label>
<CardContent className="space-y-3 sm:space-y-4 p-4 sm:p-6">
<div className="grid gap-1.5 sm:gap-2">
<Label className="text-sm sm:text-base"></Label>
<Input value={user.username} disabled />
</div>
<div className="grid gap-2">
<Label></Label>
<div className="grid gap-1.5 sm:gap-2">
<Label className="text-sm sm:text-base"></Label>
<Input value={user.email} disabled />
</div>
<div>
<Button onClick={() => setShowPasswordDialog(true)}></Button>
<Button onClick={() => setShowPasswordDialog(true)} className="w-full sm:w-auto"></Button>
</div>
</CardContent>
</Card>