이번에는 회의록 인덱싱 저장 기능을 한번 구현해 보겠다.

회의 녹취록 파일을 업로드 하면 이를 json파일로 Text화 하고, chromaDB에 인덱싱하여 저장한다. 이를 근거로 각 회의록 녹취를 RAG 검색을 하여 당시 회의에 대한 정보를 알 수 있다.

처음에는 chatGPT에 다음과 같이 요청을 했다.

회의 음성 녹음 파일을 텍스트화해 벡터DB에 저장하는 서비스가 사람들이 잘 쓸일이 있을 까?

GPT가 상당히 긴 답변을 주었지만 결론은 ‘잘 쓸 일이 있다’ 였다. 나는 이를 llama-index를 활용하고자 했다.

LlamaIndex기반으로 MVP구조를 만들어줘

/***** GPT Start *****/

🎯 MVP 목표 (중요)

이 정도만 되면 충분히 “쓸 만한 제품”입니다:

  • 음성 업로드
  • 자동 텍스트 변환
  • 의미 기반 검색 (vector search)
  • 질문하면 답변 (RAG)
  • 클릭하면 해당 음성 구간 재생

🧱 전체 아키텍처

[Frontend]
  ↓
[API Server]
  ↓
───────────────
1. STT (Whisper)
2. Chunking
3. Embedding
4. Vector DB (pgvector)
5. LlamaIndex (Query Engine)
───────────────

⚙️ 기술 스택 (현실적인 추천)

  • STT: OpenAI Whisper
  • Index/RAG: LlamaIndex
  • DB: pgvector
  • Backend: FastAPI
  • Frontend: React or Next.js

👉 이 조합이 가장 빠르고 안정적

/***** GPT End *****/

나만 쓸 건데 굳이 FASTAPI 및 React를 쓸 필요가 있을 까?

그냥 내 PC에서만 쓸 건데… 기술 스택을 FASTAPI까지 써야 할까?

그러자 GPT는 기술 스택을 좀 더 단순화하여 제안했다.

/***** GPT Start *****/

⚙️ 기술 스택 (개인만 쓴다면…) STT: Whisper Index: LlamaIndex Vector DB: Chroma UI: Streamlit Backend: 없음

/***** GPT End *****/

그래… 개인용이라면 이정도의 구성으로 충분할 것 같다.

이것을 이제 codex에 시켜보려 해. 이를 위한 프롬프트 초안을 만들어 줄 수 있어?

초안을 GPT가 만들었는데.. 상당히 길다

/***** GPT Start *****/
내 PC에서만 사용할 “회의 음성 녹음 파일 → 텍스트화 → 벡터 인덱싱 → 질의응답” 로컬 앱을 만들어줘.

목표:
- 개인용 로컬 MVP
- FastAPI 같은 서버는 사용하지 않음
- Streamlit UI 또는 CLI 기반으로 실행 가능
- 음성 파일을 업로드/선택하면 STT로 텍스트화
- 텍스트를 chunk로 나누고 LlamaIndex로 인덱싱
- 로컬 Vector DB는 Chroma 사용
- 질문을 입력하면 관련 회의 내용을 기반으로 답변
- 답변과 함께 근거 transcript 조각, 파일명, 가능하면 시작/종료 시간 표시

기술 스택:
- Python 3.11+
- LlamaIndex

... <중략>...

- Slack/Notion 연동 만들지 말 것

먼저 전체 설계를 간단히 설명한 뒤, 파일별로 코드를 작성해줘.
/***** GPT End *****/

한 번에 모든 기능을 구현시키려 하면 Codex가 종종 맥락을 놓쳤다. 그래서 기능 단위를 잘게 나누고, 각 단계마다 실행 가능한 상태를 유지하는 방식으로 하려 한다.

이를 한꺼번에 던지면 제대로 일을 못할것 같어..

“맞아요. 한 번에 던지기보다 단계별 프롬프트로 쪼개는 게 훨씬 좋습니다.”

그러면서 GPT는 프롬프트를 단계별로 쪼개 주었다. 이를 이제 Codex에 하나씩 실행시켜 보도록 한다.

/***** Codex Start *****/

1단계: 프로젝트 뼈대

로컬 개인용 “회의 음성 → 텍스트 → LlamaIndex 검색” 앱을 만들 것이다.

먼저 전체 프로젝트 뼈대만 만들어줘.

조건:

- Python 3.11+
- Streamlit UI
- CLI 스크립트도 나중에 추가 예정
- FastAPI/Django 사용 금지
- ChromaDB 사용
- LlamaIndex 사용
- OpenAI API는 .env에서 읽기

이번 단계에서는:

1. 폴더 구조 제안
2. requirements.txt 작성
3. .env.example 작성
4. README.md 초안 작성
5. 빈 모듈 파일 생성용 코드 또는 파일 목록 제시

아직 STT, 인덱싱, 검색 로직은 구현하지 말고 TODO 주석만 넣어줘. 
/***** Codex End *****/

프로젝트 구조를 만들었다. codex에 python가상환경까지 만들게 하는 것은 시간이 좀 걸리므로(보안 문제 때문에 잘 안되었다) 내가 직접 해 버렸다. 터미널에서 다음과 같이 입력한다. 가상환경에 필수 라이브러리들을 설치하는 것이다.

python3 -m venv .venv

pip install -r requirements.txt

이제 다음 단계의 프롬프트를 실행시킨다

/***** Codex Start *****/

2차 프롬프트

이제 STT 모듈을 구현해줘.

대상 파일:

  • src/stt.py
  • ingest.py 일부

요구사항:

  • audio 파일(mp3, wav, m4a)을 받아 텍스트로 변환
  • OpenAI Whisper API 사용
  • 결과를 data/transcripts/{원본파일명}.json 으로 저장
  • JSON에는 source_file, text, segments 필드를 포함
  • timestamp가 없으면 start/end는 null 처리
  • 이미 transcript json이 있으면 재처리하지 않음
  • 타입 힌트와 에러 처리 포함
/***** Codex end *****/

이 단계에서 중간 검사를 하는 게 안전할 것 같다.

이 단계를 검사하려면 어떻게 할 까?

/***** Codex Start *****/

.venv/bin/python ingest.py data/audio/A301_중간.mp3

/***** Codex End *****/

이 결과 data/transcripts/A301_중간.json 파일이 생성되면 검사는 OK

이제 3차 프롬프트 실행이다.

/***** Codex Start *****/

3차 프롬프트

이제 transcript JSON을 chunk로 나누는 기능을 구현해줘.

대상 파일:

  • src/chunking.py

요구사항:

  • transcript JSON을 읽어서 chunk 목록 생성
  • chunk 크기 기본값 700 tokens
  • overlap 기본값 100 tokens
  • 각 chunk에는 다음 metadata 포함:
    • source_file
    • chunk_id
    • start_time
    • end_time
  • timestamp가 없으면 start_time/end_time은 null
  • LlamaIndex Document로 변환하는 함수 포함
/***** Codex End *****/

이것도 결과 검사를 하면 좋겠다.

이 기능을 어떻게 검증하면 좋을까?

/***** Codex Start *****/
chunk 생성 테스트

  python -B -c "import sys; sys.path.insert(0, 'src'); import chunking; chunks =
  chunking.chunk_transcript_json('data/transcripts/A301_중간.json');
  print(len(chunks)); print(chunks[0]['metadata']); print(chunks[0]['text'][:300])"

  예상 출력은 대략 이런 형태입니다:

  {'source_file': 'data/audio/A301_중간.mp3', 'chunk_id': 'A301_중간-0000',
  'start_time': 0.0, 'end_time': 427.0}
  저희는 지능형 영업관리를 위한 ...

LlamaIndex Document 변환 테스트

  python -B -c "import sys; sys.path.insert(0, 'src'); import chunking; docs =
  chunking.transcript_json_to_documents('data/transcripts/A301_중간.json');
  print(len(docs)); print(type(docs[0]).__name__); print(docs[0].metadata)"

  예상 출력:
  Document
  {'source_file': 'data/audio/A301_중간.mp3', 'chunk_id': 'A301_중간-0000',
  'start_time': 0.0, 'end_time': 427.0}

  chunk 크기를 바꿔 보고 싶으면:

  python -B -c "import sys; sys.path.insert(0, 'src'); import chunking; chunks =
  chunking.chunk_transcript_json('data/transcripts/A301_중간.json', chunk_size=300,
  overlap=50); print(len(chunks)); [print(c['metadata']) for c in chunks]"
/***** Codex End *****/

검사를 수행하고 결과를 눈으로 확인한 후 이제 4차 프롬프트를 실행시킨다.

/***** Codex Start *****/

4차 프롬프트

이제 LlamaIndex와 ChromaDB를 연결해서 벡터 인덱싱을 구현해줘.

대상 파일:

  • src/indexer.py

요구사항:

  • data/chroma/에 Chroma persistent index 저장
  • embedding model은 text-embedding-3-small 기본값
  • transcript chunk를 LlamaIndex Document로 받아 인덱싱
  • 이미 인덱싱된 source_file은 중복 인덱싱하지 않도록 처리
  • index 로드 함수와 index 생성/추가 함수 분리
  • 타입 힌트와 에러 처리 포함
/***** Codex End *****/

지금 단계에서 따로 검사할 부분은 없어 보인다. 다음 프롬프트를 실행시킨다

/***** Codex Start *****/

5차 프롬프트

이제 질문 검색 기능을 구현해줘.

대상 파일:

  • src/query.py
  • ask.py

요구사항:

  • 저장된 Chroma index를 로드
  • 사용자의 질문을 받아 LlamaIndex query engine으로 답변 생성
  • LLM 기본값은 gpt-4o-mini
  • 답변과 source nodes 3~5개를 출력
  • source node마다 text, source_file, start_time, end_time 표시
  • CLI 실행 예: python ask.py “지난 회의에서 결정된 내용은?”
/***** Codex End *****/

테스트로 CLI실행을 해 본다.

이제 6차 프롬프트를 실행한다

/***** Codex Start *****/

6차 프롬프트

이제 Streamlit UI를 구현해줘.

대상 파일:

  • app.py

요구사항:

  • data/audio/ 폴더의 mp3, wav, m4a 파일 목록 표시
  • 파일 선택 후 “텍스트화 및 인덱싱” 버튼
  • 처리 상태 표시
  • 질문 입력창
  • 답변 출력
  • 관련 transcript 조각 3~5개 표시
  • 각 조각에는 source_file, start_time, end_time, text 표시
  • FastAPI 같은 서버는 사용하지 말 것
/***** Codex End *****/

여기까지 만들었다면, 다음과 비슷한 구조로 되어있을 것이다.

.
├── README.md
├── app.py
├── ask.py
├── data
│   ├── audio
│   │   └── A301_중간.mp3
│   ├── chroma
│   │   └── chroma.sqlite3
│   └── transcripts
│       └── A301_중간.json
├── ingest.py
├── requirements.txt
├── src
...<중략>
├── storage
│   └── chroma
└── tests

19 directories, 46 files

녹음 파일은 data디렉토리 및 audio 및에 넣어놓고 streamlit 을 실행시키면 로컬에서 이를 볼 수 있다.

> streamlit run app.py

화면은 잘 뜬다. 다른 녹음 파일을 audio폴더에 복사하고 페이지를 리로딩하면 Audio파일 목록에 반영된다.

image-20260506153757869

그런데 A304_중간.mp3 파일을 ‘텍스트화 및 인덱싱’을 한 후 Search를 하는데 Search대상이 계속 A301_중간.mp3의 내용이 검색되는 오류가 있었다. 이의 수정을 보강하자.

mp3 파일을 새로 data/audio파일에 추가하고 이를 인덱싱을 app.py를 통해 했어.. 그리고 질문을 던졌는데 계속 transcripts는 예전 것을 읽는 것 같어… 이를 선택된 오디오 파일에 맞게 transcripts를 변경하여 검색하는 걸로 바꿔줘

실행해 보니 이제 제대로 검색대상이 바뀌어 나온다.

image-20260506154223613

여기서 이제 코드의 마무리 정리를 위한 마지막 7차 프롬프트를 실행한다

/***** Codex Start *****/

7차 프롬프트

전체 코드를 점검하고 실행 가능하게 수정해줘.

확인할 것:

  • import 경로 오류
  • requirements 누락
  • .env 로딩
  • data/audio, data/transcripts, data/chroma 폴더 자동 생성
  • 중복 인덱싱 방지
  • README 실행 방법 업데이트

가능하면 최소 테스트용 mock transcript도 하나 추가해줘.

/***** Codex End *****/

이렇게 해서 나만의 회의 녹취록 관리 시스템 하나를 만들었다.

여기에서 눈여겨 볼 점은 하나의 프롬프트에 너무 많은 요구사항을 구겨넣기 보다는 단계별로 나누어 넣고 중간중간에 확인이 가능한 부분은 실행하면서 잘 진행되는지 여부를 체크해야 한다는 것이다.