feat: Add systemd service, configure API for proxy deployment, and enhance mobile audio playback with token authentication.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -29,3 +29,5 @@ qwen3-tts-frontend/.env.local
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
样本.mp3
|
样本.mp3
|
||||||
aliyun.md
|
aliyun.md
|
||||||
|
nginx.conf
|
||||||
|
deploy.md
|
||||||
@@ -9,7 +9,9 @@ from slowapi.util import get_remote_address
|
|||||||
|
|
||||||
from core.database import get_db
|
from core.database import get_db
|
||||||
from core.config import settings
|
from core.config import settings
|
||||||
|
from core.security import decode_access_token
|
||||||
from db.models import Job, JobStatus, User
|
from db.models import Job, JobStatus, User
|
||||||
|
from db.crud import get_user_by_username
|
||||||
from api.auth import get_current_user
|
from api.auth import get_current_user
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -18,6 +20,42 @@ router = APIRouter(prefix="/jobs", tags=["jobs"])
|
|||||||
limiter = Limiter(key_func=get_remote_address)
|
limiter = Limiter(key_func=get_remote_address)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_from_token_or_query(
|
||||||
|
request: Request,
|
||||||
|
token: Optional[str] = Query(None),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
) -> User:
|
||||||
|
auth_token = None
|
||||||
|
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
if auth_header and auth_header.startswith("Bearer "):
|
||||||
|
auth_token = auth_header.split(" ")[1]
|
||||||
|
elif token:
|
||||||
|
auth_token = token
|
||||||
|
|
||||||
|
if not auth_token:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Missing authentication token"
|
||||||
|
)
|
||||||
|
|
||||||
|
username = decode_access_token(auth_token)
|
||||||
|
if username is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="Invalid or expired token"
|
||||||
|
)
|
||||||
|
|
||||||
|
user = get_user_by_username(db, username=username)
|
||||||
|
if user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=401,
|
||||||
|
detail="User not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{job_id}")
|
@router.get("/{job_id}")
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def get_job(
|
async def get_job(
|
||||||
@@ -140,7 +178,7 @@ async def delete_job(
|
|||||||
async def download_job_output(
|
async def download_job_output(
|
||||||
request: Request,
|
request: Request,
|
||||||
job_id: int,
|
job_id: int,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_user_from_token_or_query),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
job = db.query(Job).filter(Job.id == job_id).first()
|
job = db.query(Job).filter(Job.id == job_id).first()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class Settings(BaseSettings):
|
|||||||
DATABASE_URL: str = Field(default="sqlite:///./qwen_tts.db")
|
DATABASE_URL: str = Field(default="sqlite:///./qwen_tts.db")
|
||||||
CACHE_DIR: str = Field(default="./voice_cache")
|
CACHE_DIR: str = Field(default="./voice_cache")
|
||||||
OUTPUT_DIR: str = Field(default="./outputs")
|
OUTPUT_DIR: str = Field(default="./outputs")
|
||||||
BASE_URL: str = Field(default="http://localhost:8000")
|
BASE_URL: str = Field(default="")
|
||||||
|
|
||||||
MODEL_DEVICE: str = Field(default="cuda:0")
|
MODEL_DEVICE: str = Field(default="cuda:0")
|
||||||
MODEL_BASE_PATH: str = Field(default="../Qwen")
|
MODEL_BASE_PATH: str = Field(default="../Qwen")
|
||||||
|
|||||||
@@ -119,13 +119,14 @@ app = FastAPI(
|
|||||||
app.state.limiter = limiter
|
app.state.limiter = limiter
|
||||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||||
|
|
||||||
app.add_middleware(
|
if settings.LOG_LEVEL == "debug":
|
||||||
CORSMiddleware,
|
app.add_middleware(
|
||||||
allow_origins=["*"],
|
CORSMiddleware,
|
||||||
allow_credentials=True,
|
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||||
allow_methods=["*"],
|
allow_credentials=True,
|
||||||
allow_headers=["*"],
|
allow_methods=["*"],
|
||||||
)
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
app.include_router(auth.router)
|
app.include_router(auth.router)
|
||||||
app.include_router(jobs.router)
|
app.include_router(jobs.router)
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -11,15 +11,34 @@ interface AudioPlayerProps {
|
|||||||
jobId: number
|
jobId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMobileDevice = () => {
|
||||||
|
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||||
|
}
|
||||||
|
|
||||||
const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
||||||
const [blobUrl, setBlobUrl] = useState<string>('')
|
const [blobUrl, setBlobUrl] = useState<string>('')
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadError, setLoadError] = useState<string | null>(null)
|
const [loadError, setLoadError] = useState<string | null>(null)
|
||||||
|
const [useMobileMode, setUseMobileMode] = useState(false)
|
||||||
const previousAudioUrlRef = useRef<string>('')
|
const previousAudioUrlRef = useRef<string>('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUseMobileMode(isMobileDevice())
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!audioUrl || audioUrl === previousAudioUrlRef.current) return
|
if (!audioUrl || audioUrl === previousAudioUrlRef.current) return
|
||||||
|
|
||||||
|
if (useMobileMode) {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const separator = audioUrl.includes('?') ? '&' : '?'
|
||||||
|
const urlWithToken = token ? `${audioUrl}${separator}token=${token}` : audioUrl
|
||||||
|
setBlobUrl(urlWithToken)
|
||||||
|
previousAudioUrlRef.current = audioUrl
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let active = true
|
let active = true
|
||||||
const prevBlobUrl = blobUrl
|
const prevBlobUrl = blobUrl
|
||||||
|
|
||||||
@@ -55,7 +74,7 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
|
|||||||
return () => {
|
return () => {
|
||||||
active = false
|
active = false
|
||||||
}
|
}
|
||||||
}, [audioUrl])
|
}, [audioUrl, useMobileMode])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -342,6 +342,10 @@ export const jobApi = {
|
|||||||
getAudioUrl: (id: number, audioPath?: string): string => {
|
getAudioUrl: (id: number, audioPath?: string): string => {
|
||||||
if (audioPath) {
|
if (audioPath) {
|
||||||
if (audioPath.startsWith('http')) {
|
if (audioPath.startsWith('http')) {
|
||||||
|
if (audioPath.includes('localhost') || audioPath.includes('127.0.0.1')) {
|
||||||
|
const url = new URL(audioPath)
|
||||||
|
return `${import.meta.env.VITE_API_URL}${url.pathname}`
|
||||||
|
}
|
||||||
return audioPath
|
return audioPath
|
||||||
} else {
|
} else {
|
||||||
const baseUrl = import.meta.env.VITE_API_URL
|
const baseUrl = import.meta.env.VITE_API_URL
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const PRESET_INSTRUCTS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '温柔关怀',
|
label: '温柔关怀',
|
||||||
instruct: '温柔体贴的女性声音,语速平缓,音调柔和,充满关怀和安慰',
|
instruct: '温柔体贴,语速平缓,音调柔和,充满关怀和安慰',
|
||||||
text: '别担心,一切都会好起来的。我会一直陪在你身边。',
|
text: '别担心,一切都会好起来的。我会一直陪在你身边。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,17 +108,17 @@ export const PRESET_INSTRUCTS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '专业播音员',
|
label: '专业播音员',
|
||||||
instruct: '专业新闻播音员。性别:女性。音高:中等偏高,音域稳定。语速:标准播音语速,吐字清晰。音量:适中,音色饱满。情绪:沉稳专业,不带个人感情色彩。语调:平直中略有起伏,重点词汇加重。性格特征:严谨、客观、权威。',
|
instruct: '专业新闻播音员。语速:标准播音语速,吐字清晰。情绪:沉稳专业,不带个人感情色彩。语调:平直中略有起伏,重点词汇加重。性格特征:严谨、客观、权威。',
|
||||||
text: '据新华社报道,我国航天事业取得重大突破,神舟系列飞船成功完成载人飞行任务。',
|
text: '据新华社报道,我国航天事业取得重大突破,神舟系列飞船成功完成载人飞行任务。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '温暖导师',
|
label: '温暖导师',
|
||||||
instruct: '温暖的中年女性导师。音色:温和醇厚,带有亲和力。语速:不急不缓,娓娓道来。音调:平稳中带有鼓励性上扬。情绪:关怀、耐心、鼓励。性格:善解人意,循循善诱,充满正能量。适合场景:心理咨询、教育引导。',
|
instruct: '温暖导师。语速:不急不缓,娓娓道来。音调:平稳中带有鼓励性上扬。情绪:关怀、耐心、鼓励。性格:善解人意,循循善诱,充满正能量。',
|
||||||
text: '每个人都有自己的节奏,不要着急。慢慢来,你一定能找到属于自己的那条路。',
|
text: '每个人都有自己的节奏,不要着急。慢慢来,你一定能找到属于自己的那条路。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '活力少年',
|
label: '活力少年',
|
||||||
instruct: '充满活力的青少年男性。音高:略高,富有朝气。语速:偏快,吐字利落。音量:响亮明快。情绪:开朗乐观,精力充沛。语调:跳跃感强,抑扬顿挫。性格:外向、自信、热情,充满青春气息。',
|
instruct: '充满活力。语速:偏快,吐字利落。情绪:开朗乐观,精力充沛。语调:跳跃感强,抑扬顿挫。性格:外向、自信、热情,充满青春气息。',
|
||||||
text: '哇,这个游戏太酷了!咱们组队一起玩吧,我保证带你们飞!',
|
text: '哇,这个游戏太酷了!咱们组队一起玩吧,我保证带你们飞!',
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export default defineConfig({
|
|||||||
output: {
|
output: {
|
||||||
manualChunks: {
|
manualChunks: {
|
||||||
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
|
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
|
||||||
|
'icons': ['lucide-react'],
|
||||||
'ui-vendor': [
|
'ui-vendor': [
|
||||||
'@radix-ui/react-tabs',
|
'@radix-ui/react-tabs',
|
||||||
'@radix-ui/react-label',
|
'@radix-ui/react-label',
|
||||||
|
|||||||
Reference in New Issue
Block a user