@paulhectork/aiiinotate 0.12.0
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/LICENSE +661 -0
- package/README.md +206 -0
- package/cli/export.js +118 -0
- package/cli/import.js +111 -0
- package/cli/index.js +45 -0
- package/cli/migrate.js +131 -0
- package/cli/serve.js +29 -0
- package/cli/utils/fastifyClient.js +86 -0
- package/cli/utils/io.js +225 -0
- package/cli/utils/mongoClient.js +21 -0
- package/cli/utils/progressbar.js +86 -0
- package/cli/xywhToInt.js +99 -0
- package/config/.env.template +48 -0
- package/docker/Dockerfile +39 -0
- package/docker/README.md +5 -0
- package/docker/docker-compose.yaml +49 -0
- package/docker/docker.sh +43 -0
- package/docker/docker_aiiinotate_import.sh +62 -0
- package/docs/api.md +381 -0
- package/docs/cli.md +132 -0
- package/docs/dev_documentation/dev_architecture.md +88 -0
- package/docs/dev_documentation/dev_db.md +58 -0
- package/docs/dev_documentation/dev_iiif_compatibility.md +43 -0
- package/docs/dev_documentation/dev_notes_quirks_and_troubleshooting.md +143 -0
- package/docs/docker.md +98 -0
- package/docs/includes/report_benchmark_aiiinotate_2026-05-28-02:50:48_7steps.png +0 -0
- package/docs/scalability.md +34 -0
- package/docs/specifications/0_w3c_open_annotations.md +332 -0
- package/docs/specifications/1_w3c_web_annotations.md +577 -0
- package/docs/specifications/2_iiif_apis.md +428 -0
- package/docs/specifications/3_iiif_annotations.md +103 -0
- package/docs/specifications/4_search_api.md +135 -0
- package/docs/specifications/5_sas.md +119 -0
- package/docs/specifications/6_mirador.md +119 -0
- package/docs/specifications/7_aikon.md +137 -0
- package/docs/specifications/include/presentation_2.0.webp +0 -0
- package/docs/specifications/include/presentation_2.0_white.png +0 -0
- package/docs/specifications/include/presentation_3.0.png +0 -0
- package/docs/specifications/include/presentation_3.0_resize.png +0 -0
- package/eslint.config.js +30 -0
- package/migrations/baseConfig.js +57 -0
- package/migrations/manageIndex.js +55 -0
- package/migrations/migrate-mongo-config-main.js +8 -0
- package/migrations/migrate-mongo-config-test.js +8 -0
- package/migrations/migrationScripts/20250825185706-collections.js +48 -0
- package/migrations/migrationScripts/20250826194832-annotations2-schema.js +42 -0
- package/migrations/migrationScripts/20250904080710-annotations2-indexes.js +69 -0
- package/migrations/migrationScripts/20251002141951-manifests2-schema.js +43 -0
- package/migrations/migrationScripts/20251006212110-manifests2-indexes.js +35 -0
- package/migrations/migrationTemplate.js +25 -0
- package/package.json +82 -0
- package/scripts/get_version.py +12 -0
- package/scripts/run.sh +36 -0
- package/scripts/setup_mongodb.sh +70 -0
- package/scripts/setup_node.sh +15 -0
- package/scripts/update_version.py +30 -0
- package/scripts/utils.sh +65 -0
- package/src/app.js +116 -0
- package/src/constants.js +73 -0
- package/src/data/annotations/annotations2.js +681 -0
- package/src/data/annotations/annotations3.js +28 -0
- package/src/data/annotations/routes.js +335 -0
- package/src/data/annotations/routes.test.js +271 -0
- package/src/data/collectionAbstract.js +283 -0
- package/src/data/index.js +29 -0
- package/src/data/manifests/manifests2.js +378 -0
- package/src/data/manifests/manifests2.test.js +53 -0
- package/src/data/manifests/manifests3.js +23 -0
- package/src/data/manifests/routes.js +122 -0
- package/src/data/manifests/routes.test.js +70 -0
- package/src/data/routes.js +181 -0
- package/src/data/routes.test.js +166 -0
- package/src/db/index.js +50 -0
- package/src/fixtures/annotations.js +41 -0
- package/src/fixtures/data/annotationList_aikon_wit9_man11_anno165_all.jsonld +827 -0
- package/src/fixtures/data/annotationList_vhs_wit250_man250_anno250_all.jsonld +37514 -0
- package/src/fixtures/data/annotationList_vhs_wit253_man253_anno253_all.jsonld +20111 -0
- package/src/fixtures/data/annotations2Invalid.jsonld +39 -0
- package/src/fixtures/data/annotations2SvgValid.jsonld +81 -0
- package/src/fixtures/data/annotations2Valid.jsonld +39 -0
- package/src/fixtures/data/bnf_invalid_manifest.json +2806 -0
- package/src/fixtures/data/bnf_valid_manifest.json +2817 -0
- package/src/fixtures/data/vhs_wit253_man253_anno253_anno-24.json +1 -0
- package/src/fixtures/generate.js +181 -0
- package/src/fixtures/index.js +69 -0
- package/src/fixtures/manifests.js +14 -0
- package/src/fixtures/utils.js +37 -0
- package/src/schemas/index.js +100 -0
- package/src/schemas/schemasBase.js +19 -0
- package/src/schemas/schemasPresentation2.js +410 -0
- package/src/schemas/schemasPresentation3.js +33 -0
- package/src/schemas/schemasResolver.js +71 -0
- package/src/schemas/schemasRoutes.js +318 -0
- package/src/server.js +25 -0
- package/src/types.js +97 -0
- package/src/utils/iiif2Utils.js +332 -0
- package/src/utils/iiif2Utils.test.js +146 -0
- package/src/utils/iiif3Utils.js +0 -0
- package/src/utils/iiifUtils.js +18 -0
- package/src/utils/logger.js +119 -0
- package/src/utils/routeUtils.js +137 -0
- package/src/utils/svg.js +417 -0
- package/src/utils/testUtils.js +289 -0
- package/src/utils/utils.js +403 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import build from "#src/app.js";
|
|
2
|
+
|
|
3
|
+
/** @typedef {import("#types").FastifyInstanceType} FastifyInstanceType */
|
|
4
|
+
/** @typedef {import("#types").FastifyReplyType} FastifyReplyType */
|
|
5
|
+
/** @typedef {import("#types").InsertResponseType} InsertResponseType */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* a client to interact with the fastify app.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* import FastifyClient from "#cli/utils/fastifyClient.js";
|
|
12
|
+
* const fastify = new FastifyClient();
|
|
13
|
+
* // load the fastify instance asynchronously
|
|
14
|
+
* await fastify.build();
|
|
15
|
+
* // run operations
|
|
16
|
+
* await fastify.importAnnotationList(...);
|
|
17
|
+
*/
|
|
18
|
+
class FastifyClient {
|
|
19
|
+
constructor () { }
|
|
20
|
+
|
|
21
|
+
// NOTE: instanciating this.fastify is asynchronous and a constructor must be synchronous,
|
|
22
|
+
// so we use a builder to set this.fastify, following this pattern: https://stackoverflow.com/a/43433773
|
|
23
|
+
async build() {
|
|
24
|
+
/** @type {FastifyInstanceType} */
|
|
25
|
+
this.fastify = await build("default");
|
|
26
|
+
// TODO find way to actually completely disable logging because it makes the CLI UI uglyyy !
|
|
27
|
+
// log only fastify errors
|
|
28
|
+
this.fastify.log.level = "error";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async stop() {
|
|
32
|
+
await this.fastify.close();
|
|
33
|
+
this.fastify.log.info("fastify instance successfully closed.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} route
|
|
38
|
+
* @returns {Promise<FastifyReplyType>}
|
|
39
|
+
*/
|
|
40
|
+
injectGet(route) {
|
|
41
|
+
return this.fastify.inject({
|
|
42
|
+
method: "GET",
|
|
43
|
+
url: route,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} route
|
|
49
|
+
* @param {object} payload
|
|
50
|
+
* @returns {Promise<FastifyReplyType>}
|
|
51
|
+
*/
|
|
52
|
+
injectPost(route, payload) {
|
|
53
|
+
return this.fastify.inject({
|
|
54
|
+
method: "POST",
|
|
55
|
+
url: route,
|
|
56
|
+
payload: payload
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async importAnnotationPage(annotationPage) {
|
|
61
|
+
// TODO
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* function to interact with all annotation createMany + manifest create routes.
|
|
66
|
+
* @type {(iiifVersion: 2|3, datatype: "manifest"|"annotation") => (data: object) => [Number, Promise<object>] }
|
|
67
|
+
*/
|
|
68
|
+
importData(iiifVersion, datatype) {
|
|
69
|
+
if (![ "2","3" ].includes(`${iiifVersion}`)) {
|
|
70
|
+
throw new Error(`fastifyClient.importData: "iiifVersion" must by 2 or 3, got "${iiifVersion}"`);
|
|
71
|
+
}
|
|
72
|
+
if (![ "manifest","annotation" ].includes(datatype)) {
|
|
73
|
+
throw new Error(`fastifyClient.importData: "datatype" must by "manifest" or "annotation", got "${datatype}"`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const routeSuffix = datatype==="manifest" ? "create" : "createMany";
|
|
77
|
+
const route = `/${datatype}s/${iiifVersion}/${routeSuffix}`;
|
|
78
|
+
|
|
79
|
+
return async (data) => {
|
|
80
|
+
const r = await this.injectPost(route, data);
|
|
81
|
+
return [ r.statusCode, r.json() ];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default FastifyClient;
|
package/cli/utils/io.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
import ProgressBar from "#cli/utils/progressbar.js";
|
|
5
|
+
|
|
6
|
+
/** @typedef {import("fs").WriteStream} WriteStreamType */
|
|
7
|
+
|
|
8
|
+
const cwd = process.cwd(); // directory the script is run from
|
|
9
|
+
const getCwd = () => process.cwd();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* convert a filepath to absolute if it is relative.
|
|
13
|
+
* @param {string} f
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
const toAbsPath = (f) => path.isAbsolute(f) ? f : path.join(cwd, f);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @returns {[string, boolean]} - [<absolute file path>, <success?>]
|
|
20
|
+
*/
|
|
21
|
+
const fileOk = (f) => {
|
|
22
|
+
f = toAbsPath(f);
|
|
23
|
+
try {
|
|
24
|
+
fs.accessSync(f, fs.constants.R_OK); // will throw if there's a problem opening.
|
|
25
|
+
return [ f, fs.lstatSync(f).isFile() ];
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return [ f, false ]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @returns {[PathLike, boolean]} */
|
|
32
|
+
const dirOk = (d) => {
|
|
33
|
+
d = toAbsPath(d);
|
|
34
|
+
try {
|
|
35
|
+
fs.accessSync(d, fs.constants.R_OK); // will throw if there's an error
|
|
36
|
+
return [ d, fs.lstatSync(d).isDirectory() ]
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return [ d, false ]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string} f
|
|
44
|
+
* @return {string?}
|
|
45
|
+
*/
|
|
46
|
+
const fileRead = (f) => {
|
|
47
|
+
f = toAbsPath(f);
|
|
48
|
+
try {
|
|
49
|
+
return fs.readFileSync(f, { encoding: "utf8" })
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`io.fileRead: could not read file: ${f}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* take an input array of filepaths. convert the paths to absolute, and check that the files exist
|
|
57
|
+
* the cli exits if any of the files don't exist
|
|
58
|
+
* @param {string[]} fileArr
|
|
59
|
+
* @returns { string[] } array of absolute filepaths
|
|
60
|
+
*/
|
|
61
|
+
function fileArrayValidate (fileArr) {
|
|
62
|
+
// array of [<absolute filepath>, <success opening file?>]
|
|
63
|
+
fileArr = fileArr.map(fileOk);
|
|
64
|
+
const successCount = fileArr.filter(x => x[1]).length;
|
|
65
|
+
if (successCount!==fileArr.length) {
|
|
66
|
+
console.error(
|
|
67
|
+
`io.fileArrayValidate: ${fileArr.length - successCount} files could not be accessed. exiting...`,
|
|
68
|
+
fileArr.filter(x => !x[1]).map(x => x[0])
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
return fileArr.map(x => x[0]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `file` is a path to a file containing paths to other files (1 file per line).
|
|
77
|
+
* parse the file and return an array of absolute paths to files.
|
|
78
|
+
* @param {str} file
|
|
79
|
+
* @returns {Promise<string[]>}
|
|
80
|
+
*/
|
|
81
|
+
async function parseImportInputFile(file) {
|
|
82
|
+
// ensure input file exists
|
|
83
|
+
const [ fileAbs, ok ] = fileOk(file);
|
|
84
|
+
if (!ok) {
|
|
85
|
+
console.error(`could not read import file: ${file}. exiting...`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// read `file` split it by lines, remove empty lines
|
|
90
|
+
const fileArr =
|
|
91
|
+
fileRead(fileAbs)
|
|
92
|
+
.split("\n")
|
|
93
|
+
.filter(l => !l.match(/^\s*$/g));
|
|
94
|
+
return [ ...new Set(fileArrayValidate(fileArr)) ];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const fileWrite = (f, data) => {
|
|
98
|
+
try {
|
|
99
|
+
fs.writeFileSync(f, data, "utf-8");
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new Error(`Error writing to file: ${f} because of error: ${e}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* NOTE: this will NOT work on collections and huge objects. use streamWriteJson to write from Mongo Cursors.
|
|
107
|
+
* @param {string|import("fs").PathLike} f
|
|
108
|
+
* @param {Array|object} data
|
|
109
|
+
* @returns {void}
|
|
110
|
+
*/
|
|
111
|
+
const fileWriteJson = (f, data) => {
|
|
112
|
+
data = JSON.stringify(data)
|
|
113
|
+
fileWrite(f, data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* wrapper for writer.write(data) that respects writer backpressure
|
|
118
|
+
*
|
|
119
|
+
* we wrap the .write() in a Promise that checks on the rturn of `writer.write`
|
|
120
|
+
* and drains the stream if necessary: if `writer.write()` returns
|
|
121
|
+
* false, the write buffer is full and must be drained by the OS
|
|
122
|
+
* before continuing.
|
|
123
|
+
*
|
|
124
|
+
* this avoids:
|
|
125
|
+
* - writes that are silently dropped or reordered
|
|
126
|
+
* - corrupt/truncated JSON with no error thrown
|
|
127
|
+
*
|
|
128
|
+
* @type {(writer: WriteStreamType) => (data: string) => Promise<void> }
|
|
129
|
+
*/
|
|
130
|
+
const writeOrWait = (writer) =>
|
|
131
|
+
(data) =>
|
|
132
|
+
new Promise((resolve, reject) => {
|
|
133
|
+
const ok = writer.write(data, (error) => {
|
|
134
|
+
if (error) return reject(error);
|
|
135
|
+
});
|
|
136
|
+
if (ok) {
|
|
137
|
+
resolve(); // buffer has room, keep going
|
|
138
|
+
} else {
|
|
139
|
+
writer.once("drain", resolve); // buffer full, wait for drain
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* WriteStream.write is actually asyncrhonous => for the last line,
|
|
145
|
+
* we need to use `writer.end` to ensure everything has been written to file.
|
|
146
|
+
*
|
|
147
|
+
* @returns {Promise<void>}
|
|
148
|
+
*/
|
|
149
|
+
const endStream = (writer, data) =>
|
|
150
|
+
new Promise((resolve, reject) => {
|
|
151
|
+
writer.end(data, (error) => {
|
|
152
|
+
if (error) return reject(error);
|
|
153
|
+
resolve();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* given a file path `fp` and a FindCursor `cursor`,
|
|
159
|
+
* write all documents in `cursor` to a file.
|
|
160
|
+
*
|
|
161
|
+
* if `totalCount` is defined, print a progress bar as well.
|
|
162
|
+
* else, just print the document number and update at each iteration.
|
|
163
|
+
*
|
|
164
|
+
* necessary to use a string when `cursor` stores a lot of documents
|
|
165
|
+
* (instead of a simple file-write):
|
|
166
|
+
* - cursor.toArray() uses TONS of memory and is slow
|
|
167
|
+
* - JSON.stringify(), used to write JSONs to file, has a maximum
|
|
168
|
+
* size for arrays and will crash if stringifying huge arrays.
|
|
169
|
+
*
|
|
170
|
+
* to check that the output is a valid JSON, use:
|
|
171
|
+
* ```bash
|
|
172
|
+
* python3 -mjson.tool path/to/json > /dev/null && echo "ok" || echo "err"
|
|
173
|
+
* ````
|
|
174
|
+
*
|
|
175
|
+
* @param {string|PathLike} fp
|
|
176
|
+
* @param {MongoFindCursorType} cursor
|
|
177
|
+
* @param {number?} totalCount
|
|
178
|
+
* @returns {Promise}
|
|
179
|
+
*/
|
|
180
|
+
const writeCursorToJson = async (fp, cursor, totalCount) => {
|
|
181
|
+
const writer = fs.createWriteStream(fp);
|
|
182
|
+
const onceWriter = writeOrWait(writer);
|
|
183
|
+
|
|
184
|
+
let pb;
|
|
185
|
+
if (totalCount) {
|
|
186
|
+
pb = new ProgressBar({ desc: "writing documents to file", total: totalCount });
|
|
187
|
+
} else {
|
|
188
|
+
console.log("");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// we are writing an array => manually write the opening "[".
|
|
192
|
+
await onceWriter("[");
|
|
193
|
+
let i = 0
|
|
194
|
+
for await (const doc of cursor) {
|
|
195
|
+
i += 1;
|
|
196
|
+
// if previous items were written to file, write a "," separator.
|
|
197
|
+
if (i!=1) await onceWriter(", ");
|
|
198
|
+
if (pb) {
|
|
199
|
+
pb.update(i);
|
|
200
|
+
} else {
|
|
201
|
+
process.stdout.clearLine(0);
|
|
202
|
+
process.stdout.cursorTo(0);
|
|
203
|
+
process.stdout.write(`writing document #${i} to file`);
|
|
204
|
+
}
|
|
205
|
+
await onceWriter(JSON.stringify(doc));
|
|
206
|
+
}
|
|
207
|
+
// close the array with a "]" and end the stream.
|
|
208
|
+
await endStream(writer, "]");
|
|
209
|
+
|
|
210
|
+
if (!pb && i>0) console.log("");
|
|
211
|
+
return totalCount;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
export {
|
|
216
|
+
fileRead,
|
|
217
|
+
fileOk,
|
|
218
|
+
dirOk,
|
|
219
|
+
fileWrite,
|
|
220
|
+
fileWriteJson,
|
|
221
|
+
getCwd,
|
|
222
|
+
fileArrayValidate,
|
|
223
|
+
parseImportInputFile,
|
|
224
|
+
writeCursorToJson
|
|
225
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MongoClient } from "mongodb";
|
|
2
|
+
|
|
3
|
+
import { MONGODB_CONNSTRING, MONGODB_DB } from "#constants";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* load a mongo client and connect it to the database. exists if there's an error
|
|
7
|
+
* @returns {{ client: import("mongodb").MongoClient, db: import("mongodb").Db }}
|
|
8
|
+
*/
|
|
9
|
+
function loadMongoClient() {
|
|
10
|
+
try {
|
|
11
|
+
const
|
|
12
|
+
client = new MongoClient(MONGODB_CONNSTRING),
|
|
13
|
+
db = client.db(MONGODB_DB);
|
|
14
|
+
return { client, db };
|
|
15
|
+
} catch (err) {
|
|
16
|
+
console.error(`mongoClient: could not connect to DB because of error ${err.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default loadMongoClient;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a progress bar
|
|
3
|
+
* @example
|
|
4
|
+
* const total = 100;
|
|
5
|
+
* const ms = 10;
|
|
6
|
+
* const pb = new ProgressBar({ desc: "Progressbar example", total: total });
|
|
7
|
+
* const sleep = (pb, i) => {
|
|
8
|
+
* pb.update(i);
|
|
9
|
+
* return new Promise((res, rej) => setTimeout(res, ms));
|
|
10
|
+
* };
|
|
11
|
+
* for (let i=0; i<=total; i++) {
|
|
12
|
+
* await sleep(pb, i);
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
class ProgressBar {
|
|
16
|
+
constructor(options = {
|
|
17
|
+
desc: "Progress",
|
|
18
|
+
total: 100
|
|
19
|
+
}) {
|
|
20
|
+
this.desc = options.desc
|
|
21
|
+
this.total = options.total
|
|
22
|
+
this.current = 0;
|
|
23
|
+
this.startTime = Date.now();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
update(current) {
|
|
27
|
+
this.current = Math.min(current, this.total);
|
|
28
|
+
this.render();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
render() {
|
|
32
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
33
|
+
|
|
34
|
+
const percent = this.total === 0 ? 100 : (this.current / this.total) * 100;
|
|
35
|
+
const percentStr = percent.toFixed(1) + "%";
|
|
36
|
+
|
|
37
|
+
// calculate elapsed and estimated time
|
|
38
|
+
const elapsedTime = Date.now() - this.startTime;
|
|
39
|
+
const elapsedSec = Math.floor(elapsedTime / 1000);
|
|
40
|
+
const rate = this.current / (elapsedTime / 1000);
|
|
41
|
+
const remainingTime = rate > 0 ? Math.floor((this.total - this.current) / rate) : 0;
|
|
42
|
+
|
|
43
|
+
const timeStr = `${this.formatTime(elapsedSec)}/${this.formatTime(remainingTime)}`;
|
|
44
|
+
const countStr = `${this.current}/${this.total}`;
|
|
45
|
+
|
|
46
|
+
// calculate available space for bar
|
|
47
|
+
const prefix = `${this.desc} [`;
|
|
48
|
+
const suffix = `] ${percentStr} ${countStr} ${timeStr}`;
|
|
49
|
+
const availableWidth = terminalWidth - prefix.length - suffix.length;
|
|
50
|
+
|
|
51
|
+
if (availableWidth < 10) {
|
|
52
|
+
process.stdout.write(`\r${this.desc}: ${percentStr}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// build progress bar
|
|
57
|
+
const filledWidth = Math.floor((this.current / this.total) * availableWidth);
|
|
58
|
+
const emptyWidth = availableWidth - filledWidth;
|
|
59
|
+
const bar = "█".repeat(filledWidth) + "░".repeat(emptyWidth);
|
|
60
|
+
|
|
61
|
+
// build and display complete line
|
|
62
|
+
const line = prefix + bar + suffix;
|
|
63
|
+
process.stdout.write("\r" + line);
|
|
64
|
+
|
|
65
|
+
// new line when complete
|
|
66
|
+
if (this.current === this.total) {
|
|
67
|
+
process.stdout.write("\n");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
formatTime(seconds) {
|
|
72
|
+
const h = Math.floor(seconds / 3600);
|
|
73
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
74
|
+
const s = seconds % 60;
|
|
75
|
+
|
|
76
|
+
if (h > 0) {
|
|
77
|
+
return `${h}h${m}m`;
|
|
78
|
+
} else if (m > 0) {
|
|
79
|
+
return `${m}m${s}s`;
|
|
80
|
+
} else {
|
|
81
|
+
return `${s}s`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default ProgressBar
|
package/cli/xywhToInt.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Command, Option, Argument } from "commander";
|
|
2
|
+
|
|
3
|
+
import loadMongoClient from "#cli/utils/mongoClient.js";
|
|
4
|
+
import ProgressBar from "#cli/utils/progressbar.js";
|
|
5
|
+
import { maybeToArray, xywhToInt } from "#src/utils/utils.js";
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("mongodb").Db} Db */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* bash command to get faulty annotations from a docker aiiinotate instance:
|
|
11
|
+
* ```
|
|
12
|
+
* docker exec docker-mongo-1 mongosh vhs_aiiinotate --eval \
|
|
13
|
+
* "JSON.stringify(db.annotations2.find({ 'on.xywh': { \$type: 'double' } }, { '_id': 0 }).toArray(), null, 2);" \
|
|
14
|
+
* > mogno_export.json
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* bash command to import annotations exported above in a local mongo instance:
|
|
18
|
+
* ```
|
|
19
|
+
* mongoimport \
|
|
20
|
+
* --host localhost \
|
|
21
|
+
* --db aiiinotate \
|
|
22
|
+
* --collection annotations2 \
|
|
23
|
+
* --file ./mogno_export.json \
|
|
24
|
+
* --jsonArray
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
*
|
|
30
|
+
* @param {Db} db - the mongo database
|
|
31
|
+
* @param {object} annotation - the annotation to update
|
|
32
|
+
* @param {boolean} dryRun - if `false`, don't update the annotation in database
|
|
33
|
+
*/
|
|
34
|
+
async function updateAnnotation(db, annotation, dryRun) {
|
|
35
|
+
let [ annotationTargetArray, converted ] = maybeToArray(annotation.on, true);
|
|
36
|
+
|
|
37
|
+
annotationTargetArray = annotationTargetArray.map((target) => {
|
|
38
|
+
target.xywh = xywhToInt(target.xywh);
|
|
39
|
+
console.log(target.xywh);
|
|
40
|
+
return target;
|
|
41
|
+
});
|
|
42
|
+
annotation.on = converted
|
|
43
|
+
? annotationTargetArray[0]
|
|
44
|
+
: annotationTargetArray;
|
|
45
|
+
|
|
46
|
+
// console.log("post: ", annotation.on);
|
|
47
|
+
// we need to do 1 update / document, since the @id filter can select 1 document at a time.
|
|
48
|
+
if (!dryRun) {
|
|
49
|
+
await db.collection("annotations2").updateOne(
|
|
50
|
+
{ "@id": annotation["@id"] },
|
|
51
|
+
{ $set: annotation },
|
|
52
|
+
{ upsert: false }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// NOTE: annotations have been exported from vhs and imported in local aiiinotate instance.
|
|
59
|
+
async function action(options) {
|
|
60
|
+
const dryRun = options.dryRun || false;
|
|
61
|
+
|
|
62
|
+
const summary = { total: 0, ok: 0, error: 0 };
|
|
63
|
+
|
|
64
|
+
const { client, db } = loadMongoClient();
|
|
65
|
+
const filter = { "on.xywh": { $type: "double" } };
|
|
66
|
+
const toUpdateCount = await db.collection("annotations2").countDocuments(filter);
|
|
67
|
+
const annotationsCursor = db.collection("annotations2").find(filter);
|
|
68
|
+
summary.total = toUpdateCount;
|
|
69
|
+
|
|
70
|
+
const pb = new ProgressBar({ desc: "updating annotations", total: toUpdateCount });
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (await annotationsCursor.hasNext()) {
|
|
73
|
+
i += 1;
|
|
74
|
+
pb.update(i);
|
|
75
|
+
const annotation = await annotationsCursor.next();
|
|
76
|
+
try {
|
|
77
|
+
await updateAnnotation(db, annotation, dryRun);
|
|
78
|
+
summary.ok += 1;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
console.error(e);
|
|
81
|
+
summary.error += 1
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`converting annotation.on.xywh to int (dry run=${dryRun}). results:`, summary);
|
|
86
|
+
await client.close()
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeXywhToIntCommand() {
|
|
91
|
+
const dryRunOpt = new Option("-d, --dry-run", "dry run (don't update data)");
|
|
92
|
+
|
|
93
|
+
return new Command("xywh-to-int")
|
|
94
|
+
.description("convert all `db.annotations2.on.xywh` values from float to int")
|
|
95
|
+
.addOption(dryRunOpt)
|
|
96
|
+
.action(async (options, command) => { return await action(options) });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default makeXywhToIntCommand;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/bin/env bash
|
|
2
|
+
|
|
3
|
+
# MongoDB host
|
|
4
|
+
MONGODB_HOST=127.0.0.1
|
|
5
|
+
# MongoDB port
|
|
6
|
+
MONGODB_PORT=27017
|
|
7
|
+
# MongoDB database name
|
|
8
|
+
MONGODB_DB=aiiinotate
|
|
9
|
+
|
|
10
|
+
# HTTP port for the app
|
|
11
|
+
AIIINOTATE_PORT=4000
|
|
12
|
+
# URL for the app
|
|
13
|
+
AIIINOTATE_HOST=127.0.0.1
|
|
14
|
+
# HTTP scheme: HTTP or HTTPS. should be HTTP in dev and in docker
|
|
15
|
+
AIIINOTATE_SCHEME=http
|
|
16
|
+
|
|
17
|
+
# where to output logs. either: "stdout"|"file"|"stdout+file"|"off"
|
|
18
|
+
AIIINOTATE_LOG_TARGET=stdout
|
|
19
|
+
# directory to save logs to (if AIIINOTATE_LOG_TARGET enables logging to file)
|
|
20
|
+
AIIINOTATE_LOG_DIR=
|
|
21
|
+
# log level. "trace"|"debug"|"info"|"warn"|"error"|"fatal"
|
|
22
|
+
AIIINOTATE_LOG_LEVEL=debug
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# max number of items to display per result page
|
|
26
|
+
AIIINOTATE_PAGE_SIZE=5000
|
|
27
|
+
# "true"|"false". strict error throwing when inserting annotations. equivalent
|
|
28
|
+
# to `throwOnCanvasIndexError=true` (raise an error if an annotation's target
|
|
29
|
+
# manifest can't be found) and `throwOnXywhError=true` (raise an error if you
|
|
30
|
+
# can't extract a bounding box for an annotation's target). use in controlled
|
|
31
|
+
# environments where you know precisely the structure of your annotations and
|
|
32
|
+
# where you know that your manifests are accessible through HTTP.
|
|
33
|
+
AIIINOTATE_STRICT_MODE=false
|
|
34
|
+
|
|
35
|
+
# IGNORE
|
|
36
|
+
AIIINOTATE_BASE_URL="$AIIINOTATE_SCHEME://$AIIINOTATE_HOST:$AIIINOTATE_PORT"
|
|
37
|
+
# IGNORE
|
|
38
|
+
MONGODB_CONNSTRING="mongodb://$MONGODB_HOST:$MONGODB_PORT/$MONGODB_DB"
|
|
39
|
+
# IGNORE
|
|
40
|
+
MONGODB_DB_TEST="${MONGODB_DB}_test"
|
|
41
|
+
# IGNORE
|
|
42
|
+
MONGODB_CONNSTRING_TEST="mongodb://$MONGODB_HOST:$MONGODB_PORT/$MONGODB_DB_TEST"
|
|
43
|
+
|
|
44
|
+
# public URL root following `scheme://host`, used to build to root of the IDs of
|
|
45
|
+
# aiiinotate data. In production, set this to your domain name. in dev, set to http://localhost, or anything you want.
|
|
46
|
+
# it should be accessible through HTTP in production, but not necessarily:
|
|
47
|
+
# the app doesn't access this URL in HTTP queries when running.
|
|
48
|
+
AIIINOTATE_PUBLIC_URL="$AIIINOTATE_BASE_URL"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
FROM node:23.11
|
|
3
|
+
|
|
4
|
+
# aiiinotate port
|
|
5
|
+
ARG PORT
|
|
6
|
+
ENV PORT=${PORT}
|
|
7
|
+
|
|
8
|
+
# path to the .env file
|
|
9
|
+
ARG ENV_PATH
|
|
10
|
+
ENV ENV_PATH="$ENV_PATH"
|
|
11
|
+
|
|
12
|
+
# set up environment
|
|
13
|
+
ENV DIR=/aiiinotate
|
|
14
|
+
ENV TERM=linux
|
|
15
|
+
ENV NPM_BIN=/aiiinotate/node_modules/.bin
|
|
16
|
+
SHELL ["/bin/bash", "-c"]
|
|
17
|
+
|
|
18
|
+
# root of the app in the docker container
|
|
19
|
+
WORKDIR ${DIR}
|
|
20
|
+
# copy the docker .env in the docker container
|
|
21
|
+
COPY "$ENV_PATH" ${DIR}/.env
|
|
22
|
+
|
|
23
|
+
# install iproute and curl for debug
|
|
24
|
+
RUN apt-get update
|
|
25
|
+
RUN apt install curl iproute2 -y
|
|
26
|
+
|
|
27
|
+
# create a npm package in $DIR to run aiiinotate from
|
|
28
|
+
RUN npm init -y
|
|
29
|
+
# install the app as an NPM library. we do a global install as it saves us from issues with `$PATH`.
|
|
30
|
+
RUN npm i aiiinotate
|
|
31
|
+
|
|
32
|
+
# expose the used port
|
|
33
|
+
EXPOSE ${PORT}
|
|
34
|
+
|
|
35
|
+
# run the migrations and start the app.
|
|
36
|
+
# ./node_modules/.bin/aiiinotate is to be able to use the `aiiinotate` cli without doing a global install of `aiiinotate`
|
|
37
|
+
# NOTE migrations must be done in CMD: they need the mongo service to be running.
|
|
38
|
+
CMD ${NPM_BIN}/dotenvx run -f ${DIR}/.env -- ${NPM_BIN}/aiiinotate migrate apply && \
|
|
39
|
+
${NPM_BIN}/dotenvx run -f ${DIR}/.env -- ${NPM_BIN}/aiiinotate serve prod;
|
package/docker/README.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Docker setup
|
|
2
|
+
|
|
3
|
+
This folder contains a `docker-compose` for an aiiinotate+MongoDB integration, with volume persistence and a script to import data.
|
|
4
|
+
|
|
5
|
+
View the documentation for aiiinotate Docker setup [here](https://github.com/Aikon-platform/aiiinotate/blob/main/docs/docker.md)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
x-proxy-settings: &proxy-settings
|
|
2
|
+
HTTP_PROXY: ${HTTP_PROXY:-}
|
|
3
|
+
HTTPS_PROXY: ${HTTPS_PROXY:-}
|
|
4
|
+
NO_PROXY: "localhost,127.0.0.1,mongo,web,.aiiinotate_network"
|
|
5
|
+
|
|
6
|
+
services:
|
|
7
|
+
mongo:
|
|
8
|
+
image: mongo:8
|
|
9
|
+
restart: always
|
|
10
|
+
networks:
|
|
11
|
+
- aiiinotate_network
|
|
12
|
+
volumes:
|
|
13
|
+
- mongodata:/data/db
|
|
14
|
+
environment:
|
|
15
|
+
<<: *proxy-settings
|
|
16
|
+
healthcheck:
|
|
17
|
+
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
|
18
|
+
interval: 10s
|
|
19
|
+
timeout: 5s
|
|
20
|
+
retries: 5
|
|
21
|
+
|
|
22
|
+
aiiinotate:
|
|
23
|
+
build:
|
|
24
|
+
context: ..
|
|
25
|
+
dockerfile: docker/Dockerfile
|
|
26
|
+
args:
|
|
27
|
+
PORT: ${AIIINOTATE_PORT}
|
|
28
|
+
ENV_PATH: ../docker/.env
|
|
29
|
+
# NOTE: be careful to point to docker/.env, not to config/.env, otherwise you will have very, very annoying errors.
|
|
30
|
+
env_file:
|
|
31
|
+
- ../docker/.env
|
|
32
|
+
ports:
|
|
33
|
+
- "${AIIINOTATE_PORT}:${AIIINOTATE_PORT}"
|
|
34
|
+
depends_on:
|
|
35
|
+
mongo:
|
|
36
|
+
condition: service_healthy
|
|
37
|
+
networks:
|
|
38
|
+
- aiiinotate_network
|
|
39
|
+
environment:
|
|
40
|
+
<<: *proxy-settings
|
|
41
|
+
restart: always
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
volumes:
|
|
45
|
+
mongodata:
|
|
46
|
+
|
|
47
|
+
networks:
|
|
48
|
+
aiiinotate_network:
|
|
49
|
+
driver: bridge
|