@webship.co/workspace 1.0.0-beta1 → 1.0.0-beta2

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 (630) hide show
  1. package/LICENSE +339 -0
  2. package/README.md +28 -364
  3. package/core/bin/workspace.js +135 -0
  4. package/core/config/settings.labels.yml +514 -0
  5. package/core/config/settings.template.yml +292 -0
  6. package/core/config/style.yml +53 -0
  7. package/core/config/theme.default.yml +161 -0
  8. package/core/config/theme.dispatch.yml +166 -0
  9. package/core/config/theme.drupal.yml +175 -0
  10. package/core/config/theme.ember.yml +160 -0
  11. package/core/config/theme.github.yml +172 -0
  12. package/core/config/theme.gitlab.yml +168 -0
  13. package/core/config/theme.press.yml +161 -0
  14. package/core/config/theme.slate.yml +160 -0
  15. package/core/config/theme.upsun.yml +174 -0
  16. package/core/docs/architecture.svg +65 -0
  17. package/core/docs/getting-started.md +51 -0
  18. package/core/docs/plugins.md +27 -0
  19. package/core/docs/releases.md +23 -0
  20. package/core/docs/testing.md +15 -0
  21. package/core/docs/workspaces.md +25 -0
  22. package/core/scripts/args/arg-ai-items.sh +65 -0
  23. package/core/scripts/args/arg-autostart.sh +31 -0
  24. package/core/scripts/args/arg-avatar-agent.sh +28 -0
  25. package/core/scripts/args/arg-avatar-catalog.sh +12 -0
  26. package/core/scripts/args/arg-avatar-character.sh +68 -0
  27. package/core/scripts/args/arg-avatar-voice.sh +24 -0
  28. package/core/scripts/args/arg-avatar.sh +22 -0
  29. package/core/scripts/args/arg-checks.sh +36 -0
  30. package/core/scripts/args/arg-clone.sh +42 -0
  31. package/core/scripts/args/arg-commands.sh +51 -0
  32. package/core/scripts/args/arg-cucumber.sh +34 -0
  33. package/core/scripts/args/arg-distribution.sh +30 -0
  34. package/core/scripts/args/arg-drupal.sh +39 -0
  35. package/core/scripts/args/arg-drupal_cms.sh +18 -0
  36. package/core/scripts/args/arg-gitlab-ci-local.sh +25 -0
  37. package/core/scripts/args/arg-graphify.sh +36 -0
  38. package/core/scripts/args/arg-graphs.sh +12 -0
  39. package/core/scripts/args/arg-hostnames.sh +23 -0
  40. package/core/scripts/args/arg-milvus-backup.sh +8 -0
  41. package/core/scripts/args/arg-milvus-mcp.sh +12 -0
  42. package/core/scripts/args/arg-milvus.sh +32 -0
  43. package/core/scripts/args/arg-obsidian.sh +17 -0
  44. package/core/scripts/args/arg-odoo.sh +18 -0
  45. package/core/scripts/args/arg-penpot-backup.sh +8 -0
  46. package/core/scripts/args/arg-penpot-mcp.sh +15 -0
  47. package/core/scripts/args/arg-penpot-token.sh +17 -0
  48. package/core/scripts/args/arg-penpot.sh +44 -0
  49. package/core/scripts/args/arg-rag.sh +21 -0
  50. package/core/scripts/args/arg-ragify.sh +35 -0
  51. package/core/scripts/args/arg-reclaim.sh +18 -0
  52. package/core/scripts/args/arg-release.sh +21 -0
  53. package/core/scripts/args/arg-screen.sh +28 -0
  54. package/core/scripts/args/arg-site-template.sh +13 -0
  55. package/core/scripts/args/arg-spdd.sh +16 -0
  56. package/core/scripts/args/arg-specs.sh +22 -0
  57. package/core/scripts/args/arg-storage.sh +36 -0
  58. package/core/scripts/args/arg-testing.sh +36 -0
  59. package/core/scripts/args/arg-webship.sh +34 -0
  60. package/core/scripts/args/arg-webships-project.sh +22 -0
  61. package/core/scripts/args/arg-webships-site-template.sh +17 -0
  62. package/core/scripts/args/arg-webships.sh +34 -0
  63. package/core/scripts/args/arg-website-site-template.sh +17 -0
  64. package/core/scripts/args/arg-website.sh +25 -0
  65. package/core/scripts/args/arg-wordpress.sh +18 -0
  66. package/core/scripts/autostart-run.sh +162 -0
  67. package/core/scripts/autostart-stop.sh +30 -0
  68. package/core/scripts/autostart-watch.sh +43 -0
  69. package/core/scripts/autostart.sh +26 -0
  70. package/core/scripts/bootstrap-functions.sh +117 -0
  71. package/core/scripts/bootstrap-libraries.sh +10 -0
  72. package/core/scripts/bootstrap.sh +71 -0
  73. package/core/scripts/ddev-shim/ddev +122 -0
  74. package/core/scripts/docker-gid.sh +119 -0
  75. package/core/scripts/functions/arg-site-template.sh +28 -0
  76. package/core/scripts/functions/fun-ai-items-lock.sh +175 -0
  77. package/core/scripts/functions/fun-ai-items-sources.sh +298 -0
  78. package/core/scripts/functions/fun-ai-items.sh +621 -0
  79. package/core/scripts/functions/fun-ai-providers.sh +72 -0
  80. package/core/scripts/functions/fun-autostart.sh +429 -0
  81. package/core/scripts/functions/fun-avatar.sh +441 -0
  82. package/core/scripts/functions/fun-backup-item.sh +50 -0
  83. package/core/scripts/functions/fun-build-distribution.sh +226 -0
  84. package/core/scripts/functions/fun-build-site-template.sh +104 -0
  85. package/core/scripts/functions/fun-build-webships.sh +161 -0
  86. package/core/scripts/functions/fun-build-website.sh +199 -0
  87. package/core/scripts/functions/fun-checks.sh +243 -0
  88. package/core/scripts/functions/fun-clone-project.sh +289 -0
  89. package/core/scripts/functions/fun-commands.sh +521 -0
  90. package/core/scripts/functions/fun-database.sh +63 -0
  91. package/core/scripts/functions/fun-ddev-hostnames.sh +213 -0
  92. package/core/scripts/functions/fun-ddev-nodejs.sh +15 -0
  93. package/core/scripts/functions/fun-ddev-telemetry.sh +15 -0
  94. package/core/scripts/functions/fun-default-settings.sh +28 -0
  95. package/core/scripts/functions/fun-distribution-cucumber.sh +45 -0
  96. package/core/scripts/functions/fun-distribution-drupal.sh +25 -0
  97. package/core/scripts/functions/fun-distribution-drupal_cms.sh +35 -0
  98. package/core/scripts/functions/fun-distribution-webship.sh +28 -0
  99. package/core/scripts/functions/fun-distribution-webships.sh +33 -0
  100. package/core/scripts/functions/fun-distribution-website.sh +19 -0
  101. package/core/scripts/functions/fun-drush.sh +48 -0
  102. package/core/scripts/functions/fun-elevenlabs.sh +179 -0
  103. package/core/scripts/functions/fun-files.sh +19 -0
  104. package/core/scripts/functions/fun-git-worktrees.sh +142 -0
  105. package/core/scripts/functions/fun-gitlab-ci.sh +156 -0
  106. package/core/scripts/functions/fun-hostnames-cli.sh +88 -0
  107. package/core/scripts/functions/fun-milvus.sh +584 -0
  108. package/core/scripts/functions/fun-odoo.sh +121 -0
  109. package/core/scripts/functions/fun-penpot.sh +724 -0
  110. package/core/scripts/functions/fun-project-name.sh +20 -0
  111. package/core/scripts/functions/fun-rag.sh +253 -0
  112. package/core/scripts/functions/fun-ragify.sh +165 -0
  113. package/core/scripts/functions/fun-reclaim.sh +297 -0
  114. package/core/scripts/functions/fun-release.sh +159 -0
  115. package/core/scripts/functions/fun-remove-project.sh +319 -0
  116. package/core/scripts/functions/fun-screen.sh +96 -0
  117. package/core/scripts/functions/fun-spdd.sh +676 -0
  118. package/core/scripts/functions/fun-specs-share.sh +237 -0
  119. package/core/scripts/functions/fun-storage.sh +347 -0
  120. package/core/scripts/functions/fun-testing.sh +329 -0
  121. package/core/scripts/functions/fun-users.sh +87 -0
  122. package/core/scripts/functions/fun-version.sh +72 -0
  123. package/core/scripts/functions/fun-video-edit.sh +48 -0
  124. package/core/scripts/functions/fun-voice.sh +45 -0
  125. package/core/scripts/functions/fun-wordpress.sh +59 -0
  126. package/core/scripts/host-mounts.sh +161 -0
  127. package/core/scripts/install.sh +602 -0
  128. package/core/scripts/libs/__pycache__/video_edit.cpython-312.pyc +0 -0
  129. package/core/scripts/libs/argparse.sh +93 -0
  130. package/core/scripts/libs/bash-yaml.sh +52 -0
  131. package/core/scripts/libs/checks_plan.js +312 -0
  132. package/core/scripts/libs/elevenlabs.py +627 -0
  133. package/core/scripts/libs/progress-bar.sh +19 -0
  134. package/core/scripts/libs/rag_index.py +446 -0
  135. package/core/scripts/libs/video_edit.py +636 -0
  136. package/core/scripts/libs/voice.py +430 -0
  137. package/core/scripts/plugins/jira/host-install.sh +119 -0
  138. package/core/scripts/plugins/jira/host-mounts.txt +2 -0
  139. package/core/scripts/plugins/upsun/args/arg-upsun.sh +51 -0
  140. package/core/scripts/plugins/upsun/functions/fun-upsun.sh +304 -0
  141. package/core/scripts/plugins/upsun/host-install.sh +99 -0
  142. package/core/scripts/plugins/upsun/host-mounts.txt +1 -0
  143. package/core/scripts/plugins/upsun/upsun-ai.sh +133 -0
  144. package/core/scripts/plugins.sh +152 -0
  145. package/core/scripts/release.sh +69 -0
  146. package/core/scripts/storage.sh +39 -0
  147. package/core/scripts/sync-tooling.sh +246 -0
  148. package/core/scripts/tests/cmd-smoke-test.sh +109 -0
  149. package/core/scripts/trust-https.sh +257 -0
  150. package/core/workspace-app/.ddev/commands/web/test-dashboard +49 -0
  151. package/core/workspace-app/.ddev/config.yaml +400 -0
  152. package/core/workspace-app/.ddev/docker-compose.dood.yaml +45 -0
  153. package/core/workspace-app/.ddev/docker-compose.plugin-jira-twg.yaml +26 -0
  154. package/core/workspace-app/.ddev/docker-compose.plugin-upsun-upsun-cli.yaml +8 -0
  155. package/core/workspace-app/.ddev/docker-compose.screens.yaml +15 -0
  156. package/core/workspace-app/.ddev/nginx_full/nginx-site.conf +114 -0
  157. package/core/workspace-app/.ddev/web-build/Dockerfile.workspace-app +203 -0
  158. package/core/workspace-app/.ddev/web-build/ddev-shim-ddev +122 -0
  159. package/core/workspace-app/README.md +380 -0
  160. package/{actions → core/workspace-app/actions}/ddev.js +0 -1
  161. package/{actions → core/workspace-app/actions}/rag.js +0 -1
  162. package/{actions → core/workspace-app/actions}/testing.js +0 -1
  163. package/{assistant.js → core/workspace-app/assistant.js} +1 -10
  164. package/{git-info.js → core/workspace-app/git-info.js} +17 -1
  165. package/core/workspace-app/package.json +29 -0
  166. package/core/workspace-app/plugin-catalog.js +50 -0
  167. package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
  168. package/{server.js → core/workspace-app/server.js} +18 -0
  169. package/core/workspace-app/tests/features/actions-rail.feature +44 -0
  170. package/core/workspace-app/tests/features/ai-items.feature +23 -0
  171. package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
  172. package/core/workspace-app/tests/features/checks.feature +39 -0
  173. package/core/workspace-app/tests/features/clone-project.feature +30 -0
  174. package/core/workspace-app/tests/features/command-authoring.feature +58 -0
  175. package/core/workspace-app/tests/features/dashboard.feature +57 -0
  176. package/core/workspace-app/tests/features/menus.feature +29 -0
  177. package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
  178. package/core/workspace-app/tests/features/plugins.feature +20 -0
  179. package/core/workspace-app/tests/features/project-actions.feature +40 -0
  180. package/core/workspace-app/tests/features/screens.feature +29 -0
  181. package/core/workspace-app/tests/features/settings.feature +46 -0
  182. package/core/workspace-app/tests/features/test-reports.feature +32 -0
  183. package/core/workspace-app/tests/features/theme.feature +40 -0
  184. package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
  185. package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +152 -0
  186. package/core/workspace-app/tests/step-definitions/preconditions.steps.js +100 -0
  187. package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
  188. package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
  189. package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
  190. package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
  191. package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
  192. package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
  193. package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
  194. package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
  195. package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
  196. package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
  197. package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
  198. package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
  199. package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
  200. package/core/workspace-app/tests/unit/assistant.test.js +61 -0
  201. package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
  202. package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
  203. package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
  204. package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
  205. package/core/workspace-app/tests/unit/browsers.test.js +76 -0
  206. package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
  207. package/core/workspace-app/tests/unit/build-order.test.js +153 -0
  208. package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
  209. package/core/workspace-app/tests/unit/builders.test.js +158 -0
  210. package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
  211. package/core/workspace-app/tests/unit/changelog.test.js +177 -0
  212. package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
  213. package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
  214. package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
  215. package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
  216. package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
  217. package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
  218. package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
  219. package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
  220. package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
  221. package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
  222. package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
  223. package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
  224. package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
  225. package/core/workspace-app/tests/unit/create-card.test.js +85 -0
  226. package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
  227. package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
  228. package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
  229. package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
  230. package/core/workspace-app/tests/unit/ddev-domains.test.js +153 -0
  231. package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
  232. package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
  233. package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
  234. package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
  235. package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
  236. package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
  237. package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
  238. package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
  239. package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
  240. package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
  241. package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
  242. package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
  243. package/core/workspace-app/tests/unit/density.test.js +52 -0
  244. package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
  245. package/core/workspace-app/tests/unit/event-log.test.js +102 -0
  246. package/core/workspace-app/tests/unit/favicon.test.js +68 -0
  247. package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
  248. package/core/workspace-app/tests/unit/git-info.test.js +26 -0
  249. package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
  250. package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
  251. package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
  252. package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
  253. package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
  254. package/core/workspace-app/tests/unit/images-create.test.js +59 -0
  255. package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
  256. package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
  257. package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
  258. package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
  259. package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
  260. package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
  261. package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
  262. package/core/workspace-app/tests/unit/narration.test.js +186 -0
  263. package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
  264. package/core/workspace-app/tests/unit/navigation.test.js +254 -0
  265. package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
  266. package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
  267. package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
  268. package/core/workspace-app/tests/unit/notes.test.js +128 -0
  269. package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
  270. package/core/workspace-app/tests/unit/page-width.test.js +59 -0
  271. package/core/workspace-app/tests/unit/php-version.test.js +90 -0
  272. package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
  273. package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
  274. package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
  275. package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
  276. package/core/workspace-app/tests/unit/plugins.test.js +58 -0
  277. package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
  278. package/core/workspace-app/tests/unit/project-make.test.js +143 -0
  279. package/core/workspace-app/tests/unit/project-name.test.js +202 -0
  280. package/core/workspace-app/tests/unit/reader.test.js +62 -0
  281. package/core/workspace-app/tests/unit/release.test.js +92 -0
  282. package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
  283. package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
  284. package/core/workspace-app/tests/unit/round-11.test.js +31 -0
  285. package/core/workspace-app/tests/unit/round-12.test.js +37 -0
  286. package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
  287. package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
  288. package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
  289. package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
  290. package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
  291. package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
  292. package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
  293. package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
  294. package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
  295. package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
  296. package/core/workspace-app/tests/unit/screens.test.js +74 -0
  297. package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
  298. package/core/workspace-app/tests/unit/session-name.test.js +64 -0
  299. package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
  300. package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
  301. package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
  302. package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
  303. package/core/workspace-app/tests/unit/speech.test.js +290 -0
  304. package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
  305. package/core/workspace-app/tests/unit/status-language.test.js +47 -0
  306. package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
  307. package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
  308. package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
  309. package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
  310. package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
  311. package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
  312. package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
  313. package/core/workspace-app/tests/unit/themes.test.js +453 -0
  314. package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
  315. package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
  316. package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
  317. package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
  318. package/core/workspace-app/tests/unit/version-check.test.js +98 -0
  319. package/core/workspace-app/tests/unit/version-link.test.js +50 -0
  320. package/core/workspace-app/tests/unit/video-borrow.test.js +198 -0
  321. package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
  322. package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
  323. package/core/workspace-app/tests/unit/video-edit.test.js +417 -0
  324. package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
  325. package/core/workspace-app/tests/unit/waiting.test.js +48 -0
  326. package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
  327. package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
  328. package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
  329. package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
  330. package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
  331. package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
  332. package/package.json +38 -36
  333. /package/{PLUGINS.md → core/workspace-app/PLUGINS.md} +0 -0
  334. /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
  335. /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
  336. /package/{actions → core/workspace-app/actions}/audio.js +0 -0
  337. /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
  338. /package/{actions → core/workspace-app/actions}/checks.js +0 -0
  339. /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
  340. /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
  341. /package/{actions → core/workspace-app/actions}/composer.js +0 -0
  342. /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
  343. /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
  344. /package/{actions → core/workspace-app/actions}/folders.js +0 -0
  345. /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
  346. /package/{actions → core/workspace-app/actions}/images.js +0 -0
  347. /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
  348. /package/{actions → core/workspace-app/actions}/node.js +0 -0
  349. /package/{actions → core/workspace-app/actions}/notes.js +0 -0
  350. /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
  351. /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
  352. /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
  353. /package/{actions → core/workspace-app/actions}/screens.js +0 -0
  354. /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
  355. /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
  356. /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
  357. /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
  358. /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
  359. /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
  360. /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
  361. /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
  362. /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
  363. /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
  364. /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
  365. /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
  366. /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
  367. /package/{checks.js → core/workspace-app/checks.js} +0 -0
  368. /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
  369. /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
  370. /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
  371. /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
  372. /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
  373. /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
  374. /package/{commands.js → core/workspace-app/commands.js} +0 -0
  375. /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
  376. /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
  377. /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
  378. /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
  379. /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
  380. /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
  381. /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
  382. /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
  383. /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
  384. /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
  385. /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
  386. /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
  387. /package/{docs → core/workspace-app/docs}/README.md +0 -0
  388. /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
  389. /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
  390. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
  391. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
  392. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
  393. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
  394. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
  395. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
  396. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
  397. /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
  398. /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
  399. /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
  400. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
  401. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
  402. /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
  403. /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
  404. /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
  405. /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
  406. /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
  407. /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
  408. /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
  409. /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
  410. /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
  411. /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
  412. /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
  413. /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
  414. /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
  415. /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
  416. /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
  417. /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
  418. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
  419. /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
  420. /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
  421. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
  422. /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
  423. /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
  424. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
  425. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
  426. /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
  427. /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
  428. /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
  429. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
  430. /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
  431. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
  432. /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
  433. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
  434. /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
  435. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
  436. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
  437. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
  438. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
  439. /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
  440. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
  441. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
  442. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
  443. /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
  444. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
  445. /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
  446. /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
  447. /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
  448. /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
  449. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
  450. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
  451. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
  452. /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
  453. /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
  454. /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
  455. /package/{folders.js → core/workspace-app/folders.js} +0 -0
  456. /package/{format.js → core/workspace-app/format.js} +0 -0
  457. /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
  458. /package/{html.js → core/workspace-app/html.js} +0 -0
  459. /package/{icons.js → core/workspace-app/icons.js} +0 -0
  460. /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
  461. /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
  462. /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
  463. /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
  464. /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
  465. /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
  466. /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
  467. /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
  468. /package/{images.js → core/workspace-app/images.js} +0 -0
  469. /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
  470. /package/{linger.js → core/workspace-app/linger.js} +0 -0
  471. /package/{lists.js → core/workspace-app/lists.js} +0 -0
  472. /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
  473. /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
  474. /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
  475. /package/{notes.js → core/workspace-app/notes.js} +0 -0
  476. /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
  477. /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
  478. /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
  479. /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
  480. /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
  481. /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
  482. /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
  483. /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
  484. /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
  485. /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
  486. /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
  487. /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
  488. /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
  489. /package/{public → core/workspace-app/public}/css/base.css +0 -0
  490. /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
  491. /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
  492. /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
  493. /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
  494. /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
  495. /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
  496. /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
  497. /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
  498. /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
  499. /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
  500. /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
  501. /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
  502. /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
  503. /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
  504. /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
  505. /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
  506. /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
  507. /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
  508. /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
  509. /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
  510. /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
  511. /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
  512. /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
  513. /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
  514. /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
  515. /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
  516. /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
  517. /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
  518. /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
  519. /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
  520. /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
  521. /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
  522. /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
  523. /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
  524. /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
  525. /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
  526. /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
  527. /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
  528. /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
  529. /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
  530. /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
  531. /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
  532. /package/{public → core/workspace-app/public}/favicon.ico +0 -0
  533. /package/{public → core/workspace-app/public}/favicon.png +0 -0
  534. /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
  535. /package/{public → core/workspace-app/public}/logo.png +0 -0
  536. /package/{public → core/workspace-app/public}/media-player.js +0 -0
  537. /package/{public → core/workspace-app/public}/mic.js +0 -0
  538. /package/{public → core/workspace-app/public}/narration.js +0 -0
  539. /package/{public → core/workspace-app/public}/palette.js +0 -0
  540. /package/{public → core/workspace-app/public}/sortable.js +0 -0
  541. /package/{public → core/workspace-app/public}/suggest.js +0 -0
  542. /package/{public → core/workspace-app/public}/terminal.js +0 -0
  543. /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
  544. /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
  545. /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
  546. /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
  547. /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
  548. /package/{public → core/workspace-app/public}/ui.js +0 -0
  549. /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
  550. /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
  551. /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
  552. /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
  553. /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
  554. /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
  555. /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
  556. /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
  557. /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
  558. /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
  559. /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
  560. /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
  561. /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
  562. /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
  563. /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
  564. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
  565. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
  566. /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
  567. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
  568. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
  569. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
  570. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
  571. /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
  572. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
  573. /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  574. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
  575. /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
  576. /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
  577. /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
  578. /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
  579. /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
  580. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
  581. /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
  582. /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
  583. /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
  584. /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
  585. /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
  586. /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
  587. /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
  588. /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
  589. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
  590. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
  591. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
  592. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
  593. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
  594. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
  595. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
  596. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
  597. /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
  598. /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
  599. /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
  600. /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
  601. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
  602. /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
  603. /package/{rag.js → core/workspace-app/rag.js} +0 -0
  604. /package/{reader.js → core/workspace-app/reader.js} +0 -0
  605. /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
  606. /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
  607. /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
  608. /package/{screenrc → core/workspace-app/screenrc} +0 -0
  609. /package/{screens.js → core/workspace-app/screens.js} +0 -0
  610. /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
  611. /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
  612. /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
  613. /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
  614. /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
  615. /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
  616. /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
  617. /package/{speech.js → core/workspace-app/speech.js} +0 -0
  618. /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
  619. /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
  620. /package/{themes.js → core/workspace-app/themes.js} +0 -0
  621. /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
  622. /package/{urls.js → core/workspace-app/urls.js} +0 -0
  623. /package/{validate.js → core/workspace-app/validate.js} +0 -0
  624. /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
  625. /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
  626. /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
  627. /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
  628. /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
  629. /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
  630. /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
@@ -0,0 +1,627 @@
1
+ #!/usr/bin/env python3
2
+ """ElevenLabs for the final voiceover of a narrated video. Standard library only.
3
+
4
+ Run through videos/cmd-tools-elevenlabs.sh, which reads the key from settings.yml and hands it over in
5
+ ELEVENLABS_API_KEY. Anything that spends credits prints a plan and stops unless --confirm is given.
6
+ Exit codes: 0 ok, 1 error, 2 usage, 3 no key, 4 plan only, 5 sections do not fit their windows.
7
+ """
8
+
9
+ import argparse
10
+ import datetime
11
+ import hashlib
12
+ import json
13
+ import os
14
+ import re
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ import urllib.error
19
+ import urllib.parse
20
+ import urllib.request
21
+ import uuid
22
+
23
+ OK, ERROR, USAGE, NO_KEY, PLAN_ONLY, NO_FIT = 0, 1, 2, 3, 4, 5
24
+ DEFAULT_BASE = 'https://api.elevenlabs.io'
25
+ DEFAULT_MODEL = 'eleven_v4'
26
+ DEFAULT_FORMAT = 'mp3_44100_128'
27
+ SPEECH_CHARS_PER_SECOND = 15.0
28
+ SLACK = 0.05
29
+
30
+ HEADING = re.compile(
31
+ r'^##\s+(\S+)\s+(.+?)\s+\(((?:\d+:)?\d{1,2}:\d{2}(?:\.\d+)?)\s+to\s+((?:\d+:)?\d{1,2}:\d{2}(?:\.\d+)?)\)\s*$')
32
+ HINTS = {
33
+ 401: 'ElevenLabs rejected the key.',
34
+ 403: 'The key is not allowed to do that.',
35
+ 404: 'Not found (check the voice id).',
36
+ 429: 'ElevenLabs is rate limiting this key. Wait, then run it again; finished sections are kept.',
37
+ }
38
+
39
+
40
+ class Stop(Exception):
41
+ def __init__(self, message, code=ERROR):
42
+ super().__init__(message)
43
+ self.code = code
44
+
45
+
46
+ def emit(args, data, lines):
47
+ print(json.dumps(data, indent=2) if args.json else '\n'.join(lines))
48
+
49
+
50
+ def mmss(seconds):
51
+ return '%02d:%04.1f' % (int(seconds // 60), seconds % 60)
52
+
53
+
54
+ def seconds_of(stamp):
55
+ total = 0.0
56
+ for part in stamp.split(':'):
57
+ total = total * 60 + float(part)
58
+ return total
59
+
60
+
61
+ def sha(*parts):
62
+ return hashlib.sha256(json.dumps(parts, sort_keys=True).encode()).hexdigest()
63
+
64
+
65
+ def stem_of(path):
66
+ return os.path.splitext(os.path.basename(path))[0]
67
+
68
+
69
+ def safe_id(text):
70
+ return re.sub(r'[^A-Za-z0-9_-]', '_', text)
71
+
72
+
73
+ def read_bytes(path):
74
+ with open(path, 'rb') as handle:
75
+ return handle.read()
76
+
77
+
78
+ def read_json(path):
79
+ with open(path, encoding='utf-8') as handle:
80
+ return json.load(handle)
81
+
82
+
83
+ def ensure_free(paths, force):
84
+ for path in paths:
85
+ if os.path.exists(path) and not force:
86
+ raise Stop('%s already exists. Pass --force to replace it. Nothing was sent.' % path)
87
+
88
+
89
+ def track_of(out):
90
+ return os.path.splitext(out)[0] + '-voiceover.mp3'
91
+
92
+
93
+ def write_atomic(path, data, force):
94
+ if os.path.exists(path) and not force:
95
+ raise Stop('%s already exists. Pass --force to replace it.' % path)
96
+ folder = os.path.dirname(os.path.abspath(path))
97
+ os.makedirs(folder, exist_ok=True)
98
+ part = os.path.join(folder, '.%s.%s.part' % (os.path.basename(path), uuid.uuid4().hex[:8]))
99
+ with open(part, 'wb') as handle:
100
+ handle.write(data)
101
+ os.replace(part, path)
102
+
103
+
104
+ # ---- the API
105
+
106
+ def key():
107
+ value = os.environ.get('ELEVENLABS_API_KEY', '').strip()
108
+ if not value:
109
+ raise Stop('No ElevenLabs key is set. Add it at /settings (ElevenLabs), or run: bash cmd-tools-elevenlabs.sh -a init', NO_KEY)
110
+ if not re.fullmatch(r'[A-Za-z0-9_.-]+', value):
111
+ raise Stop('The ElevenLabs key has characters a key never has. Check the value in settings.yml.', NO_KEY)
112
+ return value
113
+
114
+
115
+ def call(method, path, body=None, content_type=None, query=None):
116
+ url = os.environ.get('ELEVENLABS_API_BASE', DEFAULT_BASE).rstrip('/') + path
117
+ if query:
118
+ url += '?' + urllib.parse.urlencode(query)
119
+ headers = {'xi-api-key': key()}
120
+ if content_type:
121
+ headers['Content-Type'] = content_type
122
+ request = urllib.request.Request(url, data=body, method=method, headers=headers)
123
+ try:
124
+ with urllib.request.urlopen(request, timeout=int(os.environ.get('ELEVENLABS_TIMEOUT', '300'))) as response:
125
+ return response.status, response.headers, response.read()
126
+ except urllib.error.HTTPError as error:
127
+ return error.code, error.headers, error.read()
128
+ except (urllib.error.URLError, TimeoutError, OSError) as error:
129
+ raise Stop('Could not reach ElevenLabs (%s).' % getattr(error, 'reason', error))
130
+
131
+
132
+ ID_LIKE = re.compile(r'[A-Za-z0-9]{16,32}')
133
+
134
+
135
+ def resolve_voice(value):
136
+ """A voice given by name (Brian) becomes its id on this account; an id is left alone."""
137
+ if ID_LIKE.fullmatch(value):
138
+ return value
139
+ status, _, body = call('GET', '/v2/voices', query={'search': value, 'page_size': '30'})
140
+ if status != 200:
141
+ raise Stop('Could not look up the voice "%s". %s' % (value, explain(status, body)))
142
+ voices = json.loads(body).get('voices', [])
143
+ want = value.strip().lower()
144
+ hit = (next((v for v in voices if (v.get('name') or '').lower() == want), None)
145
+ or next((v for v in voices if (v.get('name') or '').lower().startswith(want)), None))
146
+ if not hit:
147
+ raise Stop('No voice named "%s" on this account. List them with -a voices.' % value, USAGE)
148
+ return hit['voice_id']
149
+
150
+
151
+ def explain(status, body):
152
+ detail = ''
153
+ try:
154
+ found = json.loads(body.decode('utf-8', 'replace')).get('detail')
155
+ if isinstance(found, dict):
156
+ detail = found.get('message') or found.get('status') or ''
157
+ elif isinstance(found, list):
158
+ detail = '; '.join(str(item.get('msg', item)) if isinstance(item, dict) else str(item) for item in found[:3])
159
+ elif found:
160
+ detail = str(found)
161
+ except Exception:
162
+ detail = body.decode('utf-8', 'replace')[:160].replace('\n', ' ')
163
+ hint = HINTS.get(status) or ('ElevenLabs had a server error; run it again later.' if status >= 500 else '')
164
+ return ' '.join(part for part in ('HTTP %d.' % status, hint, detail) if part)
165
+
166
+
167
+ def multipart(fields, files):
168
+ boundary = uuid.uuid4().hex
169
+ parts = []
170
+ for name, value in fields.items():
171
+ parts.append(('--%s\r\nContent-Disposition: form-data; name="%s"\r\n\r\n%s\r\n' % (boundary, name, value)).encode())
172
+ for name, filename, data in files:
173
+ parts.append(('--%s\r\nContent-Disposition: form-data; name="%s"; filename="%s"\r\n'
174
+ 'Content-Type: application/octet-stream\r\n\r\n' % (boundary, name, filename)).encode() + data + b'\r\n')
175
+ parts.append(('--%s--\r\n' % boundary).encode())
176
+ return b''.join(parts), 'multipart/form-data; boundary=%s' % boundary
177
+
178
+
179
+ def account():
180
+ status, _, body = call('GET', '/v1/user/subscription')
181
+ if status != 200:
182
+ return None, explain(status, body)
183
+ found = json.loads(body)
184
+ used, limit = found.get('character_count'), found.get('character_limit')
185
+ return {
186
+ 'tier': found.get('tier'), 'status': found.get('status'), 'used': used, 'limit': limit,
187
+ 'remaining': limit - used if isinstance(used, int) and isinstance(limit, int) else None,
188
+ 'resets': found.get('next_character_count_reset_unix'),
189
+ }, ''
190
+
191
+
192
+ def remaining_characters():
193
+ try:
194
+ found, _ = account()
195
+ except Stop:
196
+ return None
197
+ return found['remaining'] if found else None
198
+
199
+
200
+ # Brand names the voices misread: a client heard "Var-dough" for Webship in a final video. The respelling is
201
+ # applied ONLY to the text sent to the voice. Scripts, captions, transcripts, character counts and the
202
+ # saved section text keep the real spelling. ELEVENLABS_PRONUNCIATION ("Word=Respelling; Word=Respelling",
203
+ # from elevenlabs.pronunciation in settings.yml) adds to or overrides these.
204
+ PRONUNCIATIONS = {'Webship': 'Var-dot', 'Webship': 'Var-base'}
205
+
206
+
207
+ def pronunciations():
208
+ table = dict(PRONUNCIATIONS)
209
+ for pair in os.environ.get('ELEVENLABS_PRONUNCIATION', '').split(';'):
210
+ word, _, said = pair.partition('=')
211
+ if word.strip() and said.strip():
212
+ table[word.strip()] = said.strip()
213
+ return table
214
+
215
+
216
+ def respell(text):
217
+ for word, said in pronunciations().items():
218
+ text = re.sub(r'(?<![\w-])%s(?![\w-])' % re.escape(word), said, text)
219
+ return text
220
+
221
+
222
+ def synthesize(text, voice, model, fmt):
223
+ body = json.dumps({'text': respell(text), 'model_id': model}).encode()
224
+ status, headers, data = call('POST', '/v1/text-to-speech/' + urllib.parse.quote(voice, safe=''), body=body,
225
+ content_type='application/json', query={'output_format': fmt})
226
+ if status != 200 or not data or 'json' in (headers.get('content-type') or ''):
227
+ raise Stop('Speech failed. ' + explain(status, data))
228
+ return data, {'request_id': headers.get('request-id'), 'characters': headers.get('x-character-count')}
229
+
230
+
231
+ # ---- media
232
+
233
+ def need_tools():
234
+ for tool in ('ffmpeg', 'ffprobe'):
235
+ if not shutil.which(tool):
236
+ raise Stop('%s is needed and was not found on PATH.' % tool)
237
+
238
+
239
+ def run(command):
240
+ done = subprocess.run(command, capture_output=True, text=True)
241
+ if done.returncode != 0:
242
+ raise Stop('%s failed: %s' % (command[0], (done.stderr or '').strip().splitlines()[-1:] or 'no output'))
243
+
244
+
245
+ def duration(path):
246
+ try:
247
+ done = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', path],
248
+ capture_output=True, text=True, timeout=60)
249
+ return float(done.stdout.strip())
250
+ except Exception:
251
+ return None
252
+
253
+
254
+ def mux(video, mix_wav, out, length, force):
255
+ ensure_free([out, track_of(out)], force)
256
+ part = os.path.join(os.path.dirname(os.path.abspath(out)), '.%s.part' % os.path.basename(out))
257
+ run(['ffmpeg', '-y', '-loglevel', 'error', '-i', video, '-i', mix_wav, '-map', '0:v:0', '-map', '1:a:0',
258
+ '-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', '-movflags', '+faststart', '-t', '%.3f' % length, '-f', 'mp4', part])
259
+ os.replace(part, out)
260
+ track = track_of(out)
261
+ run(['ffmpeg', '-y', '-loglevel', 'error', '-i', mix_wav, '-c:a', 'libmp3lame', '-b:a', '192k', track])
262
+ poster = os.path.splitext(video)[0] + '-poster.jpg'
263
+ new_poster = os.path.splitext(out)[0] + '-poster.jpg'
264
+ if os.path.exists(poster) and (force or not os.path.exists(new_poster)):
265
+ shutil.copyfile(poster, new_poster)
266
+ os.remove(mix_wav)
267
+ return track, new_poster if os.path.exists(new_poster) else None
268
+
269
+
270
+ # ---- the script
271
+
272
+ def clean(line):
273
+ text = re.sub(r'\*\(optional\)\*', '', line)
274
+ text = re.sub(r'\[([^\]]+)\]\([^)]*\)', r'\1', text)
275
+ return ' '.join(re.sub(r'[*`]', '', text).split())
276
+
277
+
278
+ def load_sections(path, drop_optional=False):
279
+ try:
280
+ with open(path, encoding='utf-8') as handle:
281
+ lines = handle.read().splitlines()
282
+ except OSError as error:
283
+ raise Stop('Cannot read the script: %s' % error)
284
+ sections, current = [], None
285
+ for line in lines:
286
+ found = HEADING.match(line)
287
+ if found:
288
+ current = {'id': found.group(1), 'title': found.group(2), 'start': seconds_of(found.group(3)),
289
+ 'end': seconds_of(found.group(4)), 'spoken': [], 'optional': []}
290
+ sections.append(current)
291
+ elif line.startswith('## '):
292
+ current = None
293
+ elif current is not None and line.startswith('>'):
294
+ spoken = clean(line.lstrip('>'))
295
+ if spoken:
296
+ (current['optional'] if '*(optional)*' in line else current['spoken']).append((len(current['spoken']) + len(current['optional']), spoken))
297
+ if not sections:
298
+ raise Stop('No timed sections found. Each section needs a heading like "## D01 Opening (00:00.0 to 00:21.4)" and its '
299
+ 'spoken lines as "> " quotes. See the elevenlabs-voiceover skill.')
300
+ result, previous_end = [], 0.0
301
+ for section in sections:
302
+ if section['end'] <= section['start']:
303
+ raise Stop('Section %s ends before it starts.' % section['id'])
304
+ if section['start'] < previous_end - SLACK:
305
+ raise Stop('Section %s starts before the previous section ends.' % section['id'])
306
+ previous_end = section['end']
307
+ kept = sorted(section['spoken'] + ([] if drop_optional else section['optional']))
308
+ text = ' '.join(item[1] for item in kept)
309
+ if not text:
310
+ continue
311
+ result.append({
312
+ 'id': section['id'], 'title': section['title'], 'start': section['start'], 'end': section['end'],
313
+ 'window': round(section['end'] - section['start'], 3), 'text': text, 'chars': len(text),
314
+ 'optional_chars': sum(len(item[1]) for item in section['optional']),
315
+ })
316
+ if not result:
317
+ raise Stop('The script has sections but no spoken lines ("> " quotes).')
318
+ return result
319
+
320
+
321
+ def check_inputs(args):
322
+ for attribute in ('script', 'video', 'audio'):
323
+ value = getattr(args, attribute, None)
324
+ if value and not os.path.isfile(value):
325
+ raise Stop('%s not found: %s' % (attribute.capitalize(), value), USAGE)
326
+
327
+
328
+ def work_dir(args):
329
+ return args.work or os.path.join(os.path.dirname(os.path.abspath(args.out)), stem_of(args.out) + '-voiceover-sections')
330
+
331
+
332
+ def audio_ext(fmt):
333
+ if not re.fullmatch(r'(mp3|wav)_\d+(_\d+)?', fmt):
334
+ raise Stop('The format must be an mp3_* or wav_* one, for example %s.' % DEFAULT_FORMAT, USAGE)
335
+ return fmt.split('_')[0]
336
+
337
+
338
+ # ---- commands
339
+
340
+ def cmd_sections(args):
341
+ check_inputs(args)
342
+ sections = load_sections(args.script, args.drop_optional)
343
+ lines = ['%-5s %-34s %-18s %7s %6s %9s' % ('id', 'section', 'window', 'seconds', 'chars', 'optional')]
344
+ for s in sections:
345
+ lines.append('%-5s %-34s %-18s %7.1f %6d %9d' % (s['id'], s['title'][:34], '%s to %s' % (mmss(s['start']), mmss(s['end'])),
346
+ s['window'], s['chars'], s['optional_chars']))
347
+ lines.append('%d sections, %d characters.' % (len(sections), sum(s['chars'] for s in sections)))
348
+ emit(args, {'sections': sections, 'characters': sum(s['chars'] for s in sections)}, lines)
349
+ return OK
350
+
351
+
352
+ def cmd_voices(args):
353
+ query = {'page_size': str(args.limit)}
354
+ if args.search:
355
+ query['search'] = args.search
356
+ status, _, body = call('GET', '/v2/voices', query=query)
357
+ if status != 200:
358
+ raise Stop('Could not list voices. ' + explain(status, body))
359
+ voices = json.loads(body).get('voices', [])
360
+ rows = [{'id': v.get('voice_id'), 'name': v.get('name'), 'category': v.get('category'), 'labels': v.get('labels') or {}} for v in voices]
361
+ lines = ['%-24s %-22s %-10s %s' % ('id', 'name', 'category', 'labels')]
362
+ for r in rows:
363
+ lines.append('%-24s %-22s %-10s %s' % (r['id'], (r['name'] or '')[:22], r['category'] or '', ', '.join('%s=%s' % kv for kv in r['labels'].items())))
364
+ emit(args, {'voices': rows}, lines)
365
+ return OK
366
+
367
+
368
+ def cmd_account(args):
369
+ found, why = account()
370
+ if not found:
371
+ raise Stop('The key works for requests but the account cannot be read. %s Grant the key "User: read" to see credits.' % why)
372
+ resets = datetime.datetime.fromtimestamp(found['resets'], datetime.timezone.utc).strftime('%Y-%m-%d') if found['resets'] else 'unknown'
373
+ emit(args, found, ['Plan: %s (%s)' % (found['tier'], found['status']), 'Characters used: %s of %s' % (found['used'], found['limit']),
374
+ 'Remaining: %s' % found['remaining'], 'Resets: %s' % resets])
375
+ return OK
376
+
377
+
378
+ def cmd_speak(args):
379
+ try:
380
+ with open(args.text_file, encoding='utf-8') as handle:
381
+ text = handle.read().strip()
382
+ except OSError:
383
+ text = ''
384
+ if not text:
385
+ raise Stop('The text file is missing or empty: %s' % args.text_file, USAGE)
386
+ key()
387
+ remaining = remaining_characters()
388
+ plan = {'voice': args.voice, 'model': args.model, 'format': args.format, 'characters': len(text), 'remaining': remaining, 'out': args.out}
389
+ if not args.confirm:
390
+ emit(args, dict(plan, confirmed=False), ['Plan only, nothing was sent.', 'Voice %s, model %s, %d characters.' % (args.voice, args.model, len(text)),
391
+ 'Credits left: %s.' % ('unknown' if remaining is None else remaining), 'Run again with --confirm to create %s.' % args.out])
392
+ return PLAN_ONLY
393
+ ensure_free([args.out], args.force)
394
+ data, meta = synthesize(text, args.voice, args.model, args.format)
395
+ write_atomic(args.out, data, args.force)
396
+ emit(args, dict(plan, confirmed=True, bytes=len(data), seconds=duration(args.out), **meta), ['Wrote %s (%d bytes).' % (args.out, len(data))])
397
+ return OK
398
+
399
+
400
+ def cmd_isolate(args):
401
+ check_inputs(args)
402
+ key()
403
+ seconds = duration(args.audio)
404
+ plan = {'audio': args.audio, 'seconds': seconds, 'out': args.out}
405
+ if not args.confirm:
406
+ emit(args, dict(plan, confirmed=False), ['Plan only, nothing was sent.', 'Would clean %s (%s seconds) and write %s.' % (args.audio, seconds, args.out),
407
+ 'Run again with --confirm.'])
408
+ return PLAN_ONLY
409
+ ensure_free([args.out], args.force)
410
+ body, content_type = multipart({}, [('audio', os.path.basename(args.audio), read_bytes(args.audio))])
411
+ status, _, data = call('POST', '/v1/audio-isolation', body=body, content_type=content_type)
412
+ if status != 200 or not data:
413
+ raise Stop('Isolation failed. ' + explain(status, data))
414
+ write_atomic(args.out, data, args.force)
415
+ emit(args, dict(plan, confirmed=True, bytes=len(data)), ['Wrote %s (%d bytes).' % (args.out, len(data))])
416
+ return OK
417
+
418
+
419
+ def cmd_voiceover(args):
420
+ check_inputs(args)
421
+ need_tools()
422
+ key()
423
+ ext = audio_ext(args.format)
424
+ sections = load_sections(args.script, args.drop_optional)
425
+ length = duration(args.video)
426
+ if not length:
427
+ raise Stop('Cannot read the video duration: %s' % args.video)
428
+ if sections[-1]['end'] > length + 1.0:
429
+ raise Stop('The script runs to %s but the video is only %s long. Is this the right draft?' % (mmss(sections[-1]['end']), mmss(length)))
430
+ work = work_dir(args)
431
+ for s in sections:
432
+ s['fingerprint'] = sha(s['text'], args.voice, args.model, args.format, pronunciations())
433
+ s['file'] = os.path.join(work, '%s.%s' % (safe_id(s['id']), ext))
434
+ meta_path = os.path.join(work, '%s.json' % safe_id(s['id']))
435
+ try:
436
+ s['cached'] = os.path.isfile(s['file']) and not args.redo and read_json(meta_path)['fingerprint'] == s['fingerprint']
437
+ except (OSError, ValueError, KeyError):
438
+ s['cached'] = False
439
+ todo = [s for s in sections if not s['cached']]
440
+ to_send = sum(s['chars'] for s in todo)
441
+ remaining = remaining_characters()
442
+ rows = [dict(id=s['id'], title=s['title'], window=s['window'], chars=s['chars'], rough_seconds=round(s['chars'] / SPEECH_CHARS_PER_SECOND, 1),
443
+ may_be_long=s['chars'] / SPEECH_CHARS_PER_SECOND > s['window'] * 1.05, cached=s['cached'], has_optional=s['optional_chars'] > 0)
444
+ for s in sections]
445
+ plan = {'script': args.script, 'video': args.video, 'out': args.out, 'voice': args.voice, 'model': args.model, 'sections': rows,
446
+ 'characters_to_send': to_send, 'remaining': remaining, 'enough': None if remaining is None else remaining >= to_send, 'work': work}
447
+ if not args.confirm:
448
+ lines = ['Plan only, nothing was sent.', 'Voice %s, model %s. Video %s long.' % (args.voice, args.model, mmss(length)),
449
+ '%-5s %-30s %7s %6s %9s %s' % ('id', 'section', 'window', 'chars', '~seconds', 'note')]
450
+ for r in rows:
451
+ note = 'already made, no cost' if r['cached'] else ('may run long' + (' (has an optional sentence)' if r['has_optional'] else '') if r['may_be_long'] else '')
452
+ lines.append('%-5s %-30s %6.1fs %6d %9.1f %s' % (r['id'], r['title'][:30], r['window'], r['chars'], r['rough_seconds'], note))
453
+ lines += ['%d characters would be sent; credits left: %s.' % (to_send, 'unknown' if remaining is None else remaining),
454
+ '"~seconds" is a rough estimate from the character count. Run again with --confirm to create the voiceover.']
455
+ emit(args, dict(plan, confirmed=False), lines)
456
+ return PLAN_ONLY
457
+ if remaining is not None and remaining < to_send:
458
+ raise Stop('Not enough credits: %d characters to send, %d left. Nothing was sent.' % (to_send, remaining))
459
+ ensure_free([args.out, track_of(args.out)], args.force)
460
+ os.makedirs(work, exist_ok=True)
461
+ for s in todo:
462
+ data, meta = synthesize(s['text'], args.voice, args.model, args.format)
463
+ write_atomic(s['file'], data, True)
464
+ with open(os.path.join(work, '%s.json' % safe_id(s['id'])), 'w') as handle:
465
+ json.dump(dict(meta, fingerprint=s['fingerprint'], id=s['id']), handle)
466
+ report = []
467
+ for s in sections:
468
+ seconds = duration(s['file'])
469
+ row = {'id': s['id'], 'title': s['title'], 'window': s['window'], 'audio_seconds': seconds, 'fit': 'ok', 'speedup': 1.0, 'audio': s['file']}
470
+ if seconds is None:
471
+ raise Stop('Cannot read the audio for section %s.' % s['id'])
472
+ if seconds > s['window'] + SLACK:
473
+ ratio = seconds / s['window']
474
+ if ratio <= args.max_speedup:
475
+ fitted = os.path.join(work, '%s-fit.wav' % safe_id(s['id']))
476
+ run(['ffmpeg', '-y', '-loglevel', 'error', '-i', s['file'], '-filter:a', 'atempo=%.5f' % ratio, '-ar', '44100', '-ac', '1', fitted])
477
+ row.update(fit='sped_up', speedup=round(ratio, 4), audio=fitted, audio_seconds=duration(fitted))
478
+ else:
479
+ row.update(fit='too_long', speedup=round(ratio, 4), over_by=round(seconds - s['window'], 2), has_optional=s['optional_chars'] > 0)
480
+ report.append(row)
481
+ too_long = [r for r in report if r['fit'] == 'too_long']
482
+ if too_long:
483
+ lines = ['Created %d new section(s), but %d %s not fit its window, so no video was made. Finished sections are kept.' % (len(todo), len(too_long), 'does' if len(too_long) == 1 else 'do')]
484
+ lines += [' %s %s: %.1fs of speech in a %.1fs window (%.1fs over)%s' % (r['id'], r['title'], r['audio_seconds'], r['window'], r['over_by'],
485
+ ', has an optional sentence to drop' if r['has_optional'] else '') for r in too_long]
486
+ lines.append('Shorten the text in the script (or use --drop-optional), then run again: only the changed sections are sent.')
487
+ emit(args, {'confirmed': True, 'fit': False, 'sections': report}, lines)
488
+ return NO_FIT
489
+ mix = os.path.join(work, '_mix.wav')
490
+ chains = ['[%d:a]aformat=sample_rates=44100:channel_layouts=mono,adelay=%d:all=1[a%d]' % (i + 1, round(s['start'] * 1000), i) for i, s in enumerate(sections)]
491
+ chains.append('%samix=inputs=%d:duration=longest:normalize=0,apad=whole_dur=%.3f,atrim=end=%.3f[aout]'
492
+ % (''.join('[a%d]' % i for i in range(len(sections))), len(sections), length, length))
493
+ run(['ffmpeg', '-y', '-loglevel', 'error', '-i', args.video] + [x for r in report for x in ('-i', r['audio'])] +
494
+ ['-filter_complex', ';'.join(chains), '-map', '[aout]', '-ar', '44100', '-ac', '2', mix])
495
+ track, poster = mux(args.video, mix, args.out, length, args.force)
496
+ summary = {'created_utc': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), 'script': args.script, 'video': args.video,
497
+ 'voice': args.voice, 'model': args.model, 'format': args.format, 'characters_sent': to_send, 'sections': report}
498
+ with open(os.path.splitext(args.out)[0] + '-voiceover.json', 'w') as handle:
499
+ json.dump(summary, handle, indent=2)
500
+ emit(args, dict(summary, confirmed=True, fit=True, out=args.out, track=track, poster=poster),
501
+ ['Wrote %s' % args.out, 'Voiceover track: %s' % track] + (['Poster: %s' % poster] if poster else []) +
502
+ ['%d sections placed, %d sped up (never more than %.0f%%), %d characters sent.' % (len(report), sum(r['fit'] == 'sped_up' for r in report),
503
+ (args.max_speedup - 1) * 100, to_send),
504
+ 'Sections kept in %s so a re-run only sends what changed. Remove that folder when the video is final.' % work])
505
+ return OK
506
+
507
+
508
+ def cmd_polish(args):
509
+ check_inputs(args)
510
+ need_tools()
511
+ length, take = duration(args.video), duration(args.audio)
512
+ if not length or not take:
513
+ raise Stop('Cannot read the duration of the video or the recording.')
514
+ if args.isolate:
515
+ key()
516
+ work = work_dir(args)
517
+ plan = {'audio': args.audio, 'take_seconds': take, 'video': args.video, 'video_seconds': length, 'out': args.out, 'isolate': args.isolate,
518
+ 'normalize': args.normalize, 'trim': take > length + 0.5, 'pad': take < length - 0.5}
519
+ if args.isolate and not args.confirm:
520
+ emit(args, dict(plan, confirmed=False), ['Plan only, nothing was sent.', 'The take (%.1fs) would be cleaned by ElevenLabs, then levelled and placed on the %.1fs video.' % (take, length),
521
+ 'Run again with --confirm.'])
522
+ return PLAN_ONLY
523
+ ensure_free([args.out, track_of(args.out)], args.force)
524
+ os.makedirs(work, exist_ok=True)
525
+ source = args.audio
526
+ if args.isolate:
527
+ cleaned = os.path.join(work, 'take-clean-%s.mp3' % hashlib.sha256(read_bytes(args.audio)).hexdigest()[:12])
528
+ if not os.path.isfile(cleaned):
529
+ body, content_type = multipart({}, [('audio', os.path.basename(args.audio), read_bytes(args.audio))])
530
+ status, _, data = call('POST', '/v1/audio-isolation', body=body, content_type=content_type)
531
+ if status != 200 or not data:
532
+ raise Stop('Isolation failed. ' + explain(status, data))
533
+ write_atomic(cleaned, data, True)
534
+ source = cleaned
535
+ filters = (['loudnorm=I=-16:TP=-1.5:LRA=11'] if args.normalize else []) + (['afade=t=out:st=%.3f:d=0.3' % (length - 0.3)] if take > length + 0.5 else [])
536
+ filters += ['apad=whole_dur=%.3f' % length, 'atrim=end=%.3f' % length]
537
+ mix = os.path.join(work, '_mix.wav')
538
+ run(['ffmpeg', '-y', '-loglevel', 'error', '-i', source, '-af', ','.join(filters), '-ar', '44100', '-ac', '2', mix])
539
+ track, poster = mux(args.video, mix, args.out, length, args.force)
540
+ emit(args, dict(plan, confirmed=True, out=args.out, track=track, poster=poster),
541
+ ['Wrote %s' % args.out, 'Voiceover track: %s' % track] + (['Trimmed the take to the video length.'] if plan['trim'] else []))
542
+ return OK
543
+
544
+
545
+ # ---- command line
546
+
547
+ def text_arg(value):
548
+ if re.search(r'["$`\\\n]', value):
549
+ raise argparse.ArgumentTypeError('may not contain quotes, $, backticks or backslashes')
550
+ return value
551
+
552
+
553
+ def build_parser():
554
+ parser = argparse.ArgumentParser(prog='elevenlabs.py', description='ElevenLabs for the final voiceover of a narrated video.')
555
+ sub = parser.add_subparsers(dest='command', required=True)
556
+
557
+ def common(p, voice=False, confirm=False, out=False, work=False):
558
+ p.add_argument('--json', action='store_true', help='print JSON')
559
+ if voice:
560
+ p.add_argument('--voice', type=text_arg, default=os.environ.get('ELEVENLABS_VOICE', ''), help='voice id')
561
+ p.add_argument('--model', type=text_arg, default=os.environ.get('ELEVENLABS_MODEL') or DEFAULT_MODEL)
562
+ p.add_argument('--format', type=text_arg, default=DEFAULT_FORMAT)
563
+ if confirm:
564
+ p.add_argument('--confirm', action='store_true', help='actually send; without it only the plan is printed')
565
+ if out:
566
+ p.add_argument('--out', type=text_arg, required=True)
567
+ p.add_argument('--force', action='store_true', help='replace an existing file')
568
+ if work:
569
+ p.add_argument('--work', type=text_arg, default='', help='where the per-section audio is kept')
570
+
571
+ p = sub.add_parser('sections', help='the timed sections of a voiceover script')
572
+ p.add_argument('--script', type=text_arg, required=True)
573
+ p.add_argument('--drop-optional', action='store_true')
574
+ common(p)
575
+ p = sub.add_parser('voices', help='list the voices on the account')
576
+ p.add_argument('--search', type=text_arg, default='')
577
+ p.add_argument('--limit', type=int, default=30)
578
+ common(p)
579
+ p = sub.add_parser('account', help='plan and characters left')
580
+ common(p)
581
+ p = sub.add_parser('speak', help='one text file to one audio file')
582
+ p.add_argument('--text-file', type=text_arg, required=True)
583
+ common(p, voice=True, confirm=True, out=True)
584
+ p = sub.add_parser('isolate', help='remove background noise from a recording')
585
+ p.add_argument('--audio', type=text_arg, required=True)
586
+ common(p, confirm=True, out=True)
587
+ p = sub.add_parser('voiceover', help='speech for every section, fitted to its window, on the approved draft')
588
+ p.add_argument('--script', type=text_arg, required=True)
589
+ p.add_argument('--video', type=text_arg, required=True)
590
+ p.add_argument('--drop-optional', action='store_true')
591
+ p.add_argument('--max-speedup', type=float, default=1.08)
592
+ p.add_argument('--redo', action='store_true', help='ignore the sections already made')
593
+ common(p, voice=True, confirm=True, out=True, work=True)
594
+ p = sub.add_parser('polish', help='level a recorded take and put it on the approved draft')
595
+ p.add_argument('--audio', type=text_arg, required=True)
596
+ p.add_argument('--video', type=text_arg, required=True)
597
+ p.add_argument('--isolate', action='store_true', help='clean the take with ElevenLabs first')
598
+ p.add_argument('--no-normalize', dest='normalize', action='store_false')
599
+ common(p, confirm=True, out=True, work=True)
600
+ return parser
601
+
602
+
603
+ COMMANDS = {'sections': cmd_sections, 'voices': cmd_voices, 'account': cmd_account, 'speak': cmd_speak, 'isolate': cmd_isolate,
604
+ 'voiceover': cmd_voiceover, 'polish': cmd_polish}
605
+
606
+
607
+ def main(argv):
608
+ args = build_parser().parse_args(argv)
609
+ if args.command in ('speak', 'voiceover'):
610
+ if not args.voice:
611
+ print('No voice. Pass --voice <id>, or set voice_id in settings.yml (list them: -a voices).', file=sys.stderr)
612
+ return USAGE
613
+ audio_ext(args.format)
614
+ try:
615
+ if args.command in ('speak', 'voiceover'):
616
+ args.voice = resolve_voice(args.voice)
617
+ return COMMANDS[args.command](args)
618
+ except Stop as stop:
619
+ print(stop, file=sys.stderr)
620
+ return stop.code
621
+ except KeyboardInterrupt:
622
+ print('Interrupted. Sections already made are kept.', file=sys.stderr)
623
+ return ERROR
624
+
625
+
626
+ if __name__ == '__main__':
627
+ sys.exit(main(sys.argv[1:]))
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # 1. Create ProgressBar function
4
+ # 1.1 Input is currentState($1) and totalState($2)
5
+ function progress_bar {
6
+ # Process data
7
+ let _progress=(${1}*100/${2}*100)/100
8
+ let _done=(${_progress}*4)/10
9
+ let _left=40-$_done
10
+ # Build progressbar string lengths
11
+ _fill=$(printf "%${_done}s")
12
+ _empty=$(printf "%${_left}s")
13
+
14
+ # 1.2 Build progressbar strings and print the ProgressBar line
15
+ # 1.2.1 Output example:
16
+ # 1.2.1.1 Progress : [########################################] 100%
17
+ printf "\rProgress : [${_fill// /\#}${_empty// /-}] ${_progress}%%\n"
18
+
19
+ }