Spaces:
Running
Running
| import json | |
| import re | |
| from datetime import datetime | |
| from pathlib import Path | |
| def parse_quiz(raw_text): | |
| questions = [] | |
| # split on question markers like Q1:, Q2:, 1., 2. etc | |
| question_blocks = re.split( | |
| r"(?:^|\n)\s*(?:Q\d*[\.:]\s*|(?:\d+)[\.)\s]+)", | |
| raw_text.strip(), | |
| ) | |
| # fallback: split on blank lines | |
| if len(question_blocks) <= 1: | |
| question_blocks = re.split(r"\n\s*\n", raw_text.strip()) | |
| for block in question_blocks: | |
| block = block.strip() | |
| if not block: | |
| continue | |
| question = _parse_single_question(block) | |
| if question: | |
| questions.append(question) | |
| return questions | |
| def _parse_single_question(block): | |
| lines = block.strip().split("\n") | |
| if not lines: | |
| return None | |
| question_text = "" | |
| option_lines = [] | |
| answer_line = "" | |
| for line in lines: | |
| stripped = line.strip() | |
| if not stripped: | |
| continue | |
| # option line like A), B), etc | |
| if re.match(r"^[A-Da-d][).:\s]", stripped): | |
| option_lines.append(stripped) | |
| # answer line | |
| elif re.match(r"^(?:Answer|Correct|Ans)[:\s]", stripped, re.IGNORECASE): | |
| answer_line = stripped | |
| # question text (before any options or answer) | |
| elif not option_lines and not answer_line: | |
| if question_text: | |
| question_text += " " + stripped | |
| else: | |
| question_text = stripped | |
| if not question_text: | |
| return None | |
| options = [] | |
| for opt in option_lines: | |
| clean_opt = re.sub(r"^[A-Da-d][).:\s]+", "", opt).strip() | |
| options.append(clean_opt) | |
| correct_answer = "" | |
| if answer_line: | |
| match = re.search(r"[A-Da-d]", answer_line) | |
| if match: | |
| correct_answer = match.group().upper() | |
| return { | |
| "question": question_text, | |
| "options": options, | |
| "correct_answer": correct_answer, | |
| } | |
| def format_quiz(questions): | |
| if not questions: | |
| return "" | |
| lines = ["", " ββββββββββββββββββββββββββββββββββββββββββββ"] | |
| lines.append(" β π QUIZ β") | |
| lines.append(" ββββββββββββββββββββββββββββββββββββββββββββ") | |
| lines.append("") | |
| option_letters = ["A", "B", "C", "D"] | |
| for index, question in enumerate(questions, start=1): | |
| lines.append(f" Q{index}: {question['question']}") | |
| if question["options"]: | |
| for opt_index, option in enumerate(question["options"]): | |
| letter = option_letters[opt_index] if opt_index < 4 else f"{opt_index + 1}" | |
| lines.append(f" {letter}) {option}") | |
| if question["correct_answer"]: | |
| lines.append(f" β Answer: {question['correct_answer']}") | |
| lines.append("") | |
| lines.append(" " + "β" * 44) | |
| return "\n".join(lines) | |
| def parse_flashcards(raw_text): | |
| # Try to parse complete JSON array first | |
| try: | |
| start = raw_text.find("[") | |
| end = raw_text.rfind("]") | |
| if start != -1 and end != -1 and end > start: | |
| json_str = raw_text[start:end+1] | |
| data = json.loads(json_str) | |
| if isinstance(data, list): | |
| cards = [] | |
| for item in data: | |
| if isinstance(item, dict) and "front" in item and "back" in item: | |
| card = _clean_card(str(item["front"]), str(item["back"])) | |
| if card: | |
| cards.append(card) | |
| if cards: | |
| return cards | |
| except Exception: | |
| pass | |
| # Recover truncated JSON β LLM may have run out of tokens mid-card | |
| try: | |
| start = raw_text.find("[") | |
| if start != -1: | |
| fragment = raw_text[start:] | |
| # find the last complete card object (last "}") | |
| last_brace = fragment.rfind("}") | |
| if last_brace != -1: | |
| recovered = fragment[:last_brace + 1] + "]" | |
| data = json.loads(recovered) | |
| if isinstance(data, list): | |
| cards = [] | |
| for item in data: | |
| if isinstance(item, dict) and "front" in item and "back" in item: | |
| card = _clean_card(str(item["front"]), str(item["back"])) | |
| if card: | |
| cards.append(card) | |
| if cards: | |
| return cards | |
| except Exception: | |
| pass | |
| cards = [] | |
| # try Front: / Back: pattern first | |
| front_back_pairs = re.findall( | |
| r"(?:Front|Question|Q)\s*[:\-]\s*(.+?)(?:\n|\s{2,})(?:Back|Answer|A)\s*[:\-]\s*(.+?)(?=\n\s*(?:Front|Question|Q)\s*[:\-]|\Z)", | |
| raw_text, | |
| re.IGNORECASE | re.DOTALL, | |
| ) | |
| if front_back_pairs: | |
| for front, back in front_back_pairs: | |
| card = _clean_card(front.strip(), back.strip()) | |
| if card: | |
| cards.append(card) | |
| return cards | |
| # try numbered pairs like "1. term - definition" | |
| numbered_pairs = re.findall( | |
| r"\d+[.)]\s*(.+?)\s*[-ββ:]\s*(.+?)(?=\n\s*\d+[.)]|\Z)", | |
| raw_text, | |
| re.DOTALL, | |
| ) | |
| if numbered_pairs: | |
| for front, back in numbered_pairs: | |
| card = _clean_card(front.strip(), back.strip()) | |
| if card: | |
| cards.append(card) | |
| return cards | |
| # try bullet points like "- Term: Definition" | |
| term_defs = re.findall( | |
| r"[-β’*]\s*\**(.+?)\**\s*[:\-β]\s*(.+)", | |
| raw_text, | |
| ) | |
| if term_defs: | |
| for front, back in term_defs: | |
| card = _clean_card(front.strip(), back.strip()) | |
| if card: | |
| cards.append(card) | |
| return cards | |
| return cards | |
| def _clean_card(front, back): | |
| front = front.strip().rstrip(":") | |
| back = back.strip() | |
| if not front or not back: | |
| return None | |
| if len(front) < 2 or len(back) < 2: | |
| return None | |
| return {"front": front, "back": back} | |
| def format_flashcards(cards): | |
| if not cards: | |
| return "" | |
| lines = ["", " ββββββββββββββββββββββββββββββββββββββββββββ"] | |
| lines.append(" β π FLASHCARDS β") | |
| lines.append(" ββββββββββββββββββββββββββββββββββββββββββββ") | |
| lines.append("") | |
| for index, card in enumerate(cards, start=1): | |
| lines.append(f" Card {index}") | |
| lines.append(f" Front: {card['front']}") | |
| lines.append(f" Back: {card['back']}") | |
| lines.append("") | |
| lines.append(" " + "β" * 44) | |
| return "\n".join(lines) | |
| def save_progress(data_dir, mode, structured_data, user_prompt): | |
| if not structured_data: | |
| return | |
| progress_dir = Path(data_dir) / "progress" | |
| progress_dir.mkdir(parents=True, exist_ok=True) | |
| entry = { | |
| "timestamp": datetime.now().isoformat(timespec="seconds"), | |
| "mode": mode, | |
| "prompt": user_prompt[:200], | |
| "item_count": len(structured_data), | |
| "items": structured_data, | |
| } | |
| file_name = f"{mode}_history.jsonl" | |
| file_path = progress_dir / file_name | |
| try: | |
| with file_path.open("a", encoding="utf-8") as progress_file: | |
| progress_file.write(json.dumps(entry, ensure_ascii=False) + "\n") | |
| except OSError: | |
| pass | |
| def process_structured_output(raw_text, mode, data_dir, user_prompt): | |
| if mode == "quiz": | |
| questions = parse_quiz(raw_text) | |
| if questions: | |
| formatted = format_quiz(questions) | |
| save_progress(data_dir, mode, questions, user_prompt) | |
| return formatted, questions | |
| return raw_text, [] | |
| if mode == "flashcard": | |
| cards = parse_flashcards(raw_text) | |
| if cards: | |
| formatted = format_flashcards(cards) | |
| save_progress(data_dir, mode, cards, user_prompt) | |
| return formatted, cards | |
| return raw_text, [] | |
| return raw_text, [] | |