Przemyslaw Chachaj commited on
Commit
5296ac8
·
1 Parent(s): 99e75a7

Minimal working Behavior Changer (no Whisper, pure OpenAI)

Browse files
Files changed (1) hide show
  1. app.py +60 -147
app.py CHANGED
@@ -8,30 +8,22 @@ import gradio as gr
8
  import yt_dlp
9
  from openai import OpenAI
10
 
11
- # opcjonalny fallback na lokalny Whisper z reels-agent
12
- try:
13
- from modules import transcriber as local_transcriber # transcribe_from_url(url)
14
- except Exception:
15
- local_transcriber = None
16
-
17
- # opcjonalny summarizer z Nebiusa (reels-agent)
18
- try:
19
- from modules import summarizer as nebius_summarizer # summarize(text) -> {"insight","action"}
20
- except Exception:
21
- nebius_summarizer = None
22
-
23
  # =====================================
24
  # CONFIG
25
  # =====================================
26
 
27
- DATA_DIR = "./storage/contracts" # lokalnie
28
- # albo dla HF Spaces:
29
- # DATA_DIR = "/home/user/app/storage/contracts"
30
  os.makedirs(DATA_DIR, exist_ok=True)
31
 
32
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
33
  if not OPENAI_API_KEY:
34
- raise RuntimeError("Brak OPENAI_API_KEY w secrets Spaces.")
 
 
 
 
35
 
36
  client = OpenAI(api_key=OPENAI_API_KEY)
37
 
@@ -47,6 +39,7 @@ def _contract_path(uid: str) -> str:
47
  def save_contract(contract: Dict[str, Any]) -> None:
48
  uid = contract["uid"]
49
  path = _contract_path(uid)
 
50
  with open(path, "w", encoding="utf-8") as f:
51
  json.dump(contract, f, ensure_ascii=False, indent=2)
52
 
@@ -60,13 +53,17 @@ def load_contract(uid: str) -> Dict[str, Any]:
60
 
61
 
62
  # =====================================
63
- # TRANSKRYPCJA: OpenAI → fallback local Whisper
64
  # =====================================
65
 
66
- def _download_video_with_ytdlp(link: str) -> str:
67
  """
68
- Pobiera wideo/audio do /tmp i zwraca ścieżkę do pliku.
 
 
 
69
  """
 
70
  tmp_id = str(uuid.uuid4())
71
  out_tmpl = f"/tmp/{tmp_id}.%(ext)s"
72
 
@@ -79,65 +76,41 @@ def _download_video_with_ytdlp(link: str) -> str:
79
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
80
  info = ydl.extract_info(link, download=True)
81
  filename = ydl.prepare_filename(info)
82
- return filename
83
-
84
 
85
- def transcribe_reel(link: str) -> str:
86
- """
87
- 1) Próbuje transkrypcji w OpenAI (gpt-4o-transcribe)
88
- 2) Jeśli to padnie i mamy local_transcriber → używa Whisper lokalnie
89
- 3) Jeśli wszystko padnie → zwraca 'TRANSCRIPTION_FAILED'
90
- """
91
- # --- krok 1: OpenAI audio.transcriptions ---
92
- try:
93
- filename = _download_video_with_ytdlp(link)
94
- with open(filename, "rb") as f:
95
- transcript = client.audio.transcriptions.create(
96
- model="gpt-4o-transcribe",
97
- file=f
98
- )
99
- text = transcript.text if hasattr(transcript, "text") else str(transcript)
100
- if text and text.strip():
101
- return text.strip()
102
- except Exception as e:
103
- print(f"[WARN] OpenAI audio transcription failed: {e}")
104
-
105
- # --- krok 2: fallback – lokalny Whisper z reels-agent ---
106
- if local_transcriber is not None and hasattr(local_transcriber, "transcribe_from_url"):
107
- try:
108
- text = local_transcriber.transcribe_from_url(link) # type: ignore[attr-defined]
109
- if text and text.strip():
110
- return text.strip()
111
- except Exception as e:
112
- print(f"[WARN] Local Whisper transcription failed: {e}")
113
 
114
- # --- krok 3: totalna porażka ---
115
- return "TRANSCRIPTION_FAILED"
116
 
117
 
118
  # =====================================
119
- # EXTRACT HABITS (OpenAI – 7 kroków)
120
  # =====================================
121
 
122
  def extract_habits(transcript: str, category: str = "general") -> Dict[str, Any]:
123
  """
124
- Analizuje transkrypcję i produkuje:
125
- - goal (one sentence)
126
- - steps: 7 prostych kroków (list[str])
127
  """
128
- if not transcript or transcript == "TRANSCRIPTION_FAILED":
129
- # Fallback generic plan
130
  return {
131
- "goal": f"Improve your {category} through one small action per day.",
132
  "steps": [
133
- "Pick one tiny action related to your goal and do it today.",
134
- "Repeat the same action and make it slightly harder.",
135
- "Share your progress with someone (message or note).",
136
- "Add 10–15 minutes of focused deep work on this goal.",
137
- "Remove one distraction that blocks this goal.",
138
- "Create a simple checklist for next week.",
139
- "Summarize what you learned in 3–5 sentences."
140
- ]
141
  }
142
 
143
  system_msg = (
@@ -165,23 +138,24 @@ def extract_habits(transcript: str, category: str = "general") -> Dict[str, Any]
165
  )
166
 
167
  raw = resp.choices[0].message.content
168
-
169
- # Try to parse JSON even if model wrapped it
170
  raw = raw.strip()
171
  start = raw.find("{")
172
  end = raw.rfind("}")
173
  if start == -1 or end == -1:
 
174
  raise ValueError(f"Cannot parse JSON from model output: {raw}")
175
- json_str = raw[start:end + 1]
176
 
 
177
  data = json.loads(json_str)
178
 
179
  goal = data.get("goal") or f"Improve your {category}."
180
  steps = data.get("steps") or []
181
  steps = [s for s in steps if isinstance(s, str)]
 
 
182
  if len(steps) < 7:
183
  while len(steps) < 7:
184
- steps.append("Repeat a small action that moves you forward.")
185
  else:
186
  steps = steps[:7]
187
 
@@ -189,73 +163,35 @@ def extract_habits(transcript: str, category: str = "general") -> Dict[str, Any]
189
 
190
 
191
  # =====================================
192
- # NEBIUS SUMMARY (INSIGHT + TODAY ACTION)
193
- # =====================================
194
-
195
- def summarize_with_nebius(transcript: str) -> Dict[str, Optional[str]]:
196
- """
197
- Używa modules/summarizer.py z reels-agent (Nebius).
198
- Zwraca dict: {"insight": str | None, "action": str | None}
199
- Jeśli coś padnie → zwraca domyślne fallbacki.
200
- """
201
- default_insight = "Mały krok dzisiaj zmienia trajektorię tygodnia."
202
- default_action = "Obejrzyj materiał i zapisz 1 konkretną rzecz, którą dziś wdrożysz."
203
-
204
- if not transcript or transcript == "TRANSCRIPTION_FAILED":
205
- return {"insight": default_insight, "action": default_action}
206
-
207
- if nebius_summarizer is None or not hasattr(nebius_summarizer, "summarize"):
208
- return {"insight": default_insight, "action": default_action}
209
-
210
- try:
211
- res = nebius_summarizer.summarize(transcript) # type: ignore[attr-defined]
212
- insight = res.get("insight") if isinstance(res, dict) else None
213
- action = res.get("action") if isinstance(res, dict) else None
214
- except Exception as e:
215
- print(f"[WARN] Nebius summarizer failed: {e}")
216
- insight, action = None, None
217
-
218
- if not insight or not insight.strip():
219
- insight = default_insight
220
- if not action or not action.strip():
221
- action = default_action
222
-
223
- return {"insight": insight.strip(), "action": action.strip()}
224
-
225
-
226
- # =====================================
227
- # CONTRACT API (MCP tools)
228
  # =====================================
229
 
230
  def create_contract(
231
  link: str,
232
  category: str = "general",
233
  uid: Optional[str] = None,
234
- auto_transcribe: bool = True
235
  ) -> Dict[str, Any]:
236
  """
237
- Tworzy kontrakt nawyku z linku:
238
-
239
- - transkrypcja (OpenAI → Whisper fallback)
240
- - Nebius: INSIGHT + TODAY ACTION
241
- - OpenAI: goal + 7 kroków
242
- - zapis do /data/contracts
243
 
244
- Zwraca skrócone podsumowanie kontraktu.
 
 
 
245
  """
246
  if uid is None or not uid.strip():
247
  uid = f"{category}_{uuid.uuid4().hex[:8]}"
248
 
249
  transcript = ""
250
  if auto_transcribe:
251
- transcript = transcribe_reel(link)
252
-
253
- # Nebius: INSIGHT + AKCJA DNIA
254
- nebius_summary = summarize_with_nebius(transcript)
255
- insight = nebius_summary["insight"]
256
- today_action = nebius_summary["action"]
257
 
258
- # OpenAI: goal + 7 kroków
259
  habits = extract_habits(transcript, category=category)
260
 
261
  contract = {
@@ -267,10 +203,6 @@ def create_contract(
267
  "current_index": 0,
268
  "history": [],
269
  "created_at": datetime.datetime.utcnow().isoformat() + "Z",
270
- "insight": insight,
271
- "today_action": today_action,
272
- # trzymamy tylko preview, żeby nie zabić storage
273
- "transcript_preview": None if not transcript else transcript[:2000],
274
  }
275
 
276
  save_contract(contract)
@@ -282,15 +214,10 @@ def create_contract(
282
  "goal": contract["goal"],
283
  "first_step": contract["steps"][0],
284
  "total_steps": len(contract["steps"]),
285
- "insight": insight,
286
- "today_action": today_action,
287
  }
288
 
289
 
290
  def next_step(uid: str) -> Dict[str, Any]:
291
- """
292
- Zwraca kolejny krok dla danego kontraktu.
293
- """
294
  contract = load_contract(uid)
295
  idx = contract.get("current_index", 0)
296
  steps = contract.get("steps", [])
@@ -301,7 +228,6 @@ def next_step(uid: str) -> Dict[str, Any]:
301
  "done": True,
302
  "message": "All steps completed.",
303
  "goal": contract.get("goal", ""),
304
- "insight": contract.get("insight", ""),
305
  }
306
 
307
  return {
@@ -311,15 +237,10 @@ def next_step(uid: str) -> Dict[str, Any]:
311
  "step_index": idx,
312
  "step": steps[idx],
313
  "remaining_steps": len(steps) - idx,
314
- "insight": contract.get("insight", ""),
315
- "today_action": contract.get("today_action", ""),
316
  }
317
 
318
 
319
  def mark_done(uid: str) -> Dict[str, Any]:
320
- """
321
- Oznacza bieżący krok jako wykonany i przechodzi do kolejnego.
322
- """
323
  contract = load_contract(uid)
324
  idx = contract.get("current_index", 0)
325
  steps = contract.get("steps", [])
@@ -338,9 +259,6 @@ def mark_done(uid: str) -> Dict[str, Any]:
338
 
339
 
340
  def report(uid: str) -> Dict[str, Any]:
341
- """
342
- Zwraca pełny raport z progresu.
343
- """
344
  contract = load_contract(uid)
345
  steps = contract.get("steps", [])
346
  idx = contract.get("current_index", 0)
@@ -361,23 +279,18 @@ def report(uid: str) -> Dict[str, Any]:
361
  "progress_percent": round(progress, 1),
362
  "history": history,
363
  "current_step": steps[idx] if idx < total else None,
364
- "insight": contract.get("insight", ""),
365
- "today_action": contract.get("today_action", ""),
366
  }
367
 
368
 
369
  # =====================================
370
- # GRADIO UI + MCP
371
  # =====================================
372
 
373
  with gr.Blocks() as demo:
374
  gr.Markdown(
375
  "# Behavior Changer – Habit Agent (MCP)\n"
376
- "Kontrakty nawyków z linków do Reels / video:\n"
377
- "- transkrypcja (OpenAI → Whisper fallback)\n"
378
- "- Nebius INSIGHT + mikro-akcja\n"
379
- "- 7 kroków z OpenAI\n"
380
- "- MCP tools do użycia z Claude/Cursor."
381
  )
382
 
383
  with gr.Tab("Create Contract"):
 
8
  import yt_dlp
9
  from openai import OpenAI
10
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  # =====================================
12
  # CONFIG
13
  # =====================================
14
 
15
+ # Zamiast /data (read-only lokalnie) używamy katalogu w repo
16
+ BASE_DIR = os.path.dirname(__file__)
17
+ DATA_DIR = os.path.join(BASE_DIR, "storage", "contracts")
18
  os.makedirs(DATA_DIR, exist_ok=True)
19
 
20
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
21
  if not OPENAI_API_KEY:
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
 
 
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
 
 
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
 
 
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 = (
 
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
+ json_str = raw[start:end + 1]
149
  data = json.loads(json_str)
150
 
151
  goal = data.get("goal") or f"Improve your {category}."
152
  steps = data.get("steps") or []
153
  steps = [s for s in steps if isinstance(s, str)]
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
 
 
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 = {
 
203
  "current_index": 0,
204
  "history": [],
205
  "created_at": datetime.datetime.utcnow().isoformat() + "Z",
 
 
 
 
206
  }
207
 
208
  save_contract(contract)
 
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", [])
 
228
  "done": True,
229
  "message": "All steps completed.",
230
  "goal": contract.get("goal", ""),
 
231
  }
232
 
233
  return {
 
237
  "step_index": idx,
238
  "step": steps[idx],
239
  "remaining_steps": len(steps) - idx,
 
 
240
  }
241
 
242
 
243
  def mark_done(uid: str) -> Dict[str, Any]:
 
 
 
244
  contract = load_contract(uid)
245
  idx = contract.get("current_index", 0)
246
  steps = contract.get("steps", [])
 
259
 
260
 
261
  def report(uid: str) -> Dict[str, Any]:
 
 
 
262
  contract = load_contract(uid)
263
  steps = contract.get("steps", [])
264
  idx = contract.get("current_index", 0)
 
279
  "progress_percent": round(progress, 1),
280
  "history": history,
281
  "current_step": steps[idx] if idx < total else None,
 
 
282
  }
283
 
284
 
285
  # =====================================
286
+ # GRADIO UI + MCP EXPOSURE
287
  # =====================================
288
 
289
  with gr.Blocks() as demo:
290
  gr.Markdown(
291
  "# Behavior Changer – Habit Agent (MCP)\n"
292
+ "Tworzy kontrakty nawyków z linków do Reels / video, "
293
+ "rozbija na 7 kroków i udostępnia jako MCP tools."
 
 
 
294
  )
295
 
296
  with gr.Tab("Create Contract"):