@semiont/jobs 0.5.19 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
- import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, getPrimaryMediaType, textExtractionOf, assembleAnnotation, reconcileSelector, getLocaleEnglishName, isArray, isObject, isString, deriveViews } from '@semiont/core';
2
- import { deriveStorageUri } from '@semiont/content';
3
- import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
1
+ import { createTomlConfigLoader, didToAgent, baseUrl, STARTUP_FETCH_RETRY, retryWithBackoff, isTransientFetchError, busRequest, getPrimaryMediaType, textExtractionOf, assembleAnnotation, reconcileSelector, createFragmentSelector, getLocaleEnglishName, isArray, isObject, isString, deriveViews } from '@semiont/core';
2
+ import { deriveStorageUri, extractPdfTextLayer, locate } from '@semiont/content';
4
3
  import { generateAnnotationId } from '@semiont/event-sourcing';
4
+ import { withSpan, SpanKind, recordJobOutcome } from '@semiont/observability';
5
5
  import { homedir, hostname } from 'os';
6
6
  import { InMemorySessionStorage, setStoredSession, kbBackendUrl, SemiontClient, SemiontSession } from '@semiont/sdk';
7
7
  import { HttpTransport, HttpContentTransport } from '@semiont/http-transport';
@@ -10289,12 +10289,15 @@ function annotationDedupeKey(ann) {
10289
10289
  const target = ann.target;
10290
10290
  const selectors = Array.isArray(target?.selector) ? target.selector : [];
10291
10291
  const pos = selectors.find((s) => s.type === "TextPositionSelector");
10292
- return [
10293
- ann.motivation,
10294
- pos?.start ?? "?",
10295
- pos?.end ?? "?",
10296
- JSON.stringify(ann.body ?? null)
10297
- ].join("|");
10292
+ let anchor;
10293
+ if (pos) {
10294
+ anchor = `pos:${pos.start ?? "?"}:${pos.end ?? "?"}`;
10295
+ } else {
10296
+ const frags = selectors.filter((s) => s.type === "FragmentSelector").map((s) => s.value ?? "").join(",");
10297
+ const quote = selectors.find((s) => s.type === "TextQuoteSelector");
10298
+ anchor = `frag:${frags}|quote:${quote?.exact ?? ""}:${quote?.prefix ?? ""}:${quote?.suffix ?? ""}`;
10299
+ }
10300
+ return [ann.motivation, anchor, JSON.stringify(ann.body ?? null)].join("|");
10298
10301
  }
10299
10302
  function dedupeAnnotations(annotations) {
10300
10303
  const seen = /* @__PURE__ */ new Set();
@@ -10356,7 +10359,50 @@ function buildTextAnnotation(content, resourceId, userId, generator, motivation,
10356
10359
  ...body !== void 0 ? { body } : {}
10357
10360
  };
10358
10361
  }
10359
- async function processHighlightJob(content, inferenceClient, params, userId, generator, onProgress) {
10362
+ function buildPdfAnnotation(layer, resourceId, userId, generator, motivation, match, body) {
10363
+ const { rects, overlap } = locate(layer, match.start, match.end);
10364
+ const coveredText = overlap.length ? layer.text.substring(
10365
+ Math.min(...overlap.map((i) => i.start)),
10366
+ Math.max(...overlap.map((i) => i.end))
10367
+ ) : "";
10368
+ const normalize = (s) => s.replace(/\s+/g, " ").trim();
10369
+ if (rects.length === 0 || !normalize(coveredText).includes(normalize(match.exact))) {
10370
+ throw new Error(
10371
+ `buildPdfAnnotation invariant: covered text does not contain exact for resource ${resourceId}, motivation ${motivation}`
10372
+ );
10373
+ }
10374
+ const creator = didToAgent(userId);
10375
+ const wasAttributedTo = creator["@id"] === generator["@id"] ? [generator] : [creator, generator];
10376
+ return {
10377
+ "@context": "http://www.w3.org/ns/anno.jsonld",
10378
+ "type": "Annotation",
10379
+ "id": generateAnnotationId(),
10380
+ motivation,
10381
+ creator,
10382
+ generator,
10383
+ wasAttributedTo,
10384
+ created: (/* @__PURE__ */ new Date()).toISOString(),
10385
+ target: {
10386
+ type: "SpecificResource",
10387
+ source: resourceId,
10388
+ selector: [
10389
+ ...rects.map((coord) => ({
10390
+ type: "FragmentSelector",
10391
+ conformsTo: "http://tools.ietf.org/rfc/rfc3778",
10392
+ value: createFragmentSelector(coord)
10393
+ })),
10394
+ {
10395
+ type: "TextQuoteSelector",
10396
+ exact: match.exact,
10397
+ ...match.prefix && { prefix: match.prefix },
10398
+ ...match.suffix && { suffix: match.suffix }
10399
+ }
10400
+ ]
10401
+ },
10402
+ ...body !== void 0 ? { body } : {}
10403
+ };
10404
+ }
10405
+ async function processHighlightJob(content, inferenceClient, params, buildAnnotation, onProgress) {
10360
10406
  onProgress(10, "Loading resource...", "analyzing");
10361
10407
  onProgress(30, "Analyzing text...", "analyzing");
10362
10408
  const highlights = await AnnotationDetection.detectHighlights(
@@ -10368,7 +10414,7 @@ async function processHighlightJob(content, inferenceClient, params, userId, gen
10368
10414
  );
10369
10415
  onProgress(60, `Creating ${highlights.length} annotations...`, "creating");
10370
10416
  const annotations = dedupeAnnotations(highlights.map(
10371
- (h) => buildTextAnnotation(content, params.resourceId, userId, generator, "highlighting", h)
10417
+ (h) => buildAnnotation("highlighting", h)
10372
10418
  ));
10373
10419
  onProgress(100, `Complete! Created ${annotations.length} highlights`, "creating");
10374
10420
  return {
@@ -10376,7 +10422,7 @@ async function processHighlightJob(content, inferenceClient, params, userId, gen
10376
10422
  result: { highlightsFound: highlights.length, highlightsCreated: annotations.length }
10377
10423
  };
10378
10424
  }
10379
- async function processCommentJob(content, inferenceClient, params, userId, generator, onProgress) {
10425
+ async function processCommentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
10380
10426
  onProgress(10, "Loading resource...", "analyzing");
10381
10427
  onProgress(30, "Analyzing text...", "analyzing");
10382
10428
  const comments = await AnnotationDetection.detectComments(
@@ -10395,7 +10441,7 @@ async function processCommentJob(content, inferenceClient, params, userId, gener
10395
10441
  // Match the pre-#651 CommentAnnotationWorker: include format and
10396
10442
  // language on the body TextualBody. Optional in the schema, but
10397
10443
  // consumers that do language-aware rendering rely on them.
10398
- buildTextAnnotation(content, params.resourceId, userId, generator, "commenting", c, [
10444
+ buildAnnotation("commenting", c, [
10399
10445
  { type: "TextualBody", value: c.comment, purpose: "commenting", format: "text/plain", language: bodyLanguage }
10400
10446
  ])
10401
10447
  )
@@ -10406,7 +10452,7 @@ async function processCommentJob(content, inferenceClient, params, userId, gener
10406
10452
  result: { commentsFound: comments.length, commentsCreated: annotations.length }
10407
10453
  };
10408
10454
  }
10409
- async function processAssessmentJob(content, inferenceClient, params, userId, generator, onProgress) {
10455
+ async function processAssessmentJob(content, inferenceClient, params, buildAnnotation, onProgress) {
10410
10456
  onProgress(10, "Loading resource...", "analyzing");
10411
10457
  onProgress(30, "Analyzing text...", "analyzing");
10412
10458
  const assessments = await AnnotationDetection.detectAssessments(
@@ -10428,7 +10474,7 @@ async function processAssessmentJob(content, inferenceClient, params, userId, ge
10428
10474
  // purpose='describing' — that loses the "this is an assessment, not
10429
10475
  // a description" signal and breaks existing readers that access
10430
10476
  // `body.value` directly on the object.
10431
- buildTextAnnotation(content, params.resourceId, userId, generator, "assessing", a, {
10477
+ buildAnnotation("assessing", a, {
10432
10478
  type: "TextualBody",
10433
10479
  value: a.assessment,
10434
10480
  purpose: "assessing",
@@ -10443,7 +10489,7 @@ async function processAssessmentJob(content, inferenceClient, params, userId, ge
10443
10489
  result: { assessmentsFound: assessments.length, assessmentsCreated: annotations.length }
10444
10490
  };
10445
10491
  }
10446
- async function processReferenceJob(content, inferenceClient, params, userId, generator, onProgress, logger2) {
10492
+ async function processReferenceJob(content, inferenceClient, params, buildAnnotation, onProgress, logger2) {
10447
10493
  const entityTypeNames = params.entityTypes.map(String);
10448
10494
  const requestParams = [{ label: "Entity types", value: entityTypeNames.join(", ") }];
10449
10495
  const completedEntityTypes = [];
@@ -10500,15 +10546,7 @@ async function processReferenceJob(content, inferenceClient, params, userId, gen
10500
10546
  anchorMethod: reconciled.anchorMethod
10501
10547
  });
10502
10548
  }
10503
- const ann = buildTextAnnotation(
10504
- content,
10505
- params.resourceId,
10506
- userId,
10507
- generator,
10508
- "linking",
10509
- toMatch(reconciled),
10510
- unresolvedBody
10511
- );
10549
+ const ann = buildAnnotation("linking", toMatch(reconciled), unresolvedBody);
10512
10550
  allAnnotations.push(ann);
10513
10551
  totalEmitted++;
10514
10552
  }
@@ -10520,7 +10558,7 @@ async function processReferenceJob(content, inferenceClient, params, userId, gen
10520
10558
  result: { totalFound, totalEmitted: annotations.length, errors }
10521
10559
  };
10522
10560
  }
10523
- async function processTagJob(content, inferenceClient, params, userId, generator, onProgress) {
10561
+ async function processTagJob(content, inferenceClient, params, buildAnnotation, onProgress) {
10524
10562
  onProgress(10, "Loading resource...", "analyzing");
10525
10563
  onProgress(30, "Analyzing text for tags...", "analyzing");
10526
10564
  const allTags = [];
@@ -10539,7 +10577,7 @@ async function processTagJob(content, inferenceClient, params, userId, generator
10539
10577
  const bodyLanguage = params.language ?? "en";
10540
10578
  const annotations = dedupeAnnotations(tags.map((t) => {
10541
10579
  const category = t.category ?? "unknown";
10542
- return buildTextAnnotation(content, params.resourceId, userId, generator, "tagging", t, [
10580
+ return buildAnnotation("tagging", t, [
10543
10581
  { type: "TextualBody", value: category, purpose: "tagging", format: "text/plain", language: bodyLanguage },
10544
10582
  { type: "TextualBody", value: params.schema.id, purpose: "classifying", format: "text/plain" }
10545
10583
  ]);
@@ -10603,7 +10641,29 @@ async function processGenerationJob(inferenceClient, params, onProgress, logger2
10603
10641
  };
10604
10642
  }
10605
10643
 
10606
- // src/worker-process.ts
10644
+ // src/workers/detection/prepare-detection.ts
10645
+ async function prepareDetection(strategy, session, resourceId, userId, generator) {
10646
+ switch (strategy) {
10647
+ case "decode": {
10648
+ const text = await session.client.browse.resourceContent(resourceId);
10649
+ return {
10650
+ text,
10651
+ buildAnnotation: (motivation, match, body) => buildTextAnnotation(text, resourceId, userId, generator, motivation, match, body)
10652
+ };
10653
+ }
10654
+ case "pdf-text-layer": {
10655
+ const { data } = await session.client.browse.resourceRepresentation(resourceId);
10656
+ const layer = await extractPdfTextLayer(new Uint8Array(data));
10657
+ if (!layer) return null;
10658
+ return {
10659
+ text: layer.text,
10660
+ buildAnnotation: (motivation, match, body) => buildPdfAnnotation(layer, resourceId, userId, generator, motivation, match, body)
10661
+ };
10662
+ }
10663
+ case "none":
10664
+ return null;
10665
+ }
10666
+ }
10607
10667
  async function emitEvent(session, channel, payload) {
10608
10668
  await session.client.transport.emit(channel, payload);
10609
10669
  }
@@ -10675,16 +10735,23 @@ async function handleJobInner(adapter, config, job) {
10675
10735
  adapter.failJob(jobId, `Worker not configured for job type: ${jobType}`);
10676
10736
  return;
10677
10737
  }
10738
+ let source = null;
10678
10739
  if (jobType !== "generation") {
10679
10740
  const descriptor = await session.client.browse.resource(resourceId);
10680
10741
  const mediaType = getPrimaryMediaType(descriptor);
10681
- const extraction = mediaType ? textExtractionOf(mediaType) : "none";
10682
- if (extraction === "pdf-text-layer") {
10683
- throw new Error(`Cannot run ${jobType} on resource ${resourceId}: PDF text-layer detection is not yet supported`);
10684
- }
10685
- if (extraction !== "decode") {
10742
+ const strategy = mediaType ? textExtractionOf(mediaType) : "none";
10743
+ if (strategy === "none") {
10686
10744
  throw new Error(`Cannot run ${jobType} on resource ${resourceId}: media type '${mediaType ?? "unknown"}' has no extractable text to analyze`);
10687
10745
  }
10746
+ source = await prepareDetection(strategy, session, resourceId, userId, generator);
10747
+ if (!source) {
10748
+ await emitEvent(session, "job:complete", {
10749
+ ...lifecycleBase,
10750
+ result: { declined: true, reason: "no-text-layer", message: "This PDF has no extractable text layer (scanned or image-only); detection is not supported." }
10751
+ });
10752
+ adapter.completeJob();
10753
+ return;
10754
+ }
10688
10755
  }
10689
10756
  const onProgress = (percentage, message, stage, extra) => {
10690
10757
  adapter.touchActivity();
@@ -10701,17 +10768,12 @@ async function handleJobInner(adapter, config, job) {
10701
10768
  }).catch(() => {
10702
10769
  });
10703
10770
  };
10704
- const fetchContent = async () => {
10705
- return session.client.browse.resourceContent(resourceId);
10706
- };
10707
10771
  if (jobType === "highlight-annotation") {
10708
- const content = await fetchContent();
10709
10772
  const { annotations, result } = await processHighlightJob(
10710
- content,
10773
+ source.text,
10711
10774
  inferenceClient,
10712
10775
  job.params,
10713
- userId,
10714
- generator,
10776
+ source.buildAnnotation,
10715
10777
  onProgress
10716
10778
  );
10717
10779
  for (const ann of annotations) {
@@ -10723,13 +10785,11 @@ async function handleJobInner(adapter, config, job) {
10723
10785
  });
10724
10786
  adapter.completeJob();
10725
10787
  } else if (jobType === "comment-annotation") {
10726
- const content = await fetchContent();
10727
10788
  const { annotations, result } = await processCommentJob(
10728
- content,
10789
+ source.text,
10729
10790
  inferenceClient,
10730
10791
  job.params,
10731
- userId,
10732
- generator,
10792
+ source.buildAnnotation,
10733
10793
  onProgress
10734
10794
  );
10735
10795
  for (const ann of annotations) {
@@ -10741,13 +10801,11 @@ async function handleJobInner(adapter, config, job) {
10741
10801
  });
10742
10802
  adapter.completeJob();
10743
10803
  } else if (jobType === "assessment-annotation") {
10744
- const content = await fetchContent();
10745
10804
  const { annotations, result } = await processAssessmentJob(
10746
- content,
10805
+ source.text,
10747
10806
  inferenceClient,
10748
10807
  job.params,
10749
- userId,
10750
- generator,
10808
+ source.buildAnnotation,
10751
10809
  onProgress
10752
10810
  );
10753
10811
  for (const ann of annotations) {
@@ -10759,13 +10817,11 @@ async function handleJobInner(adapter, config, job) {
10759
10817
  });
10760
10818
  adapter.completeJob();
10761
10819
  } else if (jobType === "reference-annotation") {
10762
- const content = await fetchContent();
10763
10820
  const { annotations, result } = await processReferenceJob(
10764
- content,
10821
+ source.text,
10765
10822
  inferenceClient,
10766
10823
  job.params,
10767
- userId,
10768
- generator,
10824
+ source.buildAnnotation,
10769
10825
  onProgress,
10770
10826
  config.logger
10771
10827
  );
@@ -10778,13 +10834,11 @@ async function handleJobInner(adapter, config, job) {
10778
10834
  });
10779
10835
  adapter.completeJob();
10780
10836
  } else if (jobType === "tag-annotation") {
10781
- const content = await fetchContent();
10782
10837
  const { annotations, result } = await processTagJob(
10783
- content,
10838
+ source.text,
10784
10839
  inferenceClient,
10785
10840
  job.params,
10786
- userId,
10787
- generator,
10841
+ source.buildAnnotation,
10788
10842
  onProgress
10789
10843
  );
10790
10844
  for (const ann of annotations) {