import base64 from typing import Generator from groq import Groq GROQ_VISION_MODEL = "meta-llama/llama-4-scout-17b-16e-instruct" _SYSTEM_ANALYST = ( "You are an expert visual analyst. Analyze images thoroughly and accurately. " "Cover: main subjects, colors, composition, context, text if present, " "and any notable details." ) _SYSTEM_QA = ( "You are a precise visual analyst answering questions about an uploaded image. " "Be specific, factual, and concise. If something is not visible in the image, " "say so clearly." ) def encode_image(image_bytes: bytes) -> str: return base64.b64encode(image_bytes).decode("utf-8") def _build_qa_messages(data_url: str, question: str, conversation_history: list) -> list: """Build the messages list for Q&A, correctly embedding the image in the first user turn.""" messages = [{"role": "system", "content": _SYSTEM_QA}] if not conversation_history: messages.append({ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": data_url}}, {"type": "text", "text": question}, ], }) else: # Embed the image inside the first history user message so we never produce # two consecutive user-role messages (which some model endpoints reject). for i, msg in enumerate(conversation_history): if i == 0 and msg["role"] == "user": messages.append({ "role": "user", "content": [ {"type": "image_url", "image_url": {"url": data_url}}, {"type": "text", "text": msg["content"]}, ], }) else: messages.append(msg) messages.append({"role": "user", "content": question}) return messages def get_initial_analysis(image_bytes: bytes, api_key: str) -> tuple[str, int]: """Deep initial analysis of the image. Returns (analysis_text, tokens_used).""" try: client = Groq(api_key=api_key) data_url = f"data:image/jpeg;base64,{encode_image(image_bytes)}" response = client.chat.completions.create( model=GROQ_VISION_MODEL, messages=[ {"role": "system", "content": _SYSTEM_ANALYST}, { "role": "user", "content": [ {"type": "image_url", "image_url": {"url": data_url}}, { "type": "text", "text": ( "Provide a comprehensive analysis of this image. Cover: " "(1) What is shown, (2) Key details and objects, " "(3) Colors and composition, (4) Any text visible, " "(5) Context or likely use case of this image." ), }, ], }, ], max_tokens=600, ) text = response.choices[0].message.content or "" usage = response.usage tokens = int(usage.total_tokens) if usage and usage.total_tokens is not None else 0 return text, tokens except Exception as e: return f"Groq API error: {str(e)}", 0 def answer_followup( image_bytes: bytes, question: str, conversation_history: list, api_key: str, ) -> tuple[str, int]: """Non-streaming Q&A. Returns (answer_text, tokens_used).""" try: client = Groq(api_key=api_key) data_url = f"data:image/jpeg;base64,{encode_image(image_bytes)}" messages = _build_qa_messages(data_url, question, conversation_history) response = client.chat.completions.create( model=GROQ_VISION_MODEL, messages=messages, max_tokens=400, ) text = response.choices[0].message.content or "" usage = response.usage tokens = int(usage.total_tokens) if usage and usage.total_tokens is not None else 0 return text, tokens except Exception as e: return f"Groq API error: {str(e)}", 0 def answer_followup_stream( image_bytes: bytes, question: str, conversation_history: list, api_key: str, token_bucket: list, ) -> Generator[str, None, None]: """Streaming Q&A — yields text chunks; appends total_tokens to token_bucket when done.""" try: client = Groq(api_key=api_key) data_url = f"data:image/jpeg;base64,{encode_image(image_bytes)}" messages = _build_qa_messages(data_url, question, conversation_history) stream = client.chat.completions.create( model=GROQ_VISION_MODEL, messages=messages, max_tokens=400, stream=True, ) for chunk in stream: if chunk.choices and chunk.choices[0].delta.content: yield chunk.choices[0].delta.content if getattr(chunk, "usage", None) and chunk.usage.total_tokens: token_bucket.append(chunk.usage.total_tokens) except Exception as e: yield f"Groq API error: {str(e)}" def get_suggested_questions(analysis_text: str) -> list[str]: """Generate context-aware follow-up suggestions from the analysis (no extra API call).""" lower = analysis_text.lower() suggestions: list[str] = [] if any(w in lower for w in ["text", "word", "sign", "label", "written", "printed", "caption"]): suggestions.append("Can you read all the text visible in this image?") if any(w in lower for w in ["person", "people", "man", "woman", "child", "face", "figure", "human"]): suggestions.append("Describe the people in this image in detail") if any(w in lower for w in ["chart", "graph", "table", "data", "diagram", "plot", "bar", "pie"]): suggestions.append("Summarize the data or information shown") if any(w in lower for w in ["product", "brand", "logo", "package", "item"]): suggestions.append("Write a product description for this item") if any(w in lower for w in ["building", "architecture", "room", "indoor", "outdoor", "street", "landscape", "scene"]): suggestions.append("Describe the setting or environment in detail") if any(w in lower for w in ["animal", "dog", "cat", "bird", "wildlife", "creature"]): suggestions.append("What animal is this and what can you tell about it?") defaults = [ "List all objects you can identify", "What is the dominant color palette?", "What is the mood or atmosphere of this image?", "What context or setting does this come from?", ] for d in defaults: if len(suggestions) < 4: suggestions.append(d) return suggestions[:4]