当Ollama遇上划词翻译:我的Windows本地AI服务搭建日记
🚀 实现Windows本地大模型翻译服务 - 基于Ollama+Flask的划词翻译实践
- 🛠️ 步骤概要
- 1️⃣ python 环境准备
- 2️⃣ Ollama 安装
- 3️⃣ 一个 Flask 服务
- 4️⃣ Windows 服务化封装
- 5️⃣ 测试本地接口
- 6️⃣ 配置划词翻译自定义翻译源
- 7️⃣ 效果展示
- 8️⃣ debug 历程
- 💡 技术亮点
🛠️ 步骤概要
参考 API 文档:
- Ollama API
- 划词翻译自定义翻译源
1️⃣ python 环境准备
# 虚拟环境
conda create -n ollama_trans
conda activate ollama_trans
pip install flask flask-cors pywin32 requests waitress
# 请确保完成 pywin32_postinstall.py 的安装步骤。:
python path\to\your\envs\ollama_trans\Scripts\pywin32_postinstall.py -install
2️⃣ Ollama 安装
安装 Ollama 最好提前设置安装路径和模型下载路径,否则它都一股脑干到 C 盘。可以参考前一篇博客 《本地投喂deepseek》:
- 设置模型保存路径:新增环境变量
OLLAMA_MODELS
,值为目标地址,似乎要 重启电脑生效 - 指定安装目录:OllamaSetup.exe /DIR=“D:\some\location”
- ollama默认在 11434 端口提供 REST API,比如通过 curl 发送请求到
/api/generate
来生成文本:
curl http://localhost:11434/api/generate -d "{\"model\": \"deepseek-r1:14b\", \"prompt\": \"Why is the sky blue?\", \"stream\": false}"
- 我们的目的就是用 flask 写一个服务器适配 Ollama 和划词翻译的 API
3️⃣ 一个 Flask 服务
遇事不决问 DS
# translation_service.py
import re
import win32serviceutil
import win32service
import win32event
import servicemanager
import socket
from flask import Flask, request, jsonify
from flask_cors import CORS
from waitress import serve
import requests
import logging
import sys
import os
app = Flask(__name__)
CORS(app)
# 获取当前脚本所在的目录
current_directory = os.path.dirname(os.path.abspath(__file__))
# 定义日志文件名
log_file_path = os.path.join(current_directory, 'flask_svc.log')
# 配置日志记录器
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S',
filename=log_file_path,
filemode='a' # 使用 'a' 表示追加模式,'w' 表示覆盖模式
)
logger = logging.getLogger(__name__)
# 选择ollama的模型
MODEL_NAME = {
"qwen": "qwen2.5:7b-instruct-q8_0",
"llama": "llama3.2:3b",
"deepseek": "deepseek-r1:14b" # 其实 deepseek-r1 擅长推理,并不适合翻译,有点慢
}
# 语言映射
LANGUAGE_MAP = {
"中文(简体)": "中文",
"英语": "英文",
"日语": "日文"
}
def clean_response(text):
"""清除<think>标签内容"""
# 过滤DeepSeek思考过程的正则表达式
THINK_PATTERN = re.compile(r'<think>.*?</think>', re.DOTALL)
return THINK_PATTERN.sub('', text).strip()
def build_prompt(text, source, target):
source_lang = LANGUAGE_MAP.get(source, source)
target_lang = LANGUAGE_MAP.get(target, target)
return f"作为专业翻译官,请将以下{source_lang}内容精准翻译为{target_lang},仅输出译文:\n{text}"
@app.route('/translate', methods=['POST'])
def translate():
try:
data = request.json
logger.info(f"收到请求: {data}")
# 提取必要参数
model_name = data['name'].lower()
text = data["text"]
dest_langs = data["destination"]
source_lang = data.get("source") or "auto"
if source_lang == dest_langs[0] and len(dest_langs) > 1:
target_lang = dest_langs[1]
else:
target_lang = dest_langs[0]
response = requests.post(
"http://localhost:11434/api/generate",
json={
"model": MODEL_NAME.get(model_name, model_name),
"prompt": build_prompt(text, source_lang, target_lang),
"stream": False,
"options": {"temperature": 0.3}
}
)
# 处理翻译结果
raw_response = response.json()["response"]
translated_text = clean_response(raw_response)
return jsonify({
"text": text,
"from": source_lang,
"to": target_lang,
"result": [translated_text]
})
except Exception as e:
logger.error(f"翻译失败: {str(e)}")
return jsonify({"error": str(e)}), 500
class TranslationService(win32serviceutil.ServiceFramework):
_svc_name_ = "LocalOllamaTranslationService" # 服务名称(唯一)
_svc_display_name_ = "Ollama本地翻译服务"
_svc_description_ = "为划词翻译提供基于Ollama中运行的大模型的本地翻译服务" # 服务描述
logger.info(f"svc_name: {_svc_name_}, model_name: {MODEL_NAME}")
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
socket.setdefaulttimeout(60)
def SvcStop(self):
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
logger.info("Stopping Ollama Translation Service...")
win32event.SetEvent(self.hWaitStop)
servicemanager.LogInfoMsg("服务正在停止...")
logger.info("Service stopped successfully.")
def SvcDoRun(self):
logger.info("Starting Ollama Translation Service...")
try:
self.main()
logger.info("Service started successfully.")
except Exception as e:
logger.error(f"Failed to start service: {str(e)}")
self.ReportServiceStatus(win32service.SERVICE_STOPPED)
def main(self):
serve(app, host='127.0.0.1', port=5000)
if __name__ == '__main__':
if len(sys.argv) == 1:
servicemanager.Initialize()
servicemanager.PrepareToHostSingle(TranslationService)
servicemanager.StartServiceCtrlDispatcher()
else:
win32serviceutil.HandleCommandLine(TranslationService)
4️⃣ Windows 服务化封装
以下脚本都需要以管理员身份运行
- 启动服务脚本
install_service.bat
:: install_service.bat
@echo off
# 这里的 python 解释器要填虚拟环境那个,否则找不到包
set PYTHON_PATH=path\to\your\envs\ollama_trans\python.exe
set SCRIPT_PATH=%~dp0translation_service.py
%PYTHON_PATH% %SCRIPT_PATH% --startup=auto install
# 这里的服务名称 LocalOllamaTranslationService 要跟上面程序里面的一致,是唯一的
net start LocalOllamaTranslationService
- 关闭服务脚本
uninstall_service.bat
:: uninstall_service.bat
@echo off
net stop LocalOllamaTranslationService
set PYTHON_PATH=path\to\your\envs\ollama_trans\python.exe
set SCRIPT_PATH=%~dp0translation_service.py
%PYTHON_PATH% %SCRIPT_PATH% remove
- 最终的文件结构:
├── translation_service.py # 主服务程序
├── install_service.bat # 服务安装脚本
├── uninstall_service.bat # 服务卸载脚本
└── flask_svc.log # python 日志
5️⃣ 测试本地接口
现在我们请求的是 flask 服务的端口,我这里指定了模型名称是 qwen
,ollama 中也下载了 qwen2.5:7b-instruct-q8_0
curl -X POST http://localhost:5000/translate -H "Content-Type: application/json" -d "{\"name\": \"qwen\",\"text\": \"人工智能的发展前景\", \"destination\": [\"中文(简体)\", \"英语\"], \"source\": \"中文(简体)\"}"
返回:
{"from":"\u4e2d\u6587(\u7b80\u4f53)","result":["The Prospects for the Development of Artificial Intelligence"],"text":"\u4eba\u5de5\u667a\u80fd\u7684\u53d1\u5c55\u524d\u666f","to":"\u82f1\u8bed"}
6️⃣ 配置划词翻译自定义翻译源
插件设置
自定义翻译源
-
接口地址:http://localhost:5000/translate
-
翻译源名称:qwen,然后回车
-
测试
根据上面的程序逻辑,翻译源名称最好跟 ollama 下载的模型名称一致
7️⃣ 效果展示
- 划词翻译
- windows 服务
8️⃣ debug 历程
- 日志的重要性
- 一开始 deepseek 生成的 py 程序都没写日志,启动服务一直失败,报错:
pywintypes.error: (1063, 'StartServiceCtrlDispatcher', '服务进程无法连接到服务控制器上。')
,再拿这些报错去问它,给了一堆方案都没解决问题 😄 - 写了 logger 才发现问题所在:
- 一开始 deepseek 生成的 py 程序都没写日志,启动服务一直失败,报错:
Sat, 15 Feb 2025 21:19:16 wasyncore.py[line:449] INFO Serving on http://127.0.0.1:5001
Sat, 15 Feb 2025 21:22:47 app.py[line:875] ERROR Exception on /translate [POST]
Traceback (most recent call last):
File "D:\dev\miniconda\miniconda3\envs\flask\Lib\site-packages\flask\app.py", line 1511, in wsgi_app
response = self.full_dispatch_request()
File "D:\dev\miniconda\miniconda3\envs\flask\Lib\site-packages\flask\app.py", line 919, in full_dispatch_request
rv = self.handle_user_exception(e)
File "D:\dev\miniconda\miniconda3\envs\flask\Lib\site-packages\flask_cors\extension.py", line 165, in wrapped_function
return cors_after_request(app.make_response(f(*args, **kwargs)))
~^^^^^^^^^^^^^^^^^
File "D:\dev\miniconda\miniconda3\envs\flask\Lib\site-packages\flask\app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
File "D:\dev\miniconda\miniconda3\envs\flask\Lib\site-packages\flask\app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
TypeError: TranslationService.translate() missing 1 required positional argument: 'self'
- 服务启停
- cmd 中的双引号是需要用反斜杠
\
转义的,在 powershell 中不需要 - debug 过程中需要起了又删掉
LocalOllamaTranslationService
服务: sc queryex LocalOllamaTranslationService
得到 PIDtaskkill /f /pid <PID>
强行停止sc delete LocalOllamaTranslationService
删掉这个服务 ID 后才能再启动程序,否则会报错服务已存在- 或者以管理员身份打开 cmd 或者 VS Code,执行 python:
python translation_service.py stop
停止服务python translation_service.py remove
删除服务python translation_service.py install
安装服务python translation_service.py start
启动服务
- cmd 中的双引号是需要用反斜杠
💡 技术亮点
-
完全离线: 从模型推理到翻译服务全程本地运行
-
隐私保护: 敏感文本无需离开本地设备
-
低延迟: 省去网络传输耗时,平均响应<500ms
-
可扩展架构: 轻松切换不同大语言模型