crewly 1.5.10 → 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.
- package/config/constants.ts +3 -3
- package/config/orchestrator_tasks/prompts/orchestrator-prompt.md +73 -0
- package/config/roles/architect/prompt.md +9 -0
- package/config/roles/backend-developer/prompt.md +9 -0
- package/config/roles/content-strategist/prompt.md +10 -0
- package/config/roles/designer/prompt.md +9 -0
- package/config/roles/developer/prompt.md +9 -0
- package/config/roles/frontend-developer/prompt.md +9 -0
- package/config/roles/fullstack-dev/prompt.md +9 -0
- package/config/roles/generalist/prompt.md +9 -0
- package/config/roles/ops/prompt.md +9 -0
- package/config/roles/product-manager/prompt.md +9 -0
- package/config/roles/qa/prompt.md +9 -0
- package/config/roles/qa-engineer/prompt.md +9 -0
- package/config/roles/researcher/prompt.md +9 -0
- package/config/roles/sales/prompt.md +9 -0
- package/config/roles/support/prompt.md +9 -0
- package/config/roles/team-leader/prompt.md +11 -0
- package/config/roles/tpm/prompt.md +9 -0
- package/config/roles/ux-designer/prompt.md +9 -0
- package/config/skills/agent/core/block-task/execute.sh +3 -1
- package/config/skills/agent/core/pipe-to-sink/execute.sh +41 -0
- package/config/skills/agent/core/read-task/execute.sh +3 -1
- package/config/skills/agent/core/report-progress/execute.sh +3 -1
- package/config/skills/agent/screenshot-compare/SKILL.md +75 -0
- package/config/skills/agent/screenshot-compare/execute.sh +182 -0
- package/config/skills/agent/screenshot-compare/skill.json +10 -0
- package/config/skills/agent/xiaoyuzhoufm-transcript/SKILL.md +85 -0
- package/config/skills/agent/xiaoyuzhoufm-transcript/execute.sh +306 -0
- package/config/skills/agent/xiaoyuzhoufm-transcript/skill.json +10 -0
- package/config/skills/orchestrator/cancel-cron/SKILL.md +44 -0
- package/config/skills/orchestrator/create-cron/SKILL.md +58 -0
- package/config/skills/orchestrator/list-cron/SKILL.md +51 -0
- package/config/skills/orchestrator/update-cron/SKILL.md +52 -0
- package/config/skills/orchestrator/update-team-member/SKILL.md +36 -0
- package/config/skills/orchestrator/update-team-member/execute.sh +25 -0
- package/dist/backend/backend/src/constants.d.ts +7 -4
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +6 -3
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +21 -2
- package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.controller.js +167 -29
- package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/browser/browser.routes.js +7 -3
- package/dist/backend/backend/src/controllers/browser/browser.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/data/data.controller.d.ts +47 -0
- package/dist/backend/backend/src/controllers/data/data.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/data/data.controller.js +201 -0
- package/dist/backend/backend/src/controllers/data/data.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/data/data.routes.d.ts +18 -0
- package/dist/backend/backend/src/controllers/data/data.routes.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/data/data.routes.js +44 -0
- package/dist/backend/backend/src/controllers/data/data.routes.js.map +1 -0
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts +3 -2
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js +5 -3
- package/dist/backend/backend/src/controllers/monitoring/token-usage.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts +4 -0
- package/dist/backend/backend/src/controllers/system/cron-task.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/system/cron-task.controller.js +20 -0
- package/dist/backend/backend/src/controllers/system/cron-task.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js +18 -0
- package/dist/backend/backend/src/controllers/task-management/task-management.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts +32 -0
- package/dist/backend/backend/src/controllers/team/team-export.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/team/team-export.controller.js +61 -0
- package/dist/backend/backend/src/controllers/team/team-export.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js +66 -46
- package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.routes.js +7 -0
- package/dist/backend/backend/src/controllers/team/team.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +37 -7
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +4 -1
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +6 -2
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/idle-detection.service.js +17 -2
- package/dist/backend/backend/src/services/agent/idle-detection.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.d.ts +1 -1
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js +2 -2
- package/dist/backend/backend/src/services/agent/runtime-agent.service.abstract.js.map +1 -1
- package/dist/backend/backend/src/services/agent/task-planning.service.d.ts +134 -0
- package/dist/backend/backend/src/services/agent/task-planning.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/agent/task-planning.service.js +291 -0
- package/dist/backend/backend/src/services/agent/task-planning.service.js.map +1 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js +8 -0
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +3 -3
- package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts +13 -9
- package/dist/backend/backend/src/services/browser/browser-bridge.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js +44 -12
- package/dist/backend/backend/src/services/browser/browser-bridge.service.js.map +1 -1
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts +176 -0
- package/dist/backend/backend/src/services/browser/browser-proxy.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js +441 -0
- package/dist/backend/backend/src/services/browser/browser-proxy.service.js.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts +162 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js +350 -0
- package/dist/backend/backend/src/services/browser/browser-relay-adapter.service.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts +8 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js +27 -0
- package/dist/backend/backend/src/services/cloud/cloud-initializer.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js +2 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.types.js.map +1 -1
- package/dist/backend/backend/src/services/core/team-export.service.d.ts +103 -0
- package/dist/backend/backend/src/services/core/team-export.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/core/team-export.service.js +182 -0
- package/dist/backend/backend/src/services/core/team-export.service.js.map +1 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.d.ts +160 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.js +434 -0
- package/dist/backend/backend/src/services/data/data-object-store.service.js.map +1 -0
- package/dist/backend/backend/src/services/data/data-object.types.d.ts +190 -0
- package/dist/backend/backend/src/services/data/data-object.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/data-object.types.js +143 -0
- package/dist/backend/backend/src/services/data/data-object.types.js.map +1 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.d.ts +108 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.js +290 -0
- package/dist/backend/backend/src/services/data/schema-registry.service.js.map +1 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.d.ts +87 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.js +188 -0
- package/dist/backend/backend/src/services/data/sink-registry.service.js.map +1 -0
- package/dist/backend/backend/src/services/messaging/message-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-router.service.js +7 -0
- package/dist/backend/backend/src/services/messaging/message-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +55 -2
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js +89 -5
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts +105 -14
- package/dist/backend/backend/src/services/workflow/cron-task.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/workflow/cron-task.service.js +400 -123
- package/dist/backend/backend/src/services/workflow/cron-task.service.js.map +1 -1
- package/dist/backend/backend/src/types/cron-task.types.d.ts +1 -1
- package/dist/backend/backend/src/types/data-object.types.d.ts +117 -0
- package/dist/backend/backend/src/types/data-object.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/data-object.types.js +23 -0
- package/dist/backend/backend/src/types/data-object.types.js.map +1 -0
- package/dist/backend/backend/src/types/settings.types.js +1 -1
- package/dist/backend/config/constants.d.ts +3 -3
- package/dist/backend/config/constants.js +3 -3
- package/dist/backend/config/constants.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +7 -4
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +6 -3
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/types/settings.types.js +1 -1
- package/dist/cli/config/constants.d.ts +3 -3
- package/dist/cli/config/constants.js +3 -3
- package/dist/cli/config/constants.js.map +1 -1
- package/frontend/dist/assets/index-371b68d4.css +33 -0
- package/frontend/dist/assets/{index-76782e9e.js → index-506f70da.js} +321 -321
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- 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.
|