Haikamera: 휴대폰으로 무엇이든 비추면 오픈 웨이트 모델이 시를 돌려줍니다
요약
Haikamera는 휴대폰 카메라로 사물을 비추면 오픈 웨이트 비전 모델이 그 장면을 바탕으로 세 줄짜리 하이쿠를 생성해주는 모바일 웹 앱입니다. 이 앱은 사용자가 주변 환경에 대한 인지적 주의를 기울이도록 돕는 것이 목적이며, 프라이버시 보호를 위해 EXIF/GPS 제거, 계정 및 쿠키 미사용 등 구조적인 보안 설계를 적용했습니다.
핵심 포인트
- 오픈 웨이트 비전 모델을 활용하여 시(Haiku) 생성 기능을 구현함.
- 프라이버시를 최우선으로 고려하여 구조적 보안 설계가 특징임 (EXIF/GPS 제거, 로컬 저장).
- PWA 형태로 제작되어 모바일 환경에서 쉽게 접근 가능하며 사용성이 높음.
_본 글은 Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass에 제출하는 내용입니다.
제가 만든 것
Haikamera는 버튼 하나짜리 모바일 웹 앱입니다. 평범한 사물, 책상, 창문, 이끼 낀 벽 등을 휴대폰으로 비추고 **시 찍기(Snap a poem)**를 누르면, 오픈 웨이트 비전 모델이 프레임 안에 실제로 있는 색상과 사물을 바탕으로 세 줄짜리 하이쿠(haiku)를 작성합니다. 그런 다음 앱은 아래에 시가 적힌 자신의 사진을 보여주고, 사용자는 다시 실제 사물들을 바라보게 됩니다.
마지막 부분이 이 디자인의 핵심입니다. 이 앱의 목적은 가능한 한 짧은 방해 요소가 되는 것입니다. 새로고침할 피드도 없고 보호할 기록(streak)도 없습니다. 열어서 사진 하나를 찍고, 세 줄을 읽고, 닫습니다.
이것은 눈앞에 있는 것을 인식하도록 일깨움을 받고 싶은 누구에게나 적합합니다. 저는 사생활 침해에 대한 강의 없이 아이에게 건넬 수 있도록 만들었기 때문에, 휴대폰이나 사람에 관한 어떤 것도 기기를 벗어나지 않습니다. 단지 다운스케일된 사진 한 장만 남습니다.
여기서의 프라이버시는 정책이 아닌 구조입니다:
- 프레임은 업로드되기 전에 EXIF와 GPS를 제거하는 캔버스에서 재인코딩됩니다.
- 계정, 쿠키, 분석(analytics), 또는 기기 지문(device fingerprints)이 없으며, 로그인할 필요가 없어 로그인 화면도 없습니다.
- 사진은 단일 요청 동안 메모리에 유지되며 디스크에 절대 기록되지 않습니다.
- 화면에 표시되는 유일한 이미지는 사용자의 사진입니다. 아무것도 생성되거나 가져와지지 않기 때문에, 두 번째 서비스는 절대로 사용자의 장면에 대한 설명을 볼 수 없습니다.
- 필드 저널은 기기의
localStorage에 저장됩니다. 동기화되지 않습니다. - 모델 API 키는 서버에 존재하므로 공개 페이지에는 비밀이 없습니다.
데모
haikamera.onrender.com에서 실시간으로 확인 가능합니다.
이것은 PWA(Progressive Web App)입니다. 휴대폰에서 공유 시트를 열고 '홈 화면에 추가'를 선택하면 앱처럼 작동합니다. 후면 카메라로 사진을 찍거나, 이미 가지고 있는 사진을 업로드할 수 있습니다.
설정 없이 전체 UI를 사용해 볼 수 있습니다. API 키 없이 실행하면 DEMO MODE로 부팅되며, 모델을 만져보기 전에 모든 사진에 동일한 예시 시가 반환됩니다.

휴대폰으로. 당신의 사진, 그리고 그 사진이 얻어낸 시.

데스크톱에서도 동일한 앱.
코드
GitHub logo safeamiiir / haikamera
휴대폰을 어떤 장면이라도 비추면 무료 오픈 웨이트 비전 모델이 그것에 대한 짧은 색채 하이쿠를 작성합니다. 앱 설치, 로그인, 데이터 수집 과정이 없습니다.
🪷📸 Haikamera — 당신의 장면에 관한 작은 시
무엇이든 휴대폰으로 비추세요—책상, 창문, 오솔길 등—그리고 무료 오픈 웨이트 AI가 그것이 보는 것에 대한 짧은 색채 시를 작성합니다. 그런 다음 찍은 사진 아래에 그 시를 보여줍니다.
설치나 계정 생성 없이 작동하는 모바일 웹 앱입니다. 개인 데이터는 수집되지 않습니다.
- 보는 행위에 관한 게임입니다. 핵심은 시가 나오기 직전의 2초 동안, 머그잔의 호박색, 벽의 이끼, 하늘의 슬레이트 같은 것들을 알아차리는 것입니다. 시는 단지 당신의 주의를 세상으로 돌려줄 뿐입니다.
- 디자인적으로 짧은 스크린 타임이 특징입니다. **시 찍기(Snap a poem)**를 탭하고 사진을 찍거나 업로드하면, 잠시 후 눈앞의 색상과 사물에 대한 세 줄짜리 하이쿠가 완성됩니다. 이 앱의 전체 목적은 당신이 앱을 보는 것을 멈추게 하는 것입니다.
- 오픈 웨이트 AI…
이것은 의존성(dependency)이 없는 Node 앱이며, MIT 라이선스를 따릅니다. server.js 파일에는 전체 백엔드(정적 파일 및 세 개의 엔드포인트 포함)가 약 600줄에 담겨 있으며, public/ 폴더는 전체 프런트엔드를 구성합니다. 빌드 단계도 없고, 프레임워크도 없으며, node_modules 같은 것도 없습니다. 클론(Clone)하여 node server.js를 실행하면 바로 작동합니다.
server.js 의존성 없는 서버: /api/poem, /api/pulse, /api/health
public/index.html 전체 UI: 한 화면, 하나의 버튼, 하나의 시 카드
public/app.js 카메라 + 업로드, EXIF 제거 다운스케일링, 렌더링, 저널
...
제작 과정 (How I Built It)
모든 것을 결정한 두 가지 제약 조건이 있었습니다. 첫째, AI는 오픈 웨이트(open-weight)여야 했고, 둘째, 앱 작동에 필수적인 부분은 장식이 아니라 핵심 기능이어야 했습니다.
AI가 교체 가능한 컴포넌트인 이유
서버는 일반적인 OpenAI chat-completions 형식으로 통신합니다. 기본 모델은 OpenRouter의 무료 티어에서 사용 가능한 오픈 웨이트 비전 모델인 Google Gemma 4 31B (google/gemma-4-31b-it:free)입니다. 제공업체(provider)가 키(key)의 접두사로 추론되기 때문에, 코드를 변경할 필요 없이 키만 붙여넣으면 됩니다:
AI_API_KEY=sk-or-v1-... # sk-or- => OpenRouter; gsk_ => Groq; nvapi- => NVIDIA; hf_ => Hugging Face
다른 두뇌를 원한다면? 환경 변수 하나만 바꾸면 됩니다. 코드는 같지만, 시는 다릅니다:
AI_MODEL=meta-llama/llama-4-maverick-17b-128e-instruct # Groq 사용 시
AI_MODEL=Qwen/Qwen3-VL-30B-A3B-Instruct # Hugging Face 사용 시
시가 실제로 만들어지는 곳
모델은 엄격한 JSON 형식으로 답변해야 합니다. 제목, 세 개의 시 구절, 관찰된 사물 목록, 헥스 코드(hex code)와 함께 지배적인 색상, 그리고 분위기입니다. 프롬프트는 음절 수를 소리 내어 계산하고, 시적 진부함(clichés) 목록을 금지하며, 거절하는 것을 허용하지 않습니다:
1. 정확히 세 줄이어야 하며, 각 줄의 음절 수는 다음과 같아야 합니다: 5음절, 그리고 7음절, 그리고 5음절.
2. 모든 줄은 사진에 실제로 존재하는 무언가를 언급해야 합니다.
...
...
속도 제한(Rate limits)은 정상적인 것이므로 서버는 계속 작동합니다
무료 :free 모델 풀은 지속적으로 429 에러를 받습니다. 서버는 폴백 체인(fallback chain)을 따라 이동하며, 전송 오류가 아닌 파싱할 수 없는 JSON으로 응답했을 때만 모델을 재시도하고, 모든 것이 실패하면 _최선의 추측(best guess)_으로 표시된 보장된 구절을 반환합니다. 탭을 할 때마다 시 한 편이 생성됩니다. 상태 확인 엔드포인트는 현재 실행 중인 것에 대한 진실을 알려줍니다:
$ curl https://haikamera.onrender.com/api/health
{"ok":true,"demo":false,"provider":"openrouter","model":"google/gemma-4-31b-it:free", "models":[...]}
로딩 단어 역시 모델이 생성합니다
대기 화면에는 "Haikoizing…", "Shakespearing…", "Colour-sipping…"과 같은 동명사(gerunds)가 표시됩니다. 서버는 텍스트 모델에 새로운 배치(batch)를 만들도록 요청하고, 이를 3시간 동안 캐싱하며, 해당 호출이 실패하면 내장된 목록으로 폴백합니다. 사소한 부분이지만, 이 덕분에 2초의 대기 시간이 스피너가 아니라 앱의 일부처럼 느껴집니다.
작동하지 않았던 부분과 제가 수정한 내용
- 초기 초안은 코드 울타리 안에 감싸진 산문 형태로 반환되어
JSON.parse를 깨뜨렸습니다. 해결책은 첫 번째{부터 마지막}까지 잘라내는 관대한 추출기(tolerant extractor)를 사용하고, 파싱에 실패하면 동일한 모델을 재시도하는 것입니다. - 일부 모델은 스키마의 자리 표시자 제목을 그대로 반복했습니다. 이제 정규화기(normalizer)는 알려진 자리 표시자를 제거하고 실제 이름을 대체합니다.
- 배포 후, 새 HTML에 대해 실행되는 캐시된
app.js가Cannot set properties of null오류를 발생시켰습니다. 저는 앱 셸(app shell)을Cache-Control: no-cache로 제공하고, 서비스 워커(service worker)를 네트워크 우선(network-first)으로 설정했으며, 업데이트된 서비스 워커가 작동할 때 일회성 재로드를 추가했습니다.
클라이언트 측에서 흥미로운 부분은 개인 정보 보호 단계입니다. 1024px로 다운스케일링하고, 캔버스(canvas)를 통해 다시 인코딩하며 (이 재인코딩 과정에서 EXIF와 GPS가 제거됩니다), 그 후 데이터 URL을 POST합니다. 데스크톱에서는 웹캠을 열고, Continuity Camera가 있는 Mac에서는 사용자의 iPhone을 자동으로 카메라로 선택합니다.
배포는 Render를 사용합니다: npm install을 빌드하고, node server.js로 시작하며, /api/health에서 상태 확인을 하고, 서비스 환경에 키를 입력합니다. GitHub App을 통해 main 브랜치에 병합하면 자동으로 배포됩니다.
오픈 혁신이 중요한 이유?
앱의 가장 좋은 속성들이 AI가 개방적(open)이기 때문에 가능합니다. '오픈'이라는 부분을 제거하면 앱은 작동을 멈추거나 무료가 아니게 됩니다.
무료일 수 있다
기본 모델은 오픈 웨이트(open-weight)이며 무료 티어에서 제공되므로, 토큰당 비용이 없고 신용카드도 필요 없으며 만료되는 체험 기간도 없습니다. 폐쇄적인 프론티어 스택(frontier stack)은 모든 탭을 청구서의 한 줄로 만들고, 이는 장난감이 재미있어지기 전에 비즈니스가 되어야 함을 의미합니다. 저는 작고 즐거운 무언가를 만들고 그냥 작동하게 두고 싶었습니다.
개인 정보 보호가 설계 선택이 된다, 공급업체의 약관이 아니다
모델을 제가 원하는 곳에 연결할 수 있는 구성 요소로 사용할 때, 저는 그것을 사용하기 위해 데이터 정책을 받아들일 필요가 없습니다. 그 자유야말로 앱을 휴대폰 내부에 머무는 것에 맞춰 디자인할 수 있게 했습니다: 로컬에서 EXIF 정보를 제거하고, 아무것도 저장하지 않으며, 계정도 없습니다. 계정과 원격 측정(telemetry)을 의무화하는 폐쇄적인 API의 경우, 이 모든 선택은 기본값이 아니라 싸움이 됩니다.
종속성 없음 (No lock-in)
제공업체는 설정 라인일 뿐입니다. 호스팅 업체가 느려지거나, 제한 사항을 변경하거나, 적대적으로 변하더라도, 저는 하나의 변수를 재지정하기만 하면 앱은 변함없이 작동합니다. 이것이 AI를 기반으로 구축하는 것과 누군가의 AI 내부에 구축하는 것의 차이점입니다.
주제는 '풀밭에 발을 디디기(touch grass)'이며, 짧은 화면 사용 시간은 화면 자체가 돈을 청구하거나, 위치 정보를 수확하거나, 프로젝트를 인질로 잡고 있지 않을 때만 권장할 수 있습니다. 오픈 웨이트는 이 세 가지 비용을 모두 사라지게 만듭니다.
나의 에이전트 세션
저는 이 전체 프로젝트를 OpenCode에서 구축했으며, DevRelay로 세션을 저장했습니다. 여기에는 빌드 과정, 작업 중이던 임시 제목에서의 이름 변경, 그리고 배포까지 모두 포함되어 있습니다. 저는 대부분의 작업을 할 때 Cambridge Central Library에 앉아 있었는데, 이곳은 사진 찍기에 적합한 창문을 찾기 좋은 장소입니다:
제가 Cambridge Central Library에서 구축하는 모습.
DEV에서 읽기: Haikamera (Touch Grass), 병합된 세션
수상 부문 (Prize Categories)
저는 이 프로젝트가 사용하기 때문에 두 개의 파트너 카테고리에 참가합니다:
- Render 활용 최우수: Haikamera는 Node 웹 서비스로 Render에서 실행되며,
/api/health를 헬스 체크(health check)로 사용하고main브랜치에 병합될 때마다 자동 배포됩니다. - Gemma 활용 최우수: 기본 모델은 오픈 웨이트 Google Gemma 4 31B (
google/gemma-4-31b-it:free)이며, 폴백(fallback) 체인 역시 Gemma 우선으로 구성되어 있습니다.
만약 카메라를 무언가에 비추고 시가 당신이 수백 번 지나쳤던 색깔을 언급한다면, 그것이 이 앱이 제 역할을 하고 있다는 뜻입니다.
이 게시물은 어떻게 작성되었나: 제가 지침과 의견을 제공했고, OpenCode의 AI 에이전트가 산문을 작성했습니다. 결정은 저의 것이고, 단어는 에이전트의 것입니다. 우리가 작업하는 동안 저는 Cambridge Central Library에 있었고, 앱도 같은 방식으로 구축되었습니다.
작성: AI 및 Amirreza.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기