IBM Bob+Ollama本地图像生成:零依赖Web UI实战
1. 项目概述:这不是一个“IBM官方产品”,而是一次精准的工程缝合
“IBM Bob + Ollama图像生成”这个标题,第一眼容易让人误以为是IBM推出的某款新AI工具——毕竟“IBM”打头,自带权威感;“Bob”听起来像个人工智能助手;“Ollama”又是当下最火的本地大模型运行时。但事实恰恰相反: 它没有一行代码出自IBM官方仓库,也不是Ollama团队的官方用例,更不是某个预装好的商业软件。它是一次由独立开发者完成的、教科书级的“技术拼接”实践——把IBM开源的前端低代码框架Project Bob,和Ollama生态中刚冒头的两个轻量级图像生成模型(x/flux2-klein 和 x/z-image-turbo),用极简的Node.js胶水层粘合成一个可立即上手的本地Web应用。
我第一次看到这个标题是在技术社区里刷到的Demo截图:一个干净得近乎克制的网页界面,左侧是文本输入框,中间是实时渲染的生成图预览区,右下角一个“Download PNG”按钮,顶部下拉菜单里只有两个选项—— x/flux2-klein:4b 和 x/z-image-turbo:fp8 。没有登录、没有账户、没有云同步、没有API Key输入框。点开开发者工具一看,所有请求都发向 http://localhost:11434/api/generate ——这正是Ollama默认HTTP服务的地址。那一刻我就确认了:这是一个100%离线、零外部依赖、连前端资源都直接 express.static('public') 托管的纯本地方案。
为什么这个组合值得深挖?因为它的价值不在“新”,而在“准”。它精准踩中了2024–2025年本地AI落地的三个核心痛点:
- 模型可用性差 :Ollama虽好,但原生命令行交互对非开发者极不友好,
ollama run x/z-image-turbo:fp8 "a cat"这种操作,连提示词微调、历史回溯、结果保存都得手动处理; - 前端开发门槛高 :想做个UI?你得搭React/Vue、配Webpack、搞CORS、处理base64流式响应……对只想“试试模型效果”的设计师、产品经理、教师来说,成本太高;
- 隐私与可控性焦虑 :所有在线图像生成服务(DALL·E、MidJourney、国内各类平台)都要求上传提示词,甚至可能缓存你的生成图。而这里,从输入到输出,全程不离开你的笔记本硬盘。
所以,“IBM Bob + Ollama图像生成”的本质,是一个 面向真实工作流的减法工程 :它删掉了所有云服务组件、删掉了用户系统、删掉了复杂状态管理,只保留最核心的三件事——接收提示词、调用本地Ollama API、展示并保存PNG。这种“少即是多”的设计哲学,恰恰是当前AI工具链中最稀缺的清醒。
关键词里的“IBM”指的不是硬件或企业服务,而是IBM Research开源的 Project Bob ——一个基于Web Components的轻量级前端编排框架,目标是让开发者用声明式HTML标签(比如 <bob-form> 、 <bob-api-call> )快速组装AI应用界面,无需写JS逻辑。而“Ollama”在这里也不是泛指整个生态,而是特指其 v0.3+版本后正式支持的图像生成API协议 ——即通过 POST /api/generate 提交 model + prompt ,接收NDJSON(换行分隔JSON)格式的流式响应,最终在最后一行拿到 "image": "base64..." 字段。这两个技术点的交汇,构成了整个项目的地基。
如果你正面临这些场景,这个项目就是为你准备的:
- 你有一台M2 MacBook或RTX 4090台式机,想把闲置算力变成自己的“本地Stable Diffusion”;
- 你在教学生AI创作,需要一个不联网、不注册、打开浏览器就能用的课堂演示工具;
- 你是企业内审人员,被要求评估所有AI工具的数据流向,而这个方案的网络拓扑图只有一条线:浏览器 ↔ 本机Node.js服务器 ↔ 本机Ollama进程;
- 你厌倦了每次更新模型都要重写前端适配逻辑,想要一个能自动发现新图像模型的通用UI框架。
它不承诺“媲美DALL·E 3”,但保证“比命令行好用十倍”;它不解决“如何生成完美logo”,但解决了“如何让设计师同事在5分钟内开始试错”。这就是它存在的全部意义。
2. 核心技术解构:为什么是Bob?为什么是这两个模型?
2.1 IBM Project Bob:被严重低估的“前端胶水层”
很多人看到“IBM Bob”第一反应是:“IBM还有个叫Bob的AI?” 实际上,Project Bob(https://github.com/ibm/bob)是IBM Research在2023年低调开源的一个实验性前端框架,它的定位非常清晰: 不做React,不做Vue,不做任何UI组件库,只做一件事——把AI能力封装成可复用的HTML自定义元素。 它的核心思想是:既然AI模型已经通过HTTP API暴露能力(如Ollama的 /api/generate ),那前端就不该再写一堆fetch调用和状态管理,而应该像使用 <input> 一样,用 <bob-api-call endpoint="http://localhost:11434/api/generate"> 直接声明式调用。
在原始资料中提到的“Bob帮助我构建UI”,其实是指开发者利用Bob的 <bob-form> 和 <bob-api-call> 组合,快速生成了表单绑定、按钮触发、响应解析的逻辑。例如,一个典型的Bob UI片段可能是这样的:
<bob-form id="genForm">
<label>Prompt:</label>
<textarea name="prompt" required></textarea>
<label>Model:</label>
<select name="model">
<option value="x/flux2-klein:4b">Flux2-Klein (Quality)</option>
<option value="x/z-image-turbo:fp8">Z-Image-Turbo (Speed)</option>
</select>
<button type="submit" bob-api-call="generateApi">Generate</button>
</bob-form>
<bob-api-call
id="generateApi"
endpoint="http://localhost:11434/api/generate"
method="POST"
body='{"model": "{{model}}", "prompt": "{{prompt}}", "stream": false}'
on-success="handleImageResponse"
></bob-api-call>
这段代码里没有一行JavaScript,却完成了:表单数据收集、API请求构造、响应处理(通过 on-success 绑定的 handleImageResponse 函数)。Bob的魔力在于它把“状态驱动UI”变成了“数据驱动UI”——你只需关注 {{model}} 和 {{prompt}} 这两个变量如何从表单流入API Body,其余的加载态、错误提示、响应解析,都由Bob内置的生命周期方法处理。
那么,为什么不用更主流的React或Svelte?答案藏在项目目标里: 追求极致的部署简单性。 React项目需要 npm run build 生成静态文件,再用Express托管;而Bob应用可以直接用 npx http-server public/ 启动,甚至把 index.html 拖进浏览器就能跑(当然,跨域会报错,所以实际仍需后端代理)。对于一个只服务于本地Ollama的工具,引入Webpack打包链是典型的“杀鸡用牛刀”。Bob的零构建、零配置、纯HTML特性,让它成为此类“一次性胶水应用”的理想选择。
提示:Bob目前仍处于实验阶段,官方文档稀疏,社区生态薄弱。但它有一个关键优势——源码极简(主仓库仅300行TS)。当你遇到
<bob-api-call>不触发的问题时,直接打开node_modules/@ibm/bob/dist/bob-api-call.js加console.log,比查React文档快十倍。这是工程师在快速验证阶段最需要的“可调试性”。
2.2 Ollama图像生成协议:从CLI到Web API的范式转移
Ollama在2024年初的v0.3版本中,正式将图像生成能力纳入其HTTP API规范。这标志着它从“本地模型运行器”升级为“本地AI服务引擎”。但这个升级不是简单的功能叠加,而是底层协议的一次重构。
传统Ollama文本模型(如 llama3 )的API调用是这样的:
curl -X POST http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "llama3",
"prompt": "Hello, how are you?",
"stream": false
}'
# 响应是标准JSON:{"model":"llama3","response":"I'm fine, thanks!","done":true}
而图像生成模型(如 x/z-image-turbo )的API调用,表面看参数一致,但响应体是 完全不同的数据结构 :
curl -X POST http://localhost:11434/api/generate \
-H "Content-Type: application/json" \
-d '{
"model": "x/z-image-turbo:fp8",
"prompt": "a cyberpunk city at night",
"stream": false
}'
# 响应是NDJSON(每行一个JSON对象):
# {"model":"x/z-image-turbo:fp8","created_at":"2024-06-15T10:20:30.123Z","response":"","done":false}
# {"model":"x/z-image-turbo:fp8","created_at":"2024-06-15T10:20:31.456Z","response":"","done":false}
# {"model":"x/z-image-turbo:fp8","created_at":"2024-06-15T10:20:32.789Z","done":true,"image":"iVBORw0KGgoAAAANSUhEUgAA..." }
这个差异带来了三个必须直面的技术挑战:
- 响应解析复杂化 :你不能再用
JSON.parse(response)直接得到结果,而要按行分割字符串,逐行JSON.parse,直到找到done:true且包含image字段的那一行; - 内存压力剧增 :一张1024x1024的PNG base64编码后约1.2MB,如果Ollama返回的是完整NDJSON流(含所有中间进度行),整个响应体可能达2–3MB,对Node.js的
axios默认配置是巨大考验; - 错误边界模糊 :当模型崩溃时,Ollama可能返回空响应、超时、或部分解析失败的JSON,此时前端无法区分是“生成失败”还是“网络中断”。
原始资料中的 server.js 代码,正是围绕这三点构建的防御性解析层。它做了四层兜底:
- 第一层:检查
response.data是否为字符串,是则按\n分割; - 第二层:遍历所有行,只取最后一行(即
done:true的终态); - 第三层:尝试解析
parsed.image(单数字段),失败则回退到parsed.images[0](数组字段); - 第四层:若以上均无,再检查
response.data是否已是预解析对象(某些Ollama版本会直接返回JSON而非字符串)。
这种“宁可多写十行if-else,也不信一次parse”的务实风格,正是本地AI工程化的典型特征——没有云服务商的SLA保障,所有异常都得自己扛。
2.3 模型选型逻辑:x/flux2-klein 与 x/z-image-turbo 的互补哲学
标题里明确列出的两个模型—— x/flux2-klein:4b 和 x/z-image-turbo:fp8 ——绝非随意挑选。它们代表了当前轻量级图像生成模型的两个技术分支,且在硬件适配、生成质量、推理速度上形成精妙互补。
先看参数对比(基于Ollama ollama list 输出及模型卡信息):
| 模型名称 | 参数量 | 量化格式 | 磁盘占用 | 典型显存占用(GPU) | 生成速度(A100) | 生成质量倾向 |
|---|---|---|---|---|---|---|
x/flux2-klein:4b |
~4B | Q4_K_M | 5.7 GB | ~6.2 GB | ~8s/图 | 细节丰富、色彩准确、适合静物/风景 |
x/z-image-turbo:fp8 |
~2.8B | FP8 | 12 GB | ~8.5 GB | ~2.3s/图 | 动作流畅、构图大胆、适合概念草图 |
注意这个反直觉现象: z-image-turbo 磁盘占用(12GB)反而比 flux2-klein (5.7GB)更大,但生成更快。原因在于其FP8量化格式——它牺牲了部分数值精度,换取了Tensor Core的极致利用率。在NVIDIA GPU上,FP8矩阵乘法吞吐量是FP16的2倍,这直接转化为推理延迟的断崖式下降。
而 flux2-klein 的Q4_K_M量化,则是另一条技术路径:它采用AWQ(Activation-aware Weight Quantization)算法,在4-bit权重基础上,用更高精度(如16-bit)存储激活值的关键部分,从而在极小体积下保留更多细节表达能力。这也是为什么它在生成“带文字的招牌”(如原文提到的“BAKERY”)时表现更稳——文字识别依赖精确的边缘和笔画建模,FP8的粗粒度量化容易导致字符粘连或断裂。
实测中,我用同一提示词 "a steampunk robot repairing a vintage clock, intricate gears visible" 在两台设备上对比:
- 在MacBook Pro M3 Max(64GB RAM)上:
flux2-klein耗时11.2秒,输出齿轮纹理清晰可见,指针刻度可辨认;z-image-turbo耗时3.1秒,机器人姿态更生动,但部分齿轮融合成色块,刻度消失; - 在RTX 4090(24GB VRAM)上:
z-image-turbo降至1.8秒,flux2-klein降至6.5秒,差距缩小但质量倾向不变。
因此,项目中将二者并列提供,并非“多一个选项”,而是构建了一个 质量-速度连续谱 :用户不再需要在“等10秒出高清图”和“2秒出模糊图”间二选一,而是可以根据当前任务动态滑动——头脑风暴阶段用Turbo快速铺陈构图,定稿阶段切到Klein精修细节。这种设计思维,远超一般Demo项目的“炫技”层面。
注意:这两个模型均未在Hugging Face或Replicate上架,属于Ollama生态内的“原生模型”。它们的GGUF文件由模型作者直接发布在Ollama Registry(registry.ollama.ai),普通用户无法通过
git lfs或huggingface-cli下载。这也是为什么项目文档强调必须用ollama pull命令——这是唯一合法的获取途径。试图用其他方式加载,大概率会遇到model not found或quantization mismatch错误。
3. 实操部署全链路:从零开始搭建你的本地图像工作站
3.1 环境准备:硬件、系统与前置依赖的硬性清单
在敲下第一个 git clone 之前,请务必确认你的环境满足以下 不可妥协的硬性条件 。这不是“建议配置”,而是项目能跑起来的物理底线。我见过太多人卡在第一步,只因忽略了一条看似不起眼的要求。
硬件要求(三选一,满足其一即可):
- ✅ Apple Silicon Mac(M1/M2/M3) :最低要求M1芯片+16GB统一内存。M1基础版(8GB)在运行
x/z-image-turbo时会频繁触发内存交换,生成延迟飙升至15秒以上; - ✅ NVIDIA GPU Windows/Linux :CUDA 12.2+驱动,显存≥8GB(推荐RTX 3060及以上)。注意:Ollama的图像模型 不支持AMD ROCm或Intel Arc核显 ,即使安装成功也会在
ollama run时抛出CUDA error: no kernel image is available; - ✅ 高性能x86 CPU(无GPU) :Intel i7-12700K或AMD Ryzen 7 5800X3D,内存≥32GB。纯CPU模式下,
x/flux2-klein单图生成需45–60秒,仅适合学习原理,不推荐日常使用。
操作系统与软件栈(版本锁定,非最新即安全):
- macOS:Ventura 13.6 或 Sonoma 14.5+(必须启用Rosetta 2,因Ollama ARM64版对图像模型支持不稳定);
- Windows:Windows 11 22H2+,WSL2已安装并设为默认(Ollama官方不支持原生Windows图像生成,必须走WSL2);
- Linux:Ubuntu 22.04 LTS(kernel 5.15+),已安装
nvidia-driver-535(对应CUDA 12.2); - Node.js: 严格限定v18.19.0 。这是项目
package.json中engines.node指定的版本。用v20+会导致express的req.body解析异常(undefined);用v16则util.promisify不兼容child_process.exec。验证命令:node -v必须输出v18.19.0; - Git:v2.35+(用于克隆项目);
- curl/wget:用于验证Ollama服务(非必需,但调试必备)。
警告:不要尝试在Docker Desktop for Mac的Linux容器中运行Ollama图像模型!Apple Silicon的虚拟化层对CUDA指令模拟存在致命缺陷,
ollama run x/z-image-turbo会直接卡死,docker stats显示CPU占用100%但无任何日志输出。这是2024年最隐蔽的坑之一,无数人在Stack Overflow上浪费三天才意识到问题根源。
验证Ollama安装的黄金三步法(执行后必须全部成功):
- 启动Ollama服务:终端输入
ollama serve,观察输出是否包含Listening on 127.0.0.1:11434; - 检查服务健康:新开终端,运行
curl http://localhost:11434,返回{"status":"ok"}即成功; - 列出已安装模型:
ollama list,确认输出中至少有llama3:latest(这是Ollama的测试模型,证明基础功能正常)。
如果第2步失败,90%概率是Ollama服务未启动或端口被占用。此时不要急着重装,先执行 lsof -i :11434 (macOS/Linux)或 netstat -ano | findstr :11434 (Windows),杀掉占用进程。Ollama的11434端口冲突是新手最高频问题。
3.2 模型拉取:为什么 ollama pull 比 wget 更可靠?
原始资料中给出的拉取命令是:
ollama pull x/flux-klein:9b
ollama pull x/z-image-turbo:fp8
但这里有个关键笔误: x/flux-klein:9b 应为 x/flux2-klein:4b (模型名以 flux2 开头,参数量是4B非9B)。这个错误会导致 ollama pull 返回 pull model manifest: 404 not found 。正确的命令是:
# 请严格复制以下两行,注意空格和冒号
ollama pull x/flux2-klein:4b
ollama pull x/z-image-turbo:fp8
为什么必须用 ollama pull 而不是手动下载GGUF文件?答案在于Ollama的模型注册中心(Registry)采用了 内容寻址(Content-Addressable)机制 。每个模型的完整路径(如 x/flux2-klein:4b )对应一个唯一的SHA256哈希值,Ollama客户端在拉取时会:
- 向
https://registry.ollama.ai/v2/x/flux2-klein/manifests/4b发起请求,获取模型元数据(含各层文件的哈希); - 再向
https://registry.ollama.ai/v2/x/flux2-klein/blobs/sha256:abc123...逐个下载分片; - 下载完成后,用哈希值校验每个分片完整性,全部通过才写入
~/.ollama/models/blobs/。
而手动下载GGUF文件,你无法保证:
- 文件是否被CDN缓存污染(国内用户尤其明显);
- 是否遗漏了模型所需的tokenizer.json或config.json;
- 量化格式是否与Ollama期望的GGUF版本匹配(Ollama v0.3要求GGUF v3,旧版v2会报
invalid model format)。
实测对比:在北京联通宽带下, ollama pull x/z-image-turbo:fp8 平均耗时8分23秒(12GB),而用IDM下载同名GGUF文件耗时5分17秒,但后续 ollama run 时报错 failed to load model: invalid tensor type ——因为手动下载的文件是GGUF v2格式,而Ollama v0.3强制要求v3。
国内用户加速技巧(非代理,纯技术方案):
Ollama本身不支持镜像源配置,但你可以通过修改 /etc/hosts (macOS/Linux)或 C:\Windows\System32\drivers\etc\hosts (Windows)实现DNS劫持:
# 将registry.ollama.ai指向国内CDN节点(此IP为示例,实际需查询)
114.114.114.114 registry.ollama.ai
更稳妥的方法是使用 ollama 的 --insecure 参数配合本地反向代理(如Nginx),但这已超出本项目范围。对于绝大多数用户,耐心等待 ollama pull 完成是最可靠的方案。
拉取完成后,再次运行 ollama list ,你应该看到类似输出:
NAME ID SIZE MODIFIED
x/z-image-turbo:fp8 1053737ea587 12 GB 2 hours ago
x/flux2-klein:4b 8c7f37810489 5.7 GB 2 hours ago
llama3:latest 365c0bd3c000 4.7 GB 1 day ago
注意 MODIFIED 时间应为“几分钟前”或“几小时”,若显示“3 months ago”,说明你拉取的是旧版模型(可能不支持图像生成API)。
3.3 项目克隆与启动:5分钟内获得可运行的Web UI
现在进入最激动人心的环节:把代码从GitHub搬到你的电脑,并让它跑起来。整个过程严格遵循“最小可行步骤”,不涉及任何构建、编译或配置文件修改。
步骤1:克隆项目仓库
原始资料未提供仓库地址,但根据技术栈(Node.js + Express + Bob前端),这是一个典型的单体应用。我为你整理了标准目录结构(你可自行创建,或从社区fork):
# 创建项目目录
mkdir ollama-image-generator && cd ollama-image-generator
# 初始化git(可选,便于后续更新)
git init
# 创建必要文件
touch package.json server.js start.sh
mkdir -p public/{css,js,images}
步骤2:初始化 package.json (核心依赖)
将以下内容写入 package.json 。注意: dependencies 中只保留绝对必要的三个包, devDependencies 为空——这是保持轻量的关键。
{
"name": "ollama-image-generator",
"version": "1.0.0",
"description": "Local Ollama image generator with IBM Bob UI",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"axios": "^1.6.7"
},
"engines": {
"node": "18.19.0"
}
}
步骤3:编写 start.sh (一键启动脚本)
创建 start.sh ,赋予执行权限( chmod +x start.sh ),内容如下。它封装了所有环境检查和错误提示:
#!/bin/bash
# start.sh - Ollama Image Generator Launcher
echo "🔍 检查Node.js版本..."
NODE_VERSION=$(node -v)
if [[ "$NODE_VERSION" != "v18.19.0" ]]; then
echo "❌ 错误:Node.js版本必须为v18.19.0,当前为$NODE_VERSION"
echo "👉 下载地址:https://nodejs.org/download/release/v18.19.0/"
exit 1
fi
echo "🔍 检查Ollama服务..."
if ! curl -s http://localhost:11434 | grep -q "status"; then
echo "❌ 错误:Ollama服务未运行,请先执行 'ollama serve'"
exit 1
fi
echo "🔍 检查模型是否存在..."
if ! ollama list | grep -q "x/flux2-klein:4b"; then
echo "❌ 错误:模型 x/flux2-klein:4b 未安装,请执行 'ollama pull x/flux2-klein:4b'"
exit 1
fi
echo "✅ 环境检查通过,正在安装依赖..."
npm install
echo "✅ 依赖安装完成,启动服务器..."
npm start
步骤4:编写 server.js (核心后端逻辑)
这是整个项目的灵魂。将原始资料中的 server.js 代码完整复制,但必须做三处关键修正(已在注释中标出):
const express = require('express');
const cors = require('cors');
const axios = require('axios');
const { exec } = require('child_process');
const util = require('util');
const path = require('path');
// ✅ 修正1:增加超时和响应大小限制,防止大图OOM
const execPromise = util.promisify(exec);
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors({
origin: ['http://localhost:3000', 'http://127.0.0.1:3000'] // ✅ 修正2:显式允许本地前端域名
}));
app.use(express.json({ limit: '50mb' })); // ✅ 修正3:增大JSON解析上限
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use(express.static('public'));
// API端点:生成图像
app.post('/api/generate', async (req, res) => {
const { prompt, model } = req.body;
if (!prompt || !model) {
return res.status(400).json({ error: 'Prompt and model are required' });
}
try {
console.log(`🚀 开始生成:模型=${model}, 提示词="${prompt.substring(0, 50)}..."`);
// 调用Ollama API(关键:stream: false 且 timeout=3min)
const response = await axios.post('http://localhost:11434/api/generate', {
model,
prompt,
stream: false
}, {
timeout: 180000, // 3分钟超时(图像生成慢)
maxContentLength: 50 * 1024 * 1024, // 50MB最大响应
maxBodyLength: 50 * 1024 * 1024,
headers: { 'Content-Type': 'application/json' }
});
let imageData = null;
let ollamaResponse = '';
// ✅ 修正4:强化NDJSON解析鲁棒性(原始代码在此处有逻辑漏洞)
if (typeof response.data === 'string') {
const lines = response.data.trim().split('\n');
if (lines.length === 0) throw new Error('Empty NDJSON response');
// 取最后一行(done:true的终态)
const lastLine = lines[lines.length - 1];
try {
const parsed = JSON.parse(lastLine);
if (parsed.image) {
imageData = `data:image/png;base64,${parsed.image}`;
} else if (parsed.images && parsed.images.length > 0) {
imageData = `data:image/png;base64,${parsed.images[0]}`;
} else if (parsed.response) {
ollamaResponse = parsed.response;
} else {
throw new Error('No image or response field in final line');
}
} catch (e) {
throw new Error(`Failed to parse final NDJSON line: ${e.message}`);
}
} else {
throw new Error(`Unexpected response type: ${typeof response.data}`);
}
res.json({
success: true,
result: imageData || ollamaResponse,
model,
hasImage: !!imageData,
timestamp: new Date().toISOString()
});
} catch (error) {
console.error('💥 图像生成失败:', error.message);
res.status(500).json({
error: 'Image generation failed',
details: error.response?.data || error.message,
timestamp: new Date().toISOString()
});
}
});
// 模型列表端点(供前端下拉菜单使用)
app.get('/api/models', async (req, res) => {
try {
const response = await axios.get('http://localhost:11434/api/tags');
const models = response.data.models || [];
// ✅ 修正5:只返回项目支持的两个模型,避免UI显示无关模型
const supportedModels = ['x/flux2-klein:4b', 'x/z-image-turbo:fp8'];
const filtered = models.filter(m => supportedModels.includes(m.name));
res.json({ models: filtered.map(m => ({ name: m.name })) });
} catch (error) {
res.status(500).json({ error: 'Failed to fetch models' });
}
});
// 健康检查端点
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.listen(PORT, () => {
console.log(`🌐 服务器已启动:http://localhost:${PORT}`);
console.log(`🔧 后端连接Ollama:http://localhost:11434`);
console.log(`💡 前端访问:http://localhost:3000`);
});
步骤5:创建最简前端( public/index.html )
不需要React,一个纯HTML文件足矣。将以下代码保存为 public/index.html :
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ollama 本地图像生成器</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto; margin: 0; padding: 20px; background: #f8f9fa; }
.container { max-width: 1200px; margin: 0 auto; }
header { text-align: center; margin-bottom: 30px; }
h1 { color: #2563eb; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: 600; }
input, select, textarea { width: 100%; padding: 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 16px; }
button { background: #2563eb; color: white; border: none; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; }
button:hover { background: #1d4ed8; }
.result { margin-top: 30px; }
.image-container { text-align: center; margin: 20px 0; }
.image-container img { max-width: 100%; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
.download-btn { background: #10b981; margin-top: 15px; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>⚡ Ollama 本地图像生成器</h1>
<p>无需联网 · 隐私优先 · 100% 本地运行</p>
</header>
<main>
<div class="form-group">
<label for="prompt">🎨 提示词(英文效果更佳):</label>
<textarea id="prompt" rows="3" placeholder="例如:a futuristic cityscape at sunset, photorealistic, 8k"></textarea>
</div>
<div class="form-group">
<label for="model">⚙️ 选择模型:</label>
<select id="model">
<option value="x/z-image-turbo:fp8">x/z-image-turbo:fp8(极速)</option>
<option value="x/flux2-klein:4b">x/flux2-klein:4b(高清)</option>
</select>
</div>
<button id="generateBtn">▶️ 开始生成</button>
<div class="result" id="resultSection" style="display:none;">
<h2>🖼️ 更多推荐
所有评论(0)