@@ -179,13 +179,132 @@ def ui_full_pipeline(
# ---------------------------------------------------------------------------
# Gradio 界面
# ---------------------------------------------------------------------------
APP_ROOT = Path ( __file__ ) . resolve ( ) . parent
PWA_DIR = APP_ROOT / " pwa "
PWA_HEAD = """
<meta name= " viewport " content= " width=device-width, initial-scale=1, viewport-fit=cover " >
<meta name= " theme-color " content= " #2563eb " >
<meta name= " mobile-web-app-capable " content= " yes " >
<meta name= " apple-mobile-web-app-capable " content= " yes " >
<meta name= " apple-mobile-web-app-status-bar-style " content= " black-translucent " >
<meta name= " apple-mobile-web-app-title " content= " Trading Studio " >
<link rel= " manifest " href= " /manifest.webmanifest " >
<link rel= " icon " href= " /pwa/icons/icon.svg " type= " image/svg+xml " >
<link rel= " apple-touch-icon " href= " /pwa/icons/icon.svg " >
<script>
if ( " serviceWorker " in navigator) {
window.addEventListener( " load " , function () {
navigator.serviceWorker.register( " /sw.js " , { scope: " / " }).catch(function () {} );
});
}
</script>
"""
CUSTOM_CSS = """
/* ========== 高对比度暗色主题(确保文字清晰可读) ========== */
/* ========== 居中布局 + 响应式 + 高对比度 ========== */
html, body {
width: 100 % !important;
min-height: 100vh !important;
margin: 0 !important;
padding: 0 !important;
background: #0f1419 !important;
overflow-x: hidden !important;
}
/* 外层全宽背景,内容区水平居中 */
gradio-app,
.gradio-app,
#main,
.app,
.fillable,
.contain {
width: 100 % !important;
max-width: 100 % !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: flex-start !important;
background: #0f1419 !important;
box-sizing: border-box !important;
}
/* 核心内容容器居中 */
.gradio-container {
background: #0f1419 !important;
color: #eef2f7 !important;
font-size: 15px !important;
max- width: 14 00px !important;
width: 100 % !important;
max-width: min(1200px, 94vw) !important;
margin-left: auto !important;
margin-right: auto !important;
padding: 16px clamp(12px, 3vw, 32px) !important;
box-sizing: border-box !important;
float: none !important;
}
/* 带鱼屏 / 超宽屏 */
@media (min-width: 1920px) {
.gradio-container { max-width: min(1280px, 82vw) !important; }
}
@media (min-width: 2560px) {
.gradio-container { max-width: min(1360px, 68vw) !important; }
}
@media (min-width: 3440px) {
.gradio-container { max-width: 1440px !important; }
}
/* 平板 */
@media (max-width: 1024px) {
.gradio-container {
max-width: 100 % !important;
padding: 14px 18px !important;
}
}
/* 手机 */
@media (max-width: 640px) {
.gradio-container {
max-width: 100 % !important;
padding: 10px 12px !important;
font-size: 14px !important;
}
.gradio-container h1 { font-size: 1.35rem !important; }
.gradio-container .tab-nav {
flex-wrap: wrap !important;
gap: 6px !important;
}
.gradio-container .tab-nav button {
flex: 1 1 28 % !important;
min-width: 88px !important;
padding: 8px 8px !important;
font-size: 0.8rem !important;
}
.status-row,
.status-row > div,
.pipeline-steps,
.pipeline-steps > div,
.pipeline-output-row,
.pipeline-output-row > div {
flex-direction: column !important;
width: 100 % !important;
}
.status-row button,
.pipeline-steps button,
.pipeline-output-row button {
width: 100 % !important;
}
.dark-panel table {
font-size: 0.85rem !important;
display: block !important;
overflow-x: auto !important;
}
.install-hint { font-size: 0.85rem !important; }
}
/* 触摸设备:按钮最小点击区域 44px */
@media (hover: none) and (pointer: coarse) {
.gradio-container button { min-height: 44px !important; }
}
/* 全局文字 */
@@ -198,7 +317,6 @@ CUSTOM_CSS = """
color: #eef2f7 !important;
}
/* 标题 */
.gradio-container h1 {
color: #ffffff !important;
font-size: 1.75rem !important;
@@ -211,7 +329,6 @@ CUSTOM_CSS = """
font-weight: 600 !important;
}
/* 标签 */
.gradio-container .block-label,
.gradio-container label,
.gradio-container .label-wrap span {
@@ -220,7 +337,6 @@ CUSTOM_CSS = """
font-size: 0.95rem !important;
}
/* 输入框 / 文本域 */
.gradio-container textarea,
.gradio-container input[type= " text " ],
.gradio-container .wrap textarea,
@@ -238,14 +354,12 @@ CUSTOM_CSS = """
opacity: 1 !important;
}
/* 只读日志框 */
.gradio-container .wrap .readonly textarea {
background: #111827 !important;
color: #e5e7eb !important;
border-color: #374151 !important;
}
/* Tab 标签页 */
.gradio-container .tab-nav button {
color: #9ca3af !important;
font-weight: 600 !important;
@@ -258,7 +372,6 @@ CUSTOM_CSS = """
border-bottom: 3px solid #3b82f6 !important;
}
/* 按钮 */
.gradio-container button.primary,
.gradio-container .primary {
background: #2563eb !important;
@@ -267,9 +380,7 @@ CUSTOM_CSS = """
font-size: 0.95rem !important;
border: 1px solid #3b82f6 !important;
}
.gradio-container button.primary:hover {
background: #1d4ed8 !important;
}
.gradio-container button.primary:hover { background: #1d4ed8 !important; }
.gradio-container button.secondary,
.gradio-container button:not(.primary) {
color: #e5e7eb !important;
@@ -278,13 +389,14 @@ CUSTOM_CSS = """
font-weight: 600 !important;
}
/* 顶部信息面板 */
.dark-panel {
border: 1px solid #3b82f6 !important;
border-radius: 10px !important;
padding: 18px 20px !important;
background: #1a2332 !important;
margin-bottom: 16px !important;
width: 100 % !important;
box-sizing: border-box !important;
}
.dark-panel code {
color: #93c5fd !important;
@@ -293,7 +405,16 @@ CUSTOM_CSS = """
border-radius: 4px !important;
}
/* 状态卡片 */
.install-hint {
margin-top: 12px !important;
padding: 10px 14px !important;
border-radius: 8px !important;
background: #1e3a5f !important;
border: 1px dashed #3b82f6 !important;
color: #bfdbfe !important;
font-size: 0.9rem !important;
}
.status-card {
border-radius: 10px;
padding: 14px 16px;
@@ -301,13 +422,14 @@ CUSTOM_CSS = """
border-left: 5px solid #6b7280;
background: #1f2937;
min-height: 72px;
width: 100 % ;
box-sizing: border-box;
}
.status-card .status-title {
font-size: 0.85rem;
font-weight: 700;
color: #93c5fd !important;
margin-bottom: 8px;
letter-spacing: 0.03em;
}
.status-card .status-body {
font-size: 0.92rem;
@@ -315,23 +437,13 @@ CUSTOM_CSS = """
color: #f3f4f6 !important;
word-break: break-word;
}
.status-ok {
border-left-color: #22c55e !important;
background: #14291a !important;
}
.status-ok { border-left-color: #22c55e !important; background: #14291a !important; }
.status-ok .status-body { color: #bbf7d0 !important; }
.status-warn {
border-left-color: #f59e0b !important;
background: #2a2010 !important;
}
.status-warn { border-left-color: #f59e0b !important; background: #2a2010 !important; }
.status-warn .status-body { color: #fde68a !important; }
.status-err {
border-left-color: #ef4444 !important;
background: #2a1515 !important;
}
.status-err { border-left-color: #ef4444 !important; background: #2a1515 !important; }
.status-err .status-body { color: #fecaca !important; }
/* 音频上传区 */
.gradio-container .audio-container,
.gradio-container .upload-container {
border: 2px dashed #4b5563 !important;
@@ -413,14 +525,16 @@ def build_app() -> gr.Blocks:
| ChatTTS | 本地 GPU 固定音色合成 |
> 仓库: [ { GIT_REPO_URL } ]( { GIT_REPO_URL } )
<div class= " install-hint " >📱 <strong>安装为 App: </strong>手机/平板用浏览器菜单「添加到主屏幕」;电脑 Chrome/Edge 地址栏点击「安装 Trading Studio」图标即可。</div>
""" ,
elem_classes = [ " dark-panel " ] ,
)
with gr . Row ( ) :
with gr . Row ( elem_classes = [ " status-row " ] ) :
ollama_status = gr . HTML ( value = _status_html ( " Ollama 节点 " , " 正在检测... " , " warn " ) )
speaker_status = gr . HTML ( value = _status_html ( " 音色状态 " , " 正在检测... " , " warn " ) )
refresh_btn = gr . Button ( " 🔄 刷新状态 " , variant = " secondary " , scale = 0 )
refresh_btn = gr . Button ( " 🔄 刷新状态 " , variant = " secondary " , scale = 0 , min_width = 120 )
refresh_btn . click (
fn = lambda : ( ui_check_ollama_html ( ) , ui_speaker_status_html ( ) ) ,
@@ -455,7 +569,7 @@ def build_app() -> gr.Blocks:
# ---- Tab 2: 分步操作 ----
with gr . Tab ( " 🔧 分步流水线 " ) :
with gr . Row ( ) :
with gr . Row ( elem_classes = [ " pipeline-steps " ] ) :
with gr . Column ( scale = 1 ) :
gr . Markdown ( " ### Step 1 · 音频极速识别 " )
rec_audio = gr . Audio (
@@ -513,7 +627,7 @@ def build_app() -> gr.Blocks:
)
pipeline_btn = gr . Button ( " ▶ 启动全流程 " , variant = " primary " , size = " lg " )
pipeline_log = gr . Textbox ( label = " 流水线日志 " , lines = 6 , interactive = False )
with gr . Row ( ) :
with gr . Row ( elem_classes = [ " pipeline-output-row " ] ) :
pipe_raw = gr . Textbox ( label = " 转写原文 " , lines = 6 )
pipe_polished = gr . Textbox ( label = " 润色稿 " , lines = 6 )
pipe_output = gr . Audio ( label = " 成品配音 " , type = " filepath " )
@@ -532,18 +646,57 @@ def build_app() -> gr.Blocks:
return demo
def main ( ) - > None :
""" 主入口:启动 Gradio 服务 。"""
logger . info ( " Trading Studio 启动中... HOST= %s PORT= %s " , HOST , PORT )
app = build_app ( )
app . launch (
server_name = HOST ,
server_port = PORT ,
share = False ,
show_error = True ,
theme = build_theme ( ) ,
def create_fastapi_app ( ) :
""" 创建 FastAPI 应用:Gradio 中控 + PWA 静态资源 。"""
from fastapi import FastAPI
from fastapi . responses import FileResponse
from fastapi . staticfiles import StaticFiles
fastapi_app = FastAPI ( title = " Trading Studio " , docs_url = None , redoc_url = None )
if PWA_DIR . is_dir ( ) :
fastapi_app . mount ( " /pwa " , StaticFiles ( directory = str ( PWA_DIR ) ) , name = " pwa " )
@fastapi_app.get ( " /manifest.webmanifest " )
async def manifest ( ) :
return FileResponse (
PWA_DIR / " manifest.webmanifest " ,
media_type = " application/manifest+json " ,
)
@fastapi_app.get ( " /sw.js " )
async def service_worker ( ) :
return FileResponse (
PWA_DIR / " sw.js " ,
media_type = " application/javascript " ,
headers = { " Service-Worker-Allowed " : " / " } ,
)
blocks = build_app ( )
gr . mount_gradio_app (
fastapi_app ,
blocks ,
path = " / " ,
css = CUSTOM_CSS ,
allowed_paths = [ str ( Path ( __file__ ) . resolve ( ) . parent / " outputs " ) ] ,
theme = build_theme ( ) ,
head = PWA_HEAD ,
allowed_paths = [ str ( APP_ROOT / " outputs " ) ] ,
)
return fastapi_app
def main ( ) - > None :
""" 主入口:FastAPI + Gradio + PWA。 """
import uvicorn
logger . info ( " Trading Studio 启动中... HOST= %s PORT= %s " , HOST , PORT )
app = create_fastapi_app ( )
uvicorn . run (
app ,
host = HOST ,
port = PORT ,
log_level = " info " ,
access_log = True ,
)