| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/accessibility/ax_language_detection.h" |
| #include <algorithm> |
| #include <functional> |
| |
| #include "base/command_line.h" |
| #include "base/i18n/unicodestring.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/accessibility/accessibility_switches.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_tree.h" |
| |
| namespace ui { |
| |
| namespace { |
| // This is the maximum number of languages we assign per page, so only the top |
| // 3 languages on the top will be assigned to any node. |
| const auto kMaxDetectedLanguagesPerPage = 3; |
| |
| // This is the maximum number of languages that cld3 will detect for each |
| // input we give it, 3 was recommended to us by the ML team as a good |
| // starting point. |
| const auto kMaxDetectedLanguagesPerSpan = 3; |
| |
| const auto kShortTextIdentifierMinByteLength = 1; |
| // TODO(https://bugs.chromium.org/p/chromium/issues/detail?id=971360): |
| // Determine appropriate value for kShortTextIdentifierMaxByteLength. |
| const auto kShortTextIdentifierMaxByteLength = 1000; |
| } // namespace |
| |
| AXLanguageInfo::AXLanguageInfo() = default; |
| AXLanguageInfo::~AXLanguageInfo() = default; |
| |
| AXLanguageInfoStats::AXLanguageInfoStats() : top_results_valid_(false) {} |
| AXLanguageInfoStats::~AXLanguageInfoStats() = default; |
| |
| void AXLanguageInfoStats::Add(const std::vector<std::string>& languages) { |
| // Assign languages with higher probability a higher score. |
| // TODO(chrishall): consider more complex scoring |
| size_t score = kMaxDetectedLanguagesPerSpan; |
| for (const auto& lang : languages) { |
| lang_counts_[lang] += score; |
| --score; |
| } |
| |
| InvalidateTopResults(); |
| } |
| |
| int AXLanguageInfoStats::GetScore(const std::string& lang) const { |
| const auto& lang_count_it = lang_counts_.find(lang); |
| if (lang_count_it == lang_counts_.end()) { |
| return 0; |
| } |
| return lang_count_it->second; |
| } |
| |
| void AXLanguageInfoStats::InvalidateTopResults() { |
| top_results_valid_ = false; |
| } |
| |
| // Check if a given language is within the top results. |
| bool AXLanguageInfoStats::CheckLanguageWithinTop(const std::string& lang) { |
| if (!top_results_valid_) { |
| GenerateTopResults(); |
| } |
| |
| for (const auto& item : top_results_) { |
| if (lang == item.second) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void AXLanguageInfoStats::GenerateTopResults() { |
| top_results_.clear(); |
| |
| for (const auto& item : lang_counts_) { |
| top_results_.emplace_back(item.second, item.first); |
| } |
| |
| // Since we store the pair as (score, language) the default operator> on pairs |
| // does our sort appropriately. |
| // Sort in descending order. |
| std::sort(top_results_.begin(), top_results_.end(), |
| std::greater<std::pair<unsigned int, std::string>>()); |
| |
| // Resize down to remove all values greater than the N we are considering. |
| top_results_.resize(kMaxDetectedLanguagesPerPage); |
| |
| top_results_valid_ = true; |
| } |
| |
| AXLanguageDetectionManager::AXLanguageDetectionManager() |
| : short_text_language_identifier_(kShortTextIdentifierMinByteLength, |
| kShortTextIdentifierMaxByteLength) {} |
| AXLanguageDetectionManager::~AXLanguageDetectionManager() = default; |
| |
| // Detect language for a subtree rooted at the given node. |
| void AXLanguageDetectionManager::DetectLanguageForSubtree( |
| AXNode* subtree_root) { |
| TRACE_EVENT0("accessibility", "AXLanguageInfo::DetectLanguageForSubtree"); |
| DCHECK(subtree_root); |
| if (!::switches::IsExperimentalAccessibilityLanguageDetectionEnabled()) { |
| return; |
| } |
| |
| DetectLanguageForSubtreeInternal(subtree_root); |
| } |
| |
| // Detect language for a subtree rooted at the given node |
| // will not check feature flag. |
| void AXLanguageDetectionManager::DetectLanguageForSubtreeInternal( |
| AXNode* node) { |
| if (node->IsText()) { |
| AXLanguageInfo* lang_info = node->GetLanguageInfo(); |
| if (!lang_info) { |
| // TODO(chrishall): consider space optimisations. |
| // Currently we keep these language info instances around until |
| // destruction of the containing node, this is due to us treating AXNode |
| // as otherwise read-only and so we store any detected language |
| // information on lang info. |
| |
| node->SetLanguageInfo(std::make_unique<AXLanguageInfo>()); |
| lang_info = node->GetLanguageInfo(); |
| } else { |
| lang_info->detected_languages.clear(); |
| } |
| |
| // TODO(chrishall): implement strategy for nodes which are too small to get |
| // reliable language detection results. Consider combination of |
| // concatenation and bubbling up results. |
| auto text = node->GetStringAttribute(ax::mojom::StringAttribute::kName); |
| |
| const auto results = language_identifier_.FindTopNMostFreqLangs( |
| text, kMaxDetectedLanguagesPerSpan); |
| |
| for (const auto res : results) { |
| // The output of FindTopNMostFreqLangs is already sorted by byte count, |
| // this seems good enough for now. |
| // Only consider results which are 'reliable', this will also remove |
| // 'unknown'. |
| if (res.is_reliable) { |
| lang_info->detected_languages.push_back(res.language); |
| } |
| } |
| lang_info_stats.Add(lang_info->detected_languages); |
| } |
| |
| // TODO(chrishall): refactor this as textnodes only ever have inline text |
| // boxes as children. This means we don't need to recurse except for |
| // inheritance which can be handled elsewhere. |
| for (AXNode* child : node->children()) { |
| DetectLanguageForSubtreeInternal(child); |
| } |
| } |
| |
| // Label language for each node in the subtree rooted at the given node. |
| // This relies on DetectLanguageForSubtree having already been run. |
| void AXLanguageDetectionManager::LabelLanguageForSubtree(AXNode* subtree_root) { |
| TRACE_EVENT0("accessibility", "AXLanguageInfo::LabelLanguageForSubtree"); |
| |
| DCHECK(subtree_root); |
| |
| if (!::switches::IsExperimentalAccessibilityLanguageDetectionEnabled()) { |
| return; |
| } |
| |
| LabelLanguageForSubtreeInternal(subtree_root); |
| } |
| |
| void AXLanguageDetectionManager::LabelLanguageForSubtreeInternal(AXNode* node) { |
| AXLanguageInfo* lang_info = node->GetLanguageInfo(); |
| |
| // lang_info is only attached by Detect when it thinks a node is interesting, |
| // the presence of lang_info means that Detect expects the node to end up with |
| // a language specified. |
| // |
| // If the lang_info->language is already set then we have no more work to do |
| // for this node. |
| if (lang_info && lang_info->language.empty()) { |
| for (const auto& lang : lang_info->detected_languages) { |
| if (lang_info_stats.CheckLanguageWithinTop(lang)) { |
| lang_info->language = lang; |
| break; |
| } |
| } |
| |
| // TODO(chrishall): consider obeying the author declared lang tag in some |
| // cases, either based on proximity or based on common language detection |
| // error cases. |
| |
| // If language is still empty then we failed to detect a language from |
| // this node, we will instead try construct a language from other sources |
| // including any lang attribute and any language from the parent tree. |
| if (lang_info->language.empty()) { |
| const auto& lang_attr = |
| node->GetStringAttribute(ax::mojom::StringAttribute::kLanguage); |
| if (!lang_attr.empty()) { |
| lang_info->language = lang_attr; |
| } else { |
| // We call GetLanguage() on our parent which will return a detected |
| // language if it has one, otherwise it will search up the tree for a |
| // kLanguage attribute. |
| // |
| // This means that lang attributes are inherited indefinitely but |
| // detected language is only inherited one level. |
| // |
| // Currently we only attach detected language to text nodes, once we |
| // start attaching detected language on other nodes we need to rethink |
| // this. We may want to attach detected language information once we |
| // consider combining multiple smaller text nodes into one larger one. |
| // |
| // TODO(chrishall): reconsider detected language inheritance. |
| AXNode* parent = node->parent(); |
| if (parent) { |
| const auto& parent_lang = parent->GetLanguage(); |
| if (!parent_lang.empty()) { |
| lang_info->language = parent_lang; |
| } |
| } |
| } |
| } |
| } |
| |
| for (AXNode* child : node->children()) { |
| LabelLanguageForSubtreeInternal(child); |
| } |
| } |
| |
| std::vector<AXLanguageSpan> |
| AXLanguageDetectionManager::GetLanguageAnnotationForStringAttribute( |
| const AXNode& node, |
| ax::mojom::StringAttribute attr) { |
| std::vector<AXLanguageSpan> language_annotation; |
| if (!node.HasStringAttribute(attr)) |
| return language_annotation; |
| |
| std::string attr_value = node.GetStringAttribute(attr); |
| |
| // Use author-provided language if present. |
| if (node.HasStringAttribute(ax::mojom::StringAttribute::kLanguage)) { |
| // Use author-provided language if present. |
| language_annotation.push_back(AXLanguageSpan{ |
| 0 /* start_index */, attr_value.length() /* end_index */, |
| node.GetStringAttribute( |
| ax::mojom::StringAttribute::kLanguage) /* language */, |
| 1 /* probability */}); |
| return language_annotation; |
| } |
| // Calculate top 3 languages. |
| // TODO(akihiroota): What's a reasonable number of languages to have |
| // cld_3 find? Should vary. |
| std::vector<chrome_lang_id::NNetLanguageIdentifier::Result> top_languages = |
| short_text_language_identifier_.FindTopNMostFreqLangs( |
| attr_value, kMaxDetectedLanguagesPerPage); |
| // Create vector of AXLanguageSpans. |
| for (const auto& result : top_languages) { |
| std::vector<chrome_lang_id::NNetLanguageIdentifier::SpanInfo> ranges = |
| result.byte_ranges; |
| for (const auto& span_info : ranges) { |
| language_annotation.push_back( |
| AXLanguageSpan{span_info.start_index, span_info.end_index, |
| result.language, span_info.probability}); |
| } |
| } |
| // Sort Language Annotations by increasing start index. LanguageAnnotations |
| // with lower start index should appear earlier in the vector. |
| std::sort( |
| language_annotation.begin(), language_annotation.end(), |
| [](const AXLanguageSpan& left, const AXLanguageSpan& right) -> bool { |
| return left.start_index <= right.start_index; |
| }); |
| // Ensure that AXLanguageSpans do not overlap. |
| for (size_t i = 0; i < language_annotation.size(); ++i) { |
| if (i > 0) { |
| DCHECK(language_annotation[i].start_index <= |
| language_annotation[i - 1].end_index); |
| } |
| } |
| return language_annotation; |
| } |
| |
| } // namespace ui |