feat(web): show render and voicing cost estimates before the click - #5400
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 more1m_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:
priceRenderStepout ofuseShotCostEstimate, so the inspector and the toolbar cannot price one shot two ways, andvoiceTargetsout ofvoiceAll, 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 oneexports all public APIimport timeout, in a different package each time (@nodetool-ai/chat60s,@nodetool-ai/deploy5s) under parallel load; both pass standalone (chat 30/30, deploy 866/866).packages/image-nodesneeds themesa-vulkan-driversICD this container lacks by default (AGENTS.md § WebGPU on a headless machine); withVK_DRIVER_FILESpointed at lavapipe it is 229/229.npm run typecheck— web and electron clean. Mobile reports missingreact-native/expomodules, which is this container having nomobile/install, not the diff.npm run lint— exit 0, no errors.npm run dev:nodetool -- harness gate --base main—Gate: 8/8 selfchecks passed.New tests: 5 pricing cases in
packages/model-pricing/tests/model-pricing.test.ts, plususeRenderBatchCostEstimate(5),useVoiceCostEstimate(6),ScriptDocumentPane.voiceCost(3), and a priced-toolbar case inStoryboardBoard.test.tsx.New checks
The character/token pricing rules were inverted once and observed failing — the routing in
isParameterPriceablewas disabled and1m_charsremoved from the scalar table: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