henri-photo 2.0.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 +108 -0
- package/dist/analyze/batch-runner.d.ts +10 -0
- package/dist/analyze/batch-runner.js +144 -0
- package/dist/analyze/local-analyzer.d.ts +16 -0
- package/dist/analyze/local-analyzer.js +119 -0
- package/dist/analyze/local-schema.d.ts +381 -0
- package/dist/analyze/local-schema.js +78 -0
- package/dist/analyze/prompt.d.ts +3 -0
- package/dist/analyze/prompt.js +91 -0
- package/dist/analyze/schema.d.ts +216 -0
- package/dist/analyze/schema.js +46 -0
- package/dist/analyze/upload-analyzer.d.ts +6 -0
- package/dist/analyze/upload-analyzer.js +45 -0
- package/dist/analyze/vision-analyzer.d.ts +10 -0
- package/dist/analyze/vision-analyzer.js +56 -0
- package/dist/channels/telegram-adapter.d.ts +8 -0
- package/dist/channels/telegram-adapter.js +282 -0
- package/dist/channels/twilio-adapter.d.ts +7 -0
- package/dist/channels/twilio-adapter.js +48 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +405 -0
- package/dist/context/context-log.d.ts +22 -0
- package/dist/context/context-log.js +91 -0
- package/dist/core/conversation-router.d.ts +9 -0
- package/dist/core/conversation-router.js +199 -0
- package/dist/core/database.d.ts +4 -0
- package/dist/core/database.js +48 -0
- package/dist/core/main.d.ts +11 -0
- package/dist/core/main.js +94 -0
- package/dist/curate/collection-builder.d.ts +12 -0
- package/dist/curate/collection-builder.js +99 -0
- package/dist/curate/cross-body.d.ts +18 -0
- package/dist/curate/cross-body.js +142 -0
- package/dist/curate/essay-builder.d.ts +61 -0
- package/dist/curate/essay-builder.js +299 -0
- package/dist/curate/pair-finder.d.ts +8 -0
- package/dist/curate/pair-finder.js +93 -0
- package/dist/curate/scorer.d.ts +24 -0
- package/dist/curate/scorer.js +77 -0
- package/dist/curate/sequencer.d.ts +8 -0
- package/dist/curate/sequencer.js +66 -0
- package/dist/http/server.d.ts +7 -0
- package/dist/http/server.js +262 -0
- package/dist/ingest/eye-pipeline.d.ts +58 -0
- package/dist/ingest/eye-pipeline.js +342 -0
- package/dist/ingest/pipeline.d.ts +86 -0
- package/dist/ingest/pipeline.js +439 -0
- package/dist/lib/api.d.ts +3 -0
- package/dist/lib/api.js +25 -0
- package/dist/lib/config.d.ts +52 -0
- package/dist/lib/config.js +73 -0
- package/dist/lib/manifest.d.ts +74 -0
- package/dist/lib/manifest.js +40 -0
- package/dist/lib/state.d.ts +3 -0
- package/dist/lib/state.js +21 -0
- package/dist/lib/text.d.ts +6 -0
- package/dist/lib/text.js +51 -0
- package/dist/mcp/bin.d.ts +10 -0
- package/dist/mcp/bin.js +21 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.js +89 -0
- package/dist/photos/apple-photos.d.ts +33 -0
- package/dist/photos/apple-photos.js +125 -0
- package/dist/search/query.d.ts +10 -0
- package/dist/search/query.js +93 -0
- package/dist/signal/generate-feed.d.ts +41 -0
- package/dist/signal/generate-feed.js +134 -0
- package/dist/signal/output.d.ts +17 -0
- package/dist/signal/output.js +111 -0
- package/dist/tools/henri-tools.d.ts +7 -0
- package/dist/tools/henri-tools.js +536 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# henri-photo
|
|
2
|
+
|
|
3
|
+
Photographic intelligence via [Model Context Protocol](https://modelcontextprotocol.io). HENRI analyzes, curates, and critiques photographs with museum-curator eye.
|
|
4
|
+
|
|
5
|
+
Named after Henri Cartier-Bresson. Reads through formal analysis, historical literacy, and curatorial judgment.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Add to your Claude Code MCP settings:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"henri": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["henri-photo", "--dir", "/path/to/your/henri-data"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or run directly:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx henri-photo --dir ~/.henri
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
HENRI reads from a data directory containing your analyzed photo archive:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
~/.henri/
|
|
34
|
+
manifest-v2.json # Image manifest with analysis data
|
|
35
|
+
collections/ # Curated thematic collections
|
|
36
|
+
essays/ # Visual essays (critical prose)
|
|
37
|
+
pairs.json # Cross-body visual pairings
|
|
38
|
+
embeddings/ # CLIP embeddings, color, scene data
|
|
39
|
+
eye/ # Secondary camera manifest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
To build this data, use the HENRI CLI to analyze your photos:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Clone the repo for full CLI access
|
|
46
|
+
git clone https://github.com/sethgoldstein/henri
|
|
47
|
+
cd henri && npm install
|
|
48
|
+
|
|
49
|
+
# Set your data directory and photo source
|
|
50
|
+
export HENRI_DIR=~/.henri
|
|
51
|
+
export HENRI_PHOTOS=~/Pictures/photos
|
|
52
|
+
|
|
53
|
+
# Analyze your archive (~$0.03/image via Claude API)
|
|
54
|
+
npm run henri -- analyze --batch
|
|
55
|
+
|
|
56
|
+
# Generate collections and essays
|
|
57
|
+
npm run henri -- curate
|
|
58
|
+
npm run henri -- collections --propose
|
|
59
|
+
npm run henri -- essay -c <collection-id>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## MCP Tools
|
|
63
|
+
|
|
64
|
+
15 tools available to any MCP-compatible client:
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `search_archive` | Natural language search across all analysis axes |
|
|
69
|
+
| `get_image_analysis` | Full 5-axis analysis for a specific image |
|
|
70
|
+
| `list_collections` | All curated thematic collections |
|
|
71
|
+
| `get_collection` | Collection details with thesis and sequence |
|
|
72
|
+
| `get_daily_signal` | Daily selects, cross-body echoes, rising images |
|
|
73
|
+
| `get_tier_breakdown` | Archive quality tiers (exhibition through discard) |
|
|
74
|
+
| `get_cross_body_pairs` | Same-day echoes across camera bodies |
|
|
75
|
+
| `get_essay` | Read a visual essay |
|
|
76
|
+
| `list_essays` | All available essays |
|
|
77
|
+
| `search_by_color` | Find images by color, harmony, temperature |
|
|
78
|
+
| `get_shooting_patterns` | Temporal patterns, golden hour, burst groups |
|
|
79
|
+
| `find_similar` | CLIP-based visual similarity search |
|
|
80
|
+
| `get_scene_breakdown` | Scene types, indoor/outdoor, objects, materials |
|
|
81
|
+
| `get_people` | Face detection, distinct people, co-occurrences |
|
|
82
|
+
| `get_style_clusters` | Visual style clusters and near-duplicates |
|
|
83
|
+
|
|
84
|
+
## The 5-Axis Framework
|
|
85
|
+
|
|
86
|
+
Every image is scored across five axes:
|
|
87
|
+
|
|
88
|
+
- **Formal** -- composition, light, spatial organization, color relationships
|
|
89
|
+
- **Technical** -- sharpness, exposure, focus decisions, processing
|
|
90
|
+
- **Semantic** -- subject, narrative, emotional register, cultural context
|
|
91
|
+
- **Historical** -- photographic traditions, references, lineage
|
|
92
|
+
- **Curatorial** -- tier ranking (exhibition/portfolio/strong/study/discard), 0-100 score
|
|
93
|
+
|
|
94
|
+
## Pricing
|
|
95
|
+
|
|
96
|
+
- Analysis: ~$0.03/image (Claude API cost)
|
|
97
|
+
- Essay generation: ~$0.07/essay
|
|
98
|
+
- MCP server: free (reads pre-computed data)
|
|
99
|
+
|
|
100
|
+
## Links
|
|
101
|
+
|
|
102
|
+
- [henri.photos](https://henri.photos) -- landing page and case studies
|
|
103
|
+
- [henri.photos/ask](https://henri.photos/ask) -- chat with HENRI
|
|
104
|
+
- [henri.photos/hire](https://henri.photos/hire) -- get started
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { PATHS } from '../lib/config.js';
|
|
3
|
+
import { readJson, writeJson, log } from '../lib/state.js';
|
|
4
|
+
import { loadSiteManifest, saveHenriManifest } from '../lib/manifest.js';
|
|
5
|
+
import { analyzeImage } from './vision-analyzer.js';
|
|
6
|
+
function loadProgress() {
|
|
7
|
+
return readJson(PATHS.analysisProgress, {
|
|
8
|
+
completed: {},
|
|
9
|
+
totalCostUsd: 0,
|
|
10
|
+
totalInputTokens: 0,
|
|
11
|
+
totalOutputTokens: 0,
|
|
12
|
+
lastUpdated: new Date().toISOString(),
|
|
13
|
+
errors: [],
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function saveProgress(progress) {
|
|
17
|
+
progress.lastUpdated = new Date().toISOString();
|
|
18
|
+
writeJson(PATHS.analysisProgress, progress);
|
|
19
|
+
}
|
|
20
|
+
export async function runBatch(opts) {
|
|
21
|
+
const siteManifest = loadSiteManifest();
|
|
22
|
+
const progress = opts.resume ? loadProgress() : {
|
|
23
|
+
completed: {},
|
|
24
|
+
totalCostUsd: 0,
|
|
25
|
+
totalInputTokens: 0,
|
|
26
|
+
totalOutputTokens: 0,
|
|
27
|
+
lastUpdated: new Date().toISOString(),
|
|
28
|
+
errors: [],
|
|
29
|
+
};
|
|
30
|
+
// Determine which images to process
|
|
31
|
+
let images = siteManifest.images;
|
|
32
|
+
if (opts.singleImage) {
|
|
33
|
+
const target = opts.singleImage.endsWith('.webp') ? opts.singleImage : `${opts.singleImage}.webp`;
|
|
34
|
+
images = images.filter(img => img.filename === target);
|
|
35
|
+
if (images.length === 0) {
|
|
36
|
+
log(`image not found: ${opts.singleImage}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Filter already completed
|
|
41
|
+
const pending = images.filter(img => !progress.completed[img.filename]);
|
|
42
|
+
log(`batch: ${pending.length} pending, ${Object.keys(progress.completed).length} completed, $${progress.totalCostUsd.toFixed(2)} spent`);
|
|
43
|
+
if (opts.dryRun) {
|
|
44
|
+
log('dry run — showing first image prompt:');
|
|
45
|
+
if (pending.length > 0) {
|
|
46
|
+
const img = pending[0];
|
|
47
|
+
const { buildSystemPrompt, buildUserPrompt } = await import('./prompt.js');
|
|
48
|
+
console.log('\n--- SYSTEM PROMPT ---');
|
|
49
|
+
console.log(buildSystemPrompt());
|
|
50
|
+
console.log('\n--- USER PROMPT ---');
|
|
51
|
+
console.log(buildUserPrompt(img.exif, img.analysis));
|
|
52
|
+
console.log(`\n--- IMAGE: ${img.filename} (${opts.useThumbnails ? 'thumbnail' : 'full-res'}) ---`);
|
|
53
|
+
const imgPath = opts.useThumbnails
|
|
54
|
+
? path.join(PATHS.thumbnailDir, img.thumbnail.replace('thumbnails/', ''))
|
|
55
|
+
: path.join(PATHS.webpDir, img.path.replace('webp/', ''));
|
|
56
|
+
console.log(`Path: ${imgPath}`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Process with concurrency control
|
|
61
|
+
let active = 0;
|
|
62
|
+
let idx = 0;
|
|
63
|
+
async function processOne(img) {
|
|
64
|
+
const imgPath = opts.useThumbnails
|
|
65
|
+
? path.join(PATHS.thumbnailDir, img.thumbnail.replace('thumbnails/', ''))
|
|
66
|
+
: path.join(PATHS.webpDir, img.path.replace('webp/', ''));
|
|
67
|
+
try {
|
|
68
|
+
const result = await analyzeImage(imgPath, img.filename, img.exif, img.analysis, opts.model);
|
|
69
|
+
progress.completed[result.filename] = result.analysis;
|
|
70
|
+
progress.totalCostUsd += result.costUsd;
|
|
71
|
+
progress.totalInputTokens += result.inputTokens;
|
|
72
|
+
progress.totalOutputTokens += result.outputTokens;
|
|
73
|
+
saveProgress(progress);
|
|
74
|
+
const tier = result.analysis.curatorial.tier;
|
|
75
|
+
const score = result.analysis.curatorial.score;
|
|
76
|
+
log(`${result.filename} | ${score} | ${tier} | $${progress.totalCostUsd.toFixed(2)}/$${opts.costLimit.toFixed(2)}`);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const errMsg = err?.message || String(err);
|
|
80
|
+
log(`ERROR ${img.filename}: ${errMsg}`);
|
|
81
|
+
progress.errors.push({
|
|
82
|
+
filename: img.filename,
|
|
83
|
+
error: errMsg,
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
});
|
|
86
|
+
saveProgress(progress);
|
|
87
|
+
// Retry once on non-validation errors
|
|
88
|
+
if (!errMsg.includes('ZodError') && !errMsg.includes('parse')) {
|
|
89
|
+
log(`retrying ${img.filename}...`);
|
|
90
|
+
try {
|
|
91
|
+
const result = await analyzeImage(imgPath, img.filename, img.exif, img.analysis, opts.model);
|
|
92
|
+
progress.completed[result.filename] = result.analysis;
|
|
93
|
+
progress.totalCostUsd += result.costUsd;
|
|
94
|
+
progress.totalInputTokens += result.inputTokens;
|
|
95
|
+
progress.totalOutputTokens += result.outputTokens;
|
|
96
|
+
saveProgress(progress);
|
|
97
|
+
log(`${result.filename} | ${result.analysis.curatorial.score} | ${result.analysis.curatorial.tier} | $${progress.totalCostUsd.toFixed(2)}/$${opts.costLimit.toFixed(2)} (retry)`);
|
|
98
|
+
}
|
|
99
|
+
catch (retryErr) {
|
|
100
|
+
log(`RETRY FAILED ${img.filename}: ${retryErr?.message || retryErr}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Simple concurrency pool
|
|
106
|
+
const queue = [...pending];
|
|
107
|
+
const sessionStartCost = progress.totalCostUsd;
|
|
108
|
+
async function worker() {
|
|
109
|
+
while (queue.length > 0) {
|
|
110
|
+
// Check cost limit (per-session spend, not cumulative)
|
|
111
|
+
const sessionCost = progress.totalCostUsd - sessionStartCost;
|
|
112
|
+
if (sessionCost >= opts.costLimit) {
|
|
113
|
+
log(`session cost limit reached: $${sessionCost.toFixed(2)} >= $${opts.costLimit.toFixed(2)} (cumulative: $${progress.totalCostUsd.toFixed(2)})`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const img = queue.shift();
|
|
117
|
+
await processOne(img);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const workers = Array.from({ length: Math.min(opts.concurrency, queue.length) }, () => worker());
|
|
121
|
+
await Promise.all(workers);
|
|
122
|
+
// Build HENRI manifest
|
|
123
|
+
const henriManifest = {
|
|
124
|
+
metadata: {
|
|
125
|
+
...siteManifest.metadata,
|
|
126
|
+
henriVersion: '2.0',
|
|
127
|
+
henriAnalysisDate: new Date().toISOString(),
|
|
128
|
+
},
|
|
129
|
+
images: siteManifest.images.map(img => ({
|
|
130
|
+
...img,
|
|
131
|
+
henriAnalysis: progress.completed[img.filename],
|
|
132
|
+
})),
|
|
133
|
+
};
|
|
134
|
+
saveHenriManifest(henriManifest);
|
|
135
|
+
const completedCount = Object.keys(progress.completed).length;
|
|
136
|
+
const errorCount = progress.errors.length;
|
|
137
|
+
log(`batch complete: ${completedCount} analyzed, ${errorCount} errors, $${progress.totalCostUsd.toFixed(2)} total`);
|
|
138
|
+
// Tier breakdown
|
|
139
|
+
const tiers = { discard: 0, study: 0, strong: 0, portfolio: 0, exhibition: 0 };
|
|
140
|
+
for (const analysis of Object.values(progress.completed)) {
|
|
141
|
+
tiers[analysis.curatorial.tier]++;
|
|
142
|
+
}
|
|
143
|
+
log(`tiers: ${Object.entries(tiers).map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
144
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Analyzer — TypeScript orchestrator for Python compute modules.
|
|
3
|
+
*
|
|
4
|
+
* Calls Python workers via child_process (same pattern as osxphotos integration).
|
|
5
|
+
* Manages checkpointing and result merging.
|
|
6
|
+
*/
|
|
7
|
+
export declare function runEmbeddings(manifestPath?: string): void;
|
|
8
|
+
export declare function runColors(manifestPath?: string): void;
|
|
9
|
+
export declare function runFaces(manifestPath?: string): void;
|
|
10
|
+
export declare function runGeometry(manifestPath?: string): void;
|
|
11
|
+
export declare function runOcr(manifestPath?: string): void;
|
|
12
|
+
export declare function runTemporal(manifestPath?: string): void;
|
|
13
|
+
export declare function runScene(manifestPath?: string): void;
|
|
14
|
+
export declare function runClustering(): void;
|
|
15
|
+
export declare function runFullPipeline(manifestPath?: string): void;
|
|
16
|
+
export declare function computeStatus(): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Analyzer — TypeScript orchestrator for Python compute modules.
|
|
3
|
+
*
|
|
4
|
+
* Calls Python workers via child_process (same pattern as osxphotos integration).
|
|
5
|
+
* Manages checkpointing and result merging.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { PATHS } from '../lib/config.js';
|
|
11
|
+
import { log } from '../lib/state.js';
|
|
12
|
+
function ensureOutputDir() {
|
|
13
|
+
fs.mkdirSync(PATHS.embeddingsDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
function runPython(command, args, timeoutMs = 600_000) {
|
|
16
|
+
const pythonCmd = `python3 -m compute ${command} ${args.join(' ')}`;
|
|
17
|
+
log(`running: ${pythonCmd}`);
|
|
18
|
+
try {
|
|
19
|
+
const output = execSync(pythonCmd, {
|
|
20
|
+
cwd: path.join(PATHS.computeDir, '..'), // Run from src/ so `compute` is a package
|
|
21
|
+
timeout: timeoutMs,
|
|
22
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
25
|
+
});
|
|
26
|
+
return output;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const stderr = err.stderr?.toString() || '';
|
|
30
|
+
const stdout = err.stdout?.toString() || '';
|
|
31
|
+
log(`compute error: ${stderr.substring(0, 500)}`);
|
|
32
|
+
if (stdout)
|
|
33
|
+
log(`compute stdout: ${stdout.substring(0, 500)}`);
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ─── Individual Module Runners ──────────────────────────────────────
|
|
38
|
+
export function runEmbeddings(manifestPath) {
|
|
39
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
40
|
+
ensureOutputDir();
|
|
41
|
+
const output = runPython('embed', ['--manifest', manifest, '--output', PATHS.embeddingsDir]);
|
|
42
|
+
log(output.trim().split('\n').pop() || 'embeddings done');
|
|
43
|
+
}
|
|
44
|
+
export function runColors(manifestPath) {
|
|
45
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
46
|
+
ensureOutputDir();
|
|
47
|
+
const output = runPython('colors', ['--manifest', manifest, '--output', path.join(PATHS.embeddingsDir, 'colors.json')]);
|
|
48
|
+
log(output.trim().split('\n').pop() || 'colors done');
|
|
49
|
+
}
|
|
50
|
+
export function runFaces(manifestPath) {
|
|
51
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
52
|
+
ensureOutputDir();
|
|
53
|
+
const output = runPython('faces', ['--manifest', manifest, '--output', path.join(PATHS.embeddingsDir, 'faces.json')]);
|
|
54
|
+
log(output.trim().split('\n').pop() || 'faces done');
|
|
55
|
+
}
|
|
56
|
+
export function runGeometry(manifestPath) {
|
|
57
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
58
|
+
ensureOutputDir();
|
|
59
|
+
const output = runPython('geometry', ['--manifest', manifest, '--output', path.join(PATHS.embeddingsDir, 'geometry.json')]);
|
|
60
|
+
log(output.trim().split('\n').pop() || 'geometry done');
|
|
61
|
+
}
|
|
62
|
+
export function runOcr(manifestPath) {
|
|
63
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
64
|
+
ensureOutputDir();
|
|
65
|
+
const output = runPython('ocr', ['--manifest', manifest, '--output', path.join(PATHS.embeddingsDir, 'ocr.json')]);
|
|
66
|
+
log(output.trim().split('\n').pop() || 'ocr done');
|
|
67
|
+
}
|
|
68
|
+
export function runTemporal(manifestPath) {
|
|
69
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
70
|
+
ensureOutputDir();
|
|
71
|
+
const output = runPython('temporal', ['--manifest', manifest, '--output', path.join(PATHS.embeddingsDir, 'temporal.json')]);
|
|
72
|
+
log(output.trim().split('\n').pop() || 'temporal done');
|
|
73
|
+
}
|
|
74
|
+
export function runScene(manifestPath) {
|
|
75
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
76
|
+
ensureOutputDir();
|
|
77
|
+
const output = runPython('scene', ['--manifest', manifest, '--output', path.join(PATHS.embeddingsDir, 'scenes.json')]);
|
|
78
|
+
log(output.trim().split('\n').pop() || 'scene done');
|
|
79
|
+
}
|
|
80
|
+
export function runClustering() {
|
|
81
|
+
ensureOutputDir();
|
|
82
|
+
const output = runPython('cluster', ['--embeddings-dir', PATHS.embeddingsDir, '--output', path.join(PATHS.embeddingsDir, 'clusters.json')]);
|
|
83
|
+
log(output.trim().split('\n').pop() || 'clustering done');
|
|
84
|
+
}
|
|
85
|
+
// ─── Full Pipeline ──────────────────────────────────────────────────
|
|
86
|
+
export function runFullPipeline(manifestPath) {
|
|
87
|
+
const manifest = manifestPath || PATHS.henriManifest;
|
|
88
|
+
ensureOutputDir();
|
|
89
|
+
const output = runPython('analyze-all', ['--manifest', manifest, '--output-dir', PATHS.embeddingsDir], 1_200_000);
|
|
90
|
+
log(output.trim().split('\n').pop() || 'full pipeline done');
|
|
91
|
+
}
|
|
92
|
+
// ─── Status ─────────────────────────────────────────────────────────
|
|
93
|
+
export function computeStatus() {
|
|
94
|
+
const modules = [
|
|
95
|
+
{ name: 'CLIP embeddings', file: 'clip-embeddings.npy' },
|
|
96
|
+
{ name: 'CLIP index', file: 'clip-index.json' },
|
|
97
|
+
{ name: 'Colors', file: 'colors.json' },
|
|
98
|
+
{ name: 'Faces', file: 'faces.json' },
|
|
99
|
+
{ name: 'Face embeddings', file: 'face-embeddings.npy' },
|
|
100
|
+
{ name: 'Geometry', file: 'geometry.json' },
|
|
101
|
+
{ name: 'OCR', file: 'ocr.json' },
|
|
102
|
+
{ name: 'Temporal', file: 'temporal.json' },
|
|
103
|
+
{ name: 'Scenes', file: 'scenes.json' },
|
|
104
|
+
{ name: 'Clusters', file: 'clusters.json' },
|
|
105
|
+
];
|
|
106
|
+
log(`compute output dir: ${PATHS.embeddingsDir}`);
|
|
107
|
+
for (const mod of modules) {
|
|
108
|
+
const filePath = path.join(PATHS.embeddingsDir, mod.file);
|
|
109
|
+
if (fs.existsSync(filePath)) {
|
|
110
|
+
const stat = fs.statSync(filePath);
|
|
111
|
+
const sizeKb = Math.round(stat.size / 1024);
|
|
112
|
+
const modified = stat.mtime.toISOString().substring(0, 19);
|
|
113
|
+
log(` ${mod.name}: ${sizeKb}KB (${modified})`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
log(` ${mod.name}: not computed`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|