Files
kolaytercih/scripts/seslendir.py
bilalgursen da0e9dff7a docs(ekip): 7–10 Ekim raporları, eval sonuçları, seslendirme betiği
- docs/ekip-raporlari/2026-10-07, 2026-10-09, 2026-10-10 raporları ve ekran görüntüleri
  (ses/ altındaki mp3 dosyaları depoya alınmadı, 38 MB)
- BACKLOG #38: rehber yazılarına "Dinle" düğmesi adayı
- scripts/seslendir.py (Türkçe TTS) ve .gitignore'a .venv-tts/
- scripts/eval/sonuc: 8–10 Ekim revizyon eval çıktıları

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 00:33:39 +03:00

327 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 <girdi.md> <cikti_klasoru>
Çıktı: <klasor>/<ad>.mp3 ve <klasor>/<ad>.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()