discord 에서 미드저니로 생성한 이미지 저장하는 봇 만들기
요청이 있어서 디스코드에서 미드저니로 생성한 이미지를 저장하는 봇을 만들었다.
하나하나 다운받기도 어렵고, 생성할때 썼던 프롬프트가 뭔지 따로 저장하기도 어렵고 하여...
1. 미드저니, 니지저니로 생성한 이미지를 구글드라이브 공유폴더에 저장한다.
2. 경로는 "[midjourney|nijijourney]/[username]/%Y-%m-%d_%H:%M:%S_[filename]" 으로 한다.
3. 파일 메타정보에 프롬프트 정보를 넣는다
1. discordbot 은 이미 타겟 서버에서 gptbot 을 돌려두고 있었기 때문에, 이녀석의 일을 확장하는걸로 했다.
위 조건을 만족하는 경우 첨부파일을 로컬 디렉토리에 저장하는 함수를 발동한다. 이때 저장하고 나서 파일의 메타정보에 필요한 것들을 넣는다. 대부분의 경우 PNG 파일이 튀어나오고, 가끔가다 webp 를 주기도 하는데, PIL 을 사용해서 메타정보 써넣고 나서 무조건 PNG 로 바꿔 저장하도록 했다.
이 과정에서 JPEG 과는 달리 PNG는 exif 라는 편리한게 없기 때문에 PNG에 메타정보를 넣는 방법을 찾는 삽질이 필요했다. 아니, 메타정보를 넣는것 자체는 어렵지 않은데, 그걸 확인하는게 너무 어려우면 곤란하기 때문에... MacOS 의 기본 preview 에서 볼 수 있게 메타정보를 넣어야 했다. Chatgpt 에게도 물어보고, 검색도 많이 했는데 그냥 가져다 쓰면 끝나는 마법의 라이브러리는 없었다. PIL 에 PngInfo 라는게 있는데, 이게 Textual Information 관련 구현은 해둔 것 같았다. 하지만 아무 키로나 넣으면 뷰어에서 메타정보가 보이지 않는다. (코드나 CLI로는 확인 가능하지만...) 위 링크에 가서 찾을 수 있는 predefined keyword 들만 MacOS 기본 뷰어에서 Inspect(CMD+I) 할때 보여준다.
from PIL import Image
from PIL.PngImagePlugin import PngInfo
def add_description_to_png(image_path, author, desc, software, created_at, comment):
# 이미지 열기
image = Image.open(image_path)
metadata = PngInfo()
metadata.add_text('Author', author)
metadata.add_text('Description', desc)
metadata.add_text('Creation Time', created_at) # 이게 스펙에 있는데
metadata.add_text('CreationTime', created_at) # 이걸로 해야 mac os preview에서 보이더라?
metadata.add_text('Software', software)
metadata.add_text('Comment', comment)
... ... ...
대충 위와 같은 식으로 predefined keyword 를 넣었다. 그런데 스펙에는 "Creation Time" 이라고 쓰여 있는데, 넣어봤더니 안보여서 지원 안하나보다 싶었는데... 혹시나 해서 공백을 없애고 "CreationTime" 으로 넣으니까 Preview 에서 보이더라. 일단 둘다 넣었다. desc에는 미드저니가 이미지 생성할때 사용한 프롬프트를 넣었다. 프롬프트는 미드저니 메시지에서 적당히 뜯어내면 된다.
2. 로컬에 사전에 정한 경로 규칙에 맞춰 저장한 디렉토리를 구글드라이브 공유폴더에 싱크시켜야 한다. 몇 가지 방법을 검색해보고 rclone 을 써서 단방향 동기화를 하기로 했다. 모니터가 없는 remote + headless 리눅스 서버라서 처음에 Oauth 인증 링크를 여느라 ssh 터널링(ssh MYSERVER -L8000:127.0.0.1:8000 으로 연결하고 브라우저에서 127.0.0.1:8000 열기)도 해야 했다.
rclone 사용법도 PNG 메타데이터 넣는 것 처럼 열심히 검색을 해도 친절하게 설명이 되어 있는게 그닥 없고, chatgpt 도 헛소리를 계속 해대서 삽질을 좀 했다.
[my-midjouney-images]
type = drive
client_id = [MY_CLIENT_ID]
client_secret = [MY_CLIENT_SECRET]
scope = drive
token = {"access_token":"[MY_ACCESS_TOKEN]", ....
...
...
rclone sync /home/dgoon/midjourney-images/ my-midjourney-images:midjourney-images -L >> [LOG_FILE] 2>&1
요거를 적당히 쉘스크립트로 감싸서 crontab 에서 자주 실행하게 해준다. 뒤에 -L 옵션은 심볼릭 링크 따라가기 위해 넣은것.
3. 구글 Cloud console 에서 보니까 크론에서 실행될때마다 API 회수가 스스슥 올라간다. 파일이 새로 생긴게 없어도 API 콜은 하나보다. 그렇다고 1분마다 하지 않고 5분이나 10분마다 싱크하면 불편하잖아? 그래서 rclone sync 를 하는 쉘스크립트에서 변한게 없으면 NOOP 으로 마치도록 넣었다. 이러면 실제로 싱크할 내용이 있을때에만 API를 호출한다. sync 할 내용이 있는지 판단하는 가장 간단한 방법을 고민해봤다.
둘중 후자로 했다.
ls -RL /home/dgoon/midjourney-images/ | md5sum
이렇게 하면 저 디렉토리 밑에 있는 파일 목록을 쭉 뱉어준다. 그걸 md5sum 으로 남겨서 이전과 비교하는 방식으로 했다.
추가로, sync 하면서 1분 넘게 걸릴 수 있으니까 실행하면 PID 저장해두고, 실행중인 PID 가 있으면 NOOP 으로 끝내도록 해두었다. 이런 구현을 할 때에는 예외상황이 발생해서 PID 제거가 되지 않으면 영원히 NOOP 되어 버릴 수 있으니 주의해야 한다. 로그를 보니 파일 하나가 생성된 경우 싱크에 6~7 초 정도 걸린다. 혼자서는 어렵겠지만 여러 사람이 이미지를 많이 생성하고 있다면 1분이상 걸릴 가능성도 있겠다.
위 작업을 마치고 나서 디스코드에서 이미지를 생성하면
구글드라이브 폴더에 이렇게 나타난다.
Preview 로 열어서 CMD+I 로 인스펙터를 띄워보면 아래처럼 메타정보를 볼 수 있다.
---
끗!