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.
- turkce_normalize/__init__.py +48 -0
- turkce_normalize/__main__.py +45 -0
- turkce_normalize/cografi.py +153 -0
- turkce_normalize/cumle.py +110 -0
- turkce_normalize/isaretler.py +78 -0
- turkce_normalize/normalize.py +633 -0
- turkce_normalize/sayilar.py +175 -0
- turkce_normalize/telaffuz.py +127 -0
- turkce_normalize-0.1.0.dist-info/METADATA +333 -0
- turkce_normalize-0.1.0.dist-info/RECORD +14 -0
- turkce_normalize-0.1.0.dist-info/WHEEL +5 -0
- turkce_normalize-0.1.0.dist-info/entry_points.txt +2 -0
- turkce_normalize-0.1.0.dist-info/licenses/LICENSE +21 -0
- turkce_normalize-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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()
|