- dashboard/handlers/youtube.py: după descărcare transcriere, cheamă
`claude -p` cu un prompt structurat care generează TL;DR + puncte cheie
+ citate + idei acționabile + secțiuni tematice în proze. Fallback la
transcriptul brut dacă Claude eșuează.
- nota Grantham: format complet — TL;DR, puncte cheie, citate,
idei acționabile, secțiuni tematice în proze curgătoare.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expose a navigation layer to the agent and harden RAG, after analyzing the
OKF note and testing on the real KB.
- memory_search.search(): dedupe best-chunk-per-file (a relevant note can no
longer be buried by another file's chunks) + keyword fallback tagged
degraded:True when Ollama is unreachable (no more hard crash).
- update_notes_index.py: emit per-folder index.md + root router; prune empty
folders; fix latent subcategory->project bug.
- Exclude generated index.md from RAG rglob (reindex/incremental) + indexer
scans + heartbeat freshness check (prevents self-pollution / reindex thrash).
- CLAUDE.md: reframe memory as hybrid (navigation first, RAG for fuzzy recall).
- Delete stale orphan kb/youtube/index.json; correct the OKF source note.
- Tests: dedup, keyword fallback, index.md exclusion. Plan + review in docs/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- tools/youtube_subs.py: get_subtitles() returneaza acum (title, desc, transcript).
Functii noi is_description_about_video() si extract_relevant_description()
detecteaza daca descrierea contine capitole/timestamps (nu doar promotie autori)
si curata trailing-urile promotionale inainte sa includa descrierea in output.
- dashboard/handlers/youtube.py: aceleasi functii adaugate; nota KB generata
include acum un bloc "Descriere / Index" daca descrierea e relevanta pentru video.
- memory/kb/youtube: nota Jeremy Grantham (AI bubble, investitii, toxicitate)
cu descrierea ca index de capitole.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gmail preserves the original sender when forwarding — whitelist check
was blocking all Fwd: emails not from mmarius28@gmail.com.
echo@romfast.ro is private, so any Fwd: arriving there is from Marius.
Also strip ***SPAM*** prefix from slugs for cleaner filenames.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Voice utterances and text messages on the same Discord channel now share
one Claude session, and Echo's voice replies are mirrored back into the
text channel. Replaces the old voice:<id> session-key split.
Changes:
- src/adapters/_text_chunks.py: new leaf module for split_message
(used by both discord_bot and voice pipeline)
- src/router.py: drop voice: prefix from session_key; add [voice] marker;
strip leading [speaker:/[voice] tokens from user input (anti-jailbreak);
remove dead double-clear of voice: key
- src/claude_session.py: include personality/VOICE_MODE.md unconditionally
(rules become per-turn-aware via [speaker:] prefix instead of session flag)
- src/voice/pipeline.py: VoiceSession splits text_channel_id +
voice_channel_id; resolve text channel per-send (no stale refs); mirror
Echo's reply text into the text channel after route_message returns
- src/adapters/discord_voice.py: /voice join passes both channel ids
- src/adapters/discord_bot.py: import split_message from leaf module
- personality/VOICE_MODE.md: rewrite as per-turn dynamic rules;
add synthesis instructions for text turns after voice turns
Tests:
- tests/test_router.py: 4 new cases (plain channel_id, anti-jailbreak,
text-adapter regression, no-double-clear)
- tests/test_pipeline_mirror.py: new — Echo reply mirror chunking,
empty guard, mirror_enabled=False, send-raises resilience
- tests/test_voice_session_channel_ids.py: new — split-attr contract
+ metrics payload schema
- tests/test_voice_session_cleanup.py: update for new kwargs
Plan: /home/moltbot/.claude/plans/vreau-ca-tot-textul-greedy-rivest.md
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Squashed branch: voice/dave-recv → master. Closes Pas 12 (DAVE E2E) and lands
voice-mode UX polish + verbal voice control on top of the Pas 1-10 scaffolding
already on master.
## DAVE E2E receive-side decrypt (e4f3177)
Vendored fork: discord-ext-voice-recv 0.5.3a+echo.dave1. Patches the receive
pipeline to handle Discord's mandatory DAVE encryption on voice gateway v=8.
- `_maybe_dave_decrypt`: uses davey.can_passthrough(user_id) as primary gate,
falls through to dave.decrypt for DAVE-epoch peers, drops on decrypt failure
without killing the reader thread.
- VAD fix: silero-vad v5+ requires exactly 512 samples; our 100ms window
(1600 samples) was silently raising ValueError → STT never fired. Now slice
into 512-sample chunks.
- Whisper: bumped beam_size 1→5 and added RO initial_prompt.
- Tests: 11 DAVE unit tests + 2 callback integration tests + contract test
with fork-version guard.
## Voice UX polish (d1bc77e)
- Killed the 3s "mă gândesc" filler (always collided with Claude p50 4-7s).
- Barge-in via `ttsq.clear()` at top of `on_segment_done`.
- DTX silence-flush poller (200ms tick) — Discord stops sending RTP packets
when silent, so the inline silence-check in sink.write() never fired for
trailing audio; background thread handles it.
- `EchoStreamingAudioSource.read()` non-blocking — old `get_frame(timeout=0.1)`
wrecked Discord's 20ms cadence and the client interpreted bursts as
stuttering (Marius heard "4 de minute" instead of full sentence).
- RO time expansion: 23:09 → "douăzeci și trei și nouă minute".
- Supertonic Unicode sanitize centralized in tools/tts.py.
- Whisper local_files_only=True — no HF metadata GET on each startup.
- Diagnostic logging through sink → VAD → Claude stream → TTS chain.
## Voice mode iteration (e589e48)
- `personality/VOICE_MODE.md` — voice-tailored system prompt (short, no
markdown, no abbreviations, time without seconds, distances in
"mii"/"milioane"); plumbed via build_system_prompt(voice_mode=True).
- Isolated voice session key `voice:<channel_id>` — voice doesn't share
context with text adapter on the same channel; auto-applied without
/clear ceremony. /clear drops both keys.
- Metric units + Romanian thousands (normalize.py): "384.000 km" →
"trei sute optzeci și patru de mii de kilometri" with feminine-correct
pluralization and "de" particle for ≥20.
- `/voice setvoice <M1-F5>` slash command with native autocomplete; swaps
live + persists voice.default_voice to config.json.
- Verbal voice change (src/voice/voice_commands.py + 29 tests) — "schimbă
vocea pe M5", "voce em cinci", with permissive substring fallback for
Whisper-mangled forms like "Mâcinci"=M5 and "unul cinci"=M5. Whisper
initial_prompt now lists voice vocabulary to bias STT toward clean
outputs.
- Fast barge-in: VAD ≥2 consecutive windows (~200ms) on Marius's user
while Echo has pending TTS frames → cut him off mid-sentence so user
doesn't wait the full silence + STT cycle. Acoustic echo bleed-through
still requires headphones (no AEC).
## Test suite
130 voice + router tests pass (test_voice_recv_dave, test_voice_session_cleanup,
test_voice_adapter_contract, test_voice_normalize, test_voice_commands,
test_router).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
src/voice/tts_stream.py (~280 lines):
- clause_segments(text, min_words=8): yield Romanian-aware clause chunks.
Split la punct (./!/?;:,) cu accumulation până min_words satisfied;
edge case text < min_words → single chunk. NU split mid-word/number/
currency. Romanian intonație de frază se rupe la sentence break — 8+
words minimizează seams.
- TTSQueue worker thread: text queue in → PCM frames out. Methods:
start/stop/push_text/push_filler/clear/is_empty. normalize_for_tts()
apply first, then clause_segments(), then Supertonic synth per chunk.
- EchoStreamingAudioSource(discord.AudioSource): read() pull from PCM
queue, 20ms frames (3840 bytes 48kHz s16le stereo). Eliminates RTP
gap between play() calls — single play() per session, source pulls.
- load_thinking_wav(): one-shot cache → 140 × 20ms frames (~2.8s)
pentru filler "Stai puțin să-mi adun gândurile".
- wav_to_pcm_20ms_frames(): WAV parser + ffmpeg subprocess resample
la 48kHz s16le stereo dacă nevoie.
Smoke test (în session): clause_segments behaviour OK, thinking.wav
loads, TTSQueue + EchoStreamingAudioSource construct clean. Integration
testing deferred la convergență (Pas 7 + Pas 11).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
personality/AGENTS.md — added ## Voice mode section after ## Platform
Formatting (logical fit: voice este încă un platform-specific register,
alături de Discord/WhatsApp formatting). 7 reguli aplicabile când
adapter_name == "discord-voice":
- 1-3 propoziții max
- fără markdown / fără bullet / fără linkuri
- numere/valute conversaționale ("treizeci de lei" nu "30 RON" —
normalize.py face conversia tehnică)
- lung/structurat → "L-am scris în chat." + text mirror
- ton ca la o cafea cu Marius, nu raport corporate
personality/TOOLS.md — added ### Discord Voice section după ### Whisper:
- ce e (bot ascultă/transcrie/răspunde rostit)
- "în voce" = /voice join, presence Listening, auto-leave 5min
- latency expectations ~5s perceived, filler peste 3s
- streaming TTS per clauză (zero gap)
- limitări (1-3 propoziții, STT pe cuvinte rare/acronime)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix arhitectural general (beneficiu și pentru text adapters), nu doar voice.
src/claude_session.py:
- _session_locks: dict[str, threading.Lock] cu bootstrap lock pentru
lazy creation thread-safe.
- _get_session_lock(channel_id) helper.
- send_message() body wrapped în with _get_session_lock(channel_id).
- threading.Lock (NU asyncio.Lock) — send_message e sync subprocess.run
blocking; asyncio.Lock nu protejează cod sync rulat via to_thread.
- Per-channel granularity preserved — different channels run în paralel.
- send_message() public signature unchanged.
src/router.py:
- route_message(): dacă adapter_name == "discord-voice", prepend
[speaker:<user_name>] prefix (Config.get("voice.user_name", "user")).
- Original text variable left untouched for downstream paths.
- Text adapters: zero behavior change.
- route_message() public signature unchanged.
tests/test_claude_session_mutex.py — 6 tests REGRESSION-CRITICAL:
- same channel serializes (concurrent → mutex serializes, no overlap)
- same channel lock identity (same dict entry per channel_id)
- different channels run in parallel (overlap MUST fire)
- 3 channels all overlap
- contested acquire blocks then proceeds (policy: blocking, not fail-fast)
- lock released on subprocess exception (no deadlock on crash)
Acquisition policy: BLOCKING acquire bound by claude --timeout (5min default)
nu fail-fast — adapters already serialize via asyncio.to_thread queue, un
non-blocking acquire ar surface transient busy errors.
Test results: 82 passed (51 existing + 31 new). 2 PRE-EXISTING failures în
TestPromptInjectionProtection (stale assertion vs current prompt text) —
out of scope, recomand ticket separat.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure functions pentru TTS text normalization (RO):
- strip_markdown: regex bold/italic/code/link/heading/list
- expand_numbers_ro: num2words pentru cardinals + decimal handling
("3.14" → "trei virgulă paisprezece", "3.05" → "trei virgulă zero
cinci" digit-by-digit la leading zero)
- expand_currency: formă naturală RO ("12.50 RON" → "doisprezece lei
și cincizeci de bani", "$25.99" → "douăzeci și cinci de dolari și
nouăzeci și nouă de cenți")
- expand_symbols: %/&/@/° + whitespace collapse
- expand_abbreviations: etc./dl./dna./nr./ş.a./ş.a.m.d.
- normalize_for_tts: full pipeline + hard truncate 200 cuvinte cu
"Restul l-am scris în chat."
Pipeline order: markdown → abbreviations → currency → numbers →
symbols → truncate. Currency BEFORE numbers — altfel "12.50 RON" se
degradează la "doisprezece virgulă cincizeci RON". Romanian "de"
particle rule: n>=20 AND (n%100 not in 1..19) → "o sută de lei",
"o sută cinci lei" (no "de"). n=1 with currency → "un dolar" /
"un leu" (article, nu cardinal).
35/35 tests pass: markdown(5), cardinals(6), decimals(4), currency
RON/USD/EUR/GBP mix(8), symbols(4), abbreviations(4), truncation(2),
edge cases empty/whitespace(2).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parametrul `lang` era definit (DEFAULT_LANG = "ro") dar nu era inclus
in request-ul HTTP catre /v1/audio/speech. Adaugat "lang": lang in
body-ul JSON si lang="ro" explicit in _tts_synthesize().
OpenAPI-ul Supertonic confirma ca /v1/audio/speech accepta `lang`
ca parametru optional (OpenAISpeechRequest schema).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
newsletter-test și content-discovery eliminate la cererea lui Marius.
crontab check_newsletter_cercetasi.py de asemenea șters.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
email_digest.py folosea save_unread_emails() care salva în memory/kb/emails/.
Notițele KB trebuie create DOAR de heartbeat. Acum digest-ul face fetch
direct din IMAP (ca email_forward.py), fără side effects pe KB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instrucțiunea era prea restrictivă (doar formulare/documente "acționabile").
Acum include orice URL relevant: articole, linkuri de citit, resurse.
Același comportament adăugat și în HEARTBEAT pentru TL;DR din KB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
La salvarea unui email forwardat, se extrage acum expeditorul original
din body și se elimină prefixul Fwd: din titlu — în loc de adresa lui Marius.
Corectat și fișierul deja salvat din 07 mai.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The idle-state action called /api/projects/approve, which 404'd because
idle workspace dirs have no approved-tasks.json entry to mutate. Now the
button opens the Propose modal pre-filled with the workspace slug so the
user actually has a path forward.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two structural fixes that together let users manage feature-branch
work without manual intervention:
Approval guard — `/plan/start` returns 409 `already_committed` if the
project status is approved/running/complete, unless the body opts in
with `force=true`. Frontend now renders "Re-planifică" instead of
"Planifică" on approved cards and gates it behind a confirm dialog
that threads `force=true` through. Prevents an accidental click from
wiping `status=approved` and burning a fresh planning subprocess.
Worktree awareness — projects can now declare that they target a
feature branch on an existing Gitea repo, not a repo-per-slug clone.
Three optional fields added to approved-tasks.json: `repo` (default
= slug), `branch` (feature branch to create), `base_branch` (default
main). Wired through `/p` flag parser in router.py, the dashboard
Propose modal's new "Avansat" section, and the night-execute prompt
which clones {repo} and creates {branch} from {base_branch} before
running ralph.
CLAUDE.md updated with both flows + the new schema fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
approved-tasks.json, dashboard/status.json, anaf-monitor/monitor.log
are auto-modified by background processes (heartbeat, cron jobs, ANAF
monitor). Untracking them stops the noisy "auto-commit from dashboard"
churn. Files stay on disk; readers (router._load_approved_tasks etc.)
already handle missing files by returning empty defaults.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
State files updated by dashboard/heartbeat/cron jobs, plus new KB
captures (samsung firmware todo, scout song reel, weekly youtube notes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes that together restore the planning UX:
- Dashboard reopen showed only a 500-char truncated excerpt of the last
assistant message. Backend now reads the Claude session JSONL directly
and returns full per-turn history; frontend iterates and renders all
bubbles, falling back to last_text_excerpt when the JSONL is missing.
- Phases never advanced because the agent ran /plan-* skills inline as
tool calls and the marker protocol was loose. Tightened the planning
prompt (mandatory PHASE_STATUS marker on the last line of every turn,
ban on inline phase invocation), and the frontend now auto-calls
/plan/advance when phase_ready=true.
- The phase strip never showed visual state because data-phase values
("office-hours") didn't match orchestrator phase names ("/office-hours").
Added normalizePhase + cleanup of PHASE_STATUS markers from rendered
bubbles.
Also bumps eco.py session-content truncation from 2k to 20k so /eco
session views aren't cut mid-response either.
Bumps last_text_excerpt fallback in planning_session.py from 500 to
50_000 so even when the JSONL is unavailable, the bubble isn't sliced
mid-word.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hash fragments never reach the server so they're lost during login
redirects. ?file= survives the ?next= flow; #hash still works for
direct access when already logged in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The reverse proxy strips /echo/ before Python, so next=/workspace.html.
Both the JS redirect and the server-side already-logged-in path now
prepend /echo to produce a valid public URL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pass current path as ?next= when bouncing unauthenticated requests
to /echo/login; after successful auth, JS reads and validates the
param (must start with /echo/, not /echo/login) before redirecting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fonturile inter-*.woff2 și tokens.css nu mai sunt referențiate —
Inter se încarcă din Google Fonts, tokens.css a fost înlocuit
de professional-theme.css în romfast-website (fișier greșit în repo).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Systemd user units get a minimal PATH that omits ~/.local/bin where
the claude binary lives, causing plan/respond to 500 on every call.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
_resolve_planning_key searches all active sessions by slug regardless of
adapter, so respond/finalize/cancel/advance work even when planning was
initiated from Discord or Telegram.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>