escribano 0.4.3 → 0.4.4

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.
@@ -3,11 +3,13 @@
3
3
  *
4
4
  * Generates a work session summary from V3 processed TopicBlocks using LLM.
5
5
  */
6
+ import { execSync } from 'node:child_process';
6
7
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
7
8
  import { homedir } from 'node:os';
8
9
  import path, { dirname, resolve } from 'node:path';
9
10
  import { fileURLToPath } from 'node:url';
10
11
  import { log } from '../pipeline/context.js';
12
+ import { groupTopicBlocksIntoSubjects, saveSubjectsToDatabase, } from '../services/subject-grouping.js';
11
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
14
  /**
13
15
  * Generate a work session summary artifact from processed TopicBlocks.
@@ -19,21 +21,39 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
19
21
  * @returns Generated artifact
20
22
  */
21
23
  export async function generateSummaryV3(recordingId, repos, intelligence, options) {
22
- log('info', `[Summary V3] Generating summary for recording ${recordingId}...`);
24
+ log('info', `[Summary V3] Generating narrative for recording ${recordingId}...`);
23
25
  // Get the recording
24
26
  const recording = repos.recordings.findById(recordingId);
25
27
  if (!recording) {
26
28
  throw new Error(`Recording ${recordingId} not found`);
27
29
  }
28
30
  // Get TopicBlocks for this recording
29
- const topicBlocks = repos.topicBlocks.findByRecording(recordingId);
30
- if (topicBlocks.length === 0) {
31
+ const allTopicBlocks = repos.topicBlocks.findByRecording(recordingId);
32
+ if (allTopicBlocks.length === 0) {
31
33
  throw new Error(`No TopicBlocks found for recording ${recordingId}. Run process-v3 first.`);
32
34
  }
33
- log('info', `[Summary V3] Found ${topicBlocks.length} TopicBlocks`);
35
+ log('info', `[Summary V3] Found ${allTopicBlocks.length} TopicBlocks`);
36
+ // Group TopicBlocks into subjects
37
+ log('info', '[Summary V3] Grouping TopicBlocks into subjects...');
38
+ const groupingResult = await groupTopicBlocksIntoSubjects(allTopicBlocks, intelligence, recordingId);
39
+ const { subjects } = groupingResult;
40
+ const { personalDuration, workDuration } = groupingResult;
41
+ // Save subjects to database
42
+ log('info', `[Summary V3] Saving ${subjects.length} subjects to database...`);
43
+ saveSubjectsToDatabase(subjects, recordingId, repos);
44
+ // Filter TopicBlocks based on personal/work classification
45
+ let topicBlocksToUse = allTopicBlocks;
46
+ if (!options.includePersonal) {
47
+ // Filter out blocks from personal subjects
48
+ const personalSubjectIds = new Set(subjects.filter((s) => s.isPersonal).map((s) => s.id));
49
+ topicBlocksToUse = allTopicBlocks.filter((block) => {
50
+ const subjectForBlock = subjects.find((s) => s.topicBlockIds.includes(block.id));
51
+ return !subjectForBlock?.isPersonal;
52
+ });
53
+ }
34
54
  // Build sections from TopicBlocks
35
55
  const sections = [];
36
- for (const block of topicBlocks) {
56
+ for (const block of topicBlocksToUse) {
37
57
  const classification = JSON.parse(block.classification || '{}');
38
58
  sections.push({
39
59
  activity: classification.activity_type || 'unknown',
@@ -65,16 +85,48 @@ export async function generateSummaryV3(recordingId, repos, intelligence, option
65
85
  await mkdir(outputDir, { recursive: true });
66
86
  // Generate filename
67
87
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
68
- const fileName = `${recordingId}-summary-${timestamp}.md`;
88
+ const fileName = `${recordingId}-narrative-${timestamp}.md`;
69
89
  const filePath = path.join(outputDir, fileName);
70
90
  // Write to file
71
91
  await writeFile(filePath, summaryContent, 'utf-8');
72
92
  log('info', `[Summary V3] Summary saved to: ${filePath}`);
93
+ // Save to database
94
+ const artifactId = `artifact-${recordingId}-narrative-${Date.now()}`;
95
+ repos.artifacts.save({
96
+ id: artifactId,
97
+ recording_id: recordingId,
98
+ type: 'narrative',
99
+ content: summaryContent,
100
+ format: 'markdown',
101
+ source_block_ids: JSON.stringify(subjects.flatMap((s) => s.topicBlockIds)),
102
+ source_context_ids: null,
103
+ });
104
+ log('info', `[Summary V3] Saved to database: ${artifactId}`);
105
+ // Link subjects to artifact
106
+ repos.artifacts.linkSubjects(artifactId, subjects.map((s) => s.id));
107
+ log('info', `[Summary V3] Linked ${subjects.length} subjects to artifact`);
108
+ // Handle stdout/clipboard
109
+ if (options.printToStdout) {
110
+ console.log(`\n${summaryContent}\n`);
111
+ }
112
+ if (options.copyToClipboard && process.platform === 'darwin') {
113
+ try {
114
+ execSync('pbcopy', { input: summaryContent, encoding: 'utf-8' });
115
+ log('info', '[Summary V3] Copied to clipboard');
116
+ }
117
+ catch (error) {
118
+ log('warn', `[Summary V3] Failed to copy to clipboard: ${error}`);
119
+ }
120
+ }
73
121
  return {
74
- id: `summary-${recordingId}-${Date.now()}`,
122
+ id: artifactId,
75
123
  recordingId,
124
+ format: 'narrative',
76
125
  content: summaryContent,
77
126
  filePath,
127
+ subjects,
128
+ personalDuration,
129
+ workDuration,
78
130
  createdAt: new Date(),
79
131
  };
80
132
  }
@@ -14,6 +14,7 @@ import { execSync } from 'node:child_process';
14
14
  import { homedir } from 'node:os';
15
15
  import path from 'node:path';
16
16
  import { generateArtifactV3, } from './actions/generate-artifact-v3.js';
17
+ import { generateSummaryV3 } from './actions/generate-summary-v3.js';
17
18
  import { updateGlobalIndex } from './actions/outline-index.js';
18
19
  import { processRecordingV3 } from './actions/process-recording-v3.js';
19
20
  import { hasContentChanged, publishSummaryV3, updateRecordingOutlineMetadata, } from './actions/publish-summary-v3.js';
@@ -168,13 +169,28 @@ export async function processVideo(videoPath, ctx, options = {}) {
168
169
  const artifactRunMetadata = collectRunMetadata(ctx.resourceTracker);
169
170
  const pipelineResult = await withPipeline(recording.id, 'artifact', artifactRunMetadata, async () => {
170
171
  console.log(`\nGenerating ${format} artifact...`);
171
- const generatedArtifact = await generateArtifactV3(recording.id, repos, llm, {
172
- recordingId: recording.id,
173
- format,
174
- includePersonal,
175
- copyToClipboard,
176
- printToStdout,
177
- });
172
+ let generatedArtifact;
173
+ if (format === 'narrative') {
174
+ // Route narrative through the corrected path
175
+ generatedArtifact = await generateSummaryV3(recording.id, repos, llm, {
176
+ recordingId: recording.id,
177
+ outputDir: options.outputDir,
178
+ useTemplate: false,
179
+ includePersonal,
180
+ copyToClipboard,
181
+ printToStdout,
182
+ });
183
+ }
184
+ else {
185
+ // Card and standup use the original path
186
+ generatedArtifact = await generateArtifactV3(recording.id, repos, llm, {
187
+ recordingId: recording.id,
188
+ format,
189
+ includePersonal,
190
+ copyToClipboard,
191
+ printToStdout,
192
+ });
193
+ }
178
194
  console.log(`Artifact saved: ${generatedArtifact.filePath}`);
179
195
  if (generatedArtifact.workDuration > 0) {
180
196
  const workMins = Math.round(generatedArtifact.workDuration / 60);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "escribano",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "AI-powered session intelligence tool — turn screen recordings into structured work summaries",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",