[UMC 3기] Vibecap

용어 정리

서비스를 추상화하여 코드로 작성하기 위해 정의한 용어(개념)들이다.

  • Vibe: 유저가 제공한 정보(사진 등)와 그 정보를 바탕으로 추천한 플레이리스트
  • capture: 사용자가 제공한 정보를 활용해 음악을 추천하는 과정(행위)
  • label: 이미지로부터 추출한 키워드
  • 추가 정보: 사용자가 선택한 감정, 그 외의 클라이언트가 수집한 정보로 구성된 문자열

추상화

사용자가 보낸 정보를 이용해 적절한 검색어를 생성해 유튜브에서 검색한 결과를 클라이언트로 전송한다.
이미지 분석은 Google Cloud Vision API, 플레이리스트 검색은 YouTube Data API를 사용했다.

VibeService 내부의 모습을 간단하게 표현한 그림 음악 추천 과정 (네모는 단순히 데이터를 운반하는 객체, 타원은 특정한 로직을 실행하는 객체이다.)

  • image: 사용자가 선택한 사진
  • extra_info: 클라이언트가 전송한 정보 (일부 데이터는 사용자 몰래 전송된다.)
    계절, 현재 시간대, 감정 정보로 구성된 문자열.
  • query: 유튜브 검색에 사용할 검색어
  • playlist: 검색 결과(유튜브 플레이리스트)
  • VideoQuery: 영상 검색에 사용할 검색어를 생성하는 객체
  • PlaylistSearchEngine: YouTube 서버와 통신하는 기능을 추상화한 인터페이스
  • ImageAnalyzer: Google Vision API 서비스를 추상화한 인터페이스
  • TextTranslator: Google Cloud Translation API에 대한 인터페이스

구현

GoogleCloudTranslationClient 클래스

TextTranslator 인터페이스에 대한 구현체이다.
label이 한국어가 아닌(주로 영어) 경우가 있는데 이에 대응한다.

label영어로 된 고유명사인 경우 번역을 해버리면 의미를 보존할 수 없었다.
임시방편으로 label이 두 단어 이상으로 이루어진 경우 한글로 번역하지 않았다.

@Override
public String translate(String foreignString) throws ExternalApiException {
    Translate translate = TranslateOptions.getDefaultInstance().getService();

    Detection detection = translate.detect(foreignString);
    String detectedLanguage = detection.getLanguage();

    if (detectedLanguage.equals("ko"))
        return foreignString;
    /**
     * heuristic: 두 단어 이상일 경우 고유명사일 확률이 높기 때문에 해석하면 의미가 깨지게 된다.
     * 영어를 그대로 사용
     */
    else if (foreignString.split(" ").length > 1) {
        return foreignString;
    }

    try {
        Translation translation = translate.translate(
                foreignString,
                Translate.TranslateOption.sourceLanguage(detectedLanguage),
                Translate.TranslateOption.targetLanguage("ko"),
                Translate.TranslateOption.model("base"));

        LOGGER.warn("[VIBE] 번역 결과: " + translation.getTranslatedText());
        return translation.getTranslatedText();
    } catch (TranslateException e) {
        LOGGER.warn(e.getMessage());
        throw new ExternalApiException();
    }
}

VideoQuery 클래스

검색어 생성에는 세 가지 경우가 존재한다.

case 1

label과 추가 정보로부터 검색어를 생성한다.

단순히 계절, 시간대, 감정, label을 공백으로 join한 뒤 PLAYLIST_KR를 붙여 검색어를 생성했다.
(PLAYLIST_KR는 “플레이리스트” 라는 값을 가지는 변수)

/**
 * 사진과 추가 정보 모두 사용해서 query 생성
 * @param extraInfo
 * season, time, feeling
 * @param label
 * 이미지에서 추출한 label
 * @return
 */
public String assemble(ExtraInfo extraInfo, String label) throws ExternalApiException {
    label = textTranslator.translate(label);
    String query;
    String season = extraInfo.getSeason();
    String time = extraInfo.getTime();
    String feeling = extraInfo.getFeeling();

    query = String.format("%s %s %s %s %s",
            season, time, feeling, label, PLAYLIST_KR);

    LOGGER.warn("[생성된 검색어]: " + query);

    return query;
}

case 2

label만으로 검색어 생성

labelPLAYLIST_KR을 공백으로 이어붙였다.

/**
 * 사진만으로 검색어 생성
 * @param label
 * @return
 */
public String assemble(String label) throws ExternalApiException {
    label = textTranslator.translate(label);
    LOGGER.warn("[VIBE] 이미지에서 추출한 label: " + label);
    String query;

    query = String.format("%s %s",
            label, PLAYLIST_KR);

    LOGGER.warn("[생성된 검색어]: " + query);

    return query;
}

case 3

추가 정보만으로 검색어를 생성한다.

계절, 시간대, 감정, PLAYLIST_KR을 공백으로 이어붙였다.

/**
 * 추가 정보만 사용해서 query 생성
 * @param extraInfo
 * season, time, feeling
 * @return
 */
public String assemble(ExtraInfo extraInfo) {
    String query;
    String season = extraInfo.getSeason();
    String time = extraInfo.getTime();
    String feeling = extraInfo.getFeeling();

    query = String.format("%s %s %s %s",
            season, time, feeling, PLAYLIST_KR);

    LOGGER.warn("[생성된 검색어]: " + query);

    return query;
}

PlaylistSearchEngine 클래스

  • YouTube 객체를 생성하고 용도에 맞게 parameter 값을 설정한다.
  • NUMBER_OF_VIDEO_RETURNS개만큼의 영상을 반환받아 그 중 하나를 선택해 접근할수 있는 url을 반환한다. 하나의 영상을 선택하는 방식은 단순히 최상단(YouTube 서버가 연관성이 가장 높다고 판단한 결과)의 영상을 선택했다.
    (임의의 영상을 한 개 선택하는 방법을 구현했지만 PM이 일관성 있는 로직을 원했다.)

간혹 검색 결과가 0개가 나오는 경우가 있었다.
추가 정보와 label의 관련성이 떨어져 합쳤을 때 의도를 알 수 없는 검색어가 만들어졌기 때문이라고 판단했다.
이럴 경우 검색어를 단순화하여 재검색하도록 구현했다.
예를 들어 최초의 검색어가 “여름 낮 우울한 coffee cup 플레이리스트” 였을 경우 “여름 낮 우울한 플레이리스트“로 바꿔 다시 검색한다. (label을 제거했다.)
그래도 0개가 검색된다면 NoProperVideoException 예외를 던지도록 구현했다. YouTube Data API v3(무료 버전)는 하루에 사용할 수 있는 할당량(quota)이 한정적이기 때문이다.

개선해야할 부분

  • [HW01] 추천 로직이 너무 선형적이다. 사용자가 몇 번 추천을 받아보면 그 결과를 예측할 수 있기 때문에 굳이 앱을 사용할 이유가 없다.
  • [HW02] 추천받는 영상의 종류가 한정적이다. 접미사가 “플레이리스트“로 고정되어있기 때문이다.
  • [HW03] 영어로 된 고유명사에 대한 검색 문제
  • [HW04] 검색어에 너무 많은 정보가 담겨있어 서로 의미가 상충할 수 있다.
  • [HW05] 이미지로부터 유의미한 정보를 추출하기 힘든 경우가 많다.
  • [HW06] 의미있는 검색어를 생성하는지 여부를 확인하기 위해 그 과정을 로그로 출력하고 있는데 레벨을 WARN으로 출력하고 있다. 뭔가 부자연스럽다. custom level을 만들도록 하자

Comments