How to Access SSH in Bluehost Web Hosting

안녕하세요. 오늘은 Bluehost로 웹호스팅을 받고 있는 상태에서 SSH로 서버에 접속하는 방법에 대해서 알려드리도록 하겠습니다.

일단 블루호스트에 로그인을 하시면 제일 먼저 웹호스팅화면을 보여줍니다. 스크롤을 밑으로 쭉 내리시면 Quick Links에 CPANEL이라는 버튼이 보이실거에요. 그걸 클릭하세요.

그러면 cPanel이 뜰거거든요. 스크롤을 쭉 내리면 Security섹션이 나와요. 거기에서 젤 처음 메뉴 SSH Access를 클릭하세요.

그러면 SSH Access페이지가 뜨는데요. 거기에서 Manage SSH Keys버튼을 눌러주세요.

그러면 다음과같이 설명이 나오는데, + Generate a New Key버튼을 클릭해주세요.

그러면 다음과 같이 암호키의 이름과 비번등을 넣고 Generate Key를 누르시면 키가 생성됩니다. 여기서 Key Type은 RSA를 선택하세요. RSA는 공개키와 개인키, 이렇게 두개의 키를 생성해서 공개키를 사용해서 암호화를 하고, 나중에 받아서 암호화된 내용을 복호화할때는 개인키를 이용합니다.

그러면 다음과 같이 RSA키가 생성됩니다. 보시다시피 Public Keyd와 Private Key가 한쌍이 나란히 생성이 되었어요.

공개키는 모두에게 공개된 키로 암호화 할때 사용되며, 여기서 중요한건 개인키 Private Key입니다. Private Keys의 id_rsa옆에 있는 View/Download링크를 클릭하세요. 그러면 아래와 같이 개인키를 보여줍니다. 밑에 Download Key버튼을 눌러서 다운 받습니다.

Downloads폴더에 가시면 id_rsa이라는 파일이 다운 받아져 있을거에요.

# 일단 본인의 홈디렉토리로 들어가세요
cd ~
# 여기에서 .ssh폴더를 생성합니다
mkdir .ssh
# 생성한 폴더에 들어갑니다
cd .ssh
# 그 안에 bluehost라는 폴더를 만듭니다
mkdir bluehost
# 그리고 다운받은 id_rsa파일을 해당 파일로 옮겨줍니다
mv ~/Downloads/id_rsa ~/.ssh/bluehost/

이제 SSH에 접속해볼게요. 아래 명령어에서 아이디와 도메인을 본인의 것으로 변경한뒤, 터미널에서 실행해주세요.

ssh 아이디@도메인 -p 2222 -i ~/.ssh/bluehost/id_rsa

위의 명령어에서 혹시 아이디를 모르시면 처음에 CPANEL클릭했던 버튼 기억하시죠? 그 옆에 보면 Server Information이 있습니다. 거기에 SSH Keys라고 보이시죠? 그 바로 밑에 아이디@아이피가 있어요. 거기서 아이디를 가져오시면 됩니다. 그리고 도메인은 여러분의 도메인입니다(i.e. 어쩌구.com).

ssh 접속명령어를 실행하면 아래와 같이 서버에 SSH로 접속이 실행됩니다.

References

TTS in Python

이번시간에는 파이썬으로 텍스트-음성 변환하는 라이브러리들을 살펴보도록하겠습니다. 본강의는 맥북기준으로 진행됩니다.

pyttsx3

우선 대표적인 것으로는 pyttsx3가 있는데, 단 버젼은 pyttsx3==2.99로 설치하셔야합니다. 최근버젼에 버그가 있어서 현재시간으로는 아직 개선중인것 같습니다(참고). 2.99버젼은 pip에는 없고 https://test.pypi.org/simple/에서 가져다 설치하셔야해요.

pip install --no-cache-dir --extra-index-url https://test.pypi.org/simple/ pyttsx3==2.99

코드는 아래와 같이 사용합니다.

import pyttsx3

engine = pyttsx3.init()
text = "Hello, world!"
engine.save_to_file(text, "output_pyttsx3.mp3")
engine.runAndWait()

추가적인 옵션으로는 속도를 변경하려면 rate속성을 바꾸면 됩니다.

engine.setProperty('rate', 125)

볼륨을 변경하시려면 volume의 값을 변경하시면 되는데, 0에서 1사이의 값을 넣습니다.

engine.setProperty('volume',0.9)

그리고 목소리도 변경할 수가 있는데, voice속성에 변경할 voice ID를 할당하시면 됩니다.

voices = engine.getProperty('voices')
engine.setProperty('voice', voices[66].id)

참고로 pyttsx3에서 지원하는 voice들은 아래와 같습니다(배열방번호, ID, name, 언어, 성별, 나이).

0,com.apple.speech.synthesis.voice.Agnes,Agnes,en_US,VoiceGenderFemale,35
1,com.apple.speech.synthesis.voice.Albert,Albert,en_US,VoiceGenderNeuter,30
2,com.apple.speech.synthesis.voice.Alex,Alex,en_US,VoiceGenderMale,35
3,com.apple.speech.synthesis.voice.alice,Alice,it_IT,VoiceGenderFemale,35
4,com.apple.speech.synthesis.voice.allison.premium,Allison,en_US,VoiceGenderFemale,35
5,com.apple.speech.synthesis.voice.alva,Alva,sv_SE,VoiceGenderFemale,35
6,com.apple.speech.synthesis.voice.amelie,Amelie,fr_CA,VoiceGenderFemale,35
7,com.apple.speech.synthesis.voice.anna,Anna,de_DE,VoiceGenderFemale,35
8,com.apple.speech.synthesis.voice.ava.premium,Ava,en_US,VoiceGenderFemale,35
9,com.apple.speech.synthesis.voice.BadNews,Bad News,en_US,VoiceGenderNeuter,50
10,com.apple.speech.synthesis.voice.Bahh,Bahh,en_US,VoiceGenderNeuter,2
11,com.apple.speech.synthesis.voice.Bells,Bells,en_US,VoiceGenderNeuter,100
12,com.apple.speech.synthesis.voice.Boing,Boing,en_US,VoiceGenderNeuter,1
13,com.apple.speech.synthesis.voice.Bruce,Bruce,en_US,VoiceGenderMale,35
14,com.apple.speech.synthesis.voice.Bubbles,Bubbles,en_US,VoiceGenderNeuter,0
15,com.apple.speech.synthesis.voice.carmit,Carmit,he_IL,VoiceGenderFemale,35
16,com.apple.speech.synthesis.voice.Cellos,Cellos,en_US,VoiceGenderNeuter,50
17,com.apple.speech.synthesis.voice.damayanti,Damayanti,id_ID,VoiceGenderFemale,35
18,com.apple.speech.synthesis.voice.daniel,Daniel,en_GB,VoiceGenderMale,35
19,com.apple.speech.synthesis.voice.Deranged,Deranged,en_US,VoiceGenderNeuter,30
20,com.apple.speech.synthesis.voice.diego,Diego,es_AR,VoiceGenderMale,35
21,com.apple.speech.synthesis.voice.ellen,Ellen,nl_BE,VoiceGenderFemale,35
22,com.apple.speech.synthesis.voice.fiona,Fiona,en-scotland,VoiceGenderFemale,35
23,com.apple.speech.synthesis.voice.Fred,Fred,en_US,VoiceGenderMale,30
24,com.apple.speech.synthesis.voice.GoodNews,Good News,en_US,VoiceGenderNeuter,8
25,com.apple.speech.synthesis.voice.Hysterical,Hysterical,en_US,VoiceGenderNeuter,30
26,com.apple.speech.synthesis.voice.ioana,Ioana,ro_RO,VoiceGenderFemale,35
27,com.apple.speech.synthesis.voice.joana,Joana,pt_PT,VoiceGenderFemale,35
28,com.apple.speech.synthesis.voice.jorge,Jorge,es_ES,VoiceGenderMale,35
29,com.apple.speech.synthesis.voice.juan,Juan,es_MX,VoiceGenderMale,35
30,com.apple.speech.synthesis.voice.Junior,Junior,en_US,VoiceGenderMale,8
31,com.apple.speech.synthesis.voice.kanya,Kanya,th_TH,VoiceGenderFemale,35
32,com.apple.speech.synthesis.voice.karen,Karen,en_AU,VoiceGenderFemale,35
33,com.apple.speech.synthesis.voice.Kathy,Kathy,en_US,VoiceGenderFemale,30
34,com.apple.speech.synthesis.voice.kyoko,Kyoko,ja_JP,VoiceGenderFemale,35
35,com.apple.speech.synthesis.voice.laura,Laura,sk_SK,VoiceGenderFemale,35
36,com.apple.speech.synthesis.voice.lekha,Lekha,hi_IN,VoiceGenderFemale,35
37,com.apple.speech.synthesis.voice.luca,Luca,it_IT,VoiceGenderMale,35
38,com.apple.speech.synthesis.voice.luciana,Luciana,pt_BR,VoiceGenderFemale,35
39,com.apple.speech.synthesis.voice.maged,Maged,ar_SA,VoiceGenderMale,35
40,com.apple.speech.synthesis.voice.mariska,Mariska,hu_HU,VoiceGenderFemale,35
41,com.apple.speech.synthesis.voice.meijia,Mei-Jia,zh_TW,VoiceGenderFemale,35
42,com.apple.speech.synthesis.voice.melina,Melina,el_GR,VoiceGenderFemale,35
43,com.apple.speech.synthesis.voice.milena,Milena,ru_RU,VoiceGenderFemale,35
44,com.apple.speech.synthesis.voice.moira,Moira,en_IE,VoiceGenderFemale,35
45,com.apple.speech.synthesis.voice.monica,Monica,es_ES,VoiceGenderFemale,35
46,com.apple.speech.synthesis.voice.nora,Nora,nb_NO,VoiceGenderFemale,35
47,com.apple.speech.synthesis.voice.paulina,Paulina,es_MX,VoiceGenderFemale,35
48,com.apple.speech.synthesis.voice.Organ,Pipe Organ,en_US,VoiceGenderNeuter,500
49,com.apple.speech.synthesis.voice.Princess,Princess,en_US,VoiceGenderFemale,8
50,com.apple.speech.synthesis.voice.Ralph,Ralph,en_US,VoiceGenderMale,50
51,com.apple.speech.synthesis.voice.rishi,Rishi,en_IN,VoiceGenderMale,35
52,com.apple.speech.synthesis.voice.samantha.premium,Samantha,en_US,VoiceGenderFemale,35
53,com.apple.speech.synthesis.voice.sara,Sara,da_DK,VoiceGenderFemale,35
54,com.apple.speech.synthesis.voice.satu,Satu,fi_FI,VoiceGenderFemale,35
55,com.apple.speech.synthesis.voice.sinji,Sin-ji,zh_HK,VoiceGenderFemale,35
56,com.apple.speech.synthesis.voice.tessa,Tessa,en_ZA,VoiceGenderFemale,35
57,com.apple.speech.synthesis.voice.thomas,Thomas,fr_FR,VoiceGenderMale,35
58,com.apple.speech.synthesis.voice.tingting,Ting-Ting,zh_CN,VoiceGenderFemale,35
59,com.apple.speech.synthesis.voice.Trinoids,Trinoids,en_US,VoiceGenderNeuter,2001
60,com.apple.speech.synthesis.voice.veena,Veena,en_IN,VoiceGenderFemale,35
61,com.apple.speech.synthesis.voice.Vicki,Vicki,en_US,VoiceGenderFemale,35
62,com.apple.speech.synthesis.voice.Victoria,Victoria,en_US,VoiceGenderFemale,35
63,com.apple.speech.synthesis.voice.Whisper,Whisper,en_US,VoiceGenderNeuter,30
64,com.apple.speech.synthesis.voice.xander,Xander,nl_NL,VoiceGenderMale,35
65,com.apple.speech.synthesis.voice.yelda,Yelda,tr_TR,VoiceGenderFemale,35
66,com.apple.speech.synthesis.voice.yuna,Yuna,ko_KR,VoiceGenderFemale,35
67,com.apple.speech.synthesis.voice.yuri,Yuri,ru_RU,VoiceGenderMale,35
68,com.apple.speech.synthesis.voice.Zarvox,Zarvox,en_US,VoiceGenderNeuter,1
69,com.apple.speech.synthesis.voice.zosia,Zosia,pl_PL,VoiceGenderFemale,35
70,com.apple.speech.synthesis.voice.zuzana,Zuzana,cs_CZ,VoiceGenderFemale,35

음성출력은 아래와 같이 say()함수에 말할 텍스트를 넘겨주면 됩니다.

engine.say("Hello World!")

음성파일을 저장하시려면 아래와 같이 save_to_file()함수에 텍스트와 저장할 파일경로를 넘겨주시면 됩니다.

engine.save_to_file("Hello World!", "output_pyttsx3.mp3")

어떤 이유에서인지 pyttsx3는 mpg123로는 출력이 안되네요.

gtts

이번에는 gtts를 이용해서 TTS를 구현해 보도록 하겠습니다.

gtts를 설치해주세요.

pip install gtts

그리고 tts_with_gtts.py를 생성해서 아래 코드를 저장해주세요.

from gtts import gTTS

tts = gTTS("Hello, world!", lang="en")
tts.save("output_gtts.mp3")

실행해 보시면 output_gtts.mp3가 생성되어 있을거에요.

python tts_with_gtts.py

gtts로 만든 파일은 mpg123를 이용해서 재생할 수 있습니다.

mpg123 output_gtts.mp3

그런데 얘는 다양한 목소리 지원이나 속도, 볼륨조절등의 옵션은 없어요.

AI Avatar – 6. Play the Voiceover with the Avatar Animation

이번시간에는 TTS로 생성된 음성파일을 아바타 애니메이션과 통합하는 방법에 대해서 알려드리겠습니다.

지난 시간에 음성파일 재생을 지원하기 위해 Mac에 mpg123 라이브러리를 설치했는데요 안하신 분이 계시면 꼭 설치를 해주시기 바랍니다.

brew install mpg123

그리고 일전에 배웠던 아바타 애니메이션 코드에 아래 함수를 추가해주세요.

def play_voiceover(file_path):
    os.system(f"mpg123 {file_path} &")

def animate_avatar_with_voice(images, voiceover_path):
    play_voiceover(voiceover_path)
    animate_avatar(images)

윈도우 사용자들은 위의 play_voiceover()함수에서 음성파일 실행하는 명령어를 start로 바꿔주세요.

os.system(f'start {file_path}')

그리고 코드 맨 끝에서 animate_avatar_with_voice()함수를 호출해줍니다.

animate_avatar_with_voice(opencv_images, 'avatar_voiceover.mp3')

완성된 코드는 다음과 같습니다.

import os
import cv2
import dlib
import numpy as np
from PIL import Image

def get_files_with_extensions(directory, extensions):
    # get multiple files
    files = os.listdir(directory)
    if len(extensions) > 0:
        return [f for f in files if os.path.splitext(f)[1].lower() in extensions]
    else:
        return files

# dlib 얼굴 검출기 로드
detector = dlib.get_frontal_face_detector()

# 이미지에서 얼굴을 추출하는 함수
def extract_face(image_path):
    image = cv2.imread(image_path)
    faces = detector(image)
    if  len(faces) > 0:
        face = faces[0]   # 발견된 첫 번째 얼굴 추출
        x, y, w, h = face.left(), face.top(), face.width(), face.height()
        face_image = image[y:y+h, x:x+w]
        return cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB)
    else :
        return  None

# 여러 얼굴을 추출하여 반환
def extract_faces_from_arr(image_paths):
    faces = []
    for path in image_paths:
        face = extract_face(path)
        if face is not None :
            faces.append(face)
    return faces

# 얼굴과 비디오 프레임을 합성 아바타로 통합하는 함수
def extract_faces_from_folder(frame_dir):
    # 프레임에서 얼굴 추출하여 배열에 저장
    frame_faces = []
    for frame_file in get_files_with_extensions(frame_dir, ['.jpg', '.png']):
        frame_path = os.path.join(frame_dir, frame_file)
        frame_image = extract_face(frame_path)   # extract_face 함수 재사용
        if frame_image is not None :
            frame_faces.append(frame_image)
    return frame_faces
    
def merge_faces(image_faces, frame_faces):
    return image_faces + frame_faces

def animate_avatar(images):
    cv2.namedWindow('Avatar Animation', cv2.WINDOW_NORMAL)
    for i in range(30):  # Number of animation cycles
        for image in images:
            cv2.imshow('Avatar Animation', image)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
    cv2.destroyAllWindows()

def play_voiceover(file_path):
    os.system(f"mpg123 {file_path} &")

def animate_avatar_with_voice(images, voiceover_path):
    play_voiceover(voiceover_path)
    animate_avatar(images)

# 얼굴을 추출할 이미지 경로
image_paths = ['image1.png', 'image2.png', 'image3.png']
image_faces = extract_faces_from_arr(image_paths)

# 동영상 추출 프레임 이미지 폴더 경로
folder_path = 'frames'
frame_faces = extract_faces_from_folder(folder_path)

all_faces = merge_faces(image_faces, frame_faces)

# Convert the PIL images to OpenCV format for animation
opencv_images = [cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) for img in all_faces]

# Animate the avatar
animate_avatar_with_voice(opencv_images, 'avatar_voiceover.mp3')

실행을 해보기 전에 애니메이션 할때 사용했던 이미지파일들과 만들어 두었던 TTS음성파일을 같은 폴더에 저장합니다. 음성파일 복사해오는 대신 TTS음성파일 만드는 코드를 여기에 갖다 붙여서 사용해도 좋구요.

python voiceover.py

실행해보시면 얼굴들이 미친듯이 돌아가면서 음성파일이 실행됩니다. 전혀 AI 아바타같지는 않은데…이걸로 뭘하라는 건지 잘 모르겠네요..글쓴이는 뭔가 그럴싸한 아이디어를 주었다고 생각하는거 같은데 사실 이건 그 사람이 실제 개발하고 있는 코드는 밥그릇째 내줄수 없다 뭐 그런 느낌이에요. 여기에서 뭔가 스스로 발전시키라는 의도인것 같은데 암튼 좀더 리서치를 해보고 좋은 글을 찾아서 다시 공유하도록 하겠습니다.

References

AI Avatar – 5. Create Voice Over from Text to Speech

안녕하세요. 이번시간에는 지난시간까지 만들었던 아바타에 음성기능을 추가하기 위해서 텍스트를 음성으로 변환하는 라이브러리들을 배워볼거에요.

Text to Speech

pyttsx3

아바타에 텍스트 음성 변환(TTS) 기능을 추가하려면 pyttsx3, gTTS, 또는 pydub과 같은 Python 라이브러리를 사용할 수 있는데요. 본 강의에서는 오프라인에서도 잘 작동하고 다양한 TTS 엔진을 지원하는 pyttsx3를 사용하여 음성기능을 추가하는 방법을 진행하도록 하겠습니다.

필요한 라이브러리를 추가로 설치해주세요.

pip install pyttsx3

tts_by_pyttsx3.py라는 파일을 하나 생성하세요. 그리고 pyttsx3라이브러리를 포함합니다.

import pyttsx3

그리고 TTS엔진을 초기화 시킵니다.

# Initialize the text-to-speech engine
engine = pyttsx3.init()

다음으로는 텍스트를 음성으로 변환해주는 함수, text_to_speech()를 선언해주세요. 인자로는 변환할 텍스트와 음성파일 저장위치 입니다. 엔진의 setProperty()함수를 통해서 속도나 볼륨등을 조절한 뒤, 엔진의 say()함수를 호출해서 프로그램이 실행되면 텍스트를 음성으로 변환하여 재생하도록 합니다. 엔진에 텍스트와 음성파일의 저장위치도 전달하여 파일을 저장합니다. 함수의 맨 끝에 runAndWait()함수를 호출하여 음성재생이 끝날때까지 프로그램의 종료를 지연시킵니다.

# Function to convert text to speech
def text_to_speech(text, save_path=None):
    engine.say(text)
    
    # Optionally save the speech to a file
    if save_path:
        engine.save_to_file(text, save_path)
    
    engine.runAndWait()

임의의 문자열을 변수에 저장하고, text_to_speech()함수에 인자로 전달하여 호출합니다. 이때, 저장할 파일명도 함께 전달합니다. 참고로 속도나 볼륨, 다른 목소리 사용하기등 pyttsx3에 대한 추가옵션을 여기에서 확인하세요.

# Example text for the avatar to speak
text = "Hello! I am your AI avatar. Nice to meet you!"

# Convert the text to speech and save it to a file
text_to_speech(text, save_path='avatar_voiceover.mp3')

완성된 코드는 아래와 같습니다.

import pyttsx3

# Initialize the text-to-speech engine
engine = pyttsx3.init()

# Function to convert text to speech
def text_to_speech(text, save_path=None):
    engine.say(text)
    
    # Optionally save the speech to a file
    if save_path:
        engine.save_to_file(text, save_path)
    
    engine.runAndWait()

# Example text for the avatar to speak
text = "Hello! I am your AI avatar. Nice to meet you!"

# Convert the text to speech and save it to a file
text_to_speech(text, save_path='avatar_voiceover.mp3')

실행해볼게요.

python text_to_speech.py

해당 텍스트를 프로그램 실행하니까 바로 음성으로 제공되고, 해당 음성이 같은 폴더에 avatar_voiceover.mp3로 저장도 됩니다. Mac에서 mp3결과물을 실행해보려면 mpg123같은 음성재생라이브러리가 필요합니다.

brew install mpg123

어떤 이유에서 인지 pyttsx3로 생성한 mp3파일은 mpg123으로 재생이 안되고 에러가 나는데 이건 pyttsx3패키지 문제인것 같습니다.

$ mpg123 output_pyttsx3.mp3
High Performance MPEG 1.0/2.0/2.5 Audio Player for Layers 1, 2 and 3
	version 1.32.10; written and copyright by Michael Hipp and others
	free software (LGPL) without any warranty but with best wishes


Terminal control enabled, press 'h' for listing of keys and functions.

Playing MPEG stream 1 of 1: output_pyttsx3.mp3 ...

MPEG 1.0 L II cbr379 44100 stereo
[src/libmpg123/getbits.h:getbits():46] error: Tried to read 16 bits with -13 available.
[src/libmpg123/layer2.c:INT123_do_layer2():365] error: missing bits in layer II step two
[src/libmpg123/getbits.h:getbits():46] error: Tried to read 16 bits with -13 available.
[src/libmpg123/getbits.h:getbits():46] error: Tried to read 16 bits with -29 available.
[src/libmpg123/getbits.h:getbits():46] error: Tried to read 16 bits with -45 available.
[src/libmpg123/layer2.c:INT123_do_layer2():365] error: missing bits in layer II step two
Note: Illegal Audio-MPEG-Header 0x00f00210 at offset 6636.
Note: Trying to resync...
Note: Skipped 466 bytes in input.

Warning: Big change from first (MPEG version, layer, rate). Frankenstein stream?
> 02+65  00:00.20+00:06.79 --- 100=100   0 kb/s 1237 B acc 1656 clip p+0.000
MPEG 2.5 L II cbr95 11025 j-s
Note: Illegal Audio-MPEG-Header 0x2bd520d3 at offset 8339.
Note: Trying to resync...
Note: Skipped 658 bytes in input.

Warning: Big change from first (MPEG version, layer, rate). Frankenstein stream?
> 03+76  00:00.14+00:03.65 --- 100=100  80 kb/s  481 B acc 1179 clip p+0.000
MPEG 2.5 L III cbr80 12000 mono
Note: Illegal Audio-MPEG-Header 0xe027de53 at offset 9478.
Note: Trying to resync...
Note: Skipped 1024 bytes in input.
[src/libmpg123/parse.c:wetwork():1389] error: Giving up resync after 1024 bytes - your stream is not nice... (maybe increasing resync limit could help).
main: [src/mpg123.c:play_frame():866] error: ...in decoding next frame: Failed to find valid MPEG data within limit on resync. (code 28)

This was a Frankenstein track.
[0:00] Decoding of output_pyttsx3.mp3 finished.

gtts

이번에는 gtts를 이용해서 TTS를 구현해 보도록 하겠습니다.

gtts를 설치해주세요.

pip install gtts

그리고 tts_by_gtts.py를 생성해서 아래 코드를 저장해주세요.

from gtts import gTTS

tts = gTTS("Hello, world!", lang="en")
tts.save("output_gtts.mp3")

실행해 보시면 output_gtts.mp3가 생성되어 있을거에요.

python tts_by_gtts.py

gtts로 만든 파일은 mpg123를 이용해서 재생할 수 있습니다.

mpg123 output_gtts.mp3

그런데 얘는 다양한 목소리 지원이나 속도, 볼륨조절등의 옵션은 없어요. 그럼 다음시간에 마지막으로 애니메이션과 음성을 병합하는 코드 함께 해볼게요.

References

AI Avatar – 4. Animate the Avatar

이번시간에는 지난시간에 만든 아바타를 가지고 움직이게 만들어 볼거에요. 지난 시간에 만든 코드에 덧붙여서 만들거에요. 반드시 지난 시간 코드를 알고 계셔야합니다.

코드의 맨 마지막에 아래 코드를 추가합니다. 아래 코드는 기존에 이미지와 동영상프레임 이미지들에서 획득한 얼굴이미지들을 하나씩 돌면서 RGB형식으로 만들어진 이미지를 BGR로 변환합니다.

opencv_images = [cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) for img in faces]

그리고 아바타에 움직을 추가할 함수, animate_avatar()를 선언합니다. 인자로는 얼굴 이미지들을 넘겨받고, 이미지들을 빠르게 돌려서 움직이는 것 처럼 보이게 만듭니다.

def animate_avatar(images):
    cv2.namedWindow('Avatar Animation', cv2.WINDOW_NORMAL)
    for i in range(30):  # Number of animation cycles
        for image in images:
            cv2.imshow('Avatar Animation', image)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
    cv2.destroyAllWindows()

# Animate the avatar
animate_avatar(opencv_images)

아래는 완성된 코드입니다.

import os
import cv2
import dlib
import numpy as np
from PIL import Image

def get_files_with_extensions(directory, extensions):
    # get multiple files
    files = os.listdir(directory)
    if len(extensions) > 0:
        return [f for f in files if os.path.splitext(f)[1].lower() in extensions]
    else:
        return files

# dlib 얼굴 검출기 로드
detector = dlib.get_frontal_face_detector()

# 이미지에서 얼굴을 추출하는 함수
def extract_face(image_path):
    image = cv2.imread(image_path)
    faces = detector(image)
    if  len(faces) > 0:
        face = faces[0]   # 발견된 첫 번째 얼굴 추출
        x, y, w, h = face.left(), face.top(), face.width(), face.height()
        face_image = image[y:y+h, x:x+w]
        return cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB)
    else :
        return  None

# 여러 얼굴을 추출하여 반환
def extract_faces_from_arr(image_paths):
    faces = []
    for path in image_paths:
        face = extract_face(path)
        if face is not None :
            faces.append(face)
    return faces

# 얼굴과 비디오 프레임을 합성 아바타로 통합하는 함수
def extract_faces_from_folder(frame_dir):
    # 프레임에서 얼굴 추출하여 배열에 저장
    frame_faces = []
    for frame_file in get_files_with_extensions(frame_dir, ['.jpg', '.png']):
        frame_path = os.path.join(frame_dir, frame_file)
        frame_image = extract_face(frame_path)   # extract_face 함수 재사용
        if frame_image is not None :
            frame_faces.append(frame_image)
    return frame_faces
    
def merge_faces(image_faces, frame_faces):
    return image_faces + frame_faces

def animate_avatar(images):
    cv2.namedWindow('Avatar Animation', cv2.WINDOW_NORMAL)
    for i in range(30):  # Number of animation cycles
        for image in images:
            cv2.imshow('Avatar Animation', image)
            if cv2.waitKey(1) & 0xFF == ord('q'):
                break
    cv2.destroyAllWindows()

# 얼굴을 추출할 이미지 경로
image_paths = ['image1.png', 'image2.png', 'image3.png']
image_faces = extract_faces_from_arr(image_paths)

# 동영상 추출 프레임 이미지 폴더 경로
folder_path = 'frames'
frame_faces = extract_faces_from_folder(folder_path)

all_faces = merge_faces(image_faces, frame_faces)

# Convert the PIL images to OpenCV format for animation
opencv_images = [cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) for img in all_faces]

# Animate the avatar
animate_avatar(opencv_images)

실행을 해보도록 하겠습니다. 본 파일을 실행전에 얼굴에 사용할 이미지 3개와 동영상에서 캡쳐한 프레임이미지들이 frames에 들어있는지 확인한 뒤 실행합니다.

python animate_avatar.py

실행을 하면 팝업이 하나 뜨는데 그 안에서 지금까지 생성한 얼굴들이 빠르게 돌아갑니다. 중간에 멈추고 싶을때는 Ctrl+C를 누르면 종료합니다.

아직까지는 그닥 아바타 같아보이지가 않아서, 상용화가 가능하도록 다듬는 부분에 있어서는 아무래도 따로 리서치를 해서 추가로 블로깅을 해야할것 같습니다. 생각보다 실망스러운데 일단 다음시간에 하는 더빙까지 붙여 보도록 하겠습니다.

References

AI Avatar – 3. Integrate Images and Video Frames to Create the Avatar

이번시간에는 지난 강좌에서 만든 얼굴 이미지와 동영상에서 추출한 키프레임들을 가지고 아바타를 만들어 보도록 하겠습니다.

지난 강좌를 따라서 여기까지 오셨다면 여기에서 추가로 설치해야할 라이브러리는 없습니다. create_avatar.py라는 파일을 하나 생성하고 필요한 라이브러리를 코드에 추가해주세요.

import os
import cv2
import dlib
import numpy as np
from PIL import Image

첫번째 시간에 만들어 두었던 얼굴 추출하는 함수를 여기에서 한번더 활용하도록 하겠습니다. 이번에는 동영상 프레임에서 가져온 이미지의 얼굴을 추출하는데 사용될거에요. 참고로 동영상에서 프레임을 추출해서 아바타에 사용하는 이유는 이미지만 가지고 했을때 자연스럽지 않은 부분들을 보충하기 위함이에요. 이미지만 가지고도 충분히 해결할 수 있지만 동영상에서 추출하는게 아무래도 더욱 풍부한 얼굴표현을 가능하게 해주니까요. 이미지는 많으면 많을수록 좋습니다.

# dlib 얼굴 검출기 로드
detector = dlib.get_frontal_face_detector()

# 이미지에서 얼굴을 추출하는 함수
def extract_face(image_path):
    image = cv2.imread(image_path)
    faces = detector(image)
    if  len(faces) > 0:
        face = faces[0]   # 발견된 첫 번째 얼굴 추출
        x, y, w, h = face.left(), face.top(), face.width(), face.height()
        face_image = image[y:y+h, x:x+w]
        return cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB)
    else :
        return  None

그리고 첫번째 강좌에서 사용했던 코드 일부를 가져옵니다. 이미지경로를 배열에 저장하고, 얼굴들만 추출해서 배열에 담는 코드를 가져와서 추가합니다.

def extract_faces_from_arr(image_paths):
    faces = []
    for path in image_paths:
        face = extract_face(path)
        if face is not None :
            faces.append(face)
    return faces

image_paths = ['image1.png', 'image2.png', 'image3.png']
image_faces = extract_faces_from_arr(image_paths)

그리고 동영상에서 추출한 키프레임이미지들에서 얼굴을 획득하는 함수 extract_faces_from_folder()를 선언합니다. 동영상에서 추출한 프레임 이미지가 들어가있는 폴더 경로를 넘겨받아서 해당 폴더의 이미지파일들을 불러오고, 얼굴들을 전부 추출해서 배열에 담습니다.

def extract_faces_from_folder(frame_dir):
    # 프레임에서 얼굴 추출하여 배열에 저장
    frame_faces = []
    for frame_file in get_files_with_extensions(frame_dir, ['.jpg', '.png']):
        print(frame_file)
        frame_path = os.path.join(frame_dir, frame_file)
        frame_image = extract_face(frame_path)   # extract_face 함수 재사용
        if frame_image is not None :
            frame_faces.append(frame_image)
    return frame_faces

folder_path = 'frames'
frame_faces = extract_faces_from_folder(folder_path)

그리고 넘겨받은 얼굴배열과, 프레임에서 추출한 얼굴배열을 병합하여 하나의 배열로 만듭니다.

def merge_faces(image_faces, frame_faces):
    return image_faces + frame_faces

all_faces = merge_faces(image_faces, frame_faces)

그리고 배열을 수직으로 쌓아 아바타를 만들고, 해당 배열을 이미지로 만들어 아바타를 어떻게 만들었는지 확인합니다.

def store_faces_in_a_vertical_image(all_images):
    if all_images:
        composite_avatar = np.vstack(all_images)   # 이미지를 수직으로 쌓기
        avatar_image = Image.fromarray(composite_avatar)
        avatar_image.save('final_avatar.jpg')
        avatar_image.show()
    else :
        print("이미지나 프레임에서 유효한 얼굴을 찾을 수 없습니다.")

store_faces_in_a_vertical_image(all_faces)

완성된 코드는 다음과 같습니다.

import os
import cv2
import dlib
import numpy as np
from PIL import Image

def get_files_with_extensions(directory, extensions):
    # get multiple files
    files = os.listdir(directory)
    if len(extensions) > 0:
        return [f for f in files if os.path.splitext(f)[1].lower() in extensions]
    else:
        return files

# dlib 얼굴 검출기 로드
detector = dlib.get_frontal_face_detector()

# 이미지에서 얼굴을 추출하는 함수
def extract_face(image_path):
    image = cv2.imread(image_path)
    faces = detector(image)
    if  len(faces) > 0:
        face = faces[0]   # 발견된 첫 번째 얼굴 추출
        x, y, w, h = face.left(), face.top(), face.width(), face.height()
        face_image = image[y:y+h, x:x+w]
        return cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB)
    else :
        return  None

# 여러얼굴을 추출하여 반환
def extract_faces_from_arr(image_paths):
    faces = []
    for path in image_paths:
        face = extract_face(path)
        if face is not None :
            faces.append(face)
    return faces

# 얼굴과 비디오 프레임을 합성 아바타로 통합하는 함수
def extract_faces_from_folder(frame_dir):
    # 프레임에서 얼굴 추출하여 배열에 저장
    frame_faces = []
    for frame_file in get_files_with_extensions(frame_dir, ['.jpg', '.png']):
        print(frame_file)
        frame_path = os.path.join(frame_dir, frame_file)
        frame_image = extract_face(frame_path)   # extract_face 함수 재사용
        if frame_image is not None :
            frame_faces.append(frame_image)
    return frame_faces
    
def merge_faces(image_faces, frame_faces):
    return image_faces + frame_faces

def store_faces_in_a_vertical_image(all_images):
    if all_images:
        composite_avatar = np.vstack(all_images)   # 이미지를 수직으로 쌓기
        avatar_image = Image.fromarray(composite_avatar)
        avatar_image.save('final_avatar.jpg')
        avatar_image.show()
    else :
        print("이미지나 프레임에서 유효한 얼굴을 찾을 수 없습니다.")

# 얼굴을 추출할 이미지 경로
image_paths = ['image1.png', 'image2.png', 'image3.png']
image_faces = extract_faces_from_arr(image_paths)

# 동영상 추출 프레임 이미지 폴더 경로
folder_path = 'frames'
frame_faces = extract_faces_from_folder(folder_path)

all_faces = merge_faces(image_faces, frame_faces)
store_faces_in_a_vertical_image(all_faces)

그럼 한번 실행을 해볼까요? 실행하기 전에 필요한 파일은 얼굴로 사용할 이미지들과, 동영상에서 추출한 프레임이미지들이 들어있는 폴더입니다.

스크립트를 실행합니다.

python create_avatar.py

에러가 중간에 났는데요. 동영상의 얼굴 퀄리티가 살짝 다른 키프레임을 캡쳐했을때 이런 에러가 나는것 같습니다. 저는 1번 프레임이미지에서 추출한 얼굴이 다른 얼굴과 크기가 살짝 달라서 세로로 갖다 붙이는데 문제가 있다고 아래와 같은 에러가 났어요. 그래서 1번 프레임이미지 지우고 다시 실행했더니 되네요. 한번 해보는거니까 그냥 넘어갈게요.

  File "/Users/me/git/python/ai_avatar/3_create_avatar/create_avatar.py", line 66, in <module>
    create_composite_avatar(faces, output_frame_dir)
    ~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/me/git/python/ai_avatar/3_create_avatar/create_avatar.py", line 47, in create_composite_avatar
    composite_avatar = np.vstack(all_images)   # 이미지를 수직으로 쌓기
  File "/Users/me/git/sol1000.com/cms/.venv/lib/python3.13/site-packages/numpy/_core/shape_base.py", line 291, in vstack
    return _nx.concatenate(arrs, 0, dtype=dtype, casting=casting)
           ~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: all the input array dimensions except for the concatenation axis must match exactly, but along dimension 1, the array at index 0 has size 447 and the array at index 8 has size 536

결과는 다음과 같이 이미지들과 동영상 프레임에서 얼굴만 따서 세로로 주르륵 붙여놓은 형태의 이미지를 만들어 줍니다.

다음 시간에는 이걸 이용해서 아바타를 움직이게 만드는 강좌를 이어서 하도록 하겠습니다.

References

AI Avatar – 2. Extract Key Frames from Video

유투브 동영상 올리면 썸네일 어떤거 할거냐고 고르라고 동영상에서 이미지들 몇개 캡쳐해서 주르륵 보여주잖아요. 이번시간에는 동영상에서 키프레임을 추출하는 코드를 작성해보도록 하겠습니다. 이렇게 추출한 이미지들을 아바타로 활용하면 아무래도 여기저기서 따로 찍은 사진들을 조합해서 만드는 것보다 자연스럽고 일관된 모습의 아바타를 만들수 있기때문에 동영상에서 아바타에 사용할 이미지를 추출하는것을 추천드립니다.

동영상파일을 읽어오는데 필요한 라이브러리는 moviepy입니다. 필요한 라이브러리를 설치해주세요.

pip install moviepy pillow

extract_keyframes.py이라는 파일을 하나 생성해주세요. 그리고 해당라이브러리와 출력파일을 저장하기 위해 필요한 os도 코드에 포함시켜주세요.

import os 
from moviepy import VideoFileClip
from PIL import Image

이제 키프레임을 추출할 동영상을 변수에 담습니다

video_path = 'video2.mp4'

그리고 동영상에서 이미지를 많이 추출해달라고 요청하는 경우에 대비해서 추출한 이미지파일들을 frames라는 폴더안에 생성하도록 할게요.

output_frame_dir = 'frames'

이제 동영상에서 키프레임을 추출하는 함수를 만들어 보도록 하겠습니다. 이 함수는 동영상경로와 저장할 폴더위치, 그리고 추출하고자하는 프레임갯수를 인자로 넘겨받고 동영상을 읽어서 해당위치에 필요한 갯수만큼의 키프레임 캡쳐하여 지정한 폴더에 이미지로 저장해줄겁니다.

def extract_frames( video_path, output_dir, num_frames=10 ): 

일단 VideoFileClip()함수에 인자로 받은 동영상경로를 넘겨주어 비디오 클립 Object를 받아옵니다. 해당 동영상의 길이가 얼마나 되는지를 봐서 생성하고 싶은 키프레임으로 나누어 주면 어느정도 간격으로 프레임을 추출해야 동영상 전반에 걸쳐 고르게 추출할수 있는지가 나오겠죠.

    clip = VideoFileClip(video_path) 
    duration = clip.duration 
    interval = duration / num_frames 

이미지를 저장할 경로가 존재하지 않으면 에러가 나니까 폴더가 없으면 만들어 줍니다.

    if  not os.path.exists(output_dir): 
        os.makedirs(output_dir) 

동영상에서 이미지를 가져오는 방법은 클립 object의 get_frame()함수에게 어느시간대의 프레임을 가져오라고 하면 얘가 배열을 반환합니다. 그러면 Image.fromarray()함수로 배열을 이미지로 변환해서 지정된 폴더에 저장을 합니다.

    for i in range(num_frames): 
        frame_time = i * interval 
        frame = clip.get_frame(frame_time) 
        frame_image = Image.fromarray(frame) 
        frame_image.save( f" {output_dir} /frame_ {i} .jpg" ) 

완성된 코드는 다음과 같습니다.

import os
from moviepy import VideoFileClip
from PIL import Image

# 비디오에서 프레임을 추출하는 함수
def extract_frames(video_path, output_dir, num_frames=10):
    clip = VideoFileClip(video_path)
    duration = clip.duration
    interval = duration / num_frames
   
    if  not os.path.exists(output_dir):
        os.makedirs(output_dir)

    for i in range(num_frames):
        frame_time = i * interval
        frame = clip.get_frame(frame_time)
        frame_image = Image.fromarray(frame)
        frame_image.save(f"{output_dir}/frame_{i}.jpg")

# 프레임 추출 함수 호출
video_path = 'video1.mp4'
output_frame_dir = 'frames'
extract_frames(video_path, output_frame_dir)

그러면 파일을 실행해보도록 하겠습니다. 실행하기 전에 동영상파일을 하나 가져다가 같은 폴더에 저장합니다.

그리고 스크립트를 실행하면

python extract_keyframes.py

다음과 같이 frames라는 폴더 안에 10개의 키프레임이 이미지로 저장이 됩니다.

References

AI Avatar – 1. Extract Faces from Images

강의를 마무리하는 시점에서 첨언을 드리자면, 이거는 AI아바타를 만든다기보다 그냥 이미지 여러개를 번갈아 돌리면서 TTS로 생성된 음성파일을 들려주는 수준의 강좌입니다. 뭔가 입모양이 움직이고 3D로 새로운 얼굴의 아바타가 생성되는 그런 마법같은 일은 생기지 않습니다. 강의 마지막부분에서 결과물에 상당히 실망할수도 있음을 미리 알려드립니다. 그래도 문자열을 음성으로 변환하거나, 이미지에서 얼굴부분만 가져오는 라이브러리등은 알아두면 좋을것 같아요.

안녕하세요. 얼마전 Synthesia라는 AI동영상 생성툴을 발견해서 테스트해보던중에 AI아바타를 만들수 있지는 않을까 하는 호기심에 찾아봤는데 Creating an AI Avatar라는 아티클을 발견해서 함께 시도해보면 어떨까 싶어서 강좌를 시작합니다. 저는 AI나 머신러닝등에는 지식이 전무해서 따라하면서도 설명이 살짝 부족할 수도 있어요. 그래도 코드를 보면서 최대한 열심히 설명을 해보도록하겠습니다.

이번시간에는 Python을 이용하여, 여러개의 이미지 속에서 얼굴만 모아서 하나의 이미지에 나란히 붙이는 코딩을 해보도록 하겠습니다. 아래와 같이 코딩에 필요한 라이브러리를 설치해주세요.

pip install numpy dlib opencv-python pillow

collect_faces.py라는 파일을 하나 생성합니다. 여기에서 사용한 라이브러리는 opencv-python의 cv2, dlib, pillow의 Image, 그리고 numpy입니다. 필요한 라이브러리를 코드에 포함시켜주세요.

import cv2 
import dlib 
from PIL import Image 
import numpy as np 

그리고 얼굴이 들어있는 이미지 3개를 준비해서 같은 폴더에 저장해주시고 해당 이미지명들을 배열안에 넣어서 아래와 같이 변수에 저장해주세요.

image_paths = ['image1.png' , 'image2.png' , 'image3.png']

이미지에서 얼굴을 추출해서 반환하는 함수를 만들겠습니다. 일단 함수 안에서 사용할 얼굴인식 모듈은 코드가 무거우니까 함수 윗쪽에 한개만 선언해놓고 함수 안에서 계속 갖다 쓰는걸로 할게요. 인자로 이미지경로를 받고 cv2라이브러리로 해당이미지를 읽어서 dlib라이브러리가 제공하는 얼굴 검출 기능을 사용하여 이미지 안의 얼굴들을 떠냅니다. 이미지 안에 얼굴이 1개 이상 있는 경우를 생각해서 첫번째 얼굴을 반환하는걸로 할게요. 이때 검출한 얼굴 이미지는 칼라포멧이 BGR포멧입니다. cv2.cvtColor()함수에 변환필터 cv2.COLOR_BGR2RGB를 명시해서 BGR을 RGB포멧으로 변환해서 반환하도록 할게요.

detector = dlib.get_frontal_face_detector() 
def  extract_face (image_path): 
    image = cv2.imread(image_path) 
    faces = detector(image)
    if  len (faces) > 0 : 
        face = faces[ 0 ]   # 발견된 첫 번째 얼굴 추출
         x, y, w, h = face.left(), face.top(), face.width(), face.height() 
        face_image = image[y:y+h, x:x+w] 
        return cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB) 
    else : 
        return  None

이제 생성한 함수를 호출하여 이미지 안의 얼굴들을 모아볼게요. 함수에서 반환받은 얼굴들을 모아 저장할 배열 faces를 하나 정의합니다. 그리고 이미지들을 돌면서 방금만든 extract_face()함수에 이미지경로를 넘겨주어 호출합니다. face가 null이 아니면 faces 배열에 넘겨받은 얼굴을 추가합니다.

faces = [] 
for path in image_paths: 
    face = extract_face(path) 
    if face is  not  None : 
        faces.append(face) 

이번에는 배열에 모은 얼굴들을 하나로 합성해서 새로운 얼굴을 만들어 볼게요. numpy의 hstack()함수에 얼굴배열을 넘겨주어 하나로 통합하는 코드입니다.

if faces: 
    composite_face = np.hstack(faces) 
    composite_image = Image.fromarray(composite_face) 
    composite_image.save('composite_faces.jpg') 
    composite_image.show() 
else : 
    print ( "이미지에서 얼굴이 감지되지 않았습니다." )

아래는 완성된 코드입니다.

import cv2
import dlib
from PIL import Image
import numpy as np

# dlib 얼굴 검출기 로드
detector = dlib.get_frontal_face_detector()

# 이미지에서 얼굴을 추출하는 함수
def extract_face(image_path):
    image = cv2.imread(image_path)
    faces = detector(image)
    if  len(faces) > 0:
        face = faces[0]   # 발견된 첫 번째 얼굴 추출
        x, y, w, h = face.left(), face.top(), face.width(), face.height()
        face_image = image[y:y+h, x:x+w]
        return cv2.cvtColor(face_image, cv2.COLOR_BGR2RGB)
    else :
        return  None

# 이미지 경로
image_paths = ['image1.png', 'image2.png', 'image3.png']

# 얼굴을 추출하여 저장
faces = []
for path in image_paths:
    face = extract_face(path)
    if face is not None :
        faces.append(face)

# 얼굴을 수평으로 쌓아 합성 이미지 생성
if faces:
    composite_face = np.hstack(faces)
    composite_image = Image.fromarray(composite_face)
    composite_image.save('composite_faces.jpg')
    composite_image.show()
else :
    print ( "이미지에서 얼굴이 감지되지 않았습니다." )

그럼 코드를 실행해볼게요.

저는 이미지 3개를 다음과 같이 준비했습니다.

위의 이미지를 실행파일과 같은 폴더에 저장하고 스크립트를 실행해 볼게요.

python collect_faces.py

결과는 다음과 같이 얼굴만 같은 크기로 모아서 하나의 이미지에 나란히 붙여줍니다.

References

Google API – Authentication

Overview

안녕하세요. 이번시간에는 Google에서 제공하는 API를 이용하는 방법에 대해서 알아보도록 하겠습니다.

Create Project

Google의 API들을 사용하시려면 Google Cloud에 프로젝트를 생성하셔야합니다. Google Cloud의 APIs & Services대시보드를 열어주세요. 그리고 상단에 프로젝트 이름을 클릭하면 현재 프로젝트들이 보이는 팝업을 뜨는데요.

저는 기존에 있는 프로젝트가 2016년도에 만들어진 것들이라서 해당 프로젝트로 API에 접근하려고 보니까 API사용 quota가 다 소진되었다고 나와요. 하루 10,000번의 request가 가능한데 저 2번 요청했거든요. 근데 찾아보니까 옛날에 만들어진 프로젝트에서 나오는 오류라고 하더라구요. 그래서 저도 이번에 프로젝트를 새로 만들었습니다. 여기에서 상단에 NEW PROJECT버튼을 누르세요.

그러면 프로젝트 생성하는 양식이 나타납니다. 프로젝트 이름을 넣고 Create버튼을 눌러주세요.

그러면 해당 프로젝트의 대시보드가 눈앞에 펼쳐집니다.

Enable APIs

왼쪽 메뉴에서 Library를 누르세요. 그러면 사용가능한 모든 API를 보여줍니다.

앞으로 하나하나 다 사용해보겠지만 우선 YouTube Data API v3를 선택할게요. 밑으로 스크롤 내리다보면 YouTube섹션에 가장 처음 나오는 API인데 검색해도 나오니까 선택해주세요. 그러면 API에 대한 상세한 설명이 나오는데 여기에서 ENABLE버튼을 클릭해주세요. 다른 API도 필요하실때 라이브러리에서 Enable을 시켜주셔야 사용이 가능합니다.

Enable시키면 아래와 같이 API사용이 허용됩니다.

Authentication

Google의 API를 사용하려면 우선 인증키가 필요합니다. 인증을 하는 방법에는 간단하게 API Key를 사용하는 방법과 OAuth를 사용하는 방법 두가지가 있습니다. Google Cloud의 APIs & Services에 들어가면 아래 화면과 같이 Credential들을 확인하실 수 있습니다.

API Key

Create API Key

API Key를 생성하시려면 상단에 +Create Credentials을 클릭하면 API Key를 생성할지 OAuth Client ID를 생성할지를 물어봅니다. 여기서 API Key를 선택합니다.

그러면 다른거 안물어보고 바로 API Key를 하나 생성해서 보여줍니다.

위에 경고문에 이 API key를 갖고 뭐든지 할수 있으니 권한을 조정하시오라고 써있습니다. Edit API Key를 눌러서 허용범위를 좁혀볼게요. 접근할 수 있는 클라이언트는 제한을 두지 않았고, 접근가능한 API는 YouTube Data API v3만 허용하는 걸로 변경했습니다.

API Key를 이용한 API호출

그래도 구글인데 ?key=API_KEY를 쿼리스트링으로 암호화없이 그냥 보낸다고? 이건 진짜..큰일나는데…구글이 왜그랬지? 그밑에는 헤더에 넣어서 보내긴 하지만 헤더도 얼마든지 해킹이 가능한데..이건 진짜 너무 심했다…서비스가 너무 커서 예전 인증방식을 업그레이드 하는게 시간이 많이 걸렸나? 아직도 API_KEY방식을 지원하는 것도 좀 우습고..돈도 잘버는데 개발자들 팍팍 써서 싹 다 갈아엎어 버리지 이걸 그냥 이렇게 쓰냐….쯪….

생성한 API Key를 가지고 API를 호출해볼게요. 결과가 잘 나옵니다.

$ curl -X GET \
> "https://content-youtube.googleapis.com/youtube/v3/videoCategories?part=snippet&id=1" \
> -H "X-goog-api-key: API_KEY" \
> -H "Content-Type: application/json; charset=utf-8"
{
  "kind": "youtube#videoCategoryListResponse",
  "etag": "s6RguhiCzdsBQFsS4YzslvqtQtI",
  "items": [
    {
      "kind": "youtube#videoCategory",
      "etag": "grPOPYEUUZN3ltuDUGEWlrTR90U",
      "id": "1",
      "snippet": {
        "title": "Film & Animation",
        "assignable": true,
        "channelId": "UCBR8-60-B28hp2BmDPdntcQ"
      }
    }
  ]
}

API Key삭제

APIs & Services > Credentials에 들어가시면 API Key목록이 보입니다. 오른쪽 점세개 누르시면 Delete API Key메뉴가 뜰거에요. 그거 누르세요.

진짜 지울거냐고 물어보고

DELETE버튼 누르면 텍스트상자에 DELETE라고 입력하라고 나와요.

DELETE라고 쓰고 밑에 DELETE버튼 누르면 API Key가 삭제됩니다. 사용하지 않는 API Key들은 전부 삭제하시는게 보안상 안전합니다.

OAuth

API Key를 매번 사용하는 것은 보안상 매우 취약하기 때문에 가급적 OAuth Client ID를 이용해서 토큰을 받아와서 그걸로 API를 호출하는 방식으로 하는것을 추천드립니다.

Create OAuth client ID

API Key와 마찬가지로 좌측 Credentials메뉴를 클릭하고 들어간 화면의 상단에 + CREATE CREDENTIALS을 클릭하면 아래와 같이 어떤 서비스에 OAuth를 사용할지를 물어봅니다. 필요에 따서 앱타입을 선택하시고요.

앱의 이름도 적어줍니다.

필요한 정보를 넣고 CREATE버튼을 클릭하면, 다음과 같이 Client ID와 Client Secret을 만들어 줍니다.

Scope정하기

OAuth를 사용하려면 접근가능한 범위를 지정해야합니다. Google Auth Platform > Data Access를 열어보시면 아래와 같은 화면이 뜹니다.

중간에 ADD OR REMOVE SCOPES를 클릭합니다. 그러면 어떤걸 접근을 풀어주고, 어떤걸 접근을 막을지 선택하는 곳입니다. 저는 YouTube Data API v3에서 읽는것만 일단 허용하도록 하겠습니다. 선택하고 나서 반드시 밑에 숨어있는 UPDATE버튼을 반드시 눌러주셔야합니다. 안그러면 저장이 되지 않습니다. 그리고 팝업을 닫고 Save버튼을 눌러서 변경된 Data Access내용들을 저장해주세요. Scope을 어떤걸 선택했는지는 따로 노트해두세요. 나중에 인증할때 필요합니다. 저는 테스트니까 안전하게 YouTube Data API v3의 .../auth/youtube.readonly만 선택하도록 할게요.

Redirect URI정하기

Google Auth Platform에서 좌측메뉴 Clients를 클릭하시면 현재 소유하고 있는 Client들의 목록이 나오는데요

이중에 사용하고 싶은 클라이언트를 선택하세요. 그러면 아래와 같이 클라이언트에 대한 상세정보가 나타납니다.

여기에서 + ADD URI버튼을 클릭해주세요. 그리고 나타난 텍스트상자에 인증코드를 받아서 돌아갈 주소를 넣어주세요. 그리고 SAVE버튼을 눌러서 저장합니다. 그리고 방금 저장한 Redirect URI는 인증코드를 받을 때 필요하니까 어딘가에 노트를 해두세요.

Test User추가하기

인증테스트를 하려면 앱을 출시하기 전에 접근을 할수 있는 사용자를 등록해야 테스트를 할수 있습니다. APIs & Services의 OAuth consent screen을 클릭하시면 테스트 사용자를 등록할 수 있습니다.

+ADD USERS버튼을 누르면 슬라이드팝업이 뜨는데 여기에 접근을 허용하고자 하는 이메일을 입력한 뒤 SAVE버튼을 클릭하세요. 그러면 아래와 같이 테스트사용자가 들어갑니다.

Access Token 받아오기

타 서비스의 OAuth인증과 마찬가지로 API토큰을 받아오기 위해서 일단 Code를 받아와야합니다. 여기에서 방금 선택한 Scope과 Redirect URL,그리고 Client ID를 다른 파라메터들과 함께 넘겨줍니다. 지금은 테스트하는거니까 코드로 안하고 그냥 브라우저 주소창에 호출할게요.

https://accounts.google.com/o/oauth2/v2/auth?scope=SCOPE&include_granted_scopes=true&response_type=token&state=state_parameter_passthrough_value&redirect_uri=REDIRECT_URI&client_id=CLIENT_ID

문제가 생기면 에러를 보여줍니다. Error메세지를 읽어보면 Scope이 문제인지 Redirect URI가 문제인지 아니면 또 다른 문제인지 자세히 알려줍니다.

Scope, Redirect URI, Client ID모두 문제가 없으면 어떤 계정으로 인증을 진행할것인지를 물어봅니다. 해당 클라이언트 ID를 생성했던 계정을 선택하셔야해요.

테스트사용자로 등록된 사람이 접근을 시도하면 아래와 같은 에러가 날거에요.

해당 계정이 테스트 사용자라면 다음과 같은 안내문이 뜨고 Continue를 누릅니다.

지정한 도메인이 클라이언트가 되고 이제 테스트사용자의 계정을 통해서 해당 클라이언트가 API에 접근을 하려고 한다고 경고문을 보여줍니다. Continue를 누릅니다.

그러면 Redirect URI에 명시한 대로 해당 주소로 이동되며, 주소에 access_token을 함께 전달합니다.

https://REDIRECT_URI/#state=state_parameter_passthrough_value&access_token=ACCESS_TOKEN&token_type=Bearer&expires_in=3599&scope=https://www.googleapis.com/auth/youtube.readonly

API 호출 with API Explorer

이제 토큰을 가지고 API를 호출해볼까요? Google API Manual에 가면 Authorization하는 부분이 있는데 거기에서 API를 실행할 수 있는 양식이 준비되어 있습니다.

텍스트상자중에 access_token을 찾아서 방금 받아온 Access Token을 입력하신 뒤에 맨 밑에 Execute버튼을 누르세요. 이때 API Key체크상자는 체크를 해지한뒤에 눌러주세요.

그러면 진행할 계정을 선택하라고 나옵니다. 사용하고자 했던 클라이언트를 만든 계정을 선택하세요.

계속하면 개인정도등이 공유된다고 경고메세지를 보여줍니다. Continue를 눌러주세요.

그러면 본인의 계정에 접속하여 갖가지 데이타를 추가, 수정, 삭제할 수 있는 권한을 이 클라이언트 앱에 할당 할지를 물어봅니다. Allow버튼을 눌러주세요.

위에서 Allow버튼을 누르면 팝업이 닫히고 APIs Explorer의 Execute버튼 밑으로 API결과를 보여줍니다.

API Key를 없애려는 구글의 노력

프로젝트를 생성하면 바로 APIs & Services 의 Enabled APIs & services로 트래픽현황을 볼수 있는데요. 상단에 CREATE CREDENTIALS이 큼지막하게 보입니다. 아마도 좌측메뉴를 눌러서 API Key를 생성하기 전에 OAuth로 인증을 받도록 유도하려는 의도인것 같습니다.

Credentials > Clients

APIs & Services > Credentials은 이제 사라지고 앞으로는 Google Auth Platform > Clients에서 관리가 될것이라고 공지가 뜨네요.

OAuth consent screen > Audience

APIs & Services > OAuth consent screen도 이제 Google Auth Platform > Audience로 옮겨간다고 합니다.

Create Credentials는 OAuth만 지원

위의 화면에서 CREATE CREDENTIALS버튼을 클릭해보면 아시겠지만 인증종류를 물어보지도 않고 그냥 OAuth로 정해서 생성하도록 구성되어있습니다.

Credential Type

OAuth Consent Screen

Scopes

OAuth Client ID

Your Credentials

몇가지 화면만 봐도 API Key는 앞으로 사라지게 될것 같네요.

마무리

여기까지 Google의 API에 접근해서 데이타를 가져오는 방법에 대해서 간략하게 설명드렸습니다. OAuth로 생성한 Client ID를 제대로 활용하려면 지정한 도메인에서 돌아가는 Web앱이나, iOS, 또는 안드로이드 앱이 있어야 합니다. 사용하려는 곳의 출처를 웹앱이라면 도메인으로 소유권을 증명하고, 모바일 앱은 앱스토어 아이디등으로 누가 어디서 사용하는지를 명확히 명시해야합니다. 아마도 API를 악용하는 경우를 방지하기 위해서 2차 3차 검증을 하는 것으로 사료됩니다. API를 이용할 수 있는 웹앱이 만들어지면 그때 코드로 호출하는 방법에 대해서 알아보는 시간을 갖도록하겠습니다. 수고하셨습니다. 좋은 밤되세요!

References

YouTube Dashboard

How to Create a PDF from Images in Python

안녕하세요. 이번시간에는 이미지들을 PDF로 만드는 방법에 대해서 공부해볼게요.

우리가 사용할 패키지는 img2pdf라는 패키지에요. pip으로 패키지를 설치해줍니다.

pip install img2pdf

혹시 설치를 하시다가 pikepdf에러가 나실수 있으세요.

ERROR: Failed building wheel for pikepdf

그러면 컴퓨터에 qpdf를 설치하신 후에 파이썬 패키지 pikepdf를 설치해주셔야합니다.

brew install qpdf
pip install pikepdf

brew install이 너무 오래걸리면 Ctrl+C로 취소하시고 brew update를 해주신 뒤에 다시 설치하시면 됩니다. 그래도 오래걸리면 그냥 기다리는 수밖에 없어요 ㅠㅜ

설치가 다 끝났으면 코드를 만들어 볼게요. 일단 이미지 3개를 1.png, 2.png, 3.png로 저장해주세요. 그리고 아래와 같이 코드를 써서 app.py로 저장합니다. 파이썬파일명은 아무거나 해도 되는데 img2pdf.py로는 하시면 안되요. 그러면 패키지 img2pdf를 가져오지 않고 내가 만든 파일에서 img2pdf기능을 찾으려고 하거든요.

import img2pdf
from PIL import Image

image_paths = ["1.png", "2.png", "3.png"]
output_pdf_path = "output.pdf"

try:
    with open(output_pdf_path, "wb") as pdf_file:
        pdf_file.write(img2pdf.convert(image_paths))
except Exceptioin as e:
     raise PDFDocError(e)

실행해볼까요?

python app.py

제 PDF파일은 너무나도 완벽하게 잘 만들어졌습니다. 여러분들도 큰문제 없이 잘 변환이 되었기를 바래요. 그럼 다음시간에 만나요!