Spaces:
Sleeping
Sleeping
Przemyslaw Chachaj commited on
Commit ·
e658037
1
Parent(s): 5296ac8
Stable MCP version - no torch, no whisper
Browse files- app.py +66 -306
- modal/sms_webhook.py +0 -0
- modules/plan_generator.py +0 -29
- modules/session.py +0 -28
- modules/sms_logic.py +0 -0
- modules/summarizer.py +0 -46
- modules/transcriber.py +0 -92
- packages +0 -1
- requirements.txt +2 -5
app.py
CHANGED
|
@@ -3,339 +3,99 @@ import json
|
|
| 3 |
import uuid
|
| 4 |
import datetime
|
| 5 |
from typing import Optional, Dict, Any
|
| 6 |
-
|
| 7 |
import gradio as gr
|
| 8 |
-
import yt_dlp
|
| 9 |
-
from openai import OpenAI
|
| 10 |
-
|
| 11 |
-
# =====================================
|
| 12 |
-
# CONFIG
|
| 13 |
-
# =====================================
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
DATA_DIR = os.path.join(BASE_DIR, "storage", "contracts")
|
| 18 |
os.makedirs(DATA_DIR, exist_ok=True)
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
raise RuntimeError(
|
| 23 |
-
"OPENAI_API_KEY nie jest ustawiony.\n"
|
| 24 |
-
"Lokalnie: export OPENAI_API_KEY='...'\n"
|
| 25 |
-
"HuggingFace: Settings → Repository secrets → OPENAI_API_KEY"
|
| 26 |
-
)
|
| 27 |
-
|
| 28 |
-
client = OpenAI(api_key=OPENAI_API_KEY)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
# =====================================
|
| 32 |
-
# STORAGE UTILS
|
| 33 |
-
# =====================================
|
| 34 |
-
|
| 35 |
-
def _contract_path(uid: str) -> str:
|
| 36 |
-
return os.path.join(DATA_DIR, f"{uid}.json")
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def save_contract(contract: Dict[str, Any]) -> None:
|
| 40 |
-
uid = contract["uid"]
|
| 41 |
-
path = _contract_path(uid)
|
| 42 |
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
| 43 |
-
with open(path, "w", encoding="utf-8") as f:
|
| 44 |
-
json.dump(contract, f, ensure_ascii=False, indent=2)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def load_contract(uid: str) -> Dict[str, Any]:
|
| 48 |
-
path = _contract_path(uid)
|
| 49 |
-
if not os.path.exists(path):
|
| 50 |
-
raise ValueError(f"Contract with uid={uid} not found.")
|
| 51 |
-
with open(path, "r", encoding="utf-8") as f:
|
| 52 |
-
return json.load(f)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# =====================================
|
| 56 |
-
# CORE: TRANSCRIBE
|
| 57 |
-
# =====================================
|
| 58 |
-
|
| 59 |
-
def transcribe_reel(link: str) -> str:
|
| 60 |
-
"""
|
| 61 |
-
Download a reel/video from a link (e.g. Instagram/YouTube/TikTok)
|
| 62 |
-
and transcribe audio using OpenAI Whisper (gpt-4o-transcribe).
|
| 63 |
-
|
| 64 |
-
Zwraca czysty tekst (string).
|
| 65 |
-
"""
|
| 66 |
-
# 1) pobranie pliku przez yt-dlp
|
| 67 |
-
tmp_id = str(uuid.uuid4())
|
| 68 |
-
out_tmpl = f"/tmp/{tmp_id}.%(ext)s"
|
| 69 |
-
|
| 70 |
-
ydl_opts = {
|
| 71 |
-
"format": "mp4/bestaudio/best",
|
| 72 |
-
"outtmpl": out_tmpl,
|
| 73 |
-
"quiet": True,
|
| 74 |
-
}
|
| 75 |
-
|
| 76 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
| 77 |
-
info = ydl.extract_info(link, download=True)
|
| 78 |
-
filename = ydl.prepare_filename(info)
|
| 79 |
-
|
| 80 |
-
# 2) transkrypcja przez OpenAI
|
| 81 |
-
with open(filename, "rb") as f:
|
| 82 |
-
transcript = client.audio.transcriptions.create(
|
| 83 |
-
model="gpt-4o-transcribe",
|
| 84 |
-
file=f,
|
| 85 |
-
)
|
| 86 |
-
|
| 87 |
-
text = transcript.text if hasattr(transcript, "text") else str(transcript)
|
| 88 |
-
return text
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
# =====================================
|
| 92 |
-
# CORE: EXTRACT HABITS (goal + 7 kroków)
|
| 93 |
-
# =====================================
|
| 94 |
-
|
| 95 |
-
def extract_habits(transcript: str, category: str = "general") -> Dict[str, Any]:
|
| 96 |
-
"""
|
| 97 |
-
Z transkryptu robi:
|
| 98 |
-
- goal (jedno zdanie)
|
| 99 |
-
- steps: dokładnie 7 krótkich, wykonalnych kroków (lista stringów)
|
| 100 |
-
"""
|
| 101 |
-
if not transcript or not transcript.strip():
|
| 102 |
-
# Fallback gdy nie ma transkrypcji / błąd pobierania
|
| 103 |
-
return {
|
| 104 |
-
"goal": f"Popraw swoją sferę: {category}, robiąc jeden mały krok dziennie.",
|
| 105 |
-
"steps": [
|
| 106 |
-
"Zapisz dziś jeden konkretny cel związany z tym obszarem.",
|
| 107 |
-
"Zrób 10–15 minut skoncentrowanej pracy nad tym celem.",
|
| 108 |
-
"Usuń jedną oczywistą przeszkodę lub rozpraszacz.",
|
| 109 |
-
"Podziel swój cel na 3 mniejsze podcele i wybierz jeden.",
|
| 110 |
-
"Zrób mikro-wersję działania (np. 1 mail, 1 notatka, 1 telefon).",
|
| 111 |
-
"Podsumuj na kartce, czego nauczyłeś się z dzisiejszej próby.",
|
| 112 |
-
"Zaplanuj konkretny krok na jutro (godzina + miejsce).",
|
| 113 |
-
],
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
system_msg = (
|
| 117 |
-
"You are a behavior change coach. "
|
| 118 |
-
"User gives you a transcript of a short motivational / educational video. "
|
| 119 |
-
"Your job is to extract ONE clear goal and EXACTLY 7 simple daily actions "
|
| 120 |
-
"that move the user toward that goal. "
|
| 121 |
-
"Return STRICT JSON with keys: goal (string), steps (array of 7 short strings). "
|
| 122 |
-
"Do NOT add any explanation, only JSON."
|
| 123 |
-
)
|
| 124 |
-
|
| 125 |
-
user_msg = (
|
| 126 |
-
f"Category: {category}\n\n"
|
| 127 |
-
f"Transcript (may be truncated):\n"
|
| 128 |
-
f"{transcript[:4000]}"
|
| 129 |
-
)
|
| 130 |
-
|
| 131 |
-
resp = client.chat.completions.create(
|
| 132 |
-
model="gpt-4.1-mini",
|
| 133 |
-
messages=[
|
| 134 |
-
{"role": "system", "content": system_msg},
|
| 135 |
-
{"role": "user", "content": user_msg},
|
| 136 |
-
],
|
| 137 |
-
temperature=0.2,
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
raw = resp.choices[0].message.content
|
| 141 |
-
raw = raw.strip()
|
| 142 |
-
start = raw.find("{")
|
| 143 |
-
end = raw.rfind("}")
|
| 144 |
-
if start == -1 or end == -1:
|
| 145 |
-
# Jeżeli model coś popierdoli z formatem
|
| 146 |
-
raise ValueError(f"Cannot parse JSON from model output: {raw}")
|
| 147 |
|
| 148 |
-
|
| 149 |
-
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
|
|
|
| 154 |
|
| 155 |
-
# Normalizacja liczby kroków
|
| 156 |
-
if len(steps) < 7:
|
| 157 |
-
while len(steps) < 7:
|
| 158 |
-
steps.append("Powtórz mały krok, który realnie przesuwa Cię w tym obszarze.")
|
| 159 |
-
else:
|
| 160 |
-
steps = steps[:7]
|
| 161 |
-
|
| 162 |
-
return {"goal": goal, "steps": steps}
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
# =====================================
|
| 166 |
-
# CONTRACT LOGIC
|
| 167 |
-
# =====================================
|
| 168 |
-
|
| 169 |
-
def create_contract(
|
| 170 |
-
link: str,
|
| 171 |
-
category: str = "general",
|
| 172 |
-
uid: Optional[str] = None,
|
| 173 |
-
auto_transcribe: bool = True,
|
| 174 |
-
) -> Dict[str, Any]:
|
| 175 |
-
"""
|
| 176 |
-
Create a habit contract from a video link.
|
| 177 |
-
|
| 178 |
-
- (opcjonalnie) transkrybuje video
|
| 179 |
-
- wyciąga goal + 7 habit steps
|
| 180 |
-
- zapisuje kontrakt
|
| 181 |
-
- zwraca podsumowanie (uid, goal, pierwszy krok)
|
| 182 |
-
"""
|
| 183 |
-
if uid is None or not uid.strip():
|
| 184 |
-
uid = f"{category}_{uuid.uuid4().hex[:8]}"
|
| 185 |
-
|
| 186 |
-
transcript = ""
|
| 187 |
-
if auto_transcribe:
|
| 188 |
-
try:
|
| 189 |
-
transcript = transcribe_reel(link)
|
| 190 |
-
except Exception as e:
|
| 191 |
-
# nie zabijamy procesu – lecimy z fallbackiem w extract_habits()
|
| 192 |
-
print(f"[WARN] Transcription failed for {link}: {e}")
|
| 193 |
-
transcript = ""
|
| 194 |
-
|
| 195 |
-
habits = extract_habits(transcript, category=category)
|
| 196 |
|
|
|
|
|
|
|
|
|
|
| 197 |
contract = {
|
| 198 |
-
"uid":
|
| 199 |
-
"link":
|
| 200 |
-
"category":
|
| 201 |
-
"
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
"
|
| 212 |
-
"
|
| 213 |
-
"category": category,
|
| 214 |
-
"goal": contract["goal"],
|
| 215 |
-
"first_step": contract["steps"][0],
|
| 216 |
-
"total_steps": len(contract["steps"]),
|
| 217 |
}
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
def next_step(uid: str) -> Dict[str, Any]:
|
| 221 |
-
contract = load_contract(uid)
|
| 222 |
-
idx = contract.get("current_index", 0)
|
| 223 |
-
steps = contract.get("steps", [])
|
| 224 |
-
|
| 225 |
-
if idx >= len(steps):
|
| 226 |
-
return {
|
| 227 |
-
"uid": uid,
|
| 228 |
-
"done": True,
|
| 229 |
-
"message": "All steps completed.",
|
| 230 |
-
"goal": contract.get("goal", ""),
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
return {
|
| 234 |
-
"uid":
|
| 235 |
-
"
|
| 236 |
-
"
|
| 237 |
-
"
|
| 238 |
-
"step": steps[idx],
|
| 239 |
-
"remaining_steps": len(steps) - idx,
|
| 240 |
}
|
| 241 |
|
| 242 |
|
| 243 |
-
def
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
now = datetime.datetime.utcnow().isoformat() + "Z"
|
| 249 |
|
| 250 |
-
if idx < len(steps):
|
| 251 |
-
current_step = steps[idx]
|
| 252 |
-
contract.setdefault("history", []).append(
|
| 253 |
-
{"index": idx, "step": current_step, "done_at": now}
|
| 254 |
-
)
|
| 255 |
-
contract["current_index"] = idx + 1
|
| 256 |
-
save_contract(contract)
|
| 257 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
return next_step(uid)
|
| 259 |
|
| 260 |
|
| 261 |
-
def report(uid:
|
| 262 |
-
|
| 263 |
-
steps = contract.get("steps", [])
|
| 264 |
-
idx = contract.get("current_index", 0)
|
| 265 |
-
history = contract.get("history", [])
|
| 266 |
-
|
| 267 |
-
total = len(steps)
|
| 268 |
-
done = min(idx, total)
|
| 269 |
-
remaining = max(total - done, 0)
|
| 270 |
-
progress = (done / total * 100.0) if total > 0 else 0.0
|
| 271 |
-
|
| 272 |
return {
|
| 273 |
-
"uid":
|
| 274 |
-
"
|
| 275 |
-
"
|
| 276 |
-
"
|
| 277 |
-
"done_steps": done,
|
| 278 |
-
"remaining_steps": remaining,
|
| 279 |
-
"progress_percent": round(progress, 1),
|
| 280 |
-
"history": history,
|
| 281 |
-
"current_step": steps[idx] if idx < total else None,
|
| 282 |
}
|
| 283 |
|
| 284 |
|
| 285 |
-
#
|
| 286 |
-
|
| 287 |
-
#
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
"
|
| 292 |
-
"
|
| 293 |
-
|
| 294 |
-
)
|
| 295 |
-
|
| 296 |
-
with gr.Tab("
|
| 297 |
-
|
| 298 |
-
category_in = gr.Textbox(label="Category", value="career")
|
| 299 |
-
uid_in = gr.Textbox(label="Custom UID (optional)", value="")
|
| 300 |
-
output_create = gr.JSON(label="Contract summary")
|
| 301 |
-
|
| 302 |
-
btn_create = gr.Button("Create Contract")
|
| 303 |
-
btn_create.click(
|
| 304 |
-
create_contract,
|
| 305 |
-
inputs=[link_in, category_in, uid_in],
|
| 306 |
-
outputs=output_create,
|
| 307 |
-
)
|
| 308 |
-
|
| 309 |
-
with gr.Tab("Next Step"):
|
| 310 |
-
uid_next_in = gr.Textbox(label="Contract UID")
|
| 311 |
-
output_next = gr.JSON(label="Next step")
|
| 312 |
-
btn_next = gr.Button("Get Next Step")
|
| 313 |
-
btn_next.click(next_step, inputs=[uid_next_in], outputs=output_next)
|
| 314 |
-
|
| 315 |
-
with gr.Tab("Mark Done"):
|
| 316 |
-
uid_done_in = gr.Textbox(label="Contract UID")
|
| 317 |
-
output_done = gr.JSON(label="After marking done (next step)")
|
| 318 |
-
btn_done = gr.Button("Mark Current Step as Done")
|
| 319 |
-
btn_done.click(mark_done, inputs=[uid_done_in], outputs=output_done)
|
| 320 |
-
|
| 321 |
with gr.Tab("Report"):
|
| 322 |
-
|
| 323 |
-
output_report = gr.JSON(label="Progress report")
|
| 324 |
-
btn_report = gr.Button("Get Report")
|
| 325 |
-
btn_report.click(report, inputs=[uid_report_in], outputs=output_report)
|
| 326 |
-
|
| 327 |
-
with gr.Tab("Debug: Transcribe Only"):
|
| 328 |
-
link_tr_in = gr.Textbox(label="Reel / Video URL")
|
| 329 |
-
output_tr = gr.Textbox(label="Transcript (preview)")
|
| 330 |
-
btn_tr = gr.Button("Transcribe")
|
| 331 |
-
btn_tr.click(transcribe_reel, inputs=[link_tr_in], outputs=output_tr)
|
| 332 |
|
| 333 |
-
# MCP tools – exposed via /gradio_api/mcp
|
| 334 |
gr.api(create_contract)
|
| 335 |
gr.api(next_step)
|
| 336 |
gr.api(mark_done)
|
| 337 |
gr.api(report)
|
| 338 |
-
gr.api(transcribe_reel)
|
| 339 |
-
gr.api(extract_habits)
|
| 340 |
|
| 341 |
-
|
|
|
|
| 3 |
import uuid
|
| 4 |
import datetime
|
| 5 |
from typing import Optional, Dict, Any
|
|
|
|
| 6 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
+
# -------- STORAGE --------
|
| 9 |
+
DATA_DIR = "storage/contracts"
|
|
|
|
| 10 |
os.makedirs(DATA_DIR, exist_ok=True)
|
| 11 |
|
| 12 |
+
# -------- FILE UTILS -----
|
| 13 |
+
def _contract_path(uid:str): return os.path.join(DATA_DIR,f"{uid}.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
+
def save(c:dict):
|
| 16 |
+
with open(_contract_path(c["uid"]),"w",encoding="utf-8") as f: json.dump(c,f,indent=2,ensure_ascii=False)
|
| 17 |
|
| 18 |
+
def load(uid:str)->dict:
|
| 19 |
+
p=_contract_path(uid)
|
| 20 |
+
if not os.path.exists(p): raise ValueError(f"Brak kontraktu: {uid}")
|
| 21 |
+
return json.load(open(p,"r",encoding="utf-8"))
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
# -------- BASE CONTRACT GEN --------
|
| 25 |
+
def create_contract(link:str,category:str="general",uid:Optional[str]=None)->dict:
|
| 26 |
+
uid = uid or f"{category}_{uuid.uuid4().hex[:8]}"
|
| 27 |
contract = {
|
| 28 |
+
"uid":uid,
|
| 29 |
+
"link":link,
|
| 30 |
+
"category":category,
|
| 31 |
+
"steps":[
|
| 32 |
+
"Obejrzyj materiał i wypisz 1 mikro-wniosek.",
|
| 33 |
+
"Zastosuj mikro-wniosek w praktyce przez 5 minut.",
|
| 34 |
+
"Usuń 1 przeszkadzacz który blokuje ten nawyk.",
|
| 35 |
+
"Powtórz działanie jutro w mniejszej wersji.",
|
| 36 |
+
"Napisz krótką notatkę z efektów.",
|
| 37 |
+
"Zrób wersję X1.1 (trochę trudniejszą).",
|
| 38 |
+
"Pochwal się wynikiem komuś lub zapisz publicznie."
|
| 39 |
+
],
|
| 40 |
+
"current_index":0,
|
| 41 |
+
"history":[],
|
| 42 |
+
"created_at":datetime.datetime.utcnow().isoformat()+"Z"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
}
|
| 44 |
+
save(contract)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
return {
|
| 46 |
+
"uid":uid,
|
| 47 |
+
"goal":"Tiny habit based on reel",
|
| 48 |
+
"first_step":contract["steps"][0],
|
| 49 |
+
"total_steps":7
|
|
|
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
|
| 53 |
+
def next_step(uid:str)->dict:
|
| 54 |
+
c=load(uid)
|
| 55 |
+
i=c["current_index"]
|
| 56 |
+
if i>=len(c["steps"]): return {"uid":uid,"done":True,"message":"Wszystko wykonane."}
|
| 57 |
+
return {"uid":uid,"step":c["steps"][i],"remaining":len(c["steps"])-i}
|
|
|
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
def mark_done(uid:str)->dict:
|
| 61 |
+
c=load(uid)
|
| 62 |
+
i=c["current_index"]
|
| 63 |
+
if i<len(c["steps"]):
|
| 64 |
+
c["history"].append({"step":c["steps"][i],"ts":datetime.datetime.utcnow().isoformat()+"Z"})
|
| 65 |
+
c["current_index"]=i+1
|
| 66 |
+
save(c)
|
| 67 |
return next_step(uid)
|
| 68 |
|
| 69 |
|
| 70 |
+
def report(uid:str)->dict:
|
| 71 |
+
c=load(uid); total=len(c["steps"]); done=c["current_index"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
return {
|
| 73 |
+
"uid":uid,
|
| 74 |
+
"done":done,
|
| 75 |
+
"total":total,
|
| 76 |
+
"progress_pct":round(done/total*100,1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
}
|
| 78 |
|
| 79 |
|
| 80 |
+
# -------- UI + MCP --------
|
| 81 |
+
with gr.Blocks() as ui:
|
| 82 |
+
gr.Markdown("# Behavior Changer – MCP")
|
| 83 |
+
with gr.Tab("Create"):
|
| 84 |
+
l=gr.Textbox(label="Reel URL")
|
| 85 |
+
c=gr.Textbox(label="Category",value="career")
|
| 86 |
+
u=gr.Textbox(label="UID optional")
|
| 87 |
+
out=gr.JSON(label="Created")
|
| 88 |
+
gr.Button("Create").click(create_contract,[l,c,u],[out])
|
| 89 |
+
with gr.Tab("Next"):
|
| 90 |
+
uid=gr.Textbox(label="UID"); out=gr.JSON(); gr.Button("Next step").click(next_step,[uid],[out])
|
| 91 |
+
with gr.Tab("Done"):
|
| 92 |
+
uid=gr.Textbox(label="UID"); out=gr.JSON(); gr.Button("Mark done").click(mark_done,[uid],[out])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
with gr.Tab("Report"):
|
| 94 |
+
uid=gr.Textbox(label="UID"); out=gr.JSON(); gr.Button("Report").click(report,[uid],[out])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
|
|
|
| 96 |
gr.api(create_contract)
|
| 97 |
gr.api(next_step)
|
| 98 |
gr.api(mark_done)
|
| 99 |
gr.api(report)
|
|
|
|
|
|
|
| 100 |
|
| 101 |
+
ui.launch(mcp_server=True)
|
modal/sms_webhook.py
DELETED
|
File without changes
|
modules/plan_generator.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
# engine/plan_generator.py
|
| 2 |
-
import os
|
| 3 |
-
from openai import OpenAI
|
| 4 |
-
|
| 5 |
-
client = OpenAI(
|
| 6 |
-
base_url="https://api.tokenfactory.nebius.com/v1/",
|
| 7 |
-
api_key=os.getenv("NEBIUS_API_KEY")
|
| 8 |
-
)
|
| 9 |
-
|
| 10 |
-
def generate_plan(insight:str, category:str, steps:int=10):
|
| 11 |
-
prompt = f"""
|
| 12 |
-
Stwórz serię {steps} ultra-małych kroków nawykowych.
|
| 13 |
-
Kroki muszą być tak małe, że wykonanie ich zajmuje <2 minuty.
|
| 14 |
-
|
| 15 |
-
FORMAT WYJŚCIA (dokładny):
|
| 16 |
-
1. ...
|
| 17 |
-
2. ...
|
| 18 |
-
3. ...
|
| 19 |
-
|
| 20 |
-
INSIGHT użytkownika: {insight}
|
| 21 |
-
Kategoria: {category}
|
| 22 |
-
"""
|
| 23 |
-
|
| 24 |
-
resp = client.chat.completions.create(
|
| 25 |
-
model="deepseek-ai/DeepSeek-R1-0528",
|
| 26 |
-
messages=[{"role":"user","content":prompt}]
|
| 27 |
-
).choices[0].message.content
|
| 28 |
-
|
| 29 |
-
return [x[3:] for x in resp.split("\n") if x[:2].isdigit()]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/session.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
# engine/session.py
|
| 2 |
-
import json, os, time
|
| 3 |
-
|
| 4 |
-
PATH="state/"
|
| 5 |
-
|
| 6 |
-
def load(uid):
|
| 7 |
-
f = PATH+uid+".json"
|
| 8 |
-
return json.load(open(f)) if os.path.exists(f) else {
|
| 9 |
-
"insight":None,
|
| 10 |
-
"category":None,
|
| 11 |
-
"plan":[],
|
| 12 |
-
"done":[],
|
| 13 |
-
"created":time.time()
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
def save(uid,data):
|
| 17 |
-
os.makedirs(PATH,exist_ok=True)
|
| 18 |
-
json.dump(data,open(PATH+uid+".json","w"),indent=2)
|
| 19 |
-
|
| 20 |
-
def next_step(uid):
|
| 21 |
-
s=load(uid)
|
| 22 |
-
remaining=[x for x in s["plan"] if x not in s["done"]]
|
| 23 |
-
return remaining[0] if remaining else None
|
| 24 |
-
|
| 25 |
-
def mark_done(uid,step):
|
| 26 |
-
s=load(uid)
|
| 27 |
-
s["done"].append(step)
|
| 28 |
-
save(uid,s)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/sms_logic.py
DELETED
|
File without changes
|
modules/summarizer.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from openai import OpenAI
|
| 3 |
-
|
| 4 |
-
client = OpenAI(
|
| 5 |
-
base_url="https://api.tokenfactory.nebius.com/v1/",
|
| 6 |
-
api_key=os.environ["NEBIUS_API_KEY"]
|
| 7 |
-
)
|
| 8 |
-
|
| 9 |
-
def summarize(text:str) -> dict:
|
| 10 |
-
prompt = f"""
|
| 11 |
-
Streść najważniejszą myśl i podaj jedną mikro-akcję na dziś dla użytkownika.
|
| 12 |
-
Ultra krótko. Maks 2 zdania. Po polsku.
|
| 13 |
-
|
| 14 |
-
Format odpowiedzi bez etykiet:
|
| 15 |
-
|
| 16 |
-
INSIGHT: <jedno zdanie sedna, maks 140 znaków>
|
| 17 |
-
AKCJA: <jedna mikroczynność do wykonania dziś na podsstawie transkrypcji, maks 120 znaków>
|
| 18 |
-
|
| 19 |
-
Transkrypcja:
|
| 20 |
-
{text}
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
r = client.chat.completions.create(
|
| 24 |
-
model="moonshotai/Kimi-K2-Instruct",
|
| 25 |
-
messages=[{"role":"user","content":prompt}],
|
| 26 |
-
max_tokens=200
|
| 27 |
-
).choices[0].message.content.strip()
|
| 28 |
-
|
| 29 |
-
# Wyciągamy dwie rzeczy nawet bez idealnego formatu
|
| 30 |
-
import re
|
| 31 |
-
insight_match = re.search(r'INSIGHT:(.*)', r, re.IGNORECASE)
|
| 32 |
-
action_match = re.search(r'AKCJA:(.*)', r, re.IGNORECASE)
|
| 33 |
-
|
| 34 |
-
insight = insight_match.group(1).strip()[:140] if insight_match else None
|
| 35 |
-
action = action_match.group(1).strip()[:120] if action_match else None
|
| 36 |
-
|
| 37 |
-
# jeśli brak akcji → próbujemy z heurystyk
|
| 38 |
-
if not action:
|
| 39 |
-
# wyłuskanie najkrótszego zdania z tekstu
|
| 40 |
-
parts = [p.strip() for p in r.split("\n") if len(p.strip())<160]
|
| 41 |
-
action = parts[-1] if len(parts)>1 else "Zrób najmniejszą możliwą wersję działania."
|
| 42 |
-
|
| 43 |
-
if not insight:
|
| 44 |
-
insight = "Mały krok dzisiaj zmieni trajektorię tygodnia."
|
| 45 |
-
|
| 46 |
-
return {"insight": insight, "action": action}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
modules/transcriber.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
# 🔥 ładowanie modelu tylko raz → x3 szybciej przy wielu SMS
|
| 4 |
-
# model = whisper.load_model("base")
|
| 5 |
-
|
| 6 |
-
# def download_audio(url, out="temp.mp3"):
|
| 7 |
-
# cmd = [
|
| 8 |
-
# "yt-dlp",
|
| 9 |
-
# "-x", "--audio-format", "mp3",
|
| 10 |
-
# "--ffmpeg-location", "/usr/bin/ffmpeg",
|
| 11 |
-
# "-o", out,
|
| 12 |
-
# url
|
| 13 |
-
# ]
|
| 14 |
-
# try:
|
| 15 |
-
# subprocess.run(cmd, check=True, timeout=30) # timeout = mniej zwisów
|
| 16 |
-
# return out
|
| 17 |
-
# except Exception as e:
|
| 18 |
-
# print("AUDIO ERROR:", e)
|
| 19 |
-
# return None
|
| 20 |
-
import os
|
| 21 |
-
import shutil
|
| 22 |
-
import subprocess
|
| 23 |
-
import whisper
|
| 24 |
-
|
| 25 |
-
TEMP_AUDIO = "temp.mp3"
|
| 26 |
-
_model = whisper.load_model("base") # cache modelu, żeby nie ładować przy każdym wywołaniu
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def _get_ffmpeg_path() -> str:
|
| 30 |
-
"""
|
| 31 |
-
Zwraca ścieżkę do ffmpeg albo rzuca czytelny wyjątek,
|
| 32 |
-
jeśli ffmpeg nie jest zainstalowany.
|
| 33 |
-
"""
|
| 34 |
-
path = shutil.which("ffmpeg")
|
| 35 |
-
if path is None:
|
| 36 |
-
raise RuntimeError(
|
| 37 |
-
"ffmpeg nie jest dostępny w PATH.\n"
|
| 38 |
-
"Na Macu zainstaluj: brew install ffmpeg"
|
| 39 |
-
)
|
| 40 |
-
return path
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def download_audio(url: str, out: str = TEMP_AUDIO) -> str:
|
| 44 |
-
"""
|
| 45 |
-
Pobiera audio z URL do pliku MP3.
|
| 46 |
-
Rzuca RuntimeError jeśli yt-dlp padnie.
|
| 47 |
-
"""
|
| 48 |
-
if os.path.exists(out):
|
| 49 |
-
os.remove(out)
|
| 50 |
-
|
| 51 |
-
ffmpeg_path = _get_ffmpeg_path()
|
| 52 |
-
|
| 53 |
-
cmd = [
|
| 54 |
-
"yt-dlp",
|
| 55 |
-
"-x",
|
| 56 |
-
"--audio-format", "mp3",
|
| 57 |
-
"--ffmpeg-location", ffmpeg_path,
|
| 58 |
-
"-o", out,
|
| 59 |
-
url,
|
| 60 |
-
]
|
| 61 |
-
|
| 62 |
-
try:
|
| 63 |
-
subprocess.run(cmd, check=True)
|
| 64 |
-
except subprocess.CalledProcessError as e:
|
| 65 |
-
raise RuntimeError(f"yt-dlp nie udało się pobrać audio: {e}") from e
|
| 66 |
-
|
| 67 |
-
if not os.path.exists(out):
|
| 68 |
-
raise RuntimeError("Pobieranie się udało, ale plik audio nie powstał.")
|
| 69 |
-
|
| 70 |
-
return out
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def _get_model():
|
| 74 |
-
global _model
|
| 75 |
-
if _model is None:
|
| 76 |
-
# możesz wrócić do "small", jeśli chcesz szybciej / taniej
|
| 77 |
-
_model = whisper.load_model("base")
|
| 78 |
-
return _model
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def transcribe_from_url(url):
|
| 82 |
-
audio = download_audio(url)
|
| 83 |
-
model = _get_model()
|
| 84 |
-
if not audio: # 🔥 nowy soft fallback zamiast crasha
|
| 85 |
-
return "TRANSCRIPTION_FAILED"
|
| 86 |
-
|
| 87 |
-
try:
|
| 88 |
-
result = model.transcribe(audio)
|
| 89 |
-
return result.get("text", "")
|
| 90 |
-
except Exception as e:
|
| 91 |
-
print("TRANSCRIBE ERROR:", e)
|
| 92 |
-
return "TRANSCRIPTION_FAILED"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
packages
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
ffmpeg
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,7 +1,4 @@
|
|
| 1 |
gradio[mcp]
|
|
|
|
| 2 |
yt-dlp
|
| 3 |
-
|
| 4 |
-
requests
|
| 5 |
-
openai-whisper
|
| 6 |
-
tiktoken
|
| 7 |
-
torch==2.1.0 --extra-index-url https://download.pytorch.org/whl/cpu
|
|
|
|
| 1 |
gradio[mcp]
|
| 2 |
+
openai
|
| 3 |
yt-dlp
|
| 4 |
+
requests
|
|
|
|
|
|
|
|
|
|
|
|