UNPKG

@mfukushim/map-traveler-mcp

Version:
710 lines (709 loc) 37.5 kB
/*! map-traveler-mcp | MIT License | https://github.com/mfukushim/map-traveler-mcp */ import { createRequire as _createRequire } from "module"; const __require = _createRequire(import.meta.url); import { Effect, Schedule, Option, Schema } from "effect"; const sharp = __require("sharp"); import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform"; import dayjs from "dayjs"; import FormData from 'form-data'; import { Jimp } from "jimp"; import { PixAIClient } from '@pixai-art/client'; import 'dotenv/config'; import { logSync, McpLogService, McpLogServiceLive } from "./McpLogService.js"; import { __pwd, DbService, env, scriptTables, } from "./DbService.js"; import WebSocket from 'ws'; import * as path from "path"; import * as os from "node:os"; import * as fs from "node:fs"; import { execSync } from "node:child_process"; import { defaultAvatarId } from "./RunnerService.js"; import { sendProgressNotification } from "./McpService.js"; import { comfy_params, comfy_url, comfy_workflow_i2i, comfy_workflow_t2i, fixed_model_prompt, image_width, pixAi_key, pixAi_modelId, sd_key, ServerLog } from "./EnvUtils.js"; export const defaultBaseCharPrompt = 'depth of field, cinematic composition, masterpiece, best quality,looking at viewer,(solo:1.1),(1 girl:1.1),loli,school uniform,blue skirt,long socks,black pixie cut'; export const widthOut = Number.parseInt(image_width || "512") || 512; export const heightOut = Math.floor(widthOut * 0.75); let recentImage; const sdKey = sd_key || ''; const defaultPixAiModelId = '1648918127446573124'; const pixAiClient = new PixAIClient({ apiKey: pixAi_key || '', webSocketImpl: WebSocket }); export class ImageService extends Effect.Service()("traveler/ImageService", { accessors: true, effect: Effect.gen(function* () { const getBasePrompt = (avatarId) => { if (fixed_model_prompt) { return Effect.succeed(fixed_model_prompt); } return DbService.getAvatarModel(avatarId).pipe(Effect.andThen(a => a.baseCharPrompt + ',anime'), Effect.orElseSucceed(() => defaultBaseCharPrompt)); }; const progress = (total = 1, progress = 0) => { logSync('progress:', env.progressToken, total, progress); if (env.progressToken === undefined) { return Effect.void; } return sendProgressNotification(env.progressToken || '', total, progress).pipe(Effect.repeat(Schedule.repeatForever.pipe(Schedule.intersect(Schedule.spaced("15 seconds"))))); }; function sdMakeTextToImage(prompt, opt) { return Effect.gen(function* () { const param = prompt.split(',').reduce((p, c) => { const match = c.trim().match(/\((\w+):([0-9.]+)\)/); if (match) { p.list.push({ text: p.buf, weight: 1 }); p.list.push({ text: match[1].trim(), weight: Number.parseFloat(match[2]) }); return p; } else { return { buf: p.buf ? `${p.buf},${c}` : c, list: p.list }; } }, { list: [], buf: '' }); if (param.buf) { param.list.push({ text: param.buf, weight: 1 }); } yield* McpLogService.logTrace(`sdMakeTextToImage:${JSON.stringify(param.list)}`); if (param.list.length > 10) { return yield* Effect.fail(new Error('param weight too long')); } const client = yield* HttpClient.HttpClient; return yield* HttpClientRequest.post(`https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image`).pipe(HttpClientRequest.setHeaders({ Authorization: `Bearer ${sdKey}`, Accept: "application/json", }), HttpClientRequest.bodyJson({ cfg_scale: opt?.cfg_scale || 7, height: opt?.height || 1024, width: opt?.width || 1024, sampler: opt?.sampler || "K_DPM_2_ANCESTRAL", samples: opt?.samples || 1, steps: opt?.steps || 30, text_prompts: param.list }), Effect.flatMap(client.execute), Effect.flatMap(a => a.json), Effect.tap(a => McpLogService.logTrace('sdMakeTextToImage:' + JSON.stringify(a).slice(0, 200))), Effect.andThen(a => a), Effect.flatMap(a => { if (!a.artifacts || a.artifacts.length === 0 || a.artifacts.some(b => b.finishReason !== 'SUCCESS')) { return Effect.fail(new Error(`fail sd:${opt?.width},${opt?.height},` + JSON.stringify(a))); } return Effect.tryPromise(() => sharp(Buffer.from(a.artifacts[0].base64, 'base64')).resize({ width: 512, height: 512 }).png().toBuffer()); }), Effect.retry(Schedule.recurs(1).pipe(Schedule.intersect(Schedule.spaced("5 seconds")))), Effect.mapError(e => new Error(`sdMakeTextToImage error:${e}`)), Effect.scoped); }).pipe(Effect.provide(FetchHttpClient.layer)); } function sdMakeImageToImage(prompt, inImage, opt) { return Effect.tryPromise(() => sharp(inImage).resize({ width: opt?.width || 1024, height: opt?.height || 1024 }).png().toBuffer()).pipe(Effect.andThen(a => Effect.tryPromise({ try: () => { const formData = new FormData(); formData.append('init_image', a); formData.append('init_image_mode', 'IMAGE_STRENGTH'); formData.append('image_strength', 0.35); formData.append('text_prompts[0][text]', prompt); formData.append('cfg_scale', 7); formData.append('samples', 1); formData.append('steps', 30); return fetch(`https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/image-to-image`, { method: 'POST', headers: { ...formData.getHeaders(), Accept: 'application/json', Authorization: `Bearer ${sdKey}`, }, body: formData.getBuffer(), }); }, catch: error => new Error(`${error}`) })), Effect.andThen(a => a.json()), Effect.tap(a => McpLogService.logTrace('sdMakeImageToImage:' + JSON.stringify(a).slice(0, 200))), Effect.andThen(a => a), Effect.flatMap(a => { if (!a.artifacts || a.artifacts.length === 0 || a.artifacts.some(b => b.finishReason !== 'SUCCESS')) { return Effect.fail(new Error(`fail sd:${opt?.width},${opt?.height},` + JSON.stringify(a))); } return Effect.succeed(Buffer.from(a.artifacts[0].base64, 'base64')); })); } function sdMakeImage(prompt, inImage, opt) { if (!sdKey) { return Effect.fail(new Error('no key')); } return inImage ? sdMakeImageToImage(prompt, inImage, opt) : sdMakeTextToImage(prompt, opt); } function pixAiMakeImage(prompt, inImage, opt) { if (!pixAi_key) { return Effect.fail(new Error('no key')); } return Effect.retry(Effect.gen(function* () { let mediaId; if (inImage) { const blob = new Blob([inImage], { type: 'image/jpeg' }); const file = new File([blob], "image.jpg", { type: 'image/jpeg' }); mediaId = yield* Effect.tryPromise({ try: () => pixAiClient.uploadMedia(file), catch: error => new Error(`uploadMedia fail:${error}`) }).pipe(Effect.andThen(a1 => { return !a1.mediaId ? Effect.fail(new Error(`uploadMedia fail`)) : Effect.succeed(a1.mediaId); })); } return mediaId; }).pipe(Effect.tap(a => McpLogService.logTrace(`uploadMedia ${a}`)), Effect.tapError(a => McpLogService.logError(`uploadMedia error ${a}`)), Effect.andThen(a => { const body = a ? { prompts: prompt, modelId: pixAi_modelId || defaultPixAiModelId, width: opt?.width || 512, height: opt?.height || 512, mediaId: a } : { prompts: prompt, modelId: pixAi_modelId || defaultPixAiModelId, width: opt?.width || 512, height: opt?.height || 512, }; return Effect.tryPromise({ try: () => pixAiClient.generateImage(body), catch: error => new Error(`generateImage fail:${error}`) }).pipe(Effect.timeout('1 minute')); }), Effect.tap(a => McpLogService.logTrace(`generateImage ${a.status}`)), Effect.tapError(a => McpLogService.logError(`generateImage ${a}`)), Effect.andThen(task => { return Effect.tryPromise({ try: () => pixAiClient.getMediaFromTask(task), catch: error => new Error(`getMediaFromTask fail:${error}`) }); }), Effect.tap(() => McpLogService.logTrace(`getMediaFromTask`)), Effect.andThen(media => { if (!media) return Effect.fail(new Error(`media fail1:${media}`)); if (Array.isArray(media)) return Effect.fail(new Error(`media fail2:${media}`)); return Effect.tryPromise({ try: () => pixAiClient.downloadMedia(media), catch: error => new Error(`downloadMedia fail:${error}`) }); }), Effect.andThen(a => Buffer.from(a)), Effect.tap(a => McpLogService.logTrace(`downloadMedia out:${a.length}`)), Effect.tapError(a => McpLogService.logError(`downloadMedia err:${a}`))), Schedule.recurs(4).pipe(Schedule.intersect(Schedule.spaced("10 seconds")))).pipe(Effect.provide([McpLogServiceLive])); } function makeHotelPict(selectGen, hour, append, localDebug = false) { return Effect.gen(function* () { if (!env.anyImageAiExist || env.isPractice) { return yield* Effect.async((resume) => fs.readFile(path.join(__pwd, 'assets/hotelPict.png'), (err, data) => { if (err) { resume(Effect.fail(err)); } resume(Effect.succeed(data)); })).pipe(Effect.andThen(a => Buffer.from(a))); } const baseCharPrompt = yield* getBasePrompt(defaultAvatarId); let prompt = baseCharPrompt + ','; if (hour < 6 || hour >= 19) { prompt += 'hotel room,desk,drink,laptop computer,talking to computer,sitting,window,night,(pyjamas:1.3)'; } else if (hour < 11) { prompt += 'hotel room,desk,drink,laptop computer,talking to computer,sitting,window,morning'; } else if (hour < 16) { prompt += 'cafe terrace,outdoor dining table,outdoor bistro chair,drink,laptop computer,talking to computer,sitting,noon'; } else { prompt += 'cafe terrace,outdoor dining table,outdoor bistro chair,drink,laptop computer,talking to computer,sitting,evening'; } if (append) { prompt += `,${append}`; } return yield* selectImageGenerator(selectGen, prompt).pipe(Effect.tap(a => { recentImage = a; if (localDebug) { return Effect.async((resume) => fs.writeFile('tools/test/hotelPict.png', a, err => { if (err) { resume(Effect.fail(err)); } resume(Effect.void); })); } }), Effect.andThen(a => Effect.tryPromise(() => sharp(a).resize({ width: widthOut, height: heightOut }).png().toBuffer()))); }); } function makeEtcTripImage(selectGen, vehiclePrompt, timeZoneId, localDebug = false) { return Effect.gen(function* () { const appendPrompts = []; appendPrompts.push(vehiclePrompt); const now = dayjs(); const hour = now.tz(timeZoneId).hour(); if (hour < 6 || hour >= 19) { appendPrompts.push('night'); } else if (hour < 11) { appendPrompts.push('morning'); } else if (hour < 16) { appendPrompts.push('noon'); } else { appendPrompts.push('evening'); } const appendPrompt = appendPrompts.join(','); const baseCharPrompt = yield* getBasePrompt(defaultAvatarId); const prompt = `${baseCharPrompt},${appendPrompt}`; return yield* selectImageGenerator(selectGen, prompt).pipe(Effect.tap(a => { recentImage = a; if (localDebug) { return Effect.async((resume) => fs.writeFile('tools/test/etcPict.png', a, err => { if (err) { resume(Effect.fail(err)); } resume(Effect.void); })); } }), Effect.andThen(a => Effect.tryPromise(() => sharp(a).resize({ width: widthOut, height: heightOut }).png().toBuffer()))); }); } const generatePrompt = (baseCharPrompt, simple = false, withAbort = false) => { return Effect.gen(function* () { const backPrompt = (simple || withAbort) ? ',(simple background:1.2)' : ',road'; const basePrompt = baseCharPrompt + backPrompt; const prompt = []; const bodyRnd = Math.random(); const faceRnd = Math.random(); const poseRnd = Math.random(); if (simple || withAbort) { if (withAbort) { prompt.push('(upper body:1.3)'); } else { prompt.push(Math.random() < 0.3 ? '(upper body:1.3)' : '(cowboy shot:1.3)'); const fromRnd = Math.random(); if (fromRnd < 0.3) { prompt.push('from back'); } else if (fromRnd < 0.4) { prompt.push('from side'); } } } else { if (bodyRnd < 0.3) { prompt.push('full body'); } else if (bodyRnd < 0.6) { prompt.push('cowboy shot'); } else { prompt.push('upper body'); } } if (withAbort) { prompt.push('surprised,(calling phone:1.2),(holding smartphone to ear:1.2)'); } else { if (faceRnd < 0.1) { prompt.push(':D'); } else if (faceRnd < 0.3) { prompt.push('smiling'); } else if (faceRnd < 0.4) { prompt.push('laughing'); } else if (faceRnd < 0.6) { prompt.push('surprised'); } else if (faceRnd < 0.8) { prompt.push('grin'); } if (poseRnd < 0.2) { prompt.push('looking around'); } else if (poseRnd < 0.6) { prompt.push('walking,looking sky'); } else if (poseRnd < 0.7) { prompt.push('jumping'); } else { prompt.push('standing'); const faceRnd = Math.random(); if (faceRnd < 0.2) { prompt.push('(salute:1.3),posing,(tilt my head:1.2)'); } else if (faceRnd < 0.5) { prompt.push('posing,(tilt my head:1.2)'); } } } const ap = prompt.join(','); return { prompt: `${basePrompt},${ap}`, append: ap }; }); }; const selectImageGenerator = (generatorId, prompt, inImage, opt) => { switch (generatorId) { case 'pixAi': return pixAiMakeImage(prompt, inImage, opt); case 'comfyUi': { const optList = comfy_params ? comfy_params.split(',').map(a => { const b = a.split('='); const val = b[1].includes("'") ? b[1].replaceAll("'", "") : Number.parseFloat(b[1]); return [b[0], val]; }) : []; const optC = { ...Object.fromEntries(optList), ...opt }; if (!optC.width) { optC.width = 1024; } if (!optC.height) { optC.height = 1024; } return comfyApiMakeImage(prompt, inImage, optC); } default: return sdMakeImage(prompt, inImage, opt); } }; function checkPersonImage(avatarImage, windowSize) { return Effect.gen(function* () { const image = yield* Effect.tryPromise(() => Jimp.read(avatarImage)); let minX = Number.MAX_SAFE_INTEGER; let minY = Number.MAX_SAFE_INTEGER; let maxX = Number.MIN_SAFE_INTEGER; let maxY = Number.MIN_SAFE_INTEGER; let alphaCount = 0; image.scan(0, 0, image.width, image.height, (x, y, idx) => { const alpha = image.bitmap.data[idx + 3]; if (alpha > 127) { alphaCount++; minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); } }); const alphaNum = { alphaCount, rect: { x: minX, y: minY, w: maxX - minX, h: maxY - minY } }; const number = alphaNum.alphaCount / (windowSize.w * windowSize.h); return { alphaNum, number }; }); } const rembgCli = (sdImage) => { return Effect.gen(function* () { const tempPath = os.tmpdir(); const tempIn = path.join(tempPath, `tr-${crypto.randomUUID()}.png`); const tempOut = path.join(tempPath, `tr-${crypto.randomUUID()}.png`); fs.writeFileSync(tempIn, sdImage); let rembgPath; if (env.rembgPath) { rembgPath = env.rembgPath; } else { yield* Effect.fail(new Error('rembgPath not set')); } yield* Effect.addFinalizer(() => McpLogService.logTrace(`rembg finalizer ${tempPath}`).pipe(Effect.andThen(() => { try { fs.unlinkSync(tempIn); } catch (e) { } try { fs.unlinkSync(tempOut); } catch (e) { } }))); try { execSync(`${rembgPath} i ${tempIn} ${tempOut}`); } catch (e) { yield* Effect.fail(new Error(`rembg fail ${e}`)); } return fs.readFileSync(tempOut); }).pipe(Effect.scoped); }; const rembgService = (sdImage) => { return Effect.gen(function* () { yield* McpLogService.logTrace('in rembgService'); if (!env.remBgUrl) { return yield* Effect.fail(new Error('no rembg url')); } return yield* Effect.tryPromise({ try: () => { const formData = new FormData(); formData.append("file", sdImage, { filename: "input.png", contentType: "image/png" }); return fetch(`${env.remBgUrl}/api/remove`, { method: 'POST', headers: { ...formData.getHeaders(), }, body: formData.getBuffer(), }); }, catch: error => new Error(`${error}`) }).pipe(Effect.scoped, Effect.andThen(a => Effect.tryPromise(() => a.arrayBuffer())), Effect.tap(a => McpLogService.logTrace('rembgService out:', a.byteLength, a.toString())), Effect.tap(a => { if (a && a.byteLength) { return Effect.succeed(a); } return Effect.fail(new Error()); }), Effect.tapError(e => McpLogService.logTrace('rembgService err:', JSON.stringify(e))), Effect.retry(Schedule.recurs(1).pipe(Schedule.intersect(Schedule.spaced("2 seconds")))), Effect.andThen(a => Buffer.from(a))); }); }; const initRembgService = () => { return Effect.gen(function* () { yield* McpLogService.logTrace('initRembgService in'); if (!env.remBgUrl) { return yield* Effect.fail(new Error('no rembg url')); } const client = yield* HttpClient.HttpClient; const req = HttpClientRequest.get(`${env.remBgUrl}/api`); const response = yield* client.execute(req); return yield* response.text; }).pipe(Effect.scoped, Effect.retry({ times: 2 }), Effect.tap(a => McpLogService.logTrace('initRembgService', a)), Effect.tapError(e => McpLogService.logTrace('initRembgService err:', e.toString())), Effect.andThen(() => ({})), Effect.orElseSucceed(() => ({}))); }; const rembg = (sdImage) => { return env.remBgUrl ? rembgService(sdImage) : rembgCli(sdImage); }; function makeRunnerImageV3(basePhoto, selectGen, withAbort = false, opt = { bodyAreaRatio: 0.042, bodyHWRatio: 1.5, bodyWindowRatioW: 0.5, bodyWindowRatioH: 0.75 }, localDebug = false) { return Effect.gen(function* () { if (!env.rembgPath && !env.remBgUrl) { return { buf: yield* Effect.tryPromise(() => sharp(basePhoto).resize({ width: widthOut, height: heightOut }).png().toBuffer()), shiftX: 0, shiftY: 0, fit: false, append: '' }; } if (env.remBgUrl) { yield* initRembgService(); } const outSize = { w: 1600, h: 1000 }; const innerSize = { w: 1600, h: 1600 }; const windowSize = selectGen === 'sd' ? { w: 832, h: 1216 } : { w: Math.floor((innerSize.w * (opt?.bodyWindowRatioW || 0.5)) / 64) * 64, h: Math.floor((innerSize.h * (opt?.bodyWindowRatioH || 0.75)) / 64) * 64 }; const sideBias = opt?.sideBias || false; const cutPos = sideBias ? (Math.random() < 0.5 ? Math.random() * 0.3 : 0.7 + Math.random() * 0.3) : Math.random(); const shiftX = Math.floor((innerSize.w - windowSize.w) * cutPos); const innerImage = yield* Effect.tryPromise(() => sharp(basePhoto).resize({ width: innerSize.w, height: innerSize.h, fit: "fill" }).toBuffer()); const clopImage = yield* Effect.tryPromise(() => sharp(innerImage).extract({ left: shiftX, top: innerSize.h - windowSize.h, width: windowSize.w, height: windowSize.h }).toBuffer()); if (localDebug) { fs.writeFileSync('tools/test/testOutInClop.png', clopImage); } const retryMax = 3; let retry = retryMax; const fixedThreshold = 2; let isFixedBody = false; let appendPrompt; const avatarImage = yield* Effect.gen(function* () { const baseCharPrompt = yield* getBasePrompt(defaultAvatarId); const { prompt, append } = yield* generatePrompt(baseCharPrompt, retry < fixedThreshold, withAbort); appendPrompt = append; retry--; if (retry < fixedThreshold) { isFixedBody = true; yield* McpLogService.logTrace(`bast shot:${retry}`); return yield* selectImageGenerator(selectGen, prompt, undefined, { width: windowSize.w, height: windowSize.h }); } else { isFixedBody = false; yield* McpLogService.logTrace(`i2i:${retry}`); return yield* selectImageGenerator(selectGen, prompt, clopImage, { width: windowSize.w, height: windowSize.h, logTotal: retryMax, logProgress: retryMax - retry }); } }).pipe(Effect.tap(sdImage => localDebug && fs.writeFileSync('tools/test/testOutGen.png', sdImage)), Effect.andThen(sdImage => rembg(sdImage)), Effect.tap(avatarImage => localDebug && fs.writeFileSync('tools/test/testOutRmBg.png', avatarImage)), Effect.tap(avatarImage => { if (ServerLog && ServerLog.includes('trace')) { fs.writeFileSync(path.join(os.tmpdir(), `trd-${crypto.randomUUID()}.png`), avatarImage, { flag: "w" }); } }), Effect.tap(avatarImage => { const bodyAreaRatio = opt?.bodyAreaRatio || 0.042; const bodyHWRatio = opt?.bodyHWRatio || 2; return checkPersonImage(avatarImage, windowSize).pipe(Effect.tap(a => McpLogService.logTrace(`check runner image:${retry},${JSON.stringify(a)},${a.number}${a.number > bodyAreaRatio ? '>' : '<'}${bodyAreaRatio},${a.alphaNum.rect.h / a.alphaNum.rect.w}${a.alphaNum.rect.h / a.alphaNum.rect.w > bodyHWRatio ? '>' : '<'}${bodyHWRatio}`)), Effect.andThen(a => { return a.number > bodyAreaRatio && a.alphaNum.rect.h / a.alphaNum.rect.w > bodyHWRatio ? Effect.succeed(avatarImage) : Effect.fail(new Error('avatar fail')); })); }), Effect.retry(Schedule.recurs(2).pipe(Schedule.intersect(Schedule.spaced("5 seconds"))))); const stayImage = yield* Effect.tryPromise(() => { return sharp(innerImage).composite([{ input: avatarImage, left: shiftX, top: innerSize.h - windowSize.h }]).toBuffer(); }).pipe(Effect.andThen(a => Effect.tryPromise(() => sharp(a).extract({ left: (innerSize.w - outSize.w) / 2, top: (innerSize.h - outSize.h) / 2, width: outSize.w, height: outSize.h }).resize({ width: 512, height: 512 }).png().toBuffer()))); recentImage = stayImage; yield* McpLogService.logTrace(`stayImage:${recentImage?.length}`); return { buf: stayImage, shiftX, shiftY: innerSize.h - windowSize.h, fit: !isFixedBody, append: appendPrompt }; }); } function getRecentImageAndClear() { const image = recentImage; recentImage = undefined; return image; } function mergeParams(baseScript, map, params) { let scr = JSON.stringify(baseScript); Object.keys(params).forEach(key => { const val = params[key]; const reg = new RegExp(`"${key}"`, "g"); scr = scr.replaceAll(reg, typeof val === "number" ? val.toString() : `"${val.toString()}"`); }); return JSON.parse(scr); } function comfyUploadImage(inImage, opt) { const nowMs = dayjs().valueOf(); const fileName = `trv${nowMs}`; return Effect.tryPromise(() => sharp(inImage).resize({ width: opt?.width || 1024, height: opt?.height || 1024 }).png().toBuffer()).pipe(Effect.andThen(a => Effect.tryPromise({ try: () => { const formData = new FormData(); formData.append('image', a, { filename: fileName }); return fetch(`${comfy_url}/upload/image`, { method: 'POST', headers: { ...formData.getHeaders(), Accept: 'application/json', }, body: formData.getBuffer(), }); }, catch: error => new Error(`${error}`) })), Effect.andThen(a => a.json()), Effect.andThen(a => a), Effect.andThen(a => a.name), Effect.tapError(cause => Effect.logError(cause)), Effect.retry(Schedule.recurs(1).pipe(Schedule.intersect(Schedule.spaced("3 seconds"))))); } function comfyUpExecPrompt(script) { return Effect.gen(function* () { const client = yield* HttpClient.HttpClient; return yield* HttpClientRequest.post(`${comfy_url}/prompt`).pipe(HttpClientRequest.setHeaders({ Accept: "application/json", }), HttpClientRequest.bodyJson({ prompt: script, }), Effect.flatMap(client.execute), Effect.flatMap(a => a.json), Effect.andThen((a) => { if (a.error) { return Effect.fail(new Error(`ComfyUI error:${JSON.stringify(a.node_errors)}`)); } return Effect.succeed(a); }), Effect.tapError(McpLogService.logError), Effect.tap(a => McpLogService.logTrace(a)), Effect.retry(Schedule.recurs(1).pipe(Schedule.intersect(Schedule.spaced("5 seconds")))), Effect.scoped); }); } function downloadOutput(prompt_id, needKeyNum = 1) { const sc = Schema.Record({ key: Schema.String, value: Schema.Struct({ outputs: Schema.Record({ key: Schema.String, value: Schema.Struct({ images: Schema.Array(Schema.Struct({ filename: Schema.String, subfolder: Schema.String, type: Schema.String })) }) }) }) }); return HttpClient.get(`${comfy_url}/history`).pipe(Effect.andThen((response) => HttpClientResponse.schemaBodyJson(sc)(response)), Effect.scoped, Effect.provide(FetchHttpClient.layer), Effect.andThen((a1) => { const prOut = a1[prompt_id]; if (!prOut) { return Effect.fail(new Error('wait not ready')); } const outputs = prOut.outputs; const keys = Object.keys(outputs); return Effect.forEach(keys, a => { const imageList = outputs[a].images; return Effect.forEach(imageList, a2 => HttpClient.get(`${comfy_url}/view`, { urlParams: { filename: a2.filename, subfolder: a2.subfolder, type: a2.type } }).pipe(Effect.andThen((response) => response.arrayBuffer), Effect.scoped, Effect.provide(FetchHttpClient.layer))); }); }), Effect.retry(Schedule.recurs(44).pipe(Schedule.intersect(Schedule.spaced("10 seconds")))), Effect.andThen(a => a.flat())); } function comfyApiMakeImage(prompt, inImage, params) { if (!comfy_url) { return Effect.fail(new Error('no comfy_url')); } return Effect.gen(function* () { const logTotal = params?.logTotal; const logProgress = params?.logProgress; yield* Effect.fork(progress(logTotal || 1, logProgress || 0)); const uploadFileName = inImage ? yield* comfyUploadImage(inImage, params).pipe(Effect.andThen(a => Effect.succeedSome(a))) : Option.none(); const scriptName = inImage ? (comfy_workflow_i2i ? 'i2i' : 'i2i_sample') : (comfy_workflow_t2i ? 't2i' : 't2i_sample'); const sdT2i = scriptTables.get(scriptName); if (!sdT2i) { return yield* Effect.fail(new Error('comfyApiMakeImage no script table')); } const mappedRecord = params ? Object.fromEntries(Object.entries(params).map(([key, value]) => [`%${key}`, value])) : {}; const modelParams = { "%seed": params?.seed && params?.seed >= 0 ? params.seed : Math.floor(Math.random() * 999999999999999), "%steps": params?.steps || 20, "%cfg": params?.cfg || 6, "%sampler_name": params?.sampler_name || 'euler', "%scheduler": params?.scheduler || 'normal', "%denoise": params?.denoise || 0.7, "%ckpt_name": params?.ckpt_name || 'v1-5-pruned-emaonly-fp16.safetensors', "%prompt": prompt, "%negative_prompt": params?.negative_prompt || 'nsfw, text, watermark', "%width": params?.width || 1024, "%height": params?.height || 1024 }; const outParam = { ...mappedRecord, ...modelParams }; if (Option.isSome(uploadFileName)) { outParam["%uploadFileName"] = uploadFileName.value; } const script = mergeParams(sdT2i.script, sdT2i.nodeNameToId, outParam); const ret = yield* comfyUpExecPrompt(script); return yield* downloadOutput(ret.prompt_id, 1).pipe(Effect.tap(a => a.length !== 1 && a.length !== 2 && Effect.fail(new Error('download fail'))), Effect.andThen(a => Buffer.from(a[0]))); }); } function shrinkImage(image) { return Effect.tryPromise(() => sharp(image).resize({ width: widthOut, height: heightOut }).png().toBuffer()); } return { getRecentImageAndClear, makeHotelPict, makeEtcTripImage, makeRunnerImageV3, selectImageGenerator, generatePrompt, getBasePrompt, comfyApiMakeImage, rembgService, shrinkImage, }; }), dependencies: [McpLogServiceLive] }) { } export const ImageServiceLive = ImageService.Default;