载入中...
搜索中...
未找到
ConversationLocalization.cpp
浏览该文件的文档.
3
4#include <cerrno>
5#include <cstdlib>
6
7namespace eve::dialogue {
8namespace {
9
10using CsvRows = std::vector<std::vector<std::string>>;
11
12std::string trim(std::string value) {
13 while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || value.back() == ' ')) value.pop_back();
14 size_t begin = 0;
15 while (begin < value.size() && value[begin] == ' ') ++begin;
16 return value.substr(begin);
17}
18
19bool parseCsv(const std::string& source, CsvRows& rows, std::string& error) {
20 std::vector<std::string> row;
21 std::string field;
22 bool quoted = false;
23 for (size_t i = 0; i <= source.size(); ++i) {
24 const char ch = i < source.size() ? source[i] : '\n';
25 if (quoted) {
26 if (ch == '"' && i + 1 < source.size() && source[i + 1] == '"') {
27 field.push_back('"');
28 ++i;
29 } else if (ch == '"')
30 quoted = false;
31 else
32 field.push_back(ch);
33 } else if (ch == '"' && field.empty())
34 quoted = true;
35 else if (ch == ',') {
36 row.push_back(std::move(field));
37 field.clear();
38 } else if (ch == '\n') {
39 row.push_back(trim(std::move(field)));
40 field.clear();
41 if (!(row.size() == 1 && row.front().empty())) rows.push_back(std::move(row));
42 row.clear();
43 } else if (ch != '\r')
44 field.push_back(ch);
45 }
46 if (quoted) {
47 error = "unterminated quoted CSV field";
48 return false;
49 }
50 if (!rows.empty() && !rows[0].empty() && rows[0][0].rfind("\xEF\xBB\xBF", 0) == 0) rows[0][0].erase(0, 3);
51 return true;
52}
53
54std::string quote(std::string value) {
55 size_t position = 0;
56 while ((position = value.find('"', position)) != std::string::npos) {
57 value.insert(position, 1, '"');
58 position += 2;
59 }
60 return '"' + value + '"';
61}
62
63std::string stableKey(const eve::dnut::SequenceAsset& asset, const eve::dnut::SequenceNode& node) {
64 const std::string key = sequencePayloadString(node, "i18n");
65 return key.empty() ? asset.id + "." + node.id : key;
66}
67
68std::string mapKey(const std::string& key, const std::string& locale) { return locale + '\x1f' + key; }
69
70} // namespace
71
72int ConversationLocalizationCatalog::importCsv(const std::string& csv, const std::string& defaultLocale,
73 std::vector<ConversationDiagnostic>& diagnostics) {
74 CsvRows rows;
75 std::string error;
76 if (!parseCsv(csv, rows, error) || rows.empty()) {
77 diagnostics.push_back({ConversationDiagnostic::Severity::Error, "<localization.csv>", 0,
78 error.empty() ? "localization CSV is empty" : error});
79 return 0;
80 }
81 std::unordered_map<std::string, size_t> columns;
82 for (size_t i = 0; i < rows.front().size(); ++i) columns[rows.front()[i]] = i;
83 if (!columns.count("i18n_key")) {
84 diagnostics.push_back({ConversationDiagnostic::Severity::Error, "<localization.csv>", 1,
85 "localization CSV requires an i18n_key column"});
86 return 0;
87 }
88 const auto cell = [&](const std::vector<std::string>& row, const char* name) -> std::string {
89 const auto it = columns.find(name);
90 return it != columns.end() && it->second < row.size() ? row[it->second] : std::string{};
91 };
92 int imported = 0;
93 for (size_t line = 1; line < rows.size(); ++line) {
94 const std::string key = cell(rows[line], "i18n_key");
95 std::string locale = cell(rows[line], "locale");
96 if (locale.empty()) locale = defaultLocale;
97 if (key.empty()) {
98 diagnostics.push_back({ConversationDiagnostic::Severity::Warning, "<localization.csv>",
99 static_cast<int>(line + 1), "row has no i18n_key and was ignored"});
100 continue;
101 }
102 auto& entry = entries_[mapKey(key, locale)];
103 std::string text = cell(rows[line], "translation");
104 if (text.empty()) text = cell(rows[line], "text");
105 if (!text.empty()) entry.text = std::move(text);
106 const std::string voice = cell(rows[line], "voice");
107 if (!voice.empty()) entry.voice = voice;
108 const std::string status = cell(rows[line], "status");
109 if (!status.empty()) entry.status = status;
110 const std::string duration = cell(rows[line], "duration");
111 if (!duration.empty()) {
112 char* end = nullptr;
113 errno = 0;
114 const double value = std::strtod(duration.c_str(), &end);
115 if (end == duration.c_str() + duration.size() && errno != ERANGE)
116 entry.duration = value;
117 else
118 diagnostics.push_back({ConversationDiagnostic::Severity::Warning, "<localization.csv>",
119 static_cast<int>(line + 1), "invalid duration was ignored"});
120 }
121 ++imported;
122 }
123 return imported;
124}
125
126const ConversationLocalizationEntry* ConversationLocalizationCatalog::find(const std::string& key,
127 const std::string& locale) const {
128 const auto exact = entries_.find(mapKey(key, locale));
129 if (exact != entries_.end()) return &exact->second;
130 const size_t separator = locale.find_first_of("-_");
131 if (separator != std::string::npos) {
132 const auto language = entries_.find(mapKey(key, locale.substr(0, separator)));
133 if (language != entries_.end()) return &language->second;
134 }
135 const auto fallback = entries_.find(mapKey(key, {}));
136 return fallback == entries_.end() ? nullptr : &fallback->second;
137}
138
139std::string ConversationLocalizationCatalog::resolveText(const std::string& key, const std::string& locale,
140 const std::string& fallback) const {
141 const auto* entry = find(key, locale);
142 return entry && !entry->text.empty() ? entry->text : fallback;
143}
144
145std::string ConversationLocalizationCatalog::resolveVoice(const std::string& key, const std::string& locale,
146 const std::string& fallback) const {
147 const auto* entry = find(key, locale);
148 return entry && !entry->voice.empty() ? entry->voice : fallback;
149}
150
151std::string ConversationLocalizationCatalog::resolveStatus(const std::string& key, const std::string& locale) const {
152 const auto* entry = find(key, locale);
153 return entry ? entry->status : std::string{};
154}
155
156double ConversationLocalizationCatalog::resolveDuration(const std::string& key, const std::string& locale) const {
157 const auto* entry = find(key, locale);
158 return entry ? entry->duration : 0.0;
159}
160
161std::string ConversationLocalizationCatalog::exportMissingCsv(const std::vector<eve::dnut::SequenceAsset>& assets,
162 const std::string& locale) const {
163 std::string out = "conversation_id,node_id,i18n_key,locale,source_text,translation\r\n";
164 for (const auto& asset : assets)
165 for (const auto& node : asset.nodes) {
166 if (node.type != "line") continue;
167 const std::string key = stableKey(asset, node);
168 const auto* entry = find(key, locale);
169 if (entry && !entry->text.empty()) continue;
170 out += quote(asset.id) + ',' + quote(node.id) + ',' + quote(key) + ',' + quote(locale) + ',' +
171 quote(sequencePayloadString(node, "text")) + ",\"\"\r\n";
172 }
173 return out;
174}
175
177 const std::vector<eve::dnut::SequenceAsset>& assets, const std::string& locale) const {
178 std::string out =
179 "conversation_id,node_id,i18n_key,locale,speaker,source_text,translation,voice,status,duration\r\n";
180 for (const auto& asset : assets)
181 for (const auto& node : asset.nodes) {
182 if (node.type != "line") continue;
183 const std::string key = stableKey(asset, node);
184 const auto* entry = find(key, locale);
185 out += quote(asset.id) + ',' + quote(node.id) + ',' + quote(key) + ',' + quote(locale) + ',' +
186 quote(sequencePayloadString(node, "speaker")) + ',' + quote(sequencePayloadString(node, "text")) +
187 ',' + quote(entry ? entry->text : std::string{}) + ',' +
188 quote(entry && !entry->voice.empty() ? entry->voice : sequencePayloadString(node, "voice")) + ',' +
189 quote(entry ? entry->status : std::string{}) + ',' +
190 quote(entry && entry->duration > 0.0 ? std::to_string(entry->duration) : std::string{}) + "\r\n";
191 }
192 return out;
193}
194
195} // namespace eve::dialogue
double value
float duration
Dialogue payload codecs for L1 sequence assets.
int rows
std::uint32_t key
wgpu::PopErrorScopeStatus status
std::string text
std::string imported
char quote
std::array< float, 3 > position
std::string name
std::string error
Definition Package.cpp:60
float begin
const RoadNode * node
Cell cell
int columns
const UnitySourceAsset & source
std::string resolveText(const std::string &key, const std::string &locale, const std::string &fallback) const
Resolve text.
std::string exportVoiceRecordingCsv(const std::vector< eve::dnut::SequenceAsset > &assets, const std::string &locale) const
Export voice recording csv.
int importCsv(const std::string &csv, const std::string &defaultLocale, std::vector< ConversationDiagnostic > &diagnostics)
Import csv.
double resolveDuration(const std::string &key, const std::string &locale) const
Resolve duration.
std::string resolveVoice(const std::string &key, const std::string &locale, const std::string &fallback) const
Resolve voice.
std::string exportMissingCsv(const std::vector< eve::dnut::SequenceAsset > &assets, const std::string &locale) const
Export missing csv.
std::string resolveStatus(const std::string &key, const std::string &locale) const
Resolve status.
std::string sequencePayloadString(const eve::dnut::SequenceNode &node, std::string_view key)
Read an optional string from a node payload; missing or ill-typed values yield empty.
const EditorValue * field(const EditorValue &value, const char *name)
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:679
Runtime translation and voice-production row keyed by stable key and locale.
Immutable, parameterized and versioned compiled sequence.
One immutable step of a compiled sequence.