0.13.0
2026-09-03What's new
Brand Kit and Theme are out of a video's belly, now two separate items in the sidebar. Before this release, editing a Brand Kit meant opening or creating a video, going into Studio Editor, switching to the "Whole video" tab, then scrolling to the Theme card — four steps down into a single video just to change something every video shares. And because a kit could only be generated by capturing the colors of whichever storyboard happened to be open, no video meant no kit. Now it opens straight from the sidebar, and you can build a kit from scratch.
A kit no longer holds colors itself — it picks a Theme. Two ideas that used to be tangled together now split cleanly: a Theme is two colors, one font, and a footage color-grading recipe; a Brand Kit is an identity — logo, brand name, badge, voice, caption style. Along with that, the 24 built-in themes go from being an endpoint to a starting point: duplicate one and edit it, the original stays untouched (an old draft that has already stamped a given theme has to keep rendering in that exact color forever, so it must never be overwritten). And for the first time, your own palette is something you can save — before, keeping your own colors meant one of two paths: retype them on every single video, or wrap them into a "kit" and accept that from then on "kit" meant "theme."
The preview pane is a real frame, not an impression of one. It runs through the exact build pipeline and the exact template used when a video is actually exported, so the color, font, and logo you see are the color, font, and logo you get. Three samples, not one, because one sample doesn't say enough: footage color-grading is a filter laid over background footage — with no footage there's nothing to see; and whether captions stay readable over B-roll can't be answered without B-roll.
When a video drifts from its kit, the app says so, and opens both ways back. Apply a kit and then nudge one color, and the video is now silently different from the very kit it claims to use — before, nothing flagged this, and there was no path either way: a kit couldn't be updated from a video, and a video couldn't be repainted back to its kit. Now there's a warning banner with two choices: update the kit from this video (applies to future videos, and sits outside Undo) or save as a new kit. The 24 built-in themes only get the second option, for the exact reason above.
The app now has accounts: starting with this release, signing in with Google is mandatory. The first-run onboarding screen adds a sign-in step and a short survey — are you a shop, a creator, or an agency, which industry, do you already have an LLM key. People upgrading from an older version will hit it too: "Signed in with Google" is now a mandatory item in the readiness checklist on the Create screen. Along with the account come three events sent to the server — video created, video exported, render error — and for render errors specifically, only the error type name is sent, never the message text, because that text usually carries a path from your own machine.
Videos exported on the Free plan carry a 9Video watermark in the corner. The in-app preview doesn't — the mark is only stamped at export time. Settings → Plan shows which plan you're on and has a button to request an upgrade to turn the watermark off; Pro/Agency accounts also get their own badge in the sidebar.
The last release promised numbers next time. Here they are. Measured from the app's own logs — 184 failed scene builds — contrast accounts for 106 of them (58%), more than double the second most common cause; and of 338 contrast failures, 153 fell below 1.6:1, meaning the text was effectively invisible, not merely a near miss. Three root causes were fixed in this release:
- The message sent back to the AI now calls its own two colors by name. Before, it only received "contrast 2.33:1" plus a CSS selector path — neither side mentioned an actual color, so the AI had to guess which of its own declarations produced that ratio, and it usually burned through every retry without getting there.
- A progress bar drawn at zero width and then scaled up with scaleX stays invisible forever — zero times anything is still zero, and it was invisible in the exact exported mp4 file, not just in the preview. Across the draft library, 29 of 308 scenes carried this bug — narrowed to the newest 71 scenes, it was 5. The app now catches this directly in the scene's source code, without spending a browser-opening round-trip, and points straight at a version that works.
- A double quote nested inside a style attribute cut a color declaration short: every app-supplied font except Inter has a space in its name, so the AI would write
font: 600 26px "Playfair Display"the way it's written everywhere else — and the HTML attribute ends right at that quote mark. On one real video, a scene lost its text color and then failed with "contrast 1.39:1" three rounds in a row. The app now patches this right before building, so drafts already saved are rescued too, not only newly generated scenes.
One more reminder added for the AI: z-index can't negotiate a space that's too tight. A real scene placed three 280px side cards next to a 900px main card inside a 1080px-wide frame — there was never going to be room, so they kept covering the main card and burying the title. Three rewrite rounds only spun the z-index and swapped colors, because nothing had told it that z-index only decides who sits on top once things already overlap — it can't stop the overlap from happening.
Vietnamese narration mixes in less English. The edge-tts vi-VN voice mispronounces English words sitting in the middle of a sentence. An SSML-based fix was tried and it broke synthesis entirely on the free endpoint the app uses, so this time the only path that worked was instructing the script-writing prompt directly.
A few smaller things, but ones people run into often. Draft content in the Create screen no longer gets lost when you step over to the Brand Kit page and come back — topic, aspect ratio, length, voice, kit, recipe, and attached files all survive; it's the Brand Kit page that invited you on that trip, so it's the one responsible for the way back. Two 9Video windows no longer run side by side on the same profile — before, two processes shared one settings.json (API keys, Brand Kit, theme), whoever wrote last won, and no care taken inside the app could save you from that. A hung sign-in now times out after 90 seconds instead of getting stuck forever on an onboarding screen that has no skip button. And the very dialog you're reading right now is new: it tells you what's new after every update, and you can revisit it from Settings.
Still not measured: how many re-render rounds the three scene fixes above actually save on real videos. The numbers above say how common each bug was, not yet how much the fix claws back — that's a question left for the next release.
Features
- brand-kit: make a kit creatable without a video, and divergence detectable (db99503)
- brand: Brand Kit form picks a Theme instead of raw colour/font fields (d720011)
- brand: build the three preview samples out of the real render templates (5f9df7a)
- brand: edit a kit next to the frame it produces (11e5296)
- brand: give brand kits and themes a place of their own in the app (e80de4b)
- brand: give brand kits and themes one owner in the renderer (ffd5977)
- brand: let a chosen theme reach the first build, not just the editor (f49fddd)
- brand: make the 24 built-in themes a starting point instead of a dead end (94c2eb5)
- brand: preview a kit or theme as the frame it actually renders as (57cb6ce)
- desktop: add AuthController for Google login state machine (c76af20)
- desktop: add LoginStep component and i18n copy (705aae8)
- desktop: add me:upgradeWaitlist IPC handler (c3aaf0f)
- desktop: add plan section to Settings (18b0852)
- desktop: add plan to AppSettings (0bf76f6)
- desktop: add Plan type and shouldWatermark fail-closed gate (c4529de)
- desktop: add renderWatermarkOverlay (8b82d13)
- desktop: add required googleAuth readiness check (188038a)
- desktop: add serverClient — getProfile/completeOnboarding/recordEvent (34270d7)
- desktop: add SurveyStep component and i18n copy (13f9615)
- desktop: add syncPlanStatus (7acb034)
- desktop: add telemetry event names and extend onboarding to 5 steps (adb920f)
- desktop: add UserProfile.plan and requestUpgradeWaitlist() (bf5bd82)
- desktop: dev-only skip for onboarding's login+survey steps (f5fb742)
- desktop: package CHANGELOG.md and expose it over IPC (1002fb0)
- desktop: expose auth/onboarding-survey IPC methods on window.api (9553512)
- desktop: expose plan status + upgrade waitlist on window.api (7f8a728)
- desktop: gate the watermark on plan at render time (4483a18)
- desktop: add a what's-new modal shown after an update (5827ff8)
- desktop: name the failing token pairing in the re-author hint (cdc130d)
- desktop: parser for CHANGELOG.md and the rules for showing the what's-new modal (c8da8be)
- desktop: record video_created/video_exported/render_error telemetry (38fe2a5)
- desktop: register ninevideo:// protocol, wire AuthController and auth/onboarding IPC (cc5d3c1)
- desktop: show a plan badge in the sidebar for pro/agency (580337a)
- desktop: sync plan on login/startup, close settings:save plan hole (4669ce1)
- desktop: wire mandatory login + survey steps into onboarding flow (945d090)
- editor: say when a video has drifted from its kit, and offer both ways out (38b19fe)
- nav: route Brand Kit and Theme to separate pages (9bdc00f)
- nav: split the "Brand" rail item into Brand Kit and Theme (ef698d6)
- server: add activation rate, DAU, and D7 retention KPI queries (4592cd1)
- server: add AdvanceDripStep command handler with catch-up semantics (889a099)
- server: add BullMQ drip-emails queue and processor (6874295)
- server: add CaptureLead command and POST /leads for landing-page capture (f632afa)
- server: add CompleteOnboarding command handler and OnboardingCompletedEvent (fa5e852)
- server: add DripCampaign aggregate with D0/D3/D7/D14/D21 schedule logic (efd417a)
- server: add GET /telemetry/kpis (activation, DAU, D7 retention) (3fcf06e)
- server: add GetUserProfile query handler (7f5206c)
- server: add Google OAuth sign-in, JWT auth guard, and /me endpoints (2b0c610)
- server: add grant-plan manual plan-grant script (61c30dd)
- server: add hourly cron scan that enqueues drip campaigns with a step due (1977ef9)
- server: add IDripCampaignRepository port, MikroORM adapter, and drip_campaign migration (b83820c)
- server: add ITelemetryEventRepository port, MikroORM adapter, and telemetry_event migration (1686e18)
- server: add IUserRepository port, MikroORM adapter, and users table migration (dceee8d)
- server: add Lead aggregate, repository, and migration (df9fd23)
- server: add Lead.userId and the upgrade_waitlist source (759aa0c)
- server: add MailPort, Resend adapter, and drip email templates (1e18a7f)
- server: add plan field and grantPlan() to User (3c99d13)
- server: add POST /me/upgrade-waitlist (210a941)
- server: add RecordEvent command handler and UserSignedUp-to-telemetry subscriber (e7b7883)
- server: add shared-kernel Result type and Email value object (71d761e)
- server: add SignInWithGoogle command handler and UserSignedUpEvent (7881872)
- server: add TelemetryEvent aggregate and event-name enum (976d3f9)
- server: add User aggregate and OnboardingSurvey to Identity domain (bac19c3)
- server: capture a Lead when UpgradeWaitlistRequestedEvent fires (d0db899)
- server: CaptureLeadCommand accepts and backfills userId (2192c21)
- server: compose IdentityModule and register it in AppModule (8886833)
- server: compose TelemetryModule and add POST /telemetry/events (c33750d)
- server: enable CORS for POST /leads and graceful shutdown hooks (695f8b8)
- server: migrate users table for plan column (6c5fa11)
- server: RequestUpgradeWaitlistCommand publishes UpgradeWaitlistRequestedEvent (4338068)
- server: return plan from GET /me (0a170a5)
- server: scaffold NestJS app with health endpoint (d9dc214)
- server: start DripCampaign on OnboardingCompleted and convert Lead on UserSignedUp (e246354)
- server: validate required env vars with Joi at boot (3d209e3)
- server: wire MikroORM + Postgres/Redis docker-compose, add DB health check (4f0dadf)
- settings: persist the user's own themes; delete a field that documents a guard that isn't there (af8b236)
- theme: let a user's own palette be a saved thing, not a per-video re-edit (2ec0d3f)
- theme: stop the theme matcher from claiming a preset it only coincides with (7e3ce8d)
Bug Fixes
- add server/.env.example and unignore .env.example pattern (c145c20)
- auth: suppress orphaned-promise rejection, guard getStatus() decrypt throw (8fd485f)
- brand-kit,theme: add fallback names when sanitized name is blank (abb465d)
- brand-kit: clear meta.brandKitId when a theme or preset is applied over a kit (5d3b7c1)
- brand-kit: strip a stale kit id from drafts written before the source fix (d51679b)
- brand: close final-review findings — persist migration, warn on theme delete (5fca335)
- brand: close the pre-merge review's six findings on the Brand Kit page (da664c2)
- brand: compute Brand Kit saves against current state, not a stale render closure (ab7d59e)
- brand: re-measure preview on resize, fit the page to the viewport (997d53a)
- brand: revert the preset picker to a list, make side columns responsive (3618659)
- brand: stack preview+form below a container-query breakpoint (b237190)
- brand: stop clipping the right column and match the editor's inspector polish (cd7de58)
- brand: stop the kit branch from outliving a theme/preset applied over it (63e8265)
- build: make SERVER_URL genuinely build-time overridable (5ede57a)
- build: register ninevideo:// URL scheme in packaged builds (e32ad26)
- clarify telemetry error handling in comment (6535ddc)
- composer: reframe the Brand Kit chip as "Diện mạo" (Look), not identity (4f9c983)
- create: drop the colour preview from the Recipe chip entirely (8870a29)
- create: make the Brand Kit page's theme pick one-shot (689cd48)
- create: preserve staged composer state across Create navigation (72fc280)
- create: resolve the Brand Kit chip's swatch through the kit's theme (6cc587b)
- create: tint the recipe preview swatches with the active Brand Kit (66a5735)
- crm: establish MikroORM RequestContext for cron/queue call sites (5b9a135)
- crm: prevent BullMQ unhandled 'error' crashes in drip-emails queue (1c07b3a)
- crm: retry drip-emails jobs with exponential backoff (b747dc4)
- desktop,server: guard cold-start getSecret, fix TDZ, unify plan gate, add upgrade-waitlist e2e coverage (48508a8)
- desktop: add missing create.readiness.items.googleAuth i18n copy (0d41dc6)
- desktop: address final-review nits on the Vietnamese TTS prompt tweak (abc49dc)
- desktop: enlarge free-tier watermark overlay for visibility (d34c56b)
- desktop: fail a tween aimed at an element the html gives no size (d3fdcb5)
- desktop: keep DevTools closed by default in dev (a52a72c)
- desktop: accept em-dash headings in CHANGELOG.md and drop the hardcoded latest-entry in the test (ebf2b63)
- desktop: nudge Vietnamese narration away from unnecessary English words (90a952b)
- desktop: repair a double quote nested in an inline style attribute (f3de263)
- desktop: scale watermark overlay to match render resolution (ad93da2)
- desktop: stop render_error telemetry from leaking local paths (da7a7c5)
- desktop: tell the scene author z-index cannot fix an overlap (a4bdc07)
- desktop: time out a hung Google login attempt (3592970)
- desktop: update AuthController comment and test fixtures to the corrected scheme (9e34fde)
- desktop: wrap auth/onboarding-survey IPC errors in AppError (36cd76f)
- editor: extract brand-drift arbitration into a tested divergenceState (6be89d9)
- editor: saveVideoAsKit reads the freshly-built themes list, not a stale closure (4ffe653)
- editor: update InspectorVideo's kit/theme call sites for themeId kits (b330e58)
- generate: resolve a selected Brand Kit's colours via its linked theme (71c8379)
- main: add Electron's single-instance lock (9777129)
- main: guard recordTelemetry sync getSecret throw (2c61bc6)
- replace convoluted type with simple TestingModule annotation (8b48c14)
- server: encapsulate onboarding field and validate email in User entity (a572b26)
- server: fix docker-compose api connectivity to postgres/redis (d1b0e2e)
- server: map domain errors to HTTP status codes (ec4cba0)
- server: pin exact versions and add ESLint config (f7ab2cd)
- server: restore full typecheck coverage for api-frontend and shared-kernel (6dd412a)
- server: run migrations against the built dist output, not TS sources (674d172)
- server: type QueryBus.execute and log errors in event subscribers (378683e)
- server: use a syntactically valid custom URL scheme for the OAuth callback (7d651f5)
- settings: migrate legacy colour-owning brand kits to themeId references (40026f0)
Performance Improvements
- telemetry: add indexes on telemetry_event for KPI queries (4a138f6)