-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathcodec_textassist.py
More file actions
executable file
·186 lines (178 loc) · 9.26 KB
/
codec_textassist.py
File metadata and controls
executable file
·186 lines (178 loc) · 9.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python3.13
"""CODEC Text Assistant — mode passed as argument, no popup"""
import sys, os, json, requests, subprocess, re, time
MODE = sys.argv[1] if len(sys.argv) > 1 else "proofread"
def get_config():
try:
with open(os.path.expanduser("~/.codec/config.json")) as f: return json.load(f)
except: return {}
def call_qwen(text, mode):
cfg = get_config()
base = cfg.get("llm_base_url", "http://localhost:8081/v1")
model = cfg.get("llm_model", "mlx-community/Qwen3.5-35B-A3B-4bit")
kwargs = cfg.get("llm_kwargs", {})
prompts = {
"proofread": "Fix all spelling, grammar, and punctuation errors. Keep same tone. Output ONLY corrected text.",
"elevate": "Rewrite to be more polished and professional. Keep same meaning. Output ONLY improved text.",
"explain": "Explain this text simply and concisely. What is it about? Key points?",
"read_aloud": "READ_ALOUD_MODE",
"save": "SAVE_TO_KEEP_MODE",
"reply": "You are a smart, natural communicator. The user will give you a message they received, possibly followed by a colon : and their reply direction. If there is a colon with instructions after it, follow those instructions to craft the reply. If there is no colon, write a natural reply matching the tone. Keep it short (1-3 sentences). Output ONLY the reply text. No quotes, no labels, no explanation.",
"translate": "You are a translator. Translate the following text into English. No matter what language the input is — Ukrainian, Spanish, French, Russian, Chinese, Arabic, anything — always translate to English. Output ONLY the translated English text, nothing else.",
"prompt": "You are a prompt engineer. Rewrite the following text to be a clear, optimized prompt for an AI language model. Make it specific, structured, and effective. Remove ambiguity, add context where helpful, and ensure the intent is crystal clear. Output ONLY the optimized prompt, nothing else."
}
payload = {"model": model, "messages": [
{"role": "system", "content": prompts.get(mode, prompts["proofread"])},
{"role": "user", "content": text}
], "max_tokens": 4000, "temperature": 0.3, "stream": False,
"chat_template_kwargs": {"enable_thinking": False}}
payload.update(kwargs)
r = requests.post(f"{base}/chat/completions", json=payload, timeout=60)
result = r.json()["choices"][0]["message"]["content"].strip()
result = re.sub(r'<think>[\s\S]*?</think>', '', result).strip()
return re.sub(r'###\s*FINAL ANSWER:\s*', '', result).strip()
def overlay(text, color, duration):
env = os.environ.copy()
env["_OVERLAY_TEXT"] = text
env["_OVERLAY_COLOR"] = color
env["_OVERLAY_DURATION"] = str(duration)
return subprocess.Popen([sys.executable, "-c", """import tkinter as tk, os
t=os.environ['_OVERLAY_TEXT'];c=os.environ['_OVERLAY_COLOR'];d=int(os.environ['_OVERLAY_DURATION'])
r=tk.Tk();r.overrideredirect(True);r.attributes('-topmost',True);r.attributes('-alpha',0.95);r.configure(bg='#0a0a0a')
sw=r.winfo_screenwidth();sh=r.winfo_screenheight()
w,h=520,90
r.geometry(f'{w}x{h}+{(sw-w)//2}+{sh-130}')
cv=tk.Canvas(r,bg='#0a0a0a',highlightthickness=0,width=w,height=h);cv.pack()
cv.create_rectangle(1,1,w-1,h-1,outline=c,width=1)
cv.create_text(w//2,h//2,text=t,fill=c,font=('Helvetica',16,'bold'))
r.after(d,r.destroy);r.mainloop()"""], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=env)
text = subprocess.run(["pbpaste"], capture_output=True, text=True).stdout.strip()
if not text: sys.exit(0)
# ── Read Aloud: speak via Kokoro TTS, no LLM needed ──────────────────────────
if MODE == "read_aloud":
tts_text = text[:2000]
cfg = get_config()
tts_url = cfg.get("tts_url", "http://localhost:8085/v1/audio/speech")
tts_model = cfg.get("tts_model", "mlx-community/Kokoro-82M-bf16")
tts_voice = cfg.get("tts_voice", "am_adam")
overlay("\U0001f50a Reading aloud...", "#E8711A", 6000)
try:
import tempfile
r = requests.post(tts_url, json={
"model": tts_model, "input": tts_text, "voice": tts_voice
}, timeout=30)
if r.status_code == 200:
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
f.write(r.content)
mp3_path = f.name
subprocess.run(["afplay", mp3_path])
os.unlink(mp3_path)
else:
overlay("\u26a0 TTS unavailable", "#ff3333", 3000)
except Exception as e:
overlay("\u26a0 TTS error", "#ff3333", 3000)
print(f"TTS error: {e}")
sys.exit(0)
# ── Save: save to Apple Notes (primary) + local file backup ─────────────────
if MODE == "save":
save_text = text[:2000]
safe = save_text.replace('"', '\\"').replace("'", "")
# Save to Apple Notes
try:
subprocess.run(["osascript", "-e",
f'tell application "Notes" to make new note at folder "Notes" with properties {{body:"{safe}"}}'],
capture_output=True, text=True, timeout=10)
except Exception:
pass
# Also save local backup
notes_path = os.path.expanduser("~/.codec/saved_notes.txt")
from datetime import datetime
with open(notes_path, "a") as nf:
nf.write(f"\n--- {datetime.now().strftime('%Y-%m-%d %H:%M')} ---\n")
nf.write(save_text + "\n")
subprocess.run(["osascript", "-e",
'display notification "Saved to Apple Notes" with title "CODEC Save"'],
capture_output=True)
overlay("\u2705 Saved to Apple Notes!", "#44cc66", 2000)
sys.exit(0)
_proc_overlay = overlay("⚡ Processing...", "#00aaff", 15000)
try:
result = call_qwen(text, MODE)
# Kill processing overlay now that we have the result
if _proc_overlay:
try: _proc_overlay.terminate()
except: pass
if MODE in ("explain", "translate"):
# Show result in a styled floating window (no Terminal)
title = "CODEC Explain" if MODE == "explain" else "CODEC Translate"
# Also copy to clipboard so user can paste if needed
subprocess.run(["pbcopy"], input=result.encode(), check=True)
# Speak the result via Kokoro TTS — spawn as subprocess so it survives parent exit
cfg = get_config()
_tts_env = {**os.environ,
"_TTS_URL": cfg.get("tts_url", "http://localhost:8085/v1/audio/speech"),
"_TTS_MODEL": cfg.get("tts_model", "mlx-community/Kokoro-82M-bf16"),
"_TTS_VOICE": cfg.get("tts_voice", "am_adam"),
"_TTS_TEXT": result[:1500]}
subprocess.Popen([sys.executable, "-c", """
import requests, tempfile, subprocess, os
try:
r = requests.post(os.environ['_TTS_URL'], json={
"model": os.environ['_TTS_MODEL'],
"input": os.environ['_TTS_TEXT'],
"voice": os.environ['_TTS_VOICE']
}, timeout=30)
if r.status_code == 200:
f = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
f.write(r.content); f.close()
subprocess.run(["afplay", f.name])
os.unlink(f.name)
except Exception:
pass
"""], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=_tts_env)
# Launch a clean floating result window
safe_result = result.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"').replace("\n", "\\n")
subprocess.Popen([sys.executable, "-c", f"""import tkinter as tk
from tkinter import font as tkfont
r=tk.Tk()
r.title('{title}')
r.attributes('-topmost', True)
r.configure(bg='#1a1a1a')
sw=r.winfo_screenwidth();sh=r.winfo_screenheight()
w,h=560,400
r.geometry(f'{{w}}x{{h}}+{{(sw-w)//2}}+{{(sh-h)//2}}')
r.minsize(400,250)
# Title bar
hdr=tk.Frame(r,bg='#E8711A',height=36);hdr.pack(fill='x')
hdr.pack_propagate(False)
tk.Label(hdr,text='{title}',fg='white',bg='#E8711A',font=('Helvetica',14,'bold')).pack(side='left',padx=12)
tk.Button(hdr,text='📋 Copy',fg='#1a1a1a',bg='white',relief='flat',font=('Helvetica',11,'bold'),padx=8,
command=lambda:[r.clipboard_clear(),r.clipboard_append(txt.get('1.0','end-1c'))]).pack(side='right',padx=6,pady=4)
# Text area
txt=tk.Text(r,wrap='word',bg='#1a1a1a',fg='#e0e0e0',font=('Menlo',13),relief='flat',
padx=16,pady=12,insertbackground='#E8711A',selectbackground='#E8711A',borderwidth=0)
txt.pack(fill='both',expand=True)
txt.insert('1.0','{safe_result}')
txt.config(state='normal')
# Footer
ft=tk.Frame(r,bg='#111',height=32);ft.pack(fill='x')
ft.pack_propagate(False)
tk.Label(ft,text='Copied to clipboard \u00b7 \u2318V to paste',fg='#666',bg='#111',font=('Helvetica',10)).pack(side='left',padx=12)
tk.Button(ft,text='Close',fg='#999',bg='#222',relief='flat',font=('Helvetica',11),padx=10,
command=r.destroy).pack(side='right',padx=8,pady=3)
r.bind('<Escape>',lambda e:r.destroy())
r.mainloop()
"""], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
overlay("\u2705 {title}", "#44cc66", 2000)
else:
subprocess.run(["pbcopy"], input=result.encode(), check=True)
time.sleep(0.3)
subprocess.run(["osascript", "-e",
'tell application "System Events" to keystroke "v" using command down'],
capture_output=True, timeout=5)
overlay("✅ Text replaced!", "#44cc66", 2000)
except Exception:
if _proc_overlay:
try: _proc_overlay.terminate()
except: pass
overlay("Error - check terminal", "#ff3333", 3000)