strom-research 1.19.0-beta.1 → 1.19.0-beta.3
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/CHANGELOG.cs.md +8 -1
- package/CHANGELOG.de.md +10 -1
- package/CHANGELOG.md +9 -1
- package/assets/lang/cs.json +1 -0
- package/assets/lang/de.json +1 -0
- package/dist/cli/menu-settings.js +1 -1
- package/dist/cli/ui.js +1 -0
- package/dist/commands/media.js +23 -6
- package/dist/commands/phone.js +3 -1
- package/dist/commands/read.js +33 -3
- package/dist/core/document.js +11 -3
- package/dist/core/media.js +66 -0
- package/dist/core/phone.js +1 -1
- package/dist/core/reader.js +6 -2
- package/dist/core/tree.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.cs.md
CHANGED
|
@@ -7,7 +7,7 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
7
7
|
|
|
8
8
|
## 1.19.0
|
|
9
9
|
|
|
10
|
-
*Připravuje se: bety do 1.19.0-beta.
|
|
10
|
+
*Připravuje se: bety do 1.19.0-beta.3. Oddíl bude úplný s vydáním.*
|
|
11
11
|
|
|
12
12
|
- **Na archiv, který zmlkl, se čeká, místo opakovaných dotazů.** Když archiv přestane odpovídat, strom ho nechá na
|
|
13
13
|
pokoji hodinu — a když týž den mlčí dál, dvě a pak čtyři hodiny — a agent se dozví, do kdy: mezitím dělá
|
|
@@ -19,6 +19,13 @@ Změny každé verze ve zkratce. Úplné poznámky a instalační řádky jsou n
|
|
|
19
19
|
nastavení dál říkají, co jde uvolnit.
|
|
20
20
|
- **Digitální knihovny napoprvé správně.** U Krameria Národní knihovny ČR a Moravské zemské knihovny a u ANNO
|
|
21
21
|
Rakouské národní knihovny dostane agent jejich vlastní vyhledávací adresy — méně neúspěšných dotazů a hádání.
|
|
22
|
+
- **Ostřejší část snímku se opravdu čte.** Když z archivu přišla část snímku v lepším rozlišení, agent i jeho čtenáři
|
|
23
|
+
se na to místo dívají v ní — i když vyznačené místo kousek přesahuje, zbytek se řekne —, ne na celý zmenšený snímek;
|
|
24
|
+
ostřejší část, která místo pokrývá jen zčásti, se uvede i s tím, jak se na ni podívat.
|
|
25
|
+
- **Dokument se přečte spolehlivěji.** Čtenář zapisuje čtení do své zprávy průběžně, a čtení dvou čtenářů, kde četl
|
|
26
|
+
jen jeden, to tak řekne — nikdy, že se shodují.
|
|
27
|
+
- **Propojení s telefonem čeká na výzkum, neříká se jako chyba.** Než se po spuštění počítače spustí výzkum, `strom
|
|
28
|
+
phone` i nastavení řeknou, že propojení je zapnuté a jeho server se znovu spustí se spuštěním výzkumu.
|
|
22
29
|
|
|
23
30
|
## 1.18.0
|
|
24
31
|
|
package/CHANGELOG.de.md
CHANGED
|
@@ -7,7 +7,7 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
7
7
|
|
|
8
8
|
## 1.19.0
|
|
9
9
|
|
|
10
|
-
*In Vorbereitung: die Betas bis 1.19.0-beta.
|
|
10
|
+
*In Vorbereitung: die Betas bis 1.19.0-beta.3. Der Abschnitt ist mit dem Release vollständig.*
|
|
11
11
|
|
|
12
12
|
- **Auf ein verstummtes Archiv wird gewartet, statt es immer wieder zu fragen.** Antwortet ein Archiv nicht mehr,
|
|
13
13
|
lässt strom es eine Stunde in Ruhe — zwei, dann vier, wenn es am selben Tag still bleibt — und der Agent erfährt, bis
|
|
@@ -21,6 +21,15 @@ Die Änderungen jeder Version in Kürze. Die vollständigen Hinweise und die Ins
|
|
|
21
21
|
- **Digitale Bibliotheken beim ersten Versuch richtig durchsucht.** Für Kramerius der Tschechischen
|
|
22
22
|
Nationalbibliothek und der Mährischen Landesbibliothek und für ANNO der Österreichischen Nationalbibliothek bekommt
|
|
23
23
|
der Agent die eigenen Suchadressen der Bibliotheken — weniger fehlgeschlagene Anfragen und weniger Raten.
|
|
24
|
+
- **Ein schärfer geholter Teil eines Scans wird gelesen.** Kam ein Teil eines Scans in mehr Detail aus dem Archiv,
|
|
25
|
+
sehen der Agent und seine Leser diese Stelle darin an — auch wenn die markierte Stelle etwas darüber hinausreicht,
|
|
26
|
+
der Rest wird genannt — statt des ganzen, verkleinerten Scans; ein schärferer Teil, der die Stelle nur zum Teil
|
|
27
|
+
zeigt, wird mit dem Weg genannt, ihn anzusehen.
|
|
28
|
+
- **Ein Dokument wird verlässlicher gelesen.** Ein Leser schreibt seine Lesung laufend in seinen Bericht, und eine
|
|
29
|
+
Lesung zweier Leser, bei der nur einer las, sagt das so — nie, dass sie übereinstimmen.
|
|
30
|
+
- **Die Verbindung mit dem Telefon wartet auf die Forschung, nicht als Fehler genannt.** Bevor die Forschung nach dem
|
|
31
|
+
Neustart des Computers startet, sagen `strom phone` und die Einstellungen, dass die Verbindung an ist und ihr Server
|
|
32
|
+
mit der Forschung wieder startet.
|
|
24
33
|
|
|
25
34
|
## 1.18.0
|
|
26
35
|
|
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,7 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
7
7
|
|
|
8
8
|
## 1.19.0
|
|
9
9
|
|
|
10
|
-
*In preparation: the betas up to 1.19.0-beta.
|
|
10
|
+
*In preparation: the betas up to 1.19.0-beta.3. The section is complete with the release.*
|
|
11
11
|
|
|
12
12
|
- **An archive that went silent is waited for, not asked again and again.** When an archive stops answering, strom
|
|
13
13
|
leaves it alone for an hour — two, then four when it stays silent the same day — and the agent hears until when:
|
|
@@ -20,6 +20,14 @@ The changes of each version in short. The full notes and the install lines are o
|
|
|
20
20
|
- **Digital libraries searched right at the first try.** For Kramerius of the Czech National Library and of the
|
|
21
21
|
Moravian Library and for ANNO of the Austrian National Library, the agent gets the libraries' own search
|
|
22
22
|
addresses — fewer failed requests and less guessing.
|
|
23
|
+
- **A part of a scan fetched sharper is read from.** Where a part of a scan came from the archive in more detail, the
|
|
24
|
+
agent and its readers look at that place in it — also when the place they mark reaches a little past it, the rest
|
|
25
|
+
said — instead of the whole scan, reduced; a sharper part that holds only some of the place is named with the way
|
|
26
|
+
to look at it.
|
|
27
|
+
- **A document read more surely.** A reader writes its reading into its report as it goes, and a reading of two readers where
|
|
28
|
+
only one read says so, never that they agree.
|
|
29
|
+
- **The phone connection waits for the research, not said as an error.** Before the research starts after the computer
|
|
30
|
+
started again, `strom phone` and the settings say the connection is on and its server starts again with the research.
|
|
23
31
|
|
|
24
32
|
## 1.18.0
|
|
25
33
|
|
package/assets/lang/cs.json
CHANGED
|
@@ -1298,6 +1298,7 @@
|
|
|
1298
1298
|
"ui.phone.window.no": "Zrušit",
|
|
1299
1299
|
"ui.phone.on": "propojení s telefonem: zapnuto, v místní síti na {urls}",
|
|
1300
1300
|
"ui.phone.starting": "propojení s telefonem: zapnuto, jeho server se spouští",
|
|
1301
|
+
"ui.phone.waiting": "propojení s telefonem: zapnuto, jeho server se znovu spustí se spuštěním výzkumu",
|
|
1301
1302
|
"ui.phone.error": "propojení s telefonem: zapnuto, ale telefon se k němu nedostane: {why}",
|
|
1302
1303
|
"ui.phone.why.no-lan": "počítač není v žádné místní síti (Wi-Fi ani kabel)",
|
|
1303
1304
|
"ui.phone.why.port-busy": "porty 47800–47819 mají obsazené jiné programy",
|
package/assets/lang/de.json
CHANGED
|
@@ -1298,6 +1298,7 @@
|
|
|
1298
1298
|
"ui.phone.window.no": "Abbrechen",
|
|
1299
1299
|
"ui.phone.on": "Verbindung mit dem Telefon: an, im lokalen Netz unter {urls}",
|
|
1300
1300
|
"ui.phone.starting": "Verbindung mit dem Telefon: an, ihr Server startet",
|
|
1301
|
+
"ui.phone.waiting": "Verbindung mit dem Telefon: an, ihr Server startet wieder, sobald die Forschung startet",
|
|
1301
1302
|
"ui.phone.error": "Verbindung mit dem Telefon: an, aber ein Telefon erreicht sie nicht: {why}",
|
|
1302
1303
|
"ui.phone.why.no-lan": "der Computer ist in keinem lokalen Netz (weder WLAN noch Kabel)",
|
|
1303
1304
|
"ui.phone.why.port-busy": "die Ports 47800–47819 belegen andere Programme",
|
|
@@ -180,7 +180,7 @@ async function phoneStep(ctx, run, lang) {
|
|
|
180
180
|
await pause(ctx, lang);
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
|
-
const i = await ctx.choose(t(s.state === "on" ? "ui.phone.on" : "ui.phone.error", { urls: s.addresses.map((a) => a.url).join(", "), why: t(`ui.phone.why.${s.error?.code ?? "bind-failed"}`) }), [{ label: t("ui.phone.menu.off") }, ...s.phones.map((p) => ({ label: t("ui.phone.menu.remove", { name: p.name || p.id, date: p.pairedAt.slice(0, 10) }) }))], 0, { back: t("ui.browse.back") });
|
|
183
|
+
const i = await ctx.choose(t(s.state === "on" ? "ui.phone.on" : s.error?.waiting ? "ui.phone.waiting" : "ui.phone.error", { urls: s.addresses.map((a) => a.url).join(", "), why: t(`ui.phone.why.${s.error?.code ?? "bind-failed"}`) }), [{ label: t("ui.phone.menu.off") }, ...s.phones.map((p) => ({ label: t("ui.phone.menu.remove", { name: p.name || p.id, date: p.pairedAt.slice(0, 10) }) }))], 0, { back: t("ui.browse.back") });
|
|
184
184
|
if (i === undefined)
|
|
185
185
|
return;
|
|
186
186
|
if (i === 0)
|
package/dist/cli/ui.js
CHANGED
|
@@ -1198,6 +1198,7 @@ export const UI = {
|
|
|
1198
1198
|
"ui.phone.window.no": "Cancel",
|
|
1199
1199
|
"ui.phone.on": "phone connection: on, on the local network at {urls}",
|
|
1200
1200
|
"ui.phone.starting": "phone connection: on, its server is starting",
|
|
1201
|
+
"ui.phone.waiting": "phone connection: on, its server starts again when the research starts",
|
|
1201
1202
|
"ui.phone.error": "phone connection: on, but a phone cannot reach it: {why}",
|
|
1202
1203
|
"ui.phone.why.no-lan": "this computer is on no local network (Wi-Fi or a cable)",
|
|
1203
1204
|
"ui.phone.why.port-busy": "the ports 47800–47819 are taken by other programs",
|
package/dist/commands/media.js
CHANGED
|
@@ -9,7 +9,7 @@ import { register } from "../cli/registry.js";
|
|
|
9
9
|
import { counted, lines, moreLine, paginate, runs, shellArg, table, truncate } from "../cli/format.js";
|
|
10
10
|
import { notFound, UsageError } from "../core/errors.js";
|
|
11
11
|
import { makeNote } from "../core/actions.js";
|
|
12
|
-
import { clipText, collectFiles, coveringPart, fileSha256, findImage, imageNumbers, imageOfRef, imageRange, IMAGE_FILE_EXT, inboxFolders, inputPath, isWhole, mimeOf, onlyParts, otherCopies, parseImageList, regionText,
|
|
12
|
+
import { clipText, collectFiles, coveringPart, fileSha256, findImage, imageNumbers, imageOfRef, imageRange, IMAGE_FILE_EXT, inboxFolders, inputPath, isWhole, mimeOf, onlyParts, otherCopies, parseImageList, regionText, sharperCover, storeShared, wholeSizeOf } from "../core/media.js";
|
|
13
13
|
import { create, normId, requireRecord, update } from "../core/records.js";
|
|
14
14
|
import { exifOrientation, imageSizeOfFile } from "../image/index.js";
|
|
15
15
|
import { fileToShown, shownToFile, turnsAcross } from "../image/image.js";
|
|
@@ -522,12 +522,17 @@ function oneView(ctx, tree, shared, all, target, asked, meta = {}, noSharper = f
|
|
|
522
522
|
let shown = t.media;
|
|
523
523
|
let v;
|
|
524
524
|
let sharper;
|
|
525
|
+
let near;
|
|
525
526
|
if (t.media?.width && t.media.height && !t.media.part) {
|
|
526
527
|
// the place asked is of the picture shown (upright by its EXIF); parts are kept as the file lies
|
|
527
528
|
const o = mediaOrient(shared, t.media);
|
|
528
529
|
const [W, H] = turnsAcross(o) ? [t.media.height, t.media.width] : [t.media.width, t.media.height];
|
|
529
530
|
const px = viewRegion(spec, W, H);
|
|
530
|
-
|
|
531
|
+
// a crop drawn a little past a part fetched sharper is shown from it, cut to it (what it leaves out said)
|
|
532
|
+
const cover = sharperCover(all, t.media, shownToFile({ x: px.x / W, y: px.y / H, w: px.w / W, h: px.h / H }, o));
|
|
533
|
+
const s = cover.use && { ...cover.use, ...(cover.use.cut ? { cut: fileToShown(cover.use.cut, o), out: (cover.use.out ?? []).map((x) => fileToShown(x, o)) } : {}) };
|
|
534
|
+
if (cover.near)
|
|
535
|
+
near = { ...cover.near, shows: fileToShown(cover.near.shows, o) };
|
|
531
536
|
if (s) {
|
|
532
537
|
sharper = s;
|
|
533
538
|
shown = s.part;
|
|
@@ -560,7 +565,7 @@ function oneView(ctx, tree, shared, all, target, asked, meta = {}, noSharper = f
|
|
|
560
565
|
: whole && !whole.part
|
|
561
566
|
? clipText({ media: whole.id, region: fileToShown(inWhole, mediaOrient(shared, whole)) })
|
|
562
567
|
: clipText({ media: t.key, region: { x: v.region.x / v.original.width, y: v.region.y / v.original.height, w: v.region.w / v.original.width, h: v.region.h / v.original.height } });
|
|
563
|
-
return { v, page, ...(t !== target ? { part: t.media } : {}), ...(sharper ? { sharper } : {}), ...(fetchPart ? { fetchPart } : {}), ...(clip ? { clip } : {}) };
|
|
568
|
+
return { v, page, ...(t !== target ? { part: t.media } : {}), ...(sharper ? { sharper } : {}), ...(near ? { near } : {}), ...(fetchPart ? { fetchPart } : {}), ...(clip ? { clip } : {}) };
|
|
564
569
|
}
|
|
565
570
|
/**
|
|
566
571
|
* An image registered only in parts (no whole image of it), named as an image of its record set: a half or a crop of
|
|
@@ -592,11 +597,22 @@ function partLine(p) {
|
|
|
592
597
|
return `from ${p.id}, the part of the image that covers it (part ${regionText(p.part)}; the image is registered only in parts)`;
|
|
593
598
|
}
|
|
594
599
|
function sharperLine(s, t) {
|
|
595
|
-
|
|
600
|
+
const from = `from ${s.part.id}, ${s.part.part ? "a part of the image fetched sharper" : "a sharper copy of the image"} (${s.gain.toFixed(1)}× the detail of ${s.part.part ? "the whole image" : t.media.id})`;
|
|
601
|
+
if (!s.cut)
|
|
602
|
+
return from;
|
|
603
|
+
// a crop drawn a little past the part: the view is of what the part holds; the strips it leaves out from the whole image
|
|
604
|
+
const out = (s.out ?? []).map((x) => `strom media view ${t.ref} --crop ${regionText(x)}`);
|
|
605
|
+
return `${from}, cut to it: ${regionText(s.cut)} of the place asked${out.length ? ` — the rest (the whole image, less sharp): ${out.join(" · ")}` : ""}`;
|
|
596
606
|
}
|
|
607
|
+
/** A sharper part beside a view from the whole image: where it shows the place, and the view of that from it. */
|
|
608
|
+
function nearLine(n, t) {
|
|
609
|
+
return `${n.part.id}, a part of the image fetched sharper (${n.gain.toFixed(1)}× the detail), shows ${regionText(n.shows)} of this place, not all of it — this view is from the whole image; that place sharper: strom media view ${t.ref} --crop ${regionText(n.shows)}`;
|
|
610
|
+
}
|
|
611
|
+
/** A place as the text says it: to a thousandth. */
|
|
612
|
+
const rounded = (r) => ({ x: Math.round(r.x * 1000) / 1000, y: Math.round(r.y * 1000) / 1000, w: Math.round(r.w * 1000) / 1000, h: Math.round(r.h * 1000) / 1000 });
|
|
597
613
|
function viewData(t, r) {
|
|
598
614
|
const v = r.v;
|
|
599
|
-
return { view: v.file, width: v.width, height: v.height, scale: v.scale, region: v.region, original: v.original, image: r.part?.id ?? t.key, ...(r.sharper ? { from: r.sharper.part.id } : r.part ? { from: r.part.id } : {}), page: r.page || undefined, ...(r.clip ? { clip: r.clip } : {}) };
|
|
615
|
+
return { view: v.file, width: v.width, height: v.height, scale: v.scale, region: v.region, original: v.original, image: r.part?.id ?? t.key, ...(r.sharper ? { from: r.sharper.part.id, ...(r.sharper.cut ? { cut: rounded(r.sharper.cut), left: (r.sharper.out ?? []).map(rounded) } : {}) } : r.part ? { from: r.part.id } : {}), ...(r.near ? { sharper: { part: r.near.part.id, gain: Math.round(r.near.gain * 10) / 10, shows: rounded(r.near.shows) } } : {}), page: r.page || undefined, ...(r.clip ? { clip: r.clip } : {}) };
|
|
600
616
|
}
|
|
601
617
|
register({
|
|
602
618
|
path: ["media", "add"],
|
|
@@ -955,7 +971,7 @@ register({
|
|
|
955
971
|
const t = targets[0];
|
|
956
972
|
const r = oneView(ctx, tree, shared, all, t, { ...spec, max: maxOf(parts[0], t), half: parts[0].half, crop: parts[0].crop }, { capFrom: capFromOf(t) }, noSharperOf(t));
|
|
957
973
|
return {
|
|
958
|
-
text: lines(describeView(r.v, (p) => ctx.display(p), r.fetchPart, isOverview(parts[0])), r.clip ? `the source of an entry read here: --clip ${r.clip}` : undefined, r.sharper ? sharperLine(r.sharper, t) : undefined, r.part ? partLine(r.part) : undefined, r.page ? `page ${r.page}` : undefined),
|
|
974
|
+
text: lines(describeView(r.v, (p) => ctx.display(p), r.fetchPart, isOverview(parts[0])), r.clip ? `the source of an entry read here: --clip ${r.clip}` : undefined, r.sharper ? sharperLine(r.sharper, t) : undefined, r.near ? nearLine(r.near, t) : undefined, r.part ? partLine(r.part) : undefined, r.page ? `page ${r.page}` : undefined),
|
|
959
975
|
data: viewData(t, r),
|
|
960
976
|
};
|
|
961
977
|
}
|
|
@@ -988,6 +1004,7 @@ register({
|
|
|
988
1004
|
ctx.display(r.v.file),
|
|
989
1005
|
` ${[`${t.ref}${t.ref !== t.key ? ` (${t.key}${r.page ? `, page ${r.page}` : ""})` : r.page ? ` (page ${r.page})` : ""}`, label, note, viewLine(r.v), viewSize(r.v), r.clip ? `--clip ${r.clip}` : undefined].filter(Boolean).join(" · ")}`,
|
|
990
1006
|
r.sharper ? ` ${sharperLine(r.sharper, t)}` : undefined,
|
|
1007
|
+
r.near ? ` ${nearLine(r.near, t)}` : undefined,
|
|
991
1008
|
r.part ? ` ${partLine(r.part)}` : undefined,
|
|
992
1009
|
r.fetchPart ? ` this part sharper from the archive (one request): ${r.fetchPart}` : undefined,
|
|
993
1010
|
]), missing.length ? `not registered: ${missing.join(", ")} — strom media list --recordset ${missing[0].split(":")[0]}` : undefined, ...failed.map((f) => `no view of ${f.ref}: ${f.error}${f.hint ? ` → ${f.hint}` : ""}`), sizes.has("reduced") ? `reduced: ${REDUCED_HINT}${made.some((m) => m.overview && viewSize(m.r.v) === "reduced") ? `; ${OVERVIEW_HINT}` : ""}` : undefined, sizes.has("enlarged") ? `enlarged: the scan has no more detail there. ${ENLARGED_HINT}; ${SHARPER_SCAN}` : undefined);
|
package/dist/commands/phone.js
CHANGED
|
@@ -13,7 +13,9 @@ function stateLines(lang, s) {
|
|
|
13
13
|
? ui(lang, "ui.phone.on", { urls: s.addresses.map((a) => a.url).join(", ") })
|
|
14
14
|
: ui(lang, "ui.phone.starting")
|
|
15
15
|
: s.state === "error"
|
|
16
|
-
?
|
|
16
|
+
? s.error?.waiting
|
|
17
|
+
? ui(lang, "ui.phone.waiting")
|
|
18
|
+
: ui(lang, "ui.phone.error", { why: ui(lang, `ui.phone.why.${s.error?.code ?? "bind-failed"}`) })
|
|
17
19
|
: s.state === "confirm"
|
|
18
20
|
? ui(lang, "ui.phone.confirm")
|
|
19
21
|
: s.state === "declined"
|
package/dist/commands/read.js
CHANGED
|
@@ -11,14 +11,14 @@ import { DEFAULT_READ_PARALLEL, READ_PARALLEL_MAX } from "../core/config.js";
|
|
|
11
11
|
import { normId, requireRecord } from "../core/records.js";
|
|
12
12
|
import { bookRoute } from "../core/samebook.js";
|
|
13
13
|
import { pageOf } from "../core/calibration.js";
|
|
14
|
-
import { coveringPart, findImage, IMAGE_RANGE_MAX, imageOfRef, imageRange, onlyParts, regionText, wholeSizeOf } from "../core/media.js";
|
|
15
|
-
import { cropOf, halves, makeView, parseCrop, readInHalves, tiles } from "../core/views.js";
|
|
14
|
+
import { coveringPart, findImage, IMAGE_RANGE_MAX, imageOfRef, imageRange, onlyParts, regionText, sharperCover, wholeSizeOf } from "../core/media.js";
|
|
15
|
+
import { cropOf, halves, makeView, mediaOrient, parseCrop, readInHalves, tiles } from "../core/views.js";
|
|
16
16
|
import { IMAGE_MAX, IMAGE_MAX_LARGE } from "../agents/images.js";
|
|
17
17
|
import { viewSizesFor } from "../core/viewsizes.js";
|
|
18
18
|
import { currentSession } from "../core/session.js";
|
|
19
19
|
import { BATCH, BATCH_MAX, batches, NEXT_BLOCK, parseReport, READER_MINUTES_HELP, readerPrompt, readerSettings, viewCount, withoutNextBlock } from "../core/reader.js";
|
|
20
20
|
import { exifOrientation, shownSize } from "../image/index.js";
|
|
21
|
-
import { turnsAcross } from "../image/image.js";
|
|
21
|
+
import { fileToShown, shownToFile, turnsAcross } from "../image/image.js";
|
|
22
22
|
import { RUNNERS } from "../runners/index.js";
|
|
23
23
|
import { which, withoutAgentMarks } from "../core/which.js";
|
|
24
24
|
import { permissionPath } from "../agents/files.js";
|
|
@@ -50,6 +50,32 @@ function partCovering(parts, crop) {
|
|
|
50
50
|
});
|
|
51
51
|
return { part: found.part, crop: regionText(found.crop) };
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The copy a reader reads a crop of a whole image from (as strom media view shows it): the part fetched sharper that
|
|
55
|
+
* holds the place, or nearly all of it (cut to it, the rest said); a sharper part that shows only some of it said. The
|
|
56
|
+
* crop is of the picture as it is shown (upright by its EXIF); in the part, kept in `partCrops`.
|
|
57
|
+
*/
|
|
58
|
+
function sharperForReader(ctx, all, m, crop, partCrops) {
|
|
59
|
+
const shared = ctx.settings.shared()?.value;
|
|
60
|
+
if (m.part || !m.width || !m.height || m.recordset === undefined || m.image === undefined)
|
|
61
|
+
return m;
|
|
62
|
+
const o = mediaOrient(shared, m);
|
|
63
|
+
const [W, H] = turnsAcross(o) ? [m.height, m.width] : [m.width, m.height];
|
|
64
|
+
const px = parseCrop(crop, W, H);
|
|
65
|
+
const cover = sharperCover(all, m, shownToFile({ x: px.x / W, y: px.y / H, w: px.w / W, h: px.h / H }, o));
|
|
66
|
+
const say = (l) => (ctx.json ? ctx.io.stderr : ctx.io.stdout)(l + "\n");
|
|
67
|
+
const ref = `${m.recordset}:${m.image}`;
|
|
68
|
+
if (cover.near)
|
|
69
|
+
say(`${ref}: ${cover.near.part.id}, a part of the image fetched sharper (${cover.near.gain.toFixed(1)}× the detail), shows ${regionText(fileToShown(cover.near.shows, o))} of the crop, not all of it — read from the whole image; that place sharper: strom read ${ref} --crop ${regionText(fileToShown(cover.near.shows, o))} …`);
|
|
70
|
+
const s = cover.use;
|
|
71
|
+
if (!s)
|
|
72
|
+
return m;
|
|
73
|
+
partCrops.set(s.part.id, regionText(fileToShown(s.crop, mediaOrient(shared, s.part))));
|
|
74
|
+
const rest = (s.out ?? []).map((x) => `strom media view ${ref} --crop ${regionText(fileToShown(x, o))}`);
|
|
75
|
+
say(`${ref}: read from ${s.part.id}, ${s.part.part ? "a part of the image fetched sharper" : "a sharper copy of the image"} (${s.gain.toFixed(1)}× the detail)` +
|
|
76
|
+
(s.cut ? `, cut to it: ${regionText(fileToShown(s.cut, o))} of the crop${rest.length ? ` — the rest (the whole image, less sharp): ${rest.join(" · ")}` : ""}` : ""));
|
|
77
|
+
return s.part;
|
|
78
|
+
}
|
|
53
79
|
/**
|
|
54
80
|
* --images of strom read: a range ("40-69", "40–69", "40..69") or a list of numbers and ranges as strom says them back
|
|
55
81
|
* ("5-19,21-39,41-43", "71, 77–88, 91–94") — the same parser as strom media view and strom readings (imageRange), at
|
|
@@ -238,6 +264,10 @@ register({
|
|
|
238
264
|
});
|
|
239
265
|
label = images.map((m) => m.id).join("-").slice(0, 40);
|
|
240
266
|
}
|
|
267
|
+
// a crop of a whole image a part fetched sharper holds (or nearly all of, cut to it): read from that part; one that
|
|
268
|
+
// shows only some of it said (as strom media view does)
|
|
269
|
+
if (typeof opts.crop === "string")
|
|
270
|
+
images = images.map((m) => sharperForReader(ctx, all, m, opts.crop, partCrops));
|
|
241
271
|
// inside a sandbox that lets no agent start (Codex's): said at once, nothing prepared
|
|
242
272
|
refuseReadersInSandbox(ctx, "strom read");
|
|
243
273
|
const agentId = readerAgent(ctx, tree);
|
package/dist/core/document.js
CHANGED
|
@@ -1219,19 +1219,27 @@ export function buildResult(tree, s) {
|
|
|
1219
1219
|
return out;
|
|
1220
1220
|
};
|
|
1221
1221
|
let entries = entriesOf(firsts, false);
|
|
1222
|
+
const firstRead = entries.length > 0;
|
|
1222
1223
|
const blindRan = blinds.some((r) => r);
|
|
1223
|
-
// the first reader failed, the blind one read: its entries, untranslated
|
|
1224
|
+
// the first reader failed, the blind one read: its entries, untranslated — never held against themselves
|
|
1224
1225
|
if (!entries.length && blindRan)
|
|
1225
1226
|
entries = entriesOf(blinds, false);
|
|
1226
1227
|
let agree = null;
|
|
1227
|
-
if (blindRan && s.readers > 1 &&
|
|
1228
|
+
if (blindRan && s.readers > 1 && firstRead) {
|
|
1228
1229
|
const blindEntries = entriesOf(blinds, true);
|
|
1229
1230
|
const c = compareReadings(entries, blindEntries);
|
|
1230
1231
|
entries = c.entries;
|
|
1231
1232
|
agree = c.agree;
|
|
1232
1233
|
}
|
|
1233
1234
|
entries = withTree(tree, entries);
|
|
1234
|
-
|
|
1235
|
+
// a reader ran when it ended well and its report says something (an answer in prose only is no reading)
|
|
1236
|
+
const said = (r) => {
|
|
1237
|
+
if (r?.outcome !== "ok")
|
|
1238
|
+
return false;
|
|
1239
|
+
const rep = read(r);
|
|
1240
|
+
return !!rep && (rep.entries.length > 0 || rep.done || rep.doc.legible !== undefined);
|
|
1241
|
+
};
|
|
1242
|
+
const ran = (firsts.some(said) ? 1 : 0) + (blinds.some(said) ? 1 : 0);
|
|
1235
1243
|
const usd = s.reports.reduce((n, r) => n + (r.usd ?? 0), 0);
|
|
1236
1244
|
const partial = s.reports.some((r) => r.outcome !== undefined && (r.usd === undefined || r.partial));
|
|
1237
1245
|
return {
|
package/dist/core/media.js
CHANGED
|
@@ -329,6 +329,72 @@ export function sharperPart(all, whole, r) {
|
|
|
329
329
|
}
|
|
330
330
|
return best;
|
|
331
331
|
}
|
|
332
|
+
/** Of a part that shows a place in part: the share of the place it shows, of the part in the place, and where they meet. */
|
|
333
|
+
function overlap(r, p) {
|
|
334
|
+
const x0 = Math.max(r.x, p.x);
|
|
335
|
+
const y0 = Math.max(r.y, p.y);
|
|
336
|
+
const x1 = Math.min(r.x + r.w, p.x + p.w);
|
|
337
|
+
const y1 = Math.min(r.y + r.h, p.y + p.h);
|
|
338
|
+
if (x1 <= x0 || y1 <= y0 || !(r.w > 0 && r.h > 0 && p.w > 0 && p.h > 0))
|
|
339
|
+
return undefined;
|
|
340
|
+
const a = (x1 - x0) * (y1 - y0);
|
|
341
|
+
// to a thousandth, as coveringPart counts it
|
|
342
|
+
return { of: Math.min(1, Math.round((a / (r.w * r.h)) * 1000) / 1000), ofPart: Math.min(1, Math.round((a / (p.w * p.h)) * 1000) / 1000), meet: { x: x0, y: y0, w: x1 - x0, h: y1 - y0 } };
|
|
343
|
+
}
|
|
344
|
+
/** The strips of a region outside another that lies in it (above, below, left, right), thinner than a pixel left out. */
|
|
345
|
+
function stripsOutside(r, m) {
|
|
346
|
+
const out = [];
|
|
347
|
+
const thin = 0.0005;
|
|
348
|
+
if (m.y - r.y > thin)
|
|
349
|
+
out.push({ x: r.x, y: r.y, w: r.w, h: m.y - r.y });
|
|
350
|
+
if (r.y + r.h - (m.y + m.h) > thin)
|
|
351
|
+
out.push({ x: r.x, y: m.y + m.h, w: r.w, h: r.y + r.h - (m.y + m.h) });
|
|
352
|
+
if (m.x - r.x > thin)
|
|
353
|
+
out.push({ x: r.x, y: m.y, w: m.x - r.x, h: m.h });
|
|
354
|
+
if (r.x + r.w - (m.x + m.w) > thin)
|
|
355
|
+
out.push({ x: m.x + m.w, y: m.y, w: r.x + r.w - (m.x + m.w), h: m.h });
|
|
356
|
+
return out;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* A sharper part for a view of a place of a whole image (fractions of it, as the file lies), as an agent reads by it:
|
|
360
|
+
* the sharpest that holds all of the place (sharperPart); else, of the sharper parts that show nearly all of it
|
|
361
|
+
* (COVERS, as coveringPart takes a part of an image registered only in parts — a crop drawn a little past the entry
|
|
362
|
+
* fetched sharper), the one that shows the most, then the sharpest, the place cut to it (`use.cut`, `use.out`). A
|
|
363
|
+
* sharper part that shows less of the place, but much of it or lies mostly in it, is not used: `near` says it — the
|
|
364
|
+
* part, its detail and what of the place it shows. Nothing sharper: {}.
|
|
365
|
+
*/
|
|
366
|
+
export function sharperCover(all, whole, r) {
|
|
367
|
+
const full = sharperPart(all, whole, r);
|
|
368
|
+
if (full)
|
|
369
|
+
return { use: full };
|
|
370
|
+
if (!whole.width || whole.part || whole.recordset === undefined || whole.image === undefined)
|
|
371
|
+
return {};
|
|
372
|
+
let best;
|
|
373
|
+
let near;
|
|
374
|
+
for (const m of all) {
|
|
375
|
+
const p = m.part;
|
|
376
|
+
if (!p || !m.width || m.retracted || m.recordset !== whole.recordset || m.image !== whole.image)
|
|
377
|
+
continue;
|
|
378
|
+
const o = overlap(r, p);
|
|
379
|
+
if (!o)
|
|
380
|
+
continue;
|
|
381
|
+
const gain = m.width / p.w / whole.width;
|
|
382
|
+
if (gain < SHARPER)
|
|
383
|
+
continue;
|
|
384
|
+
if (o.of >= COVERS) {
|
|
385
|
+
if (!best || o.of > best.of || (o.of === best.of && gain > best.gain))
|
|
386
|
+
best = { part: m, gain, of: o.of, meet: o.meet };
|
|
387
|
+
}
|
|
388
|
+
else if ((o.of >= 0.25 || o.ofPart >= 0.5) && (!near || o.of > near.of || (o.of === near.of && gain > near.gain)))
|
|
389
|
+
near = { part: m, gain, of: o.of, meet: o.meet };
|
|
390
|
+
}
|
|
391
|
+
if (best) {
|
|
392
|
+
const p = best.part.part;
|
|
393
|
+
const crop = { x: (best.meet.x - p.x) / p.w, y: (best.meet.y - p.y) / p.h, w: best.meet.w / p.w, h: best.meet.h / p.h };
|
|
394
|
+
return { use: { part: best.part, gain: best.gain, crop, cut: best.meet, out: stripsOutside(r, best.meet) } };
|
|
395
|
+
}
|
|
396
|
+
return near ? { near: { part: near.part, gain: near.gain, shows: near.meet } } : {};
|
|
397
|
+
}
|
|
332
398
|
/**
|
|
333
399
|
* A part of image n a connector would fetch that strom holds already, so that no request is made: the same part
|
|
334
400
|
* registered; or a part it lies in, or the whole image, with at least as much detail as a new part could bring — a new
|
package/dist/core/phone.js
CHANGED
|
@@ -511,7 +511,7 @@ export function phoneState(env) {
|
|
|
511
511
|
// just turned on: its server on the way
|
|
512
512
|
if (c.since && Date.now() - Date.parse(c.since) < 15_000)
|
|
513
513
|
return { ...base, state: "on", since: c.since, phones };
|
|
514
|
-
return { ...base, state: "error", since: c.since ?? null, error: { code: "bind-failed", detail: run?.ended ? `the server ended: ${run.ended.reason}` : "the server does not run" }, phones };
|
|
514
|
+
return { ...base, state: "error", since: c.since ?? null, error: { code: "bind-failed", detail: run?.ended ? `the server ended: ${run.ended.reason}` : "the server does not run", waiting: true }, phones };
|
|
515
515
|
}
|
|
516
516
|
const a = readAsk(env);
|
|
517
517
|
if (asking(a))
|
package/dist/core/reader.js
CHANGED
|
@@ -226,11 +226,15 @@ export function parseReport(text) {
|
|
|
226
226
|
}
|
|
227
227
|
return out;
|
|
228
228
|
}
|
|
229
|
-
/**
|
|
229
|
+
/**
|
|
230
|
+
* Claude Code settings for a reader: it may read these views and read and write its report — nothing else. Its report
|
|
231
|
+
* read too: Claude Code edits no file it has not read (found live 2026-10-21: every edit refused, the report kept only
|
|
232
|
+
* from the answer, a reader that answered in prose lost).
|
|
233
|
+
*/
|
|
230
234
|
export function readerSettings(views, report, toPermission) {
|
|
231
235
|
return {
|
|
232
236
|
permissions: {
|
|
233
|
-
allow: [...views.map((v) => `Read(${toPermission(v)})`), `Write(${toPermission(report)})`, `Edit(${toPermission(report)})`],
|
|
237
|
+
allow: [...views.map((v) => `Read(${toPermission(v)})`), `Read(${toPermission(report)})`, `Write(${toPermission(report)})`, `Edit(${toPermission(report)})`],
|
|
234
238
|
deny: ["Bash", "WebFetch", "WebSearch"],
|
|
235
239
|
},
|
|
236
240
|
};
|
package/dist/core/tree.js
CHANGED
|
@@ -27,7 +27,7 @@ import { lastBackup } from "./backup.js";
|
|
|
27
27
|
export const TREE_FILE = "strom.json";
|
|
28
28
|
// between releases the candidate being made (a bridge run from the sources says what it is: the Strom app goes by it);
|
|
29
29
|
// at a release the release's own (package.json)
|
|
30
|
-
export const VERSION = "1.19.0-beta.
|
|
30
|
+
export const VERSION = "1.19.0-beta.3";
|
|
31
31
|
const GITIGNORE = `# Strom Research — local state and large files are not versioned
|
|
32
32
|
.strom/
|
|
33
33
|
/media/
|