Spaces:
Running
Running
add health check
Browse files- demo/app.py +13 -2
- demo/health.py +145 -0
- demo/layout.py +52 -0
demo/app.py
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
def create_app() -> gr.Blocks:
|
| 7 |
-
"""
|
| 8 |
with gr.Blocks(title="Aileen3 Demo") as demo:
|
| 9 |
-
gr.HTML("")
|
|
|
|
|
|
|
| 10 |
return demo
|
| 11 |
|
| 12 |
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
import os
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
|
| 7 |
+
import health
|
| 8 |
+
from layout import CELL_CSS, wrap_cell
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def render_health_panel() -> str:
|
| 12 |
+
return wrap_cell(health.render_health_notice())
|
| 13 |
+
|
| 14 |
|
| 15 |
def create_app() -> gr.Blocks:
|
| 16 |
+
"""Create the Gradio application with a minimal notebook-like health cell."""
|
| 17 |
with gr.Blocks(title="Aileen3 Demo") as demo:
|
| 18 |
+
gr.HTML(f"<style>{CELL_CSS}</style>")
|
| 19 |
+
health_panel = gr.HTML(value=wrap_cell(health.render_placeholder_notice()))
|
| 20 |
+
demo.load(fn=render_health_panel, outputs=health_panel, queue=False)
|
| 21 |
return demo
|
| 22 |
|
| 23 |
|
demo/health.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import html
|
| 4 |
+
import importlib.util
|
| 5 |
+
import re
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from itertools import zip_longest
|
| 10 |
+
from typing import Iterable
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
MIN_DENO_VERSION = (2, 0, 0)
|
| 14 |
+
MIN_YTDLP_VERSION = (2025, 11, 12)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class ToolStatus:
|
| 19 |
+
label: str
|
| 20 |
+
ok: bool
|
| 21 |
+
detail: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass
|
| 25 |
+
class HealthReport:
|
| 26 |
+
ok: bool
|
| 27 |
+
summary: str
|
| 28 |
+
tools: list[ToolStatus]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _split_version(raw: str) -> Iterable[int]:
|
| 32 |
+
return tuple(int(part) for part in re.findall(r"\d+", raw))
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _version_at_least(raw: str, expected: tuple[int, ...]) -> bool:
|
| 36 |
+
parsed = _split_version(raw)
|
| 37 |
+
if not parsed:
|
| 38 |
+
return False
|
| 39 |
+
for current, required in zip_longest(parsed, expected, fillvalue=0):
|
| 40 |
+
if current > required:
|
| 41 |
+
return True
|
| 42 |
+
if current < required:
|
| 43 |
+
return False
|
| 44 |
+
return True
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def _check_deno() -> ToolStatus:
|
| 48 |
+
label = "🦕 Deno"
|
| 49 |
+
binary = shutil.which("deno")
|
| 50 |
+
if not binary:
|
| 51 |
+
return ToolStatus(label, False, "`deno` command not found on PATH")
|
| 52 |
+
try:
|
| 53 |
+
completed = subprocess.run(
|
| 54 |
+
[binary, "--version"],
|
| 55 |
+
check=False,
|
| 56 |
+
capture_output=True,
|
| 57 |
+
text=True,
|
| 58 |
+
timeout=10,
|
| 59 |
+
)
|
| 60 |
+
except (FileNotFoundError, subprocess.SubprocessError, OSError) as exc:
|
| 61 |
+
return ToolStatus(label, False, f"Unable to execute: {exc}")
|
| 62 |
+
if completed.returncode != 0:
|
| 63 |
+
detail = completed.stderr.strip() or completed.stdout.strip() or "Unknown error"
|
| 64 |
+
return ToolStatus(label, False, detail)
|
| 65 |
+
match = re.search(r"deno\s+([0-9.]+)", completed.stdout)
|
| 66 |
+
if not match:
|
| 67 |
+
return ToolStatus(label, False, "Could not parse version output")
|
| 68 |
+
version = match.group(1)
|
| 69 |
+
if not _version_at_least(version, MIN_DENO_VERSION):
|
| 70 |
+
return ToolStatus(label, False, f"Found v{version}; need ≥ {'.'.join(map(str, MIN_DENO_VERSION))}")
|
| 71 |
+
return ToolStatus(label, True, f"v{version} ready for yt-dlp")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _check_yt_dlp_python() -> ToolStatus:
|
| 75 |
+
label = "📼 yt-dlp Python"
|
| 76 |
+
try:
|
| 77 |
+
import yt_dlp
|
| 78 |
+
from yt_dlp import YoutubeDL
|
| 79 |
+
from yt_dlp.version import __version__ as version_str
|
| 80 |
+
except Exception as exc:
|
| 81 |
+
return ToolStatus(label, False, f"Import failed: {exc}")
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
with YoutubeDL(params={"skip_download": True, "quiet": True}) as ydl:
|
| 85 |
+
_ = ydl.params.get("skip_download")
|
| 86 |
+
except Exception as exc:
|
| 87 |
+
return ToolStatus(label, False, f"YoutubeDL bootstrap failed: {exc}")
|
| 88 |
+
|
| 89 |
+
if not _version_at_least(version_str, MIN_YTDLP_VERSION):
|
| 90 |
+
minimum = ".".join(map(str, MIN_YTDLP_VERSION))
|
| 91 |
+
return ToolStatus(label, False, f"Detected {version_str}; require ≥ {minimum}")
|
| 92 |
+
|
| 93 |
+
has_ejs = importlib.util.find_spec("yt_dlp_ejs") is not None
|
| 94 |
+
addon_note = "yt_dlp_ejs ready" if has_ejs else "yt_dlp_ejs missing (JS sites will fail)"
|
| 95 |
+
return ToolStatus(label, has_ejs, f"v{version_str} import OK — {addon_note}")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def run_health_report() -> HealthReport:
|
| 99 |
+
tool_statuses = [_check_deno(), _check_yt_dlp_python()]
|
| 100 |
+
ok = all(status.ok for status in tool_statuses)
|
| 101 |
+
if ok:
|
| 102 |
+
summary = "All systems look good—ready for notebook-style experimentation."
|
| 103 |
+
else:
|
| 104 |
+
blocking = ", ".join(status.label for status in tool_statuses if not status.ok)
|
| 105 |
+
summary = f"Needs attention: {blocking}."
|
| 106 |
+
return HealthReport(ok=ok, summary=summary, tools=tool_statuses)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _sanitize(text: str) -> str:
|
| 110 |
+
return html.escape(text, quote=False)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def render_health_notice() -> str:
|
| 114 |
+
report = run_health_report()
|
| 115 |
+
if report.ok:
|
| 116 |
+
bg = "#dcfce7"
|
| 117 |
+
border = "#86efac"
|
| 118 |
+
text_color = "#14532d"
|
| 119 |
+
icon = "✅"
|
| 120 |
+
else:
|
| 121 |
+
bg = "#fee2e2"
|
| 122 |
+
border = "#fecaca"
|
| 123 |
+
text_color = "#7f1d1d"
|
| 124 |
+
icon = "🧯"
|
| 125 |
+
bullet_rows = "".join(
|
| 126 |
+
f"<li>{_sanitize(status.label)}: "
|
| 127 |
+
f"<strong>{'Ready' if status.ok else 'Needs attention'}</strong> — "
|
| 128 |
+
f"{_sanitize(status.detail)}</li>"
|
| 129 |
+
for status in report.tools
|
| 130 |
+
)
|
| 131 |
+
return (
|
| 132 |
+
f"<div class='health-box' style='background:{bg};border-color:{border};color:{text_color};'>"
|
| 133 |
+
f"<div class='health-head'>{icon} {_sanitize(report.summary)}</div>"
|
| 134 |
+
f"<ul>{bullet_rows}</ul>"
|
| 135 |
+
"</div>"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def render_placeholder_notice() -> str:
|
| 140 |
+
return (
|
| 141 |
+
"<div class='health-box health-placeholder'>"
|
| 142 |
+
"<div class='health-head'>⏳ Running automated health check…</div>"
|
| 143 |
+
"<p>Step 0 runs implicitly to validate Deno and yt-dlp before any other cell.</p>"
|
| 144 |
+
"</div>"
|
| 145 |
+
)
|
demo/layout.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
CELL_CSS = """
|
| 4 |
+
.cell-wrapper {
|
| 5 |
+
border: 1px solid rgba(0, 0, 0, 0.9);
|
| 6 |
+
border-radius: 14px;
|
| 7 |
+
padding: 10px;
|
| 8 |
+
margin-bottom: 10px;
|
| 9 |
+
background: transparent;
|
| 10 |
+
}
|
| 11 |
+
.cell-title {
|
| 12 |
+
font-size: 1.15rem;
|
| 13 |
+
font-weight: 600;
|
| 14 |
+
margin: 0 0 10px;
|
| 15 |
+
}
|
| 16 |
+
.health-box {
|
| 17 |
+
border-radius: 14px;
|
| 18 |
+
padding: 18px;
|
| 19 |
+
border: 1px solid transparent;
|
| 20 |
+
}
|
| 21 |
+
.health-box ul {
|
| 22 |
+
margin: 12px 0 0;
|
| 23 |
+
padding-left: 22px;
|
| 24 |
+
}
|
| 25 |
+
.health-box li {
|
| 26 |
+
margin-bottom: 6px;
|
| 27 |
+
}
|
| 28 |
+
.health-placeholder {
|
| 29 |
+
background: #f5f5f5 !important;
|
| 30 |
+
border-color: #d4d4d4 !important;
|
| 31 |
+
color: #1f2937 !important;
|
| 32 |
+
}
|
| 33 |
+
@media (prefers-color-scheme: dark) {
|
| 34 |
+
.cell-wrapper {
|
| 35 |
+
border-color: rgba(255, 255, 255, 0.45);
|
| 36 |
+
}
|
| 37 |
+
.health-placeholder {
|
| 38 |
+
background: rgba(148, 163, 184, 0.15) !important;
|
| 39 |
+
border-color: rgba(148, 163, 184, 0.4) !important;
|
| 40 |
+
color: #e2e8f0 !important;
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def wrap_cell(body_html: str) -> str:
|
| 47 |
+
return (
|
| 48 |
+
"<section class='cell-wrapper'>"
|
| 49 |
+
"<div class='cell-title'>🧪 Health check</div>"
|
| 50 |
+
f"{body_html}"
|
| 51 |
+
"</section>"
|
| 52 |
+
)
|