An international collaborative work

Introduction
At the start of the year, I developed an image generation application using Bob to build a custom interface. The backend integrates the ‘x/flux2-klein’ and ‘x/z-image-turbo’ models alongside Ollama. Recently, the application received significant enhancements thanks to contributions from my colleague, Akira Onishi-san, in Japan.
What’s truly fantastic is the way my colleague transformed the app; by leveraging Bob, he made the architecture modular and much more secure. He also went the extra mile by refining the bash scripts, making the automatic startup and shutdown processes more robust than ever.
So let’s dig into the application’s enhancements.
Implementation

The architecture of the application remains the same, but the main enhanced modules are provided hereafter (the change log is accessible on GitHub)
- app.js: The latest update to
app.js introduces significant hardening and performance tracking. On the security front, we’ve eliminated vulnerabilities by replacing innerHTML with safe DOM manipulation and implementing filename sanitization to prevent directory traversal. Additionally, we’ve integrated memory usage tracking within the history logs and refined the IME composition handling to ensure a seamless experience for Japanese input.
// Application state
let generationHistory = [];
// DOM elements
const promptInput = document.getElementById('prompt');
const modelSelect = document.getElementById('model');
const generateBtn = document.getElementById('generateBtn');
const resultContainer = document.getElementById('result');
const statusDiv = document.getElementById('status');
const historyList = document.getElementById('historyList');
const btnText = generateBtn.querySelector('.btn-text');
const loader = generateBtn.querySelector('.loader');
// Check Ollama connection on load
checkOllamaConnection();
// Event listeners
generateBtn.addEventListener('click', generateImage);
// Check if Ollama is running
async function checkOllamaConnection() {
statusDiv.textContent = 'Checking connection...';
statusDiv.className = 'status checking';
try {
const response = await fetch('/api/health');
const data = await response.json();
if (data.status === 'ok') {
statusDiv.textContent = '✓ Connected to Ollama';
statusDiv.className = 'status connected';
} else {
statusDiv.textContent = '✗ Ollama not connected';
statusDiv.className = 'status disconnected';
}
} catch (error) {
statusDiv.textContent = '✗ Cannot connect to server';
statusDiv.className = 'status disconnected';
}
}
// Generate image function
async function generateImage() {
const prompt = promptInput.value.trim();
const model = modelSelect.value;
if (!prompt) {
alert('Please enter a prompt');
return;
}
// Disable button and show loading state
generateBtn.disabled = true;
btnText.textContent = 'Generating...';
loader.style.display = 'block';
// Clear previous result
resultContainer.innerHTML = '';
const placeholder = document.createElement('div');
placeholder.className = 'placeholder';
const p = document.createElement('p');
p.textContent = 'Generating your image...';
placeholder.appendChild(p);
resultContainer.appendChild(placeholder);
try {
const response = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt, model })
});
const data = await response.json();
if (data.success) {
displayResult(data, prompt, model);
addToHistory(prompt, model, data.result, data.memoryUsage);
} else {
displayError(data.error || 'Failed to generate image');
}
} catch (error) {
displayError('Network error: ' + error.message);
} finally {
// Re-enable button
generateBtn.disabled = false;
btnText.textContent = 'Generate Image';
loader.style.display = 'none';
}
}
// Display the result
function displayResult(data, prompt, model) {
const resultContent = document.createElement('div');
resultContent.className = 'result-content';
// Check if result contains image data (base64)
if (data.result && (data.result.startsWith('data:image') || data.result.includes('base64'))) {
const img = document.createElement('img');
img.src = data.result;
img.alt = 'Generated image';
img.className = 'result-image';
resultContent.appendChild(img);
// Add download button
const downloadBtn = document.createElement('button');
downloadBtn.className = 'download-btn';
downloadBtn.textContent = '⬇️ Download Image';
downloadBtn.onclick = () => downloadImage(data.result, prompt);
resultContent.appendChild(downloadBtn);
} else {
// Display as text if not an image
const textDiv = document.createElement('div');
textDiv.className = 'result-text';
textDiv.textContent = data.result;
resultContent.appendChild(textDiv);
}
// Add info section
const infoDiv = document.createElement('div');
infoDiv.className = 'result-info';
const modelLabel = document.createElement('strong');
modelLabel.textContent = 'Model:';
infoDiv.appendChild(modelLabel);
infoDiv.appendChild(document.createTextNode(' ' + model));
infoDiv.appendChild(document.createElement('br'));
const promptLabel = document.createElement('strong');
promptLabel.textContent = 'Prompt:';
infoDiv.appendChild(promptLabel);
infoDiv.appendChild(document.createTextNode(' ' + prompt));
resultContent.appendChild(infoDiv);
resultContainer.innerHTML = '';
resultContainer.appendChild(resultContent);
}
// Download image with custom filename
function downloadImage(imageDataUrl, defaultPrompt) {
// Generate a default filename from the prompt (sanitized)
const sanitizedPrompt = defaultPrompt
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-')
.substring(0, 50);
const timestamp = new Date().toISOString().split('T')[0];
const defaultFilename = `${sanitizedPrompt}-${timestamp}.png`;
// Prompt user for filename
const filename = prompt('Enter filename for the image:', defaultFilename);
// If user cancels, don't download
if (!filename) return;
// Remove all path separators to prevent directory traversal
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9._-]/g, '_');
// Ensure filename has .png extension
const finalFilename = sanitizedFilename.endsWith('.png') ? sanitizedFilename : `${sanitizedFilename}.png`;
// Create a temporary link element and trigger download
const link = document.createElement('a');
link.href = imageDataUrl;
link.download = finalFilename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Display error message
function displayError(message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
const strong = document.createElement('strong');
strong.textContent = 'Error:';
const messageText = document.createTextNode(' ' + message);
const br1 = document.createElement('br');
const br2 = document.createElement('br');
const small = document.createElement('small');
small.textContent = 'Make sure Ollama is running and the models are installed.';
errorDiv.appendChild(strong);
errorDiv.appendChild(messageText);
errorDiv.appendChild(br1);
errorDiv.appendChild(br2);
errorDiv.appendChild(small);
resultContainer.innerHTML = '';
resultContainer.appendChild(errorDiv);
}
// Add to history
function addToHistory(prompt, model, result, memoryUsage) {
const historyItem = {
prompt,
model,
result,
timestamp: new Date().toLocaleString(),
memoryUsage: memoryUsage || null
};
generationHistory.unshift(historyItem);
// Keep only last 10 items
if (generationHistory.length > 10) {
generationHistory.pop();
}
updateHistoryDisplay();
}
// Update history display
function updateHistoryDisplay() {
historyList.innerHTML = '';
if (generationHistory.length === 0) {
const emptyMessage = document.createElement('p');
emptyMessage.style.color = '#9ca3af';
emptyMessage.style.textAlign = 'center';
emptyMessage.textContent = 'No generation history yet';
historyList.appendChild(emptyMessage);
return;
}
generationHistory.forEach((item, index) => {
const historyItemDiv = document.createElement('div');
historyItemDiv.className = 'history-item';
const promptDiv = document.createElement('div');
promptDiv.className = 'history-item-prompt';
promptDiv.textContent = item.prompt;
historyItemDiv.appendChild(promptDiv);
const modelDiv = document.createElement('div');
modelDiv.className = 'history-item-model';
modelDiv.textContent = item.model;
historyItemDiv.appendChild(modelDiv);
const timeDiv = document.createElement('div');
timeDiv.className = 'history-item-time';
timeDiv.textContent = item.timestamp;
historyItemDiv.appendChild(timeDiv);
if (item.memoryUsage) {
const memoryDiv = document.createElement('div');
memoryDiv.className = 'history-item-memory';
memoryDiv.textContent = `Memory: RSS +${item.memoryUsage.rssDiff}MB, Heap +${item.memoryUsage.heapUsedDiff}MB`;
historyItemDiv.appendChild(memoryDiv);
}
historyItemDiv.addEventListener('click', () => {
promptInput.value = item.prompt;
modelSelect.value = item.model;
displayResult({ result: item.result }, item.prompt, item.model);
});
historyList.appendChild(historyItemDiv);
});
}
// Allow Enter key to submit (with Shift+Enter for new line)
// Ignore Enter key during Japanese IME composition
promptInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault();
generateImage();
}
});
// Made with Bob
- server.js: The
server.js file has been completely refactored into a modular, well-documented architecture using helper functions. To improve observability, we integrated comprehensive memory tracking (monitoring RSS, heap, and external usage), alongside centralized configuration constants for easier maintenance. Finally, the service is now more resilient and easier to troubleshoot thanks to enhanced error handling, robust response parsing, and the addition of conditional debug logging.
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;
const OLLAMA_API_URL = 'http://localhost:11434';
const MAX_CONTENT_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
const IMAGE_GENERATION_TIMEOUT_MS = 180000; // 3 minutes timeout for image generation
const OLLAMA_REQUEST_CONFIG = {
timeout: IMAGE_GENERATION_TIMEOUT_MS,
maxContentLength: MAX_CONTENT_SIZE_BYTES,
maxBodyLength: MAX_CONTENT_SIZE_BYTES
};
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Validate request body for image generation
*/
function validateRequest(body) {
const { prompt, model } = body;
if (!prompt || !model) {
return { valid: false, error: 'Prompt and model are required' };
}
return { valid: true, prompt, model };
}
/**
* Measure current memory usage
*/
function measureMemory() {
return process.memoryUsage();
}
/**
* Calculate and log memory usage difference
*/
function logMemoryUsage(memBefore, memAfter) {
const memDiff = {
rss: ((memAfter.rss - memBefore.rss) / 1024 / 1024).toFixed(2),
heapTotal: ((memAfter.heapTotal - memBefore.heapTotal) / 1024 / 1024).toFixed(2),
heapUsed: ((memAfter.heapUsed - memBefore.heapUsed) / 1024 / 1024).toFixed(2),
external: ((memAfter.external - memBefore.external) / 1024 / 1024).toFixed(2)
};
debugLog('=== Memory Usage Before Generation ===');
debugLog(`RSS: ${(memBefore.rss / 1024 / 1024).toFixed(2)} MB`);
debugLog(`Heap Total: ${(memBefore.heapTotal / 1024 / 1024).toFixed(2)} MB`);
debugLog(`Heap Used: ${(memBefore.heapUsed / 1024 / 1024).toFixed(2)} MB`);
debugLog(`External: ${(memBefore.external / 1024 / 1024).toFixed(2)} MB`);
debugLog('=== Memory Usage After Generation ===');
debugLog(`RSS: ${(memAfter.rss / 1024 / 1024).toFixed(2)} MB (Δ ${memDiff.rss} MB)`);
debugLog(`Heap Total: ${(memAfter.heapTotal / 1024 / 1024).toFixed(2)} MB (Δ ${memDiff.heapTotal} MB)`);
debugLog(`Heap Used: ${(memAfter.heapUsed / 1024 / 1024).toFixed(2)} MB (Δ ${memDiff.heapUsed} MB)`);
debugLog(`External: ${(memAfter.external / 1024 / 1024).toFixed(2)} MB (Δ ${memDiff.external} MB)`);
return memDiff;
}
/**
* Track memory usage for a generation operation
*/
function trackMemoryUsage(beforeMem) {
const afterMem = measureMemory();
return logMemoryUsage(beforeMem, afterMem);
}
/**
* Conditional debug logging
*/
function debugLog(...args) {
if (process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true') {
console.log(...args);
}
}
/**
* Extract base64 image data from various response formats
*/
function extractImageData(data) {
if (data.image) return `data:image/png;base64,${data.image}`;
if (data.images?.[0]) return `data:image/png;base64,${data.images[0]}`;
if (Buffer.isBuffer(data)) return `data:image/png;base64,${data.toString('base64')}`;
return null;
}
/**
* Parse NDJSON string and return last line as JSON
*/
function parseNDJSON(data) {
const lines = data.trim().split('\n');
return JSON.parse(lines[lines.length - 1]);
}
/**
* Handle string response (NDJSON format)
*/
function parseStringResponse(data, log) {
log('Response is a string, length:', data.length);
try {
const parsed = parseNDJSON(data);
log('Parsed response keys:', Object.keys(parsed));
const imageData = extractImageData(parsed);
if (imageData) {
log('✓ Found image in parsed response');
return { imageData, ollamaResponse: '' };
}
if (parsed.response) {
log('Found response field, length:', parsed.response.length);
return { imageData: null, ollamaResponse: parsed.response };
}
} catch (e) {
log('Failed to parse as JSON:', e.message);
return { imageData: null, ollamaResponse: data };
}
return { imageData: null, ollamaResponse: '' };
}
/**
* Handle object/Buffer response
*/
function parseObjectResponse(data, log) {
const imageData = extractImageData(data);
if (imageData) {
log('✓ Found image in response');
return { imageData, ollamaResponse: '' };
}
if (data.response) {
log('Response length:', data.response.length);
return { imageData: null, ollamaResponse: data.response };
}
return { imageData: null, ollamaResponse: '' };
}
/**
* Parse Ollama API response and extract image/text data
*
* CRITICAL: Ollama returns newline-delimited JSON (NDJSON) for image generation
* Format: Each line is a separate JSON object
* Example:
* {"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..."}
*
* The LAST line contains the complete image in the 'image' field (singular, not 'images')
*/
function parseOllamaResponse(data) {
debugLog('Ollama API response received');
debugLog('Response data type:', typeof data);
if (typeof data === 'object' && !Buffer.isBuffer(data)) {
debugLog('Response data keys:', Object.keys(data));
}
if (typeof data === 'string') {
return parseStringResponse(data, debugLog);
}
if (typeof data === 'object' || Buffer.isBuffer(data)) {
return parseObjectResponse(data, debugLog);
}
debugLog('✗ No image or response data found');
return { imageData: null, ollamaResponse: '' };
}
/**
* Build API response object
*/
function buildResponse({ imageData, ollamaResponse, model, memDiff, responseData }) {
return {
success: true,
result: imageData || ollamaResponse || 'No image generated',
model,
hasImage: !!imageData,
memoryUsage: {
rssDiff: memDiff.rss,
heapUsedDiff: memDiff.heapUsed
},
debug: {
responseType: typeof responseData,
isBuffer: Buffer.isBuffer(responseData),
responseLength: ollamaResponse.length,
hasImages: !!(responseData.images)
}
};
}
/**
* Generate image using Ollama API
*/
async function generateImageWithOllama(model, prompt) {
return axios.post(
`${OLLAMA_API_URL}/api/generate`,
{ model, prompt, stream: false },
OLLAMA_REQUEST_CONFIG
);
}
/**
* Handle error responses
*/
function handleError(error, res) {
console.error('Error generating image:', error.message);
if (error.response?.data) {
console.error('Error details:', error.response.data);
}
if (error.stack) {
console.error('Stack trace:', error.stack);
}
res.status(500).json({
error: 'Failed to generate image',
details: error.message,
response: error.response?.data || ''
});
}
// ============================================================================
// API Endpoints
// ============================================================================
/**
* Endpoint to generate image using Ollama HTTP API
* This endpoint handles image generation requests from the frontend
*/
app.post('/api/generate', async (req, res) => {
const { valid, error, prompt, model } = validateRequest(req.body);
if (!valid) {
return res.status(400).json({ error });
}
try {
const memBefore = measureMemory();
debugLog(`Generating image with model: ${model}`);
const response = await generateImageWithOllama(model, prompt);
const { imageData, ollamaResponse } = parseOllamaResponse(response.data);
const memDiff = trackMemoryUsage(memBefore);
res.json(buildResponse({
imageData,
ollamaResponse,
model,
memDiff,
responseData: response.data
}));
} catch (error) {
handleError(error, res);
}
});
// Endpoint to check available mod