Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import requests | |
| import json | |
| import pandas as pd | |
| import matplotlib.pyplot as plt | |
| import numpy as np | |
| from io import BytesIO | |
| import base64 | |
| import PyPDF2 | |
| from docx import Document | |
| import re | |
| from matplotlib.backends.backend_pdf import PdfPages | |
| from datetime import datetime | |
| # ---------- Configuration ---------- | |
| NVIDIA_API_KEY = os.environ.get("NVIDIA_API_KEY") | |
| NVIDIA_URL = "https://integrate.api.nvidia.com/v1/chat/completions" | |
| MODEL = "meta/llama-4-maverick-17b-128e-instruct" | |
| # Two separate folders for the two framework types | |
| AFSCTP_FOLDER = "./frameworks/standards" | |
| ATQF_FOLDER = "./frameworks/qualifications" | |
| # ---------- AFSCTP SCORECARD (Standards and Competencies) ---------- | |
| AFSCTP_CRITERIA = { | |
| "S1": { | |
| "short_label": "Career Path Structure", | |
| "section": "Career & CPD", | |
| "levels": { | |
| 0: "No career stages or progression pathway defined for teachers", | |
| 1: "Generic promotion levels exist but not based on professional competence", | |
| 2: "Some progression levels defined but not aligned to the 4-stage continental model", | |
| 3: "Four stages mentioned (Beginner, Proficient, Expert, Distinguished) but lack clear descriptors or requirements", | |
| 4: "Four stages defined with competence descriptors for each stage but missing progression criteria", | |
| 5: "Fully defined 4-stage career path (Beginner→Proficient→Expert→Distinguished) with detailed competences, CPD requirements, and progression criteria for each stage" | |
| } | |
| }, | |
| "S2": { | |
| "short_label": "Teacher Professional Standards (5 Domains)", | |
| "section": "Professional Standards", | |
| "levels": { | |
| 0: "No professional standards or competences defined for teachers", | |
| 1: "Generic teaching guidelines exist but not structured as professional standards", | |
| 2: "Some professional domains covered but incomplete (missing 2-3 domains from the 5-domain framework)", | |
| 3: "All 5 domains mentioned (Knowledge, Skills, Values, Partnerships, Leadership) but lack detailed competences/descriptors", | |
| 4: "All 5 domains with substantial competences defined but partial alignment to AFSCTP", | |
| 5: "Full alignment: All 5 domains (Professional Knowledge, Skills, Values/Attributes, Partnerships, Leadership) with comprehensive competences matching AFSCTP" | |
| } | |
| }, | |
| "S3": { | |
| "short_label": "School Leadership Standards", | |
| "section": "School Leadership", | |
| "levels": { | |
| 0: "No standards defined for school leaders/principals", | |
| 1: "Generic management standards only (administrative focus)", | |
| 2: "Some leadership standards exist but cover fewer than 4 of the 7 key areas", | |
| 3: "Leadership standards exist but not aligned to the 7-standard continental framework", | |
| 4: "Substantial alignment with 7 standards but missing detailed competences for career stages", | |
| 5: "Full alignment with all 7 leadership standards: Developing Self/Others, Leading Knowledge/Practice, Managing Resources, Promoting Improvement, Generating Resources, Supporting Learners, and Community Engagement" | |
| } | |
| }, | |
| "S4": { | |
| "short_label": "Competency Assessment Framework", | |
| "section": "Professional Regulation", | |
| "levels": { | |
| 0: "No competency assessment or testing mentioned", | |
| 1: "Informal/self-assessment only with no standardized testing", | |
| 2: "Assessment exists but not explicitly linked to career stage progression", | |
| 3: "Competency testing mentioned for some transitions but not standardized across all stages", | |
| 4: "Structured competency tests for progression but lacking technology-based implementation (CBT)", | |
| 5: "Comprehensive competency testing for all career transitions (Beginner→Proficient→Expert→Distinguished) using standardized computer-based testing aligned to AFSCTP" | |
| } | |
| }, | |
| "S5": { | |
| "short_label": "Continental Strategy Alignment", | |
| "section": "Policy Context", | |
| "levels": { | |
| 0: "No reference to African Union education strategies or international frameworks", | |
| 1: "Generic reference to African education without specific strategy alignment", | |
| 2: "Mentions CESA 16-25 but not integrated into the standards framework", | |
| 3: "References CESA 16-25 and Agenda 2063 but lacks explicit implementation pathways", | |
| 4: "Substantial alignment with CESA 16-25 Strategic Objective 1 and continental teacher policies", | |
| 5: "Explicit alignment with CESA 16-25, Agenda 2063, SDG4c, and Continental Teacher Mobility Protocol with clear implementation strategies" | |
| } | |
| } | |
| } | |
| # ---------- ATQF/CTQF SCORECARD (Qualifications Framework) ---------- | |
| ATQF_CRITERIA = { | |
| "Q1": { | |
| "short_label": "Minimum Entry Qualifications", | |
| "section": "Entry Requirements", | |
| "levels": { | |
| 0: "No minimum entry standards defined; certificates or lower qualifications accepted", | |
| 1: "Diploma or sub-degree qualifications accepted as minimum entry", | |
| 2: "Degree required but not specific to education (any degree accepted without pedagogy)", | |
| 3: "Education degree required but not aligned to B.Ed./PGDE model or allows many exceptions", | |
| 4: "B.Ed. or Degree+PGDE required as primary pathways with minor administrative exceptions", | |
| 5: "Strict requirement: Only B.Ed. (5-year integrated or 3+2 model) or Bachelor's degree + PGDE accepted as entry pathways" | |
| } | |
| }, | |
| "Q2": { | |
| "short_label": "B.Ed. Curriculum Structure", | |
| "section": "Initial Teacher Education", | |
| "levels": { | |
| 0: "No B.Ed. standards defined; unstructured teacher preparation", | |
| 1: "B.Ed. exists but without specific credit requirements or professional module percentages", | |
| 2: "B.Ed. defined but <150 credits OR <40% professional modules (missing key elements)", | |
| 3: "B.Ed. meets duration requirement but either credits <150 OR professional modules <40%", | |
| 4: "Meets 150 credits and ≥40% professional modules but teaching practice <1 year or not structured", | |
| 5: "Full CTQF compliance: 150 credits minimum, ≥40% professional modules, 1-year teaching practice (split or continuous), ISCED Level 6 alignment" | |
| } | |
| }, | |
| "Q3": { | |
| "short_label": "PGDE Curriculum Structure", | |
| "section": "Initial Teacher Education", | |
| "levels": { | |
| 0: "No postgraduate diploma pathway defined for degree holders", | |
| 1: "Generic postgraduate certificate without specific structure", | |
| 2: "PGDE exists but <50 credits OR <3 months teaching practice", | |
| 3: "PGDE with 50 credits but teaching practice <3 months or not supervised", | |
| 4: "PGDE with 50 credits and 3 months practice but missing ISCED alignment or quality framework", | |
| 5: "Full compliance: 50-credit PGDE with 3-month teaching practice, ISCED Level 6 professional orientation, for graduates entering teaching" | |
| } | |
| }, | |
| "Q4": { | |
| "short_label": "Accreditation System (Dual)", | |
| "section": "Quality Assurance", | |
| "levels": { | |
| 0: "No accreditation system for teacher education programs", | |
| 1: "Voluntary or informal accreditation only", | |
| 2: "Generic institutional accreditation only (no professional teaching accreditation)", | |
| 3: "Professional accreditation mentioned but not conducted by Teaching Regulatory Authority", | |
| 4: "Dual accreditation exists (Generic HE Council + Teaching Council) but gaps in implementation or coverage", | |
| 5: "Mandatory dual accreditation: Generic (Higher Education Council/Commission) + Professional (Teaching Regulatory Authority) with AQRM/ASG-QA alignment" | |
| } | |
| }, | |
| "Q5": { | |
| "short_label": "CPD Credit System", | |
| "section": "Career & CPD", | |
| "levels": { | |
| 0: "No CPD requirements or continuous education defined", | |
| 1: "CPD encouraged but voluntary/not structured", | |
| 2: "CPD required but no credit system or accumulation mechanism defined", | |
| 3: "CPD credit system exists but <55 credits per 5-year cycle OR not linked to career progression", | |
| 4: "55-credit system per 5-year cycle defined but missing endorsement mechanisms for providers", | |
| 5: "Full system: 55 CPD credits per 5-year cycle covering 7 modules, endorsed providers, linked to career stage advancement (Beginner→Proficient→Expert→Distinguished)" | |
| } | |
| }, | |
| "Q6": { | |
| "short_label": "School Leadership Certification", | |
| "section": "School Leadership", | |
| "levels": { | |
| 0: "No specific qualification requirements for school leaders", | |
| 1: "Generic leadership training accepted without specific diploma requirements", | |
| 2: "Leadership qualification required but <60 credits or not mandatory", | |
| 3: "60-credit Diploma in School Leadership required but not linked to teacher career stages", | |
| 4: "60-credit Diploma required + must have reached Proficient stage but missing competency assessment", | |
| 5: "Full compliance: 60-credit Diploma in School Leadership + Proficient teacher status + competency assessment + workplace project for headteacher/principal appointment" | |
| } | |
| }, | |
| "Q7": { | |
| "short_label": "Induction & Mentoring System", | |
| "section": "Professional Entry", | |
| "levels": { | |
| 0: "No induction or mentoring program for newly qualified teachers", | |
| 1: "Brief orientation (days/weeks) only without structured mentoring", | |
| 2: "Induction exists but <2 years duration or not mandatory", | |
| 3: "Mandatory induction exists but lacks formal mentoring structure or assessment", | |
| 4: "2-year induction/mentoring specified but not fully integrated with CPD portfolio", | |
| 5: "Mandatory structured 2-year induction and mentoring program with assigned mentors, assessment, and integration into CPD credits for Beginner teachers" | |
| } | |
| } | |
| } | |
| def get_criteria_display(criteria_dict, framework_name): | |
| """Generate markdown display of criteria for a specific framework""" | |
| md = f"### 📋 {framework_name} Evaluation Criteria\n\n" | |
| sections = {} | |
| for cid, data in criteria_dict.items(): | |
| section = data['section'] | |
| if section not in sections: | |
| sections[section] = [] | |
| sections[section].append((cid, data)) | |
| for section, items in sections.items(): | |
| md += f"\n#### {section}\n\n" | |
| for cid, data in items: | |
| md += f"**{cid} - {data['short_label']}**\n" | |
| md += "<details><summary>View Scoring Descriptors (0-5)</summary>\n\n" | |
| for level, desc in data['levels'].items(): | |
| emoji = "🔴" if level <= 1 else "🟡" if level <= 3 else "🟢" | |
| md += f"{emoji} **{level}:** {desc}\n\n" | |
| md += "</details>\n\n" | |
| return md | |
| def get_frameworks_for_type(framework_type): | |
| """Get available frameworks based on selected type""" | |
| if framework_type == "Standards and Competencies (AFSCTP)": | |
| folder = AFSCTP_FOLDER | |
| default_msg = "No standards frameworks available" | |
| else: | |
| folder = ATQF_FOLDER | |
| default_msg = "No qualifications frameworks available" | |
| # Create folder if it doesn't exist | |
| if not os.path.exists(folder): | |
| os.makedirs(folder) | |
| return [default_msg + f" - Add PDFs to {folder}"] | |
| pdf_files = [f for f in os.listdir(folder) if f.lower().endswith('.pdf')] | |
| if not pdf_files: | |
| return [default_msg + f" - Add PDFs to {folder}"] | |
| return ["Select a framework..."] + sorted([f[:-4] for f in pdf_files]) | |
| def get_folder_for_framework(framework_type): | |
| """Return the appropriate folder path based on framework type""" | |
| if framework_type == "Standards and Competencies (AFSCTP)": | |
| return AFSCTP_FOLDER | |
| else: | |
| return ATQF_FOLDER | |
| def extract_text_from_file(file_path): | |
| if file_path.endswith('.pdf'): | |
| with open(file_path, 'rb') as f: | |
| reader = PyPDF2.PdfReader(f) | |
| text = "".join(page.extract_text() for page in reader.pages) | |
| return re.sub(r'\s+', ' ', text) | |
| elif file_path.endswith('.docx'): | |
| doc = Document(file_path) | |
| return "\n".join([para.text for para in doc.paragraphs]) | |
| else: | |
| with open(file_path, 'r', encoding='utf-8') as f: | |
| return f.read() | |
| def detect_country(document_text): | |
| """Detect country from document""" | |
| if not NVIDIA_API_KEY: | |
| return "Unknown Country" | |
| prompt = f"""Identify which African country this teacher education framework document belongs to. | |
| Look for country names, national bodies (e.g., "Kenya National Qualifications Framework", "Ghana Education Service"). | |
| Return ONLY the country name (e.g., "Kenya", "Ghana", "Nigeria"). If uncertain, return "Unknown". | |
| DOCUMENT TEXT (first 3000 characters): | |
| {document_text[:3000]} | |
| Country:""" | |
| try: | |
| headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"} | |
| payload = { | |
| "model": MODEL, | |
| "messages": [{"role": "user", "content": prompt}], | |
| "temperature": 0.1, | |
| "max_tokens": 50 | |
| } | |
| response = requests.post(NVIDIA_URL, headers=headers, json=payload) | |
| if response.status_code == 200: | |
| country = response.json()["choices"][0]["message"]["content"].strip() | |
| country = country.replace("The ", "").replace("the ", "").replace(".", "").replace("\n", " ").strip() | |
| return country if country else "Unknown Country" | |
| except: | |
| pass | |
| return "Unknown Country" | |
| def build_evaluation_prompt(document_text, criteria_dict, framework_name): | |
| """Build prompt with anchored descriptors""" | |
| prompt_parts = [] | |
| prompt_parts.append(f"You are evaluating a national teacher framework against the {framework_name}. ") | |
| prompt_parts.append("For each criterion below, select the SINGLE best description (0-5) that matches the document content. ") | |
| prompt_parts.append("Return ONLY a JSON object where keys are criterion IDs (e.g., 'S1' or 'Q1') and values are integers 0-5. No explanations.\n") | |
| for cid, data in criteria_dict.items(): | |
| prompt_parts.append(f"\n{cid} - {data['short_label']}:") | |
| for level, desc in data['levels'].items(): | |
| prompt_parts.append(f" [{level}] {desc}") | |
| prompt_parts.append(f"\n\nDOCUMENT TEXT:\n{document_text[:20000]}\n") | |
| prompt_parts.append("\nRespond with valid JSON only: {\"S1\": 4, \"S2\": 3, ...} or {\"Q1\": 5, \"Q2\": 4, ...}") | |
| return "\n".join(prompt_parts) | |
| def call_nvidia_llm(prompt): | |
| headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Content-Type": "application/json"} | |
| payload = { | |
| "model": MODEL, | |
| "messages": [{"role": "user", "content": prompt}], | |
| "temperature": 0.1, | |
| "max_tokens": 2000 | |
| } | |
| response = requests.post(NVIDIA_URL, headers=headers, json=payload) | |
| if response.status_code != 200: | |
| raise Exception(f"NVIDIA API error: {response.status_code} - {response.text}") | |
| content = response.json()["choices"][0]["message"]["content"] | |
| content = content.strip() | |
| if "```json" in content: | |
| content = content.split("```json")[1].split("```")[0] | |
| elif "```" in content: | |
| content = content.split("```")[1].split("```")[0] | |
| content = content.strip() | |
| try: | |
| return json.loads(content) | |
| except json.JSONDecodeError: | |
| match = re.search(r'\{.*\}', content, re.DOTALL) | |
| if match: | |
| return json.loads(match.group()) | |
| raise Exception(f"Could not parse JSON from: {content[:200]}") | |
| def generate_charts(df, country, framework_name): | |
| """Generate visualization charts""" | |
| labels = df["Label"].tolist() | |
| scores = df["Score"].tolist() | |
| values = np.array(scores) | |
| # Bar chart | |
| plt.figure(figsize=(14, 9)) | |
| colors = ['#b71c1c' if v == 0 else '#d32f2f' if v == 1 else '#f57c00' if v == 2 else '#fbc02d' if v == 3 else '#689f38' if v == 4 else '#2e7d32' for v in values] | |
| bars = plt.barh(labels, values, color=colors, edgecolor='black', linewidth=0.5) | |
| plt.xlim(0, 5) | |
| plt.xlabel("Score (0-5)", fontsize=12, fontweight='bold') | |
| plt.title(f"{country}\n{framework_name} Alignment Evaluation", fontsize=16, fontweight='bold', pad=20) | |
| plt.axvline(x=3, color='gray', linestyle='--', alpha=0.5) | |
| for i, (bar, score) in enumerate(zip(bars, values)): | |
| plt.text(score + 0.1, bar.get_y() + bar.get_height()/2, f"{score}", va='center', fontweight='bold', fontsize=11) | |
| plt.tight_layout() | |
| bar_buf = BytesIO() | |
| plt.savefig(bar_buf, format='png', dpi=150, bbox_inches='tight') | |
| bar_buf.seek(0) | |
| bar_img = base64.b64encode(bar_buf.read()).decode() | |
| plt.close() | |
| # Radar chart | |
| N = len(labels) | |
| angles = [n / float(N) * 2 * np.pi for n in range(N)] | |
| angles += angles[:1] | |
| values_radar = list(values) + [values[0]] | |
| fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(polar=True)) | |
| ax.plot(angles, values_radar, 'o-', linewidth=2.5, color='#1565c0') | |
| ax.fill(angles, values_radar, alpha=0.25, color='#1565c0') | |
| ax.set_xticks(angles[:-1]) | |
| ax.set_xticklabels(labels, size=10) | |
| ax.set_ylim(0, 5) | |
| ax.set_yticks([1, 2, 3, 4, 5]) | |
| ax.set_yticklabels(['1', '2', '3', '4', '5'], color="grey", size=8) | |
| ax.set_title(f"{country}\nAlignment Profile", size=16, fontweight='bold', pad=20) | |
| radar_buf = BytesIO() | |
| plt.savefig(radar_buf, format='png', dpi=150, bbox_inches='tight') | |
| radar_buf.seek(0) | |
| radar_img = base64.b64encode(radar_buf.read()).decode() | |
| plt.close() | |
| # Gauge | |
| total_score = sum(values) | |
| max_score = len(values) * 5 | |
| percent = (total_score / max_score) * 100 | |
| fig, ax = plt.subplots(figsize=(10, 4)) | |
| ax.barh([0], [100], color='#e0e0e0', height=0.5, edgecolor='black') | |
| if percent < 40: | |
| color = '#d32f2f' | |
| elif percent < 60: | |
| color = '#f57c00' | |
| elif percent < 75: | |
| color = '#fbc02d' | |
| elif percent < 90: | |
| color = '#689f38' | |
| else: | |
| color = '#2e7d32' | |
| ax.barh([0], [percent], color=color, height=0.5, edgecolor='black') | |
| ax.set_xlim(0, 100) | |
| ax.set_xticks([0, 25, 50, 75, 100]) | |
| ax.set_xticklabels(['0%', '25%', '50%', '75%', '100%'], fontsize=11) | |
| ax.set_yticks([]) | |
| ax.set_xlabel(f"Overall Alignment Score", fontsize=14, fontweight='bold') | |
| ax.set_title(f"{country}\n{percent:.1f}% Aligned with {framework_name}", fontsize=18, fontweight='bold', pad=15) | |
| ax.text(50, 0, f"{percent:.1f}%", ha='center', va='center', fontsize=20, fontweight='bold', color='white' if percent > 50 else 'black') | |
| gauge_buf = BytesIO() | |
| plt.savefig(gauge_buf, format='png', dpi=150, bbox_inches='tight') | |
| gauge_buf.seek(0) | |
| gauge_img = base64.b64encode(gauge_buf.read()).decode() | |
| plt.close() | |
| return bar_img, radar_img, gauge_img, percent | |
| def generate_pdf_report(df, country, framework_name, percent, bar_img_data, radar_img_data, gauge_img_data): | |
| """Generate PDF report""" | |
| pdf_path = "/tmp/framework_evaluation_report.pdf" | |
| with PdfPages(pdf_path) as pdf: | |
| # Page 1: Cover | |
| fig = plt.figure(figsize=(8.5, 11)) | |
| fig.text(0.5, 0.9, f"{country}", ha='center', fontsize=24, fontweight='bold') | |
| fig.text(0.5, 0.85, f"{framework_name} Evaluation Report", ha='center', fontsize=18, fontweight='bold') | |
| fig.text(0.5, 0.80, f"Overall Alignment: {percent:.1f}%", ha='center', fontsize=16, | |
| color='#2e7d32' if percent >= 75 else '#f57c00' if percent >= 50 else '#d32f2f') | |
| fig.text(0.5, 0.75, f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}", ha='center', fontsize=10, style='italic') | |
| # Summary | |
| summary_text = [] | |
| summary_text.append("EXECUTIVE SUMMARY") | |
| summary_text.append("-" * 50) | |
| strong = df[df['Score'] >= 4]['Label'].tolist() | |
| weak = df[df['Score'] <= 2]['Label'].tolist() | |
| moderate = df[df['Score'] == 3]['Label'].tolist() | |
| summary_text.append(f"\nStrengths (Score 4-5): {len(strong)} criteria") | |
| if strong: | |
| for s in strong: | |
| summary_text.append(f" ✓ {s}") | |
| summary_text.append(f"\nModerate Alignment (Score 3): {len(moderate)} criteria") | |
| if moderate: | |
| for m in moderate: | |
| summary_text.append(f" ~ {m}") | |
| summary_text.append(f"\nPriority Areas (Score 0-2): {len(weak)} criteria") | |
| if weak: | |
| for w in weak: | |
| summary_text.append(f" ✗ {w}") | |
| y_pos = 0.65 | |
| for line in summary_text: | |
| weight = 'bold' if line.isupper() or line.startswith('-') else 'normal' | |
| size = 11 if line.startswith(' ') else 12 | |
| fig.text(0.1, y_pos, line, fontsize=size, fontweight=weight) | |
| y_pos -= 0.025 | |
| plt.axis('off') | |
| pdf.savefig(fig, bbox_inches='tight') | |
| plt.close() | |
| # Page 2: Bar Chart | |
| if bar_img_data: | |
| fig, ax = plt.subplots(figsize=(11, 8.5)) | |
| img_data = base64.b64decode(bar_img_data) | |
| img = plt.imread(BytesIO(img_data)) | |
| ax.imshow(img) | |
| ax.axis('off') | |
| pdf.savefig(fig, bbox_inches='tight') | |
| plt.close() | |
| # Page 3: Radar Chart | |
| if radar_img_data: | |
| fig, ax = plt.subplots(figsize=(11, 8.5)) | |
| img_data = base64.b64decode(radar_img_data) | |
| img = plt.imread(BytesIO(img_data)) | |
| ax.imshow(img) | |
| ax.axis('off') | |
| pdf.savefig(fig, bbox_inches='tight') | |
| plt.close() | |
| # Page 4: Detailed Table | |
| fig, ax = plt.subplots(figsize=(11, 8.5)) | |
| ax.axis('tight') | |
| ax.axis('off') | |
| table_data = [['Criterion', 'Section', 'Score', 'Assessment Level']] | |
| for _, row in df.iterrows(): | |
| table_data.append([ | |
| row['Label'], | |
| row['Section'], | |
| str(row['Score']), | |
| row['Level'][:80] + '...' if len(row['Level']) > 80 else row['Level'] | |
| ]) | |
| table = ax.table(cellText=table_data, cellLoc='left', loc='center', | |
| colWidths=[0.25, 0.15, 0.08, 0.52]) | |
| table.auto_set_font_size(False) | |
| table.set_fontsize(9) | |
| table.scale(1, 2) | |
| # Style header | |
| for i in range(4): | |
| table[(0, i)].set_facecolor('#1565c0') | |
| table[(0, i)].set_text_props(weight='bold', color='white') | |
| # Color code scores | |
| for i in range(1, len(table_data)): | |
| score = int(table_data[i][2]) | |
| color = '#b71c1c' if score == 0 else '#d32f2f' if score == 1 else '#f57c00' if score == 2 else '#fbc02d' if score == 3 else '#689f38' if score == 4 else '#2e7d32' | |
| table[(i, 2)].set_facecolor(color) | |
| table[(i, 2)].set_text_props(weight='bold', color='white') | |
| pdf.savefig(fig, bbox_inches='tight') | |
| plt.close() | |
| return pdf_path | |
| def evaluate_framework(file_path, framework_type): | |
| """Main evaluation function""" | |
| if not NVIDIA_API_KEY: | |
| return pd.DataFrame(), "❌ NVIDIA_API_KEY not set.", "", "", "", 0, "Error", None, "Unknown Country" | |
| # Select criteria based on framework type | |
| if framework_type == "Standards and Competencies (AFSCTP)": | |
| criteria_dict = AFSCTP_CRITERIA | |
| framework_name = "AFSCTP" | |
| else: | |
| criteria_dict = ATQF_CRITERIA | |
| framework_name = "ATQF/CTQF" | |
| try: | |
| text = extract_text_from_file(file_path) | |
| if not text.strip(): | |
| return pd.DataFrame(), "❌ File contains no extractable text.", "", "", "", 0, "Error", None, "Unknown Country" | |
| if len(text) < 100: | |
| return pd.DataFrame(), "❌ Extracted text too short.", "", "", "", 0, "Error", None, "Unknown Country" | |
| except Exception as e: | |
| return pd.DataFrame(), f"❌ Text extraction error: {e}", "", "", "", 0, "Error", None, "Unknown Country" | |
| country = detect_country(text) | |
| try: | |
| prompt = build_evaluation_prompt(text, criteria_dict, framework_name) | |
| results = call_nvidia_llm(prompt) | |
| except Exception as e: | |
| return pd.DataFrame(), f"❌ AI evaluation error: {e}", "", "", "", 0, "Error", None, country | |
| # Process results | |
| rows = [] | |
| for cid, data in criteria_dict.items(): | |
| score = results.get(cid, 0) | |
| try: | |
| score = int(score) | |
| if score < 0 or score > 5: | |
| score = 0 | |
| except: | |
| score = 0 | |
| rows.append({ | |
| "ID": cid, | |
| "Label": data['short_label'], | |
| "Section": data['section'], | |
| "Score": score, | |
| "Level": data['levels'][score] | |
| }) | |
| if not rows: | |
| return pd.DataFrame(), "❌ No valid results returned.", "", "", "", 0, "Error", None, country | |
| df = pd.DataFrame(rows) | |
| # Generate charts | |
| bar_img, radar_img, gauge_img, percent = generate_charts(df, country, framework_name) | |
| # Generate PDF | |
| pdf_path = generate_pdf_report(df, country, framework_name, percent, bar_img, radar_img, gauge_img) | |
| # Create summary | |
| strong_criteria = df[df['Score'] >= 4]['Label'].tolist() | |
| weak_criteria = df[df['Score'] <= 2]['Label'].tolist() | |
| summary_md = f""" | |
| ### **{country} - {framework_name} Evaluation Report** | |
| **Overall Alignment: {percent:.1f}%** | |
| **Framework:** {framework_type} | |
| **Scale:** 0 = Non-existent | 1 = Weak | 2 = Developing | 3 = Moderate | 4 = Strong | 5 = Excellent | |
| **🟢 Strengths ({len(strong_criteria)}):** {', '.join(strong_criteria) if strong_criteria else 'None identified'} | |
| **🔴 Priority Areas ({len(weak_criteria)}):** {', '.join(weak_criteria) if weak_criteria else 'None - all criteria adequate'} | |
| """ | |
| # Convert images to HTML | |
| bar_html = f'<img src="data:image/png;base64,{bar_img}" style="width:100%; max-width:900px;">' | |
| radar_html = f'<img src="data:image/png;base64,{radar_img}" style="width:100%; max-width:600px;">' | |
| gauge_html = f'<img src="data:image/png;base64,{gauge_img}" style="width:100%; max-width:800px;">' | |
| return df, summary_md, bar_html, radar_html, gauge_html, percent, "Success", pdf_path, country | |
| def process_inputs(file, framework_type, dropdown_selection): | |
| """Process either uploaded file or selected framework""" | |
| # Determine which input to use | |
| if dropdown_selection and not dropdown_selection.startswith("Select") and not dropdown_selection.startswith("No"): | |
| folder = get_folder_for_framework(framework_type) | |
| file_path = os.path.join(folder, dropdown_selection + ".pdf") | |
| if not os.path.exists(file_path): | |
| return pd.DataFrame(), f"❌ File not found: {file_path}", "", "", "", 0, "Error", None, "Unknown Country", gr.update(visible=False) | |
| elif file is not None: | |
| file_path = file.name | |
| else: | |
| return pd.DataFrame(), "Please upload a document or select a framework from the dropdown.", "", "", "", 0, "Waiting", None, "Unknown Country", gr.update(visible=False) | |
| df, summary, bar_html, radar_html, gauge_html, percent, status, pdf_path, country = evaluate_framework(file_path, framework_type) | |
| if status != "Success": | |
| return df, summary, "", "", "", percent, status, country, gr.update(visible=False) | |
| return df, summary, bar_html, radar_html, gauge_html, percent, status, country, gr.update(value=pdf_path, visible=True) | |
| # Generate displays for both frameworks | |
| AFSCTP_DISPLAY = get_criteria_display(AFSCTP_CRITERIA, "AFSCTP (Standards & Competencies)") | |
| ATQF_DISPLAY = get_criteria_display(ATQF_CRITERIA, "ATQF/CTQF (Qualifications)") | |
| # Initialize folders and get initial choices | |
| os.makedirs(AFSCTP_FOLDER, exist_ok=True) | |
| os.makedirs(ATQF_FOLDER, exist_ok=True) | |
| INITIAL_DROPDOWN_CHOICES = get_frameworks_for_type("Standards and Competencies (AFSCTP)") | |
| # Gradio UI | |
| with gr.Blocks(title="African Union Teacher Frameworks Self Assessment Tool") as demo: | |
| gr.Markdown("# African Union Teacher Frameworks Self Assessment Tool") | |
| gr.Markdown("Evaluate national teacher frameworks against the **African Framework of Standards and Competences for Teachers (AFSCTP)** or the **Continental Teacher Qualification Framework (ATQF/CTQF)**.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # Dynamic criteria display based on selection | |
| with gr.Accordion("View Evaluation Criteria & Scoring Descriptors", open=False): | |
| criteria_markdown = gr.Markdown(AFSCTP_DISPLAY) | |
| gr.Markdown("### Framework Selection") | |
| # Framework type selector (default to Standards) | |
| framework_selector = gr.Dropdown( | |
| label="Select Evaluation Framework", | |
| choices=["Standards and Competencies (AFSCTP)", "Qualifications Framework (ATQF/CTQF)"], | |
| value="Standards and Competencies (AFSCTP)", | |
| interactive=True | |
| ) | |
| gr.Markdown("### Input Selection") | |
| # Dropdown for pre-loaded frameworks - now dynamic based on type | |
| framework_dropdown = gr.Dropdown( | |
| label="Select Pre-loaded Framework", | |
| choices=INITIAL_DROPDOWN_CHOICES, | |
| value=INITIAL_DROPDOWN_CHOICES[0] if INITIAL_DROPDOWN_CHOICES else None, | |
| interactive=True | |
| ) | |
| gr.Markdown("**OR**") | |
| # File upload | |
| file_input = gr.File(label="Upload Custom Document", file_types=[".pdf", ".docx", ".txt"]) | |
| evaluate_btn = gr.Button("Evaluate Framework", variant="primary") | |
| status_text = gr.Textbox(label="Status", value="Waiting", interactive=False) | |
| country_text = gr.Textbox(label="Detected Country", value="Unknown", interactive=False) | |
| pdf_output = gr.File(label="Download PDF Report", visible=False) | |
| with gr.Column(scale=2): | |
| summary_output = gr.Markdown("") | |
| gauge_output = gr.HTML("") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### Detailed Scores by Criterion") | |
| bar_output = gr.HTML() | |
| with gr.Column(): | |
| gr.Markdown("### Alignment Profile") | |
| radar_output = gr.HTML() | |
| with gr.Row(): | |
| table_output = gr.Dataframe(label="Detailed Evaluation Results") | |
| # Update dropdown and criteria when framework type changes | |
| def on_framework_change(framework_type): | |
| choices = get_frameworks_for_type(framework_type) | |
| criteria = AFSCTP_DISPLAY if framework_type == "Standards and Competencies (AFSCTP)" else ATQF_DISPLAY | |
| return { | |
| framework_dropdown: gr.update(choices=choices, value=choices[0] if choices else None), | |
| criteria_markdown: criteria | |
| } | |
| framework_selector.change( | |
| fn=on_framework_change, | |
| inputs=framework_selector, | |
| outputs=[framework_dropdown, criteria_markdown] | |
| ) | |
| # Main evaluation handler | |
| evaluate_btn.click( | |
| fn=process_inputs, | |
| inputs=[file_input, framework_selector, framework_dropdown], | |
| outputs=[table_output, summary_output, bar_output, radar_output, gauge_output, | |
| gr.Number(visible=False), status_text, country_text, pdf_output] | |
| ).then( | |
| lambda ft: gr.update(value=get_frameworks_for_type(ft)[0] if get_frameworks_for_type(ft) else None), | |
| inputs=[framework_selector], | |
| outputs=[framework_dropdown] | |
| ) | |
| demo.launch() |