@hustle-together/api-dev-tools 3.3.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +712 -377
- package/commands/api-create.md +68 -23
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +1 -1
- package/demo/hustle-together/blog/interview-driven-api-development.html +1 -1
- package/demo/hustle-together/blog/tdd-for-ai.html +1 -1
- package/demo/hustle-together/index.html +2 -2
- package/demo/workflow-demo-v3.5-backup.html +5008 -0
- package/demo/workflow-demo.html +5137 -3805
- package/hooks/enforce-deep-research.py +6 -1
- package/hooks/enforce-disambiguation.py +7 -1
- package/hooks/enforce-documentation.py +6 -1
- package/hooks/enforce-environment.py +5 -1
- package/hooks/enforce-interview.py +5 -1
- package/hooks/enforce-refactor.py +3 -1
- package/hooks/enforce-schema.py +0 -0
- package/hooks/enforce-scope.py +5 -1
- package/hooks/enforce-tdd-red.py +5 -1
- package/hooks/enforce-verify.py +0 -0
- package/hooks/track-tool-use.py +167 -0
- package/hooks/verify-implementation.py +0 -0
- package/package.json +1 -1
- package/templates/api-dev-state.json +24 -0
- package/demo/audio/audio-sync.js +0 -295
- package/demo/audio/generate-all-narrations.js +0 -581
- package/demo/audio/generate-narration.js +0 -486
- package/demo/audio/generate-voice-previews.js +0 -140
- package/demo/audio/narration-adam-timing.json +0 -4675
- package/demo/audio/narration-adam.mp3 +0 -0
- package/demo/audio/narration-creature-timing.json +0 -4675
- package/demo/audio/narration-creature.mp3 +0 -0
- package/demo/audio/narration-gaming-timing.json +0 -4675
- package/demo/audio/narration-gaming.mp3 +0 -0
- package/demo/audio/narration-hope-timing.json +0 -4675
- package/demo/audio/narration-hope.mp3 +0 -0
- package/demo/audio/narration-mark-timing.json +0 -4675
- package/demo/audio/narration-mark.mp3 +0 -0
- package/demo/audio/narration-timing.json +0 -3614
- package/demo/audio/narration-timing.sample.json +0 -48
- package/demo/audio/narration.mp3 +0 -0
- package/demo/audio/previews/manifest.json +0 -30
- package/demo/audio/previews/preview-creature.mp3 +0 -0
- package/demo/audio/previews/preview-gaming.mp3 +0 -0
- package/demo/audio/previews/preview-hope.mp3 +0 -0
- package/demo/audio/previews/preview-mark.mp3 +0 -0
- package/demo/audio/voices-manifest.json +0 -50
package/demo/audio/audio-sync.js
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Audio Sync Controller for Workflow Demo
|
|
3
|
-
*
|
|
4
|
-
* This module syncs audio narration with GSAP animations.
|
|
5
|
-
* It uses word-level timestamps to trigger highlights at the right moment.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* const sync = new AudioSyncController('audio/narration.mp3', 'audio/narration-timing.json');
|
|
9
|
-
* await sync.init();
|
|
10
|
-
* sync.play();
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
class AudioSyncController {
|
|
14
|
-
constructor(audioUrl, timingUrl) {
|
|
15
|
-
this.audioUrl = audioUrl;
|
|
16
|
-
this.timingUrl = timingUrl;
|
|
17
|
-
this.audio = null;
|
|
18
|
-
this.timing = null;
|
|
19
|
-
this.currentSection = null;
|
|
20
|
-
this.highlightedElements = new Set();
|
|
21
|
-
this.isPlaying = false;
|
|
22
|
-
this.onSectionChange = null;
|
|
23
|
-
this.onHighlight = null;
|
|
24
|
-
this.onWordSpoken = null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Initialize the controller by loading audio and timing data
|
|
29
|
-
*/
|
|
30
|
-
async init() {
|
|
31
|
-
// Load timing data
|
|
32
|
-
const response = await fetch(this.timingUrl);
|
|
33
|
-
this.timing = await response.json();
|
|
34
|
-
|
|
35
|
-
// Create audio element
|
|
36
|
-
this.audio = new Audio(this.audioUrl);
|
|
37
|
-
this.audio.preload = 'auto';
|
|
38
|
-
|
|
39
|
-
// Set up event listeners
|
|
40
|
-
this.audio.addEventListener('timeupdate', () => this.onTimeUpdate());
|
|
41
|
-
this.audio.addEventListener('ended', () => this.onEnded());
|
|
42
|
-
this.audio.addEventListener('play', () => { this.isPlaying = true; });
|
|
43
|
-
this.audio.addEventListener('pause', () => { this.isPlaying = false; });
|
|
44
|
-
|
|
45
|
-
// Wait for audio to be ready
|
|
46
|
-
return new Promise((resolve, reject) => {
|
|
47
|
-
this.audio.addEventListener('canplaythrough', () => resolve(), { once: true });
|
|
48
|
-
this.audio.addEventListener('error', (e) => reject(e), { once: true });
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Get the current playback time
|
|
54
|
-
*/
|
|
55
|
-
get currentTime() {
|
|
56
|
-
return this.audio ? this.audio.currentTime : 0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Get the total duration
|
|
61
|
-
*/
|
|
62
|
-
get duration() {
|
|
63
|
-
return this.audio ? this.audio.duration : 0;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Play the audio
|
|
68
|
-
*/
|
|
69
|
-
play() {
|
|
70
|
-
if (this.audio) {
|
|
71
|
-
this.audio.play();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Pause the audio
|
|
77
|
-
*/
|
|
78
|
-
pause() {
|
|
79
|
-
if (this.audio) {
|
|
80
|
-
this.audio.pause();
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Toggle play/pause
|
|
86
|
-
*/
|
|
87
|
-
toggle() {
|
|
88
|
-
if (this.isPlaying) {
|
|
89
|
-
this.pause();
|
|
90
|
-
} else {
|
|
91
|
-
this.play();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Seek to a specific time
|
|
97
|
-
*/
|
|
98
|
-
seek(time) {
|
|
99
|
-
if (this.audio) {
|
|
100
|
-
this.audio.currentTime = time;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Seek to a specific section
|
|
106
|
-
*/
|
|
107
|
-
seekToSection(sectionId) {
|
|
108
|
-
const section = this.timing.sections.find(s => s.id === sectionId);
|
|
109
|
-
if (section) {
|
|
110
|
-
this.seek(section.timestamp);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Handle time updates from the audio element
|
|
116
|
-
*/
|
|
117
|
-
onTimeUpdate() {
|
|
118
|
-
const currentTime = this.audio.currentTime;
|
|
119
|
-
|
|
120
|
-
// Check for section changes
|
|
121
|
-
const newSection = this.getCurrentSection(currentTime);
|
|
122
|
-
if (newSection !== this.currentSection) {
|
|
123
|
-
this.currentSection = newSection;
|
|
124
|
-
this.clearHighlights();
|
|
125
|
-
if (this.onSectionChange) {
|
|
126
|
-
this.onSectionChange(newSection);
|
|
127
|
-
}
|
|
128
|
-
// Scroll to section
|
|
129
|
-
this.scrollToSection(newSection);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Check for highlights that should be active
|
|
133
|
-
this.updateHighlights(currentTime);
|
|
134
|
-
|
|
135
|
-
// Check for words being spoken
|
|
136
|
-
if (this.onWordSpoken) {
|
|
137
|
-
const currentWord = this.getCurrentWord(currentTime);
|
|
138
|
-
if (currentWord) {
|
|
139
|
-
this.onWordSpoken(currentWord);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Get the current section based on time
|
|
146
|
-
*/
|
|
147
|
-
getCurrentSection(time) {
|
|
148
|
-
let current = null;
|
|
149
|
-
for (const section of this.timing.sections) {
|
|
150
|
-
if (section.timestamp <= time) {
|
|
151
|
-
current = section.id;
|
|
152
|
-
} else {
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return current;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Get the current word being spoken
|
|
161
|
-
*/
|
|
162
|
-
getCurrentWord(time) {
|
|
163
|
-
for (const word of this.timing.words) {
|
|
164
|
-
if (time >= word.start && time <= word.end) {
|
|
165
|
-
return word;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Update highlights based on current time
|
|
173
|
-
*/
|
|
174
|
-
updateHighlights(time) {
|
|
175
|
-
// Get highlights that should be active (within 3 seconds of their timestamp)
|
|
176
|
-
const HIGHLIGHT_DURATION = 3; // seconds
|
|
177
|
-
|
|
178
|
-
for (const highlight of this.timing.highlights) {
|
|
179
|
-
const isActive = time >= highlight.timestamp &&
|
|
180
|
-
time < highlight.timestamp + HIGHLIGHT_DURATION;
|
|
181
|
-
|
|
182
|
-
const wasHighlighted = this.highlightedElements.has(highlight.selector);
|
|
183
|
-
|
|
184
|
-
if (isActive && !wasHighlighted) {
|
|
185
|
-
// Add highlight
|
|
186
|
-
this.applyHighlight(highlight.selector);
|
|
187
|
-
this.highlightedElements.add(highlight.selector);
|
|
188
|
-
if (this.onHighlight) {
|
|
189
|
-
this.onHighlight(highlight.selector, true);
|
|
190
|
-
}
|
|
191
|
-
} else if (!isActive && wasHighlighted) {
|
|
192
|
-
// Remove highlight
|
|
193
|
-
this.removeHighlight(highlight.selector);
|
|
194
|
-
this.highlightedElements.delete(highlight.selector);
|
|
195
|
-
if (this.onHighlight) {
|
|
196
|
-
this.onHighlight(highlight.selector, false);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Apply highlight animation to an element
|
|
204
|
-
*/
|
|
205
|
-
applyHighlight(selector) {
|
|
206
|
-
const elements = document.querySelectorAll(selector);
|
|
207
|
-
elements.forEach(el => {
|
|
208
|
-
// Add highlight class
|
|
209
|
-
el.classList.add('audio-highlighted');
|
|
210
|
-
|
|
211
|
-
// Use GSAP for smooth animation if available
|
|
212
|
-
if (typeof gsap !== 'undefined') {
|
|
213
|
-
gsap.to(el, {
|
|
214
|
-
boxShadow: '0 0 30px var(--accent-red-glow), 0 0 60px var(--accent-red-glow)',
|
|
215
|
-
borderColor: 'var(--accent-red)',
|
|
216
|
-
scale: 1.02,
|
|
217
|
-
duration: 0.3,
|
|
218
|
-
ease: 'power2.out'
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Remove highlight from an element
|
|
226
|
-
*/
|
|
227
|
-
removeHighlight(selector) {
|
|
228
|
-
const elements = document.querySelectorAll(selector);
|
|
229
|
-
elements.forEach(el => {
|
|
230
|
-
el.classList.remove('audio-highlighted');
|
|
231
|
-
|
|
232
|
-
if (typeof gsap !== 'undefined') {
|
|
233
|
-
gsap.to(el, {
|
|
234
|
-
boxShadow: 'none',
|
|
235
|
-
borderColor: 'var(--grey)',
|
|
236
|
-
scale: 1,
|
|
237
|
-
duration: 0.3,
|
|
238
|
-
ease: 'power2.out'
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Clear all highlights
|
|
246
|
-
*/
|
|
247
|
-
clearHighlights() {
|
|
248
|
-
for (const selector of this.highlightedElements) {
|
|
249
|
-
this.removeHighlight(selector);
|
|
250
|
-
}
|
|
251
|
-
this.highlightedElements.clear();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Scroll to a section
|
|
256
|
-
*/
|
|
257
|
-
scrollToSection(sectionId) {
|
|
258
|
-
const element = document.getElementById(sectionId);
|
|
259
|
-
if (element) {
|
|
260
|
-
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Handle audio ended
|
|
266
|
-
*/
|
|
267
|
-
onEnded() {
|
|
268
|
-
this.isPlaying = false;
|
|
269
|
-
this.clearHighlights();
|
|
270
|
-
if (this.onSectionChange) {
|
|
271
|
-
this.onSectionChange(null);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Get all section timestamps for building a progress bar
|
|
277
|
-
*/
|
|
278
|
-
getSectionMarkers() {
|
|
279
|
-
return this.timing.sections.map(s => ({
|
|
280
|
-
id: s.id,
|
|
281
|
-
timestamp: s.timestamp,
|
|
282
|
-
percentage: (s.timestamp / this.duration) * 100
|
|
283
|
-
}));
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Export for use in browser
|
|
288
|
-
if (typeof window !== 'undefined') {
|
|
289
|
-
window.AudioSyncController = AudioSyncController;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Export for Node.js (if needed for testing)
|
|
293
|
-
if (typeof module !== 'undefined') {
|
|
294
|
-
module.exports = AudioSyncController;
|
|
295
|
-
}
|