Skip to content

feat(web): show render and voicing cost estimates before the click - #5400

Merged
georgi merged 2 commits into
mainfrom
claude/render-cost-estimates-9a623f
Aug 30, 2026
Merged

feat(web): show render and voicing cost estimates before the click#5400
georgi merged 2 commits into
mainfrom
claude/render-cost-estimates-9a623f

Conversation

@georgi

@georgi georgi commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

What changed

The storyboard's Render stills / Render clips buttons and the Script editor's Voice all each fire N generations at once, and none of them said what that would cost — the per-shot inspector answered the question one shot at a time while the batch buttons spend many times that in one click. Each button now carries its own figure in its label, priced over exactly the items it loops (clips shot by shot at their effective duration, stills at one rung times the batch, voicing by the characters of the lines that are actually unvoiced), with the breakdown and any catalog caveats in its tooltip. A batch nothing prices keeps its plain label and says why in the tooltip, because a "$0" beside a billed call is worse than no number.

Speech pricing had to be fixed first. The 22 text-to-speech rows in the GenSpend catalog are billed 1m_chars — ElevenLabs' $100 per million — and 6 more 1m_tokens, and neither unit was ever converted: read as a per-run figure, voicing one line quoted $100. Character units are now multiplied by the text length the caller states and decline when none was given; token units decline outright, since a speech model's tokens are the audio it produced and no text length converts into them. That also corrects the chat composer's audio mode, which was showing the same inflated figure.

Two helpers were extracted so a quote cannot drift from the click it describes: priceRenderStep out of useShotCostEstimate, so the inspector and the toolbar cannot price one shot two ways, and voiceTargets out of voiceAll, so the estimate and the action agree on which lines get voiced.

Verification

  • npm run test:affected — one fully green pass (96/96 turbo tasks; web 1221 suites / 13868 tests; electron 63 suites / 686 tests). Two later runs each hit one exports all public API import timeout, in a different package each time (@nodetool-ai/chat 60s, @nodetool-ai/deploy 5s) under parallel load; both pass standalone (chat 30/30, deploy 866/866). packages/image-nodes needs the mesa-vulkan-drivers ICD this container lacks by default (AGENTS.md § WebGPU on a headless machine); with VK_DRIVER_FILES pointed at lavapipe it is 229/229.
  • npm run typecheck — web and electron clean. Mobile reports missing react-native/expo modules, which is this container having no mobile/ install, not the diff.
  • npm run lint — exit 0, no errors.
  • npm run dev:nodetool -- harness gate --base mainGate: 8/8 selfchecks passed.

New tests: 5 pricing cases in packages/model-pricing/tests/model-pricing.test.ts, plus useRenderBatchCostEstimate (5), useVoiceCostEstimate (6), ScriptDocumentPane.voiceCost (3), and a priced-toolbar case in StoryboardBoard.test.tsx.

New checks

The character/token pricing rules were inverted once and observed failing — the routing in isParameterPriceable was disabled and 1m_chars removed from the scalar table:

 × prices a speech model by the characters it will synthesize
 × declines a character-billed model when no text length is given
 Tests  2 failed | 152 passed (154)

Restored, the suite is 154/154. The tests assert a real catalog row (elevenlabs:eleven_multilingual_v2, unit_class: "per-1m-chars") rather than a fixture, so they cannot pass by matching nothing.

Agent capabilities

No capability added, and no capability's declared contract changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_0135cCcEMiX3FFEoLNmaCHYK


Generated by Claude Code

claude added 2 commits August 30, 2026 17:18
The storyboard's *Render stills* / *Render clips* buttons and the Script
editor's *Voice all* fire N generations at once, and none of them said what
that would cost. The per-shot inspector already answered the question one shot
at a time; the batch buttons spend many times that in one click.

Each button now carries its own figure, priced over exactly the items it loops,
with the breakdown and any catalog caveats in its tooltip. A batch nothing
prices keeps the plain label and says why — a "$0" beside a billed call is
worse than no number.

Speech pricing had to be fixed first. The 22 text-to-speech rows in the
GenSpend catalog are billed `1m_chars` (ElevenLabs' $100 per million) and the
6 `1m_tokens` rows likewise, and neither unit was converted: read as a per-run
figure, voicing one line quoted $100. Character units are now multiplied by the
text length a caller states and decline when none was given; token units
decline outright, because a speech model's tokens are the audio it produced and
no text length converts into them. This also corrects the composer's audio
mode, which showed the same inflated figure.

- `priceScalarUnit` and `priceGenspendEntry` learn character and token units;
  `ModelPriceParams` gains `characters`
- `priceRenderStep` extracted from `useShotCostEstimate`, so the inspector and
  the toolbar cannot quote one shot two different prices
- `voiceTargets` extracted from `voiceAll`, so the quote and the click agree on
  which lines get voiced
- clips are priced shot by shot at their effective duration; stills are one
  rung times the batch
`keys the cache by package+path` loaded `@nodetool-ai/fal-nodes`'s
`fal-manifest.json` and asserted the catalog was non-empty. That file only
exists in fal-nodes' `dist/`, and fal-nodes *depends on* runtime — so turbo's
`test` dependsOn `^build`, which builds a package's dependencies and never its
dependents, cannot order that build before this test. On a `--affected` CI run
that reached runtime the manifest was simply not on disk yet and the catalog
came back empty.

`providers/aki-manifest.json` is registered to `@nodetool-ai/runtime` itself
and yields three image models, so the same assertion holds with no
cross-package ordering to lose.

Reproduced both ways: hiding fal-nodes' built manifest fails the old test with
the identical `expected 0 to be greater than 0`, and hiding runtime's own
manifest still fails the new one, so the assertion cannot pass by matching
nothing.
@georgi
georgi enabled auto-merge August 30, 2026 17:52
@georgi
georgi merged commit 56c1cf8 into main Aug 30, 2026
22 checks passed
@georgi
georgi deleted the claude/render-cost-estimates-9a623f branch August 30, 2026 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants