Files
felhom.eu/documentation/audits/sizing-2026-10-13/tools/make_content.py
T
admin c885b5acf4 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.
2026-10-13 13:23:51 +02:00

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()