use serde::{Deserialize, Serialize};
use crate::{CompletionRequest, Glossary, Message, ModelAdapter};
const ECHO_ADAPTER_NAME: &str = "echo";
pub const ECHO_PASSTHROUGH_NOTE: &str = "adapter is the echo stub (no local .gguf) — drafts are a \
pass-through of the source and the round-trip signal is meaningless; treat every AcceptLocal \
here as unproven and route to cloud in production.";
pub const DEFAULT_ACCEPT_THRESHOLD: f32 = 0.85;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Decision {
AcceptLocal,
SendToCloud,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TwoPassConfig {
pub accept_threshold: f32,
pub min_comfortable_chars: usize,
pub max_comfortable_chars: usize,
pub source_lang: String,
pub target_lang: String,
}
impl Default for TwoPassConfig {
fn default() -> Self {
Self {
accept_threshold: DEFAULT_ACCEPT_THRESHOLD,
min_comfortable_chars: 24,
max_comfortable_chars: 320,
source_lang: "Chinese".to_string(),
target_lang: "English".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConfidenceSignals {
pub char_len: usize,
pub length_score: f32,
pub glossary_hits: usize,
pub glossary_score: f32,
pub round_trip_similarity: f32,
pub round_trip_trusted: bool,
pub round_trip_score: f32,
pub confidence: f32,
}
impl ConfidenceSignals {
pub const W_LENGTH: f32 = 0.35;
pub const W_ROUND_TRIP: f32 = 0.45;
pub const W_GLOSSARY: f32 = 0.20;
#[must_use]
pub fn combine(length_score: f32, round_trip_score: f32, glossary_score: f32) -> f32 {
(Self::W_LENGTH * length_score
+ Self::W_ROUND_TRIP * round_trip_score
+ Self::W_GLOSSARY * glossary_score)
.clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SegmentPlan {
pub index: usize,
pub source: String,
pub local_draft: String,
pub confidence: f32,
pub decision: Decision,
pub signals: ConfidenceSignals,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TwoPassPlan {
pub segments: Vec<SegmentPlan>,
pub notes: Vec<String>,
}
impl TwoPassPlan {
pub const AUTHORITATIVE: bool = false;
#[must_use]
pub fn summary(&self) -> TwoPassSummary {
let total = self.segments.len();
let accepted_local =
self.segments.iter().filter(|s| s.decision == Decision::AcceptLocal).count();
let sent_to_cloud = total - accepted_local;
let local_fraction =
if total == 0 { 0.0 } else { accepted_local as f32 / total as f32 };
TwoPassSummary { total, accepted_local, sent_to_cloud, local_fraction }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TwoPassSummary {
pub total: usize,
pub accepted_local: usize,
pub sent_to_cloud: usize,
pub local_fraction: f32,
}
#[must_use]
pub fn draft_and_route(
adapter: &dyn ModelAdapter,
segments: &[String],
glossary: Option<&Glossary>,
cfg: &TwoPassConfig,
) -> TwoPassPlan {
let echo_stub = adapter.name() == ECHO_ADAPTER_NAME;
let plans = segments
.iter()
.enumerate()
.map(|(index, source)| plan_segment(adapter, index, source, glossary, cfg, echo_stub))
.collect();
let mut notes = vec![
"AcceptLocal is a routing decision, not a correctness guarantee: it means only that the \
signals did not justify a cloud review, never that the draft is correct. Widen the local \
share past the conservative default only with measured evidence (§13 Task III-6)."
.to_string(),
];
if echo_stub {
notes.push(ECHO_PASSTHROUGH_NOTE.to_string());
}
TwoPassPlan { segments: plans, notes }
}
fn plan_segment(
adapter: &dyn ModelAdapter,
index: usize,
source: &str,
glossary: Option<&Glossary>,
cfg: &TwoPassConfig,
echo_stub: bool,
) -> SegmentPlan {
let raw_draft = translate(adapter, source, &cfg.source_lang, &cfg.target_lang);
let (local_draft, glossary_hits) = match glossary {
Some(g) if !g.is_empty() => {
let applied = g.apply(&raw_draft);
let hits = applied.total_substitutions();
(applied.output, hits)
}
_ => (raw_draft, 0),
};
let back = translate(adapter, &local_draft, &cfg.target_lang, &cfg.source_lang);
let round_trip_similarity = dice_similarity(source, &back);
let char_len = source.chars().count();
let length_score = length_score(char_len, cfg);
let glossary_score = glossary_score(glossary_hits, glossary.is_some());
let round_trip_trusted = !echo_stub;
let round_trip_score = if round_trip_trusted { round_trip_similarity } else { 0.5 };
let confidence = ConfidenceSignals::combine(length_score, round_trip_score, glossary_score);
let decision = if confidence >= cfg.accept_threshold {
Decision::AcceptLocal
} else {
Decision::SendToCloud
};
SegmentPlan {
index,
source: source.to_string(),
local_draft,
confidence,
decision,
signals: ConfidenceSignals {
char_len,
length_score,
glossary_hits,
glossary_score,
round_trip_similarity,
round_trip_trusted,
round_trip_score,
confidence,
},
}
}
fn translate(adapter: &dyn ModelAdapter, text: &str, from: &str, to: &str) -> String {
let request = CompletionRequest::new([
Message::system(format!(
"Translate the {from} text the user sends into {to}. Output only the {to} \
translation, nothing else."
)),
Message::user(text),
]);
adapter.complete(&request).map(|r| r.content).unwrap_or_default()
}
#[must_use]
pub fn length_score(char_len: usize, cfg: &TwoPassConfig) -> f32 {
let min = cfg.min_comfortable_chars.max(1);
let max = cfg.max_comfortable_chars.max(min);
if char_len == 0 {
0.0
} else if char_len < min {
char_len as f32 / min as f32
} else if char_len <= max {
1.0
} else {
let over = (char_len - max) as f32;
let span = max as f32; (1.0 - 0.7 * (over / span)).max(0.3)
}
}
#[must_use]
pub fn glossary_score(hits: usize, glossary_present: bool) -> f32 {
if !glossary_present {
return 0.5;
}
0.5 + 0.5 * (1.0 - 1.0 / (1.0 + hits as f32))
}
#[must_use]
pub fn dice_similarity(a: &str, b: &str) -> f32 {
let ba = bigrams(a);
let bb = bigrams(b);
if ba.is_empty() || bb.is_empty() {
return if a == b { 1.0 } else { 0.0 };
}
let mut bb_counts: std::collections::HashMap<(char, char), i32> = std::collections::HashMap::new();
for g in &bb {
*bb_counts.entry(*g).or_insert(0) += 1;
}
let mut inter = 0usize;
for g in &ba {
let e = bb_counts.entry(*g).or_insert(0);
if *e > 0 {
*e -= 1;
inter += 1;
}
}
(2.0 * inter as f32) / (ba.len() as f32 + bb.len() as f32)
}
fn bigrams(s: &str) -> Vec<(char, char)> {
let chars: Vec<char> = s.chars().collect();
if chars.len() < 2 {
return Vec::new();
}
chars.windows(2).map(|w| (w[0], w[1])).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CompletionResponse, EchoAdapter, Entry, Glossary};
use std::sync::atomic::{AtomicUsize, Ordering};
struct CountingAdapter {
reply: String,
calls: AtomicUsize,
}
impl CountingAdapter {
fn new(reply: &str) -> Self {
Self { reply: reply.to_string(), calls: AtomicUsize::new(0) }
}
}
impl ModelAdapter for CountingAdapter {
fn name(&self) -> &str {
"counting-test"
}
fn complete(&self, _request: &CompletionRequest) -> anyhow::Result<CompletionResponse> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(CompletionResponse { content: self.reply.clone(), model: "counting-test".into() })
}
}
const _: () = assert!(!TwoPassPlan::AUTHORITATIVE);
fn segs(v: &[&str]) -> Vec<String> {
v.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn drafts_and_round_trips_route_through_the_adapter() {
let adapter = CountingAdapter::new("some translation");
let plan = draft_and_route(&adapter, &segs(&["源文本一", "源文本二"]), None, &TwoPassConfig::default());
assert_eq!(plan.segments.len(), 2);
assert_eq!(adapter.calls.load(Ordering::SeqCst), 4, "draft + round-trip must both route through the adapter");
assert_eq!(plan.segments[0].local_draft, "some translation");
}
#[test]
fn very_short_segment_scores_low_and_goes_to_cloud() {
let plan = draft_and_route(&EchoAdapter, &segs(&["##"]), None, &TwoPassConfig::default());
let s = &plan.segments[0];
assert!(s.signals.length_score < 0.2, "a 2-char segment must score low on length: {}", s.signals.length_score);
assert_eq!(s.decision, Decision::SendToCloud);
}
#[test]
fn summary_local_fraction_matches_decisions() {
let cfg = TwoPassConfig { accept_threshold: 0.4, ..TwoPassConfig::default() };
let long = "这是一段足够长的技术性中文文本用于测试本地优先的两遍翻译流程的置信度评分";
let plan = draft_and_route(&EchoAdapter, &segs(&[long, "##"]), None, &cfg);
let summary = plan.summary();
let expected_local =
plan.segments.iter().filter(|s| s.decision == Decision::AcceptLocal).count();
assert_eq!(summary.total, 2);
assert_eq!(summary.accepted_local, expected_local);
assert_eq!(summary.sent_to_cloud, 2 - expected_local);
assert!((summary.local_fraction - expected_local as f32 / 2.0).abs() < 1e-6);
assert_eq!(summary.accepted_local, 1);
assert_eq!(summary.sent_to_cloud, 1);
}
#[test]
fn glossary_hits_raise_the_confidence_signal() {
let source = "华龙一号的数字孪生模型用于压水堆的乏燃料管理与安全评估研究工作";
let glossary = Glossary::from_entries([
Entry::new("华龙一号", "Hualong One"),
Entry::new("数字孪生", "digital twin"),
])
.unwrap();
let cfg = TwoPassConfig::default();
let without = draft_and_route(&EchoAdapter, &segs(&[source]), None, &cfg);
let with = draft_and_route(&EchoAdapter, &segs(&[source]), Some(&glossary), &cfg);
assert_eq!(with.segments[0].signals.glossary_hits, 2, "both terms should hit");
assert!(
with.segments[0].signals.glossary_score > without.segments[0].signals.glossary_score,
"glossary hits must raise the glossary sub-score ({} !> {})",
with.segments[0].signals.glossary_score,
without.segments[0].signals.glossary_score
);
assert!(
with.segments[0].confidence > without.segments[0].confidence,
"glossary hits must raise overall confidence"
);
}
#[test]
fn conservative_default_sends_most_to_cloud() {
let batch = segs(&[
"第一段中等长度的中文技术文本内容用于测试保守默认阈值的路由行为表现",
"第二段中等长度的中文技术文本内容用于测试保守默认阈值的路由行为表现",
"第三段中等长度的中文技术文本内容用于测试保守默认阈值的路由行为表现",
"短",
"##",
]);
let plan = draft_and_route(&EchoAdapter, &batch, None, &TwoPassConfig::default());
let summary = plan.summary();
assert!(
summary.sent_to_cloud > summary.accepted_local,
"conservative default must send most to cloud: {summary:?}"
);
assert!(plan.notes.iter().any(|n| n == ECHO_PASSTHROUGH_NOTE));
}
#[test]
fn echo_round_trip_is_recorded_but_not_trusted() {
let plan = draft_and_route(&EchoAdapter, &segs(&["一段用于测试往返信号折扣处理的中文文本内容"]), None, &TwoPassConfig::default());
let sig = &plan.segments[0].signals;
assert!(!sig.round_trip_trusted, "echo round-trip must not be trusted");
assert!((sig.round_trip_score - 0.5).abs() < 1e-6, "untrusted round-trip folds in as neutral 0.5");
}
#[test]
fn non_echo_round_trip_is_trusted_and_measured() {
let adapter = CountingAdapter::new("a completely unrelated english reply");
let plan = draft_and_route(&adapter, &segs(&["一段足够长的中文源文本用于测试往返一致性信号"]), None, &TwoPassConfig::default());
let sig = &plan.segments[0].signals;
assert!(sig.round_trip_trusted);
assert!(sig.round_trip_similarity < 0.2, "unrelated back-translation must diverge: {}", sig.round_trip_similarity);
assert_eq!(plan.segments[0].decision, Decision::SendToCloud);
}
#[test]
fn plan_is_deterministic() {
let glossary = Glossary::from_entries([Entry::new("压水堆", "PWR")]).unwrap();
let batch = segs(&["压水堆机组的数字孪生", "##", "another segment of moderate length here"]);
let cfg = TwoPassConfig::default();
let a = draft_and_route(&EchoAdapter, &batch, Some(&glossary), &cfg);
let b = draft_and_route(&EchoAdapter, &batch, Some(&glossary), &cfg);
assert_eq!(a, b, "same input must yield an identical plan");
}
#[test]
fn empty_batch_has_zero_local_fraction() {
let plan = draft_and_route(&EchoAdapter, &[], None, &TwoPassConfig::default());
let summary = plan.summary();
assert_eq!(summary.total, 0);
assert_eq!(summary.accepted_local, 0);
assert_eq!(summary.local_fraction, 0.0);
}
#[test]
fn length_score_shape() {
let cfg = TwoPassConfig::default();
assert_eq!(length_score(0, &cfg), 0.0);
assert!(length_score(2, &cfg) < 0.2); assert_eq!(length_score(cfg.min_comfortable_chars, &cfg), 1.0);
assert_eq!(length_score(cfg.max_comfortable_chars, &cfg), 1.0);
assert!(length_score(cfg.max_comfortable_chars * 10, &cfg) >= 0.3);
assert!(length_score(cfg.max_comfortable_chars + 1, &cfg) < 1.0);
}
#[test]
fn glossary_score_shape() {
assert!((glossary_score(0, false) - 0.5).abs() < 1e-6);
assert!((glossary_score(0, true) - 0.5).abs() < 1e-6);
assert!(glossary_score(1, true) > glossary_score(0, true));
assert!(glossary_score(3, true) > glossary_score(1, true));
assert!(glossary_score(100, true) < 1.0);
}
#[test]
fn dice_similarity_bounds() {
assert_eq!(dice_similarity("华龙一号", "华龙一号"), 1.0);
assert_eq!(dice_similarity("abc", "abc"), 1.0);
assert!(dice_similarity("华龙一号", "完全不同的文本").abs() < 0.1);
assert_eq!(dice_similarity("a", "a"), 1.0);
assert_eq!(dice_similarity("a", "b"), 0.0);
assert_eq!(dice_similarity("", ""), 1.0);
}
#[test]
fn serde_derives_are_present() {
fn assert_serde<T: serde::Serialize + serde::de::DeserializeOwned>() {}
assert_serde::<TwoPassPlan>();
assert_serde::<SegmentPlan>();
assert_serde::<TwoPassSummary>();
assert_serde::<ConfidenceSignals>();
assert_serde::<TwoPassConfig>();
assert_serde::<Decision>();
}
}