33#include < stdcorelib/str.h>
44#include < synthrt/G2P/Support/PhonemeDict.h>
55
6+ #include < cctype>
67#include < cstring>
78#include < fstream>
9+ #include < string>
810
911namespace srt ::g2p {
1012
13+ // / Matches a CMU-style trailing "(digits)" variant suffix in \p key.
14+ // / On match returns true and sets \p pos to the index of '(' and \p no to
15+ // / the parsed variant number. Requires at least one base character before
16+ // / '(' and at least one digit; keys like "word(x)" or "(1)" never match.
17+ static bool parse_variant_suffix (const char *key, size_t len, size_t &pos, uint32_t &no) {
18+ if (len < 4 || key[len - 1 ] != ' )' ) // minimum form: "a(1)"
19+ return false ;
20+ size_t i = len - 2 ;
21+ if (!std::isdigit (static_cast <unsigned char >(key[i])))
22+ return false ;
23+ while (i > 0 && std::isdigit (static_cast <unsigned char >(key[i])))
24+ --i;
25+ if (i == 0 || key[i] != ' (' )
26+ return false ;
27+ if (len - 2 - i > 9 ) // digit run too long, avoid uint32 overflow
28+ return false ;
29+ uint32_t n = 0 ;
30+ for (size_t j = i + 1 ; j <= len - 2 ; ++j)
31+ n = n * 10 + static_cast <uint32_t >(key[j] - ' 0' );
32+ pos = i;
33+ no = n;
34+ return true ;
35+ }
36+
1137 static std::error_code make_last_error () {
1238#ifdef _WIN32
1339 return std::error_code (errno, stdc::windows_utf8_category ());
@@ -33,8 +59,12 @@ namespace srt::g2p {
3359 struct Entry {
3460 uint32_t offset;
3561 uint32_t count;
62+ uint32_t suffix; // 0 = bare base row, n = "(n)" variant number
3663 };
37- using MapType = spp::sparse_hash_map<char *, Entry, const_char_hash, const_char_equal>;
64+ // Rows sharing a base word (after "(n)" suffix stripping) merge into one
65+ // variant group; per-group variants keep file order (base row first).
66+ using MapType =
67+ spp::sparse_hash_map<char *, std::vector<Entry>, const_char_hash, const_char_equal>;
3868 using SppIterator = MapType::const_iterator;
3969
4070 std::vector<char > filebuf;
@@ -57,6 +87,15 @@ namespace srt::g2p {
5787 row = buf[0 ];
5888 col = buf[1 ];
5989 }
90+
91+ // / Index of the variant with the given suffix number within a group, or -1.
92+ static int findSuffixIndex (const std::vector<Entry> &variants, uint32_t suffix) {
93+ for (size_t i = 0 ; i < variants.size (); ++i) {
94+ if (variants[i].suffix == suffix)
95+ return static_cast <int >(i);
96+ }
97+ return -1 ;
98+ }
6099 };
61100
62101 PhonemeDict::PhonemeDict () : _impl(std::make_shared<Impl>()) {
@@ -102,7 +141,21 @@ namespace srt::g2p {
102141 filebuf.clear ();
103142 return false ;
104143 }
105- filebuf[file_size] = ' \n ' ; // add terminating line break
144+
145+ // Strip a UTF-8 BOM if the dict file starts with EF BB BF. Without
146+ // this, the first key would carry the BOM bytes and never match or
147+ // merge with its "(n)" variants (several bundled dicts carry a BOM:
148+ // deu/fra/ita/por/rus/spa). Physically shifting the buffer keeps all
149+ // offsets consistent with impl.filebuf.data().
150+ size_t parsed_size = static_cast <size_t >(file_size);
151+ if (parsed_size >= 3 && static_cast <unsigned char >(filebuf[0 ]) == 0xEF &&
152+ static_cast <unsigned char >(filebuf[1 ]) == 0xBB &&
153+ static_cast <unsigned char >(filebuf[2 ]) == 0xBF ) {
154+ std::memmove (filebuf.data (), filebuf.data () + 3 , parsed_size - 3 );
155+ parsed_size -= 3 ;
156+ }
157+ filebuf.resize (parsed_size + 1 ); // shrink to content + terminator slot
158+ filebuf[parsed_size] = ' \n ' ; // add terminating line break
106159 map.clear ();
107160
108161 // Parse the buffer
@@ -111,7 +164,7 @@ namespace srt::g2p {
111164
112165 // Estimate line numbers if the file is too large
113166 static constexpr size_t large_file_size = 1 * 1024 * 1024 ;
114- if (file_size > large_file_size) {
167+ if (parsed_size > large_file_size) {
115168 const size_t line_cnt = std::count (buffer_begin, buffer_end, ' \n ' ) + 1 ;
116169 map.reserve (line_cnt);
117170 }
@@ -192,8 +245,50 @@ namespace srt::g2p {
192245 value_cnt = compacted_count;
193246 }
194247
195- map[start] = Impl::Entry{static_cast <uint32_t >(value_start - buffer_begin), value_cnt};
196- start = p + 1 ;
248+ // Strip a CMU-style "(n)" variant suffix from the key in place
249+ // (filebuf is mutable memory), so all rows of a multi-pronunciation
250+ // word merge under the bare base key. Keys without a strict
251+ // tail "(digits)" pattern are left untouched, keeping other
252+ // languages' dictionaries byte-for-byte compatible.
253+ uint32_t suffix = 0 ;
254+ {
255+ const size_t key_len = std::strlen (start);
256+ size_t spos = 0 ;
257+ uint32_t sno = 0 ;
258+ if (parse_variant_suffix (start, key_len, spos, sno)) {
259+ start[spos] = ' \0 ' ;
260+ suffix = sno;
261+ }
262+ }
263+
264+ // Merge into the base key's variant group (file order). Skip
265+ // exact duplicates (same suffix number AND identical phoneme
266+ // sequence) so repeated lines don't multiply candidates.
267+ auto &variants = map[start];
268+ Impl::Entry newEntry{static_cast <uint32_t >(value_start - buffer_begin), value_cnt,
269+ suffix};
270+ bool dup = false ;
271+ for (const auto &e : variants) {
272+ if (e.suffix != suffix || e.count != value_cnt)
273+ continue ;
274+ const char *a = buffer_begin + e.offset ;
275+ const char *b = value_start;
276+ uint32_t k = 0 ;
277+ for (; k < value_cnt; ++k) {
278+ if (std::strcmp (a, b) != 0 )
279+ break ;
280+ a += std::strlen (a) + 1 ;
281+ b += std::strlen (b) + 1 ;
282+ }
283+ if (k == value_cnt) {
284+ dup = true ;
285+ break ;
286+ }
287+ }
288+ if (!dup) {
289+ variants.push_back (newEntry);
290+ }
291+ start = p + 1 ;
197292 }
198293 }
199294 return true ;
@@ -207,23 +302,28 @@ namespace srt::g2p {
207302 if (_copy) {
208303 return ;
209304 }
210- auto it = Impl::loadIter (_row, _col);
211- const char *key = it->first ;
212- PhonemeList value (_buf + it->second .offset , it->second .count );
305+ auto it = Impl::loadIter (_row, _col);
306+ const char *key = it->first ;
307+ const auto &variants = it->second ;
308+ const size_t idx =
309+ (_var >= 0 && static_cast <size_t >(_var) < variants.size ()) ? static_cast <size_t >(_var) : 0 ;
310+ PhonemeList value (_buf + variants[idx].offset , variants[idx].count );
213311 _copy = std::make_pair (key, value);
214312 }
215313
216314 void PhonemeDict::iterator::next () {
217315 auto it = Impl::loadIter (_row, _col);
218316 ++it;
219317 Impl::storeIter (it, _row, _col);
318+ _var = 0 ;
220319 _copy.reset ();
221320 }
222321
223322 void PhonemeDict::iterator::prev () {
224323 auto it = Impl::loadIter (_row, _col);
225324 --it;
226325 Impl::storeIter (it, _row, _col);
326+ _var = 0 ;
227327 _copy.reset ();
228328 }
229329
@@ -240,38 +340,87 @@ namespace srt::g2p {
240340 return end ();
241341 }
242342 // const_cast is safe: sparsepp::sparse_hash_map::find() takes non-const key but does not modify it
243- const auto it = map.find (const_cast <char *>(key));
343+ auto it = map.find (const_cast <char *>(key));
344+
345+ int variantIndex = 0 ;
244346 if (it == map.end ()) {
245- return end ();
347+ // Suffix-aware fallback (D4): a suffixed key like "word(2)"
348+ // resolves against the merged base group and selects exactly the
349+ // variant with that suffix number. No candidates are involved.
350+ size_t pos = 0 ;
351+ uint32_t no = 0 ;
352+ if (!parse_variant_suffix (key, std::strlen (key), pos, no)) {
353+ return end ();
354+ }
355+ const std::string base (key, pos);
356+ it = map.find (const_cast <char *>(base.c_str ()));
357+ if (it == map.end ()) {
358+ return end ();
359+ }
360+ variantIndex = Impl::findSuffixIndex (it->second , no);
361+ if (variantIndex < 0 ) {
362+ return end ();
363+ }
246364 }
365+
247366 const void *row = nullptr ;
248367 const void *col = nullptr ;
249368 Impl::storeIter (it, row, col);
250- return iterator (impl.filebuf .data (), row, col);
369+ return iterator (impl.filebuf .data (), row, col, variantIndex );
251370 }
252371
253372 bool PhonemeDict::contains (const char *key) const {
254- stdc_impl_t ;
255- auto &map = impl.map ;
256- if (!key) {
257- return false ;
258- }
259- // const_cast is safe: sparsepp::sparse_hash_map::find() only reads, never modifies
260- return map.find (const_cast <char *>(key)) != map.end ();
373+ return find (key) != end ();
261374 }
262375
263376 PhonemeList PhonemeDict::operator [](const char *key) const {
377+ const auto it = find (key);
378+ if (it == end ()) {
379+ return PhonemeList ();
380+ }
381+ return it->second ;
382+ }
383+
384+ std::vector<PhonemeList> PhonemeDict::lookupAll (const char *key) const {
264385 stdc_impl_t ;
265- auto &map = impl. map ;
386+ std::vector<PhonemeList> out ;
266387 if (!key) {
267- return PhonemeList () ;
388+ return out ;
268389 }
390+ auto &map = impl.map ;
269391 // const_cast is safe: sparsepp::sparse_hash_map::find() only reads, never modifies
270- const auto it = map.find (const_cast <char *>(key));
392+ auto it = map.find (const_cast <char *>(key));
393+
271394 if (it == map.end ()) {
272- return PhonemeList ();
395+ // Suffix-aware fallback (D4): return only the matching variant,
396+ // never the whole group, so no candidate choice is implied.
397+ size_t pos = 0 ;
398+ uint32_t no = 0 ;
399+ if (!parse_variant_suffix (key, std::strlen (key), pos, no)) {
400+ return out;
401+ }
402+ const std::string base (key, pos);
403+ it = map.find (const_cast <char *>(base.c_str ()));
404+ if (it == map.end ()) {
405+ return out;
406+ }
407+ const int idx = Impl::findSuffixIndex (it->second , no);
408+ if (idx < 0 ) {
409+ return out;
410+ }
411+ const auto &e = it->second [static_cast <size_t >(idx)];
412+ // NOTE: construct the PhonemeList here and push a copy —
413+ // vector::emplace_back would invoke the protected constructor from
414+ // inside allocator_traits (not a friend) and fails on MSVC (C2672).
415+ out.push_back (PhonemeList (impl.filebuf .data () + e.offset , e.count ));
416+ return out;
417+ }
418+
419+ out.reserve (it->second .size ());
420+ for (const auto &e : it->second ) {
421+ out.push_back (PhonemeList (impl.filebuf .data () + e.offset , e.count ));
273422 }
274- return PhonemeList (impl. filebuf . data () + it-> second . offset , it-> second . count ) ;
423+ return out ;
275424 }
276425
277426 bool PhonemeDict::empty () const {
0 commit comments