이 글은 Hacktoberfest Open-Source AI Challenge: Week 1, Touch Grass 제출물입니다.
내가 만든 것 (What I Built)
지난 몇 주 동안 시간 가는 줄 몰랐습니다. 시계를 보면 1시인데, 오후인지 한밤중인지 솔직히 알 수 없을 정도였습니다.
저는 프로젝트에 너무 깊이 빠져서 휴식을 취하는 것을 잊었습니다. 이것이 제가 touch-grass를 만들게 된 계기입니다.
이것은 git pre-push hook입니다. 실내에서 3시간(원하는 대로 변경 가능)이 지나면 이 일이 발생합니다. 테스트 중 실제 작동 예시가 있습니다. 제한 시간을 1분으로 설정했을 때의 예시입니다:
$ git push
🌱 touch-grass: 하늘을 본 지 9m 경과. 푸시 차단됨.
...
푸시를 다시 하려면 touchgrass prove를 실행해야 합니다. 그러면 QR 코드가 출력됩니다. 이 코드를 스캔하면 휴대폰 카메라가 열리고, 바깥으로 나가서 사진을 찍게 됩니다.
그런 다음 Gemma 4가 사용자의 노트북에서 이 사진을 보고 유효한지 판단합니다.
데모 (Demo)
영상을 녹화하지 않았기 때문에 대신 테스트 중 실제 prove 실행 과정을 보여드리겠습니다. 터널 위에서 실행하고 코드를 스캔한 다음 바깥으로 나갔습니다:
$ touchgrass prove --tunnel
📱 휴대폰(어디서든)에서 열기:
https://….trycloudflare.com/?t=…
...
Got Fooled By Computer Screen. LOL!
코드: https://github.com/amrit-dash/MLH-touch-grass
사람들이 밖에 나가게 하는 방법 (How It Gets People Outside)
제가 시도해 본 모든
내가 git push가 바깥 공기를 접할 때까지 작동하지 않도록 만들었다
이것은 내가 만들고 설정한 후 금방 잊어버린 것이었습니다. 그러다 푸시(push)가 차단되었고, 화면에서 고개를 들었을 때 바깥에는 해가 지고 있었습니다.
이것을 어떻게 만들었나
필요한 의존성(dependency)은 전혀 없으며, 파이썬 코드 약 700줄과 HTML 페이지 1개로 구성되어 있습니다.
- 훅(hook).
touchgrass install은 pre-push 훅을 작성합니다. 이 훅은 현재 시간과 마지막으로 사진이 전송된 시간을 비교합니다.
- 휴대폰 페이지.
touchgrass prove는 Wi-Fi를 통해 작은 서버를 시작하며, URL에 일회용 토큰(one-time token)이 포함됩니다. 이 페이지는 후면 카메라를 직접 열고(capture="environment"), 사진을 1024px로 축소한 다음 촬영 시간과 함께 전송합니다. 같은 Wi-Fi가 아니면 어떨까요? touchgrass prove --tunnel은 Cloudflare quick tunnel을 통해 페이지에 임시 HTTPS 주소를 부여하여 모바일 데이터에서도 작동하게 합니다.
- 확인 과정. 사진은 Ollama를 통해 Gemma로,
localhost에서 전송됩니다. 프롬프트는 JSON 형식만 요청합니다: outdoors, sky_or_plants, photo_of_screen, 한 줄 설명 및 농담(roast)입니다.
- 판정. 모델이 아니라 순수한 파이썬 코드가 결정합니다. 바깥 공기, 그리고 하늘이나 식물, 그리고 화면이 아닌지 여부입니다.
- 재확인. 통과하는 모든 항목은 화면에 대한 추가 질문(아래에서 더 다룹니다)을 받습니다. 그런 다음 시계가 초기화되고
첫 번째 시도에서는 18개 중 16개를 맞혔습니다. 실제 사진은 모두 정확했습니다. 가짜 사진들은 "잔디 확인됨(Grass confirmed)"으로 통과했지만, "당신의 커밋 히스토리가 잔디처럼 푸르기를 바라!"라는 비난(무례하고 또한 틀린 내용이었습니다)을 받았습니다.
그래서 두 가지를 변경했습니다. 이제 프롬프트는 모델에게 장면을 보기 전에 화면을 찾도록 지시합니다. 그리고 통과하는 모든 사진에 대해 더 좁고 구체적인 질문 하나가 추가되었습니다: 이 사진의 가장자리에는 무엇이 있습니까? 베젤(bezel)입니까? 작은 모델들은 소를 설명하느라 바쁠 때는 모니터 프레임을 알아차리는 데 서툽니다. 하지만 그것만이 유일한 질문일 때에는 잘 해냅니다.
두 번째 시도: 4코어 CPU에 GPU 없이 사진당 약 26초가 걸렸지만, 18개 중 18개를 맞혔습니다. 전체 결과는 docs/benchmark.md에서 확인할 수 있습니다.
(사진 18장은 적은 양이며, 가짜가 실패하는 것을 보고 프롬프트를 수정했으므로 이를 "알려진 구멍이 막혔다"로 이해해 주시고, "완벽하다"고 해석하지 마세요. 또한 이 모델은 소를 매번 말로 인식합니다.)
그러고는 실제로 제 휴대폰으로 테스트해 보았습니다.
첫 번째 실제 사진:
✅ 잔디 확인됨(Grass confirmed). 파란 하늘 아래 빨래가 널려 있는 여러 층의 주거 건물이 보입니다.
"당신의 배포가 세탁 사이클이 끝날 때까지 대기하는 것처럼 보이네요."
꽤 공정했습니다.
다음으로 모니터 위에 잔디밭 사진을 놓고, 휴대폰을 충분히 가까이 대서 잔디가 프레임 전체를 채우도록 했습니다. 베젤도 없고 책상도 없었습니다.
✅ 잔디 확인됨(Grass confirmed). 흐린 하늘 아래 키 큰 잔디의 선명한 녹색 들판이 나무 줄기를 향해 펼쳐져 있습니다.
"당신의 코드가 이 들판처럼 무성하고 푸르지 않다면, 잡초 속에서 디버깅하게 될 겁니다."
저를 간파했습니다. 두 가지 검사 모두 놓쳤습니다.
다시 시도했을 때 브라우저 바의 일부가 모서리 쪽으로 스며들자마자 즉시 포착했습니다:
❌ 이것은 화면 사진입니다. 수고하셨습니다.
저는 풀프레임 사진을 다시 찍고 저장한 다음, Gemma의 사고(thinking) 기능을 켜거나 끄는 등 네 가지 다른 화면 프롬프트로 테스트했습니다. 모든 경우에 실제 현장이라고 나왔습니다. 화면이 프레임을 가득 채우면 볼 것이 별로 없습니다. 약간 너무 푸른 풀 정도입니다.
이것이 알려진 허점입니다. 만약 2분 동안 걸어가는 것을 피하기 위해 휴대폰을 모니터에 대고 싶다면, 당신은 이길 수 있습니다. 하지만 알아차릴 것입니다.
그리고 위 데모의 언덕 비탈은 새로운 HTTPS 터널을 통과했습니다.
왜 오픈(Open)이 여기서 중요한가
이것은 사용자의 위치 사진을 확인합니다. 거리, 뒷마당, 어쩌면 얼굴일 수도 있습니다.
제가 코드를 푸시할 때마다 이것이 다른 사람의 API로 전송되는 것을 원하지 않습니다. 오픈 웨이트 모델(open-weight model)을 사용하면 오직 제 노트북만이 그것을 볼 뿐입니다. ( --tunnel을 사용할 경우, 사진은 가는 도중에 Cloudflare를 통해 암호화되어 지나가지만, 다른 누구의 모델도 보지 못하며 아무것도 저장되지 않습니다.) 또한 체크당 비용이 들지 않고, 오프라인에서 작동하며, 제 규칙이 너무 엄격하다고 생각하더라도 프롬프트를 변경하거나 환경 변수(env var) 하나로 모델을 교체할 수 있습니다.
부족한 점
--no-verify는 이를 건너뜁니다. 이것은 감옥이 아니라 농담으로 하는 재촉입니다.
- 캡처 시간은 휴대폰 브라우저에서 오기 때문에, 결심한 사기꾼은 그것을 위조할 수 있습니다.
- 전체 프레임을 채울 만큼 가까이에 대고 있는 화면은 통과합니다. 저는 이 부분은 프롬프트로 해결할 수 없었습니다.
- 기본적으로 동일한 Wi-Fi를 사용합니다.
--tunnel이 이것을 고치지만, 그러면 사진이 Cloudflare를 거쳐 지나갑니다.
상품 부문