@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.
- package/LICENSE +339 -0
- package/README.md +28 -364
- package/core/bin/workspace.js +135 -0
- package/core/config/settings.labels.yml +514 -0
- package/core/config/settings.template.yml +292 -0
- package/core/config/style.yml +53 -0
- package/core/config/theme.default.yml +161 -0
- package/core/config/theme.dispatch.yml +166 -0
- package/core/config/theme.drupal.yml +175 -0
- package/core/config/theme.ember.yml +160 -0
- package/core/config/theme.github.yml +172 -0
- package/core/config/theme.gitlab.yml +168 -0
- package/core/config/theme.press.yml +161 -0
- package/core/config/theme.slate.yml +160 -0
- package/core/config/theme.upsun.yml +174 -0
- package/core/docs/architecture.svg +65 -0
- package/core/docs/getting-started.md +51 -0
- package/core/docs/plugins.md +27 -0
- package/core/docs/releases.md +23 -0
- package/core/docs/testing.md +15 -0
- package/core/docs/workspaces.md +25 -0
- package/core/scripts/args/arg-ai-items.sh +65 -0
- package/core/scripts/args/arg-autostart.sh +31 -0
- package/core/scripts/args/arg-avatar-agent.sh +28 -0
- package/core/scripts/args/arg-avatar-catalog.sh +12 -0
- package/core/scripts/args/arg-avatar-character.sh +68 -0
- package/core/scripts/args/arg-avatar-voice.sh +24 -0
- package/core/scripts/args/arg-avatar.sh +22 -0
- package/core/scripts/args/arg-checks.sh +36 -0
- package/core/scripts/args/arg-clone.sh +42 -0
- package/core/scripts/args/arg-commands.sh +51 -0
- package/core/scripts/args/arg-cucumber.sh +34 -0
- package/core/scripts/args/arg-distribution.sh +30 -0
- package/core/scripts/args/arg-drupal.sh +39 -0
- package/core/scripts/args/arg-drupal_cms.sh +18 -0
- package/core/scripts/args/arg-gitlab-ci-local.sh +25 -0
- package/core/scripts/args/arg-graphify.sh +36 -0
- package/core/scripts/args/arg-graphs.sh +12 -0
- package/core/scripts/args/arg-hostnames.sh +23 -0
- package/core/scripts/args/arg-milvus-backup.sh +8 -0
- package/core/scripts/args/arg-milvus-mcp.sh +12 -0
- package/core/scripts/args/arg-milvus.sh +32 -0
- package/core/scripts/args/arg-obsidian.sh +17 -0
- package/core/scripts/args/arg-odoo.sh +18 -0
- package/core/scripts/args/arg-penpot-backup.sh +8 -0
- package/core/scripts/args/arg-penpot-mcp.sh +15 -0
- package/core/scripts/args/arg-penpot-token.sh +17 -0
- package/core/scripts/args/arg-penpot.sh +44 -0
- package/core/scripts/args/arg-rag.sh +21 -0
- package/core/scripts/args/arg-ragify.sh +35 -0
- package/core/scripts/args/arg-reclaim.sh +18 -0
- package/core/scripts/args/arg-release.sh +21 -0
- package/core/scripts/args/arg-screen.sh +28 -0
- package/core/scripts/args/arg-site-template.sh +13 -0
- package/core/scripts/args/arg-spdd.sh +16 -0
- package/core/scripts/args/arg-specs.sh +22 -0
- package/core/scripts/args/arg-storage.sh +36 -0
- package/core/scripts/args/arg-testing.sh +36 -0
- package/core/scripts/args/arg-webship.sh +34 -0
- package/core/scripts/args/arg-webships-project.sh +22 -0
- package/core/scripts/args/arg-webships-site-template.sh +17 -0
- package/core/scripts/args/arg-webships.sh +34 -0
- package/core/scripts/args/arg-website-site-template.sh +17 -0
- package/core/scripts/args/arg-website.sh +25 -0
- package/core/scripts/args/arg-wordpress.sh +18 -0
- package/core/scripts/autostart-run.sh +162 -0
- package/core/scripts/autostart-stop.sh +30 -0
- package/core/scripts/autostart-watch.sh +43 -0
- package/core/scripts/autostart.sh +26 -0
- package/core/scripts/bootstrap-functions.sh +117 -0
- package/core/scripts/bootstrap-libraries.sh +10 -0
- package/core/scripts/bootstrap.sh +71 -0
- package/core/scripts/ddev-shim/ddev +122 -0
- package/core/scripts/docker-gid.sh +119 -0
- package/core/scripts/functions/arg-site-template.sh +28 -0
- package/core/scripts/functions/fun-ai-items-lock.sh +175 -0
- package/core/scripts/functions/fun-ai-items-sources.sh +298 -0
- package/core/scripts/functions/fun-ai-items.sh +621 -0
- package/core/scripts/functions/fun-ai-providers.sh +72 -0
- package/core/scripts/functions/fun-autostart.sh +429 -0
- package/core/scripts/functions/fun-avatar.sh +441 -0
- package/core/scripts/functions/fun-backup-item.sh +50 -0
- package/core/scripts/functions/fun-build-distribution.sh +226 -0
- package/core/scripts/functions/fun-build-site-template.sh +104 -0
- package/core/scripts/functions/fun-build-webships.sh +161 -0
- package/core/scripts/functions/fun-build-website.sh +199 -0
- package/core/scripts/functions/fun-checks.sh +243 -0
- package/core/scripts/functions/fun-clone-project.sh +289 -0
- package/core/scripts/functions/fun-commands.sh +521 -0
- package/core/scripts/functions/fun-database.sh +63 -0
- package/core/scripts/functions/fun-ddev-hostnames.sh +213 -0
- package/core/scripts/functions/fun-ddev-nodejs.sh +15 -0
- package/core/scripts/functions/fun-ddev-telemetry.sh +15 -0
- package/core/scripts/functions/fun-default-settings.sh +28 -0
- package/core/scripts/functions/fun-distribution-cucumber.sh +45 -0
- package/core/scripts/functions/fun-distribution-drupal.sh +25 -0
- package/core/scripts/functions/fun-distribution-drupal_cms.sh +35 -0
- package/core/scripts/functions/fun-distribution-webship.sh +28 -0
- package/core/scripts/functions/fun-distribution-webships.sh +33 -0
- package/core/scripts/functions/fun-distribution-website.sh +19 -0
- package/core/scripts/functions/fun-drush.sh +48 -0
- package/core/scripts/functions/fun-elevenlabs.sh +179 -0
- package/core/scripts/functions/fun-files.sh +19 -0
- package/core/scripts/functions/fun-git-worktrees.sh +142 -0
- package/core/scripts/functions/fun-gitlab-ci.sh +156 -0
- package/core/scripts/functions/fun-hostnames-cli.sh +88 -0
- package/core/scripts/functions/fun-milvus.sh +584 -0
- package/core/scripts/functions/fun-odoo.sh +121 -0
- package/core/scripts/functions/fun-penpot.sh +724 -0
- package/core/scripts/functions/fun-project-name.sh +20 -0
- package/core/scripts/functions/fun-rag.sh +253 -0
- package/core/scripts/functions/fun-ragify.sh +165 -0
- package/core/scripts/functions/fun-reclaim.sh +297 -0
- package/core/scripts/functions/fun-release.sh +159 -0
- package/core/scripts/functions/fun-remove-project.sh +319 -0
- package/core/scripts/functions/fun-screen.sh +96 -0
- package/core/scripts/functions/fun-spdd.sh +676 -0
- package/core/scripts/functions/fun-specs-share.sh +237 -0
- package/core/scripts/functions/fun-storage.sh +347 -0
- package/core/scripts/functions/fun-testing.sh +329 -0
- package/core/scripts/functions/fun-users.sh +87 -0
- package/core/scripts/functions/fun-version.sh +72 -0
- package/core/scripts/functions/fun-video-edit.sh +48 -0
- package/core/scripts/functions/fun-voice.sh +45 -0
- package/core/scripts/functions/fun-wordpress.sh +59 -0
- package/core/scripts/host-mounts.sh +161 -0
- package/core/scripts/install.sh +602 -0
- package/core/scripts/libs/__pycache__/video_edit.cpython-312.pyc +0 -0
- package/core/scripts/libs/argparse.sh +93 -0
- package/core/scripts/libs/bash-yaml.sh +52 -0
- package/core/scripts/libs/checks_plan.js +312 -0
- package/core/scripts/libs/elevenlabs.py +627 -0
- package/core/scripts/libs/progress-bar.sh +19 -0
- package/core/scripts/libs/rag_index.py +446 -0
- package/core/scripts/libs/video_edit.py +636 -0
- package/core/scripts/libs/voice.py +430 -0
- package/core/scripts/plugins/jira/host-install.sh +119 -0
- package/core/scripts/plugins/jira/host-mounts.txt +2 -0
- package/core/scripts/plugins/upsun/args/arg-upsun.sh +51 -0
- package/core/scripts/plugins/upsun/functions/fun-upsun.sh +304 -0
- package/core/scripts/plugins/upsun/host-install.sh +99 -0
- package/core/scripts/plugins/upsun/host-mounts.txt +1 -0
- package/core/scripts/plugins/upsun/upsun-ai.sh +133 -0
- package/core/scripts/plugins.sh +152 -0
- package/core/scripts/release.sh +69 -0
- package/core/scripts/storage.sh +39 -0
- package/core/scripts/sync-tooling.sh +246 -0
- package/core/scripts/tests/cmd-smoke-test.sh +109 -0
- package/core/scripts/trust-https.sh +257 -0
- package/core/workspace-app/.ddev/commands/web/test-dashboard +49 -0
- package/core/workspace-app/.ddev/config.yaml +400 -0
- package/core/workspace-app/.ddev/docker-compose.dood.yaml +45 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-jira-twg.yaml +26 -0
- package/core/workspace-app/.ddev/docker-compose.plugin-upsun-upsun-cli.yaml +8 -0
- package/core/workspace-app/.ddev/docker-compose.screens.yaml +15 -0
- package/core/workspace-app/.ddev/nginx_full/nginx-site.conf +114 -0
- package/core/workspace-app/.ddev/web-build/Dockerfile.workspace-app +203 -0
- package/core/workspace-app/.ddev/web-build/ddev-shim-ddev +122 -0
- package/core/workspace-app/README.md +380 -0
- package/{actions → core/workspace-app/actions}/ddev.js +0 -1
- package/{actions → core/workspace-app/actions}/rag.js +0 -1
- package/{actions → core/workspace-app/actions}/testing.js +0 -1
- package/{assistant.js → core/workspace-app/assistant.js} +1 -10
- package/{git-info.js → core/workspace-app/git-info.js} +17 -1
- package/core/workspace-app/package.json +29 -0
- package/core/workspace-app/plugin-catalog.js +50 -0
- package/{plugins-page.js → core/workspace-app/plugins-page.js} +28 -2
- package/{server.js → core/workspace-app/server.js} +18 -0
- package/core/workspace-app/tests/features/actions-rail.feature +44 -0
- package/core/workspace-app/tests/features/ai-items.feature +23 -0
- package/core/workspace-app/tests/features/assistant-speech.feature +63 -0
- package/core/workspace-app/tests/features/checks.feature +39 -0
- package/core/workspace-app/tests/features/clone-project.feature +30 -0
- package/core/workspace-app/tests/features/command-authoring.feature +58 -0
- package/core/workspace-app/tests/features/dashboard.feature +57 -0
- package/core/workspace-app/tests/features/menus.feature +29 -0
- package/core/workspace-app/tests/features/plugins/jira/jira.feature +47 -0
- package/core/workspace-app/tests/features/plugins.feature +20 -0
- package/core/workspace-app/tests/features/project-actions.feature +40 -0
- package/core/workspace-app/tests/features/screens.feature +29 -0
- package/core/workspace-app/tests/features/settings.feature +46 -0
- package/core/workspace-app/tests/features/test-reports.feature +32 -0
- package/core/workspace-app/tests/features/theme.feature +40 -0
- package/core/workspace-app/tests/step-definitions/actions-post.steps.js +25 -0
- package/core/workspace-app/tests/step-definitions/assistant-speech.steps.js +152 -0
- package/core/workspace-app/tests/step-definitions/preconditions.steps.js +100 -0
- package/core/workspace-app/tests/unit/_isolated-config.js +31 -0
- package/core/workspace-app/tests/unit/accessibility.test.js +124 -0
- package/core/workspace-app/tests/unit/actions-rail-ux.test.js +56 -0
- package/core/workspace-app/tests/unit/actions-rail.test.js +51 -0
- package/core/workspace-app/tests/unit/ai-inventory.test.js +64 -0
- package/core/workspace-app/tests/unit/app-docs.test.js +64 -0
- package/core/workspace-app/tests/unit/assistant-avatar.test.js +110 -0
- package/core/workspace-app/tests/unit/assistant-cast.test.js +105 -0
- package/core/workspace-app/tests/unit/assistant-send-animation.test.js +15 -0
- package/core/workspace-app/tests/unit/assistant-suggestions.test.js +112 -0
- package/core/workspace-app/tests/unit/assistant-switches.test.js +53 -0
- package/core/workspace-app/tests/unit/assistant-views.test.js +146 -0
- package/core/workspace-app/tests/unit/assistant-working-indicator.test.js +49 -0
- package/core/workspace-app/tests/unit/assistant.test.js +61 -0
- package/core/workspace-app/tests/unit/audio-player.test.js +147 -0
- package/core/workspace-app/tests/unit/audio-voice.test.js +634 -0
- package/core/workspace-app/tests/unit/auth-google.test.js +180 -0
- package/core/workspace-app/tests/unit/autostart-watch.test.js +76 -0
- package/core/workspace-app/tests/unit/browsers.test.js +76 -0
- package/core/workspace-app/tests/unit/build-distribution.test.js +125 -0
- package/core/workspace-app/tests/unit/build-order.test.js +153 -0
- package/core/workspace-app/tests/unit/builder-args.test.js +113 -0
- package/core/workspace-app/tests/unit/builders.test.js +158 -0
- package/core/workspace-app/tests/unit/card-state-toggle.test.js +29 -0
- package/core/workspace-app/tests/unit/changelog.test.js +177 -0
- package/core/workspace-app/tests/unit/chat-thread.test.js +92 -0
- package/core/workspace-app/tests/unit/checks-plan.test.js +205 -0
- package/core/workspace-app/tests/unit/claude-args.test.js +182 -0
- package/core/workspace-app/tests/unit/claude-code-here.test.js +15 -0
- package/core/workspace-app/tests/unit/claude-resume.test.js +147 -0
- package/core/workspace-app/tests/unit/claude-screen-options.test.js +84 -0
- package/core/workspace-app/tests/unit/claude-session.test.js +41 -0
- package/core/workspace-app/tests/unit/command-flags.test.js +45 -0
- package/core/workspace-app/tests/unit/command-palette.test.js +105 -0
- package/core/workspace-app/tests/unit/commands-center.test.js +338 -0
- package/core/workspace-app/tests/unit/composer-menu.test.js +187 -0
- package/core/workspace-app/tests/unit/config-yaml.test.js +116 -0
- package/core/workspace-app/tests/unit/confirm-dialog.test.js +69 -0
- package/core/workspace-app/tests/unit/create-card.test.js +85 -0
- package/core/workspace-app/tests/unit/ddev-addons.test.js +144 -0
- package/core/workspace-app/tests/unit/ddev-auth-ssh.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-custom-commands.test.js +56 -0
- package/core/workspace-app/tests/unit/ddev-diagnose.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-domains.test.js +153 -0
- package/core/workspace-app/tests/unit/ddev-hostnames.test.js +143 -0
- package/core/workspace-app/tests/unit/ddev-import.test.js +82 -0
- package/core/workspace-app/tests/unit/ddev-mail.test.js +135 -0
- package/core/workspace-app/tests/unit/ddev-menu-and-create.test.js +28 -0
- package/core/workspace-app/tests/unit/ddev-mutagen.test.js +37 -0
- package/core/workspace-app/tests/unit/ddev-options.test.js +51 -0
- package/core/workspace-app/tests/unit/ddev-share.test.js +40 -0
- package/core/workspace-app/tests/unit/ddev-shim.test.js +57 -0
- package/core/workspace-app/tests/unit/ddev-snapshots.test.js +142 -0
- package/core/workspace-app/tests/unit/ddev-start-daemons.test.js +62 -0
- package/core/workspace-app/tests/unit/ddev-xdebug.test.js +41 -0
- package/core/workspace-app/tests/unit/deep-chat-origin.test.js +38 -0
- package/core/workspace-app/tests/unit/density.test.js +52 -0
- package/core/workspace-app/tests/unit/elevenlabs.test.js +453 -0
- package/core/workspace-app/tests/unit/event-log.test.js +102 -0
- package/core/workspace-app/tests/unit/favicon.test.js +68 -0
- package/core/workspace-app/tests/unit/files-browser.test.js +224 -0
- package/core/workspace-app/tests/unit/git-info.test.js +26 -0
- package/core/workspace-app/tests/unit/grid-and-list.test.js +103 -0
- package/core/workspace-app/tests/unit/host-tool-wrapper.test.js +18 -0
- package/core/workspace-app/tests/unit/html-patterns.test.js +51 -0
- package/core/workspace-app/tests/unit/ide-infra.test.js +28 -0
- package/core/workspace-app/tests/unit/images-ai.test.js +201 -0
- package/core/workspace-app/tests/unit/images-create.test.js +59 -0
- package/core/workspace-app/tests/unit/images-providers.test.js +248 -0
- package/core/workspace-app/tests/unit/images-svg.test.js +213 -0
- package/core/workspace-app/tests/unit/images-workspace.test.js +144 -0
- package/core/workspace-app/tests/unit/jobs-fragment.test.js +66 -0
- package/core/workspace-app/tests/unit/media-ux.test.js +164 -0
- package/core/workspace-app/tests/unit/modal-fit.test.js +87 -0
- package/core/workspace-app/tests/unit/modules-load.test.js +23 -0
- package/core/workspace-app/tests/unit/narration.test.js +186 -0
- package/core/workspace-app/tests/unit/navigation-editor.test.js +260 -0
- package/core/workspace-app/tests/unit/navigation.test.js +254 -0
- package/core/workspace-app/tests/unit/new-folder.test.js +121 -0
- package/core/workspace-app/tests/unit/new-screen-form.test.js +149 -0
- package/core/workspace-app/tests/unit/node-menu.test.js +161 -0
- package/core/workspace-app/tests/unit/notes.test.js +128 -0
- package/core/workspace-app/tests/unit/page-structure.test.js +40 -0
- package/core/workspace-app/tests/unit/page-width.test.js +59 -0
- package/core/workspace-app/tests/unit/php-version.test.js +90 -0
- package/core/workspace-app/tests/unit/plan-card.test.js +44 -0
- package/core/workspace-app/tests/unit/plugin-catalog.test.js +31 -0
- package/core/workspace-app/tests/unit/plugin-system.test.js +251 -0
- package/core/workspace-app/tests/unit/plugins-page.test.js +28 -0
- package/core/workspace-app/tests/unit/plugins.test.js +58 -0
- package/core/workspace-app/tests/unit/project-activity-filter.test.js +180 -0
- package/core/workspace-app/tests/unit/project-make.test.js +143 -0
- package/core/workspace-app/tests/unit/project-name.test.js +202 -0
- package/core/workspace-app/tests/unit/reader.test.js +62 -0
- package/core/workspace-app/tests/unit/release.test.js +92 -0
- package/core/workspace-app/tests/unit/remove-confirm.test.js +37 -0
- package/core/workspace-app/tests/unit/report-pdf.test.js +171 -0
- package/core/workspace-app/tests/unit/round-11.test.js +31 -0
- package/core/workspace-app/tests/unit/round-12.test.js +37 -0
- package/core/workspace-app/tests/unit/row-layout.test.js +585 -0
- package/core/workspace-app/tests/unit/row-menus-fold.test.js +29 -0
- package/core/workspace-app/tests/unit/rows-add-modal.test.js +68 -0
- package/core/workspace-app/tests/unit/screen-runner-queue.test.js +41 -0
- package/core/workspace-app/tests/unit/screen-window.test.js +116 -0
- package/core/workspace-app/tests/unit/screens-beside-running.test.js +19 -0
- package/core/workspace-app/tests/unit/screens-columns.test.js +79 -0
- package/core/workspace-app/tests/unit/screens-menu.test.js +84 -0
- package/core/workspace-app/tests/unit/screens-modal-overlay.test.js +14 -0
- package/core/workspace-app/tests/unit/screens-waiting.test.js +129 -0
- package/core/workspace-app/tests/unit/screens.test.js +74 -0
- package/core/workspace-app/tests/unit/second-storage.test.js +218 -0
- package/core/workspace-app/tests/unit/session-name.test.js +64 -0
- package/core/workspace-app/tests/unit/settings-file.test.js +173 -0
- package/core/workspace-app/tests/unit/settings-nested-list.test.js +54 -0
- package/core/workspace-app/tests/unit/settings-raw.test.js +177 -0
- package/core/workspace-app/tests/unit/shared-prefs.test.js +49 -0
- package/core/workspace-app/tests/unit/speech.test.js +290 -0
- package/core/workspace-app/tests/unit/static-caching.test.js +39 -0
- package/core/workspace-app/tests/unit/status-language.test.js +47 -0
- package/core/workspace-app/tests/unit/tab-modal.test.js +69 -0
- package/core/workspace-app/tests/unit/tab-title.test.js +37 -0
- package/core/workspace-app/tests/unit/template-literals.test.js +52 -0
- package/core/workspace-app/tests/unit/terminal-scrollback.test.js +61 -0
- package/core/workspace-app/tests/unit/terminal-size.test.js +103 -0
- package/core/workspace-app/tests/unit/terminal-style.test.js +84 -0
- package/core/workspace-app/tests/unit/terminal-windows.test.js +154 -0
- package/core/workspace-app/tests/unit/themes.test.js +453 -0
- package/core/workspace-app/tests/unit/tool-labels.test.js +34 -0
- package/core/workspace-app/tests/unit/topbar-icons.test.js +47 -0
- package/core/workspace-app/tests/unit/update-docker-gid.test.js +51 -0
- package/core/workspace-app/tests/unit/user-guide.test.js +60 -0
- package/core/workspace-app/tests/unit/version-check.test.js +98 -0
- package/core/workspace-app/tests/unit/version-link.test.js +50 -0
- package/core/workspace-app/tests/unit/video-borrow.test.js +198 -0
- package/core/workspace-app/tests/unit/video-edit-ask.test.js +93 -0
- package/core/workspace-app/tests/unit/video-edit-short.test.js +42 -0
- package/core/workspace-app/tests/unit/video-edit.test.js +417 -0
- package/core/workspace-app/tests/unit/video-transcript.test.js +237 -0
- package/core/workspace-app/tests/unit/waiting.test.js +48 -0
- package/core/workspace-app/tests/unit/wordpress-odoo-builders.test.js +38 -0
- package/core/workspace-app/tests/unit/workspace-bin.test.js +48 -0
- package/core/workspace-app/tests/unit/workspace-card-tools.test.js +30 -0
- package/core/workspace-app/tests/unit/workspace-delete.test.js +77 -0
- package/core/workspace-app/tests/unit/workspace-types.test.js +68 -0
- package/core/workspace-app/tests/unit/worktree-clone.test.js +250 -0
- package/package.json +38 -36
- /package/{PLUGINS.md → core/workspace-app/PLUGINS.md} +0 -0
- /package/{WORKSPACES.md → core/workspace-app/WORKSPACES.md} +0 -0
- /package/{actions → core/workspace-app/actions}/ai-items.js +0 -0
- /package/{actions → core/workspace-app/actions}/audio.js +0 -0
- /package/{actions → core/workspace-app/actions}/build-order.js +0 -0
- /package/{actions → core/workspace-app/actions}/checks.js +0 -0
- /package/{actions → core/workspace-app/actions}/claude-session.js +0 -0
- /package/{actions → core/workspace-app/actions}/commands-center.js +0 -0
- /package/{actions → core/workspace-app/actions}/composer.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-addons.js +0 -0
- /package/{actions → core/workspace-app/actions}/ddev-mail.js +0 -0
- /package/{actions → core/workspace-app/actions}/folders.js +0 -0
- /package/{actions → core/workspace-app/actions}/images-svg.js +0 -0
- /package/{actions → core/workspace-app/actions}/images.js +0 -0
- /package/{actions → core/workspace-app/actions}/navigation.js +0 -0
- /package/{actions → core/workspace-app/actions}/node.js +0 -0
- /package/{actions → core/workspace-app/actions}/notes.js +0 -0
- /package/{actions → core/workspace-app/actions}/plugins.js +0 -0
- /package/{actions → core/workspace-app/actions}/project-make.js +0 -0
- /package/{actions → core/workspace-app/actions}/row-actions.js +0 -0
- /package/{actions → core/workspace-app/actions}/screens.js +0 -0
- /package/{actions → core/workspace-app/actions}/video-edit.js +0 -0
- /package/{ai-inventory.js → core/workspace-app/ai-inventory.js} +0 -0
- /package/{ai-items.js → core/workspace-app/ai-items.js} +0 -0
- /package/{assistant-cast.js → core/workspace-app/assistant-cast.js} +0 -0
- /package/{assistant-suggestions.js → core/workspace-app/assistant-suggestions.js} +0 -0
- /package/{audio-voice.js → core/workspace-app/audio-voice.js} +0 -0
- /package/{auth-google.js → core/workspace-app/auth-google.js} +0 -0
- /package/{build-ai.js → core/workspace-app/build-ai.js} +0 -0
- /package/{build-order-page.js → core/workspace-app/build-order-page.js} +0 -0
- /package/{build-order.js → core/workspace-app/build-order.js} +0 -0
- /package/{builder-args.js → core/workspace-app/builder-args.js} +0 -0
- /package/{changelog.js → core/workspace-app/changelog.js} +0 -0
- /package/{chat-threads.js → core/workspace-app/chat-threads.js} +0 -0
- /package/{checks.js → core/workspace-app/checks.js} +0 -0
- /package/{claude-args.js → core/workspace-app/claude-args.js} +0 -0
- /package/{claude-sessions.js → core/workspace-app/claude-sessions.js} +0 -0
- /package/{command-author.js → core/workspace-app/command-author.js} +0 -0
- /package/{command-block.js → core/workspace-app/command-block.js} +0 -0
- /package/{commands → core/workspace-app/commands}/defaults.yml +0 -0
- /package/{commands-page.js → core/workspace-app/commands-page.js} +0 -0
- /package/{commands.js → core/workspace-app/commands.js} +0 -0
- /package/{cucumber.js → core/workspace-app/cucumber.js} +0 -0
- /package/{ddev-addons-page.js → core/workspace-app/ddev-addons-page.js} +0 -0
- /package/{ddev-addons.js → core/workspace-app/ddev-addons.js} +0 -0
- /package/{ddev-custom-commands.js → core/workspace-app/ddev-custom-commands.js} +0 -0
- /package/{ddev-domains.js → core/workspace-app/ddev-domains.js} +0 -0
- /package/{ddev-import.js → core/workspace-app/ddev-import.js} +0 -0
- /package/{ddev-mail-page.js → core/workspace-app/ddev-mail-page.js} +0 -0
- /package/{ddev-mail.js → core/workspace-app/ddev-mail.js} +0 -0
- /package/{ddev-options.js → core/workspace-app/ddev-options.js} +0 -0
- /package/{ddev-snapshots.js → core/workspace-app/ddev-snapshots.js} +0 -0
- /package/{ddev.js → core/workspace-app/ddev.js} +0 -0
- /package/{docs → core/workspace-app/docs}/DEVELOPER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/README.md +0 -0
- /package/{docs → core/workspace-app/docs}/REFERENCE.md +0 -0
- /package/{docs → core/workspace-app/docs}/USER-GUIDE.md +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-board.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-coffee.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-colours.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-icons.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-moves.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-pick-cookieman.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-settings-voices.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/assistant/avatar-status.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/create-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/create-card/start-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-address.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-off.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-clone-worktree-on.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ddev-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-domains.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-home.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-ide-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-remove-refusal.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-rows-worktree.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-screens.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/guide-workspace-page.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/card-settings.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/drag-reorder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/home/edit-card.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/claude-code-image.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/edit-with-ai.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/gallery.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-description.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/generate-plan.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/providers.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/svg-result.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/images/upload.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/2-form.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/8-ddev-options.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/new-folder/files-folder.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/ddev-custom-commands.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/projects/state-chip-stop.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dashboard-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/dev-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/docs-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/ai.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/code.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/docs.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/services.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/icons/sites.svg +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-report-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-scenarios.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/test-steps.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-dark.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/readme/videos-light.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-menu.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/screens-new.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/auth-google-lists.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-1-row.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-2-confirm.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-3-type-name.png +0 -0
- /package/{docs → core/workspace-app/docs}/screenshots/settings/delete-4-type-yes.png +0 -0
- /package/{event-log.js → core/workspace-app/event-log.js} +0 -0
- /package/{files-browser.js → core/workspace-app/files-browser.js} +0 -0
- /package/{folders.js → core/workspace-app/folders.js} +0 -0
- /package/{format.js → core/workspace-app/format.js} +0 -0
- /package/{github-auth.js → core/workspace-app/github-auth.js} +0 -0
- /package/{html.js → core/workspace-app/html.js} +0 -0
- /package/{icons.js → core/workspace-app/icons.js} +0 -0
- /package/{images-ai.js → core/workspace-app/images-ai.js} +0 -0
- /package/{images-create.js → core/workspace-app/images-create.js} +0 -0
- /package/{images-page.js → core/workspace-app/images-page.js} +0 -0
- /package/{images-records.js → core/workspace-app/images-records.js} +0 -0
- /package/{images-render.js → core/workspace-app/images-render.js} +0 -0
- /package/{images-svg-core.js → core/workspace-app/images-svg-core.js} +0 -0
- /package/{images-svg-sanitize.js → core/workspace-app/images-svg-sanitize.js} +0 -0
- /package/{images-svg.js → core/workspace-app/images-svg.js} +0 -0
- /package/{images.js → core/workspace-app/images.js} +0 -0
- /package/{jobs.js → core/workspace-app/jobs.js} +0 -0
- /package/{linger.js → core/workspace-app/linger.js} +0 -0
- /package/{lists.js → core/workspace-app/lists.js} +0 -0
- /package/{media-info.js → core/workspace-app/media-info.js} +0 -0
- /package/{navigation-page.js → core/workspace-app/navigation-page.js} +0 -0
- /package/{navigation.js → core/workspace-app/navigation.js} +0 -0
- /package/{notes.js → core/workspace-app/notes.js} +0 -0
- /package/{palette-index.js → core/workspace-app/palette-index.js} +0 -0
- /package/{playwright.config.js → core/workspace-app/playwright.config.js} +0 -0
- /package/{plugin-install.js → core/workspace-app/plugin-install.js} +0 -0
- /package/{plugin-loader.js → core/workspace-app/plugin-loader.js} +0 -0
- /package/{plugin-manifest.js → core/workspace-app/plugin-manifest.js} +0 -0
- /package/{plugin-settings.js → core/workspace-app/plugin-settings.js} +0 -0
- /package/{plugins → core/workspace-app/plugins}/workspace-notes/index.js +0 -0
- /package/{project-make.js → core/workspace-app/project-make.js} +0 -0
- /package/{project-name.js → core/workspace-app/project-name.js} +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-moves.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar-ui.js +0 -0
- /package/{public → core/workspace-app/public}/assistant-avatar.js +0 -0
- /package/{public → core/workspace-app/public}/avatar/druplicon.vrm +0 -0
- /package/{public → core/workspace-app/public}/css/base.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ai-assistant.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-avatar.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/assistant-views.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/build-order.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/clone-as-a-git-worktree.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/command-palette.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/commands-center.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/confirm-dialog.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/create-card.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/ddev-menu.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/deep-chat.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/density.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/floating-assistant-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/folders.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-left-assistant-sidebar-all-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/full-height-right-actions-rail-pages-that-have-actions.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/grid-and-list.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/help-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/hero.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/images.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/layout-rhythm-and-spacing-polish.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/mic-on-any-field-that-wants-typing.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/modal-fit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/page-width.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/plugins-page.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/project-rows.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/reordering-the-workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/responsive-home-and-the-workspace-pages.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/running-commands-as-toasts.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/settings-editor-core-config-yml.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/short-results-as-a-toast-at-the-top.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/sound-player.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/suggest-agents-skills-commands.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/tab-modal.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/terminals.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/test-report-media-one-file-per-row-paged.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/top-toolbar-logo-breadcrumb.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/video-edit.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/web-ide-open-with.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/working-indicators.css +0 -0
- /package/{public → core/workspace-app/public}/css/components/workspace-cards.css +0 -0
- /package/{public → core/workspace-app/public}/favicon.ico +0 -0
- /package/{public → core/workspace-app/public}/favicon.png +0 -0
- /package/{public → core/workspace-app/public}/htmx.min.js +0 -0
- /package/{public → core/workspace-app/public}/logo.png +0 -0
- /package/{public → core/workspace-app/public}/media-player.js +0 -0
- /package/{public → core/workspace-app/public}/mic.js +0 -0
- /package/{public → core/workspace-app/public}/narration.js +0 -0
- /package/{public → core/workspace-app/public}/palette.js +0 -0
- /package/{public → core/workspace-app/public}/sortable.js +0 -0
- /package/{public → core/workspace-app/public}/suggest.js +0 -0
- /package/{public → core/workspace-app/public}/terminal.js +0 -0
- /package/{public → core/workspace-app/public}/themes/default/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/ember/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/press/theme.css +0 -0
- /package/{public → core/workspace-app/public}/themes/slate/theme.css +0 -0
- /package/{public → core/workspace-app/public}/touch-icon.png +0 -0
- /package/{public → core/workspace-app/public}/ui.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ace.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-language_tools.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/ext-searchbox.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-css.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-html.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-javascript.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-markdown.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-sh.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/mode-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-textmate.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/theme-tomorrow_night.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/ace/worker-yaml.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/deep-chat.bundle.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/dm-serif-text-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-italic.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/figtree-latin-600-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/fonts/ubuntu-mono-latin-wght-normal.woff2 +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-LICENSE.txt +0 -0
- /package/{public → core/workspace-app/public}/vendor/tabler-sprite.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/loaders/GLTFLoader.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/BufferGeometryUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/addons/utils/SkeletonUtils.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.core.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three/three.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/LICENSE +0 -0
- /package/{public → core/workspace-app/public}/vendor/three-vrm/three-vrm.module.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit-icons.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/uikit.min.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/bookmark-outlined.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/headset.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/menu.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/pause.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/play.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/share.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-off.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/sound-on.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/webship-icons/ui/stop.svg +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-fit.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-unicode11.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/addon-web-links.js +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.css +0 -0
- /package/{public → core/workspace-app/public}/vendor/xterm/xterm.js +0 -0
- /package/{rag.js → core/workspace-app/rag.js} +0 -0
- /package/{reader.js → core/workspace-app/reader.js} +0 -0
- /package/{repo-blobs.js → core/workspace-app/repo-blobs.js} +0 -0
- /package/{report-pdf.js → core/workspace-app/report-pdf.js} +0 -0
- /package/{row-layout.js → core/workspace-app/row-layout.js} +0 -0
- /package/{screenrc → core/workspace-app/screenrc} +0 -0
- /package/{screens.js → core/workspace-app/screens.js} +0 -0
- /package/{scripts → core/workspace-app/scripts}/code-server-start.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/images-claude-finish.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-host.sh +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-runner.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/screen-then.js +0 -0
- /package/{scripts → core/workspace-app/scripts}/update.sh +0 -0
- /package/{settings-file.js → core/workspace-app/settings-file.js} +0 -0
- /package/{speech.js → core/workspace-app/speech.js} +0 -0
- /package/{tab-modal.js → core/workspace-app/tab-modal.js} +0 -0
- /package/{test-reports.js → core/workspace-app/test-reports.js} +0 -0
- /package/{themes.js → core/workspace-app/themes.js} +0 -0
- /package/{transcripts.js → core/workspace-app/transcripts.js} +0 -0
- /package/{urls.js → core/workspace-app/urls.js} +0 -0
- /package/{validate.js → core/workspace-app/validate.js} +0 -0
- /package/{version-check.js → core/workspace-app/version-check.js} +0 -0
- /package/{video-edit.js → core/workspace-app/video-edit.js} +0 -0
- /package/{waiting.js → core/workspace-app/waiting.js} +0 -0
- /package/{workspace-card-tools.js → core/workspace-app/workspace-card-tools.js} +0 -0
- /package/{workspace-delete.js → core/workspace-app/workspace-delete.js} +0 -0
- /package/{workspace-types.js → core/workspace-app/workspace-types.js} +0 -0
- /package/{workspaces.js → core/workspace-app/workspaces.js} +0 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Edit a video into a NEW version of itself: cut, speed up or slow down, add a section, drop a section,
|
|
3
|
+
open and close with the Webship logo, and put music under either end. Standard library plus ffmpeg.
|
|
4
|
+
|
|
5
|
+
The video it starts from is never changed. A change is described as a timeline of pieces (a stretch of the
|
|
6
|
+
source, or a card with the logo), one ffmpeg run builds it, and the timed voiceover script beside the video is
|
|
7
|
+
rewritten so every section still points at the same words in the new one.
|
|
8
|
+
Exit codes: 0 ok, 1 error, 2 usage, 3 no key, 4 plan only.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import datetime
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
|
|
21
|
+
sys.dont_write_bytecode = True
|
|
22
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
23
|
+
import elevenlabs as el # noqa: E402
|
|
24
|
+
|
|
25
|
+
OK, ERROR, USAGE, PLAN_ONLY = el.OK, el.ERROR, el.USAGE, el.PLAN_ONLY
|
|
26
|
+
MIN_SPEED, MAX_SPEED = 0.25, 4.0
|
|
27
|
+
CARD_SECONDS = (1.0, 10.0)
|
|
28
|
+
SECTION_PAD = 0.8
|
|
29
|
+
FADE_IN, FADE_OUT = 0.6, 1.8
|
|
30
|
+
BRAND_BG = '#2b447c'
|
|
31
|
+
CHROME = ('google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def workspace_root():
|
|
35
|
+
return os.environ.get('WORKSPACE_ROOT') or os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..'))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def public_dir():
|
|
39
|
+
return os.path.join(workspace_root(), 'core', 'workspace-app', 'public')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---- the timeline: pieces in order, and where a moment of the source ends up --------------------------
|
|
43
|
+
|
|
44
|
+
class Piece:
|
|
45
|
+
"""A stretch of the source video (`kind` source), or a card shown for `seconds` (`kind` card)."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, kind, start=0.0, end=0.0, speed=1.0, seconds=0.0, title='', subtitle='', audio=None, src=0):
|
|
48
|
+
# src 0 is the video being edited; 1.. are videos a section was borrowed from (their own clock)
|
|
49
|
+
self.kind, self.start, self.end, self.speed, self.src = kind, start, end, speed, src
|
|
50
|
+
self.seconds, self.title, self.subtitle, self.audio = seconds, title, subtitle, audio
|
|
51
|
+
self.out_start = 0.0
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def length(self):
|
|
55
|
+
return (self.end - self.start) / self.speed if self.kind == 'source' else self.seconds
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def lay_out(pieces):
|
|
59
|
+
at = 0.0
|
|
60
|
+
for piece in pieces:
|
|
61
|
+
piece.out_start = at
|
|
62
|
+
at += piece.length
|
|
63
|
+
return at
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def source_pieces(total, cuts, speed):
|
|
67
|
+
"""The source with the `cuts` (start, end) taken out, every kept stretch at `speed`."""
|
|
68
|
+
kept, at = [], 0.0
|
|
69
|
+
for start, end in sorted(cuts):
|
|
70
|
+
start, end = max(0.0, start), min(total, end)
|
|
71
|
+
if end <= start:
|
|
72
|
+
continue
|
|
73
|
+
if start > at:
|
|
74
|
+
kept.append((at, start))
|
|
75
|
+
at = max(at, end)
|
|
76
|
+
if at < total:
|
|
77
|
+
kept.append((at, total))
|
|
78
|
+
return [Piece('source', a, b, speed) for a, b in kept if b - a > 0.04]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---- the script: timed sections the way the voiceover tools read them -------------------------------
|
|
82
|
+
|
|
83
|
+
def section_heading(sid, title, start, end):
|
|
84
|
+
return '## %s %s (%s to %s)' % (sid, title, el.mmss(start), el.mmss(end))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_blocks(text):
|
|
88
|
+
"""The script as a preamble and a list of sections {id, title, start, end, lines}."""
|
|
89
|
+
pre, sections, current = [], [], None
|
|
90
|
+
for line in text.splitlines():
|
|
91
|
+
found = el.HEADING.match(line)
|
|
92
|
+
if found:
|
|
93
|
+
current = {'id': found.group(1), 'title': found.group(2), 'start': el.seconds_of(found.group(3)),
|
|
94
|
+
'end': el.seconds_of(found.group(4)), 'lines': []}
|
|
95
|
+
sections.append(current)
|
|
96
|
+
elif line.startswith('## '):
|
|
97
|
+
current = {'id': None, 'lines': [line]}
|
|
98
|
+
sections.append(current)
|
|
99
|
+
elif current is None:
|
|
100
|
+
pre.append(line)
|
|
101
|
+
else:
|
|
102
|
+
current['lines'].append(line)
|
|
103
|
+
return pre, sections
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def retime_script(text, pieces, added=()):
|
|
107
|
+
"""The script for the new video. A section cut away is dropped; one cut in part is shortened; the rest
|
|
108
|
+
follow their words to where the pieces put them. `added` sections (dicts with after, id, title, lines)
|
|
109
|
+
go in where their card now is. Anything that is not a timed section is kept where it was."""
|
|
110
|
+
pre, sections = parse_blocks(text)
|
|
111
|
+
out = list(pre)
|
|
112
|
+
placed = {}
|
|
113
|
+
for a in added:
|
|
114
|
+
placed.setdefault(a['after'], []).append(a)
|
|
115
|
+
for a in placed.pop('start', []):
|
|
116
|
+
out += card_section_lines(a, pieces)
|
|
117
|
+
for section in sections:
|
|
118
|
+
if section['id'] is None:
|
|
119
|
+
out += section['lines']
|
|
120
|
+
continue
|
|
121
|
+
inside = [p for p in pieces if p.kind == 'source' and p.src == 0 and p.end > section['start'] + 1e-6 and p.start < section['end'] - 1e-6]
|
|
122
|
+
if inside:
|
|
123
|
+
start = min(p.out_start + (max(p.start, section['start']) - p.start) / p.speed for p in inside)
|
|
124
|
+
end = max(p.out_start + (min(p.end, section['end']) - p.start) / p.speed for p in inside)
|
|
125
|
+
out += [section_heading(section['id'], section['title'], start, end)] + section['lines']
|
|
126
|
+
for a in placed.pop(section['id'], []):
|
|
127
|
+
out += card_section_lines(a, pieces)
|
|
128
|
+
for a in placed.pop('end', []):
|
|
129
|
+
out += card_section_lines(a, pieces)
|
|
130
|
+
for rest in placed.values():
|
|
131
|
+
for a in rest:
|
|
132
|
+
out += card_section_lines(a, pieces)
|
|
133
|
+
text = '\n'.join(out).rstrip('\n') + '\n'
|
|
134
|
+
return re.sub(r'\n{3,}', '\n\n', text)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def card_section_lines(added, pieces):
|
|
138
|
+
span = added.get('pieces') or [added['card']]
|
|
139
|
+
start, end = span[0].out_start, span[-1].out_start + span[-1].length
|
|
140
|
+
body = list(added['lines']) if added.get('lines') else ['> ' + added['text']]
|
|
141
|
+
while body and not body[-1].strip():
|
|
142
|
+
body.pop()
|
|
143
|
+
return ['', section_heading(added['id'], added['title'], start, end)] + body + ['']
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def free_section_id(sections, prefix='N'):
|
|
147
|
+
taken = {s['id'] for s in sections if s['id']}
|
|
148
|
+
number = 1
|
|
149
|
+
while '%s%02d' % (prefix, number) in taken:
|
|
150
|
+
number += 1
|
|
151
|
+
return '%s%02d' % (prefix, number)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---- reading the video ------------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
def probe(path):
|
|
157
|
+
done = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'stream=codec_type,width,height,r_frame_rate',
|
|
158
|
+
'-show_entries', 'format=duration', '-of', 'json', path], capture_output=True, text=True, timeout=60)
|
|
159
|
+
try:
|
|
160
|
+
data = json.loads(done.stdout)
|
|
161
|
+
except ValueError:
|
|
162
|
+
raise el.Stop('Cannot read the video: %s' % path)
|
|
163
|
+
video = next((s for s in data.get('streams', []) if s.get('codec_type') == 'video'), None)
|
|
164
|
+
if not video:
|
|
165
|
+
raise el.Stop('There is no picture in %s.' % path)
|
|
166
|
+
num, _, den = (video.get('r_frame_rate') or '30/1').partition('/')
|
|
167
|
+
fps = float(num) / float(den or 1) if float(den or 1) else 30.0
|
|
168
|
+
return {'width': int(video['width']) // 2 * 2, 'height': int(video['height']) // 2 * 2, 'fps': round(fps, 3) if 1 <= fps <= 120 else 30.0,
|
|
169
|
+
'duration': float(data['format']['duration']), 'has_audio': any(s.get('codec_type') == 'audio' for s in data.get('streams', []))}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def parse_range(value):
|
|
173
|
+
found = re.match(r'^\s*([\d:.]+)\s*-\s*([\d:.]+)\s*$', value or '')
|
|
174
|
+
if not found:
|
|
175
|
+
raise el.Stop('A range is "start-end", in seconds or minutes:seconds, like 0:05-0:40.', USAGE)
|
|
176
|
+
return el.seconds_of(found.group(1)), el.seconds_of(found.group(2))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---- cards: the Webship logo, and words, as a picture ------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def chrome():
|
|
182
|
+
for name in CHROME:
|
|
183
|
+
found = shutil.which(name)
|
|
184
|
+
if found:
|
|
185
|
+
return found
|
|
186
|
+
raise el.Stop('A card needs Chrome or Chromium on this machine, and none was found.')
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def card_html(title, subtitle, width, height):
|
|
190
|
+
pub = public_dir()
|
|
191
|
+
fonts = os.path.join(pub, 'vendor', 'fonts')
|
|
192
|
+
logo = os.path.join(pub, 'logo.png')
|
|
193
|
+
if not os.path.isfile(logo):
|
|
194
|
+
raise el.Stop('The workspace logo is missing: %s' % logo)
|
|
195
|
+
esc = lambda s: s.replace('&', '&').replace('<', '<').replace('>', '>') # noqa: E731
|
|
196
|
+
scale = height / 720.0
|
|
197
|
+
return ('<!doctype html><meta charset="utf-8"><style>'
|
|
198
|
+
'@font-face{font-family:Figtree;font-weight:600;src:url("file://%(f)s/figtree-latin-600-normal.woff2")}'
|
|
199
|
+
'@font-face{font-family:Figtree;font-weight:400;src:url("file://%(f)s/figtree-latin-400-normal.woff2")}'
|
|
200
|
+
'html,body{margin:0;width:%(w)dpx;height:%(h)dpx;background:%(bg)s;display:flex;flex-direction:column;align-items:center;'
|
|
201
|
+
'justify-content:center;font-family:Figtree,sans-serif;text-align:center;overflow:hidden}'
|
|
202
|
+
'img{width:%(logo)dpx}h1{color:#fff;font-weight:600;font-size:%(t)dpx;margin:%(gap)dpx 8%% 0;line-height:1.25}'
|
|
203
|
+
'p{color:#deedff;font-size:%(s)dpx;margin:%(gap2)dpx 8%% 0;line-height:1.35}</style>'
|
|
204
|
+
'<img src="file://%(logo_file)s">%(h1)s%(p)s'
|
|
205
|
+
% {'f': fonts, 'w': width, 'h': height, 'bg': BRAND_BG, 'logo': int(420 * scale), 't': int(44 * scale), 's': int(26 * scale),
|
|
206
|
+
'gap': int(40 * scale), 'gap2': int(10 * scale), 'logo_file': logo,
|
|
207
|
+
'h1': '<h1>%s</h1>' % esc(title) if title else '', 'p': '<p>%s</p>' % esc(subtitle) if subtitle else ''})
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def render_card(path, title, subtitle, width, height):
|
|
211
|
+
page = path + '.html'
|
|
212
|
+
with open(page, 'w', encoding='utf-8') as handle:
|
|
213
|
+
handle.write(card_html(title, subtitle, width, height))
|
|
214
|
+
profile = tempfile.mkdtemp(prefix='ws-card-profile-')
|
|
215
|
+
try:
|
|
216
|
+
done = subprocess.run([chrome(), '--headless=new', '--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--allow-file-access-from-files',
|
|
217
|
+
'--user-data-dir=' + profile, '--window-size=%d,%d' % (width, height), '--screenshot=' + path, 'file://' + page],
|
|
218
|
+
capture_output=True, text=True, timeout=120)
|
|
219
|
+
finally:
|
|
220
|
+
shutil.rmtree(profile, ignore_errors=True)
|
|
221
|
+
if not os.path.isfile(path):
|
|
222
|
+
raise el.Stop('The card was not drawn: %s' % ((done.stderr or '').strip().splitlines()[-1:] or 'no output'))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---- building it ------------------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
def atempo_chain(speed):
|
|
228
|
+
"""atempo takes 0.5 to 2 in older builds; chain it so any speed in range works."""
|
|
229
|
+
chain, rest = [], speed
|
|
230
|
+
while rest > 2.0:
|
|
231
|
+
chain.append(2.0)
|
|
232
|
+
rest /= 2.0
|
|
233
|
+
while rest < 0.5:
|
|
234
|
+
chain.append(0.5)
|
|
235
|
+
rest /= 0.5
|
|
236
|
+
chain.append(rest)
|
|
237
|
+
return ','.join('atempo=%.5f' % f for f in chain)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_filters(pieces, info, music_in, music_out, volume, cards):
|
|
241
|
+
"""The ffmpeg inputs and filter graph for the timeline. `cards` maps a card piece to its picture."""
|
|
242
|
+
extras = info.get('extra', [])
|
|
243
|
+
inputs, chains, labels = [], [], []
|
|
244
|
+
count = [0]
|
|
245
|
+
|
|
246
|
+
def add(*flags):
|
|
247
|
+
inputs.extend(flags)
|
|
248
|
+
count[0] += 1
|
|
249
|
+
return count[0] - 1
|
|
250
|
+
|
|
251
|
+
w, h, fps = info['width'], info['height'], info['fps']
|
|
252
|
+
fmt = 'aformat=sample_rates=44100:channel_layouts=stereo'
|
|
253
|
+
for i, piece in enumerate(pieces):
|
|
254
|
+
if piece.kind == 'source':
|
|
255
|
+
origin = info if piece.src == 0 else extras[piece.src - 1]
|
|
256
|
+
has_audio = origin['has_audio']
|
|
257
|
+
# every stretch is its own seeked input of the file: one input split into many trims makes ffmpeg
|
|
258
|
+
# hold the whole rest of the video in memory while the timeline waits on an earlier piece
|
|
259
|
+
n = add('-ss', '%.3f' % piece.start, '-t', '%.3f' % (piece.end - piece.start), '-i', origin['path'])
|
|
260
|
+
# a borrowed picture keeps its own shape: fitted inside this video's frame, never stretched
|
|
261
|
+
fit = ('scale=%d:%d' % (w, h)) if piece.src == 0 else \
|
|
262
|
+
('scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2:color=black' % (w, h, w, h))
|
|
263
|
+
chains.append('[%d:v]setpts=(PTS-STARTPTS)/%.5f,%s,fps=%.3f,format=yuv420p,setsar=1[v%d]'
|
|
264
|
+
% (n, piece.speed, fit, fps, i))
|
|
265
|
+
if has_audio:
|
|
266
|
+
tempo = ',' + atempo_chain(piece.speed) if abs(piece.speed - 1.0) > 1e-6 else ''
|
|
267
|
+
chains.append('[%d:a]asetpts=PTS-STARTPTS%s,%s,apad=whole_dur=%.3f,atrim=end=%.3f,asetpts=PTS-STARTPTS[a%d]'
|
|
268
|
+
% (n, tempo, fmt, piece.length, piece.length, i))
|
|
269
|
+
else:
|
|
270
|
+
chains.append('anullsrc=r=44100:cl=stereo,atrim=duration=%.3f,%s[a%d]' % (piece.length, fmt, i))
|
|
271
|
+
else:
|
|
272
|
+
n = add('-loop', '1', '-framerate', '%.3f' % fps, '-t', '%.3f' % piece.seconds, '-i', cards[id(piece)])
|
|
273
|
+
chains.append('[%d:v]scale=%d:%d,fps=%.3f,format=yuv420p,setsar=1[v%d]' % (n, w, h, fps, i))
|
|
274
|
+
if piece.audio:
|
|
275
|
+
m = add('-i', piece.audio)
|
|
276
|
+
chains.append('[%d:a]%s,apad=whole_dur=%.3f,atrim=end=%.3f,asetpts=PTS-STARTPTS[a%d]' % (m, fmt, piece.seconds, piece.seconds, i))
|
|
277
|
+
else:
|
|
278
|
+
chains.append('anullsrc=r=44100:cl=stereo,atrim=duration=%.3f,%s[a%d]' % (piece.seconds, fmt, i))
|
|
279
|
+
labels.append('[v%d][a%d]' % (i, i))
|
|
280
|
+
total = sum(p.length for p in pieces)
|
|
281
|
+
chains.append('%sconcat=n=%d:v=1:a=1[vout][amain]' % (''.join(labels), len(pieces)))
|
|
282
|
+
mixes, names = [], ['[amain]']
|
|
283
|
+
for tag, music, at_end in (('in', music_in, False), ('out', music_out, True)):
|
|
284
|
+
if not music:
|
|
285
|
+
continue
|
|
286
|
+
path, length = music
|
|
287
|
+
length = min(length, total)
|
|
288
|
+
n = add('-i', path)
|
|
289
|
+
delay = max(0.0, total - length) if at_end else 0.0
|
|
290
|
+
mixes.append('[%d:a]%s,atrim=end=%.3f,asetpts=PTS-STARTPTS,volume=%.3f,afade=t=in:st=0:d=%.2f,afade=t=out:st=%.3f:d=%.2f,adelay=%d:all=1[m%s]'
|
|
291
|
+
% (n, fmt, length, volume, min(FADE_OUT if at_end else FADE_IN, length / 2), max(0.0, length - FADE_OUT), min(FADE_OUT, length),
|
|
292
|
+
int(delay * 1000), tag))
|
|
293
|
+
names.append('[m%s]' % tag)
|
|
294
|
+
if mixes:
|
|
295
|
+
chains += mixes
|
|
296
|
+
chains.append('%samix=inputs=%d:duration=first:normalize=0[aout]' % (''.join(names), len(names)))
|
|
297
|
+
else:
|
|
298
|
+
chains.append('[amain]anull[aout]')
|
|
299
|
+
return inputs, ';'.join(chains), total
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def speaker(engine, voice, model, confirm):
|
|
303
|
+
"""A function that speaks a text and returns the mp3 bytes."""
|
|
304
|
+
import voice as vo # noqa: E402
|
|
305
|
+
if engine == 'piper':
|
|
306
|
+
installed = [v['id'] for v in vo.piper_voices() if v['installed']]
|
|
307
|
+
voice = voice or (installed[0] if installed else '')
|
|
308
|
+
if not vo.piper_bin() or voice not in installed:
|
|
309
|
+
raise el.Stop(vo.NOT_INSTALLED if not vo.piper_bin() else 'The Piper voice "%s" is not installed. Installed: %s.' % (voice, ', '.join(installed) or 'none'), USAGE)
|
|
310
|
+
return lambda text: vo.piper_synthesize(text, voice, '', 'mp3')[0]
|
|
311
|
+
el.key()
|
|
312
|
+
if not voice:
|
|
313
|
+
raise el.Stop('Choose an ElevenLabs voice for the new section.', USAGE)
|
|
314
|
+
voice_id = el.resolve_voice(voice)
|
|
315
|
+
return lambda text: el.synthesize(text, voice_id, model or el.DEFAULT_MODEL, el.DEFAULT_FORMAT)[0]
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---- the command ------------------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
def cmd_info(args):
|
|
321
|
+
info = probe(args.video)
|
|
322
|
+
el.emit(args, dict(info, path=args.video), ['%dx%d at %s fps, %s long, %s' % (info['width'], info['height'], info['fps'], el.mmss(info['duration']),
|
|
323
|
+
'with sound' if info['has_audio'] else 'silent')])
|
|
324
|
+
return OK
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def plan_edit(args, info, script_text):
|
|
328
|
+
"""The pieces the options ask for, and a description of each change for the record."""
|
|
329
|
+
total = info['duration']
|
|
330
|
+
cuts, changes = [], []
|
|
331
|
+
if args.trim:
|
|
332
|
+
start, end = parse_range(args.trim)
|
|
333
|
+
if not 0 <= start < end <= total + 0.5:
|
|
334
|
+
raise el.Stop('The trim %s is outside the video, which is %s long.' % (args.trim, el.mmss(total)), USAGE)
|
|
335
|
+
cuts += [(0.0, start), (end, total)]
|
|
336
|
+
changes.append('trimmed to %s-%s' % (el.mmss(start), el.mmss(end)))
|
|
337
|
+
if args.remove:
|
|
338
|
+
if not script_text:
|
|
339
|
+
raise el.Stop('Removing a section needs the video\'s timed voiceover script.', USAGE)
|
|
340
|
+
_, sections = parse_blocks(script_text)
|
|
341
|
+
by_id = {s['id']: s for s in sections if s['id']}
|
|
342
|
+
for sid in [x for x in args.remove.split(',') if x.strip()]:
|
|
343
|
+
if sid.strip() not in by_id:
|
|
344
|
+
raise el.Stop('The script has no section "%s". It has: %s.' % (sid.strip(), ', '.join(by_id) or 'none'), USAGE)
|
|
345
|
+
cuts.append((by_id[sid.strip()]['start'], by_id[sid.strip()]['end']))
|
|
346
|
+
changes.append('removed section %s' % sid.strip())
|
|
347
|
+
speed = args.speed or 1.0
|
|
348
|
+
pieces = source_pieces(total, cuts, 1.0)
|
|
349
|
+
if not pieces:
|
|
350
|
+
raise el.Stop('Nothing is left of the video after those cuts.', USAGE)
|
|
351
|
+
if args.length:
|
|
352
|
+
kept = sum(p.end - p.start for p in pieces)
|
|
353
|
+
target = el.seconds_of(args.length) if ':' in args.length else float(args.length)
|
|
354
|
+
speed = kept / target
|
|
355
|
+
if not MIN_SPEED <= speed <= MAX_SPEED:
|
|
356
|
+
raise el.Stop('To fit %s the picture would play at %.2fx; the range is %.2fx to %.0fx. Longest possible: %s, shortest: %s.'
|
|
357
|
+
% (el.mmss(target), speed, MIN_SPEED, MAX_SPEED, el.mmss(kept / MIN_SPEED), el.mmss(kept / MAX_SPEED)), USAGE)
|
|
358
|
+
if not MIN_SPEED <= speed <= MAX_SPEED:
|
|
359
|
+
raise el.Stop('Speed goes from %.2fx to %.0fx.' % (MIN_SPEED, MAX_SPEED), USAGE)
|
|
360
|
+
for piece in pieces:
|
|
361
|
+
piece.speed = speed
|
|
362
|
+
if abs(speed - 1.0) > 1e-6:
|
|
363
|
+
changes.append('at %.2fx speed' % speed)
|
|
364
|
+
return pieces, changes
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def insert_pieces(pieces, script_text, where, new, taken_at_start=0):
|
|
368
|
+
"""Put `new` pieces at `where`: start, end, or after the section of the edited video with that id. The source
|
|
369
|
+
piece a section ends inside is split so the new pieces land exactly after it. Returns the index they went to."""
|
|
370
|
+
if where == 'start':
|
|
371
|
+
pieces[taken_at_start:taken_at_start] = new
|
|
372
|
+
return taken_at_start
|
|
373
|
+
if where == 'end' or not script_text:
|
|
374
|
+
pieces.extend(new)
|
|
375
|
+
return len(pieces) - len(new)
|
|
376
|
+
end = next(s['end'] for s in parse_blocks(script_text)[1] if s['id'] == where)
|
|
377
|
+
at = max((i + 1 for i, p in enumerate(pieces) if p.kind == 'source' and p.src == 0 and p.start < end - 1e-6), default=len(pieces))
|
|
378
|
+
for i, p in enumerate(pieces):
|
|
379
|
+
if p.kind == 'source' and p.src == 0 and p.start + 1e-6 < end < p.end - 1e-6:
|
|
380
|
+
pieces[i:i + 1] = [Piece('source', p.start, end, p.speed), Piece('source', end, p.end, p.speed)]
|
|
381
|
+
at = i + 1
|
|
382
|
+
break
|
|
383
|
+
# several things going after one section keep the order they were asked for
|
|
384
|
+
while at < len(pieces) and (pieces[at].kind == 'card' or pieces[at].src > 0):
|
|
385
|
+
at += 1
|
|
386
|
+
pieces[at:at] = new
|
|
387
|
+
return at
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def parse_borrow(spec):
|
|
391
|
+
"""`VIDEO|SECTION-or-START-END|AFTER|SCRIPT`: the last two parts are optional."""
|
|
392
|
+
parts = [x.strip() for x in (spec or '').split('|')]
|
|
393
|
+
if len(parts) < 2 or not parts[0] or not parts[1] or len(parts) > 4:
|
|
394
|
+
raise el.Stop('--borrow is VIDEO|SECTION|AFTER|SCRIPT, like clip.mp4|T03|end|clip-voiceover-script.md (the last two are optional). '
|
|
395
|
+
'SECTION is a section id of that video\'s script, or a range like 0:10-0:30.', USAGE)
|
|
396
|
+
parts += [''] * (4 - len(parts))
|
|
397
|
+
return {'video': parts[0], 'what': parts[1], 'after': parts[2] or 'end', 'script': parts[3]}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def plan_borrows(args, script_text, sources):
|
|
401
|
+
"""Borrowed stretches from other videos: a list of {pieces, after, section, title, lines, from} and the
|
|
402
|
+
`sources` list (each {path, has_audio, ...}) the pieces' `src` numbers point into."""
|
|
403
|
+
out = []
|
|
404
|
+
own = [s['id'] for s in parse_blocks(script_text)[1] if s['id']] if script_text else []
|
|
405
|
+
for raw in args.borrow or []:
|
|
406
|
+
spec = parse_borrow(raw)
|
|
407
|
+
if os.path.abspath(spec['video']) == os.path.abspath(args.video):
|
|
408
|
+
raise el.Stop('A section borrowed from the video itself is already there: copy it by editing the video, or borrow from another.', USAGE)
|
|
409
|
+
if not os.path.isfile(spec['video']):
|
|
410
|
+
raise el.Stop('The video to borrow from is missing: %s' % spec['video'], USAGE)
|
|
411
|
+
if spec['after'] not in ('start', 'end') and spec['after'] not in own:
|
|
412
|
+
raise el.Stop('Cannot put it after "%s": %s' % (spec['after'], 'this video has sections ' + ', '.join(own) if own else
|
|
413
|
+
'this video has no timed script, so only start or end'), USAGE)
|
|
414
|
+
index = next((i for i, s in enumerate(sources) if s['path'] == spec['video']), None)
|
|
415
|
+
if index is None:
|
|
416
|
+
info = probe(spec['video'])
|
|
417
|
+
info['path'] = spec['video']
|
|
418
|
+
sources.append(info)
|
|
419
|
+
index = len(sources) - 1
|
|
420
|
+
info = sources[index]
|
|
421
|
+
title, lines, label = '', [], spec['what']
|
|
422
|
+
if re.match(r'^[\d:.]+\s*-\s*[\d:.]+$', spec['what']):
|
|
423
|
+
start, end = parse_range(spec['what'])
|
|
424
|
+
title = 'Borrowed %s-%s' % (el.mmss(start), el.mmss(end))
|
|
425
|
+
else:
|
|
426
|
+
if not spec['script']:
|
|
427
|
+
raise el.Stop('A section id needs the script of %s: add it as the fourth part of --borrow, or borrow a range like 0:10-0:30.'
|
|
428
|
+
% os.path.basename(spec['video']), USAGE)
|
|
429
|
+
try:
|
|
430
|
+
with open(spec['script'], encoding='utf-8') as handle:
|
|
431
|
+
theirs = parse_blocks(handle.read())[1]
|
|
432
|
+
except OSError as error:
|
|
433
|
+
raise el.Stop('Cannot read the script of the video to borrow from: %s' % error, USAGE)
|
|
434
|
+
found = next((s for s in theirs if s['id'] == spec['what']), None)
|
|
435
|
+
if not found:
|
|
436
|
+
raise el.Stop('%s has no section "%s". It has: %s.' % (os.path.basename(spec['script']), spec['what'],
|
|
437
|
+
', '.join(s['id'] for s in theirs if s['id']) or 'none'), USAGE)
|
|
438
|
+
start, end, title, lines = found['start'], found['end'], found['title'], found['lines']
|
|
439
|
+
if not 0 <= start < end <= info['duration'] + 0.5:
|
|
440
|
+
raise el.Stop('%s-%s is outside %s, which is %s long.' % (el.mmss(start), el.mmss(end), os.path.basename(spec['video']), el.mmss(info['duration'])), USAGE)
|
|
441
|
+
out.append({'pieces': [Piece('source', start, min(end, info['duration']), 1.0, src=index + 1)], 'after': spec['after'], 'what': label,
|
|
442
|
+
'title': title, 'lines': lines, 'from': os.path.basename(spec['video'])})
|
|
443
|
+
return out
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
STRING_OPTIONS = ('add_title', 'add_subtitle', 'add_text', 'intro_title', 'intro_subtitle', 'outro_title', 'outro_subtitle')
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def read_strings(args):
|
|
450
|
+
if not args.strings_file:
|
|
451
|
+
return
|
|
452
|
+
try:
|
|
453
|
+
with open(args.strings_file, encoding='utf-8') as handle:
|
|
454
|
+
given = json.load(handle)
|
|
455
|
+
except (OSError, ValueError) as error:
|
|
456
|
+
raise el.Stop('Cannot read the strings file: %s' % error, USAGE)
|
|
457
|
+
for name in STRING_OPTIONS:
|
|
458
|
+
if isinstance(given.get(name), str):
|
|
459
|
+
setattr(args, name, given[name].strip())
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def cmd_edit(args):
|
|
463
|
+
read_strings(args)
|
|
464
|
+
el.need_tools()
|
|
465
|
+
info = probe(args.video)
|
|
466
|
+
info['path'] = args.video
|
|
467
|
+
info['extra'] = []
|
|
468
|
+
script_text = ''
|
|
469
|
+
if args.script:
|
|
470
|
+
try:
|
|
471
|
+
with open(args.script, encoding='utf-8') as handle:
|
|
472
|
+
script_text = handle.read()
|
|
473
|
+
except OSError as error:
|
|
474
|
+
raise el.Stop('Cannot read the script: %s' % error, USAGE)
|
|
475
|
+
pieces, changes = plan_edit(args, info, script_text)
|
|
476
|
+
added = []
|
|
477
|
+
work = tempfile.mkdtemp(prefix='ws-video-edit-')
|
|
478
|
+
try:
|
|
479
|
+
# an added section: a card with the Webship logo and a title, spoken by the chosen voice
|
|
480
|
+
if args.add_text:
|
|
481
|
+
if not args.add_title:
|
|
482
|
+
raise el.Stop('A new section needs a title.', USAGE)
|
|
483
|
+
where = args.add_after or 'end'
|
|
484
|
+
if script_text and where not in ('start', 'end') and where not in {s['id'] for s in parse_blocks(script_text)[1] if s['id']}:
|
|
485
|
+
raise el.Stop('The script has no section "%s" to add after.' % where, USAGE)
|
|
486
|
+
engine = args.engine or 'piper'
|
|
487
|
+
if engine == 'elevenlabs' and not args.confirm:
|
|
488
|
+
chars = len(args.add_text)
|
|
489
|
+
el.emit(args, {'plan': 'new section', 'characters_to_send': chars, 'voice': args.voice, 'confirmed': False},
|
|
490
|
+
['Plan only, nothing was sent.', 'The new section "%s" would send %d characters to ElevenLabs (voice %s).' % (args.add_title, chars, args.voice),
|
|
491
|
+
'Run again with --confirm to make the video.'])
|
|
492
|
+
return PLAN_ONLY
|
|
493
|
+
say = speaker(engine, args.voice, args.model, args.confirm)
|
|
494
|
+
speech = os.path.join(work, 'section.mp3')
|
|
495
|
+
with open(speech, 'wb') as handle:
|
|
496
|
+
handle.write(say(args.add_text))
|
|
497
|
+
spoken = el.duration(speech) or 0.0
|
|
498
|
+
card = Piece('card', seconds=round(spoken + SECTION_PAD, 2), title=args.add_title, subtitle=args.add_subtitle or '', audio=speech)
|
|
499
|
+
insert_pieces(pieces, script_text, where, [card])
|
|
500
|
+
sections = parse_blocks(script_text)[1] if script_text else []
|
|
501
|
+
added.append({'after': where if script_text else 'end', 'id': free_section_id(sections), 'title': args.add_title, 'text': args.add_text, 'card': card})
|
|
502
|
+
changes.append('added section "%s"' % args.add_title)
|
|
503
|
+
sections_now = parse_blocks(script_text)[1] if script_text else []
|
|
504
|
+
borrowed = plan_borrows(args, script_text, info['extra'])
|
|
505
|
+
start_slot = 0
|
|
506
|
+
for item in borrowed:
|
|
507
|
+
at = insert_pieces(pieces, script_text, item['after'], item['pieces'], start_slot)
|
|
508
|
+
if item['after'] == 'start':
|
|
509
|
+
start_slot = at + len(item['pieces'])
|
|
510
|
+
taken = sections_now + [{'id': a['id']} for a in added]
|
|
511
|
+
item['id'] = free_section_id(taken, 'B')
|
|
512
|
+
if script_text:
|
|
513
|
+
added.append({'after': item['after'], 'id': item['id'],
|
|
514
|
+
'title': item['title'], 'lines': item['lines'] or ['> (borrowed from %s)' % item['from']],
|
|
515
|
+
'text': '', 'pieces': item['pieces']})
|
|
516
|
+
changes.append('borrowed %s "%s" from %s' % (item['what'], item['title'], item['from']))
|
|
517
|
+
for side, seconds, title, subtitle in (('start', args.intro_card, args.intro_title, args.intro_subtitle), ('end', args.outro_card, args.outro_title, args.outro_subtitle)):
|
|
518
|
+
if seconds:
|
|
519
|
+
if not CARD_SECONDS[0] <= seconds <= CARD_SECONDS[1]:
|
|
520
|
+
raise el.Stop('A logo scene lasts %.0f to %.0f seconds.' % CARD_SECONDS, USAGE)
|
|
521
|
+
card = Piece('card', seconds=seconds, title=title or '', subtitle=subtitle or '')
|
|
522
|
+
pieces.insert(0, card) if side == 'start' else pieces.append(card)
|
|
523
|
+
changes.append('%s with the Webship logo' % ('opens' if side == 'start' else 'closes'))
|
|
524
|
+
# a stretch with no length would make ffmpeg read to the end of its file (-t 0 means no limit there)
|
|
525
|
+
pieces[:] = [p for p in pieces if p.kind == 'card' or p.end - p.start > 0.04]
|
|
526
|
+
total = lay_out(pieces)
|
|
527
|
+
music = {}
|
|
528
|
+
for side, path, card, seconds in (('in', args.intro_music, args.intro_card, args.intro_seconds), ('out', args.outro_music, args.outro_card, args.outro_seconds)):
|
|
529
|
+
if path:
|
|
530
|
+
if not os.path.isfile(path):
|
|
531
|
+
raise el.Stop('The music file is missing: %s' % path, USAGE)
|
|
532
|
+
music[side] = (path, card or seconds)
|
|
533
|
+
changes.append('%s music' % ('intro' if side == 'in' else 'outro'))
|
|
534
|
+
out = args.out
|
|
535
|
+
if os.path.exists(out) and not args.force:
|
|
536
|
+
raise el.Stop('%s already exists. Pass --force to replace it.' % out, USAGE)
|
|
537
|
+
copy_only = not changes
|
|
538
|
+
if copy_only:
|
|
539
|
+
if not args.clone:
|
|
540
|
+
raise el.Stop('Nothing to change: pick a change, or use --clone for a plain copy.', USAGE)
|
|
541
|
+
changes.append('copied for re-work')
|
|
542
|
+
if copy_only:
|
|
543
|
+
tmp = os.path.join(os.path.dirname(os.path.abspath(out)), '.%s.part' % os.path.basename(out))
|
|
544
|
+
shutil.copyfile(args.video, tmp)
|
|
545
|
+
os.replace(tmp, out)
|
|
546
|
+
else:
|
|
547
|
+
cards = {}
|
|
548
|
+
for i, piece in enumerate(pieces):
|
|
549
|
+
if piece.kind == 'card':
|
|
550
|
+
picture = os.path.join(work, 'card%d.png' % i)
|
|
551
|
+
render_card(picture, piece.title, piece.subtitle, info['width'], info['height'])
|
|
552
|
+
cards[id(piece)] = picture
|
|
553
|
+
inputs, graph, total = build_filters(pieces, info, music.get('in'), music.get('out'), args.music_volume, cards)
|
|
554
|
+
part = os.path.join(os.path.dirname(os.path.abspath(out)), '.%s.part' % os.path.basename(out))
|
|
555
|
+
el.run(['ffmpeg', '-y', '-loglevel', 'error'] + inputs + ['-filter_complex', graph, '-map', '[vout]', '-map', '[aout]',
|
|
556
|
+
'-c:v', 'libx264', '-preset', 'veryfast', '-crf', '20', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k',
|
|
557
|
+
'-movflags', '+faststart', '-f', 'mp4', part])
|
|
558
|
+
os.replace(part, out)
|
|
559
|
+
poster = os.path.splitext(args.video)[0] + '-poster.jpg'
|
|
560
|
+
new_poster = os.path.splitext(out)[0] + '-poster.jpg'
|
|
561
|
+
if os.path.exists(poster) and (args.force or not os.path.exists(new_poster)):
|
|
562
|
+
shutil.copyfile(poster, new_poster)
|
|
563
|
+
script_out = None
|
|
564
|
+
if script_text:
|
|
565
|
+
script_out = args.script_out or os.path.splitext(out)[0] + '-voiceover-script.md'
|
|
566
|
+
new_text = retime_script(script_text, pieces, added) if not copy_only else script_text
|
|
567
|
+
if args.force or not os.path.exists(script_out):
|
|
568
|
+
with open(script_out, 'w', encoding='utf-8') as handle:
|
|
569
|
+
handle.write(new_text)
|
|
570
|
+
length = el.duration(out)
|
|
571
|
+
record = {'kind': 'video-edit', 'borrowed': [{'from': b['from'], 'section': b['what'], 'title': b['title']} for b in borrowed], 'created_utc': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
572
|
+
'source': os.path.basename(args.video), 'changes': changes, 'seconds': round(length, 2) if length else None,
|
|
573
|
+
'script': os.path.basename(script_out) if script_out else None}
|
|
574
|
+
with open(os.path.splitext(out)[0] + '-edit.json', 'w') as handle:
|
|
575
|
+
json.dump(record, handle, indent=2)
|
|
576
|
+
el.emit(args, dict(record, out=out), ['Wrote %s (%s)' % (out, el.mmss(length) if length else '?')] + [' ' + c for c in changes] +
|
|
577
|
+
(['Script: %s' % script_out] if script_out else []))
|
|
578
|
+
return OK
|
|
579
|
+
finally:
|
|
580
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def build_parser():
|
|
584
|
+
parser = argparse.ArgumentParser(prog='video_edit.py', description='Edit a video into a new version of itself.')
|
|
585
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
586
|
+
info = sub.add_parser('info', help='size, frame rate and length of a video')
|
|
587
|
+
info.add_argument('--video', type=el.text_arg, required=True)
|
|
588
|
+
info.add_argument('--json', action='store_true')
|
|
589
|
+
edit = sub.add_parser('edit', help='a new version with the changes asked for; with none, a plain copy')
|
|
590
|
+
edit.add_argument('--video', type=el.text_arg, required=True)
|
|
591
|
+
edit.add_argument('--out', type=el.text_arg, required=True)
|
|
592
|
+
edit.add_argument('--script', type=el.text_arg, default='', help='the timed voiceover script; it is rewritten for the new version')
|
|
593
|
+
edit.add_argument('--script-out', type=el.text_arg, default='')
|
|
594
|
+
edit.add_argument('--strings-file', type=el.text_arg, default='', help='a JSON file holding the free text (titles, subtitles, add_text), so none of it is on a command line')
|
|
595
|
+
edit.add_argument('--clone', action='store_true', help='a plain copy for re-work')
|
|
596
|
+
edit.add_argument('--speed', type=float, default=None, help='%.2f to %.0f; 2 plays twice as fast' % (MIN_SPEED, MAX_SPEED))
|
|
597
|
+
edit.add_argument('--length', type=el.text_arg, default='', help='make it this long: seconds or minutes:seconds')
|
|
598
|
+
edit.add_argument('--trim', type=el.text_arg, default='', help='keep only start-end')
|
|
599
|
+
edit.add_argument('--remove', type=el.text_arg, default='', help='section ids to cut out, comma separated')
|
|
600
|
+
edit.add_argument('--add-title', type=el.text_arg, default='')
|
|
601
|
+
edit.add_argument('--add-subtitle', type=el.text_arg, default='')
|
|
602
|
+
edit.add_argument('--add-text', type=el.text_arg, default='', help='what the new section says')
|
|
603
|
+
edit.add_argument('--add-after', type=el.text_arg, default='', help='a section id, start or end')
|
|
604
|
+
edit.add_argument('--borrow', action='append', type=el.text_arg, default=[],
|
|
605
|
+
help='VIDEO|SECTION-or-START-END|AFTER|SCRIPT: a stretch of another video, put after a section, start or end (repeatable)')
|
|
606
|
+
edit.add_argument('--engine', choices=('piper', 'elevenlabs'), default='')
|
|
607
|
+
edit.add_argument('--voice', type=el.text_arg, default='')
|
|
608
|
+
edit.add_argument('--model', type=el.text_arg, default=os.environ.get('ELEVENLABS_MODEL') or el.DEFAULT_MODEL)
|
|
609
|
+
edit.add_argument('--intro-card', type=float, default=0, help='seconds of the Webship logo before the video')
|
|
610
|
+
edit.add_argument('--intro-title', type=el.text_arg, default='')
|
|
611
|
+
edit.add_argument('--intro-subtitle', type=el.text_arg, default='')
|
|
612
|
+
edit.add_argument('--outro-card', type=float, default=0, help='seconds of the Webship logo after the video')
|
|
613
|
+
edit.add_argument('--outro-title', type=el.text_arg, default='')
|
|
614
|
+
edit.add_argument('--outro-subtitle', type=el.text_arg, default='')
|
|
615
|
+
edit.add_argument('--intro-music', type=el.text_arg, default='')
|
|
616
|
+
edit.add_argument('--intro-seconds', type=float, default=8.0, help='how long the intro music plays when there is no logo scene')
|
|
617
|
+
edit.add_argument('--outro-music', type=el.text_arg, default='')
|
|
618
|
+
edit.add_argument('--outro-seconds', type=float, default=8.0)
|
|
619
|
+
edit.add_argument('--music-volume', type=float, default=0.5)
|
|
620
|
+
edit.add_argument('--confirm', action='store_true', help='ElevenLabs only: actually send the new section\'s text')
|
|
621
|
+
edit.add_argument('--force', action='store_true')
|
|
622
|
+
edit.add_argument('--json', action='store_true')
|
|
623
|
+
return parser
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def main(argv):
|
|
627
|
+
args = build_parser().parse_args(argv)
|
|
628
|
+
try:
|
|
629
|
+
return {'info': cmd_info, 'edit': cmd_edit}[args.command](args)
|
|
630
|
+
except el.Stop as stop:
|
|
631
|
+
print(stop, file=sys.stderr)
|
|
632
|
+
return stop.code
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
if __name__ == '__main__':
|
|
636
|
+
sys.exit(main(sys.argv[1:]))
|