blume 2.1.0 → 2.1.2
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/CHANGELOG.md +110 -0
- package/dist/cli/{chunk-ga0pf4aj.js → chunk-0n9xkp3d.js} +12 -3
- package/dist/cli/{chunk-ga0pf4aj.js.map → chunk-0n9xkp3d.js.map} +3 -3
- package/dist/cli/{chunk-43zcf4gz.js → chunk-0pmb4tr4.js} +541 -368
- package/dist/cli/chunk-0pmb4tr4.js.map +35 -0
- package/dist/cli/{chunk-9geymqns.js → chunk-0w6hs479.js} +15 -15
- package/dist/cli/{chunk-xkz5xbtv.js → chunk-124fvmzq.js} +2802 -1422
- package/dist/cli/chunk-124fvmzq.js.map +220 -0
- package/dist/cli/chunk-150jk7km.js +52 -0
- package/dist/cli/chunk-150jk7km.js.map +10 -0
- package/dist/cli/{chunk-y4ykbark.js → chunk-1f882900.js} +2 -2
- package/dist/cli/{chunk-5ffywsmb.js → chunk-297ajdcv.js} +8 -6
- package/dist/cli/{chunk-5ffywsmb.js.map → chunk-297ajdcv.js.map} +3 -3
- package/dist/cli/{chunk-h2a48yhd.js → chunk-32khwcb6.js} +120 -16
- package/dist/cli/{chunk-h2a48yhd.js.map → chunk-32khwcb6.js.map} +10 -10
- package/dist/cli/{chunk-5cg2rzgf.js → chunk-36bxs9c7.js} +20 -49
- package/dist/cli/{chunk-5cg2rzgf.js.map → chunk-36bxs9c7.js.map} +4 -5
- package/dist/cli/{chunk-py62fewh.js → chunk-53jqw8z0.js} +3 -3
- package/dist/cli/{chunk-5wj2qwn2.js → chunk-58bt3hag.js} +4 -4
- package/dist/cli/{chunk-e7vvk5ya.js → chunk-5cmts6dv.js} +3 -3
- package/dist/cli/{chunk-y5h4r2y0.js → chunk-60hz265r.js} +15 -5
- package/dist/cli/chunk-60hz265r.js.map +10 -0
- package/dist/cli/chunk-74wfhv79.js +65 -0
- package/dist/cli/chunk-74wfhv79.js.map +10 -0
- package/dist/cli/{chunk-g2z9x01v.js → chunk-9qzgg6mj.js} +3 -3
- package/dist/cli/{chunk-q3wxxq39.js → chunk-a57yh0h9.js} +5 -5
- package/dist/cli/{chunk-713mngdq.js → chunk-ave1wcbr.js} +76 -10
- package/dist/cli/chunk-ave1wcbr.js.map +14 -0
- package/dist/cli/{chunk-2n4ps1b3.js → chunk-bqxaqn5p.js} +40 -22
- package/dist/cli/chunk-bqxaqn5p.js.map +15 -0
- package/dist/cli/{chunk-jnwj5kjj.js → chunk-bxwhtvw8.js} +20 -7
- package/dist/cli/{chunk-jnwj5kjj.js.map → chunk-bxwhtvw8.js.map} +3 -3
- package/dist/cli/chunk-ckrcm08h.js +324 -0
- package/dist/cli/chunk-ckrcm08h.js.map +14 -0
- package/dist/cli/{chunk-2f3ndkpw.js → chunk-desvm72z.js} +3 -3
- package/dist/cli/{chunk-gp84za0h.js → chunk-dhwtqnnd.js} +2 -2
- package/dist/cli/{chunk-4h8z9vv5.js → chunk-dpy8qdjf.js} +93 -47
- package/dist/cli/chunk-dpy8qdjf.js.map +17 -0
- package/dist/cli/{chunk-2f7n4qzt.js → chunk-dqhg54mr.js} +1 -1
- package/dist/cli/{chunk-0gz36akr.js → chunk-dv2z0rzf.js} +21 -5
- package/dist/cli/chunk-dv2z0rzf.js.map +12 -0
- package/dist/cli/{chunk-cm89scdv.js → chunk-es8h0v3j.js} +9 -11
- package/dist/cli/{chunk-cm89scdv.js.map → chunk-es8h0v3j.js.map} +3 -3
- package/dist/cli/{chunk-zhvd6adm.js → chunk-gfc5fqvf.js} +14 -14
- package/dist/cli/{chunk-nd1821sk.js → chunk-gxtg6ed2.js} +33 -19
- package/dist/cli/chunk-gxtg6ed2.js.map +10 -0
- package/dist/cli/{chunk-p804f3pe.js → chunk-hvcxng5f.js} +6 -6
- package/dist/cli/{chunk-ta27feev.js → chunk-j1x92mx9.js} +21 -2
- package/dist/cli/chunk-j1x92mx9.js.map +10 -0
- package/dist/cli/{chunk-rekqz4qg.js → chunk-k3gr1nm9.js} +19 -16
- package/dist/cli/chunk-k3gr1nm9.js.map +10 -0
- package/dist/cli/{chunk-1n228xqb.js → chunk-kdpzw38a.js} +2 -2
- package/dist/cli/{chunk-cy5bm0j0.js → chunk-ms9z66nz.js} +2 -2
- package/dist/cli/{chunk-cy5bm0j0.js.map → chunk-ms9z66nz.js.map} +2 -2
- package/dist/cli/{chunk-zf748avv.js → chunk-nbyvgnnm.js} +193 -286
- package/dist/cli/chunk-nbyvgnnm.js.map +36 -0
- package/dist/cli/{chunk-6bm0xpgd.js → chunk-s0bdnc7v.js} +2 -2
- package/dist/cli/{chunk-yxmk4pec.js → chunk-sc93apfs.js} +17 -16
- package/dist/cli/{chunk-yxmk4pec.js.map → chunk-sc93apfs.js.map} +3 -3
- package/dist/cli/{chunk-cmtcnh9f.js → chunk-tfycddbt.js} +2 -2
- package/dist/cli/{chunk-9xr4dgee.js → chunk-twy7psdz.js} +29 -16
- package/dist/cli/chunk-twy7psdz.js.map +10 -0
- package/dist/cli/{chunk-ntv4qrt7.js → chunk-y4msrwm9.js} +15 -15
- package/dist/cli/{chunk-yj17vs39.js → chunk-ydp3065p.js} +5 -5
- package/dist/cli/index.js +24 -19
- package/dist/cli/index.js.map +3 -3
- package/dist/types/ai/ask-context.d.ts +6 -2
- package/dist/types/ai/ask.d.ts +24 -5
- package/dist/types/ai/component-markdown.d.ts +2 -0
- package/dist/types/ai/openapi-components.d.ts +4 -1
- package/dist/types/analytics/clics.d.ts +37 -0
- package/dist/types/analytics/index.d.ts +2 -0
- package/dist/types/analytics/schema.d.ts +18 -0
- package/dist/types/astro/integration.d.ts +11 -4
- package/dist/types/astro/markdown-negotiation.d.ts +3 -0
- package/dist/types/astro/pages.d.ts +10 -0
- package/dist/types/cli/init/scaffold.d.ts +1 -1
- package/dist/types/components/content/api-field.d.ts +4 -1
- package/dist/types/components/content/api-page.d.ts +6 -1
- package/dist/types/components/openapi/code-samples.d.ts +29 -0
- package/dist/types/components/openapi/helpers.d.ts +22 -1
- package/dist/types/components/openapi/security.d.ts +10 -0
- package/dist/types/components/openapi/snippets.d.ts +60 -0
- package/dist/types/core/changelog-index.d.ts +8 -0
- package/dist/types/core/config-input.d.ts +37 -6
- package/dist/types/core/data.d.ts +4 -0
- package/dist/types/core/i18n-ui.d.ts +2 -0
- package/dist/types/core/links.d.ts +8 -0
- package/dist/types/core/powered-by.d.ts +12 -0
- package/dist/types/core/probe.d.ts +2 -0
- package/dist/types/core/project-graph.d.ts +6 -0
- package/dist/types/core/redirect-patterns.d.ts +11 -3
- package/dist/types/core/schema.d.ts +228 -3
- package/dist/types/core/sources/cache.d.ts +3 -0
- package/dist/types/core/sources/contentful-rich-text.d.ts +11 -2
- package/dist/types/core/sources/contentful.d.ts +17 -1
- package/dist/types/core/sources/lexical.d.ts +5 -0
- package/dist/types/core/sources/lower.d.ts +10 -2
- package/dist/types/core/sources/normalize.d.ts +20 -1
- package/dist/types/core/sources/notion.d.ts +8 -0
- package/dist/types/core/sources/remote.d.ts +37 -10
- package/dist/types/core/sources/sanity.d.ts +1 -1
- package/dist/types/core/types.d.ts +17 -0
- package/dist/types/deploy/adapter-output.d.ts +2 -1
- package/dist/types/deploy/adapters/cloudflare.d.ts +7 -5
- package/dist/types/deploy/adapters/node.d.ts +58 -23
- package/dist/types/deploy/adapters/registry.d.ts +10 -0
- package/dist/types/deploy/adapters/types.d.ts +3 -2
- package/dist/types/deploy/cloudflare-negotiation.d.ts +5 -2
- package/dist/types/deploy/node-headers.d.ts +12 -4
- package/dist/types/deploy/platforms/cloudflare.d.ts +10 -7
- package/dist/types/deploy/platforms/node.d.ts +3 -1
- package/dist/types/deploy/platforms/types.d.ts +18 -7
- package/dist/types/deploy/platforms/vercel.d.ts +21 -2
- package/dist/types/deploy/redirects.d.ts +17 -1
- package/dist/types/deploy/sitemap.d.ts +2 -1
- package/dist/types/deploy/vercel-negotiation.d.ts +44 -21
- package/dist/types/narration/build.d.ts +34 -12
- package/dist/types/narration/provider.d.ts +96 -8
- package/dist/types/openapi/checks.d.ts +27 -0
- package/dist/types/openapi/graphql.d.ts +6 -0
- package/dist/types/openapi/model.d.ts +15 -4
- package/dist/types/openapi/parse.d.ts +3 -0
- package/dist/types/openapi/references.d.ts +16 -0
- package/dist/types/openapi/sentence.d.ts +11 -1
- package/dist/types/ratelimit/index.d.ts +1 -1
- package/dist/types/ratelimit/schema.d.ts +8 -0
- package/dist/types/ratelimit/upstash.d.ts +39 -14
- package/dist/types/reference/graphql.d.ts +76 -1
- package/dist/types/reference/index.d.ts +1 -1
- package/dist/types/reference/openapi.d.ts +2 -2
- package/dist/types/reference/options.d.ts +37 -1
- package/dist/types/reference/schema.d.ts +84 -0
- package/dist/types/search/adapters/registry.d.ts +3 -0
- package/dist/types/search/adapters/typesense.d.ts +14 -4
- package/dist/types/search/build.d.ts +3 -1
- package/dist/types/search/orama-index.d.ts +9 -9
- package/dist/types/search/sync/algolia.d.ts +5 -2
- package/dist/types/search/sync/index.d.ts +3 -1
- package/dist/types/search/sync/typesense.d.ts +11 -7
- package/dist/types/seo/jsonld.d.ts +11 -6
- package/dist/types/sources/contentful.d.ts +16 -0
- package/dist/types/sources/github-releases.d.ts +8 -1
- package/dist/types/sources/notion.d.ts +8 -0
- package/dist/types/sources/payload.d.ts +2 -0
- package/dist/types/sources/registry.d.ts +63 -0
- package/dist/types/sources/shared.d.ts +3 -0
- package/dist/types/sources/strapi.d.ts +2 -0
- package/docs/01-quickstart.mdx +4 -2
- package/docs/02-deployment.mdx +59 -13
- package/docs/03-upgrading.mdx +2 -1
- package/docs/04-migrating.mdx +9 -9
- package/docs/08-faq.mdx +12 -12
- package/docs/advanced/changelog.mdx +30 -2
- package/docs/advanced/custom-pages.mdx +1 -1
- package/docs/advanced/skills.mdx +1 -1
- package/docs/cli/audit.mdx +30 -7
- package/docs/cli/doctor.mdx +6 -2
- package/docs/cli/evals.mdx +4 -4
- package/docs/cli/translate.mdx +1 -0
- package/docs/cli/validate.mdx +4 -3
- package/docs/configuration/analytics.mdx +19 -1
- package/docs/configuration/assistant.mdx +6 -2
- package/docs/configuration/customization.mdx +4 -2
- package/docs/configuration/export.mdx +5 -3
- package/docs/configuration/index.mdx +4 -2
- package/docs/configuration/narration.mdx +43 -6
- package/docs/configuration/rate-limiting.mdx +23 -2
- package/docs/configuration/search.mdx +37 -14
- package/docs/content/components.mdx +2 -2
- package/docs/content/frontmatter.mdx +5 -3
- package/docs/content/i18n.mdx +2 -2
- package/docs/content/includes.mdx +1 -1
- package/docs/content/navigation.mdx +8 -6
- package/docs/content/sources/contentful.mdx +9 -4
- package/docs/content/sources/custom.mdx +7 -3
- package/docs/content/sources/github-releases.mdx +12 -2
- package/docs/content/sources/index.mdx +3 -3
- package/docs/content/sources/notion.mdx +17 -2
- package/docs/content/sources/obsidian.mdx +3 -1
- package/docs/content/sources/payload.mdx +5 -2
- package/docs/content/sources/remote-mdx.mdx +6 -0
- package/docs/content/sources/sanity.mdx +4 -2
- package/docs/content/sources/strapi.mdx +5 -2
- package/docs/content/syntax.mdx +2 -2
- package/docs/discoverability/agent-discovery.mdx +1 -1
- package/docs/discoverability/index.mdx +1 -1
- package/docs/discoverability/json-api.mdx +2 -0
- package/docs/discoverability/llms-txt.mdx +1 -1
- package/docs/discoverability/markdown.mdx +3 -1
- package/docs/discoverability/mcp.mdx +6 -0
- package/docs/discoverability/sitemap-and-robots.mdx +1 -1
- package/docs/discoverability/structured-data.mdx +8 -6
- package/docs/references/api-pages.mdx +1 -1
- package/docs/references/asyncapi.mdx +5 -1
- package/docs/references/graphql.mdx +15 -1
- package/docs/references/openapi.mdx +64 -8
- package/package.json +9 -9
- package/skills/blume/SKILL.md +1 -1
- package/skills/blume-migrate/SKILL.md +2 -2
- package/skills/blume-migrate/assets/{oxfmt@0.67.0.patch → oxfmt@0.71.0.patch} +7 -7
- package/skills/blume-migrate/references/docusaurus.md +4 -4
- package/skills/blume-migrate/references/fumadocs.md +17 -13
- package/skills/blume-migrate/references/monorepo.md +5 -5
- package/skills/blume-migrate/references/nextra.md +8 -5
- package/skills/blume-migrate/references/starlight.md +9 -7
- package/src/ai/ask-context.ts +33 -3
- package/src/ai/ask-limits.ts +13 -0
- package/src/ai/ask.ts +24 -6
- package/src/ai/changelog-markdown.ts +11 -6
- package/src/ai/component-markdown.ts +1 -1
- package/src/ai/cors.ts +12 -3
- package/src/ai/mcp/query.ts +14 -6
- package/src/ai/openapi-components.ts +28 -4
- package/src/ai/relative-links.ts +4 -38
- package/src/analytics/clics.ts +58 -0
- package/src/analytics/head.ts +5 -0
- package/src/analytics/index.ts +2 -0
- package/src/analytics/schema.ts +2 -0
- package/src/astro/generate.ts +91 -56
- package/src/astro/integration.ts +72 -22
- package/src/astro/markdown-negotiation.ts +3 -0
- package/src/astro/module-types.ts +3 -1
- package/src/astro/pages.ts +23 -0
- package/src/astro/render-deps.ts +10 -5
- package/src/astro/runtime-deps.ts +23 -2
- package/src/astro/templates.ts +129 -81
- package/src/audit/catalog.ts +19 -3
- package/src/audit/checks/links.ts +14 -1
- package/src/audit/checks/network.ts +110 -6
- package/src/audit/checks/redirects.ts +6 -4
- package/src/audit/checks/robots.ts +58 -2
- package/src/audit/run.ts +3 -0
- package/src/audit/types.ts +2 -0
- package/src/audit/url.ts +14 -5
- package/src/blume-modules.d.ts +5 -1
- package/src/cli/args.ts +51 -0
- package/src/cli/build-failure.ts +23 -0
- package/src/cli/commands/audit.ts +10 -1
- package/src/cli/commands/dev.ts +18 -2
- package/src/cli/commands/doctor.ts +45 -12
- package/src/cli/commands/eval.ts +36 -4
- package/src/cli/commands/init.ts +6 -4
- package/src/cli/commands/validate.ts +10 -1
- package/src/cli/init/scaffold.ts +9 -6
- package/src/cli/prepare.ts +6 -3
- package/src/cli/required-secrets.ts +3 -2
- package/src/components/content/ApiPlayground.astro +6 -2
- package/src/components/content/api-field.ts +4 -1
- package/src/components/content/api-page.ts +31 -7
- package/src/components/content/mermaid-element.ts +15 -1
- package/src/components/content/view-switcher.ts +7 -4
- package/src/components/islands/hooks.ts +27 -1
- package/src/components/layout/NavTree.astro +49 -5
- package/src/components/layout/NavTreeScript.astro +18 -0
- package/src/components/layout/PageActions.astro +139 -24
- package/src/components/layout/PageLayout.astro +13 -3
- package/src/components/layout/RootLayout.astro +27 -14
- package/src/components/layout/Search.astro +17 -3
- package/src/components/layout/TableOfContents.astro +24 -17
- package/src/components/layout/analytics-client.ts +8 -8
- package/src/components/layout/head-scripts.ts +6 -1
- package/src/components/layout/search/algolia.ts +10 -1
- package/src/components/layout/search/analytics.ts +42 -14
- package/src/components/layout/search/endpoint.ts +48 -5
- package/src/components/layout/search/pagefind.ts +114 -5
- package/src/components/layout/search/types.ts +12 -0
- package/src/components/layout/search/typesense.ts +18 -6
- package/src/components/openapi/AsyncApiOperation.astro +14 -7
- package/src/components/openapi/Authorization.astro +10 -0
- package/src/components/openapi/GraphqlOperation.astro +5 -0
- package/src/components/openapi/Operation.astro +24 -1
- package/src/components/openapi/ParametersTable.astro +8 -4
- package/src/components/openapi/Playground.astro +7 -1
- package/src/components/openapi/RequestBody.astro +19 -2
- package/src/components/openapi/Responses.astro +1 -0
- package/src/components/openapi/SchemaTable.astro +15 -3
- package/src/components/openapi/async-snippets.ts +61 -5
- package/src/components/openapi/async.ts +123 -29
- package/src/components/openapi/graphql-helpers.ts +35 -12
- package/src/components/openapi/helpers.ts +36 -2
- package/src/components/openapi/message-model.ts +54 -11
- package/src/components/openapi/message.ts +14 -4
- package/src/components/openapi/operation-model.ts +16 -7
- package/src/components/openapi/playground-client.ts +77 -58
- package/src/components/openapi/playground-schema.ts +1 -1
- package/src/components/openapi/schema-tree.ts +44 -14
- package/src/components/openapi/security.ts +10 -0
- package/src/components/openapi/snippets.ts +5 -3
- package/src/components/openapi/validate-json.ts +4 -3
- package/src/components/powered-by-middleware.ts +33 -0
- package/src/core/changelog-index.ts +31 -1
- package/src/core/component-diagnostics.ts +53 -0
- package/src/core/component-overrides.ts +32 -0
- package/src/core/config-input.ts +42 -6
- package/src/core/data.ts +2 -0
- package/src/core/diagnostics.ts +5 -0
- package/src/core/i18n-ui.ts +2 -0
- package/src/core/includes.ts +1 -1
- package/src/core/links.ts +47 -17
- package/src/core/powered-by.ts +12 -0
- package/src/core/probe.ts +26 -6
- package/src/core/project-graph.ts +59 -18
- package/src/core/redirect-patterns.ts +36 -7
- package/src/core/schema.ts +26 -6
- package/src/core/sources/cache.ts +6 -0
- package/src/core/sources/contentful-rich-text.ts +29 -6
- package/src/core/sources/contentful.ts +135 -25
- package/src/core/sources/filesystem.ts +26 -4
- package/src/core/sources/github-releases.ts +12 -1
- package/src/core/sources/lexical.ts +15 -10
- package/src/core/sources/lower.ts +19 -3
- package/src/core/sources/mdx-remote.ts +62 -10
- package/src/core/sources/normalize.ts +227 -41
- package/src/core/sources/notion.ts +34 -10
- package/src/core/sources/obsidian.ts +16 -3
- package/src/core/sources/payload.ts +38 -15
- package/src/core/sources/remote.ts +72 -15
- package/src/core/sources/resolve.ts +11 -1
- package/src/core/sources/sanity.ts +43 -25
- package/src/core/sources/strapi.ts +2 -2
- package/src/core/types.ts +18 -0
- package/src/core/ui-packs/ar.ts +1 -0
- package/src/core/ui-packs/bg.ts +1 -0
- package/src/core/ui-packs/bn.ts +1 -0
- package/src/core/ui-packs/ca.ts +1 -0
- package/src/core/ui-packs/cs.ts +1 -0
- package/src/core/ui-packs/da.ts +1 -0
- package/src/core/ui-packs/de.ts +1 -0
- package/src/core/ui-packs/el.ts +1 -0
- package/src/core/ui-packs/es.ts +1 -0
- package/src/core/ui-packs/fa.ts +1 -0
- package/src/core/ui-packs/fi.ts +1 -0
- package/src/core/ui-packs/fr.ts +1 -0
- package/src/core/ui-packs/he.ts +1 -0
- package/src/core/ui-packs/hi.ts +1 -0
- package/src/core/ui-packs/hr.ts +1 -0
- package/src/core/ui-packs/hu.ts +1 -0
- package/src/core/ui-packs/id.ts +1 -0
- package/src/core/ui-packs/it.ts +1 -0
- package/src/core/ui-packs/ja.ts +1 -0
- package/src/core/ui-packs/ko.ts +1 -0
- package/src/core/ui-packs/nl.ts +1 -0
- package/src/core/ui-packs/no.ts +1 -0
- package/src/core/ui-packs/pl.ts +1 -0
- package/src/core/ui-packs/pt-br.ts +1 -0
- package/src/core/ui-packs/pt.ts +1 -0
- package/src/core/ui-packs/ro.ts +1 -0
- package/src/core/ui-packs/ru.ts +1 -0
- package/src/core/ui-packs/sk.ts +1 -0
- package/src/core/ui-packs/sr.ts +1 -0
- package/src/core/ui-packs/sv.ts +1 -0
- package/src/core/ui-packs/th.ts +1 -0
- package/src/core/ui-packs/tr.ts +1 -0
- package/src/core/ui-packs/uk.ts +1 -0
- package/src/core/ui-packs/vi.ts +1 -0
- package/src/core/ui-packs/zh-tw.ts +1 -0
- package/src/core/ui-packs/zh.ts +1 -0
- package/src/deploy/adapter-output.ts +7 -2
- package/src/deploy/adapters/cloudflare.ts +7 -5
- package/src/deploy/adapters/node.ts +47 -9
- package/src/deploy/adapters/types.ts +7 -3
- package/src/deploy/artifacts.ts +2 -1
- package/src/deploy/cloudflare-negotiation.ts +36 -7
- package/src/deploy/headers.ts +15 -1
- package/src/deploy/node-headers.ts +32 -20
- package/src/deploy/platforms/cloudflare.ts +14 -9
- package/src/deploy/platforms/netlify.ts +2 -1
- package/src/deploy/platforms/node.ts +12 -2
- package/src/deploy/platforms/types.ts +21 -7
- package/src/deploy/platforms/vercel.ts +115 -17
- package/src/deploy/redirects.ts +66 -6
- package/src/deploy/rss.ts +3 -1
- package/src/deploy/sitemap.ts +39 -5
- package/src/deploy/vercel-negotiation.ts +325 -103
- package/src/eval/agents.ts +18 -1
- package/src/eval/prompts.ts +6 -3
- package/src/eval/report.ts +34 -13
- package/src/eval/run.ts +5 -0
- package/src/markdown/base-links.ts +15 -14
- package/src/markdown/package-commands.ts +9 -8
- package/src/markdown/relative-links.ts +3 -2
- package/src/narration/build.ts +218 -40
- package/src/narration/provider.ts +89 -9
- package/src/openapi/checks.ts +273 -0
- package/src/openapi/graphql-build.ts +21 -0
- package/src/openapi/graphql.ts +11 -0
- package/src/openapi/model.ts +15 -3
- package/src/openapi/parse.ts +127 -2
- package/src/openapi/references.ts +43 -3
- package/src/openapi/render-mdx.ts +68 -8
- package/src/openapi/sentence.ts +29 -2
- package/src/openapi/source.ts +39 -14
- package/src/ratelimit/index.ts +1 -1
- package/src/ratelimit/runtime.ts +37 -19
- package/src/ratelimit/upstash.ts +40 -10
- package/src/reference/graphql.ts +15 -1
- package/src/reference/index.ts +1 -0
- package/src/reference/openapi.ts +2 -2
- package/src/reference/options.ts +29 -1
- package/src/registry/eject.ts +8 -2
- package/src/search/adapters/locale-scope.ts +30 -0
- package/src/search/adapters/typesense.ts +14 -4
- package/src/search/build.ts +21 -3
- package/src/search/orama-index.ts +163 -31
- package/src/search/source-pages.ts +29 -0
- package/src/search/sync/algolia.ts +99 -9
- package/src/search/sync/index.ts +52 -13
- package/src/search/sync/typesense.ts +72 -27
- package/src/seo/jsonld.ts +38 -20
- package/src/sources/contentful.ts +12 -0
- package/src/sources/github-releases.ts +7 -1
- package/src/sources/notion.ts +7 -0
- package/src/sources/shared.ts +3 -0
- package/src/theme/color.ts +9 -10
- package/src/theme/entry.ts +43 -6
- package/src/translate/validate.ts +23 -1
- package/dist/cli/chunk-0gz36akr.js.map +0 -12
- package/dist/cli/chunk-2n4ps1b3.js.map +0 -15
- package/dist/cli/chunk-43zcf4gz.js.map +0 -36
- package/dist/cli/chunk-4ejxbzee.js +0 -39
- package/dist/cli/chunk-4ejxbzee.js.map +0 -10
- package/dist/cli/chunk-4h8z9vv5.js.map +0 -16
- package/dist/cli/chunk-713mngdq.js.map +0 -14
- package/dist/cli/chunk-9xr4dgee.js.map +0 -10
- package/dist/cli/chunk-amrb6hgh.js +0 -145
- package/dist/cli/chunk-amrb6hgh.js.map +0 -11
- package/dist/cli/chunk-nd1821sk.js.map +0 -10
- package/dist/cli/chunk-rekqz4qg.js.map +0 -10
- package/dist/cli/chunk-ta27feev.js.map +0 -10
- package/dist/cli/chunk-xkz5xbtv.js.map +0 -213
- package/dist/cli/chunk-y5h4r2y0.js.map +0 -10
- package/dist/cli/chunk-zf748avv.js.map +0 -37
- /package/dist/cli/{chunk-9geymqns.js.map → chunk-0w6hs479.js.map} +0 -0
- /package/dist/cli/{chunk-y4ykbark.js.map → chunk-1f882900.js.map} +0 -0
- /package/dist/cli/{chunk-py62fewh.js.map → chunk-53jqw8z0.js.map} +0 -0
- /package/dist/cli/{chunk-5wj2qwn2.js.map → chunk-58bt3hag.js.map} +0 -0
- /package/dist/cli/{chunk-e7vvk5ya.js.map → chunk-5cmts6dv.js.map} +0 -0
- /package/dist/cli/{chunk-g2z9x01v.js.map → chunk-9qzgg6mj.js.map} +0 -0
- /package/dist/cli/{chunk-q3wxxq39.js.map → chunk-a57yh0h9.js.map} +0 -0
- /package/dist/cli/{chunk-2f3ndkpw.js.map → chunk-desvm72z.js.map} +0 -0
- /package/dist/cli/{chunk-gp84za0h.js.map → chunk-dhwtqnnd.js.map} +0 -0
- /package/dist/cli/{chunk-2f7n4qzt.js.map → chunk-dqhg54mr.js.map} +0 -0
- /package/dist/cli/{chunk-zhvd6adm.js.map → chunk-gfc5fqvf.js.map} +0 -0
- /package/dist/cli/{chunk-p804f3pe.js.map → chunk-hvcxng5f.js.map} +0 -0
- /package/dist/cli/{chunk-1n228xqb.js.map → chunk-kdpzw38a.js.map} +0 -0
- /package/dist/cli/{chunk-6bm0xpgd.js.map → chunk-s0bdnc7v.js.map} +0 -0
- /package/dist/cli/{chunk-cmtcnh9f.js.map → chunk-tfycddbt.js.map} +0 -0
- /package/dist/cli/{chunk-ntv4qrt7.js.map → chunk-y4msrwm9.js.map} +0 -0
- /package/dist/cli/{chunk-yj17vs39.js.map → chunk-ydp3065p.js.map} +0 -0
package/src/eval/report.ts
CHANGED
|
@@ -10,27 +10,40 @@ import { duration, money, seconds } from "../cli/report-format.ts";
|
|
|
10
10
|
import { countBySeverity } from "../core/diagnostics.ts";
|
|
11
11
|
import type { EvalResult, QuestionResult, QuestionStatus } from "./run.ts";
|
|
12
12
|
|
|
13
|
+
/** How a question reads in the report: its status, or `warn` for a warned miss. */
|
|
14
|
+
type ShownStatus = QuestionStatus | "warn";
|
|
15
|
+
|
|
13
16
|
const GLYPH = {
|
|
14
17
|
error: "!",
|
|
15
18
|
fail: "✖",
|
|
16
19
|
pass: "✔",
|
|
17
20
|
skip: "⊘",
|
|
18
|
-
|
|
21
|
+
warn: "⚠",
|
|
22
|
+
} satisfies Record<ShownStatus, string>;
|
|
19
23
|
|
|
20
24
|
const STATUS_COLOR = {
|
|
21
25
|
error: colors.yellow,
|
|
22
26
|
fail: colors.red,
|
|
23
27
|
pass: colors.green,
|
|
24
28
|
skip: colors.dim,
|
|
25
|
-
|
|
29
|
+
warn: colors.yellow,
|
|
30
|
+
} satisfies Record<ShownStatus, ColorFunction>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A failed `severity: warning` question: its miss never counts against the
|
|
34
|
+
* gate, so the report shows and counts it as a warning, not a failure.
|
|
35
|
+
*/
|
|
36
|
+
const warned = (result: QuestionResult): boolean =>
|
|
37
|
+
result.status === "fail" && result.severity === "warning";
|
|
26
38
|
|
|
27
39
|
/** Longest id gets the room; everything shorter aligns to it. */
|
|
28
40
|
const ID_PAD = 28;
|
|
29
41
|
|
|
30
42
|
/** One question's progress/report line: glyph, id, status, score, time, cost. */
|
|
31
43
|
export const questionLine = (result: QuestionResult): string => {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
44
|
+
const shown: ShownStatus = warned(result) ? "warn" : result.status;
|
|
45
|
+
const color = STATUS_COLOR[shown];
|
|
46
|
+
const glyph = color(GLYPH[shown]);
|
|
34
47
|
const id = result.id.padEnd(ID_PAD);
|
|
35
48
|
if (result.status === "skip") {
|
|
36
49
|
return ` ${glyph} ${id} ${colors.dim("skipped")}`;
|
|
@@ -38,7 +51,7 @@ export const questionLine = (result: QuestionResult): string => {
|
|
|
38
51
|
const score = result.score === undefined ? "" : result.score.toFixed(2);
|
|
39
52
|
const cost = money(result.costUsd);
|
|
40
53
|
const cells = [
|
|
41
|
-
color(
|
|
54
|
+
color(shown),
|
|
42
55
|
score,
|
|
43
56
|
colors.dim(seconds(result.durationMs)),
|
|
44
57
|
cost === "" ? "" : colors.dim(cost),
|
|
@@ -75,9 +88,12 @@ export const questionDetails = (
|
|
|
75
88
|
/** The one-line totals: `9 passed · 2 failed · 1 skipped · 1m 42s · $0.71`. */
|
|
76
89
|
export const summaryLine = (result: EvalResult): string => {
|
|
77
90
|
const { counts } = result;
|
|
91
|
+
const warnings = result.results.filter(warned).length;
|
|
92
|
+
const failures = counts.fail - warnings;
|
|
78
93
|
const parts = [
|
|
79
94
|
`${counts.pass} passed`,
|
|
80
|
-
|
|
95
|
+
failures > 0 ? `${failures} failed` : "",
|
|
96
|
+
warnings > 0 ? `${warnings} warned` : "",
|
|
81
97
|
counts.error > 0 ? `${counts.error} errored` : "",
|
|
82
98
|
counts.skip > 0 ? `${counts.skip} skipped` : "",
|
|
83
99
|
duration(result.durationMs),
|
|
@@ -98,10 +114,11 @@ export const startLine = (id: string, index: number, total: number): string =>
|
|
|
98
114
|
* `fix:` pointers for failed questions, naming the file that resolves each. A
|
|
99
115
|
* question whose agent run errored gets a `run failed:` line pointing at the
|
|
100
116
|
* question instead: the run never graded the docs, so there's no page to fix.
|
|
117
|
+
* A `severity: warning` question's miss is a warning line instead.
|
|
101
118
|
*/
|
|
102
119
|
export const fixLines = (result: EvalResult, root: string): string[] =>
|
|
103
120
|
result.diagnostics
|
|
104
|
-
.filter((diagnostic) => diagnostic.
|
|
121
|
+
.filter((diagnostic) => diagnostic.severity !== "warning")
|
|
105
122
|
.map((finding) => {
|
|
106
123
|
const site = finding.file
|
|
107
124
|
? `${relative(root, finding.file)}${finding.line ? `:${finding.line}` : ""}`
|
|
@@ -113,10 +130,13 @@ export const fixLines = (result: EvalResult, root: string): string[] =>
|
|
|
113
130
|
return ` ${label} ${site} ${colors.dim(finding.message)}`;
|
|
114
131
|
});
|
|
115
132
|
|
|
116
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Dim warnings: route hints that no longer match a page, and the misses of
|
|
135
|
+
* `severity: warning` questions.
|
|
136
|
+
*/
|
|
117
137
|
export const warningLines = (result: EvalResult, root: string): string[] =>
|
|
118
138
|
result.diagnostics
|
|
119
|
-
.filter((diagnostic) => diagnostic.
|
|
139
|
+
.filter((diagnostic) => diagnostic.severity === "warning")
|
|
120
140
|
.map((finding) => {
|
|
121
141
|
const site = finding.file
|
|
122
142
|
? ` ${relative(root, finding.file)}${finding.line ? `:${finding.line}` : ""}`
|
|
@@ -140,10 +160,11 @@ export const formatEvalReport = (
|
|
|
140
160
|
}
|
|
141
161
|
lines.push("");
|
|
142
162
|
|
|
143
|
-
// The finding tells the author which file fixes which failure
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
163
|
+
// The finding tells the author which file fixes which failure, warnings
|
|
164
|
+
// first, as the command prints them.
|
|
165
|
+
const findings = [...warningLines(result, root), ...fixLines(result, root)];
|
|
166
|
+
lines.push(...findings);
|
|
167
|
+
if (findings.length > 0) {
|
|
147
168
|
lines.push("");
|
|
148
169
|
}
|
|
149
170
|
|
package/src/eval/run.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface QuestionResult {
|
|
|
40
40
|
question: string;
|
|
41
41
|
routes: string[];
|
|
42
42
|
score?: number;
|
|
43
|
+
/** The question's severity: a `warning` miss is reported as a warning. */
|
|
44
|
+
severity?: EvalQuestion["severity"];
|
|
43
45
|
status: QuestionStatus;
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -99,6 +101,7 @@ const errored = (
|
|
|
99
101
|
missing: [],
|
|
100
102
|
question: question.question,
|
|
101
103
|
routes: question.routes,
|
|
104
|
+
severity: question.severity,
|
|
102
105
|
status: "error",
|
|
103
106
|
});
|
|
104
107
|
|
|
@@ -177,6 +180,7 @@ const runQuestion = async (
|
|
|
177
180
|
question: question.question,
|
|
178
181
|
routes: question.routes,
|
|
179
182
|
score: verdict.score,
|
|
183
|
+
severity: question.severity,
|
|
180
184
|
status: verdict.pass ? "pass" : "fail",
|
|
181
185
|
};
|
|
182
186
|
};
|
|
@@ -250,6 +254,7 @@ export const runEval = async (options: EvalRunOptions): Promise<EvalResult> => {
|
|
|
250
254
|
missing: [],
|
|
251
255
|
question: question.question,
|
|
252
256
|
routes: question.routes,
|
|
257
|
+
severity: question.severity,
|
|
253
258
|
status: "skip",
|
|
254
259
|
});
|
|
255
260
|
continue;
|
|
@@ -24,6 +24,21 @@ interface MdastUrlContext {
|
|
|
24
24
|
const isUrl = (url: string | null | undefined): url is string =>
|
|
25
25
|
typeof url === "string";
|
|
26
26
|
|
|
27
|
+
/** Record `based(url)` on the node when its root-relative URL changes. */
|
|
28
|
+
const rebase = (
|
|
29
|
+
node: UrlNode,
|
|
30
|
+
ctx: MdastUrlContext,
|
|
31
|
+
based: (url: string) => string
|
|
32
|
+
): void => {
|
|
33
|
+
const { url } = node;
|
|
34
|
+
if (isUrl(url) && isInternalPath(url)) {
|
|
35
|
+
const next = based(url);
|
|
36
|
+
if (next !== url) {
|
|
37
|
+
ctx.setProperty(node, "url", next);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
27
42
|
export interface BaseLinksPluginOptions {
|
|
28
43
|
/**
|
|
29
44
|
* The `blume:data` JSON file, for an ejected app: with no CLI in the process
|
|
@@ -72,20 +87,6 @@ export const baseLinksPlugin = (
|
|
|
72
87
|
const servesPage = (route: string): boolean =>
|
|
73
88
|
servesRoute(servedRoutes(), route);
|
|
74
89
|
|
|
75
|
-
/** Record `based(url)` on the node when its root-relative URL changes. */
|
|
76
|
-
const rebase = (
|
|
77
|
-
node: UrlNode,
|
|
78
|
-
ctx: MdastUrlContext,
|
|
79
|
-
based: (url: string) => string
|
|
80
|
-
): void => {
|
|
81
|
-
const { url } = node;
|
|
82
|
-
if (isUrl(url) && isInternalPath(url)) {
|
|
83
|
-
const next = based(url);
|
|
84
|
-
if (next !== url) {
|
|
85
|
-
ctx.setProperty(node, "url", next);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
90
|
const rebaseLink = (node: UrlNode, ctx: MdastUrlContext): void =>
|
|
90
91
|
rebase(node, ctx, (url) =>
|
|
91
92
|
withAuthoredBasePath(deployBase, basePath, url, servesPage)
|
|
@@ -205,6 +205,9 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
|
205
205
|
return words.join(" ");
|
|
206
206
|
};
|
|
207
207
|
|
|
208
|
+
const normalizeCommand = (command: string): string =>
|
|
209
|
+
command.replaceAll(WHITESPACE_RUN, " ").trim();
|
|
210
|
+
|
|
208
211
|
/**
|
|
209
212
|
* Convert an install command into the equivalent for every supported package
|
|
210
213
|
* manager. Accepts a bare package list (`react`) or a full command
|
|
@@ -212,14 +215,12 @@ const buildCommand = (manager: PackageManager, intent: Intent): string => {
|
|
|
212
215
|
*/
|
|
213
216
|
export const toPackageCommands = (input: string) => {
|
|
214
217
|
const intent = parseIntent(input);
|
|
215
|
-
const normalize = (command: string): string =>
|
|
216
|
-
command.replaceAll(WHITESPACE_RUN, " ").trim();
|
|
217
218
|
return {
|
|
218
|
-
aube:
|
|
219
|
-
bun:
|
|
220
|
-
npm:
|
|
221
|
-
nub:
|
|
222
|
-
pnpm:
|
|
223
|
-
yarn:
|
|
219
|
+
aube: normalizeCommand(buildCommand("aube", intent)),
|
|
220
|
+
bun: normalizeCommand(buildCommand("bun", intent)),
|
|
221
|
+
npm: normalizeCommand(buildCommand("npm", intent)),
|
|
222
|
+
nub: normalizeCommand(buildCommand("nub", intent)),
|
|
223
|
+
pnpm: normalizeCommand(buildCommand("pnpm", intent)),
|
|
224
|
+
yarn: normalizeCommand(buildCommand("yarn", intent)),
|
|
224
225
|
} satisfies Record<PackageManager, string>;
|
|
225
226
|
};
|
|
@@ -266,8 +266,9 @@ export const relativeLinksPlugin = (
|
|
|
266
266
|
|
|
267
267
|
// A component's string `href` (`<Card href="./install">`) is the same kind
|
|
268
268
|
// of link, and `blume validate` reads it as one. Lowercase elements are
|
|
269
|
-
// raw HTML in a `.md` page, which
|
|
270
|
-
//
|
|
269
|
+
// raw HTML in a `.md` page, which isn't rewritten, so only components are
|
|
270
|
+
// rewritten (validate reads a raw `<a href>` as the browser does); an
|
|
271
|
+
// expression-valued `href={…}` is left alone.
|
|
271
272
|
const rewriteHref = (node: JsxNode, ctx: RelativeLinksContext): void => {
|
|
272
273
|
if (!node.name || !COMPONENT_NAME.test(node.name)) {
|
|
273
274
|
return;
|
package/src/narration/build.ts
CHANGED
|
@@ -10,15 +10,24 @@ import {
|
|
|
10
10
|
writeFile,
|
|
11
11
|
} from "node:fs/promises";
|
|
12
12
|
|
|
13
|
+
import type * as OpenAISdk from "@ai-sdk/openai";
|
|
14
|
+
import type { OpenAIProvider } from "@ai-sdk/openai";
|
|
13
15
|
import { createGateway, generateSpeech } from "ai";
|
|
16
|
+
import type { SpeechModel } from "ai";
|
|
14
17
|
import { HTMLElement, parse, TextNode } from "node-html-parser";
|
|
15
18
|
import type { Node } from "node-html-parser";
|
|
16
19
|
import { dirname, join } from "pathe";
|
|
17
20
|
|
|
18
21
|
import { normalizeBasePath } from "../core/base-path.ts";
|
|
22
|
+
import { nodeRequire } from "../core/node-require.ts";
|
|
19
23
|
import type { BlumeProject } from "../core/project-graph.ts";
|
|
20
24
|
import type { ProjectContext } from "../core/types.ts";
|
|
21
|
-
import
|
|
25
|
+
import { hasCredentials, OPENAI_SPEECH_DEP } from "./provider.ts";
|
|
26
|
+
import type {
|
|
27
|
+
GatewayNarrationProvider,
|
|
28
|
+
NarrationProvider,
|
|
29
|
+
OpenAINarrationProvider,
|
|
30
|
+
} from "./provider.ts";
|
|
22
31
|
import {
|
|
23
32
|
extractNarration,
|
|
24
33
|
isNarratable,
|
|
@@ -37,10 +46,11 @@ import type {
|
|
|
37
46
|
* carries the player exactly as the browser will (same walker, same blocks),
|
|
38
47
|
* split it into sentences, and give each sentence a clip from the configured
|
|
39
48
|
* speech model. Clips are cached by what decides their sound (text, language,
|
|
40
|
-
* model, voice, instructions, provider options), so a rebuild only
|
|
41
|
-
* sentences that changed, and one sentence shared by many pages is
|
|
42
|
-
* once. Each page gets a manifest listing its clips; the player
|
|
43
|
-
* order and falls back to browser voices wherever a manifest is
|
|
49
|
+
* model, voice, instructions, provider options, endpoint), so a rebuild only
|
|
50
|
+
* pays for sentences that changed, and one sentence shared by many pages is
|
|
51
|
+
* generated once. Each page gets a manifest listing its clips; the player
|
|
52
|
+
* plays them in order and falls back to browser voices wherever a manifest is
|
|
53
|
+
* missing.
|
|
44
54
|
*/
|
|
45
55
|
|
|
46
56
|
/** How the build logs: the integration's Astro logger. */
|
|
@@ -54,7 +64,10 @@ export type Speak = (text: string, lang: string) => Promise<Uint8Array>;
|
|
|
54
64
|
|
|
55
65
|
/** What a narration build did, for the log line and tests. */
|
|
56
66
|
export interface NarrationBuildResult {
|
|
57
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Clips that failed to generate, or that a missing key left ungenerated;
|
|
69
|
+
* their pages get no manifest.
|
|
70
|
+
*/
|
|
58
71
|
failed: number;
|
|
59
72
|
/** Clips generated by the speech model on this build. */
|
|
60
73
|
generated: number;
|
|
@@ -67,6 +80,13 @@ export interface NarrationBuildResult {
|
|
|
67
80
|
// Bump to regenerate every clip: part of every cache key.
|
|
68
81
|
const CLIP_FORMAT = 1;
|
|
69
82
|
|
|
83
|
+
// Where `openai()` speaks with neither a `baseUrl` nor `OPENAI_BASE_URL`.
|
|
84
|
+
const OPENAI_API_URL = "https://api.openai.com/v1";
|
|
85
|
+
|
|
86
|
+
// What `generateSpeech` reports for bytes it recognized as MP3. Bytes it can't
|
|
87
|
+
// place at all (a web page, raw PCM) default to `audio/mp3` instead.
|
|
88
|
+
const MP3_MEDIA_TYPE = "audio/mpeg";
|
|
89
|
+
|
|
70
90
|
// Parallel speech requests. Low enough to stay inside provider rate limits on
|
|
71
91
|
// a first build of a large site, where every sentence is new.
|
|
72
92
|
const CONCURRENCY = 4;
|
|
@@ -144,23 +164,52 @@ export const readPlayerPage = (html: string): PlayerPage | null => {
|
|
|
144
164
|
const speechLanguage = (lang: string): string =>
|
|
145
165
|
lang.toLowerCase().replaceAll("_", "-").split("-")[0] ?? lang;
|
|
146
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The server `openai()` speaks to: its `baseUrl`, else `OPENAI_BASE_URL` as
|
|
169
|
+
* OpenAI's SDKs read it, else OpenAI, without a trailing slash, which the SDK
|
|
170
|
+
* drops too. Resolved here rather than left to the SDK, so the clip key names
|
|
171
|
+
* the server the clips come from.
|
|
172
|
+
*/
|
|
173
|
+
const openaiEndpoint = (provider: OpenAINarrationProvider): string =>
|
|
174
|
+
(
|
|
175
|
+
provider.options.baseUrl ||
|
|
176
|
+
process.env.OPENAI_BASE_URL ||
|
|
177
|
+
OPENAI_API_URL
|
|
178
|
+
).replace(/\/+$/u, "");
|
|
179
|
+
|
|
180
|
+
/** The server a provider's clips come from: none named for `gateway()`. */
|
|
181
|
+
const speakerEndpoint = (provider: NarrationProvider): string | undefined =>
|
|
182
|
+
provider.kind === "openai" ? openaiEndpoint(provider) : undefined;
|
|
183
|
+
|
|
147
184
|
/**
|
|
148
185
|
* A clip's cache key: a digest of everything that decides how it sounds. The
|
|
149
186
|
* Blume version is deliberately left out, unlike the OG card's: an upgrade
|
|
150
|
-
* shouldn't pay to regenerate audio that would sound the same.
|
|
187
|
+
* shouldn't pay to regenerate audio that would sound the same. A build
|
|
188
|
+
* resolves `endpoint` once and passes it in, rather than reading the
|
|
189
|
+
* environment for every sentence.
|
|
151
190
|
*/
|
|
152
191
|
export const clipKey = (
|
|
153
192
|
provider: NarrationProvider,
|
|
154
193
|
lang: string,
|
|
155
|
-
text: string
|
|
194
|
+
text: string,
|
|
195
|
+
endpoint = speakerEndpoint(provider)
|
|
156
196
|
): string => {
|
|
157
197
|
const { instructions, model, providerOptions, voice } = provider.options;
|
|
198
|
+
const gateway = provider.kind === "gateway";
|
|
158
199
|
return createHash("sha256")
|
|
159
200
|
.update(
|
|
160
201
|
JSON.stringify({
|
|
202
|
+
// Which server speaks: none for `gateway()`, which JSON drops, so its
|
|
203
|
+
// clips keep the keys they had before narration took other providers.
|
|
204
|
+
endpoint: gateway
|
|
205
|
+
? undefined
|
|
206
|
+
: { baseUrl: endpoint, kind: provider.kind },
|
|
161
207
|
format: CLIP_FORMAT,
|
|
162
208
|
instructions,
|
|
163
|
-
|
|
209
|
+
// Only the gateway is sent a language (`openaiSpeaker` sends none), so
|
|
210
|
+
// an `openai()` sentence sounds the same in every locale and is
|
|
211
|
+
// generated once.
|
|
212
|
+
lang: gateway ? speechLanguage(lang) : undefined,
|
|
164
213
|
model,
|
|
165
214
|
providerOptions,
|
|
166
215
|
text,
|
|
@@ -172,7 +221,7 @@ export const clipKey = (
|
|
|
172
221
|
};
|
|
173
222
|
|
|
174
223
|
/** Speak through the Vercel AI Gateway with the provider's model and voice. */
|
|
175
|
-
export const gatewaySpeaker = (provider:
|
|
224
|
+
export const gatewaySpeaker = (provider: GatewayNarrationProvider): Speak => {
|
|
176
225
|
const { apiKeyEnv, headers, instructions, model, providerOptions, voice } =
|
|
177
226
|
provider.options;
|
|
178
227
|
const gateway = createGateway({ apiKey: process.env[apiKeyEnv], headers });
|
|
@@ -190,6 +239,104 @@ export const gatewaySpeaker = (provider: NarrationProvider): Speak => {
|
|
|
190
239
|
};
|
|
191
240
|
};
|
|
192
241
|
|
|
242
|
+
/**
|
|
243
|
+
* `fetch` for a keyless request: `createOpenAI` always sends the key as a
|
|
244
|
+
* bearer token, so an empty one would arrive as a bare `Bearer` that servers
|
|
245
|
+
* and proxies checking auth can reject. An `Authorization` from `headers`
|
|
246
|
+
* stays.
|
|
247
|
+
*/
|
|
248
|
+
const withoutEmptyBearer = async (
|
|
249
|
+
input: Parameters<typeof fetch>[0],
|
|
250
|
+
init?: RequestInit
|
|
251
|
+
): Promise<Response> => {
|
|
252
|
+
const headers = new Headers(init?.headers);
|
|
253
|
+
if (headers.get("authorization") === "Bearer") {
|
|
254
|
+
headers.delete("authorization");
|
|
255
|
+
}
|
|
256
|
+
return await fetch(input, { ...init, headers });
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Speak through OpenAI's speech API, or with `baseUrl` through any server
|
|
261
|
+
* that serves it (`POST /audio/speech`): Kokoro-FastAPI, Speaches, LiteLLM.
|
|
262
|
+
* That API takes no language, so none is sent and the model or voice
|
|
263
|
+
* decides it. Without a key, no bearer token is sent (an `Authorization` in
|
|
264
|
+
* `headers` still is). A build resolves `baseURL` once and passes it in.
|
|
265
|
+
*/
|
|
266
|
+
export const openaiSpeaker = (
|
|
267
|
+
provider: OpenAINarrationProvider,
|
|
268
|
+
baseURL = openaiEndpoint(provider)
|
|
269
|
+
): Speak => {
|
|
270
|
+
const { apiKeyEnv, headers, instructions, model, providerOptions, voice } =
|
|
271
|
+
provider.options;
|
|
272
|
+
// Made on the first clip rather than up front, so what can go wrong here
|
|
273
|
+
// fails that clip and the build carries on with browser voices: an ejected
|
|
274
|
+
// app's `astro build` runs no dependency preflight, and `OPENAI_BASE_URL`
|
|
275
|
+
// isn't validated the way `baseUrl` is.
|
|
276
|
+
const connect = (): OpenAIProvider => {
|
|
277
|
+
if (hasCredentials(baseURL)) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"OPENAI_BASE_URL has credentials in it, which fetch won't send: name the env var holding the key in apiKeyEnv instead"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
// `require`, not `import()`: this runs in `astro:build:done` (see
|
|
283
|
+
// `core/node-require.ts`).
|
|
284
|
+
const { createOpenAI }: typeof OpenAISdk = nodeRequire(OPENAI_SPEECH_DEP);
|
|
285
|
+
const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;
|
|
286
|
+
return createOpenAI({
|
|
287
|
+
// Never undefined: the SDK would fall back to OPENAI_API_KEY and send
|
|
288
|
+
// that to a custom endpoint that names no key.
|
|
289
|
+
apiKey: apiKey ?? "",
|
|
290
|
+
baseURL,
|
|
291
|
+
// SAFETY: the SDK only calls it, so Bun's `fetch.preconnect` (which its
|
|
292
|
+
// `typeof fetch` adds) is never read.
|
|
293
|
+
fetch: apiKey ? undefined : (withoutEmptyBearer as typeof fetch),
|
|
294
|
+
headers,
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
let openai: OpenAIProvider | undefined;
|
|
298
|
+
return async (text) => {
|
|
299
|
+
openai ??= connect();
|
|
300
|
+
const { audio, responses } = await generateSpeech({
|
|
301
|
+
instructions,
|
|
302
|
+
// SAFETY: both sides are the same `SpeechModelV4` spec. `ai` and
|
|
303
|
+
// `@ai-sdk/openai` each pin their own `@ai-sdk/provider`, and the type
|
|
304
|
+
// only differs between those copies, not in what `generateSpeech` calls.
|
|
305
|
+
model: openai.speech(model) as SpeechModel,
|
|
306
|
+
outputFormat: "mp3",
|
|
307
|
+
providerOptions,
|
|
308
|
+
text,
|
|
309
|
+
voice,
|
|
310
|
+
});
|
|
311
|
+
// The SDK takes any 2xx body as audio, and the clip is cached and served
|
|
312
|
+
// as an MP3 whatever it holds, so a page a redirect led to (an SSO
|
|
313
|
+
// proxy's sign-in, say) or WAV from a server that ignores
|
|
314
|
+
// `response_format` would be reused on every build after: fail the clip
|
|
315
|
+
// unless its bytes are MP3.
|
|
316
|
+
if (audio.mediaType !== MP3_MEDIA_TYPE) {
|
|
317
|
+
const type = responses[0]?.headers?.["content-type"] ?? "no content type";
|
|
318
|
+
throw new Error(`The speech server answered with ${type}, not MP3 audio`);
|
|
319
|
+
}
|
|
320
|
+
return audio.uint8Array;
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The env var a build needs to generate with `provider` and doesn't have, or
|
|
326
|
+
* `null` when it can go ahead: the key is set, Vercel's OIDC token stands in
|
|
327
|
+
* for the gateway's, or the provider names no key (`openai()` with a
|
|
328
|
+
* `baseUrl` and no `apiKeyEnv`).
|
|
329
|
+
*/
|
|
330
|
+
export const missingKey = (provider: NarrationProvider): string | null => {
|
|
331
|
+
const env = provider.options.apiKeyEnv;
|
|
332
|
+
if (env === undefined || process.env[env]) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return provider.kind === "gateway" && process.env.VERCEL_OIDC_TOKEN
|
|
336
|
+
? null
|
|
337
|
+
: env;
|
|
338
|
+
};
|
|
339
|
+
|
|
193
340
|
/** Every `.html` file under `dir`. */
|
|
194
341
|
const listHtml = async (dir: string): Promise<string[]> => {
|
|
195
342
|
const entries = await readdir(dir, { recursive: true, withFileTypes: true });
|
|
@@ -274,35 +421,23 @@ const manifestFor = (plan: PagePlan): NarrationManifest => ({
|
|
|
274
421
|
version: 1,
|
|
275
422
|
});
|
|
276
423
|
|
|
424
|
+
/** What a build narrates: each page's plan, and every clip by its key. */
|
|
425
|
+
interface SitePlan {
|
|
426
|
+
/** Where the clips are published, or `null` with no narratable page. */
|
|
427
|
+
audioDir: string | null;
|
|
428
|
+
plans: PagePlan[];
|
|
429
|
+
texts: Map<string, { lang: string; text: string }>;
|
|
430
|
+
}
|
|
431
|
+
|
|
277
432
|
/**
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* and warns instead of generating when the provider's key is missing (the
|
|
281
|
-
* pages then read with browser voices). `speak` replaces the gateway call.
|
|
433
|
+
* Read every built page that carries the player and plan its narration,
|
|
434
|
+
* keying each sentence's clip with `keyFor`.
|
|
282
435
|
*/
|
|
283
|
-
|
|
284
|
-
project: BlumeProject,
|
|
436
|
+
const planSite = async (
|
|
285
437
|
distDir: string,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
): Promise<
|
|
289
|
-
const { narration } = project.config;
|
|
290
|
-
const { provider } = narration;
|
|
291
|
-
if (!(narration.enabled && provider)) {
|
|
292
|
-
return null;
|
|
293
|
-
}
|
|
294
|
-
const env = provider.options.apiKeyEnv;
|
|
295
|
-
if (!(speak || process.env[env] || process.env.VERCEL_OIDC_TOKEN)) {
|
|
296
|
-
logger.warn(
|
|
297
|
-
`Narration audio was not generated: ${env} is not set, so pages will be read with browser voices.`
|
|
298
|
-
);
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
const say = speak ?? gatewaySpeaker(provider);
|
|
302
|
-
const base = normalizeBasePath(project.config.deployment.options.base);
|
|
303
|
-
|
|
304
|
-
// Read every page first, so the log can say what generation will cost
|
|
305
|
-
// before any of it is spent.
|
|
438
|
+
base: string,
|
|
439
|
+
keyFor: (lang: string, text: string) => string
|
|
440
|
+
): Promise<SitePlan> => {
|
|
306
441
|
const plans: PagePlan[] = [];
|
|
307
442
|
const texts = new Map<string, { lang: string; text: string }>();
|
|
308
443
|
let audioDir: string | null = null;
|
|
@@ -322,7 +457,7 @@ export const buildNarration = async (
|
|
|
322
457
|
audioDir ??= distPath(distDir, base, page.audioBase);
|
|
323
458
|
const segments = segmentNarration(extract, page.lang);
|
|
324
459
|
const keys = segments.map((segment) => {
|
|
325
|
-
const key =
|
|
460
|
+
const key = keyFor(page.lang, segment.text);
|
|
326
461
|
texts.set(key, { lang: page.lang, text: segment.text });
|
|
327
462
|
return key;
|
|
328
463
|
});
|
|
@@ -333,6 +468,37 @@ export const buildNarration = async (
|
|
|
333
468
|
segments,
|
|
334
469
|
});
|
|
335
470
|
}
|
|
471
|
+
return { audioDir, plans, texts };
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Generate the narration for a built site in `distDir`: a clip per sentence
|
|
476
|
+
* and a manifest per page. Does nothing unless `narration.provider` is set.
|
|
477
|
+
* When new clips are needed and the provider's key is missing, it warns and
|
|
478
|
+
* generates nothing, so only pages whose clips are all cached get audio (the
|
|
479
|
+
* rest read with browser voices). `speak` replaces the provider call.
|
|
480
|
+
*/
|
|
481
|
+
export const buildNarration = async (
|
|
482
|
+
project: BlumeProject,
|
|
483
|
+
distDir: string,
|
|
484
|
+
logger: NarrationLogger,
|
|
485
|
+
speak?: Speak
|
|
486
|
+
): Promise<NarrationBuildResult | null> => {
|
|
487
|
+
const { narration } = project.config;
|
|
488
|
+
const { provider } = narration;
|
|
489
|
+
if (!(narration.enabled && provider)) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
const endpoint = speakerEndpoint(provider);
|
|
493
|
+
const base = normalizeBasePath(project.config.deployment.options.base);
|
|
494
|
+
|
|
495
|
+
// Read every page first, so the log can say what generation will cost
|
|
496
|
+
// before any of it is spent.
|
|
497
|
+
const { audioDir, plans, texts } = await planSite(
|
|
498
|
+
distDir,
|
|
499
|
+
base,
|
|
500
|
+
(lang, text) => clipKey(provider, lang, text, endpoint)
|
|
501
|
+
);
|
|
336
502
|
if (!audioDir || plans.length === 0) {
|
|
337
503
|
return { failed: 0, generated: 0, pages: 0, reused: 0 };
|
|
338
504
|
}
|
|
@@ -341,7 +507,14 @@ export const buildNarration = async (
|
|
|
341
507
|
const cached = (key: string) => join(cacheDir, `${key}.mp3`);
|
|
342
508
|
const missing = [...texts.keys()].filter((key) => !existsSync(cached(key)));
|
|
343
509
|
const reused = texts.size - missing.length;
|
|
344
|
-
|
|
510
|
+
// Checked only once something needs generating, so a build without the key
|
|
511
|
+
// (a fork's pull request, say) still ships every clip the cache restored.
|
|
512
|
+
const env = speak || missing.length === 0 ? null : missingKey(provider);
|
|
513
|
+
if (env) {
|
|
514
|
+
logger.warn(
|
|
515
|
+
`Narration audio was not generated: ${env} is not set, so pages missing clips will be read with browser voices.`
|
|
516
|
+
);
|
|
517
|
+
} else if (missing.length > 0) {
|
|
345
518
|
const chars = missing.reduce(
|
|
346
519
|
(total, key) => total + (texts.get(key)?.text.length ?? 0),
|
|
347
520
|
0
|
|
@@ -350,13 +523,18 @@ export const buildNarration = async (
|
|
|
350
523
|
`Generating narration: ${missing.length} new clip(s), ${chars.toLocaleString("en-US")} characters, with ${provider.options.model}`
|
|
351
524
|
);
|
|
352
525
|
}
|
|
526
|
+
const say =
|
|
527
|
+
speak ??
|
|
528
|
+
(provider.kind === "gateway"
|
|
529
|
+
? gatewaySpeaker(provider)
|
|
530
|
+
: openaiSpeaker(provider, endpoint));
|
|
353
531
|
|
|
354
532
|
// The AI SDK already retries a transient failure, so one that gets through
|
|
355
533
|
// (a bad key, an unknown model, a spent quota) would fail every clip after
|
|
356
534
|
// it too: stop generating at the first, and keep what's already cached.
|
|
357
|
-
const failures = new Set
|
|
535
|
+
const failures = new Set(env ? missing : []);
|
|
358
536
|
let firstError: string | null = null;
|
|
359
|
-
await eachLimited(missing, CONCURRENCY, async (key) => {
|
|
537
|
+
await eachLimited(env ? [] : missing, CONCURRENCY, async (key) => {
|
|
360
538
|
const entry = texts.get(key);
|
|
361
539
|
if (!entry || firstError !== null) {
|
|
362
540
|
failures.add(key);
|