LCOV - code coverage report
Current view: top level - src/ngram - ngram_backend.dart (source / functions) Coverage Total Hit
Test: lcov.info Lines: 100.0 % 24 24
Test Date: 2026-08-24 07:45:26 Functions: - 0 0

            Line data    Source code
       1              : // Copyright 2026 The Authors.
       2              : //
       3              : // Licensed under the Apache License, Version 2.0 (the "License");
       4              : // you may not use this file except in compliance with the License.
       5              : // You may obtain a copy of the License at
       6              : //
       7              : //     https://www.apache.org/licenses/LICENSE-2.0
       8              : //
       9              : // Unless required by applicable law or agreed to in writing, software
      10              : // distributed under the License is distributed on an "AS IS" BASIS,
      11              : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12              : // See the License for the specific language governing permissions and
      13              : // limitations under the License.
      14              : 
      15              : import '../backend.dart';
      16              : import '../guess.dart';
      17              : import 'ngram_extractor.dart';
      18              : import 'profiles.g.dart';
      19              : 
      20              : /// The profile size used both when generating reference profiles
      21              : /// (`tool/generate_ngram_profiles.dart`) and when extracting a query's own
      22              : /// ranked n-gram list. Also doubles as the maximum "out-of-place" penalty
      23              : /// for an n-gram absent from a profile — the standard choice in the
      24              : /// Cavnar & Trenkle algorithm family (see [NgramBackend]).
      25              : const int ngramProfileSize = 300;
      26              : 
      27              : /// A [LanguageDetectorBackend] implementing Cavnar & Trenkle's
      28              : /// "N-Gram-Based Text Categorization" (1994): each language has a reference
      29              : /// profile — its top-[ngramProfileSize] character n-grams, ranked by
      30              : /// frequency in a training corpus (see `profiles.g.dart`). An input text is
      31              : /// scored against every candidate language's profile by summing, for each of
      32              : /// the input's own top-ranked n-grams, the absolute difference in rank
      33              : /// between the two lists (an "out-of-place" distance); n-grams the profile
      34              : /// doesn't have at all are penalized by the maximum possible amount,
      35              : /// [ngramProfileSize].
      36              : ///
      37              : /// Confidence is **not** a calibrated probability — it is a linear rescaling
      38              : /// of each candidate's distance across the *candidate set being compared*:
      39              : /// the best-scoring candidate gets `1.0`, the worst gets `0.0`, and everyone
      40              : /// else is scaled linearly in between. This means the same input text can
      41              : /// report a different confidence for the same language depending on which
      42              : /// other languages it was compared against (see `restrictTo` on
      43              : /// `LanguageDetector` / `CompositeBackend`) — narrowing the candidate set
      44              : /// sharpens confidence, by design.
      45              : final class NgramBackend implements LanguageDetectorBackend {
      46              :   /// Reference profiles to score against, keyed by language code. Defaults
      47              :   /// to the full generated set (`profiles.g.dart`); a smaller map can be
      48              :   /// passed directly for testing or for a pre-narrowed candidate set.
      49              :   final Map<String, List<String>> _profiles;
      50              : 
      51              :   /// Creates a backend scoring against [profiles] (language code -> ranked
      52              :   /// n-gram list). Defaults to the generated 58-language profile set.
      53            3 :   NgramBackend([Map<String, List<String>>? profiles])
      54              :     : _profiles = profiles ?? ngramProfiles;
      55              : 
      56            1 :   @override
      57            3 :   Set<String> get supportedLanguages => _profiles.keys.toSet();
      58              : 
      59            3 :   @override
      60              :   List<LanguageGuess> score(String text) {
      61            3 :     final queryNgrams = extractRankedNgrams(text, limit: ngramProfileSize);
      62            9 :     if (queryNgrams.isEmpty || _profiles.isEmpty) return const [];
      63              : 
      64            3 :     final distances = <String, int>{};
      65            9 :     for (final entry in _profiles.entries) {
      66           12 :       distances[entry.key] = _distance(queryNgrams, entry.value);
      67              :     }
      68              : 
      69           12 :     final minDistance = distances.values.reduce((a, b) => a < b ? a : b);
      70           12 :     final maxDistance = distances.values.reduce((a, b) => a > b ? a : b);
      71            3 :     final range = maxDistance - minDistance;
      72              : 
      73            3 :     return [
      74            3 :       for (final entry in distances.entries)
      75            3 :         LanguageGuess(
      76            3 :           entry.key,
      77           15 :           range == 0 ? 1.0 : 1.0 - (entry.value - minDistance) / range,
      78              :         ),
      79              :     ];
      80              :   }
      81              : 
      82              :   /// Sums, for each n-gram in [queryNgrams], the absolute rank difference
      83              :   /// against its position in [profile] — or [ngramProfileSize] if [profile]
      84              :   /// doesn't contain it at all.
      85            3 :   int _distance(List<String> queryNgrams, List<String> profile) {
      86            3 :     final profileRank = <String, int>{
      87           15 :       for (var i = 0; i < profile.length; i++) profile[i]: i,
      88              :     };
      89              : 
      90              :     var total = 0;
      91            9 :     for (var rank = 0; rank < queryNgrams.length; rank++) {
      92            6 :       final profileIndex = profileRank[queryNgrams[rank]];
      93            3 :       total += profileIndex == null
      94              :           ? ngramProfileSize
      95            6 :           : (rank - profileIndex).abs();
      96              :     }
      97              :     return total;
      98              :   }
      99              : }
        

Generated by: LCOV version 2.0-1