Skip to content

chore(deps): bump cryptography from 49.0.0 to 50.0.0 in the pip group across 1 directory - #17

Merged
Davut Akça (DavutAkca) merged 42 commits into
mainfrom
dependabot/pip/pip-18c674f953
Aug 6, 2026
Merged

chore(deps): bump cryptography from 49.0.0 to 50.0.0 in the pip group across 1 directory#17
Davut Akça (DavutAkca) merged 42 commits into
mainfrom
dependabot/pip/pip-18c674f953

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 5, 2026

Copy link
Copy Markdown
Contributor

Bumps the pip group with 1 update in the / directory: cryptography.

Updates cryptography from 49.0.0 to 50.0.0

Changelog

Sourced from cryptography's changelog.

50.0.0 - 2026-07-31


* **SECURITY ISSUE**:
  :func:`~cryptography.hazmat.primitives.serialization.pkcs7.pkcs7_decrypt_der`
  and its PEM and S/MIME variants no longer expose distinguishable errors or
  timing when unwrapping a ``RecipientInfo``'s ``encryptedKey``, which could
  act as a Bleichenbacher oracle for callers that decrypt untrusted messages.
  A random key is now substituted on failure, as described in :rfc:`3218`.
  Credit to **@X1AOxiang** for reporting the issue. **CVE-2026-69247**
* Deprecated Diffie-Hellman key exchange over finite fields (FFDH).
  Everything FFDH is deprecated, including the types in
  ``cryptography.hazmat.primitives.asymmetric.dh`` and loading FFDH keys or
  parameters with the key loading APIs. Users should migrate to a more
  modern key exchange algorithm.
* Added ``xof()`` class methods to
  :class:`~cryptography.hazmat.primitives.hashes.SHAKE128` and
  :class:`~cryptography.hazmat.primitives.hashes.SHAKE256` for constructing
  algorithm instances configured for use with
  :class:`~cryptography.hazmat.primitives.hashes.XOFHash`.
* The :mod:`X.509 verification <cryptography.x509.verification>` APIs are now
  considered stable and are subject to our API stability policy.
* Added the :doc:`/cobblestone` recipe, an implementation of the
  Cobblestone-128 and Cobblestone-256 instantiations of the `C2SP
  chunked-encryption specification
  <https://c2sp.org/chunked-encryption>`_ for streaming authenticated
  encryption of large messages.
* Parsing a Signed Certificate Timestamp list now rejects encodings that
  carry trailing bytes after the list or after an individual SCT, instead of
  silently ignoring them.
* Added support for using :class:`~cryptography.x509.Name` as a field type in
  the :doc:`/hazmat/asn1/index` module.
* Loading a public key or an EC private key now rejects DER where the
  ``subjectPublicKey`` (or EC ``publicKey``) ``BIT STRING`` declares a non-zero
  number of unused bits, instead of silently ignoring it.
* Parsing a CRL entry's ``InvalidityDate`` extension now rejects a
  ``GeneralizedTime`` that carries fractional seconds or another non-DER form,
  matching the strict encoding already required for every other X.509 time
  field.
* :func:`~cryptography.x509.ocsp.load_der_ocsp_request` and
  :func:`~cryptography.x509.ocsp.load_der_ocsp_response` now reject a request
  or response whose ``version`` field is not ``v1``, the only version defined
  by RFC 6960, matching the version validation already performed when loading
  certificates, CSRs and CRLs.
* :class:`~cryptography.hazmat.primitives.hashes.XOFHash` is now supported
  when building against AWS-LC.
* HMAC (and therefore PBKDF2-HMAC) with SHA-3 hashes is now supported when
  building against AWS-LC.
* Diffie-Hellman (:doc:`/hazmat/primitives/asymmetric/dh`) is now supported
  when building against AWS-LC.
</tr></table> 

... (truncated)

Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore <dependency name> major version will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
  • @dependabot ignore <dependency name> minor version will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
  • @dependabot ignore <dependency name> will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
  • @dependabot unignore <dependency name> will remove all of the ignore conditions of the specified dependency
  • @dependabot unignore <dependency name> <ignore condition> will remove the ignore condition of the specified dependency and ignore conditions
    You can disable automated security fix PRs for this repo from the Security Alerts page.

Davut Akça (DavutAkca) and others added 30 commits July 26, 2026 11:11
Public history was squashed to a single commit today, so the entries
grandfathering docs/INSTALL_SAAS.md and store-listings/ in the old
history no longer apply to anything — there is no pre-squash history
left in the public repo to grandfather. Left as empty containers (not
deleted) so a future genuinely-accepted history leak has somewhere to
go without re-adding the machinery.
Reworded the title and body to read as a heads-up rather than a legal
consent notice ("Toplantı modu — onay" -> "Toplantı modu hakkında" /
"Meeting mode — consent" -> "About Meeting mode"), matching the tone
of the rest of the onboarding copy.
The loopback listener used for the browser-relay Google/email sign-in
reflected any request's Origin header back verbatim in
Access-Control-Allow-Origin, letting any site open in the user's
browser make cross-origin requests to it during the listener's
lifetime. Only the exact voxislive.com origin is allowed now.
… majors

Clears the Node.js 20 deprecation warning GitHub Actions annotates on
every run — the pinned v4/v5 releases predate each action's Node 24
runtime upgrade.
The repo moved from the personal DavutAkca account to the VoxisLive
org; update hardcoded clone URLs, badges, and issue/advisory links
across the READMEs and .github/ templates accordingly.
Weekly automated version-bump PRs for Python dependencies and the
Actions used in CI, instead of relying on manual bumps.
Bumps [onnxruntime](https://github.com/microsoft/onnxruntime) from 1.27.0 to 1.28.0.
- [Release notes](https://github.com/microsoft/onnxruntime/releases)
- [Changelog](https://github.com/microsoft/onnxruntime/blob/main/docs/ReleaseManagement.md)
- [Commits](microsoft/onnxruntime@v1.27.0...v1.28.0)

---
updated-dependencies:
- dependency-name: onnxruntime
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [google-genai](https://github.com/googleapis/python-genai) from 2.8.0 to 2.14.0.
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](googleapis/python-genai@v2.8.0...v2.14.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-version: 2.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [scipy](https://github.com/scipy/scipy) from 1.17.1 to 1.18.0.
- [Release notes](https://github.com/scipy/scipy/releases)
- [Commits](scipy/scipy@v1.17.1...v1.18.0)

---
updated-dependencies:
- dependency-name: scipy
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Applied by hand — the Dependabot PR (#6) hit a merge conflict against
requirements.txt after the other three same-day dependency PRs landed
first.
requirements.txt already had google-genai 2.14.0, numpy 2.5.1,
onnxruntime 1.28.0, and scipy 1.18.0 from the recent Dependabot merges,
but CI installs from requirements.lock (--require-hashes), which was
still pinned to the old versions — so the "passing" CI on those PRs
never actually exercised the new dependency code paths. Regenerated
with pip-compile and verified locally: full pytest suite (401 tests)
and ruff (app tests scripts) both pass against the real new versions.
numpy 2.5.1 and scipy 1.18.0 both require Python >=3.12, so pip could
not even install them under the 3.11 test job (confirmed on CI: "ERROR:
Could not find a version that satisfies the requirement numpy==2.5.1").
Held both back to their prior versions and left a comment explaining
why, so a future Dependabot PR for either doesn't get merged blind
again. google-genai and onnxruntime bumps are unaffected and stay.
… expires

/auth/quota only reads auth-core's token cache (5-minute TTL, not extended on
read) — only /auth/verify repopulates it — so the endpoint 401s once per TTL by
construction. get_quota() mapped that 401 to None, and the UI keeps its previous
QUOTA whenever a poll yields nothing, so the counter silently froze at a stale
number instead of counting down. A user watching a 150-minute film read "60 min
left" while the balance actually reached zero and the session was cut mid-film,
with the one signal that would have warned him never moving.

get_quota now does what report_usage has always done with its own identical 401:
re-verify once. /auth/verify answers with the same snapshot shape, so its body is
the refreshed quota. When it declines to answer — 402, a paid tier that is
genuinely exhausted — resolveEntry has still re-cached the entry on the way to
that 402, so one retry of the read yields the exhausted snapshot, which is
exactly the number the counter must show.

Only a FAILED re-verify is throttled (30s): a successful one warms the cache for
a full TTL and cannot recur per-beat, whereas a failing one (PocketBase down)
would otherwise turn a 6-second poll into three requests every six seconds
against an already-sick server.
base_translator.py did not import logging at all. Every rotation, reconnect,
backoff and watchdog trip existed only as an on-screen status line pushed
through on_status — nothing reached voxis.log and nothing reached the saved
transcript. A field report of "it reconnected once mid-meeting and the audio
cut" therefore had no evidence anywhere: a 63-minute session runs roughly four
13-minute rotations and not one of them left a trace.

Log once per occurrence, off the audio path, at the points that decide what the
session does next: a successful connect (with target + model), a planned
rotation, each transient failure with its counter and backoff, the terminal
classification that ends the reconnect loop, both watchdog trips (stall and
no-output) with the measured idle time, and _give_up — including whether a
substitute engine took over, which is the difference between a degraded session
and a dead one.
Every response.*.done cleared _out_acc. The next response's first delta then
extended an EMPTY accumulator, so _delta returned its text bare and webui's
`_cur_line += text` glued it straight onto the previous sentence. A measured
session rendered "Bir dakika bekleyelim.Bu arada,Eğer giderseniz" — 211
occurrences across 131 of 525 turns (25%), and they land in the exports too,
where one of them produced a 46-character unbreakable token in an SRT cue.

_delta already handled the other reset path correctly: the "unrelated text"
branch prepends a space. What was missing is that an EXPLICIT reset is the same
kind of boundary. _boundary_pending records it, and the next increment that
actually carries text leads with a separator (never doubling one Qwen already
sent). A no-growth event leaves the boundary pending rather than spending it.

A rotation counts as a boundary too: _reset_session_state clears the
accumulators but not the caption line downstream, so the new session's first
increment continues an open sentence. Harmless on the very first session — a
leading space is stripped when the turn finalizes.

Tests drive the real event shape (delta…delta, done, delta…) and assert the
exact string the field transcript got wrong; also pins the no-growth,
double-space and rotation cases, plus the lifecycle logging added previously.
render_srt/render_vtt wrote the cue body exactly as stored. A simultaneous
engine that never pauses long enough to split a turn produces very long ones, so
482 of 525 cues in a measured session exceeded the conventional 42-character
line and the longest was a single 548-character line no player can display.
_wrap folds on word boundaries and never truncates — an over-long word is left
intact rather than broken mid-token. Re-rendering that session's record gives
834 over-long lines -> 1 (a merged token the previous commit removes at source),
max 548 -> 46 characters, and zero words lost. Only the cue path wraps; the TXT
export keeps its long lines, where they are not a problem.

build_record also gains an optional `events` list — the engine lifecycle now
logged by base_translator, so a saved transcript can answer "did it drop at
16:31?" instead of the answer scrolling past in the UI. Schema-additive: the key
is omitted entirely when nothing happened, so an uneventful session serializes
byte-identically to every record written before this existed.

Turns gain an optional `leg` (which side of a meeting spoke) and exports prefix
a two-way record with the localized side name on every turn — the sides
alternate constantly, so an omitted tag would read as "same side as before"
exactly when it is not. Video/Game records carry no leg and render unchanged.
ModeController._build handed BOTH meeting pipelines the same on_text, and the
Bridge kept one set of transcript buffers. The lock around _on_text kept those
buffers consistent, but it could not keep the two CONVERSATIONS apart: the other
party's translation and the user's own interleaved inside a single caption line
and landed in the record indistinguishable from each other. Never seen in
Video/Game, which only ever runs one direction.

_text_sink binds a side to each pipeline and _LegState gives each its own
caption line, source queue, turn start and re-speak guard. The turn list stays
shared — the record is one chronological timeline — and is re-sorted by offset
at flush so a side that was mid-utterance at stop still lands in order. Turns
carry `leg`, rendered as a "Them:"/"Me:" prefix; the UI opens a new bubble on
every side change. Speaker labels stay incoming-only (the tracker hears only the
capture), and the overlay and OBS file — single-line surfaces — keep showing the
other party rather than the user's own words.

Billing is deliberately untouched. The server bills meeting 1:1 on purpose
(MeetingBillingMultiplier) because two sessions cost ~1.0-1.2x, not 2x: output
audio dominates and total speech is conserved across turn-taking. That multiplier
keys off the exact `source` string, so surfacing the second leg there would move
money; session_live carries `two_way` instead.

Also in the caption path, from the same session audit:

* The transcript timeline now starts at the session, not at the first translated
  token. A session that began before anyone spoke shifted every exported cue
  against its own WAV — 7m46s in the measured case. Sharing the anchor with the
  session-folder stamp also makes the JSON's `started` match the folder it sits in.
* LINE_GAP alone never fires on a continuously-streaming engine, so one turn
  absorbed 20+ seconds of speech. Past a length or time budget the stream splits
  at the next sentence end, with a hard ceiling for a run-on that never punctuates.
* The re-speak guard matched exact text, so a regenerated echo with a leading
  connective landed as a second turn that was never spoken aloud. It now matches
  on a normalized form, in the streaming path and the stop-time flush alike.
  Short repeats still survive — "Evet." twice is dialogue.

Video/Game output is unchanged: no leg key, no prefix, byte-identical records.
`l` reads as `1` in most fonts, which is why ruff rejects it (E741). Caught by
CI's static-analysis step, not by the local test run.
Smart mode already stops streaming after roughly 1.5 seconds of silence, so a
quiet room sends nothing upstream. Accrual, though, runs on wall clock: a
session joined before the meeting starts bills the wait. In a measured 63-minute
session that was 7m46s of digital silence — about 12% of the session — during
which nothing was sent and nothing was translated.

The gate now records when it last decided a frame was speech, and the heartbeat
skips accrual once every capture source has been quiet for a minute. Meeting
needs BOTH sides quiet, so the minimum idle wins. The threshold sits far above
any conversational pause; only an empty room, a call on hold or a paused video
reaches it. Resumption is immediate on the next speech frame and nothing is
deferred — _consume_minutes already advances the watermark when accrue is false,
so the silent stretch is simply never billed. The stop() tail applies the same
rule, or a session ended after sitting quiet would bill its last stretch anyway.

Fails OPEN: a pipeline whose idle time cannot be read is never treated as idle,
so an unreadable source keeps billing rather than silently giving the session
away. One status line per transition, so a paused counter reads as intentional
rather than broken.
Qwen accepts a term list (corpus.phrases) and Voxis has always passed one:
parse_hotwords feeds every standard-routed Qwen session, beta opt-in or not.
Only the UI was missing — cfg["beta"]["hotwords"] could be edited nowhere but
config.json, so in practice the feature did not exist.

The field audit measured what that costs. Across one meeting the speaker said
"Antler" 33 times and Voxis spelled it correctly 4 times: the rest came out as
Anter, Enter, Anthos, Atler, and once "enterözis". "MENAP" was said 14 times and
never once written correctly. For a user whose meetings are full of product and
company names, that is the difference between a usable transcript and one they
have to fix by hand.

Settings > General now takes one term per line, with term=replacement when it
should come out differently. The count on screen is produced by mirroring the
ENGINE's own parse rule (blank and #comment lines skipped) so it cannot drift
from what is actually sent, and it warns past the 50-pair server limit instead
of letting the engine truncate silently.

set_hotwords writes through a copy of the beta dict: clone, source_lang and
vad_ms live beside it and are still config-file-only, so a whole-dict overwrite
would quietly reset them. Saved on blur rather than per keystroke, because the
term list rides the session handshake and therefore restarts a running session.
A turn's `src` is not data, it is a PAIRING: the source text that had arrived by
roughly SRC_LAG_S ago, attached to the turn finalizing now. That estimate holds
for Gemini, which transcribes and translates on one path. Qwen runs its source
ASR as a separate, slower service, so the translation lands first and the
matching source arrives seconds later — an audit of one real session measured
the resulting column shift at a median -8s across 84% of turns.

Only the RESULT of the pairing has ever reached disk. The arrival times that
produced it were discarded, so the error could not be measured after the fact
and no corrected rule could be checked against a session that had already
happened. The record now carries the raw material alongside: what source text
arrived, and when, independent of which turn it was attached to. Consecutive
increments are merged inside a one-second window, since the useful resolution is
"when did this stretch arrive", not one row per token, and the list is bounded so
a long session cannot grow the file without limit.

Schema-additive and omitted when empty, so every existing record and every
session that captures no source serializes exactly as before.

This deliberately does not change the pairing itself. The obvious repair — shift
each cut back by one turn — fits the audited session because its turns happened
to run about as long as the lag; on shorter turns it under-corrects and on longer
ones it introduces a new error, and with nothing but the paired result on disk
there was no way to tell those cases apart. One real Qwen session recorded with
this track is enough to derive the rule and prove it, which is a far better
trade than shipping a guess into the one artifact users keep.
Owner reported the translated voice skipping. Transcribing the translated WAV of
a measured session confirmed it: 968 spoken words against 1067 captioned. The
asymmetry is the tell — 97.6% of the audio appears in the captions, but only
88.6% of the captions appear in the audio, so text was captioned and never
spoken, in small fragments rather than whole sentences.

Nothing in the record could localize it. The caption stream was persisted; the
audio stream was not. The playback counters were clean throughout — backlog 0-2s,
trimmed_total 0.0, dup_audio 0 — which rules out the pacing layer as the place
the words went, and leaves the engine's own output as the remaining suspect,
with no evidence either way.

This is the output-side counterpart of the source track: cumulative seconds of
translated speech the engine produced, sampled while captions flow. A stretch
where the turn timeline advances and this does not is text that was never spoken,
and now it can be pointed at rather than described.

Counted at the top of the sink, before the preview-mute and cascade branches,
because the question is what the ENGINE produced — not what reached the speakers.
Summed across pipelines so a meeting accounts for both legs. Sampling rides the
caption path rather than a timer: no new thread, and the samples land exactly
where they matter. A controller without the counter simply yields no samples;
instrumentation must never be able to break a session.

Schema-additive and omitted when empty, so existing records are unchanged.
The cross-turn guard compares whole turns, so it never saw this shape:

    A. <connective>, A. <tail>

where the engine emits a full clause twice inside ONE caption line. Measured
across three recorded sessions: 12 turns, 174 words, 2.2% of all caption text —
4.2% and 9.0% in two podcast sessions, 1.1% in a conference talk. Identical in
both runs of the same video, so it is engine behaviour rather than a race.

This is not lost audio. The translated speech says the clause once and is
correct; only the caption carries it twice. That distinction matters beyond the
transcript: the duplicate is a large part of what made caption text look "never
spoken" and sent the audio-gap investigation after a dropout that, for those
stretches, was never there.

The repair deletes text, so it is deliberately hard to trigger: at least five
words, non-overlapping copies, a 0.9 match, and the trailing words must match
exactly. That last rule is load-bearing — a 0.9 threshold tolerates one mismatch
in ten, which lets the match run a word past the real repeat and eat the first
word of the tail, and that word is something the speaker actually said. Short
echoes ("Evet." twice) stay: that is dialogue.

Replayed over the 8,006 recorded words of three sessions, the output is always a
subsequence of the input — deletions only, never a reorder, never an invention.

Applied at both finalize points so the re-speak guard, the record and the
exports all read the same text. Known limit: it lands on the record and the
exports, not on the caption bubble already rendered on screen.
Two recorded sessions captioned more than they spoke — 968 words against 1067,
then 578 against 652. Part of that was the caption repeating itself, fixed
separately. The rest is text the engine described and never voiced, and it does
not reproduce across two runs of the same video, so it is timing rather than
content.

What was missing was a name for it. The two streams were only ever counted in
aggregate: audio_track gives a session-level production total, and per-turn
attribution from it does not work because audio trails text by a variable amount
(replayed against a real session, four of five turns it called silent had in fact
been spoken). So "some text was never voiced" could be measured but never
pointed at.

The response id is the one place the two streams share an identity. Booking
produced characters and produced audio bytes against it makes a response with
captions and zero audio nameable, which is the whole point.

Judged at session end, never at its own .done: audio and text for one response
interleave in no guaranteed order, so an immediate verdict would report silence
that had merely not arrived yet. The id rides the *.done events for certain and
is best-effort on the deltas, so an event without one books against the most
recent response rather than being dropped.

_on_thread_exit is added to the base class for this: a rotation-time flush only
ever sees the sessions BEFORE the last one, so without it the final session —
usually the longest — would never be judged. Default no-op, best-effort by
contract, so a broken hook cannot take the teardown with it.

Log-only, bounded, and off the audio path.
With a virtual cable installed, both meeting directions shared it: the
outgoing leg writes the translated voice into CABLE Input and the vbcable
capture path reads CABLE Output, so the translation just sent into the call
came back in as if the other party had spoken it and was translated again.
Meeting now forces the driverless capture path, whose process-exclude
loopback cannot hear our own output.
The v1.0.49 test file imported `types` without using it, so `ruff check`
failed the Quality workflow while pytest and the i18n parity check passed.
Test-only change; the frozen 1.0.49 artifact is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
run_session.py --prod took the server-issued key but dropped the model
that came with it, so make_translator fell back through resolve_model to
the undated client alias. On DashScope the dated snapshot the server
issues and that alias are separate models with separate free quotas, and
the alias's was spent: every --prod Qwen run died with "1007 free quota
has been exhausted" long after a healthy session.created, because the
rejection only lands once audio is sent. That reads like a dead account
rather than a wrong model id. Production was never affected — pipeline
passes model=_model correctly.

--prod is also routing-aware now. The bare session-key call is the
server's backward-compat path and always answers Gemini, so benching any
other engine has to request routing by target; the returned engine is
checked rather than assumed, since silently benching Gemini under a
'qwen' label is the failure this harness exists to prevent.

Adds --clone off|once|always. beta_active has to be passed explicitly or
engines.make_translator reads clone as "off" and both arms of the A/B
come out identical. audio_total_s joins audio_s because the overrun that
drives playback backlog is a property of the whole stream, not just its
audible frames.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the silent-room billing pause. Accrual is back on wall clock for the
whole session: smart mode already stops streaming after ~1.5 s of quiet, so
those minutes cost almost nothing upstream and pausing the meter gave that
away fleet-wide for something no user had asked for.

Removes _GatedSource._last_speech_ts/idle_seconds, ModeController's
IDLE_PAUSE_SECONDS/_speech_idle_seconds/_idle_too_long/_update_idle_notice
and both call sites, plus the st_idle_paused/st_idle_resumed strings in all
23 locales.

_is_session_live is deliberately untouched — a session whose capture or
translator has actually died still stops billing. Silence is billed; failure
is not.
Davut Akça (DavutAkca) and others added 12 commits July 30, 2026 10:27
Every session spoke with one voice: the client never sent `voice`, so the
Qwen leg used the server default, which is female (Tina). There is now an
auto/female/male setting per direction — incoming (the video, or the other
party) and outgoing (how the user is heard in a meeting) are independent,
because a meeting has two speakers and both legs read the same config.

Qwen-only, and measured rather than assumed. Qwen honors session.voice:
Tina reads ~261 Hz against Ethan's ~170 Hz, and the two arms are different
voices by CAM++ cosine (0.34, against 0.92 between the default and Tina).
Gemini's translate-preview ignores the field outright — five valid names
AND a garbage name all returned the same voice, and the garbage name was
not even rejected — so a gender is never mapped to a Gemini voice and the
picker says so on those targets instead of silently doing nothing.

Only synthesis-verified names may enter the table: DashScope validates the
name lazily (session.update echoes anything back; the reject arrives with
the first response and strands the session in the retry loop with no
audio), and its own published roster is wrong for this model — Cherry and
Dylan are documented for it and rejected live. Cloning still wins where
it is on: a named voice alongside enable_voice_clone is a hard reject, and
the clone already carries the speaker's gender.

Alongside it, three things the same users kept missing:

- Release notes in the app, once per update, in the user's own language.
  Store updates are silent, so the only place to read what changed was the
  Store listing, which nobody reopens — feature work shipped invisibly. A
  fresh install is marked seen silently; it gets the onboarding tour, not a
  changelog for a version it never ran. The text comes from the same
  23-locale notes the release already writes for the listing
  (scripts/gen_whatsnew.py), so the two cannot drift.

- The term box moved out of General into its own Translation tab and lost
  the name "Meeting terms" — it has always fed video sessions too — and it
  no longer starts empty: ~31 curated proper nouns ship with it, switchable
  and viewable in place. Brands that are also ordinary words are excluded on
  purpose; a hotword biases recognition toward the term. The user's own
  entries are merged first, so the 50-pair cap can only drop a default.

- The history window's close button sits at the top right like every other
  window: .mfoot .spacer had a flex rule and .mhead .spacer did not.

Also drops a CHANGELOG entry for the silence-billing pause, which a8530a9
had already reverted in code.

i18n: 20 new keys across all 23 languages, in their own block that
check_i18n.py now parses too, so a missing language fails the gate.
…ring

The Qwen engine reports a rejected utterance as
input_audio_transcription.failed. That branch logged the error and returned,
without marking that input had arrived — so a run of ASR errors disarmed both
self-heal watchdogs at once: the no-output watchdog is gated on recent input,
and the stall watchdog cannot fire because every server frame, .failed
included, resets it. The session then held its connection, showed as active,
translated nothing and never reconnected.

A field meeting lost its last 12m52s to this: audio still reaching the server,
the server still answering, no captions, no voice, the timer running.

A .failed only arrives when the server heard an utterance and tried to
transcribe it, so it is a truthful input signal. Marking it arms the no-output
watchdog, which reconnects up to WATCHDOG_ROTATE_MAX times and refills its
budget as soon as healthy output resumes.

Also folds in the pending Turkish and German status-string polish.
… notes

The whatsnew gate degrades to silence on a version with no entry, which is the
right runtime behaviour — a forgotten release should not raise an empty dialog.
It is the wrong release outcome: bumping APP_VERSION and skipping
`scripts/gen_whatsnew.py <ver>` ships an update nobody is told about, and every
other test here still passes because they only check the entries that exist.
…ed release notes

Measured across every recorded session (usage_events at session_id level):
40% of Windows-app sessions end under 30s, 59% under a minute, and 71% of
those short sessions are followed by another attempt a median 14s later.
Users whose best session stayed under a minute come back on another day 8%
of the time; those who once got a 5-minute session, 39%. The first minute
is where the product is lost, and two silences caused it.

- No-input notice: the no-output watchdog only fires when input EXISTED
  (base_translator gates on _last_input_ts > 0), so a session with nothing
  playing — or a user speaking into their microphone, which Video mode does
  not translate — got no signal, no caption and no explanation. Video mode
  only: total silence on a meeting's incoming leg is a normal call nobody is
  talking in yet. Reads the running input peak, not the slow-release meter.
  Billing is untouched; this is a status line, not the idle-pause machinery
  removed on 2026-07-29.
- Waiting cue: the stream was blank from Start until the first caption, while
  the connect handshake alone exceeds 10s for 16% of sessions (p90 33s). The
  cue shows progress, escalates past 10s, sets the latency expectation BEFORE
  the wait, and clears on the first translation.
- session_end{reason, seconds, first_audio_s, speech_seen}: the funnel stopped
  at session_live, so an abandoned session and a completed one were identical.
  reason distinguishes a settings-driven restart from a real give-up; a null
  first_audio_s means the user never heard one word. RTTEstimator already
  measured that span for the UI and never reported it.
- session_error's `other` bucket swallowed 82 of 143 recorded start failures;
  the fallback now carries the exception type, never its message, and a
  socket/DNS failure is no longer mislabelled os_import.
- Release notes cover every version the user skipped, not just the running
  one. whatsnew_seen already stored a version string, so the range was there
  to compute. 1.0.49/1.0.50 backfilled; 1.0.49's silence-billing bullet was
  dropped since that behaviour was reverted in 1.0.50 and would have read as
  a billing promise this build does not keep.
…rever

A translator session that connects and stays open past the 5-second
'proven healthy' mark, then dies having produced zero output, kept
resetting the transient-failure counter every retry (measured 2026-08-01:
DashScope's qwen3.5-livetranslate-flash-realtime repeatedly closing
connections with a raw 500 / 'thread pool exhausted' after 6-13s, never
having sent a single translated word). Time-alive alone was treated as
proof the connection was healthy, so MAX_TRANSIENT_FAILURES was never
reached and the engine never gave up in favor of a working substitute.
The reset now also requires that the session actually produced output.

Paid plans additionally get a 1-strike failover (any dropped connection
swaps straight to the backup engine) instead of the default multi-retry
budget — a paying session should not sit through a flaky free engine's
normal tolerance.
…ool before Gemini

DashScope's "thread pool exhausted" is qwen3.5-livetranslate-flash-realtime's
account-wide 10 req/60s capacity ceiling (vs 60/60s for the omni models) -
it was classified as generic transient and retried up to 8 times with fast
1-6s backoff, which only added more load to an already-saturated shared
rate limit and prolonged the outage it was reacting to. It's now terminal.

A terminal Qwen error with an unused server-provided fallback credential
(session_key.go's new qwenFallbackCredentials, mirroring the fleet-level
rotation order WatchEngineHealth already uses) gets one immediate retry on
the sibling pool before giving up to Gemini - applies to every session,
paid or free, layered under the existing paid-customer fast-fail carve-out
which only covers unclassified transient errors.

Backend change already deployed (additive-only JSON field). This commit is
the client leg; not yet released to end users.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…volume ceiling on VB-CABLE setups

Qwen's "thread pool exhausted" capacity error is now terminal instead of
generic-transient (it was being retried up to 8 times, hammering an
already-saturated shared rate limit). A terminal error now gets one
immediate retry on the server-provided sibling pool before falling back
to Gemini.

The VB-CABLE volume mirror assumed the physical playback device sat at
0 dB; if it didn't, that device's own level silently capped the output
under the mirrored value. OutputLevelNeutralizer now raises the physical
device to full for the session and restores it on stop, crash-safe via
a sidecar snapshot.

Also: new legal/policy docs (EULA, Privacy, Refund, Terms, AI Disclosure,
Meeting Consent), README copy edits, and a Palabra benchmark script.
ruff E741 flagged single-letter `l` in the manifest read comprehension.
… a public app.json manifest

Voxis had no way to tell a user which build they were running, or that a
newer one existed (Silvano feedback). Adds a top-bar version badge plus a
background check against a new public https://voxislive.com/app.json
(generated by scripts/gen_app_manifest.py, SSOT'd off config.LANGS +
local_tts.VOICES + index.html LANG_NAMES) that surfaces an
"update available" badge linking to the Store listing. The manifest fetch
is unauthenticated and works on the OSS/BYOK build too, not just official.
app/whatsnew.py and CHANGELOG.md were missing the version badge / update
check shipped in the previous commit. Regenerated from
.local/store-listings/notes_1054.json (scripts/gen_whatsnew.py) after
appending the new bullet across all 23 locales.
Bumps the pip group with 1 update in the / directory: [cryptography](https://github.com/pyca/cryptography).


Updates `cryptography` from 49.0.0 to 50.0.0
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](pyca/cryptography@49.0.0...50.0.0)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 50.0.0
  dependency-type: direct:production
  dependency-group: pip
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file python Pull requests that update python code labels Aug 5, 2026
@DavutAkca
Davut Akça (DavutAkca) merged commit 09ea1d5 into main Aug 6, 2026
6 checks passed
@DavutAkca
Davut Akça (DavutAkca) deleted the dependabot/pip/pip-18c674f953 branch August 6, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant