@aspan-corporation/ac-shared 1.2.38 → 1.2.40
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/lib/utils/diary.d.ts +5 -0
- package/lib/utils/diary.js +84 -8
- package/lib/utils/hintFallback.d.ts +14 -0
- package/lib/utils/hintFallback.js +51 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/processMeta.d.ts +11 -1
- package/lib/utils/processMeta.js +40 -14
- package/package.json +1 -1
package/lib/utils/diary.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export declare const TAG_DIARY_PREVIEW = "ac:diary:preview";
|
|
|
17
17
|
export declare const TAG_DIARY_PHOTO = "ac:diary:photo";
|
|
18
18
|
/** One per embedded audio recording: value is the referenced diary key. */
|
|
19
19
|
export declare const TAG_DIARY_AUDIO = "ac:diary:audio";
|
|
20
|
+
/** One per embedded video: value is the referenced media key. Unlike audio,
|
|
21
|
+
* diary videos DO run through the full video pipeline (encode + thumbnail),
|
|
22
|
+
* so they get their own tag to keep the photo count accurate rather than
|
|
23
|
+
* reusing TAG_DIARY_PHOTO. */
|
|
24
|
+
export declare const TAG_DIARY_VIDEO = "ac:diary:video";
|
|
20
25
|
/** Namespace for body word tokens: `ac:text:<word>`. */
|
|
21
26
|
export declare const TEXT_TOKEN_PREFIX = "ac:text:";
|
|
22
27
|
/**
|
package/lib/utils/diary.js
CHANGED
|
@@ -18,6 +18,11 @@ export const TAG_DIARY_PREVIEW = "ac:diary:preview";
|
|
|
18
18
|
export const TAG_DIARY_PHOTO = "ac:diary:photo";
|
|
19
19
|
/** One per embedded audio recording: value is the referenced diary key. */
|
|
20
20
|
export const TAG_DIARY_AUDIO = "ac:diary:audio";
|
|
21
|
+
/** One per embedded video: value is the referenced media key. Unlike audio,
|
|
22
|
+
* diary videos DO run through the full video pipeline (encode + thumbnail),
|
|
23
|
+
* so they get their own tag to keep the photo count accurate rather than
|
|
24
|
+
* reusing TAG_DIARY_PHOTO. */
|
|
25
|
+
export const TAG_DIARY_VIDEO = "ac:diary:video";
|
|
21
26
|
/** Namespace for body word tokens: `ac:text:<word>`. */
|
|
22
27
|
export const TEXT_TOKEN_PREFIX = "ac:text:";
|
|
23
28
|
/**
|
|
@@ -91,14 +96,85 @@ export const parseDiaryKeyDate = (key) => {
|
|
|
91
96
|
// A small English stop-word set — high-frequency words that add noise and bloat
|
|
92
97
|
// the index without improving recall for a personal diary.
|
|
93
98
|
const STOP_WORDS = new Set([
|
|
94
|
-
"the",
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
99
|
+
"the",
|
|
100
|
+
"and",
|
|
101
|
+
"for",
|
|
102
|
+
"are",
|
|
103
|
+
"but",
|
|
104
|
+
"not",
|
|
105
|
+
"you",
|
|
106
|
+
"all",
|
|
107
|
+
"any",
|
|
108
|
+
"can",
|
|
109
|
+
"had",
|
|
110
|
+
"her",
|
|
111
|
+
"was",
|
|
112
|
+
"one",
|
|
113
|
+
"our",
|
|
114
|
+
"out",
|
|
115
|
+
"day",
|
|
116
|
+
"get",
|
|
117
|
+
"has",
|
|
118
|
+
"him",
|
|
119
|
+
"his",
|
|
120
|
+
"how",
|
|
121
|
+
"its",
|
|
122
|
+
"may",
|
|
123
|
+
"new",
|
|
124
|
+
"now",
|
|
125
|
+
"old",
|
|
126
|
+
"see",
|
|
127
|
+
"two",
|
|
128
|
+
"way",
|
|
129
|
+
"who",
|
|
130
|
+
"did",
|
|
131
|
+
"yes",
|
|
132
|
+
"his",
|
|
133
|
+
"she",
|
|
134
|
+
"they",
|
|
135
|
+
"them",
|
|
136
|
+
"this",
|
|
137
|
+
"that",
|
|
138
|
+
"with",
|
|
139
|
+
"have",
|
|
140
|
+
"from",
|
|
141
|
+
"your",
|
|
142
|
+
"were",
|
|
143
|
+
"been",
|
|
144
|
+
"their",
|
|
145
|
+
"what",
|
|
146
|
+
"when",
|
|
147
|
+
"then",
|
|
148
|
+
"than",
|
|
149
|
+
"into",
|
|
150
|
+
"just",
|
|
151
|
+
"like",
|
|
152
|
+
"over",
|
|
153
|
+
"also",
|
|
154
|
+
"back",
|
|
155
|
+
"after",
|
|
156
|
+
"would",
|
|
157
|
+
"could",
|
|
158
|
+
"there",
|
|
159
|
+
"here",
|
|
160
|
+
"about",
|
|
161
|
+
"which",
|
|
162
|
+
"while",
|
|
163
|
+
"these",
|
|
164
|
+
"those",
|
|
165
|
+
"where",
|
|
166
|
+
"very",
|
|
167
|
+
"much",
|
|
168
|
+
"some",
|
|
169
|
+
"such",
|
|
170
|
+
"only",
|
|
171
|
+
"more",
|
|
172
|
+
"most",
|
|
173
|
+
"will",
|
|
174
|
+
"well",
|
|
175
|
+
"went",
|
|
176
|
+
"going",
|
|
177
|
+
"got",
|
|
102
178
|
]);
|
|
103
179
|
/**
|
|
104
180
|
* Tokenise diary markdown into a deduplicated set of searchable lowercase
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type TagInput = {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Fallback tags from device hints for fields the file's own metadata (EXIF
|
|
7
|
+
* for photos, ffprobe container tags for videos) didn't provide. The
|
|
8
|
+
* extracted metadata always wins: a hint is used only when the corresponding
|
|
9
|
+
* key is absent from `extractedTags`. Only `dateCreated` / `latitude` /
|
|
10
|
+
* `longitude` are pushed; processMeta derives year/month/day from dateCreated
|
|
11
|
+
* and reverse-geocodes lat/long into country/region tags exactly as it does
|
|
12
|
+
* for real metadata. Shared by the image and video meta-extractors.
|
|
13
|
+
*/
|
|
14
|
+
export default function hintFallbackTags(extractedTags: TagInput[], s3Metadata: Record<string, string> | undefined): TagInput[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// S3 user-metadata keys written by the diary upload-url route when the
|
|
2
|
+
// browser supplied device hints (mobile OSes strip GPS EXIF/ffprobe metadata —
|
|
3
|
+
// and sometimes the date — from files handed to web pages, so the client
|
|
4
|
+
// sends what it knows: the file's own timestamp and, for just-taken photos or
|
|
5
|
+
// videos, the device's current position). S3 lowercases metadata keys, so
|
|
6
|
+
// these are the exact wire names as they come back from HeadObject.
|
|
7
|
+
const HINT_DATE = "hint-date";
|
|
8
|
+
const HINT_LATITUDE = "hint-latitude";
|
|
9
|
+
const HINT_LONGITUDE = "hint-longitude";
|
|
10
|
+
// Number() quirks: Number("") and Number(" ") are 0, not NaN — an empty
|
|
11
|
+
// hint must not turn into a valid coordinate at (0, 0).
|
|
12
|
+
const parseFinite = (value) => {
|
|
13
|
+
if (value === undefined || value.trim() === "")
|
|
14
|
+
return undefined;
|
|
15
|
+
const n = Number(value);
|
|
16
|
+
return Number.isFinite(n) ? n : undefined;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Fallback tags from device hints for fields the file's own metadata (EXIF
|
|
20
|
+
* for photos, ffprobe container tags for videos) didn't provide. The
|
|
21
|
+
* extracted metadata always wins: a hint is used only when the corresponding
|
|
22
|
+
* key is absent from `extractedTags`. Only `dateCreated` / `latitude` /
|
|
23
|
+
* `longitude` are pushed; processMeta derives year/month/day from dateCreated
|
|
24
|
+
* and reverse-geocodes lat/long into country/region tags exactly as it does
|
|
25
|
+
* for real metadata. Shared by the image and video meta-extractors.
|
|
26
|
+
*/
|
|
27
|
+
export default function hintFallbackTags(extractedTags, s3Metadata) {
|
|
28
|
+
if (!s3Metadata)
|
|
29
|
+
return [];
|
|
30
|
+
const has = (key) => extractedTags.some((t) => t.key === key);
|
|
31
|
+
const out = [];
|
|
32
|
+
if (!has("dateCreated") && s3Metadata[HINT_DATE]) {
|
|
33
|
+
const d = new Date(s3Metadata[HINT_DATE]);
|
|
34
|
+
if (!isNaN(d.getTime())) {
|
|
35
|
+
out.push({ key: "dateCreated", value: d.toISOString() });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Coordinates only ever make sense as a pair; note 0 is a valid value
|
|
39
|
+
// (equator / prime meridian), so presence checks must not be falsy checks.
|
|
40
|
+
if (!has("latitude") && !has("longitude")) {
|
|
41
|
+
const lat = parseFinite(s3Metadata[HINT_LATITUDE]);
|
|
42
|
+
const lon = parseFinite(s3Metadata[HINT_LONGITUDE]);
|
|
43
|
+
if (lat !== undefined &&
|
|
44
|
+
lon !== undefined &&
|
|
45
|
+
Math.abs(lat) <= 90 &&
|
|
46
|
+
Math.abs(lon) <= 180) {
|
|
47
|
+
out.push({ key: "latitude", value: String(lat) }, { key: "longitude", value: String(lon) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
package/lib/utils/index.d.ts
CHANGED
package/lib/utils/index.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { DynamoDBService, LocationService } from "../services/index.js";
|
|
2
2
|
import { Logger } from "@aws-lambda-powertools/logger";
|
|
3
|
+
/** Indexed tag holding the normalized media type of a meta record. */
|
|
4
|
+
export declare const TAG_MEDIA_TYPE = "ac:tau:type";
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a file extension into the coarse media type used for search
|
|
7
|
+
* filtering. Mirrors the allowlists that already gate pipeline dispatch
|
|
8
|
+
* (thumbsKey.ts's image/video lists, diary.ts's audio list) — kept as a
|
|
9
|
+
* single derivation point so the index tag and the dispatch gating never
|
|
10
|
+
* disagree about what an extension is.
|
|
11
|
+
*/
|
|
12
|
+
export declare const deriveMediaType: (extension: string) => "photo" | "video" | "audio" | undefined;
|
|
3
13
|
type ExtractMetadataParams = {
|
|
4
14
|
id: string;
|
|
5
15
|
meta: Array<{
|
|
@@ -20,7 +30,7 @@ type ExtractMetadataParams = {
|
|
|
20
30
|
* 4. add all old tags that are not present in new tags,
|
|
21
31
|
*
|
|
22
32
|
*/
|
|
23
|
-
export declare const processMeta: ({ id, meta, metaTableName, placeIndexName, size, logger, locationService, dynamoDBService }: ExtractMetadataParams) => Promise<void>;
|
|
33
|
+
export declare const processMeta: ({ id, meta, metaTableName, placeIndexName, size, logger, locationService, dynamoDBService, }: ExtractMetadataParams) => Promise<void>;
|
|
24
34
|
/**
|
|
25
35
|
* Parent folder of an S3 key, used as the `folder` GSI partition key on the meta table.
|
|
26
36
|
*
|
package/lib/utils/processMeta.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
|
-
import { getKeyExtension } from "./thumbsKey.js";
|
|
1
|
+
import { ALLOWED_EXTENSIONS, ALLOWED_VIDEO_EXTENSIONS, getKeyExtension, } from "./thumbsKey.js";
|
|
2
|
+
import { AUDIO_EXTENSIONS } from "./diary.js";
|
|
2
3
|
import { parseFolderDate } from "./parseFolderDate.js";
|
|
4
|
+
/** Indexed tag holding the normalized media type of a meta record. */
|
|
5
|
+
export const TAG_MEDIA_TYPE = "ac:tau:type";
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes a file extension into the coarse media type used for search
|
|
8
|
+
* filtering. Mirrors the allowlists that already gate pipeline dispatch
|
|
9
|
+
* (thumbsKey.ts's image/video lists, diary.ts's audio list) — kept as a
|
|
10
|
+
* single derivation point so the index tag and the dispatch gating never
|
|
11
|
+
* disagree about what an extension is.
|
|
12
|
+
*/
|
|
13
|
+
export const deriveMediaType = (extension) => {
|
|
14
|
+
if (ALLOWED_VIDEO_EXTENSIONS.includes(extension))
|
|
15
|
+
return "video";
|
|
16
|
+
if (AUDIO_EXTENSIONS.includes(extension))
|
|
17
|
+
return "audio";
|
|
18
|
+
if (ALLOWED_EXTENSIONS.includes(extension))
|
|
19
|
+
return "photo";
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
3
22
|
/**
|
|
4
23
|
* 1. read <id>
|
|
5
24
|
* 2. if <id> doesn't exist then use PutCommand
|
|
@@ -7,7 +26,7 @@ import { parseFolderDate } from "./parseFolderDate.js";
|
|
|
7
26
|
* 4. add all old tags that are not present in new tags,
|
|
8
27
|
*
|
|
9
28
|
*/
|
|
10
|
-
export const processMeta = async ({ id, meta, metaTableName, placeIndexName, size, logger, locationService, dynamoDBService }) => {
|
|
29
|
+
export const processMeta = async ({ id, meta, metaTableName, placeIndexName, size, logger, locationService, dynamoDBService, }) => {
|
|
11
30
|
// expand location data
|
|
12
31
|
const latitude = Number(meta.find((tag) => tag.key === "latitude")?.value);
|
|
13
32
|
const longitude = Number(meta.find((tag) => tag.key === "longitude")?.value);
|
|
@@ -16,7 +35,7 @@ export const processMeta = async ({ id, meta, metaTableName, placeIndexName, siz
|
|
|
16
35
|
try {
|
|
17
36
|
const res = await locationService.searchPlaceIndexForPositionCommand({
|
|
18
37
|
IndexName: placeIndexName,
|
|
19
|
-
Position: [longitude, latitude]
|
|
38
|
+
Position: [longitude, latitude],
|
|
20
39
|
});
|
|
21
40
|
if (res?.Results && res?.Results?.length > 0) {
|
|
22
41
|
geoPositionResult = res?.Results[0];
|
|
@@ -27,7 +46,7 @@ export const processMeta = async ({ id, meta, metaTableName, placeIndexName, siz
|
|
|
27
46
|
}
|
|
28
47
|
catch (error) {
|
|
29
48
|
logger.error("Error fetching geo data", {
|
|
30
|
-
message: error instanceof Error ? error.message : ""
|
|
49
|
+
message: error instanceof Error ? error.message : "",
|
|
31
50
|
});
|
|
32
51
|
}
|
|
33
52
|
}
|
|
@@ -53,8 +72,12 @@ export const processMeta = async ({ id, meta, metaTableName, placeIndexName, siz
|
|
|
53
72
|
// Date tags derived from the S3 key path (used when EXIF has no date).
|
|
54
73
|
const keyDateTags = dateCreatedTag ? [] : extractMetaFromKey(id);
|
|
55
74
|
// Sentinel tags — always written so the search index can filter by absence of data.
|
|
56
|
-
const hasDate = !!(dateCreatedTag ||
|
|
75
|
+
const hasDate = !!(dateCreatedTag ||
|
|
76
|
+
extraDateTags.length > 0 ||
|
|
77
|
+
keyDateTags.length > 0);
|
|
57
78
|
const hasLocation = !!geoPositionResult?.Place?.Country;
|
|
79
|
+
const extension = id.endsWith("/") ? "" : getKeyExtension(id);
|
|
80
|
+
const mediaType = deriveMediaType(extension);
|
|
58
81
|
const newTags = [
|
|
59
82
|
...meta,
|
|
60
83
|
...extraDateTags,
|
|
@@ -62,9 +85,10 @@ export const processMeta = async ({ id, meta, metaTableName, placeIndexName, siz
|
|
|
62
85
|
...(id.endsWith("/")
|
|
63
86
|
? []
|
|
64
87
|
: [
|
|
65
|
-
{ key: "extension", value:
|
|
88
|
+
{ key: "extension", value: extension },
|
|
66
89
|
{ key: "size", value: String(size) },
|
|
67
90
|
{ key: "sizeMb", value: String(Math.round(size / 1024 ** 2)) },
|
|
91
|
+
...(mediaType ? [{ key: "type", value: mediaType }] : []),
|
|
68
92
|
]),
|
|
69
93
|
...(geoPositionResult?.Place?.Label
|
|
70
94
|
? [{ key: "label", value: geoPositionResult?.Place?.Label }]
|
|
@@ -91,15 +115,15 @@ export const processMeta = async ({ id, meta, metaTableName, placeIndexName, siz
|
|
|
91
115
|
{ key: "hasLocation", value: hasLocation ? "true" : "false" },
|
|
92
116
|
].map((tag) => ({
|
|
93
117
|
key: "ac:tau:" + tag.key,
|
|
94
|
-
value: tag.value
|
|
118
|
+
value: tag.value,
|
|
95
119
|
}));
|
|
96
120
|
logger.debug("metaData", { newTags });
|
|
97
121
|
logger.debug("trying to read existing metadata");
|
|
98
122
|
const getResponse = await dynamoDBService.getCommand({
|
|
99
123
|
TableName: metaTableName,
|
|
100
124
|
Key: {
|
|
101
|
-
id
|
|
102
|
-
}
|
|
125
|
+
id,
|
|
126
|
+
},
|
|
103
127
|
});
|
|
104
128
|
const oldTags = getResponse.Item?.tags;
|
|
105
129
|
logger.debug("existing tags", { getResponse });
|
|
@@ -108,17 +132,17 @@ export const processMeta = async ({ id, meta, metaTableName, placeIndexName, siz
|
|
|
108
132
|
const updateResponse = await dynamoDBService.updateCommand({
|
|
109
133
|
TableName: metaTableName,
|
|
110
134
|
Key: {
|
|
111
|
-
id
|
|
135
|
+
id,
|
|
112
136
|
},
|
|
113
137
|
UpdateExpression: "set tags = :tags, #folder = :folder",
|
|
114
138
|
ExpressionAttributeNames: {
|
|
115
|
-
"#folder": "folder"
|
|
139
|
+
"#folder": "folder",
|
|
116
140
|
},
|
|
117
141
|
ExpressionAttributeValues: {
|
|
118
142
|
":tags": reconciledTags,
|
|
119
|
-
":folder": deriveFolder(id)
|
|
143
|
+
":folder": deriveFolder(id),
|
|
120
144
|
},
|
|
121
|
-
ReturnValues: "ALL_NEW"
|
|
145
|
+
ReturnValues: "ALL_NEW",
|
|
122
146
|
});
|
|
123
147
|
logger.debug("sent UpdateCommand", { updateResponse });
|
|
124
148
|
};
|
|
@@ -162,7 +186,9 @@ const extractMetaFromKey = (key) => {
|
|
|
162
186
|
if (!key)
|
|
163
187
|
return [];
|
|
164
188
|
// Look at the parent folder, not the file itself.
|
|
165
|
-
const parent = key.endsWith("/")
|
|
189
|
+
const parent = key.endsWith("/")
|
|
190
|
+
? key
|
|
191
|
+
: key.slice(0, key.lastIndexOf("/") + 1);
|
|
166
192
|
const parsed = parseFolderDate(parent);
|
|
167
193
|
if (!parsed)
|
|
168
194
|
return [];
|