project-graph-mcp 2.3.1 → 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 -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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transform/template — String template interpolation
|
|
3
|
+
*
|
|
4
|
+
* Replaces {{variable}} placeholders in template string with values from data object.
|
|
5
|
+
* Supports nested access via dot notation: {{user.name}}.
|
|
6
|
+
*
|
|
7
|
+
* @module symbiote-node/packs/transform/template
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
type: 'transform/template',
|
|
12
|
+
category: 'transform',
|
|
13
|
+
icon: 'text_snippet',
|
|
14
|
+
|
|
15
|
+
driver: {
|
|
16
|
+
description: 'Template interpolation — replace {{var}} with data values',
|
|
17
|
+
inputs: [
|
|
18
|
+
{ name: 'template', type: 'string' },
|
|
19
|
+
{ name: 'data', type: 'any' },
|
|
20
|
+
],
|
|
21
|
+
outputs: [
|
|
22
|
+
{ name: 'result', type: 'string' },
|
|
23
|
+
{ name: 'data', type: 'any' },
|
|
24
|
+
],
|
|
25
|
+
params: {
|
|
26
|
+
template: { type: 'textarea', default: '', description: 'Message template ({{var}} syntax)' },
|
|
27
|
+
replyMarkup: { type: 'textarea', default: '', description: 'Inline keyboard JSON (Telegram reply_markup)' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
lifecycle: {
|
|
32
|
+
// No validate: template comes from params.template or inputs.template
|
|
33
|
+
// Execute handles both cases
|
|
34
|
+
|
|
35
|
+
cacheKey: (inputs) =>
|
|
36
|
+
`tpl:${inputs.template}:${JSON.stringify(inputs.data)}`,
|
|
37
|
+
|
|
38
|
+
execute: async (inputs, params) => {
|
|
39
|
+
const template = params?.template || inputs.template;
|
|
40
|
+
const { data } = inputs;
|
|
41
|
+
|
|
42
|
+
const result = template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
43
|
+
const trimmed = key.trim();
|
|
44
|
+
// Support dot notation: {{user.name}}
|
|
45
|
+
const value = trimmed.split('.').reduce((obj, k) => {
|
|
46
|
+
if (obj === null || obj === undefined) return undefined;
|
|
47
|
+
return obj[k];
|
|
48
|
+
}, data);
|
|
49
|
+
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
console.warn(`[template] ⚠️ Missing variable "${trimmed}" in data keys: [${data ? Object.keys(data).join(', ') : 'NO DATA'}]`);
|
|
52
|
+
return match;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
55
|
+
return String(value);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Output rendered text in both formats:
|
|
59
|
+
// - result: raw string (for chaining)
|
|
60
|
+
// - data: full context with text field (for telegram/chat)
|
|
61
|
+
const outputField = params?.outputField || 'text';
|
|
62
|
+
const outputData = { ...(typeof data === 'object' ? data : {}), [outputField]: result };
|
|
63
|
+
|
|
64
|
+
// Attach inline keyboard if configured
|
|
65
|
+
if (params?.replyMarkup) {
|
|
66
|
+
try {
|
|
67
|
+
outputData.reply_markup = JSON.parse(params.replyMarkup);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.warn('[template] ⚠️ Invalid replyMarkup JSON:', e.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
result,
|
|
75
|
+
data: outputData,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transform/timeline-build — Whisper + beats → timeline segments
|
|
3
|
+
*
|
|
4
|
+
* Combines word timestamps from ai/whisper with beat data from ai/beat-detect
|
|
5
|
+
* to produce a continuous timeline of segments with 100% coverage.
|
|
6
|
+
*
|
|
7
|
+
* Core logic:
|
|
8
|
+
* 1. Build phrases from whisper words (punctuation-based splitting)
|
|
9
|
+
* 2. Fill gaps between phrases with beat-snapped segments
|
|
10
|
+
* 3. Enforce minimum/maximum segment duration (merge/split)
|
|
11
|
+
* 4. Calculate coverage statistics
|
|
12
|
+
*
|
|
13
|
+
* Simplified port of TimelineGenerator from
|
|
14
|
+
* Mr-Computer/modules/ai-music-video/src/services/timeline-generator.js
|
|
15
|
+
*
|
|
16
|
+
* @module agi-graph/packs/transform/timeline-build
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
type: 'transform/timeline-build',
|
|
21
|
+
category: 'transform',
|
|
22
|
+
icon: 'view_timeline',
|
|
23
|
+
|
|
24
|
+
driver: {
|
|
25
|
+
description: 'Whisper words + beat data → timeline segments with 100% coverage',
|
|
26
|
+
inputs: [
|
|
27
|
+
{ name: 'whisperData', type: 'any' },
|
|
28
|
+
{ name: 'beatData', type: 'any' },
|
|
29
|
+
],
|
|
30
|
+
outputs: [
|
|
31
|
+
{ name: 'segments', type: 'any' },
|
|
32
|
+
{ name: 'stats', type: 'any' },
|
|
33
|
+
{ name: 'error', type: 'string' },
|
|
34
|
+
],
|
|
35
|
+
params: {
|
|
36
|
+
minSegmentDuration: { type: 'number', default: 1.8, description: 'Min segment duration (seconds)' },
|
|
37
|
+
maxSegmentDuration: { type: 'number', default: 5.0, description: 'Max segment duration (seconds)' },
|
|
38
|
+
shortMergeThreshold: { type: 'number', default: 1.2, description: 'Merge lyrics segments shorter than this' },
|
|
39
|
+
gapType: { type: 'string', default: 'beat', description: 'Type label for gap-fill segments' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
lifecycle: {
|
|
44
|
+
validate: (inputs) => {
|
|
45
|
+
if (!inputs.whisperData) return false;
|
|
46
|
+
return true;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
cacheKey: (inputs, params) => {
|
|
50
|
+
const wd = inputs.whisperData;
|
|
51
|
+
const bd = inputs.beatData;
|
|
52
|
+
return `timeline:${wd.duration || 0}:${bd?.tempo || 0}:${params.minSegmentDuration}`;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
execute: async (inputs, params) => {
|
|
56
|
+
try {
|
|
57
|
+
const { whisperData, beatData } = inputs;
|
|
58
|
+
const words = whisperData.words || [];
|
|
59
|
+
const duration = whisperData.duration || beatData?.duration || 0;
|
|
60
|
+
const beats = beatData?.beats || [];
|
|
61
|
+
|
|
62
|
+
if (words.length === 0) {
|
|
63
|
+
return { segments: null, stats: null, error: 'No whisper words provided' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Step 1: Build phrases from words
|
|
67
|
+
let segments = buildPhrases(words);
|
|
68
|
+
|
|
69
|
+
// Step 2: Fill gaps with beat-snapped segments
|
|
70
|
+
segments = fillGaps(segments, beats, duration, params);
|
|
71
|
+
|
|
72
|
+
// Step 3: Remove overlaps
|
|
73
|
+
segments = removeOverlaps(segments);
|
|
74
|
+
|
|
75
|
+
// Step 4: Merge short segments
|
|
76
|
+
segments = mergeShort(segments, params.shortMergeThreshold || 1.2);
|
|
77
|
+
|
|
78
|
+
// Step 5: Enforce min duration
|
|
79
|
+
segments = enforceMinDuration(segments, params.minSegmentDuration || 1.8);
|
|
80
|
+
|
|
81
|
+
// Step 6: Cap max duration
|
|
82
|
+
segments = capMaxDuration(segments, params.maxSegmentDuration || 5.0);
|
|
83
|
+
|
|
84
|
+
// Sort final
|
|
85
|
+
segments.sort((a, b) => a.start - b.start);
|
|
86
|
+
|
|
87
|
+
// Stats
|
|
88
|
+
const stats = calculateStats(segments, duration);
|
|
89
|
+
|
|
90
|
+
return { segments, stats, error: null };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return { segments: null, stats: null, error: err.message };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build phrases from whisper words using punctuation-based splitting
|
|
100
|
+
* @param {Array<{word: string, start: number, end: number}>} words
|
|
101
|
+
* @returns {Array<{start: number, end: number, text: string, type: string, wordCount: number}>}
|
|
102
|
+
*/
|
|
103
|
+
function buildPhrases(words) {
|
|
104
|
+
const phrases = [];
|
|
105
|
+
let current = null;
|
|
106
|
+
|
|
107
|
+
for (const w of words) {
|
|
108
|
+
if (!current) {
|
|
109
|
+
current = {
|
|
110
|
+
start: w.start,
|
|
111
|
+
end: w.end,
|
|
112
|
+
words: [w.word],
|
|
113
|
+
type: 'lyrics',
|
|
114
|
+
};
|
|
115
|
+
} else {
|
|
116
|
+
current.end = w.end;
|
|
117
|
+
current.words.push(w.word);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Split on sentence endings, commas, or long pauses
|
|
121
|
+
const endsWithPunct = /[.!?;]$/.test(w.word);
|
|
122
|
+
const nextWord = words[words.indexOf(w) + 1];
|
|
123
|
+
const hasGap = nextWord && (nextWord.start - w.end > 0.8);
|
|
124
|
+
|
|
125
|
+
if (endsWithPunct || hasGap || current.words.length >= 12) {
|
|
126
|
+
phrases.push({
|
|
127
|
+
start: current.start,
|
|
128
|
+
end: current.end,
|
|
129
|
+
text: current.words.join(' '),
|
|
130
|
+
type: 'lyrics',
|
|
131
|
+
wordCount: current.words.length,
|
|
132
|
+
});
|
|
133
|
+
current = null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Close last phrase
|
|
138
|
+
if (current && current.words.length > 0) {
|
|
139
|
+
phrases.push({
|
|
140
|
+
start: current.start,
|
|
141
|
+
end: current.end,
|
|
142
|
+
text: current.words.join(' '),
|
|
143
|
+
type: 'lyrics',
|
|
144
|
+
wordCount: current.words.length,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return phrases;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Snap a time to the nearest beat
|
|
153
|
+
* @param {number} time - Seconds
|
|
154
|
+
* @param {number[]} beats - Beat timestamps
|
|
155
|
+
* @returns {number} Snapped time
|
|
156
|
+
*/
|
|
157
|
+
function snapToBeat(time, beats) {
|
|
158
|
+
if (!beats || beats.length === 0) return time;
|
|
159
|
+
|
|
160
|
+
let closest = beats[0];
|
|
161
|
+
let minDist = Math.abs(beats[0] - time);
|
|
162
|
+
|
|
163
|
+
for (const beat of beats) {
|
|
164
|
+
const dist = Math.abs(beat - time);
|
|
165
|
+
if (dist < minDist) {
|
|
166
|
+
minDist = dist;
|
|
167
|
+
closest = beat;
|
|
168
|
+
}
|
|
169
|
+
if (beat > time + minDist) break;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Only snap if within 0.3s of a beat
|
|
173
|
+
return minDist < 0.3 ? closest : time;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Fill gaps between segments with beat-snapped segments
|
|
178
|
+
* @param {Array} segments
|
|
179
|
+
* @param {number[]} beats
|
|
180
|
+
* @param {number} duration
|
|
181
|
+
* @param {Object} params
|
|
182
|
+
* @returns {Array}
|
|
183
|
+
*/
|
|
184
|
+
function fillGaps(segments, beats, duration, params) {
|
|
185
|
+
if (segments.length === 0) return segments;
|
|
186
|
+
|
|
187
|
+
const result = [];
|
|
188
|
+
const gapType = params.gapType || 'beat';
|
|
189
|
+
|
|
190
|
+
// Gap at start?
|
|
191
|
+
if (segments[0].start > 0.1) {
|
|
192
|
+
result.push({
|
|
193
|
+
start: 0,
|
|
194
|
+
end: snapToBeat(segments[0].start, beats),
|
|
195
|
+
text: '',
|
|
196
|
+
type: gapType,
|
|
197
|
+
wordCount: 0,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < segments.length; i++) {
|
|
202
|
+
result.push(segments[i]);
|
|
203
|
+
|
|
204
|
+
// Gap to next segment?
|
|
205
|
+
const next = segments[i + 1];
|
|
206
|
+
if (next) {
|
|
207
|
+
const gapStart = segments[i].end;
|
|
208
|
+
const gapEnd = next.start;
|
|
209
|
+
const gapSize = gapEnd - gapStart;
|
|
210
|
+
|
|
211
|
+
if (gapSize > 0.2) {
|
|
212
|
+
result.push({
|
|
213
|
+
start: snapToBeat(gapStart, beats),
|
|
214
|
+
end: snapToBeat(gapEnd, beats),
|
|
215
|
+
text: '',
|
|
216
|
+
type: gapType,
|
|
217
|
+
wordCount: 0,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Gap at end?
|
|
224
|
+
const lastEnd = segments[segments.length - 1].end;
|
|
225
|
+
if (duration > 0 && duration - lastEnd > 0.2) {
|
|
226
|
+
result.push({
|
|
227
|
+
start: snapToBeat(lastEnd, beats),
|
|
228
|
+
end: duration,
|
|
229
|
+
text: '',
|
|
230
|
+
type: gapType,
|
|
231
|
+
wordCount: 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Remove overlapping segments (trim shorter one)
|
|
240
|
+
* @param {Array} segments
|
|
241
|
+
* @returns {Array}
|
|
242
|
+
*/
|
|
243
|
+
function removeOverlaps(segments) {
|
|
244
|
+
if (segments.length < 2) return segments;
|
|
245
|
+
|
|
246
|
+
segments.sort((a, b) => a.start - b.start);
|
|
247
|
+
|
|
248
|
+
for (let i = 1; i < segments.length; i++) {
|
|
249
|
+
const prev = segments[i - 1];
|
|
250
|
+
const curr = segments[i];
|
|
251
|
+
|
|
252
|
+
if (curr.start < prev.end) {
|
|
253
|
+
// Overlap: trim the gap segment, or split at midpoint
|
|
254
|
+
if (prev.type !== 'lyrics' && curr.type === 'lyrics') {
|
|
255
|
+
prev.end = curr.start;
|
|
256
|
+
} else if (prev.type === 'lyrics' && curr.type !== 'lyrics') {
|
|
257
|
+
curr.start = prev.end;
|
|
258
|
+
} else {
|
|
259
|
+
const mid = (prev.end + curr.start) / 2;
|
|
260
|
+
prev.end = mid;
|
|
261
|
+
curr.start = mid;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Remove zero/negative duration segments
|
|
267
|
+
return segments.filter(s => s.end - s.start > 0.05);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Merge short lyrics segments into neighbors
|
|
272
|
+
* @param {Array} segments
|
|
273
|
+
* @param {number} threshold
|
|
274
|
+
* @returns {Array}
|
|
275
|
+
*/
|
|
276
|
+
function mergeShort(segments, threshold) {
|
|
277
|
+
if (segments.length < 2) return segments;
|
|
278
|
+
|
|
279
|
+
const result = [segments[0]];
|
|
280
|
+
|
|
281
|
+
for (let i = 1; i < segments.length; i++) {
|
|
282
|
+
const curr = segments[i];
|
|
283
|
+
const prev = result[result.length - 1];
|
|
284
|
+
const currDuration = curr.end - curr.start;
|
|
285
|
+
|
|
286
|
+
// Merge short lyrics into previous
|
|
287
|
+
if (curr.type === 'lyrics' && currDuration < threshold && prev.type === 'lyrics') {
|
|
288
|
+
prev.end = curr.end;
|
|
289
|
+
prev.text = prev.text + ' ' + curr.text;
|
|
290
|
+
prev.wordCount = (prev.wordCount || 0) + (curr.wordCount || 0);
|
|
291
|
+
} else {
|
|
292
|
+
result.push(curr);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Enforce minimum duration by merging
|
|
301
|
+
* @param {Array} segments
|
|
302
|
+
* @param {number} minDuration
|
|
303
|
+
* @returns {Array}
|
|
304
|
+
*/
|
|
305
|
+
function enforceMinDuration(segments, minDuration) {
|
|
306
|
+
if (segments.length < 2) return segments;
|
|
307
|
+
|
|
308
|
+
const result = [segments[0]];
|
|
309
|
+
|
|
310
|
+
for (let i = 1; i < segments.length; i++) {
|
|
311
|
+
const prev = result[result.length - 1];
|
|
312
|
+
const prevDuration = prev.end - prev.start;
|
|
313
|
+
|
|
314
|
+
if (prevDuration < minDuration) {
|
|
315
|
+
// Extend previous to absorb current
|
|
316
|
+
prev.end = segments[i].end;
|
|
317
|
+
if (segments[i].text) {
|
|
318
|
+
prev.text = (prev.text ? prev.text + ' ' : '') + segments[i].text;
|
|
319
|
+
}
|
|
320
|
+
if (segments[i].type === 'lyrics') prev.type = 'lyrics';
|
|
321
|
+
} else {
|
|
322
|
+
result.push(segments[i]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Cap segments at max duration by splitting evenly
|
|
331
|
+
* @param {Array} segments
|
|
332
|
+
* @param {number} maxDuration
|
|
333
|
+
* @returns {Array}
|
|
334
|
+
*/
|
|
335
|
+
function capMaxDuration(segments, maxDuration) {
|
|
336
|
+
const result = [];
|
|
337
|
+
|
|
338
|
+
for (const seg of segments) {
|
|
339
|
+
const duration = seg.end - seg.start;
|
|
340
|
+
|
|
341
|
+
if (duration <= maxDuration) {
|
|
342
|
+
result.push(seg);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Split evenly
|
|
347
|
+
const parts = Math.ceil(duration / maxDuration);
|
|
348
|
+
const partDuration = duration / parts;
|
|
349
|
+
|
|
350
|
+
for (let i = 0; i < parts; i++) {
|
|
351
|
+
result.push({
|
|
352
|
+
...seg,
|
|
353
|
+
start: seg.start + i * partDuration,
|
|
354
|
+
end: seg.start + (i + 1) * partDuration,
|
|
355
|
+
text: i === 0 ? seg.text : '',
|
|
356
|
+
_splitPart: i + 1,
|
|
357
|
+
_splitTotal: parts,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return result;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Calculate timeline coverage statistics
|
|
367
|
+
* @param {Array} segments
|
|
368
|
+
* @param {number} audioDuration
|
|
369
|
+
* @returns {Object}
|
|
370
|
+
*/
|
|
371
|
+
function calculateStats(segments, audioDuration) {
|
|
372
|
+
const totalSegments = segments.length;
|
|
373
|
+
const lyricsSegments = segments.filter(s => s.type === 'lyrics').length;
|
|
374
|
+
const gapSegments = totalSegments - lyricsSegments;
|
|
375
|
+
|
|
376
|
+
const coveredDuration = segments.reduce((sum, s) => sum + (s.end - s.start), 0);
|
|
377
|
+
const lyricsDuration = segments
|
|
378
|
+
.filter(s => s.type === 'lyrics')
|
|
379
|
+
.reduce((sum, s) => sum + (s.end - s.start), 0);
|
|
380
|
+
|
|
381
|
+
const coverage = audioDuration > 0
|
|
382
|
+
? Math.round((coveredDuration / audioDuration) * 100)
|
|
383
|
+
: 0;
|
|
384
|
+
|
|
385
|
+
const avgDuration = totalSegments > 0
|
|
386
|
+
? Math.round((coveredDuration / totalSegments) * 100) / 100
|
|
387
|
+
: 0;
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
totalSegments,
|
|
391
|
+
lyricsSegments,
|
|
392
|
+
gapSegments,
|
|
393
|
+
coveredDuration: Math.round(coveredDuration * 100) / 100,
|
|
394
|
+
audioDuration: Math.round(audioDuration * 100) / 100,
|
|
395
|
+
coverage,
|
|
396
|
+
lyricsDuration: Math.round(lyricsDuration * 100) / 100,
|
|
397
|
+
avgDuration,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/delay — Pause pipeline execution
|
|
3
|
+
*
|
|
4
|
+
* Waits for specified milliseconds, then passes input value through.
|
|
5
|
+
* Useful for rate limiting, animation timing, and API cooldowns.
|
|
6
|
+
*
|
|
7
|
+
* @module agi-graph/packs/util/delay
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
type: 'util/delay',
|
|
12
|
+
category: 'util',
|
|
13
|
+
icon: 'hourglass_empty',
|
|
14
|
+
|
|
15
|
+
driver: {
|
|
16
|
+
description: 'Pause execution for N milliseconds, pass value through',
|
|
17
|
+
inputs: [
|
|
18
|
+
{ name: 'value', type: 'any' },
|
|
19
|
+
],
|
|
20
|
+
outputs: [
|
|
21
|
+
{ name: 'value', type: 'any' },
|
|
22
|
+
],
|
|
23
|
+
params: {
|
|
24
|
+
ms: { type: 'int', default: 1000, description: 'Delay in milliseconds' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
lifecycle: {
|
|
29
|
+
validate: () => true,
|
|
30
|
+
|
|
31
|
+
// Never cache delays
|
|
32
|
+
cacheKey: null,
|
|
33
|
+
|
|
34
|
+
execute: async (inputs, params) => {
|
|
35
|
+
await new Promise(resolve => setTimeout(resolve, params.ms || 1000));
|
|
36
|
+
return { value: inputs.value };
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util/log — Console logger passthrough
|
|
3
|
+
*
|
|
4
|
+
* Logs input value to console and passes it through unchanged.
|
|
5
|
+
* Useful for debugging pipelines.
|
|
6
|
+
*
|
|
7
|
+
* @module symbiote-node/packs/util/log */
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
type: 'util/log',
|
|
11
|
+
category: 'util',
|
|
12
|
+
icon: 'terminal',
|
|
13
|
+
|
|
14
|
+
driver: {
|
|
15
|
+
description: 'Log value to console and pass through',
|
|
16
|
+
inputs: [
|
|
17
|
+
{ name: 'value', type: 'any' },
|
|
18
|
+
],
|
|
19
|
+
outputs: [
|
|
20
|
+
{ name: 'value', type: 'any' },
|
|
21
|
+
],
|
|
22
|
+
params: {
|
|
23
|
+
label: { type: 'string', default: '', description: 'Log label prefix' },
|
|
24
|
+
level: { type: 'string', default: 'info', description: 'Log level: log | info | warn | error' },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
lifecycle: {
|
|
29
|
+
validate: () => true,
|
|
30
|
+
cacheKey: null,
|
|
31
|
+
|
|
32
|
+
execute: async (inputs, params) => {
|
|
33
|
+
const label = params.label ? `[${params.label}]` : '[symbiote-node]'; const method = params.level || 'info';
|
|
34
|
+
|
|
35
|
+
const logFn = console[method] || console.log;
|
|
36
|
+
logFn(label, typeof inputs.value === 'object'
|
|
37
|
+
? JSON.stringify(inputs.value, null, 2)
|
|
38
|
+
: inputs.value
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return { value: inputs.value };
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|