// 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 FQwenTTSPresetDetailsCustomization::MakeInstance() { return MakeShared(); } void FQwenTTSPresetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { TArray> Objects; DetailBuilder.GetObjectsBeingCustomized(Objects); if (Objects.Num() > 0) { PresetAsset = Cast(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>) .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 Request = FHttpModule::Get().CreateRequest(); Request->SetURL(StatusUrl); Request->SetVerb(TEXT("GET")); Request->SetHeader(TEXT("Accept"), TEXT("application/json")); Request->OnProcessRequestComplete().BindSP(StaticCastSharedRef(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 JsonObject; const TSharedRef> 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>* 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& Value : *PresetRefs) { const TSharedPtr PresetObject = Value.IsValid() ? Value->AsObject() : nullptr; if (!PresetObject.IsValid()) { continue; } FString Id; if (!PresetObject->TryGetStringField(TEXT("id"), Id) || Id.IsEmpty()) { continue; } TSharedRef Choice = MakeShared(); 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(SettingsObject)) { return Settings->GetResolvedGatewayStatusUrl(); } } if (const UQwenTTSPresetDataAsset* Preset = PresetAsset.Get()) { if (Preset->Settings) { return Preset->Settings->GetResolvedGatewayStatusUrl(); } } return GetDefault()->GetResolvedGatewayStatusUrl(); } TSharedPtr FQwenTTSPresetDetailsCustomization::GetSelectedChoice() const { FString CurrentPresetId; if (PresetIdHandle.IsValid()) { PresetIdHandle->GetValue(CurrentPresetId); } for (const TSharedPtr& Choice : PresetChoices) { if (Choice.IsValid() && Choice->Id == CurrentPresetId) { return Choice; } } return nullptr; } FText FQwenTTSPresetDetailsCustomization::GetSelectedPresetText() const { if (const TSharedPtr 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 Choice = GetSelectedChoice()) { if (!Choice->RefText.IsEmpty()) { return FText::FromString(FString::Printf(TEXT("%s: %s"), *Choice->Source, *Choice->RefText)); } } return StatusText; } void FQwenTTSPresetDetailsCustomization::HandleSelectionChanged(TSharedPtr 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 FQwenTTSPresetDetailsCustomization::MakePresetComboWidget(TSharedPtr 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