// Georgy Treshchev 2026 All Rights Reserved.

#include "RuntimeVoskModelLibrary.h"
#include "Misc/Paths.h"
#include "HAL/PlatformFileManager.h"
#include "HAL/FileManager.h"
#include "RuntimeSpeechRecognizerVosk.h"

#if PLATFORM_ANDROID
#include "Async/Async.h"

extern FString RuntimeVoskAndroidNoBackupFilesDir;
extern bool RuntimeVoskAndroidNoBackupFilesDirValid;
#endif

namespace RuntimeVoskModelSizes
{
	constexpr int64 MB(int64 N) { return N * 1024LL * 1024LL; }
	inline int64 GB(double N) { return static_cast<int64>(N * 1024.0 * 1024.0 * 1024.0); }
}

TArray<FVoskModelLanguageGroup> URuntimeVoskModelLibrary::GetPredefinedVoskModels()
{
	using namespace RuntimeVoskModelSizes;

	TArray<FVoskModelLanguageGroup> Groups;

	auto AddGroup = [&Groups](const TCHAR* GroupName) -> FVoskModelLanguageGroup&
	{
		FVoskModelLanguageGroup NewGroup;
		NewGroup.GroupDisplayName = GroupName;
		return Groups[Groups.Add(MoveTemp(NewGroup))];
	};

	auto AddVariant = [](FVoskModelLanguageGroup& Group, const TCHAR* ModelId, const TCHAR* Url, int64 SizeBytes, const TCHAR* Info, const TCHAR* License)
	{
		FVoskModelVariant Variant;
		Variant.ModelId = ModelId;
		Variant.DownloadURL = Url;
		Variant.ApproximateSizeBytes = SizeBytes;
		Variant.Info = Info;
		Variant.License = License;
		Group.Variants.Add(MoveTemp(Variant));
	};

	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("English"));
		AddVariant(G, TEXT("vosk-model-small-en-us-0.15"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip"), MB(40), TEXT("Lightweight wideband model for Android and RPi. WER: 9.85 (librispeech test-clean), 10.38 (tedlium)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-en-us-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-0.22.zip"), GB(1.8), TEXT("Accurate generic US English model. WER: 5.69 (librispeech test-clean), 6.05 (tedlium), 29.78 (callcenter)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-en-us-0.22-lgraph"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-0.22-lgraph.zip"), MB(128), TEXT("Big US English model with dynamic graph. WER: 7.82 (librispeech), 8.20 (tedlium)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-en-us-0.42-gigaspeech"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-0.42-gigaspeech.zip"), GB(2.3), TEXT("Accurate model trained on Gigaspeech. Mostly for podcasts, not telephony. WER: 5.64 (librispeech test-clean), 6.24 (tedlium), 30.17 (callcenter)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("English Other"));
		AddVariant(G, TEXT("vosk-model-en-us-daanzu-20200905"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-daanzu-20200905.zip"), GB(1.0), TEXT("Wideband dictation model from Kaldi-active-grammar. WER: 7.08 (librispeech test-clean), 8.25 (tedlium)"), TEXT("AGPL"));
		AddVariant(G, TEXT("vosk-model-en-us-daanzu-20200905-lgraph"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-daanzu-20200905-lgraph.zip"), MB(129), TEXT("Same as above with configurable graph. WER: 8.20 (librispeech test-clean), 9.28 (tedlium)"), TEXT("AGPL"));
		AddVariant(G, TEXT("vosk-model-en-us-librispeech-0.2"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-librispeech-0.2.zip"), MB(845), TEXT("Repackaged Librispeech model from Kaldi, not very accurate"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-en-us-zamia-0.5"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-en-us-zamia-0.5.zip"), MB(49), TEXT("Repackaged Zamia model f_250, mainly for research. WER: 11.55 (librispeech test-clean), 12.64 (tedlium)"), TEXT("LGPL-3.0"));
		AddVariant(G, TEXT("vosk-model-en-us-aspire-0.2"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-aspire-0.2.zip"), GB(1.4), TEXT("Kaldi original ASPIRE model, not very accurate. WER: 13.64 (librispeech test-clean), 12.89 (tedlium), 33.82 (callcenter)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-en-us-0.21"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-us-0.21.zip"), GB(1.6), TEXT("Wideband model, previous generation. WER: 5.43 (librispeech test-clean), 6.42 (tedlium), 40.63 (callcenter)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Indian English"));
		AddVariant(G, TEXT("vosk-model-en-in-0.5"), TEXT("https://alphacephei.com/vosk/models/vosk-model-en-in-0.5.zip"), GB(1.0), TEXT("Generic Indian English model for telecom and broadcast. WER: 36.12 (NPTEL Pure)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-en-in-0.4"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-en-in-0.4.zip"), MB(36), TEXT("Lightweight Indian English model for mobile applications. WER: 49.05 (NPTEL Pure)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Chinese"));
		AddVariant(G, TEXT("vosk-model-small-cn-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip"), MB(42), TEXT("Lightweight model for Android and RPi. WER: 23.54 (SpeechIO-02), 38.29 (SpeechIO-06), 17.15 (THCHS)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-cn-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-cn-0.22.zip"), GB(1.3), TEXT("Big generic Chinese model for server processing. WER: 13.98 (SpeechIO-02), 27.30 (SpeechIO-06), 7.43 (THCHS)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Chinese Other"));
		AddVariant(G, TEXT("vosk-model-cn-kaldi-multicn-0.15"), TEXT("https://alphacephei.com/vosk/models/vosk-model-cn-kaldi-multicn-0.15.zip"), GB(1.5), TEXT("Original wideband Kaldi multi-cn model with Vosk LM. WER: 17.44 (SpeechIO-02), 9.56 (THCHS)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Russian"));
		AddVariant(G, TEXT("vosk-model-ru-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ru-0.42.zip"), GB(1.8), TEXT("Big mixed band Russian model for servers. WER: 4.5 (audiobooks), 11.1 (open_stt audiobooks), 19.5 (open_stt youtube), 36.0 (openstt calls), 4.4 (golos crowd), 17.9 (sova devices)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-ru-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-ru-0.22.zip"), MB(45), TEXT("Lightweight wideband model for Android/iOS and RPi. WER: 22.71 (openstt audiobooks), 31.97 (openstt youtube), 29.89 (sova devices), 11.79 (golos crowd)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Russian Other"));
		AddVariant(G, TEXT("vosk-model-ru-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ru-0.22.zip"), GB(1.5), TEXT("Big mixed band Russian model for servers. WER: 5.74 (audiobooks), 13.35 (open_stt audiobooks), 20.73 (open_stt youtube), 37.38 (openstt calls), 8.65 (golos crowd), 19.71 (sova devices)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-ru-0.10"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ru-0.10.zip"), GB(2.5), TEXT("Big narrowband Russian model for servers. WER: 5.71 (audiobooks), 16.26 (open_stt audiobooks), 26.20 (public_youtube_700_val), 40.15 (asr_calls_2_val)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("French"));
		AddVariant(G, TEXT("vosk-model-small-fr-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-fr-0.22.zip"), MB(41), TEXT("Lightweight wideband model for Android/iOS and RPi. WER: 23.95 (cv test), 19.30 (mtedx), 27.25 (podcast)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-fr-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-fr-0.22.zip"), GB(1.4), TEXT("Big accurate model for servers. WER: 14.72 (cv test), 11.64 (mls), 13.10 (mtedx), 21.61 (podcast), 13.22 (voxpopuli)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("French Other"));
		AddVariant(G, TEXT("vosk-model-small-fr-pguyot-0.3"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-fr-pguyot-0.3.zip"), MB(39), TEXT("Lightweight model trained by Paul Guyot. WER: 37.04 (cv test), 28.72 (mtedx), 37.46 (podcast)"), TEXT("CC-BY-NC-SA 4.0"));
		AddVariant(G, TEXT("vosk-model-fr-0.6-linto-2.2.0"), TEXT("https://alphacephei.com/vosk/models/vosk-model-fr-0.6-linto-2.2.0.zip"), GB(1.5), TEXT("Model from the LINTO project. WER: 16.19 (cv test), 16.44 (mtedx), 23.77 (podcast), 0.4xRT"), TEXT("AGPL"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("German"));
		AddVariant(G, TEXT("vosk-model-de-0.21"), TEXT("https://alphacephei.com/vosk/models/vosk-model-de-0.21.zip"), GB(1.9), TEXT("Big German model for telephony and server. WER: 9.83 (Tuda-de test), 24.00 (podcast), 12.82 (cv-test), 12.42 (mls), 33.26 (mtedx)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-de-tuda-0.6-900k"), TEXT("https://alphacephei.com/vosk/models/vosk-model-de-tuda-0.6-900k.zip"), GB(4.4), TEXT("Latest big wideband model from Tuda-DE. WER: 9.48 (Tuda-de test), 25.82 (podcast), 4.97 (cv-test), 11.01 (mls), 35.20 (mtedx)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-de-zamia-0.3"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-de-zamia-0.3.zip"), MB(49), TEXT("Zamia f_250 small model repackaged (not recommended). WER: 14.81 (Tuda-de test), 37.46 (podcast)"), TEXT("LGPL-3.0"));
		AddVariant(G, TEXT("vosk-model-small-de-0.15"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-de-0.15.zip"), MB(45), TEXT("Lightweight wideband model for Android and RPi. WER: 13.75 (Tuda-de test), 30.67 (podcast)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Spanish"));
		AddVariant(G, TEXT("vosk-model-small-es-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-es-0.42.zip"), MB(39), TEXT("Lightweight wideband model. WER: 16.02 (cv test), 16.72 (mtedx test), 11.21 (mls)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-es-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-es-0.42.zip"), GB(1.4), TEXT("Big model for Spanish. WER: 7.50 (cv test), 10.05 (mtedx test), 5.84 (mls)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Portuguese/Brazilian Portuguese"));
		AddVariant(G, TEXT("vosk-model-small-pt-0.3"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-pt-0.3.zip"), MB(31), TEXT("Lightweight wideband model. WER: 68.92 (coraa dev), 32.60 (cv test)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-pt-fb-v0.1.1-20220516_2113"), TEXT("https://alphacephei.com/vosk/models/vosk-model-pt-fb-v0.1.1-20220516_2113.zip"), GB(1.6), TEXT("Big model from FalaBrazil. WER: 54.34 (coraa dev), 27.70 (cv test)"), TEXT("GPLv3.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Greek"));
		AddVariant(G, TEXT("vosk-model-el-gr-0.7"), TEXT("https://alphacephei.com/vosk/models/vosk-model-el-gr-0.7.zip"), GB(1.1), TEXT("Big narrowband Greek model for server processing, not extremely accurate"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Turkish"));
		AddVariant(G, TEXT("vosk-model-small-tr-0.3"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-tr-0.3.zip"), MB(35), TEXT("Lightweight wideband model for Android and RPi"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Vietnamese"));
		AddVariant(G, TEXT("vosk-model-small-vn-0.4"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-vn-0.4.zip"), MB(32), TEXT("Lightweight Vietnamese model. WER: 15.70 (Vivos test)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-vn-0.4"), TEXT("https://alphacephei.com/vosk/models/vosk-model-vn-0.4.zip"), MB(78), TEXT("Bigger Vietnamese model for server. WER: 15.70 (Vivos test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Italian"));
		AddVariant(G, TEXT("vosk-model-small-it-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-it-0.22.zip"), MB(48), TEXT("Lightweight model. WER: 16.88 (cv test), 25.87 (mls), 17.01 (mtedx)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-it-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-it-0.22.zip"), GB(1.2), TEXT("Big generic Italian model for servers. WER: 8.10 (cv test), 15.68 (mls), 11.23 (mtedx)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Dutch"));
		AddVariant(G, TEXT("vosk-model-small-nl-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-nl-0.22.zip"), MB(39), TEXT("Lightweight model for Dutch. WER: 22.45 (cv test), 26.80 (tv), 25.84 (mls), 24.09 (voxpopuli)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Dutch Other"));
		AddVariant(G, TEXT("vosk-model-nl-spraakherkenning-0.6"), TEXT("https://alphacephei.com/vosk/models/vosk-model-nl-spraakherkenning-0.6.zip"), MB(860), TEXT("Medium Dutch model from Kaldi_NL. WER: 20.40 (cv test), 32.64 (tv), 17.73 (mls), 19.96 (voxpopuli)"), TEXT("CC-BY-NC-SA"));
		AddVariant(G, TEXT("vosk-model-nl-spraakherkenning-0.6-lgraph"), TEXT("https://alphacephei.com/vosk/models/vosk-model-nl-spraakherkenning-0.6-lgraph.zip"), MB(100), TEXT("Smaller model with dynamic graph. WER: 22.82 (cv test), 34.01 (tv), 18.81 (mls), 21.01 (voxpopuli)"), TEXT("CC-BY-NC-SA"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Catalan"));
		AddVariant(G, TEXT("vosk-model-small-ca-0.4"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-ca-0.4.zip"), MB(42), TEXT("Lightweight wideband model for Android and RPi for Catalan"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Arabic"));
		AddVariant(G, TEXT("vosk-model-ar-mgb2-0.4"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ar-mgb2-0.4.zip"), MB(318), TEXT("Repackaged Arabic model trained on MGB2 from Kaldi. WER: 16.40 (MGB-2 dev set)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-ar-0.22-linto-1.1.0"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ar-0.22-linto-1.1.0.zip"), GB(1.3), TEXT("Big model from the LINTO project. WER: 52.87 (cv test), 28.50 (MBG-2 dev set), 1.0xRT"), TEXT("AGPL"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Arabic Tunisian"));
		AddVariant(G, TEXT("vosk-model-small-ar-tn-0.1-linto"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-ar-tn-0.1-linto.zip"), MB(158), TEXT("Small Arabic Tunisian model from Linagora. WER: 16.06 (TARIC set)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-ar-tn-0.1-linto"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ar-tn-0.1-linto.zip"), MB(517), TEXT("Arabic Tunisian model from Linagora. WER: 16.06 (TARIC set)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Farsi"));
		AddVariant(G, TEXT("vosk-model-fa-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-fa-0.42.zip"), GB(1.6), TEXT("Large vocabulary model (Persian). WER: 16.7 (CV17), 11.1 (Fleurs)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-fa-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-fa-0.42.zip"), MB(53), TEXT("Small model for desktop/mobile (Persian). WER: 23.4 (CV17), 14.0 (Fleurs)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Farsi Other"));
		AddVariant(G, TEXT("vosk-model-fa-0.5"), TEXT("https://alphacephei.com/vosk/models/vosk-model-fa-0.5.zip"), GB(1.0), TEXT("Large vocabulary model (Persian). WER: 29.7 (CV17), 25.1 (Fleurs)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-fa-0.5"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-fa-0.5.zip"), MB(60), TEXT("Bigger small model for desktop apps (Persian). WER: 31.2 (CV17), 26.2 (Fleurs)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Filipino"));
		AddVariant(G, TEXT("vosk-model-tl-ph-generic-0.6"), TEXT("https://alphacephei.com/vosk/models/vosk-model-tl-ph-generic-0.6.zip"), MB(320), TEXT("Medium wideband model for Filipino (Tagalog). WER: 18.87 (FLEURS-dev), 18.61 (FLEURS-test), 97.9 (BABEL-dev), 41.31 (MATERIAL-dev)"), TEXT("CC-BY-NC-SA 4.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Ukrainian"));
		AddVariant(G, TEXT("vosk-model-small-uk-v3-nano"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-uk-v3-nano.zip"), MB(73), TEXT("Nano model from Speech Recognition for Ukrainian"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-uk-v3-small"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-uk-v3-small.zip"), MB(133), TEXT("Small model from Speech Recognition for Ukrainian"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-uk-v3"), TEXT("https://alphacephei.com/vosk/models/vosk-model-uk-v3.zip"), MB(343), TEXT("Bigger model from Speech Recognition for Ukrainian"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-uk-v3-lgraph"), TEXT("https://alphacephei.com/vosk/models/vosk-model-uk-v3-lgraph.zip"), MB(325), TEXT("Big dynamic model from Speech Recognition for Ukrainian"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Kazakh"));
		AddVariant(G, TEXT("vosk-model-small-kz-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-kz-0.42.zip"), MB(58), TEXT("Small mobile model for Kazakh. WER: 21.10 (fleurs), 30.0 (CV17), 9.7 (KSC), 24.86 (KSC Other)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-kz-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-kz-0.42.zip"), GB(1.3), TEXT("Bigger model for Kazakh. WER: 13.09 (fleurs), 12.50 (CV17), 4.49 (KSC), 18.51 (KSC Other)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Swedish"));
		AddVariant(G, TEXT("vosk-model-small-sv-rhasspy-0.15"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-sv-rhasspy-0.15.zip"), MB(289), TEXT("Repackaged model from the Rhasspy project"), TEXT("MIT"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Japanese"));
		AddVariant(G, TEXT("vosk-model-small-ja-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-ja-0.22.zip"), MB(48), TEXT("Lightweight wideband model. CER: 9.52 (csj), 17.07 (ted10k)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-ja-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ja-0.22.zip"), MB(1024), TEXT("Big model for Japanese. CER: 8.40 (csj), 13.91 (ted10k)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Esperanto"));
		AddVariant(G, TEXT("vosk-model-small-eo-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-eo-0.42.zip"), MB(42), TEXT("Lightweight model for Esperanto. WER: 7.24 (CV Test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Hindi"));
		AddVariant(G, TEXT("vosk-model-small-hi-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-hi-0.22.zip"), MB(42), TEXT("Lightweight model for Hindi. WER: 20.89 (IITM Challenge), 24.72 (MUCS Challenge)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-hi-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-hi-0.22.zip"), MB(1536), TEXT("Big accurate model for servers. WER: 14.85 (CV Test), 14.83 (IITM Challenge), 13.11 (MUCS Challenge)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Czech"));
		AddVariant(G, TEXT("vosk-model-small-cs-0.4-rhasspy"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-cs-0.4-rhasspy.zip"), MB(44), TEXT("Lightweight model for Czech from the Rhasspy project. WER: 21.29 (CV Test)"), TEXT("MIT"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Polish"));
		AddVariant(G, TEXT("vosk-model-small-pl-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-pl-0.22.zip"), MB(50), TEXT("Lightweight model for Polish. WER: 18.36 (CV Test), 16.88 (MLS Test), 11.55 (Voxpopuli Test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Uzbek"));
		AddVariant(G, TEXT("vosk-model-small-uz-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-uz-0.22.zip"), MB(49), TEXT("Lightweight model for Uzbek. WER: 13.54 (CV Test), 12.92 (IS2AI USC test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Korean"));
		AddVariant(G, TEXT("vosk-model-small-ko-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-ko-0.22.zip"), MB(82), TEXT("Lightweight model for Korean. WER: 28.1 (Zeroth Test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Breton"));
		AddVariant(G, TEXT("vosk-model-br-0.8"), TEXT("https://alphacephei.com/vosk/models/vosk-model-br-0.8.zip"), MB(70), TEXT("Breton model from the vosk-br project. WER: 36.4 (MCV11 Test)"), TEXT("MIT"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Gujarati"));
		AddVariant(G, TEXT("vosk-model-gu-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-gu-0.42.zip"), MB(700), TEXT("Big Gujarati model. WER: 16.45 (MS Test)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-gu-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-gu-0.42.zip"), MB(100), TEXT("Lightweight model for Gujarati. WER: 20.49 (MS Test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Tajik"));
		AddVariant(G, TEXT("vosk-model-tg-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-tg-0.22.zip"), MB(327), TEXT("Big Tajik model. WER: 41.1 (Fleurs test)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-small-tg-0.22"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-tg-0.22.zip"), MB(50), TEXT("Lightweight model for Tajik. WER: 38.4 (Fleurs test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Telugu"));
		AddVariant(G, TEXT("vosk-model-small-te-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-te-0.42.zip"), MB(58), TEXT("Lightweight model for Telugu. WER: 87.9 (Fleurs test)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Kyrgyz"));
		AddVariant(G, TEXT("vosk-model-small-ky-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-ky-0.42.zip"), MB(49), TEXT("Small mobile model for Kyrgyz. WER: 18.95 (Fleurs), 16.96 (CV17)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-ky-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ky-0.42.zip"), GB(1.1), TEXT("Bigger model for Kyrgyz. WER: 13.45 (Fleurs), 8.75 (CV17)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Georgian"));
		AddVariant(G, TEXT("vosk-model-small-ka-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-small-ka-0.42.zip"), MB(45), TEXT("Small mobile model for Georgian. WER: 18.0 (Fleurs), 14.2 (CV17)"), TEXT("Apache 2.0"));
		AddVariant(G, TEXT("vosk-model-ka-0.42"), TEXT("https://alphacephei.com/vosk/models/vosk-model-ka-0.42.zip"), MB(700), TEXT("Bigger model for Georgian. WER: 12.7 (Fleurs), 5.2 (CV17)"), TEXT("Apache 2.0"));
	}
	{
		FVoskModelLanguageGroup& G = AddGroup(TEXT("Speaker Identification"));
		AddVariant(G, TEXT("vosk-model-spk-0.4"), TEXT("https://alphacephei.com/vosk/models/vosk-model-spk-0.4.zip"), MB(13), TEXT("Model for speaker identification, should work for all languages"), TEXT("Apache 2.0"));
	}

	return Groups;
}

bool URuntimeVoskModelLibrary::FindVoskModelVariant(const FString& ModelId, FVoskModelVariant& OutVariant)
{
	for (const FVoskModelLanguageGroup& Group : GetPredefinedVoskModels())
	{
		for (const FVoskModelVariant& Variant : Group.Variants)
		{
			if (Variant.ModelId == ModelId)
			{
				OutVariant = Variant;
				return true;
			}
		}
	}
	return false;
}

FString URuntimeVoskModelLibrary::GetVoskModelsDirectory()
{
#if PLATFORM_ANDROID
	const FString BaseDir = RuntimeVoskAndroidNoBackupFilesDir.IsEmpty()
		? FPaths::ProjectPersistentDownloadDir()
		: RuntimeVoskAndroidNoBackupFilesDir;
	return FPaths::Combine(BaseDir, TEXT("RuntimeSpeechRecognizerVosk"), TEXT("Models"));
#else
	const FString VirtualPath = FPaths::Combine(FPaths::ProjectContentDir(), TEXT("RuntimeSpeechRecognizerVosk"), TEXT("Models"));
	return FPlatformFileManager::Get().GetPlatformFile().ConvertToAbsolutePathForExternalAppForRead(*VirtualPath);
#endif
}

FString URuntimeVoskModelLibrary::GetVoskModelsContentRelativePath()
{
	return TEXT("RuntimeSpeechRecognizerVosk/Models");
}

bool URuntimeVoskModelLibrary::IsVoskModelInPackagedContent(const FString& ModelId)
{
#if PLATFORM_ANDROID
	const FString SrcPath = FPaths::Combine(FPaths::ProjectContentDir(), GetVoskModelsContentRelativePath(), ModelId);
	return FPlatformFileManager::Get().GetPlatformFile().DirectoryExists(*SrcPath);
#else
	// On Desktop, GetVoskModelsDirectory() already points at Content, so "in packaged content"
	// and "on disk" are the same check.
	return IsVoskModelOnDisk(ModelId);
#endif
}

FString URuntimeVoskModelLibrary::GetVoskModelFolderPath(const FString& ModelId)
{
	return FPaths::Combine(GetVoskModelsDirectory(), ModelId);
}

FString URuntimeVoskModelLibrary::GetModelCompleteMarkerFileName()
{
	return TEXT(".vosk_model_complete");
}

bool URuntimeVoskModelLibrary::IsVoskModelOnDisk(const FString& ModelId)
{
	const FString MarkerPath = FPaths::Combine(GetVoskModelFolderPath(ModelId), GetModelCompleteMarkerFileName());
	return FPlatformFileManager::Get().GetPlatformFile().FileExists(*MarkerPath);
}

TArray<FName> URuntimeVoskModelLibrary::GetAvailableVoskModelNames()
{
	TSet<FName> ResultSet;

	for (const FName& Name : GetExtractedVoskModelNames())
	{
		ResultSet.Add(Name);
	}

#if PLATFORM_ANDROID
	const FString ContentModelsDir = FPaths::Combine(FPaths::ProjectContentDir(), GetVoskModelsContentRelativePath());
	if (FPlatformFileManager::Get().GetPlatformFile().DirectoryExists(*ContentModelsDir))
	{
		TArray<FString> SubDirs;
		IFileManager::Get().FindFiles(SubDirs, *FPaths::Combine(ContentModelsDir, TEXT("*")), false, true);
		for (const FString& SubDir : SubDirs)
		{
			ResultSet.Add(FName(*SubDir));
		}
	}
#endif

	return ResultSet.Array();
}

bool URuntimeVoskModelLibrary::IsVoskModelAvailable(const FString& ModelId)
{
	return IsVoskModelOnDisk(ModelId) || IsVoskModelInPackagedContent(ModelId);
}

#if PLATFORM_ANDROID

namespace RuntimeVoskAndroidCopy
{
	static TMap<FString, TSharedPtr<TArray<TSharedRef<TPromise<bool>>>>> InFlightCopies;
	static FCriticalSection InFlightCopiesGuard;

	static bool CopyModelDirectory(const FString& ModelId)
	{
		IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

		const FString ModelsContentRelativePath = URuntimeVoskModelLibrary::GetVoskModelsContentRelativePath();
		const FString DestModelsRoot = URuntimeVoskModelLibrary::GetVoskModelsDirectory();
		const FString DestPath = FPaths::Combine(DestModelsRoot, ModelId);
		const FString SrcPath = FPaths::Combine(FPaths::ProjectContentDir(), ModelsContentRelativePath, ModelId);

		UE_LOG(LogRuntimeSpeechRecognizerVosk, Log, TEXT("CopyModelDirectory: SrcPath='%s' DestPath='%s'"), *SrcPath, *DestPath);

		if (!PlatformFile.DirectoryExists(*DestPath) && !PlatformFile.CreateDirectoryTree(*DestPath))
		{
			UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: Failed to create destination directory '%s'"), *DestPath);
			return false;
		}
		if (!PlatformFile.DirectoryExists(*SrcPath))
		{
			UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: Source directory does not exist: '%s'"), *SrcPath);
			return false;
		}

		class FModelCopyVisitor : public IPlatformFile::FDirectoryVisitor
		{
			IPlatformFile& PlatformFile;
			TArray<FString>& DirsNext;
			FString ModelsContentRelativePath;
			FString DestModelsRoot;

		public:
			FModelCopyVisitor(IPlatformFile& InPlatformFile, TArray<FString>& DirsNextIn, FString InModelsContentRelativePath, FString InDestModelsRoot)
				: PlatformFile(InPlatformFile), DirsNext(DirsNextIn), ModelsContentRelativePath(MoveTemp(InModelsContentRelativePath)), DestModelsRoot(MoveTemp(InDestModelsRoot))
			{
			}

			virtual bool Visit(const TCHAR* FilenameOrDirectory, bool bIsDirectory) override
			{
				const FString CurrentPath = FilenameOrDirectory;

				FString ContentRelative;
				if (!CurrentPath.Split(TEXT("/Content/"), nullptr, &ContentRelative))
				{
					UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: Could not find '/Content/' marker in path '%s'"), *CurrentPath);
					return false;
				}

				FString RelativeToModels = ContentRelative;
				if (!RelativeToModels.RemoveFromStart(ModelsContentRelativePath + TEXT("/")))
				{
					UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: '%s' did not start with expected '%s/'"), *ContentRelative, *ModelsContentRelativePath);
					return false;
				}

				const FString DestFilePath = FPaths::Combine(DestModelsRoot, RelativeToModels);
				const FString SrcFilePath = FPaths::Combine(FPaths::ProjectContentDir(), ContentRelative);

				if (bIsDirectory)
				{
					if (!PlatformFile.CreateDirectoryTree(*DestFilePath) && !PlatformFile.DirectoryExists(*DestFilePath))
					{
						UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: Failed to create directory '%s'"), *DestFilePath);
						return false;
					}
					DirsNext.Emplace(SrcFilePath);
				}
				else if (!PlatformFile.FileExists(*DestFilePath))
				{
					if (!PlatformFile.CopyFile(*DestFilePath, *SrcFilePath))
					{
						UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: Failed to copy file '%s' -> '%s'"), *SrcFilePath, *DestFilePath);
						return false;
					}
				}
				return true;
			}
		};

		TArray<FString> DirsToVisit;
		FModelCopyVisitor Visitor(PlatformFile, DirsToVisit, ModelsContentRelativePath, DestModelsRoot);

		bool bSuccess = true;
		DirsToVisit.Add(SrcPath);
		int32 VisitedDirCount = 0;
		while (bSuccess && DirsToVisit.Num() > 0)
		{
			const FString CurrentDir = DirsToVisit.Pop();
			bSuccess = PlatformFile.IterateDirectory(*CurrentDir, Visitor);
			++VisitedDirCount;
			if (!bSuccess)
			{
				UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: IterateDirectory failed on '%s'"), *CurrentDir);
			}
		}

		UE_LOG(LogRuntimeSpeechRecognizerVosk, Log, TEXT("CopyModelDirectory: Visited %d directories, bSuccess=%s"), VisitedDirCount, bSuccess ? TEXT("true") : TEXT("false"));

		if (bSuccess)
		{
			const FString MarkerPath = FPaths::Combine(DestPath, URuntimeVoskModelLibrary::GetModelCompleteMarkerFileName());
			IFileHandle* Handle = PlatformFile.OpenWrite(*MarkerPath);
			bSuccess = (Handle != nullptr);
			if (Handle)
			{
				delete Handle;
			}
			else
			{
				UE_LOG(LogRuntimeSpeechRecognizerVosk, Error, TEXT("CopyModelDirectory: Failed to write marker file '%s'"), *MarkerPath);
			}
		}

		return bSuccess;
	}
}
 
TFuture<bool> URuntimeVoskModelLibrary::MakeVoskModelReadyOnAndroid(const FString& ModelId)
{
	if (IsVoskModelOnDisk(ModelId))
	{
		return MakeFulfilledPromise<bool>(true).GetFuture();
	}
 
	using namespace RuntimeVoskAndroidCopy;
 
	TSharedRef<TPromise<bool>> Promise = MakeShared<TPromise<bool>>();
	TFuture<bool> Future = Promise->GetFuture();
 
	bool bAlreadyInFlight = false;
	{
		FScopeLock Lock(&InFlightCopiesGuard);
		TSharedPtr<TArray<TSharedRef<TPromise<bool>>>>& Waiters = InFlightCopies.FindOrAdd(ModelId);
		if (!Waiters.IsValid())
		{
			Waiters = MakeShared<TArray<TSharedRef<TPromise<bool>>>>();
		}
		else
		{
			bAlreadyInFlight = true;
		}
		Waiters->Add(Promise);
	}
 
	if (bAlreadyInFlight)
	{
		return Future;
	}
 
	AsyncTask(ENamedThreads::AnyBackgroundHiPriTask, [ModelId]()
	{
		const bool bSuccess = RuntimeVoskAndroidCopy::CopyModelDirectory(ModelId);
 
		AsyncTask(ENamedThreads::GameThread, [ModelId, bSuccess]()
		{
			TSharedPtr<TArray<TSharedRef<TPromise<bool>>>> Waiters;
			{
				FScopeLock Lock(&RuntimeVoskAndroidCopy::InFlightCopiesGuard);
				RuntimeVoskAndroidCopy::InFlightCopies.RemoveAndCopyValue(ModelId, Waiters);
			}
			if (Waiters.IsValid())
			{
				for (const TSharedRef<TPromise<bool>>& Waiter : *Waiters)
				{
					Waiter->SetValue(bSuccess);
				}
			}
		});
	});
 
	return Future;
}
 
#else
 
TFuture<bool> URuntimeVoskModelLibrary::MakeVoskModelReadyOnAndroid(const FString& ModelId)
{
	return MakeFulfilledPromise<bool>(IsVoskModelOnDisk(ModelId)).GetFuture();
}
 
#endif

TArray<FName> URuntimeVoskModelLibrary::GetExtractedVoskModelNames()
{
	TArray<FName> Result;

	const FString ModelsDir = GetVoskModelsDirectory();
	if (!FPlatformFileManager::Get().GetPlatformFile().DirectoryExists(*ModelsDir))
	{
		return Result;
	}

	TArray<FString> SubDirs;
	IFileManager::Get().FindFiles(SubDirs, *FPaths::Combine(ModelsDir, TEXT("*")), false, true);

	for (const FString& SubDir : SubDirs)
	{
		if (IsVoskModelOnDisk(SubDir))
		{
			Result.Add(FName(*SubDir));
		}
	}

	return Result;
}

bool URuntimeVoskModelLibrary::DeleteVoskModelFiles(const FString& ModelId)
{
	return IFileManager::Get().DeleteDirectory(*GetVoskModelFolderPath(ModelId), false, true);
}