我如何用 FLUX.1 Kontext 打造 AI 家具合成演示
一位家具客户带着一个每个小电商卖家最终都会撞上的问题来找我:他们的产品图都是孤立的产品照——一张白底桌子、一张白底椅子——但顾客不会从一片白茫茫的虚无里买家具,他们买的是房间。给每个产品、每种室内风格都拍一次实体棚拍,对一个人的生意来说根本不可能。
需求是:拿两张产品照(一张餐桌、一把椅子),把它们一起放进一个可信的、高级感的室内场景——生成出来的,而不是拍出来的。这篇文章讲的就是我为验证这件事可行而做的概念验证:我选的模型,以及三个差点把它搞砸的 bug。
为什么选 FLUX.1 Kontext——以及那个关键的端点
第一反应自然是通用文生图模型:「一张餐桌和一把椅子放在现代客厅里」。可当客户说*「不——是我的桌子、我的椅子,就是我产品页上那一款」*时,这个思路就崩了。生成一个相似品毫无价值;重点是要让真实产品原封不动,只改变它周围的房间。
这正是 FLUX.1 Kontext(通过 fal.ai)专门解决的问题:以图生图,参考图锚定产品,提示词描述场景。但有个坑让我耗掉一个下午:这里有两个端点。
| 端点 | 参考图输入 | 用途 |
|---|---|---|
fal-ai/flux-pro/kontext | image_url(单张) | 编辑 / 重新语境化一张图 |
fal-ai/flux-pro/kontext/multi | image_urls(数组) | 合并多张参考图 |
我需要把两个独立产品融合进一个场景,所以普通 kontext 不够——它只吃一张参考图。multi 变体接受参考图数组,这才是「桌子 + 椅子 → 一个房间」能成立的关键。它被标记为实验版,但这件事上它是唯一的选择。
用一个纯 HTML 文件调用 fal.ai——不用 SDK,不用服务器
做概念验证我不想搭后端。fal.ai 暴露了一个纯 REST 队列 API,所以一个 index.html 配 vanilla JavaScript 就能搞定全部。但这是三步,不是一步:
// 1. 提交——返回 request_id + 轮询 URL
const submit = await fetch(
"https://queue.fal.run/fal-ai/flux-pro/kontext/multi",
{
method: "POST",
headers: {
Authorization: "Key " + FAL_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "...",
image_urls: [tableDataUrl, chairDataUrl],
enhance_prompt: false,
}),
}
);
const { request_id, status_url, response_url } = await submit.json();
// 2. 轮询状态直到 COMPLETED(约 5–30 秒)
let status;
do {
await new Promise((r) => setTimeout(r, 2000));
status = (await (await fetch(status_url, {
headers: { Authorization: "Key " + FAL_KEY },
})).json()).status;
} while (status === "IN_QUEUE" || status === "IN_PROGRESS");
// 3. 取结果
const result = await (await fetch(response_url, {
headers: { Authorization: "Key " + FAL_KEY },
})).json();
// result.images[0].url 就是生成的图片
有两件事我很早就验证了,因为它们决定了「单文件」方案成不成立:
- CORS 是开放的。
queue.fal.run返回宽松的access-control-allow-origin,并允许authorization头,所以浏览器可以直接调用,无需代理。我在写任何 UI 之前就用 preflight 确认了这一点。 - 本地图以 base64 data URI 传入。 fal.ai 的
image_urls接受data:URI,所以我根本不需要先把客户的照片传到存储桶——demo 直接读文件、压缩,然后塞进请求体。
Canvas 压缩层
手机照片有 5–10 MB。两张这样、再 base64 编码,请求体会膨胀到无法使用,生成也会变慢。所以提交前,demo 会把每张图过一遍 <canvas> 来缩放并重新编码:
async function imgToDataURI(file, maxSize = 1024, quality = 0.85) {
const img = await createImageBitmap(file); // 或 new Image()
const scale = Math.min(1, maxSize / Math.max(img.width, img.height));
const c = document.createElement("canvas");
c.width = Math.round(img.width * scale);
c.height = Math.round(img.height * scale);
c.getContext("2d").drawImage(img, 0, 0, c.width, c.height);
return c.toDataURL("image/jpeg", quality); // 每张约 200–400 KB
}
一张 7 MB 的手机照变成约 300 KB 的 JPEG。上传快,模型处理也快。
Bug #1 ——「Tainted canvases may not be exported」
demo 在我脑子里能跑,在浏览器里却不行。我一点「生成」就收到:
Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
这是浏览器的安全边界,不是 fal.ai 的问题。当你用 file:// 打开页面、并用 <img src="chair.jpg"> 加载一张本地图片时,那张图属于不透明源。它一被画到 canvas 上,canvas 就变成 tainted,toDataURL() 拒绝导出——浏览器不会让网页读回一张它无法证明有权读取的文件的像素。
有两个修复,我在不同阶段都用过:
- 把默认图以 base64 data URI 内嵌进 HTML 源码。 data URI 不会污染 canvas,所以即使从磁盘双击打开也能跑。
- 走 HTTP、用同源相对路径(
src="chair.jpg")。一旦页面和图片同源,canvas 就保持干净。这是真实部署的正确答案——demo 现在跑在一个 URL 上,而不是双击打开的文件。
通用规则:只要任何不透明源的图片碰到 canvas,toDataURL 立刻报错。 如果你要加载本地文件,要么内嵌,要么走 HTTP。不改浏览器安全设置,没有第三条路。
Bug #2 —— API key 就在 HTML 里
对 demo 来说这可以容忍;对任何公开的东西来说这是个洞。客户端 fetch 里的 Key 头意味着 key 就在页面源码里,任何人按 F12 就能读到。给我交给客户的概念验证没问题,上生产就不可接受。
真正 WordPress 插件(下一阶段)的计划是把 key 挪到服务端 PHP:插件的端点去做带鉴权的 fal.ai 调用并返回图片,浏览器只跟插件自己的路由对话。demo 证明了管线能跑通;插件会把秘密放回秘密该待的地方。
Bug #3 —— 输出看起来「像第二张图」
这是让我怀疑 multi 到底能不能用的那个。我换了一张新桌子图、生成,结果看起来跟椅子参考图几乎一模一样——好像模型直接忽略了桌子、复制了椅子。
在怪模型之前,我先查了一件可证伪的事:multi 到底是在合成,还是只是在回显某一张输入? 我用感知哈希和平均像素差对比了生成图和两张源图:
| 对比 | 平均像素差(0 = 相同) | dHash 距离 |
|---|---|---|
| 结果 vs 桌子 | 55.7 | 31 |
| 结果 vs 椅子 | 55.3 | 29 |
| 桌子 vs 椅子 | 69.3 | 30 |
结果离两张输入都很远——端点是真在合成新场景,不是复制。所以「像椅子」的问题不在 API,而在输入和提示词。
三个叠加的原因:
- 参考图不一致。 最初的
chair.jpg是一张完整的「桌+椅」场景照,不是干净的单椅——于是模型看到一张图就已经满足「桌+椅同框」,就倾向照着它来。 - 按位置指代不可靠。 Kontext 按图片内容匹配,而不是数组顺序。说「第一张图」/「第二张图」并不能可靠地映射到「桌子」/「椅子」。修复是直接命名物体——「the dining table」和「the chair」——让模型自己去匹配正确的参考图。
- prompt enhancement 开着。 fal.ai 的
enhance_prompt(默认关,但我一直惦记着它)会把你的提示词改写成更丰富的审美描述——当目标是*「别重新解读产品」*时,这正好帮倒忙。我把它钉死为false,让模型遵守字面提示词,而不是添油加醋。
最终起作用的提示词
客户的核心要求很严格:原样保留家具,只去发明房间。这需要一段把大部分篇幅花在阻止重新解读、而不是描述风格上的提示词:
Create a photorealistic interior photograph using the exact dining table and exact chair from the reference images. Preserve both furniture pieces exactly as shown — do not redesign, recolor, repaint, restyle, replace, or reinterpret either product. Keep their original geometry, proportions, construction, material, finish, texture, and color exactly unchanged.
Place the chair naturally beside the dining table in a realistic dining position, slightly pulled under the table and properly aligned with it. Maintain realistic scale, perspective, and physical contact with the floor. The chair and table must look photographed together in the same real room, with natural contact shadows — not composited or pasted together.
Preserve the true original color. Do not allow the room lighting, wall or floor colors, or grading to alter the furniture.
Set the scene in a spacious modern living room with floor-to-ceiling windows and subtle warm afternoon daylight. Use restrained, neutral interior colors.
Photorealistic commercial furniture photography, realistic camera perspective, natural proportions, high detail.
demo 里内置了六段这样的提示词,每种场景风格一段(现代客厅、温馨餐厅、北欧风、侘寂风、以及干净的影棚),每一段只改「set the scene」那一节。
结果
一个 index.html,没有后端、没有 SDK——两张产品照进,一个布置好的房间出,每张图 $0.04,生成约 15–20 秒。客户现在有了一个能拿给他们顾客看的可用预览,带场景选择器和可编辑提示词,然后再决定是否投入完整的 WordPress 插件。
这个概念验证回答了一个它生来就要回答的问题:是的,你可以保留真实产品、只生成它周围的房间——只要你尊重那三条真正要紧的约束:每个产品一张干净的参考图、按物体命名(而非按位置)的提示词、以及 enhance_prompt: false。
我会做不同的地方
- 第一天就要求干净的单品参考图。 下游的每一种失败模式——「像某一张图」的问题、比例漂移——都能追溯到不一致的源照片。我会在碰任何模型之前,先发一份给客户的简短说明(「每张照片一个产品、纯色背景、没有其他家具」)。
- 先显式关掉
enhance_prompt。 我浪费了一次生成,就因为 enhancement 把我精心写的「别重新解读」改写成恰恰相反的东西。 - 在写 UI 之前先用真实 preflight 验证 CORS。 两行
curl的事,却能给整个架构决策祛除风险。
想为你的产品目录做这个吗?
我为电商企业构建 AI 图像管线、WordPress 插件,以及自托管基础设施。如果你想把产品摆进风格化的房间场景——或者想雇我做类似的 AI 集成——我非常乐意交流:
- 📱 WhatsApp: +60 12-797 2969
- 📧 邮箱: [email protected]
- 🌐 网站: hoelee.com