@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.
- package/dist/index.d.ts +21 -6
- package/dist/index.js +21 -75
- package/dist/index.js.map +1 -1
- package/dist/worker-main.js +110 -56
- package/dist/worker-main.js.map +1 -1
- package/package.json +8 -8
package/dist/worker-main.js
CHANGED
|
@@ -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
|
-
|
|
10293
|
-
|
|
10294
|
-
pos
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
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
|
-
|
|
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) =>
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
|
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/
|
|
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
|
|
10682
|
-
if (
|
|
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
|
-
|
|
10773
|
+
source.text,
|
|
10711
10774
|
inferenceClient,
|
|
10712
10775
|
job.params,
|
|
10713
|
-
|
|
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
|
-
|
|
10789
|
+
source.text,
|
|
10729
10790
|
inferenceClient,
|
|
10730
10791
|
job.params,
|
|
10731
|
-
|
|
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
|
-
|
|
10805
|
+
source.text,
|
|
10747
10806
|
inferenceClient,
|
|
10748
10807
|
job.params,
|
|
10749
|
-
|
|
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
|
-
|
|
10821
|
+
source.text,
|
|
10765
10822
|
inferenceClient,
|
|
10766
10823
|
job.params,
|
|
10767
|
-
|
|
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
|
-
|
|
10838
|
+
source.text,
|
|
10784
10839
|
inferenceClient,
|
|
10785
10840
|
job.params,
|
|
10786
|
-
|
|
10787
|
-
generator,
|
|
10841
|
+
source.buildAnnotation,
|
|
10788
10842
|
onProgress
|
|
10789
10843
|
);
|
|
10790
10844
|
for (const ann of annotations) {
|