c885b5acf4
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.
150 lines
7.2 KiB
Python
150 lines
7.2 KiB
Python
#!/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()
|