@nodaro/shared 1.4.1
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 +202 -0
- package/README.md +32 -0
- package/dist/index.cjs +12506 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +12269 -0
- package/dist/index.d.ts +12269 -0
- package/dist/index.js +11831 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/ai-avatar-pricing.test.ts +321 -0
- package/src/__tests__/apply-video-negative-prompt.test.ts +76 -0
- package/src/__tests__/attach-columns.test.ts +28 -0
- package/src/__tests__/audio-crossfade-curves.test.ts +33 -0
- package/src/__tests__/build-model-menu.test.ts +58 -0
- package/src/__tests__/character-aspect-defaults.test.ts +142 -0
- package/src/__tests__/character-mention-slug.test.ts +199 -0
- package/src/__tests__/character-reference-photo-kinds.test.ts +32 -0
- package/src/__tests__/character-usage-mode.test.ts +147 -0
- package/src/__tests__/character-variant-assets.test.ts +142 -0
- package/src/__tests__/character-voice.test.ts +130 -0
- package/src/__tests__/cinematic-avatar-pricing.test.ts +119 -0
- package/src/__tests__/combine-transitions.test.ts +137 -0
- package/src/__tests__/component-types.test.ts +87 -0
- package/src/__tests__/condition-variables.test.ts +98 -0
- package/src/__tests__/credit-identifiers.test.ts +811 -0
- package/src/__tests__/default-video-provider.test.ts +63 -0
- package/src/__tests__/entity-image-handle.test.ts +43 -0
- package/src/__tests__/entity-variants.test.ts +33 -0
- package/src/__tests__/fal-lip-sync-sets.test.ts +31 -0
- package/src/__tests__/featured-entities.test.ts +33 -0
- package/src/__tests__/film-pricing.test.ts +64 -0
- package/src/__tests__/filter-condition.test.ts +285 -0
- package/src/__tests__/freecut-protocol.test.ts +35 -0
- package/src/__tests__/generate-text-items.test.ts +19 -0
- package/src/__tests__/generated-results.test.ts +50 -0
- package/src/__tests__/graph-utils.test.ts +618 -0
- package/src/__tests__/group-aggregation.test.ts +83 -0
- package/src/__tests__/i18n-locale-completeness.test.ts +156 -0
- package/src/__tests__/image-critic-node.test.ts +86 -0
- package/src/__tests__/image-mask-mode.test.ts +16 -0
- package/src/__tests__/image-ref-limit.test.ts +78 -0
- package/src/__tests__/inpaint-region.test.ts +24 -0
- package/src/__tests__/input-field-map-parity.test.ts +30 -0
- package/src/__tests__/json-evaluator.test.ts +289 -0
- package/src/__tests__/json-path.test.ts +64 -0
- package/src/__tests__/lip-sync-pricing.test.ts +142 -0
- package/src/__tests__/list-merge.test.ts +96 -0
- package/src/__tests__/list-sort.test.ts +134 -0
- package/src/__tests__/llm-models.test.ts +445 -0
- package/src/__tests__/location-mention-slug.test.ts +246 -0
- package/src/__tests__/locations-coverage-critic-schema.test.ts +92 -0
- package/src/__tests__/loop-delimiter.test.ts +39 -0
- package/src/__tests__/lottie-slots.test.ts +245 -0
- package/src/__tests__/model-catalog-flux2-resolutions.test.ts +21 -0
- package/src/__tests__/model-constants-location.test.ts +20 -0
- package/src/__tests__/model-constants.test.ts +122 -0
- package/src/__tests__/model-tree.test.ts +86 -0
- package/src/__tests__/no-internal-imports.test.ts +25 -0
- package/src/__tests__/node-default-mappings.test.ts +175 -0
- package/src/__tests__/node-mappable-fields.test.ts +7 -0
- package/src/__tests__/node-preset-export.test.ts +46 -0
- package/src/__tests__/node-preset-extract.test.ts +166 -0
- package/src/__tests__/node-refs-canonical.test.ts +54 -0
- package/src/__tests__/node-refs-combine.test.ts +54 -0
- package/src/__tests__/node-refs.test.ts +187 -0
- package/src/__tests__/node-runtime-keys.test.ts +99 -0
- package/src/__tests__/object-aspect-defaults.test.ts +59 -0
- package/src/__tests__/parameter-node-value.test.ts +137 -0
- package/src/__tests__/pinterest-url.test.ts +35 -0
- package/src/__tests__/pipeline-chat.test.ts +110 -0
- package/src/__tests__/pipeline-validation.test.ts +98 -0
- package/src/__tests__/presentation-utils.test.ts +1137 -0
- package/src/__tests__/producer-types.test.ts +47 -0
- package/src/__tests__/prompt-length-limits.test.ts +156 -0
- package/src/__tests__/provider-directive-defaults.test.ts +46 -0
- package/src/__tests__/reduce-strategy-registry.test.ts +43 -0
- package/src/__tests__/reference-board-templates.test.ts +33 -0
- package/src/__tests__/reference-roles.test.ts +36 -0
- package/src/__tests__/resolve-field-mappings.test.ts +110 -0
- package/src/__tests__/resolve-pipeline-model.test.ts +76 -0
- package/src/__tests__/scene-node-types.test.ts +80 -0
- package/src/__tests__/scraper-actors.test.ts +67 -0
- package/src/__tests__/seedance-2-catalog.test.ts +18 -0
- package/src/__tests__/seedance-2-credit-id.test.ts +23 -0
- package/src/__tests__/seedance-aspects.test.ts +52 -0
- package/src/__tests__/selector.test.ts +1151 -0
- package/src/__tests__/settled-with-limit.test.ts +214 -0
- package/src/__tests__/social-media-specs.test.ts +129 -0
- package/src/__tests__/sub-workflow-validation.test.ts +88 -0
- package/src/__tests__/switchx-pricing.test.ts +28 -0
- package/src/__tests__/to-connected-references.test.ts +153 -0
- package/src/__tests__/utilities.test.ts +541 -0
- package/src/__tests__/video-analysis-pricing.test.ts +58 -0
- package/src/__tests__/video-analysis.test.ts +63 -0
- package/src/__tests__/video-audio-capability.test.ts +204 -0
- package/src/__tests__/video-gen-providers.test.ts +28 -0
- package/src/__tests__/video-mode-aliases.test.ts +150 -0
- package/src/__tests__/video-ref-limits.test.ts +72 -0
- package/src/__tests__/video-utils.test.ts +303 -0
- package/src/__tests__/voice-changer-models.test.ts +33 -0
- package/src/__tests__/workflow-export-strip.test.ts +46 -0
- package/src/ai-avatar-pricing.ts +213 -0
- package/src/ancestor-refs.ts +48 -0
- package/src/animals.ts +231 -0
- package/src/audio-crossfade-curves.ts +73 -0
- package/src/audio-fx-presets.ts +64 -0
- package/src/avatar-payload-validators.ts +214 -0
- package/src/caption-styles.ts +31 -0
- package/src/character-aspect-defaults.ts +102 -0
- package/src/character-facets.ts +37 -0
- package/src/character-mention-slug.ts +166 -0
- package/src/character-usage-mode.ts +109 -0
- package/src/character-variant-assets.ts +121 -0
- package/src/character-voice.ts +151 -0
- package/src/cinematic-avatar-pricing.ts +111 -0
- package/src/clone-utils.ts +46 -0
- package/src/combine-transitions.ts +482 -0
- package/src/community.ts +112 -0
- package/src/component-types.ts +127 -0
- package/src/condition-variables.ts +65 -0
- package/src/credit-estimators/index.ts +15 -0
- package/src/credit-estimators/video-utils.ts +165 -0
- package/src/credit-identifiers.ts +304 -0
- package/src/entity-approval-types.ts +31 -0
- package/src/entity-aspect-defaults.ts +82 -0
- package/src/entity-asset-types.ts +296 -0
- package/src/entity-image-handle.ts +36 -0
- package/src/extra-refs.ts +139 -0
- package/src/featured-entities.ts +136 -0
- package/src/film-pricing.ts +117 -0
- package/src/filter-condition.ts +250 -0
- package/src/flux2-pricing.ts +18 -0
- package/src/freecut-protocol.ts +52 -0
- package/src/furniture.ts +167 -0
- package/src/generate-text-items.ts +15 -0
- package/src/generated-results.ts +63 -0
- package/src/group-aggregation.ts +75 -0
- package/src/hint-graph-types.ts +18 -0
- package/src/i18n/action-fx.ar.ts +89 -0
- package/src/i18n/action-fx.de.ts +89 -0
- package/src/i18n/action-fx.es.ts +89 -0
- package/src/i18n/action-fx.fr.ts +89 -0
- package/src/i18n/action-fx.he.ts +89 -0
- package/src/i18n/action-fx.hi.ts +89 -0
- package/src/i18n/action-fx.ja.ts +89 -0
- package/src/i18n/action-fx.ko.ts +89 -0
- package/src/i18n/action-fx.pt-BR.ts +89 -0
- package/src/i18n/action-fx.ru.ts +89 -0
- package/src/i18n/action-fx.zh-CN.ts +89 -0
- package/src/i18n/aesthetic.ar.ts +170 -0
- package/src/i18n/aesthetic.de.ts +48 -0
- package/src/i18n/aesthetic.es.ts +169 -0
- package/src/i18n/aesthetic.fr.ts +52 -0
- package/src/i18n/aesthetic.he.ts +169 -0
- package/src/i18n/aesthetic.hi.ts +55 -0
- package/src/i18n/aesthetic.ja.ts +165 -0
- package/src/i18n/aesthetic.ko.ts +169 -0
- package/src/i18n/aesthetic.pt-BR.ts +148 -0
- package/src/i18n/aesthetic.ru.ts +155 -0
- package/src/i18n/aesthetic.zh-CN.ts +172 -0
- package/src/i18n/animals.ar.ts +145 -0
- package/src/i18n/animals.de.ts +144 -0
- package/src/i18n/animals.es.ts +158 -0
- package/src/i18n/animals.fr.ts +144 -0
- package/src/i18n/animals.he.ts +155 -0
- package/src/i18n/animals.hi.ts +157 -0
- package/src/i18n/animals.ja.ts +159 -0
- package/src/i18n/animals.ko.ts +159 -0
- package/src/i18n/animals.pt-BR.ts +157 -0
- package/src/i18n/animals.ru.ts +155 -0
- package/src/i18n/animals.zh-CN.ts +155 -0
- package/src/i18n/atmosphere.ar.ts +49 -0
- package/src/i18n/atmosphere.de.ts +47 -0
- package/src/i18n/atmosphere.es.ts +50 -0
- package/src/i18n/atmosphere.fr.ts +47 -0
- package/src/i18n/atmosphere.he.ts +47 -0
- package/src/i18n/atmosphere.hi.ts +49 -0
- package/src/i18n/atmosphere.ja.ts +49 -0
- package/src/i18n/atmosphere.ko.ts +49 -0
- package/src/i18n/atmosphere.pt-BR.ts +49 -0
- package/src/i18n/atmosphere.ru.ts +47 -0
- package/src/i18n/atmosphere.zh-CN.ts +152 -0
- package/src/i18n/backdrop.ar.ts +53 -0
- package/src/i18n/backdrop.de.ts +46 -0
- package/src/i18n/backdrop.es.ts +54 -0
- package/src/i18n/backdrop.fr.ts +52 -0
- package/src/i18n/backdrop.he.ts +51 -0
- package/src/i18n/backdrop.hi.ts +59 -0
- package/src/i18n/backdrop.ja.ts +61 -0
- package/src/i18n/backdrop.ko.ts +59 -0
- package/src/i18n/backdrop.pt-BR.ts +59 -0
- package/src/i18n/backdrop.ru.ts +57 -0
- package/src/i18n/backdrop.zh-CN.ts +57 -0
- package/src/i18n/camera-format.ar.ts +40 -0
- package/src/i18n/camera-format.de.ts +37 -0
- package/src/i18n/camera-format.es.ts +41 -0
- package/src/i18n/camera-format.fr.ts +37 -0
- package/src/i18n/camera-format.he.ts +37 -0
- package/src/i18n/camera-format.hi.ts +42 -0
- package/src/i18n/camera-format.ja.ts +37 -0
- package/src/i18n/camera-format.ko.ts +37 -0
- package/src/i18n/camera-format.pt-BR.ts +46 -0
- package/src/i18n/camera-format.ru.ts +42 -0
- package/src/i18n/camera-format.zh-CN.ts +42 -0
- package/src/i18n/camera-motions.ar.ts +86 -0
- package/src/i18n/camera-motions.de.ts +74 -0
- package/src/i18n/camera-motions.es.ts +86 -0
- package/src/i18n/camera-motions.fr.ts +86 -0
- package/src/i18n/camera-motions.he.ts +86 -0
- package/src/i18n/camera-motions.hi.ts +99 -0
- package/src/i18n/camera-motions.ja.ts +99 -0
- package/src/i18n/camera-motions.ko.ts +99 -0
- package/src/i18n/camera-motions.pt-BR.ts +99 -0
- package/src/i18n/camera-motions.ru.ts +99 -0
- package/src/i18n/camera-motions.zh-CN.ts +99 -0
- package/src/i18n/character-fx.ar.ts +72 -0
- package/src/i18n/character-fx.de.ts +72 -0
- package/src/i18n/character-fx.es.ts +72 -0
- package/src/i18n/character-fx.fr.ts +72 -0
- package/src/i18n/character-fx.he.ts +72 -0
- package/src/i18n/character-fx.hi.ts +72 -0
- package/src/i18n/character-fx.ja.ts +72 -0
- package/src/i18n/character-fx.ko.ts +72 -0
- package/src/i18n/character-fx.pt-BR.ts +72 -0
- package/src/i18n/character-fx.ru.ts +72 -0
- package/src/i18n/character-fx.zh-CN.ts +72 -0
- package/src/i18n/color-look.ar.ts +50 -0
- package/src/i18n/color-look.de.ts +47 -0
- package/src/i18n/color-look.es.ts +50 -0
- package/src/i18n/color-look.fr.ts +50 -0
- package/src/i18n/color-look.he.ts +49 -0
- package/src/i18n/color-look.hi.ts +52 -0
- package/src/i18n/color-look.ja.ts +54 -0
- package/src/i18n/color-look.ko.ts +54 -0
- package/src/i18n/color-look.pt-BR.ts +54 -0
- package/src/i18n/color-look.ru.ts +52 -0
- package/src/i18n/color-look.zh-CN.ts +53 -0
- package/src/i18n/composition-effects.ar.ts +26 -0
- package/src/i18n/composition-effects.de.ts +25 -0
- package/src/i18n/composition-effects.es.ts +27 -0
- package/src/i18n/composition-effects.fr.ts +25 -0
- package/src/i18n/composition-effects.he.ts +25 -0
- package/src/i18n/composition-effects.hi.ts +27 -0
- package/src/i18n/composition-effects.ja.ts +27 -0
- package/src/i18n/composition-effects.ko.ts +27 -0
- package/src/i18n/composition-effects.pt-BR.ts +25 -0
- package/src/i18n/composition-effects.ru.ts +82 -0
- package/src/i18n/composition-effects.zh-CN.ts +25 -0
- package/src/i18n/era.ar.ts +37 -0
- package/src/i18n/era.de.ts +34 -0
- package/src/i18n/era.es.ts +37 -0
- package/src/i18n/era.fr.ts +37 -0
- package/src/i18n/era.he.ts +36 -0
- package/src/i18n/era.hi.ts +39 -0
- package/src/i18n/era.ja.ts +41 -0
- package/src/i18n/era.ko.ts +39 -0
- package/src/i18n/era.pt-BR.ts +41 -0
- package/src/i18n/era.ru.ts +121 -0
- package/src/i18n/era.zh-CN.ts +39 -0
- package/src/i18n/exposure-settings.ar.ts +28 -0
- package/src/i18n/exposure-settings.de.ts +26 -0
- package/src/i18n/exposure-settings.es.ts +28 -0
- package/src/i18n/exposure-settings.fr.ts +29 -0
- package/src/i18n/exposure-settings.he.ts +28 -0
- package/src/i18n/exposure-settings.hi.ts +31 -0
- package/src/i18n/exposure-settings.ja.ts +31 -0
- package/src/i18n/exposure-settings.ko.ts +31 -0
- package/src/i18n/exposure-settings.pt-BR.ts +31 -0
- package/src/i18n/exposure-settings.ru.ts +31 -0
- package/src/i18n/exposure-settings.zh-CN.ts +31 -0
- package/src/i18n/framing.ar.ts +78 -0
- package/src/i18n/framing.de.ts +73 -0
- package/src/i18n/framing.es.ts +78 -0
- package/src/i18n/framing.fr.ts +78 -0
- package/src/i18n/framing.he.ts +77 -0
- package/src/i18n/framing.hi.ts +82 -0
- package/src/i18n/framing.ja.ts +84 -0
- package/src/i18n/framing.ko.ts +82 -0
- package/src/i18n/framing.pt-BR.ts +84 -0
- package/src/i18n/framing.ru.ts +82 -0
- package/src/i18n/framing.zh-CN.ts +82 -0
- package/src/i18n/furniture.ar.ts +92 -0
- package/src/i18n/furniture.de.ts +93 -0
- package/src/i18n/furniture.es.ts +101 -0
- package/src/i18n/furniture.fr.ts +93 -0
- package/src/i18n/furniture.he.ts +101 -0
- package/src/i18n/furniture.hi.ts +101 -0
- package/src/i18n/furniture.ja.ts +101 -0
- package/src/i18n/furniture.ko.ts +101 -0
- package/src/i18n/furniture.pt-BR.ts +101 -0
- package/src/i18n/furniture.ru.ts +101 -0
- package/src/i18n/furniture.zh-CN.ts +101 -0
- package/src/i18n/held-prop.ar.ts +74 -0
- package/src/i18n/held-prop.de.ts +65 -0
- package/src/i18n/held-prop.es.ts +83 -0
- package/src/i18n/held-prop.fr.ts +74 -0
- package/src/i18n/held-prop.he.ts +73 -0
- package/src/i18n/held-prop.hi.ts +82 -0
- package/src/i18n/held-prop.ja.ts +84 -0
- package/src/i18n/held-prop.ko.ts +84 -0
- package/src/i18n/held-prop.pt-BR.ts +84 -0
- package/src/i18n/held-prop.ru.ts +82 -0
- package/src/i18n/held-prop.zh-CN.ts +82 -0
- package/src/i18n/index.ts +208 -0
- package/src/i18n/instrumentation.ar.ts +195 -0
- package/src/i18n/instrumentation.de.ts +195 -0
- package/src/i18n/instrumentation.es.ts +195 -0
- package/src/i18n/instrumentation.fr.ts +195 -0
- package/src/i18n/instrumentation.he.ts +195 -0
- package/src/i18n/instrumentation.hi.ts +195 -0
- package/src/i18n/instrumentation.ja.ts +195 -0
- package/src/i18n/instrumentation.ko.ts +195 -0
- package/src/i18n/instrumentation.pt-BR.ts +195 -0
- package/src/i18n/instrumentation.ru.ts +195 -0
- package/src/i18n/instrumentation.zh-CN.ts +195 -0
- package/src/i18n/lens.ar.ts +24 -0
- package/src/i18n/lens.de.ts +24 -0
- package/src/i18n/lens.es.ts +24 -0
- package/src/i18n/lens.fr.ts +24 -0
- package/src/i18n/lens.he.ts +24 -0
- package/src/i18n/lens.hi.ts +24 -0
- package/src/i18n/lens.ja.ts +24 -0
- package/src/i18n/lens.ko.ts +24 -0
- package/src/i18n/lens.pt-BR.ts +24 -0
- package/src/i18n/lens.ru.ts +25 -0
- package/src/i18n/lens.zh-CN.ts +24 -0
- package/src/i18n/lighting.ar.ts +86 -0
- package/src/i18n/lighting.de.ts +84 -0
- package/src/i18n/lighting.es.ts +89 -0
- package/src/i18n/lighting.fr.ts +84 -0
- package/src/i18n/lighting.he.ts +88 -0
- package/src/i18n/lighting.hi.ts +90 -0
- package/src/i18n/lighting.ja.ts +90 -0
- package/src/i18n/lighting.ko.ts +90 -0
- package/src/i18n/lighting.pt-BR.ts +90 -0
- package/src/i18n/lighting.ru.ts +88 -0
- package/src/i18n/lighting.zh-CN.ts +92 -0
- package/src/i18n/loop-subject.ar.ts +39 -0
- package/src/i18n/loop-subject.de.ts +39 -0
- package/src/i18n/loop-subject.es.ts +39 -0
- package/src/i18n/loop-subject.fr.ts +39 -0
- package/src/i18n/loop-subject.he.ts +39 -0
- package/src/i18n/loop-subject.hi.ts +39 -0
- package/src/i18n/loop-subject.ja.ts +39 -0
- package/src/i18n/loop-subject.ko.ts +39 -0
- package/src/i18n/loop-subject.pt-BR.ts +39 -0
- package/src/i18n/loop-subject.ru.ts +39 -0
- package/src/i18n/loop-subject.zh-CN.ts +39 -0
- package/src/i18n/materials.ar.ts +80 -0
- package/src/i18n/materials.de.ts +80 -0
- package/src/i18n/materials.es.ts +87 -0
- package/src/i18n/materials.fr.ts +80 -0
- package/src/i18n/materials.he.ts +86 -0
- package/src/i18n/materials.hi.ts +86 -0
- package/src/i18n/materials.ja.ts +88 -0
- package/src/i18n/materials.ko.ts +88 -0
- package/src/i18n/materials.pt-BR.ts +88 -0
- package/src/i18n/materials.ru.ts +86 -0
- package/src/i18n/materials.zh-CN.ts +86 -0
- package/src/i18n/mood.ar.ts +61 -0
- package/src/i18n/mood.de.ts +61 -0
- package/src/i18n/mood.es.ts +66 -0
- package/src/i18n/mood.fr.ts +61 -0
- package/src/i18n/mood.he.ts +64 -0
- package/src/i18n/mood.hi.ts +66 -0
- package/src/i18n/mood.ja.ts +66 -0
- package/src/i18n/mood.ko.ts +66 -0
- package/src/i18n/mood.pt-BR.ts +64 -0
- package/src/i18n/mood.ru.ts +64 -0
- package/src/i18n/mood.zh-CN.ts +64 -0
- package/src/i18n/music-genre.ar.ts +302 -0
- package/src/i18n/music-genre.de.ts +302 -0
- package/src/i18n/music-genre.es.ts +302 -0
- package/src/i18n/music-genre.fr.ts +302 -0
- package/src/i18n/music-genre.he.ts +302 -0
- package/src/i18n/music-genre.hi.ts +302 -0
- package/src/i18n/music-genre.ja.ts +302 -0
- package/src/i18n/music-genre.ko.ts +302 -0
- package/src/i18n/music-genre.pt-BR.ts +302 -0
- package/src/i18n/music-genre.ru.ts +302 -0
- package/src/i18n/music-genre.zh-CN.ts +302 -0
- package/src/i18n/music-mood.ar.ts +75 -0
- package/src/i18n/music-mood.de.ts +75 -0
- package/src/i18n/music-mood.es.ts +75 -0
- package/src/i18n/music-mood.fr.ts +75 -0
- package/src/i18n/music-mood.he.ts +75 -0
- package/src/i18n/music-mood.hi.ts +75 -0
- package/src/i18n/music-mood.ja.ts +75 -0
- package/src/i18n/music-mood.ko.ts +75 -0
- package/src/i18n/music-mood.pt-BR.ts +75 -0
- package/src/i18n/music-mood.ru.ts +75 -0
- package/src/i18n/music-mood.zh-CN.ts +75 -0
- package/src/i18n/person.ar.ts +638 -0
- package/src/i18n/person.de.ts +629 -0
- package/src/i18n/person.es.ts +645 -0
- package/src/i18n/person.fr.ts +621 -0
- package/src/i18n/person.he.ts +656 -0
- package/src/i18n/person.hi.ts +654 -0
- package/src/i18n/person.ja.ts +662 -0
- package/src/i18n/person.ko.ts +662 -0
- package/src/i18n/person.pt-BR.ts +656 -0
- package/src/i18n/person.ru.ts +656 -0
- package/src/i18n/person.zh-CN.ts +656 -0
- package/src/i18n/photo-genre.ar.ts +58 -0
- package/src/i18n/photo-genre.de.ts +52 -0
- package/src/i18n/photo-genre.es.ts +64 -0
- package/src/i18n/photo-genre.fr.ts +58 -0
- package/src/i18n/photo-genre.he.ts +62 -0
- package/src/i18n/photo-genre.hi.ts +63 -0
- package/src/i18n/photo-genre.ja.ts +65 -0
- package/src/i18n/photo-genre.ko.ts +63 -0
- package/src/i18n/photo-genre.pt-BR.ts +65 -0
- package/src/i18n/photo-genre.ru.ts +63 -0
- package/src/i18n/photo-genre.zh-CN.ts +63 -0
- package/src/i18n/photographer.ar.ts +75 -0
- package/src/i18n/photographer.de.ts +74 -0
- package/src/i18n/photographer.es.ts +77 -0
- package/src/i18n/photographer.fr.ts +77 -0
- package/src/i18n/photographer.he.ts +78 -0
- package/src/i18n/photographer.hi.ts +80 -0
- package/src/i18n/photographer.ja.ts +84 -0
- package/src/i18n/photographer.ko.ts +84 -0
- package/src/i18n/photographer.pt-BR.ts +83 -0
- package/src/i18n/photographer.ru.ts +78 -0
- package/src/i18n/photographer.zh-CN.ts +78 -0
- package/src/i18n/pose.ar.ts +95 -0
- package/src/i18n/pose.de.ts +96 -0
- package/src/i18n/pose.es.ts +104 -0
- package/src/i18n/pose.fr.ts +96 -0
- package/src/i18n/pose.he.ts +104 -0
- package/src/i18n/pose.hi.ts +104 -0
- package/src/i18n/pose.ja.ts +104 -0
- package/src/i18n/pose.ko.ts +104 -0
- package/src/i18n/pose.pt-BR.ts +104 -0
- package/src/i18n/pose.ru.ts +104 -0
- package/src/i18n/pose.zh-CN.ts +104 -0
- package/src/i18n/post-process-effects.ar.ts +25 -0
- package/src/i18n/post-process-effects.de.ts +24 -0
- package/src/i18n/post-process-effects.es.ts +26 -0
- package/src/i18n/post-process-effects.fr.ts +24 -0
- package/src/i18n/post-process-effects.he.ts +24 -0
- package/src/i18n/post-process-effects.hi.ts +26 -0
- package/src/i18n/post-process-effects.ja.ts +26 -0
- package/src/i18n/post-process-effects.ko.ts +26 -0
- package/src/i18n/post-process-effects.pt-BR.ts +24 -0
- package/src/i18n/post-process-effects.ru.ts +24 -0
- package/src/i18n/post-process-effects.zh-CN.ts +24 -0
- package/src/i18n/render-quality.ar.ts +34 -0
- package/src/i18n/render-quality.de.ts +30 -0
- package/src/i18n/render-quality.es.ts +35 -0
- package/src/i18n/render-quality.fr.ts +34 -0
- package/src/i18n/render-quality.he.ts +33 -0
- package/src/i18n/render-quality.hi.ts +39 -0
- package/src/i18n/render-quality.ja.ts +39 -0
- package/src/i18n/render-quality.ko.ts +39 -0
- package/src/i18n/render-quality.pt-BR.ts +37 -0
- package/src/i18n/render-quality.ru.ts +37 -0
- package/src/i18n/render-quality.zh-CN.ts +37 -0
- package/src/i18n/seasons.ar.ts +10 -0
- package/src/i18n/seasons.de.ts +10 -0
- package/src/i18n/seasons.es.ts +10 -0
- package/src/i18n/seasons.fr.ts +10 -0
- package/src/i18n/seasons.he.ts +10 -0
- package/src/i18n/seasons.hi.ts +10 -0
- package/src/i18n/seasons.ja.ts +10 -0
- package/src/i18n/seasons.ko.ts +10 -0
- package/src/i18n/seasons.pt-BR.ts +10 -0
- package/src/i18n/seasons.ru.ts +10 -0
- package/src/i18n/seasons.zh-CN.ts +10 -0
- package/src/i18n/setting.ar.ts +73 -0
- package/src/i18n/setting.de.ts +73 -0
- package/src/i18n/setting.es.ts +77 -0
- package/src/i18n/setting.fr.ts +73 -0
- package/src/i18n/setting.he.ts +76 -0
- package/src/i18n/setting.hi.ts +76 -0
- package/src/i18n/setting.ja.ts +78 -0
- package/src/i18n/setting.ko.ts +76 -0
- package/src/i18n/setting.pt-BR.ts +78 -0
- package/src/i18n/setting.ru.ts +76 -0
- package/src/i18n/setting.zh-CN.ts +76 -0
- package/src/i18n/style.ar.ts +55 -0
- package/src/i18n/style.de.ts +54 -0
- package/src/i18n/style.es.ts +54 -0
- package/src/i18n/style.fr.ts +54 -0
- package/src/i18n/style.he.ts +54 -0
- package/src/i18n/style.hi.ts +54 -0
- package/src/i18n/style.ja.ts +54 -0
- package/src/i18n/style.ko.ts +54 -0
- package/src/i18n/style.pt-BR.ts +56 -0
- package/src/i18n/style.ru.ts +54 -0
- package/src/i18n/style.zh-CN.ts +54 -0
- package/src/i18n/styling.ar.ts +295 -0
- package/src/i18n/styling.de.ts +285 -0
- package/src/i18n/styling.es.ts +317 -0
- package/src/i18n/styling.fr.ts +285 -0
- package/src/i18n/styling.he.ts +307 -0
- package/src/i18n/styling.hi.ts +323 -0
- package/src/i18n/styling.ja.ts +317 -0
- package/src/i18n/styling.ko.ts +311 -0
- package/src/i18n/styling.pt-BR.ts +301 -0
- package/src/i18n/styling.ru.ts +301 -0
- package/src/i18n/styling.zh-CN.ts +307 -0
- package/src/i18n/temporal.ar.ts +31 -0
- package/src/i18n/temporal.de.ts +24 -0
- package/src/i18n/temporal.es.ts +27 -0
- package/src/i18n/temporal.fr.ts +28 -0
- package/src/i18n/temporal.he.ts +31 -0
- package/src/i18n/temporal.hi.ts +31 -0
- package/src/i18n/temporal.ja.ts +31 -0
- package/src/i18n/temporal.ko.ts +31 -0
- package/src/i18n/temporal.pt-BR.ts +31 -0
- package/src/i18n/temporal.ru.ts +31 -0
- package/src/i18n/temporal.zh-CN.ts +31 -0
- package/src/i18n/transitions.ar.ts +97 -0
- package/src/i18n/transitions.de.ts +97 -0
- package/src/i18n/transitions.es.ts +97 -0
- package/src/i18n/transitions.fr.ts +97 -0
- package/src/i18n/transitions.he.ts +97 -0
- package/src/i18n/transitions.hi.ts +97 -0
- package/src/i18n/transitions.ja.ts +97 -0
- package/src/i18n/transitions.ko.ts +97 -0
- package/src/i18n/transitions.pt-BR.ts +97 -0
- package/src/i18n/transitions.ru.ts +97 -0
- package/src/i18n/transitions.zh-CN.ts +97 -0
- package/src/i18n/types.ts +158 -0
- package/src/i18n/vehicles.ar.ts +137 -0
- package/src/i18n/vehicles.de.ts +125 -0
- package/src/i18n/vehicles.es.ts +138 -0
- package/src/i18n/vehicles.fr.ts +125 -0
- package/src/i18n/vehicles.he.ts +136 -0
- package/src/i18n/vehicles.hi.ts +138 -0
- package/src/i18n/vehicles.ja.ts +138 -0
- package/src/i18n/vehicles.ko.ts +138 -0
- package/src/i18n/vehicles.pt-BR.ts +136 -0
- package/src/i18n/vehicles.ru.ts +136 -0
- package/src/i18n/vehicles.zh-CN.ts +136 -0
- package/src/i18n/voice-character.ar.ts +126 -0
- package/src/i18n/voice-character.de.ts +126 -0
- package/src/i18n/voice-character.es.ts +126 -0
- package/src/i18n/voice-character.fr.ts +126 -0
- package/src/i18n/voice-character.he.ts +126 -0
- package/src/i18n/voice-character.hi.ts +126 -0
- package/src/i18n/voice-character.ja.ts +126 -0
- package/src/i18n/voice-character.ko.ts +126 -0
- package/src/i18n/voice-character.pt-BR.ts +126 -0
- package/src/i18n/voice-character.ru.ts +126 -0
- package/src/i18n/voice-character.zh-CN.ts +126 -0
- package/src/i18n/voice-delivery.ar.ts +77 -0
- package/src/i18n/voice-delivery.de.ts +77 -0
- package/src/i18n/voice-delivery.es.ts +77 -0
- package/src/i18n/voice-delivery.fr.ts +77 -0
- package/src/i18n/voice-delivery.he.ts +77 -0
- package/src/i18n/voice-delivery.hi.ts +77 -0
- package/src/i18n/voice-delivery.ja.ts +77 -0
- package/src/i18n/voice-delivery.ko.ts +77 -0
- package/src/i18n/voice-delivery.pt-BR.ts +77 -0
- package/src/i18n/voice-delivery.ru.ts +77 -0
- package/src/i18n/voice-delivery.zh-CN.ts +77 -0
- package/src/i18n/weapons.ar.ts +115 -0
- package/src/i18n/weapons.de.ts +114 -0
- package/src/i18n/weapons.es.ts +116 -0
- package/src/i18n/weapons.fr.ts +103 -0
- package/src/i18n/weapons.he.ts +114 -0
- package/src/i18n/weapons.hi.ts +116 -0
- package/src/i18n/weapons.ja.ts +116 -0
- package/src/i18n/weapons.ko.ts +116 -0
- package/src/i18n/weapons.pt-BR.ts +114 -0
- package/src/i18n/weapons.ru.ts +114 -0
- package/src/i18n/weapons.zh-CN.ts +114 -0
- package/src/image-critic-node.ts +61 -0
- package/src/image-critic-types.ts +40 -0
- package/src/index.ts +866 -0
- package/src/inject-upstream.ts +15 -0
- package/src/inpaint-region.ts +51 -0
- package/src/json-evaluator.ts +732 -0
- package/src/json-path.ts +52 -0
- package/src/lip-sync-pricing.ts +90 -0
- package/src/list-merge.ts +40 -0
- package/src/list-sort.ts +122 -0
- package/src/list-source-resolver.ts +50 -0
- package/src/llm-models.ts +245 -0
- package/src/location-mention-slug.ts +291 -0
- package/src/location-preset-catalog-map.ts +128 -0
- package/src/loop-delimiter.ts +73 -0
- package/src/lora-routing.ts +95 -0
- package/src/lottie-overlay-catalog.ts +175 -0
- package/src/lottie-slots.ts +309 -0
- package/src/model-catalog.ts +2386 -0
- package/src/model-constants.ts +1910 -0
- package/src/model-tree.ts +120 -0
- package/src/monetization.ts +15 -0
- package/src/multi-pick.ts +42 -0
- package/src/node-default-mappings.ts +262 -0
- package/src/node-mappable-fields.ts +70 -0
- package/src/node-preset-export.ts +56 -0
- package/src/node-preset-extract.ts +147 -0
- package/src/node-refs.ts +253 -0
- package/src/node-runtime-keys.ts +117 -0
- package/src/object-aspect-defaults.ts +72 -0
- package/src/object-picker-types.ts +22 -0
- package/src/parameter-node-value.ts +284 -0
- package/src/pinterest-url.ts +28 -0
- package/src/pipeline-chat.ts +72 -0
- package/src/pipeline-defaults.ts +318 -0
- package/src/pipeline-events.ts +162 -0
- package/src/pipeline-state-types.ts +196 -0
- package/src/pipeline-types.ts +647 -0
- package/src/pipeline-validation.ts +87 -0
- package/src/presentation-types.ts +23 -0
- package/src/presentation-utils.ts +397 -0
- package/src/producer-types.ts +188 -0
- package/src/progress-curve.ts +99 -0
- package/src/provider-directive-defaults.ts +69 -0
- package/src/reduce-strategy-registry.ts +121 -0
- package/src/reference-board-templates.ts +81 -0
- package/src/reference-roles.ts +153 -0
- package/src/reference-sheet/__tests__/board-to-column.test.ts +20 -0
- package/src/reference-sheet/__tests__/catalog.test.ts +50 -0
- package/src/reference-sheet/__tests__/cost.test.ts +38 -0
- package/src/reference-sheet/__tests__/no-drift.test.ts +28 -0
- package/src/reference-sheet/__tests__/panel-plan.test.ts +61 -0
- package/src/reference-sheet/__tests__/panel-prompts.test.ts +17 -0
- package/src/reference-sheet/__tests__/plan-generation.test.ts +72 -0
- package/src/reference-sheet/__tests__/presets.test.ts +54 -0
- package/src/reference-sheet/__tests__/types.test.ts +24 -0
- package/src/reference-sheet/catalog.ts +144 -0
- package/src/reference-sheet/cost.ts +37 -0
- package/src/reference-sheet/credit-id.ts +7 -0
- package/src/reference-sheet/index.ts +8 -0
- package/src/reference-sheet/panel-plan.ts +65 -0
- package/src/reference-sheet/panel-prompts.ts +14 -0
- package/src/reference-sheet/plan-generation.ts +79 -0
- package/src/reference-sheet/presets.ts +77 -0
- package/src/reference-sheet/types.ts +92 -0
- package/src/repeat-types.ts +97 -0
- package/src/resolve-field-mappings.ts +119 -0
- package/src/route-filter.ts +80 -0
- package/src/scene-helper-types.ts +249 -0
- package/src/scene-node-types.ts +281 -0
- package/src/scraper-actors.ts +57 -0
- package/src/scraper-output-schemas.ts +25 -0
- package/src/seasons.ts +45 -0
- package/src/selector.ts +864 -0
- package/src/settled-with-limit.ts +42 -0
- package/src/shot-element.ts +43 -0
- package/src/social-media-specs.ts +121 -0
- package/src/social-post.ts +24 -0
- package/src/sub-workflow-validation.ts +78 -0
- package/src/supported-fonts.ts +40 -0
- package/src/surround.ts +113 -0
- package/src/switchx-pricing.ts +33 -0
- package/src/text-separators.ts +59 -0
- package/src/to-connected-references.ts +77 -0
- package/src/types.ts +272 -0
- package/src/variant-job-id.ts +17 -0
- package/src/vehicles.ts +213 -0
- package/src/video-analysis-pricing.ts +74 -0
- package/src/video-analysis.ts +127 -0
- package/src/video-duration.ts +25 -0
- package/src/voice-changer-models.ts +36 -0
- package/src/voice-matcher-types.ts +37 -0
- package/src/voices.ts +92 -0
- package/src/weapons.ts +190 -0
- package/src/workflow-export.ts +153 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
VIDEO_UTIL_PRICING,
|
|
4
|
+
estimateLoopVideoCredits,
|
|
5
|
+
estimateTrimVideoCredits,
|
|
6
|
+
estimateCombineVideosCredits,
|
|
7
|
+
} from "../credit-estimators/video-utils.js"
|
|
8
|
+
import {
|
|
9
|
+
estimateLoopTrimAddonCredits,
|
|
10
|
+
type LoopTrimEstimatorInput,
|
|
11
|
+
} from "../credit-estimators/video-utils.js"
|
|
12
|
+
import { assembleNarratedVideoCredits } from "../credit-estimators/video-utils.js"
|
|
13
|
+
|
|
14
|
+
describe("estimateLoopVideoCredits", () => {
|
|
15
|
+
describe("repeat mode", () => {
|
|
16
|
+
it("uses upstream duration when provided", () => {
|
|
17
|
+
// 4× a 5s clip = 20s output → ceil(20/5) = 4
|
|
18
|
+
expect(estimateLoopVideoCredits({ mode: "repeat", repeatCount: 4 }, 5)).toBe(4)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("falls back to 8s per repeat when upstream duration is missing", () => {
|
|
22
|
+
// 4 × 8 = 32s → ceil(32/5) = 7
|
|
23
|
+
expect(estimateLoopVideoCredits({ mode: "repeat", repeatCount: 4 })).toBe(7)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it("defaults repeatCount to 2 when missing", () => {
|
|
27
|
+
// 2 × 8 = 16s → ceil(16/5) = 4
|
|
28
|
+
expect(estimateLoopVideoCredits({ mode: "repeat" })).toBe(4)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe("duration mode", () => {
|
|
33
|
+
it("uses targetDuration regardless of upstream", () => {
|
|
34
|
+
// ceil(60/5) = 12
|
|
35
|
+
expect(estimateLoopVideoCredits({ mode: "duration", targetDuration: 60 }, 5)).toBe(12)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("defaults targetDuration to fallback when missing", () => {
|
|
39
|
+
// ceil(8/5) = 2
|
|
40
|
+
expect(estimateLoopVideoCredits({ mode: "duration" })).toBe(2)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe("smart-loop-cut adder", () => {
|
|
45
|
+
it("adds ceil(lookback/24) when enabled", () => {
|
|
46
|
+
// 30s base = 6, lookback 16 → ceil(16/24) = 1, total 7
|
|
47
|
+
expect(estimateLoopVideoCredits({
|
|
48
|
+
mode: "duration", targetDuration: 30,
|
|
49
|
+
smartLoopCutBeforeRepeat: true, smartLoopCutLookback: 16,
|
|
50
|
+
})).toBe(7)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("uses default lookback 16 when smart-cut on but lookback missing", () => {
|
|
54
|
+
// 60s base = 12, default lookback 16 → +1 = 13
|
|
55
|
+
expect(estimateLoopVideoCredits({
|
|
56
|
+
mode: "duration", targetDuration: 60,
|
|
57
|
+
smartLoopCutBeforeRepeat: true,
|
|
58
|
+
})).toBe(13)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("max lookback 64 stays within bound", () => {
|
|
62
|
+
// 60s base = 12, lookback 64 → ceil(64/24) = 3, total 15
|
|
63
|
+
expect(estimateLoopVideoCredits({
|
|
64
|
+
mode: "duration", targetDuration: 60,
|
|
65
|
+
smartLoopCutBeforeRepeat: true, smartLoopCutLookback: 64,
|
|
66
|
+
})).toBe(15)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("does NOT add when smartLoopCutBeforeRepeat is false", () => {
|
|
70
|
+
expect(estimateLoopVideoCredits({
|
|
71
|
+
mode: "duration", targetDuration: 60,
|
|
72
|
+
smartLoopCutBeforeRepeat: false, smartLoopCutLookback: 64,
|
|
73
|
+
})).toBe(12)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
describe("min-1 floor", () => {
|
|
78
|
+
it("never returns 0 — even for sub-second output", () => {
|
|
79
|
+
expect(estimateLoopVideoCredits({ mode: "duration", targetDuration: 0.5 })).toBe(1)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe("constants", () => {
|
|
84
|
+
it("exports the expected pricing constants", () => {
|
|
85
|
+
expect(VIDEO_UTIL_PRICING.CREDITS_PER_5_SEC).toBe(1)
|
|
86
|
+
expect(VIDEO_UTIL_PRICING.FRAMES_PER_CREDIT).toBe(24)
|
|
87
|
+
expect(VIDEO_UTIL_PRICING.FALLBACK_DURATION_SECONDS).toBe(8)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("estimateTrimVideoCredits", () => {
|
|
93
|
+
describe("time mode (default)", () => {
|
|
94
|
+
it("computes credits from endTime - startTime", () => {
|
|
95
|
+
// 0–10s = 10s → ceil(10/5) = 2
|
|
96
|
+
expect(estimateTrimVideoCredits({ trimMode: "time", startTime: 0, endTime: 10 })).toBe(2)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("treats missing trimMode as time mode", () => {
|
|
100
|
+
// ceil(15/5) = 3
|
|
101
|
+
expect(estimateTrimVideoCredits({ startTime: 5, endTime: 20 })).toBe(3)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("defaults missing start/end to 0", () => {
|
|
105
|
+
// 0-0 = 0s, but min-1 → 1
|
|
106
|
+
expect(estimateTrimVideoCredits({ trimMode: "time" })).toBe(1)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe("frames mode", () => {
|
|
111
|
+
it("subtracts trim frames from upstream duration at 24fps", () => {
|
|
112
|
+
// 10s upstream - 24/24 - 24/24 = 8s → ceil(8/5) = 2
|
|
113
|
+
expect(estimateTrimVideoCredits({
|
|
114
|
+
trimMode: "frames", trimStartFrames: 24, trimEndFrames: 24,
|
|
115
|
+
}, 10)).toBe(2)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("uses 8s fallback when upstream missing", () => {
|
|
119
|
+
// 8s - 0 - 0 = 8s → ceil(8/5) = 2
|
|
120
|
+
expect(estimateTrimVideoCredits({ trimMode: "frames" })).toBe(2)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it("clamps to 0 if frame trim exceeds duration", () => {
|
|
124
|
+
// 5s - 240/24 - 0 = -5 clamped to 0 → max(1, ceil(0)) = 1
|
|
125
|
+
expect(estimateTrimVideoCredits({
|
|
126
|
+
trimMode: "frames", trimStartFrames: 240, trimEndFrames: 0,
|
|
127
|
+
}, 5)).toBe(1)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("smart-loop-cut mode", () => {
|
|
132
|
+
it("upstream-based base + lookback adder", () => {
|
|
133
|
+
// 10s upstream → base 2, lookback 16 → +1 = 3
|
|
134
|
+
expect(estimateTrimVideoCredits({
|
|
135
|
+
trimMode: "smart-loop-cut", smartLoopCutLookback: 16,
|
|
136
|
+
}, 10)).toBe(3)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("uses default lookback 16 when missing", () => {
|
|
140
|
+
// 8s upstream → base 2, default 16 → +1 = 3
|
|
141
|
+
expect(estimateTrimVideoCredits({ trimMode: "smart-loop-cut" })).toBe(3)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
describe("seconds mode (relative trim in seconds)", () => {
|
|
146
|
+
it("subtracts trim seconds from upstream duration", () => {
|
|
147
|
+
// 30s - 2 - 3 = 25s → ceil(25/5) = 5
|
|
148
|
+
expect(estimateTrimVideoCredits({
|
|
149
|
+
trimMode: "seconds", trimStartSeconds: 2, trimEndSeconds: 3,
|
|
150
|
+
}, 30)).toBe(5)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("clamps to min 1 credit if trim exceeds duration", () => {
|
|
154
|
+
// 5s - 10 - 0 = -5 clamp to 0 → max(1, ceil(0)) = 1
|
|
155
|
+
expect(estimateTrimVideoCredits({
|
|
156
|
+
trimMode: "seconds", trimStartSeconds: 10,
|
|
157
|
+
}, 5)).toBe(1)
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
describe("keep-first-seconds mode", () => {
|
|
162
|
+
it("output is min(upstream, keepFirstSeconds)", () => {
|
|
163
|
+
// keepFirst=8, upstream=20 → output 8s → ceil(8/5) = 2
|
|
164
|
+
expect(estimateTrimVideoCredits({
|
|
165
|
+
trimMode: "keep-first-seconds", keepFirstSeconds: 8,
|
|
166
|
+
}, 20)).toBe(2)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("clamps to upstream when keep > upstream", () => {
|
|
170
|
+
// keepFirst=30, upstream=5 → output 5s → ceil(5/5) = 1
|
|
171
|
+
expect(estimateTrimVideoCredits({
|
|
172
|
+
trimMode: "keep-first-seconds", keepFirstSeconds: 30,
|
|
173
|
+
}, 5)).toBe(1)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe("keep-last-seconds mode", () => {
|
|
178
|
+
it("output is min(upstream, keepLastSeconds)", () => {
|
|
179
|
+
// keepLast=10, upstream=30 → output 10s → ceil(10/5) = 2
|
|
180
|
+
expect(estimateTrimVideoCredits({
|
|
181
|
+
trimMode: "keep-last-seconds", keepLastSeconds: 10,
|
|
182
|
+
}, 30)).toBe(2)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("uses 8s fallback when upstream missing", () => {
|
|
186
|
+
// keepLast=10, upstream undefined → upstream=8 → min(8,10)=8 → ceil(8/5)=2
|
|
187
|
+
expect(estimateTrimVideoCredits({
|
|
188
|
+
trimMode: "keep-last-seconds", keepLastSeconds: 10,
|
|
189
|
+
})).toBe(2)
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
describe("estimateCombineVideosCredits", () => {
|
|
195
|
+
it("2 inputs with cut transition", () => {
|
|
196
|
+
// 5+5=10s, cut → 0 transition overlap, 0 frame trim, numInputs-2=0
|
|
197
|
+
// → ceil(10/5) + 0 = 2
|
|
198
|
+
expect(estimateCombineVideosCredits({ transition: "cut" }, [5, 5])).toBe(2)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("3 inputs with fade transition (0.5s)", () => {
|
|
202
|
+
// 10+10+10=30s, 2 fades × 0.5 = 1s overlap → 29s → ceil(29/5)=6
|
|
203
|
+
// numInputs-2 = 1 → 6 + 1 = 7
|
|
204
|
+
expect(estimateCombineVideosCredits({
|
|
205
|
+
transition: "fade", transitionDuration: 0.5,
|
|
206
|
+
}, [10, 10, 10])).toBe(7)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("5 inputs with dissolve and per-clip frame trim (24+24)", () => {
|
|
210
|
+
// 5×8=40s, 4 transitions × 0.5 = 2s overlap, frame trim 48/24=2s × 5 = 10s
|
|
211
|
+
// → 40 - 2 - 10 = 28s → ceil(28/5) = 6
|
|
212
|
+
// numInputs-2 = 3 → 6 + 3 = 9
|
|
213
|
+
expect(estimateCombineVideosCredits({
|
|
214
|
+
transition: "dissolve",
|
|
215
|
+
transitionDuration: 0.5,
|
|
216
|
+
trimStartFrames: 24,
|
|
217
|
+
trimEndFrames: 24,
|
|
218
|
+
}, [8, 8, 8, 8, 8])).toBe(9)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("falls back to 8s per missing upstream entry", () => {
|
|
222
|
+
// [undef, undef, undef] → 3×8=24s, cut → ceil(24/5)=5, +1 (numInputs-2) = 6
|
|
223
|
+
expect(estimateCombineVideosCredits(
|
|
224
|
+
{ transition: "cut" },
|
|
225
|
+
[undefined, undefined, undefined],
|
|
226
|
+
)).toBe(6)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it("treats empty input array as 2-input fallback", () => {
|
|
230
|
+
// numInputs = 0 || 2 = 2; total = 0 → max(1, …) = 1
|
|
231
|
+
expect(estimateCombineVideosCredits({ transition: "cut" }, [])).toBe(1)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it("min-1 floor when total clamps to 0", () => {
|
|
235
|
+
// 2 × 0s = 0, no overlap, num-2=0 → max(1, ceil(0)) = 1
|
|
236
|
+
expect(estimateCombineVideosCredits({ transition: "cut" }, [0, 0])).toBe(1)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("does not subtract transition overlap on cut", () => {
|
|
240
|
+
// [10, 10] cut → ceil(20/5)=4, numInputs-2=0 → 4
|
|
241
|
+
expect(estimateCombineVideosCredits({ transition: "cut", transitionDuration: 999 }, [10, 10])).toBe(4)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it("uses default transitionDuration 0.5 when missing on a non-cut", () => {
|
|
245
|
+
// [10, 10] fade, no transitionDuration → 20 - 0.5 = 19.5 → ceil(19.5/5)=4
|
|
246
|
+
expect(estimateCombineVideosCredits({ transition: "fade" }, [10, 10])).toBe(4)
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
describe("estimateLoopTrimAddonCredits", () => {
|
|
251
|
+
it("returns 0 when loopTrim is undefined", () => {
|
|
252
|
+
expect(estimateLoopTrimAddonCredits(undefined, 8)).toBe(0)
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("returns 0 when enabled is false", () => {
|
|
256
|
+
expect(estimateLoopTrimAddonCredits({ enabled: false }, 8)).toBe(0)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it("computes ceil(duration/5) + ceil(framesToTest/24) when enabled", () => {
|
|
260
|
+
// 8s / 5 = 2 (ceil), 16 / 24 = 1 (ceil) → 3
|
|
261
|
+
expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 16 }, 8)).toBe(3)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("uses default framesToTest=16 when omitted", () => {
|
|
265
|
+
// 10s / 5 = 2, 16 / 24 = 1 → 3
|
|
266
|
+
expect(estimateLoopTrimAddonCredits({ enabled: true }, 10)).toBe(3)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it("clamps framesToTest to 64 max", () => {
|
|
270
|
+
// 8s / 5 = 2, 64 / 24 = 3 → 5
|
|
271
|
+
expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 999 }, 8)).toBe(5)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it("clamps framesToTest to 1 min", () => {
|
|
275
|
+
// 8s / 5 = 2, 1 / 24 = 1 → 3
|
|
276
|
+
expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 0 }, 8)).toBe(3)
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("scales with output duration", () => {
|
|
280
|
+
// 60s / 5 = 12, 16 / 24 = 1 → 13
|
|
281
|
+
expect(estimateLoopTrimAddonCredits({ enabled: true, framesToTest: 16 }, 60)).toBe(13)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it("returns 0 when framesToTest=64 but enabled is false (precedence)", () => {
|
|
285
|
+
expect(estimateLoopTrimAddonCredits({ enabled: false, framesToTest: 64 }, 60)).toBe(0)
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe("assembleNarratedVideoCredits", () => {
|
|
290
|
+
it("matches the worked examples 6→4, 24→7, 60→13", () => {
|
|
291
|
+
expect(assembleNarratedVideoCredits(6)).toBe(4)
|
|
292
|
+
expect(assembleNarratedVideoCredits(24)).toBe(7)
|
|
293
|
+
expect(assembleNarratedVideoCredits(60)).toBe(13)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it("is 3 flat credits at the theoretical N=0 floor (never shown directly — call sites clamp N to 1)", () => {
|
|
297
|
+
expect(assembleNarratedVideoCredits(0)).toBe(3)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it("returns the 1-block floor of 4 credits", () => {
|
|
301
|
+
expect(assembleNarratedVideoCredits(1)).toBe(4)
|
|
302
|
+
})
|
|
303
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
VOICE_CHANGER_MODELS,
|
|
4
|
+
VOICE_CHANGER_MODEL_IDS,
|
|
5
|
+
DEFAULT_VOICE_CHANGER_MODEL,
|
|
6
|
+
} from "../voice-changer-models.js"
|
|
7
|
+
|
|
8
|
+
describe("voice-changer-models", () => {
|
|
9
|
+
it("exposes the ElevenLabs speech-to-speech models", () => {
|
|
10
|
+
expect(VOICE_CHANGER_MODEL_IDS).toEqual([
|
|
11
|
+
"eleven_multilingual_sts_v2",
|
|
12
|
+
"eleven_english_sts_v2",
|
|
13
|
+
])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("defaults to the Multilingual model (ElevenLabs-recommended, even for English)", () => {
|
|
17
|
+
// ElevenLabs' docs recommend Multilingual v2 even for English source
|
|
18
|
+
// audio (it often outperforms the English-only model), and it's
|
|
19
|
+
// required for non-English audio — so it's the default. English v2
|
|
20
|
+
// remains selectable.
|
|
21
|
+
expect(DEFAULT_VOICE_CHANGER_MODEL).toBe("eleven_multilingual_sts_v2")
|
|
22
|
+
expect(VOICE_CHANGER_MODEL_IDS).toContain(DEFAULT_VOICE_CHANGER_MODEL)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("gives every model a non-empty label and description for the picker", () => {
|
|
26
|
+
expect(VOICE_CHANGER_MODELS.length).toBe(VOICE_CHANGER_MODEL_IDS.length)
|
|
27
|
+
for (const m of VOICE_CHANGER_MODELS) {
|
|
28
|
+
expect(m.value.length).toBeGreaterThan(0)
|
|
29
|
+
expect(m.label.length).toBeGreaterThan(0)
|
|
30
|
+
expect(m.desc.length).toBeGreaterThan(0)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { stripExportContent } from "../workflow-export.js"
|
|
3
|
+
import { EXECUTION_DATA_KEYS } from "../node-runtime-keys.js"
|
|
4
|
+
import type { GenericNode } from "../types.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Invariant guard for the template-export leak class (audit R2-H4): a
|
|
8
|
+
* "shareable" template export must never carry runtime/result fields —
|
|
9
|
+
* generated media URLs, internal job ids, trained-LoRA identity, etc. Those
|
|
10
|
+
* are enumerated in EXECUTION_DATA_KEYS (the single source of truth), and
|
|
11
|
+
* GENERATED_FIELDS is built from it, so this test fails the moment a new
|
|
12
|
+
* runtime key is added without being covered.
|
|
13
|
+
*/
|
|
14
|
+
describe("stripExportContent — template export hygiene", () => {
|
|
15
|
+
// `shots` lives in EXECUTION_DATA_KEYS but is user config (Kling-3.0
|
|
16
|
+
// storyboard) — it must SURVIVE a template export, not be stripped.
|
|
17
|
+
const CONFIG_KEPT = new Set(["shots"])
|
|
18
|
+
|
|
19
|
+
it("strips runtime/result EXECUTION_DATA_KEYS while keeping config fields", () => {
|
|
20
|
+
const data: Record<string, unknown> = {}
|
|
21
|
+
for (const key of EXECUTION_DATA_KEYS) data[key] = "SENSITIVE_RUNTIME_VALUE"
|
|
22
|
+
data.prompt = "keep me" // a real config field — must survive the strip
|
|
23
|
+
data.provider = "veo3.1"
|
|
24
|
+
|
|
25
|
+
const node: GenericNode = { id: "n1", type: "generate-video", data }
|
|
26
|
+
const [out] = stripExportContent([node])
|
|
27
|
+
const outData = out.data as Record<string, unknown>
|
|
28
|
+
|
|
29
|
+
for (const key of EXECUTION_DATA_KEYS) {
|
|
30
|
+
if (CONFIG_KEPT.has(key)) continue
|
|
31
|
+
expect(outData[key], `${key} must be stripped from a template export`).toBeUndefined()
|
|
32
|
+
}
|
|
33
|
+
expect(outData.prompt).toBe("keep me")
|
|
34
|
+
expect(outData.provider).toBe("veo3.1")
|
|
35
|
+
// Regression guard: Kling-3.0 multishot config must survive template export.
|
|
36
|
+
expect(outData.shots, "shots (Kling-3 config) must NOT be stripped").toBe("SENSITIVE_RUNTIME_VALUE")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("clears faceDbId / referencedWorkflowId on face + sub-workflow template nodes", () => {
|
|
40
|
+
const face: GenericNode = { id: "f1", type: "face", data: { faceDbId: "exporter-face-id", name: "Hero" } }
|
|
41
|
+
const sub: GenericNode = { id: "s1", type: "sub-workflow", data: { referencedWorkflowId: "exporter-wf-id" } }
|
|
42
|
+
const [outFace, outSub] = stripExportContent([face, sub])
|
|
43
|
+
expect((outFace.data as Record<string, unknown>).faceDbId).toBeUndefined()
|
|
44
|
+
expect((outSub.data as Record<string, unknown>).referencedWorkflowId).toBeUndefined()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Avatar (HeyGen) duration-bucketing + credit-identifier helpers.
|
|
3
|
+
* Single source of truth for the NON-monetary side of ai-avatar billing
|
|
4
|
+
* (duration estimation, bucket selection, composite credit-id construction) —
|
|
5
|
+
* imported by the frontend node UI (to build the id it hands to the
|
|
6
|
+
* `/v1/credits/model-cost` display lookup) AND the backend route/orchestrator
|
|
7
|
+
* (to resolve the credit-guard reservation id pre-Zod).
|
|
8
|
+
*
|
|
9
|
+
* The provider-$ rate table and the USD/credit cost FORMULAS derived from it
|
|
10
|
+
* live in `backend/src/lib/pricing/ai-avatar-cost.ts` (core, not ee/ — the
|
|
11
|
+
* HeyGen provider integration needs them regardless of edition). They were
|
|
12
|
+
* moved out of this package (published Apache-2.0 on npm — an irrevocable
|
|
13
|
+
* grant) per the 2026-07-06 public-flip IP audit, S5.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type AiAvatarEngine = "avatar-v" | "avatar-iv"
|
|
17
|
+
export type AiAvatarResolution = "720p" | "1080p" | "4k"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Worst-case clip duration used as the credit-reserve ceiling.
|
|
21
|
+
* A 5000-character script at voiceSpeed 0.5 ≈ 5000/12/0.5 ≈ 833s → top bucket 900s.
|
|
22
|
+
*/
|
|
23
|
+
export const AI_AVATAR_MAX_DURATION_SEC = 900
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Duration buckets (seconds) used to pick a reserve ceiling at creditGuard
|
|
27
|
+
* time, when the actual clip length is unknown (text-mode scripts) or only
|
|
28
|
+
* known via a server-side ffprobe (audio mode).
|
|
29
|
+
*
|
|
30
|
+
* The low end is FINE-GRAINED (5/10/15/30/60s) so short clips — the common
|
|
31
|
+
* case — don't over-reserve. A 15s clip lands in the 15s bucket, not the old
|
|
32
|
+
* coarse 30s floor (which doubled the hold). The high end stays coarse because
|
|
33
|
+
* long-form scripts are rare and the bucket-up is a one-time refundable surplus.
|
|
34
|
+
*
|
|
35
|
+
* Bucket selection:
|
|
36
|
+
* - Text mode: bucket = pickAiAvatarBucket(ceil(scriptChars / 12 / voiceSpeed)).
|
|
37
|
+
* - Audio mode: bucket = pickAiAvatarBucket(ceil(probedDurationSec)) when the
|
|
38
|
+
* ffprobe preHandler stashed `__probedDurationSec` on the body; otherwise a
|
|
39
|
+
* MODEST 120s default (NOT the 900s top bucket — that was the bug: it
|
|
40
|
+
* reserved ~4000+ credits for a sub-minute clip).
|
|
41
|
+
*
|
|
42
|
+
* Worst-case derivation: 5000 chars at voiceSpeed=0.5 → ceil(5000/12/0.5) = 834s
|
|
43
|
+
* → fits in 900s bucket. The 900s top bucket covers all legal text inputs.
|
|
44
|
+
*
|
|
45
|
+
* Note: audio >900s (no probe) would undercharge against the 120s default, but
|
|
46
|
+
* the commit-time metered true-up CANNOT charge more than reserved — so a long
|
|
47
|
+
* un-probed audio clip is a (small) revenue leak, not an over-charge. The probe
|
|
48
|
+
* closes it for the normal path. The 900s bucket remains available for the
|
|
49
|
+
* probed long-audio case.
|
|
50
|
+
*/
|
|
51
|
+
export const AI_AVATAR_DURATION_BUCKETS = [5, 10, 15, 30, 60, 120, 240, 360, 600, 900] as const
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Modest fallback bucket (seconds) for AUDIO mode when the ffprobe preHandler
|
|
55
|
+
* did NOT stash a probed duration. Replaces the old 900s top-bucket fallback
|
|
56
|
+
* that over-reserved ~4000+ credits for short clips. The commit-time metered
|
|
57
|
+
* true-up is refund-only, so this is a reserve ceiling, not a charge.
|
|
58
|
+
*/
|
|
59
|
+
export const AI_AVATAR_AUDIO_FALLBACK_SEC = 120
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Hard cap (seconds) on AUDIO-mode clip length. HeyGen has no natural length
|
|
63
|
+
* limit on an audio-driven avatar, so a long audio = an expensive / possibly
|
|
64
|
+
* rejected generation and a bounded under-reserve window. We cap it here:
|
|
65
|
+
* - The worker TRIMS incoming audio longer than this down to AI_AVATAR_MAX_AUDIO_SEC
|
|
66
|
+
* (and warns the user, non-fatal).
|
|
67
|
+
* - The credit RESERVE is capped at this bucket too: even a 30-min probed
|
|
68
|
+
* audio buckets at 600s, never the 900s top bucket. Because the worker
|
|
69
|
+
* trims the actual clip to ≤600s, the metered true-up can only refund.
|
|
70
|
+
*
|
|
71
|
+
* Text mode (5000-char script) and cinematic (4–15s) are bounded elsewhere and
|
|
72
|
+
* are NOT affected by this cap.
|
|
73
|
+
*/
|
|
74
|
+
export const AI_AVATAR_MAX_AUDIO_SEC = 600
|
|
75
|
+
|
|
76
|
+
export type AiAvatarDurationBucket = (typeof AI_AVATAR_DURATION_BUCKETS)[number]
|
|
77
|
+
|
|
78
|
+
const ALLOWED_ENGINES = new Set<AiAvatarEngine>(["avatar-v", "avatar-iv"])
|
|
79
|
+
const ALLOWED_RESOLUTIONS = new Set<AiAvatarResolution>(["720p", "1080p", "4k"])
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Estimate script duration in seconds from character count and voice speed.
|
|
83
|
+
*
|
|
84
|
+
* Uses ~12 chars/sec at normal speed (voiceSpeed=1) — deliberately generous
|
|
85
|
+
* (biased high) so the credit hold covers TTS pacing variations. Slower speech
|
|
86
|
+
* (voiceSpeed < 1) produces a longer clip and thus a higher estimate.
|
|
87
|
+
* Minimum 1s.
|
|
88
|
+
*
|
|
89
|
+
* @param script Script text (undefined treated as empty).
|
|
90
|
+
* @param voiceSpeed HeyGen voice speed parameter, range 0.5–1.5 (default 1).
|
|
91
|
+
* Slower speed → more seconds → higher hold.
|
|
92
|
+
*/
|
|
93
|
+
export function estimateScriptDurationSec(script: string | undefined, voiceSpeed = 1): number {
|
|
94
|
+
const speed = Math.min(1.5, Math.max(0.5, voiceSpeed || 1))
|
|
95
|
+
return Math.max(1, Math.ceil((script?.length ?? 0) / 12 / speed))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pick the smallest duration bucket that is ≥ sec.
|
|
100
|
+
* Falls back to the maximum bucket (900s) when sec exceeds all buckets.
|
|
101
|
+
*/
|
|
102
|
+
export function pickAiAvatarBucket(sec: number): AiAvatarDurationBucket {
|
|
103
|
+
for (const b of AI_AVATAR_DURATION_BUCKETS) {
|
|
104
|
+
if (sec <= b) return b
|
|
105
|
+
}
|
|
106
|
+
return 900
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Composite credit identifier for a duration-bucketed reserve.
|
|
111
|
+
*
|
|
112
|
+
* Format: `"heygen-<engine>:<resolution>:<bucketSec>s"`
|
|
113
|
+
* e.g. `"heygen-avatar-iv:720p:60s"`.
|
|
114
|
+
*/
|
|
115
|
+
export function aiAvatarReserveCreditId(
|
|
116
|
+
engine: AiAvatarEngine,
|
|
117
|
+
resolution: AiAvatarResolution,
|
|
118
|
+
bucketSec: number,
|
|
119
|
+
): string {
|
|
120
|
+
return `heygen-${engine}:${resolution}:${bucketSec}s`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve the credit identifier from a raw request body BEFORE Zod validation.
|
|
125
|
+
* Called at creditGuard preHandler time — mirrors how `resolveLipSyncIdentifier`
|
|
126
|
+
* reads the raw body in `backend/src/routes/lip-sync.ts`.
|
|
127
|
+
*
|
|
128
|
+
* - `speechMode === "text"`: bucket by estimated script duration, accounting for
|
|
129
|
+
* voiceSpeed (slower → longer clip → larger bucket). Surplus is refunded at commit.
|
|
130
|
+
* - Any other speechMode (audio / missing): bucket by `__probedDurationSec` when
|
|
131
|
+
* the ffprobe preHandler stashed it on the raw body; otherwise a MODEST 120s
|
|
132
|
+
* default (AI_AVATAR_AUDIO_FALLBACK_SEC) — NOT the 900s top bucket. Reserving
|
|
133
|
+
* the 900s ceiling for a sub-minute audio clip was the over-reservation bug
|
|
134
|
+
* (~4000+ credits held for a clip that costs ~40). The commit-time metered
|
|
135
|
+
* true-up is refund-only, so a modest reserve never over-charges. The effective
|
|
136
|
+
* seconds are ALSO capped at AI_AVATAR_MAX_AUDIO_SEC (600s) — the worker trims
|
|
137
|
+
* audio longer than that, so a 30-min audio reserves the 600s bucket, never 900s.
|
|
138
|
+
*
|
|
139
|
+
* - `avatarSource === "image"`: image-source mode is IV-class (HeyGen's own
|
|
140
|
+
* engine, no avatar_id) — bill at the avatar-iv rate regardless of the
|
|
141
|
+
* (ignored) `engine` field, reusing the existing heygen-avatar-iv:* ids.
|
|
142
|
+
*
|
|
143
|
+
* Falls back to ("avatar-iv", "720p", 120s) when fields are missing or invalid.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveAiAvatarCreditId(
|
|
146
|
+
body: Record<string, unknown> | undefined,
|
|
147
|
+
): string {
|
|
148
|
+
const rawEngine = body?.engine as string | undefined
|
|
149
|
+
const rawResolution = body?.resolution as string | undefined
|
|
150
|
+
const speechMode = body?.speechMode as string | undefined
|
|
151
|
+
const avatarSource = body?.avatarSource as string | undefined
|
|
152
|
+
|
|
153
|
+
// Image-source mode is IV-class (its own engine, no IV/V lever) — pin the
|
|
154
|
+
// rate engine to avatar-iv and reuse the existing heygen-avatar-iv:* ids.
|
|
155
|
+
const engine: AiAvatarEngine =
|
|
156
|
+
avatarSource === "image"
|
|
157
|
+
? "avatar-iv"
|
|
158
|
+
: rawEngine !== undefined && ALLOWED_ENGINES.has(rawEngine as AiAvatarEngine)
|
|
159
|
+
? (rawEngine as AiAvatarEngine)
|
|
160
|
+
: "avatar-iv"
|
|
161
|
+
|
|
162
|
+
const resolution: AiAvatarResolution =
|
|
163
|
+
rawResolution !== undefined && ALLOWED_RESOLUTIONS.has(rawResolution as AiAvatarResolution)
|
|
164
|
+
? (rawResolution as AiAvatarResolution)
|
|
165
|
+
: "720p"
|
|
166
|
+
|
|
167
|
+
let bucketSec: AiAvatarDurationBucket
|
|
168
|
+
if (speechMode === "text") {
|
|
169
|
+
// voiceSpeed is a float in [0.5, 1.5]; coerce to number, default 1.
|
|
170
|
+
// Slower speed → more seconds → larger bucket → larger hold (no undercharge).
|
|
171
|
+
const voiceSpeed = Number(body?.voiceSpeed) || 1
|
|
172
|
+
bucketSec = pickAiAvatarBucket(
|
|
173
|
+
estimateScriptDurationSec(body?.script as string | undefined, voiceSpeed),
|
|
174
|
+
)
|
|
175
|
+
} else {
|
|
176
|
+
// Audio/unknown mode. The ffprobe preHandler (video-sfx pattern) stashes
|
|
177
|
+
// the measured clip length on the raw body as `__probedDurationSec` so we
|
|
178
|
+
// can bucket by the ACTUAL audio duration. When present, bucket by it;
|
|
179
|
+
// otherwise fall back to a MODEST 120s default — NOT the 900s top bucket.
|
|
180
|
+
// The old 900s fallback over-reserved ~4000+ credits for sub-minute clips.
|
|
181
|
+
//
|
|
182
|
+
// Effective seconds are capped at AI_AVATAR_MAX_AUDIO_SEC (600s): audio mode
|
|
183
|
+
// has no natural length limit, so the worker TRIMS the actual clip to ≤600s
|
|
184
|
+
// and we cap the RESERVE at the matching 600s bucket. A 30-min probed audio
|
|
185
|
+
// thus reserves the 600s bucket, never the 900s top bucket. Since the worker
|
|
186
|
+
// trims actual to ≤600s, the metered commit can only refund.
|
|
187
|
+
const probed = Number(body?.__probedDurationSec)
|
|
188
|
+
const effectiveSec = Math.min(
|
|
189
|
+
Number.isFinite(probed) && probed > 0
|
|
190
|
+
? Math.ceil(probed)
|
|
191
|
+
: AI_AVATAR_AUDIO_FALLBACK_SEC,
|
|
192
|
+
AI_AVATAR_MAX_AUDIO_SEC,
|
|
193
|
+
)
|
|
194
|
+
bucketSec = pickAiAvatarBucket(effectiveSec)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return aiAvatarReserveCreditId(engine, resolution, bucketSec)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* All 60 reserve credit IDs — every (engine × resolution × bucket) combination.
|
|
202
|
+
* 2 engines × 3 resolutions × 10 buckets (5/10/15/30/60/120/240/360/600/900s) = 60.
|
|
203
|
+
* Seeded in STATIC_CREDIT_COSTS and the model_pricing migration so
|
|
204
|
+
* getModelCreditBaseCost never hard-fails (503 price_not_configured) for any
|
|
205
|
+
* legal creditGuard input.
|
|
206
|
+
*/
|
|
207
|
+
export const AI_AVATAR_RESERVE_IDS: string[] = [...ALLOWED_ENGINES].flatMap((engine) =>
|
|
208
|
+
[...ALLOWED_RESOLUTIONS].flatMap((resolution) =>
|
|
209
|
+
AI_AVATAR_DURATION_BUCKETS.map((bucketSec) =>
|
|
210
|
+
aiAvatarReserveCreditId(engine, resolution, bucketSec),
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ancestor reference image collection — traverses upstream nodes to find
|
|
3
|
+
* image-producing ancestors, with passthrough for text/logic nodes.
|
|
4
|
+
* Shared between frontend and backend via a callback for extracting image URLs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { GenericNode, GenericEdge } from "./types.js"
|
|
8
|
+
|
|
9
|
+
// Node types that produce reference images
|
|
10
|
+
export const IMAGE_REF_TYPES = new Set([
|
|
11
|
+
"upload-image", "face", "character", "object", "creature", "location",
|
|
12
|
+
"generate-image", "edit-image", "image-to-image",
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
// Node types that pass through (don't produce images but connect to ancestors that do)
|
|
16
|
+
export const PASSTHROUGH_TYPES = new Set([
|
|
17
|
+
"ai-writer", "llm-chat", "split-text", "combine-text", "text-prompt", "list",
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Collect reference image URLs from upstream ancestor nodes.
|
|
22
|
+
* @param getImageUrl - Callback to extract an image URL from a node. Frontend uses
|
|
23
|
+
* extractNodeOutput(), backend uses nodeStates[id]?.output?.imageUrl.
|
|
24
|
+
*/
|
|
25
|
+
export function collectAncestorRefs<N extends GenericNode, E extends GenericEdge>(
|
|
26
|
+
nodeId: string,
|
|
27
|
+
nodes: N[],
|
|
28
|
+
edges: E[],
|
|
29
|
+
getImageUrl: (node: N) => string | undefined,
|
|
30
|
+
visited = new Set<string>(),
|
|
31
|
+
): string[] {
|
|
32
|
+
if (visited.has(nodeId)) return []
|
|
33
|
+
visited.add(nodeId)
|
|
34
|
+
const refs: string[] = []
|
|
35
|
+
const incoming = edges.filter((e) => e.target === nodeId)
|
|
36
|
+
for (const edge of incoming) {
|
|
37
|
+
const src = nodes.find((n) => n.id === edge.source)
|
|
38
|
+
if (!src) continue
|
|
39
|
+
if (IMAGE_REF_TYPES.has(src.type)) {
|
|
40
|
+
const url = getImageUrl(src)
|
|
41
|
+
if (url?.trim()) refs.push(url.trim())
|
|
42
|
+
}
|
|
43
|
+
if (PASSTHROUGH_TYPES.has(src.type)) {
|
|
44
|
+
refs.push(...collectAncestorRefs(src.id, nodes, edges, getImageUrl, visited))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return refs
|
|
48
|
+
}
|