Add Games File

This commit is contained in:
Ivan Anikin
2026-05-19 13:42:41 +07:00
parent 7908479e0c
commit 12e7e22c1a
3672 changed files with 125922 additions and 0 deletions
@@ -0,0 +1,8 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSPresetDataAsset.h"
UQwenTTSPresetDataAsset::UQwenTTSPresetDataAsset()
{
PresetId = TEXT("23c7eff6-5a38-451c-9b31-2d312a4e8b3d");
}
@@ -0,0 +1,454 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSStreamingComponent.h"
#include "Async/Async.h"
#include "IWebSocket.h"
#include "Json.h"
#include "Misc/Base64.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
#include "QwenTTSStreamingLog.h"
#include "Sound/SoundWaveProcedural.h"
#include "WebSocketsModule.h"
namespace
{
struct FDecodedWavChunk
{
TArray<uint8> PCM;
int32 SampleRate = 0;
int32 NumChannels = 0;
int32 BitsPerSample = 0;
};
uint16 ReadLE16(const uint8* Data)
{
return static_cast<uint16>(Data[0]) | (static_cast<uint16>(Data[1]) << 8);
}
uint32 ReadLE32(const uint8* Data)
{
return static_cast<uint32>(Data[0]) | (static_cast<uint32>(Data[1]) << 8) | (static_cast<uint32>(Data[2]) << 16) | (static_cast<uint32>(Data[3]) << 24);
}
bool HasTag(const TArray<uint8>& Bytes, int32 Offset, const ANSICHAR* Tag)
{
return Bytes.IsValidIndex(Offset + 3)
&& Bytes[Offset] == static_cast<uint8>(Tag[0])
&& Bytes[Offset + 1] == static_cast<uint8>(Tag[1])
&& Bytes[Offset + 2] == static_cast<uint8>(Tag[2])
&& Bytes[Offset + 3] == static_cast<uint8>(Tag[3]);
}
bool DecodeWavPCM16(const TArray<uint8>& WavBytes, FDecodedWavChunk& OutChunk, FString& OutError)
{
if (WavBytes.Num() < 44 || !HasTag(WavBytes, 0, "RIFF") || !HasTag(WavBytes, 8, "WAVE"))
{
OutError = TEXT("audio_b64 is not a RIFF/WAVE chunk");
return false;
}
bool bFoundFmt = false;
bool bFoundData = false;
uint16 AudioFormat = 0;
int32 Offset = 12;
int32 DataOffset = 0;
int32 DataSize = 0;
while (Offset + 8 <= WavBytes.Num())
{
const uint32 ChunkSize = ReadLE32(&WavBytes[Offset + 4]);
const int32 ChunkDataOffset = Offset + 8;
const int32 NextOffset = ChunkDataOffset + static_cast<int32>(ChunkSize) + (ChunkSize % 2);
if (ChunkDataOffset + static_cast<int32>(ChunkSize) > WavBytes.Num())
{
break;
}
if (HasTag(WavBytes, Offset, "fmt "))
{
if (ChunkSize < 16)
{
OutError = TEXT("Invalid WAV fmt chunk");
return false;
}
AudioFormat = ReadLE16(&WavBytes[ChunkDataOffset]);
OutChunk.NumChannels = static_cast<int32>(ReadLE16(&WavBytes[ChunkDataOffset + 2]));
OutChunk.SampleRate = static_cast<int32>(ReadLE32(&WavBytes[ChunkDataOffset + 4]));
OutChunk.BitsPerSample = static_cast<int32>(ReadLE16(&WavBytes[ChunkDataOffset + 14]));
bFoundFmt = true;
}
else if (HasTag(WavBytes, Offset, "data"))
{
DataOffset = ChunkDataOffset;
DataSize = static_cast<int32>(ChunkSize);
bFoundData = true;
}
Offset = NextOffset;
}
if (!bFoundFmt || !bFoundData)
{
OutError = TEXT("WAV chunk has no fmt/data section");
return false;
}
if (AudioFormat != 1 || OutChunk.BitsPerSample != 16)
{
OutError = FString::Printf(TEXT("Unsupported WAV format: format=%d bits=%d"), AudioFormat, OutChunk.BitsPerSample);
return false;
}
OutChunk.PCM.SetNumUninitialized(DataSize);
FMemory::Memcpy(OutChunk.PCM.GetData(), &WavBytes[DataOffset], DataSize);
return true;
}
void AddStringField(TSharedRef<FJsonObject> JsonObject, const FString& Key, const FString& Value)
{
if (!Value.IsEmpty())
{
JsonObject->SetStringField(Key, Value);
}
}
}
UQwenTTSStreamingComponent::UQwenTTSStreamingComponent()
{
bAutoActivate = false;
bAutoDestroy = false;
PrimaryComponentTick.bCanEverTick = false;
}
bool UQwenTTSStreamingComponent::StreamText(const FString& Text)
{
if (Text.TrimStartAndEnd().IsEmpty())
{
BroadcastError(TEXT("Text is empty"));
return false;
}
if (!Settings)
{
BroadcastError(TEXT("Qwen TTS settings data asset is not set"));
return false;
}
if (!Preset)
{
BroadcastError(TEXT("Qwen TTS preset data asset is not set"));
return false;
}
if (Settings->GatewayWebSocketUrl.IsEmpty())
{
BroadcastError(TEXT("GatewayWebSocketUrl is empty"));
return false;
}
if (Settings->bStopAudioBeforeRequest)
{
UE_LOG(LogQwenTTSStreaming, Verbose, TEXT("Stopping current audio before a new Qwen TTS request."));
Stop();
}
StopStreaming();
PendingText = Text;
UE_LOG(
LogQwenTTSStreaming,
Log,
TEXT("Starting Qwen TTS stream. Url=%s PresetId=%s Language=%s TextChars=%d"),
*Settings->GatewayWebSocketUrl,
Preset ? *Preset->PresetId : TEXT("<none>"),
Preset ? *Preset->Language : TEXT("<none>"),
Text.Len());
Connect();
return true;
}
void UQwenTTSStreamingComponent::StopStreaming()
{
if (bStreaming || Socket.IsValid() || ProceduralSoundWave)
{
UE_LOG(LogQwenTTSStreaming, Verbose, TEXT("Stopping Qwen TTS stream."));
}
bStreaming = false;
bPlaybackStarted = false;
PendingText.Reset();
if (Socket.IsValid())
{
Socket->Close();
Socket.Reset();
}
if (ProceduralSoundWave)
{
ProceduralSoundWave->ResetAudio();
}
}
bool UQwenTTSStreamingComponent::IsStreaming() const
{
return bStreaming && Socket.IsValid();
}
bool UQwenTTSStreamingComponent::IsGeneratingVoice() const
{
return bStreaming;
}
bool UQwenTTSStreamingComponent::IsPlayingVoiceAudio() const
{
return IsPlaying();
}
void UQwenTTSStreamingComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
StopStreaming();
Super::EndPlay(EndPlayReason);
}
void UQwenTTSStreamingComponent::Connect()
{
FWebSocketsModule& WebSocketsModule = FModuleManager::LoadModuleChecked<FWebSocketsModule>(TEXT("WebSockets"));
Socket = WebSocketsModule.CreateWebSocket(Settings->GatewayWebSocketUrl);
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Connecting Qwen TTS WebSocket: %s"), *Settings->GatewayWebSocketUrl);
Socket->OnConnected().AddUObject(this, &UQwenTTSStreamingComponent::HandleConnected);
Socket->OnConnectionError().AddUObject(this, &UQwenTTSStreamingComponent::HandleConnectionError);
Socket->OnClosed().AddUObject(this, &UQwenTTSStreamingComponent::HandleClosed);
Socket->OnMessage().AddUObject(this, &UQwenTTSStreamingComponent::HandleMessage);
bStreaming = true;
Socket->Connect();
}
void UQwenTTSStreamingComponent::HandleConnected()
{
if (!Socket.IsValid())
{
return;
}
const FString RequestJson = BuildRequestJson();
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS WebSocket connected. Sending request bytes=%d"), RequestJson.Len());
Socket->Send(RequestJson);
OnStreamStarted.Broadcast();
}
void UQwenTTSStreamingComponent::HandleConnectionError(const FString& Error)
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS WebSocket connection error: %s"), *Error);
bStreaming = false;
BroadcastError(Error);
}
void UQwenTTSStreamingComponent::HandleClosed(int32 StatusCode, const FString& Reason, bool bWasClean)
{
if (bWasClean)
{
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS WebSocket closed. StatusCode=%d Clean=true Reason=%s"), StatusCode, *Reason);
}
else
{
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS WebSocket closed. StatusCode=%d Clean=false Reason=%s"), StatusCode, *Reason);
}
bStreaming = false;
if (!bWasClean && StatusCode != 1000)
{
BroadcastError(FString::Printf(TEXT("Qwen TTS socket closed: %d %s"), StatusCode, *Reason));
}
}
void UQwenTTSStreamingComponent::HandleMessage(const FString& Message)
{
TSharedPtr<FJsonObject> JsonObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Bad JSON message from Qwen TTS gateway. Bytes=%d"), Message.Len());
BroadcastError(TEXT("Bad JSON message from Qwen TTS gateway"));
return;
}
const FString Type = JsonObject->GetStringField(TEXT("type"));
UE_LOG(LogQwenTTSStreaming, VeryVerbose, TEXT("Qwen TTS message received. Type=%s Bytes=%d"), *Type, Message.Len());
if (Type == TEXT("chunk"))
{
FString AudioBase64;
if (JsonObject->TryGetStringField(TEXT("audio_b64"), AudioBase64))
{
const int32 Index = static_cast<int32>(JsonObject->GetIntegerField(TEXT("index")));
HandleAudioBase64(Index, AudioBase64);
}
else
{
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS chunk message has no audio_b64 field."));
}
return;
}
if (Type == TEXT("done"))
{
const int32 ChunkCount = JsonObject->HasTypedField<EJson::Number>(TEXT("chunks"))
? static_cast<int32>(JsonObject->GetIntegerField(TEXT("chunks")))
: -1;
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS stream finished. Chunks=%d"), ChunkCount);
bStreaming = false;
if (Socket.IsValid())
{
Socket->Close();
Socket.Reset();
}
OnStreamFinished.Broadcast();
return;
}
if (Type == TEXT("error"))
{
FString ErrorMessage;
if (!JsonObject->TryGetStringField(TEXT("message"), ErrorMessage))
{
JsonObject->TryGetStringField(TEXT("body"), ErrorMessage);
}
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS gateway error message: %s"), *(ErrorMessage.IsEmpty() ? Message : ErrorMessage));
BroadcastError(ErrorMessage.IsEmpty() ? Message : ErrorMessage);
}
}
void UQwenTTSStreamingComponent::HandleAudioBase64(int32 Index, const FString& AudioBase64)
{
TArray<uint8> WavBytes;
if (!FBase64::Decode(AudioBase64, WavBytes))
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Failed to decode Qwen TTS audio chunk. Index=%d Base64Chars=%d"), Index, AudioBase64.Len());
BroadcastError(TEXT("Failed to decode audio_b64"));
return;
}
FDecodedWavChunk DecodedChunk;
FString Error;
if (!DecodeWavPCM16(WavBytes, DecodedChunk, Error))
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Failed to decode Qwen TTS WAV chunk. Index=%d Error=%s"), Index, *Error);
BroadcastError(Error);
return;
}
EnsureProceduralWave(DecodedChunk.SampleRate, DecodedChunk.NumChannels);
if (!ProceduralSoundWave)
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS procedural sound wave is missing after decode. Index=%d"), Index);
return;
}
ProceduralSoundWave->QueueAudio(DecodedChunk.PCM.GetData(), DecodedChunk.PCM.Num());
UE_LOG(
LogQwenTTSStreaming,
Verbose,
TEXT("Queued Qwen TTS audio chunk. Index=%d SampleRate=%d Channels=%d PCMBytes=%d"),
Index,
DecodedChunk.SampleRate,
DecodedChunk.NumChannels,
DecodedChunk.PCM.Num());
if (Settings && Settings->bAutoPlayOnFirstChunk && !bPlaybackStarted)
{
SetSound(ProceduralSoundWave);
Play();
bPlaybackStarted = true;
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Qwen TTS playback started on first chunk. Index=%d"), Index);
}
FQwenTTSStreamChunk Chunk;
Chunk.Index = Index;
Chunk.AudioBase64 = AudioBase64;
Chunk.SampleRate = DecodedChunk.SampleRate;
Chunk.NumChannels = DecodedChunk.NumChannels;
Chunk.PCMBytes = DecodedChunk.PCM.Num();
OnAudioChunk.Broadcast(Chunk);
}
void UQwenTTSStreamingComponent::EnsureProceduralWave(int32 SampleRate, int32 NumChannels)
{
if (ProceduralSoundWave && CurrentSampleRate == SampleRate && CurrentNumChannels == NumChannels)
{
return;
}
ProceduralSoundWave = NewObject<USoundWaveProcedural>(this);
ProceduralSoundWave->SetSampleRate(SampleRate);
ProceduralSoundWave->NumChannels = NumChannels;
ProceduralSoundWave->Duration = Settings ? Settings->ProceduralDurationSeconds : INDEFINITELY_LOOPING_DURATION;
ProceduralSoundWave->bLooping = false;
ProceduralSoundWave->SampleByteSize = sizeof(int16);
ProceduralSoundWave->SoundGroup = SOUNDGROUP_Voice;
CurrentSampleRate = SampleRate;
CurrentNumChannels = NumChannels;
SetSound(ProceduralSoundWave);
UE_LOG(
LogQwenTTSStreaming,
Log,
TEXT("Created Qwen TTS procedural sound wave. SampleRate=%d Channels=%d Duration=%.2f"),
SampleRate,
NumChannels,
ProceduralSoundWave->Duration);
}
FString UQwenTTSStreamingComponent::BuildRequestJson() const
{
TSharedRef<FJsonObject> JsonObject = MakeShared<FJsonObject>();
JsonObject->SetStringField(TEXT("text"), PendingText);
if (Preset)
{
AddStringField(JsonObject, TEXT("preset_id"), Preset->PresetId);
AddStringField(JsonObject, TEXT("language"), Preset->Language);
AddStringField(JsonObject, TEXT("mode"), Preset->Mode);
JsonObject->SetNumberField(TEXT("chunk_size"), Preset->ChunkSize);
JsonObject->SetNumberField(TEXT("first_chunk_size"), Preset->FirstChunkSize);
JsonObject->SetNumberField(TEXT("crossfade_samples"), Preset->CrossfadeSamples);
JsonObject->SetNumberField(TEXT("temperature"), Preset->Temperature);
JsonObject->SetNumberField(TEXT("top_k"), Preset->TopK);
JsonObject->SetNumberField(TEXT("repetition_penalty"), Preset->RepetitionPenalty);
JsonObject->SetNumberField(TEXT("repetition_penalty_window"), Preset->RepetitionPenaltyWindow);
for (const TPair<FString, FString>& Pair : Preset->ExtraFields)
{
AddStringField(JsonObject, Pair.Key, Pair.Value);
}
}
if (Settings)
{
for (const TPair<FString, FString>& Pair : Settings->ExtraFields)
{
AddStringField(JsonObject, Pair.Key, Pair.Value);
}
}
FString Output;
const TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Output);
FJsonSerializer::Serialize(JsonObject, Writer);
return Output;
}
void UQwenTTSStreamingComponent::BroadcastError(const FString& Error)
{
UE_LOG(LogQwenTTSStreaming, Error, TEXT("Qwen TTS stream error: %s"), *Error);
OnStreamError.Broadcast(Error);
}
@@ -0,0 +1,9 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSStreamingLog.h"
#include "Modules/ModuleManager.h"
DEFINE_LOG_CATEGORY(LogQwenTTSStreaming);
IMPLEMENT_MODULE(FDefaultModuleImpl, QwenTTSStreaming)
@@ -0,0 +1,33 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSStreamingSettingsDataAsset.h"
UQwenTTSStreamingSettingsDataAsset::UQwenTTSStreamingSettingsDataAsset()
{
GatewayWebSocketUrl = TEXT("wss://services.ai-games-studio.ru/qwen-tts/generate/stream/ws");
GatewayStatusUrl = TEXT("https://services.ai-games-studio.ru/qwen-tts/status");
}
FString UQwenTTSStreamingSettingsDataAsset::GetResolvedGatewayStatusUrl() const
{
if (!GatewayStatusUrl.IsEmpty())
{
return GatewayStatusUrl;
}
FString ResolvedUrl = GatewayWebSocketUrl;
if (ResolvedUrl.StartsWith(TEXT("wss://")))
{
ResolvedUrl.RemoveFromStart(TEXT("wss://"));
ResolvedUrl.InsertAt(0, TEXT("https://"));
}
else if (ResolvedUrl.StartsWith(TEXT("ws://")))
{
ResolvedUrl.RemoveFromStart(TEXT("ws://"));
ResolvedUrl.InsertAt(0, TEXT("http://"));
}
ResolvedUrl.RemoveFromEnd(TEXT("/generate/stream/ws"));
ResolvedUrl.RemoveFromEnd(TEXT("/"));
return ResolvedUrl / TEXT("status");
}
@@ -0,0 +1,54 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "QwenTTSPresetDataAsset.generated.h"
class UQwenTTSStreamingSettingsDataAsset;
UCLASS(BlueprintType)
class QWENTTSSTREAMING_API UQwenTTSPresetDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UQwenTTSPresetDataAsset();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
TObjectPtr<UQwenTTSStreamingSettingsDataAsset> Settings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
FString PresetId;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
FString Language = TEXT("Russian");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Preset")
FString Mode = TEXT("voice_clone");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 ChunkSize = 25;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 FirstChunkSize = 8;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "0"))
int32 CrossfadeSamples = 256;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "0.0", ClampMax = "2.0"))
float Temperature = 0.7f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 TopK = 20;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "0.0"))
float RepetitionPenalty = 1.1f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Generation", meta = (ClampMin = "1"))
int32 RepetitionPenaltyWindow = 64;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Payload")
TMap<FString, FString> ExtraFields;
};
@@ -0,0 +1,82 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Components/AudioComponent.h"
#include "QwenTTSStreamingTypes.h"
#include "QwenTTSStreamingComponent.generated.h"
class IWebSocket;
class UQwenTTSPresetDataAsset;
class UQwenTTSStreamingSettingsDataAsset;
class USoundWaveProcedural;
UCLASS(ClassGroup = (Audio, QwenTTS), meta = (BlueprintSpawnableComponent))
class QWENTTSSTREAMING_API UQwenTTSStreamingComponent : public UAudioComponent
{
GENERATED_BODY()
public:
UQwenTTSStreamingComponent();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS")
TObjectPtr<UQwenTTSStreamingSettingsDataAsset> Settings;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS")
TObjectPtr<UQwenTTSPresetDataAsset> Preset;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamSimpleDelegate OnStreamStarted;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamChunkDelegate OnAudioChunk;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamSimpleDelegate OnStreamFinished;
UPROPERTY(BlueprintAssignable, Category = "Qwen TTS|Events")
FQwenTTSStreamMessageDelegate OnStreamError;
UFUNCTION(BlueprintCallable, Category = "Qwen TTS")
bool StreamText(const FString& Text);
UFUNCTION(BlueprintCallable, Category = "Qwen TTS")
void StopStreaming();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
bool IsStreaming() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
bool IsGeneratingVoice() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
bool IsPlayingVoiceAudio() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS")
USoundWaveProcedural* GetProceduralSoundWave() const { return ProceduralSoundWave; }
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
TSharedPtr<IWebSocket> Socket;
UPROPERTY(Transient)
TObjectPtr<USoundWaveProcedural> ProceduralSoundWave;
FString PendingText;
bool bStreaming = false;
bool bPlaybackStarted = false;
int32 CurrentSampleRate = 0;
int32 CurrentNumChannels = 0;
void Connect();
void HandleConnected();
void HandleConnectionError(const FString& Error);
void HandleClosed(int32 StatusCode, const FString& Reason, bool bWasClean);
void HandleMessage(const FString& Message);
void HandleAudioBase64(int32 Index, const FString& AudioBase64);
void EnsureProceduralWave(int32 SampleRate, int32 NumChannels);
FString BuildRequestJson() const;
void BroadcastError(const FString& Error);
};
@@ -0,0 +1,7 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
QWENTTSSTREAMING_API DECLARE_LOG_CATEGORY_EXTERN(LogQwenTTSStreaming, Log, All);
@@ -0,0 +1,40 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "QwenTTSStreamingSettingsDataAsset.generated.h"
UCLASS(BlueprintType)
class QWENTTSSTREAMING_API UQwenTTSStreamingSettingsDataAsset : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
UQwenTTSStreamingSettingsDataAsset();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Gateway")
FString GatewayWebSocketUrl;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Gateway")
FString GatewayStatusUrl;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Qwen TTS|Gateway")
FString GetResolvedGatewayStatusUrl() const;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Gateway", meta = (ClampMin = "1.0"))
float ConnectionTimeoutSeconds = 15.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Audio")
bool bAutoPlayOnFirstChunk = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Audio")
bool bStopAudioBeforeRequest = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Audio", meta = (ClampMin = "0.01"))
float ProceduralDurationSeconds = 300.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Qwen TTS|Payload")
TMap<FString, FString> ExtraFields;
};
@@ -0,0 +1,31 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "QwenTTSStreamingTypes.generated.h"
USTRUCT(BlueprintType)
struct QWENTTSSTREAMING_API FQwenTTSStreamChunk
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 Index = 0;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
FString AudioBase64;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 SampleRate = 0;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 NumChannels = 0;
UPROPERTY(BlueprintReadOnly, Category = "Qwen TTS")
int32 PCMBytes = 0;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FQwenTTSStreamChunkDelegate, const FQwenTTSStreamChunk&, Chunk);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FQwenTTSStreamMessageDelegate, const FString&, Message);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FQwenTTSStreamSimpleDelegate);
@@ -0,0 +1,24 @@
using UnrealBuildTool;
public class QwenTTSStreaming : ModuleRules
{
public QwenTTSStreaming(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"AudioMixer",
"Json",
"JsonUtilities",
"WebSockets"
});
}
}
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSDataAssetFactories.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
UQwenTTSStreamingSettingsDataAssetFactory::UQwenTTSStreamingSettingsDataAssetFactory()
{
SupportedClass = UQwenTTSStreamingSettingsDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UQwenTTSStreamingSettingsDataAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UQwenTTSStreamingSettingsDataAsset>(InParent, Class, Name, Flags | RF_Transactional);
}
UQwenTTSPresetDataAssetFactory::UQwenTTSPresetDataAssetFactory()
{
SupportedClass = UQwenTTSPresetDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UQwenTTSPresetDataAssetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UQwenTTSPresetDataAsset>(InParent, Class, Name, Flags | RF_Transactional);
}
@@ -0,0 +1,286 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "QwenTTSPresetDetailsCustomization.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "Json.h"
#include "PropertyHandle.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSStreamingLog.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "FQwenTTSPresetDetailsCustomization"
FText FQwenTTSPresetChoice::GetDisplayText() const
{
if (!Label.IsEmpty())
{
return FText::FromString(FString::Printf(TEXT("%s (%s)"), *Label, *Id));
}
return FText::FromString(Id);
}
TSharedRef<IDetailCustomization> FQwenTTSPresetDetailsCustomization::MakeInstance()
{
return MakeShared<FQwenTTSPresetDetailsCustomization>();
}
void FQwenTTSPresetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
TArray<TWeakObjectPtr<UObject>> Objects;
DetailBuilder.GetObjectsBeingCustomized(Objects);
if (Objects.Num() > 0)
{
PresetAsset = Cast<UQwenTTSPresetDataAsset>(Objects[0].Get());
}
SettingsHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UQwenTTSPresetDataAsset, Settings));
PresetIdHandle = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UQwenTTSPresetDataAsset, PresetId));
DetailBuilder.HideProperty(SettingsHandle);
DetailBuilder.HideProperty(PresetIdHandle);
IDetailCategoryBuilder& PresetCategory = DetailBuilder.EditCategory(TEXT("Qwen TTS|Preset"));
PresetCategory.AddProperty(SettingsHandle.ToSharedRef());
PresetCategory.AddCustomRow(LOCTEXT("PresetPickerFilter", "Preset Picker"))
.NameContent()
[
SNew(STextBlock)
.Text(LOCTEXT("PresetPickerLabel", "Preset Picker"))
.Font(IDetailLayoutBuilder::GetDetailFont())
]
.ValueContent()
.MinDesiredWidth(520.0f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SAssignNew(PresetComboBox, SComboBox<TSharedPtr<FQwenTTSPresetChoice>>)
.OptionsSource(&PresetChoices)
.OnGenerateWidget(this, &FQwenTTSPresetDetailsCustomization::MakePresetComboWidget)
.OnSelectionChanged(this, &FQwenTTSPresetDetailsCustomization::HandleSelectionChanged)
[
SNew(STextBlock)
.Text(this, &FQwenTTSPresetDetailsCustomization::GetSelectedPresetText)
.Font(IDetailLayoutBuilder::GetDetailFont())
]
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(6.0f, 0.0f, 0.0f, 0.0f)
[
SNew(SButton)
.Text(LOCTEXT("RefreshPresets", "Refresh"))
.OnClicked(this, &FQwenTTSPresetDetailsCustomization::RefreshPresets)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 4.0f, 0.0f, 0.0f)
[
SNew(STextBlock)
.Text(this, &FQwenTTSPresetDetailsCustomization::GetStatusText)
.Font(IDetailLayoutBuilder::GetDetailFont())
.AutoWrapText(true)
]
];
PresetCategory.AddProperty(PresetIdHandle.ToSharedRef());
RefreshPresets();
}
FReply FQwenTTSPresetDetailsCustomization::RefreshPresets()
{
const FString StatusUrl = GetStatusUrl();
if (StatusUrl.IsEmpty())
{
StatusText = LOCTEXT("MissingStatusUrl", "Set Settings or Gateway Status Url before refreshing presets.");
return FReply::Handled();
}
StatusText = FText::Format(LOCTEXT("LoadingPresets", "Loading presets from {0}"), FText::FromString(StatusUrl));
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Refreshing Qwen TTS presets from %s"), *StatusUrl);
const TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
Request->SetURL(StatusUrl);
Request->SetVerb(TEXT("GET"));
Request->SetHeader(TEXT("Accept"), TEXT("application/json"));
Request->OnProcessRequestComplete().BindSP(StaticCastSharedRef<FQwenTTSPresetDetailsCustomization>(AsShared()), &FQwenTTSPresetDetailsCustomization::HandlePresetResponse);
Request->ProcessRequest();
return FReply::Handled();
}
void FQwenTTSPresetDetailsCustomization::HandlePresetResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
if (!bWasSuccessful || !Response.IsValid())
{
StatusText = LOCTEXT("PresetRequestFailed", "Preset request failed.");
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset request failed."));
return;
}
if (Response->GetResponseCode() < 200 || Response->GetResponseCode() >= 300)
{
StatusText = FText::Format(LOCTEXT("PresetHttpFailed", "Preset request failed with HTTP {0}."), Response->GetResponseCode());
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset request failed. HTTP=%d Body=%s"), Response->GetResponseCode(), *Response->GetContentAsString());
return;
}
SetPresetChoicesFromStatusJson(Response->GetContentAsString());
}
void FQwenTTSPresetDetailsCustomization::SetPresetChoicesFromStatusJson(const FString& ResponseText)
{
TSharedPtr<FJsonObject> JsonObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ResponseText);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
StatusText = LOCTEXT("BadPresetJson", "Preset response is not valid JSON.");
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset response is not valid JSON."));
return;
}
const TArray<TSharedPtr<FJsonValue>>* PresetRefs = nullptr;
if (!JsonObject->TryGetArrayField(TEXT("preset_refs"), PresetRefs))
{
StatusText = LOCTEXT("NoPresetRefs", "Preset response has no preset_refs array.");
UE_LOG(LogQwenTTSStreaming, Warning, TEXT("Qwen TTS preset response has no preset_refs array."));
return;
}
PresetChoices.Reset();
for (const TSharedPtr<FJsonValue>& Value : *PresetRefs)
{
const TSharedPtr<FJsonObject> PresetObject = Value.IsValid() ? Value->AsObject() : nullptr;
if (!PresetObject.IsValid())
{
continue;
}
FString Id;
if (!PresetObject->TryGetStringField(TEXT("id"), Id) || Id.IsEmpty())
{
continue;
}
TSharedRef<FQwenTTSPresetChoice> Choice = MakeShared<FQwenTTSPresetChoice>();
Choice->Id = Id;
PresetObject->TryGetStringField(TEXT("label"), Choice->Label);
PresetObject->TryGetStringField(TEXT("ref_text"), Choice->RefText);
PresetObject->TryGetStringField(TEXT("source"), Choice->Source);
PresetChoices.Add(Choice);
}
if (PresetComboBox.IsValid())
{
PresetComboBox->RefreshOptions();
}
StatusText = FText::Format(LOCTEXT("PresetLoadedCount", "Loaded {0} preset(s)."), PresetChoices.Num());
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Loaded %d Qwen TTS preset(s)."), PresetChoices.Num());
}
FString FQwenTTSPresetDetailsCustomization::GetStatusUrl() const
{
UObject* SettingsObject = nullptr;
if (SettingsHandle.IsValid() && SettingsHandle->GetValue(SettingsObject) == FPropertyAccess::Success)
{
if (const UQwenTTSStreamingSettingsDataAsset* Settings = Cast<UQwenTTSStreamingSettingsDataAsset>(SettingsObject))
{
return Settings->GetResolvedGatewayStatusUrl();
}
}
if (const UQwenTTSPresetDataAsset* Preset = PresetAsset.Get())
{
if (Preset->Settings)
{
return Preset->Settings->GetResolvedGatewayStatusUrl();
}
}
return GetDefault<UQwenTTSStreamingSettingsDataAsset>()->GetResolvedGatewayStatusUrl();
}
TSharedPtr<FQwenTTSPresetChoice> FQwenTTSPresetDetailsCustomization::GetSelectedChoice() const
{
FString CurrentPresetId;
if (PresetIdHandle.IsValid())
{
PresetIdHandle->GetValue(CurrentPresetId);
}
for (const TSharedPtr<FQwenTTSPresetChoice>& Choice : PresetChoices)
{
if (Choice.IsValid() && Choice->Id == CurrentPresetId)
{
return Choice;
}
}
return nullptr;
}
FText FQwenTTSPresetDetailsCustomization::GetSelectedPresetText() const
{
if (const TSharedPtr<FQwenTTSPresetChoice> Choice = GetSelectedChoice())
{
return Choice->GetDisplayText();
}
FString CurrentPresetId;
if (PresetIdHandle.IsValid() && PresetIdHandle->GetValue(CurrentPresetId) == FPropertyAccess::Success && !CurrentPresetId.IsEmpty())
{
return FText::FromString(CurrentPresetId);
}
return LOCTEXT("NoPresetSelected", "Select preset");
}
FText FQwenTTSPresetDetailsCustomization::GetStatusText() const
{
if (const TSharedPtr<FQwenTTSPresetChoice> Choice = GetSelectedChoice())
{
if (!Choice->RefText.IsEmpty())
{
return FText::FromString(FString::Printf(TEXT("%s: %s"), *Choice->Source, *Choice->RefText));
}
}
return StatusText;
}
void FQwenTTSPresetDetailsCustomization::HandleSelectionChanged(TSharedPtr<FQwenTTSPresetChoice> Choice, ESelectInfo::Type SelectInfo)
{
if (!Choice.IsValid() || SelectInfo == ESelectInfo::Direct)
{
return;
}
if (PresetIdHandle.IsValid())
{
PresetIdHandle->SetValue(Choice->Id);
StatusText = FText::Format(LOCTEXT("PresetSelected", "Selected {0}."), Choice->GetDisplayText());
UE_LOG(LogQwenTTSStreaming, Log, TEXT("Selected Qwen TTS preset. Label=%s Id=%s"), *Choice->Label, *Choice->Id);
}
}
TSharedRef<SWidget> FQwenTTSPresetDetailsCustomization::MakePresetComboWidget(TSharedPtr<FQwenTTSPresetChoice> Choice) const
{
return SNew(STextBlock)
.Text(Choice.IsValid() ? Choice->GetDisplayText() : LOCTEXT("InvalidPresetChoice", "Invalid preset"))
.ToolTipText(Choice.IsValid() ? FText::FromString(Choice->RefText) : FText::GetEmpty());
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,47 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "IDetailCustomization.h"
#include "Interfaces/IHttpRequest.h"
#include "Widgets/Input/SComboBox.h"
class IPropertyHandle;
class UQwenTTSPresetDataAsset;
struct FQwenTTSPresetChoice
{
FString Id;
FString Label;
FString RefText;
FString Source;
FText GetDisplayText() const;
};
class FQwenTTSPresetDetailsCustomization : public IDetailCustomization
{
public:
static TSharedRef<IDetailCustomization> MakeInstance();
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
private:
TWeakObjectPtr<UQwenTTSPresetDataAsset> PresetAsset;
TSharedPtr<IPropertyHandle> PresetIdHandle;
TSharedPtr<IPropertyHandle> SettingsHandle;
TArray<TSharedPtr<FQwenTTSPresetChoice>> PresetChoices;
TSharedPtr<SComboBox<TSharedPtr<FQwenTTSPresetChoice>>> PresetComboBox;
FText StatusText = FText::FromString(TEXT("Not loaded"));
FReply RefreshPresets();
void HandlePresetResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
void SetPresetChoicesFromStatusJson(const FString& ResponseText);
FString GetStatusUrl() const;
TSharedPtr<FQwenTTSPresetChoice> GetSelectedChoice() const;
FText GetSelectedPresetText() const;
FText GetStatusText() const;
void HandleSelectionChanged(TSharedPtr<FQwenTTSPresetChoice> Choice, ESelectInfo::Type SelectInfo);
TSharedRef<SWidget> MakePresetComboWidget(TSharedPtr<FQwenTTSPresetChoice> Choice) const;
};
@@ -0,0 +1,114 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions_Base.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Modules/ModuleManager.h"
#include "PropertyEditorModule.h"
#include "QwenTTSPresetDataAsset.h"
#include "QwenTTSPresetDetailsCustomization.h"
#include "QwenTTSStreamingSettingsDataAsset.h"
#define LOCTEXT_NAMESPACE "FQwenTTSStreamingEditorModule"
namespace
{
class FQwenTTSSettingsAssetTypeActions : public FAssetTypeActions_Base
{
public:
explicit FQwenTTSSettingsAssetTypeActions(EAssetTypeCategories::Type InCategory)
: Category(InCategory)
{
}
virtual FText GetName() const override { return LOCTEXT("QwenTTSSettingsName", "Qwen TTS Streaming Settings"); }
virtual FColor GetTypeColor() const override { return FColor(86, 182, 194); }
virtual UClass* GetSupportedClass() const override { return UQwenTTSStreamingSettingsDataAsset::StaticClass(); }
virtual uint32 GetCategories() override { return Category; }
virtual FText GetAssetDescription(const FAssetData& AssetData) const override
{
return LOCTEXT("QwenTTSSettingsDescription", "Gateway and playback settings for Qwen TTS streaming.");
}
private:
EAssetTypeCategories::Type Category;
};
class FQwenTTSPresetAssetTypeActions : public FAssetTypeActions_Base
{
public:
explicit FQwenTTSPresetAssetTypeActions(EAssetTypeCategories::Type InCategory)
: Category(InCategory)
{
}
virtual FText GetName() const override { return LOCTEXT("QwenTTSPresetName", "Qwen TTS Preset"); }
virtual FColor GetTypeColor() const override { return FColor(120, 176, 96); }
virtual UClass* GetSupportedClass() const override { return UQwenTTSPresetDataAsset::StaticClass(); }
virtual uint32 GetCategories() override { return Category; }
virtual FText GetAssetDescription(const FAssetData& AssetData) const override
{
return LOCTEXT("QwenTTSPresetDescription", "Voice preset and generation parameters for Qwen TTS streaming.");
}
private:
EAssetTypeCategories::Type Category;
};
}
class FQwenTTSStreamingEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
AudioCategory = AssetTools.FindAdvancedAssetCategory(FName("Audio"));
if (AudioCategory == EAssetTypeCategories::None)
{
AudioCategory = AssetTools.RegisterAdvancedAssetCategory(FName("Audio"), LOCTEXT("AudioCategory", "Audio"));
}
RegisterAssetTypeAction(AssetTools, MakeShared<FQwenTTSSettingsAssetTypeActions>(AudioCategory));
RegisterAssetTypeAction(AssetTools, MakeShared<FQwenTTSPresetAssetTypeActions>(AudioCategory));
FPropertyEditorModule& PropertyEditor = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditor.RegisterCustomClassLayout(
UQwenTTSPresetDataAsset::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(&FQwenTTSPresetDetailsCustomization::MakeInstance));
PropertyEditor.NotifyCustomizationModuleChanged();
}
virtual void ShutdownModule() override
{
if (FModuleManager::Get().IsModuleLoaded("AssetTools"))
{
IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
for (const TSharedRef<IAssetTypeActions>& Action : RegisteredAssetTypeActions)
{
AssetTools.UnregisterAssetTypeActions(Action);
}
}
RegisteredAssetTypeActions.Empty();
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyEditor = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditor.UnregisterCustomClassLayout(UQwenTTSPresetDataAsset::StaticClass()->GetFName());
PropertyEditor.NotifyCustomizationModuleChanged();
}
}
private:
void RegisterAssetTypeAction(IAssetTools& AssetTools, TSharedRef<IAssetTypeActions> Action)
{
AssetTools.RegisterAssetTypeActions(Action);
RegisteredAssetTypeActions.Add(Action);
}
EAssetTypeCategories::Type AudioCategory = EAssetTypeCategories::Sounds;
TArray<TSharedRef<IAssetTypeActions>> RegisteredAssetTypeActions;
};
IMPLEMENT_MODULE(FQwenTTSStreamingEditorModule, QwenTTSStreamingEditor)
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,31 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "QwenTTSDataAssetFactories.generated.h"
UCLASS()
class UQwenTTSStreamingSettingsDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UQwenTTSStreamingSettingsDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override { return true; }
};
UCLASS()
class UQwenTTSPresetDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UQwenTTSPresetDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual bool ShouldShowInNewMenu() const override { return true; }
};
@@ -0,0 +1,29 @@
using UnrealBuildTool;
public class QwenTTSStreamingEditor : ModuleRules
{
public QwenTTSStreamingEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"QwenTTSStreaming"
});
PrivateDependencyModuleNames.AddRange(new string[]
{
"AssetTools",
"HTTP",
"InputCore",
"Json",
"PropertyEditor",
"Slate",
"SlateCore",
"UnrealEd"
});
}
}