Optimize tablet load: defer health check, lighten service worker, drop Google Fonts.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+198
-162
@@ -1,162 +1,198 @@
|
||||
"""
|
||||
远程 Ollama LLM 润色服务
|
||||
通过局域网 HTTP 请求 Gemma4 模型,对交易复盘转写稿进行纪律审判式润色。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Tuple
|
||||
|
||||
import requests
|
||||
|
||||
from config import MODEL_NAME, OLLAMA_TIMEOUT, OLLAMA_URL, SYSTEM_PROMPT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _build_payload(raw_text: str) -> dict:
|
||||
"""构造 Ollama /api/chat 非流式请求体。"""
|
||||
return {
|
||||
"model": MODEL_NAME,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"以下是我的交易复盘录音转写原文,请严格按系统要求润色:\n\n"
|
||||
f"{raw_text}"
|
||||
),
|
||||
},
|
||||
],
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": 0.7,
|
||||
"num_predict": 4096,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _extract_content(response_json: dict) -> str:
|
||||
"""从 Ollama 响应 JSON 中提取 assistant 文本。"""
|
||||
# /api/chat 标准格式
|
||||
message = response_json.get("message")
|
||||
if isinstance(message, dict):
|
||||
content = message.get("content", "").strip()
|
||||
if content:
|
||||
return content
|
||||
|
||||
# 兼容 /api/generate 格式(部分旧版或代理)
|
||||
if "response" in response_json:
|
||||
content = str(response_json["response"]).strip()
|
||||
if content:
|
||||
return content
|
||||
|
||||
raise ValueError(f"无法从 Ollama 响应中解析文本内容: {response_json}")
|
||||
|
||||
|
||||
def polish_text(raw_text: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
调用远程 Ollama 对原始转写文本进行润色。
|
||||
|
||||
Args:
|
||||
raw_text: Whisper 转写得到的原始口语文本
|
||||
|
||||
Returns:
|
||||
(success, polished_text_or_error_message)
|
||||
"""
|
||||
if not raw_text or not raw_text.strip():
|
||||
return False, "润色输入为空,请先完成语音识别。"
|
||||
|
||||
payload = _build_payload(raw_text.strip())
|
||||
|
||||
try:
|
||||
logger.info("正在请求 Ollama: %s, model=%s", OLLAMA_URL, MODEL_NAME)
|
||||
response = requests.post(
|
||||
OLLAMA_URL,
|
||||
json=payload,
|
||||
timeout=OLLAMA_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
polished = _extract_content(data)
|
||||
|
||||
if not polished:
|
||||
return False, "Ollama 返回内容为空,请检查模型是否正常加载。"
|
||||
|
||||
logger.info("润色完成,输出字数: %d", len(polished))
|
||||
return True, polished
|
||||
|
||||
except requests.exceptions.ConnectTimeout:
|
||||
err = (
|
||||
f"连接 Ollama 超时(>{OLLAMA_TIMEOUT}s)。"
|
||||
f"请确认 {OLLAMA_URL} 可达且 Ollama 服务已启动。"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except requests.exceptions.ReadTimeout:
|
||||
err = (
|
||||
f"Ollama 响应超时(>{OLLAMA_TIMEOUT}s)。"
|
||||
"模型可能正在加载或生成长度过长,请稍后重试。"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
err = (
|
||||
f"无法连接到 Ollama 节点 ({OLLAMA_URL})。"
|
||||
"请检查局域网连通性、防火墙及 Ollama 是否监听 0.0.0.0:11434。\n"
|
||||
f"详情: {exc}"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except requests.exceptions.HTTPError as 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 ""
|
||||
err = (
|
||||
f"Ollama HTTP 错误 ({status})。"
|
||||
f"请确认模型 `{MODEL_NAME}` 已通过 ollama pull 下载。\n"
|
||||
f"响应片段: {body}"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except ValueError as exc:
|
||||
logger.error("Ollama 响应解析失败: %s", exc)
|
||||
return False, str(exc)
|
||||
|
||||
except requests.exceptions.RequestException as exc:
|
||||
err = f"Ollama 请求异常: {exc}"
|
||||
logger.exception(err)
|
||||
return False, err
|
||||
|
||||
except Exception as exc:
|
||||
err = f"润色过程发生未知错误: {exc}"
|
||||
logger.exception(err)
|
||||
return False, err
|
||||
|
||||
|
||||
def check_ollama_health() -> Tuple[bool, str]:
|
||||
"""
|
||||
快速检测 Ollama 节点是否在线(不触发完整推理)。
|
||||
|
||||
Returns:
|
||||
(online, message)
|
||||
"""
|
||||
base_url = OLLAMA_URL.rsplit("/api/", 1)[0]
|
||||
try:
|
||||
resp = requests.get(f"{base_url}/api/tags", timeout=10)
|
||||
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):
|
||||
return True, f"Ollama 在线,已检测到模型: {MODEL_NAME}"
|
||||
return True, (
|
||||
f"Ollama 在线,但未找到模型 {MODEL_NAME},"
|
||||
f"请执行: ollama pull {MODEL_NAME}"
|
||||
)
|
||||
except Exception as exc:
|
||||
return False, f"Ollama 不可达: {exc}"
|
||||
"""
|
||||
远程 Ollama LLM 润色服务
|
||||
通过局域网 HTTP 请求 Gemma4 模型,对交易复盘转写稿进行纪律审判式润色。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Tuple
|
||||
|
||||
import requests
|
||||
|
||||
from config import (
|
||||
HEALTH_CHECK_CACHE_SECONDS,
|
||||
HEALTH_CHECK_CONNECT_TIMEOUT,
|
||||
HEALTH_CHECK_READ_TIMEOUT,
|
||||
MODEL_NAME,
|
||||
OLLAMA_TIMEOUT,
|
||||
OLLAMA_URL,
|
||||
SYSTEM_PROMPT,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 健康检查短时缓存,避免平板/手机反复打开页面时重复等待
|
||||
_health_cache: dict = {"ts": 0.0, "ok": False, "msg": ""}
|
||||
|
||||
|
||||
def _build_payload(raw_text: str) -> dict:
|
||||
"""构造 Ollama /api/chat 非流式请求体。"""
|
||||
return {
|
||||
"model": MODEL_NAME,
|
||||
"messages": [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
{
|
||||
"role": "user",
|
||||
"content": (
|
||||
"以下是我的交易复盘录音转写原文,请严格按系统要求润色:\n\n"
|
||||
f"{raw_text}"
|
||||
),
|
||||
},
|
||||
],
|
||||
"stream": False,
|
||||
"options": {
|
||||
"temperature": 0.7,
|
||||
"num_predict": 4096,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _extract_content(response_json: dict) -> str:
|
||||
"""从 Ollama 响应 JSON 中提取 assistant 文本。"""
|
||||
# /api/chat 标准格式
|
||||
message = response_json.get("message")
|
||||
if isinstance(message, dict):
|
||||
content = message.get("content", "").strip()
|
||||
if content:
|
||||
return content
|
||||
|
||||
# 兼容 /api/generate 格式(部分旧版或代理)
|
||||
if "response" in response_json:
|
||||
content = str(response_json["response"]).strip()
|
||||
if content:
|
||||
return content
|
||||
|
||||
raise ValueError(f"无法从 Ollama 响应中解析文本内容: {response_json}")
|
||||
|
||||
|
||||
def polish_text(raw_text: str) -> Tuple[bool, str]:
|
||||
"""
|
||||
调用远程 Ollama 对原始转写文本进行润色。
|
||||
|
||||
Args:
|
||||
raw_text: Whisper 转写得到的原始口语文本
|
||||
|
||||
Returns:
|
||||
(success, polished_text_or_error_message)
|
||||
"""
|
||||
if not raw_text or not raw_text.strip():
|
||||
return False, "润色输入为空,请先完成语音识别。"
|
||||
|
||||
payload = _build_payload(raw_text.strip())
|
||||
|
||||
try:
|
||||
logger.info("正在请求 Ollama: %s, model=%s", OLLAMA_URL, MODEL_NAME)
|
||||
response = requests.post(
|
||||
OLLAMA_URL,
|
||||
json=payload,
|
||||
timeout=OLLAMA_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
polished = _extract_content(data)
|
||||
|
||||
if not polished:
|
||||
return False, "Ollama 返回内容为空,请检查模型是否正常加载。"
|
||||
|
||||
logger.info("润色完成,输出字数: %d", len(polished))
|
||||
return True, polished
|
||||
|
||||
except requests.exceptions.ConnectTimeout:
|
||||
err = (
|
||||
f"连接 Ollama 超时(>{OLLAMA_TIMEOUT}s)。"
|
||||
f"请确认 {OLLAMA_URL} 可达且 Ollama 服务已启动。"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except requests.exceptions.ReadTimeout:
|
||||
err = (
|
||||
f"Ollama 响应超时(>{OLLAMA_TIMEOUT}s)。"
|
||||
"模型可能正在加载或生成长度过长,请稍后重试。"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
err = (
|
||||
f"无法连接到 Ollama 节点 ({OLLAMA_URL})。"
|
||||
"请检查局域网连通性、防火墙及 Ollama 是否监听 0.0.0.0:11434。\n"
|
||||
f"详情: {exc}"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except requests.exceptions.HTTPError as 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 ""
|
||||
err = (
|
||||
f"Ollama HTTP 错误 ({status})。"
|
||||
f"请确认模型 `{MODEL_NAME}` 已通过 ollama pull 下载。\n"
|
||||
f"响应片段: {body}"
|
||||
)
|
||||
logger.error(err)
|
||||
return False, err
|
||||
|
||||
except ValueError as exc:
|
||||
logger.error("Ollama 响应解析失败: %s", exc)
|
||||
return False, str(exc)
|
||||
|
||||
except requests.exceptions.RequestException as exc:
|
||||
err = f"Ollama 请求异常: {exc}"
|
||||
logger.exception(err)
|
||||
return False, err
|
||||
|
||||
except Exception as exc:
|
||||
err = f"润色过程发生未知错误: {exc}"
|
||||
logger.exception(err)
|
||||
return False, err
|
||||
|
||||
|
||||
def check_ollama_health(force: bool = False) -> Tuple[bool, str]:
|
||||
"""
|
||||
快速检测 Ollama 节点是否在线(不触发完整推理)。
|
||||
默认 2+3 秒超时,结果缓存 30 秒,避免平板首屏长时间白屏。
|
||||
|
||||
Returns:
|
||||
(online, message)
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user