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:
326
scripts/seslendir.py
Normal file
326
scripts/seslendir.py
Normal 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()
|
||||
Reference in New Issue
Block a user