diff --git a/qwen3-tts-backend/api/auth.py b/qwen3-tts-backend/api/auth.py index 1d4cdde..6c0b170 100644 --- a/qwen3-tts-backend/api/auth.py +++ b/qwen3-tts-backend/api/auth.py @@ -14,7 +14,7 @@ from core.security import ( decode_access_token ) from db.database import get_db -from db.crud import get_user_by_username, get_user_by_email, create_user, change_user_password, update_user_aliyun_key, get_user_preferences, update_user_preferences +from db.crud import get_user_by_username, get_user_by_email, create_user, change_user_password, update_user_aliyun_key, get_user_preferences, update_user_preferences, is_local_model_enabled from schemas.user import User, UserCreate, Token, PasswordChange, AliyunKeyUpdate, AliyunKeyVerifyResponse, UserPreferences, UserPreferencesResponse router = APIRouter(prefix="/auth", tags=["authentication"]) @@ -246,7 +246,17 @@ async def get_preferences( db: Session = Depends(get_db) ): prefs = get_user_preferences(db, current_user.id) - return UserPreferencesResponse(**prefs) + + local_enabled = is_local_model_enabled(db) + available_backends = ["aliyun"] + if local_enabled or current_user.is_superuser: + available_backends.append("local") + + return { + "default_backend": prefs.get("default_backend", "aliyun"), + "onboarding_completed": prefs.get("onboarding_completed", False), + "available_backends": available_backends + } @router.put("/preferences") @limiter.limit("10/minute") @@ -256,10 +266,24 @@ async def update_preferences( current_user: Annotated[User, Depends(get_current_user)], db: Session = Depends(get_db) ): - user = update_user_preferences(db, current_user.id, preferences.dict()) - if not user: + if preferences.default_backend == "local": + local_enabled = is_local_model_enabled(db) + if not local_enabled and not current_user.is_superuser: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Local model is not available. Please contact administrator." + ) + + updated_user = update_user_preferences( + db, + current_user.id, + preferences.model_dump() + ) + + if not updated_user: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) - return {"message": "Preferences updated"} + + return {"message": "Preferences updated successfully"} diff --git a/qwen3-tts-backend/api/tts.py b/qwen3-tts-backend/api/tts.py index a584c9c..b224ad5 100644 --- a/qwen3-tts-backend/api/tts.py +++ b/qwen3-tts-backend/api/tts.py @@ -285,14 +285,27 @@ async def create_custom_voice_job( db: Session = Depends(get_db) ): from core.security import decrypt_api_key + from db.crud import get_user_preferences, is_local_model_enabled - backend_type = req_data.backend or settings.DEFAULT_BACKEND - if backend_type == "aliyun": - if not current_user.aliyun_api_key: - raise HTTPException(status_code=400, detail="Aliyun API key not configured. Please set your API key first.") - user_api_key = decrypt_api_key(current_user.aliyun_api_key) - if not user_api_key: - raise HTTPException(status_code=400, detail="Invalid Aliyun API key. Please update your API key.") + user_prefs = get_user_preferences(db, current_user.id) + preferred_backend = user_prefs.get("default_backend", "aliyun") + + local_enabled = is_local_model_enabled(db) + can_use_local = local_enabled or current_user.is_superuser + + backend_type = req_data.backend if hasattr(req_data, 'backend') and req_data.backend else preferred_backend + + if backend_type == "local" and not can_use_local: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Local model is not available. Please contact administrator." + ) + + if backend_type == "aliyun" and not current_user.aliyun_api_key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Aliyun API key not configured. Please set your API key in Settings." + ) try: validate_text_length(req_data.text) @@ -362,14 +375,27 @@ async def create_voice_design_job( db: Session = Depends(get_db) ): from core.security import decrypt_api_key + from db.crud import get_user_preferences, is_local_model_enabled - backend_type = req_data.backend or settings.DEFAULT_BACKEND - if backend_type == "aliyun": - if not current_user.aliyun_api_key: - raise HTTPException(status_code=400, detail="Aliyun API key not configured. Please set your API key first.") - user_api_key = decrypt_api_key(current_user.aliyun_api_key) - if not user_api_key: - raise HTTPException(status_code=400, detail="Invalid Aliyun API key. Please update your API key.") + user_prefs = get_user_preferences(db, current_user.id) + preferred_backend = user_prefs.get("default_backend", "aliyun") + + local_enabled = is_local_model_enabled(db) + can_use_local = local_enabled or current_user.is_superuser + + backend_type = req_data.backend if hasattr(req_data, 'backend') and req_data.backend else preferred_backend + + if backend_type == "local" and not can_use_local: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Local model is not available. Please contact administrator." + ) + + if backend_type == "aliyun" and not current_user.aliyun_api_key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Aliyun API key not configured. Please set your API key in Settings." + ) try: validate_text_length(req_data.text) @@ -450,14 +476,27 @@ async def create_voice_clone_job( db: Session = Depends(get_db) ): from core.security import decrypt_api_key + from db.crud import get_user_preferences, is_local_model_enabled - backend_type = backend or settings.DEFAULT_BACKEND - if backend_type == "aliyun": - if not current_user.aliyun_api_key: - raise HTTPException(status_code=400, detail="Aliyun API key not configured. Please set your API key first.") - user_api_key = decrypt_api_key(current_user.aliyun_api_key) - if not user_api_key: - raise HTTPException(status_code=400, detail="Invalid Aliyun API key. Please update your API key.") + user_prefs = get_user_preferences(db, current_user.id) + preferred_backend = user_prefs.get("default_backend", "aliyun") + + local_enabled = is_local_model_enabled(db) + can_use_local = local_enabled or current_user.is_superuser + + backend_type = backend if backend else preferred_backend + + if backend_type == "local" and not can_use_local: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Local model is not available. Please contact administrator." + ) + + if backend_type == "aliyun" and not current_user.aliyun_api_key: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Aliyun API key not configured. Please set your API key in Settings." + ) try: validate_text_length(text) diff --git a/qwen3-tts-backend/api/users.py b/qwen3-tts-backend/api/users.py index 2a5e074..e2030a5 100644 --- a/qwen3-tts-backend/api/users.py +++ b/qwen3-tts-backend/api/users.py @@ -15,9 +15,12 @@ from db.crud import ( list_users, create_user_by_admin, update_user, - delete_user + delete_user, + get_system_setting, + update_system_setting, + is_local_model_enabled ) -from schemas.user import User, UserCreateByAdmin, UserUpdate, UserListResponse +from schemas.user import User, UserCreateByAdmin, UserUpdate, UserListResponse, SystemSettingsUpdate, SystemSettingsResponse router = APIRouter(prefix="/users", tags=["users"]) limiter = Limiter(key_func=get_remote_address) @@ -167,3 +170,42 @@ async def delete_user_by_id( status_code=status.HTTP_404_NOT_FOUND, detail="User not found" ) + +@router.get("/system/settings", response_model=SystemSettingsResponse) +async def get_system_settings( + current_user: Annotated[User, Depends(require_superuser)], + db: Session = Depends(get_db) +): + local_enabled = is_local_model_enabled(db) + return {"local_model_enabled": local_enabled} + +@router.put("/system/settings") +async def update_system_settings( + settings: SystemSettingsUpdate, + current_user: Annotated[User, Depends(require_superuser)], + db: Session = Depends(get_db) +): + from db.models import User + from datetime import datetime + + update_system_setting(db, "local_model_enabled", {"enabled": settings.local_model_enabled}) + + if not settings.local_model_enabled: + users = db.query(User).filter(User.is_superuser == False).all() + migrated_count = 0 + + for user in users: + prefs = user.user_preferences or {} + if prefs.get("default_backend") == "local": + prefs["default_backend"] = "aliyun" + user.user_preferences = prefs + user.updated_at = datetime.utcnow() + migrated_count += 1 + + db.commit() + return { + "message": "System settings updated", + "users_migrated": migrated_count + } + + return {"message": "System settings updated", "users_migrated": 0} diff --git a/qwen3-tts-backend/db/crud.py b/qwen3-tts-backend/db/crud.py index d76f8e1..6e67b91 100644 --- a/qwen3-tts-backend/db/crud.py +++ b/qwen3-tts-backend/db/crud.py @@ -3,7 +3,7 @@ from typing import Optional, List, Dict, Any from datetime import datetime from sqlalchemy.orm import Session -from db.models import User, Job, VoiceCache +from db.models import User, Job, VoiceCache, SystemSettings def get_user_by_username(db: Session, username: str) -> Optional[User]: return db.query(User).filter(User.username == username).first() @@ -233,7 +233,7 @@ def delete_cache_entry(db: Session, cache_id: int, user_id: int) -> bool: def get_user_preferences(db: Session, user_id: int) -> dict: user = get_user_by_id(db, user_id) if not user or not user.user_preferences: - return {"default_backend": "local", "onboarding_completed": False} + return {"default_backend": "aliyun", "onboarding_completed": False} return user.user_preferences def update_user_preferences(db: Session, user_id: int, preferences: dict) -> Optional[User]: @@ -245,3 +245,27 @@ def update_user_preferences(db: Session, user_id: int, preferences: dict) -> Opt db.commit() db.refresh(user) return user + +def get_system_setting(db: Session, key: str) -> Optional[dict]: + setting = db.query(SystemSettings).filter(SystemSettings.key == key).first() + if not setting: + return None + return setting.value + +def update_system_setting(db: Session, key: str, value: dict) -> SystemSettings: + setting = db.query(SystemSettings).filter(SystemSettings.key == key).first() + if setting: + setting.value = value + setting.updated_at = datetime.utcnow() + else: + setting = SystemSettings(key=key, value=value, updated_at=datetime.utcnow()) + db.add(setting) + db.commit() + db.refresh(setting) + return setting + +def is_local_model_enabled(db: Session) -> bool: + setting = get_system_setting(db, "local_model_enabled") + if not setting: + return False + return setting.get("enabled", False) diff --git a/qwen3-tts-backend/db/models.py b/qwen3-tts-backend/db/models.py index 96a91fe..60c883c 100644 --- a/qwen3-tts-backend/db/models.py +++ b/qwen3-tts-backend/db/models.py @@ -21,7 +21,7 @@ class User(Base): is_active = Column(Boolean, default=True, nullable=False) is_superuser = Column(Boolean, default=False, nullable=False) aliyun_api_key = Column(Text, nullable=True) - user_preferences = Column(JSON, nullable=True, default=lambda: {"default_backend": "local", "onboarding_completed": False}) + user_preferences = Column(JSON, nullable=True, default=lambda: {"default_backend": "aliyun", "onboarding_completed": False}) created_at = Column(DateTime, default=datetime.utcnow, nullable=False) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) @@ -68,3 +68,11 @@ class VoiceCache(Base): __table_args__ = ( Index('idx_user_hash', 'user_id', 'ref_audio_hash'), ) + +class SystemSettings(Base): + __tablename__ = "system_settings" + + id = Column(Integer, primary_key=True, index=True) + key = Column(String(100), unique=True, nullable=False, index=True) + value = Column(JSON, nullable=False) + updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False) diff --git a/qwen3-tts-backend/migrate_user_preferences.py b/qwen3-tts-backend/migrate_user_preferences.py deleted file mode 100644 index 9704168..0000000 --- a/qwen3-tts-backend/migrate_user_preferences.py +++ /dev/null @@ -1,37 +0,0 @@ -import sqlite3 -import json -import os - -DB_PATH = os.path.join(os.path.dirname(__file__), "qwen_tts.db") - -def migrate(): - conn = sqlite3.connect(DB_PATH) - cursor = conn.cursor() - - try: - cursor.execute("SELECT user_preferences FROM users LIMIT 1") - print("Column user_preferences already exists, skipping migration") - except sqlite3.OperationalError: - print("Adding user_preferences column to users table...") - cursor.execute(""" - ALTER TABLE users - ADD COLUMN user_preferences TEXT DEFAULT NULL - """) - - cursor.execute("SELECT id FROM users") - user_ids = cursor.fetchall() - - default_prefs = json.dumps({"default_backend": "local", "onboarding_completed": False}) - for (user_id,) in user_ids: - cursor.execute( - "UPDATE users SET user_preferences = ? WHERE id = ?", - (default_prefs, user_id) - ) - - conn.commit() - print(f"Migration completed: Added user_preferences column and initialized {len(user_ids)} users") - - conn.close() - -if __name__ == "__main__": - migrate() diff --git a/qwen3-tts-backend/schemas/user.py b/qwen3-tts-backend/schemas/user.py index ad71f0e..1a6f666 100644 --- a/qwen3-tts-backend/schemas/user.py +++ b/qwen3-tts-backend/schemas/user.py @@ -120,9 +120,16 @@ class AliyunKeyVerifyResponse(BaseModel): message: str class UserPreferences(BaseModel): - default_backend: str = Field(default="local", pattern="^(local|aliyun)$") + default_backend: str = Field(default="aliyun", pattern="^(local|aliyun)$") onboarding_completed: bool = Field(default=False) class UserPreferencesResponse(BaseModel): - default_backend: str - onboarding_completed: bool + default_backend: str = Field(default="aliyun", pattern="^(local|aliyun)$") + onboarding_completed: bool = Field(default=False) + available_backends: list[str] = Field(default=["aliyun"]) + +class SystemSettingsUpdate(BaseModel): + local_model_enabled: bool + +class SystemSettingsResponse(BaseModel): + local_model_enabled: bool diff --git a/qwen3-tts-backend/scripts/migrate_add_system_settings.py b/qwen3-tts-backend/scripts/migrate_add_system_settings.py new file mode 100644 index 0000000..11a5569 --- /dev/null +++ b/qwen3-tts-backend/scripts/migrate_add_system_settings.py @@ -0,0 +1,70 @@ +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from datetime import datetime +import sys +from pathlib import Path + +sys.path.append(str(Path(__file__).parent.parent)) + +from config import settings + +engine = create_engine( + settings.DATABASE_URL, + connect_args={"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +def upgrade(): + db = SessionLocal() + try: + db.execute(text(""" + CREATE TABLE IF NOT EXISTS system_settings ( + id INTEGER PRIMARY KEY, + key VARCHAR(100) UNIQUE NOT NULL, + value JSON NOT NULL, + updated_at DATETIME NOT NULL + ) + """)) + + db.execute(text(""" + CREATE INDEX IF NOT EXISTS ix_system_settings_key ON system_settings (key) + """)) + + result = db.execute(text( + "SELECT COUNT(*) as count FROM system_settings WHERE key = 'local_model_enabled'" + )) + count = result.fetchone()[0] + + if count == 0: + db.execute(text( + "INSERT INTO system_settings (key, value, updated_at) VALUES " + "('local_model_enabled', '{\"enabled\": false}', :now)" + ), {"now": datetime.utcnow()}) + + db.execute(text(""" + UPDATE users + SET user_preferences = json_set( + COALESCE(user_preferences, '{}'), + '$.default_backend', + 'aliyun' + ) + WHERE json_extract(user_preferences, '$.default_backend') IS NULL + OR json_extract(user_preferences, '$.default_backend') = 'local' + """)) + + db.commit() + print("Migration completed successfully!") + print("- Created system_settings table") + print("- Added local_model_enabled setting (default: false)") + print("- Updated user preferences to use aliyun backend by default") + + except Exception as e: + db.rollback() + print(f"Migration failed: {e}") + raise + finally: + db.close() + +if __name__ == "__main__": + upgrade() diff --git a/qwen3-tts-frontend/index.html b/qwen3-tts-frontend/index.html index 1d40e2e..fc2fcfa 100644 --- a/qwen3-tts-frontend/index.html +++ b/qwen3-tts-frontend/index.html @@ -2,7 +2,7 @@ - + Qwen3-TTS-WebUI diff --git a/qwen3-tts-frontend/package-lock.json b/qwen3-tts-frontend/package-lock.json index 618f181..d77f435 100644 --- a/qwen3-tts-frontend/package-lock.json +++ b/qwen3-tts-frontend/package-lock.json @@ -21,6 +21,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "axios": "^1.13.3", @@ -2019,6 +2020,35 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", diff --git a/qwen3-tts-frontend/package.json b/qwen3-tts-frontend/package.json index 656b8d5..efb79e0 100644 --- a/qwen3-tts-frontend/package.json +++ b/qwen3-tts-frontend/package.json @@ -23,6 +23,7 @@ "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "axios": "^1.13.3", diff --git a/qwen3-tts-frontend/public/favicon.ico b/qwen3-tts-frontend/public/favicon.ico new file mode 100644 index 0000000..0b52767 Binary files /dev/null and b/qwen3-tts-frontend/public/favicon.ico differ diff --git a/qwen3-tts-frontend/public/qwen.svg b/qwen3-tts-frontend/public/qwen.svg index 26de1c9..038633b 100644 --- a/qwen3-tts-frontend/public/qwen.svg +++ b/qwen3-tts-frontend/public/qwen.svg @@ -1 +1,15 @@ -Redirecting to /@lobehub/icons-static-svg@1.78.0/icons/qwen.svg \ No newline at end of file + + + + + + + + + + + + + + + diff --git a/qwen3-tts-frontend/src/components/tts/CustomVoiceForm.tsx b/qwen3-tts-frontend/src/components/tts/CustomVoiceForm.tsx index f0cf737..7ab01da 100644 --- a/qwen3-tts-frontend/src/components/tts/CustomVoiceForm.tsx +++ b/qwen3-tts-frontend/src/components/tts/CustomVoiceForm.tsx @@ -33,7 +33,6 @@ const formSchema = z.object({ top_k: z.number().min(1).max(100).optional(), top_p: z.number().min(0).max(1).optional(), repetition_penalty: z.number().min(0).max(2).optional(), - backend: z.string().optional(), }) type FormData = z.infer @@ -77,16 +76,9 @@ const CustomVoiceForm = forwardRef((_props, ref) => { top_k: 20, top_p: 0.7, repetition_penalty: 1.05, - backend: preferences?.default_backend || 'local', }, }) - useEffect(() => { - if (preferences?.default_backend) { - setValue('backend', preferences.default_backend) - } - }, [preferences?.default_backend, setValue]) - useImperativeHandle(ref, () => ({ loadParams: (params: any) => { setValue('text', params.text || '') @@ -142,22 +134,6 @@ const CustomVoiceForm = forwardRef((_props, ref) => { return (
-
- - -
-
setValue('backend', value)} - > - - - - - 本地模型 - 阿里云 API - - -
-
setValue('backend', value)} - > - - - - - 本地模型 - 阿里云 API - - -
-