import torch import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt # ==== MNIST データセットから1枚取得 ==== transform = transforms.Compose([ transforms.ToTensor(), ]) dataset = torchvision.datasets.MNIST(root="./data", train=True, download=True, transform=transform) image, _ = dataset[0] # 1枚だけ image = image.squeeze(0) # (1,28,28) → (28,28) # ==== 拡散プロセス ==== T = 10 # ステップ数 noisy_images = [] x = image.clone() for t in range(T): noise = torch.randn_like(x) * 0.1 x = (x + noise).clamp(0,1) # ノイズを足す noisy_images.append(x) # ===== 擬似的な逆拡散(平均を取ってノイズを少しずつ減らすだけ) ===== denoised_images = [] y = noisy_images[-1].clone() for t in range(T): y = (y*0.9 + image*0.1) # 単純な補間で元画像に近づける denoised_images.append(y) # ===== 可視化 ===== fig, axes = plt.subplots(3, T, figsize=(15, 5)) # 元画像 axes[0,0].imshow(image, cmap="gray") axes[0,0].set_title("Original") axes[0,0].axis("off") # 拡散 (ノイズ付与) for i in range(T): axes[1,i].imshow(noisy_images[i], cmap="gray") axes[1,i].axis("off") axes[1,0].set_title("Forward Diffusion") # 逆拡散 (ノイズ除去の雰囲気だけ) for i in range(T): axes[2,i].imshow(denoised_images[i], cmap="gray") axes[2,i].axis("off") axes[2,0].set_title("Reverse (Toy)") plt.show()
Month: August 2025
DCGAN
import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt # ==== Generator ==== class Generator(nn.Module): def __init__(self, nz=100, ngf=64, nc=1): # nz: 潜在変数次元, nc: チャネル数 super().__init__() self.main = nn.Sequential( nn.ConvTranspose2d(nz, ngf*4, 7, 1, 0, bias=False), nn.BatchNorm2d(ngf*4), nn.ReLU(True), nn.ConvTranspose2d(ngf*4, ngf*2, 4, 2, 1, bias=False), nn.BatchNorm2d(ngf*2), nn.ReLU(True), nn.ConvTranspose2d(ngf*2, nc, 4, 2, 1, bias=False), nn.Tanh() ) def forward(self, x): return self.main(x) # ==== Discriminator ==== class Discriminator(nn.Module): def __init__(self, nc=1, ndf=64): super().__init__() self.main = nn.Sequential( nn.Conv2d(nc, ndf, 4, 2, 1, bias=False), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(ndf, ndf*2, 4, 2, 1, bias=False), nn.BatchNorm2d(ndf*2), nn.LeakyReLU(0.2, inplace=True), nn.Conv2d(ndf*2, 1, 7, 1, 0, bias=False), nn.Sigmoid() ) def forward(self, x): return self.main(x).view(-1, 1) # ==== データセットの準備 ==== transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) dataset = torchvision.datasets.MNIST(root="./data", train=True, download=True, transform=transform) dataloader = torch.utils.data.DataLoader(dataset, batch_size=64, shuffle=True) # ==== モデルの初期化 ==== device = "cuda" if torch.cuda.is_available() else "cpu" netG = Generator().to(device) netD = Discriminator().to(device) criterion = nn.BCELoss() optimizerD = optim.Adam(netD.parameters(), lr=0.0002, betas=(0.5, 0.999)) optimizerG = optim.Adam(netG.parameters(), lr=0.0002, betas=(0.5, 0.999)) # ==== 学習の実行 ==== nz = 100 nz = 100 for epoch in range(1): # デモなので1エポックだけ for i, (real, _) in enumerate(dataloader): real = real.to(device) b_size = real.size(0) label_real = torch.ones(b_size, 1, device=device) label_fake = torch.zeros(b_size, 1, device=device) # --- Discriminator 学習 --- netD.zero_grad() output_real = netD(real) lossD_real = criterion(output_real, label_real) noise = torch.randn(b_size, nz, 1, 1, device=device) fake = netG(noise) output_fake = netD(fake.detach()) lossD_fake = criterion(output_fake, label_fake) lossD = lossD_real + lossD_fake lossD.backward() optimizerD.step() # --- Generator 学習 --- netG.zero_grad() output = netD(fake) lossG = criterion(output, label_real) lossG.backward() optimizerG.step() if i % 200 == 0: print(f"Epoch[{epoch}] Step[{i}] Loss_D: {lossD.item():.4f} Loss_G: {lossG.item():.4f}") # ==== 生成画像の表示 ==== noise = torch.randn(64, nz, 1, 1, device=device) fake = netG(noise).detach().cpu() grid = torchvision.utils.make_grid(fake, padding=2, normalize=True) plt.imshow(grid.permute(1, 2, 0)) plt.show()
$ python3 dcgan.py
Epoch[0] Step[0] Loss_D: 1.2740 Loss_G: 0.8563
Epoch[0] Step[200] Loss_D: 0.9803 Loss_G: 1.6264
Epoch[0] Step[400] Loss_D: 0.6745 Loss_G: 1.1438
Epoch[0] Step[600] Loss_D: 0.5692 Loss_G: 1.5479
Epoch[0] Step[800] Loss_D: 0.5681 Loss_G: 1.5251
生成AIで音声と顔の表情などが同期する動画を作りたい
### 全体の流れ
テキスト作成(セリフや説明文)
音声生成(VOICEROID / YMM / CeVIO など)
立ち絵やキャラクター素材の配置
動画編集で背景・効果音・字幕を追加
書き出してYouTube等にアップロード
### 立ち絵と音声を準備
ずんだもんの立ち絵画像(psd)をダウンロードします。
### VOICEVOXによる音声作成
VOICEVOXをダウンロード、インストール
VOICEVOXキャラクターを選んで、テキストからキャラクターボイスに変換して、.wavファイルでエクスポートします。
以下がエクスポートしたwavファイルの例です。
センテンスごとにエクスポートすることも可能です。
### HeyGenでの動画作成
– HeyGenの場合、Avatar画像(png)と音声データ(もしくはテキストデータ+HeyGenの音声)を用意すれば、完全に音声と映像が同期した動画が作成できてしまいます。ただし、freeプランだと15秒以内という制限があるので、長い動画を作るには有料プランにupgradeする必要があります。
ユーザーインターフェイスもわかりやすく、非常に簡単な操作で動画が生成できます。
### ゆっくりMovieMaker4(YMM4)
ゆっくり系の動画を生成できるソフトです。元となるキャラクター画像(立ち絵)に動きやセリフをつけながら編集することができます。YMM4内で音声をつけられるので、VOICEVOXで音声データを作る必要はありません。
YMM4はwindowsにしかインストールできず、macには対応していないので注意が必要です。
HeyGenの場合は、口、顔の表情や体の揺れなどの動作が完全に自動で生成されますが、
YMM4の場合は、音声に合わせた立ち絵の動作はユーザが指定する仕組みとなっており、アルゴリズムが違うような印象です。
capcutというブラウザで動画を編集できるソフトもあるが、こちらはどちらかというと音声に合わせてキャラクターの表情や口元が変化するような機能はない。
### HeyGenとYMM4の違い
🔹 HeyGen の仕組み(AI駆動型)
音声解析+AIモーション生成
音声データから「発話のリズム・イントネーション」を自動解析
AIモデルが「口の開閉」「表情の変化」「頭や体の動き」を推定して生成
ユーザー操作不要
立ち絵をアップするだけで、「人が喋っているような自然な動作」が自動でつく
イメージ的には「モーションキャプチャの自動生成」
🔹 YMM4 の仕組み(プリセット切替型)
ユーザーが動きを指定
「口パクON/OFF」「笑顔に差し替え」「まばたき」などを手動で配置
音声に合わせてタイムラインで「差分画像」を切り替える仕組み
つまり、HeyGenの場合は 音声データから、「口の開閉」「表情の変化」「頭や体の動き」を推定して生成している!
なるほど、つまり
=> JSONやスクリプトの形で、「フレームごとに口パク・まばたき・首の動き」を指示するタイムラインを自動生成
=> 既存のアニメーションライブラリ(例えば Live2D、Unity、After Effects、Blender など)に食わせるデータを準備
をすれば、HeyGenに近いことができるようになる。なるほど〜、仕組み的に面白いですね。
LSTM(Long Short-Term Memory)
import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt # ===== データ生成 ===== x = np.linspace(0, 100, 1000) y = np.sin(x) # 学習用に系列データを作成 seq_length = 20 X, Y = [], [] for i in range(len(y) - seq_length): X.append(y[i:i + seq_length]) Y.append(y[i + seq_length]) X = np.array(X) Y = np.array(Y) X_train = torch.tensor(X, dtype=torch.float32).unsqueeze(-1) # (batch_size, seq_length, input_size) Y_train = torch.tensor(Y, dtype=torch.float32).unsqueeze(-1) # (batch_size, output_size) # ===== LSTMモデル定義 ===== class LSTMModel(nn.Module): def __init__(self, input_size=1, hidden_size=50, output_size=1): super(LSTMModel, self).__init__() self.lstm = nn.LSTM(input_size, hidden_size, batch_first=True) self.fc = nn.Linear(hidden_size, output_size) def forward(self, x): out, _ = self.lstm(x) out = out[:, -1, :] # 最後のタイムステップの出力を取得 out = self.fc(out) # 最後のタイムステップの出力を使用 return out model = LSTMModel() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # ===== 学習 ===== epochs = 10 for epoch in range(epochs): optimizer.zero_grad() outputs = model(X_train) loss = criterion(outputs, Y_train) loss.backward() optimizer.step() print(f'Epoch [{epoch + 1}], Loss: {loss.item():.4f}') # ===== 予測 ===== model.eval() with torch.no_grad(): preds = model(X_train).numpy() # ===== 結果のプロット ===== plt.plot(Y, label='True') plt.plot(preds, label='Predicted') plt.legend() plt.show()
$ python3 lstm.py
Epoch [1], Loss: 0.4882
Epoch [2], Loss: 0.4804
Epoch [3], Loss: 0.4726
Epoch [4], Loss: 0.4648
Epoch [5], Loss: 0.4570
Epoch [6], Loss: 0.4492
Epoch [7], Loss: 0.4414
Epoch [8], Loss: 0.4335
Epoch [9], Loss: 0.4254
Epoch [10], Loss: 0.4172
変分オートエンコーダ(VAE)
import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader transform = transforms.Compose([ transforms.ToTensor(), ]) train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True) # ==== VAE model ==== class VAE(nn.Module): def __init__(self, input_dim=784, hidden_dim=400, latent_dim=20): super(VAE, self).__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc_mu = nn.Linear(hidden_dim, latent_dim) self.fc_logvar = nn.Linear(hidden_dim, latent_dim) self.fc2 = nn.Linear(latent_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, input_dim) def encode(self, x): h = torch.relu(self.fc1(x)) mu = self.fc_mu(h) logvar = self.fc_logvar(h) return mu, logvar def reparameterize(self, mu, logvar): std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) return mu + eps * std def decode(self, z): h = torch.relu(self.fc2(z)) return torch.sigmoid(self.fc3(h)) def forward(self, x): mu, logvar = self.encode(x) z = self.reparameterize(mu, logvar) return self.decode(z), mu, logvar # ===== model training ===== model = VAE() optimizer = optim.Adam(model.parameters(), lr=1e-3) def loss_function(recon_x, x, mu, logvar): BCE = nn.functional.binary_cross_entropy(recon_x, x, reduction='sum') KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) return BCE + KLD # ===== training loop ===== epochs = 5 for epoch in range(epochs): model.train() train_loss = 0 for batch_idx, (data, _) in enumerate(train_loader): data = data.view(-1, 784) # Flatten the input optimizer.zero_grad() recon_batch, mu, logvar = model(data) loss = loss_function(recon_batch, data, mu, logvar) loss.backward() train_loss += loss.item() optimizer.step() print(f'Epoch {epoch + 1}, Loss: {train_loss / len(train_loader.dataset):.4f}') # ===== new sample creating ===== model.eval() with torch.no_grad(): z = torch.randn(16, 20) # Generate random latent vectors samples = model.decode(z).view(-1, 1, 28, 28) # Decode to images
$ python3 vae.py
Epoch 1, Loss: 164.1793
Epoch 2, Loss: 121.6226
Epoch 3, Loss: 114.7562
Epoch 4, Loss: 111.7122
Epoch 5, Loss: 109.9405
Loss がエポックごとに下がっている → 学習が進んでいる証拠
VAE の Loss は「再構築誤差 (BCE) + KLダイバージェンス」なので、単純な分類モデルの Accuracy とは違って「小さくなるほど良い」という見方
畳み込みニューラルネットワーク(CNN)
import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, ), (0.5,)) ]) trainset = torchvision.datasets.MNIST(root='./data', train=True, download=True, transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True) testset = torchvision.datasets.MNIST(root='./data', train=False, download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False) class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 = nn.Conv2d(1, 16, 3, padding=1) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(16, 32, 3, padding=1) self.fc1 = nn.Linear(32 * 7 * 7, 128) self.fc2 = nn.Linear(128, 10) def forward(self, x): x = self.pool(torch.relu(self.conv1(x))) x = self.pool(torch.relu(self.conv2(x))) x = x.view(-1, 32 * 7 * 7) x = torch.relu(self.fc1(x)) x = self.fc2(x) return x model = SimpleCNN() criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001) for epoch in range(2): running_loss = 0.0 for images, labels in trainloader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() print(f'Epoch {epoch + 1}, Loss: {running_loss / len(trainloader):.4f}') # Evaluate the model correct, total = 0, 0 with torch.no_grad(): for images, labels in testloader: outputs = model(images) _, predicted = torch.max(outputs.data, 1) total += labels.size(0) correct += (predicted == labels).sum().item() print(f'Accuracy of the model on the test images: {100 * correct / total:.2f}%')
$ python3 cnn.py
100%|██████████████████████████████████████████████████████████████████████████████| 9.91M/9.91M [00:04<00:00, 2.06MB/s]
100%|███████████████████████████████████████████████████████████████████████████████| 28.9k/28.9k [00:00<00:00, 136kB/s]
100%|██████████████████████████████████████████████████████████████████████████████| 1.65M/1.65M [00:01<00:00, 1.20MB/s]
100%|██████████████████████████████████████████████████████████████████████████████| 4.54k/4.54k [00:00<00:00, 1.05MB/s]
Epoch 1, Loss: 0.1991
Epoch 2, Loss: 0.0537
Accuracy of the model on the test images: 98.68%
多層ニューラルネットワーク(MLP)
人間の脳の神経回路を模した計算モデルで、入力から出力までをつなぐ「層」の集まりで構成される。
from sklearn.datasets import load_digits from sklearn.model_selection import train_test_split from sklearn.neural_network import MLPClassifier from sklearn.metrics import accuracy_score X, y = load_digits(return_X_y=True) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) clf = MLPClassifier(hidden_layer_sizes=(100, 50), max_iter=300, random_state=42) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print("Accuracy:", accuracy_score(y_test, y_pred))
$ python3 mlp.py
Accuracy: 0.9666666666666667
線型多項分類器
from sklearn.linear_model import LogisticRegression from sklearn.datasets import load_iris X, y = load_iris(return_X_y=True) clf = LogisticRegression(multi_class='multinomial', solver='lbfgs', max_iter=200) clf.fit(X, y) print(clf.predict(X[:5]))
$ python3 linear_classifier.py
/home/vagrant/.local/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:1272: FutureWarning: ‘multi_class’ was deprecated in version 1.5 and will be removed in 1.7. From then on, it will always use ‘multinomial’. Leave it to its default value to avoid this warning.
warnings.warn(
[0 0 0 0 0]
音声解析をバックエンドからレスポンスする処理
@app.route("/avatar", methods=["POST"]) def avator_response(): data = request.json user_text = data.get("text", "") # 1. Creating a reply using ChatGPT etc. (This is a simple example) client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) completion = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "You are a helpful avatar assistant."}, {"role": "user", "content": user_text} ] ) reply_text = completion.choices[0].message.content # 2. TTS speech generation tts = gTTS(text=reply_text, lang='ja') audio_filename = os.path.join(AUDIO_DIR, f"output_{uuid.uuid4().hex}.mp3") tts.save(audio_filename) # 3. Convert MP3 to WAV using pydub wav_filename = audio_filename.replace(".mp3", ".wav") sound = AudioSegment.from_mp3(audio_filename) sound.export(wav_filename, format="wav") # 4. Audio data analysis (amplitude calculation for lip-syncing) audio = AudioSegment.from_wav(wav_filename) samples = np.array(audio.get_array_of_samples()) if audio.channels == 2: samples = samples.reshape((-1, 2)).mean(axis=1) samples = samples / np.max(np.abs(samples)) # Normalize # Frame-by-frame amplitude sampling fps = 30 samples_per_frame = int(audio.frame_rate / fps) lip_sync = [] for i in range(0, len(samples), samples_per_frame): frame = samples[i:i + samples_per_frame] lip_sync.append(float(np.abs(frame).mean())) # 5. Return to client response_data = { "text": reply_text, "audio_url": url_for("get_audio", filename=os.path.basename(wav_filename), _external=True), "lip_sync": lip_sync } return Response(json.dumps(response_data, ensure_ascii=False), mimetype="application/json") @app.route("/audio/<filename>", methods=["GET"]) def get_audio(filename): file_path = os.path.join(AUDIO_DIR, filename) return send_file(file_path, mimetype="audio/wav") if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)
$ curl -X POST http://127.0.0.1:5000/avatar \
-H “Content-Type: application/json” \
-d ‘{“text”: “おはよう!”}’
{“text”: “おはようございます!お困りのことはありますか?”, “audio_url”: “http://127.0.0.1:5000/audio/output_8b9b617f9ea44b4e9382d99069279c2b.wav”, “lip_sync”: [0.0, 5.401201227152919e-08, 0.008150034567687852, 0.1152054616946809, 0.12006854124357258, 0.08367767791556842, 0.026896253726828846, 0.01888351769433522, 0.07841339713952383, 0.2201013265350214, 0.2508166616255455, 0.2270837834334356, 0.18286134036209653, 0.12693546644773795, 0.19306745020092467, 0.2823540595428423, 0.26987355787927236, 0.30742827204770345, 0.33021129499200624, 0.3036520222097394, 0.13783822322084432, 0.053725370522404184, 0.23884381886531564, 0.26545121635051633, 0.16945415460398394, 0.04699428552910167, 0.037515015339411484, 0.22347993993864235, 0.2646327183165536, 0.22138405781445794, 0.19320739532472023, 0.20940100678390874, 0.23490348053407076, 0.22536436503478374, 0.21555653977444586, 0.14586462429244265, 0.14904603983926024, 0.13877635786198853, 0.08746219159140994, 0.02229572656958908, 0.01466869031672644, 0.010831244868858834, 0.008575973296461132, 0.0059669770556971865, 0.002284438059024327, 0.0007382901957395324, 0.0006873028561552089, 0.00023819297411744372, 0.00011202091345115153, 0.00011088666119344939, 0.00011083264918117787, 0.00014950524996759277, 0.054377403534546086, 0.15464892192023505, 0.11003192109925247, 0.01149159573089055, 0.0017083999481484681, 0.01060147776865575, 0.21648749081795793, 0.2837956941623817, 0.2357313766581688, 0.295791492027827, 0.24570480274813122, 0.2426950913883248, 0.22178412478935314, 0.1279997191375362, 0.1125969515620274, 0.18296455731754743, 0.2677368966858229, 0.30668674113122757, 0.252059802099987, 0.22629239942963317, 0.24090750983018622, 0.0999186038975068, 0.005041211165363177, 0.01174237350386726, 0.16595396016073974, 0.22518860994685216, 0.04122672082271097, 0.002156267553903988, 0.054671390917340024, 0.2686635267683533, 0.24548022080110612, 0.2177663332325109, 0.16169052197208658, 0.25034897161128633, 0.2103575595212375, 0.17521005271572399, 0.15601337337423843, 0.12766689711791904, 0.1107986756254591, 0.047134932809056736, 0.08557376960636046, 0.11917485848852785, 0.14922184893920407, 0.17545402497515447, 0.15926343818865316, 0.14388913494361147, 0.15382718316553604, 0.08909978179147043, 0.019018493713001773, 0.022057209523398003, 0.019663235103487015, 0.0030874346454651514, 0.0014317504212936955, 0.009182042086159962, 0.0501337337423843, 0.07244177505077129, 0.0849778010629564, 0.06556064468737847, 0.044696560515058555, 0.017215464719353583, 0.0009286285269844013, 0.0002996046320701724, 0.00023268374886574773, 0.00010975240893574729, 0.00011115672125480706, 0.00011115672125480704, 0.00011056258911982022, 0.00011158881735297929, 0.00011050857710754869, 0.00011126474527935009, 0.00011148079332843622, 0.00011083264918117787, 0.00011083264918117786, 7.388843278745191e-05]}
HeyGenのようにアバターが回答する仕組み
テキストベースのチャットbotとは異なり、複数の技術が組み合わさっている。
### アバターが回答する仕組み(HeyGenのようなシステム)
1. 音声合成(Text-to-Speech, TTS)… Google Cloud Text-to-Speech, Amazon Polly
ユーザのテキスト入力を音声に変換する技術
2. 顔の動きや表情の生成
アバターの顔の動きや表情を生成するために、以下の技術が使用される
– 3Dモデリングとアニメーション: アバターの3Dモデルを作成し、表情や動きをアニメーションで表現 Unity, Unreal Engine
– フェイシャルキャプチャ: ユーザの表情をリアルタイムでキャプチャし、それをアバターに反映させる
3. 音声とアニメーションの同期
生成した音声とアニメーションを再生
### gTTSによる簡単な音声合成(Text-to-Speech, TTS)
$ pip3 install gTTS
from gtts import gTTS import os text = "こんにちは、私はAIアバターです!" tts = gTTS(text=text, lang='ja') tts.save("output.mp3") # os.system("start output.mp3") # For Windows
### 口パク同期処理
$ pip install gTTS pygame pydub numpy
gTTS -> 音声生成
pygame -> 口パクの可視化(簡易アバター表示)
pydub -> 音声の振幅解析
numpy -> 音声データ処理
from gtts import gTTS from pydub import AudioSegment import numpy as np import pygame import os # --- 1. 音声生成 --- text = "こんにちは、私はAIアバターです!" tts = gTTS(text=text, lang='ja') tts.save("output.mp3") # mp3 → wav に変換(pydubで扱いやすくするため) sound = AudioSegment.from_mp3("output.mp3") sound.export("output.wav", format="wav") # --- 2. 音声データを読み込む --- audio = AudioSegment.from_wav("output.wav") samples = np.array(audio.get_array_of_samples()) # モノラルに変換(ステレオの場合) if audio.channels == 2: samples = samples.reshape((-1, 2)) samples = samples.mean(axis=1) # 振幅を正規化 samples = samples / np.max(np.abs(samples)) # --- 3. Pygameで口パク表示 --- pygame.init() screen = pygame.display.set_mode((400, 400)) pygame.display.set_caption("口パクアバター") # 音声再生開始 os.system("start output.wav") # macOS: afplay output.wav / Linux: aplay output.wav clock = pygame.time.Clock() running = True idx = 0 sample_rate = audio.frame_rate fps = 30 # 1秒あたりのフレーム数 samples_per_frame = int(sample_rate / fps) while running and idx < len(samples): for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 現フレームの振幅を計算 frame = samples[idx:idx+samples_per_frame] amplitude = np.abs(frame).mean() # 口の高さを振幅に応じて変化 mouth_height = int(50 + amplitude * 200) # 背景 screen.fill((255, 255, 255)) # 顔(円) pygame.draw.circle(screen, (255, 224, 189), (200, 200), 100) # 口(長方形) pygame.draw.rect(screen, (150, 0, 0), (150, 250, 100, mouth_height)) pygame.display.flip() idx += samples_per_frame clock.tick(fps) pygame.quit()
### フロント(X-code, AndroidStudio)とバックエンド(Python)の切り分け
##### バックエンドで担当する処理
1. ユーザー入力の受け取り
アプリから送られてくるテキストメッセージを受信
例: /chat エンドポイントで JSON 受け取り
2. AI応答生成
ChatGPT APIなどを呼び出して返信テキストを生成
例: “こんにちは!今日はどんなことを話しましょうか?”
3. 音声生成(Text-to-Speech, TTS)
生成したテキストを音声データに変換
gTTS, OpenAI TTS, Coqui TTS など
出力形式は MP3/WAV など
4. 音声解析(口パク用振幅解析)
音声データを読み込んでフレームごとの振幅を算出
numpy や pydub で RMS / 平均振幅を計算
口パクアニメーションの高さや動きの指標として返す
5. バックエンドからクライアントへの送信
{ "text": "こんにちは!", "audio_url": "https://server/output.wav", "lip_sync": [0.1, 0.2, 0.3, ...] # フレームごとの振幅データ }
##### クライアント(Android / iOS)で担当する処理
1. ユーザー入力の送信
テキストをバックエンドに送る
HTTP POST / WebSocket
2. 受信したデータの処理
テキスト表示
音声データの再生
Android: MediaPlayer / ExoPlayer
iOS: AVAudioPlayer
口パクデータの再生(振幅に応じてアバターの口を動かす)
3. 口パクアニメーション
受信した lip_sync 配列をフレーム単位で参照
Unity / SceneKit / SpriteKit などでアバターの口の高さや形を変化させる
バックエンドは「音声と口パクデータの生成」まで
実際の描画や音声再生はアプリ側で行う
なるほど、この仕組みは凄い!