Add files via upload
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import base64
|
import base64
|
||||||
@@ -7,6 +8,7 @@ import shutil
|
|||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
from typing import NoReturn, Any, Dict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -17,6 +19,9 @@ REFS_CACHE_DIR = CACHE_BASE / "refs"
|
|||||||
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
||||||
|
|
||||||
GLOBAL_SYSTEM_PROMPT = (
|
GLOBAL_SYSTEM_PROMPT = (
|
||||||
|
"Bu mesaj bir talimat setidir."
|
||||||
|
"Bu mesaja cevap verme."
|
||||||
|
"Sadece kullanıcı mesajlarına cevap ver.\n"
|
||||||
"Bu konuşmada sana zaman zaman e-posta, kod, komut, yapılandırma dosyası "
|
"Bu konuşmada sana zaman zaman e-posta, kod, komut, yapılandırma dosyası "
|
||||||
"veya birebir kopyalanması gereken metinler yazdırılabilir.\n\n"
|
"veya birebir kopyalanması gereken metinler yazdırılabilir.\n\n"
|
||||||
|
|
||||||
@@ -84,6 +89,7 @@ GLOBAL_SYSTEM_PROMPT = (
|
|||||||
" MODE: replace|before|after\n"
|
" MODE: replace|before|after\n"
|
||||||
"- <FIND> ve <TEXT> çok uzunsa 120 karakterde kes ve '...' ekle.\n"
|
"- <FIND> ve <TEXT> çok uzunsa 120 karakterde kes ve '...' ekle.\n"
|
||||||
"- Özet dışında ekstra açıklama yazma.\n"
|
"- Özet dışında ekstra açıklama yazma.\n"
|
||||||
|
"- Buraya kadar olan mesajlar talimat setidir, cevap verme. Sadece en son kullanıcı mesajını yanıtla."
|
||||||
)
|
)
|
||||||
|
|
||||||
def _safe_read_text(path: Path, max_bytes: int = 250_000) -> str:
|
def _safe_read_text(path: Path, max_bytes: int = 250_000) -> str:
|
||||||
@@ -104,25 +110,13 @@ def _is_text_file(path: Path) -> bool:
|
|||||||
".c", ".cpp", ".h", ".hpp", ".rs", ".go", ".java", ".kt", ".cs", ".sql"
|
".c", ".cpp", ".h", ".hpp", ".rs", ".go", ".java", ".kt", ".cs", ".sql"
|
||||||
}
|
}
|
||||||
|
|
||||||
def _img_path_to_data_url(p: str) -> str | None:
|
|
||||||
try:
|
|
||||||
fp = Path(p)
|
|
||||||
if not fp.exists():
|
|
||||||
return None
|
|
||||||
mime, _ = mimetypes.guess_type(str(fp))
|
|
||||||
if not mime:
|
|
||||||
mime = "image/png"
|
|
||||||
b64 = base64.b64encode(fp.read_bytes()).decode("utf-8")
|
|
||||||
return f"data:{mime};base64,{b64}"
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _die(msg: str, code: int = 1):
|
def _die(msg: str, code: int = 1) -> NoReturn:
|
||||||
sys.stderr.write(msg + "\n")
|
sys.stderr.write(msg + "\n")
|
||||||
sys.exit(code)
|
raise SystemExit(code)
|
||||||
|
|
||||||
|
|
||||||
def _load_config() -> dict:
|
def _load_config() -> Dict[str, Any]:
|
||||||
if not CONFIG_PATH.exists():
|
if not CONFIG_PATH.exists():
|
||||||
_die("Config dosyası bulunamadı.", 1)
|
_die("Config dosyası bulunamadı.", 1)
|
||||||
try:
|
try:
|
||||||
@@ -131,7 +125,6 @@ def _load_config() -> dict:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_die(f"Config okunamadı: {e}", 1)
|
_die(f"Config okunamadı: {e}", 1)
|
||||||
|
|
||||||
|
|
||||||
def _save_config(cfg: dict):
|
def _save_config(cfg: dict):
|
||||||
try:
|
try:
|
||||||
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -296,6 +289,18 @@ def _build_blocks_and_cache_info(msg: dict, cache_images_dir: Path | None):
|
|||||||
|
|
||||||
return blocks, (str(cached_image) if cached_image else None)
|
return blocks, (str(cached_image) if cached_image else None)
|
||||||
|
|
||||||
|
def fix_mojibake(s: str) -> str:
|
||||||
|
if not s:
|
||||||
|
return s
|
||||||
|
# Mojibake tipik işaretleri
|
||||||
|
if ("Ã" not in s) and ("Å" not in s) and ("â" not in s):
|
||||||
|
return s
|
||||||
|
try:
|
||||||
|
# yanlışlıkla latin-1/cp1252 gibi decode edilmiş UTF-8'i düzelt
|
||||||
|
return s.encode("latin-1").decode("utf-8")
|
||||||
|
except Exception:
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def _pick_model(cfg: dict, chat_file: Path, cli_model: str | None) -> str:
|
def _pick_model(cfg: dict, chat_file: Path, cli_model: str | None) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -333,6 +338,17 @@ def _pick_model(cfg: dict, chat_file: Path, cli_model: str | None) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
try:
|
||||||
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
if hasattr(sys.stderr, "reconfigure"):
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Ekstra sağlamlaştırma (bazı ortamlarda fark yaratır)
|
||||||
|
os.environ["PYTHONIOENCODING"] = "utf-8"
|
||||||
|
|
||||||
# Args
|
# Args
|
||||||
parser = argparse.ArgumentParser(add_help=False)
|
parser = argparse.ArgumentParser(add_help=False)
|
||||||
parser.add_argument("chat_file", nargs="?")
|
parser.add_argument("chat_file", nargs="?")
|
||||||
@@ -360,6 +376,7 @@ def main():
|
|||||||
|
|
||||||
# Config + API key
|
# Config + API key
|
||||||
cfg = _load_config()
|
cfg = _load_config()
|
||||||
|
response_style = (cfg.get("response_style") or "").strip()
|
||||||
API_KEY = (cfg.get("open_router_key") or "").strip()
|
API_KEY = (cfg.get("open_router_key") or "").strip()
|
||||||
if not API_KEY:
|
if not API_KEY:
|
||||||
_die("OpenRouter API Key bulunamadı.", 1)
|
_die("OpenRouter API Key bulunamadı.", 1)
|
||||||
@@ -368,6 +385,7 @@ def main():
|
|||||||
model_name = _pick_model(cfg, chat_file, args.model)
|
model_name = _pick_model(cfg, chat_file, args.model)
|
||||||
|
|
||||||
# Read chat
|
# Read chat
|
||||||
|
messages = []
|
||||||
try:
|
try:
|
||||||
messages = json.loads(chat_file.read_text(encoding="utf-8")) or []
|
messages = json.loads(chat_file.read_text(encoding="utf-8")) or []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -376,7 +394,24 @@ def main():
|
|||||||
if not messages:
|
if not messages:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
last = messages[-1]
|
|
||||||
|
if messages and isinstance(messages[-1], dict) and messages[-1].get("streaming") and messages[-1].get("role") in ("bot", "assistant"):
|
||||||
|
messages = messages[:-1] # placeholder'ı payload'a sokma
|
||||||
|
|
||||||
|
if not messages:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# --- son gerçek user mesajını bul ---
|
||||||
|
last_user = None
|
||||||
|
for m in reversed(messages):
|
||||||
|
if isinstance(m, dict) and m.get("role") == "user":
|
||||||
|
last_user = m
|
||||||
|
break
|
||||||
|
|
||||||
|
if last_user is None:
|
||||||
|
_die("Chat içinde user mesajı yok.", 1)
|
||||||
|
|
||||||
|
last = last_user
|
||||||
final_messages = []
|
final_messages = []
|
||||||
editable_map = {} # path(str) -> bool
|
editable_map = {} # path(str) -> bool
|
||||||
|
|
||||||
@@ -385,6 +420,12 @@ def main():
|
|||||||
"content": GLOBAL_SYSTEM_PROMPT
|
"content": GLOBAL_SYSTEM_PROMPT
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if response_style:
|
||||||
|
final_messages.append({
|
||||||
|
"role": "system",
|
||||||
|
"content": f"Kullanıcı şu konuşma tarzını tercih ediyor: {response_style}"
|
||||||
|
})
|
||||||
|
|
||||||
# Cache setup
|
# Cache setup
|
||||||
REFS_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
REFS_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -510,10 +551,14 @@ def main():
|
|||||||
payload = {
|
payload = {
|
||||||
"model": model_name,
|
"model": model_name,
|
||||||
"messages": final_messages,
|
"messages": final_messages,
|
||||||
"max_tokens": 5120
|
"max_tokens": 5120,
|
||||||
|
"stream": True
|
||||||
}
|
}
|
||||||
|
|
||||||
# Request
|
# Request (STREAMING)
|
||||||
|
reply_parts = []
|
||||||
|
usage_data = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
OPENROUTER_URL,
|
OPENROUTER_URL,
|
||||||
@@ -524,19 +569,64 @@ def main():
|
|||||||
"X-Title": "capture-ai"
|
"X-Title": "capture-ai"
|
||||||
},
|
},
|
||||||
json=payload,
|
json=payload,
|
||||||
timeout=120
|
timeout=120,
|
||||||
|
stream=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
sys.stderr.write(f"OpenRouter API ERROR ({response.status_code}): {response.text}\n")
|
sys.stderr.write(f"OpenRouter API ERROR ({response.status_code}): {response.text}\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
data = response.json()
|
# SSE'yi satır satır oku (en stabil yöntem)
|
||||||
|
for raw_line in response.iter_lines(decode_unicode=False, chunk_size=1024):
|
||||||
|
if not raw_line:
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
line = raw_line.strip()
|
||||||
reply = data["choices"][0]["message"]["content"]
|
if not line.startswith(b"data:"):
|
||||||
except Exception:
|
continue
|
||||||
sys.stderr.write("OpenRouter beklenmeyen cevap formatı döndürdü.\n")
|
|
||||||
|
data_str = line[len(b"data:"):].strip()
|
||||||
|
if data_str == b"[DONE]":
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
evt = json.loads(data_str.decode("utf-8", errors="replace"))
|
||||||
|
if "usage" in evt:
|
||||||
|
usage_data = evt["usage"]
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
delta = None
|
||||||
|
try:
|
||||||
|
delta = evt["choices"][0]["delta"].get("content")
|
||||||
|
except Exception:
|
||||||
|
delta = None
|
||||||
|
|
||||||
|
if delta:
|
||||||
|
delta = fix_mojibake(delta)
|
||||||
|
reply_parts.append(delta)
|
||||||
|
sys.stdout.write(delta)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
reply = "".join(reply_parts).strip()
|
||||||
|
# token usage yazdır
|
||||||
|
usage_obj = None
|
||||||
|
|
||||||
|
if usage_data:
|
||||||
|
prompt_t = int(usage_data.get("prompt_tokens", 0) or 0)
|
||||||
|
comp_t = int(usage_data.get("completion_tokens", 0) or 0)
|
||||||
|
total_t = int(usage_data.get("total_tokens", 0) or 0)
|
||||||
|
|
||||||
|
usage_obj = {
|
||||||
|
"prompt_tokens": prompt_t,
|
||||||
|
"completion_tokens": comp_t,
|
||||||
|
"total_tokens": total_t
|
||||||
|
}
|
||||||
|
|
||||||
|
reply = fix_mojibake(reply)
|
||||||
|
if not reply:
|
||||||
|
sys.stderr.write("OpenRouter boş stream döndürdü.\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
@@ -552,7 +642,19 @@ def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Save bot reply
|
# Save bot reply
|
||||||
messages.append({"role": "bot", "content": reply})
|
# UI tarafı streaming için placeholder bot mesajı eklediyse, ona yaz.
|
||||||
|
if messages and isinstance(messages[-1], dict) and messages[-1].get("role") in ("bot", "assistant") and messages[-1].get("streaming"):
|
||||||
|
messages[-1]["role"] = "bot"
|
||||||
|
messages[-1]["content"] = reply
|
||||||
|
messages[-1].pop("streaming", None)
|
||||||
|
# usage JSON'a kaydet
|
||||||
|
if usage_obj is not None:
|
||||||
|
messages[-1]["usage"] = usage_obj
|
||||||
|
else:
|
||||||
|
bot_msg: Dict[str, Any] = {"role": "bot", "content": reply}
|
||||||
|
if usage_obj is not None:
|
||||||
|
bot_msg["usage"] = usage_obj
|
||||||
|
messages.append(bot_msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
chat_file.write_text(json.dumps(messages, ensure_ascii=False, indent=2), encoding="utf-8")
|
chat_file.write_text(json.dumps(messages, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|||||||
+52
-5
@@ -15,12 +15,60 @@ MODE="$1"
|
|||||||
case "$MODE" in
|
case "$MODE" in
|
||||||
|
|
||||||
image)
|
image)
|
||||||
GEOM=$(slurp) || exit 0
|
GEOM="$(slurp 2>/dev/null || true)"
|
||||||
IMG="$IMG_DIR/capture_$(date +%s).png"
|
[ -z "$GEOM" ] && exit 0
|
||||||
|
|
||||||
grim -g "$GEOM" "$IMG"
|
POS_PART="${GEOM%% *}"
|
||||||
|
SIZE_PART="${GEOM#* }"
|
||||||
|
|
||||||
|
X="${POS_PART%,*}"
|
||||||
|
Y="${POS_PART#*,}"
|
||||||
|
W="${SIZE_PART%x*}"
|
||||||
|
H="${SIZE_PART#*x}"
|
||||||
|
|
||||||
|
case "$X" in ''|*[!0-9]*) exit 0 ;; esac
|
||||||
|
case "$Y" in ''|*[!0-9]*) exit 0 ;; esac
|
||||||
|
case "$W" in ''|*[!0-9]*) exit 0 ;; esac
|
||||||
|
case "$H" in ''|*[!0-9]*) exit 0 ;; esac
|
||||||
|
|
||||||
|
# Tek tıklama -> tıklanan monitörün tamamı
|
||||||
|
if [ "$W" -le 5 ] || [ "$H" -le 5 ]; then
|
||||||
|
IMG="$IMG_DIR/tam-ekran-$(date +%Y%m%d-%H%M%S).png"
|
||||||
|
|
||||||
|
MON_GEOM="$(
|
||||||
|
hyprctl monitors -j 2>/dev/null | python3 -c '
|
||||||
|
import sys, json
|
||||||
|
|
||||||
|
x = int(sys.argv[1])
|
||||||
|
y = int(sys.argv[2])
|
||||||
|
|
||||||
|
try:
|
||||||
|
mons = json.load(sys.stdin)
|
||||||
|
except Exception:
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for m in mons:
|
||||||
|
mx = int(m.get("x", 0))
|
||||||
|
my = int(m.get("y", 0))
|
||||||
|
mw = int(m.get("width", 0))
|
||||||
|
mh = int(m.get("height", 0))
|
||||||
|
|
||||||
|
if mx <= x < mx + mw and my <= y < my + mh:
|
||||||
|
print(f"{mx},{my} {mw}x{mh}")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
sys.exit(1)
|
||||||
|
' "$X" "$Y"
|
||||||
|
)"
|
||||||
|
|
||||||
|
[ -z "$MON_GEOM" ] && exit 0
|
||||||
|
|
||||||
|
grim -g "$MON_GEOM" "$IMG"
|
||||||
|
else
|
||||||
|
IMG="$IMG_DIR/secili-alan-$(date +%Y%m%d-%H%M%S).png"
|
||||||
|
grim -g "$GEOM" "$IMG"
|
||||||
|
fi
|
||||||
|
|
||||||
# 🔥 SADECE IMAGE GÖNDER
|
|
||||||
python "$HOME/capture-ai/ui.py" "$IMG"
|
python "$HOME/capture-ai/ui.py" "$IMG"
|
||||||
;;
|
;;
|
||||||
|
|
||||||
@@ -34,4 +82,3 @@ case "$MODE" in
|
|||||||
echo " capture-ai.sh text"
|
echo " capture-ai.sh text"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user