Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/memory/document-evidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ function record(value:unknown):Record<string,unknown>{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)||value<min||value>max)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 {
Expand All @@ -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<string,DocumentCell>();
const documentFilename=filename(v.filename),parser=text(v.parser,128),segments:EvidenceSegment[]=[],cells=new Map<string,DocumentCell>();
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<string>(),identities=new Set<string>(),tables=new Map<string,DocumentCell[]>();
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 11 additions & 6 deletions src/memory/document-video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)||value<min||value>max)throw Error('Invalid video evidence count.');
return value;
Expand All @@ -41,19 +44,21 @@ 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'||start<previous||end<=start
||decoder.decode(encoded.subarray(start,end))!==label)throw Error('Video region does not match its UTF-8 source span.');
||decoder.decode(encoded.subarray(start,end))!==label||decoder.decode(encoded.subarray(previous,start)).trim())throw Error('Video region does not match its UTF-8 source span.');
previous=end;
const box=list(v.box,4);
if(box.length!==4||!box.every(n=>typeof n==='number'&&Number.isFinite(n)&&n>=0&&n<=1))throw Error('Invalid video OCR box.');
const rectangle=box as [number,number,number,number];
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{
Expand Down Expand Up @@ -84,7 +89,7 @@ export function parseVideoCatalogue(value:unknown,source:DocumentSource,episodeI
const relative=(position-start)*numerator;
if(relative<BigInt(requestedSeconds.at(-1)!)*denominator||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.');
Expand All @@ -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};
}

Expand Down
29 changes: 29 additions & 0 deletions tests/document-video.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Loading