crewly 1.5.11 → 1.5.12

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 (172) hide show
  1. package/config/constants.ts +3 -3
  2. package/config/orchestrator_tasks/prompts/orchestrator-prompt.md +73 -0
  3. package/config/roles/architect/prompt.md +9 -0
  4. package/config/roles/backend-developer/prompt.md +9 -0
  5. package/config/roles/content-strategist/prompt.md +10 -0
  6. package/config/roles/designer/prompt.md +9 -0
  7. package/config/roles/developer/prompt.md +9 -0
  8. package/config/roles/frontend-developer/prompt.md +9 -0
  9. package/config/roles/fullstack-dev/prompt.md +9 -0
  10. package/config/roles/generalist/prompt.md +9 -0
  11. package/config/roles/ops/prompt.md +9 -0
  12. package/config/roles/product-manager/prompt.md +9 -0
  13. package/config/roles/qa/prompt.md +9 -0
  14. package/config/roles/qa-engineer/prompt.md +9 -0
  15. package/config/roles/researcher/prompt.md +9 -0
  16. package/config/roles/sales/prompt.md +9 -0
  17. package/config/roles/support/prompt.md +9 -0
  18. package/config/roles/team-leader/prompt.md +11 -0
  19. package/config/roles/tpm/prompt.md +9 -0
  20. package/config/roles/ux-designer/prompt.md +9 -0
  21. package/config/skills/agent/core/block-task/execute.sh +3 -1
  22. package/config/skills/agent/core/pipe-to-sink/execute.sh +41 -0
  23. package/config/skills/agent/core/read-task/execute.sh +3 -1
  24. package/config/skills/agent/core/report-progress/execute.sh +3 -1
  25. package/config/skills/agent/screenshot-compare/SKILL.md +75 -0
  26. package/config/skills/agent/screenshot-compare/execute.sh +182 -0
  27. package/config/skills/agent/screenshot-compare/skill.json +10 -0
  28. package/config/skills/agent/xiaoyuzhoufm-transcript/SKILL.md +85 -0
  29. package/config/skills/agent/xiaoyuzhoufm-transcript/execute.sh +306 -0
  30. package/config/skills/agent/xiaoyuzhoufm-transcript/skill.json +10 -0
  31. package/config/skills/orchestrator/cancel-cron/SKILL.md +44 -0
  32. package/config/skills/orchestrator/create-cron/SKILL.md +58 -0
  33. package/config/skills/orchestrator/list-cron/SKILL.md +51 -0
  34. package/config/skills/orchestrator/update-cron/SKILL.md +52 -0
  35. package/dist/backend/backend/src/constants.d.ts +7 -4
  36. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  37. package/dist/backend/backend/src/constants.js +6 -3
  38. package/dist/backend/backend/src/constants.js.map +1 -1
  39. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +21 -2
  40. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
  41. package/dist/backend/backend/src/controllers/browser/browser.controller.js +167 -29
  42. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  43. package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts +1 -1
  44. package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts.map +1 -1
  45. package/dist/backend/backend/src/controllers/browser/browser.routes.js +7 -3
  46. package/dist/backend/backend/src/controllers/browser/browser.routes.js.map +1 -1
  47. package/dist/backend/backend/src/controllers/data/data.controller.d.ts +47 -0
  48. package/dist/backend/backend/src/controllers/data/data.controller.d.ts.map +1 -0
  49. package/dist/backend/backend/src/controllers/data/data.controller.js +201 -0
  50. package/dist/backend/backend/src/controllers/data/data.controller.js.map +1 -0
  51. package/dist/backend/backend/src/controllers/data/data.routes.d.ts +18 -0
  52. package/dist/backend/backend/src/controllers/data/data.routes.d.ts.map +1 -0
  53. package/dist/backend/backend/src/controllers/data/data.routes.js +44 -0
  54. package/dist/backend/backend/src/controllers/data/data.routes.js.map +1 -0
  55. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts +3 -2
  56. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts.map +1 -1
  57. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js +5 -3
  58. package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js.map +1 -1
  59. package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts +4 -0
  60. package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts.map +1 -1
  61. package/dist/backend/backend/src/controllers/system/cron-task.controller.js +20 -0
  62. package/dist/backend/backend/src/controllers/system/cron-task.controller.js.map +1 -1
  63. package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
  64. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +18 -0
  65. package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
  66. package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts +32 -0
  67. package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts.map +1 -0
  68. package/dist/backend/backend/src/controllers/team/team-export.controller.js +61 -0
  69. package/dist/backend/backend/src/controllers/team/team-export.controller.js.map +1 -0
  70. package/dist/backend/backend/src/controllers/team/team.routes.d.ts.map +1 -1
  71. package/dist/backend/backend/src/controllers/team/team.routes.js +7 -0
  72. package/dist/backend/backend/src/controllers/team/team.routes.js.map +1 -1
  73. package/dist/backend/backend/src/index.d.ts.map +1 -1
  74. package/dist/backend/backend/src/index.js +37 -7
  75. package/dist/backend/backend/src/index.js.map +1 -1
  76. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  77. package/dist/backend/backend/src/routes/api.routes.js +4 -1
  78. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  79. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  80. package/dist/backend/backend/src/services/agent/agent-registration.service.js +6 -2
  81. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  82. package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/agent/idle-detection.service.js +17 -2
  84. package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +1 -1
  86. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +2 -2
  87. package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
  88. package/dist/backend/backend/src/services/agent/task-planning.service.d.ts +134 -0
  89. package/dist/backend/backend/src/services/agent/task-planning.service.d.ts.map +1 -0
  90. package/dist/backend/backend/src/services/agent/task-planning.service.js +291 -0
  91. package/dist/backend/backend/src/services/agent/task-planning.service.js.map +1 -0
  92. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -1
  93. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js +8 -0
  94. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
  95. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  96. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +3 -3
  97. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  98. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +13 -9
  99. package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
  100. package/dist/backend/backend/src/services/browser/browser-bridge.service.js +44 -12
  101. package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
  102. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +176 -0
  103. package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -0
  104. package/dist/backend/backend/src/services/browser/browser-proxy.service.js +441 -0
  105. package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -0
  106. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts +162 -0
  107. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts.map +1 -0
  108. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js +350 -0
  109. package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js.map +1 -0
  110. package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts +8 -0
  111. package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
  112. package/dist/backend/backend/src/services/cloud/cloud-initializer.js +27 -0
  113. package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
  114. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
  115. package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
  116. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +2 -0
  117. package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
  118. package/dist/backend/backend/src/services/core/team-export.service.d.ts +103 -0
  119. package/dist/backend/backend/src/services/core/team-export.service.d.ts.map +1 -0
  120. package/dist/backend/backend/src/services/core/team-export.service.js +182 -0
  121. package/dist/backend/backend/src/services/core/team-export.service.js.map +1 -0
  122. package/dist/backend/backend/src/services/data/data-object-store.service.d.ts +160 -0
  123. package/dist/backend/backend/src/services/data/data-object-store.service.d.ts.map +1 -0
  124. package/dist/backend/backend/src/services/data/data-object-store.service.js +434 -0
  125. package/dist/backend/backend/src/services/data/data-object-store.service.js.map +1 -0
  126. package/dist/backend/backend/src/services/data/data-object.types.d.ts +190 -0
  127. package/dist/backend/backend/src/services/data/data-object.types.d.ts.map +1 -0
  128. package/dist/backend/backend/src/services/data/data-object.types.js +143 -0
  129. package/dist/backend/backend/src/services/data/data-object.types.js.map +1 -0
  130. package/dist/backend/backend/src/services/data/schema-registry.service.d.ts +108 -0
  131. package/dist/backend/backend/src/services/data/schema-registry.service.d.ts.map +1 -0
  132. package/dist/backend/backend/src/services/data/schema-registry.service.js +290 -0
  133. package/dist/backend/backend/src/services/data/schema-registry.service.js.map +1 -0
  134. package/dist/backend/backend/src/services/data/sink-registry.service.d.ts +87 -0
  135. package/dist/backend/backend/src/services/data/sink-registry.service.d.ts.map +1 -0
  136. package/dist/backend/backend/src/services/data/sink-registry.service.js +188 -0
  137. package/dist/backend/backend/src/services/data/sink-registry.service.js.map +1 -0
  138. package/dist/backend/backend/src/services/messaging/message-router.service.d.ts.map +1 -1
  139. package/dist/backend/backend/src/services/messaging/message-router.service.js +7 -0
  140. package/dist/backend/backend/src/services/messaging/message-router.service.js.map +1 -1
  141. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +55 -2
  142. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  143. package/dist/backend/backend/src/services/monitoring/token-usage.service.js +89 -5
  144. package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  145. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
  146. package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
  147. package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts +105 -14
  148. package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
  149. package/dist/backend/backend/src/services/workflow/cron-task.service.js +400 -123
  150. package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
  151. package/dist/backend/backend/src/types/cron-task.types.d.ts +1 -1
  152. package/dist/backend/backend/src/types/data-object.types.d.ts +117 -0
  153. package/dist/backend/backend/src/types/data-object.types.d.ts.map +1 -0
  154. package/dist/backend/backend/src/types/data-object.types.js +23 -0
  155. package/dist/backend/backend/src/types/data-object.types.js.map +1 -0
  156. package/dist/backend/backend/src/types/settings.types.js +1 -1
  157. package/dist/backend/config/constants.d.ts +3 -3
  158. package/dist/backend/config/constants.js +3 -3
  159. package/dist/backend/config/constants.js.map +1 -1
  160. package/dist/cli/backend/src/constants.d.ts +7 -4
  161. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  162. package/dist/cli/backend/src/constants.js +6 -3
  163. package/dist/cli/backend/src/constants.js.map +1 -1
  164. package/dist/cli/backend/src/types/settings.types.js +1 -1
  165. package/dist/cli/config/constants.d.ts +3 -3
  166. package/dist/cli/config/constants.js +3 -3
  167. package/dist/cli/config/constants.js.map +1 -1
  168. package/frontend/dist/assets/index-371b68d4.css +33 -0
  169. package/frontend/dist/assets/{index-9af2ea40.js → index-506f70da.js} +321 -321
  170. package/frontend/dist/index.html +2 -2
  171. package/package.json +1 -1
  172. package/frontend/dist/assets/index-b19b2478.css +0 -33
@@ -0,0 +1,182 @@
1
+ #!/bin/bash
2
+ # Screenshot Compare — Gemini Vision API dual-image comparison
3
+ #
4
+ # Sends two screenshots (e.g., iOS reference + Web implementation) to
5
+ # Gemini Vision and returns a structured diff report.
6
+ #
7
+ # Usage:
8
+ # bash execute.sh '{"reference":"/path/to/ios-screenshot.png","target":"/path/to/web-screenshot.png"}'
9
+ # bash execute.sh '{"reference":"ref.png","target":"web.png","focus":"icons,layout,colors"}'
10
+ #
11
+ # Requires: GEMINI_API_KEY environment variable
12
+ #
13
+ # Output: JSON with differences categorized by type (icon, layout, color, text, image)
14
+
15
+ set -euo pipefail
16
+
17
+ # ── Configuration ──────────────────────────────────────────────────────────────
18
+ MODEL="${GEMINI_MODEL:-gemini-2.5-flash-preview-05-20}"
19
+ GEMINI_API_BASE="https://generativelanguage.googleapis.com"
20
+ GEMINI_CONTENT_URL="${GEMINI_API_BASE}/v1beta/models/${MODEL}:generateContent"
21
+ MAX_IMAGE_SIZE_MB=4
22
+
23
+ # ── Input parsing ──────────────────────────────────────────────────────────────
24
+ INPUT="${1:-}"
25
+ if [ -z "$INPUT" ]; then
26
+ echo '{"success":false,"error":"Usage: execute.sh \u0027{\"reference\":\"/path/to/ref.png\",\"target\":\"/path/to/target.png\"}\u0027"}'
27
+ exit 1
28
+ fi
29
+
30
+ REFERENCE=$(echo "$INPUT" | jq -r '.reference // empty')
31
+ TARGET=$(echo "$INPUT" | jq -r '.target // empty')
32
+ FOCUS=$(echo "$INPUT" | jq -r '.focus // "all"')
33
+ CONTEXT=$(echo "$INPUT" | jq -r '.context // ""')
34
+
35
+ if [ -z "$REFERENCE" ] || [ -z "$TARGET" ]; then
36
+ echo '{"success":false,"error":"Both reference and target image paths are required"}'
37
+ exit 1
38
+ fi
39
+
40
+ if [ ! -f "$REFERENCE" ]; then
41
+ echo "{\"success\":false,\"error\":\"Reference image not found: $REFERENCE\"}"
42
+ exit 1
43
+ fi
44
+
45
+ if [ ! -f "$TARGET" ]; then
46
+ echo "{\"success\":false,\"error\":\"Target image not found: $TARGET\"}"
47
+ exit 1
48
+ fi
49
+
50
+ # ── API key resolution ─────────────────────────────────────────────────────────
51
+ if [ -z "${GEMINI_API_KEY:-}" ]; then
52
+ # Try loading from settings via Crewly API
53
+ GEMINI_API_KEY=$(curl -sf "http://localhost:8787/api/settings" 2>/dev/null | jq -r '.data.apiKeys.global.gemini // empty' 2>/dev/null || true)
54
+ fi
55
+
56
+ if [ -z "${GEMINI_API_KEY:-}" ]; then
57
+ echo '{"success":false,"error":"GEMINI_API_KEY not set. Set via environment or Settings > API Keys."}'
58
+ exit 1
59
+ fi
60
+
61
+ # ── Image encoding ─────────────────────────────────────────────────────────────
62
+ get_mime_type() {
63
+ local file="$1"
64
+ local ext="${file##*.}"
65
+ case "$ext" in
66
+ png) echo "image/png" ;;
67
+ jpg|jpeg) echo "image/jpeg" ;;
68
+ gif) echo "image/gif" ;;
69
+ webp) echo "image/webp" ;;
70
+ *) echo "image/png" ;; # default
71
+ esac
72
+ }
73
+
74
+ REF_MIME=$(get_mime_type "$REFERENCE")
75
+ TGT_MIME=$(get_mime_type "$TARGET")
76
+ REF_B64=$(base64 < "$REFERENCE" | tr -d '\n')
77
+ TGT_B64=$(base64 < "$TARGET" | tr -d '\n')
78
+
79
+ # ── Build prompt ───────────────────────────────────────────────────────────────
80
+ FOCUS_INSTRUCTION=""
81
+ if [ "$FOCUS" != "all" ]; then
82
+ FOCUS_INSTRUCTION="Focus specifically on these areas: ${FOCUS}."
83
+ fi
84
+
85
+ CONTEXT_INSTRUCTION=""
86
+ if [ -n "$CONTEXT" ]; then
87
+ CONTEXT_INSTRUCTION="Additional context: ${CONTEXT}"
88
+ fi
89
+
90
+ PROMPT="You are a pixel-perfect UI comparison expert. Compare these two screenshots:
91
+
92
+ IMAGE 1 (Reference — the source of truth, e.g., iOS app or design mockup)
93
+ IMAGE 2 (Target — the implementation to verify, e.g., web app)
94
+
95
+ ${FOCUS_INSTRUCTION}
96
+ ${CONTEXT_INSTRUCTION}
97
+
98
+ Analyze every visible difference and return a JSON array of issues found. Each issue must have:
99
+ - \"type\": one of \"icon_missing\", \"icon_wrong\", \"layout_shift\", \"spacing\", \"color_mismatch\", \"text_mismatch\", \"font_mismatch\", \"image_missing\", \"image_wrong\", \"border_radius\", \"shadow\", \"alignment\", \"responsive\", \"other\"
100
+ - \"severity\": \"critical\" (functionality broken), \"major\" (clearly visible), \"minor\" (subtle)
101
+ - \"element\": which UI element is affected (e.g., \"header logo\", \"submit button\", \"nav bar\")
102
+ - \"reference\": what it looks like in Image 1
103
+ - \"target\": what it looks like in Image 2
104
+ - \"suggestion\": specific CSS/code fix suggestion
105
+
106
+ Return ONLY valid JSON in this format:
107
+ {
108
+ \"summary\": \"Brief overall assessment\",
109
+ \"matchScore\": 85,
110
+ \"totalIssues\": 5,
111
+ \"issues\": [
112
+ {
113
+ \"type\": \"...\",
114
+ \"severity\": \"...\",
115
+ \"element\": \"...\",
116
+ \"reference\": \"...\",
117
+ \"target\": \"...\",
118
+ \"suggestion\": \"...\"
119
+ }
120
+ ]
121
+ }"
122
+
123
+ # ── API call ───────────────────────────────────────────────────────────────────
124
+ REQUEST_BODY=$(cat <<ENDJSON
125
+ {
126
+ "contents": [{
127
+ "parts": [
128
+ {"text": $(echo "$PROMPT" | jq -Rs .)},
129
+ {
130
+ "inline_data": {
131
+ "mime_type": "${REF_MIME}",
132
+ "data": "${REF_B64}"
133
+ }
134
+ },
135
+ {
136
+ "inline_data": {
137
+ "mime_type": "${TGT_MIME}",
138
+ "data": "${TGT_B64}"
139
+ }
140
+ }
141
+ ]
142
+ }],
143
+ "generationConfig": {
144
+ "temperature": 0.1,
145
+ "maxOutputTokens": 4096,
146
+ "responseMimeType": "application/json"
147
+ }
148
+ }
149
+ ENDJSON
150
+ )
151
+
152
+ TMPFILE=$(mktemp /tmp/gemini-compare-XXXXXX.json)
153
+ echo "$REQUEST_BODY" > "$TMPFILE"
154
+ RESPONSE=$(curl -sf -X POST \
155
+ "${GEMINI_CONTENT_URL}?key=${GEMINI_API_KEY}" \
156
+ -H "Content-Type: application/json" \
157
+ -d "@${TMPFILE}" 2>&1)
158
+ rm -f "$TMPFILE"
159
+
160
+ HTTP_CODE=$?
161
+ if [ $HTTP_CODE -ne 0 ]; then
162
+ echo "{\"success\":false,\"error\":\"Gemini API request failed (curl exit $HTTP_CODE)\"}"
163
+ exit 1
164
+ fi
165
+
166
+ # ── Parse response ─────────────────────────────────────────────────────────────
167
+ ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty' 2>/dev/null)
168
+ if [ -n "$ERROR_MSG" ]; then
169
+ echo "{\"success\":false,\"error\":\"Gemini API error: $ERROR_MSG\"}"
170
+ exit 1
171
+ fi
172
+
173
+ # Extract the text content from Gemini response
174
+ RESULT_TEXT=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].text // empty' 2>/dev/null)
175
+
176
+ if [ -z "$RESULT_TEXT" ]; then
177
+ echo '{"success":false,"error":"Empty response from Gemini API"}'
178
+ exit 1
179
+ fi
180
+
181
+ # Output: wrap the Gemini JSON result in a success envelope
182
+ echo "{\"success\":true,\"model\":\"${MODEL}\",\"reference\":\"${REFERENCE}\",\"target\":\"${TARGET}\",\"comparison\":${RESULT_TEXT}}"
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "screenshot-compare",
3
+ "displayName": "Screenshot Compare (Gemini Vision)",
4
+ "description": "Compare two UI screenshots using Gemini Vision API. Returns categorized diffs with severity and fix suggestions.",
5
+ "category": "qa",
6
+ "assignableRoles": ["*"],
7
+ "version": "1.0.0",
8
+ "author": "Crewly Team",
9
+ "requires": ["GEMINI_API_KEY"]
10
+ }
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: xiaoyuzhoufm-transcript
3
+ description: Extract and transcribe 小宇宙 (Xiaoyuzhou FM) podcast episodes. Downloads audio from episode URL and uses Gemini Audio API for transcription with speaker identification and timestamps.
4
+ category: content
5
+ assignableRoles:
6
+ - "*"
7
+ version: "1.0.0"
8
+ tags:
9
+ - podcast
10
+ - transcript
11
+ - audio
12
+ - gemini
13
+ - xiaoyuzhou
14
+ - chinese
15
+ ---
16
+
17
+ # 小宇宙 Podcast Transcript
18
+
19
+ Extract and transcribe podcast episodes from 小宇宙 (Xiaoyuzhou FM) using Gemini Audio API.
20
+
21
+ ## Pipeline
22
+
23
+ ```
24
+ Episode URL → Fetch HTML → Extract audio URL → Download m4a → Upload to Gemini → Transcribe → Output
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```bash
30
+ # Transcribe from URL
31
+ bash execute.sh '{"url":"https://www.xiaoyuzhoufm.com/episode/69bbc8ea3c625cc5ae21b461"}'
32
+
33
+ # Save transcript to file
34
+ bash execute.sh '{"url":"https://www.xiaoyuzhoufm.com/episode/...","outputFile":"./transcripts/episode.md"}'
35
+
36
+ # Transcribe a local audio file (skip download)
37
+ bash execute.sh '{"audioFile":"/path/to/podcast.m4a"}'
38
+
39
+ # Specify language hint
40
+ bash execute.sh '{"url":"...","language":"Chinese"}'
41
+ ```
42
+
43
+ ## Parameters
44
+
45
+ | Parameter | Required | Description |
46
+ |-----------|----------|-------------|
47
+ | `url` | Yes* | 小宇宙 episode URL |
48
+ | `audioFile` | Yes* | Path to local audio file (alternative to url) |
49
+ | `outputFile` | No | Save transcript as markdown to this path |
50
+ | `language` | No | Language hint for Gemini (default: auto-detect) |
51
+
52
+ *Either `url` or `audioFile` is required.
53
+
54
+ ## Output Format
55
+
56
+ ```json
57
+ {
58
+ "success": true,
59
+ "title": "Episode Title",
60
+ "url": "https://www.xiaoyuzhoufm.com/episode/...",
61
+ "audioUrl": "https://media.xyzcdn.net/.../audio.m4a",
62
+ "model": "gemini-2.5-flash-preview-05-20",
63
+ "transcriptLength": 12345,
64
+ "transcript": "**Speaker 1:** [00:00:00] Welcome to the show..."
65
+ }
66
+ ```
67
+
68
+ ## Transcript Format
69
+
70
+ - Speaker turns identified with `**Speaker Name:** text`
71
+ - Timestamps at segment/topic changes: `[HH:MM:SS]`
72
+ - Preserves original language (Chinese/English/mixed)
73
+
74
+ ## Prerequisites
75
+
76
+ - `GEMINI_API_KEY` environment variable or configured in Settings > API Keys
77
+ - `curl` and `jq` available in PATH
78
+ - Internet access to xiaoyuzhoufm.com and Gemini API
79
+
80
+ ## Notes
81
+
82
+ - Audio files are typically 30-90 minutes (20-80 MB)
83
+ - Gemini processing takes 30-120 seconds depending on audio length
84
+ - Maximum wait time: 5 minutes for Gemini processing
85
+ - Temp files are auto-cleaned on exit
@@ -0,0 +1,306 @@
1
+ #!/bin/bash
2
+ # 小宇宙 Podcast Transcript — Extract and transcribe podcast episodes
3
+ #
4
+ # Pipeline: URL → HTML → audio URL → download → ffmpeg split → Gemini per-chunk → merge
5
+ #
6
+ # Long podcasts (>15min) are split into 10-minute chunks via ffmpeg,
7
+ # each chunk is uploaded and transcribed separately, then merged.
8
+ #
9
+ # Usage:
10
+ # bash execute.sh '{"url":"https://www.xiaoyuzhoufm.com/episode/69bbc8ea3c625cc5ae21b461"}'
11
+ # bash execute.sh '{"url":"...","outputFile":"/path/to/transcript.md"}'
12
+ # bash execute.sh '{"audioFile":"/path/to/local.m4a"}'
13
+ #
14
+ # Requires: GEMINI_API_KEY, ffmpeg, curl, jq
15
+
16
+ set -euo pipefail
17
+
18
+ # ── Configuration ──────────────────────────────────────────────────────────────
19
+ MODEL="${GEMINI_MODEL:-gemini-2.0-flash}"
20
+ GEMINI_API_BASE="https://generativelanguage.googleapis.com"
21
+ GEMINI_UPLOAD_URL="${GEMINI_API_BASE}/upload/v1beta/files"
22
+ GEMINI_CONTENT_URL="${GEMINI_API_BASE}/v1beta/models/${MODEL}:generateContent"
23
+ MAX_WAIT_SECONDS=300
24
+ CHUNK_DURATION_SECONDS=600 # 10 minutes per chunk
25
+
26
+ # ── Input parsing ──────────────────────────────────────────────────────────────
27
+ INPUT="${1:-}"
28
+ if [ -z "$INPUT" ]; then
29
+ echo '{"success":false,"error":"Usage: execute.sh \u0027{\"url\":\"https://www.xiaoyuzhoufm.com/episode/...\"}\u0027"}'
30
+ exit 1
31
+ fi
32
+
33
+ URL=$(echo "$INPUT" | jq -r '.url // empty')
34
+ AUDIO_FILE=$(echo "$INPUT" | jq -r '.audioFile // empty')
35
+ OUTPUT_FILE=$(echo "$INPUT" | jq -r '.outputFile // empty')
36
+ LANGUAGE=$(echo "$INPUT" | jq -r '.language // "auto"')
37
+
38
+ if [ -z "$URL" ] && [ -z "$AUDIO_FILE" ]; then
39
+ echo '{"success":false,"error":"Either url or audioFile is required"}'
40
+ exit 1
41
+ fi
42
+
43
+ # ── Dependency check ───────────────────────────────────────────────────────────
44
+ if ! command -v ffmpeg >/dev/null 2>&1; then
45
+ echo '{"success":false,"error":"ffmpeg is required but not installed. Install via: brew install ffmpeg"}'
46
+ exit 1
47
+ fi
48
+
49
+ # ── API key resolution ─────────────────────────────────────────────────────────
50
+ if [ -z "${GEMINI_API_KEY:-}" ]; then
51
+ GEMINI_API_KEY=$(curl -sf "http://localhost:8787/api/settings" 2>/dev/null \
52
+ | jq -r '.data.apiKeys.global.gemini // empty' 2>/dev/null || true)
53
+ fi
54
+ if [ -z "${GEMINI_API_KEY:-}" ]; then
55
+ echo '{"success":false,"error":"GEMINI_API_KEY not set. Set via environment or Settings > API Keys."}'
56
+ exit 1
57
+ fi
58
+
59
+ # ── Temp directory with auto-cleanup ───────────────────────────────────────────
60
+ TMPDIR_WORK=$(mktemp -d /tmp/xiaoyuzhoufm-XXXXXX)
61
+ cleanup() { rm -rf "$TMPDIR_WORK"; }
62
+ trap cleanup EXIT
63
+
64
+ # ── Step 1: Extract audio URL from 小宇宙 page ────────────────────────────────
65
+ TITLE="Unknown Episode"
66
+ AUDIO_URL=""
67
+
68
+ if [ -n "$URL" ] && [ -z "$AUDIO_FILE" ]; then
69
+ echo '{"status":"fetching","message":"Fetching episode page..."}' >&2
70
+
71
+ HTML_FILE="${TMPDIR_WORK}/page.html"
72
+ curl -sL -o "$HTML_FILE" \
73
+ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
74
+ "$URL" 2>/dev/null
75
+
76
+ if [ ! -s "$HTML_FILE" ]; then
77
+ echo '{"success":false,"error":"Failed to fetch episode page"}'
78
+ exit 1
79
+ fi
80
+
81
+ TITLE=$(grep -o '<title>[^<]*</title>' "$HTML_FILE" \
82
+ | sed 's/<title>//;s/<\/title>//' \
83
+ | sed 's/ - .* | 小宇宙.*//' \
84
+ | head -1)
85
+ [ -z "$TITLE" ] && TITLE="Unknown Episode"
86
+
87
+ AUDIO_URL=$(grep -oE 'https://media\.xyzcdn\.net/[^"]*\.(m4a|mp3|aac|ogg)' "$HTML_FILE" | head -1)
88
+ if [ -z "$AUDIO_URL" ]; then
89
+ AUDIO_URL=$(grep -oE 'https://[^"]*\.(m4a|mp3|aac|ogg)' "$HTML_FILE" | grep -v 'apple\|google\|spotify' | head -1)
90
+ fi
91
+ if [ -z "$AUDIO_URL" ]; then
92
+ echo '{"success":false,"error":"Could not find audio URL in page HTML"}'
93
+ exit 1
94
+ fi
95
+
96
+ echo "{\"status\":\"downloading\",\"message\":\"Downloading audio...\",\"title\":$(echo "$TITLE" | jq -Rs .)}" >&2
97
+
98
+ AUDIO_FILE="${TMPDIR_WORK}/audio.m4a"
99
+ curl -sL -o "$AUDIO_FILE" \
100
+ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
101
+ -H "Referer: https://www.xiaoyuzhoufm.com/" \
102
+ "$AUDIO_URL" 2>/dev/null
103
+
104
+ if [ ! -s "$AUDIO_FILE" ]; then
105
+ echo '{"success":false,"error":"Failed to download audio file"}'
106
+ exit 1
107
+ fi
108
+ AUDIO_SIZE=$(wc -c < "$AUDIO_FILE" | tr -d ' ')
109
+ echo "{\"status\":\"downloaded\",\"message\":\"Audio downloaded (${AUDIO_SIZE} bytes)\"}" >&2
110
+ fi
111
+
112
+ if [ ! -f "$AUDIO_FILE" ]; then
113
+ echo "{\"success\":false,\"error\":\"Audio file not found: ${AUDIO_FILE}\"}"
114
+ exit 1
115
+ fi
116
+
117
+ # ── Step 2: Get duration and split into chunks ─────────────────────────────────
118
+ DURATION_SECS=$(ffprobe -v quiet -show_entries format=duration -of csv=p=0 "$AUDIO_FILE" 2>/dev/null | cut -d. -f1)
119
+ DURATION_SECS=${DURATION_SECS:-0}
120
+
121
+ CHUNKS_DIR="${TMPDIR_WORK}/chunks"
122
+ mkdir -p "$CHUNKS_DIR"
123
+
124
+ if [ "$DURATION_SECS" -gt "$CHUNK_DURATION_SECONDS" ]; then
125
+ NUM_CHUNKS=$(( (DURATION_SECS + CHUNK_DURATION_SECONDS - 1) / CHUNK_DURATION_SECONDS ))
126
+ echo "{\"status\":\"splitting\",\"message\":\"Splitting ${DURATION_SECS}s audio into ${NUM_CHUNKS} chunks of ${CHUNK_DURATION_SECONDS}s...\"}" >&2
127
+
128
+ ffmpeg -i "$AUDIO_FILE" -f segment -segment_time "$CHUNK_DURATION_SECONDS" \
129
+ -c copy -reset_timestamps 1 \
130
+ "${CHUNKS_DIR}/chunk_%03d.m4a" 2>/dev/null
131
+
132
+ CHUNK_FILES=$(ls "${CHUNKS_DIR}"/chunk_*.m4a 2>/dev/null | sort)
133
+ else
134
+ # Short audio — single chunk
135
+ cp "$AUDIO_FILE" "${CHUNKS_DIR}/chunk_000.m4a"
136
+ CHUNK_FILES="${CHUNKS_DIR}/chunk_000.m4a"
137
+ NUM_CHUNKS=1
138
+ echo "{\"status\":\"splitting\",\"message\":\"Short audio (${DURATION_SECS}s), no splitting needed.\"}" >&2
139
+ fi
140
+
141
+ # ── Helpers: upload + transcribe a single chunk ────────────────────────────────
142
+
143
+ # Determine MIME type from file extension
144
+ get_mime() {
145
+ case "${1##*.}" in
146
+ mp3) echo "audio/mpeg" ;; m4a) echo "audio/mp4" ;; aac) echo "audio/aac" ;;
147
+ ogg) echo "audio/ogg" ;; wav) echo "audio/wav" ;; *) echo "audio/mp4" ;;
148
+ esac
149
+ }
150
+
151
+ upload_and_wait() {
152
+ local filepath="$1"
153
+ local mime_type="$2"
154
+ local file_size
155
+ file_size=$(wc -c < "$filepath" | tr -d ' ')
156
+ local display_name
157
+ display_name=$(basename "$filepath")
158
+
159
+ # Resumable upload: init
160
+ local init_resp
161
+ init_resp=$(curl -s -D- -X POST \
162
+ "${GEMINI_UPLOAD_URL}?key=${GEMINI_API_KEY}" \
163
+ -H "X-Goog-Upload-Protocol: resumable" \
164
+ -H "X-Goog-Upload-Command: start" \
165
+ -H "X-Goog-Upload-Header-Content-Length: ${file_size}" \
166
+ -H "X-Goog-Upload-Header-Content-Type: ${mime_type}" \
167
+ -H "Content-Type: application/json" \
168
+ -d "{\"file\":{\"display_name\":\"${display_name}\"}}" 2>&1)
169
+
170
+ local upload_url
171
+ upload_url=$(echo "$init_resp" | grep -i 'x-goog-upload-url:' | sed 's/.*: //' | tr -d '\r')
172
+ [ -z "$upload_url" ] && { echo ""; return 1; }
173
+
174
+ # Resumable upload: send bytes
175
+ local upload_resp
176
+ upload_resp=$(curl -s -X POST "$upload_url" \
177
+ -H "X-Goog-Upload-Offset: 0" \
178
+ -H "X-Goog-Upload-Command: upload, finalize" \
179
+ -H "Content-Type: ${mime_type}" \
180
+ --data-binary "@${filepath}" 2>&1)
181
+
182
+ local file_uri file_name
183
+ file_uri=$(echo "$upload_resp" | jq -r '.file.uri // empty' 2>/dev/null)
184
+ file_name=$(echo "$upload_resp" | jq -r '.file.name // empty' 2>/dev/null)
185
+ [ -z "$file_uri" ] && { echo ""; return 1; }
186
+
187
+ # Poll until ACTIVE
188
+ local state="PROCESSING" waited=0
189
+ while [ "$state" = "PROCESSING" ] && [ "$waited" -lt "$MAX_WAIT_SECONDS" ]; do
190
+ sleep 3
191
+ waited=$((waited + 3))
192
+ state=$(curl -s "${GEMINI_API_BASE}/v1beta/${file_name}?key=${GEMINI_API_KEY}" \
193
+ | jq -r '.state // "PROCESSING"' 2>/dev/null)
194
+ done
195
+
196
+ [ "$state" != "ACTIVE" ] && { echo ""; return 1; }
197
+ echo "$file_uri"
198
+ }
199
+
200
+ transcribe_chunk() {
201
+ local file_uri="$1"
202
+ local mime_type="$2"
203
+ local chunk_index="$3"
204
+ local total_chunks="$4"
205
+
206
+ local lang_hint=""
207
+ [ "$LANGUAGE" != "auto" ] && lang_hint="The audio is in ${LANGUAGE}. "
208
+
209
+ local prompt="${lang_hint}Transcribe this audio segment (chunk $((chunk_index+1)) of ${total_chunks}). Output the full transcript preserving speaker turns. Format:
210
+
211
+ **Speaker Name/Label:** text spoken...
212
+
213
+ Use Speaker 1, Speaker 2 etc if names unknown. Maintain original language. Add [MM:SS] timestamps at topic changes (relative to this chunk start)."
214
+
215
+ local req_file="${TMPDIR_WORK}/req_${chunk_index}.json"
216
+ cat > "$req_file" <<ENDJSON
217
+ {
218
+ "contents": [{"parts": [
219
+ {"text": $(echo "$prompt" | jq -Rs .)},
220
+ {"file_data": {"mime_type": "${mime_type}", "file_uri": "${file_uri}"}}
221
+ ]}],
222
+ "generationConfig": {"temperature": 0.1, "maxOutputTokens": 32768}
223
+ }
224
+ ENDJSON
225
+
226
+ local resp
227
+ resp=$(curl -s --max-time 300 -X POST \
228
+ "${GEMINI_CONTENT_URL}?key=${GEMINI_API_KEY}" \
229
+ -H "Content-Type: application/json" \
230
+ -d "@${req_file}" 2>&1)
231
+
232
+ local err_msg
233
+ err_msg=$(echo "$resp" | jq -r '.error.message // empty' 2>/dev/null)
234
+ [ -n "$err_msg" ] && { echo "[Transcription error: ${err_msg}]"; return 0; }
235
+
236
+ echo "$resp" | jq -r '.candidates[0].content.parts[0].text // "[No transcript for this chunk]"' 2>/dev/null
237
+ }
238
+
239
+ # ── Step 3: Upload and transcribe each chunk ───────────────────────────────────
240
+ FULL_TRANSCRIPT=""
241
+ CHUNK_INDEX=0
242
+ TOTAL_CHUNKS=$(echo "$CHUNK_FILES" | wc -w | tr -d ' ')
243
+
244
+ for chunk_file in $CHUNK_FILES; do
245
+ CHUNK_INDEX_DISPLAY=$((CHUNK_INDEX + 1))
246
+ echo "{\"status\":\"transcribing\",\"message\":\"Chunk ${CHUNK_INDEX_DISPLAY}/${TOTAL_CHUNKS}: uploading...\"}" >&2
247
+
248
+ MIME=$(get_mime "$chunk_file")
249
+ FILE_URI=$(upload_and_wait "$chunk_file" "$MIME")
250
+
251
+ if [ -z "$FILE_URI" ]; then
252
+ echo "{\"status\":\"error\",\"message\":\"Chunk ${CHUNK_INDEX_DISPLAY} upload/processing failed, skipping\"}" >&2
253
+ FULL_TRANSCRIPT="${FULL_TRANSCRIPT}
254
+
255
+ ---
256
+ [Chunk ${CHUNK_INDEX_DISPLAY} failed to process]
257
+ ---
258
+ "
259
+ CHUNK_INDEX=$((CHUNK_INDEX + 1))
260
+ continue
261
+ fi
262
+
263
+ echo "{\"status\":\"transcribing\",\"message\":\"Chunk ${CHUNK_INDEX_DISPLAY}/${TOTAL_CHUNKS}: transcribing...\"}" >&2
264
+ CHUNK_TEXT=$(transcribe_chunk "$FILE_URI" "$MIME" "$CHUNK_INDEX" "$TOTAL_CHUNKS")
265
+
266
+ # Add time offset header for multi-chunk
267
+ if [ "$TOTAL_CHUNKS" -gt 1 ]; then
268
+ OFFSET_MINS=$(( CHUNK_INDEX * CHUNK_DURATION_SECONDS / 60 ))
269
+ FULL_TRANSCRIPT="${FULL_TRANSCRIPT}
270
+
271
+ --- Segment ${CHUNK_INDEX_DISPLAY} (from ${OFFSET_MINS}:00) ---
272
+
273
+ ${CHUNK_TEXT}"
274
+ else
275
+ FULL_TRANSCRIPT="$CHUNK_TEXT"
276
+ fi
277
+
278
+ CHUNK_INDEX=$((CHUNK_INDEX + 1))
279
+ echo "{\"status\":\"transcribing\",\"message\":\"Chunk ${CHUNK_INDEX_DISPLAY}/${TOTAL_CHUNKS}: done.\"}" >&2
280
+ done
281
+
282
+ if [ -z "$FULL_TRANSCRIPT" ]; then
283
+ echo '{"success":false,"error":"All chunks failed to transcribe"}'
284
+ exit 1
285
+ fi
286
+
287
+ # ── Step 4: Output ─────────────────────────────────────────────────────────────
288
+ if [ -n "$OUTPUT_FILE" ]; then
289
+ mkdir -p "$(dirname "$OUTPUT_FILE")"
290
+ cat > "$OUTPUT_FILE" <<ENDMD
291
+ # ${TITLE}
292
+
293
+ > Source: ${URL:-local file}
294
+ > Transcribed: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
295
+ > Model: ${MODEL}
296
+ > Duration: ${DURATION_SECS}s (${TOTAL_CHUNKS} chunks)
297
+
298
+ ---
299
+
300
+ ${FULL_TRANSCRIPT}
301
+ ENDMD
302
+ echo "{\"status\":\"saved\",\"message\":\"Transcript saved to ${OUTPUT_FILE}\"}" >&2
303
+ fi
304
+
305
+ TRANSCRIPT_LENGTH=${#FULL_TRANSCRIPT}
306
+ echo "{\"success\":true,\"title\":$(echo "$TITLE" | jq -Rs .),\"url\":$(echo "${URL:-}" | jq -Rs .),\"audioUrl\":$(echo "${AUDIO_URL:-}" | jq -Rs .),\"model\":\"${MODEL}\",\"duration\":${DURATION_SECS},\"chunks\":${TOTAL_CHUNKS},\"transcriptLength\":${TRANSCRIPT_LENGTH},\"transcript\":$(echo "$FULL_TRANSCRIPT" | jq -Rs .)}"
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "xiaoyuzhoufm-transcript",
3
+ "displayName": "小宇宙 Podcast Transcript",
4
+ "description": "Extract and transcribe 小宇宙 podcast episodes using Gemini Audio API. Supports speaker identification and timestamps.",
5
+ "category": "content",
6
+ "assignableRoles": ["*"],
7
+ "version": "1.0.0",
8
+ "author": "Crewly Team",
9
+ "requires": ["GEMINI_API_KEY"]
10
+ }
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: Cancel Cron Task
3
+ description: Permanently delete a cron task.
4
+ version: 1.0.0
5
+ category: scheduling
6
+ skillType: claude-skill
7
+ assignableRoles:
8
+ - orchestrator
9
+ triggers:
10
+ - cancel cron
11
+ - delete cron
12
+ - remove cron task
13
+ tags:
14
+ - cron
15
+ - scheduling
16
+ - cleanup
17
+ execution:
18
+ type: script
19
+ script:
20
+ file: execute.sh
21
+ interpreter: bash
22
+ timeoutMs: 15000
23
+ ---
24
+
25
+ # Cancel Cron Task
26
+
27
+ Permanently delete a cron task from the Crewly backend.
28
+
29
+ ## Usage
30
+
31
+ ```bash
32
+ bash {{ORCHESTRATOR_SKILLS_PATH}}/cancel-cron/execute.sh '{"id":"cron-abc123"}'
33
+ ```
34
+
35
+ ## Parameters
36
+
37
+ | Parameter | Required | Description |
38
+ |-----------|----------|-------------|
39
+ | `id` | Yes | The cron task ID to delete |
40
+
41
+ ## Notes
42
+
43
+ - This permanently removes the cron task. To temporarily stop it, use `update-cron` with `{"enabled": false}` instead.
44
+ - Only the orchestrator or the user can cancel cron tasks.