turkce-normalize 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,48 @@
1
+ """
2
+ turkce-normalize — TTS için Türkçe metin normalizasyonu.
3
+
4
+ Sayıları, tarihleri, saatleri, para tutarlarını, kısaltmaları, Roma
5
+ rakamlarını, kesirleri, e-posta ve web adreslerini Türkçe okunuşlarına
6
+ çevirir. Bağımlılığı yoktur.
7
+
8
+ >>> from turkce_normalize import normalize
9
+ >>> normalize("Toplantı 14:30'da, %18 KDV dahil 1.250,50 ₺.")
10
+ 'Toplantı on dört otuzda, yüzde on sekiz katma değer vergisi dahil bin iki yüz elli lira elli kuruş.'
11
+ """
12
+ from .cografi import cografi_uygula
13
+ from .cumle import CumleAyirici, cumle_akisi, cumle_akisi_async, cumleleri_ayir
14
+ from .isaretler import (
15
+ EN_UZUN_DURAKLAMA, VARSAYILAN_DURAKLAMA, Parca,
16
+ isaret_var, isaretleri_ayir, isaretleri_temizle,
17
+ )
18
+ from .normalize import (
19
+ BILINEN_KELIMELER, BIRIMLER, KELIME_GIBI_OKUNANLAR, KISALTMALAR, ROMA_KARA_LISTE,
20
+ araliklari_oku, eposta_ve_adres_oku, kesirleri_oku, kisaltmalari_oku,
21
+ normalize, roma_coz, simgeleri_oku,
22
+ )
23
+ from .sayilar import (
24
+ HARF_OKUNUSU, bulunma_eki, harf_oku, ondalik_oku, rakamlari_tek_tek,
25
+ sayi_oku, sayiyi_coz, sira_oku, turkce_buyuk, turkce_kucuk,
26
+ )
27
+ from .telaffuz import CBS_TERIMLERI, VARSAYILAN_SOZLUK, sozluk_birlestir, telaffuz_uygula
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ __all__ = [
32
+ "normalize",
33
+ # sayılar
34
+ "sayi_oku", "sira_oku", "ondalik_oku", "rakamlari_tek_tek", "sayiyi_coz",
35
+ "bulunma_eki", "harf_oku", "turkce_buyuk", "turkce_kucuk", "HARF_OKUNUSU",
36
+ # alt adımlar
37
+ "cografi_uygula", "telaffuz_uygula", "eposta_ve_adres_oku", "simgeleri_oku",
38
+ "kesirleri_oku", "araliklari_oku", "kisaltmalari_oku", "roma_coz",
39
+ # sözlükler
40
+ "KISALTMALAR", "BIRIMLER", "KELIME_GIBI_OKUNANLAR", "BILINEN_KELIMELER",
41
+ "ROMA_KARA_LISTE", "CBS_TERIMLERI", "VARSAYILAN_SOZLUK", "sozluk_birlestir",
42
+ # cümle ayırıcı
43
+ "CumleAyirici", "cumleleri_ayir", "cumle_akisi", "cumle_akisi_async",
44
+ # işaretler
45
+ "Parca", "isaretleri_ayir", "isaret_var", "isaretleri_temizle",
46
+ "VARSAYILAN_DURAKLAMA", "EN_UZUN_DURAKLAMA",
47
+ "__version__",
48
+ ]
@@ -0,0 +1,45 @@
1
+ """
2
+ Komut satırı: ``python -m turkce_normalize "metin"``
3
+
4
+ Metin verilmezse standart girdiden satır satır okur::
5
+
6
+ echo "Toplantı 14:30'da." | python -m turkce_normalize
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import sys
12
+
13
+ from . import __version__, normalize
14
+
15
+
16
+ def main(argv=None) -> int:
17
+ ayristirici = argparse.ArgumentParser(
18
+ prog="python -m turkce_normalize",
19
+ description="Türkçe metni TTS için okunur hale getirir.",
20
+ )
21
+ ayristirici.add_argument("metin", nargs="*", help="normalize edilecek metin (yoksa stdin)")
22
+ ayristirici.add_argument("--cografi-kapali", action="store_true",
23
+ help="koordinat, ölçek, pafta kurallarını uygulama")
24
+ ayristirici.add_argument("--version", action="version", version=__version__)
25
+ secenek = ayristirici.parse_args(argv)
26
+
27
+ # Windows konsolunda Türkçe karakterler bozulmasın
28
+ for akis in (sys.stdout, sys.stdin):
29
+ if hasattr(akis, "reconfigure"):
30
+ try:
31
+ akis.reconfigure(encoding="utf-8")
32
+ except (OSError, ValueError):
33
+ pass
34
+
35
+ cografi = not secenek.cografi_kapali
36
+ if secenek.metin:
37
+ print(normalize(" ".join(secenek.metin), cografi=cografi))
38
+ else:
39
+ for satir in sys.stdin:
40
+ print(normalize(satir, cografi=cografi))
41
+ return 0
42
+
43
+
44
+ if __name__ == "__main__":
45
+ sys.exit(main())
@@ -0,0 +1,153 @@
1
+ """
2
+ Coğrafi ve kadastral gösterimlerin okunuşu.
3
+
4
+ Genel amaçlı TTS servisleri bu gösterimleri bilmez: koordinatı ondalık sayı
5
+ sanır, ölçeği bölme işlemi sanır, pafta kodunu hecelemeye çalışır. CBS
6
+ uygulamasında bunlar her ekranda geçer.
7
+
8
+ En kritik olan koordinat çifti. Normalizasyonun ondalık kuralı
9
+ ``39.9334, 32.8597`` gösterimini tek bir ondalık sayı sanıp
10
+ "üç yüz doksan dokuz bin üç yüz otuz dört virgül otuz iki" diye okur —
11
+ tamamen yanlış. Bu yüzden coğrafi kurallar sayı kurallarından ÖNCE çalışır.
12
+
13
+ Koordinat kesirleri rakam rakam okunur: "otuz dokuz virgül dokuz üç üç dört".
14
+ Sayı olarak okumak ("dokuz bin üç yüz otuz dört") hem uzun hem yanıltıcı.
15
+
16
+ ``normalize()`` bu modülü varsayılan olarak kullanır; ``cografi=False`` ile
17
+ kapatılır. Tek başına da çağrılabilir: ``cografi_uygula(metin)``.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from typing import Optional
23
+
24
+ from .sayilar import bulunma_eki, harf_oku, rakamlari_tek_tek, sayi_oku, sira_oku
25
+
26
+ #: Yön harfleri — hem Türkçe (K/G/D/B) hem İngilizce (N/S/E/W) gösterim.
27
+ YONLER = {
28
+ "K": "kuzey", "G": "güney", "D": "doğu", "B": "batı",
29
+ "N": "kuzey", "S": "güney", "E": "doğu", "W": "batı",
30
+ "KD": "kuzeydoğu", "KB": "kuzeybatı", "GD": "güneydoğu", "GB": "güneybatı",
31
+ "NE": "kuzeydoğu", "NW": "kuzeybatı", "SE": "güneydoğu", "SW": "güneybatı",
32
+ }
33
+
34
+ #: Alan ve uzunluk birimleri.
35
+ #:
36
+ #: Hepsi rakamın ardından gelmek zorunda. "da" (dekar) aynı zamanda Türkçe bir
37
+ #: ektir — "bu da", "orada" — rakam şartı olmadan sözlüğe koymak felaket olur.
38
+ OLCU_BIRIMLERI = {
39
+ "ha": "hektar",
40
+ "da": "dekar",
41
+ "km²": "kilometrekare", "km2": "kilometrekare",
42
+ "cm²": "santimetrekare", "cm2": "santimetrekare",
43
+ "mm²": "milimetrekare", "mm2": "milimetrekare",
44
+ "km³": "kilometreküp", "km3": "kilometreküp",
45
+ }
46
+
47
+ # Yön harfinin ardından harf gelmemeli: "39°56' Doğu" içindeki "D" yön sanılmasın.
48
+ _YON = r"(?:([KGDBNSEW])(?![^\W\d_]))?"
49
+
50
+ _DMS = re.compile(
51
+ r"(\d{1,3})\s*°\s*(\d{1,2})\s*['′]\s*(?:(\d{1,2}(?:[.,]\d+)?)\s*[\"″]\s*)?" + _YON
52
+ )
53
+ _ONDALIK_DERECE = re.compile(r"(\d{1,3})[.,](\d+)\s*°\s*" + _YON)
54
+ _KOORDINAT_CIFTI = re.compile(r"(?<![\w.-])(-?\d{1,3})\.(\d{4,})\s*,\s*(-?\d{1,3})\.(\d{4,})\b", re.ASCII)
55
+ _OLCEK = re.compile(r"\b1\s*[/:]\s*([\d.]{3,})\b", re.ASCII)
56
+ _PAFTA = re.compile(r"\b([A-ZĞÜŞİÖÇ])(\d{1,2})((?:-[a-d0-9]{1,3})+)\b", re.ASCII)
57
+ _EPSG = re.compile(r"\bEPSG\s*:?\s*(\d{4,6})\b", re.ASCII | re.IGNORECASE)
58
+ _UTM = re.compile(r"\bUTM\s*(\d{1,2})(?:\s*([NS]))?\b", re.ASCII | re.IGNORECASE)
59
+ _OLCU = [
60
+ (re.compile(r"(?:(?<=\d)|(?<=\d\s))" + re.escape(kisa) + r"(?![^\W_])"), OLCU_BIRIMLERI[kisa])
61
+ for kisa in sorted(OLCU_BIRIMLERI, key=len, reverse=True)
62
+ ]
63
+
64
+
65
+ def _kesir_oku(kesir: str) -> str:
66
+ """Ondalık kısmı rakam rakam okur: "9334" -> "dokuz üç üç dört"."""
67
+ return rakamlari_tek_tek(kesir)
68
+
69
+
70
+ def _yon_oku(harf: Optional[str]) -> str:
71
+ if not harf:
72
+ return ""
73
+ return " " + YONLER.get(harf.upper(), harf)
74
+
75
+
76
+ def _dms_oku(e: re.Match) -> str:
77
+ """Derece-dakika-saniye: 39°56'12"K"""
78
+ derece, dakika, saniye, yon = e.groups()
79
+ parcalar = [f"{sayi_oku(derece)} derece", f"{sayi_oku(dakika)} dakika"]
80
+ if saniye is not None:
81
+ tam, _, kesir = saniye.replace(",", ".").partition(".")
82
+ parcalar.append(
83
+ f"{sayi_oku(tam)} virgül {_kesir_oku(kesir)} saniye" if kesir else f"{sayi_oku(tam)} saniye"
84
+ )
85
+ return " ".join(parcalar) + _yon_oku(yon)
86
+
87
+
88
+ def _pafta_oku(e: re.Match) -> str:
89
+ """Pafta kodu — G22-b-14c-2d. Harf harf ve parça parça okunur."""
90
+ harf, sayi, kuyruk = e.groups()
91
+ parcalar = [harf_oku(harf), sayi_oku(sayi)]
92
+ for p in filter(None, kuyruk.split("-")):
93
+ for oge in re.findall(r"\d+|[a-d]", p):
94
+ parcalar.append(sayi_oku(oge) if oge.isdigit() else harf_oku(oge))
95
+ return " ".join(parcalar)
96
+
97
+
98
+ def _olcek_oku(e: re.Match) -> str:
99
+ """Ölçek — 1/25.000 -> "yirmi beş binde bir", 1:1.000.000 -> "bir milyonda bir"."""
100
+ payda = sayi_oku(e.group(1).replace(".", ""))
101
+ return f"{payda}{bulunma_eki(payda)} bir"
102
+
103
+
104
+ def _utm_oku(e: re.Match) -> str:
105
+ """UTM dilimi — "UTM 35N" -> "u te me otuz beşinci dilim kuzey".
106
+
107
+ Metin zaten "dilim" kelimesiyle devam ediyorsa ("UTM 35 dilimi") tekrar eklenmez.
108
+ """
109
+ devami = e.string[e.end():].lstrip()
110
+ dilim = "" if devami.startswith("dilim") else " dilim"
111
+ return f"u te me {sira_oku(int(e.group(1)))}{dilim}{_yon_oku(e.group(2))}"
112
+
113
+
114
+ def cografi_uygula(metin: str) -> str:
115
+ """Koordinat, ölçek, pafta, EPSG, UTM ve alan birimlerini okunur hale getirir."""
116
+ m = metin
117
+
118
+ # 1) Derece-dakika-saniye — 39°56'12"K / 39° 56' 12" K
119
+ m = _DMS.sub(_dms_oku, m)
120
+
121
+ # 2) Ondalık derece — 39.9334° K / 39,9334° K
122
+ m = _ONDALIK_DERECE.sub(
123
+ lambda e: f"{sayi_oku(e.group(1))} virgül {_kesir_oku(e.group(2))} derece{_yon_oku(e.group(3))}",
124
+ m,
125
+ )
126
+
127
+ # 3) Derece işaretsiz koordinat çifti — 39.9334, 32.8597
128
+ # Dört ve üzeri ondalık basamak koordinat sayılır; para ve ölçüm bu kadar
129
+ # hassas yazılmaz. Enlem/boylam sırası CBS'de standarttır, adıyla okunur.
130
+ m = _KOORDINAT_CIFTI.sub(
131
+ lambda e: f"enlem {sayi_oku(e.group(1))} virgül {_kesir_oku(e.group(2))}, "
132
+ f"boylam {sayi_oku(e.group(3))} virgül {_kesir_oku(e.group(4))}",
133
+ m,
134
+ )
135
+
136
+ # 4) Ölçek — 1/25.000 ve 1:25000 -> "yirmi beş binde bir"
137
+ m = _OLCEK.sub(_olcek_oku, m)
138
+
139
+ # 5) Pafta kodu — G22-b-14c-2d
140
+ # Harf harf ve parça parça okunur; hecelemeye çalışmak anlaşılmaz çıkarır.
141
+ m = _PAFTA.sub(_pafta_oku, m)
142
+
143
+ # 6) EPSG kodu — EPSG:5254. Kod, sayı değil; rakam rakam okunur.
144
+ m = _EPSG.sub(lambda e: f"e pe es ge {rakamlari_tek_tek(e.group(1))}", m)
145
+
146
+ # 7) UTM dilimi — UTM 35N
147
+ m = _UTM.sub(_utm_oku, m)
148
+
149
+ # 8) Alan ve uzunluk birimleri — yalnızca rakamın ardından
150
+ for desen, okunus in _OLCU:
151
+ m = desen.sub(okunus, m)
152
+
153
+ return m
@@ -0,0 +1,110 @@
1
+ """
2
+ Türkçe cümle ayırıcı — LLM token akışını cümle cümle TTS'e vermek için.
3
+
4
+ Doğallık hissinin kaynağı bu: cevabın tamamı bitmeden ilk cümle
5
+ seslendirilmeye başlar. Kısaltmalarda ve tarihlerde yanlış bölme yapmaz.
6
+
7
+ ayirici = CumleAyirici()
8
+ for parca in llm_akisi:
9
+ for cumle in ayirici.push(parca):
10
+ seslendir(cumle)
11
+ for cumle in ayirici.flush():
12
+ seslendir(cumle)
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from typing import AsyncIterable, AsyncIterator, Iterable, Iterator, List
18
+
19
+ from .sayilar import turkce_kucuk
20
+
21
+ #: Ardından nokta gelince cümleyi bitirmeyen kısaltmalar (küçük harfle).
22
+ KISALTMALAR = {
23
+ "dr", "prof", "doç", "av", "sn", "sayın", "no", "tel", "vb", "vs", "vd",
24
+ "bkz", "örn", "ltd", "şti", "mah", "cad", "sok", "apt", "bl", "kat",
25
+ }
26
+
27
+ _SONLANDIRICI = re.compile(r"[.!?…]+[\s\"'»)]*")
28
+ _KISALTMA_SONU = re.compile(r"([^\W_]+)\.\s*$")
29
+ _SAYI_SONU = re.compile(r"\d\.\s*$")
30
+
31
+ _PENCERE = 64
32
+
33
+ #: Bundan kısa cümleler tek başına gönderilmez, sonrakiyle birleşir ("Evet.").
34
+ MIN_UZUNLUK = 12
35
+
36
+
37
+ class CumleAyirici:
38
+ """Akış halinde cümle üretir.
39
+
40
+ LLM'den gelen token'ları ``push()`` ile besle, dönen listedeki cümleleri
41
+ hemen TTS kuyruğuna at. Akış bitince ``flush()`` çağır.
42
+ """
43
+
44
+ def __init__(self, min_uzunluk: int = MIN_UZUNLUK) -> None:
45
+ self.min_uzunluk = min_uzunluk
46
+ self._tampon = ""
47
+
48
+ def push(self, parca: str) -> List[str]:
49
+ self._tampon += parca
50
+ cikan: List[str] = []
51
+ ara = 0
52
+
53
+ while True:
54
+ eslesme = _SONLANDIRICI.search(self._tampon, ara)
55
+ if not eslesme:
56
+ break
57
+
58
+ son = eslesme.end()
59
+ aday = self._tampon[:son]
60
+ # Yalnızca adayın sonu incelenir; bütün tamponu her noktada baştan
61
+ # taramak uzun metinde işi ikinci dereceden yavaşlatır.
62
+ kuyruk = aday[-_PENCERE:]
63
+ kisaltma = _KISALTMA_SONU.search(kuyruk)
64
+ if kisaltma is not None and kisaltma.start() == 0 and aday[-_PENCERE - 1:-_PENCERE].isalnum():
65
+ kisaltma = None # pencereye sığmayan uzun kelime, kısaltma olamaz
66
+
67
+ bekle = (
68
+ (kisaltma is not None and turkce_kucuk(kisaltma.group(1)) in KISALTMALAR) # "Dr. Ayşe"
69
+ or _SAYI_SONU.search(kuyruk) is not None # "15.03.2026"
70
+ or len(aday.strip()) < self.min_uzunluk # "Evet."
71
+ )
72
+
73
+ if bekle:
74
+ ara = son # bu noktayı atla, bir sonrakine bak
75
+ continue
76
+
77
+ cikan.append(aday.strip())
78
+ self._tampon = self._tampon[son:]
79
+ ara = 0
80
+ return cikan
81
+
82
+ def flush(self) -> List[str]:
83
+ """Akış bittiğinde tamponda kalanı verir."""
84
+ kalan = self._tampon.strip()
85
+ self._tampon = ""
86
+ return [kalan] if kalan else []
87
+
88
+
89
+ def cumleleri_ayir(metin: str, min_uzunluk: int = MIN_UZUNLUK) -> List[str]:
90
+ """Tek parça metni cümlelere böler."""
91
+ ayirici = CumleAyirici(min_uzunluk)
92
+ return ayirici.push(metin) + ayirici.flush()
93
+
94
+
95
+ def cumle_akisi(akis: Iterable[str], min_uzunluk: int = MIN_UZUNLUK) -> Iterator[str]:
96
+ """Parça akışını (ör. LLM stream'i) cümle akışına çevirir."""
97
+ ayirici = CumleAyirici(min_uzunluk)
98
+ for parca in akis:
99
+ yield from ayirici.push(parca)
100
+ yield from ayirici.flush()
101
+
102
+
103
+ async def cumle_akisi_async(akis: AsyncIterable[str], min_uzunluk: int = MIN_UZUNLUK) -> AsyncIterator[str]:
104
+ """``cumle_akisi`` ile aynı, async akışlar için."""
105
+ ayirici = CumleAyirici(min_uzunluk)
106
+ async for parca in akis:
107
+ for cumle in ayirici.push(parca):
108
+ yield cumle
109
+ for cumle in ayirici.flush():
110
+ yield cumle
@@ -0,0 +1,78 @@
1
+ """
2
+ Satır içi duraklama işaretleri — SSML'in olmadığı yerde en yakın şey.
3
+
4
+ Birçok açık kaynak TTS modeli SSML desteklemez. Ama metni işaretlerden bölüp
5
+ parçaları ayrı üretir, aralarına sessizlik koyarsan duraklamayı kendin kurarsın::
6
+
7
+ Sorgu tamamlandı. [dur:0.8] Toplam üç yüz kırk hektar.
8
+
9
+ ``[dur]`` tek başına varsayılan duraklamadır.
10
+
11
+ Vurgu için ayrı bir işaret yok ve olmamalı: çoğu modelde vurgu parametresi
12
+ bulunmuyor, uydurulmuş bir işaret sessizce hiçbir şey yapardı.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass
18
+ from typing import List
19
+
20
+ #: ``[dur]`` işaretinin varsayılan uzunluğu, saniye.
21
+ VARSAYILAN_DURAKLAMA = 0.45
22
+
23
+ #: İşaretlerin izin verilen üst sınırı — 5 saniyeden uzun sessizlik hata gibi duyulur.
24
+ EN_UZUN_DURAKLAMA = 5.0
25
+
26
+ # Python'da derlenmiş desenin durumu yoktur (JavaScript'teki ``lastIndex``
27
+ # tuzağı burada yaşanmaz); tek desen güvenle paylaşılır.
28
+ _ISARET = re.compile(r"\[dur(?::\s*(\d+(?:[.,]\d+)?))?\]", re.IGNORECASE)
29
+
30
+
31
+ @dataclass
32
+ class Parca:
33
+ """Seslendirilecek bir parça."""
34
+
35
+ #: Seslendirilecek metin.
36
+ metin: str
37
+ #: Bu parçadan sonra bırakılacak sessizlik, saniye.
38
+ duraklama: float
39
+
40
+
41
+ def isaretleri_ayir(metin: str) -> List[Parca]:
42
+ """Metni işaretlerden böler.
43
+
44
+ Bilinmeyen bir işaret (``[vurgu]`` gibi) metinde bırakılır — model onu okur
45
+ ve sen hatayı duyarsın. Sessizce silmek, işaretin çalıştığını sanmana yol açardı.
46
+ """
47
+ parcalar: List[Parca] = []
48
+ son = 0
49
+
50
+ for e in _ISARET.finditer(metin):
51
+ oncesi = metin[son:e.start()].strip()
52
+ uzunluk = (
53
+ min(float(e.group(1).replace(",", ".")), EN_UZUN_DURAKLAMA)
54
+ if e.group(1) else VARSAYILAN_DURAKLAMA
55
+ )
56
+
57
+ if oncesi:
58
+ parcalar.append(Parca(oncesi, uzunluk))
59
+ elif parcalar:
60
+ # Art arda iki işaret: önceki duraklamayı uzat
61
+ parcalar[-1].duraklama += uzunluk
62
+ son = e.end()
63
+
64
+ kalan = metin[son:].strip()
65
+ if kalan:
66
+ parcalar.append(Parca(kalan, 0))
67
+
68
+ return parcalar if parcalar else [Parca(metin.strip(), 0)]
69
+
70
+
71
+ def isaret_var(metin: str) -> bool:
72
+ """Metinde işaret var mı — tek üretimle geçilebilecek mi diye bakmak için."""
73
+ return _ISARET.search(metin) is not None
74
+
75
+
76
+ def isaretleri_temizle(metin: str) -> str:
77
+ """İşaretleri temizler. Altyazı metni için — okuyucu ``[dur:0.8]`` görmemeli."""
78
+ return re.sub(r"\s{2,}", " ", _ISARET.sub(" ", metin)).strip()