holosphere 1.1.19 → 2.0.0-alpha0

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.
Files changed (146) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/README.md +476 -531
  5. package/bin/holosphere-activitypub.js +158 -0
  6. package/cleanup-test-data.js +204 -0
  7. package/examples/demo.html +1333 -0
  8. package/examples/example-bot.js +197 -0
  9. package/package.json +47 -87
  10. package/scripts/check-bundle-size.js +54 -0
  11. package/scripts/check-quest-ids.js +77 -0
  12. package/scripts/import-holons.js +578 -0
  13. package/scripts/publish-to-relay.js +101 -0
  14. package/scripts/read-example.js +186 -0
  15. package/scripts/relay-diagnostic.js +59 -0
  16. package/scripts/relay-example.js +179 -0
  17. package/scripts/resync-to-relay.js +245 -0
  18. package/scripts/revert-import.js +196 -0
  19. package/scripts/test-hybrid-mode.js +108 -0
  20. package/scripts/test-local-storage.js +63 -0
  21. package/scripts/test-nostr-direct.js +55 -0
  22. package/scripts/test-read-data.js +45 -0
  23. package/scripts/test-write-read.js +63 -0
  24. package/scripts/verify-import.js +95 -0
  25. package/scripts/verify-relay-data.js +139 -0
  26. package/src/ai/aggregation.js +319 -0
  27. package/src/ai/breakdown.js +511 -0
  28. package/src/ai/classifier.js +217 -0
  29. package/src/ai/council.js +228 -0
  30. package/src/ai/embeddings.js +279 -0
  31. package/src/ai/federation-ai.js +324 -0
  32. package/src/ai/h3-ai.js +955 -0
  33. package/src/ai/index.js +112 -0
  34. package/src/ai/json-ops.js +225 -0
  35. package/src/ai/llm-service.js +205 -0
  36. package/src/ai/nl-query.js +223 -0
  37. package/src/ai/relationships.js +353 -0
  38. package/src/ai/schema-extractor.js +218 -0
  39. package/src/ai/spatial.js +293 -0
  40. package/src/ai/tts.js +194 -0
  41. package/src/content/social-protocols.js +168 -0
  42. package/src/core/holosphere.js +273 -0
  43. package/src/crypto/secp256k1.js +259 -0
  44. package/src/federation/discovery.js +334 -0
  45. package/src/federation/hologram.js +1042 -0
  46. package/src/federation/registry.js +386 -0
  47. package/src/hierarchical/upcast.js +110 -0
  48. package/src/index.js +2669 -0
  49. package/src/schema/validator.js +91 -0
  50. package/src/spatial/h3-operations.js +110 -0
  51. package/src/storage/backend-factory.js +125 -0
  52. package/src/storage/backend-interface.js +142 -0
  53. package/src/storage/backends/activitypub/server.js +653 -0
  54. package/src/storage/backends/activitypub-backend.js +272 -0
  55. package/src/storage/backends/gundb-backend.js +233 -0
  56. package/src/storage/backends/nostr-backend.js +136 -0
  57. package/src/storage/filesystem-storage-browser.js +41 -0
  58. package/src/storage/filesystem-storage.js +138 -0
  59. package/src/storage/global-tables.js +81 -0
  60. package/src/storage/gun-async.js +281 -0
  61. package/src/storage/gun-wrapper.js +221 -0
  62. package/src/storage/indexeddb-storage.js +122 -0
  63. package/src/storage/key-storage-simple.js +76 -0
  64. package/src/storage/key-storage.js +136 -0
  65. package/src/storage/memory-storage.js +59 -0
  66. package/src/storage/migration.js +338 -0
  67. package/src/storage/nostr-async.js +811 -0
  68. package/src/storage/nostr-client.js +939 -0
  69. package/src/storage/nostr-wrapper.js +211 -0
  70. package/src/storage/outbox-queue.js +208 -0
  71. package/src/storage/persistent-storage.js +109 -0
  72. package/src/storage/sync-service.js +164 -0
  73. package/src/subscriptions/manager.js +142 -0
  74. package/test-ai-real-api.js +202 -0
  75. package/tests/unit/ai/aggregation.test.js +295 -0
  76. package/tests/unit/ai/breakdown.test.js +446 -0
  77. package/tests/unit/ai/classifier.test.js +294 -0
  78. package/tests/unit/ai/council.test.js +262 -0
  79. package/tests/unit/ai/embeddings.test.js +384 -0
  80. package/tests/unit/ai/federation-ai.test.js +344 -0
  81. package/tests/unit/ai/h3-ai.test.js +458 -0
  82. package/tests/unit/ai/index.test.js +304 -0
  83. package/tests/unit/ai/json-ops.test.js +307 -0
  84. package/tests/unit/ai/llm-service.test.js +390 -0
  85. package/tests/unit/ai/nl-query.test.js +383 -0
  86. package/tests/unit/ai/relationships.test.js +311 -0
  87. package/tests/unit/ai/schema-extractor.test.js +384 -0
  88. package/tests/unit/ai/spatial.test.js +279 -0
  89. package/tests/unit/ai/tts.test.js +279 -0
  90. package/tests/unit/content.test.js +332 -0
  91. package/tests/unit/contract/core.test.js +88 -0
  92. package/tests/unit/contract/crypto.test.js +198 -0
  93. package/tests/unit/contract/data.test.js +223 -0
  94. package/tests/unit/contract/federation.test.js +181 -0
  95. package/tests/unit/contract/hierarchical.test.js +113 -0
  96. package/tests/unit/contract/schema.test.js +114 -0
  97. package/tests/unit/contract/social.test.js +217 -0
  98. package/tests/unit/contract/spatial.test.js +110 -0
  99. package/tests/unit/contract/subscriptions.test.js +128 -0
  100. package/tests/unit/contract/utils.test.js +159 -0
  101. package/tests/unit/core.test.js +152 -0
  102. package/tests/unit/crypto.test.js +328 -0
  103. package/tests/unit/federation.test.js +234 -0
  104. package/tests/unit/gun-async.test.js +252 -0
  105. package/tests/unit/hierarchical.test.js +399 -0
  106. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  107. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  108. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  109. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  110. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  111. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  112. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  113. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  114. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  115. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  116. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  117. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  118. package/tests/unit/performance/benchmark.test.js +85 -0
  119. package/tests/unit/schema.test.js +213 -0
  120. package/tests/unit/spatial.test.js +158 -0
  121. package/tests/unit/storage.test.js +195 -0
  122. package/tests/unit/subscriptions.test.js +328 -0
  123. package/tests/unit/test-data-permanence-debug.js +197 -0
  124. package/tests/unit/test-data-permanence.js +340 -0
  125. package/tests/unit/test-key-persistence-fixed.js +148 -0
  126. package/tests/unit/test-key-persistence.js +172 -0
  127. package/tests/unit/test-relay-permanence.js +376 -0
  128. package/tests/unit/test-second-node.js +95 -0
  129. package/tests/unit/test-simple-write.js +89 -0
  130. package/vite.config.js +49 -0
  131. package/vitest.config.js +20 -0
  132. package/FEDERATION.md +0 -213
  133. package/compute.js +0 -298
  134. package/content.js +0 -1022
  135. package/federation.js +0 -1234
  136. package/global.js +0 -736
  137. package/hexlib.js +0 -335
  138. package/hologram.js +0 -183
  139. package/holosphere-bundle.esm.js +0 -34549
  140. package/holosphere-bundle.js +0 -34580
  141. package/holosphere-bundle.min.js +0 -49
  142. package/holosphere.d.ts +0 -604
  143. package/holosphere.js +0 -739
  144. package/node.js +0 -246
  145. package/schema.js +0 -139
  146. package/utils.js +0 -302
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Schema Extractor - Extract structured data from text using JSON schemas
3
+ */
4
+
5
+ /**
6
+ * Schema Extractor class
7
+ */
8
+ export class SchemaExtractor {
9
+ /**
10
+ * @param {LLMService} llmService - LLM service instance
11
+ */
12
+ constructor(llmService) {
13
+ this.llm = llmService;
14
+ }
15
+
16
+ /**
17
+ * Extract data from text based on JSON schema
18
+ * @param {string} text - Text to extract from
19
+ * @param {Object} schema - JSON schema defining expected structure
20
+ * @param {Object} options - Extraction options
21
+ * @returns {Promise<Object>} Extracted data conforming to schema
22
+ */
23
+ async extractToSchema(text, schema, options = {}) {
24
+ const schemaDescription = this._describeSchema(schema);
25
+
26
+ const systemPrompt = `You are a data extraction expert. Extract structured information from the provided text according to this JSON schema:
27
+
28
+ ${JSON.stringify(schema, null, 2)}
29
+
30
+ Schema field descriptions:
31
+ ${schemaDescription}
32
+
33
+ Rules:
34
+ 1. Extract ONLY information present in the text
35
+ 2. Use null for fields where information is not available
36
+ 3. Follow the exact schema structure and field types
37
+ 4. For arrays, include all relevant items found
38
+ 5. Return ONLY valid JSON matching the schema`;
39
+
40
+ const response = await this.llm.getJSON(systemPrompt, text, {
41
+ temperature: 0.2,
42
+ maxTokens: options.maxTokens || 2000
43
+ });
44
+
45
+ return response;
46
+ }
47
+
48
+ /**
49
+ * Generate a human-readable description of the schema
50
+ * @private
51
+ */
52
+ _describeSchema(schema, path = '') {
53
+ const descriptions = [];
54
+
55
+ if (schema.description) {
56
+ descriptions.push(`${path || 'root'}: ${schema.description}`);
57
+ }
58
+
59
+ if (schema.properties) {
60
+ for (const [key, prop] of Object.entries(schema.properties)) {
61
+ const fieldPath = path ? `${path}.${key}` : key;
62
+ const type = prop.type || 'any';
63
+ const required = schema.required?.includes(key) ? ' (required)' : '';
64
+ const desc = prop.description || '';
65
+
66
+ descriptions.push(`- ${fieldPath} [${type}]${required}: ${desc}`);
67
+
68
+ if (prop.type === 'object' && prop.properties) {
69
+ descriptions.push(this._describeSchema(prop, fieldPath));
70
+ }
71
+
72
+ if (prop.type === 'array' && prop.items) {
73
+ if (prop.items.properties) {
74
+ descriptions.push(this._describeSchema(prop.items, `${fieldPath}[]`));
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ return descriptions.join('\n');
81
+ }
82
+
83
+ /**
84
+ * Extract multiple items from text
85
+ * @param {string} text - Text containing multiple items
86
+ * @param {Object} itemSchema - Schema for each item
87
+ * @param {Object} options - Options
88
+ * @returns {Promise<Object[]>} Array of extracted items
89
+ */
90
+ async extractMultiple(text, itemSchema, options = {}) {
91
+ const systemPrompt = `You are a data extraction expert. Extract ALL items matching this schema from the text:
92
+
93
+ ${JSON.stringify(itemSchema, null, 2)}
94
+
95
+ Return a JSON array of all items found. If no items found, return [].
96
+ Return ONLY valid JSON array.`;
97
+
98
+ const response = await this.llm.getJSON(systemPrompt, text, {
99
+ temperature: 0.2,
100
+ maxTokens: options.maxTokens || 3000
101
+ });
102
+
103
+ return Array.isArray(response) ? response : [response];
104
+ }
105
+
106
+ /**
107
+ * Fill missing fields in existing data
108
+ * @param {Object} data - Existing data with some fields
109
+ * @param {string} text - Text to extract missing data from
110
+ * @param {Object} schema - Full schema
111
+ * @returns {Promise<Object>} Complete data
112
+ */
113
+ async fillMissing(data, text, schema) {
114
+ const missingFields = this._findMissingFields(data, schema);
115
+
116
+ if (missingFields.length === 0) {
117
+ return data;
118
+ }
119
+
120
+ const systemPrompt = `You have existing data:
121
+ ${JSON.stringify(data, null, 2)}
122
+
123
+ Extract ONLY the following missing fields from the text:
124
+ ${missingFields.join(', ')}
125
+
126
+ Based on this schema:
127
+ ${JSON.stringify(schema, null, 2)}
128
+
129
+ Return JSON with ONLY the missing fields that you can extract. Do not modify existing fields.`;
130
+
131
+ const extracted = await this.llm.getJSON(systemPrompt, text, { temperature: 0.2 });
132
+
133
+ return { ...data, ...extracted };
134
+ }
135
+
136
+ /**
137
+ * Find fields missing from data based on schema
138
+ * @private
139
+ */
140
+ _findMissingFields(data, schema, path = '') {
141
+ const missing = [];
142
+
143
+ if (schema.properties) {
144
+ for (const [key, prop] of Object.entries(schema.properties)) {
145
+ const fieldPath = path ? `${path}.${key}` : key;
146
+ const value = data[key];
147
+
148
+ if (value === undefined || value === null) {
149
+ missing.push(fieldPath);
150
+ } else if (prop.type === 'object' && prop.properties) {
151
+ missing.push(...this._findMissingFields(value, prop, fieldPath));
152
+ }
153
+ }
154
+ }
155
+
156
+ return missing;
157
+ }
158
+
159
+ /**
160
+ * Validate extracted data against schema
161
+ * @param {Object} data - Extracted data
162
+ * @param {Object} schema - JSON schema
163
+ * @returns {{valid: boolean, errors: string[]}}
164
+ */
165
+ validateExtraction(data, schema) {
166
+ const errors = [];
167
+
168
+ if (schema.required) {
169
+ for (const field of schema.required) {
170
+ if (data[field] === undefined || data[field] === null) {
171
+ errors.push(`Missing required field: ${field}`);
172
+ }
173
+ }
174
+ }
175
+
176
+ if (schema.properties) {
177
+ for (const [key, prop] of Object.entries(schema.properties)) {
178
+ const value = data[key];
179
+
180
+ if (value !== undefined && value !== null) {
181
+ const typeValid = this._validateType(value, prop.type);
182
+ if (!typeValid) {
183
+ errors.push(`Invalid type for ${key}: expected ${prop.type}, got ${typeof value}`);
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ return {
190
+ valid: errors.length === 0,
191
+ errors
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Validate value type
197
+ * @private
198
+ */
199
+ _validateType(value, type) {
200
+ switch (type) {
201
+ case 'string':
202
+ return typeof value === 'string';
203
+ case 'number':
204
+ case 'integer':
205
+ return typeof value === 'number';
206
+ case 'boolean':
207
+ return typeof value === 'boolean';
208
+ case 'array':
209
+ return Array.isArray(value);
210
+ case 'object':
211
+ return typeof value === 'object' && !Array.isArray(value);
212
+ default:
213
+ return true;
214
+ }
215
+ }
216
+ }
217
+
218
+ export default SchemaExtractor;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Spatial Analysis - AI-powered analysis of data patterns across geographic regions
3
+ */
4
+
5
+ /**
6
+ * Spatial Analysis class
7
+ */
8
+ export class SpatialAnalysis {
9
+ /**
10
+ * @param {LLMService} llmService - LLM service instance
11
+ * @param {Object} holosphere - HoloSphere instance
12
+ */
13
+ constructor(llmService, holosphere = null) {
14
+ this.llm = llmService;
15
+ this.holosphere = holosphere;
16
+ }
17
+
18
+ /**
19
+ * Set HoloSphere instance
20
+ * @param {Object} holosphere - HoloSphere instance
21
+ */
22
+ setHoloSphere(holosphere) {
23
+ this.holosphere = holosphere;
24
+ }
25
+
26
+ /**
27
+ * Analyze data patterns in a region
28
+ * @param {string} holon - Holon identifier
29
+ * @param {string} lens - Lens to analyze (optional, all if null)
30
+ * @param {string} aspect - Analysis aspect (activity, trends, distribution, etc.)
31
+ * @returns {Promise<Object>} Analysis results
32
+ */
33
+ async analyzeRegion(holon, lens = null, aspect = 'general') {
34
+ if (!this.holosphere) {
35
+ throw new Error('HoloSphere instance required');
36
+ }
37
+
38
+ // Get data from holon
39
+ let data = [];
40
+ if (lens) {
41
+ data = await this.holosphere.getAll(holon, lens);
42
+ } else {
43
+ // Get from all known lenses - would need lens list
44
+ data = await this.holosphere.getAll(holon, 'default');
45
+ }
46
+
47
+ const dataStr = JSON.stringify(data.slice(0, 50), null, 2); // Sample for analysis
48
+
49
+ const systemPrompt = `You are a geospatial data analyst. Analyze the following data from a geographic region.
50
+
51
+ Region (holon): ${holon}
52
+ Lens: ${lens || 'all'}
53
+ Analysis aspect: ${aspect}
54
+ Total items: ${data.length}
55
+
56
+ Provide insights about:
57
+ 1. Key patterns and trends
58
+ 2. Notable clusters or concentrations
59
+ 3. Anomalies or outliers
60
+ 4. Recommendations for the region
61
+
62
+ Return JSON:
63
+ {
64
+ "summary": "brief overview",
65
+ "patterns": ["pattern1", "pattern2"],
66
+ "insights": ["insight1", "insight2"],
67
+ "metrics": {"metric1": value},
68
+ "recommendations": ["rec1", "rec2"],
69
+ "confidence": 0.0-1.0
70
+ }`;
71
+
72
+ const analysis = await this.llm.getJSON(systemPrompt, dataStr, { temperature: 0.3 });
73
+
74
+ return {
75
+ holon,
76
+ lens,
77
+ aspect,
78
+ dataCount: data.length,
79
+ analysis,
80
+ timestamp: Date.now(),
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Compare data between two regions
86
+ * @param {string} holon1 - First holon
87
+ * @param {string} holon2 - Second holon
88
+ * @param {string} lens - Lens to compare
89
+ * @returns {Promise<Object>} Comparison results
90
+ */
91
+ async compareRegions(holon1, holon2, lens) {
92
+ if (!this.holosphere) {
93
+ throw new Error('HoloSphere instance required');
94
+ }
95
+
96
+ const data1 = await this.holosphere.getAll(holon1, lens);
97
+ const data2 = await this.holosphere.getAll(holon2, lens);
98
+
99
+ const systemPrompt = `You are a comparative analyst. Compare data between two geographic regions.
100
+
101
+ Region 1 (${holon1}): ${data1.length} items
102
+ Region 2 (${holon2}): ${data2.length} items
103
+ Lens: ${lens}
104
+
105
+ Analyze and compare:
106
+ 1. Similarities between regions
107
+ 2. Key differences
108
+ 3. Relative strengths of each region
109
+ 4. Opportunities for collaboration
110
+
111
+ Return JSON:
112
+ {
113
+ "summary": "brief comparison",
114
+ "similarities": ["sim1", "sim2"],
115
+ "differences": ["diff1", "diff2"],
116
+ "region1_strengths": ["strength1"],
117
+ "region2_strengths": ["strength1"],
118
+ "collaboration_opportunities": ["opp1"],
119
+ "metrics_comparison": {"metric": {"region1": val, "region2": val}}
120
+ }`;
121
+
122
+ const userMessage = `Region 1 data sample:
123
+ ${JSON.stringify(data1.slice(0, 25), null, 2)}
124
+
125
+ Region 2 data sample:
126
+ ${JSON.stringify(data2.slice(0, 25), null, 2)}`;
127
+
128
+ const comparison = await this.llm.getJSON(systemPrompt, userMessage, { temperature: 0.3 });
129
+
130
+ return {
131
+ regions: [holon1, holon2],
132
+ lens,
133
+ dataCounts: { [holon1]: data1.length, [holon2]: data2.length },
134
+ comparison,
135
+ timestamp: Date.now(),
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Identify spatial trends over time
141
+ * @param {string} holon - Holon identifier
142
+ * @param {string} lens - Lens to analyze
143
+ * @param {Object} timeRange - Time range {start, end}
144
+ * @returns {Promise<Object>} Trend analysis
145
+ */
146
+ async spatialTrends(holon, lens, timeRange = {}) {
147
+ if (!this.holosphere) {
148
+ throw new Error('HoloSphere instance required');
149
+ }
150
+
151
+ const data = await this.holosphere.getAll(holon, lens);
152
+
153
+ // Filter by time if timestamps available
154
+ let filteredData = data;
155
+ if (timeRange.start || timeRange.end) {
156
+ filteredData = data.filter(item => {
157
+ const ts = item.timestamp || item.created_at || item.date;
158
+ if (!ts) return true;
159
+
160
+ const itemTime = new Date(ts).getTime();
161
+ if (timeRange.start && itemTime < new Date(timeRange.start).getTime()) return false;
162
+ if (timeRange.end && itemTime > new Date(timeRange.end).getTime()) return false;
163
+ return true;
164
+ });
165
+ }
166
+
167
+ const systemPrompt = `You are a trend analyst. Analyze temporal patterns in geographic data.
168
+
169
+ Region: ${holon}
170
+ Lens: ${lens}
171
+ Time range: ${JSON.stringify(timeRange)}
172
+ Items analyzed: ${filteredData.length}
173
+
174
+ Identify:
175
+ 1. Growth/decline trends
176
+ 2. Seasonal patterns
177
+ 3. Emerging themes
178
+ 4. Predictions
179
+
180
+ Return JSON:
181
+ {
182
+ "trends": [{"trend": "description", "direction": "up|down|stable", "strength": 0.0-1.0}],
183
+ "patterns": ["pattern1"],
184
+ "emerging_themes": ["theme1"],
185
+ "predictions": ["pred1"],
186
+ "summary": "overview"
187
+ }`;
188
+
189
+ const trends = await this.llm.getJSON(
190
+ systemPrompt,
191
+ JSON.stringify(filteredData.slice(0, 50), null, 2),
192
+ { temperature: 0.3 }
193
+ );
194
+
195
+ return {
196
+ holon,
197
+ lens,
198
+ timeRange,
199
+ dataCount: filteredData.length,
200
+ trends,
201
+ timestamp: Date.now(),
202
+ };
203
+ }
204
+
205
+ /**
206
+ * Find hotspots in hierarchical data
207
+ * @param {string} holon - Starting holon
208
+ * @param {string} lens - Lens to analyze
209
+ * @param {string} metric - Metric to identify hotspots by
210
+ * @returns {Promise<Object>} Hotspot analysis
211
+ */
212
+ async findHotspots(holon, lens, metric = 'count') {
213
+ if (!this.holosphere) {
214
+ throw new Error('HoloSphere instance required');
215
+ }
216
+
217
+ // Get data from holon and children
218
+ const data = await this.holosphere.getAll(holon, lens);
219
+
220
+ const systemPrompt = `You are a spatial analyst. Identify hotspots (areas of high concentration/activity) in geographic data.
221
+
222
+ Region: ${holon}
223
+ Lens: ${lens}
224
+ Metric: ${metric}
225
+ Data points: ${data.length}
226
+
227
+ Identify:
228
+ 1. High-activity areas
229
+ 2. Emerging areas
230
+ 3. Underserved areas
231
+ 4. Recommended focus areas
232
+
233
+ Return JSON:
234
+ {
235
+ "hotspots": [{"location": "desc", "intensity": 0.0-1.0, "type": "high_activity|emerging|underserved"}],
236
+ "concentration_patterns": ["pattern1"],
237
+ "recommendations": ["rec1"],
238
+ "summary": "overview"
239
+ }`;
240
+
241
+ return this.llm.getJSON(
242
+ systemPrompt,
243
+ JSON.stringify(data.slice(0, 50), null, 2),
244
+ { temperature: 0.3 }
245
+ );
246
+ }
247
+
248
+ /**
249
+ * Generate spatial report for a region
250
+ * @param {string} holon - Holon identifier
251
+ * @param {Object} options - Report options
252
+ * @returns {Promise<string>} Formatted report
253
+ */
254
+ async generateReport(holon, options = {}) {
255
+ if (!this.holosphere) {
256
+ throw new Error('HoloSphere instance required');
257
+ }
258
+
259
+ const lenses = options.lenses || ['default'];
260
+ const allData = {};
261
+
262
+ for (const lens of lenses) {
263
+ allData[lens] = await this.holosphere.getAll(holon, lens);
264
+ }
265
+
266
+ const systemPrompt = `You are a regional analyst. Generate a comprehensive report for this geographic region.
267
+
268
+ Region: ${holon}
269
+ Data by lens: ${Object.entries(allData).map(([l, d]) => `${l}: ${d.length} items`).join(', ')}
270
+
271
+ Generate a professional report including:
272
+ 1. Executive Summary
273
+ 2. Key Statistics
274
+ 3. Notable Patterns
275
+ 4. Opportunities
276
+ 5. Recommendations
277
+
278
+ Format as markdown.`;
279
+
280
+ const dataSample = {};
281
+ for (const [lens, data] of Object.entries(allData)) {
282
+ dataSample[lens] = data.slice(0, 20);
283
+ }
284
+
285
+ return this.llm.sendMessage(
286
+ systemPrompt,
287
+ JSON.stringify(dataSample, null, 2),
288
+ { temperature: 0.5, maxTokens: 2000 }
289
+ );
290
+ }
291
+ }
292
+
293
+ export default SpatialAnalysis;
package/src/ai/tts.js ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Text-to-Speech - OpenAI TTS integration
3
+ */
4
+
5
+ /**
6
+ * Available voices for TTS
7
+ */
8
+ export const VOICES = {
9
+ ALLOY: 'alloy',
10
+ ECHO: 'echo',
11
+ FABLE: 'fable',
12
+ ONYX: 'onyx',
13
+ NOVA: 'nova',
14
+ SHIMMER: 'shimmer',
15
+ };
16
+
17
+ /**
18
+ * Available TTS models
19
+ */
20
+ export const MODELS = {
21
+ TTS_1: 'tts-1',
22
+ TTS_1_HD: 'tts-1-hd',
23
+ };
24
+
25
+ /**
26
+ * TTS class for text-to-speech conversion
27
+ */
28
+ export class TTS {
29
+ /**
30
+ * @param {Object} openai - OpenAI client instance
31
+ */
32
+ constructor(openai) {
33
+ this.openai = openai;
34
+ this.defaultVoice = VOICES.ECHO;
35
+ this.defaultModel = MODELS.TTS_1;
36
+ }
37
+
38
+ /**
39
+ * Convert text to speech
40
+ * @param {string} text - Text to convert
41
+ * @param {Object} options - TTS options
42
+ * @returns {Promise<Buffer>} Audio buffer (MP3)
43
+ */
44
+ async speak(text, options = {}) {
45
+ const {
46
+ voice = this.defaultVoice,
47
+ model = this.defaultModel,
48
+ speed = 1.0,
49
+ responseFormat = 'mp3',
50
+ } = options;
51
+
52
+ try {
53
+ const response = await this.openai.audio.speech.create({
54
+ model,
55
+ voice,
56
+ input: text,
57
+ speed,
58
+ response_format: responseFormat,
59
+ });
60
+
61
+ // Convert response to Buffer
62
+ const arrayBuffer = await response.arrayBuffer();
63
+ return Buffer.from(arrayBuffer);
64
+ } catch (error) {
65
+ throw new Error(`TTS failed: ${error.message}`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Convert text to speech and return as base64
71
+ * @param {string} text - Text to convert
72
+ * @param {Object} options - TTS options
73
+ * @returns {Promise<string>} Base64 encoded audio
74
+ */
75
+ async speakBase64(text, options = {}) {
76
+ const buffer = await this.speak(text, options);
77
+ return buffer.toString('base64');
78
+ }
79
+
80
+ /**
81
+ * Convert text to speech and return as data URL
82
+ * @param {string} text - Text to convert
83
+ * @param {Object} options - TTS options
84
+ * @returns {Promise<string>} Data URL for audio
85
+ */
86
+ async speakDataUrl(text, options = {}) {
87
+ const format = options.responseFormat || 'mp3';
88
+ const mimeType = format === 'mp3' ? 'audio/mpeg' : `audio/${format}`;
89
+ const base64 = await this.speakBase64(text, options);
90
+ return `data:${mimeType};base64,${base64}`;
91
+ }
92
+
93
+ /**
94
+ * Set default voice
95
+ * @param {string} voice - Voice name
96
+ */
97
+ setDefaultVoice(voice) {
98
+ if (!Object.values(VOICES).includes(voice)) {
99
+ throw new Error(`Invalid voice: ${voice}. Use one of: ${Object.values(VOICES).join(', ')}`);
100
+ }
101
+ this.defaultVoice = voice;
102
+ }
103
+
104
+ /**
105
+ * Set default model
106
+ * @param {string} model - Model name
107
+ */
108
+ setDefaultModel(model) {
109
+ if (!Object.values(MODELS).includes(model)) {
110
+ throw new Error(`Invalid model: ${model}. Use one of: ${Object.values(MODELS).join(', ')}`);
111
+ }
112
+ this.defaultModel = model;
113
+ }
114
+
115
+ /**
116
+ * Get available voices
117
+ * @returns {string[]} Available voice names
118
+ */
119
+ static getVoices() {
120
+ return Object.values(VOICES);
121
+ }
122
+
123
+ /**
124
+ * Get available models
125
+ * @returns {string[]} Available model names
126
+ */
127
+ static getModels() {
128
+ return Object.values(MODELS);
129
+ }
130
+
131
+ /**
132
+ * Estimate audio duration (approximate)
133
+ * @param {string} text - Text to estimate
134
+ * @param {number} speed - Speech speed (default: 1.0)
135
+ * @returns {number} Estimated duration in seconds
136
+ */
137
+ static estimateDuration(text, speed = 1.0) {
138
+ // Average speaking rate: ~150 words per minute
139
+ const words = text.split(/\s+/).length;
140
+ const minutes = words / 150;
141
+ return (minutes * 60) / speed;
142
+ }
143
+
144
+ /**
145
+ * Split text into chunks for long content
146
+ * @param {string} text - Long text
147
+ * @param {number} maxChars - Max characters per chunk (default: 4000)
148
+ * @returns {string[]} Text chunks
149
+ */
150
+ static splitText(text, maxChars = 4000) {
151
+ if (text.length <= maxChars) {
152
+ return [text];
153
+ }
154
+
155
+ const chunks = [];
156
+ const sentences = text.split(/(?<=[.!?])\s+/);
157
+ let currentChunk = '';
158
+
159
+ for (const sentence of sentences) {
160
+ if ((currentChunk + sentence).length > maxChars) {
161
+ if (currentChunk) {
162
+ chunks.push(currentChunk.trim());
163
+ }
164
+ currentChunk = sentence;
165
+ } else {
166
+ currentChunk += (currentChunk ? ' ' : '') + sentence;
167
+ }
168
+ }
169
+
170
+ if (currentChunk) {
171
+ chunks.push(currentChunk.trim());
172
+ }
173
+
174
+ return chunks;
175
+ }
176
+
177
+ /**
178
+ * Convert long text to speech (handles chunking)
179
+ * @param {string} text - Long text
180
+ * @param {Object} options - TTS options
181
+ * @returns {Promise<Buffer[]>} Array of audio buffers
182
+ */
183
+ async speakLong(text, options = {}) {
184
+ const chunks = TTS.splitText(text, options.maxChars || 4000);
185
+
186
+ const buffers = await Promise.all(
187
+ chunks.map(chunk => this.speak(chunk, options))
188
+ );
189
+
190
+ return buffers;
191
+ }
192
+ }
193
+
194
+ export default TTS;