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 -![Light Mode](./images/lightmode-english.png) - -### Desktop - Dark Mode -![Dark Mode](./images/darkmode-chinese.png) - -### Mobile - - - - - -
Mobile Light ModeMobile Settings
- -### Audiobook Generation -![Audiobook Overview](./images/audiobook-overview.png) - - - - - - -
Audiobook CharactersAudiobook Chapters
- -## 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 ( -
-
- - Qwen -

- 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 (