[ios] Improve TTS voice selection

This commit allows a user to select more voices (e.g. English (United States),
English (India)). Currently, it's only possible to select a subset of the
available voices on iOS. For example, if a user selects English as TTS
language, an Australian voice is selected because en_AU comes before en_US in
the voice list of iOS.

Fixes #6840
Fixes #3222
Fixes #2178

Signed-off-by: Fabian Wüthrich <me@fabwu.ch>
This commit is contained in:
Fabian Wüthrich 2024-05-16 09:18:47 -06:00 committed by Viktor Havaka
parent 10b01c93d8
commit d898cf16ac
6 changed files with 94 additions and 36 deletions

View file

@ -9,10 +9,11 @@
// * name in bcp47;
// * localized name;
- (std::vector<std::pair<std::string, std::string>>)availableLanguages;
- (std::pair<std::string, std::string>)standardLanguage;
@end
namespace tts
{
std::string translatedTwine(std::string const & twine);
std::string translateLocale(std::string const & localeString);
} // namespace tts

View file

@ -27,15 +27,15 @@ std::vector<std::pair<std::string, std::string>> availableLanguages()
using namespace routing::turns::sound;
std::vector<std::pair<std::string, std::string>> result;
for (auto const & p : kLanguageList)
for (auto const & [twineRouting, _] : kLanguageList)
{
for (std::pair<std::string, std::string> const & lang : native)
for (auto const & [twineVoice, bcp47Voice] : native)
{
if (lang.first == p.first)
if (twineVoice == twineRouting)
{
// Twine names are equal. Make a pair: bcp47 name, localized name.
result.emplace_back(make_pair(lang.second, p.second));
break;
auto pair = std::make_pair(bcp47Voice, tts::translateLocale(bcp47Voice));
if (std::find(result.begin(), result.end(), pair) == result.end())
result.emplace_back(std::move(pair));
}
}
}
@ -91,7 +91,7 @@ using Observers = NSHashTable<Observer>;
std::pair<std::string, std::string> const lan =
std::make_pair(preferedLanguageBcp47.UTF8String,
tts::translatedTwine(bcp47ToTwineLanguage(preferedLanguageBcp47)));
tts::translateLocale(preferedLanguageBcp47.UTF8String));
if (find(_availableLanguages.begin(), _availableLanguages.end(), lan) !=
_availableLanguages.end())
@ -123,6 +123,9 @@ using Observers = NSHashTable<Observer>;
self.speechSynthesizer.delegate = nil;
}
- (std::vector<std::pair<std::string, std::string>>)availableLanguages { return _availableLanguages; }
- (std::pair<std::string, std::string>)standardLanguage {
return std::make_pair(kDefaultLanguage.UTF8String, tts::translateLocale(kDefaultLanguage.UTF8String));
}
- (void)setNotificationsLocale:(NSString *)locale {
NSUserDefaults * ud = NSUserDefaults.standardUserDefaults;
[ud setObject:locale forKey:kUserDefaultsTTSLanguageBcp47];
@ -184,10 +187,6 @@ using Observers = NSHashTable<Observer>;
AVSpeechSynthesisVoice * voice = nil;
for (NSString * loc in candidateLocales) {
if ([loc isEqualToString:@"en-US"])
voice = [AVSpeechSynthesisVoice voiceWithIdentifier:AVSpeechSynthesisVoiceIdentifierAlex];
if (voice)
break;
voice = [AVSpeechSynthesisVoice voiceWithLanguage:loc];
if (voice)
break;
@ -301,16 +300,12 @@ using Observers = NSHashTable<Observer>;
namespace tts
{
std::string translatedTwine(std::string const & twine)
std::string translateLocale(std::string const & localeString)
{
auto const & list = routing::turns::sound::kLanguageList;
auto const it =
find_if(list.begin(), list.end(),
[&twine](std::pair<std::string, std::string> const & pair) { return pair.first == twine; });
if (it != list.end())
return it->second;
else
return "";
NSString * nsLocaleString = [NSString stringWithUTF8String: localeString.c_str()];
NSLocale * locale = [[NSLocale alloc] initWithLocaleIdentifier: nsLocaleString];
NSString * localizedName = [locale localizedStringForLocaleIdentifier:nsLocaleString];
localizedName = [localizedName capitalizedString];
return std::string(localizedName.UTF8String);
}
} // namespace tts

View file

@ -260,6 +260,7 @@
47F86CFF20C936FC00FEE291 /* TabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F86CFE20C936FC00FEE291 /* TabView.swift */; };
47F86D0120C93D8D00FEE291 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47F86D0020C93D8D00FEE291 /* TabViewController.swift */; };
4A300ED51C6DCFD400140018 /* countries-strings in Resources */ = {isa = PBXBuildFile; fileRef = 4A300ED31C6DCFD400140018 /* countries-strings */; };
4B4153B52BF9695500EE4B02 /* MWMTextToSpeechTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B4153B42BF9695500EE4B02 /* MWMTextToSpeechTests.mm */; };
6741A9421BF340DE002C974C /* sound-strings in Resources */ = {isa = PBXBuildFile; fileRef = 5605022E1B6211E100169CAD /* sound-strings */; };
6741A9451BF340DE002C974C /* classificator.txt in Resources */ = {isa = PBXBuildFile; fileRef = EE026F0511D6AC0D00645242 /* classificator.txt */; };
6741A9491BF340DE002C974C /* countries.txt in Resources */ = {isa = PBXBuildFile; fileRef = FA46DA2B12D4166E00968C36 /* countries.txt */; };
@ -1164,6 +1165,7 @@
4A7D89C21B2EBF3B00AC843E /* resources-mdpi_dark */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "resources-mdpi_dark"; path = "../../data/resources-mdpi_dark"; sourceTree = "<group>"; };
4A7D89C31B2EBF3B00AC843E /* resources-xhdpi_dark */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "resources-xhdpi_dark"; path = "../../data/resources-xhdpi_dark"; sourceTree = "<group>"; };
4A7D89C41B2EBF3B00AC843E /* resources-xxhdpi_dark */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "resources-xxhdpi_dark"; path = "../../data/resources-xxhdpi_dark"; sourceTree = "<group>"; };
4B4153B42BF9695500EE4B02 /* MWMTextToSpeechTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MWMTextToSpeechTests.mm; sourceTree = "<group>"; };
5605022E1B6211E100169CAD /* sound-strings */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "sound-strings"; path = "../../data/sound-strings"; sourceTree = "<group>"; };
6741AA5D1BF340DE002C974C /* Organic Maps (Debug).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Organic Maps (Debug).app"; sourceTree = BUILT_PRODUCTS_DIR; };
6B15907026623AE500944BBA /* 00_NotoSansThai-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "00_NotoSansThai-Regular.ttf"; path = "../../data/00_NotoSansThai-Regular.ttf"; sourceTree = "<group>"; };
@ -2611,6 +2613,38 @@
path = TabView;
sourceTree = "<group>";
};
4B4153B62BF9709100EE4B02 /* Core */ = {
isa = PBXGroup;
children = (
4B4153B72BF970A000EE4B02 /* TextToSpeech */,
);
path = Core;
sourceTree = "<group>";
};
4B4153B72BF970A000EE4B02 /* TextToSpeech */ = {
isa = PBXGroup;
children = (
4B4153B42BF9695500EE4B02 /* MWMTextToSpeechTests.mm */,
);
path = TextToSpeech;
sourceTree = "<group>";
};
4B4153B82BF970B800EE4B02 /* Classes */ = {
isa = PBXGroup;
children = (
4B4153B92BF970BD00EE4B02 /* CarPlay */,
);
path = Classes;
sourceTree = "<group>";
};
4B4153B92BF970BD00EE4B02 /* CarPlay */ = {
isa = PBXGroup;
children = (
ED1ADA322BC6B1B40029209F /* CarPlayServiceTests.swift */,
);
path = CarPlay;
sourceTree = "<group>";
};
97B4E9271851DAB300BEC5D7 /* Custom Views */ = {
isa = PBXGroup;
children = (
@ -2977,7 +3011,8 @@
ED1ADA312BC6B19E0029209F /* Tests */ = {
isa = PBXGroup;
children = (
ED1ADA322BC6B1B40029209F /* CarPlayServiceTests.swift */,
4B4153B82BF970B800EE4B02 /* Classes */,
4B4153B62BF9709100EE4B02 /* Core */,
);
path = Tests;
sourceTree = "<group>";
@ -4476,6 +4511,7 @@
buildActionMask = 2147483647;
files = (
ED1ADA332BC6B1B40029209F /* CarPlayServiceTests.swift in Sources */,
4B4153B52BF9695500EE4B02 /* MWMTextToSpeechTests.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -0,0 +1,28 @@
#import <XCTest/XCTest.h>
#import "MWMTextToSpeech+CPP.h"
@interface MWMTextToSpeechTest : XCTestCase
@end
@implementation MWMTextToSpeechTest
- (void)testAvailableLanguages {
MWMTextToSpeech * tts = [MWMTextToSpeech tts];
std::vector<std::pair<std::string, std::string>> langs = tts.availableLanguages;
auto const defaultLang = std::make_pair("en-US", "English (United States)");
XCTAssertTrue(std::find(langs.begin(), langs.end(), defaultLang) != langs.end());
}
- (void)testTranslateLocaleWithTwineString {
XCTAssertEqual(tts::translateLocale("en"), "English");
}
- (void)testTranslateLocaleWithBcp47String {
XCTAssertEqual(tts::translateLocale("en-US"), "English (United States)");
}
- (void)testTranslateLocaleWithUnknownString {
XCTAssertEqual(tts::translateLocale("unknown"), "");
}
@end

View file

@ -241,21 +241,20 @@ struct StreetNamesCellStrategy : BaseCellStategy
MWMTextToSpeech * tts = [MWMTextToSpeech tts];
m_languages.reserve(3);
auto const & v = tts.availableLanguages;
NSAssert(!v.empty(), @"Vector can't be empty!");
pair<string, string> const standart = v.front();
m_languages.push_back(standart);
pair<string, string> const standard = tts.standardLanguage;
m_languages.push_back(standard);
using namespace tts;
NSString * currentBcp47 = [AVSpeechSynthesisVoice currentLanguageCode];
string const currentBcp47Str = currentBcp47.UTF8String;
string const currentTwineStr = bcp47ToTwineLanguage(currentBcp47);
if (currentBcp47Str != standart.first && !currentBcp47Str.empty())
if (currentBcp47Str != standard.first && !currentBcp47Str.empty())
{
string const translated = translatedTwine(currentTwineStr);
pair<string, string> const cur{currentBcp47Str, translated};
auto const & v = tts.availableLanguages;
NSAssert(!v.empty(), @"Vector can't be empty!");
std::string const translated = translateLocale(currentBcp47Str);
auto cur = std::make_pair(currentBcp47Str, translated);
if (translated.empty() || find(v.begin(), v.end(), cur) != v.end())
m_languages.push_back(cur);
m_languages.push_back(std::move(cur));
else
self.isLocaleLanguageAbsent = YES;
}
@ -263,11 +262,10 @@ struct StreetNamesCellStrategy : BaseCellStategy
NSString * nsSavedLanguage = [MWMTextToSpeech savedLanguage];
if (nsSavedLanguage.length)
{
string const savedLanguage = nsSavedLanguage.UTF8String;
if (savedLanguage != currentBcp47Str && savedLanguage != standart.first &&
std::string const savedLanguage = nsSavedLanguage.UTF8String;
if (savedLanguage != currentBcp47Str && savedLanguage != standard.first &&
!savedLanguage.empty())
m_languages.emplace_back(
make_pair(savedLanguage, translatedTwine(bcp47ToTwineLanguage(nsSavedLanguage))));
m_languages.emplace_back(savedLanguage, translateLocale(savedLanguage));
}
}