omission-oracle / app.py
Bitsage
Fix: Load real-time data from registry API with Shadow Scores
9b56ccc
"""
CROVIA OMISSION ORACLE v2.1.0
The Evidence Protocol for AI Training Compliance
Building the global standard for AI trust — in public, from day one.
"""
import json
import hashlib
import gradio as gr
import requests
from datetime import datetime, timezone
from typing import Dict, List, Any, Optional
VERSION = "2.1.0"
PROTOCOL_VERSION = "v1.0"
TPR_API_URL = "https://registry.croviatrust.com"
DATASET_ID = "Crovia/global-ai-training-omissions"
# Fallback cards for display
FALLBACK_CARDS = [
{"model_id": "meta-llama/Llama-3.1-8B", "score": 72, "badge": "BRONZE", "violations": ["NEC#1", "NEC#7"], "author": "meta-llama", "downloads": "N/A"},
{"model_id": "mistralai/Mistral-7B-v0.3", "score": 94, "badge": "GOLD", "violations": [], "author": "mistralai", "downloads": "N/A"},
]
try:
from huggingface_hub import hf_hub_download
HF_AVAILABLE = True
except ImportError:
HF_AVAILABLE = False
def load_latest_cards_from_registry() -> List[Dict[str, Any]]:
"""Load latest observations from live registry API with Shadow Scores."""
try:
# Fetch live data from registry
response = requests.get(f"{TPR_API_URL}/api/registry/recent?limit=12", timeout=5)
if response.status_code == 200:
data = response.json()
observations = data.get('observations', [])
# Get Shadow Scores from dataset (updated by cron)
shadow_scores = {}
try:
if HF_AVAILABLE:
index_path = hf_hub_download(
repo_id=DATASET_ID,
filename="badges/index.json",
repo_type="dataset"
)
with open(index_path, 'r') as f:
badges_data = json.load(f)
for badge in badges_data.get("badges", []):
shadow_scores[badge.get("model_id")] = badge.get("score", 0)
except:
pass
# Convert observations to card format
cards = []
for obs in observations:
# Skip test/internal files
target_id = obs.get('target_id', '')
if any(pattern in target_id.lower() for pattern in ['test-', 'crovia-', 'debug-', 'demo-']):
continue
# Get Shadow Score if available
score = shadow_scores.get(target_id, 0)
# Determine badge based on score
if score >= 90:
badge = "GOLD"
elif score >= 70:
badge = "SILVER"
elif score >= 50:
badge = "BRONZE"
else:
badge = "UNVERIFIED"
cards.append({
"model_id": target_id,
"score": score,
"badge": badge,
"violations": [],
"author": target_id.split("/")[0] if "/" in target_id else "Unknown",
"downloads": "N/A",
"observation_type": obs.get('observation_type', 'unknown'),
"observed_at": obs.get('observed_at', '')
})
if cards:
return cards
# Fallback to dataset if API fails
return load_latest_cards_from_dataset()
except Exception as e:
print(f"Error loading from registry: {e}")
return load_latest_cards_from_dataset()
def load_latest_cards_from_dataset() -> List[Dict[str, Any]]:
"""Load pre-computed model cards from public dataset (fallback)."""
try:
if not HF_AVAILABLE:
return FALLBACK_CARDS
index_path = hf_hub_download(
repo_id=DATASET_ID,
filename="badges/index.json",
repo_type="dataset"
)
with open(index_path, 'r') as f:
data = json.load(f)
cards = []
for badge in data.get("badges", [])[:12]:
model_id = badge.get("model_id", "Unknown")
score = badge.get("score", 0)
badge_type = badge.get("badge", "UNVERIFIED")
violations = []
try:
card_filename = f"cards/{model_id.replace('/', '__')}.json"
card_path = hf_hub_download(
repo_id=DATASET_ID,
filename=card_filename,
repo_type="dataset"
)
with open(card_path, 'r') as cf:
card_data = json.load(cf)
violations = card_data.get("violations", [])
except:
pass
author = model_id.split("/")[0] if "/" in model_id else "Unknown"
cards.append({
"model_id": model_id,
"score": score,
"badge": badge_type,
"violations": violations,
"author": author,
"downloads": "N/A"
})
return cards if cards else FALLBACK_CARDS
except Exception as e:
print(f"Error loading cards: {e}")
return FALLBACK_CARDS
# Load cards from live registry (real-time data)
PRELOADED_CARDS = load_latest_cards_from_registry()
NECESSITY_CANON = {
"NEC#1": {"name": "Missing data provenance", "severity": 75},
"NEC#2": {"name": "Missing license attribution", "severity": 80},
"NEC#7": {"name": "Missing usage scope", "severity": 45},
"NEC#10": {"name": "Missing temporal validity", "severity": 40},
"NEC#13": {"name": "Missing accountable entity", "severity": 70},
}
def analyze_model_live(model_id: str) -> Optional[Dict[str, Any]]:
"""Live analysis via TPR API (if available)."""
if not model_id or not model_id.strip():
return None
try:
response = requests.post(
f"{TPR_API_URL}/api/analyze",
json={"model_id": model_id.strip()},
timeout=30
)
if response.status_code == 200:
result = response.json()
result["analyzed_via"] = "tpr-api"
result["timestamp"] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
return result
else:
return None
except:
return None
def analyze_model_fallback(model_id: str) -> Optional[Dict[str, Any]]:
"""Fallback analysis using HuggingFace API directly."""
if not model_id or not model_id.strip():
return None
model_id = model_id.strip()
if not HF_AVAILABLE:
return {"error": "Analysis unavailable - HuggingFace Hub not accessible"}
try:
from huggingface_hub import HfApi
api = HfApi()
info = api.model_info(model_id)
card_data = info.card_data or {}
license_val = card_data.get("license") or getattr(info, "license", None)
datasets = card_data.get("datasets", []) or []
author = info.author or ""
tags = info.tags or []
violations = []
if not datasets:
violations.append("NEC#1")
if not license_val:
violations.append("NEC#2")
has_use = any("task" in t.lower() or "use" in t.lower() for t in tags)
if not has_use:
violations.append("NEC#7")
if not author:
violations.append("NEC#13")
total_severity = sum(NECESSITY_CANON.get(v, {}).get("severity", 30) for v in violations)
score = max(0, min(100, 100 - int(total_severity * 0.15)))
if score >= 90:
badge = "GOLD"
elif score >= 75:
badge = "SILVER"
elif score >= 60:
badge = "BRONZE"
else:
badge = "UNVERIFIED"
return {
"model_id": model_id,
"score": score,
"badge": badge,
"violations": violations,
"author": author or "Unknown",
"license": license_val or "Not declared",
"evidence": hashlib.sha256(f"{model_id}:{score}".encode()).hexdigest()[:12],
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
"analyzed_via": "hf-fallback"
}
except Exception as e:
return {"error": f"Model not found: {model_id}"}
def analyze_model(model_id: str) -> Optional[Dict[str, Any]]:
"""Main analysis function with TPR API and fallback."""
result = analyze_model_live(model_id)
if not result:
result = analyze_model_fallback(model_id)
return result
def get_badge_color(badge: str) -> str:
colors = {
"GOLD": "#F59E0B",
"SILVER": "#94A3B8",
"BRONZE": "#D97706",
"UNVERIFIED": "#EF4444"
}
return colors.get(badge, "#6B7280")
def run_analysis(model_id: str):
result = analyze_model(model_id)
if not result:
return "", "", ""
if "error" in result:
error_msg = result.get("message", result["error"])
return f'<div class="error-box">{error_msg}</div>', "", ""
color = get_badge_color(result["badge"])
score_html = f'''
<div class="result-card">
<div class="score-circle" style="border-color: {color}; box-shadow: 0 0 30px {color}40;">
<span class="score-value" style="color: {color};">{result["score"]}</span>
<span class="score-label">SHADOW SCORE</span>
</div>
<div class="badge-pill" style="background: {color}20; color: {color};">{result["badge"]}</div>
</div>
'''
info_html = f'''
<div class="info-card">
<div class="info-header">
<span class="info-label">MODEL</span>
<span class="info-value">{result["model_id"]}</span>
</div>
<div class="info-row">
<span>Author: <strong>{result.get("author", "Unknown")}</strong></span>
</div>
<div class="info-row">
<span>License: <strong>{result.get("license", "Not declared")}</strong></span>
</div>
<div class="evidence">
Evidence: {result.get("evidence", "N/A")} · {result.get("timestamp", "")}
</div>
</div>
'''
if result.get("violations"):
viol_items = "".join([f'<span class="viol-tag">{v}</span>' for v in result["violations"]])
viol_html = f'<div class="violations-box"><span class="viol-label">VIOLATIONS:</span>{viol_items}</div>'
else:
viol_html = '<div class="clean-box">✓ ALL CHECKS PASSED</div>'
if result.get("analyzed_via") == "hf-fallback":
viol_html += '<div class="note-box">⚠️ Basic analysis mode (TPR API unavailable)</div>'
return score_html, info_html, viol_html
def generate_cards_html(cards_data=None):
if cards_data is None:
cards_data = load_latest_cards_from_registry()
cards = ""
for c in cards_data:
color = get_badge_color(c["badge"])
viols = " ".join([f'<span class="card-viol">{v}</span>' for v in c["violations"]]) if c["violations"] else '<span class="card-clean">CLEAN</span>'
cards += f'''
<div class="model-card">
<div class="card-header">
<span class="card-score" style="color:{color};">{c["score"]}</span>
<span class="card-badge" style="background:{color}20;color:{color};">{c["badge"]}</span>
</div>
<div class="card-model">{c["model_id"]}</div>
<div class="card-meta">{c["author"]}</div>
<div class="card-violations">{viols}</div>
</div>
'''
return cards
CSS = """
.gradio-container {
max-width: 100% !important;
padding: 0 !important;
margin: 0 !important;
background: #030305 !important;
}
.main, .contain, .wrap {
max-width: 100% !important;
padding: 0 !important;
}
footer { display: none !important; }
.gr-form { background: transparent !important; border: none !important; }
.hero {
background: linear-gradient(180deg, #0a0a12 0%, #030305 100%);
padding: 60px 20px 40px;
text-align: center;
border-bottom: 1px solid #1a1a2e;
}
.hero-badge {
display: inline-block;
background: #8B5CF620;
border: 1px solid #8B5CF640;
color: #8B5CF6;
padding: 6px 16px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 20px;
}
.hero-logo {
width: 70px;
height: 70px;
border-radius: 16px;
margin: 0 auto 20px;
background: linear-gradient(135deg, #8B5CF6 0%, #6D28D9 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 40px #8B5CF650;
}
.hero-logo img { width: 50px; height: 50px; }
.hero-title {
font-size: 42px;
font-weight: 900;
color: #fff;
letter-spacing: 0.15em;
margin: 0 0 12px 0;
text-shadow: 0 0 30px #8B5CF640;
}
.hero-subtitle {
color: #9CA3AF;
font-size: 18px;
font-weight: 500;
margin: 0 0 20px 0;
letter-spacing: 0.02em;
}
.hero-desc {
color: #6B7280;
font-size: 15px;
max-width: 700px;
margin: 0 auto 30px;
line-height: 1.7;
}
.hero-emphasis {
color: #8B5CF6;
font-weight: 600;
}
.hero-stats {
display: flex;
justify-content: center;
gap: 40px;
margin: 30px auto 0;
max-width: 900px;
flex-wrap: wrap;
}
.stat-item {
text-align: center;
}
.stat-label {
color: #6B7280;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 600;
}
.stat-value {
color: #fff;
font-size: 16px;
font-weight: 700;
margin-top: 4px;
}
.stat-link {
color: #8B5CF6;
text-decoration: none;
font-size: 16px;
font-weight: 700;
}
.stat-link:hover {
color: #A78BFA;
}
.nav {
display: flex;
justify-content: center;
gap: 24px;
padding: 16px;
background: #050507;
border-bottom: 1px solid #1a1a2e;
flex-wrap: wrap;
}
.nav a {
color: #9CA3AF;
text-decoration: none;
font-size: 13px;
font-weight: 500;
transition: color 0.2s;
}
.nav a:hover { color: #8B5CF6; }
.search-section {
max-width: 700px;
margin: 40px auto;
padding: 0 20px;
}
.search-box {
display: flex;
gap: 12px;
}
input[type="text"], textarea {
flex: 1;
background: #0a0a12 !important;
border: 2px solid #1a1a2e !important;
color: #fff !important;
border-radius: 12px !important;
padding: 16px 20px !important;
font-size: 16px !important;
}
input:focus, textarea:focus {
border-color: #8B5CF6 !important;
box-shadow: 0 0 20px #8B5CF620 !important;
outline: none !important;
}
button.primary {
background: linear-gradient(135deg, #8B5CF6 0%, #6D28D9 100%) !important;
border: none !important;
padding: 16px 32px !important;
border-radius: 12px !important;
font-weight: 700 !important;
font-size: 14px !important;
letter-spacing: 0.05em !important;
color: #fff !important;
cursor: pointer !important;
transition: all 0.2s;
}
button.primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px #8B5CF650 !important;
}
.results-section {
max-width: 800px;
margin: 0 auto 40px;
padding: 0 20px;
}
.result-card {
background: #0a0a12;
border: 1px solid #1a1a2e;
border-radius: 16px;
padding: 30px;
text-align: center;
}
.score-circle {
width: 140px;
height: 140px;
border-radius: 50%;
border: 4px solid;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
background: #050507;
}
.score-value { font-size: 48px; font-weight: 900; }
.score-label { font-size: 10px; color: #6B7280; letter-spacing: 0.1em; }
.badge-pill {
display: inline-block;
padding: 8px 24px;
border-radius: 20px;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.1em;
}
.info-card {
background: #0a0a12;
border: 1px solid #1a1a2e;
border-radius: 16px;
padding: 20px;
margin-top: 16px;
}
.info-header { margin-bottom: 12px; }
.info-label { color: #8B5CF6; font-size: 10px; letter-spacing: 0.1em; display: block; }
.info-value { color: #fff; font-size: 18px; font-weight: 700; }
.info-row { color: #9CA3AF; font-size: 13px; margin: 8px 0; }
.info-row strong { color: #fff; }
.evidence { color: #4B5563; font-size: 11px; font-family: monospace; margin-top: 12px; padding-top: 12px; border-top: 1px solid #1a1a2e; }
.violations-box { background: #1a0a0a; border: 1px solid #EF444440; border-radius: 12px; padding: 16px; margin-top: 16px; }
.viol-label { color: #EF4444; font-size: 11px; letter-spacing: 0.1em; margin-right: 12px; }
.viol-tag { background: #EF444420; color: #EF4444; padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; margin: 0 4px; }
.clean-box { background: #0a1a0a; border: 1px solid #22C55E40; border-radius: 12px; padding: 16px; margin-top: 16px; color: #22C55E; text-align: center; font-weight: 600; }
.note-box { background: #1a1a0a; border: 1px solid #F59E0B40; border-radius: 12px; padding: 12px; margin-top: 12px; color: #F59E0B; text-align: center; font-size: 12px; }
.error-box { background: #1a0a0a; border: 1px solid #EF4444; border-radius: 12px; padding: 20px; color: #EF4444; text-align: center; }
.cards-section {
background: #050507;
padding: 40px 20px;
border-top: 1px solid #1a1a2e;
}
.cards-title {
text-align: center;
color: #fff;
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.cards-sub {
text-align: center;
color: #6B7280;
font-size: 13px;
margin-bottom: 30px;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
max-width: 1200px;
margin: 0 auto;
}
.model-card {
background: #0a0a12;
border: 1px solid #1a1a2e;
border-radius: 12px;
padding: 20px;
transition: transform 0.2s, box-shadow 0.2s;
}
.model-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 30px rgba(139, 92, 246, 0.15);
border-color: #8B5CF640;
}
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.card-score { font-size: 32px; font-weight: 900; }
.card-badge { padding: 4px 12px; border-radius: 6px; font-size: 11px; font-weight: 700; }
.card-model { color: #fff; font-size: 14px; font-weight: 600; margin-bottom: 4px; word-break: break-all; }
.card-meta { color: #6B7280; font-size: 12px; margin-bottom: 12px; }
.card-violations { display: flex; flex-wrap: wrap; gap: 6px; }
.card-viol { background: #EF444420; color: #EF4444; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; }
.card-clean { background: #22C55E20; color: #22C55E; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; }
.site-footer {
background: #030305;
border-top: 1px solid #1a1a2e;
padding: 40px 20px 30px;
text-align: center;
}
.footer-tagline {
color: #8B5CF6;
font-size: 14px;
font-weight: 600;
margin-bottom: 20px;
letter-spacing: 0.05em;
}
.footer-links { display: flex; justify-content: center; gap: 24px; margin-bottom: 20px; flex-wrap: wrap; }
.footer-links a { color: #6B7280; text-decoration: none; font-size: 13px; }
.footer-links a:hover { color: #8B5CF6; }
.footer-copy { color: #4B5563; font-size: 11px; margin-bottom: 8px; }
.footer-protocol { color: #374151; font-size: 10px; font-family: monospace; }
@media (max-width: 640px) {
.hero-title { font-size: 32px; }
.hero-subtitle { font-size: 16px; }
.hero-stats { gap: 20px; }
.nav { gap: 16px; }
.search-box { flex-direction: column; }
button.primary { width: 100%; }
.cards-grid { grid-template-columns: 1fr; }
}
"""
HEADER_HTML = f'''
<div class="hero">
<div class="hero-badge">🔴 FOUNDING STAGE · PROTOCOL {PROTOCOL_VERSION}</div>
<div class="hero-logo">
<img src="https://huggingface.co/spaces/Crovia/omission-oracle/resolve/main/crovia-logo.png" alt="Crovia" onerror="this.style.display='none'">
</div>
<h1 class="hero-title">CROVIA</h1>
<p class="hero-subtitle">The Evidence Protocol for AI Training</p>
<p class="hero-desc">
We're building the <span class="hero-emphasis">global standard</span> for AI compliance verification
— in public, from day one. Cryptographic evidence. Multi-regulation. Open protocol.
</p>
<div class="hero-stats">
<div class="stat-item">
<div class="stat-label">Protocol</div>
<div class="stat-value">{PROTOCOL_VERSION} · Jan 2026</div>
</div>
<div class="stat-item">
<div class="stat-label">Public Dataset</div>
<a href="https://huggingface.co/datasets/{DATASET_ID}" target="_blank" class="stat-link">
Updated Every 6h →
</a>
</div>
<div class="stat-item">
<div class="stat-label">Open Source</div>
<a href="https://github.com/croviatrust" target="_blank" class="stat-link">
GitHub →
</a>
</div>
</div>
</div>
<div class="nav">
<a href="https://croviatrust.com" target="_blank">🏠 Home</a>
<a href="https://huggingface.co/datasets/{DATASET_ID}" target="_blank">📊 Rankings</a>
<a href="https://huggingface.co/datasets/Crovia/cep-capsules" target="_blank">📦 Evidence Capsules</a>
<a href="https://huggingface.co/spaces/Crovia/cep-terminal" target="_blank">🔍 CEP Terminal</a>
<a href="https://github.com/croviatrust" target="_blank">⚙️ GitHub</a>
<a href="https://github.com/croviatrust/CROVIA_DEV/blob/master/CROVIA_PROTOCOL_SPEC.md" target="_blank">📄 Protocol Spec</a>
</div>
'''
CARDS_HTML = f'''
<div class="cards-section">
<h2 class="cards-title">Latest Oracle Cards</h2>
<p class="cards-sub">Auto-generated evidence · Public dataset · Updated every 6 hours</p>
<div class="cards-grid">
{generate_cards_html()}
</div>
</div>
'''
FOOTER_HTML = '''
<div class="site-footer">
<div class="footer-tagline">Evidence, not promises. Observation, not judgment.</div>
<div class="footer-links">
<a href="https://croviatrust.com">Official Site</a>
<a href="https://github.com/croviatrust">GitHub</a>
<a href="https://huggingface.co/datasets/Crovia/global-ai-training-omissions">Dataset</a>
<a href="https://github.com/croviatrust/CROVIA_DEV/blob/master/CROVIA_PROTOCOL_SPEC.md">Protocol Spec</a>
<a href="https://github.com/croviatrust/CROVIA_DEV/blob/master/TPR_RESOLUTION_CONTRACT.md">TPR Contract</a>
</div>
<p class="footer-copy">© 2026 Crovia Protocol · Apache 2.0 License</p>
<p class="footer-protocol">Built in public by contributors worldwide.</p>
</div>
'''
with gr.Blocks(css=CSS, title="CROVIA · Omission Oracle") as app:
gr.HTML(HEADER_HTML)
with gr.Column(elem_classes="search-section"):
with gr.Row(elem_classes="search-box"):
model_input = gr.Textbox(
placeholder="Enter model ID (e.g., meta-llama/Llama-3.1-8B)",
show_label=False,
container=False,
)
analyze_btn = gr.Button("🔍 ANALYZE", variant="primary")
with gr.Column(elem_classes="results-section"):
score_output = gr.HTML()
info_output = gr.HTML()
viol_output = gr.HTML()
gr.HTML(CARDS_HTML)
gr.HTML(FOOTER_HTML)
analyze_btn.click(run_analysis, inputs=[model_input], outputs=[score_output, info_output, viol_output])
model_input.submit(run_analysis, inputs=[model_input], outputs=[score_output, info_output, viol_output])
if __name__ == "__main__":
app.launch()