Skip to content

Commit 5abbb48

Browse files
committed
feat(g2p): multi-pronunciation dict variants (PhonemeDict merge, DictStep candidates, BOM strip)
1 parent 0f18203 commit 5abbb48

5 files changed

Lines changed: 474 additions & 41 deletions

File tree

‎include/synthrt/G2P/Support/PhonemeDict.h‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ namespace srt::g2p
115115
using pointer = const value_type *;
116116
using reference = const value_type &;
117117

118-
iterator() : _buf(nullptr), _row(nullptr), _col(nullptr) {}
118+
iterator() : _buf(nullptr), _row(nullptr), _col(nullptr), _var(0) {}
119119

120120
reference operator*() const {
121121
fetch();
@@ -147,7 +147,8 @@ namespace srt::g2p
147147
bool operator!=(const iterator &RHS) const { return !(*this == RHS); }
148148

149149
private:
150-
iterator(const char *buf, const void *row, const void *col) : _buf(buf), _row(row), _col(col) {}
150+
iterator(const char *buf, const void *row, const void *col, int var = 0)
151+
: _buf(buf), _row(row), _col(col), _var(var) {}
151152

152153
SRT_G2P_EXPORT void fetch() const;
153154
SRT_G2P_EXPORT void next();
@@ -156,6 +157,10 @@ namespace srt::g2p
156157

157158
const char *_buf;
158159
const void *_row, *_col;
160+
// Variant index within the merged entry group. 0 for plain keys and
161+
// range iterators; set by find() when a suffixed key ("word(2)")
162+
// resolves to a specific variant. Reset to 0 by next()/prev().
163+
int _var;
159164
mutable std::optional<std::pair<const char *, PhonemeList>> _copy;
160165

161166
friend class PhonemeDict;
@@ -164,10 +169,23 @@ namespace srt::g2p
164169
using reverse_iterator = stdc::reverse_iterator<iterator>;
165170

166171
/// \note The key must be a null-terminated string.
172+
///
173+
/// Dictionaries may carry CMU-style variant keys like "word(1)"/"word(2)":
174+
/// the "(n)" suffix is stripped at load time and all rows sharing the same
175+
/// base word are merged into one variant group (bare base row first, then
176+
/// variants in file order). find/contains/operator[] on a base key return
177+
/// the first variant; on a suffixed key they fall back to the variant group
178+
/// and return exactly the variant whose suffix number matches (D4 decision:
179+
/// precise suffixed lookup returns that single pronunciation, no candidates).
167180
iterator find(const char *key) const;
168181
bool contains(const char *key) const;
169182
PhonemeList operator[](const char *key) const;
170183

184+
/// Returns every pronunciation of a (base) key in file order, as
185+
/// lightweight views into the dictionary buffer (zero-copy).
186+
/// For a suffixed key like "word(2)", returns only the matching variant.
187+
std::vector<PhonemeList> lookupAll(const char *key) const;
188+
171189
bool empty() const;
172190
size_t size() const;
173191

‎lib/G2P/Support/PhonemeDict.cpp‎

Lines changed: 172 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,37 @@
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

911
namespace 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

Comments
 (0)