Not a Plant Fehlermeldung

This commit is contained in:
2026-04-17 13:12:36 +02:00
parent 383d8484a6
commit 77b98a3ebf
12 changed files with 831 additions and 195 deletions

View File

@@ -27,13 +27,18 @@ const OPENAI_SCAN_MODEL_CHAIN = parseModelChain(OPENAI_SCAN_MODEL, OPENAI_SCAN_F
const OPENAI_SCAN_MODEL_CHAIN_PRO = parseModelChain(OPENAI_SCAN_MODEL_PRO, OPENAI_SCAN_FALLBACK_MODELS_PRO);
const OPENAI_HEALTH_MODEL_CHAIN = parseModelChain(OPENAI_HEALTH_MODEL, OPENAI_HEALTH_FALLBACK_MODELS);
const getScanModelChain = (plan) => {
return plan === 'pro' ? OPENAI_SCAN_MODEL_CHAIN_PRO : OPENAI_SCAN_MODEL_CHAIN;
};
const clamp = (value, min, max) => {
return Math.min(max, Math.max(min, value));
};
const getScanModelChain = (plan) => {
return plan === 'pro' ? OPENAI_SCAN_MODEL_CHAIN_PRO : OPENAI_SCAN_MODEL_CHAIN;
};
const isReasoningModel = (model) => {
const normalized = String(model || '').toLowerCase();
return normalized.startsWith('gpt-5') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4');
};
const clamp = (value, min, max) => {
return Math.min(max, Math.max(min, value));
};
const toErrorMessage = (error) => {
if (error instanceof Error) return error.message;
@@ -215,11 +220,12 @@ const buildIdentifyPrompt = (language, mode) => {
? '- "name" must be an English common name only. Never return a German or other non-English common name. If no reliable English common name is known, use "botanicalName" as "name" instead of inventing or translating.'
: `- "name" must be strictly written in ${getLanguageLabel(language)}. If a reliable common name in that language is not known, use "botanicalName" as "name" instead of inventing a localized name.`;
return [
`${reviewInstruction}`,
'Return strict JSON only in this shape:',
'{"name":"...","botanicalName":"...","confidence":0.0,"description":"...","careInfo":{"waterIntervalDays":7,"light":"...","temp":"..."}}',
'Rules:',
return [
`${reviewInstruction}`,
'If the image does not clearly show a plant (for example a person, animal, room, furniture, or no identifiable foliage), return {"notAPlant":true} and nothing else.',
'Return strict JSON only in this shape:',
'{"name":"...","botanicalName":"...","confidence":0.0,"description":"...","careInfo":{"waterIntervalDays":7,"light":"...","temp":"..."}}',
'Rules:',
nameLanguageInstruction,
`- "description" and "careInfo.light" must be written in ${getLanguageLabel(language)}.`,
`- "careInfo.light": short light requirement in ${getLanguageLabel(language)} (e.g. "bright indirect light", "full sun", "partial shade"). Must always be a real value, never "Unknown".`,
@@ -279,14 +285,33 @@ const extractMessageContent = (payload) => {
.join('')
.trim();
}
return '';
};
const postChatCompletion = async ({ modelChain, messages, imageUri, temperature }) => {
if (!OPENAI_API_KEY) return null;
if (typeof fetch !== 'function') {
throw new Error('Global fetch is not available in this Node runtime.');
}
return '';
};
const buildRequestBody = ({ model, messages, temperature, maxCompletionTokens }) => {
const body = {
model,
response_format: { type: 'json_object' },
messages,
};
if (typeof temperature === 'number') body.temperature = temperature;
if (isReasoningModel(model)) {
body.reasoning_effort = 'minimal';
body.max_completion_tokens = maxCompletionTokens;
} else {
body.max_tokens = maxCompletionTokens;
}
return body;
};
const postChatCompletion = async ({ modelChain, messages, imageUri, temperature, maxCompletionTokens = 600 }) => {
if (!OPENAI_API_KEY) return null;
if (typeof fetch !== 'function') {
throw new Error('Global fetch is not available in this Node runtime.');
}
const attemptedModels = [];
@@ -295,18 +320,13 @@ const postChatCompletion = async ({ modelChain, messages, imageUri, temperature
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), OPENAI_TIMEOUT_MS);
try {
const body = {
model,
response_format: { type: 'json_object' },
messages,
};
if (typeof temperature === 'number') body.temperature = temperature;
const response = await fetch(OPENAI_CHAT_COMPLETIONS_URL, {
method: 'POST',
headers: {
try {
const body = buildRequestBody({ model, messages, temperature, maxCompletionTokens });
const response = await fetch(OPENAI_CHAT_COMPLETIONS_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${OPENAI_API_KEY}`,
},
@@ -362,15 +382,16 @@ const identifyPlant = async ({ imageUri, language, mode = 'primary', plan = 'fre
role: 'system',
content: 'You are a plant identification assistant. Return strict JSON only.',
},
{
role: 'user',
content: [
{ type: 'text', text: buildIdentifyPrompt(language, mode) },
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
],
},
],
});
{
role: 'user',
content: [
{ type: 'text', text: buildIdentifyPrompt(language, mode) },
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
],
},
],
maxCompletionTokens: 600,
});
if (!completion?.payload) {
return {
@@ -391,19 +412,25 @@ const identifyPlant = async ({ imageUri, language, mode = 'primary', plan = 'fre
}
const parsed = parseContentToJson(content);
if (!parsed) {
console.warn('OpenAI identify returned non-JSON content.', {
model: completion.modelUsed || modelChain[0],
mode,
preview: content.slice(0, 220),
});
return { result: null, modelUsed: completion.modelUsed, attemptedModels: completion.attemptedModels };
}
const normalized = normalizeIdentifyResult(parsed, language);
if (!normalized) {
console.warn('OpenAI identify JSON did not match schema.', {
model: completion.modelUsed || modelChain[0],
if (!parsed) {
console.warn('OpenAI identify returned non-JSON content.', {
model: completion.modelUsed || modelChain[0],
mode,
preview: content.slice(0, 220),
});
return { result: null, modelUsed: completion.modelUsed, attemptedModels: completion.attemptedModels };
}
if (parsed.notAPlant === true) {
const error = new Error('Image does not contain a plant.');
error.code = 'NOT_A_PLANT';
throw error;
}
const normalized = normalizeIdentifyResult(parsed, language);
if (!normalized) {
console.warn('OpenAI identify JSON did not match schema.', {
model: completion.modelUsed || modelChain[0],
mode,
keys: Object.keys(parsed),
});
@@ -422,15 +449,16 @@ const analyzePlantHealth = async ({ imageUri, language, plantContext }) => {
role: 'system',
content: 'You are a plant health diagnosis assistant. Return strict JSON only.',
},
{
role: 'user',
content: [
{ type: 'text', text: buildHealthPrompt(language, plantContext) },
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
],
},
],
});
{
role: 'user',
content: [
{ type: 'text', text: buildHealthPrompt(language, plantContext) },
{ type: 'image_url', image_url: { url: imageUri, detail: 'low' } },
],
},
],
maxCompletionTokens: 800,
});
if (!completion?.payload) {
return {