1 package com.acumenvelocity.ath.steps;
2
3 import java.util.ArrayList;
4 import java.util.Iterator;
5 import java.util.List;
6 import java.util.Spliterator;
7 import java.util.Spliterators;
8 import java.util.stream.StreamSupport;
9
10 import com.acumenvelocity.ath.common.AthUtil;
11 import com.acumenvelocity.ath.common.Const;
12 import com.acumenvelocity.ath.common.ControllerUtil;
13 import com.acumenvelocity.ath.common.Log;
14 import com.acumenvelocity.ath.common.OkapiUtil;
15 import com.acumenvelocity.ath.common.OriginalTuAnnotation;
16 import com.acumenvelocity.ath.model.MtResources;
17 import com.acumenvelocity.ath.mt.confidence.ConfidenceScoredTranslation;
18 import com.acumenvelocity.ath.mt.confidence.HybridDataStructures.HybridEvaluationResult;
19 import com.acumenvelocity.ath.mt.confidence.HybridDataStructures.HybridScoredSegment;
20 import com.acumenvelocity.ath.mt.confidence.HybridDataStructures.HybridTranslationScore;
21 import com.acumenvelocity.ath.mt.confidence.HybridQualityEstimator;
22
23 import net.sf.okapi.common.Event;
24 import net.sf.okapi.common.IResource;
25 import net.sf.okapi.common.Util;
26 import net.sf.okapi.common.annotation.AltTranslation;
27 import net.sf.okapi.common.annotation.AltTranslationsAnnotation;
28 import net.sf.okapi.common.query.MatchType;
29 import net.sf.okapi.common.resource.ISegments;
30 import net.sf.okapi.common.resource.ITextUnit;
31 import net.sf.okapi.common.resource.Segment;
32 import net.sf.okapi.common.resource.TextContainer;
33 import net.sf.okapi.common.resource.TextFragment;
34 import net.sf.okapi.common.resource.TextFragmentUtil;
35 import net.sf.okapi.lib.translation.QueryUtil;
36
37
38
39
40
41
42
43
44
45 public class MtConfidenceScoringStep extends BaseTuBatchProcessingStep {
46
47 private final List<MtResources> mtCustomResources;
48 private final List<MtResources> modelConfigs = new ArrayList<>();
49 private final List<String> sourceTexts = new ArrayList<>();
50 private final List<TextFragment> sourceTfs = new ArrayList<>();
51 private final List<SegmentInfo> segmentInfos = new ArrayList<>();
52 private final boolean mtSendPlainText;
53 private final QueryUtil qutil = new QueryUtil();
54
55 private HybridEvaluationResult results;
56
57
58
59
60
61
62 public MtConfidenceScoringStep(List<MtResources> mtCustomResources, boolean mtSendPlainText) {
63 super();
64 this.mtCustomResources = mtCustomResources != null ? mtCustomResources : new ArrayList<>();
65 this.mtSendPlainText = mtSendPlainText;
66 }
67
68 public String getName() {
69 return "MT Confidence Scoring";
70 }
71
72 @Override
73 public String getDescription() {
74 return "Machine translates source segments using Google Cloud Translate API v3 "
75 + "(NMT, Translation LLM) and custom AutoML models/glossaries, then uses "
76 + "Google Vertex AI evaluation service and heuristics to calculate confidence "
77 + "scores for those translations.";
78 }
79
80 private String buildGeneralModelId(String modelId) {
81 return String.format("projects/%s/locations/%s/models/%s", ControllerUtil.getProjectId(),
82 Const.ATH_GCP_PROJECT_LOCATION, modelId);
83 }
84
85
86
87
88 private void initializeModelConfigs() {
89 modelConfigs.clear();
90
91
92
93
94 MtResources nmt = new MtResources();
95
96 nmt.setMtModelId(buildGeneralModelId("general/nmt"));
97 modelConfigs.add(nmt);
98
99
100 MtResources translationLlm = new MtResources();
101
102 translationLlm.setMtModelId(buildGeneralModelId("general/translation-llm"));
103 modelConfigs.add(translationLlm);
104
105
106 if (!Util.isEmpty(mtCustomResources)) {
107 modelConfigs.addAll(mtCustomResources);
108 }
109
110 Log.info(MtConfidenceScoringStep.class,
111 "Initialized {} model configurations (2 built-in + {} custom)",
112 modelConfigs.size(), mtCustomResources != null ? mtCustomResources.size() : 0);
113 }
114
115
116
117
118 private void preProcessTextUnit(ITextUnit tu) {
119 TextContainer source = tu.getSource();
120
121 if (source == null) {
122 Log.error(getClass(), "Source of TU '{}' is null", tu.getId());
123 return;
124 }
125
126 TextContainer target = tu.getTarget(getTargetLocale());
127 ISegments targetSegments = target == null ? null : target.getSegments();
128
129
130 for (Segment segment : source.getSegments()) {
131 TextFragment srcTf = segment.getContent();
132
133
134 if (srcTf == null || srcTf.isEmpty()) {
135 Log.trace(getClass(), "Skipping empty segment in TU '{}'", tu.getId());
136 continue;
137 }
138
139 if (targetSegments != null) {
140 Segment tseg = targetSegments.get(segment.getId());
141
142 if (tseg != null && tseg.getContent() != null && !tseg.getContent().isEmpty()) {
143
144 continue;
145 }
146 }
147
148 sourceTfs.add(srcTf.clone());
149 }
150
151 if (mtSendPlainText) {
152
153
154 tu.setAnnotation(new OriginalTuAnnotation(tu.clone(), getSourceLocale()));
155 OkapiUtil.removeCodes(tu, true);
156 }
157
158
159 for (Segment segment : source.getSegments()) {
160 TextFragment srcTf = segment.getContent();
161
162
163 if (srcTf == null || srcTf.isEmpty()) {
164 Log.trace(getClass(), "Skipping empty segment in TU '{}'", tu.getId());
165 continue;
166 }
167
168 if (targetSegments != null) {
169 Segment tseg = targetSegments.get(segment.getId());
170
171 if (tseg != null && tseg.getContent() != null && !tseg.getContent().isEmpty()) {
172
173 continue;
174 }
175 }
176
177 String sourceText = null;
178
179 if (mtSendPlainText) {
180 sourceText = srcTf.getText();
181
182 } else {
183 sourceText = qutil.toCodedHTML(srcTf);
184 }
185
186 sourceTexts.add(sourceText);
187
188
189 segmentInfos.add(new SegmentInfo(tu, segment.getId()));
190
191 Log.trace(getClass(), "Collected segment [{}]: '{}'",
192 sourceTexts.size() - 1, sourceText);
193 }
194 }
195
196
197
198
199 private void postProcessTextUnits(HybridEvaluationResult results) {
200 if (results == null || results.getSegments().isEmpty()) {
201 Log.warn(getClass(), "No evaluation results available");
202 return;
203 }
204
205 List<HybridScoredSegment> scoredSegments = results.getSegments();
206
207 if (scoredSegments.size() != segmentInfos.size()) {
208 Log.error(getClass(),
209 "Mismatch: {} scored segments but {} segment infos",
210 scoredSegments.size(), segmentInfos.size());
211
212 return;
213 }
214
215
216 for (int i = 0; i < scoredSegments.size(); i++) {
217 HybridScoredSegment scoredSegment = scoredSegments.get(i);
218 SegmentInfo segInfo = segmentInfos.get(i);
219
220 ITextUnit tu = segInfo.textUnit;
221 String segmentId = segInfo.segmentId;
222
223
224 TextContainer target = tu.getTarget(getTargetLocale());
225
226 if (target == null) {
227 target = tu.createTarget(getTargetLocale(), false, IResource.COPY_SEGMENTATION);
228 Log.trace(getClass(), "Created target container for TU '{}'", tu.getId());
229 }
230
231
232 Segment targetSegment = target.getSegments().get(segmentId);
233
234 if (targetSegment == null) {
235 targetSegment = new Segment(segmentId);
236 target.append(targetSegment);
237 Log.trace(getClass(), "Created target segment '{}' in TU '{}'", segmentId, tu.getId());
238 }
239
240
241 List<HybridTranslationScore> scores = scoredSegment.getScores();
242
243 if (scores.isEmpty()) {
244 Log.warn(getClass(), "No translations available for segment {} in TU '{}'",
245 i, tu.getId());
246
247 continue;
248 }
249
250
251 AltTranslationsAnnotation ata = targetSegment.getAnnotation(
252 AltTranslationsAnnotation.class);
253
254 if (ata == null) {
255 ata = new AltTranslationsAnnotation();
256 targetSegment.setAnnotation(ata);
257 Log.trace(getClass(), "Created AltTranslationsAnnotation for segment '{}'", segmentId);
258 }
259
260
261
262 TextFragment sourceTf = scoredSegment.getSourceTf();
263
264 for (HybridTranslationScore score : scores) {
265 String targetText = score.getTranslation();
266 TextFragment targetTf = null;
267
268 if (mtSendPlainText) {
269 targetTf = new TextFragment(targetText);
270
271 } else {
272 targetTf = qutil.fromCodedHTMLToFragment(targetText, null);
273 }
274
275 OkapiUtil.removeExtraCodes(sourceTf.getCodes(), targetTf);
276
277
278 TextFragmentUtil.alignAndCopyCodeMetadata(sourceTf, targetTf, true, true);
279
280
281 OkapiUtil.rearrangeCodes(sourceTf.getCodes(), targetTf);
282
283
284 ConfidenceScoredTranslation cst = new ConfidenceScoredTranslation(
285 getSourceLocale(),
286 getTargetLocale(),
287 sourceTf,
288 sourceTf,
289 targetTf,
290 MatchType.MT,
291 AthUtil.extractLastSection(score.getModelId()),
292 score.getConfidence());
293
294
295 if (score.isAnomalyFlagged()) {
296 cst.setEngine("ANOMALY: " + score.getAnomalyReason());
297
298 } else {
299 cst.setEngine(score.getMethod());
300 }
301
302 ata.add(cst);
303
304 Log.trace(getClass(),
305 "Added alt-trans [{}] confidence={} MetricX={} Heuristic={}: '{}'",
306 score.getModelId(),
307 score.getConfidence(),
308 score.getMetricXScore() != null ? String.format("%.2f", score.getMetricXScore())
309 : "N/A",
310
311 score.getHeuristicScore() != null ? String.format("%.3f", score.getHeuristicScore())
312 : "N/A",
313
314 score.getTranslation());
315 }
316
317
318 ata.sort();
319
320 Iterator<AltTranslation> iter = ata.iterator();
321
322 ConfidenceScoredTranslation bestCst = (ConfidenceScoredTranslation) StreamSupport.stream(
323 Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED),
324 false)
325 .filter(at -> at instanceof ConfidenceScoredTranslation)
326 .findFirst()
327 .orElse(null);
328
329 if (bestCst != null) {
330 TextFragment bestTranslation = bestCst.getTarget().getFirstContent();
331 targetSegment.setContent(bestTranslation);
332
333 Log.debug(getClass(),
334 "Set best translation for TU '{}' segment '{}': '{}' (confidence: {})",
335 tu.getId(), segmentId, bestTranslation, bestCst.getConfidenceScore());
336 }
337 }
338 }
339
340
341
342
343 public static int convertToPercentage(double confidence) {
344 return (int) Math.round(confidence * 100.0);
345 }
346
347 @Override
348 protected void clear() {
349 sourceTexts.clear();
350 segmentInfos.clear();
351 results = null;
352 }
353
354 @Override
355 protected void processTuEvents(List<Event> tuEvents) {
356 initializeModelConfigs();
357
358
359 Log.info(getClass(), "Pre-processing {} text units...", tuEvents.size());
360
361 for (Event tue : tuEvents) {
362 ITextUnit tu = tue.getTextUnit();
363 preProcessTextUnit(tu);
364 }
365
366 Log.info(getClass(), "Collected {} source segment(s) from {} text unit(s)",
367 sourceTexts.size(), tuEvents.size());
368
369
370 if (!sourceTexts.isEmpty()) {
371 try (HybridQualityEstimator hqe = new HybridQualityEstimator(
372 ControllerUtil.getProjectId(),
373 Const.ATH_GCP_PROJECT_LOCATION,
374 mtSendPlainText)) {
375
376 Log.info(getClass(),
377 "Evaluating translations: {} segment(s), {} models, {}→{}",
378 sourceTexts.size(),
379 modelConfigs.size(),
380 getSourceLocale(),
381 getTargetLocale());
382
383 results = hqe.evaluateTranslations(
384 sourceTexts,
385 sourceTfs,
386 getSourceLocale().toString(),
387 getTargetLocale().toString(),
388 modelConfigs);
389
390 Log.info(getClass(),
391 "Evaluation complete using strategy: {}",
392 results.getStrategy());
393
394 } catch (Exception e) {
395 Log.error(getClass(), "Error during translation evaluation: {}", e.getMessage(), e);
396
397 }
398
399 } else {
400 Log.warn(getClass(), "No source segments to translate");
401 }
402
403
404 if (results != null) {
405 Log.info(getClass(), "Post-processing text units with evaluation results...");
406 postProcessTextUnits(results);
407 }
408
409 Log.info(getClass(),
410 "MT Confidence Scoring complete: processed {} TU(s), {} segment(s)",
411 getNumProcessedTus(), sourceTexts.size());
412 }
413 }