v0.2 - 19 features: comparator, allergies, gamification, shopping list, achievements, stats, profile, share, bottom nav

This commit is contained in:
2026-02-10 18:52:42 -03:00
parent e8f4788a33
commit ecdd7546d3
33 changed files with 2105 additions and 309 deletions

View File

@@ -19,7 +19,7 @@ Responda SEMPRE em JSON válido com esta estrutura exata:
"proteinas": "<valor, ex: 0g>",
"carboidratos": "<valor, ex: 35g>"
},
"nutrition_verdict": "<frase curta sobre o perfil nutricional, ex: Alto em açúcar, zero fibras>",
"nutrition_verdict": "<frase curta sobre o perfil nutricional>",
"ingredients": [
{
"name": "<nome no rótulo>",
@@ -31,32 +31,50 @@ Responda SEMPRE em JSON válido com esta estrutura exata:
],
"recipe": {
"title": "<nome da receita saudável>",
"description": "<descrição curta, 1-2 frases>",
"description": "<descrição curta>",
"prep_time": "<tempo de preparo>",
"calories": "<calorias aproximadas da receita>",
"ingredients_list": ["<ingrediente 1>", "<ingrediente 2>", ...],
"steps": ["<passo 1>", "<passo 2>", ...],
"tip": "<dica nutricional relacionada>"
}
"calories": "<calorias aproximadas>",
"ingredients_list": ["<ingrediente 1>", ...],
"steps": ["<passo 1>", ...],
"tip": "<dica nutricional>"
},
"substitutions": [
{
"name": "<nome do produto alternativo>",
"brand": "<marca sugerida ou genérica>",
"reason": "<por que é melhor, 1 frase>",
"estimated_score": <int 0-100>
}
]
}
REGRAS PARA SUBSTITUIÇÕES:
- SOMENTE inclua "substitutions" se o score for < 50
- Sugira 3 produtos REAIS brasileiros que sejam alternativas mais saudáveis
- Se score >= 50, retorne "substitutions": null
Para a receita:
- Se o produto for SAUDÁVEL (score > 70): sugira uma receita usando o produto
- Se o produto for RUIM (score <= 70): sugira uma alternativa saudável que substitua o produto
- A receita deve ser simples, rápida e brasileira quando possível
- Se score > 70: sugira receita usando o produto
- Se score <= 70: sugira alternativa saudável
Critérios para o score:
- 90-100: Alimento natural, minimamente processado, sem aditivos
- 70-89: Bom, com poucos aditivos ou processamento leve
- 50-69: Médio, processado mas aceitável com moderação
- 30-49: Ruim, ultraprocessado com vários aditivos
- 0-29: Péssimo, alto em açúcar/sódio/gordura trans, muitos aditivos
- 90-100: Natural, minimamente processado
- 70-89: Bom, poucos aditivos
- 50-69: Médio, processado mas aceitável
- 30-49: Ruim, ultraprocessado
- 0-29: Péssimo, alto em açúcar/sódio/gordura trans
Use os dados nutricionais fornecidos quando disponíveis. Estime quando não disponíveis.
Considere Nutri-Score, classificação NOVA, e ingredientes problemáticos.
Seja direto e honesto. Use linguagem simples."""
Use linguagem simples e direta."""
async def analyze_product(product_data: dict) -> dict:
HEALTH_PROFILE_PROMPTS = {
"normal": "",
"crianca": "\n⚠️ PERFIL: CRIANÇA. Alerte sobre: excesso de açúcar, corantes artificiais, cafeína, sódio alto. Seja mais rigoroso com ultraprocessados.",
"gestante": "\n⚠️ PERFIL: GESTANTE. Alerte sobre: cafeína, adoçantes artificiais, sódio excessivo, conservantes. Priorize folato, ferro, cálcio.",
"diabetico": "\n⚠️ PERFIL: DIABÉTICO. Alerte sobre: açúcares, carboidratos refinados, índice glicêmico alto. Valorize fibras e proteínas.",
"hipertenso": "\n⚠️ PERFIL: HIPERTENSO. Alerte sobre: sódio (>400mg é ALTO), glutamato monossódico, conservantes com sódio. Limite: <2g sódio/dia.",
}
async def analyze_product(product_data: dict, user_context: dict = None) -> dict:
if not settings.OPENAI_API_KEY:
return _mock_analysis(product_data)
@@ -65,21 +83,30 @@ async def analyze_product(product_data: dict) -> dict:
nutrition_info = product_data.get('nutrition', {})
nutrition_str = json.dumps(nutrition_info, ensure_ascii=False) if nutrition_info else 'Não disponível'
system = SYSTEM_PROMPT
extra = ""
if user_context:
hp = user_context.get("health_profile", "normal")
system += HEALTH_PROFILE_PROMPTS.get(hp, "")
allergies = user_context.get("allergies", [])
if allergies:
extra = f"\n\n⚠️ ALERGIAS DO USUÁRIO: {', '.join(allergies)}. Destaque QUALQUER ingrediente que possa conter esses alérgenos."
user_msg = f"""Produto: {product_data.get('name', 'Desconhecido')}
Marca: {product_data.get('brand', '')}
Categoria: {product_data.get('category', '')}
Ingredientes: {product_data.get('ingredients_text', 'Não disponível')}
Nutri-Score: {product_data.get('nutri_score', 'N/A')}
NOVA: {product_data.get('nova_group', 'N/A')}
Dados Nutricionais: {nutrition_str}
Dados Nutricionais: {nutrition_str}{extra}
Analise este produto com informações nutricionais detalhadas e sugira uma receita."""
Analise este produto completo."""
try:
resp = await client.chat.completions.create(
model=settings.OPENAI_MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "system", "content": system},
{"role": "user", "content": user_msg}
],
response_format={"type": "json_object"},
@@ -115,5 +142,74 @@ def _mock_analysis(product_data: dict) -> dict:
"nutrition": {},
"nutrition_verdict": "",
"ingredients": [],
"recipe": None
"recipe": None,
"substitutions": None,
}
PHOTO_PROMPT = """Analise esta foto de um produto alimentar/suplemento. A foto pode mostrar o rótulo, tabela nutricional, ingredientes ou embalagem.
IMPORTANTE: Mesmo que a foto esteja parcial, TENTE extrair o máximo de informações. NUNCA retorne erro se conseguir identificar o produto.
Responda em JSON com este formato:
{
"product_name": "<nome>",
"brand": "<marca>",
"category": "<categoria>",
"ingredients_text": "<ingredientes>",
"nutri_score": "<a-e ou null>",
"nova_group": <1-4 ou null>,
"score": <int 0-100>,
"summary": "<resumo 2-3 frases>",
"positives": ["..."],
"negatives": ["..."],
"nutrition": {"calorias":"...","acucar":"...","gordura_total":"...","gordura_saturada":"...","sodio":"...","carboidratos":"...","fibras":"...","proteinas":"..."},
"nutrition_verdict": "<frase curta>",
"ingredients": [{"name":"...","popular_name":"...","explanation":"...","classification":"good|warning|bad","reason":"..."}],
"recipe": {"title":"...","description":"...","prep_time":"...","calories":"...","ingredients_list":["..."],"steps":["..."],"tip":"..."},
"substitutions": null
}
Se score < 50, inclua "substitutions" com 3 alternativas reais brasileiras.
SOMENTE retorne {"error": "mensagem"} se NÃO for alimento."""
async def analyze_photo(b64_image: str, user_context: dict = None) -> dict:
if not settings.OPENAI_API_KEY:
return None
client = AsyncOpenAI(api_key=settings.OPENAI_API_KEY)
system = "Voce eh um nutricionista brasileiro expert em analise de rotulos alimentares."
if user_context:
hp = user_context.get("health_profile", "normal")
system += HEALTH_PROFILE_PROMPTS.get(hp, "")
prompt = PHOTO_PROMPT
if user_context and user_context.get("allergies"):
prompt += f"\n\n⚠️ ALERGIAS: {', '.join(user_context['allergies'])}. Destaque alérgenos!"
for detail_level in ["low", "auto"]:
try:
resp = await client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64_image}", "detail": detail_level}}
]}
],
response_format={"type": "json_object"},
temperature=0.3,
timeout=30,
max_tokens=1500,
)
result = json.loads(resp.choices[0].message.content)
if result.get("error"):
continue
return result
except Exception as e:
print(f"OpenAI photo error (detail={detail_level}): {e}")
continue
return None