"""Sesle ya da yaziyla "modeli sunu yap" komutu ve sohbet basina model yonlendirmesi (10 Ekim 2026). Olcum (sesli sohbet 01a12780): telefonda secilen model bilgisayara yalnizca YENI TUR baslatan yazili mesajla ulasiyor. Calisan tura eklenen mesaj (nokta dahil) secimi tasimiyor; dort denemenin ucunde secim kayboldu. Codex'in pencereler arasi kanali da sohbet bir pencerede acik degilse istegi kabul etmiyor. Bu yuzden degisiklik koprude yapilir: kullanici "modeli gemini flash low yap" der (sesli asistan bunu her soz gibi ana modele iletir), kopru cumleyi tanir, o sohbetin isteklerini secilen modele yonlendirir ve cevabi kendisi verir. - Model bostayken: kopru yerel cevap verir ("Sohbetin modeli artik ..."). - Model calisirken (tur ortasi): komut mesaji kisa bir devir notuna cevrilir ve AYNI tur yeni modelle surer; yarim kalan is kesilmez, yeni model kaldigi yerden devam eder. - Kullanici sonradan listeden baska model secerse (Codex'in gonderdigi ad degisir) yonlendirme kendiliginden kalkar. Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday varsa degisiklik yapilmaz, adaylar sorulur. """ import json import os import collections import re import threading import time DOSYA = "model-yonlendirme.json" OMUR_SN = 7 * 24 * 3600 YENILE_SN = 24 * 3600 EN_COK_KAYIT = 200 EN_COK_KELIME = 14 _KILIT = threading.Lock() _TR = str.maketrans("çğıöşüâîû", "cgiosuaiu") # Yalniz olumlu emir/istek bicimleri: "yapma", "gecme", "yapay", "secili" gibi sozler komut fiili degildir. FIILLER = {"gec", "gecir", "gecelim", "gecirelim", "gecer", "gecirir", "gecsene", "degis", "degistir", "degistirelim", "degistirir", "yap", "yapalim", "yapar", "yapsana", "sec", "secelim", "secer", "kullan", "kullanalim", "cevir", "cevirelim", "olsun", "ayarla", "ayarlayalim"} OLUMSUZ = ("yapma", "gecme", "gecirme", "degistirme", "secme", "kullanma", "cevirme", "olmasin", "ayarlama", "degisme") # "Hangi model daha ucuz?" genel sorudur; yalniz bu sohbetin modelini soran soz kopruden cevaplanir. SORU_ISARETI = {"secili", "aktif", "anki", "suanki", "sohbet", "sohbetin", "sohbette", "chat", "chatte", "chatin", "ana", "modelsin", "modeldesin", "modeldeyiz", "modeldeyim", "kullaniyorsun", "kullaniliyor", "calisiyor"} SORU = ("hangi", "hangisi", "kim", "kimdir") ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir", "olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi", "bana", "icin", "de", "da", "ve", "mi", "misin", "musun", "mu", "yeni", "diger", "baska", "olan", "codex", "ya", "e", "a", "i", "u", "yi", "ye", "na", "ne", "nu", "seni", "sen", "biz", "onu", "ona", "nokta"} # Yaziyla soylenen surum rakamlari ("opus bes bes", "uc nokta sekiz"). "bir" belirtec de olabildigi icin # ("bir model sec") yalniz baska bir rakamin ya da "nokta"nin yaninda rakam sayilir. SAYI = {"sifir": "0", "bir": "1", "iki": "2", "uc": "3", "dort": "4", "bes": "5", "alti": "6", "yedi": "7", "sekiz": "8", "dokuz": "9"} # Komutun ardindan gelen ve is tasimayan ek ("... yap ve devam et"): komutu dusurmez, atilir. DEVAM_EKI = {"ve", "sonra", "devam", "et", "edelim", "edin", "lutfen"} # Konusmadan yaziya ceviride sik gorulen yazimlar -> model adindaki parca TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus", "cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash", "kuven": "qwen", "kiven": "qwen", "kuen": "qwen", "kven": "qwen", "kodeks": "codex", "dusuk": "low", "yuksek": "high", "orta": "medium", "sonet": "sonnet", "haiku": "haiku", "sansursuz": "uncensored", "ansansird": "uncensored", "dusunen": "thinking", "dusunmeli": "thinking", "ciptii": "gpt", "cipiti": "gpt", "gipiti": "gpt"} # Listede kisa yazilan saglayici -> konusurken soylenen ad SAGLAYICI = {"cc": ("claude",), "agy": ("gemini", "antigravity"), "ag": ("gemini", "antigravity"), "cx": ("codex", "gpt")} def duz(metin): return str(metin or "").lower().translate(_TR) def parcala(metin): """Harf ve rakam obeklerine ayirir: "agy/gemini-3.8-flash-low" -> agy gemini 3 8 flash low.""" return [TAKMA.get(p, p) for p in re.findall(r"[a-z]+|\d+", duz(metin))] def son_sira(girdi): """Son kullanici mesajinin sirasi. Arkasindaki talimat (developer/system) mesajlari atlanir: kancalar kullanici mesajinin ardina baglam ekler (olcum 10 Ekim 2026). Baska bir oge varsa -1.""" if not isinstance(girdi, list): return -1 for i in range(len(girdi) - 1, -1, -1): oge = girdi[i] if isinstance(oge, dict) and oge.get("type", "message") == "message": if oge.get("role") == "user": return i if oge.get("role") in ("developer", "system"): continue return -1 return -1 def son_kullanici_metni(req): """(metin, sira, sesli). Son girdi kullanici metin mesaji degilse (None, -1, False). Sesli aktarimda () yalnizca icindeki soz alinir.""" girdi = req.get("input") sira = son_sira(girdi) if sira < 0: return None, -1, False son = girdi[sira] icerik = son.get("content") if isinstance(icerik, str): metin = icerik elif isinstance(icerik, list) and icerik and all(isinstance(p, dict) and p.get("type") in ("input_text", "text") for p in icerik): metin = "".join(str(p.get("text") or "") for p in icerik) else: return None, -1, False m = re.search(r".*?(.*?)", metin, re.S) if m: return m.group(1).strip(), sira, True if "<" in metin and ">" in metin: return None, -1, False # baska bir sarmalli sistem mesaji; komut aranmaz return metin.strip(), sira, False def _sayilari_cevir(kelimeler): cikti = [] for i, k in enumerate(kelimeler): if k in SAYI: komsu = [kelimeler[j] for j in (i - 1, i + 1) if 0 <= j < len(kelimeler)] if k != "bir" or any(x == "nokta" or x.isdigit() or (x in SAYI and x != "bir") for x in komsu): cikti.append(SAYI[k]) continue cikti.append(k) return cikti DOLGU = {"evet", "tamam", "hadi", "peki", "tabii", "hayir", "yani", "sey", "ee", "eee", "hmm", "olur", "simdi"} def _harfleri_birlestir(kelimeler): """Harf harf soylenen ad tek sozcuk olur: "s e e d iki nokta bir turbo" -> "seed iki nokta bir turbo".""" cikti, harfler = [], [] for k in kelimeler + [""]: if len(k) == 1 and k.isalpha(): harfler.append(k) continue if len(harfler) >= 3: cikti.append("".join(harfler)) else: cikti.extend(harfler) harfler = [] if k: cikti.append(k) # Ad hem heceleyerek hem duz soylendiyse ("seed s e e d ...") tekrar atilir. return [k for i, k in enumerate(cikti) if i == 0 or k != cikti[i - 1]] def komut_coz(metin): """None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}.""" kelimeler = _harfleri_birlestir(re.findall(r"[a-z0-9]+", duz(metin))) if not kelimeler or len(kelimeler) > EN_COK_KELIME: return None if not any(k.startswith("model") for k in kelimeler): # "modeli" denmeden soylenen komut ("Seed iki nokta bir turbo yap", "gemini flash low'a gec"): yalniz cumle # bir komut fiiliyle bitiyorsa ve ad bir modele TAM uyuyorsa komut sayilir (kesin=False: cagiran, ad # bulunamazsa mesaji komut saymaz, modele birakir). if any(k.startswith(OLUMSUZ) for k in kelimeler) or kelimeler[-1] not in FIILLER or len(kelimeler) > 9: return None kelimeler = _sayilari_cevir(kelimeler) ad = [TAKMA.get(k, k) for k in kelimeler[:-1] if k not in ATLA and k not in DOLGU] ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)] return {"tur": "gec", "ad": ad, "kesin": False} if len(ad) >= 2 else None if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler): return {"tur": "sor"} if any(k in SORU_ISARETI for k in kelimeler) else None if any(k.startswith(OLUMSUZ) for k in kelimeler): return None fiil = [k for k in kelimeler if k in FIILLER] if fiil: son_fiil = max(i for i, k in enumerate(kelimeler) if k in FIILLER) if all(k in DEVAM_EKI for k in kelimeler[son_fiil + 1:]): kelimeler = kelimeler[:son_fiil + 1] kelimeler = _sayilari_cevir(kelimeler) ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil and k not in DOLGU] ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)] # Ad soylenmemis ("modeli değiştir", "başka modele geç"): degisiklik yok, nasil soylenecegi anlatilir. return {"tur": "gec", "ad": ad, "kesin": True} if ad else {"tur": "nasil"} return None def _uyar(sorgu, parca): """Turkce ek almis soz ("geminiye", "opusa") model parcasiyla baslar.""" if sorgu == parca: return True if len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit(): return True # Yarim soylenen ya da yarim yaziya dokulen soz adin basidir: "think" -> thinking, "obli" -> obliterated # (olcum 11 Ekim 2026: "qwen 3.8 27b obli think" uc kez bulunamadi). return len(sorgu) >= 4 and sorgu.isalpha() and parca.startswith(sorgu) def eslestir(ad, modeller): """ad: sorgu parcalari; modeller: [(slug, gorunen ad)]. Doner: {"slug", "ad"} (tek aday) | {"adaylar": [gorunen adlar]} (belirsiz) | None (ad bir modele benzemiyor).""" if not ad: return None puanli = [] for slug, gorunen in modeller: parcalar = parcala(gorunen) or parcala(slug) kume = set(parcalar) | set(parcala(slug)) for kisa, uzunlar in SAGLAYICI.items(): if kisa in kume: kume.update(uzunlar) tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume)) harf = sum(1 for s in ad if not s.isdigit() and any(_uyar(s, p) for p in kume)) if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz kendi = [p for p in parcalar if p not in SAGLAYICI and p not in ("nano", "ide", "9router")] hepsi = len(kendi) if kendi and all(any(_uyar(s, p) for s in ad) for p in kendi) else 0 puanli.append((tutan, -len(parcalar), slug, gorunen, harf, hepsi)) if not puanli: return None en = max(p[0] for p in puanli) harfler = [s for s in ad if not s.isdigit()] en_harf = max(p[4] for p in puanli) if en_harf < len(harfler): return None # adin bir sozcugu hicbir modelde yok: komut degil iyi = sorted((p for p in puanli if p[0] == en and p[4] == len(harfler)), key=lambda p: (-p[1], p[3])) if not iyi: return None if en < len(ad): # sozcukler tuttu ama surum rakami uymadi: sor, tahmin etme return {"adaylar": [p[3] for p in iyi[:4]]} if len(iyi) == 1: return {"slug": iyi[0][2], "ad": iyi[0][3]} tam = [p for p in iyi if p[5]] # adinin butun parcalari soylenen model if tam: # Ayni adi tasiyanlar arasinda sira: Codex'in kendi abonelik modeli, sonra adi en uzun (en ozgul) olan, # sonra saglayici/model bicimindeki tekil model. Hala esitse sorulur. def oncelik(p): return (p[2].startswith("ide/"), -p[5], "/" not in p[3]) tam.sort(key=oncelik) if len(tam) == 1 or oncelik(tam[0]) != oncelik(tam[1]): return {"slug": tam[0][2], "ad": tam[0][3]} return {"adaylar": [p[3] for p in tam[:4]]} # Adaylardan biri digerlerinin "fazladan sozcuklu" hali degilse sorulur. Biri digerlerinin govdesiyse # ("...-obliterated" ile "...-obliterated:thinking") soylenmeyen eki tasimayan secilir: kullanici "thinking" # demediyse dusunmeyeni istiyordur (olcum 11 Ekim 2026). Fark surum rakamiysa tahmin edilmez, sorulur. sayim = [(collections.Counter(parcala(p[3]) or parcala(p[2])), p) for p in iyi] sayim.sort(key=lambda c: sum(c[0].values())) govde, secilen = sayim[0] if all(not (govde - c) and (c - govde) and not any(k.isdigit() for k in (c - govde)) for c, _ in sayim[1:]): return {"slug": secilen[2], "ad": secilen[3]} return {"adaylar": [p[3] for p in iyi[:4]]} _ARAC = {"function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "reasoning", "local_shell_call", "local_shell_call_output", "tool_search_call", "tool_search_output"} def tur_ortasi(req, sira): """Mesaj calisan bir turun icine mi dustu? Geriye dogru, kullanici/talimat mesajlarini atlayarak bakilir: once arac ogesi ya da ara (commentary) mesaj gelirse tur suruyordur; once son cevap gelirse tur yeni baslamistir.""" girdi = req.get("input") or [] for oge in reversed(girdi[:max(sira, 0)]): if not isinstance(oge, dict): continue tur = oge.get("type", "message") if tur in _ARAC: return True if tur == "message": if oge.get("role") == "assistant": return oge.get("phase") == "commentary" continue return False def metni_degistir(req, sira, yeni): """sira'daki kullanici mesajinin metnini yeni ile degistirir (sesli aktarimda yalnizca icini).""" oge = req["input"][sira] icerik = oge.get("content") eski = icerik if isinstance(icerik, str) else "".join(str(p.get("text") or "") for p in icerik) if "" in eski: metin = re.sub(r"().*?()", lambda m: m.group(1) + yeni + m.group(2), eski, count=1, flags=re.S) else: metin = yeni oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}] def gecmisi_temizle(req, modeller, etkin=None): """Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki model ayni cumleyi yeniden gorup "modeli ben degistiremem" diye cevap veriyor (olcum 11 Ekim 2026). etkin: sohbetin su an gercekten kullandigi model. Verilirse, en son kullanici mesajindaki komut (ardindan arac ciktisi ya da sikistirma tetigi geldigi icin "gecmis" gorunen) yalniz bu modele gecirdiyse nota cevrilir; uygulanmamis komut ("baglanti kapali" denen gibi) "degistirildi" diye anlatilmaz. Doner: degistirilen mesaj sayisi.""" girdi = req.get("input") if not isinstance(girdi, list): return 0 son = son_sira(girdi) son_kullanici = max((i for i, oge in enumerate(girdi) if isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"), default=-1) n = 0 for i, oge in enumerate(girdi): if i == son or not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"): continue metin, _, _ = son_kullanici_metni({"input": [oge]}) if not metin or "model" not in duz(metin) or metin.startswith("[Köprü notu"): continue komut = komut_coz(metin) sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None if sonuc and "slug" in sonuc: if etkin is not None and i == son_kullanici and sonuc["slug"] != etkin: continue metni_degistir(req, i, f"[Köprü notu: kullanıcı burada sohbetin modelini {sonuc['ad']} olarak değiştirdi; " "değişiklik köprü tarafından yapıldı, senin bir şey yapman gerekmiyor.]") n += 1 return n BILGI_ISARETI = "[Model değiştirme]" BILGI = (BILGI_ISARETI + " Bu sohbetin modeli söylenerek değiştirilebilir: kullanıcı \"modeli yap\" dediğinde " "(örnek: \"modeli gemini flash low yap\", \"modeli opus beş beş yap\") değişikliği aradaki köprü yapar; senin bir " "araç çağırman ya da ayar değiştirmen gerekmez. Kullanıcı modeli değiştirip değiştiremeyeceğini sorarsa " "\"değiştiremem\" deme; bu cümleyi söylemesinin yeterli olduğunu kısaca anlat. Hangi modelde olduğunu öğrenmek " "için \"seçili model hangisi\" demesi yeter. Codex'in kendi modellerine (listenin başındaki abonelik modelleri) " "sesle geçilmez: kullanıcı onlardan birine geçmek isterse ekrandaki model seçiciden seçip tek başına \".\" " "yazması gerektiğini söyle.") NASIL = ("Hangi modele geçeyim? Adını söylemen yeterli, örneğin: \"modeli gemini flash low yap\" ya da " "\"modeli opus beş beş yap\". Şu anki modeli öğrenmek için \"seçili model hangisi\" de. Codex'in kendi " "modellerine sesle geçilmez; onlar için ekrandaki model seçiciden seçip tek başına \".\" yaz.") def bilgi_ekle(req): """Modele, sohbetin modelinin soylenerek degistirilebildigini bildirir (olcum 11 Ekim 2026: Opus "bu sohbetin modelini buradan değiştiremem" dedi). Istegin basina tek developer mesaji; zaten varsa eklenmez. True: eklendi.""" girdi = req.get("input") if not isinstance(girdi, list) or not girdi: return False for oge in girdi[:4]: icerik = oge.get("content") if isinstance(oge, dict) else None yazi = icerik if isinstance(icerik, str) else " ".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict)) if BILGI_ISARETI in yazi: return False girdi.insert(0, {"type": "message", "role": "developer", "content": [{"type": "input_text", "text": BILGI}]}) return True def devir_notu(yeni_ad, eski_ad): return (f"[Köprü notu: kullanıcı bu sohbetin modelini değiştirdi. Önceki model {eski_ad} idi; şu andan itibaren " f"cevap veren model sensin ({yeni_ad}). Yarım kalan işi kaldığı yerden sürdür, baştan başlama. " f"Cevabının ilk cümlesinde kısaca \"{yeni_ad} olarak devam ediyorum\" de.]") class Yonlendirme: """sohbet anahtari -> {"gorulen": Codex'in gonderdigi ad, "hedef": yonlenen ad, "zaman"}; diske yazilir.""" def __init__(self, home): self.yol = os.path.join(home, DOSYA) if home else None self.veri = {} if self.yol: try: with open(self.yol, encoding="utf-8") as f: okunan = json.load(f) if isinstance(okunan, dict): self.veri = {k: v for k, v in okunan.items() if isinstance(v, dict) and v.get("hedef")} except (OSError, ValueError): pass def _yaz(self): if not self.yol: return try: gecici = self.yol + ".tmp" with open(gecici, "w", encoding="utf-8") as f: json.dump(self.veri, f, ensure_ascii=False) os.replace(gecici, self.yol) except OSError: pass def al(self, sohbet, gelen, simdi=None): """Bu sohbet icin yonlenen ad; yoksa None. Codex baska bir ad gonderiyorsa (kullanici listeden yeni model secti) ya da kayit eskidiyse yonlendirme silinir.""" simdi = simdi or time.time() with _KILIT: k = self.veri.get(sohbet) if not k: return None if k.get("gorulen") != gelen or simdi - float(k.get("zaman") or 0) > OMUR_SN: del self.veri[sohbet] self._yaz() return None if simdi - float(k.get("zaman") or 0) > YENILE_SN: # Kullanilan yonlendirme eskimez: omur son kullanimdan sayilir (gunde en cok bir yazma). k["zaman"] = simdi self._yaz() return k["hedef"] def koy(self, sohbet, gelen, hedef, simdi=None): simdi = simdi or time.time() with _KILIT: if hedef == gelen: self.veri.pop(sohbet, None) else: self.veri[sohbet] = {"gorulen": gelen, "hedef": hedef, "zaman": simdi} if len(self.veri) > EN_COK_KAYIT: for eski in sorted(self.veri, key=lambda a: self.veri[a].get("zaman") or 0)[:-EN_COK_KAYIT]: del self.veri[eski] self._yaz()