practitioner
This commit is contained in:
261
rename_phone_practitioner.py
Normal file
261
rename_phone_practitioner.py
Normal 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())
|
||||
Reference in New Issue
Block a user