Updated the streamlit app and removed the train.py
Browse files- src/streamlit_app.py +9 -418
src/streamlit_app.py
CHANGED
|
@@ -42,8 +42,6 @@ import time
|
|
| 42 |
import zipfile
|
| 43 |
import hashlib
|
| 44 |
import re
|
| 45 |
-
import subprocess
|
| 46 |
-
import sys
|
| 47 |
from datetime import datetime
|
| 48 |
from pathlib import Path
|
| 49 |
|
|
@@ -104,7 +102,7 @@ def _safe_binary_closing(mask, footprint):
|
|
| 104 |
# CONFIG β edit these two lines to match your HF model repo
|
| 105 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
MODEL_REPO_ID = "skarugu/myotube-unet"
|
| 107 |
-
MODEL_FILENAME =
|
| 108 |
|
| 109 |
CONF_FLAG_THR = 0.60 # images below this confidence are queued for retraining
|
| 110 |
QUEUE_DIR = Path("retrain_queue")
|
|
@@ -1411,153 +1409,28 @@ class UNet(nn.Module):
|
|
| 1411 |
return self.out(x)
|
| 1412 |
|
| 1413 |
|
| 1414 |
-
def _group_count(channels: int) -> int:
|
| 1415 |
-
for groups in (8, 4, 2, 1):
|
| 1416 |
-
if channels % groups == 0:
|
| 1417 |
-
return groups
|
| 1418 |
-
return 1
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
class ResidualBlock(nn.Module):
|
| 1422 |
-
def __init__(self, in_ch: int, out_ch: int):
|
| 1423 |
-
super().__init__()
|
| 1424 |
-
self.conv1 = nn.Conv2d(in_ch, out_ch, 3, padding=1, bias=False)
|
| 1425 |
-
self.norm1 = nn.GroupNorm(_group_count(out_ch), out_ch)
|
| 1426 |
-
self.conv2 = nn.Conv2d(out_ch, out_ch, 3, padding=1, bias=False)
|
| 1427 |
-
self.norm2 = nn.GroupNorm(_group_count(out_ch), out_ch)
|
| 1428 |
-
self.skip = nn.Identity() if in_ch == out_ch else nn.Conv2d(in_ch, out_ch, 1, bias=False)
|
| 1429 |
-
|
| 1430 |
-
def forward(self, x):
|
| 1431 |
-
identity = self.skip(x)
|
| 1432 |
-
x = torch.nn.functional.silu(self.norm1(self.conv1(x)))
|
| 1433 |
-
x = self.norm2(self.conv2(x))
|
| 1434 |
-
return torch.nn.functional.silu(x + identity)
|
| 1435 |
-
|
| 1436 |
-
|
| 1437 |
-
class AttentionGate(nn.Module):
|
| 1438 |
-
def __init__(self, gate_ch: int, skip_ch: int, inter_ch: int):
|
| 1439 |
-
super().__init__()
|
| 1440 |
-
self.gate = nn.Sequential(
|
| 1441 |
-
nn.Conv2d(gate_ch, inter_ch, 1, bias=True),
|
| 1442 |
-
nn.GroupNorm(_group_count(inter_ch), inter_ch),
|
| 1443 |
-
)
|
| 1444 |
-
self.skip = nn.Sequential(
|
| 1445 |
-
nn.Conv2d(skip_ch, inter_ch, 1, bias=True),
|
| 1446 |
-
nn.GroupNorm(_group_count(inter_ch), inter_ch),
|
| 1447 |
-
)
|
| 1448 |
-
self.psi = nn.Sequential(nn.Conv2d(inter_ch, 1, 1, bias=True), nn.Sigmoid())
|
| 1449 |
-
|
| 1450 |
-
def forward(self, gate, skip):
|
| 1451 |
-
alpha = self.psi(torch.nn.functional.silu(self.gate(gate) + self.skip(skip)))
|
| 1452 |
-
return skip * alpha
|
| 1453 |
-
|
| 1454 |
-
|
| 1455 |
-
def _center_pad_like(x, ref):
|
| 1456 |
-
diff_y = ref.size(2) - x.size(2)
|
| 1457 |
-
diff_x = ref.size(3) - x.size(3)
|
| 1458 |
-
if diff_x or diff_y:
|
| 1459 |
-
x = torch.nn.functional.pad(
|
| 1460 |
-
x,
|
| 1461 |
-
[diff_x // 2, diff_x - diff_x // 2, diff_y // 2, diff_y - diff_y // 2],
|
| 1462 |
-
)
|
| 1463 |
-
return x
|
| 1464 |
-
|
| 1465 |
-
|
| 1466 |
-
class AttentionResUNetV2(nn.Module):
|
| 1467 |
-
def __init__(self, in_ch: int = 2, out_ch: int = 2, base: int = 32):
|
| 1468 |
-
super().__init__()
|
| 1469 |
-
self.e1 = ResidualBlock(in_ch, base)
|
| 1470 |
-
self.e2 = ResidualBlock(base, base * 2)
|
| 1471 |
-
self.e3 = ResidualBlock(base * 2, base * 4)
|
| 1472 |
-
self.e4 = ResidualBlock(base * 4, base * 8)
|
| 1473 |
-
self.pool = nn.MaxPool2d(2)
|
| 1474 |
-
self.bridge = ResidualBlock(base * 8, base * 16)
|
| 1475 |
-
|
| 1476 |
-
self.up4 = nn.ConvTranspose2d(base * 16, base * 8, 2, 2)
|
| 1477 |
-
self.att4 = AttentionGate(base * 8, base * 8, base * 4)
|
| 1478 |
-
self.dec4 = ResidualBlock(base * 16, base * 8)
|
| 1479 |
-
self.up3 = nn.ConvTranspose2d(base * 8, base * 4, 2, 2)
|
| 1480 |
-
self.att3 = AttentionGate(base * 4, base * 4, base * 2)
|
| 1481 |
-
self.dec3 = ResidualBlock(base * 8, base * 4)
|
| 1482 |
-
self.up2 = nn.ConvTranspose2d(base * 4, base * 2, 2, 2)
|
| 1483 |
-
self.att2 = AttentionGate(base * 2, base * 2, base)
|
| 1484 |
-
self.dec2 = ResidualBlock(base * 4, base * 2)
|
| 1485 |
-
self.up1 = nn.ConvTranspose2d(base * 2, base, 2, 2)
|
| 1486 |
-
self.att1 = AttentionGate(base, base, max(base // 2, 1))
|
| 1487 |
-
self.dec1 = ResidualBlock(base * 2, base)
|
| 1488 |
-
self.out = nn.Conv2d(base, out_ch, 1)
|
| 1489 |
-
|
| 1490 |
-
def forward(self, x):
|
| 1491 |
-
e1 = self.e1(x)
|
| 1492 |
-
e2 = self.e2(self.pool(e1))
|
| 1493 |
-
e3 = self.e3(self.pool(e2))
|
| 1494 |
-
e4 = self.e4(self.pool(e3))
|
| 1495 |
-
b = self.bridge(self.pool(e4))
|
| 1496 |
-
|
| 1497 |
-
x = _center_pad_like(self.up4(b), e4)
|
| 1498 |
-
x = self.dec4(torch.cat([x, self.att4(x, e4)], dim=1))
|
| 1499 |
-
x = _center_pad_like(self.up3(x), e3)
|
| 1500 |
-
x = self.dec3(torch.cat([x, self.att3(x, e3)], dim=1))
|
| 1501 |
-
x = _center_pad_like(self.up2(x), e2)
|
| 1502 |
-
x = self.dec2(torch.cat([x, self.att2(x, e2)], dim=1))
|
| 1503 |
-
x = _center_pad_like(self.up1(x), e1)
|
| 1504 |
-
x = self.dec1(torch.cat([x, self.att1(x, e1)], dim=1))
|
| 1505 |
-
return self.out(x)
|
| 1506 |
-
|
| 1507 |
-
|
| 1508 |
@st.cache_resource
|
| 1509 |
def load_model(device: str):
|
| 1510 |
-
|
| 1511 |
-
|
| 1512 |
-
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
force_download=True,
|
| 1516 |
-
)
|
| 1517 |
-
except Exception as exc:
|
| 1518 |
-
missing_requested_model = (
|
| 1519 |
-
active_filename != "model_final.pt"
|
| 1520 |
-
and (
|
| 1521 |
-
"EntryNotFound" in exc.__class__.__name__
|
| 1522 |
-
or "404" in str(exc)
|
| 1523 |
-
or "Entry Not Found" in str(exc)
|
| 1524 |
-
)
|
| 1525 |
-
)
|
| 1526 |
-
if not missing_requested_model:
|
| 1527 |
-
raise
|
| 1528 |
-
st.warning(
|
| 1529 |
-
f"`{active_filename}` is not in `{MODEL_REPO_ID}` yet. "
|
| 1530 |
-
"Using `model_final.pt` so the app can stay open while v2 is trained."
|
| 1531 |
-
)
|
| 1532 |
-
active_filename = "model_final.pt"
|
| 1533 |
-
local = hf_hub_download(
|
| 1534 |
-
repo_id=MODEL_REPO_ID,
|
| 1535 |
-
filename=active_filename,
|
| 1536 |
-
force_download=True,
|
| 1537 |
-
)
|
| 1538 |
file_sha = sha256_file(local)
|
| 1539 |
mtime = time.ctime(os.path.getmtime(local))
|
| 1540 |
size_mb = os.path.getsize(local) / 1e6
|
| 1541 |
|
| 1542 |
st.sidebar.markdown("### π Model debug")
|
| 1543 |
st.sidebar.caption(f"Repo: `{MODEL_REPO_ID}`")
|
| 1544 |
-
st.sidebar.caption(f"
|
| 1545 |
-
st.sidebar.caption(f"Loaded file: `{active_filename}`")
|
| 1546 |
st.sidebar.caption(f"Size: {size_mb:.2f} MB")
|
| 1547 |
st.sidebar.caption(f"Modified: {mtime}")
|
| 1548 |
st.sidebar.caption(f"SHA256: `{file_sha[:20]}β¦`")
|
| 1549 |
|
| 1550 |
ckpt = torch.load(local, map_location=device)
|
| 1551 |
state = ckpt["model"] if isinstance(ckpt, dict) and "model" in ckpt else ckpt
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
base = int(args.get("base", 32)) if isinstance(args, dict) else 32
|
| 1555 |
-
|
| 1556 |
-
if arch == "attention_resunet_v2":
|
| 1557 |
-
model = AttentionResUNetV2(in_ch=2, out_ch=2, base=base)
|
| 1558 |
-
else:
|
| 1559 |
-
model = UNet(in_ch=2, out_ch=2, base=32)
|
| 1560 |
-
st.sidebar.caption(f"Architecture: `{arch}`")
|
| 1561 |
model.load_state_dict(state)
|
| 1562 |
model.to(device).eval()
|
| 1563 |
return model
|
|
@@ -1585,147 +1458,6 @@ h1,h2,h3,h4 { color:#90caf9; }
|
|
| 1585 |
st.title("π¬ MyoSeg β Myotube & Nuclei Analyser")
|
| 1586 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 1587 |
|
| 1588 |
-
V2_RUN_DIR = Path("runs_v2")
|
| 1589 |
-
V2_PID_PATH = V2_RUN_DIR / "train_v2.pid"
|
| 1590 |
-
V2_LOG_PATH = V2_RUN_DIR / "train_v2_latest.log"
|
| 1591 |
-
|
| 1592 |
-
|
| 1593 |
-
def _pid_is_running(pid: int) -> bool:
|
| 1594 |
-
if pid <= 0:
|
| 1595 |
-
return False
|
| 1596 |
-
try:
|
| 1597 |
-
os.kill(pid, 0)
|
| 1598 |
-
return True
|
| 1599 |
-
except OSError:
|
| 1600 |
-
return False
|
| 1601 |
-
|
| 1602 |
-
|
| 1603 |
-
def _v2_training_status():
|
| 1604 |
-
if not V2_PID_PATH.exists():
|
| 1605 |
-
return False, None
|
| 1606 |
-
try:
|
| 1607 |
-
pid = int(V2_PID_PATH.read_text().strip())
|
| 1608 |
-
except Exception:
|
| 1609 |
-
return False, None
|
| 1610 |
-
return _pid_is_running(pid), pid
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
def _start_v2_training(epochs: int, batch_size: int, base: int, image_size: int):
|
| 1614 |
-
V2_RUN_DIR.mkdir(parents=True, exist_ok=True)
|
| 1615 |
-
cmd = [
|
| 1616 |
-
sys.executable,
|
| 1617 |
-
"train_model_v2.py",
|
| 1618 |
-
"--data_root", "data",
|
| 1619 |
-
"--corrections_dir", "corrections",
|
| 1620 |
-
"--out_dir", str(V2_RUN_DIR),
|
| 1621 |
-
"--epochs", str(int(epochs)),
|
| 1622 |
-
"--batch_size", str(int(batch_size)),
|
| 1623 |
-
"--image_size", str(int(image_size)),
|
| 1624 |
-
"--base", str(int(base)),
|
| 1625 |
-
"--push_to_hub",
|
| 1626 |
-
"--hf_repo", MODEL_REPO_ID,
|
| 1627 |
-
"--hf_filename", "model_v2.pt",
|
| 1628 |
-
]
|
| 1629 |
-
env = os.environ.copy()
|
| 1630 |
-
env["PYTHONUNBUFFERED"] = "1"
|
| 1631 |
-
with V2_LOG_PATH.open("ab", buffering=0) as log_f:
|
| 1632 |
-
log_f.write(
|
| 1633 |
-
f"\n\n=== MyoSeg v2 training started {datetime.now().isoformat()} ===\n".encode()
|
| 1634 |
-
)
|
| 1635 |
-
log_f.write(("Command: " + " ".join(cmd) + "\n\n").encode())
|
| 1636 |
-
proc = subprocess.Popen(
|
| 1637 |
-
cmd,
|
| 1638 |
-
stdout=log_f,
|
| 1639 |
-
stderr=subprocess.STDOUT,
|
| 1640 |
-
cwd=".",
|
| 1641 |
-
env=env,
|
| 1642 |
-
)
|
| 1643 |
-
V2_PID_PATH.write_text(str(proc.pid))
|
| 1644 |
-
return proc.pid
|
| 1645 |
-
|
| 1646 |
-
|
| 1647 |
-
def render_v2_training_panel(image_size: int, key_prefix: str = "v2_top"):
|
| 1648 |
-
train_script = Path("train_model_v2.py")
|
| 1649 |
-
has_script = train_script.exists()
|
| 1650 |
-
has_token = bool(os.environ.get("HF_TOKEN"))
|
| 1651 |
-
running, pid = _v2_training_status()
|
| 1652 |
-
latest_model = V2_RUN_DIR / "model_v2.pt"
|
| 1653 |
-
latest_metrics = V2_RUN_DIR / "metrics_v2.json"
|
| 1654 |
-
|
| 1655 |
-
s1, s2, s3 = st.columns(3)
|
| 1656 |
-
s1.metric("train_model_v2.py", "Found" if has_script else "Missing")
|
| 1657 |
-
s2.metric("HF_TOKEN", "Ready" if has_token else "Missing")
|
| 1658 |
-
s3.metric("v2 status", f"Running pid {pid}" if running else "Idle")
|
| 1659 |
-
|
| 1660 |
-
if not has_script:
|
| 1661 |
-
st.error("Upload train_model_v2.py to the Space before starting v2 training.")
|
| 1662 |
-
if not has_token:
|
| 1663 |
-
st.warning("Add HF_TOKEN as a Space secret with write access before training.")
|
| 1664 |
-
|
| 1665 |
-
c1, c2, c3 = st.columns(3)
|
| 1666 |
-
with c1:
|
| 1667 |
-
v2_epochs = st.number_input(
|
| 1668 |
-
"v2 epochs", min_value=5, max_value=200, value=80, step=5,
|
| 1669 |
-
key=f"{key_prefix}_epochs",
|
| 1670 |
-
)
|
| 1671 |
-
with c2:
|
| 1672 |
-
v2_batch = st.selectbox("v2 batch size", [2, 4], index=0, key=f"{key_prefix}_batch")
|
| 1673 |
-
with c3:
|
| 1674 |
-
v2_base = st.selectbox("v2 base width", [24, 32], index=1, key=f"{key_prefix}_base")
|
| 1675 |
-
|
| 1676 |
-
st.caption(
|
| 1677 |
-
"This trains attention_resunet_v2 on data/images plus data/masks/Nuclei_m "
|
| 1678 |
-
"and data/masks/Myotubes_m, then uploads model_v2.pt to the model repo."
|
| 1679 |
-
)
|
| 1680 |
-
|
| 1681 |
-
start_disabled = running or not has_script or not has_token
|
| 1682 |
-
if st.button("Start v2 training now", type="primary", disabled=start_disabled, key=f"{key_prefix}_start"):
|
| 1683 |
-
new_pid = _start_v2_training(
|
| 1684 |
-
epochs=int(v2_epochs),
|
| 1685 |
-
batch_size=int(v2_batch),
|
| 1686 |
-
base=int(v2_base),
|
| 1687 |
-
image_size=int(image_size),
|
| 1688 |
-
)
|
| 1689 |
-
st.success(f"v2 training started in the background. Process id: {new_pid}")
|
| 1690 |
-
st.info("Refresh this page to update status. Watch the log preview below for progress.")
|
| 1691 |
-
|
| 1692 |
-
if running:
|
| 1693 |
-
st.info("v2 training is currently running. Avoid restarting the Space until it finishes.")
|
| 1694 |
-
|
| 1695 |
-
if latest_model.exists():
|
| 1696 |
-
st.success(f"Local v2 model exists: {latest_model}")
|
| 1697 |
-
st.download_button(
|
| 1698 |
-
"Download model_v2.pt",
|
| 1699 |
-
data=latest_model.read_bytes(),
|
| 1700 |
-
file_name="model_v2.pt",
|
| 1701 |
-
mime="application/octet-stream",
|
| 1702 |
-
key=f"{key_prefix}_download_model",
|
| 1703 |
-
)
|
| 1704 |
-
if latest_metrics.exists():
|
| 1705 |
-
try:
|
| 1706 |
-
metrics_text = latest_metrics.read_text()
|
| 1707 |
-
st.json(json.loads(metrics_text))
|
| 1708 |
-
st.download_button(
|
| 1709 |
-
"Download metrics_v2.json",
|
| 1710 |
-
data=metrics_text,
|
| 1711 |
-
file_name="metrics_v2.json",
|
| 1712 |
-
mime="application/json",
|
| 1713 |
-
key=f"{key_prefix}_download_metrics",
|
| 1714 |
-
)
|
| 1715 |
-
except Exception:
|
| 1716 |
-
st.caption(f"Metrics file exists: {latest_metrics}")
|
| 1717 |
-
|
| 1718 |
-
if V2_LOG_PATH.exists():
|
| 1719 |
-
log_text = V2_LOG_PATH.read_text(errors="replace")
|
| 1720 |
-
st.download_button(
|
| 1721 |
-
"Download v2 training log",
|
| 1722 |
-
data=log_text,
|
| 1723 |
-
file_name="train_v2_latest.log",
|
| 1724 |
-
mime="text/plain",
|
| 1725 |
-
key=f"{key_prefix}_download_log",
|
| 1726 |
-
)
|
| 1727 |
-
st.text_area("Latest v2 training log", log_text[-6000:], height=260, key=f"{key_prefix}_log")
|
| 1728 |
-
|
| 1729 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1730 |
# SIDEBAR
|
| 1731 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -1919,10 +1651,6 @@ with st.sidebar:
|
|
| 1919 |
|
| 1920 |
|
| 1921 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1922 |
-
with st.expander("Train v2 model", expanded=True):
|
| 1923 |
-
render_v2_training_panel(image_size=int(image_size), key_prefix="v2_top")
|
| 1924 |
-
|
| 1925 |
-
|
| 1926 |
# FILE UPLOADER
|
| 1927 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1928 |
_uploader_label = (
|
|
@@ -2187,64 +1915,6 @@ with c3:
|
|
| 2187 |
|
| 2188 |
st.divider()
|
| 2189 |
|
| 2190 |
-
V2_RUN_DIR = Path("runs_v2")
|
| 2191 |
-
V2_PID_PATH = V2_RUN_DIR / "train_v2.pid"
|
| 2192 |
-
V2_LOG_PATH = V2_RUN_DIR / "train_v2_latest.log"
|
| 2193 |
-
|
| 2194 |
-
|
| 2195 |
-
def _pid_is_running(pid: int) -> bool:
|
| 2196 |
-
if pid <= 0:
|
| 2197 |
-
return False
|
| 2198 |
-
try:
|
| 2199 |
-
os.kill(pid, 0)
|
| 2200 |
-
return True
|
| 2201 |
-
except OSError:
|
| 2202 |
-
return False
|
| 2203 |
-
|
| 2204 |
-
|
| 2205 |
-
def _v2_training_status():
|
| 2206 |
-
if not V2_PID_PATH.exists():
|
| 2207 |
-
return False, None
|
| 2208 |
-
try:
|
| 2209 |
-
pid = int(V2_PID_PATH.read_text().strip())
|
| 2210 |
-
except Exception:
|
| 2211 |
-
return False, None
|
| 2212 |
-
return _pid_is_running(pid), pid
|
| 2213 |
-
|
| 2214 |
-
|
| 2215 |
-
def _start_v2_training(epochs: int, batch_size: int, base: int, image_size: int):
|
| 2216 |
-
V2_RUN_DIR.mkdir(parents=True, exist_ok=True)
|
| 2217 |
-
cmd = [
|
| 2218 |
-
sys.executable,
|
| 2219 |
-
"train_model_v2.py",
|
| 2220 |
-
"--data_root", "data",
|
| 2221 |
-
"--corrections_dir", "corrections",
|
| 2222 |
-
"--out_dir", str(V2_RUN_DIR),
|
| 2223 |
-
"--epochs", str(int(epochs)),
|
| 2224 |
-
"--batch_size", str(int(batch_size)),
|
| 2225 |
-
"--image_size", str(int(image_size)),
|
| 2226 |
-
"--base", str(int(base)),
|
| 2227 |
-
"--push_to_hub",
|
| 2228 |
-
"--hf_repo", MODEL_REPO_ID,
|
| 2229 |
-
"--hf_filename", "model_v2.pt",
|
| 2230 |
-
]
|
| 2231 |
-
env = os.environ.copy()
|
| 2232 |
-
env["PYTHONUNBUFFERED"] = "1"
|
| 2233 |
-
with V2_LOG_PATH.open("ab", buffering=0) as log_f:
|
| 2234 |
-
log_f.write(
|
| 2235 |
-
f"\n\n=== MyoSeg v2 training started {datetime.now().isoformat()} ===\n".encode()
|
| 2236 |
-
)
|
| 2237 |
-
log_f.write(("Command: " + " ".join(cmd) + "\n\n").encode())
|
| 2238 |
-
proc = subprocess.Popen(
|
| 2239 |
-
cmd,
|
| 2240 |
-
stdout=log_f,
|
| 2241 |
-
stderr=subprocess.STDOUT,
|
| 2242 |
-
cwd=".",
|
| 2243 |
-
env=env,
|
| 2244 |
-
)
|
| 2245 |
-
V2_PID_PATH.write_text(str(proc.pid))
|
| 2246 |
-
return proc.pid
|
| 2247 |
-
|
| 2248 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2249 |
# PER-IMAGE PREVIEW + ANIMATED METRICS
|
| 2250 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -2637,82 +2307,3 @@ with st.expander("π§ Self-training queue status"):
|
|
| 2637 |
import subprocess
|
| 2638 |
subprocess.Popen(["python", "self_train.py", "--manual"])
|
| 2639 |
st.info("Retraining started in the background. Check terminal / logs for progress.")
|
| 2640 |
-
|
| 2641 |
-
|
| 2642 |
-
with st.expander("Train v2 model"):
|
| 2643 |
-
train_script = Path("train_model_v2.py")
|
| 2644 |
-
has_script = train_script.exists()
|
| 2645 |
-
has_token = bool(os.environ.get("HF_TOKEN"))
|
| 2646 |
-
running, pid = _v2_training_status()
|
| 2647 |
-
latest_model = V2_RUN_DIR / "model_v2.pt"
|
| 2648 |
-
latest_metrics = V2_RUN_DIR / "metrics_v2.json"
|
| 2649 |
-
|
| 2650 |
-
s1, s2, s3 = st.columns(3)
|
| 2651 |
-
s1.metric("train_model_v2.py", "Found" if has_script else "Missing")
|
| 2652 |
-
s2.metric("HF_TOKEN", "Ready" if has_token else "Missing")
|
| 2653 |
-
s3.metric("v2 status", f"Running pid {pid}" if running else "Idle")
|
| 2654 |
-
|
| 2655 |
-
if not has_script:
|
| 2656 |
-
st.error("Upload train_model_v2.py to the Space before starting v2 training.")
|
| 2657 |
-
if not has_token:
|
| 2658 |
-
st.warning("Add HF_TOKEN as a Space secret with write access before training.")
|
| 2659 |
-
|
| 2660 |
-
c1, c2, c3 = st.columns(3)
|
| 2661 |
-
with c1:
|
| 2662 |
-
v2_epochs = st.number_input("v2 epochs", min_value=5, max_value=200, value=80, step=5)
|
| 2663 |
-
with c2:
|
| 2664 |
-
v2_batch = st.selectbox("v2 batch size", [2, 4], index=0)
|
| 2665 |
-
with c3:
|
| 2666 |
-
v2_base = st.selectbox("v2 base width", [24, 32], index=1)
|
| 2667 |
-
|
| 2668 |
-
st.caption(
|
| 2669 |
-
"This trains attention_resunet_v2 on data/images plus data/masks/Nuclei_m "
|
| 2670 |
-
"and data/masks/Myotubes_m, then uploads model_v2.pt to the model repo."
|
| 2671 |
-
)
|
| 2672 |
-
|
| 2673 |
-
start_disabled = running or not has_script or not has_token
|
| 2674 |
-
if st.button("Start v2 training now", type="primary", disabled=start_disabled):
|
| 2675 |
-
new_pid = _start_v2_training(
|
| 2676 |
-
epochs=int(v2_epochs),
|
| 2677 |
-
batch_size=int(v2_batch),
|
| 2678 |
-
base=int(v2_base),
|
| 2679 |
-
image_size=int(image_size),
|
| 2680 |
-
)
|
| 2681 |
-
st.success(f"v2 training started in the background. Process id: {new_pid}")
|
| 2682 |
-
st.info("Refresh this page to update status. Watch the log preview below for progress.")
|
| 2683 |
-
|
| 2684 |
-
if running:
|
| 2685 |
-
st.info("v2 training is currently running. Avoid restarting the Space until it finishes.")
|
| 2686 |
-
|
| 2687 |
-
if latest_model.exists():
|
| 2688 |
-
st.success(f"Local v2 model exists: {latest_model}")
|
| 2689 |
-
st.download_button(
|
| 2690 |
-
"Download model_v2.pt",
|
| 2691 |
-
data=latest_model.read_bytes(),
|
| 2692 |
-
file_name="model_v2.pt",
|
| 2693 |
-
mime="application/octet-stream",
|
| 2694 |
-
key="v2_bottom_download_model",
|
| 2695 |
-
)
|
| 2696 |
-
if latest_metrics.exists():
|
| 2697 |
-
try:
|
| 2698 |
-
metrics_text = latest_metrics.read_text()
|
| 2699 |
-
st.json(json.loads(metrics_text))
|
| 2700 |
-
st.download_button(
|
| 2701 |
-
"Download metrics_v2.json",
|
| 2702 |
-
data=metrics_text,
|
| 2703 |
-
file_name="metrics_v2.json",
|
| 2704 |
-
mime="application/json",
|
| 2705 |
-
key="v2_bottom_download_metrics",
|
| 2706 |
-
)
|
| 2707 |
-
except Exception:
|
| 2708 |
-
st.caption(f"Metrics file exists: {latest_metrics}")
|
| 2709 |
-
|
| 2710 |
-
if V2_LOG_PATH.exists():
|
| 2711 |
-
log_text = V2_LOG_PATH.read_text(errors="replace")
|
| 2712 |
-
st.download_button(
|
| 2713 |
-
"Download v2 training log",
|
| 2714 |
-
data=log_text,
|
| 2715 |
-
file_name="train_v2_latest.log",
|
| 2716 |
-
mime="text/plain",
|
| 2717 |
-
)
|
| 2718 |
-
st.text_area("Latest v2 training log", log_text[-6000:], height=260)
|
|
|
|
| 42 |
import zipfile
|
| 43 |
import hashlib
|
| 44 |
import re
|
|
|
|
|
|
|
| 45 |
from datetime import datetime
|
| 46 |
from pathlib import Path
|
| 47 |
|
|
|
|
| 102 |
# CONFIG β edit these two lines to match your HF model repo
|
| 103 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
MODEL_REPO_ID = "skarugu/myotube-unet"
|
| 105 |
+
MODEL_FILENAME = "model_final.pt"
|
| 106 |
|
| 107 |
CONF_FLAG_THR = 0.60 # images below this confidence are queued for retraining
|
| 108 |
QUEUE_DIR = Path("retrain_queue")
|
|
|
|
| 1409 |
return self.out(x)
|
| 1410 |
|
| 1411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1412 |
@st.cache_resource
|
| 1413 |
def load_model(device: str):
|
| 1414 |
+
local = hf_hub_download(
|
| 1415 |
+
repo_id=MODEL_REPO_ID,
|
| 1416 |
+
filename=MODEL_FILENAME,
|
| 1417 |
+
force_download=True,
|
| 1418 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1419 |
file_sha = sha256_file(local)
|
| 1420 |
mtime = time.ctime(os.path.getmtime(local))
|
| 1421 |
size_mb = os.path.getsize(local) / 1e6
|
| 1422 |
|
| 1423 |
st.sidebar.markdown("### π Model debug")
|
| 1424 |
st.sidebar.caption(f"Repo: `{MODEL_REPO_ID}`")
|
| 1425 |
+
st.sidebar.caption(f"File: `{MODEL_FILENAME}`")
|
|
|
|
| 1426 |
st.sidebar.caption(f"Size: {size_mb:.2f} MB")
|
| 1427 |
st.sidebar.caption(f"Modified: {mtime}")
|
| 1428 |
st.sidebar.caption(f"SHA256: `{file_sha[:20]}β¦`")
|
| 1429 |
|
| 1430 |
ckpt = torch.load(local, map_location=device)
|
| 1431 |
state = ckpt["model"] if isinstance(ckpt, dict) and "model" in ckpt else ckpt
|
| 1432 |
+
model = UNet(in_ch=2, out_ch=2, base=32)
|
| 1433 |
+
st.sidebar.caption("Architecture: `unet_v1`")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1434 |
model.load_state_dict(state)
|
| 1435 |
model.to(device).eval()
|
| 1436 |
return model
|
|
|
|
| 1458 |
st.title("π¬ MyoSeg β Myotube & Nuclei Analyser")
|
| 1459 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 1460 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1461 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1462 |
# SIDEBAR
|
| 1463 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1651 |
|
| 1652 |
|
| 1653 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1654 |
# FILE UPLOADER
|
| 1655 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1656 |
_uploader_label = (
|
|
|
|
| 1915 |
|
| 1916 |
st.divider()
|
| 1917 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1918 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1919 |
# PER-IMAGE PREVIEW + ANIMATED METRICS
|
| 1920 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 2307 |
import subprocess
|
| 2308 |
subprocess.Popen(["python", "self_train.py", "--manual"])
|
| 2309 |
st.info("Retraining started in the background. Check terminal / logs for progress.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|