From 3a0dff87bfed675713784eb0e577e548c8932d85 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 12 Jun 2026 14:25:57 +0800 Subject: [PATCH] Center responsive layout and add PWA install support for mobile, tablet, and desktop. Co-authored-by: Cursor --- app.py | 237 ++++++++++++++++++++++++++++++++------- pwa/icons/icon.svg | 6 + pwa/manifest.webmanifest | 27 +++++ pwa/sw.js | 50 +++++++++ requirements.txt | 48 ++++---- 5 files changed, 303 insertions(+), 65 deletions(-) create mode 100644 pwa/icons/icon.svg create mode 100644 pwa/manifest.webmanifest create mode 100644 pwa/sw.js diff --git a/app.py b/app.py index 0e04348..f10ba2d 100644 --- a/app.py +++ b/app.py @@ -179,13 +179,132 @@ def ui_full_pipeline( # --------------------------------------------------------------------------- # Gradio 界面 # --------------------------------------------------------------------------- +APP_ROOT = Path(__file__).resolve().parent +PWA_DIR = APP_ROOT / "pwa" + +PWA_HEAD = """ + + + + + + + + + + +""" + 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: 1400px !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}) + +
📱 安装为 App:手机/平板用浏览器菜单「添加到主屏幕」;电脑 Chrome/Edge 地址栏点击「安装 Trading Studio」图标即可。
""", 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, ) diff --git a/pwa/icons/icon.svg b/pwa/icons/icon.svg new file mode 100644 index 0000000..37fc39f --- /dev/null +++ b/pwa/icons/icon.svg @@ -0,0 +1,6 @@ + + + + + TS + diff --git a/pwa/manifest.webmanifest b/pwa/manifest.webmanifest new file mode 100644 index 0000000..a9812ec --- /dev/null +++ b/pwa/manifest.webmanifest @@ -0,0 +1,27 @@ +{ + "name": "Trading Studio 交易复盘配音", + "short_name": "TradingStudio", + "description": "本地量化交易复盘 → B 站配音生产流水线", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "background_color": "#0f1419", + "theme_color": "#2563eb", + "lang": "zh-CN", + "categories": ["productivity", "utilities"], + "icons": [ + { + "src": "/pwa/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/pwa/icons/icon.svg", + "sizes": "512x512", + "type": "image/svg+xml", + "purpose": "maskable" + } + ] +} diff --git a/pwa/sw.js b/pwa/sw.js new file mode 100644 index 0000000..5e91af4 --- /dev/null +++ b/pwa/sw.js @@ -0,0 +1,50 @@ +/** + * Trading Studio PWA Service Worker + * 缓存应用壳,支持离线打开已访问页面;API 请求始终走网络。 + */ +const CACHE_NAME = "trading-studio-v1"; +const SHELL = ["/", "/manifest.webmanifest"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL)).catch(() => {}) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + const { request } = event; + const url = new URL(request.url); + + // API / 文件上传 / Gradio 动态接口不走缓存 + if ( + request.method !== "GET" || + url.pathname.startsWith("/gradio_api") || + url.pathname.startsWith("/file=") || + url.pathname.startsWith("/upload") || + url.pathname.includes("call") + ) { + return; + } + + event.respondWith( + fetch(request) + .then((response) => { + if (response.ok && url.origin === self.location.origin) { + const clone = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(request, clone)); + } + return response; + }) + .catch(() => caches.match(request).then((r) => r || caches.match("/"))) + ); +}); diff --git a/requirements.txt b/requirements.txt index 95856c1..780a454 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,25 @@ -# Trading Studio 依赖清单 -# CUDA 版 PyTorch 请按 DEPLOY.md 单独安装(cu121),此处不重复指定 - -# Web 中控 -gradio>=4.44.0 - -# 语音识别(CUDA 加速) -faster-whisper>=1.0.0 - -# 远程 LLM 通信 -requests>=2.31.0 - -# 语音合成 -ChatTTS @ git+https://github.com/2noise/ChatTTS.git -torchaudio>=2.1.0 -scipy>=1.11.0 -numpy>=1.24.0 -librosa>=0.10.0 - -# 音频处理辅助 -soundfile>=0.12.0 - -# PM2 通过 Node.js 全局安装,不在 pip 范围内 +# Trading Studio 依赖清单 +# CUDA 版 PyTorch 请按 DEPLOY.md 单独安装(cu121),此处不重复指定 + +# Web 中控 +gradio>=4.44.0 +fastapi>=0.100.0 +uvicorn[standard]>=0.23.0 + +# 语音识别(CUDA 加速) +faster-whisper>=1.0.0 + +# 远程 LLM 通信 +requests>=2.31.0 + +# 语音合成 +ChatTTS @ git+https://github.com/2noise/ChatTTS.git +torchaudio>=2.1.0 +scipy>=1.11.0 +numpy>=1.24.0 +librosa>=0.10.0 + +# 音频处理辅助 +soundfile>=0.12.0 + +# PM2 通过 Node.js 全局安装,不在 pip 范围内