Browse Source

Matched with Base, Simplified Settings

master
Tillman Staffen 3 weeks ago
parent
commit
c8e198e2ff
  1. BIN
      Unreal/Content/Project/AnimationTesting/BP_AnimationTesting_Manager.uasset
  2. BIN
      Unreal/Content/Project/BP/BP_Project_Manager.uasset
  3. BIN
      Unreal/Content/Project/BP/EnumsAndStructs/S_ConfigSettings.uasset
  4. BIN
      Unreal/Content/Project/BP/EnumsAndStructs/S_DEMO_Settings.uasset
  5. BIN
      Unreal/Content/Project/BP/EnumsAndStructs/S_ProjectBase_Settings.uasset
  6. BIN
      Unreal/Content/Project/Maps/M_Startup.umap
  7. BIN
      Unreal/Content/Project/Widgets/W_DialogueBox.uasset
  8. BIN
      Unreal/Content/Project/Widgets/W_Main.uasset
  9. BIN
      Unreal/Content/SPIE/BP/S_SPIE_ConfigSettings.uasset
  10. 326
      Unreal/Content/Schema/Spie_Config.schema.json
  11. 8
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/.claude/settings.local.json
  12. 32
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/AIBaseManager.cpp
  13. 10
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/MCP/MCPUnrealCommand.cpp
  14. 10
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/OpenRouter/AvatarCoreAIOpenRouter.cpp
  15. 4
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/RealtimeAPI/AvatarCoreAIRealtime.cpp
  16. 324
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/Tools/OpenRouterResponder.cpp
  17. 2
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/AIBaseConfig.h
  18. 5
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/AIBaseManager.h
  19. 12
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/MCP/MCPUnrealCommand.h
  20. 136
      Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/Tools/OpenRouterResponder.h
  21. BIN
      Unreal/Plugins/AvatarCore_Manager/Content/AvatarCoreManager.uasset
  22. BIN
      Unreal/Plugins/AvatarCore_Manager/Content/S_AvatarCoreSettings.uasset
  23. BIN
      Unreal/Plugins/AvatarCore_Manager/Content/StateManagement/BP_StateManager.uasset
  24. BIN
      Unreal/Plugins/AvatarCore_Manager/Content/StateManagement/States/BP_QnA_State.uasset
  25. BIN
      Unreal/Plugins/AvatarCore_Manager/Content/UnrealCommands/AICommand_CurrentLocation.uasset
  26. BIN
      Unreal/Plugins/AvatarCore_Manager/Content/UnrealCommands/OpenAI_Websearch_Command.uasset
  27. BIN
      Unreal/Plugins/AvatarCore_Manager/Content/UnrealCommands/WebResearch_Command.uasset
  28. BIN
      Unreal/Plugins/AvatarCore_MetaHuman/Content/Animation/AnimationConfigs/AnimationConfig_Base.uasset
  29. BIN
      Unreal/Plugins/AvatarCore_MetaHuman/Content/BP/MetaHuman/BaseAvatar.uasset
  30. 21
      Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Private/Cartesia/CartesiaTTSManager.cpp
  31. 18
      Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Private/TTSManagerBase.cpp
  32. 4
      Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Public/Cartesia/TTSCartesiaConfig.h
  33. 19
      Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Public/TTSBaseConfig.h

BIN
Unreal/Content/Project/AnimationTesting/BP_AnimationTesting_Manager.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/Project/BP/BP_Project_Manager.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/Project/BP/EnumsAndStructs/S_ConfigSettings.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/Project/BP/EnumsAndStructs/S_DEMO_Settings.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/Project/BP/EnumsAndStructs/S_ProjectBase_Settings.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/Project/Maps/M_Startup.umap (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/Project/Widgets/W_DialogueBox.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/Project/Widgets/W_Main.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Content/SPIE/BP/S_SPIE_ConfigSettings.uasset (Stored with Git LFS)

Binary file not shown.

326
Unreal/Content/Schema/Spie_Config.schema.json

@ -1,10 +1,7 @@
{
"Categories": [
"SPIE",
"SPIESettings",
"Project Setup",
"Debug",
"Volume",
"Engine Settings",
"Avatar Core",
"STT Settings",
"STT",
@ -12,39 +9,12 @@
"TTS"
],
"Variables": [
{
"InitialMode":
{
"type": "integer",
"tooltip": "Which mode to load at startup",
"default": 0,
"category": "SPIE"
}
},
{
"UseAvatarWithSafetyVest":
{
"type": "boolean",
"default": true,
"category": "SPIE"
}
},
{
"AvatarInstance":
{
"type": "string",
"tooltip": "Unique Name of this Avatar Application",
"default": "SPIE",
"category": "SPIE"
}
},
{
"UseLogging":
{
"type": "boolean",
"tooltip": "Do you want to log all interaction?",
"default": true,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -54,16 +24,7 @@
"tooltip": "Showing in background logo",
"default": "One SPIE. \r\nJust ask me.",
"hotreload": true,
"category": "SPIE"
}
},
{
"ButtonHintSpeech":
{
"type": "string",
"tooltip": "What to say, when user is allowed to speak?",
"default": "Drücke und halte beim Sprechen den Knopf vor dir, damit ich dich hören kann.",
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -72,7 +33,7 @@
"type": "string",
"tooltip": "What to say, when the Transcription was empty",
"default": "Ich kann dich nicht hören: Drücke und halte den Knopf vor dir, um zu sprechen!",
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -81,7 +42,7 @@
"type": "string",
"tooltip": "What to say if the microphone does not seem to work",
"default": "Das Mikrofon scheint nicht zu funktionieren - am besten suchst du dir Hilfe!",
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -90,7 +51,7 @@
"type": "string",
"tooltip": "Intro speech for the Innovation Day mode",
"default": "Hallo und willkommen auf der One SPIE ! Ich bin ein virtueller Avatar mit dem du dich über unsere Hausmesse unterhalten kannst. By the way, you can talk in any language with me!",
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -98,7 +59,7 @@
{
"type": "float",
"default": 20,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -106,7 +67,7 @@
{
"type": "float",
"default": 1,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -115,7 +76,7 @@
"type": "float",
"default": 1,
"hotreload": true,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -129,7 +90,7 @@
"Z": 0
},
"hotreload": true,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -137,7 +98,7 @@
{
"type": "float",
"default": 0.02,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -145,7 +106,7 @@
{
"type": "float",
"default": 10,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
@ -153,180 +114,155 @@
{
"type": "boolean",
"default": false,
"category": "SPIE"
"category": "SPIESettings"
}
},
{
"BaseProjectSettings":
{
"type": "struct",
"fields":
{
"AvatarInstance":
{
"type": "string",
"tooltip": "Name of this Instance of the avatar application. Can be used for separation of logs or future purposes"
},
"InitialMode":
{
"type": "integer",
"tooltip": "Which mode to load on startup"
},
"UseLogging":
{
"type": "boolean"
},
"ButtonHintSpeech":
{
"type": "string",
"tooltip": "Should the Avatar say something when QnA Mode starts"
},
"HideUI":
{
"type": "boolean",
"tooltip": "Hides the UI",
"default": false,
"hotreload": true,
"category": "Project Setup"
}
"tooltip": "Hides the UI"
},
{
"HideDialogueBoxAtStart":
{
"type": "boolean",
"tooltip": "If activated, the DialogueBox will hide after the first Button press to initialize the conversation. Can be show again by pressing \"H\"",
"default": false,
"hotreload": true,
"category": "Project Setup"
}
"tooltip": "If activated, the DialogueBox will hide after the first Button press to initialize the conversation. Can be show again by pressing \"H\""
},
{
"ConstrainAspectRatio":
{
"type": "boolean",
"tooltip": "If the camera should contrain to a vertical aspect ration. Can be used to enable a horizontal screen",
"default": false,
"hotreload": true,
"category": "Project Setup"
}
"tooltip": "If the camera should contrain to a vertical aspect ration. Can be used to enable a horizontal screen"
},
"AvatarVolume":
{
"type": "float"
},
"VFXVolume":
{
"type": "float",
"tooltip": "Sound volume in general."
},
"LoadAnimationTestmapOnStart":
{
"type": "boolean"
},
"ConsoleCommands":
{
"type": "array",
"tooltip": "Console commands to always run",
"itemsType": "string"
},
"CurrentLocation":
{
"type": "string",
"tooltip": "Where is the user currently located?",
"default": "EUREF-Campus, 40472 Düsseldorf, Germany",
"tooltip": "Where is the user located? To help the AI with its answers."
},
"AvatarResetTimerAnimation":
{
"type": "float",
"tooltip": "For how many seconds of the end of the reset timer length should we show the circle animation?"
},
"AvatarResetTimerLength":
{
"type": "float",
"tooltip": "How long to wait for a reset after avatar stopped talking. A value of 0 deactivates the timer"
},
"UIReactionDistancePercentages":
{
"type": "array",
"tooltip": "3 Values From near to far: distances which indicate the alpha percentage of vector from player to current camera for UI reactions like clicking on buttons",
"itemsType": "float"
}
},
"default":
{
"AvatarInstance": "Default Avatar",
"InitialMode": 0,
"UseLogging": true,
"ButtonHintSpeech": "Du kannst den Button drücken und halten, um mit mir zu sprechen.",
"HideUI": false,
"HideDialogueBoxAtStart": false,
"ConstrainAspectRatio": false,
"AvatarVolume": 1,
"VFXVolume": 1,
"LoadAnimationTestmapOnStart": false,
"ConsoleCommands": [],
"CurrentLocation": "",
"AvatarResetTimerAnimation": 15,
"AvatarResetTimerLength": 60,
"UIReactionDistancePercentages": [ 0.25, 0.5, 0.75 ]
},
"category": "Project Setup"
}
},
{
"DebugAI":
"AvatarCoreSettings":
{
"type": "struct",
"fields":
{
"DebugSTT":
{
"type": "enum",
"tooltip": "Debugging mode for the AI Module",
"tooltip": "Deactivate or Debug STT Module",
"enum": [
"Normal",
"DebugModule",
"DebugNoModule"
],
"enumTypeName": "EAvatarCoreDebugModules",
"default": "Normal",
"category": "Debug"
}
"enumTypeName": "EAvatarCoreDebugModules"
},
{
"DebugTTS":
"DebugAI":
{
"type": "enum",
"tooltip": "Debugging mode for the TTS Module",
"tooltip": "Deactivate or Debug AI Module",
"enum": [
"Normal",
"DebugModule",
"DebugNoModule"
],
"enumTypeName": "EAvatarCoreDebugModules",
"default": "Normal",
"category": "Debug"
}
"enumTypeName": "EAvatarCoreDebugModules"
},
{
"DebugSTT":
"DebugTTS":
{
"type": "enum",
"tooltip": "Debugging mode for the STT Module",
"tooltip": "Deactivate or Debug TTS Module",
"enum": [
"Normal",
"DebugModule",
"DebugNoModule"
],
"enumTypeName": "EAvatarCoreDebugModules",
"default": "Normal",
"category": "Debug"
}
},
{
"DebugAvatar":
{
"type": "boolean",
"tooltip": "Activated the debugging Mode for the Avatar",
"default": false,
"category": "Debug"
}
},
{
"AvatarVolume":
{
"type": "float",
"tooltip": "Volume from 0-1 for the Avatar (Doesnt work with A2F)",
"default": 1,
"category": "Volume"
}
},
{
"VFXVolume":
{
"type": "float",
"tooltip": "Volume from 0-1 for the VFX Sounds",
"default": 0.29999999999999999,
"hotreload": true,
"category": "Volume"
}
},
{
"ConsoleCommands":
{
"type": "array",
"tooltip": "The one place to configure Console Commands",
"itemsType": "string",
"default": [
"r.ScreenPercentage 100",
"t.MaxFPS 60",
"r.VSync 1",
"r.AntiAliasingMethod 2",
"r.RayTracing.Reflections.SamplesPerPixel 1",
"r.HairStrands.ComposeAfterTranslucency 0",
"r.HairStrands.DOFDepth 0",
"r.Lumen.ScreenProbeGather.DownsampleFactor 32",
"r.Lumen.Reflections.RadianceCache 1",
"r.Lumen.Reflections.MaxRoughnessToTraceClamp 0.3",
"r.Lumen.Reflections.AsyncCompute 1",
"r.RayTracing.Shadows 0",
"r.TemporalAASamples 16",
"r.RayTracing.Shadows.SamplesPerPixel 2"
],
"hotreload": true,
"category": "Engine Settings"
}
"enumTypeName": "EAvatarCoreDebugModules"
},
{
"LookAtEnabled":
{
"type": "boolean",
"tooltip": "Activated LookAt in Avatar AnimationSystem",
"default": true,
"category": "Avatar Core"
}
},
{
"LookAtLocation":
{
"type": "vector3",
"tooltip": "Location for the LookAt",
"default":
{
"X": 0,
"Y": 560,
"Z": 110
},
"category": "Avatar Core"
}
},
{
"UseMCPServer":
{
"type": "boolean",
"tooltip": "Active MCP Server",
"default": false,
"category": "Avatar Core"
}
"tooltip": "IsLookAtEnabled"
},
{
"LipSyncModel":
{
"type": "enum",
@ -335,38 +271,22 @@
"Semi-Optimized (Balanced)",
"Highly Optimized (Fastest)"
],
"enumTypeName": "ERealisticMetaHumanLipSyncModelType",
"default": "Original (Highest Quality)",
"category": "Avatar Core"
}
"enumTypeName": "ERealisticMetaHumanLipSyncModelType"
},
"DebugAvatar":
{
"AvatarResetTimerLength":
{
"type": "float",
"tooltip": "How long to wait for a reset after avatar stopped talking. A value of 0 deactivates the timer",
"default": 60,
"hotreload": true,
"category": "Avatar Core"
"type": "boolean"
}
},
"default":
{
"AvatarResetTimerAnimation":
{
"type": "float",
"tooltip": "For how many seconds of the end of the reset timer length should we show the circle animation?",
"default": 15,
"hotreload": true,
"category": "Avatar Core"
}
"DebugSTT": "Normal",
"DebugAI": "Normal",
"DebugTTS": "Normal",
"LookAtEnabled": true,
"LipSyncModel": "Original (Highest Quality)",
"DebugAvatar": false
},
{
"TalkByHoldTimerLength":
{
"type": "float",
"tooltip": "In seconds: If jumping to TalkToAvatar by pressing the button, how long to wait to skip the disclaimer if still holding the button?",
"default": 2,
"hotreload": true,
"category": "Avatar Core"
}
},
@ -967,8 +887,9 @@
},
"WordReplacements":
{
"type": "string",
"tooltip": "A map of words to replace in the text to fix pronauncation"
"type": "array",
"tooltip": "A map of words to replace in the text to fix pronauncation use separator \"|\". Left of it = word to preplace; right of it word to replace with.",
"itemsType": "string"
},
"AudioNumChannels":
{
@ -1076,6 +997,7 @@
"default":
{
"UseCacheSystem": true,
"WordReplacements": [],
"AudioNumChannels": 1,
"AudioSampleRate": 22050,
"ResampleToSampleRate": -1,

8
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/.claude/settings.local.json

@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"WebFetch(domain:openrouter.ai)",
"WebSearch"
]
}
}

32
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/AIBaseManager.cpp

@ -167,8 +167,18 @@ void UAIBaseManager::SendResponse(FAIMessage Message, bool NotifyDelay)
if (NotifyDelay)
UAIBaseManager::StartDelayedAnswerTimer();
SendResponseChild(Message, NotifyDelay);
if(Message.Role == EAvatarCoreAIPromptRole::User || Message.Role == EAvatarCoreAIPromptRole::Tool)
if (Message.Role == EAvatarCoreAIPromptRole::User)
AddMessageToArray(Message);
else if (Message.Role == EAvatarCoreAIPromptRole::Tool)
{
// ConfirmCommand may have already stored a placeholder with this Id.
// If so, CommandFinished updated it in-place — don't add a duplicate.
const bool bAlreadyStored = PreviousMessages.ContainsByPredicate([&](const FAIMessage& M) {
return M.Role == EAvatarCoreAIPromptRole::Tool && M.Id == Message.Id;
});
if (!bAlreadyStored)
AddMessageToArray(Message);
}
}
void UAIBaseManager::RepeatText(FString TextToRepeat, bool DoRephrase)
@ -321,6 +331,7 @@ void UAIBaseManager::RunMCPCommand(FString CommandName, FString Payload, FString
Cmd->SetWorldContext(WorldReferenceActor.Get());
ActiveCommands.Add(Cmd);
Cmd->OnCommandConfirmed.AddDynamic(this, &UAIBaseManager::CommandConfirmed);
Cmd->OnCommandDone.AddDynamic(this, &UAIBaseManager::CommandFinished);
Cmd->OnCommandFailed.AddDynamic(this, &UAIBaseManager::CommandFailed);
@ -363,6 +374,14 @@ FString UAIBaseManager::GetRoleAsString(EAvatarCoreAIPromptRole Role)
}
}
void UAIBaseManager::CommandConfirmed(const FAIMessage& Message)
{
// Store the placeholder immediately so orphan detection doesn't strip the
// assistant tool_calls entry while the long-running task is in progress.
AddMessageToArray(Message);
BroadcastAILog(FString::Printf(TEXT("Command confirmed with placeholder: %s"), *Message.Message), true);
}
void UAIBaseManager::CommandFinished(const FAIMessage& Message)
{
ActiveCommands.RemoveAll([&Message](UMCPUnrealCommand* Cmd)
@ -377,6 +396,17 @@ void UAIBaseManager::CommandFinished(const FAIMessage& Message)
else
BroadcastAILog(TEXT("Command ran successfully."), true);
// If ConfirmCommand was called, a placeholder is already in history with this Id.
// Update it in-place so the AI receives the real result, not the interim message.
for (FAIMessage& Prev : PreviousMessages)
{
if (Prev.Role == EAvatarCoreAIPromptRole::Tool && Prev.Id == Message.Id)
{
Prev.Message = Message.Message;
break;
}
}
SendResponse(Message, false);
}

10
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/MCP/MCPUnrealCommand.cpp

@ -52,6 +52,16 @@ void UMCPUnrealCommand::StartTimeout()
}
}
void UMCPUnrealCommand::ConfirmCommand(const FString& InProgressMessage)
{
FAIMessage Msg;
Msg.Role = EAvatarCoreAIPromptRole::Tool;
Msg.Message = InProgressMessage;
Msg.Id = Id;
Msg.bTriggerResponse = false;
OnCommandConfirmed.Broadcast(Msg);
}
void UMCPUnrealCommand::FinishCommand(const FString& Payload, bool bTriggerResponse)
{
FAIMessage Msg;

10
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/OpenRouter/AvatarCoreAIOpenRouter.cpp

@ -282,7 +282,15 @@ TArray<TSharedPtr<FJsonValue>> UAvatarCoreAIOpenRouter::BuildMessagesArray(FAIMe
for (const FAIMessage& Msg : History)
AppendMessage(Msg);
// Current message appended last — ensures it is always the newest entry
// If ConfirmCommand placed a placeholder in history and CommandFinished updated it,
// CurrentMessage (the final result) is already present — don't append it again.
const bool bCurrentAlreadyInHistory =
CurrentMessage.Role == EAvatarCoreAIPromptRole::Tool && !CurrentMessage.Id.IsEmpty() &&
History.ContainsByPredicate([&](const FAIMessage& M) {
return M.Role == EAvatarCoreAIPromptRole::Tool && M.Id == CurrentMessage.Id;
});
if (!bCurrentAlreadyInHistory)
AppendMessage(CurrentMessage);
return Messages;

4
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/RealtimeAPI/AvatarCoreAIRealtime.cpp

@ -590,7 +590,9 @@ void UAvatarCoreAIRealtime::OnWebSocketConnectionStringReceived(const FString& M
{
CurrentRequestID.Empty(); //Response is dead now
CommandName = MessageJson->GetStringField(TEXT("name"));
RunMCPCommand(CommandName, MessageJson->GetStringField(TEXT("arguments")));
FString CallId;
MessageJson->TryGetStringField(TEXT("call_id"), CallId);
RunMCPCommand(CommandName, MessageJson->GetStringField(TEXT("arguments")), CallId);
}
else
{

324
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Private/Tools/OpenRouterResponder.cpp

@ -0,0 +1,324 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Tools/OpenRouterResponder.h"
#include "HttpModule.h"
#include "Interfaces/IHttpResponse.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
FString UOpenRouterResponder::StoredApiKey;
FString UOpenRouterResponder::StoredModel = TEXT("openai/gpt-4.1");
FString UOpenRouterResponder::StoredSiteUrl;
FString UOpenRouterResponder::StoredSiteName;
TArray<TWeakObjectPtr<UOpenRouterResponder>> UOpenRouterResponder::ActiveRequests;
void UOpenRouterResponder::InitResponder(const FString& ApiKey, const FString& Model, const FString& SiteUrl, const FString& SiteName)
{
StoredApiKey = ApiKey;
StoredModel = Model;
StoredSiteUrl = SiteUrl;
StoredSiteName = SiteName;
}
UOpenRouterResponder* UOpenRouterResponder::CallOpenRouterResponse(
UObject* WorldContextObject,
const FString& Prompt,
const TArray<FRouterServerToolConfig>& ServerTools,
const FString& Instructions,
const FString& OverrideModel,
int32 MaxOutputTokens,
ERouterReasoning Reasoning,
ERouterToolChoice ToolChoice)
{
UOpenRouterResponder* Action = NewObject<UOpenRouterResponder>();
Action->InputPrompt = Prompt;
Action->InputInstructions = Instructions;
Action->InputModel = OverrideModel.IsEmpty() ? StoredModel : OverrideModel;
Action->InputMaxOutputTokens = MaxOutputTokens;
Action->InputReasoning = Reasoning;
Action->InputServerTools = ServerTools;
Action->InputToolChoice = ToolChoice;
Action->RegisterWithGameInstance(WorldContextObject);
return Action;
}
void UOpenRouterResponder::Activate()
{
if (StoredApiKey.IsEmpty())
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: API key not set. Call InitResponder first."));
Failed.Broadcast(TEXT("API key not set. Call InitResponder first."));
return;
}
RequestStartTime = FPlatformTime::Seconds();
ActiveRequests.Add(this);
// ── Build JSON request body ────────────────────────────────────────────
TSharedRef<FJsonObject> RootObject = MakeShared<FJsonObject>();
RootObject->SetStringField(TEXT("model"), InputModel);
// messages array: optional system + user
{
TArray<TSharedPtr<FJsonValue>> MessagesArray;
if (!InputInstructions.IsEmpty())
{
TSharedRef<FJsonObject> SystemMsg = MakeShared<FJsonObject>();
SystemMsg->SetStringField(TEXT("role"), TEXT("system"));
SystemMsg->SetStringField(TEXT("content"), InputInstructions);
MessagesArray.Add(MakeShared<FJsonValueObject>(SystemMsg));
}
TSharedRef<FJsonObject> UserMsg = MakeShared<FJsonObject>();
UserMsg->SetStringField(TEXT("role"), TEXT("user"));
UserMsg->SetStringField(TEXT("content"), InputPrompt);
MessagesArray.Add(MakeShared<FJsonValueObject>(UserMsg));
RootObject->SetArrayField(TEXT("messages"), MessagesArray);
}
if (InputMaxOutputTokens > 0)
{
RootObject->SetNumberField(TEXT("max_tokens"), InputMaxOutputTokens);
}
// Reasoning
if (InputReasoning != ERouterReasoning::None)
{
FString EffortStr;
switch (InputReasoning)
{
case ERouterReasoning::Minimal: EffortStr = TEXT("minimal"); break;
case ERouterReasoning::Low: EffortStr = TEXT("low"); break;
case ERouterReasoning::Medium: EffortStr = TEXT("medium"); break;
case ERouterReasoning::High: EffortStr = TEXT("high"); break;
case ERouterReasoning::XHigh: EffortStr = TEXT("xhigh"); break;
default: break;
}
TSharedRef<FJsonObject> ReasoningObject = MakeShared<FJsonObject>();
ReasoningObject->SetStringField(TEXT("effort"), EffortStr);
ReasoningObject->SetStringField(TEXT("summary"), TEXT("auto"));
RootObject->SetObjectField(TEXT("reasoning"), ReasoningObject);
}
// Server tools array
if (InputServerTools.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> ToolsArray;
for (const FRouterServerToolConfig& ToolConfig : InputServerTools)
{
TSharedRef<FJsonObject> ToolObject = MakeShared<FJsonObject>();
switch (ToolConfig.ToolType)
{
case ERouterServerTool::WebSearch:
ToolObject->SetStringField(TEXT("type"), TEXT("openrouter:web_search"));
if (ToolConfig.MaxResults > 0)
{
TSharedRef<FJsonObject> Params = MakeShared<FJsonObject>();
Params->SetNumberField(TEXT("max_results"), ToolConfig.MaxResults);
ToolObject->SetObjectField(TEXT("parameters"), Params);
}
break;
case ERouterServerTool::DateTime:
ToolObject->SetStringField(TEXT("type"), TEXT("openrouter:datetime"));
break;
case ERouterServerTool::ImageGeneration:
ToolObject->SetStringField(TEXT("type"), TEXT("openrouter:image_generation"));
break;
case ERouterServerTool::WebFetch:
ToolObject->SetStringField(TEXT("type"), TEXT("openrouter:web_fetch"));
break;
}
ToolsArray.Add(MakeShared<FJsonValueObject>(ToolObject));
}
RootObject->SetArrayField(TEXT("tools"), ToolsArray);
// Only send tool_choice when the caller specified Auto or Required
if (InputToolChoice == ERouterToolChoice::Auto)
{
RootObject->SetStringField(TEXT("tool_choice"), TEXT("auto"));
}
else if (InputToolChoice == ERouterToolChoice::Required)
{
RootObject->SetStringField(TEXT("tool_choice"), TEXT("required"));
}
}
// Serialize to string
FString Body;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&Body);
FJsonSerializer::Serialize(RootObject, Writer);
// ── Fire HTTP request ──────────────────────────────────────────────────
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HttpRequest = FHttpModule::Get().CreateRequest();
HttpRequest->SetURL(TEXT("https://openrouter.ai/api/v1/chat/completions"));
HttpRequest->SetVerb(TEXT("POST"));
HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
HttpRequest->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("Bearer %s"), *StoredApiKey));
if (!StoredSiteUrl.IsEmpty())
{
HttpRequest->SetHeader(TEXT("HTTP-Referer"), StoredSiteUrl);
}
if (!StoredSiteName.IsEmpty())
{
HttpRequest->SetHeader(TEXT("X-Title"), StoredSiteName);
}
HttpRequest->SetContentAsString(Body);
HttpRequest->OnProcessRequestComplete().BindUObject(this, &UOpenRouterResponder::HandleHttpResponse);
if (!HttpRequest->ProcessRequest())
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: Failed to start HTTP request."));
Failed.Broadcast(TEXT("Failed to start HTTP request."));
}
}
void UOpenRouterResponder::ClearAllResponses()
{
TArray<TWeakObjectPtr<UOpenRouterResponder>> RequestsCopy = ActiveRequests;
ActiveRequests.Empty();
for (const TWeakObjectPtr<UOpenRouterResponder>& Weak : RequestsCopy)
{
if (UOpenRouterResponder* Action = Weak.Get())
{
Action->SetReadyToDestroy();
}
}
}
void UOpenRouterResponder::HandleHttpResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
ActiveRequests.RemoveAll([this](const TWeakObjectPtr<UOpenRouterResponder>& Weak)
{
return !Weak.IsValid() || Weak.Get() == this;
});
const float ElapsedTime = static_cast<float>(FPlatformTime::Seconds() - RequestStartTime);
if (!bWasSuccessful || !Response.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: HTTP request failed."));
Failed.Broadcast(TEXT("HTTP request failed."));
return;
}
const int32 Code = Response->GetResponseCode();
const FString Body = Response->GetContentAsString();
if (Code < 200 || Code >= 300)
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: HTTP %d — %s"), Code, *Body);
Failed.Broadcast(FString::Printf(TEXT("HTTP %d — %s"), Code, *Body));
return;
}
// Parse root JSON
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Body);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: Failed to parse response JSON."));
Failed.Broadcast(TEXT("Failed to parse response JSON."));
return;
}
// Check for API-level error
if (RootObject->HasField(TEXT("error")))
{
const TSharedPtr<FJsonObject>* ErrorObject;
if (RootObject->TryGetObjectField(TEXT("error"), ErrorObject))
{
FString ErrorMsg = (*ErrorObject)->GetStringField(TEXT("message"));
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: API error — %s"), *ErrorMsg);
Failed.Broadcast(ErrorMsg);
return;
}
}
// Navigate choices[0].message
const TArray<TSharedPtr<FJsonValue>>* ChoicesArray;
if (!RootObject->TryGetArrayField(TEXT("choices"), ChoicesArray) || ChoicesArray->Num() == 0)
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: No choices in response."));
Failed.Broadcast(TEXT("No choices in response."));
return;
}
const TSharedPtr<FJsonObject>& FirstChoice = (*ChoicesArray)[0]->AsObject();
if (!FirstChoice.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: Invalid choice object."));
Failed.Broadcast(TEXT("Invalid choice object."));
return;
}
const TSharedPtr<FJsonObject>* MessageObjectPtr;
if (!FirstChoice->TryGetObjectField(TEXT("message"), MessageObjectPtr) || !(*MessageObjectPtr).IsValid())
{
UE_LOG(LogTemp, Error, TEXT("OpenRouterResponder: No message in choice."));
Failed.Broadcast(TEXT("No message in choice."));
return;
}
const TSharedPtr<FJsonObject>& MessageObject = *MessageObjectPtr;
FRouterResult Result;
Result.ExecutionTime = ElapsedTime;
// content can be a plain string or, for multimodal models, an array — handle both
{
FString ContentStr;
if (MessageObject->TryGetStringField(TEXT("content"), ContentStr))
{
Result.Content = ContentStr;
}
else
{
const TArray<TSharedPtr<FJsonValue>>* ContentArray;
if (MessageObject->TryGetArrayField(TEXT("content"), ContentArray))
{
for (const TSharedPtr<FJsonValue>& Item : *ContentArray)
{
const TSharedPtr<FJsonObject>& ItemObj = Item->AsObject();
if (!ItemObj.IsValid()) continue;
FString ItemType;
FString ItemText;
if (ItemObj->TryGetStringField(TEXT("type"), ItemType) && ItemType == TEXT("text")
&& ItemObj->TryGetStringField(TEXT("text"), ItemText))
{
Result.Content += ItemText;
}
}
}
}
}
// reasoning is optional — present only when the model emits chain-of-thought
MessageObject->TryGetStringField(TEXT("reasoning"), Result.Reasoning);
if (!Result.Content.IsEmpty() || !Result.Reasoning.IsEmpty())
{
Success.Broadcast(Result);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("OpenRouterResponder: Response contained no content or reasoning."));
Failed.Broadcast(TEXT("Response contained no content or reasoning."));
}
}

2
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/AIBaseConfig.h

@ -35,7 +35,7 @@ struct FGlobalAISettings
// Boot up a FastMCP Server on Startup (On close keep open in Editor Mode, kill in shipping)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreAI|MCP", meta = (ExposeOnSpawn = "true"))
bool bUseMCPServer = true;
bool bUseMCPServer = false;
// Does the AI model generate Audio Chunks that can be forwarded to the TTS Manager?
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreAI|Settings", meta = (ExposeOnSpawn = "true"))

5
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/AIBaseManager.h

@ -227,6 +227,11 @@ public:
protected:
/** Bound to UMCPUnrealCommand::OnCommandConfirmed — stores a placeholder tool result
* immediately so the assistant tool_calls entry is never seen as orphaned during long tasks. */
UFUNCTION()
void CommandConfirmed(const FAIMessage& Message);
/** Bound to UMCPUnrealCommand::OnCommandDone — message already has Role=Tool and Id set. */
UFUNCTION()
void CommandFinished(const FAIMessage& Message);

12
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/MCP/MCPUnrealCommand.h

@ -7,6 +7,7 @@
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAICommandDone, const FAIMessage&, Message);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAICommandFailed, const FAIMessage&, Message);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAICommandConfirmed, const FAIMessage&, Message);
/**
* Base class for MCP/AI commands.
@ -44,6 +45,12 @@ public:
UPROPERTY(BlueprintAssignable, Category = "Command")
FOnAICommandFailed OnCommandFailed;
// Intermediate confirmation — call this at the start of a long-running task to
// place a placeholder tool result in history immediately. FinishCommand/FailCommand
// will update the placeholder with the real result when the task completes.
UPROPERTY(BlueprintAssignable, Category = "Command")
FOnAICommandConfirmed OnCommandConfirmed;
/**
* Execute the command using the provided world reference and a JSON payload from the AI model.
* All world/actor operations should use this reference. The payload contains arguments or context as JSON.
@ -74,6 +81,11 @@ public:
UFUNCTION(BlueprintCallable, Category = "Command")
FString GetCommandOutputScheme();
/** Call at the start of a long-running task to place a placeholder in message history.
* FinishCommand/FailCommand will replace it with the real result when done. */
UFUNCTION(BlueprintCallable, Category = "Command")
void ConfirmCommand(const FString& InProgressMessage);
/** Call this when the command is finished successfully. Set bTriggerResponse=false to suppress the AI follow-up. */
UFUNCTION(BlueprintCallable, Category = "Command")
void FinishCommand(const FString& Payload, bool bTriggerResponse = true);

136
Unreal/Plugins/AvatarCore_AI/Source/AvatarCore_AI/Public/Tools/OpenRouterResponder.h

@ -0,0 +1,136 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintAsyncActionBase.h"
#include "Http.h"
#include "OpenRouterResponder.generated.h"
// ── Enums ──────────────────────────────────────────────────────────────────
UENUM(BlueprintType)
enum class ERouterToolChoice : uint8
{
None UMETA(DisplayName = "None (omit)"),
Auto UMETA(DisplayName = "Auto"),
Required UMETA(DisplayName = "Required")
};
UENUM(BlueprintType)
enum class ERouterReasoning : uint8
{
None UMETA(DisplayName = "None"),
Minimal UMETA(DisplayName = "Minimal"),
Low UMETA(DisplayName = "Low"),
Medium UMETA(DisplayName = "Medium"),
High UMETA(DisplayName = "High"),
XHigh UMETA(DisplayName = "XHigh")
};
UENUM(BlueprintType)
enum class ERouterServerTool : uint8
{
WebSearch UMETA(DisplayName = "Web Search"),
DateTime UMETA(DisplayName = "Date/Time"),
ImageGeneration UMETA(DisplayName = "Image Generation"),
WebFetch UMETA(DisplayName = "Web Fetch")
};
// ── Structs ────────────────────────────────────────────────────────────────
USTRUCT(BlueprintType)
struct FRouterServerToolConfig
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreAI|Router")
ERouterServerTool ToolType = ERouterServerTool::WebSearch;
/** Maximum results returned. Only used when ToolType is WebSearch. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreAI|Router")
int32 MaxResults = 3;
};
USTRUCT(BlueprintType)
struct FRouterResult
{
GENERATED_BODY()
/** Main text content from choices[0].message.content */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreAI|Router")
FString Content;
/** Chain-of-thought from choices[0].message.reasoning. Empty if the model does not return it. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreAI|Router")
FString Reasoning;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreAI|Router")
float ExecutionTime = 0.f;
};
// ── Delegates ──────────────────────────────────────────────────────────────
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRouterSuccess, const FRouterResult&, Result);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRouterFailed, const FString&, ErrorMessage);
// ── Class ──────────────────────────────────────────────────────────────────
UCLASS()
class AVATARCORE_AI_API UOpenRouterResponder : public UBlueprintAsyncActionBase
{
GENERATED_BODY()
public:
/** Store API key, default model, and optional attribution headers sent to OpenRouter. */
UFUNCTION(BlueprintCallable, Category = "AvatarCoreAI|Router", meta = (DisplayName = "Init OpenRouter Responder"))
static void InitResponder(
const FString& ApiKey,
const FString& Model = TEXT("openai/gpt-4.1"),
const FString& SiteUrl = TEXT(""),
const FString& SiteName = TEXT(""));
/** Send a one-shot request to the OpenRouter Chat Completions API. */
UFUNCTION(BlueprintCallable, Category = "AvatarCoreAI|Router",
meta = (BlueprintInternalUseOnly = "true", DisplayName = "Call OpenRouter Response", WorldContext = "WorldContextObject",
AutoCreateRefTerm = "Instructions,OverrideModel,ServerTools"))
static UOpenRouterResponder* CallOpenRouterResponse(
UObject* WorldContextObject,
const FString& Prompt,
const TArray<FRouterServerToolConfig>& ServerTools,
const FString& Instructions,
const FString& OverrideModel,
int32 MaxOutputTokens = 0,
ERouterReasoning Reasoning = ERouterReasoning::None,
ERouterToolChoice ToolChoice = ERouterToolChoice::Auto);
/** Cancel all active OpenRouter requests. */
UFUNCTION(BlueprintCallable, Category = "AvatarCoreAI|Router", meta = (DisplayName = "Clear All Router Responses"))
static void ClearAllResponses();
UPROPERTY(BlueprintAssignable)
FOnRouterSuccess Success;
UPROPERTY(BlueprintAssignable)
FOnRouterFailed Failed;
virtual void Activate() override;
private:
void HandleHttpResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful);
FString InputPrompt;
FString InputInstructions;
FString InputModel;
int32 InputMaxOutputTokens = 0;
ERouterReasoning InputReasoning = ERouterReasoning::None;
TArray<FRouterServerToolConfig> InputServerTools;
ERouterToolChoice InputToolChoice = ERouterToolChoice::Auto;
double RequestStartTime = 0.0;
static FString StoredApiKey;
static FString StoredModel;
static FString StoredSiteUrl;
static FString StoredSiteName;
static TArray<TWeakObjectPtr<UOpenRouterResponder>> ActiveRequests;
};

BIN
Unreal/Plugins/AvatarCore_Manager/Content/AvatarCoreManager.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_Manager/Content/S_AvatarCoreSettings.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_Manager/Content/StateManagement/BP_StateManager.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_Manager/Content/StateManagement/States/BP_QnA_State.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_Manager/Content/UnrealCommands/AICommand_CurrentLocation.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_Manager/Content/UnrealCommands/OpenAI_Websearch_Command.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_Manager/Content/UnrealCommands/WebResearch_Command.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_MetaHuman/Content/Animation/AnimationConfigs/AnimationConfig_Base.uasset (Stored with Git LFS)

Binary file not shown.

BIN
Unreal/Plugins/AvatarCore_MetaHuman/Content/BP/MetaHuman/BaseAvatar.uasset (Stored with Git LFS)

Binary file not shown.

21
Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Private/Cartesia/CartesiaTTSManager.cpp

@ -23,25 +23,6 @@ namespace
FJsonSerializer::Serialize(Obj.ToSharedRef(), Writer);
return Out;
}
static const FString TTSLanguageStrings[] = {
TEXT("en"), TEXT("fr"), TEXT("de"), TEXT("es"), TEXT("pt"), TEXT("zh"), TEXT("ja"),
TEXT("hi"), TEXT("it"), TEXT("ko"), TEXT("nl"), TEXT("pl"), TEXT("ru"), TEXT("sv"),
TEXT("tr"), TEXT("tl"), TEXT("bg"), TEXT("ro"), TEXT("ar"), TEXT("cs"), TEXT("el"),
TEXT("fi"), TEXT("hr"), TEXT("ms"), TEXT("sk"), TEXT("da"), TEXT("ta"), TEXT("uk"),
TEXT("hu"), TEXT("no"), TEXT("vi"), TEXT("bn"), TEXT("th"), TEXT("he"), TEXT("ka"),
TEXT("id"), TEXT("te"), TEXT("gu"), TEXT("kn"), TEXT("ml"), TEXT("mr"), TEXT("pa")
};
static FString TTSLanguageToString(ETTSLanguage Language)
{
const int32 Index = static_cast<int32>(Language);
if (Index >= 0 && Index < UE_ARRAY_COUNT(TTSLanguageStrings))
{
return TTSLanguageStrings[Index];
}
return TEXT("de");
}
}
FString UCartesiaTTSManager::BuildWebSocketUrl() const
@ -315,7 +296,7 @@ void UCartesiaTTSManager::StartStreamingGeneration(int32 TaskID, const FString&
{
if (!WeakThis.IsValid()) return;
UCartesiaTTSManager* Self = WeakThis.Get();
Self->TTSLog(FString::Printf(TEXT("[Cartesia][%d] <- %s"), TaskID, *Message));
//Self->TTSLog(FString::Printf(TEXT("[Cartesia][%d] <- %s"), TaskID, *Message));
TSharedPtr<FJsonObject> Obj;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Message);

18
Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Private/TTSManagerBase.cpp

@ -205,18 +205,24 @@ FString UTTSManagerBase::ProcessTextForGeneration(const FString& Input)
// Apply word/phrase replacements from config (case-insensitive)
if (TTSConfig && TTSConfig->GlobalTTSSettings.WordReplacements.Num() > 0)
{
for (const TPair<FString, FString>& Pair : TTSConfig->GlobalTTSSettings.WordReplacements)
for (const FString& Entry : TTSConfig->GlobalTTSSettings.WordReplacements)
{
const FString& From = Pair.Key;
const FString& To = Pair.Value;
FString From;
FString To;
// Split "from|to"
if (Entry.Split(TEXT("|"), &From, &To))
{
From = From.TrimStartAndEnd();
To = To.TrimStartAndEnd();
if (!From.IsEmpty())
{
Result.ReplaceInline(*From, *To, ESearchCase::CaseSensitive);
Result.ReplaceInline(*From, *To, ESearchCase::IgnoreCase);
}
}
}
}
return Result;
}

4
Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Public/Cartesia/TTSCartesiaConfig.h

@ -18,10 +18,10 @@ struct FCartesiaTTSSettings
FString CartesiaVoiceId = TEXT("e00dd3df-19e7-4cd4-827a-7ff6687b6954");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreTTS|Cartesia", meta = (ExposeOnSpawn = "true"))
FString CartesiaModelId = TEXT("sonic-3");
FString CartesiaModelId = TEXT("sonic-3.5");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreTTS|Cartesia", meta = (ExposeOnSpawn = "true"))
FString CartesiaLanguage = TEXT("en");
FString CartesiaLanguage = TEXT("de");
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreTTS|Cartesia", meta = (ExposeOnSpawn = "true"))
bool StreamInputText = true;

19
Unreal/Plugins/AvatarCore_TTS/Source/AvatarCore_TTS/Public/TTSBaseConfig.h

@ -69,6 +69,21 @@ enum class ETTSLanguage : uint8
pa UMETA(DisplayName = "Punjabi"),
};
// Returns the ISO 639-1 code string for the given language enum (e.g. ETTSLanguage::de → "de").
// Derived from the enum value name via UE reflection, so it stays in sync automatically.
static FORCEINLINE FString TTSLanguageToString(ETTSLanguage Language)
{
FString Name = StaticEnum<ETTSLanguage>()->GetNameStringByValue(static_cast<int64>(Language));
// StaticEnum returns the short name (no "ETTSLanguage::" prefix in UE5 GetNameStringByValue)
// but strip the prefix defensively in case the runtime returns the qualified form.
int32 ScopePos;
if (Name.FindLastChar(TEXT(':'), ScopePos))
{
Name = Name.RightChop(ScopePos + 1);
}
return Name.IsEmpty() ? TEXT("en") : Name;
}
USTRUCT(BlueprintType)
struct FGlobalTTSSettings
{
@ -78,9 +93,9 @@ struct FGlobalTTSSettings
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreTTS|Base", meta = (ExposeOnSpawn = "true"))
bool UseCacheSystem = true;
//A map of words to replace in the text to fix pronauncation
//A map of words to replace in the text to fix pronauncation use separator "|". Left of it = word to preplace; right of it word to replace with.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreTTS|Base", meta = (ExposeOnSpawn = "true"))
TMap<FString, FString> WordReplacements;
TArray<FString> WordReplacements = {"b.ReX|biRäx", "AI|EyEi"};
//Number of Audio Channels - Only tested with 1 but who needs more anyway?
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AvatarCoreTTS|Base", meta = (ExposeOnSpawn = "true"))

Loading…
Cancel
Save