저번 글에서는 Gemma 4를 사용설정하고, 이미지 번역 테스트까지 해보았습니다.
이번에는 Python으로 Gemma 4가 이미지를 분석할 수 있는 능력을 활용해서
영수증을 인식해 자동으로 가계부에 작성을 해주는 AI 에이전트를 만들어보겠습니다.
Python 및 VSCode가 설정은 Python 개발 환경 설정 게시글을 확인해주세요.
Python 개발 환경 설정하기(Python)
이제 본격적으로 Python을 설치해보겠습니다.앞으로 이 블로그의 예제는 대부분 Python을 기준으로 진행하기 때문에, 개발 환경을 맞춰두시는 것이 중요합니다. Welcome to Python.orgThe official home of the P
togchae.tistory.com
우선 시작하기 전에 저번에 설치한 Gemma 4가 잘 있는지 확인해보겠습니다.
저는 윈도우의 PowerShell을 사용합니다. 일반 터미널을 활용하셔도 비슷하게 동작합니다.
그리고 기본적으로 뭔가 잘 안된다 하시면 관리자 권한으로 실행을 추천드립니다.

ollama list

이 명령어를 사용하시면 현재 설치되어있는 인공지능 모델이 나옵니다.
다른 모델을 설치하신 분들은 e4b, 26b, 31b 혹은 그냥 gemma4라고 나오실겁니다.
우선 도구를 만들 폴더를 만들겠습니다.
(터미널이 아닌 파일 탐색기에서 새폴더 만들기를 하셔도 됩니다.)
mkdir receipt-accountbook # 폴더 만들기
cd receipt-accountbook # 폴더로 이동하기
그리고 항상 작업하실때는 가상환경을 생성하시는게 좋습니다.
python -m venv receipt # receipt라는 가상환경 만들기
가상환경을 실행하는 명령어는 운영체제에 따라 다릅니다.
.\receipt\Scripts\activate # Window
source receipt\Scripts\activate # Linux / MacOS
가상환경 활성화가 보안 오류로 막히시는 경우에는
아래 명령어를 실행하여 스크립트 실행 권한을 변경하시면 됩니다.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

이미지와 같이 Y나 A를 한번 더 쳐주셔야 정상적으로 작동합니다.
왼쪽 (receipt)가 나왔는지 확인해서 정상적으로 가상환경에 들어왔는지 확인해주세요.

필요한 패키지를 설치해줍니다.
pip install requests openpyxl python-dotenv

마지막 부분에 이런 문구가 나와있으면 정상적으로 설치가 완료된 것입니다.
폴더 구조를 아래와 같이 설정해주세요.
receipt-account-book/
├─ receipts/
│ └─ sample.jpg
├─ output/
├─ account_book.xlsx
├─ main.py
└─ .env
.env 파일을 만들줄 모르시는 분은 게시글을 참고해주세요!
.env와 같은 확장자가 없는 파일 만들기
Linux / MacOS 에서는 확장자가 없는 일반 파일을 만들기 쉽습니다.터미널에서 아래와 같은 명령어를 입력하시면 됩니다.touch filename 윈도우에서는 다양한 방법이 있습니다.1. PowerShell을 활용하는 방
togchae.tistory.com
.env 파일의 내용을 아래와 같이 설정해주세요. (모델명은 설치한 모델명과 동일하게)
OLLAMA_URL=http://localhost:11434/api/generate
OLLAMA_MODEL=gemma4:e2b
EXCEL_FILE=account_book.xlsx
엑셀 시트(account_book.xlsx)의 컬럼은 다음과 같이 설정하겠습니다.
날짜 | 상호명 | 카테고리 | 항목 | 수량 | 금액 | 결제수단 | 메모
main.py를 작성해주겠습니다.
vs code 설정을 안하신 분들은 그냥 메모장에 작성후 확장자만 .py로 바꿔주셔도 됩니다.
import os
import json
import base64
import re
from datetime import datetime
from pathlib import Path
import requests
from dotenv import load_dotenv
from openpyxl import Workbook, load_workbook
load_dotenv()
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434/api/generate")
OLLAMA_MODEL = os.getenv("OLLAMA_MODEL", "gemma4")
EXCEL_FILE = os.getenv("EXCEL_FILE", "account_book.xlsx")
PROMPT = """
너는 한국 영수증을 가계부용 데이터로 변환하는 도우미다.
이미지 속 영수증을 읽고 아래 JSON 형식만 반환해라.
설명 문장, 마크다운, 코드블록은 절대 쓰지 마라.
반환 형식:
{
"date": "YYYY-MM-DD",
"store": "상호명",
"category": "식비|카페|교통|생활|의료|쇼핑|기타",
"payment_method": "카드|현금|간편결제|알수없음",
"total_amount": 0,
"items": [
{
"name": "상품명",
"quantity": 1,
"amount": 0
}
],
"memo": "특이사항 또는 빈 문자열"
}
규칙:
- 날짜가 없으면 빈 문자열로 둔다.
- 상호명이 불명확하면 빈 문자열로 둔다.
- 금액은 숫자만 사용한다.
- 총액과 개별 항목 금액이 모두 보이면 둘 다 추출한다.
- 항목 구분이 어렵다면 items에는 대표 항목 1개를 넣고 total_amount를 사용한다.
- 카테고리는 영수증 내용으로 추정한다.
"""
def encode_image(image_path: str) -> str:
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def extract_json(text: str) -> dict:
"""
모델이 실수로 앞뒤 설명을 붙였을 때 JSON 부분만 추출.
"""
text = text.strip()
try:
return json.loads(text)
except json.JSONDecodeError:
pass
match = re.search(r"\{.*\}", text, re.DOTALL)
if not match:
raise ValueError(f"JSON을 찾지 못했습니다.\n응답:\n{text}")
return json.loads(match.group(0))
def analyze_receipt(image_path: str) -> dict:
image_base64 = encode_image(image_path)
payload = {
"model": OLLAMA_MODEL,
"prompt": PROMPT,
"images": [image_base64],
"stream": False
}
response = requests.post(OLLAMA_URL, json=payload, timeout=180)
response.raise_for_status()
result = response.json()
raw_text = result.get("response", "")
data = extract_json(raw_text)
return normalize_receipt_data(data)
def normalize_receipt_data(data: dict) -> dict:
"""
엑셀 입력 전에 기본값 보정.
"""
data.setdefault("date", "")
data.setdefault("store", "")
data.setdefault("category", "기타")
data.setdefault("payment_method", "알수없음")
data.setdefault("total_amount", 0)
data.setdefault("items", [])
data.setdefault("memo", "")
if not data["items"]:
data["items"] = [{
"name": "영수증 합계",
"quantity": 1,
"amount": data.get("total_amount", 0)
}]
for item in data["items"]:
item.setdefault("name", "항목 미상")
item.setdefault("quantity", 1)
item.setdefault("amount", 0)
return data
def ensure_excel_file(path: str):
if Path(path).exists():
return
wb = Workbook()
ws = wb.active
ws.title = "가계부"
ws.append(["날짜", "상호명", "카테고리", "항목", "수량", "금액", "결제수단", "메모"])
wb.save(path)
def append_to_excel(data: dict, excel_path: str):
ensure_excel_file(excel_path)
wb = load_workbook(excel_path)
ws = wb.active
for item in data["items"]:
ws.append([
data["date"],
data["store"],
data["category"],
item["name"],
item["quantity"],
item["amount"],
data["payment_method"],
data["memo"]
])
wb.save(excel_path)
def save_debug_json(data: dict, image_path: str):
Path("output").mkdir(exist_ok=True)
stem = Path(image_path).stem
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = Path("output") / f"{stem}_{timestamp}.json"
with open(output_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
return output_path
def main():
image_path = input("영수증 이미지 경로를 입력하세요: ").strip()
if not Path(image_path).exists():
print("파일이 존재하지 않습니다.")
return
data = analyze_receipt(image_path)
append_to_excel(data, EXCEL_FILE)
debug_path = save_debug_json(data, image_path)
print("처리 완료")
print(f"엑셀 파일: {EXCEL_FILE}")
print(f"JSON 로그: {debug_path}")
print(json.dumps(data, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
이제 한번 실행해보겠습니다.
receipts 폴더에 정리를 원하는 영수증을 넣고 sample.jpg로 이름을 변경해줍니다.
(인터넷에서 가져왔습니다)


ollama에서 제대로 실행되고 있는지 확인하기 위해서 다음 명령어를 쳐줍니다.
ollama serve
이미 실행중이라는 문구가 나오시는 분은 윈도우에서 ollama가 실행중인지 확인해주세요.
한번 프로그램을 구동해볼까요?
ollama serve로 로그를 보고 있으니
새로운 터미널을 열어서 가상환경에 접속한 뒤에 실행해봅시다.
# 경로로 이동
.\receipt\Scripts\activate
python main.py

이미지 경로까지 입력해주면, 여러분의 컴퓨터가 우웅 하면서 일을 시작합니다.
아까 ollama serve를 쳤던 로그를 보러 가볼까요?

작업을 시작했고 완료 됐다고 나와있네요.

다시 실행한 터미널로 돌아와보니 결과가 잘 나왔습니다.

그리고 엑셀 파일에도 잘 작성이 되었네요.
하지만, 날짜는 맞았지만 상호명 항목 금액 결제수단을 잘 이해하지 못했네요.
e2b를 활용해서 수준이 낮은 걸수도 있고,
프롬포트를 잘 설정해주지 못해서 AI가 일을 못했을수도 있습니다.
다음 글에서는 더 높은 모델도 사용해보고, 프롬포트도 수정해보는
파인튜닝을 한번 해보도록 하겠습니다!
※ 추가

실행 중 이렇게 PermissionError가 나오시는 분은 엑셀을 열어두진 않았는지 확인해주시면 됩니다!