2026-09-19(토) 세션 — LIVE STT 회의 참석자용 공개 링크 · 화자 이름 필터 · 토큰 소비량
[페페] 「Live stt pc 버전에만 공개 funnel 링크 생성해줘. 모바일에서는 안보이게. 회의때 내가 직접 이 링크를 전달해준 사람은 정리본을 실시간으로 볼 수 있다. 그 말은 링크는 라이브 녹음중인 전사본의 정리본만 상대에게 보여주는 페이지 생성해서 연결해야 한다. 여기에 화자가 파악되면, 상단에 화자 이름으로 필터 버튼을 실시간 반영해준다. 상대는 자신에게 해당하는 버튼을 누르고 그에 맞는 정리본 내용을 본다. 추가로 실시간 녹음 페이지에는 5분 간격 토큰 소비량, 완료본에는 최종 토큰 소비량을 표기해줘.」 같은 턴 추가 = 「모바일 페이지도 필요하겠다. 같이 작업해줘」 → 공개 페이지를 폰(412)·PC(1024) 한 벌 반응형으로.
상세 설계·화면 = devplan C:\dev\devplan\live-meeting-stt.html 「2026-09-19(토)」 절 · 규칙 = rules.md 🆕
🔴 이 세션의 제일 중요한 판단 — 8445 를 funnel 에 걸지 않았다
tailscale funnel 의 proxy 마운트는 경로 접두어를 «떼고» 넘긴다(당근에서 이미 실측된 사실,
projects/danggeun-ai-study/rules.md L56). 그러니 처음 생각한 443 /mtg → 8445 는
https://…/mtg/api/items 를 서버에 /api/items 로 배달한다 = 전 회의 목록·전사 전문·
/ws(녹음 시작)·/ingest(wav 쓰기)가 통째로 인터넷에 열린다. 정규식으로 걸러내는 대신
라우팅 표면 자체를 갈랐다 — 같은 프로세스 안에 /pub/<토큰> 두 라우트만 가진 별도 aiohttp 앱을
127.0.0.1:8448 에 띄우고(make_pub_app + on_startup), funnel 은 거기만 본다.
사설 API 는 «막은» 게 아니라 그 앱에 없다. 실측 = /mtg/pub/<tok> 200 · /mtg/api/items 404.
- Plan(fable)·코덱스 둘 다 독립적으로 같은 처방(별도 포트)을 냈다. 코덱스 왈 「미들웨어 단독은 비추천」.
한 일
| # | 무엇 | 실물 |
|---|---|---|
| ① | 공개 링크 발급·폐기 — POST/DELETE /api/items/{ts}/share. 토큰 = secrets.token_urlsafe(16)(22자·128비트). 48시간 자동 만료(재발급하면 링크 유지·만료만 연장) |
live_server.py pub_issue/pub_revoke/api_share |
| ② | 공개 전용 앱(8448) + 페이지 — 폰·PC 한 벌 | make_pub_app · static/pub.html |
| ③ | 화자 이름 필터 칩(실시간) | meeting_system.txt ## 👥 화자별 + pub.html |
| ④ | 토큰 소비량 — 녹음 5분 구간 · 완료 총량 | make_notes_blocking → (md, err, usage) · notes_usage[] |
| ⑤ | PC 전용 발급 버튼·시트 | static/index.html .desktop-only + #shareSheet |
- 공개 응답에 나가는 것 = 제목·부제·정리본·진행상태뿐(키 8개 허용목록). 전사 세그먼트·음성·
토큰소비·
notes_error(로컬 경로가 실린다)는 싣지도 않는다. - 헤더 =
no-store, private·no-referrer·noindex· CSPdefault-src 'none'. 모르는 토큰은 분당 20회 제한.
화자 이름 — 라이브엔 화자분리가 없다(설계의 핵심 제약)
음향 화자분리(diarize_blocking)는 정지 후 finalize 에서만 돈다 → 라이브 중 sess.spk 는 비어 있다.
그래서 「화자 이름」은 정리본 LLM 이 전사 내용에서 실제로 불린 호칭으로 뽑는다.
🔑코덱스가 짚은 함정을 프롬프트에 그대로 박았다 — 한국어에서 부르는 이름은 보통 「말한 사람」이 아니라 「듣는 사람」이다(「김 대리님 어떻게 보세요?」). 절을 만들 근거는 ①본인이 자기를 밝힌 발언 ②호명 직후 대답이 명백한 흐름 둘뿐. 확신 없으면 절을 안 만든다. 화면엔 「이름은 대화 내용으로 추정한 것이라 틀릴 수 있어요」를 항상 붙인다.
실측 — 화자 라벨이 있는 전사와 없는(= 라이브와 같은) 전사 둘 다에서 신동승·박광은 과장·이수민 3명 정확. Plan 이 걱정한 「정지하면 이름이 화자N 으로 뒤집힌다」는 안 일어났다(라벨이 있어도 이름을 찾는다).
토큰 소비량 — 그리고 내가 과장한 것의 정정
❌ 첫 보고(틀림)
짧은 시험 전사로 잰 회당 $0.22~0.34 에 「25초마다 1회」를 곱해 「1시간 회의 = 100회 넘고 수십 달러」라고 적었다.
✅ [페페] 지적 후 다시 잰 것 — C:\dev\talklog\live\_bench_notes_cost.py
[페페] 「정리본 1시간 토큰이 설명처럼 그렇게 많이 들어간다는 느낌은 없었어」 → 페페 말이 맞았다. 진짜 회의 전사를 길이별로 잘라 각 1회씩 실제 호출:
| 전사 누적 | 회의 경과 | 토큰 | 비용 | 호출 소요 |
|---|---|---|---|---|
| 3,970자 | ≈8분 | 40,432 | $0.31 | 37.2초 |
| 11,953자 | ≈24분 | 49,725 | $0.42 | 60.9초 |
| 21,948자 | ≈44분 | 60,787 | $0.54 | 75.5초 |
| 29,981자 | ≈60분 | 69,286 | $0.64 | 92.3초 |
틀렸던 이유 2개(방향이 반대라 서로 상쇄됐다):
1. 회당값을 바닥값으로 쟀다 — 짧은 전사는 claude-code 자체 시스템프롬프트(≈33k)만 실린 고정 오버헤드다.
실제론 누적 전사 전문이 매번 통째로 들어가 회당 비용이 회의 내내 2배로 커진다.
2. 주기가 25초가 아니다 — notes_loop 는 await asyncio.sleep(NOTES_SEC) 다음에 동기 호출이라
한 주기 = 25초 + 호출시간(60~120초). 그래서 시간당 144회가 아니라 43회다.
👉 1시간 회의(쉼 없이 말한 상한) = 43회 · 2,209,309 tok · $18.66. 그리고 이건 API 환산이고 구독 OAuth 라 실청구는 0원이다. 전사가 안 늘어난 턴(침묵)은 호출을 건너뛰니 실제는 더 적다. 페페가 지금까지 돌린 녹음이 20~30분대였던 것까지 감안하면 「느낌이 없었다」가 정확한 관찰이다.
🔑교훈 = LLM 호출 비용을 「회당 고정 × 고정 주기」로 어림하면 안 된다 — 입력이 누적되는 파이프라인은 회당값이 시간에 따라 커지고, 동기 호출이면 주기도 sleep 값이 아니다. 둘 다 실물로 재야 한다.
⏳[페페] 판단 = TALK_NOTES_SEC 를 올릴지 — 이제 화면의 5분 구간표가 «추정 말고 실제»를 준다.
검증
--selftest-pub35/35(신설) — 발급 멱등 · 128비트 · 접두어 양쪽 200 · 응답 키 == 허용목록 정확히 · 전사문/notes_error미유출 ·/api/items·/ws·/·/app.js·상위경로 전부 404 · 폐기·재발급·만료- 회귀 =
--selftest-notes3/3 ·--selftest-hallu9/9 ·_test_diar_health.py21/21 - 실화면(Playwright) =
_test_pub_ui.py폰412·PC1280 전부 통과 ·_test_share_ui.py전부 통과 - 실제 funnel 왕복 = 공개 200 ·
/mtg/api/items404 · 모르는 토큰 404 · 폐기 후 404 - 포트 장부 =
portgate claim 8448+ funnel 비고 기재. 서버 재기동 = 8445·8448 한 pid 에서 둘 다 LISTENING - 시험 흔적 정리 = 합성 항목
lecture-20260101-000000.json→C:\dev_attic\2026-09-19\talklog-pub-test\· 시험으로 실물 녹음에 걸었던 토큰 전부 폐기(공개 토큰이 걸린 항목 0건 확인)
🆕 같은 날 추가 지시 — 일시정지/재개 · 공개 링크를 폰에서도
[페페] 「녹음 정지, 일시정지 버튼 추가 / 퍼널 링크 모바일 피시 모든 양식에서 볼 수 있게 수정」 (아침의 「모바일에선 안 보이게」를 뒤집은 것 — 그대로 따랐다)
🔑 일시정지는 «서버»가 끊는다
폰 앱(APK) 소스가 이 리포에 없어 pause 명령을 새로 넣을 수 없다 — 앱은 일시정지를 모른 채 계속 올린다.
그래서 웹에서 마이크만 끊으면 앱 녹음에선 일시정지가 아예 안 먹는다. 진짜 차단은
Session.paused → add_pcm() 이 들어온 소리를 버리는 것이고, 창구는 POST /api/items/{ts}/pause 하나다.
ws 가 아니라 HTTP 인 이유 = 폰 앱으로 녹음 중일 때 PC 웹은 소리 없이 watch 로만 붙어 있는데 거기서도 눌러야 한다.
곁들인 것 4개 — ①일시정지 중엔 STALL_SEC 「소리가 끊겼다」 텔레그램을 안 울린다
②paused 를 항목 json 에 안 적는다(적으면 서버 재시작 때 «멈춘 채» 부활해 소리가 조용히 사라진다)
③finalize 진입에서 자동으로 푼다(꼬리 소리 보존) ④웹 경로는 보내지도 않는다(회선에 소리를 안 태운다).
잊고 회의 날리는 걸 막는 3중 신호
상단 점(빨강 펄스 → 주황 정지, sticky) · 액션바 「▶ 재개」 강조색(fixed) · 최상단 배너 + 마이크 칩이 녹음 경과를 동결하고 일시정지 경과를 센다(2분 30초에 진동 1회). 통화차단 배너(빨강·자동복구)와 색도 자리도 가른다.
액션바 2행
공개 링크가 폰에도 생기며 녹음 화면 버튼이 5개가 됐다 → web-designer 실측 412px 한 줄 강제 시 141px 넘침.
폰만 2행(위=녹음 제어 한 쌍, 아래=유틸 3개), PC 는 display:contents 로 한 줄 유지.
footer 가 133px 이 돼 #screen-record .content 하단 여백도 148px 로 올렸다.
곁에서 고친 것
🔴GET /api/items/{ts} 가 녹음 시작 직후 404 였다 — 항목 json 은 «첫 전사 확정»에야 쓰이는데 디스크만 봤다.
api_patch_item 이 이미 쓰던 「살아 있는 세션이 정본」 규약을 읽기 경로에도 적용했다(이번 _test_pause.py 가 밟았다).
검증
C:\dev\talklog\live\_test_pause.py(신설) 전부 통과 — 앱과 같은POST /ingest경로로 올리면서 일시정지 중 올린 3초가 wav 에 안 들어가는 걸 바이트로 확인(64,044 → 64,044), 재개하면 자라고(→128,044), 최종 길이 4.0초(올린 7초 − 버린 3초).C:\dev\talklog\live\_test_share_ui.py전부 통과 — 412 두 행 380/380 · 버튼 5개 전부 1줄(높이 42) · 하단여백 148 > 액션바 133 · 3중 신호 · 1280 한 줄 유지(전 버튼 top 832) · 발급→폐기 왕복(시험분 자동 폐기).- 회귀 =
--selftest-pub35/35 ·--selftest-notes3/3 ·--selftest-hallu9/9 ·_test_diar_health.py21/21.
⏳ [페페] 몫(추가)
- 안드로이드 실기 1회 — 2행 액션바 실측은 헤드리스 크롬(데스크톱 엔진)이다. 한글 폰트 폴백에서
「일시정지」·「공개 링크」 실제 폭 · 네이티브 WebView 에서
display:contents가 먹는지. - 정지(빨강)와 재개(테라코타)가 색약에서 구분되는지.
⏳ 남은 것 (페페 몫)
- 실제 회의에서 공개 링크 1회 시험 — 참석자 폰, 특히 카톡 인앱 브라우저에서 열리는지.
- 회의 초반 이름이 안 잡혀 칩이 「전체」뿐일 때 어색하지 않은지 · 이름 오인이 실제로 나는지.
TALK_NOTES_SEC25초를 올릴지(비용).- 같은 오리진 8788 리포트의 스코프
/서비스워커가/mtg/폴링을 가로채는지 1회 확인(페페 PC 한정).