@aspan-corporation/ac-shared 1.2.37 → 1.2.39

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.
@@ -15,8 +15,26 @@ export declare const TAG_DIARY_TITLE = "ac:diary:title";
15
15
  export declare const TAG_DIARY_PREVIEW = "ac:diary:preview";
16
16
  /** One per embedded photo: value is the referenced media key. */
17
17
  export declare const TAG_DIARY_PHOTO = "ac:diary:photo";
18
+ /** One per embedded audio recording: value is the referenced diary key. */
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";
18
25
  /** Namespace for body word tokens: `ac:text:<word>`. */
19
26
  export declare const TEXT_TOKEN_PREFIX = "ac:text:";
27
+ /**
28
+ * Audio formats a browser MediaRecorder can produce: Safari records
29
+ * audio/mp4 (.m4a), Chromium/Firefox audio/webm or audio/ogg (opus).
30
+ * Deliberately kept SEPARATE from the image-only `isAllowedExtension`
31
+ * (thumbsKey.ts) — that allowlist is what keeps diary audio out of the
32
+ * meta-extractor/resizer dispatch on save, so recordings stay entry-only
33
+ * (no meta item, never in browse/search).
34
+ */
35
+ export declare const AUDIO_EXTENSIONS: string[];
36
+ /** True when a key's extension is a supported diary audio format. */
37
+ export declare const isAllowedAudioExtension: (key: string) => boolean;
20
38
  /** Max body characters surfaced in the preview tag. */
21
39
  export declare const DIARY_PREVIEW_LENGTH = 160;
22
40
  /** Upper bound on distinct word tokens written per entry (bounds item size). */
@@ -5,6 +5,7 @@
5
5
  * searchable by tokenising it into `ac:text:<word>` tags that ride the existing
6
6
  * inverted-index search table.
7
7
  */
8
+ import { getKeyExtension } from "./thumbsKey.js";
8
9
  /** Key prefix for every diary object (and the `folder`/id namespace). */
9
10
  export const DIARY_PREFIX = "diary/";
10
11
  /** Marks a meta item as a diary entry (value "true"). */
@@ -15,8 +16,26 @@ export const TAG_DIARY_TITLE = "ac:diary:title";
15
16
  export const TAG_DIARY_PREVIEW = "ac:diary:preview";
16
17
  /** One per embedded photo: value is the referenced media key. */
17
18
  export const TAG_DIARY_PHOTO = "ac:diary:photo";
19
+ /** One per embedded audio recording: value is the referenced diary key. */
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";
18
26
  /** Namespace for body word tokens: `ac:text:<word>`. */
19
27
  export const TEXT_TOKEN_PREFIX = "ac:text:";
28
+ /**
29
+ * Audio formats a browser MediaRecorder can produce: Safari records
30
+ * audio/mp4 (.m4a), Chromium/Firefox audio/webm or audio/ogg (opus).
31
+ * Deliberately kept SEPARATE from the image-only `isAllowedExtension`
32
+ * (thumbsKey.ts) — that allowlist is what keeps diary audio out of the
33
+ * meta-extractor/resizer dispatch on save, so recordings stay entry-only
34
+ * (no meta item, never in browse/search).
35
+ */
36
+ export const AUDIO_EXTENSIONS = ["m4a", "webm", "ogg"];
37
+ /** True when a key's extension is a supported diary audio format. */
38
+ export const isAllowedAudioExtension = (key) => AUDIO_EXTENSIONS.includes(getKeyExtension(key));
20
39
  /** Max body characters surfaced in the preview tag. */
21
40
  export const DIARY_PREVIEW_LENGTH = 160;
22
41
  /** Upper bound on distinct word tokens written per entry (bounds item size). */
@@ -77,14 +96,85 @@ export const parseDiaryKeyDate = (key) => {
77
96
  // A small English stop-word set — high-frequency words that add noise and bloat
78
97
  // the index without improving recall for a personal diary.
79
98
  const STOP_WORDS = new Set([
80
- "the", "and", "for", "are", "but", "not", "you", "all", "any", "can", "had",
81
- "her", "was", "one", "our", "out", "day", "get", "has", "him", "his", "how",
82
- "its", "may", "new", "now", "old", "see", "two", "way", "who", "did", "yes",
83
- "his", "she", "they", "them", "this", "that", "with", "have", "from", "your",
84
- "were", "been", "their", "what", "when", "then", "than", "into", "just",
85
- "like", "over", "also", "back", "after", "would", "could", "there", "here",
86
- "about", "which", "while", "these", "those", "where", "very", "much", "some",
87
- "such", "only", "more", "most", "will", "well", "went", "going", "got",
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",
88
178
  ]);
89
179
  /**
90
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
+ }
@@ -5,3 +5,4 @@ export * from "./helpers.js";
5
5
  export * from "./processMeta.js";
6
6
  export * from "./parseFolderDate.js";
7
7
  export * from "./diary.js";
8
+ export { default as hintFallbackTags, type TagInput } from "./hintFallback.js";
@@ -9,3 +9,4 @@ export * from "./helpers.js";
9
9
  export * from "./processMeta.js";
10
10
  export * from "./parseFolderDate.js";
11
11
  export * from "./diary.js";
12
+ export { default as hintFallbackTags } from "./hintFallback.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspan-corporation/ac-shared",
3
- "version": "1.2.37",
3
+ "version": "1.2.39",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "exports": {