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>
This commit is contained in:
bilalgursen
2026-10-11 00:33:39 +03:00
parent e9dfa354f8
commit da0e9dff7a
81 changed files with 18396 additions and 0 deletions

326
scripts/seslendir.py Normal file
View File

@@ -0,0 +1,326 @@
"""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()