Commit 90cd59a9 authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

feat(n8n-desc): implement Gemini vision fallback and raw text output

parent 927d9c1c
...@@ -27,11 +27,11 @@ logger = logging.getLogger(__name__) ...@@ -27,11 +27,11 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/product-desc", tags=["n8n Ultra Description"]) router = APIRouter(prefix="/api/product-desc", tags=["n8n Ultra Description"])
from config import FREE_LLM_API_KEY, LITELLM_BASE_URL from config import FREE_LLM_API_KEY, LITELLM_BASE_URL, GOOGLE_API_KEY
# ── FreeLLMAPI config ── # ── Config ──
VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct" VISION_MODELS = ["gemini-3.1-flash-lite-preview", "meta-llama/llama-4-scout-17b-16e-instruct"]
WRITER_MODEL = "openai/gpt-oss-20b" WRITER_MODEL = "openai/gpt-oss-20b"
...@@ -106,6 +106,41 @@ async def _groq_call(messages: list, model: str, max_tokens: int = 2000, ...@@ -106,6 +106,41 @@ async def _groq_call(messages: list, model: str, max_tokens: int = 2000,
raise RuntimeError("All FreeLLMAPI models/retries exhausted") raise RuntimeError("All FreeLLMAPI models/retries exhausted")
async def _gemini_vision_call(prompt: str, img_b64: str, model_name: str) -> str:
"""Native Gemini API call cho Vision phase (vì proxy chặn mảng image)."""
if not GOOGLE_API_KEY:
raise ValueError("GOOGLE_API_KEY is missing in config.py")
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={GOOGLE_API_KEY}"
payload = {
"contents": [
{
"parts": [
{"text": prompt},
{"inline_data": {"mime_type": "image/jpeg", "data": img_b64}}
]
}
]
}
for _ in range(2):
try:
async with httpx.AsyncClient(timeout=30) as client:
resp = await client.post(url, json=payload)
if resp.status_code == 200:
return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
elif resp.status_code == 429:
await asyncio.sleep(2)
continue
else:
raise Exception(f"Gemini API Error {resp.status_code}: {resp.text}")
except Exception as e:
logger.warning(f"Gemini Vision call failed: {e}")
await asyncio.sleep(1)
raise RuntimeError("Gemini Vision failed after retries")
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
# Request Schema — match voi data n8n gui tu Google Sheets # Request Schema — match voi data n8n gui tu Google Sheets
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
...@@ -249,11 +284,7 @@ QUY TẮC VIẾT: ...@@ -249,11 +284,7 @@ QUY TẮC VIẾT:
VÍ DỤ MINH HỌA CÁCH VIẾT (Để hiểu độ sâu và chi tiết cần có): VÍ DỤ MINH HỌA CÁCH VIẾT (Để hiểu độ sâu và chi tiết cần có):
- Hoàn cảnh sử dụng: "Trong những buổi sáng cuối tuần thảnh thơi dạo bước trên phố, chiếc Áo ba lỗ này là lựa chọn hoàn hảo để giải phóng cơ thể khỏi sự gò bó của trang phục công sở. Hoặc trong các buổi tập gym cường độ cao, chất liệu thấm hút nhanh sẽ hỗ trợ tối đa..." - Hoàn cảnh sử dụng: "Trong những buổi sáng cuối tuần thảnh thơi dạo bước trên phố, chiếc Áo ba lỗ này là lựa chọn hoàn hảo để giải phóng cơ thể khỏi sự gò bó của trang phục công sở. Hoặc trong các buổi tập gym cường độ cao, chất liệu thấm hút nhanh sẽ hỗ trợ tối đa..."
- Mix & Match: "Outfit 1 (Năng động ngày hè): Kết hợp chiếc Áo ba lỗ này cùng một chiếc Quần soóc khaki. Form đứng của Quần soóc sẽ bù trừ hoàn hảo cho độ mềm mại của áo, tạo nên tỷ lệ cơ thể vững chãi nam tính. Thêm một đôi sneaker trắng và Mũ thể thao Canifa để hoàn thiện diện mạo..." - Mix & Match: "Outfit 1 (Năng động ngày hè): Kết hợp chiếc Áo ba lỗ này cùng một chiếc Quần soóc khaki. Form đứng của Quần soóc sẽ bù trừ hoàn hảo cho độ mềm mại của áo, tạo nên tỷ lệ cơ thể vững chãi nam tính. Thêm một đôi sneaker trắng và Mũ thể thao Canifa để hoàn thiện diện mạo..."
"""
Trả về JSON:
{{
"ai_description": "Bài viết mô tả cực kỳ chi tiết và chuyên sâu (1000-2000 từ)"
}}"""
# ═══════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════
...@@ -277,13 +308,22 @@ async def n8n_generate_description(req: N8nDescRequest): ...@@ -277,13 +308,22 @@ async def n8n_generate_description(req: N8nDescRequest):
try: try:
# ── Phase 1: Vision ── # ── Phase 1: Vision ──
logger.info(f"[n8n-desc] {ma_sp} — Phase 1: Vision ({VISION_MODEL})") logger.info(f"[n8n-desc] {ma_sp} — Phase 1: Vision ({VISION_MODELS})")
vision_data = {} vision_data = {}
try: try:
img_b64 = await _fetch_image_base64(req.image_url) img_b64 = await _fetch_image_base64(req.image_url)
logger.info(f"[n8n-desc] {ma_sp} — Image fetched: {len(img_b64)} chars base64") logger.info(f"[n8n-desc] {ma_sp} — Image fetched: {len(img_b64)} chars base64")
vision_prompt = _build_vision_prompt(req)
vision_raw = None
for v_model in VISION_MODELS:
try:
logger.info(f"[n8n-desc] {ma_sp} — Trying Vision Model: {v_model}")
if v_model.startswith("gemini"):
vision_raw = await _gemini_vision_call(vision_prompt, img_b64, v_model)
else:
vision_messages = [ vision_messages = [
{ {
"role": "user", "role": "user",
...@@ -292,15 +332,21 @@ async def n8n_generate_description(req: N8nDescRequest): ...@@ -292,15 +332,21 @@ async def n8n_generate_description(req: N8nDescRequest):
"type": "image_url", "type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}, "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"},
}, },
{"type": "text", "text": _build_vision_prompt(req)}, {"type": "text", "text": vision_prompt},
], ],
} }
] ]
vision_raw = await _groq_call( vision_raw = await _groq_call(
vision_messages, VISION_MODEL, vision_messages, v_model,
max_tokens=800, is_vision=True max_tokens=800, is_vision=True
) )
vision_data = _extract_json(vision_raw) or {} if vision_raw:
break # Thanh cong, thoat khoi vong lap fallback
except Exception as e:
logger.warning(f"[n8n-desc] {ma_sp} — Vision failed with {v_model}: {e}")
continue
vision_data = _extract_json(vision_raw) if vision_raw else {}
logger.info(f"[n8n-desc] {ma_sp} — Vision done: {list(vision_data.keys())}") logger.info(f"[n8n-desc] {ma_sp} — Vision done: {list(vision_data.keys())}")
except Exception as vision_err: except Exception as vision_err:
...@@ -316,7 +362,7 @@ async def n8n_generate_description(req: N8nDescRequest): ...@@ -316,7 +362,7 @@ async def n8n_generate_description(req: N8nDescRequest):
"content": ( "content": (
"Bạn là AI Stylist cao cấp Canifa. Viết mô tả sản phẩm thời trang " "Bạn là AI Stylist cao cấp Canifa. Viết mô tả sản phẩm thời trang "
"bằng tiếng Việt. Phong cách: tự tin, gợi hình, không bịa chất liệu. " "bằng tiếng Việt. Phong cách: tự tin, gợi hình, không bịa chất liệu. "
"Luôn trả về JSON chuẩn, không text thừa." "Viết thẳng đoạn văn bản mô tả (không cần format JSON)."
), ),
}, },
{"role": "user", "content": _build_writer_prompt(req, vision_data)}, {"role": "user", "content": _build_writer_prompt(req, vision_data)},
...@@ -326,11 +372,9 @@ async def n8n_generate_description(req: N8nDescRequest): ...@@ -326,11 +372,9 @@ async def n8n_generate_description(req: N8nDescRequest):
writer_messages, WRITER_MODEL, writer_messages, WRITER_MODEL,
max_tokens=4000 max_tokens=4000
) )
result = _extract_json(writer_raw)
if not result or not result.get("ai_description"):
# Fallback: neu JSON fail, lay raw text lam description
ai_desc = writer_raw.strip() if writer_raw else "" ai_desc = writer_raw.strip() if writer_raw else ""
if not ai_desc: if not ai_desc:
return JSONResponse( return JSONResponse(
status_code=500, status_code=500,
...@@ -341,7 +385,6 @@ async def n8n_generate_description(req: N8nDescRequest): ...@@ -341,7 +385,6 @@ async def n8n_generate_description(req: N8nDescRequest):
"raw": writer_raw[:500] if writer_raw else None, "raw": writer_raw[:500] if writer_raw else None,
}, },
) )
result = {"ai_description": ai_desc}
elapsed = round(time.time() - t0, 2) elapsed = round(time.time() - t0, 2)
logger.info(f"[n8n-desc] {ma_sp} — Done in {elapsed}s") logger.info(f"[n8n-desc] {ma_sp} — Done in {elapsed}s")
...@@ -350,7 +393,7 @@ async def n8n_generate_description(req: N8nDescRequest): ...@@ -350,7 +393,7 @@ async def n8n_generate_description(req: N8nDescRequest):
"status": "success", "status": "success",
"ma_san_pham": ma_sp, "ma_san_pham": ma_sp,
"ten_san_pham": req.ten_san_pham, "ten_san_pham": req.ten_san_pham,
"ai_description": result["ai_description"], "ai_description": ai_desc,
"elapsed_s": elapsed, "elapsed_s": elapsed,
# Debug (n8n co the bo qua) # Debug (n8n co the bo qua)
"vision_data": vision_data, "vision_data": vision_data,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment