mmv-voice / tests /test_readable.py
moebiusT7's picture
mmv-voice — byte-identical mirror of GitHub tag v0.2.13
d23c885 verified
Raw History Blame Contribute Delete
13.1 kB
import json
import threading
import unittest
from unittest.mock import patch
from types import SimpleNamespace
from http.server import ThreadingHTTPServer
from test_mmv_formatter import Handler
import voice_readable as vr
from voice_mmv import MODEL_DIGEST, audit_report
class ReadableTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
import whisper_gui
cls.gui = whisper_gui
cls.server = ThreadingHTTPServer(('127.0.0.1',0),Handler)
cls.thread = threading.Thread(target=cls.server.serve_forever,daemon=True);cls.thread.start()
cls.host = 'http://127.0.0.1:'+str(cls.server.server_port)
@classmethod
def tearDownClass(cls):
cls.server.shutdown();cls.server.server_close();cls.thread.join()
def setUp(self):
Handler.requests.clear();Handler.digest=MODEL_DIGEST;Handler.content=''
self.client=vr.ReadableFormatter(self.gui.call_adapter,self.host)
def test_real_harness_no_qa_route_or_scaffold_three_languages(self):
cases=[('en','Unfortunately driver behavior cannot be predicted with100% certainty.','Driver behavior cannot be predicted with100% certainty.'),('ja','えー、来週ならたぶんできます。','来週ならたぶんできます。'),('zh','嗯,下周大概能处理。','下周大概能处理。')]
for lang,src,out in cases:
Handler.content=out
text,rows=self.client.format(src,lang)
self.assertEqual(text,out);self.assertEqual(rows[0]['reason'],'draft_for_review')
self.assertFalse(rows[0]['human_verified']);self.assertFalse(rows[0]['semantic_verified'])
self.assertNotIn('Correction of the premise:',text)
req=Handler.requests[-1];self.assertEqual(req['prompt'],vr.PROMPTS_12B[lang]+json.dumps({'transcript':src},ensure_ascii=False))
self.assertEqual(len(Handler.requests),3)
self.assertTrue(all(self.client.profile[k] is False for k in ['route_transformer','post_validator','force_reanchor_v2']))
def test_diagnostics_calibrated_changed_and_unchanged(self):
for s,c,flag in [('15万円です','50万円です','numeric_expression_changed'),('採用しません','採用します','negation_changed'),('We might deliver','We will deliver','uncertainty_changed'),('没有批准','已经批准','negation_changed')]:
self.assertIn(flag,vr.diagnostics(s,c));self.assertNotIn(flag,vr.diagnostics(s,s))
self.assertEqual(vr.diagnostics('予算は15万円です','予算は15万円です。'),[])
def test_guard_is_stricter_than_marker_rules(self):
# The marker-rule negative controls below are edits that must not be FLAGGED; the bounded-edit guard
# may still RETAIN several of them (paraphrase, dropped filler not in the list). Documented, not a defect.
retained=[(s,c) for s,c in [('人数が少ないです','人数は少数です'),('不好意思,我们下周不能来。','我们下周不能来。')] if vr.readable_postcondition(s,c,'ja' if s[0]>'ぁ' and s[0]<'ヿ' or s[0]=='人' else 'zh')]
self.assertEqual(len(retained),2)
def test_diagnostics_negative_controls_ordinary_edits(self):
# Ordinary readable edits that the prompt itself requests must NOT trip the warning rules (v0.2.8 review S1).
for s,c in [('人数が少ないです','人数は少数です'),('それはできないです','それはできません'),('もったいないですね','惜しいですね'),
('不好意思,我们下周不能来。','我们下周不能来。'),('这个不错,不过要再看看。','这个很好,但要再看看。'),
('It is not ready; we cannot ship.','We cannot ship; it is not ready.'),('採用しません。理由は二つです。','理由は二つです。採用しません。')]:
self.assertEqual(vr.diagnostics(s,c),[],(s,c))
# …while real polarity/uncertainty removals still flag.
self.assertIn('negation_changed',vr.diagnostics('我们下周不能来。','我们下周能来。'))
self.assertIn('uncertainty_changed',vr.diagnostics('たぶん来週です','来週です'))
def test_bounded_edit_guard_known_good_and_bad(self):
ok=[('en','um so the budget is 320 dollars and it is due friday','The budget is 320 dollars and it is due Friday.'),
('ja','えー 申請は終わりました あの 結果を待ちます','申請は終わりました。あの、結果を待ちます。'), # えー is a filler; あの is kept (demonstrative)
('zh','嗯 下周大概能处理','下周大概能处理。'),
('en','we will need about 50 units','We will need about 50 units.')]
for lang,s,c in ok: self.assertEqual(vr.readable_postcondition(s,c,lang),[],(s,c))
ok+= [('en',"you know what i mean the budget is 320 dollars",'The budget is 320 dollars.'), # filler phrases
('zh','他说这很重要','他说“这很重要”。'), # added quotes are not content
('ja','東京で会います','東京で会います。')]
for lang,s,c in ok: self.assertEqual(vr.readable_postcondition(s,c,lang),[],(s,c))
ok+= [('ja','私はそこに行きます','私はそこに行きます。'),('ja','資料を確認しています','資料を確認しています。'),
('ja','彼ウェールズは嘘をついていました','彼はウェールズ(※要確認)は、嘘をついていました。')] # source name + new particles is not new content
for lang,s,c in ok: self.assertEqual(vr.readable_postcondition(s,c,lang),[],(s,c))
bad=[('ja','そこに行く','そこに行かない。','negation_count_changed'), # plain verb negation (review 2026-09-26 #1)
('ja','一つ選んでください','すべて選んでください。','content_added'), # kana-only quantifier flip (#2)
('ja','あの人が犯人です','人が犯人です。','omission_over_limit'), # あの is a demonstrative, not a filler (#3)
('en','we will not ship on friday','We will ship on Friday.','negation_count_changed'), # dropped negation (1 token)
('en','we will ship on friday','We will not ship on Friday.','content_added'),
('ja','来週に出荷します','来週には出荷しません。','negation_count_changed'), # kana-only polarity flip
('en',"that's right we mean the second one",'We the second one.','omission_over_limit'), # 'right'/'mean' are content
('en','we will need about fifty units','We will need about 50 units.','numerals_changed'),
('en','the budget is 320 dollars and it is due friday','The budget is 320 dollars.','omission_over_limit'),
('en','en ik vind het van markt to tell you','I think from a sell the market to tell you.','content_added'),
('ja','予算は15万円です','予算は50万円です。','numerals_changed'),
('zh','黄庚承会来','黄循财会来。','content_added')]
for lang,s,c,rule in bad: self.assertTrue(any(v.startswith(rule) for v in vr.readable_postcondition(s,c,lang)),(s,c,rule))
def test_guard_violation_keeps_source_and_shows_draft(self):
Handler.content='黄循财会来。';text,rows=self.client.format('黄庚承会来','zh')
self.assertEqual(text,'黄庚承会来');self.assertEqual(rows[0]['status'],'source_retained');self.assertIn('readable_guard',rows[0]['reason'])
self.assertEqual(rows[0]['candidate'],'黄循财会来。');self.assertTrue(rows[0]['needs_review']);self.assertIn('[changes]',audit_report(rows));self.assertIn('Bounded-edit guard',audit_report(rows))
def test_26b_tag_absent_is_visible_error_never_12b_fallback(self):
client=vr.ReadableFormatter(self.gui.call_adapter,self.host,model='gemma4:26b-a4b-it-qat')
self.assertEqual(client.profile['model_id'],'gemma4:26b-a4b-it-qat')
Handler.content='x';text,rows=client.format('你好,我们下周来。','zh') # fixture only advertises the 12B tag
self.assertEqual(text,'你好,我们下周来。');self.assertEqual(Handler.requests,[])
self.assertIn('model_unavailable',rows[0]['reason']);self.assertIn('26b-a4b',rows[0]['reason']);self.assertEqual(rows[0]['mode'],'readable')
def test_digest_and_meta_record_readable_mode(self):
import tempfile,os
with tempfile.TemporaryDirectory() as d, patch.object(self.gui,'DIGEST_DIR',d):
path=self.gui.write_secretary_digest({'engine':'MMV-Readable draft (review required) (gemma4:12b-it-qat) · local','format_mode':'readable','language':'ja'},'本文','','report')
body=open(path,encoding='utf-8').read()
self.assertIn('format_mode: readable',body);self.assertIn('human_verified: false',body)
def test_flags_do_not_silently_revert_or_certify(self):
# A changed uncertainty expression now retains the source; a marker cannot legitimize certainty.
Handler.content='We will deliver on Friday.';out,rows=self.client.format('we might deliver on friday','en')
self.assertEqual(out,'we might deliver on friday');self.assertIn('uncertainty_changed',rows[0]['guard_violations']);self.assertIn('uncertainty_changed',rows[0]['review_flags']);self.assertTrue(rows[0]['needs_review'])
report=audit_report(rows);self.assertIn('NOT verified',report);self.assertIn('might',report);self.assertIn('[changes]',report);self.assertIn('diagnostic rules: 3',report)
# Dropping a whole uncertainty word in a short Japanese chunk is content loss: the guard keeps the source.
Handler.content='来週です。';out2,rows2=self.client.format('たぶん来週です','ja');self.assertEqual(out2,'たぶん来週です');self.assertIn('omission_over_limit',rows2[0]['reason'])
# A numeral change is a guard violation: source kept, draft visible with both numbers in the report.
Handler.content='予算は50万円です。';out,rows=self.client.format('予算は15万円です','ja')
self.assertEqual(out,'予算は15万円です');self.assertEqual(rows[0]['status'],'source_retained');report=audit_report(rows);self.assertIn('15万円',report);self.assertIn('50万円',report);self.assertIn('Bounded-edit guard',report)
def test_empty_error_truncation_and_preamble_fallback(self):
for text,error,tokens,reason in [('',None,0,'empty_output'),('changed','offline',3,'backend_error'),('long',None,1024,'possible_output_truncation'),('Correction of the premise: x',None,8,'unexpected_wrapper_preamble')]:
client=vr.ReadableFormatter(lambda *a:SimpleNamespace(text=text,error=error,tokens_in=3,tokens_out=tokens),self.host)
row=client.format_chunk(' original ','en');self.assertEqual(row['text'],' original ');self.assertIn(reason,row['reason']);self.assertEqual(row['candidate'],text)
def test_model_mismatch_preserves_observable_provenance_without_generate(self):
Handler.digest='wrong';text,rows=self.client.format('你好','zh')
self.assertEqual(text,'你好');self.assertEqual(Handler.requests,[]);self.assertIn('model_unavailable',rows[0]['reason']);self.assertEqual(rows[0]['diagnostic_rule_count'],3);self.assertEqual(rows[0]['profile_path'],str(vr.PROFILE_PATH))
def test_whitespace_and_oversize_do_not_generate(self):
for src in [' ','x'*1001]:
row=self.client.format_chunk(src,'en');self.assertEqual(row['text'],src)
self.assertEqual(Handler.requests,[])
def test_gui_boundary_default_verbatim_and_readable_opt_in(self):
# Default engine stays verbatim (preservation check); readable is a separate opt-in engine binding.
self.assertEqual(self.gui.MMV_M['format_mode'],'verbatim')
self.assertEqual(self.gui.FORMAT_MODE,'verbatim')
self.assertIsNotNone(self.gui.MMV_READABLE);self.assertEqual(self.gui.MMV_READABLE['format_mode'],'readable')
engine=dict(self.gui.MMV_READABLE);engine['client']=self.client;Handler.content='来週なら対応できます。'
out,pairs=self.gui.mmv_formatting('えー、来週なら対応できます。',engine=engine,lang='ja')
self.assertEqual(out,Handler.content);self.assertTrue(engine['format_audit'][0]['needs_review']);self.assertEqual(len(Handler.requests),1)
# The verbatim engine rejects the same wording change and retains the source.
verbatim=dict(self.gui.MMV_M);verbatim['client']=self.gui.MMVFormatter(self.client.call_adapter, endpoint=self.client.profile['endpoint'])
Handler.content='来週なら対応できます。'
out2,_=self.gui.mmv_formatting('えー、来週なら対応できます。',engine=verbatim,lang='ja')
self.assertEqual(out2,'えー、来週なら対応できます。');self.assertEqual(verbatim['format_audit'][0]['status'],'source_retained')