"""Markdown raporu → konuşma metni → EMA Lightning (Türkçe TTS) ile mp3. Kurulum (bir kez; sanal ortam repo dışında ya da gitignore'lu kalsın): python3 -m venv .venv-tts .venv-tts/bin/pip install ema-lightning soundfile # ffmpeg gerekir (brew install ffmpeg) Kullanım: .venv-tts/bin/python scripts/seslendir.py Çıktı: /.mp3 ve /.konusma.txt (okunan metin, denetim için). Model dosyaları ilk çalıştırmada indirilir (~34 MB), sonrası çevrimdışı. CPU'da gerçek zamana yakın hız (16 dk ses ≈ 4 dk üretim, M-serisi Mac). Dönüşüm kuralları: - Başlığında "kaynak" ya da "teknik kapsam" geçen bölümler okunmaz; URL, dosya yolu ve kod atılır. - Tablolar "başlık: değer" cümlelerine çevrilir; madde işaretleri düz cümle olur. - Model büyük harfli kısaltmaları harf harf heceler; SOZLUK listesi bunları Türkçe okunuşa çevirir (YÖK → Yök, YKS → ye ka se, WhatsApp → Vatsap). Bozuk okunan kelime duyulursa buraya satır eklenir ve dosya yeniden üretilir. - Rehber yazıları için kullanılacaksa frontmatter (--- ... ---) önce kesilmeli; henüz yok. İlk kullanım: 9 Eki 2026, docs/ekip-raporlari/2026-10-07/ses/ (7 rapor). """ import re import subprocess import sys import time from pathlib import Path import numpy as np import soundfile as sf # --- Kısaltma ve sembol sözlüğü (model büyük harfi harf harf okuyor) --- SOZLUK = [ (r"founder-arastirma-0?(\d+)", r"araştırma raporu \1"), (r"\bfounder\b", "faundır"), (r"\bFounder\b", "Faundır"), (r"\bBACKLOG\b", "iş listesi"), (r"\blean\b", "yalın"), (r"\bworktree\b", "çalışma ağacı"), (r"\binfluencer\b", "influensır"), (r"\bfooter\b", "alt bilgi"), (r"\bTrends\b", "Trendz"), (r"\bWebGPU\b", "veb ci pi yu"), (r"\bKolayTercih\b", "Kolay Tercih"), (r"\bYÖK Atlas\b", "Yök Atlas"), (r"\bYÖK\b", "Yök"), (r"\bÖSYM\b", "Ösym"), (r"\bMEBİ\b", "Mebi"), (r"\bMEB\b", "Meb"), (r"\bİBB\b", "i be be"), (r"\bYKS\b", "ye ka se"), (r"\bTYT\b", "te ye te"), (r"\bAYT\b", "a ye te"), (r"\bDGS\b", "de ge se"), (r"\bLGS\b", "le ge se"), (r"\bKPSS\b", "ka pe se se"), (r"\bEKPSS\b", "e ka pe se se"), (r"\bAÖF\b", "a ö fe"), (r"\bOBP\b", "o be pe"), (r"\bKVKK\b", "ka ve ka ka"), (r"\bETBİS\b", "Etbis"), (r"\bMERSİS\b", "Mersis"), (r"\bTÜRKPATENT\b", "Türk Patent"), (r"\bTÜİK\b", "Tüik"), (r"\bPDR\b", "pe de re"), (r"\bRAM\b", "ram"), (r"\bPDF\b", "pe de fe"), (r"\bSEO\b", "seo"), (r"\bGEO\b", "ceo"), (r"\bLLM\b", "dil modeli"), (r"\bAI\b", "yapay zekâ"), (r"\bNPS\b", "en pe es"), (r"\bCTA\b", "çağrı düğmesi"), (r"\bUTM\b", "u te me"), (r"\bOG\b", "o ge"), (r"\bPWA\b", "web uygulaması"), (r"\bMCP\b", "em si pi"), (r"\bAPI\b", "a pe i"), (r"\bCTO\b", "si ti o"), (r"\bCEO\b", "si i o"), (r"\bB2B\b", "kuruma satış"), (r"\bTL\b", "lira"), (r"\bUSD\b", "dolar"), (r"\bTR\b", "Türkiye"), (r"\bAB\b", "Avrupa Birliği"), (r"\bWER\b", "hata oranı"), (r"\bQS\b", "ku es"), (r"\bTHE\b", "ti eyç i"), (r"\bMSY\b", "Mesafeli Sözleşmeler Yönetmeliği"), (r"\bDHA\b", "de ha a"), (r"\bIAB\b", "ay a be"), (r"\bWhatsApp\b", "Vatsap"), (r"\bLinkedIn\b", "Linkdin"), (r"\bYouTube\b", "Yutup"), (r"\bShorts\b", "Şorts"), (r"\bTikTok\b", "Tiktok"), (r"\bFacebook\b", "Feysbuk"), (r"\bGoogle\b", "Gugıl"), (r"\bChatGPT\b", "Çet ci pi ti"), (r"\bDiscord\b", "Diskord"), (r"\bRybbit\b", "Ribit"), (r"\biyzico\b", "iyziko"), (r"\bWebrazzi\b", "Vebrazzi"), (r"\bArmut\b", "Armut"), (r"\bTally\b", "Tali"), (r"\bFormbricks\b", "Formbriks"), (r"\bCanny\b", "Keni"), (r"\bNotion\b", "Noşın"), (r"\bKhan Academy\b", "Kan Akademi"), (r"\bDuolingo\b", "Duolingo"), (r"\bKunduz\b", "Kunduz"), (r"\bTrustpilot\b", "Trast paylot"), (r"\bŞikayetvar\b", "Şikayet var"), (r"\bEkşi Sözlük\b", "Ekşi Sözlük"), (r"\bHypeAuditor\b", "Hayp oditır"), (r"\bWebtekno\b", "Vebtekno"), (r"\bResend\b", "Risend"), (r"\bDrizzle\b", "Drizıl"), (r"\bTercihAI\b", "Tercih yapay zekâ"), (r"\bUniverlist\b", "Üniverlist"), (r"\bKariyer\.net\b", "Kariyer net"), (r"\bMemurlar\.net\b", "Memurlar net"), (r"\bArc\b", "Ark"), (r"\bfp32\b", "ef pe otuz iki"), (r"\bfp16\b", "ef pe on altı"), (r"\be-posta\b", "e posta"), (r"\be-Arşiv\b", "e arşiv"), (r"\be-Fatura\b", "e fatura"), (r"\be-Devlet\b", "e devlet"), (r"\be-Rehberlik\b", "e rehberlik"), (r"\bQ([1-4])\b", r"hızlı iş \1"), (r"\bK(\d{1,2})\b", r"karar \1"), (r"\bD(\d)\b", r"de \1"), (r"#(\d+)", r"madde \1"), (r"§\s*(\d+)", r"bölüm \1"), (r"\bmd\.\s*(\d+)", r"madde \1"), (r"\bvb\.", "ve benzeri"), (r"\bvs\.", "ve saire"), (r"\bör\.", "örneğin"), (r"\bbkz\.", "bakınız"), (r"\bdk\b", "dakika"), (r"\bsn\b", "saniye"), (r"\bHf\b", "hafta"), (r"\bEyl\b", "Eylül"), (r"\bEki\b", "Ekim"), (r"\bKas\b", "Kasım"), (r"\bAra\b", "Aralık"), (r"\bOca\b", "Ocak"), (r"\bŞub\b", "Şubat"), (r"\bMar\b", "Mart"), (r"\bNis\b", "Nisan"), (r"\bMay\b", "Mayıs"), (r"\bHaz\b", "Haziran"), (r"\bTem\b", "Temmuz"), (r"\bAğu\b", "Ağustos"), ] def sembol(t: str) -> str: t = re.sub(r"https?://\S+", "", t) t = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", t) # [metin](url) → metin t = t.replace("[D]", "").replace("[K]", "") t = re.sub(r"\[\?\]", " (doğrulanmadı)", t) t = re.sub(r"\[DOĞRULANAMADI\]", " (doğrulanamadı)", t) t = re.sub(r"\[BİLAL[^\]]*\]", " (Bilal karar verecek)", t) t = re.sub(r"\[(\d+)\]", "", t) # [12] dipnot # kod ve dosya yolları t = re.sub(r"`([^`]*)`", lambda m: "" if re.search(r"[/\\.:]|\(\)", m.group(1)) else m.group(1), t) t = re.sub(r"\b[\w-]+\.(tsx?|md|ts|py|db|json|css|sql)\b", "", t) t = re.sub(r"(\d)\s*→\s*(\d)", r"\1'den \2'e", t) t = t.replace("←", ", öncesi ") t = t.replace("≈", "yaklaşık ").replace("≥", "en az ").replace("≤", "en çok ") t = re.sub(r"~\s*(\d)", r"yaklaşık \1", t) t = re.sub(r"%\s*(\d+(?:[,.]\d+)?)", r"yüzde \1", t) t = re.sub(r"(\d+(?:[,.]\d+)?)\s*%", r"yüzde \1", t) t = re.sub(r"(\d)\s*[–-]\s*(\d)", r"\1 ile \2", t) # 10–20 → 10 ile 20 t = re.sub(r"(\d)\s*×\s*(\d)", r"\1 çarpı \2", t) t = t.replace("×", " kere") t = re.sub(r"(\d)\.(\d{3})\b", r"\1\2", t) # 1.279.439 → 1279439 t = re.sub(r"(\d)\.(\d{3})\b", r"\1\2", t) t = re.sub(r"(\d),(\d)", r"\1 virgül \2", t) t = re.sub(r"(\d)\s*M\s+TL", r"\1 milyon lira", t) t = re.sub(r"(\d)\s*M\b", r"\1 milyon", t) t = re.sub(r"(\d)\s*K\b", r"\1 bin", t) t = re.sub(r"\b(\d{4})-(\d{2})-(\d{2})\b", r"\3 \2 \1", t) t = t.replace("·", ", ").replace("—", ", ").replace("–", ", ") t = re.sub(r"/\s*hafta\b", " haftada", t) t = re.sub(r"/\s*ay\b", " ayda", t) t = re.sub(r"/\s*gün\b", " günde", t) t = re.sub(r"/\s*yıl\b", " yılda", t) t = re.sub(r"/\s*paylaşım\b", " paylaşım başına", t) t = re.sub(r"/\s*ödeme\b", " ödeme başına", t) t = t.replace("/", ", ") t = re.sub(r"\+\s*(\d)", r" artı \1", t) t = t.replace("+", " artı ") t = t.replace("@", " et ") t = t.replace("→", " ile ") t = t.replace("…", " ile ") t = re.sub(r"\*\*([^*]+)\*\*", r"\1", t) t = re.sub(r"\*([^*]+)\*", r"\1", t) t = re.sub(r"[#>|_*`~^]", " ", t) t = re.sub(r"[()\[\]{}\"“”«»]", " ", t) for pat, rep in SOZLUK: t = re.sub(pat, rep, t) t = re.sub(r"(\s*,\s*){2,}", ", ", t) t = re.sub(r"\s+([.,;:])", r"\1", t) t = re.sub(r"\s+", " ", t).strip() return t def tablo_cumle(satirlar: list[str]) -> list[str]: hucreler = [[h.strip() for h in s.strip().strip("|").split("|")] for s in satirlar] hucreler = [h for h in hucreler if not all(re.fullmatch(r":?-+:?", c or "-") for c in h)] if not hucreler: return [] baslik, govde = hucreler[0], hucreler[1:] out = [] for satir in govde: parcalar = [] for i, c in enumerate(satir): c = c.strip() if not c or c in ("-", "–", "—"): continue b = baslik[i].strip() if i < len(baslik) and baslik[i].strip() and baslik[i].strip() != "#" else "" parcalar.append(f"{b}: {c}" if b and i > 0 else c) if parcalar: out.append(". ".join(parcalar) + ".") return out def md_to_paragraflar(md: str) -> list[str]: paragraflar: list[str] = [] satirlar = md.splitlines() i = 0 atla = False kod = False while i < len(satirlar): s = satirlar[i] if s.strip().startswith("```"): kod = not kod i += 1 continue if kod: i += 1 continue if re.match(r"^#{1,6}\s", s): baslik = re.sub(r"^#+\s*", "", s) atla = bool(re.search(r"kaynak|teknik kapsam", baslik, re.I)) if atla: i += 1 continue baslik = re.sub(r"^\(?[a-e]\)\s*", "", baslik) paragraflar.append("§BOLUM§ " + baslik + ".") i += 1 continue if atla or not s.strip() or s.strip() in ("---", "***"): i += 1 continue if s.lstrip().startswith("|"): blok = [] while i < len(satirlar) and satirlar[i].lstrip().startswith("|"): blok.append(satirlar[i]) i += 1 paragraflar.extend(tablo_cumle(blok)) continue # liste maddesi / paragraf: tek satır s = re.sub(r"^\s*(?:[-*+]|\d+[.)])\s+", "", s) paragraflar.append(s) i += 1 return paragraflar def cumlele(metin: str, maks=240) -> list[str]: parcalar = re.split(r"(?<=[.!?;:])\s+", metin) out, cur = [], "" for p in parcalar: if len(cur) + len(p) + 1 > maks and cur: out.append(cur.strip()) cur = p else: cur = (cur + " " + p).strip() if cur: out.append(cur.strip()) return [c for c in out if re.search(r"[A-Za-zÇĞİÖŞÜçğıöşü]", c)] def main(): girdi, cikti = Path(sys.argv[1]), Path(sys.argv[2]) cikti.mkdir(parents=True, exist_ok=True) ad = girdi.stem paragraflar = md_to_paragraflar(girdi.read_text(encoding="utf-8")) # (metin, oncesinde_sessizlik_sn) plan: list[tuple[str, float]] = [] for p in paragraflar: bolum = p.startswith("§BOLUM§") t = sembol(p.replace("§BOLUM§", "")) if not t: continue if bolum: plan.append((t, 1.2)) continue for k, c in enumerate(cumlele(t)): plan.append((c, 0.55 if k == 0 else 0.0)) (cikti / f"{ad}.konusma.txt").write_text("\n".join(t for t, _ in plan), encoding="utf-8") from ema_lightning import EMA tts = EMA() sr = 48000 ses: list[np.ndarray] = [] t0 = time.time() for n, (t, bosluk) in enumerate(plan, 1): if bosluk: ses.append(np.zeros(int(sr * bosluk), dtype=np.float32)) sp = tts.say(t, seed=7) ses.append(sp.audio.astype(np.float32)) ses.append(np.zeros(int(sr * 0.18), dtype=np.float32)) if n % 20 == 0: print(f"{ad}: {n}/{len(plan)} cümle, {time.time()-t0:.0f} sn", flush=True) tum = np.concatenate(ses) wav = cikti / f"{ad}.wav" sf.write(wav, tum, sr) mp3 = cikti / f"{ad}.mp3" subprocess.run(["ffmpeg", "-y", "-loglevel", "error", "-i", str(wav), "-codec:a", "libmp3lame", "-b:a", "96k", str(mp3)], check=True) wav.unlink() print(f"BITTI {ad}: {len(plan)} cümle, {len(tum)/sr/60:.1f} dk ses, {time.time()-t0:.0f} sn üretim → {mp3}", flush=True) if __name__ == "__main__": main()