mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(tools): identify historical camera fallback imagery (#1118)
The CCTV frame proxy marks a Street View fallback with X-CCTV-Source, but the portable source dropped the header and get_cctv_snapshot always reported a current view credited to the camera's agency. The source now keeps the recognised provenance, and the tool labels Street View frames as historical Google imagery, refuses synthetic placeholders, and reports unknown provenance without claiming a current view.
This commit is contained in:
1 parent
79176667b0
commit
2fd1b4323c
7 files changed
+256
-10
No files matched your search
@@ -118,9 +118,11 @@ export const findCctvCameras = defineTool({
|
||||
|
||||
export const getCctvSnapshot = defineTool({
|
||||
name: 'get_cctv_snapshot',
|
||||
title: 'Current camera view',
|
||||
title: 'Camera snapshot',
|
||||
description:
|
||||
'The current image from one public camera, by the id from find_cctv_cameras.',
|
||||
'An image from one public camera, by the id from find_cctv_cameras. ' +
|
||||
'Street View fallback imagery is historical, not the current camera view; ' +
|
||||
'unknown frame provenance is explicit.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: { camera_id: { type: 'string', minLength: 1, maxLength: 200 } },
|
||||
@@ -138,7 +140,7 @@ export const getCctvSnapshot = defineTool({
|
||||
`No camera has id ${args.camera_id}; use find_cctv_cameras first`,
|
||||
);
|
||||
const frame = await services.cctv.getFrame(camera, { signal });
|
||||
if (!FRAME_TYPES.has(frame.contentType))
|
||||
if (frame.sourceKind === 'synthetic' || !FRAME_TYPES.has(frame.contentType))
|
||||
throw new ToolError(
|
||||
'unavailable',
|
||||
`Camera ${camera.name || camera.id} has no still image right now`,
|
||||
@@ -149,8 +151,22 @@ export const getCctvSnapshot = defineTool({
|
||||
'The camera image is too large to return',
|
||||
);
|
||||
const name = text(camera.name) || camera.id;
|
||||
const fallback = frame.sourceKind === 'streetview';
|
||||
const current = ['upstream-image', 'hls-pull'].includes(frame.sourceKind);
|
||||
const credit = fallback ? 'Google Street View' : text(camera.credit);
|
||||
const label = fallback
|
||||
? 'Street View fallback for'
|
||||
: current
|
||||
? 'Current view from'
|
||||
: 'Image from';
|
||||
return {
|
||||
summary: `Current view from ${name}${camera.city ? ` in ${camera.city}` : ''}${camera.credit ? `, courtesy of ${camera.credit}` : ''}.`,
|
||||
summary:
|
||||
`${label} ${name}${camera.city ? ` in ${camera.city}` : ''}${credit ? `, courtesy of ${credit}` : ''}.` +
|
||||
(fallback
|
||||
? ' Historical imagery; the current camera frame is unavailable.'
|
||||
: current
|
||||
? ''
|
||||
: ' Frame provenance is unknown.'),
|
||||
data: {
|
||||
view: suggestView(services, {
|
||||
point: { lat: camera.lat, lon: camera.lon },
|
||||
@@ -160,8 +176,10 @@ export const getCctvSnapshot = defineTool({
|
||||
id: camera.id,
|
||||
name,
|
||||
city: text(camera.city),
|
||||
provider: text(camera.provider),
|
||||
credit: text(camera.credit),
|
||||
provider: fallback ? 'Google Street View' : text(camera.provider),
|
||||
credit,
|
||||
source_kind: frame.sourceKind ?? null,
|
||||
current_view: current ? true : fallback ? false : null,
|
||||
mime_type: frame.contentType,
|
||||
bytes: frame.bytes.byteLength,
|
||||
},
|
||||
|
||||
@@ -30,7 +30,13 @@ const cameras = [
|
||||
{ id: 'no-position', name: 'Unplaced' },
|
||||
];
|
||||
const PNG = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const cctv = (frame = { contentType: 'image/png', bytes: PNG }) => ({
|
||||
const cctv = (
|
||||
frame = {
|
||||
contentType: 'image/png',
|
||||
bytes: PNG,
|
||||
sourceKind: 'upstream-image',
|
||||
},
|
||||
) => ({
|
||||
frames: [],
|
||||
async getCatalog() {
|
||||
return { sources: cameras };
|
||||
@@ -104,6 +110,183 @@ test('a snapshot returns the frame as an image with its credit', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('mounted camera fallback provenance survives the source and MCP image result', async (t) => {
|
||||
const { cctvProxy } = await import('../../../server/providers/cctv.js');
|
||||
const { createCctvSource } = await import('../../layers/cctv/source.js');
|
||||
const camera = {
|
||||
id: 'fixture-camera',
|
||||
name: 'Fixture crossing',
|
||||
city: 'Fixture City',
|
||||
provider: 'Fixture transport agency',
|
||||
credit: 'Fixture transport agency',
|
||||
lat: 30.2685,
|
||||
lon: -97.7425,
|
||||
headingDeg: 180,
|
||||
fovDeg: 60,
|
||||
url: 'https://camera.example/current.png',
|
||||
feedType: 'image',
|
||||
};
|
||||
const environment = {
|
||||
CCTV_SOURCES_JSON: JSON.stringify([camera]),
|
||||
CCTV_PREFER_AUSTIN: '0',
|
||||
CCTV_FORCE_AUSTIN: '0',
|
||||
CCTV_SOURCES_FILE: 'missing-cctv-provenance-fixture.json',
|
||||
GOOGLE_MAPS_SERVER_API_KEY: 'fixture-key',
|
||||
GOOGLE_MAPS_API_KEY: '',
|
||||
};
|
||||
for (const [name, value] of Object.entries(environment)) {
|
||||
const previous = process.env[name];
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete process.env[name];
|
||||
else process.env[name] = previous;
|
||||
});
|
||||
process.env[name] = value;
|
||||
}
|
||||
t.mock.method(console, 'warn', () => {});
|
||||
let upstreamAvailable = false;
|
||||
const calls = [];
|
||||
t.mock.method(globalThis, 'fetch', async (raw) => {
|
||||
const url = new URL(raw);
|
||||
calls.push(url.origin);
|
||||
if (url.origin === 'https://camera.example')
|
||||
return upstreamAvailable
|
||||
? new Response(PNG, { headers: { 'Content-Type': 'image/png' } })
|
||||
: new Response(null, { status: 503 });
|
||||
assert.equal(
|
||||
url.origin,
|
||||
'https://maps.googleapis.com',
|
||||
'no actual upstream is contacted',
|
||||
);
|
||||
assert.equal(url.searchParams.get('key'), 'fixture-key');
|
||||
return new Response(PNG, { headers: { 'Content-Type': 'image/png' } });
|
||||
});
|
||||
let middleware;
|
||||
cctvProxy().configureServer({
|
||||
middlewares: {
|
||||
use(route, handler) {
|
||||
assert.equal(route, '/api/cctv');
|
||||
middleware = handler;
|
||||
},
|
||||
},
|
||||
});
|
||||
const source = createCctvSource({
|
||||
fetchImpl: async (path) => {
|
||||
assert.ok(path.startsWith('/api/cctv/'));
|
||||
const response = {
|
||||
writeHead(status, headers) {
|
||||
this.status = status;
|
||||
this.headers = headers;
|
||||
},
|
||||
end(body) {
|
||||
this.body = body;
|
||||
},
|
||||
};
|
||||
await middleware(
|
||||
{ url: path.slice('/api/cctv'.length), method: 'GET', headers: {} },
|
||||
response,
|
||||
);
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
});
|
||||
},
|
||||
});
|
||||
const catalog = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { cctv: source },
|
||||
});
|
||||
const fallback = await catalog.call('get_cctv_snapshot', {
|
||||
camera_id: camera.id,
|
||||
});
|
||||
assert.equal(fallback.data.source_kind, 'streetview');
|
||||
assert.equal(fallback.data.current_view, false);
|
||||
assert.equal(fallback.data.credit, 'Google Street View');
|
||||
assert.match(fallback.summary, /Street View fallback/);
|
||||
assert.match(fallback.summary, /Historical imagery/);
|
||||
assert.doesNotMatch(
|
||||
fallback.summary,
|
||||
/Current view|courtesy of Fixture transport agency/,
|
||||
);
|
||||
assert.equal(
|
||||
fallback.images.length,
|
||||
1,
|
||||
'the historical image stays available with its caveat',
|
||||
);
|
||||
assert.deepEqual(calls, [
|
||||
'https://camera.example',
|
||||
'https://maps.googleapis.com',
|
||||
]);
|
||||
upstreamAvailable = true;
|
||||
const current = await catalog.call('get_cctv_snapshot', {
|
||||
camera_id: camera.id,
|
||||
});
|
||||
assert.equal(current.data.source_kind, 'upstream-image');
|
||||
assert.equal(current.data.current_view, true);
|
||||
assert.equal(current.data.credit, camera.credit);
|
||||
assert.match(current.summary, /^Current view from/);
|
||||
const server = createMcpServer({ catalog, name: 'test', version: '1' });
|
||||
upstreamAvailable = false;
|
||||
const result = await server.handle({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'tools/call',
|
||||
params: { name: 'get_cctv_snapshot', arguments: { camera_id: camera.id } },
|
||||
});
|
||||
assert.ok(
|
||||
result.result.content.some(
|
||||
(entry) => entry.type === 'text' && /Historical imagery/.test(entry.text),
|
||||
),
|
||||
);
|
||||
assert.ok(result.result.content.some((entry) => entry.type === 'image'));
|
||||
process.env.GOOGLE_MAPS_SERVER_API_KEY = '';
|
||||
const placeholder = await source.getFrame(camera);
|
||||
assert.equal(placeholder.sourceKind, 'synthetic');
|
||||
assert.equal(placeholder.contentType, 'image/svg+xml');
|
||||
await assert.rejects(
|
||||
catalog.call('get_cctv_snapshot', { camera_id: camera.id }),
|
||||
(error) => error.code === 'unavailable',
|
||||
);
|
||||
const unavailable = await server.handle({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'tools/call',
|
||||
params: { name: 'get_cctv_snapshot', arguments: { camera_id: camera.id } },
|
||||
});
|
||||
assert.equal(unavailable.result.isError, true);
|
||||
assert.ok(
|
||||
!unavailable.result.content.some((entry) => entry.type === 'image'),
|
||||
);
|
||||
});
|
||||
|
||||
test('snapshot placeholders are refused and unknown frame provenance is explicit', async () => {
|
||||
const synthetic = composeCatalog({
|
||||
tools: coreTools,
|
||||
services: {
|
||||
cctv: cctv({
|
||||
contentType: 'image/png',
|
||||
bytes: PNG,
|
||||
sourceKind: 'synthetic',
|
||||
}),
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
synthetic.call('get_cctv_snapshot', { camera_id: 'austin-1' }),
|
||||
(error) => error.code === 'unavailable',
|
||||
);
|
||||
const unknown = await composeCatalog({
|
||||
tools: coreTools,
|
||||
services: { cctv: cctv({ contentType: 'image/png', bytes: PNG }) },
|
||||
}).call('get_cctv_snapshot', { camera_id: 'austin-1' });
|
||||
assert.equal(unknown.data.current_view, null);
|
||||
assert.equal(
|
||||
unknown.data.credit,
|
||||
cameras[0].credit,
|
||||
'catalog attribution is preserved',
|
||||
);
|
||||
assert.match(unknown.summary, /provenance is unknown/);
|
||||
assert.doesNotMatch(unknown.summary, /Current view/);
|
||||
});
|
||||
|
||||
test('unknown cameras, video-only feeds and oversized frames are reported', async () => {
|
||||
const catalog = (frame) =>
|
||||
composeCatalog({ tools: coreTools, services: { cctv: cctv(frame) } });
|
||||
|
||||
Reference in new issue
Block a user