HomePilot / frontend /src /ui /lib /visionError.ts
HomePilot Deploy Bot
chore(hf): sync HomePilot to HF Space
2c440a8
Raw History Blame Contribute Delete
4.49 kB
/**
* Turning a vision failure into a sentence somebody can act on.
*
* What the chat used to show, verbatim, when a local model crashed:
*
* Image analysis failed: HTTP 422 Unprocessable Entity: {"ok":false,"error":"Ollama HTTP
* 500: {\"error\":\"model runner has unexpectedly stopped, this may be due to resource
* limitations or an internal error, check ollama server logs for details\"}",
* "error_code":"","analysis_text":"","meta":{...}}. Make sure a multimodal model is
* installed (e.g. ollama pull moondream).
*
* Two separate failures there, and the second is the worse one.
*
* **It dumps JSON at a person.** A transport status, a nested provider status and a serialised
* envelope, none of which is the thing that went wrong. The backend already writes a sentence
* for exactly this β€” `error` β€” and it was buried inside the blob rather than shown.
*
* **The advice was wrong.** Moondream *was* installed; the user was looking at it in the model
* list while being told to install it. That sentence was appended unconditionally, so the one
* piece of advice on screen pointed away from the actual problem β€” a model too large for the
* machine's memory β€” and sent them to check something that was already true.
*
* So the rule here is: say what the backend said, and add advice only when the error code says
* that advice applies. Where there is no code to go on, add nothing rather than guess.
*/
/** The failure envelope the backend returns. Every field is optional in practice. */
export type VisionFailure = {
error?: string;
error_code?: string;
meta?: { model?: string; fallback_from?: string } | null;
};
/** Pull the backend's own envelope out of a thrown transport error, if it is in there. */
export function parseVisionFailure(raw: unknown): VisionFailure | null {
const text = typeof raw === "string" ? raw : (raw as any)?.message;
if (typeof text !== "string") return null;
// The envelope arrives embedded in a transport message, so find the JSON rather than
// assuming the whole string is JSON.
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
if (start < 0 || end <= start) return null;
try {
const parsed = JSON.parse(text.slice(start, end + 1));
if (parsed && typeof parsed === "object") return parsed as VisionFailure;
} catch {
/* not JSON: fall through to the raw text, which is all there is */
}
return null;
}
/**
* Advice, keyed on what actually failed. `''` when there is nothing honest to add.
*
* `no_vision_model` is the only case where "install one" is true, and it is the only case that
* says it.
*/
function adviceFor(code: string): string {
switch (code) {
case "model_runner_stopped":
return "Try a smaller multimodal model in Settings, or close some applications and try again.";
case "empty_model_response":
return "That model had nothing to say about this image. A larger multimodal model in Settings will usually do better.";
case "image_too_large":
return "A screenshot of a single screen, or a smaller copy, will work.";
case "no_vision_model":
return "Install one first, for example: ollama pull gemma3:4b";
default:
return "";
}
}
/**
* The line to show in the chat.
*
* Prefers the backend's own sentence, which is written for a person and names the model that
* failed. Falls back to the transport message only when there is nothing better β€” and even
* then says nothing further, because a guess about the cause is what caused this.
*/
export function visionErrorMessage(raw: unknown): string {
const failure = parseVisionFailure(raw);
const backendSentence = String(failure?.error || "").trim();
const transport =
typeof raw === "string" ? raw : String((raw as any)?.message || "").trim();
// A nested envelope can leave JSON inside `error` too. If what we have still looks like a
// blob, it is not a sentence, and showing it is the bug rather than the fix.
const looksLikeJson = (s: string) =>
s.startsWith("{") || s.includes('"error_code"');
const sentence =
backendSentence && !looksLikeJson(backendSentence)
? backendSentence
: transport && !looksLikeJson(transport)
? transport
: "That image couldn't be analysed.";
const advice = adviceFor(String(failure?.error_code || ""));
const head = sentence.replace(/\s*\.\s*$/, "");
return advice ? `${head}. ${advice}` : `${head}.`;
}