block_changes
def block_changes(
blocks, text:str, changes
)->list | None:Make changes to text in the blocks their words fall in, splitting a change at a block’s end, or return None when the blocks do not read as text.
A vision model’s reading of a page can contain slips. Other models, called readers on this page, can read the same pages again and compare their readings with the stored text. Each difference is filed as a change. A change is a run of words to replace, insert or delete, with the names of the readers who made it. Nothing in the document changes until a person decides. This page calls that person the owner.
Vault.proofread(doc_id, readers) has each reader read the pages, then files the changes. A change that at least agree readers (2 by default) make identically has the state proposed. A change that fewer readers make has the state doubt. A reader named twice counts once. The owner then works through the review queue with these methods:
| method | what it does |
|---|---|
changes(doc_id, state) |
lists the changes, all of them or those in one state: proposed, doubt, accepted or rejected |
accept(doc_id, ids) |
applies changes, all the proposed ones when ids is None, as a new version of each page they touch (route proof), and re-indexes the document |
reject(doc_id, ids) |
sets changes aside. They stay in the record as not taken |
edit_change(doc_id, id, after) |
rewrites the proposed text of a change that is still open |
undo_decision(doc_id, ids) |
takes a decision back. A rejected change returns to the state it had. The page of an accepted change returns to its text before the accept, and every change that the accept applied is proposed again |
revert_page(doc_id, page, version) |
files any old version of a page again as its newest version (route revert) |
A change filed against an older version of its page cannot be accepted. accept raises an error that asks for a new proofread. A person can undo an accept only while its page has not changed since. edit_change, undo_decision and revert_page are for a person only. They raise PermissionError when called with by='agent'.
proof_md writes the record of a proofread as markdown, page by page. It lists what changed, what is proposed, what is in doubt and what was not taken.
word_changes(a, b) compares two texts word by word. It returns each run of words that b replaces, inserts or deletes, as {at, n, before, after}. at is the index of the first word in a. n is the number of words of a that the change replaces (0 for an insertion). before and after are the words themselves.
apply_changes applies a list of such changes from the last to the first. It keeps the spacing and line breaks of the text everywhere else.
A stored page also has blocks, and the blocks must match its text. block_changes applies each change to the block that holds its words. It splits a change that crosses the end of a block. The cells of a table and the ## of a heading stay as read. change_box finds the box of the block that holds a change. A review screen uses it to show the change on the page image.
Make changes to text in the blocks their words fall in, splitting a change at a block’s end, or return None when the blocks do not read as text.
Make changes in t, each replacing n words from word at with after, and keep the text’s own spacing elsewhere.
The box of the block that word at of text falls in, the last block for a position past the end, or None when the blocks do not read as text.
Compare a and b word by word and return each run of words b changes, as {at, n, before, after}.
_a, _b = 'धर्मक्षेत्रे कुरुक्षेत्रे समवेता युयुत्सवः', 'धर्मक्षेत्रे कुरुक्षेत्रे समवेता युयुत्सवाः ॥१॥'
_c = word_changes(_a, _b)
test_eq(_c, [dict(at=3, n=1, before='युयुत्सवः', after='युयुत्सवाः ॥१॥')])
test_eq(apply_changes(_a, _c), _b)
test_eq(apply_changes('a b c', word_changes('a b c', 'a c')), 'a c')
test_eq(apply_changes('a c', word_changes('a c', 'a b c')), 'a b c')
test_eq(apply_changes('line one\nline two', word_changes('line one\nline two', 'line 1\nline two')), 'line 1\nline two')
_bl = [dict(kind='heading', text='Title here', box=[0, 0, 1, .1]), dict(kind='prose', text='one two three', box=[0, .2, 1, .4]), dict(kind='prose', text='four')]
_tx = _md(_bl)
test_eq([change_box(_bl, _tx, i) for i in (0, 2, 3, 5, 6, 9)], [[0, 0, 1, .1], [0, 0, 1, .1], [0, .2, 1, .4], [0, .2, 1, .4], None, None]) # the heading is 3 words; past the end is the last block
test_eq((change_box(_bl, 'other words', 0), change_box(None, _tx, 0)), (None, None))
for x, y in [('', 'new words'), ('a', ''), ('x y z', 'z y x'), ('one two', 'one two three')]: test_eq(apply_changes(x, word_changes(x, y)), y if x or y else '')
test_eq([b['text'] for b in block_changes(_bl, _tx, word_changes(_tx, '## Title there\n\none 2 three\n\nfour'))], ['Title there', 'one 2 three', 'four'])
test_eq([b['text'] for b in block_changes(_bl, _tx, word_changes(_tx, '## Title here\n\none two 3 4'))], ['Title here', 'one two 3 4', '']) # a run across a block's end splits there
test_eq([b['text'] for b in block_changes(_bl, _tx, word_changes(_tx, '## Title here\n\none two three\n\nfour five'))], ['Title here', 'one two three', 'four five'])
test_eq(block_changes(_bl, _tx, word_changes(_tx, 'Title here one two three four'))[0]['text'], 'Title here') # the ## goes from the text, the heading stays
_tb = [dict(kind='table', rows=[['a', 'b']]), dict(kind='prose', text='after it')]
test_eq(block_changes(_tb, _md(_tb), word_changes(_md(_tb), _md(_tb).replace('a', 'x', 1).replace('after', 'later'))), [_tb[0], dict(kind='prose', text='later it')])
test_eq(block_changes(_bl, 'not the blocks', []), None)readers are model ids. chats maps a reader to a stand-in callable. The tests use it to run without models.
Each reader reads the page image through read_page, so the privacy rules are the rules of read_page. A reader that may not see a page skips it, and blocked reports the skip. A hosted reader may not see a Personal page without allow=True. No hosted reader sees a Local only page. The vault writes each hosted reading to the send log.
A second run of proofread replaces the open changes (proposed and doubt) on each page it reads. It leaves decided changes alone. A change keeps the box of the block that holds it when the blocks of the page have boxes.
progress(done, total, page, route, cost) is called once with done=0 and again after each page. If it returns False, the rest of the pages are not read. The result carries cancelled, unread and the summed cost.
The record of a document’s proofreading as markdown, by page: what changed, what is proposed or in doubt, and what was not taken.
Take decisions back: a rejected change returns to its old state, and an accepted change’s page returns to its text before the accept, with that accept’s changes proposed again; only a person may.
File an old version of a page as its newest version, route revert, and re-index the document; only a person may.
Rewrite the proposed text of a change that is still open; only a person may.
Apply changes, all the proposed ones when ids is None, as a new version of each page they touch, with its blocks changed to match, and re-index the document.
Set this document’s open changes aside as not taken, remembering the state each had so that undo_decision can restore it, and return how many were set aside.
List a document’s filed changes in page and word order, or only those in one state: proposed, doubt, accepted or rejected.
def proofread(
doc_id:str, readers, pages:NoneType=None, agree:int=2, preset:str=None, script:str=None, hint:str=None,
dpi:int=200, chats:dict=None,
pii:str='redact', # what `read_page` applies: a reader that may not see a page is skipped for it and reported in `blocked`
allow:bool=False, # allow once, for Personal pages
progress:NoneType=None, # `progress(done, total, page, route, cost)` after each page; False stops the rest
)->dict:Have each of readers read the pages again and file their word changes to the current text, proposed where at least agree readers make the same one.
from vishalakshi.read import read_doc
from pdflite import pdf_doc
class Says:
"A reader that reads every page as `text`."
def __init__(self, text): self.text, self.model = text, 'fake'
def __call__(self, msg): return dict(content=json.dumps(dict(items=[dict(kind='prose', text=self.text)])))
_d = Path(tempfile.mkdtemp())
from pdf_oxide import Pdf
_fx = Path(tempfile.mkdtemp())
for _i, _t in enumerate(['# Attention Is All You Need\n\nThe dominant sequence transduction models are based on recurrent or convolutional networks.',
'# Background\n\nThe goal of reducing sequential computation also forms the foundation of the Extended Neural GPU.']):
Pdf.from_markdown(_t).save(str(_fx/f'p{_i}.pdf'))
Pdf.merge([str(_fx/'p0.pdf'), str(_fx/'p1.pdf')]).save(str(_fx/'two.pdf'))
_src = _fx/'two.pdf'
_scan = _d/'scan.pdf'; _scan.write_bytes(pdf_doc(pdf_doc(_src).extract_pages_to_bytes([0])).flatten_to_images(dpi=72))
_v = Vault(Path(tempfile.mkdtemp())/'v.db', offline=True)
_o = _v.read_doc(_scan, parse='llm', chat=Says('the dominat sequence transduction models'))
_id = _o['doc_id']
_r = _v.proofread(_id, ['a', 'b', 'c'], chats=dict(a=Says('the dominant sequence transduction models'), b=Says('the dominant sequence transduction models'),
c=Says('the dominat sequence transduktion models')))
test_eq(_r['changes'], 2); test_eq(_r['proposed'], 1)
_p = _v.changes(_id, 'proposed')[0]; test_eq((_p.before, _p.after, _p.by), ('dominat', 'dominant', ['a', 'b']))
_dt = _v.changes(_id, 'doubt')[0]; test_eq((_dt.after, _dt.by), ('transduktion', ['c']))
test_eq(_v.reject(_id, [_dt.id]), 1)
test_eq(_v.accept(_id)['accepted'], 1)
test_eq(_v.reject(_id, [_p.id, 'nope']), 0) # an accepted change and an unknown id are not set aside
test_eq(_v.pages(_id)[0].text, 'the dominant sequence transduction models'); test_eq(_v.pages(_id)[0].route, 'proof')
assert 'dominant' in _v.search('dominant sequence')[0]['snippet']
_md_ = _v.proof_md(_id)
assert '## Changed' in _md_ and 'dominat → dominant (a, b)' in _md_ and '## Not taken' in _md_
for _i, _a in enumerate('xy'): _v._proof_t().insert(dict(id=f'ov{_i}', doc_id=_id, store=_v.name, page=0, version=_v.pages(_id)[0].version, at=1 + _i, n=2, before='', after=_a, by='["a", "b"]', state='proposed', ts=0.))
test_fail(lambda: _v.accept(_id, ['ov0', 'ov1']), contains='overlap') # overlapping ranges are refused before any page changes
test_eq(_v.pages(_id)[0].text, 'the dominant sequence transduction models')
_v.proofread(_id, ['a'], chats=dict(a=Says('the dominant sequence transduction model')))
_v.reparse(_id, chat=Says('a different reading entirely'))
test_fail(lambda: _v.accept(_id, [c.id for c in _v.changes(_id, 'doubt')]), contains='older version')