# app.py — CSS LexiScan for Hugging Face Spaces # Uses HF Secret 'SCAN' for Groq API key # Fully self-contained: précis + vocabulary extraction + Gradio UI import os import json from typing import Dict import gradio as gr import pandas as pd from groq import Groq # === Prompt Templates === SYSTEM_PROMPT = ( "You are a helpful, concise assistant specialized in creating high-quality précis " "and teaching vocabulary for CSS aspirants. Always output valid JSON with keys 'precis' and 'difficult_words'." ) USER_PROMPT_TEMPLATE = ( "Here is a passage delimited by triple backticks.\n" "Task:\n" "1) Produce a concise précis (≤30% of original length).\n" "2) Identify up to 7 difficult/uncommon words, each with word, part_of_speech, meaning (1–2 sentences), example sentence.\n" "Rules:\n" "- Return ONLY valid JSON.\n" "- Avoid repetition.\n" "Passage:\n```{passage}```" ) FEW_SHOT = ( "Example input: 'The rapid expansion of renewable energy sources has altered global energy markets.'\n" "Example output (JSON):\n" "{\n" " \"precis\": \"Renewable energy growth is reshaping global markets.\",\n" " \"difficult_words\": [\n" " {\"word\": \"renewable\", \"part_of_speech\": \"adjective\", \"meaning\": \"Able to be replenished naturally without being exhausted.\", \"example\": \"Wind and sunlight are renewable sources of power.\"}\n" " ]\n" "}" ) # === Helper Functions === def get_groq_api_key(secret_name: str = "SCAN") -> str: key = os.environ.get(secret_name) if not key: raise ValueError(f"No Groq API key found. Set it in HF Secrets as '{secret_name}'.") return key def make_groq_client(api_key: str): return Groq(api_key=api_key) def truncate(text: str, max_len: int) -> str: text = text.strip() return text if len(text) <= max_len else text[:max_len-1] + "…" def extract_json(text: str) -> str: start = text.find("{") end = text.rfind("}") if start == -1 or end == -1 or end <= start: raise ValueError("No JSON found in model response.") return text[start:end+1] def post_process(parsed: Dict) -> Dict: precis = parsed.get("precis", "").strip() difficult = parsed.get("difficult_words", []) seen = set() cleaned = [] for item in difficult: word = item.get("word", "").strip() if not word or word.lower() in seen: continue seen.add(word.lower()) cleaned.append({ "word": word, "part_of_speech": item.get("part_of_speech", "").strip(), "meaning": truncate(item.get("meaning", ""), 200), "example": truncate(item.get("example", ""), 200) }) return {"precis": truncate(precis, 2000), "difficult_words": cleaned} def generate_precis_and_vocab(client: Groq, passage: str, model: str = "llama-3.3-70b-versatile") -> Dict: passage = passage.strip() chat_completion = client.chat.completions.create( messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": FEW_SHOT}, {"role": "user", "content": USER_PROMPT_TEMPLATE.format(passage=passage)} ], model=model, temperature=0.2, max_tokens=800 ) raw_output = chat_completion.choices[0].message.content json_text = extract_json(raw_output) parsed = json.loads(json_text) return post_process(parsed) # === Gradio UI === def build_app(client: Groq): def process_text(passage: str): if not passage.strip(): return "Please enter a passage.", None try: parsed = generate_precis_and_vocab(client, passage) except Exception as e: return f"Error: {e}", None precis = parsed.get("precis", "") df = pd.DataFrame(parsed.get("difficult_words", [])) return precis, df with gr.Blocks() as demo: gr.Markdown("# 📝 CSS LexiScan — Précis & Vocabulary Assistant") passage_input = gr.Textbox(lines=10, label="Paste your passage here:") generate_btn = gr.Button("Generate Précis & Vocabulary") precis_output = gr.Textbox(lines=5, label="Generated Précis") vocab_output = gr.Dataframe( headers=["word", "part_of_speech", "meaning", "example"], label="Difficult Words" ) generate_btn.click(fn=process_text, inputs=passage_input, outputs=[precis_output, vocab_output]) return demo # === Main === if __name__ == "__main__": api_key = get_groq_api_key("SCAN") # <-- Uses HF secret named 'SCAN' client = make_groq_client(api_key) app = build_app(client) app.launch() # HF Spaces will automatically host this