@chronicle.app/apple-voice-memos 0.4.0 → 0.5.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/README.md CHANGED
@@ -18,7 +18,7 @@ Recordings come out newest first, and `--since` and `--until` bound when each on
18
18
 
19
19
  | Voice Memos thing | Chronicle node | Key | Properties |
20
20
  | ----------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21
- | Recording a memo | `RecordAction` | `@type`, `source`, `sourceId` (the recording's UUID) | `agent` (you), `instrument` (the OS release and the kind of device), `result` (the recording), `timestamp` (when recording started) |
21
+ | Recording a memo | `RecordAction` | `@type`, `source`, `result.sourceId` (the recording it made) | `agent` (you), `instrument` (the OS release and the kind of device), `result` (the recording), `timestamp` (when recording started) |
22
22
  | The OS release the file names | `OperatingSystem` | `@type`, `source` (`apple-voice-memos`), `handle` (the tag as written, such as `Watch Version 11.5 (Build 22T572)`) | |
23
23
  | The kind of device | `ProductModel` | `source` (`wikidata`), `sourceId` (its Wikidata ID, such as `Q2766`) | `name` (`iPhone`, `Apple Watch`, or `Mac`), `url` (its Wikidata page) |
24
24
  | The recording | `AudioObject` | `@type`, `source` (`apple-voice-memos`), `sourceId` (its UUID) | `name` (its title), `duration`, `contentPath` and `mimeType` (its audio file, when it's on this Mac: `audio/mp4` for `.m4a`, `audio/x-quicktime` for `.qta`), `transcript` (its text) |
@@ -50,7 +50,7 @@ Recordings come out newest first, and `--since` and `--until` bound when each on
50
50
 
51
51
  **The words spoken are the recording's `transcript`.** Voice Memos transcribes recordings itself, from iOS 18 on. The text is the recording's `transcript`, plain text with no timings, not its `body`, since it's a machine's reading of what was said rather than text anyone wrote. It's Voice Memos' own transcript: when Voice Memos makes a new one, such as after a trim, it takes the old one's place. A transcript from another speech recognizer belongs to that source's own record of the recording, so it never replaces Apple's. A recording Voice Memos heard no speech in has an empty transcript, or only spaces, which is left out. The extracted record keeps when each word was spoken and the transcript's locale, which have no Chronicle terms yet.
52
52
 
53
- **The agent is your iCloud account,** whose library this is, from `@chronicle.app/icloud`. It's omitted when no account can be read. An explicit `account` object (or null) describes a copied library without a host account lookup.
53
+ **The agent is your iCloud account,** whose library this is, from `@chronicle.app/apple`. It's omitted when no account can be read. An explicit `account` object (or null) describes a copied library without a host account lookup.
54
54
 
55
55
  **The files are read in the plugin.** A transcript is JSON inside the audio file: a `.m4a` keeps it in its audio track's `tsrp` box, and a `.qta` in its track's QuickTime metadata under `com.apple.VoiceMemos.tsrp`. `mp4.ts` reads only the movie header, not the audio. The transcript lists its text in runs, each with when it was spoken; the extracted record keeps the runs and the locale. A file or transcript that doesn't read is logged and left out; the recording is kept.
56
56
 
@@ -1,4 +1,4 @@
1
- import { Record } from '@chronicle.app/etl';
1
+ import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
2
2
  import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
3
3
  import { type Transcript } from '../transcript.js';
4
4
  import AppleVoiceMemosTransformer from './AppleVoiceMemosTransformer.js';
@@ -36,6 +36,11 @@ export declare class AppleVoiceMemosExtractor extends SqliteExtractor<typeof App
36
36
  static default: boolean;
37
37
  static defaultTransformer: typeof AppleVoiceMemosTransformer;
38
38
  static newestFirst: boolean;
39
+ static databases: {
40
+ tables: {
41
+ ZCLOUDRECORDING: string[];
42
+ };
43
+ }[];
39
44
  static schema: any;
40
45
  private libraryChanged;
41
46
  private unreadable;
@@ -61,5 +66,7 @@ export declare class AppleVoiceMemosExtractor extends SqliteExtractor<typeof App
61
66
  */
62
67
  private hintUnreadable;
63
68
  determineCount(): Promise<number | null>;
69
+ /** The recordings a run reads and when they started, with how long they last, to the second. */
70
+ survey(options?: SurveyOptions): Promise<Survey | null>;
64
71
  }
65
72
  //# sourceMappingURL=AppleVoiceMemosExtractor.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { statSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
- import { APPLE_EPOCH_OFFSET_SECONDS, SqliteExtractor, getRow, iterateRows, timeRangeConditions, unixToSafariTimestamp, } from '@chronicle.app/etl-sqlite';
4
- import { getICloudAccount } from '@chronicle.app/icloud';
3
+ import { APPLE_EPOCH_OFFSET_SECONDS, SqliteExtractor, allRows, countsByMonth, getRow, iterateRows, monthSql, timeRangeConditions, unixToSafariTimestamp, } from '@chronicle.app/etl-sqlite';
4
+ import { getICloudAccount } from '@chronicle.app/apple';
5
5
  import { z } from 'zod';
6
6
  import { readRecordingMetadata } from '../mp4.js';
7
7
  import { parseTranscript } from '../transcript.js';
@@ -25,6 +25,22 @@ export class AppleVoiceMemosExtractor extends SqliteExtractor {
25
25
  static default = true;
26
26
  static defaultTransformer = AppleVoiceMemosTransformer;
27
27
  static newestFirst = true;
28
+ // What extract() and survey() read, which a CloudRecordings.db must have.
29
+ static databases = [
30
+ {
31
+ tables: {
32
+ ZCLOUDRECORDING: [
33
+ 'ZUNIQUEID',
34
+ 'ZDATE',
35
+ 'ZDURATION',
36
+ 'ZENCRYPTEDTITLE',
37
+ 'ZPATH',
38
+ 'ZFLAGS',
39
+ 'ZEVICTIONDATE',
40
+ ],
41
+ },
42
+ },
43
+ ];
28
44
  static schema = SqliteExtractor.schema.omit({ input: true }).extend({
29
45
  input: z.string().describe('Path to CloudRecordings.db').default(DEFAULT_DB),
30
46
  account: z
@@ -157,6 +173,28 @@ export class AppleVoiceMemosExtractor extends SqliteExtractor {
157
173
  return null;
158
174
  }
159
175
  }
176
+ /** The recordings a run reads and when they started, with how long they last, to the second. */
177
+ async survey(options = {}) {
178
+ if (!this.db)
179
+ return null;
180
+ const { sql, values } = this.query(`COUNT(*) AS n, MIN(ZDATE) AS first, MAX(ZDATE) AS last,
181
+ COALESCE(ROUND(SUM(ZDURATION)), 0) AS seconds`);
182
+ const row = getRow(this.db.prepare(sql), ...values);
183
+ const months = this.query(`${monthSql(`ZDATE + ${APPLE_EPOCH_OFFSET_SECONDS}`)} AS month, COUNT(*) AS n`);
184
+ const byMonth = options.byMonth &&
185
+ countsByMonth(allRows(this.db.prepare(`${months.sql} GROUP BY month`), ...months.values));
186
+ return {
187
+ recordings: {
188
+ count: row.n,
189
+ ...(row.first !== null && { earliest: new Date(appleDate(row.first)) }),
190
+ ...(row.last !== null && { latest: new Date(appleDate(row.last)) }),
191
+ ...(byMonth && { byMonth }),
192
+ measures: [
193
+ { key: 'recordingSeconds', label: 'recordings', value: row.seconds, unit: 'seconds' },
194
+ ],
195
+ },
196
+ };
197
+ }
160
198
  }
161
199
  /**
162
200
  * The file, when it's on this Mac. A recording iCloud hasn't downloaded has
@@ -1,6 +1,6 @@
1
1
  import { extname } from 'node:path';
2
2
  import { ChronicleTransformer } from '@chronicle.app/etl';
3
- import { buildICloudPersonSchema } from '@chronicle.app/icloud';
3
+ import { buildICloudPersonSchema } from '@chronicle.app/apple';
4
4
  import { readEncoder } from '../encoder.js';
5
5
  const SOURCE = 'apple-voice-memos';
6
6
  // What each kind of recording file is. `.qta` is a QuickTime audio movie,
@@ -29,9 +29,9 @@ export default class AppleVoiceMemosTransformer extends ChronicleTransformer {
29
29
  // recording's duration isn't always the end.
30
30
  const action = {
31
31
  '@type': 'RecordAction',
32
- '@key': ['@type', 'source', 'sourceId'],
32
+ // Keyed by the recording it made, which has the memo's ID.
33
+ '@key': ['@type', 'source', 'result.sourceId'],
33
34
  source: SOURCE,
34
- sourceId: memo.uniqueID,
35
35
  timestamp: new Date(memo.date),
36
36
  ...(recorder && { agent: recorder }),
37
37
  ...(instrument.length > 0 && { instrument }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronicle.app/apple-voice-memos",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -24,9 +24,9 @@
24
24
  "type": "module",
25
25
  "description": "Chronicle plugin for your Voice Memos recordings and their transcripts, from the library macOS keeps in step with iCloud",
26
26
  "devDependencies": {
27
- "@chronicle.app/etl": "0.4.0",
28
- "@chronicle.app/schema": "0.4.0",
29
- "@chronicle.app/tsconfig": "0.4.0",
27
+ "@chronicle.app/etl": "0.5.0",
28
+ "@chronicle.app/schema": "0.5.0",
29
+ "@chronicle.app/tsconfig": "0.5.0",
30
30
  "@types/node": "^22.10.1",
31
31
  "typescript": "^5.7.2"
32
32
  },
@@ -41,8 +41,8 @@
41
41
  "registry": "https://registry.npmjs.org/"
42
42
  },
43
43
  "dependencies": {
44
- "@chronicle.app/etl-sqlite": "0.4.0",
45
- "@chronicle.app/icloud": "0.4.0",
44
+ "@chronicle.app/etl-sqlite": "0.5.0",
45
+ "@chronicle.app/apple": "0.5.0",
46
46
  "zod": "^3.24.1"
47
47
  },
48
48
  "peerDependencies": {
@@ -68,11 +68,34 @@
68
68
  "recordTypes": [
69
69
  "recordings"
70
70
  ],
71
- "default": true
71
+ "default": true,
72
+ "locations": [
73
+ {
74
+ "path": "~/Library/Group Containers/group.com.apple.VoiceMemos.shared/Recordings/CloudRecordings.db"
75
+ }
76
+ ],
77
+ "databases": [
78
+ {
79
+ "tables": {
80
+ "ZCLOUDRECORDING": [
81
+ "ZUNIQUEID",
82
+ "ZDATE",
83
+ "ZDURATION",
84
+ "ZENCRYPTEDTITLE",
85
+ "ZPATH",
86
+ "ZFLAGS",
87
+ "ZEVICTIONDATE"
88
+ ]
89
+ }
90
+ }
91
+ ]
72
92
  }
73
93
  },
74
94
  "platforms": [
75
95
  "darwin"
96
+ ],
97
+ "requires": [
98
+ "full-disk-access"
76
99
  ]
77
100
  }
78
101
  }