practitioner

This commit is contained in:
2026-04-27 22:18:28 +03:00
parent 210f46ab21
commit 7ee5c94617
159 changed files with 125122 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
"""Copies practitioner MP3s to audio_phone/ with phone-friendly names + ID3 tags + cover.
For each module:
- rename to M{N}-{idx:02d}-{tema}.mp3
- tag: title, album="NLP Practitioner - Modul N - Tema", artist, track, year, genre
- embed PNG cover (generated with Pillow) showing module + theme
Run: .venv/Scripts/python.exe rename_phone_practitioner.py
"""
from __future__ import annotations
import json
import shutil
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from mutagen.id3 import APIC, ID3, ID3NoHeaderError, TALB, TIT2, TPE1, TPE2, TRCK, TYER, TCON
from mutagen.mp3 import MP3
ROOT = Path(__file__).parent
PRACT = ROOT / "nlp-practitioner"
AUDIO_SRC = PRACT / "audio"
AUDIO_DST = PRACT / "audio_phone"
COVERS_DIR = PRACT / "audio_phone" / "_covers"
MANIFEST = PRACT / "manifest.json"
THEMES = {
1: {"slug": "PACING_RAPORT", "title_full": "Pacing si Raport", "filename_phrase": "Pacing si Raport"},
2: {"slug": "SUBMODALITATI", "title_full": "Submodalitati si VAK", "filename_phrase": "Submodalitati"},
3: {"slug": "OBIECTIVE", "title_full": "Setarea si Atingerea Obiectivelor", "filename_phrase": "Obiective"},
4: {"slug": "NIVELURI_NEUROLOGICE", "title_full": "Niveluri Neurologice si Metamodel", "filename_phrase": "Niveluri Neurologice"},
5: {"slug": "MILTON_MODEL", "title_full": "Modelul Milton, Hipnoza si Asertivitate","filename_phrase": "Modelul Milton"},
6: {"slug": "RECADRARE_PARTI", "title_full": "Recadrare si Parti Interioare", "filename_phrase": "Recadrare si Parti"},
7: {"slug": "PARTI_INTERIOARE", "title_full": "Parti Interioare si Triunghi Dramatic", "filename_phrase": "Parti Interioare"},
8: {"slug": "INTEGRARE_MICROSTRATEGII", "title_full": "Integrare, Linia Timpului, Microstrategii","filename_phrase": "Microstrategii"},
}
ARTIST = "cursnlp.ro"
ALBUM_ARTIST = "Horia Radu si Marius Mutu"
YEAR = "2024"
GENRE = "Speech"
COURSE_PREFIX = "NLP Practitioner"
def find_font(size: int) -> ImageFont.FreeTypeFont:
candidates = [
"C:/Windows/Fonts/segoeuib.ttf",
"C:/Windows/Fonts/arialbd.ttf",
"C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/segoeui.ttf",
]
for c in candidates:
if Path(c).exists():
return ImageFont.truetype(c, size)
return ImageFont.load_default()
def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.ImageDraw) -> list[str]:
words = text.split()
lines: list[str] = []
cur = ""
for w in words:
trial = (cur + " " + w).strip()
bbox = draw.textbbox((0, 0), trial, font=font)
if bbox[2] - bbox[0] <= max_width:
cur = trial
else:
if cur:
lines.append(cur)
cur = w
if cur:
lines.append(cur)
return lines
# Per-module accent colors (calm, distinct, readable on white text)
PALETTE = {
1: ("#1f4e79", "#2e75b6"), # blue
2: ("#5b2c6f", "#9b59b6"), # purple
3: ("#922b21", "#cd6155"), # red
4: ("#1e8449", "#52be80"), # green
5: ("#7e5109", "#d68910"), # amber/gold
6: ("#0e6655", "#48c9b0"), # teal
7: ("#4a235a", "#7d3c98"), # deep purple
8: ("#212f3c", "#566573"), # slate
}
def make_cover(module_num: int, theme_full: str, out_path: Path) -> Path:
"""Generate 1000x1000 PNG cover with gradient + module + theme text."""
size = 1000
img = Image.new("RGB", (size, size), "#ffffff")
draw = ImageDraw.Draw(img)
c1, c2 = PALETTE.get(module_num, ("#222", "#666"))
def hex_to_rgb(h: str) -> tuple[int, int, int]:
h = h.lstrip("#")
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
r1, g1, b1 = hex_to_rgb(c1)
r2, g2, b2 = hex_to_rgb(c2)
for y in range(size):
t = y / (size - 1)
r = int(r1 + (r2 - r1) * t)
g = int(g1 + (g2 - g1) * t)
b = int(b1 + (b2 - b1) * t)
draw.line([(0, y), (size, y)], fill=(r, g, b))
# Course label (small, top)
f_small = find_font(36)
label = COURSE_PREFIX + " - 2024"
bbox = draw.textbbox((0, 0), label, font=f_small)
draw.text(((size - (bbox[2] - bbox[0])) / 2, 60), label, font=f_small, fill="#ffffff")
# Big module number
f_huge = find_font(280)
mod_text = f"M{module_num}"
bbox = draw.textbbox((0, 0), mod_text, font=f_huge)
draw.text(((size - (bbox[2] - bbox[0])) / 2, 160), mod_text, font=f_huge, fill="#ffffff")
# "MODUL N" word
f_med = find_font(54)
sub = f"MODUL {module_num}"
bbox = draw.textbbox((0, 0), sub, font=f_med)
draw.text(((size - (bbox[2] - bbox[0])) / 2, 470), sub, font=f_med, fill="#ffffff")
# Theme (wrapped)
f_theme = find_font(64)
lines = wrap_text(theme_full, f_theme, max_width=size - 120, draw=draw)
y = 600
for line in lines:
bbox = draw.textbbox((0, 0), line, font=f_theme)
draw.text(((size - (bbox[2] - bbox[0])) / 2, y), line, font=f_theme, fill="#ffffff")
y += int((bbox[3] - bbox[1]) * 1.4)
# Footer
f_foot = find_font(28)
foot = ARTIST
bbox = draw.textbbox((0, 0), foot, font=f_foot)
draw.text(((size - (bbox[2] - bbox[0])) / 2, size - 70), foot, font=f_foot, fill="#ffffff")
out_path.parent.mkdir(parents=True, exist_ok=True)
img.save(out_path, "PNG", optimize=True)
return out_path
def safe_filename_part(s: str) -> str:
bad = '<>:"/\\|?*'
for ch in bad:
s = s.replace(ch, "")
return s.strip()
def tag_mp3(mp3_path: Path, *, title: str, album: str, track_num: int, track_total: int, cover_png: Path) -> None:
try:
tags = ID3(mp3_path)
tags.delete()
except ID3NoHeaderError:
tags = ID3()
tags.add(TIT2(encoding=3, text=title))
tags.add(TALB(encoding=3, text=album))
tags.add(TPE1(encoding=3, text=ARTIST))
tags.add(TPE2(encoding=3, text=ALBUM_ARTIST))
tags.add(TRCK(encoding=3, text=f"{track_num}/{track_total}"))
tags.add(TYER(encoding=3, text=YEAR))
tags.add(TCON(encoding=3, text=GENRE))
with open(cover_png, "rb") as f:
img_bytes = f.read()
tags.add(APIC(encoding=3, mime="image/png", type=3, desc="Cover", data=img_bytes))
tags.save(mp3_path, v2_version=3)
def main() -> int:
if not MANIFEST.exists():
print(f"ERROR: manifest not found: {MANIFEST}")
return 1
with MANIFEST.open(encoding="utf-8") as f:
manifest = json.load(f)
AUDIO_DST.mkdir(parents=True, exist_ok=True)
COVERS_DIR.mkdir(parents=True, exist_ok=True)
total_copied = 0
total_skipped = 0
total_errors = 0
for mod in manifest["modules"]:
name = mod["name"] # "Modul N"
try:
mod_num = int(name.split()[-1])
except Exception:
print(f"SKIP module without numeric suffix: {name}")
continue
theme = THEMES.get(mod_num)
if theme is None:
print(f"SKIP module {mod_num}: no theme defined")
continue
audio_lectures = [lec for lec in mod.get("lectures", []) if lec.get("type") == "audio"]
if not audio_lectures:
continue
cover_path = COVERS_DIR / f"M{mod_num}.png"
if not cover_path.exists():
make_cover(mod_num, theme["title_full"], cover_path)
print(f" cover: {cover_path.name}")
album = f"{COURSE_PREFIX} - Modul {mod_num} - {theme['title_full']}"
track_total = len(audio_lectures)
out_dir = AUDIO_DST / f"M{mod_num} - {theme['filename_phrase']}"
out_dir.mkdir(parents=True, exist_ok=True)
for idx, lec in enumerate(audio_lectures, start=1):
src_path_str = lec.get("audio_path", "")
src = ROOT / src_path_str
if not src.exists():
print(f" MISSING src: {src}")
total_errors += 1
continue
phrase = safe_filename_part(theme["filename_phrase"])
new_name = f"M{mod_num}-{idx:02d}-{phrase}.mp3"
dst = out_dir / new_name
if dst.exists() and dst.stat().st_size == src.stat().st_size:
print(f" skip (already copied): {new_name}")
total_skipped += 1
else:
shutil.copy2(src, dst)
print(f" copied: {new_name}")
total_copied += 1
title_short = lec.get("title", new_name)
track_title = f"M{mod_num} {title_short} - {theme['filename_phrase']}"
try:
tag_mp3(
dst,
title=track_title,
album=album,
track_num=idx,
track_total=track_total,
cover_png=cover_path,
)
except Exception as e:
print(f" TAG ERROR for {new_name}: {e}")
total_errors += 1
print(f"\nDONE. copied={total_copied} skipped={total_skipped} errors={total_errors}")
print(f"Output: {AUDIO_DST}")
return 0 if total_errors == 0 else 2
if __name__ == "__main__":
sys.exit(main())