合并外部维护版本并同步自动每日功能

This commit is contained in:
2026-09-28 13:55:07 +08:00
parent 2d81615f78
commit 9040bcb5e5
59 changed files with 3176 additions and 179 deletions
+31
View File
@@ -0,0 +1,31 @@
import datetime
import sys
import psutil
targets = []
for p in psutil.process_iter(["name", "cmdline"]):
try:
cmd = " ".join(p.info["cmdline"] or [])
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
if "main.py" in cmd and "--role" in cmd:
targets.append(p)
for p in targets:
try:
parent = p.parent()
print(
p.pid,
p.name(),
"started",
datetime.datetime.fromtimestamp(p.create_time()).strftime("%m-%d %H:%M:%S"),
"parent",
parent.pid if parent else None,
parent.name() if parent else "",
)
except psutil.NoSuchProcess:
print(p.pid, "exited")
if not targets:
print("NO_SERVICE_PROCESS")
+33
View File
@@ -0,0 +1,33 @@
import os
import sys
import time
import psutil
TARGET = "perf_sampler.py"
mode = sys.argv[1] if len(sys.argv) > 1 else "kill"
me = os.getpid()
matched = []
for p in psutil.process_iter(["name", "cmdline"]):
if p.pid == me:
continue
try:
parts = [x for x in (p.info["cmdline"] or [])]
# only match when the script is the program argument, not substring of -c code
if any(part.endswith(TARGET) for part in parts):
matched.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
if mode == "kill":
for p in matched:
try:
p.kill()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
time.sleep(1)
print("killed:", [p.pid for p in matched])
else:
import datetime
for p in matched:
print(p.pid, p.name(), datetime.datetime.fromtimestamp(p.create_time()).strftime("%H:%M:%S"))
+1 -1
View File
@@ -34,7 +34,7 @@ function Get-ProjectPython {
$env:LIVE_STREAMING_PYTHON,
(Join-Path $Root ".venv-tts\Scripts\python.exe"),
(Join-Path $Root ".venv\Scripts\python.exe"),
"E:\Programs\Anaconda3\envs\Live-streaming\python.exe",
$(if ($env:CONDA_PREFIX) { Join-Path $env:CONDA_PREFIX "python.exe" }),
"python"
) | Where-Object { $_ }
foreach ($candidate in $candidates) {
+94
View File
@@ -0,0 +1,94 @@
"""Update the Kirara sprite rectangles in web/index.html from transparent PNGs.
Usage: .venv/Scripts/python.exe scripts/build_kirara_sprite_catalog.py
"""
from pathlib import Path
from PIL import Image
ROOT = Path(__file__).resolve().parents[1]
ASSETS = ROOT / "web" / "uploads"
PAGE = ROOT / "web" / "index.html"
START = " // Source rectangles come from the four generated transparent 2×2 walk sheets."
END = " let kiraraDirection = \"front\";"
def frame_boxes(path: Path) -> list[list[int]]:
image = Image.open(path).convert("RGBA")
width, height = image.size
if width < 100 or height < 100:
raise ValueError(f"Sprite is unexpectedly small: {path}")
boxes = []
for frame in range(4):
start = round(frame * width / 4)
stop = round((frame + 1) * width / 4)
alpha = image.crop((start, 0, stop, height)).getchannel("A")
content = alpha.point(lambda value: 255 if value >= 16 else 0).getbbox()
if content is None:
raise ValueError(f"Empty frame {frame}: {path}")
left = max(0, content[0] - 3)
top = max(0, content[1] - 3)
right = min(stop - start, content[2] + 3)
bottom = min(height, content[3] + 3)
boxes.append([start + left, top, right - left, bottom - top])
return boxes
names = [
*(f"kirara_walk_{direction}" for direction in ("front", "back", "left", "right")),
"kirara_eat_rice", "kirara_eat_dango", "kirara_bath_sponge", "kirara_bath_shower",
*(f"kirara_idle_{name}" for name in (
"delivery", "reading", "phone", "sleep", "stretch", "groom",
"butterfly", "tea", "yarn", "humming")),
"kirara_rest_breathe",
*(f"kirara_emotion_{name}_{variant}" for name in (
"plus3", "plus1", "minus1", "minus3") for variant in ("a", "b")),
]
lines = [
" // Crops follow each generated sprite's transparent silhouette.",
" const kiraraSpriteBoxes = {",
]
for name in names:
boxes = frame_boxes(ASSETS / f"{name}.png")
formatted = ", ".join(str(box).replace(" ", "") for box in boxes)
lines.append(f' "{name}": [{formatted}],')
lines += [
" };",
" const kiraraSpriteAsset = name => ({",
" src: `/uploads/${name}.png`, boxes: kiraraSpriteBoxes[name]",
" });",
" const kiraraWalkAssets = Object.fromEntries(",
" [\"front\", \"back\", \"left\", \"right\"].map(direction =>",
" [direction, kiraraSpriteAsset(`kirara_walk_${direction}`)])",
" );",
" const kiraraActionAssets = {",
" feed: [kiraraSpriteAsset(\"kirara_eat_rice\"), kiraraSpriteAsset(\"kirara_eat_dango\")],",
" bath: [kiraraSpriteAsset(\"kirara_bath_sponge\"), kiraraSpriteAsset(\"kirara_bath_shower\")]",
" };",
" const kiraraIdleAssets = Object.fromEntries(",
" [\"delivery\", \"reading\", \"phone\", \"sleep\", \"stretch\",",
" \"groom\", \"butterfly\", \"tea\", \"yarn\", \"humming\"].map(type =>",
" [type, kiraraSpriteAsset(`kirara_idle_${type}`)])",
" );",
" const kiraraRestAsset = kiraraSpriteAsset(\"kirara_rest_breathe\");",
" const kiraraEmotionAssets = {",
]
for delta, word in ((3, "plus3"), (1, "plus1"), (-1, "minus1"), (-3, "minus3")):
lines.append(
f' "{delta}": [kiraraSpriteAsset("kirara_emotion_{word}_a"), '
f'kiraraSpriteAsset("kirara_emotion_{word}_b")],'
)
lines += [" };", ""]
text = PAGE.read_text(encoding="utf-8")
start = text.find(START)
if start < 0:
start = text.find(" // Crops follow each generated sprite's transparent silhouette.")
end = text.find(END, start)
if start < 0 or end < 0:
raise ValueError("Kirara asset block was not found")
PAGE.write_text(text[:start] + "\n".join(lines) + text[end:], encoding="utf-8")
print(f"Updated {len(names)} Kirara sprite sheets in {PAGE}")
+132
View File
@@ -0,0 +1,132 @@
"""Lightweight perf sampler: GPU (nvidia-smi) + system/per-process CPU every 30s.
Writes CSV rows to logs/perf_samples.csv. Designed to run all day in background
so TTS synthesis duration (logs/tts_memory.log) can be correlated against
GPU temperature/clocks and CPU pressure afterwards.
"""
from __future__ import annotations
import csv
import shutil
import subprocess
import time
from datetime import datetime
from pathlib import Path
import psutil
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "logs" / "perf_samples.csv"
INTERVAL_SEC = 30
NVIDIA_SMI = shutil.which("nvidia-smi") or r"C:\Windows\System32\nvidia-smi.exe"
# process name substrings (lowercase) to track individually
TRACKED = {
"livehime": "livehime.exe",
"yuanshen": "yuanshen.exe",
"bettergi": "bettergi.exe",
"mpv": "mpv.exe",
"audiodg": "audiodg.exe",
"python": None, # aggregated below (main + TTS worker)
}
FIELDNAMES = [
"ts",
"gpu_temp_c", "gpu_clock_mhz", "gpu_mem_clock_mhz", "gpu_util_pct",
"gpu_power_w", "gpu_throttle",
"cpu_total_pct",
"cpu_livehime", "cpu_yuanshen", "cpu_bettergi", "cpu_mpv", "cpu_audiodg",
"cpu_python_total", "python_proc_count",
]
def query_gpu() -> dict:
keys = (
"temperature.gpu", "clocks.current.graphics", "clocks.current.memory",
"utilization.gpu", "power.draw", "clocks_event_reasons.active",
)
try:
out = subprocess.check_output(
[NVIDIA_SMI, f"--query-gpu={','.join(keys)}", "--format=csv,noheader,nounits"],
text=True, timeout=5,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
).strip()
temp, clk, memclk, util, power, throttle = [x.strip() for x in out.split(",")]
return {
"gpu_temp_c": temp,
"gpu_clock_mhz": clk,
"gpu_mem_clock_mhz": memclk,
"gpu_util_pct": util,
"gpu_power_w": power,
"gpu_throttle": throttle,
}
except Exception:
return {k: "" for k in FIELDNAMES if k.startswith("gpu_")}
def sample_cpu(procs: dict[str, list[psutil.Process]]) -> dict:
row = {"cpu_total_pct": psutil.cpu_percent(interval=None)}
py_total = 0.0
py_count = 0
for name, plist in procs.items():
total = 0.0
alive = []
for p in plist:
try:
total += p.cpu_percent(interval=None)
alive.append(p)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
procs[name] = alive
if name == "python":
py_total, py_count = total, len(alive)
else:
row[f"cpu_{name}"] = round(total, 1)
row["cpu_python_total"] = round(py_total, 1)
row["python_proc_count"] = py_count
return row
def find_tracked() -> dict[str, list[psutil.Process]]:
found: dict[str, list[psutil.Process]] = {k: [] for k in TRACKED}
for p in psutil.process_iter(["name"]):
try:
pname = (p.info["name"] or "").lower()
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
for key in TRACKED:
if key in pname:
found[key].append(p)
return found
def main() -> None:
OUT.parent.mkdir(parents=True, exist_ok=True)
new_file = not OUT.exists()
procs = find_tracked()
# prime cpu_percent baselines
psutil.cpu_percent(interval=None)
for plist in procs.values():
for p in plist:
try:
p.cpu_percent(interval=None)
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
with OUT.open("a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
if new_file:
writer.writeheader()
while True:
time.sleep(INTERVAL_SEC)
if time.monotonic() % 300 < INTERVAL_SEC: # re-discover processes ~every 5 min
procs = find_tracked()
row = {"ts": datetime.now().isoformat(timespec="seconds")}
row.update(query_gpu())
row.update(sample_cpu(procs))
writer.writerow(row)
f.flush()
if __name__ == "__main__":
main()