まるぼ実験場

アプリの開発日記を載せるサイトでしたが、ただの技術ブログになりました。

【Python】VTuberアプリソースコード【2024/05/12更新】


何の変哲もないアプリで遅くなりましたが公開いたします!
サムネもなんとなくYouTube風。

VTuberって、最近では手軽に参入できるアプリが出来はじめたものの、自分オリジナルのキャラを使いたい!と思うとすごーく手間がかかるってイメージですよね。

まずキャラクターをデザインして…
3Dだったらモデリングして、モーションキャプチャーして…
2Dでもグラフィックを細かくパーツ分けして…
配信を賑やかにしたいけどちょっとそこまでは厳しい…って思いながらひそかにバーチャルアバター配信を見て(最低限どんな動作があればぎりぎりそれっぽく見えるのか)研究しつつ、考えたものがこちら。

『なんちゃってVTuber』基本仕様

・2Dイラストのみ対応
・一定時間ごとにイラストが目パチ(瞬き)する
・マイク入力に合わせて口パクする

これだけ。カメラも使わない音声検知だけ簡単なんちゃってVTuber

なんか…どっかで見たような絵面だなと思ってたらゆっくりだこれ。
…これでVTuberなんて言ったらファンに怒られますかね?

でもやっぱり、自分の動画内でキャラクターが反応してくれるって面白いね…口パクと目パチだけでだいぶ印象違う。リアルタイム感が出た気がします。

技術的な話。

動作ツールはPython、使用した画像も顔のベース、目と口それぞれ二種、目と口より上に来るパーツの透明情報入り画像たった6枚だけ。

ウィンドウ画面表示に関しては、
OpenCVを使うかPygameにするか悩んだのですが、FPSの調整とかがやりやすそうだなと思いPygameを使用。
将来的に凝った画像加工や認識を使いたいならOpenCVにする方がいいかもしれない。
 
というか今の仕様なら画像の透明部分重ねて表示できればなんでもいい。

音声認識に関してはこちらを参考にしました。
qiita.com
基本的な流れとしては、Pygameの1ループに一回音声を検知して、入力があったら画像を切り替えるだけの雑な設定。

更新履歴

2021/05/30 初版
2024/05/03 不具合修正版ソースコードを初公開
2024/05/12 全体的に記事を修正。素材用画像差し替え。

※当ソースコードを用いて発生した問題について、管理人は責任を負いかねます。ご了承ください。

使い方

1. Pythonを実行できる環境を作る
2.素材をダウンロードする
ダウンロードリンクはこちら。
https://3ka.me/sozai/nantyatteV.zip

3. コマンドプロンプトを起動し、ファイルを置いたディレクトリにcdで移動し『python main_v02.py』で実行

同梱の画像はブログ筆者がフリー素材として手描きしたものなので、ご自由にお使いください。
前のサンプル画像(記事サムネで使ってるキャラ)は当時適当に描いたもので、今見たら少し恥ずかしかったので改めて新キャラクターで描き直しました。
改変などもご自由にどうぞ(zipファイルの中にクリスタ形式のファイルも同梱しております)。

画像の構造は、
base.png

kuti.png

kuti2.png

me.png

me2.png

me3.png

add.png(※全て透明色の画像)

こんな感じで重ね合わせる前提でパーツ分けされてますが、ちょっとソースいじったら通常ととじ目、口開けの3枚だけでも
何とかなると思います。
これなら、https://character-nantoka.appspot.comとかの画像も使えますね。
今回画像については将来機能拡張することを考えていますので、細かめに。

ソースコード

2024/05/12 更新版

import pygame
from pygame.locals import *
import sys
import pyaudio
import numpy as np
import threading

#pyaudio初期化
chunk = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 44100

p = pyaudio.PyAudio()
stream = p.open(format = FORMAT,channels = CHANNELS, rate = RATE, input = True, frames_per_buffer = chunk)
# 閾値
threshold = 0.025
# 音データ格納用配列
x = np.empty(chunk)

#pygame初期化
pygame.init()
screen = pygame.display.set_mode((500,500))
pygame.display.set_caption("VTuber?")

#画像取得
#基本
bg = pygame.image.load("Vtuber_base.png").convert_alpha()
image_rect = bg.get_rect()
#目
me1 = pygame.image.load("Vtuber_me.png").convert_alpha()
me2 = pygame.image.load("Vtuber_me2.png").convert_alpha()
me3 = pygame.image.load("Vtuber_me3.png").convert_alpha()
#口
kuti1 = pygame.image.load("Vtuber_kuti.png").convert_alpha()
kuti2 = pygame.image.load("Vtuber_kuti2.png").convert_alpha()
#その他
add = pygame.image.load("Vtuber_add.png").convert_alpha()
#フォント
font = pygame.font.Font(None, 20) 
#タイマー変数
Timer = 0

#描画周期設定
Clock = pygame.time.Clock()
FPS = 30

	
def draw():
	while True:
		pygame.display.update()
		
		#時間経過
		Timer = pygame.time.get_ticks()
		# 画像生成
		ImageOut(screen, image_rect, bg, Select_Eye(Timer), Select_mouse(x, threshold), Select_Add())
		
		#デバッグ用
		#DebugGraph(screen, Timer)

		pygame.display.flip()
		Clock.tick(FPS)                     #画面更新

def streamread():
	global x
	while True:
		# 音データの取得
		data = stream.read(chunk)
		
		# ndarrayに変換
		x = np.frombuffer(data, dtype="int16") / 32768.0


def main():
	while True:
	# 終了用のイベント処理
		for event in pygame.event.get():
			# 閉じるボタンが押されたときの処理
			if event.type == QUIT:
				stream.close()
				p.terminate()
				pygame.quit()
				sys.exit()

#----------------
# パーツ選択
#----------------
def Select_Eye(Timer):
	
	# 瞬き(力技。。。もうちょい自然な見せ方があるはず
	if ((Timer % 2100) <= 160):
		put_me = me2        #目(閉じ)描画
	else:
		put_me = me1        #目描画
	
	#キー押下によって目の描画オブジェクトを変更する
	pressed_key = pygame.key.get_pressed()
	if pressed_key[K_q]:
		put_me = me3            #目(><)描画
	
	return put_me

def Select_mouse(x, threshold):
	#音声認識によって口の描画オブジェクトを変更する
	if x.max() > threshold:
		put_kuti = kuti1      #音声入力があるときの口描画
	else:
		put_kuti = kuti2      #何もないときの口描画
		
	return put_kuti
	
def Select_Add():
	#一番上に描画する画像(アクセサリーなど)
	put_add = add
	
	return put_add
	
#----------------
# 描画
#----------------
def ImageOut(screen, image_rect, bg, me, kuti, add):
	screen.fill((0, 255, 0, 0))         # 画面の背景色(緑)
	screen.blit(bg, image_rect)         # ベース画像の描画
	screen.blit(me, image_rect)     # 目画像の描画
	screen.blit(kuti, image_rect)   # 口画像の描画
	screen.blit(add, image_rect)     # さらにその上に重なる画像の描画
	
	return

#----------------
# デバッグ用表示
#----------------
def DebugGraph(screen, Timer):
	print(Timer)
	
	return

#スレッド作成
thread1 = threading.Thread(target=draw)
thread2 = threading.Thread(target=streamread)
thread1.setDaemon(True)
thread2.setDaemon(True)
thread1.start()
thread2.start()

if __name__ == "__main__":
	main()

使い方

サンプル素材の画像を使用して実行したウィンドウ。

時間や音声の条件を元にばらばらだった画像が合成されるので、それで出力されたウィンドウをOBSに設定するだけ!

アプリで合成したウィンドウの使用例。
…ただ特筆したような機能はないし、作ろうと思えば上記情報をもとにすぐ作れると思うので、ちょっとしたネタにいかがですか!?
やろうと思えばキャラをもっと動かしたり音声の音量で表情パターン変えたりとかいろいろできそうですね。…あんまり凝りすぎてももうそれLive2Dで良くね?ってなりそうですが!

今後の展望

このプログラム、問題点は音ゲー配信と相性が悪いこと。
マイクが鍵盤の音を拾っちゃってそれで常に口パクしてる(うざい)という問題が発覚し、プレー配信時の合成画像は口パクのコードを削除した状態で撮った。
単指向性マイクにしたり音域カットしたりしたらマシになるのかもしれない。
・マイク感度の改善
・外部デバイスとの連携
・起動してるとタスクトレイのマイクが荒ぶる問題の解決(←大問題じゃないか
※2024/05/03 追記
解決しました。


いろいろと拡張できる題材だと思うので、こんな機能つけてみたよ!とか色々拡張して遊べると思います。

余談

最初Markdown記法でソース貼ろうかと思ったんだけど、
コメントの#に反応していちいち見出しになってたのね…
なので今回の記事ははてな記法で書いています。