docs: 발표·특허·AI영상요약 자료 및 작업기록 추가

- 발표자료(좌표투영·측점기반재생) 갱신 + 그림/PDF
- 특허 출원 초안 및 도면
- AI 영상요약 서비스 작업기록(docs/history)
- 관련 scripts 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 11:20:03 +09:00
co-authored by Claude Opus 4.8
parent 5c8b499979
commit fe25ce00f1
78 changed files with 6186 additions and 15 deletions
@@ -0,0 +1,41 @@
# 초기 구동 기본값 변경 — 좌측패널 OFF + 시설등급 1·2종만 체크
**소요 시간**: 약 20분
**Context 사용량**: input ~48k / output ~7k tokens
## 요청
> 처음 플레이어가 구동되면 "좌측패널 OFF"이고, 시설등급은 1종·2종만 체크된 상태여야 한다.
(같은 세션에서 앞서 "로컬 실행 가능 여부", "지도 나침반 가운데버튼 패닝 가능 여부"는 코드 변경 없이 질의응답만 진행.)
## 변경 파일
- `client/src/store/settingsStore.ts`
## 변경 내용
1. **좌측 노선 패널 기본값**: `showRoutePanel: true → false` (초기 OFF)
2. **시설등급 기본 필터**: `DEFAULT_GRADE_FILTER['기타'] true → false`
- 결과: 1종☑ 2종☑ 3종☐ 기타☐ → "1종·2종만 체크" 충족
3. **persist 마이그레이션 추가**: `version: 1` + `migrate()` 신설
- 사유: 설정이 localStorage(`ghivideo.settings`)에 지속되므로, 기본값만 바꾸면 이미 실행해본 브라우저의 옛 저장값(showRoutePanel:true, 기타:true)이 `merge`에서 우선되어 안 바뀜.
- 처리: 과거 저장본(version 0)은 최초 1회 `showRoutePanel:false` + `gradeFilter=기본값`으로 강제 재설정. 이후 사용자의 토글 변경은 정상적으로 지속됨.
- 신규 브라우저(저장값 없음)는 initializer 기본값이 그대로 적용됨.
4. 관련 주석 2곳 갱신.
## 검증
- `npm run build -w client` (tsc + vite build) 성공 — 타입 에러 없음, 220 모듈 변환, dist 재생성.
- Node 20.20.2(nvm)로 빌드.
## 반영 방법
- 프로덕션(55000, PM2): 정적 dist를 서빙하므로 브라우저 **하드 리로드**로 반영(에셋 해시 변경됨). PM2 재시작 불필요.
- 개발(5173): dev 서버가 소스를 직접 읽으므로 즉시 반영.
- 기존 브라우저는 첫 로드 시 마이그레이션이 1회 동작해 좌측패널 OFF·시설등급 1·2종으로 재설정됨(수동 localStorage 삭제 불필요).
## 참고 / 판단 사항
- "처음 구동 시 OFF"를 "매 실행마다 강제 OFF"가 아니라 "초기 기본값 = OFF, 이후 사용자 토글은 기억"으로 해석함. 만약 매 실행마다 무조건 OFF로 시작하길 원하면 `showRoutePanel`을 persist 대상에서 제외(partialize)하는 추가 작업 필요.
- `기타` 등급은 이번에 기본 해제로 바뀌어, `isGradeVisible` 규칙상 '기타' 등급 구조물이 기본 화면에서 숨겨짐(요청 문구 "1종·2종만" 준수 결과).
@@ -0,0 +1,37 @@
# 경영진(사장님) 발표 자료 3장 작성
**소요 시간**: 약 15분
**Context 사용량**: input ~72k / output ~11k tokens
## 요청
- 사장님께 GhiVideo 프로그램을 발표·설명할 예정 → **3장 내외 발표 자료** 작성.
- 근거 자료: `docs/` 폴더에 취합된 기존 문서들.
## 산출물
- `docs/발표_GhiVideo_경영진요약_3장.md` (원본)
- `docs/발표_GhiVideo_경영진요약_3장.html` (자체완결)
- `docs/발표_GhiVideo_경영진요약_3장.pdf` (A4, 한글 임베드, 3페이지)
## 구성 (임원 눈높이 · 각 1페이지)
1. **무엇 / 왜** — 한 줄 요약, 필요성(기존 시간축 탐색의 한계), 핵심 3기능(측점탐색·AR정합·대용량재생), 처리 흐름 도식(SVG)
2. **핵심 기술·차별성** — 대표 4기술, 정량 성과 KPI(부각오차 42.7°→11°, DEM 대비 ~0.2m, 4K 60fps, 왕복오차≈0), DEM-free 차별화, 특허 2건
3. **현황·기대효과·계획·검토요청** — 동작 제품(55000 시연), 기대효과 표, 향후 로드맵, 의사결정 요청(특허 출원 승인·시범적용)
## 근거 문서
- `보고서_GhiVideo_개발내용및핵심기술.md` (핵심 내용·수치·흐름도 SVG 재사용)
- `기술명세_GhiVideo_종합기술문서.md`, 각종 특허/쉬운설명 문서
## 방법 / 검증
- 기존 파이프라인 `scripts/md2docs.sh`(pandoc + weasyprint, report.css)로 HTML/PDF 생성.
- HTML 검증: `break-before: page` 2개(=3페이지) 확인, 흐름도 SVG 포함, 10개 섹션 제목 정상.
- PDF 68KB 정상 생성(렌더 이미지 미리보기 도구 poppler 미설치로 화면 캡처 확인은 생략).
## 참고
- 파일 상단 KPI·차별화 문구는 사장님 대상이라 기술용어 최소화, 효과·비용·의사결정 중심으로 재구성.
- 숫자·특허 현황은 기존 보고서 값 그대로 인용(부각오차·표고 일치도 등).
@@ -0,0 +1,34 @@
# KMZ → 영상 표출 전 과정 그림 설명서 작성
**소요 시간**: 약 20분
**Context 사용량**: input ~95k / output ~14k tokens
## 요청
- "KMZ 파일을 시작으로 동영상에 위치시키기까지의 과정을 논리적으로 그림과 함께 자세히 설명"
## 산출물
- `docs/쉬운설명_KMZ에서_영상표출까지_전과정.md` (+ .html / .pdf, md2docs.sh)
- SVG 도식 6점: 전체 파이프라인 / KMZ 내부구조·Placemark 해부 / 표고 datum / 좌표 3단 변환 / 핀홀 투영 / cover 정렬·평활
## 문서 구성 (8단계 + 보너스)
1. KMZ 압축 해제 — fflate `unzipSync`, `.kml` 우선 (`geoData.ts parseKmz` L295)
2. KML 폴더 트리 재귀 + Placemark 수집 (`walk` L376, `parseKmlDescProps` L276)
3. 폴더명 기반 POI/구조물 분류 — 교량·터널·구교=구조물, 지장물·출입문=POI, KAKAO_RAIL=POI+역사 (`handlePlacemark` L331)
4. 표고 결정 — 드래그보정 > DEM > 최근접 측점 정표고, geoidOffset(대전 25.8m) datum 정합
5. 좌표 변환 — 위경도 → EPSG:5186 TM(proj4) → 드론 기준 상대 ENU (`projectPoint` L334)
6. 회전 — Rz(yaw)·Rx(pitch)·Ry(roll) → 카메라 좌표, Zc≤0/FOV/거리 필터 (`toCameraCoords` L301)
7. 핀홀 투영 — u=0.5+(Xc/Zc)(f/sW) (`pixelFromCamera` L126)
8. 화면 배치 — object-fit:cover 정렬(StationOverlay L948) + 속도적응 평활(smoothStep L49) + 60fps RAF
★ 역투영 드래그 보정 (`worldFromPixel` L149)
## 검증
- 실제 소스(geoData.ts / geoProjection.ts / StationOverlay.tsx)를 읽고 단계·라인 확인 후 작성 (라인은 2026-07-03 기준).
- md2docs.sh 변환 정상(HTML 31KB, PDF 140KB).
## 참고
- 초등학생 수준 눈높이(메모리 기본값) + 파일·라인 근거 병기 형식. 기존 쉬운설명 시리즈와 동일한 스타일.
@@ -0,0 +1,29 @@
# 쉬운설명_프레임기반에서_측점기반_플레이어 — 3절 그림 개선
**소요 시간**: 약 10분
**Context 사용량**: input ~110k / output ~5k tokens
## 요청
- 「3. 핵심 비밀 — 매 장면마다 '시간'과 '측점'을 짝지어 둔다」 절의 그림을 더 명료하고 이해하기 쉽게 재작성.
## 문제 진단 (기존 그림)
- 마름모꼴 화살표 흐름(위 1박스 → 좌우 2박스 → 아래 1박스)이라 시선 동선이 교차함.
- 결과물인 "시간↔측점 표"가 문구로만 존재 — 표(사전)라는 핵심 개념이 시각화되지 않음.
- 표를 "왜 만드는지"(재생 중 측점 표시 / 측점 검색 점프)가 그림에 없음.
## 개선 내용 (docs/쉬운설명_프레임기반에서_측점기반_플레이어.md)
3단 구성으로 교체 (viewBox 720×330):
1. **① 사진 1장마다 두 값 계산** — 프레임 카드(번호+GPS) → ⏱시간(파랑)·📐측점(초록) 두 칩으로 분기
2. **② '시간↔측점' 표 완성** — 실제 표 모양(헤더+5행, 예시행 191초/192초/193초/⋮/348초, 현재행 하이라이트)으로 시각화. "모든 사진에 반복" 화살표로 ①→② 연결
3. **③ 표 하나로 양방향 찾기** — 재생 중("지금 192초, 여긴 어디?"→측점 표시) / 검색("160k130 가줘!"→192초 점프) 두 사용례 배지
- figcaption도 ①②③ 구조에 맞춰 갱신. 색 체계는 문서 기존 규칙 유지(시간=파랑, 측점=초록, 표=앰버, 화살표=보라).
## 검증
- md2docs.sh 재변환 정상 (HTML 24KB, PDF 94KB 재생성).
- SVG 좌표 겹침 검토(패널 간격·표 행 경계·하단 배지) 완료.
@@ -0,0 +1,28 @@
# SVG 내부 빈 줄로 인한 그림 깨짐(빈 화면) 수정
**소요 시간**: 약 10분
**Context 사용량**: input ~125k / output ~6k tokens
## 증상
- 「쉬운설명_프레임기반에서_측점기반_플레이어」 3절 새 그림이 HTML/PDF에서 **빈 상자**로 나오고, 그림 속 글자들이 그림 밖 본문에 줄글로 쏟아짐 (사용자 스크린샷 제보).
## 원인
- pandoc(gfm)은 **raw HTML 블록을 빈 줄에서 종료**함 → `<svg>` 내부에 빈 줄이 있으면 블록이 쪼개져 이후 `<text>` 등의 내용이 마크다운 본문으로 처리됨.
- 직전 작업(3절 그림 개선)에서 가독성용 빈 줄 4개를 SVG 안에 넣은 것이 직접 원인. 추가로 1절 기존 그림(41행)에도 동일 문제 잠재.
## 수정
1. `docs/쉬운설명_프레임기반에서_측점기반_플레이어.md` — SVG 내부 빈 줄 5곳 제거 (41, 72, 86, 91, 113행). 주석(`<!-- -->`)은 빈 줄 없이는 무해하므로 유지.
2. **전수 검사**(awk, docs/*.md 전체)로 동일 문제 추가 발견: `발표_GhiVideo_좌표투영과_측점기반재생.md` 117행 → 제거.
3. 두 문서 md2docs.sh 재변환.
## 검증
- md 전수 검사: SVG 내부 빈 줄 0건.
- 생성 HTML 파이썬 검사: svg 블록 제거 후 본문에 그림 전용 문구 누출 **없음** ✅ (두 문서 모두), 3절 svg에 text 25개 정상 포함.
## 재발 방지 메모
- **SVG를 md에 인라인할 때 `<svg>...</svg>` 사이에 빈 줄 금지.** 구획 구분은 빈 줄 대신 주석 한 줄로.
@@ -0,0 +1,33 @@
# '시간↔측점 대응표' 용어 통일 + 보고서용 단독 그림 출력
**소요 시간**: 약 15분
**Context 사용량**: input ~150k / output ~8k tokens
## 요청
1. "시간↔측점 지도" 명칭 개선 논의 → **"시간↔측점 대응표"** 로 결정, 문서 반영("수정해줘")
2. 사용자 보고서에 넣을 수 있게 **해당 도식만 따로 그려서 출력**
## 변경 내용
### 1. 용어 통일 (지도/표 → 대응표)
- `docs/발표_GhiVideo_좌표투영과_측점기반재생.md` — "시간↔측점 지도" 4곳 + 그림 내 "시간 ↔ 측점 지도 완성!" → "대응표". figcaption에 "재생 기준을 '시간'에서 '스테이션(측점)'으로 전환하기 위한 색인 구조" 문구 추가
- `docs/쉬운설명_프레임기반에서_측점기반_플레이어.md` — "시간↔측점 표" 5곳 + 그림 내 "② '시간 ↔ 측점' 표 완성" → "대응표"
- 특허 초안의 "매핑" 표현은 특허 문체에 적합하므로 유지
- 두 문서 md2docs.sh 재변환 완료
### 2. 단독 그림 산출물 (`docs/그림_시간측점대응표.*`)
- 사용자 보고서 스타일(오피스 단색 박스: 회색/파랑/초록/황금 + 보라 화살표)에 맞춘 SVG 도식 작성
- 사용자 재작성본의 오타 정정 반영: "측정"→"측점" 2곳, 예시 "2368940"→"위도·경도", 하단 "지도"→"대응표"
- 출력 3종: `.md`(원본), `.pdf`(**여백 0, 도식 크기 페이지** — weasyprint 커스텀 @page 270×117.5mm), `.png`(**2143×933 고해상도** — PyMuPDF 렌더)
- PNG 품질 육안 검증 완료 (한글 폰트·화살표·색상 정상)
## 새로 확보한 방법 (재사용 가능)
- **SVG 도식 → 보고서 삽입용 PNG 파이프라인**: 그림 전용 HTML(@page 여백0) → weasyprint PDF → PyMuPDF(fitz) Matrix 2.8배 렌더 → PNG. poppler 없이 동작.
## 남은 사항
- 사용자 자신의 보고서(외부 파일)에는 직접 붙여넣기 필요 — PNG 경로: `docs/그림_시간측점대응표.png`
@@ -0,0 +1,30 @@
# 단독 그림(시간↔측점 대응표) 글자 아랫부분 잘림 수정 — PIL 재렌더
**소요 시간**: 약 10분
**Context 사용량**: input ~165k / output ~5k tokens
## 증상 (사용자 제보)
- `docs/그림_시간측점대응표.png` 글자들의 **아랫부분이 조금씩 잘려** 보임.
## 원인
- 확대 검증 결과 weasyprint(61.2)의 SVG `<text>` 렌더러가 **베이스라인 아래 획(디센더)을 클리핑**함.
- "fps"의 p 꼬리가 잘려 "fos"처럼 보임, "드론·기록" 등 한글 하단 획 일부 손상.
- 이전 파이프라인: 인라인 SVG → weasyprint PDF → fitz PNG. (SVG 텍스트가 weasyprint를 지나는 순간 발생)
## 수정
- 렌더러 교체: **PIL 직접 드로잉** (`ImageFont.truetype` malgun/malgunbd + `anchor='mm'`,
rectangle/line/polygon 화살촉) → `그림_시간측점대응표.png` (2160×940) 재생성.
- PDF: fitz로 PNG 삽입, `deflate=True, garbage=4` 압축 (6MB → 52KB).
## 검증
- PNG 육안 검증: fps 디센더·한글 받침 모두 온전 ✅. 임시 파일(_z*, _zoom*) 정리.
## 파급 참고
- **본문 문서들의 인라인 SVG 그림도 weasyprint PDF에서는 동일하게 디센더가 잘릴 수 있음**
(HTML 보기는 브라우저 렌더라 정상). PDF 품질이 중요한 그림이 생기면 PIL 재렌더 방식 사용.
memory(report-doc-toolchain)에 기록함.
@@ -0,0 +1,30 @@
# 전체 흐름 다이어그램 작성 (보고서 삽입용)
**소요 시간**: 약 10분
**Context 사용량**: input ~180k / output ~6k tokens
## 요청
- 스테이션기반 플레이어의 **전체 흐름을 다이어그램**으로 표현 (사용자 보고서 삽입용).
## 산출물
- `docs/그림_전체흐름도.png` (2400×1100, 고해상도) / `docs/그림_전체흐름도.pdf` (압축)
- `scripts/fig_전체흐름도.py`**재생성 스크립트** (문구·색 수정 후 `python3 scripts/fig_전체흐름도.py` 재실행)
## 다이어그램 구성 (좌→우 4단)
1. **입력 — 데이터 폴더 1개** (회색 그룹): 영상(mp4) / 드론 비행로그(csv, 위치·자세각·초점) / 측점(csv, 실측 정표고) / POI·구조물(kmz) + 노선 보정(json·선택)
2. **① 자동 파싱** (파랑): 인코딩 자동감지 · KMZ 해제
3. **② 좌표·측점 계산** (초록 2박스): 시간↔측점 대응표 생성 / 좌표 투영 계산(드론 위치·자세각 → 화면 픽셀)
4. **③ 화면 출력** (황금 3박스): 측점 스테이션바 / 영상 AR 오버레이 / 대용량 영상 재생
- 하단 엘보 경로: "영상 원본은 계산 없이 곧바로 스트리밍 재생" (계산 우회 표현)
## 기술 방식
- 직전 확립한 **PIL 직접 드로잉** 파이프라인 (weasyprint SVG 디센더 잘림 회피). 폰트 자동 축소(fit) 헬퍼로 박스 폭 초과 방지.
- 용어는 최근 정정 사항 반영: "스테이션기반", "드론 촬영 철도시설물 영상", "자세각", "시간↔측점 대응표".
## 검증
- PNG 육안 검증: 글자 잘림 없음, 배치·화살표 정상 ✅
@@ -0,0 +1,21 @@
# 시간↔측점 대응표 그림 — 단계별 소스 파일 매칭
**소요 시간**: 5분
**Context 사용량**: input 60k / output 4k tokens (추정)
## 질문
`docs/그림_시간측점대응표.png` 그림의 4개 단계(영상 프레임 → 프레임÷fps=시간 / GPS→측점 투영 → 시간↔측점 대응표)에 해당하는 실제 소스 파일이 무엇인지.
## 답변 요약
| 그림 단계 | 파일 |
|-----------|------|
| ① 영상 프레임 + 드론 GPS 기록 (데이터 읽기) | `client/src/utils/geoData.ts` (CSV 파싱), `client/src/store/geoStore.ts` (보관), `client/src/types/geo.ts` (타입) |
| ② 프레임번호 ÷ fps = 시간 | `client/src/utils/timecode.ts` (`frameToSeconds`), `client/src/stationbar/StationBar.tsx` (videoFps 유도 + `f.frame / videoFps`), `client/src/App.tsx:161` (`handleSeek(frame / fps)`) |
| ③ GPS → 측점 (선로에 투영) | `client/src/stationbar/StationBar.tsx` (`projectChainage`, `stationLine`), `client/src/components/overlay/RoutePanel.tsx` (동일 계산), `client/src/utils/chainage.ts` (공유 유틸), 서버측 `server/src/services/geoMatch.ts` |
| ④ 시간 ↔ 측점 대응표 | `client/src/stationbar/StationBar.tsx` (ViewedPoint[] precompute = 대응표 그 자체), `client/src/components/overlay/StationOverlay.tsx` (시간→측점 표시), `client/src/components/geo/GeoSearch.tsx` (측점→시간 검색 이동) |
## 참고
- `utils/chainage.ts` 는 공유 유틸로 존재하나 현재 import 하는 곳이 없음 — StationBar/RoutePanel 이 각자 동일 로직을 내장하고 있음 (통합 여지 있음).
@@ -0,0 +1,18 @@
# 발표자료 HTML→md 변환 요청 — 확인 결과
**소요 시간**: 3분
**Context 사용량**: input 70k / output 2k tokens (추정)
## 요청
`docs/발표_GhiVideo_좌표투영과_측점기반재생.html` 을 md 로도 만들어 달라.
## 확인 결과 (변환 불필요)
- `docs/발표_GhiVideo_좌표투영과_측점기반재생.md`**이미 존재** (13:20 수정, 233줄).
- HTML(13:21)·PDF(13:21)는 이 md 를 원본으로 `scripts/md2docs.sh`(pandoc) 로 생성된 산출물 — 즉 **md 가 원본**이고 html 이 사본.
- 내용 대조: 제목·섹션("한눈에 — GhiVideo는 뭐가 특별한가요?", "시간 ↔ 측점 대응표") 모두 일치 확인 → 새로 만들 것 없음.
## 참고
- 해당 md 서두에 "드론 주행영상 플레이어" 표현이 남아 있음 — 용어 정정 방침('주행영상' 아님, 항공 영상/스테이션기반 플레이어)과 어긋나므로 수정 후보.
@@ -0,0 +1,28 @@
# 시간↔측점 대응표 '상세판' 다이어그램 — 측량 데이터 분리 + 투영 믹싱 상세화
**소요 시간**: 약 15분
**Context 사용량**: input ~200k / output ~7k tokens
## 요청
- 기존 대응표 그림에서 **철도 노선 측량 데이터를 별도 박스로 분리**하고,
**GPS와 믹싱되는 부분(선로 수직 투영)을 더 구체적·상세하게** 표현.
## 산출물
- `docs/그림_시간측점대응표_상세.png` (2200×1420) / `.pdf` (압축)
- `scripts/fig_대응표_상세.py` — 재생성 스크립트
- 기존 단순판(`그림_시간측점대응표.*`)은 그대로 유지 (용도별 선택 사용)
## 구성
- **소스 2종 분리**: 회색 "영상 프레임(번호+GPS)" [드론이 촬영하며 기록] vs 청록 "철도 노선 측량 데이터(측점 목록·01)측점.csv)" [드론과 무관한 측량 성과·별도 입력] — 출처가 다름을 태그·색으로 명시
- **믹싱 패널(초록)**: "드론 GPS를 선로에 수직 투영해 측점값 산출"
- 단계 3개: ① 측점들을 이어 선로 기준선 생성 ② GPS 점을 기준선에 수직 투영 ③ 발끝점의 이정 = 측점값
- 미니 일러스트: 선로(측점 160k000·160k200 점) + 드론 GPS(파랑) + 빨간 점선 수직 투영 + 발끝점(160k130)
- 좌측 파랑(시간 계산) + 하단 황금(대응표 완성)은 기존 구성 유지
## 기술/검증
- PIL 직접 드로잉(디센더 잘림 회피), 점선 헬퍼(dash)·폰트 자동축소(fit) 추가.
- 1차 렌더 검토에서 라벨 2곳 겹침 발견·수정(GPS 라벨 화살표 겹침, 패널 하단 캡션 테두리 초과) 후 재렌더 육안 확인 ✅
@@ -0,0 +1,27 @@
# 플레이어 비교 그림(시간 축 vs 측점 축) — 선명한 단독 이미지로 재렌더
**소요 시간**: 10분
**Context 사용량**: input 90k / output 4k tokens (추정)
## 요청
`쉬운설명_프레임기반에서_측점기반_플레이어.md` 1절의 "① 보통 플레이어(시간 축) vs ② 우리 플레이어(측점 축)" 그림을
**더 선명하게, 글자 안 잘리게** 단독 이미지로 재생성.
## 작업
- 원본: 해당 md 인라인 SVG (720×250). 기존 렌더(weasyprint)는 텍스트 디센더 잘림 이슈 있음
(2026-07-03_1330 히스토리와 동일 원인).
- `scripts/fig_플레이어비교.py` 신규 작성 — **PIL 직접 드로잉, 3배 해상도(2160×760)**.
`fig_전체흐름도.py` 패턴 재사용 (malgun/malgunbd + anchor='mm', fitz PDF 압축 삽입).
- 이모지(😵)는 렌더 가능한 폰트가 없어 캡션에서 제외 (텍스트만 유지).
## 산출물
- `docs/그림_플레이어비교.png` (2160×760, 43KB)
- `docs/그림_플레이어비교.pdf` (37KB, deflate 압축)
## 검증
- 확대 육안 검증: "160k" 배지 원 내부 완전 수용 ✅, "회덕터널"·"대전조차장" 받침 온전 ✅,
시간 라벨(0:00/03:12/09:28)·캡션 잘림 없음 ✅.
@@ -0,0 +1,27 @@
# 좌표 투영(POI 영상 표출) 상세 도식 작성
**소요 시간**: 약 15분
**Context 사용량**: input ~215k / output ~7k tokens
## 요청
- "카메라 영상에 POI를 정확한 위치에 표출하는 기술"을 대응표 상세판과 같은 스타일로 도식화.
## 산출물
- `docs/그림_좌표투영_상세.png` (2200×1520) / `.pdf` (압축)
- `scripts/fig_투영_상세.py` — 재생성 스크립트
## 구성 (대응표 상세판과 짝을 이루는 시리즈)
- **소스 3종**: 회색 "드론 비행로그(csv) — 위치·자세각·초점거리" / 청록 "POI·구조물(kmz) — 명칭·위경도(높이 없음)" / 청록 "측점(csv) — 실측 표고(높이 보완용)"
- **투영 패널(초록)**: "3차원 위치를 그 순간 카메라가 보는 화면의 픽셀로 변환"
- 단계 4: ① POI 높이 보완(최근접 측점 표고, DEM 불필요) ② 좌표 변환(드론 기준 상대위치) ③ 시선 회전(자세각) ④ 핀홀 투영(중앙+(옆÷앞)×(초점÷센서))
- 미니 일러스트: 드론 카메라 + 화면(이미지 평면) + 교량 POI + 주황 점선 광선 → '맺힌 점 = 화면 속 위치'
- **출력(황금)**: "영상 속 정확한 자리에 POI 표출!" — 매 프레임 재계산(60fps)
- 각주: 어긋나면 라벨 드래그 1회 보정(역투영·자동 저장)
## 검증
- 1차 렌더에서 좌측 화살표가 라벨('초점거리')을 관통 → 라벨 이동 후 재렌더, 크롭 확대로 확인 ✅
- 실제 코드 순서(geoProjection.ts: 표고 datum → ENU → 회전 → 핀홀) 반영.
@@ -0,0 +1,21 @@
# 2026-07-03 발끝점 용어 설명 + 특허 초안 요청 확인
**소요 시간**: 5분
**Context 사용량**: input 45k / output 3k tokens (추정)
## 작업 내용
### 1. "발끝점" 용어 설명
- 사용자 질문: 발표 그림에 나온 "발끝점"이 무엇인지
- 답변: 드론 GPS 위치에서 선로 기준선으로 수직으로 내린 선이 선로와 만나는 점(수학 용어 '수선의 발'). 이 점의 이정을 읽어 측점값을 산출.
- 출처: `scripts/fig_대응표_상세.py` 79~103행, `docs/history/2026-07-03_1400_대응표상세판-측량데이터분리-투영믹싱상세.md`
- 참고: 공식 문서에는 '수직 투영점'/'수선의 발' 표기가 더 정확할 수 있음을 안내
### 2. 특허 초안 작성 요청 → 기존 문서 확인
- 요청: "시간→스테이션 매핑 + GPS POI 영상 맵핑 플레이어의 특허 초안 작성"
- 확인 결과: **동일 내용의 초안이 이미 존재**`docs/특허_GhiVideo_명세서초안_좌표투영_측점기반재생.md` (2026-07-02 18:27 작성)
- 제1발명(좌표투영): GPS 위치 POI를 영상 화면 픽셀에 정합·오버레이 (청구항 7개)
- 제2발명(측점기반재생): 시간→측점 매핑 기반 탐색·재생 (청구항 8개)
- md/html/pdf 3종 동기화 상태 확인 (html·pdf가 md보다 최신)
- 부록 A에 인용된 소스 4개 파일(geoProjection.ts, chainage.ts, StationOverlay.tsx, StationBar.tsx) 실존 확인
- 신규 작성 없이 기존 문서 안내로 종료. 수정/보강 요청 시 후속 작업 예정
@@ -0,0 +1,16 @@
# 전체흐름도 검토 — '초점' → '초점거리' 용어 통일
**소요 시간**: 약 5분
**Context 사용량**: input ~230k / output ~2k tokens
## 요청
- 보고서에 삽입한 전체흐름도(그림_전체흐름도)에 수정할 것이 있는지 검토.
## 검토 결과
- 구조·화살표·용어(스테이션기반 정정 사항 포함) 전반 이상 없음.
- 1건 수정: 드론 비행로그 박스 부제 "프레임별 위치·자세각·**초점**" → "**초점거리**"
(상세 도식 2장·보고서 문단과 용어 통일). `scripts/fig_전체흐름도.py` 수정 후 재렌더, 크롭 확인 ✅
- 참고(수정 불요로 판단): 하단 우회 화살표가 입력 그룹 전체에서 출발하는 것처럼 보이나,
라벨("영상 원본은 계산 없이 곧바로 스트리밍 재생")이 대상을 명시하므로 그대로 둠.
@@ -0,0 +1,27 @@
# 특허 문서용 동작 원리 1·2 서술 작성
**소요 시간**: 약 15분
**Context 사용량**: input ~245k / output ~8k tokens
## 요청
- 사용자 보고서(`D:\_project\20260612_abcvideo\docs\스테이션기반 플레이어_260703.html`)를 참고하여,
동작 원리 1(시간을 스테이션으로 표출)·2(POI 영상 맵핑)를 **특허 문서용**으로 작성.
## 진행
1. `/mnt/d/...` 경유로 보고서 HTML 텍스트 추출 — 구조 확인 (□ 프로그램 소개 / 필요 데이터 / 동작 원리 1·2 / 전체 흐름도 / 특허)
2. 기존 `특허_GhiVideo_명세서초안_좌표투영_측점기반재생.md` 목차·청구항 문체 확인
- 대응 관계: 동작 원리 1 ↔ 초안 제2발명(측점 기반 재생), 동작 원리 2 ↔ 초안 제1발명(정합 표출)
3. 특허 명세서 문체(기술 요지 → 단계 (a)~(f) → 종속항 후보)로 재서술
## 산출물
- `docs/특허서술_동작원리1_2.md` (+ .html / .pdf)
- 구성: 동작 원리별 [기술 요지 / 방법 단계 (a)~(f) / 특징적 구성(종속항 후보)] + 결합 관계 출원 전략 메모
- 포함한 특허 포인트: 수직 투영 측점 산출, 데이터 기반 fps 산출, 이동거리 비례 탐색축,
측점 실측 표고 + 지오이드 datum 정합(DEM-free), 사거리 유지 단일 드래그 역투영, 화각 1점 보정, 속도적응 평활
## 참고
- 변리사 검토 전 초안임을 문서 서두에 명시. 세부 수식·도면은 기존 명세서 초안 참조로 연결.
@@ -0,0 +1,25 @@
# 2026-07-03 특허 출원 초안 신규 작성 (그림 4장 포함)
**소요 시간**: 20분
**Context 사용량**: input 90k / output 15k tokens (추정)
## 요청
- 기존 특허 초안과 별개로 **새로 작성**하되, **이미지와 같이 정리**할 것
- 참고 문서: `D:\_project\20260612_abcvideo\docs\스테이션기반 플레이어_260703.html` (HWP 내보내기, 사내 보고서)
- 구성: 프로그램 소개 → 필요 데이터 → 동작 원리 1(시간→스테이션) → 동작 원리 2(POI 맵핑) → 전체 흐름도 → **□ 특허(빈 절)**
- 그림 4장(hd1~hd4) 포함
## 산출물
1. **`scripts/fig_POI투영_상세.py`** (신규) — 참고 문서의 투영 그림(hd3)을 PIL 고해상도(2200×1520)로 재작성
`docs/그림_POI투영_상세.png` / `.pdf` 생성 (소스 3종 → 투영 4단계 ①높이보완 ②좌표변환 ③시선회전 ④핀홀투영 + 미니 일러스트)
2. **`docs/특허_GhiVideo_출원초안_스테이션재생_POI맵핑.md`** (신규) + `.html` + `.pdf` (16페이지, md2docs.sh 변환)
- 참고 문서의 순서·용어를 따름: 발명 1 = 측점(스테이션) 기반 재생, 발명 2 = POI 영상 정합 표시
- 그림 4장 삽입: 그림_플레이어비교 / 그림_시간측점대응표_상세 / 그림_POI투영_상세(신규) / 그림_전체흐름도
- 보고서 "□ 특허" 절에 붙여넣기 좋은 **"2. 특허 요지 (한눈 요약)"** 표 포함
- 기존 초안 대비 추가된 청구 포인트: **POI 높이 보완(최근접 측점 실측 표고, DEM 불필요)**, **드래그 역투영 좌표 보정** — 코드 확인함 (geoProjection.ts 역투영·표고 역산, StationOverlay.tsx 드래그 처리)
- 용어 준수: '스테이션기반 플레이어', '항공영상'(주행영상 아님 — 메모리 ghivideo-video-terminology)
## 참고
- 기존 초안 `docs/특허_GhiVideo_명세서초안_좌표투영_측점기반재생.md`(7/2 작성)는 그대로 유지 (SVG 개념도 버전)
- md 내 이미지 상대경로 때문에 변환은 docs 디렉토리에서 실행해야 함: `cd docs && ../scripts/md2docs.sh <파일.md>`
- 출원 시 그림은 흑백 선도로 재작성 필요(부록 B에 명기)
@@ -0,0 +1,25 @@
# 특허서술 문서에 도면(도 1·도 2) 삽입
**소요 시간**: 약 10분
**Context 사용량**: input ~260k / output ~4k tokens
## 요청
- 특허서술_동작원리1_2 문서에 동작 원리별 그림을 각각 삽입.
## 변경
- `docs/특허서술_동작원리1_2.md`
- 동작 원리 1 기술 요지 뒤: **도 1** = 그림_시간측점대응표_상세.png (+본문 "(도 1 참조)")
- 동작 원리 2 기술 요지 뒤: **도 2** = 그림_좌표투영_상세.png (+본문 "(도 2 참조)")
- `<figure><img style="width:100%">` + 특허식 figcaption("도 N. …") 형식, 블록 내 빈 줄 없음(pandoc 규칙 준수)
## 이슈/해결
- 저장소 루트에서 md2docs.sh 실행 시 pandoc `--self-contained`가 **이미지를 cwd 기준으로 탐색**해 변환 실패(HTML 미갱신).
→ **docs 디렉토리에서 실행**하면 정상. (md2docs.sh는 상대 이미지가 있는 md의 경우 md가 있는 폴더에서 실행할 것)
## 검증
- HTML에 base64 이미지 2장 임베드 확인(415KB), PDF 4쪽(354KB) 생성.
- PDF 페이지 렌더 육안 검증: p1 도 1, p3 도 2 배치·캡션·본문 흐름 정상 ✅
@@ -0,0 +1,47 @@
# AI Video Brief — 온라인 영상 음성다운로드·AI요약 서비스 신규 구축
**소요 시간**: 35분
**Context 사용량**: input 500k / output 20k tokens (추정값)
## 요청 내용
유튜브 등 온라인 영상 URL을 입력하면 ① 음성(mp3) 다운로드 ② 내용 AI 요약을 해주는 서비스 구축.
## 사용자 결정 사항
- 위치: 별도 새 프로젝트 `/home/hanmac/projects/gitea/b23042/ai_video_brief`
- 텍스트 확보: **자막 우선, 없으면 Whisper 음성인식 대체**
- 요약 AI: **로컬 무료 모델 (Ollama)** — 유료 API 대신
- 형태: 웹 화면
## 구축 내용
### 스택
- Python 3.10 venv (`/usr/bin/python3.10` — anaconda 3.8은 구버전이라 회피)
- FastAPI + uvicorn (포트 **55100**, GhiVideo 55000과 충돌 없음)
- yt-dlp 2026.7.4 (다운로드·자막), faster-whisper 1.2.1 (small/CPU/int8)
- Ollama v0.31.1 + **exaone3.5:7.8b** (LG 한국어 특화 모델, 4.8GB)
### 파이프라인
1. `download`: yt-dlp → bestaudio → ffmpeg mp3 변환 → 다운로드 링크
2. `summarize`: 자막(수동→자동, ko→en) 탐색 → 없으면 mp3 다운로드 후 Whisper 받아쓰기 → 6,000자 초과 시 부분요약→최종요약(map-reduce) → EXAONE 한국어 요약
3. 작업은 백그라운드 스레드 + 웹에서 1.5초 폴링으로 진행률 표시
### 특이사항 (다음 에이전트 참고)
- **sudo 사용 불가** (비밀번호 필요) → python3.10-venv apt 설치 실패 → `pip install --user virtualenv`로 우회
- Ollama 최신 릴리스는 `.tgz`가 아닌 **`.tar.zst`** 형식 → anaconda의 zstd로 해제, `~/ollama/bin/ollama`에 수동 설치 (systemd 서비스 없음, run.sh가 자동 기동)
- GPU 없음 → Whisper·Ollama 모두 CPU 동작 (RAM 31GB로 충분)
## 테스트 결과
- ✅ mp3 다운로드: 19초 영상 → 0.4MB mp3 정상 생성
- ✅ Whisper 받아쓰기: 19초 영상 정확히 전사
- ✅ 요약 전 과정: 자막 발견 → EXAONE 한국어 요약 생성 (약 10초 소요)
- ✅ 웹 화면 http://localhost:55100 정상 서빙
## 실행 방법
```bash
cd /home/hanmac/projects/gitea/b23042/ai_video_brief && ./run.sh
# 브라우저: http://localhost:55100
```
@@ -0,0 +1,38 @@
# AI Video Brief — 요약 보관함(좌측 목록 + DB 저장) 추가
**소요 시간**: 20분
**Context 사용량**: input 550k / output 30k tokens (추정값)
## 요청 내용
왼쪽에 요약된 영상의 제목 리스트를 보여주고, 클릭 시 해당 요약본을 본문에 표시.
요약 결과는 DB/파일에 저장해서, 이미 요약된 항목은 로컬 저장본을 불러와 표시.
## 구현 내용 (`ai_video_brief` 프로젝트)
### 백엔드
- `app/db.py` 신규 — SQLite 보관소 (`storage/summaries.db`, 파이썬 내장 sqlite3, 추가 의존성 없음)
- summaries 테이블: id, video_id, url, title, duration, source, summary, transcript, created_at
- `app/main.py`
- 요약 파이프라인 시작 시 `video_id`(없으면 URL)로 저장본 조회 → 있으면 **계산 없이 즉시 반환** (`result.cached=true`)
- 요약 완료 시 DB에 자동 저장 (받아쓴 원문 포함)
- 신규 API: `GET /api/summaries`(목록), `GET /api/summaries/{id}`(상세), `GET /api/summaries/{id}/transcript`(원문), `DELETE /api/summaries/{id}`(삭제)
- `POST /api/jobs``force` 옵션 — 저장본 무시하고 재요약
### 웹 화면 (2단 레이아웃으로 개편)
- 좌측 사이드바: 📚 요약 보관함 — 제목/날짜/길이/출처 목록, 클릭 시 본문에 표시
- 우측 본문: URL 입력 + 진행 중 작업 + 선택된 요약 상세
- 상세 화면 버튼: 📋 복사 / 📄 원문 보기 / 🔄 다시 요약(force) / 🗑 삭제
- 요약 작업 완료 시 목록 자동 갱신 + 해당 요약 자동 표시, 저장본 사용 시 "💾 저장본에서 불러옴" 배지
## 테스트 결과
- ✅ 요약 → DB 저장 → 목록 노출 확인
- ✅ 같은 영상 재요청 → 2초 내 저장본 반환 (`cached: True`)
- ✅ 상세/원문/삭제 API 정상
- ✅ 새 2단 화면 정상 서빙
## 다음 에이전트 참고
- `pkill -f "uvicorn app.main"`을 서버 재시작 명령과 **한 줄에 쓰면 안 됨** — 명령 문자열 자체가 패턴에 걸려 자기 셸이 죽는다 (exit 144). 별도 호출로 분리할 것.
- 서버 재시작: 구 프로세스 종료 후 `cd ai_video_brief && ./run.sh` 또는 nohup uvicorn.
@@ -0,0 +1,43 @@
# AI Video Brief — PWA(폰 설치형 앱) + 영상(mp4) 다운로드 기능 추가
**소요 시간**: 25분
**Context 사용량**: input 600k / output 40k tokens (추정값)
## 요청 내용
1. 안드로이드 앱으로 만들기 → 추천안(PWA 설치형 웹앱 + 네트워크 설정) 승인받아 진행
2. (작업 중 추가 요청) 영상 자체 다운로드 기능도 추가
## 구현 내용 (`ai_video_brief` 프로젝트)
### PWA (폰 홈 화면 설치형 웹앱)
- 앱 아이콘 3종 PIL로 생성 (192/512/maskable-512, 파랑→보라 그라데이션 + 재생버튼 도안)
- `manifest.json` (standalone, 한국어, short_name "영상요약")
- `sw.js` 서비스워커 — 화면은 네트워크 우선+캐시 폴백, `/api/`는 캐시 안 함
- FastAPI에 `/manifest.json`, `/sw.js` 루트 라우트 추가 (SW 범위 확보), `/static` 마운트
- index.html: manifest/theme-color/아이콘 연결, SW 등록, `beforeinstallprompt` 시 "📲 앱으로 설치" 버튼 표시
- 모바일 CSS: 폰에서 입력창이 위·보관함이 아래(order), 버튼 확대
### 폰 접속 설정
- `windows-setup/포트열기.ps1` — 관리자 PowerShell에서 1회 실행:
WSL IP 자동 감지 → netsh portproxy(55100) + 방화벽 규칙 + 폰 접속 주소 출력
- WSL2 IP는 재부팅마다 바뀌므로 재부팅 후 접속 안 되면 스크립트 재실행 (스크립트가 자동 처리)
- README에 단계별 안내 추가: ① 포트 열기 ② 홈 화면 추가 ③ (선택) Tailscale — https 확보 시 완전한 standalone 설치 + 집 밖 접속
### 영상(mp4) 다운로드
- `config.py`: `VIDEO_DIR`, `VIDEO_MAX_HEIGHT`(기본 1080)
- `downloader.download_video()`: bestvideo(mp4)+bestaudio(m4a) → ffmpeg 병합 mp4
- `main.py`: task `"video"` 파이프라인 추가, `/api/download/{id}`가 mp3/mp4 모두 서빙
- 웹 화면: 🎬 영상 다운로드 버튼(청록색) 추가, 작업명/파일 라벨 맵 처리
## 테스트 결과
- ✅ manifest/sw.js/아이콘 모두 200 + 올바른 content-type
- ✅ 서버 재시작 후 보관함 데이터(SQLite) 유지 확인
- ✅ 영상 다운로드: 테스트 영상 → mp4 0.5MB 저장, `/api/download` 200 video/mp4
## 다음 에이전트 참고
- **PWA 설치의 한계**: `http://LAN IP` 접속은 보안 컨텍스트가 아니라 Chrome이 "바로가기" 형태로만 추가됨 (주소창 보임). 완전한 standalone 설치는 https 필요 → README의 Tailscale 3단계 (`tailscale serve`) 안내 참조.
- 윈도우 쪽 명령(관리자 netsh/방화벽)은 에이전트가 실행 불가 → 사용자가 `포트열기.ps1` 직접 실행해야 함.
- `pkill -f "uvicorn app[.]main"` 은 반드시 서버 시작 명령과 **별도 Bash 호출**로 (같은 호출에 start 명령이 있으면 그 문자열에 패턴이 걸려 자기 셸이 죽음, exit 144 재발함).
@@ -0,0 +1,14 @@
# AI Video Brief — 작업 시작 후 주소 입력창 자동 삭제 해제
**소요 시간**: 2분
**Context 사용량**: input 620k / output 42k tokens (추정값)
## 요청 내용
영상/음성/요약 버튼을 눌러도 입력한 주소가 지워지지 않게.
## 수정 내용
- `ai_video_brief/app/static/index.html``submitJob()`에서 작업 시작 후 `$('url').value = ''`
입력창을 비우던 한 줄 제거. 같은 주소로 영상→음성→요약을 연달아 실행하기 편해짐.
- 서버는 index.html을 매 요청마다 읽으므로 재시작 불필요 — 새로고침만으로 반영 확인 완료.
@@ -0,0 +1,29 @@
# AI Video Brief — 자막 다운로드 429 오류 시 요약 실패 문제 수정
**소요 시간**: 6분
**Context 사용량**: input 650k / output 45k tokens (추정값)
## 문제
요약 버튼 클릭 시 `ERROR: Unable to download video subtitles for 'en': HTTP Error 429: Too Many Requests` 로 작업 전체가 실패.
- 원인 1: 유튜브가 자막을 자주 요청하는 IP를 일시 차단(429). 기존 코드는 언어 4개(ko/ko-KR/en/en-US)의 수동+자동 자막을 한꺼번에 요청해서 차단에 걸리기 쉬웠음.
- 원인 2: 자막 다운로드 예외가 그대로 전파되어, Whisper 음성인식 폴백이 있는데도 작업이 죽음.
## 수정 (`ai_video_brief`)
`app/services/downloader.py``fetch_subtitles()` 재작성:
1. 자막 **목록만 먼저 조회**(다운로드 아님) → 우선순위에 맞는 언어 **딱 1개만** 선택해 다운로드 (요청 수 최소화, `sleep_interval_subtitles: 1` 추가)
2. 함수 전체를 try/except로 감싸 **어떤 오류든 None 반환** → 호출부(main.py)가 자동으로 Whisper 음성인식으로 진행
3. 수동 자막 우선, 없으면 자동 생성 자막 (기존 우선순위 유지)
`app/main.py` — 진행 메시지를 "자막을 못 가져와 소리를 내려받는 중 (음성인식으로 진행)"으로 명확화.
## 테스트
- ✅ 강제 재요약(force) 전 과정 통과: 자막 1개 선택 다운로드 → 요약 완료 (16초)
- ✅ 예외 시 None 폴백 경로는 코드 보장 (Whisper 경로는 이전 세션에서 검증 완료)
## 다음 에이전트 참고
- 429는 시간이 지나면 자동으로 풀림. 앞으로 자막 실패는 사용자에게 오류로 보이지 않고 음성인식으로 조용히 전환됨 (원문 출처 표시로 구분 가능: "자막" vs "음성인식(Whisper)").
@@ -0,0 +1,28 @@
# AI Video Brief — 좌측 목록 삭제·이름 수정 기능 추가
**소요 시간**: 5분
**Context 사용량**: input 680k / output 48k tokens (추정값)
## 요청 내용
좌측 요약 보관함 목록에서 항목 삭제와 목록 이름(제목) 수정 기능.
## 구현 내용 (`ai_video_brief`)
### 백엔드
- `db.update_title(rec_id, title)` 추가
- `PUT /api/summaries/{id}``{title}` 로 이름 수정 (빈 제목 400, 없는 항목 404)
- 삭제는 기존 `DELETE /api/summaries/{id}` 재사용
### 웹 화면
- 좌측 목록 각 항목 오른쪽에 ✏️(이름 수정)·✕(삭제) 버튼 추가
- 항목 본문(li-body)과 버튼(li-actions)을 분리해 버튼 클릭이 항목 선택으로 번지지 않게 stopPropagation 처리
- 이름 수정: prompt 창에 현재 제목 미리 채움 → 저장 후 목록·본문 제목 즉시 갱신
- 삭제: 확인창에 대상 제목 표시, 선택 중이던 항목이면 본문도 닫음
- 본문 상세 화면에도 "✏️ 이름 수정" 버튼 추가 (목록과 동일 동작)
## 테스트 결과
- ✅ PUT 이름 수정 → 목록에 반영 확인
- ✅ 빈 제목 400 / 없는 항목 404 정상
- ✅ 화면에 rename 함수·버튼 렌더 확인, 테스트 후 원래 제목 복원
@@ -0,0 +1,33 @@
# AI Video Brief — "왜 60프레임이 아니냐" 질문 조사 + 화질/프레임 표시 추가
**소요 시간**: 10분
**Context 사용량**: input 720k / output 52k tokens (추정값)
## 질문
저장한 동영상이 60fps가 아니라 30fps 정도밖에 안 되는 이유.
## 조사 결과 (결론: 다운로드 문제 아님, 원본이 24fps)
- ffprobe로 저장 파일 확인: 사용자가 받은 영상들은 1080p **23.98fps**(AV1), 1920x960 **29.97fps**
- 해당 영상("1시간으로 끝내는 DJI 미니 5 프로 사용 설명서", oItten2naxk)의 유튜브 제공 포맷 전수 조사:
144p~2160p(4K) **전부 24fps만 존재** — 원본이 24fps로 업로드된 영상
- 유튜브는 원본보다 높은 프레임을 생성하지 않으므로 60fps는 불가능. 다운로더는 제공되는 최고 품질(1080p24)을 정상적으로 받았음
- 추가 발견: 이 영상은 4K까지 제공되지만 기본 상한 `VIDEO_MAX_HEIGHT=1080`이라 1080p로 받음 (환경변수로 조정 가능)
## 보완 구현
1. `downloader.download_video()` — 포맷 선택에서 `ext=mp4` 코덱 제한 제거
(`bestvideo[height<=H]+bestaudio` → mp4 통에 병합). vp9만 60fps를 제공하는 옛 영상도 프레임을 놓치지 않게.
반환값을 `(경로, {height, fps})` 튜플로 변경.
2. `main.py``result["quality"]`에 "1080p 24fps" 형식 저장
3. 웹 화면 — 다운로드 완료 링크에 실제 받은 화질/프레임 표시: "⬇️ mp4 저장 (240p 15fps · 0.5MB)"
## 테스트
- ✅ 테스트 영상 다운로드 → "240p 15fps · 0.5MB" 정확히 표시 (2005년 저화질 영상이라 정상)
## 다음 에이전트 참고
- 사용자가 프레임/화질 관련 질문 시: 먼저 `ffprobe`로 저장 파일 확인 → yt-dlp `extract_info`로 유튜브 제공 포맷 목록과 비교. 원본 fps 이상은 어떤 방법으로도 불가능.
- 4K로 받고 싶다는 요청이 오면 `VIDEO_MAX_HEIGHT=2160` 환경변수 (run.sh 실행 전 export).
@@ -0,0 +1,38 @@
# AI Video Brief — 화질/음질 선택 다운로드 기능 추가
**소요 시간**: 9분
**Context 사용량**: input 780k / output 58k tokens (추정값)
## 요청 내용
다운로드 가능한 영상 해상도들을 항목으로 보여주고 클릭 시 그 화질로 다운로드.
음성도 품질 목록을 보여주고 사용자가 선택.
## 구현 내용 (`ai_video_brief`)
### 백엔드
- `downloader.list_formats(url)` — 유튜브 제공 포맷을 조회해
영상: (해상도, fps) 조합별 대표 1개 (video-only 우선 + 코덱 우선순위 av01>vp9>avc1, 예상 크기 포함)
음성: 비트레이트별 대표 1개 (kbps, 확장자, 예상 크기)
- `GET /api/formats?url=...` — 선택 화면용 목록 API
- `POST /api/jobs` 확장: `format_id`(선택 스트림), `video_only`(병합 필요 여부), `quality_label`(표시용)
- `download_video`: format_id 지정 시 `{fid}+bestaudio/...` 폴백 체인, 진행형(소리 포함) 스트림은 `{fid}/best`
- `download_audio`: format_id 지정 시 해당 스트림 → mp3 변환
### 웹 화면
- 🎬/🎵 버튼 클릭 → 즉시 다운로드 대신 **품질 선택 패널** 표시
- "품질 목록을 가져오는 중…" 로딩 → 항목 목록 (최상단 항목에 "추천" 배지)
- 영상: "1080p 24fps · av01 · 약 537MB", 음성: "129kbps · m4a · 약 56MB" 형식
- 항목 클릭 → 그 품질로 다운로드 시작, 닫기 버튼으로 취소
- 완료 후 링크에 선택/실제 품질 표시
## 테스트 결과
- ✅ 목록 API: DJI 영상 → 영상 8개(144p~4K, 크기 포함) / 음성 4개 정상
- ✅ 영상 선택 다운로드: 144p 15fps 선택 → ffprobe로 정확히 144p/15fps 수신 확인
- ✅ 음성 선택 다운로드: 47kbps 선택 → 완료, 품질 라벨 표시 확인
## 다음 에이전트 참고
- 4K 등 1080p 초과 화질도 이제 선택 패널에서 받을 수 있음 (`VIDEO_MAX_HEIGHT`는 '자동 최고' 경로에만 적용)
- 영상 항목의 예상 크기는 영상 스트림만의 크기 (소리 병합 시 소폭 증가) — "약" 표기로 처리
@@ -0,0 +1,21 @@
# AI Video Brief — "유튜브에 60프레임이 없나?" 질문 검증·답변
**소요 시간**: 4분
**Context 사용량**: input 820k / output 60k tokens (추정값)
## 질문
유튜브 영상은 60프레임이 없는지, 왜 선택 목록에 30프레임까지만 보이는지.
## 검증 (코드 수정 없음)
- yt-dlp의 유튜브 검색(ytsearch)으로 실제 60fps 영상을 찾음:
"Real 4K HDR 60fps: LG Jazz HDR UHD" (mkggXE5e2yk)
- 우리 `/api/formats` 로 조회 → 선택 목록에 **2160p 60fps ~ 360p 60fps** 정상 표시 확인
## 결론 (사용자 안내)
- 프레임 수는 **영상 제작자가 촬영·업로드할 때 결정**되며, 유튜브는 원본보다 높은 프레임을 만들지 않음
- 사용자가 넣었던 영상들(DJI 강의 24fps 등)은 원본 자체가 24~30fps → 목록에 그것만 보인 것
- 60fps로 제작된 영상(게임·스포츠·테스트 영상 등)을 넣으면 목록에 60fps 항목이 정상적으로 뜸 (검증 완료)
- 시스템 이상 없음, 수정 사항 없음
@@ -0,0 +1,28 @@
# AI Video Brief — 다운로드 파일명을 "컨텐츠명_스펙"으로 변경
**소요 시간**: 3분
**Context 사용량**: input 860k / output 63k tokens (추정값)
## 요청 내용
음성/영상 저장 시 파일명을 컨텐츠 이름으로 하고, 이름 끝에 "_" 뒤에 화질/음질 스펙을 붙여서 저장.
## 구현 내용 (`ai_video_brief`)
`app/main.py``download_file()` (`GET /api/download/{id}`):
- 기존: `{안전한제목}{ext}`
- 변경: `{안전한제목}_{스펙}{ext}`
- 스펙은 `job.result["quality"]`에서 공백 제거 (`"1080p 24fps"``1080p24fps`, `"47kbps"`)
- 스펙이 없으면 기존처럼 제목만
- 제목 안전화 필터에서 불필요한 `한` 문자 제거 (한글은 `isalnum()`으로 이미 통과)
## 테스트 결과
- ✅ 영상: `Me at the zoo_144p15fps.mp4`
- ✅ 음성: `Me at the zoo_47kbps.mp3`
- Content-Disposition의 `filename*=utf-8''` 로 한글/유니코드 제목도 정상 처리
## 참고
- 화질/음질 스펙은 사용자가 선택 패널에서 고른 라벨 기준 (자동 다운로드 경로도 실제 받은 화질을 quality에 기록하므로 스펙이 붙음)
- mp3는 소스 비트레이트를 라벨로 쓰지만 실제 인코딩은 192kbps 고정 (기존 동작, 이번 변경 범위 아님)
@@ -0,0 +1,42 @@
# AI Video Brief — 음성 선택지 코덱 효율 반영 추천 정렬
**소요 시간**: 4분
**Context 사용량**: input 900k / output 66k tokens (추정값)
## 요청 내용
음성 목록에서 같은 상황일 때 자동으로 가장 음질 좋은 걸 "추천"으로 더 똑똑하게 골라주도록.
## 배경
기존 음성 정렬은 단순히 비트레이트(abr) 내림차순 → 코덱 효율 차이를 무시.
Opus는 AAC보다 ~1.3배 효율적이라, 105kbps Opus ≈ 130kbps AAC 수준인데도 순위에서 밀렸음.
## 구현 내용 (`ai_video_brief`)
`app/services/downloader.py`:
- `_ACODEC_INFO` / `_audio_codec()` 추가 — acodec → (표시이름, 효율계수)
- Opus 1.3, AAC 1.0, Vorbis 0.9, MP3 0.85, Dolby 1.0
- `list_formats()` 음성 파트:
- 각 항목에 `codec`(표시이름), `score`(= abr × 효율계수, 실제 체감 음질) 추가
- 중복 제거 키를 (코덱, abr)로 변경 → 같은 코덱+비트레이트만 병합, 코덱 다르면 둘 다 유지
- 정렬을 `-score, -abr` 순으로 (실제 음질 높은 순, 동점 시 비트레이트 높은 순)
`app/static/index.html`:
- 음성 항목 메타에 코덱 표시 추가: "Opus · webm · 약 45MB"
## 테스트 결과 (DJI 영상)
```
105kbps Opus(webm) 점수=136 ← 추천(맨위) ※ 예전엔 129kbps AAC가 맨위였음
129kbps AAC(m4a) 점수=129
50kbps Opus(webm) 점수=65
49kbps AAC(m4a) 점수=49
```
Opus 105kbps가 AAC 129kbps보다 위로 올라와, 실제 음질 기준으로 정확히 추천됨.
## 참고
- 최종 저장은 여전히 mp3 192kbps 재인코딩 (소스 음질이 좋을수록 mp3 결과도 좋음)
- 효율계수는 일반적 통념 기반의 근사값 (정밀 측정값 아님)
@@ -0,0 +1,39 @@
# AI Video Brief — 다운로드 기록 목록 + 다시 받기 기능
**소요 시간**: 12분
**Context 사용량**: input 960k / output 72k tokens (추정값)
## 요청 내용
음악이든 영화든 다운로드 받았던 것을 히스토리 목록으로 저장해 다시 받을 수 있게.
## 구현 내용 (`ai_video_brief`)
### 백엔드
- `db.py`: `downloads` 테이블 신규 (id, video_id, url, title, kind, quality, format_id, video_only, size_mb, created_at)
- `save_download()` — 같은 (video_id/url, kind, quality)면 옛 기록 지우고 갱신(중복 방지)
- `list_downloads()` / `get_download()` / `delete_download()`
- `main.py`:
- 다운로드 완료 시 `_save_download_record()` 로 기록 저장 (음성/영상 파이프라인 둘 다)
- `GET /api/downloads` — 기록 목록 + 각 항목 `file_exists`(파일이 아직 디스크에 있는지) 계산
- `DELETE /api/downloads/{id}` — 기록 + 저장 파일 함께 삭제
- `download_file()` 개선 — 메모리 작업이 없으면(서버 재시작 등) DB 기록에서 제목/품질 가져와 파일명 생성
### 웹 화면
- 좌측에 "⬇️ 다운로드 기록" 카드 추가 (요약 보관함 아래)
- 항목: 🎬/🎵 + 제목 + 품질 + 날짜 + 크기, 파일 없으면 "파일없음" 표시
- 파일 있으면 ⬇️(즉시 저장, 직접 링크), 없으면 🔄(저장했던 품질 그대로 다시 받기)
- ✕ 버튼: 기록+파일 삭제
- 다운로드 작업 완료 시 기록 목록 자동 새로고침
## 테스트 결과
- ✅ 영상 다운로드 → 기록에 저장 (제목/품질/크기/파일있음=True)
- ✅ 파일 강제 삭제 → file_exists=False 로 전환, format_id/video_only 보존(다시받기 가능)
- ✅ DELETE → 기록·파일 삭제 확인
- ✅ 화면에 다운로드 기록 UI 정상 렌더
## 다음 에이전트 참고
- "다시 받기"는 파일이 있으면 즉시 저장, 없으면 저장된 format_id로 유튜브에서 재다운로드 (새 job id로 새 파일 생성 → 기록은 dedupe로 갱신)
- 다운로드 기록과 요약 보관함은 별도 테이블 (downloads vs summaries)
@@ -0,0 +1,33 @@
# AI Video Brief — 다운로드 파일명이 제목으로 안 나오는 문제 수정
**소요 시간**: 10분
**Context 사용량**: input 1000k / output 78k tokens (추정값)
## 문제
저장은 되는데 파일명이 영상/음원 제목이 아니라 작업 id로 저장됨.
## 원인 (재현으로 확인)
- Starlette `FileResponse(filename=...)`는 한글 등 비ASCII 파일명일 때
`Content-Disposition: attachment; filename*=utf-8''<인코딩>` **한 형식만** 출력하고
구형/모바일 브라우저·다운로드관리자·curl이 인식하는 기본 `filename="..."` 형식을 빠뜨림
- `filename*`만 있으면 이를 못 읽는 클라이언트는 **URL 마지막 경로(작업 id)로 저장** → 문제 발생
- `curl -OJ` 재현: 파일이 `099fe812f033` 로 저장됨 (제목 아님)
## 수정 (`ai_video_brief/app/main.py`)
- `_content_disposition()` 헬퍼 추가 — RFC 6266 방식으로 **두 형식 모두** 출력:
`attachment; filename="<ASCII대체>"; filename*=utf-8''<UTF-8>`
- `filename*` : 최신 브라우저가 사용 → 완전한 한글 제목
- `filename` : ASCII 대체(한글은 빠지고 스펙/확장자는 유지) → 구형/모바일/curl용, 최소한 작업 id는 아님
- `download_file()``filename=` 대신 이 헤더를 직접 지정
## 테스트 결과
```
헤더: content-disposition: attachment; filename="1 DJI 5 _144p24fps.mp4"; filename*=utf-8''1%EC%8B%9C...설명서_144p24fps.mp4
curl -OJ 저장: "1 DJI 5 _144p24fps.mp4" (예전엔 "099fe812f033")
```
- ✅ 최신 브라우저(Chrome/Edge/Firefox)는 filename* 우선 → "1시간으로 끝내는 DJI 미니 5 프로 사용 설명서_144p24fps.mp4" 전체 한글 제목으로 저장
- ✅ filename*를 못 읽는 클라이언트도 읽을 수 있는 ASCII 이름으로 저장 (작업 id 아님)
@@ -0,0 +1,31 @@
# AI Video Brief — 목록 항목 선택 시 컨텐츠 주소 입력창 자동 채움
**소요 시간**: 5분
**Context 사용량**: input 1040k / output 82k tokens (추정값)
## 요청 내용
히스토리(요약 보관함)나 다운로드 기록 항목을 선택하면 그 컨텐츠 주소를 위 입력창에 붙여넣기.
## 구현 내용 (`ai_video_brief`)
### 백엔드
- `db.list_summaries()` SELECT에 `url` 추가 (다운로드 기록 목록은 이미 url 포함)
### 웹 화면
- `fillUrl(u)` 헬퍼 추가 — 입력창(#url)에 주소 채우고 노란색 플래시 애니메이션 + 스크롤(block:nearest)
- CSS `input.flash` / `@keyframes urlflash` — 입력창 잠깐 노랗게 강조
- 요약 보관함 항목 클릭: `selectSummary()` 시작 시 `listCache[id].url`로 주소 자동 채움 (요약 본문 표시는 그대로 유지)
- 다운로드 기록 항목 본문 클릭: 기존 즉시 재다운로드 → **주소 채우기**로 변경
(실제 다운로드는 우측 ⬇️/🔄 버튼이 담당하므로 역할 분리)
## 테스트 결과
-`/api/summaries` 응답에 url 포함 확인 (예: youtube watch url)
-`/api/downloads` 응답에 url 포함 확인
- ✅ 화면에 fillUrl 연결 확인
## 동작
- 왼쪽에서 항목을 누르면 위 주소창에 그 컨텐츠 URL이 채워지고 잠깐 노랗게 반짝임
- 이어서 🎬/🎵/📝 버튼으로 원하는 작업을 바로 실행 가능
@@ -0,0 +1,44 @@
# AI Video Brief — 컴퓨터 켤 때 자동 시작 설정
**소요 시간**: 15분
**Context 사용량**: input 1100k / output 90k tokens (추정값)
## 요청 내용
컴퓨터를 켜면 자동으로 서비스가 구동되게.
## 환경 확인
- WSL 배포판: **Ubuntu-22.04** (`$WSL_DISTRO_NAME`)
- Windows 활성 사용자: **User** (시작폴더 `C:\Users\User\...\Startup`)
- 에이전트가 sudo 불가 → 무권한 방식(시작프로그램 폴더 + VBS) 채택
## 구현 내용 (`ai_video_brief`)
### `start-service.sh` (신규)
- Ollama·uvicorn(55100)을 켠다. 이미 켜져 있으면 건드리지 않음(idempotent).
- **핵심: `setsid`로 프로세스를 새 세션으로 완전 독립**시킴.
- 처음엔 `nohup ... &`만 썼더니 wsl.exe 명령이 끝날 때 uvicorn이 함께 종료됨(로그도 비어 있음).
- `setsid bash -c "exec ... > log 2>&1" &` 로 바꾸니 wsl 세션 종료 후에도 서버 생존.
- 뜰 때까지 짧게 대기 루프(부팅 시 프로세스 안착 보장).
### Windows 시작프로그램 (신규)
- `C:\Users\User\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\AI-Video-Brief.vbs`
- 내용: `WScript.Shell.Run "wsl.exe -d Ubuntu-22.04 -u hanmac bash -lc ""...start-service.sh""", 0, False`
- 창 숨김(0), 비대기(False) → 로그인 시 조용히 실행
### README
- "컴퓨터 켤 때 자동 시작" 절 추가 (동작·확인·해제 방법, 폰 접속은 별도 안내)
## 테스트 결과
- ✅ start-service.sh 직접 실행 → Ollama·웹서버 둘 다 ON
- ✅ wsl.exe 경유 실행 후 세션 종료 → setsid 덕분에 두 서비스 생존 확인 (uvicorn PID 유지)
- ✅ 실제 경로(VBS를 cscript로 실행) → 3초 내 둘 다 ON
- (참고: PowerShell Start-Process로 VBS 경로에 공백이 있어 테스트가 한 번 실패했으나, Windows 시작폴더 자체 실행은 경로를 정상 처리하므로 실사용은 문제없음)
## 다음 에이전트 참고
- **자동시작은 WSL 안의 서비스만** 켬. 폰(LAN) 접속용 포트포워딩(netsh)은 관리자 권한이라 자동화 제외 — 재부팅 후 `포트열기.ps1` 수동 실행 필요.
- 로그인(user logon) 트리거임 — 로그인 전 부팅 단계에서는 안 뜸(일반적 "컴퓨터 켜면"에는 부합).
- setsid 없이 nohup만으로는 wsl.exe 호출 종료 시 백그라운드 프로세스가 죽는다 (WSL 특성). 반드시 setsid.
@@ -0,0 +1,22 @@
# 2026-07-08 데이터 폴더 입력 명세 작성
**소요 시간**: 15분
**Context 사용량**: input 120k / output 10k tokens (추정)
## 요청
- 웹플레이어가 동영상이 있는 폴더 경로를 전달받아 재생할 때 **필요한 데이터 명세** 작성
## 산출물
- `docs/명세_GhiVideo_데이터폴더_입력명세.md` (+ `.html` / `.pdf`, md2docs.sh 변환)
- 폴더 구성 요약(필수 4종 + 선택 2종 + 무시 파일), 파일별 상세 명세(컬럼·단위·폴백 규칙), 인코딩 공통 규칙, 누락 시 동작, 소스 근거 매핑
- 근거 코드: `client/src/utils/geoData.ts` (loadFolderGeoData 및 개별 파서), `client/src/types/geo.ts` (RouteMeta/RouteInfo/RouteStructure)
- 실데이터 검증: `/mnt/d/_project/20260612_abcvideo/import data/` 상행·하행 v3.0 폴더의 실제 헤더·route.json 확인
## 발견 이슈 (문서 3.3절 주의사항으로 명기)
- **상행 v3.0 `01)측점.csv` 표고 컬럼 불일치**: 실측 표고가 `z` 컬럼에 있고 `Z좌표`=0.0인데,
파서(parseStations)는 `Z좌표_한국``Z좌표` 순서만 읽음 → **표고가 0으로 로드됨**.
하행 파일(`Z좌표_한국` 있음)은 정상. 해결: 데이터 컬럼명 통일 또는 파서에 `z` 폴백 추가 필요.
## 참고
- 필수 파일: `<base>.MP4`, `<base>.csv`(비행로그), `01)측점.csv`(또는 측점.csv), `<base>.kmz`(POI·구조물 유일 소스 — CSV 폴백 없음)
- 선택 파일: `<base>.route.json`(노선정보·구조물 보정), `<base>_poi_overrides.json`(드래그 역투영 보정값)
@@ -0,0 +1,35 @@
# 외부 접속 방법 검토
**날짜**: 2026-07-08
**소요 시간**: 10분
**Context 사용량**: input 45k / output 5k tokens (추정)
## 질문
1. GhiVideo 웹플레이어를 회사 내부가 아닌 외부에서 사용하게 하는 방법
2. (후속) Cloudflare 서비스를 이용하면 되는 것인지 확인
## 현재 상태 확인
- 서버는 `0.0.0.0:55000`으로 리슨 (PM2 `ghiVideo`, ecosystem.config.js)
- 인증(로그인) 기능 없음 → 외부에 그대로 열면 누구나 열람/업로드/삭제 가능
- 서버가 WSL2 내부에서 구동 중 → 직접 포트포워딩 시 Windows portproxy 단계 추가 필요
## 제시한 방법
1. **Tailscale** (소수 인원용 추천): 앱 설치만으로 사설망 구성, 코드 수정 불필요
2. **Cloudflare Tunnel + Access**: 도메인 필요, 이메일 인증 관문, 공인 IP 불필요
3. **공인 IP + 포트포워딩 + Nginx HTTPS**: IT 승인 및 로그인 개발 필요
4. **클라우드 이전**: 정식 공개 서비스용, 현 단계에서는 과함
## Cloudflare Tunnel 상세 설명 (후속 답변)
- 서버에 `cloudflared` 설치 → 서버가 Cloudflare로 아웃바운드 연결 유지 → 방화벽/포트포워딩 불필요
- 준비물: Cloudflare 계정(무료) + 도메인 1개(연 1~2만 원) + cloudflared 상시 실행
- Cloudflare Access(무료 50명)로 허용 이메일만 통과하는 인증 관문 구성 가능 — 앱 코드 수정 불필요
- 주의: 영상이 외부 업체(Cloudflare) 경유(보안 규정 확인 필요), 대량 트래픽 시 무료 플랜 부담, 호스트 PC 꺼지면 서비스 중단
## 다음 행동
- 사용자 결정 대기: ① 도메인 보유 여부, ② 허용할 사용자 이메일 수
- 결정 시 cloudflared 설치 → 터널 연결 → Access 설정 → 외부 접속 확인 순으로 진행 예정