Commit Graph

548 Commits

Author SHA1 Message Date
hyungi 03a37c4b01 chore(reports): Phase 1/2 baseline + 2026-04~05 평가·관측 자료 보존
Phase 1.1a~1.3 / Phase 2.1~2.3 평가셋 측정 결과 + regression baseline + D9 STT 후속 VRAM 피크 관측 데이터.
project_search_v2 메모리에 Phase 2 평가셋 v0.2 baseline용 보존 명시.
2026-05-15 04:45:56 +00:00
hyungi 10244a726f Merge pull request 'feat(study): Mac mini derived-worker (PR-MacMini-Derived-Worker-1)' (#21) from feat/macmini-derived-explanation into main
Reviewed-on: #21
2026-05-15 13:36:26 +09:00
hyungi 5125f82d4a feat(study): Mac mini derived-worker (PR-MacMini-Derived-Worker-1)
GPU = RAG context provider, Mac mini = LLM 가공 공장.

GPU 측 변경:
- app/api/internal_study.py: GET /internal/study/explanation-context/{qid}
  Bearer auth, gather_explanation_context + _render_envelope_prompt 재호출.
  204=evidence missing, 410=deleted/ready.
- app/workers/study_queue_consumer.py: settings.study_explanation_enabled
  false 시 explanation 분기 skip (status/attempts 미변경, pending 유지 → Mac mini 흡수).
- app/core/config.py: study_explanation_enabled + internal_worker_token 2 setting.
- app/main.py: internal_study_router include (prefix /internal/study).
- docker-compose.yml: fastapi ports → 100.110.63.63:8000 Tailscale bind,
  STUDY_EXPLANATION_ENABLED + INTERNAL_WORKER_TOKEN env 추가.

Mac mini 측: ~/derived-worker/ (별도 push 0, 어제 작성).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 03:13:43 +00:00
Hyungi Ahn 261036c7b2 ops(file-watcher): idle fire 로그 가시화
watch_inbox() 가 new_count/changed_count 둘 다 0 일 때 silent — PR-NAS-Watch-Folder 검증 시 fire 추적 부재 확인 후 보완. else 분기 추가해 매 5min fire 마다 "변경 없음 (idle)" info 로그 한 줄.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:32:38 +09:00
Hyungi Ahn a6b8dae18e fix(gpu-health): container_ip() 가 document_server network IP 만 추출
ollama 는 home-gateway-network / document_server / ollama_default 3개 network 에 속해
range loop 가 모든 IP concat. (index .NetworkSettings.Networks "hyungi_document_server_default").IPAddress
로 명시. 다른 GPU 서비스 4개도 동일 single-network 이라 호환.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 10:02:54 +09:00
Hyungi Ahn 8f4413a38c fix(gpu-health): scripts 호출 도구를 host curl + container IP 로 통일
OCR/STT 컨테이너 안에 curl 미설치 (slim python image). docker exec curl 표준은
실측 OCI exec 실패. host curl + docker bridge IP (172.20.0.x) 로 변경 — host
publish 추가 아니라 docker network 내부 검증이라 보안 표면 동일.

reranker 만 curl 있고 OCR/marker/STT 는 python 만 있어 분기 발생을 회피.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:51:59 +09:00
Hyungi Ahn 98ee7dffe2 ops(gpu-health): GPU 서비스 health/smoke 표준화 + synthetic VRAM 피크 가드
PR-GPU-Health-1. 운영 준비성 표준화 PR (모델 성능 개선 아님).

- OCR /smoke endpoint 추가 (160x60 OK PNG in-memory, 200/503 분기, Docker healthcheck 미사용)
- marker /health endpoint 추가 (stt/ocr 동일 시그니처)
- reranker docker-compose healthcheck 추가 (TEI :80/health)
- scripts/gpu_service_smoke.sh: docker exec 표준 점검 (OCR/STT expose-only)
- scripts/gpu_vram_fixture.sh: Mode A sequential + Mode B light overlap + --stress 옵션
- tests/load/fixtures/: synthetic ocr_ok.png / sine_30s.wav / lorem_1p.pdf

OCR 빈 응답 false negative — root cause: ports 미매핑.
결정: ocr-service / stt-service 는 expose-only 유지, 운영 점검은 docker exec 내부 curl 표준.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:42:07 +09:00
hyungi f1399459c5 Merge pull request 'refactor(ai): GPU Ollama LLM 제거 — Mac mini 26B 단일 generation 호스트로 통일' (#20) from feat/gpu-llm-remove into main
Reviewed-on: #20
2026-05-14 08:34:00 +09:00
Hyungi Ahn 4eed0bc4f8 refactor(ai): GPU Ollama LLM 제거 — Mac mini 26B 단일 generation 호스트로 통일
GPU 서버 정체성 = embedding/rerank/STT/OCR/marker 특화 백엔드.
Generative LLM 0. Mac mini gemma-4-26B-A4B 가 triage + primary +
classifier 모두 흡수. fallback 은 Claude Sonnet 4 API (자동 trigger,
premium 과 budget 공유).

- triage: GPU Ollama gemma4:e4b → Mac mini :8801 26B (primary 동일 endpoint)
- fallback: GPU Ollama gemma4:e4b → Claude Sonnet 4 API (require_explicit_trigger=false)
- classifier: GPU Ollama gemma4:e4b → Mac mini :8801 26B (max_tokens 512)
- primary / premium / embedding / rerank: 변경 0

후속 (별 커밋): `ssh gpu "ollama rm gemma4:e4b-it-q8_0"` — VRAM ~11GB 회수.

Mac mini 단일화 위험 mitigation = (1) Mac mini uptime 31d 무중단 검증,
(2) Claude Sonnet 4 API daily_budget $5 안 (Mac mini up 가정 호출 빈도 낮음),
(3) Beszel siteMonitor :8801 health check + Synology Chat alert.

plan: ~/.claude/plans/rosy-launching-otter.md §C/§D/§E (7-device LLM 배치 + 운영 전략)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:16:40 +09:00
hyungi 92aa2aaf53 Merge pull request 'feat(auth): voice-memo bot 365d access token (PoC v1)' (#19) from feat/voice-memo-bot-token into main
Reviewed-on: #19
2026-05-13 14:19:41 +09:00
Hyungi Ahn 52f86acda7 feat(auth): voice-memo bot 365d access token (PoC v1)
bot 계정(`voice-memo-bot`) 한정 long-expiry access token 발급 경로 추가.
일반 사용자 흐름 영향 0 (env gate default false).

- core/auth.py: create_voice_memo_bot_token() 신규 (env gate + username hard-match)
- api/auth.py: login route 에 bot 분기 (bot 이면 long token 반환, 일반은 기존 흐름)
- docker-compose.yml: 3 env (VOICE_MEMO_BOT_TOKEN_ENABLED/_USERNAME/_EXPIRE_DAYS) default false

OpenClaw `/voice-memo` plugin → DS `/memos/` Bearer proxy 의 auth 기반.
정식 service-account/api_keys 테이블은 Phase 2 (multi-service 인입 추가 시점).

plan: ~/.claude/plans/rosy-launching-otter.md
project: ~/.claude/projects/-Users-hyungiahn/memory/project_voice_memo_pipeline.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:24:18 +09:00
Hyungi Ahn 08e7fed984 ops(search): reranker drift fix 사후 재측정 (postfix observation) 2026-05-13 12:06:20 +09:00
Hyungi Ahn d3303cec1c fix(search): point reranker endpoint to TEI service 2026-05-13 12:02:26 +09:00
hyungi 1293c7094a Merge pull request 'feat/news-tech-ai-sources' (#17) from feat/news-tech-ai-sources into main
Reviewed-on: #17
2026-05-13 07:54:59 +09:00
hyungi 38b3630492 Merge pull request 'feat(briefing): date picker + 카드별 읽음/하이라이트 액션' (#16) from feat/briefing-date-picker-and-actions into main
Reviewed-on: #16
2026-05-13 07:54:51 +09:00
hyungi 4b8120d83f feat(briefing): date picker + 카드별 읽음/하이라이트 액션
사용자 요청 (2026-05-13):
- 오늘 briefing 만 보여주고 과거 못 보는 게 아쉬움 → 날짜 선택 UI
- 시간대 별 나열은 오히려 불편 → date dropdown 1단계 선택
- 각 카드에 읽음/하이라이트 토글

Schema (migrations 263~266, 단일 statement):
- briefing_topics.is_read BOOL NOT NULL DEFAULT false
- briefing_topics.read_at TIMESTAMPTZ
- briefing_topics.highlighted BOOL NOT NULL DEFAULT false
- briefing_topics.highlighted_at TIMESTAMPTZ

API (app/api/briefing.py):
- TopicResponse 에 id / is_read / read_at / highlighted / highlighted_at 추가
- GET /api/briefing/dates → 사용 가능 날짜 목록 (60일 cap)
  · briefing_date / total_topics / total_articles / status / read_count / highlighted_count
- PATCH /api/briefing/topics/{id}/read body {value: bool} → 읽음 토글
- PATCH /api/briefing/topics/{id}/highlight body {value: bool} → 하이라이트 토글
- 토글 시 *_at 컬럼 자동 설정/NULL

UI (frontend/src/routes/news/+page.svelte):
- 헤더 우측 <select> date dropdown — 최신 + N일치 (highlighted_count 별 표시)
- 선택 시 /api/briefing?date=… 로 해당 날짜 briefing 로드
- 카드 우측 상단 ★ (하이라이트) + 읽음 버튼
- 하이라이트 = Card class ring-2 ring-yellow-400
- 읽음 = 외부 div class opacity-60 (시각 차분화, 펴기 가능)
- 토글 즉시 PATCH 호출 + 로컬 state 갱신

each key topic.topic_rank → topic.id 변경 (이미 unique).
2026-05-12 22:05:06 +00:00
hyungi 5a86e045f1 feat(news): seed 14 tech/AI news sources (8 countries)
briefing/digest 의 cross-country tech 토픽 다양성 확보용 source seed.
- KR ×2: GeekNews (Hada), AI Times
- US ×4: Hacker News, ArsTechnica AI, The Verge Tech, TechCrunch
- GB ×2: The Register, BBC Technology
- DE ×1: Heise Online
- JP ×2: ITmedia News, Gigazine
- CN ×1: 36Kr
- FR ×1: ZDNet France
- IN ×1: Analytics India Magazine

idempotent: WHERE NOT EXISTS (name). 운영 DB 에는 이미 적용됨,
백업 복원/신규 deploy 환경에서 자동 시드.

수집 검증 (2026-05-13 1차 fire, 8 source):
- 성공: Hacker News 30 / ArsTechnica AI 20 / Verge 10 / TC 20 / Register 50 / Heise 153 (총 283건 신규)
- 후속 fix: GeekNews 의 http redirect → feedburner 직접 URL, AI Times URL 오타 → S1N1.xml.

content category 는 news_sources.category (Tech / AI) 로 보존, briefing 의 country
필터 (MIN_COUNTRIES_PER_TOPIC ≥ 2) 와 호환.
2026-05-12 21:47:15 +00:00
hyungi 1d3d61d31e fix(briefing): lower clustering threshold 0.78 → 0.70
배포 후 관측 결과 (2026-05-13 새벽):
- 126 docs / 7 countries 인데 THRESHOLD=0.78 로 raw_clusters=124, dropped_min_articles=122, kept=1.
- 거의 매 article 이 별 cluster 로 갈려 토픽 묶음 실패.
- 같은 cron 어제 (5/12) 는 101 docs 에서 6 topics 성공 — 그날 뉴스가 우연히 같은 토픽으로 더 모인 case.

수동 측정 (5/13 동일 docs):
- 0.78 → kept=1
- 0.70 → kept=5 (allowed)

영구 변경 = THRESHOLD=0.70. cross-country 필터 (MIN_COUNTRIES≥2) + min_articles(≥2) 그대로
유지하므로 noise topic 위험은 제한적.

원본 주석 (0.75~0.80 중간값) 도 갱신.
2026-05-12 21:44:00 +00:00
hyungi 12ebc7c78c Merge pull request 'fix/scheduler-kst-timezone' (#15) from fix/scheduler-kst-timezone into main
Reviewed-on: #15
2026-05-13 06:34:12 +09:00
hyungi 2dbbeac1c7 fix(daily_digest): cast today to date object for KST comparison
매일 20:00 KST cron fire 시 fail:
  UndefinedFunctionError: operator does not exist: date = character varying

원인: today 가 strftime("%Y-%m-%d") 로 string, func.date(created_at) 가 date 타입.
PostgreSQL 가 date = string 비교 거부.

Fix: today = datetime.now(ZoneInfo("Asia/Seoul")).date() — date 객체로.
KST 기준은 scheduler cron 이 KST 20:00 에 fire 되므로 자연 일치.

scope: app/workers/daily_digest.py:24
2026-05-12 21:30:41 +00:00
hyungi 138f689c98 fix(scheduler): pass KST timezone to all CronTriggers
AsyncIOScheduler(timezone="Asia/Seoul") 의 scheduler-level timezone 이
CronTrigger 에 자동 전파되지 않아 6 cron 모두 UTC 로 fire 되던 버그.

영향 (모두 9h 오차):
- morning_briefing  의도 05:10 KST → 실제 14:10 KST
- daily_digest      의도 20:00 KST → 실제 05:00 KST (다음날)
- global_digest     의도 04:00 KST → 실제 13:00 KST
- law_monitor       의도 07:00 KST → 실제 16:00 KST
- mailplus_morning  의도 07:00 KST → 실제 16:00 KST
- mailplus_evening  의도 18:00 KST → 실제 03:00 KST (다음날)

Fix: 모든 CronTrigger 에 timezone=KST (= ZoneInfo("Asia/Seoul")) 명시.

검증 (재시작 후):
  law_monitor          next: 2026-05-13 07:00 KST
  mailplus_morning     next: 2026-05-13 07:00 KST
  mailplus_evening     next: 2026-05-13 18:00 KST
  daily_digest         next: 2026-05-13 20:00 KST
  global_digest        next: 2026-05-14 04:00 KST
  morning_briefing     next: 2026-05-14 05:10 KST
2026-05-12 21:30:34 +00:00
Hyungi Ahn 8f7871b443 ops(search): PR-RAG-Time-1 1주 후 재측정 PASS
baseline (2026-05-03) + week1 (2026-05-12) 두 측정 결과 JSON/MD 합본.

회귀 판정 4신호 모두 통과:
- top3 doc_id 변동: 0/6 쿼리
- freshness_ms max: 0.54ms (임계 10ms)
- total_ms max: 413ms (임계 500ms, warmup 후)
- policy 분포: 9/30 동일

별 이슈: reranker 404 drift 발견 (config.yaml endpoint = ollama 호출, 실제는 TEI 컨테이너). PR-RAG-Time-1 본질 회귀와 분리. 별 incident 트랙.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:41:40 +09:00
hyungi 626e859a81 Merge pull request 'docs(claude): refresh — drop stale model/IP, inventory authoritative' (#14) from docs/claude-md-refresh into main
Reviewed-on: #14
2026-05-12 15:07:39 +09:00
Hyungi Ahn f6f8f3b9d8 docs(claude): refresh — drop stale model/IP, inventory authoritative
stale 영역 정리:
- Qwen3.5-35B-A3B / nomic-embed-text / Qwen2.5-VL-7B → 역할별 표기 (실제 모델은 inventory)
- Mac mini Tailscale 100.76.254.116 / GPU 100.111.160.84 / NAS 100.101.79.37 → 모두 폐기 (D21 closure 2026-05-12), LAN 표기만 유지
- Mac mini nginx 앞단 프록시 → 폐기 (home-caddy 가 직접 ingress)
- "Mac mini 메인 docker compose" → GPU 가 메인 정정

추가:
- 운영 변경 정책 (inventory → config → deploy → verify)
- 머신 역할 표 / AI 파이프라인 역할 표 / 워커 스케줄 표
- 아침 브리핑 / global digest 진입점 + scheduler timezone
- asyncpg multi-statement 1 파일 1 statement 규칙 (PR-MorningBriefing-1 fix 교훈)
- 디자인 토큰 only 규칙
- 한국어 NFS 경로 NFC/NFD
2026-05-12 15:07:12 +09:00
hyungi 1f4bbb9413 Merge pull request 'docs(readme): refresh stack/features/infra to 2026-05 reality' (#13) from docs/readme-refresh into main
Reviewed-on: #13
2026-05-12 15:05:20 +09:00
Hyungi Ahn 6d8d207669 docs(readme): refresh stack/features/infra to 2026-05 reality
- AI: Qwen3.5-35B → gemma-4 26B MLX / 4B triage / bge-m3 / TEI reranker / Surya OCR / MLX Whisper
- infra: Mac mini Docker Compose → GPU 서버 메인 / Mac mini = MLX inference + STT
- features: 아침 브리핑, Phase 4 Digest, library, memos, events, study, audio/video, marker
- inventory authoritative 안내 (README 가 stale 진실 대신 inventory 우선)
- gpu-server/ deprecated 표기
2026-05-12 15:03:52 +09:00
hyungi 49f44bba60 Merge pull request 'feat(briefing): register 05:10 KST APScheduler cron' (#12) from feat/morning-briefing-scheduler into main
Reviewed-on: #12
2026-05-12 14:54:52 +09:00
Hyungi Ahn 55e39818ec feat(briefing): register 05:10 KST APScheduler cron
매일 KST 05:10 morning_briefing_run 자동 실행. scheduler timezone=Asia/Seoul
이라 hour=5 minute=10 만 명시. Phase 4 04:00 cron 종료 후 70분 buffer + MLX
semaphore 충돌 회피.
2026-05-12 14:54:20 +09:00
hyungi ff351e5a0f Merge pull request 'feat/morning-briefing-frontend' (#11) from feat/morning-briefing-frontend into main
Reviewed-on: #11
2026-05-12 14:53:18 +09:00
Hyungi Ahn 1696926b8c refactor(briefing): nav label to 아침 브리핑 2026-05-12 14:35:16 +09:00
Hyungi Ahn 4d9beb37ef feat(briefing): swap /news to morning briefing card UI
- /news/+page.svelte 전면 재작성: article list 폐기, /api/briefing/latest fetch → topic 카드 list
- 각 카드: topic_label + headline + country_perspectives (flag + 한국어 + summary + article #id 링크) + divergences/convergences/key_quotes + historical_context
- status 4-state UI 분기 (empty/partial/failed/success)
- 디자인 시스템 토큰 only, Card 공용 컴포넌트 재사용, Svelte 5 runes + TS
- layout 라벨 뉴스 → 브리핑 (라우트 /news 유지)
- 백업: git history
2026-05-12 14:30:42 +09:00
hyungi 8b4f4e53f4 Merge pull request 'feat/morning-briefing-backend' (#10) from feat/morning-briefing-backend into main
Reviewed-on: #10
2026-05-12 14:26:13 +09:00
Hyungi Ahn 6966be9cf6 fix(briefing): backfill country_perspectives[].article_ids from cluster members
LLM 이 article_ids 를 자율적으로 비워두는 케이스 (2026-05-12 첫 briefing 6
topics 모두 빈 list) 를 서버에서 보정.

후처리 정책 (_resolve_article_ids):
1. LLM 이 준 id ∩ cluster member id (엉뚱한 id 차단, hallucination 방어)
2. 비어있으면 같은 country cluster member top weight N 개 자동 주입
3. cluster 안 country 매칭 멤버 0 → []

per-country cap = MAX_ARTICLE_IDS_PER_COUNTRY = 5. weight 내림차순.

API 계약 강화: country_perspectives 가 있는 topic 은 article_ids ≥ 1 보장
(같은 country cluster member 존재 시). frontend / 외부 채널 / archive UI
모두 신뢰 가능.

tests 3 케이스 추가.
2026-05-12 13:15:26 +09:00
Hyungi Ahn 36fea2789a fix(briefing): split migration into 4 single-statement files
asyncpg 의 prepared statement 가 multi-statement 불허. Phase 4 101 SQL 은
2026-04-08 적용 당시엔 통과했지만 현재 asyncpg/sqlalchemy 버전에서 fail.

255_morning_briefings_table.sql  CREATE TABLE morning_briefings
256_morning_briefings_idx.sql    CREATE INDEX (briefing_date)
257_briefing_topics_table.sql    CREATE TABLE briefing_topics + UNIQUE
258_briefing_topics_idx.sql      CREATE INDEX (briefing_id, topic_rank)
2026-05-12 13:04:56 +09:00
Hyungi Ahn 4aed9c6173 fix(briefing): simplify migration SQL (remove unicode, ::jsonb cast)
asyncpg 'cannot insert multiple commands into a prepared statement' 회피.
가설: 한국어 코멘트의 special char (lambda/arrow) + '::jsonb' cast 가 asyncpg
prepare 에서 multi-statement 오인. Phase 4 101 SQL 패턴과 정확히 맞춤 — JSONB
column 이라 default literal 은 자동 cast.
2026-05-12 13:02:16 +09:00
Hyungi Ahn 431d4fe010 feat(briefing): add morning briefing schema + services + api (historical off)
야간 수집 뉴스 (KST 00:00~05:00) topic×country 비교 분석 1페이지 카드.
Phase 4 Global Digest 와 코드/로직/테이블 분리, 알고리즘만 services/clustering_common 공유.

Backend 신규:
- migrations/255_morning_briefings.sql: morning_briefings + briefing_topics
  (briefing_date UNIQUE, UNIQUE(briefing_id,topic_rank), FK CASCADE,
  historical_* 3컬럼 nullable, cluster_members JSONB, country_perspectives
  JSONB, status 4-state success|partial|failed|empty)
- app/models/briefing.py: SQLAlchemy ORM
- app/services/briefing/loader.py: KST 5h 윈도우 + news_sources prefix
  fallback (Phase 4 패턴 미러) + historical candidate pool 로더
- app/services/briefing/clustering.py: cluster_global topic-first
  (LAMBDA=ln(2)/2h, MIN_COUNTRIES_PER_TOPIC=2, MAX_TOPICS=7)
- app/services/briefing/comparator.py: call_primary 26B + JSON envelope
  sanitize (cap perspectives 10 / divergences 3 / convergences 2 /
  quotes 5) + fallback row 고정 형태 + retrieve_historical cosine top-K
- app/services/briefing/pipeline.py: load→cluster→select(K=7,λ=0.6)
  →historical→compare→status 4-state→delete+insert transaction
- app/workers/briefing_worker.py: APScheduler/수동 호출 공용 진입점,
  600s hard cap
- app/prompts/briefing_comparative.txt: 한국어 비교 분석 JSON 프롬프트,
  {articles_block} + {historical_block} 2섹션, 인용 금지 라벨
- app/api/briefing.py: GET /latest, GET ?date=, POST /regenerate?date=
  (admin, sync delete+insert tx, regenerated:true)

Backend 수정:
- app/main.py: briefing_router 등록 (/api/briefing prefix). scheduler
  등록은 PR-3 에서.
- app/services/digest/selection.py: select_for_llm 매개변수화 (K, λ
  caller 주입). Phase 4 동작은 default 값으로 보존.

Historical 정책:
- BRIEFING_HISTORICAL_ENABLED env flag, default off.
- flag off → historical_* 컬럼 모두 NULL, prompt {historical_block} 빈
  라벨, retrieval 호출 안 함.
- flag on (PR-1b 에서 enable) → cluster centroid 와 과거 30일 doc
  embedding cosine top-K 5 (sim≥0.70), prompt 에 주입.

Country canonical (실측 확인 후):
- documents.country 컬럼 부재 확정
- document_chunks.country 매칭률 0% (chunks 자체가 뉴스에 안 만들어짐)
- 유일 country 신호 = news_sources prefix 매핑 (Phase 4 와 동일)

Tests:
- tests/test_briefing_historical.py: 3 경로 회귀 (flag off/on with
  fixture/on zero match) + sanitize cap + fallback row 형태.

Verification: PR-1.8 에서 GPU 컨테이너 pytest + 수동 regenerate.
2026-05-12 12:58:50 +09:00
Hyungi Ahn 1ca6d8b522 refactor(digest): extract clustering helpers to clustering_common
Phase 4 Global Digest 의 클러스터링 핵심 알고리즘 (time-decay weight,
adaptive threshold, greedy cosine assign + EMA centroid, importance
normalize) 을 `app/services/clustering_common.py` 로 추출. country
축은 caller 책임 — Phase 4 cluster_country 는 그대로 country 별 호출,
신규 morning briefing 모듈이 country 없이 cluster_global 로 호출 예정.

selection.py 의 중복 _normalize 도 공통 util 로 통일.

동작 변경 0:
- LAMBDA / threshold / EMA alpha / MIN_ARTICLES 모두 Phase 4 기본값 유지
- docs.sort (in-place) → sorted (copy) 변경했으나 caller 가 정렬된
  docs 를 재사용하지 않으므로 무관 (dict element 의 weight 부여는
  reference 라 그대로 반영)

다음 commit 에서 Phase 4 회귀 검증 (digest regenerate diff 0).
2026-05-12 12:38:32 +09:00
hyungi de36a9abca Merge pull request 'fix(memos): voice memo file_type → 'immutable' (doc_type enum 호환)' (#9) from fix/memos-voice-doc-type into main
Reviewed-on: #9
2026-05-11 12:29:44 +09:00
Hyungi Ahn 3dc78e4f94 fix(memos): voice memo file_type → 'immutable' (doc_type enum 호환)
GPU 서버 main pull 후 /api/memos/?archived=false 가 500 — doc_type enum 에
'audio' 값 없음 (immutable/editable/note 만). list_memos WHERE file_type IN
('note', 'audio') 가 invalid_text_representation.

수정:
- voice upload Document.file_type = 'audio' → 'immutable' (기존 audio 컨테이너
  인입과 같은 패턴: file_type='immutable' + category='audio' + source_channel='voice')
- list_memos 필터에서 file_type 조건 제거 (source_channel IN ('memo','voice') 만으로
  분리 — file_type='immutable' 필터는 일반 PDF 까지 끌어옴, 위험)
- module docstring + voice upload 주석 업데이트

원본 plan 의 file_type='audio' 결정은 doc_type enum 미확인이 원인.
enum 확장(ALTER TYPE ADD VALUE 'audio') 대신 기존 패턴 재사용 — 안전 + 회귀 X.
2026-05-11 12:28:58 +09:00
hyungi f3693fa2ea Merge pull request 'feat/memo-intake-upgrade' (#8) from feat/memo-intake-upgrade into main
Reviewed-on: #8
2026-05-11 12:10:50 +09:00
Hyungi Ahn 1424e79495 docs(memos): iOS Shortcuts guide for voice memo upload 2026-05-11 12:09:12 +09:00
Hyungi Ahn e3adbb8961 feat(frontend): show memo triage and voice source UI
PR-2B/2C frontend (commit 4/4). plan v9 Memo Intake Upgrade.

PR-2B 분류 표시 + 1-click promote:
- 메모 카드 상단에 AI 분류 배지 (task/calendar/activity/reference + confidence%)
- ai_event_kind != 'note' 메모 하단에 4 버튼:
  · [할 일로] [일정으로] [활동으로] (AI 추천 kind 는 색깔 highlight)
  · [그냥 메모] (dismiss → ai_event_kind='note' 강제)
- promote 후 메모 카드에 "→ events #N" link 배지 (사용자 시각 확인)

PR-2C 음성 메모 표시:
- source_channel='voice' 메모는 🎙️ "음성" 배지
- audio player (<audio src=/api/documents/{id}/file?token=>) — 기존 file endpoint 재활용
- STT 대기 중인 voice 메모는 "음성 → 텍스트 변환 대기 중…" placeholder

API helpers:
- promoteMemo(memoId, kind) → POST /memos/{id}/promote-to-event
- dismissEventSuggestion(memoId) → POST /memos/{id}/dismiss-event-suggestion
- voiceAudioUrl(memoId) → /api/documents/{id}/file?token= (access token URL pattern)

Sidebar 영향 0 (events 진입점은 이미 PR-2 에서 추가됨).

원칙 (재명시): AI worker 는 events row 직접 생성 X — 본 UI 의 promote 버튼만이 events 진입.
2026-05-11 12:08:34 +09:00
Hyungi Ahn 6490050b04 feat(memos): promote memo to event + voice memo upload endpoint
PR-2B/2C backend 2/2. plan v9 commit 분할 2~3 통합 (memos.py 단일 파일 변경).

PR-2B promote-to-event:
- POST /api/memos/{memo_id}/promote-to-event — 메모 → events 1-click 승급
  · kind 결정: body.kind > documents.ai_event_kind > 400
  · activity_log 면 status=done + ended_at=now() 자동 (5초 행동 기록 UX)
  · calendar_event + start_at 있으면 status=scheduled
  · Event row + events_history(create) 자동 생성
  · memo_document_id 자동 link + source='memo' + raw_metadata 에 AI 추천값 보존
  · 한 메모 → N events 가능 (사용자 의도에 따라 dedup 없음)
- POST /api/memos/{memo_id}/dismiss-event-suggestion — '그냥 메모' (ai_event_kind='note' 강제)
  · MVP: AI 추천값과 사용자 확정값 같은 컬럼 (정확도 측정 흐려질 수 있음)
  · 백로그: user_event_kind 별 컬럼 분리 (plan Memo Intake Upgrade 백로그)
- MemoResponse 확장: ai_event_kind / ai_event_confidence / source_channel / file_type / file_path
- list_memos 필터 완화: file_type IN (note, audio) + source_channel IN (memo, voice)
  → voice 메모도 같은 inbox list 에 표시 (사용자 의도: 메모 = 모든 입력의 inbox)

PR-2C voice upload:
- migration 254: ALTER TYPE source_channel ADD VALUE 'voice'
- POST /api/memos/voice (multipart audio + recorded_at + device_hint)
  · 검증: Content-Type audio/* + size ≤ 50MB + 확장자 화이트리스트
  · NAS 저장: /documents/PKM/Recordings/{YYYY-MM}/{uuid}.{ext}
  · fsync + rename(atomic) 패턴 (NAS soft mount 안전)
  · Document row: file_type='audio' + source_channel='voice' + category='audio'
  · enqueue stt 큐 → 기존 stt_worker → classify (PR-2B triage) → embed → chunk
  · extract_meta 에 device_hint / recorded_at 보존
- 응답: MemoResponse (file_path 포함, frontend audio player 용)

원칙: AI worker 는 events row 직접 생성 X. 본 endpoint 가 사용자 의도 channel.
2026-05-11 12:06:41 +09:00
Hyungi Ahn 63990ac632 feat(memos): add AI event-kind triage fields
PR-2B (Memo Inbox Triage) backend 1/2. plan: beszel-tingly-sloth.md 라운드 13.
사용자 비전 = 메모는 inbox, AI 는 triage assistant. AI worker 는 events row 직접 생성 X.

Migrations 250–253 (실측 N=250):
- 250 CREATE TYPE event_kind_hint AS ENUM (note|task|calendar_event|activity_log|reference)
- 251 ALTER TABLE documents ADD ai_event_kind event_kind_hint
- 252 ALTER TABLE documents ADD ai_event_confidence NUMERIC(3,2) + CHECK 0–1
- 253 CREATE INDEX idx_documents_ai_event_kind partial WHERE ai_event_kind IS NOT NULL

ORM:
- Document.ai_event_kind / ai_event_confidence 컬럼 추가 (Enum SQLAlchemy 동기)
- source_channel enum 에 'voice' 추가 (PR-2C 와 호환)

Worker:
- classify_worker Phase 3 (Gemma 4B triage) 확장
  · TriageOutput 에 event_kind_hint + event_kind_confidence 필드 추가
  · 4B 응답에 hint 가 있을 때만 Document 에 저장 (enum 외 값은 무시)
- prompt p3a_short_summary.txt 확장 — note/task/calendar_event/activity_log/reference
  분류 기준 + confidence + default='note' 명시

원칙: AI worker 는 hint 만 제공. events 생성은 다음 commit 의 promote endpoint 에서만.
2026-05-11 12:04:21 +09:00
hyungi a842dc682e Merge pull request 'wip/gpu-main-snapshot-2026-05-11' (#7) from wip/gpu-main-snapshot-2026-05-11 into main
Reviewed-on: #7
2026-05-11 08:11:44 +09:00
hyungi 2f7b45d82c Merge pull request 'feat/events-ui-mvp' (#6) from feat/events-ui-mvp into main
Reviewed-on: #6
2026-05-11 08:11:32 +09:00
Hyungi Ahn 6d71116553 feat(events): PR-2 UI MVP — 4-tab + 빠른 행동 기록 + 상세/생성/이력
plan v6 PR-2 scope. 5초 행동 기록 UX 가 핵심 가설.

Backend:
- GET /api/events/{id}/history — events_history timeline 조회 (lifecycle op 자동 기록)

Frontend (SvelteKit 5 runes mode):
- /events 메인 — 4-tab (오늘/Inbox/예정/활동) + 빠른 행동 기록 widget
  · 단일 입력 + Enter → POST /api/events kind=activity_log
  · status=done + 시간 default 채워짐 (서버 측) → Activity 탭 즉시 반영
  · 새 항목을 list 최상단 prepend (refetch 불필요)
  · 연속 입력 위해 입력 ref focus 유지
  · lifecycle 버튼 (complete/defer/cancel/reactivate) — activity_log 는 lifecycle 대상 X
- /events/[id] 상세 — PATCH 허용 필드 edit (title/desc/시간/priority/project_tag) + history timeline
  · PATCH 금지 필드는 UI 노출 X (status/completed_at/cancelled_at/defer_until 은 별 버튼)
- /events/new — kind 선택 (task/calendar_event/activity_log) 후 필드 분기 form
  · task: due_at + start_at (선택, "14:00 전화" 같은 시각 task 허용 — 라운드 10)
  · calendar_event: start_at 필수 + end_at + all_day
  · activity_log: started_at/ended_at 비우면 서버 default now()
- Sidebar 메모 옆에 events 진입점 (CalendarCheck icon)

API helpers: frontend/src/lib/utils/events.ts (createEvent / logActivity / list*
/ lifecycle ops / kind&status enum label/color).

quickref doc: docs/events_api_quickref.md (이전 commit, PR-2 frontend reference).

PR-2 핵심 가설 검증 = 빠른 입력 → 저장 → Activity 즉시 반영 → 새로고침 유지.
PR-1 deferred HTTP behavior 5건도 본 UI 의 자연 사용으로 닫힘.
2026-05-11 07:56:31 +09:00
Hyungi Ahn 477be3892a docs(events): PR-1 → PR-2 quickref — API contract + 5초 행동 기록 UX 가이드
PR-2 (frontend UI MVP) 진입 전 reference doc. plan: beszel-tingly-sloth.md v6.

내용:
- JWT 인증 flow (curl 예시)
- 9 endpoint 표 (Create/List/Detail + 4 Lifecycle + 3 View)
- kind / status enum 의미 + UI 분기 hint
- 빠른 행동 기록 5초 UX (PR-2 핵심 가설)
- PR-2 smoke 로 자연 검증할 5건 (PR-1 closure 의 deferred 항목)
- events_history 조회 endpoint 미존재 (필요 시 PR-2 에서 추가)

authoritative API contract = /openapi.json. 본 doc 은 frontend cheat sheet.
2026-05-11 07:50:33 +09:00
hyungi bce18386f0 Merge pull request 'docs(storage): Storage PR-1 — read-only inventory + 정책 문서' (#4) from chore/storage-inventory into main
Reviewed-on: #4
2026-05-11 07:26:46 +09:00
hyungi dc96d2b298 Merge pull request 'feat(events): PR-1 Events Core — schema + ORM + 최소 API' (#5) from feat/events-core into main
Reviewed-on: #5
2026-05-11 07:26:31 +09:00