여행당일기 - 메인

재연의 개발블로그

한문 조판용 웹사이트

이미지

링크

개발 시작: 2024.04.27(토) 개발 기간: 3-4시간

소개: 간단한 한문 자료를 영인본 모양으로 조판할 수 있는 사이트입니다.

가능한 것

  • 단순한 한문 자료의 조판이 가능합니다.
  • 책 제목을 지정할 수 있습니다.
  • 작은 글씨로 된 주석을 넣을 수 있습니다.
  • 문단 사이를 ○로 구분할 수 있습니다.

불가능한 것

  • 내려쓰기가 안 됩니다 – 들여쓰기가 아닙니다: 이래서 조금 퀄리티가 떨어집니다. 본문을 한 칸 올려쓰거나 내려쓰는 것 자체가 고서에서 본문의 층위를 파악할 수 있는 중요한 방법인데 이 부분이 미구현 상태입니다.
  • 그림이나 수식 등의, 두 줄을 넘어가는 요소를 넣을 수 없습니다 – 이건 전근대 과학 자료에 많은 편인데.. 생략하겠습니다.
  • 일본어 상용한자를 벗어나는 글자는 예쁘게 표시되지 않습니다 – 사용된 폰트가 일본어 폰트라 그렇습니다.

  • PDF로 내보내기...? ← 구현할 예정

문법

  • 단순히 개행을 한다고 새 문단으로 인식하지는 않으니 마음껏 개행해주세요.
  • 구두점 자동삭제 기능 없습니다. 쓰지 마세요.
  • 문단을 구분하고 싶으면 앞 문단을 마침표(.) 로 끝내주세요.
  • 주석에 해당하는 텍스트는 작은따옴표(') 로 감싸주세요.
+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

세계관 및 캐릭터를 정리하는 정적웹사이트 만들기

자작 캐릭터, 자작 세계관 구상.

평소 만화나 애니메이션을 좋아하는 사람들의 전유물이었는데요,

남녀노소 할 것없이 웹소설이나 웹툰을 대중적으로 접하게 되면서 자작 세계관을 구상해 본 분들이 꽤 되실 것 같아요.

저도 요즘 그쪽에 다시 빠지게 되어 관련 자료를 정리하는 서랍장 같은 툴이 필요하게 되었어요.

사실 평범하게 노션이나 위키를 설치하려고 했는데 노션은 너무 무겁고, 위키는 서버 자원을 추가로 먹게 되어 부담스러웠어요.

그래서 Misskey라는 SNS에 계정 하나를 생성하여, 해당 계정을 단일 세계관의 캐릭터 위키 겸 작품 업로드 게시판으로 쓸 수 있도록 하려고 해요.

CabinetKey

캐비닛키는 사실 자작 세계관 정리용으로 제작되고 있진 않아요.

기본적으로 *.json(별돋조선)의 하위 프로젝트인데,

제목에서 알 수 있듯이 실제 역사 기반으로 돌아가는 작품이다 보니 구체적인 세계관 (역사, 문화, 경제 등) 설정이 필요 없기 때문이에요. 정도전씨가 다 해줬어요

그래서 딱 장소와 캐릭터 에 중점을 두고 기능을 구현하기로 했어요.

타겟

JSON

받아올 json은 이렇게 생겼어요.

{
    "info": {
        "title": "",
        "subTitle": "",
        "summary": "",
        "description": "",
        "mainYear": "",
        "map": "",
        "hashtag": [""]
    },
    "character": {
        "category": [""],
        "list": [
            {
                "avatar": "",
                "name": "",
                "meaning": "",
                "courtesyName": "",
                "nickname": "",
                "lived": [0, 0],
                "category": "",
                "subCategory": "",
                "eventChronology": {
                    "0.0": "",
                    "0.0": "",
                    "0.0": ""
                },
                "positionChronology": {
                    "0.0": "",
                    "0.0": ""
                },
                "relatedTo": {
                    "분류1": [0],
                    "분류2": [0]
                },
                "goal": ["", ""],
                "themeSong": [0],
                "summary": "",
                "description": "",
                "secret": "",
                "hashtag": ""
            }
        ]
    },
    "world": {
        "x,y": {
            "name": "",
            "image": "",
            "summary": "",
            "description": "",
            "eventChronology": {
                "0.0": ""
            },
            "relatedTo": {
                "분류1": [0]
            }
        }
    },
    "themeSong": [
        {
            "embed": "",
            "title": "",
            "artist": "",
            "character": 0,
            "summary": "",
            "description": "",
            "lyrics": ""
        }
    ]
}

기능 구현

구현된 기능

  • 메인 페이지 상에 간단하게 10x10으로 구획된 지도와 캐릭터 목록을 출력해 줘요.
  • 지도 상에 배경이미지를 지정하여, 지도의 각 구획마다 어떤 특징적인 장소가 존재하는지 표시할 수 있어요.
    • 실제 지도와 흡사한 그림을 사용해도 되지만, 수도권 전철노선도처럼 도형화 된 이미지가 좋아요.
  • 지도 상의 각 위치마다, 즉 최대 100개 장소의 정보를 기록할 수 있어요.
  • 캐릭터들의 분류와 하위분류를 설정할 수 있어요.
  • 어떤 캐릭터와 다른 캐릭터들 사이를 관계로 연결시킬 수 있어요. 관계는 분류처럼 작용해요.
  • 지도상의 어떤 장소와 캐릭터들 사이를 관계로 연결시킬 수 있어요. 관계는 분류처럼 작용해요.
  • 캐릭터의 연표가 존재하는데, 캐릭터의 작중 포지션과 사건이 구분되어 있어요.
  • Main Year (작중에서 가장 중점적으로 다루는, 많은 사건이 일어나는 연도) 를 지정할 수 있어요.
    • 캐릭터의 연표상에서 Main Year는 연 단위가 아닌 월 단위로 나타나고, 접어도 접히지 않아요.
  • 특정 연도를 지정하면(초기값은 Main Year로 설정되어 있어요), 그 연도에 살아 있는 캐릭터들만 볼 수 있어요.

구현할 기능

  • 모바일 화면과 같은 작은 세로화면에 대응할 계획이에요.
  • 세계관소개, 작품모음 페이지를 추가할 계획이에요.
  • 캐릭터처럼, 지도상에서의 공간 변화나 장소 이전을 반영할 수 있도록 해 볼 예정이에요.
  • 작품목록과 별개로, 캐릭터의 테마곡을 몇 곡 지정할 수 있어요.
    • 임베딩 기능을 사용하므로 기성 곡이든 자작곡이든 다 가능해요.
    • 곡 정보 페이지도 개설할 수 있어요.

미스키 연동 기능

  • 처음 로그인하면 미스키에 cabinetkey.json 페이지가 개설되어요. 여기에서 대부분의 내용을 작성하고 수정해요.
    • 캐릭터 정보, 월드 정보는 미스키 내부가 아니라 캐비닛키 사이트에서 추가, 수정, 삭제할 수 있어요.
  • 해시태그 기능으로 미스키 노트에서 각 캐릭터와 관계된 작품을 불러올 수 있어요.
  • 해시태그 기능으로 미스키 노트 중 캐릭터와 관계된 사소한 이야기를 불러올 수 있어요.
    • 이런 작품과 이야기는 캐비닛키 사이트에서 추가 및 삭제할 수 있어요.
    • 미스키에서는 수정 기능이 없는데요, 대신 삭제 및 편집 기능으로 수정된 작품을 재게시 할 수 있도록 할 예정이에요.

#CabinetKey

+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

어디까지나 가설에 불과하며, 검증과정을 거칠 예정은 없습니다.

수식

저의 모델에선 로그함수를 이용해서 이미지 데이터를 노멀라이징 했기 때문에,

모델이 생성해 준 이미지를 stft 이미지로 변경하는 데에는 다음과 같은 지수함수를 이용해야 합니다.

$$ y = ae^{bx} $$

그리고 이 지수함수는 원칙 상 처음 stft 이미지를 노멀라이징 한 함수의 역함수를 사용하면 됩니다.

그러나 실제로 완성된 모델에 역함수를 바로 적용하면 당황스러운 상황을 마주하게 될 것입니다.

대략 두 경우 중 하나이며, 때로는 둘 다 해당될 수도 있습니다.

  • 소리에 갈라짐이 있는 경우.
  • 소리가 먹먹한 경우.

갈라짐이 있는 경우는 가장 판단하기 쉬우며 자주 발생합니다.

먹먹한 경우는 변환된 소리만 단독으로 들었을 때는 판단하기 어렵습니다만,

레퍼런스가 될 만한 원본 목소리를 같이 틀어보면 먹먹하다는 것을 바로 알 수 있습니다.

완성된 모델의 문제점 중 이 두 경우는 의외로 모델을 직접 건드리지 않고도 고칠 수 있는 경우에 해당합니다.

다만 모델에 따라 무려 보컬 에디터에서 수식을 직접 계산해 보며 조정해야 하는 점이 문제라면 문제여서,

이 점은 모델을 만들 때 수식을 계산해서 적용할 수 있도록 보완해 볼 계획입니다.

자, 그러면 두 경우가 어째서 발생하고, 어째서 제가 고칠 수 있다 고 하는지 한번 알아봅시다.

갈라짐이 심한 경우

사실 딥러닝을 통해 파동이 아닌 주파수의 이미지를 생성했기 때문에 발생하는 문제이지, 자연적인 소리라면 존재해서는 안 되는 경우랍니다.

소리가 갈라지는 이유는 여러 종류의 서로 다른 소리가 중첩되어 들리는 것으로 볼 수 있습니다.

제 모델의 경우 소리는 대체로 두 개로 분리되어 들리게 되는데, 그것은 stft 이미지의 맨 아래 첫째줄과 둘째줄에서 나오는 소리입니다.

img

아래의 첫째줄과 둘째줄까지 밝기가 강렬해 보이죠?

이 두 소리가 거의 경쟁적인데도 완벽한 배음을 이루지 못하게 되면서 불협에 가까운 소리가 나는 것으로 파악됩니다.

이 문제를 해결하기 위해서

먹먹한 경우

먹먹한 소리가 나는 이유는 고음역대 신호가 약해서 입니다. 이 경우 단순히 저음역 대비 고음역대 신호를 올려 주기 위해 기울기를 감소시키면 되는데,

먹먹한 경우

다음 4가지 식을 만족하는 함수

$$ g(x) = a'e^{b'x}+c' $$

가 필요합니다.

  • \(f(-1) = g(-1)\) : 이것을 만족하지 않으면 전체 구간에 잡음이 낍니다.
  • \(f'(1) = g'(1)\): 이것을 만족하지 않으면 소리가 선명해짐과 동시에 갈라지거나, 오히려 더 먹먹해집니다.
  • \(f'(0) < g'(0)\) 이며 \(f''(0) > g''(0)\) : 이것을 만족하지 않으면 먹먹함이 해결되지 않습니다.

으로부터 다음과 같은 식이 얻어집니다.

$$ ae^{-b} = a'e^{-b'} + c' $$ $$ abe^{b} = a'b'e^{b'} $$ $$ b > b', a < a' $$

이 식을 풀면, 다음과 같은 방법으로 \(a', b', c'\) 을 구할 수 있습니다.

  • \( b' < b \) 를 만족하는 임의의 \( b'\) ( \( b'\) 값 결정)
  • \( a' = \frac{abe^{b}}{b'e^{b'}} \) 을 만족하는 하나의 \( a'\)
  • \( c' = ae^{-b} – a'e^{-b'} \) 을 만족하는 하나의 \( c'\)

예를 들어, 지금 먹먹한 현상을 보이는 변환식이

$$ 0.0837e^{7x} $$

인 경우 \( a = 0.0837 \) , \( b = 7 \) 이 되겠습니다.

우선 \( b' \) 의 경우 7보다 작은 임의의 숫자인 6으로 잡겠습니다.

그리고 두번째 식으로부터,

$$ a'= \frac{0.0837\times{7}e^{7}}{6e^{6}} = 0.265 $$

로 결정합니다.

마지막으로,

$$ c' = 0.0837e^{-7} – 0.265e^{-6.5} = – 0.000322 $$

을 구합니다.

이 세 값을 앞의 수식에 대입해서 변환식을

$$ 0.265e^{6x} – 0.000322 $$

로 최종 수정합니다.

+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

데이터셋의 preprocessing 이야기, 그리고 최고의 메모리 관리는 메모리를 늘리는 것이라는 이야기

우선, 어느 정도 완성된 모델의 목소리를 들어 보도록 합시다.

epoch 45 정도 돌린 것이니 사실상 거의 학습이 끝났다고 보아도 됩니다.

물론 epoch 53 돌고 있는 지금도 로스는 꾸준히 내려가고 있긴 합니다만...

데이터셋 라벨링 – 임의 음소 지정

미캉톤의 데이터셋은 음절 단위가 아니라 음소 단위이기에,

음절 단위의 라벨 데이터를 음소 단위로 직접 나누어 주어야 한다... 고 생각했습니다.

하지만 아니었습니다.

각 음소의 길이까지 학습할 거라면 그게 맞지만, 미캉톤의 경우 모델 실행 코드(에디터) 에서 음소의 길이를 임의로 지정하고 있습니다.

    if len(syl) > 0:
      for j in range(len(syl)):
        if syl[j] in 'aeiouNT':
          timeV += 1
        elif syl[j] in 'wy':
          timeC += 1
      if timeV > 0:
        lenV = (time - timeC*1)/timeV
      else :
        lenV = time
      for j in range(len(syl)):
        if j > 0:
          if syl[j-1] in 'aeiouNT':
            timeArr.append(timeArr[-1]+lenV)
          elif syl[j-1] in 'wy':
            timeArr.append(timeArr[-1] + 1)
          elif syl[j-1] in 'rktp' or syl[j-1] == 'ch' or syl[j-1] == 'ts':
            timeArr[-1] = timeArr[-1] - 1
            timeArr.append(timeArr[-1] + 1)
          else:
            timeArr[-1] = timeArr[-1] - 2
            timeArr.append(timeArr[-1] + 2)
        else:
          timeArr.append(0)

이것은

  • 우선, 각 노트에 포함된 모음의 개수 + 받침의 개수(일본어는 받침이 1음절이기 때문입니다.) 를 1음절이라고 보고 해당 노트를 n등분 해 주고
    • 한국어 라이브러리를 만들 경우 이 부분은 수정해야 합니다.
  • w나 y같은 반모음은 노트 안에 포함된 자음이라고 보고 1단위만큼 할당해 주고
  • 나머지 자음은 노트 밖에 포함된 자음이라고 보고
  • 짧은 자음은 1단위만큼 노트 이전에 할당해 주고
  • 긴 자음(음가가 있는 자음)은 2단위만큼 노트 이전에 할당해 주는

코드입니다.

개인의 발성 습관을 고려하지 않고 일괄적으로 발성 시간을 통일해 주었기 때문에,

기존의 정확한 라벨링 데이터를 사용하면 오히려 부자연스러워지게 됩니다.

그런데 음절 단위로 라벨링을 한 다음, 방금 제시한 코드대로 자음과 모음을 임의로 지정해 준다면

컴퓨터는 그 임의로 지정한 음소들 사이에서 규칙을 찾아서 자연스러운 결과를 도출하게 됩니다.

입력 데이터의 길이에 상관 없이 입력 받기

이런 식이라면, 굳이 힘들게 라벨링 작업을 한 번 더 할 필요 없이,

그냥 제공받은 원본 데이터와 라벨 데이터를 그대로 입력해 주면 되는 것입니다.

대신 기존에는 모든 파일을 15초씩 수동으로 끊어서 입력했는데,

이 부분을 수정해서 일단 파일을 그대로 입력받고, 알아서 필요한 길이만큼 잘라 주는 코드를 작성해서 적용했습니다.

그러니 데이터셋의 준비가 거의 몇 분 만에 끝났습니다.

그러나.. 메모리의 역습

자, 그렇게 준비된 저의 데이터셋은 약 130분 분량으로, 5초씩 끊어 별도의 이미지로 저장하고 있기 때문에 총 1562개의 세트가 들어 있습니다.

1562장의, 512x512픽셀의 RGB 이미지, 인풋 아웃풋 2장.

그리고 이걸 하나의 텐서로서 관리합니다.

단순히 계산해 보면 13기가바이트 정도의 메모리를 잡습니다.

변수를 아무리 재활용해가며 메모리 관리를 해도 이미 텐서 하나가 13기가바이트이기에,

일반적인 컴퓨터에서 돌리긴 어렵습니다.

img

구글에서 기본적으로 제공하는 T4 GPU + 12GB CPU 메모리 + 15GB GPU 메모리 역시 사용할 수 없습니다.

img

대신 런타임 유형 변경에서 TPU v2를 선택하면, 제가 Colab Pro를 구독하고 있어서 그런지 300기가바이트 정도의 메모리를 할당해 주는데,

여기에서 작업을 하면 메모리 문제를 생각하지 않고 여유로운 작업이 가능합니다.

(대신 학습속도가 4배나 느리긴 합니다... -_–)

img

이렇게 15시간 정도 학습시킨 결과물입니다. (T4 기준이면 4시간 좀 안될 겁니다.)

위 영상은 12시간짜리 결과물이구요.... (그리고 T4 기준 3시간 ㅠ)

+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

잡음을 어느정도 처리했으며, segment 사이의 격차를 줄이는 스무딩 작업을 진행했습니다.

고주파 대역 잡음 처리

문제 제기

현재까지의 MIKANTONE 모델은 input data에 없었던 고주파 대역 잡음이 계속 섞여들어왔습니다.

예를 들면 이런 식이었습니다. y=350 즈음에 밝은 점이 군데군데 찍혀 있는 게 보이죠.

img

이걸 직접 소리로 들어보면, 다음 영상과 같이 계속 높고 날카로운 소리가 섞여 듣기 불편합니다.

보다 쉬운 분석을 위해 DAW 상에서 이퀄라이저를 켜 보면, 이 날카로운 소리는 6000-10000Hz에 걸쳐 있습니다.

이 대역은 인간의 목소리 중 칼칼함을 더해 주며 ㅈ, ㅊ, ㅆ등의 잇소리를 담당하는 부분인데,

(그래서 너무 거슬린다 싶으면 디에서로 눌러 주기도 합니다.)

중요한 부분이라 노이즈 제거한답시고 아예 High-cut으로 잘라내 버리면 소리가 엄청나게 먹먹해지게 됩니다.

해결

이걸 어떻게 따로 처리를 해야 하나,

스무딩을 해야 하나(그러면 당연히 소리가 뭉그러집니다.)

디노이즈만을 담당하는 AI 모델을 따로 만들어야 하나 생각하고 있었는데,

생각보다 간단한 방법으로 처리가 가능했습니다.

우선 “고주파 대역” 에 대한 것입니다.

우리의 목소리는 생각보다 높지 않기에, 대부분 메인 파형은 중음역대에 걸쳐 있습니다.

위 이미지도 보시면 밝은 점이 찍혀있는 부근에서는 기본적으로 그렇게 센 신호가 나오지 않습니다.

반대로 노이즈 자체의 신호는 굉장히 셌습니다.

어느정도냐 하면 아무리 컴프레서를 걸어도 걸어도 노이즈만 들리면 클리핑이 생겼을 정도였죠.

하지만 그렇다고 저음역-중음역대의 신호보다 눈에 띄게 강한 것은 아니었습니다.

클리핑이 생겼던 건 고음역대라 특히 더 크게 들렸나 봅니다.

아무튼 그러니, 고주파 대역에서”만” 특정 임계값보다 강한 신호가 있으면 제거해 주면 되는 일이었습니다.

그러면 제가 할 일은 단지 코드 한 줄을 추가하는 것입니다.

prediction[200:][prediction[200:] > 7] = 0.000001

여기에서 prediction은 모델을 통해 도출된 output 이미지입니다.

prediction의 첫번째 차원 index가 200과 같거나 큰 모든 원소에 대해서,

(즉 고주파 영역대의 신호를 나타내는 모든 원소에 대해서,)

그 원소의 수치가 제가 설정한 임계값보다 크면 (일단은 10으로 설정했다가, 7로 낮추었습니다.)

원래 값을 무시하고 0에 가까운 값으로 대치합니다.

0에 가까운 값인 이유는, 앞으로 저의 모델에 대해서 이야기할 기회가 있으면 설명하겠지만,

결과값 그래프 자체가 로그 스케일이기 때문입니다.

img

이와 같이 튀는 값이 많이 사라졌습니다.

(두번째 그래프가 STFT 그래프, 세번째 그래프가 파형 그래프인데 둘 다 튀는 값이 사라졌어요.)

스무딩 작업

MIKANTONE은 규격이 정해진 img to img 변환을 사용하기 때문에, 가변적인 길이에 취약합니다. 현재까지 5초짜리 음원을 생성할 수 있습니다.

그리고 일반적인 음원은 3분 – 4분 정도의 길이이므로,

지금까지 미캉톤은 5초짜리 STFT 그래프를 전부 갖다 붙여서 GriffinLim 변환을 하는 단순한 방법을 사용했답니다.

하지만 이렇게 하면 당연히 중간에 음높이, 음량 등의 급격한 변화가 생기는 구간이 있게 됩니다.

이번 기회에 각 STFT 그래프들끼리 '겹치는 구간'을 만들어 두 그래프의 값을 평균 내는 식으로 스무딩을 진행해 주었습니다.

잡음 처리, 스무딩 작업이 완료된 후의 음원입니다. 고음역대의 잡음 및 끊김이 거의 없는 것을 보실 수 있습니다.

후기

확실히 지난 최적화도 그렇고, 2023년 중순의 저보다 2024년 초의 저는,

비록 언어가 다르긴 했지만, 그 사이에 이것저것 다양하게 코드를 짜 보아서 그런지,

좀더 많은 문제에 좀더 유연하고 지혜롭게 대처할 수 있었습니다.

발전이 보여서 매우 기쁩니다.

+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

데이터 변환 시간은 짧게, 음정과 박자는 정확하게 개선하였습니다.

그간 미캉톤 코드 자체를 제대로 건드리지 못하기도 했지만, 에디터 작업은 특히나 아예 손을 놓고 있었습니다.

그 이유 중 몇 가지는 이러했습니다.

  • 왜인지 오류를 뿜는 미디 파일의 시간 데이터
    • 주기적으로 탈락된 노트들이 발견되었습니다.
  • 너무 오래 걸리는 데이터 변환
    • 이것은 모델을 만들 때에도 마찬가지로, 분 단위가 소요되었습니다.

어째서인지 이것은 신경쓰지 않으면 신경쓰지 않을수록 더 심해지게 되어,

솔직히 말씀드리면 오늘 이전의 미캉톤 에디터는 거의 쓸 수 없는 물건에 가까웠습니다.

그리고 그것을 오늘 날을 잡고 제대로 개선해 보았습니다. 총 두 파트에 걸쳐서 개선했습니다.

MusicXML

기존의 MIDI 파일 대신 MusicXML 파일을 입력받았을 때 장점이 몇 가지 있었습니다.

  • 가사와 멜로디를 한 번에 입력받을 수 있게 되었습니다.
  • 가사와 멜로디 파일을 Synthesizer V나 Utau 같은 곳에서 만든 다음 utaformatix 에서 변환하면 모든 튜닝이 끝나게 되었습니다.
  • Colab에서 기본으로 지원하지 않는 python 모듈 'mido' 를 따로 설치할 필요가 없어졌습니다.
  • MusicXML은 미디 파일과 달리 “on” “off” 가 있는 게 아니라 “여기부터 다음 노트까지 이 음정” “여기부터 다음 노트까지 쉼표” 라는 형식이어서 저의 input array 형식으로 변환하기 더 간단했습니다.

input list → image 변환 코드 최적화

input list는 시간에 따른 데이터가 변화하는 지점을 표시하는 배열이었고,

image 데이터는 시간에 따른 모든 데이터를 찍어 주어야 하는 배열이기에,

이걸 변환하는 과정은 아주 간단한 과정은 아니긴 합니다.

하지만 그렇다 하더라도 과거의 제 코드는 최적화가 너무 안 되었더라구요.

inputData = []
for o in range(3):
    inputData.append([])
    for k in range(int(inputArray[-1][0]/150+1)*150):
      for l in range(len(inputArray)):
        if l == len(inputArray) - 1:
          if k >= inputArray[l][0] and k < int(inputArray[-1][0]/150+1)*150:
            inputData[o].append([np.multiply(freqOneHot[inputArray[l][1]+2],(num[inputArray[l][2]][o]))])
        else:
          if k >= inputArray[l][0] and k < inputArray[l+1][0]:
            inputData[o].append([np.multiply(freqOneHot[inputArray[l][1]+2],(num[inputArray[l][2]][o]))])

아래는 현재 코드입니다.

inputData = []
for o in range(3):
    inputData.append([])
    l = 0
    for k in range(int(lyrics[-1][0]/150+1)*150):
      if l < len(lyrics)-1:
        if k >= lyrics[l+1][0]:
          l += 1
      inputData[o].append([np.multiply(freqOneHot[lyrics[l][1]+2],(num[lyrics[l][2]][o]))])

원본 배열이 시간에 따라 배치되어 있다는 가정 하에, 둘 다 동일한 값을 리턴합니다.

완성본

img

미캉톤 도호쿠 키리탄 개발 1일차 결과물입니다.

입력 데이터가 5분 45초 분량으로 짧아서 발음 등이 선명하지는 못합니다.

하지만 원곡을 아시는 분들이라면 적당히 들리실 거라고 생각합니다.

입력 데이터의 적정 분량은 기존의 AI 음성합성엔진 기준으로 한시간 정도라고 하는데,

이것은 라이브러리 제공자의 음높이, 지속시간, 가사에 따른 각 파형 및 노래 습관을 파악하기 위해 필요한 최소 시간으로 생각되며,

미캉톤 또한 유의미하게 줄이지는 못할 것으로 보입니다.

+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

AI 음성합성엔진 '미캉톤'

개요

mikantone

가창 데이터를 직접 라이브러리로 활용, 자신만의 라이브러리를 만들 수 있는 '미캉톤(MIKANTONE)'은 2023년 중순부터 개발을 시작했다가 한두 달쯤 뒤 본업에 집중하기 위해 개발 중단한 딥 러닝 기반 인공지능 음성합성엔진입니다.

미캉톤은 본래 '서당고양이' 라는 이름이었다가, 'PyChang' 이라는 이름으로 바뀌었고, 인간의 음성 합성뿐 아니라 해금 등의 동아시아 악기 소리 합성에도 활용하게 되면서 올해 초에 'MIKANTONE' 으로 개명하였습니다.

이 엔진을 다시 개발하게 된 이유는 앞으로 음향 AI를 집중적으로 공부해 보기로 결정했기 때문입니다. 제대로 된 완성품을 만들기 위해서는 취미 레벨에서의 딥 러닝이 아니라, 조금 더 전문적인 레벨에서의 공부가 필요했고, 그걸 위한 첫걸음으로서 미캉톤을 활용해 보기로 한 것입니다.

미캉톤은 에스페란토어 '나는 노래한다' 에 해당하는 'Mi kantas'와 영어로 '음'에 해당하는 'Tone' 의 합성어입니다. 아이콘으로는 일본어로 'Mikan' 이라고 발음되는 을 사용하고 있습니다.

비슷한 소프트웨어로 일본의 NNSVS(Enunu)가 있습니다만, UTAU라는 소프트웨어에 플러그인처럼 물려 사용해야 해서 MacOS에서의 직접적인 사용이 불편하다거나, 일본어 이외의 문자나 음소에 대응하려면 추가적인 개발이 필요하다는 등의 단점이 있었습니다. 또 음소 기반이 아니라 음절 기반이라, 일본어보다 연속적인 자음이 등장하는 언어(영어, 러시아어, 상고한어, 한국어도 포함)에는 어색할 수 있다는 것도 단점이 될 수 있습니다.

비교군

현재까지는 저의 목소리로 개발을 진행했으며, 18분 가량의 녹음 데이터에 기반한 모델이 있습니다. 그러나 제대로 된 녹음환경에서 생성되지 않은 저의 녹음본은 불완전할 수밖에 없고, 따라서 더욱 더 확실한 성능 비교를 위해 비교군을 지정했습니다.

데이터베이스는 NNSVS Namine Ritsu, 뉴트리노 도호쿠 키리탄 에 사용된 것과 같은 것을 사용합니다.

현재는 높은 음질을 위해 .wav 파일을 사용하고 있으나, Colab의 용량 문제나 속도 문제 등의 이슈로 손실 압축된 파일로 변경될 수 있습니다.

방법

Colab

현재 시점에서 이용 가능한 GPU가 없기에, 또 딥 러닝과 접점이 없는 유저들이 간단하게 모델을 만들 수 있도록 하기 위해 Google Colab 환경을 사용합니다. Colab은 Jupyter Notebook 기반이므로 조금 더 쉽게 코드를 짜고, 공유하고, 자유롭게 변경하여 사용할 수 있습니다.

STFT

stft

STFT(short-time fourier transform)는 신호가 시간에 따라 변화할 때 해당 부분의 사인파 주파수 및 위상을 분석하는 데 사용되는 푸리에 변환입니다. 실제로 STFT를 계산하는 절차는 더 긴 시간 신호를 동일한 길이의 더 짧은 부분으로 나눈 다음 각 더 짧은 부분에서 푸리에 변환을 개별적으로 계산하는 것입니다. [영문 위키백과]

음성의 배열 데이터를 그대로 사용하여 LSTM과 같은 시계열 데이터 모델을 적용할 수도 있지만, 여러 시도 끝에 그것보다는 음성 데이터의 푸리에 변환 그래프를 입력 이미지 값으로 사용하는 것이 가장 이상적인 결과를 내는 것을 확인했습니다.

STFT로 변환된 음성 데이터는 시간에 따른 푸리에 변환을 나타내는 이미지 데이터로 존재합니다. 즉, 높은 주파수의 소리는 이미지의 윗부분에, 낮은 주파수의 소리는 이미지의 아랫부분에, 시간적으로 먼저 나는 소리는 이미지의 왼쪽 부분에, 나중에 나는 소리는 이미지의 오른쪽 부분에 자리하게 됩니다.

만약 사인파에 STFT를 적용한다면, 단일 주파수를 가지고 있기에 위 그림과 같이 음의 높이를 직관적으로 알 수 있게 됩니다. 하지만 인간의 음성과 같이 우리가 자연에서 들을 수 있는 소리는 보다 복잡한 주파수의 소리들로 구성이 되어 있습니다. 그렇기 때문에, 우리가 사용할 데이터에 STFT를 적용하면 다음 그림과 같은 여러 겹의 주파수가 나타나게 됩니다.

img

이런 상황에서 음의 높이는 단순히 그래프상에서의 위치를 확인하는 것이 아니라, 각 주파수 층 사이의 높이, 그러니까 그래프의 빽빽한 정도로 판별할 수 있습니다.

Pix2Pix

사용한 모델은 pix2pix 입니다. 이 모델은 스케치로부터 사진과 같은 이미지를 렌더링하거나, 낮에 찍은 사진으로부터 밤에 찍은 사진을 예측하는 등, 입력 이미지에서 출력 이미지를 매핑하는 작업에 사용되었던 모델입니다.

이 모델의 단점이자 장점이라고 하면, 변화시키고자 하는 특성 외의 다른 모든 성질은 동일한 “pair Image”가 필요하다는 점이라고 합니다. 그러니까, 요즘 사용되는 디퓨전 모델을 기반으로 한 img-img 변환과 달리 input 이미지의 지정된 위치에만 output 이미지를 생성할 수 있다는 점입니다.

이 특성을 제가 “장점” 이라고 서술한 이유는, 저의 데이터 특성상 input 이미지에서 지정한 자리와 정확히 같은 자리에 데이터가 생성되어야 했기 때문입니다. 그렇지 않고 조금이라도 어긋난다면, 제대로 된 음높이가 만들어지지 않겠죠. 불협화음처럼 들릴지도 모르겠습니다.

Dataset Generator

아직은 미캉톤으로 개명되지 않은 Pychang 홈페이지 에서 데이터셋을 라벨링해 볼 수 있습니다. 필요한 것은 15초짜리 MIDI 파일과 녹음된 wav 파일입니다. (정확히 15초여야 합니다.)

img

여기에서 음소 단위 로 라벨링을 해 주시면 됩니다. 간단하게 사용법을 설명하자면

  • 체크 버튼: Select mode(선택 모드) 입니다.
    • 키보드 단축키 1번입니다.
    • 악보의 아무 곳이나 클릭하면 그 곳으로 재생바가 이동합니다.
    • 악보의 노트를 클릭하면 음소를 라벨링할 수 있습니다.
    • 악보의 노트를 드래그하면 노트가 이동됩니다.
  • 연필 버튼: Edit mode(편집 모드) 입니다.
    • 키보드 단축키 2번입니다.
    • 악보의 아무 곳이나 클릭하면 그 곳에 노트가 생깁니다.
    • 악보의 노트를 클릭하면 그 노트는 삭제됩니다.
  • 재생 및 일시정지: 스페이스바 로 재생 및 일시정지 가능합니다.

그럼 저는 우선... 데이터 라벨링부터 하고 오겠습니다. 적어도 40분 분량의 데이터셋이 완성된 다음부터 본격적으로 모델을 만들어 볼 예정입니다.

+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

WriteFreely 블로그에 썸네일과 요약문을 만들어볼까요?

개요

기본적으로, 제 블로그와 같은 WriteFreely 블로그는 메인화면에 포스트의 모든 내용을 다 보여줍니다.

저는 개인적으로 목록형 / 웹진형 블로그를 선호하는 편이어서 이게 불편했습니다.

서로이웃 때문에 플랫폼을 옮기지는 못했지만, 네이버 블로그 시절부터 목록형 블로그나 웹진형 블로그를 동경했으니까요.

지금까지 운영하던 WriteFreely 블로그의 경우 아예 내용부분을 접어놓는 방식으로 이를 해결했지만(그냥 display: none 해주면 됩니다!),

요번 블로그의 경우 아무래도 개발 블로그다보니, 내용의 일부인 썸네일하고 요약문까지 대략적으로 보여줄 수 있으면 좋을 것 같다는 생각이 들었어요.

기본준비

각 포스트에서 썸네일로 지정할 이미지들을 준비해 주세요.

이미지를 맨 위에 첨부합니다. 마크다운 문법 대신 아래의 HTML 코드를 사용합니다.

# 제목

<img src="{url}" class="thumbnail">

물론, 썸네일이 없는 포스트도 존재할 것입니다. 그런 경우 그냥 남겨 두시면 됩니다.

또한, 각 포스트의 요약문을 인용구로 작성해 주세요. 요약문은 썸네일용 이미지 바로 아래쪽에 첨부하는 게 제일 이상적인 듯 합니다.

# 제목

<img src="{url}" class="thumbnail">

> 요약문

CSS 적용

{인스턴스 주소}/me/c/{블로그ID} 에서 블로그의 커스텀 CSS 를 변경합니다.

일단 지금 썸네일 이미지하고 요약문이 적용된 블로그 요소들을 보시면,

image

이런 식입니다.

즉 이미지는 .book 클래스 아래의 p 아래에 img.thumbnail,

요약문은 .book 아래에 blockquote 아래에 p 로 지정하면 됩니다.

.book 은 포스트의 본문 내용 전체가 들어가는 부모요소 입니다.

그러므로 우선 .book의 자식요소 중 p와 첫번째 blockquote를 제외한 모든 요소를 보이지 않게 합니다.

.book > *:not(p, blockquote:nth-of-type(1)) {
    display: none;
}

그리고 p 중에서도 자식요소로 .thumbnail을 가지고 있지 않은 요소는 보이지 않게 합니다.

.book > p:not(:has(.thumbnail)) {
    display: none !important;
}

여기까지 처리했으면 썸네일로 지정했던 이미지와 요약문 말고 모든 본문 내용이 사라져 있을 것입니다. 이제 이미지와 요약문이 보다 웹진형 블로그 처럼 보이도록 너비와 높이를 고정해 줍니다.

.book blockquote {
    border: 0 !important;
    margin: 0 !important;
    padding: 0 !important;
    color: inherit !important;
}

.book img {
    display: block !important;
    height: 200px;
    width: 100%;
    object-fit: cover;
    border-radius: 20px;
}

위 CSS를 적용하면 다음과 같이 보입니다.

img

포스트 내부 CSS 적용

이제는 블로그 내부의 포스트에 CSS를 적용해 볼 것입니다. 이미지와 요약글이 맨 위에 올라가 있어서 포스트 초반이 조금 지저분해 보일 수 있기 때문입니다.

그래서, 우선은 썸네일 이미지를 포스트 제목 뒤로 빼 줍니다. 메뉴바를 제외한 포스트 상단에 딱 붙이고요.

높이와 너비를 지정해서 마치 포스트 제목의 구성요소인 것처럼 보이게 만듭니다.

마지막으로 밝기를 줄여줍니다.

#post-body .thumbnail{
    position: absolute;
    top: 70px;
    left: 0;
    z-index: -1;
    height: 180px;
    width: 100vw;
    object-fit: cover;
    filter: brightness(50%);
}

여기까지 적용했다면, 저처럼 밝은 테마를 쓰시는 분들은 포스트 제목과 날짜가 잘 보이지 않으실 겁니다.

포스트 제목과 날짜에 배경과 색을 적용해서, 썸네일 이미지가 있을 때와 없을 때를 모두 대응합시다.

#post-body #title {
    margin-top: 50px !important;
    color: white;
    background: linear-gradient(90deg, black, transparent);
}

#post-body time {
    color: white !important;
    background: linear-gradient(90deg, black, transparent);
}

마지막으로 요약문 역시 포스트 헤더의 일부로 보이도록 CSS를 변경해 주었습니다.

이 요약문 부분은 그대로 두어도 꽤 예쁘기 때문에, 적용하지 않으셔도 상관없습니다.

#post-body > .e-content > blockquote:nth-of-type(1) {
    padding-top: 20px;
    padding-bottom: 10px;
    position: relative;
    top: -50px;
    z-index: -2;
    display: block !important;
    border-bottom: 2px solid #ddd;
    border-right: 2px solid #ddd;
    border-left: 2px solid #ddd !important;
    border-radius: 0 0 20px 20px;
}

여기까지 적용하셨으면 포스트 내부도 이렇게 깔끔하게 보이게 됩니다.

img

WriteFreely가 텍스트 중심인 블로그여서, 이미지 관련 기능은 거의 없다시피 하지만 CSS와 마크다운을 적절히 활용하면 불가능한 것도 아니어서 다행입니다.

+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연

뮤직 플레이어 클라이언트

기본정보

제작동기

  • 제작자의 사운드클라우드 저장용량이 30퍼센트도 안 남았습니다.
  • 그런 상황에서 팀원들과 음악 파일을 지속적으로 공유해야 할 일이 생겼습니다.
  • 파일 링크를 그대로 줘도 되지만, 사운드클라우드와 같은 제대로 된 플레이어로 주고받고 싶었습니다.
  • 기왕 공유하는 거, 악보 링크도 같이 공유해 주고 싶었습니다.
  • 정식 발매되지 않은, 합법적으로 가지고 있는 음원을 스포티파이처럼 리스트로 만들어서 브라우저에서 재생하고 싶었습니다.

플레이어 구성

  • 기본적으로 온라인상의 아무 음원이나 재생할 수 있지만, CORS 나 CORB 이슈로 인해 재생되지 않는 음원이 있을 수 있습니다.
  • 이를 피하기 위해서는 미스키 드라이브 같은 오브젝트 스토리지를 이용하거나 개인 서버에서 직접 음원파일을 호스팅하는 방법을 사용할 수 있습니다.
  • 재생, 일시정지, 정지, 볼륨 조절, 루프, 셔플 등의 기능이 있습니다.
  • 스페이스바를 통해 재생/일시정지 할 수 있습니다.

플레이리스트 모드

  • 재생목록을 불러와서 순서대로 재생해주는 기능입니다.
  • 셔플(랜덤 재생) 모드가 있습니다. 단, 화면 하단에 표시되는 재생목록의 순서가 같이 바뀌는 것은 아닙니다.
  • 루프를 끄면 처음부터 끝까지 한 번만 재생하고 멈추게 됩니다. 루프를 켜면 순서대로 반복 재생합니다.
  • 플레이리스트의 경우 모바일에서 한 곡씩 한 번만 백그라운드 재생이 가능합니다. 재생이 끝나도 다음 곡으로 넘어가지 않습니다.

로컬 플레이리스트 모드

image

  • 사용자의 로컬스토리지를 사용해서, '로컬 플레이리스트' 라고 하는 개인 재생목록을 불러오게 됩니다. 물론, 기기가 바뀌면 재생목록이 바뀝니다.
  • 재생목록에 곡을 추가하고 삭제할 수 있습니다.
  • [계획] 재생목록 내보내기 기능을 추가할 계획입니다... 생성된 텍스트파일을 Github gist같은 곳에 올리면, 다른 사람과 플레이리스트 공유가 가능합니다.

리모트 플레이리스트 모드

image

  • (CORB 이슈가 없는 한) 외부 txt파일을 읽어와서, 해당 텍스트파일의 재생목록을 읽어옵니다. 텍스트파일은 다음과 같은 구조입니다.
제목1,URL2
제목2,URL2
...
제목n,URLn
  • 리모트 모드의 경우, 재생목록을 수정할 수 없습니다(수정해도 저장할 서버가 없으니까요!).
  • 웹에서 링크를 읽어오는 것이므로, https 프로토콜을 사용하는 주소인 이상 다른 사람과 리스트 공유가 가능합니다.

악보 모니터링 모드

image

  • 한 개의 음원과, 한 개의 PDF를 위아래에 두고 함께 볼 수 있습니다.
  • PDF는 악보가 있는 경우 악보를 띄우는 것을 추천합니다.
  • PDF 주소를 입력하지 않는 경우 PDF 뷰어 자체가 보이지 않습니다.
  • 해당 음원은 데스크탑과 모바일에서 모두 백그라운드 재생, 반복 재생이 가능합니다.
+

여행당일기

잡다한 블로그

카테고리

메인
전체보기
소개
공지
일기
개발
전체보기
연합우주
MIKANTONE
CabinetKey
창작
전체보기
커미션
단편
일러스트
3D
음악
한문
현재연