Optimize tablet load: defer health check, lighten service worker, drop Google Fonts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -197,9 +197,14 @@ PWA_HEAD = """
|
|||||||
var deferredPrompt = null;
|
var deferredPrompt = null;
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
window.addEventListener("load", function () {
|
function registerSW() {
|
||||||
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(function () {});
|
navigator.serviceWorker.register("/sw.js", { scope: "/" }).catch(function () {});
|
||||||
});
|
}
|
||||||
|
if ("requestIdleCallback" in window) {
|
||||||
|
requestIdleCallback(registerSW, { timeout: 5000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(registerSW, 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStandalone() {
|
function isStandalone() {
|
||||||
@@ -711,24 +716,37 @@ def _status_html(title: str, message: str, level: str = "warn") -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def ui_check_ollama_html() -> str:
|
def ui_check_ollama_html(force: bool = False) -> str:
|
||||||
ok, msg = check_ollama_health()
|
ok, msg = check_ollama_health(force=force)
|
||||||
return _status_html("Ollama 节点", msg, "ok" if ok else "err")
|
return _status_html("Ollama 节点", msg, "ok" if ok else "err")
|
||||||
|
|
||||||
|
|
||||||
|
def ui_initial_load() -> tuple[str, str]:
|
||||||
|
"""首屏立即返回,不发起网络请求,避免平板白屏等待。"""
|
||||||
|
return (
|
||||||
|
_status_html("Ollama 节点", "后台检测中,请稍候…", "warn"),
|
||||||
|
ui_speaker_status_html(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def ui_refresh_status_html(force: bool = False) -> tuple[str, str]:
|
||||||
|
"""刷新 Ollama + 音色状态(供 Timer / 按钮调用)。"""
|
||||||
|
return ui_check_ollama_html(force=force), ui_speaker_status_html()
|
||||||
|
|
||||||
|
|
||||||
def ui_speaker_status_html() -> str:
|
def ui_speaker_status_html() -> str:
|
||||||
ok, msg = speaker_is_ready()
|
ok, msg = speaker_is_ready()
|
||||||
return _status_html("音色状态", msg, "ok" if ok else "warn")
|
return _status_html("音色状态", msg, "ok" if ok else "warn")
|
||||||
|
|
||||||
|
|
||||||
def build_theme() -> gr.themes.Base:
|
def build_theme() -> gr.themes.Base:
|
||||||
"""高对比度暗色主题(Gradio 6.0 需在 launch() 传入)。"""
|
"""高对比度暗色主题;使用系统字体,避免平板拉取 Google Fonts 卡顿。"""
|
||||||
return gr.themes.Base(
|
return gr.themes.Base(
|
||||||
primary_hue="blue",
|
primary_hue="blue",
|
||||||
secondary_hue="blue",
|
secondary_hue="blue",
|
||||||
neutral_hue="slate",
|
neutral_hue="slate",
|
||||||
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
|
font=["system-ui", "-apple-system", "Segoe UI", "Roboto", "sans-serif"],
|
||||||
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "Consolas", "monospace"],
|
font_mono=["Consolas", "Monaco", "Courier New", "monospace"],
|
||||||
).set(
|
).set(
|
||||||
body_background_fill="#0f1419",
|
body_background_fill="#0f1419",
|
||||||
body_background_fill_dark="#0f1419",
|
body_background_fill_dark="#0f1419",
|
||||||
@@ -780,7 +798,25 @@ def build_app() -> gr.Blocks:
|
|||||||
refresh_btn = gr.Button("🔄 刷新状态", variant="secondary", scale=0, min_width=120)
|
refresh_btn = gr.Button("🔄 刷新状态", variant="secondary", scale=0, min_width=120)
|
||||||
|
|
||||||
refresh_btn.click(
|
refresh_btn.click(
|
||||||
fn=lambda: (ui_check_ollama_html(), ui_speaker_status_html()),
|
fn=lambda: ui_refresh_status_html(force=True),
|
||||||
|
outputs=[ollama_status, speaker_status],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 首屏秒开:仅本地检测音色,Ollama 延后到 Timer
|
||||||
|
demo.load(
|
||||||
|
fn=ui_initial_load,
|
||||||
|
outputs=[ollama_status, speaker_status],
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1 秒后后台检测 Ollama;之后每 30s 刷新(30s 内走缓存)
|
||||||
|
status_timer = gr.Timer(value=1, active=True)
|
||||||
|
status_timer.tick(
|
||||||
|
fn=lambda: ui_refresh_status_html(force=False),
|
||||||
|
outputs=[ollama_status, speaker_status],
|
||||||
|
)
|
||||||
|
status_timer_slow = gr.Timer(value=30, active=True)
|
||||||
|
status_timer_slow.tick(
|
||||||
|
fn=lambda: ui_refresh_status_html(force=True),
|
||||||
outputs=[ollama_status, speaker_status],
|
outputs=[ollama_status, speaker_status],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -886,11 +922,6 @@ def build_app() -> gr.Blocks:
|
|||||||
[pipe_raw, pipe_polished, pipe_output, pipeline_log],
|
[pipe_raw, pipe_polished, pipe_output, pipeline_log],
|
||||||
)
|
)
|
||||||
|
|
||||||
demo.load(
|
|
||||||
fn=lambda: (ui_check_ollama_html(), ui_speaker_status_html()),
|
|
||||||
outputs=[ollama_status, speaker_status],
|
|
||||||
)
|
|
||||||
|
|
||||||
return demo
|
return demo
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +1,90 @@
|
|||||||
"""
|
"""
|
||||||
Trading Studio 全局配置模块
|
Trading Studio 全局配置模块
|
||||||
统一存放局域网节点、模型名称、固定 Prompt 及本地路径。
|
统一存放局域网节点、模型名称、固定 Prompt 及本地路径。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 网络与服务
|
# 网络与服务
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 远程 Ollama 节点(局域网大模型审查润色)
|
# 远程 Ollama 节点(局域网大模型审查润色)
|
||||||
OLLAMA_HOST = "192.168.8.64"
|
OLLAMA_HOST = "192.168.8.64"
|
||||||
OLLAMA_PORT = 11434
|
OLLAMA_PORT = 11434
|
||||||
OLLAMA_URL = f"http://{OLLAMA_HOST}:{OLLAMA_PORT}/api/chat"
|
OLLAMA_URL = f"http://{OLLAMA_HOST}:{OLLAMA_PORT}/api/chat"
|
||||||
|
|
||||||
# 指定无限制版 Gemma4 模型
|
# 指定无限制版 Gemma4 模型
|
||||||
MODEL_NAME = "huihui_ai/gemma-4-abliterated:e4b"
|
MODEL_NAME = "huihui_ai/gemma-4-abliterated:e4b"
|
||||||
|
|
||||||
# Gradio 中控固定端口(硬性死规则)
|
# Gradio 中控固定端口(硬性死规则)
|
||||||
HOST = "0.0.0.0"
|
HOST = "0.0.0.0"
|
||||||
PORT = 5683
|
PORT = 5683
|
||||||
|
|
||||||
# HTTP 请求超时(秒)
|
# HTTP 请求超时(秒)
|
||||||
OLLAMA_TIMEOUT = 60
|
OLLAMA_TIMEOUT = 60
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# 健康检查快速超时(秒)— 避免平板首屏被长时间阻塞
|
||||||
# LLM 系统 Prompt
|
HEALTH_CHECK_CONNECT_TIMEOUT = 2
|
||||||
# ---------------------------------------------------------------------------
|
HEALTH_CHECK_READ_TIMEOUT = 3
|
||||||
SYSTEM_PROMPT = (
|
HEALTH_CHECK_CACHE_SECONDS = 30
|
||||||
"你是一个冷静、极其严格的数字资产量化交易员。"
|
|
||||||
"请把下面这段口语化、包含结巴和逻辑混乱的交易复盘录音转写,"
|
# ---------------------------------------------------------------------------
|
||||||
"润色成一段逻辑清晰、行文通顺的 B 站长视频反思配音稿。"
|
# LLM 系统 Prompt
|
||||||
"语气要内向、克制、严谨。"
|
# ---------------------------------------------------------------------------
|
||||||
"如果原视频中有由于心态不好、违背交易纪律(如手贱乱开仓、提前平仓)"
|
SYSTEM_PROMPT = (
|
||||||
"导致少赚或亏损的部分,请用冷酷、严厉的语气狠狠地自我吐槽、反思该点。"
|
"你是一个冷静、极其严格的数字资产量化交易员。"
|
||||||
"去掉所有无意义的口头禅,字数不做删减。"
|
"请把下面这段口语化、包含结巴和逻辑混乱的交易复盘录音转写,"
|
||||||
)
|
"润色成一段逻辑清晰、行文通顺的 B 站长视频反思配音稿。"
|
||||||
|
"语气要内向、克制、严谨。"
|
||||||
# ---------------------------------------------------------------------------
|
"如果原视频中有由于心态不好、违背交易纪律(如手贱乱开仓、提前平仓)"
|
||||||
# Faster-Whisper 配置
|
"导致少赚或亏损的部分,请用冷酷、严厉的语气狠狠地自我吐槽、反思该点。"
|
||||||
# ---------------------------------------------------------------------------
|
"去掉所有无意义的口头禅,字数不做删减。"
|
||||||
WHISPER_MODEL_SIZE = "small"
|
)
|
||||||
WHISPER_DEVICE = "cuda"
|
|
||||||
WHISPER_COMPUTE_TYPE = "float16"
|
# ---------------------------------------------------------------------------
|
||||||
WHISPER_LANGUAGE = "zh"
|
# Faster-Whisper 配置
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
# ---------------------------------------------------------------------------
|
WHISPER_MODEL_SIZE = "small"
|
||||||
# ChatTTS 配置
|
WHISPER_DEVICE = "cuda"
|
||||||
# ---------------------------------------------------------------------------
|
WHISPER_COMPUTE_TYPE = "float16"
|
||||||
# 标准生产安装路径(/opt,root 部署)
|
WHISPER_LANGUAGE = "zh"
|
||||||
INSTALL_DIR = Path("/opt/Trading_Studio")
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
# 项目根目录(开发/生产均自适应,以实际 app.py 所在目录为准)
|
# ChatTTS 配置
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
# ---------------------------------------------------------------------------
|
||||||
|
# 标准生产安装路径(/opt,root 部署)
|
||||||
# 固定音色 Embedding 存储路径
|
INSTALL_DIR = Path("/opt/Trading_Studio")
|
||||||
SPEAKER_EMB_PATH = BASE_DIR / "speaker_emb.pt"
|
|
||||||
|
# 项目根目录(开发/生产均自适应,以实际 app.py 所在目录为准)
|
||||||
# 合成音频输出目录
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
OUTPUT_DIR = BASE_DIR / "outputs"
|
|
||||||
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
# 固定音色 Embedding 存储路径
|
||||||
|
SPEAKER_EMB_PATH = BASE_DIR / "speaker_emb.pt"
|
||||||
# ChatTTS 采样率(Hz)
|
|
||||||
TTS_SAMPLE_RATE = 24000
|
# 合成音频输出目录
|
||||||
|
OUTPUT_DIR = BASE_DIR / "outputs"
|
||||||
# 音色样本时长建议(秒)
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
SPEAKER_SAMPLE_MIN_SEC = 10
|
|
||||||
SPEAKER_SAMPLE_MAX_SEC = 30
|
# ChatTTS 采样率(Hz)
|
||||||
|
TTS_SAMPLE_RATE = 24000
|
||||||
# TTS 推理默认参数(低 temperature 有助于音色稳定)
|
|
||||||
TTS_TEMPERATURE = 0.3
|
# 音色样本时长建议(秒)
|
||||||
TTS_TOP_P = 0.7
|
SPEAKER_SAMPLE_MIN_SEC = 10
|
||||||
TTS_TOP_K = 20
|
SPEAKER_SAMPLE_MAX_SEC = 30
|
||||||
TTS_SPEED_PROMPT = "[speed_5]"
|
|
||||||
|
# TTS 推理默认参数(低 temperature 有助于音色稳定)
|
||||||
# ---------------------------------------------------------------------------
|
TTS_TEMPERATURE = 0.3
|
||||||
# 上传临时文件目录
|
TTS_TOP_P = 0.7
|
||||||
# ---------------------------------------------------------------------------
|
TTS_TOP_K = 20
|
||||||
UPLOAD_DIR = BASE_DIR / "uploads"
|
TTS_SPEED_PROMPT = "[speed_5]"
|
||||||
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
# ---------------------------------------------------------------------------
|
# 上传临时文件目录
|
||||||
# Git 仓库(文档引用)
|
# ---------------------------------------------------------------------------
|
||||||
# ---------------------------------------------------------------------------
|
UPLOAD_DIR = BASE_DIR / "uploads"
|
||||||
GIT_REPO_URL = "https://git.bz121.com/dekun/Trading_Studio.git"
|
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Git 仓库(文档引用)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
GIT_REPO_URL = "https://git.bz121.com/dekun/Trading_Studio.git"
|
||||||
|
|||||||
+198
-162
@@ -1,162 +1,198 @@
|
|||||||
"""
|
"""
|
||||||
远程 Ollama LLM 润色服务
|
远程 Ollama LLM 润色服务
|
||||||
通过局域网 HTTP 请求 Gemma4 模型,对交易复盘转写稿进行纪律审判式润色。
|
通过局域网 HTTP 请求 Gemma4 模型,对交易复盘转写稿进行纪律审判式润色。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Tuple
|
import time
|
||||||
|
from typing import Tuple
|
||||||
import requests
|
|
||||||
|
import requests
|
||||||
from config import MODEL_NAME, OLLAMA_TIMEOUT, OLLAMA_URL, SYSTEM_PROMPT
|
|
||||||
|
from config import (
|
||||||
logger = logging.getLogger(__name__)
|
HEALTH_CHECK_CACHE_SECONDS,
|
||||||
|
HEALTH_CHECK_CONNECT_TIMEOUT,
|
||||||
|
HEALTH_CHECK_READ_TIMEOUT,
|
||||||
def _build_payload(raw_text: str) -> dict:
|
MODEL_NAME,
|
||||||
"""构造 Ollama /api/chat 非流式请求体。"""
|
OLLAMA_TIMEOUT,
|
||||||
return {
|
OLLAMA_URL,
|
||||||
"model": MODEL_NAME,
|
SYSTEM_PROMPT,
|
||||||
"messages": [
|
)
|
||||||
{"role": "system", "content": SYSTEM_PROMPT},
|
|
||||||
{
|
logger = logging.getLogger(__name__)
|
||||||
"role": "user",
|
|
||||||
"content": (
|
# 健康检查短时缓存,避免平板/手机反复打开页面时重复等待
|
||||||
"以下是我的交易复盘录音转写原文,请严格按系统要求润色:\n\n"
|
_health_cache: dict = {"ts": 0.0, "ok": False, "msg": ""}
|
||||||
f"{raw_text}"
|
|
||||||
),
|
|
||||||
},
|
def _build_payload(raw_text: str) -> dict:
|
||||||
],
|
"""构造 Ollama /api/chat 非流式请求体。"""
|
||||||
"stream": False,
|
return {
|
||||||
"options": {
|
"model": MODEL_NAME,
|
||||||
"temperature": 0.7,
|
"messages": [
|
||||||
"num_predict": 4096,
|
{"role": "system", "content": SYSTEM_PROMPT},
|
||||||
},
|
{
|
||||||
}
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
"以下是我的交易复盘录音转写原文,请严格按系统要求润色:\n\n"
|
||||||
def _extract_content(response_json: dict) -> str:
|
f"{raw_text}"
|
||||||
"""从 Ollama 响应 JSON 中提取 assistant 文本。"""
|
),
|
||||||
# /api/chat 标准格式
|
},
|
||||||
message = response_json.get("message")
|
],
|
||||||
if isinstance(message, dict):
|
"stream": False,
|
||||||
content = message.get("content", "").strip()
|
"options": {
|
||||||
if content:
|
"temperature": 0.7,
|
||||||
return content
|
"num_predict": 4096,
|
||||||
|
},
|
||||||
# 兼容 /api/generate 格式(部分旧版或代理)
|
}
|
||||||
if "response" in response_json:
|
|
||||||
content = str(response_json["response"]).strip()
|
|
||||||
if content:
|
def _extract_content(response_json: dict) -> str:
|
||||||
return content
|
"""从 Ollama 响应 JSON 中提取 assistant 文本。"""
|
||||||
|
# /api/chat 标准格式
|
||||||
raise ValueError(f"无法从 Ollama 响应中解析文本内容: {response_json}")
|
message = response_json.get("message")
|
||||||
|
if isinstance(message, dict):
|
||||||
|
content = message.get("content", "").strip()
|
||||||
def polish_text(raw_text: str) -> Tuple[bool, str]:
|
if content:
|
||||||
"""
|
return content
|
||||||
调用远程 Ollama 对原始转写文本进行润色。
|
|
||||||
|
# 兼容 /api/generate 格式(部分旧版或代理)
|
||||||
Args:
|
if "response" in response_json:
|
||||||
raw_text: Whisper 转写得到的原始口语文本
|
content = str(response_json["response"]).strip()
|
||||||
|
if content:
|
||||||
Returns:
|
return content
|
||||||
(success, polished_text_or_error_message)
|
|
||||||
"""
|
raise ValueError(f"无法从 Ollama 响应中解析文本内容: {response_json}")
|
||||||
if not raw_text or not raw_text.strip():
|
|
||||||
return False, "润色输入为空,请先完成语音识别。"
|
|
||||||
|
def polish_text(raw_text: str) -> Tuple[bool, str]:
|
||||||
payload = _build_payload(raw_text.strip())
|
"""
|
||||||
|
调用远程 Ollama 对原始转写文本进行润色。
|
||||||
try:
|
|
||||||
logger.info("正在请求 Ollama: %s, model=%s", OLLAMA_URL, MODEL_NAME)
|
Args:
|
||||||
response = requests.post(
|
raw_text: Whisper 转写得到的原始口语文本
|
||||||
OLLAMA_URL,
|
|
||||||
json=payload,
|
Returns:
|
||||||
timeout=OLLAMA_TIMEOUT,
|
(success, polished_text_or_error_message)
|
||||||
)
|
"""
|
||||||
response.raise_for_status()
|
if not raw_text or not raw_text.strip():
|
||||||
|
return False, "润色输入为空,请先完成语音识别。"
|
||||||
data = response.json()
|
|
||||||
polished = _extract_content(data)
|
payload = _build_payload(raw_text.strip())
|
||||||
|
|
||||||
if not polished:
|
try:
|
||||||
return False, "Ollama 返回内容为空,请检查模型是否正常加载。"
|
logger.info("正在请求 Ollama: %s, model=%s", OLLAMA_URL, MODEL_NAME)
|
||||||
|
response = requests.post(
|
||||||
logger.info("润色完成,输出字数: %d", len(polished))
|
OLLAMA_URL,
|
||||||
return True, polished
|
json=payload,
|
||||||
|
timeout=OLLAMA_TIMEOUT,
|
||||||
except requests.exceptions.ConnectTimeout:
|
)
|
||||||
err = (
|
response.raise_for_status()
|
||||||
f"连接 Ollama 超时(>{OLLAMA_TIMEOUT}s)。"
|
|
||||||
f"请确认 {OLLAMA_URL} 可达且 Ollama 服务已启动。"
|
data = response.json()
|
||||||
)
|
polished = _extract_content(data)
|
||||||
logger.error(err)
|
|
||||||
return False, err
|
if not polished:
|
||||||
|
return False, "Ollama 返回内容为空,请检查模型是否正常加载。"
|
||||||
except requests.exceptions.ReadTimeout:
|
|
||||||
err = (
|
logger.info("润色完成,输出字数: %d", len(polished))
|
||||||
f"Ollama 响应超时(>{OLLAMA_TIMEOUT}s)。"
|
return True, polished
|
||||||
"模型可能正在加载或生成长度过长,请稍后重试。"
|
|
||||||
)
|
except requests.exceptions.ConnectTimeout:
|
||||||
logger.error(err)
|
err = (
|
||||||
return False, err
|
f"连接 Ollama 超时(>{OLLAMA_TIMEOUT}s)。"
|
||||||
|
f"请确认 {OLLAMA_URL} 可达且 Ollama 服务已启动。"
|
||||||
except requests.exceptions.ConnectionError as exc:
|
)
|
||||||
err = (
|
logger.error(err)
|
||||||
f"无法连接到 Ollama 节点 ({OLLAMA_URL})。"
|
return False, err
|
||||||
"请检查局域网连通性、防火墙及 Ollama 是否监听 0.0.0.0:11434。\n"
|
|
||||||
f"详情: {exc}"
|
except requests.exceptions.ReadTimeout:
|
||||||
)
|
err = (
|
||||||
logger.error(err)
|
f"Ollama 响应超时(>{OLLAMA_TIMEOUT}s)。"
|
||||||
return False, err
|
"模型可能正在加载或生成长度过长,请稍后重试。"
|
||||||
|
)
|
||||||
except requests.exceptions.HTTPError as exc:
|
logger.error(err)
|
||||||
status = exc.response.status_code if exc.response is not None else "?"
|
return False, err
|
||||||
body = exc.response.text[:500] if exc.response is not None else ""
|
|
||||||
err = (
|
except requests.exceptions.ConnectionError as exc:
|
||||||
f"Ollama HTTP 错误 ({status})。"
|
err = (
|
||||||
f"请确认模型 `{MODEL_NAME}` 已通过 ollama pull 下载。\n"
|
f"无法连接到 Ollama 节点 ({OLLAMA_URL})。"
|
||||||
f"响应片段: {body}"
|
"请检查局域网连通性、防火墙及 Ollama 是否监听 0.0.0.0:11434。\n"
|
||||||
)
|
f"详情: {exc}"
|
||||||
logger.error(err)
|
)
|
||||||
return False, err
|
logger.error(err)
|
||||||
|
return False, err
|
||||||
except ValueError as exc:
|
|
||||||
logger.error("Ollama 响应解析失败: %s", exc)
|
except requests.exceptions.HTTPError as exc:
|
||||||
return False, str(exc)
|
status = exc.response.status_code if exc.response is not None else "?"
|
||||||
|
body = exc.response.text[:500] if exc.response is not None else ""
|
||||||
except requests.exceptions.RequestException as exc:
|
err = (
|
||||||
err = f"Ollama 请求异常: {exc}"
|
f"Ollama HTTP 错误 ({status})。"
|
||||||
logger.exception(err)
|
f"请确认模型 `{MODEL_NAME}` 已通过 ollama pull 下载。\n"
|
||||||
return False, err
|
f"响应片段: {body}"
|
||||||
|
)
|
||||||
except Exception as exc:
|
logger.error(err)
|
||||||
err = f"润色过程发生未知错误: {exc}"
|
return False, err
|
||||||
logger.exception(err)
|
|
||||||
return False, err
|
except ValueError as exc:
|
||||||
|
logger.error("Ollama 响应解析失败: %s", exc)
|
||||||
|
return False, str(exc)
|
||||||
def check_ollama_health() -> Tuple[bool, str]:
|
|
||||||
"""
|
except requests.exceptions.RequestException as exc:
|
||||||
快速检测 Ollama 节点是否在线(不触发完整推理)。
|
err = f"Ollama 请求异常: {exc}"
|
||||||
|
logger.exception(err)
|
||||||
Returns:
|
return False, err
|
||||||
(online, message)
|
|
||||||
"""
|
except Exception as exc:
|
||||||
base_url = OLLAMA_URL.rsplit("/api/", 1)[0]
|
err = f"润色过程发生未知错误: {exc}"
|
||||||
try:
|
logger.exception(err)
|
||||||
resp = requests.get(f"{base_url}/api/tags", timeout=10)
|
return False, err
|
||||||
resp.raise_for_status()
|
|
||||||
tags = resp.json().get("models", [])
|
|
||||||
model_names = [m.get("name", "") for m in tags]
|
def check_ollama_health(force: bool = False) -> Tuple[bool, str]:
|
||||||
if any(MODEL_NAME.split(":")[0] in name for name in model_names):
|
"""
|
||||||
return True, f"Ollama 在线,已检测到模型: {MODEL_NAME}"
|
快速检测 Ollama 节点是否在线(不触发完整推理)。
|
||||||
return True, (
|
默认 2+3 秒超时,结果缓存 30 秒,避免平板首屏长时间白屏。
|
||||||
f"Ollama 在线,但未找到模型 {MODEL_NAME},"
|
|
||||||
f"请执行: ollama pull {MODEL_NAME}"
|
Returns:
|
||||||
)
|
(online, message)
|
||||||
except Exception as exc:
|
"""
|
||||||
return False, f"Ollama 不可达: {exc}"
|
global _health_cache
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
if (
|
||||||
|
not force
|
||||||
|
and _health_cache["msg"]
|
||||||
|
and (now - _health_cache["ts"]) < HEALTH_CHECK_CACHE_SECONDS
|
||||||
|
):
|
||||||
|
return _health_cache["ok"], _health_cache["msg"]
|
||||||
|
|
||||||
|
base_url = OLLAMA_URL.rsplit("/api/", 1)[0]
|
||||||
|
timeout = (HEALTH_CHECK_CONNECT_TIMEOUT, HEALTH_CHECK_READ_TIMEOUT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.get(f"{base_url}/api/tags", timeout=timeout)
|
||||||
|
resp.raise_for_status()
|
||||||
|
tags = resp.json().get("models", [])
|
||||||
|
model_names = [m.get("name", "") for m in tags]
|
||||||
|
if any(MODEL_NAME.split(":")[0] in name for name in model_names):
|
||||||
|
msg = f"Ollama 在线,已检测到模型: {MODEL_NAME}"
|
||||||
|
ok = True
|
||||||
|
else:
|
||||||
|
ok = True
|
||||||
|
msg = (
|
||||||
|
f"Ollama 在线,但未找到模型 {MODEL_NAME},"
|
||||||
|
f"请执行: ollama pull {MODEL_NAME}"
|
||||||
|
)
|
||||||
|
except requests.exceptions.Timeout:
|
||||||
|
ok, msg = False, (
|
||||||
|
f"Ollama 检测超时(>{HEALTH_CHECK_READ_TIMEOUT}s)。"
|
||||||
|
"页面已加载,可稍后点击「刷新状态」重试。"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
ok, msg = False, f"Ollama 不可达: {exc}"
|
||||||
|
|
||||||
|
_health_cache.update({"ts": now, "ok": ok, "msg": msg})
|
||||||
|
return ok, msg
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Trading Studio PWA Service Worker
|
* Trading Studio PWA Service Worker(轻量版)
|
||||||
* 缓存应用壳,支持离线打开已访问页面;API 请求始终走网络。
|
* 仅处理导航请求,不拦截 Gradio 静态资源 — 避免平板端加载变慢。
|
||||||
*/
|
*/
|
||||||
const CACHE_NAME = "trading-studio-v1";
|
const CACHE_NAME = "trading-studio-v2";
|
||||||
const SHELL = ["/", "/manifest.webmanifest"];
|
|
||||||
|
|
||||||
self.addEventListener("install", (event) => {
|
self.addEventListener("install", (event) => {
|
||||||
event.waitUntil(
|
|
||||||
caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL)).catch(() => {})
|
|
||||||
);
|
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,28 +19,13 @@ self.addEventListener("activate", (event) => {
|
|||||||
|
|
||||||
self.addEventListener("fetch", (event) => {
|
self.addEventListener("fetch", (event) => {
|
||||||
const { request } = event;
|
const { request } = event;
|
||||||
const url = new URL(request.url);
|
|
||||||
|
|
||||||
// API / 文件上传 / Gradio 动态接口不走缓存
|
// 仅缓存页面导航,Gradio JS/CSS/API 全部直连网络
|
||||||
if (
|
if (request.method !== "GET" || request.mode !== "navigate") {
|
||||||
request.method !== "GET" ||
|
|
||||||
url.pathname.startsWith("/gradio_api") ||
|
|
||||||
url.pathname.startsWith("/file=") ||
|
|
||||||
url.pathname.startsWith("/upload") ||
|
|
||||||
url.pathname.includes("call")
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
fetch(request)
|
fetch(request).catch(() => caches.match("/"))
|
||||||
.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("/")))
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user