gigafibre-fsm/services/targo-hub/lib/vision.js
louispaulb 607ea54b5c refactor: reduce token count, DRY code, consolidate docs
Backend services:
- targo-hub: extract deepGetValue to helpers.js, DRY disconnect reasons
  lookup map, compact CAPABILITIES, consolidate vision.js prompts/schemas,
  extract dispatch scoring weights, trim section dividers across 9 files
- modem-bridge: extract getSession() helper (6 occurrences), resetIdleTimer(),
  consolidate DM query factory, fix duplicate username fill bug, trim headers
  (server.js -36%, tplink-session.js -47%, docker-compose.yml -57%)

Frontend:
- useWifiDiagnostic: extract THRESHOLDS const, split processDiagnostic into
  6 focused helpers (processOnlineStatus, processWanIPs, processRadios,
  processMeshNodes, processClients, checkRadioIssues)
- EquipmentDetail: merge duplicate ROLE_LABELS, remove verbose comments

Documentation (17 → 13 files, -1,400 lines):
- New consolidated README.md (architecture, services, dependencies, auth)
- Merge ECOSYSTEM-OVERVIEW into ARCHITECTURE.md
- Merge MIGRATION-PLAN + ARCHITECTURE-COMPARE + FIELD-GAP + CHANGELOG → MIGRATION.md
- Merge COMPETITIVE-ANALYSIS into PLATFORM-STRATEGY.md
- Update ROADMAP.md with current phase status
- Delete CONTEXT.md (absorbed into README)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:39:58 -04:00

100 lines
4.7 KiB
JavaScript

'use strict'
const cfg = require('./config')
const { log, json, parseBody } = require('./helpers')
const GEMINI_URL = () => `https://generativelanguage.googleapis.com/v1beta/models/${cfg.AI_MODEL}:generateContent?key=${cfg.AI_API_KEY}`
async function geminiVision (base64Image, prompt, schema) {
const resp = await fetch(GEMINI_URL(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }, { inline_data: { mime_type: 'image/jpeg', data: base64Image } }] }],
generationConfig: { temperature: 0.1, maxOutputTokens: 1024, responseMimeType: 'application/json', responseSchema: schema },
}),
})
if (!resp.ok) { const t = await resp.text(); throw new Error(`Gemini API ${resp.status}: ${t.slice(0, 200)}`) }
const data = await resp.json()
const text = (data.candidates?.[0]?.content?.parts?.[0]?.text || '').trim()
log(`Vision response: ${text.slice(0, 300)}`)
let parsed
try { parsed = JSON.parse(text) } catch { const m = text.match(/\{[\s\S]*\}/); if (m) try { parsed = JSON.parse(m[0]) } catch {} }
return parsed
}
function extractBase64 (req, body, label) {
if (!cfg.AI_API_KEY) return { error: 'AI_API_KEY not configured', status: 500 }
if (!body.image) return { error: 'Missing image field (base64)', status: 400 }
const base64 = body.image.replace(/^data:image\/[^;]+;base64,/, '')
log(`Vision ${label}: received image ${Math.round(base64.length * 3 / 4 / 1024)}KB`)
return { base64 }
}
const BARCODE_PROMPT = `Read ALL identifiers on this equipment label photo (may be blurry/tilted).
Extract: barcode text, serial numbers (S/N, SN), MAC addresses (12 hex chars), model numbers (M/N, Model, P/N), IMEI, GPON SN.
Examples: 1608K44D9E79FAFF5, TPLG-A1B2C3D4, 04:18:D6:A1:B2:C3, HWTC87654321.
Try your BEST on every character. Return max 3 most important (serial/MAC first).`
const BARCODE_SCHEMA = {
type: 'object',
properties: { barcodes: { type: 'array', items: { type: 'string' }, maxItems: 3 } },
required: ['barcodes'],
}
async function handleBarcodes (req, res) {
const body = await parseBody(req)
const check = extractBase64(req, body, 'barcode')
if (check.error) return json(res, check.status, { error: check.error })
try {
const result = await extractBarcodes(check.base64)
return json(res, 200, result)
} catch (e) {
log('Vision barcode error:', e.message)
return json(res, 500, { error: 'Vision extraction failed: ' + e.message })
}
}
async function extractBarcodes (base64Image) {
const parsed = await geminiVision(base64Image, BARCODE_PROMPT, BARCODE_SCHEMA)
if (!parsed) return { barcodes: [] }
const arr = Array.isArray(parsed) ? parsed : Array.isArray(parsed.barcodes) ? parsed.barcodes : []
const barcodes = arr.filter(v => typeof v === 'string' && v.trim().length > 3).map(v => v.trim().replace(/\s+/g, '')).slice(0, 3)
log(`Vision: extracted ${barcodes.length} barcode(s): ${barcodes.join(', ')}`)
return { barcodes }
}
const EQUIP_PROMPT = `Read this ISP equipment label (ONT/ONU/router/modem). Return structured JSON.
Extract: brand/manufacturer, model (M/N, P/N), serial (S/N, SN, under barcode), MAC address (12 hex, no separators), GPON SN, HW version, barcodes.
Try your BEST on blurry/angled text. Set missing fields to null.`
const EQUIP_SCHEMA = {
type: 'object',
properties: {
brand: { type: 'string', nullable: true }, model: { type: 'string', nullable: true },
serial_number: { type: 'string', nullable: true }, mac_address: { type: 'string', nullable: true },
gpon_sn: { type: 'string', nullable: true }, hw_version: { type: 'string', nullable: true },
equipment_type: { type: 'string', nullable: true },
barcodes: { type: 'array', items: { type: 'string' }, maxItems: 5 },
},
required: ['serial_number'],
}
async function handleEquipment (req, res) {
const body = await parseBody(req)
const check = extractBase64(req, body, 'equipment')
if (check.error) return json(res, check.status, { error: check.error })
try {
const parsed = await geminiVision(check.base64, EQUIP_PROMPT, EQUIP_SCHEMA)
if (!parsed) return json(res, 200, { serial_number: null, barcodes: [] })
if (parsed.mac_address) parsed.mac_address = parsed.mac_address.replace(/[:\-.\s]/g, '').toUpperCase()
if (parsed.serial_number) parsed.serial_number = parsed.serial_number.replace(/\s+/g, '').trim()
log(`Vision equipment: brand=${parsed.brand} model=${parsed.model} sn=${parsed.serial_number} mac=${parsed.mac_address}`)
return json(res, 200, parsed)
} catch (e) {
log('Vision equipment error:', e.message)
return json(res, 500, { error: 'Vision extraction failed: ' + e.message })
}
}
module.exports = { handleBarcodes, extractBarcodes, handleEquipment }