wizards 0.1.9
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 +110 -0
- package/README.md +170 -0
- package/apps/runtime/dist/agents/architect.js +453 -0
- package/apps/runtime/dist/agents/project-assistant.js +354 -0
- package/apps/runtime/dist/agents/subscription.js +206 -0
- package/apps/runtime/dist/agents/thought.js +21 -0
- package/apps/runtime/dist/agents/website.js +197 -0
- package/apps/runtime/dist/app.js +153 -0
- package/apps/runtime/dist/auth/account.js +121 -0
- package/apps/runtime/dist/auth/index.js +329 -0
- package/apps/runtime/dist/auth/keys.js +55 -0
- package/apps/runtime/dist/auth/local-ticket.js +23 -0
- package/apps/runtime/dist/authoring/example.js +73 -0
- package/apps/runtime/dist/authoring/guide.js +259 -0
- package/apps/runtime/dist/authoring/ops.js +117 -0
- package/apps/runtime/dist/cli/autostart.js +134 -0
- package/apps/runtime/dist/cli/clients.js +44 -0
- package/apps/runtime/dist/cli/environment.js +81 -0
- package/apps/runtime/dist/cli/exec.js +45 -0
- package/apps/runtime/dist/cli/home.js +74 -0
- package/apps/runtime/dist/cli/machine.js +145 -0
- package/apps/runtime/dist/cli/main.js +125 -0
- package/apps/runtime/dist/cli/setup.js +273 -0
- package/apps/runtime/dist/cli/start.js +190 -0
- package/apps/runtime/dist/cli/status.js +120 -0
- package/apps/runtime/dist/cli/ui.js +12 -0
- package/apps/runtime/dist/cli/update.js +58 -0
- package/apps/runtime/dist/connectors/builtin.js +70 -0
- package/apps/runtime/dist/connectors/external.js +400 -0
- package/apps/runtime/dist/connectors/gmail.js +107 -0
- package/apps/runtime/dist/connectors/imap.js +217 -0
- package/apps/runtime/dist/connectors/index.js +266 -0
- package/apps/runtime/dist/connectors/mail.js +72 -0
- package/apps/runtime/dist/connectors/outlook.js +90 -0
- package/apps/runtime/dist/credits/credits.js +103 -0
- package/apps/runtime/dist/credits/estimate.js +132 -0
- package/apps/runtime/dist/db/adopt.js +149 -0
- package/apps/runtime/dist/db/client.js +146 -0
- package/apps/runtime/dist/db/control-schema.js +70 -0
- package/apps/runtime/dist/db/migrations/control/0000_last_absorbing_man.sql +47 -0
- package/apps/runtime/dist/db/migrations/control/0001_gorgeous_nicolaos.sql +41 -0
- package/apps/runtime/dist/db/migrations/control/0002_secret_penance.sql +1 -0
- package/apps/runtime/dist/db/migrations/control/0003_nasty_mordo.sql +3 -0
- package/apps/runtime/dist/db/migrations/control/0004_breezy_eddie_brock.sql +6 -0
- package/apps/runtime/dist/db/migrations/control/meta/0000_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0001_snapshot.json +599 -0
- package/apps/runtime/dist/db/migrations/control/meta/0002_snapshot.json +607 -0
- package/apps/runtime/dist/db/migrations/control/meta/0003_snapshot.json +311 -0
- package/apps/runtime/dist/db/migrations/control/meta/0004_snapshot.json +350 -0
- package/apps/runtime/dist/db/migrations/control/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/migrations/tenant/0000_eminent_freak.sql +188 -0
- package/apps/runtime/dist/db/migrations/tenant/0001_salty_master_chief.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0002_cloudy_firebird.sql +1 -0
- package/apps/runtime/dist/db/migrations/tenant/0003_brown_warbound.sql +52 -0
- package/apps/runtime/dist/db/migrations/tenant/0004_romantic_nocturne.sql +6 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0000_snapshot.json +1323 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0001_snapshot.json +1330 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0002_snapshot.json +1337 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0003_snapshot.json +1605 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0004_snapshot.json +1645 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +41 -0
- package/apps/runtime/dist/db/schema.js +278 -0
- package/apps/runtime/dist/documents/parse.js +230 -0
- package/apps/runtime/dist/engenty/connections-external/build-connector.js +193 -0
- package/apps/runtime/dist/engenty/connections-external/errors.js +11 -0
- package/apps/runtime/dist/engenty/connections-external/import-service.js +270 -0
- package/apps/runtime/dist/engenty/connections-external/importer/classify.js +34 -0
- package/apps/runtime/dist/engenty/connections-external/importer/map-auth.js +185 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-mcp.js +63 -0
- package/apps/runtime/dist/engenty/connections-external/importer/normalize-openapi.js +130 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/http-invoker.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/invoke/mcp-client.js +143 -0
- package/apps/runtime/dist/engenty/connections-external/net/guarded-fetch.js +119 -0
- package/apps/runtime/dist/engenty/connections-external/oauth-dcr.js +88 -0
- package/apps/runtime/dist/engenty/connections-external/registry-client.js +336 -0
- package/apps/runtime/dist/engenty/connections-external/registry-source.js +82 -0
- package/apps/runtime/dist/engenty/connections-external/types.js +7 -0
- package/apps/runtime/dist/engenty/connections-github/connector.js +209 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +359 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/contacts.js +94 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/drive.js +290 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/gmail.js +567 -0
- package/apps/runtime/dist/engenty/connections-google/definitions.js +11 -0
- package/apps/runtime/dist/engenty/connections-google/shared.js +73 -0
- package/apps/runtime/dist/engenty/connections-hubspot/connector.js +140 -0
- package/apps/runtime/dist/engenty/connections-microsoft/action.js +17 -0
- package/apps/runtime/dist/engenty/connections-microsoft/graph.js +114 -0
- package/apps/runtime/dist/engenty/connections-microsoft/onedrive.js +286 -0
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +464 -0
- package/apps/runtime/dist/engenty/connections-s3/s3.js +328 -0
- package/apps/runtime/dist/engenty/connections-sdk/files-capability.js +86 -0
- package/apps/runtime/dist/engenty/connections-sdk/oauth2.js +150 -0
- package/apps/runtime/dist/engenty/connections-sdk/registry.js +89 -0
- package/apps/runtime/dist/engenty/connections-sdk/storage-capability.js +84 -0
- package/apps/runtime/dist/engenty/connections-sdk/types.js +30 -0
- package/apps/runtime/dist/engenty/connections-slack/connector.js +360 -0
- package/apps/runtime/dist/engenty/csv-import/csv-matrix.js +178 -0
- package/apps/runtime/dist/engenty/csv-import/import-sources.js +87 -0
- package/apps/runtime/dist/engenty/csv-import/parse-csv.js +129 -0
- package/apps/runtime/dist/engenty/csv-import/serialize-csv.js +20 -0
- package/apps/runtime/dist/engenty/csv-import/types.js +1 -0
- package/apps/runtime/dist/engenty/csv-import/xlsx-workbook.js +159 -0
- package/apps/runtime/dist/engenty/doc-converter/interface.js +9 -0
- package/apps/runtime/dist/engenty/doc-converter/page-break.js +92 -0
- package/apps/runtime/dist/engenty/doc-converter/providers/local/index.js +72 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/classifier.js +30 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/index.js +4 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/invoice.js +73 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/receipt.js +63 -0
- package/apps/runtime/dist/engenty/document-scanner/schemas/shared.js +40 -0
- package/apps/runtime/dist/engenty/search-index/chunks.js +164 -0
- package/apps/runtime/dist/engenty/search-index/contracts.js +9 -0
- package/apps/runtime/dist/engenty/shims/connections-sdk.js +39 -0
- package/apps/runtime/dist/engenty/shims/plugin-sdk.js +1 -0
- package/apps/runtime/dist/engenty/shims/web-ingest.js +3 -0
- package/apps/runtime/dist/engenty/web-ingest/ssrf.js +277 -0
- package/apps/runtime/dist/engine/asks.js +60 -0
- package/apps/runtime/dist/engine/events.js +46 -0
- package/apps/runtime/dist/engine/input.js +170 -0
- package/apps/runtime/dist/engine/prepare.js +51 -0
- package/apps/runtime/dist/engine/requirements.js +124 -0
- package/apps/runtime/dist/engine/resources.js +89 -0
- package/apps/runtime/dist/engine/runner.js +560 -0
- package/apps/runtime/dist/engine/steps.js +598 -0
- package/apps/runtime/dist/engine/template.js +183 -0
- package/apps/runtime/dist/engine/types.js +3 -0
- package/apps/runtime/dist/env.js +224 -0
- package/apps/runtime/dist/files/blobs.js +22 -0
- package/apps/runtime/dist/files/objects.js +86 -0
- package/apps/runtime/dist/files/storage.js +95 -0
- package/apps/runtime/dist/geocode.js +58 -0
- package/apps/runtime/dist/harness/claude.js +138 -0
- package/apps/runtime/dist/harness/codex.js +228 -0
- package/apps/runtime/dist/harness/cursor.js +93 -0
- package/apps/runtime/dist/harness/env.js +124 -0
- package/apps/runtime/dist/harness/gemini.js +126 -0
- package/apps/runtime/dist/harness/index.js +99 -0
- package/apps/runtime/dist/harness/model.js +267 -0
- package/apps/runtime/dist/harness/prompt.js +121 -0
- package/apps/runtime/dist/harness/terminal.js +193 -0
- package/apps/runtime/dist/harness/types.js +7 -0
- package/apps/runtime/dist/index.js +76 -0
- package/apps/runtime/dist/limits.js +57 -0
- package/apps/runtime/dist/link-preview.js +59 -0
- package/apps/runtime/dist/manage.js +151 -0
- package/apps/runtime/dist/mcp/auth.js +102 -0
- package/apps/runtime/dist/mcp/bridge.js +59 -0
- package/apps/runtime/dist/mcp/handler.js +42 -0
- package/apps/runtime/dist/mcp/scopes.js +5 -0
- package/apps/runtime/dist/mcp/tools.js +401 -0
- package/apps/runtime/dist/media/ffmpeg.js +121 -0
- package/apps/runtime/dist/media/generate.js +120 -0
- package/apps/runtime/dist/media/marking.js +121 -0
- package/apps/runtime/dist/media/transcribe.js +30 -0
- package/apps/runtime/dist/model-errors.js +3 -0
- package/apps/runtime/dist/models.js +489 -0
- package/apps/runtime/dist/plugin.js +61 -0
- package/apps/runtime/dist/render/chromium.js +85 -0
- package/apps/runtime/dist/render/convert.js +161 -0
- package/apps/runtime/dist/render/downloads.js +277 -0
- package/apps/runtime/dist/render/guard.js +25 -0
- package/apps/runtime/dist/routes/api.js +74 -0
- package/apps/runtime/dist/routes/delivery.js +52 -0
- package/apps/runtime/dist/routes/internal.js +49 -0
- package/apps/runtime/dist/routes/marketplace.js +36 -0
- package/apps/runtime/dist/routes/runs.js +805 -0
- package/apps/runtime/dist/routes/studio.js +702 -0
- package/apps/runtime/dist/routes/wizard-stream.js +30 -0
- package/apps/runtime/dist/running.js +39 -0
- package/apps/runtime/dist/sandbox/agentos.js +191 -0
- package/apps/runtime/dist/sandbox/docker.js +77 -0
- package/apps/runtime/dist/sandbox/index.js +30 -0
- package/apps/runtime/dist/sandbox/types.js +1 -0
- package/apps/runtime/dist/secrets/crypto.js +28 -0
- package/apps/runtime/dist/secrets/signing.js +34 -0
- package/apps/runtime/dist/secrets/vault.js +59 -0
- package/apps/runtime/dist/services/architect.js +92 -0
- package/apps/runtime/dist/services/brand.js +20 -0
- package/apps/runtime/dist/services/cloud.js +52 -0
- package/apps/runtime/dist/services/draft-events.js +27 -0
- package/apps/runtime/dist/services/errors.js +24 -0
- package/apps/runtime/dist/services/files.js +129 -0
- package/apps/runtime/dist/services/links.js +16 -0
- package/apps/runtime/dist/services/marketplace.js +292 -0
- package/apps/runtime/dist/services/package.js +202 -0
- package/apps/runtime/dist/services/profile.js +23 -0
- package/apps/runtime/dist/services/project-files.js +266 -0
- package/apps/runtime/dist/services/project-index.js +109 -0
- package/apps/runtime/dist/services/projects.js +181 -0
- package/apps/runtime/dist/services/runs.js +224 -0
- package/apps/runtime/dist/services/shares.js +129 -0
- package/apps/runtime/dist/services/widgets.js +34 -0
- package/apps/runtime/dist/services/wizards.js +342 -0
- package/apps/runtime/dist/settings.js +16 -0
- package/apps/runtime/dist/store/index.js +279 -0
- package/apps/runtime/dist/tenants/control.js +106 -0
- package/apps/runtime/dist/tenants/tenant.js +26 -0
- package/apps/runtime/dist/tools/browser.js +380 -0
- package/apps/runtime/dist/tools/connector.js +204 -0
- package/apps/runtime/dist/tools/index.js +234 -0
- package/apps/runtime/dist/tools/mail.js +104 -0
- package/apps/runtime/dist/tools/net-guard.js +59 -0
- package/apps/runtime/dist/tools/project.js +53 -0
- package/apps/runtime/dist/tools/shared.js +42 -0
- package/apps/runtime/dist/tools/store.js +238 -0
- package/apps/runtime/dist/update.js +118 -0
- package/apps/runtime/dist/widgets/bundle.js +99 -0
- package/apps/runtime/dist/widgets/render.js +312 -0
- package/apps/runtime/dist/widgets/runtime.js +84 -0
- package/apps/runtime/dist/widgets/script.js +30 -0
- package/apps/runtime/dist/widgets/step.js +192 -0
- package/apps/web/dist/ai-labels/ai-black.svg +20 -0
- package/apps/web/dist/ai-labels/ai-generated-black.svg +31 -0
- package/apps/web/dist/ai-labels/ai-generated-white.svg +32 -0
- package/apps/web/dist/ai-labels/ai-modified-black.svg +30 -0
- package/apps/web/dist/ai-labels/ai-modified-white.svg +31 -0
- package/apps/web/dist/ai-labels/ai-white.svg +21 -0
- package/apps/web/dist/assets/ChangelogDialog-O00vEnbm.js +1 -0
- package/apps/web/dist/assets/ChatPanel-CH9ry0Hp.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-DRzecwUw.js +1 -0
- package/apps/web/dist/assets/EditorPage-BRnBf02Q.js +2 -0
- package/apps/web/dist/assets/FlowPreview-DDOj8IAH.js +1 -0
- package/apps/web/dist/assets/HomePage-CAGTTG3N.js +1 -0
- package/apps/web/dist/assets/ImportWizard-0RRrAnX3.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-BrP-ENHg.css +1 -0
- package/apps/web/dist/assets/LocalRuntime-C-TBYrzs.js +16 -0
- package/apps/web/dist/assets/NewWizardPage-kI90h3_9.js +2 -0
- package/apps/web/dist/assets/PublicRunner-CKRDeIVd.js +1 -0
- package/apps/web/dist/assets/RunnerView-Rc4H8wYH.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CESMuZxq.js +2 -0
- package/apps/web/dist/assets/SetupPage-D1rrYU14.js +1 -0
- package/apps/web/dist/assets/SharePage-COTAmOgh.js +1 -0
- package/apps/web/dist/assets/api-BY28cjGY.js +10 -0
- package/apps/web/dist/assets/arrow-left-UpjOBfQp.js +1 -0
- package/apps/web/dist/assets/arrow-right-CucQycEy.js +1 -0
- package/apps/web/dist/assets/changelog-p3wsaDDS.js +1 -0
- package/apps/web/dist/assets/check-Bjx41xbB.js +1 -0
- package/apps/web/dist/assets/createLucideIcon-DBu2Ovxj.js +1 -0
- package/apps/web/dist/assets/definition-cy7uvY5L.js +55 -0
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +1 -0
- package/apps/web/dist/assets/esm-D1OEhjTK.js +7 -0
- package/apps/web/dist/assets/external-link-CRg6DVdO.js +1 -0
- package/apps/web/dist/assets/fluffy-engenty-CJ3wCQgT.js +456 -0
- package/apps/web/dist/assets/index-CQ_q9iMO.css +1 -0
- package/apps/web/dist/assets/index-D0gxh90t.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +1 -0
- package/apps/web/dist/assets/meta-BbSdtTUa.js +1 -0
- package/apps/web/dist/assets/mic-CeqQ89Dj.js +1 -0
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +1 -0
- package/apps/web/dist/assets/outputs-D6pxH6ve.js +69 -0
- package/apps/web/dist/assets/plus-HA210wXA.js +1 -0
- package/apps/web/dist/assets/preload-helper-CNEI7N0N.js +1 -0
- package/apps/web/dist/assets/qr-scanner-worker.min-CJaXo3yu.js +98 -0
- package/apps/web/dist/assets/qr-scanner.min-C_UI0gH-.js +1 -0
- package/apps/web/dist/assets/type-BzYgVWKL.js +1 -0
- package/apps/web/dist/assets/ui-Ddwl35A6.js +1 -0
- package/apps/web/dist/assets/upload-DsgLfQ3U.js +1 -0
- package/apps/web/dist/assets/video-Dk1fuTDl.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-Bf97fs8T.js +1 -0
- package/apps/web/dist/embed.js +102 -0
- package/apps/web/dist/favicon.svg +1 -0
- package/apps/web/dist/icons/apple-touch-icon.png +0 -0
- package/apps/web/dist/icons/icon-192.png +0 -0
- package/apps/web/dist/icons/icon-512.png +0 -0
- package/apps/web/dist/icons/icon-maskable-512.png +0 -0
- package/apps/web/dist/index.html +46 -0
- package/apps/web/dist/install.sh +532 -0
- package/apps/web/dist/manifest.webmanifest +20 -0
- package/apps/web/dist/sw.js +15 -0
- package/bin/wizards.mjs +13 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.d.ts +77 -0
- package/node_modules/@engenty-wizards/shared/dist/connectors.js +2 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +1415 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.js +762 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.d.ts +5 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-format.js +132 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.d.ts +11 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns-value.js +209 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.d.ts +532 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/columns.js +216 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.d.ts +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/data-tables/index.js +3 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.d.ts +14 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-lexical.js +166 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.d.ts +33 -0
- package/node_modules/@engenty-wizards/shared/dist/engenty/search-index/catalog-rank.js +99 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.d.ts +35 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-entry.js +86 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.d.ts +20 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace-search.js +133 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +298 -0
- package/node_modules/@engenty-wizards/shared/dist/marketplace.js +211 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.d.ts +7 -0
- package/node_modules/@engenty-wizards/shared/dist/merge.js +40 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.d.ts +74 -0
- package/node_modules/@engenty-wizards/shared/dist/projects.js +96 -0
- package/node_modules/@engenty-wizards/shared/dist/run.d.ts +219 -0
- package/node_modules/@engenty-wizards/shared/dist/run.js +1 -0
- package/node_modules/@engenty-wizards/shared/dist/store.d.ts +100 -0
- package/node_modules/@engenty-wizards/shared/dist/store.js +44 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.d.ts +16 -0
- package/node_modules/@engenty-wizards/shared/dist/workspace.js +51 -0
- package/node_modules/@engenty-wizards/shared/package.json +10 -0
- package/package.json +62 -0
- package/plugin/.claude-plugin/plugin.json +9 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/commands/wizard.md +8 -0
- package/plugin/skills/build-wizard/SKILL.md +27 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { env } from "../env.js";
|
|
5
|
+
import { extFor } from "../files/storage.js";
|
|
6
|
+
import { ffmpeg, hasFfmpeg, probeMedia, withTempDir } from "../media/ffmpeg.js";
|
|
7
|
+
import { newContext, offlineContext } from "../render/chromium.js";
|
|
8
|
+
export { hasFfmpeg } from "../media/ffmpeg.js";
|
|
9
|
+
const FPS = 25;
|
|
10
|
+
const MAX_SECONDS = 60;
|
|
11
|
+
/** Exports set `wizard.mode` to "export" so the widget hides its controls and does not autoplay. */
|
|
12
|
+
function forExport(html) {
|
|
13
|
+
return html.replace('<script type="application/json" id="wizard-payload">', '<script>window.__WIZARD_EXPORT=true</script><script type="application/json" id="wizard-payload">');
|
|
14
|
+
}
|
|
15
|
+
async function withWidget(html, size, scale, use) {
|
|
16
|
+
const context = await offlineContext({
|
|
17
|
+
viewport: { width: size.width, height: size.height },
|
|
18
|
+
deviceScaleFactor: scale,
|
|
19
|
+
});
|
|
20
|
+
const errors = [];
|
|
21
|
+
try {
|
|
22
|
+
const page = await context.newPage();
|
|
23
|
+
page.on("pageerror", (err) => errors.push(err.message));
|
|
24
|
+
page.on("console", (msg) => {
|
|
25
|
+
if (msg.type() === "error") {
|
|
26
|
+
errors.push(msg.text());
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
await page.setContent(forExport(html), { waitUntil: "load", timeout: 30_000 });
|
|
30
|
+
// A widget calls wizard.ready() after its first frame; one that never does gets 3 seconds.
|
|
31
|
+
await page.evaluate(() => Promise.race([window.wizard?.__ready, new Promise((r) => setTimeout(r, 3000))]));
|
|
32
|
+
return await use(page, errors);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await context.close();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function timelineOf(page) {
|
|
39
|
+
return page.evaluate(() => {
|
|
40
|
+
const t = window.wizard?.__timeline?.();
|
|
41
|
+
return t ? { duration: Number(t.duration), poster: Number(t.poster ?? 0) } : null;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function seek(page, seconds) {
|
|
45
|
+
await page.evaluate(async (t) => {
|
|
46
|
+
// A film's seek waits for its clips to reach the frame; a drawn animation returns at once.
|
|
47
|
+
await window.wizard.__timeline().seek(t);
|
|
48
|
+
await new Promise((r) => requestAnimationFrame(() => r(null)));
|
|
49
|
+
}, seconds);
|
|
50
|
+
}
|
|
51
|
+
/** Loads the widget once: does it run, and does it animate? */
|
|
52
|
+
export async function probeWidget(html, size) {
|
|
53
|
+
return withWidget(html, size, 1, async (page, errors) => {
|
|
54
|
+
const timeline = await timelineOf(page);
|
|
55
|
+
if (timeline) {
|
|
56
|
+
await seek(page, timeline.poster);
|
|
57
|
+
}
|
|
58
|
+
const png = await page.screenshot({ type: "png" });
|
|
59
|
+
return {
|
|
60
|
+
duration: timeline ? Math.min(timeline.duration, MAX_SECONDS) : null,
|
|
61
|
+
errors: [...new Set(errors)].slice(0, 10),
|
|
62
|
+
png,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export async function widgetPng(html, size) {
|
|
67
|
+
return withWidget(html, size, 2, async (page) => {
|
|
68
|
+
const timeline = await timelineOf(page);
|
|
69
|
+
if (timeline) {
|
|
70
|
+
await seek(page, timeline.poster);
|
|
71
|
+
}
|
|
72
|
+
return page.screenshot({ type: "png", fullPage: true });
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
export async function widgetPdf(html, size) {
|
|
76
|
+
return withWidget(html, size, 1, async (page) => {
|
|
77
|
+
const timeline = await timelineOf(page);
|
|
78
|
+
if (timeline) {
|
|
79
|
+
await seek(page, timeline.poster);
|
|
80
|
+
}
|
|
81
|
+
const height = await page.evaluate(() => document.documentElement.scrollHeight);
|
|
82
|
+
return page.pdf({
|
|
83
|
+
width: `${size.width}px`,
|
|
84
|
+
height: `${Math.max(size.height, height)}px`,
|
|
85
|
+
printBackground: true,
|
|
86
|
+
pageRanges: "1",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Steps through the timeline frame by frame and encodes the frames as H.264 (no sound). */
|
|
91
|
+
async function encodeFrames(page, seconds, out, onProgress) {
|
|
92
|
+
const encoder = spawn(env.ffmpegPath, [
|
|
93
|
+
"-y",
|
|
94
|
+
"-loglevel",
|
|
95
|
+
"error",
|
|
96
|
+
"-f",
|
|
97
|
+
"image2pipe",
|
|
98
|
+
"-framerate",
|
|
99
|
+
String(FPS),
|
|
100
|
+
"-i",
|
|
101
|
+
"-",
|
|
102
|
+
"-c:v",
|
|
103
|
+
"libx264",
|
|
104
|
+
"-preset",
|
|
105
|
+
"veryfast",
|
|
106
|
+
"-crf",
|
|
107
|
+
"20",
|
|
108
|
+
"-pix_fmt",
|
|
109
|
+
"yuv420p",
|
|
110
|
+
"-movflags",
|
|
111
|
+
"+faststart",
|
|
112
|
+
out,
|
|
113
|
+
], { stdio: ["pipe", "ignore", "pipe"] });
|
|
114
|
+
let stderr = "";
|
|
115
|
+
encoder.stderr.on("data", (d) => {
|
|
116
|
+
stderr += String(d);
|
|
117
|
+
});
|
|
118
|
+
const done = new Promise((resolve, reject) => {
|
|
119
|
+
encoder.on("error", reject);
|
|
120
|
+
encoder.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`ffmpeg: ${stderr.slice(0, 300)}`)));
|
|
121
|
+
});
|
|
122
|
+
const frames = Math.max(1, Math.round(seconds * FPS));
|
|
123
|
+
for (let i = 0; i < frames; i++) {
|
|
124
|
+
await seek(page, (i * seconds) / frames);
|
|
125
|
+
const jpeg = await page.screenshot({ type: "jpeg", quality: 92 });
|
|
126
|
+
if (!encoder.stdin.write(jpeg)) {
|
|
127
|
+
await new Promise((r) => encoder.stdin.once("drain", r));
|
|
128
|
+
}
|
|
129
|
+
if (i % FPS === 0) {
|
|
130
|
+
onProgress?.(i / frames);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
encoder.stdin.end();
|
|
134
|
+
await done;
|
|
135
|
+
}
|
|
136
|
+
/** Steps through the widget's timeline frame by frame and encodes the frames as H.264. */
|
|
137
|
+
export async function widgetMp4(html, size) {
|
|
138
|
+
if (!(await hasFfmpeg())) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const even = { width: size.width - (size.width % 2), height: size.height - (size.height % 2) };
|
|
142
|
+
return withTempDir(async (dir) => {
|
|
143
|
+
const out = join(dir, "out.mp4");
|
|
144
|
+
return withWidget(html, even, 1, async (page) => {
|
|
145
|
+
const timeline = await timelineOf(page);
|
|
146
|
+
if (!timeline) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
await encodeFrames(page, Math.min(timeline.duration, MAX_SECONDS), out);
|
|
150
|
+
return new Uint8Array(await readFile(out));
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// --- films: a timeline of clips and stills, with sound ---------------------------------
|
|
155
|
+
/** Where a film's page finds this run's clips, images and voice-over while it is rendered. */
|
|
156
|
+
export const FILM_MEDIA_ORIGIN = "http://wizard.media";
|
|
157
|
+
/**
|
|
158
|
+
* Renders a film widget: its page draws every frame from this run's clips and images, and names
|
|
159
|
+
* the sound that belongs to it (voice-over, the clips' own sound). Clips are re-encoded with a
|
|
160
|
+
* key frame every few frames first, so the page reaches any frame quickly and exactly.
|
|
161
|
+
*/
|
|
162
|
+
export async function renderFilm(html, size, media, onProgress) {
|
|
163
|
+
if (!(await hasFfmpeg())) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const even = { width: size.width - (size.width % 2), height: size.height - (size.height % 2) };
|
|
167
|
+
return withTempDir(async (dir) => {
|
|
168
|
+
// url → the file as it came (for its sound) and what the page is served.
|
|
169
|
+
const files = new Map();
|
|
170
|
+
let n = 0;
|
|
171
|
+
for (const [url, file] of media) {
|
|
172
|
+
const path = join(dir, `media-${n++}.${extFor(file.mime)}`);
|
|
173
|
+
await writeFile(path, file.bytes);
|
|
174
|
+
let served = file;
|
|
175
|
+
if (file.mime.startsWith("video/")) {
|
|
176
|
+
const seekable = join(dir, `seek-${n}.mp4`);
|
|
177
|
+
const { code } = await ffmpeg([
|
|
178
|
+
"-y",
|
|
179
|
+
"-loglevel",
|
|
180
|
+
"error",
|
|
181
|
+
"-i",
|
|
182
|
+
path,
|
|
183
|
+
"-an",
|
|
184
|
+
"-c:v",
|
|
185
|
+
"libx264",
|
|
186
|
+
"-preset",
|
|
187
|
+
"veryfast",
|
|
188
|
+
"-crf",
|
|
189
|
+
"20",
|
|
190
|
+
"-g",
|
|
191
|
+
"5",
|
|
192
|
+
"-pix_fmt",
|
|
193
|
+
"yuv420p",
|
|
194
|
+
"-movflags",
|
|
195
|
+
"+faststart",
|
|
196
|
+
seekable,
|
|
197
|
+
]);
|
|
198
|
+
if (code === 0) {
|
|
199
|
+
served = { bytes: new Uint8Array(await readFile(seekable)), mime: "video/mp4" };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
files.set(url, { path, served });
|
|
203
|
+
}
|
|
204
|
+
const context = await newContext({ viewport: even, deviceScaleFactor: 1 });
|
|
205
|
+
const errors = [];
|
|
206
|
+
try {
|
|
207
|
+
await context.route("**/*", async (route) => {
|
|
208
|
+
const request = route.request();
|
|
209
|
+
const file = files.get(request.url().split(/[?#]/)[0])?.served;
|
|
210
|
+
if (!file) {
|
|
211
|
+
await route.abort("blockedbyclient");
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Players ask for byte ranges and only seek freely when they get them.
|
|
215
|
+
const total = file.bytes.byteLength;
|
|
216
|
+
const range = request.headers().range?.match(/^bytes=(\d+)-(\d*)$/);
|
|
217
|
+
const from = range ? Math.min(Number(range[1]), total - 1) : 0;
|
|
218
|
+
const to = range?.[2] ? Math.min(Number(range[2]), total - 1) : total - 1;
|
|
219
|
+
await route.fulfill({
|
|
220
|
+
status: range ? 206 : 200,
|
|
221
|
+
contentType: file.mime,
|
|
222
|
+
headers: {
|
|
223
|
+
"accept-ranges": "bytes",
|
|
224
|
+
...(range ? { "content-range": `bytes ${from}-${to}/${total}` } : {}),
|
|
225
|
+
},
|
|
226
|
+
body: Buffer.from(file.bytes.subarray(from, to + 1)),
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
const page = await context.newPage();
|
|
230
|
+
page.on("pageerror", (err) => errors.push(err.message));
|
|
231
|
+
await page.setContent(forExport(html), { waitUntil: "load", timeout: 30_000 });
|
|
232
|
+
// A film is ready once its clips are loaded; that takes longer than a drawn widget.
|
|
233
|
+
await page.evaluate(() => Promise.race([window.wizard?.__ready, new Promise((r) => setTimeout(r, 45_000))]));
|
|
234
|
+
const timeline = await page.evaluate(() => {
|
|
235
|
+
const t = window.wizard?.__timeline?.();
|
|
236
|
+
return t
|
|
237
|
+
? {
|
|
238
|
+
duration: Number(t.duration),
|
|
239
|
+
poster: Number(t.poster ?? 0),
|
|
240
|
+
audio: (Array.isArray(t.audio) ? t.audio : []),
|
|
241
|
+
}
|
|
242
|
+
: null;
|
|
243
|
+
});
|
|
244
|
+
if (!timeline) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const seconds = Math.min(timeline.duration, MAX_SECONDS);
|
|
248
|
+
const silent = join(dir, "silent.mp4");
|
|
249
|
+
await encodeFrames(page, seconds, silent, onProgress);
|
|
250
|
+
await seek(page, Math.min(timeline.poster, seconds));
|
|
251
|
+
const png = await page.screenshot({ type: "png" });
|
|
252
|
+
const tracks = [];
|
|
253
|
+
for (const track of timeline.audio.slice(0, 16)) {
|
|
254
|
+
const path = files.get(String(track.src).split(/[?#]/)[0])?.path;
|
|
255
|
+
if (path && track.start < seconds && (await probeMedia(path)).audio) {
|
|
256
|
+
tracks.push({ ...track, path });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
let out = silent;
|
|
260
|
+
if (tracks.length) {
|
|
261
|
+
const mixed = join(dir, "film.mp4");
|
|
262
|
+
const chains = tracks.map((track, i) => {
|
|
263
|
+
const length = Math.min(track.duration ?? seconds, seconds - track.start);
|
|
264
|
+
const volume = Math.max(0, Math.min(2, track.volume ?? 1));
|
|
265
|
+
const delay = Math.round(Math.max(0, track.start) * 1000);
|
|
266
|
+
return `[${i + 1}:a]atrim=0:${length.toFixed(3)},afade=t=out:st=${Math.max(0, length - 0.25).toFixed(3)}:d=0.25,adelay=${delay}:all=1,volume=${volume}[a${i}]`;
|
|
267
|
+
});
|
|
268
|
+
const mix = `${tracks.map((_, i) => `[a${i}]`).join("")}amix=inputs=${tracks.length}:normalize=0:duration=longest[mix]`;
|
|
269
|
+
const { code, stderr } = await ffmpeg([
|
|
270
|
+
"-y",
|
|
271
|
+
"-loglevel",
|
|
272
|
+
"error",
|
|
273
|
+
"-i",
|
|
274
|
+
silent,
|
|
275
|
+
...tracks.flatMap((track) => ["-i", track.path]),
|
|
276
|
+
"-filter_complex",
|
|
277
|
+
[...chains, mix].join(";"),
|
|
278
|
+
"-map",
|
|
279
|
+
"0:v",
|
|
280
|
+
"-map",
|
|
281
|
+
"[mix]",
|
|
282
|
+
"-c:v",
|
|
283
|
+
"copy",
|
|
284
|
+
"-c:a",
|
|
285
|
+
"aac",
|
|
286
|
+
"-b:a",
|
|
287
|
+
"160k",
|
|
288
|
+
"-t",
|
|
289
|
+
seconds.toFixed(3),
|
|
290
|
+
"-movflags",
|
|
291
|
+
"+faststart",
|
|
292
|
+
mixed,
|
|
293
|
+
]);
|
|
294
|
+
if (code === 0) {
|
|
295
|
+
out = mixed;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
errors.push(`Der Ton konnte nicht gemischt werden: ${stderr.slice(-200)}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
mp4: new Uint8Array(await readFile(out)),
|
|
303
|
+
png,
|
|
304
|
+
duration: seconds,
|
|
305
|
+
errors: [...new Set(errors)].slice(0, 10),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
await context.close();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `window.wizard` — the whole contract between a widget and the platform. Injected ahead of the
|
|
3
|
+
* widget's own scripts; reads its payload from <script type="application/json" id="wizard-payload">.
|
|
4
|
+
*
|
|
5
|
+
* wizard.data the run's data (keys from the widget step's `data` map)
|
|
6
|
+
* wizard.brand { name, accent, logo, colors, facts } — logo as a data URL or null,
|
|
7
|
+
* colors [{ name, value }], facts { key: value }
|
|
8
|
+
* wizard.ai "generated" | "edited" when the run's media in `data` was made or
|
|
9
|
+
* changed by a model, else null — a film labels itself with it
|
|
10
|
+
* wizard.mode "view" in the browser, "export" while a PNG/PDF/MP4 is rendered
|
|
11
|
+
* wizard.file(path) text of a workspace file (JSON, CSV, SVG, …)
|
|
12
|
+
* wizard.json(path) a workspace JSON file, parsed
|
|
13
|
+
* wizard.url(path) a workspace file as a data URL (images, fonts, audio)
|
|
14
|
+
* wizard.ready() call once the first frame is drawn; exports wait for it
|
|
15
|
+
* wizard.timeline({ duration, seek, poster?, audio? })
|
|
16
|
+
* an animation of `duration` seconds; seek(t) must draw time t —
|
|
17
|
+
* synchronously, or as a promise that resolves once it is drawn (a
|
|
18
|
+
* film waits for its clips). Enables the MP4 export; `poster` is the
|
|
19
|
+
* second the PNG/PDF show (default 0). A film names its sound in
|
|
20
|
+
* `audio`: [{ src, start, duration?, volume? }].
|
|
21
|
+
*/
|
|
22
|
+
export const WIDGET_RUNTIME = `(function () {
|
|
23
|
+
var el = document.getElementById("wizard-payload");
|
|
24
|
+
var payload = el ? JSON.parse(el.textContent) : { data: {}, brand: {}, files: {}, base: "" };
|
|
25
|
+
var files = payload.files || {};
|
|
26
|
+
var resolveReady;
|
|
27
|
+
var readyPromise = new Promise(function (r) { resolveReady = r; });
|
|
28
|
+
var timeline = null;
|
|
29
|
+
function find(path) {
|
|
30
|
+
var p = String(path || "").replace(/^\\.?\\/+/, "");
|
|
31
|
+
return files[p] || files[payload.base + p] || null;
|
|
32
|
+
}
|
|
33
|
+
function utf8ToB64(text) {
|
|
34
|
+
var bytes = new TextEncoder().encode(text);
|
|
35
|
+
var bin = "";
|
|
36
|
+
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
37
|
+
return btoa(bin);
|
|
38
|
+
}
|
|
39
|
+
function b64ToUtf8(b64) {
|
|
40
|
+
var bin = atob(b64);
|
|
41
|
+
var bytes = new Uint8Array(bin.length);
|
|
42
|
+
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
43
|
+
return new TextDecoder().decode(bytes);
|
|
44
|
+
}
|
|
45
|
+
window.wizard = {
|
|
46
|
+
data: payload.data || {},
|
|
47
|
+
brand: payload.brand || {},
|
|
48
|
+
ai: payload.ai || null,
|
|
49
|
+
mode: window.__WIZARD_EXPORT ? "export" : "view",
|
|
50
|
+
file: function (path) {
|
|
51
|
+
var f = find(path);
|
|
52
|
+
if (!f) return undefined;
|
|
53
|
+
return f.text != null ? f.text : b64ToUtf8(f.b64);
|
|
54
|
+
},
|
|
55
|
+
json: function (path) {
|
|
56
|
+
var t = this.file(path);
|
|
57
|
+
return t == null ? undefined : JSON.parse(t);
|
|
58
|
+
},
|
|
59
|
+
url: function (path) {
|
|
60
|
+
var f = find(path);
|
|
61
|
+
if (!f) return "";
|
|
62
|
+
return "data:" + f.mime + ";base64," + (f.b64 != null ? f.b64 : utf8ToB64(f.text));
|
|
63
|
+
},
|
|
64
|
+
ready: function () { resolveReady(); },
|
|
65
|
+
timeline: function (t) {
|
|
66
|
+
if (t && typeof t.seek === "function" && t.duration > 0) timeline = t;
|
|
67
|
+
},
|
|
68
|
+
__ready: readyPromise,
|
|
69
|
+
__timeline: function () { return timeline; }
|
|
70
|
+
};
|
|
71
|
+
// A sandboxed frame can start at 0×0 and get its real size later without a "resize" event;
|
|
72
|
+
// widgets that fit themselves to the window would stay at scale 0. Fire it for them.
|
|
73
|
+
var lastW = innerWidth, lastH = innerHeight;
|
|
74
|
+
addEventListener("resize", function () { lastW = innerWidth; lastH = innerHeight; });
|
|
75
|
+
if (window.ResizeObserver) {
|
|
76
|
+
new ResizeObserver(function () {
|
|
77
|
+
if (innerWidth !== lastW || innerHeight !== lastH) {
|
|
78
|
+
lastW = innerWidth;
|
|
79
|
+
lastH = innerHeight;
|
|
80
|
+
dispatchEvent(new Event("resize"));
|
|
81
|
+
}
|
|
82
|
+
}).observe(document.documentElement);
|
|
83
|
+
}
|
|
84
|
+
})();`;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { offlineContext } from "../render/chromium.js";
|
|
2
|
+
/**
|
|
3
|
+
* Runs a model-written script over workspace files inside Chromium with no network: reshaping
|
|
4
|
+
* downloaded data (simplify outlines, merge files, precompute paths) without passing it through
|
|
5
|
+
* the model. `files` maps paths to text; the script's return value is the result.
|
|
6
|
+
*/
|
|
7
|
+
export async function runScript(script, files, timeoutMs = 20_000) {
|
|
8
|
+
const context = await offlineContext();
|
|
9
|
+
try {
|
|
10
|
+
const page = await context.newPage();
|
|
11
|
+
await page.setContent("<!doctype html><html><body></body></html>");
|
|
12
|
+
const run = page.evaluate(async ({ script, files }) => {
|
|
13
|
+
const fn = new Function("files", `return (async () => {\n${script}\n})();`);
|
|
14
|
+
return await fn(files);
|
|
15
|
+
}, { script, files });
|
|
16
|
+
let timer;
|
|
17
|
+
const timeout = new Promise((_, reject) => {
|
|
18
|
+
timer = setTimeout(() => reject(new Error(`The script ran longer than ${timeoutMs / 1000} seconds.`)), timeoutMs);
|
|
19
|
+
});
|
|
20
|
+
try {
|
|
21
|
+
return await Promise.race([run, timeout]);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await context.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { allFields, dataRef, widgetSize, } from "@engenty-wizards/shared/definition";
|
|
2
|
+
import { resolveRef } from "../engine/template.js";
|
|
3
|
+
import { StepError } from "../engine/types.js";
|
|
4
|
+
import { extFor, loadAsset } from "../files/storage.js";
|
|
5
|
+
import { markMedia } from "../media/marking.js";
|
|
6
|
+
import { mainLogoId } from "../services/brand.js";
|
|
7
|
+
import { snapshotFile } from "../services/files.js";
|
|
8
|
+
import { bundleWidget } from "./bundle.js";
|
|
9
|
+
import { FILM_MEDIA_ORIGIN, probeWidget, renderFilm } from "./render.js";
|
|
10
|
+
export async function widgetBrand(project) {
|
|
11
|
+
const logoId = await mainLogoId(project.id);
|
|
12
|
+
const logo = logoId ? await loadAsset(logoId) : null;
|
|
13
|
+
const colors = project.brand.colors ?? [];
|
|
14
|
+
return {
|
|
15
|
+
name: project.brand.name ?? "",
|
|
16
|
+
accent: colors[0]?.value ?? null,
|
|
17
|
+
logo: logo ? `data:${logo.row.mime};base64,${logo.data.toString("base64")}` : null,
|
|
18
|
+
colors,
|
|
19
|
+
facts: Object.fromEntries(project.facts.filter((f) => f.value.trim()).map((f) => [f.key, f.value])),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** A widget inlines media up to this size; a film gets its media from the renderer instead. */
|
|
23
|
+
const INLINE_LIMIT = 5_000_000;
|
|
24
|
+
const MEDIA_KINDS = { image: "image", video: "video", voice: "audio" };
|
|
25
|
+
/**
|
|
26
|
+
* The step's data map resolved against the run: raw values; uploads and the images, clips and
|
|
27
|
+
* voice-over earlier steps made as URLs the page can show — data URLs, or for a film addresses
|
|
28
|
+
* its renderer answers (kept in `media`).
|
|
29
|
+
*/
|
|
30
|
+
async function widgetData(step, ctx) {
|
|
31
|
+
const out = {};
|
|
32
|
+
const media = new Map();
|
|
33
|
+
const fields = allFields(ctx.def);
|
|
34
|
+
// Which of the run's media a model made or changed: what the widget shows of it makes the
|
|
35
|
+
// widget's own result AI media too.
|
|
36
|
+
const made = new Map(Object.values(ctx.state.outputs)
|
|
37
|
+
.flatMap((o) => o.assets ?? [])
|
|
38
|
+
.filter((a) => a.ai)
|
|
39
|
+
.map((a) => [a.id, a.ai]));
|
|
40
|
+
const seen = { generated: 0, edited: 0, own: 0 };
|
|
41
|
+
const urlOf = async (id) => {
|
|
42
|
+
const found = typeof id === "string" && id ? await loadAsset(id) : null;
|
|
43
|
+
if (!found || found.row.runId !== ctx.runId) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
seen[made.get(found.row.id) ?? "own"]++;
|
|
47
|
+
if (step.video) {
|
|
48
|
+
const url = `${FILM_MEDIA_ORIGIN}/${found.row.id}.${extFor(found.row.mime)}`;
|
|
49
|
+
media.set(url, { bytes: new Uint8Array(found.data), mime: found.row.mime });
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
return found.data.byteLength < INLINE_LIMIT
|
|
53
|
+
? `data:${found.row.mime};base64,${found.data.toString("base64")}`
|
|
54
|
+
: null;
|
|
55
|
+
};
|
|
56
|
+
for (const [key, raw] of Object.entries(step.data)) {
|
|
57
|
+
const ref = dataRef(raw);
|
|
58
|
+
const field = fields.find((f) => f.id === ref);
|
|
59
|
+
if (field && (field.kind === "image" || field.kind === "file" || field.kind === "signature")) {
|
|
60
|
+
const value = ctx.state.values[ref];
|
|
61
|
+
out[key] = Array.isArray(value)
|
|
62
|
+
? (await Promise.all(value.map(urlOf))).filter(Boolean)
|
|
63
|
+
: await urlOf(value);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const source = ctx.def.steps.find((s) => `steps.${s.id}` === ref);
|
|
67
|
+
if (source?.type === "generate" && source.asset in MEDIA_KINDS) {
|
|
68
|
+
const kind = MEDIA_KINDS[source.asset];
|
|
69
|
+
const assets = (ctx.state.outputs[source.id]?.assets ?? []).filter((a) => a.kind === kind);
|
|
70
|
+
const urls = (await Promise.all(assets.map((a) => urlOf(a.id)))).filter(Boolean);
|
|
71
|
+
out[key] = source.each ? urls : (urls[0] ?? null);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
out[key] = resolveRef(ref, ctx.scope) ?? null;
|
|
75
|
+
}
|
|
76
|
+
// Only generated media: a model made the film's pictures. Edited pictures, or AI media among
|
|
77
|
+
// the person's own: the result is edited material.
|
|
78
|
+
const origin = seen.generated || seen.edited ? (seen.edited || seen.own ? "edited" : "generated") : undefined;
|
|
79
|
+
return { data: out, media, origin };
|
|
80
|
+
}
|
|
81
|
+
/** A film: the widget's timeline rendered to an MP4 with sound — that video is the result. */
|
|
82
|
+
async function runFilmStep(step, ctx, html, data, media, origin) {
|
|
83
|
+
await ctx.emit("info", "Der Film wird geschnitten – das dauert etwa eine Minute.");
|
|
84
|
+
let told = 0;
|
|
85
|
+
const film = await renderFilm(html, widgetSize(step), media, (done) => {
|
|
86
|
+
if (done - told >= 0.25) {
|
|
87
|
+
told = done;
|
|
88
|
+
void ctx.emit("info", `${Math.round(done * 100)} % geschnitten`);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (!film) {
|
|
92
|
+
throw new StepError("Der Film konnte nicht gerendert werden: dazu braucht es ffmpeg und eine Zeitleiste im Widget.");
|
|
93
|
+
}
|
|
94
|
+
if (film.errors.length) {
|
|
95
|
+
await ctx.emit("info", `Der Film meldet: ${film.errors[0].slice(0, 200)}`);
|
|
96
|
+
}
|
|
97
|
+
const assets = [
|
|
98
|
+
await ctx.saveAsset({
|
|
99
|
+
stepId: step.id,
|
|
100
|
+
kind: "video",
|
|
101
|
+
mime: "video/mp4",
|
|
102
|
+
name: `${step.id}.mp4`,
|
|
103
|
+
// Cutting re-encodes every frame, which drops what the clips and stills carried.
|
|
104
|
+
data: origin ? markMedia(film.mp4, "video/mp4", { origin }) : film.mp4,
|
|
105
|
+
ai: origin,
|
|
106
|
+
}),
|
|
107
|
+
await ctx.saveAsset({
|
|
108
|
+
stepId: step.id,
|
|
109
|
+
kind: "poster",
|
|
110
|
+
mime: "image/png",
|
|
111
|
+
name: `${step.id}.png`,
|
|
112
|
+
data: origin ? markMedia(film.png, "image/png", { origin }) : film.png,
|
|
113
|
+
ai: origin,
|
|
114
|
+
}),
|
|
115
|
+
];
|
|
116
|
+
return {
|
|
117
|
+
json: data,
|
|
118
|
+
assets,
|
|
119
|
+
widget: { duration: film.duration, errors: film.errors },
|
|
120
|
+
at: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** A widget step makes no model call: it binds this run's data into the widget and keeps the file. */
|
|
124
|
+
export async function runWidgetStep(step, ctx) {
|
|
125
|
+
const { data, media, origin } = await widgetData(step, ctx);
|
|
126
|
+
const html = await bundleWidget({
|
|
127
|
+
entry: step.entry,
|
|
128
|
+
files: ctx.files,
|
|
129
|
+
data,
|
|
130
|
+
brand: await widgetBrand(ctx.project),
|
|
131
|
+
media: step.video ? FILM_MEDIA_ORIGIN : undefined,
|
|
132
|
+
ai: origin,
|
|
133
|
+
});
|
|
134
|
+
if (step.video) {
|
|
135
|
+
return runFilmStep(step, ctx, html, data, media, origin);
|
|
136
|
+
}
|
|
137
|
+
let probe = null;
|
|
138
|
+
try {
|
|
139
|
+
probe = await probeWidget(html, widgetSize(step));
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
console.error(`[widget ${step.id}]`, err);
|
|
143
|
+
}
|
|
144
|
+
if (probe?.errors.length) {
|
|
145
|
+
await ctx.emit("info", `Das Widget meldet einen Fehler: ${probe.errors[0].slice(0, 200)}`);
|
|
146
|
+
}
|
|
147
|
+
const assets = [
|
|
148
|
+
await ctx.saveAsset({
|
|
149
|
+
stepId: step.id,
|
|
150
|
+
kind: "widget",
|
|
151
|
+
mime: "text/html",
|
|
152
|
+
name: `${step.id}.html`,
|
|
153
|
+
data: html,
|
|
154
|
+
}),
|
|
155
|
+
];
|
|
156
|
+
if (probe) {
|
|
157
|
+
assets.push(await ctx.saveAsset({
|
|
158
|
+
stepId: step.id,
|
|
159
|
+
kind: "poster",
|
|
160
|
+
mime: "image/png",
|
|
161
|
+
name: `${step.id}.png`,
|
|
162
|
+
data: probe.png,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
json: data,
|
|
167
|
+
assets,
|
|
168
|
+
widget: { duration: probe?.duration ?? null, errors: probe?.errors ?? [] },
|
|
169
|
+
at: new Date().toISOString(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** The widget with its sample data (or none) — for the studio preview and the architect's check. */
|
|
173
|
+
export async function widgetPreviewHtml(step, files, project, data) {
|
|
174
|
+
let sample = data ?? {};
|
|
175
|
+
if (data === undefined && step.sample) {
|
|
176
|
+
const found = await snapshotFile(files, step.sample);
|
|
177
|
+
if (found) {
|
|
178
|
+
try {
|
|
179
|
+
sample = JSON.parse(found.data.toString("utf8"));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
sample = {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return bundleWidget({
|
|
187
|
+
entry: step.entry,
|
|
188
|
+
files,
|
|
189
|
+
data: sample,
|
|
190
|
+
brand: await widgetBrand(project),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 566.93 566.93">
|
|
3
|
+
<defs>
|
|
4
|
+
<style>
|
|
5
|
+
.cls-1 {
|
|
6
|
+
fill-rule: evenodd;
|
|
7
|
+
opacity: .5;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.cls-2 {
|
|
11
|
+
fill: #fff;
|
|
12
|
+
}
|
|
13
|
+
</style>
|
|
14
|
+
</defs>
|
|
15
|
+
<path class="cls-1" d="M272.03,100.72c100.92,0,182.74,81.82,182.74,182.75s-81.82,182.74-182.74,182.74-182.75-81.82-182.75-182.74,81.82-182.75,182.75-182.75"/>
|
|
16
|
+
<g>
|
|
17
|
+
<path class="cls-2" d="M170.79,353.74c-1.08,0-2.05-.43-2.92-1.31-.88-.87-1.31-1.84-1.31-2.92,0-.67.07-1.27.2-1.81l47.34-129.32c.4-1.48,1.24-2.79,2.52-3.93,1.27-1.14,3.05-1.71,5.34-1.71h29.81c2.28,0,4.06.57,5.34,1.71,1.27,1.14,2.11,2.45,2.52,3.93l47.14,129.32c.27.54.4,1.14.4,1.81,0,1.08-.44,2.05-1.31,2.92s-1.91,1.31-3.12,1.31h-24.78c-2.01,0-3.52-.5-4.53-1.51-1.01-1.01-1.65-1.91-1.91-2.72l-7.86-20.55h-53.78l-7.65,20.55c-.27.81-.88,1.71-1.81,2.72-.94,1.01-2.55,1.51-4.83,1.51h-24.78ZM218.13,299.96h37.47l-18.93-53.18-18.53,53.18Z"/>
|
|
18
|
+
<path class="cls-2" d="M328.11,353.74c-1.48,0-2.69-.47-3.63-1.41-.94-.94-1.41-2.15-1.41-3.63v-130.93c0-1.48.47-2.68,1.41-3.63s2.15-1.41,3.63-1.41h26.99c1.48,0,2.68.47,3.63,1.41.94.94,1.41,2.15,1.41,3.63v130.93c0,1.48-.47,2.69-1.41,3.63-.94.94-2.15,1.41-3.63,1.41h-26.99Z"/>
|
|
19
|
+
</g>
|
|
20
|
+
</svg>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1789.84 566.93">
|
|
3
|
+
<defs>
|
|
4
|
+
<style>
|
|
5
|
+
.cls-1 {
|
|
6
|
+
fill-rule: evenodd;
|
|
7
|
+
opacity: .5;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.cls-2 {
|
|
11
|
+
fill: #fff;
|
|
12
|
+
}
|
|
13
|
+
</style>
|
|
14
|
+
</defs>
|
|
15
|
+
<path class="cls-1" d="M352.84,410.77h1093.16c85.55,0,145.54-59.94,145.54-133.2h0c0-73.27-59.99-133.21-145.54-133.21H352.84c-85.55,0-145.54,59.94-145.54,133.2h0c0,73.26,64.99,133.2,150.54,133.2"/>
|
|
16
|
+
<g>
|
|
17
|
+
<path class="cls-2" d="M326.09,341c-.98,0-1.87-.4-2.66-1.19-.8-.79-1.19-1.68-1.19-2.66,0-.61.06-1.16.18-1.65l43.18-117.96c.37-1.35,1.13-2.54,2.3-3.58,1.16-1.04,2.79-1.56,4.87-1.56h27.19c2.08,0,3.7.52,4.87,1.56,1.16,1.04,1.93,2.24,2.3,3.58l42.99,117.96c.24.49.37,1.04.37,1.65,0,.98-.4,1.87-1.19,2.66-.8.8-1.75,1.19-2.85,1.19h-22.6c-1.84,0-3.22-.46-4.13-1.38-.92-.92-1.5-1.75-1.75-2.48l-7.17-18.74h-49.06l-6.98,18.74c-.25.74-.8,1.56-1.65,2.48-.86.92-2.33,1.38-4.41,1.38h-22.6ZM369.27,291.94h34.18l-17.27-48.51-16.9,48.51Z"/>
|
|
18
|
+
<path class="cls-2" d="M469.58,341c-1.35,0-2.45-.43-3.31-1.29-.86-.86-1.29-1.96-1.29-3.31v-119.43c0-1.35.43-2.45,1.29-3.31s1.96-1.29,3.31-1.29h24.62c1.35,0,2.45.43,3.31,1.29.86.86,1.29,1.96,1.29,3.31v119.43c0,1.35-.43,2.45-1.29,3.31s-1.96,1.29-3.31,1.29h-24.62Z"/>
|
|
19
|
+
</g>
|
|
20
|
+
<g>
|
|
21
|
+
<path class="cls-2" d="M639.25,325.87c-8.43,0-15.63-1.41-21.59-4.24-5.97-2.83-10.58-6.95-13.86-12.38-3.27-5.42-5.04-11.99-5.31-19.71-.09-3.86-.13-7.98-.13-12.38s.04-8.56.13-12.51c.27-7.53,2.04-13.94,5.31-19.24,3.27-5.29,7.94-9.35,13.99-12.17,6.05-2.83,13.2-4.24,21.46-4.24,6.64,0,12.46.85,17.49,2.56,5.02,1.7,9.24,3.92,12.65,6.66,3.41,2.74,6.01,5.72,7.8,8.95,1.79,3.23,2.74,6.32,2.83,9.28,0,.81-.27,1.5-.81,2.08-.54.58-1.26.87-2.15.87h-16.68c-.9,0-1.59-.18-2.08-.54-.49-.36-.92-.9-1.28-1.61-.63-1.61-1.64-3.25-3.03-4.91-1.39-1.66-3.25-3.07-5.58-4.24-2.33-1.16-5.38-1.75-9.15-1.75-5.65,0-10.11,1.48-13.39,4.44-3.27,2.96-5.04,7.8-5.31,14.53-.27,7.71-.27,15.56,0,23.54.27,7,2.08,12.02,5.45,15.07,3.36,3.05,7.87,4.57,13.52,4.57,3.68,0,6.97-.65,9.89-1.95,2.91-1.3,5.22-3.34,6.93-6.12,1.7-2.78,2.56-6.32,2.56-10.63v-3.36h-14.66c-.9,0-1.66-.34-2.29-1.01-.63-.67-.94-1.46-.94-2.35v-8.61c0-.99.31-1.79.94-2.42.63-.63,1.39-.94,2.29-.94h32.96c.99,0,1.79.31,2.42.94.63.63.94,1.44.94,2.42v14.66c0,7.71-1.68,14.31-5.04,19.78-3.36,5.47-8.16,9.66-14.39,12.58-6.23,2.92-13.52,4.37-21.86,4.37Z"/>
|
|
22
|
+
<path class="cls-2" d="M710.73,324.52c-.99,0-1.79-.31-2.42-.94-.63-.63-.94-1.43-.94-2.42v-87.44c0-.99.31-1.79.94-2.42s1.43-.94,2.42-.94h60.94c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v10.76c0,.9-.32,1.66-.94,2.29-.63.63-1.44.94-2.42.94h-43.59v21.12h40.63c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v9.95c0,.9-.32,1.66-.94,2.29s-1.44.94-2.42.94h-40.63v21.79h44.66c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v10.63c0,.99-.32,1.79-.94,2.42s-1.44.94-2.42.94h-62.02Z"/>
|
|
23
|
+
<path class="cls-2" d="M805.88,324.52c-.99,0-1.79-.31-2.42-.94-.63-.63-.94-1.43-.94-2.42v-87.44c0-.99.31-1.79.94-2.42s1.43-.94,2.42-.94h12.24c1.34,0,2.33.32,2.96.94.63.63,1.03,1.12,1.21,1.48l35.92,56.1v-55.15c0-.99.31-1.79.94-2.42s1.39-.94,2.29-.94h13.72c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v87.44c0,.9-.32,1.68-.94,2.35-.63.67-1.44,1.01-2.42,1.01h-12.38c-1.35,0-2.31-.34-2.89-1.01-.58-.67-1.01-1.14-1.28-1.41l-35.78-54.35v53.41c0,.99-.32,1.79-.94,2.42s-1.44.94-2.42.94h-13.59Z"/>
|
|
24
|
+
<path class="cls-2" d="M912.19,324.52c-.99,0-1.79-.31-2.42-.94-.63-.63-.94-1.43-.94-2.42v-87.44c0-.99.31-1.79.94-2.42s1.43-.94,2.42-.94h60.94c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v10.76c0,.9-.32,1.66-.94,2.29-.63.63-1.44.94-2.42.94h-43.59v21.12h40.63c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v9.95c0,.9-.32,1.66-.94,2.29s-1.44.94-2.42.94h-40.63v21.79h44.66c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v10.63c0,.99-.32,1.79-.94,2.42s-1.44.94-2.42.94h-62.02Z"/>
|
|
25
|
+
<path class="cls-2" d="M1007.35,324.52c-.99,0-1.79-.31-2.42-.94-.63-.63-.94-1.43-.94-2.42v-87.44c0-.99.31-1.79.94-2.42s1.43-.94,2.42-.94h35.65c11.21,0,20.04,2.58,26.5,7.74,6.46,5.16,9.69,12.58,9.69,22.26,0,6.55-1.57,12.02-4.71,16.41-3.14,4.4-7.35,7.67-12.65,9.82l18.97,33.63c.27.54.4,1.03.4,1.48,0,.72-.27,1.37-.81,1.95-.54.58-1.21.88-2.02.88h-14.93c-1.61,0-2.82-.42-3.63-1.28-.81-.85-1.39-1.63-1.75-2.35l-16.41-30.8h-16.28v31.07c0,.99-.32,1.79-.94,2.42s-1.44.94-2.42.94h-14.66ZM1025.38,273h17.22c4.93,0,8.59-1.12,10.96-3.36,2.38-2.24,3.56-5.38,3.56-9.42s-1.17-7.22-3.5-9.55c-2.33-2.33-6.01-3.5-11.03-3.5h-17.22v25.83Z"/>
|
|
26
|
+
<path class="cls-2" d="M1100.62,324.52c-.81,0-1.48-.29-2.02-.88-.54-.58-.81-1.23-.81-1.95,0-.45.04-.85.13-1.21l32.02-86.5c.27-.99.81-1.84,1.61-2.56.81-.72,1.93-1.08,3.36-1.08h17.22c1.43,0,2.56.36,3.36,1.08.81.72,1.35,1.57,1.61,2.56l32.02,86.5c.09.36.13.76.13,1.21,0,.72-.27,1.37-.81,1.95-.54.58-1.21.88-2.02.88h-13.99c-1.35,0-2.33-.31-2.96-.94-.63-.63-1.03-1.21-1.21-1.75l-5.78-15.07h-37.94l-5.78,15.07c-.18.54-.58,1.12-1.21,1.75-.63.63-1.61.94-2.96.94h-13.99ZM1129.54,289.41h27.85l-13.86-38.74-13.99,38.74Z"/>
|
|
27
|
+
<path class="cls-2" d="M1225.64,324.52c-.99,0-1.79-.31-2.42-.94-.63-.63-.94-1.43-.94-2.42v-72.1h-24.48c-.9,0-1.66-.31-2.29-.94-.63-.63-.94-1.39-.94-2.29v-12.11c0-.99.31-1.79.94-2.42s1.39-.94,2.29-.94h70.49c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v12.11c0,.9-.32,1.66-.94,2.29s-1.44.94-2.42.94h-24.35v72.1c0,.99-.32,1.79-.94,2.42s-1.44.94-2.42.94h-14.93Z"/>
|
|
28
|
+
<path class="cls-2" d="M1298.32,324.52c-.99,0-1.79-.31-2.42-.94-.63-.63-.94-1.43-.94-2.42v-87.44c0-.99.31-1.79.94-2.42s1.43-.94,2.42-.94h60.94c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v10.76c0,.9-.32,1.66-.94,2.29-.63.63-1.44.94-2.42.94h-43.59v21.12h40.63c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v9.95c0,.9-.32,1.66-.94,2.29s-1.44.94-2.42.94h-40.63v21.79h44.66c.99,0,1.79.32,2.42.94.63.63.94,1.44.94,2.42v10.63c0,.99-.32,1.79-.94,2.42s-1.44.94-2.42.94h-62.02Z"/>
|
|
29
|
+
<path class="cls-2" d="M1393.48,324.52c-.99,0-1.79-.31-2.42-.94-.63-.63-.94-1.43-.94-2.42v-87.44c0-.99.31-1.79.94-2.42s1.43-.94,2.42-.94h33.77c8.97,0,16.46,1.41,22.46,4.24,6.01,2.83,10.6,6.95,13.79,12.38,3.18,5.43,4.86,12.13,5.04,20.11.09,3.95.13,7.4.13,10.36s-.05,6.37-.13,10.22c-.27,8.34-1.93,15.25-4.98,20.72-3.05,5.47-7.53,9.53-13.45,12.17-5.92,2.65-13.32,3.97-22.2,3.97h-34.44ZM1411.51,307.17h15.74c4.48,0,8.16-.67,11.03-2.02,2.87-1.35,5-3.47,6.39-6.39,1.39-2.91,2.13-6.7,2.22-11.37.09-2.6.16-4.93.2-7,.04-2.06.04-4.12,0-6.19-.05-2.06-.11-4.35-.2-6.86-.18-6.73-1.91-11.68-5.18-14.87-3.27-3.18-8.32-4.78-15.13-4.78h-15.07v59.46Z"/>
|
|
30
|
+
</g>
|
|
31
|
+
</svg>
|