crewly 1.20.131 → 1.20.133

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 (186) hide show
  1. package/config/constants.ts +49 -0
  2. package/config/index.ts +2 -0
  3. package/config/skills/agent/core/find-skill/SKILL.md +58 -0
  4. package/config/skills/agent/core/find-skill/execute.sh +67 -0
  5. package/config/skills/agent/core/find-skill/execute.test.sh +69 -0
  6. package/config/skills/agent/core/generate-pdf/SKILL.md +24 -29
  7. package/config/skills/agent/core/generate-pdf/execute.sh +22 -78
  8. package/config/skills/agent/core/generate-pdf/execute.test.sh +45 -0
  9. package/config/skills/agent/core/install-skill/SKILL.md +67 -0
  10. package/config/skills/agent/core/install-skill/execute.sh +80 -0
  11. package/config/skills/agent/core/install-skill/execute.test.sh +74 -0
  12. package/config/skills/agent/pdf-tools/SKILL.md +103 -0
  13. package/config/skills/agent/pdf-tools/execute.sh +301 -0
  14. package/config/skills/agent/pdf-tools/execute.test.sh +218 -0
  15. package/config/skills/agent/pdf-tools/instructions.md +14 -0
  16. package/config/skills/agent/pdf-tools/pdf_tools.py +268 -0
  17. package/config/skills/agent/pdf-tools/skill.json +103 -0
  18. package/config/skills/agent/pdf-tools/style.css +143 -0
  19. package/config/skills/agent/transcribe-audio/SKILL.md +42 -5
  20. package/config/skills/agent/transcribe-audio/execute.sh +44 -11
  21. package/config/skills/agent/transcribe-audio/execute.test.sh +90 -0
  22. package/config/skills/agent/transcribe-audio/install-whisper-cpp.sh +126 -0
  23. package/config/skills/agent/transcribe-audio/install-whisper-cpp.test.sh +69 -0
  24. package/config/skills/agent/transcribe-audio/skill.json +67 -6
  25. package/config/skills/marketplace-registry.ts +8 -1
  26. package/config/skills/registry.json +337 -3
  27. package/dist/backend/backend/src/constants.d.ts +71 -0
  28. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  29. package/dist/backend/backend/src/constants.js +96 -0
  30. package/dist/backend/backend/src/constants.js.map +1 -1
  31. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts +56 -0
  32. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.d.ts.map +1 -0
  33. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js +114 -0
  34. package/dist/backend/backend/src/controllers/cloud/cloud-device-pairing.controller.js.map +1 -0
  35. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +25 -0
  36. package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
  37. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +73 -55
  38. package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
  39. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts +3 -0
  40. package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
  41. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +9 -0
  42. package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
  43. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts +49 -0
  44. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.d.ts.map +1 -0
  45. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js +185 -0
  46. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.controller.js.map +1 -0
  47. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts +21 -0
  48. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.d.ts.map +1 -0
  49. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js +29 -0
  50. package/dist/backend/backend/src/controllers/skill-setup/skill-setup.routes.js.map +1 -0
  51. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  52. package/dist/backend/backend/src/routes/api.routes.js +5 -0
  53. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  54. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  55. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  56. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  57. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  58. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
  59. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
  60. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
  61. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
  62. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts +115 -0
  63. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.d.ts.map +1 -0
  64. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js +160 -0
  65. package/dist/backend/backend/src/services/cloud/cloud-device-pairing.service.js.map +1 -0
  66. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +8 -0
  68. package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -1
  69. package/dist/backend/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -1
  70. package/dist/backend/backend/src/services/marketplace/marketplace.service.js +11 -3
  71. package/dist/backend/backend/src/services/marketplace/marketplace.service.js.map +1 -1
  72. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  73. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  74. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  75. package/dist/backend/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  76. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts +243 -0
  77. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.d.ts.map +1 -0
  78. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js +439 -0
  79. package/dist/backend/backend/src/services/skill-setup/skill-install-job.service.js.map +1 -0
  80. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  81. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  82. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  83. package/dist/backend/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  84. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  85. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  86. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  87. package/dist/backend/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  88. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  89. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  90. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  91. package/dist/backend/backend/src/types/marketplace.types.d.ts +16 -1
  92. package/dist/backend/backend/src/types/marketplace.types.d.ts.map +1 -1
  93. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  94. package/dist/backend/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  95. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js +78 -0
  96. package/dist/backend/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  97. package/dist/backend/build-info.json +2 -2
  98. package/dist/backend/config/constants.d.ts +48 -0
  99. package/dist/backend/config/constants.d.ts.map +1 -1
  100. package/dist/backend/config/constants.js +48 -0
  101. package/dist/backend/config/constants.js.map +1 -1
  102. package/dist/backend/config/index.d.ts +2 -2
  103. package/dist/backend/config/index.d.ts.map +1 -1
  104. package/dist/backend/config/index.js +2 -2
  105. package/dist/backend/config/index.js.map +1 -1
  106. package/dist/cli/backend/src/constants.d.ts +71 -0
  107. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  108. package/dist/cli/backend/src/constants.js +96 -0
  109. package/dist/cli/backend/src/constants.js.map +1 -1
  110. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts +14 -0
  111. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  112. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +33 -1
  113. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  114. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts +132 -0
  115. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.d.ts.map +1 -0
  116. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js +228 -0
  117. package/dist/cli/backend/src/services/cloud/cloud-device-pairing.client.js.map +1 -0
  118. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts +101 -0
  119. package/dist/cli/backend/src/services/marketplace/marketplace.service.d.ts.map +1 -0
  120. package/dist/cli/backend/src/services/marketplace/marketplace.service.js +392 -0
  121. package/dist/cli/backend/src/services/marketplace/marketplace.service.js.map +1 -0
  122. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts +281 -0
  123. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.d.ts.map +1 -0
  124. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js +559 -0
  125. package/dist/cli/backend/src/services/skill-setup/skill-discovery.service.js.map +1 -0
  126. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts +138 -0
  127. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.d.ts.map +1 -0
  128. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js +281 -0
  129. package/dist/cli/backend/src/services/skill-setup/skill-setup-manifest.js.map +1 -0
  130. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts +338 -0
  131. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.d.ts.map +1 -0
  132. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js +761 -0
  133. package/dist/cli/backend/src/services/skill-setup/skill-setup-runner.service.js.map +1 -0
  134. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  135. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +6 -0
  136. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  137. package/dist/cli/backend/src/types/marketplace.types.d.ts +219 -0
  138. package/dist/cli/backend/src/types/marketplace.types.d.ts.map +1 -0
  139. package/dist/cli/backend/src/types/marketplace.types.js +6 -0
  140. package/dist/cli/backend/src/types/marketplace.types.js.map +1 -0
  141. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts +55 -0
  142. package/dist/cli/backend/src/utils/inbound-file-hint.utils.d.ts.map +1 -0
  143. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js +78 -0
  144. package/dist/cli/backend/src/utils/inbound-file-hint.utils.js.map +1 -0
  145. package/dist/cli/backend/src/utils/package-root.d.ts +15 -0
  146. package/dist/cli/backend/src/utils/package-root.d.ts.map +1 -0
  147. package/dist/cli/backend/src/utils/package-root.js +38 -0
  148. package/dist/cli/backend/src/utils/package-root.js.map +1 -0
  149. package/dist/cli/cli/src/commands/cloud.d.ts +45 -25
  150. package/dist/cli/cli/src/commands/cloud.d.ts.map +1 -1
  151. package/dist/cli/cli/src/commands/cloud.js +153 -47
  152. package/dist/cli/cli/src/commands/cloud.js.map +1 -1
  153. package/dist/cli/cli/src/commands/install.d.ts +13 -1
  154. package/dist/cli/cli/src/commands/install.d.ts.map +1 -1
  155. package/dist/cli/cli/src/commands/install.js +40 -3
  156. package/dist/cli/cli/src/commands/install.js.map +1 -1
  157. package/dist/cli/cli/src/commands/onboard-checklist.js +3 -3
  158. package/dist/cli/cli/src/commands/onboard-checklist.js.map +1 -1
  159. package/dist/cli/cli/src/commands/skills.d.ts +58 -0
  160. package/dist/cli/cli/src/commands/skills.d.ts.map +1 -0
  161. package/dist/cli/cli/src/commands/skills.js +132 -0
  162. package/dist/cli/cli/src/commands/skills.js.map +1 -0
  163. package/dist/cli/cli/src/index.js +18 -7
  164. package/dist/cli/cli/src/index.js.map +1 -1
  165. package/dist/cli/cli/src/utils/archive-creator.d.ts +14 -0
  166. package/dist/cli/cli/src/utils/archive-creator.d.ts.map +1 -1
  167. package/dist/cli/cli/src/utils/archive-creator.js +24 -3
  168. package/dist/cli/cli/src/utils/archive-creator.js.map +1 -1
  169. package/dist/cli/cli/src/utils/cloud-submit.d.ts.map +1 -1
  170. package/dist/cli/cli/src/utils/cloud-submit.js +14 -5
  171. package/dist/cli/cli/src/utils/cloud-submit.js.map +1 -1
  172. package/dist/cli/cli/src/utils/package-validator.d.ts +2 -0
  173. package/dist/cli/cli/src/utils/package-validator.d.ts.map +1 -1
  174. package/dist/cli/cli/src/utils/package-validator.js +16 -0
  175. package/dist/cli/cli/src/utils/package-validator.js.map +1 -1
  176. package/dist/cli/config/constants.d.ts +48 -0
  177. package/dist/cli/config/constants.d.ts.map +1 -1
  178. package/dist/cli/config/constants.js +48 -0
  179. package/dist/cli/config/constants.js.map +1 -1
  180. package/dist/cli/config/index.d.ts +2 -2
  181. package/dist/cli/config/index.d.ts.map +1 -1
  182. package/dist/cli/config/index.js +2 -2
  183. package/dist/cli/config/index.js.map +1 -1
  184. package/frontend/dist/assets/{index-5b98bf77.js → index-e6c403b6.js} +265 -265
  185. package/frontend/dist/index.html +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,143 @@
1
+ /*
2
+ * pdf-tools default stylesheet (Crewly, MIT).
3
+ *
4
+ * Clean A4 document typography with CJK-first font stacks: PingFang /
5
+ * Hiragino on macOS, Noto Sans/Serif CJK (fonts-noto-cjk) on Linux,
6
+ * Microsoft YaHei on Windows. Used for Markdown input; HTML input keeps its
7
+ * own styling unless `addDefaultStyle` is set.
8
+ *
9
+ * Page helpers: <div class="page-break"></div> starts a new page;
10
+ * class="keep-together" avoids splitting a block across pages.
11
+ */
12
+
13
+ @page {
14
+ size: A4;
15
+ margin: 18mm 16mm 20mm 16mm;
16
+ }
17
+
18
+ :root {
19
+ --text: #1f2328;
20
+ --muted: #57606a;
21
+ --accent: #2f6feb;
22
+ --rule: #d8dee4;
23
+ --code-bg: #f3f4f6;
24
+ --table-head: #f0f3f6;
25
+ --zebra: #fafbfc;
26
+ --sans: "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Noto Sans SC", "Source Han Sans SC",
27
+ "Microsoft YaHei", "WenQuanYi Micro Hei", -apple-system, "Helvetica Neue", Arial, sans-serif;
28
+ --serif: "Songti SC", "Noto Serif CJK SC", "Noto Serif SC", "Source Han Serif SC", Georgia, serif;
29
+ --mono: "SF Mono", Menlo, "JetBrains Mono", "Noto Sans Mono CJK SC", Consolas, monospace;
30
+ }
31
+
32
+ html {
33
+ -webkit-print-color-adjust: exact;
34
+ print-color-adjust: exact;
35
+ }
36
+
37
+ body {
38
+ font-family: var(--sans);
39
+ font-size: 10.5pt;
40
+ line-height: 1.7;
41
+ color: var(--text);
42
+ margin: 0;
43
+ text-align: justify;
44
+ word-break: normal;
45
+ overflow-wrap: anywhere;
46
+ }
47
+
48
+ main.doc {
49
+ max-width: 100%;
50
+ }
51
+
52
+ h1, h2, h3, h4, h5 {
53
+ font-family: var(--sans);
54
+ font-weight: 650;
55
+ line-height: 1.35;
56
+ color: #111;
57
+ break-after: avoid;
58
+ page-break-after: avoid;
59
+ }
60
+
61
+ h1 {
62
+ font-size: 20pt;
63
+ margin: 0 0 10pt;
64
+ padding-bottom: 6pt;
65
+ border-bottom: 2px solid var(--text);
66
+ }
67
+
68
+ h2 {
69
+ font-size: 14.5pt;
70
+ margin: 18pt 0 8pt;
71
+ padding-bottom: 3pt;
72
+ border-bottom: 1px solid var(--rule);
73
+ }
74
+
75
+ h3 { font-size: 12pt; margin: 14pt 0 6pt; }
76
+ h4 { font-size: 10.5pt; margin: 12pt 0 4pt; color: var(--muted); }
77
+
78
+ p { margin: 0 0 8pt; }
79
+
80
+ a { color: var(--accent); text-decoration: none; }
81
+
82
+ ul, ol { margin: 0 0 8pt; padding-left: 18pt; }
83
+ li { margin: 2pt 0; }
84
+
85
+ blockquote {
86
+ margin: 10pt 0;
87
+ padding: 6pt 12pt;
88
+ border-left: 3px solid var(--accent);
89
+ background: #f5f8ff;
90
+ color: #333;
91
+ }
92
+
93
+ blockquote > :last-child { margin-bottom: 0; }
94
+
95
+ code {
96
+ font-family: var(--mono);
97
+ font-size: 0.9em;
98
+ background: var(--code-bg);
99
+ padding: 1pt 4pt;
100
+ border-radius: 3px;
101
+ }
102
+
103
+ pre {
104
+ font-family: var(--mono);
105
+ font-size: 8.5pt;
106
+ line-height: 1.5;
107
+ background: var(--code-bg);
108
+ padding: 8pt 10pt;
109
+ border-radius: 4px;
110
+ white-space: pre-wrap;
111
+ break-inside: avoid;
112
+ }
113
+
114
+ pre code { background: none; padding: 0; }
115
+
116
+ table {
117
+ width: 100%;
118
+ border-collapse: collapse;
119
+ margin: 10pt 0;
120
+ font-size: 9.5pt;
121
+ break-inside: auto;
122
+ }
123
+
124
+ th, td {
125
+ border: 1px solid var(--rule);
126
+ padding: 4pt 7pt;
127
+ text-align: left;
128
+ vertical-align: top;
129
+ }
130
+
131
+ th { background: var(--table-head); font-weight: 600; }
132
+ tr:nth-child(even) td { background: var(--zebra); }
133
+ thead { display: table-header-group; }
134
+ tr { break-inside: avoid; }
135
+
136
+ img { max-width: 100%; }
137
+
138
+ hr { border: none; border-top: 1px solid var(--rule); margin: 16pt 0; }
139
+
140
+ .page-break { break-after: page; page-break-after: always; }
141
+ .keep-together { break-inside: avoid; page-break-inside: avoid; }
142
+ .muted { color: var(--muted); }
143
+ .serif { font-family: var(--serif); }
@@ -1,19 +1,34 @@
1
1
  ---
2
2
  name: transcribe-audio
3
- description: Transcribe a local audio (or video) file to text with timestamps using Whisper. Defaults to local whisper.cpp (free, offline, word/segment-level timestamps) and automatically falls back to the OpenAI Whisper API (whisper-1) when the local engine is not installed. Any agent can call it for meetings, podcasts, voice notes, interviews, or video audio tracks.
4
- category: content
3
+ description: Transcribe a local audio (or video) file — Slack voice messages / audio clips, meetings, podcasts, interviews — to text with timestamps using Whisper. Defaults to local whisper.cpp (free, offline) and falls back to the OpenAI Whisper API (whisper-1) when the local engine is not installed. When a dependency is missing it returns `needsSetup:true` — run `install-skill --id transcribe-audio` instead of telling the user it cannot be done.
4
+ category: content-creation
5
5
  assignableRoles:
6
6
  - "*"
7
- version: "1.0.0"
7
+ version: "1.1.0"
8
+ author: Crewly Team
8
9
  tags:
9
10
  - audio
11
+ - voice
12
+ - voice-message
13
+ - speech-to-text
10
14
  - transcribe
11
15
  - transcription
12
16
  - whisper
13
17
  - whisper.cpp
14
18
  - openai
15
- - speech-to-text
16
19
  - timestamps
20
+ - m4a
21
+ - mp3
22
+ - video
23
+ - 语音
24
+ - 录音
25
+ - 转文字
26
+ triggers:
27
+ - transcribe audio
28
+ - transcribe voice message
29
+ - speech to text
30
+ - what does this recording say
31
+ - audio clip
17
32
  ---
18
33
 
19
34
  # Transcribe Audio (Whisper)
@@ -91,10 +106,32 @@ downstream consumers have a stable shape).
91
106
 
92
107
  ## Dependencies & Setup
93
108
 
109
+ **Automatic:** everything below is declared in `skill.json` → `setup`. Run
110
+ `install-skill --id transcribe-audio` (agents) or `crewly skills setup transcribe-audio`
111
+ (terminal) and Crewly installs what is missing — ffmpeg, whisper.cpp and the
112
+ 547 MB model (sha256-verified) — and reports "already satisfied" for the rest.
113
+ On Linux, whisper.cpp comes from the official prebuilt Ubuntu release binaries
114
+ (`install-whisper-cpp.sh`, pinned + sha256-checked) with a cmake source build as
115
+ the fallback; it lands in `~/.crewly/bin/whisper-cli`.
116
+
117
+ **When something is missing** the skill fails with machine-readable JSON:
118
+
119
+ ```json
120
+ {"success":false,"error":"ffmpeg is required but not installed","needsSetup":true,
121
+ "skill":"transcribe-audio","missing":["ffmpeg"],"hint":"Run install-skill --id transcribe-audio …"}
122
+ ```
123
+
124
+ On `needsSetup:true`, tell the user in one line that you are installing the
125
+ transcription skill (a few minutes), run `install-skill --id transcribe-audio`,
126
+ and continue when the `[SKILL INSTALLED]` message arrives. Never just reply
127
+ "whisper.cpp is not installed".
128
+
129
+ Manual details:
130
+
94
131
  - **`ffmpeg`** — required for both engines (audio is normalized to 16 kHz mono WAV).
95
132
  Install: `brew install ffmpeg`.
96
133
  - **Local engine (`whisper.cpp`)** — needs the `whisper-cli` binary and a model file:
97
- - Binary: `brew install whisper-cpp` (provides `whisper-cli`). Override with `FLOPOST_WHISPER_BIN`.
134
+ - Binary: `brew install whisper-cpp` (provides `whisper-cli`); on Linux `~/.crewly/bin/whisper-cli`. Override with `FLOPOST_WHISPER_BIN`.
98
135
  - Model: `ggml-large-v3-turbo-q5_0.bin` in `~/.flopost/whisper/` or `~/.cache/whisper-models/`. Override with `FLOPOST_WHISPER_MODEL`.
99
136
  - If the binary or model is missing, the skill falls back to OpenAI (or reports a clear hint when `engine:"local"` is forced).
100
137
  - **OpenAI engine** — needs an OpenAI API key. Resolution order:
@@ -15,6 +15,10 @@
15
15
  # bash execute.sh '{"audioFile":"/x.wav","engine":"openai"}'
16
16
  #
17
17
  # Requires: ffmpeg, jq, curl. Local engine also needs whisper-cli + a model file.
18
+ # All of it is declared in skill.json's `setup` block. When something is
19
+ # missing the error JSON carries `"needsSetup": true, "skill": "transcribe-audio"`
20
+ # so an agent can go straight to `install-skill --id transcribe-audio`
21
+ # instead of telling the user "whisper.cpp is not installed".
18
22
 
19
23
  set -euo pipefail
20
24
 
@@ -25,14 +29,29 @@ WHISPER_CACHE_DIR="${HOME}/.cache/whisper-models"
25
29
  OPENAI_TRANSCRIBE_URL="https://api.openai.com/v1/audio/transcriptions"
26
30
  OPENAI_MODEL="whisper-1"
27
31
  SETTINGS_URL="${CREWLY_API_URL:-http://localhost:${WEB_PORT:-8787}}/api/settings"
32
+ SKILL_ID="transcribe-audio"
33
+ # Crewly-managed binaries (the Linux whisper.cpp install lands here).
34
+ CREWLY_BIN_DIR="${CREWLY_HOME:-${HOME}/.crewly}/bin"
28
35
 
29
36
  err_json() { printf '{"success":false,"error":%s}\n' "$(printf '%s' "$1" | jq -Rsa .)"; exit 1; }
30
37
 
38
+ # needs_setup_json <message> <missing...> — a failure an install can fix.
39
+ # Works without jq (jq itself may be the missing piece).
40
+ needs_setup_json() {
41
+ local msg="$1"; shift
42
+ local missing="" item esc
43
+ for item in "$@"; do missing="${missing:+${missing},}\"${item}\""; done
44
+ esc=$(printf '%s' "$msg" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' | tr '\n' ' ')
45
+ printf '{"success":false,"error":"%s","needsSetup":true,"skill":"%s","missing":[%s],"hint":"Run install-skill --id %s (it installs the missing pieces in the background and messages you when done), then retry."}\n' \
46
+ "$esc" "$SKILL_ID" "$missing" "$SKILL_ID"
47
+ exit 1
48
+ }
49
+
31
50
  # ── Input parsing ───────────────────────────────────────────────────────────────
32
51
  INPUT="${1:-}"
33
52
  [ -z "$INPUT" ] && err_json "Usage: execute.sh '{\"audioFile\":\"/path/to/audio.m4a\"}'"
34
53
 
35
- command -v jq >/dev/null 2>&1 || { echo '{"success":false,"error":"jq is required but not installed (brew install jq)"}'; exit 1; }
54
+ command -v jq >/dev/null 2>&1 || needs_setup_json "jq is required but not installed" jq
36
55
 
37
56
  AUDIO_FILE=$(printf '%s' "$INPUT" | jq -r '.audioFile // empty')
38
57
  OUTPUT_FILE=$(printf '%s' "$INPUT" | jq -r '.outputFile // empty')
@@ -43,14 +62,18 @@ ENGINE=$(printf '%s' "$INPUT" | jq -r '.engine // "auto"')
43
62
  [ -f "$AUDIO_FILE" ] || err_json "audioFile not found: ${AUDIO_FILE}"
44
63
  case "$ENGINE" in auto|local|openai) ;; *) err_json "engine must be one of: auto, local, openai (got: ${ENGINE})";; esac
45
64
 
46
- command -v ffmpeg >/dev/null 2>&1 || err_json "ffmpeg is required but not installed (brew install ffmpeg)"
65
+ command -v ffmpeg >/dev/null 2>&1 || needs_setup_json "ffmpeg is required but not installed" ffmpeg
47
66
 
48
67
  # ── Local whisper.cpp detection (lifted from whisperModule.ts) ───────────────────
49
68
  resolve_whisper_binary() {
50
69
  if [ -n "${FLOPOST_WHISPER_BIN:-}" ] && [ -x "${FLOPOST_WHISPER_BIN}" ]; then echo "${FLOPOST_WHISPER_BIN}"; return; fi
51
70
  if [ -x "${FLOPOST_WHISPER_DIR}/whisper-cli" ]; then echo "${FLOPOST_WHISPER_DIR}/whisper-cli"; return; fi
71
+ if [ -x "${CREWLY_BIN_DIR}/whisper-cli" ]; then echo "${CREWLY_BIN_DIR}/whisper-cli"; return; fi
52
72
  if command -v whisper-cli >/dev/null 2>&1; then command -v whisper-cli; return; fi
53
- for p in /opt/homebrew/bin/whisper-cli /usr/local/bin/whisper-cli; do
73
+ # Homebrew locations (a backend started by launchd may lack them on PATH).
74
+ # TRANSCRIBE_WHISPER_BIN_CANDIDATES overrides the list (tests).
75
+ local candidates="${TRANSCRIBE_WHISPER_BIN_CANDIDATES-/opt/homebrew/bin/whisper-cli /usr/local/bin/whisper-cli}"
76
+ for p in $candidates; do
54
77
  [ -x "$p" ] && { echo "$p"; return; }
55
78
  done
56
79
  echo ""
@@ -61,11 +84,10 @@ resolve_whisper_model() {
61
84
  if [ -f "${WHISPER_CACHE_DIR}/${MODEL_FILENAME}" ]; then echo "${WHISPER_CACHE_DIR}/${MODEL_FILENAME}"; return; fi
62
85
  echo ""
63
86
  }
64
- binary_hint() {
65
- case "$(uname -s)" in
66
- Darwin) echo "brew install whisper-cpp" ;;
67
- *) echo "Build whisper.cpp from https://github.com/ggerganov/whisper.cpp and put whisper-cli on PATH" ;;
68
- esac
87
+ # Names of the local-engine pieces that are missing (for needsSetup JSON).
88
+ missing_local_parts() {
89
+ [ -n "$WHISPER_BIN" ] || printf '%s\n' whisper-cli
90
+ [ -n "$WHISPER_MODEL" ] || printf '%s\n' whisper-model
69
91
  }
70
92
 
71
93
  WHISPER_BIN="$(resolve_whisper_binary)"
@@ -82,7 +104,10 @@ resolve_openai_key() {
82
104
  # ── Choose engine ────────────────────────────────────────────────────────────────
83
105
  CHOSEN=""
84
106
  if [ "$ENGINE" = "local" ]; then
85
- [ "$LOCAL_AVAILABLE" = true ] || err_json "engine=local requested but whisper.cpp is not installed. Binary: $( [ -n "$WHISPER_BIN" ] && echo found || echo "missing ($(binary_hint))" ); Model: $( [ -n "$WHISPER_MODEL" ] && echo found || echo "missing ${MODEL_FILENAME} (put it in ${FLOPOST_WHISPER_DIR}/ or set FLOPOST_WHISPER_MODEL)" )."
107
+ if [ "$LOCAL_AVAILABLE" != true ]; then
108
+ # shellcheck disable=SC2046 # word-splitting the list of missing parts is intended
109
+ needs_setup_json "engine=local requested but the local whisper.cpp engine is not set up (binary: $( [ -n "$WHISPER_BIN" ] && echo found || echo missing ); model ${MODEL_FILENAME}: $( [ -n "$WHISPER_MODEL" ] && echo found || echo missing ))" $(missing_local_parts)
110
+ fi
86
111
  CHOSEN="local"
87
112
  elif [ "$ENGINE" = "openai" ]; then
88
113
  CHOSEN="openai"
@@ -138,7 +163,7 @@ run_local() {
138
163
  # ── Engine: OpenAI Whisper API ───────────────────────────────────────────────────
139
164
  run_openai() {
140
165
  local key; key="$(resolve_openai_key)"
141
- [ -z "$key" ] && { fail "OpenAI engine selected but no OpenAI API key found. Set OPENAI_API_KEY (Crewly secrets) or configure Settings > API Keys."; return 1; }
166
+ [ -z "$key" ] && { fail "OpenAI engine selected but no OpenAI API key found. Set OPENAI_API_KEY (Crewly secrets) or configure Settings > API Keys."; : > "${WORK}/no-openai-key"; return 1; }
142
167
  echo "{\"status\":\"transcribing\",\"engine\":\"openai-whisper-1\",\"message\":\"Uploading to OpenAI Whisper API...\"}" >&2
143
168
  local resp="${WORK}/openai.json"
144
169
  local lang_args=(); [ "$LANGUAGE" != "auto" ] && lang_args=(-F "language=${LANGUAGE}")
@@ -179,7 +204,15 @@ if [ "$CHOSEN" = "local" ]; then
179
204
  err_json "$(cat "${WORK}/engine.err" 2>/dev/null)"
180
205
  fi
181
206
  else
182
- run_openai || err_json "$(cat "${WORK}/engine.err" 2>/dev/null)"
207
+ if ! run_openai; then
208
+ # auto mode with no local engine and no OpenAI key: installing the local
209
+ # engine fixes it, so say so in a way an agent can act on.
210
+ if [ "$ENGINE" = "auto" ] && [ -f "${WORK}/no-openai-key" ]; then
211
+ # shellcheck disable=SC2046
212
+ needs_setup_json "No transcription engine is available: the local whisper.cpp engine is not set up and there is no OpenAI API key" $(missing_local_parts)
213
+ fi
214
+ err_json "$(cat "${WORK}/engine.err" 2>/dev/null)"
215
+ fi
183
216
  fi
184
217
 
185
218
  [ -f "$RESULT_FILE" ] || err_json "Transcription produced no result"
@@ -0,0 +1,90 @@
1
+ #!/bin/bash
2
+ # Tests for transcribe-audio — run with: bash execute.test.sh </dev/null
3
+ # Uses fake ffmpeg / ffprobe / whisper-cli on an isolated PATH and HOME, so it
4
+ # never touches a real model or the network. Exit 0 on pass.
5
+ set -u
6
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ EXEC="$HERE/execute.sh"
8
+ PASS=0; FAIL=0
9
+ check() {
10
+ local name="$1"; local got="$2"; local want="$3"
11
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
12
+ }
13
+
14
+ JQ="$(command -v jq)"
15
+ BASH_BIN="${TEST_BASH:-$(command -v bash)}"
16
+ [ -n "$JQ" ] || { echo "jq is required to run these tests"; exit 1; }
17
+ T="$(mktemp -d)"
18
+ trap 'rm -rf "$T"' EXIT
19
+ mkdir -p "$T/bin" "$T/home"
20
+ # Minimal PATH: basic tools + jq, but no ffmpeg / whisper-cli.
21
+ for tool in bash jq mktemp cat rm tail dirname mkdir sed tr sysctl nproc uname basename; do
22
+ src="$(command -v "$tool" 2>/dev/null)" && ln -sf "$src" "$T/bin/$tool"
23
+ done
24
+ echo "fake audio" > "$T/clip.m4a"
25
+
26
+ run() {
27
+ env -i HOME="$T/home" PATH="$1" CREWLY_HOME="$T/home/.crewly" CREWLY_API_URL="http://127.0.0.1:9" \
28
+ TRANSCRIBE_WHISPER_BIN_CANDIDATES="" "$BASH_BIN" "$EXEC" "$2" 2>/dev/null </dev/null
29
+ }
30
+
31
+ # 1. ffmpeg missing → needsSetup, machine-readable.
32
+ OUT=$(run "$T/bin" "{\"audioFile\":\"$T/clip.m4a\"}"); RC=$?
33
+ check "no ffmpeg: exit 1" "$RC" "1"
34
+ check "no ffmpeg: needsSetup" "$(printf '%s' "$OUT" | "$JQ" -r .needsSetup)" "true"
35
+ check "no ffmpeg: skill" "$(printf '%s' "$OUT" | "$JQ" -r .skill)" "transcribe-audio"
36
+ check "no ffmpeg: missing" "$(printf '%s' "$OUT" | "$JQ" -c .missing)" '["ffmpeg"]'
37
+ check "no ffmpeg: hint names install-skill" "$(printf '%s' "$OUT" | "$JQ" -r '.hint | test("install-skill --id transcribe-audio")')" "true"
38
+
39
+ # Fake ffmpeg (writes the last argument, the WAV) and ffprobe.
40
+ cat > "$T/bin/ffmpeg" <<'EOF'
41
+ #!/bin/bash
42
+ for a in "$@"; do last="$a"; done
43
+ echo "RIFF" > "$last"
44
+ EOF
45
+ printf '#!/bin/bash\necho 1.5\n' > "$T/bin/ffprobe"
46
+ chmod 755 "$T/bin/ffmpeg" "$T/bin/ffprobe"
47
+
48
+ # 2. engine=local without whisper-cli or model → needsSetup listing both.
49
+ OUT=$(run "$T/bin" "{\"audioFile\":\"$T/clip.m4a\",\"engine\":\"local\"}"); RC=$?
50
+ check "local missing: exit 1" "$RC" "1"
51
+ check "local missing: needsSetup" "$(printf '%s' "$OUT" | "$JQ" -r .needsSetup)" "true"
52
+ check "local missing: missing parts" "$(printf '%s' "$OUT" | "$JQ" -c .missing)" '["whisper-cli","whisper-model"]'
53
+
54
+ # 3. auto engine, no local engine, no OpenAI key → needsSetup (not a bare error).
55
+ ln -sf "$(command -v curl)" "$T/bin/curl"
56
+ OUT=$(run "$T/bin" "{\"audioFile\":\"$T/clip.m4a\"}"); RC=$?
57
+ check "auto no engine: exit 1" "$RC" "1"
58
+ check "auto no engine: needsSetup" "$(printf '%s' "$OUT" | "$JQ" -r .needsSetup)" "true"
59
+ check "auto no engine: message" "$(printf '%s' "$OUT" | "$JQ" -r '.error | test("no OpenAI API key")')" "true"
60
+
61
+ # 4. engine=openai explicitly without a key → plain error, no needsSetup (installing does not help).
62
+ OUT=$(run "$T/bin" "{\"audioFile\":\"$T/clip.m4a\",\"engine\":\"openai\"}")
63
+ check "openai no key: not needsSetup" "$(printf '%s' "$OUT" | "$JQ" -r '.needsSetup // false')" "false"
64
+
65
+ # 5. Everything present (whisper-cli in $CREWLY_HOME/bin, model in ~/.cache) → transcript.
66
+ mkdir -p "$T/home/.crewly/bin" "$T/home/.cache/whisper-models"
67
+ : > "$T/home/.cache/whisper-models/ggml-large-v3-turbo-q5_0.bin"
68
+ cat > "$T/home/.crewly/bin/whisper-cli" <<'EOF'
69
+ #!/bin/bash
70
+ while [ $# -gt 0 ]; do case "$1" in -of) prefix="$2"; shift 2 ;; *) shift ;; esac; done
71
+ printf '{"result":{"language":"zh"},"transcription":[{"offsets":{"from":0,"to":1500},"text":" 你好,今天开会"},{"offsets":{"from":1500,"to":1600},"text":" [BLANK_AUDIO]"}]}' > "${prefix}.json"
72
+ EOF
73
+ chmod 755 "$T/home/.crewly/bin/whisper-cli"
74
+ OUT=$(run "$T/bin" "{\"audioFile\":\"$T/clip.m4a\",\"outputFile\":\"$T/out.md\"}"); RC=$?
75
+ check "local ok: exit 0" "$RC" "0"
76
+ check "local ok: success" "$(printf '%s' "$OUT" | "$JQ" -r .success)" "true"
77
+ check "local ok: engine" "$(printf '%s' "$OUT" | "$JQ" -r .engine)" "whisper.cpp"
78
+ check "local ok: text" "$(printf '%s' "$OUT" | "$JQ" -r .text)" "你好,今天开会"
79
+ check "local ok: blank tag dropped" "$(printf '%s' "$OUT" | "$JQ" -r .segmentCount)" "1"
80
+ check "local ok: markdown written" "$(grep -c '你好,今天开会' "$T/out.md")" "2"
81
+
82
+ # 6. The shipped setup block is valid JSON and names the skill's own install script.
83
+ check "setup: whisper linux script" "$("$JQ" -r '.setup.steps[] | select(.id=="whisper-cli") | .install.linux.script' "$HERE/skill.json")" "install-whisper-cpp.sh"
84
+ [ -f "$HERE/install-whisper-cpp.sh" ] && OK=yes || OK=no
85
+ check "setup: install script exists" "$OK" "yes"
86
+ bash -n "$HERE/install-whisper-cpp.sh" && OK=yes || OK=no
87
+ check "setup: install script parses" "$OK" "yes"
88
+
89
+ echo "=== Results: $PASS passed, $FAIL failed ==="
90
+ [ "$FAIL" -eq 0 ]
@@ -0,0 +1,126 @@
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # install-whisper-cpp.sh — put a working `whisper-cli` in $CREWLY_BIN_DIR on Linux.
4
+ #
5
+ # Run by the Crewly skill-setup runner (the `whisper-cli` step of this skill's
6
+ # setup block) when whisper-cli is not already installed. Never prompts.
7
+ #
8
+ # 1. Prebuilt: the official whisper.cpp release ships Ubuntu binaries
9
+ # (whisper-bin-ubuntu-{x64,arm64}.tar.gz). The pinned tarball is downloaded,
10
+ # its sha256 checked, extracted to $CREWLY_HOME/opt/whisper.cpp-<tag>/, and a
11
+ # wrapper $CREWLY_BIN_DIR/whisper-cli is written that sets LD_LIBRARY_PATH to
12
+ # the bundled shared libraries. A smoke test (`whisper-cli --help`) decides
13
+ # whether the binary runs here (it needs a recent enough glibc).
14
+ # 2. Source: when there is no prebuilt for this CPU, or it does not run, the
15
+ # same tag is built with cmake into $CREWLY_HOME/opt/ and whisper-cli copied
16
+ # to $CREWLY_BIN_DIR. Needs cmake, a C++ compiler, curl and tar; when they
17
+ # are missing and root/passwordless sudo is available ($CREWLY_SUDO), they
18
+ # are installed with apt-get, otherwise the script says what to install.
19
+ #
20
+ # Environment (set by the runner):
21
+ # CREWLY_HOME Crewly home (default ~/.crewly)
22
+ # CREWLY_BIN_DIR where whisper-cli goes (default $CREWLY_HOME/bin)
23
+ # CREWLY_SUDO '' = root, 'sudo -n' = passwordless sudo, 'unavailable'
24
+ # WHISPER_CPP_SKIP_PREBUILT=1 force the source build (testing)
25
+ # WHISPER_CPP_DOWNLOAD_BASE override the download host (testing)
26
+ # =============================================================================
27
+ set -euo pipefail
28
+
29
+ # ── Pinned release (sha256 from the GitHub release asset digests) ─────────────
30
+ WHISPER_TAG="v1.9.2"
31
+ SHA256_UBUNTU_X64="46811a3ecf584307480a220b9ef5ff81b7b22dc41577cbc274ce3afc61f753b1"
32
+ SHA256_UBUNTU_ARM64="7e26fa6a36d9174d5c0bf033ccbc026c3b5e569e2ee787058241346ef5392719"
33
+ DOWNLOAD_BASE="${WHISPER_CPP_DOWNLOAD_BASE:-https://github.com/ggml-org/whisper.cpp}"
34
+ BUILD_JOBS="$(nproc 2>/dev/null || echo 2)"
35
+
36
+ CREWLY_HOME="${CREWLY_HOME:-${HOME}/.crewly}"
37
+ BIN_DIR="${CREWLY_BIN_DIR:-${CREWLY_HOME}/bin}"
38
+ OPT_DIR="${CREWLY_HOME}/opt"
39
+ SUDO_MODE="${CREWLY_SUDO-unavailable}"
40
+
41
+ log() { printf '[install-whisper-cpp] %s\n' "$*" >&2; }
42
+ die() { log "ERROR: $*"; exit 1; }
43
+
44
+ [ "$(uname -s)" = "Linux" ] || die "this installer is for Linux (on macOS the setup uses: brew install whisper-cpp)"
45
+ mkdir -p "$BIN_DIR" "$OPT_DIR"
46
+
47
+ WORK="$(mktemp -d "${TMPDIR:-/tmp}/whisper-cpp-XXXXXX")"
48
+ trap 'rm -rf "$WORK"' EXIT
49
+
50
+ sha256_of() {
51
+ if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1
52
+ else shasum -a 256 "$1" | cut -d' ' -f1; fi
53
+ }
54
+
55
+ # Write the wrapper that makes the bundled .so files findable.
56
+ write_wrapper() {
57
+ local real="$1" libdir="$2"
58
+ cat > "${BIN_DIR}/whisper-cli" <<EOF
59
+ #!/bin/sh
60
+ # Crewly wrapper for whisper.cpp ${WHISPER_TAG} (written by install-whisper-cpp.sh)
61
+ LD_LIBRARY_PATH="${libdir}\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}" exec "${real}" "\$@"
62
+ EOF
63
+ chmod 755 "${BIN_DIR}/whisper-cli"
64
+ }
65
+
66
+ smoke_test() { "${BIN_DIR}/whisper-cli" --help >/dev/null 2>&1; }
67
+
68
+ try_prebuilt() {
69
+ local arch asset sha
70
+ arch="$(uname -m)"
71
+ case "$arch" in
72
+ x86_64|amd64) asset="whisper-bin-ubuntu-x64.tar.gz"; sha="$SHA256_UBUNTU_X64" ;;
73
+ aarch64|arm64) asset="whisper-bin-ubuntu-arm64.tar.gz"; sha="$SHA256_UBUNTU_ARM64" ;;
74
+ *) log "no prebuilt binary for ${arch}"; return 1 ;;
75
+ esac
76
+ local url="${DOWNLOAD_BASE}/releases/download/${WHISPER_TAG}/${asset}"
77
+ log "downloading ${url}"
78
+ curl -fsSL --retry 3 -o "${WORK}/${asset}" "$url" || { log "download failed"; return 1; }
79
+ local got; got="$(sha256_of "${WORK}/${asset}")"
80
+ [ "$got" = "$sha" ] || { log "checksum mismatch for ${asset}: expected ${sha}, got ${got}"; return 1; }
81
+ local dest="${OPT_DIR}/whisper.cpp-${WHISPER_TAG}"
82
+ rm -rf "$dest"; mkdir -p "$dest"
83
+ tar -xzf "${WORK}/${asset}" -C "$dest" --strip-components=1
84
+ [ -x "${dest}/whisper-cli" ] || { log "whisper-cli not found in ${asset}"; return 1; }
85
+ write_wrapper "${dest}/whisper-cli" "$dest"
86
+ smoke_test || { log "the prebuilt whisper-cli does not run on this system (glibc too old?)"; return 1; }
87
+ log "installed prebuilt whisper.cpp ${WHISPER_TAG} → ${BIN_DIR}/whisper-cli"
88
+ }
89
+
90
+ ensure_build_tools() {
91
+ local missing=()
92
+ command -v cmake >/dev/null 2>&1 || missing+=(cmake)
93
+ { command -v c++ >/dev/null 2>&1 || command -v g++ >/dev/null 2>&1; } || missing+=(build-essential)
94
+ command -v curl >/dev/null 2>&1 || missing+=(curl)
95
+ [ ${#missing[@]} -eq 0 ] && return 0
96
+ if [ "$SUDO_MODE" = "unavailable" ] || ! command -v apt-get >/dev/null 2>&1; then
97
+ die "building whisper.cpp needs: ${missing[*]}. Install them (e.g. sudo apt-get install -y ${missing[*]}) and run the setup again."
98
+ fi
99
+ log "installing build tools: ${missing[*]}"
100
+ # shellcheck disable=SC2086 # SUDO_MODE is '' or 'sudo -n'
101
+ $SUDO_MODE env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${missing[@]}" </dev/null \
102
+ || { $SUDO_MODE apt-get update </dev/null && $SUDO_MODE env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${missing[@]}" </dev/null; } \
103
+ || die "apt-get could not install ${missing[*]}"
104
+ }
105
+
106
+ build_from_source() {
107
+ ensure_build_tools
108
+ local url="${DOWNLOAD_BASE}/archive/refs/tags/${WHISPER_TAG}.tar.gz"
109
+ log "building whisper.cpp ${WHISPER_TAG} from source (${url})"
110
+ curl -fsSL --retry 3 -o "${WORK}/src.tar.gz" "$url" || die "could not download ${url}"
111
+ mkdir -p "${WORK}/src"
112
+ tar -xzf "${WORK}/src.tar.gz" -C "${WORK}/src" --strip-components=1
113
+ cmake -S "${WORK}/src" -B "${WORK}/build" -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DWHISPER_BUILD_TESTS=OFF >&2
114
+ cmake --build "${WORK}/build" --config Release --target whisper-cli -j "$BUILD_JOBS" >&2
115
+ local dest="${OPT_DIR}/whisper.cpp-${WHISPER_TAG}-src"
116
+ rm -rf "$dest"; mkdir -p "$dest"
117
+ cp "${WORK}/build/bin/whisper-cli" "${dest}/whisper-cli"
118
+ write_wrapper "${dest}/whisper-cli" "$dest"
119
+ smoke_test || die "the whisper-cli built from source does not run"
120
+ log "built whisper.cpp ${WHISPER_TAG} → ${BIN_DIR}/whisper-cli"
121
+ }
122
+
123
+ if [ "${WHISPER_CPP_SKIP_PREBUILT:-0}" != "1" ] && try_prebuilt; then
124
+ exit 0
125
+ fi
126
+ build_from_source
@@ -0,0 +1,69 @@
1
+ #!/bin/bash
2
+ # Tests for install-whisper-cpp.sh — run with: bash install-whisper-cpp.test.sh </dev/null
3
+ # Fakes uname (Linux x86_64), curl (serves a crafted release tarball) and
4
+ # sha256sum on an isolated PATH; nothing is downloaded or built. Exit 0 on pass.
5
+ set -u
6
+ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ SCRIPT="$HERE/install-whisper-cpp.sh"
8
+ PASS=0; FAIL=0
9
+ check() {
10
+ local name="$1"; local got="$2"; local want="$3"
11
+ if [ "$got" = "$want" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $name"; echo " want: $want"; echo " got: $got"; fi
12
+ }
13
+ BASH_BIN="${TEST_BASH:-$(command -v bash)}"
14
+ T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
15
+ mkdir -p "$T/bin" "$T/pkg/whisper-bin-ubuntu-x64"
16
+ for tool in mktemp cat rm tar mkdir chmod cp cut dirname grep sed head; do
17
+ src="$(command -v "$tool" 2>/dev/null)" && ln -sf "$src" "$T/bin/$tool"
18
+ done
19
+ EXPECTED_SHA="$(grep '^SHA256_UBUNTU_X64=' "$SCRIPT" | cut -d'"' -f2)"
20
+
21
+ # The release tarball: whisper-cli that answers --help.
22
+ printf '#!/bin/sh\n[ "$1" = "--help" ] && echo "usage: whisper-cli" && exit 0\necho "ran $*"\n' > "$T/pkg/whisper-bin-ubuntu-x64/whisper-cli"
23
+ chmod 755 "$T/pkg/whisper-bin-ubuntu-x64/whisper-cli"
24
+ (cd "$T/pkg" && tar -czf "$T/release.tar.gz" whisper-bin-ubuntu-x64)
25
+
26
+ printf '#!/bin/sh\ncase "$1" in -s) echo Linux ;; -m) echo x86_64 ;; *) echo Linux ;; esac\n' > "$T/bin/uname"
27
+ printf '#!/bin/sh\necho 4\n' > "$T/bin/nproc"
28
+ # curl -fsSL --retry 3 -o <out> <url>: log the URL, copy the tarball.
29
+ cat > "$T/bin/curl" <<EOT
30
+ #!/bin/sh
31
+ while [ \$# -gt 0 ]; do case "\$1" in -o) out="\$2"; shift 2 ;; -*) shift ;; *) url="\$1"; shift ;; esac; done
32
+ echo "\$url" >> "$T/curl.log"
33
+ cp "$T/release.tar.gz" "\$out"
34
+ EOT
35
+ cat > "$T/bin/sha256sum" <<'EOT'
36
+ #!/bin/sh
37
+ echo "${FAKE_SHA} $1"
38
+ EOT
39
+ chmod 755 "$T/bin/"*
40
+
41
+ run() { env -i HOME="$T/home" PATH="$T/bin" CREWLY_HOME="$T/home/.crewly" CREWLY_SUDO=unavailable "$@" "$BASH_BIN" "$SCRIPT" </dev/null 2>"$T/err.log"; }
42
+
43
+ # 1. Prebuilt: checksum matches → extracted, wrapper written, smoke test passes.
44
+ run FAKE_SHA="$EXPECTED_SHA"; RC=$?
45
+ check "prebuilt: exit 0" "$RC" "0"
46
+ check "prebuilt: pinned release URL" "$(tail -n 1 "$T/curl.log")" "https://github.com/ggml-org/whisper.cpp/releases/download/v1.9.2/whisper-bin-ubuntu-x64.tar.gz"
47
+ check "prebuilt: wrapper is executable" "$([ -x "$T/home/.crewly/bin/whisper-cli" ] && echo yes)" "yes"
48
+ check "prebuilt: wrapper runs the extracted binary" "$(PATH="$T/bin" "$T/home/.crewly/bin/whisper-cli" -m x)" "ran -m x"
49
+ check "prebuilt: sets LD_LIBRARY_PATH to the bundle" "$(grep -c "LD_LIBRARY_PATH=\"$T/home/.crewly/opt/whisper.cpp-v1.9.2" "$T/home/.crewly/bin/whisper-cli")" "1"
50
+
51
+ # 2. Checksum mismatch → prebuilt rejected, source build needs tools; no sudo → clear failure.
52
+ rm -rf "$T/home"; : > "$T/curl.log"
53
+ run FAKE_SHA=0000; RC=$?
54
+ check "mismatch: exit 1" "$RC" "1"
55
+ grep -q "checksum mismatch for whisper-bin-ubuntu-x64.tar.gz" "$T/err.log" && OK=yes || OK=no
56
+ check "mismatch: says so" "$OK" "yes"
57
+ grep -q "building whisper.cpp needs: cmake build-essential. Install them (e.g. sudo apt-get install -y cmake build-essential)" "$T/err.log" && OK=yes || OK=no
58
+ check "no build tools + no sudo: tells the owner what to install" "$OK" "yes"
59
+ check "mismatch: no whisper-cli left behind" "$([ -e "$T/home/.crewly/bin/whisper-cli" ] && echo present || echo absent)" "absent"
60
+
61
+ # 3. Not Linux → refuses (macOS uses brew).
62
+ printf '#!/bin/sh\necho Darwin\n' > "$T/bin/uname"
63
+ run FAKE_SHA="$EXPECTED_SHA"; RC=$?
64
+ check "darwin: exit 1" "$RC" "1"
65
+ grep -q "this installer is for Linux" "$T/err.log" && OK=yes || OK=no
66
+ check "darwin: says brew is used there" "$OK" "yes"
67
+
68
+ echo "=== Results: $PASS passed, $FAIL failed ==="
69
+ [ "$FAIL" -eq 0 ]