From bea77581151843c66a28262b0038beeffbe81256 Mon Sep 17 00:00:00 2001 From: DrDrewCain Date: Sat, 12 Sep 2026 21:00:16 -0500 Subject: [PATCH] Align video catalogue limits and OCR coverage with native evidence --- src/memory/document-evidence.ts | 7 +++++-- src/memory/document-video.ts | 17 +++++++++++------ tests/document-video.test.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/memory/document-evidence.ts b/src/memory/document-evidence.ts index 4f30a52..c2e0e78 100644 --- a/src/memory/document-evidence.ts +++ b/src/memory/document-evidence.ts @@ -20,6 +20,9 @@ function record(value:unknown):Record{if(!value||typeof value!== function text(value:unknown,max=4096,empty=false):string { if(typeof value!=='string'||value.length>max||(!empty&&!value.length)||decoder.decode(encoder.encode(value))!==value)throw Error('Invalid document evidence text');return value; } +function filename(value:unknown):string{ + const result=text(value,2048);if(Array.from(result).length>1024)throw Error('Document filename exceeds its character limit');return result; +} function count(value:unknown,max:number,min=0):number{if(typeof value!=='number'||!Number.isSafeInteger(value)||valuemax)throw Error('Invalid document evidence count');return value;} function list(value:unknown,max:number):unknown[]{if(!Array.isArray(value)||value.length>max)throw Error('Document evidence exceeds its list limit');return value;} function attachment(value:unknown):DocumentAttachment { @@ -38,7 +41,7 @@ function references(value:unknown,allowed:string[]):CellReference[]{return list( export function parseDocumentEvidence(value:unknown,source:DocumentSource):DocumentEvidence { const v=record(value);sameAttachment(v.original,source.binding.original);sameAttachment(v.manifest,source.binding.manifest); const format=text(v.format,64);if(format!==source.binding.format)throw Error('Document format does not match this source'); - const filename=text(v.filename,1024),parser=text(v.parser,128),segments:EvidenceSegment[]=[],cells=new Map(); + const documentFilename=filename(v.filename),parser=text(v.parser,128),segments:EvidenceSegment[]=[],cells=new Map(); const metadata=v.metadata===undefined?{}:record(v.metadata),pdfOcr=metadata.pdf_ocr===undefined?undefined:parsePdfOcrEvidence(metadata.pdf_ocr); if(pdfOcr&&format!=='pdf')throw Error('PDF OCR settings do not match the document format'); const occupied=new Set(),identities=new Set(),tables=new Map(); @@ -81,7 +84,7 @@ export function parseDocumentEvidence(value:unknown,source:DocumentSource):Docum } }} } - return {filename,format,parser,segments,cells,tables:Array.from(tables,([locator,cells])=>({locator,cells:cells.sort((a,b)=>a.row-b.row||a.column-b.column)})),notes,pdfOcr,media:parseMediaTranscript(format,parser,metadata,list(v.segments,20000))}; + return {filename:documentFilename,format,parser,segments,cells,tables:Array.from(tables,([locator,cells])=>({locator,cells:cells.sort((a,b)=>a.row-b.row||a.column-b.column)})),notes,pdfOcr,media:parseMediaTranscript(format,parser,metadata,list(v.segments,20000))}; } export function cellExcerpt(cell:DocumentCell,evidence:DocumentEvidence){ const segment=evidence.segments[cell.segment],bytes=encoder.encode(segment.text),start=cell.start-segment.start,end=cell.end-segment.start; diff --git a/src/memory/document-video.ts b/src/memory/document-video.ts index b4e7952..0c5ad76 100644 --- a/src/memory/document-video.ts +++ b/src/memory/document-video.ts @@ -19,6 +19,9 @@ function text(value:unknown,max:number):string{ if(typeof value!=='string'||!value.length||value.length>max||decoder.decode(encoder.encode(value))!==value)throw Error('Invalid video evidence text.'); return value; } +function characters(value:unknown,max:number):string{ + const result=text(value,max*2);if(Array.from(result).length>max)throw Error('Video text exceeds its character limit.');return result; +} function integer(value:unknown,max:number,min=0):number{ if(typeof value!=='number'||!Number.isSafeInteger(value)||valuemax)throw Error('Invalid video evidence count.'); return value; @@ -41,10 +44,10 @@ function timeBase(value:unknown):[string,bigint,bigint]{ } function regions(value:unknown,content:string):VideoRegion[]{ const encoded=encoder.encode(content);let previous=0; - return list(value,10000).map(item=>{ - const v=record(item),start=integer(v.start,encoded.length),end=integer(v.end,encoded.length),label=text(v.text,100000); + const result=list(value,20000).map(item=>{ + const v=record(item),start=integer(v.start,encoded.length),end=integer(v.end,encoded.length),label=characters(v.text,100000); if(v.coordinate_space!=='normalized_displayed_frame_top_left'||starttypeof n==='number'&&Number.isFinite(n)&&n>=0&&n<=1))throw Error('Invalid video OCR box.'); @@ -52,8 +55,10 @@ function regions(value:unknown,content:string):VideoRegion[]{ if(rectangle[0]>=rectangle[2]||rectangle[1]>=rectangle[3])throw Error('Video OCR box is empty.'); const score=v.score??null; if(score!==null&&(typeof score!=='number'||!Number.isFinite(score)||score<0||score>1))throw Error('Invalid OCR recognizer score.'); - return {text:label,start,end,box:[...rectangle],score}; + return {text:label,start,end,box:[...rectangle] as [number,number,number,number],score}; }); + if(decoder.decode(encoded.subarray(previous)).trim())throw Error('Video OCR regions omit recognized text.'); + return result; } export function parseVideoCatalogue(value:unknown,source:DocumentSource,episodeId:number,space:string):VideoCatalogue{ @@ -84,7 +89,7 @@ export function parseVideoCatalogue(value:unknown,source:DocumentSource,episodeI const relative=(position-start)*numerator; if(relative=duration)throw Error('Video frame falls outside its sampling interval.'); totalBytes+=bytes;lastOrdinal=ordinal;lastTimestamp=position; - return {ordinal,presentationTimestamp:pts,requestedSeconds,width,height,bytes,sha256:hash(f.png_sha256),ocrEngine:text(f.ocr_engine,96), + return {ordinal,presentationTimestamp:pts,requestedSeconds,width,height,bytes,sha256:hash(f.png_sha256),ocrEngine:characters(f.ocr_engine,96), empty:f.empty,text:'',regions:[] as VideoRegion[]}; }); if(!frames.length||totalBytes>maxTotalBytes||covered+unavailableRequests!==requests.length)throw Error('Video sampling coverage is inconsistent.'); @@ -100,7 +105,7 @@ export function parseVideoCatalogue(value:unknown,source:DocumentSource,episodeI if(frames.some(frame=>!frame.empty&&!seen.has(String(frame.ordinal))))throw Error('Video frame text is missing.'); const modelRevision=text(v.model_revision,128); if(!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(modelRevision))throw Error('Invalid video OCR model revision.'); - return {document,sourceSha256,decoderRevision:hash(v.decoder_revision),policyRevision:text(v.policy_revision,96),modelRevision, + return {document,sourceSha256,decoderRevision:hash(v.decoder_revision),policyRevision:characters(v.policy_revision,96),modelRevision, streamIndex,timeBase:base,startTimestamp,durationTicks,intervalSeconds,decodedFrames,unavailableRequests,frames}; } diff --git a/tests/document-video.test.ts b/tests/document-video.test.ts index bc52d56..98d7561 100644 --- a/tests/document-video.test.ts +++ b/tests/document-video.test.ts @@ -122,3 +122,32 @@ test('all-empty sampled frames retain a catalogue with no fabricated source text f.catalogue.evidence.video.frames[0].empty=false; assert.throws(()=>parseVideoCatalogue(f.catalogue,f.source,7,'alpha')); }); + +test('one sampled frame may carry the native 20000-region allowance',()=>{ + const f=videoFixture(),segment=f.catalogue.evidence.segments[0]; + segment.text=Array.from({length:10001},()=> 'x').join('\n');f.source.content=segment.text; + segment.regions=Array.from({length:10001},(_,i)=>({...segment.regions[0],text:'x',start:i*2,end:i*2+1})); + assert.equal(parseVideoCatalogue(f.catalogue,f.source,7,'alpha').frames[0].regions.length,10001); +}); +test('OCR regions must account for nonwhitespace text between and after boxes',()=>{ + for(const [content,start,end] of [['prefix Café',7,12],['Café suffix',0,5]] as const){ + const f=videoFixture(),segment=f.catalogue.evidence.segments[0]; + segment.text=content;f.source.content=content;segment.regions[0].start=start;segment.regions[0].end=end; + assert.throws(()=>parseVideoCatalogue(f.catalogue,f.source,7,'alpha')); + } +}); +test('video OCR metadata uses native Unicode character limits',()=>{ + const f=videoFixture(),v=f.catalogue.evidence.video; + v.frames[0].ocr_engine='😀'.repeat(96);f.catalogue.evidence.segments[0].metadata.engine=v.frames[0].ocr_engine; + v.policy_revision='😀'.repeat(96); + assert.equal(parseVideoCatalogue(f.catalogue,f.source,7,'alpha').frames[0].ocrEngine,v.frames[0].ocr_engine); +}); +test('native supplementary-character filenames fit the catalogue character limit',()=>{ + const f=videoFixture();f.catalogue.evidence.filename='😀'.repeat(512)+'.mp4'; + assert.equal(parseVideoCatalogue(f.catalogue,f.source,7,'alpha').document.filename,f.catalogue.evidence.filename); +}); +test('a native-valid OCR region counts Unicode characters rather than UTF16 units',()=>{ + const f=videoFixture(),label='😀'.repeat(50001),segment=f.catalogue.evidence.segments[0]; + segment.text=label;f.source.content=label;segment.regions[0].text=label;segment.regions[0].end=new TextEncoder().encode(label).length; + assert.equal(parseVideoCatalogue(f.catalogue,f.source,7,'alpha').frames[0].regions[0].text,label); +});