mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(hud): state word first, layer names last; CJK cap by locale
Follow-up review fixes:
- The HUD line is STATE | text | LAYERS for the AI summary and the
fallback line, so the narrow minimal and cyber layouts show both the
warning and the summary. get_hud_caption builds its caption the same
way. The unused "{state} {layers}" key is pruned from the catalogs.
- The 20-character cap applies only when the request locale is Japanese
or Chinese; other languages keep five words around a Han place name.
- enabledLayers names are localized too, and the instructions name layers
only from enabledLayerLabels, never from feedProvenance or its note.
- The keyless test restores GEV_RATELIMIT_OPENAI_PER_MIN again; comments
and docs updated.
This commit is contained in:
1 parent
f967218a04
commit
7b223d7d7b
17 files changed
+79
-50
No files matched your search
@@ -5257,8 +5257,8 @@ and unreachable upstream (502/504) separately from road geometry.
|
|||||||
- HUD `SUMMARY` readout requests a five-word intelligence-style summary from `/api/openai/hud-summary` (model `OPENAI_HUD_SUMMARY_MODEL`, default `gpt-5-nano`, minimal reasoning).
|
- HUD `SUMMARY` readout requests a five-word intelligence-style summary from `/api/openai/hud-summary` (model `OPENAI_HUD_SUMMARY_MODEL`, default `gpt-5-nano`, minimal reasoning).
|
||||||
- Input is the live basemap label context (place/street/nearby-place labels + enabled layers, the layer names as the user sees them) and `feedProvenance`; the model is instructed not to infer from coordinates.
|
- Input is the live basemap label context (place/street/nearby-place labels + enabled layers, the layer names as the user sees them) and `feedProvenance`; the model is instructed not to infer from coordinates.
|
||||||
- Language (October 2026): the HUD sends `locale`, which is part of the dedupe signature. The proxy keeps a shipped locale (English otherwise) and names the language in the instructions and at the start of the input; Japanese and Chinese get a 20-character phrase instead of five words.
|
- Language (October 2026): the HUD sends `locale`, which is part of the dedupe signature. The proxy keeps a shipped locale (English otherwise) and names the language in the instructions and at the start of the input; Japanese and Chinese get a 20-character phrase instead of five words.
|
||||||
- The reply is structured (`text.format` JSON schema): `{summary, feedState}`, `feedState` being the snapshot state or `none`. For a view that is not nominal, a missing or different `feedState` falls back to the deterministic line. An accepted summary is shown after the HUD's own translated state tag (`STALE LIVE FLIGHTS | …`), which leads so the one-line readout never clips it.
|
- The reply is structured (`text.format` JSON schema): `{summary, feedState}`, `feedState` being the snapshot state or `none`. For a view that is not nominal, a missing or different `feedState` falls back to the deterministic line. An accepted summary and the deterministic line carry the HUD's own translated state: the state word leads and the layer names trail (`STALE | … | LIVE FLIGHTS`), so the one-line readout clips neither the warning nor the text. `get_hud_caption` builds its caption the same way and returns `feedState`.
|
||||||
- Output is sanitized to five words (20 characters for Japanese and Chinese); falls back to the deterministic telemetry summary on error/timeout (5s abort); typewriter animation on update.
|
- Output is sanitized to five words (20 characters when the request locale is Japanese or Chinese, whatever script the words use); falls back to the deterministic telemetry summary on error/timeout (5s abort); typewriter animation on update.
|
||||||
- `partial` is a feed state of its own, ranked between `fallback` and `nominal`, so one partial layer no longer reads as off and partial plus nominal reads partial (HUD, voice view state, `analyst_query`, `brief`, the agent world packet). Its narration note says coverage is incomplete rather than not live.
|
- `partial` is a feed state of its own, ranked between `fallback` and `nominal`, so one partial layer no longer reads as off and partial plus nominal reads partial (HUD, voice view state, `analyst_query`, `brief`, the agent world packet). Its narration note says coverage is incomplete rather than not live.
|
||||||
- A summary asked for before the camera starts moving never paints over the next view: `moveStart` marks the summary dirty and bumps its revision, as `moveEnd` does, aborts the request in flight and stops a reply still typing; neither a reply nor a fallback line (on an error, the 5 s timeout, the abort, or a failed context build) paints once the revision has moved. A dropped request forgets its signature and a timeout counts as a failure, so the next 15 s tick asks again even over the same place. An interrupted typewriter reply also forgets its signature; fully displayed answers remain reusable.
|
- A summary asked for before the camera starts moving never paints over the next view: `moveStart` marks the summary dirty and bumps its revision, as `moveEnd` does, aborts the request in flight and stops a reply still typing; neither a reply nor a fallback line (on an error, the 5 s timeout, the abort, or a failed context build) paints once the revision has moved. A dropped request forgets its signature and a timeout counts as a failure, so the next 15 s tick asks again even over the same place. An interrupted typewriter reply also forgets its signature; fully displayed answers remain reusable.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
HUD_SUMMARY_RESPONSE_FORMAT,
|
HUD_SUMMARY_RESPONSE_FORMAT,
|
||||||
hudSummaryInput,
|
hudSummaryInput,
|
||||||
hudSummaryInstructions,
|
hudSummaryInstructions,
|
||||||
|
hudSummaryIsUnspaced,
|
||||||
keylessHudSummaryResponse,
|
keylessHudSummaryResponse,
|
||||||
} from '../../../src/hudSummaryResponse.js';
|
} from '../../../src/hudSummaryResponse.js';
|
||||||
import { enforceRateLimit, openAiRateLimiter } from './rate-limit.js';
|
import { enforceRateLimit, openAiRateLimiter } from './rate-limit.js';
|
||||||
@@ -23,7 +24,7 @@ function extractOpenAiResponseText(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Parse the summary and normalize the canonical feed state. */
|
/** Parse the summary and normalize the canonical feed state. */
|
||||||
function parseHudSummaryReply(text) {
|
function parseHudSummaryReply(text, locale) {
|
||||||
let reply = null;
|
let reply = null;
|
||||||
try {
|
try {
|
||||||
reply = JSON.parse(text);
|
reply = JSON.parse(text);
|
||||||
@@ -34,12 +35,12 @@ function parseHudSummaryReply(text) {
|
|||||||
? reply.feedState
|
? reply.feedState
|
||||||
: null;
|
: null;
|
||||||
return {
|
return {
|
||||||
summary: toFiveWordHudSummary(reply?.summary),
|
summary: toFiveWordHudSummary(reply?.summary, hudSummaryIsUnspaced(locale)),
|
||||||
feedState: feedState === 'none' ? null : feedState,
|
feedState: feedState === 'none' ? null : feedState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toFiveWordHudSummary(value) {
|
function toFiveWordHudSummary(value, unspaced) {
|
||||||
const words = String(value || '')
|
const words = String(value || '')
|
||||||
.replace(/[^\p{L}\p{N}\s-]/gu, ' ')
|
.replace(/[^\p{L}\p{N}\s-]/gu, ' ')
|
||||||
.trim()
|
.trim()
|
||||||
@@ -48,8 +49,8 @@ function toFiveWordHudSummary(value) {
|
|||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
// Japanese and Chinese have no spaces to count words by: cap characters.
|
// Japanese and Chinese have no spaces to count words by: cap characters.
|
||||||
if (!/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(words))
|
// Other languages keep five words, even around a Han place name.
|
||||||
return words;
|
if (!unspaced) return words;
|
||||||
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
||||||
return [...graphemes.segment(words)]
|
return [...graphemes.segment(words)]
|
||||||
.slice(0, HUD_SUMMARY_MAX_UNSPACED_CHARS)
|
.slice(0, HUD_SUMMARY_MAX_UNSPACED_CHARS)
|
||||||
@@ -117,6 +118,7 @@ async function handleHudSummary(req, res) {
|
|||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
const { summary, feedState } = parseHudSummaryReply(
|
const { summary, feedState } = parseHudSummaryReply(
|
||||||
extractOpenAiResponseText(data),
|
extractOpenAiResponseText(data),
|
||||||
|
context?.locale,
|
||||||
);
|
);
|
||||||
res.statusCode = response.ok && summary ? 200 : response.status || 502;
|
res.statusCode = response.ok && summary ? 200 : response.status || 502;
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
|||||||
+20
-19
@@ -104,22 +104,21 @@ const FEED_STATE_LABELS = Object.freeze({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The summary's feed-state suffix in the active language, e.g.
|
* A summary line with its feed-state warning in the active language, e.g.
|
||||||
* `UNAVAILABLE: LIVE AIS VESSELS`. State ids and layer names stay English in
|
* `UNAVAILABLE | <text> | LIVE AIS VESSELS`. The state word leads so the
|
||||||
* layer data; only this line translates them.
|
* one-line readout never clips it; the layer names trail so they never push
|
||||||
|
* the text out. State ids and layer names stay English in layer data; only
|
||||||
|
* this line translates them.
|
||||||
*/
|
*/
|
||||||
function localizedProvenanceTag(provenance) {
|
function withProvenance(text, provenance) {
|
||||||
if (!provenance) return null;
|
if (!provenance) return text;
|
||||||
const state = FEED_STATE_LABELS[provenance.state]
|
const state = FEED_STATE_LABELS[provenance.state]
|
||||||
? t(FEED_STATE_LABELS[provenance.state])
|
? t(FEED_STATE_LABELS[provenance.state])
|
||||||
: provenance.state.toUpperCase();
|
: provenance.state.toUpperCase();
|
||||||
if (!provenance.names.length) return state;
|
const layers = provenance.names
|
||||||
return t('{state} {layers}', {
|
|
||||||
state,
|
|
||||||
layers: provenance.names
|
|
||||||
.map((name) => localizeLayerName(name).toUpperCase())
|
.map((name) => localizeLayerName(name).toUpperCase())
|
||||||
.join('/'),
|
.join('/');
|
||||||
});
|
return [state, text, layers].filter(Boolean).join(' | ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The name shown for a _viewBand() id. */
|
/** The name shown for a _viewBand() id. */
|
||||||
@@ -764,8 +763,8 @@ export class IntelHUD {
|
|||||||
LOCALIZED_LOCALITY,
|
LOCALIZED_LOCALITY,
|
||||||
);
|
);
|
||||||
|
|
||||||
const provenance = localizedProvenanceTag(
|
const provenance = hudTelemetryProvenance(
|
||||||
hudTelemetryProvenance(this._dataManager?.getAll?.() || []),
|
this._dataManager?.getAll?.() || [],
|
||||||
);
|
);
|
||||||
// ALT, SUN and ONA are sensor codes, as in the corner readouts.
|
// ALT, SUN and ONA are sensor codes, as in the corner readouts.
|
||||||
const line = t(
|
const line = t(
|
||||||
@@ -782,8 +781,7 @@ export class IntelHUD {
|
|||||||
zone: localTag,
|
zone: localTag,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// The feed state leads: the one-line readout clips its end.
|
return withProvenance(line, provenance);
|
||||||
return provenance ? `${provenance} | ${line}` : line;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop a summary still typing out, leaving what it has typed so far. */
|
/** Stop a summary still typing out, leaving what it has typed so far. */
|
||||||
@@ -895,12 +893,11 @@ export class IntelHUD {
|
|||||||
throw new Error(data?.error || `HTTP ${response.status}`);
|
throw new Error(data?.error || `HTTP ${response.status}`);
|
||||||
}
|
}
|
||||||
if (hudSummaryMatchesProvenance(data, context.feedProvenance)) {
|
if (hudSummaryMatchesProvenance(data, context.feedProvenance)) {
|
||||||
// The warning is the HUD's own translated tag, not the model's
|
// The warning is the HUD's own translated state, not the model's words.
|
||||||
// words, and it leads so the one-line readout never clips it.
|
this._lastSummaryText = withProvenance(
|
||||||
const tag = localizedProvenanceTag(
|
data.summary,
|
||||||
provenanceTag(context.feedProvenance),
|
provenanceTag(context.feedProvenance),
|
||||||
);
|
);
|
||||||
this._lastSummaryText = tag ? `${tag} | ${data.summary}` : data.summary;
|
|
||||||
}
|
}
|
||||||
this._setSummaryText(this._lastSummaryText || fallbackText, animate);
|
this._setSummaryText(this._lastSummaryText || fallbackText, animate);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -973,6 +970,10 @@ export class IntelHUD {
|
|||||||
// The names the user sees; feedProvenance keeps the English ids.
|
// The names the user sees; feedProvenance keeps the English ids.
|
||||||
enabledLayerLabels:
|
enabledLayerLabels:
|
||||||
layerContext.enabledLayerLabels.map(localizeLayerName),
|
layerContext.enabledLayerLabels.map(localizeLayerName),
|
||||||
|
enabledLayers: layerContext.enabledLayers.map((layer) => ({
|
||||||
|
...layer,
|
||||||
|
name: localizeLayerName(layer.name),
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export const HUD_SUMMARY_INSTRUCTIONS = [
|
|||||||
"Write one concise intelligence-HUD summary for God's Eye View.",
|
"Write one concise intelligence-HUD summary for God's Eye View.",
|
||||||
'Use only the supplied place, street, nearby-place, enabled-layer labels, and feedProvenance.',
|
'Use only the supplied place, street, nearby-place, enabled-layer labels, and feedProvenance.',
|
||||||
'Prefer the clearest named place and include a relevant enabled layer only when useful.',
|
'Prefer the clearest named place and include a relevant enabled layer only when useful.',
|
||||||
|
'Name layers only as in enabledLayerLabels; never copy layer names or state words from feedProvenance or its note.',
|
||||||
'Set feedState to feedProvenance.overall exactly as given, or none when it is null.',
|
'Set feedState to feedProvenance.overall exactly as given, or none when it is null.',
|
||||||
'If feedProvenance.overall is partial, do not present the coverage as complete; if it is any other state but nominal, do not present the view as live or healthy.',
|
'If feedProvenance.overall is partial, do not present the coverage as complete; if it is any other state but nominal, do not present the view as live or healthy.',
|
||||||
'Do not infer from coordinates or invent a place or feed state.',
|
'Do not infer from coordinates or invent a place or feed state.',
|
||||||
@@ -95,7 +96,6 @@ export function hudSummaryLocale(value) {
|
|||||||
return normalizeLocale(value) || DEFAULT_LOCALE;
|
return normalizeLocale(value) || DEFAULT_LOCALE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The language a request's summary is written in, named in English. */
|
|
||||||
// Names Intl.DisplayNames gets wrong for the shipped catalogs: pt is the
|
// Names Intl.DisplayNames gets wrong for the shipped catalogs: pt is the
|
||||||
// Brazilian variant, and zh-Hant is the zh-CN wording in Traditional script.
|
// Brazilian variant, and zh-Hant is the zh-CN wording in Traditional script.
|
||||||
const SUMMARY_LANGUAGES = Object.freeze({
|
const SUMMARY_LANGUAGES = Object.freeze({
|
||||||
@@ -107,6 +107,12 @@ const SUMMARY_LANGUAGES = Object.freeze({
|
|||||||
/** Scripts written without spaces, where five words do not apply. */
|
/** Scripts written without spaces, where five words do not apply. */
|
||||||
const UNSPACED_LOCALES = /^(?:ja|zh)(?:-|$)/;
|
const UNSPACED_LOCALES = /^(?:ja|zh)(?:-|$)/;
|
||||||
|
|
||||||
|
/** Whether the summary for this locale is capped in characters, not words. */
|
||||||
|
export function hudSummaryIsUnspaced(locale) {
|
||||||
|
return UNSPACED_LOCALES.test(hudSummaryLocale(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The language a request's summary is written in, named in English. */
|
||||||
function summaryLanguage(tag) {
|
function summaryLanguage(tag) {
|
||||||
if (SUMMARY_LANGUAGES[tag]) return SUMMARY_LANGUAGES[tag];
|
if (SUMMARY_LANGUAGES[tag]) return SUMMARY_LANGUAGES[tag];
|
||||||
try {
|
try {
|
||||||
@@ -125,7 +131,7 @@ export function hudSummaryInstructions(locale) {
|
|||||||
const tag = hudSummaryLocale(locale);
|
const tag = hudSummaryLocale(locale);
|
||||||
if (tag === DEFAULT_LOCALE) return HUD_SUMMARY_INSTRUCTIONS;
|
if (tag === DEFAULT_LOCALE) return HUD_SUMMARY_INSTRUCTIONS;
|
||||||
const language = summaryLanguage(tag);
|
const language = summaryLanguage(tag);
|
||||||
const length = UNSPACED_LOCALES.test(tag)
|
const length = hudSummaryIsUnspaced(tag)
|
||||||
? ` Instead of five words, write one phrase of at most ${HUD_SUMMARY_MAX_UNSPACED_CHARS} characters.`
|
? ` Instead of five words, write one phrase of at most ${HUD_SUMMARY_MAX_UNSPACED_CHARS} characters.`
|
||||||
: '';
|
: '';
|
||||||
return `${HUD_SUMMARY_INSTRUCTIONS} Write the summary in ${language} (${tag}), not English, giving places their usual ${language} names and layers the names in enabledLayerLabels; feedState stays the English value.${length}`;
|
return `${HUD_SUMMARY_INSTRUCTIONS} Write the summary in ${language} (${tag}), not English, giving places their usual ${language} names and layers the names in enabledLayerLabels; feedState stays the English value.${length}`;
|
||||||
@@ -172,7 +178,7 @@ export function hudSummaryLayerContext(layers = [], options) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The feed-state tag for the HUD telemetry suffix when an enabled feed is not
|
* The warning for the HUD telemetry line when an enabled feed is not
|
||||||
* nominal.
|
* nominal.
|
||||||
* @param {Array<object>} [layers]
|
* @param {Array<object>} [layers]
|
||||||
* @param {{ now?: number }} [options]
|
* @param {{ now?: number }} [options]
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ test('the installed keyless HUD route stays successful after the voice quota is
|
|||||||
} finally {
|
} finally {
|
||||||
if (previousKey === undefined) delete process.env.OPENAI_API_KEY;
|
if (previousKey === undefined) delete process.env.OPENAI_API_KEY;
|
||||||
else process.env.OPENAI_API_KEY = previousKey;
|
else process.env.OPENAI_API_KEY = previousKey;
|
||||||
|
if (previousLimit === undefined)
|
||||||
|
delete process.env.GEV_RATELIMIT_OPENAI_PER_MIN;
|
||||||
|
else process.env.GEV_RATELIMIT_OPENAI_PER_MIN = previousLimit;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,10 +266,20 @@ test('the HUD sends its UI locale inside the context it signs for dedupe', () =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the state tag leads the one-line summary so clipping cannot hide it', () => {
|
test('the state word leads and the layer names trail the one-line summary', () => {
|
||||||
const hud = readFileSync(new URL('./hud.js', import.meta.url), 'utf8');
|
const hud = readFileSync(new URL('./hud.js', import.meta.url), 'utf8');
|
||||||
assert.match(hud, /tag \? `\$\{tag\} \| \$\{data\.summary\}`/);
|
assert.match(
|
||||||
assert.match(hud, /provenance \? `\$\{provenance\} \| \$\{line\}`/);
|
hud,
|
||||||
|
/return \[state, text, layers\]\.filter\(Boolean\)\.join\(' \| '\);/,
|
||||||
|
);
|
||||||
|
assert.match(hud, /withProvenance\(\s*data\.summary,/);
|
||||||
|
assert.match(hud, /return withProvenance\(line, provenance\);/);
|
||||||
|
// The model sees only the layer names the user sees.
|
||||||
|
assert.match(hud, /name: localizeLayerName\(layer\.name\)/);
|
||||||
|
assert.match(
|
||||||
|
HUD_SUMMARY_INSTRUCTIONS,
|
||||||
|
/Name layers only as in enabledLayerLabels/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shipped catalogs get their exact language, and CJK a character limit', () => {
|
test('shipped catalogs get their exact language, and CJK a character limit', () => {
|
||||||
@@ -370,6 +383,20 @@ test('a Japanese summary is capped in characters, not words', async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const [locale, summary] of [
|
||||||
|
['en', 'Shibuya 渋谷 live flights overhead'],
|
||||||
|
['es', 'Vuelos sobre 東京駅 datos antiguos'],
|
||||||
|
['ru', 'Башня Тайбэй 101 台北101 рейсы'],
|
||||||
|
]) {
|
||||||
|
test(`a ${locale} summary with a Han place name keeps its five words`, async () => {
|
||||||
|
const { response } = await postHudSummary(
|
||||||
|
{ locale, feedProvenance: { overall: 'stale' } },
|
||||||
|
JSON.stringify({ summary, feedState: 'stale' }),
|
||||||
|
);
|
||||||
|
assert.equal(response.body.summary, summary);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test('a reply that skips the format carries no summary and no state', async () => {
|
test('a reply that skips the format carries no summary and no state', async () => {
|
||||||
const { response } = await postHudSummary(
|
const { response } = await postHudSummary(
|
||||||
{ locale: 'ru', feedProvenance: { overall: 'stale' } },
|
{ locale: 'ru', feedProvenance: { overall: 'stale' } },
|
||||||
|
|||||||
@@ -1125,7 +1125,6 @@
|
|||||||
"OCEANIA": "OZEANIEN",
|
"OCEANIA": "OZEANIEN",
|
||||||
"NORTHERN OCEANIC GRID": "NÖRDLICHES OZEANRASTER",
|
"NORTHERN OCEANIC GRID": "NÖRDLICHES OZEANRASTER",
|
||||||
"SOUTHERN OCEANIC GRID": "SÜDLICHES OZEANRASTER",
|
"SOUTHERN OCEANIC GRID": "SÜDLICHES OZEANRASTER",
|
||||||
"{state} {layers}": "{state} {layers}",
|
|
||||||
"view band|STREET": "STRASSE",
|
"view band|STREET": "STRASSE",
|
||||||
"view band|CITY": "STADT",
|
"view band|CITY": "STADT",
|
||||||
"view band|METRO": "GROSSRAUM",
|
"view band|METRO": "GROSSRAUM",
|
||||||
|
|||||||
@@ -2025,7 +2025,6 @@
|
|||||||
"Globe field image unavailable": "Imagen del campo en el globo no disponible",
|
"Globe field image unavailable": "Imagen del campo en el globo no disponible",
|
||||||
"status chip|UNAVAILABLE": "NO DISP.",
|
"status chip|UNAVAILABLE": "NO DISP.",
|
||||||
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Contiene referencias a contenido registrado o medios vinculados. Esos archivos externos no se incluyen en un paquete de escena. Sus avisos originales siguen vigentes.",
|
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Contiene referencias a contenido registrado o medios vinculados. Esos archivos externos no se incluyen en un paquete de escena. Sus avisos originales siguen vigentes.",
|
||||||
"{state} {layers}": "{state}: {layers}",
|
|
||||||
"Selected Space Mission": "Misión espacial seleccionada",
|
"Selected Space Mission": "Misión espacial seleccionada",
|
||||||
"{duration}s + {hold}s": "{duration} s + {hold} s",
|
"{duration}s + {hold}s": "{duration} s + {hold} s",
|
||||||
"Configured Source": "Fuente configurada",
|
"Configured Source": "Fuente configurada",
|
||||||
|
|||||||
@@ -332,7 +332,6 @@
|
|||||||
"STALE": "OBSOLÈTE",
|
"STALE": "OBSOLÈTE",
|
||||||
"PARTIAL": "PARTIEL",
|
"PARTIAL": "PARTIEL",
|
||||||
"FALLBACK": "SECOURS",
|
"FALLBACK": "SECOURS",
|
||||||
"{state} {layers}": "{state} : {layers}",
|
|
||||||
"view band|STREET": "RUE",
|
"view band|STREET": "RUE",
|
||||||
"view band|CITY": "VILLE",
|
"view band|CITY": "VILLE",
|
||||||
"view band|METRO": "MÉTROPOLE",
|
"view band|METRO": "MÉTROPOLE",
|
||||||
|
|||||||
@@ -332,7 +332,6 @@
|
|||||||
"STALE": "OBSOLETO",
|
"STALE": "OBSOLETO",
|
||||||
"PARTIAL": "PARZIALE",
|
"PARTIAL": "PARZIALE",
|
||||||
"FALLBACK": "RISERVA",
|
"FALLBACK": "RISERVA",
|
||||||
"{state} {layers}": "{state} {layers}",
|
|
||||||
"view band|STREET": "STRADA",
|
"view band|STREET": "STRADA",
|
||||||
"view band|CITY": "CITTÀ",
|
"view band|CITY": "CITTÀ",
|
||||||
"view band|METRO": "METRO",
|
"view band|METRO": "METRO",
|
||||||
|
|||||||
@@ -326,7 +326,6 @@
|
|||||||
"STALE": "更新遅延",
|
"STALE": "更新遅延",
|
||||||
"PARTIAL": "部分的",
|
"PARTIAL": "部分的",
|
||||||
"FALLBACK": "フォールバック",
|
"FALLBACK": "フォールバック",
|
||||||
"{state} {layers}": "{state} {layers}",
|
|
||||||
"view band|STREET": "街路",
|
"view band|STREET": "街路",
|
||||||
"view band|CITY": "市街",
|
"view band|CITY": "市街",
|
||||||
"view band|METRO": "都市圏",
|
"view band|METRO": "都市圏",
|
||||||
|
|||||||
@@ -332,7 +332,6 @@
|
|||||||
"STALE": "DESATUALIZADO",
|
"STALE": "DESATUALIZADO",
|
||||||
"PARTIAL": "PARCIAL",
|
"PARTIAL": "PARCIAL",
|
||||||
"FALLBACK": "ALTERNATIVO",
|
"FALLBACK": "ALTERNATIVO",
|
||||||
"{state} {layers}": "{state}: {layers}",
|
|
||||||
"view band|STREET": "RUA",
|
"view band|STREET": "RUA",
|
||||||
"view band|CITY": "CIDADE",
|
"view band|CITY": "CIDADE",
|
||||||
"view band|METRO": "METRÓPOLE",
|
"view band|METRO": "METRÓPOLE",
|
||||||
|
|||||||
@@ -2059,7 +2059,6 @@
|
|||||||
"Globe field image unavailable": "Изображение поля на глобусе недоступно",
|
"Globe field image unavailable": "Изображение поля на глобусе недоступно",
|
||||||
"status chip|UNAVAILABLE": "НЕДОСТ.",
|
"status chip|UNAVAILABLE": "НЕДОСТ.",
|
||||||
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Используется зарегистрированный контент или связанные медиафайлы. Эти внешние файлы не входят в архив сцены. Их исходные условия использования по-прежнему действуют.",
|
"Uses registered content or linked media. Those external files are not included in a scene bundle. Their original notices still apply.": "Используется зарегистрированный контент или связанные медиафайлы. Эти внешние файлы не входят в архив сцены. Их исходные условия использования по-прежнему действуют.",
|
||||||
"{state} {layers}": "{state}: {layers}",
|
|
||||||
"Selected Space Mission": "Выбранная космическая миссия",
|
"Selected Space Mission": "Выбранная космическая миссия",
|
||||||
"{duration}s + {hold}s": "{duration} с + {hold} с",
|
"{duration}s + {hold}s": "{duration} с + {hold} с",
|
||||||
"Configured Source": "Настроенный источник",
|
"Configured Source": "Настроенный источник",
|
||||||
|
|||||||
@@ -335,7 +335,6 @@
|
|||||||
"STALE": "ЗАСТАРІЛО",
|
"STALE": "ЗАСТАРІЛО",
|
||||||
"PARTIAL": "ЧАСТКОВО",
|
"PARTIAL": "ЧАСТКОВО",
|
||||||
"FALLBACK": "РЕЗЕРВ",
|
"FALLBACK": "РЕЗЕРВ",
|
||||||
"{state} {layers}": "{state}: {layers}",
|
|
||||||
"view band|STREET": "ВУЛИЦЯ",
|
"view band|STREET": "ВУЛИЦЯ",
|
||||||
"view band|CITY": "МІСТО",
|
"view band|CITY": "МІСТО",
|
||||||
"view band|METRO": "АГЛОМЕРАЦІЯ",
|
"view band|METRO": "АГЛОМЕРАЦІЯ",
|
||||||
|
|||||||
@@ -326,7 +326,6 @@
|
|||||||
"STALE": "过期",
|
"STALE": "过期",
|
||||||
"PARTIAL": "部分",
|
"PARTIAL": "部分",
|
||||||
"FALLBACK": "备用",
|
"FALLBACK": "备用",
|
||||||
"{state} {layers}": "{state} {layers}",
|
|
||||||
"view band|STREET": "街道",
|
"view band|STREET": "街道",
|
||||||
"view band|CITY": "城市",
|
"view band|CITY": "城市",
|
||||||
"view band|METRO": "都市圈",
|
"view band|METRO": "都市圈",
|
||||||
|
|||||||
@@ -326,7 +326,6 @@
|
|||||||
"STALE": "過期",
|
"STALE": "過期",
|
||||||
"PARTIAL": "部分",
|
"PARTIAL": "部分",
|
||||||
"FALLBACK": "備用",
|
"FALLBACK": "備用",
|
||||||
"{state} {layers}": "{state} {layers}",
|
|
||||||
"view band|STREET": "街道",
|
"view band|STREET": "街道",
|
||||||
"view band|CITY": "城市",
|
"view band|CITY": "城市",
|
||||||
"view band|METRO": "都市圈",
|
"view band|METRO": "都市圈",
|
||||||
|
|||||||
@@ -256,14 +256,16 @@ export const getHudCaption = defineTool({
|
|||||||
const words = response?.data?.summary;
|
const words = response?.data?.summary;
|
||||||
if (!response?.ok || typeof words !== 'string' || !words)
|
if (!response?.ok || typeof words !== 'string' || !words)
|
||||||
throw new ToolError('unavailable', 'The caption service did not answer');
|
throw new ToolError('unavailable', 'The caption service did not answer');
|
||||||
// As on the HUD: the warning is the app's own state tag, and a reply that
|
// As on the HUD: the app's own state leads and its layers trail, and a
|
||||||
// names another state than the app's keeps only the place.
|
// reply that names another state than the app's keeps only the place.
|
||||||
const tag = provenanceTag(feedProvenance);
|
const tag = provenanceTag(feedProvenance);
|
||||||
const text = hudSummaryMatchesProvenance(response.data, feedProvenance)
|
const text = hudSummaryMatchesProvenance(response.data, feedProvenance)
|
||||||
? words
|
? words
|
||||||
: area.label;
|
: area.label;
|
||||||
const caption = tag
|
const caption = tag
|
||||||
? `${text} | ${[tag.state, ...tag.names].join(' ').toUpperCase()}`
|
? [tag.state.toUpperCase(), text, tag.names.join('/').toUpperCase()]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' | ')
|
||||||
: text;
|
: text;
|
||||||
return {
|
return {
|
||||||
summary: caption,
|
summary: caption,
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { composeCatalog, coreTools } from '../index.js';
|
import { composeCatalog, coreTools } from '../index.js';
|
||||||
|
|
||||||
// The proxy returns the feed state as its own field ({ summary, feedState });
|
// get_hud_caption shows the app's own feed state, not the model's words.
|
||||||
// the five words no longer carry STALE, so get_hud_caption adds the app's own
|
|
||||||
// state tag and keeps only the place when the reply names another state.
|
|
||||||
async function staleCaption(reply) {
|
async function staleCaption(reply) {
|
||||||
const conditions = {
|
const conditions = {
|
||||||
observedAt: '2026-10-01T19:15:00.000Z',
|
observedAt: '2026-10-01T19:15:00.000Z',
|
||||||
@@ -53,7 +51,10 @@ test('get_hud_caption keeps the canonical feed state of a stale view', async ()
|
|||||||
feedState: 'stale',
|
feedState: 'stale',
|
||||||
});
|
});
|
||||||
assert.equal(data.feedState, 'stale');
|
assert.equal(data.feedState, 'stale');
|
||||||
assert.equal(data.caption, 'Austin weather across the area | STALE WEATHER');
|
assert.equal(
|
||||||
|
data.caption,
|
||||||
|
'STALE | Austin weather across the area | WEATHER',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('get_hud_caption drops words whose feed state is not the app one', async () => {
|
test('get_hud_caption drops words whose feed state is not the app one', async () => {
|
||||||
@@ -62,5 +63,5 @@ test('get_hud_caption drops words whose feed state is not the app one', async ()
|
|||||||
feedState: 'nominal',
|
feedState: 'nominal',
|
||||||
});
|
});
|
||||||
assert.equal(data.feedState, 'stale');
|
assert.equal(data.feedState, 'stale');
|
||||||
assert.equal(data.caption, 'Austin, Texas | STALE WEATHER');
|
assert.equal(data.caption, 'STALE | Austin, Texas | WEATHER');
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user