project-graph-mcp 2.3.0 → 2.3.2
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/package.json +1 -3
- package/project-graph-mcp-2.3.0.tgz +0 -0
- package/src/network/web-server.js +1 -1
- package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
- package/vendor/symbiote-node/engine/Executor.js +371 -0
- package/vendor/symbiote-node/engine/Graph.js +314 -0
- package/vendor/symbiote-node/engine/GraphServer.js +353 -0
- package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
- package/vendor/symbiote-node/engine/History.js +83 -0
- package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
- package/vendor/symbiote-node/engine/Persistence.js +84 -0
- package/vendor/symbiote-node/engine/Registry.js +264 -0
- package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
- package/vendor/symbiote-node/engine/cli.js +404 -0
- package/vendor/symbiote-node/engine/index.js +56 -0
- package/vendor/symbiote-node/engine/nanoid.js +28 -0
- package/vendor/symbiote-node/engine/package.json +26 -0
- package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
- package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
- package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
- package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
- package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
- package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
- package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
- package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
- package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
- package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
- package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
- package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
- package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
- package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
- package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
- package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
- package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
- package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
- package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
- package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
- package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
- package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
- package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
- package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
- package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
- package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
- package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
- package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
- package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
- package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
- package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
- package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
- package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
- package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
- package/vendor/symbiote-node/engine/packs/video-pack.js +323 -0
- package/vendor/symbiote-node/package.json +2 -2
- package/web/app.js +6 -3
- package/web/components/canvas-graph.js +50 -11
- package/web/components/code-block.js +1 -1
- package/web/components/event-feed/MiniGraphWidget.js +105 -15
- package/web/components/follow-ribbon.js +134 -0
- package/web/follow-controller.js +241 -0
- package/web/panels/code-viewer.js +1 -1
- package/web/panels/dep-graph.js +21 -42
- package/web/style.css +6 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transform/effects-skeleton — Beat data → visual effects metadata
|
|
3
|
+
*
|
|
4
|
+
* Pure data transform: analyzes beat/energy arrays to produce
|
|
5
|
+
* structured metadata for driving visual effects (transitions,
|
|
6
|
+
* drops, fades, intensity zones).
|
|
7
|
+
*
|
|
8
|
+
* Input: beatData from ai/beat-detect handler
|
|
9
|
+
* Output: effects skeleton with zones, anchors, markers
|
|
10
|
+
*
|
|
11
|
+
* Ported from Mr-Computer/modules/ai-music-video/src/services/effects-skeleton.js
|
|
12
|
+
*
|
|
13
|
+
* @module agi-graph/packs/transform/effects-skeleton
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
type: 'transform/effects-skeleton',
|
|
18
|
+
category: 'transform',
|
|
19
|
+
icon: 'equalizer',
|
|
20
|
+
|
|
21
|
+
driver: {
|
|
22
|
+
description: 'Analyze beat data → intensity zones, drops, fades, transitions',
|
|
23
|
+
inputs: [
|
|
24
|
+
{ name: 'beatData', type: 'any' },
|
|
25
|
+
],
|
|
26
|
+
outputs: [
|
|
27
|
+
{ name: 'skeleton', type: 'any' },
|
|
28
|
+
{ name: 'error', type: 'string' },
|
|
29
|
+
],
|
|
30
|
+
params: {
|
|
31
|
+
energyPerSecond: { type: 'int', default: 10, description: 'Energy samples per second' },
|
|
32
|
+
fadeWindowSize: { type: 'number', default: 2.0, description: 'Fade detection window (seconds)' },
|
|
33
|
+
transitionThreshold: { type: 'number', default: 0.25, description: 'Intensity change threshold' },
|
|
34
|
+
dropThreshold: { type: 'number', default: 0.7, description: 'Drop peak threshold (0-1)' },
|
|
35
|
+
// annotate mode
|
|
36
|
+
segments: { type: 'any', default: null, description: 'Timeline segments to annotate with effects' },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
lifecycle: {
|
|
41
|
+
validate: (inputs) => {
|
|
42
|
+
if (!inputs.beatData) return false;
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
cacheKey: (inputs, params) => {
|
|
47
|
+
const bd = inputs.beatData;
|
|
48
|
+
return `effects:${bd.duration || 0}:${bd.tempo || 0}:${params.energyPerSecond}`;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
execute: async (inputs, params) => {
|
|
52
|
+
try {
|
|
53
|
+
const skeleton = generateEffectsSkeleton(inputs.beatData, params);
|
|
54
|
+
|
|
55
|
+
// If segments provided, annotate them with effects
|
|
56
|
+
if (params.segments) {
|
|
57
|
+
skeleton.annotatedSegments = annotateSegmentsWithEffects(params.segments, skeleton);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { skeleton, error: null };
|
|
61
|
+
} catch (err) {
|
|
62
|
+
return { skeleton: null, error: err.message };
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// --- Pure analysis functions (ported from effects-skeleton.js) ---
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detect intensity zones from energy data
|
|
72
|
+
* @param {number[]} energy - Energy values per time unit
|
|
73
|
+
* @param {number} eps - Samples per second
|
|
74
|
+
* @returns {Array<{start: number, end: number, level: string, avgEnergy: number}>}
|
|
75
|
+
*/
|
|
76
|
+
function detectIntensityZones(energy, eps) {
|
|
77
|
+
if (!energy || energy.length === 0) return [];
|
|
78
|
+
|
|
79
|
+
const sorted = [...energy].sort((a, b) => a - b);
|
|
80
|
+
const lowThreshold = sorted[Math.floor(sorted.length * 0.33)];
|
|
81
|
+
const highThreshold = sorted[Math.floor(sorted.length * 0.66)];
|
|
82
|
+
|
|
83
|
+
const zones = [];
|
|
84
|
+
let current = null;
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < energy.length; i++) {
|
|
87
|
+
const time = i / eps;
|
|
88
|
+
const e = energy[i];
|
|
89
|
+
const level = e <= lowThreshold ? 'low' : e >= highThreshold ? 'high' : 'medium';
|
|
90
|
+
|
|
91
|
+
if (!current || current.level !== level) {
|
|
92
|
+
if (current) {
|
|
93
|
+
current.end = time;
|
|
94
|
+
current.avgEnergy = current._sum / current._n;
|
|
95
|
+
delete current._sum;
|
|
96
|
+
delete current._n;
|
|
97
|
+
zones.push(current);
|
|
98
|
+
}
|
|
99
|
+
current = { start: time, end: time, level, _sum: e, _n: 1 };
|
|
100
|
+
} else {
|
|
101
|
+
current.end = time;
|
|
102
|
+
current._sum += e;
|
|
103
|
+
current._n++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (current) {
|
|
108
|
+
current.avgEnergy = current._sum / current._n;
|
|
109
|
+
delete current._sum;
|
|
110
|
+
delete current._n;
|
|
111
|
+
zones.push(current);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Merge short zones (< 1s)
|
|
115
|
+
const merged = [];
|
|
116
|
+
for (const zone of zones) {
|
|
117
|
+
if (zone.end - zone.start < 1.0 && merged.length > 0) {
|
|
118
|
+
merged[merged.length - 1].end = zone.end;
|
|
119
|
+
} else {
|
|
120
|
+
merged.push(zone);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return merged;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Find fade zones — where intensity decreases significantly
|
|
128
|
+
* @param {number[]} energy
|
|
129
|
+
* @param {number} eps
|
|
130
|
+
* @param {number} windowSize - Seconds
|
|
131
|
+
* @returns {Array<{start: number, end: number, fadeAmount: number}>}
|
|
132
|
+
*/
|
|
133
|
+
function detectFadeZones(energy, eps, windowSize) {
|
|
134
|
+
if (!energy || energy.length === 0) return [];
|
|
135
|
+
|
|
136
|
+
const ws = Math.floor(windowSize * eps);
|
|
137
|
+
const fades = [];
|
|
138
|
+
|
|
139
|
+
for (let i = ws; i < energy.length; i++) {
|
|
140
|
+
const prevAvg = energy.slice(i - ws, i).reduce((a, b) => a + b, 0) / ws;
|
|
141
|
+
const currEnd = Math.min(i + ws, energy.length);
|
|
142
|
+
const currAvg = energy.slice(i, currEnd).reduce((a, b) => a + b, 0) / (currEnd - i);
|
|
143
|
+
|
|
144
|
+
const fadeAmount = prevAvg - currAvg;
|
|
145
|
+
if (fadeAmount > 0.15) {
|
|
146
|
+
const time = i / eps;
|
|
147
|
+
const last = fades[fades.length - 1];
|
|
148
|
+
if (!last || time - last.end > 2.0) {
|
|
149
|
+
fades.push({
|
|
150
|
+
start: time - windowSize,
|
|
151
|
+
end: time + windowSize,
|
|
152
|
+
fadeAmount: Math.round(fadeAmount * 100) / 100,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return fades;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Detect transition anchors — significant intensity changes
|
|
162
|
+
* @param {number[]} energy
|
|
163
|
+
* @param {number} eps
|
|
164
|
+
* @param {number} threshold
|
|
165
|
+
* @returns {Array<{time: number, type: string, magnitude: number}>}
|
|
166
|
+
*/
|
|
167
|
+
function detectTransitionAnchors(energy, eps, threshold) {
|
|
168
|
+
if (!energy || energy.length === 0) return [];
|
|
169
|
+
|
|
170
|
+
const anchors = [];
|
|
171
|
+
for (let i = 1; i < energy.length; i++) {
|
|
172
|
+
const change = energy[i] - energy[i - 1];
|
|
173
|
+
if (Math.abs(change) > threshold) {
|
|
174
|
+
anchors.push({
|
|
175
|
+
time: i / eps,
|
|
176
|
+
type: change > 0 ? 'rise' : 'drop',
|
|
177
|
+
magnitude: Math.round(Math.abs(change) * 100) / 100,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Deduplicate within 1s
|
|
183
|
+
const deduped = [];
|
|
184
|
+
for (const a of anchors) {
|
|
185
|
+
const last = deduped[deduped.length - 1];
|
|
186
|
+
if (!last || a.time - last.time > 1.0) {
|
|
187
|
+
deduped.push(a);
|
|
188
|
+
} else if (a.magnitude > last.magnitude) {
|
|
189
|
+
deduped[deduped.length - 1] = a;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return deduped;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Detect drop points — high energy peaks
|
|
197
|
+
* @param {number[]} energy
|
|
198
|
+
* @param {number} eps
|
|
199
|
+
* @param {number[]} strongOnsets
|
|
200
|
+
* @param {number} threshold
|
|
201
|
+
* @returns {Array<{time: number, intensity: number, source: string}>}
|
|
202
|
+
*/
|
|
203
|
+
function detectDropPoints(energy, eps, strongOnsets, threshold) {
|
|
204
|
+
if (!energy || energy.length === 0) return [];
|
|
205
|
+
|
|
206
|
+
const drops = [];
|
|
207
|
+
const maxEnergy = Math.max(...energy);
|
|
208
|
+
|
|
209
|
+
for (let i = 1; i < energy.length - 1; i++) {
|
|
210
|
+
if (energy[i] > energy[i - 1] && energy[i] > energy[i + 1] && energy[i] > maxEnergy * threshold) {
|
|
211
|
+
drops.push({
|
|
212
|
+
time: i / eps,
|
|
213
|
+
intensity: Math.round((energy[i] / maxEnergy) * 100) / 100,
|
|
214
|
+
source: 'energy-peak',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const onset of strongOnsets) {
|
|
220
|
+
drops.push({ time: onset, intensity: 0.8, source: 'strong-onset' });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
drops.sort((a, b) => a.time - b.time);
|
|
224
|
+
|
|
225
|
+
const deduped = [];
|
|
226
|
+
for (const d of drops) {
|
|
227
|
+
const last = deduped[deduped.length - 1];
|
|
228
|
+
if (!last || d.time - last.time > 0.5) deduped.push(d);
|
|
229
|
+
else if (d.intensity > last.intensity) deduped[deduped.length - 1] = d;
|
|
230
|
+
}
|
|
231
|
+
return deduped;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get interpolated energy at a time point
|
|
236
|
+
* @param {number} time
|
|
237
|
+
* @param {number[]} energy
|
|
238
|
+
* @param {number} eps
|
|
239
|
+
* @returns {number}
|
|
240
|
+
*/
|
|
241
|
+
function getEnergyAtTime(time, energy, eps) {
|
|
242
|
+
if (!energy || energy.length === 0) return 0.5;
|
|
243
|
+
const idx = time * eps;
|
|
244
|
+
const lo = Math.floor(idx);
|
|
245
|
+
const hi = Math.ceil(idx);
|
|
246
|
+
if (lo < 0) return energy[0] || 0.5;
|
|
247
|
+
if (hi >= energy.length) return energy[energy.length - 1] || 0.5;
|
|
248
|
+
if (lo === hi) return energy[lo];
|
|
249
|
+
const f = idx - lo;
|
|
250
|
+
return energy[lo] * (1 - f) + energy[hi] * f;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Generate beat markers with per-beat energy
|
|
255
|
+
* @param {number[]} beats
|
|
256
|
+
* @param {number[]} energy
|
|
257
|
+
* @param {number} eps
|
|
258
|
+
* @returns {Array<{time: number, index: number, energy: number, isStrong: boolean, isDownbeat: boolean}>}
|
|
259
|
+
*/
|
|
260
|
+
function generateBeatMarkers(beats, energy, eps) {
|
|
261
|
+
if (!beats || beats.length === 0) return [];
|
|
262
|
+
|
|
263
|
+
const evals = beats.map(t => getEnergyAtTime(t, energy, eps));
|
|
264
|
+
const avg = evals.reduce((a, b) => a + b, 0) / evals.length;
|
|
265
|
+
const strongThreshold = avg * 1.3;
|
|
266
|
+
|
|
267
|
+
return beats.map((time, index) => {
|
|
268
|
+
const e = getEnergyAtTime(time, energy, eps);
|
|
269
|
+
return {
|
|
270
|
+
time,
|
|
271
|
+
index,
|
|
272
|
+
energy: Math.round(e * 1000) / 1000,
|
|
273
|
+
isStrong: e > strongThreshold,
|
|
274
|
+
isDownbeat: index % 4 === 0,
|
|
275
|
+
isOffbeat: index % 2 === 1,
|
|
276
|
+
};
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Detect high-resolution transitions using beat timestamps
|
|
282
|
+
* @param {number[]} beats
|
|
283
|
+
* @param {number[]} energy
|
|
284
|
+
* @param {number} eps
|
|
285
|
+
* @returns {Array<{time: number, type: string, magnitude: number, beatIndex: number}>}
|
|
286
|
+
*/
|
|
287
|
+
function detectHiResTransitions(beats, energy, eps) {
|
|
288
|
+
if (!beats || beats.length < 2) return [];
|
|
289
|
+
|
|
290
|
+
const transitions = [];
|
|
291
|
+
const windowBeats = 4;
|
|
292
|
+
const threshold = 0.2;
|
|
293
|
+
|
|
294
|
+
for (let i = windowBeats; i < beats.length; i++) {
|
|
295
|
+
let prevSum = 0;
|
|
296
|
+
for (let j = i - windowBeats; j < i; j++) {
|
|
297
|
+
prevSum += getEnergyAtTime(beats[j], energy, eps);
|
|
298
|
+
}
|
|
299
|
+
const prevAvg = prevSum / windowBeats;
|
|
300
|
+
|
|
301
|
+
let currSum = 0;
|
|
302
|
+
const end = Math.min(i + windowBeats, beats.length);
|
|
303
|
+
for (let j = i; j < end; j++) {
|
|
304
|
+
currSum += getEnergyAtTime(beats[j], energy, eps);
|
|
305
|
+
}
|
|
306
|
+
const currAvg = currSum / (end - i);
|
|
307
|
+
|
|
308
|
+
const change = currAvg - prevAvg;
|
|
309
|
+
if (Math.abs(change) > threshold) {
|
|
310
|
+
transitions.push({
|
|
311
|
+
time: beats[i],
|
|
312
|
+
beatIndex: i,
|
|
313
|
+
type: change > 0 ? 'rise' : 'drop',
|
|
314
|
+
magnitude: Math.round(Math.abs(change) * 100) / 100,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const deduped = [];
|
|
320
|
+
for (const t of transitions) {
|
|
321
|
+
const last = deduped[deduped.length - 1];
|
|
322
|
+
if (!last || t.beatIndex - last.beatIndex > 2) deduped.push(t);
|
|
323
|
+
else if (t.magnitude > last.magnitude) deduped[deduped.length - 1] = t;
|
|
324
|
+
}
|
|
325
|
+
return deduped;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Generate complete effects skeleton from beat data
|
|
330
|
+
* @param {Object} beatData - Data from ai/beat-detect
|
|
331
|
+
* @param {Object} params - Handler params
|
|
332
|
+
* @returns {Object} Effects skeleton
|
|
333
|
+
*/
|
|
334
|
+
function generateEffectsSkeleton(beatData, params) {
|
|
335
|
+
const {
|
|
336
|
+
energy = [],
|
|
337
|
+
peaks = [],
|
|
338
|
+
beats = [],
|
|
339
|
+
tempo = 120,
|
|
340
|
+
duration = 0,
|
|
341
|
+
quietZones = [],
|
|
342
|
+
strongOnsets = [],
|
|
343
|
+
} = beatData;
|
|
344
|
+
|
|
345
|
+
// Support both snake_case (librosa output) and camelCase
|
|
346
|
+
const rawQuietZones = beatData.quiet_zones || quietZones;
|
|
347
|
+
const rawStrongOnsets = beatData.strong_onsets || strongOnsets;
|
|
348
|
+
const rawEps = beatData.energy_per_second || params.energyPerSecond || 10;
|
|
349
|
+
const rawDownbeats = beatData.downbeats || [];
|
|
350
|
+
|
|
351
|
+
const eps = rawEps;
|
|
352
|
+
|
|
353
|
+
const intensityZones = detectIntensityZones(energy, eps);
|
|
354
|
+
const fadeZones = detectFadeZones(energy, eps, params.fadeWindowSize || 2.0);
|
|
355
|
+
const transitionAnchors = detectTransitionAnchors(energy, eps, params.transitionThreshold || 0.25);
|
|
356
|
+
const dropPoints = detectDropPoints(energy, eps, rawStrongOnsets, params.dropThreshold || 0.7);
|
|
357
|
+
const beatMarkers = generateBeatMarkers(beats, energy, eps);
|
|
358
|
+
const hiResTransitions = detectHiResTransitions(beats, energy, eps);
|
|
359
|
+
|
|
360
|
+
const normalizedQuietZones = rawQuietZones.map(z => ({
|
|
361
|
+
start: z.start,
|
|
362
|
+
end: z.end || (z.start + (z.duration || 0)),
|
|
363
|
+
duration: z.duration || (z.end ? z.end - z.start : 0),
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
const downbeatAnchors = rawDownbeats.map(t => ({ time: t, type: 'downbeat' }));
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
metadata: {
|
|
370
|
+
duration,
|
|
371
|
+
tempo,
|
|
372
|
+
totalEnergySamples: energy.length,
|
|
373
|
+
energyPerSecond: eps,
|
|
374
|
+
totalBeats: beats.length,
|
|
375
|
+
resolution: 'millisecond',
|
|
376
|
+
},
|
|
377
|
+
intensityZones,
|
|
378
|
+
fadeZones,
|
|
379
|
+
transitionAnchors,
|
|
380
|
+
hiResTransitions,
|
|
381
|
+
dropPoints,
|
|
382
|
+
quietZones: normalizedQuietZones,
|
|
383
|
+
downbeatAnchors,
|
|
384
|
+
beatMarkers,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Annotate timeline segments with effects metadata
|
|
390
|
+
* @param {Array} segments - Timeline segments [{start, end, ...}]
|
|
391
|
+
* @param {Object} skeleton - Effects skeleton
|
|
392
|
+
* @returns {Array} Segments with .effects field
|
|
393
|
+
*/
|
|
394
|
+
function annotateSegmentsWithEffects(segments, skeleton) {
|
|
395
|
+
return segments.map(seg => {
|
|
396
|
+
const { start, end } = seg;
|
|
397
|
+
const mid = (start + end) / 2;
|
|
398
|
+
|
|
399
|
+
const zone = skeleton.intensityZones.find(z => z.start <= mid && z.end >= mid);
|
|
400
|
+
const inFade = skeleton.fadeZones.some(z => z.start <= end && z.end >= start);
|
|
401
|
+
const drops = skeleton.dropPoints.filter(d => d.time >= start && d.time <= end);
|
|
402
|
+
const trans = skeleton.transitionAnchors.filter(a => a.time >= start && a.time <= end);
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
...seg,
|
|
406
|
+
effects: {
|
|
407
|
+
intensity: zone?.level || 'medium',
|
|
408
|
+
avgEnergy: zone?.avgEnergy || 0.5,
|
|
409
|
+
isFadeCandidate: inFade,
|
|
410
|
+
hasDrops: drops.length > 0,
|
|
411
|
+
dropCount: drops.length,
|
|
412
|
+
hasTransition: trans.length > 0,
|
|
413
|
+
transitionType: trans[0]?.type || null,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transform/json-parse — Parse JSON string to object
|
|
3
|
+
*
|
|
4
|
+
* Safely parses JSON input. Returns error instead of throwing.
|
|
5
|
+
*
|
|
6
|
+
* @module agi-graph/packs/transform/json-parse
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
type: 'transform/json-parse',
|
|
11
|
+
category: 'transform',
|
|
12
|
+
icon: 'data_object',
|
|
13
|
+
|
|
14
|
+
driver: {
|
|
15
|
+
description: 'Parse JSON string to object',
|
|
16
|
+
inputs: [
|
|
17
|
+
{ name: 'input', type: 'string' },
|
|
18
|
+
],
|
|
19
|
+
outputs: [
|
|
20
|
+
{ name: 'result', type: 'any' },
|
|
21
|
+
{ name: 'error', type: 'string' },
|
|
22
|
+
],
|
|
23
|
+
params: {},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
lifecycle: {
|
|
27
|
+
validate: (inputs) => {
|
|
28
|
+
if (inputs.input === undefined || inputs.input === null) return false;
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
cacheKey: (inputs) => `json:${inputs.input}`,
|
|
33
|
+
|
|
34
|
+
execute: async (inputs) => {
|
|
35
|
+
try {
|
|
36
|
+
const result = JSON.parse(inputs.input);
|
|
37
|
+
return { result, error: null };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { result: null, error: err.message };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|