合并外部维护版本并同步自动每日功能
This commit is contained in:
@@ -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")
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user