Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """Gradio Pangram demo app for a Hugging Face Space. | |
| Paste a Pangram API key live, paste text, and inspect: | |
| - overall prediction | |
| - percentage breakdown | |
| - annotated text spans | |
| - flagged windows | |
| No key or text is persisted by this app. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| from collections import Counter | |
| from dataclasses import dataclass | |
| from typing import Any | |
| from urllib import error, request | |
| import gradio as gr | |
| API_URL = "https://text.api.pangram.com/v3" | |
| COLOR_MAP = { | |
| "AI": "#f1b7b0", | |
| "AI-assisted": "#f2d99c", | |
| "Human": "#d9e8d1", | |
| "Neutral": "#ece6d9", | |
| } | |
| def _coerce_int(value: Any, default: int = 0) -> int: | |
| try: | |
| return int(value) | |
| except (TypeError, ValueError): | |
| return default | |
| def _coerce_float(value: Any) -> float | None: | |
| try: | |
| return float(value) | |
| except (TypeError, ValueError): | |
| return None | |
| class Window: | |
| start: int | |
| end: int | |
| label: str | |
| confidence: float | None | |
| def _pretty_label(value: Any) -> str: | |
| if value is None: | |
| return "-" | |
| text = str(value).replace("_", " ").strip() | |
| return " ".join(part[:1].upper() + part[1:] for part in text.split()) or "-" | |
| def _normalize_label(value: Any) -> str: | |
| text = str(value or "").lower() | |
| if "assist" in text: | |
| return "AI-assisted" | |
| if "human" in text: | |
| return "Human" | |
| if "ai" in text: | |
| return "AI" | |
| return "Neutral" | |
| def _post_json(url: str, api_key: str, payload: dict[str, Any]) -> dict[str, Any]: | |
| body = json.dumps(payload).encode("utf-8") | |
| req = request.Request( | |
| url, | |
| data=body, | |
| headers={ | |
| "Content-Type": "application/json", | |
| "X-API-Key": api_key, | |
| }, | |
| method="POST", | |
| ) | |
| try: | |
| with request.urlopen(req, timeout=60) as response: | |
| return json.loads(response.read().decode("utf-8")) | |
| except error.HTTPError as exc: | |
| raw = exc.read().decode("utf-8", errors="replace") | |
| try: | |
| data = json.loads(raw) | |
| except json.JSONDecodeError: | |
| data = {"error": raw or exc.reason} | |
| message = data.get("error") or data.get("detail") or data.get("message") or str(exc) | |
| raise gr.Error(f"Pangram request failed: {message}") from exc | |
| except Exception as exc: # pragma: no cover - network edge cases | |
| raise gr.Error(f"Pangram request failed: {exc}") from exc | |
| def _extract_windows(raw_windows: Any) -> list[Window]: | |
| windows: list[Window] = [] | |
| if not isinstance(raw_windows, list): | |
| return windows | |
| for item in raw_windows: | |
| if not isinstance(item, dict): | |
| continue | |
| start = max(0, _coerce_int(item.get("start_index", 0), 0)) | |
| end = max(start, _coerce_int(item.get("end_index", start), start)) | |
| confidence = _coerce_float(item.get("confidence")) | |
| windows.append( | |
| Window( | |
| start=start, | |
| end=end, | |
| label=_normalize_label(item.get("label")), | |
| confidence=confidence, | |
| ) | |
| ) | |
| windows.sort(key=lambda w: (w.start, w.end)) | |
| return windows | |
| def _build_highlighted_segments(text: str, windows: list[Window]) -> list[tuple[str, str | None]]: | |
| if not text: | |
| return [] | |
| segments: list[tuple[str, str | None]] = [] | |
| cursor = 0 | |
| for window in windows: | |
| start = min(max(window.start, cursor, 0), len(text)) | |
| end = min(max(window.end, start), len(text)) | |
| if start > cursor: | |
| segments.append((text[cursor:start], None)) | |
| if end > start: | |
| segments.append((text[start:end], window.label)) | |
| cursor = end | |
| if cursor < len(text): | |
| segments.append((text[cursor:], None)) | |
| if not segments: | |
| segments.append((text, None)) | |
| return segments | |
| def _percentages_table(raw_percentages: Any) -> list[list[str]]: | |
| if not isinstance(raw_percentages, dict): | |
| return [] | |
| rows: list[list[str]] = [] | |
| sortable: list[tuple[str, float]] = [] | |
| for label, value in raw_percentages.items(): | |
| pct = _coerce_float(value) | |
| if pct is None: | |
| continue | |
| sortable.append((str(label), pct)) | |
| for label, pct in sorted(sortable, key=lambda item: item[1], reverse=True): | |
| rows.append([_pretty_label(label), f"{pct:.1f}%"]) | |
| return rows | |
| def _flagged_windows_table(text: str, windows: list[Window]) -> list[list[str]]: | |
| rows: list[list[str]] = [] | |
| for index, window in enumerate(windows, start=1): | |
| if window.label not in {"AI", "AI-assisted"}: | |
| continue | |
| excerpt = text[window.start:window.end].strip() or "[empty window]" | |
| confidence = "-" if window.confidence is None else f"{window.confidence:.3f}" | |
| rows.append([str(index), window.label, confidence, excerpt]) | |
| return rows | |
| def _summary_markdown(result: dict[str, Any], windows: list[Window]) -> str: | |
| prediction = _pretty_label(result.get("prediction_short") or result.get("prediction")) | |
| public_link = result.get("public_dashboard_link") | |
| flagged = sum(1 for window in windows if window.label in {"AI", "AI-assisted"}) | |
| counts = Counter(window.label for window in windows) | |
| top_label = counts.most_common(1)[0][0] if counts else "-" | |
| lines = [ | |
| f"**Prediction:** {prediction}", | |
| f"**Flagged windows:** {flagged} / {len(windows)}", | |
| f"**Most frequent window label:** {top_label}", | |
| ] | |
| if public_link: | |
| lines.append(f"**Public dashboard:** {public_link}") | |
| return "\n\n".join(lines) | |
| def analyze_text(api_key: str, text: str, public_dashboard_link: bool) -> tuple[Any, ...]: | |
| api_key = (api_key or "").strip() | |
| text = text or "" | |
| if not api_key: | |
| raise gr.Error("Paste a Pangram API key first.") | |
| if not text.strip(): | |
| raise gr.Error("Paste text to analyze first.") | |
| payload = { | |
| "text": text, | |
| "public_dashboard_link": bool(public_dashboard_link), | |
| } | |
| result = _post_json(API_URL, api_key, payload) | |
| windows = _extract_windows(result.get("windows")) | |
| highlighted = _build_highlighted_segments(text, windows) | |
| percentages = _percentages_table(result.get("percentages")) | |
| flagged = _flagged_windows_table(text, windows) | |
| summary = _summary_markdown(result, windows) | |
| return ( | |
| summary, | |
| highlighted, | |
| percentages, | |
| flagged, | |
| result, | |
| ) | |
| def clear_outputs() -> tuple[Any, ...]: | |
| return ("", "", False, "", [], [], {}, []) | |
| with gr.Blocks(title="Pangram Live Demo") as demo: | |
| gr.Markdown( | |
| """ | |
| # Pangram Live Demo | |
| Paste a Pangram API key and a text sample to inspect flagged spans live. | |
| The key is used only for the current request and is not stored by the app. | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| api_key = gr.Textbox( | |
| label="Pangram API key", | |
| type="password", | |
| placeholder="Paste live API key here", | |
| ) | |
| source_text = gr.Textbox( | |
| label="Text to analyze", | |
| lines=16, | |
| placeholder="Paste text here...", | |
| ) | |
| public_link = gr.Checkbox( | |
| label="Request public dashboard link", | |
| value=False, | |
| ) | |
| with gr.Row(): | |
| analyze = gr.Button("Analyze", variant="primary") | |
| clear = gr.Button("Clear") | |
| with gr.Column(scale=5): | |
| summary = gr.Markdown(label="Summary") | |
| annotated = gr.HighlightedText( | |
| label="Annotated output", | |
| color_map=COLOR_MAP, | |
| combine_adjacent=True, | |
| show_legend=True, | |
| show_inline_category=False, | |
| ) | |
| percentages = gr.Dataframe( | |
| headers=["Label", "Percentage"], | |
| datatype=["str", "str"], | |
| row_count=(0, "dynamic"), | |
| col_count=(2, "fixed"), | |
| label="Percentages", | |
| wrap=True, | |
| ) | |
| flagged = gr.Dataframe( | |
| headers=["#", "Label", "Confidence", "Excerpt"], | |
| datatype=["str", "str", "str", "str"], | |
| row_count=(0, "dynamic"), | |
| col_count=(4, "fixed"), | |
| label="Flagged windows", | |
| wrap=True, | |
| ) | |
| raw_json = gr.JSON(label="Raw Pangram response") | |
| analyze.click( | |
| fn=analyze_text, | |
| inputs=[api_key, source_text, public_link], | |
| outputs=[summary, annotated, percentages, flagged, raw_json], | |
| api_name=False, | |
| ) | |
| clear.click( | |
| fn=clear_outputs, | |
| inputs=[], | |
| outputs=[api_key, source_text, public_link, summary, percentages, flagged, raw_json, annotated], | |
| api_name=False, | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |