shmakk 1.2.0 → 1.2.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/README.md +68 -2
- package/package.json +2 -2
- package/scripts/demo/record.py +196 -0
- package/scripts/demo/scenes.html +913 -0
- package/skills/media-video-compose.md +320 -0
- package/skills/media-video-script.md +204 -0
- package/skills/media-video-voice.md +184 -0
- package/src/agent-overview.js +320 -0
- package/src/agent-roster.js +53 -0
- package/src/agent.js +178 -18
- package/src/cli.js +220 -86
- package/src/completions.js +3 -1
- package/src/correction.js +11 -4
- package/src/endpoints.js +94 -31
- package/src/guard.js +101 -0
- package/src/index.js +19 -5
- package/src/llm.js +462 -52
- package/src/markdown.js +217 -0
- package/src/notify.js +34 -0
- package/src/pty.js +1 -1
- package/src/review.js +8 -1
- package/src/self-commands.js +108 -2
- package/src/session.js +58 -2
- package/src/ssh.js +255 -0
- package/src/subagent.js +12 -1
- package/src/taskClassifier.js +2 -2
- package/src/team.js +22 -0
- package/src/tools.js +487 -1
- package/src/workflows.js +32 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-voice
|
|
3
|
+
description: Generate per-segment voice-over audio using the tts_generate tool. Takes a storyboard JSON from the script agent and produces a WAV audio file for each segment. Part of the video production pipeline.
|
|
4
|
+
category: media
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Video Voice-Over
|
|
8
|
+
|
|
9
|
+
Generate spoken audio for each segment of a video storyboard. This agent receives the script agent's JSON output and produces individual WAV files — one per segment — that the compositor will synchronize with visuals.
|
|
10
|
+
|
|
11
|
+
## When to use
|
|
12
|
+
|
|
13
|
+
- You receive a storyboard JSON with `segments` containing `narration` fields
|
|
14
|
+
- You are the voice agent in the video production pipeline, running in parallel with the visual agent
|
|
15
|
+
- The user explicitly asks to generate voice-over audio for a video script
|
|
16
|
+
|
|
17
|
+
## When not to use
|
|
18
|
+
|
|
19
|
+
- The user wants a single TTS clip outside the video pipeline (use `tts_generate` directly)
|
|
20
|
+
- There is no storyboard — wait for the script agent to finish first
|
|
21
|
+
- All narration fields are empty strings (music-only video — skip voice generation)
|
|
22
|
+
|
|
23
|
+
## Input format
|
|
24
|
+
|
|
25
|
+
You receive the script agent's handoff — a JSON object with a `segments` array:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"segments": [
|
|
30
|
+
{
|
|
31
|
+
"index": 0,
|
|
32
|
+
"durationSec": 3.5,
|
|
33
|
+
"startSec": 0.0,
|
|
34
|
+
"narration": "Your best ideas don't wait for the right moment.",
|
|
35
|
+
"visualDesc": "...",
|
|
36
|
+
"transition": null
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Tool: `tts_generate`
|
|
43
|
+
|
|
44
|
+
The `tts_generate` tool wraps the local Kokoro TTS engine (`src/services/tts.js`). It takes text and produces a WAV file.
|
|
45
|
+
|
|
46
|
+
### Parameters
|
|
47
|
+
|
|
48
|
+
| Parameter | Type | Required | Default | Description |
|
|
49
|
+
|-----------|------|----------|---------|-------------|
|
|
50
|
+
| `text` | string | Yes | — | The text to synthesize. Must be non-empty. |
|
|
51
|
+
| `voice` | string | No | `"af_heart"` | Voice ID. See available voices below. |
|
|
52
|
+
| `speed` | number | No | `1.5` | Speech rate multiplier. 1.0 = normal, 1.5 = slightly faster (good for video). Range: 0.5–3.0. |
|
|
53
|
+
| `outputPath` | string | No | auto-generated temp path | Where to save the WAV file. Always provide an explicit path in `$SHMAKK_OUTPUT_DIR/voice/` so the compositor can find the files. |
|
|
54
|
+
|
|
55
|
+
### Returns
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"audioPath": "/path/to/output.wav",
|
|
60
|
+
"voice": "af_heart",
|
|
61
|
+
"durationSec": 3.4
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Available voices
|
|
66
|
+
|
|
67
|
+
Kokoro provides multiple voices. Run `tts.listVoices()` to get the full list. Common voices:
|
|
68
|
+
|
|
69
|
+
| Voice ID | Language | Gender | Character |
|
|
70
|
+
|----------|----------|--------|-----------|
|
|
71
|
+
| `af_heart` | en-us | female | Warm, natural, good default for narration |
|
|
72
|
+
| `af_bella` | en-us | female | Energetic, younger sounding |
|
|
73
|
+
| `af_nicole` | en-us | female | Calm, professional |
|
|
74
|
+
| `af_sarah` | en-us | female | Bright, articulate |
|
|
75
|
+
| `af_sky` | en-us | female | Soft, gentle |
|
|
76
|
+
| `am_adam` | en-us | male | Deep, authoritative |
|
|
77
|
+
| `am_michael` | en-us | male | Neutral, clear |
|
|
78
|
+
| `am_eric` | en-us | male | Friendly, casual |
|
|
79
|
+
| `am_jesse` | en-us | male | Relaxed, conversational |
|
|
80
|
+
|
|
81
|
+
### Voice selection strategy
|
|
82
|
+
|
|
83
|
+
- **Single narrator:** Pick one voice for all segments and use it consistently. `af_heart` (female) or `am_michael` (male) are solid defaults unless the user specifies a preference.
|
|
84
|
+
- **Multiple speakers:** If the storyboard narration fields contain speaker labels like `[Interviewer]: ...` and `[Speaker]: ...`, assign different voices to each role. Extract the speaker label, strip it from the text sent to TTS, and apply the assigned voice.
|
|
85
|
+
- **User preference:** If the user specifies a voice (e.g., "use a British female voice" or "deep male voice"), map that to the closest available Kokoro voice. If no match exists, pick the closest and note the choice in the output.
|
|
86
|
+
|
|
87
|
+
## Workflow
|
|
88
|
+
|
|
89
|
+
### Step 1: Receive the storyboard
|
|
90
|
+
|
|
91
|
+
Extract the `segments` array from the script agent's handoff. Validate that it is an array with at least one segment and that segments have non-empty `narration` fields (skip segments where narration is empty).
|
|
92
|
+
|
|
93
|
+
### Step 2: Choose voice(s)
|
|
94
|
+
|
|
95
|
+
- If the storyboard uses speaker labels, identify all unique speakers
|
|
96
|
+
- Assign a distinct voice to each speaker role
|
|
97
|
+
- If no speaker labels, pick a single voice based on:
|
|
98
|
+
1. User's explicit request (if any)
|
|
99
|
+
2. Content tone: energetic → `af_bella`, professional → `af_nicole`, warm → `af_heart`, authoritative → `am_adam`
|
|
100
|
+
3. Default: `af_heart`
|
|
101
|
+
|
|
102
|
+
### Step 3: Create output directory
|
|
103
|
+
|
|
104
|
+
Use `make_dir` to create the output directory. The convention is:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
output/voice/
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
All audio files go here so the compositor can reference them by path.
|
|
111
|
+
|
|
112
|
+
### Step 4: Generate audio per segment
|
|
113
|
+
|
|
114
|
+
For each segment with non-empty narration:
|
|
115
|
+
|
|
116
|
+
1. **Extract text:** If narration contains a speaker label like `"[Speaker]: text here"`, strip the label and only pass the text after the colon to TTS.
|
|
117
|
+
2. **Call `tts_generate`:** Pass the text, voice, and explicit output path.
|
|
118
|
+
3. **Name files predictably:** Use the pattern `segment-{index}.wav` (e.g., `segment-0.wav`, `segment-1.wav`). This makes it trivial for the compositor to match audio to segments.
|
|
119
|
+
|
|
120
|
+
For segments with empty narration, skip generation and mark the segment with `audioPath: null`.
|
|
121
|
+
|
|
122
|
+
### Step 5: Collect results
|
|
123
|
+
|
|
124
|
+
After all TTS calls complete, assemble the output payload:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"voice": "af_heart",
|
|
129
|
+
"speed": 1.5,
|
|
130
|
+
"segments": [
|
|
131
|
+
{
|
|
132
|
+
"index": 0,
|
|
133
|
+
"audioPath": "output/voice/segment-0.wav",
|
|
134
|
+
"durationSec": 3.4,
|
|
135
|
+
"voice": "af_heart"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"index": 1,
|
|
139
|
+
"audioPath": "output/voice/segment-1.wav",
|
|
140
|
+
"durationSec": 5.1,
|
|
141
|
+
"voice": "af_heart"
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Step 6: Hand off
|
|
148
|
+
|
|
149
|
+
Return this payload. The compositor will merge it with the visual agent's output to assemble the final video. Include the `durationSec` for each segment (from `tts_generate` return value) — the compositor uses this to verify timing alignment.
|
|
150
|
+
|
|
151
|
+
## Budget awareness
|
|
152
|
+
|
|
153
|
+
`tts_generate` costs 1 budget point per call. However, TTS runs locally on Kokoro (no API cost). Be mindful of:
|
|
154
|
+
- Each non-empty narration segment = 1 `tts_generate` call
|
|
155
|
+
- A 12-segment video with all narration = 12 budget points
|
|
156
|
+
- If budget is tight, consider whether segments with very short narration (< 10 words) can be merged with adjacent segments (coordinate with script agent — but if you already have the storyboard, proceed as-is; script changes are the script agent's responsibility)
|
|
157
|
+
|
|
158
|
+
## Edge cases
|
|
159
|
+
|
|
160
|
+
- **Empty narration (music-only segment):** Skip `tts_generate`. Set `audioPath: null` and `durationSec: null` in the output for that segment. The compositor will use the visual duration for timing.
|
|
161
|
+
- **Very long narration (> 75 words):** Kokoro handles it fine, but video pacing may suffer. Flag in a note but generate the audio anyway.
|
|
162
|
+
- **Speaker label with no colon:** Treat the entire string as narration text. If the label pattern is ambiguous, generate as-is.
|
|
163
|
+
- **TTS generation fails:** If a single segment fails, retry once with `speed: 1.0` (some voices handle slower speeds more reliably). If it still fails, log the error and set `audioPath: null` for that segment — the compositor can still assemble the video with silence for that segment.
|
|
164
|
+
- **Voice not found:** Run `tts.listVoices()` to get the available voice list. Pick the closest match by gender/language. If the user specified a voice that does not exist, explain and pick a fallback.
|
|
165
|
+
|
|
166
|
+
## Example
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
# After receiving storyboard, create output directory and generate audio:
|
|
170
|
+
make_dir output/voice/
|
|
171
|
+
|
|
172
|
+
# For each segment with narration:
|
|
173
|
+
tts_generate --text "Your best ideas don't wait for the right moment." \
|
|
174
|
+
--voice af_heart \
|
|
175
|
+
--speed 1.5 \
|
|
176
|
+
--outputPath output/voice/segment-0.wav
|
|
177
|
+
|
|
178
|
+
tts_generate --text "They arrive in the shower, on a walk, or right before you fall asleep." \
|
|
179
|
+
--voice af_heart \
|
|
180
|
+
--speed 1.5 \
|
|
181
|
+
--outputPath output/voice/segment-1.wav
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Note: The actual `tts_generate` tool is invoked via the LLM function call interface, not shell commands. The examples above illustrate the parameter values, not the invocation syntax.
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
// Agent overview — live tracking registry for multi-agent team execution.
|
|
2
|
+
//
|
|
3
|
+
// Maintains an in-memory registry of all agents (active and completed) during
|
|
4
|
+
// a team run. Provides query methods for the overview self-commands so users
|
|
5
|
+
// can see which agents are working, which skills they use, and drill into
|
|
6
|
+
// specific agents for detailed output.
|
|
7
|
+
//
|
|
8
|
+
// Architecture:
|
|
9
|
+
// team.js → agentOverview.register(id, meta) when an agent starts
|
|
10
|
+
// team.js → agentOverview.update(id, patch) when an agent finishes
|
|
11
|
+
// self-commands → agentOverview.getAll() for overview display
|
|
12
|
+
// self-commands → agentOverview.get(id) for detailed drill-down
|
|
13
|
+
|
|
14
|
+
const MAX_HISTORY = 50; // keep at most N completed entries after reset
|
|
15
|
+
|
|
16
|
+
// In-memory state — one registry per process lifetime.
|
|
17
|
+
// Keys: agent id (string). Values: entry object.
|
|
18
|
+
const registry = new Map();
|
|
19
|
+
|
|
20
|
+
// Stable order of registration (for overview listing).
|
|
21
|
+
const order = [];
|
|
22
|
+
|
|
23
|
+
let teamRunActive = false;
|
|
24
|
+
let teamRunId = null;
|
|
25
|
+
|
|
26
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function startTeamRun(id) {
|
|
29
|
+
teamRunActive = true;
|
|
30
|
+
teamRunId = id || `team-${Date.now()}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function endTeamRun() {
|
|
34
|
+
teamRunActive = false;
|
|
35
|
+
teamRunId = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isTeamRunActive() {
|
|
39
|
+
return teamRunActive;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function register(id, meta) {
|
|
43
|
+
if (!id) id = `agent-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
44
|
+
|
|
45
|
+
const entry = {
|
|
46
|
+
id,
|
|
47
|
+
role: meta.role || 'unknown',
|
|
48
|
+
skill: meta.skill || null,
|
|
49
|
+
skillSource: meta.skillSource || null,
|
|
50
|
+
task: meta.task || '',
|
|
51
|
+
fileScope: meta.fileScope || null,
|
|
52
|
+
topology: meta.topology || 'unknown',
|
|
53
|
+
status: meta.status || 'pending', // pending | running | done | error
|
|
54
|
+
startTime: meta.startTime || Date.now(),
|
|
55
|
+
endTime: null,
|
|
56
|
+
toolCount: 0,
|
|
57
|
+
error: null,
|
|
58
|
+
output: '', // truncated on store; full accessed via separate buffer
|
|
59
|
+
outputPreview: '', // first 2000 chars for quick display
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
registry.set(id, entry);
|
|
63
|
+
order.push(id);
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function update(id, patch) {
|
|
68
|
+
const entry = registry.get(id);
|
|
69
|
+
if (!entry) return false;
|
|
70
|
+
|
|
71
|
+
if (patch.status) entry.status = patch.status;
|
|
72
|
+
if (patch.endTime) entry.endTime = patch.endTime;
|
|
73
|
+
if (patch.toolCount !== undefined) entry.toolCount = patch.toolCount;
|
|
74
|
+
if (patch.error !== undefined) entry.error = patch.error;
|
|
75
|
+
if (patch.skill !== undefined) entry.skill = patch.skill;
|
|
76
|
+
if (patch.skillSource !== undefined) entry.skillSource = patch.skillSource;
|
|
77
|
+
|
|
78
|
+
if (patch.output !== undefined) {
|
|
79
|
+
const stripped = stripAnsi(String(patch.output));
|
|
80
|
+
entry.outputPreview = stripped.slice(0, 2000);
|
|
81
|
+
entry.output = stripped.slice(0, 8000); // keep reasonable cap
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Mark an agent as running (transitions from pending → running).
|
|
88
|
+
function markRunning(id) {
|
|
89
|
+
return update(id, { status: 'running', startTime: Date.now() });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Mark an agent as completed with result data.
|
|
93
|
+
function markDone(id, { toolCount, output, skill, skillSource } = {}) {
|
|
94
|
+
return update(id, {
|
|
95
|
+
status: 'done',
|
|
96
|
+
endTime: Date.now(),
|
|
97
|
+
toolCount: toolCount || 0,
|
|
98
|
+
output: output || '',
|
|
99
|
+
skill: skill || undefined,
|
|
100
|
+
skillSource: skillSource || undefined,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Mark an agent as errored.
|
|
105
|
+
function markError(id, error) {
|
|
106
|
+
return update(id, { status: 'error', endTime: Date.now(), error: String(error || '') });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Query API ─────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function get(id) {
|
|
112
|
+
const entry = registry.get(id);
|
|
113
|
+
if (!entry) return null;
|
|
114
|
+
return { ...entry }; // defensive copy
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getAll() {
|
|
118
|
+
// Return entries in registration order, newest last.
|
|
119
|
+
return order.map(id => {
|
|
120
|
+
const e = registry.get(id);
|
|
121
|
+
return e ? { ...e } : null;
|
|
122
|
+
}).filter(Boolean);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getActive() {
|
|
126
|
+
return getAll().filter(e => e.status === 'running' || e.status === 'pending');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getCompleted() {
|
|
130
|
+
return getAll().filter(e => e.status === 'done' || e.status === 'error');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Find by role name (case-insensitive partial match).
|
|
134
|
+
function findByRole(role) {
|
|
135
|
+
const lower = String(role).toLowerCase();
|
|
136
|
+
return getAll().filter(e => e.role.toLowerCase().includes(lower));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Find by skill name.
|
|
140
|
+
function findBySkill(skill) {
|
|
141
|
+
const lower = String(skill).toLowerCase();
|
|
142
|
+
return getAll().filter(e => e.skill && e.skill.toLowerCase().includes(lower));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Reset ─────────────────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
// Called at the end of a team run to clear state for the next run.
|
|
148
|
+
// Keeps a small history window for post-run inspection.
|
|
149
|
+
function reset() {
|
|
150
|
+
const completed = getCompleted();
|
|
151
|
+
// Trim to MAX_HISTORY, keeping most recent.
|
|
152
|
+
const toKeep = completed.slice(-MAX_HISTORY);
|
|
153
|
+
|
|
154
|
+
registry.clear();
|
|
155
|
+
order.length = 0;
|
|
156
|
+
|
|
157
|
+
for (const e of toKeep) {
|
|
158
|
+
registry.set(e.id, e);
|
|
159
|
+
order.push(e.id);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
teamRunActive = false;
|
|
163
|
+
teamRunId = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Formatting helpers ────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
function stripAnsi(s) {
|
|
169
|
+
return String(s || '').replace(/\x1b\[[0-9;]*m/g, '');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatDuration(ms) {
|
|
173
|
+
if (!ms || ms < 0) return '—';
|
|
174
|
+
const s = ms / 1000;
|
|
175
|
+
if (s < 1) return `${Math.round(ms)}ms`;
|
|
176
|
+
if (s < 60) return `${s.toFixed(1)}s`;
|
|
177
|
+
const m = Math.floor(s / 60);
|
|
178
|
+
const sec = Math.round(s % 60);
|
|
179
|
+
return `${m}m ${sec}s`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function statusIcon(status) {
|
|
183
|
+
switch (status) {
|
|
184
|
+
case 'pending': return '\x1b[2m○\x1b[0m'; // dim circle
|
|
185
|
+
case 'running': return '\x1b[36m◉\x1b[0m'; // cyan filled circle
|
|
186
|
+
case 'done': return '\x1b[32m●\x1b[0m'; // green filled circle
|
|
187
|
+
case 'error': return '\x1b[31m●\x1b[0m'; // red filled circle
|
|
188
|
+
default: return '\x1b[2m?\x1b[0m';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build a compact overview table as an array of strings.
|
|
193
|
+
function formatOverview(agents) {
|
|
194
|
+
if (!agents || agents.length === 0) {
|
|
195
|
+
return ['\x1b[2mNo agents registered.\x1b[0m'];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
const lines = [];
|
|
200
|
+
|
|
201
|
+
// Header
|
|
202
|
+
const teamTag = teamRunId ? ` \x1b[2m(team: ${teamRunId.slice(-8)})\x1b[0m` : '';
|
|
203
|
+
lines.push(`\x1b[1mAgent Overview${teamTag}\x1b[0m`);
|
|
204
|
+
lines.push('');
|
|
205
|
+
|
|
206
|
+
// Column widths
|
|
207
|
+
const roleWidth = Math.max(8, ...agents.map(a => a.role.length));
|
|
208
|
+
const skillWidth = Math.max(5, ...agents.map(a => (a.skill || '—').length));
|
|
209
|
+
const taskWidth = Math.min(60, Math.max(4, ...agents.map(a => (a.task || '').length)));
|
|
210
|
+
|
|
211
|
+
for (const a of agents) {
|
|
212
|
+
const icon = statusIcon(a.status);
|
|
213
|
+
const role = a.role.padEnd(roleWidth);
|
|
214
|
+
const skill = (a.skill || '\x1b[2m—\x1b[0m').padEnd(skillWidth + (a.skill ? 0 : 9)); // +9 for ANSI codes
|
|
215
|
+
const task = (a.task || '').slice(0, taskWidth);
|
|
216
|
+
const elapsed = a.endTime
|
|
217
|
+
? formatDuration(a.endTime - a.startTime)
|
|
218
|
+
: formatDuration(now - a.startTime);
|
|
219
|
+
const tools = a.toolCount > 0 ? `${a.toolCount} tools` : '';
|
|
220
|
+
|
|
221
|
+
lines.push(` ${icon} \x1b[36m${role}\x1b[0m \x1b[2m${skill.trim()}\x1b[0m ${task}`);
|
|
222
|
+
const infoParts = [];
|
|
223
|
+
if (elapsed) infoParts.push(elapsed);
|
|
224
|
+
if (tools) infoParts.push(tools);
|
|
225
|
+
if (a.error) infoParts.push(`\x1b[31m${a.error}\x1b[0m`);
|
|
226
|
+
lines.push(` \x1b[2m${' '.repeat(roleWidth)} ${infoParts.join(' · ')}\x1b[0m`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Summary line
|
|
230
|
+
const active = agents.filter(a => a.status === 'running' || a.status === 'pending').length;
|
|
231
|
+
const done = agents.filter(a => a.status === 'done').length;
|
|
232
|
+
const errors = agents.filter(a => a.status === 'error').length;
|
|
233
|
+
const summaryParts = [];
|
|
234
|
+
if (active) summaryParts.push(`\x1b[36m${active} active\x1b[0m`);
|
|
235
|
+
if (done) summaryParts.push(`\x1b[32m${done} done\x1b[0m`);
|
|
236
|
+
if (errors) summaryParts.push(`\x1b[31m${errors} errors\x1b[0m`);
|
|
237
|
+
lines.push('');
|
|
238
|
+
lines.push(`\x1b[2m${agents.length} total${summaryParts.length ? ' · ' + summaryParts.join(' · ') : ''}\x1b[0m`);
|
|
239
|
+
|
|
240
|
+
return lines;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Build a detailed single-agent view.
|
|
244
|
+
function formatAgentDetail(agent) {
|
|
245
|
+
if (!agent) return ['\x1b[31mAgent not found.\x1b[0m'];
|
|
246
|
+
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
const lines = [];
|
|
249
|
+
|
|
250
|
+
lines.push(`\x1b[1m${agent.role}\x1b[0m ${statusIcon(agent.status)} ${agent.status}`);
|
|
251
|
+
lines.push(`\x1b[2mid: ${agent.id}\x1b[0m`);
|
|
252
|
+
lines.push('');
|
|
253
|
+
|
|
254
|
+
if (agent.task) {
|
|
255
|
+
lines.push(`\x1b[1mTask\x1b[0m`);
|
|
256
|
+
lines.push(` ${agent.task}`);
|
|
257
|
+
lines.push('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
lines.push(`\x1b[1mDetails\x1b[0m`);
|
|
261
|
+
lines.push(` Role: ${agent.role}`);
|
|
262
|
+
lines.push(` Skill: ${agent.skill || '\x1b[2m(none — using roster hint)\x1b[0m'}`);
|
|
263
|
+
if (agent.skillSource) lines.push(` Source: \x1b[2m${agent.skillSource}\x1b[0m`);
|
|
264
|
+
lines.push(` Topology: ${agent.topology}`);
|
|
265
|
+
if (agent.fileScope) lines.push(` Scope: ${agent.fileScope}`);
|
|
266
|
+
|
|
267
|
+
const elapsed = agent.endTime
|
|
268
|
+
? formatDuration(agent.endTime - agent.startTime)
|
|
269
|
+
: `\x1b[36m${formatDuration(now - agent.startTime)} (running)\x1b[0m`;
|
|
270
|
+
lines.push(` Duration: ${elapsed}`);
|
|
271
|
+
lines.push(` Tools: ${agent.toolCount}`);
|
|
272
|
+
|
|
273
|
+
if (agent.error) {
|
|
274
|
+
lines.push(` Error: \x1b[31m${agent.error}\x1b[0m`);
|
|
275
|
+
}
|
|
276
|
+
lines.push('');
|
|
277
|
+
|
|
278
|
+
if (agent.outputPreview) {
|
|
279
|
+
lines.push(`\x1b[1mOutput\x1b[0m (first 2000 chars)`);
|
|
280
|
+
lines.push('\x1b[2m──────────────────────────────────────────────────────\x1b[0m');
|
|
281
|
+
lines.push(agent.outputPreview);
|
|
282
|
+
lines.push('\x1b[2m──────────────────────────────────────────────────────\x1b[0m');
|
|
283
|
+
if (agent.output && agent.output.length >= 2000) {
|
|
284
|
+
lines.push(`\x1b[2m... output truncated (${agent.output.length} total)\x1b[0m`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
// lifecycle
|
|
295
|
+
startTeamRun,
|
|
296
|
+
endTeamRun,
|
|
297
|
+
isTeamRunActive,
|
|
298
|
+
reset,
|
|
299
|
+
|
|
300
|
+
// mutation
|
|
301
|
+
register,
|
|
302
|
+
update,
|
|
303
|
+
markRunning,
|
|
304
|
+
markDone,
|
|
305
|
+
markError,
|
|
306
|
+
|
|
307
|
+
// query
|
|
308
|
+
get,
|
|
309
|
+
getAll,
|
|
310
|
+
getActive,
|
|
311
|
+
getCompleted,
|
|
312
|
+
findByRole,
|
|
313
|
+
findBySkill,
|
|
314
|
+
|
|
315
|
+
// formatting
|
|
316
|
+
formatOverview,
|
|
317
|
+
formatAgentDetail,
|
|
318
|
+
statusIcon,
|
|
319
|
+
formatDuration,
|
|
320
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Additional agent roster entries for media/video production roles.
|
|
2
|
+
//
|
|
3
|
+
// These extend the main AGENT_ROSTER in src/team.js with specialist roles
|
|
4
|
+
// for the video production pipeline: script writing and video compositing.
|
|
5
|
+
//
|
|
6
|
+
// The voice and visual roles are handled by the existing media-video-voice
|
|
7
|
+
// and media-imagegen skills respectively.
|
|
8
|
+
//
|
|
9
|
+
// Each entry maps to a skill file in the skills/ directory:
|
|
10
|
+
// script → skills/media-video-script.md
|
|
11
|
+
// compositor → skills/media-video-compose.md
|
|
12
|
+
|
|
13
|
+
const AGENT_ROSTER_EXTENSIONS = {
|
|
14
|
+
script: {
|
|
15
|
+
profile: 'deep',
|
|
16
|
+
hint: `Specialist: Video Script Writer
|
|
17
|
+
Focus: turning user prompts into structured timed storyboards for video production.
|
|
18
|
+
Guidelines:
|
|
19
|
+
- Output valid JSON: an array of segments, each with startTime, endTime, narration, visualDesc.
|
|
20
|
+
- startTime/endTime in seconds (floating-point). Total duration must match user request.
|
|
21
|
+
- narration: conversational text suitable for TTS. Keep each segment under 30 seconds of speech (~75 words max).
|
|
22
|
+
- visualDesc: detailed visual prompt for image generation. Describe scene, style, composition, color palette.
|
|
23
|
+
- Match the user's requested tone, pacing, and style. For explainer videos, prefer clear logical flow. For demos, prefer step-by-step walkthrough.
|
|
24
|
+
- If duration or segment count is unclear, ask before finalizing.`,
|
|
25
|
+
skill: 'media-video-script',
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
compositor: {
|
|
29
|
+
profile: 'builder',
|
|
30
|
+
hint: `Specialist: Video Compositor
|
|
31
|
+
Tools: video_compose (assemble clips/images/audio into a segment), video_concat (join rendered segments), video_probe (inspect metadata).
|
|
32
|
+
Focus: assembling audio, images, and transitions into a final video file.
|
|
33
|
+
Guidelines:
|
|
34
|
+
- Read the script agent's output first — it defines the timeline and assets per segment.
|
|
35
|
+
- For each segment: call video_compose with the image path, audio path, startTime, and endTime to render that segment.
|
|
36
|
+
- Use video_probe to verify audio duration and image dimensions before composing.
|
|
37
|
+
- After all segments are rendered, call video_concat to join them into the final output.
|
|
38
|
+
- Transitions: prefer crossfade (0.3–0.5s) between segments unless otherwise specified.
|
|
39
|
+
- Output format: H.264 video (libx264), AAC audio, .mp4 container. Match the first segment's resolution.
|
|
40
|
+
- If an asset is missing or has wrong duration, report the exact segment and path — do not silently skip.
|
|
41
|
+
- Verify the final output with video_probe: check total duration matches expected.`,
|
|
42
|
+
skill: 'media-video-compose',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Role-to-skill mapping for these extensions. Used by src/team.js to look up
|
|
47
|
+
// skill files that provide the full agent instructions.
|
|
48
|
+
const ROLE_TO_SKILL_EXTENSIONS = {
|
|
49
|
+
script: 'media-video-script',
|
|
50
|
+
compositor: 'media-video-compose',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
module.exports = { AGENT_ROSTER_EXTENSIONS, ROLE_TO_SKILL_EXTENSIONS };
|