Files
momentry_core/scripts/dashboard.py
Accusys 39ba5ddf76 feat: Phase 1 handover - schema migration, correction mechanism, API fixes
Schema changes: dev.chunks->dev.chunk, remove old_chunk_id/chunk_index
Correction: asr-1.json format, generate/apply scripts
API: 37/37 endpoints fixed and tested
Docs: HANDOVER_V2.0.md for M4
2026-05-11 07:03:22 +08:00

472 lines
16 KiB
Python

#!/opt/homebrew/bin/python3.11
"""
Momentry Dashboard v2 — Direct DB/Qdrant/Redis queries, no subprocess blocking
"""
import json, os, platform, time
from pathlib import Path
from flask import Flask, jsonify, render_template_string
import psycopg2
import urllib.request
app = Flask(__name__)
PROJECT = Path(__file__).resolve().parent.parent
HOSTNAME = platform.node()
IS_M5 = "MacBook" in HOSTNAME
SYSTEM_ROLE = "M5 (MacBook Pro)" if IS_M5 else "M4 (Mac Mini)"
SYSTEM_COLOR = "#58a6ff" if IS_M5 else "#f0883e"
DB_URL = "postgresql://accusys@localhost:5432/momentry?host=/tmp"
QDRANT_URL = "http://localhost:6333"
LLM_URL = "http://localhost:8082/v1/chat/completions"
EMBED_URL = "http://localhost:11436/v1/embeddings"
COLLECTIONS = [
"momentry_dev_v1", "momentry_dev_stories", "momentry_dev_voice",
"momentry_dev_faces", "sentence_story", "sentence_summary",
"momentry_dev_rule1_v2",
]
UUID = "aeed71342a899fe4b4c57b7d41bcb692"
def db_query(sql, params=None):
conn = psycopg2.connect(DB_URL)
cur = conn.cursor()
cur.execute(sql, params or ())
rows = cur.fetchall()
conn.close()
return rows
def qdrant_get(path):
try:
resp = urllib.request.urlopen(f"{QDRANT_URL}{path}", timeout=5)
return json.loads(resp.read())
except:
return None
def qdrant_count(col):
r = qdrant_get(f"/collections/{col}")
if r:
return r.get("result", {}).get("points_count", 0)
return -1
def qdrant_dim(col):
r = qdrant_get(f"/collections/{col}")
if r:
cfg = r.get("result", {}).get("config", {}).get("params", {}).get("vectors", {})
return cfg.get("size", "?")
return "?"
@app.route("/")
def index():
return render_template_string(TEMPLATE, SYSTEM_ROLE=SYSTEM_ROLE)
@app.route("/api/all")
def api_all():
return jsonify({
"system": {"hostname": HOSTNAME, "role": SYSTEM_ROLE, "is_m5": IS_M5},
"status": get_status(),
"qdrant": get_qdrant_info(),
"db": get_db_info(),
"processes": get_processes(),
})
@app.route("/api/status")
def api_status():
return jsonify(get_status())
@app.route("/api/qdrant")
def api_qdrant():
return jsonify(get_qdrant_info())
@app.route("/api/db")
def api_db():
return jsonify(get_db_info())
@app.route("/api/processes")
def api_processes():
return jsonify(get_processes())
def get_status():
"""Pipeline checklist — direct DB queries"""
t0 = time.time()
stages = []
# 1. ASR file
asr_path = f"/Users/accusys/momentry/output_dev/{UUID}.asr.json"
asr_segs = 0
try:
if os.path.exists(asr_path):
d = json.load(open(asr_path))
asr_segs = len(d.get("segments", []))
except: pass
stages.append({"name":"ASR","passed":asr_segs>0,"detail":f"{asr_segs} seg","elapsed":0.0})
# 2. ASRX file
asrx_path = f"/Users/accusys/momentry/output_dev/{UUID}.asrx.json"
asrx_segs = 0
try:
if os.path.exists(asrx_path):
d = json.load(open(asrx_path))
asrx_segs = len(d.get("segments", []))
except: pass
stages.append({"name":"ASRX","passed":asrx_segs>0,"detail":f"{asrx_segs} seg","elapsed":0.0})
# 3. Sentence chunks
try:
cnt = db_query("SELECT count(*) FROM dev.chunks WHERE file_uuid=%s AND chunk_type='sentence'", (UUID,))[0][0]
except:
cnt = 0
stages.append({"name":"Sentence","passed":cnt>0,"detail":f"{cnt} chunks","elapsed":0.0})
# 4. Vectorization (Qdrant)
v1 = qdrant_count("momentry_dev_v1")
stages.append({"name":"Vectorize","passed":v1>0,"detail":f"{v1} Qdrant","elapsed":0.0})
# 5. Face traces
try:
traces = db_query("SELECT count(DISTINCT trace_id) FROM dev.face_detections WHERE file_uuid=%s AND trace_id IS NOT NULL", (UUID,))[0][0]
faces = db_query("SELECT count(*) FROM dev.face_detections WHERE file_uuid=%s AND trace_id IS NOT NULL", (UUID,))[0][0]
except:
traces = faces = 0
stages.append({"name":"FaceTrace","passed":traces>0,"detail":f"{traces} traces, {faces} faces","elapsed":0.0})
# 6. TKG
try:
nodes = db_query("SELECT count(*) FROM dev.tkg_nodes WHERE file_uuid=%s", (UUID,))[0][0]
edges = db_query("SELECT count(*) FROM dev.tkg_edges WHERE file_uuid=%s", (UUID,))[0][0]
except:
nodes = edges = 0
stages.append({"name":"TKG","passed":nodes>0,"detail":f"{nodes} nodes, {edges} edges","elapsed":0.0})
# 7. Trace chunks
try:
tc = db_query("SELECT count(*) FROM dev.chunks WHERE file_uuid=%s AND chunk_type='trace'", (UUID,))[0][0]
except:
tc = 0
stages.append({"name":"TraceChunks","passed":tc>0,"detail":f"{tc} chunks","elapsed":0.0})
# 8. Phase 1 release
p1 = PROJECT / "release" / "phase1" / "latest"
p1_ok = p1.exists() and (p1 / "RELEASE_INFO.txt").exists()
p1_size = sum(f.stat().st_size for f in p1.rglob("*") if f.is_file()) // (1024*1024) if p1.exists() else 0
stages.append({"name":"Phase1","passed":p1_ok,"detail":f"{p1_size}MB","elapsed":0.0})
all_passed = all(s["passed"] for s in stages)
return {
"uuid": UUID,
"passed": all_passed,
"stages": stages,
"checked_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"total_elapsed": round(time.time() - t0, 1),
"health": get_health(),
}
def get_health():
h = {}
try:
import os
load = os.getloadavg()
h["cpu_load_1m"] = round(load[0], 1)
h["cpu_load_5m"] = round(load[1], 1)
except:
h["cpu_load_1m"] = h["cpu_load_5m"] = -1
try:
import subprocess
rss = 0
out = subprocess.run(["ps", "-A", "-o", "rss="], capture_output=True, text=True, timeout=5).stdout
for line in out.strip().split("\n"):
if line.strip():
rss += int(line.strip())
h["memory_used_mb"] = rss // 1024 if rss else 0
except:
pass
try:
d = subprocess.run(["df", "-h", "/Users/accusys/momentry/output_dev"],
capture_output=True, text=True, timeout=5).stdout.strip().split("\n")[-1].split()
h["disk_use_pct"] = d[4] if len(d) > 4 else "?"
h["disk_avail"] = d[3] if len(d) > 3 else "?"
except:
pass
try:
import torch
h["gpu_available"] = torch.backends.mps.is_available()
except:
h["gpu_available"] = False
services = {"postgresql": False, "qdrant": False, "embedding": False, "llm": False}
try:
conn = psycopg2.connect(DB_URL)
conn.close()
services["postgresql"] = True
except:
pass
try:
r = qdrant_get("/collections")
services["qdrant"] = r is not None
except:
pass
try:
resp = urllib.request.urlopen("http://localhost:11436/health", timeout=3)
services["embedding"] = resp.status == 200
except:
pass
try:
req = urllib.request.Request(LLM_URL,
data=json.dumps({"model":"google_gemma-4-26B-A4B-it-Q5_K_M.gguf","messages":[{"role":"user","content":"ping"}],"max_tokens":1}).encode(),
headers={"Content-Type":"application/json"}, method="POST")
resp = urllib.request.urlopen(req, timeout=3)
services["llm"] = resp.status == 200
except:
pass
h["services"] = services
return h
def get_qdrant_info():
result = []
for col in COLLECTIONS:
r = qdrant_get(f"/collections/{col}")
if r:
info = r.get("result", {})
cfg = info.get("config", {}).get("params", {}).get("vectors", {})
result.append({
"name": col,
"points": info.get("points_count", 0),
"dim": cfg.get("size", "?"),
})
else:
result.append({"name": col, "points": -1, "dim": "?"})
return result
def get_db_info():
result = {}
try:
rows = db_query("""
SELECT 'videos', count(*) FROM dev.videos
UNION ALL SELECT 'chunks', count(*) FROM dev.chunks
UNION ALL SELECT 'face_detections', count(*) FROM dev.face_detections
UNION ALL SELECT 'identities', count(*) FROM dev.identities
UNION ALL SELECT 'tkg_nodes', count(*) FROM dev.tkg_nodes
UNION ALL SELECT 'tkg_edges', count(*) FROM dev.tkg_edges
""")
for r in rows:
result[r[0]] = r[1]
except:
pass
return result
def get_processes():
import subprocess
scripts = ["clean_sentence_text.py", "generate_sentence_summaries.py"]
result = {}
for s in scripts:
try:
r = subprocess.run(["pgrep", "-f", s], capture_output=True, text=True, timeout=3)
pids = [p.strip() for p in r.stdout.strip().split("\n") if p.strip()]
if pids:
r2 = subprocess.run(["ps", "-o", "etime=", "-p", pids[0]], capture_output=True, text=True, timeout=3)
result[s] = {"pid": int(pids[0]), "elapsed": r2.stdout.strip()}
else:
result[s] = None
except:
result[s] = None
return result
TEMPLATE = """<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Momentry Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117; color: #c9d1d9; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { font-size: 24px; margin-bottom: 20px; color: #58a6ff; }
h2 { font-size: 16px; margin-bottom: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: 1px; }
.section { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.row { display: flex; gap: 16px; flex-wrap: wrap; }
.col { flex: 1; min-width: 300px; }
table { width: 100%; border-collapse: collapse; font-size: 14px; }
th, td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #21262d; }
th { color: #8b949e; font-weight: 600; }
.pass { color: #3fb950; font-weight: bold; }
.fail { color: #f85149; font-weight: bold; }
.stat-value { font-size: 28px; font-weight: 700; }
.stat-label { font-size: 12px; color: #8b949e; margin-top: 4px; }
.stat-card { background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 16px; text-align: center; }
.refresh-bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.last-updated { color: #8b949e; font-size: 13px; }
button { background: #238636; color: white; border: none; padding: 8px 20px; border-radius: 6px; cursor: pointer; font-size: 14px; }
button:hover { background: #2ea043; }
#error { display: none; background: #3a1b1b; border: 1px solid #f85149; border-radius: 6px; padding: 12px; margin-bottom: 16px; color: #f85149; font-size: 13px; }
@media (max-width: 768px) { .col { min-width: 100%; } }
</style>
</head>
<body>
<div class="container">
<div class="refresh-bar">
<h1>Momentry Dashboard <span id="roleBadge" style="font-size:14px;background:#1f2937;padding:4px 12px;border-radius:12px;margin-left:8px">\U0001F4BB {{ SYSTEM_ROLE }}</span></h1>
<div style="display:flex;align-items:center;gap:8px">
<span class="last-updated" id="lastUpdated">\u2014</span>
<button onclick="load()" style="background:#238636;padding:6px 14px;font-size:13px">\u27F3 Refresh</button>
</div>
</div>
<div id="error"></div>
<div class="row">
<div class="col">
<div class="section">
<h2>\u2705 Pipeline Checklist</h2>
<table id="checklist"><tr><td>Loading...</td></tr></table>
</div>
</div>
<div class="col">
<div class="section">
<h2>\U0001F4BB System Health</h2>
<div id="health" style="font-size:14px">Loading...</div>
</div>
<div class="section">
<h2>\U0001F6E0 Services</h2>
<div id="services" style="font-size:14px">Loading...</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="section">
<h2>\U0001F4CA Qdrant Collections</h2>
<div id="qdrant" style="font-size:14px">Loading...</div>
</div>
</div>
<div class="col">
<div class="section">
<h2>\u2699\uFE0F Background Processes</h2>
<div id="processes" style="font-size:14px">Loading...</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="section">
<h2>\U0001F4DB Database</h2>
<div id="db" style="font-size:14px">Loading...</div>
</div>
</div>
</div>
</div>
<script>
async function load() {
const ts = new Date().toISOString().slice(11,19);
document.getElementById("lastUpdated").textContent = "\U0001F504 " + ts;
document.getElementById("error").style.display = "none";
try {
const resp = await fetch("/api/all");
if (!resp.ok) throw new Error("HTTP " + resp.status);
const d = await resp.json();
renderChecklist(d.status);
renderHealth(d.status.health);
renderQdrant(d.qdrant);
renderProcesses(d.processes);
renderDb(d.db);
document.getElementById("lastUpdated").textContent = "\u2705 " + ts;
} catch(e) {
showError(e.message);
document.getElementById("lastUpdated").textContent = "\u274C " + ts;
}
}
function showError(msg) {
document.getElementById("error").innerHTML = "\u26A0\uFE0F " + msg;
document.getElementById("error").style.display = "block";
}
function renderChecklist(status) {
const job = status || {};
const stages = job.stages || [];
let h = "<tr><th>Stage</th><th>Status</th><th>Detail</th></tr>";
for (const s of stages) {
h += "<tr><td>" + s.name + '</td><td class="' + (s.passed ? "pass" : "fail") + '">' + (s.passed ? "\u2705" : "\u274C") + "</td><td>" + s.detail + "</td></tr>";
}
h += '<tr style="font-weight:bold;border-top:2px solid #30363d"><td>TOTAL</td><td class="' + (job.passed ? "pass" : "fail") + '">' + (job.passed ? "\u2705" : "\u274C") + "</td><td></td></tr>";
document.getElementById("checklist").innerHTML = h;
}
function renderHealth(h) {
if (!h) return;
let cards = '<div class="row">';
cards += '<div class="col"><div class="stat-card"><div class="stat-value">' + (h.cpu_load_1m ?? "?") + '</div><div class="stat-label">CPU Load (1m)</div></div></div>';
const memPct = h.memory_used_mb ? (h.memory_used_mb / 49152 * 100).toFixed(1) : "?";
cards += '<div class="col"><div class="stat-card"><div class="stat-value">' + memPct + '%</div><div class="stat-label">Memory</div></div></div>';
cards += '<div class="col"><div class="stat-card"><div class="stat-value">' + (h.disk_use_pct ?? "?") + '</div><div class="stat-label">Disk</div></div></div>';
cards += "</div>";
document.getElementById("health").innerHTML = cards;
const svc = h.services || {};
let svcHtml = "";
for (const [k, v] of Object.entries(svc)) {
svcHtml += '<span style="margin-right:16px">' + (v ? "\u2705" : "\u274C") + " " + k + "</span>";
}
document.getElementById("services").innerHTML = svcHtml;
}
function renderQdrant(cols) {
if (!cols) return;
let h = "<table><tr><th>Collection</th><th>Points</th><th>Dim</th></tr>";
for (let i = 0; i < cols.length; i++) {
const c = cols[i];
h += "<tr><td>" + c.name + "</td><td>" + (c.points >= 0 ? Number(c.points).toLocaleString() : "err") + "</td><td>" + c.dim + "</td></tr>";
}
h += "</table>";
document.getElementById("qdrant").innerHTML = h;
}
function renderProcesses(procs) {
if (!procs) return;
let h = "<table><tr><th>Script</th><th>Status</th></tr>";
for (const name in procs) {
const info = procs[name];
if (info) {
h += "<tr><td>" + name + "</td><td>\u25B6 running " + info.elapsed + "</td></tr>";
} else {
h += '<tr style="color:#8b949e"><td>' + name + "</td><td>\u23F3 idle</td></tr>";
}
}
h += "</table>";
document.getElementById("processes").innerHTML = h;
}
function renderDb(d) {
if (!d) return;
const keys = ["videos","chunks","face_detections","identities","tkg_nodes","tkg_edges"];
let h = '<div class="row">';
for (let i = 0; i < keys.length; i++) {
const v = d[keys[i]] ?? 0;
h += '<div class="col"><div class="stat-card"><div class="stat-value">' + Number(v).toLocaleString() + '</div><div class="stat-label">' + keys[i].replace(/_/g," ") + '</div></div></div>';
}
h += "</div>";
document.getElementById("db").innerHTML = h;
}
load();
setInterval(load, 30000);
</script>
</body>
</html>"""
if __name__ == "__main__":
port = int(os.environ.get("DASHBOARD_PORT", 5050))
print(f"Momentry Dashboard v2: http://0.0.0.0:{port}")
app.run(host="0.0.0.0", port=port, threaded=True)