アプリケーション開発ポータルサイト
ServerNote.NET
カテゴリー【PythonDebian
オープンソースリップシンクエンジンSadTalkerをAPI化してアプリから呼ぶ【2】
POSTED BY
2025-07-08

オープンソースリップシンクエンジンSadTalkerをDebianで動かす
オープンソースリップシンクエンジンSadTalkerをAPI化してアプリから呼ぶ【1】

前回まででAPI待ち受けのサーバー準備は整ったので、次はSwift UI iOSアプリからあらかじめ用意した画像とWAVデータを投げて、生成された動画を受け取り表示する。

1、しゃべらせる顔の画像「face_sample.png」を用意して、Assetsにドロップして登録する。

2、しゃべらせる音声データ「voice_sample.wav」を用意して、プロジェクト直下(ContentView.swiftなどと同じ場所)へドロップして登録する。「Copy items if needed」「Add to targets」を忘れずに。

3、以下のContentView.swiftコードを書く。

SwiftContentView.swiftGitHub Source
import SwiftUI
import AVKit

struct ContentView: View {
    @State private var videoURL: URL?
    @State private var isLoading = false

    var body: some View {
        VStack {
            if let videoURL = videoURL {
                VideoPlayer(player: AVPlayer(url: videoURL))
                    .frame(height: 300)
            }

            Button("アップロードして動画生成") {
                Task {
                    await uploadAndFetchVideo()
                }
            }
            .disabled(isLoading)
            .padding()

            if isLoading {
                ProgressView("生成中...")
            }
        }
    }

    func uploadAndFetchVideo() async {
        guard let image = UIImage(named: "face_sample"),
              let imageData = image.jpegData(compressionQuality: 0.8),
              let audioURL = Bundle.main.url(forResource: "voice_sample", withExtension: "wav")
        else {
            print("画像または音声が見つかりません")
            return
        }

        isLoading = true
        defer { isLoading = false }

        let boundary = "Boundary-\(UUID().uuidString)"
        var request = URLRequest(url: URL(string: "https://voicetech.jorudan.co.jp/sadtalker_api/generate")!)
        request.httpMethod = "POST"
        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

        var body = Data()

        // 画像パート
        body.append("--\(boundary)\r\n")
        body.append("Content-Disposition: form-data; name=\"source_image\"; filename=\"image.jpg\"\r\n")
        body.append("Content-Type: image/jpeg\r\n\r\n")
        body.append(imageData)
        body.append("\r\n")

        // 音声パート
        if let audioData = try? Data(contentsOf: audioURL) {
            body.append("--\(boundary)\r\n")
            body.append("Content-Disposition: form-data; name=\"driven_audio\"; filename=\"audio.wav\"\r\n")
            body.append("Content-Type: audio/wav\r\n\r\n")
            body.append(audioData)
            body.append("\r\n")
        }

        body.append("--\(boundary)--\r\n")

        do {
            let (data, response) = try await URLSession.shared.upload(for: request, from: body)
            guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 else {
                print("エラー: サーバー応答 \(response)")
                return
            }

            // 一時ファイルに保存
            let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("result.mp4")
            try data.write(to: tempURL)
            videoURL = tempURL

        } catch {
            print("アップロードエラー: \(error)")
        }
    }
}

// ヘルパー:Data拡張
extension Data {
    mutating func append(_ string: String) {
        if let data = string.data(using: .utf8) {
            append(data)
        }
    }
}

これで実行すると、かなり待つものの生成されたしゃべる静止画をアプリで再生できる。サーバー側は成功していれば以下のようなログが出ているはずだ。

using safetensor as default
landmark Det:: 100%|| 1/1 [19.13it/s]
3DMM Extraction In Video:: 100%|| 1/1 [66.61it/s]
mel:: 100%|| 79/79 [44898.38it/s]
audio2exp:: 100%|| 8/8 [293.43it/s]
Face Renderer:: 100%|| 40/40 [2.97it/s]
The generated video is named /home/hogeuser/SadTalker/working_dir/20250708_212639/2025_07_08_21.26.39/input_input.mp4
INFO:     xxx.yy.zzz.aa:0 - "POST /sadtalker_api/generate HTTP/1.1" 200 OK

あとはアプリ側は実際にマイクで録音したデータやカメラで撮った自写真などをそのまま送れるようにすると完成が近い。疲れたので次回以降に。

※本記事は当サイト管理人の個人的な備忘録です。本記事の参照又は付随ソースコード利用後にいかなる損害が発生しても当サイト及び管理人は一切責任を負いません。
※本記事内容の無断転載を禁じます。
【WEBMASTER/管理人】
自営業プログラマーです。お仕事ください!
ご連絡は以下アドレスまでお願いします★

【キーワード検索】