|
|
@@ -0,0 +1,408 @@
|
|
|
+/**
|
|
|
+ * 毛坯房量尺 — 5轮聚焦提示词模块
|
|
|
+ *
|
|
|
+ * 从 analyze-photos-v4.mjs 移植,API 调用委托给 vision-client.mjs。
|
|
|
+ *
|
|
|
+ * 使用示例:
|
|
|
+ * import { processPhoto, PASS_CONFIGS } from './prompts/room-measurement.mjs';
|
|
|
+ * const result = await processPhoto('/path/to/photo.jpg', 'img-001', 'room-a.jpg');
|
|
|
+ */
|
|
|
+
|
|
|
+import fs from 'fs';
|
|
|
+import path from 'path';
|
|
|
+import { callVisionAPI } from '../vision-client.mjs';
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// 5轮聚焦提示词
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+export const PASS1_SYSTEM = `你是一位建筑空间分析专家。你的任务是精确分析毛坯房照片的**空间结构**。
|
|
|
+
|
|
|
+## 规则
|
|
|
+1. **透视类型**:判断一点透视/两点透视/三点透视。
|
|
|
+ - 一点透视:正面墙正对镜头,水平线汇聚到画面中心
|
|
|
+ - 两点透视:墙角在画面中心附近,两侧墙面分别向左右消失
|
|
|
+ - 三点透视:仰拍/俯拍导致垂直线也汇聚
|
|
|
+ - 特别注意:如果看到两个墙面以夹角呈现(墙角在画面中心附近),必须报告 twoPoint
|
|
|
+
|
|
|
+2. **墙面多边形**:每面可见墙标注**精确的4个角点**(四边形),沿建筑实际边缘。
|
|
|
+ - surfaceType: facing(正面)/leftWall(左墙)/rightWall(右墙)
|
|
|
+ - 每条边放3个等分测量点(measurePoints)
|
|
|
+
|
|
|
+3. **天花/地面区域**:各标注4个角点的多边形
|
|
|
+
|
|
|
+4. **阴阳角**:标注位置(x,y)
|
|
|
+
|
|
|
+5. **忽略**所有小物件、家具、装饰、门窗、吊顶细节——这些会在后续分析中处理
|
|
|
+
|
|
|
+## 输出格式(严格JSON,无markdown代码块)
|
|
|
+{
|
|
|
+ "pass": 1,
|
|
|
+ "perspective": {"type": "onePoint|twoPoint|threePoint", "description": "透视说明", "vanishingPoints": [{"x": 50, "y": 40}]},
|
|
|
+ "surfaces": {
|
|
|
+ "walls": [
|
|
|
+ {"id": "w1", "label": "正面主墙", "surfaceType": "facing",
|
|
|
+ "polygon": [{"x":20,"y":25},{"x":75,"y":25},{"x":75,"y":82},{"x":20,"y":80}],
|
|
|
+ "measureLines": [
|
|
|
+ {"label":"顶边3点","type":"horizontal","edge":"top","startPoint":{"x":20,"y":25},"endPoint":{"x":75,"y":25},"measurePoints":[{"x":20,"y":25},{"x":47.5,"y":25},{"x":75,"y":25}]},
|
|
|
+ {"label":"底边3点","type":"horizontal","edge":"bottom","startPoint":{"x":20,"y":80},"endPoint":{"x":75,"y":82},"measurePoints":[{"x":20,"y":80},{"x":47.5,"y":81},{"x":75,"y":82}]},
|
|
|
+ {"label":"左边3点","type":"vertical","edge":"left","startPoint":{"x":20,"y":25},"endPoint":{"x":20,"y":80},"measurePoints":[{"x":20,"y":25},{"x":20,"y":52.5},{"x":20,"y":80}]},
|
|
|
+ {"label":"右边3点","type":"vertical","edge":"right","startPoint":{"x":75,"y":25},"endPoint":{"x":75,"y":82},"measurePoints":[{"x":75,"y":25},{"x":75,"y":53.5},{"x":75,"y":82}]}
|
|
|
+ ]}
|
|
|
+ ],
|
|
|
+ "floorRegion": {"polygon": [{"x":0,"y":80},{"x":100,"y":80},{"x":100,"y":100},{"x":0,"y":100}], "label": "可见地面"},
|
|
|
+ "ceilingRegion": {"polygon": [{"x":0,"y":0},{"x":100,"y":0},{"x":100,"y":20},{"x":0,"y":20}], "label": "可见天花"}
|
|
|
+ },
|
|
|
+ "corners": [
|
|
|
+ {"id":"c1","type":"internal","label":"左阴角","position":{"x":20,"y":55}},
|
|
|
+ {"id":"c2","type":"internal","label":"右阴角","position":{"x":75,"y":55}}
|
|
|
+ ]
|
|
|
+}`;
|
|
|
+
|
|
|
+export const PASS1_USER = `请分析这张照片的**空间结构**:
|
|
|
+1. 判断透视类型(一点/两点/三点),找消失点
|
|
|
+2. 标注每面可见墙的4角多边形,区分facing/leftWall/rightWall
|
|
|
+3. 标注天花/地面区域
|
|
|
+4. 标注阴阳角位置
|
|
|
+
|
|
|
+只输出JSON,不包含其他内容:`;
|
|
|
+
|
|
|
+export const PASS2_SYSTEM = `你是一位吊顶与天花结构分析专家。你的任务是精确分析照片中的**天花板特征**。
|
|
|
+
|
|
|
+## 规则
|
|
|
+1. **只标注天花板上的结构特征**,忽略墙面、地面、门窗、障碍物
|
|
|
+2. **关键:每个特征必须用4个角点的简单四边形标注**。即使实际形状不规则,也只能用4点近似。禁止使用5点或更多点。
|
|
|
+3. 特征类型:
|
|
|
+ - cornice: 石膏线/阴角线(天花与墙面交界处的装饰线条)
|
|
|
+ - trayStep: 吊顶叠级/双眼皮(不同高度的吊顶分界线)
|
|
|
+ - beam: 梁/下返结构
|
|
|
+ - bulkhead: 窗帘盒/设备带(局部下返区域)
|
|
|
+ - soffit: 管道包封/检修口
|
|
|
+4. polygon的4个点按顺时针方向标注
|
|
|
+
|
|
|
+## 输出格式(严格JSON,无markdown代码块)
|
|
|
+{
|
|
|
+ "pass": 2,
|
|
|
+ "ceilingFeatures": [
|
|
|
+ {"id":"cf1","type":"cornice","label":"石膏阴角线",
|
|
|
+ "polygon": [{"x":0,"y":8},{"x":100,"y":8},{"x":100,"y":12},{"x":0,"y":12}]},
|
|
|
+ {"id":"cf2","type":"trayStep","label":"第一层叠级线",
|
|
|
+ "polygon": [{"x":20,"y":22},{"x":80,"y":22},{"x":80,"y":26},{"x":20,"y":26}]}
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
+如果没有可见的天花特征,返回空数组:{"pass":2,"ceilingFeatures":[]}`;
|
|
|
+
|
|
|
+export const PASS2_USER = `请分析这张照片的**天花板特征**:
|
|
|
+1. 石膏线/阴角线(cornice)
|
|
|
+2. 吊顶叠级/双眼皮(trayStep)
|
|
|
+3. 梁/下返结构(beam)
|
|
|
+4. 窗帘盒/设备带(bulkhead)
|
|
|
+
|
|
|
+记住:每个特征只能用4个角点标注!简单四边形!
|
|
|
+
|
|
|
+只输出JSON:`;
|
|
|
+
|
|
|
+export const PASS3_SYSTEM = `你是一位门窗洞口测量专家。你的任务是精确分析照片中的**所有门洞和窗洞**。
|
|
|
+
|
|
|
+## 规则
|
|
|
+1. **只标注门洞和窗洞**,忽略其他所有元素(墙壁、天花、障碍物等)
|
|
|
+2. 每个洞口标注**双层框架**:
|
|
|
+ - outerPolygon: 洞口在墙面上的外轮廓(4个角点,即墙面上的实际开口边缘)
|
|
|
+ - innerPolygon: 门扇/窗扇/玻璃区域的内轮廓(4个角点)
|
|
|
+ - frameThickness: 门套/窗套线宽度(百分比),如无套线则为0
|
|
|
+3. 测量线沿外框放置:上中下宽度3点 + 左中右高度3点
|
|
|
+4. 如果无可见洞口,返回空数组
|
|
|
+
|
|
|
+## 输出格式(严格JSON,无markdown代码块)
|
|
|
+{
|
|
|
+ "pass": 3,
|
|
|
+ "openings": [
|
|
|
+ {"id":"d1","type":"door","label":"入户门",
|
|
|
+ "frame": {
|
|
|
+ "outerPolygon": [{"x":35,"y":20},{"x":55,"y":18},{"x":55,"y":80},{"x":35,"y":82}],
|
|
|
+ "innerPolygon": [{"x":37,"y":22},{"x":53,"y":20},{"x":53,"y":78},{"x":37,"y":80}],
|
|
|
+ "frameThickness": 2.0
|
|
|
+ },
|
|
|
+ "measureLines": [
|
|
|
+ {"label":"门洞上口宽","type":"horizontal","startPoint":{"x":35,"y":20},"endPoint":{"x":55,"y":18},"measurePoints":[{"x":35,"y":20},{"x":45,"y":19},{"x":55,"y":18}]},
|
|
|
+ {"label":"门洞左口高","type":"vertical","startPoint":{"x":35,"y":20},"endPoint":{"x":35,"y":82},"measurePoints":[{"x":35,"y":20},{"x":35,"y":51},{"x":35,"y":82}]}
|
|
|
+ ]}
|
|
|
+ ]
|
|
|
+}`;
|
|
|
+
|
|
|
+export const PASS3_USER = `请分析这张照片的**所有门洞和窗洞**:
|
|
|
+1. 标注外层框架(outerPolygon,墙上开口的精确边缘)
|
|
|
+2. 标注内层框架(innerPolygon,门扇/玻璃边缘)
|
|
|
+3. 标注门套/窗套厚度(frameThickness)
|
|
|
+4. 放置测量点
|
|
|
+
|
|
|
+只输出JSON:`;
|
|
|
+
|
|
|
+export const PASS4_SYSTEM = `你是一位全屋定制障碍物检测专家。你的任务是精确标注照片中**所有可见障碍物**的包围盒。
|
|
|
+
|
|
|
+## 核心原则
|
|
|
+每个包围盒(boundingBox)告诉测量人员"需要测量这个矩形区域的实际尺寸"。你必须非常精确——贴合物体的真实可见边缘。
|
|
|
+
|
|
|
+## 障碍物类型
|
|
|
+- outlet(插座): 86型约2%×2%, 118型约3%×2%
|
|
|
+- switch(开关): 同插座
|
|
|
+- electricBox(电箱): 箱体外框,通常5-15%
|
|
|
+- vent(风口): 格栅外框在吊顶/墙上
|
|
|
+- pipe(管道): 管道与墙/地接触范围
|
|
|
+- baseboard(踢脚线): 墙底水平条带
|
|
|
+- doorFrame(门套线): 门套在墙上的宽度条带
|
|
|
+- windowFrame(窗套线): 窗套在墙上的范围
|
|
|
+- gasMeter(燃气表): 表箱外框
|
|
|
+- floorDrain(地漏): 地面位置
|
|
|
+- downlight(筒灯): 天花位置
|
|
|
+
|
|
|
+## ⚠️ 踢脚线高度规则(非常重要!)
|
|
|
+- 踢脚线(baseboard)的高度必须在 2%-5% 之间
|
|
|
+- 这是踢脚线条带**本身**的高度,不是从踢脚线到墙顶的距离
|
|
|
+- 正面墙(facing)踢脚线:沿着墙底的水平窄条,height = 2-4%
|
|
|
+- 侧墙(leftWall/rightWall)踢脚线:height = 2-5%(不要被透视缩短误导!)
|
|
|
+- **如果标注的height > 10%,一定是错误的——请重新检查!**那是整面墙的高度,不是踢脚线
|
|
|
+- 侧墙的踢脚线:看墙底部那条水平的细线/条带,标注那条条带的高度
|
|
|
+
|
|
|
+## 包围盒格式
|
|
|
+boundingBox: { x, y, width, height } — 全部百分比
|
|
|
+- x, y: 包围盒左上角相对于图片的百分比位置
|
|
|
+- width, height: 包围盒的宽高百分比
|
|
|
+
|
|
|
+## 输出格式(严格JSON,无markdown代码块)
|
|
|
+{
|
|
|
+ "pass": 4,
|
|
|
+ "obstacles": [
|
|
|
+ {"id":"obs1","type":"outlet","label":"五孔插座(86型)","boundingBox":{"x":42,"y":56,"width":2.5,"height":3.2}},
|
|
|
+ {"id":"obs2","type":"baseboard","label":"木质踢脚线","boundingBox":{"x":20,"y":80,"width":55,"height":3}},
|
|
|
+ {"id":"obs3","type":"vent","label":"空调出风口","boundingBox":{"x":8,"y":10,"width":14,"height":4}}
|
|
|
+ ]
|
|
|
+}`;
|
|
|
+
|
|
|
+export const PASS4_USER = `请分析这张照片的**所有障碍物**:
|
|
|
+1. 插座、开关、电箱
|
|
|
+2. 风口(空调、新风、排风)
|
|
|
+3. 管道
|
|
|
+4. 踢脚线(⚠️ height必须2-5%,不能是整面墙高度!)
|
|
|
+5. 门套线、窗套线
|
|
|
+6. 燃气表、地漏
|
|
|
+7. 筒灯、射灯
|
|
|
+
|
|
|
+每个障碍物用精确的boundingBox{x,y,width,height}标注。
|
|
|
+只输出JSON:`;
|
|
|
+
|
|
|
+export const PASS5_SYSTEM = `你是一位全屋定制测量专家。你有4份针对同一房间的分析数据,分别来自不同专家的独立观察。请将它们合并为一份完整的测量分析报告。
|
|
|
+
|
|
|
+## 你的任务
|
|
|
+1. 阅读4份数据,理解空间结构
|
|
|
+2. 写出 sceneDescription(完整的场景描述,2-3句话)
|
|
|
+3. 判断 roomType(卧室/客厅/厨房/卫生间/阳台/走廊/储物间/其他)
|
|
|
+4. 生成 measurementPlan(测量计划),将所有元素关联到测量步骤
|
|
|
+5. 评估 photoQuality(是否广角、畸变程度、是否需要补拍)
|
|
|
+6. 列出 issues(如有遮挡、光线不足等问题)
|
|
|
+
|
|
|
+## 测量计划规则
|
|
|
+- 每面墙至少一个步骤(3点宽+3点高)
|
|
|
+- 每个门洞/窗洞一个步骤
|
|
|
+- 每组同类障碍物可以合并为一个步骤(如"测量所有插座位置")
|
|
|
+- 步骤按重要性排序:required > recommended > optional
|
|
|
+- elementIds必须引用实际存在的ID(来自输入数据)
|
|
|
+- 工具:激光测距仪(长距离)、卷尺(小尺寸)、水平仪(垂直度)
|
|
|
+
|
|
|
+## 输出格式(严格JSON,无markdown代码块)
|
|
|
+{
|
|
|
+ "pass": 5,
|
|
|
+ "sceneDescription": "完整的场景描述...",
|
|
|
+ "roomType": "卧室",
|
|
|
+ "measurementPlan": [
|
|
|
+ {"step":1,"action":"测量正面主墙顶中底3点宽度与左中右3点高度","target":"w1","tool":"激光测距仪","priority":"required","elementIds":["w1"]}
|
|
|
+ ],
|
|
|
+ "photoQuality": {"isWideAngle":true,"distortionLevel":"low","recommendReshoot":false,"reshootAdvice":""},
|
|
|
+ "issues": []
|
|
|
+}`;
|
|
|
+
|
|
|
+export const PASS5_USER_TEMPLATE = `以下是一个房间的4份独立分析数据。请将它们合并:
|
|
|
+
|
|
|
+=== 空间结构 ===
|
|
|
+__PASS1__
|
|
|
+
|
|
|
+=== 吊顶特征 ===
|
|
|
+__PASS2__
|
|
|
+
|
|
|
+=== 门窗洞口 ===
|
|
|
+__PASS3__
|
|
|
+
|
|
|
+=== 障碍物 ===
|
|
|
+__PASS4__
|
|
|
+
|
|
|
+请生成完整的测量分析报告。只输出JSON:`;
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// 轮次配置(供 callMultiPass 使用)
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+export const PASS_CONFIGS = [
|
|
|
+ { name: 'spatial', systemPrompt: PASS1_SYSTEM, userPrompt: PASS1_USER, maxTokens: 2000 },
|
|
|
+ { name: 'ceiling', systemPrompt: PASS2_SYSTEM, userPrompt: PASS2_USER, maxTokens: 1000 },
|
|
|
+ { name: 'openings', systemPrompt: PASS3_SYSTEM, userPrompt: PASS3_USER, maxTokens: 2500 },
|
|
|
+ { name: 'obstacles', systemPrompt: PASS4_SYSTEM, userPrompt: PASS4_USER, maxTokens: 1500 },
|
|
|
+];
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// 合并函数
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+export function mergeResults(photoId, fileName, passResults) {
|
|
|
+ const p1 = passResults[0]?.parsed || {};
|
|
|
+ const p2 = passResults[1]?.parsed || {};
|
|
|
+ const p3 = passResults[2]?.parsed || {};
|
|
|
+ const p4 = passResults[3]?.parsed || {};
|
|
|
+ const p5 = passResults[4]?.parsed || {};
|
|
|
+
|
|
|
+ const merged = {
|
|
|
+ version: 'v4-multipass',
|
|
|
+ photoId,
|
|
|
+ fileName,
|
|
|
+ analyzedAt: new Date().toISOString(),
|
|
|
+ passes: passResults.map((p, i) => ({
|
|
|
+ pass: i + 1,
|
|
|
+ name: p.name || `pass${i + 1}`,
|
|
|
+ status: p.error ? 'error' : 'ok',
|
|
|
+ error: p.error || null,
|
|
|
+ usage: p.usage || null,
|
|
|
+ })),
|
|
|
+ parsed: {
|
|
|
+ sceneDescription: p5.sceneDescription || '',
|
|
|
+ roomType: p5.roomType || '',
|
|
|
+ perspective: p1.perspective || { type: 'onePoint', description: '', vanishingPoints: [] },
|
|
|
+ surfaces: p1.surfaces || { walls: [], floorRegion: null, ceilingRegion: null },
|
|
|
+ openings: p3.openings || [],
|
|
|
+ ceilingFeatures: p2.ceilingFeatures || [],
|
|
|
+ corners: p1.corners || [],
|
|
|
+ obstacles: p4.obstacles || [],
|
|
|
+ measurementPlan: p5.measurementPlan || [],
|
|
|
+ issues: p5.issues || [],
|
|
|
+ photoQuality: p5.photoQuality || { isWideAngle: false, distortionLevel: 'unknown', recommendReshoot: false, reshootAdvice: '' },
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // 质量验证:踢脚线高度检查
|
|
|
+ const suspiciousBaseboards = (merged.parsed.obstacles || []).filter(
|
|
|
+ o => o.type === 'baseboard' && o.boundingBox?.height > 10
|
|
|
+ );
|
|
|
+ if (suspiciousBaseboards.length > 0) {
|
|
|
+ console.log(` ⚠ 发现 ${suspiciousBaseboards.length} 个异常踢脚线高度>10%:`);
|
|
|
+ suspiciousBaseboards.forEach(o => {
|
|
|
+ console.log(` ${o.id}: height=${o.boundingBox.height}% (预计2-5%)`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 质量验证:吊顶特征顶点数检查
|
|
|
+ const complexCeilings = (merged.parsed.ceilingFeatures || []).filter(
|
|
|
+ cf => cf.polygon && cf.polygon.length > 4
|
|
|
+ );
|
|
|
+ if (complexCeilings.length > 0) {
|
|
|
+ console.log(` ⚠ 发现 ${complexCeilings.length} 个吊顶特征顶点>4:`);
|
|
|
+ complexCeilings.forEach(cf => {
|
|
|
+ console.log(` ${cf.id}: ${cf.polygon.length}点 (期望4点)`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return merged;
|
|
|
+}
|
|
|
+
|
|
|
+// ============================================================
|
|
|
+// 主流程:处理单张照片
|
|
|
+// ============================================================
|
|
|
+
|
|
|
+/**
|
|
|
+ * 对单张毛坯房照片执行 5-pass 分析
|
|
|
+ *
|
|
|
+ * @param {string} imagePath 图片路径
|
|
|
+ * @param {string} photoId 照片 ID(用于缓存目录命名)
|
|
|
+ * @param {string} fileName 原始文件名
|
|
|
+ * @param {Object} [opts]
|
|
|
+ * @param {string} [opts.cacheDir] 缓存目录,默认 './output/v4/<photoId>'
|
|
|
+ * @param {string} [opts.model] 模型名
|
|
|
+ * @returns {Promise<Object>} 合并后的分析结果
|
|
|
+ */
|
|
|
+export async function processPhoto(imagePath, photoId, fileName, opts = {}) {
|
|
|
+ const cacheDir = opts.cacheDir || path.resolve('./output/v4', photoId);
|
|
|
+
|
|
|
+ // Pass 1-4: 视觉分析
|
|
|
+ const passResults = [];
|
|
|
+
|
|
|
+ for (const cfg of PASS_CONFIGS) {
|
|
|
+ const passNum = cfg.name === 'spatial' ? 1 : cfg.name === 'ceiling' ? 2 : cfg.name === 'openings' ? 3 : 4;
|
|
|
+ const cacheFile = path.join(cacheDir, `pass${passNum}.json`);
|
|
|
+
|
|
|
+ if (fs.existsSync(cacheFile)) {
|
|
|
+ console.log(` Pass ${passNum} (${cfg.name}): 已有缓存,跳过`);
|
|
|
+ passResults.push(JSON.parse(fs.readFileSync(cacheFile, 'utf-8')));
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(` Pass ${passNum} (${cfg.name}, ${cfg.maxTokens}t)...`);
|
|
|
+ try {
|
|
|
+ const result = await callVisionAPI({
|
|
|
+ imagePath,
|
|
|
+ systemPrompt: cfg.systemPrompt,
|
|
|
+ userPrompt: cfg.userPrompt,
|
|
|
+ maxTokens: cfg.maxTokens,
|
|
|
+ model: opts.model,
|
|
|
+ });
|
|
|
+ const entry = { pass: passNum, name: cfg.name, ...result };
|
|
|
+ if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
|
+ fs.writeFileSync(cacheFile, JSON.stringify(entry, null, 2));
|
|
|
+ passResults.push(entry);
|
|
|
+ console.log(` ${result.error ? '✗ ' + result.error : '✓ OK'} | tokens:${result.usage?.total_tokens || '?'}`);
|
|
|
+ } catch (e) {
|
|
|
+ console.log(` ✗ ${e.message}`);
|
|
|
+ const entry = { pass: passNum, name: cfg.name, error: e.message, parsed: null, usage: null };
|
|
|
+ if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
|
+ fs.writeFileSync(cacheFile, JSON.stringify(entry, null, 2));
|
|
|
+ passResults.push(entry);
|
|
|
+ }
|
|
|
+
|
|
|
+ await new Promise(r => setTimeout(r, 1500));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Pass 5: 文本合并
|
|
|
+ const pass5File = path.join(cacheDir, 'pass5.json');
|
|
|
+ if (fs.existsSync(pass5File)) {
|
|
|
+ console.log(' Pass 5 (merge): 已有缓存,跳过');
|
|
|
+ passResults.push(JSON.parse(fs.readFileSync(pass5File, 'utf-8')));
|
|
|
+ } else {
|
|
|
+ console.log(' Pass 5 (merge, 2000t)...');
|
|
|
+ const p1Json = JSON.stringify(passResults[0]?.parsed || {}, null, 2);
|
|
|
+ const p2Json = JSON.stringify(passResults[1]?.parsed || {}, null, 2);
|
|
|
+ const p3Json = JSON.stringify(passResults[2]?.parsed || {}, null, 2);
|
|
|
+ const p4Json = JSON.stringify(passResults[3]?.parsed || {}, null, 2);
|
|
|
+ const mergePrompt = PASS5_USER_TEMPLATE
|
|
|
+ .replace('__PASS1__', p1Json)
|
|
|
+ .replace('__PASS2__', p2Json)
|
|
|
+ .replace('__PASS3__', p3Json)
|
|
|
+ .replace('__PASS4__', p4Json);
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await callVisionAPI({
|
|
|
+ systemPrompt: PASS5_SYSTEM,
|
|
|
+ userPrompt: mergePrompt,
|
|
|
+ maxTokens: 2000,
|
|
|
+ model: opts.model,
|
|
|
+ });
|
|
|
+ const entry = { pass: 5, name: 'merge', ...result };
|
|
|
+ fs.writeFileSync(pass5File, JSON.stringify(entry, null, 2));
|
|
|
+ passResults.push(entry);
|
|
|
+ console.log(` ${result.error ? '✗ ' + result.error : '✓ OK'} | tokens:${result.usage?.total_tokens || '?'}`);
|
|
|
+ } catch (e) {
|
|
|
+ console.log(` ✗ ${e.message}`);
|
|
|
+ const entry = { pass: 5, name: 'merge', error: e.message, parsed: null, usage: null };
|
|
|
+ fs.writeFileSync(pass5File, JSON.stringify(entry, null, 2));
|
|
|
+ passResults.push(entry);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return mergeResults(photoId, fileName, passResults);
|
|
|
+}
|