diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index c17620d..0000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-# These are supported funding model platforms
-
-github: bdim404
diff --git a/.github/workflows/docker-backend.yml b/.github/workflows/docker-backend.yml
deleted file mode 100644
index c2c0fc6..0000000
--- a/.github/workflows/docker-backend.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: Publish Backend Image
-
-on:
- push:
- branches: [main]
- paths:
- - 'qwen3-tts-backend/**'
- - 'qwen_tts/**'
- - 'docker/backend/**'
-
-jobs:
- build-and-push:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Log in to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Build and push
- uses: docker/build-push-action@v6
- with:
- context: .
- file: docker/backend/Dockerfile
- push: true
- tags: bdim404/qwen3-tts-backend:latest
- cache-from: type=gha,scope=backend
- cache-to: type=gha,mode=max,scope=backend
diff --git a/.github/workflows/docker-frontend.yml b/.github/workflows/docker-frontend.yml
deleted file mode 100644
index c1fbf35..0000000
--- a/.github/workflows/docker-frontend.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Publish Frontend Image
-
-on:
- push:
- branches: [main]
- paths:
- - 'qwen3-tts-frontend/**'
- - 'docker/frontend/**'
-
-jobs:
- build-and-push:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
-
- - name: Log in to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Build and push
- uses: docker/build-push-action@v6
- with:
- context: .
- file: docker/frontend/Dockerfile
- push: true
- tags: bdim404/qwen3-tts-frontend:latest
- cache-from: type=gha,scope=frontend
- cache-to: type=gha,mode=max,scope=frontend
diff --git a/README.md b/README.md
deleted file mode 100644
index ba0e428..0000000
--- a/README.md
+++ /dev/null
@@ -1,348 +0,0 @@
-# Qwen3-TTS WebUI
-
-> **⚠️ Notice:** This project is largely AI-generated and is currently in an unstable state. Stable releases will be published in the [Releases](../../releases) section.
-
-**Unofficial** text-to-speech web application based on Qwen3-TTS, supporting custom voice, voice design, and voice cloning with an intuitive interface.
-
-> This is an unofficial project. For the official Qwen3-TTS repository, please visit [QwenLM/Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS).
-
-[中文文档](./README.zh.md)
-
-## Features
-
-- Custom Voice: Predefined speaker voices
-- Voice Design: Create voices from natural language descriptions
-- Voice Cloning: Clone voices from uploaded audio
-- **IndexTTS2**: High-quality voice cloning with emotion control (happy, angry, sad, fear, surprise, etc.) powered by [IndexTTS2](https://github.com/iszhanjiawei/indexTTS2)
-- Audiobook Generation: Upload EPUB files and generate multi-character audiobooks with LLM-powered character extraction and voice assignment; supports IndexTTS2 per character
-- Dual Backend Support: Switch between local model and Aliyun TTS API
-- Multi-language Support: English, 简体中文, 繁體中文, 日本語, 한국어
-- JWT auth, async tasks, voice cache, dark mode
-
-## Interface Preview
-
-### Desktop - Light Mode
-
-
-### Desktop - Dark Mode
-
-
-### Mobile
-
-
-  |
-  |
-
-
-
-### Audiobook Generation
-
-
-
-
-  |
-  |
-
-
-
-## Tech Stack
-
-**Backend**: FastAPI + SQLAlchemy + PyTorch + JWT
-- Direct PyTorch inference with Qwen3-TTS models
-- Async task processing with batch optimization
-- Local model support + Aliyun API integration
-
-**Frontend**: React 19 + TypeScript + Vite + Tailwind + Shadcn/ui
-
-## Docker Deployment
-
-Pre-built images are available on Docker Hub: [bdim404/qwen3-tts-backend](https://hub.docker.com/r/bdim404/qwen3-tts-backend), [bdim404/qwen3-tts-frontend](https://hub.docker.com/r/bdim404/qwen3-tts-frontend)
-
-**Prerequisites**: Docker, Docker Compose, NVIDIA GPU + [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html)
-
-```bash
-git clone https://github.com/bdim404/Qwen3-TTS-WebUI.git
-cd Qwen3-TTS-webUI
-
-# Download models to docker/models/ (see Installation > Download Models below)
-mkdir -p docker/models docker/data
-
-# Configure
-cp docker/.env.example docker/.env
-# Edit docker/.env and set SECRET_KEY
-
-cd docker
-
-# Pull pre-built images
-docker compose pull
-
-# Start (CPU only)
-docker compose up -d
-
-# Start (with GPU)
-docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d
-```
-
-Access the application at `http://localhost`. Default credentials: `admin` / `admin123456`
-
-## Installation
-
-### Prerequisites
-
-- Python 3.9+ with CUDA support (for local model inference)
-- Node.js 18+ (for frontend)
-- Git
-
-### 1. Clone Repository
-
-```bash
-git clone https://github.com/bdim404/Qwen3-TTS-WebUI.git
-cd Qwen3-TTS-webUI
-```
-
-### 2. Download Models
-
-**Important**: Models are **NOT** automatically downloaded. You need to manually download them first.
-
-For more details, visit the official repository: [Qwen3-TTS Models](https://github.com/QwenLM/Qwen3-TTS)
-
-Navigate to the models directory:
-```bash
-# Docker deployment
-mkdir -p docker/models && cd docker/models
-
-# Local deployment
-cd qwen3-tts-backend && mkdir -p Qwen && cd Qwen
-```
-
-**Option 1: Download through ModelScope (Recommended for users in Mainland China)**
-
-```bash
-pip install -U modelscope
-
-modelscope download --model Qwen/Qwen3-TTS-Tokenizer-12Hz --local_dir ./Qwen3-TTS-Tokenizer-12Hz
-modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --local_dir ./Qwen3-TTS-12Hz-1.7B-CustomVoice
-modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --local_dir ./Qwen3-TTS-12Hz-1.7B-VoiceDesign
-modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-Base --local_dir ./Qwen3-TTS-12Hz-1.7B-Base
-```
-
-Optional 0.6B models (smaller, faster):
-```bash
-modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local_dir ./Qwen3-TTS-12Hz-0.6B-CustomVoice
-modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-Base --local_dir ./Qwen3-TTS-12Hz-0.6B-Base
-```
-
-**Option 2: Download through Hugging Face**
-
-```bash
-pip install -U "huggingface_hub[cli]"
-
-hf download Qwen/Qwen3-TTS-Tokenizer-12Hz --local-dir ./Qwen3-TTS-Tokenizer-12Hz
-hf download Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --local-dir ./Qwen3-TTS-12Hz-1.7B-CustomVoice
-hf download Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign --local-dir ./Qwen3-TTS-12Hz-1.7B-VoiceDesign
-hf download Qwen/Qwen3-TTS-12Hz-1.7B-Base --local-dir ./Qwen3-TTS-12Hz-1.7B-Base
-```
-
-Optional 0.6B models (smaller, faster):
-```bash
-hf download Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local-dir ./Qwen3-TTS-12Hz-0.6B-CustomVoice
-hf download Qwen/Qwen3-TTS-12Hz-0.6B-Base --local-dir ./Qwen3-TTS-12Hz-0.6B-Base
-```
-
-**IndexTTS2 Model (optional, for emotion-controlled voice cloning)**
-
-IndexTTS2 is an optional feature. Only download these files if you want to use it. Navigate to the same `Qwen/` directory and run:
-
-```bash
-# Only the required files — no need to download the full repository
-hf download IndexTeam/IndexTTS-2 \
- bpe.model config.yaml feat1.pt feat2.pt gpt.pth s2mel.pth wav2vec2bert_stats.pt \
- --local-dir ./IndexTTS2
-```
-
-Then install the indextts package:
-```bash
-git clone https://github.com/iszhanjiawei/indexTTS2.git
-cd indexTTS2
-pip install -e . --no-deps
-cd ..
-```
-
-**Final directory structure:**
-
-Docker deployment (`docker/models/`):
-```
-Qwen3-TTS-webUI/
-└── docker/
- └── models/
- ├── Qwen3-TTS-Tokenizer-12Hz/
- ├── Qwen3-TTS-12Hz-1.7B-CustomVoice/
- ├── Qwen3-TTS-12Hz-1.7B-VoiceDesign/
- └── Qwen3-TTS-12Hz-1.7B-Base/
-```
-
-Local deployment (`qwen3-tts-backend/Qwen/`):
-```
-Qwen3-TTS-webUI/
-└── qwen3-tts-backend/
- └── Qwen/
- ├── Qwen3-TTS-Tokenizer-12Hz/
- ├── Qwen3-TTS-12Hz-1.7B-CustomVoice/
- ├── Qwen3-TTS-12Hz-1.7B-VoiceDesign/
- ├── Qwen3-TTS-12Hz-1.7B-Base/
- └── IndexTTS2/ ← optional, for IndexTTS2 feature
- ├── bpe.model
- ├── config.yaml
- ├── feat1.pt
- ├── feat2.pt
- ├── gpt.pth
- ├── s2mel.pth
- └── wav2vec2bert_stats.pt
-```
-
-### 3. Backend Setup
-
-```bash
-cd qwen3-tts-backend
-
-# Create virtual environment
-python -m venv venv
-source venv/bin/activate # On Windows: venv\Scripts\activate
-
-# Install dependencies
-pip install -r requirements.txt
-
-# Install Qwen3-TTS
-pip install qwen-tts
-
-# Create configuration file
-cp .env.example .env
-
-# Edit .env file
-# For local model: Set MODEL_BASE_PATH=./Qwen
-# For Aliyun API only: Set DEFAULT_BACKEND=aliyun
-nano .env # or use your preferred editor
-```
-
-**Important Backend Configuration** (`.env`):
-```env
-MODEL_DEVICE=cuda:0 # Use GPU (or cpu for CPU-only)
-MODEL_BASE_PATH=./Qwen # Path to your downloaded models
-DEFAULT_BACKEND=local # Use 'local' for local models, 'aliyun' for API
-DATABASE_URL=sqlite:///./qwen_tts.db
-SECRET_KEY=your-secret-key-here # Change this!
-```
-
-Start the backend server:
-```bash
-# Using uvicorn directly
-uvicorn main:app --host 0.0.0.0 --port 8000 --reload
-
-# Or using conda (if you prefer)
-conda run -n qwen3-tts uvicorn main:app --host 0.0.0.0 --port 8000 --reload
-```
-
-Verify backend is running:
-```bash
-curl http://127.0.0.1:8000/health
-```
-
-### 4. Frontend Setup
-
-```bash
-cd qwen3-tts-frontend
-
-# Install dependencies
-npm install
-
-# Create configuration file
-cp .env.example .env
-
-# Start development server
-npm run dev
-```
-
-### 5. Access the Application
-
-Open your browser and visit: `http://localhost:5173`
-
-**Default Credentials**:
-- Username: `admin`
-- Password: `admin123456`
-- **IMPORTANT**: Change the password immediately after first login!
-
-### Production Build
-
-For production deployment:
-
-```bash
-# Backend: Use gunicorn or similar WSGI server
-cd qwen3-tts-backend
-gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000
-
-# Frontend: Build static files
-cd qwen3-tts-frontend
-npm run build
-# Serve the 'dist' folder with nginx or another web server
-```
-
-## Configuration
-
-### Backend Configuration
-
-Backend `.env` key settings:
-
-```env
-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/)
-
-## 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
-
-## Acknowledgments
-
-This project is built upon the excellent work of the official [Qwen3-TTS](https://github.com/QwenLM/Qwen3-TTS) repository by the Qwen Team at Alibaba Cloud. Special thanks to the Qwen Team for open-sourcing such a powerful text-to-speech model.
-
-## License
-
-Apache-2.0 license
diff --git a/dev.sh b/dev.sh
new file mode 100755
index 0000000..22c5af3
--- /dev/null
+++ b/dev.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+trap 'kill 0' EXIT
+
+(cd qwen3-tts-backend && conda run -n qwen3-tts uvicorn main:app --host 0.0.0.0 --port 8000 --reload 2>&1 | sed 's/^/[backend] /') &
+(cd qwen3-tts-frontend && npm run dev 2>&1 | sed 's/^/[frontend] /') &
+
+wait
diff --git a/images/audiobook-chapters.png b/images/audiobook-chapters.png
deleted file mode 100644
index dc1905e..0000000
Binary files a/images/audiobook-chapters.png and /dev/null differ
diff --git a/images/audiobook-characters.png b/images/audiobook-characters.png
deleted file mode 100644
index 5df4ec9..0000000
Binary files a/images/audiobook-characters.png and /dev/null differ
diff --git a/images/audiobook-overview.png b/images/audiobook-overview.png
deleted file mode 100644
index 5547cbd..0000000
Binary files a/images/audiobook-overview.png and /dev/null differ
diff --git a/images/darkmode-chinese.png b/images/darkmode-chinese.png
deleted file mode 100644
index ef77ad3..0000000
Binary files a/images/darkmode-chinese.png and /dev/null differ
diff --git a/images/lightmode-english.png b/images/lightmode-english.png
deleted file mode 100644
index 8ed5893..0000000
Binary files a/images/lightmode-english.png and /dev/null differ
diff --git a/images/mobile-lightmode-custom.png b/images/mobile-lightmode-custom.png
deleted file mode 100644
index 73efbb4..0000000
Binary files a/images/mobile-lightmode-custom.png and /dev/null differ
diff --git a/images/mobile-settings.png b/images/mobile-settings.png
deleted file mode 100644
index f5f039d..0000000
Binary files a/images/mobile-settings.png and /dev/null differ
diff --git a/qwen3-tts-frontend/src/App.tsx b/qwen3-tts-frontend/src/App.tsx
index 787b592..fc13641 100644
--- a/qwen3-tts-frontend/src/App.tsx
+++ b/qwen3-tts-frontend/src/App.tsx
@@ -4,18 +4,13 @@ import { Toaster } from 'sonner'
import { ThemeProvider } from '@/contexts/ThemeContext'
import { AuthProvider, useAuth } from '@/contexts/AuthContext'
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext'
-import { AppProvider } from '@/contexts/AppContext'
-import { JobProvider } from '@/contexts/JobContext'
-import { HistoryProvider } from '@/contexts/HistoryContext'
import ErrorBoundary from '@/components/ErrorBoundary'
import LoadingScreen from '@/components/LoadingScreen'
import { SuperAdminRoute } from '@/components/SuperAdminRoute'
const Login = lazy(() => import('@/pages/Login'))
-const Home = lazy(() => import('@/pages/Home'))
const Settings = lazy(() => import('@/pages/Settings'))
const UserManagement = lazy(() => import('@/pages/UserManagement'))
-const VoiceManagement = lazy(() => import('@/pages/VoiceManagement'))
const Audiobook = lazy(() => import('@/pages/Audiobook'))
const AdminStats = lazy(() => import('@/pages/AdminStats'))
@@ -49,7 +44,7 @@ function PublicRoute({ children }: { children: React.ReactNode }) {
}
if (isAuthenticated) {
- return
+ return
}
return <>{children}>
@@ -73,20 +68,7 @@ function App() {
}
/>
-
-
-
-
-
-
-
-
-
- }
- />
+ } />
}
/>
-
-
-
- }
- />
void
- error?: string
-}
-
-export function AudioInputSelector({ value, onChange, error }: AudioInputSelectorProps) {
- const [activeTab, setActiveTab] = useState('upload')
-
- const handleTabChange = (newTab: string) => {
- onChange(null)
- setActiveTab(newTab)
- }
-
- return (
-
-
-
-
- 上传文件
-
-
-
- 录制音频
-
-
-
-
-
-
-
-
-
- {error && {error}
}
-
-
- )
-}
diff --git a/qwen3-tts-frontend/src/components/AudioRecorder.tsx b/qwen3-tts-frontend/src/components/AudioRecorder.tsx
deleted file mode 100644
index 2719d52..0000000
--- a/qwen3-tts-frontend/src/components/AudioRecorder.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import { useEffect, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { Button } from '@/components/ui/button'
-import { Mic, Trash2, RotateCcw, FileAudio } from 'lucide-react'
-import { toast } from 'sonner'
-import { useAudioRecorder } from '@/hooks/useAudioRecorder'
-import { useAudioValidation } from '@/hooks/useAudioValidation'
-
-interface AudioRecorderProps {
- onChange: (file: File | null) => void
-}
-
-export function AudioRecorder({ onChange }: AudioRecorderProps) {
- const { t } = useTranslation('voice')
- const {
- isRecording,
- recordingDuration,
- audioBlob,
- error: recorderError,
- isSupported,
- startRecording,
- stopRecording,
- clearRecording,
- } = useAudioRecorder()
-
- const { validateAudioFile } = useAudioValidation()
- const [audioInfo, setAudioInfo] = useState<{ duration: number; size: number } | null>(null)
- const [validationError, setValidationError] = useState(null)
-
- useEffect(() => {
- if (recorderError) {
- toast.error(recorderError)
- }
- }, [recorderError])
-
- useEffect(() => {
- if (audioBlob) {
- handleValidateRecording(audioBlob)
- }
- }, [audioBlob])
-
- const handleValidateRecording = async (blob: Blob) => {
- const file = new File([blob], 'recording.wav', { type: 'audio/wav' })
-
- const result = await validateAudioFile(file)
-
- console.log('录音验证结果:', {
- valid: result.valid,
- duration: result.duration,
- recordingDuration: recordingDuration,
- error: result.error
- })
-
- if (result.valid && result.duration) {
- onChange(file)
- setAudioInfo({ duration: result.duration, size: file.size })
- setValidationError(null)
- } else {
- setValidationError(result.error || t('recordingValidationFailed'))
- clearRecording()
- onChange(null)
- }
- }
-
- const handleMouseDown = () => {
- if (!isRecording && !audioBlob) {
- startRecording()
- }
- }
-
- const handleMouseUp = () => {
- if (isRecording) {
- stopRecording()
- }
- }
-
- const handleReset = (e: React.MouseEvent) => {
- e.preventDefault()
- e.stopPropagation()
- clearRecording()
- setAudioInfo(null)
- setValidationError(null)
- onChange(null)
- }
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === ' ' && !isRecording && !audioBlob) {
- e.preventDefault()
- startRecording()
- }
- }
-
- const handleKeyUp = (e: React.KeyboardEvent) => {
- if (e.key === ' ' && isRecording) {
- e.preventDefault()
- stopRecording()
- }
- }
-
- if (!isSupported) {
- return (
-
- {t('browserNotSupported')}
-
- )
- }
-
- if (audioBlob && audioInfo) {
- return (
-
-
-
-
-
{t('recordingComplete')}
-
- {(audioInfo.size / 1024 / 1024).toFixed(2)} MB · {audioInfo.duration.toFixed(1)} {t('seconds')}
-
-
-
-
-
- )
- }
-
- return (
-
-
-
- {validationError && (
-
-
{validationError}
-
-
- )}
-
- )
-}
diff --git a/qwen3-tts-frontend/src/components/HistoryItem.tsx b/qwen3-tts-frontend/src/components/HistoryItem.tsx
deleted file mode 100644
index 188ec32..0000000
--- a/qwen3-tts-frontend/src/components/HistoryItem.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-import { memo, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import type { Job } from '@/types/job'
-import { Badge } from '@/components/ui/badge'
-import { Button } from '@/components/ui/button'
-import {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogTitle,
- AlertDialogTrigger,
-} from '@/components/ui/alert-dialog'
-import { Trash2, AlertCircle, Loader2, Clock, Eye } from 'lucide-react'
-import { getRelativeTime, cn } from '@/lib/utils'
-import { JobDetailDialog } from '@/components/JobDetailDialog'
-
-interface HistoryItemProps {
- job: Job
- onDelete: (id: number) => void
-}
-
-const jobTypeBadgeVariant = {
- custom_voice: 'default' as const,
- voice_design: 'secondary' as const,
- voice_clone: 'outline' as const,
-}
-
-const HistoryItem = memo(({ job, onDelete }: HistoryItemProps) => {
- const { t } = useTranslation('job')
- const { t: tCommon } = useTranslation('common')
- const [detailDialogOpen, setDetailDialogOpen] = useState(false)
-
- const jobTypeLabel = {
- custom_voice: t('typeCustomVoice'),
- voice_design: t('typeVoiceDesign'),
- voice_clone: t('typeVoiceClone'),
- }
-
- const getLanguageDisplay = (lang: string | undefined) => {
- if (!lang || lang === 'Auto') return t('autoDetect')
- return lang
- }
-
- const handleCardClick = (e: React.MouseEvent) => {
- if ((e.target as HTMLElement).closest('button')) return
- setDetailDialogOpen(true)
- }
-
- return (
-
-
-
- {jobTypeLabel[job.type]}
-
-
- {getRelativeTime(job.created_at)}
-
-
-
-
-
- {job.parameters?.text && (
-
- {t('synthesisText')}:
- {job.parameters.text}
-
- )}
-
-
- {t('language')}{getLanguageDisplay(job.parameters?.language)}
-
-
- {job.type === 'custom_voice' && job.parameters?.speaker && (
-
- {t('speaker')}{job.parameters.speaker}
-
- )}
-
- {job.type === 'voice_design' && job.parameters?.instruct && (
-
- {t('voiceDescription')}:
- {job.parameters.instruct}
-
- )}
-
- {job.type === 'voice_clone' && job.parameters?.ref_text && (
-
- {t('referenceText')}:
- {job.parameters.ref_text}
-
- )}
-
-
- {job.status === 'processing' && (
-
-
- {t('statusProcessing')}
-
- )}
-
- {job.status === 'pending' && (
-
-
- {t('statusPending')}
-
- )}
-
- {job.status === 'failed' && job.error_message && (
-
-
-
{job.error_message}
-
- )}
-
-
-
-
-
-
-
-
- {t('deleteJob')}
-
- {t('deleteJobConfirm')}
-
-
-
- {tCommon('cancel')}
- onDelete(job.id)}
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
- >
- {tCommon('delete')}
-
-
-
-
-
-
-
-
- )
-}, (prevProps, nextProps) => {
- return (
- prevProps.job.id === nextProps.job.id &&
- prevProps.job.status === nextProps.job.status &&
- prevProps.job.updated_at === nextProps.job.updated_at &&
- prevProps.job.error_message === nextProps.job.error_message
- )
-})
-
-HistoryItem.displayName = 'HistoryItem'
-
-export { HistoryItem }
diff --git a/qwen3-tts-frontend/src/components/HistorySidebar.tsx b/qwen3-tts-frontend/src/components/HistorySidebar.tsx
deleted file mode 100644
index 1f88e0d..0000000
--- a/qwen3-tts-frontend/src/components/HistorySidebar.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { useRef, useEffect } from 'react'
-import { useTranslation } from 'react-i18next'
-import { Link } from 'react-router-dom'
-import { useHistoryContext } from '@/contexts/HistoryContext'
-import { HistoryItem } from '@/components/HistoryItem'
-import { ScrollArea } from '@/components/ui/scroll-area'
-import { Sheet, SheetContent } from '@/components/ui/sheet'
-import { Button } from '@/components/ui/button'
-import { Loader2, FileAudio, RefreshCw } from 'lucide-react'
-
-interface HistorySidebarProps {
- open: boolean
- onOpenChange: (open: boolean) => void
-}
-
-function HistorySidebarContent() {
- const { t } = useTranslation('job')
- const { jobs, loading, loadingMore, hasMore, loadMore, deleteJob, error, retry } = useHistoryContext()
- const observerTarget = useRef(null)
-
- useEffect(() => {
- const observer = new IntersectionObserver(
- (entries) => {
- if (entries[0].isIntersecting && hasMore && !loadingMore) {
- loadMore()
- }
- },
- { threshold: 0.5 }
- )
-
- if (observerTarget.current) {
- observer.observe(observerTarget.current)
- }
-
- return () => observer.disconnect()
- }, [hasMore, loadingMore, loadMore])
-
- return (
-
-
-
-

-
- Qwen3-TTS-WebUI
-
-
-
{t('historyTitle')}
-
{t('historyCount', { count: jobs.length })}
-
-
-
-
- {loading ? (
-
-
-
- ) : error ? (
-
-
{error}
-
-
- ) : jobs.length === 0 ? (
-
-
-
{t('noHistory')}
-
- {t('historyDescription')}
-
-
- ) : (
- <>
- {jobs.map((job) => (
-
- ))}
-
- {hasMore && (
-
-
-
- )}
- >
- )}
-
-
-
- )
-}
-
-export function HistorySidebar({ open, onOpenChange }: HistorySidebarProps) {
- return (
- <>
-
-
-
-
-
-
-
- >
- )
-}
diff --git a/qwen3-tts-frontend/src/components/Navbar.tsx b/qwen3-tts-frontend/src/components/Navbar.tsx
index 7866d6e..8039443 100644
--- a/qwen3-tts-frontend/src/components/Navbar.tsx
+++ b/qwen3-tts-frontend/src/components/Navbar.tsx
@@ -1,5 +1,5 @@
-import { Menu, LogOut, Users, Settings, Globe, Home, Mic, BookOpen, BarChart2 } from 'lucide-react'
-import { Link, useLocation } from 'react-router-dom'
+import { LogOut, Users, Settings, Globe, BookOpen, BarChart2 } from 'lucide-react'
+import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
@@ -12,43 +12,13 @@ import { ThemeToggle } from '@/components/ThemeToggle'
import { useAuth } from '@/contexts/AuthContext'
import { useUserPreferences } from '@/contexts/UserPreferencesContext'
-interface NavbarProps {
- onToggleSidebar?: () => void
-}
-
-export function Navbar({ onToggleSidebar }: NavbarProps) {
+export function Navbar() {
const { logout, user } = useAuth()
const { changeLanguage } = useUserPreferences()
const { t, i18n } = useTranslation(['nav', 'constants'])
- const location = useLocation()
return (