GPT4でc++ほぼ書けない人がwhisperAIを叩けるプラグインを作ってみた

# GPT4を使ってc++ほぼ書けない人がunrealengineのプラグインを作ってみた
タイトルにある通りc++を触る必要性がいままでなかったので触ってこなかったのですが、apiを叩く上でue4だと既存のプラグインだとstringでしかリクエストを送れない問題がありました。ですので今回はgpt4君にwhisper APIを使って文字起こししてくれるpluginを作ってもらいました。
## 完成したもの
先にお見せします
![スクリーンショット 2023-03-15 202713.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/6cc2ed89-20a0-b7d1-e2c6-c12966e88cd6.png)
### 入力したワード-1
> ヘッダーファイル
 // Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"
/*** 
 */UCLASS()
class MYPROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()	
};
と
cppファイル
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyBlueprintFunctionLibrary.h"
を修正してwhisper APIのhttpリクエストを送れるようにしてください。whisperは
curl --request POST \
  --url https://api.openai.com/v1/audio/transcriptions \
  --header 'Authorization: Bearer TOKEN' \
  --header 'Content-Type: multipart/form-data' \
  --form file=@/path/to/file/openai.mp3 \
  --form model=whisper-1
のように投稿できます。今回はTOKENとfileのパスを変更できるようにしてください
### 送られてきたもの-1

```
// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "Whisper API")
	static void SendWhisperAPIRequest(FString Token, FString FilePath);
};
```

```
// Fill out your copyright notice in the Description page of Project Settings.

#include "MyBlueprintFunctionLibrary.h"
#include "Runtime/Online/HTTP/Public/Http.h"

void UMyBlueprintFunctionLibrary::SendWhisperAPIRequest(FString Token, FString FilePath)
{
	FHttpModule* Http = &FHttpModule::Get();
	
	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
	Request->SetVerb("POST");
	Request->SetURL("https://api.openai.com/v1/audio/transcriptions");
	Request->SetHeader("Authorization", FString::Printf(TEXT("Bearer %s"), *Token));
	Request->SetHeader("Content-Type", "multipart/form-data");

	// Load file
	TArray<uint8> FileContent;
	if (!FFileHelper::LoadFileToArray(FileContent, *FilePath))
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to load file: %s"), *FilePath);
		return;
	}

	// Add file content to request
	FString Boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
	FString PreFileBoundary = FString::Printf(TEXT("--%s\r\nContent-Disposition: form-data; name=\"file\"; filename=\"openai.mp3\"\r\nContent-Type: audio/mpeg\r\n\r\n"), *Boundary);
	FString PostFileBoundary = FString::Printf(TEXT("\r\n--%s\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\nwhisper-1\r\n--%s--\r\n"), *Boundary, *Boundary);
	Request->SetHeader("Content-Type", FString::Printf(TEXT("multipart/form-data; boundary=%s"), *Boundary));

	TArray<uint8> RequestContent;
	RequestContent.Append((uint8*)TCHAR_TO_UTF8(*PreFileBoundary), PreFileBoundary.Len());
	RequestContent.Append(FileContent);
	RequestContent.Append((uint8*)TCHAR_TO_UTF8(*PostFileBoundary), PostFileBoundary.Len());
	Request->SetContent(RequestContent);

	Request->OnProcessRequestComplete().BindLambda([](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
	{
		if (!bWasSuccessful || !Response.IsValid())
		{
			UE_LOG(LogTemp, Error, TEXT("Whisper API request failed"));
			return;
		}

		UE_LOG(LogTemp, Log, TEXT("Whisper API response: %s"), *Response->GetContentAsString());
	});

	Request->ProcessRequest();
}
```
### 入力したワード-2
>CompilerResultsLog: Error:   MyBlueprintFunctionLibrary.cpp.obj : error LNK2019: ???????̊O???V???{?? "__declspec(dllimport) public: static class FHttpModule & __cdecl FHttpModule::Get(void)" (__imp_?Get@FHttpModule@@SAAEAV1@XZ) ???֐? "public: static void __cdecl UMyBlueprintFunctionLibrary::SendWhisperAPIRequest(class FString,class FString)"....

### 送られてきたもの-2
>すみません。エラーが発生しているようです。このエラーは、FHttpModuleがリンクされていないために起こっています。プロジェクトのビルド設定でHTTPモジュールを追加することで解決できます。

### 入力したワード-3
>成功したかの判定と、戻り値としてstringを出力して

### 送られてきたもの-3

```

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "Whisper API")
	static FString SendWhisperAPIRequest(FString Token, FString FilePath, bool& bWasSuccessful);
};

```

```
// Fill out your copyright notice in the Description page of Project Settings.

#include "MyBlueprintFunctionLibrary.h"
#include "Runtime/Online/HTTP/Public/Http.h"

FString UMyBlueprintFunctionLibrary::SendWhisperAPIRequest(FString Token, FString FilePath, bool& bWasSuccessful)
{
	FHttpModule* Http = &FHttpModule::Get();
	
	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
	Request->SetVerb("POST");
	Request->SetURL("https://api.openai.com/v1/audio/transcriptions");
	Request->SetHeader("Authorization", FString::Printf(TEXT("Bearer %s"), *Token));
	Request->SetHeader("Content-Type", "multipart/form-data");

	// Load file
	TArray<uint8> FileContent;
	if (!FFileHelper::LoadFileToArray(FileContent, *FilePath))
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to load file: %s"), *FilePath);
		bWasSuccessful = false;
		return FString();
	}

	// Add file content to request
	FString Boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
	FString PreFileBoundary = FString::Printf(TEXT("--%s\r\nContent-Disposition: form-data; name=\"file\"; filename=\"openai.mp3\"\r\nContent-Type: audio/mpeg\r\n\r\n"), *Boundary);
	FString PostFileBoundary = FString::Printf(TEXT("\r\n--%s\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\nwhisper-1\r\n--%s--\r\n"), *Boundary, *Boundary);
	Request->SetHeader("Content-Type", FString::Printf(TEXT("multipart/form-data; boundary=%s"), *Boundary));

	TArray<uint8> RequestContent;
	RequestContent.Append((uint8*)TCHAR_TO_UTF8(*PreFileBoundary), PreFileBoundary.Len());
	RequestContent.Append(FileContent);
	RequestContent.Append((uint8*)TCHAR_TO_UTF8(*PostFileBoundary), PostFileBoundary.Len());
	Request->SetContent(RequestContent);

	FString ResponseContent;
	FEvent* Event = FGenericPlatformProcess::GetSynchEventFromPool(true);

	Request->OnProcessRequestComplete().BindLambda([&](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccessful)
	{
		if (!bSuccessful || !Response.IsValid())
		{
			UE_LOG(LogTemp, Error, TEXT("Whisper API request failed"));
			bWasSuccessful = false;
		}
		else
		{
			ResponseContent = Response->GetContentAsString();
			bWasSuccessful = true;
		}
		Event->Trigger();
	});

	Request->ProcessRequest();
	Event->Wait();
	FGenericPlatformProcess::ReturnSynchEventToPool(Event);
	Event = nullptr;

	return ResponseContent;
}
```
>この関数を呼び出す際に、結果が成功したかどうかを判断することができます。ただし、この実装は同期的に実行されるため、リクエストの処理が終わるまでゲームが停止する可能性があります。

## 実行してみた
![スクリーンショット 2023-03-15 194007.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/b02ccae3-375d-2560-6710-cc98e80bf523.png)

いわれた通りフリーズした

### 入力したワード-4
>続けて

### 送られてきたもの-4
```
#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "MyBlueprintFunctionLibrary.generated.h"

DECLARE_DYNAMIC_DELEGATE_TwoParams(FWhisperAPIResponseDelegate, bool, bWasSuccessful, FString, ResponseContent);

UCLASS()
class MYPROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "Whisper API")
	static void SendWhisperAPIRequestAsync(FString Token, FString FilePath, FWhisperAPIResponseDelegate ResponseDelegate);
};
```
```
#include "MyBlueprintFunctionLibrary.h"
#include "Runtime/Online/HTTP/Public/Http.h"

void UMyBlueprintFunctionLibrary::SendWhisperAPIRequestAsync(FString Token, FString FilePath, FWhisperAPIResponseDelegate ResponseDelegate)
{
	FHttpModule* Http = &FHttpModule::Get();
	
	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = Http->CreateRequest();
	Request->SetVerb("POST");
	Request->SetURL("https://api.openai.com/v1/audio/transcriptions");
	Request->SetHeader("Authorization", FString::Printf(TEXT("Bearer %s"), *Token));
	Request->SetHeader("Content-Type", "multipart/form-data");

	TArray<uint8> FileContent;
	if (!FFileHelper::LoadFileToArray(FileContent, *FilePath))
	{
		UE_LOG(LogTemp, Error, TEXT("Failed to load file: %s"), *FilePath);
		ResponseDelegate.ExecuteIfBound(false, FString());
		return;
	}

	FString Boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW";
	FString PreFileBoundary = FString::Printf(TEXT("--%s\r\nContent-Disposition: form-data; name=\"file\"; filename=\"openai.mp3\"\r\nContent-Type: audio/mpeg\r\n\r\n"), *Boundary);
	FString PostFileBoundary = FString::Printf(TEXT("\r\n--%s\r\nContent-Disposition: form-data; name=\"model\"\r\n\r\nwhisper-1\r\n--%s--\r\n"), *Boundary, *Boundary);
	Request->SetHeader("Content-Type", FString::Printf(TEXT("multipart/form-data; boundary=%s"), *Boundary));

	TArray<uint8> RequestContent;
	RequestContent.Append((uint8*)TCHAR_TO_UTF8(*PreFileBoundary), PreFileBoundary.Len());
	RequestContent.Append(FileContent);
	RequestContent.Append((uint8*)TCHAR_TO_UTF8(*PostFileBoundary), PostFileBoundary.Len());
	Request->SetContent(RequestContent);

	Request->OnProcessRequestComplete().BindLambda([ResponseDelegate](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccessful)
	{
		if (!bSuccessful || !Response.IsValid())
		{
			UE_LOG(LogTemp, Error, TEXT("Whisper API request failed"));
			ResponseDelegate.ExecuteIfBound(false, FString());
		}
		else
		{
			FString ResponseContent = Response->GetContentAsString();
			ResponseDelegate.ExecuteIfBound(true, ResponseContent);
		}
	});

	Request->ProcessRequest();
}
```
↑動いた
## まとめ
一回じゃ成功しませんがエラー文章コピペすれば大体直してくれます。
すごい時代になりましたね。
[UE4のデモはこちらから](https://drive.google.com/file/d/1sbsWRrk7JcAAWYenTVcZvXp6LnNxDe5J/view?usp=share_link)