// engine_io.mbt
// =====================================================================
// P10: 从 engine.mbt 拆出 — 同包(@lib)多文件,调用点零修改。
// 主题: TMX/XLIFF 解析导出 + 引擎健康 metrics + 双快照 drift_report (#6–#7 互操作与可观测)
// ---- #6 TMS 互通(XLIFF / TMX 解析侧) ----
// 解析 TMX(1.4/2.0):每个 取 src/tgt 的 / 建 TM。返回加载条数。
// 注意:必须以单元「闭合标签」 切分,而非 开端标签——
// 因为 split_on(xml," 内的两个 拆到不同片段,导致译文落空/丢弃。
// 而 不是 的子串,按闭合标签切分可保证一个单元的全部 落在同一片段。
pub fn ProphecyEngine::parse_tmx(self : ProphecyEngine, xml : String) -> Int {
let mut count = 0
let tus = split_on(xml, "")
for tu in tus {
let tuvs = split_on(tu, "/ 内的 / 建 TM。返回加载条数。
// 注意:split_on 返回的 segs[0] 是首个标签之前的前缀(不含 source/target),
// 必须从 index=1 起遍历,否则无匹配时会把整段 XML 误当作一个单元重复建 TM。
pub fn ProphecyEngine::parse_xliff(self : ProphecyEngine, xml : String) -> Int {
let mut count = 0
// XLIFF 1.2: ; XLIFF 2.0:
let segs1 = split_on(xml, "(已由 segs1 处理),仅处理真正的 (2.0)
if str_contains(u, "trans-unit") { continue }
let src = first_tag_text(u, "source")
let tgt = first_tag_text(u, "target")
if src != "" && tgt != "" {
let _ = self.add_tm(src, tgt)
count = count + 1
}
}
count
}
// 导出 TMX(1.4):将所有 mtype=="tm" 节点序列化为标准 TMX 片段(XML 转义已做)
pub fn ProphecyEngine::export_tmx(
self : ProphecyEngine,
src_lang~ : String = "en-US",
tgt_lang~ : String = "zh-CN",
) -> String {
let b = StringBuilder()
b.write_string("\n\n\n")
for _, m in self.memories.iter2() {
if m.mtype != "tm" { continue }
b.write_string("\n")
b.write_string(xml_escape(m.text))
b.write_string("\n")
b.write_string(xml_escape(m.translation))
b.write_string("\n\n")
}
b.write_string("\n")
b.to_string()
}
// ---- #7 可观测 / 漂移监控 ----
// 引擎健康指标:TM 数 / 术语数 / 总记忆数 / 术语覆盖率(术语出现在任一 TM 源或译文的比例)
pub fn ProphecyEngine::metrics(self : ProphecyEngine) -> Json {
let mut tm_count = 0
let mut term_count = 0
let mut term_used = 0
let mut total_mem = 0
let tm_texts : Array[String] = []
for _, m in self.memories.iter2() {
total_mem = total_mem + 1
if m.mtype == "tm" {
tm_count = tm_count + 1
tm_texts.push(m.text)
tm_texts.push(m.translation)
} else if m.is_term {
term_count = term_count + 1
let covered = arr_contains(tm_texts, m.text) || arr_contains(tm_texts, m.translation)
if covered { term_used = term_used + 1 }
}
}
let coverage = if term_count == 0 { 0.0 } else { term_used.to_double() / term_count.to_double() }
obj([
("tm_count", num_json(tm_count.to_double())),
("term_count", num_json(term_count.to_double())),
("total_memories", num_json(total_mem.to_double())),
("term_coverage", num_json(r4(coverage))),
])
}
// 两快照(to_json 输出)对比的漂移报告:按 (type|is_term|text|translation) 键算 TM/术语 增删
// P4 增强:在 after snapshot 上加 text_chrf_avg 字段,算所有 TM (text, translation) 的 chrF 平均
// 作为「记忆库整体翻译质量」指标;纯本地 (chrf_score 零依赖),无云端。
pub fn ProphecyEngine::drift_report(
_self : ProphecyEngine,
before : Json,
after : Json,
) -> Json {
let b_mem = get_obj(before, "memories")
let a_mem = get_obj(after, "memories")
let b_keys : Array[String] = []
let a_keys : Array[String] = []
match b_mem {
Json::Object(m) => for _, mn in m.iter2() { b_keys.push(node_key(mn)) }
_ => ()
}
match a_mem {
Json::Object(m) => for _, mn in m.iter2() { a_keys.push(node_key(mn)) }
_ => ()
}
let mut tm_added = 0
let mut tm_removed = 0
let mut term_added = 0
let mut term_removed = 0
for k in a_keys {
if !arr_contains(b_keys, k) {
if str_contains(k, "tm|") { tm_added = tm_added + 1 } else { term_added = term_added + 1 }
}
}
for k in b_keys {
if !arr_contains(a_keys, k) {
if str_contains(k, "tm|") { tm_removed = tm_removed + 1 } else { term_removed = term_removed + 1 }
}
}
let mut b_tm = 0
let mut b_term = 0
for k in b_keys {
if str_contains(k, "tm|") { b_tm = b_tm + 1 } else { b_term = b_term + 1 }
}
let mut a_tm = 0
let mut a_term = 0
for k in a_keys {
if str_contains(k, "tm|") { a_tm = a_tm + 1 } else { a_term = a_term + 1 }
}
// P4:after snapshot 所有 TM (text, translation) chrF 平均 — 翻译质量漂移指标
// 注:to_json 字段名是 "type" 不是 "mtype"(见 node_key L2195)
let mut chrf_sum = 0.0
let mut chrf_n = 0
match a_mem {
Json::Object(m) =>
for _, mn in m.iter2() {
match mn {
Json::Object(nobj) => {
let is_tm = match nobj.get("type") {
Some(Json::String(s)) => s == "tm"
_ => false
}
if is_tm {
let text = get_str(mn, "text")
let trans = get_str(mn, "translation")
if text != "" && trans != "" {
chrf_sum = chrf_sum + chrf_score(text, trans)
chrf_n = chrf_n + 1
}
}
}
_ => ()
}
}
_ => ()
}
let chrf_avg = if chrf_n > 0 { r4(chrf_sum / chrf_n.to_double()) } else { 0.0 }
obj([
("before_tm", num_json(b_tm.to_double())),
("after_tm", num_json(a_tm.to_double())),
("before_term", num_json(b_term.to_double())),
("after_term", num_json(a_term.to_double())),
("tm_added", num_json(tm_added.to_double())),
("tm_removed", num_json(tm_removed.to_double())),
("term_added", num_json(term_added.to_double())),
("term_removed", num_json(term_removed.to_double())),
// P4 新增:after snapshot 翻译质量漂移指标(chrF 平均,0..1)
("text_chrf_avg", num_json(chrf_avg)),
("text_chrf_n", num_json(chrf_n.to_double())),
])
}