Ofrenda Local: 오프라인 오프렌다 빌더, 그리고 할머니의 말씀이 구절별로 하나의 언어를 필요로 하는 이유
요약
Ofrenda Local은 녹음된 가족의 이야기를 오프라인에서 오프렌다 카드(추모 글, 음식 등)로 변환하는 AI 빌더입니다. 이 시스템은 음성 인식에 whisper.cpp를, 텍스트 생성에는 llama.cpp 기반의 Gemma 3 4B 모델을 결합하여 작동합니다. 모든 과정이 노트북 환경에서 외부 의존성 없이 독립적으로 실행되는 것이 특징입니다.
핵심 포인트
- 오프라인 환경에서 동작하는 완전한 AI 빌더를 구현했습니다.
- `whisper.cpp`와 Gemma 3 4B 모델을 결합하여 사용합니다.
- 녹음된 이야기의 보존에 초점을 맞추어 데이터 유출이 없습니다.
- 오디오 녹음부터 HTML 카드까지 End-to-End 프로세스를 구현했습니다.
_이 글은 Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine에 제출하는 글입니다.
제가 만든 것 (What I Built)
Ofrenda Local은 돌아가신 친척에 대한 이야기를 말로 녹음하면, 그 내용을 오프렌다 카드(그들이 요리했던 음식, 불렀던 노래, 살았던 장소, 정확히 사용하던 말들, 그리고 화자의 목소리로 작성된 짧은 추모 글)로 변환합니다. 또한 스크립트와 story.md 파일을 저장하여 이야기가 예쁜 페이지일 뿐만 아니라 텍스트 형태로도 보존되게 합니다.
모든 것은 노트북에서 실행됩니다. 녹취록 변환(Transcription)에는 whisper.cpp를 사용하고, 작성에는 llama.cpp 기반의 Gemma 3 4B를 사용하며, 이들을 연결하는 접착제는 표준 라이브러리 외에 의존성이 없는 단일 Python 파일입니다.
저는 주제를 “우리가 간직하는 기억과 이야기”로 해석했습니다. 이 기계 속의 유령(ghost)은 녹음된 사람 자체입니다. 이 기계는 그들을 보존하는 데 도움을 주어야 하며, 절대로 그들의 입에 말을 넣어서는 안 됩니다.
시연 (Demo)
샘플 녹음을 통해 생성된 라이브 카드: https://designmaster-solutions.github.io/ofrenda-local/
샘플(samples/abuela-rosa.m4a)은 합성 자료입니다. 저는 가상의 할머니인 Rosa Delgado에 대한 짧은 이야기를 작성하고, 영어에는 macOS의 'say' 음성 중 Samantha를, 스페인어 말씀에는 Paulina를 사용하여 녹음했습니다. 오디오부터 HTML까지 Mac에서 전체 실행하는 데 걸린 시간은 약 16초였습니다.
코드 (Code)
GitHub logo DesignMaster-Solutions / ofrenda-local
녹음된 가족 이야기를 완전히 오프라인으로 Día de los Muertos의 오프렌다로 변환하기 (whisper.cpp + Gemma)
Ofrenda Local
돌아가신 분에 대한 이야기를 친척이 말하는 것을 녹음하세요. Ofrenda Local은 그 녹음을 오프렌다 카드(좋아했던 음식, 노래, 장소, 사용하던 말들, 그리고 짧은 추모 글)로 변환하며, 어떤 정보도 컴퓨터 밖으로 나가지 않습니다.
DEV Hacktoberfest 오픈소스 AI 챌린지, 4주차를 위해 제작됨: 기계 속의 유령(Ghost in the Machine).
story.m4a ──ffmpeg──► 16 kHz wav ──silence cuts──► phrases ──whisper.cpp (language per phrase)──► transcript
transcript ──Gemma 3 4B on llama.cpp (JSON-schema grammar)──► card ──grounding filter──► ofrenda.html + story.md
실행하기
brew install llama.cpp whisper-cpp ffmpeg
whisper 모델: https://huggingface.co/ggerganov/whisper.cpp의 ggml-small.bin 사용
llama-server -hf ggml-org/gemma-3-4b-it-GGUF --port 18099 -c 4096 &
python3 ofrenda.py samples/abuela-rosa.m4a -o out # 전사(transcribe) 및 카드 빌드
python3 ofrenda.py samples/abuela-rosa.m4a -t out/transcript.txt -o out # 수동으로 이름을 수정한 후
open out/ofrenda.html
환경 변수: OFRENDA_LLM (json_schema를 지원하는 모든 OpenAI 호환 엔드포인트), OFRENDA_WHISPER (모델 경로) Python 표준 라이브러리만 사용 가능. 테스트: python3…
어떻게 만들었는지
story.m4a -> ffmpeg -> 16 kHz wav -> 일시 정지 지점에서 자르기 -> whisper.cpp, 구절별 언어 감지 -> transcript
transcript -> llama-server의 Gemma 3 4B, JSON 스키마를 문법으로 컴파일 -> card -> grounding filter -> ofrenda.html
모델 선택보다 더 중요했던 네 가지 요소.
1. Whisper는 파일당 하나의 언어만 인식합니다. 제가 처음 전사한 내용은 다음과 같았습니다: "Whenever we complained, she used to say, And when we left her house she always said, I miss her hands...". 스페인어 구절 두 개가 사라졌습니다. Whisper가 전체 파일에 대해 영어로 감지하여 영어 부분이 아닌 부분은 조용히 삭제한 것입니다. 이 구절들은 ofrenda의 가장 중요한 부분인데, 할머니가 문장 중간에 언어를 바꾸는 것은 예외적인 상황이 아니라 일반적인 경우입니다.
해결책: ffmpeg의 silencedetect를 실행하여 일시 정지 지점을 찾고 오디오를 그곳에서 자른 다음, 각 구절을 -l auto로 전사합니다. 이제 구절들이 나타납니다: "Con paciencia y saliva, el elefante se comió a la hormiga."
2. 짧은 구절은 이름을 잃는다. 고정된 언어는 확보했지만 장소는 무너졌다: 오악사카(Oaxaca)는 '와하카(Wahaka)'가 되었고 틀라콜룰라(Tlacolula)는 '툴리카룰라(Tulicalula)'가 되었다. 이는 2초짜리 클립에는 문맥이 없기 때문이다. whisper.cpp는 --prompt를 사용하기 때문에, 이제 각 구절은 지금까지의 이야기 중 마지막 400자만큼을 받는다. 그 결과 오악사카와 틀라콜룰라는 원래대로 돌아왔다.
3. 문법이 애원보다 강하다. Gemma 3 1B를 사용하여 '기억(remembrance)' 필드는 ["" 형태로 복구되었다. 더 긴 프롬프트를 제공하는 대신, llama-server에 JSON 스키마(response_format: json_schema)를 전달했다. llama.cpp는 이를 문법으로 컴파일하여 모델이 카드 형태 외의 다른 것을 생성할 수 없게 만들었고, 기억 필드에 minLength: 120을 설정함으로써 두 글자에서 멈출 수 없도록 했다. 작성 품질을 위해 Gemma 3 4B로 옮겼으며, 이 문법 덕분에 출력이 신뢰성을 갖게 되었다.
4. 모델은 할아버지를 지어낸다. 테스트를 하던 중 실수로 빈 스크립트를 전달했다. Gemma는 파눌레(pan dulce)와 라 밤바(La Bamba)를 좋아했던 '아부에로 리카르도(Abuelo Ricardo)'에 대한 완벽하고 감동적인 기억을 반환했다. 아무도 말을 하지 않았던 것이다. 돌아가신 실제 사람들에 대해 글을 쓰는 도구로서, 이는 최악의 버그다. 그래서 다음과 같은 규칙을 적용했다:
- 모델 호출 전에 빈 스크립트나 매우 짧은 스크립트는 거부된다;
- 모든 음식, 노래, 장소, 표현은 스크립트와 단어를 공유해야 하며 그렇지 않으면 제외됩니다;
- 카드 이름이 이야기에 포함되어 있지 않다면 실행 자체가 실패합니다.
같은 규칙을 기억 프롬프트에도 적용했다. 초기의 카드 하나는 '그녀가 오악사카에서 샌애너토니오로 이사한 것이 아쉽다'라는 문장으로 끝났다. 이것 역시 아무도 말하지 않은 내용이었다. 그래서 이제 프롬프트는 의견을 요구하지 않도록 요청하며, 코드는 요청된 세 문장만 유지한다. 왜냐하면 길이가 제한된 작은 모델은 때때로 계속 진행하여 프롬프트를 반복하기 시작하는 경향이 있기 때문이다.
그라운딩 필터(grounding filter)와 HTML 이스케이핑(HTML escaping)에 대한 단위 테스트를 마련했으며, 이를 신뢰하기 전에 필터를 비활성화했을 때 실패하는 것을 지켜보았다.
Whisper는 여전히 "rajas" 대신 "tamales de rojas"를 듣고, "Pedro Infante" 대신 "Pedro in Fonte"를 듣습니다. 저는 모델이 이들을 추측하도록 만들지 않았습니다. 녹취록은 카드 옆에 저장되고, 가족 구성원이 네 단어를 수정하면 python3 ofrenda.py story.m4a -t out/transcript.txt 명령어로 수정된 텍스트로부터 카드를 재구축합니다. 가족이 수정하고, 모델은 추측하지 않습니다.
개방형 혁신(Open Innovation)이 중요한 이유?
녹음물에 담긴 내용 때문입니다. 할머니의 목소리, 가족 모두의 이름, 그들이 살았던 곳, 아무도 말하지 않는 질병 같은 것들 말입니다. 이것은 제가 카드로 변환될 수 있도록 서비스에 업로드하고 싶은 것이 아니며, 바로 가족들이 50년 동안 보관하는 녹음물의 종류와 같습니다. whisper.cpp와 Gemma를 로컬에서 실행한다는 것은 오디오가 기기를 떠나지 않는다는 의미이며, 인터넷 연결이 없는 오악사카(Oaxaca)의 부엌에서도 작동하고, 이야기 하나당 비용이 들지 않습니다.
개방형 접근 방식은 또한 폐쇄적인 API로는 제가 건드릴 수 없었던 부분들을 변경할 수 있게 해 주었다는 점을 의미했습니다. 즉, 제가 원하는 커트 지점(cut points)으로 구절별로 전사하고, whisper에 자체 실행 컨텍스트를 공급하며, 모델이 단순히 지침을 따르기를 바라는 대신 문법(grammar)으로 모델을 제약할 수 있었습니다. 그리고 30년 후, 오늘날의 서비스들이 사라지더라도, llama-server와 GGUF 파일은 여전히 같은 방식으로 같은 이야기를 실행할 것입니다.
본질(The Spirit of It)
샘플의 마지막 문장은 "I miss her hands, and her kitchen that always smelled like cinnamon."입니다. 카드는 영리할 필요가 없습니다. 그것은 샹푸라도(champurrado)와 라디오 속 페드로 인판테(Pedro Infante), 그리고 그녀가 말한 그대로의 "Ve con Dios, mijo"를 보존해야 하며, 단 하나라도 추가해서는 안 됩니다.
수상 부문
Best Use of Gemma: Gemma 3 4B (개발 중에는 1B) 모델을 llama.cpp를 통해 로컬에서 실행하며, JSON-schema 문법에 의해 제약된 모든 카드를 작성합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기