IBM Bob + Ollama图像生成

自从我在Ollama上发现了两个有趣的图像生成模型已经过去了几天,LinkedIn和其他技术圈内关于x/flux2-kleinz-image-turbo的炒作确实是真实的。在本地测试后,我对它们的性能印象深刻,但在终端中运行命令只能走这么远——我想把它们包装在一个经过打磨的、具有适当UI的功能应用程序中。

自然地,我在这个任务中涉及了Bob(正如你可能已经意识到的那样,我现在不用我最喜欢的AI合作伙伴就不会处理任何事情)。Bob再次表现出色,只需做一些小的调整,我们就有一个工作的界面。我们将在下面遍历应用程序和构建过程。

1、构建应用程序:功能和实现

为了将这些强大的模型变为现实,Bob帮助我构建了一个本地优先的Web应用程序,填补了Ollama CLI和用户友好体验之间的空白。目标是创建一个"干净和现代"的东西,感觉与它支持的"Turbo"模型一样响应迅速。

实现侧重于几个关键功能,使生成过程无缝衔接。

2、Ollama图像生成器的关键功能

  • 🎨 干净现代的用户界面:使用vanilla JavaScript和CSS构建的精简、响应式Web仪表板,确保焦点保持在生成的艺术上。
  • 🤖 多模型支持:对两种目前趋势的出色模型的本地集成;(x/flux2-klein:4b:优化高质量、详细视觉输出)和(x/z-image-turbo:fp8:设计用于闪电般快速生成速度)。
  • 📝 实时交互:文本提示输入,直接连接到Ollama后端,即时反馈。
  • ⬇️ 智能下载:生成图像后,你可以使用自定义文件名(默认为{sanitized-prompt}-{date}.png格式)将其本地保存。
  • 📊 生成历史:持久化跟踪系统,以记录你的创意会话。
  • 🔄 连接监控:实时状态指示器,确保你的本地Ollama实例已连接并准备处理请求。
  • 💾 100%本地处理:隐私是内置的——一切都在你自己的硬件上运行,没有云依赖或外部数据传输。

3、逐步安装和部署

随着架构到位,运行环境很简单。以下是如何设置项目甚至使用Bob帮助我构建的自动化将其投入生产。

3.1 本地设置

在开始之前,确保你已安装Node.js (v14+)和Ollama。你需要使用终端提取前面提到的特定模型:

ollama pull x/flux-klein:9b
ollama pull x/z-image-turbo:fp8
ollama list
NAME                        ID              SIZE      MODIFIED     
x/z-image-turbo:fp8         1053737ea587    12 GB     4 hours ago     
x/flux2-klein:4b            8c7f37810489    5.7 GB    4 hours ago     
llama3.2:latest             a80c4f17acd5    2.0 GB    5 weeks ago     
ibm/granite4:tiny-h         566b725534ea    4.2 GB    7 weeks ago     
granite3.2-vision:2b        3be41a661804    2.4 GB    7 weeks ago     
ibm/granite4:latest         98b5cfd619dd    2.1 GB    7 weeks ago     
ministral-3:latest          77300ee7514e    6.0 GB    7 weeks ago     
llama3.2-vision:latest      6f2f9757ae97    7.8 GB    7 weeks ago     
embeddinggemma:latest       85462619ee72    621 MB    7 weeks ago     
llama3:latest               365c0bd3c000    4.7 GB    7 weeks ago     
granite3.3:latest           fd429f23b909    4.9 GB    8 weeks ago     
deepseek-r1:latest          6995872bfe4c    5.2 GB    2 months ago    
llama3:8b-instruct-q4_0     365c0bd3c000    4.7 GB    2 months ago    
mistral:7b                  6577803aa9a0    4.4 GB    2 months ago    
ibm/granite4:micro          89962fcc7523    2.1 GB    2 months ago    
mxbai-embed-large:latest    468836162de7    669 MB    3 months ago    
all-minilm:latest           1b226e2802db    45 MB     3 months ago     
granite-embedding:latest    eb4c533ba6f7    62 MB     3 months ago     
qwen3-vl:235b-cloud         7fc468f95411    -         3 months ago     
granite4:micro-h            ba791654cc27    1.9 GB    3 months ago     
granite4:latest             4235724a127c    2.1 GB    3 months ago     
granite-embedding:278m      1a37926bf842    562 MB    3 months ago     
nomic-embed-text:latest     0a109f422b47    274 MB    5 months ago

要在本地启动应用程序:

  • 导航到项目目录:cd /xxx/ollama-image-generator
  • 通过运行npm install安装依赖项
  • 使用提供的脚本启动应用程序./start.sh
  • http://localhost:3000访问UI .
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');

const execPromise = util.promisify(exec);

const app = express();
const PORT = process.env.PORT || 3000;

app.use(cors());
app.use(express.json());
app.use(express.static('public'));

// 使用Ollama HTTP 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(`Generating image with model: ${model}`);
        console.log(`Prompt: ${prompt}`);

        // 调用Ollama的HTTP API端点进行图像生成
        // 文档:https://github.com/ollama/ollama/blob/main/docs/api.md
        //
        // 请求格式:
        // POST /api/generate
        // {
        //   "model": "x/flux2-klein:4b",
        //   "prompt": "your prompt here",
        //   "stream": false  // 我们使用非流式以简化
        // }
        //
        // 响应格式:换行分隔的JSON (NDJSON)
        // 每行是一个单独的JSON对象,显示生成进度
        最后一行包含'image'字段中的完整图像

        const response = await axios.post('http://localhost:11434/api/generate', {
            model: model,
            prompt: prompt,
            stream: false // 非流模式一次返回所有数据
        }, {
            timeout: 180000, // 3分钟超时(图像生成慢)
            maxContentLength: 50 * 1024 * 1024, // 50MB最大响应大小
            maxBodyLength: 50 * 1024 * 1024     // 50MB最大请求大小
        });

        console.log('Ollama API response received');
        console.log('Response data type:', typeof response.data);
        console.log('Is Buffer:', Buffer.isBuffer(response.data));

        let imageData = null;
        let ollamaResponse = '';

        // 关键:Ollama返回换行分隔的JSON (NDJSON) 进行图像生成
        // 格式:每行是一个单独的JSON对象
        // 示例:
        // {"model":"x/flux2-klein:4b","created_at":"...","response":"","done":false}
        // {"model":"x/flux2-klein:4b","created_at":"...","response":"","done":false}
        // {"model":"x/flux2-klein:4b","created_at":"...","done":true,"image":"base64data..."}
        //
        // 最后一行包含 'image' 字段(单数,不是'images')中的完整图像

        if (typeof response.data === 'string') {
            console.log('Response is a string, length:', response.data.length);
            console.log('First 200 chars:', response.data.substring(0, 200));

            try {
                // 将换行分隔的JSON拆分为单独的行
                const lines = response.data.trim().split('\n');
                console.log('Number of lines:', lines.length);

                // 解析包含最终响应和图像数据的最后一行
                const lastLine = lines[lines.length - 1];
                const parsed = JSON.parse(lastLine);

                console.log('Parsed response keys:', Object.keys(parsed));

                // 重要:图像生成模型返回单数 'image' 字段
                // 不是 'images' 数组。这与某些其他API不同。
                if (parsed.image) {
                    // 将base64 PNG数据转换为数据URI以便浏览器显示
                    imageData = `data:image/png;base64,${parsed.image}`;
                    console.log('✓ Found image in parsed response (singular image field)');
                }
                // 回退:检查images数组(某些模型可能使用这个)
                else if (parsed.images && parsed.images.length > 0) {
                    imageData = `data:image/png;base64,${parsed.images[0]}`;
                    console.log('✓ Found image in parsed response images array');
                }
                // 对于基于文本的模型,response字段包含文本
                else if (parsed.response) {
                    ollamaResponse = parsed.response;
                    console.log('Found response field, length:', ollamaResponse.length);
                }
            } catch (e) {
                console.log('Failed to parse as JSON:', e.message);
                ollamaResponse = response.data;
            }
        }
        // 回退:处理二进制Buffer响应(对Ollama来说很罕见)
        else if (Buffer.isBuffer(response.data)) {
            const base64Data = response.data.toString('base64');
            imageData = `data:image/png;base64,${base64Data}`;
            console.log('✓ Converted Buffer to base64 image');
        }
        // 回退:处理带有images数组的预解析JSON
        else if (response.data.images && response.data.images.length > 0) {
            imageData = `data:image/png;base64,${response.data.images[0]}`;
            console.log('✓ Found image in images array');
        }
        // 回退:处理带有response字段的预解析JSON
        else if (response.data.response) {
            ollamaResponse = response.data.response;
            console.log('Response length:', ollamaResponse.length);
        }

        if (!imageData && !ollamaResponse) {
            console.log('✗ No image or response data found');
        }

        res.json({
            success: true,
            result: imageData || ollamaResponse || 'No image generated',
            model: model,
            hasImage: !!imageData,
            debug: {
                responseType: typeof response.data,
                isBuffer: Buffer.isBuffer(response.data),
                responseLength: ollamaResponse.length,
                hasImages: !!response.data.images
            }
        });

    } catch (error) {
        console.error('Error generating image:', error.message);
        console.error('Error details:', error.response?.data);
        res.status(500).json({
            error: 'Failed to generate image',
            details: error.message,
            response: error.response?.data || ''
        });
    }
});

// 检查可用模型的端点
app.get('/api/models', async (req, res) => {
    try {
        const response = await axios.get('http://localhost:11434/api/tags');
        const models = response.data.models || [];

        res.json({
            models: models.map(m => ({ name: m.name }))
        });
    } catch (error) {
        console.error('Error fetching models:', error.message);
        res.status(500).json({
            error: 'Failed to fetch models',
            details: error.message
        });
    }
});

// 健康检查端点
app.get('/api/health', async (req, res) => {
    try {
        await axios.get('http://localhost:11434/api/tags', { timeout: 5000 });
        res.json({ status: 'ok', ollama: 'connected' });
    } catch (error) {
        res.status(503).json({ status: 'error', ollama: 'disconnected', details: error.message });
    }
});

app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
    console.log(`Using Ollama API for image generation`);
    console.log(`Supported models: x/flux2-klein:4b, x/z-image-turbo:fp8`);
});

// 由Bob制作

3.2 Docker化工作流程

如果你更喜欢容器化方法,Bob帮助创建了Dockerfile以简化部署。

  • 构建镜像docker build -t ollama-image-generator:latest .
  • 运行容器:使用docker run命令,确保你将OLLAMA_URL环境变量指向你的本地主机。
# Ollama图像生成器的多阶段构建
FROM node:18-alpine AS builder

# 设置工作目录
WORKDIR /app

# 复制包文件
COPY package*.json ./

# 安装依赖
RUN npm ci --only=production

# 生产阶段
FROM node:18-alpine

# 设置工作目录
WORKDIR /app

# 创建非root用户
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# 从builder复制依赖项
COPY --from=builder /app/node_modules ./node_modules

# 复制应用程序文件
COPY --chown=nodejs:nodejs . .

# 设置环境变量
ENV NODE_ENV=production \
    PORT=3000 \
    OLLAMA_URL=http://ollama:11434

# 暴露端口
EXPOSE 3000

# 切换到非root用户
USER nodejs

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"

# 启动应用程序
CMD ["node", "server.js"]

3.3 使用Kubernetes扩展

对于那些希望在更健壮环境中运行的人,项目包括完整的Kubernetes清单。

  • ConfigMap:处理环境配置,如Ollama服务URL。
  • Deployment:管理应用程序生命周期,支持扩展到多个副本。
  • Service:在端口80上公开接口的LoadBalancer服务。

你可以使用单个命令将整个堆栈部署到集群:kubectl apply -f k8s/

4、Bob如何实现

使用IBM Project Bob,我能够快速迭代处理处理Ollama独特的换行分隔JSON响应格式所需的server.js逻辑。因为Ollama流式传输生成进度,Bob帮助我实现了一个专门从流的最后一行中隔离最终base64编码PNG数据的解析器。

生成的架构是一个健壮的Node.js和Express后端,通过REST API与前端通信,将原始CLI力量转化为可点击的视觉体验。

4.1 用户界面和模型使用

Bob编排的用户界面设计用于高速实验。它具有一个精简的仪表板,最终用户可以通过简单的下拉菜单在x/flux2-klein:4b(高质量)和x/z-image-turbo:fp8(快速生成)模型之间进行选择。一旦选择了模型,你只需在文本区域键入你的提示——例如测试用例"一个充满生物发光植物和发光生物的异想天开的森林"——来触发生成。

然后,UI为生成的艺术提供专用的显示区域,并带有下载和使用自定义、清理过的文件名保存图像的按钮。

正如我们在测试结果中看到的,虽然速度令人印象深刻,但视觉输出有时会受到影响;例如,模型可能在复杂文本或超特定细节上挣扎,反映了围绕这些轻量级本地模型的当前实验"炒作"。

  • 好的

如上面所见的一些真正好的例子👏,提示如"生成一个宇航员在太空行走时凝视地球的图像"或Ollama上提供的示例"一个用金字母写着'BAKERY'的店面招牌",这理所当然地不会给出与Ollama模型页面相同的图像。

  • 不好的

即使尝试了几次,我仍然无法生成带有"IBM"作为建筑物上的徽标的图像🤷‍♂️

我们可以随时回到提示符以重新使用或重新处理它们。

5、结束语

总之,虽然这些本地模型对于维护隐私和构建自定义应用程序而不依赖基于云的服务非常出色,但它们在多功能性方面仍有成长空间。目前,我仍将继续使用Google的Nano Banana和其他高级服务;主要原因是能够提供我自己的参考图像——就像为这篇博客文章生成的图像一样——来组合和迭代全新的视觉概念。虽然本地堆栈强大且私密,但基于云的套件的多模态灵活性在复杂的创意工作流程中仍然难以击败。


原文链接: The Best of Both Worlds: Merging IBM's Project Bob with Ollama's Image Ecosystem

汇智网翻译整理,转载请标明出处