""" Video Explainer Chat Handler - answers questions using video title, subject, lesson, and associated file. Uses OpenAI API (credentials from OPENAI_API_KEY environment variable). Laravel sends file content as base64 (since it may be on a different host). """ import base64 import os import tempfile from openai import OpenAI from file_processor import extract_text api_key = os.getenv("OPENAI_API_KEY") if api_key == "PASTE_YOUR_KEY_HERE": print("WARNING: Set OPENAI_API_KEY or OPENAI_API_KEY_VIDEO environment variable") client = OpenAI(api_key=api_key) def get_text_from_base64_file(file_content_base64: str, file_filename: str) -> str: """ Decode base64 file content, write to temp file, extract text, and return. Returns empty string on any error. """ if not file_content_base64 or not file_filename: return "" try: raw = base64.b64decode(file_content_base64, validate=True) except Exception: return "" if not raw: return "" suffix = os.path.splitext(file_filename)[1] if file_filename else ".bin" if not suffix or suffix == ".": suffix = ".bin" tmp_path = None try: fd, tmp_path = tempfile.mkstemp(suffix=suffix) os.write(fd, raw) os.close(fd) return extract_text(tmp_path) except Exception: return "" finally: if tmp_path and os.path.exists(tmp_path): try: os.unlink(tmp_path) except OSError: pass def chat_video_explainer(question, title, subject, lesson, file_content_base64=None, file_filename=None): """ Answer question using video metadata (title, subject, lesson) and associated file content. File content is received as base64 from Laravel (different hosting). Returns (answer, file_fetched). """ file_content = get_text_from_base64_file( file_content_base64 or "", file_filename or "" ) file_fetched = bool(file_content.strip()) has_file = file_fetched system_prompt = """You are a video assistant. You receive VIDEO TITLE, SUBJECT, LESSON/TOPIC, and ASSOCIATED MATERIAL (parsed file content). CRITICAL RULES: 1. Always use the provided context. Title, subject, and lesson are always sent—use them. 2. When ASSOCIATED MATERIAL is provided: It contains parsed content from the video's uploaded file (notes, transcript, slides). Use it for summarize, explain, and all answers. Give concrete summaries and quote specific points. 3. When asked to "summarize" or "explain the video": If you have ASSOCIATED MATERIAL, summarize that content and tie it to the title, subject, and lesson. If you have NO associated material, summarize what you know: "This video (title: X) covers [subject] — [lesson/topic]. No transcript or notes were provided, so I can't detail the full content. I can help with questions about [topic]." 4. Never say "I don't have access" or "I don't have notes" when ASSOCIATED MATERIAL is in the context. 5. Use clear, simple language. Be helpful.""" context_parts = [ "=== VIDEO METADATA (always provided) ===", f"TITLE: {title or 'Not specified'}", f"SUBJECT: {subject or 'Not specified'}", f"LESSON/TOPIC: {lesson or 'Not specified'}", "", ] if has_file: context_parts.append("=== ASSOCIATED MATERIAL (parsed file content—use for summarize/explain) ===") context_parts.append(file_content) else: context_parts.append("=== ASSOCIATED MATERIAL ===") context_parts.append("(None—no file was uploaded or parsing failed.)") user_prompt = f"""VIDEO CONTEXT: {chr(10).join(context_parts)} STUDENT QUESTION: {question} TASK: Answer using the context above. For summarize/explain: use ASSOCIATED MATERIAL if present; otherwise use TITLE, SUBJECT, LESSON. Be specific.""" try: response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=0.5, max_tokens=600, ) answer = response.choices[0].message.content.strip() return (answer, file_fetched) except Exception as e: err_msg = f"Sorry, I couldn't process your question. Please check that OPENAI_API_KEY is set correctly. Error: {str(e)}" return (err_msg, False)