gigafibre-fsm/services/targo-hub/lib/vision.js
louispaulb 320655b0a0 refactor: major cleanup — remove dead dispatch app, commit all backend code, extract client composables
- Remove apps/dispatch/ (100% replaced by ops dispatch module, unmaintained)
- Commit services/targo-hub/lib/ (24 modules, 6290 lines — was never tracked)
- Commit services/docuseal + services/legacy-db docker-compose configs
- Extract client app composables: useOTP, useAddressSearch, catalog data, format utils
- Refactor CartPage.vue 630→175 lines, CatalogPage.vue 375→95 lines
- Clean hardcoded credentials from config.js fallback values
- Add client portal: catalog, cart, checkout, OTP verification, address search
- Add ops: NetworkPage, AgentFlowsPage, ConversationPanel, UnifiedCreateModal
- Add ops composables: useBestTech, useConversations, usePermissions, useScanner
- Add field app: scanner composable, docker/nginx configs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:38:38 -04:00

233 lines
8.1 KiB
JavaScript

'use strict'
const cfg = require('./config')
const { log, json, parseBody } = require('./helpers')
/**
* POST /vision/barcodes
* Accepts { image: "base64..." } and uses Gemini Flash Vision to extract barcode values.
* Returns { barcodes: ["VALUE1", "VALUE2", ...] }
*/
async function handleBarcodes (req, res) {
if (!cfg.AI_API_KEY) return json(res, 500, { error: 'AI_API_KEY not configured' })
const body = await parseBody(req)
if (!body.image) return json(res, 400, { error: 'Missing image field (base64)' })
// Strip data URI prefix if present
const base64 = body.image.replace(/^data:image\/[^;]+;base64,/, '')
const sizeKB = Math.round(base64.length * 3 / 4 / 1024)
log(`Vision: received image ${sizeKB}KB`)
try {
const result = await extractBarcodes(base64)
return json(res, 200, result)
} catch (e) {
log('Vision barcode error:', e.message)
return json(res, 500, { error: 'Vision extraction failed: ' + e.message })
}
}
const VISION_PROMPT = `You are reading equipment labels from a photo taken by a field technician. The image may be blurry, tilted, at an angle, or have poor lighting.
Your job: find and read ALL identifiers on this device label. This includes:
1. TEXT PRINTED BELOW OR NEAR A BARCODE — this is the barcode value (ignore the barcode lines themselves)
2. Serial numbers — after "S/N", "SN", "Serial", or standalone long alphanumeric strings (8+ chars)
3. MAC addresses — after "MAC", "MAC ID", "MAC Address" — 12 hex chars, with or without colons/dashes
4. Model numbers — after "M/N", "Model", "P/N"
5. Any other identifier: IMEI, GPON SN, PON SN, hardware version
Examples of values to extract:
- 1608K44D9E79FAFF5 (printed under a barcode)
- TPLG-A1B2C3D4 (serial number)
- 04:18:D6:A1:B2:C3 (MAC address)
- ERLite-3 (model number)
- HWTC87654321 (Huawei serial)
Even if blurry, try your BEST to read each character. Return all identifiers found, maximum 3 most important ones (serial and MAC first, model last).`
/**
* Call Gemini Flash Vision to extract barcode/serial number values from an image.
* Uses the native Gemini REST API with JSON response mode.
*/
async function extractBarcodes (base64Image) {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${cfg.AI_MODEL}:generateContent?key=${cfg.AI_API_KEY}`
const payload = {
contents: [{
parts: [
{ text: VISION_PROMPT },
{
inline_data: {
mime_type: 'image/jpeg',
data: base64Image
}
}
]
}],
generationConfig: {
temperature: 0.1,
maxOutputTokens: 1024,
responseMimeType: 'application/json',
responseSchema: {
type: 'object',
properties: {
barcodes: {
type: 'array',
items: { type: 'string' },
maxItems: 3,
}
},
required: ['barcodes'],
},
}
}
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!resp.ok) {
const text = await resp.text()
throw new Error(`Gemini API ${resp.status}: ${text.slice(0, 200)}`)
}
const data = await resp.json()
const candidate = data.candidates?.[0]
const text = (candidate?.content?.parts?.[0]?.text || '').trim()
const finishReason = candidate?.finishReason || 'unknown'
log(`Vision response (finish: ${finishReason}): ${text.slice(0, 300)}`)
// Parse response
let parsed
try {
parsed = JSON.parse(text)
} catch {
const jsonMatch = text.match(/\{[\s\S]*\}/)
if (jsonMatch) {
try { parsed = JSON.parse(jsonMatch[0]) } catch {}
}
}
if (!parsed) {
log('Vision: could not parse response')
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 }
}
// ── POST /vision/equipment ──────────────────────────────────────────────────
// Full equipment label reading — returns structured fields
const EQUIP_PROMPT = `You are reading an equipment label from a photo taken by an ISP field technician.
The device is typically an ONT, ONU, router, modem, decoder, or similar telecom equipment.
Read ALL information visible on the label and return structured JSON.
Look for:
- Brand / Manufacturer: "Hisense", "TP-Link", "Huawei", "ZTE", "Nokia", "Sagemcom", etc.
- Model number: after "Model", "M/N", "P/N", or on a prominent line (e.g. "LTE3415-SHA+", "HG8245H")
- Serial number: after "S/N", "SN", "Serial", or printed under a barcode
- MAC address: after "MAC", "MAC ID", "MAC Address" — 12 hex characters (with or without : or - separators). Return WITHOUT separators, just 12 hex chars.
- GPON SN / PON SN: if present
- Hardware version: "HW Ver", "H/W"
- Any other barcode values visible
Even if the image is blurry or at an angle, try your BEST to read each character.
If a field is not visible, set it to null.`
async function handleEquipment (req, res) {
if (!cfg.AI_API_KEY) return json(res, 500, { error: 'AI_API_KEY not configured' })
const body = await parseBody(req)
if (!body.image) return json(res, 400, { error: 'Missing image field (base64)' })
const base64 = body.image.replace(/^data:image\/[^;]+;base64,/, '')
log(`Vision equipment: received image ${Math.round(base64.length * 3 / 4 / 1024)}KB`)
try {
const url = `https://generativelanguage.googleapis.com/v1beta/models/${cfg.AI_MODEL}:generateContent?key=${cfg.AI_API_KEY}`
const payload = {
contents: [{
parts: [
{ text: EQUIP_PROMPT },
{ inline_data: { mime_type: 'image/jpeg', data: base64 } },
]
}],
generationConfig: {
temperature: 0.1,
maxOutputTokens: 1024,
responseMimeType: 'application/json',
responseSchema: {
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'],
},
}
}
const resp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!resp.ok) {
const text = await resp.text()
throw new Error(`Gemini API ${resp.status}: ${text.slice(0, 200)}`)
}
const data = await resp.json()
const text = (data.candidates?.[0]?.content?.parts?.[0]?.text || '').trim()
log(`Vision equipment response: ${text.slice(0, 400)}`)
let parsed
try { parsed = JSON.parse(text) } catch {
const m = text.match(/\{[\s\S]*\}/)
if (m) try { parsed = JSON.parse(m[0]) } catch {}
}
if (!parsed) return json(res, 200, { serial_number: null, barcodes: [] })
// Clean MAC: remove separators, uppercase
if (parsed.mac_address) {
parsed.mac_address = parsed.mac_address.replace(/[:\-.\s]/g, '').toUpperCase()
}
// Clean serial
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 }