#include "vox_parser.hpp" #include #include #include #include #include static inline std::wstring trim(const std::wstring& s){ size_t a=0,b=s.size(); while(aa && iswspace(s[b-1])) --b; return s.substr(a,b-a); } static inline std::wstring to_lower(std::wstring s){ for(auto& c: s) c=(wchar_t)towlower(c); return s; } static inline bool is_all_digits(const std::wstring& t){ if(t.empty()) return false; for(wchar_t c: t) if(!iswdigit(c)) return false; return true; } static inline bool is_titlecase_word(const std::wstring& t){ if(t.empty()) return false; if(!iswalpha(t[0]) || !iswupper(t[0])) return false; return true; } static inline std::wstring strip_trailing_punct(std::wstring t){ while(!t.empty()){ wchar_t c = t.back(); if(c==L','||c==L'.'||c==L';'||c==L':'||c==L'!'||c==L'?'||c==L'"'||c==L'\'') t.pop_back(); else break; } return t; } static inline bool ends_with_br(const std::wstring& s){ size_t n = s.size(); return n >= 5 && s.compare(n-5, 5, L" \\!br") == 0; } // Sentence splitter (keeps terminator punctuation attached) static std::vector split_sentences(const std::wstring& in){ std::vector out; std::wstring cur; for(size_t i=0;i “thee” // - Backward rule: after preps (to/for/of/in/on/at) or "on behalf of" // - NEW Forward rule: before TitleCase, single letter (X or X:), or a number // Robust across \!br tags; preserves trailing punctuation. static void apply_thee_rule(std::wstring& s){ // tokenize by whitespace; keep tags as tokens std::wstringstream ss(s); std::vector tok; std::wstring t; while (ss >> t) tok.push_back(t); if (tok.empty()) return; auto is_tag = [](const std::wstring& w){ return (w==L"\\!br" || w==L"\\!wH1" || w==L"\\!wH0"); }; auto to_lower_copy = [](std::wstring v){ for (auto& c: v) c=(wchar_t)towlower(c); return v; }; // split trailing punctuation; return (core, punctTail) auto split_tail = [](const std::wstring& x)->std::pair{ std::wstring core=x, tail; while (!core.empty()){ wchar_t c = core.back(); if (c==L','||c==L'.'||c==L';'||c==L':'||c==L'!'||c==L'?'||c==L'"'||c==L'\''){ tail.insert(tail.begin(), c); core.pop_back(); } else break; } return {core, tail}; }; auto clean_lower = [&](const std::wstring& x)->std::wstring{ auto p = split_tail(x); return to_lower_copy(p.first); }; auto bare = [&](const std::wstring& x)->std::wstring{ return split_tail(x).first; }; auto prev_word = [&](int i, int nth)->std::wstring{ for (int k=i-1; k>=0; --k){ if (is_tag(tok[k])) continue; if (--nth==0) return clean_lower(tok[k]); } return L""; }; auto next_word = [&](int i, int nth)->std::wstring{ for (int k=i+1; k<(int)tok.size(); ++k){ if (is_tag(tok[k])) continue; if (--nth==0) return tok[k]; // not lowered; we want case info } return L""; }; auto is_prep = [&](const std::wstring& w)->bool{ std::wstring tl = to_lower_copy(w); return (tl==L"to"||tl==L"for"||tl==L"of"||tl==L"in"||tl==L"on"||tl==L"at"); }; auto is_title = [&](const std::wstring& w)->bool{ auto b = bare(w); return !b.empty() && iswalpha(b[0]) && iswupper(b[0]); }; auto is_single_letter = [&](const std::wstring& w)->bool{ auto b = bare(w); if (b.size()==1 && iswalpha(b[0]) && iswupper(b[0])) return true; if (b.size()==2 && iswalpha(b[0]) && iswupper(b[0]) && b[1]==L':') return true; return false; }; auto is_number = [&](const std::wstring& w)->bool{ auto b = bare(w); if (b.empty()) return false; for (wchar_t c: b) if (!iswdigit(c)) return false; return true; }; for (int i=0; i<(int)tok.size(); ++i){ if (is_tag(tok[i])) continue; auto [core, tail] = split_tail(tok[i]); std::wstring cl = to_lower_copy(core); if (cl != L"the") { tok[i] = core + tail; continue; } bool make_thee = false; // Backward (prepositions, incl. "on behalf of") std::wstring p1 = prev_word(i, 1); if (!p1.empty()){ if (is_prep(p1)) make_thee = true; else if (to_lower_copy(p1) == L"of"){ std::wstring p2 = prev_word(i, 2); std::wstring p3 = prev_word(i, 3); if (to_lower_copy(p2)==L"behalf" && to_lower_copy(p3)==L"on") make_thee = true; } } // Forward (before proper names / letters / numbers) if (!make_thee){ std::wstring n1 = next_word(i, 1); if (!n1.empty()){ if (is_title(n1) || is_single_letter(n1) || is_number(n1)){ make_thee = true; } } } tok[i] = (make_thee ? L"thee" : core) + tail; } // rebuild std::wstring out; for (size_t i=0;i break before the hour s = std::regex_replace(s, std::wregex(LR"((\b[Tt]he\s+time\s+is)\s+(\d+))"), L"$1 \\!br $2"); // 24h “HH00 hours” -> “HH \!br hundred \!br hours” s = std::regex_replace(s, std::wregex(LR"(\b(\d{2})00\s+hours\b)", std::regex_constants::icase), L"$1 \\!br hundred \\!br hours"); // Three-digit non-round numbers: 105 -> “100 \!br and \!br 5” (strip leading zero in remainder) { std::wregex n3(LR"(\b([1-9])(\d)(\d)\b)"); std::wstring out; out.reserve(s.size()+32); std::wsregex_iterator it(s.begin(), s.end(), n3), end; size_t last = 0; for(; it!=end; ++it){ auto m = *it; std::wstring yz = m.str(2)+m.str(3); if (yz == L"00") continue; // round, leave as-is size_t nz = 0; while (nz < yz.size() && yz[nz]==L'0') ++nz; std::wstring rem = yz.substr(nz); out.append(s, last, m.position()-last); out += m.str(1)+L"00 \\!br and \\!br " + (rem.empty()? yz : rem); last = m.position() + m.length(); } out.append(s, last, std::wstring::npos); s.swap(out); } // Degrees: “93 degrees” -> “93 \!br degrees \!br” s = std::regex_replace(s, std::wregex(LR"(\b(\d+)\s+degrees\b)", std::regex_constants::icase), L"$1 \\!br degrees \\!br"); // Fix any lingering "M:." -> "M:" (meridiem shouldn’t carry a period) s = std::regex_replace(s, std::wregex(LR"(M:\s*\.)"), L"M:"); } // Syllable heuristic static int syllables(const std::wstring& w){ if(w.empty()) return 0; std::wstring s; s.reserve(w.size()); for(wchar_t c: w) s.push_back((wchar_t)towlower(c)); auto isv=[&](wchar_t c){ return c==L'a'||c==L'e'||c==L'i'||c==L'o'||c==L'u'||c==L'y'; }; int count=0; bool in=false; for(wchar_t c: s){ if(isv(c)){ if(!in){ ++count; in=true; } } else in=false; } if(count>1 && s.size()>1 && s.back()==L'e' && !(s.size()>2 && s[s.size()-2]==L'l')) --count; // silent 'e' (not "...le") if(s.size()>2 && s.back()==L'e' && s[s.size()-2]==L'l') ++count; // “…le” return std::max(1,count); } enum Wt { LIGHT, MEDIUM, HEAVY }; static bool is_stop(const std::wstring& tl){ static const wchar_t* sw[] = { L"the",L"thee",L"a",L"an",L"to",L"for",L"of",L"in",L"on",L"at",L"by",L"with",L"from", L"as",L"is",L"are",L"was",L"were",L"this",L"that" }; for(auto* w: sw) if(tl==w) return true; return false; } static bool is_lightverb(const std::wstring& tl){ static const wchar_t* vw[] = { L"welcome",L"wait",L"stand",L"provide",L"provided",L"return",L"remain",L"maintain", L"maintained",L"verify",L"contact",L"board",L"arriving",L"inbound",L"bound", L"commence",L"commences" }; for(auto* w: vw) if(tl==w) return true; return false; } static bool is_unit(const std::wstring& tl){ static const wchar_t* uw[] = { L"degrees", L"hours", L"percent", L"%" }; for(auto* w: uw) if(tl==w) return true; return false; } static Wt weight(const std::wstring& tok){ if(tok.empty()) return LIGHT; std::wstring pfree = strip_trailing_punct(tok); std::wstring tl = to_lower(pfree); if(is_stop(tl) || is_lightverb(tl)) return LIGHT; if(is_all_digits(pfree)) return HEAVY; if(pfree.find(L'-')!=std::wstring::npos) return HEAVY; if(is_titlecase_word(pfree)) return HEAVY; if(is_unit(tl)) return HEAVY; int syl = syllables(pfree); if(syl>=3 || pfree.size()>=8) return HEAVY; // long or many-syllable content words return MEDIUM; } static bool is_and(const std::wstring& t){ return to_lower(strip_trailing_punct(t))==L"and"; } static bool starts_with_punct(const std::wstring& t){ if(t.empty()) return false; wchar_t c = t.front(); return (c==L'.'||c==L','||c==L';'||c==L':'||c==L'!'||c==L'?'||c==L')'); } static bool is_open_punct(const std::wstring& t){ if(t.empty()) return false; wchar_t c=t.front(); return (c==L'('||c==L'"'||c==L'\''); } // Tokenize on whitespace (keep punctuation with token) static std::vector tokenize_ws(const std::wstring& s){ std::wstringstream ss(s); std::vector v; std::wstring t; while(ss >> t) v.push_back(t); return v; } // Insert TitleCase run breaks and Area/Level/Sector blocks static void emit_titlecase_run(const std::vector& v, size_t i, size_t j, std::wstring& out){ // place a break before the run if(!out.empty() && !ends_with_br(out)) out += L" \\!br"; for(size_t k=i;kbool{ std::wstring b = bare(w); if (b.size()==1 && iswalpha(b[0]) && iswupper(b[0])) return true; // C if (b.size()==2 && iswalpha(b[0]) && iswupper(b[0]) && b[1]==L':') return true; // C: return false; }; // split trailing punctuation; return (core, punctTail) auto split_tail = [](const std::wstring& x)->std::pair{ std::wstring core=x, tail; while (!core.empty()){ wchar_t c = core.back(); if (c==L','||c==L'.'||c==L';'||c==L':'||c==L'!'||c==L'?'||c==L'"'||c==L'\''){ tail.insert(tail.begin(), c); core.pop_back(); } else break; } return {core, tail}; }; // emit hyphen/dash-split parts with breaks between them; keep tail on last part auto emit_dash_split = [&](const std::wstring& tok)->bool{ auto [core, tail] = split_tail(tok); // find internal separators std::vector cuts; for(size_t p=0;p0 && p+1i+1){ emit_titlecase_run(v,i,j,out); i=j; reset_after_break(); continue; } } if (is_titlecase_word(bare(v[i]))){ size_t j=i; while(j=i+2){ emit_titlecase_run(v,i,j,out); i=j; reset_after_break(); continue; } } // ---- "and" between heavier items ---- if(is_and(v[i]) && i>0 && i+1=MEDIUM && rw>=MEDIUM){ if(!ends_with_br(out)) out += L" \\!br"; out += L" and \\!br"; ++i; reset_after_break(); continue; } } // ---- Standalone dash ( -, –, — ) => hard boundary ---- if (v[i]==L"-" || v[i]==L"\u2013" || v[i]==L"\u2014"){ if(!ends_with_br(out)) out += L" \\!br"; reset_after_break(); ++i; continue; } // ---- Symmetric linker isolation: [BR] linker [BR] ---- { std::wstring tok = v[i]; std::wstring tl = lower(bare(tok)); bool in_head_span = (i < 2); if (!in_head_span && (tl==L"at" || tl==L"in" || tl==L"on" || tl==L"to" || tl==L"of" || tl==L"for"|| tl==L"from"|| tl==L"by")){ if(!ends_with_br(out)) out += L" \\!br"; // BEFORE linker reset_after_break(); if(!out.empty() && !starts_with_punct(tok) && !is_open_punct(tok)) out.push_back(L' '); out += tok; if(!ends_with_br(out)) out += L" \\!br"; // AFTER linker reset_after_break(); ++i; continue; } } // ---- Pre-copula break: put a beat BEFORE "is" when followed by determiner/contenty ---- { std::wstring tl_is = to_lower(bare(v[i])); bool in_head_span = (i < 2); auto is_tag_local = [&](const std::wstring& w){ return (w==L"\\!br" || w==L"\\!wH1" || w==L"\\!wH0"); }; auto is_single_letter = [&](const std::wstring& w)->bool{ std::wstring b = strip_trailing_punct(w); if (b.size()==1 && iswalpha(b[0]) && iswupper(b[0])) return true; // C if (b.size()==2 && iswalpha(b[0]) && iswupper(b[0]) && b[1]==L':') return true; // C: return false; }; auto is_determiner = [&](const std::wstring& w)->bool{ std::wstring b = to_lower(strip_trailing_punct(w)); return (b==L"the"||b==L"thee"||b==L"a"||b==L"an"|| b==L"this"||b==L"that"||b==L"these"||b==L"those"|| b==L"some"||b==L"any"||b==L"each"||b==L"every"||b==L"no"); }; if (!in_head_span && tl_is == L"is") { // peek next non-tag token size_t k = i + 1; while (k < v.size() && is_tag_local(v[k])) ++k; bool trigger = false; if (k < v.size()) { const std::wstring& n1 = v[k]; if (is_determiner(n1) || is_titlecase_word(strip_trailing_punct(n1)) || is_all_digits(strip_trailing_punct(n1)) || is_single_letter(n1) || weight(n1) == HEAVY) { trigger = true; } } if (trigger && !ends_with_br(out)) { out += L" \\!br"; // reset run state like a real boundary since_break_all_light = true; content_run = 0; prev_w = LIGHT; } } } // ---- Hyphenated / en-dash / em-dash compounds inside token ---- if (emit_dash_split(v[i])) { ++i; continue; } // ---- Generic token logic ---- std::wstring tok = v[i]; Wt w_eff = weight(tok); // effective weight; may be softened at sentence start // Soft-start: first two tokens—avoid pre-breaks unless necessary. bool in_head_span = (i < 2); if (i==0 && is_titlecase_word(bare(tok))) { bool next_is_title = (i+1 < v.size()) && is_titlecase_word(bare(v[i+1])); if (!next_is_title && w_eff == HEAVY) w_eff = MEDIUM; // e.g., "Good evening" } // ---- HEAVY-GATE ---- if (w_eff==HEAVY && content_run >= 1){ int k = static_cast(i) - 1; while (k >= 0 && is_tag(v[k])) --k; bool exempt = false; if (k >= 0){ std::wstring prev = v[k]; if (is_titlecase_word(bare(prev)) || is_all_digits(bare(prev)) || is_single_letter(prev)) { exempt = true; } } if (!exempt){ if(!ends_with_br(out)) out += L" \\!br"; reset_after_break(); } } // 3-beat limiter if (w_eff==HEAVY && content_run >= 2){ if(!ends_with_br(out)) out += L" \\!br"; reset_after_break(); } // Heavy after only LIGHT (but not inside head span) if (w_eff==HEAVY && since_break_all_light && !in_head_span){ if(!out.empty() && !ends_with_br(out)) out += L" \\!br"; reset_after_break(); } // Heavy-Heavy adjacency (outside TitleCase run) if (i>0 && prev_w==HEAVY && w_eff==HEAVY && !in_head_span){ if(!ends_with_br(out)) out += L" \\!br"; reset_after_break(); } // Append token with spacing if(!out.empty() && !starts_with_punct(tok) && !is_open_punct(tok)) out.push_back(L' '); out += tok; // Punctuation boundary: break after ',' or ';' if (!tok.empty()){ wchar_t last = tok.back(); if ((last==L',' || last==L';') && !ends_with_br(out)){ out += L" \\!br"; reset_after_break(); ++i; continue; } } // Update run stats if (w_eff==LIGHT){ // remain in light span }else{ ++content_run; // MEDIUM or HEAVY since_break_all_light = false; } prev_w = w_eff; ++i; } return trim(out); } // Tidy spaces and tags static std::wstring tidy(const std::wstring& s){ std::wstring t = std::regex_replace(s, std::wregex(LR"(\s+)"), L" "); // collapse spaces t = std::regex_replace(t, std::wregex(LR"(\s*\\!br\s*)"), L" \\!br "); // normalize tag spacing t = std::regex_replace(t, std::wregex(LR"((\s*\\!br\s*){2,})"), L" \\!br "); // collapse multiple breaks t = std::regex_replace(t, std::wregex(LR"(\s+([.,!?;:]))"), L"$1"); // no space before punctuation // trimyou size_t a=0,b=t.size(); while(aa && iswspace(t[b-1])) --b; return t.substr(a,b-a); } // Convert standalone letters into stable "letter tokens" for FlexTalk. // - "A:" / "a:" / bare "A"/"a" near a boundary -> "Ay:" // - any single UPPERCASE letter near a boundary -> "X:" (exactly one colon) // Boundaries are start/end or a \!br tag on either side. We preserve trailing punctuation. static void normalize_letter_tokens(std::wstring& s){ // split by whitespace; keep tags as tokens std::wstringstream ss(s); std::vector tok; std::wstring t; while (ss >> t) tok.push_back(t); if (tok.empty()) return; auto is_tag = [](const std::wstring& w){ return (w==L"\\!br" || w==L"\\!wH1" || w==L"\\!wH0"); }; // strip trailing punctuation, but lift out a single trailing ':' as a flag auto split_tail = [](const std::wstring& x)->std::tuple{ std::wstring core = x, punct; bool had_colon = false; // peel general punctuation, but remember a single trailing ':' while(!core.empty()){ wchar_t c = core.back(); if(c==L':' && !had_colon){ had_colon = true; core.pop_back(); continue; } if(c==L','||c==L'.'||c==L';'||c==L'!'||c==L'?'||c==L'"'||c==L'\''){ punct.insert(punct.begin(), c); core.pop_back(); continue; } break; } return {core, had_colon, punct}; }; auto is_boundary = [&](int idx)->bool{ return (idx < 0 || idx >= (int)tok.size() || is_tag(tok[idx])); }; for (int i=0;i<(int)tok.size();++i){ if (is_tag(tok[i])) continue; auto [coreOrig, had_colon, tail] = split_tail(tok[i]); std::wstring core = coreOrig; const bool near_left = is_boundary(i-1); const bool near_right = is_boundary(i+1); const bool near_bound = (near_left || near_right); // Normalize A / a cases to "Ay:" when isolated or already written as A: if ((core==L"A" || core==L"a") && (near_bound || had_colon)){ core = L"Ay"; had_colon = true; // ensure exactly one colon added below } // Any other single UPPERCASE letter becomes a letter token near a boundary else if (core.size()==1 && iswalpha(core[0]) && iswupper(core[0]) && near_bound){ // ensure exactly one colon // (if there was already one as trailing punct, we won't add another) // special "A" handled above had_colon = true; } // Reassemble: add colon iff had_colon is true, then the rest of punctuation std::wstring rebuilt = core; if (had_colon) rebuilt.push_back(L':'); rebuilt += tail; tok[i] = rebuilt; } // Rebuild s (tidy() will normalize spaces/tags later) std::wstring out; for (size_t i=0;i