Audit: what a Felhom box needs — floor, 64 apps' memory and CPU, power (N100 bench); R-993..R-995; R-231 due check read green
Measured on demo-felhom (N100, 16 GB): 61 apps deployed and removed through the product, sampled every 5 s; heavy apps with one real action (Immich 200 photos, Jellyfin transcode, Paperless OCR, Nextcloud 500 MB, ...). sizing.json for the estimator (tools/build_sizing.py), SIZING.md with the proposed minimum machine, METHOD.md. Power is RAPL CPU-package only (no meter). R-993 asks the operator to approve the minimum machine; R-994 lists catalog limits/requests that are too low; R-995 records leftover unnamed volumes after removal. R-231's dated check measured: dooplex-backup.service success, journal green.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""make_content.py OUTDIR — the bench's test content. Generated, or public domain. No personal data.
|
||||
|
||||
photos/ 140 generated 4000x3000 JPEGs (gradients + noise, phone-photo sized)
|
||||
+ up to 60 CC0 portrait PAINTINGS from The Met's Open Access API (isPublicDomain=true),
|
||||
resized to 4000 px — faces for Immich's face detection, of people long dead.
|
||||
pdfs/ 20 image-only A4 "scans" at 300 dpi (rendered text, slight skew + noise) — OCR has work
|
||||
video/ 3 generated 1080p H.264 clips (testsrc2 + tone), 3 min each
|
||||
upload/ 50 x 10 MiB random files = 500 MiB
|
||||
books/ 20 minimal EPUB 3 books (generated text)
|
||||
roms/ a few generated dummy files in platform folders (RomM's scan only; no real ROMs)
|
||||
|
||||
Run with `python3 -I`. Needs Pillow and ffmpeg (both present on DooPlex).
|
||||
"""
|
||||
import io, json, os, random, subprocess, sys, urllib.request, zipfile
|
||||
from PIL import Image, ImageDraw, ImageFilter, ImageFont
|
||||
|
||||
OUT = sys.argv[1]
|
||||
rnd = random.Random(20261013)
|
||||
|
||||
|
||||
def mk(p):
|
||||
os.makedirs(os.path.join(OUT, p), exist_ok=True)
|
||||
return os.path.join(OUT, p)
|
||||
|
||||
|
||||
def photos():
|
||||
d = mk("photos")
|
||||
for i in range(140):
|
||||
w, h = 4000, 3000
|
||||
c1 = tuple(rnd.randrange(256) for _ in range(3)); c2 = tuple(rnd.randrange(256) for _ in range(3))
|
||||
g = Image.linear_gradient("L").resize((w, h))
|
||||
im = Image.composite(Image.new("RGB", (w, h), c1), Image.new("RGB", (w, h), c2), g)
|
||||
dr = ImageDraw.Draw(im)
|
||||
for _ in range(40):
|
||||
x, y = rnd.randrange(w), rnd.randrange(h); r = rnd.randrange(50, 600)
|
||||
dr.ellipse([x - r, y - r, x + r, y + r], fill=tuple(rnd.randrange(256) for _ in range(3)))
|
||||
noise = Image.effect_noise((w, h), 40).convert("RGB")
|
||||
im = Image.blend(im, noise, 0.25)
|
||||
im.save(os.path.join(d, f"gen-{i:03d}.jpg"), quality=92)
|
||||
# CC0 portrait paintings (The Met, European Paintings, public domain)
|
||||
try:
|
||||
q = "https://collectionapi.metmuseum.org/public/collection/v1.1/search?departmentId=11&hasImages=true&q=portrait&limit=150&offset=0"
|
||||
ids = json.load(urllib.request.urlopen(q, timeout=60))["objectIDs"]
|
||||
except Exception as e:
|
||||
print("met search failed:", e); ids = []
|
||||
got, credits = 0, []
|
||||
for oid in ids:
|
||||
if got >= 60:
|
||||
break
|
||||
try:
|
||||
o = json.load(urllib.request.urlopen(f"https://collectionapi.metmuseum.org/public/collection/v1/objects/{oid}", timeout=60))
|
||||
if not o.get("isPublicDomain") or not o.get("primaryImage"):
|
||||
continue
|
||||
raw = urllib.request.urlopen(o["primaryImage"], timeout=120).read()
|
||||
im = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||
im.thumbnail((4000, 4000))
|
||||
im.save(os.path.join(d, f"met-{oid}.jpg"), quality=92)
|
||||
credits.append(f"{oid}\t{o.get('title','')[:80]}\t{o.get('objectURL','')}\tCC0")
|
||||
got += 1
|
||||
except Exception as e:
|
||||
print("skip", oid, e)
|
||||
open(os.path.join(OUT, "photos-credits.tsv"), "w").write("\n".join(credits) + "\n")
|
||||
print("photos:", 140 + got, "of which paintings:", got)
|
||||
|
||||
|
||||
WORDS = ("alma korte szilva haz kert konyv level szamla ugyfel datum osszeg fizetes hatarido "
|
||||
"szerzodes biztositas orvos recept iskola munka berlet viz gaz aram adoszam").split()
|
||||
|
||||
|
||||
def text(n):
|
||||
return " ".join(rnd.choice(WORDS) for _ in range(n))
|
||||
|
||||
|
||||
def pdfs():
|
||||
d = mk("pdfs")
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf", 38)
|
||||
except OSError:
|
||||
font = ImageFont.load_default()
|
||||
for i in range(20):
|
||||
im = Image.new("L", (2480, 3508), 245)
|
||||
dr = ImageDraw.Draw(im)
|
||||
y = 200
|
||||
dr.text((200, y), f"Bench document {i + 1:02d} - 2026-10-13", font=font, fill=20); y += 120
|
||||
while y < 3300:
|
||||
dr.text((200, y), text(9), font=font, fill=30); y += 62
|
||||
im = im.rotate(rnd.uniform(-1.2, 1.2), fillcolor=235).filter(ImageFilter.GaussianBlur(0.6))
|
||||
im = Image.blend(im, Image.effect_noise(im.size, 25).convert("L"), 0.08)
|
||||
im.save(os.path.join(d, f"scan-{i + 1:02d}.pdf"), resolution=300)
|
||||
print("pdfs: 20")
|
||||
|
||||
|
||||
def video():
|
||||
d = mk("video/Bench Films")
|
||||
for i in range(3):
|
||||
sub = os.path.join(d, f"Bench Film {i + 1} (2026)")
|
||||
os.makedirs(sub, exist_ok=True)
|
||||
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-f", "lavfi", "-i", f"testsrc2=size=1920x1080:rate=30",
|
||||
"-f", "lavfi", "-i", f"sine=frequency={440 + 110 * i}:sample_rate=48000",
|
||||
"-t", "180", "-c:v", "libx264", "-preset", "veryfast", "-b:v", "8M", "-pix_fmt", "yuv420p",
|
||||
"-c:a", "aac", "-b:a", "192k", os.path.join(sub, f"Bench Film {i + 1} (2026).mkv")], check=True)
|
||||
print("video: 3 x 180 s 1080p")
|
||||
|
||||
|
||||
def upload():
|
||||
d = mk("upload")
|
||||
for i in range(50):
|
||||
with open(os.path.join(d, f"blob-{i:02d}.bin"), "wb") as f:
|
||||
f.write(os.urandom(10 * 1024 * 1024))
|
||||
print("upload: 500 MiB")
|
||||
|
||||
|
||||
def books():
|
||||
d = mk("books")
|
||||
for i in range(20):
|
||||
p = os.path.join(d, f"bench-book-{i + 1:02d}.epub")
|
||||
with zipfile.ZipFile(p, "w") as z:
|
||||
z.writestr(zipfile.ZipInfo("mimetype"), "application/epub+zip", compress_type=zipfile.ZIP_STORED)
|
||||
z.writestr("META-INF/container.xml",
|
||||
'<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">'
|
||||
'<rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>')
|
||||
z.writestr("OEBPS/content.opf",
|
||||
f'<?xml version="1.0"?><package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="id">'
|
||||
f'<metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:identifier id="id">bench-{i}</dc:identifier>'
|
||||
f'<dc:title>Bench Book {i + 1}</dc:title><dc:creator>Felhom Bench</dc:creator><dc:language>hu</dc:language>'
|
||||
f'<meta property="dcterms:modified">2026-10-13T00:00:00Z</meta></metadata>'
|
||||
f'<manifest><item id="c" href="c.xhtml" media-type="application/xhtml+xml"/>'
|
||||
f'<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/></manifest>'
|
||||
f'<spine><itemref idref="c"/></spine></package>')
|
||||
z.writestr("OEBPS/nav.xhtml", '<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">'
|
||||
'<body><nav epub:type="toc"><ol><li><a href="c.xhtml">1</a></li></ol></nav></body></html>')
|
||||
z.writestr("OEBPS/c.xhtml", '<?xml version="1.0"?><html xmlns="http://www.w3.org/1999/xhtml"><body>'
|
||||
+ "".join(f"<p>{text(80)}</p>" for _ in range(200)) + "</body></html>")
|
||||
print("books: 20")
|
||||
|
||||
|
||||
def roms():
|
||||
for plat, ext in (("nes", "nes"), ("snes", "sfc"), ("gb", "gb")):
|
||||
d = mk(f"roms/{plat}")
|
||||
for i in range(5):
|
||||
with open(os.path.join(d, f"Bench Game {i + 1}.{ext}"), "wb") as f:
|
||||
f.write(os.urandom(256 * 1024))
|
||||
print("roms: 15 dummy files")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
for fn in (pdfs, books, roms, upload, video, photos):
|
||||
fn()
|
||||
Reference in New Issue
Block a user