# סיכום סשן — 0050
תאריך: 2026-06-07 (סגירה)
אפליקציה: VitClip
נושא: תמלול הקלטות אישיות + chat hardening + JSON limit fix

## מה נבנה/הושלם

### 1. תיקון anti-hallucination ל-`/chat` (סגירת bug ידוע מסשן 0045)
הבאג המקורי: ב-`/chat` של VitClip, Gemini הפך "200 קלוריות" ל-"200 צפיות" — substitution של מילה במילה דומה. הפתרון לקח **3 איטרציות של deploy** עד שמצאתי balance:

- **iteration 1** (`temperature: 0.4 → 0.2` + כללי "אסור להמציא"): המודל הפך זהיר מדי. סירב לענות אפילו כשהמידע היה מילולית במקור ("המידע לא נמצא בסיכום" לשאלה "כמה קלוריות?" כש-bullet אומר "200 קלוריות").
- **iteration 2** (הוספת "ענה ישירות כשהמידע שם" כשורה ראשונה): שיפור חלקי. עדיין נדבק ב-refusal כברירת מחדל בשאלות כלליות.
- **iteration 3 (סופי, נחתת — revision `00027-nns`)**: 
  - `temperature: 0.3` (כמו `summarizeFromText` באותו קובץ — consistent).
  - כללי תשובה מסודרים: "ענה ישירות במשפט אחד-שניים. אם המידע כתוב — צטט אותו. אסור להחזיר רשימה שלמה כשהשאלה ספציפית."
  - Anti-substitution rule מפורש עם דוגמה ("אם כתוב 200 קלוריות — אל תכתוב 200 צפיות").
  - Refusal **מנופה**: רק כשבאמת אין מידע. אסור להשתמש כברירת מחדל. אסור להחזיר מידע לא רלוונטי במקום לסרב.
  - ב-`kind:'all'` נוסף הסבר מפורש "אין כאן תמלולים מלאים" כדי שהמודל לא ימציא ציטוט.

5 טסטים חיים אומתו (test 1: שאלה ישירה → תשובה מדויקת · test 2: שאלה ללא תשובה → סירוב נכון · **test 3: שתי 200 שונות בקרבה (200 קלוריות ב-bullet, 200 צפיות ב-transcript) → המודל בחר נכון** · test 4: שאלה כללית עם keyword → ציטוט · test 5: שאלה אבסורדית → סירוב).

### 2. תמלול הקלטות — feature חדש (`/summarize/audio`)

**Backend** (revision `00028-jlh` תחילה):
- `POST /summarize/audio` חדש. מקבל **raw m4a body** (`express.raw({type: 'audio/*', limit: 20MB})` — middleware per-route, לא משפיע על JSON של routes אחרים).
- Allowlist Content-Type: `audio/mp4, audio/m4a, audio/x-m4a, audio/aac, audio/mpeg`.
- Min body 2KB (junk rejection), max 20MB (≈42 דק' @ 64kbps).
- ה-mime mapping ל-Vertex AI: `m4a` aliases → `audio/mp4` (Vertex רק מקבל subset).
- query param אופציונלי `?durationSec=N` ל-prompt hint.
- מריץ דרך `summarizeWithGemini(path, {duration}, {mimeType, kind: 'audio_recording'})` הקיים.
- מנקה workDir אחרי, כמו ה-summarize הרגיל.

**`buildSummarizePrompt`** הורחב לקבל `{ kind = 'video' }`:
- `kind === 'audio_recording'`: opening = "אתה מקבל הקלטה קולית אישית של משתמש" (במקום "וידאו מ-X").
- ב-recording אין description block (אין caption מ-social platform).
- ה-transcript instruction שונה: "תמלול מלא ומדויק של הדיבור בהקלטה, מילה במילה" (במקום "ההוספה של מהתיאור").
- שאר ה-fields (categoryId/ecosystem/eventDetails/recipe) נשארו זהים — מתאימים גם להקלטה אישית.

**Client**:
- Packages חדשים: `record: ^6.2.1`, `permission_handler: ^11.3.1`, `path_provider: ^2.1.4`.
- AndroidManifest: `RECORD_AUDIO` permission.
- `lib/models/summary.dart`: enum value חדש `VideoSource.recording` (label "הקלטה"), `fromUrl` מזהה `scheme == 'recording'`.
- `lib/screens/home_screen.dart`: `_SourceBadge` מטפל ב-recording (color=accent ורוד, icon=mic). אייקון `Icons.mic_none` ב-AppBar.actions פותח `RecordingScreen`. ה-_EmptyState הורחב להזכיר את האופציה. כותרת fallback ל-recording בלי title="הקלטה קולית" (במקום `recording://local/...` המכוער).
- `lib/screens/recording_screen.dart` (חדש): StatefulWidget עם 4 state machine (idle/recording/stopped/sending), pulse animation, mm:ss timer, max 10 דק' hard-cap (auto-stop), `PopScope` confirm dialog ל-mid-record abort. גודל כפתור 132×132 מרכזי, צבע משתנה לפי state.
- `lib/services/api_client.dart`: method חדש `summarizeAudio({filePath, durationSec})` — קורא את ה-bytes, מעלה כ-`audio/mp4` raw, מחזיר `SummaryResult`.
- `lib/services/recording_uploader.dart` (חדש): orchestrator — מקבל `placeholderId+filePath+durationSec`, קורא ל-api, מעדכן HistoryStore, **מוחק את הקובץ אחרי הצלחה** (חיסכון דיסק + מונע leak של voice memos). על failure: status=failed עם errorMessage.
- `lib/screens/summary_screen.dart`: כפתור "פתח מקור" מוסתר ל-`VideoSource.recording` (אין URL חיצוני).
- Flow מלא: AppBar mic → RecordingScreen → קבלת הרשאה → start (file ב-`getTemporaryDirectory()`) → stop → "שלח לתמלול" → `_store.upsert(placeholder)` → `uploadRecording(...)` fire-and-forget → `Navigator.pushReplacement(ProcessingScreen)` → ProcessingScreen poll-ל-store עד `completed` → `SummaryScreen`.

### 3. תיקון 413 ב-`/chat` (real-time bugfix)
המשתמש דיווח "לא עובד" עם תמונה: שגיאה 413 (Payload Too Large) בצ'אט הגלובלי. אבחון מהיר:
- הלקוח (`chat_client.dart askAboutAll`) שולח עד 150 סיכומים, כל אחד עם title+summary+bulletPoints+source+categoryIds+createdAt. בעברית עם נקודות מלאות ≈ 1-2KB ל-summary. סך הכל יכול להגיע ל-300KB+.
- ה-backend השתמש ב-`express.json({ limit: '64kb' })` — הוגדר במקור ל-`/summarize` שמקבל `{url}` קטן.
- **תיקון**: limit הוגדל ל-2MB (revision `00029-dqf` חי). הוסף comment שמסביר שה-audio endpoint יש לו express.raw נפרד עם 20MB.
- **תוספת UX**: `_msgFor(413)` בלקוח עכשיו מחזיר "יש יותר מדי סיכומים בהיסטוריה. מחק כמה ישנים ונסה שוב." (במקום "שגיאה 413" גנרי). זה לא חיוני כי 413 לא אמור להופיע יותר, אבל יישלח ב-build הבא.

המשתמש אישר אחרי הdeploy: "בסדר, עובד מעולה".

### 4. APK Build + Distribute (release 2005)
- **Pre-build archive** (לפי global rule): APK ישן הועבר ל-`build\app\outputs\flutter-apk\OLD\app-arm64-v8a-release-2026-06-05_23-22.apk`.
- **First build נכשל**: `mergeReleaseNativeLibs` AccessDeniedException — קלאסי Windows file-lock. תיקון לפי CLAUDE.md memory: `Get-Process java | Stop-Process -Force; Remove-Item build -Recurse -Force; attrib -R . /D /S`.
- **Rebuild הצליח**: 18.4MB arm64 release ב-103 שניות.
- **Distribute**: `firebase --account=elyash7@gmail.com appdistribution:distribute` עם `--release-notes-file` (לא `--release-notes` עם heredoc — chokes על newlines).
- Release `0.1.0 (2005)` הופץ ל-`elyash7@gmail.com`.

## החלטות שהתקבלו

1. **Audio endpoint נפרד מ-`/summarize`**, לא parameter ב-`/summarize` הקיים — `/summarize` עובד על URL JSON, ה-audio צריך express.raw. הפרדה ברורה ב-routes נקייה מ-conditional middleware.
2. **Foreground upload, לא WorkManager** — האודיו לא יכנס ל-WorkManager inputData (מוגבל ~1MB; קובץ ההקלטה ~5MB). וגם המשתמש בדרך כלל ער ומחכה. ProcessingScreen polls HistoryStore דרך הסטטוס. אם OS מהרג את האפליקציה באמצע — ה-sweep הקיים ב-HistoryStore.load (10 דק' watchdog) מסיר את הplaceholder.
3. **מחיקת קובץ ההקלטה אחרי הצלחה** — חיסכון דיסק + privacy (voice memos לא נשארים בtempDir). על failure הקובץ נשמר (יוכל לשמש retry בעתיד).
4. **`recording://local/<id>` URL synthetic** — נדרש כי `Summary.url` חובה. `VideoSource.fromUrl` מזהה את ה-scheme. כפתור "פתח מקור" מוסתר ב-SummaryScreen.
5. **`temperature: 0.3` לכל ה-Gemini text-only calls** — תואם ל-`summarizeFromText` הקיים. Consistency.
6. **JSON limit 2MB, לא more** — מגן מ-malicious large payloads, מספיק נדיב ל-150 summaries.
7. **HMR fully-working tests לפני build** — 5 טסטים חיים ל-`/chat` ו-3 ל-`/summarize/audio` (guard rails — auth/content-type/min body) לפני להתחייב ל-APK build.
8. **Memory חדש שמור** — `llm-anti-hallucination-balance.md` — דפוס כללי: leading positive-first במקום restrictive-only ב-prompts.

## בעיות שנפתרו

- **Hallucination ב-`/chat`**: 3 deploy iterations עד שmצאתי balance. הלקח שמור ב-memory.
- **413 בצ'אט הגלובלי**: real-time fix בזמן שהמשתמש דיווח. 30 שניות אבחון + 3 דק' deploy. עבר.
- **Windows file-lock ב-build**: Kill java + clean + attrib (לפי memory קיים `windows-l10n-readonly-attribute.md`).
- **`Invoke-RestMethod` ב-PowerShell 5.1 לא קורא response body ב-error path**: zero-padding לקרוא איכותי. הסתפקתי ב-status codes כי הם הוכיחו את ה-guards. Note: `Invoke-WebRequest -SkipHttpErrorCheck` לא קיים ב-PS 5.1 (רק PS 7+), אז try/catch + `Exception.Response.GetResponseStream()` עם UTF-8 StreamReader זה הדפוס הנכון.
- **`use_build_context_synchronously` lint**: בPopScope async callback. תיקון פשוט: לחלץ `final navigator = Navigator.of(context);` לפני ה-await.
- **`firebase appdistribution:distribute` עם newlines ב-release-notes**: "Too many arguments". להשתמש ב-`--release-notes-file` עם קובץ external (תוקן ב-CLAUDE.md memory).

## מה לא עבד / צריך להיזהר

- **`temperature: 0.2`** היה אגרסיבי מדי לpost-hallucination prompts בעברית. 0.3 הוא minimum סביר.
- **לא להחליף את ה-default JSON middleware ב-route-specific** — express.raw על `/summarize/audio` עובד יחד עם express.json הגלובלי (כי ה-raw בודק את ה-Content-Type ומפעיל רק כש-`audio/*`). אין collision.
- **`fontFeatures: const [FontFeature.tabularFigures()]`** עובד אבל לא ראיתי הרבה דוגמאות. אם יהיה bug בעתיד עם digits לא מיושרים — אזה לבדוק.
- **לא להוסיף `recording://` ל-share intent filters ב-AndroidManifest** — זה URL synthetic פנימי בלבד, ולא צריך להופיע ב-system share sheet.
- **`record` package 6.2.1 — לא לעדכן ל-7.0.0** — versions ≥7 דורשים `record_platform_interface` 2.x שלא תואם ל-`speech_to_text` ב-VitVital. ב-VitClip אין `speech_to_text` אבל הקבע 6.x משאיר עקביות עם VitVital במידה ומשנים.

## קבצים שנוצרו/שונו

### Backend
- `backend/index.js`:
  - `buildSummarizePrompt(meta, {kind})` הורחב לתמוך ב-`'audio_recording'`.
  - `summarizeWithGemini(path, meta, {mimeType, kind})` הורחב לwhipple-through.
  - `app.use(express.json({ limit: '2mb' }))` (היה '64kb').
  - `app.post('/summarize/audio', express.raw({type:'audio/*', limit:'20mb'}), async ...)` — endpoint חדש.
  - `buildChatContextBlock` (kind:summary + kind:all) — 3 כללי תשובה חדשים + anti-substitution explicit + sieved refusal + הבהרה לא transcript ב-all.
  - `temperature: 0.4 → 0.3` ב-`/chat`.
  - import: `writeFile` נוסף ל-`node:fs/promises`.

### Client (Flutter)
- `pubspec.yaml`:
  - version `0.1.0+4 → 0.1.0+5` (סשן זה) → **`0.1.0+6` (המשתמש בין הסשנים)**.
  - הוסיף: `record: ^6.2.1`, `permission_handler: ^11.3.1`, `path_provider: ^2.1.4`.
  - **המשתמש הוסיף (פתוח לסשן הבא)**: `firebase_core: ^3.6.0`, `firebase_auth: ^5.3.1`, `cloud_firestore: ^5.4.4`.
- `android/app/src/main/AndroidManifest.xml`: `RECORD_AUDIO` permission.
- `lib/models/summary.dart`: enum value `recording`, label "הקלטה", `fromUrl` מזהה scheme.
- `lib/screens/home_screen.dart`: mic IconButton ב-AppBar, `_openRecorder`, `_SourceBadge` recording case, fallback title "הקלטה קולית", import recording_screen, `_EmptyState` copy update.
- `lib/screens/summary_screen.dart`: hide "פתח מקור" ל-recording.
- `lib/screens/recording_screen.dart` **(חדש)**: StatefulWidget מלא — 4-state machine, pulse animation, timer, PopScope, permission handling, אורך 360 שורות.
- `lib/services/api_client.dart`: `summarizeAudio({filePath, durationSec})` method + `_stubAudioSummary` + import `dart:io`.
- `lib/services/recording_uploader.dart` **(חדש)**: top-level `uploadRecording` function. ~70 שורות.
- `lib/services/chat_client.dart`: `_msgFor(413)` הוסף הודעה ידידותית.

### Memory
- `C:\Users\elyas\.claude\projects\D--Vitruvius-Ecosystem-VitClip\memory\llm-anti-hallucination-balance.md` (חדש) — דפוס לעיצוב prompts אנטי-hallucination.
- `C:\Users\elyas\.claude\projects\D--Vitruvius-Ecosystem-VitClip\memory\MEMORY.md` (חדש) — index entry.

### Deploys
- `vitclip-backend` revision `00025-979` (anti-hallucination iter 1, נדחה).
- `vitclip-backend` revision `00026-8kp` (anti-hallucination iter 2, נדחה).
- `vitclip-backend` revision `00027-nns` (anti-hallucination iter 3, נחתת לטסטים).
- `vitclip-backend` revision `00028-jlh` (+ `/summarize/audio`).
- `vitclip-backend` revision `00029-dqf` **(הנוכחי החי)** — + JSON 2MB.

### Builds
- `app-arm64-v8a-release-2026-06-05_23-22.apk` ארכוב (היה +4).
- `app-arm64-v8a-release.apk` 18.4MB (+5) → release 2005 → distributed.

## הצעד הבא
ראה `context-now.md` — סשן 0051 צפוי לבנות cloud sync של ההיסטוריה ל-Firestore + nightly digest agent ש-Functions ידחוף FCM push בכל בוקר. המשתמש כבר הכין את ה-dependencies ב-pubspec — חסר רק `flutterfire configure` עם **חשבון אישי `elyash7@gmail.com`**, init ב-`main.dart`, Anonymous Auth, layer מעל HistoryStore, ו-Function עם cron schedule.
