From 63998fdeef1167bdebc5cdd51ea6e8feec2e1beb Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 7 Jul 2026 04:31:58 +0000 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=B4=E6=97=B6BgFilter?= =?UTF-8?q?=E6=8E=A2=E6=B4=BB=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除一次性 BgFilter live probe 脚本。 更新决策日志,避免引用已删除的探活命令。 --- .../shared-memory/decision-log.md | 4 +- scripts/check-bgfilter-live.mjs | 273 ------------------ 2 files changed, 2 insertions(+), 275 deletions(-) delete mode 100644 scripts/check-bgfilter-live.mjs diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index f09ef1a42..da6a33eae 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -34,10 +34,10 @@ ## 2026-07-05 BgFilter 失败时本地纯色去背兜底 -- 背景:`scripts/check-bgfilter-live.mjs --width=2048 --height=2048` 可稳定复现 BgFilter 对 2K 输入返回 `HTTP 500 {"detail":"inference failed"}`,浏览器生成链路会因此收到“BgFilter 服务返回非成功状态”。 +- 背景:曾用一次性 BgFilter live probe 稳定复现 BgFilter 对 2K 输入返回 `HTTP 500 {"detail":"inference failed"}`,浏览器生成链路会因此收到“BgFilter 服务返回非成功状态”。 - 决策:角色形象生成、图标 spritesheet 生成和 UI 设计图素材提取仍优先调用独立 BgFilter;若 BgFilter 请求失败、返回非成功状态、返回空图片或非法图片,api-server 记录 warning 后使用本地 `editor_green_screen` 按解析后的纯色背景执行确定性去背兜底,不中断生成。手动任意图片去背景仍只走独立 BiRefNet BFF,不使用该兜底。 - 影响范围:`server-rs/crates/api-server/src/editor_project.rs`、BgFilter 运维排障、图片画布生成后处理。 -- 验证方式:运行 `node scripts/check-bgfilter-live.mjs --width=2048 --height=2048 --timeout-ms=300000` 复现远端 500;运行 `cargo test -p api-server editor_canvas_screen_background_generation_uses_bgfilter_postprocess editor_green_screen --manifest-path server-rs/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。 +- 验证方式:运行 `cargo test -p api-server editor_canvas_screen_background_generation_uses_bgfilter_postprocess editor_green_screen --manifest-path server-rs/Cargo.toml`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【编辑器】画板角色形象生成入口设计-2026-06-15.md`、`docs/【编辑器】画板UI设计图生成入口设计-2026-06-17.md`、`docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md`。 ## 2026-07-03 图片画布生成纯色背景资产接入 BgFilter diff --git a/scripts/check-bgfilter-live.mjs b/scripts/check-bgfilter-live.mjs deleted file mode 100644 index 56dbd99ca..000000000 --- a/scripts/check-bgfilter-live.mjs +++ /dev/null @@ -1,273 +0,0 @@ -import { deflateSync } from 'node:zlib'; -import { existsSync, readFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const root = resolve(__dirname, '..'); - -const DEFAULT_BASE_URL = 'http://58.87.105.82/bgfilter'; -const DEFAULT_SCREEN_COLOR = '#CFEFFF'; -const DEFAULT_SEG_MODEL = 'birefnet'; -const DEFAULT_TIMEOUT_MS = 60_000; - -function loadEnvFile(path) { - if (!existsSync(path)) { - return {}; - } - const env = {}; - const content = readFileSync(path, 'utf8'); - for (const line of content.split(/\r?\n/)) { - const trimmed = line.trim(); - if (!trimmed || trimmed.startsWith('#')) { - continue; - } - const eqIndex = trimmed.indexOf('='); - if (eqIndex < 0) { - continue; - } - const key = trimmed.slice(0, eqIndex).trim(); - let value = trimmed.slice(eqIndex + 1).trim(); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { - value = value.slice(1, -1); - } - env[key] = value; - } - return env; -} - -const fileEnv = { - ...loadEnvFile(resolve(root, '.env')), - ...loadEnvFile(resolve(root, '.env.local')), - ...loadEnvFile(resolve(root, '.env.secrets.local')), -}; - -function readConfig(name) { - return process.env[name] || fileEnv[name] || ''; -} - -function readArg(name) { - const prefix = `--${name}=`; - const value = process.argv.find((arg) => arg.startsWith(prefix)); - return value ? value.slice(prefix.length) : ''; -} - -function readPositiveIntArg(name, fallback) { - const rawValue = readArg(name); - if (!rawValue) { - return fallback; - } - return parsePositiveInt(`--${name}`, rawValue); -} - -function parsePositiveInt(label, rawValue) { - if (!/^\d+$/.test(rawValue)) { - throw new Error(`${label} 必须是正整数,当前为 ${rawValue}`); - } - const value = Number.parseInt(rawValue, 10); - if (value <= 0) { - throw new Error(`${label} 必须是正整数,当前为 ${rawValue}`); - } - return value; -} - -function normalizeHexColor(value) { - const color = value.trim(); - if (!/^#[0-9a-fA-F]{6}$/.test(color)) { - throw new Error(`screen_color 必须是 #RRGGBB,当前为 ${value}`); - } - return color.toUpperCase(); -} - -function parseHexColor(value) { - const color = normalizeHexColor(value); - return [ - Number.parseInt(color.slice(1, 3), 16), - Number.parseInt(color.slice(3, 5), 16), - Number.parseInt(color.slice(5, 7), 16), - ]; -} - -function crc32(buffer) { - let crc = 0xffffffff; - for (const byte of buffer) { - crc ^= byte; - for (let bit = 0; bit < 8; bit += 1) { - crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1; - } - } - return (crc ^ 0xffffffff) >>> 0; -} - -function pngChunk(type, data) { - const typeBuffer = Buffer.from(type, 'ascii'); - const length = Buffer.alloc(4); - length.writeUInt32BE(data.length, 0); - const checksum = Buffer.alloc(4); - checksum.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0); - return Buffer.concat([length, typeBuffer, data, checksum]); -} - -function makeProbePng(screenColor, width, height) { - const [red, green, blue] = parseHexColor(screenColor); - const rowSize = 1 + width * 4; - const raw = Buffer.alloc(rowSize * height); - const subjectLeft = Math.floor(width * 0.31); - const subjectRight = Math.ceil(width * 0.69); - const subjectTop = Math.floor(height * 0.25); - const subjectBottom = Math.ceil(height * 0.75); - - for (let y = 0; y < height; y += 1) { - const row = y * rowSize; - raw[row] = 0; - for (let x = 0; x < width; x += 1) { - const offset = row + 1 + x * 4; - const inSubject = - x >= subjectLeft && x < subjectRight && y >= subjectTop && y < subjectBottom; - raw[offset] = inSubject ? 35 : red; - raw[offset + 1] = inSubject ? 35 : green; - raw[offset + 2] = inSubject ? 35 : blue; - raw[offset + 3] = 255; - } - } - - const ihdr = Buffer.alloc(13); - ihdr.writeUInt32BE(width, 0); - ihdr.writeUInt32BE(height, 4); - ihdr[8] = 8; - ihdr[9] = 6; - ihdr[10] = 0; - ihdr[11] = 0; - ihdr[12] = 0; - - return Buffer.concat([ - Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), - pngChunk('IHDR', ihdr), - pngChunk('IDAT', deflateSync(raw)), - pngChunk('IEND', Buffer.alloc(0)), - ]); -} - -function buildEndpoint(baseUrl) { - const base = baseUrl.trim().replace(/\/+$/, ''); - if (!base) { - throw new Error('BgFilter base url 为空'); - } - return `${base}/remove-background`; -} - -const baseUrl = - readArg('base-url') || - readConfig('GENARRATIVE_EDITOR_BGFILTER_BASE_URL') || - DEFAULT_BASE_URL; -const screenColor = normalizeHexColor( - readArg('screen-color') || - readConfig('GENARRATIVE_EDITOR_BGFILTER_SCREEN_COLOR') || - DEFAULT_SCREEN_COLOR, -); -const segModel = - readArg('seg-model') || - readConfig('GENARRATIVE_EDITOR_BGFILTER_SEG_MODEL') || - DEFAULT_SEG_MODEL; -const token = - readConfig('GENARRATIVE_EDITOR_BGFILTER_TOKEN') || - readConfig('GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN'); -const timeoutMs = readPositiveIntArg( - 'timeout-ms', - parsePositiveInt( - 'GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS', - readConfig('GENARRATIVE_EDITOR_BGFILTER_REQUEST_TIMEOUT_MS') || - `${DEFAULT_TIMEOUT_MS}`, - ), -); -const width = readPositiveIntArg('width', 96); -const height = readPositiveIntArg('height', width); - -const endpoint = buildEndpoint(baseUrl); -const inputImage = makeProbePng(screenColor, width, height); -const form = new FormData(); -form.append( - 'file', - new Blob([inputImage], { type: 'image/png' }), - 'bgfilter-live-probe.png', -); -form.append('screen_color', screenColor); -form.append('seg_model', segModel); - -const controller = new AbortController(); -const timer = setTimeout(() => controller.abort(), timeoutMs); -const startedAt = Date.now(); - -console.log('BgFilter 探活'); -console.log(`目标: ${endpoint}`); -console.log(`screen_color: ${screenColor}`); -console.log(`seg_model: ${segModel}`); -console.log(`image: ${width}x${height}, ${inputImage.length} bytes`); -console.log(`timeout_ms: ${timeoutMs}`); -console.log(`token: ${token ? '已配置' : '未配置'}\n`); - -try { - const headers = {}; - if (token) { - headers['X-Genarrative-Image-Token'] = token; - } - const response = await fetch(endpoint, { - method: 'POST', - headers, - body: form, - signal: controller.signal, - }); - clearTimeout(timer); - - const elapsedMs = Date.now() - startedAt; - const contentType = response.headers.get('content-type') || ''; - const upstreamElapsedMs = response.headers.get('x-bgfilter-elapsed-ms') || ''; - const upstreamSegModel = response.headers.get('x-bgfilter-seg-model') || ''; - const upstreamScreenColor = response.headers.get('x-bgfilter-screen-color') || ''; - const body = Buffer.from(await response.arrayBuffer()); - - if (!response.ok) { - console.error(`失败: HTTP ${response.status} ${response.statusText}`); - console.error(`耗时: ${elapsedMs}ms`); - console.error(body.toString('utf8').slice(0, 500)); - process.exit(1); - } - - if (!contentType.toLowerCase().startsWith('image/')) { - console.error(`失败: 返回 content-type 不是图片: ${contentType || '(empty)'}`); - console.error(body.toString('utf8').slice(0, 500)); - process.exit(1); - } - - if (body.length === 0) { - console.error('失败: 返回图片为空'); - process.exit(1); - } - - console.log(`成功: HTTP ${response.status} ${response.statusText}`); - console.log(`耗时: ${elapsedMs}ms`); - console.log(`content-type: ${contentType}`); - console.log(`bytes: ${body.length}`); - if (upstreamElapsedMs) { - console.log(`x-bgfilter-elapsed-ms: ${upstreamElapsedMs}`); - } - if (upstreamSegModel) { - console.log(`x-bgfilter-seg-model: ${upstreamSegModel}`); - } - if (upstreamScreenColor) { - console.log(`x-bgfilter-screen-color: ${upstreamScreenColor}`); - } -} catch (error) { - clearTimeout(timer); - const elapsedMs = Date.now() - startedAt; - const message = - error?.name === 'AbortError' - ? `请求超时 (${timeoutMs}ms)` - : error?.message || String(error); - console.error(`失败: ${message}`); - console.error(`耗时: ${elapsedMs}ms`); - process.exit(1); -}