From 2631ff3ef40bfee85d748b67d997798d205da840 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:04:53 +0530 Subject: [PATCH 01/85] feat(cla): require a Contributor License Agreement on pull requests Adds the individual and corporate CLA 1.0, signed by comment and recorded by account ID on the cla-signatures branch. An in-repo checker replaces CLA Assistant Lite: it checks the PR opener and every commit author and co-author, paginates commits and comments, cannot be satisfied by changing a git name, sets a required "CLA" status, never locks pull requests, creates its store on first use, and lets past contributors sign on a maintainer-labelled issue. Includes unit tests, a privileged-workflow guard, hash-pinned agreement texts, and an audit script for unsigned contributors. --- .github/CCLA-1.0.md | 106 +++++++++ .github/CLA-1.0.md | 151 ++++++++++++ .github/CONTRIBUTING.md | 39 ++-- .github/pull_request_template.md | 1 + .github/scripts/cla_check.py | 388 +++++++++++++++++++++++++++++++ .github/workflows/cla.yml | 48 ++++ CHANGELOG.md | 1 + LICENSE-NOTICE.md | 5 + scripts/cla_audit.py | 228 ++++++++++++++++++ tests/test_cla_check.py | 244 +++++++++++++++++++ tests/test_cla_workflow.py | 106 +++++++++ 11 files changed, 1304 insertions(+), 13 deletions(-) create mode 100644 .github/CCLA-1.0.md create mode 100644 .github/CLA-1.0.md create mode 100644 .github/scripts/cla_check.py create mode 100644 .github/workflows/cla.yml create mode 100644 scripts/cla_audit.py create mode 100644 tests/test_cla_check.py create mode 100644 tests/test_cla_workflow.py diff --git a/.github/CCLA-1.0.md b/.github/CCLA-1.0.md new file mode 100644 index 000000000..566764817 --- /dev/null +++ b/.github/CCLA-1.0.md @@ -0,0 +1,106 @@ +# VoiceStudio Corporate Contributor License Agreement 1.0 + +This Agreement lets an organisation license the Contributions its employees +and contractors make to VoiceStudio. It does not transfer the organisation's +copyright. Each covered individual also signs the +[Individual Contributor License Agreement 1.0](CLA-1.0.md) (the "Individual +Agreement") on their first pull request. + +This text is fixed. A later version will have a new version number and file, +and will need a new signature. + +## How the organisation accepts + +An officer or other person authorised to bind the organisation completes the +signature block below and emails a signed copy to `hi@voicestudio.sh`. +The signature must be handwritten, or an electronic signature recognised by +the Information Technology Act, 2000 or the law where the Corporation is +established. The Maintainer keeps the +signed copy and confirms receipt. The Agreement takes effect when the +Maintainer confirms receipt. + +## 1. Definitions + +- **"Corporation"** means the organisation named in the signature block, and + any entity that controls it, is controlled by it, or is under common control + with it. +- **"Designated Contributor"** means an employee or contractor of the + Corporation listed in Schedule A, as updated under section 5. +- **"Maintainer"** (Yupcha Softwares Private Limited and its affiliates and + assignees), **"Project"**, and **"Contribution"** have the meanings in the + Individual Agreement. Here, a Contribution is one submitted by a + Designated Contributor in which the Corporation has rights. + +This Agreement covers every Contribution a Designated Contributor has submitted +to the Project, whether before or after the date it takes effect. It adds to, +and does not reduce, any licence granted earlier for a Contribution. + +## 2. Licences + +In consideration of the Maintainer's commitment in section 4 of the Individual +Agreement and the Maintainer's review of the Contributions, the Corporation +grants the Maintainer, and recipients of software the Maintainer distributes, +the copyright licence in section 2 and the patent licence in section 3 of the +Individual Agreement, on the same terms, for every Contribution. + +The Maintainer may license the Contributions under any terms, including the +GNU Affero General Public License, other open-source licences, and commercial +or proprietary licences. The Corporation intends these licences to cover those +uses, including closed-source commercial products and hosted services, and +forms of use that are not yet known when this Agreement takes effect. These +licences are perpetual, worldwide, and do not lapse if the Maintainer does not +exercise them. + +Section 4 of the Individual Agreement (open-source commitment) applies to the +Contributions in the same way. + +## 3. Corporation's representations + +The Corporation represents that: + +1. the person signing is authorised to bind the Corporation; +2. the Corporation has the right to grant these licences for each + Contribution; +3. each Designated Contributor is authorised to submit Contributions on the + Corporation's behalf; and +4. to its knowledge, no Contribution infringes the rights of any third party. + +Except for these representations, Contributions are provided "as is", without +warranties or conditions of any kind. The Corporation has no obligation to +provide support for Contributions. + +## 4. Moral rights + +To the extent permitted by law, the Corporation will ensure that moral rights +of its Designated Contributors in the Contributions are waived, or not +asserted, against the uses licensed in this Agreement. + +## 5. Designated Contributors + +The Corporation keeps Schedule A current by emailing an updated list to +`hi@voicestudio.sh`. Removing a person stops future Contributions from +being covered. It does not affect licences already granted for earlier +Contributions. + +## 6. Governing law and general terms + +Sections 9 (Governing law) and 10 (General terms) of the Individual Agreement +apply to this Agreement, with "the Corporation" in place of "You". + +## Signature block + +| Field | Value | +|---|---| +| Corporation legal name | | +| Registered address | | +| Point of contact (name, email) | | +| Signatory name | | +| Signatory title | | +| Signature | | +| Date | | + +## Schedule A: Designated Contributors + +| Full name | GitHub username | Commit email addresses | +|---|---|---| +| | | | diff --git a/.github/CLA-1.0.md b/.github/CLA-1.0.md new file mode 100644 index 000000000..339273ede --- /dev/null +++ b/.github/CLA-1.0.md @@ -0,0 +1,151 @@ +# VoiceStudio Individual Contributor License Agreement 1.0 + +Thank you for contributing to VoiceStudio. This Agreement records the rights +you grant for your Contributions. It does not transfer your copyright: you keep +ownership of your work and may use it for any other purpose. + +This text is fixed. A later version will have a new version number and file, +and will need a new signature. + +## How you accept + +Everyone who opens a pull request in `debpalash/VoiceStudio`, or authored or +co-authored a commit in one, accepts this Agreement once by posting this +comment, on its own line, when the CLA check asks: + +> I have read the VoiceStudio CLA 1.0 and I hereby sign it. + +The check stores your GitHub username and account ID, the pull request +number, the ID and time of your comment, and this Agreement's version in the +public `cla-signatures` branch of the repository. This public record is kept as evidence of the Agreement for as long +as the Project uses Your Contributions. See the privacy policy at +. + +Your comment is a licence in writing by You, and an electronic record, for the +purposes of section 30 of the Indian Copyright Act, 1957 and the Information +Technology Act, 2000. + +## 1. Definitions + +- **"You"** means the individual who accepts this Agreement. +- **"Maintainer"** means Yupcha Softwares Private Limited (CIN + U62013TR2023PTC014212), a company incorporated in India with its registered + office in Agartala, Tripura, together with its affiliates and any assignee + under section 10. An affiliate is an entity that controls, is controlled by, + or is under common control with Yupcha Softwares Private Limited. +- **"Project"** means the VoiceStudio software and documentation published at + `https://github.com/debpalash/VoiceStudio`, including work published under + its former name, OmniVoice Studio. +- **"Contribution"** means any code, documentation, asset, or other work of + authorship that You submit to the Project for inclusion, through a pull + request, patch, or commit. It does not include work You clearly mark in + writing as "Not a Contribution". + +This Agreement covers every Contribution You have submitted to the Project, +whether before or after the date You accept it. It adds to, and does not +reduce, any licence You granted earlier for a Contribution. + +## 2. Copyright licence + +In consideration of the Maintainer's commitment in section 4 and the +Maintainer's review of Your Contributions, You grant the Maintainer, and +recipients of software the Maintainer distributes, a perpetual, worldwide, +non-exclusive, no-charge, royalty-free, irrevocable copyright licence to +reproduce, prepare derivative works of, publicly display, publicly perform, +sublicense, and distribute Your Contributions and derivative works of them. + +The Maintainer may license Your Contributions under any terms, including the +GNU Affero General Public License, other open-source licences, and commercial +or proprietary licences. You intend this licence to cover those uses, including +closed-source commercial products and hosted services, and forms of use that +are not yet known when You accept this Agreement. These licences do not lapse +if the Maintainer does not exercise them. + +## 3. Patent licence + +You grant the Maintainer, and recipients of software the Maintainer +distributes, a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable patent licence to make, have made, use, offer to sell, sell, +import, and otherwise transfer the Project. This licence covers only patent +claims You can license that are necessarily infringed by Your Contribution +alone or by the combination of Your Contribution with the Project version to +which You submitted it. + +If any entity starts patent litigation, including a cross-claim or +counterclaim, alleging that a Contribution or the Project infringes a patent, +the patent licences granted to that entity under this Agreement end on the +date the litigation is filed. + +## 4. Open-source commitment + +As long as a Contribution is included in the public source repository of the +Project, the Maintainer will make it available there under the GNU Affero +General Public License version 3 or another licence approved by the Open Source +Initiative. + +This commitment applies only to that public repository. It does not require +the Maintainer to publish source code for commercial or proprietary editions, +builds, or services. It is a promise by the Maintainer and not a condition of +the licences in sections 2 and 3, which continue whether or not it is met. + +## 5. Your representations + +You represent that: + +1. You are of legal age to enter into this Agreement where You live, or a + parent or legal guardian has agreed to it on Your behalf; +2. each Contribution is Your original work, or You otherwise have the right to + submit it under this Agreement; +3. if Your employer or another party has rights in a Contribution, You have its + permission to grant these licences, or it has signed the VoiceStudio + Corporate Contributor License Agreement for You; +4. You identify any third-party work in a Contribution, with its source and + licence, when You submit it; +5. if You used an AI tool or other automated system to produce part of a + Contribution, You reviewed that output, take the same responsibility for it + as for Your own work, and the tool's terms allow You to submit it under this + Agreement; and +6. to Your knowledge, no Contribution infringes the rights of any third party. + +You will tell the Maintainer promptly if any of these statements stops being +accurate. + +## 6. Moral rights + +To the extent permitted by law, You waive, or agree not to assert, moral rights +in Your Contributions that would prevent the uses licensed in this Agreement. + +## 7. No warranty or support + +Except for the representations in section 5, Contributions are provided +"as is", without warranties or conditions of any kind. You have no obligation +to provide support for Your Contributions. The Maintainer has no obligation to +use any Contribution. + +## 8. Contributions for an employer + +If You contribute as part of Your work for an employer or other organisation, +ask that organisation to sign the +[Corporate Contributor License Agreement](CCLA-1.0.md) before You submit the +Contribution. You still sign this Agreement yourself. + +## 9. Governing law + +This Agreement is governed by the laws of India, without regard to conflict of +laws rules. The courts at Agartala, Tripura, India have non-exclusive +jurisdiction over any dispute arising from it. + +## 10. General terms + +- **Assignment.** The Maintainer may assign this Agreement, and the licences + granted under it, to any affiliate or to any person or entity that acquires + the Project or the business that maintains it. Any assignee is bound by + section 4. +- **Severability.** If a court holds any part of this Agreement unenforceable, + that part is enforced to the maximum extent permitted and the rest remains in + effect. +- **Entire agreement.** This Agreement is the entire agreement between You and + the Maintainer about the licensing of Your Contributions, except that any + earlier licence You granted continues as stated in section 1. +- **Survival.** The licences and commitments in this Agreement continue after + You stop contributing. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 09791941d..c9875f5c3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -327,19 +327,32 @@ hard rules from the first prompt. ## Contribution licensing VoiceStudio is **AGPL-3.0-only**, and the maintainer also offers a -**commercial license** (see [LICENSE](LICENSE)). By submitting a contribution -you agree that: - -1. you have the right to submit it (your own work, or compatibly licensed); -2. it is licensed to the project under **AGPL-3.0**; and -3. you grant the project maintainer a perpetual, worldwide, non-exclusive - right to also distribute your contribution under the project's commercial - license terms. - -This inbound grant is what keeps the dual-license model viable. If you can't -agree to (3) for a particular contribution, say so in the PR and we'll discuss -before merging. Adding a `Signed-off-by:` line (DCO) to your commits is -appreciated but not required. +**commercial license** (see [LICENSE-NOTICE.md](../LICENSE-NOTICE.md)). Before +a pull request can merge, the person who opened it and every commit author and +co-author sign the [Contributor License Agreement](CLA-1.0.md) once. You keep +your copyright. The agreement lets Yupcha Softwares Private Limited, the +company that maintains VoiceStudio, ship your work in both the AGPL-3.0 app and +commercial builds, and commits to keeping it available under an open-source +licence. AI tools listed as co-authors don't sign; the person submitting the +work does. + +The **CLA** check comments on your pull request when someone still needs to +sign. To sign, post this as a new comment, on its own line: + +```text +I have read the VoiceStudio CLA 1.0 and I hereby sign it. +``` + +The signature covers your earlier and future contributions, and the check +turns green on its own. Contributed before and have no open pull request? Post +the same line on the [issue labelled `cla`](https://github.com/debpalash/VoiceStudio/issues?q=label%3Acla). If it lists a commit it cannot link to a GitHub +account, either add that commit email to your account (Settings → Emails), or +rewrite the commits with an email that is on it (`git commit --amend +--reset-author`, or an interactive rebase) and push again. Comment `recheck` to +run the check without pushing. Contributing as part of your job? Your employer +signs the [Corporate CLA](CCLA-1.0.md) first, and you still sign the CLA +yourself. Adding a `Signed-off-by:` line (DCO) is appreciated but does not +replace the CLA. --- diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 592519273..4a988e06e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -29,6 +29,7 @@ ## Checklist - [ ] I've tested this locally +- [ ] Every commit author has signed the [CLA](https://github.com/debpalash/VoiceStudio/blob/main/.github/CLA-1.0.md) (the **CLA** check tells you how) - [ ] I've updated relevant documentation (if applicable) - [ ] No local machine paths, logs, or personal env details in this PR - [ ] Maintained version files are in sync (if an owner-requested bump): root `package.json`, `pyproject.toml`, `backend/core/version.py`, and lockfiles diff --git a/.github/scripts/cla_check.py b/.github/scripts/cla_check.py new file mode 100644 index 000000000..0eca5928c --- /dev/null +++ b/.github/scripts/cla_check.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +"""Contributor License Agreement check for pull requests. + +Runs from .github/workflows/cla.yml on the base branch's copy of this file, so +pull-request code never executes with the write token. Standard library only. + +A pull request passes when the person who opened it and every commit author and +co-author has signed .github/CLA-.md, or is a maintainer or GitHub App +bot recognised by account ID. People are matched by GitHub account ID, never by +git name, so changing `user.name` cannot impersonate a signer. Authors whose +commit email is not linked to a GitHub account cannot be verified and fail the +check with instructions. + +Each run rescans every comment on the pull request, so a run that GitHub +cancels while queued cannot lose a signature. The result is a commit status +named "CLA" on the head commit, which branch protection can require. +""" +from __future__ import annotations + +import base64 +import json +import os +import re +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass, field + +CLA_VERSION = "1.0" +SIGN_PHRASE = f"I have read the VoiceStudio CLA {CLA_VERSION} and I hereby sign it." +DOCUMENT_PATH = f".github/CLA-{CLA_VERSION}.md" +SIGNATURE_BRANCH = "cla-signatures" +SIGNATURE_PATH = f"signatures/v{CLA_VERSION}/cla.json" +STATUS_CONTEXT = "CLA" +# Issues carrying this label (only maintainers can apply labels) accept +# signatures from anyone, so past contributors can sign without a pull request. +SIGNING_LABEL = "cla" +COMMENT_MARKER = "" +MAX_COMMITS = 250 + +# GitHub account IDs exempt from signing. IDs cannot be claimed by anyone else. +MAINTAINER_IDS = {4178343} # debpalash +BOT_IDS = { + 49699333, # dependabot[bot] + 41898282, # github-actions[bot] +} +# Co-author identities that name a tool rather than a person. The human who +# submits the work signs for it (CLA section 5.5). +# GitHub App bots also commit as "+[bot]@users.noreply.github.com"; +# a person cannot hold such a login, and the authenticated opener still signs. +TOOL_EMAILS = re.compile( + r"^(noreply@anthropic\.com|cursoragent@cursor\.com|codex@openai\.com|noreply@coderabbit\.ai" + r"|\d+\+[^@]*\[bot\]@users\.noreply\.github\.com)$", + re.I, +) +NOREPLY = re.compile(r"^(?:(\d+)\+)?([A-Za-z0-9-]+)@users\.noreply\.github\.com$", re.I) +CO_AUTHOR = re.compile(r"^co-authored-by:\s*(.*?)\s*<([^>]*)>\s*$", re.I | re.M) + + +# ── Pure logic (unit-tested) ──────────────────────────────────────────── + + +@dataclass(frozen=True) +class Person: + login: str + id: int + + +@dataclass +class Evaluation: + unsigned: list[Person] = field(default_factory=list) + unknown: list[str] = field(default_factory=list) # git identities with no GitHub account + + @property + def passed(self) -> bool: + return not self.unsigned and not self.unknown + + +def is_sign_comment(body: str) -> bool: + """True when a line of the comment is exactly the sign phrase (not a quote).""" + target = " ".join(SIGN_PHRASE.lower().split()) + return any(" ".join(line.lower().split()) == target for line in (body or "").splitlines()) + + +def is_recheck(body: str) -> bool: + return (body or "").strip().lower() == "recheck" + + +def exempt(person: Person) -> bool: + return person.id in MAINTAINER_IDS or person.id in BOT_IDS + + +def evaluate(opener: Person, opener_is_bot: bool, actors: list[dict], signed_ids: set[int]) -> Evaluation: + """Decide who still has to sign. + + `actors` holds one entry per commit author or co-author: + {"name", "email", "login" or None, "id" or None}. + """ + result = Evaluation() + seen: set[int] = set() + + def require(person: Person) -> None: + if person.id in seen or exempt(person): + return + seen.add(person.id) + if person.id not in signed_ids: + result.unsigned.append(person) + + if not opener_is_bot: + require(opener) + for actor in actors: + email = (actor.get("email") or "").strip() + if actor.get("id"): + require(Person(actor["login"], int(actor["id"]))) + continue + if match := NOREPLY.match(email): + if match.group(1): + require(Person(match.group(2), int(match.group(1)))) + continue + if TOOL_EMAILS.match(email) or (opener_is_bot and "[bot]" in email): + continue + identity = f"{actor.get('name') or '?'} <{email or 'no email'}>" + if identity not in result.unknown: + result.unknown.append(identity) + return result + + +def co_authors(message: str) -> list[dict]: + return [{"name": n, "email": e, "login": None, "id": None} for n, e in CO_AUTHOR.findall(message or "")] + + +def add_signatures(store: dict, new: list[dict]) -> bool: + """Append signatures whose account ID is not stored yet. Returns True if changed.""" + have = {s["id"] for s in store["signatures"]} + added = [s for s in new if s["id"] not in have] + store["signatures"].extend(added) + return bool(added) + + +def empty_store() -> dict: + return {"version": CLA_VERSION, "document": DOCUMENT_PATH, "signatures": []} + + +def _safe(text: str) -> str: + """Render untrusted git identities as inert inline code.""" + return "`" + re.sub(r"[`\r\n]", "'", text)[:200] + "`" + + +def render_comment(evaluation: Evaluation, doc_url: str) -> str: + if evaluation.passed: + return f"{COMMENT_MARKER}\nAll contributors on this pull request have signed the VoiceStudio CLA. Thank you!" + lines = [ + COMMENT_MARKER, + "Thank you for contributing to VoiceStudio. Before this pull request can merge, everyone who " + f"contributed to it must sign the [Contributor License Agreement {CLA_VERSION}]({doc_url}) once. " + "You keep your copyright; the agreement lets Yupcha Softwares Private Limited, the company that " + "maintains VoiceStudio, ship your work in both the AGPL-3.0 app and commercial builds.", + ] + if evaluation.unsigned: + lines += ["", "**Still to sign:** " + ", ".join(f"@{p.login}" for p in evaluation.unsigned), "", + "To sign, post this as a new comment on its own line:", "", "```text", SIGN_PHRASE, "```"] + if evaluation.unknown: + lines += ["", "**Commits we cannot link to a GitHub account:**", ""] + lines += [f"- {_safe(identity)}" for identity in evaluation.unknown] + lines += ["", "Add that email to your GitHub account (Settings → Emails), or rewrite the commits with " + "an email that is on it (`git commit --amend --reset-author`, or an interactive rebase), " + "then push. Each push re-runs this check."] + lines += ["", "Comment `recheck` to run the check again."] + return "\n".join(lines) + + +# ── GitHub API ────────────────────────────────────────────────────────── + + +class GitHub: + def __init__(self, token: str, repo: str, api: str = "https://api.github.com"): + self.token, self.repo, self.api = token, repo, api.rstrip("/") + + def request(self, method: str, path: str, body: dict | None = None) -> tuple[int, object, dict]: + url = path if path.startswith("http") else f"{self.api}{path}" + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(url, data=data, method=method) + req.add_header("Authorization", f"Bearer {self.token}") + req.add_header("Accept", "application/vnd.github+json") + req.add_header("X-GitHub-Api-Version", "2022-11-28") + try: + with urllib.request.urlopen(req, timeout=30) as resp: + raw = resp.read() + return resp.status, json.loads(raw) if raw else None, dict(resp.headers) + except urllib.error.HTTPError as err: + raw = err.read() + try: + payload = json.loads(raw) if raw else None + except ValueError: + payload = None + return err.code, payload, dict(err.headers) + + def get(self, path: str) -> object: + status, payload, _ = self.request("GET", path) + if status != 200: + raise RuntimeError(f"GET {path} failed with {status}: {payload}") + return payload + + def paginate(self, path: str, limit: int | None = None) -> list: + items: list = [] + url = f"{self.api}{path}{'&' if '?' in path else '?'}per_page=100" + while url: + status, payload, headers = self.request("GET", url) + if status != 200: + raise RuntimeError(f"GET {url} failed with {status}: {payload}") + items.extend(payload) + if limit and len(items) >= limit: + return items + links = headers.get("Link") or headers.get("link") or "" + url = next((part.split(";")[0].strip(" <>") for part in links.split(",") if 'rel="next"' in part), None) + return items + + def graphql(self, query: str, variables: dict) -> dict: + status, payload, _ = self.request("POST", "/graphql", {"query": query, "variables": variables}) + if status != 200 or payload.get("errors"): + raise RuntimeError(f"GraphQL failed with {status}: {payload}") + return payload["data"] + + +COMMITS_QUERY = """ +query($owner: String!, $name: String!, $number: Int!, $after: String) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + commits(first: 100, after: $after) { + pageInfo { hasNextPage endCursor } + nodes { commit { authors(first: 20) { nodes { name email user { login databaseId } } } } } + } + } + } +} +""" + + +def pr_actors(gh: GitHub, number: int) -> tuple[list[dict], int]: + owner, name = gh.repo.split("/") + actors: list[dict] = [] + count, after = 0, None + while True: + data = gh.graphql(COMMITS_QUERY, {"owner": owner, "name": name, "number": number, "after": after}) + commits = data["repository"]["pullRequest"]["commits"] + for node in commits["nodes"]: + count += 1 + for author in node["commit"]["authors"]["nodes"]: + user = author.get("user") or {} + actors.append({"name": author.get("name"), "email": author.get("email"), + "login": user.get("login"), "id": user.get("databaseId")}) + if not commits["pageInfo"]["hasNextPage"] or count >= MAX_COMMITS: + return actors, count + after = commits["pageInfo"]["endCursor"] + + +def load_store(gh: GitHub) -> tuple[dict, str | None]: + status, payload, _ = gh.request("GET", f"/repos/{gh.repo}/contents/{SIGNATURE_PATH}?ref={SIGNATURE_BRANCH}") + if status == 404: + return empty_store(), None + if status != 200: + raise RuntimeError(f"Could not read signatures ({status}): {payload}") + return json.loads(base64.b64decode(payload["content"])), payload["sha"] + + +def ensure_branch(gh: GitHub) -> None: + """Create the signature branch as an orphan commit if it does not exist yet.""" + status, _, _ = gh.request("GET", f"/repos/{gh.repo}/git/ref/heads/{SIGNATURE_BRANCH}") + if status == 200: + return + readme = ("Signatures for the VoiceStudio Contributor License Agreement, written by " + ".github/workflows/cla.yml. Do not edit by hand.\n") + tree = gh.request("POST", f"/repos/{gh.repo}/git/trees", {"tree": [ + {"path": "README.md", "mode": "100644", "type": "blob", "content": readme}]})[1] + commit = gh.request("POST", f"/repos/{gh.repo}/git/commits", { + "message": "chore(cla): create signature store", "tree": tree["sha"], "parents": []})[1] + status, payload, _ = gh.request("POST", f"/repos/{gh.repo}/git/refs", + {"ref": f"refs/heads/{SIGNATURE_BRANCH}", "sha": commit["sha"]}) + if status not in (201, 422): # 422: another run created it first + raise RuntimeError(f"Could not create {SIGNATURE_BRANCH} ({status}): {payload}") + + +def save_signatures(gh: GitHub, new: list[dict], number: int) -> None: # number: PR or issue + """Write new signatures, retrying when another run updated the file first.""" + ensure_branch(gh) + for attempt in range(6): + store, sha = load_store(gh) + if not add_signatures(store, new): + return + logins = ", ".join(f"@{s['login']}" for s in new) + body = {"message": f"chore(cla): record signature for {logins} (#{number})", "branch": SIGNATURE_BRANCH, + "content": base64.b64encode((json.dumps(store, indent=2) + "\n").encode()).decode()} + if sha: + body["sha"] = sha + status, payload, _ = gh.request("PUT", f"/repos/{gh.repo}/contents/{SIGNATURE_PATH}", body) + if status in (200, 201): + return + if status not in (409, 422): + raise RuntimeError(f"Could not save signatures ({status}): {payload}") + time.sleep(1 + attempt) + raise RuntimeError("Could not save signatures after retries") + + +def upsert_comment(gh: GitHub, number: int, body: str, create: bool) -> None: + for comment in gh.paginate(f"/repos/{gh.repo}/issues/{number}/comments"): + if COMMENT_MARKER in (comment.get("body") or "") and (comment.get("user") or {}).get("type") == "Bot": + if comment["body"] != body: + gh.request("PATCH", f"/repos/{gh.repo}/issues/comments/{comment['id']}", {"body": body}) + return + if create: + gh.request("POST", f"/repos/{gh.repo}/issues/{number}/comments", {"body": body}) + + +def sign_on_issue(gh: GitHub, event: dict) -> bool: + """Record a signature posted on a maintainer-labelled signing issue.""" + issue, comment = event.get("issue") or {}, event.get("comment") or {} + user = comment.get("user") or {} + labels = {label.get("name") for label in issue.get("labels") or []} + if SIGNING_LABEL not in labels or user.get("type") != "User" or not is_sign_comment(comment.get("body")): + return False + save_signatures(gh, [{"login": user["login"], "id": int(user["id"]), "issue": issue["number"], + "comment_id": comment["id"], "signed_at": comment["created_at"], + "version": CLA_VERSION, "repo_id": (event.get("repository") or {}).get("id")}], + issue["number"]) + gh.request("POST", f"/repos/{gh.repo}/issues/comments/{comment['id']}/reactions", {"content": "+1"}) + return True + + +def run(gh: GitHub, event_name: str, event: dict, server_url: str = "https://github.com") -> Evaluation: + number = (event.get("pull_request") or event.get("issue") or {}).get("number") + pr = gh.get(f"/repos/{gh.repo}/pulls/{number}") + if pr.get("state") != "open": + return Evaluation() + opener = Person(pr["user"]["login"], int(pr["user"]["id"])) + opener_is_bot = pr["user"].get("type") == "Bot" and opener.id in BOT_IDS + + actors, count = pr_actors(gh, number) + store, _ = load_store(gh) + signed_ids = {int(s["id"]) for s in store["signatures"]} + evaluation = evaluate(opener, opener_is_bot, actors, signed_ids) + + # Record signatures from any comment by someone who still has to sign. + pending = {p.id: p for p in evaluation.unsigned} + new = [] + for comment in gh.paginate(f"/repos/{gh.repo}/issues/{number}/comments"): + user = comment.get("user") or {} + if user.get("id") in pending and is_sign_comment(comment.get("body")): + person = pending.pop(user["id"]) + new.append({"login": person.login, "id": person.id, "pull_request": number, + "comment_id": comment["id"], "signed_at": comment["created_at"], + "version": CLA_VERSION, "repo_id": pr["base"]["repo"]["id"]}) + if new: + save_signatures(gh, new, number) + evaluation = evaluate(opener, opener_is_bot, actors, signed_ids | {s["id"] for s in new}) + + doc_url = f"{server_url}/{gh.repo}/blob/{pr['base']['repo']['default_branch']}/{DOCUMENT_PATH}" + if count >= MAX_COMMITS: + evaluation.unknown.append(f"more than {MAX_COMMITS} commits; ask a maintainer to review the CLA by hand") + description = ("All contributors have signed the CLA" if evaluation.passed + else f"{len(evaluation.unsigned) + len(evaluation.unknown)} contributor(s) still need to sign") + gh.request("POST", f"/repos/{gh.repo}/statuses/{pr['head']['sha']}", { + "state": "success" if evaluation.passed else "failure", "context": STATUS_CONTEXT, + "description": description[:140], "target_url": doc_url}) + upsert_comment(gh, number, render_comment(evaluation, doc_url), create=not evaluation.passed) + return evaluation + + +def main() -> int: + event_name = os.environ["GITHUB_EVENT_NAME"] + with open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8") as handle: + event = json.load(handle) + gh = GitHub(os.environ["GITHUB_TOKEN"], os.environ["GITHUB_REPOSITORY"], + os.environ.get("GITHUB_API_URL", "https://api.github.com")) + if event_name == "issue_comment": + body = (event.get("comment") or {}).get("body") + if not (event.get("issue") or {}).get("pull_request"): + print("CLA:", "signature recorded" if sign_on_issue(gh, event) else "not a signing comment") + return 0 + if not (is_sign_comment(body) or is_recheck(body)): + return 0 + evaluation = run(gh, event_name, event, os.environ.get("GITHUB_SERVER_URL", "https://github.com")) + print("CLA:", "passed" if evaluation.passed else f"waiting on {evaluation.unsigned} {evaluation.unknown}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 000000000..4b4148605 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,48 @@ +# Contributor License Agreement check. +# +# Every pull request's opener and commit authors and co-authors sign +# .github/CLA-1.0.md once, by comment. .github/scripts/cla_check.py records +# signatures on the `cla-signatures` branch (created on first use) and sets the +# "CLA" commit status that branch protection requires. Past contributors can +# also sign on any issue a maintainer labels `cla`. +# +# Security: pull_request_target and issue_comment run with a write token, even +# for fork PRs. This workflow therefore checks out only the base branch (the +# default for both events) and runs the base branch's checker; pull-request code +# is never checked out or executed. tests/test_cla_workflow.py enforces that. + +name: CLA + +on: + pull_request_target: + types: [opened, synchronize, reopened] + issue_comment: + types: [created] + +permissions: + contents: read + +concurrency: + group: cla-${{ github.event.pull_request.number || github.event.issue.number }} + +jobs: + cla: + if: >- + github.event_name == 'pull_request_target' || + contains(github.event.comment.body, 'VoiceStudio CLA') || + (github.event.issue.pull_request && contains(github.event.comment.body, 'recheck')) + runs-on: ubuntu-24.04 + permissions: + contents: write + issues: write + pull-requests: write + statuses: write + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + sparse-checkout: .github/scripts + - name: Check CLA signatures + run: python3 .github/scripts/cla_check.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 362b82f7f..c31790517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ metadata and the backend fallback mirror it. - Footer sponsor hover details show dated GitHub reach, and its X opens the Pro page (#2302) - Linux AppImages use the static runtime so launching them no longer needs libfuse2 (#2328) — thanks @shuvashish76! - Voice Clone shows upload and record side by side instead of behind a toggle (#2307) +- Pull requests ask their opener and every commit author and co-author to sign a one-time Contributor License Agreement by comment; the contributing guide explains how ### Docs - Chinese README now matches the Electron installation and migration guide (#2377) — thanks @lg114! diff --git a/LICENSE-NOTICE.md b/LICENSE-NOTICE.md index 2ed1563cb..5a4458f58 100644 --- a/LICENSE-NOTICE.md +++ b/LICENSE-NOTICE.md @@ -26,6 +26,11 @@ VoiceStudio in a closed-source or proprietary product or service without the AGPL-3.0 copyleft obligations. Pricing tiers are coming soon; for inquiries contact `VoiceStudio@palash.dev`. +Contributors license their contributions to Yupcha Softwares Private Limited, +the company that maintains VoiceStudio, under the +[Contributor License Agreement](.github/CLA-1.0.md). Contributions stay +available under the AGPL-3.0. + (This Notice is a plain-language summary; the binding terms are the full GNU AGPL-3.0 text in [`LICENSE`](LICENSE).) diff --git a/scripts/cla_audit.py b/scripts/cla_audit.py new file mode 100644 index 000000000..4b63b882f --- /dev/null +++ b/scripts/cla_audit.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""List contributors whose code is in the tree but who have not signed the CLA. + +Blames every tracked text file at HEAD, credits each line to its commit's +author and co-authors, maps them to GitHub accounts, and compares the result +with the signature store that .github/scripts/cla_check.py writes to the +`cla-signatures` branch. + + git fetch origin cla-signatures + GH_TOKEN=... python scripts/cla_audit.py # Markdown report + GH_TOKEN=... python scripts/cla_audit.py --json out.json + +People are matched by GitHub account ID. An author is mapped by their GitHub +no-reply address, else by the account linked to the commit email. Commits by AI +agents or unlinked emails are credited to the author of the pull request that +merged them, because that person submitted the work. Without a token the GitHub +API rate limit runs out quickly; unresolved authors are then listed by email. +Blame follows moves within a file only, so code copied between files is +credited to whoever moved it, and squash commits that fold in other people's +pull requests are listed separately for manual review. +""" +from __future__ import annotations + +import argparse +import collections +import importlib.util +import json +import os +import re +import subprocess +import sys +import urllib.error +import urllib.request +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +SLUG = "debpalash/VoiceStudio" + +_spec = importlib.util.spec_from_file_location("cla_check", REPO / ".github" / "scripts" / "cla_check.py") +cla = importlib.util.module_from_spec(_spec) +sys.modules["cla_check"] = cla +_spec.loader.exec_module(cla) + +SIGNATURES = f"origin/{cla.SIGNATURE_BRANCH}:{cla.SIGNATURE_PATH}" +BINARY = re.compile(r"\.(lock|png|jpe?g|gif|webp|ico|icns|svg|wav|mp3|flac|ogg|woff2?|ttf|pdf|onnx|bin)$", re.I) +# Identities that do not name the person who submitted the work. +AGENT = re.compile(r"^(noreply@anthropic\.com|.*@openai\.com|codex@users\.noreply\.github\.com|test@local|you@example\.com|)$") +FOLDS_IN = re.compile(r"\bsupersed", re.I) +NOTES = { + "lookup-failed": "GitHub lookup failed; set GH_TOKEN and rerun", + "name": "matched by display name; confirm", + "unresolved": "no linked account or pull request; resolve by hand", + "submitted": "submitted from an unlinked or agent identity; confirm the work is theirs", +} + + +def git(*args: str) -> str: + result = subprocess.run(["git", "-C", str(REPO), *args], capture_output=True, text=True, errors="replace") + if result.returncode != 0: + raise SystemExit(f"git {' '.join(args)} failed: {result.stderr.strip()}") + return result.stdout + + +def github(path: str) -> object: + request = urllib.request.Request(f"https://api.github.com/repos/{SLUG}/{path}") + if token := os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN"): + request.add_header("Authorization", f"Bearer {token}") + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response) + + +def blame(path: str) -> collections.Counter: + lines: collections.Counter = collections.Counter() + commit = name = email = None + out = subprocess.run(["git", "-C", str(REPO), "blame", "-w", "-M", "--line-porcelain", "HEAD", "--", path], + capture_output=True, text=True, errors="replace").stdout + for row in out.splitlines(): + if re.match(r"^[0-9a-f]{40} ", row): + commit = row[:40] + elif row.startswith("author "): + name = row[7:] + elif row.startswith("author-mail "): + email = row[12:].strip("<>").lower() + elif row.startswith("\t"): + lines[(email, name, commit, path)] += 1 + return lines + + +def commit_messages(commits: set[str]) -> dict[str, str]: + messages = {} + for chunk in git("log", "--no-walk=unsorted", "--format=%H%x00%B%x01", *sorted(commits)).split("\x01"): + if "\x00" in chunk: + sha, body = chunk.strip().split("\x00", 1) + messages[sha] = body + return messages + + +def account_for(email: str, commit: str) -> tuple[str | None, int | None, str]: + """GitHub account for an author email: (login, id, noreply|api|unlinked|lookup-failed).""" + if AGENT.match(email): + return None, None, "unlinked" + if (match := cla.NOREPLY.match(email)) and match.group(1): + return match.group(2), int(match.group(1)), "noreply" + try: + author = github(f"commits/{commit}").get("author") or {} + except (OSError, ValueError, urllib.error.HTTPError): + return None, None, "lookup-failed" + return (author["login"], author["id"], "api") if author.get("id") else (None, None, "unlinked") + + +def pr_author_for(commit: str) -> tuple[str | None, int | None, str]: + try: + pulls = github(f"commits/{commit}/pulls") + except (OSError, ValueError, urllib.error.HTTPError): + return None, None, "lookup-failed" + return (pulls[0]["user"]["login"], pulls[0]["user"]["id"], "submitted") if pulls else (None, None, "unresolved") + + +def names_to_logins() -> dict[str, tuple[str, int]]: + """Display names seen with a GitHub no-reply address that includes an account ID.""" + names: dict[str, tuple[str, int]] = {} + log = git("log", "--format=%aN <%aE>%n%(trailers:key=Co-authored-by,valueonly)") + for match in re.finditer(r"^(.+?) <([^>]+)>$", log, re.M): + noreply = cla.NOREPLY.match(match.group(2)) + if noreply and noreply.group(1) and not AGENT.match(match.group(2).lower()): + names.setdefault(match.group(1).strip().lower(), (noreply.group(2), int(noreply.group(1)))) + return names + + +def signed_ids(allow_missing: bool) -> set[int]: + result = subprocess.run(["git", "-C", str(REPO), "show", SIGNATURES], capture_output=True, text=True) + if result.returncode != 0: + if allow_missing: + return set() + raise SystemExit(f"{SIGNATURES} not found. Run `git fetch origin {cla.SIGNATURE_BRANCH}`, " + "or pass --no-signatures to report everyone.") + return {int(s["id"]) for s in json.loads(result.stdout)["signatures"]} + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--json", type=Path, help="also write the full report as JSON") + parser.add_argument("--no-signatures", action="store_true", help="report everyone as unsigned") + args = parser.parse_args() + signed = signed_ids(args.no_signatures) + + files = [f for f in git("ls-files").splitlines() if f and not BINARY.search(f)] + blamed: collections.Counter = collections.Counter() + with ThreadPoolExecutor(16) as pool: + for counts in pool.map(blame, files): + blamed.update(counts) + messages = commit_messages({c for _, _, c, _ in blamed}) + + # Credit each line to the commit author and to every person co-author. + credits: collections.Counter = collections.Counter() # (email, name, commit, path) -> lines + for (email, name, commit, path), n in blamed.items(): + credits[(email, name, commit, path)] += n + for co in cla.co_authors(messages.get(commit, "")): + co_email = co["email"].lower() + if not cla.TOOL_EMAILS.match(co_email) and co_email != email: + credits[(co_email, co["name"], commit, path)] += n + + first_commit: dict[str, str] = {} + for email, _, commit, _ in credits: + first_commit.setdefault(email, commit) + with ThreadPoolExecutor(4) as pool: + accounts = dict(zip(first_commit, pool.map(lambda e: account_for(e, first_commit[e]), first_commit))) + per_commit = sorted({c for e, _, c, _ in credits if accounts[e][2] == "unlinked"}) + with ThreadPoolExecutor(4) as pool: + submitters = dict(zip(per_commit, pool.map(pr_author_for, per_commit))) + + names = names_to_logins() + people: dict[str, dict] = collections.defaultdict( + lambda: {"lines": 0, "files": collections.Counter(), "emails": set(), "notes": set(), "login": None, "id": None} + ) + folded: collections.Counter = collections.Counter() + for (email, name, commit, path), n in credits.items(): + login, account_id, source = accounts[email] + if source == "unlinked": + login, account_id, source = submitters[commit] + if not account_id and (guess := names.get((name or "").strip().lower())): + (login, account_id), source = guess, "name" + if account_id in cla.MAINTAINER_IDS and source in ("noreply", "api"): + if FOLDS_IN.search(messages.get(commit, "")): + folded[commit] += n + continue + if account_id in cla.BOT_IDS: + continue + key = str(account_id) if account_id else (email or name or "?") + person = people[key] + person["lines"] += n + person["files"][path] += n + person["emails"].add(email) + person["login"] = person["login"] or login + person["id"] = account_id + if source in NOTES: + person["notes"].add(source) + + unsigned = sorted( + ((key, p) for key, p in people.items() if p["id"] not in signed), + key=lambda item: -item[1]["lines"], + ) + print(f"# CLA audit — {len(unsigned)} unsigned of {len(people)} contributors with code at HEAD\n") + print("| Contributor | Lines at HEAD | Largest files |\n|---|---:|---|") + for key, p in unsigned: + label = f"@{p['login']}" if p["login"] else f"`{key}`" + if p["notes"]: + label += " (" + "; ".join(NOTES[n] for n in sorted(p["notes"])) + ")" + top = ", ".join(f"`{f}` ({n})" for f, n in p["files"].most_common(3)) + print(f"| {label} | {p['lines']} | {top} |") + if folded: + print("\n## Maintainer commits that fold in other people's work (review by hand)\n") + for commit, n in folded.most_common(): + subject = messages.get(commit, "").splitlines()[0] if messages.get(commit) else "" + print(f"- `{commit[:9]}` {subject} — {n} lines at HEAD") + + if args.json: + args.json.write_text(json.dumps({ + "people": {key: {"login": p["login"], "id": p["id"], "lines": p["lines"], "emails": sorted(p["emails"]), + "signed": p["id"] in signed, "notes": sorted(p["notes"]), + "files": p["files"].most_common()} for key, p in people.items()}, + "folded_commits": dict(folded), + }, indent=1)) + + +if __name__ == "__main__": + main() diff --git a/tests/test_cla_check.py b/tests/test_cla_check.py new file mode 100644 index 000000000..d198e4e38 --- /dev/null +++ b/tests/test_cla_check.py @@ -0,0 +1,244 @@ +"""Unit tests for .github/scripts/cla_check.py (the CLA pull-request check).""" +from __future__ import annotations + +import base64 +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +_SCRIPT = Path(__file__).resolve().parents[1] / ".github" / "scripts" / "cla_check.py" +_spec = importlib.util.spec_from_file_location("cla_check", _SCRIPT) +cla = importlib.util.module_from_spec(_spec) +sys.modules["cla_check"] = cla # dataclasses resolve their module by name +_spec.loader.exec_module(cla) + +OPENER = cla.Person("alice", 1001) +MAINTAINER = cla.Person("debpalash", 4178343) + + +def actor(login=None, id=None, name="Someone", email="someone@example.com"): + return {"name": name, "email": email, "login": login, "id": id} + + +# ── Signing comments ──────────────────────────────────────────────────── + + +@pytest.mark.parametrize("body", [ + cla.SIGN_PHRASE, + cla.SIGN_PHRASE + "\n", + " " + cla.SIGN_PHRASE.upper() + " ", + "Thanks!\n\n" + cla.SIGN_PHRASE + "\n\nCheers", + cla.SIGN_PHRASE.replace(" ", " "), +]) +def test_sign_phrase_is_recognised_on_its_own_line(body): + assert cla.is_sign_comment(body) + + +@pytest.mark.parametrize("body", [ + "> " + cla.SIGN_PHRASE, # quoting the bot is not signing + "I will sign: " + cla.SIGN_PHRASE, + cla.SIGN_PHRASE.replace("1.0", "0.9"), + "", + None, +]) +def test_quotes_and_other_text_are_not_signatures(body): + assert not cla.is_sign_comment(body) + + +# ── Who must sign ─────────────────────────────────────────────────────── + + +def test_signed_opener_and_authors_pass(): + result = cla.evaluate(OPENER, False, [actor("alice", 1001), actor("bob", 1002)], {1001, 1002}) + assert result.passed + + +def test_unsigned_linked_author_is_listed_once(): + result = cla.evaluate(OPENER, False, [actor("bob", 1002), actor("bob", 1002)], {1001}) + assert result.unsigned == [cla.Person("bob", 1002)] + + +def test_opener_must_sign_even_without_commits(): + result = cla.evaluate(OPENER, False, [], set()) + assert result.unsigned == [OPENER] + + +def test_git_name_cannot_impersonate_the_maintainer(): + # Unlinked email with the maintainer's name: matched by account, not name. + spoof = actor(name="debpalash", email="someone@gmail.com") + result = cla.evaluate(OPENER, False, [spoof], {1001}) + assert result.unknown == ["debpalash "] + + +def test_maintainer_and_bots_are_exempt_by_id(): + actors = [actor("debpalash", 4178343), actor("dependabot[bot]", 49699333)] + assert cla.evaluate(MAINTAINER, False, actors, set()).passed + + +def test_noreply_email_maps_to_account_id(): + result = cla.evaluate(OPENER, False, [actor(email="2002+carol@users.noreply.github.com")], {1001}) + assert result.unsigned == [cla.Person("carol", 2002)] + + +def test_ai_tool_co_authors_are_skipped_but_people_are_not(): + actors = cla.co_authors( + "fix: thing\n\nCo-authored-by: Cursor Agent \n" + "Co-authored-by: Dave <3003+dave@users.noreply.github.com>\n" + ) + result = cla.evaluate(OPENER, False, actors, {1001}) + assert result.unsigned == [cla.Person("dave", 3003)] and not result.unknown + + +def test_bot_opened_pr_skips_bot_commits_but_not_people(): + bot = cla.Person("dependabot[bot]", 49699333) + actors = [actor(email="49699333+dependabot[bot]@users.noreply.github.com"), actor("erin", 4004)] + result = cla.evaluate(bot, True, actors, set()) + assert result.unsigned == [cla.Person("erin", 4004)] + + +def test_unknown_identity_fails_the_check(): + result = cla.evaluate(OPENER, False, [actor(name="mergetest", email="test@local")], {1001}) + assert not result.passed and result.unknown == ["mergetest "] + + +# ── Store and comment ─────────────────────────────────────────────────── + + +def test_signatures_are_added_once_per_account(): + store = cla.empty_store() + assert cla.add_signatures(store, [{"id": 1, "login": "a"}]) + assert not cla.add_signatures(store, [{"id": 1, "login": "a-renamed"}]) + assert [s["id"] for s in store["signatures"]] == [1] + + +def test_comment_renders_untrusted_identities_as_inert_code(): + evaluation = cla.Evaluation(unknown=["[click](https://evil.example) @everyone "]) + body = cla.render_comment(evaluation, "https://example.test/cla") + line = next(l for l in body.splitlines() if "evil" in l) + assert line.startswith("- `") and line.count("`") == 2 + assert cla.SIGN_PHRASE not in body # nobody listed as unsigned → no sign instructions + + +# ── End-to-end with a fake GitHub ─────────────────────────────────────── + + +class FakeGitHub: + repo = "debpalash/VoiceStudio" + + def __init__(self, comments, actors, store=None): + self.comments, self.actors = comments, actors + self.files = {} if store is None else {cla.SIGNATURE_PATH: store} + self.branch = store is not None + self.statuses, self.posted, self.patched = [], [], [] + + def get(self, path): + assert path.endswith("/pulls/7") + return {"state": "open", "user": {"login": "alice", "id": 1001, "type": "User"}, + "head": {"sha": "abc"}, "base": {"repo": {"id": 99, "default_branch": "main"}}} + + def paginate(self, path, limit=None): + return self.comments + + def graphql(self, query, variables): + nodes = [{"commit": {"authors": {"nodes": [ + {"name": a["name"], "email": a["email"], + "user": {"login": a["login"], "databaseId": a["id"]} if a["id"] else None}]}}} for a in self.actors] + return {"repository": {"pullRequest": {"commits": { + "pageInfo": {"hasNextPage": False, "endCursor": None}, "nodes": nodes}}}} + + def request(self, method, path, body=None): + if "/contents/" in path and method == "GET": + if cla.SIGNATURE_PATH not in self.files: + return 404, None, {} + raw = base64.b64encode(json.dumps(self.files[cla.SIGNATURE_PATH]).encode()).decode() + return 200, {"content": raw, "sha": "s1"}, {} + if "/contents/" in path and method == "PUT": + self.files[cla.SIGNATURE_PATH] = json.loads(base64.b64decode(body["content"])) + return 201, {}, {} + if "/git/ref/heads/" in path: + return (200 if self.branch else 404), {}, {} + if path.endswith("/git/trees"): + return 201, {"sha": "t"}, {} + if path.endswith("/git/commits"): + assert body["parents"] == [] + return 201, {"sha": "c"}, {} + if path.endswith("/git/refs"): + self.branch = True + return 201, {}, {} + if "/statuses/" in path: + self.statuses.append(body) + return 201, {}, {} + if method == "POST" and path.endswith("/comments"): + self.posted.append(body["body"]) + return 201, {}, {} + if method == "PATCH": + self.patched.append(body["body"]) + return 200, {}, {} + raise AssertionError(f"unexpected {method} {path}") + + +def test_first_signature_creates_the_store_and_turns_the_status_green(): + comments = [{"id": 5, "user": {"id": 1001, "login": "alice", "type": "User"}, + "body": cla.SIGN_PHRASE, "created_at": "2026-10-02T00:00:00Z"}] + gh = FakeGitHub(comments, [actor("alice", 1001)]) + result = cla.run(gh, "issue_comment", {"issue": {"number": 7}}) + assert result.passed and gh.branch + saved = gh.files[cla.SIGNATURE_PATH]["signatures"] + assert saved == [{"login": "alice", "id": 1001, "pull_request": 7, "comment_id": 5, + "signed_at": "2026-10-02T00:00:00Z", "version": cla.CLA_VERSION, "repo_id": 99}] + assert gh.statuses[-1]["state"] == "success" and gh.statuses[-1]["context"] == "CLA" + assert not gh.posted # nothing to ask for, so no new comment + + +def test_unsigned_pr_fails_and_asks_once(): + gh = FakeGitHub([], [actor("alice", 1001)], store=cla.empty_store()) + result = cla.run(gh, "pull_request_target", {"pull_request": {"number": 7}}) + assert not result.passed and gh.statuses[-1]["state"] == "failure" + assert len(gh.posted) == 1 and "@alice" in gh.posted[0] + + +def test_someone_elses_sign_comment_does_not_count(): + comments = [{"id": 5, "user": {"id": 2002, "login": "mallory", "type": "User"}, + "body": cla.SIGN_PHRASE, "created_at": "2026-10-02T00:00:00Z"}] + gh = FakeGitHub(comments, [actor("alice", 1001)], store=cla.empty_store()) + assert not cla.run(gh, "issue_comment", {"issue": {"number": 7}}).passed + assert gh.files[cla.SIGNATURE_PATH]["signatures"] == [] + + +def _issue_event(labels, user_type="User", body=None): + return {"issue": {"number": 42, "labels": [{"name": l} for l in labels]}, + "comment": {"id": 9, "body": body or cla.SIGN_PHRASE, "created_at": "2026-10-02T00:00:00Z", + "user": {"login": "velixio", "id": 270455167, "type": user_type}}, + "repository": {"id": 99}} + + +def test_past_contributors_sign_on_a_labelled_issue(): + gh = FakeGitHub([], [], store=cla.empty_store()) + gh.request_log = [] + original = gh.request + + def request(method, path, body=None): + if path.endswith("/reactions"): + gh.request_log.append(body) + return 201, {}, {} + return original(method, path, body) + + gh.request = request + assert cla.sign_on_issue(gh, _issue_event(["cla"])) + assert gh.files[cla.SIGNATURE_PATH]["signatures"][0]["id"] == 270455167 + assert gh.files[cla.SIGNATURE_PATH]["signatures"][0]["issue"] == 42 + assert gh.request_log == [{"content": "+1"}] + + +@pytest.mark.parametrize("event", [ + _issue_event([]), # unlabelled issue + _issue_event(["cla"], user_type="Bot"), # bots cannot sign + _issue_event(["cla"], body="> " + cla.SIGN_PHRASE), +]) +def test_issue_signing_requires_the_label_a_person_and_the_phrase(event): + gh = FakeGitHub([], [], store=cla.empty_store()) + assert not cla.sign_on_issue(gh, event) + assert gh.files[cla.SIGNATURE_PATH]["signatures"] == [] diff --git a/tests/test_cla_workflow.py b/tests/test_cla_workflow.py new file mode 100644 index 000000000..07c1a82be --- /dev/null +++ b/tests/test_cla_workflow.py @@ -0,0 +1,106 @@ +"""Guards for the Contributor License Agreement check (.github/workflows/cla.yml). + +A signature is evidence of agreement to one exact text, so published agreements +are pinned by hash: changing the terms means a new version file, sign phrase, +and signature store, never an edit in place. These tests also keep the checker, +workflow, and docs in step, and keep workflows that hold a write token on +untrusted events from running pull-request code. +""" +from __future__ import annotations + +import hashlib +import importlib.util +import re +import sys +from pathlib import Path + +import yaml + +_REPO = Path(__file__).resolve().parents[1] +_WORKFLOWS = _REPO / ".github" / "workflows" +_CLA_WORKFLOW = _WORKFLOWS / "cla.yml" +# Triggers that run with a write token and repository secrets, even on fork PRs. +_PRIVILEGED_TRIGGERS = {"pull_request_target", "issue_comment", "workflow_run"} +# SHA-256 of each published agreement with LF line endings. Never update a hash: +# publish a new version file instead, so existing signatures keep their text. +_PUBLISHED_AGREEMENTS = { + ".github/CLA-1.0.md": "b21cab097b9ea9b7f9d938724910276bd1253083800f42d5e176c9d335086b84", + ".github/CCLA-1.0.md": "4207f055243a9c4c87f89dcb1de49e61b46ffb1605ae020e848709c8705c9d5c", +} + +_spec = importlib.util.spec_from_file_location("cla_check", _REPO / ".github" / "scripts" / "cla_check.py") +cla = importlib.util.module_from_spec(_spec) +sys.modules["cla_check"] = cla +_spec.loader.exec_module(cla) + + +def _load(path: Path) -> dict: + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + # PyYAML reads the bare `on:` key as boolean True. + data["on"] = data.pop(True, data.get("on")) or {} + return data + + +def _triggers(workflow: dict) -> set[str]: + on = workflow["on"] + return set(on) if isinstance(on, (dict, list)) else {on} + + +def test_checker_document_store_and_phrase_share_one_version(): + version = cla.CLA_VERSION + assert cla.DOCUMENT_PATH == f".github/CLA-{version}.md" + assert cla.SIGNATURE_PATH == f"signatures/v{version}/cla.json" + assert f"CLA {version} " in cla.SIGN_PHRASE + assert (_REPO / cla.DOCUMENT_PATH).is_file() + + +def test_sign_phrase_matches_the_agreement_and_contributing(): + for path in (_REPO / cla.DOCUMENT_PATH, _REPO / ".github" / "CONTRIBUTING.md"): + assert cla.SIGN_PHRASE in path.read_text(encoding="utf-8"), path + + +def test_workflow_comment_filter_admits_the_sign_phrase_and_recheck(): + job_filter = _load(_CLA_WORKFLOW)["jobs"]["cla"]["if"] + needles = re.findall(r"contains\(github\.event\.comment\.body, '([^']+)'\)", job_filter) + assert any(n in cla.SIGN_PHRASE for n in needles) + assert "recheck" in needles + + +def test_published_agreements_are_never_edited(): + for path, expected in _PUBLISHED_AGREEMENTS.items(): + text = (_REPO / path).read_text(encoding="utf-8") + assert hashlib.sha256(text.encode()).hexdigest() == expected, ( + f"{path} changed. Publish the new terms as a new version file instead." + ) + + +def test_cla_workflow_actions_are_pinned_to_commits(): + for job in _load(_CLA_WORKFLOW)["jobs"].values(): + for step in job.get("steps", []): + if "uses" in step: + ref = step["uses"].split("@", 1)[1] + assert re.fullmatch(r"[0-9a-f]{40}", ref), step["uses"] + + +def test_signatures_never_go_to_main(): + assert cla.SIGNATURE_BRANCH not in {"main", "master"} + + +def test_privileged_workflows_never_run_pull_request_code(): + for path in sorted(_WORKFLOWS.glob("*.y*ml")): + workflow = _load(path) + if not _triggers(workflow) & _PRIVILEGED_TRIGGERS: + continue + text = path.read_text(encoding="utf-8") + assert "pull_request.head" not in text and "head_sha" not in text, f"{path.name} references PR head code" + for name, job in (workflow.get("jobs") or {}).items(): + where = f"{path.name}:{name}" + assert "uses" not in job, f"{where} calls a reusable workflow" + for step in job.get("steps", []): + uses = step.get("uses", "") + assert not uses.startswith("actions/download-artifact@"), f"{where} downloads artifacts" + if uses.startswith("actions/checkout@"): + options = step.get("with") or {} + assert not {"ref", "repository"} & set(options), f"{where} checks out a non-base ref" + assert options.get("persist-credentials") is False, f"{where} keeps credentials" + assert "${{ github.event" not in str(step.get("run", "")), f"{where} interpolates event data" From a7052b986fab3e96180fd2439df9753f5190e152 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:04:10 +0530 Subject: [PATCH 02/85] ci: pin third-party actions to commit SHAs and track them with Dependabot Pin every remote action in .github/workflows to the 40-char commit SHA its current tag/branch resolves to (no behaviour change), add a grouped weekly github-actions Dependabot config, and add tests/test_actions_pinned.py to fail CI on any unpinned remote action. --- .github/CONTRIBUTING.md | 3 + .github/dependabot.yml | 14 +++ .github/workflows/build-omnivoice-tts.yml | 4 +- .github/workflows/ci.yml | 32 +++---- .github/workflows/cosyvoice-dependencies.yml | 4 +- .github/workflows/docker.yml | 28 +++--- .github/workflows/docs-drift.yml | 6 +- .github/workflows/electron-build.yml | 14 +-- .github/workflows/electron-release.yml | 18 ++-- .github/workflows/evals.yml | 8 +- .github/workflows/install-smoke.yml | 8 +- .github/workflows/security.yml | 24 +++--- tests/test_actions_pinned.py | 89 ++++++++++++++++++++ 13 files changed, 179 insertions(+), 73 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 tests/test_actions_pinned.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c9875f5c3..a40726192 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -323,6 +323,9 @@ hard rules from the first prompt. - **Security posture:** the backend serves loopback HTTP — treat every query/path/form parameter as hostile. User-chosen filesystem destinations are authorized in Electron main (native save dialog), never via HTTP params. +- **CI supply chain:** every remote action in `.github/workflows/` is pinned + to a full 40-char commit SHA with a trailing `# vX.Y.Z` comment + (`tests/test_actions_pinned.py`); Dependabot bumps the pins weekly. ## Contribution licensing diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..87cbb4223 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + # Keeps the SHA-pinned actions in .github/workflows current (pins are + # enforced by tests/test_actions_pinned.py). One grouped PR per week. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - "*" + commit-message: + prefix: "chore(ci)" diff --git a/.github/workflows/build-omnivoice-tts.yml b/.github/workflows/build-omnivoice-tts.yml index 5ad0ada85..74400cf77 100644 --- a/.github/workflows/build-omnivoice-tts.yml +++ b/.github/workflows/build-omnivoice-tts.yml @@ -67,7 +67,7 @@ jobs: # at the pinned SHA (#2105); non-experimental to catch regressions. experimental: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Read pinned omnivoice.cpp SHA from quant_map.json @@ -110,7 +110,7 @@ jobs: --platform "$PLATFORM" \ --commit-sha "$PIN_SHA" - name: Upload binary artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: omnivoice-tts-${{ matrix.platform }} path: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b698efb69..fb29eef8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,8 @@ jobs: name: Electron installer contracts runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22' - name: Shell installers and Worker routing @@ -45,17 +45,17 @@ jobs: UV_HTTP_TIMEOUT: "120" UV_HTTP_RETRIES: "5" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" # enable-cache persists ~/.cache/uv across runs, keyed on uv.lock — # turns `uv sync` from ~45 s cold to ~5 s warm. - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4 with: enable-cache: true cache-dependency-glob: "uv.lock" @@ -63,16 +63,16 @@ jobs: # Node 22 is needed for --experimental-strip-types so node:test can # import .ts files directly from electron/src/shared/api/*. - name: Setup Node 22 - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22' - name: Setup Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@f4d14e03ff726c06358e5557344e1da148b56cf7 # v1.2.2 # apt install ffmpeg is ~30 s every run; cache the resolved .debs. - name: System deps (ffmpeg) - uses: awalsh128/cache-apt-pkgs-action@v1.6.3 + uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 with: packages: ffmpeg version: 1.0 @@ -115,7 +115,7 @@ jobs: # Cache ~/.bun/install/cache keyed on bun.lock — `bun install` drops # from ~15 s cold to near-instant on warm cache. - name: Cache bun deps - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} @@ -169,12 +169,12 @@ jobs: name: Linux native package runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22' - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: native/desktop-bridge -> target key: linux-native-package @@ -237,15 +237,15 @@ jobs: UV_HTTP_TIMEOUT: "120" UV_HTTP_RETRIES: "5" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Setup Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4 with: enable-cache: true cache-dependency-glob: "uv.lock" @@ -300,7 +300,7 @@ jobs: - name: System deps (Linux) if: runner.os == 'Linux' && matrix.backend_supported - uses: awalsh128/cache-apt-pkgs-action@v1.6.3 + uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 with: packages: ffmpeg libsndfile1 version: 1.0 diff --git a/.github/workflows/cosyvoice-dependencies.yml b/.github/workflows/cosyvoice-dependencies.yml index 949986f45..12373fe14 100644 --- a/.github/workflows/cosyvoice-dependencies.yml +++ b/.github/workflows/cosyvoice-dependencies.yml @@ -20,8 +20,8 @@ jobs: runs-on: ${{ matrix.os }} timeout-minutes: 15 steps: - - uses: actions/checkout@v4 - - uses: astral-sh/setup-uv@v3 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4 - name: Build and import Python 3.10 dependencies shell: bash run: | diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index e686f0fd7..c89508681 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -74,7 +74,7 @@ jobs: build-and-push: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: ${{ inputs.backfill_release && inputs.release_ref || github.sha }} @@ -114,13 +114,13 @@ jobs: # QEMU enables cross-platform builds (arm64 on x64 runner). # Skipped for now — only building linux/amd64. - # - uses: docker/setup-qemu-action@v3 + # - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -134,7 +134,7 @@ jobs: - name: Log in to Docker Hub if: steps.dockerhub.outputs.enabled == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -150,7 +150,7 @@ jobs: # tags (those contain a `-`) so a prerelease can't clobber it. - name: Extract metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: # Same tag set applied to both registries. The Docker Hub line is # blank when the secret is unset, so metadata-action emits GHCR-only @@ -168,7 +168,7 @@ jobs: type=raw,value=sha-${{ steps.source.outputs.short_sha }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . file: deploy/Dockerfile @@ -223,7 +223,7 @@ jobs: - name: Update Docker Hub description if: steps.dockerhub.outputs.enabled == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' continue-on-error: true - uses: peter-evans/dockerhub-description@v4 + uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -240,7 +240,7 @@ jobs: build-and-push-rocm: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: ${{ inputs.backfill_release && inputs.release_ref || github.sha }} @@ -278,10 +278,10 @@ jobs: df -h / - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - name: Log in to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -295,7 +295,7 @@ jobs: - name: Log in to Docker Hub if: steps.dockerhub.outputs.enabled == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -304,7 +304,7 @@ jobs: # prerelease gating) with a `-rocm` suffix on every tag. - name: Extract metadata (tags, labels) id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -327,7 +327,7 @@ jobs: # multi-GB ROCm runtime layers would blow GitHub's 10 GB per-repo # Actions cache budget and evict the CUDA job's cache. - name: Build and push (ROCm) - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . file: deploy/Dockerfile diff --git a/.github/workflows/docs-drift.yml b/.github/workflows/docs-drift.yml index ad15abec1..19832031a 100644 --- a/.github/workflows/docs-drift.yml +++ b/.github/workflows/docs-drift.yml @@ -25,9 +25,9 @@ jobs: drift: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" @@ -40,7 +40,7 @@ jobs: run: python scripts/check-docs-drift.py --output drift-report.md - name: Update rolling docs-drift issue - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: DRIFT_OUTCOME: ${{ steps.drift.outcome }} with: diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index d353c3a75..3cee8128a 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -61,21 +61,21 @@ jobs: VOICESTUDIO_UPDATE_CHANNEL: electron-preview-${{ matrix.platform }}-${{ matrix.arch }} CSC_IDENTITY_AUTO_DISCOVERY: 'false' steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22' - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: '1.4.2' - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: native/desktop-bridge -> target key: electron-${{ matrix.target }} - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: '0.12.13' enable-cache: false @@ -136,7 +136,7 @@ jobs: node tests/packaged-smoke.mjs --install fi - name: Save installers and updater metadata for review - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: electron-rehearsal-${{ matrix.platform }}-${{ matrix.arch }} retention-days: 14 diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index 669e443da..e4361943a 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -35,7 +35,7 @@ jobs: if: github.event_name == 'workflow_dispatch' || github.ref_name != vars.TAURI_SUNSET_TAG runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: ${{ env.RELEASE_REF }} - name: Require an exact version tag @@ -107,23 +107,23 @@ jobs: VITE_POSTHOG_KEY: ${{ secrets.POSTHOG_PROJECT_TOKEN }} VITE_POSTHOG_HOST: ${{ vars.POSTHOG_HOST }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: ${{ env.RELEASE_REF }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22' - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: '1.4.2' - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 with: workspaces: native/desktop-bridge -> target key: electron-${{ matrix.target }} - - uses: astral-sh/setup-uv@v6 + - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 with: version: '0.12.13' enable-cache: false @@ -208,7 +208,7 @@ jobs: node tests/packaged-smoke.mjs --setup fi - name: Save installers and updater metadata for review - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: electron-release-${{ matrix.platform }}-${{ matrix.arch }} retention-days: 14 @@ -228,10 +228,10 @@ jobs: SUNSET_TAG: ${{ vars.TAURI_SUNSET_TAG }} PUBLISH: ${{ inputs.publish }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: ${{ env.RELEASE_REF }} - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: pattern: electron-release-* merge-multiple: true diff --git a/.github/workflows/evals.yml b/.github/workflows/evals.yml index 84d49b80c..60411ac04 100644 --- a/.github/workflows/evals.yml +++ b/.github/workflows/evals.yml @@ -26,13 +26,13 @@ jobs: evals: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" - - uses: astral-sh/setup-uv@v3 + - uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4 with: enable-cache: true cache-dependency-glob: "uv.lock" @@ -48,7 +48,7 @@ jobs: run: uv run --no-sync python tests/evals/run_evals.py --output eval-report.json - name: Upload report artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: eval-report path: eval-report.json diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index f0af3b811..429884c70 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -21,14 +21,14 @@ jobs: matrix: os: [ubuntu-22.04, macos-latest, windows-latest] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: '22' - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version-file: package.json - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@6bed0761d98439e5a578e2877258200ad565ba87 # stable - name: Linux build dependencies if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y build-essential pkg-config libasound2-dev libxdo-dev libxtst-dev libx11-dev libxkbcommon-dev libwayland-dev libssl-dev binutils zsync diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9ba7c5fa0..1ff8b15e0 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -48,14 +48,14 @@ jobs: name: Secret scan (gitleaks) runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: # gitleaks needs full history to scan all commits on push events. fetch-depth: 0 # No authed git needed after clone; don't persist GITHUB_TOKEN. persist-credentials: false - name: gitleaks - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITLEAKS_LICENSE is only required for GitHub *organizations*; this is @@ -73,12 +73,12 @@ jobs: matrix: language: [python, javascript-typescript] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@1190a975f95ce23525efb6a3fc21ea29567c1b52 # v3.38.2 with: languages: ${{ matrix.language }} # Both targets are interpreted — no compiled build step needed. @@ -105,7 +105,7 @@ jobs: - "**/*.test.tsx" - name: Analyze - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@1190a975f95ce23525efb6a3fc21ea29567c1b52 # v3.38.2 with: category: "/language:${{ matrix.language }}" @@ -117,12 +117,12 @@ jobs: contents: read security-events: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Setup Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" @@ -142,7 +142,7 @@ jobs: # continue-on-error: this job is reporting-only. If bandit can't write a # SARIF for any reason (no findings dir, pipx hiccup), don't fail the job. - name: Upload bandit SARIF - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@1190a975f95ce23525efb6a3fc21ea29567c1b52 # v3.38.2 if: always() continue-on-error: true with: @@ -154,18 +154,18 @@ jobs: name: Dependency audit runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3.2.4 with: enable-cache: true cache-dependency-glob: "uv.lock" - name: Setup Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" @@ -179,7 +179,7 @@ jobs: # Pin a floor: `bun audit` was added in bun 1.2.x, so guarantee it exists. - name: Setup Bun - uses: oven-sh/setup-bun@v1 + uses: oven-sh/setup-bun@f4d14e03ff726c06358e5557344e1da148b56cf7 # v1.2.2 with: bun-version: "1.2" diff --git a/tests/test_actions_pinned.py b/tests/test_actions_pinned.py new file mode 100644 index 000000000..1bf15758f --- /dev/null +++ b/tests/test_actions_pinned.py @@ -0,0 +1,89 @@ +"""Every remote GitHub Action must be pinned to a full commit SHA. + +A tag (``@v4``) or branch (``@stable``) is a mutable pointer: whoever controls +the action's repository can move it, and the next CI run executes the new code +with this repo's secrets (Docker Hub / GHCR push tokens, release upload). A +40-hex commit SHA is immutable. The trailing ``# vX.Y.Z`` comment keeps the pin +human-readable and lets Dependabot (``.github/dependabot.yml``) bump it. + +Local (``./``) actions and ``docker://`` images are exempt. +""" +from __future__ import annotations + +import pathlib +import re + +import yaml + +_ROOT = pathlib.Path(__file__).resolve().parent.parent +_GITHUB = _ROOT / ".github" +_SHA = re.compile(r"^[^@\s]+@[0-9a-f]{40}$") + + +def _files() -> list[pathlib.Path]: + found = sorted((_GITHUB / "workflows").glob("*.y*ml")) + found += sorted(_GITHUB.rglob("action.y*ml")) # composite actions + return found + + +def _uses(doc: dict) -> list[str]: + """Collect ``uses:`` values from jobs/steps (workflows) and runs.steps (composite).""" + out: list[str] = [] + steps: list = [] + for job in (doc.get("jobs") or {}).values(): + if isinstance(job, dict): + if "uses" in job: # reusable workflow call + out.append(str(job["uses"])) + steps += job.get("steps") or [] + steps += (doc.get("runs") or {}).get("steps") or [] + out += [str(s["uses"]) for s in steps if isinstance(s, dict) and "uses" in s] + return out + + +def _remote(ref: str) -> bool: + return not ref.startswith(("./", "docker://")) + + +def _collect() -> dict[pathlib.Path, list[str]]: + result = {} + for path in _files(): + doc = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + result[path] = [u for u in _uses(doc) if _remote(u)] + return result + + +def test_scanner_finds_actions(): + # Guard against a vacuous pass if the layout or parsing ever changes. + assert sum(len(v) for v in _collect().values()) > 0 + + +def test_remote_actions_pinned_to_commit_sha(): + bad = [ + f"{p.relative_to(_ROOT)}: {u}" + for p, refs in _collect().items() + for u in refs + if not _SHA.match(u) + ] + assert not bad, ( + "Pin these actions to a full 40-char commit SHA with a trailing " + "'# vX.Y.Z' comment (resolve via `git ls-remote --tags " + "https://github.com//` and use the peeled ^{} SHA):\n" + + "\n".join(bad) + ) + + +def test_pins_carry_version_comment(): + bad = [] + for path, refs in _collect().items(): + text = path.read_text(encoding="utf-8") + for u in set(refs): + for m in re.finditer(rf"uses:\s*{re.escape(u)}([^\n]*)", text): + if not re.match(r"\s+#\s*\S", m.group(1)): + bad.append(f"{path.relative_to(_ROOT)}: {u}") + assert not bad, "Add a '# vX.Y.Z' comment after each SHA pin:\n" + "\n".join(bad) + + +def test_dependabot_tracks_github_actions(): + cfg = yaml.safe_load((_GITHUB / "dependabot.yml").read_text(encoding="utf-8")) + ecosystems = {u.get("package-ecosystem") for u in cfg.get("updates", [])} + assert "github-actions" in ecosystems From 33ab3ae55d25337c7d0e163b4cc4a628ad74c04a Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:05:27 +0530 Subject: [PATCH 03/85] chore(governance): add CODEOWNERS, repository-settings guide, and refresh stale docs - CODEOWNERS for workflows, licensing, CLA, release/packaging, version and lockfiles - LICENSE-NOTICE scope now describes the Electron layout and real lockfile paths - CONTRIBUTING local-first gate lists the sanctioned outbound calls - RELEASING adds a CLA audit step before tagging - docs/maintainers/repository-settings.md documents rulesets and security settings --- .github/CODEOWNERS | 33 ++++++ .github/CONTRIBUTING.md | 14 ++- LICENSE-NOTICE.md | 13 +-- docs/RELEASING.md | 7 +- docs/maintainers/repository-settings.md | 138 ++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 docs/maintainers/repository-settings.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..5b0d93b12 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,33 @@ +# Code owners: GitHub requests review from these owners on matching pull requests. +# Scope is limited to governance, licensing, release, and dependency surfaces. +# There is deliberately no catch-all `*` rule. The last matching pattern wins. +# Settings that cannot live in code: docs/maintainers/repository-settings.md + +# Governance and CI +/.github/CODEOWNERS @debpalash +/.github/workflows/ @debpalash +/.github/scripts/ @debpalash + +# Licensing and contributor agreements +/LICENSE @debpalash +/LICENSE-NOTICE.md @debpalash +/.github/CLA-*.md @debpalash +/.github/CCLA-*.md @debpalash + +# Release, packaging, and container builds +/electron/electron-builder.config.mjs @debpalash +/electron/build/ @debpalash +/electron/scripts/embed-appimage-update.mjs @debpalash +/scripts/prepare_electron_release.py @debpalash +/scripts/check_electron_release_assets.py @debpalash +/scripts/clear-release-rerun-assets.py @debpalash +/scripts/verify-macos-signing.sh @debpalash +/deploy/ @debpalash +/docs/RELEASING.md @debpalash + +# Version source, its mirrors, and lockfiles +/package.json @debpalash +/pyproject.toml @debpalash +/backend/core/version.py @debpalash +/bun.lock @debpalash +/uv.lock @debpalash diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a40726192..f7005feb5 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -317,9 +317,17 @@ hard rules from the first prompt. path — existing `omnivoice_data/` must keep working with no manual steps. - **Engine back-compat:** already-installed engines (model weights on disk) must not require reinstall or re-download. -- **Local-first:** no new outbound calls except GitHub Issues (opt-in - reporting) and HuggingFace model downloads. Never log or persist secrets or - absolute home paths. +- **Local-first:** no new outbound calls, and the app must work fully offline + with every prompt declined. The only sanctioned ones are: Hugging Face model + downloads (gated on install state or an explicit user action); bug reports + as prefilled GitHub Issue URLs opened in the user's browser; PostHog + analytics only after a yes at the first-run consent prompt + (`backend/core/analytics.py`, allowlisted content-free metadata); the + GitHub star count (no credentials or referrer, refreshed every 20 minutes + while shown); packaged-app update checks against GitHub Releases (downloads + wait for the user); and the Lemon Squeezy Pro licence check, only after the + user enters a key (`electron/src/main/pro-license.ts`). Adding to this list + needs owner approval. Never log or persist secrets or absolute home paths. - **Security posture:** the backend serves loopback HTTP — treat every query/path/form parameter as hostile. User-chosen filesystem destinations are authorized in Electron main (native save dialog), never via HTTP params. diff --git a/LICENSE-NOTICE.md b/LICENSE-NOTICE.md index 5a4458f58..4391e88a6 100644 --- a/LICENSE-NOTICE.md +++ b/LICENSE-NOTICE.md @@ -36,10 +36,11 @@ AGPL-3.0 text in [`LICENSE`](LICENSE).) ### Scope -These terms cover the VoiceStudio application — the Tauri desktop shell -(`frontend/src-tauri/`), the React frontend (`frontend/src/`), the FastAPI -backend (`backend/`), and supporting build / packaging scripts (`scripts/`, -`Dockerfile`, `docker-compose.yml`, `.github/`). +These terms cover the VoiceStudio application — the Electron desktop and web +app (`electron/`), its native desktop helper (`native/`), the FastAPI backend +(`backend/`), and supporting build / packaging files (`scripts/`, `deploy/`, +`.github/`). Electron components adapted from T3 Code keep their MIT notice in +`electron/T3CODE-LICENSE.txt`. The bundled `omnivoice/` Python package — the underlying TTS model by Han Zhu — is **separately licensed under Apache License 2.0** by its upstream authors and @@ -52,8 +53,8 @@ weights as CC-BY-NC. Its `audio_tokenizer/LICENSE` contains separate Boson Higgs Audio 2 and Meta Llama community terms. A commercial license for VoiceStudio-owned code does not replace any of those terms. -Third-party dependencies retain their own licenses. See `Cargo.lock`, -`bun.lock`, and `uv.lock` for the resolved set. +Third-party dependencies retain their own licenses. See `bun.lock`, `uv.lock`, +and `native/desktop-bridge/Cargo.lock` for the resolved set. ### Reference diff --git a/docs/RELEASING.md b/docs/RELEASING.md index e3dfbd179..a4e580080 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -56,7 +56,12 @@ version only after validation. include migration steps and real screenshots when relevant, and verify human contributors and bug reporters from the tag comparison and included PRs. 5. Run `uv run pytest tests/test_app_version.py tests/test_changelog_style.py -q`. -6. Confirm the version matches the intended tag. +6. Audit contributor agreements: `git fetch origin cla-signatures`, then + `GH_TOKEN=... python scripts/cla_audit.py`. Follow up with each unsigned + contributor it lists, asking them to sign `.github/CLA-1.0.md` (or their + employer the Corporate CLA). This step applies once the CLA workflow and + `scripts/cla_audit.py` are on `main`. +7. Confirm the version matches the intended tag. ## Build a release draft diff --git a/docs/maintainers/repository-settings.md b/docs/maintainers/repository-settings.md new file mode 100644 index 000000000..22a48c922 --- /dev/null +++ b/docs/maintainers/repository-settings.md @@ -0,0 +1,138 @@ +# Repository settings + +GitHub stores these settings outside the repository, so no file or CI check can +enforce them. Apply them once as a repository admin, then re-check them after +changing workflow or job names. The `gh api` commands below assume an +authenticated `gh` with admin access to `debpalash/VoiceStudio`. + +## Ruleset: `main` + +Requires the backend/frontend test job and the CLA check, and blocks force-push +and deletion. Required checks also stop direct pushes, so changes land through +pull requests. + +The `context` values must match the check names shown on a pull request: + +- `Tests (backend + frontend)`: the `name:` of the `test` job in `.github/workflows/ci.yml`. +- `cla`: the job ID in `.github/workflows/cla.yml`. That job has no `name:`, so + its check is called `cla`. If the job gains `name: CLA`, use `CLA` here. + +`integration_id` 15368 is GitHub Actions, so only workflow runs can satisfy +these checks. `strict_required_status_checks_policy` requires branches to be up +to date with `main` before merging. + +```bash +gh api --method POST repos/debpalash/VoiceStudio/rulesets --input - <<'JSON' +{ + "name": "main", + "target": "branch", + "enforcement": "active", + "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, + "bypass_actors": [], + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": true, + "required_status_checks": [ + { "context": "Tests (backend + frontend)", "integration_id": 15368 }, + { "context": "cla", "integration_id": 15368 } + ] + } + } + ] +} +JSON +``` + +Leave `bypass_actors` empty unless the owner chooses an emergency bypass. To add +one, use `{ "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request" }` +(repository admins, pull requests only). Do not enable "Require review from +Code Owners" while `@debpalash` is the only code owner, because GitHub does not +let authors approve their own pull requests. + +UI: **Settings → Rules → Rulesets → New ruleset → New branch ruleset**. Set the +target to the default branch, enable **Restrict deletions**, **Block force +pushes** and **Require status checks to pass**, then add both checks with source +**GitHub Actions**. + +## Ruleset: `cla-signatures` + +`.github/workflows/cla.yml` commits signatures to this branch with the +workflow's `GITHUB_TOKEN`. The ruleset blocks deletion and force-push only. It +does not restrict updates or require checks, so the workflow can keep committing. +You can create the ruleset before the branch exists. + +```bash +gh api --method POST repos/debpalash/VoiceStudio/rulesets --input - <<'JSON' +{ + "name": "cla-signatures", + "target": "branch", + "enforcement": "active", + "conditions": { "ref_name": { "include": ["refs/heads/cla-signatures"], "exclude": [] } }, + "bypass_actors": [], + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" } + ] +} +JSON +``` + +Check both rulesets with `gh api repos/debpalash/VoiceStudio/rulesets`. + +## Secret scanning and push protection + +UI: **Settings → Code security → Secret Protection**. Enable **Secret scanning** +and **Push protection**. + +```bash +gh api --method PATCH repos/debpalash/VoiceStudio --input - <<'JSON' +{ "security_and_analysis": { + "secret_scanning": { "status": "enabled" }, + "secret_scanning_push_protection": { "status": "enabled" } } } +JSON +``` + +The PostHog project token committed in `backend/core/analytics.py` and +`electron/src/shared/utils/analytics.ts` is a publishable write-only token by +design (see `tests/test_no_committed_analytics_token.py`). If an alert flags it, +close the alert as "used in tests" or "false positive". Do not remove the token. + +## Private vulnerability reporting + +`.github/SECURITY.md` names GitHub Security Advisories as the preferred channel, +and that link only works while this setting is on. + +UI: **Settings → Code security → Private vulnerability reporting → Enable**. + +```bash +gh api --method PUT repos/debpalash/VoiceStudio/private-vulnerability-reporting +``` + +## Dependabot alerts + +UI: **Settings → Code security → Dependabot alerts → Enable**. This setting +turns on alerts only. The repository has no `.github/dependabot.yml`, so +Dependabot does not open version-update pull requests. + +```bash +gh api --method PUT repos/debpalash/VoiceStudio/vulnerability-alerts +``` + +## Maintainer commit email + +Each maintainer should go to **GitHub → Settings → Emails** and enable **Keep my +email addresses private** and **Block command line pushes that expose my +email**. Then set the no-reply address shown on that page as the commit +identity: + +```bash +git config --global user.email "ID+USERNAME@users.noreply.github.com" +``` + +Commits keep their GitHub attribution, and `scripts/cla_audit.py` maps no-reply +addresses to GitHub logins without an API lookup. Commits already pushed +keep their old address. From 37ce4db81bc9b1e987c4ff458146672b691a3556 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:05:04 +0530 Subject: [PATCH 04/85] ci(security): block leaked and placeholder commit identities on PRs Add a pull_request gate that checks every new PR commit's author, committer, and Co-authored-by/Signed-off-by emails against a SHA-256 block list (plaintext never committed) plus known placeholder, tool, and hostname-style auto-detected identities. --- .github/CONTRIBUTING.md | 7 ++ .github/workflows/commit-identity.yml | 36 ++++++ scripts/blocked_identity_hashes.txt | 16 +++ scripts/check_commit_identities.py | 173 ++++++++++++++++++++++++++ tests/test_commit_identities.py | 162 ++++++++++++++++++++++++ 5 files changed, 394 insertions(+) create mode 100644 .github/workflows/commit-identity.yml create mode 100644 scripts/blocked_identity_hashes.txt create mode 100755 scripts/check_commit_identities.py create mode 100644 tests/test_commit_identities.py diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f7005feb5..437923e5b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -240,6 +240,13 @@ bad: update bad: WIP ``` +Commit with a real identity (your GitHub noreply address works). The +`commit-identity` PR check (`scripts/check_commit_identities.py`) fails when a +PR commit's author, committer, or `Co-authored-by:`/`Signed-off-by:` email is +a placeholder (`test@local`, `you@example.com`, `mergetest`, hostname-style +`*.local`) or on the hashed block list; fix it with +`git rebase --exec 'git commit --amend --no-edit --reset-author' origin/main`. + --- ## Testing diff --git a/.github/workflows/commit-identity.yml b/.github/workflows/commit-identity.yml new file mode 100644 index 000000000..2ae671211 --- /dev/null +++ b/.github/workflows/commit-identity.yml @@ -0,0 +1,36 @@ +# Commit identity gate — fails a PR when any of its commits carries a blocked +# author/committer/trailer identity (hashed personal emails, placeholder and +# hostname-style tool identities). The blocked emails are stored only as +# SHA-256 digests in scripts/blocked_identity_hashes.txt; the job output names +# the commit and the kind of match, never the address. +# +# Only commits new to the PR (origin/..HEAD) are checked, so existing +# history does not fail the gate. + +name: commit-identity + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: commit-identity-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + identities: + name: Commit identities + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Check PR commit identities + env: + BASE_REF: ${{ github.base_ref }} + run: python3 scripts/check_commit_identities.py --range "origin/${BASE_REF}..HEAD" diff --git a/scripts/blocked_identity_hashes.txt b/scripts/blocked_identity_hashes.txt new file mode 100644 index 000000000..f11f64490 --- /dev/null +++ b/scripts/blocked_identity_hashes.txt @@ -0,0 +1,16 @@ +# SHA-256 digests of commit emails that must never appear in this repo's history. +# Read by scripts/check_commit_identities.py (CI job: commit-identity). +# +# Only digests live here so the blocked addresses are not republished. To add +# one, normalise the address (trim + lowercase), hash it, and append the digest +# on its own line — never commit the plaintext: +# +# printf '%s' 'someone@example.org' | tr A-Z a-z | sha256sum | cut -d' ' -f1 +# +# One 64-character lowercase hex digest per line; blank lines and '#' comments +# are ignored, anything else fails the check. +29903483b62ec490ee09babe59630be06739ca13b1988cf4914ff12464a83f0f +99f5b0bf85ee5ec1a066ae5c047fb262981efa5531e8b073686e997f64db05a5 +fbc9e302a66cac29f8676413b11e924ba1bacffc2d14f8b684a0884804f1b306 +b67be71e27a6bac639ac2add1ce064c51b40765222e2628b36885eebb415752d +f98addabdf15792bb3674587b71a20ab11a1570c915c0e673f0ef9fd0448954a diff --git a/scripts/check_commit_identities.py b/scripts/check_commit_identities.py new file mode 100755 index 000000000..b41850f54 --- /dev/null +++ b/scripts/check_commit_identities.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +"""Fail when commits in a revision range carry a blocked identity. + +Checks every commit's author and committer name/email plus each +``Co-authored-by:`` / ``Signed-off-by:`` trailer email against: + +* hashed personal emails (``scripts/blocked_identity_hashes.txt``): emails are + trimmed, lowercased, SHA-256 hashed and compared, so the list never + republishes the addresses it guards; +* literal placeholder/tool identities that are not personal data + (``mergetest``, ``test@local``, ``you@example.com``) and hostname-style + auto-detected emails (``*.local``, ``*.localdomain``, ``*.(none)``). + +Findings name the commit, the field and the *kind* of match only; the +matched value is never printed. Exit 0 = clean, 1 = violations, 2 = error. + +Usage: check_commit_identities.py [--range A..B | --base BRANCH] [--hash-file PATH] +Default range is ``origin/..HEAD`` with base from ``--base``, +``$GITHUB_BASE_REF``, or ``main``. +""" + +from __future__ import annotations + +import argparse +import hashlib +import os +import re +import subprocess +import sys +from pathlib import Path + +DEFAULT_HASH_FILE = Path(__file__).with_name("blocked_identity_hashes.txt") + +HASHED_KIND = "blocked personal email (hashed)" +BLOCKED_NAMES = {"mergetest": "placeholder identity name 'mergetest'"} +BLOCKED_EMAILS = { + "test@local": "placeholder email 'test@local'", + "you@example.com": "placeholder email 'you@example.com'", +} +BLOCKED_LOCALPARTS = {"mergetest": "placeholder identity 'mergetest'"} +# Git's auto-detected "user@host" fallbacks leak machine hostnames. +HOSTNAME_SUFFIXES = (".local", ".localdomain", ".(none)") +HOSTNAME_KIND = "hostname-style auto-detected email" + +_HEX64 = re.compile(r"^[0-9a-f]{64}$") +_TRAILER = re.compile(r"^[ \t]*(co-authored-by|signed-off-by)[ \t]*:(.*)$", re.IGNORECASE | re.MULTILINE) +_FIELD_SEP = "\x1f" + + +def normalise(email: str) -> str: + return email.strip().strip("<>").strip().lower() + + +def email_digest(email: str) -> str: + return hashlib.sha256(normalise(email).encode("utf-8")).hexdigest() + + +def load_hashes(path: Path) -> set[str]: + digests: set[str] = set() + for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + line = raw.strip() + if not line or line.startswith("#"): + continue + if not _HEX64.match(line): + raise ValueError(f"{path}:{lineno}: expected a 64-char lowercase hex SHA-256 digest") + digests.add(line) + return digests + + +def classify_email(email: str, hashes: set[str]) -> list[str]: + value = normalise(email) + if not value: + return [] + kinds: list[str] = [] + if email_digest(value) in hashes: + kinds.append(HASHED_KIND) + if value in BLOCKED_EMAILS: + kinds.append(BLOCKED_EMAILS[value]) + local = value.split("@", 1)[0] + if local in BLOCKED_LOCALPARTS: + kinds.append(BLOCKED_LOCALPARTS[local]) + if "@" in value and value.endswith(HOSTNAME_SUFFIXES): + kinds.append(HOSTNAME_KIND) + return kinds + + +def classify_name(name: str) -> list[str]: + kind = BLOCKED_NAMES.get(name.strip().lower()) + return [kind] if kind else [] + + +def trailer_emails(body: str) -> list[tuple[str, str]]: + found = [] + for key, value in _TRAILER.findall(body): + match = re.search(r"<([^>]*)>", value) + email = match.group(1) if match else value + if "@" in email: + found.append((key.lower(), email)) + return found + + +def read_commits(rev_range: str) -> list[dict[str, str]]: + fmt = _FIELD_SEP.join(["%H", "%an", "%ae", "%cn", "%ce", "%B"]) + proc = subprocess.run( + ["git", "-c", "log.showSignature=false", "log", "-z", "--no-color", f"--format={fmt}", rev_range, "--"], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=False, + ) + if proc.returncode != 0: + raise RuntimeError(proc.stderr.strip() or f"git log failed for {rev_range!r}") + commits = [] + for record in proc.stdout.split("\0"): + record = record.lstrip("\n") + if not record: + continue + sha, an, ae, cn, ce, body = (record.split(_FIELD_SEP, 5) + [""] * 6)[:6] + commits.append({"sha": sha, "an": an, "ae": ae, "cn": cn, "ce": ce, "body": body}) + return commits + + +def find_violations(commits: list[dict[str, str]], hashes: set[str]) -> list[tuple[str, str, str]]: + violations = [] + for c in commits: + short = c["sha"][:10] + checks = [ + ("author name", classify_name(c["an"])), + ("author email", classify_email(c["ae"], hashes)), + ("committer name", classify_name(c["cn"])), + ("committer email", classify_email(c["ce"], hashes)), + ] + for key, email in trailer_emails(c["body"]): + label = "Co-authored-by" if key == "co-authored-by" else "Signed-off-by" + checks.append((f"{label} trailer email", classify_email(email, hashes))) + for field, kinds in checks: + violations.extend((short, field, kind) for kind in kinds) + return violations + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--range", dest="rev_range", help="git revision range, e.g. origin/main..HEAD") + parser.add_argument("--base", default=os.environ.get("GITHUB_BASE_REF") or "main", help="base branch for the default range") + parser.add_argument("--hash-file", type=Path, default=DEFAULT_HASH_FILE, help="blocked email digest list") + args = parser.parse_args(argv) + rev_range = args.rev_range or f"origin/{args.base}..HEAD" + + try: + hashes = load_hashes(args.hash_file) + commits = read_commits(rev_range) + except (OSError, ValueError, RuntimeError) as exc: + print(f"commit-identity: error: {exc}", file=sys.stderr) + return 2 + + violations = find_violations(commits, hashes) + if not violations: + print(f"commit-identity: {len(commits)} commit(s) in {rev_range} clean.") + return 0 + print(f"commit-identity: blocked identities in {rev_range}:") + for short, field, kind in violations: + print(f" {short} {field}: {kind}") + print( + "Set an allowed identity (e.g. your GitHub noreply address), then rewrite and force-push:\n" + " git rebase --exec 'git commit --amend --no-edit --reset-author' origin/\n" + "Remove blocked Co-authored-by/Signed-off-by trailers with `git rebase -i` (reword)." + ) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_commit_identities.py b/tests/test_commit_identities.py new file mode 100644 index 000000000..0827bcbe3 --- /dev/null +++ b/tests/test_commit_identities.py @@ -0,0 +1,162 @@ +"""Commit-identity CI gate: scripts/check_commit_identities.py + workflow.""" + +from __future__ import annotations + +import hashlib +import os +import re +import secrets +import subprocess +import sys +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "check_commit_identities.py" +HASH_FILE = ROOT / "scripts" / "blocked_identity_hashes.txt" +WORKFLOW = ROOT / ".github" / "workflows" / "commit-identity.yml" +CLEAN = ("Clean Dev", "12345+clean@users.noreply.github.com") + + +@pytest.fixture +def repo(tmp_path: Path): + """Throwaway repo isolated from the developer's global git config/hooks.""" + work = tmp_path / "repo" + work.mkdir() + empty = tmp_path / "gitconfig" + empty.write_text("") + env = {**os.environ, "GIT_CONFIG_GLOBAL": str(empty), "GIT_CONFIG_NOSYSTEM": "1"} + env.pop("GITHUB_BASE_REF", None) + + def git(*args: str, **extra: str) -> str: + return subprocess.run( + ["git", *args], cwd=work, env={**env, **extra}, check=True, capture_output=True, text=True + ).stdout.strip() + + def commit(message: str, author=CLEAN, committer=CLEAN) -> str: + git( + "commit", "--allow-empty", "--no-verify", "-q", "-m", message, + GIT_AUTHOR_NAME=author[0], GIT_AUTHOR_EMAIL=author[1], + GIT_COMMITTER_NAME=committer[0], GIT_COMMITTER_EMAIL=committer[1], + ) + return git("rev-parse", "HEAD") + + def check(*args: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, str(SCRIPT), *args], cwd=work, env=env, capture_output=True, text=True + ) + + git("init", "-q", "-b", "main") + commit("chore: base") + git("tag", "base") + return git, commit, check + + +@pytest.fixture +def secret_email(tmp_path: Path): + email = f"{secrets.token_hex(8)}@example.invalid" + hashes = tmp_path / "hashes.txt" + hashes.write_text("# test list\n" + hashlib.sha256(email.encode()).hexdigest() + "\n") + return email, str(hashes) + + +def _only(sha: str) -> list[str]: + return ["--range", f"{sha}^..{sha}"] + + +def test_hashed_author_email_blocked_without_printing_it(repo, secret_email): + _, commit, check = repo + email, hashes = secret_email + sha = commit("feat: leak", author=("Someone", " " + email.upper() + " ")) + result = check(*_only(sha), "--hash-file", hashes) + out = result.stdout + result.stderr + assert result.returncode == 1 + assert sha[:10] in out and "author email: blocked personal email (hashed)" in out + assert email.split("@")[0] not in out.lower() + # Without the digest the same commit is clean: the match is hash-driven. + assert check(*_only(sha)).returncode == 0 + + +def test_blocked_trailer_emails(repo, secret_email): + _, commit, check = repo + email, hashes = secret_email + co = commit(f"fix: thing\n\nCo-authored-by: Someone <{email}>") + so = commit(f"fix: other\n\nsigned-off-by: Someone <{email.upper()}>") + for sha, label in ((co, "Co-authored-by"), (so, "Signed-off-by")): + result = check(*_only(sha), "--hash-file", hashes) + assert result.returncode == 1 + assert f"{label} trailer email: blocked personal email (hashed)" in result.stdout + assert email.split("@")[0] not in (result.stdout + result.stderr).lower() + + +@pytest.mark.parametrize( + "author,committer,expected", + [ + (("mergetest", CLEAN[1]), CLEAN, "author name: placeholder identity name 'mergetest'"), + (CLEAN, ("Dev", "test@local"), "committer email: placeholder email 'test@local'"), + (("Your Name", "you@example.com"), CLEAN, "author email: placeholder email 'you@example.com'"), + (CLEAN, ("Dev", "dev@Some-MacBook.LOCAL"), "committer email: hostname-style auto-detected email"), + (CLEAN, ("Dev", "dev@box.localdomain"), "committer email: hostname-style auto-detected email"), + ], +) +def test_literal_placeholder_identities_blocked(repo, author, committer, expected): + _, commit, check = repo + sha = commit("chore: tool commit", author=author, committer=committer) + result = check(*_only(sha)) + assert result.returncode == 1 + assert expected in result.stdout + assert "some-macbook" not in result.stdout.lower() + + +def test_clean_commits_pass_and_range_reports_each_offender(repo, secret_email): + _, commit, check = repo + email, hashes = secret_email + commit("feat: clean one") + assert check("--range", "base..HEAD", "--hash-file", hashes).returncode == 0 + bad = commit("chore: merge", author=("mergetest", "mergetest@example.com")) + commit("feat: clean two") + result = check("--range", "base..HEAD", "--hash-file", hashes) + assert result.returncode == 1 + offenders = set(re.findall(r"^ ([0-9a-f]{10}) ", result.stdout, re.MULTILINE)) + assert offenders == {bad[:10]} + + +def test_default_range_uses_origin_base(repo): + git, commit, check = repo + git("update-ref", "refs/remotes/origin/main", "base") + commit("chore: tool", committer=("mergetest", "mergetest@example.com")) + assert check().returncode == 1 + assert check("--base", "nonexistent").returncode == 2 + + +def test_malformed_hash_list_is_an_error(repo, tmp_path): + bad = tmp_path / "bad.txt" + bad.write_text("not-a-digest\n") + _, _, check = repo + assert check("--range", "base..HEAD", "--hash-file", str(bad)).returncode == 2 + + +def test_shipped_hash_list_is_digests_only(): + entries = [ + line.strip() + for line in HASH_FILE.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.strip().startswith("#") + ] + assert len(entries) == 5 + assert len(set(entries)) == 5 + assert all(re.fullmatch(r"[0-9a-f]{64}", e) for e in entries) + + +def test_workflow_runs_on_untrusted_safe_pull_request(): + wf = yaml.load(WORKFLOW.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) + assert set(wf["on"]) == {"pull_request"} + assert wf["permissions"] == {"contents": "read"} + (job,) = wf["jobs"].values() + checkout, run = job["steps"] + assert checkout["uses"] == "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" + assert checkout["with"] == {"fetch-depth": "0", "persist-credentials": "false"} + assert "${{" not in run["run"] + assert run["env"]["BASE_REF"] == "${{ github.base_ref }}" + assert "scripts/check_commit_identities.py" in run["run"] and "origin/${BASE_REF}..HEAD" in run["run"] From e5e135ae2e660444c12d3e854f86deda6cfc5552 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:06:09 +0530 Subject: [PATCH 05/85] chore(contact): publish only hi@, partner@ and security@voicestudio.sh Replaces the palash.dev contact addresses in docs, UI, locales, package metadata and legal notices, and adds a guard test so only the three project addresses can appear outside test fixtures. --- .github/CODE_OF_CONDUCT.md | 2 +- .github/SECURITY.md | 2 +- CHANGELOG.md | 1 + LICENSE-NOTICE.md | 2 +- electron/package.json | 2 +- .../src/renderer/src/i18n/locales/ar.json | 2 +- .../src/renderer/src/i18n/locales/de.json | 2 +- .../src/renderer/src/i18n/locales/en.json | 2 +- .../src/renderer/src/i18n/locales/es.json | 2 +- .../src/renderer/src/i18n/locales/fr.json | 2 +- .../src/renderer/src/i18n/locales/hi.json | 2 +- .../src/renderer/src/i18n/locales/id.json | 2 +- .../src/renderer/src/i18n/locales/it.json | 2 +- .../src/renderer/src/i18n/locales/ja.json | 2 +- .../src/renderer/src/i18n/locales/ko.json | 2 +- .../src/renderer/src/i18n/locales/nl.json | 2 +- .../src/renderer/src/i18n/locales/pl.json | 2 +- .../src/renderer/src/i18n/locales/pt.json | 2 +- .../src/renderer/src/i18n/locales/ru.json | 2 +- .../src/renderer/src/i18n/locales/sv.json | 2 +- .../src/renderer/src/i18n/locales/th.json | 2 +- .../src/renderer/src/i18n/locales/tr.json | 2 +- .../src/renderer/src/i18n/locales/uk.json | 2 +- .../src/renderer/src/i18n/locales/vi.json | 2 +- .../src/renderer/src/i18n/locales/zh-CN.json | 2 +- .../src/renderer/src/i18n/locales/zh-TW.json | 2 +- electron/src/shared/i18n/locales/ar.json | 2 +- electron/src/shared/i18n/locales/de.json | 2 +- electron/src/shared/i18n/locales/en.json | 2 +- electron/src/shared/i18n/locales/es.json | 2 +- electron/src/shared/i18n/locales/fr.json | 2 +- electron/src/shared/i18n/locales/hi.json | 2 +- electron/src/shared/i18n/locales/id.json | 2 +- electron/src/shared/i18n/locales/it.json | 2 +- electron/src/shared/i18n/locales/ja.json | 2 +- electron/src/shared/i18n/locales/ko.json | 2 +- electron/src/shared/i18n/locales/nl.json | 2 +- electron/src/shared/i18n/locales/pl.json | 2 +- electron/src/shared/i18n/locales/pt.json | 2 +- electron/src/shared/i18n/locales/ru.json | 2 +- electron/src/shared/i18n/locales/sv.json | 2 +- electron/src/shared/i18n/locales/th.json | 2 +- electron/src/shared/i18n/locales/tr.json | 2 +- electron/src/shared/i18n/locales/uk.json | 2 +- electron/src/shared/i18n/locales/vi.json | 2 +- electron/src/shared/i18n/locales/zh-CN.json | 2 +- electron/src/shared/i18n/locales/zh-TW.json | 2 +- electron/src/shared/pages/ContactPage.jsx | 2 +- electron/src/shared/pages/SupportPage.jsx | 4 +- electron/src/shared/test/ContactPage.test.jsx | 2 +- electron/src/shared/utils/contactLinks.js | 6 +-- pyproject.toml | 2 +- tests/test_contact_emails.py | 43 +++++++++++++++++++ 53 files changed, 98 insertions(+), 54 deletions(-) create mode 100644 tests/test_contact_emails.py diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 9bf7fd06a..81b9c8d23 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -46,7 +46,7 @@ an individual is officially representing the community in public spaces. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at -**VoiceStudio@palash.dev**. +**hi@voicestudio.sh**. All complaints will be reviewed and investigated promptly and fairly. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index a7fcd8d75..ee1b26902 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -24,7 +24,7 @@ download, and never run executables bundled with model archives. Instead, report them privately via one of these channels: 1. **GitHub Security Advisories** (preferred) — [Report a vulnerability](https://github.com/debpalash/VoiceStudio/security/advisories/new) -2. **Email** — Send details to **security@palash.dev** +2. **Email** — Send details to **security@voicestudio.sh** ### What to include diff --git a/CHANGELOG.md b/CHANGELOG.md index c31790517..844ba1bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ metadata and the backend fallback mirror it. - Pull requests ask their opener and every commit author and co-author to sign a one-time Contributor License Agreement by comment; the contributing guide explains how ### Docs +- Contact addresses are now hi@voicestudio.sh (general and licensing), partner@voicestudio.sh (partnerships) and security@voicestudio.sh (security reports) - Chinese README now matches the Electron installation and migration guide (#2377) — thanks @lg114! - Record the supplied audio comparisons and installed-engine quality validation (#2406) diff --git a/LICENSE-NOTICE.md b/LICENSE-NOTICE.md index 4391e88a6..33de83324 100644 --- a/LICENSE-NOTICE.md +++ b/LICENSE-NOTICE.md @@ -24,7 +24,7 @@ text in [`LICENSE`](LICENSE). A **commercial license is available** for organizations that want to embed VoiceStudio in a closed-source or proprietary product or service without the AGPL-3.0 copyleft obligations. Pricing tiers are coming soon; for inquiries -contact `VoiceStudio@palash.dev`. +contact `hi@voicestudio.sh`. Contributors license their contributions to Yupcha Softwares Private Limited, the company that maintains VoiceStudio, under the diff --git a/electron/package.json b/electron/package.json index f519e3445..aa253c7d9 100644 --- a/electron/package.json +++ b/electron/package.json @@ -7,7 +7,7 @@ "license": "AGPL-3.0-only", "author": { "name": "VoiceStudio contributors", - "email": "VoiceStudio@palash.dev" + "email": "hi@voicestudio.sh" }, "type": "module", "main": "./out/main/index.js", diff --git a/electron/src/renderer/src/i18n/locales/ar.json b/electron/src/renderer/src/i18n/locales/ar.json index 2a20084b7..8f6cf32ef 100644 --- a/electron/src/renderer/src/i18n/locales/ar.json +++ b/electron/src/renderer/src/i18n/locales/ar.json @@ -2491,7 +2491,7 @@ "pricing_desc": "لا يزال يجري وضع اللمسات النهائية على المستويات والتسعير.", "pricing_detail": "وإلى أن يتم نشرها بشكل عام، يتم اقتباس كل عملية نشر تجارية بشكل فردي حتى نتمكن من تحديد الحجم المناسب لفريقك وعبء العمل.", "request_quote": "طلب عرض أسعار", - "footer_email": "أسئلة؟ تواصل معنا على VoiceStudio@palash.dev", + "footer_email": "أسئلة؟ تواصل معنا على hi@voicestudio.sh", "footer_discord": "انضم إلى Discord للحصول على دعم المجتمع.", "benefit_ip": "ملكية IP كاملة", "benefit_ip_desc": "أنت تمتلك كل مخرجات، كل نموذج، كل مشتق. لا يوجد إيجار مشترك، ولا يوجد تسرب للبيانات.", diff --git a/electron/src/renderer/src/i18n/locales/de.json b/electron/src/renderer/src/i18n/locales/de.json index f57560190..c64660b1a 100644 --- a/electron/src/renderer/src/i18n/locales/de.json +++ b/electron/src/renderer/src/i18n/locales/de.json @@ -2483,7 +2483,7 @@ "pricing_desc": "Stufen und Preise werden noch finalisiert.", "pricing_detail": "Bis zur Veröffentlichung wird für jede kommerzielle Bereitstellung ein individuelles Angebot erstellt, damit wir die richtige Größe für Ihr Team und Ihre Arbeitslast festlegen können.", "request_quote": "Fordern Sie ein Angebot an", - "footer_email": "Fragen? Kontaktieren Sie uns unter VoiceStudio@palash.dev", + "footer_email": "Fragen? Kontaktieren Sie uns unter hi@voicestudio.sh", "footer_discord": "Treten Sie unserem Discord bei, um Community-Unterstützung zu erhalten.", "benefit_ip": "Vollständiges IP-Eigentum", "benefit_ip_desc": "Sie besitzen jede Ausgabe, jedes Modell, jedes Derivat. Keine gemeinsame Miete, keine Datenlecks.", diff --git a/electron/src/renderer/src/i18n/locales/en.json b/electron/src/renderer/src/i18n/locales/en.json index dd47c8642..2f78ae7d3 100644 --- a/electron/src/renderer/src/i18n/locales/en.json +++ b/electron/src/renderer/src/i18n/locales/en.json @@ -2543,7 +2543,7 @@ "pricing_desc": "Tiers and pricing are still being finalized.", "pricing_detail": "Until they’re public, every commercial deployment is being quoted individually so we can right-size for your team and workload.", "request_quote": "Request a quote", - "footer_email": "Questions? Reach out at VoiceStudio@palash.dev", + "footer_email": "Questions? Reach out at hi@voicestudio.sh", "footer_discord": "Join our Discord for community support.", "benefit_ip": "Full IP ownership", "benefit_ip_desc": "You own every output, every model, every derivative. No shared tenancy, no data leaks.", diff --git a/electron/src/renderer/src/i18n/locales/es.json b/electron/src/renderer/src/i18n/locales/es.json index 73f85da36..2c25ed87f 100644 --- a/electron/src/renderer/src/i18n/locales/es.json +++ b/electron/src/renderer/src/i18n/locales/es.json @@ -2485,7 +2485,7 @@ "pricing_desc": "Los niveles y los precios aún se están ultimando.", "pricing_detail": "Hasta que sean públicas, cada implementación comercial se cotiza individualmente para que podamos adaptarla al tamaño adecuado para su equipo y carga de trabajo.", "request_quote": "Solicitar una cotización", - "footer_email": "¿Preguntas? Comuníquese con VoiceStudio@palash.dev", + "footer_email": "¿Preguntas? Comuníquese con hi@voicestudio.sh", "footer_discord": "Únase a nuestro Discord para obtener apoyo de la comunidad.", "benefit_ip": "Propiedad total de propiedad intelectual", "benefit_ip_desc": "Eres dueño de cada producto, cada modelo, cada derivado. Sin arrendamiento compartido, sin fugas de datos.", diff --git a/electron/src/renderer/src/i18n/locales/fr.json b/electron/src/renderer/src/i18n/locales/fr.json index b267c06c7..cedec259d 100644 --- a/electron/src/renderer/src/i18n/locales/fr.json +++ b/electron/src/renderer/src/i18n/locales/fr.json @@ -2485,7 +2485,7 @@ "pricing_desc": "Les niveaux et les tarifs sont encore en cours de finalisation.", "pricing_detail": "Jusqu'à ce qu'ils soient publics, chaque déploiement commercial est proposé individuellement afin que nous puissions l'adapter à votre équipe et à votre charge de travail.", "request_quote": "Demander un devis", - "footer_email": "Des questions ? Contactez VoiceStudio@palash.dev", + "footer_email": "Des questions ? Contactez hi@voicestudio.sh", "footer_discord": "Rejoignez notre Discord pour le soutien de la communauté.", "benefit_ip": "Propriété IP complète", "benefit_ip_desc": "Vous possédez chaque sortie, chaque modèle, chaque dérivé. Pas de location partagée, pas de fuite de données.", diff --git a/electron/src/renderer/src/i18n/locales/hi.json b/electron/src/renderer/src/i18n/locales/hi.json index 57eb1d30f..23c4eb13e 100644 --- a/electron/src/renderer/src/i18n/locales/hi.json +++ b/electron/src/renderer/src/i18n/locales/hi.json @@ -2483,7 +2483,7 @@ "pricing_desc": "टियर और मूल्य निर्धारण को अभी भी अंतिम रूप दिया जा रहा है।", "pricing_detail": "जब तक वे सार्वजनिक नहीं होते, प्रत्येक व्यावसायिक तैनाती को व्यक्तिगत रूप से उद्धृत किया जा रहा है ताकि हम आपकी टीम और कार्यभार के लिए सही आकार दे सकें।", "request_quote": "एक उद्धरण का अनुरोध करें", - "footer_email": "प्रश्न? VoiceStudio@palash.dev पर संपर्क करें", + "footer_email": "प्रश्न? hi@voicestudio.sh पर संपर्क करें", "footer_discord": "सामुदायिक समर्थन के लिए हमारे डिस्कोर्ड में शामिल हों।", "benefit_ip": "पूर्ण आईपी स्वामित्व", "benefit_ip_desc": "आप हर आउटपुट, हर मॉडल, हर डेरिवेटिव के मालिक हैं। कोई साझा किरायेदारी नहीं, कोई डेटा लीक नहीं।", diff --git a/electron/src/renderer/src/i18n/locales/id.json b/electron/src/renderer/src/i18n/locales/id.json index b40787d69..05ec82d20 100644 --- a/electron/src/renderer/src/i18n/locales/id.json +++ b/electron/src/renderer/src/i18n/locales/id.json @@ -2483,7 +2483,7 @@ "pricing_desc": "Tingkatan dan harga masih dalam tahap penyelesaian.", "pricing_detail": "Sebelum dipublikasikan, setiap penerapan komersial akan dikutip satu per satu sehingga kami dapat menyesuaikan ukurannya untuk tim dan beban kerja Anda.", "request_quote": "Minta penawaran", - "footer_email": "Pertanyaan? Hubungi VoiceStudio@palash.dev", + "footer_email": "Pertanyaan? Hubungi hi@voicestudio.sh", "footer_discord": "Bergabunglah dengan Discord kami untuk mendapatkan dukungan komunitas.", "benefit_ip": "Kepemilikan IP penuh", "benefit_ip_desc": "Anda memiliki setiap keluaran, setiap model, setiap turunan. Tidak ada sewa bersama, tidak ada kebocoran data.", diff --git a/electron/src/renderer/src/i18n/locales/it.json b/electron/src/renderer/src/i18n/locales/it.json index 86cadbd14..b3bc76c71 100644 --- a/electron/src/renderer/src/i18n/locales/it.json +++ b/electron/src/renderer/src/i18n/locales/it.json @@ -2485,7 +2485,7 @@ "pricing_desc": "Livelli e prezzi sono ancora in fase di definizione.", "pricing_detail": "Fino a quando non saranno pubbliche, ogni implementazione commerciale verrà quotata individualmente in modo da poter dimensionare correttamente il tuo team e il tuo carico di lavoro.", "request_quote": "Richiedi un preventivo", - "footer_email": "Domande? Contatta VoiceStudio@palash.dev", + "footer_email": "Domande? Contatta hi@voicestudio.sh", "footer_discord": "Unisciti al nostro Discord per il supporto della comunità.", "benefit_ip": "Proprietà IP completa", "benefit_ip_desc": "Possiedi ogni risultato, ogni modello, ogni derivato. Nessuna locazione condivisa, nessuna fuga di dati.", diff --git a/electron/src/renderer/src/i18n/locales/ja.json b/electron/src/renderer/src/i18n/locales/ja.json index 8e700c47e..56297fcb0 100644 --- a/electron/src/renderer/src/i18n/locales/ja.json +++ b/electron/src/renderer/src/i18n/locales/ja.json @@ -2483,7 +2483,7 @@ "pricing_desc": "レベルと価格はまだ最終決定中です。", "pricing_detail": "公開されるまで、すべての商用展開は個別に見積もられるため、お客様のチームとワークロードに合わせて適切な規模を設定できます。", "request_quote": "見積もりを依頼する", - "footer_email": "質問がありますか? VoiceStudio@palash.dev までご連絡ください。", + "footer_email": "質問がありますか? hi@voicestudio.sh までご連絡ください。", "footer_discord": "コミュニティサポートのために Discord に参加してください。", "benefit_ip": "完全な IP 所有権", "benefit_ip_desc": "あなたはすべての出力、すべてのモデル、すべての派生製品を所有します。共有テナントやデータ漏洩はありません。", diff --git a/electron/src/renderer/src/i18n/locales/ko.json b/electron/src/renderer/src/i18n/locales/ko.json index ef5bd37dd..dcde670ab 100644 --- a/electron/src/renderer/src/i18n/locales/ko.json +++ b/electron/src/renderer/src/i18n/locales/ko.json @@ -2483,7 +2483,7 @@ "pricing_desc": "등급과 가격은 아직 확정 중입니다.", "pricing_detail": "공개될 때까지 모든 상용 배포는 개별적으로 견적이 나오므로 귀하의 팀과 작업량에 맞게 규모를 조정할 수 있습니다.", "request_quote": "견적 요청", - "footer_email": "질문이 있으신가요? VoiceStudio@palash.dev로 문의하세요.", + "footer_email": "질문이 있으신가요? hi@voicestudio.sh로 문의하세요.", "footer_discord": "커뮤니티 지원을 위해 Discord에 참여하세요.", "benefit_ip": "전체 IP 소유권", "benefit_ip_desc": "귀하는 모든 산출물, 모든 모델, 모든 파생물을 소유합니다. 공유 테넌시가 없고 데이터 유출도 없습니다.", diff --git a/electron/src/renderer/src/i18n/locales/nl.json b/electron/src/renderer/src/i18n/locales/nl.json index 4e54205b5..d68c2304e 100644 --- a/electron/src/renderer/src/i18n/locales/nl.json +++ b/electron/src/renderer/src/i18n/locales/nl.json @@ -2483,7 +2483,7 @@ "pricing_desc": "Niveaus en prijzen worden nog afgerond.", "pricing_detail": "Totdat ze openbaar zijn, wordt elke commerciële implementatie afzonderlijk geciteerd, zodat we de juiste maat kunnen vinden voor uw team en werklast.", "request_quote": "Vraag een offerte aan", - "footer_email": "Vragen? Neem contact op met VoiceStudio@palash.dev", + "footer_email": "Vragen? Neem contact op met hi@voicestudio.sh", "footer_discord": "Sluit je aan bij onze Discord voor community-ondersteuning.", "benefit_ip": "Volledig IP-eigendom", "benefit_ip_desc": "U bezit elke output, elk model, elke afgeleide. Geen gedeelde huur, geen datalekken.", diff --git a/electron/src/renderer/src/i18n/locales/pl.json b/electron/src/renderer/src/i18n/locales/pl.json index 70a0e0199..8ec16b051 100644 --- a/electron/src/renderer/src/i18n/locales/pl.json +++ b/electron/src/renderer/src/i18n/locales/pl.json @@ -2487,7 +2487,7 @@ "pricing_desc": "Poziomy i ceny są wciąż finalizowane.", "pricing_detail": "Dopóki nie zostaną one upublicznione, każde wdrożenie komercyjne jest wyceniane indywidualnie, abyśmy mogli dopasować rozmiar do Twojego zespołu i obciążenia pracą.", "request_quote": "Poproś o wycenę", - "footer_email": "Pytania? Skontaktuj się z nami pod adresem VoiceStudio@palash.dev", + "footer_email": "Pytania? Skontaktuj się z nami pod adresem hi@voicestudio.sh", "footer_discord": "Dołącz do naszego Discorda, aby uzyskać wsparcie społeczności.", "benefit_ip": "Pełna własność IP", "benefit_ip_desc": "Jesteś właścicielem każdego produktu, każdego modelu i każdej pochodnej. Żadnych wspólnych najemców, żadnych wycieków danych.", diff --git a/electron/src/renderer/src/i18n/locales/pt.json b/electron/src/renderer/src/i18n/locales/pt.json index 54d946758..106ef84a2 100644 --- a/electron/src/renderer/src/i18n/locales/pt.json +++ b/electron/src/renderer/src/i18n/locales/pt.json @@ -2485,7 +2485,7 @@ "pricing_desc": "Os níveis e os preços ainda estão sendo finalizados.", "pricing_detail": "Até que sejam públicas, cada implantação comercial será cotada individualmente para que possamos dimensionar adequadamente sua equipe e carga de trabalho.", "request_quote": "Solicite um orçamento", - "footer_email": "Perguntas? Entre em contato com VoiceStudio@palash.dev", + "footer_email": "Perguntas? Entre em contato com hi@voicestudio.sh", "footer_discord": "Junte-se ao nosso Discord para obter suporte da comunidade.", "benefit_ip": "Propriedade total de IP", "benefit_ip_desc": "Você possui todos os resultados, todos os modelos, todas as derivadas. Sem locação compartilhada, sem vazamento de dados.", diff --git a/electron/src/renderer/src/i18n/locales/ru.json b/electron/src/renderer/src/i18n/locales/ru.json index d1a693b2a..2f732fce7 100644 --- a/electron/src/renderer/src/i18n/locales/ru.json +++ b/electron/src/renderer/src/i18n/locales/ru.json @@ -2487,7 +2487,7 @@ "pricing_desc": "Уровни и цены еще уточняются.", "pricing_detail": "Пока они не станут общедоступными, каждое коммерческое развертывание оценивается индивидуально, чтобы мы могли подобрать правильный размер для вашей команды и рабочей нагрузки.", "request_quote": "Запросить ценовое предложение", - "footer_email": "Вопросы? Свяжитесь с нами по адресу VoiceStudio@palash.dev.", + "footer_email": "Вопросы? Свяжитесь с нами по адресу hi@voicestudio.sh.", "footer_discord": "Присоединяйтесь к нашему Discord для поддержки сообщества.", "benefit_ip": "Полное владение интеллектуальной собственностью", "benefit_ip_desc": "Вы владеете каждым продуктом, каждой моделью, каждым производным продуктом. Никакой совместной аренды, никаких утечек данных.", diff --git a/electron/src/renderer/src/i18n/locales/sv.json b/electron/src/renderer/src/i18n/locales/sv.json index 140ae24b6..8a2245e52 100644 --- a/electron/src/renderer/src/i18n/locales/sv.json +++ b/electron/src/renderer/src/i18n/locales/sv.json @@ -2483,7 +2483,7 @@ "pricing_desc": "Nivåer och prissättning håller fortfarande på att slutföras.", "pricing_detail": "Tills de är offentliga citeras varje kommersiell distribution individuellt så att vi kan anpassa storleken för ditt team och din arbetsbelastning.", "request_quote": "Begär en offert", - "footer_email": "Frågor? Kontakta VoiceStudio@palash.dev", + "footer_email": "Frågor? Kontakta hi@voicestudio.sh", "footer_discord": "Gå med i vår Discord för samhällsstöd.", "benefit_ip": "Fullt IP-ägande", "benefit_ip_desc": "Du äger varje utgång, varje modell, varje derivat. Ingen delad hyresrätt, inga dataläckor.", diff --git a/electron/src/renderer/src/i18n/locales/th.json b/electron/src/renderer/src/i18n/locales/th.json index 68e9b14ed..0a9904c31 100644 --- a/electron/src/renderer/src/i18n/locales/th.json +++ b/electron/src/renderer/src/i18n/locales/th.json @@ -2483,7 +2483,7 @@ "pricing_desc": "ระดับและราคายังอยู่ในระหว่างการสรุปผล", "pricing_detail": "จนกว่าจะเปิดเผยต่อสาธารณะ การใช้งานเชิงพาณิชย์ทุกครั้งจะถูกเสนอราคาเป็นรายบุคคล เพื่อให้เราสามารถปรับขนาดให้เหมาะสมกับทีมและปริมาณงานของคุณได้", "request_quote": "ขอใบเสนอราคา", - "footer_email": "มีคำถาม? ติดต่อได้ที่ VoiceStudio@palash.dev", + "footer_email": "มีคำถาม? ติดต่อได้ที่ hi@voicestudio.sh", "footer_discord": "เข้าร่วม Discord ของเราเพื่อรับการสนับสนุนจากชุมชน", "benefit_ip": "ความเป็นเจ้าของ IP เต็มรูปแบบ", "benefit_ip_desc": "คุณเป็นเจ้าของทุกผลผลิต ทุกรุ่น และอนุพันธ์ทั้งหมด ไม่มีการเช่าร่วมกัน ไม่มีข้อมูลรั่วไหล", diff --git a/electron/src/renderer/src/i18n/locales/tr.json b/electron/src/renderer/src/i18n/locales/tr.json index 7845be0ee..10cfe28a6 100644 --- a/electron/src/renderer/src/i18n/locales/tr.json +++ b/electron/src/renderer/src/i18n/locales/tr.json @@ -2483,7 +2483,7 @@ "pricing_desc": "Katmanlar ve fiyatlandırma henüz kesinleşme aşamasındadır.", "pricing_detail": "Herkese açık hale gelene kadar her ticari dağıtım ayrı ayrı fiyatlandırılır, böylece ekibinize ve iş yükünüze göre doğru boyutu belirleyebiliriz.", "request_quote": "Fiyat teklifi isteyin", - "footer_email": "Sorunuz mu var? VoiceStudio@palash.dev adresinden bize ulaşın", + "footer_email": "Sorunuz mu var? hi@voicestudio.sh adresinden bize ulaşın", "footer_discord": "Topluluk desteği için Discord'umuza katılın.", "benefit_ip": "Tam IP sahipliği", "benefit_ip_desc": "Her çıktının, her modelin, her türevinin sahibi sizsiniz. Paylaşılan kiracılık yok, veri sızıntısı yok.", diff --git a/electron/src/renderer/src/i18n/locales/uk.json b/electron/src/renderer/src/i18n/locales/uk.json index 38038cc78..747635fe6 100644 --- a/electron/src/renderer/src/i18n/locales/uk.json +++ b/electron/src/renderer/src/i18n/locales/uk.json @@ -2487,7 +2487,7 @@ "pricing_desc": "Рівні та ціни ще завершуються.", "pricing_detail": "Поки вони не стануть загальнодоступними, кожне комерційне розгортання оцінюється окремо, щоб ми могли підібрати правильний розмір для вашої команди та робочого навантаження.", "request_quote": "Запитати цінову пропозицію", - "footer_email": "Запитання? Зверніться за адресою VoiceStudio@palash.dev", + "footer_email": "Запитання? Зверніться за адресою hi@voicestudio.sh", "footer_discord": "Приєднуйтесь до нашого Discord для підтримки спільноти.", "benefit_ip": "Повне володіння IP", "benefit_ip_desc": "Ви володієте кожним результатом, кожною моделлю, кожною похідною. Жодної спільної оренди, жодних витоків даних.", diff --git a/electron/src/renderer/src/i18n/locales/vi.json b/electron/src/renderer/src/i18n/locales/vi.json index 71510fd87..aef0b0b17 100644 --- a/electron/src/renderer/src/i18n/locales/vi.json +++ b/electron/src/renderer/src/i18n/locales/vi.json @@ -2483,7 +2483,7 @@ "pricing_desc": "Các cấp độ và giá cả vẫn đang được hoàn thiện.", "pricing_detail": "Cho đến khi chúng được công khai, mọi hoạt động triển khai thương mại đều được báo giá riêng lẻ để chúng tôi có thể điều chỉnh quy mô phù hợp với nhóm và khối lượng công việc của bạn.", "request_quote": "Yêu cầu báo giá", - "footer_email": "Câu hỏi? Hãy liên hệ tại VoiceStudio@palash.dev", + "footer_email": "Câu hỏi? Hãy liên hệ tại hi@voicestudio.sh", "footer_discord": "Tham gia Discord của chúng tôi để được hỗ trợ cộng đồng.", "benefit_ip": "Quyền sở hữu IP đầy đủ", "benefit_ip_desc": "Bạn sở hữu mọi đầu ra, mọi mô hình, mọi công cụ phái sinh. Không có thuê chung, không có rò rỉ dữ liệu.", diff --git a/electron/src/renderer/src/i18n/locales/zh-CN.json b/electron/src/renderer/src/i18n/locales/zh-CN.json index a34391f82..fb315e941 100644 --- a/electron/src/renderer/src/i18n/locales/zh-CN.json +++ b/electron/src/renderer/src/i18n/locales/zh-CN.json @@ -2487,7 +2487,7 @@ "pricing_desc": "定价层级仍在确定中。", "pricing_detail": "在公开之前,每个商业部署都将单独报价,以便为您的团队和工作负载量身定制。", "request_quote": "请求报价", - "footer_email": "有问题?请联系 VoiceStudio@palash.dev", + "footer_email": "有问题?请联系 hi@voicestudio.sh", "footer_discord": "加入我们的 Discord 获取社区支持。", "benefit_ip": "完全 IP 所有权", "benefit_ip_desc": "您拥有所有输出、模型和衍生作品。无共享租户,无数据泄露。", diff --git a/electron/src/renderer/src/i18n/locales/zh-TW.json b/electron/src/renderer/src/i18n/locales/zh-TW.json index f761bf075..12769c1ce 100644 --- a/electron/src/renderer/src/i18n/locales/zh-TW.json +++ b/electron/src/renderer/src/i18n/locales/zh-TW.json @@ -2483,7 +2483,7 @@ "pricing_desc": "等級和定價仍在最終確定中。", "pricing_detail": "在公開之前,每個商業部署都會單獨報價,以便我們可以根據您的團隊和工作負載調整規模。", "request_quote": "索取報價", - "footer_email": "有問題嗎?請透過 VoiceStudio@palash.dev 聯繫", + "footer_email": "有問題嗎?請透過 hi@voicestudio.sh 聯繫", "footer_discord": "加入我們的 Discord 以獲得社群支持。", "benefit_ip": "完全IP所有權", "benefit_ip_desc": "您擁有每一個輸出、每一個模型、每一個衍生品。無共享租賃,無資料外洩。", diff --git a/electron/src/shared/i18n/locales/ar.json b/electron/src/shared/i18n/locales/ar.json index 3c078a5ca..3d73ff631 100644 --- a/electron/src/shared/i18n/locales/ar.json +++ b/electron/src/shared/i18n/locales/ar.json @@ -1785,7 +1785,7 @@ "pricing_desc": "لا يزال يجري وضع اللمسات النهائية على المستويات والتسعير.", "pricing_detail": "وإلى أن يتم نشرها بشكل عام، يتم اقتباس كل عملية نشر تجارية بشكل فردي حتى نتمكن من تحديد الحجم المناسب لفريقك وعبء العمل.", "request_quote": "طلب عرض أسعار", - "footer_email": "أسئلة؟ تواصل معنا على VoiceStudio@palash.dev", + "footer_email": "أسئلة؟ تواصل معنا على hi@voicestudio.sh", "footer_discord": "انضم إلى Discord للحصول على دعم المجتمع.", "benefit_ip": "ملكية IP كاملة", "benefit_ip_desc": "أنت تمتلك كل مخرجات، كل نموذج، كل مشتق. لا يوجد إيجار مشترك، ولا يوجد تسرب للبيانات.", diff --git a/electron/src/shared/i18n/locales/de.json b/electron/src/shared/i18n/locales/de.json index 2539c92bf..c3cd4b8ba 100644 --- a/electron/src/shared/i18n/locales/de.json +++ b/electron/src/shared/i18n/locales/de.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Stufen und Preise werden noch finalisiert.", "pricing_detail": "Bis zur Veröffentlichung wird für jede kommerzielle Bereitstellung ein individuelles Angebot erstellt, damit wir die richtige Größe für Ihr Team und Ihre Arbeitslast festlegen können.", "request_quote": "Fordern Sie ein Angebot an", - "footer_email": "Fragen? Kontaktieren Sie uns unter VoiceStudio@palash.dev", + "footer_email": "Fragen? Kontaktieren Sie uns unter hi@voicestudio.sh", "footer_discord": "Treten Sie unserem Discord bei, um Community-Unterstützung zu erhalten.", "benefit_ip": "Vollständiges IP-Eigentum", "benefit_ip_desc": "Sie besitzen jede Ausgabe, jedes Modell, jedes Derivat. Keine gemeinsame Miete, keine Datenlecks.", diff --git a/electron/src/shared/i18n/locales/en.json b/electron/src/shared/i18n/locales/en.json index 663e90a9f..56a020d95 100644 --- a/electron/src/shared/i18n/locales/en.json +++ b/electron/src/shared/i18n/locales/en.json @@ -2062,7 +2062,7 @@ "pricing_desc": "Tiers and pricing are still being finalized.", "pricing_detail": "Until they’re public, every commercial deployment is being quoted individually so we can right-size for your team and workload.", "request_quote": "Request a quote", - "footer_email": "Questions? Reach out at VoiceStudio@palash.dev", + "footer_email": "Questions? Reach out at hi@voicestudio.sh", "footer_discord": "Join our Discord for community support.", "benefit_ip": "Full IP ownership", "benefit_ip_desc": "You own every output, every model, every derivative. No shared tenancy, no data leaks.", diff --git a/electron/src/shared/i18n/locales/es.json b/electron/src/shared/i18n/locales/es.json index 84ae65593..3b72b91ac 100644 --- a/electron/src/shared/i18n/locales/es.json +++ b/electron/src/shared/i18n/locales/es.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Los niveles y los precios aún se están ultimando.", "pricing_detail": "Hasta que sean públicas, cada implementación comercial se cotiza individualmente para que podamos adaptarla al tamaño adecuado para su equipo y carga de trabajo.", "request_quote": "Solicitar una cotización", - "footer_email": "¿Preguntas? Comuníquese con VoiceStudio@palash.dev", + "footer_email": "¿Preguntas? Comuníquese con hi@voicestudio.sh", "footer_discord": "Únase a nuestro Discord para obtener apoyo de la comunidad.", "benefit_ip": "Propiedad total de propiedad intelectual", "benefit_ip_desc": "Eres dueño de cada producto, cada modelo, cada derivado. Sin arrendamiento compartido, sin fugas de datos.", diff --git a/electron/src/shared/i18n/locales/fr.json b/electron/src/shared/i18n/locales/fr.json index bb4a8f102..b4e410dfd 100644 --- a/electron/src/shared/i18n/locales/fr.json +++ b/electron/src/shared/i18n/locales/fr.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Les niveaux et les tarifs sont encore en cours de finalisation.", "pricing_detail": "Jusqu'à ce qu'ils soient publics, chaque déploiement commercial est proposé individuellement afin que nous puissions l'adapter à votre équipe et à votre charge de travail.", "request_quote": "Demander un devis", - "footer_email": "Des questions ? Contactez VoiceStudio@palash.dev", + "footer_email": "Des questions ? Contactez hi@voicestudio.sh", "footer_discord": "Rejoignez notre Discord pour le soutien de la communauté.", "benefit_ip": "Propriété IP complète", "benefit_ip_desc": "Vous possédez chaque sortie, chaque modèle, chaque dérivé. Pas de location partagée, pas de fuite de données.", diff --git a/electron/src/shared/i18n/locales/hi.json b/electron/src/shared/i18n/locales/hi.json index 531f0720c..0b3972a05 100644 --- a/electron/src/shared/i18n/locales/hi.json +++ b/electron/src/shared/i18n/locales/hi.json @@ -1785,7 +1785,7 @@ "pricing_desc": "टियर और मूल्य निर्धारण को अभी भी अंतिम रूप दिया जा रहा है।", "pricing_detail": "जब तक वे सार्वजनिक नहीं होते, प्रत्येक व्यावसायिक तैनाती को व्यक्तिगत रूप से उद्धृत किया जा रहा है ताकि हम आपकी टीम और कार्यभार के लिए सही आकार दे सकें।", "request_quote": "एक उद्धरण का अनुरोध करें", - "footer_email": "प्रश्न? VoiceStudio@palash.dev पर संपर्क करें", + "footer_email": "प्रश्न? hi@voicestudio.sh पर संपर्क करें", "footer_discord": "सामुदायिक समर्थन के लिए हमारे डिस्कोर्ड में शामिल हों।", "benefit_ip": "पूर्ण आईपी स्वामित्व", "benefit_ip_desc": "आप हर आउटपुट, हर मॉडल, हर डेरिवेटिव के मालिक हैं। कोई साझा किरायेदारी नहीं, कोई डेटा लीक नहीं।", diff --git a/electron/src/shared/i18n/locales/id.json b/electron/src/shared/i18n/locales/id.json index 639e3adb2..fdd1e5348 100644 --- a/electron/src/shared/i18n/locales/id.json +++ b/electron/src/shared/i18n/locales/id.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Tingkatan dan harga masih dalam tahap penyelesaian.", "pricing_detail": "Sebelum dipublikasikan, setiap penerapan komersial akan dikutip satu per satu sehingga kami dapat menyesuaikan ukurannya untuk tim dan beban kerja Anda.", "request_quote": "Minta penawaran", - "footer_email": "Pertanyaan? Hubungi VoiceStudio@palash.dev", + "footer_email": "Pertanyaan? Hubungi hi@voicestudio.sh", "footer_discord": "Bergabunglah dengan Discord kami untuk mendapatkan dukungan komunitas.", "benefit_ip": "Kepemilikan IP penuh", "benefit_ip_desc": "Anda memiliki setiap keluaran, setiap model, setiap turunan. Tidak ada sewa bersama, tidak ada kebocoran data.", diff --git a/electron/src/shared/i18n/locales/it.json b/electron/src/shared/i18n/locales/it.json index 14ecb2a0a..ed62ff418 100644 --- a/electron/src/shared/i18n/locales/it.json +++ b/electron/src/shared/i18n/locales/it.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Livelli e prezzi sono ancora in fase di definizione.", "pricing_detail": "Fino a quando non saranno pubbliche, ogni implementazione commerciale verrà quotata individualmente in modo da poter dimensionare correttamente il tuo team e il tuo carico di lavoro.", "request_quote": "Richiedi un preventivo", - "footer_email": "Domande? Contatta VoiceStudio@palash.dev", + "footer_email": "Domande? Contatta hi@voicestudio.sh", "footer_discord": "Unisciti al nostro Discord per il supporto della comunità.", "benefit_ip": "Proprietà IP completa", "benefit_ip_desc": "Possiedi ogni risultato, ogni modello, ogni derivato. Nessuna locazione condivisa, nessuna fuga di dati.", diff --git a/electron/src/shared/i18n/locales/ja.json b/electron/src/shared/i18n/locales/ja.json index dfde39e6c..879681829 100644 --- a/electron/src/shared/i18n/locales/ja.json +++ b/electron/src/shared/i18n/locales/ja.json @@ -1785,7 +1785,7 @@ "pricing_desc": "レベルと価格はまだ最終決定中です。", "pricing_detail": "公開されるまで、すべての商用展開は個別に見積もられるため、お客様のチームとワークロードに合わせて適切な規模を設定できます。", "request_quote": "見積もりを依頼する", - "footer_email": "質問がありますか? VoiceStudio@palash.dev までご連絡ください。", + "footer_email": "質問がありますか? hi@voicestudio.sh までご連絡ください。", "footer_discord": "コミュニティサポートのために Discord に参加してください。", "benefit_ip": "完全な IP 所有権", "benefit_ip_desc": "あなたはすべての出力、すべてのモデル、すべての派生製品を所有します。共有テナントやデータ漏洩はありません。", diff --git a/electron/src/shared/i18n/locales/ko.json b/electron/src/shared/i18n/locales/ko.json index 5bb1887a6..feaf62c35 100644 --- a/electron/src/shared/i18n/locales/ko.json +++ b/electron/src/shared/i18n/locales/ko.json @@ -2096,7 +2096,7 @@ "pricing_desc": "등급과 가격은 아직 확정 중입니다.", "pricing_detail": "공개될 때까지 모든 상용 배포는 개별적으로 견적이 나오므로 귀하의 팀과 작업량에 맞게 규모를 조정할 수 있습니다.", "request_quote": "견적 요청", - "footer_email": "질문이 있으신가요? VoiceStudio@palash.dev로 문의하세요.", + "footer_email": "질문이 있으신가요? hi@voicestudio.sh로 문의하세요.", "footer_discord": "커뮤니티 지원을 위해 Discord에 참여하세요.", "benefit_ip": "전체 IP 소유권", "benefit_ip_desc": "귀하는 모든 산출물, 모든 모델, 모든 파생물을 소유합니다. 공유 테넌시가 없고 데이터 유출도 없습니다.", diff --git a/electron/src/shared/i18n/locales/nl.json b/electron/src/shared/i18n/locales/nl.json index 483d81781..4942b1131 100644 --- a/electron/src/shared/i18n/locales/nl.json +++ b/electron/src/shared/i18n/locales/nl.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Niveaus en prijzen worden nog afgerond.", "pricing_detail": "Totdat ze openbaar zijn, wordt elke commerciële implementatie afzonderlijk geciteerd, zodat we de juiste maat kunnen vinden voor uw team en werklast.", "request_quote": "Vraag een offerte aan", - "footer_email": "Vragen? Neem contact op met VoiceStudio@palash.dev", + "footer_email": "Vragen? Neem contact op met hi@voicestudio.sh", "footer_discord": "Sluit je aan bij onze Discord voor community-ondersteuning.", "benefit_ip": "Volledig IP-eigendom", "benefit_ip_desc": "U bezit elke output, elk model, elke afgeleide. Geen gedeelde huur, geen datalekken.", diff --git a/electron/src/shared/i18n/locales/pl.json b/electron/src/shared/i18n/locales/pl.json index 474071a09..ccd27ef0a 100644 --- a/electron/src/shared/i18n/locales/pl.json +++ b/electron/src/shared/i18n/locales/pl.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Poziomy i ceny są wciąż finalizowane.", "pricing_detail": "Dopóki nie zostaną one upublicznione, każde wdrożenie komercyjne jest wyceniane indywidualnie, abyśmy mogli dopasować rozmiar do Twojego zespołu i obciążenia pracą.", "request_quote": "Poproś o wycenę", - "footer_email": "Pytania? Skontaktuj się z nami pod adresem VoiceStudio@palash.dev", + "footer_email": "Pytania? Skontaktuj się z nami pod adresem hi@voicestudio.sh", "footer_discord": "Dołącz do naszego Discorda, aby uzyskać wsparcie społeczności.", "benefit_ip": "Pełna własność IP", "benefit_ip_desc": "Jesteś właścicielem każdego produktu, każdego modelu i każdej pochodnej. Żadnych wspólnych najemców, żadnych wycieków danych.", diff --git a/electron/src/shared/i18n/locales/pt.json b/electron/src/shared/i18n/locales/pt.json index 8ef42a99d..9749ea2d4 100644 --- a/electron/src/shared/i18n/locales/pt.json +++ b/electron/src/shared/i18n/locales/pt.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Os níveis e os preços ainda estão sendo finalizados.", "pricing_detail": "Até que sejam públicas, cada implantação comercial será cotada individualmente para que possamos dimensionar adequadamente sua equipe e carga de trabalho.", "request_quote": "Solicite um orçamento", - "footer_email": "Perguntas? Entre em contato com VoiceStudio@palash.dev", + "footer_email": "Perguntas? Entre em contato com hi@voicestudio.sh", "footer_discord": "Junte-se ao nosso Discord para obter suporte da comunidade.", "benefit_ip": "Propriedade total de IP", "benefit_ip_desc": "Você possui todos os resultados, todos os modelos, todas as derivadas. Sem locação compartilhada, sem vazamento de dados.", diff --git a/electron/src/shared/i18n/locales/ru.json b/electron/src/shared/i18n/locales/ru.json index 57ec11c3f..5f3c9e729 100644 --- a/electron/src/shared/i18n/locales/ru.json +++ b/electron/src/shared/i18n/locales/ru.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Уровни и цены еще уточняются.", "pricing_detail": "Пока они не станут общедоступными, каждое коммерческое развертывание оценивается индивидуально, чтобы мы могли подобрать правильный размер для вашей команды и рабочей нагрузки.", "request_quote": "Запросить ценовое предложение", - "footer_email": "Вопросы? Свяжитесь с нами по адресу VoiceStudio@palash.dev.", + "footer_email": "Вопросы? Свяжитесь с нами по адресу hi@voicestudio.sh.", "footer_discord": "Присоединяйтесь к нашему Discord для поддержки сообщества.", "benefit_ip": "Полное владение интеллектуальной собственностью", "benefit_ip_desc": "Вы владеете каждым продуктом, каждой моделью, каждым производным продуктом. Никакой совместной аренды, никаких утечек данных.", diff --git a/electron/src/shared/i18n/locales/sv.json b/electron/src/shared/i18n/locales/sv.json index d68822c9c..ccec1f04e 100644 --- a/electron/src/shared/i18n/locales/sv.json +++ b/electron/src/shared/i18n/locales/sv.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Nivåer och prissättning håller fortfarande på att slutföras.", "pricing_detail": "Tills de är offentliga citeras varje kommersiell distribution individuellt så att vi kan anpassa storleken för ditt team och din arbetsbelastning.", "request_quote": "Begär en offert", - "footer_email": "Frågor? Kontakta VoiceStudio@palash.dev", + "footer_email": "Frågor? Kontakta hi@voicestudio.sh", "footer_discord": "Gå med i vår Discord för samhällsstöd.", "benefit_ip": "Fullt IP-ägande", "benefit_ip_desc": "Du äger varje utgång, varje modell, varje derivat. Ingen delad hyresrätt, inga dataläckor.", diff --git a/electron/src/shared/i18n/locales/th.json b/electron/src/shared/i18n/locales/th.json index 93f38ec32..4f9c5cbb3 100644 --- a/electron/src/shared/i18n/locales/th.json +++ b/electron/src/shared/i18n/locales/th.json @@ -1785,7 +1785,7 @@ "pricing_desc": "ระดับและราคายังอยู่ในระหว่างการสรุปผล", "pricing_detail": "จนกว่าจะเปิดเผยต่อสาธารณะ การใช้งานเชิงพาณิชย์ทุกครั้งจะถูกเสนอราคาเป็นรายบุคคล เพื่อให้เราสามารถปรับขนาดให้เหมาะสมกับทีมและปริมาณงานของคุณได้", "request_quote": "ขอใบเสนอราคา", - "footer_email": "มีคำถาม? ติดต่อได้ที่ VoiceStudio@palash.dev", + "footer_email": "มีคำถาม? ติดต่อได้ที่ hi@voicestudio.sh", "footer_discord": "เข้าร่วม Discord ของเราเพื่อรับการสนับสนุนจากชุมชน", "benefit_ip": "ความเป็นเจ้าของ IP เต็มรูปแบบ", "benefit_ip_desc": "คุณเป็นเจ้าของทุกผลผลิต ทุกรุ่น และอนุพันธ์ทั้งหมด ไม่มีการเช่าร่วมกัน ไม่มีข้อมูลรั่วไหล", diff --git a/electron/src/shared/i18n/locales/tr.json b/electron/src/shared/i18n/locales/tr.json index 1e3301f0f..8a5f804d6 100644 --- a/electron/src/shared/i18n/locales/tr.json +++ b/electron/src/shared/i18n/locales/tr.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Katmanlar ve fiyatlandırma henüz kesinleşme aşamasındadır.", "pricing_detail": "Herkese açık hale gelene kadar her ticari dağıtım ayrı ayrı fiyatlandırılır, böylece ekibinize ve iş yükünüze göre doğru boyutu belirleyebiliriz.", "request_quote": "Fiyat teklifi isteyin", - "footer_email": "Sorunuz mu var? VoiceStudio@palash.dev adresinden bize ulaşın", + "footer_email": "Sorunuz mu var? hi@voicestudio.sh adresinden bize ulaşın", "footer_discord": "Topluluk desteği için Discord'umuza katılın.", "benefit_ip": "Tam IP sahipliği", "benefit_ip_desc": "Her çıktının, her modelin, her türevinin sahibi sizsiniz. Paylaşılan kiracılık yok, veri sızıntısı yok.", diff --git a/electron/src/shared/i18n/locales/uk.json b/electron/src/shared/i18n/locales/uk.json index 714d599de..ea581578f 100644 --- a/electron/src/shared/i18n/locales/uk.json +++ b/electron/src/shared/i18n/locales/uk.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Рівні та ціни ще завершуються.", "pricing_detail": "Поки вони не стануть загальнодоступними, кожне комерційне розгортання оцінюється окремо, щоб ми могли підібрати правильний розмір для вашої команди та робочого навантаження.", "request_quote": "Запитати цінову пропозицію", - "footer_email": "Запитання? Зверніться за адресою VoiceStudio@palash.dev", + "footer_email": "Запитання? Зверніться за адресою hi@voicestudio.sh", "footer_discord": "Приєднуйтесь до нашого Discord для підтримки спільноти.", "benefit_ip": "Повне володіння IP", "benefit_ip_desc": "Ви володієте кожним результатом, кожною моделлю, кожною похідною. Жодної спільної оренди, жодних витоків даних.", diff --git a/electron/src/shared/i18n/locales/vi.json b/electron/src/shared/i18n/locales/vi.json index 4111af3e9..d8c2c3240 100644 --- a/electron/src/shared/i18n/locales/vi.json +++ b/electron/src/shared/i18n/locales/vi.json @@ -1785,7 +1785,7 @@ "pricing_desc": "Các cấp độ và giá cả vẫn đang được hoàn thiện.", "pricing_detail": "Cho đến khi chúng được công khai, mọi hoạt động triển khai thương mại đều được báo giá riêng lẻ để chúng tôi có thể điều chỉnh quy mô phù hợp với nhóm và khối lượng công việc của bạn.", "request_quote": "Yêu cầu báo giá", - "footer_email": "Câu hỏi? Hãy liên hệ tại VoiceStudio@palash.dev", + "footer_email": "Câu hỏi? Hãy liên hệ tại hi@voicestudio.sh", "footer_discord": "Tham gia Discord của chúng tôi để được hỗ trợ cộng đồng.", "benefit_ip": "Quyền sở hữu IP đầy đủ", "benefit_ip_desc": "Bạn sở hữu mọi đầu ra, mọi mô hình, mọi công cụ phái sinh. Không có thuê chung, không có rò rỉ dữ liệu.", diff --git a/electron/src/shared/i18n/locales/zh-CN.json b/electron/src/shared/i18n/locales/zh-CN.json index 3acaf2e90..38b145142 100644 --- a/electron/src/shared/i18n/locales/zh-CN.json +++ b/electron/src/shared/i18n/locales/zh-CN.json @@ -2048,7 +2048,7 @@ "pricing_desc": "定价层级仍在确定中。", "pricing_detail": "在公开之前,每个商业部署都将单独报价,以便为您的团队和工作负载量身定制。", "request_quote": "请求报价", - "footer_email": "有问题?请联系 VoiceStudio@palash.dev", + "footer_email": "有问题?请联系 hi@voicestudio.sh", "footer_discord": "加入我们的 Discord 获取社区支持。", "benefit_ip": "完全 IP 所有权", "benefit_ip_desc": "您拥有所有输出、模型和衍生作品。无共享租户,无数据泄露。", diff --git a/electron/src/shared/i18n/locales/zh-TW.json b/electron/src/shared/i18n/locales/zh-TW.json index eb5380f44..c01653424 100644 --- a/electron/src/shared/i18n/locales/zh-TW.json +++ b/electron/src/shared/i18n/locales/zh-TW.json @@ -1785,7 +1785,7 @@ "pricing_desc": "等級和定價仍在最終確定中。", "pricing_detail": "在公開之前,每個商業部署都會單獨報價,以便我們可以根據您的團隊和工作負載調整規模。", "request_quote": "索取報價", - "footer_email": "有問題嗎?請透過 VoiceStudio@palash.dev 聯繫", + "footer_email": "有問題嗎?請透過 hi@voicestudio.sh 聯繫", "footer_discord": "加入我們的 Discord 以獲得社群支持。", "benefit_ip": "完全IP所有權", "benefit_ip_desc": "您擁有每一個輸出、每一個模型、每一個衍生品。無共享租賃,無資料外洩。", diff --git a/electron/src/shared/pages/ContactPage.jsx b/electron/src/shared/pages/ContactPage.jsx index 0df628077..ce5ab6dcf 100644 --- a/electron/src/shared/pages/ContactPage.jsx +++ b/electron/src/shared/pages/ContactPage.jsx @@ -28,7 +28,7 @@ const DISCORD_URL = 'https://discord.gg/bzQavDfVV9'; // GitHub Security Advisories = the private "report a vulnerability" flow that // SECURITY.md points at (never a public issue for security bugs). const SECURITY_URL = `${REPO_URL}/security/advisories/new`; -const EMAIL = 'VoiceStudio@palash.dev'; +const EMAIL = 'hi@voicestudio.sh'; const WEBSITE_URL = 'https://palash.dev'; const X_URL = 'https://x.com/idebpalash'; diff --git a/electron/src/shared/pages/SupportPage.jsx b/electron/src/shared/pages/SupportPage.jsx index 7513884e6..fdc450c21 100644 --- a/electron/src/shared/pages/SupportPage.jsx +++ b/electron/src/shared/pages/SupportPage.jsx @@ -363,9 +363,9 @@ function SupportView() { } /* ── Commercial License panel ─────────────────────────────────────────── */ -const LICENSE_EMAIL = 'VoiceStudio@palash.dev'; +const LICENSE_EMAIL = 'hi@voicestudio.sh'; const LICENSE_MAILTO = - 'mailto:VoiceStudio@palash.dev?subject=VoiceStudio Commercial License Inquiry' + + 'mailto:hi@voicestudio.sh?subject=VoiceStudio Commercial License Inquiry' + '&body=Hi Palash,%0A%0AI%27d like to talk about a commercial license for VoiceStudio.%0A%0AOrganization:%0ATeam size:%0AUse case:%0A'; function LicenseView() { diff --git a/electron/src/shared/test/ContactPage.test.jsx b/electron/src/shared/test/ContactPage.test.jsx index 25fceb7bd..a47260d40 100644 --- a/electron/src/shared/test/ContactPage.test.jsx +++ b/electron/src/shared/test/ContactPage.test.jsx @@ -51,7 +51,7 @@ describe('contact sections', () => { expect(href('Join the Discord')).toBe('https://discord.gg/bzQavDfVV9'); expect(href('Follow on X')).toBe('https://x.com/idebpalash'); expect(href('Report privately')).toBe(`${REPO}/security/advisories/new`); - expect(href(/licensing/i)).toBe(`mailto:VoiceStudio@palash.dev`); + expect(href(/licensing/i)).toBe(`mailto:hi@voicestudio.sh`); expect(href(/more about the project/i)).toBe('https://palash.dev'); }); diff --git a/electron/src/shared/utils/contactLinks.js b/electron/src/shared/utils/contactLinks.js index d4cb12045..ab5eeb949 100644 --- a/electron/src/shared/utils/contactLinks.js +++ b/electron/src/shared/utils/contactLinks.js @@ -4,11 +4,11 @@ export const DISCORD_URL = 'https://discord.gg/bzQavDfVV9'; // GitHub Security Advisories = the private "report a vulnerability" flow that // SECURITY.md points at (never a public issue for security bugs). export const SECURITY_URL = `${REPO_URL}/security/advisories/new`; -export const EMAIL = 'VoiceStudio@palash.dev'; +export const EMAIL = 'hi@voicestudio.sh'; export const WEBSITE_URL = 'https://palash.dev'; export const X_URL = 'https://x.com/idebpalash'; -export const LICENSE_EMAIL = 'VoiceStudio@palash.dev'; +export const LICENSE_EMAIL = 'hi@voicestudio.sh'; export const LICENSE_MAILTO = - 'mailto:VoiceStudio@palash.dev?subject=VoiceStudio Commercial License Inquiry' + + 'mailto:hi@voicestudio.sh?subject=VoiceStudio Commercial License Inquiry' + '&body=Hi Palash,%0A%0AI%27d like to talk about a commercial license for VoiceStudio.%0A%0AOrganization:%0ATeam size:%0AUse case:%0A'; diff --git a/pyproject.toml b/pyproject.toml index 3299cd0ce..3b101e987 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "VoiceStudio — a private, local-first studio for voice cloning, readme = "README.md" # Free and open-source under the GNU Affero General Public License v3 (see # LICENSE). A commercial license is available for proprietary/closed-source use -# without AGPL obligations — contact OmniVoice@palash.dev. The bundled omnivoice/ +# without AGPL obligations — contact hi@voicestudio.sh. The bundled omnivoice/ # TTS model by Han Zhu remains Apache-2.0 upstream (Apache-2.0 is AGPL-compatible). license = "AGPL-3.0-only" requires-python = ">=3.11" diff --git a/tests/test_contact_emails.py b/tests/test_contact_emails.py new file mode 100644 index 000000000..ce43df827 --- /dev/null +++ b/tests/test_contact_emails.py @@ -0,0 +1,43 @@ +"""VoiceStudio publishes exactly three contact addresses (owner-set 2026-10-02). + + hi@voicestudio.sh general, business, licensing, collaboration + partner@voicestudio.sh brand and company partnerships + security@voicestudio.sh vulnerability reports + +Any other real address in a shipped file (docs, UI, locales, package metadata, +legal text) is a stale or personal contact. Tests and fixtures may use +reserved example domains (RFC 2606 / RFC 6761) or fake addresses. +""" +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +_REPO = Path(__file__).resolve().parents[1] +_CONTACTS = {"hi@voicestudio.sh", "partner@voicestudio.sh", "security@voicestudio.sh"} +_EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}") +_RESERVED_DOMAIN = re.compile(r"(^|\.)(example\.(com|net|org)|example|test|invalid|localhost|local)$", re.I) +_TEST_PATH = re.compile(r"(^|/)(tests?|__tests__|fixtures)/|\.test\.[a-z]+$|_test\.py$") + + +def _tracked_text_files(): + names = subprocess.run( + ["git", "-C", str(_REPO), "ls-files", "-z"], capture_output=True, text=True, check=True + ).stdout.split("\0") + for name in filter(None, names): + try: + yield name, (_REPO / name).read_text(encoding="utf-8") + except (UnicodeDecodeError, FileNotFoundError, IsADirectoryError): + continue + + +def test_only_project_contact_addresses_are_published(): + stray = [] + for name, text in _tracked_text_files(): + for address in _EMAIL.findall(text): + domain = address.rsplit("@", 1)[1] + if address.lower() in _CONTACTS or _RESERVED_DOMAIN.search(domain) or _TEST_PATH.search(name): + continue + stray.append(f"{name}: {address}") + assert not stray, "Use hi@, partner@ or security@voicestudio.sh:\n" + "\n".join(stray) From 901895b5de2eb8a6198058bd21ad475c36e9c396 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:07:06 +0530 Subject: [PATCH 06/85] docs(maintainers): require the CLA status and record the hardening changes Points the main ruleset at the CLA commit status the checker sets, notes that the checker creates the signature branch, lets the contact-address guard accept GitHub no-reply examples in docs, and adds changelog lines for the batch. --- CHANGELOG.md | 3 +++ docs/maintainers/repository-settings.md | 19 +++++++++++-------- tests/test_contact_emails.py | 8 ++++++-- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 844ba1bcb..eda2e1742 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,8 +78,11 @@ metadata and the backend fallback mirror it. - Linux AppImages use the static runtime so launching them no longer needs libfuse2 (#2328) — thanks @shuvashish76! - Voice Clone shows upload and record side by side instead of behind a toggle (#2307) - Pull requests ask their opener and every commit author and co-author to sign a one-time Contributor License Agreement by comment; the contributing guide explains how +- Pull requests fail when a commit uses a leaked personal email or a placeholder identity such as an unconfigured hostname address +- GitHub Actions are pinned to exact commits and kept current by Dependabot ### Docs +- Maintainer guide for repository settings that can't live in code; the licence notice scope and the contributing guide's list of network calls match the current app - Contact addresses are now hi@voicestudio.sh (general and licensing), partner@voicestudio.sh (partnerships) and security@voicestudio.sh (security reports) - Chinese README now matches the Electron installation and migration guide (#2377) — thanks @lg114! diff --git a/docs/maintainers/repository-settings.md b/docs/maintainers/repository-settings.md index 22a48c922..41789cddc 100644 --- a/docs/maintainers/repository-settings.md +++ b/docs/maintainers/repository-settings.md @@ -14,11 +14,14 @@ pull requests. The `context` values must match the check names shown on a pull request: - `Tests (backend + frontend)`: the `name:` of the `test` job in `.github/workflows/ci.yml`. -- `cla`: the job ID in `.github/workflows/cla.yml`. That job has no `name:`, so - its check is called `cla`. If the job gains `name: CLA`, use `CLA` here. - -`integration_id` 15368 is GitHub Actions, so only workflow runs can satisfy -these checks. `strict_required_status_checks_policy` requires branches to be up +- `CLA`: the commit status that `.github/scripts/cla_check.py` sets on the + pull request's head commit. Require this status, not the `cla` job: the job + succeeds whenever the checker runs, and comment-triggered runs are not + attached to the pull request. + +`integration_id` 15368 is GitHub Actions, so only workflow runs can satisfy the +test check. The `CLA` entry omits it, so the status counts whichever token the +workflow uses. `strict_required_status_checks_policy` requires branches to be up to date with `main` before merging. ```bash @@ -38,7 +41,7 @@ gh api --method POST repos/debpalash/VoiceStudio/rulesets --input - <<'JSON' "strict_required_status_checks_policy": true, "required_status_checks": [ { "context": "Tests (backend + frontend)", "integration_id": 15368 }, - { "context": "cla", "integration_id": 15368 } + { "context": "CLA" } ] } } @@ -60,8 +63,8 @@ pushes** and **Require status checks to pass**, then add both checks with source ## Ruleset: `cla-signatures` -`.github/workflows/cla.yml` commits signatures to this branch with the -workflow's `GITHUB_TOKEN`. The ruleset blocks deletion and force-push only. It +`.github/scripts/cla_check.py` creates this branch on the first signature and +commits signatures to it with the workflow's `GITHUB_TOKEN`. The ruleset blocks deletion and force-push only. It does not restrict updates or require checks, so the workflow can keep committing. You can create the ruleset before the branch exists. diff --git a/tests/test_contact_emails.py b/tests/test_contact_emails.py index ce43df827..1387609d1 100644 --- a/tests/test_contact_emails.py +++ b/tests/test_contact_emails.py @@ -6,7 +6,8 @@ Any other real address in a shipped file (docs, UI, locales, package metadata, legal text) is a stale or personal contact. Tests and fixtures may use -reserved example domains (RFC 2606 / RFC 6761) or fake addresses. +reserved example domains (RFC 2606 / RFC 6761) or fake addresses, and docs may +show GitHub no-reply commit addresses. """ from __future__ import annotations @@ -18,6 +19,8 @@ _CONTACTS = {"hi@voicestudio.sh", "partner@voicestudio.sh", "security@voicestudio.sh"} _EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}") _RESERVED_DOMAIN = re.compile(r"(^|\.)(example\.(com|net|org)|example|test|invalid|localhost|local)$", re.I) +# GitHub no-reply commit addresses are identities, not contact addresses. +_NOREPLY_DOMAIN = "users.noreply.github.com" _TEST_PATH = re.compile(r"(^|/)(tests?|__tests__|fixtures)/|\.test\.[a-z]+$|_test\.py$") @@ -37,7 +40,8 @@ def test_only_project_contact_addresses_are_published(): for name, text in _tracked_text_files(): for address in _EMAIL.findall(text): domain = address.rsplit("@", 1)[1] - if address.lower() in _CONTACTS or _RESERVED_DOMAIN.search(domain) or _TEST_PATH.search(name): + if (address.lower() in _CONTACTS or _RESERVED_DOMAIN.search(domain) or domain.lower() == _NOREPLY_DOMAIN + or _TEST_PATH.search(name)): continue stray.append(f"{name}: {address}") assert not stray, "Use hi@, partner@ or security@voicestudio.sh:\n" + "\n".join(stray) From 2d934e2bc52b7e6e76d5d0f585de33940a33177a Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 02:19:49 +0530 Subject: [PATCH 07/85] fix(cla): close review gaps before rollout Moves concurrency to the job so skipped comment runs never cancel real ones, rescans signing issues so concurrent signatures are kept, ignores edited comments and stores a hash of each signing comment, treats AI-agent identities (including Copilot and any openai.com address) the same in the checker and the audit, asks authors of superseded pull requests to sign, adds a maintainer cla-override label, counts each co-author once per commit in the audit, keeps the agreements LF on every OS, limits grouped Dependabot updates to minor and patch, and documents the rollout order and admin bypass. --- .gitattributes | 3 + .github/CONTRIBUTING.md | 3 +- .github/dependabot.yml | 6 +- .github/scripts/cla_check.py | 105 +++++++++++++++++------- .github/workflows/cla.yml | 12 +-- docs/maintainers/repository-settings.md | 33 ++++++-- scripts/cla_audit.py | 20 +++-- tests/test_cla_check.py | 100 ++++++++++++++++------ tests/test_cla_workflow.py | 2 +- 9 files changed, 209 insertions(+), 75 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..e060cb51f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Hash-pinned legal texts (tests/test_cla_workflow.py): identical bytes on every OS. +.github/CLA-*.md text eol=lf +.github/CCLA-*.md text eol=lf diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 437923e5b..2cf089139 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -362,7 +362,8 @@ I have read the VoiceStudio CLA 1.0 and I hereby sign it. ``` The signature covers your earlier and future contributions, and the check -turns green on its own. Contributed before and have no open pull request? Post +turns green on its own. If a maintainer folds your pull request into another +one, you sign once there too. Contributed before and have no open pull request? Post the same line on the [issue labelled `cla`](https://github.com/debpalash/VoiceStudio/issues?q=label%3Acla). If it lists a commit it cannot link to a GitHub account, either add that commit email to your account (Settings → Emails), or rewrite the commits with an email that is on it (`git commit --amend diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 87cbb4223..404eb66b8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,8 @@ version: 2 updates: # Keeps the SHA-pinned actions in .github/workflows current (pins are - # enforced by tests/test_actions_pinned.py). One grouped PR per week. + # enforced by tests/test_actions_pinned.py). Minor and patch updates arrive + # as one grouped PR per week; major versions come as separate PRs. - package-ecosystem: github-actions directory: / schedule: @@ -10,5 +11,8 @@ updates: github-actions: patterns: - "*" + update-types: + - minor + - patch commit-message: prefix: "chore(ci)" diff --git a/.github/scripts/cla_check.py b/.github/scripts/cla_check.py index 0eca5928c..505e1e845 100644 --- a/.github/scripts/cla_check.py +++ b/.github/scripts/cla_check.py @@ -11,13 +11,17 @@ commit email is not linked to a GitHub account cannot be verified and fail the check with instructions. -Each run rescans every comment on the pull request, so a run that GitHub -cancels while queued cannot lose a signature. The result is a commit status -named "CLA" on the head commit, which branch protection can require. +Each run rescans every comment on the pull request (or signing issue) and +records any valid signature not yet stored, so the next run picks up anything a +run cancelled while queued missed. Edited comments never count. The result is a +commit status named "CLA" on the head commit, which branch protection can +require. A maintainer can apply the `cla-override` label after reviewing a pull +request by hand, for example one that folds in commits from agent identities. """ from __future__ import annotations import base64 +import hashlib import json import os import re @@ -36,6 +40,7 @@ # Issues carrying this label (only maintainers can apply labels) accept # signatures from anyone, so past contributors can sign without a pull request. SIGNING_LABEL = "cla" +OVERRIDE_LABEL = "cla-override" COMMENT_MARKER = "" MAX_COMMITS = 250 @@ -50,10 +55,14 @@ # GitHub App bots also commit as "+[bot]@users.noreply.github.com"; # a person cannot hold such a login, and the authenticated opener still signs. TOOL_EMAILS = re.compile( - r"^(noreply@anthropic\.com|cursoragent@cursor\.com|codex@openai\.com|noreply@coderabbit\.ai" + r"^(noreply@anthropic\.com|cursoragent@cursor\.com|[^@]+@openai\.com|noreply@coderabbit\.ai" + r"|codex@users\.noreply\.github\.com|\d+\+copilot@users\.noreply\.github\.com" r"|\d+\+[^@]*\[bot\]@users\.noreply\.github\.com)$", re.I, ) +# A pull request that says it supersedes others must also be signed by their authors. +SUPERSEDES = re.compile(r"supersed", re.I) +PR_REF = re.compile(r"#(\d+)") NOREPLY = re.compile(r"^(?:(\d+)\+)?([A-Za-z0-9-]+)@users\.noreply\.github\.com$", re.I) CO_AUTHOR = re.compile(r"^co-authored-by:\s*(.*?)\s*<([^>]*)>\s*$", re.I | re.M) @@ -83,8 +92,25 @@ def is_sign_comment(body: str) -> bool: return any(" ".join(line.lower().split()) == target for line in (body or "").splitlines()) -def is_recheck(body: str) -> bool: - return (body or "").strip().lower() == "recheck" +def is_valid_signature(comment: dict) -> bool: + """A signing comment that was never edited after it was posted.""" + edited = comment.get("updated_at") and comment.get("updated_at") != comment.get("created_at") + return is_sign_comment(comment.get("body")) and not edited + + +def superseded_numbers(body: str) -> list[int]: + """Pull request numbers on lines of a description that say they are superseded.""" + found: list[int] = [] + for line in (body or "").splitlines(): + if SUPERSEDES.search(line): + found += [n for n in map(int, PR_REF.findall(line)) if n not in found] + return list(dict.fromkeys(found)) + + +def signature(user: dict, comment: dict, repo_id, **where) -> dict: + return {"login": user["login"], "id": int(user["id"]), **where, "comment_id": comment["id"], + "signed_at": comment["created_at"], "version": CLA_VERSION, "repo_id": repo_id, + "body_sha256": hashlib.sha256((comment.get("body") or "").encode()).hexdigest()} def exempt(person: Person) -> bool: @@ -111,6 +137,8 @@ def require(person: Person) -> None: require(opener) for actor in actors: email = (actor.get("email") or "").strip() + if TOOL_EMAILS.match(email): + continue if actor.get("id"): require(Person(actor["login"], int(actor["id"]))) continue @@ -118,7 +146,7 @@ def require(person: Person) -> None: if match.group(1): require(Person(match.group(2), int(match.group(1)))) continue - if TOOL_EMAILS.match(email) or (opener_is_bot and "[bot]" in email): + if opener_is_bot and "[bot]" in email: continue identity = f"{actor.get('name') or '?'} <{email or 'no email'}>" if identity not in result.unknown: @@ -312,19 +340,29 @@ def upsert_comment(gh: GitHub, number: int, body: str, create: bool) -> None: gh.request("POST", f"/repos/{gh.repo}/issues/{number}/comments", {"body": body}) -def sign_on_issue(gh: GitHub, event: dict) -> bool: - """Record a signature posted on a maintainer-labelled signing issue.""" - issue, comment = event.get("issue") or {}, event.get("comment") or {} - user = comment.get("user") or {} - labels = {label.get("name") for label in issue.get("labels") or []} - if SIGNING_LABEL not in labels or user.get("type") != "User" or not is_sign_comment(comment.get("body")): - return False - save_signatures(gh, [{"login": user["login"], "id": int(user["id"]), "issue": issue["number"], - "comment_id": comment["id"], "signed_at": comment["created_at"], - "version": CLA_VERSION, "repo_id": (event.get("repository") or {}).get("id")}], - issue["number"]) - gh.request("POST", f"/repos/{gh.repo}/issues/comments/{comment['id']}/reactions", {"content": "+1"}) - return True +def sign_on_issue(gh: GitHub, event: dict) -> int: + """Record every valid signature on a maintainer-labelled signing issue. + + Rescans all comments, so signatures posted while another run was queued are + not lost. Returns the number of new signatures. + """ + issue = event.get("issue") or {} + if SIGNING_LABEL not in {label.get("name") for label in issue.get("labels") or []}: + return 0 + store, _ = load_store(gh) + have = {int(s["id"]) for s in store["signatures"]} + new, comment_ids = [], [] + for comment in gh.paginate(f"/repos/{gh.repo}/issues/{issue['number']}/comments"): + user = comment.get("user") or {} + if user.get("type") == "User" and int(user.get("id") or 0) not in have and is_valid_signature(comment): + have.add(int(user["id"])) + new.append(signature(user, comment, (event.get("repository") or {}).get("id"), issue=issue["number"])) + comment_ids.append(comment["id"]) + if new: + save_signatures(gh, new, issue["number"]) + for comment_id in comment_ids: + gh.request("POST", f"/repos/{gh.repo}/issues/comments/{comment_id}/reactions", {"content": "+1"}) + return len(new) def run(gh: GitHub, event_name: str, event: dict, server_url: str = "https://github.com") -> Evaluation: @@ -334,8 +372,19 @@ def run(gh: GitHub, event_name: str, event: dict, server_url: str = "https://git return Evaluation() opener = Person(pr["user"]["login"], int(pr["user"]["id"])) opener_is_bot = pr["user"].get("type") == "Bot" and opener.id in BOT_IDS + doc_url = f"{server_url}/{gh.repo}/blob/{pr['base']['repo']['default_branch']}/{DOCUMENT_PATH}" + if OVERRIDE_LABEL in {label.get("name") for label in pr.get("labels") or []}: + gh.request("POST", f"/repos/{gh.repo}/statuses/{pr['head']['sha']}", { + "state": "success", "context": STATUS_CONTEXT, "target_url": doc_url, + "description": "CLA reviewed by a maintainer"}) + return Evaluation() actors, count = pr_actors(gh, number) + for ref in superseded_numbers(pr.get("body")): + status, other, _ = gh.request("GET", f"/repos/{gh.repo}/pulls/{ref}") + if status == 200 and (other.get("user") or {}).get("type") == "User": + actors.append({"name": other["user"]["login"], "email": "", + "login": other["user"]["login"], "id": other["user"]["id"]}) store, _ = load_store(gh) signed_ids = {int(s["id"]) for s in store["signatures"]} evaluation = evaluate(opener, opener_is_bot, actors, signed_ids) @@ -345,18 +394,15 @@ def run(gh: GitHub, event_name: str, event: dict, server_url: str = "https://git new = [] for comment in gh.paginate(f"/repos/{gh.repo}/issues/{number}/comments"): user = comment.get("user") or {} - if user.get("id") in pending and is_sign_comment(comment.get("body")): - person = pending.pop(user["id"]) - new.append({"login": person.login, "id": person.id, "pull_request": number, - "comment_id": comment["id"], "signed_at": comment["created_at"], - "version": CLA_VERSION, "repo_id": pr["base"]["repo"]["id"]}) + if user.get("id") in pending and is_valid_signature(comment): + pending.pop(user["id"]) + new.append(signature(user, comment, pr["base"]["repo"]["id"], pull_request=number)) if new: save_signatures(gh, new, number) evaluation = evaluate(opener, opener_is_bot, actors, signed_ids | {s["id"] for s in new}) - doc_url = f"{server_url}/{gh.repo}/blob/{pr['base']['repo']['default_branch']}/{DOCUMENT_PATH}" if count >= MAX_COMMITS: - evaluation.unknown.append(f"more than {MAX_COMMITS} commits; ask a maintainer to review the CLA by hand") + evaluation.unknown.append(f"more than {MAX_COMMITS} commits; a maintainer can review them and apply `{OVERRIDE_LABEL}`") description = ("All contributors have signed the CLA" if evaluation.passed else f"{len(evaluation.unsigned) + len(evaluation.unknown)} contributor(s) still need to sign") gh.request("POST", f"/repos/{gh.repo}/statuses/{pr['head']['sha']}", { @@ -373,11 +419,8 @@ def main() -> int: gh = GitHub(os.environ["GITHUB_TOKEN"], os.environ["GITHUB_REPOSITORY"], os.environ.get("GITHUB_API_URL", "https://api.github.com")) if event_name == "issue_comment": - body = (event.get("comment") or {}).get("body") if not (event.get("issue") or {}).get("pull_request"): - print("CLA:", "signature recorded" if sign_on_issue(gh, event) else "not a signing comment") - return 0 - if not (is_sign_comment(body) or is_recheck(body)): + print("CLA: recorded", sign_on_issue(gh, event), "signature(s) from the issue") return 0 evaluation = run(gh, event_name, event, os.environ.get("GITHUB_SERVER_URL", "https://github.com")) print("CLA:", "passed" if evaluation.passed else f"waiting on {evaluation.unsigned} {evaluation.unknown}") diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 4b4148605..74a56c080 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -4,7 +4,8 @@ # .github/CLA-1.0.md once, by comment. .github/scripts/cla_check.py records # signatures on the `cla-signatures` branch (created on first use) and sets the # "CLA" commit status that branch protection requires. Past contributors can -# also sign on any issue a maintainer labels `cla`. +# also sign on any issue a maintainer labels `cla`; a maintainer can apply +# `cla-override` to a pull request after reviewing it by hand. # # Security: pull_request_target and issue_comment run with a write token, even # for fork PRs. This workflow therefore checks out only the base branch (the @@ -15,16 +16,13 @@ name: CLA on: pull_request_target: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened, labeled] issue_comment: types: [created] permissions: contents: read -concurrency: - group: cla-${{ github.event.pull_request.number || github.event.issue.number }} - jobs: cla: if: >- @@ -32,6 +30,10 @@ jobs: contains(github.event.comment.body, 'VoiceStudio CLA') || (github.event.issue.pull_request && contains(github.event.comment.body, 'recheck')) runs-on: ubuntu-24.04 + # Job level, so comments the `if` skips never take the queue slot. Each run + # rescans all comments, so a queued run GitHub cancels loses nothing. + concurrency: + group: cla-${{ github.event.pull_request.number || github.event.issue.number }} permissions: contents: write issues: write diff --git a/docs/maintainers/repository-settings.md b/docs/maintainers/repository-settings.md index 41789cddc..452592f9a 100644 --- a/docs/maintainers/repository-settings.md +++ b/docs/maintainers/repository-settings.md @@ -5,6 +5,23 @@ enforce them. Apply them once as a repository admin, then re-check them after changing workflow or job names. The `gh api` commands below assume an authenticated `gh` with admin access to `debpalash/VoiceStudio`. +## Rolling out the CLA check + +Do these in order, after the pull request that adds `.github/workflows/cla.yml` +is merged: + +1. Create the labels `cla` (signing issues) and `cla-override` (maintainer + review): `gh label create cla` and `gh label create cla-override`. +2. Open the public signing issue, label it `cla`, and pin it. +3. Comment `recheck` on each open pull request, so it gets a `CLA` status. +4. Then apply the `main` ruleset below. A required check that never reported + blocks the merge with "Expected — waiting for status". +5. Apply the `cla-signatures` ruleset once the first signature creates the + branch, or before; the ruleset can exist first. + +When you fold other people's pull requests into one, write `Supersedes #N` in +the description: the check then asks their authors to sign too. + ## Ruleset: `main` Requires the backend/frontend test job and the CLA check, and blocks force-push @@ -31,7 +48,9 @@ gh api --method POST repos/debpalash/VoiceStudio/rulesets --input - <<'JSON' "target": "branch", "enforcement": "active", "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, - "bypass_actors": [], + "bypass_actors": [ + { "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request" } + ], "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" }, @@ -50,9 +69,11 @@ gh api --method POST repos/debpalash/VoiceStudio/rulesets --input - <<'JSON' JSON ``` -Leave `bypass_actors` empty unless the owner chooses an emergency bypass. To add -one, use `{ "actor_id": 5, "actor_type": "RepositoryRole", "bypass_mode": "pull_request" }` -(repository admins, pull requests only). Do not enable "Require review from +The bypass lets repository admins merge a pull request whose checks are not +green, for example to land an urgent fix, but never to push to `main` directly. +For the CLA, prefer the `cla-override` label: it records that a maintainer +reviewed the pull request by hand. Remove the bypass entry for a stricter +setup. Do not enable "Require review from Code Owners" while `@debpalash` is the only code owner, because GitHub does not let authors approve their own pull requests. @@ -118,8 +139,8 @@ gh api --method PUT repos/debpalash/VoiceStudio/private-vulnerability-reporting ## Dependabot alerts UI: **Settings → Code security → Dependabot alerts → Enable**. This setting -turns on alerts only. The repository has no `.github/dependabot.yml`, so -Dependabot does not open version-update pull requests. +turns on security alerts. Version updates for GitHub Actions are configured +separately in `.github/dependabot.yml`. ```bash gh api --method PUT repos/debpalash/VoiceStudio/vulnerability-alerts diff --git a/scripts/cla_audit.py b/scripts/cla_audit.py index 4b63b882f..54cbb287d 100644 --- a/scripts/cla_audit.py +++ b/scripts/cla_audit.py @@ -44,8 +44,13 @@ SIGNATURES = f"origin/{cla.SIGNATURE_BRANCH}:{cla.SIGNATURE_PATH}" BINARY = re.compile(r"\.(lock|png|jpe?g|gif|webp|ico|icns|svg|wav|mp3|flac|ogg|woff2?|ttf|pdf|onnx|bin)$", re.I) -# Identities that do not name the person who submitted the work. -AGENT = re.compile(r"^(noreply@anthropic\.com|.*@openai\.com|codex@users\.noreply\.github\.com|test@local|you@example\.com|)$") +# Identities that do not name the person who submitted the work: the checker's +# AI-tool list plus placeholder identities, credited to whoever opened the PR. +PLACEHOLDER = re.compile(r"^(test@local|you@example\.com|)$|\.local$", re.I) + + +def is_agent(email: str) -> bool: + return bool(cla.TOOL_EMAILS.match(email) or PLACEHOLDER.search(email)) FOLDS_IN = re.compile(r"\bsupersed", re.I) NOTES = { "lookup-failed": "GitHub lookup failed; set GH_TOKEN and rerun", @@ -98,7 +103,7 @@ def commit_messages(commits: set[str]) -> dict[str, str]: def account_for(email: str, commit: str) -> tuple[str | None, int | None, str]: """GitHub account for an author email: (login, id, noreply|api|unlinked|lookup-failed).""" - if AGENT.match(email): + if is_agent(email): return None, None, "unlinked" if (match := cla.NOREPLY.match(email)) and match.group(1): return match.group(2), int(match.group(1)), "noreply" @@ -123,7 +128,7 @@ def names_to_logins() -> dict[str, tuple[str, int]]: log = git("log", "--format=%aN <%aE>%n%(trailers:key=Co-authored-by,valueonly)") for match in re.finditer(r"^(.+?) <([^>]+)>$", log, re.M): noreply = cla.NOREPLY.match(match.group(2)) - if noreply and noreply.group(1) and not AGENT.match(match.group(2).lower()): + if noreply and noreply.group(1) and not is_agent(match.group(2).lower()): names.setdefault(match.group(1).strip().lower(), (noreply.group(2), int(noreply.group(1)))) return names @@ -156,10 +161,11 @@ def main() -> None: credits: collections.Counter = collections.Counter() # (email, name, commit, path) -> lines for (email, name, commit, path), n in blamed.items(): credits[(email, name, commit, path)] += n - for co in cla.co_authors(messages.get(commit, "")): - co_email = co["email"].lower() + # Each co-author once per commit, however often a trailer repeats. + co_emails = {co["email"].lower(): co["name"] for co in cla.co_authors(messages.get(commit, ""))} + for co_email, co_name in co_emails.items(): if not cla.TOOL_EMAILS.match(co_email) and co_email != email: - credits[(co_email, co["name"], commit, path)] += n + credits[(co_email, co_name, commit, path)] += n first_commit: dict[str, str] = {} for email, _, commit, _ in credits: diff --git a/tests/test_cla_check.py b/tests/test_cla_check.py index d198e4e38..bc82cb443 100644 --- a/tests/test_cla_check.py +++ b/tests/test_cla_check.py @@ -128,8 +128,9 @@ def test_comment_renders_untrusted_identities_as_inert_code(): class FakeGitHub: repo = "debpalash/VoiceStudio" - def __init__(self, comments, actors, store=None): + def __init__(self, comments, actors, store=None, pr=None, others=None): self.comments, self.actors = comments, actors + self.pr, self.others = pr or {}, others or {} self.files = {} if store is None else {cla.SIGNATURE_PATH: store} self.branch = store is not None self.statuses, self.posted, self.patched = [], [], [] @@ -137,7 +138,7 @@ def __init__(self, comments, actors, store=None): def get(self, path): assert path.endswith("/pulls/7") return {"state": "open", "user": {"login": "alice", "id": 1001, "type": "User"}, - "head": {"sha": "abc"}, "base": {"repo": {"id": 99, "default_branch": "main"}}} + "head": {"sha": "abc"}, "base": {"repo": {"id": 99, "default_branch": "main"}}, **self.pr} def paginate(self, path, limit=None): return self.comments @@ -150,6 +151,9 @@ def graphql(self, query, variables): "pageInfo": {"hasNextPage": False, "endCursor": None}, "nodes": nodes}}}} def request(self, method, path, body=None): + if method == "GET" and "/pulls/" in path: + number = int(path.rsplit("/", 1)[1]) + return (200, {"user": self.others[number]}, {}) if number in self.others else (404, None, {}) if "/contents/" in path and method == "GET": if cla.SIGNATURE_PATH not in self.files: return 404, None, {} @@ -187,6 +191,7 @@ def test_first_signature_creates_the_store_and_turns_the_status_green(): result = cla.run(gh, "issue_comment", {"issue": {"number": 7}}) assert result.passed and gh.branch saved = gh.files[cla.SIGNATURE_PATH]["signatures"] + assert len(saved) == 1 and len(saved[0].pop("body_sha256")) == 64 assert saved == [{"login": "alice", "id": 1001, "pull_request": 7, "comment_id": 5, "signed_at": "2026-10-02T00:00:00Z", "version": cla.CLA_VERSION, "repo_id": 99}] assert gh.statuses[-1]["state"] == "success" and gh.statuses[-1]["context"] == "CLA" @@ -208,37 +213,86 @@ def test_someone_elses_sign_comment_does_not_count(): assert gh.files[cla.SIGNATURE_PATH]["signatures"] == [] -def _issue_event(labels, user_type="User", body=None): - return {"issue": {"number": 42, "labels": [{"name": l} for l in labels]}, - "comment": {"id": 9, "body": body or cla.SIGN_PHRASE, "created_at": "2026-10-02T00:00:00Z", - "user": {"login": "velixio", "id": 270455167, "type": user_type}}, - "repository": {"id": 99}} +def _comment(user_id=1001, login="alice", body=None, edited=False, user_type="User", cid=5): + created = "2026-10-02T00:00:00Z" + return {"id": cid, "user": {"id": user_id, "login": login, "type": user_type}, + "body": cla.SIGN_PHRASE if body is None else body, + "created_at": created, "updated_at": "2026-10-03T00:00:00Z" if edited else created} -def test_past_contributors_sign_on_a_labelled_issue(): - gh = FakeGitHub([], [], store=cla.empty_store()) - gh.request_log = [] +def test_edited_sign_comments_do_not_count(): + gh = FakeGitHub([_comment(edited=True)], [actor("alice", 1001)], store=cla.empty_store()) + assert not cla.run(gh, "issue_comment", {"issue": {"number": 7}}).passed + assert gh.files[cla.SIGNATURE_PATH]["signatures"] == [] + + +def test_stored_signature_records_a_hash_of_the_comment(): + gh = FakeGitHub([_comment()], [actor("alice", 1001)], store=cla.empty_store()) + cla.run(gh, "issue_comment", {"issue": {"number": 7}}) + stored = gh.files[cla.SIGNATURE_PATH]["signatures"][0] + assert len(stored["body_sha256"]) == 64 and stored["pull_request"] == 7 + + +@pytest.mark.parametrize("email", [ + "cline@openai.com", "codex@openai.com", "175728472+Copilot@users.noreply.github.com", + "cursoragent@cursor.com", "41898282+github-actions[bot]@users.noreply.github.com", +]) +def test_agent_identities_never_need_to_sign(email): + linked = actor("Copilot", 175728472, email=email) if "Copilot" in email else actor(email=email) + assert cla.evaluate(OPENER, False, [linked], {1001}).passed + + +def test_superseded_pull_request_authors_must_sign(): + assert cla.superseded_numbers("Consolidates fixes.\nSupersedes #12, #15 and #12.\nCloses #99") == [12, 15] + + +def _issue_event(labels): + return {"issue": {"number": 42, "labels": [{"name": l} for l in labels]}, "repository": {"id": 99}} + + +def _with_reactions(gh): + gh.reactions = [] original = gh.request def request(method, path, body=None): if path.endswith("/reactions"): - gh.request_log.append(body) + gh.reactions.append(path) return 201, {}, {} return original(method, path, body) gh.request = request - assert cla.sign_on_issue(gh, _issue_event(["cla"])) - assert gh.files[cla.SIGNATURE_PATH]["signatures"][0]["id"] == 270455167 - assert gh.files[cla.SIGNATURE_PATH]["signatures"][0]["issue"] == 42 - assert gh.request_log == [{"content": "+1"}] + return gh -@pytest.mark.parametrize("event", [ - _issue_event([]), # unlabelled issue - _issue_event(["cla"], user_type="Bot"), # bots cannot sign - _issue_event(["cla"], body="> " + cla.SIGN_PHRASE), -]) -def test_issue_signing_requires_the_label_a_person_and_the_phrase(event): - gh = FakeGitHub([], [], store=cla.empty_store()) - assert not cla.sign_on_issue(gh, event) +def test_past_contributors_sign_on_a_labelled_issue_and_none_are_lost(): + comments = [_comment(270455167, "velixio", cid=1), _comment(2002, "carol", cid=2), + _comment(3003, "dave", body="> " + cla.SIGN_PHRASE, cid=3), + _comment(4004, "erin", edited=True, cid=4), _comment(5005, "bot", user_type="Bot", cid=5)] + gh = _with_reactions(FakeGitHub(comments, [], store=cla.empty_store())) + assert cla.sign_on_issue(gh, _issue_event(["cla"])) == 2 + stored = gh.files[cla.SIGNATURE_PATH]["signatures"] + assert [s["id"] for s in stored] == [270455167, 2002] and stored[0]["issue"] == 42 + assert len(gh.reactions) == 2 + assert cla.sign_on_issue(gh, _issue_event(["cla"])) == 0 # idempotent + + +def test_unlabelled_issues_record_nothing(): + gh = FakeGitHub([_comment()], [], store=cla.empty_store()) + assert cla.sign_on_issue(gh, _issue_event([])) == 0 assert gh.files[cla.SIGNATURE_PATH]["signatures"] == [] + + +def test_superseding_pr_waits_for_the_original_authors(): + others = {12: {"login": "frank", "id": 6006, "type": "User"}, + 13: {"login": "dependabot[bot]", "id": 49699333, "type": "Bot"}} + gh = FakeGitHub([], [actor("alice", 1001)], store=cla.empty_store(), + pr={"body": "Supersedes #12 and #13"}, others=others) + result = cla.run(gh, "pull_request_target", {"pull_request": {"number": 7}}) + assert result.unsigned == [cla.Person("alice", 1001), cla.Person("frank", 6006)] + + +def test_maintainer_override_label_passes_without_checking(): + gh = FakeGitHub([], [actor(email="you@example.com")], store=cla.empty_store(), + pr={"labels": [{"name": cla.OVERRIDE_LABEL}]}) + assert cla.run(gh, "pull_request_target", {"pull_request": {"number": 7}}).passed + assert gh.statuses[-1]["state"] == "success" and "maintainer" in gh.statuses[-1]["description"] diff --git a/tests/test_cla_workflow.py b/tests/test_cla_workflow.py index 07c1a82be..ae30832ca 100644 --- a/tests/test_cla_workflow.py +++ b/tests/test_cla_workflow.py @@ -68,7 +68,7 @@ def test_workflow_comment_filter_admits_the_sign_phrase_and_recheck(): def test_published_agreements_are_never_edited(): for path, expected in _PUBLISHED_AGREEMENTS.items(): - text = (_REPO / path).read_text(encoding="utf-8") + text = (_REPO / path).read_text(encoding="utf-8").replace("\r\n", "\n") assert hashlib.sha256(text.encode()).hexdigest() == expected, ( f"{path} changed. Publish the new terms as a new version file instead." ) From f9429c9235ca6a133e2117452ef7c002082ef171 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:36:21 +0530 Subject: [PATCH 08/85] docs(changelog): file the hardening entries under Added and CI with the PR ref --- CHANGELOG.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eda2e1742..c625a30f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ metadata and the backend fallback mirror it. - Calls workspace with a live transcript, take-over, hang-up and an after-call summary (#2305) - Create Story from a dub: its speakers become characters, its segments become lines, and each character keeps the voice the dub assigned — no retyping the script (#2300) — thanks @shivsin25! - Linux AppImages include standard update information and a published `.zsync` file so AppImageUpdate and desktop managers can download only changed bytes (#2327) — thanks @shuvashish76! +- Pull requests ask their opener and every commit author and co-author to sign a one-time Contributor License Agreement by comment; the contributing guide explains how (#2556) ### Changed - Audio quality and Voice controls open as compact popovers from the Synthesize box on Clone and Voice Design (#2419) @@ -77,13 +78,10 @@ metadata and the backend fallback mirror it. - Footer sponsor hover details show dated GitHub reach, and its X opens the Pro page (#2302) - Linux AppImages use the static runtime so launching them no longer needs libfuse2 (#2328) — thanks @shuvashish76! - Voice Clone shows upload and record side by side instead of behind a toggle (#2307) -- Pull requests ask their opener and every commit author and co-author to sign a one-time Contributor License Agreement by comment; the contributing guide explains how -- Pull requests fail when a commit uses a leaked personal email or a placeholder identity such as an unconfigured hostname address -- GitHub Actions are pinned to exact commits and kept current by Dependabot ### Docs -- Maintainer guide for repository settings that can't live in code; the licence notice scope and the contributing guide's list of network calls match the current app -- Contact addresses are now hi@voicestudio.sh (general and licensing), partner@voicestudio.sh (partnerships) and security@voicestudio.sh (security reports) +- Maintainer guide for repository settings that can't live in code; the licence notice scope and the contributing guide's list of network calls match the current app (#2556) +- Contact addresses are now hi@voicestudio.sh (general and licensing), partner@voicestudio.sh (partnerships) and security@voicestudio.sh (security reports) (#2556) - Chinese README now matches the Electron installation and migration guide (#2377) — thanks @lg114! - Record the supplied audio comparisons and installed-engine quality validation (#2406) @@ -155,6 +153,10 @@ metadata and the backend fallback mirror it. - A reference longer than 20 s is transcribed with the speech-to-text model already installed, instead of failing when OmniVoice's own Whisper snapshot is not cached (#2301) — thanks @Cengokill! - Pronunciation dictionary entries in Japanese, Chinese and Thai apply inside a sentence, not only to a line that is the key alone (#2392) — thanks @kevin9327! +### CI +- Pull requests fail when a commit uses a leaked personal email or a placeholder identity such as an unconfigured hostname address (#2556) +- GitHub Actions are pinned to exact commits and kept current by Dependabot (#2556) + ## [0.5.6] — 2026-09-23 **VoiceStudio now talks to your other tools.** Answer Twilio phone calls in a saved voice, and connect Claude Code, Cursor, Codex CLI and the OpenAI Agents SDK to VoiceStudio with copyable setup that works, including in Docker, where MCP previously returned HTTP 405. Integration cards now say plainly which ones work with VoiceStudio and which are external links. From 14c1c00d4112828bfc355e89a9db6de472dbdea1 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 06:51:55 +0530 Subject: [PATCH 09/85] docs(license): state the CLA open-source commitment as the CLA words it --- LICENSE-NOTICE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/LICENSE-NOTICE.md b/LICENSE-NOTICE.md index 33de83324..075f22604 100644 --- a/LICENSE-NOTICE.md +++ b/LICENSE-NOTICE.md @@ -28,8 +28,9 @@ contact `hi@voicestudio.sh`. Contributors license their contributions to Yupcha Softwares Private Limited, the company that maintains VoiceStudio, under the -[Contributor License Agreement](.github/CLA-1.0.md). Contributions stay -available under the AGPL-3.0. +[Contributor License Agreement](.github/CLA-1.0.md). While a contribution is +in the public repository, it stays available there under the AGPL-3.0 or +another OSI-approved licence (CLA section 4). (This Notice is a plain-language summary; the binding terms are the full GNU AGPL-3.0 text in [`LICENSE`](LICENSE).) From f92b3b470dcb6589535b641a108002c332c77ac9 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:05:11 +0530 Subject: [PATCH 10/85] ci(security): fail PRs that credit AI agents or carry agent identities --- .github/CONTRIBUTING.md | 14 ++++-- .github/workflows/commit-identity.yml | 6 ++- AGENTS.md | 1 + CHANGELOG.md | 2 +- CLAUDE.md | 2 + docs/agents/issue-tracker.md | 5 +- scripts/check_commit_identities.py | 66 ++++++++++++++++++++++++--- tests/test_commit_identities.py | 65 ++++++++++++++++++++++++++ 8 files changed, 146 insertions(+), 15 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2cf089139..7ead51f8c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -244,7 +244,8 @@ Commit with a real identity (your GitHub noreply address works). The `commit-identity` PR check (`scripts/check_commit_identities.py`) fails when a PR commit's author, committer, or `Co-authored-by:`/`Signed-off-by:` email is a placeholder (`test@local`, `you@example.com`, `mergetest`, hostname-style -`*.local`) or on the hashed block list; fix it with +`*.local`), an AI agent, or on the hashed block list, or when a commit message +or the PR description credits an AI agent; fix it with `git rebase --exec 'git commit --amend --no-edit --reset-author' origin/main`. --- @@ -298,6 +299,12 @@ Plenty of contributions here are built with Claude Code, Cursor, and similar agents — welcome, with the same quality bar as hand-written PRs (real bug, correct fix, regression test; see the quality gates below). +You submit agent-assisted work as your own, under your own git identity. Don't +credit agents in commits or the PR description: no `Co-authored-by:` trailer +for an AI agent, no "Generated with …" line, no session links. The +`commit-identity` check fails PRs that carry them. Co-authors who are people +are welcome. + One practical tip: this codebase is large, and re-explaining it to your agent every session burns context and tokens fast. A persistent memory layer fixes that — the agent recalls the architecture, conventions, and your past findings @@ -351,8 +358,9 @@ co-author sign the [Contributor License Agreement](CLA-1.0.md) once. You keep your copyright. The agreement lets Yupcha Softwares Private Limited, the company that maintains VoiceStudio, ship your work in both the AGPL-3.0 app and commercial builds, and commits to keeping it available under an open-source -licence. AI tools listed as co-authors don't sign; the person submitting the -work does. +licence. AI agents can't be co-authors (see +[Contributing with AI agents](#contributing-with-ai-agents)); the person +submitting the work signs for it. The **CLA** check comments on your pull request when someone still needs to sign. To sign, post this as a new comment, on its own line: diff --git a/.github/workflows/commit-identity.yml b/.github/workflows/commit-identity.yml index 2ae671211..c00964eb0 100644 --- a/.github/workflows/commit-identity.yml +++ b/.github/workflows/commit-identity.yml @@ -1,6 +1,7 @@ # Commit identity gate — fails a PR when any of its commits carries a blocked # author/committer/trailer identity (hashed personal emails, placeholder and -# hostname-style tool identities). The blocked emails are stored only as +# hostname-style tool identities, AI agents) or when a commit message or the +# PR description carries AI agent attribution. The blocked emails are stored only as # SHA-256 digests in scripts/blocked_identity_hashes.txt; the job output names # the commit and the kind of match, never the address. # @@ -11,6 +12,7 @@ name: commit-identity on: pull_request: + types: [opened, synchronize, reopened, edited] permissions: contents: read @@ -33,4 +35,4 @@ jobs: - name: Check PR commit identities env: BASE_REF: ${{ github.base_ref }} - run: python3 scripts/check_commit_identities.py --range "origin/${BASE_REF}..HEAD" + run: python3 scripts/check_commit_identities.py --range "origin/${BASE_REF}..HEAD" --event "$GITHUB_EVENT_PATH" diff --git a/AGENTS.md b/AGENTS.md index da73ea6f3..0e12c0b7f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,7 @@ Binding for every AI agent (Claude, Codex, Cursor, review bots, …). CLAUDE.md - Tagged release announcements lead with the biggest user-visible change; redesigns need real UI screenshots and migrations need installer links and steps. Verify all contributor credits from the tag comparison and included PRs; list authors and bug reporters separately (see `docs/RELEASING.md`). - Versioning: root `package.json` is the single source of truth; never bump without the owner asking. - JavaScript dependency changes require regenerating root `bun.lock` (Docker runs `--frozen-lockfile`). +- Attribution: commits, PR descriptions and comments carry only the submitter's git identity. Never credit an agent (no agent `Co-authored-by:`, "Generated with …", session links) and never add other names/emails. Human co-authors are fine. Enforced by the `commit-identity` check. - Issues: absorb or decline — never defer to a future version. Check the open-PR queue before implementing community-reported fixes. ## Shared select controls diff --git a/CHANGELOG.md b/CHANGELOG.md index c625a30f2..a15681828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,7 +154,7 @@ metadata and the backend fallback mirror it. - Pronunciation dictionary entries in Japanese, Chinese and Thai apply inside a sentence, not only to a line that is the key alone (#2392) — thanks @kevin9327! ### CI -- Pull requests fail when a commit uses a leaked personal email or a placeholder identity such as an unconfigured hostname address (#2556) +- Pull requests fail when a commit uses a leaked personal email, a placeholder identity, or an AI agent identity, or when a commit or the description credits an AI agent (#2556) - GitHub Actions are pinned to exact commits and kept current by Dependabot (#2556) ## [0.5.6] — 2026-09-23 diff --git a/CLAUDE.md b/CLAUDE.md index 259e94fde..9351a4e2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,6 +67,8 @@ For anything new: prefer what's already pinned in `pyproject.toml` / `electron/p **Keep main green (hard rule, owner-set 2026-06-16):** A merge must **never break `main`'s CI**. Before a change lands, verify the *full* active CI matrix and `deploy/Dockerfile`, not only the checks you happened to run. Dependency / lockfile / config changes must be validated against all maintained consumers. The lockfile is the repo-root `bun.lock`; any workspace package change requires regenerating it and confirming `bun install --frozen-lockfile` passes. Re-check CodeQL/Security on code changes and the Electron native helper on Rust changes. Do not use the archived Tauri build as a gate. Other conventions not yet established. Will populate as patterns emerge during development. +**Attribution and identity (hard rule, owner-set 2026-10-02):** commits, PR descriptions, and issue/PR comments carry only the submitter's own git identity. AI agents are never credited: no agent `Co-authored-by:` trailer, no "Generated with …" line, no session links, no agent author/committer. Never put names or emails other than the configured git identity into commits or comments (no personal emails, no placeholder identities). Human co-authors are fine. Enforced on PRs by `scripts/check_commit_identities.py` (the `commit-identity` check). + diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md index dec0caa20..179a2b676 100644 --- a/docs/agents/issue-tracker.md +++ b/docs/agents/issue-tracker.md @@ -40,8 +40,9 @@ These come from `CLAUDE.md` and apply to anything a skill opens or merges here: - **Gate every merge** on the "Tests (backend + frontend)" check passing and the PR being `MERGEABLE`. - **Always pass an explicit `--body` to `gh pr merge --squash`.** Letting it - auto-generate injects `Co-authored-by:` trailers, which this repo forbids - (see the no-AI-attribution rule). + auto-generate copies every commit message, including any agent + `Co-authored-by:` or "Generated with" lines, which this repo forbids (see + the attribution rule in `CLAUDE.md`). - **Check the open-PR queue** before implementing any community-reported fix — a contributor may already have submitted one. diff --git a/scripts/check_commit_identities.py b/scripts/check_commit_identities.py index b41850f54..d3c37af09 100755 --- a/scripts/check_commit_identities.py +++ b/scripts/check_commit_identities.py @@ -9,12 +9,17 @@ republishes the addresses it guards; * literal placeholder/tool identities that are not personal data (``mergetest``, ``test@local``, ``you@example.com``) and hostname-style - auto-detected emails (``*.local``, ``*.localdomain``, ``*.(none)``). + auto-detected emails (``*.local``, ``*.localdomain``, ``*.(none)``); +* AI agents: an agent email in any field, an agent name in a trailer, or an + agent attribution line ("Generated with ...", session links) in the commit + message or, with ``--event``, the pull request description. Commits carry + the git identity of the person submitting them and nobody else's; human + co-authors are allowed. Findings name the commit, the field and the *kind* of match only; the matched value is never printed. Exit 0 = clean, 1 = violations, 2 = error. -Usage: check_commit_identities.py [--range A..B | --base BRANCH] [--hash-file PATH] +Usage: check_commit_identities.py [--range A..B | --base BRANCH] [--hash-file PATH] [--event PATH] Default range is ``origin/..HEAD`` with base from ``--base``, ``$GITHUB_BASE_REF``, or ``main``. """ @@ -23,6 +28,7 @@ import argparse import hashlib +import json import os import re import subprocess @@ -42,6 +48,29 @@ HOSTNAME_SUFFIXES = (".local", ".localdomain", ".(none)") HOSTNAME_KIND = "hostname-style auto-detected email" +AI_KIND = "AI agent identity" +AI_ATTRIBUTION_KIND = "AI agent attribution" +# Agents that commit or get credited as co-authors. GitHub App bots such as +# dependabot[bot] are not agents and stay allowed. +AI_EMAILS = re.compile( + r"^([^@]+@(anthropic\.com|cursor\.com|openai\.com|coderabbit\.ai|devin\.ai|cognition\.ai)" + r"|codex@users\.noreply\.github\.com" + r"|(\d+\+)?(copilot|claude|codex|cursor|devin-ai-integration|google-labs-jules|copilot-swe-agent" + r"|coderabbitai|openhands-agent|sweep-ai)(\[bot\])?@users\.noreply\.github\.com)$" +) +# Only checked on trailers: a human author may be called Claude. +AI_NAMES = re.compile( + r"^(claude( (code|opus|sonnet|haiku|fable|\d).*)?|cursor( agent)?|cursoragent|(github )?copilot" + r"|(openai )?codex|chatgpt|devin( ai)?|gemini( code assist)?|(google )?jules|aider|cline|coderabbit(ai)?)" + r"(\[bot\])?$" +) +_AGENTS = r"(claude|cursor|copilot|codex|chatgpt|openai|gemini|devin|jules|aider|cline|windsurf|an? ai\b|ai\b)" +AI_ATTRIBUTION = re.compile( + rf"^[ \t>*_-]*(generated|written|created|authored)[ \t]+(with|by|using)[ \t]+\[?{_AGENTS}" + r"|🤖[ \t]*(generated|written|created|authored)\b|claude\.(com|ai)/(claude-)?code|^[ \t]*claude-session[ \t]*:", + re.IGNORECASE | re.MULTILINE, +) + _HEX64 = re.compile(r"^[0-9a-f]{64}$") _TRAILER = re.compile(r"^[ \t]*(co-authored-by|signed-off-by)[ \t]*:(.*)$", re.IGNORECASE | re.MULTILINE) _FIELD_SEP = "\x1f" @@ -81,6 +110,8 @@ def classify_email(email: str, hashes: set[str]) -> list[str]: kinds.append(BLOCKED_LOCALPARTS[local]) if "@" in value and value.endswith(HOSTNAME_SUFFIXES): kinds.append(HOSTNAME_KIND) + if AI_EMAILS.match(value): + kinds.append(AI_KIND) return kinds @@ -89,16 +120,30 @@ def classify_name(name: str) -> list[str]: return [kind] if kind else [] -def trailer_emails(body: str) -> list[tuple[str, str]]: +def classify_trailer_name(name: str) -> list[str]: + return [AI_KIND] if AI_NAMES.match(" ".join(name.lower().split())) else [] + + +def classify_text(text: str) -> list[str]: + return [AI_ATTRIBUTION_KIND] if AI_ATTRIBUTION.search(text or "") else [] + + +def trailers(body: str) -> list[tuple[str, str, str]]: + """(key, name, email) for each Co-authored-by / Signed-off-by trailer.""" found = [] for key, value in _TRAILER.findall(body): match = re.search(r"<([^>]*)>", value) email = match.group(1) if match else value - if "@" in email: - found.append((key.lower(), email)) + name = value[: match.start()] if match else "" + found.append((key.lower(), name.strip(), email if "@" in email else "")) return found +def pr_body(event_path: Path) -> str: + event = json.loads(event_path.read_text(encoding="utf-8")) + return ((event.get("pull_request") or {}).get("body")) or "" + + def read_commits(rev_range: str) -> list[dict[str, str]]: fmt = _FIELD_SEP.join(["%H", "%an", "%ae", "%cn", "%ce", "%B"]) proc = subprocess.run( @@ -131,9 +176,11 @@ def find_violations(commits: list[dict[str, str]], hashes: set[str]) -> list[tup ("committer name", classify_name(c["cn"])), ("committer email", classify_email(c["ce"], hashes)), ] - for key, email in trailer_emails(c["body"]): + for key, name, email in trailers(c["body"]): label = "Co-authored-by" if key == "co-authored-by" else "Signed-off-by" + checks.append((f"{label} trailer name", classify_trailer_name(name))) checks.append((f"{label} trailer email", classify_email(email, hashes))) + checks.append(("message", classify_text(c["body"]))) for field, kinds in checks: violations.extend((short, field, kind) for kind in kinds) return violations @@ -144,17 +191,20 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--range", dest="rev_range", help="git revision range, e.g. origin/main..HEAD") parser.add_argument("--base", default=os.environ.get("GITHUB_BASE_REF") or "main", help="base branch for the default range") parser.add_argument("--hash-file", type=Path, default=DEFAULT_HASH_FILE, help="blocked email digest list") + parser.add_argument("--event", type=Path, help="GitHub event JSON; also checks the pull request description") args = parser.parse_args(argv) rev_range = args.rev_range or f"origin/{args.base}..HEAD" try: hashes = load_hashes(args.hash_file) commits = read_commits(rev_range) + description = pr_body(args.event) if args.event else "" except (OSError, ValueError, RuntimeError) as exc: print(f"commit-identity: error: {exc}", file=sys.stderr) return 2 violations = find_violations(commits, hashes) + violations += [("PR", "description", kind) for kind in classify_text(description)] if not violations: print(f"commit-identity: {len(commits)} commit(s) in {rev_range} clean.") return 0 @@ -164,7 +214,9 @@ def main(argv: list[str] | None = None) -> int: print( "Set an allowed identity (e.g. your GitHub noreply address), then rewrite and force-push:\n" " git rebase --exec 'git commit --amend --no-edit --reset-author' origin/\n" - "Remove blocked Co-authored-by/Signed-off-by trailers with `git rebase -i` (reword)." + "Remove blocked Co-authored-by/Signed-off-by trailers and AI agent attribution lines\n" + "with `git rebase -i` (reword) and from the pull request description. Commits carry\n" + "only your own git identity: no AI agent co-authors or 'Generated with ...' lines." ) return 1 diff --git a/tests/test_commit_identities.py b/tests/test_commit_identities.py index 0827bcbe3..d1c046ca0 100644 --- a/tests/test_commit_identities.py +++ b/tests/test_commit_identities.py @@ -152,6 +152,7 @@ def test_shipped_hash_list_is_digests_only(): def test_workflow_runs_on_untrusted_safe_pull_request(): wf = yaml.load(WORKFLOW.read_text(encoding="utf-8"), Loader=yaml.BaseLoader) assert set(wf["on"]) == {"pull_request"} + assert "edited" in wf["on"]["pull_request"]["types"] # description edits are rechecked assert wf["permissions"] == {"contents": "read"} (job,) = wf["jobs"].values() checkout, run = job["steps"] @@ -160,3 +161,67 @@ def test_workflow_runs_on_untrusted_safe_pull_request(): assert "${{" not in run["run"] assert run["env"]["BASE_REF"] == "${{ github.base_ref }}" assert "scripts/check_commit_identities.py" in run["run"] and "origin/${BASE_REF}..HEAD" in run["run"] + assert '--event "$GITHUB_EVENT_PATH"' in run["run"] + + +# ── AI agents ─────────────────────────────────────────────────────────── +# Built at runtime so this file never contains a literal agent address. +AGENT_EMAIL = "noreply" + "@" + "anthropic.com" +HUMAN = "Dana <4242+dana@users.noreply.github.com>" + + +@pytest.mark.parametrize( + "message,expected", + [ + (f"feat: x\n\nCo-authored-by: Claude <{AGENT_EMAIL}>", "Co-authored-by trailer email: AI agent identity"), + ("feat: x\n\nCo-authored-by: Claude Opus 5.5 ", "Co-authored-by trailer name: AI agent identity"), + ("feat: x\n\nCo-authored-by: Cursor Agent ", "Co-authored-by trailer name: AI agent identity"), + ("feat: x\n\nCo-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>", "AI agent identity"), + ("feat: x\n\n\U0001f916 Generated with [Some Agent](https://example.com)", "message: AI agent attribution"), + ("feat: x\n\nGenerated with Codex", "message: AI agent attribution"), + ("feat: x\n\nClaude-Session: abc123", "message: AI agent attribution"), + ], +) +def test_ai_agent_attribution_in_commits_is_blocked(repo, message, expected): + _, commit, check = repo + sha = commit(message) + result = check(*_only(sha)) + assert result.returncode == 1 and expected in result.stdout + + +def test_ai_agent_as_author_is_blocked(repo): + _, commit, check = repo + sha = commit("feat: x", author=("Agent", AGENT_EMAIL)) + result = check(*_only(sha)) + assert result.returncode == 1 and "author email: AI agent identity" in result.stdout + + +@pytest.mark.parametrize( + "message", + [ + f"feat: pair work\n\nCo-authored-by: {HUMAN}", + "feat: x\n\nCo-authored-by: Claude Monet ", # a person called Claude + "fix: audio generated with the default model sounded clipped", + ], +) +def test_people_and_ordinary_prose_pass(repo, message): + _, commit, check = repo + sha = commit(message) + assert check(*_only(sha)).returncode == 0 + + +def test_bot_authors_that_are_not_agents_pass(repo): + _, commit, check = repo + sha = commit("chore(deps): bump", author=("dependabot[bot]", "49699333+dependabot[bot]@users.noreply.github.com")) + assert check(*_only(sha)).returncode == 0 + + +def test_pull_request_description_attribution_is_blocked(repo, tmp_path): + _, commit, check = repo + sha = commit("feat: clean") + event = tmp_path / "event.json" + event.write_text('{"pull_request": {"body": "Adds X.\\n\\nGenerated with Claude"}}') + result = check(*_only(sha), "--event", str(event)) + assert result.returncode == 1 and "PR description: AI agent attribution" in result.stdout + event.write_text('{"pull_request": {"body": null}}') + assert check(*_only(sha), "--event", str(event)).returncode == 0 From 0bd9becea6e795ddab926703751404db08af07d2 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:06:06 +0530 Subject: [PATCH 11/85] ci(docker): publish release images only when the GitHub Release is published Tag pushes no longer publish :X.Y.Z, :X.Y, :stable or an implicit :latest. Release images build on release: published; :latest stays main-only in both the CUDA and ROCm jobs. electron-release dispatches the docker backfill path after a GITHUB_TOKEN publish, which cannot fire a release event. --- .github/workflows/docker.yml | 57 +++++++++++-------- .github/workflows/electron-release.yml | 9 +++ tests/test_docker_workflow.py | 78 ++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 tests/test_docker_workflow.py diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c89508681..309d092ee 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,18 +1,23 @@ # Publish Docker images to GitHub Container Registry (GHCR). # # Triggers: -# - push of a tag matching `v*` (e.g. `v0.3.0`) → :0.3.0, :0.3, :latest, :sha- -# - push to main branch → :main, :sha- (rolling "edge" build) -# - workflow_dispatch → :sha- only (ad-hoc test build) +# - GitHub Release published (`v*` tag, e.g. `v0.3.0`) → :0.3.0, :0.3, :stable, :sha- +# (never :latest; a tag push alone publishes nothing, so release images wait +# until the reviewed Electron draft release is published) +# - push to main branch → :latest, :main, :sha- (rolling preview) +# - workflow_dispatch → :sha- only (ad-hoc test build), or +# with backfill_release=true the release tags for an existing `vX.Y.Z` tag. +# Releases published by electron-release.yml (GITHUB_TOKEN, which cannot +# fire `release` events) dispatch that backfill path automatically. # # Tag ↔ image mapping (versioning hard rule, owner-set 2026-06-11: # :latest IS the preview channel; stable users pin :stable or a version tag) -# :latest — rolling preview: latest commit on main (always last release + 1 dev) +# :latest — rolling preview: latest commit on main (never a release build) # :main — alias of the same rolling main build (kept for back-compat) -# :stable — most recent versioned release (set on every v* tag push) +# :stable — most recent versioned release (set when a non-prerelease is published) # :0.3.6 — exact version from the git tag # :0.3 — major.minor floating tag (updated on every patch within the minor) -# :sha-xxxx — specific commit SHA; produced by workflow_dispatch +# :sha-xxxx — specific commit SHA; produced by every build # # ROCm/AMD GPU variant (#1165) — same semantics, `-rocm` suffixed, built by the # build-and-push-rocm job from the same Dockerfile via the BASE_IMAGE build-arg: @@ -31,7 +36,7 @@ # deploy/dockerhub-overview.md (source of truth for the hub.docker.com page). # # NOTE: the Docker image is the headless web-server build of VoiceStudio (FastAPI -# backend + pre-built React frontend served over HTTP). The Tauri desktop +# backend + pre-built React frontend served over HTTP). The desktop # auto-updater and its update-channel toggle are desktop-only features; they do # NOT apply to the Docker image. @@ -39,8 +44,9 @@ name: Docker (GHCR) on: push: - tags: ['v*'] branches: [main] + release: + types: [published] workflow_dispatch: inputs: backfill_release: @@ -72,6 +78,8 @@ env: jobs: build-and-push: + # Only version releases publish images; other release tags are ignored. + if: github.event_name != 'release' || startsWith(github.event.release.tag_name, 'v') runs-on: ubuntu-22.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -140,14 +148,15 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} # Tag strategy (`:sha-` is emitted on every trigger): - # v0.3.6 tag push → :0.3.6, :0.3, :stable, :sha- - # main branch push → :latest, :main, :sha- - # workflow_dispatch → :sha- only + # v0.3.6 release published → :0.3.6, :0.3, :stable, :sha- + # main branch push → :latest, :main, :sha- + # workflow_dispatch → :sha- only (backfill retags below) # - # All mutable-tag rules stay gated on `github.event_name == 'push'` so a - # manual workflow_dispatch can only ever produce a throwaway `:sha-` tag - # (the stale-:latest fix from #249/#251). :stable excludes prerelease - # tags (those contain a `-`) so a prerelease can't clobber it. + # Every mutable-tag rule is gated on its event, so a manual + # workflow_dispatch can only ever produce a throwaway `:sha-` tag (the + # stale-:latest fix from #249/#251). latest=false stops metadata-action's + # `auto` default from adding :latest to release builds; :latest is the + # main preview channel only. :stable excludes prereleases. - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 @@ -159,10 +168,12 @@ jobs: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} ${{ env.REGISTRY }}/${{ env.LEGACY_IMAGE_NAME }} ${{ steps.dockerhub.outputs.enabled == 'true' && env.DOCKERHUB_IMAGE || '' }} + flavor: | + latest=false tags: | - type=semver,pattern={{version}},enable=${{ github.event_name == 'push' }} - type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'push' }} - type=raw,value=stable,enable=${{ github.event_name == 'push' && github.ref_type == 'tag' && !contains(github.ref, '-') }} + type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }} + type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'release' }} + type=raw,value=stable,enable=${{ github.event_name == 'release' && !github.event.release.prerelease && !contains(github.ref, '-') }} type=raw,value=latest,enable=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} type=raw,value=main,enable=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} type=raw,value=sha-${{ steps.source.outputs.short_sha }} @@ -238,6 +249,8 @@ jobs: # build step — so it gets a full runner disk to itself: the ROCm base alone # is ~10 GB compressed / ~25 GB unpacked. build-and-push-rocm: + # Only version releases publish images; other release tags are ignored. + if: github.event_name != 'release' || startsWith(github.event.release.tag_name, 'v') runs-on: ubuntu-22.04 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 @@ -311,14 +324,14 @@ jobs: ${{ env.REGISTRY }}/${{ env.LEGACY_IMAGE_NAME }} ${{ steps.dockerhub.outputs.enabled == 'true' && env.DOCKERHUB_IMAGE || '' }} # latest=false is load-bearing: metadata-action's default (`auto`) - # would add a bare un-suffixed `:latest` on release-tag pushes, + # would add a bare un-suffixed `:latest` on release builds, # clobbering the CUDA preview channel with a ROCm image. flavor: | latest=false tags: | - type=semver,pattern={{version}},suffix=-rocm,enable=${{ github.event_name == 'push' }} - type=semver,pattern={{major}}.{{minor}},suffix=-rocm,enable=${{ github.event_name == 'push' }} - type=raw,value=stable-rocm,enable=${{ github.event_name == 'push' && github.ref_type == 'tag' && !contains(github.ref, '-') }} + type=semver,pattern={{version}},suffix=-rocm,enable=${{ github.event_name == 'release' }} + type=semver,pattern={{major}}.{{minor}},suffix=-rocm,enable=${{ github.event_name == 'release' }} + type=raw,value=stable-rocm,enable=${{ github.event_name == 'release' && !github.event.release.prerelease && !contains(github.ref, '-') }} type=raw,value=rocm,enable=${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} type=raw,value=sha-${{ steps.source.outputs.short_sha }}-rocm diff --git a/.github/workflows/electron-release.yml b/.github/workflows/electron-release.yml index e4361943a..de674fee0 100644 --- a/.github/workflows/electron-release.yml +++ b/.github/workflows/electron-release.yml @@ -222,6 +222,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + # Dispatch docker.yml after publishing: GITHUB_TOKEN releases fire no `release` event. + actions: write env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ inputs.release_tag || github.ref_name }} @@ -268,3 +270,10 @@ jobs: - name: Publish only when explicitly requested if: github.event_name == 'workflow_dispatch' && inputs.publish == true run: gh release edit "$TAG" --draft=false --latest + # docker.yml publishes release images on `release: published`, which a + # GITHUB_TOKEN publish cannot trigger; dispatch its release backfill path. + - name: Publish release container images + if: github.event_name == 'workflow_dispatch' && inputs.publish == true + run: | + gh workflow run docker.yml --ref main \ + -f backfill_release=true -f release_ref="$TAG" -f promote_stable=true diff --git a/tests/test_docker_workflow.py b/tests/test_docker_workflow.py new file mode 100644 index 000000000..a274ef09e --- /dev/null +++ b/tests/test_docker_workflow.py @@ -0,0 +1,78 @@ +"""Release images publish only from a published GitHub Release; :latest is main-only. + +A `v*` tag push used to publish :X.Y.Z / :X.Y / :stable (and, through +metadata-action's `latest=auto` default, :latest) before the Electron draft +release was reviewed. These guards keep the release path behind publication +and keep :latest on the rolling main build in both GPU flavors. +""" +from __future__ import annotations + +from pathlib import Path + +import yaml + +_ROOT = Path(__file__).resolve().parents[1] +_DOCKER = _ROOT / ".github" / "workflows" / "docker.yml" +_RELEASE = _ROOT / ".github" / "workflows" / "electron-release.yml" +_JOBS = ("build-and-push", "build-and-push-rocm") +_MAIN_ONLY = "github.event_name == 'push' && github.ref == 'refs/heads/main'" + + +def _load(path: Path) -> dict: + data = yaml.safe_load(path.read_text(encoding="utf-8")) + data["on"] = data.pop(True, data.get("on")) + return data + + +def _meta_step(job: str) -> dict: + steps = _load(_DOCKER)["jobs"][job]["steps"] + return next(s for s in steps if s.get("uses", "").startswith("docker/metadata-action@")) + + +def _tag_lines(job: str) -> list[str]: + return [line.strip() for line in _meta_step(job)["with"]["tags"].splitlines() if line.strip()] + + +def test_release_images_wait_for_a_published_release(): + on = _load(_DOCKER)["on"] + assert on["release"]["types"] == ["published"] + assert "tags" not in (on.get("push") or {}), "a tag push must not publish release images" + assert on["push"]["branches"] == ["main"] + + +def test_metadata_never_adds_latest_implicitly(): + for job in _JOBS: + flavor = _meta_step(job)["with"].get("flavor", "") + assert "latest=false" in flavor.split(), f"{job} must set flavor latest=false" + + +def test_rolling_tags_come_only_from_main_pushes(): + rolling = {"build-and-push": ("latest", "main"), "build-and-push-rocm": ("rocm",)} + for job, names in rolling.items(): + lines = _tag_lines(job) + for name in names: + matches = [line for line in lines if f"value={name}," in line] + assert len(matches) == 1, f"{job} must emit :{name} exactly once" + assert _MAIN_ONLY in matches[0], f"{job} :{name} must be main-push only" + assert not any("value=latest" in line and _MAIN_ONLY not in line for line in lines) + + +def test_release_tags_come_only_from_release_events(): + for job in _JOBS: + release_lines = [line for line in _tag_lines(job) if "type=semver" in line or "value=stable" in line] + assert len(release_lines) == 3, job + for line in release_lines: + assert "github.event_name == 'release'" in line, line + assert "github.event_name == 'push'" not in line, line + stable = next(line for line in release_lines if "value=stable" in line) + assert "!github.event.release.prerelease" in stable + + +def test_token_published_releases_still_publish_images(): + """A GITHUB_TOKEN publish fires no `release` event, so it must dispatch docker.yml.""" + job = _load(_RELEASE)["jobs"]["release"] + assert job["permissions"].get("actions") == "write" + runs = "\n".join(step.get("run", "") for step in job["steps"]) + assert "gh workflow run docker.yml" in runs + assert "backfill_release=true" in runs + assert "promote_stable=true" in runs From 5be86db46f002a9800d02ecca0fde016afd3a0f2 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:09:17 +0530 Subject: [PATCH 12/85] fix(review): point review bots at live Electron paths and guard them CodeRabbit and Greptile still named the removed frontend/ tree, Tauri version mirrors, a telemetry-free outbound list and major-tag action pins. Rules now follow the root package.json version source, both Electron locale catalogs, SHA-pinned actions and the sanctioned network list. tests/test_bot_configs.py fails on missing paths, dead review globs and any frontend/ or src-tauri mention. CONTRIBUTING now matches the diagram-free CodeRabbit summary. --- .coderabbit.yaml | 75 ++++++++++-------- .github/CONTRIBUTING.md | 6 +- greptile.json | 6 +- tests/test_bot_configs.py | 158 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 206 insertions(+), 39 deletions(-) create mode 100644 tests/test_bot_configs.py diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 5b80adce4..0a73ba0f7 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -44,7 +44,6 @@ reviews: - "!**/uv.lock" - "!**/dist/**" - "!**/build/**" - - "!frontend/src-tauri/target/**" - "!**/*.min.js" - "!**/*.svg" - "!**/*.png" @@ -57,11 +56,13 @@ reviews: path_instructions: - path: "**/*.{py,rs,js,jsx,ts,tsx}" instructions: >- - OmniVoice is fully-local (no cloud, no API keys, no telemetry). Flag any - new outbound network call that is not to github.com issues, a HuggingFace - model download, or an explicitly opt-in endpoint. Flag any code that - persists or logs values matching *TOKEN*/*KEY*/*SECRET* or absolute user - home paths (/Users//, C:\\Users\\\\). + VoiceStudio is local-first: no required cloud calls, accounts or API + keys. Flag any new outbound network call that is not on the sanctioned + list in CLAUDE.md (Local-first guarantee) and .github/CONTRIBUTING.md + (Quality gates); analytics stays behind the first-run consent prompt. + Flag any code that persists or logs values matching + *TOKEN*/*KEY*/*SECRET* or absolute user home paths (/Users//, + C:\\Users\\\\). - path: "backend/services/**/*.py" instructions: >- Review as an ML-inference/audio engineer. Check: thread-safety of model @@ -81,28 +82,30 @@ reviews: every query/path/form param as hostile (path traversal, log injection, CSRF from a browser tab), and never route user-chosen filesystem destinations through HTTP — that authorization belongs in Electron main. - - path: "frontend/src/**/*.{js,jsx,ts,tsx}" + - path: "electron/src/{shared,renderer}/**/*.{js,jsx,ts,tsx}" instructions: >- Review as a product-minded senior frontend engineer. Check: stale state and races (async results landing after unmount or after newer requests); every user-visible failure has an actionable, non-technical error message; loading/disabled states during long operations. Every new user-facing string must be an i18n t('...') key present in ALL 21 - frontend/src/i18n/locales/*.json files — flag hardcoded UI strings and - keys missing from any locale. + locale files of the catalog the code imports from — + electron/src/shared/i18n/locales/*.json (tests/test_locale_parity.py) + or electron/src/renderer/src/i18n/locales/*.json (electron + locale:check) — flag hardcoded UI strings and keys missing from any + locale. - path: "electron/**/*.{ts,tsx}" instructions: >- Electron is the only maintained desktop; Tauri is sunset and archived. Review main/preload IPC authorization, sender validation, narrow bridges, native file dialogs, runtime supervision and renderer lifecycle. Never - require a Tauri UI backport. User-facing strings belong in all 21 Electron - locale bundles. Validate user-visible behavior on macOS, Windows and Linux. + require a Tauri UI backport. User-facing strings belong in all 21 files + of the matching locale catalog. Validate user-visible behavior on macOS, Windows and Linux. - path: "native/**/*.rs" instructions: >- - Review shared Electron native helpers for input validation, process and - filesystem scope, thread affinity and cross-platform behavior. Archived - Tauri modules imported by these helpers remain shared code; do not require - unrelated Tauri shell work. + Review the Electron native helper (native/desktop-bridge) for input + validation, process and filesystem scope, thread affinity and + cross-platform behavior. Never require work on the removed Tauri shell. - path: "tests/**/*.py" instructions: >- Review as a test-infrastructure engineer. Check: the test would fail @@ -114,8 +117,9 @@ reviews: a justification. - path: ".github/workflows/**" instructions: >- - Pin actions to a major version tag at minimum. Flag any workflow that - grants write permissions it does not need. + Every remote action must be pinned to a full 40-character commit SHA + with a trailing "# vX.Y.Z" comment (tests/test_actions_pinned.py). + Flag any workflow that grants write permissions it does not need. - path: "CHANGELOG.md" instructions: >- Hard rule (owner-restyled 2026-07-17): the Unreleased section is a @@ -124,16 +128,17 @@ reviews: the (#NNN) ref and, for community contributions, a "— thanks @user!" credit. Flag multi-line or bold-lead paragraph entries, missing refs, and missing credits. - - path: "frontend/package.json" + - path: "**/package.json" instructions: >- - This is a bun workspace monorepo: any dependency change here requires - regenerating the repo-root bun.lock in the same PR — - deploy/Dockerfile runs `bun install --frozen-lockfile`, so CI-green - does not imply Docker-green. Flag package.json dependency changes - without a matching root bun.lock diff. Also: this file is the single - source of truth for the app version — flag any version change not - mirrored in pyproject.toml, frontend/src-tauri/Cargo.toml and - backend/core/version.py in lockstep. + This is a bun workspace monorepo: any dependency change in a workspace + package.json requires regenerating the repo-root bun.lock in the same + PR — deploy/Dockerfile runs `bun install --frozen-lockfile`, so + CI-green does not imply Docker-green. Flag package.json dependency + changes without a matching root bun.lock diff. The root package.json + is the single source of truth for the app version — flag any version + change not mirrored in pyproject.toml and backend/core/version.py + (_FALLBACK_VERSION) in lockstep (tests/test_app_version.py), and any + version bump the owner did not request. # Non-gating pre-merge audits of the project's hard rules (warning mode — # the human owner is the gate, these make the checklist visible per-PR). @@ -157,17 +162,21 @@ reviews: - name: "i18n completeness (21 locales)" mode: warning instructions: >- - For every new or changed t('...') key in frontend code, verify the - key exists in all 21 files under frontend/src/i18n/locales/. List any - locale files missing the key. Also flag hardcoded user-facing strings + For every new or changed t('...') key in Electron UI code, verify the + key exists in all 21 files of the catalog it uses: + electron/src/shared/i18n/locales/ or + electron/src/renderer/src/i18n/locales/. List any locale files + missing the key. Also flag hardcoded user-facing strings that bypass i18n entirely. - name: "Local-first guarantee" mode: warning instructions: >- - Verify the PR adds no required cloud calls, accounts, API keys or - telemetry. Outbound traffic is only allowed to GitHub Issues (opt-in - bug reporting) and HuggingFace model downloads. The app must remain - fully functional offline and with reporting disabled. + Verify the PR adds no required cloud calls, accounts or API keys, + and no outbound call outside the sanctioned list in CLAUDE.md + (Local-first guarantee) and .github/CONTRIBUTING.md (Quality gates). + Product analytics must stay behind the first-run consent prompt. The + app must remain fully functional offline with every prompt + declined. - name: "Backward compatibility" mode: warning instructions: >- diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2cf089139..5c1574ce3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -278,9 +278,9 @@ cargo check --manifest-path native/desktop-bridge/Cargo.toml Every PR is reviewed by two AI reviewers before a human looks at it: -- **CodeRabbit** posts a walkthrough (with a sequence diagram, and an ASCII - before/after sketch for UI changes), inline findings, and warning-mode - pre-merge checks against the project's hard rules. +- **CodeRabbit** posts a short collapsed summary (no diagrams), inline + findings, and warning-mode pre-merge checks against the project's hard + rules (configured in `.coderabbit.yaml`). - **Greptile** reviews with the same project rubrics and learns from 👍/👎 reactions on its comments — react to train it. diff --git a/greptile.json b/greptile.json index 2c441480b..5371155e5 100644 --- a/greptile.json +++ b/greptile.json @@ -7,7 +7,7 @@ "files": ["CLAUDE.md"], "rules": [ "Default features must behave identically on macOS, Windows and Linux; a platform-divergent default is a P0 — platform-only behavior must sit behind explicit opt-in (Settings toggle, env var, or CLI flag).", - "Every new user-facing string must be an i18n t('...') key present in all 21 locale bundles for the affected maintained UI (electron/src/renderer/src/i18n/locales for Electron; frontend/src/i18n/locales for shared web UI); hardcoded CJK outside the allowlist in tests/test_no_hardcoded_cjk.py fails CI.", + "Every new user-facing string must be an i18n t('...') key present in all 21 files of the locale catalog the code imports from (electron/src/shared/i18n/locales, checked by tests/test_locale_parity.py, or electron/src/renderer/src/i18n/locales, checked by the electron locale:check script); hardcoded CJK outside the allowlist in tests/test_no_hardcoded_cjk.py fails CI.", "Existing omnivoice_data/ and already-installed engine model state must keep working without manual migration; DB schema changes go through alembic with an upgrade path.", "Never propose version bumps, release candidates, or deferrals to a future version — everything ships on the current line.", "The backend serves loopback HTTP: treat every query/path/form parameter as hostile (path traversal, log injection, CSRF from a browser tab); user-chosen filesystem destinations are authorized in Electron main, never via HTTP params.", @@ -16,13 +16,13 @@ "CHANGELOG.md Unreleased entries (owner-restyled 2026-07-17) are single-line bullets ending with the (#NNN) ref — plus a '— thanks @user!' credit for community PRs — under a short **Highlights** list; flag multi-line or bold-lead paragraph entries.", "No silent multi-GB downloads: any Hugging Face fetch in backend/services or backend/engines must be preceded by an installed-ness check (is_cached/cache_is_complete) or an explicit user install action; tests run under HF_HUB_OFFLINE=1.", "All synthetic-audio producers must route through the mark_synthetic watermark chokepoint — no audio route may ship unmarked.", - "A frontend/package.json dependency change requires regenerating the repo-root bun.lock in the same PR (deploy/Dockerfile runs bun install --frozen-lockfile)." + "Any workspace package.json dependency change requires regenerating the repo-root bun.lock in the same PR (deploy/Dockerfile runs bun install --frozen-lockfile). The root package.json is the single app-version source; pyproject.toml and backend/core/version.py mirror it in lockstep (tests/test_app_version.py)." ] }, "triggerOnUpdates": true, "triggerOnDrafts": false, "statusCheck": true, - "ignorePatterns": "bun.lock\nuv.lock\nfrontend/src-tauri/target/**\ntests/fixtures/**\n*.svg\n*.png\n*.wav\n*.onnx", + "ignorePatterns": "bun.lock\nuv.lock\ntests/fixtures/**\n*.svg\n*.png\n*.wav\n*.onnx", "sequenceDiagramSection": { "included": false }, diff --git a/tests/test_bot_configs.py b/tests/test_bot_configs.py new file mode 100644 index 000000000..ec7297bd9 --- /dev/null +++ b/tests/test_bot_configs.py @@ -0,0 +1,158 @@ +"""Review-bot configs must describe the repository that exists today. + +CodeRabbit (``.coderabbit.yaml``) and Greptile (``greptile.json``) review every +PR with instructions that name repo paths. When the Tauri shell and the +``frontend/`` UI were removed, those instructions kept pointing reviewers at +``frontend/src/i18n/locales`` and ``frontend/src-tauri`` mirrors, so the bots +enforced rules against files that no longer exist. This test fails when either +config references a missing repo path, a path-scoped review rule matches no +tracked file, or the removed ``frontend/`` / ``src-tauri`` trees reappear. + +Extraction is deliberately conservative: only tokens that start with a +top-level tracked directory (``backend/...``) or bare file names with a config +extension (``CLAUDE.md``) are checked; glob tokens are checked up to their +first wildcard. +""" + +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +import pytest +import yaml + +ROOT = Path(__file__).resolve().parents[1] +CONFIGS = (".coderabbit.yaml", "greptile.json") +_GLOB_CHARS = "*?{[" +_BANNED = ("src-tauri", "frontend/") + + +def _tracked() -> list[str]: + try: + out = subprocess.run( + ["git", "ls-files"], cwd=ROOT, capture_output=True, text=True, check=True + ).stdout + except (OSError, subprocess.CalledProcessError): + pytest.skip("git ls-files unavailable") + return [line for line in out.splitlines() if line] + + +def _glob_regex(pattern: str) -> re.Pattern[str]: + out, i = "", 0 + while i < len(pattern): + if pattern.startswith("**/", i): + out, i = out + "(?:.*/)?", i + 3 + elif pattern.startswith("**", i): + out, i = out + ".*", i + 2 + elif pattern[i] == "*": + out, i = out + "[^/]*", i + 1 + elif pattern[i] == "?": + out, i = out + "[^/]", i + 1 + elif pattern[i] == "{": + end = pattern.index("}", i) + options = pattern[i + 1 : end].split(",") + out, i = out + "(?:" + "|".join(map(re.escape, options)) + ")", end + 1 + else: + out, i = out + re.escape(pattern[i]), i + 1 + return re.compile(out + r"\Z") + + +def _prefix_exists(token: str) -> bool: + """True when the non-glob part of ``token`` names an existing path.""" + cut = min((token.find(c) for c in _GLOB_CHARS if c in token), default=-1) + if cut == -1: + return (ROOT / token).exists() + prefix = token[:cut] + if "/" not in prefix: + return True # glob starts at the repo root, e.g. "**/*.lock" + return (ROOT / prefix[: prefix.rindex("/")]).is_dir() + + +def _path_tokens(text: str, top_dirs: set[str], basenames: set[str]) -> list[str]: + dirs = "|".join(sorted(map(re.escape, top_dirs), key=len, reverse=True)) + found = re.findall(rf"(? list[str]: + if isinstance(value, str): + return [value] + if isinstance(value, dict): + return [s for item in value.values() for s in _strings(item)] + if isinstance(value, list): + return [s for item in value for s in _strings(item)] + return [] + + +def _config_text() -> dict[str, str]: + """Raw YAML (comments count) and decoded JSON strings (no ``\\n`` escapes).""" + texts = {} + for name in CONFIGS: + raw = (ROOT / name).read_text(encoding="utf-8") + texts[name] = "\n".join(_strings(json.loads(raw))) if name.endswith(".json") else raw + return texts + + +def test_bot_configs_do_not_mention_removed_ui_trees(): + hits = [f"{name}: {banned}" for name, text in _config_text().items() for banned in _BANNED if banned in text] + assert not hits, ( + f"Bot configs mention removed paths {hits}; Electron (electron/) is the only UI " + "and the Tauri shell is gone — point the rule at the live path." + ) + + +def test_bot_config_path_references_exist(): + tracked = _tracked() + top_dirs = {path.split("/", 1)[0] for path in tracked if "/" in path} + basenames = {path.rsplit("/", 1)[-1] for path in tracked} + missing = [] + for name, text in _config_text().items(): + for token in _path_tokens(text, top_dirs, basenames): + if "/" in token and not _prefix_exists(token): + missing.append(f"{name}: {token}") + elif "/" not in token: + missing.append(f"{name}: {token} (no tracked file has this name)") + assert not missing, "Review-bot configs reference missing paths:\n" + "\n".join(missing) + + +def test_bot_config_globs_match_tracked_files(): + tracked = _tracked() + coderabbit = yaml.safe_load((ROOT / ".coderabbit.yaml").read_text(encoding="utf-8")) + greptile = json.loads((ROOT / "greptile.json").read_text(encoding="utf-8")) + reviews = coderabbit["reviews"] + scoped = [entry["path"] for entry in reviews["path_instructions"]] + scoped += coderabbit["knowledge_base"]["code_guidelines"]["filePatterns"] + scoped += greptile["customContext"]["files"] + dead = [glob for glob in scoped if not any(_glob_regex(glob).match(p) for p in tracked)] + assert not dead, f"Review rules scoped to globs that match no tracked file: {dead}" + # Ignore lists may legitimately match nothing yet; only their roots must exist. + ignores = [p.lstrip("!") for p in reviews["path_filters"]] + ignores += greptile["ignorePatterns"].split("\n") + gone = [p for p in ignores if not _prefix_exists(p)] + assert not gone, f"Review-bot ignore patterns under missing directories: {gone}" + + +def test_extractor_flags_stale_paths(): + """Guard the extractor itself so it cannot silently stop matching.""" + tokens = _path_tokens( + "Check frontend/src/i18n/locales/*.json, backend/core/version.py and NOPE.md.", + {"frontend", "backend"}, + {"version.py"}, + ) + assert tokens == ["frontend/src/i18n/locales/*.json", "backend/core/version.py", "NOPE.md"] + assert not _prefix_exists("frontend/src-tauri/Cargo.toml") + assert _prefix_exists("electron/src/{shared,renderer}/**/*.tsx") + assert _glob_regex("electron/src/{shared,renderer}/**/*.{js,tsx}").match( + "electron/src/shared/components/Foo.tsx" + ) From 08952ab31fb3dfb3b1611b220c016625c6deb4c1 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:09:34 +0530 Subject: [PATCH 13/85] docs(security): support only the latest 0.5.x and document API auth The policy promised 0.2.7 security fixes while CONTRIBUTING rules out backports, claimed the API has no auth although OMNIVOICE_API_KEY and the share PIN exist, and said gitleaks blocks merge although it only fails CI. Relative links from .github/ now resolve. --- .github/SECURITY.md | 11 +++++------ .github/SUPPORT.md | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index ee1b26902..aef174908 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -4,9 +4,8 @@ | Version | Supported | |---------|-----------| -| 0.5.x (latest release + `main` previews) | ✅ Current — all fixes land here | -| 0.2.7 | ⚠️ Legacy stable — security fixes only, upgrade recommended | -| < 0.2.7 | ❌ No longer supported | +| Latest 0.5.x release (and `main`) | ✅ All fixes, including security fixes, land here | +| Older releases, including final Tauri builds (≤ 0.5.3) | ❌ No backports — upgrade ([Tauri migration guide](../docs/electron-migration.md)) | ## Model supply chain @@ -59,9 +58,9 @@ VoiceStudio runs **100% locally** by default. The primary attack surface is: ## Automated scanning -Every pull request and push to `main` runs [`.github/workflows/security.yml`](.github/workflows/security.yml): +Every pull request and push to `main` runs [`.github/workflows/security.yml`](workflows/security.yml): -- **gitleaks** — secret scanning (blocks merge on a leaked credential) +- **gitleaks** — secret scanning (fails CI on a leaked credential) - **CodeQL** — Python + JavaScript/TypeScript static analysis → Security tab - **bandit** — Python static analysis → Security tab - **pip-audit** / **bun audit** — dependency advisory checks (reporting) @@ -71,6 +70,6 @@ GitHub Apps on creation. ## Security Best Practices for Users -- **Do not expose VoiceStudio to the internet without authentication.** The API has no built-in auth. Use a reverse proxy (Caddy, nginx, Tailscale) if you need remote access. +- **Do not expose VoiceStudio beyond loopback without authentication.** The API is unauthenticated on loopback by default. Before remote access, set `OMNIVOICE_API_KEY` (or enable the in-app share PIN for LAN guests) as described in [API authentication](../docs/api-auth.md), and use TLS through a reverse proxy or a private network such as Tailscale. - **Keep your installation updated.** The desktop app auto-checks for updates via the built-in updater. - **Review model sources.** Only download models from trusted Hugging Face repositories. diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index e66a1e7d0..ed882fe37 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -15,7 +15,7 @@ VoiceStudio is fully local — there's nothing to deactivate, just folders to delete. `scripts/uninstall.sh` (macOS/Linux) or `scripts\uninstall.ps1` (Windows) lists every VoiceStudio folder with its size (dry-run first) and removes them on `--yes`. The complete per-platform path list is in -[docs/install/uninstall.md](docs/install/uninstall.md). +[docs/install/uninstall.md](../docs/install/uninstall.md). ## Model sources we support From 1228f64f5afa88ddc6cf5cf6b14550412423b038 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:10:13 +0530 Subject: [PATCH 14/85] build(deps): align root TypeScript with Electron on 7.x Root declared typescript ^6.0.3 while the Electron workspace used ^7.0.2, so bun.lock carried two compilers. Both now resolve to 7.0.2. --- bun.lock | 6 +- package.json | 2 +- scripts/record_promo.mjs | 179 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 scripts/record_promo.mjs diff --git a/bun.lock b/bun.lock index 2601580cb..8e6162fad 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "playwright": "^1.63.0", "taze": "^21", "turbo": "^2.10.12", - "typescript": "^6.0.3", + "typescript": "^7.0.2", "wait-on": "^9.1.0", }, }, @@ -2320,7 +2320,7 @@ "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], @@ -2716,8 +2716,6 @@ "vitest/vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="], - "voicestudio-electron/typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "word-extractor/saxes": ["saxes@5.0.1", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], diff --git a/package.json b/package.json index 8572a252c..5b0d3a088 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "playwright": "^1.63.0", "taze": "^21", "turbo": "^2.10.12", - "typescript": "^6.0.3", + "typescript": "^7.0.2", "wait-on": "^9.1.0" } } diff --git a/scripts/record_promo.mjs b/scripts/record_promo.mjs new file mode 100644 index 000000000..797d9a445 --- /dev/null +++ b/scripts/record_promo.mjs @@ -0,0 +1,179 @@ +import { chromium } from 'playwright'; +import { execSync } from 'child_process'; +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const URL = 'http://localhost:5173'; +const OUTPUT_DIR = path.join(__dirname, '..', 'pics'); +const RAW_VIDEO = path.join(OUTPUT_DIR, 'raw_record.webm'); +const FINAL_MP4 = path.join(OUTPUT_DIR, 'promo.mp4'); +const FINAL_GIF = path.join(OUTPUT_DIR, 'promo.gif'); + +// Ensure output directory exists +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +async function run() { + console.log('🎬 Starting Cinematic Promo Recording...'); + + const browser = await chromium.launch({ + headless: false, + args: [ + '--window-size=1920,1080', + '--force-device-scale-factor=2', // retina + '--disable-font-subpixel-positioning', + '--disable-smooth-scrolling' + ] + }); + + const context = await browser.newContext({ + viewport: { width: 1920, height: 1080 }, + deviceScaleFactor: 2, + recordVideo: { + dir: OUTPUT_DIR, + size: { width: 1920, height: 1080 } + } + }); + + const page = await context.newPage(); + let recordingOffsetSeconds = 0; + const scriptStartTime = Date.now(); + + // Helper for cinematic pausing + const wait = (ms) => new Promise(r => setTimeout(r, ms)); + + // Helper for smooth zoom injections + const cinematicZoom = async (selector, scale, xOffset = 0, yOffset = 0) => { + await page.evaluate(({ selector, scale, xOffset, yOffset }) => { + const el = document.querySelector('body'); + el.style.transition = 'transform 2s cubic-bezier(0.25, 1, 0.5, 1)'; + el.style.transformOrigin = `${xOffset}% ${yOffset}%`; + el.style.transform = `scale(${scale})`; + }, { selector, scale, xOffset, yOffset }); + }; + + const resetZoom = async () => { + await page.evaluate(() => { + const el = document.querySelector('body'); + el.style.transform = 'scale(1)'; + }); + }; + + try { + console.log('📍 Navigating to VoiceStudio Launchpad...'); + await page.goto(URL, { waitUntil: 'load' }); + + // Give it enough time to stabilize the layout + await wait(2500); + + console.log('📍 Switching to History tab in sidebar...'); + await page.locator('button:has-text("History")').first().click(); + await wait(1500); + + console.log('📍 Loading Project from History...'); + // Click the "Load" button on the first history item + await page.locator('button:has-text("Load")').first().click(); + + // Wait for the workspace to fully load, waveforms to draw, etc + console.log('⏳ Waiting for dubbing workspace to render...'); + await wait(3500); + + // 🎥 Cinematic Zoom on the Video Player + console.log('📍 Zooming on Video Playback...'); + // Assuming video is roughly in the top left/center of the main view + await cinematicZoom('.video-player-container', 1.4, 20, 30); + await wait(2000); + + // Play the video + console.log('📍 Playing timeline...'); + // Click play button (WaveSurfer play or native) + // Finding the play button by its lucide icon class or explicit text is best + // Using a broad selector for the play button in the waveform bar + const playBtn = page + .locator('button:has(.lucide-play), [aria-label="Playback controls"] button') + .first(); + if (await playBtn.isVisible()) { + await playBtn.click(); + } + + // Let it play for a bit while zoomed + await wait(6000); + + // Reset zoom and zoom into the segment table + console.log('📍 Highlighting Segment Controls...'); + await resetZoom(); + await wait(1500); + await cinematicZoom('.segment-table', 1.3, 80, 50); + await wait(3000); + + // Highlight Output Options + console.log('📍 Showing Export Options...'); + await resetZoom(); + await wait(1000); + await cinematicZoom('text=Output Options', 1.5, 50, 95); + await wait(3000); + + // Reset zoom + await resetZoom(); + await wait(1500); + + // Show Clone Tab + console.log('📍 Navigating Tabs (Clone, Design)...'); + await page.click('text=Clone'); + await wait(2500); + + // Show Design Tab + console.log('📍 Showing Voice Design...'); + await page.click('text=Design'); + await wait(2500); + + // Back to Dub + await page.click('text=Dub'); + await wait(2000); + + console.log('✅ Browser automation complete.'); + } catch (error) { + console.error('❌ Error during automation:', error); + } finally { + // Stop recording and close + const videoPath = await page.video().path(); + await context.close(); + await browser.close(); + + // Rename the raw recording + if (fs.existsSync(videoPath)) { + fs.renameSync(videoPath, RAW_VIDEO); + console.log(`\n💾 Raw recording saved to: ${RAW_VIDEO}`); + + console.log('✂️ Processing with FFmpeg (Trimming, Speeding, GIF Generation)...'); + try { + console.log(`✂️ Processing final MP4 without trimming...`); + + // Generate MP4 Promo (Lossless quality) + // preset=veryslow, crf=16 is virtually lossless + // Speed up the entire video slightly (0.85*PTS is about 15% faster) since UI interactions can feel slow on playback + execSync(`ffmpeg -y -i "${RAW_VIDEO}" -filter_complex "[0:v]setpts=0.85*PTS[v]" -map "[v]" -c:v libx264 -preset veryslow -crf 16 -pix_fmt yuv420p "${FINAL_MP4}"`, { stdio: 'inherit' }); + console.log(`\n✨ HIGH QUALITY MP4 generated: ${FINAL_MP4}`); + + // Generate GIF Promo (24fps, 1080p width, high quality palette) + execSync(`ffmpeg -y -i "${FINAL_MP4}" -vf "fps=24,scale=1080:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=255:stats_mode=diff[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5" -loop 0 "${FINAL_GIF}"`, { stdio: 'inherit' }); + console.log(`✨ HIGH QUALITY GIF generated: ${FINAL_GIF}`); + + // Clean up raw + fs.unlinkSync(RAW_VIDEO); + + } catch (e) { + console.error('❌ FFmpeg processing failed. Please ensure ffmpeg is installed natively.'); + console.error(e.message); + } + + } + } +} + +run(); From 256ae92a27969ad345bdb3c2286b8dd752b30f6b Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:10:13 +0530 Subject: [PATCH 15/85] ci: pin bun to the packageManager version in CI, audit and Docker ci.yml ran an unpinned bun, security.yml bun 1.2 and the Docker frontend builder oven/bun:1-alpine. All now use 1.4.2; the audit installs from the root workspace lockfile. --- .github/workflows/ci.yml | 4 +++- .github/workflows/security.yml | 11 +++++------ deploy/Dockerfile | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb29eef8e..b4e7d6f09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,9 @@ jobs: node-version: '22' - name: Setup Bun - uses: oven-sh/setup-bun@f4d14e03ff726c06358e5557344e1da148b56cf7 # v1.2.2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: '1.4.2' # apt install ffmpeg is ~30 s every run; cache the resolved .debs. - name: System deps (ffmpeg) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 1ff8b15e0..4fdf879e9 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -177,17 +177,16 @@ jobs: bash scripts/uv-sync-retry.sh uv run --with pip-audit pip-audit - # Pin a floor: `bun audit` was added in bun 1.2.x, so guarantee it exists. + # Same bun as package.json packageManager (tests/test_bun_toolchain_pins.py). - name: Setup Bun - uses: oven-sh/setup-bun@f4d14e03ff726c06358e5557344e1da148b56cf7 # v1.2.2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: - bun-version: "1.2" + bun-version: '1.4.2' - # `bun audit` reports advisories against the Electron lockfile. Non-gating - # for the same reason; also tolerant of older bun without the subcommand. + # `bun audit` reports advisories against the root workspace lockfile + # (bun.lock, which covers Electron). Non-gating for the same reason. - name: bun audit (Electron) continue-on-error: true - working-directory: electron run: | bun install --frozen-lockfile bun audit diff --git a/deploy/Dockerfile b/deploy/Dockerfile index e97f5d58b..d1e75cd9e 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -9,7 +9,8 @@ ARG BASE_IMAGE=pytorch/pytorch:2.8.0-cuda12.8-cudnn9-runtime # ========================================== # Builder Stage: Compile React Frontend # ========================================== -FROM oven/bun:1-alpine AS frontend-builder +# Keep in lockstep with package.json packageManager (tests/test_bun_toolchain_pins.py). +FROM oven/bun:1.4.2-alpine AS frontend-builder WORKDIR /app # Monorepo — bun workspace with lockfile at repo root. Copy manifests first From fbde425e9a255509d180b0cb4765cea0a2835055 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:10:13 +0530 Subject: [PATCH 16/85] build: keep one workspace lockfile and one dependency range per package Remove the stale electron/bun.lock (nothing reads it; the root bun.lock is the workspace lock) and the standalone scripts/ package whose playwright ^1.43.0 lagged the root ^1.63.0. The promo recorder now uses the root playwright as scripts/record_promo.mjs. Guard test pins bun versions, a single lockfile, workspace-only manifests and matching ranges. --- electron/bun.lock | 2011 ------------------------------ scripts/bun.lock | 19 - scripts/package.json | 11 - scripts/record_promo.js | 179 --- tests/test_bun_toolchain_pins.py | 93 ++ 5 files changed, 93 insertions(+), 2220 deletions(-) delete mode 100644 electron/bun.lock delete mode 100644 scripts/bun.lock delete mode 100644 scripts/package.json delete mode 100644 scripts/record_promo.js create mode 100644 tests/test_bun_toolchain_pins.py diff --git a/electron/bun.lock b/electron/bun.lock deleted file mode 100644 index bc9821a7c..000000000 --- a/electron/bun.lock +++ /dev/null @@ -1,2011 +0,0 @@ -{ - "lockfileVersion": 2, - "configVersion": 1, - "workspaces": { - "": { - "name": "voicestudio-electron", - "dependencies": { - "@base-ui/react": "^1.8.0", - "@electron-toolkit/preload": "^3.0.2", - "@electron-toolkit/utils": "^4.0.0", - "@fontsource-variable/inter": "^5.3.0", - "@fontsource/ibm-plex-mono": "^5.3.0", - "@tanstack/react-form": "^1.33.5", - "@tanstack/react-pacer": "^0.23.0", - "@tanstack/react-query": "^5.102.8", - "@tanstack/react-router": "^1.170.34", - "@tanstack/react-store": "^0.11.1", - "@tanstack/react-virtual": "^3.14.11", - "@tanstack/store": "^0.11.1", - "@vidstack/react": "1.15.6", - "class-variance-authority": "^0.7.1", - "cn": "^0.2.6", - "dashjs": "^5.2.1", - "electron-updater": "^6.8.9", - "hls.js": "^1.7.2", - "i18next": "^26.4.2", - "lucide-react": "^1.44.0", - "qrcode": "^1.5.4", - "react": "^19.3.0", - "react-dom": "^19.3.0", - "react-i18next": "^17.0.13", - "remotion": "4", - "shadcn": "^4.21.0", - "sonner": "^2.0.8", - "tw-animate-css": "^1.4.0", - "wavesurfer.js": "^7.12.11", - "word-extractor": "1.0.4", - }, - "devDependencies": { - "@tailwindcss/vite": "^4.3.3", - "@tanstack/react-query-devtools": "^5.102.8", - "@tanstack/react-router-devtools": "^1.167.1", - "@testing-library/jest-dom": "^7.0.1", - "@testing-library/react": "^16.3.3", - "@types/node": "^24", - "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.7", - "@vitejs/plugin-react": "^6.1.1", - "builder-util": "26.15.3", - "electron": "^44.3.0", - "electron-builder": "^26.15.3", - "electron-vite": "6.0.0-beta.1", - "jsdom": "^28", - "tailwindcss": "^4.3.3", - "typescript": "^7.0.2", - "vite": "^8.3.0", - "vite-plus": "^0.3.1", - }, - }, - }, - "trustedDependencies": [ - "electron", - "esbuild", - "electron-builder", - ], - "packages": { - "@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], - - "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], - - "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="], - - "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@6.8.1", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.1.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.6" } }, "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ=="], - - "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="], - - "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], - - "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - - "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - - "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - - "@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="], - - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="], - - "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="], - - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], - - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="], - - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - - "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - - "@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="], - - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="], - - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ=="], - - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="], - - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="], - - "@babel/preset-typescript": ["@babel/preset-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-transform-modules-commonjs": "^7.29.7", "@babel/plugin-transform-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ=="], - - "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - - "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - - "@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="], - - "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - - "@base-ui/react": ["@base-ui/react@1.8.0", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@base-ui/utils": "0.4.0", "@floating-ui/react-dom": "^2.1.9", "@floating-ui/utils": "^0.2.12", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q=="], - - "@base-ui/utils": ["@base-ui/utils@0.4.0", "", { "dependencies": { "@babel/runtime": "^7.29.7", "@floating-ui/utils": "^0.2.12", "reselect": "^5.2.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw=="], - - "@blazediff/core": ["@blazediff/core@1.9.1", "", {}, "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA=="], - - "@bramus/specificity": ["@bramus/specificity@2.4.2", "", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="], - - "@csstools/color-helpers": ["@csstools/color-helpers@6.1.1", "", {}, "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w=="], - - "@csstools/css-calc": ["@csstools/css-calc@3.3.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ=="], - - "@csstools/css-color-parser": ["@csstools/css-color-parser@4.2.2", "", { "dependencies": { "@csstools/color-helpers": "^6.1.1", "@csstools/css-calc": "^3.3.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg=="], - - "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="], - - "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.12", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw=="], - - "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], - - "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.75.1", "", { "dependencies": { "@dotenvx/primitives": "^0.8.0", "commander": "^11.1.0", "conf": "^10.2.0", "dotenv": "^17.2.1", "enquirer": "^2.4.1", "env-paths": "^2.2.1", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "open": "^8.4.2", "picomatch": "^4.0.4", "systeminformation": "^5.22.11", "undici": "^7.11.0", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-/BITOC9dmS/edY2zQwZNicQ059O6RKabtQfyEafV0nGtfYRNHYy1DIPiYVcov40+tob9hfmBnbR963dS+EQ1DQ=="], - - "@dotenvx/primitives": ["@dotenvx/primitives@0.8.0", "", {}, "sha512-VYJy0uhFm9zTJ1TxBaW/pA8bjbOM/OttaNMwZ1RHG4JKyRG7DhSdiqD1ipQoAyoD22olUtxbP78W9xY3Wd11bg=="], - - "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.5", "", {}, "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA=="], - - "@electron-toolkit/preload": ["@electron-toolkit/preload@3.0.2", "", { "peerDependencies": { "electron": ">=13.0.0" } }, "sha512-TWWPToXd8qPRfSXwzf5KVhpXMfONaUuRAZJHsKthKgZR/+LqX1dZVSSClQ8OTAEduvLGdecljCsoT2jSshfoUg=="], - - "@electron-toolkit/utils": ["@electron-toolkit/utils@4.0.0", "", { "peerDependencies": { "electron": ">=13.0.0" } }, "sha512-qXSntwEzluSzKl4z5yFNBknmPGjPa3zFhE4mp9+h0cgokY5ornAeP+CJQDBhKsL1S58aOQfcwkD3NwLZCl+64g=="], - - "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], - - "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], - - "@electron/get": ["@electron/get@5.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^3.0.0", "graceful-fs": "^4.2.11", "progress": "^2.0.3", "semver": "^7.6.3", "sumchecker": "^3.0.1" }, "optionalDependencies": { "undici": "^7.24.4" } }, "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA=="], - - "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], - - "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], - - "@electron/rebuild": ["@electron/rebuild@4.2.0", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^12.2.0", "read-binary-file-arch": "^1.0.6" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ=="], - - "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], - - "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "@exodus/bytes": ["@exodus/bytes@1.15.1", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q=="], - - "@floating-ui/core": ["@floating-ui/core@1.8.0", "", { "dependencies": { "@floating-ui/utils": "^0.2.12" } }, "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ=="], - - "@floating-ui/dom": ["@floating-ui/dom@1.8.0", "", { "dependencies": { "@floating-ui/core": "^1.8.0", "@floating-ui/utils": "^0.2.12" } }, "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg=="], - - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.9", "", { "dependencies": { "@floating-ui/dom": "^1.8.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg=="], - - "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], - - "@fontsource-variable/inter": ["@fontsource-variable/inter@5.3.0", "", {}, "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA=="], - - "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.3.0", "", {}, "sha512-eTgnZjZEGk1QtD3ZstF+Vclo2HLAni8YMy34/DxllwZvyz1lR/1RF/xTiAquOBO7MvqBx8D2Ig2WCPMVfdZu7Q=="], - - "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], - - "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], - - "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], - - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], - - "@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="], - - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], - - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], - - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - - "@oxc-project/runtime": ["@oxc-project/runtime@0.148.0", "", {}, "sha512-0ExYgJZv8+nFuoV0T/xzX5ZyXFXwkHJFJg9LWTwhqgRLpe+IzBLirvyYKSdS8mPofTBKAjDYIA2o84xh5BHXew=="], - - "@oxc-project/types": ["@oxc-project/types@0.148.0", "", {}, "sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A=="], - - "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.66.0", "", { "os": "android", "cpu": "arm" }, "sha512-2Me9eoptv6ERdEuI2P8AOlYdHHraXebJaM6SC0kc2Dfb+mLrep2db+fedBPKaYn673h/vBgvP4tkOdAbaudX6w=="], - - "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.66.0", "", { "os": "android", "cpu": "arm64" }, "sha512-u7O+bSSF0HGsDKkQQxBqvLGVepu93RA+JKu+ONqvfh4sCnCEbj31wZj4iG5gk3XfRwrmYj0/8catkO2LcblQKQ=="], - - "@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.66.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/ikyMIVjX/sdo7KtjxoEsSUosfPzveVhT9RWMx9yGqFDKFJ89JAEKuEeLBmurDjrkb4w8tOnAdSO3SBaplY3bw=="], - - "@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.66.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-q5xUsKeFqawa9NXa6ZGXWimFV19m8MogKPdTaSVDAAk2EQKBmBZRDeluwcl1p8ty/OFc9s9888OKEh3xfPVH0g=="], - - "@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.66.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-CR+x4VzMY0pRXLK/xFQ/RzsSFkP5t2Z2mef0QY6OP/rTRcMUoMLCOM62/3Fp/t0K+UDoBKxvMyeb6D0zPMjleA=="], - - "@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.66.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZEYmO/LbH9tTQCADILHGZE4GeOXOAj2VzedHkASNwjmwlwtutJCLpCJbIs37wRGTFgWRoEcD72jpMX+IBJUGjQ=="], - - "@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.66.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hNtR9/oU0CeTkq7JnRkmBQwqe17v2ZaAMLC4VcN7IIOWeRyWDk0knSPWS9iiLmtbZ2RRBBtsG01jQgkZmKCJeQ=="], - - "@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.66.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uwOVQ8i6I1LT/+eDzfsgrrcZp8Fn6NPVUPn8fF5gdFGekFf0PddF+LEuwsD0/pbNUcKZhDj2rQ5UpITh9gF4iQ=="], - - "@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.66.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg=="], - - "@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.66.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw=="], - - "@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.66.0", "", { "os": "linux", "cpu": "none" }, "sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q=="], - - "@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.66.0", "", { "os": "linux", "cpu": "none" }, "sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA=="], - - "@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.66.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A=="], - - "@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.66.0", "", { "os": "linux", "cpu": "x64" }, "sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA=="], - - "@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.66.0", "", { "os": "linux", "cpu": "x64" }, "sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw=="], - - "@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.66.0", "", { "os": "none", "cpu": "arm64" }, "sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ=="], - - "@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.66.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-soV+0vESv7e5ntCHWC61x4gg8OSak6IHHnWsZmHrJFlvMj2AK+kmldErCNkVkrvc1Ts2/++rJXn+IuAb2WMXhw=="], - - "@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.66.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-YCPi23uRIEYuIKTZohAkKbPFpujQ5QBuUM5iDv+UqbCmTPAkaFsxjsSuB8xlBpRT0G7eP/4HMF+cPDSqHtOD9A=="], - - "@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.66.0", "", { "os": "win32", "cpu": "x64" }, "sha512-bwTQcv/JVRPkOqQtMF0X7vpvpncDQiBcXHxZ9S2hR12Hlo8bvBdUR5x5XnxzDZ3kM0qoZw1rv7KaD66Ly+pFWA=="], - - "@oxlint-tsgolint/darwin-arm64": ["@oxlint-tsgolint/darwin-arm64@7.0.2001", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w=="], - - "@oxlint-tsgolint/darwin-x64": ["@oxlint-tsgolint/darwin-x64@7.0.2001", "", { "os": "darwin", "cpu": "x64" }, "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw=="], - - "@oxlint-tsgolint/linux-arm64": ["@oxlint-tsgolint/linux-arm64@7.0.2001", "", { "os": "linux", "cpu": "arm64" }, "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A=="], - - "@oxlint-tsgolint/linux-x64": ["@oxlint-tsgolint/linux-x64@7.0.2001", "", { "os": "linux", "cpu": "x64" }, "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ=="], - - "@oxlint-tsgolint/win32-arm64": ["@oxlint-tsgolint/win32-arm64@7.0.2001", "", { "os": "win32", "cpu": "arm64" }, "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q=="], - - "@oxlint-tsgolint/win32-x64": ["@oxlint-tsgolint/win32-x64@7.0.2001", "", { "os": "win32", "cpu": "x64" }, "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog=="], - - "@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.81.0", "", { "os": "android", "cpu": "arm" }, "sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw=="], - - "@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.81.0", "", { "os": "android", "cpu": "arm64" }, "sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ=="], - - "@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.81.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ=="], - - "@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.81.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw=="], - - "@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.81.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA=="], - - "@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.81.0", "", { "os": "linux", "cpu": "arm" }, "sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA=="], - - "@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.81.0", "", { "os": "linux", "cpu": "arm" }, "sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ=="], - - "@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.81.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA=="], - - "@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.81.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg=="], - - "@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.81.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g=="], - - "@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.81.0", "", { "os": "linux", "cpu": "none" }, "sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg=="], - - "@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.81.0", "", { "os": "linux", "cpu": "none" }, "sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA=="], - - "@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.81.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw=="], - - "@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.81.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA=="], - - "@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.81.0", "", { "os": "linux", "cpu": "x64" }, "sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg=="], - - "@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.81.0", "", { "os": "none", "cpu": "arm64" }, "sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg=="], - - "@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.81.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA=="], - - "@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.81.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA=="], - - "@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.81.0", "", { "os": "win32", "cpu": "x64" }, "sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA=="], - - "@oxlint/plugins": ["@oxlint/plugins@1.79.0", "", {}, "sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg=="], - - "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.9.4", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg=="], - - "@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="], - - "@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="], - - "@peculiar/webcrypto": ["@peculiar/webcrypto@1.7.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "tslib": "^2.8.1", "webcrypto-core": "^1.9.2" } }, "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ=="], - - "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - - "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.8", "", { "os": "android", "cpu": "arm" }, "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw=="], - - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.8", "", { "os": "android", "cpu": "arm64" }, "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ=="], - - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA=="], - - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA=="], - - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ=="], - - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.8", "", { "os": "linux", "cpu": "arm" }, "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q=="], - - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew=="], - - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA=="], - - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA=="], - - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ=="], - - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g=="], - - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA=="], - - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.8", "", { "os": "none", "cpu": "arm64" }, "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg=="], - - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A=="], - - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng=="], - - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], - - "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - - "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], - - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@svta/cml-608": ["@svta/cml-608@1.0.2", "", {}, "sha512-ZEJ68330gcLKfvVv6Qifr1HR7+GldDUxzkjqSbqRK7jHtHSLSV1JAyDwlYe8+C7ABuY1bp8MpgJ4/Gg+jl+pLQ=="], - - "@svta/cml-cmcd": ["@svta/cml-cmcd@2.3.2", "", { "peerDependencies": { "@svta/cml-structured-field-values": "1.1.3", "@svta/cml-utils": "1.5.0" } }, "sha512-SKBBjLmci0WK8HMjuv+36tVIMktonoOoxsXblOFZmB+ePPV2zjRMTD+2ZmE/1VEPJkKHENyhSjSHgJyeOlvZ1A=="], - - "@svta/cml-cmsd": ["@svta/cml-cmsd@1.0.6", "", { "peerDependencies": { "@svta/cml-cta": "1.0.6", "@svta/cml-structured-field-values": "1.1.3", "@svta/cml-utils": "1.5.0" } }, "sha512-LUORV6bb0TbU4rSC2HoPqUCix1igLrXkRQXWiIyJo2OMzb14kAK/1jsW0mzY6up6w1GrjKQcjc6OwqJdo/zd/g=="], - - "@svta/cml-cta": ["@svta/cml-cta@1.0.6", "", { "peerDependencies": { "@svta/cml-structured-field-values": "1.1.3", "@svta/cml-utils": "1.5.0" } }, "sha512-l13/4myTX1EbRd38nY8J1umVY5UdR/nZO6CeKqVLXnMMIayg7/fu0TueZckjTA9Loal55MGK1xbHnXVjz7OUUw=="], - - "@svta/cml-dash": ["@svta/cml-dash@1.0.6", "", { "peerDependencies": { "@svta/cml-utils": "1.5.0" } }, "sha512-4XtHYlPrzL/dRe/8XmRQoLnTo9S86tISgrl67eUqKl5MtNTpZBYTncuPrspslPZPZROBBWNrBuepYfYUtU9CKA=="], - - "@svta/cml-id3": ["@svta/cml-id3@1.0.6", "", { "peerDependencies": { "@svta/cml-utils": "1.5.0" } }, "sha512-63j8gkAnPOmOBWlp0hIZPvsIioZttdbg6/TgwITqMYbSLYVJ+6QGa/UtIP0I84NsfstANw6QdCn7i8SS08kn1A=="], - - "@svta/cml-request": ["@svta/cml-request@1.0.12", "", { "peerDependencies": { "@svta/cml-cmcd": "2.3.2", "@svta/cml-utils": "1.5.0", "@svta/cml-xml": "1.1.4" } }, "sha512-4sJvnnoNpq58j2mCGP8k+MF6wVy/qa4gbt6kfT1dPIKmn3mPxw+JVfilhcWsUi+peK2yCZxOJJYyHj1cAcQE1w=="], - - "@svta/cml-structured-field-values": ["@svta/cml-structured-field-values@1.1.3", "", { "peerDependencies": { "@svta/cml-utils": "1.5.0" } }, "sha512-XqLzQOTznz6kh/nsSh8dy1kV7GQjL4csG+2U5EvLGhAqNuNUczbVg5EAsDobKDVg8xhdthdXx2UuwM+ZHQCxSg=="], - - "@svta/cml-utils": ["@svta/cml-utils@1.5.0", "", {}, "sha512-JMqclD7Akd+GSJiuaYNUHOP2wNtf/nauKeszlYeivSHfi0Lp3pmSW5PXDvJ2dO4aPmmSOQbF0ztTsW9Vcs2Whw=="], - - "@svta/cml-xml": ["@svta/cml-xml@1.1.4", "", { "peerDependencies": { "@svta/cml-utils": "1.5.0" } }, "sha512-jbixqjiJIc16SGxylHwiOzO+DuhkGfuP+fJ9AHeVJKdFDKnabgfCDpnp6dvZpZnjMj4nHvzVtuUV7RISPIwYXw=="], - - "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.3.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.24.1", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.3" } }, "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.3", "@tailwindcss/oxide-darwin-arm64": "4.3.3", "@tailwindcss/oxide-darwin-x64": "4.3.3", "@tailwindcss/oxide-freebsd-x64": "4.3.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", "@tailwindcss/oxide-linux-x64-musl": "4.3.3", "@tailwindcss/oxide-wasm32-wasi": "4.3.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" } }, "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.3", "", { "os": "android", "cpu": "arm64" }, "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.3", "", { "dependencies": { "@emnapi/core": "^1.11.1", "@emnapi/runtime": "^1.11.1", "@emnapi/wasi-threads": "^1.2.2", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - - "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.4", "", { "bin": { "intent": "./bin/intent.js" } }, "sha512-6T5Yop/793YI+H+5J8Hsyj4kCih9sl4t3ElLgKioW5hk3ocn+ZdSJ94tT7vL7uabxSugWYBZlOTMPzEw2puvQw=="], - - "@tanstack/form-core": ["@tanstack/form-core@1.33.5", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.11.0" } }, "sha512-3dfx9MBP0aq5sXKteikG629X9oviptrQj0IFRk9YGcb+lB7Kv5x8S17oOSk1wUWgjQZ4xVJEMbKwOAODymocgA=="], - - "@tanstack/history": ["@tanstack/history@1.162.3", "", {}, "sha512-zZTDxZxdDxZudHXGp42d221R/sEKoQPbZsW2ezpdWknHpOmJVLyiljW21pdWtKXfMnLq+Kg5VJpVBwOe18+ZzQ=="], - - "@tanstack/pacer": ["@tanstack/pacer@0.22.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.5.0", "@tanstack/store": "^0.11.1" } }, "sha512-Zq23KOn30tFiA6ZYWihs5x++ttHkOIcnIIjRrJq05tGrYx9+iHsmVaj8jb7UJVemEI+JRWGzJ9C+50YV4nRz0w=="], - - "@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="], - - "@tanstack/query-core": ["@tanstack/query-core@5.102.8", "", {}, "sha512-ZNjkJ33CqvPNec/6lZBnHqLc3EVGPZ9ySLhYahU9TcuRFdmwXewuj0c4hwSWcGHqEUwcSrKeZ+oGcvPBqXcQcg=="], - - "@tanstack/query-devtools": ["@tanstack/query-devtools@5.102.8", "", {}, "sha512-ZgeMKuF5d/zOE+tgWm3cSbD6Zcbr6IugVsnbHzUcSismqLZDSUPBKM6ILUxExgwe6rPAOox2x5bA5T+PSOQG0Q=="], - - "@tanstack/react-form": ["@tanstack/react-form@1.33.5", "", { "dependencies": { "@tanstack/form-core": "1.33.5", "@tanstack/react-store": "^0.11.0" }, "peerDependencies": { "@tanstack/react-start": "*", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@tanstack/react-start"] }, "sha512-LlRB28qJwO/QCGaHvWnbdh4haBgTFiZVmzA2uzxSBS3YA7/IqrQ6HOBK70CkFQ+DbflZ7NawsmSln13h5iIdTA=="], - - "@tanstack/react-pacer": ["@tanstack/react-pacer@0.23.0", "", { "dependencies": { "@tanstack/pacer": "0.22.0", "@tanstack/react-store": "^0.11.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-Ec3h+kT8kYlpGb1M8F7836q+udE/M2RmE8KLr1rhs+UDBgvxjfmyR2bIUeaDEe9Zn1YXNI986RbErXyr5fEJIg=="], - - "@tanstack/react-query": ["@tanstack/react-query@5.102.8", "", { "dependencies": { "@tanstack/query-core": "5.102.8" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-TYBea4OuXWD7MhaSHq069TWbFe7rcwWN6kzT7JF0OKi1K6c1gTv2IzD6A6ExJsCMozdkqBWeuIUZmu4KQg0O5A=="], - - "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.102.8", "", { "dependencies": { "@tanstack/query-devtools": "5.102.8" }, "peerDependencies": { "@tanstack/react-query": "^5.102.8", "react": "^18 || ^19" } }, "sha512-QKb7A44BZOU7nxsGA4gFN1fofjYovar5O0T83Ff4Y+2eRq09RGFrAzzutVdF/6/emfSaMDohp6e759BjB3fxEw=="], - - "@tanstack/react-router": ["@tanstack/react-router@1.170.34", "", { "dependencies": { "@tanstack/history": "1.162.3", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.29", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-zYy1SNjIIAEMfU9mqmB5171PeELs/k3SR+U6QxDKSqyMLw4Aq6R+eC57Sa8iB6X91q1ouES30khauyYww4JMcQ=="], - - "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.167.1", "", { "dependencies": { "@tanstack/router-devtools-core": "1.168.1" }, "peerDependencies": { "@tanstack/react-router": "^1.170.19", "@tanstack/router-core": "^1.171.16", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-pjfGrmjj4d7naEPM7oshqFfwBoxDPNo/UxltlHH5ePbHsJ+plBhd+JaAewm1ueYOjZ0js9hckjWWDYXpCrSfKw=="], - - "@tanstack/react-store": ["@tanstack/react-store@0.11.1", "", { "dependencies": { "@tanstack/store": "0.11.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ=="], - - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.11", "", { "dependencies": { "@tanstack/virtual-core": "3.17.9" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-SStWf8bYdTgAquYqG4Pi2+d1XimQKoCIJ94H3jsm0D6UA0yqffvWuvUzrrQ4idewFWq8BSPpahnmLVosJIAs6g=="], - - "@tanstack/router-core": ["@tanstack/router-core@1.171.29", "", { "dependencies": { "@tanstack/history": "1.162.3", "cookie-es": "^3.0.0", "seroval": "^1.6.2", "seroval-plugins": "^1.6.2" } }, "sha512-tBXJrU4rDl+UsfTvP/WM1pcQ+fXk4rb2uuK/FMw29JNHEkFdEbptA0yvk8vkNjooTF7DAiTUrZMf2H4X7eGt4g=="], - - "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.168.1", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.171.16", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-qr4voa4cpSMwQvS3867xkU3AB3MtJbTuovKIy+btjJ/Faju6er9w0nDylmD+005Mk/3YKw9/iueZJl2JAB7JOA=="], - - "@tanstack/store": ["@tanstack/store@0.11.1", "", {}, "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA=="], - - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.9", "", {}, "sha512-M8Bzy7CCMvUjRiuoHVH9mRjyUVczRs8v8RcUEphLFGc445ZP4KQ15Y1sLqhQqJi/HgGJrxfCHnEnIX0x9mVrBg=="], - - "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], - - "@testing-library/jest-dom": ["@testing-library/jest-dom@7.0.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" }, "peerDependencies": { "@testing-library/dom": ">=10 <11", "vitest": ">= 0.32" }, "optionalPeers": ["vitest"] }, "sha512-oMDTC3oA+6CXSO2JZnvOI7CA6oVub6kij5ggk9ohwye5slmkwxYDXcPOVxgMw/RQlticjtO0C1RZkR97HgrWMw=="], - - "@testing-library/react": ["@testing-library/react@16.3.3", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg=="], - - "@testing-library/user-event": ["@testing-library/user-event@14.6.7", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg=="], - - "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], - - "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], - - "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], - - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - - "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - - "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], - - "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], - - "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/node": ["@types/node@24.13.4", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw=="], - - "@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="], - - "@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="], - - "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], - - "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], - - "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], - - "@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="], - - "@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="], - - "@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="], - - "@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="], - - "@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="], - - "@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="], - - "@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="], - - "@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="], - - "@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="], - - "@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="], - - "@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="], - - "@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="], - - "@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="], - - "@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="], - - "@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="], - - "@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="], - - "@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="], - - "@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="], - - "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], - - "@vidstack/react": ["@vidstack/react@1.15.6", "", { "dependencies": { "@floating-ui/dom": "^1.6.10", "media-captions": "^1.0.4" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0" } }, "sha512-hO7cLld5yfxCdDnDCYqh5aS7JxmHfsmX43PmKBJWzDTwK40P3hh5qAdDQzl3qDtEJLYjuo3Sv+kr6tt/VRtJzQ=="], - - "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="], - - "@vitest/browser": ["@vitest/browser@4.1.11", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.11" } }, "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w=="], - - "@vitest/browser-preview": ["@vitest/browser-preview@4.1.11", "", { "dependencies": { "@testing-library/dom": "^10.4.1", "@testing-library/user-event": "^14.6.1", "@vitest/browser": "4.1.11" }, "peerDependencies": { "vitest": "4.1.11" } }, "sha512-iPKSE6Ibayey6HFgK1V1/aHgyhx7HSRk1YMi+lnBZGmlIiNV5Uc7xRkD9Su8RDylTxDECK23t7kTHdRKoqSYDQ=="], - - "@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="], - - "@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="], - - "@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="], - - "@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="], - - "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], - - "@voidzero-dev/vite-plus-darwin-arm64": ["@voidzero-dev/vite-plus-darwin-arm64@0.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3xK+G6zJHa4Q60NgAnnZ38ML5NSf5n3+DBHSINqCgsPpQ7XtMWkpEsXArWU7u3nlW3f1F2XLAvuAk54TmQXptw=="], - - "@voidzero-dev/vite-plus-darwin-x64": ["@voidzero-dev/vite-plus-darwin-x64@0.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-GbJ2RWZezmpr/erPlJ4Kw9T3y2EP3cwZSkh+Ph8IfcJMj3MRqTw1lfxCl81x8Opyct6wYU6kDRU3zH95Tl341Q=="], - - "@voidzero-dev/vite-plus-linux-arm64-gnu": ["@voidzero-dev/vite-plus-linux-arm64-gnu@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-vYXxMRxE7DAa8QIh3k5IkfTeBbA/8JHYHL14rsodq4er4FNOd4AAM07eAczin7Pdf4Aie/2BPkymSF8hFyjZDg=="], - - "@voidzero-dev/vite-plus-linux-arm64-musl": ["@voidzero-dev/vite-plus-linux-arm64-musl@0.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LAnyEqhZ2Fji6bXDnigSWbEjS/B7u/Al01N4JERSR6scr4PbZpfMW8/1EUHfi+9pIdxwoDazIRZeh5DvEDX8eQ=="], - - "@voidzero-dev/vite-plus-linux-x64-gnu": ["@voidzero-dev/vite-plus-linux-x64-gnu@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-F9Ek2p+ZnDB2FrO6ugZU2ZnuOO/Q9GR9jwUcnK2oUHwl6AZUvg671L7vU57rrtY4f9pS+AcL9lWRoS4wedXm3A=="], - - "@voidzero-dev/vite-plus-linux-x64-musl": ["@voidzero-dev/vite-plus-linux-x64-musl@0.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3Q+hJ19iw1k2NOCoop/gK+etMPnPYbgdqXBS9mgUuuDUA142v1PoYH+gk3NMV+KyDvSZaLkgDuGrdNav0pJtqw=="], - - "@voidzero-dev/vite-plus-win32-arm64-msvc": ["@voidzero-dev/vite-plus-win32-arm64-msvc@0.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aZAfZdQBFDzktOF/0OJDYrkOP551IxmoMRGfU3KdJm25Gpj8WHl5QTNx/mL8216rbZ7p4QIdBc4gajoRCAtsuA=="], - - "@voidzero-dev/vite-plus-win32-x64-msvc": ["@voidzero-dev/vite-plus-win32-x64-msvc@0.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-8rxCvdTpd1v/+g/ECjqKnHxBlxmk8O9kNl4wwDcfWszbL/O/pNNSktKHq6rR3A6m6p0NCoZsenbGLuJsHE+q9A=="], - - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.15", "", {}, "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA=="], - - "@yuku-codegen/binding-android-arm64": ["@yuku-codegen/binding-android-arm64@0.9.5", "", { "os": "android", "cpu": "arm64" }, "sha512-jrOY5WM+AaAqkv51fHP1x28ifto4WgcZVzodLUyMU1jMWn5Sq+VciRdk/n8E0Ey5w4p4cWWE+m5GfXWOYh7Kzw=="], - - "@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4O4lkCQIzPZGjiNB1GORSI6hBECbiiSBS/APZXPvNKYH+nhY4uuqv03LNXA+SET3hoBjvr95P5rIhY8KQaQUBA=="], - - "@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-9EvoUO0SEhD6/d8VHdWuPerepPMSR1y84+UEgz8Un1Ope14Oe7xMvePpEQLTLovoIFZ8Zg3iZ8z8E11pZMqC3g=="], - - "@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.9.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-HqT78WwgHTmp8lwujoUa9CrIortX4DdpuiVC18ZPSGvuuJf4ylpIEI6QrQEM78Zwz3muhAWAOXZ5irdiYY+AyA=="], - - "@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/binding-linux-arm-gnu@0.9.5", "", { "os": "linux", "cpu": "arm" }, "sha512-QJXwIW6Ms3QIawbcBIedmrmXnfdpuAOjZJ/eAABq5XTzWvSxZ7lutu9W5yIHahlaTaFnBo7ikrVMD1szLTpPUw=="], - - "@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/binding-linux-arm-musl@0.9.5", "", { "os": "linux", "cpu": "arm" }, "sha512-eHbFy3IHGb+IYarrYwAo0yWSEQV3eAmEn6VrsKA6I1Wy1YPJxWqSJlV69JhqsHtJuJlljUIF3ex0y46yaKIu9w=="], - - "@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/binding-linux-arm64-gnu@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ByoJMbySTaDhjAXSu8q6Lh7HKg3YoesXpcT72aYk0Aiw4PCznmY4ybpLTq0RCvp0RIPhFm/6yECFiGBwyCC1nw=="], - - "@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/binding-linux-arm64-musl@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-gD9vfXIoBw1toSxhO9rgmSu/FEfy3PMznJAxVjIuH8DrWEiDKXmJO0pJKfj1Ltbe/TmtWVZR+TjISqeSIGQzMg=="], - - "@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/binding-linux-x64-gnu@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-BARvdnvqMGjOr5Iel2JH+9H5vAIE0R6H0Z2fsT02xrvmI/1ZXNB8lkuX+ZGevPhZb3zJ9WeC/R8JZstrsUOK5g=="], - - "@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/binding-linux-x64-musl@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-x8flcevS1fbb7ESrmpOY/pON4eInSaE/7Ktjqx2udOE2W33BNSZmJuwpyUgo54AoHNqrcaM7szqydyJn5fNvew=="], - - "@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/binding-win32-arm64@0.9.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-KOL/rBatWqH4ZpCNoF8ZtSNdOJbAxBJLmk/VeRciepGROPTbPum1A9t67GpFgOU7qrkUWlPJdJ+KHKbvbmOt+w=="], - - "@yuku-codegen/binding-win32-x64": ["@yuku-codegen/binding-win32-x64@0.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-FxENahEjWSan59Syh/us/Kf4wNq8qrJLFJ3R2N4Oiwtb6yNdz/rWLNTIoNSssnsp7IoWJdUP6o/Z8ppg7lXMcg=="], - - "@yuku-parser/binding-android-arm64": ["@yuku-parser/binding-android-arm64@0.9.5", "", { "os": "android", "cpu": "arm64" }, "sha512-A2JCFCSHfnficqYEw4Iujpx7XrkMM3UfFcgJFmYpZSo9zM4nyhmdIIE0FogSduuU60lhM0/UcfuUBXIVNBMlGQ=="], - - "@yuku-parser/binding-darwin-arm64": ["@yuku-parser/binding-darwin-arm64@0.9.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3PiyU+Eare4YuKaQ22N98/yAiROPY5o/NQJHraICzDvk4pgS+m+bgLKOvkGBRn33OnV95Vdv1Mn38b+MQHpULQ=="], - - "@yuku-parser/binding-darwin-x64": ["@yuku-parser/binding-darwin-x64@0.9.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-blFMAFI7AInI83XaiOF8cIeiRM46Nz9EfpZtZPRLbKxSA9aAr5v8aHYpmfN6NoyXxAv/10pwS4gsAuc1W6fy5Q=="], - - "@yuku-parser/binding-freebsd-x64": ["@yuku-parser/binding-freebsd-x64@0.9.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TsNuL4qsZdO0tHp5GW43Y5fHeLPpPHA675wdcPNTcdq2ZO5AvsQWFFoiDg8CH4oBktfsQ9tdvKhjLnLYwKvp4g=="], - - "@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/binding-linux-arm-gnu@0.9.5", "", { "os": "linux", "cpu": "arm" }, "sha512-b0afYK5gHeV8RdmOcqAlgM8ONsye4cax4DMnIBaoJyPMd1UTGvTbpkqQcPVdhmHlcfcGVGMV1laeVovlr/dscw=="], - - "@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/binding-linux-arm-musl@0.9.5", "", { "os": "linux", "cpu": "arm" }, "sha512-fD3lKzl+r6j6n8DwiMY53qnh5DLqD8KJjCp+NbVud54Nnh3Q9Wprqss3YzyMHP3NSJk663Wlg1E1X5qOuFVFig=="], - - "@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/binding-linux-arm64-gnu@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-aRU/aCphV1MWCl/lvI6NX8LgucHQ68Fx+vz7NBb/5MEr2EuzCxiPOQqeFcMdWh+2AED/p0AvAREch0c+cSnlhA=="], - - "@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/binding-linux-arm64-musl@0.9.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-5+Guro0l8H473YXlEjVNBRLN/IbPbJdnQh1zo0OLt49xxnON+XMJRRaEyTOTfkn9QSRg0+BhEKGR4W9Gu2uZJQ=="], - - "@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/binding-linux-x64-gnu@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-pwwSyV9q+GlvzSXlsMZBMlgk22L5bud714/vqZCdeUTivzeUVltQzUaf3IQXOGXdpc59JYTeuMCtbGquaAUoCA=="], - - "@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/binding-linux-x64-musl@0.9.5", "", { "os": "linux", "cpu": "x64" }, "sha512-z18j6JN3lBHH8vzN7gG1M8fI0nlgItSfnC9PfbmUbt97iHViKfw2iA2G9fGwRMe5LyPRZBrejIlapbygPbpdaw=="], - - "@yuku-parser/binding-win32-arm64": ["@yuku-parser/binding-win32-arm64@0.9.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-s6Gwttb1dQvtPX6Bgkw+UPC9IO3UBDXc6Zezog8MMgvu3UfJBBB9TQqKBX/2quu0Eyh+lLSAyNlIyyecaagUPg=="], - - "@yuku-parser/binding-win32-x64": ["@yuku-parser/binding-win32-x64@0.9.5", "", { "os": "win32", "cpu": "x64" }, "sha512-FrERt9YWatY3bJfSUdi4YWY+6iQcyo3MzCC821BxlWM5TFZEHijnpz/bknR79VHlXY/9CvXz8ZlaAGPsTtN3nw=="], - - "@yuku-toolchain/types": ["@yuku-toolchain/types@0.9.5", "", {}, "sha512-KiuLNNgX9uNealaWAR+G3/cMXnRk9x4TY2EkYe/KIag+UPdwiA0RRf1hr1WAxzTP8KGzCTkqUdLPvqh32sEO3w=="], - - "abbrev": ["abbrev@4.0.0", "", {}, "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA=="], - - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - - "ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="], - - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "app-builder-lib": ["app-builder-lib@26.15.3", "", { "dependencies": { "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.4", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@noble/hashes": "^2.2.0", "@peculiar/webcrypto": "^1.7.1", "@types/fs-extra": "9.0.13", "ajv": "^8.18.0", "asn1js": "^3.0.10", "async-exit-hook": "^2.0.1", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.15.3", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.2.5", "pkijs": "^3.4.0", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "unzipper": "^0.12.3", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.15.3", "electron-builder-squirrel-windows": "26.15.3" } }, "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "asn1js": ["asn1js@3.0.10", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.5", "tslib": "^2.8.1" } }, "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - - "ast-types": ["ast-types@0.16.3", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-FvWoWYfSCM6kRxCSH+MGLHIKKGRL6A6AW7Zek2O32REPQRdg131428uRTKMBYAeRd3XXAaHDS60Wpri7CdKDrA=="], - - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - - "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], - - "atomically": ["atomically@1.7.0", "", {}, "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w=="], - - "aws4": ["aws4@1.13.2", "", {}, "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw=="], - - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="], - - "bcp-47-match": ["bcp-47-match@2.0.3", "", {}, "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], - - "bidi-js": ["bidi-js@1.1.0", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg=="], - - "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], - - "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], - - "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], - - "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="], - - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], - - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - - "builder-util": ["builder-util@26.15.3", "", { "dependencies": { "@types/debug": "^4.1.6", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw=="], - - "builder-util-runtime": ["builder-util-runtime@9.7.0", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw=="], - - "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "bytestreamjs": ["bytestreamjs@2.0.1", "", {}, "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ=="], - - "cac": ["cac@7.0.0", "", {}, "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ=="], - - "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], - - "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], - - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - - "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - - "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], - - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], - - "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], - - "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], - - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - - "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "cn": ["cn@0.2.6", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-+i4L0zUGgRcEnhsxueVrP7iBGxBx5iD0WOTYg1MwFEu2ZyCmH5Ov2V1cul2Ht5UchRQQgftCf4be/RxspuW6QQ=="], - - "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], - - "codem-isoboxer": ["codem-isoboxer@0.3.10", "", {}, "sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - - "commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - - "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "conf": ["conf@10.2.0", "", { "dependencies": { "ajv": "^8.6.3", "ajv-formats": "^2.1.1", "atomically": "^1.7.0", "debounce-fn": "^4.0.0", "dot-prop": "^6.0.1", "env-paths": "^2.2.1", "json-schema-typed": "^7.0.3", "onetime": "^5.1.2", "pkg-up": "^3.1.0", "semver": "^7.3.5" } }, "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg=="], - - "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - - "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - - "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - - "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - - "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - - "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], - - "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], - - "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "cssstyle": ["cssstyle@6.2.0", "", { "dependencies": { "@asamuzakjp/css-color": "^5.0.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.28", "css-tree": "^3.1.0", "lru-cache": "^11.2.6" } }, "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig=="], - - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - - "dashjs": ["dashjs@5.2.1", "", { "dependencies": { "@svta/cml-608": "1.0.2", "@svta/cml-cmcd": "2.3.2", "@svta/cml-cmsd": "1.0.6", "@svta/cml-dash": "1.0.6", "@svta/cml-id3": "1.0.6", "@svta/cml-request": "1.0.12", "@svta/cml-xml": "1.1.4", "bcp-47-match": "^2.0.3", "codem-isoboxer": "0.3.10", "fast-deep-equal": "3.1.3", "html-entities": "^2.6.0", "imsc": "^1.1.5", "localforage": "^1.10.0", "path-browserify": "^1.0.1" } }, "sha512-axcJmLeJCTJMLUfQRErUJB1KOiU5iHF+DMLBc3FKeox1HPYtGUQlC6Xm5Mm6vhBUR3CHHLIPOqw0JDIqMtP+Dw=="], - - "data-urls": ["data-urls@7.0.0", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="], - - "debounce-fn": ["debounce-fn@4.0.0", "", { "dependencies": { "mimic-fn": "^3.0.0" } }, "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - - "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], - - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], - - "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], - - "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], - - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], - - "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], - - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], - - "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], - - "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], - - "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], - - "dmg-builder": ["dmg-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0" } }, "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ=="], - - "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], - - "dot-prop": ["dot-prop@6.0.1", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA=="], - - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - - "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], - - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], - - "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], - - "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - - "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], - - "electron": ["electron@44.3.0", "", { "dependencies": { "@electron-internal/extract-zip": "^1.0.1", "@electron/get": "^5.0.0", "@types/node": "^24.9.0" }, "bin": { "electron": "cli.js", "install-electron": "install.js" } }, "sha512-St9EV7F2VtYaYWD2qaAjBwUgKxx39eJOUsUJ5+/1113sqbVfNqv4Dbm/W1rN7qmYSPa+mWwR6yr+b7MfgjgVfQ=="], - - "electron-builder": ["electron-builder@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.15.3", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "./cli.js", "install-app-deps": "./install-app-deps.js" } }, "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA=="], - - "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.15.3", "", { "dependencies": { "app-builder-lib": "26.15.3", "builder-util": "26.15.3", "electron-winstaller": "5.4.0" } }, "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA=="], - - "electron-publish": ["electron-publish@26.15.3", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "aws4": "^1.13.2", "builder-util": "26.15.3", "builder-util-runtime": "9.7.0", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q=="], - - "electron-to-chromium": ["electron-to-chromium@1.5.426", "", {}, "sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g=="], - - "electron-updater": ["electron-updater@6.8.9", "", { "dependencies": { "builder-util-runtime": "9.7.0", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig=="], - - "electron-vite": ["electron-vite@6.0.0-beta.1", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-arrow-functions": "^7.27.1", "cac": "^7.0.0", "esbuild": "^0.25.11", "magic-string": "^0.30.21", "picocolors": "^1.1.1" }, "peerDependencies": { "@swc/core": "^1.0.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@swc/core"], "bin": { "electron-vite": "bin/electron-vite.js" } }, "sha512-jltST77AwNxIeTTDtYhnIEA8ZM0RW9jmSJcqS8x/dQcgeeLlxlcJoYNNJ1tv7/Swor0AbXMYteBGdezoAOt+Nw=="], - - "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], - - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - - "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], - - "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], - - "entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="], - - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - - "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], - - "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], - - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], - - "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - - "es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], - - "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], - - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], - - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - - "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - - "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], - - "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], - - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], - - "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - - "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - - "express-rate-limit": ["express-rate-limit@8.7.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="], - - "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], - - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], - - "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - - "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - - "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], - - "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], - - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], - - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], - - "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - - "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], - - "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - - "get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="], - - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - - "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - - "goober": ["goober@2.1.19", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg=="], - - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - - "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - - "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - - "hls.js": ["hls.js@1.7.2", "", {}, "sha512-CW/pPvSOFIRsosbwxrYaE9ERmpTo5fbTqL7wCvuCFlqW1Bmb1K5fXsY7yiH95rmlr4rVuA7UXHbM/cIXQ6AXwg=="], - - "hono": ["hono@4.13.7", "", {}, "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ=="], - - "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - - "html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], - - "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], - - "html-parse-stringify": ["html-parse-stringify@4.0.1", "", {}, "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw=="], - - "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - - "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - - "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - - "i18next": ["i18next@26.4.2", "", { "peerDependencies": { "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["typescript"] }, "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug=="], - - "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imsc": ["imsc@1.1.5", "", { "dependencies": { "sax": "1.2.1" } }, "sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ=="], - - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ip-address": ["ip-address@10.7.0", "", {}, "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA=="], - - "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - - "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], - - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - - "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], - - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - - "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], - - "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - - "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], - - "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], - - "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], - - "isbot": ["isbot@5.2.2", "", {}, "sha512-iQcBXcd+Rv/pkubRyGh2utW2j1oPG5hZY6TUhVPpqK4G+o3IbxpJNx04hgksjc/N7GK5pEorUxDeg31cFgEk/w=="], - - "isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], - - "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], - - "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - - "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], - - "jsdom": ["jsdom@28.1.0", "", { "dependencies": { "@acemir/cssom": "^0.9.31", "@asamuzakjp/dom-selector": "^6.8.1", "@bramus/specificity": "^2.4.2", "@exodus/bytes": "^1.11.0", "cssstyle": "^6.0.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.0", "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - - "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - - "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], - - "lie": ["lie@3.1.1", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="], - - "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], - - "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], - - "localforage": ["localforage@1.10.0", "", { "dependencies": { "lie": "3.1.1" } }, "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg=="], - - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - - "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], - - "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], - - "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], - - "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - - "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - - "lucide-react": ["lucide-react@1.44.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2egNApH4hX4j/qdCgRublh88+9u3mEhz9iSlW5ckm4kaQEqZbXbMr0l5u5JZLy8nmWRx2dbHGQkEDYz6C9aCgw=="], - - "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], - - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - - "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], - - "media-captions": ["media-captions@1.0.4", "", {}, "sha512-cyDNmuZvvO4H27rcBq2Eudxo9IZRDCOX/I7VEyqbxsEiD2Ei7UYUhG/Sc5fvMZjmathgz3fEK7iAKqvpY+Ux1w=="], - - "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], - - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - - "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - - "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - - "mimic-fn": ["mimic-fn@3.1.0", "", {}, "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ=="], - - "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], - - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - - "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - - "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - - "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], - - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - - "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - - "node-abi": ["node-abi@4.35.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q=="], - - "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], - - "node-gyp": ["node-gyp@12.4.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "nopt": "^9.0.0", "proc-log": "^6.0.0", "semver": "^7.3.5", "tar": "^7.5.4", "tinyglobby": "^0.2.12", "undici": "^6.25.0", "which": "^6.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw=="], - - "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], - - "node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="], - - "nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="], - - "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], - - "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], - - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], - - "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], - - "obug": ["obug@2.2.1", "", {}, "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q=="], - - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "open": ["open@11.0.2", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q=="], - - "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], - - "oxfmt": ["oxfmt@0.66.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.66.0", "@oxfmt/binding-android-arm64": "0.66.0", "@oxfmt/binding-darwin-arm64": "0.66.0", "@oxfmt/binding-darwin-x64": "0.66.0", "@oxfmt/binding-freebsd-x64": "0.66.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.66.0", "@oxfmt/binding-linux-arm-musleabihf": "0.66.0", "@oxfmt/binding-linux-arm64-gnu": "0.66.0", "@oxfmt/binding-linux-arm64-musl": "0.66.0", "@oxfmt/binding-linux-ppc64-gnu": "0.66.0", "@oxfmt/binding-linux-riscv64-gnu": "0.66.0", "@oxfmt/binding-linux-riscv64-musl": "0.66.0", "@oxfmt/binding-linux-s390x-gnu": "0.66.0", "@oxfmt/binding-linux-x64-gnu": "0.66.0", "@oxfmt/binding-linux-x64-musl": "0.66.0", "@oxfmt/binding-openharmony-arm64": "0.66.0", "@oxfmt/binding-win32-arm64-msvc": "0.66.0", "@oxfmt/binding-win32-ia32-msvc": "0.66.0", "@oxfmt/binding-win32-x64-msvc": "0.66.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-FfvqR8RFtV6JJpRrpkfqyVCQ7HDvZ/VriWFx7veftCgL1B5ZO9qNr+1rvPieycMQnNfVG0PWyJQiy7p0hq1I5w=="], - - "oxlint": ["oxlint@1.81.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.81.0", "@oxlint/binding-android-arm64": "1.81.0", "@oxlint/binding-darwin-arm64": "1.81.0", "@oxlint/binding-darwin-x64": "1.81.0", "@oxlint/binding-freebsd-x64": "1.81.0", "@oxlint/binding-linux-arm-gnueabihf": "1.81.0", "@oxlint/binding-linux-arm-musleabihf": "1.81.0", "@oxlint/binding-linux-arm64-gnu": "1.81.0", "@oxlint/binding-linux-arm64-musl": "1.81.0", "@oxlint/binding-linux-ppc64-gnu": "1.81.0", "@oxlint/binding-linux-riscv64-gnu": "1.81.0", "@oxlint/binding-linux-riscv64-musl": "1.81.0", "@oxlint/binding-linux-s390x-gnu": "1.81.0", "@oxlint/binding-linux-x64-gnu": "1.81.0", "@oxlint/binding-linux-x64-musl": "1.81.0", "@oxlint/binding-openharmony-arm64": "1.81.0", "@oxlint/binding-win32-arm64-msvc": "1.81.0", "@oxlint/binding-win32-ia32-msvc": "1.81.0", "@oxlint/binding-win32-x64-msvc": "1.81.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg=="], - - "oxlint-tsgolint": ["oxlint-tsgolint@7.0.2001", "", { "optionalDependencies": { "@oxlint-tsgolint/darwin-arm64": "7.0.2001", "@oxlint-tsgolint/darwin-x64": "7.0.2001", "@oxlint-tsgolint/linux-arm64": "7.0.2001", "@oxlint-tsgolint/linux-x64": "7.0.2001", "@oxlint-tsgolint/win32-arm64": "7.0.2001", "@oxlint-tsgolint/win32-x64": "7.0.2001" }, "bin": { "tsgolint": "./bin/tsgolint.js" } }, "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg=="], - - "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], - - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - - "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], - - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], - - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - - "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - - "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], - - "pkijs": ["pkijs@3.4.0", "", { "dependencies": { "@noble/hashes": "1.4.0", "asn1js": "^3.0.6", "bytestreamjs": "^2.0.1", "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw=="], - - "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - - "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], - - "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], - - "postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="], - - "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], - - "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], - - "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], - - "pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="], - - "proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="], - - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], - - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - - "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], - - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - - "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], - - "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], - - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], - - "pvutils": ["pvutils@1.2.0", "", {}, "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg=="], - - "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], - - "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], - - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], - - "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], - - "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], - - "react": ["react@19.3.0", "", {}, "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog=="], - - "react-dom": ["react-dom@19.3.0", "", { "dependencies": { "scheduler": "^0.28.0" }, "peerDependencies": { "react": "^19.3.0" } }, "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q=="], - - "react-i18next": ["react-i18next@17.0.13", "", { "dependencies": { "@babel/runtime": "^7.29.7", "html-parse-stringify": "^4.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q=="], - - "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - - "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], - - "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "recast": ["recast@0.23.21", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw=="], - - "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], - - "remotion": ["remotion@4.0.523", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-dtDmkgFNsy9Dbh4EdyFwa53/UC3EpAhV1VvAPjzNbijqaEDCuALF7jnycOe/grex8DKB64NGUP6PaCrs5dcDOw=="], - - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - - "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], - - "reselect": ["reselect@5.3.0", "", {}, "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg=="], - - "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], - - "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], - - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], - - "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - - "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], - - "rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="], - - "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - - "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - - "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - - "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], - - "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], - - "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], - - "scheduler": ["scheduler@0.28.0", "", {}, "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw=="], - - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - - "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], - - "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - - "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], - - "seroval": ["seroval@1.6.7", "", {}, "sha512-AeDcLh0yO2SFm9W71essgnSzLV9DI8ZH0x0knXn2DMnUZj728mpLbxjlbB6IqKCmqh8JA3cEqRyGoNkt584JcQ=="], - - "seroval-plugins": ["seroval-plugins@1.6.7", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4Nk35ttD3DTDJW4hgw5StsVAPeU6qnDFnULAouw6tQ7oLTV/ICXrWpsXo2EE52eSP2joUMazbVf52mFEcADqRw=="], - - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], - - "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - - "shadcn": ["shadcn@4.21.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "cn": "^0.2.4", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "kleur": "^4.1.5", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "socks": "^2.8.8", "stringify-object": "^5.0.0", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "undici": "^7.27.2", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-UU2mFNusW8C5rvadKdH69vERYZqUlOOlXBcf0MYhYLdTGP6DPti7X4qovCu+RTfCqsAgq/T+YfE0Vnttxh9aiw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], - - "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], - - "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - - "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], - - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], - - "socks": ["socks@2.8.10", "", { "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" } }, "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ=="], - - "sonner": ["sonner@2.0.8", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-UM/ByIoFra8yzV75n1o0Puu0bw5U/9UNnDacrJNspekBewIfsQ3D6ez1nvlWpt7aTsO6rujQtifBpycwIivqlg=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - - "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], - - "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - - "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], - - "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - - "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], - - "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - - "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], - - "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], - - "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - - "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], - - "systeminformation": ["systeminformation@5.33.10", "", { "os": "!aix", "bin": { "systeminformation": "lib/cli.js" } }, "sha512-/NXbMVASt2UbSVgmWto4aBTIAeuYY7zOELpZybmNxqpsrbo5uYHpuEPf5nkyrPng0uG5YOb+Yv6s0FyKY4mDQg=="], - - "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], - - "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - - "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], - - "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], - - "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], - - "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], - - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - - "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], - - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], - - "tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="], - - "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], - - "tldts": ["tldts@7.4.12", "", { "dependencies": { "tldts-core": "^7.4.12" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WylhSDKVeYnWXL3a+vKTaOxjnOeEGw938hImY8zoRWJjRRK/Jp1K+IihBzIONpUmW4e3WmXT6q5FW6vlESVZCA=="], - - "tldts-core": ["tldts-core@7.4.12", "", {}, "sha512-nYNzS2WRf4QJmjzFFgAxLOBjyBxAGRbCy9PVBPaglcYyYajh40VBn+v5Ngr96ZMc7oM0+aCJdtQnNejvdBnXMQ=="], - - "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], - - "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], - - "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - - "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], - - "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], - - "tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="], - - "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], - - "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], - - "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - - "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], - - "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - - "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - - "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], - - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], - - "unzipper": ["unzipper@0.12.5", "", { "dependencies": { "bluebird": "~3.7.2", "duplexer2": "~0.1.4", "fs-extra": "11.3.1", "graceful-fs": "^4.2.2", "node-int64": "^0.4.0" } }, "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A=="], - - "update-browserslist-db": ["update-browserslist-db@1.3.2", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw=="], - - "use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], - - "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], - - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - - "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="], - - "vite-plus": ["vite-plus@0.3.1", "", { "dependencies": { "@oxc-project/types": "=0.148.0", "@oxlint/plugins": "=1.79.0", "@vitest/browser": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "oxfmt": "=0.66.0", "oxlint": "=1.81.0", "oxlint-tsgolint": "=7.0.2001", "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1", "vitest": "4.1.11" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.1", "@voidzero-dev/vite-plus-darwin-x64": "0.3.1", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.1", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.1", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.1", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.1", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.1", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.1" }, "peerDependencies": { "@vitest/browser-playwright": "4.1.11", "@vitest/browser-webdriverio": "4.1.11" }, "optionalPeers": ["@vitest/browser-playwright", "@vitest/browser-webdriverio"], "bin": { "vp": "./bin/vp", "vpr": "./bin/vpr", "oxfmt": "./bin/oxfmt", "oxlint": "./bin/oxlint" } }, "sha512-U8KZ3c3mbX0qLfigx9rW2W9J73w9wjdf4s/s91H77ff5afqSYEdZd8Or41Jl99kD42a+UCL3LFXzb5b+bIBCpQ=="], - - "vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="], - - "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], - - "wavesurfer.js": ["wavesurfer.js@7.12.11", "", {}, "sha512-Sx0yZIz7jRJ9J9p7UL+pl9y0UQBB6UN/XNo+R7Gy4tCR5xZI9jMyA0dnt1R8TMVSQ4LZ10SeB2HJYQ+dV2dPSA=="], - - "webcrypto-core": ["webcrypto-core@1.9.2", "", { "dependencies": { "@peculiar/asn1-schema": "^2.7.0", "@peculiar/json-schema": "^1.1.12", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q=="], - - "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], - - "whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="], - - "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="], - - "which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], - - "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - - "word-extractor": ["word-extractor@1.0.4", "", { "dependencies": { "saxes": "^5.0.1", "yauzl": "^2.10.0" } }, "sha512-PyAGZQ2gjnVA5kcZAOAxoYciCMaAvu0dbVlw/zxHphhy+3be8cDeYKHJPO8iedIM3Sx0arA/ugKTJyXhZNgo6g=="], - - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], - - "wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="], - - "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], - - "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - - "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], - - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - - "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], - - "yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], - - "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "yocto-spinner": ["yocto-spinner@1.2.2", "", { "dependencies": { "yoctocolors": "^2.1.1" } }, "sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng=="], - - "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], - - "yuku-ast": ["yuku-ast@0.9.5", "", { "dependencies": { "@yuku-toolchain/types": "^0.9.5" } }, "sha512-Q8qW8WwQnN5Cm0ZZivdRIfv0sRLTjUq0YumXJkw8CYN1aCdICH9rk4C47/4n6kA5EqDtlj+F608twoTSV2MwdQ=="], - - "yuku-codegen": ["yuku-codegen@0.9.5", "", { "dependencies": { "@yuku-toolchain/types": "^0.9.5" }, "optionalDependencies": { "@yuku-codegen/binding-android-arm64": "0.9.5", "@yuku-codegen/binding-darwin-arm64": "0.9.5", "@yuku-codegen/binding-darwin-x64": "0.9.5", "@yuku-codegen/binding-freebsd-x64": "0.9.5", "@yuku-codegen/binding-linux-arm-gnu": "0.9.5", "@yuku-codegen/binding-linux-arm-musl": "0.9.5", "@yuku-codegen/binding-linux-arm64-gnu": "0.9.5", "@yuku-codegen/binding-linux-arm64-musl": "0.9.5", "@yuku-codegen/binding-linux-x64-gnu": "0.9.5", "@yuku-codegen/binding-linux-x64-musl": "0.9.5", "@yuku-codegen/binding-win32-arm64": "0.9.5", "@yuku-codegen/binding-win32-x64": "0.9.5" } }, "sha512-zGUVyDpSK4b6c9B0yyxi2P0wujn1XZTbG9dCb7d6gyAoP63EMgyLzUwPUvwxPG5jgYqhlI7w+S3oCyapqGr4PA=="], - - "yuku-parser": ["yuku-parser@0.9.5", "", { "dependencies": { "@yuku-toolchain/types": "^0.9.5", "yuku-ast": "^0.9.5" }, "optionalDependencies": { "@yuku-parser/binding-android-arm64": "0.9.5", "@yuku-parser/binding-darwin-arm64": "0.9.5", "@yuku-parser/binding-darwin-x64": "0.9.5", "@yuku-parser/binding-freebsd-x64": "0.9.5", "@yuku-parser/binding-linux-arm-gnu": "0.9.5", "@yuku-parser/binding-linux-arm-musl": "0.9.5", "@yuku-parser/binding-linux-arm64-gnu": "0.9.5", "@yuku-parser/binding-linux-arm64-musl": "0.9.5", "@yuku-parser/binding-linux-x64-gnu": "0.9.5", "@yuku-parser/binding-linux-x64-musl": "0.9.5", "@yuku-parser/binding-win32-arm64": "0.9.5", "@yuku-parser/binding-win32-x64": "0.9.5" } }, "sha512-IBnAdNVMswWbJcM7woPluOudectJzFjJ1N8jVRxP9Uq4CIv5hZdBIcdmtRbFDYF/Ph2j2gVup4YJ4N9mh0jYFA=="], - - "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - - "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], - - "@dotenvx/dotenvx/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], - - "@dotenvx/dotenvx/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - - "@dotenvx/dotenvx/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - - "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], - - "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], - - "@electron/asar/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/get/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], - - "@electron/universal/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - - "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - - "@electron/windows-sign/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - - "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@modelcontextprotocol/sdk/zod": ["zod@4.6.1", "", {}, "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q=="], - - "@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@tanstack/pacer/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.5.0", "", { "bin": { "intent": "./bin/intent.js" } }, "sha512-H+OH3zC6Vhu/K0NaVfQKknEKawc/+2PT+D3SB3Ox0V8SiMlTo0abbmH2rH0721R2aNYbjdMXA1oENOd8E2UVoA=="], - - "@tanstack/react-router/@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], - - "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="], - - "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], - - "@vitest/browser/pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], - - "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], - - "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], - - "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - - "cacheable-request/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], - - "conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "conf/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="], - - "conf/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "cosmiconfig/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "dir-compare/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "dot-prop/is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], - - "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], - - "enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], - - "global-agent/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "imsc/sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="], - - "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - - "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], - - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - - "node-abi/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "node-api-version/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "node-gyp/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "node-gyp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "node-gyp/undici": ["undici@6.28.1", "", {}, "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA=="], - - "node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="], - - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - - "onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "ora/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - - "p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "pkg-up/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], - - "pkijs/@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="], - - "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - - "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], - - "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - - "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - - "rolldown/@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="], - - "shadcn/fs-extra": ["fs-extra@11.4.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA=="], - - "simple-update-notifier/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi/ansi-regex": ["ansi-regex@6.3.0", "", {}, "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ=="], - - "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - - "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - - "unzipper/fs-extra": ["fs-extra@11.3.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g=="], - - "vite-plus/vite": ["@voidzero-dev/vite-plus-core@0.3.1", "", { "dependencies": { "@oxc-project/runtime": "=0.148.0", "@oxc-project/types": "=0.148.0", "lightningcss": "^1.33.0", "postcss": "^8.5.6", "yuku-codegen": "^0.9.3", "yuku-parser": "^0.9.3" }, "optionalDependencies": { "@voidzero-dev/vite-plus-darwin-arm64": "0.3.1", "@voidzero-dev/vite-plus-darwin-x64": "0.3.1", "@voidzero-dev/vite-plus-linux-arm64-gnu": "0.3.1", "@voidzero-dev/vite-plus-linux-arm64-musl": "0.3.1", "@voidzero-dev/vite-plus-linux-x64-gnu": "0.3.1", "@voidzero-dev/vite-plus-linux-x64-musl": "0.3.1", "@voidzero-dev/vite-plus-win32-arm64-msvc": "0.3.1", "@voidzero-dev/vite-plus-win32-x64-msvc": "0.3.1", "fsevents": "~2.3.3" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "publint": "^0.3.8", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0", "unplugin-unused": ">=0.5.0", "unrun": "*", "yaml": "^2.4.2" }, "optionalPeers": ["@arethetypeswrong/core", "@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "publint", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "typescript", "unplugin-unused", "unrun", "yaml"] }, "sha512-3uVTyZzrLWKV3aIjRPuqkPwzG0iNQIncJlCDIT/gXFk0FQOW5US9bSHf3jsDqD+NjgTP1vAfI99MZuYTUjEDbg=="], - - "word-extractor/saxes": ["saxes@5.0.1", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "wsl-utils/is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], - - "wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - - "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], - - "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - - "@dotenvx/dotenvx/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - - "@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], - - "@dotenvx/dotenvx/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "@electron/asar/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], - - "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], - - "@tailwindcss/node/lightningcss/lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], - - "@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], - - "@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], - - "@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], - - "@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], - - "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], - - "@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], - - "@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], - - "@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], - - "@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], - - "@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - - "@tanstack/react-router/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], - - "app-builder-lib/@electron/get/env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - - "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - - "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "dir-compare/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], - - "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - - "filelist/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="], - - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "glob/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], - - "hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - - "node-gyp/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="], - - "ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - - "pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], - - "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], - - "qrcode/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], - - "qrcode/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], - - "@electron/asar/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "@electron/universal/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - - "dir-compare/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "filelist/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], - - "pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], - - "qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - - "pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - } -} diff --git a/scripts/bun.lock b/scripts/bun.lock deleted file mode 100644 index e6efde49a..000000000 --- a/scripts/bun.lock +++ /dev/null @@ -1,19 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "omnivoice-promo-recorder", - "dependencies": { - "playwright": "^1.43.0", - }, - }, - }, - "packages": { - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - - "playwright": ["playwright@1.59.1", "", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], - - "playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], - } -} diff --git a/scripts/package.json b/scripts/package.json deleted file mode 100644 index f7fefdd37..000000000 --- a/scripts/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "omnivoice-promo-recorder", - "version": "1.0.0", - "type": "module", - "scripts": { - "record": "node record_promo.js" - }, - "dependencies": { - "playwright": "^1.43.0" - } -} diff --git a/scripts/record_promo.js b/scripts/record_promo.js deleted file mode 100644 index 797d9a445..000000000 --- a/scripts/record_promo.js +++ /dev/null @@ -1,179 +0,0 @@ -import { chromium } from 'playwright'; -import { execSync } from 'child_process'; -import path from 'path'; -import fs from 'fs'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const URL = 'http://localhost:5173'; -const OUTPUT_DIR = path.join(__dirname, '..', 'pics'); -const RAW_VIDEO = path.join(OUTPUT_DIR, 'raw_record.webm'); -const FINAL_MP4 = path.join(OUTPUT_DIR, 'promo.mp4'); -const FINAL_GIF = path.join(OUTPUT_DIR, 'promo.gif'); - -// Ensure output directory exists -if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); -} - -async function run() { - console.log('🎬 Starting Cinematic Promo Recording...'); - - const browser = await chromium.launch({ - headless: false, - args: [ - '--window-size=1920,1080', - '--force-device-scale-factor=2', // retina - '--disable-font-subpixel-positioning', - '--disable-smooth-scrolling' - ] - }); - - const context = await browser.newContext({ - viewport: { width: 1920, height: 1080 }, - deviceScaleFactor: 2, - recordVideo: { - dir: OUTPUT_DIR, - size: { width: 1920, height: 1080 } - } - }); - - const page = await context.newPage(); - let recordingOffsetSeconds = 0; - const scriptStartTime = Date.now(); - - // Helper for cinematic pausing - const wait = (ms) => new Promise(r => setTimeout(r, ms)); - - // Helper for smooth zoom injections - const cinematicZoom = async (selector, scale, xOffset = 0, yOffset = 0) => { - await page.evaluate(({ selector, scale, xOffset, yOffset }) => { - const el = document.querySelector('body'); - el.style.transition = 'transform 2s cubic-bezier(0.25, 1, 0.5, 1)'; - el.style.transformOrigin = `${xOffset}% ${yOffset}%`; - el.style.transform = `scale(${scale})`; - }, { selector, scale, xOffset, yOffset }); - }; - - const resetZoom = async () => { - await page.evaluate(() => { - const el = document.querySelector('body'); - el.style.transform = 'scale(1)'; - }); - }; - - try { - console.log('📍 Navigating to VoiceStudio Launchpad...'); - await page.goto(URL, { waitUntil: 'load' }); - - // Give it enough time to stabilize the layout - await wait(2500); - - console.log('📍 Switching to History tab in sidebar...'); - await page.locator('button:has-text("History")').first().click(); - await wait(1500); - - console.log('📍 Loading Project from History...'); - // Click the "Load" button on the first history item - await page.locator('button:has-text("Load")').first().click(); - - // Wait for the workspace to fully load, waveforms to draw, etc - console.log('⏳ Waiting for dubbing workspace to render...'); - await wait(3500); - - // 🎥 Cinematic Zoom on the Video Player - console.log('📍 Zooming on Video Playback...'); - // Assuming video is roughly in the top left/center of the main view - await cinematicZoom('.video-player-container', 1.4, 20, 30); - await wait(2000); - - // Play the video - console.log('📍 Playing timeline...'); - // Click play button (WaveSurfer play or native) - // Finding the play button by its lucide icon class or explicit text is best - // Using a broad selector for the play button in the waveform bar - const playBtn = page - .locator('button:has(.lucide-play), [aria-label="Playback controls"] button') - .first(); - if (await playBtn.isVisible()) { - await playBtn.click(); - } - - // Let it play for a bit while zoomed - await wait(6000); - - // Reset zoom and zoom into the segment table - console.log('📍 Highlighting Segment Controls...'); - await resetZoom(); - await wait(1500); - await cinematicZoom('.segment-table', 1.3, 80, 50); - await wait(3000); - - // Highlight Output Options - console.log('📍 Showing Export Options...'); - await resetZoom(); - await wait(1000); - await cinematicZoom('text=Output Options', 1.5, 50, 95); - await wait(3000); - - // Reset zoom - await resetZoom(); - await wait(1500); - - // Show Clone Tab - console.log('📍 Navigating Tabs (Clone, Design)...'); - await page.click('text=Clone'); - await wait(2500); - - // Show Design Tab - console.log('📍 Showing Voice Design...'); - await page.click('text=Design'); - await wait(2500); - - // Back to Dub - await page.click('text=Dub'); - await wait(2000); - - console.log('✅ Browser automation complete.'); - } catch (error) { - console.error('❌ Error during automation:', error); - } finally { - // Stop recording and close - const videoPath = await page.video().path(); - await context.close(); - await browser.close(); - - // Rename the raw recording - if (fs.existsSync(videoPath)) { - fs.renameSync(videoPath, RAW_VIDEO); - console.log(`\n💾 Raw recording saved to: ${RAW_VIDEO}`); - - console.log('✂️ Processing with FFmpeg (Trimming, Speeding, GIF Generation)...'); - try { - console.log(`✂️ Processing final MP4 without trimming...`); - - // Generate MP4 Promo (Lossless quality) - // preset=veryslow, crf=16 is virtually lossless - // Speed up the entire video slightly (0.85*PTS is about 15% faster) since UI interactions can feel slow on playback - execSync(`ffmpeg -y -i "${RAW_VIDEO}" -filter_complex "[0:v]setpts=0.85*PTS[v]" -map "[v]" -c:v libx264 -preset veryslow -crf 16 -pix_fmt yuv420p "${FINAL_MP4}"`, { stdio: 'inherit' }); - console.log(`\n✨ HIGH QUALITY MP4 generated: ${FINAL_MP4}`); - - // Generate GIF Promo (24fps, 1080p width, high quality palette) - execSync(`ffmpeg -y -i "${FINAL_MP4}" -vf "fps=24,scale=1080:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=255:stats_mode=diff[p];[s1][p]paletteuse=dither=bayer:bayer_scale=5" -loop 0 "${FINAL_GIF}"`, { stdio: 'inherit' }); - console.log(`✨ HIGH QUALITY GIF generated: ${FINAL_GIF}`); - - // Clean up raw - fs.unlinkSync(RAW_VIDEO); - - } catch (e) { - console.error('❌ FFmpeg processing failed. Please ensure ffmpeg is installed natively.'); - console.error(e.message); - } - - } - } -} - -run(); diff --git a/tests/test_bun_toolchain_pins.py b/tests/test_bun_toolchain_pins.py new file mode 100644 index 000000000..8f6450f2c --- /dev/null +++ b/tests/test_bun_toolchain_pins.py @@ -0,0 +1,93 @@ +"""One JavaScript toolchain: the bun in package.json, one lockfile, one range per dependency. + +CI, the security audit, and the Docker frontend build each drifted to their +own bun (unpinned, "1.2", `1-alpine`), a stale electron/bun.lock and a +standalone scripts/ package kept second dependency trees alive, and root and +Electron disagreed on TypeScript. Every consumer now follows package.json. +""" +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +import yaml + +_ROOT = Path(__file__).resolve().parents[1] +_WORKFLOWS = _ROOT / ".github" / "workflows" +_PACKAGE = json.loads((_ROOT / "package.json").read_text(encoding="utf-8")) + + +def _bun_version() -> str: + manager = _PACKAGE["packageManager"] + assert manager.startswith("bun@"), manager + return manager.removeprefix("bun@") + + +def _tracked(pattern: str) -> list[str]: + out = subprocess.run( + ["git", "ls-files", "--", pattern], cwd=_ROOT, capture_output=True, text=True, check=True + ).stdout + return [line for line in out.splitlines() if line] + + +def _setup_bun_steps(): + for path in sorted(_WORKFLOWS.glob("*.yml")): + data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} + for job_name, job in (data.get("jobs") or {}).items(): + for step in job.get("steps") or []: + if str(step.get("uses", "")).startswith("oven-sh/setup-bun@"): + yield f"{path.name}:{job_name}", step.get("with") or {} + + +def test_every_setup_bun_step_uses_the_package_manager_version(): + steps = list(_setup_bun_steps()) + assert steps, "expected at least one setup-bun step" + for where, with_ in steps: + if with_.get("bun-version-file") == "package.json": + continue + assert str(with_.get("bun-version")) == _bun_version(), ( + f"{where} must pin bun-version {_bun_version()!r} (package.json packageManager)" + ) + + +def test_docker_frontend_builder_uses_the_package_manager_version(): + dockerfile = (_ROOT / "deploy" / "Dockerfile").read_text(encoding="utf-8") + images = re.findall(r"^FROM\s+oven/bun:(\S+)", dockerfile, flags=re.MULTILINE) + assert images, "deploy/Dockerfile no longer builds with oven/bun" + for image in images: + tag = image.split("@", 1)[0] + assert tag in (_bun_version(), f"{_bun_version()}-alpine"), ( + f"deploy/Dockerfile uses oven/bun:{image}; pin {_bun_version()}-alpine" + ) + + +def test_the_root_bun_lock_is_the_only_lockfile(): + locks = _tracked("*bun.lock") + _tracked("*bun.lockb") + assert locks == ["bun.lock"], f"nested bun lockfiles drift from the workspace lock: {locks}" + + +def test_every_package_manifest_is_in_the_workspace(): + workspaces = set(_PACKAGE.get("workspaces", [])) + manifests = {str(Path(p).parent) for p in _tracked("*package.json")} + assert manifests == {"."} | workspaces, ( + f"package.json files outside the bun workspace get their own dependency tree: " + f"{sorted(manifests - {'.'} - workspaces)}" + ) + + +def test_workspace_manifests_agree_on_shared_dependency_ranges(): + def deps(manifest: dict) -> dict: + merged: dict = {} + for key in ("dependencies", "devDependencies", "optionalDependencies"): + merged.update(manifest.get(key) or {}) + return merged + + root = deps(_PACKAGE) + for workspace in _PACKAGE.get("workspaces", []): + member = deps(json.loads((_ROOT / workspace / "package.json").read_text(encoding="utf-8"))) + mismatched = { + name: (root[name], member[name]) for name in root.keys() & member.keys() if root[name] != member[name] + } + assert not mismatched, f"root and {workspace} declare different ranges: {mismatched}" From 5bd1a30f985a732151b02e8a73b9304e6ff185db Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:10:56 +0530 Subject: [PATCH 17/85] ci: run the frontend node:test suites in the required test job tests/frontend/*.test.mjs ran in no workflow, and apiClient.test.mjs had broken on an extensionless import that Node's type stripping cannot resolve. Import backendStage.ts explicitly and run test:frontend in Tests (backend + frontend). --- .github/workflows/ci.yml | 5 +++++ electron/src/shared/utils/backendLifecycle.ts | 2 +- tests/test_bun_toolchain_pins.py | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4e7d6f09..d11edce69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,6 +130,11 @@ jobs: # Docker build (deploy/Dockerfile), which is what reddened main on #485. run: bun install --frozen-lockfile + # node:test suites for shared renderer modules (tests/frontend/*.test.mjs). + # Node 22 strips TypeScript types natively; imports need explicit `.ts`. + - name: Frontend unit tests + run: bun run test:frontend + # Electron used to be built only after a release started, so renderer, # preload and packaging regressions could pass the required PR gate. # Keep this command shared with electron-release.yml through the root script. diff --git a/electron/src/shared/utils/backendLifecycle.ts b/electron/src/shared/utils/backendLifecycle.ts index 1e47d3290..ed02fe913 100644 --- a/electron/src/shared/utils/backendLifecycle.ts +++ b/electron/src/shared/utils/backendLifecycle.ts @@ -28,7 +28,7 @@ * The stage probe now returns `{ stage, message }` and callers surface it. */ -import { isBackendBusy } from './backendStage'; +import { isBackendBusy } from './backendStage.ts'; export type BackendLifecycleStage = 'ready' | 'starting' | 'failed' | 'unknown'; diff --git a/tests/test_bun_toolchain_pins.py b/tests/test_bun_toolchain_pins.py index 8f6450f2c..4629a90f7 100644 --- a/tests/test_bun_toolchain_pins.py +++ b/tests/test_bun_toolchain_pins.py @@ -91,3 +91,12 @@ def deps(manifest: dict) -> dict: name: (root[name], member[name]) for name in root.keys() & member.keys() if root[name] != member[name] } assert not mismatched, f"root and {workspace} declare different ranges: {mismatched}" + + +def test_the_required_test_job_runs_the_frontend_node_tests(): + """`bun run test:frontend` ran in no workflow, so its suites rotted unseen.""" + ci = yaml.safe_load((_WORKFLOWS / "ci.yml").read_text(encoding="utf-8")) + job = next(j for j in ci["jobs"].values() if j.get("name") == "Tests (backend + frontend)") + runs = [str(step.get("run", "")) for step in job["steps"]] + assert any("bun run test:frontend" in run for run in runs) + assert "test:frontend" in _PACKAGE["scripts"] From c644fb72f01dbcc580ba424e686a735b1ca19967 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:11:05 +0530 Subject: [PATCH 18/85] docs(deploy): name the ROCm 7.2.4 torch build in the constraints comment --- deploy/torch-constraints.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deploy/torch-constraints.txt b/deploy/torch-constraints.txt index b06d9f827..f1fa9156f 100644 --- a/deploy/torch-constraints.txt +++ b/deploy/torch-constraints.txt @@ -23,8 +23,9 @@ # install path stops passing --constraint. # # The versions carry no local segment on purpose: PEP 440 matches `==2.8.0` -# against `2.8.0+cu128` and `2.8.0+rocm6.4` alike, so one file serves the CUDA -# image, the ROCm image, and Colab without clobbering a vendor build. +# against `2.8.0+cu128` and the ROCm 7.2.4 base's `2.8.0+rocm7.2.4...` build +# alike, so one file serves the CUDA image, the ROCm image, and Colab without +# clobbering a vendor build. # # Side effect, deliberate: torch 2.8.0 publishes no cp314 wheels, so on Python # 3.14 the install now FAILS with an unsatisfiable-resolution message instead From 1605af5f1ad3b8f3cc77eac60acc07ac417200e0 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:13:10 +0530 Subject: [PATCH 19/85] build: drop the unused PyInstaller spec and runtime dependency backend.spec and its runtime hooks targeted the removed Tauri sidecar; no workflow, script or Electron packaging step runs them, yet every install pulled pyinstaller as a core dependency. Remove both, relock, and drop the tests that only asserted the spec's contents. --- backend.spec | 263 ------------------ backend/core/version.py | 3 +- backend/hooks/pyi_rth_numpy_compat.py | 28 -- .../hooks/pyi_rth_torch_compiler_disable.py | 21 -- backend/main.py | 4 +- pyproject.toml | 13 +- tests/test_app_version.py | 13 - tests/test_socks_proxy.py | 20 +- uv.lock | 81 ------ 9 files changed, 9 insertions(+), 437 deletions(-) delete mode 100644 backend.spec delete mode 100644 backend/hooks/pyi_rth_numpy_compat.py delete mode 100644 backend/hooks/pyi_rth_torch_compiler_disable.py diff --git a/backend.spec b/backend.spec deleted file mode 100644 index 20bd3f21e..000000000 --- a/backend.spec +++ /dev/null @@ -1,263 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- -# PyInstaller spec for VoiceStudio backend. -# -# Produces a one-folder bundle at dist/omnivoice-backend/ that Tauri launches -# as a sidecar binary. Kept intentionally permissive with collect_all(...) -# on the heavy ML deps because PyInstaller's static analysis misses their -# runtime-imported submodules, C extensions, and data files. -# -# Cross-platform: targets mac-ARM, mac-Intel, Linux x64, Windows x64. mlx -# deps are gated on mac-ARM (sys.platform=='darwin' + machine=='arm64') so -# PyInstaller on other hosts doesn't blow up trying to find mlx wheels. -# -# Run: uv run pyinstaller backend.spec --noconfirm --clean -import platform -import sys -from PyInstaller.utils.hooks import collect_data_files, collect_all, collect_submodules, copy_metadata - -IS_MAC_ARM = sys.platform == "darwin" and platform.machine() == "arm64" - -datas = [] -binaries = [] - -# Bundle the omnivoice package's .dist-info so importlib.metadata.version() -# resolves inside the frozen build. Without it the backend can't read its own -# version and falls back to the literal in backend/core/version.py — which is -# how a 0.3.6 desktop build shipped reporting "0.3.5" in About + bug reports. -datas += copy_metadata('omnivoice') -hiddenimports = [ - # Web stack - 'uvicorn', 'uvicorn.logging', 'uvicorn.loops', 'uvicorn.loops.auto', - 'uvicorn.protocols', 'uvicorn.protocols.http', 'uvicorn.protocols.http.auto', - 'uvicorn.protocols.websockets', 'uvicorn.protocols.websockets.auto', - 'uvicorn.lifespan', 'uvicorn.lifespan.on', - 'fastapi', 'fastapi.responses', 'starlette', - 'multipart', - # SOCKS proxy support (#959). httpx imports socksio lazily inside a - # try/except (only when a socks5:// proxy env var is set), so - # PyInstaller's static tracer never sees it — without this entry the - # frozen installers keep raising "Using SOCKS proxy, but the 'socksio' - # package is not installed" on every model load under a SOCKS proxy, - # even though pyproject.toml ships the package. Guarded by - # tests/test_socks_proxy.py. - 'socksio', - # Remote GPU workers (backend/worker/). The feature is opt-in, so every - # import of it is deliberately deferred to the moment it is switched on — - # inside `lifespan` and inside `ControlPlane.start()`. That keeps the cost - # off users who never enable it, but it also means a frozen build has no - # static import chain to follow, so the modules must be named here or the - # feature raises ModuleNotFoundError only in the installers. - 'grpc', 'grpc.aio', - 'worker.service', 'worker.agent', - 'worker.transport.server', 'worker.transport.client', - 'worker.protocol.gen.worker_v1_pb2', 'worker.protocol.gen.worker_v1_pb2_grpc', - - # Core - 'uuid', 'asyncio', - - # Audio / ML - 'torch', 'torchaudio', 'soundfile', 'scipy', 'numpy', - 'numpy.random._pickle', - - # Cross-platform primary ASR — WhisperX (faster-whisper + wav2vec2 - # alignment) is the default on every platform. faster-whisper is the - # transcription engine; WhisperX adds forced alignment for ±10-30 ms - # word timing, which directly improves dub lip-sync. Both backends are - # registered in asr_backend.py; the user can switch via Settings. - 'whisperx', 'whisperx.alignment', 'whisperx.asr', 'whisperx.diarize', - 'whisperx.vad', 'whisperx.audio', 'whisperx.utils', - 'faster_whisper', 'faster_whisper.transcribe', 'faster_whisper.audio', - 'faster_whisper.utils', 'faster_whisper.tokenizer', 'faster_whisper.vad', - 'ctranslate2', - - # Lightweight English TTS tier — ONNX-based, cross-platform. The ONNX - # Runtime wheels ship platform-specific .so/.dll/.dylib which collect_all - # picks up; the kittentts Python package is pure Python but has a couple - # of asset files the bundler needs to include. - 'kittentts', 'onnxruntime', - - # Pipeline - 'yt_dlp', 'demucs', 'demucs.separate', - - # Numbers→words for the pre-TTS text normalization pass - # (services/text_normalization.py). Imported inside a function (lazy), - # so pin it explicitly rather than trusting the tracer. - 'num2words', - - # OmniVoice's own package - 'omnivoice', 'omnivoice.models', 'omnivoice.models.omnivoice', -] - -if IS_MAC_ARM: - # MLX Whisper on Apple Silicon (optional speedup path). mlx's pure-Python - # submodules (nn, utils, …) are imported lazily by mlx_whisper at - # transcribe time and the plain dep tracer misses them. We deliberately - # do NOT collect_all() mlx because that double-registers mlx.core with - # nanobind and the binary aborts on the first mlx.core touch. - hiddenimports.append('mlx_whisper') - # Parakeet TDT v3 ASR (services.asr_backend.ParakeetMLXBackend) — imported - # lazily at is_available()/transcribe time, so the tracer misses it. Same - # rule as mlx_whisper: list the package, never collect_all() anything that - # touches nanobind-registered mlx.core. - hiddenimports.append('parakeet_mlx') - # mlx-audio engine multiplexer — Kokoro / CSM / Dia / Qwen3-TTS / - # Chatterbox / MeloTTS / OuteTTS / … — gives mac-ARM users a rich - # engine picker. Like mlx_whisper it's mac-ARM-only; also like - # mlx_whisper we list it here but avoid collect_all() because it - # depends on the same nanobind-registered mlx.core. - hiddenimports += [ - 'mlx_audio', 'mlx_audio.tts', 'mlx_audio.tts.utils', - 'mlx_audio.tts.models', 'mlx_audio.tts.generate', - 'mlx_audio.stt', 'mlx_audio.codec', - ] - # Kokoro's phonemizer (misaki) loads the spaCy model en_core_web_sm - # DYNAMICALLY (spacy.load by name), so PyInstaller never sees the import — - # a frozen build without it would hit misaki's in-process downloader at - # first English generation (#1133 class; contained since #1143, but the - # generation still degrades). It's a plain data-heavy package with no - # nanobind involvement, so collect_all is safe here (unlike mlx itself). - _sm_datas, _sm_bins, _sm_hidden = collect_all('en_core_web_sm') - datas += _sm_datas - binaries += _sm_bins - hiddenimports += _sm_hidden - -# Note: we deliberately DON'T enumerate mlx submodules here. Any variant of -# `collect_submodules('mlx')` or `collect_all('mlx')` — even filtered to -# exclude mlx.core — reliably re-triggers the nanobind duplicate-key error -# the first time anything imports mlx.core ("refusing to add duplicate key -# 'cpu' to enumeration mlx.core.DeviceType"). Shipping without mlx in the -# frozen bundle leaves mlx-whisper unavailable; asr_backend falls back to -# pytorch-whisper (slower but functional on Apple Silicon). Revisit once -# we have a minimal repro or a PyInstaller hook specifically for mlx. - -# The nuclear option on heavy ML libs — pull every submodule, C ext, and -# data file. Cost: bigger bundle. Benefit: we don't ship a binary that -# ImportErrors the first time a user hits a code path. -# Note: 'mlx' is intentionally NOT in this list. Calling collect_all('mlx') -# alongside collect_all('mlx_whisper') causes the nanobind binding init to -# run twice in the frozen bundle, crashing with -# "Critical nanobind error: refusing to add duplicate key 'cpu' -# to enumeration 'mlx.core.DeviceType'!" -# the first time anything imports mlx.core. mlx_whisper already depends on -# mlx and PyInstaller's dep tracer pulls the needed mlx submodules + the .so. -_collect_pkgs = [ - 'torch', 'torchaudio', 'soundfile', 'scipy', 'numpy', - 'omnivoice', 'demucs', 'yt_dlp', 'fastapi', 'uvicorn', - # Primary cross-platform ASR. collect_all pulls CTranslate2's bundled - # .so/.dylib/.dll plus its compiled kernel data. WhisperX ships its own - # pure-Python code + some asset files (e.g. language metadata). - 'whisperx', 'faster_whisper', 'ctranslate2', - # ONNX-based lightweight TTS. onnxruntime's collect_all pulls the - # platform-appropriate .so/.dll/.dylib + CUDA providers when present. - 'kittentts', 'onnxruntime', -] -if IS_MAC_ARM: - # Only attempt mlx_whisper collection on mac-ARM — no wheels exist for - # Linux/Windows/mac-Intel, so collect_all would fail on CI for those. - _collect_pkgs.append('mlx_whisper') - -for pkg in _collect_pkgs: - try: - tmp_datas, tmp_binaries, tmp_hidden = collect_all(pkg) - datas += tmp_datas - binaries += tmp_binaries - hiddenimports += tmp_hidden - except Exception as e: # noqa: BLE001 - print(f"[backend.spec] collect_all({pkg!r}) skipped: {e}") - -# Include the backend's own modules as data so imports like -# `api.routers.dub_generate` resolve inside the frozen bundle. -datas += [ - ('backend/api', 'api'), - ('backend/core', 'core'), - ('backend/services', 'services'), - ('backend/schemas', 'schemas'), - ('backend/migrations', 'migrations'), -] - -a = Analysis( - ['backend/main.py'], - pathex=['backend', '.'], - binaries=binaries, - datas=datas, - hiddenimports=hiddenimports, - hookspath=[], - hooksconfig={}, - runtime_hooks=[ - 'backend/hooks/pyi_rth_numpy_compat.py', - 'backend/hooks/pyi_rth_torch_compiler_disable.py', - ], - excludes=[ - # Desktop-only bloat the frozen backend never uses. - 'tkinter', 'matplotlib', 'PIL.ImageQt', 'PyQt5', 'PyQt6', - # CUDA / NVIDIA wheels on every platform — we ship CPU-only inference - # for the desktop app. Models download on first run via HF cache, and - # GPU use is surfaced only when a user-installed driver is detected - # at runtime. Excluding these saves ~2 GB per bundle, which is what - # keeps Linux .deb / Windows MSI under GH Releases' 2 GB asset cap. - 'nvidia', 'nvidia.cublas', 'nvidia.cudnn', 'nvidia.cuda_runtime', - 'nvidia.cuda_nvrtc', 'nvidia.nccl', 'nvidia.nvtx', - 'nvidia.curand', 'nvidia.cusolver', 'nvidia.cusparse', - 'nvidia.cufft', 'nvidia.cuda_cupti', 'nvidia.cusparselt', - 'nvidia.nvjitlink', 'nvidia.cufile', - 'triton', 'flash_attn', - # Torch internals we never invoke at inference time — distributed - # training, compile, FX tracing, tensorboard, testing helpers. These - # pull hundreds of MB of Python source + transitive deps. - 'torch.distributed', 'torch._dynamo', 'torch._inductor', - 'torch._export', 'torch.testing', 'torch.utils.tensorboard', - 'torch.utils.benchmark', 'torch.fx.experimental', - 'torch._functorch', 'torch.ao', 'torch.onnx', - # torchaudio prototype / deprecated — nothing in the backend touches - # these; removing saves tens of MB and silences the deprecation log - # noise on startup. - 'torchaudio.prototype', 'torchaudio.models.hifigan', - # Heavy optional deps that are in pyproject.toml but the Studio - # backend never imports (verified with `grep ^import`). Excluding - # keeps them out of the frozen bundle; nothing on the runtime path - # breaks. - 'gradio', 'gradio_client', 'tensorboardX', 'webdataset', - 's3prl', 'funasr', 'pedalboard', - # Test / example trees that get swept up by collect_all. - 'scipy.special.tests', 'scipy.tests', 'numpy.f2py.tests', - 'numpy.tests', 'numpy.testing.tests', - ], - noarchive=False, - # optimize=2 compiles the embedded stdlib + site-packages with -OO, - # stripping assert statements + docstrings. Saves ~50-80 MB on a bundle - # this size. Runtime impact is negligible because we never inspect - # docstrings at runtime. - optimize=2, -) - -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='omnivoice-backend', - debug=False, - bootloader_ignore_signals=False, - # strip=True removes debug symbols from ELF/Mach-O binaries (no-op on - # Windows since MSVC doesn't emit symbols in the same way). Saves - # 10-30% on native libraries like libtorch_cpu.so (~300 MB → ~220 MB). - strip=True, - upx=False, # UPX often corrupts ML native libs — disabled. - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) -coll = COLLECT( - exe, - a.binaries, - a.datas, - strip=True, - upx=False, - upx_exclude=[], - name='omnivoice-backend', -) diff --git a/backend/core/version.py b/backend/core/version.py index e084478d5..4825469b5 100644 --- a/backend/core/version.py +++ b/backend/core/version.py @@ -6,8 +6,7 @@ "0.3.5" because the *frozen* backend couldn't read its own metadata. Resolution order: - 1. installed package metadata — correct in any ``uv sync``'d env and, thanks - to ``copy_metadata('omnivoice')`` in ``backend.spec``, in the frozen build; + 1. installed package metadata — correct in any ``uv sync``'d env; 2. ``pyproject.toml`` walked up from this file — correct for a raw source checkout that was never installed; 3. ``_FALLBACK_VERSION`` — a last resort, kept in lockstep with the maintained diff --git a/backend/hooks/pyi_rth_numpy_compat.py b/backend/hooks/pyi_rth_numpy_compat.py deleted file mode 100644 index ffd0d35d4..000000000 --- a/backend/hooks/pyi_rth_numpy_compat.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Numpy compatibility shim for PyInstaller. - -Two things we have to guarantee in a frozen build: - -1. `numpy` gets imported before anything that monkey-touches its C API - (scipy, torch, torchaudio) so the extension loader runs in a clean - state and we don't hit "module compiled against API version X but - this version of numpy is Y" surprises. - -2. NUMPY_EXPERIMENTAL_ARRAY_FUNCTION is left at its default. Some - frozen builds end up with it unset because PyInstaller strips certain - env-based defaults — explicitly mirror numpy's own default so - downstream libs don't disable fast paths by accident. - -This hook runs BEFORE the main script via `runtime_hooks=[...]` in the -spec, so the import order is deterministic regardless of which library -the user's code touches first. -""" -import os - -os.environ.setdefault("NUMPY_EXPERIMENTAL_ARRAY_FUNCTION", "1") - -try: - import numpy # noqa: F401 (side-effect import: primes the C extension) -except ImportError: - # Let the main app fail loudly with its own error message rather - # than crashing the runtime hook itself. - pass diff --git a/backend/hooks/pyi_rth_torch_compiler_disable.py b/backend/hooks/pyi_rth_torch_compiler_disable.py deleted file mode 100644 index 1eeec8e01..000000000 --- a/backend/hooks/pyi_rth_torch_compiler_disable.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Disable torch.compile / dynamo / inductor in PyInstaller-frozen builds. - -`torch.compile` uses a mix of C++ ABI magic and source-file introspection -that breaks inside a frozen bundle: -- Dynamo's FX graph builder walks `__file__` paths that no longer exist - once the Python source is shipped as PYC inside the bundle. -- TorchInductor tries to read and JIT-compile additional C++ kernels at - runtime, relying on a compiler toolchain the end user's Mac won't have. -- Frozen modules load in a different order than the source tree, which - occasionally surfaces circular-import issues inside dynamo guards. - -Setting these env vars before torch is imported anywhere else is enough -to keep PyTorch on its eager path. Negligible perf hit for inference-only -workloads — we're not training here. -""" -import os - -os.environ.setdefault("TORCHDYNAMO_DISABLE", "1") -os.environ.setdefault("TORCH_COMPILE_DISABLE", "1") -os.environ.setdefault("TORCHINDUCTOR_DISABLE", "1") -os.environ.setdefault("PYTORCH_DISABLE_PER_OP_PROFILING", "1") diff --git a/backend/main.py b/backend/main.py index 145956644..3232b910f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -571,8 +571,8 @@ async def _cancel_and_await_tasks(*tasks, timeout: float = 3.0) -> None: def _phase_a_build() -> None: """Everything heavy that used to run at module scope, same relative - order per step. Idempotent. Imports are literal statements so - PyInstaller's tracer still sees them (backend.spec unchanged). + order per step. Idempotent. Imports are literal statements so static + import analysis still sees them. `_phase_a_finished` is set on EVERY exit — including the already-built early return — so a shutdown that observed `_phase_a_started` can never diff --git a/pyproject.toml b/pyproject.toml index 3b101e987..565565b34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,6 @@ dependencies = [ # with TypeError. whisperx tests against pyannote 3.3.2+, so we track # that range and revisit when whisperx releases a 4-compatible build. "pyannote-audio>=3.3.2,<4.0", - "pyinstaller>=6.19.0", "imageio-ffmpeg>=0.6.0", # Directly used by video_context; 12.1 adds get_flattened_data(), the # replacement for getdata() ahead of its Pillow 14 removal. @@ -179,9 +178,7 @@ dependencies = [ # model load/download 500'd for SOCKS-proxy users ("Using SOCKS proxy, but # the 'socksio' package is not installed"). Same failure shape for the # OpenAI SDK's client. Pure-Python, MIT, zero transitive deps, ~13 KB — - # identical on macOS/Windows/Linux. Also in backend.spec hiddenimports: - # httpx imports it lazily inside try/except, so PyInstaller's tracer - # misses it and frozen installers would stay broken without the entry. + # identical on macOS/Windows/Linux. "socksio>=1.0", # OS trust store for TLS (#976). Users behind a corporate/antivirus proxy # that TLS-inspects traffic get a raw "[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE]" @@ -204,13 +201,11 @@ dependencies = [ "en-core-web-sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl", # Remote GPU workers (backend/worker/). A core dependency rather than an # extra even though the feature is opt-in: an installer user who enables a - # remote worker in Settings cannot run `uv pip install` inside a frozen - # app bundle, so shipping the transport separately would make the feature + # remote worker in Settings cannot run `uv pip install` inside the + # installed app, so shipping the transport separately would make the feature # source-installs-only — a platform/packaging split the parity rule does # not allow. Ships prebuilt wheels for every target we build (cp311 - # macOS arm64/x86_64, Windows x64, manylinux) and PyInstaller has a - # first-party hook, so the frozen build needs no spec changes beyond the - # protobuf runtime already present transitively. + # macOS arm64/x86_64, Windows x64, manylinux). "grpcio>=1.60", "protobuf>=5.29.6", # Security floors for pyannote-audio's checkpoint loader (GHSA-qqmf-gpg7-g8gw). diff --git a/tests/test_app_version.py b/tests/test_app_version.py index e67757e47..b3da5a285 100644 --- a/tests/test_app_version.py +++ b/tests/test_app_version.py @@ -80,16 +80,3 @@ def test_fallback_version_resolves_to_pyproject(): (root / "pyproject.toml").read_text(encoding="utf-8"), ).group(1) assert _fallback_version() == pyproject - - -def test_frozen_build_collects_package_metadata(): - """backend.spec must copy_metadata('omnivoice') so the frozen backend reads - its real version via importlib.metadata instead of the fallback literal.""" - from pathlib import Path - - spec = (Path(__file__).resolve().parents[1] / "backend.spec").read_text( - encoding="utf-8" - ) - assert ( - "copy_metadata('omnivoice')" in spec or 'copy_metadata("omnivoice")' in spec - ), "backend.spec must copy_metadata('omnivoice') (frozen-build version reporting)" diff --git a/tests/test_socks_proxy.py b/tests/test_socks_proxy.py index 3ec78ca64..a85bca810 100644 --- a/tests/test_socks_proxy.py +++ b/tests/test_socks_proxy.py @@ -10,9 +10,8 @@ longer masked the proxy env). Three layers, each covered here: - (a) ship socksio — pyproject dependency + backend.spec hiddenimports - (httpx imports it lazily in try/except, so PyInstaller's tracer misses - it: nothing imports it statically, hence the recurrence guard); + (a) ship socksio — pyproject dependency (httpx imports it lazily in + try/except, so nothing imports it statically, hence the recurrence guard); (b) cache-first model resolution — a complete local cache resolves with ``local_files_only=True`` (no HTTP session constructed), so no session-construction failure can break synthesis of an installed model; @@ -55,21 +54,6 @@ def test_socksio_declared_in_pyproject_dependencies(): ) -def test_socksio_in_backend_spec_hiddenimports(): - # httpx imports socksio lazily inside try/except — PyInstaller's static - # tracer never sees it, so a pyproject dep alone leaves the FROZEN - # installers broken. Comments are stripped so a mention in a comment - # can't satisfy the check. - code_lines = [ - line.split("#", 1)[0] - for line in (PROJECT_ROOT / "backend.spec").read_text().splitlines() - ] - assert any("'socksio'" in line or '"socksio"' in line for line in code_lines), ( - "#959 regression: 'socksio' must be listed in backend.spec " - "hiddenimports or the frozen installers ship without SOCKS support." - ) - - def test_httpx_client_constructs_under_socks_proxy_env(monkeypatch): # Construction only — no network. FAILS (ImportError) in an env without # socksio; passes once (a) ships it. diff --git a/uv.lock b/uv.lock index 179225f35..4e5eaa53c 100644 --- a/uv.lock +++ b/uv.lock @@ -253,15 +253,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/5c/0132193d7da2c735669a1ed103b142fd63c9455984d48c5a88a1a516efaa/aliyun_python_sdk_kms-2.16.5-py2.py3-none-any.whl", hash = "sha256:24b6cdc4fd161d2942619479c8d050c63ea9cd22b044fe33b60bbb60153786f0", size = 99495, upload-time = "2024-08-30T09:01:18.462Z" }, ] -[[package]] -name = "altgraph" -version = "0.17.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, -] - [[package]] name = "annotated-doc" version = "0.0.5" @@ -2965,18 +2956,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/c6/32d68bfbf1d0c36888530ef6fd72864861af23dc546302b41033471a8c3a/llvmlite-0.49.0-cp314-cp314t-win_amd64.whl", hash = "sha256:be637e465010bc9c50f070468f7f1cf5385e92fee364d192dd5e6cea790ecba9", size = 42986602, upload-time = "2026-08-11T16:25:57.69Z" }, ] -[[package]] -name = "macholib" -version = "1.16.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "altgraph" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, -] - [[package]] name = "mako" version = "1.4.1" @@ -3960,7 +3939,6 @@ dependencies = [ { name = "psutil" }, { name = "pyannote-audio" }, { name = "pydub" }, - { name = "pyinstaller" }, { name = "pypdf" }, { name = "python-multipart" }, { name = "pytorch-lightning" }, @@ -4063,7 +4041,6 @@ requires-dist = [ { name = "psutil", specifier = ">=7.2.2" }, { name = "pyannote-audio", specifier = ">=3.3.2,<4.0" }, { name = "pydub" }, - { name = "pyinstaller", specifier = ">=6.19.0" }, { name = "pypdf", specifier = ">=6.19.0" }, { name = "python-multipart", specifier = ">=0.0.31" }, { name = "pytorch-lightning", specifier = ">=2.6.6" }, @@ -4380,15 +4357,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/70/1c7feb9ba1285c45136a1a90b909558f377339c872df213458a7167edf89/pedalboard-0.9.20-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e3bbbdf62c1558ff8ed2170660a33e7e6cb532fa7a32bc546546d62df8956e7", size = 4814919, upload-time = "2026-01-12T20:45:17.971Z" }, ] -[[package]] -name = "pefile" -version = "2024.8.26" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, -] - [[package]] name = "phonemizer-fork" version = "3.3.2" @@ -5105,46 +5073,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] -[[package]] -name = "pyinstaller" -version = "6.22.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "altgraph" }, - { name = "macholib", marker = "sys_platform == 'darwin'" }, - { name = "packaging" }, - { name = "pefile", marker = "sys_platform == 'win32'" }, - { name = "pyinstaller-hooks-contrib" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/2b/836d9def811c02522e0921d8b8cdf0c16b0545a216e97e71041758057859/pyinstaller-6.22.2.tar.gz", hash = "sha256:89b65a3ad07d9dd5832253e37bc45f31872d10d7f9d5c9fd0fdd6088a83829dd", size = 4092631, upload-time = "2026-08-17T20:53:22.231Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/39/08cd53632276de70426e7c273820277a48253fee397b4048301ec03c3566/pyinstaller-6.22.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ebd1b1ca932d7cf25d7366ce691aaf79a5ff9425811ed7328b5116e4471b6d6d", size = 1062734, upload-time = "2026-08-17T20:52:17.635Z" }, - { url = "https://files.pythonhosted.org/packages/74/22/2d865896782cbb41e2388c7314207c17a98acdcc1b8e5eef668873505c9f/pyinstaller-6.22.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f5ccb847451df4207bce18bf53a57b124c9bb4e7e4bad08c5ecc627bcf00b28c", size = 755697, upload-time = "2026-08-17T20:52:21.676Z" }, - { url = "https://files.pythonhosted.org/packages/91/2b/6c11e4d5a76e716ea68da1946ab706a4bdf6d8e68b0e1dea314366d046a0/pyinstaller-6.22.2-py3-none-manylinux2014_i686.whl", hash = "sha256:becb47ad78272bede87acf2ed830d7545a8f65ab11d06a68fe2b99ba1afaac6d", size = 768870, upload-time = "2026-08-17T20:52:25.511Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e2/dbfea6a58b68acf644f7193b11d570476d6ffaed57381b6d1dd9e898a971/pyinstaller-6.22.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:06d7b3827a8049db4a2d47e3ec4ae2f69a1041577d8833b8f169745cde573ab4", size = 767445, upload-time = "2026-08-17T20:52:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/5e/db/f24a21af2f87ce1df4e07fdad95eec65ef9f284267a7c1e5eeea40f49aa4/pyinstaller-6.22.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:7bee432404eca5dc3ef37c36811c266561b03988f6d3e70ab0fa5352dd5de9e4", size = 762113, upload-time = "2026-08-17T20:52:34.025Z" }, - { url = "https://files.pythonhosted.org/packages/2b/07/b304ff3f5f8333778065e3658b604b0e108cd934b920f3fbab825a0dd5b7/pyinstaller-6.22.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9622686ecc5d5fa492fe6cde29d47df9dd41138cff8177be9f901ca3260f2096", size = 762173, upload-time = "2026-08-17T20:52:38.477Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9b/0af69d93dfad2e3d590a5de5828d5bcd903747c0224bd9560ab476904dfe/pyinstaller-6.22.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:0260eaad6be3f6fbc1affffe6dc7b8e5b636dbca51b463224daba971610b6fc1", size = 761674, upload-time = "2026-08-17T20:52:42.646Z" }, - { url = "https://files.pythonhosted.org/packages/d3/4e/28b6094dcbd1e1bbb868daf4f8752999a20c1020ab64569232be42af2be8/pyinstaller-6.22.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c4b14caad38c1f3df8e9bf5276fc265e27fe8b4180cab4a37864288427bc0", size = 761053, upload-time = "2026-08-17T20:52:46.594Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2c/f91b63fd01422111ac04e3b9bf61c039da5a3292acb4fe5248905f0be688/pyinstaller-6.22.2-py3-none-win32.whl", hash = "sha256:9a078877caa3920558a3242d0a7019fe5b825cff4b65ed380c7d1e1bd200ddd9", size = 1344474, upload-time = "2026-08-17T20:52:53.108Z" }, - { url = "https://files.pythonhosted.org/packages/3f/53/8ba1d0f6159b490f700eac6161a4be5f0d4672608a6dae9fd73679f183ee/pyinstaller-6.22.2-py3-none-win_amd64.whl", hash = "sha256:9b990fa6bbe143572f06644a984ad0d7aa2e2ccc6929d4916031343a5888e9a7", size = 1405725, upload-time = "2026-08-17T20:52:59.667Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/9a3062cc34c939f694d4262c4754681f6ffe853c21c124209ad2d08b8e84/pyinstaller-6.22.2-py3-none-win_arm64.whl", hash = "sha256:afb6f9a95d19b6dcd3a7decc40d9adb6ba9c4f8802ddd6c972dfb552953f384e", size = 1353806, upload-time = "2026-08-17T20:53:06.222Z" }, -] - -[[package]] -name = "pyinstaller-hooks-contrib" -version = "2026.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/5b/c9fe0db5e83ee1c39b2258fa21d23b15e1a60786b6c5990ee5074ead8bb6/pyinstaller_hooks_contrib-2026.6.tar.gz", hash = "sha256:bef5002c32f4f50bd55b005da12cff64eca8783e7eaf86a06a62410164bab725", size = 173354, upload-time = "2026-06-08T22:37:16.152Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/31/f2d7343d8ed5f7c4678377886f6ce533e6eaaa131b252ce950114c2a7efa/pyinstaller_hooks_contrib-2026.6-py3-none-any.whl", hash = "sha256:fd13b8ac126b35361175edacd41a0d97080b75dd5f4b594ecefefff969509dd3", size = 457159, upload-time = "2026-06-08T22:37:14.722Z" }, -] - [[package]] name = "pyjwt" version = "2.15.0" @@ -5335,15 +5263,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, ] -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3" From 228fbb83611ef165d23a69ec70469e7106abaff8 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:12:06 +0530 Subject: [PATCH 20/85] docs(contributing): match setup, structure and checks to the repo Python 3.11 per pyproject, the two pytest runs CI uses, the Electron tree in place of the removed frontend/, the real Tailwind/shadcn styling and lint rules, _LAZY_REGISTRY plus the features.yaml inventory for new engines, both locale catalogs and their checks, and the precise list of sanctioned network calls (first-run setup, galleries, update checks). Roadmap and troubleshooting links now resolve from .github/. --- .github/CONTRIBUTING.md | 118 +++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 49 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5c1574ce3..469ddabd3 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -9,7 +9,7 @@ Thanks for your interest in improving VoiceStudio! This guide covers everything | 💬 **Chat** | [Discord](https://discord.gg/bzQavDfVV9) | | 🐛 **Bugs** | [GitHub Issues](https://github.com/debpalash/VoiceStudio/issues) | | 🏷️ **Good First Issues** | [Filtered list](https://github.com/debpalash/VoiceStudio/labels/good%20first%20issue) | -| 📋 **Roadmap** | [README → Roadmap](README.md#roadmap) | +| 📋 **Roadmap** | [docs/ROADMAP.md](../docs/ROADMAP.md) | --- @@ -29,7 +29,7 @@ Read it before opening a proposal; the licence check in particular ends most of - [uv](https://docs.astral.sh/uv/) (Python environment manager) - [ffmpeg](https://ffmpeg.org/) (audio/video processing) - [Rust / Cargo](https://rustup.rs/) (`native/desktop-bridge` and its imported Rust modules) -- Python 3.10+ (managed automatically by `uv`) +- Python 3.11+ (`requires-python` in `pyproject.toml`; managed automatically by `uv`) Linux desktop development needs the native helper libraries. On Debian or Ubuntu, install the same packages used by CI: @@ -66,12 +66,12 @@ bun run smoke-test -- --install # also install and start the managed backend bun run dev:web # maintained Electron renderer in a browser + backend ``` -The legacy browser command starts both services: +`bun run dev:web` starts both services: | Service | URL | What it does | |---------|-----|---| | **Backend** | `localhost:3900` | FastAPI server — TTS, ASR, diarization, dubbing pipeline | -| **Frontend** | `localhost:3901` | React + Vite UI | +| **Frontend** | `localhost:3901` | Electron renderer served by Vite in a browser | The backend runs through `scripts/dev-backend.mjs` (the `dev:api` script): the uvicorn command is unchanged, but if the backend **dies** (OOM kill, hard @@ -79,7 +79,7 @@ crash), the wrapper prints a boxed exit banner with the exit code/signal and the last 20 lines of `omnivoice.log` before the dev stack shuts down — so the cause doesn't scroll away with the terminal. The same death is also reported as a crash notice in the UI the next time the backend starts (see -[docs/install/troubleshooting.md §14c](docs/install/troubleshooting.md)). +[docs/install/troubleshooting.md §14c](../docs/install/troubleshooting.md)). ### Retired desktop (Tauri) @@ -98,19 +98,20 @@ VoiceStudio/ ├── backend/ # Python FastAPI server │ ├── api/ # Route handlers │ ├── core/ # Config, prefs, constants +│ ├── engines/ # Engines in their own module (lazy-registered) │ └── services/ # TTS engines, ASR, dubbing, audio DSP │ └── tts_backend.py # ← Multi-engine TTS registry -├── electron/ # Active Electron desktop: main, preload, renderer -├── native/ # Desktop helpers used by Electron -├── frontend/ # Transitional modules shared by Electron -│ ├── src/ -│ │ ├── components/ # UI components -│ │ ├── hooks/ # Custom React hooks -│ │ ├── stores/ # Zustand state slices -│ │ └── utils/ # Shared utilities -├── deploy/ # Docker, CI configs -├── docs/ # Screenshots, MCP config -└── scripts/ # Build & release scripts +├── electron/ # The only desktop and web UI +│ └── src/ +│ ├── main/ # Electron main process (IPC, updater, runtime) +│ ├── preload/ # Narrow renderer bridges +│ ├── renderer/src/ # App shell, features, shadcn ui/, i18n catalog +│ └── shared/ # Pages, components, hooks, Zustand store/, i18n +├── native/ # Rust desktop helper used by Electron +├── deploy/ # Dockerfile, compose, install worker +├── docs/ # User, install, release and design docs +├── scripts/ # Build, release and CI helper scripts +└── tests/ # Repo-wide pytest suite (CI: tests/ + backend/tests/) ``` --- @@ -132,8 +133,9 @@ Open an [issue](https://github.com/debpalash/VoiceStudio/issues/new) with: 2. **Keep PRs focused** — one feature or fix per PR 3. **Run tests** before pushing: ```bash - # Backend tests - uv run pytest backend/ -x -q + # Backend tests (the same two runs as CI) + uv run pytest tests/ -q + uv run pytest backend/tests/ -q # Frontend build check bun run check:electron @@ -173,8 +175,13 @@ class MyEngineBackend(TTSBackend): # ... call your engine, return [1, num_samples] tensor ``` -3. Register it in `_REGISTRY` at the bottom of the file -4. That's it — it auto-appears in Settings → TTS Engine +3. Register it at the bottom of the file: in-file classes go in `_REGISTRY`; + an engine that lives in its own `backend/engines/` module goes in + `_LAZY_REGISTRY` as `"id": ("engines.", "ClassName")` +4. Add the same id under `tts_engines` in `docs/features.yaml` with its README + string (`readme:`) or a doc page (`doc:`) — the daily docs-drift job + (`python scripts/check-docs-drift.py`) flags registry ids missing from it +5. It then appears in Settings → TTS Engine --- @@ -190,9 +197,9 @@ class MyEngineBackend(TTSBackend): ### JavaScript/React (Frontend) - **Components**: Functional components with hooks -- **State**: Zustand stores in `src/stores/`, organized by slice +- **State**: Zustand slices in `electron/src/shared/store/` - **Brand assets**: Reuse the canonical mark, palette, naming, and compatibility rules in [`docs/branding.md`](../docs/branding.md); do not redraw or rename runtime identifiers ad hoc -- **CSS**: **Utilities-first + shadcn/ui, one stylesheet.** UI is built on the shadcn/ui primitives in `src/components/ui/` (wrapped by the `src/ui/` barrel, themed to the VoiceStudio palette), composed with Tailwind v4 utility classes. **All styling now lives in a single file — `src/index.css`**: the `@theme` / `[data-theme]` token foundation plus the irreducible set utilities can't express (`@keyframes`, glassmorphism/`backdrop-filter`, pseudo-elements, `:has()`, unlayered cascade overrides, and styling hooks on library-generated DOM like virtualized rows / WaveSurfer). The per-component `.css` files were eliminated in the CSS→Tailwind/shadcn migration — **do not create new ones.** Reach for shadcn primitives + utilities; if a rule is genuinely irreducible, add it to `src/index.css` with a provenance comment. (The only other `.css` is the test-only visual harness. See `docs/shadcn-migration.md`.) +- **CSS**: Tailwind v4 utilities + the shadcn/ui primitives in `electron/src/renderer/src/components/ui/`, themed by the tokens in `electron/src/renderer/src/styles/` (`globals.css`, `t3-theme.css`). Prefer utilities; add or extend a co-located `.css` file only for what utilities cannot express (`@keyframes`, pseudo-elements, styling hooks on library-generated DOM). - **Naming**: `PascalCase` for components, `camelCase` for hooks and utils ### Rust (shared native helpers) @@ -207,22 +214,19 @@ class MyEngineBackend(TTSBackend): Frontend code stays modular so an edit loads one small file, not a 1900-line one. The rules: -- **Size caps:** **soft 300 lines**, **hard 500 lines** per `.jsx` file. - Anything over 500 lines must be split. (The cap does **not** apply to - `src/index.css` — it is the single, intentional styling foundation and the - only app stylesheet; see the CSS rule above.) +- **Size caps:** **soft 300 lines**, **hard 500 lines** per component file + (`.jsx`/`.tsx`). Anything over 500 lines must be split. - **Pages are thin orchestrators.** A file in `electron/src/shared/pages/` is just layout + routing + state wiring that composes feature components — no inline sub-component over ~50 lines. - **One component per file.** Co-locate `Foo.jsx` + `Foo.test.jsx` together in a per-page feature folder under `electron/src/shared/components/` (e.g. - `components/settings/`, `components/dub/`). Styling is **not** co-located — - it's utilities + shadcn, with any irreducible rules in `src/index.css`. + `components/settings/`, `components/dub/`). Style with utilities + shadcn + first (see the CSS rule above). - **Shared bits go in a `primitives/` folder** inside the feature folder (`components/settings/primitives/` is the existing example). -- **Enforced by ESLint `max-lines`** (`max: 500`) — **warn-only for now** so it - never breaks CI, with the goal of upgrading to `error` once the backlog of - oversized files clears. +- **Enforced in review, not by lint:** `bun run lint` (oxlint) has no + `max-lines` rule, so reviewers apply the caps. --- @@ -257,11 +261,12 @@ Cover delayed startup separately so runner scheduling does not masquerade as a native-call timeout or leak work into later tests. ```bash -# Run all backend tests -uv run pytest backend/ -x -q +# Run the backend suites CI runs (kept separate: backend/tests is isolated) +uv run pytest tests/ -q +uv run pytest backend/tests/ -q # Run a specific test file -uv run pytest backend/tests/test_api.py -x -q +uv run pytest tests/test_app_version.py -q # Electron desktop validation, from the repository root bun run check:electron @@ -272,8 +277,6 @@ cargo check --manifest-path native/desktop-bridge/Cargo.toml --- ---- - ## What code review looks like Every PR is reviewed by two AI reviewers before a human looks at it: @@ -315,9 +318,11 @@ hard rules from the first prompt. Platform-only features go behind an explicit opt-in (Settings toggle, env var, or CLI flag). - **i18n — all 21 locales (hard rule):** every user-facing string goes through - `t('...')` and the key must exist in **all 21** files under - `electron/src/shared/i18n/locales/`. Translate; don't copy English into non-English - locales. CI fails on hardcoded CJK outside the allowlist in + `t('...')` and the key must exist in **all 21** files of the catalog the code + uses: `electron/src/shared/i18n/locales/` (checked by + `tests/test_locale_parity.py`) or `electron/src/renderer/src/i18n/locales/` + (checked by `bun run --cwd electron locale:check`). Translate; don't copy + English into non-English locales. CI fails on hardcoded CJK outside the allowlist in `tests/test_no_hardcoded_cjk.py` (extend `_ALLOWED_FILES` with a justification for legitimate functional CJK). - **DB schema changes** go through an alembic migration with a tested upgrade @@ -325,16 +330,31 @@ hard rules from the first prompt. - **Engine back-compat:** already-installed engines (model weights on disk) must not require reinstall or re-download. - **Local-first:** no new outbound calls, and the app must work fully offline - with every prompt declined. The only sanctioned ones are: Hugging Face model - downloads (gated on install state or an explicit user action); bug reports - as prefilled GitHub Issue URLs opened in the user's browser; PostHog - analytics only after a yes at the first-run consent prompt - (`backend/core/analytics.py`, allowlisted content-free metadata); the - GitHub star count (no credentials or referrer, refreshed every 20 minutes - while shown); packaged-app update checks against GitHub Releases (downloads - wait for the user); and the Lemon Squeezy Pro licence check, only after the - user enters a key (`electron/src/main/pro-license.ts`). Adding to this list - needs owner approval. Never log or persist secrets or absolute home paths. + with every prompt declined. The only sanctioned ones are: + - **First-run setup the user starts:** the pinned, SHA-256-verified + ffmpeg/ffprobe download from GitHub (`zackees/ffmpeg_bins`) when no usable + copy is found (`backend/services/media_tools.py`); the huggingface.co vs + hf-mirror.com reachability probe that picks a download endpoint on + restricted networks, skipped when the user set an endpoint + (`backend/services/endpoint_race.py`); and Hugging Face model downloads + (gated on install state or an explicit user action). + - **Galleries the user opens or enables:** the community gallery manifest + from jsDelivr plus its GitHub-hosted previews + (`backend/api/routers/community.py`); the voice-preview gallery's signed + downloads from `omnivoice-gallery` GitHub Releases, only after it is turned + on in Settings (`backend/services/gallery.py`). + - **Packaged-app update checks** against GitHub Releases; downloads wait for + the user (`electron/src/main/updater.ts`). + - Bug reports as prefilled GitHub Issue URLs opened in the user's browser. + - PostHog analytics only after a yes at the first-run consent prompt + (`backend/core/analytics.py`, allowlisted content-free metadata). + - The GitHub star count (no credentials or referrer, refreshed every 20 + minutes while shown). + - The Lemon Squeezy Pro licence check, only after the user enters a key + (`electron/src/main/pro-license.ts`). + + Adding to this list needs owner approval. Never log or persist secrets or + absolute home paths. - **Security posture:** the backend serves loopback HTTP — treat every query/path/form parameter as hostile. User-chosen filesystem destinations are authorized in Electron main (native save dialog), never via HTTP params. From 33477f0745fac662d879be305177a444f2def797 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:12:25 +0530 Subject: [PATCH 21/85] fix(issues): route new bug and install reports to needs-triage The forms applied a bare triage label outside the documented map, so new reports skipped the needs-triage queue. install and sponsor are now documented area labels, and a test keeps template labels within the map. --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/install_problem.yml | 2 +- docs/agents/triage-labels.md | 12 ++++++-- tests/test_issue_template_labels.py | 33 ++++++++++++++++++++++ 4 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 tests/test_issue_template_labels.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 92b92676a..05f697d88 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,7 +1,7 @@ name: 🐛 Bug report description: Something works incorrectly or crashes (not a first-run/install problem — use the install template for those). title: "[Bug] " -labels: ["bug", "triage"] +labels: ["bug", "needs-triage"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/install_problem.yml b/.github/ISSUE_TEMPLATE/install_problem.yml index 003e8af3c..51305c06c 100644 --- a/.github/ISSUE_TEMPLATE/install_problem.yml +++ b/.github/ISSUE_TEMPLATE/install_problem.yml @@ -1,7 +1,7 @@ name: 🧩 Install / first-run problem description: The app won't install, set up, download models, or reach a first working output. title: "[Install] " -labels: ["install", "triage"] +labels: ["install", "needs-triage"] body: - type: markdown attributes: diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md index cfabf250e..d3a754a23 100644 --- a/docs/agents/triage-labels.md +++ b/docs/agents/triage-labels.md @@ -27,6 +27,12 @@ vocabulary on 2026-08-07. This repo also carries labels outside the triage vocabulary — `bug`, `enhancement`, `documentation`, `question`, `duplicate`, `invalid`, `good first issue`, `help wanted`, `roadmap`, `from-discord`, -`v0.3.0-investigate`. They classify *what* an issue is; the five above track -*where it is in the queue*. The two sets are orthogonal — applying a triage -label never means removing a type label. +`v0.3.0-investigate`, plus the area labels `install` (installer, setup and +first-run problems) and `sponsor` (sponsorship inquiries). They classify *what* +an issue is; the five above track *where it is in the queue*. The two sets are +orthogonal — applying a triage label never means removing a type label. + +Issue templates apply only labels from this file: the bug and install-problem +forms add `needs-triage` next to their type/area label; the feature and +sponsor forms add only their type/area label. Do not reintroduce a bare +`triage` label. diff --git a/tests/test_issue_template_labels.py b/tests/test_issue_template_labels.py new file mode 100644 index 000000000..73930ea00 --- /dev/null +++ b/tests/test_issue_template_labels.py @@ -0,0 +1,33 @@ +"""Issue templates may only apply labels documented in docs/agents/triage-labels.md. + +The bug and install forms once applied a bare ``triage`` label that no triage +skill or maintainer query used, so new reports never reached the +``needs-triage`` queue. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] + + +def test_issue_template_labels_are_documented(): + documented = set( + re.findall(r"`([^`]+)`", (ROOT / "docs/agents/triage-labels.md").read_text(encoding="utf-8")) + ) + undocumented = {} + for template in sorted((ROOT / ".github/ISSUE_TEMPLATE").glob("*.yml")): + labels = (yaml.safe_load(template.read_text(encoding="utf-8")) or {}).get("labels") or [] + if isinstance(labels, str): + labels = [label.strip() for label in labels.split(",")] + missing = [label for label in labels if label not in documented] + if missing: + undocumented[template.name] = missing + assert not undocumented, ( + f"Issue templates apply labels missing from docs/agents/triage-labels.md: {undocumented}" + ) + assert "needs-triage" in documented From f058ee854a5d63e98ae794f9b6d8474fb5891e20 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:13:14 +0530 Subject: [PATCH 22/85] docs(releasing): document the five-target matrix, analytics secrets and Docker on publish Adds Windows ARM64 (experimental) to the release matrix, the POSTHOG_PROJECT_TOKEN secret and POSTHOG_HOST variable, and states that Docker release tags build only when the GitHub Release is published while :latest tracks main alone. Drops the removed Tauri release.yml and the preview update channel; previews are builds from main or Docker :latest. --- docs/RELEASING.md | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/RELEASING.md b/docs/RELEASING.md index a4e580080..35228de4f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -15,10 +15,14 @@ Electron publication uses these GitHub Actions secrets: Windows Authenticode signing. The macOS certificate cannot sign Windows apps. - The repository `GITHUB_TOKEN` for draft creation and asset uploads. - `DOCKERHUB_USERNAME` and `DOCKERHUB_TOKEN` for the Docker Hub mirror. +- `POSTHOG_PROJECT_TOKEN` (secret) and `POSTHOG_HOST` (repository variable), + baked into packaged builds as the analytics token and host. They override the + in-repo publishable token; analytics still sends nothing without a yes at the + first-run consent prompt. The archived `TAURI_SIGNING_PRIVATE_KEY*` secrets are retained only so the final -Tauri release can be audited. Do not use `.github/workflows/release.yml` for a new -version. `TAURI_SUNSET_TAG` must continue to identify v0.5.3 so Electron releases +Tauri release can be audited. The Tauri release workflow has been removed; every +new version ships through `electron-release.yml`. `TAURI_SUNSET_TAG` must continue to identify v0.5.3 so Electron releases can copy the final signed `latest.json` and `latest-user.json` feeds unchanged. Those feeds must always point to immutable v0.5.3 Tauri assets; old Tauri clients must never receive an Electron installer. @@ -42,7 +46,9 @@ version only after validation. 1. Merge only after required CI and review are green. 2. Run the artifact-only Electron rehearsal, `.github/workflows/electron-build.yml`, - and inspect all four outputs: Linux x64, Windows x64, macOS arm64, macOS x64. + and inspect all five outputs: Linux x64, Windows x64, Windows ARM64, macOS + arm64, macOS x64. Windows ARM64 is experimental: a failed leg does not block + the other four. It first checks the setup screen, then installs and starts the managed Python runtime in a separate temporary profile on Linux, Windows and Apple Silicon. Intel Macs retain packaging/setup checks under their existing @@ -70,8 +76,9 @@ git tag vX.Y.Z git push origin vX.Y.Z ``` -The tag starts `.github/workflows/electron-release.yml`. It builds all four -platform targets, validates packaged startup and updater metadata, creates or +The tag starts `.github/workflows/electron-release.yml`. It builds the same five +targets (a failed experimental Windows ARM64 leg does not block the others, and +its feed is checked only when it was published), validates packaged startup and updater metadata, creates or updates a draft, and uploads: - Electron installers and platform-specific `electron-stable-*.yml` feeds @@ -79,7 +86,8 @@ updates a draft, and uploads: - authored CHANGELOG release notes - immutable copies of the final Tauri updater feeds for old clients -Tag pushes never publish. Inspect the draft and downloaded installers first. +Tag pushes never publish, and they do not build Docker images. Inspect the +draft and downloaded installers first. If the workflow itself needs a fix after tagging, keep the tag immutable. Merge and validate the workflow fix on `main`, then dispatch `electron-release.yml` @@ -93,20 +101,24 @@ and notarization are required by default. The owner may explicitly set `allow_unsigned=true`; the workflow then adds the installer-trust disclosure to the release notes. Never select that exception without the owner's decision. +Publishing the GitHub Release triggers the Docker workflow for that tag. Docker +release tags are never built from a tag push, and `:latest` only ever tracks +`main`; a release does not move it. + After publication, verify each maintained channel: | Channel | Workflow | Verification | |---|---|---| -| GitHub Release | `electron-release.yml` | Four platform targets, updater feeds, checksums, CHANGELOG notes, retained v0.5.3 Tauri feeds | -| GHCR CUDA | `docker.yml` | `:X.Y.Z`, `:X.Y`, `:stable` manifests | -| GHCR ROCm | `docker.yml` | `:X.Y.Z-rocm`, `:X.Y-rocm`, `:stable-rocm` manifests | -| Docker Hub | `docker.yml` | Matching CUDA/ROCm tags | +| GitHub Release | `electron-release.yml` | Linux x64, Windows x64 (+ ARM64 when its leg passed), macOS arm64 and x64 installers, updater feeds, checksums, CHANGELOG notes, retained v0.5.3 Tauri feeds | +| GHCR CUDA | `docker.yml` (release published) | `:X.Y.Z`, `:X.Y`, `:stable` manifests | +| GHCR ROCm | `docker.yml` (release published) | `:X.Y.Z-rocm`, `:X.Y-rocm`, `:stable-rocm` manifests | +| Docker Hub | `docker.yml` (release published) | Matching CUDA/ROCm tags | | Docker Hub overview | `docker.yml` | Read the `Update Docker Hub description` step log; the step may continue after a 403 | -| Rolling containers | `docker.yml` on `main` | `:latest`, `:main`, and `:rocm` timestamps move | +| Rolling containers | `docker.yml` on `main` pushes only | `:latest`, `:main`, and `:rocm` timestamps move; a release never moves them | -A missing channel is a release bug. There are no RC tags; previews source from -`main`, never a side branch. The Electron artifact workflow is the desktop -rehearsal channel; no Tauri or desktop-preview build is maintained. +A missing channel is a release bug. There are no RC tags and no preview update +channel; previews are builds from `main` (the `electron-build.yml` artifacts or +Docker `:latest`), never a side branch. No Tauri build is maintained. ## Package requirements @@ -117,9 +129,8 @@ the downloaded image on a clean host because build-runner libraries can hide relocation errors. Linux release AppImages include `gh-releases-zsync` update information and a -versioned `.AppImage.zsync` asset for AppImageUpdate/AppImageLauncher. Stable -images follow the latest stable GitHub release; previews follow the `preview` -release instead of downgrading to stable. The build uses `readelf` and +versioned `.AppImage.zsync` asset for AppImageUpdate/AppImageLauncher. Release +images follow the latest stable GitHub release. The build uses `readelf` and `zsyncmake` (Ubuntu package `zsync`) to embed this information before regenerating electron-updater's blockmap and checksums; the existing in-app updater still uses its separate channel manifest. Verify both update paths From 1f908adcb67398397442b0948b245302f21ffd12 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:13:14 +0530 Subject: [PATCH 23/85] docs(scripts): drop the nonexistent validate-install-docs companion check-docs-drift.py named a PR-gating companion script that does not exist; the docstring now says the check is daily-only. --- scripts/check-docs-drift.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/check-docs-drift.py b/scripts/check-docs-drift.py index c876b61ce..f2da540a2 100644 --- a/scripts/check-docs-drift.py +++ b/scripts/check-docs-drift.py @@ -17,7 +17,8 @@ ``--output``, to a Markdown report consumed by the rolling-issue automation in ``.github/workflows/docs-drift.yml``. -Companion to ``scripts/validate-install-docs.py`` (the PR-gating half). +Runs daily and on dispatch, not per PR; nothing else checks this inventory, +so run it locally when a change adds or renames a feature or engine. Rolling-issue pattern adapted from Patter (MIT) — see docs/competitive-analysis.md, Patter deep dive 4. """ From 555fdd23d6090796fa80f672bc910a357a28257f Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:13:18 +0530 Subject: [PATCH 24/85] chore(install): remove the duplicate install-redirect worker infra/install-redirect claimed the same voicestudio.sh/install routes as deploy/install-worker, which docs/install/script.md documents and CI tests. Nothing deployed or referenced the infra copy. --- infra/install-redirect/worker.js | 86 --------------------------- infra/install-redirect/wrangler.jsonc | 10 ---- 2 files changed, 96 deletions(-) delete mode 100644 infra/install-redirect/worker.js delete mode 100644 infra/install-redirect/wrangler.jsonc diff --git a/infra/install-redirect/worker.js b/infra/install-redirect/worker.js deleted file mode 100644 index b0316dfdf..000000000 --- a/infra/install-redirect/worker.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * VoiceStudio installer redirect — one URL for every platform. - * - * curl -fsSL https://voicestudio.sh/install | sh # macOS / Linux / WSL - * irm https://voicestudio.sh/install | iex # Windows PowerShell - * - * `/install` sniffs the User-Agent and serves the matching script; - * `/install.sh` and `/install.ps1` force a format explicitly. - * Scripts are proxied live from `main` on GitHub, so pushing to main - * updates what the URL serves — no redeploy needed (5-min edge cache). - */ - -const REPO_RAW = "https://raw.githubusercontent.com/debpalash/VoiceStudio/main/scripts"; - -const HTML = ` - - - - -Install VoiceStudio - - - -
-

🎙 Install VoiceStudio

-

macOS / Linux / WSL (prebuilt app):

- curl -fsSL https://voicestudio.sh/install | sh -

Pick a version, or build from source:

- curl -fsSL https://voicestudio.sh/install | sh -s -- --version 0.5.2
curl -fsSL https://voicestudio.sh/install | sh -s -- --source
-

Windows PowerShell:

- irm https://voicestudio.sh/install | iex -

PowerShell source mode: set $env:VOICESTUDIO_INSTALL_MODE = "source" first.

-

First launch downloads ~5 GB of model weights. Everything runs locally.

-

GitHub →

-
- -`; - -function pickFormat(request, pathname) { - if (pathname === "/install.sh") return "sh"; - if (pathname === "/install.ps1") return "ps1"; - const ua = request.headers.get("User-Agent") || ""; - if (/PowerShell|Pwsh/i.test(ua)) return "ps1"; - return "sh"; -} - -export default { - async fetch(request) { - const url = new URL(request.url); - const { pathname } = url; - - if (pathname === "/install" || pathname === "/install.sh" || pathname === "/install.ps1") { - // Browsers hitting /install get the landing page instead of script soup. - const accept = request.headers.get("Accept") || ""; - if (pathname === "/install" && accept.includes("text/html")) { - return new Response(HTML, { headers: { "Content-Type": "text/html; charset=utf-8" } }); - } - const format = pickFormat(request, pathname); - const upstream = await fetch(`${REPO_RAW}/install.${format}`, { - cf: { cacheTtl: 300, cacheEverything: true }, - }); - if (!upstream.ok) { - return new Response("Installer script unavailable — try again shortly.", { status: 502 }); - } - return new Response(upstream.body, { - status: 200, - headers: { - "Content-Type": format === "sh" - ? "application/x-sh; charset=utf-8" - : "text/plain; charset=utf-8", - "Cache-Control": "public, max-age=300", - }, - }); - } - - return Response.redirect(`https://github.com/debpalash/VoiceStudio${pathname}`, 302); - }, -}; diff --git a/infra/install-redirect/wrangler.jsonc b/infra/install-redirect/wrangler.jsonc deleted file mode 100644 index 2174a3524..000000000 --- a/infra/install-redirect/wrangler.jsonc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "https://unpkg.com/wrangler/config/schema.json", - "name": "voicestudio-install", - "main": "worker.js", - "compatibility_date": "2026-08-01", - "routes": [ - { "pattern": "voicestudio.sh/install*", "zone_name": "voicestudio.sh" }, - { "pattern": "www.voicestudio.sh/install*", "zone_name": "voicestudio.sh" } - ] -} From 84821eea34eedb889f2ec4562d05226191cf0e57 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:13:34 +0530 Subject: [PATCH 25/85] docs(license): say commercial use is free and link the Pro plans Replaces "pricing tiers are coming soon" with what is actually paid: the commercial licence for closed-source embedding and the Pro features, with the existing voicestudio.sh/pro plans page. --- LICENSE-NOTICE.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/LICENSE-NOTICE.md b/LICENSE-NOTICE.md index 075f22604..4215e66c7 100644 --- a/LICENSE-NOTICE.md +++ b/LICENSE-NOTICE.md @@ -21,10 +21,11 @@ a network, you must also offer those users the complete corresponding source code of your modified version under these same AGPL-3.0 terms. See the full text in [`LICENSE`](LICENSE). -A **commercial license is available** for organizations that want to embed -VoiceStudio in a closed-source or proprietary product or service without -the AGPL-3.0 copyleft obligations. Pricing tiers are coming soon; for inquiries -contact `hi@voicestudio.sh`. +Commercial use under the AGPL-3.0 is free. What is paid is a **commercial +license** for organizations that want to embed VoiceStudio in a closed-source or +proprietary product or service without the AGPL-3.0 copyleft obligations, and +the VoiceStudio Pro features. See the plans at ; for +other inquiries contact `hi@voicestudio.sh`. Contributors license their contributions to Yupcha Softwares Private Limited, the company that maintains VoiceStudio, under the From 5a5efdb1155cf22e3c3b4b5386b8b4f55689ea0a Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:14:14 +0530 Subject: [PATCH 26/85] docs(agents): state the current release line, Intel Mac scope and sanctioned calls The release-cadence rule named the closed v0.3.x line and a ROADMAP phase that no longer exists; it now applies to the current line. The parity constraint matches README's Intel Mac UI-only scope, the local-first rule lists the sanctioned setup, gallery and update calls, locale rules name both Electron catalogs, the release channel lists the real platform matrix, and CONTEXT.md is described as created lazily. --- AGENTS.md | 6 +++--- CLAUDE.md | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index da73ea6f3..a02264048 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,8 +36,8 @@ Binding for every AI agent (Claude, Codex, Cursor, review bots, …). CLAUDE.md ## Change rules (see CLAUDE.md for full text) - Root-cause the class, not the instance; fail-before/pass-after regression test; smallest correct change. - Default behavior identical on macOS/Windows/Linux; platform-only features go behind explicit opt-in. Divergent default = P0. -- Local-first: no new required network calls; any HF download gated on installed-ness or explicit user action; all synthetic audio through the `mark_synthetic` chokepoint. Owner-approved exception (2026-09-28): the public GitHub star count may refresh automatically every 20 minutes, without credentials/referrer or user content; offline use remains unaffected. -- Every user-facing string via i18n, present in ALL 21 `electron/src/shared/i18n/locales/*.json` with real translations. +- Local-first: no new required network calls; any HF download gated on installed-ness or explicit user action; all synthetic audio through the `mark_synthetic` chokepoint. Sanctioned calls are listed in `.github/CONTRIBUTING.md` → Quality gates: user-started first-run setup (ffmpeg from GitHub `zackees/ffmpeg_bins`, the huggingface.co/hf-mirror.com reachability probe, HF model downloads), opening/enabling the galleries (jsDelivr manifest, GitHub Releases), packaged update checks, consent-gated analytics, prefilled-URL bug reports, and the Pro licence check after a key is entered. Owner-approved exception (2026-09-28): the public GitHub star count may refresh automatically every 20 minutes, without credentials/referrer or user content; offline use remains unaffected. +- Every user-facing string via i18n, present in ALL 21 locale files of its catalog with real translations: `electron/src/shared/i18n/locales/*.json` (`tests/test_locale_parity.py`) or `electron/src/renderer/src/i18n/locales/*.json` (Electron `locale:check`). - Docs-sync in the same PR. CHANGELOG Unreleased: quiet one-liners ending `(#N)` + `— thanks @user!` for community work, under a short `**Highlights**` list. - Tagged release announcements lead with the biggest user-visible change; redesigns need real UI screenshots and migrations need installer links and steps. Verify all contributor credits from the tag comparison and included PRs; list authors and bug reporters separately (see `docs/RELEASING.md`). - Versioning: root `package.json` is the single source of truth; never bump without the owner asking. @@ -65,4 +65,4 @@ The five canonical roles, each label string equal to its name. See `docs/agents/ ### Domain docs -Single-context: `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`. +Single-context: `docs/adr/` plus a root `CONTEXT.md` that is created lazily and does not exist yet — skip it when absent. See `docs/agents/domain.md`. diff --git a/CLAUDE.md b/CLAUDE.md index 259e94fde..a5992ff80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,11 +22,11 @@ Everything else (new engines, fancy features) is downstream of "the thing instal ### Constraints - **Existing engine compatibility**: Users with already-installed engines (IndexTTS, CosyVoice, etc.) must not have to reinstall. Fixes touching engine code must be backward-compatible with on-disk model state. -- **Cross-platform parity**: Every fix must work on macOS (Apple Silicon + Intel), Windows (x64), and Linux (AppImage + deb). No platform-only regressions; the cross-platform bug bash (PR #51) is the baseline. +- **Cross-platform parity**: Every fix must work on macOS (Apple Silicon; Intel Macs are limited to the app UI with a remote backend, as README.md and `docs/install/macos.md` document), Windows (x64), and Linux (AppImage + deb). No platform-only regressions; the cross-platform bug bash (PR #51) is the baseline. - **Default features must work on every platform (strict rule, 2026-05-20):** A feature that ships in default mode — out-of-the-box, no user customization, no opt-in toggle — must behave identically on macOS, Windows, and Linux. Platform-specific *implementation code* is allowed for OS APIs / shells / packaging, but the user-visible *default behavior* cannot diverge. Platform-only features (e.g., a macOS-only global shortcut, a Windows-only path picker) must go behind explicit user opt-in: Settings toggle, env var, or CLI flag. When a default doesn't work on a platform, that's a P0 bug — either fix it on the missing platform or move it behind opt-in. No third option. **This rule governs BEHAVIOUR, not PERFORMANCE** (clarified 2026-07-30, council): hardware acceleration is expected to vary by host — CUDA, MPS, DirectML, Triton availability and `torch.compile` are all host-dependent by design, and reading the rule to forbid that would forbid GPU support itself. An optimization that is skipped where it cannot work (missing Triton, an arch the wheel lacks, a path its toolchain cannot link) is NOT a parity violation; a *feature* the user can see and use on one OS but not another is. - **Backward-compatible project data**: Existing `omnivoice_data/` (user voices, projects, settings) must keep working without manual migration. Any DB schema change goes through alembic with a tested upgrade path. -- **Local-first guarantee preserved**: nothing leaves the machine without the user's **explicit yes**, and the app must remain fully functional with everything declined. Auto bug reporting is opt-in and submits only to GitHub Issues (prefilled-URL, from the user's own browser). Product analytics (owner-sanctioned 2026-07-16; US project migration 2026-09-26) is opt-in PostHog US with a **first-run consent prompt** — two equal-weight Yes/No buttons, never default-on, skipping = off; consent-gated, allowlisted content-free metadata only (`backend/core/analytics.py`); every build — installer, Docker, and source alike (owner reversal 2026-07-20, #1193) — carries the in-repo publishable write-only token and shows the same consent ask, with env/baked token overriding it. No required cloud calls, accounts, or API keys. Narrow owner-approved exception (2026-09-28): the GitHub Star badge may fetch the public repository star count when shown and refresh every 20 minutes while visible, without credentials or a referrer. It sends no project, recording, or account data, retains the last count on failure, and never blocks offline use. -- **Beta release cadence (no RC, no ceremony — strict rule, 2026-05-20):** the v0.3.x line has **no release candidates, no 48h soak, no formal release ceremony**. Every fix goes continuous-to-main; the owner tags a patch (`v0.3.Z`) from main whenever the current state is worth cutting. No `-rc` tags. No phased release. No `v0.4` deferrals while the v0.3.x line is open — every open issue and every open community PR gets absorbed into the v0.3.x line or explicitly declined. Users follow `main` for previews; users wanting stable stay on the latest tagged release. ROADMAP.md's Phase 6 "Release/Verify/Retro" entries are obsolete unless the user revives them. +- **Local-first guarantee preserved**: nothing leaves the machine without the user's **explicit yes**, and the app must remain fully functional with everything declined. Auto bug reporting is opt-in and submits only to GitHub Issues (prefilled-URL, from the user's own browser). Product analytics (owner-sanctioned 2026-07-16; US project migration 2026-09-26) is opt-in PostHog US with a **first-run consent prompt** — two equal-weight Yes/No buttons, never default-on, skipping = off; consent-gated, allowlisted content-free metadata only (`backend/core/analytics.py`); every build — installer, Docker, and source alike (owner reversal 2026-07-20, #1193) — carries the in-repo publishable write-only token and shows the same consent ask, with env/baked token overriding it. No required cloud calls, accounts, or API keys. Sanctioned calls (owner-set 2026-10-02; full list in `.github/CONTRIBUTING.md` → Quality gates): during first-run setup the user starts — the pinned ffmpeg download from GitHub `zackees/ffmpeg_bins` (`backend/services/media_tools.py`), the huggingface.co/hf-mirror.com reachability probe for restricted networks (`backend/services/endpoint_race.py`), and Hugging Face model downloads; opening the community gallery (jsDelivr manifest, `backend/api/routers/community.py`) or enabling the voice-preview gallery (GitHub Releases, `backend/services/gallery.py`); and packaged update checks against GitHub Releases (`electron/src/main/updater.ts`). Narrow owner-approved exception (2026-09-28): the GitHub Star badge may fetch the public repository star count when shown and refresh every 20 minutes while visible, without credentials or a referrer. It sends no project, recording, or account data, retains the last count on failure, and never blocks offline use. +- **Beta release cadence (no RC, no ceremony — strict rule, 2026-05-20):** the current release line (0.5.x today) has **no release candidates, no 48h soak, no formal release ceremony**. Every fix goes continuous-to-main; the owner tags a patch from main whenever the current state is worth cutting. No `-rc` tags. No phased release. No deferrals to a future minor or major version — every open issue and every open community PR gets absorbed into the current line or explicitly declined. Users follow `main` for previews; users wanting stable stay on the latest tagged release. @@ -58,9 +58,9 @@ For anything new: prefer what's already pinned in `pyproject.toml` / `electron/p **Release presentation and credits (owner-set 2026-09-17):** Tagged release announcements lead with the biggest user-visible change; redesigns need real UI screenshots and migrations need installer links and steps. Verify all contributor credits from the tag comparison and included PRs; list authors and bug reporters separately (see `docs/RELEASING.md`). Keep Highlights to 3–5 bullets; the release introduction can include prose, images, and a download table before the concise change entries. -**Localization (hard rule):** No hardcoded non-English (CJK) **user-facing text** anywhere in the codebase except the translation layer (`electron/src/shared/i18n/`). All UI strings go through i18n (`t('...')` keys in `locales/*.json`); native language names live in `i18n/index.ts` (`LANGUAGES`). Functional CJK is allowed and tracked via the allowlist in `tests/test_no_hardcoded_cjk.py` — text-processing regexes, model/engine vocabulary & identifiers (e.g. CosyVoice speaker IDs), localized error matching, demo/eval data, and test fixtures. CI fails on any hardcoded CJK outside the allowlist; to add legitimate functional CJK, extend `_ALLOWED_FILES` there with a justification. +**Localization (hard rule):** No hardcoded non-English (CJK) **user-facing text** anywhere in the codebase except the translation layers (`electron/src/shared/i18n/`, parity-checked by `tests/test_locale_parity.py`, and `electron/src/renderer/src/i18n/`, checked by Electron `locale:check`). All UI strings go through i18n (`t('...')` keys in `locales/*.json`); native language names live in `i18n/index.ts` (`LANGUAGES`). Functional CJK is allowed and tracked via the allowlist in `tests/test_no_hardcoded_cjk.py` — text-processing regexes, model/engine vocabulary & identifiers (e.g. CosyVoice speaker IDs), localized error matching, demo/eval data, and test fixtures. CI fails on any hardcoded CJK outside the allowlist; to add legitimate functional CJK, extend `_ALLOWED_FILES` there with a justification. -**Release deployment channels (hard rule, owner-set 2026-07-16):** a version bump is not "released" until **every** maintained channel ships it — the full checklist lives in `docs/RELEASING.md`. The channels are the four-platform Electron GitHub Release, GHCR and Docker Hub in CUDA and ROCm flavors, and the Docker Hub overview page synced from `deploy/dockerhub-overview.md`. The final Tauri feeds are immutable compatibility assets copied into releases for old clients; they are never rebuilt. Verify all channels after tagging. Preview/RC builds always source from `main`; never cut a side-branch build. +**Release deployment channels (hard rule, owner-set 2026-07-16):** a version bump is not "released" until **every** maintained channel ships it — the full checklist lives in `docs/RELEASING.md`. The channels are the Electron GitHub Release (Linux x64, Windows x64 + experimental ARM64, macOS arm64 + x64), GHCR and Docker Hub in CUDA and ROCm flavors, and the Docker Hub overview page synced from `deploy/dockerhub-overview.md`. The final Tauri feeds are immutable compatibility assets copied into releases for old clients; they are never rebuilt. Verify all channels after tagging. Preview/RC builds always source from `main`; never cut a side-branch build. **Fix quality (hard rule, owner-set 2026-06-16):** Fix issues *properly* and future-maintenance-proof — don't stop at the symptom. Root-cause fully, fix the whole **class** of the bug (not just the one reported instance), add a fail-before/pass-after regression test, and harden against recurrence (e.g. if a lockfile drift only fails in Docker, also make CI catch it). Go the extra mile where it durably pays off. Be token-efficient about it — extra **effort**, not extra **verbosity**: no padding, no redundant re-checks, the smallest correct change that is also recurrence-proof. Don't be shy to spend the effort a proper fix needs; do be shy about wasting tokens. @@ -117,4 +117,4 @@ The five canonical roles, each label string equal to its name. See `docs/agents/ ### Domain docs -Single-context: `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`. +Single-context: `docs/adr/` plus a root `CONTEXT.md` that is created lazily and does not exist yet — skip it when absent. See `docs/agents/domain.md`. From eb7a732e8efdde037b58c9a702f82fd99dc2a61d Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:14:30 +0530 Subject: [PATCH 27/85] fix(backend): single-source browser origin policy and real port reporting - Expose every response header the renderer reads through CORS, guarded by a test that scans the client for header reads. - OMNIVOICE_UI_PORT is canonical for Vite and the backend allow-list; VOICESTUDIO_UI_PORT stays accepted as an alias. - Sharing shows the Electron dev renderer port and hides the UI port for the packaged app:// renderer. - Stop persisting OMNIVOICE_PORT/OMNIVOICE_UI_PORT, which the binding process never read, and ignore values saved by older versions. - Drop the retired Tauri webview origins from the CORS/CSRF defaults. --- backend/api/routers/dub_core.py | 4 +- backend/api/routers/system.py | 42 +++++----- backend/api/schemas.py | 2 +- backend/core/csrf.py | 78 ++++++++++++++++--- backend/core/prefs.py | 8 ++ backend/main.py | 29 +++---- backend/services/network_share.py | 4 +- docs/api-auth.md | 11 +-- electron/README.md | 4 +- electron/electron.vite.config.ts | 9 ++- electron/src/main/backend-launch.test.ts | 16 ++++ electron/src/main/backend.ts | 16 ++++ .../settings/sharing-settings.test.tsx | 19 ++++- .../features/settings/sharing-settings.tsx | 11 ++- electron/vite.web.config.ts | 4 +- tests/test_cors_exposed_headers.py | 57 ++++++++++++++ tests/test_csrf_origin.py | 28 ++++++- tests/test_network_middleware.py | 8 +- tests/test_network_share.py | 47 +++++++++++ tests/test_youtube_cookie_import.py | 8 +- 20 files changed, 322 insertions(+), 83 deletions(-) create mode 100644 tests/test_cors_exposed_headers.py diff --git a/backend/api/routers/dub_core.py b/backend/api/routers/dub_core.py index 41ecae941..e80e81d31 100644 --- a/backend/api/routers/dub_core.py +++ b/backend/api/routers/dub_core.py @@ -61,9 +61,7 @@ def _cookie_transport_allowed( origin_host = urlsplit(origin or "").hostname or "" except ValueError: return False - return is_local_host(client_host or "") and ( - is_local_host(origin_host) or origin_host == "tauri.localhost" - ) + return is_local_host(client_host or "") and is_local_host(origin_host) def _stage_cookie_export(contents: str | None) -> str | None: diff --git a/backend/api/routers/system.py b/backend/api/routers/system.py index 4c7754a30..165b493dd 100644 --- a/backend/api/routers/system.py +++ b/backend/api/routers/system.py @@ -215,19 +215,21 @@ def _nvidia_live_stats() -> tuple[float, float, float] | None: return None -def _ui_port() -> int: - """The Vite UI dev-server port, single-sourced from OMNIVOICE_UI_PORT. - - Mirrors the resolver in main.py (kept local to avoid importing the app - module). Falls back to 3901 on a missing or malformed value. +def _ui_port() -> int | None: + """The browser UI dev-server port, or None when there is no such server. + + Shares core.csrf's resolver (OMNIVOICE_UI_PORT, alias VOICESTUDIO_UI_PORT) + so Settings → Sharing reports the port the CORS/CSRF allow-list trusts. + The packaged desktop app serves its UI from app://voicestudio, so with no + UI port configured a desktop-contained backend has none to report; the + Electron dev shell passes its renderer port explicitly. """ - raw = os.environ.get("OMNIVOICE_UI_PORT") - if raw is None: - return 3901 - try: - return int(raw) - except (TypeError, ValueError): - return 3901 + from core.csrf import configured_ui_port, ui_port + + configured = configured_ui_port() + if configured is None and os.environ.get("OMNIVOICE_DESKTOP_CONTAINED") == "1": + return None + return configured or ui_port() def _fast_download_status() -> dict: @@ -1171,14 +1173,16 @@ async def ack_crash(): "DEEPL_API_KEY", "DEEPL_BASE_URL", "MICROSOFT_API_KEY", "MICROSOFT_BASE_URL", "GOOGLE_TRANSLATE_API_KEY", "MICROSOFT_REGION", "AWS_PROFILE", "AWS_REGION", - # User-configurable network ports. Persisted so they survive restarts; - # the Rust sidecar reads OMNIVOICE_PORT at startup and the backend derives - # the LAN-share/UI ports from the others. - "OMNIVOICE_PORT", "OMNIVOICE_SHARE_PORT", "OMNIVOICE_UI_PORT", + # The LAN-share port is the only persisted port: the backend binds that + # listener itself. OMNIVOICE_PORT and OMNIVOICE_UI_PORT are deliberately + # NOT persisted — the process that binds them (the desktop shell, the dev + # script, Docker, Vite) reads its own environment, never prefs.json, so a + # saved value only made the backend misreport the ports it was given. + # core.prefs.restore_env ignores values saved by older versions. + "OMNIVOICE_SHARE_PORT", # Per-job compute-time budgets (#1787). Both are captured at import time # by services/model_manager.py (GPU_JOB_TIMEOUT_S / CPU_JOB_TIMEOUT_S), so - # a value saved here takes effect on the NEXT backend restart — same - # contract as OMNIVOICE_PORT above. Restored into os.environ during the + # a value saved here takes effect on the NEXT backend restart. Restored into os.environ during the # "env_prefs" startup step (main.py), which runs before model_manager is # first imported ("ml_imports"), so the restored value is what the module # captures. The Settings UI must say so (RestartBadge). @@ -1198,7 +1202,7 @@ async def ack_crash(): # Keys whose value must be a valid TCP port (1024–65535). Validated before # being set so a bad value never reaches uvicorn / the share listener. -_PORT_KEYS = {"OMNIVOICE_PORT", "OMNIVOICE_SHARE_PORT", "OMNIVOICE_UI_PORT"} +_PORT_KEYS = {"OMNIVOICE_SHARE_PORT"} # Keys whose value is a wall-clock compute-time budget in seconds (#1787). # Validated the same way as _PORT_KEYS: reject anything that isn't a diff --git a/backend/api/schemas.py b/backend/api/schemas.py index ca65793a7..c52bd17de 100644 --- a/backend/api/schemas.py +++ b/backend/api/schemas.py @@ -79,7 +79,7 @@ class SystemInfoResponse(BaseModel): pin_required: bool = False backend_port: int = 3900 share_port_base: int = 3901 - ui_port: int = 3901 + ui_port: int | None = 3901 class ModelStatusResponse(BaseModel): diff --git a/backend/core/csrf.py b/backend/core/csrf.py index c9a40d8fd..d250d0cf2 100644 --- a/backend/core/csrf.py +++ b/backend/core/csrf.py @@ -73,7 +73,7 @@ def _origin_tuple(value: str | None) -> tuple[str, str, int | None] | None: ): return None scheme = parsed.scheme.lower() - if scheme not in {"http", "https", "tauri", "app"}: + if scheme not in {"http", "https", "app"}: return None if port is None: if scheme == "http": @@ -83,26 +83,80 @@ def _origin_tuple(value: str | None) -> tuple[str, str, int | None] | None: return scheme, parsed.hostname.lower(), port -DEFAULT_DESKTOP_ORIGINS = ("tauri://localhost", "http://tauri.localhost", "app://voicestudio") +# Electron serves the packaged renderer from app://voicestudio. The retired +# Tauri shell's origins are deliberately absent: final Tauri installs spawn the +# backend bundled with them, never this one. +DEFAULT_DESKTOP_ORIGINS = ("app://voicestudio",) -def configured_allowed_origins() -> frozenset[tuple[str, str, int | None]]: - raw_port = os.environ.get("OMNIVOICE_UI_PORT", "3901") - try: - ui_port = int(raw_port) - except (TypeError, ValueError): - ui_port = 3901 +# The browser UI dev-server port. OMNIVOICE_UI_PORT is canonical, matching +# OMNIVOICE_PORT; VOICESTUDIO_UI_PORT is the name the Vite configs read before +# the two were unified and stays accepted as an alias. Mirrored in +# electron/electron.vite.config.ts and electron/vite.web.config.ts. +UI_PORT_ENV = "OMNIVOICE_UI_PORT" +UI_PORT_ENV_ALIASES = ("VOICESTUDIO_UI_PORT",) +DEFAULT_UI_PORT = 3901 + + +def configured_ui_port() -> int | None: + """The UI port named by the environment, or None when none is set. + + The first non-empty name wins; a malformed or out-of-range value falls + back to the default port rather than disabling the allow-list. + """ + for name in (UI_PORT_ENV, *UI_PORT_ENV_ALIASES): + raw = os.environ.get(name, "").strip() + if not raw: + continue + try: + port = int(raw) + except ValueError: + return DEFAULT_UI_PORT + return port if 0 < port <= 65535 else DEFAULT_UI_PORT + return None + + +def ui_port() -> int: + """The UI dev-server port the default origin allow-list trusts.""" + return configured_ui_port() or DEFAULT_UI_PORT + + +def allowed_origin_values() -> list[str]: + """Raw allow-list entries shared by CORS (main.py) and the CSRF checks.""" + port = ui_port() values = os.environ.get( "OMNIVOICE_ALLOWED_ORIGINS", - f"http://localhost:{ui_port},http://127.0.0.1:{ui_port}," - + ",".join(DEFAULT_DESKTOP_ORIGINS), + f"http://localhost:{port},http://127.0.0.1:{port}," + ",".join(DEFAULT_DESKTOP_ORIGINS), ).split(",") + return [value.strip() for value in values if value.strip()] + + +def configured_allowed_origins() -> frozenset[tuple[str, str, int | None]]: return frozenset( origin - for value in values - if (origin := _origin_tuple(value.strip())) is not None + for value in allowed_origin_values() + if (origin := _origin_tuple(value)) is not None ) +# Response headers the browser client reads cross-origin. A header missing here +# is silently null to a renderer on another origin (the web UI, a remote +# backend), so every non-safelisted header the client reads must be listed. +# tests/test_cors_exposed_headers.py scans the renderer to keep this complete. +CORS_EXPOSED_HEADERS = ( + "Content-Disposition", + "X-Audio-Id", + "X-Audio-Path", + "X-Audio-Duration", + "X-Gen-Time", + "X-Seed", + "X-OmniVoice-Routing", + "X-OmniVoice-Routing-Reason", + "X-OmniVoice-Dropped-Chunks", + "X-OmniVoice-Dropped-Text", + "X-Clean-Filename", +) + + def _destination_origin(connection) -> tuple[str, str, int | None] | None: scheme = effective_scheme(connection) url = getattr(connection, "url", None) diff --git a/backend/core/prefs.py b/backend/core/prefs.py index f045fa87c..18dc76344 100644 --- a/backend/core/prefs.py +++ b/backend/core/prefs.py @@ -129,6 +129,12 @@ def resolve(key: str, *, env: Optional[str] = None, default: Any = None) -> Any: _EXTERNALLY_PROVIDED: frozenset[str] = frozenset() +# Ports older versions persisted although the process that binds them never +# reads prefs.json. Restoring one made the backend report a port it was not +# listening on (Settings → Sharing, LAN share and Tailscale targets). +UNRESTORED_ENV_KEYS = frozenset({"OMNIVOICE_PORT", "OMNIVOICE_UI_PORT"}) + + def restore_env(data: dict) -> None: """Restore ``env.*`` prefs into ``os.environ`` (startup only). @@ -145,6 +151,8 @@ def restore_env(data: dict) -> None: for k, v in data.items(): if not k.startswith("env.") or not v or k[len("env."):] in SECRET_ENV_KEYS: continue + if k[len("env."):] in UNRESTORED_ENV_KEYS: + continue os.environ.setdefault(k[len("env."):], str(v)) for key in SECRET_ENV_KEYS: legacy = data.get("env." + key) diff --git a/backend/main.py b/backend/main.py index 145956644..9444a2b6f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1777,25 +1777,12 @@ async def __call__(self, scope, receive, send): return await self.app(scope, receive, send) -# UI dev-server port — single-sourced from OMNIVOICE_UI_PORT so a user who -# moves the Vite dev server off 3901 still gets a matching CORS allow-list. -def _ui_port() -> int: - raw = os.environ.get("OMNIVOICE_UI_PORT") - if raw is None: - return 3901 - try: - return int(raw) - except (TypeError, ValueError): - return 3901 - - -from core.csrf import DEFAULT_DESKTOP_ORIGINS +# Origin policy is single-sourced in core.csrf: the UI port (OMNIVOICE_UI_PORT, +# alias VOICESTUDIO_UI_PORT) and OMNIVOICE_ALLOWED_ORIGINS feed both this CORS +# allow-list and the CSRF origin checks, so the two can never disagree. +from core.csrf import CORS_EXPOSED_HEADERS, allowed_origin_values, ui_port as _ui_port -_ui = _ui_port() -_allowed = os.environ.get( - "OMNIVOICE_ALLOWED_ORIGINS", - f"http://localhost:{_ui},http://127.0.0.1:{_ui}," + ",".join(DEFAULT_DESKTOP_ORIGINS), -).split(",") +_allowed = allowed_origin_values() # Registered FIRST → innermost: the startup gate holds every request except # the two probe paths until the deferred startup completes (and is a no-op @@ -1820,14 +1807,16 @@ def _ui_port() -> int: app.add_middleware( CORSMiddleware, - allow_origins=[o.strip() for o in _allowed if o.strip()], + allow_origins=_allowed, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], # The marker must be readable cross-origin too — a browser UI served from # another origin is exactly the deployment that needs to tell "the backend # answered 404" from "something else answered 404" (#1385). - expose_headers=["Content-Disposition", BACKEND_MARKER_HEADER], + # The /generate take metadata (X-Audio-Id, X-Seed, routing, ...) is read + # from headers too, so every header the client reads is exposed. + expose_headers=[*CORS_EXPOSED_HEADERS, BACKEND_MARKER_HEADER], ) # Registered LAST, which in Starlette means OUTERMOST — so the marker lands on diff --git a/backend/services/network_share.py b/backend/services/network_share.py index c78a18ecf..18d9d05f2 100644 --- a/backend/services/network_share.py +++ b/backend/services/network_share.py @@ -24,8 +24,8 @@ def backend_port() -> int: """The port the main backend listens on. - Single source of truth is the ``OMNIVOICE_PORT`` env var (read by the Rust - sidecar at startup and passed through to uvicorn's ``--port``). LAN-share + Single source of truth is the ``OMNIVOICE_PORT`` env var (the Electron + shell sets it and passes the same value to uvicorn's ``--port``). LAN-share and Tailscale derive their target ports from this so a user who runs the backend on a custom port gets a consistent share/proxy port. Falls back to the default on a missing or malformed value — never throws. diff --git a/docs/api-auth.md b/docs/api-auth.md index 7d0767fc3..3c34565df 100644 --- a/docs/api-auth.md +++ b/docs/api-auth.md @@ -319,9 +319,9 @@ credential at all**, because the API-key middleware waved it through as Everything above gates *authentication*. A **browser** frontend served from a different origin than the backend hits a separate wall first: CORS. The -backend's allow-list defaults to loopback + Tauri origins only +backend's allow-list defaults to loopback + the desktop origin only (`http://localhost:`, `http://127.0.0.1:`, -`tauri://localhost`, `http://tauri.localhost`), so opening a dev/source UI via +`app://voicestudio`), so opening a dev/source UI via a LAN IP (e.g. `http://192.168.1.159:3901` talking to `…:3900`) blocks every request with *"Missing Header: Access-Control-Allow-Origin"* — regardless of `OMNIVOICE_SERVER_MODE` or `OMNIVOICE_TRUSTED_NETWORKS`, neither of which @@ -330,16 +330,17 @@ touches CORS (#1348). Add the exact origin the browser shows in its address bar: ```bash -export OMNIVOICE_ALLOWED_ORIGINS="http://192.168.1.159:3901,http://localhost:3901,http://127.0.0.1:3901,tauri://localhost,http://tauri.localhost" +export OMNIVOICE_ALLOWED_ORIGINS="http://192.168.1.159:3901,http://localhost:3901,http://127.0.0.1:3901,app://voicestudio" ``` Each entry must be a bare origin — `scheme://host:port`, exactly what the browser sends in its `Origin` header — with no path and no trailing slash (`http://192.168.1.159:3901/` would never match). The variable **replaces** -the default list, so restate the loopback/Tauri origins alongside your own. (The in-app LAN share and Tailscale flows in +the default list, so restate the loopback and desktop origins alongside your own. (The in-app LAN share and Tailscale flows in [docs/sharing.md](sharing.md) don't need this — they serve UI and API from the same origin.) If you only moved the Vite dev server's port, set -`OMNIVOICE_UI_PORT` instead and the default list follows it. +`OMNIVOICE_UI_PORT` (the older `VOICESTUDIO_UI_PORT` name is still accepted) +instead; both Vite and the default list follow it. CORS wraps both authentication gates: credentialless browser preflights are answered before PIN/API-key enforcement, and gate-generated `401` responses diff --git a/electron/README.md b/electron/README.md index 9b4bc20b1..241a6e754 100644 --- a/electron/README.md +++ b/electron/README.md @@ -32,8 +32,8 @@ otherwise it checks the prepared `.venv` imports, **spawns** its Python interpre supervises it (restart on crash, exit code 78 = port already in use). The child's stdin is the liveness signal — closing it makes the backend exit. -Environment knobs: `OMNIVOICE_PORT` (backend port), `VOICESTUDIO_UI_PORT` -(renderer dev server, default 3902), `VOICESTUDIO_SKIP_BACKEND=1` (never +Environment knobs: `OMNIVOICE_PORT` (backend port), `OMNIVOICE_UI_PORT` +(renderer dev server, default 3902; `VOICESTUDIO_UI_PORT` is still accepted), `VOICESTUDIO_SKIP_BACKEND=1` (never spawn, only attach), `OMNIVOICE_BACKEND_CMD` (argv override, JSON array or whitespace-separated), `OMNIVOICE_STARTUP_BUDGET_S` (default 300, or 600 on a host with four or fewer cores or 8 GB of RAM or less; measured from the spawn so diff --git a/electron/electron.vite.config.ts b/electron/electron.vite.config.ts index f97531ac2..a8abca777 100644 --- a/electron/electron.vite.config.ts +++ b/electron/electron.vite.config.ts @@ -27,6 +27,11 @@ const define = { __POSTHOG_HOST__: JSON.stringify(process.env.VITE_POSTHOG_HOST ?? process.env.POSTHOG_HOST ?? ''), }; +// OMNIVOICE_UI_PORT is canonical (the backend reads it for its origin +// allow-list); VOICESTUDIO_UI_PORT is the accepted legacy alias. +const rendererPort = + Number(process.env.OMNIVOICE_UI_PORT || process.env.VOICESTUDIO_UI_PORT) || 3902; + export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], @@ -90,7 +95,7 @@ export default defineConfig({ exclude: ['@scalar/api-reference-react'], }, server: { - port: Number(process.env.VOICESTUDIO_UI_PORT) || 3902, + port: rendererPort, strictPort: true, // Dev counterpart of main's app:// proxy (CONTRACT.md, same-origin API rule). proxy: { @@ -101,7 +106,7 @@ export default defineConfig({ ws: true, configure(proxy) { proxy.on('proxyReqWs', (outgoing, incoming) => { - const port = Number(process.env.VOICESTUDIO_UI_PORT) || 3902; + const port = rendererPort; const origin = incoming.headers.origin; if (origin === `http://localhost:${port}` || origin === `http://127.0.0.1:${port}`) { outgoing.removeHeader('origin'); diff --git a/electron/src/main/backend-launch.test.ts b/electron/src/main/backend-launch.test.ts index e6081c3db..a2a162917 100644 --- a/electron/src/main/backend-launch.test.ts +++ b/electron/src/main/backend-launch.test.ts @@ -31,6 +31,7 @@ vi.mock('./legacy-storage', () => ({ }), })); import { + devRendererPort, resolveSpawnPlan, managedBackendSpawnOptions, spawnFailureMessage, @@ -218,3 +219,18 @@ it('ignores supervisor log lines when this launch printed nothing', async () => expect(supervisor.status.message).toContain('It printed no output.'); expect(supervisor.status.message).not.toContain('Reusing compatible'); }); + +it('tells the backend the dev renderer port so Sharing reports the real UI port', () => { + expect(devRendererPort('http://localhost:3902')).toBe('3902'); + expect(devRendererPort('app://voicestudio/index.html')).toBeNull(); + expect(devRendererPort(undefined)).toBeNull(); + vi.stubEnv('OMNIVOICE_UI_PORT', ''); + vi.stubEnv('VOICESTUDIO_UI_PORT', ''); + vi.stubEnv('ELECTRON_RENDERER_URL', 'http://localhost:4102'); + expect(managedBackendSpawnOptions(3900).env.OMNIVOICE_UI_PORT).toBe('4102'); + vi.stubEnv('OMNIVOICE_UI_PORT', '4200'); + expect(managedBackendSpawnOptions(3900).env.OMNIVOICE_UI_PORT).toBe('4200'); + vi.stubEnv('OMNIVOICE_UI_PORT', ''); + vi.stubEnv('ELECTRON_RENDERER_URL', ''); + expect(managedBackendSpawnOptions(3900).env.OMNIVOICE_UI_PORT).toBe(''); +}); diff --git a/electron/src/main/backend.ts b/electron/src/main/backend.ts index 01b924513..79a9d641c 100644 --- a/electron/src/main/backend.ts +++ b/electron/src/main/backend.ts @@ -372,6 +372,17 @@ export async function resolveSpawnPlan( }; } +/** The port of the electron-vite dev renderer URL, if it is a local http URL. */ +export function devRendererPort(rendererUrl: string | undefined): string | null { + if (!rendererUrl) return null; + try { + const url = new URL(rendererUrl); + return url.protocol === 'http:' && url.port ? url.port : null; + } catch { + return null; + } +} + function childEnv( port: number, region: RuntimeRegion = 'auto', @@ -395,6 +406,11 @@ function childEnv( // Arms backend/core/parent_liveness.py: stdin EOF == "the shell is gone". env.OMNIVOICE_DESKTOP_CONTAINED = '1'; env.OMNIVOICE_PORT = String(port); + // The dev renderer is served by Vite on a real port; tell the backend so + // Settings -> Sharing reports it. Packaged builds serve app:// (no port). + const rendererPort = devRendererPort(env.ELECTRON_RENDERER_URL); + if (rendererPort && !env.OMNIVOICE_UI_PORT?.trim() && !env.VOICESTUDIO_UI_PORT?.trim()) + env.OMNIVOICE_UI_PORT = rendererPort; // #2215: the backend resolves uv as OMNIVOICE_BUNDLED_UV first and // `shutil.which("uv")` second. The packaged uv lives in resources/tools, // which is on nobody's PATH, and a GUI launch does not inherit the shell's diff --git a/electron/src/renderer/src/features/settings/sharing-settings.test.tsx b/electron/src/renderer/src/features/settings/sharing-settings.test.tsx index c49a0ec5e..2ba3e4bad 100644 --- a/electron/src/renderer/src/features/settings/sharing-settings.test.tsx +++ b/electron/src/renderer/src/features/settings/sharing-settings.test.tsx @@ -25,7 +25,7 @@ afterEach(() => { vi.clearAllMocks(); }); -function renderSettings(overrides: { tailscale?: object } = {}) { +function renderSettings(overrides: { tailscale?: object; uiPort?: number | null } = {}) { let state: { enabled: boolean; share_port: number | null; @@ -35,7 +35,11 @@ function renderSettings(overrides: { tailscale?: object } = {}) { mock.api.mockImplementation((path: string, init?: RequestInit) => { if (path === '/system/network/state') return Promise.resolve(state); if (path === '/system/info') { - return Promise.resolve({ backend_port: 3900, ui_port: 3902, share_port_base: 3901 }); + return Promise.resolve({ + backend_port: 3900, + ui_port: overrides.uiPort === undefined ? 3902 : overrides.uiPort, + share_port_base: 3901, + }); } if (path === '/system/tailscale/status') { return Promise.resolve(overrides.tailscale ?? { installed: false, running: false }); @@ -110,3 +114,14 @@ it('saves the share port and exposes explicit Tailscale controls', async () => { fireEvent.click(screen.getByRole('button', { name: 'sharing.tailscale_open' })); expect(mock.openExternal).toHaveBeenCalledWith('https://studio.tailnet.ts.net'); }); + +it('shows the dev UI port and hides it for the packaged app', async () => { + renderSettings(); + expect(await screen.findByText('3902')).toBeInTheDocument(); + expect(screen.getByText('sharing.ui_port')).toBeInTheDocument(); + cleanup(); + + renderSettings({ uiPort: null }); + expect(await screen.findByText('sharing.backend_port')).toBeInTheDocument(); + expect(screen.queryByText('sharing.ui_port')).not.toBeInTheDocument(); +}); diff --git a/electron/src/renderer/src/features/settings/sharing-settings.tsx b/electron/src/renderer/src/features/settings/sharing-settings.tsx index 57f7ef310..600a6be9d 100644 --- a/electron/src/renderer/src/features/settings/sharing-settings.tsx +++ b/electron/src/renderer/src/features/settings/sharing-settings.tsx @@ -31,7 +31,8 @@ interface NetworkState { interface PortInfo { backend_port: number; - ui_port: number; + /** Null for the packaged app, whose UI is served from app:// rather than a port. */ + ui_port: number | null; share_port_base: number; } @@ -339,9 +340,11 @@ export function SharingSettings() { {ports.data.backend_port} - - {ports.data.ui_port} - + {ports.data.ui_port !== null && ( + + {ports.data.ui_port} + + )} dict[str, set[str]]: + reads: dict[str, set[str]] = {} + for root in CLIENT_ROOTS: + for path in root.rglob("*"): + if path.suffix not in {".ts", ".tsx", ".js", ".jsx"} or ".test." in path.name: + continue + for name in _READ.findall(path.read_text(encoding="utf-8")): + reads.setdefault(name.lower(), set()).add(str(path.relative_to(ROOT))) + return reads + + +def test_scan_finds_the_generate_take_headers(): + reads = _client_header_reads() + for name in ("x-audio-id", "x-seed", "x-omnivoice-routing", "x-gen-time"): + assert name in reads, f"header scan no longer sees {name}; fix the regex" + + +def test_every_header_the_client_reads_is_exposed(): + from core.csrf import CORS_EXPOSED_HEADERS + + main_source = (ROOT / "backend/main.py").read_text(encoding="utf-8") + marker = re.search(r'BACKEND_MARKER_HEADER = "([^"]+)"', main_source).group(1) + assert "expose_headers=[*CORS_EXPOSED_HEADERS, BACKEND_MARKER_HEADER]" in main_source + + exposed = {name.lower() for name in CORS_EXPOSED_HEADERS} | {marker.lower()} + missing = { + name: sorted(files) + for name, files in _client_header_reads().items() + if name not in exposed and name not in SAFELISTED + } + assert not missing, f"add these to core.csrf.CORS_EXPOSED_HEADERS: {missing}" diff --git a/tests/test_csrf_origin.py b/tests/test_csrf_origin.py index bf1259fb7..6e4f183d1 100644 --- a/tests/test_csrf_origin.py +++ b/tests/test_csrf_origin.py @@ -24,6 +24,7 @@ def _resolve_active_csrf_module(): def _clean_origin_environment(monkeypatch): monkeypatch.delenv("OMNIVOICE_ALLOWED_ORIGINS", raising=False) monkeypatch.delenv("OMNIVOICE_UI_PORT", raising=False) + monkeypatch.delenv("VOICESTUDIO_UI_PORT", raising=False) def _connection( @@ -97,9 +98,30 @@ def test_explicit_allowed_origin_is_exact_and_port_bound(monkeypatch): assert origin_allowed(_connection(origin="https://ui.test.evil")) is False -def test_default_tauri_origins_are_allowed(): - assert origin_allowed(_connection(origin="tauri://localhost")) is True - assert origin_allowed(_connection(origin="http://tauri.localhost")) is True +def test_default_desktop_origin_is_allowed_and_retired_tauri_origins_are_not(): + assert origin_allowed(_connection(origin="app://voicestudio")) is True + # Final Tauri installs talk to the backend they bundle, never this one. + assert origin_allowed(_connection(origin="tauri://localhost")) is False + assert origin_allowed(_connection(origin="http://tauri.localhost")) is False + + +@pytest.mark.parametrize("name", ["OMNIVOICE_UI_PORT", "VOICESTUDIO_UI_PORT"]) +def test_ui_port_accepts_the_canonical_name_and_its_alias(monkeypatch, name): + monkeypatch.delenv("OMNIVOICE_UI_PORT", raising=False) + monkeypatch.delenv("VOICESTUDIO_UI_PORT", raising=False) + monkeypatch.delenv("OMNIVOICE_ALLOWED_ORIGINS", raising=False) + monkeypatch.setenv(name, "4100") + + assert origin_allowed(_connection(origin="http://localhost:4100")) is True + assert origin_allowed(_connection(origin="http://localhost:3901")) is False + + +def test_canonical_ui_port_wins_over_the_alias(monkeypatch): + ui_port = importlib.import_module("core.csrf").ui_port + + monkeypatch.setenv("OMNIVOICE_UI_PORT", "4100") + monkeypatch.setenv("VOICESTUDIO_UI_PORT", "4200") + assert ui_port() == 4100 def test_invalid_ui_port_falls_back_to_the_default_allowlist(monkeypatch): diff --git a/tests/test_network_middleware.py b/tests/test_network_middleware.py index 227f1ee92..c974521b0 100644 --- a/tests/test_network_middleware.py +++ b/tests/test_network_middleware.py @@ -91,19 +91,19 @@ def test_cors_wraps_both_auth_gates_and_answers_credentialless_preflight(monkeyp monkeypatch.setenv("OMNIVOICE_API_KEY", "master-key") c = TestClient(_app_with_pin("654321"), client=("10.0.0.5", 1)) cors = { - "Origin": "tauri://localhost", + "Origin": "app://voicestudio", "Access-Control-Request-Method": "GET", "Access-Control-Request-Headers": "authorization,x-omnivoice-pin", } preflight = c.options("/api/voices", headers=cors) - rejected = c.get("/api/voices", headers={"Origin": "tauri://localhost"}) + rejected = c.get("/api/voices", headers={"Origin": "app://voicestudio"}) assert preflight.status_code == 200 - assert preflight.headers["access-control-allow-origin"] == "tauri://localhost" + assert preflight.headers["access-control-allow-origin"] == "app://voicestudio" assert "authorization" in preflight.headers["access-control-allow-headers"].lower() assert rejected.status_code == 401 - assert rejected.headers["access-control-allow-origin"] == "tauri://localhost" + assert rejected.headers["access-control-allow-origin"] == "app://voicestudio" def test_middleware_is_plain_asgi_not_buffering(): diff --git a/tests/test_network_share.py b/tests/test_network_share.py index 32fa0b702..16b19b63a 100644 --- a/tests/test_network_share.py +++ b/tests/test_network_share.py @@ -171,3 +171,50 @@ def test_set_env_share_port_accepts_valid(monkeypatch): # Clean up the process-level env mutation so other tests aren't affected. import os os.environ.pop("OMNIVOICE_SHARE_PORT", None) + + +def test_system_info_reports_no_ui_port_for_the_packaged_desktop(monkeypatch): + """The packaged app serves app://voicestudio; there is no UI port to show.""" + for name in ("OMNIVOICE_UI_PORT", "VOICESTUDIO_UI_PORT"): + monkeypatch.delenv(name, raising=False) + monkeypatch.setenv("OMNIVOICE_DESKTOP_CONTAINED", "1") + assert _loopback_client().get("/system/info").json()["ui_port"] is None + + # The Electron dev shell passes its renderer port explicitly. + monkeypatch.setenv("OMNIVOICE_UI_PORT", "3902") + assert _loopback_client().get("/system/info").json()["ui_port"] == 3902 + + +def test_system_info_reports_the_ui_port_alias(monkeypatch): + monkeypatch.delenv("OMNIVOICE_UI_PORT", raising=False) + monkeypatch.delenv("OMNIVOICE_DESKTOP_CONTAINED", raising=False) + monkeypatch.setenv("VOICESTUDIO_UI_PORT", "4100") + assert _loopback_client().get("/system/info").json()["ui_port"] == 4100 + + +def test_ports_bound_by_other_processes_are_not_persisted(): + """The desktop shell, dev script and Docker bind these from their own + environment; a saved value only made the backend misreport its ports.""" + c = _loopback_client() + for key in ("OMNIVOICE_PORT", "OMNIVOICE_UI_PORT"): + r = c.post("/system/set-env", json={"key": key, "value": "4000"}) + assert r.status_code == 400 + + +def test_stale_saved_bound_ports_are_not_restored(monkeypatch): + from core import prefs + + for key in ("OMNIVOICE_PORT", "OMNIVOICE_UI_PORT", "OMNIVOICE_SHARE_PORT"): + monkeypatch.delenv(key, raising=False) + prefs.restore_env({ + "env.OMNIVOICE_PORT": "4000", + "env.OMNIVOICE_UI_PORT": "4100", + "env.OMNIVOICE_SHARE_PORT": "5050", + }) + import os + try: + assert "OMNIVOICE_PORT" not in os.environ + assert "OMNIVOICE_UI_PORT" not in os.environ + assert os.environ["OMNIVOICE_SHARE_PORT"] == "5050" + finally: + os.environ.pop("OMNIVOICE_SHARE_PORT", None) diff --git a/tests/test_youtube_cookie_import.py b/tests/test_youtube_cookie_import.py index 40037ee2d..5c7404f4f 100644 --- a/tests/test_youtube_cookie_import.py +++ b/tests/test_youtube_cookie_import.py @@ -56,7 +56,9 @@ def test_cookie_export_accepts_a_bom_and_rejects_empty_or_oversized_files(dub_co @pytest.mark.parametrize( ("scheme", "host", "origin", "allowed"), [ - ("http", "127.0.0.1", "http://tauri.localhost", True), + ("http", "127.0.0.1", "http://localhost:3901", True), + # The retired Tauri webview origin is no longer a trusted local UI. + ("http", "127.0.0.1", "http://tauri.localhost", False), ("http", "::1", "http://localhost:3901", True), ("https", "192.0.2.20", "https://studio.example", True), ("http", "192.0.2.20", "http://localhost", False), @@ -176,7 +178,7 @@ async def fail_add(*_args, **_kwargs): request = Request( {"type": "http", "scheme": "http", "server": ("127.0.0.1", 80), "client": ("127.0.0.1", 1234), "path": "/dub/ingest-url", - "headers": [(b"origin", b"http://tauri.localhost")]} + "headers": [(b"origin", b"http://localhost:3901")]} ) with pytest.raises(RuntimeError, match="queue closed"): asyncio.run( @@ -219,7 +221,7 @@ def fail_makedirs(*_args, **_kwargs): "server": ("127.0.0.1", 80), "client": ("127.0.0.1", 1234), "path": "/dub/ingest-url", - "headers": [(b"origin", b"http://tauri.localhost")], + "headers": [(b"origin", b"http://localhost:3901")], } ) From 548f086caf1f6a6c7cfb58748242ec0ce3af85c7 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:15:29 +0530 Subject: [PATCH 28/85] fix(i18n): drop orphan zh-CN player keys and reject renderer locale orphans zh-CN carried four player.* keys English never defines. locale-coverage now fails on keys absent from en.json, allowing only CLDR plural forms of keys English pluralizes. --- electron/src/renderer/src/i18n/locales/zh-CN.json | 4 ---- electron/tests/locale-coverage.mjs | 14 +++++++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/electron/src/renderer/src/i18n/locales/zh-CN.json b/electron/src/renderer/src/i18n/locales/zh-CN.json index fb315e941..60d706c09 100644 --- a/electron/src/renderer/src/i18n/locales/zh-CN.json +++ b/electron/src/renderer/src/i18n/locales/zh-CN.json @@ -336,11 +336,7 @@ }, "player": { "generated_audio": "生成的音频", - "now_playing": "正在播放", "seek": "跳转", - "stop": "停止", - "streaming_preview": "流式预览", - "untitled": "未命名", "fullscreen": "全屏", "mute": "静音", "unmute": "取消静音", diff --git a/electron/tests/locale-coverage.mjs b/electron/tests/locale-coverage.mjs index 0d8d5999f..c0236bad2 100644 --- a/electron/tests/locale-coverage.mjs +++ b/electron/tests/locale-coverage.mjs @@ -10,13 +10,24 @@ function flatten(value, prefix = '', output = {}) { return output; } const english = flatten(JSON.parse(await readFile(new URL('en.json', directory), 'utf8'))); +// Locales may carry CLDR plural forms English lacks (ar `_few`, ru `_many`), +// but only for keys English pluralizes; anything else is a stale orphan. +const PLURAL = /^(.*)_(?:zero|one|two|few|many|other)$/; +const isOrphan = (key) => { + if (key in english) return false; + const base = PLURAL.exec(key)?.[1]; + return !base || !(`${base}_other` in english); +}; const missing = {}; +const orphans = {}; const namespaces = {}; for (const file of (await readdir(directory)).filter( (name) => name.endsWith('.json') && name !== 'en.json', )) { const translated = flatten(JSON.parse(await readFile(new URL(file, directory), 'utf8'))); missing[file.slice(0, -5)] = Object.keys(english).filter((key) => !(key in translated)); + const extra = Object.keys(translated).filter(isOrphan); + if (extra.length) orphans[file.slice(0, -5)] = extra; for (const key of missing[file.slice(0, -5)]) { const namespace = key.split('.')[0]; namespaces[namespace] = (namespaces[namespace] ?? 0) + 1; @@ -28,10 +39,11 @@ console.log( { missingEntries, byNamespace: Object.fromEntries(Object.entries(namespaces).sort((a, b) => b[1] - a[1])), + orphans, ...(process.argv.includes('--details') ? { missing } : {}), }, null, 2, ), ); -if (missingEntries > 0) process.exitCode = 1; +if (missingEntries > 0 || Object.keys(orphans).length > 0) process.exitCode = 1; From 14cdd445feb6d4d26c5dbc5da8e5809bc25e8d89 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:15:38 +0530 Subject: [PATCH 29/85] fix(performance): apply speed/quality tiers to the OmniVoice sidecar The tier gates named an unregistered omnivoice-isolated id, so the registered omnivoice-subprocess engine ignored the selected tier. One backend constant now drives the gates, and the renderer mirrors the preset using the engine list the backend reports. --- backend/services/performance_profiles.py | 14 +++++++-- .../hooks/use-performance-profile.test.tsx | 31 +++++++++++++++++++ .../src/hooks/use-performance-profile.ts | 4 ++- tests/test_performance_profiles.py | 28 +++++++++++++++++ 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/backend/services/performance_profiles.py b/backend/services/performance_profiles.py index 43c7b9a7e..f7a2dfd7b 100644 --- a/backend/services/performance_profiles.py +++ b/backend/services/performance_profiles.py @@ -6,6 +6,12 @@ from services.performance_budget import TIERS as _PERFORMANCE_TIERS +#: TTS engines whose sampling the performance tiers tune: native OmniVoice and +#: the same model in its registered crash-isolated sidecar. The one source for +#: the backend gates and the renderer (via ``tts_tiered_engines``). +TIERED_TTS_ENGINES = frozenset({"omnivoice", "omnivoice-subprocess"}) + + _PERFORMANCE_PROFILE_KEY = "performance_profile" _PERFORMANCE_FAMILIES = ( "tts", @@ -269,7 +275,7 @@ def profile_state(choice: str | None = None) -> dict: "dictation": selected_dictation.kind if selected_dictation else "inactive", } supported_engines = { - "tts": {"omnivoice", "omnivoice-isolated"}, + "tts": TIERED_TTS_ENGINES, "asr": {"faster-whisper", "faster-whisper-isolated"}, "translation": {"nllb"}, "dictation": {"offline-transducer", "online-transducer"}, @@ -301,7 +307,7 @@ def profile_state(choice: str | None = None) -> dict: # OmniVoice has one checkpoint family today; its performance tiers # tune sampling rather than silently changing voice capabilities. "model": "k2-fsa/OmniVoice" - if tts_engine in {"omnivoice", "omnivoice-isolated", "omnivoice-subprocess"} + if tts_engine in TIERED_TTS_ENGINES else tts_engine, }, "asr": { @@ -351,6 +357,8 @@ def profile_state(choice: str | None = None) -> dict: "families": list(_PERFORMANCE_FAMILIES), "implemented_families": list(_PERFORMANCE_TARGETS), "applicable_families": applicable_families, + # The renderer mirrors the TTS preset only for these engines. + "tts_tiered_engines": sorted(TIERED_TTS_ENGINES), "targets": { family: _PERFORMANCE_TARGETS[family][effective[family]] for family in _PERFORMANCE_TARGETS @@ -475,7 +483,7 @@ def reconcile_active_profile() -> dict[str, dict]: def tts_defaults(engine: str = "omnivoice") -> dict: """Only map sampling controls verified for the selected engine family.""" tier = requested_tier("tts") - if tier is None or engine not in {"omnivoice", "omnivoice-isolated"}: + if tier is None or engine not in TIERED_TTS_ENGINES: return {} target = _PERFORMANCE_TARGETS["tts"][tier] return {"num_step": target["steps"], "postprocess_output": target["postprocess"]} diff --git a/electron/src/renderer/src/hooks/use-performance-profile.test.tsx b/electron/src/renderer/src/hooks/use-performance-profile.test.tsx index 0c3a024ee..3c3a5b70b 100644 --- a/electron/src/renderer/src/hooks/use-performance-profile.test.tsx +++ b/electron/src/renderer/src/hooks/use-performance-profile.test.tsx @@ -13,6 +13,7 @@ it('changes the global preset without replacing Dubbing production overrides', a const state = { targets: { tts: { steps: 32, postprocess: true } }, selections: { tts: { engine: 'omnivoice' } }, + tts_tiered_engines: ['omnivoice', 'omnivoice-subprocess'], }; mocks.api.mockResolvedValue(state); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); @@ -44,6 +45,7 @@ it('keeps an applied backend preset successful when the clone draft chunk cannot mocks.api.mockResolvedValue({ targets: { tts: { steps: 8, postprocess: false } }, selections: { tts: { engine: 'omnivoice' } }, + tts_tiered_engines: ['omnivoice', 'omnivoice-subprocess'], }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); const { result, unmount } = renderHook(() => usePerformanceProfile(), { @@ -67,6 +69,7 @@ it('finishes saving before dependent catalogue refreshes complete', async () => mocks.api.mockResolvedValue({ targets: { tts: { steps: 16, postprocess: true } }, selections: { tts: { engine: 'omnivoice' } }, + tts_tiered_engines: ['omnivoice', 'omnivoice-subprocess'], }); const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); vi.spyOn(client, 'invalidateQueries').mockImplementation(() => new Promise(() => {})); @@ -87,3 +90,31 @@ it('finishes saving before dependent catalogue refreshes complete', async () => unmount(); client.clear(); }); + +it('mirrors the preset into the clone draft only for backend-listed tiered engines', async () => { + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const { result, unmount } = renderHook(() => usePerformanceProfile(), { + wrapper: ({ children }) => ( + {children} + ), + }); + const tiered = ['omnivoice', 'omnivoice-subprocess']; + for (const [engine, mirrored] of [ + ['omnivoice-subprocess', true], + ['kittentts', false], + ] as const) { + mocks.clone.mockClear(); + mocks.api.mockResolvedValue({ + targets: { tts: { steps: 8, postprocess: false } }, + selections: { tts: { engine } }, + tts_tiered_engines: tiered, + }); + await act(async () => { + await result.current.setTier({ tier: 'fast', family: null }); + }); + if (mirrored) expect(mocks.clone).toHaveBeenCalledWith({ steps: 8, postprocess: false }); + else expect(mocks.clone).not.toHaveBeenCalled(); + } + unmount(); + client.clear(); +}); diff --git a/electron/src/renderer/src/hooks/use-performance-profile.ts b/electron/src/renderer/src/hooks/use-performance-profile.ts index f7fd1cb93..25fe73547 100644 --- a/electron/src/renderer/src/hooks/use-performance-profile.ts +++ b/electron/src/renderer/src/hooks/use-performance-profile.ts @@ -15,6 +15,8 @@ export interface PerformanceProfileState { families: PerformanceFamily[]; implemented_families: PerformanceFamily[]; applicable_families?: PerformanceFamily[]; + /** TTS engines whose sampling the tiers tune (backend-owned list). */ + tts_tiered_engines?: string[]; targets: Record< PerformanceFamily, { @@ -94,7 +96,7 @@ export function usePerformanceProfile() { }); const target = state.targets.tts; if ( - ['omnivoice', 'omnivoice-isolated'].includes(state.selections.tts.engine) && + state.tts_tiered_engines?.includes(state.selections.tts.engine) && typeof target.steps === 'number' && typeof target.postprocess === 'boolean' ) { diff --git a/tests/test_performance_profiles.py b/tests/test_performance_profiles.py index 0590d54cd..af339f6a1 100644 --- a/tests/test_performance_profiles.py +++ b/tests/test_performance_profiles.py @@ -227,3 +227,31 @@ def test_initial_visible_balanced_profile_is_reconciled(monkeypatch): assert profiles.reconcile_active_profile() == {} assert calls == [("balanced", None)] + + +def test_tiered_tts_engines_are_registered_omnivoice_runtimes(): + """The tier gate must name engine ids the registry can actually activate; + a stale id there silently ignored the speed/quality tier for the sidecar.""" + from services import tts_backend + + assert profiles.TIERED_TTS_ENGINES == {"omnivoice", "omnivoice-subprocess"} + for engine in profiles.TIERED_TTS_ENGINES: + assert engine in tts_backend._REGISTRY + + +def test_tier_applies_to_the_omnivoice_sidecar(monkeypatch): + from core import prefs + from services import asr_backend, tts_backend + + values = {"performance_profile": {"global": "fast"}} + monkeypatch.setattr(prefs, "get", lambda key, default=None: values.get(key, default)) + fast = profiles.tts_defaults("omnivoice") + assert fast and profiles.tts_defaults("omnivoice-subprocess") == fast + assert profiles.tts_defaults("kittentts") == {} + + monkeypatch.setattr(tts_backend, "active_backend_id", lambda: "omnivoice-subprocess") + monkeypatch.setattr(asr_backend, "active_backend_id", lambda: "faster-whisper") + state = profiles.profile_state() + assert "tts" in state["applicable_families"] + assert state["selections"]["tts"]["model"] == "k2-fsa/OmniVoice" + assert state["tts_tiered_engines"] == ["omnivoice", "omnivoice-subprocess"] From 72df26adafd98ab0a8040b0acdcb43bdc6bb62fa Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:16:42 +0530 Subject: [PATCH 30/85] docs(install): replace Tauri-era platform guides with Electron instructions Platform guides now give Electron downloads (VoiceStudio-Electron-* DMG, NSIS exe, AppImage and .deb), real bun scripts for source builds, and Electron Linux GPU and ROCm guidance. Dead Tauri build, MSI, WebKitGTK and WebView2 sections are removed; the troubleshooting anchors the app links to stay live. --- README.md | 3 +- docs/install/agent.md | 5 +- docs/install/linux.md | 436 ++++++++++---------------------- docs/install/macos.md | 188 +++++++------- docs/install/troubleshooting.md | 179 ++++--------- docs/install/windows.md | 316 ++++------------------- 6 files changed, 327 insertions(+), 800 deletions(-) diff --git a/README.md b/README.md index 893aa2b74..e5367fb91 100644 --- a/README.md +++ b/README.md @@ -72,13 +72,14 @@ curl -fsSL https://voicestudio.sh/install | sh -s -- --main curl -fsSL https://voicestudio.sh/install | sh -s -- --uninstall ``` +On Windows, run `irm https://voicestudio.sh/install | iex` in PowerShell. Release downloads require curl and a SHA-256 tool. `--main` requires Git, Node.js 22+, Bun, Rust/Cargo, and platform build tools; see [installer prerequisites and behavior](docs/install/script.md). The installer preserves your settings, projects, and models. Older versions must contain Electron packages; it never falls back to archived Tauri builds. -Download from [Releases](https://github.com/debpalash/VoiceStudio/releases/latest), then follow your platform guide: +Or download from [Releases](https://github.com/debpalash/VoiceStudio/releases/latest) (macOS `.dmg`, Windows `.exe`, Linux `.AppImage` or `.deb`) and follow your platform guide: **[macOS](docs/install/macos.md) · [Windows](docs/install/windows.md) · [Linux](docs/install/linux.md) · [Docker](docs/install/docker.md)** diff --git a/docs/install/agent.md b/docs/install/agent.md index b1bdf0777..bac2b9d70 100644 --- a/docs/install/agent.md +++ b/docs/install/agent.md @@ -8,7 +8,7 @@ points here. Complete the setup, not just a plan. - Install the **Electron** desktop app. Tauri is archived; never install or launch it. - Reuse existing data, settings and models. Ask before downloading a model: state its size and license. -- Keep cloud services and analytics opt-in. +- Keep cloud services and analytics opt-in. Leave the first-run analytics consent prompt to the user; never answer it for them. - Verify the actual compute device; do not assume GPU support. - Name any permission or manual step you cannot perform instead of skipping it. @@ -23,7 +23,8 @@ points here. Complete the setup, not just a plan. existing VoiceStudio install, backend or downloaded models. 3. **Install.** - Default: the latest stable release asset named `VoiceStudio-Electron-*` for this - OS and architecture, or the [install script](script.md). + OS and architecture (macOS `.dmg`, Windows `.exe`, Linux `.AppImage` or `.deb`), + or the [install script](script.md). - From source: follow [`electron/README.md`](../../electron/README.md) — `bun install`, `bun run setup:api`, `bun run dev` from the repository root. Let Electron supervise the backend; do not start a second one. diff --git a/docs/install/linux.md b/docs/install/linux.md index 12e78de12..33bffe4c7 100644 --- a/docs/install/linux.md +++ b/docs/install/linux.md @@ -1,18 +1,59 @@ # VoiceStudio — Install on Linux -## Electron desktop (current) +VoiceStudio's Linux desktop app is built with Electron. The archived Tauri app +is no longer maintained; if you still run it, follow the +[migration guide](../electron-migration.md). -From the repository root, install Bun and uv, then run: +## Requirements -```sh -bun install -bun run setup:api # prepare Python dependencies before starting Electron -bun run dev +- **Linux x86_64** with a graphical desktop session (X11 or Wayland). There are + no ARM64 Linux packages. +- **~10 GB free disk** for the app, its Python environment, and model weights. +- Optional: an **NVIDIA driver** for CUDA acceleration. Without one, setup + installs the CPU build of PyTorch (see below). For AMD GPUs see + [AMD GPU (ROCm)](#amd-gpu-rocm). + +Python, FFmpeg/FFprobe, yt-dlp, and model weights are bootstrapped by the app +on first launch; no toolchain is needed for the packaged app. If no FFmpeg +resolves anywhere, the app downloads its own checksummed static build; +**Settings → Audio tools** shows which binaries are in use and lets you +override them or update yt-dlp. + +## Install (AppImage or .deb) + + + +Download a package from the +[Releases page](https://github.com/debpalash/VoiceStudio/releases/latest): + +| Package | File | +|---|---| +| AppImage (any distribution) | `VoiceStudio-Electron--linux-x64.AppImage` | +| Debian / Ubuntu package | `VoiceStudio-Electron--linux-x64.deb` | + +**AppImage:** + +```bash +chmod +x VoiceStudio-Electron-*-linux-x64.AppImage +./VoiceStudio-Electron-*-linux-x64.AppImage +``` + +The AppImage uses the static AppImage runtime, so it does not need `libfuse2`. +The [shell installer](script.md) (`curl -fsSL https://voicestudio.sh/install | sh`) +downloads the latest AppImage, verifies it against the release's +`SHA256SUMS.txt`, and installs it as `~/.local/bin/VoiceStudio`. + +**.deb:** + +```bash +sudo apt install ./VoiceStudio-Electron--linux-x64.deb ``` -Use `bun run desktop-prod` to build and launch Electron, or `bun run dist` -to create local installers without publishing. The app manages its backend. -See [Electron setup](../../electron/README.md) and [migration notes](../electron-migration.md). +The in-app updater's Linux feed carries the AppImage. Update a `.deb` +installation by installing the newer `.deb`; remove it with your package +manager. + +Compare any download with the release's `SHA256SUMS.txt` before installing. ### Machines without an NVIDIA GPU @@ -27,122 +68,80 @@ existing install keeps working untouched. Source installs (`bun run setup:api`) follow the lockfile and still fetch the CUDA build; use the packaged app on a CPU-only machine. -## ChromeOS, iPad and other devices - -There is no native ChromeOS or iPadOS app, and VoiceStudio does not run -inside the browser alone: the models need a real backend. - -- **Chromebook with Linux (Beta) on x86-64:** use the AppImage, the supported - Linux package ([Install (AppImage)](#install-appimage)); `.deb` bundling is - disabled. Expect CPU-only generation (the container has no GPU access on most - devices) and enough RAM for the model you pick. ARM Chromebooks are not - supported: the Linux builds are x86-64 only. -- **Any browser, including ChromeOS, iPad and phones:** run VoiceStudio on a - PC, Mac or server and open its web interface from the device. On the same - network turn on **Network** sharing and scan the QR code; from anywhere, use - Tailscale. See [Sharing & Remote Access](../sharing.md); for a headless host - see [Docker](docker.md). - -## Legacy Tauri installation and troubleshooting - -The instructions below apply to the sunset Tauri app and existing Tauri installers. +## Building from source -This page is self-contained: follow it top to bottom and you'll end up with a -working VoiceStudio install on a Debian / Ubuntu / Fedora / Arch host. +Use this for development or to run current `main`. To build and install a +desktop package from `main` without a checkout, use the shell installer's +[`--main` mode](script.md#building-main). -## Prerequisites +Prerequisites: -### Using the AppImage - -- **Linux x86_64 with glibc 2.39+** and a desktop session (X11 or Wayland) - capable of running a Tauri / WebKitGTK app. -- **~10 GB free disk** for the app, its Python environment, and model weights. -- Optional: an **NVIDIA driver** for CUDA GPU acceleration — the app runs - CPU-only without one. For AMD GPUs see [AMD GPU (ROCm)](#amd-gpu-rocm). -That's it — Python, FFmpeg/FFprobe, yt-dlp, and the model weights are bundled -or bootstrapped by the app itself on first launch. No toolchain needed. (If no -FFmpeg resolves anywhere, the app downloads its own checksummed static build -in the background during setup; **Settings → Audio tools** shows exactly which -binaries are in use and lets you override them or update yt-dlp.) - -### Building from source - -Everything above, plus the toolchain: - -- **git** — `sudo apt install git` (Debian/Ubuntu), `sudo dnf install git` (Fedora), or `sudo pacman -S git` (Arch). -- **curl** — usually preinstalled; used by the Bun and rustup install one-liners below. -- **Python 3.11+** — typically `sudo apt install python3.11` on Debian/Ubuntu, - `sudo dnf install python3.11` on Fedora, or already installed on Arch. -- **Bun** — `curl -fsSL https://bun.sh/install | bash`. -- **Rust / Cargo** — `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` or via your package manager (e.g., `sudo apt install rustc cargo`). - If you use rustup, reopen the shell or source `"$HOME/.cargo/env"` before running `bun run tauri:desktop-prod`. -- **GTK/WebKit deps** for the Tauri shell: +- **git** and **curl** +- **Bun** — `curl -fsSL https://bun.sh/install | bash` +- **uv** — `curl -LsSf https://astral.sh/uv/install.sh | sh` (it provides the + managed Python 3.11 the backend uses) +- **Node.js 22+**, **Rust / Cargo** + (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`), and the + native development libraries used by the Electron native helper. If you use + rustup, reopen the shell or run `source "$HOME/.cargo/env"` first. ```bash # Debian / Ubuntu sudo apt-get update - sudo apt-get install -y \ - libwebkit2gtk-4.1-dev libgtk-3-dev libpango1.0-dev libcairo2-dev \ - libsoup-3.0-dev libgdk-pixbuf-2.0-dev \ - libayatana-appindicator3-dev librsvg2-dev libssl-dev libxdo-dev \ - gstreamer1.0-plugins-good \ - libasound2-dev build-essential curl wget file + sudo apt-get install -y build-essential pkg-config libasound2-dev libxdo-dev \ + libxtst-dev libx11-dev libxkbcommon-dev libwayland-dev libssl-dev \ + binutils zsync # Fedora - sudo dnf install webkit2gtk4.1-devel libappindicator-gtk3-devel librsvg2-devel openssl-devel libxdo-devel gstreamer1-plugins-good + sudo dnf install gcc gcc-c++ make pkgconf-pkg-config alsa-lib-devel libxdo-devel \ + libXtst-devel libX11-devel libxkbcommon-devel wayland-devel openssl-devel # Arch - sudo pacman -S --needed base-devel webkit2gtk-4.1 libayatana-appindicator librsvg openssl xdotool gst-plugins-good + sudo pacman -S --needed base-devel alsa-lib xdotool libxtst libx11 \ + libxkbcommon wayland openssl ``` -- Optional: a **Hugging Face token** for diarization + the larger TTS engines - (see [docs/setup/huggingface-token.md](../setup/huggingface-token.md)). + `bun run dist` also needs `readelf` (binutils) and `zsyncmake` (zsync) to + embed AppImage update information. -## Install (from source) - -One-liner (installs prerequisites, clones, and builds; WSL works too): - -```bash -curl -fsSL https://voicestudio.sh/install | sh -``` - -Or manually: +Then, from a clone of the repository: ```bash git clone https://github.com/debpalash/VoiceStudio.git cd VoiceStudio bun install -source "$HOME/.cargo/env" # only needed in a shell opened before rustup finished -bun tauri # development build with hot reload +bun run setup:api # create the Python environment with uv +bun run dev # Electron with hot reload; it starts and supervises the backend ``` -Use `bun run tauri:desktop-prod` instead when you need to build and launch the -production bundle. Both commands create the Python environment via `uv`, sync -dependencies, and start the backend automatically; do not start the backend in -a second terminal. +Do not start the backend in a second terminal; Electron attaches to a backend +already listening on port 3900, otherwise it starts its own. Other commands: -The first Rust build takes longer because Cargo compiles the Tauri shell. If it -fails with `Package gdk-3.0 was not found`, `pango.pc` missing, -`libsoup-3.0` missing, or `javascriptcoregtk-4.1` missing, install the complete -Debian/Ubuntu package block above. Those messages mean the development -libraries are absent, not that `PKG_CONFIG_PATH` needs changing. Verify them -with: +| Command | What it does | +|---|---| +| `bun run desktop-prod` | Build Electron and launch the production bundle | +| `bun run dist` | Build local installers (AppImage and `.deb`) in `electron/release/` without publishing | +| `bun run smoke-test` | Build and launch an isolated packaged app | -```bash -pkg-config --exists \ - gdk-3.0 pango cairo libsoup-3.0 javascriptcoregtk-4.1 gdk-pixbuf-2.0 \ - && echo "Tauri system libraries are ready" -``` +See [Electron setup](../../electron/README.md) for backend configuration and +quality gates. The first launch downloads model weights only when you install +a model. + +## ChromeOS, iPad and other devices -`bun tauri` also checks the native `libxdo` linker input and GStreamer's -`autoaudiosink` before starting. The latter is required even if you do not plan -to record: WebKitGTK 2.52 aborts its renderer when a page creates an audio -element without that plugin, which otherwise turns a running app blank. The -launcher prints one distro-specific install command when either dependency is -missing. +There is no native ChromeOS or iPadOS app, and VoiceStudio does not run +inside the browser alone: the models need a real backend. -The first app launch downloads model weights on demand. Subsequent launches -reuse the Rust build, Python environment, and installed models. +- **Chromebook with Linux development environment on x86-64:** install the + AppImage or `.deb` ([Install](#install-appimage-or-deb)). Expect CPU-only + generation (the container has no GPU access on most devices) and enough RAM + for the model you pick. ARM Chromebooks are not supported: the Linux builds + are x86-64 only. +- **Any browser, including ChromeOS, iPad and phones:** run VoiceStudio on a + PC, Mac or server and open its web interface from the device. On the same + network turn on **Network** sharing and scan the QR code; from anywhere, use + Tailscale. See [Sharing & Remote Access](../sharing.md); for a headless host + see [Docker](docker.md). ## Wayland dictation shortcut @@ -187,195 +186,31 @@ See the portal project's [service integration checks](https://flatpak.github.io/ and the Arch Linux [backend compatibility table](https://wiki.archlinux.org/title/XDG_Desktop_Portal#List_of_backends_and_interfaces) for concrete service and desktop-backend checks. -## Install (AppImage) - -Download the latest AppImage from the -[Releases page](https://github.com/debpalash/VoiceStudio/releases/latest), -make it executable, and run: - -```bash -chmod +x VoiceStudio.Studio_*.AppImage -./VoiceStudio.Studio_*.AppImage -``` - -No FUSE? Use `--appimage-extract-and-run`: - -```bash -./VoiceStudio.Studio_*.AppImage --appimage-extract-and-run -``` - -## .deb package - -Not currently published: `.deb` bundling is disabled in the release pipeline -because of a `tauri-cli` bug (`Failed to create control scripts`) — see the -comment in `.github/workflows/release.yml` for the tracking note. The -AppImage above is the supported Linux install path until a `tauri-cli` -version resolves it. `apt install`-able `.deb`s shipped before v0.3 (see -[.deb ffprobe conflict](#deb-ffprobe-conflict) below) if you're upgrading -from one of those. - -The desktop app uses these canonical paths (kept in sync with -`scripts/desktop-prod.sh` by the docs-drift CI gate): - - -```bash -APP_ID="com.debpalash.omnivoice-studio" -APP_NAME="VoiceStudio" -``` - -## AppImage white screen / EGL errors (Fedora 44, Ubuntu 24.04+, 26.04) +## Blank window or GPU errors -Two separate WebKitGTK rendering issues land the Tauri window as a -fully-white frame with no UI. Which one you have depends on your WebKitGTK -version (`pkg-config --modversion webkit2gtk-4.1` prints it). - -**Modern WebKitGTK (2.48+ — Ubuntu 24.04 and newer, incl. 26.04): try this -first.** WebKit's DMA-BUF renderer fails against some GPU drivers; the -terminal typically shows: - -``` -Could not create default EGL display: EGL_BAD_PARAMETER -``` - -Disable the DMA-BUF renderer before launching: +The Electron app renders with Chromium, not WebKitGTK, so the `WEBKIT_*` +variables and the bundled-WebKit/GStreamer workarounds from the archived Tauri +AppImage no longer apply. If the window stays blank or the terminal shows GPU +process errors, start the app once with Chromium's GPU acceleration disabled: ```bash -WEBKIT_DISABLE_DMABUF_RENDERER=1 ./VoiceStudio.Studio_*.AppImage +./VoiceStudio-Electron-*-linux-x64.AppImage --disable-gpu ``` -**WebKitGTK 2.44 / 2.46 (Fedora 44, Ubuntu 24.04 at release):** a -compositing-mode regression blanks the surface on first paint. Disable -compositing mode instead: - -```bash -WEBKIT_DISABLE_COMPOSITING_MODE=1 ./VoiceStudio.Studio_*.AppImage -``` - -VoiceStudio's AppRun launcher autodetects the broken 2.44/2.46 range and sets -this second variable for you (shipped in v0.3+). The manual env-var path -remains the documented fallback when running from a checked-out source tree. - -**Last resort** — if neither variable alone helps, force software rendering -(slower, but always paints): - -```bash -WEBKIT_DISABLE_DMABUF_RENDERER=1 LIBGL_ALWAYS_SOFTWARE=1 ./VoiceStudio.Studio_*.AppImage -``` - -### If no environment variable helps at all (Mesa 26.1+) - -On a host with **Mesa 26.1 or newer** — Arch/CachyOS, and rolling distros -generally — none of the variables above make any difference, including -`WEBKIT_DMABUF_RENDERER_FORCE_SHM`, `WEBKIT_SKIA_ENABLE_CPU_RENDERING`, -`EGL_PLATFORM=surfaceless` and `MESA_LOADER_DRIVER_OVERRIDE=swrast`. That is -expected: the failure is in EGL **display creation**, which happens before -WebKit consults any rendering-path flag, so there is nothing left for a flag -to change. - -The cause is a version pairing, not a bug in either half. The AppImage bundles -a WebKitGTK built on Ubuntu but ships no `libEGL` of its own, so that bundled -WebKit runs against *your* Mesa. On Mesa ≥ 26.1 it calls -`eglGetPlatformDisplay()` in a way the newer driver rejects. Your distro's own -WebKitGTK is fine, because it was compiled against the Mesa you are running — -which is why building from source works on the same machine. - -**From v0.4.1 the AppImage handles this itself:** when your system has a -WebKitGTK at least as new as the bundled one, the launcher lets your copy take -precedence, and the bundled libraries fill in only what your system lacks. - -That check reads your WebKit version from `pkg-config`, which is only installed -alongside the **development** package. If you have the runtime but not the dev -package, the launcher can't compare versions and keeps the bundled copy — so -tell it explicitly: - -```bash -OMNIVOICE_PREFER_SYSTEM_WEBKIT=1 ./VoiceStudio.Studio_*.AppImage -``` - -(Set it to `0` to force the bundled copy — useful if your distro's WebKitGTK is -older than ours and you'd rather keep the newer bundled one.) - -If you are on v0.4.0 or older, either update or build from source: - -```bash -git clone https://github.com/debpalash/VoiceStudio.git -cd VoiceStudio -bun install -bun run tauri:desktop-prod -``` - -Tracking issues: [#62](https://github.com/debpalash/VoiceStudio/issues/62), -[#961](https://github.com/debpalash/VoiceStudio/issues/961), -[#1258](https://github.com/debpalash/VoiceStudio/issues/1258). - -## AppImage: "No microphone found" while the raw binary records fine - -Recording from the AppImage fails with *"No microphone found. Connect or enable -a microphone and try again."* — but `pactl list short sources` shows your -microphones, `gst-launch-1.0 pulsesrc … ! fakesink` captures, and running -`frontend/src-tauri/target/debug/omnivoice-studio` directly records without -trouble. `GST_DEBUG=2` shows the real message: - -```text -WARN GST_REGISTRY gst_registry_binary_check_magic: - Binary registry magic version is different : 1.23.90 != 1.3.0 -GStreamer element appsink not found. Please install it. -``` - -Same shape as the blank-window problem above, in a different library. The -AppImage bundles the GStreamer **core** (WebKitGTK links it) but not its -**plugins** — those are loaded dynamically at runtime, so the packaging step -cannot see them to copy. The bundled core then reads *your* plugin directory, -whose plugins were built against *your* core, the version check rejects them, -and the scan produces nothing. `appsink` is one of the elements that goes -missing, and it is the one WebKit needs to hand over a capture stream — so -`getUserMedia()` reports no device. Your raw binary works because it uses your -core with your plugins, which agree. - -**From v0.4.3 the launcher prefers your system's GStreamer**, which is the only -core that can match the plugins that will actually load. It does that by -preloading that one library (`LD_PRELOAD`) rather than by putting your system -library directory ahead of the bundle — your GStreamer shares that directory -with most of the system, so hoisting it would quietly replace every *other* -bundled library too. If your distro's GStreamer is itself broken and you would -rather fall back to the bundled core: - -```bash -OMNIVOICE_PREFER_SYSTEM_GSTREAMER=0 ./VoiceStudio.Studio_*.AppImage -``` - -The launcher checks first that your GStreamer can actually load alongside the -libraries the AppImage bundles — a system core built against newer GLib than we -ship would fail to load and take the whole app down with it, which is worse than -a missing microphone. If that check fails it prints a warning, keeps the bundled -core, and the app still starts (with capture still broken); building from source -avoids the mismatch entirely. - -The AppImage also keeps its plugin-scan cache to itself, at -`~/.cache/VoiceStudio/gstreamer-registry.bin`, rather than in the shared -`~/.cache/gstreamer-1.0/`. GStreamer names that shared file by architecture -alone, so two cores of different versions overwrite each other's — which both -makes this failure depend on whichever application ran last, and lets the -AppImage corrupt the cache every other GStreamer app on your machine reads. - -If you set `XDG_CACHE_HOME`, the path follows it -(`$XDG_CACHE_HOME/VoiceStudio/gstreamer-registry.bin`); `~/.cache` is the default -when it is unset. - -Tracking issue: [#1333](https://github.com/debpalash/VoiceStudio/issues/1333). +On Wayland sessions you can also try forcing X11 (XWayland) with +`--ozone-platform=x11`. If either flag helps, report your distribution, desktop +and GPU driver in a GitHub issue. ## .deb ffprobe conflict -Pre-v0.3 `.deb` packages installed `ffprobe` into `/usr/bin/ffprobe` and -clobbered the system copy on some distros. v0.3+ relocates the bundled -binary into `/usr/lib/omnivoice-studio/bin/ffprobe` and the `postrm` script -runs `dpkg --search` to undo the old conflict on upgrade. If you upgraded -from a pre-v0.3 .deb and `ffprobe -version` now reports the wrong binary, -re-install the system package: +This applies only to upgrades from **pre-v0.3** `.deb` packages, which +installed `ffprobe` into `/usr/bin/ffprobe` and could clobber the system copy. +If `ffprobe -version` reports the wrong binary after such an upgrade, +reinstall the system package: ```bash sudo apt install --reinstall ffmpeg @@ -387,7 +222,7 @@ If `uv` times out fetching the python-build-standalone tarball or PyPI: ```bash # Use a faster Python source mirror (China only — verify a current mirror) -export UV_PYTHON_INSTALL_MIRROR=https://ghproxy.com/https://github.com/astral-sh/python-build-standalone/releases/download +export UV_PYTHON_INSTALL_MIRROR=https://gh-proxy.com/https://github.com/astral-sh/python-build-standalone/releases/download # Use a PyPI mirror export UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple @@ -400,8 +235,10 @@ export UV_HTTP_TIMEOUT=120 export UV_HTTP_RETRIES=5 ``` -The Phase 3 install milestone (INST-07..11) ships an OS-level mirror cascade -that picks these defaults automatically; for v0.3 set them by hand. +Packaged setup already tries a Python download mirror and raises the uv +network budget automatically; set these to force a specific mirror, or before +`bun run setup:api` in a source checkout. See +[troubleshooting](troubleshooting.md#first-run-setup-fails-on-a-restricted-network-githubpypi-blocked). ## AMD GPU (ROCm) @@ -419,28 +256,25 @@ on CPU until you opt into the ROCm variant. Windows; see [windows.md](windows.md#gpu-support) for what a Radeon card can do there.) -Three ways to opt in, in order of preference: +Ways to opt in, in order of preference: -**1. First-run setup screen (recommended).** On Linux the setup screen's -**Compute** card offers **"AMD GPU (ROCm, Linux)"** next to the default -**Auto**. When VoiceStudio detects an AMD GPU *and* the ROCm userspace -(`/opt/rocm` present, or `rocminfo` on PATH), the ROCm option is pre-selected; -with an AMD GPU but no ROCm runtime it stays offered-but-unselected — install -ROCm first (or continue on CPU). Choosing ROCm makes the bootstrap reinstall -`torch`/`torchaudio` from the ROCm wheel index +**1. Packaged app (environment variable).** Install the ROCm userspace +(`/opt/rocm` or `rocminfo` on PATH), then launch VoiceStudio once with +`OMNIVOICE_TORCH_VARIANT=rocm` set, for example +`OMNIVOICE_TORCH_VARIANT=rocm ./VoiceStudio-Electron-*-linux-x64.AppImage`. +Runtime setup then reinstalls `torch`/`torchaudio` from the ROCm wheel index (`https://download.pytorch.org/whl/rocm6.4` by default) right after the -dependency sync — matched to the app's pinned `torch==2.8.0` (the rocm6.2 -index only ever published up to torch 2.5.1, so it silently failed the -reinstall and left the CPU-only CUDA build in place). - -**2. Environment variable (existing installs / headless / source).** Set -`OMNIVOICE_TORCH_VARIANT=rocm` before launching — the next bootstrap performs -the same ROCm reinstall. Source installs honour it too: -`OMNIVOICE_TORCH_VARIANT=rocm bun run tauri` swaps torch right after -`uv sync` and launches the backend without re-syncing, so the wheel is not -reverted on the next start (#1665). Without the variable, `bun run tauri` -restores the lockfile's CUDA build — a hand-swapped ROCm wheel does not -survive it. `OMNIVOICE_TORCH_INDEX=` overrides the wheel +dependency sync, matched to the app's pinned `torch==2.8.0` (the rocm6.2 index +only ever published up to torch 2.5.1). **Settings → Performance → GPU +acceleration** reports which PyTorch build is installed. The option is ignored +on macOS and Windows. + +**2. Source checkout.** `OMNIVOICE_TORCH_VARIANT=rocm bun run setup:api` swaps +torch right after `uv sync`; `bun run dev` then starts the backend from that +environment, and `bun run dev:api` launches it without re-syncing, so the +wheel is not reverted (#1665). Running `bun run setup:api` without the +variable restores the lockfile's CUDA build — a hand-swapped ROCm wheel does +not survive it. `OMNIVOICE_TORCH_INDEX=` overrides the wheel index when you need a different ROCm version — e.g. AMD publishes newer driver-matched builds (7.2.x) at `repo.radeon.com` as a `--find-links` page rather than a PyPI-style index: @@ -468,7 +302,7 @@ Once a ROCm build of PyTorch is in the venv, detection is automatic — `get_best_device()` returns the GPU (ROCm-built PyTorch reports through `torch.cuda.is_available()`), and VoiceStudio auto-sets `HSA_OVERRIDE_GFX_VERSION` for consumer cards whose GFX ID isn't in the -official ROCm support matrix. Relaunch and the Settings → System panel should +official ROCm support matrix. Relaunch and **Settings → Performance → GPU acceleration** should report the GPU device instead of `cpu`. Verify the wheel sees your card: ```bash diff --git a/docs/install/macos.md b/docs/install/macos.md index 318fd5e4d..bddf0acda 100644 --- a/docs/install/macos.md +++ b/docs/install/macos.md @@ -1,64 +1,29 @@ # VoiceStudio — Install on macOS -## Electron desktop (current) - -On Intel Macs, packaged Electron setup stops before creating a Python environment -or downloading dependencies and displays remote-backend guidance. Use the remote -connection controls in the setup screen; local inference requires a supported host. - -From the repository root, install Bun and uv, then run: - -```sh -bun install -bun run setup:api # prepare Python dependencies before starting Electron -bun run dev -``` - -Use `bun run desktop-prod` to build and launch Electron, or `bun run dist` -to create local installers without publishing. The app manages its backend. -See [Electron setup](../../electron/README.md) and [migration notes](../electron-migration.md). - -## Legacy Tauri installation and troubleshooting - -The instructions below apply to the sunset Tauri app and existing Tauri installers. - -This page is self-contained: follow it top to bottom and you'll end up with a -working VoiceStudio install on macOS (Apple Silicon). +VoiceStudio's macOS desktop app is built with Electron. The archived Tauri app +is no longer maintained; if you still run it, follow the +[migration guide](../electron-migration.md). > [!IMPORTANT] -> **Intel Macs are not supported.** The app UI installs and launches, but the -> local Python backend **cannot run**: PyTorch stopped shipping Intel-Mac -> (macOS x86_64) wheels after 2.2.x, and VoiceStudio's dependencies require a -> newer torch — so the first-run dependency install can never succeed, from -> the DMG *or* from source -> ([#889](https://github.com/debpalash/VoiceStudio/issues/889)). The app -> detects this at first launch and tells you directly instead of failing with -> a raw installer error. Your options on an Intel Mac: point the UI at a -> remote backend running on another machine (**Settings → Sharing → Remote -> backend**), or run VoiceStudio on an Apple Silicon Mac, Windows, or Linux. - -## Prerequisites - -### Using the DMG - -- **macOS 13.3 (Ventura) or newer** — Apple Silicon (Intel: UI only, see the +> **Intel Macs: app UI only.** PyTorch stopped shipping Intel-Mac (macOS +> x86_64) wheels after 2.2.x and VoiceStudio's dependencies need a newer torch, +> so a local Python backend cannot install on an Intel Mac — from the DMG *or* +> from source ([#889](https://github.com/debpalash/VoiceStudio/issues/889)). +> On Intel, packaged setup stops before creating a Python environment or +> downloading dependencies and shows remote-backend guidance instead: use the +> remote connection controls on the setup screen to point the app at a backend +> on another machine, or run VoiceStudio on an Apple Silicon Mac, Windows, or +> Linux. + +## Requirements + +- **macOS 13.3 (Ventura) or newer** on Apple Silicon (Intel: UI only, see the note above). - **~10 GB free disk** for the app, its Python environment, and model weights. -That's it — GPU acceleration (Apple MPS) is automatic on Apple Silicon, and -Python, FFmpeg, and the model weights are bundled or bootstrapped by the app -itself on first launch. No toolchain needed. - -### Building from source - -Everything above, plus the toolchain: - -- **Xcode Command Line Tools** — `xcode-select --install` (includes **git** - and the C toolchain; `curl` ships with macOS). -- **Python 3.11+** — `brew install python@3.11` (or use `pyenv` / the system Python if you already have ≥3.11). -- **Bun** — `curl -fsSL https://bun.sh/install | bash`. -- **Rust / Cargo** — `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` or `brew install rust`. - If you use rustup, reopen the terminal or source `"$HOME/.cargo/env"` before running `bun run tauri:desktop-prod`. +GPU acceleration (Apple MPS) is automatic on Apple Silicon. Python, FFmpeg, +and model weights are bootstrapped by the app itself on first launch; no +toolchain is needed for the packaged app. FFmpeg/FFprobe and yt-dlp are **not** prerequisites on any install path: the app resolves them itself (a static build ships with the Python environment; @@ -66,52 +31,33 @@ if nothing resolves, the app downloads its own checksummed build on first run). Power users can inspect or override the binaries in **Settings → Audio tools** — including pointing at a Homebrew copy. -Optional but recommended: - -- **A Hugging Face account** for diarization and the larger TTS models. See - [docs/setup/huggingface-token.md](../setup/huggingface-token.md). +Optional but recommended: **a Hugging Face account** for diarization and the +larger TTS models. See [docs/setup/huggingface-token.md](../setup/huggingface-token.md). -## Install (from source) +## Install (DMG) -One-liner (installs prerequisites, clones, and builds): - -```bash -curl -fsSL https://voicestudio.sh/install | sh -``` - -Or manually: - -```bash -git clone https://github.com/debpalash/VoiceStudio.git -cd VoiceStudio -bun install -bun run tauri:desktop-prod -``` - -The first launch builds the Tauri shell, creates the Python venv via `uv`, -syncs deps, and downloads model weights (~2.4 GB). The splash screen shows -live progress for every step. - -## Install (pre-built `.app`) - -Download the latest DMG from the +Download the DMG that matches your Mac (check **Apple menu → About This Mac → +Chip/Processor**) from the [Releases page](https://github.com/debpalash/VoiceStudio/releases/latest), -double-click to mount, drag **VoiceStudio.app** into `/Applications`. - -Pick the DMG that matches your Mac (check **Apple menu → About This Mac → Chip/Processor**): +double-click to mount it, and drag **VoiceStudio.app** into `/Applications`. | Mac | DMG to download | |-----|-----------------| -| Apple Silicon (M1/M2/M3/M4…) | `VoiceStudio.Studio__aarch64.dmg` | -| Intel | `VoiceStudio.Studio__x64.dmg` — **UI only**: the local backend cannot run on Intel ([#889](https://github.com/debpalash/VoiceStudio/issues/889)) | +| Apple Silicon (M1/M2/M3/M4…) | `VoiceStudio-Electron--mac-arm64.dmg` | +| Intel | `VoiceStudio-Electron--mac-x64.dmg` — **UI only**: the local backend cannot run on Intel ([#889](https://github.com/debpalash/VoiceStudio/issues/889)) | The architectures are **not** interchangeable: an Intel Mac cannot run the -`aarch64` build (Rosetta 2 only translates the other direction — it lets Apple -Silicon run Intel apps, never the reverse). And note the Intel caveat above: -the `x64` DMG installs and launches, but is only useful together with a -remote backend — the local Python backend cannot install on Intel because -PyTorch no longer ships Intel-Mac wheels. Installing from source does not -help; the dependency resolution fails the same way. +`arm64` build (Rosetta 2 only translates the other direction). Compare the +download with the release's `SHA256SUMS.txt` before opening it. + +Or install the latest release from Terminal with the +[shell installer](script.md), which picks the right DMG, verifies its +checksum, and installs `VoiceStudio.app` into `/Applications` (or +`~/Applications` when `/Applications` is not writable): + +```bash +curl -fsSL https://voicestudio.sh/install | sh +``` If the first launch is blocked by macOS Gatekeeper ("VoiceStudio cannot be opened because the developer cannot be verified"), see the next section — it @@ -161,6 +107,37 @@ for all platform credentials and verification gates. An Apple Developer membership is required; the archived Tauri workflow's `APPLE_CERTIFICATE` secrets do not sign Electron installers. +## Building from source + +Use this for development or to run current `main`. To build and install a +desktop package from `main` without a checkout, use the shell installer's +[`--main` mode](script.md#building-main). Local source builds are not +local-backend capable on Intel Macs either. + +Prerequisites (use a native arm64 terminal on Apple Silicon): + +- **Xcode Command Line Tools** — `xcode-select --install` (includes **git** + and the C toolchain; `curl` ships with macOS). +- **Bun** — `curl -fsSL https://bun.sh/install | bash`. +- **uv** — `curl -LsSf https://astral.sh/uv/install.sh | sh` (it provides the + managed Python 3.11 the backend uses). +- **Node.js 22+** and **Rust / Cargo** (`brew install rust`, or rustup — + reopen the terminal or source `"$HOME/.cargo/env"` afterwards) for the + Electron native helper. + +```bash +git clone https://github.com/debpalash/VoiceStudio.git +cd VoiceStudio +bun install +bun run setup:api # create the Python environment with uv +bun run dev # Electron with hot reload; it starts and supervises the backend +``` + +Use `bun run desktop-prod` to build and launch the production bundle, or +`bun run dist` to create local DMG/zip packages in `electron/release/` without +publishing. Local packages are unsigned. See +[Electron setup](../../electron/README.md) for backend configuration. + ## Apple Silicon vs Intel - **Apple Silicon (M-series):** VoiceStudio automatically picks the `mlx-whisper` @@ -176,8 +153,7 @@ secrets do not sign Electron installers. - **Intel Macs:** the local backend is **unsupported** — PyTorch no longer ships Intel-Mac wheels, so the Python environment can never install ([#889](https://github.com/debpalash/VoiceStudio/issues/889)). The UI - works only when pointed at a remote backend (**Settings → Sharing → Remote - backend**). + works only when connected to a remote backend. The picker in **Model Catalogue** shows which backend is active. @@ -200,15 +176,23 @@ The in-app error UI (the React error boundary that fires on backend errors) includes an **"Open docs for this error"** button — that button deeplinks back into this docs tree at the right section for the error class. -### Desktop window chrome +## Hugging Face token (optional but recommended) + +The default install works without a token, but diarization (the +`pyannote/speaker-diarization-3.1` model) is gated and the larger +voice-design engines also download faster with a token attached. + +- Open **Settings → API Keys** in the app. +- Or set the env var `export HF_TOKEN=hf_…` in `~/.zshrc`. + +Full details: [docs/setup/huggingface-token.md](../setup/huggingface-token.md). + +## Troubleshooting + +Hit a wall? See [docs/install/troubleshooting.md](troubleshooting.md). -The main window uses native macOS traffic lights with an overlay title bar; -window sizing, resize limits, and application file-drop behavior match the -other desktop platforms. The platform configuration repeats the complete window -list because Tauri replaces arrays when merging it with the shared config. -The capture widget remains a separate borderless window created at runtime. -Its window-scoped Tauri capability permits hiding after recording or idle -reconciliation on every desktop platform. +The in-app error UI includes an **"Open docs for this error"** button that +deeplinks back into this docs tree at the right section for the error class. ### Fast process shutdown diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index c4cb779f1..3926ebc42 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -125,7 +125,7 @@ retry. -**Symptom:** the splash screen shows `ModuleNotFoundError: No module named +**Symptom:** the setup screen shows `ModuleNotFoundError: No module named 'pkg_resources'` during WhisperX import, and the app never advances past the "Setting up models" step. @@ -300,29 +300,27 @@ itself does not grant access. **Symptom:** "VoiceStudio.app is damaged and can't be opened." -**Cause:** the app is not yet notarised (signing is wired in `release.yml` and -activates once the maintainer adds the Apple cert secrets) — until then macOS -quarantines every download. +**Cause:** Gatekeeper cannot verify the app's Apple Developer identity — the +build is unsigned or ad-hoc signed (local `bun run dist` packages, or a release +published without notarization; signing and notarization run in +`electron-release.yml` when the Apple credentials are configured). **Fix:** see [macos.md#gatekeeper-quarantine](macos.md#gatekeeper-quarantine). -## 4. AppImage white screen / EGL errors (Fedora 44, Ubuntu 24.04+, 26.04) +## 4. Linux: blank window or GPU errors -**Symptom:** the AppImage window opens fully white. No UI ever appears. On -newer distros (Ubuntu 24.04 and later, incl. 26.04) the terminal often shows -`Could not create default EGL display: EGL_BAD_PARAMETER`. +**Symptom:** the Linux app window opens blank, or the terminal shows GPU +process errors. -**Cause:** WebKitGTK rendering regressions — the DMA-BUF renderer on modern -WebKitGTK (2.48+), or the 2.44 / 2.46 compositing mode. +**Cause:** the Electron app renders with Chromium; some GPU driver and +compositor combinations fail to initialise hardware acceleration. (The +`WEBKIT_*` variables from older versions of this entry applied only to the +archived Tauri app, which rendered with WebKitGTK.) -**Fix:** try `WEBKIT_DISABLE_DMABUF_RENDERER=1` first (modern WebKitGTK / the -EGL error), then `WEBKIT_DISABLE_COMPOSITING_MODE=1` — full walkthrough incl. -the software-rendering last resort: +**Fix:** launch once with `--disable-gpu` (and on Wayland, optionally +`--ozone-platform=x11`) — see [linux.md#appimage-white-screen-on-fedora-44--ubuntu-2404](linux.md#appimage-white-screen-on-fedora-44--ubuntu-2404). -**Linked issues:** [#62](https://github.com/debpalash/VoiceStudio/issues/62), -[#961](https://github.com/debpalash/VoiceStudio/issues/961) - ## 5. Windows Triton / torch.compile OOM **Symptom:** the first synthesis call fails with `OutOfMemoryError: CUDA out @@ -1002,63 +1000,23 @@ because the backend could not start, you get: The reason is also **retained** across a Retry or an automatic respawn, so a later attempt can't erase the diagnosis of the first one. -**From source (`bun desktop`)?** If the app builds but the window never comes -up, the shell now prints the exit code and where to look (the cargo/tauri -output above it, plus `omnivoice.log` and `backend_err.log` in your VoiceStudio -data folder) instead of exiting silently. - -If Cargo stops before the window is built with `Package gdk-3.0 was not found`, -`pango.pc` missing, `libsoup-3.0` missing, or `javascriptcoregtk-4.1` missing, -the Ubuntu/Debian WebKitGTK development packages are absent. Install the full -package block in the [Linux source-build guide](linux.md#building-from-source), -then rerun `source "$HOME/.cargo/env"` and `bun desktop`. Do not set a custom -`PKG_CONFIG_PATH` unless the libraries were deliberately installed outside the -system package manager. +**From source (`bun run dev`)?** Run `bun run setup:api` first so the +backend's Python environment exists. If the window never comes up, read the +terminal output plus `omnivoice.log` and `backend_err.log` in your VoiceStudio +data folder. If a packaging build (`bun run dist`) stops while compiling the +Rust native helper with a missing `pkg-config` library, install the package +block in the [Linux source-build guide](linux.md#building-from-source). **Still stuck?** Open the details, copy the output, and file it with **Report** — that output is the thing that makes the failure diagnosable. -## 15. Stuck at "preparing" forever after a crash / BSOD (Windows) - -**Symptom:** after an unclean shutdown (Windows BSOD, forced power-off), every -launch sits on the "preparing" splash indefinitely — even though the backend is -actually healthy (its log shows models loaded, and -`http://127.0.0.1:3900/health` answers `{"status":"ok"}` in a browser). The -WebView log contains: - -``` -IPC custom protocol failed, Tauri will now use the postMessage interface instead -TypeError: Failed to fetch -``` - -**Cause:** the crash corrupted cache directories inside the WebView2 profile at -`%LOCALAPPDATA%\com.debpalash.omnivoice-studio\EBWebView`. Both the IPC custom -protocol *and* its postMessage fallback break, so the splash never hears the -"ready" signal from the app shell (issue #879). - -**Fix:** current builds handle this automatically — if the splash gets no IPC -signal within ~10 s it checks the backend over plain HTTP and proceeds on its -own; if the backend isn't up either, after ~45 s a recovery panel appears with -**Repair and restart** (Windows), which clears cache-only directories and -relaunches. It deliberately preserves `Default\Local Storage` and -`Default\IndexedDB`, where browser-owned settings and long-form projects live. - -On older builds (≤ 0.3.8), or if the automatic repair fails, do it manually: -quit VoiceStudio, delete only the cache directories below, then start the app -again. Do not delete the whole `EBWebView` profile; doing so also deletes -browser-owned projects and settings. +## 15. Archived Tauri app: stuck at "preparing" after a crash (Windows) - -```powershell -$voiceStudioWebView = "$env:LOCALAPPDATA\com.debpalash.omnivoice-studio\EBWebView" -@( - "Default\Cache", "Default\Code Cache", "Default\GPUCache", "Default\DawnCache", - "Default\Service Worker\CacheStorage", "Default\Service Worker\ScriptCache", - "GPUCache", "DawnCache", "ShaderCache", "GrShaderCache", "GraphiteDawnCache" -) | ForEach-Object { - Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $voiceStudioWebView $_) -} -``` +This entry covered a corrupted WebView2 profile in the archived Tauri app +(`IPC custom protocol failed, Tauri will now use the postMessage interface +instead`, issue #879). The Electron app does not use WebView2. Install Electron +with the [migration guide](../electron-migration.md); if the Electron app hangs +at startup, see [14d](#14d-cant-reach-the-local-voicestudio-backend-when-the-backend-never-started). ## 16. macOS: microphone permission never prompts, VoiceStudio never appears in System Settings @@ -1067,26 +1025,23 @@ System Settings → Privacy & Security → Microphone and enable VoiceStudio" but VoiceStudio never appears in that list, so there's nothing to enable. `NSMicrophoneUsageDescription` is present in the app's `Info.plist`, and resetting the permission (`tccutil reset Microphone -com.debpalash.omnivoice-studio`) followed by a relaunch changes nothing — no +com.voicestudio.desktop`) followed by a relaunch changes nothing — no system prompt ever appears. **Cause:** the app bundle was missing the Hardened Runtime *entitlement* for -microphone access. An earlier revision of this section blamed an upstream -Tauri/WebKit limitation — that was wrong (a community contributor, -[@MahdiHedhli](https://github.com/MahdiHedhli), read the sources more -carefully and found the real gap). wry's `WKUIDelegate` already grants the -WebKit-layer media-capture request; but Tauri's macOS bundler enables -Hardened Runtime by default, and Hardened Runtime blocks microphone hardware -access unless `com.apple.security.device.audio-input` is present in the -signed binary's entitlements — regardless of `Info.plist`'s -`NSMicrophoneUsageDescription` (that only supplies the prompt *text*). -Without the entitlement, macOS's TCC layer never registers a request, which -is exactly why the app never appears in the System Settings list. - -**Fix:** ships in the release after v0.3.12 (the bundle now carries -`src-tauri/entitlements.plist` — [#1016](https://github.com/debpalash/VoiceStudio/pull/1016), -contributed by the same person who diagnosed it). Update and live recording -works, with a normal macOS permission prompt on first use. +microphone access. Hardened Runtime blocks microphone hardware access unless +`com.apple.security.device.audio-input` is present in the signed binary's +entitlements — regardless of `Info.plist`'s `NSMicrophoneUsageDescription` +(that only supplies the prompt *text*). Without the entitlement, macOS's TCC +layer never registers a request, which is exactly why the app never appears in +the System Settings list. (Diagnosed in the archived Tauri app by +[@MahdiHedhli](https://github.com/MahdiHedhli).) + +**Fix:** fixed since the release after v0.3.12 +([#1016](https://github.com/debpalash/VoiceStudio/pull/1016)); the Electron app +signs with the same entitlement (`electron/build/entitlements.mac.plist`). +Update and live recording works, with a normal macOS permission prompt on +first use. **Workaround on older builds (≤ v0.3.12):** record your voice sample in any other app (Voice Memos, QuickTime, etc.) and upload the resulting file in @@ -1120,51 +1075,19 @@ install `.pth` embeds that path in UTF-8, which is rarely a valid byte sequence in the active ANSI code page (`gbk`, `shift_jis`, etc.), and the interpreter can never start (#1783). -**Fix:** current builds resolve the managed environment through Windows' 8.3 -short filename for any path containing non-ASCII bytes (the same trick -already used for the HuggingFace cache — see -[`backend/core/config.py`](../../backend/core/config.py)) whenever there is -no usable environment yet, or an existing one shows exactly this crash — so -a first-time install, and an install already broken by this bug, both land -at an ASCII-safe path automatically with no user action needed. The old -broken environment (if any) is left in place, not deleted, in case manual -recovery is ever needed. An existing environment that already works — ASCII -path or not — is never touched or relocated. - -**Prevention, on a brand new install:** on the first-run setup screen (before -clicking through it), the **Change…** button on the "App environment" row -(or "Portable folder" in portable mode) lets you pick an ASCII-only path up -front — nothing below is needed if you do this before setup completes. - -If it still happens — most likely because Windows' 8.3 short filenames are -off on the system drive, or the affected folder already existed before this -fix shipped — the app names this cause specifically rather than the generic -"backend never reported ready." By the time this message can appear, setup -has already been confirmed (it's only reached after the first-run screen -hands off to the installer), so the error screen you're actually looking at -offers only **Retry** and **Clean & Retry** — neither changes where the -environment is stored, so both fail identically, and the first-run picker -above is no longer reachable either. The right fix depends on which install -mode you're in: - -- **Standard (non-portable) install:** quit VoiceStudio, open (creating it - if it doesn't exist) `%LOCALAPPDATA%\com.debpalash.omnivoice-studio\config.json` - in a text editor, add `"env_dir": "C:/VoiceStudio/env"` (any path using - only English letters/numbers — forward slashes are fine on Windows), save, - and relaunch. -- **Portable install:** the `env_dir` config key above does **not** apply — - portable mode resolves its own environment folder from the portable - location and never consults it. Quit VoiceStudio, then either move the - whole VoiceStudio folder (the app plus its `OmniVoiceStudio-Data` folder) - to an ASCII-only path and run it from there, or create a `portable.path` - text file beside the app containing one line — an absolute ASCII-only path - for the data folder (e.g. `C:\VoiceStudio\Data`) — and relaunch. +**Fix:** keep the app environment on an ASCII-only path. On the first-run +setup screen, press **Change…** beside **App environment** and pick a folder +such as `C:\VoiceStudio` before setup starts. An environment that cannot start +fails VoiceStudio's runtime check, so setup opens again: choose an ASCII-only +App environment location there. The broken environment is left in place for +manual removal. + +The archived Tauri app's automatic 8.3 short-path relocation and its +`env_dir` / `portable.path` overrides do not apply to the Electron app. Re-enabling 8.3 short filenames (`fsutil 8dot3name`) is deliberately **not** -recommended here: the setting is per-volume and only affects directories -created *after* it's changed, so toggling it does nothing for a folder that -already exists — it would not actually fix this without also recreating the -folder, which the ASCII-path options above already do more reliably. +recommended: the setting is per-volume and only affects directories created +*after* it's changed, so it does nothing for a folder that already exists. **Linked issue:** [#1783](https://github.com/debpalash/VoiceStudio/issues/1783) (auto-captured from [#1771](https://github.com/debpalash/VoiceStudio/issues/1771)) diff --git a/docs/install/windows.md b/docs/install/windows.md index 77440f288..9493ff3b9 100644 --- a/docs/install/windows.md +++ b/docs/install/windows.md @@ -1,20 +1,20 @@ # VoiceStudio — Install on Windows -## Electron desktop (current) +VoiceStudio's Windows desktop app is built with Electron. The archived Tauri +app (MSI installers) is no longer maintained; if you still run it, follow the +[migration guide](../electron-migration.md). -From the repository root, install Bun and uv, then run: +## Requirements -```sh -bun install -bun run setup:api # prepare Python dependencies before starting Electron -bun run dev -``` +- **Windows 10 or 11**, x64 — or Windows on ARM (experimental, see below). +- **~10 GB free disk** for the app, its Python environment, and model weights. +- Optional: an **NVIDIA GPU + driver** for CUDA acceleration — see + [GPU support on Windows](#gpu-support). -Use `bun run desktop-prod` to build and launch Electron, or `bun run dist` -to create local installers without publishing. The app manages its backend. -See [Electron setup](../../electron/README.md) and [migration notes](../electron-migration.md). +Python, FFmpeg, and model weights are bootstrapped by the app itself on first +launch; no toolchain is needed for the installer. -### Windows installers +## Install Download the installer matching your PC from the [Releases page](https://github.com/debpalash/VoiceStudio/releases/latest): @@ -24,7 +24,17 @@ Download the installer matching your PC from the | Intel / AMD (x64) | `VoiceStudio-Electron--win-x64.exe` | | Windows on ARM (Snapdragon X etc., experimental) | `VoiceStudio-Electron--win-arm64.exe` (releases without it: the x64 installer also works, under emulation) | -`irm https://voicestudio.sh/install | iex` picks the right one automatically. +The setup wizard installs for the current user by default (no administrator +rights needed) and lets you choose the installation folder. Compare the download with the +release's `SHA256SUMS.txt` before running it. + +Or, from a regular (non-admin) PowerShell, let the +[install script](script.md) pick the right installer, verify it, and open the +wizard: + +```powershell +irm https://voicestudio.sh/install | iex +``` ### PCs without an NVIDIA GPU @@ -53,7 +63,7 @@ It is designed to work but will be slower than on an x64 PC; Windows 11 24H2 or newer gives the emulator the AVX2 support PyTorch benefits from. Setup shows a "Windows on ARM" notice and applies the same light-engine advice as above. -### Everything on an external drive (portable-style use) +### Everything on an external drive The Electron app has no single-file portable build. To keep it all on another drive (for example an external SSD): @@ -61,51 +71,13 @@ drive (for example an external SSD): 1. Run the installer and choose the external drive on its **Installation Folder** page. 2. On the setup screen, press **Change…** beside **App environment** to put the - Python environment on the drive. + Python environment on the drive. uv's download cache and managed Python stay + beside that environment, so multi-GB wheels are not staged on `C:`. 3. In **Settings > Storage**, move the application data folder (voices, projects, settings) and the model cache to the drive. The drive must be connected when VoiceStudio starts. -## Legacy Tauri installation and troubleshooting - -The instructions below apply to the sunset Tauri app and existing Tauri installers. - -This page is self-contained: follow it top to bottom and you'll end up with a -working VoiceStudio install on Windows 10 / 11 (x64). - -## Prerequisites - -### Using the MSI installer - -- **Windows 10 (21H2 or newer) or Windows 11**, x64. -- **~10 GB free disk** for the app, its Python environment, and model weights. -- Optional: an **NVIDIA GPU + driver** for CUDA acceleration — see - [GPU support on Windows](#gpu-support). AMD GPUs do not accelerate PyTorch - engines on Windows (audio.cpp can use them via Vulkan). - -That's it — Python, FFmpeg, and the model weights are bundled or bootstrapped -by the app itself on first launch. No toolchain needed. - -### Building from source - -Everything above, plus the toolchain: - -- **Git for Windows** — `winget install --id Git.Git -e`. Needed for - `git clone`, and it includes **Git Bash**, which `bun run tauri:desktop-prod` - uses to run its build-and-launch script. Without it, `desktop-prod` stops - with an error telling you to install it. -- **Python 3.11+** — `winget install Python.Python.3.11` (or download from - [python.org](https://www.python.org/downloads/windows/)). -- **Microsoft C++ Build Tools** — required by some PyPI source distributions - (`pyannote.audio`, occasional torch wheel rebuild). Install via the - [Visual Studio 2022 Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) - with the **"Desktop development with C++"** workload checked. -- **Bun** — `powershell -c "irm bun.sh/install.ps1 | iex"`. -- **FFmpeg** — `winget install Gyan.FFmpeg`. -- **Rust / Cargo** — `winget install Rustlang.Rustup` or download `rustup-init.exe` from [rustup.rs](https://rustup.rs/). - After installing Rustup, close and reopen PowerShell before running `bun run tauri:desktop-prod`. - ## GPU support on Windows @@ -143,194 +115,39 @@ verdict for every engine (uses the GPU, runs on the CPU and why, or CPU by design). **Settings → About → Run self-check** names the card instead of just saying "no GPU acceleration detected". -## Install (from source) +## Building from source -One-liner from a regular (non-admin) PowerShell — installs Git, FFmpeg, uv, -and bun via winget, then clones and builds: +Use this for development or to run current `main`. The install script's +`main` mode (`$env:VOICESTUDIO_INSTALL_MODE='main'; irm https://voicestudio.sh/install | iex`) +builds and installs a desktop package from `main` without a checkout; see +[the install script guide](script.md). -```powershell -irm https://voicestudio.sh/install | iex -``` +Prerequisites: -Or manually: +- **Git for Windows** — `winget install --id Git.Git -e`. +- **Bun** — `powershell -c "irm bun.sh/install.ps1 | iex"`. +- **uv** — `powershell -c "irm https://astral.sh/uv/install.ps1 | iex"` (it + provides the managed Python 3.11 the backend uses). +- **Node.js 22+**. +- **Rust / Cargo** — `winget install Rustlang.Rustup`; close and reopen + PowerShell afterwards. +- **Microsoft C++ Build Tools** — the + [Visual Studio 2022 Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) + with the **"Desktop development with C++"** workload (plus the ARM64 build + tools on Windows on ARM). -```bash +```powershell git clone https://github.com/debpalash/VoiceStudio.git cd VoiceStudio bun install -bun run tauri:desktop-prod -``` - -The first launch creates the Python venv via `uv`, syncs deps, and downloads -model weights. The splash screen shows progress. - -> **Note:** `bun run tauri:desktop-prod` runs a bash script under the hood. You can -> launch it from PowerShell or cmd as shown — it finds Git Bash automatically -> (installed with Git for Windows, see Prerequisites). If no Git Bash is -> found, it prints instructions instead of failing silently. Alternatives -> that don't need bash: `bun run tauri` (dev mode) or the pre-built MSI -> below. - -## Install (pre-built MSI) - -Download the latest MSI from the -[Releases page](https://github.com/debpalash/VoiceStudio/releases/latest). - -| Artifact name | Scope | Administrator required | Default location | -|---|---|---|---| -| `VoiceStudio__x64_en-US.msi` | All users (per-machine) | Yes | `%ProgramFiles%\VoiceStudio` | -| `VoiceStudio_Current_User__x64_en-US.msi` | Current user only | No | `%LOCALAPPDATA%\VoiceStudio (Current User)` | - -Run the artifact matching the required scope and follow the wizard. The -per-user artifact can be installed, updated, and removed by a standard Windows -account. It has a separate Windows Installer upgrade identity, shortcut name, -and signed updater manifest, so it cannot upgrade or uninstall the per-machine -copy (or vice versa). Both copies use the same VoiceStudio data directory; do -not run them simultaneously against the same projects. - -Automatic updates preserve the installed scope. Managed deployments should -continue to use the per-machine MSI. Users without elevation should choose the -artifact containing `Current_User`. - -### Installing to a different drive - - - -The wizard's **directory picker** lets you install the app to any **local** -drive (D:, E:, …). Two caveats: - -- **Mapped network drives (Z: → a share) are not supported** — this is a - Windows Installer limitation, not a VoiceStudio bug: MSI custom actions run - as a service account that doesn't see per-user drive mappings, so the - install fails or rolls back. Install to a local drive instead. -- The install location only moves the ~200 MB app itself. The big data - (models, voices, projects — tens of GB) lives in the **data directory**, - which you move independently: **Settings → Storage → Models directory** - in-app, or `OMNIVOICE_DATA_DIR` / [Portable mode](#portable-install) for - the whole data tree. - -When the Python **environment folder** (first-run setup → Advanced, or -portable mode) is on a different drive than Windows, the installer keeps -uv's package cache and managed Python **inside the environment folder** -(`uv-cache/`, `uv-python/`) instead of `%LOCALAPPDATA%\uv`. Without that, -every wheel (PyTorch alone is several GB) would be staged on `C:` and then -copied across drives — filling the system drive you were trying to spare. -The same applies to engine sidecar installs when the data directory is on -another drive (`engines\.uv-cache`). An explicit `UV_CACHE_DIR` / -`UV_PYTHON_INSTALL_DIR` you set yourself always wins. - -If an install to a local non-C: drive fails anyway, capture a log with -`msiexec /i VoiceStudio*.msi /L*V install.log` and -[open an issue](https://github.com/debpalash/VoiceStudio/issues) with it -— that log shows exactly which step rolled back. - -## Managed WebView2 installation - -The MSI checks the `pv` version value in both the machine-wide and current-user -Edge Update registry keys for the WebView2 Runtime product -`{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}`. When no `pv` version is detected and -the bootstrapper was not explicitly allowed (or was explicitly disabled), the -install fails without making a network request and directs the operator to the -offline runtime. `ALLOWWEBVIEW2BOOTSTRAP=1` instead selects the opt-in path -documented below. - -Managed or offline deployments can prohibit that network action: - -```powershell -msiexec /i VoiceStudio_0.5.2_x64_en-US.msi DISABLEWEBVIEW2BOOTSTRAP=1 AUTOLAUNCHAPP=0 /qn /L*V "%TEMP%\VoiceStudio-install.log" +bun run setup:api # create the Python environment with uv +bun run dev # Electron with hot reload; it starts and supervises the backend ``` -The per-user artifact never contains a WebView2 download or installer action. -It does not require an elevated terminal, and fails closed when the runtime is -absent: - -```powershell -msiexec /i VoiceStudio_Current_User_0.5.2_x64_en-US.msi DISABLEWEBVIEW2BOOTSTRAP=1 AUTOLAUNCHAPP=0 /qn /L*V "%TEMP%\VoiceStudio-user-install.log" -``` - -For the per-machine artifact, `DISABLEWEBVIEW2BOOTSTRAP=1` leaves detection -enabled but prevents every WebView2 PowerShell, download, and installer action. -For the per-user artifact that property is redundant because those actions are -omitted from the MSI. If the runtime is absent, either MSI fails before copying -VoiceStudio and tells the operator to deploy the -[Microsoft Evergreen Runtime](https://developer.microsoft.com/microsoft-edge/webview2/#download-section) -first. `/L*V` records detection and action selection in the named Windows -Installer log. - -An interactive administrator may explicitly permit Microsoft's network -bootstrapper for the per-machine artifact by setting -`ALLOWWEBVIEW2BOOTSTRAP=1`. Only then does that MSI download -`https://go.microsoft.com/fwlink/p/?LinkId=2124703` with PowerShell and invoke -it silently with `/install`; `DISABLEWEBVIEW2BOOTSTRAP=1` always wins if both -properties are supplied. `ALLOWWEBVIEW2BOOTSTRAP` has no effect on the -per-user artifact. - -`AUTOLAUNCHAPP=0` is independent: it prevents VoiceStudio from launching after -a successful silent install. It does not change WebView2 detection; on the -per-machine artifact it also does not disable an otherwise permitted setup. - -## Portable install (Windows) - - - -VoiceStudio has a **Portable** mode: instead of scattering data across -`%APPDATA%` and `%LOCALAPPDATA%`, the whole install — Python env, model -weights, voices, projects, settings — lives in a single folder. By **default** -that is `OmniVoiceStudio-Data` next to the executable; you can put it anywhere -writable from the setup screen (see below). Moving or copying the app folder -(exe + that data folder together) relocates the entire install, USB-stick -style. - -The first-run setup screen offers Portable, and the folder is **yours to -choose** — press **Change…** on the Portable folder row and point it at any -writable location (an external SSD, a second drive, a USB stick). - -Where the folder lives decides how far it travels, and the setup screen tells -you which of these you're getting: - -- **Inside the app's own folder** (e.g. `D:\Apps\VoiceStudio\MyData`, with the - exe in `D:\Apps\VoiceStudio`) — the `portable.path` marker records it as a - **relative** path. Move the app folder as a unit, to another machine or a - different drive letter, and the install still finds itself. This is the - USB-stick case Portable exists for. -- **Somewhere else, app folder writable** (e.g. exe in `D:\Apps`, data on - `E:\VoiceStudio`) — the marker can only record an **absolute** path, so the - install is tied to that exact path. Change the drive letter or the mount - point and you'll have to point the app at it again. -- **App folder read-only** (a default `C:\Program Files` MSI install) — no - marker can be written at all, so the location is remembered for **your user - account only** on this machine. - -That second case used to be a hard block: Portable was greyed out entirely -after a default install -([#766](https://github.com/debpalash/VoiceStudio/issues/766)). It no longer -is — you just get the machine-bound variant unless the app itself sits -somewhere writable. - -If you want the fully-portable version, install to a user-writable folder: - -- Re-run the MSI and choose a custom destination folder in the setup wizard - (e.g. `D:\Apps\VoiceStudio`), or -- From a terminal: - `msiexec /i VoiceStudio.Studio__x64_en-US.msi INSTALLDIR="D:\Apps\VoiceStudio"` - -On the next launch, pick **Portable** on the first-run setup screen. With the -default folder, what lives next to the exe afterwards: - - -``` -D:\Apps\VoiceStudio\ -├── VoiceStudio.exe ← the app -└── OmniVoiceStudio-Data\ ← the whole install, self-contained - ├── config.json ← install-mode + app settings - ├── env\ ← Python venv + backend code - └── data\ ← voices, projects, settings DB - └── models\ ← model weights (HF cache) -``` - -Prefer the default Program Files install? **Installed** mode is the same app — -data just lives in `%APPDATA%\OmniVoice` and the model cache in -`%LOCALAPPDATA%\OmniVoice\hf_cache`. +Use `bun run desktop-prod` to build and launch the production bundle, or +`bun run dist` to create a local NSIS installer in `electron/release/` without +publishing. See [Electron setup](../../electron/README.md) for backend +configuration. ## HF_TOKEN persistence @@ -377,7 +194,7 @@ for the engine actually loading. ```powershell $env:TORCH_COMPILE_DISABLE = "1" -bun run tauri:desktop-prod +bun run dev ``` The OOM this section describes is Windows-specific, but the toggle itself works @@ -396,37 +213,4 @@ See [docs/setup/huggingface-token.md](../setup/huggingface-token.md). Hit a wall? See [docs/install/troubleshooting.md](troubleshooting.md). -### Building the current-user MSI - -Build the system MSI first. `scripts/render-per-user-wix.py` requires -`--system-wxs` pointing to its fully rendered `release/wix/x64/main.wxs`, -in addition to the canonical `--source` template and `--output` destination. -The renderer preserves Tauri resource destinations while assigning distinct, -stable per-user component identities, HKCU registry keypaths, and uninstall -cleanup for nested resource folders. Missing or unrendered resources fail the -build. The canonical system installer retains its per-machine authoring. - -The per-user build reuses the system build's frontend output: its config clears -`beforeBuildCommand` so a second Vite build cannot replace the hashed files -referenced by the rendered WiX template. Keep using `tauri build` for the -per-user stage; it still recompiles the shell with its own product configuration. - -CI builds both scopes with a tiny executable, an external helper, and nested -resources using the CLI version locked in `bun.lock`. Its frontend-like build -hook rotates resource filenames, verifying the per-user build preserves the -system build's resource snapshot. The Windows MSI authoring -job runs after the test suite and preserves verbose WiX logs and rendered XML. -For focused diagnosis, dispatch CI with `windows_wix_diagnostic=true`; it skips -the other jobs and never signs, publishes, or installs the fixture bundles. - -The release smoke installs and removes the current-user MSI using a standard -Windows account. On disposable GitHub-hosted Windows Server runners, it explicitly -allows unmanaged MSI installations for the duration of this test, then restores -the previous Installer policy value and type (or its absence), even on failure. -This test-host preparation does not change installer privileges or user machines. -Verbose MSI logs are printed if installation or removal fails. The Windows CI -job also rejects an invalid MSI and verifies policy absence, value types, account -cleanup, and verbose failure logs using Windows PowerShell 5.1. - - Migration configuration is kept ASCII so Alembic can read it under Windows locale code pages as well as UTF-8. This applies to source installs and direct Alembic commands. From 20d4867a0c9ef1fa4e3117dde9b2bd6951b5d5d6 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:16:42 +0530 Subject: [PATCH 31/85] docs(docker): fix HF cache mount, tag semantics and analytics wording The image sets HF_HOME inside the data volume, so the /root/.cache/huggingface mount did nothing. Tags follow the release-publish model (:latest = main only, :sha- on every build), and the privacy line describes opt-in analytics. --- deploy/dockerhub-overview.md | 32 ++++++++++++---------- docs/install/docker.md | 51 +++++++++++++++++++++--------------- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/deploy/dockerhub-overview.md b/deploy/dockerhub-overview.md index 55cdcf7d4..ce80718c1 100644 --- a/deploy/dockerhub-overview.md +++ b/deploy/dockerhub-overview.md @@ -15,7 +15,10 @@ cloning, and cinematic video dubbing — fully local, with no cloud API keys or ![VoiceStudio — the open-source ElevenLabs alternative](https://raw.githubusercontent.com/debpalash/VoiceStudio/main/.github/assets/social-preview.png) VoiceStudio runs entirely on your own hardware (CUDA / ROCm / CPU -auto-detect) — nothing is sent to the cloud. This image is the **headless +auto-detect). Nothing leaves the machine without your explicit yes: product +analytics are opt-in (PostHog, content-free usage metadata only) behind a +first-run consent prompt — declining or skipping keeps them off, and +`-e OMNIVOICE_ANALYTICS_DISABLED=1` disables them entirely. This image is the **headless web-server build**: a FastAPI backend serving a pre-built React UI over HTTP, so you can run it on an AMD64 homelab box or GPU server and open the UI in a browser. @@ -29,9 +32,8 @@ which can be much slower. See the [architecture requirements](https://github.com/debpalash/VoiceStudio/blob/main/docs/install/docker.md#architecture) before pulling an image. -> The Tauri desktop app's auto-updater and update-channel toggle are -> **desktop-only** and do not apply to this image — to update, pull a newer tag -> and recreate the container. +> The desktop app's auto-updater is **desktop-only** and does not apply to this +> image — to update, pull a newer tag and recreate the container. **What you need:** 8 GB RAM (16 GB+ recommended), ~10 GB free disk for model weights + cache (20 GB+ comfortable), and optionally a GPU — 4 GB VRAM works @@ -58,7 +60,6 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - -v ~/.cache/huggingface:/root/.cache/huggingface \ palashdeb/omnivoice-studio:latest ``` @@ -77,7 +78,6 @@ docker run -d --name omnivoice --gpus all \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - -v ~/.cache/huggingface:/root/.cache/huggingface \ palashdeb/omnivoice-studio:latest ``` @@ -99,7 +99,6 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - -v ~/.cache/huggingface:/root/.cache/huggingface \ palashdeb/omnivoice-studio:rocm ``` @@ -118,15 +117,16 @@ publishing the web UI — see the [Docker install guide](https://github.com/debp | Tag | What you get | |-----|--------------| -| `:latest` | **Rolling preview** — latest commit on `main`, at or ahead of the last release. This is the preview channel; pin `:stable` for production. | -| `:stable` | Most recent versioned release (updated on every `v*` git tag) | -| `:0.5.6` | Exact release version | -| `:0.5` | Latest patch within the `0.5` minor | +| `:latest` | **Rolling preview** — the latest commit on `main`, at or ahead of the last release. Only `main` builds move it; publishing a release never does. Pin `:stable` for production. | | `:main` | Alias of the same rolling `main` build as `:latest` | -| `:sha-xxxxxxx` | A specific commit (produced by manual workflow dispatch) | +| `:stable` | Most recent stable release — moves when a GitHub Release is published | +| `:0.5.6` | Exact release version, published with its GitHub Release | +| `:0.5` | Latest released patch within the `0.5` minor | +| `:sha-xxxxxxx` | Exact commit — produced by every image build (main, releases, manual runs) | | `:rocm` | **AMD GPU (ROCm) build** of the rolling preview — the ROCm analogue of `:latest` | | `:stable-rocm`, `:0.5.6-rocm`, `:0.5-rocm`, `:sha-xxxxxxx-rocm` | ROCm builds of the corresponding tags above | +Release tags are published only when the GitHub Release is published. Preview builds always come from `main` and never version-sort below `:stable`, so upgrades flow naturally. The same images and tags are mirrored on GHCR at @@ -158,8 +158,12 @@ more), auto-detected and selectable in Settings. | Mount | Purpose | |-------|---------| -| `omnivoice-data:/app/omnivoice_data` | Project DB, user voices, settings, encrypted HF token — survives upgrades | -| `~/.cache/huggingface:/root/.cache/huggingface` | HF model cache — reuse the host cache to skip multi-GB re-downloads | +| `omnivoice-data:/app/omnivoice_data` | Project DB, user voices, settings, encrypted HF token, and the Hugging Face model cache — survives upgrades | + +The image sets `HF_HOME=/app/omnivoice_data/huggingface`, so models already +persist in that volume. To reuse an existing host cache instead, bind it with +`-v ~/.cache/huggingface:/app/omnivoice_data/huggingface` (files the container +adds there are root-owned). --- diff --git a/docs/install/docker.md b/docs/install/docker.md index a2b85625a..cc5e66634 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -5,6 +5,13 @@ The docker image bundles the backend; the UI is served over HTTP and you open it in a normal browser. It is built from the same maintained renderer as the Electron desktop app; controls that require native desktop access are hidden. +Inference runs inside the container. Nothing is sent off the machine without +your explicit yes: product analytics are opt-in (PostHog, content-free usage +metadata only) behind the same first-run consent prompt as the desktop app, +and declining or skipping it keeps them off. Set +`-e OMNIVOICE_ANALYTICS_DISABLED=1` to switch analytics off entirely. Model +weights download from Hugging Face when you install a model. + **Official images:** [`ghcr.io/debpalash/voicestudio`](https://github.com/debpalash/VoiceStudio/pkgs/container/voicestudio) and [`palashdeb/omnivoice-studio` on Docker Hub](https://hub.docker.com/r/palashdeb/omnivoice-studio) — same images, same tags. The former `ghcr.io/debpalash/omnivoice-studio` path remains a compatible alias. @@ -32,17 +39,18 @@ On an ARM64 host, pulling without an explicit platform can fail with > > | Tag | What you get | > |-----|--------------| -> | `:latest` | **Rolling preview** — latest commit on `main`, at or ahead of the last release. This is the preview channel; pin `:stable` for production. | -> | `:stable` | Most recent versioned release (updated on every `v*` git tag) | -> | `:0.5.6` | Exact release version | -> | `:0.5` | Latest patch within the 0.5 minor | +> | `:latest` | **Rolling preview** — the latest commit on `main`, at or ahead of the last release. Only `main` builds move it; publishing a release never does. Pin `:stable` for production. | > | `:main` | Alias of the same rolling `main` build as `:latest` | -> | `:sha-xxxxxxx` | Specific commit (produced by manual workflow dispatch) | +> | `:stable` | Most recent stable release — moves when a GitHub Release is published | +> | `:0.5.6` | Exact release version, published with its GitHub Release | +> | `:0.5` | Latest released patch within the 0.5 minor | +> | `:sha-xxxxxxx` | Exact commit — produced by every image build (main, releases, manual runs) | > | `:rocm` | **AMD GPU (ROCm) build** of the rolling preview — the ROCm analogue of `:latest` | > | `:stable-rocm`, `:0.5.6-rocm`, `:0.5-rocm`, `:sha-xxxxxxx-rocm` | ROCm builds of the corresponding CUDA tags above | > -> Versioning rule: preview builds always come from `main` and never -> version-sort below `:stable` — upgrades flow naturally. +> Release tags (`:X.Y.Z`, `:X.Y`, `:stable` and their `-rocm` forms) are +> published only when the GitHub Release is published. Previews come only from +> `main` (`:latest`, `:main`, `:rocm`); to follow previews, use `:latest`. > > **Note on desktop-only controls:** Electron updates, global shortcuts, native > file pickers, and runtime management do **not** apply to Docker. To update a @@ -71,7 +79,6 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - -v ~/.cache/huggingface:/root/.cache/huggingface \ ghcr.io/debpalash/voicestudio:latest ``` @@ -91,7 +98,6 @@ docker run -d --name omnivoice --gpus all \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - -v ~/.cache/huggingface:/root/.cache/huggingface \ ghcr.io/debpalash/voicestudio:latest ``` @@ -112,7 +118,6 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - -v ~/.cache/huggingface:/root/.cache/huggingface \ ghcr.io/debpalash/voicestudio:rocm ``` @@ -136,7 +141,6 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - -v ~/.cache/huggingface:/root/.cache/huggingface \ ghcr.io/debpalash/voicestudio:rocm ``` @@ -378,20 +382,25 @@ docker run -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ ## Volume mounts -Two paths are worth persisting across container restarts: +Persist one path across container restarts: + +| Mount | Purpose | +|-------|---------| +| `omnivoice-data:/app/omnivoice_data` | Project DB, user voices, settings, encrypted HF token, **and the Hugging Face model cache** — survives upgrades | -| Mount | Purpose | Why | -|-------|---------|-----| -| `omnivoice_data:/app/omnivoice_data` | Project DB, user voices, settings | Survives upgrade; encrypted HF token lives here | -| `~/.cache/huggingface:/root/.cache/huggingface` | HF model cache | Re-using your host's cache saves ~2.4 GB of re-downloads | +The image sets `HF_HOME=/app/omnivoice_data/huggingface`, so downloaded models +already live in that volume; mounting a host cache at +`/root/.cache/huggingface` has no effect. To reuse an existing host cache +instead, bind it to the image's cache path: +`-v ~/.cache/huggingface:/app/omnivoice_data/huggingface`. The container runs +as root, so files it adds there are root-owned on the host. ## Troubleshooting -- **Container reports 0.2.7 but image is tagged 0.3.x:** This was a workflow bug - (fixes #249, #251) — the `:latest` tag was not being updated on release tag - pushes. Pull the image again after the fix is merged: `docker pull ghcr.io/debpalash/voicestudio:latest`. - The running version is now shown in **Settings → About → Version** (read live - from the backend), so the web UI no longer displays a dash in Docker. +- **Container reports an older or newer version than expected:** `:latest` + follows `main`, not releases. For a released version, pull `:stable` or the + exact `:X.Y.Z` tag and recreate the container. The running version is shown + in **Settings → About → Version** (read live from the backend). - **Checking which version is running:** `docker exec python3 -c "import importlib.metadata; print(importlib.metadata.version('omnivoice'))"`, or hit the `/health` endpoint — it returns `{"status": "ok", "device": ..., "version": "0.3.x"}`. Use the container name listed by `docker compose ps` (or `omnivoice` for the `docker run` examples). - **Watching startup:** the port answers within about a second of container start, but heavy initialization (PyTorch, API routes, database migration) From 8594fa99a78e0158ac5add08bcca6dad70743ab9 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:16:42 +0530 Subject: [PATCH 32/85] docs(updates): document stable-only desktop updates The Preview channel and its nightly feed no longer exist; previews come from main or the Docker :latest image. --- docs/update-channels.md | 91 +++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 53 deletions(-) diff --git a/docs/update-channels.md b/docs/update-channels.md index 17c7655fa..e3f23e540 100644 --- a/docs/update-channels.md +++ b/docs/update-channels.md @@ -1,32 +1,41 @@ -# Update channels (Stable / Preview) - -VoiceStudio checks for updates shortly after launch and every six hours while -it stays open. You choose **which builds** it offers you with the update -channel in **Settings → Updates → Update channel**. - -| Channel | What you get | Who it's for | -|---------|--------------|--------------| -| **Stable** (default) | The latest tagged `vX.Y.Z` release. | Everyone. This is the default on every install and every launch. | -| **Preview** | The latest `main` build (a rolling `preview` prerelease). Newer features, less testing. Falls back to a stable release if one is ahead. | Users who want to try fixes/features before they're tagged, and report issues. | - -Switching is instant — the next automatic or manual check uses your chosen -channel. Downloads show transferred size, total size, speed, percentage and -ETA, and can continue while synthesis, transcription or dubbing work runs. -VoiceStudio only enables **Restart to update** after the package has downloaded -and passed the updater's signature/checksum verification, and blocks that -restart until active work has finished and current drafts are flushed. - -There are **no accounts or updater telemetry**. Stable checks fetch only the -Stable manifest. Preview checks compare the small Stable and Preview manifests, -then use whichever has the newer version; package download begins when the user -asks. Both channels use a different release location: - -- Stable → `releases/latest/download/` -- Preview → `releases/download/preview/` - -The Electron feed publishes a platform/architecture-specific manifest with a -SHA-512 package checksum; the Tauri feed publishes its signed `latest.json`. -Each desktop shell rejects a package that fails its integrity check. +# Desktop updates + +VoiceStudio's desktop app offers **stable releases only**. It checks for an +update about 10 seconds after launch and every six hours while it stays open; +**Settings → Updates** also has a manual check. The app never downloads an +update until you ask, and an available update is only offered, never forced. + +| Build | Where it comes from | Who it's for | +|-------|--------------------|--------------| +| **Stable** | The latest published `vX.Y.Z` GitHub Release. | Everyone. This is the only update channel. | + +There is no Preview update channel and no preview desktop feed. To try changes +before they are released: + +- **Run `main` from source** — see the platform install guides + ([macOS](install/macos.md#building-from-source) · + [Windows](install/windows.md#building-from-source) · + [Linux](install/linux.md#building-from-source)), or build and install a + package from `main` with the [install script's `--main` mode](install/script.md#building-main). +- **Docker** — `ghcr.io/debpalash/voicestudio:latest` follows `main`; see + [image tags](install/docker.md#image-tags). + +Downloads show transferred size, total size, speed, percentage and ETA, and +can continue while synthesis, transcription or dubbing work runs. VoiceStudio +only enables **Restart to update** after the package has downloaded and passed +the updater's checksum verification, and blocks that restart until active work +has finished and current drafts are flushed. + +There are **no accounts or updater telemetry**. A check fetches one small +platform/architecture-specific manifest from +`github.com/debpalash/VoiceStudio/releases/latest/download/`; it carries the +package's SHA-512 checksum, and the app rejects a package that fails it. On +Linux the in-app updater uses the AppImage; update a `.deb` installation by +installing the newer `.deb`. Source checkouts do not self-update. + +The archived Tauri app keeps its frozen, signed `latest.json` feeds at +immutable URLs; they never offer Electron installers. Move to Electron with the +[migration guide](electron-migration.md). ## Your data during updates @@ -56,27 +65,3 @@ drift after an app update is reconciled **in place** with `uv sync`, and a failed sync keeps the previous environment working. The venv is only ever rebuilt when its interpreter is *confirmed* broken (structural check + a direct probe) or when you explicitly use **Clean & Retry**. - -## For maintainers — how previews are built - -Preview builds come from **`main`**, two ways: - -- **Nightly (automatic).** A scheduled job (07:00 UTC) rebuilds the rolling - `preview` prerelease from `main` — but only when `main` actually moved in the - last day, so idle days cost nothing. Preview is never more than ~24h behind - `main`. -- **On demand.** **Actions → Desktop Release → Run workflow** on `main`, set - **publish_preview = true**. Useful to refresh immediately without waiting - for the nightly. Previews build from `main` **only** (hard rule, owner-set - 2026-07-16) — the preview-gate refuses any other branch; to preview a fix, - merge it to `main` first. - -Either way it builds the matrix and publishes/updates a single rolling -`preview` **prerelease** — always flagged prerelease, and carrying the same -platform set as stable (both verified in CI after each preview publish) — with -its own signed `latest.json`. The tagged `latest` stable release is never -affected. Preview users get the new build on their next check; stable users see -nothing. - -To stop offering previews, delete the `preview` release/tag on GitHub — the -Preview channel then falls back to stable. From afec17a2c3c5fff6236ad0d1ccb9a53061cb56d0 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:16:42 +0530 Subject: [PATCH 33/85] docs: describe the Electron tree, version source and path authorization STRUCTURE.md drops frontend/src, src-tauri and removed scripts; the maintainer skill names the root package.json as the version source; api-auth credits Electron main with native path capabilities and lists app://voicestudio. --- docs/STRUCTURE.md | 78 ++++++++++++-------------- docs/api-auth.md | 25 +++++---- skills/voicestudio-maintainer/SKILL.md | 25 +++++---- 3 files changed, 64 insertions(+), 64 deletions(-) diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index 04624a3c4..acb6d75da 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -14,8 +14,9 @@ VoiceStudio/ │ ├── pyproject.toml ⟵ Python project manifest (+ pytest / lint config) ├── uv.lock ⟵ Python lockfile -├── package.json ⟵ monorepo manifest (Bun workspaces + Turborepo) -├── bun.lock ⟵ JS lockfile — repo-root, covers frontend/ too +├── package.json ⟵ monorepo manifest (Bun workspaces + Turborepo) and THE app +│ version (pyproject.toml + backend/core/version.py mirror it) +├── bun.lock ⟵ JS lockfile — repo-root, covers the electron/ workspace ├── turbo.json ⟵ turborepo pipeline │ ├── .coderabbit.yaml ⟵ CodeRabbit PR review config (fed CLAUDE.md) @@ -56,32 +57,22 @@ VoiceStudio/ │ ├── config/models.yaml model catalogue │ └── tests/ the isolated pytest session — see "Where tests live" │ -├── electron/ ⟵ maintained Electron desktop: main, preload, renderer -│ ├── src/main/ lifecycle, backend supervisor, updater, native IPC -│ ├── src/preload/ typed renderer bridge -│ ├── src/renderer/ React 19 desktop UI -│ ├── tests/ packaged and workflow smoke tests -│ └── electron-builder.config.mjs +├── electron/ ⟵ the only desktop app and web UI (Electron + React 19) +│ ├── src/main/ lifecycle, backend supervisor, runtime setup, updater, native IPC +│ ├── src/preload/ typed contextBridge → window.voicestudio +│ ├── src/renderer/ desktop React app: routes/, features/, components/, i18n/locales/ +│ ├── src/shared/ modules shared by the desktop and browser builds (api/, i18n/, +│ │ store/, ui/, utils/) — not a runnable app +│ ├── tests/ packaged-app, installer and workflow smoke tests +│ ├── build/ packaging inputs: icons, entitlements, Info.plist +│ ├── scripts/ dev launcher, AppImage update-info embedding +│ ├── vite.web.config.ts browser build (`bun run build:web`) → frontend/dist/ +│ └── electron-builder.config.mjs installers: DMG/zip, NSIS, AppImage + deb │ ├── native/desktop-bridge/ ⟵ Rust helper used by Electron │ -├── frontend/ ⟵ legacy web UI plus shared app metadata -│ ├── package.json THE maintained app version -│ ├── src/ -│ │ ├── pages/ one file per top-level view -│ │ ├── components/ reusable UI (+ audiobook/ clone/ dub/ gallery/ settings/ …) -│ │ ├── ui/, lib/ shared primitives and helpers -│ │ ├── api/ typed API clients, one per router group -│ │ ├── store/ Zustand slices (+ persisted-state migrations) -│ │ ├── hooks/ custom React hooks -│ │ ├── i18n/locales/ the ONLY home for user-facing strings -│ │ ├── config/, data/, assets/, utils/ -│ │ └── test/ vitest setup + visual-test helpers -│ ├── e2e/, e2e-perf/, e2e-prod/ Playwright suites: functional, perf, packaged bundle -│ ├── src-tauri/ archived v0.5.3 Tauri shell and retained shared assets -│ │ ├── capabilities/, icons/, wix/, debian/, appimage/ packaging inputs -│ │ └── tests/ -│ └── public/ +├── frontend/dist/ ⟵ build output of `bun run build:web` (gitignored): the web UI +│ the backend serves in Docker, bundled into Electron packages │ ├── omnivoice/ ⟵ the underlying TTS model package │ ├── models/ @@ -98,12 +89,14 @@ VoiceStudio/ │ ├── smoke/ fast end-to-end checks (own CI job, HF_HUB_OFFLINE=1) │ ├── evals/ quality evals (evals.yml) │ ├── probe/, fixtures/ -│ └── frontend/ Node-based frontend tests (legacy; vitest is the default) +│ └── frontend/ `node:test` checks for shared helpers and dev scripts │ ├── scripts/ ⟵ dev / build / release scripts (shell, python, mjs) -│ ├── install.sh / install.ps1 universal installers -│ ├── desktop-*.mjs dev, prod and fresh desktop launchers -│ ├── smoke-test.sh end-to-end validation +│ ├── install.sh / install.ps1 shell + PowerShell installers served at voicestudio.sh/install +│ ├── dev-backend.mjs `bun run dev:api` backend supervisor +│ ├── setup.py `bun run setup:api` post-sync platform setup (incl. ROCm opt-in) +│ ├── electron-smoke-test.mjs `bun run smoke-test` packaged-app launch check +│ ├── prepare_electron_release.py, check_electron_release_assets.py release asset gates │ ├── check-docs-drift.py the docs-drift.yml checker (docs/features.yaml is canonical) │ └── build-omnivoice-tts.sh builds the bin/ sidecars │ @@ -113,15 +106,14 @@ VoiceStudio/ │ skills-lock.json — followed by path, never symlinked ├── skills/ ⟵ skills this repo publishes (voicestudio, voicestudio-maintainer) │ -├── infra/ ⟵ edge/deploy workers (not the Docker deploy path) -│ └── install-redirect/ voicestudio.sh/install — UA-sniffing installer worker -│ -├── deploy/ ⟵ Docker deployment configs +├── deploy/ ⟵ Docker deployment configs + the installer worker │ ├── Dockerfile CUDA by default; CI builds the ROCm variant from the same │ │ file via BASE_IMAGE / GPU_FLAVOR overrides │ ├── docker-compose.yml one-click local deployment │ ├── torch-constraints.txt pinned torch resolution for the image -│ └── dockerhub-overview.md synced to the Docker Hub overview page at release +│ ├── dockerhub-overview.md synced to the Docker Hub overview page at release +│ └── install-worker/ live voicestudio.sh/install Cloudflare worker; bundles +│ scripts/install.sh + install.ps1 (docs/install/script.md) │ ├── docs/ ⟵ developer docs, screenshots, branding │ ├── ROADMAP.md where this project is going @@ -143,8 +135,9 @@ VoiceStudio/ ├── omnivoice_data/ ⟵ Docker bind-mount target (gitignored) │ DB + HF cache live here when running via compose │ -├── .github/workflows/ ⟵ ci, docker, release, security, docs-drift, evals, -│ install-smoke, build-omnivoice-tts +├── .github/workflows/ ⟵ ci, electron-build, electron-release, docker, security, +│ docs-drift, evals, install-smoke, build-omnivoice-tts, +│ cla, commit-identity, cosyvoice-dependencies └── .git/ ``` @@ -156,7 +149,7 @@ VoiceStudio/ 3. **Each subdirectory owns one concern.** If you can't describe what goes in a directory in one sentence, it's wrong. -4. **Every package has a manifest.** `backend/`, `frontend/`, `omnivoice/` each have their own deps declared via `pyproject.toml` / `package.json` — they are independently testable. The JS lockfile is the **repo-root** `bun.lock` (Bun workspace), and `deploy/Dockerfile` installs from it with `--frozen-lockfile`. +4. **Every package has a manifest.** `backend/`, `electron/`, `omnivoice/` each have their own deps declared via `pyproject.toml` / `package.json` — they are independently testable. The JS lockfile is the **repo-root** `bun.lock` (Bun workspace), and `deploy/Dockerfile` installs from it with `--frozen-lockfile`. ## Where tests live @@ -167,19 +160,19 @@ Three homes, each with its own runner. CI runs all three inside the single `test |---|---|---| | `tests/` | `pytest tests/` — the `testpaths` default | The main suite. Its `conftest.py` points `OMNIVOICE_DATA_DIR` at a throwaway dir so a run can never touch the developer's real app state (#878). | | `backend/tests/` | `pytest backend/tests/` — its own pytest session (the `Run pytest (backend/tests, isolated)` step) | Runs as an isolated session against `backend/`'s bare imports. Its `conftest.py` sets the same hermetic data dir; **never** reintroduce module-level `sys.modules` stubs there — they leak process-wide at collection time and poison mixed runs. | -| `frontend/src/**/*.test.{js,jsx,ts,tsx}` | `bun run test` (vitest, jsdom) | Co-located with the component under test. `frontend/e2e*/` hold the Playwright suites; `tests/frontend/` is the older `node:test` set. | +| `electron/src/**/*.test.{ts,tsx}` | `bun run test` (vitest, jsdom); CI runs it through `bun run check:electron` | Co-located with the module under test. `electron/tests/` holds the packaged-app and workflow smoke tests; `tests/frontend/` is the older `node:test` set (`bun run test:frontend`). | ## What lives where | Kind of thing | Goes in | |---|---| -| User-facing product code | `backend/`, `frontend/` | +| User-facing product code | `backend/`, `electron/` | | The TTS model (independent of the studio) | `omnivoice/` | | A new TTS/ASR engine adapter | `backend/engines//` | | Everything executable but not user-facing | `scripts/` | | Prebuilt platform sidecars | `bin/` | | Python tests | `tests/` (or `backend/tests/` when the isolated session is required) | -| Frontend unit tests | next to the component, as `*.test.jsx` | +| Electron / UI unit tests | next to the module, as `*.test.ts(x)` | | Developer + user docs (Markdown) | `docs/` | | Architecture decision records (ADRs) | `docs/adr/` | | Agent-facing docs | `docs/agents/` | @@ -219,8 +212,7 @@ The current flat layout works fine for the current size. If the project grows to VoiceStudio/ ├── apps/ │ ├── api/ ← was backend/ -│ ├── web/ ← was frontend/ -│ └── desktop/ ← was electron/ +│ └── desktop/ ← was electron/ (desktop + browser UI) ├── packages/ │ ├── omnivoice-model/ ← was omnivoice/ │ └── tts-adapters/ ← new; the pluggable TTS interface from ROADMAP phase 3 @@ -238,7 +230,7 @@ VoiceStudio/ - `backend.spec` (`['backend/main.py']`, `pathex=['.']`) - Electron Builder, native-helper, updater and packaged-smoke paths - every import that reads `from backend.main import …` (tests, scripts) -- `frontend/package.json` as the version source of truth and its active mirrors +- the root `package.json` as the version source of truth and its mirrors Migrate when adding the second `apps/*` or the second `packages/*`. Not before. diff --git a/docs/api-auth.md b/docs/api-auth.md index 7d0767fc3..1f0355923 100644 --- a/docs/api-auth.md +++ b/docs/api-auth.md @@ -91,7 +91,7 @@ the operator's port mapping controls access in that mode. The API key is the backend's durable root credential for a GPU box, Docker container, or reverse-proxied host. Direct API clients may send it on each -request. The first-party browser/Tauri UI instead exchanges it once for a +request. The first-party browser/Electron UI instead exchanges it once for a short-lived administrator session and never stores the master. Set it on the **backend** process: @@ -167,7 +167,8 @@ The bundled UI uses a narrower protocol: 1. `POST /api/auth/session` receives the master in an `Authorization` header exactly once and selects `{"transport":"cookie"}` for exact same-origin - browsers or `{"transport":"bearer"}` for Tauri/cross-origin clients. + browsers or `{"transport":"bearer"}` for cross-origin clients (including + the Electron app's remote-backend connections). 2. Cookie transport returns `204` and sets `ov_session` as HttpOnly, SameSite=Strict, path `/`, with an eight-hour maximum lifetime. Bearer transport returns an opaque `ovs_admin_session_…` value which the UI keeps @@ -298,10 +299,10 @@ Managed sidecar installation remains true-loopback-only even with an API key. Its installer fetches mutable source and creates an editable environment, so it must be run directly on that machine until the source supply chain is pinned. -Host paths are never selected through HTTP. The native Tauri process validates -model-cache and export destinations plus custom FFmpeg/FFprobe binaries, writes -a private one-shot capability, and only that opaque authorization reaches the -backend. `/export` therefore accepts an `authorization` token, never a +Host paths are never selected through HTTP. The Electron main process validates +model-cache and export destinations (chosen in a native save/open dialog) plus +custom FFmpeg/FFprobe binaries, writes a private one-shot capability, and only +that opaque authorization reaches the backend. `/export` therefore accepts an `authorization` token, never a `destination_path`; revealing an arbitrary exported path runs in the native process, while the HTTP fallback is limited to the server-owned data root. `/system/set-env` does not accept executable-path keys at all. Server mode and @@ -319,9 +320,10 @@ credential at all**, because the API-key middleware waved it through as Everything above gates *authentication*. A **browser** frontend served from a different origin than the backend hits a separate wall first: CORS. The -backend's allow-list defaults to loopback + Tauri origins only -(`http://localhost:`, `http://127.0.0.1:`, -`tauri://localhost`, `http://tauri.localhost`), so opening a dev/source UI via +backend's allow-list defaults to loopback + desktop origins only +(`http://localhost:`, `http://127.0.0.1:`, Electron's +`app://voicestudio`, and the archived Tauri app's `tauri://localhost` / +`http://tauri.localhost`), so opening a dev/source UI via a LAN IP (e.g. `http://192.168.1.159:3901` talking to `…:3900`) blocks every request with *"Missing Header: Access-Control-Allow-Origin"* — regardless of `OMNIVOICE_SERVER_MODE` or `OMNIVOICE_TRUSTED_NETWORKS`, neither of which @@ -330,13 +332,14 @@ touches CORS (#1348). Add the exact origin the browser shows in its address bar: ```bash -export OMNIVOICE_ALLOWED_ORIGINS="http://192.168.1.159:3901,http://localhost:3901,http://127.0.0.1:3901,tauri://localhost,http://tauri.localhost" +export OMNIVOICE_ALLOWED_ORIGINS="http://192.168.1.159:3901,http://localhost:3901,http://127.0.0.1:3901,app://voicestudio" ``` Each entry must be a bare origin — `scheme://host:port`, exactly what the browser sends in its `Origin` header — with no path and no trailing slash (`http://192.168.1.159:3901/` would never match). The variable **replaces** -the default list, so restate the loopback/Tauri origins alongside your own. (The in-app LAN share and Tailscale flows in +the default list, so restate the loopback and `app://voicestudio` origins +alongside your own (add the Tauri origins only for an archived Tauri client). (The in-app LAN share and Tailscale flows in [docs/sharing.md](sharing.md) don't need this — they serve UI and API from the same origin.) If you only moved the Vite dev server's port, set `OMNIVOICE_UI_PORT` instead and the default list follows it. diff --git a/skills/voicestudio-maintainer/SKILL.md b/skills/voicestudio-maintainer/SKILL.md index 25b47d53e..4aca89ace 100644 --- a/skills/voicestudio-maintainer/SKILL.md +++ b/skills/voicestudio-maintainer/SKILL.md @@ -58,18 +58,23 @@ bun run dist # local installers; publishing disabled `electron/src/main/` owns lifecycle, IPC, native helpers, and backend supervision; `electron/src/preload/` exposes the renderer bridge; `electron/src/renderer/src/` contains the React app. Keep privileged filesystem/process work out of the renderer. -`backend/` supplies the shared Python API; `native/desktop-bridge/` supplies native -capabilities. `frontend/` still serves the browser UI and legacy Tauri shell: do not -remove it or rename internal `omnivoice` packages, environment keys, or data paths -as a branding cleanup. - -Electron is the default desktop. Tauri is retained for its final sunset update. -Use `electron-build.yml` for artifact-only four-platform packaging rehearsals; -inspect its results for Windows, Linux, macOS Intel, and macOS Apple Silicon. +`electron/src/shared/` holds modules shared with the browser build; it is not a +runnable app. `backend/` supplies the shared Python API; `native/desktop-bridge/` +supplies native capabilities. `frontend/` only receives the browser build output +(`bun run build:web` → `frontend/dist/`, served by the backend in Docker). Do not +rename internal `omnivoice` packages, environment keys, or data paths as a +branding cleanup. + +Electron is the only desktop app and web UI. The Tauri shell is removed; do not +restore it. Its final v0.5.3 updater feeds are immutable compatibility assets, +and old Tauri installs move over with `docs/electron-migration.md`. +Use `electron-build.yml` for artifact-only packaging rehearsals; inspect its +results for Windows x64, Linux, macOS Intel, and macOS Apple Silicon (Windows +ARM64 is an experimental leg that may fail without blocking). Do not dispatch release/publishing workflows to test packaging. Signing, updater migration, and successful installation are separate checks from a green build. -The app version still comes from `frontend/package.json`; do not move or bump it -without an explicit versioning task. +The app version comes from the root `package.json` (mirrored in `pyproject.toml` +and `backend/core/version.py`); do not bump it without an explicit versioning task. For backend tests, use the repo's CI dependencies and an empty temporary `HF_HUB_CACHE` with `HF_HUB_OFFLINE=1`; installed developer models must not hide From 2000a5ae4994c480996d866519fb265b4824f7a8 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:16:42 +0530 Subject: [PATCH 34/85] docs: mark migration-era notes historical and retire Tauri release docs Electron feature notes flag their Tauri references as migration history, the obsolete Tauri build tracker is removed, and macOS signing verification now describes the Electron release workflow. --- docs/DESKTOP_RELEASE.md | 3 + docs/competitive-analysis.md | 3 + docs/desktop-build.md | 218 ----------------------------- docs/electron-connections.md | 6 + docs/electron-dubbing.md | 6 + docs/electron-gallery.md | 6 + docs/electron-longform.md | 6 + docs/electron-parity.md | 12 +- docs/electron-performance.md | 6 + docs/electron-privacy.md | 6 + docs/electron-projects.md | 6 + docs/electron-runtime.md | 6 + docs/electron-storage.md | 6 + docs/electron-tools.md | 6 + docs/electron-transcriptions.md | 6 + docs/electron-voice-design.md | 6 + docs/macos-signing-verification.md | 179 +++++++++-------------- 17 files changed, 151 insertions(+), 336 deletions(-) delete mode 100644 docs/desktop-build.md diff --git a/docs/DESKTOP_RELEASE.md b/docs/DESKTOP_RELEASE.md index 690aa0a0c..b7645aea0 100644 --- a/docs/DESKTOP_RELEASE.md +++ b/docs/DESKTOP_RELEASE.md @@ -5,6 +5,9 @@ > signing, and publication instructions are not maintained and must not be used > for a current release. Electron is the only maintained desktop app; follow > [RELEASING.md](RELEASING.md) for all current build and publication work. +> The files it names (`frontend/src-tauri/`, `.github/workflows/release.yml`, +> `scripts/desktop-prod.sh`) were removed with the Tauri shell; read them from git +> history at the `v0.5.3` tag. A shippable macOS (and eventually cross-platform) desktop release where the user drags the `.app` to `Applications`, double-clicks once, and does **everything else from the UI** — dependency runtime, model weights, first-run consent, all inside the app. diff --git a/docs/competitive-analysis.md b/docs/competitive-analysis.md index ffc5d2cf9..83fb87f44 100644 --- a/docs/competitive-analysis.md +++ b/docs/competitive-analysis.md @@ -1,5 +1,8 @@ # Competitive Analysis — voicebox · pyvideotrans · Patter (+ second-tier landscape) +> **Dated analysis.** `scripts/validate-install-docs.py`, cited below as an existing +> gate, has since been removed; `scripts/check-docs-drift.py` (`docs-drift.yml`) remains. + *Compiled 2026-06-11 from four parallel research passes (one per competitor repo + a full feature-surface inventory of this codebase). Star counts and versions are as-of-date snapshots. VoiceStudio grades (A–D) come from the self-inventory: code diff --git a/docs/desktop-build.md b/docs/desktop-build.md deleted file mode 100644 index 9882eb19e..000000000 --- a/docs/desktop-build.md +++ /dev/null @@ -1,218 +0,0 @@ -# Desktop build — progress tracker - -> **Archived Tauri plan.** This document records the retired v0.5.3 shell. -> Current desktop work lives in `electron/` and [Electron setup](../electron/README.md). - -Working doc for the desktop release effort. Every box maps to a concrete -deliverable; mark `[x]` when verified end-to-end on a fresh environment, -not just "code compiles." See `docs/DESKTOP_RELEASE.md` for the full -engineering plan that grounds these milestones. - -**Primary target:** macOS Apple Silicon (arm64), unsigned. -**Stretch targets:** macOS Intel, Windows x64, Linux — parked in CI matrix -until the arm64 path is green end-to-end. - ---- - -## Source of truth files - -| Concern | Owner file | -|---|---| -| Backend freeze spec | `backend.spec` | -| PyInstaller runtime hooks | `backend/hooks/*.py` (TBD) | -| Tauri sidecar launcher | `frontend/src-tauri/src/lib.rs` | -| Tauri bundle config | `frontend/src-tauri/tauri.conf.json` | -| HF download progress | `backend/utils/hf_progress.py` | -| First-run wizard endpoints | `backend/api/routers/setup.py` | -| First-run wizard UI | `frontend/src/pages/SetupWizard.jsx` (TBD) | -| CI release matrix | `.github/workflows/release.yml` | -| Reference implementation | — | - ---- - -## Phase A — Frozen backend binary (✅ 2026-04-21) - -- [x] Add runtime hooks to `backend/hooks/` - - [x] `pyi_rth_numpy_compat.py` — pre-imports numpy to prime the C ext - - [x] `pyi_rth_torch_compiler_disable.py` — disables dynamo/inductor via env -- [x] Wire hooks into `backend.spec` via `runtime_hooks=[...]` -- [x] Add Apple Silicon exclude list to `backend.spec` (`nvidia.*`, `triton`, `flash_attn`) -- [x] `uv run pyinstaller backend.spec --noconfirm --clean` produces a clean bundle — **140 s build time** -- [x] `./dist/omnivoice-backend/omnivoice-backend` starts and serves `/system/info` — **~23 s cold start** (not ≤3 s as targeted, but acceptable behind splash screen in Phase D) -- [x] Frozen binary serves all core endpoints (`/system/info`, `/setup/status`, `/engines`) -- [x] `hf_progress` patch installs on frozen start (confirmed via log) -- [ ] Frozen binary transcribes Fireship fixture → Spanish dub end-to-end (deferred to Phase B — needs Tauri WebView for the frontend, or direct `curl`-driven harness) -- [ ] **Bundle size ≤600 MB** — **currently 1.1 GB, ~2× over target.** Likely shavable to ~700 MB by excluding `torch.distributed.*`, transformers bloat, scipy tests. Not a Phase A blocker; cleanup tracked as Phase A.1. - -**Verified:** -```bash -./dist/omnivoice-backend/omnivoice-backend -# → "Uvicorn running on http://0.0.0.0:8000" -curl -s http://127.0.0.1:8000/system/info # ✓ returns JSON -curl -s http://127.0.0.1:8000/setup/status # ✓ {"models_ready":true,...} -curl -s http://127.0.0.1:8000/engines # ✓ omnivoice/voxcpm2/moss-tts-nano listed -``` - -**Lessons learned (feeding into Phase B):** -- PyInstaller buffers stdout by default — Tauri sidecar spawn must set `PYTHONUNBUFFERED=1` so logs surface promptly. -- `main.py`'s `if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)` is what makes the frozen binary serve. Keep it. -- `frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist")` at `main.py:172` will never resolve in a frozen build; frontend assets come from Tauri's WebView instead, not the Python backend. Guard is fine; note for Phase B. - -### Phase A.1 — Bundle size reduction (optional, before Phase D) - -- [ ] Add `torch.distributed.*` to excludes (not used on Apple Silicon single-device inference) -- [ ] Exclude `scipy.special.tests.*`, `numpy.tests.*`, `torch.testing.*` -- [ ] Exclude `transformers.models.*` for non-used model families -- [ ] Target: <700 MB bundle - ---- - -## Phase B — Tauri launches the frozen sidecar (in progress 2026-04-21) - -- [ ] ~~Change backend default port 8000 → 17493~~ — deferred. Port-switching ripples through the frontend API client + bench scripts + install scripts. Keep 8000 for now; switch in a dedicated refactor when we need to. -- [x] Rewrite `lib.rs::setup` to: - - [x] Probe `/system/info` on port 8000 and attach if responding (`backend_healthy()`) - - [x] Kill orphans on port 8000 via `lsof -ti :8000 | xargs kill -9` - - [x] Launch bundled `Contents/Resources/backend/omnivoice-backend/omnivoice-backend` if free - - [x] Fall back to `uv run uvicorn ...` in dev (when bundled binary not present) - - [x] Set `PYTHONUNBUFFERED=1` on sidecar env so backend logs flush in real time - - [x] Export `OMNIVOICE_FFMPEG` pointing at bundled ffmpeg + prepend its dir to `PATH` -- [x] Wire `tauri.conf.json` `bundle.resources`: - - [x] `../../dist/omnivoice-backend` → `backend/omnivoice-backend/` - - [x] ~~`binaries/ffmpeg` → `bin/ffmpeg`~~ — **removed**. Hit `Permission denied (os error 13)` at bundle time (brew shim carried `com.apple.provenance` xattr that `xattr -c` couldn't strip under SIP). Dropped the separate resource: `imageio_ffmpeg` already ships a 47 MB static arm64 ffmpeg inside the PyInstaller bundle at `_internal/imageio_ffmpeg/binaries/ffmpeg-macos-aarch64-v7.1`, and `find_ffmpeg()` picks it up via `imageio_ffmpeg.get_ffmpeg_exe()` — zero extra bundling. -- [x] Rust compile clean — `cargo check` passes -- [x] `bunx tauri build` produces an `.app` (1.3 GB) — bundle contents verified: `Contents/Resources/backend/omnivoice-backend/omnivoice-backend`. ffmpeg travels inside the backend resource (see above). -- [x] `bunx tauri build` produces a `.dmg` — **400 MB** ✅ (well under the 600 MB target). -- [x] Fix API host in production build — `frontend/src/api/client.ts` previously used `API = ''` in prod, which caused relative fetches against `tauri://localhost` to fail with *"The string did not match the expected pattern"* in the Settings → Models tab. Now hardcodes `http://localhost:8000` so the webview always reaches the bundled sidecar. CORS + CSP already allow it. -- [ ] `.app` opens, connects to bundled backend, ingests/transcribes without error (manual test pending) - -### DMG packaging failure to investigate - -Tauri's `bundle_dmg.sh` runs `hdiutil` to convert the intermediate RW DMG → compressed read-only DMG. On our 1.7 GB payload this failed silently; next session's task is to run the shell script manually and capture stderr. Possible fixes in priority order: -1. Shrink the backend bundle (Phase A.1 — we're 1.1 GB, target is ~500 MB; cuts DMG pipeline latency and works around many hdiutil edge cases). -2. Pass `hdiutilArgs` in `tauri.conf.json` → `["-format", "UDZO", "-imagekey", "zlib-level=1"]` — cheaper compression, faster, fewer hdiutil quirks. -3. If hdiutil errors with disk space: `TMPDIR=/Volumes/other bunx tauri build` to route the scratch volume elsewhere. -4. Last resort: build `.app.tar.gz` with `create-dmg` externally. - -**Verification target:** -```bash -open frontend/src-tauri/target/release/bundle/macos/"VoiceStudio.app" -# The window should open, segment table should render from a fresh drop. -``` - ---- - -## Phase C — First-run wizard + HF progress (✅ 2026-04-21) - -- [x] **`backend/utils/hf_progress.py`** — tqdm monkey-patch -- [x] **`backend/api/routers/setup.py`** - - [x] `GET /setup/status` — missing + disk-free - - [x] `GET /setup/download-stream` — SSE stream - - [x] `POST /setup/warmup` — background model load - - [x] `GET /models` — every known model + install state (Phase M seed) - - [x] `POST /models/install` — single-repo install (progress via SSE) - - [x] `DELETE /models/{repo_id}` — evict cached revisions, free disk -- [x] `frontend/src/pages/SetupWizard.jsx` + `.css` - - [x] Mounts on boot, calls `/setup/status` - - [x] Per-file progress bars from SSE - - [x] Disk-space error state if `disk_free_gb < min_free_gb` - - [x] Polls `/setup/status` every 5 s during install to detect completion -- [x] Route guard in `App.jsx`: `if (setupNeeded) return ` -- [x] Settings → Models tab: `ModelStoreTab` replaces the old read-only summary - - [x] Lists every KNOWN_MODEL with status badge, role, size, repo_id - - [x] Install / Reinstall / Delete buttons per row - - [x] Aggregate "on-disk" footer - - [x] Live per-file progress bars driven by the shared SSE stream - -**Verification:** -```bash -# Simulate fresh install -mv ~/.cache/huggingface/hub /tmp/hf-hub.bak -bun run dev -# Wizard appears, progress ticks, UI unlocks when done. -mv /tmp/hf-hub.bak ~/.cache/huggingface/hub # restore -``` - ---- - -## Phase D — DMG + clean-machine test - -- [ ] `frontend/src-tauri/tauri.conf.json` `bundle.targets = ["dmg", "app"]` -- [ ] `scripts/build_desktop.sh` — one-shot: PyInstaller → Tauri build → DMG -- [ ] Build produces `VoiceStudio_0.1.0_aarch64.dmg` -- [ ] DMG size ≤600 MB -- [ ] DMG runs on a fresh macOS user account (no brew/uv/bun present) - - [ ] Mount → drag to Applications → right-click → Open (Gatekeeper override) - - [ ] First launch shows setup wizard, models download cleanly - - [ ] End-to-end: ingest YouTube URL → transcribe → translate → generate → play -- [ ] README section: "Download DMG" with right-click → Open instructions - -**Verification:** -```bash -bash scripts/build_desktop.sh -ls -lh frontend/src-tauri/target/release/bundle/dmg/*.dmg -# Copy to fresh user account via System Settings → Users → Add. -# Log in, install DMG, walk the whole flow. -``` - ---- - -## Phase E — Signing + notarization (blocked on Apple Developer ID) - -- [ ] Apple Developer ID ($99/yr) -- [ ] Generate signing cert + App Store Connect API key -- [ ] Store secrets in GitHub: `APPLE_SIGNING_IDENTITY`, `APPLE_API_KEY`, `APPLE_API_ISSUER`, `APPLE_PROVIDER_SHORT_NAME` -- [ ] Enable signing in `tauri-apps/tauri-action@v0.6` step -- [ ] **Post-build DMG re-notarize step** (workaround for Sequoia): - - [ ] `xcrun notarytool submit "$DMG" --wait --apple-id "$APPLE_ID" --password "$APP_SPECIFIC_PASSWORD"` - - [ ] `xcrun stapler staple "$DMG"` - - [ ] Re-upload stapled DMG as release asset -- [ ] Verify on clean Mac: no Gatekeeper warning on first open - ---- - -## Phase F — CI release matrix (parallel track) - -- [x] **`.github/workflows/release.yml`** — unsigned matrix build - - [x] `macos-14` arm64 primary - - [x] Intel / Windows / Linux stubbed (commented), ready to un-comment - - [x] `workflow_dispatch` uploads as workflow artifacts - - [x] Tag push (`v*`) attaches to GitHub Release -- [ ] First green run — push a `v0.1.0-preview` tag, see DMG attached -- [ ] Un-comment `macos-13` row once arm64 is stable -- [ ] Un-comment `windows-2022` row + add Windows-specific PyInstaller notes -- [ ] Un-comment `ubuntu-22.04` row once the Linux packaging path is chosen (AppImage vs deb) - ---- - -## Cross-platform expansion (post-Phase-D) - -One session per platform. Ordered by payoff: - -- [ ] **macOS Intel (`macos-13`)** — easiest; same cert, different PyInstaller wheel set -- [ ] **Windows x64 CPU (`windows-2022`)** — `mlx_whisper` absent, fall back to PyTorch Whisper. Needs Windows code-signing cert (~$100–300/yr) -- [ ] **Windows x64 CUDA** — follow `scripts/package_cuda.py` pattern, ship as lazy-download pack -- [ ] **Linux x64 AppImage** — `appimagetool` builds; unsigned is acceptable on Linux - ---- - -## Risks we're tracking - -| # | Risk | Mitigation | Status | -|---|---|---|---| -| 1 | PyInstaller can't bundle torch Metal libs | Use `collect_all()` calls; fallback: portable-venv inside `.app/Contents/Resources/` | Untested | -| 2 | `torch.compile` breaks under frozen imports | Port `pyi_rth_torch_compiler_disable.py` hook | Pending | -| 3 | First-run model download fails halfway | SSE retry + resumable `hf_hub_download` (native) | Partial — frontend UI TBD | -| 4 | User has <10 GB free disk | `/setup/status` refuses download; shows error | ✅ Implemented | -| 5 | Gatekeeper blocks unsigned app | Document right-click → Open in README | Not yet documented | -| 6 | DMG size >800 MB | Exclude nvidia/triton/matplotlib; lazy-download optional packs | Exclude list in spec | -| 7 | macOS 15 Sequoia rejects un-notarized DMG wrapper | Phase E includes explicit `stapler staple` step | Blocked on Phase E | - ---- - -## Change log (this doc) - -- **2026-04-21** — initial tracker created. Phase C partially shipped: - tqdm monkey-patch + `/setup/status` + SSE stream + `/setup/warmup` live - and smoke-tested. Phase F skeleton committed (CI workflow, primary target - only — non-arm64 rows parked). Phases A, B, D, E pending. diff --git a/docs/electron-connections.md b/docs/electron-connections.md index 0dd3fa802..bde03f972 100644 --- a/docs/electron-connections.md +++ b/docs/electron-connections.md @@ -1,5 +1,11 @@ # Electron network and credentials +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Settings now exposes Network and Credentials in the shared settings shell. Sidebar search includes proxy, Hugging Face, DeepL and Microsoft labels. Network reads the configured proxy from `/system/info`. Save and Clear update all six upper/lowercase HTTP, HTTPS and ALL proxy variables using the same helper as Tauri. Writes run sequentially; failures stop the sequence and do not show success. A partial failure can be retried or cleared. Saving does not modify the audio-tool executable setting; its link opens Audio tools. diff --git a/docs/electron-dubbing.md b/docs/electron-dubbing.md index c82d6a352..3fdb735dd 100644 --- a/docs/electron-dubbing.md +++ b/docs/electron-dubbing.md @@ -1,5 +1,11 @@ # Electron dubbing workspace +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + The idle workspace includes an original/dubbed demo comparison with compact player controls. Sync playheads aligns positions without starting both videos. Sample transcript edits are retained per language while the demo is mounted; diff --git a/docs/electron-gallery.md b/docs/electron-gallery.md index 820d26cb0..b21c3065a 100644 --- a/docs/electron-gallery.md +++ b/docs/electron-gallery.md @@ -1,5 +1,11 @@ # Electron voice gallery +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Open VoiceStudio Gallery from the cloning sidebar or command search. Browse the existing local archetype catalogue by category, search it, and load more results. Age, gender, pitch, accent, language and whisper filters reuse the Tauri taxonomy. diff --git a/docs/electron-longform.md b/docs/electron-longform.md index dc9f18b36..7040b2373 100644 --- a/docs/electron-longform.md +++ b/docs/electron-longform.md @@ -1,5 +1,11 @@ # Electron Stories and Audiobooks +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Stories and Audiobook are available from the sidebar and command search. Each keeps a separate draft. Choose a saved default voice, enter or import text, then render. Stories supports per-line voice overrides; chapter headings and pause markup use the same `storyToSpans` compiler as Tauri. Audiobook submits the original script to the existing backend parser. Plain-text audiobook imports recognize chapter-title lines with LF, CRLF or CR line endings. Manuscripts with existing Markdown H1 headings retain their explicit structure and original text. diff --git a/docs/electron-parity.md b/docs/electron-parity.md index c63d6c5d2..7ee41a2ab 100644 --- a/docs/electron-parity.md +++ b/docs/electron-parity.md @@ -1,5 +1,11 @@ # Electron and Tauri behavior parity +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + This inventory tracks user-visible behavior. A row is **implemented** when Electron exposes the same task and backend contract. Platform-specific performance may differ. A native row stays **verification pending** until its real macOS, Windows, and Linux smoke gates pass. @@ -45,9 +51,9 @@ Clone handoff and cleanup, and round-tripped a generated take into a disposable Its live route sweep remained free of renderer warnings, exceptions and API failures after updater, dictation, QR, locale loading, translation-file and local-agent discovery rejection paths were contained at their initiating UI boundaries. -The inventory guard maps every current Tauri page, every static Electron route and every model -family to its maintained capability or responsive-layout evidence; adding an unmapped page or route -now fails the required test gate. +During the migration, an inventory guard mapped every Tauri page, every static Electron route and +every model family to its maintained capability or responsive-layout evidence; it was retired with +the Tauri shell. An isolated clean Ubuntu 26.04 workspace also completed the Electron typecheck and production package, then passed the unpacked Linux artifact contract with its x64 app, bundled `uv`, branded resources and native dictation helper present. diff --git a/docs/electron-performance.md b/docs/electron-performance.md index 9d334916d..08d88db5d 100644 --- a/docs/electron-performance.md +++ b/docs/electron-performance.md @@ -1,5 +1,11 @@ # Electron compute and performance settings +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Settings > Compute device exposes the existing device override, a physical CUDA adapter selector on multi-GPU NVIDIA hosts, the torch.compile workaround, generation time budgets, and hardware readouts. The CUDA selector persists a stable GPU UUID through `CUDA_VISIBLE_DEVICES`; restart the app to apply it to the backend and every engine subprocess. CUDA selection uses the validated `/api/settings/cuda-device` endpoint; the generic environment setter cannot change it or alter the running process's GPU visibility. diff --git a/docs/electron-privacy.md b/docs/electron-privacy.md index c791de6ed..6465fced3 100644 --- a/docs/electron-privacy.md +++ b/docs/electron-privacy.md @@ -1,5 +1,11 @@ # Electron privacy and retention +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Settings > Privacy exposes the existing invisible-watermark setting, analytics consent and generation-history retention. Watermark controls appear when the backend reports AudioSeal available and affect new audio through the existing marking path. No audio producer bypasses `mark_synthetic`. Analytics consent uses the existing backend opt-in endpoint and remains unchanged until the user explicitly switches it. After consent, the renderer records fixed screen labels and workflow action labels (clone, design, dubbing, transcription); the backend records lifecycle/error events. Privacy-safe manual pageviews use fixed app paths under `app.voicestudio.sh`, enabling aggregate Web Analytics without sending renderer URLs or dynamic route segments. Uncaught renderer errors also reach PostHog Error Tracking with their message replaced and credentials, home paths, extension frames, text, audio, filenames, and user-named content removed. Raw exception autocapture, DOM autocapture, automatic pageviews, and session recordings remain disabled. Unavailable features do not display inert toggles, and failed saves preserve confirmed state. diff --git a/docs/electron-projects.md b/docs/electron-projects.md index 53998bd19..aa0efcb8e 100644 --- a/docs/electron-projects.md +++ b/docs/electron-projects.md @@ -1,5 +1,11 @@ # Electron projects +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Open Projects from the sidebar, command search, or Dubbing header. Current dubbing work can be saved with a name or saved as a new copy. Projects use the existing backend `/projects` database and Tauri state format. Opening a project asks before replacing the current draft. Active dubbing work and unresolved task recovery prevent switching. Deletion requires confirmation, removes only the saved project record, and detaches it from the current draft. It does not delete source media or generated audio. diff --git a/docs/electron-runtime.md b/docs/electron-runtime.md index fa3aa4817..908269a3e 100644 --- a/docs/electron-runtime.md +++ b/docs/electron-runtime.md @@ -1,5 +1,11 @@ # Electron runtime setup +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + The top bar places a GitHub Star button immediately right of Get Pro, showing the exact star count from GitHub's small public endpoint. It refreshes when shown and every 20 minutes while visible; browser caching keeps repeat requests small. A bundled count remains visible if GitHub is unavailable. Requests send no credentials or referrer, and clicking the shortcut opens GitHub in the external browser. The footer shows the project website icon, then Join Discord immediately left of X. Discord opens the community invite in the external browser. The footer includes one X button with a hover popup of follow cards for @idebpalash and @voicestudiosh, plus a heart shortcut to donation options, immediately left of the footer collapse control. The profile popup also opens by click or keyboard; follow links open in the external browser. Profile cards span the popup and bundle their images locally for offline display. The heart opens compact cards with locally bundled brand icons for Ko-fi, PayPal, and GitHub stars, plus the full Support page. Both popups support click and keyboard access. Icons have localized accessible labels. The footer chevron collapses the bar; a slim expand control restores it. Get Pro remains in the top bar. Low-disk warnings in the notification menu open Settings > Storage, where users can inspect disk use and clear temporary files. diff --git a/docs/electron-storage.md b/docs/electron-storage.md index ca83b76a5..74ab33ed5 100644 --- a/docs/electron-storage.md +++ b/docs/electron-storage.md @@ -1,5 +1,11 @@ # Electron storage settings +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Settings > Storage reads the existing cached disk report, shows volume use/free space, model cache, application data, engine environments and temporary files, and marks incomplete scans explicitly. Largest models and data subtotals expand inline. Warning formatting and byte formatting are shared with Tauri. Open folder uses Electron's native reveal bridge, with the existing backend reveal route for browser development. Model and log links open their existing management views. Temporary-file cleanup requires explicit confirmation with the running-job warning. A partial deletion reports failure instead of claiming all files were cleared, and refreshes usage. Opening the page never deletes anything. diff --git a/docs/electron-tools.md b/docs/electron-tools.md index aa6c70c73..9c85c51ce 100644 --- a/docs/electron-tools.md +++ b/docs/electron-tools.md @@ -1,5 +1,11 @@ # Electron tools workspace +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Tools in the cloning sidebar or command search opens three utility panes: - Directorial AI parses direction into instruction, translation hint, rate bias, diff --git a/docs/electron-transcriptions.md b/docs/electron-transcriptions.md index 402a82f9e..d58beeb25 100644 --- a/docs/electron-transcriptions.md +++ b/docs/electron-transcriptions.md @@ -1,5 +1,11 @@ # Electron transcriptions +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Open Transcriptions above the engine list, or through command search. Upload an audio file or start dictation, then stop recording to transcribe. The workspace checks dictation readiness before recording/upload and again before transcription. diff --git a/docs/electron-voice-design.md b/docs/electron-voice-design.md index 55839b584..40092b9d9 100644 --- a/docs/electron-voice-design.md +++ b/docs/electron-voice-design.md @@ -1,5 +1,11 @@ # Electron voice design +> **Historical context:** this page was written while the Electron and Tauri apps +> coexisted. Mentions of Tauri helpers, pages, tests and regression results describe +> that migration period; the Tauri shell has since been removed and the shared code +> now lives in `electron/src/shared/`. Existing Tauri installs: see the +> [migration guide](electron-migration.md). + Open Design from the cloning sidebar or command search. Choose a preset or adjust voice traits, write a script, and synthesize. The existing Tauri category, conflict resolution and seed helpers build the request; clone references are never forwarded. diff --git a/docs/macos-signing-verification.md b/docs/macos-signing-verification.md index e0fff6b33..221f5f681 100644 --- a/docs/macos-signing-verification.md +++ b/docs/macos-signing-verification.md @@ -1,138 +1,87 @@ -# macOS Build, Signing & Notarization — Requirements & Verification - -> **Archived Tauri notes.** Current Electron signing and notarization are -> documented in [RELEASING.md](RELEASING.md). - -The canonical checklist for producing and verifying the VoiceStudio macOS -desktop bundle. It pairs with two helper scripts and the release workflow: - -- **`scripts/verify-macos-signing.sh`** — runs every verification command below - and reports PASS / WARN / FAIL. Strict mode (`--require-signed`) fails on any - unsigned/un-notarized component. -- **`scripts/macos-dev-unquarantine.sh`** — local-dev-only quarantine stripper. -- **`.github/workflows/release.yml`** — builds, optionally signs (opt-in), and - runs `verify-macos-signing.sh` on the macOS leg. - -> **Current state of this repo:** Apple Developer ID signing + notarization is -> **opt-in and OFF by default** (see the *Configure Apple signing* step in -> `release.yml`). Default stable + all preview builds are **ad-hoc signed** — -> `tauri.conf.json > bundle > macOS > signingIdentity = "-"` gives the bundle a -> *valid* seal (free, no Apple account), so a downloaded copy shows the -> **GUI-bypassable "unidentified developer"** prompt (right-click → Open) instead -> of the un-bypassable **"app is damaged"** error a broken/missing seal produces. -> It is **not** notarized, so users still confirm the first launch once (see -> [`docs/install/macos.md`](install/macos.md#gatekeeper-quarantine), issues -> #134/#72). For a warning-free double-click, enable signing: fix the Apple -> secrets, set the repo variable `MACOS_SIGNING_ENABLED = true` (the env-set -> `APPLE_SIGNING_IDENTITY` overrides the `"-"` default) — at which point the -> verification below switches to **strict** automatically and gates the release. - -### Signing tiers at a glance - -| Tier | Cost | What the user sees | Terminal? | -|------|------|--------------------|-----------| -| **Unsigned / broken seal** | free | "app is damaged" — **no GUI bypass** | yes (`xattr`) | -| **Ad-hoc signed** (current default) | free | "unidentified developer" → right-click → Open / "Open Anyway" | **no** | -| **Developer ID, not notarized** | $99/yr | "unidentified developer" → right-click → Open | no | -| **Developer ID + notarized + stapled** | $99/yr | nothing — clean double-click | no | - ---- - -## Requirements - -When modifying or releasing this Tauri application: - -1. **Produce a valid macOS `.app` bundle** via the current Tauri release process - (`tauri-apps/tauri-action` in `release.yml`; locally, `bun desktop-prod`). -2. **Verify the app launches** on the latest supported macOS version. -3. **Signature integrity** — before release, run: - ```bash - codesign --verify --deep --strict --verbose=4 "" - ``` -4. **Gatekeeper acceptance**: - ```bash - spctl -a -vv "" - ``` -5. **No unsigned nested components** — no unsigned binaries, frameworks, helper - apps, or dynamic libraries may exist inside the bundle. -6. **Sign nested before root** — every nested component is signed *before* the - root application bundle. -7. **Production releases:** - - Use an **Apple Developer ID** certificate. - - **Notarize** the application. - - **Staple** the notarization ticket (`xcrun stapler staple`). - - macOS 15 (Sequoia) also rejects a DMG that *wraps* a signed `.app` but isn't - itself notarized — **re-notarize + staple the DMG**, then re-upload it (see - `docs/DESKTOP_RELEASE.md`, Phase E). -8. **Validate downloaded builds do NOT trigger:** - - "App is damaged and can't be opened" - - "Move to Trash" - - any Gatekeeper rejection message -9. **Test both scenarios:** fresh download, and existing-user upgrade - (`bun desktop-prod` = fresh; `bun desktop-prod:upgrade` = rebuild keeping data). -10. **Fail closed** — if signing or notarization fails, **stop the release and - report the exact error** rather than producing an unsigned artifact. The - strict-mode verification step in `release.yml` enforces this on the signed path. - -### Required verification commands +# macOS Signing & Notarization — Verification -```bash -codesign --verify --deep --strict --verbose=4 "VoiceStudio.app" -spctl -a -vv "VoiceStudio.app" -xcrun notarytool history # needs Apple credentials / keychain profile -``` +How to verify the Electron macOS app's signature, notarization, and Gatekeeper +acceptance. Credentials and the release procedure live in +[RELEASING.md](RELEASING.md#credentials); end-user Gatekeeper help lives in +[install/macos.md](install/macos.md#gatekeeper-quarantine). + +## How releases are signed -A release is considered **successful only when all verification checks pass.** +`electron-release.yml` packages macOS with electron-builder. When the +Developer ID certificate (`ELECTRON_MACOS_CSC_LINK`, +`ELECTRON_MACOS_CSC_KEY_PASSWORD`) and Apple notarization credentials +(`APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`) are all present, +the build enables Hardened Runtime and notarization together +(`electron/electron-builder.config.mjs`). A publishing run refuses missing +credentials unless the owner explicitly sets `allow_unsigned=true`, and before +publication it runs: ---- +```bash +codesign --verify --deep --strict "VoiceStudio.app" +spctl --assess --type execute --verbose=2 "VoiceStudio.app" +xcrun stapler validate "VoiceStudio.app" +``` -## How to verify +Without credentials (artifact rehearsals, local `bun run dist`), packages keep +electron-builder's unsigned/ad-hoc defaults; users then see the +"unidentified developer" prompt described in the install guide. + +| Tier | What the user sees | Terminal needed? | +|------|--------------------|------------------| +| Unsigned / broken seal | "app is damaged" — no GUI bypass | yes (`xattr`) | +| Ad-hoc signed | "unidentified developer" → right-click → Open / "Open Anyway" | no | +| Developer ID, not notarized | "unidentified developer" → right-click → Open | no | +| Developer ID + notarized + stapled | clean double-click | no | + +## Requirements for a signed release + +1. Every nested binary, framework, helper app and dynamic library (including the + Rust native helper and bundled `uv`) is signed before the root bundle; no + unsigned nested components. +2. The app is signed with a **Developer ID Application** certificate, + notarized, and the ticket is stapled. +3. The microphone entitlement (`com.apple.security.device.audio-input` in + `electron/build/entitlements.mac.plist`) is present in the signed app. +4. A downloaded DMG from the draft release opens without "damaged", "Move to + Trash", or any other Gatekeeper rejection on a clean Mac, both as a fresh + install and as an upgrade over the previous release. +5. If signing or notarization fails, stop the release and report the exact + error rather than publishing an unsigned artifact. + +## Verifying a bundle locally + +`scripts/verify-macos-signing.sh` runs the codesign, Gatekeeper, stapler and +(when credentials are available) `notarytool` checks and reports PASS / WARN / +FAIL: ```bash -# Auto-discover the most recent built .app and report status (dev default): +# Auto-discover the most recent built .app: scripts/verify-macos-signing.sh -# Verify a specific bundle or DMG: +# A specific bundle or DMG (for example from electron/release/): scripts/verify-macos-signing.sh "path/to/VoiceStudio.app" -scripts/verify-macos-signing.sh ~/Downloads/VoiceStudio*.dmg +scripts/verify-macos-signing.sh ~/Downloads/VoiceStudio-Electron-*.dmg -# Production gate — fail on ANY unsigned/un-notarized component: +# Release gate — fail on any unsigned or un-notarized component: scripts/verify-macos-signing.sh "VoiceStudio.app" --require-signed ``` -The script runs checks 3–8 plus stapler/notarytool, and exits non-zero in strict -mode if anything fails — the machine-checkable form of requirement #10. - -`xcrun notarytool history` is included when credentials are present in the -environment (`NOTARYTOOL_KEYCHAIN_PROFILE`, or `APPLE_ID` + `APPLE_PASSWORD` + +`xcrun notarytool history` runs when credentials are present in the environment +(`NOTARYTOOL_KEYCHAIN_PROFILE`, or `APPLE_ID` + `APPLE_PASSWORD` + `APPLE_TEAM_ID`); otherwise it is skipped with a note. ---- - ## Local development -For **local test artifacts only**, you may remove the quarantine attribute so an -unsigned dev build launches without the right-click → Open dance: +For **local test artifacts only**, remove the quarantine attribute so an +unsigned build launches without the right-click → Open step: ```bash scripts/macos-dev-unquarantine.sh "path/to/VoiceStudio.app" ``` -> ⚠️ This is a **development convenience only** and is **never a substitute** for -> proper Developer ID signing + notarization of production releases. Shipped -> artifacts must be signed and notarized so end users never need it. - ---- - -## Enabling signed + notarized stable releases - -1. Obtain an Apple Developer ID (~$99/yr) and a Developer ID Application cert. -2. Add the secrets used by `release.yml`: `APPLE_CERTIFICATE`, - `APPLE_CERTIFICATE_PASSWORD`, `APPLE_SIGNING_IDENTITY`, `APPLE_ID`, - `APPLE_PASSWORD`, `APPLE_TEAM_ID`. -3. Set repo variable **`MACOS_SIGNING_ENABLED = true`**. -4. Tag a `v*` release. The build then signs, notarizes, and the *Verify macOS - signing* step runs in **strict** mode — a failure stops the release (req #10). -5. Confirm the DMG itself is notarized + stapled (macOS 15 requirement, #7). +This is never a substitute for Developer ID signing and notarization of +published releases. -See `docs/DESKTOP_RELEASE.md` (Phase E) for the full pipeline rationale. +The archived Tauri app used a different pipeline (`tauri-action`, +`APPLE_CERTIFICATE*` secrets, `MACOS_SIGNING_ENABLED`); none of it applies to +Electron. From 615f868b4d1842bed101e86ba26eaa393a01da60 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:18:13 +0530 Subject: [PATCH 35/85] fix(generate): outlast the backend budget before the client aborts The 21-minute client backstop gave up while the backend could still be queueing (30 min), cold-loading or extending a progressing job, so the UI reported a failure for a job that later finished. The backstop now derives from mirrored backend defaults and grows with the text; a test keeps the mirror equal to the backend. The heartbeat extension cap gets an unambiguous name, OMNIVOICE_PROGRESS_EXTENSION_CAP_S; OMNIVOICE_MODEL_LOAD_TIMEOUT_S stays accepted as a deprecated alias. --- backend/services/model_manager.py | 20 +++++- backend/worker/deadlines.py | 12 +++- docs/performance.md | 1 + .../src/renderer/src/lib/api/generate.test.ts | 25 ++++++- electron/src/renderer/src/lib/api/generate.ts | 11 +--- electron/src/shared/hooks/useTTS.js | 11 ++-- electron/src/shared/utils/generateBudget.ts | 52 +++++++++++++++ tests/test_generate_abort_budget.py | 66 +++++++++++++++++++ tests/test_worker_deadlines.py | 20 ++++++ 9 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 electron/src/shared/utils/generateBudget.ts create mode 100644 tests/test_generate_abort_budget.py diff --git a/backend/services/model_manager.py b/backend/services/model_manager.py index 4ec0e091d..25d909829 100644 --- a/backend/services/model_manager.py +++ b/backend/services/model_manager.py @@ -450,8 +450,24 @@ def __getattr__(name: str): # Without a cap, a load that heartbeats but never finishes would hold its # worker forever. 1800s of extension ≈ a 5 GB model at ~2.5 MB/s on top of the # 300s base — beyond that, telling the user is better than silently waiting. -MODEL_LOAD_EXTRA_TIMEOUT_S = float( - os.environ.get("OMNIVOICE_MODEL_LOAD_TIMEOUT_S", "1800.0")) +# +# OMNIVOICE_PROGRESS_EXTENSION_CAP_S names it. The old name, +# OMNIVOICE_MODEL_LOAD_TIMEOUT_S, read as the load ceiling that +# OMNIVOICE_MODEL_LOAD_TIMEOUT (no _S) really is; it stays accepted as a +# deprecated alias so existing configurations keep their value. +PROGRESS_EXTENSION_CAP_ENV = "OMNIVOICE_PROGRESS_EXTENSION_CAP_S" +PROGRESS_EXTENSION_CAP_LEGACY_ENV = "OMNIVOICE_MODEL_LOAD_TIMEOUT_S" + + +def progress_extension_cap_s(env=os.environ) -> float: + """The heartbeat extension cap, honoring the deprecated alias.""" + return float( + env.get(PROGRESS_EXTENSION_CAP_ENV) + or env.get(PROGRESS_EXTENSION_CAP_LEGACY_ENV) + or "1800.0") + + +MODEL_LOAD_EXTRA_TIMEOUT_S = progress_extension_cap_s() # The cap also grows with the job's own budget: a job that keeps reporting # progress may run for this many extra budgets. A fixed 1800s cap made a job's # length a hard limit however steadily it progressed. A 50k-character audiobook diff --git a/backend/worker/deadlines.py b/backend/worker/deadlines.py index 7ffb86cba..7de5390f8 100644 --- a/backend/worker/deadlines.py +++ b/backend/worker/deadlines.py @@ -38,7 +38,17 @@ _CPU_GENERATE_TIMEOUT_S = float( os.environ.get("OMNIVOICE_CPU_GENERATE_TIMEOUT_S", "600.0") ) -_MODEL_LOAD_EXTRA_S = float(os.environ.get("OMNIVOICE_MODEL_LOAD_TIMEOUT_S", "1800.0")) +def _progress_extension_cap_s(env=os.environ) -> float: + """Mirror of model_manager.progress_extension_cap_s (deprecated alias: + OMNIVOICE_MODEL_LOAD_TIMEOUT_S).""" + return float( + env.get("OMNIVOICE_PROGRESS_EXTENSION_CAP_S") + or env.get("OMNIVOICE_MODEL_LOAD_TIMEOUT_S") + or "1800.0" + ) + + +_MODEL_LOAD_EXTRA_S = _progress_extension_cap_s() _HEARTBEAT_GRACE_S = float(os.environ.get("OMNIVOICE_MODEL_LOAD_HEARTBEAT_GRACE_S", "30.0")) # Free character allowance before the execution budget starts scaling, and the diff --git a/docs/performance.md b/docs/performance.md index d000c71af..462e82661 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -103,6 +103,7 @@ None of them are required — the defaults are chosen for the common case. | `OMNIVOICE_CPU_GENERATE_TIMEOUT_S` | `600` | Same budget, for hosts that render on the CPU — correct CPU synthesis legitimately takes longer than the accelerated floor, so it gets its own, higher one. It is also the floor a CUDA/ROCm GPU below the engine's declared VRAM floor gets, since that is the performance class it actually falls into. An explicit value here always governs CPU-family generation, independent of `OMNIVOICE_GENERATE_TIMEOUT_S` above — that var only doubles as a CPU floor when *this* one is left unset (a legacy shortcut: setting only `OMNIVOICE_GENERATE_TIMEOUT_S` lowers the watchdog everywhere with one var). Also settable from **Settings → Performance & Device**, which flags a row an external env var is already shadowing instead of claiming a save will apply. | | `OMNIVOICE_ENGINE_IMPORT_PROBE_TIMEOUT_S` | `60` | How long to wait while checking that a sidecar engine's virtualenv can import the engine. Only affects how quickly a *broken* venv is ruled out — a probe that runs out of time is treated as "unproven", and the venv is used anyway, so a slow machine is never told its engine is missing. Per-engine override: `OMNIVOICE_INDEXTTS_IMPORT_PROBE_TIMEOUT_S` (and the same shape for `CONFUCIUS4`, `DOTS_TTS`, `MOSS_TTS_V15`). | | `OMNIVOICE_GPU_QUEUE_TIMEOUT_S` | `1800` | How long a job may sit in the GPU queue before it's reported as a saturated pool (a retryable condition — nothing ran). Waiting is normal on 1-worker machines; lower this only if you'd rather fail fast than queue. | +| `OMNIVOICE_PROGRESS_EXTENSION_CAP_S` | `1800` | Extra time a job that keeps reporting progress (a model download heartbeat, a finished chunk) may run past its budget — the larger of this and three times the job's own budget. A job that goes silent still stops at its budget. Replaces the old name `OMNIVOICE_MODEL_LOAD_TIMEOUT_S`, which is still accepted but deprecated; it is unrelated to the cold-load ceiling `OMNIVOICE_MODEL_LOAD_TIMEOUT`. The app waits out the backend's longest default budget before it gives up on a request itself. | **torch.compile** is probe-based, not platform-based: it's attempted only where the runtime check says it can work (a CUDA device with Triton importable diff --git a/electron/src/renderer/src/lib/api/generate.test.ts b/electron/src/renderer/src/lib/api/generate.test.ts index 1f517973b..0de6366ee 100644 --- a/electron/src/renderer/src/lib/api/generate.test.ts +++ b/electron/src/renderer/src/lib/api/generate.test.ts @@ -11,6 +11,7 @@ import { toGenerateForm, } from './generate'; import type { CloneGenerateInput } from './types'; +import { BACKEND_GENERATE_BUDGET_S, generateAbortMs } from '@shared/utils/generateBudget'; // Engine vocabulary (a regional Chinese dialect tag), escaped to keep the source ASCII. const DIALECT = '\u56DB\u5DDD\u8BDD'; @@ -302,10 +303,32 @@ describe('generateClone', () => { }), ); const pending = generateClone({ ...BASE_INPUT, profileId: 'p1' }); + let settled = false; + pending.catch(() => {}).finally(() => (settled = true)); const assertion = expect(pending).rejects.toMatchObject({ name: 'AbortError' }); - await vi.advanceTimersByTimeAsync(21 * 60 * 1000 + 1); + // Still waiting where the old 21-minute backstop gave up on a job the + // backend was legitimately queueing (30 min) or cold-loading. + await vi.advanceTimersByTimeAsync(generateAbortMs(BASE_INPUT.text.length) - 1); + expect(settled).toBe(false); + await vi.advanceTimersByTimeAsync(2); await assertion; }); + + it('outlasts the backend budget, which grows with the text', () => { + const budget = BACKEND_GENERATE_BUDGET_S; + const backendMaxS = + budget.modelLoad + + budget.queueWait + + budget.executionBase + + budget.sidecarGrace + + Math.max( + budget.progressExtensionCap, + budget.progressExtensionBudgets * (budget.executionBase + budget.sidecarGrace), + ); + expect(generateAbortMs(0)).toBeGreaterThan(backendMaxS * 1000); + expect(generateAbortMs(0)).toBeGreaterThan(21 * 60 * 1000); + expect(generateAbortMs(50_000)).toBeGreaterThan(generateAbortMs(0) + 4 * 1220 * 1000 - 1); + }); }); it('localizes streamed profile language refusals and prevents classic fallback', async () => { diff --git a/electron/src/renderer/src/lib/api/generate.ts b/electron/src/renderer/src/lib/api/generate.ts index 7be8b5a32..493a162ad 100644 --- a/electron/src/renderer/src/lib/api/generate.ts +++ b/electron/src/renderer/src/lib/api/generate.ts @@ -1,3 +1,4 @@ +import { generateAbortMs } from '@shared/utils/generateBudget'; import { generationFailureMessage } from '@shared/utils/generationFailureMessage.ts'; import i18next from 'i18next'; import { languageRejectionMessage } from '@shared/utils/languageRejection.ts'; @@ -13,13 +14,6 @@ export const CLONE_MAX_SECONDS = 15; * windows (omnivoice/utils/audio.py CLONE_REF_MAX_WINDOWS) — rejected outright. */ export const REF_HARD_MAX_SECONDS = 75; -/** - * Client-side abort backstop. The first /generate may cold-load the model; - * the backend bounds that itself and returns a descriptive error, so this sits - * just above its load timeout to make sure the UI never spins forever if the - * backend goes silent. - */ -export const GENERATE_ABORT_MS = 21 * 60 * 1000; // ── Instruct whitelist ───────────────────────────────────────────────────── // The engine validator (omnivoice/models/omnivoice.py::_resolve_instruct) @@ -407,7 +401,8 @@ export async function generateClone( const forwardAbort = (): void => controller.abort(); if (opts.signal?.aborted) controller.abort(); else opts.signal?.addEventListener('abort', forwardAbort, { once: true }); - const backstop = setTimeout(forwardAbort, GENERATE_ABORT_MS); + // Outlasts the backend's own budget so its descriptive error always wins. + const backstop = setTimeout(forwardAbort, generateAbortMs(input.text.length)); const finishActivity = beginAppActivity('synthesis'); try { const res = await apiFetch('/generate', { diff --git a/electron/src/shared/hooks/useTTS.js b/electron/src/shared/hooks/useTTS.js index b67ea123f..61e071bde 100644 --- a/electron/src/shared/hooks/useTTS.js +++ b/electron/src/shared/hooks/useTTS.js @@ -2,6 +2,7 @@ import { useState, useRef, useCallback } from 'react'; import { useAppStore } from '../store'; import { generateSpeech, TtsGenerationBusyError } from '../api/generate'; import { pickDesignSeed } from '../utils/seed'; +import { generateAbortMs } from '../utils/generateBudget'; import { playBlobAudio, playPing } from '../utils/media'; import { StreamingPreviewError, @@ -237,13 +238,11 @@ export default function useTTS({ selectedProfile, setSelectedProfile, loadHistor } } - // The first /generate may cold-load/download the model. The backend now - // bounds that and returns an error rather than hanging; this client-side - // abort is a backstop so the UI never spins forever even if the backend - // is unreachable. The ceiling sits just above the backend's load timeout - // so the backend's descriptive error wins in the normal case. + // The backend bounds every phase of a generate and returns a descriptive + // error; this abort only catches a backend gone silent, so it outlasts + // the backend's longest legitimate budget (see generateBudget.ts). const ac = new AbortController(); - abortTimer = setTimeout(() => ac.abort(), 21 * 60 * 1000); + abortTimer = setTimeout(() => ac.abort(), generateAbortMs(text.length)); // #1330 — one voice for both delivery paths. A dropped chunk is not an // error (the audio is real), so it is a persistent-ish warning toast diff --git a/electron/src/shared/utils/generateBudget.ts b/electron/src/shared/utils/generateBudget.ts new file mode 100644 index 000000000..28d7a732f --- /dev/null +++ b/electron/src/shared/utils/generateBudget.ts @@ -0,0 +1,52 @@ +/** + * Client backstop for a single /generate request. + * + * The backend bounds every phase of a generate itself and answers with a + * descriptive error when one runs out, so the client abort exists only for a + * backend that has gone silent. It must therefore outlast the backend's + * longest legitimate budget: aborting first reports a failure while the job is + * still running (and later lands in history). + * + * The values mirror the backend defaults in backend/services/model_manager.py + * and are held equal by tests/test_generate_abort_budget.py: + * + * - modelLoad: cold-load ceiling, OMNIVOICE_MODEL_LOAD_TIMEOUT + * - queueWait: GPU queue wait, OMNIVOICE_GPU_QUEUE_TIMEOUT_S + * - executionBase: the largest default execution base (sidecar receive + * timeouts reach 900 s; CPU hosts get 600 s, accelerated hosts 300 s) + * - sidecarGrace: the grace generate_timeout_s adds for sidecar engines + * - progressExtensionCap / progressExtensionBudgets: heartbeat extension, + * max(OMNIVOICE_PROGRESS_EXTENSION_CAP_S, budgets x execution budget) + * - freeChars / charsPerSecond: the execution budget's length scaling + * + * Operators who raise those budgets through the environment are beyond what + * the client can know; the margin covers scheduling jitter only. + */ +export const BACKEND_GENERATE_BUDGET_S = { + modelLoad: 1200, + queueWait: 1800, + executionBase: 900, + sidecarGrace: 5, + progressExtensionCap: 1800, + progressExtensionBudgets: 3, + freeChars: 1200, + charsPerSecond: 40, +} as const; + +const CLIENT_MARGIN_S = 60; + +/** Milliseconds before the client gives up on a /generate for this text. */ +export function generateAbortMs(textLength = 0): number { + const budget = BACKEND_GENERATE_BUDGET_S; + const execution = + budget.executionBase + + budget.sidecarGrace + + Math.max(0, textLength - budget.freeChars) / budget.charsPerSecond; + const extension = Math.max( + budget.progressExtensionCap, + budget.progressExtensionBudgets * execution, + ); + return Math.ceil( + (budget.modelLoad + budget.queueWait + execution + extension + CLIENT_MARGIN_S) * 1000, + ); +} diff --git a/tests/test_generate_abort_budget.py b/tests/test_generate_abort_budget.py new file mode 100644 index 000000000..deb3a6f8d --- /dev/null +++ b/tests/test_generate_abort_budget.py @@ -0,0 +1,66 @@ +"""The UI's /generate backstop must outlast the backend's own budget. + +electron/src/shared/utils/generateBudget.ts mirrors the backend's default +budgets. If the backend grows one (a longer queue wait, a bigger sidecar +receive timeout) without the client following, the UI reports a failure while +the job is still running — the 21-minute backstop did exactly that against a +30-minute queue wait. This keeps the mirror equal to the backend defaults. +""" +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +BUDGET_TS = ROOT / "electron/src/shared/utils/generateBudget.ts" + + +def _client_budget() -> dict[str, float]: + text = BUDGET_TS.read_text(encoding="utf-8") + block = re.search(r"BACKEND_GENERATE_BUDGET_S = \{(.*?)\} as const", text, re.S).group(1) + return {key: float(value) for key, value in re.findall(r"(\w+): ([\d.]+)", block)} + + +def _source_default(path: Path, env: str) -> float: + match = re.search(rf'"{env}",\s*"([\d.]+)"', path.read_text(encoding="utf-8")) + assert match, f"default for {env} not found in {path}" + return float(match.group(1)) + + +def test_client_budget_mirrors_backend_defaults(monkeypatch): + from services import model_manager + from worker import deadlines + + monkeypatch.delenv("OMNIVOICE_MODEL_LOAD_TIMEOUT", raising=False) + manager = ROOT / "backend/services/model_manager.py" + client = _client_budget() + + assert client["modelLoad"] == model_manager._model_load_timeout() + assert client["queueWait"] == _source_default(manager, "OMNIVOICE_GPU_QUEUE_TIMEOUT_S") + assert client["progressExtensionCap"] == model_manager.progress_extension_cap_s({}) + assert client["progressExtensionBudgets"] == model_manager.PROGRESS_EXTENSION_BUDGETS + assert client["freeChars"] == deadlines._FREE_CHARS + assert client["charsPerSecond"] == deadlines._CHARS_PER_SECOND + grace = re.search(r"sidecar_grace = ([\d.]+) if _include_sidecar_grace", manager.read_text()) + assert grace and client["sidecarGrace"] >= float(grace.group(1)) + + +def test_client_execution_base_covers_every_default_execution_budget(): + """The largest of the host budgets and every sidecar's receive timeout.""" + manager = ROOT / "backend/services/model_manager.py" + bases = [ + _source_default(manager, "OMNIVOICE_GENERATE_TIMEOUT_S"), + _source_default(manager, "OMNIVOICE_CPU_GENERATE_TIMEOUT_S"), + ] + from services.subprocess_backend import GENERATE_RECV_TIMEOUT_S + + bases.append(GENERATE_RECV_TIMEOUT_S) + for path in (ROOT / "backend/engines").rglob("*.py"): + bases += [ + float(value) + for value in re.findall( + r'"OMNIVOICE_\w+_RECV_TIMEOUT_S",\s*"([\d.]+)"', path.read_text(encoding="utf-8") + ) + ] + assert len(bases) > 3, "receive-timeout scan found nothing; fix the regex" + assert _client_budget()["executionBase"] >= max(bases) diff --git a/tests/test_worker_deadlines.py b/tests/test_worker_deadlines.py index b75982acc..3f77a3bee 100644 --- a/tests/test_worker_deadlines.py +++ b/tests/test_worker_deadlines.py @@ -184,3 +184,23 @@ def test_total_is_the_sum_of_the_phases(): grace_seconds=99, ) assert d.total_seconds == 15 + + +@pytest.mark.parametrize( + ("env", "expected"), + [ + ({}, 1800.0), + ({"OMNIVOICE_PROGRESS_EXTENSION_CAP_S": "900"}, 900.0), + # Deprecated alias keeps existing configurations working. + ({"OMNIVOICE_MODEL_LOAD_TIMEOUT_S": "600"}, 600.0), + ({"OMNIVOICE_PROGRESS_EXTENSION_CAP_S": "900", + "OMNIVOICE_MODEL_LOAD_TIMEOUT_S": "600"}, 900.0), + # The load ceiling is a different knob and never feeds the cap. + ({"OMNIVOICE_MODEL_LOAD_TIMEOUT": "60"}, 1800.0), + ], +) +def test_progress_extension_cap_name_and_deprecated_alias(env, expected): + from services import model_manager + + assert model_manager.progress_extension_cap_s(env) == expected + assert deadlines._progress_extension_cap_s(env) == expected From 63d6a6b8e1966ae4005ba6e10128ba8908800340 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:19:09 +0530 Subject: [PATCH 36/85] fix(dev): keep dev:api on loopback and follow OMNIVOICE_PORT dev:api bound 0.0.0.0, publishing the unauthenticated API to the LAN against the backend's loopback default, and hard-coded port 3900 while the web dev server proxies to OMNIVOICE_PORT. It now honours OMNIVOICE_BIND_HOST (default 127.0.0.1) and OMNIVOICE_PORT. clear-dev-ports also recognises a backend running from the Electron app's managed runtime, and its comment no longer cites tauri.conf.json. --- scripts/clear-dev-ports.mjs | 16 +++++++--- scripts/dev-backend.mjs | 44 +++++++++++++++++++-------- tests/frontend/clearDevPorts.test.mjs | 13 ++++++++ tests/frontend/devBackend.test.mjs | 25 ++++++++++++--- tests/test_setup_rocm_variant.py | 2 +- 5 files changed, 78 insertions(+), 22 deletions(-) diff --git a/scripts/clear-dev-ports.mjs b/scripts/clear-dev-ports.mjs index 29791fa70..9e8936893 100644 --- a/scripts/clear-dev-ports.mjs +++ b/scripts/clear-dev-ports.mjs @@ -76,11 +76,12 @@ function normalized(value, windows = process.platform === "win32") { return path.posix.resolve(String(value || "")); } -// The app's own reverse-DNS identity (tauri.conf.json `identifier`). A backend -// the Tauri shell spawned lives under a per-app directory named after this — +// The retired Tauri shell's reverse-DNS identifier. A backend a final Tauri +// install spawned lives under a per-app directory named after it — // `…/com.debpalash.omnivoice-studio/project/.venv/…` — so its path names // VoiceStudio as unambiguously as the checkout path does, just from the other -// direction. +// direction. The Electron app's managed runtime is matched by +// ELECTRON_RUNTIME_VENV below. // // Without this the ownership test only recognised a listener running out of // the git checkout, so an app-managed backend left holding the port was @@ -92,6 +93,12 @@ function normalized(value, windows = process.platform === "win32") { // on the machine carries it, which is the whole point of the namespace. export const APP_BUNDLE_ID = "com.debpalash.omnivoice-studio"; +// The Electron app's managed runtime venv: `/runtime/project/.venv` +// where userData is the `VoiceStudio` app folder, or a custom runtime location, +// which the app always roots in a folder named `VoiceStudio` +// (electron/src/main/backend.ts defaultRuntimeRoot / selectedRuntimeRoot). +export const ELECTRON_RUNTIME_VENV = /[\\/]voicestudio[\\/](?:runtime[\\/])?project[\\/]\.venv[\\/]/i; + export function belongsToCheckout( cwd, command, @@ -110,7 +117,8 @@ export function belongsToCheckout( // App-managed backend: the bundle id appears in the executable path or in // the command line, whichever the platform gave us. for (const value of [cwd, executable, command]) { - if (String(value || "").toLowerCase().includes(APP_BUNDLE_ID)) return true; + const text = String(value || ""); + if (text.toLowerCase().includes(APP_BUNDLE_ID) || ELECTRON_RUNTIME_VENV.test(text)) return true; } const haystack = windows ? String(command || "") diff --git a/scripts/dev-backend.mjs b/scripts/dev-backend.mjs index d56c6a737..cf972c785 100644 --- a/scripts/dev-backend.mjs +++ b/scripts/dev-backend.mjs @@ -28,19 +28,38 @@ import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; +export const DEFAULT_BACKEND_PORT = 3900; + +/** The port Vite proxies to (electron/vite.web.config.ts: OMNIVOICE_PORT || 3900). */ +export function backendPort(env = process.env) { + const port = Number.parseInt(String(env.OMNIVOICE_PORT ?? "").trim(), 10); + return Number.isInteger(port) && port > 0 && port < 65536 ? port : DEFAULT_BACKEND_PORT; +} + +/** + * Loopback by default, like backend/main.py: the API ships unauthenticated on + * loopback, so `dev:api` must not publish it to the LAN. OMNIVOICE_BIND_HOST + * opts in to another interface, the same knob the backend honours. + */ +export function bindHost(env = process.env) { + return String(env.OMNIVOICE_BIND_HOST ?? "").trim() || "127.0.0.1"; +} + // The wrapper owns Python source reloads so uvicorn's reload parent cannot stay // alive after its server worker dies (#1690). -export const UVICORN_ARGS = [ - "run", - "uvicorn", - "main:app", - "--app-dir", - "backend", - "--host", - "0.0.0.0", - "--port", - "3900", -]; +export function uvicornArgs(env = process.env) { + return [ + "run", + "uvicorn", + "main:app", + "--app-dir", + "backend", + "--host", + bindHost(env), + "--port", + String(backendPort(env)), + ]; +} export const CRASH_RESTART_DELAY_MS = 1_000; export const CRASH_RESTART_LIMIT = 3; @@ -134,8 +153,9 @@ export function buildExitBanner({ code, signal, logTail, logPath, platform = pro // the CUDA build). `bun run setup:api` already did the sync, so skip it here // whenever the ROCm variant is requested (#1665). export function uvRunArgs(env = process.env) { + const args = uvicornArgs(env); const rocm = (env.OMNIVOICE_TORCH_VARIANT || "").trim().toLowerCase() === "rocm"; - return rocm ? [UVICORN_ARGS[0], "--no-sync", ...UVICORN_ARGS.slice(1)] : UVICORN_ARGS; + return rocm ? [args[0], "--no-sync", ...args.slice(1)] : args; } /** diff --git a/tests/frontend/clearDevPorts.test.mjs b/tests/frontend/clearDevPorts.test.mjs index fbf0cd31b..5f9a6e336 100644 --- a/tests/frontend/clearDevPorts.test.mjs +++ b/tests/frontend/clearDevPorts.test.mjs @@ -218,6 +218,19 @@ test("an app-managed backend is recognised as ours", () => { assert.equal(belongsToCheckout("", "", winApp, true, ["C:", "repo"].join(sep)), true); }); +test("an Electron-managed runtime backend is recognised as ours", () => { + const root = "/work/VoiceStudio"; + const linux = "/home/x/.config/VoiceStudio/runtime/project/.venv/bin/python"; + assert.equal(belongsToCheckout("", `${linux} -m uvicorn main:app`, "", false, root), true); + const custom = "/mnt/fast/VoiceStudio/project/.venv/bin/python"; + assert.equal(belongsToCheckout("", "", custom, false, root), true); + const sep = String.fromCharCode(92); + const win = ["C:", "Users", "x", "AppData", "Roaming", "VoiceStudio", "runtime", "project", ".venv", "Scripts", "python.exe"].join(sep); + assert.equal(belongsToCheckout("", "", win, true, ["C:", "repo"].join(sep)), true); + // Another project's venv in a folder that merely mentions the name is not ours. + assert.equal(belongsToCheckout("", "", "/srv/voicestudio-clone/.venv/bin/python", false, root), false); +}); + test("the bundle id match is case-insensitive", () => { // Windows paths come back with inconsistent casing depending on the API. const root = "/work/VoiceStudio"; diff --git a/tests/frontend/devBackend.test.mjs b/tests/frontend/devBackend.test.mjs index 68f32a5be..195cb9f22 100644 --- a/tests/frontend/devBackend.test.mjs +++ b/tests/frontend/devBackend.test.mjs @@ -16,13 +16,14 @@ import { mkdtempSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { - UVICORN_ARGS, buildExitBanner, createBackendSupervisor, isBackendSourceChange, killProcessTree, resolveDataDir, tailFile, + uvRunArgs, + uvicornArgs, } from '../../scripts/dev-backend.mjs'; function supervisorHarness(overrides = {}) { @@ -87,12 +88,26 @@ function supervisorHarness(overrides = {}) { }; } -test('uvicorn runs directly because the wrapper owns source reloads', () => { +test('uvicorn runs directly on loopback because the wrapper owns source reloads', () => { assert.equal( - ['uv', ...UVICORN_ARGS].join(' '), - 'uv run uvicorn main:app --app-dir backend --host 0.0.0.0 --port 3900', + ['uv', ...uvicornArgs({})].join(' '), + 'uv run uvicorn main:app --app-dir backend --host 127.0.0.1 --port 3900', ); - assert.equal(UVICORN_ARGS.includes('--reload'), false); + assert.equal(uvicornArgs({}).includes('--reload'), false); +}); + +test('dev backend honours the backend bind host and port knobs', () => { + const args = uvicornArgs({ OMNIVOICE_BIND_HOST: '0.0.0.0', OMNIVOICE_PORT: '3912' }); + assert.deepEqual(args.slice(-4), ['--host', '0.0.0.0', '--port', '3912']); + for (const bad of ['', 'nope', '0', '70000']) { + assert.deepEqual(uvicornArgs({ OMNIVOICE_PORT: bad }).slice(-2), ['--port', '3900']); + } + assert.deepEqual(uvicornArgs({ OMNIVOICE_BIND_HOST: ' ' }).slice(-4, -2), ['--host', '127.0.0.1']); + assert.deepEqual(uvRunArgs({ OMNIVOICE_TORCH_VARIANT: 'rocm', OMNIVOICE_PORT: '3912' }).slice(0, 3), [ + 'run', + '--no-sync', + 'uvicorn', + ]); }); test('only Python source changes trigger backend reloads', () => { diff --git a/tests/test_setup_rocm_variant.py b/tests/test_setup_rocm_variant.py index 10c49c79c..9437b6524 100644 --- a/tests/test_setup_rocm_variant.py +++ b/tests/test_setup_rocm_variant.py @@ -60,7 +60,7 @@ def test_dev_backend_skips_resync_when_rocm_requested(): script = f""" const mod = await import({json.dumps(module_uri)}); console.log(JSON.stringify({{ - base: mod.UVICORN_ARGS, + base: mod.uvicornArgs({{}}), unset: mod.uvRunArgs({{}}), auto: mod.uvRunArgs({{ OMNIVOICE_TORCH_VARIANT: "auto" }}), rocm: mod.uvRunArgs({{ OMNIVOICE_TORCH_VARIANT: " ROCm " }}), From 8b931a5f72ff160fcc3388a166a2a4b3e588d6fa Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:19:45 +0530 Subject: [PATCH 37/85] chore(i18n): prune shared catalog keys no code references The shared catalog is loaded only by the transitional shared modules' tests, never by the app. Remove the 157 keys nothing references from all 21 files; the remaining 464 untranslated keys stay ratcheted. --- electron/src/shared/i18n/locales/ar.json | 170 +------------------ electron/src/shared/i18n/locales/de.json | 170 +------------------ electron/src/shared/i18n/locales/en.json | 175 +------------------ electron/src/shared/i18n/locales/es.json | 170 +------------------ electron/src/shared/i18n/locales/fr.json | 170 +------------------ electron/src/shared/i18n/locales/hi.json | 170 +------------------ electron/src/shared/i18n/locales/id.json | 170 +------------------ electron/src/shared/i18n/locales/it.json | 170 +------------------ electron/src/shared/i18n/locales/ja.json | 170 +------------------ electron/src/shared/i18n/locales/ko.json | 177 +------------------- electron/src/shared/i18n/locales/nl.json | 170 +------------------ electron/src/shared/i18n/locales/pl.json | 170 +------------------ electron/src/shared/i18n/locales/pt.json | 170 +------------------ electron/src/shared/i18n/locales/ru.json | 170 +------------------ electron/src/shared/i18n/locales/sv.json | 170 +------------------ electron/src/shared/i18n/locales/th.json | 170 +------------------ electron/src/shared/i18n/locales/tr.json | 170 +------------------ electron/src/shared/i18n/locales/uk.json | 170 +------------------ electron/src/shared/i18n/locales/vi.json | 170 +------------------ electron/src/shared/i18n/locales/zh-CN.json | 177 +------------------- electron/src/shared/i18n/locales/zh-TW.json | 170 +------------------ 21 files changed, 188 insertions(+), 3401 deletions(-) diff --git a/electron/src/shared/i18n/locales/ar.json b/electron/src/shared/i18n/locales/ar.json index 3d73ff631..f49db7de3 100644 --- a/electron/src/shared/i18n/locales/ar.json +++ b/electron/src/shared/i18n/locales/ar.json @@ -59,15 +59,10 @@ "title": "اسأل VoiceStudio Agent", "notInstalled": "غير مثبّت", "noSource": "افتح نسخة قابلة للكتابة من مصدر VoiceStudio ليتمكن الوكيل من إصلاحها.", - "noneFound": "ثبّت Codex أو Claude Code أو OpenCode أو Pi لاستخدام الإصلاح المحلي.", "ready": "جاهز", "placeholder": "ما الذي حدث؟ أضف الخطوات أو الأخطاء الظاهرة أو السلوك المتوقع.", - "prReady": "اكتمل الإصلاح. راجع التغييرات واقترح طلب سحب بعد نجاح الاختبارات المستهدفة.", "diagnose": "تشخيص", "fix": "إصلاح", - "chooseDefault": "اختر الوكيل الذي يجب أن يستخدمه VoiceStudio تلقائيًا عند تعطل هذه الشاشة.", - "useAutomatically": "استخدام {{agent}} تلقائيًا", - "captured": "تم التقاط {{count}} من أسطر سجل المشكلات", "contextNotice": "يتلقى الوكيل المحدد هذا البلاغ والشاشة الحالية وسجلات التطبيق والخلفية الحديثة وتشخيصات النظام.", "complete": "مكتمل" }, @@ -607,7 +602,6 @@ "characters": "{{count}} حرفًا", "minutes": "~{{count}} دقيقة", "chars": "{{count}} أحرف", - "exportDone": "تم تنزيل الكتاب الصوتي", "exportFailed": "فشل التصدير — تحقق من تشغيل الواجهة الخلفية.", "global_speed": "السرعة", "global_speed_hint": "سرعة القراءة لجميع الخطوط دون تجاوز السرعة الخاصة بها", @@ -818,7 +812,6 @@ "data_dir": "دليل البيانات", "outputs": "النواتج", "crash_log": "سجل الأعطال", - "update_endpoint": "تحديث نقطة النهاية", "yes": "نعم", "no": "لا", "web_preview": "معاينة الويب", @@ -879,38 +872,21 @@ "title": "أوراق الاعتماد", "desc": "تم تعيين مفاتيح واجهة برمجة التطبيقات والرموز المميزة <1>لهذه الجلسة فقط. للاستمرارية عبر عمليات إعادة التشغيل، قم بتعيينها كمتغيرات بيئة في ملف تعريف Shell الخاص بك.", "hf_token": "رمز معانقة الوجه", - "hf_help": "مطلوب لتدوين المتحدثين وتنزيل النماذج بشكل أسرع. احصل عليها على Huggingface.co/settings/tokens.", - "translate_key": "LLM API مفتاح", - "translate_help": "اختياري — لموفري خدمات الترجمة المدفوعة أو DeepL أو OpenAI. ليست هناك حاجة لترجمة Google (الطبقة المجانية).", "save": "حفظ", "saved": "✓ تعيين", - "not_set": "✗ غير محدد", "get_token": "الحصول على رمز →", - "saved_session": "{{key}} تم حفظه لهذه الجلسة", "save_failed": "فشل الحفظ", "save_error": "فشل الحفظ: {{message}}", "deepl_key": "مفتاح DeepL API", - "deepl_help": "لمحرك الترجمة DeepL. احصل على مفتاحك على Deepl.com/pro-api.", "deepl_base_url": "عنوان URL الأساسي لـ DeepL", "deepl_base_url_help": "نقطة نهاية DeepL API المخصصة (الافتراضي: https://api.deepl.com/v2). التغيير للوكيل/الاستضافة الذاتية.", "microsoft_key": "مفتاح مترجم مايكروسوفت", - "microsoft_help": "لمحرك مترجم مايكروسوفت. احصل على مفتاحك من Azure Portal.", "microsoft_base_url": "عنوان URL لقاعدة مترجم Microsoft", "microsoft_base_url_help": "نقطة نهاية Azure Translator المخصصة (الافتراضية: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "عنوان URL الأساسي لـ LLM", - "llm_base_url_help": "للجودة السينمائية أو محرك LLM. أي نقطة نهاية متوافقة مع OpenAI (OpenAI، وOllama، وLM Studio، وما إلى ذلك).", - "llm_model": "نموذج LLM", - "llm_model_help": "اسم النموذج المطلوب استخدامه مع محرك LLM (على سبيل المثال gpt-4o، Deepseek-chat، qwen2.5).", - "group_huggingface": "HuggingFace", - "group_translate": "الترجمة (ماجستير / سينمائية)", - "group_deepl": "نقطة النهاية المخصصة DeepL", - "group_microsoft": "نقطة النهاية المخصصة لمترجم مايكروسوفت", "region": "المنطقة", "aws_profile": "ملف AWS" }, "engines": { - "banners_on": "لافتات المرحلة على", - "banners_off": "لافتات المرحلة قبالة", "backend": "الخلفية", "status": "الحالة", "ready": "جاهز", @@ -941,7 +917,6 @@ "dictation_model_hint": "يُستخدم في الإملاء المباشر وفي هذا المحرك عند التفريغ.", "dictation_not_installed": "غير مثبّت — اختره للتنزيل", "active": "نشط", - "whyUnavailable": "ما الذي يحتاجه", "diskDetails": "تفاصيل القرص", "diskModelDownload": "تنزيل النموذج", "diskPackageDownload": "تنزيل الحزم", @@ -965,11 +940,8 @@ "sectionReady": "جاهزة للاستخدام", "sectionMore": "أضف المزيد من المحركات", "lastError": "الخطأ الأخير: {{error}}", - "installedAndReady": "مثبتة وجاهزة", - "notInstalled": "غير مثبت", "available": "متاح", "subprocessTitle": "يعمل في العملية الفرعية الخاصة به + venv", - "inProcessTitle": "يعمل في عملية VoiceStudio Python", "testEngine": "محرك الاختبار", "testing": "اختبار…", "recheck": "أعد الفحص", @@ -986,7 +958,6 @@ "routingUnknown": "غير معروف", "routingEffectiveChip": "يعمل على {{device}} على هذا الجهاز", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "تم تحديد وحدة معالجة الرسومات، ولكن: {{reason}}", "selectCpuFallback": "{{engine}}: يعمل على المعالج المركزي — {{reason}}", "selectWithCaveat": "تم التبديل إلى {{engine}} — {{reason}}", "generateCaveat": "قد يكون {{engine}} بطيئًا على هذا الجهاز — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "لا يوجد سجل Tauri متاح. يعمل في غلاف سطح المكتب فقط.", "empty_backend": "سجل وقت التشغيل فارغ. سيظهر النشاط هنا عندما تقوم الواجهة الخلفية بتسجيله.", "title": "سجلات", - "source_backend": "الخلفية", - "source_frontend": "الواجهة الأمامية", - "source_tauri": "تاوري", "expand": "قم بتوسيع السجلات", "collapse": "طي السجلات", "expand_aria": "قم بتوسيع لوحة السجلات", @@ -1104,12 +1072,10 @@ "unlock": "فتح", "try_voice": "جرب هذا الصوت", "test_phrase": "عبارة اختبارية", - "test_help": "اكتب أي شيء — سنقوم بإنشائه باستخدام الإعدادات الحالية لهذا الصوت.", "test_placeholder": "اكتب شيئًا لسماع هذا الصوت يقوله...", "gen_preview": "إنشاء المعاينة", "generating": "جارٍ الإنشاء…", "used_title": "حيث تم استخدام هذا الصوت", - "used_empty": "هذا الصوت لم يستخدم بعد قم بإنشاء معاينة أعلاه، أو اخترها في دبلجة.", "synth_clips_one": "{{count}} مقطع موالفة", "synth_clips_other": "{{count}} مقاطع موالفة", "projects_count_one": "{{count}} المشروع", @@ -1152,9 +1118,7 @@ "translate": "ترجمة", "generate": "إنشاء", "export": "تصدير", - "no_video": "لم يتم تحميل أي فيديو", "segments": "شرائح", - "speakers": "مكبرات الصوت", "video_dubbing_studio": "استوديو دبلجة الفيديو", "drop_here": "قم بإسقاط الفيديو أو الصوت هنا", "paste_url": "... أو الصق عنوان URL للفيديو/YouTube", @@ -1168,7 +1132,6 @@ "cookie_size_error": "يجب ألا يتجاوز تصدير ملفات تعريف الارتباط 1 ميغابايت.", "save": "حفظ", "reset": "إعادة تعيين", - "save_project": "حفظ المشروع", "change_file": "تغيير الملف", "import_srt": "استيراد .srt", "import_srt_alt": "قم باستيراد .srt بدلاً من ذلك", @@ -1186,9 +1149,6 @@ "retranslate": "إعادة الترجمة", "transcript": "نسخة", "generate_dub": "توليد لقب", - "export_mp4": "MP4", - "export_wav": "واف", - "export_srt": "SRT", "upload_transcribe": "تحميل ونسخ", "multi_lang": "متعدد اللغات", "prep_download": "جارٍ تنزيل الفيديو…", @@ -1206,7 +1166,6 @@ "transcribing": "جارٍ نسخ الصوت…", "elapsed": "انقضى", "remaining": "المتبقية", - "sidebar_toggle": "تبديل الشريط الجانبي", "retry_transcription": "أعد محاولة النسخ", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "عندما يكون عنوان URL عبارة عن مضيف يحمل تعليقًا (YouTube، أو Vimeo، أو TED...)، فاسحب أيضًا التسميات التوضيحية الأصلية وأي ترجمات تلقائية على YouTube. بذور المحرر دون تشغيل Whisper؛ تخطي ترجمة الكل للغات التي يغطيها YouTube بالفعل.", @@ -1249,7 +1208,6 @@ "engine_label": "المحرك", "install_engine": "تثبيت هذا المحرك", "installing_engine": "...تثبيت", - "needs_dev_install": "يحتاج إلى تثبيت المطور", "install_disabled_title": "يتم تعطيل عمليات التثبيت في الإصدارات المجمعة", "needs_install_suffix": "- يحتاج إلى تثبيت", "glossary_title": "تثبيت الترجمات للمصطلحات المتكررة (الأسماء، كلمات العلامات التجارية، المصطلحات)", @@ -1283,7 +1241,6 @@ "original_track": "أصلي", "selected_dub": "{{code}} (الدبلجة المحددة)", "dub_track": "{{code}} (مدبلج)", - "export_tracks": "مسارات التصدير:", "stopping": "جارٍ الإيقاف…", "stop_progress": "توقف ({{current}}/{{total}})", "regen_changed": "تم تغيير ريجين {{count}}", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} تم التثبيت", "install_failed": "فشل التثبيت: {{message}}", "prep_elapsed": "انقضى {{time}}", - "add_language": "أضف لغة", "search_languages": "لغات البحث...", "languages_selected_one": "تم تحديد لغة {{count}}", "languages_selected_other": "تم تحديد {{count}} اللغات", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "الوقت", - "spkr": "سبكر", "text": "نص", "lang": "لانج", "voice": "صوت", @@ -1403,8 +1358,6 @@ "select_all_title": "حدد الكل الذي تمت تصفيته", "sel_count": "{{count}}سيل", "select_title": "تحديد المقطع (Shift + النقر للنطاق)", - "sync_title": "الصوت الذي تم إنشاؤه هو {{pct}}% مدة الصوت الأصلي", - "sync_label": "المزامنة: {{pct}}%", "rate_title": "تناسب معدل الكلام: {{ratio}}× بالنسبة إلى الفتحة{{error}}", "speaker_id": "معرف المتحدث", "translate_error_title": "خطأ في الترجمة: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "استعادة النص الأصلي", "lang_default": "(مدافع)", "voice_default": "الافتراضي", - "from_video": "من الفيديو", - "clone_profiles": "ملفات تعريف الاستنساخ", - "design_presets": "إعدادات التصميم المسبقة", "preview_title": "المعاينة المباشرة", "set_direction": "تحديد الاتجاه…", "edit_direction": "تحرير الاتجاه...", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "الشخصية", - "pick_personality": "اختر إعدادًا مسبقًا للشخصية...", "instruct": "إرشاد", "reference_text": "النص المرجعي", "language": "اللغة", @@ -1535,9 +1484,7 @@ "stories": "قصص", "untitled_story": "قصة بلا عنوان", "story_lines": "{{count}} السطر", - "story_lines_plural": "{{count}} خطوط", "story_voices": "{{count}} صوت", - "story_voices_plural": "{{count}} الأصوات", "audiobooks": "كتب صوتية", "audiobook": "كتاب مسموع", "story": "قصة", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio معرض", - "search_placeholder": "بحث في يوتيوب...", - "all_voices": "جميع الأصوات ({{count}})", - "no_voices": "لا توجد أصوات بعد", "download_hint": "ابحث في الأعلى لتنزيل المقاطع", - "search_btn": "بحث", - "download_btn": "تحميل", "saving": "جارٍ الحفظ…", "trim": "تقليم", - "delete_confirm": "هل تريد حذف هذا المقطع الصوتي؟", - "view_mode_list": "قائمة", - "view_mode_grid": "الشبكة", - "youtube_results": "نتائج يوتيوب ({{count}})", - "clone_profile": "الملف الشخصي استنساخ", - "crop_audio": "اقتصاص الصوت", "subtitle": "مئات من الأصوات المصممة الجاهزة - اختر واحدة وانطلق.", "zone_archetypes": "النماذج الأولية", "zone_imports": "وارداتي", @@ -1658,8 +1594,6 @@ "delete": "حذف", "community_empty": "لم يتم تحميل أصوات المجتمع بعد — اتصل بالإنترنت وأعد الفتح، أو كن أول من يرسل صوتًا.", "community_explainer": "إعدادات مسبقة مصممة وأصوات مسجلة يشاركها المجتمع، تُحمَّل من omnivoice-gallery.", - "no_designer": "صوت مسجل — استخدم \"استخدم الصوت\" بدلًا من ذلك.", - "no_preview": "لا توجد معاينة — أضفه عبر \"استخدم الصوت\" لسماعه.", "submit_failed": "تعذر فتح نموذج الإرسال.", "submit_preset": "إرسال إعداد مسبق", "submit_voice": "إرسال صوت", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "أنت جاهز لإنشاء صوتك الأول. يمكنك إعداد الإملاء الآن أو لاحقًا في الإعدادات.", - "welcome": "مرحبًا", "system_check": "فحص النظام", "install_models": "تثبيت النماذج", - "pick_engines": "اختر المحركات", "system_check_desc": "دقق في ذاكرة الوصول العشوائي (RAM) والقرص ووحدة معالجة الرسومات (GPU) وffmpeg والشبكة. يتم وضع علامة على أدوات الحظر مقدمًا حتى تعرفها قبل التنزيل.", "install_models_desc": "نزّل ~2.4 جيجابايت من الأوزان — نموذج TTS وحده هو المطلوب. أضف نماذج التفريغ المختارة لعتادك الآن أو لاحقاً من الإعدادات.", "pick_engines_desc": "اختر الواجهات الخلفية TTS / ASR / LLM. تعمل الإعدادات الافتراضية خارج الصندوق - قم بتخصيصها في أي وقت في الإعدادات.", "hero_desc": "الدبلجة واستنساخ الصوت وتصميم الصوت - كلها تعمل محليًا على جهازك.", - "first_run": "يستغرق التشغيل الأول من 5 إلى 10 دقائق للتنزيل. بعد ذلك، يكون كل إطلاق فوريًا وغير متصل بالإنترنت بشكل كامل.", - "get_started": "ابدأ", "back": "العودة", "continue_ok": "كل شيء جيد - استمر", "continue_warn": "متابعة (مع التحذيرات)", @@ -1727,7 +1657,6 @@ "try_dictation": "حاول الإملاء", "try_dictation_desc": "اضغط على اختصارك مرة واحدة للتأكد من أن الإملاء يعمل قبل إنهاء الإعداد.", "dictation_step_label": "الإملاء", - "next_try_dictation": "التالي: حاول الإملاء", "step_aria": "الخطوة {{num}}: {{label}}", "step_completed": "مكتملة", "cache_label": "ذاكرة التخزين المؤقت النموذجية" @@ -1743,7 +1672,6 @@ "platforms": "المنصات", "footer": "تساعد كل مساهمة في دفع حدود الذكاء الاصطناعي المحلي.", "back": "العودة إلى الاستوديو", - "commercial": "رخصة تجارية", "pill": "دعم أومنيفويس", "suggested_title": "اختر مبلغًا", "most_common": "الأكثر شيوعا", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "ادعم تطوير VoiceStudio", - "lead_default": "سعيد أن عملت! VoiceStudio مجاني ومحلي بالكامل. إذا كان ذلك يوفر عليك الوقت، فستقوم شريحة شهرية صغيرة بتمويل كلود ماكس الذي يقف وراءه.", - "lead_first_clone": "لقد تم استنساخ صوتك الأول – رائع! يعمل VoiceStudio بالكامل على جهازك، ويحافظ دعمك عليه على هذا النحو.", - "lead_tenth_dub": "عشر دبلجة - من الواضح أنك تعمل على تنفيذها. شريحة شهرية صغيرة تمول كلود ماكس الذي يشحن هذه الميزات.", - "lead_sustained": "لقد كنت مع VoiceStudio لمدة شهر. إذا حصلت على مكان في سير عملك، ففكر في المساعدة في تمويل الخطوة التالية.", - "chip_in": "رقاقة في", - "later": "ربما في وقت لاحق", - "star": "نجمة على جيثب", - "opt_out": "لا تسأل مرة أخرى", - "dismiss_aria": "استبعاد" + "later": "ربما في وقت لاحق" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "رخصة تجارية", "hero_title": "شحن أصوات الذكاء الاصطناعي في الإنتاج", "hero_desc": "VoiceStudio برنامج حر ومفتوح المصدر بموجب رخصة GNU Affero العمومية الإصدار 3 (AGPL-3.0) — مجاني للاستخدام، بما في ذلك الاستخدام التجاري والداخلي في الشركات. لا تحتاج إلى ترخيص تجاري إلا إذا أردت تضمين VoiceStudio في منتج أو خدمة مغلقة المصدر أو احتكارية دون التزامات الحقوق المتروكة (copyleft) في AGPL-3.0.", - "why_title": "لماذا تختار الشركات VoiceStudio", - "pricing_title": "التسعير", - "faq_title": "الأسئلة الشائعة", "pricing_desc": "لا يزال يجري وضع اللمسات النهائية على المستويات والتسعير.", - "pricing_detail": "وإلى أن يتم نشرها بشكل عام، يتم اقتباس كل عملية نشر تجارية بشكل فردي حتى نتمكن من تحديد الحجم المناسب لفريقك وعبء العمل.", "request_quote": "طلب عرض أسعار", - "footer_email": "أسئلة؟ تواصل معنا على hi@voicestudio.sh", - "footer_discord": "انضم إلى Discord للحصول على دعم المجتمع.", "benefit_ip": "ملكية IP كاملة", "benefit_ip_desc": "أنت تمتلك كل مخرجات، كل نموذج، كل مشتق. لا يوجد إيجار مشترك، ولا يوجد تسرب للبيانات.", "benefit_cost": "تكاليف الدقيقة صفر", "benefit_cost_desc": "لا توجد فواتير API، ولا توجد زيادات مفاجئة. يعمل على أجهزتك الخاصة منذ اليوم الأول.", - "benefit_team": "الوصول على مستوى الفريق", "benefit_team_desc": "One license covers unlimited users within your organization.", "benefit_support": "الدعم المباشر", "benefit_support_desc": "أولوية دعم البريد الإلكتروني ومكالمات الفيديو من الفريق الهندسي الأساسي.", - "benefit_source": "مصدر متاح الأساسية", "benefit_source_desc": "الرؤية الكاملة في المكدس. التدقيق والشوكة والتكيف ضمن شروط الترخيص.", - "benefit_lang": "646 لغة", "benefit_lang_desc": "قم بالنسخ والترجمة والدبلجة عبر 646 لغة بجودة تضاهي المستوى البشري.", - "hero_note": "الاستخدام والاستضافة الذاتية والاستخدام التجاري كلها مجانية بموجب AGPL-3.0 — حتى على نطاق واسع. AGPL رخصة حقوق متروكة شبكية: إذا عدّلت VoiceStudio وقدّمت النسخة المعدّلة للآخرين عبر الشبكة، فعليك مشاركة شيفرتك المصدرية المعدّلة بالشروط نفسها. الترخيص التجاري يرفع هذه الالتزامات عن عمليات النشر الاحتكارية مغلقة المصدر. خطط الأسعار قادمة قريبًا — تواصل معنا في هذه الأثناء.", - "hero_simple": "VoiceStudio مجاني ومفتوح المصدر بموجب AGPL-3.0 — بما في ذلك الاستخدام التجاري والداخلي. ما عليك سوى الحصول على ترخيص تجاري لتضمينه في منتج مغلق المصدر دون التزامات AGPL بالحقوق المتروكة.", - "contact_lead": "يتم تحديد الأسعار لكل عملية نشر بحيث تناسب فريقك وعبء العمل. أخبرني بحالة الاستخدام الخاصة بك وسأقدم لك عرض أسعار." + "hero_simple": "VoiceStudio مجاني ومفتوح المصدر بموجب AGPL-3.0 — بما في ذلك الاستخدام التجاري والداخلي. ما عليك سوى الحصول على ترخيص تجاري لتضمينه في منتج مغلق المصدر دون التزامات AGPL بالحقوق المتروكة." }, "exportModal": { "export": "تصدير", @@ -2006,7 +1915,6 @@ "loaded_ready": "محملة وجاهزة", "loading_first_run": "جارٍ التحميل... (قد يستغرق هذا من دقيقة إلى دقيقتين عند التشغيل لأول مرة)", "failed_to_load": "فشل التحميل", - "not_loaded_yet": "لم يتم تحميله بعد - سيتم تحميله عند النسخ الأول", "tts_not_loaded_yet": "لم يتم تحميله بعد — سيُحمّل عند أول توليد صوتي", "error_check_logs": "خطأ: {{error}}. تحقق من السجلات وحاول إعادة التشغيل.", "check_logs_restart": "تحقق من السجلات بحثًا عن أخطاء تحميل النموذج. حاول إعادة التشغيل.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "صب المتحدث", - "auto_assign_title": "تعيين الأصوات تلقائيًا من نسخ السماعات المستخرجة", - "auto_cast": "الإرسال التلقائي", - "all_cast": "كل الممثلين", - "segments_count": "{{count}} المقاطع", - "assign_voice": "تخصيص صوت…", - "from_video": "🎤 من الفيديو ({{name}})", - "no_profiles": "لم يتم حفظ أي ملفات تعريف صوتية حتى الآن.", - "preview_voice": "معاينة الصوت" + "segments_count": "{{count}} المقاطع" }, "checkpoint": { "timing_review": "هناك {{count}} مقطعًا بحاجة إلى مراجعة التوقيت", @@ -2203,8 +2104,6 @@ "play_failed": "فشل التشغيل: {{message}}" }, "models": { - "hf_token_set_toast": "مجموعة الرموز المميزة HuggingFace - تمكين التنزيلات الأسرع", - "hf_token_save_failed": "فشل حفظ الرمز المميز", "install_started": "بدأ التثبيت — التقدم في الصف", "delete_confirm": "هل تريد حذف {{repoId}}؟ ويمكنك إعادة تثبيته لاحقًا.", "delete_confirm_title": "حذف النموذج", @@ -2247,8 +2146,6 @@ "not_installed": "غير مثبت", "required_tag": "مطلوب", "delete_btn": "حذف", - "hf_token_btn": "رمز HF", - "hf_set_title": "قم بتعيين رمز HuggingFace للتنزيل بشكل أسرع", "get_token": "الحصول على رمز →", "reco_installed_for": "الحزمة الموصى بها المثبتة لـ {{device}}", "reco_for": "موصى به لـ {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "مطلوب ~{{size}} غيغابايت", "all_size": "الكل ~{{size}} غيغابايت", "req_tag": "مطلوب", - "search_placeholder": "نماذج البحث...", - "search_label": "نماذج البحث", "no_matches": "لا توجد نماذج تتطابق مع المرشحات الخاصة بك.", "sort_by": "فرز حسب {{column}}", "column_model": "نموذج", @@ -2267,12 +2162,8 @@ "ready_badge": "جاهز", "loading_badge": "جارٍ التحميل…", "idle_badge": "خامل", - "started_downloading_required_one": "بدأ تنزيل النموذج المطلوب {{count}}", - "started_downloading_required_other": "بدأ تنزيل {{count}} النماذج المطلوبة", "to_download": "للتحميل", "cached": "مخبأة", - "fast_download_badge": "تحميل سريع", - "fast_download_title": "تنزيلات سريعة عبر Xet {{version}} — نقل مجزأ ومتوازي", "voice_previews": "معاينات الأصوات", "voice_previews_desc": "نزِّل معاينات مُجهَّزة مسبقًا لتُشغَّل الأصوات فورًا، حتى قبل اكتمال تنزيل نموذج صوتي. لا يتم تنزيل أي شيء حتى تُفعِّل هذا الخيار.", "voice_previews_off": "معطَّل — تُنشأ المعاينات على هذا الجهاز", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "هل أحتاج إلى ترخيص للأدوات الداخلية؟", - "a_internal_tools": "لا. استخدام موظفيك ومتعاقديك — بما في ذلك التعديل والاستضافة الذاتية داخليًا — مجاني بموجب AGPL-3.0. لا يلزم الترخيص التجاري إلا إذا ضمّنت VoiceStudio في منتج أو خدمة مغلقة المصدر أو احتكارية ولم ترغب في الالتزام بمتطلبات AGPL لمشاركة الشيفرة المصدرية.", - "q_try_before": "هل يمكنني المحاولة قبل الالتزام؟", - "a_try_before": "نعم. التطبيق الكامل مجاني للتنزيل والتشغيل والاستضافة الذاتية بموجب AGPL-3.0 — دون أي اتفاقية. عندما تكون مستعدًا لمناقشة ترخيص تجاري (للاستخدام الاحتكاري)، راسلنا عبر البريد الإلكتروني وسنرتّب التفاصيل معًا.", - "q_watermark": "ماذا عن العلامة المائية؟", - "a_watermark": "تُضمَّن علامة AudioSeal المائية غير المرئية افتراضيًا للجميع. يمكنك إيقافها من الإعدادات ← الخصوصية، وهي تؤثر فقط على الصوت المُولَّد بعد التغيير." - }, "gallery_extra": { - "save_prompt": "أدخل اسمًا لملف التعريف الصوتي هذا:", - "saved_as_profile": "تم حفظ الصوت كملف شخصي!", - "save_profile_failed": "فشل حفظ الملف الشخصي", - "download_failed": "فشل التنزيل: {{message}}", - "trim_load_failed": "فشل تحميل الصوت للاقتطاع: {{message}}", - "upload_crop_failed": "فشل تحميل الصوت المقصوص: {{message}}" + "trim_load_failed": "فشل تحميل الصوت للاقتطاع: {{message}}" }, "dub_workflow": { "preparing_audio": "جارٍ تحضير الصوت…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "جارٍ التحميل…", - "trimmed_loaded": "تم تحميل الصوت المقطوع", - "toast_exported": "تم التصدير: {{name}}", - "toast_export_failed": "فشل التصدير: {{message}}", - "toast_open_folder_failed": "لا يمكن فتح المجلد: {{message}}", "toast_saving": "جارٍ حفظ {{name}}...", "toast_saved": "تم الحفظ: {{path}}", "toast_save_error": "خطأ في الحفظ: {{message}}", "toast_processing": "معالجة {{name}}...", "toast_downloaded": "تم التنزيل {{name}}", "toast_download_error": "خطأ في التنزيل: {{message}}", - "toast_upload_first": "الرجاء النقر فوق \"تحميل ونسخ\" أولاً حتى تتم معالجة الفيديو على الخادم قبل حفظه.", - "toast_save_failed": "فشل الحفظ: {{message}}", - "toast_opened": "مفتوح: {{name}}", - "toast_project_deleted": "تم حذف المشروع", - "toast_restored_state": "تمت استعادة حالة الجيل السابق", - "toast_history_deleted": "تم حذف عنصر السجل", "toast_flushed": "متدفق — ذاكرة الوصول العشوائي {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· تم تفريغ النموذج", "toast_flush_failed": "فشل التدفق: {{message}}", - "toast_project_saved": "تم حفظ المشروع", - "toast_project_created": "تم إنشاء المشروع" + "toast_project_saved": "تم حفظ المشروع" }, "update": { "available": "يتوفّر التحديث {{version}}", @@ -2531,13 +2398,11 @@ "not_writable": "غير قابل للكتابة", "change": "تغيير…", "compute_title": "المعالجة", - "compute_label": "GPU / مسرّع", "compute_auto": "تلقائي (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "بطاقة AMD ‏(ROCm، Linux)", "channel_label": "قناة التحديث", "channel_stable": "مستقرة", "channel_preview": "معاينة (أحدث main)", - "network_title": "الشبكة", "region_label": "منطقة التنزيل", "mirrors_title": "مرايا مخصصة (متقدم)", "mirror_pypi": "عنوان فهرس PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "الخصوصية أولاً: أصواتك وتسجيلاتك ومشاريعك لا تغادر هذا الجهاز أبدًا — بلا حساب ولا معالجة سحابية.", "resume_note": "تُستأنف التنزيلات المتقطعة تلقائيًا — إغلاق التطبيق آمن.", "eta_left": "متبقٍ ~{{eta}}", - "first_sound_text": "مرحبًا بك في الاستوديو الخاص بك. كل كلمة تسمعها وُلِّدت على هذا الجهاز للتو.", - "first_sound_done": "ذلك الصوت؟ وُلِّد قبل ثوانٍ، محليًا. مرحبًا بك.", - "hf_token_title": "رمز Hugging Face (اختياري)", - "hf_token_hint": "يفتح النماذج المقيّدة — مثل فصل المتحدثين للدبلجة متعددة الأصوات (pyannote). رمز حساب مجاني؛ يبقى على هذا الجهاز.", "hf_token_save": "حفظ", "hf_token_saving": "جارٍ الحفظ…", "hf_token_saved": "تم حفظ رمز Hugging Face", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} اليسار", "hf_token_card_title": "أضف رمز Hugging Face المميز مجانًا لإجراء تنزيلات أسرع", "hf_token_get": "ليس لديك رمز مميز؟ احصل على واحدة مجانًا →", - "hf_token_saved_fast": "تم حفظ رمز Hugging Face - أصبحت التنزيلات الآن أسرع", "hf_token_inline_prompt": "قم بتسريع التنزيلات باستخدام رمز Hugging Face المميز المجاني", "hf_token_inline_ph": "الصق hf_... الرمز المميز (اختياري)", "hf_token_get_short": "احصل على واحدة مجانًا →", @@ -2607,15 +2467,11 @@ "unstar_take": "إزالة النجمة — السماح بالتنظيف", "play_take": "تحميل كإخراج نشط", "filter_starred": "المميزة بنجمة", - "load_take_failed": "تعذر تحميل تلك اللقطة: {{message}}", "mode_clone": "استنساخ", "mode_synth": "تركيب", "generation_seconds": "{{duration}} ث", - "expand_text": "توسيع النص", - "collapse_text": "طي النص", "seed": "البذرة {{seed}}", - "load_config": "تحميل الإعدادات", - "delete_confirm": "حذف عنصر السجل هذا؟" + "load_config": "تحميل الإعدادات" }, "openapi": { "title": "واجهة برمجة تطبيقات VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "معاينة الراعي", "message": "علامتك التجارية وموقعك ورسالتك", "email_app": "افتح تطبيق البريد", - "copy_email": "نسخ البريد الإلكتروني", - "preview_detail": "يمكن أن يظهر هنا شعارك ورابطك ونبذة عنك." + "copy_email": "نسخ البريد الإلكتروني" }, "supportPlans": { "remove": "إزالة شريط الرعاة", "title": "المجاني مقابل Pro", "free": "مجاني", "pro": "Pro", - "sponsor_bar": "شريط الرعاة", "visible": "ظاهر", - "hideable": "يمكن إخفاؤه", "unavailable": "لم يتم إعداد تفعيل Pro بعد.", "telemetry": "بيانات الاستخدام", - "opt_in": "اختيارية وبموافقة مسبقة", "disabled": "معطّلة", - "badge": "شارة Pro", - "advanced": "أدوات متقدمة", - "standard": "قياسية", - "included": "مضمّنة" + "badge": "شارة Pro" }, "integrationCatalog": { "title": "التكاملات", @@ -3042,8 +2891,7 @@ "n8nHint": "استورد سير العمل هذا إلى n8n، وعدّل الطلب ثم شغّله. أبقِ VoiceStudio قيد التشغيل. يحتاج Docker وn8n البعيد إلى عنوان خلفية يمكن الوصول إليه وبيانات الاعتماد المطلوبة." }, "directoryExamples": { - "example": "مثال في الدليل", - "notice": "أمثلة في الدليل فقط — ليست جهات راعية أو تكاملات متصلة." + "example": "مثال في الدليل" }, "workflowRun": { "run": "تشغيل سير العمل", diff --git a/electron/src/shared/i18n/locales/de.json b/electron/src/shared/i18n/locales/de.json index c3cd4b8ba..34009215b 100644 --- a/electron/src/shared/i18n/locales/de.json +++ b/electron/src/shared/i18n/locales/de.json @@ -59,15 +59,10 @@ "title": "VoiceStudio Agent fragen", "notInstalled": "Nicht installiert", "noSource": "Öffnen Sie eine beschreibbare VoiceStudio-Quelle zum Auschecken, damit ein Agent sie reparieren kann.", - "noneFound": "Installieren Sie Codex, Claude Code, OpenCode oder Pi, um die lokale Reparatur zu verwenden.", "ready": "Bereit", "placeholder": "Was ist schiefgelaufen? Ergänze Schritte, sichtbare Fehler oder das erwartete Verhalten.", - "prReady": "Fix abgeschlossen. Überprüfen Sie die Änderungen und schlagen Sie eine PR vor, wenn die gezielten Tests erfolgreich sind.", "diagnose": "Diagnose", "fix": "Reparieren", - "chooseDefault": "Wähle den Agenten, den VoiceStudio bei einem Absturz dieser Ansicht automatisch verwenden soll.", - "useAutomatically": "{{agent}} automatisch verwenden", - "captured": "{{count}} problematische Protokollzeilen erfasst", "contextNotice": "Der ausgewählte Agent erhält diesen Bericht, die aktuelle Ansicht, aktuelle App- und Backend-Protokolle sowie Systemdiagnosen.", "complete": "Abgeschlossen" }, @@ -607,7 +602,6 @@ "characters": "{{count}} Zeichen", "minutes": "~{{count}} min", "chars": "{{count}} Zeichen", - "exportDone": "Hörbuch heruntergeladen", "exportFailed": "Der Export ist fehlgeschlagen. Überprüfen Sie, ob das Backend ausgeführt wird.", "global_speed": "Geschwindigkeit", "global_speed_hint": "Lesegeschwindigkeit für alle Zeilen ohne eigene Geschwindigkeitsüberschreibung", @@ -818,7 +812,6 @@ "data_dir": "Datenverzeichnis", "outputs": "Ausgänge", "crash_log": "Absturzprotokoll", - "update_endpoint": "Endpunkt aktualisieren", "yes": "ja", "no": "Nein", "web_preview": "Webvorschau", @@ -879,38 +872,21 @@ "title": "Anmeldeinformationen", "desc": "API-Schlüssel und Token werden <1>nur für diese Sitzung festgelegt. Um die Persistenz über Neustarts hinweg zu gewährleisten, legen Sie sie als Umgebungsvariablen in Ihrem Shell-Profil fest.", "hf_token": "HuggingFace-Token", - "hf_help": "Erforderlich für die Sprecherdiagnose und schnellere Modell-Downloads. Holen Sie sich Ihr Exemplar unter Huggingface.co/settings/tokens.", - "translate_key": "LLM-API-Schlüssel", - "translate_help": "Optional – für DeepL, OpenAI oder kostenpflichtige Übersetzungsanbieter. Für Google Translate (kostenloses Kontingent) nicht erforderlich.", "save": "Speichern", "saved": "✓ Festlegen", - "not_set": "✗ Nicht eingestellt", "get_token": "Token holen →", - "saved_session": "{{key}} für diese Sitzung gespeichert", "save_failed": "Speichern fehlgeschlagen", "save_error": "Speichern fehlgeschlagen: {{message}}", "deepl_key": "DeepL-API-Schlüssel", - "deepl_help": "Für die DeepL-Übersetzungs-Engine. Holen Sie sich Ihren Schlüssel unter deepl.com/pro-api.", "deepl_base_url": "DeepL-Basis-URL", "deepl_base_url_help": "Benutzerdefinierter DeepL-API-Endpunkt (Standard: https://api.deepl.com/v2). Änderung für Proxy/Selbstgehostet.", "microsoft_key": "Microsoft-Übersetzerschlüssel", - "microsoft_help": "Für die Microsoft Translator-Engine. Holen Sie sich Ihren Schlüssel vom Azure-Portal.", "microsoft_base_url": "Basis-URL des Microsoft-Übersetzers", "microsoft_base_url_help": "Benutzerdefinierter Azure Translator-Endpunkt (Standard: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "LLM-Basis-URL", - "llm_base_url_help": "Für Kinoqualität oder LLM-Engine. Jeder OpenAI-kompatible Endpunkt (OpenAI, Ollama, LM Studio usw.).", - "llm_model": "LLM-Modell", - "llm_model_help": "Modellname zur Verwendung mit der LLM-Engine (z. B. gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "Umarmendes Gesicht", - "group_translate": "Übersetzung (LLM / Film)", - "group_deepl": "Benutzerdefinierter DeepL-Endpunkt", - "group_microsoft": "Benutzerdefinierter Microsoft Translator-Endpunkt", "region": "Region", "aws_profile": "AWS-Profil" }, "engines": { - "banners_on": "Bühnenbanner auf", - "banners_off": "Bühnenbanner abgehängt", "backend": "Backend", "status": "Status", "ready": "fertig", @@ -941,7 +917,6 @@ "dictation_model_hint": "Wird vom Live-Diktat und von dieser Engine beim Transkribieren verwendet.", "dictation_not_installed": "Nicht installiert – zum Herunterladen auswählen", "active": "aktiv", - "whyUnavailable": "Was benötigt wird", "diskDetails": "Speicherplatzdetails", "diskModelDownload": "Modelldownload", "diskPackageDownload": "Paketdownload", @@ -965,11 +940,8 @@ "sectionReady": "Einsatzbereit", "sectionMore": "Weitere Motoren hinzufügen", "lastError": "Letzter Fehler: {{error}}", - "installedAndReady": "Installiert und fertig", - "notInstalled": "Nicht installiert", "available": "Verfügbar", "subprocessTitle": "Läuft in einem eigenen Unterprozess + venv", - "inProcessTitle": "Wird im VoiceStudio-Python-Prozess ausgeführt", "testEngine": "Testmotor", "testing": "Testen…", "recheck": "Überprüfen Sie es noch einmal", @@ -986,7 +958,6 @@ "routingUnknown": "Unbekannt", "routingEffectiveChip": "Läuft unter {{device}} auf diesem Computer", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU ausgewählt, aber: {{reason}}", "selectCpuFallback": "{{engine}}: läuft auf der CPU — {{reason}}", "selectWithCaveat": "Zu {{engine}} gewechselt — {{reason}}", "generateCaveat": "{{engine}} ist auf diesem Rechner möglicherweise langsam — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Kein Tauri-Protokoll verfügbar. Läuft nur in der Desktop-Shell.", "empty_backend": "Das Laufzeitprotokoll ist leer. Die Aktivität wird hier angezeigt, wenn das Backend sie protokolliert.", "title": "Protokolle", - "source_backend": "Backend", - "source_frontend": "Frontend", - "source_tauri": "Tauri", "expand": "Protokolle erweitern", "collapse": "Protokolle ausblenden", "expand_aria": "Erweitern Sie den Protokollbereich", @@ -1104,12 +1072,10 @@ "unlock": "Entsperren", "try_voice": "Versuchen Sie es mit dieser Stimme", "test_phrase": "Testphrase", - "test_help": "Geben Sie etwas ein – wir generieren es mit den aktuellen Einstellungen dieser Stimme.", "test_placeholder": "Geben Sie etwas ein, um zu hören, wie diese Stimme es sagt ...", "gen_preview": "Vorschau generieren", "generating": "Generieren…", "used_title": "Wo diese Stimme verwendet wurde", - "used_empty": "Diese Stimme wurde noch nicht verwendet. Erstellen Sie oben eine Vorschau oder wählen Sie sie in einem Dub aus.", "synth_clips_one": "{{count}} Synthesizer-Clip", "synth_clips_other": "{{count}} Synthesizer-Clips", "projects_count_one": "{{count}} Projekt", @@ -1152,9 +1118,7 @@ "translate": "Übersetzen", "generate": "Generieren", "export": "Exportieren", - "no_video": "Kein Video geladen", "segments": "Segmente", - "speakers": "Lautsprecher", "video_dubbing_studio": "Video-Synchronisationsstudio", "drop_here": "Legen Sie hier Video oder Audio ab", "paste_url": "…oder fügen Sie die YouTube-/Video-URL ein", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Der Cookie-Export darf höchstens 1 MB groß sein.", "save": "Speichern", "reset": "Zurücksetzen", - "save_project": "Projekt speichern", "change_file": "Datei ändern", "import_srt": ".srt importieren", "import_srt_alt": "Importieren Sie stattdessen .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Neu übersetzen", "transcript": "Transkript", "generate_dub": "Dub generieren", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Hochladen und transkribieren", "multi_lang": "Mehrsprachig", "prep_download": "Video wird heruntergeladen…", @@ -1206,7 +1166,6 @@ "transcribing": "Audio transkribieren…", "elapsed": "verstrichen", "remaining": "Es bleibt übrig", - "sidebar_toggle": "Seitenleiste umschalten", "retry_transcription": "Versuchen Sie die Transkription erneut", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Wenn es sich bei der URL um einen Host mit Untertiteln handelt (YouTube, Vimeo, TED …), rufen Sie auch die Originaluntertitel und alle automatischen YouTube-Übersetzungen ab. Setzt den Editor, ohne Whisper auszuführen; Überspringen Sie „Alles übersetzen“ für Sprachen, die YouTube bereits abdeckt.", @@ -1249,7 +1208,6 @@ "engine_label": "Motor", "install_engine": "Installieren Sie diesen Motor", "installing_engine": "…installieren", - "needs_dev_install": "Benötigt eine Entwicklerinstallation", "install_disabled_title": "Installationen sind in gepackten Builds deaktiviert", "needs_install_suffix": "— Muss installiert werden", "glossary_title": "Pin-Übersetzungen für wiederkehrende Begriffe (Namen, Markenwörter, Fachjargon)", @@ -1283,7 +1241,6 @@ "original_track": "Original", "selected_dub": "{{code}} (Ausgewählter Dub)", "dub_track": "{{code}} (Dub)", - "export_tracks": "Titel exportieren:", "stopping": "Anhalten…", "stop_progress": "Stopp ({{current}}/{{total}})", "regen_changed": "Regen {{count}} geändert", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} installiert", "install_failed": "Installation fehlgeschlagen: {{message}}", "prep_elapsed": "{{time}} abgelaufen", - "add_language": "Sprache hinzufügen", "search_languages": "Sprachen suchen…", "more_to_narrow": "+{{count}} mehr – zum Eingrenzen eingeben", "no_matches": "Keine Übereinstimmungen", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Zeit", - "spkr": "Spkr", "text": "Text", "lang": "Lang", "voice": "Stimme", @@ -1403,8 +1358,6 @@ "select_all_title": "Alle gefilterten auswählen", "sel_count": "{{count}} sel", "select_title": "Segment auswählen (Umschalt+Klick für Bereich)", - "sync_title": "Das generierte Audio beträgt {{pct}} % der Dauer des Originals", - "sync_label": "Synchronisierung: {{pct}}%", "rate_title": "Sprachgeschwindigkeitsanpassung: {{ratio}}× relativ zum Slot{{error}}", "speaker_id": "Sprecher-ID", "translate_error_title": "Übersetzungsfehler: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Originaltext wiederherstellen", "lang_default": "(Def)", "voice_default": "Standard", - "from_video": "Aus Video", - "clone_profiles": "Profile klonen", - "design_presets": "Design-Voreinstellungen", "preview_title": "Live-Vorschau", "set_direction": "Richtung festlegen…", "edit_direction": "Richtung bearbeiten…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Persönlichkeit", - "pick_personality": "Wählen Sie eine Persönlichkeitsvoreinstellung ...", "instruct": "Unterweisen", "reference_text": "Referenztext", "language": "Sprache", @@ -1535,9 +1484,7 @@ "stories": "Geschichten", "untitled_story": "Geschichte ohne Titel", "story_lines": "{{count}} Zeile", - "story_lines_plural": "{{count}} Zeilen", "story_voices": "{{count}} Stimme", - "story_voices_plural": "{{count}} Stimmen", "audiobooks": "Hörbücher", "audiobook": "Hörbuch", "story": "Geschichte", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galerie", - "search_placeholder": "YouTube durchsuchen…", - "all_voices": "Alle Stimmen ({{count}})", - "no_voices": "Noch keine Stimmen", "download_hint": "Suchen Sie oben, um Clips herunterzuladen", - "search_btn": "Suchen", - "download_btn": "Herunterladen", "saving": "Sparen…", "trim": "Trimmen", - "delete_confirm": "Diesen Sprachclip löschen?", - "view_mode_list": "Liste", - "view_mode_grid": "Gitter", - "youtube_results": "YouTube-Ergebnisse ({{count}})", - "clone_profile": "Profil klonen", - "crop_audio": "Audio zuschneiden", "subtitle": "Hunderte vorgefertigte Stimmen – wählen Sie eine aus und legen Sie los.", "zone_archetypes": "Archetypen", "zone_imports": "Meine Importe", @@ -1658,8 +1594,6 @@ "delete": "Löschen", "community_empty": "Noch keine Community-Stimmen geladen — stellen Sie eine Internetverbindung her und öffnen Sie erneut, oder reichen Sie als Erste(r) eine ein.", "community_explainer": "Gestaltete Voreinstellungen und aufgenommene Stimmen aus der Community, geladen aus der omnivoice-gallery.", - "no_designer": "Aufgenommene Stimme — verwenden Sie stattdessen „Benutzen Sie die Stimme“.", - "no_preview": "Keine Vorschau — fügen Sie sie mit „Benutzen Sie die Stimme“ hinzu, um sie anzuhören.", "submit_failed": "Das Einreichungsformular konnte nicht geöffnet werden.", "submit_preset": "Voreinstellung einreichen", "submit_voice": "Stimme einreichen", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Du kannst jetzt deine erste Stimme erstellen. Die Diktierfunktion kannst du jetzt oder später in den Einstellungen einrichten.", - "welcome": "Willkommen", "system_check": "Systemprüfung", "install_models": "Modelle installieren", - "pick_engines": "Wählen Sie Motoren aus", "system_check_desc": "Prüfen Sie RAM, Festplatte, GPU, ffmpeg und Netzwerk. Blocker werden im Voraus gekennzeichnet, sodass Sie vor dem Herunterladen Bescheid wissen.", "install_models_desc": "Laden Sie ca. 2,4 GB Gewichte herunter – nur das TTS-Modell ist erforderlich. Fügen Sie für Ihre Hardware kuratierte Transkriptionsmodelle jetzt oder jederzeit später in den Einstellungen hinzu.", "pick_engines_desc": "Wählen Sie TTS-/ASR-/LLM-Backends. Die Standardeinstellungen sind sofort einsatzbereit und können jederzeit in den Einstellungen angepasst werden.", "hero_desc": "Synchronisation, Klonen von Stimmen und Stimmendesign – alles läuft lokal auf Ihrem Computer.", - "first_run": "Der Download der ersten Ausführung dauert 5–10 Minuten. Danach erfolgt jeder Start sofort und vollständig offline.", - "get_started": "Fangen Sie an", "back": "Zurück", "continue_ok": "Alles gut – weiter", "continue_warn": "Weiter (mit Warnungen)", @@ -1727,7 +1657,6 @@ "try_dictation": "Versuchen Sie es mit Diktat", "try_dictation_desc": "Drücken Sie einmal Ihre Tastenkombination, um sicherzugehen, dass die Diktierfunktion funktioniert, bevor Sie die Einrichtung abschließen.", "dictation_step_label": "Diktat", - "next_try_dictation": "Als nächstes: Versuchen Sie es mit dem Diktat", "step_aria": "Schritt {{num}}: {{label}}", "step_completed": "abgeschlossen", "cache_label": "Modellcache" @@ -1743,7 +1672,6 @@ "platforms": "Plattformen", "footer": "Jeder Beitrag trägt dazu bei, die Grenzen der lokalen KI zu erweitern.", "back": "Zurück zum Studio", - "commercial": "Kommerzielle Lizenz", "pill": "Unterstützen Sie VoiceStudio", "suggested_title": "Wählen Sie einen Betrag", "most_common": "am häufigsten", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Unterstütze die Entwicklung von VoiceStudio", - "lead_default": "Ich bin froh, dass das funktioniert hat! VoiceStudio ist kostenlos und vollständig lokal. Wenn es Ihnen Zeit spart, finanziert ein kleiner monatlicher Chip-In den dahinter stehenden Claude Max.", - "lead_first_clone": "Ihr erster Sprachklon ist fertig – schön! VoiceStudio läuft vollständig auf Ihrem Computer und Ihr Support sorgt dafür, dass dies auch so bleibt.", - "lead_tenth_dub": "Nach zehn Dubs – Sie setzen es eindeutig um. Ein kleiner monatlicher Chip-In finanziert den Claude Max, der diese Funktionen bereitstellt.", - "lead_sustained": "Sie sind seit einem Monat bei VoiceStudio. Wenn es einen Platz in Ihrem Arbeitsablauf verdient hat, denken Sie darüber nach, bei der Finanzierung der nächsten Schritte mitzuhelfen.", - "chip_in": "Machen Sie mit", - "later": "Vielleicht später", - "star": "Stern auf GitHub", - "opt_out": "Fragen Sie nicht noch einmal", - "dismiss_aria": "Schließen" + "later": "Vielleicht später" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Kommerzielle Lizenz", "hero_title": "Versenden Sie KI-Stimmen in der Produktion", "hero_desc": "VoiceStudio ist freie Open-Source-Software unter der GNU Affero General Public License v3 (AGPL-3.0) — kostenlos nutzbar, auch für kommerzielle und interne geschäftliche Zwecke. Eine kommerzielle Lizenz benötigen Sie nur, wenn Sie VoiceStudio ohne die Copyleft-Pflichten der AGPL-3.0 in ein Closed-Source- oder proprietäres Produkt oder einen entsprechenden Dienst einbetten möchten.", - "why_title": "Warum Unternehmen sich für VoiceStudio entscheiden", - "pricing_title": "Preise", - "faq_title": "Häufige Fragen", "pricing_desc": "Stufen und Preise werden noch finalisiert.", - "pricing_detail": "Bis zur Veröffentlichung wird für jede kommerzielle Bereitstellung ein individuelles Angebot erstellt, damit wir die richtige Größe für Ihr Team und Ihre Arbeitslast festlegen können.", "request_quote": "Fordern Sie ein Angebot an", - "footer_email": "Fragen? Kontaktieren Sie uns unter hi@voicestudio.sh", - "footer_discord": "Treten Sie unserem Discord bei, um Community-Unterstützung zu erhalten.", "benefit_ip": "Vollständiges IP-Eigentum", "benefit_ip_desc": "Sie besitzen jede Ausgabe, jedes Modell, jedes Derivat. Keine gemeinsame Miete, keine Datenlecks.", "benefit_cost": "Null Kosten pro Minute", "benefit_cost_desc": "Keine API-Rechnungen, keine überraschenden Überschreitungen. Läuft vom ersten Tag an auf Ihrer eigenen Hardware.", - "benefit_team": "Teamweiter Zugriff", "benefit_team_desc": "Eine Lizenz deckt eine unbegrenzte Anzahl von Benutzern innerhalb Ihrer Organisation ab.", "benefit_support": "Direkte Unterstützung", "benefit_support_desc": "Vorrangiger E-Mail- und Videoanruf-Support durch das Kern-Engineering-Team.", - "benefit_source": "Quellverfügbarer Kern", "benefit_source_desc": "Volle Sicht auf den Stapel. Prüfen, forken und anpassen Sie innerhalb der Lizenzbedingungen.", - "benefit_lang": "646 Sprachen", "benefit_lang_desc": "Transkribieren, übersetzen und synchronisieren Sie in 646 Sprachen mit menschlicher Qualität.", - "hero_note": "Nutzung, Self-Hosting und kommerzielle Nutzung sind unter der AGPL-3.0 kostenlos — auch im großen Maßstab. Die AGPL ist eine Netzwerk-Copyleft-Lizenz: Wenn Sie VoiceStudio modifizieren und diese modifizierte Version anderen über ein Netzwerk anbieten, müssen Sie Ihren geänderten Quellcode zu denselben Bedingungen offenlegen. Eine kommerzielle Lizenz hebt diese Copyleft-Pflichten für proprietäre Closed-Source-Deployments auf. Preismodelle folgen in Kürze — melden Sie sich in der Zwischenzeit gerne.", - "hero_simple": "VoiceStudio ist kostenlos und Open Source gemäß AGPL-3.0 – auch für die kommerzielle und interne Geschäftsnutzung. Sie benötigen lediglich eine kommerzielle Lizenz, um es in ein Closed-Source-Produkt einzubetten, ohne die Copyleft-Verpflichtungen von AGPL.", - "contact_lead": "Die Preise werden pro Bereitstellung angegeben, sodass sie zu Ihrem Team und Ihrer Arbeitslast passen. Nennen Sie mir Ihren Anwendungsfall und ich erstelle Ihnen ein Angebot." + "hero_simple": "VoiceStudio ist kostenlos und Open Source gemäß AGPL-3.0 – auch für die kommerzielle und interne Geschäftsnutzung. Sie benötigen lediglich eine kommerzielle Lizenz, um es in ein Closed-Source-Produkt einzubetten, ohne die Copyleft-Verpflichtungen von AGPL." }, "exportModal": { "export": "Exportieren", @@ -2006,7 +1915,6 @@ "loaded_ready": "Geladen und fertig", "loading_first_run": "Wird geladen… (dies kann beim ersten Durchlauf 1-2 Minuten dauern)", "failed_to_load": "Laden fehlgeschlagen", - "not_loaded_yet": "Noch nicht geladen – wird bei der ersten Transkription geladen", "tts_not_loaded_yet": "Noch nicht geladen – wird bei der ersten Spracherzeugung geladen", "error_check_logs": "Fehler: {{error}}. Überprüfen Sie die Protokolle und versuchen Sie einen Neustart.", "check_logs_restart": "Überprüfen Sie die Protokolle auf Modellladefehler. Versuchen Sie einen Neustart.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Sprecher-Casting", - "auto_assign_title": "Weisen Sie automatisch Stimmen aus extrahierten Sprecherklonen zu", - "auto_cast": "Automatische Besetzung", - "all_cast": "Alles besetzt", - "segments_count": "{{count}} Segmente", - "assign_voice": "Stimme zuweisen…", - "from_video": "🎤 Aus Video ({{name}})", - "no_profiles": "Noch keine Sprachprofile gespeichert.", - "preview_voice": "Vorschau der Stimme" + "segments_count": "{{count}} Segmente" }, "checkpoint": { "timing_review": "{{count}} Segmente benötigen eine Timing-Prüfung", @@ -2203,8 +2104,6 @@ "play_failed": "Wiedergabe fehlgeschlagen: {{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace-Token-Set – schnellere Downloads ermöglicht", - "hf_token_save_failed": "Token konnte nicht gespeichert werden", "install_started": "Installation gestartet – Fortschritt in der Zeile", "delete_confirm": "{{repoId}} löschen? Sie können es später erneut installieren.", "delete_confirm_title": "Modell löschen", @@ -2247,8 +2146,6 @@ "not_installed": "nicht installiert", "required_tag": "erforderlich", "delete_btn": "Löschen", - "hf_token_btn": "HF-Token", - "hf_set_title": "Legen Sie das HuggingFace-Token für schnellere Downloads fest", "get_token": "Token holen →", "reco_installed_for": "Empfohlenes Bundle für {{device}} installiert", "reco_for": "Empfohlen für {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Erforderlich: ~{{size}} GB", "all_size": "Alle ~{{size}} GB", "req_tag": "erf", - "search_placeholder": "Modelle suchen…", - "search_label": "Modelle suchen", "no_matches": "Kein Modell entspricht Ihren Filtern.", "sort_by": "Sortieren nach {{column}}", "column_model": "Modell", @@ -2267,12 +2162,8 @@ "ready_badge": "Bereit", "loading_badge": "Laden…", "idle_badge": "Leerlauf", - "started_downloading_required_one": "Mit dem Herunterladen des erforderlichen Modells {{count}} begonnen", - "started_downloading_required_other": "Mit dem Herunterladen der für {{count}} erforderlichen Modelle begonnen", "to_download": "zum Herunterladen", "cached": "zwischengespeichert", - "fast_download_badge": "schneller Download", - "fast_download_title": "Schnelle Downloads über Xet {{version}} – parallele Chunk-Übertragung", "voice_previews": "Stimmvorschauen", "voice_previews_desc": "Lade vorab gerenderte Vorschauen herunter, damit Stimmen sofort abspielen – schon bevor ein Sprachmodell fertig geladen ist. Es wird nichts heruntergeladen, bis du dies aktivierst.", "voice_previews_off": "Aus – Vorschauen werden auf diesem Gerät gerendert", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Benötige ich eine Lizenz für interne Tools?", - "a_internal_tools": "Nein. Die Nutzung durch Ihre Mitarbeitenden und Auftragnehmer — einschließlich interner Modifikation und internem Self-Hosting — ist unter der AGPL-3.0 kostenlos. Eine kommerzielle Lizenz ist nur nötig, wenn Sie VoiceStudio in ein Closed-Source- oder proprietäres Produkt oder einen Dienst einbetten und die Quelloffenlegungspflichten der AGPL nicht erfüllen möchten.", - "q_try_before": "Kann ich es versuchen, bevor ich mich verpflichte?", - "a_try_before": "Ja. Die vollständige App lässt sich unter der AGPL-3.0 kostenlos herunterladen, ausführen und selbst hosten — ganz ohne Vertrag. Wenn Sie über eine kommerzielle Lizenz (für proprietäre Nutzung) sprechen möchten, schreiben Sie uns eine E-Mail und wir klären die Details gemeinsam.", - "q_watermark": "Was ist mit dem Wasserzeichen?", - "a_watermark": "Das unsichtbare AudioSeal-Wasserzeichen wird standardmäßig für alle eingebettet. Sie können es unter Einstellungen → Datenschutz abschalten; es betrifft nur Audio, das nach der Änderung erzeugt wird." - }, "gallery_extra": { - "save_prompt": "Geben Sie einen Namen für dieses Sprachprofil ein:", - "saved_as_profile": "Stimme als Profil gespeichert!", - "save_profile_failed": "Profil konnte nicht gespeichert werden", - "download_failed": "Download fehlgeschlagen: {{message}}", - "trim_load_failed": "Audio zum Zuschneiden konnte nicht geladen werden: {{message}}", - "upload_crop_failed": "Zugeschnittene Stimme konnte nicht hochgeladen werden: {{message}}" + "trim_load_failed": "Audio zum Zuschneiden konnte nicht geladen werden: {{message}}" }, "dub_workflow": { "preparing_audio": "Audio wird vorbereitet…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Laden…", - "trimmed_loaded": "Zugeschnittenes Audio geladen", - "toast_exported": "Exportiert: {{name}}", - "toast_export_failed": "Export fehlgeschlagen: {{message}}", - "toast_open_folder_failed": "Ordner konnte nicht geöffnet werden: {{message}}", "toast_saving": "Speichern {{name}}...", "toast_saved": "Gespeichert: {{path}}", "toast_save_error": "Speicherfehler: {{message}}", "toast_processing": "Verarbeitung {{name}}...", "toast_downloaded": "Heruntergeladen {{name}}", "toast_download_error": "Download-Fehler: {{message}}", - "toast_upload_first": "Bitte klicken Sie zuerst auf „Hochladen und transkribieren“, damit das Video vor dem Speichern auf dem Server verarbeitet wird.", - "toast_save_failed": "Speichern fehlgeschlagen: {{message}}", - "toast_opened": "Geöffnet: {{name}}", - "toast_project_deleted": "Projekt gelöscht", - "toast_restored_state": "Wiederhergestellter Zustand der vorherigen Generation", - "toast_history_deleted": "Verlaufselement gelöscht", "toast_flushed": "Gespült – RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· Modell entladen", "toast_flush_failed": "Spülung fehlgeschlagen: {{message}}", - "toast_project_saved": "Projekt gespeichert", - "toast_project_created": "Projekt erstellt" + "toast_project_saved": "Projekt gespeichert" }, "update": { "available": "Update {{version}} verfügbar", @@ -2531,13 +2398,11 @@ "not_writable": "nicht beschreibbar", "change": "Ändern…", "compute_title": "Rechenleistung", - "compute_label": "GPU / Beschleuniger", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD-GPU (ROCm, Linux)", "channel_label": "Update-Kanal", "channel_stable": "Stabil", "channel_preview": "Vorschau (aktueller main)", - "network_title": "Netzwerk", "region_label": "Download-Region", "mirrors_title": "Eigene Mirrors (erweitert)", "mirror_pypi": "PyPI-Index-URL", @@ -2570,10 +2435,6 @@ "trust_line": "Datenschutz zuerst: Deine Stimmen, Aufnahmen und Projekte verlassen diesen Rechner nie — kein Konto, keine Cloud-Verarbeitung.", "resume_note": "Unterbrochene Downloads werden automatisch fortgesetzt — die App zu schließen ist sicher.", "eta_left": "noch ~{{eta}}", - "first_sound_text": "Willkommen in deinem Studio. Jedes Wort, das du hörst, wurde gerade eben auf diesem Rechner erzeugt.", - "first_sound_done": "Diese Stimme? Vor Sekunden lokal erzeugt. Willkommen.", - "hf_token_title": "Hugging-Face-Token (optional)", - "hf_token_hint": "Schaltet beschränkte Modelle frei — z. B. Sprecher-Diarisierung für Mehrsprecher-Dubbing (pyannote). Kostenloses Konto-Token; bleibt auf diesem Rechner.", "hf_token_save": "Speichern", "hf_token_saving": "speichert…", "hf_token_saved": "Hugging-Face-Token gespeichert", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} übrig", "hf_token_card_title": "Fügen Sie für schnellere Downloads ein kostenloses Hugging Face-Token hinzu", "hf_token_get": "Sie haben kein Token? Holen Sie sich eins gratis →", - "hf_token_saved_fast": "Hugging Face-Token gespeichert – Downloads sind jetzt schneller", "hf_token_inline_prompt": "Beschleunigen Sie Downloads mit einem kostenlosen Hugging Face-Token", "hf_token_inline_ph": "HF_…-Token einfügen (optional)", "hf_token_get_short": "Holen Sie sich eins gratis →", @@ -2607,15 +2467,11 @@ "unstar_take": "Markierung entfernen — Aufräumen erlauben", "play_take": "Als aktive Ausgabe laden", "filter_starred": "Markiert", - "load_take_failed": "Take konnte nicht geladen werden: {{message}}", "mode_clone": "Klon", "mode_synth": "Synthese", "generation_seconds": "{{duration}} s", - "expand_text": "Text erweitern", - "collapse_text": "Text einklappen", "seed": "Seed {{seed}}", - "load_config": "Konfiguration laden", - "delete_confirm": "Diesen Verlaufseintrag löschen?" + "load_config": "Konfiguration laden" }, "openapi": { "title": "VoiceStudio-API", @@ -3015,25 +2871,18 @@ "preview": "Sponsor-Vorschau", "message": "Deine Marke, Website und Nachricht", "email_app": "E-Mail-App öffnen", - "copy_email": "E-Mail-Adresse kopieren", - "preview_detail": "Hier könnten dein Logo, Link und eine Vorstellung stehen." + "copy_email": "E-Mail-Adresse kopieren" }, "supportPlans": { "remove": "Sponsorenleiste entfernen", "title": "Free und Pro im Vergleich", "free": "Kostenlos", "pro": "Pro", - "sponsor_bar": "Sponsorenleiste", "visible": "Sichtbar", - "hideable": "Ausblendbar", "unavailable": "Die Pro-Aktivierung ist noch nicht eingerichtet.", "telemetry": "Telemetrie", - "opt_in": "Optional, nur mit Zustimmung", "disabled": "Deaktiviert", - "badge": "Pro-Abzeichen", - "advanced": "Erweiterte Werkzeuge", - "standard": "Standard", - "included": "Enthalten" + "badge": "Pro-Abzeichen" }, "integrationCatalog": { "title": "Integrationen", @@ -3042,8 +2891,7 @@ "n8nHint": "Importiere diesen Workflow in n8n, bearbeite die Anfrage und führe ihn aus. Lass VoiceStudio laufen. Docker und entfernte n8n-Instanzen benötigen eine erreichbare Backend-Adresse und gegebenenfalls Zugangsdaten." }, "directoryExamples": { - "example": "Verzeichnisbeispiel", - "notice": "Nur Verzeichnisbeispiele — keine Sponsoren oder verbundenen Integrationen." + "example": "Verzeichnisbeispiel" }, "workflowRun": { "run": "Workflow ausführen", diff --git a/electron/src/shared/i18n/locales/en.json b/electron/src/shared/i18n/locales/en.json index 56a020d95..ff11eb450 100644 --- a/electron/src/shared/i18n/locales/en.json +++ b/electron/src/shared/i18n/locales/en.json @@ -59,15 +59,10 @@ "title": "Ask VoiceStudio Agent", "notInstalled": "Not installed", "noSource": "Open a writable VoiceStudio source checkout to let an agent repair it.", - "noneFound": "Install Codex, Claude Code, OpenCode, or Pi to use local repair.", "ready": "Ready", "placeholder": "What went wrong? Add steps, visible errors, or expected behavior.", - "prReady": "Fix completed. Review the changes and propose a PR when the targeted tests pass.", "diagnose": "Diagnose", "fix": "Fix", - "chooseDefault": "Choose the agent VoiceStudio should use automatically when this screen crashes.", - "useAutomatically": "Use {{agent}} automatically", - "captured": "Captured {{count}} problem log lines", "contextNotice": "The selected agent receives this report, the current screen, recent app and backend logs, and system diagnostics.", "complete": "Complete" }, @@ -270,7 +265,6 @@ "characters": "{{count}} characters", "minutes": "~{{count}} min", "chars": "{{count}} chars", - "exportDone": "Audiobook downloaded", "exportFailed": "Export failed — check the backend is running.", "deleteProjectConfirm": "Delete this project permanently?" }, @@ -1122,7 +1116,6 @@ "data_dir": "Data directory", "outputs": "Outputs", "crash_log": "Crash log", - "update_endpoint": "Update endpoint", "update_channel": "Update channel", "channel_stable": "Stable", "channel_preview": "Preview", @@ -1193,33 +1186,18 @@ "title": "Credentials", "desc": "API keys and tokens are set <1>for this session only. For persistence across restarts, set them as environment variables in your shell profile.", "hf_token": "HuggingFace Token", - "hf_help": "Required for speaker diarization and faster model downloads. Get yours at huggingface.co/settings/tokens.", - "translate_key": "LLM API Key", - "translate_help": "Optional — for DeepL, OpenAI, or paid translation providers. Not needed for Google Translate (free tier).", "save": "Save", "saved": "✓ Set", - "not_set": "✗ Not set", "get_token": "Get token →", - "saved_session": "{{key}} saved for this session", "saved_persisted": "{{key}} saved", "save_failed": "Failed to save", "save_error": "Save failed: {{message}}", "deepl_key": "DeepL API Key", - "deepl_help": "For DeepL translation engine. Get your key at deepl.com/pro-api.", "deepl_base_url": "DeepL Base URL", "deepl_base_url_help": "Custom DeepL API endpoint (default: https://api.deepl.com/v2). Change for proxy/self-hosted.", "microsoft_key": "Microsoft Translator Key", - "microsoft_help": "For Microsoft Translator engine. Get your key from Azure Portal.", "microsoft_base_url": "Microsoft Translator Base URL", "microsoft_base_url_help": "Custom Azure Translator endpoint (default: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "LLM Base URL", - "llm_base_url_help": "For Cinematic quality or LLM engine. Any OpenAI-compatible endpoint (OpenAI, Ollama, LM Studio, etc.).", - "llm_model": "LLM Model", - "llm_model_help": "Model name to use with the LLM engine (e.g. gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "HuggingFace", - "group_translate": "Translation (LLM / Cinematic)", - "group_deepl": "DeepL Custom Endpoint", - "group_microsoft": "Microsoft Translator Custom Endpoint", "region": "Region", "aws_profile": "AWS profile" }, @@ -1354,12 +1332,10 @@ "unlock": "Unlock", "try_voice": "Try this voice", "test_phrase": "Test phrase", - "test_help": "Type anything — we'll generate it with this voice's current settings.", "test_placeholder": "Type something to hear this voice say it…", "gen_preview": "Generate preview", "generating": "Generating…", "used_title": "Where this voice has been used", - "used_empty": "This voice hasn't been used yet. Generate a preview above, or pick it in a dub.", "synth_clips_one": "{{count}} synth clip", "synth_clips_other": "{{count}} synth clips", "projects_count_one": "{{count}} project", @@ -1381,9 +1357,7 @@ "translate": "Translate", "generate": "Generate", "export": "Export", - "no_video": "No video loaded", "segments": "segments", - "speakers": "speakers", "video_dubbing_studio": "Video Dubbing Studio", "drop_here": "Drop video or audio here", "paste_url": "…or paste YouTube / video URL", @@ -1397,7 +1371,6 @@ "cookie_size_error": "The cookie export must be 1 MB or smaller.", "save": "Save", "reset": "Reset", - "save_project": "Save project", "change_file": "Change file", "import_srt": "Import .srt", "import_srt_alt": "Import .srt instead", @@ -1418,9 +1391,6 @@ "retranslate": "Re-translate", "transcript": "Transcript", "generate_dub": "Generate Dub", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Upload & Transcribe", "num_speakers_label": "Speakers", "num_speakers_auto": "Auto", @@ -1441,7 +1411,6 @@ "transcribing": "Transcribing audio…", "elapsed": "elapsed", "remaining": "s remaining", - "sidebar_toggle": "Toggle Sidebar", "retry_transcription": "Retry transcription", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "When the URL is a caption-bearing host (YouTube, Vimeo, TED…), also pull the original captions and any YouTube auto-translations. Seeds the editor without running Whisper; skip Translate All for languages YouTube already covers.", @@ -1484,14 +1453,12 @@ "clean_up_title": "Merge tiny fragments and adjacent short segments", "popular": "Popular", "all_languages": "All languages", - "add_language": "Add language", "search_languages": "Search languages…", "more_to_narrow": "+{{count}} more — type to narrow", "no_matches": "No matches", "engine_label": "Engine", "install_engine": "Install this engine", "installing_engine": "…installing", - "needs_dev_install": "needs dev install", "install_disabled_title": "Installs are disabled in packaged builds", "needs_install_suffix": " — needs install", "glossary_title": "Pin translations for recurring terms (names, brand words, jargon)", @@ -1530,7 +1497,6 @@ "original_track": "Original", "selected_dub": "{{code}} (Selected Dub)", "dub_track": "{{code}} (Dub)", - "export_tracks": "Export Tracks:", "stopping": "Stopping…", "stop_progress": "Stop ({{current}}/{{total}})", "regen_changed": "Regen {{count}} changed", @@ -1555,7 +1521,6 @@ "advanced": "Advanced", "cinematic_needs_llm_hint": "Cinematic needs an LLM. Configure one in Settings → Credentials → LLM endpoint (Ollama runs locally, no key needed).", "hq_needs_llm_hint": "High-quality translation fits each line to its segment time using a local or cloud LLM. Set one up to enable it.", - "set_up_llm": "Set up", "generate_dub_multi": "Generate {{count}} dubs", "multi_translating": "Translating → {{lang}} ({{current}}/{{total}})…", "multi_lang_skipped": "Translation failed for {{langs}} — those dubs were skipped so you never get a wrong-language track.", @@ -1630,7 +1595,6 @@ }, "segment": { "time": "Time", - "spkr": "Spkr", "text": "Text", "lang": "Lang", "voice": "Voice", @@ -1641,8 +1605,6 @@ "select_all_title": "Select all filtered", "sel_count": "{{count}} sel", "select_title": "Select segment (shift+click for range)", - "sync_title": "Generated audio is {{pct}}% the duration of original", - "sync_label": "Sync: {{pct}}%", "rate_title": "Speech-rate fit: {{ratio}}× relative to slot{{error}}", "speaker_id": "Speaker ID", "translate_degraded_title": "Translated (plain) — the polish pass was skipped: {{reason}}", @@ -1653,9 +1615,6 @@ "restore_title": "Restore original text", "lang_default": "(Def)", "voice_default": "Default", - "from_video": "From Video", - "clone_profiles": "Clone Profiles", - "design_presets": "Design Presets", "preview_title": "Live Preview", "set_direction": "Set direction…", "edit_direction": "Edit direction…", @@ -1712,7 +1671,6 @@ }, "voice": { "personality": "Personality", - "pick_personality": "Pick a personality preset…", "instruct": "Instruct", "reference_text": "Reference Text", "language": "Language", @@ -1759,9 +1717,7 @@ "stories": "Stories", "untitled_story": "Untitled story", "story_lines": "{{count}} line", - "story_lines_plural": "{{count}} lines", "story_voices": "{{count}} voice", - "story_voices_plural": "{{count}} voices", "voice_profiles": "Voice Profiles", "transcripts": "Transcripts", "audiobooks": "Audiobooks", @@ -1847,20 +1803,9 @@ }, "gallery": { "title": "VoiceStudio Gallery", - "search_placeholder": "Search YouTube…", - "all_voices": "All Voices ({{count}})", - "no_voices": "No voices yet", "download_hint": "Search above to download clips", - "search_btn": "Search", - "download_btn": "Download", "saving": "Saving…", "trim": "Trim", - "delete_confirm": "Delete this voice clip?", - "view_mode_list": "List", - "view_mode_grid": "Grid", - "youtube_results": "YouTube Results ({{count}})", - "clone_profile": "Clone profile", - "crop_audio": "Crop audio", "subtitle": "Hundreds of ready-made designed voices — pick one and go.", "zone_archetypes": "Archetypes", "zone_imports": "My Imports", @@ -1902,8 +1847,6 @@ "delete": "Delete", "community_empty": "No community voices loaded yet — connect to the internet and reopen, or be the first to submit one.", "community_explainer": "Designed presets and recorded voices shared by the community, loaded from the omnivoice-gallery.", - "no_designer": "Recorded voice — use \"Use voice\" instead.", - "no_preview": "No preview — add it with \"Use voice\" to hear it.", "submit_failed": "Could not open the submission form.", "submit_preset": "Submit a preset", "submit_voice": "Submit a voice", @@ -1959,24 +1902,18 @@ }, "setup": { "ready_desc": "You’re ready to create your first voice. You can set up dictation now or later in Settings.", - "welcome": "Welcome", "system_check": "System check", "install_models": "Install models", - "pick_engines": "Pick engines", "system_check_desc": "Probe RAM, disk, GPU, and network. Blockers are flagged upfront so you know before downloading.", "media_engine_preparing": "Preparing media engine…", "media_engine_failed_title": "Media engine download failed", "media_engine_failed_desc": "The app couldn't fetch its bundled audio/video engine (FFmpeg). Retry, or point it at a copy already on this computer.", "media_engine_retry": "Retry", "media_engine_use_system": "Use a system copy", - "media_engine_detect_failed": "No system copy found — retry the download or choose the file manually.", "media_engine_choose_file": "Choose file…", - "media_engine_ready": "Media engine configured.", "install_models_desc": "Download ~2.4 GB of weights — only the TTS model is required. Add transcription models curated for your hardware now, or anytime from Settings.", "pick_engines_desc": "Choose TTS / ASR / LLM backends. Defaults work out of the box — customize anytime in Settings.", "hero_desc": "Dubbing, voice cloning, and voice design — all running locally on your machine.", - "first_run": "First run takes 5–10 minutes to download. After that, every launch is instant and fully offline.", - "get_started": "Get started", "back": "Back", "continue_ok": "All good — continue", "continue_warn": "Continue (with warnings)", @@ -1998,7 +1935,6 @@ "try_dictation": "Try dictation", "try_dictation_desc": "Press your shortcut once so you know dictation works before you finish setup.", "dictation_step_label": "Dictation", - "next_try_dictation": "Next: Try dictation", "step_aria": "Step {{num}}: {{label}}", "step_completed": "completed", "mirror_rescue_title": "Can't reach Hugging Face? Use a mirror", @@ -2017,7 +1953,6 @@ "platforms": "Platforms", "footer": "Every contribution helps push the boundaries of local AI.", "back": "Back to Studio", - "commercial": "Commercial License", "pill": "Support VoiceStudio", "suggested_title": "Pick an amount", "most_common": "most common", @@ -2035,15 +1970,7 @@ }, "postcard": { "title": "Support VoiceStudio development", - "lead_default": "Glad that worked! VoiceStudio is free and fully local. If it saves you time, a small monthly chip-in funds the Claude Max behind it.", - "lead_first_clone": "Your first voice clone is done — nice! VoiceStudio runs entirely on your machine, and your support keeps it that way.", - "lead_tenth_dub": "Ten dubs in — you're clearly putting it to work. A small monthly chip-in funds the Claude Max that ships these features.", - "lead_sustained": "You've been with VoiceStudio for a month. If it's earned a spot in your workflow, consider helping fund what's next.", - "chip_in": "Chip in", - "later": "Maybe later", - "star": "Star on GitHub", - "opt_out": "Don't ask again", - "dismiss_aria": "Dismiss" + "later": "Maybe later" } }, "enterprise": { @@ -2052,29 +1979,18 @@ "back": "Back to Studio", "badge": "Commercial License", "hero_simple": "VoiceStudio is free and open-source under the AGPL-3.0 — including for commercial and internal business use. You only need a commercial license to embed it in a closed-source product without AGPL’s copyleft obligations.", - "contact_lead": "Pricing is quoted per deployment so it fits your team and workload. Tell me your use case and I’ll get you a quote.", "hero_title": "Ship AI voices in production", "hero_desc": "VoiceStudio is free and open-source software under the GNU Affero General Public License v3 (AGPL-3.0) — free to use, including for commercial and internal business use. A commercial license is needed only if you want to embed VoiceStudio in a closed-source or proprietary product or service without AGPL-3.0's copyleft obligations.", - "hero_note": "Use, self-hosting, and commercial use are all free under the AGPL-3.0 — including at scale. AGPL is a network-copyleft license: if you modify VoiceStudio and offer that modified version to others over a network, you must share your modified source under the same terms. A commercial license lifts those copyleft obligations for proprietary, closed-source deployments. Pricing tiers are coming soon — get in touch in the meantime.", - "why_title": "Why businesses choose VoiceStudio", - "pricing_title": "Pricing", - "faq_title": "Common questions", "pricing_desc": "Tiers and pricing are still being finalized.", - "pricing_detail": "Until they’re public, every commercial deployment is being quoted individually so we can right-size for your team and workload.", "request_quote": "Request a quote", - "footer_email": "Questions? Reach out at hi@voicestudio.sh", - "footer_discord": "Join our Discord for community support.", "benefit_ip": "Full IP ownership", "benefit_ip_desc": "You own every output, every model, every derivative. No shared tenancy, no data leaks.", "benefit_cost": "Zero per-minute costs", "benefit_cost_desc": "No API bills, no surprise overages. Runs on your own hardware from day one.", - "benefit_team": "Team-wide access", "benefit_team_desc": "One license covers unlimited users within your organization.", "benefit_support": "Direct support", "benefit_support_desc": "Priority email and video-call support from the core engineering team.", - "benefit_source": "Source-available core", "benefit_source_desc": "Full visibility into the stack. Audit, fork, and adapt within the license terms.", - "benefit_lang": "646 languages", "benefit_lang_desc": "Transcribe, translate, and dub across 646 languages with human-level quality." }, "exportModal": { @@ -2255,8 +2171,6 @@ "taxonomyTokens": "taxonomy tokens" }, "engines": { - "banners_on": "Stage banners on", - "banners_off": "Stage banners off", "backend": "Backend", "status": "Status", "ready": "ready", @@ -2280,8 +2194,6 @@ "selectHint": "Select an engine to see what it needs, test it, or install it.", "familyMatrixTitle": "{{family}} Engines", "colEngine": "Engine", - "colGpuCompat": "GPU compat", - "colIsolation": "Isolation", "colActions": "Actions", "loadFailed": "Failed to load engines: {{message}}", "couldNotLoad": "Could not load engines: {{message}}", @@ -2292,7 +2204,6 @@ "dictation_model_hint": "Used by live dictation and by this engine when it transcribes.", "dictation_not_installed": "Not installed — select to download", "active": "active", - "whyUnavailable": "What it needs", "diskDetails": "Disk details", "diskModelDownload": "Model download", "diskPackageDownload": "Package download", @@ -2316,11 +2227,8 @@ "sectionReady": "Ready to use", "sectionMore": "Add more engines", "lastError": "Last error: {{error}}", - "installedAndReady": "Installed and ready", - "notInstalled": "Not installed", "available": "Available", "subprocessTitle": "Runs in its own subprocess + venv", - "inProcessTitle": "Runs in the VoiceStudio Python process", "testEngine": "Test engine", "testing": "Testing…", "recheck": "Re-check", @@ -2347,7 +2255,6 @@ "routingUnknown": "Unknown", "routingEffectiveChip": "Runs on {{device}} on this machine", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU selected, but: {{reason}}", "selectCpuFallback": "{{engine}}: running on CPU — {{reason}}", "curatedModelLabel": "Model", "curatedModelAria": "Model for {{engine}}", @@ -2541,7 +2448,6 @@ "loaded_ready": "Loaded and ready", "loading_first_run": "Loading… (this may take 1-2 minutes on first run)", "failed_to_load": "Failed to load", - "not_loaded_yet": "Not loaded yet — will load on first transcription", "tts_not_loaded_yet": "Not loaded yet — will load on first speech generation", "error_check_logs": "Error: {{error}}. Check logs and try restarting.", "check_logs_restart": "Check logs for model loading errors. Try restarting.", @@ -2665,9 +2571,6 @@ "empty_tauri": "No Tauri log available. Runs in the desktop shell only.", "empty_backend": "Runtime log is empty. Activity will appear here as the backend logs it.", "title": "Logs", - "source_backend": "Backend", - "source_frontend": "Frontend", - "source_tauri": "Tauri", "expand": "Expand logs", "collapse": "Collapse logs", "expand_aria": "Expand logs panel", @@ -2761,14 +2664,7 @@ }, "casting": { "title": "Speaker Casting", - "auto_assign_title": "Auto-assign voices from extracted speaker clones", - "auto_cast": "Auto-cast", - "all_cast": "All cast", - "segments_count": "{{count}} segments", - "assign_voice": "Assign voice…", - "from_video": "🎤 From video ({{name}})", - "no_profiles": "No voice profiles saved yet.", - "preview_voice": "Preview voice" + "segments_count": "{{count}} segments" }, "checkpoint": { "timing_review": "{{count}} segments need timing review", @@ -2810,8 +2706,6 @@ "play_failed": "Play failed: {{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace token set — faster downloads enabled", - "hf_token_save_failed": "Failed to save token", "install_started": "Install started — progress in the row", "delete_confirm": "Delete {{repoId}}? You can reinstall it later.", "delete_confirm_title": "Delete model", @@ -2834,8 +2728,6 @@ "files_progress": "{{done}}/{{total}} files", "to_download": "to download", "cached": "cached", - "fast_download_badge": "fast download", - "fast_download_title": "Fast downloads via Xet {{version}} — parallel chunked transfer", "install_error": "Install failed: {{error}}", "retry_btn": "Retry", "cancel_btn": "Cancel", @@ -2866,8 +2758,6 @@ "repair_title": "Finish the interrupted download", "required_tag": "required", "delete_btn": "Delete", - "hf_token_btn": "HF Token", - "hf_set_title": "Set HuggingFace token for faster downloads", "get_token": "Get token →", "reco_installed_for": "Recommended bundle installed for {{device}}", "reco_for": "Recommended for {{device}}", @@ -2875,8 +2765,6 @@ "required_size": "Required ~{{size}} GB", "all_size": "All ~{{size}} GB", "req_tag": "req", - "search_placeholder": "Search models…", - "search_label": "Search models", "no_matches": "No models match your filters.", "clear_filters": "Clear filters", "in_memory": "In memory", @@ -2895,8 +2783,6 @@ "ready_badge": "Ready", "loading_badge": "Loading…", "idle_badge": "Idle", - "started_downloading_required_one": "Started downloading {{count}} required model", - "started_downloading_required_other": "Started downloading {{count}} required models", "role_all": "All", "role_tts": "TTS", "role_asr": "ASR", @@ -2961,21 +2847,8 @@ "voice_previews_check": "Check now", "voice_previews_rejected": "Voice gallery update rejected: {{message}}" }, - "enterprise_faq": { - "q_internal_tools": "Do I need a license for internal tools?", - "a_internal_tools": "No. Use by your employees and contractors — including modifying and self-hosting internally — is free under the AGPL-3.0. A commercial license is only needed if you embed VoiceStudio in a closed-source or proprietary product or service and don't want to comply with AGPL's source-sharing obligations.", - "q_try_before": "Can I try before committing?", - "a_try_before": "Yes. The full app is free to download, run, and self-host under the AGPL-3.0 — no agreement required. When you're ready to discuss a commercial (proprietary-use) license, email us and we'll work through the details together.", - "q_watermark": "What about the watermark?", - "a_watermark": "The invisible AudioSeal watermark is embedded by default for everyone. You can turn it off in Settings → Privacy; it only affects audio generated after the change." - }, "gallery_extra": { - "save_prompt": "Enter a name for this voice profile:", - "saved_as_profile": "Voice saved as profile!", - "save_profile_failed": "Failed to save profile", - "download_failed": "Download failed: {{message}}", - "trim_load_failed": "Failed to load audio for trimming: {{message}}", - "upload_crop_failed": "Failed to upload cropped voice: {{message}}" + "trim_load_failed": "Failed to load audio for trimming: {{message}}" }, "dub_workflow": { "preparing_audio": "Preparing audio…", @@ -3136,27 +3009,16 @@ }, "app": { "loading": "Loading…", - "trimmed_loaded": "Trimmed audio loaded", - "toast_exported": "Exported: {{name}}", - "toast_export_failed": "Export failed: {{message}}", - "toast_open_folder_failed": "Could not open folder: {{message}}", "toast_saving": "Saving {{name}}...", "toast_saved": "Saved: {{path}}", "toast_save_error": "Save error: {{message}}", "toast_processing": "Processing {{name}}...", "toast_downloaded": "Downloaded {{name}}", "toast_download_error": "Download error: {{message}}", - "toast_upload_first": "Please click 'Upload & Transcribe' first so the video is processed on the server before saving.", - "toast_save_failed": "Save failed: {{message}}", - "toast_opened": "Opened: {{name}}", - "toast_project_deleted": "Project deleted", - "toast_restored_state": "Restored previous generation state", - "toast_history_deleted": "History item deleted", "toast_flushed": "Flushed — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": " · model unloaded", "toast_flush_failed": "Flush failed: {{message}}", - "toast_project_saved": "Project saved", - "toast_project_created": "Project created" + "toast_project_saved": "Project saved" }, "support": { "tab_support": "Support", @@ -3172,7 +3034,6 @@ "sponsors_empty_desc": "Your logo here", "sponsors_become": "Become a sponsor", "sponsors_become_aria": "Become a sponsor — opens a prefilled GitHub issue in your browser", - "sponsors_perk": "Sponsors get their logo in the app, in the README, and a slot on the project site.", "sponsors_learn_more": "What sponsors get", "sponsors_logo_aria": "Visit {{name}}, a VoiceStudio sponsor", "sponsors_tier_platinum": "Platinum", @@ -3225,7 +3086,6 @@ "security_title": "Report a security issue", "security_desc": "Found a vulnerability? Please do not open a public issue. Report it privately through GitHub Security Advisories so it can be fixed before it is disclosed.", "security_cta": "Report privately", - "other_ways": "Other ways to reach me", "email": "Email", "email_desc": "Email — licensing, partnerships, or anything private", "website": "Website", @@ -3264,13 +3124,11 @@ "not_writable": "not writable", "change": "Change…", "compute_title": "Compute", - "compute_label": "GPU / accelerator", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD GPU (ROCm, Linux)", "channel_label": "Update channel", "channel_stable": "Stable", "channel_preview": "Preview (latest main)", - "network_title": "Network", "region_label": "Download region", "mirrors_title": "Custom mirrors (advanced)", "mirror_pypi": "PyPI index URL", @@ -3308,16 +3166,11 @@ "resume_note": "Interrupted downloads resume automatically — closing the app is safe.", "eta_left": "~{{eta}} left", "size_left": "{{size}} left", - "first_sound_text": "Welcome to your studio. Every word you hear was generated on this machine, just now.", - "first_sound_done": "That voice? Generated seconds ago, locally. Welcome in.", - "hf_token_title": "Hugging Face token — faster, more reliable downloads (optional)", - "hf_token_hint": "A free account token gives authenticated downloads (faster, higher rate limits, fewer stalls) and unlocks gated models like speaker diarization for multi-speaker dubbing (pyannote). Stays on this machine.", "hf_token_card_title": "Add a free Hugging Face token for faster downloads", "hf_token_get": "Don't have a token? Get one free →", "hf_token_save": "Save", "hf_token_saving": "saving…", "hf_token_saved": "Hugging Face token saved", - "hf_token_saved_fast": "Hugging Face token saved — downloads are now faster", "hf_token_error": "Could not save the token — try again or set it later in Settings → Credentials.", "hf_token_inline_prompt": "Speed up downloads with a free Hugging Face token", "hf_token_inline_ph": "Paste hf_… token (optional)", @@ -3342,15 +3195,11 @@ "unstar_take": "Unstar — allow cleanup", "play_take": "Load as active output", "filter_starred": "Starred", - "load_take_failed": "Couldn't load that take: {{message}}", "mode_clone": "Clone", "mode_synth": "Synthesis", "generation_seconds": "{{duration}}s", - "expand_text": "Expand text", - "collapse_text": "Collapse text", "seed": "Seed {{seed}}", - "load_config": "Load settings", - "delete_confirm": "Delete this history item?" + "load_config": "Load settings" }, "recording": { "cleaned_loaded": "Recording cleaned & loaded!", @@ -3492,25 +3341,18 @@ "preview": "Sponsor preview", "message": "Your brand, website & message", "email_app": "Open email app", - "copy_email": "Copy email", - "preview_detail": "Your logo, link and introduction could appear here." + "copy_email": "Copy email" }, "supportPlans": { "remove": "Remove sponsor bar", "title": "Free vs Pro", "free": "Free", "pro": "Pro", - "sponsor_bar": "Sponsor bar", "visible": "Visible", - "hideable": "Can be hidden", "unavailable": "Pro activation is not configured yet.", "telemetry": "Telemetry", - "opt_in": "Optional, opt-in", "disabled": "Disabled", - "badge": "Pro badge", - "advanced": "Advanced tools", - "standard": "Standard", - "included": "Included" + "badge": "Pro badge" }, "integrationCatalog": { "title": "Integrations", @@ -3519,8 +3361,7 @@ "n8nHint": "Import this workflow into n8n, edit the request, and run it. Keep VoiceStudio running. Docker and remote n8n need a reachable backend address and any required credentials." }, "directoryExamples": { - "example": "Directory example", - "notice": "Directory examples only — not sponsors or connected integrations." + "example": "Directory example" }, "workflowRun": { "run": "Run workflow", diff --git a/electron/src/shared/i18n/locales/es.json b/electron/src/shared/i18n/locales/es.json index 3b72b91ac..5ede824db 100644 --- a/electron/src/shared/i18n/locales/es.json +++ b/electron/src/shared/i18n/locales/es.json @@ -59,15 +59,10 @@ "title": "Preguntar a VoiceStudio Agent", "notInstalled": "No instalado", "noSource": "Abra una caja fuente VoiceStudio grabable para permitir que un agente la repare.", - "noneFound": "Instale Codex, Claude Code, OpenCode o Pi para utilizar la reparación local.", "ready": "Listo", "placeholder": "¿Qué salió mal? Añade los pasos, los errores visibles o el comportamiento esperado.", - "prReady": "Reparación completada. Revise los cambios y proponga un PR cuando pasen las pruebas específicas.", "diagnose": "Diagnosticar", "fix": "Corregir", - "chooseDefault": "Elige el agente que VoiceStudio usará automáticamente si esta pantalla falla.", - "useAutomatically": "Usar {{agent}} automáticamente", - "captured": "Se capturaron {{count}} líneas de registro con problemas", "contextNotice": "El agente seleccionado recibe este informe, la pantalla actual, los registros recientes de la aplicación y del backend, y los diagnósticos del sistema.", "complete": "Completado" }, @@ -607,7 +602,6 @@ "characters": "{{count}} caracteres", "minutes": "~{{count}} min", "chars": "{{count}} caracteres", - "exportDone": "Audiolibro descargado", "exportFailed": "Falló la exportación: verifique que el backend se esté ejecutando.", "global_speed": "Velocidad", "global_speed_hint": "Velocidad de lectura para todas las líneas sin su propia anulación de velocidad", @@ -818,7 +812,6 @@ "data_dir": "Directorio de datos", "outputs": "Salidas", "crash_log": "Registro de fallos", - "update_endpoint": "Actualizar punto final", "yes": "si", "no": "no", "web_preview": "vista previa web", @@ -879,38 +872,21 @@ "title": "Credenciales", "desc": "Las claves y tokens API están configurados <1>solo para esta sesión. Para lograr persistencia entre reinicios, configúrelos como variables de entorno en su perfil de shell.", "hf_token": "Token de cara de abrazo", - "hf_help": "Requerido para la registro de los oradores y descargas de modelos más rápidas. Consigue el tuyo en huggingface.co/settings/tokens.", - "translate_key": "Clave API de LLM", - "translate_help": "Opcional: para DeepL, OpenAI o proveedores de traducción pagos. No es necesario para Google Translate (nivel gratuito).", "save": "Guardar", "saved": "✓ Establecer", - "not_set": "✗ No establecido", "get_token": "Obtener ficha →", - "saved_session": "{{key}} guardado para esta sesión", "save_failed": "No se pudo guardar", "save_error": "Error al guardar: {{message}}", "deepl_key": "Clave API de DeepL", - "deepl_help": "Para motor de traducción DeepL. Obtenga su clave en deepl.com/pro-api.", "deepl_base_url": "URL base de DeepL", "deepl_base_url_help": "Punto final de API DeepL personalizado (predeterminado: https://api.deepl.com/v2). Cambio por proxy/autohospedado.", "microsoft_key": "Clave del traductor de Microsoft", - "microsoft_help": "Para el motor Microsoft Translator. Obtenga su clave del Portal de Azure.", "microsoft_base_url": "URL base de Microsoft Translator", "microsoft_base_url_help": "Punto final personalizado de Azure Translator (predeterminado: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "URL base del LLM", - "llm_base_url_help": "Para calidad cinematográfica o motor LLM. Cualquier punto final compatible con OpenAI (OpenAI, Ollama, LM Studio, etc.).", - "llm_model": "Modelo de Maestría en Derecho", - "llm_model_help": "Nombre del modelo que se utilizará con el motor LLM (por ejemplo, gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "AbrazosCara", - "group_translate": "Traducción (LLM / Cinemática)", - "group_deepl": "Punto final personalizado de DeepL", - "group_microsoft": "Punto final personalizado de Microsoft Translator", "region": "Región", "aws_profile": "Perfil de AWS" }, "engines": { - "banners_on": "Pancartas del escenario encendidas", - "banners_off": "pancartas del escenario apagadas", "backend": "backend", "status": "Estado", "ready": "listo", @@ -941,7 +917,6 @@ "dictation_model_hint": "Lo usan el dictado en vivo y este motor al transcribir.", "dictation_not_installed": "No instalado: selecciónalo para descargarlo", "active": "activo", - "whyUnavailable": "Qué necesita", "diskDetails": "Detalles de disco", "diskModelDownload": "Descarga del modelo", "diskPackageDownload": "Descarga de paquetes", @@ -965,11 +940,8 @@ "sectionReady": "Listos para usar", "sectionMore": "Añadir más motores", "lastError": "Último error: {{error}}", - "installedAndReady": "Instalado y listo", - "notInstalled": "No instalado", "available": "Disponible", "subprocessTitle": "Se ejecuta en su propio subproceso + venv", - "inProcessTitle": "Se ejecuta en el proceso VoiceStudio Python", "testEngine": "motor de prueba", "testing": "Probando…", "recheck": "Vuelva a comprobar", @@ -986,7 +958,6 @@ "routingUnknown": "Desconocido", "routingEffectiveChip": "Se ejecuta en {{device}} en esta máquina", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU seleccionada, pero: {{reason}}", "selectCpuFallback": "{{engine}}: ejecutándose en la CPU — {{reason}}", "selectWithCaveat": "Se cambió a {{engine}}: {{reason}}", "generateCaveat": "{{engine}} puede ir lento en este equipo: {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "No hay registros de Tauri disponibles. Se ejecuta únicamente en el shell del escritorio.", "empty_backend": "El registro de tiempo de ejecución está vacío. La actividad aparecerá aquí cuando el backend la registre.", "title": "Registros", - "source_backend": "backend", - "source_frontend": "Interfaz", - "source_tauri": "Tauro", "expand": "Expandir registros", "collapse": "Contraer registros", "expand_aria": "Expandir el panel de registros", @@ -1104,12 +1072,10 @@ "unlock": "Desbloquear", "try_voice": "Prueba esta voz", "test_phrase": "frase de prueba", - "test_help": "Escribe cualquier cosa: lo generaremos con la configuración actual de esta voz.", "test_placeholder": "Escribe algo para escuchar esta voz decirlo...", "gen_preview": "Generar vista previa", "generating": "Generando…", "used_title": "Donde se ha usado esta voz", - "used_empty": "Esta voz aún no se ha utilizado. Genere una vista previa arriba o selecciónela en un doblaje.", "synth_clips_one": "{{count}} clip de sintetizador", "synth_clips_other": "{{count}} clips de sintetizador", "projects_count_one": "{{count}} proyecto", @@ -1152,9 +1118,7 @@ "translate": "Traducir", "generate": "generar", "export": "Exportar", - "no_video": "No hay vídeo cargado", "segments": "segmentos", - "speakers": "altavoces", "video_dubbing_studio": "Estudio de doblaje de vídeo", "drop_here": "Suelta vídeo o audio aquí", "paste_url": "…o pegue la URL de YouTube/vídeo", @@ -1168,7 +1132,6 @@ "cookie_size_error": "El archivo de cookies debe tener 1 MB o menos.", "save": "Guardar", "reset": "Reiniciar", - "save_project": "Guardar proyecto", "change_file": "Cambiar archivo", "import_srt": "Importar .srt", "import_srt_alt": "Importe .srt en su lugar", @@ -1186,9 +1149,6 @@ "retranslate": "Retraducir", "transcript": "Transcripción", "generate_dub": "Generar doblaje", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "TER", "upload_transcribe": "Cargar y transcribir", "multi_lang": "Multi-idioma", "prep_download": "Descargando vídeo…", @@ -1206,7 +1166,6 @@ "transcribing": "Transcribiendo audio…", "elapsed": "transcurrido", "remaining": "queda lo que queda", - "sidebar_toggle": "Alternar barra lateral", "retry_transcription": "Reintentar la transcripción", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Cuando la URL es un host que contiene subtítulos (YouTube, Vimeo, TED...), extraiga también los subtítulos originales y las traducciones automáticas de YouTube. Inicia el editor sin ejecutar Whisper; omita Traducir todo para los idiomas que YouTube ya cubre.", @@ -1249,7 +1208,6 @@ "engine_label": "motor", "install_engine": "instalar este motor", "installing_engine": "…instalando", - "needs_dev_install": "necesita instalación de desarrollador", "install_disabled_title": "Las instalaciones están deshabilitadas en compilaciones empaquetadas", "needs_install_suffix": "- necesita instalación", "glossary_title": "Traducciones de pines para términos recurrentes (nombres, palabras de marca, jerga)", @@ -1283,7 +1241,6 @@ "original_track": "Originales", "selected_dub": "{{code}} (Doblaje seleccionado)", "dub_track": "{{code}} (Doblaje)", - "export_tracks": "Exportar pistas:", "stopping": "Parando…", "stop_progress": "Detener ({{current}}/{{total}})", "regen_changed": "Regeneración {{count}} cambiada", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} instalado", "install_failed": "Error de instalación: {{message}}", "prep_elapsed": "{{time}} transcurrido", - "add_language": "Agregar idioma", "search_languages": "Buscar idiomas…", "more_to_narrow": "+{{count}} más — escribe para limitar", "no_matches": "No hay coincidencias", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "tiempo", - "spkr": "spkr", "text": "Texto", "lang": "Lang", "voice": "Voz", @@ -1403,8 +1358,6 @@ "select_all_title": "Seleccionar todo filtrado", "sel_count": "selección {{count}}", "select_title": "Seleccionar segmento (mayús+clic para rango)", - "sync_title": "El audio generado tiene una duración del {{pct}}% del original.", - "sync_label": "Sincronización: {{pct}}%", "rate_title": "Ajuste de la velocidad del habla: {{ratio}}× en relación con la ranura{{error}}", "speaker_id": "Identificación del orador", "translate_error_title": "Error de traducción: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Restaurar texto original", "lang_default": "(Definitivamente)", "voice_default": "Predeterminado", - "from_video": "Del vídeo", - "clone_profiles": "Clonar perfiles", - "design_presets": "Ajustes preestablecidos de diseño", "preview_title": "Vista previa en vivo", "set_direction": "Establecer dirección…", "edit_direction": "Editar dirección…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Personalidad", - "pick_personality": "Elige un ajuste preestablecido de personalidad...", "instruct": "Instruir", "reference_text": "Texto de referencia", "language": "Idioma", @@ -1535,9 +1484,7 @@ "stories": "Historias", "untitled_story": "Historia sin título", "story_lines": "{{count}} línea", - "story_lines_plural": "{{count}} líneas", "story_voices": "{{count}} voz", - "story_voices_plural": "{{count}} voces", "audiobooks": "Audiolibros", "audiobook": "Audiolibro", "story": "historia", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galería", - "search_placeholder": "Buscar en YouTube…", - "all_voices": "Todas las voces ({{count}})", - "no_voices": "Aún no hay voces", "download_hint": "Busque arriba para descargar clips", - "search_btn": "Buscar", - "download_btn": "Descargar", "saving": "Guardando…", "trim": "Recortar", - "delete_confirm": "¿Eliminar este clip de voz?", - "view_mode_list": "Lista", - "view_mode_grid": "Cuadrícula", - "youtube_results": "Resultados de YouTube ({{count}})", - "clone_profile": "Clonar perfil", - "crop_audio": "Recortar audio", "subtitle": "Cientos de voces diseñadas ya preparadas: elige una y listo.", "zone_archetypes": "Arquetipos", "zone_imports": "Mis Importaciones", @@ -1658,8 +1594,6 @@ "delete": "Eliminar", "community_empty": "Aún no se han cargado voces de la comunidad: conéctate a internet y vuelve a abrir, o sé el primero en enviar una.", "community_explainer": "Preajustes diseñados y voces grabadas compartidos por la comunidad, cargados desde la omnivoice-gallery.", - "no_designer": "Voz grabada — usa \"Usar voz\" en su lugar.", - "no_preview": "Sin vista previa — añádela con \"Usar voz\" para escucharla.", "submit_failed": "No se pudo abrir el formulario de envío.", "submit_preset": "Enviar un preajuste", "submit_voice": "Enviar una voz", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Ya puedes crear tu primera voz. Puedes configurar el dictado ahora o más tarde en Ajustes.", - "welcome": "Bienvenido", "system_check": "Verificación del sistema", "install_models": "Instalar modelos", - "pick_engines": "elegir motores", "system_check_desc": "Sondee RAM, disco, GPU, ffmpeg y red. Los bloqueadores se marcan por adelantado para que sepas antes de descargarlos.", "install_models_desc": "Descargue ~2,4 GB de pesos: solo se requiere el modelo TTS. Añada modelos de transcripción seleccionados para su hardware ahora o más tarde desde Ajustes.", "pick_engines_desc": "Elija backends TTS / ASR / LLM. Los valores predeterminados funcionan de inmediato: personalícelos en cualquier momento en Configuración.", "hero_desc": "Doblaje, clonación de voz y diseño de voz: todo ejecutándose localmente en su máquina.", - "first_run": "La primera ejecución tarda entre 5 y 10 minutos en descargarse. Después de eso, cada lanzamiento es instantáneo y completamente fuera de línea.", - "get_started": "empezar", "back": "Atrás", "continue_ok": "Todo bien - continúa", "continue_warn": "Continuar (con advertencias)", @@ -1727,7 +1657,6 @@ "try_dictation": "Prueba el dictado", "try_dictation_desc": "Pulsa tu atajo una vez para comprobar que el dictado funciona antes de terminar la configuración.", "dictation_step_label": "Dictado", - "next_try_dictation": "Siguiente: Prueba el dictado", "step_aria": "Paso {{num}}: {{label}}", "step_completed": "completado", "cache_label": "Caché de modelo" @@ -1743,7 +1672,6 @@ "platforms": "Plataformas", "footer": "Cada contribución ayuda a ampliar los límites de la IA local.", "back": "Volver al estudio", - "commercial": "Licencia Comercial", "pill": "Soporte VoiceStudio", "suggested_title": "Elige una cantidad", "most_common": "más común", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Apoya el desarrollo de VoiceStudio", - "lead_default": "¡Me alegro de que haya funcionado! VoiceStudio es gratuito y totalmente local. Si le ahorra tiempo, un pequeño aporte mensual financia el Claude Max detrás de él.", - "lead_first_clone": "Tu primer clon de voz está listo, ¡bien! VoiceStudio se ejecuta completamente en su máquina y su soporte lo mantiene así.", - "lead_tenth_dub": "Diez doblajes: claramente lo estás poniendo a funcionar. Un pequeño aporte mensual financia el Claude Max que incluye estas funciones.", - "lead_sustained": "Llevas un mes en VoiceStudio. Si se ganó un lugar en su flujo de trabajo, considere ayudar a financiar lo que sigue.", - "chip_in": "Chip en", - "later": "tal vez más tarde", - "star": "Estrella en GitHub", - "opt_out": "no vuelvas a preguntar", - "dismiss_aria": "Descartar" + "later": "tal vez más tarde" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Licencia Comercial", "hero_title": "Enviar voces de IA en producción", "hero_desc": "VoiceStudio es software libre y de código abierto bajo la GNU Affero General Public License v3 (AGPL-3.0): gratuito para cualquier uso, incluido el uso comercial y empresarial interno. Solo necesitas una licencia comercial si quieres integrar VoiceStudio en un producto o servicio propietario o de código cerrado sin las obligaciones copyleft de la AGPL-3.0.", - "why_title": "Por qué las empresas eligen VoiceStudio", - "pricing_title": "Precios", - "faq_title": "Preguntas comunes", "pricing_desc": "Los niveles y los precios aún se están ultimando.", - "pricing_detail": "Hasta que sean públicas, cada implementación comercial se cotiza individualmente para que podamos adaptarla al tamaño adecuado para su equipo y carga de trabajo.", "request_quote": "Solicitar una cotización", - "footer_email": "¿Preguntas? Comuníquese con hi@voicestudio.sh", - "footer_discord": "Únase a nuestro Discord para obtener apoyo de la comunidad.", "benefit_ip": "Propiedad total de propiedad intelectual", "benefit_ip_desc": "Eres dueño de cada producto, cada modelo, cada derivado. Sin arrendamiento compartido, sin fugas de datos.", "benefit_cost": "Costos cero por minuto", "benefit_cost_desc": "Sin facturas de API, sin excedentes sorpresa. Se ejecuta en su propio hardware desde el primer día.", - "benefit_team": "Acceso para todo el equipo", "benefit_team_desc": "Una licencia cubre usuarios ilimitados dentro de su organización.", "benefit_support": "Soporte directo", "benefit_support_desc": "Soporte prioritario por correo electrónico y videollamada del equipo central de ingeniería.", - "benefit_source": "Núcleo disponible en la fuente", "benefit_source_desc": "Visibilidad total de la pila. Audite, bifurque y adapte dentro de los términos de la licencia.", - "benefit_lang": "646 idiomas", "benefit_lang_desc": "Transcribe, traduce y dobla en 646 idiomas con calidad de nivel humano.", - "hero_note": "El uso, el autoalojamiento y el uso comercial son gratuitos bajo la AGPL-3.0, incluso a gran escala. La AGPL es una licencia copyleft de red: si modificas VoiceStudio y ofreces esa versión modificada a terceros a través de una red, debes compartir tu código fuente modificado bajo los mismos términos. Una licencia comercial elimina esas obligaciones copyleft para despliegues propietarios de código cerrado. Los planes de precios llegarán pronto; mientras tanto, contáctanos.", - "hero_simple": "VoiceStudio es gratuito y de código abierto según AGPL-3.0, incluso para uso comercial y empresarial interno. Sólo necesita una licencia comercial para integrarlo en un producto de código cerrado sin las obligaciones de copyleft de AGPL.", - "contact_lead": "El precio se cotiza por implementación para que se ajuste a su equipo y carga de trabajo. Cuénteme su caso de uso y le conseguiré una cotización." + "hero_simple": "VoiceStudio es gratuito y de código abierto según AGPL-3.0, incluso para uso comercial y empresarial interno. Sólo necesita una licencia comercial para integrarlo en un producto de código cerrado sin las obligaciones de copyleft de AGPL." }, "exportModal": { "export": "Exportar", @@ -2006,7 +1915,6 @@ "loaded_ready": "Cargado y listo", "loading_first_run": "Cargando... (esto puede tardar entre 1 y 2 minutos en la primera ejecución)", "failed_to_load": "No se pudo cargar", - "not_loaded_yet": "Aún no cargado: se cargará en la primera transcripción", "tts_not_loaded_yet": "Aún no está cargado; se cargará al generar voz por primera vez", "error_check_logs": "Error: {{error}}. Verifique los registros e intente reiniciar.", "check_logs_restart": "Verifique los registros para detectar errores de carga del modelo. Intenta reiniciar.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Casting de oradores", - "auto_assign_title": "Asignar automáticamente voces a partir de clones de altavoces extraídos", - "auto_cast": "emisión automática", - "all_cast": "Todo el elenco", - "segments_count": "{{count}} segmentos", - "assign_voice": "Asignar voz…", - "from_video": "🎤 Del vídeo ({{name}})", - "no_profiles": "Aún no se han guardado perfiles de voz.", - "preview_voice": "Vista previa de voz" + "segments_count": "{{count}} segmentos" }, "checkpoint": { "timing_review": "{{count}} segmentos necesitan revisión de tiempos", @@ -2203,8 +2104,6 @@ "play_failed": "Error al reproducir: {{message}}" }, "models": { - "hf_token_set_toast": "Conjunto de tokens HuggingFace: descargas más rápidas habilitadas", - "hf_token_save_failed": "No se pudo guardar el token", "install_started": "Instalación iniciada: progreso en la fila", "delete_confirm": "¿Eliminar {{repoId}}? Puedes reinstalarlo más tarde.", "delete_confirm_title": "Eliminar modelo", @@ -2247,8 +2146,6 @@ "not_installed": "no instalado", "required_tag": "requerido", "delete_btn": "Eliminar", - "hf_token_btn": "Ficha HF", - "hf_set_title": "Configure el token HuggingFace para descargas más rápidas", "get_token": "Obtener token →", "reco_installed_for": "Paquete recomendado instalado para {{device}}", "reco_for": "Recomendado para {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Requerido ~{{size}} GB", "all_size": "Todo ~{{size}} GB", "req_tag": "req", - "search_placeholder": "Buscar modelos…", - "search_label": "Buscar modelos", "no_matches": "Ningún modelo coincide con tus filtros.", "sort_by": "Ordenar por {{column}}", "column_model": "modelo", @@ -2267,12 +2162,8 @@ "ready_badge": "Listo", "loading_badge": "Cargando…", "idle_badge": "inactivo", - "started_downloading_required_one": "Comenzó a descargar {{count}} modelo requerido", - "started_downloading_required_other": "Comenzó a descargar {{count}} modelos requeridos", "to_download": "para descargar", "cached": "almacenado en caché", - "fast_download_badge": "descarga rápida", - "fast_download_title": "Descargas rápidas a través de Xet {{version}}: transferencia fragmentada paralela", "voice_previews": "Vistas previas de voz", "voice_previews_desc": "Descarga vistas previas renderizadas de antemano para que las voces suenen al instante, incluso antes de que termine de descargarse un modelo de voz. No se descarga nada hasta que actives esta opción.", "voice_previews_off": "Desactivado: las vistas previas se generan en este equipo", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "¿Necesito una licencia para herramientas internas?", - "a_internal_tools": "No. El uso por parte de tus empleados y contratistas —incluida la modificación y el autoalojamiento interno— es gratuito bajo la AGPL-3.0. Solo necesitas una licencia comercial si integras VoiceStudio en un producto o servicio propietario o de código cerrado y no quieres cumplir las obligaciones de compartir el código fuente de la AGPL.", - "q_try_before": "¿Puedo intentarlo antes de comprometerme?", - "a_try_before": "Sí. La aplicación completa se puede descargar, ejecutar y autoalojar gratis bajo la AGPL-3.0, sin ningún acuerdo. Cuando quieras hablar de una licencia comercial (uso propietario), escríbenos y resolveremos los detalles juntos.", - "q_watermark": "¿Qué pasa con la marca de agua?", - "a_watermark": "La marca de agua invisible AudioSeal se incrusta de forma predeterminada para todos. Puede desactivarla en Ajustes → Privacidad; solo afecta al audio generado después del cambio." - }, "gallery_extra": { - "save_prompt": "Ingrese un nombre para este perfil de voz:", - "saved_as_profile": "¡Voz guardada como perfil!", - "save_profile_failed": "No se pudo guardar el perfil", - "download_failed": "Error de descarga: {{message}}", - "trim_load_failed": "No se pudo cargar el audio para recortar: {{message}}", - "upload_crop_failed": "No se pudo cargar la voz recortada: {{message}}" + "trim_load_failed": "No se pudo cargar el audio para recortar: {{message}}" }, "dub_workflow": { "preparing_audio": "Preparando audio…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Cargando…", - "trimmed_loaded": "Audio recortado cargado", - "toast_exported": "Exportado: {{name}}", - "toast_export_failed": "Error al exportar: {{message}}", - "toast_open_folder_failed": "No se pudo abrir la carpeta: {{message}}", "toast_saving": "Guardando {{name}}...", "toast_saved": "Guardado: {{path}}", "toast_save_error": "Error al guardar: {{message}}", "toast_processing": "Procesando {{name}}...", "toast_downloaded": "Descargado {{name}}", "toast_download_error": "Error de descarga: {{message}}", - "toast_upload_first": "Primero haga clic en \"Cargar y transcribir\" para que el video se procese en el servidor antes de guardarlo.", - "toast_save_failed": "Error al guardar: {{message}}", - "toast_opened": "Abierto: {{name}}", - "toast_project_deleted": "Proyecto eliminado", - "toast_restored_state": "Estado de generación anterior restaurado", - "toast_history_deleted": "Elemento del historial eliminado", "toast_flushed": "Vaciado — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· modelo descargado", "toast_flush_failed": "Error de descarga: {{message}}", - "toast_project_saved": "Proyecto guardado", - "toast_project_created": "Proyecto creado" + "toast_project_saved": "Proyecto guardado" }, "update": { "available": "Actualización {{version}} disponible", @@ -2531,13 +2398,11 @@ "not_writable": "no escribible", "change": "Cambiar…", "compute_title": "Cómputo", - "compute_label": "GPU / acelerador", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Canal de actualizaciones", "channel_stable": "Estable", "channel_preview": "Preview (último main)", - "network_title": "Red", "region_label": "Región de descarga", "mirrors_title": "Mirrors personalizados (avanzado)", "mirror_pypi": "URL del índice PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "La privacidad primero: tus voces, grabaciones y proyectos nunca salen de esta máquina; sin cuenta y sin procesamiento en la nube.", "resume_note": "Las descargas interrumpidas se reanudan solas; cerrar la app es seguro.", "eta_left": "quedan ~{{eta}}", - "first_sound_text": "Bienvenido a tu estudio. Cada palabra que oyes se generó en esta máquina, ahora mismo.", - "first_sound_done": "¿Esa voz? Generada hace segundos, en local. Bienvenido.", - "hf_token_title": "Token de Hugging Face (opcional)", - "hf_token_hint": "Desbloquea modelos restringidos — p. ej. diarización de hablantes para doblaje multivoz (pyannote). Token de cuenta gratuita; se queda en esta máquina.", "hf_token_save": "Guardar", "hf_token_saving": "guardando…", "hf_token_saved": "Token de Hugging Face guardado", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} izquierda", "hf_token_card_title": "Agregue un token de Hugging Face gratuito para descargas más rápidas", "hf_token_get": "¿No tienes una ficha? Consigue uno gratis →", - "hf_token_saved_fast": "Token de Hugging Face guardado: las descargas ahora son más rápidas", "hf_token_inline_prompt": "Acelere las descargas con un token Hugging Face gratuito", "hf_token_inline_ph": "Pegue el token hf_… (opcional)", "hf_token_get_short": "Consigue uno gratis →", @@ -2607,15 +2467,11 @@ "unstar_take": "Quitar estrella — permitir limpieza", "play_take": "Cargar como salida activa", "filter_starred": "Destacadas", - "load_take_failed": "No se pudo cargar esa toma: {{message}}", "mode_clone": "Clonación", "mode_synth": "Síntesis", "generation_seconds": "{{duration}} s", - "expand_text": "Expandir texto", - "collapse_text": "Contraer texto", "seed": "Semilla {{seed}}", - "load_config": "Cargar ajustes", - "delete_confirm": "¿Eliminar este elemento del historial?" + "load_config": "Cargar ajustes" }, "openapi": { "title": "API de VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Vista previa del patrocinador", "message": "Tu marca, sitio web y mensaje", "email_app": "Abrir correo", - "copy_email": "Copiar correo", - "preview_detail": "Aquí podrían aparecer tu logo, enlace y presentación." + "copy_email": "Copiar correo" }, "supportPlans": { "remove": "Quitar barra de patrocinadores", "title": "Gratis vs Pro", "free": "Gratis", "pro": "Pro", - "sponsor_bar": "Barra de patrocinadores", "visible": "Visible", - "hideable": "Se puede ocultar", "unavailable": "La activación de Pro aún no está configurada.", "telemetry": "Telemetría", - "opt_in": "Opcional, con consentimiento", "disabled": "Desactivada", - "badge": "Insignia Pro", - "advanced": "Herramientas avanzadas", - "standard": "Estándar", - "included": "Incluidas" + "badge": "Insignia Pro" }, "integrationCatalog": { "title": "Integraciones", @@ -3042,8 +2891,7 @@ "n8nHint": "Importa este flujo en n8n, edita la solicitud y ejecútalo. Mantén VoiceStudio abierto. Docker y n8n remoto necesitan una dirección de backend accesible y las credenciales requeridas." }, "directoryExamples": { - "example": "Ejemplo del directorio", - "notice": "Solo ejemplos del directorio; no son patrocinadores ni integraciones conectadas." + "example": "Ejemplo del directorio" }, "workflowRun": { "run": "Ejecutar flujo", diff --git a/electron/src/shared/i18n/locales/fr.json b/electron/src/shared/i18n/locales/fr.json index b4e410dfd..6aaefddd0 100644 --- a/electron/src/shared/i18n/locales/fr.json +++ b/electron/src/shared/i18n/locales/fr.json @@ -59,15 +59,10 @@ "title": "Demander à VoiceStudio Agent", "notInstalled": "Non installé", "noSource": "Ouvrez une extraction de source VoiceStudio inscriptible pour permettre à un agent de la réparer.", - "noneFound": "Installez Codex, Claude Code, OpenCode ou Pi pour utiliser la réparation locale.", "ready": "Prêt", "placeholder": "Que s’est-il passé ? Ajoutez les étapes, les erreurs visibles ou le comportement attendu.", - "prReady": "Correction terminée. Examinez les changements et proposez un PR lorsque les tests ciblés réussissent.", "diagnose": "Diagnostiquer", "fix": "Réparer", - "chooseDefault": "Choisissez l’agent que VoiceStudio utilisera automatiquement si cet écran plante.", - "useAutomatically": "Utiliser {{agent}} automatiquement", - "captured": "{{count}} lignes de journal problématiques capturées", "contextNotice": "L’agent sélectionné reçoit ce rapport, l’écran actuel, les journaux récents de l’application et du backend, ainsi que les diagnostics système.", "complete": "Complet" }, @@ -607,7 +602,6 @@ "characters": "{{count}} caractères", "minutes": "~{{count}} min", "chars": "{{count}} caractères", - "exportDone": "Livre audio téléchargé", "exportFailed": "Échec de l'exportation : vérifiez que le backend est en cours d'exécution.", "global_speed": "Vitesse", "global_speed_hint": "Vitesse de lecture pour toutes les lignes sans leur propre priorité de vitesse", @@ -818,7 +812,6 @@ "data_dir": "Répertoire de données", "outputs": "Sorties", "crash_log": "Journal des incidents", - "update_endpoint": "Mettre à jour le point de terminaison", "yes": "oui", "no": "non", "web_preview": "aperçu Web", @@ -879,38 +872,21 @@ "title": "Informations d'identification", "desc": "Les clés et jetons API sont définis <1>pour cette session uniquement. Pour la persistance après les redémarrages, définissez-les comme variables d'environnement dans votre profil shell.", "hf_token": "Jeton de visage câlin", - "hf_help": "Nécessaire pour la diarisation des enceintes et les téléchargements de modèles plus rapides. Obtenez le vôtre sur huggingface.co/settings/tokens.", - "translate_key": "Clé API LLM", - "translate_help": "Facultatif : pour DeepL, OpenAI ou les fournisseurs de traduction payants. Non nécessaire pour Google Translate (niveau gratuit).", "save": "Enregistrer", "saved": "✓ Définir", - "not_set": "✗ Non défini", "get_token": "Obtenir un jeton →", - "saved_session": "{{key}} enregistré pour cette session", "save_failed": "Échec de l'enregistrement", "save_error": "Échec de l'enregistrement : {{message}}", "deepl_key": "Clé API DeepL", - "deepl_help": "Pour le moteur de traduction DeepL. Obtenez votre clé sur deepl.com/pro-api.", "deepl_base_url": "URL de base DeepL", "deepl_base_url_help": "Point de terminaison de l'API DeepL personnalisé (par défaut : https://api.deepl.com/v2). Changer pour proxy/auto-hébergé.", "microsoft_key": "Clé du traducteur Microsoft", - "microsoft_help": "Pour le moteur Microsoft Translator. Obtenez votre clé sur le portail Azure.", "microsoft_base_url": "URL de base du traducteur Microsoft", "microsoft_base_url_help": "Point de terminaison Azure Translator personnalisé (par défaut : https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "URL de base LLM", - "llm_base_url_help": "Pour une qualité cinématique ou un moteur LLM. Tout point de terminaison compatible OpenAI (OpenAI, Ollama, LM Studio, etc.).", - "llm_model": "Modèle LLM", - "llm_model_help": "Nom du modèle à utiliser avec le moteur LLM (par exemple gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "ÉtreindreVisage", - "group_translate": "Traduction (LLM / Cinématique)", - "group_deepl": "Point de terminaison personnalisé DeepL", - "group_microsoft": "Point de terminaison personnalisé du traducteur Microsoft", "region": "Région", "aws_profile": "Profil AWS" }, "engines": { - "banners_on": "Bannières de scène sur", - "banners_off": "Les bannières de la scène sont retirées", "backend": "Back-end", "status": "Statut", "ready": "prêt", @@ -941,7 +917,6 @@ "dictation_model_hint": "Utilisé par la dictée en direct et par ce moteur pour la transcription.", "dictation_not_installed": "Non installé — sélectionnez pour télécharger", "active": "actif", - "whyUnavailable": "Ce qu'il lui faut", "diskDetails": "Détails du disque", "diskModelDownload": "Téléchargement du modèle", "diskPackageDownload": "Téléchargement des paquets", @@ -965,11 +940,8 @@ "sectionReady": "Prêts à l'emploi", "sectionMore": "Ajouter d'autres moteurs", "lastError": "Dernière erreur : {{error}}", - "installedAndReady": "Installé et prêt", - "notInstalled": "Non installé", "available": "Disponible", "subprocessTitle": "S'exécute dans son propre sous-processus + venv", - "inProcessTitle": "S'exécute dans le processus VoiceStudio Python", "testEngine": "Moteur de test", "testing": "Test…", "recheck": "Revérifier", @@ -986,7 +958,6 @@ "routingUnknown": "Inconnu", "routingEffectiveChip": "Fonctionne le {{device}} sur cette machine", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU sélectionné, mais : {{reason}}", "selectCpuFallback": "{{engine}} : fonctionne sur le processeur — {{reason}}", "selectWithCaveat": "Basculé vers {{engine}} — {{reason}}", "generateCaveat": "{{engine}} risque d'être lent sur cette machine — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Aucun journal Tauri disponible. Fonctionne uniquement dans le shell du bureau.", "empty_backend": "Le journal d'exécution est vide. L'activité apparaîtra ici au fur et à mesure que le backend l'enregistrera.", "title": "Journaux", - "source_backend": "Back-end", - "source_frontend": "Front-end", - "source_tauri": "Taureau", "expand": "Développer les journaux", "collapse": "Réduire les journaux", "expand_aria": "Développer le panneau des journaux", @@ -1104,12 +1072,10 @@ "unlock": "Déverrouiller", "try_voice": "Essayez cette voix", "test_phrase": "Phrase de test", - "test_help": "Tapez n'importe quoi - nous le générerons avec les paramètres actuels de cette voix.", "test_placeholder": "Tapez quelque chose pour entendre cette voix le dire…", "gen_preview": "Générer un aperçu", "generating": "Générer…", "used_title": "Où cette voix a été utilisée", - "used_empty": "Cette voix n'a pas encore été utilisée. Générez un aperçu ci-dessus ou sélectionnez-le dans un doublage.", "synth_clips_one": "{{count}} clip de synthétiseur", "synth_clips_other": "{{count}} extraits de synthétiseur", "projects_count_one": "Projet {{count}}", @@ -1152,9 +1118,7 @@ "translate": "Traduire", "generate": "Générer", "export": "Exporter", - "no_video": "Aucune vidéo chargée", "segments": "segments", - "speakers": "haut-parleurs", "video_dubbing_studio": "Studio de doublage vidéo", "drop_here": "Déposez la vidéo ou l'audio ici", "paste_url": "…ou collez l’URL YouTube/vidéo", @@ -1168,7 +1132,6 @@ "cookie_size_error": "L’export de cookies doit faire 1 Mo maximum.", "save": "Enregistrer", "reset": "Réinitialiser", - "save_project": "Enregistrer le projet", "change_file": "Changer de fichier", "import_srt": "Importer .srt", "import_srt_alt": "Importez plutôt .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Retraduire", "transcript": "Transcription", "generate_dub": "Générer un doublage", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Télécharger et transcrire", "multi_lang": "Multilingue", "prep_download": "Téléchargement de la vidéo…", @@ -1206,7 +1166,6 @@ "transcribing": "Transcription audio…", "elapsed": "écoulé", "remaining": "il reste", - "sidebar_toggle": "Basculer la barre latérale", "retry_transcription": "Réessayer la transcription", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Lorsque l'URL est un hôte portant des sous-titres (YouTube, Vimeo, TED…), extrayez également les sous-titres originaux et toutes les traductions automatiques YouTube. Lance l'éditeur sans exécuter Whisper ; ignorez Tout traduire pour les langues déjà couvertes par YouTube.", @@ -1249,7 +1208,6 @@ "engine_label": "Moteur", "install_engine": "Installer ce moteur", "installing_engine": "…installation", - "needs_dev_install": "a besoin d'une installation de développement", "install_disabled_title": "Les installations sont désactivées dans les versions packagées", "needs_install_suffix": "- doit être installé", "glossary_title": "Épinglez les traductions des termes récurrents (noms, mots de marque, jargon)", @@ -1283,7 +1241,6 @@ "original_track": "Originale", "selected_dub": "{{code}} (doublage sélectionné)", "dub_track": "{{code}} (doublage)", - "export_tracks": "Exporter les pistes :", "stopping": "Arrêter…", "stop_progress": "Arrêter ({{current}}/{{total}})", "regen_changed": "Régénération {{count}} modifiée", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} installé", "install_failed": "Échec de l'installation : {{message}}", "prep_elapsed": "{{time}} écoulé", - "add_language": "Ajouter une langue", "search_languages": "Rechercher des langues…", "more_to_narrow": "+{{count}} plus — tapez pour affiner", "no_matches": "Aucune correspondance", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Temps", - "spkr": "Spkr", "text": "Texte", "lang": "Lang", "voice": "Voix", @@ -1403,8 +1358,6 @@ "select_all_title": "Sélectionner tout filtré", "sel_count": "{{count}} sel", "select_title": "Sélectionnez un segment (maj+clic pour la plage)", - "sync_title": "L'audio généré correspond à {{pct}} % de la durée de l'original.", - "sync_label": "Synchronisation : {{pct}} %", "rate_title": "Ajustement du débit vocal : {{ratio}}× par rapport à l'emplacement{{error}}", "speaker_id": "ID du haut-parleur", "translate_error_title": "Erreur de traduction : {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Restaurer le texte original", "lang_default": "(Déf)", "voice_default": "Par défaut", - "from_video": "À partir de la vidéo", - "clone_profiles": "Cloner des profils", - "design_presets": "Préréglages de conception", "preview_title": "Aperçu en direct", "set_direction": "Définir la direction…", "edit_direction": "Modifier la direction…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Personnalité", - "pick_personality": "Choisissez un préréglage de personnalité…", "instruct": "Instruire", "reference_text": "Texte de référence", "language": "Langue", @@ -1535,9 +1484,7 @@ "stories": "Histoires", "untitled_story": "Histoire sans titre", "story_lines": "Ligne {{count}}", - "story_lines_plural": "{{count}} lignes", "story_voices": "{{count}} voix", - "story_voices_plural": "{{count}} voix", "audiobooks": "Livres audio", "audiobook": "Livre audio", "story": "Histoire", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galerie", - "search_placeholder": "Rechercher sur YouTube…", - "all_voices": "Toutes les voix ({{count}})", - "no_voices": "Pas encore de voix", "download_hint": "Recherchez ci-dessus pour télécharger des clips", - "search_btn": "Rechercher", - "download_btn": "Télécharger", "saving": "Sauvegarde…", "trim": "Couper", - "delete_confirm": "Supprimer ce clip vocal ?", - "view_mode_list": "Liste", - "view_mode_grid": "Grille", - "youtube_results": "Résultats YouTube ({{count}})", - "clone_profile": "Cloner le profil", - "crop_audio": "Recadrer l'audio", "subtitle": "Des centaines de voix prêtes à l'emploi : choisissez-en une et c'est parti.", "zone_archetypes": "Archétypes", "zone_imports": "Mes importations", @@ -1658,8 +1594,6 @@ "delete": "Supprimer", "community_empty": "Aucune voix communautaire chargée pour l'instant — connectez-vous à internet et rouvrez, ou soyez le premier à en soumettre une.", "community_explainer": "Préréglages conçus et voix enregistrées partagés par la communauté, chargés depuis l'omnivoice-gallery.", - "no_designer": "Voix enregistrée — utilisez plutôt « Utiliser la voix ».", - "no_preview": "Aucun aperçu — ajoutez-la avec « Utiliser la voix » pour l'entendre.", "submit_failed": "Impossible d'ouvrir le formulaire de soumission.", "submit_preset": "Soumettre un préréglage", "submit_voice": "Soumettre une voix", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Vous pouvez créer votre première voix. Configurez la dictée maintenant ou plus tard dans les paramètres.", - "welcome": "Bienvenue", "system_check": "Vérification du système", "install_models": "Installer des modèles", - "pick_engines": "Choisir des moteurs", "system_check_desc": "Sondez la RAM, le disque, le GPU, ffmpeg et le réseau. Les bloqueurs sont signalés à l’avance afin que vous le sachiez avant de télécharger.", "install_models_desc": "Téléchargez ~2,4 Go de poids — seul le modèle TTS est requis. Ajoutez des modèles de transcription adaptés à votre matériel maintenant, ou plus tard depuis les Réglages.", "pick_engines_desc": "Choisissez les backends TTS / ASR / LLM. Les valeurs par défaut fonctionnent immédiatement : personnalisez-les à tout moment dans les paramètres.", "hero_desc": "Doublage, clonage vocal et conception vocale, le tout exécuté localement sur votre ordinateur.", - "first_run": "La première exécution prend 5 à 10 minutes pour le téléchargement. Après cela, chaque lancement est instantané et entièrement hors ligne.", - "get_started": "Commencez", "back": "Retour", "continue_ok": "Tout va bien - continuez", "continue_warn": "Continuer (avec avertissements)", @@ -1727,7 +1657,6 @@ "try_dictation": "Essayez la dictée", "try_dictation_desc": "Appuyez une fois sur votre raccourci pour vérifier que la dictée fonctionne avant de terminer la configuration.", "dictation_step_label": "Dictée", - "next_try_dictation": "Suivant : Essayez la dictée", "step_aria": "Étape {{num}} : {{label}}", "step_completed": "terminée", "cache_label": "Cache modèle" @@ -1743,7 +1672,6 @@ "platforms": "Plateformes", "footer": "Chaque contribution contribue à repousser les limites de l’IA locale.", "back": "Retour à l'Atelier", - "commercial": "Licence commerciale", "pill": "Prise en charge d'VoiceStudio", "suggested_title": "Choisissez un montant", "most_common": "le plus courant", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Soutenez le développement de VoiceStudio", - "lead_default": "Content que ça ait fonctionné ! VoiceStudio est gratuit et entièrement local. Si cela vous fait gagner du temps, une petite contribution mensuelle finance le Claude Max derrière.", - "lead_first_clone": "Votre premier clone de voix est terminé – super ! VoiceStudio fonctionne entièrement sur votre ordinateur et votre assistance le maintient ainsi.", - "lead_tenth_dub": "Dix doublages – vous le mettez clairement à profit. Une petite contribution mensuelle finance le Claude Max qui fournit ces fonctionnalités.", - "lead_sustained": "Vous travaillez chez VoiceStudio depuis un mois. Si cela a gagné une place dans votre flux de travail, envisagez de contribuer à financer la suite.", - "chip_in": "Participez", - "later": "Peut-être plus tard", - "star": "Étoile sur GitHub", - "opt_out": "Ne demande plus", - "dismiss_aria": "Rejeter" + "later": "Peut-être plus tard" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Licence commerciale", "hero_title": "Expédier les voix de l'IA en production", "hero_desc": "VoiceStudio est un logiciel libre et open source sous licence GNU Affero General Public License v3 (AGPL-3.0) — gratuit pour tout usage, y compris commercial et interne en entreprise. Une licence commerciale n'est nécessaire que si vous souhaitez intégrer VoiceStudio dans un produit ou service propriétaire ou à code fermé, sans les obligations copyleft de l'AGPL-3.0.", - "why_title": "Pourquoi les entreprises choisissent VoiceStudio", - "pricing_title": "Tarifs", - "faq_title": "Questions courantes", "pricing_desc": "Les niveaux et les tarifs sont encore en cours de finalisation.", - "pricing_detail": "Jusqu'à ce qu'ils soient publics, chaque déploiement commercial est proposé individuellement afin que nous puissions l'adapter à votre équipe et à votre charge de travail.", "request_quote": "Demander un devis", - "footer_email": "Des questions ? Contactez hi@voicestudio.sh", - "footer_discord": "Rejoignez notre Discord pour le soutien de la communauté.", "benefit_ip": "Propriété IP complète", "benefit_ip_desc": "Vous possédez chaque sortie, chaque modèle, chaque dérivé. Pas de location partagée, pas de fuite de données.", "benefit_cost": "Zéro coût par minute", "benefit_cost_desc": "Pas de factures API, pas de dépassements surprises. Fonctionne sur votre propre matériel dès le premier jour.", - "benefit_team": "Accès à l’échelle de l’équipe", "benefit_team_desc": "Une licence couvre un nombre illimité d'utilisateurs au sein de votre organisation.", "benefit_support": "Assistance directe", "benefit_support_desc": "Assistance prioritaire par e-mail et par appel vidéo de la part de l'équipe d'ingénierie principale.", - "benefit_source": "Noyau disponible à la source", "benefit_source_desc": "Visibilité totale sur la pile. Auditez, forkez et adaptez selon les termes de la licence.", - "benefit_lang": "646 langues", "benefit_lang_desc": "Transcrivez, traduisez et doublez dans 646 langues avec une qualité humaine.", - "hero_note": "L'utilisation, l'auto-hébergement et l'usage commercial sont gratuits sous AGPL-3.0 — y compris à grande échelle. L'AGPL est une licence copyleft réseau : si vous modifiez VoiceStudio et proposez cette version modifiée à des tiers via un réseau, vous devez partager votre code source modifié aux mêmes conditions. Une licence commerciale lève ces obligations copyleft pour les déploiements propriétaires à code fermé. Les tarifs arrivent bientôt — contactez-nous d'ici là.", - "hero_simple": "VoiceStudio est gratuit et open source sous AGPL-3.0, y compris pour un usage commercial et interne. Vous n’avez besoin que d’une licence commerciale pour l’intégrer dans un produit à source fermée sans les obligations de copyleft de l’AGPL.", - "contact_lead": "Le prix est indiqué par déploiement afin de s'adapter à votre équipe et à votre charge de travail. Dites-moi votre cas d'utilisation et je vous ferai un devis." + "hero_simple": "VoiceStudio est gratuit et open source sous AGPL-3.0, y compris pour un usage commercial et interne. Vous n’avez besoin que d’une licence commerciale pour l’intégrer dans un produit à source fermée sans les obligations de copyleft de l’AGPL." }, "exportModal": { "export": "Exporter", @@ -2006,7 +1915,6 @@ "loaded_ready": "Chargé et prêt", "loading_first_run": "Chargement… (cela peut prendre 1 à 2 minutes lors de la première exécution)", "failed_to_load": "Échec du chargement", - "not_loaded_yet": "Pas encore chargé – se chargera lors de la première transcription", "tts_not_loaded_yet": "Pas encore chargé — se chargera lors de la première génération vocale", "error_check_logs": "Erreur : {{error}}. Vérifiez les journaux et essayez de redémarrer.", "check_logs_restart": "Vérifiez les journaux pour détecter les erreurs de chargement du modèle. Essayez de redémarrer.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Casting de haut-parleurs", - "auto_assign_title": "Attribuer automatiquement les voix des clones de haut-parleurs extraits", - "auto_cast": "Diffusion automatique", - "all_cast": "Tous les acteurs", - "segments_count": "{{count}} segments", - "assign_voice": "Attribuer une voix…", - "from_video": "🎤 À partir de la vidéo ({{name}})", - "no_profiles": "Aucun profil vocal n'a encore été enregistré.", - "preview_voice": "Aperçu de la voix" + "segments_count": "{{count}} segments" }, "checkpoint": { "timing_review": "{{count}} segments nécessitent une vérification du minutage", @@ -2203,8 +2104,6 @@ "play_failed": "Échec de la lecture : {{message}}" }, "models": { - "hf_token_set_toast": "Ensemble de jetons HuggingFace – téléchargements plus rapides activés", - "hf_token_save_failed": "Échec de l'enregistrement du jeton", "install_started": "Installation démarrée – progression dans la ligne", "delete_confirm": "Supprimer {{repoId}} ? Vous pourrez le réinstaller plus tard.", "delete_confirm_title": "Supprimer le modèle", @@ -2247,8 +2146,6 @@ "not_installed": "non installé", "required_tag": "requis", "delete_btn": "Supprimer", - "hf_token_btn": "Jeton HF", - "hf_set_title": "Définir le jeton HuggingFace pour des téléchargements plus rapides", "get_token": "Obtenir un jeton →", "reco_installed_for": "Offre groupée recommandée installée pour {{device}}", "reco_for": "Recommandé pour {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Requis ~{{size}} Go", "all_size": "Tous ~{{size}} Go", "req_tag": "demande", - "search_placeholder": "Rechercher des modèles…", - "search_label": "Rechercher des modèles", "no_matches": "Aucun modèle ne correspond à vos filtres.", "sort_by": "Trier par {{column}}", "column_model": "Modèle", @@ -2267,12 +2162,8 @@ "ready_badge": "Prêt", "loading_badge": "Chargement…", "idle_badge": "Inactif", - "started_downloading_required_one": "Début du téléchargement du modèle requis {{count}}", - "started_downloading_required_other": "Début du téléchargement des {{count}} modèles requis", "to_download": "télécharger", "cached": "mis en cache", - "fast_download_badge": "téléchargement rapide", - "fast_download_title": "Téléchargements rapides via Xet {{version}} — transfert fragmenté parallèle", "voice_previews": "Aperçus de voix", "voice_previews_desc": "Téléchargez des aperçus pré-rendus pour que les voix se lisent instantanément, même avant la fin du téléchargement d'un modèle vocal. Rien n'est téléchargé tant que vous n'activez pas cette option.", "voice_previews_off": "Désactivé — les aperçus sont générés sur cet appareil", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Ai-je besoin d’une licence pour les outils internes ?", - "a_internal_tools": "Non. L'utilisation par vos employés et prestataires — y compris la modification et l'auto-hébergement en interne — est gratuite sous AGPL-3.0. Une licence commerciale n'est nécessaire que si vous intégrez VoiceStudio dans un produit ou service propriétaire ou à code fermé sans vouloir respecter les obligations de partage du code source de l'AGPL.", - "q_try_before": "Puis-je essayer avant de m'engager ?", - "a_try_before": "Oui. L'application complète est gratuite à télécharger, exécuter et auto-héberger sous AGPL-3.0 — aucun accord requis. Quand vous serez prêt à discuter d'une licence commerciale (usage propriétaire), écrivez-nous et nous verrons les détails ensemble.", - "q_watermark": "Et le filigrane ?", - "a_watermark": "Le filigrane invisible AudioSeal est intégré par défaut pour tout le monde. Vous pouvez le désactiver dans Réglages → Confidentialité ; il n'affecte que l'audio généré après la modification." - }, "gallery_extra": { - "save_prompt": "Saisissez un nom pour ce profil vocal :", - "saved_as_profile": "Voix enregistrée en tant que profil !", - "save_profile_failed": "Échec de l'enregistrement du profil", - "download_failed": "Échec du téléchargement : {{message}}", - "trim_load_failed": "Échec du chargement de l'audio à découper : {{message}}", - "upload_crop_failed": "Échec de l'importation de la voix tronquée : {{message}}" + "trim_load_failed": "Échec du chargement de l'audio à découper : {{message}}" }, "dub_workflow": { "preparing_audio": "Préparation du son…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Chargement…", - "trimmed_loaded": "Audio découpé chargé", - "toast_exported": "Exporté : {{name}}", - "toast_export_failed": "Échec de l'exportation : {{message}}", - "toast_open_folder_failed": "Impossible d'ouvrir le dossier : {{message}}", "toast_saving": "Sauvegarde de {{name}}...", "toast_saved": "Enregistré : {{path}}", "toast_save_error": "Erreur d'enregistrement : {{message}}", "toast_processing": "Traitement {{name}}...", "toast_downloaded": "Téléchargé {{name}}", "toast_download_error": "Erreur de téléchargement : {{message}}", - "toast_upload_first": "Veuillez d'abord cliquer sur « Télécharger et transcrire » afin que la vidéo soit traitée sur le serveur avant de l'enregistrer.", - "toast_save_failed": "Échec de l'enregistrement : {{message}}", - "toast_opened": "Ouvert : {{name}}", - "toast_project_deleted": "Projet supprimé", - "toast_restored_state": "État de la génération précédente restauré", - "toast_history_deleted": "Élément d'historique supprimé", "toast_flushed": "Vidé — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· modèle déchargé", "toast_flush_failed": "Échec du rinçage : {{message}}", - "toast_project_saved": "Projet enregistré", - "toast_project_created": "Projet créé" + "toast_project_saved": "Projet enregistré" }, "update": { "available": "Mise à jour {{version}} disponible", @@ -2531,13 +2398,11 @@ "not_writable": "non inscriptible", "change": "Modifier…", "compute_title": "Calcul", - "compute_label": "GPU / accélérateur", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Canal de mise à jour", "channel_stable": "Stable", "channel_preview": "Préversion (dernier main)", - "network_title": "Réseau", "region_label": "Région de téléchargement", "mirrors_title": "Miroirs personnalisés (avancé)", "mirror_pypi": "URL de l'index PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "La confidentialité d’abord : vos voix, enregistrements et projets ne quittent jamais cette machine — sans compte, sans traitement dans le cloud.", "resume_note": "Les téléchargements interrompus reprennent automatiquement — fermer l’application est sans risque.", "eta_left": "~{{eta}} restantes", - "first_sound_text": "Bienvenue dans votre studio. Chaque mot que vous entendez vient d’être généré sur cette machine.", - "first_sound_done": "Cette voix ? Générée il y a quelques secondes, en local. Bienvenue.", - "hf_token_title": "Jeton Hugging Face (optionnel)", - "hf_token_hint": "Débloque les modèles restreints — p. ex. la diarisation pour le doublage multi-locuteurs (pyannote). Jeton de compte gratuit ; reste sur cette machine.", "hf_token_save": "Enregistrer", "hf_token_saving": "enregistrement…", "hf_token_saved": "Jeton Hugging Face enregistré", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} reste", "hf_token_card_title": "Ajoutez un jeton Hugging Face gratuit pour des téléchargements plus rapides", "hf_token_get": "Vous n'avez pas de jeton ? Obtenez-en un gratuitement →", - "hf_token_saved_fast": "Jeton Hugging Face enregistré – les téléchargements sont désormais plus rapides", "hf_token_inline_prompt": "Accélérez les téléchargements avec un jeton Hugging Face gratuit", "hf_token_inline_ph": "Coller le jeton hf_… (facultatif)", "hf_token_get_short": "Obtenez-en un gratuitement →", @@ -2607,15 +2467,11 @@ "unstar_take": "Retirer l'étoile — autoriser le nettoyage", "play_take": "Charger comme sortie active", "filter_starred": "Étoilées", - "load_take_failed": "Impossible de charger cette prise : {{message}}", "mode_clone": "Clonage", "mode_synth": "Synthèse", "generation_seconds": "{{duration}} s", - "expand_text": "Développer le texte", - "collapse_text": "Réduire le texte", "seed": "Graine {{seed}}", - "load_config": "Charger les réglages", - "delete_confirm": "Supprimer cet élément de l’historique ?" + "load_config": "Charger les réglages" }, "openapi": { "title": "API VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Aperçu du sponsor", "message": "Votre marque, site web et message", "email_app": "Ouvrir la messagerie", - "copy_email": "Copier l’adresse e-mail", - "preview_detail": "Votre logo, lien et présentation pourraient apparaître ici." + "copy_email": "Copier l’adresse e-mail" }, "supportPlans": { "remove": "Retirer la barre des sponsors", "title": "Gratuit ou Pro", "free": "Gratuit", "pro": "Pro", - "sponsor_bar": "Barre des sponsors", "visible": "Visible", - "hideable": "Peut être masquée", "unavailable": "L’activation de Pro n’est pas encore configurée.", "telemetry": "Télémétrie", - "opt_in": "Facultative, sur consentement", "disabled": "Désactivée", - "badge": "Badge Pro", - "advanced": "Outils avancés", - "standard": "Standard", - "included": "Inclus" + "badge": "Badge Pro" }, "integrationCatalog": { "title": "Intégrations", @@ -3042,8 +2891,7 @@ "n8nHint": "Importez ce workflow dans n8n, modifiez la requête et exécutez-le. Gardez VoiceStudio ouvert. Docker et les instances n8n distantes nécessitent une adresse de backend accessible et les identifiants requis." }, "directoryExamples": { - "example": "Exemple du répertoire", - "notice": "Exemples uniquement : ni sponsors ni intégrations connectées." + "example": "Exemple du répertoire" }, "workflowRun": { "run": "Exécuter le flux", diff --git a/electron/src/shared/i18n/locales/hi.json b/electron/src/shared/i18n/locales/hi.json index 0b3972a05..fa6b211f3 100644 --- a/electron/src/shared/i18n/locales/hi.json +++ b/electron/src/shared/i18n/locales/hi.json @@ -59,15 +59,10 @@ "title": "VoiceStudio Agent से पूछें", "notInstalled": "इंस्टॉल नहीं है", "noSource": "एजेंट को सुधार करने देने के लिए लिखने योग्य VoiceStudio स्रोत चेकआउट खोलें।", - "noneFound": "स्थानीय सुधार के लिए Codex, Claude Code, OpenCode या Pi इंस्टॉल करें।", "ready": "तैयार", "placeholder": "क्या गलत हुआ? चरण, दिखाई देने वाली त्रुटियाँ या अपेक्षित व्यवहार जोड़ें।", - "prReady": "सुधार पूरा हुआ। बदलावों की समीक्षा करें और लक्षित परीक्षण पास होने पर PR प्रस्तावित करें।", "diagnose": "जाँचें", "fix": "सुधारें", - "chooseDefault": "इस स्क्रीन के क्रैश होने पर VoiceStudio जिस एजेंट का अपने आप उपयोग करे, उसे चुनें।", - "useAutomatically": "{{agent}} का अपने आप उपयोग करें", - "captured": "समस्या वाली {{count}} लॉग पंक्तियाँ कैप्चर की गईं", "contextNotice": "चुने गए एजेंट को यह रिपोर्ट, वर्तमान स्क्रीन, हाल के ऐप और बैकएंड लॉग तथा सिस्टम निदान मिलते हैं।", "complete": "पूरा" }, @@ -607,7 +602,6 @@ "characters": "{{count}} वर्ण", "minutes": "~{{count}} मि", "chars": "{{count}} वर्ण", - "exportDone": "ऑडियोबुक डाउनलोड किया गया", "exportFailed": "निर्यात विफल - जांचें कि बैकएंड चल रहा है।", "global_speed": "गति", "global_speed_hint": "अपनी स्वयं की गति ओवरराइड के बिना सभी लाइनों के लिए पढ़ने की गति", @@ -818,7 +812,6 @@ "data_dir": "डेटा निर्देशिका", "outputs": "आउटपुट", "crash_log": "क्रैश लॉग", - "update_endpoint": "समापन बिंदु अद्यतन करें", "yes": "हाँ", "no": "नहीं", "web_preview": "वेब पूर्वावलोकन", @@ -879,38 +872,21 @@ "title": "साख", "desc": "एपीआई कुंजियाँ और टोकन केवल इस सत्र के लिए <1> सेट हैं। पुनरारंभ के दौरान निरंतरता के लिए, उन्हें अपने शेल प्रोफ़ाइल में पर्यावरण चर के रूप में सेट करें।", "hf_token": "हगिंगफेस टोकन", - "hf_help": "स्पीकर डायराइज़ेशन और तेज़ मॉडल डाउनलोड के लिए आवश्यक है। अपना अपना प्राप्त करेंhuggingface.co/settings/tokens पर।", - "translate_key": "एलएलएम एपीआई कुंजी", - "translate_help": "वैकल्पिक - डीपएल, ओपनएआई, या सशुल्क अनुवाद प्रदाताओं के लिए। Google Translate (निःशुल्क स्तर) के लिए आवश्यक नहीं है।", "save": "सहेजें", "saved": "✓ सेट", - "not_set": "✗ सेट नहीं है", "get_token": "टोकन प्राप्त करें →", - "saved_session": "{{key}} इस सत्र के लिए सहेजा गया", "save_failed": "सहेजने में विफल", "save_error": "सहेजना विफल: {{message}}", "deepl_key": "डीपएल एपीआई कुंजी", - "deepl_help": "डीपएल अनुवाद इंजन के लिए। अपनी कुंजी Deepl.com/pro-api पर प्राप्त करें।", "deepl_base_url": "डीपएल बेस यूआरएल", "deepl_base_url_help": "कस्टम डीपएल एपीआई एंडपॉइंट (डिफ़ॉल्ट: https://api.depl.com/v2)। प्रॉक्सी/स्वयं-होस्टेड के लिए परिवर्तन।", "microsoft_key": "माइक्रोसॉफ्ट अनुवादक कुंजी", - "microsoft_help": "माइक्रोसॉफ्ट ट्रांसलेटर इंजन के लिए. Azure पोर्टल से अपनी कुंजी प्राप्त करें।", "microsoft_base_url": "माइक्रोसॉफ्ट ट्रांसलेटर बेस यूआरएल", "microsoft_base_url_help": "कस्टम एज़्योर ट्रांसलेटर एंडपॉइंट (डिफ़ॉल्ट: https://api.cognitive.microsofttranslator.com)।", - "llm_base_url": "एलएलएम बेस यूआरएल", - "llm_base_url_help": "सिनेमाई गुणवत्ता या एलएलएम इंजन के लिए। कोई भी OpenAI-संगत समापन बिंदु (OpenAI, Ollama, LM Studio, आदि)।", - "llm_model": "एलएलएम मॉडल", - "llm_model_help": "एलएलएम इंजन के साथ उपयोग करने के लिए मॉडल का नाम (उदाहरण के लिए जीपीटी-4ओ, डीपसीक-चैट, क्यूवेन2.5)।", - "group_huggingface": "आलिंगन चेहरा", - "group_translate": "अनुवाद (एलएलएम/सिनेमैटिक)", - "group_deepl": "डीपएल कस्टम एंडपॉइंट", - "group_microsoft": "माइक्रोसॉफ्ट ट्रांसलेटर कस्टम एंडपॉइंट", "region": "क्षेत्र", "aws_profile": "AWS प्रोफ़ाइल" }, "engines": { - "banners_on": "मंच पर बैनर", - "banners_off": "मंच के बैनर उतारे गए", "backend": "बैकएंड", "status": "स्थिति", "ready": "तैयार", @@ -941,7 +917,6 @@ "dictation_model_hint": "लाइव डिक्टेशन और ट्रांसक्रिप्शन के समय इस इंजन द्वारा उपयोग किया जाता है।", "dictation_not_installed": "इंस्टॉल नहीं है — डाउनलोड करने के लिए चुनें", "active": "सक्रिय", - "whyUnavailable": "इसे क्या चाहिए", "diskDetails": "डिस्क विवरण", "diskModelDownload": "मॉडल डाउनलोड", "diskPackageDownload": "पैकेज डाउनलोड", @@ -965,11 +940,8 @@ "sectionReady": "उपयोग के लिए तैयार", "sectionMore": "और इंजन जोड़ें", "lastError": "अंतिम त्रुटि: {{error}}", - "installedAndReady": "स्थापित और तैयार", - "notInstalled": "स्थापित नहीं", "available": "उपलब्ध", "subprocessTitle": "अपने स्वयं के उपप्रोसेस + वेनव में चलता है", - "inProcessTitle": "ओमनीवॉइस पायथन प्रक्रिया में चलता है", "testEngine": "परीक्षण इंजन", "testing": "परीक्षण...", "recheck": "दोबारा जांचें", @@ -986,7 +958,6 @@ "routingUnknown": "अज्ञात", "routingEffectiveChip": "इस मशीन पर {{device}} पर चलता है", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU चयनित, लेकिन: {{reason}}", "selectCpuFallback": "{{engine}}: CPU पर चल रहा है — {{reason}}", "selectWithCaveat": "{{engine}} पर स्विच किया गया — {{reason}}", "generateCaveat": "इस मशीन पर {{engine}} धीमा हो सकता है — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "कोई टौरी लॉग उपलब्ध नहीं है. केवल डेस्कटॉप शेल में चलता है.", "empty_backend": "रनटाइम लॉग खाली है. जैसे ही बैकएंड इसे लॉग करेगा गतिविधि यहां दिखाई देगी।", "title": "लॉग", - "source_backend": "बैकएंड", - "source_frontend": "अग्रभाग", - "source_tauri": "टौरी", "expand": "लॉग का विस्तार करें", "collapse": "लॉग संक्षिप्त करें", "expand_aria": "लॉग पैनल का विस्तार करें", @@ -1104,12 +1072,10 @@ "unlock": "अनलॉक", "try_voice": "इस आवाज को आज़माएं", "test_phrase": "परीक्षण वाक्यांश", - "test_help": "कुछ भी टाइप करें - हम इसे इस आवाज की वर्तमान सेटिंग्स के साथ उत्पन्न करेंगे।", "test_placeholder": "इस आवाज को सुनने के लिए कुछ टाइप करें और कहें...", "gen_preview": "पूर्वावलोकन उत्पन्न करें", "generating": "उत्पन्न हो रहा है...", "used_title": "जहां इस आवाज का इस्तेमाल किया गया है", - "used_empty": "इस आवाज़ का अभी तक उपयोग नहीं किया गया है. ऊपर एक पूर्वावलोकन बनाएं, या इसे डब में चुनें।", "synth_clips_one": "{{count}} सिंथ क्लिप", "synth_clips_other": "{{count}} सिंथ क्लिप", "projects_count_one": "{{count}} प्रोजेक्ट", @@ -1152,9 +1118,7 @@ "translate": "अनुवाद करें", "generate": "उत्पन्न करें", "export": "निर्यात करें", - "no_video": "कोई वीडियो लोड नहीं हुआ", "segments": "खंड", - "speakers": "वक्ता", "video_dubbing_studio": "वीडियो डबिंग स्टूडियो", "drop_here": "वीडियो या ऑडियो यहां छोड़ें", "paste_url": "...या यूट्यूब/वीडियो यूआरएल पेस्ट करें", @@ -1168,7 +1132,6 @@ "cookie_size_error": "कुकी निर्यात 1 MB या उससे छोटा होना चाहिए।", "save": "सहेजें", "reset": "रीसेट करें", - "save_project": "प्रोजेक्ट सहेजें", "change_file": "फ़ाइल बदलें", "import_srt": "आयात .srt", "import_srt_alt": "इसके बजाय .srt आयात करें", @@ -1186,9 +1149,6 @@ "retranslate": "पुनः अनुवाद करें", "transcript": "प्रतिलिपि", "generate_dub": "डब उत्पन्न करें", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "एसआरटी", "upload_transcribe": "अपलोड करें और ट्रांसक्राइब करें", "multi_lang": "मल्टी-लैंग", "prep_download": "वीडियो डाउनलोड हो रहा है...", @@ -1206,7 +1166,6 @@ "transcribing": "ऑडियो ट्रांसक्राइब हो रहा है…", "elapsed": "बीत गया", "remaining": "शेष है", - "sidebar_toggle": "साइडबार टॉगल करें", "retry_transcription": "प्रतिलेखन पुनः प्रयास करें", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "जब यूआरएल एक कैप्शन-धारक होस्ट (यूट्यूब, वीमियो, टीईडी…) है, तो मूल कैप्शन और किसी भी यूट्यूब ऑटो-अनुवाद को भी खींचें। व्हिस्पर चलाए बिना संपादक को सीड करता है; YouTube द्वारा पहले से ही कवर की गई भाषाओं के लिए Translate All को छोड़ें।", @@ -1249,7 +1208,6 @@ "engine_label": "इंजन", "install_engine": "इस इंजन को स्थापित करें", "installing_engine": "…इंस्टॉल कर रहा हूँ", - "needs_dev_install": "देव इंस्टालेशन की आवश्यकता है", "install_disabled_title": "पैकेज्ड बिल्ड में इंस्टॉल अक्षम हैं", "needs_install_suffix": "- इंस्टॉल की जरूरत है", "glossary_title": "आवर्ती शब्दों के लिए पिन अनुवाद (नाम, ब्रांड शब्द, शब्दजाल)", @@ -1283,7 +1241,6 @@ "original_track": "मौलिक", "selected_dub": "{{code}} (चयनित डब)", "dub_track": "{{code}} (डब)", - "export_tracks": "निर्यात ट्रैक:", "stopping": "रुक रहा हूँ...", "stop_progress": "रुकें ({{current}}/{{total}})", "regen_changed": "रेजेन {{count}} बदल गया", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} स्थापित", "install_failed": "इंस्टॉल विफल: {{message}}", "prep_elapsed": "{{time}} बीत गया", - "add_language": "भाषा जोड़ें", "search_languages": "भाषाएँ खोजें...", "more_to_narrow": "+{{count}} अधिक - संकीर्ण करने के लिए टाइप करें", "no_matches": "कोई मेल नहीं", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "समय", - "spkr": "Spkr", "text": "पाठ", "lang": "लंग", "voice": "आवाज", @@ -1403,8 +1358,6 @@ "select_all_title": "सभी फ़िल्टर किए गए का चयन करें", "sel_count": "{{count}} सेल", "select_title": "सेगमेंट चुनें (श्रेणी के लिए शिफ्ट+क्लिक करें)", - "sync_title": "उत्पन्न ऑडियो मूल की अवधि का {{pct}}% है", - "sync_label": "सिंक: {{pct}}%", "rate_title": "वाक्-दर फिट: {{ratio}}× स्लॉट के सापेक्ष{{error}}", "speaker_id": "स्पीकर आईडी", "translate_error_title": "अनुवाद त्रुटि: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "मूल पाठ पुनर्स्थापित करें", "lang_default": "(डीईएफ़)", "voice_default": "डिफ़ॉल्ट", - "from_video": "वीडियो से", - "clone_profiles": "क्लोन प्रोफाइल", - "design_presets": "डिज़ाइन प्रीसेट", "preview_title": "लाइव पूर्वावलोकन", "set_direction": "दिशा निर्धारित करें...", "edit_direction": "दिशा संपादित करें...", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "व्यक्तित्व", - "pick_personality": "एक व्यक्तित्व पूर्व निर्धारित चुनें...", "instruct": "निर्देश देना", "reference_text": "संदर्भ पाठ", "language": "भाषा", @@ -1535,9 +1484,7 @@ "stories": "कहानियां", "untitled_story": "शीर्षकहीन कहानी", "story_lines": "{{count}} लाइन", - "story_lines_plural": "{{count}} पंक्तियाँ", "story_voices": "{{count}} आवाज", - "story_voices_plural": "{{count}} आवाजें", "audiobooks": "ऑडियोबुक", "audiobook": "ऑडियोबुक", "story": "कहानी", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio गैलरी", - "search_placeholder": "यूट्यूब पर खोजें...", - "all_voices": "सभी आवाज़ें ({{count}})", - "no_voices": "अभी तक कोई आवाज़ नहीं", "download_hint": "क्लिप डाउनलोड करने के लिए ऊपर खोजें", - "search_btn": "खोजें", - "download_btn": "डाउनलोड करें", "saving": "सहेजा जा रहा है...", "trim": "ट्रिम", - "delete_confirm": "यह वॉइस क्लिप हटाएं?", - "view_mode_list": "सूची", - "view_mode_grid": "ग्रिड", - "youtube_results": "यूट्यूब परिणाम ({{count}})", - "clone_profile": "क्लोन प्रोफ़ाइल", - "crop_audio": "ऑडियो काटें", "subtitle": "सैकड़ों तैयार डिजाइन वाली आवाजें - एक चुनें और जाएं।", "zone_archetypes": "मूलरूप", "zone_imports": "मेरा आयात", @@ -1658,8 +1594,6 @@ "delete": "हटाएँ", "community_empty": "अभी तक कोई सामुदायिक आवाज़ें लोड नहीं हुईं — इंटरनेट से कनेक्ट करके फिर से खोलें, या सबसे पहले एक सबमिट करें।", "community_explainer": "समुदाय द्वारा साझा किए गए डिज़ाइन प्रीसेट और रिकॉर्ड की गई आवाज़ें, omnivoice-gallery से लोड की गईं।", - "no_designer": "रिकॉर्ड की गई आवाज़ — इसके बजाय \"आवाज का प्रयोग करें\" इस्तेमाल करें।", - "no_preview": "कोई पूर्वावलोकन नहीं — इसे सुनने के लिए \"आवाज का प्रयोग करें\" से जोड़ें।", "submit_failed": "सबमिशन फ़ॉर्म नहीं खोला जा सका।", "submit_preset": "प्रीसेट सबमिट करें", "submit_voice": "आवाज़ सबमिट करें", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "अब आप अपनी पहली आवाज़ बना सकते हैं। डिक्टेशन अभी या बाद में सेटिंग्स में सेट कर सकते हैं।", - "welcome": "स्वागत है", "system_check": "सिस्टम जांच", "install_models": "मॉडल स्थापित करें", - "pick_engines": "इंजन चुनें", "system_check_desc": "रैम, डिस्क, जीपीयू, एफएफएमपीईजी और नेटवर्क की जांच करें। अवरोधकों को अग्रिम रूप से चिह्नित किया जाता है ताकि आप डाउनलोड करने से पहले जान सकें।", "install_models_desc": "~2.4 जीबी वेट डाउनलोड करें — केवल टीटीएस मॉडल आवश्यक है। अपने हार्डवेयर के लिए चुने गए ट्रांसक्रिप्शन मॉडल अभी जोड़ें, या बाद में सेटिंग्स से।", "pick_engines_desc": "टीटीएस/एएसआर/एलएलएम बैकएंड चुनें। डिफ़ॉल्ट बॉक्स से बाहर काम करते हैं - सेटिंग्स में किसी भी समय अनुकूलित करें।", "hero_desc": "डबिंग, वॉइस क्लोनिंग, और वॉइस डिज़ाइन - सभी आपकी मशीन पर स्थानीय रूप से चल रहे हैं।", - "first_run": "पहले रन को डाउनलोड होने में 5-10 मिनट लगते हैं। उसके बाद, प्रत्येक लॉन्च तुरंत और पूरी तरह से ऑफ़लाइन होता है।", - "get_started": "आरंभ करें", "back": "वापस", "continue_ok": "सब ठीक है - जारी रखें", "continue_warn": "जारी रखें (चेतावनी के साथ)", @@ -1727,7 +1657,6 @@ "try_dictation": "श्रुतलेख का प्रयास करें", "try_dictation_desc": "सेटअप पूरा करने से पहले यह जांचने के लिए अपना शॉर्टकट एक बार दबाएँ कि डिक्टेशन काम कर रहा है।", "dictation_step_label": "डिक्टेशन", - "next_try_dictation": "अगला: श्रुतलेख का प्रयास करें", "step_aria": "चरण {{num}}: {{label}}", "step_completed": "पूर्ण", "cache_label": "मॉडल कैश" @@ -1743,7 +1672,6 @@ "platforms": "प्लेटफार्म", "footer": "प्रत्येक योगदान स्थानीय एआई की सीमाओं को आगे बढ़ाने में मदद करता है।", "back": "स्टूडियो में वापस", - "commercial": "वाणिज्यिक लाइसेंस", "pill": "ओमनीवॉइस का समर्थन करें", "suggested_title": "एक राशि चुनें", "most_common": "सबसे आम", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "VoiceStudio के विकास में सहयोग करें", - "lead_default": "खुशी है कि यह काम कर गया! ओमनीवॉइस मुफ़्त और पूरी तरह से स्थानीय है। यदि यह आपका समय बचाता है, तो इसके पीछे एक छोटा सा मासिक चिप-इन क्लाउड मैक्स को फंड करता है।", - "lead_first_clone": "आपकी पहली आवाज़ का क्लोन तैयार हो गया है - बढ़िया! ओमनीवॉइस पूरी तरह से आपकी मशीन पर चलता है, और आपका समर्थन इसे उसी तरह बनाए रखता है।", - "lead_tenth_dub": "दस डब - आप स्पष्ट रूप से इसे कार्यान्वित कर रहे हैं। एक छोटा सा मासिक चिप-इन क्लाउड मैक्स को फंड करता है जो इन सुविधाओं को शिप करता है।", - "lead_sustained": "आप एक महीने से ओमनीवॉइस के साथ हैं। यदि इसने आपके वर्कफ़्लो में एक स्थान अर्जित किया है, तो आगे क्या करना है इसके लिए फंड देने पर विचार करें।", - "chip_in": "चिप लगाओ", - "later": "शायद बाद में", - "star": "GitHub पर स्टार लगाएं", - "opt_out": "दोबारा मत पूछो", - "dismiss_aria": "ख़ारिज करें" + "later": "शायद बाद में" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "वाणिज्यिक लाइसेंस", "hero_title": "उत्पादन में एआई आवाजें भेजें", "hero_desc": "VoiceStudio GNU Affero General Public License v3 (AGPL-3.0) के अंतर्गत मुफ़्त और ओपन-सोर्स सॉफ़्टवेयर है — व्यावसायिक और आंतरिक कारोबारी उपयोग सहित, उपयोग के लिए मुफ़्त। व्यावसायिक लाइसेंस केवल तभी चाहिए जब आप AGPL-3.0 की कॉपीलेफ़्ट बाध्यताओं के बिना VoiceStudio को किसी क्लोज़्ड-सोर्स या प्रोप्राइटरी उत्पाद या सेवा में शामिल करना चाहें।", - "why_title": "व्यवसाय ओमनीवॉइस क्यों चुनते हैं?", - "pricing_title": "मूल्य निर्धारण", - "faq_title": "सामान्य प्रश्न", "pricing_desc": "टियर और मूल्य निर्धारण को अभी भी अंतिम रूप दिया जा रहा है।", - "pricing_detail": "जब तक वे सार्वजनिक नहीं होते, प्रत्येक व्यावसायिक तैनाती को व्यक्तिगत रूप से उद्धृत किया जा रहा है ताकि हम आपकी टीम और कार्यभार के लिए सही आकार दे सकें।", "request_quote": "एक उद्धरण का अनुरोध करें", - "footer_email": "प्रश्न? hi@voicestudio.sh पर संपर्क करें", - "footer_discord": "सामुदायिक समर्थन के लिए हमारे डिस्कोर्ड में शामिल हों।", "benefit_ip": "पूर्ण आईपी स्वामित्व", "benefit_ip_desc": "आप हर आउटपुट, हर मॉडल, हर डेरिवेटिव के मालिक हैं। कोई साझा किरायेदारी नहीं, कोई डेटा लीक नहीं।", "benefit_cost": "शून्य प्रति मिनट लागत", "benefit_cost_desc": "कोई एपीआई बिल नहीं, कोई आश्चर्य की बात नहीं। पहले दिन से आपके अपने हार्डवेयर पर चलता है।", - "benefit_team": "टीम-व्यापी पहुंच", "benefit_team_desc": "एक लाइसेंस आपके संगठन के भीतर असीमित उपयोगकर्ताओं को कवर करता है।", "benefit_support": "प्रत्यक्ष समर्थन", "benefit_support_desc": "कोर इंजीनियरिंग टीम से प्राथमिकता ईमेल और वीडियो-कॉल समर्थन।", - "benefit_source": "स्रोत-उपलब्ध कोर", "benefit_source_desc": "स्टैक में पूर्ण दृश्यता. लाइसेंस शर्तों के भीतर ऑडिट, फोर्क और अनुकूलन करें।", - "benefit_lang": "646 भाषाएँ", "benefit_lang_desc": "मानव-स्तरीय गुणवत्ता के साथ 646 भाषाओं में प्रतिलेखन, अनुवाद और डब करें।", - "hero_note": "उपयोग, सेल्फ़-होस्टिंग और व्यावसायिक उपयोग — बड़े पैमाने पर भी — AGPL-3.0 के अंतर्गत सभी मुफ़्त हैं। AGPL एक नेटवर्क-कॉपीलेफ़्ट लाइसेंस है: यदि आप VoiceStudio को संशोधित करके वह संशोधित संस्करण नेटवर्क के ज़रिये दूसरों को उपलब्ध कराते हैं, तो आपको अपना संशोधित सोर्स कोड उन्हीं शर्तों पर साझा करना होगा। व्यावसायिक लाइसेंस प्रोप्राइटरी, क्लोज़्ड-सोर्स परिनियोजनों के लिए ये कॉपीलेफ़्ट बाध्यताएँ हटा देता है। मूल्य-योजनाएँ जल्द आ रही हैं — तब तक हमसे संपर्क करें।", - "hero_simple": "ओमनीवॉइस स्टूडियो एजीपीएल-3.0 के तहत मुफ़्त और खुला स्रोत है - जिसमें वाणिज्यिक और आंतरिक व्यावसायिक उपयोग शामिल है। एजीपीएल के कॉपीलेफ्ट दायित्वों के बिना इसे बंद-स्रोत उत्पाद में एम्बेड करने के लिए आपको केवल एक वाणिज्यिक लाइसेंस की आवश्यकता है।", - "contact_lead": "मूल्य निर्धारण प्रति तैनाती के अनुसार उद्धृत किया गया है, इसलिए यह आपकी टीम और कार्यभार के अनुरूप है। मुझे अपना उपयोग मामला बताएं और मैं आपको एक उद्धरण दूंगा।" + "hero_simple": "ओमनीवॉइस स्टूडियो एजीपीएल-3.0 के तहत मुफ़्त और खुला स्रोत है - जिसमें वाणिज्यिक और आंतरिक व्यावसायिक उपयोग शामिल है। एजीपीएल के कॉपीलेफ्ट दायित्वों के बिना इसे बंद-स्रोत उत्पाद में एम्बेड करने के लिए आपको केवल एक वाणिज्यिक लाइसेंस की आवश्यकता है।" }, "exportModal": { "export": "निर्यात करें", @@ -2006,7 +1915,6 @@ "loaded_ready": "लोड और तैयार", "loading_first_run": "लोड हो रहा है... (पहली बार चलाने में 1-2 मिनट लग सकते हैं)", "failed_to_load": "लोड करने में विफल", - "not_loaded_yet": "अभी तक लोड नहीं हुआ है - प्रथम प्रतिलेखन पर लोड होगा", "tts_not_loaded_yet": "अभी लोड नहीं हुआ — पहली बार आवाज़ बनाने पर लोड होगा", "error_check_logs": "त्रुटि: {{error}}. लॉग जांचें और पुनः प्रारंभ करने का प्रयास करें।", "check_logs_restart": "मॉडल लोडिंग त्रुटियों के लिए लॉग की जाँच करें। पुनः आरंभ करने का प्रयास करें.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "स्पीकर कास्टिंग", - "auto_assign_title": "निकाले गए स्पीकर क्लोन से आवाजें स्वतः असाइन करें", - "auto_cast": "स्वतः-कास्ट", - "all_cast": "सभी कलाकार", - "segments_count": "{{count}} खंड", - "assign_voice": "आवाज असाइन करें...", - "from_video": "🎤वीडियो से ({{name}})", - "no_profiles": "अभी तक कोई ध्वनि प्रोफ़ाइल सहेजी नहीं गई है.", - "preview_voice": "आवाज का पूर्वावलोकन करें" + "segments_count": "{{count}} खंड" }, "checkpoint": { "timing_review": "{{count}} खंडों के समय की समीक्षा आवश्यक है", @@ -2203,8 +2104,6 @@ "play_failed": "प्ले विफल: {{message}}" }, "models": { - "hf_token_set_toast": "हगिंगफेस टोकन सेट - तेज़ डाउनलोड सक्षम", - "hf_token_save_failed": "टोकन सहेजने में विफल", "install_started": "इंस्टालेशन प्रारंभ - पंक्ति में प्रगति", "delete_confirm": "{{repoId}} हटाएं? आप इसे बाद में पुनः इंस्टॉल कर सकते हैं.", "delete_confirm_title": "मॉडल हटाएँ", @@ -2247,8 +2146,6 @@ "not_installed": "स्थापित नहीं", "required_tag": "आवश्यक", "delete_btn": "हटाएँ", - "hf_token_btn": "एचएफ टोकन", - "hf_set_title": "तेज़ डाउनलोड के लिए हगिंगफेस टोकन सेट करें", "get_token": "टोकन प्राप्त करें →", "reco_installed_for": "{{device}} के लिए अनुशंसित बंडल स्थापित किया गया", "reco_for": "{{device}} के लिए अनुशंसित", @@ -2256,8 +2153,6 @@ "required_size": "आवश्यक ~{{size}} GB", "all_size": "सभी ~{{size}} GB", "req_tag": "अनुरोध", - "search_placeholder": "मॉडल खोजें…", - "search_label": "मॉडल खोजें", "no_matches": "कोई भी मॉडल आपके फ़िल्टर से मेल नहीं खाता.", "sort_by": "{{column}} के अनुसार क्रमबद्ध करें", "column_model": "मॉडल", @@ -2267,12 +2162,8 @@ "ready_badge": "तैयार", "loading_badge": "लोड हो रहा है...", "idle_badge": "निष्क्रिय", - "started_downloading_required_one": "{{count}} आवश्यक मॉडल डाउनलोड करना प्रारंभ किया", - "started_downloading_required_other": "{{count}} आवश्यक मॉडल डाउनलोड करना प्रारंभ किया", "to_download": "डाउनलोड करने के लिए", "cached": "कैश्ड", - "fast_download_badge": "तेजी से डाउनलोड", - "fast_download_title": "Xet {{version}} के माध्यम से तेज़ डाउनलोड - समानांतर खंडित स्थानांतरण", "voice_previews": "आवाज़ के नमूने", "voice_previews_desc": "पहले से तैयार नमूने डाउनलोड करें ताकि आवाज़ें तुरंत चलें — तब भी जब कोई वॉइस मॉडल डाउनलोड हो रहा हो। जब तक आप इसे चालू नहीं करते, कुछ भी डाउनलोड नहीं होता।", "voice_previews_off": "बंद — नमूने इसी डिवाइस पर तैयार होते हैं", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "क्या मुझे आंतरिक उपकरणों के लिए लाइसेंस की आवश्यकता है?", - "a_internal_tools": "नहीं। आपके कर्मचारियों और ठेकेदारों द्वारा उपयोग — आंतरिक संशोधन और सेल्फ़-होस्टिंग सहित — AGPL-3.0 के अंतर्गत मुफ़्त है। व्यावसायिक लाइसेंस केवल तभी चाहिए जब आप VoiceStudio को किसी क्लोज़्ड-सोर्स या प्रोप्राइटरी उत्पाद या सेवा में शामिल करें और AGPL की सोर्स साझा करने की बाध्यताओं का पालन न करना चाहें।", - "q_try_before": "क्या मैं प्रतिबद्ध होने से पहले प्रयास कर सकता हूँ?", - "a_try_before": "हाँ। पूरा ऐप AGPL-3.0 के अंतर्गत मुफ़्त डाउनलोड, चलाने और सेल्फ़-होस्ट करने के लिए उपलब्ध है — किसी समझौते की ज़रूरत नहीं। जब आप व्यावसायिक (प्रोप्राइटरी-उपयोग) लाइसेंस पर चर्चा के लिए तैयार हों, तो हमें ईमेल करें और हम मिलकर विवरण तय करेंगे।", - "q_watermark": "वॉटरमार्क के बारे में क्या?", - "a_watermark": "अदृश्य AudioSeal वॉटरमार्क सभी के लिए डिफ़ॉल्ट रूप से जोड़ा जाता है। आप इसे सेटिंग्स → गोपनीयता में बंद कर सकते हैं; यह केवल बदलाव के बाद बनाए गए ऑडियो पर लागू होता है।" - }, "gallery_extra": { - "save_prompt": "इस ध्वनि प्रोफ़ाइल के लिए एक नाम दर्ज करें:", - "saved_as_profile": "आवाज़ प्रोफ़ाइल के रूप में सहेजी गई!", - "save_profile_failed": "प्रोफ़ाइल सहेजने में विफल", - "download_failed": "डाउनलोड विफल: {{message}}", - "trim_load_failed": "ट्रिमिंग के लिए ऑडियो लोड करने में विफल: {{message}}", - "upload_crop_failed": "क्रॉप की गई आवाज अपलोड करने में विफल: {{message}}" + "trim_load_failed": "ट्रिमिंग के लिए ऑडियो लोड करने में विफल: {{message}}" }, "dub_workflow": { "preparing_audio": "ऑडियो तैयार किया जा रहा है…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "लोड हो रहा है...", - "trimmed_loaded": "ट्रिम किया गया ऑडियो लोड किया गया", - "toast_exported": "निर्यातित: {{name}}", - "toast_export_failed": "निर्यात विफल: {{message}}", - "toast_open_folder_failed": "फ़ोल्डर नहीं खुल सका: {{message}}", "toast_saving": "सहेजा जा रहा है {{name}}...", "toast_saved": "सहेजा गया: {{path}}", "toast_save_error": "त्रुटि सहेजें: {{message}}", "toast_processing": "प्रसंस्करण {{name}}...", "toast_downloaded": "डाउनलोड किया गया {{name}}", "toast_download_error": "डाउनलोड त्रुटि: {{message}}", - "toast_upload_first": "कृपया पहले 'अपलोड और ट्रांसक्राइब' पर क्लिक करें ताकि वीडियो सहेजने से पहले सर्वर पर संसाधित हो जाए।", - "toast_save_failed": "सहेजना विफल: {{message}}", - "toast_opened": "खोला गया: {{name}}", - "toast_project_deleted": "प्रोजेक्ट हटा दिया गया", - "toast_restored_state": "पिछली पीढ़ी की स्थिति बहाल की गई", - "toast_history_deleted": "इतिहास आइटम हटा दिया गया", "toast_flushed": "फ्लश - RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· मॉडल उतार दिया गया", "toast_flush_failed": "फ्लश विफल: {{message}}", - "toast_project_saved": "प्रोजेक्ट सहेजा गया", - "toast_project_created": "प्रोजेक्ट बनाया गया" + "toast_project_saved": "प्रोजेक्ट सहेजा गया" }, "update": { "available": "अपडेट {{version}} उपलब्ध है", @@ -2531,13 +2398,11 @@ "not_writable": "लिखने योग्य नहीं", "change": "बदलें…", "compute_title": "कंप्यूट", - "compute_label": "GPU / एक्सेलेरेटर", "compute_auto": "ऑटो (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD GPU (ROCm, Linux)", "channel_label": "अपडेट चैनल", "channel_stable": "स्थिर", "channel_preview": "प्रीव्यू (नवीनतम main)", - "network_title": "नेटवर्क", "region_label": "डाउनलोड क्षेत्र", "mirrors_title": "कस्टम मिरर (उन्नत)", "mirror_pypi": "PyPI इंडेक्स URL", @@ -2570,10 +2435,6 @@ "trust_line": "निजता सबसे पहले: आपकी आवाज़ें, रिकॉर्डिंग और प्रोजेक्ट कभी इस मशीन से बाहर नहीं जाते — न खाता, न क्लाउड प्रोसेसिंग।", "resume_note": "रुके हुए डाउनलोड अपने आप फिर शुरू हो जाते हैं — ऐप बंद करना सुरक्षित है।", "eta_left": "~{{eta}} शेष", - "first_sound_text": "आपके स्टूडियो में स्वागत है। आप जो भी शब्द सुन रहे हैं, वह अभी-अभी इसी मशीन पर बना है।", - "first_sound_done": "वह आवाज़? कुछ सेकंड पहले, लोकली बनी। स्वागत है।", - "hf_token_title": "Hugging Face टोकन (वैकल्पिक)", - "hf_token_hint": "प्रतिबंधित मॉडल खोलता है — जैसे मल्टी-स्पीकर डबिंग के लिए स्पीकर डायराइज़ेशन (pyannote)। मुफ़्त खाते का टोकन; इसी मशीन पर रहता है।", "hf_token_save": "सहेजें", "hf_token_saving": "सहेजा जा रहा है…", "hf_token_saved": "Hugging Face टोकन सहेजा गया", @@ -2583,7 +2444,6 @@ "size_left": "{{size}}बाएं", "hf_token_card_title": "तेज़ डाउनलोड के लिए निःशुल्क हगिंग फेस टोकन जोड़ें", "hf_token_get": "टोकन नहीं है? एक निःशुल्क प्राप्त करें →", - "hf_token_saved_fast": "हगिंग फेस टोकन सहेजा गया - डाउनलोड अब तेज़ हैं", "hf_token_inline_prompt": "निःशुल्क हगिंग फेस टोकन के साथ डाउनलोड की गति बढ़ाएं", "hf_token_inline_ph": "hf_...टोकन चिपकाएँ (वैकल्पिक)", "hf_token_get_short": "एक निःशुल्क प्राप्त करें →", @@ -2607,15 +2467,11 @@ "unstar_take": "स्टार हटाएँ — सफ़ाई की अनुमति दें", "play_take": "सक्रिय आउटपुट के रूप में लोड करें", "filter_starred": "स्टार किए गए", - "load_take_failed": "वह टेक लोड नहीं हो सका: {{message}}", "mode_clone": "क्लोन", "mode_synth": "संश्लेषण", "generation_seconds": "{{duration}} सेकंड", - "expand_text": "टेक्स्ट फैलाएँ", - "collapse_text": "टेक्स्ट समेटें", "seed": "सीड {{seed}}", - "load_config": "सेटिंग लोड करें", - "delete_confirm": "यह इतिहास आइटम मिटाएँ?" + "load_config": "सेटिंग लोड करें" }, "openapi": { "title": "VoiceStudio API", @@ -3015,25 +2871,18 @@ "preview": "प्रायोजक पूर्वावलोकन", "message": "आपका ब्रांड, वेबसाइट और संदेश", "email_app": "ईमेल ऐप खोलें", - "copy_email": "ईमेल कॉपी करें", - "preview_detail": "आपका लोगो, लिंक और परिचय यहाँ दिख सकते हैं।" + "copy_email": "ईमेल कॉपी करें" }, "supportPlans": { "remove": "प्रायोजक बार हटाएँ", "title": "मुफ़्त बनाम Pro", "free": "मुफ़्त", "pro": "Pro", - "sponsor_bar": "प्रायोजक बार", "visible": "दिखाई देता है", - "hideable": "छिपाया जा सकता है", "unavailable": "Pro सक्रियण अभी कॉन्फ़िगर नहीं किया गया है।", "telemetry": "टेलीमेट्री", - "opt_in": "वैकल्पिक, सहमति पर", "disabled": "बंद", - "badge": "Pro बैज", - "advanced": "उन्नत टूल", - "standard": "मानक", - "included": "शामिल" + "badge": "Pro बैज" }, "integrationCatalog": { "title": "इंटीग्रेशन", @@ -3042,8 +2891,7 @@ "n8nHint": "यह वर्कफ़्लो n8n में आयात करें, अनुरोध संपादित करें और चलाएँ। VoiceStudio चालू रखें। Docker और रिमोट n8n के लिए पहुँच योग्य बैकएंड पता और आवश्यक क्रेडेंशियल चाहिए।" }, "directoryExamples": { - "example": "डायरेक्टरी उदाहरण", - "notice": "केवल डायरेक्टरी उदाहरण — प्रायोजक या जुड़े हुए इंटीग्रेशन नहीं।" + "example": "डायरेक्टरी उदाहरण" }, "workflowRun": { "run": "वर्कफ़्लो चलाएँ", diff --git a/electron/src/shared/i18n/locales/id.json b/electron/src/shared/i18n/locales/id.json index fdd1e5348..104de2488 100644 --- a/electron/src/shared/i18n/locales/id.json +++ b/electron/src/shared/i18n/locales/id.json @@ -59,15 +59,10 @@ "title": "Tanya VoiceStudio Agent", "notInstalled": "Belum terpasang", "noSource": "Buka checkout sumber VoiceStudio yang dapat ditulis agar agen dapat memperbaikinya.", - "noneFound": "Pasang Codex, Claude Code, OpenCode, atau Pi untuk perbaikan lokal.", "ready": "Siap", "placeholder": "Apa yang salah? Tambahkan langkah, galat yang terlihat, atau perilaku yang diharapkan.", - "prReady": "Perbaikan selesai. Tinjau perubahan dan usulkan PR setelah pengujian terarah lulus.", "diagnose": "Diagnosis", "fix": "Perbaiki", - "chooseDefault": "Pilih agen yang otomatis digunakan VoiceStudio saat layar ini mengalami crash.", - "useAutomatically": "Gunakan {{agent}} secara otomatis", - "captured": "{{count}} baris log bermasalah ditangkap", "contextNotice": "Agen yang dipilih menerima laporan ini, layar saat ini, log aplikasi dan backend terbaru, serta diagnostik sistem.", "complete": "Selesai" }, @@ -607,7 +602,6 @@ "characters": "{{count}} karakter", "minutes": "~{{count}} mnt", "chars": "{{count}} karakter", - "exportDone": "Buku audio diunduh", "exportFailed": "Ekspor gagal — periksa apakah backend sedang berjalan.", "global_speed": "Kecepatan", "global_speed_hint": "Kecepatan membaca untuk semua lini tanpa mengesampingkan kecepatannya sendiri", @@ -818,7 +812,6 @@ "data_dir": "Direktori data", "outputs": "Keluaran", "crash_log": "Catatan kerusakan", - "update_endpoint": "Perbarui titik akhir", "yes": "ya", "no": "tidak", "web_preview": "pratinjau web", @@ -879,38 +872,21 @@ "title": "Kredensial", "desc": "Kunci dan token API disetel <1>hanya untuk sesi ini. Untuk kegigihan saat memulai ulang, tetapkan variabel tersebut sebagai variabel lingkungan di profil shell Anda.", "hf_token": "Token Memeluk Wajah", - "hf_help": "Diperlukan untuk diarisasi speaker dan pengunduhan model yang lebih cepat. Dapatkan milik Anda di huggingface.co/settings/tokens.", - "translate_key": "Kunci API LLM", - "translate_help": "Opsional — untuk DeepL, OpenAI, atau penyedia terjemahan berbayar. Tidak diperlukan untuk Google Terjemahan (tingkat gratis).", "save": "Simpan", "saved": "✓ Tetapkan", - "not_set": "✗ Tidak disetel", "get_token": "Dapatkan token →", - "saved_session": "{{key}} disimpan untuk sesi ini", "save_failed": "Gagal menyimpan", "save_error": "Gagal menyimpan: {{message}}", "deepl_key": "Kunci API DeepL", - "deepl_help": "Untuk mesin terjemahan DeepL. Dapatkan kunci Anda di deepl.com/pro-api.", "deepl_base_url": "URL Basis DeepL", "deepl_base_url_help": "Titik akhir API DeepL khusus (default: https://api.deepl.com/v2). Ubah untuk proxy/hosting mandiri.", "microsoft_key": "Kunci Penerjemah Microsoft", - "microsoft_help": "Untuk mesin Microsoft Translator. Dapatkan kunci Anda dari Portal Azure.", "microsoft_base_url": "URL Basis Penerjemah Microsoft", "microsoft_base_url_help": "Titik akhir Azure Translator kustom (default: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "URL Dasar LLM", - "llm_base_url_help": "Untuk kualitas Sinematik atau mesin LLM. Semua titik akhir yang kompatibel dengan OpenAI (OpenAI, Ollama, LM Studio, dll.).", - "llm_model": "Model LLM", - "llm_model_help": "Nama model yang akan digunakan dengan mesin LLM (misalnya gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "Memeluk Wajah", - "group_translate": "Terjemahan (LLM / Sinematik)", - "group_deepl": "Titik Akhir Kustom DeepL", - "group_microsoft": "Titik Akhir Kustom Penerjemah Microsoft", "region": "Wilayah", "aws_profile": "Profil AWS" }, "engines": { - "banners_on": "Spanduk panggung menyala", - "banners_off": "Spanduk panggung dilepas", "backend": "Bagian belakang", "status": "Status", "ready": "siap", @@ -941,7 +917,6 @@ "dictation_model_hint": "Dipakai oleh dikte langsung dan oleh mesin ini saat mentranskripsi.", "dictation_not_installed": "Belum terpasang — pilih untuk mengunduh", "active": "aktif", - "whyUnavailable": "Apa yang dibutuhkan", "diskDetails": "Rincian disk", "diskModelDownload": "Unduhan model", "diskPackageDownload": "Unduhan paket", @@ -965,11 +940,8 @@ "sectionReady": "Siap digunakan", "sectionMore": "Tambahkan mesin lainnya", "lastError": "Kesalahan terakhir: {{error}}", - "installedAndReady": "Terpasang dan siap", - "notInstalled": "Tidak dipasang", "available": "Tersedia", "subprocessTitle": "Berjalan dalam subprosesnya sendiri + venv", - "inProcessTitle": "Berjalan dalam proses VoiceStudio Python", "testEngine": "Mesin uji", "testing": "Pengujian…", "recheck": "Periksa kembali", @@ -986,7 +958,6 @@ "routingUnknown": "Tidak diketahui", "routingEffectiveChip": "Berjalan pada {{device}} di mesin ini", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU dipilih, tetapi: {{reason}}", "selectCpuFallback": "{{engine}}: berjalan di CPU — {{reason}}", "selectWithCaveat": "Beralih ke {{engine}} — {{reason}}", "generateCaveat": "{{engine}} mungkin lambat di komputer ini — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Tidak ada log Tauri yang tersedia. Berjalan di shell desktop saja.", "empty_backend": "Log waktu proses kosong. Aktivitas akan muncul di sini saat backend mencatatnya.", "title": "Log", - "source_backend": "Bagian belakang", - "source_frontend": "Bagian depan", - "source_tauri": "Tauri", "expand": "Perluas log", "collapse": "Ciutkan log", "expand_aria": "Luaskan panel log", @@ -1104,12 +1072,10 @@ "unlock": "Buka kunci", "try_voice": "Coba suara ini", "test_phrase": "Frasa tes", - "test_help": "Ketik apa saja — kami akan membuatnya dengan pengaturan suara ini saat ini.", "test_placeholder": "Ketik sesuatu untuk mendengar suara ini mengatakannya…", "gen_preview": "Hasilkan pratinjau", "generating": "Menghasilkan…", "used_title": "Dimana suara ini telah digunakan", - "used_empty": "Suara ini belum digunakan. Buat pratinjau di atas, atau pilih di sulih suara.", "synth_clips_one": "{{count}} klip synth", "synth_clips_other": "{{count}} klip synth", "projects_count_one": "proyek {{count}}", @@ -1152,9 +1118,7 @@ "translate": "Terjemahkan", "generate": "Hasilkan", "export": "Ekspor", - "no_video": "Tidak ada video yang dimuat", "segments": "segmen", - "speakers": "pembicara", "video_dubbing_studio": "Studio Sulih Suara Video", "drop_here": "Letakkan video atau audio di sini", "paste_url": "…atau tempel URL YouTube/video", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Ekspor kuki harus berukuran 1 MB atau kurang.", "save": "Simpan", "reset": "Setel ulang", - "save_project": "Simpan proyek", "change_file": "Ubah berkas", "import_srt": "Impor .srt", "import_srt_alt": "Impor .srt sebagai gantinya", @@ -1186,9 +1149,6 @@ "retranslate": "Terjemahkan ulang", "transcript": "Transkrip", "generate_dub": "Hasilkan Dub", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Unggah & Transkripsikan", "multi_lang": "Multi-bahasa", "prep_download": "Mengunduh video…", @@ -1206,7 +1166,6 @@ "transcribing": "Mentranskripsikan audio…", "elapsed": "berlalu", "remaining": "tersisa", - "sidebar_toggle": "Alihkan Bilah Sisi", "retry_transcription": "Coba lagi transkripsi", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Jika URL-nya adalah host yang memuat teks (YouTube, Vimeo, TED…), tarik juga teks asli dan terjemahan otomatis YouTube apa pun. Menjadikan editor tanpa menjalankan Whisper; lewati Terjemahkan Semua untuk bahasa yang sudah dicakup oleh YouTube.", @@ -1249,7 +1208,6 @@ "engine_label": "Mesin", "install_engine": "Pasang mesin ini", "installing_engine": "…memasang", - "needs_dev_install": "perlu instalasi dev", "install_disabled_title": "Penginstalan dinonaktifkan dalam paket build", "needs_install_suffix": "— perlu dipasang", "glossary_title": "Pin terjemahan untuk istilah berulang (nama, kata merek, jargon)", @@ -1283,7 +1241,6 @@ "original_track": "Asli", "selected_dub": "{{code}} (Suara yang Dipilih)", "dub_track": "{{code}} (sulih suara)", - "export_tracks": "Ekspor Lagu:", "stopping": "Berhenti…", "stop_progress": "Berhenti ({{current}}/{{total}})", "regen_changed": "Regen {{count}} berubah", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} terpasang", "install_failed": "Pemasangan gagal: {{message}}", "prep_elapsed": "{{time}} telah berlalu", - "add_language": "Tambahkan bahasa", "search_languages": "Cari bahasa…", "languages_selected_one": "{{count}} bahasa dipilih", "languages_selected_other": "{{count}} bahasa dipilih", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Waktu", - "spkr": "Spkr", "text": "Teks", "lang": "Lang", "voice": "Suara", @@ -1403,8 +1358,6 @@ "select_all_title": "Pilih semua yang difilter", "sel_count": "{{count}} sel", "select_title": "Pilih segmen (shift+klik untuk rentang)", - "sync_title": "Audio yang dihasilkan berdurasi {{pct}}% dari aslinya", - "sync_label": "Sinkronisasi: {{pct}}%", "rate_title": "Kesesuaian kecepatan bicara: {{ratio}}× relatif terhadap slot{{error}}", "speaker_id": "ID Pembicara", "translate_error_title": "Kesalahan terjemahan: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Kembalikan teks asli", "lang_default": "(Tentu)", "voice_default": "Bawaan", - "from_video": "Dari Video", - "clone_profiles": "Profil Klon", - "design_presets": "Preset Desain", "preview_title": "Pratinjau Langsung", "set_direction": "Tetapkan arah…", "edit_direction": "Edit arah…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Kepribadian", - "pick_personality": "Pilih preset kepribadian…", "instruct": "Instruksikan", "reference_text": "Teks Referensi", "language": "Bahasa", @@ -1535,9 +1484,7 @@ "stories": "Cerita", "untitled_story": "Cerita tanpa judul", "story_lines": "baris {{count}}", - "story_lines_plural": "{{count}} baris", "story_voices": "{{count}} suara", - "story_voices_plural": "{{count}} suara", "audiobooks": "Buku audio", "audiobook": "Buku Audio", "story": "Cerita", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galeri", - "search_placeholder": "Telusuri YouTube…", - "all_voices": "Semua Suara ({{count}})", - "no_voices": "Belum ada suara", "download_hint": "Telusuri di atas untuk mengunduh klip", - "search_btn": "Cari", - "download_btn": "Unduh", "saving": "Menyimpan…", "trim": "Pangkas", - "delete_confirm": "Hapus klip suara ini?", - "view_mode_list": "Daftar", - "view_mode_grid": "jaringan", - "youtube_results": "Hasil YouTube ({{count}})", - "clone_profile": "Profil klon", - "crop_audio": "Pangkas audio", "subtitle": "Ratusan desain suara siap pakai — pilih satu dan mulai.", "zone_archetypes": "Arketipe", "zone_imports": "Impor Saya", @@ -1658,8 +1594,6 @@ "delete": "Hapus", "community_empty": "Belum ada suara komunitas yang dimuat — sambungkan ke internet lalu buka kembali, atau jadilah yang pertama mengirimkannya.", "community_explainer": "Preset desain dan suara rekaman yang dibagikan komunitas, dimuat dari omnivoice-gallery.", - "no_designer": "Suara rekaman — gunakan \"Gunakan suara\" sebagai gantinya.", - "no_preview": "Tidak ada pratinjau — tambahkan dengan \"Gunakan suara\" untuk mendengarnya.", "submit_failed": "Tidak dapat membuka formulir pengiriman.", "submit_preset": "Kirim preset", "submit_voice": "Kirim suara", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Anda siap membuat suara pertama. Atur dikte sekarang atau nanti di Pengaturan.", - "welcome": "Selamat datang", "system_check": "Pemeriksaan sistem", "install_models": "Instal model", - "pick_engines": "Pilih mesin", "system_check_desc": "Selidiki RAM, disk, GPU, ffmpeg, dan jaringan. Pemblokir ditandai terlebih dahulu sehingga Anda mengetahuinya sebelum mengunduh.", "install_models_desc": "Unduh bobot ~2,4 GB — hanya model TTS yang wajib. Tambahkan model transkripsi pilihan untuk perangkat keras Anda sekarang atau nanti dari Pengaturan.", "pick_engines_desc": "Pilih backend TTS / ASR / LLM. Standarnya langsung dapat digunakan — sesuaikan kapan saja di Pengaturan.", "hero_desc": "Sulih suara, kloning suara, dan desain suara — semuanya berjalan secara lokal di mesin Anda.", - "first_run": "Pengunduhan pertama memerlukan waktu 5–10 menit. Setelah itu, setiap peluncuran bersifat instan dan sepenuhnya offline.", - "get_started": "Mulailah", "back": "Kembali", "continue_ok": "Semuanya baik-baik saja — lanjutkan", "continue_warn": "Lanjutkan (dengan peringatan)", @@ -1727,7 +1657,6 @@ "try_dictation": "Coba dikte", "try_dictation_desc": "Tekan pintasan Anda sekali untuk memastikan dikte berfungsi sebelum menyelesaikan penyiapan.", "dictation_step_label": "Dikte", - "next_try_dictation": "Berikutnya: Coba dikte", "step_aria": "Langkah {{num}}: {{label}}", "step_completed": "selesai", "cache_label": "Tembolok model" @@ -1743,7 +1672,6 @@ "platforms": "Platform", "footer": "Setiap kontribusi membantu mendorong batasan AI lokal.", "back": "Kembali ke Studio", - "commercial": "Lisensi Komersial", "pill": "Mendukung VoiceStudio", "suggested_title": "Pilih jumlah", "most_common": "paling umum", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Dukung pengembangan VoiceStudio", - "lead_default": "Senang itu berhasil! VoiceStudio gratis dan sepenuhnya lokal. Jika ini menghemat waktu Anda, chip-in bulanan kecil akan mendanai Claude Max di belakangnya.", - "lead_first_clone": "Klon suara pertama Anda selesai — bagus! VoiceStudio berjalan sepenuhnya di mesin Anda, dan dukungan Anda menjaganya tetap seperti itu.", - "lead_tenth_dub": "Sepuluh sulih suara - Anda jelas berhasil. Chip-in bulanan kecil mendanai Claude Max yang mengirimkan fitur-fitur ini.", - "lead_sustained": "Anda telah menggunakan VoiceStudio selama sebulan. Jika hal ini mendapat tempat dalam alur kerja Anda, pertimbangkan untuk membantu mendanai langkah selanjutnya.", - "chip_in": "ikut serta", - "later": "Mungkin nanti", - "star": "Bintangi di GitHub", - "opt_out": "Jangan bertanya lagi", - "dismiss_aria": "Singkirkan" + "later": "Mungkin nanti" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Lisensi Komersial", "hero_title": "Kirimkan suara AI dalam produksi", "hero_desc": "VoiceStudio adalah perangkat lunak bebas dan open source di bawah GNU Affero General Public License v3 (AGPL-3.0) — gratis digunakan, termasuk untuk penggunaan komersial dan bisnis internal. Lisensi komersial hanya diperlukan jika Anda ingin menyematkan VoiceStudio ke dalam produk atau layanan closed-source atau proprietary tanpa kewajiban copyleft AGPL-3.0.", - "why_title": "Mengapa bisnis memilih VoiceStudio", - "pricing_title": "Harga", - "faq_title": "Pertanyaan umum", "pricing_desc": "Tingkatan dan harga masih dalam tahap penyelesaian.", - "pricing_detail": "Sebelum dipublikasikan, setiap penerapan komersial akan dikutip satu per satu sehingga kami dapat menyesuaikan ukurannya untuk tim dan beban kerja Anda.", "request_quote": "Minta penawaran", - "footer_email": "Pertanyaan? Hubungi hi@voicestudio.sh", - "footer_discord": "Bergabunglah dengan Discord kami untuk mendapatkan dukungan komunitas.", "benefit_ip": "Kepemilikan IP penuh", "benefit_ip_desc": "Anda memiliki setiap keluaran, setiap model, setiap turunan. Tidak ada sewa bersama, tidak ada kebocoran data.", "benefit_cost": "Nol biaya per menit", "benefit_cost_desc": "Tidak ada tagihan API, tidak ada kelebihan yang mengejutkan. Berjalan di perangkat keras Anda sendiri sejak hari pertama.", - "benefit_team": "Akses seluruh tim", "benefit_team_desc": "Satu lisensi mencakup pengguna tanpa batas dalam organisasi Anda.", "benefit_support": "Dukungan langsung", "benefit_support_desc": "Dukungan email prioritas dan panggilan video dari tim teknik inti.", - "benefit_source": "Inti yang tersedia sumber", "benefit_source_desc": "Visibilitas penuh ke dalam tumpukan. Audit, fork, dan adaptasi sesuai ketentuan lisensi.", - "benefit_lang": "646 bahasa", "benefit_lang_desc": "Transkripsikan, terjemahkan, dan sulih suara dalam 646 bahasa dengan kualitas setingkat manusia.", - "hero_note": "Penggunaan, self-hosting, dan penggunaan komersial semuanya gratis di bawah AGPL-3.0 — termasuk dalam skala besar. AGPL adalah lisensi copyleft jaringan: jika Anda memodifikasi VoiceStudio dan menawarkan versi modifikasi itu kepada orang lain melalui jaringan, Anda harus membagikan kode sumber modifikasi Anda dengan ketentuan yang sama. Lisensi komersial menghapus kewajiban copyleft tersebut untuk penerapan proprietary closed-source. Paket harga segera hadir — sementara itu, hubungi kami.", - "hero_simple": "VoiceStudio gratis dan bersumber terbuka di bawah AGPL-3.0 — termasuk untuk penggunaan komersial dan bisnis internal. Anda hanya memerlukan lisensi komersial untuk menyematkannya dalam produk sumber tertutup tanpa kewajiban copyleft AGPL.", - "contact_lead": "Harga dikutip per penerapan sehingga sesuai dengan tim dan beban kerja Anda. Ceritakan kasus penggunaan Anda dan saya akan memberi Anda penawaran." + "hero_simple": "VoiceStudio gratis dan bersumber terbuka di bawah AGPL-3.0 — termasuk untuk penggunaan komersial dan bisnis internal. Anda hanya memerlukan lisensi komersial untuk menyematkannya dalam produk sumber tertutup tanpa kewajiban copyleft AGPL." }, "exportModal": { "export": "Ekspor", @@ -2006,7 +1915,6 @@ "loaded_ready": "Sudah terisi dan siap", "loading_first_run": "Memuat… (ini mungkin memakan waktu 1-2 menit saat pertama kali dijalankan)", "failed_to_load": "Gagal memuat", - "not_loaded_yet": "Belum dimuat — akan dimuat pada transkripsi pertama", "tts_not_loaded_yet": "Belum dimuat — akan dimuat saat pertama kali menghasilkan suara", "error_check_logs": "Kesalahan: {{error}}. Periksa log dan coba mulai ulang.", "check_logs_restart": "Periksa log untuk mengetahui kesalahan pemuatan model. Coba mulai ulang.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Pengecoran Pembicara", - "auto_assign_title": "Menetapkan suara secara otomatis dari klon speaker yang diekstrak", - "auto_cast": "Transmisi otomatis", - "all_cast": "Semua pemeran", - "segments_count": "{{count}} segmen", - "assign_voice": "Tetapkan suara…", - "from_video": "🎤 Dari video ({{name}})", - "no_profiles": "Belum ada profil suara yang disimpan.", - "preview_voice": "Pratinjau suara" + "segments_count": "{{count}} segmen" }, "checkpoint": { "timing_review": "{{count}} segmen perlu peninjauan waktu", @@ -2203,8 +2104,6 @@ "play_failed": "Gagal bermain: {{message}}" }, "models": { - "hf_token_set_toast": "Kumpulan token HuggingFace — pengunduhan lebih cepat diaktifkan", - "hf_token_save_failed": "Gagal menyimpan token", "install_started": "Penginstalan dimulai — kemajuan dalam baris", "delete_confirm": "Hapus {{repoId}}? Anda dapat menginstalnya kembali nanti.", "delete_confirm_title": "Hapus modelnya", @@ -2247,8 +2146,6 @@ "not_installed": "tidak dipasang", "required_tag": "diperlukan", "delete_btn": "Hapus", - "hf_token_btn": "Token HF", - "hf_set_title": "Setel token HuggingFace untuk pengunduhan yang lebih cepat", "get_token": "Dapatkan token →", "reco_installed_for": "Paket yang direkomendasikan dipasang untuk {{device}}", "reco_for": "Direkomendasikan untuk {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Diperlukan ~{{size}} GB", "all_size": "Semua ~{{size}} GB", "req_tag": "permintaan", - "search_placeholder": "Model pencarian…", - "search_label": "Model pencarian", "no_matches": "Tidak ada model yang cocok dengan filter Anda.", "sort_by": "Urutkan berdasarkan {{column}}", "column_model": "Model", @@ -2267,12 +2162,8 @@ "ready_badge": "Siap", "loading_badge": "Memuat…", "idle_badge": "Menganggur", - "started_downloading_required_one": "Mulai mengunduh {{count}} model yang diperlukan", - "started_downloading_required_other": "Mulai mengunduh {{count}} model yang diperlukan", "to_download": "untuk mengunduh", "cached": "di-cache", - "fast_download_badge": "unduh cepat", - "fast_download_title": "Pengunduhan cepat melalui Xet {{version}} — transfer potongan paralel", "voice_previews": "Pratinjau suara", "voice_previews_desc": "Unduh pratinjau yang sudah dirender lebih dulu agar suara langsung diputar — bahkan sebelum model suara selesai diunduh. Tidak ada yang diunduh sampai Anda mengaktifkan ini.", "voice_previews_off": "Nonaktif — pratinjau dirender di perangkat ini", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Apakah saya memerlukan lisensi untuk alat internal?", - "a_internal_tools": "Tidak. Penggunaan oleh karyawan dan kontraktor Anda — termasuk memodifikasi dan self-hosting secara internal — gratis di bawah AGPL-3.0. Lisensi komersial hanya diperlukan jika Anda menyematkan VoiceStudio ke dalam produk atau layanan closed-source atau proprietary dan tidak ingin mematuhi kewajiban berbagi kode sumber AGPL.", - "q_try_before": "Bisakah saya mencoba sebelum melakukan?", - "a_try_before": "Ya. Aplikasi lengkap gratis untuk diunduh, dijalankan, dan di-host sendiri di bawah AGPL-3.0 — tanpa perjanjian apa pun. Saat Anda siap membahas lisensi komersial (penggunaan proprietary), kirim email kepada kami dan kita akan menyelesaikan detailnya bersama.", - "q_watermark": "Bagaimana dengan tanda airnya?", - "a_watermark": "Tanda air AudioSeal yang tak terlihat disematkan secara bawaan untuk semua orang. Anda dapat mematikannya di Pengaturan → Privasi; ini hanya memengaruhi audio yang dibuat setelah perubahan." - }, "gallery_extra": { - "save_prompt": "Masukkan nama untuk profil suara ini:", - "saved_as_profile": "Suara disimpan sebagai profil!", - "save_profile_failed": "Gagal menyimpan profil", - "download_failed": "Pengunduhan gagal: {{message}}", - "trim_load_failed": "Gagal memuat audio untuk pemangkasan: {{message}}", - "upload_crop_failed": "Gagal mengunggah suara yang dipotong: {{message}}" + "trim_load_failed": "Gagal memuat audio untuk pemangkasan: {{message}}" }, "dub_workflow": { "preparing_audio": "Mempersiapkan audio…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Memuat…", - "trimmed_loaded": "Audio yang dipangkas dimuat", - "toast_exported": "Diekspor: {{name}}", - "toast_export_failed": "Ekspor gagal: {{message}}", - "toast_open_folder_failed": "Tidak dapat membuka folder: {{message}}", "toast_saving": "Menyimpan {{name}}...", "toast_saved": "Disimpan: {{path}}", "toast_save_error": "Kesalahan penyimpanan: {{message}}", "toast_processing": "Memproses {{name}}...", "toast_downloaded": "Diunduh {{name}}", "toast_download_error": "Kesalahan pengunduhan: {{message}}", - "toast_upload_first": "Silakan klik 'Unggah & Transkripsikan' terlebih dahulu agar video diproses di server sebelum disimpan.", - "toast_save_failed": "Gagal menyimpan: {{message}}", - "toast_opened": "Dibuka: {{name}}", - "toast_project_deleted": "Proyek dihapus", - "toast_restored_state": "Memulihkan keadaan generasi sebelumnya", - "toast_history_deleted": "Item riwayat dihapus", "toast_flushed": "Dibilas — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· model dibongkar", "toast_flush_failed": "Gagal menyiram: {{message}}", - "toast_project_saved": "Proyek disimpan", - "toast_project_created": "Proyek dibuat" + "toast_project_saved": "Proyek disimpan" }, "update": { "available": "Pembaruan {{version}} tersedia", @@ -2531,13 +2398,11 @@ "not_writable": "tidak dapat ditulisi", "change": "Ubah…", "compute_title": "Komputasi", - "compute_label": "GPU / akselerator", "compute_auto": "Otomatis (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Saluran pembaruan", "channel_stable": "Stabil", "channel_preview": "Pratinjau (main terbaru)", - "network_title": "Jaringan", "region_label": "Wilayah unduhan", "mirrors_title": "Mirror kustom (lanjutan)", "mirror_pypi": "URL indeks PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "Utamakan privasi: suara, rekaman, dan proyek Anda tidak pernah meninggalkan mesin ini — tanpa akun, tanpa pemrosesan cloud.", "resume_note": "Unduhan yang terputus dilanjutkan otomatis — menutup aplikasi aman.", "eta_left": "sisa ~{{eta}}", - "first_sound_text": "Selamat datang di studio Anda. Setiap kata yang Anda dengar baru saja dihasilkan di mesin ini.", - "first_sound_done": "Suara tadi? Dihasilkan beberapa detik lalu, secara lokal. Selamat datang.", - "hf_token_title": "Token Hugging Face (opsional)", - "hf_token_hint": "Membuka model terbatas — mis. diarisasi pembicara untuk sulih suara multi-suara (pyannote). Token akun gratis; tetap di mesin ini.", "hf_token_save": "Simpan", "hf_token_saving": "menyimpan…", "hf_token_saved": "Token Hugging Face tersimpan", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} kiri", "hf_token_card_title": "Tambahkan token Hugging Face gratis untuk pengunduhan lebih cepat", "hf_token_get": "Tidak punya tokennya? Dapatkan satu gratis →", - "hf_token_saved_fast": "Token Hugging Face disimpan — pengunduhan kini lebih cepat", "hf_token_inline_prompt": "Percepat pengunduhan dengan token Hugging Face gratis", "hf_token_inline_ph": "Tempel hf_… token (opsional)", "hf_token_get_short": "Dapatkan satu gratis →", @@ -2607,15 +2467,11 @@ "unstar_take": "Hapus bintang — izinkan pembersihan", "play_take": "Muat sebagai keluaran aktif", "filter_starred": "Berbintang", - "load_take_failed": "Tidak dapat memuat take itu: {{message}}", "mode_clone": "Klon", "mode_synth": "Sintesis", "generation_seconds": "{{duration}} dtk", - "expand_text": "Perluas teks", - "collapse_text": "Ciutkan teks", "seed": "Seed {{seed}}", - "load_config": "Muat pengaturan", - "delete_confirm": "Hapus item riwayat ini?" + "load_config": "Muat pengaturan" }, "openapi": { "title": "API VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Pratinjau sponsor", "message": "Merek, situs web & pesan Anda", "email_app": "Buka aplikasi email", - "copy_email": "Salin email", - "preview_detail": "Logo, tautan, dan perkenalan Anda bisa tampil di sini." + "copy_email": "Salin email" }, "supportPlans": { "remove": "Hapus bilah sponsor", "title": "Gratis vs Pro", "free": "Gratis", "pro": "Pro", - "sponsor_bar": "Bilah sponsor", "visible": "Terlihat", - "hideable": "Dapat disembunyikan", "unavailable": "Aktivasi Pro belum dikonfigurasi.", "telemetry": "Telemetri", - "opt_in": "Opsional, dengan persetujuan", "disabled": "Dinonaktifkan", - "badge": "Lencana Pro", - "advanced": "Alat lanjutan", - "standard": "Standar", - "included": "Termasuk" + "badge": "Lencana Pro" }, "integrationCatalog": { "title": "Integrasi", @@ -3042,8 +2891,7 @@ "n8nHint": "Impor alur kerja ini ke n8n, edit permintaan, lalu jalankan. Biarkan VoiceStudio berjalan. Docker dan n8n jarak jauh memerlukan alamat backend yang dapat dijangkau serta kredensial yang diperlukan." }, "directoryExamples": { - "example": "Contoh direktori", - "notice": "Hanya contoh direktori — bukan sponsor atau integrasi yang terhubung." + "example": "Contoh direktori" }, "workflowRun": { "run": "Jalankan alur", diff --git a/electron/src/shared/i18n/locales/it.json b/electron/src/shared/i18n/locales/it.json index ed62ff418..00f6fb299 100644 --- a/electron/src/shared/i18n/locales/it.json +++ b/electron/src/shared/i18n/locales/it.json @@ -59,15 +59,10 @@ "title": "Chiedi a VoiceStudio Agent", "notInstalled": "Non installato", "noSource": "Apri un checkout scrivibile del sorgente di VoiceStudio per consentire la riparazione.", - "noneFound": "Installa Codex, Claude Code, OpenCode o Pi per usare la riparazione locale.", "ready": "Pronto", "placeholder": "Che cosa non ha funzionato? Aggiungi passaggi, errori visibili o comportamento previsto.", - "prReady": "Correzione completata. Controlla le modifiche e proponi una PR quando i test mirati passano.", "diagnose": "Diagnostica", "fix": "Correggi", - "chooseDefault": "Scegli l’agente che VoiceStudio userà automaticamente se questa schermata si arresta.", - "useAutomatically": "Usa {{agent}} automaticamente", - "captured": "Acquisite {{count}} righe di log problematiche", "contextNotice": "L’agente selezionato riceve questa segnalazione, la schermata corrente, i log recenti dell’app e del backend e la diagnostica di sistema.", "complete": "Completato" }, @@ -607,7 +602,6 @@ "characters": "{{count}} caratteri", "minutes": "~{{count}} min", "chars": "{{count}} car", - "exportDone": "Audiolibro scaricato", "exportFailed": "Esportazione non riuscita: controlla che il backend sia in esecuzione.", "global_speed": "Velocità", "global_speed_hint": "Velocità di lettura per tutte le linee senza il proprio override di velocità", @@ -818,7 +812,6 @@ "data_dir": "Directory dei dati", "outputs": "Uscite", "crash_log": "Registro degli arresti anomali", - "update_endpoint": "Aggiorna endpoint", "yes": "sì", "no": "no", "web_preview": "anteprima web", @@ -879,38 +872,21 @@ "title": "Credenziali", "desc": "Le chiavi API e i token sono impostati <1>solo per questa sessione. Per la persistenza tra i riavvii, impostali come variabili di ambiente nel profilo della shell.", "hf_token": "Gettone del volto abbracciato", - "hf_help": "Necessario per la diarizzazione degli oratori e download più rapidi dei modelli. Ottieni il tuo su Huggingface.co/settings/tokens.", - "translate_key": "Chiave API LLM", - "translate_help": "Facoltativo: per DeepL, OpenAI o fornitori di traduzioni a pagamento. Non necessario per Google Translate (livello gratuito).", "save": "Salva", "saved": "✓ Imposta", - "not_set": "✗ Non impostato", "get_token": "Ottieni gettone →", - "saved_session": "{{key}} salvato per questa sessione", "save_failed": "Impossibile salvare", "save_error": "Salvataggio non riuscito: {{message}}", "deepl_key": "Chiave API DeepL", - "deepl_help": "Per il motore di traduzione DeepL. Ottieni la tua chiave su deepl.com/pro-api.", "deepl_base_url": "URL di base DeepL", "deepl_base_url_help": "Endpoint API DeepL personalizzato (impostazione predefinita: https://api.deepl.com/v2). Modifica per proxy/self-hosted.", "microsoft_key": "Chiave del traduttore Microsoft", - "microsoft_help": "Per il motore Microsoft Translator. Ottieni la chiave dal portale di Azure.", "microsoft_base_url": "URL di base di Microsoft Translator", "microsoft_base_url_help": "Endpoint personalizzato di traduttore di Azure (impostazione predefinita: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "URL di base LLM", - "llm_base_url_help": "Per qualità cinematografica o motore LLM. Qualsiasi endpoint compatibile con OpenAI (OpenAI, Ollama, LM Studio, ecc.).", - "llm_model": "Modello LLM", - "llm_model_help": "Nome del modello da utilizzare con il motore LLM (ad esempio gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "Abbracciare il viso", - "group_translate": "Traduzione (LLM / Cinematografica)", - "group_deepl": "Endpoint personalizzato DeepL", - "group_microsoft": "Endpoint personalizzato di Microsoft Translator", "region": "Regione", "aws_profile": "Profilo AWS" }, "engines": { - "banners_on": "Striscioni sul palco accesi", - "banners_off": "Gli striscioni sul palco sono spenti", "backend": "Backend", "status": "Stato", "ready": "pronto", @@ -941,7 +917,6 @@ "dictation_model_hint": "Usato dalla dettatura dal vivo e da questo motore per la trascrizione.", "dictation_not_installed": "Non installato: seleziona per scaricare", "active": "attivo", - "whyUnavailable": "Cosa serve", "diskDetails": "Dettagli disco", "diskModelDownload": "Download del modello", "diskPackageDownload": "Download dei pacchetti", @@ -965,11 +940,8 @@ "sectionReady": "Pronti all'uso", "sectionMore": "Aggiungi altri motori", "lastError": "Ultimo errore: {{error}}", - "installedAndReady": "Installato e pronto", - "notInstalled": "Non installato", "available": "Disponibile", "subprocessTitle": "Viene eseguito nel proprio sottoprocesso + venv", - "inProcessTitle": "Viene eseguito nel processo VoiceStudio Python", "testEngine": "Motore di prova", "testing": "Test…", "recheck": "Ricontrolla", @@ -986,7 +958,6 @@ "routingUnknown": "Sconosciuto", "routingEffectiveChip": "Funziona su {{device}} su questa macchina", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU selezionata, ma: {{reason}}", "selectCpuFallback": "{{engine}}: in esecuzione sulla CPU — {{reason}}", "selectWithCaveat": "Passato a {{engine}} — {{reason}}", "generateCaveat": "{{engine}} potrebbe essere lento su questo computer — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Nessun registro Tauri disponibile. Funziona solo nella shell del desktop.", "empty_backend": "Il registro di runtime è vuoto. L'attività verrà visualizzata qui man mano che il backend la registra.", "title": "Registri", - "source_backend": "Backend", - "source_frontend": "Frontend", - "source_tauri": "Tauri", "expand": "Espandi i log", "collapse": "Comprimi i log", "expand_aria": "Espandi il pannello dei registri", @@ -1104,12 +1072,10 @@ "unlock": "Sblocca", "try_voice": "Prova questa voce", "test_phrase": "Frase di prova", - "test_help": "Digita qualsiasi cosa: lo genereremo con le impostazioni attuali di questa voce.", "test_placeholder": "Digita qualcosa per sentire questa voce dirlo...", "gen_preview": "Genera anteprima", "generating": "Generazione…", "used_title": "Dove è stata usata questa voce", - "used_empty": "Questa voce non è stata ancora utilizzata. Genera un'anteprima qui sopra o selezionala in un doppiaggio.", "synth_clips_one": "{{count}} clip sintetizzatore", "synth_clips_other": "{{count}} clip sintetizzatori", "projects_count_one": "progetto {{count}}", @@ -1152,9 +1118,7 @@ "translate": "Traduci", "generate": "Genera", "export": "Esportazione", - "no_video": "Nessun video caricato", "segments": "segmenti", - "speakers": "altoparlanti", "video_dubbing_studio": "Studio di doppiaggio video", "drop_here": "Rilascia video o audio qui", "paste_url": "...o incolla l'URL di YouTube/video", @@ -1168,7 +1132,6 @@ "cookie_size_error": "L’esportazione dei cookie deve essere al massimo di 1 MB.", "save": "Salva", "reset": "Ripristina", - "save_project": "Salva progetto", "change_file": "Cambia file", "import_srt": "Importa .srt", "import_srt_alt": "Importa invece .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Ritradurre", "transcript": "Trascrizione", "generate_dub": "Genera doppiaggio", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Carica e trascrivi", "multi_lang": "Multilingue", "prep_download": "Download del video…", @@ -1206,7 +1166,6 @@ "transcribing": "Trascrizione audio…", "elapsed": "trascorso", "remaining": "rimane", - "sidebar_toggle": "Attiva/disattiva la barra laterale", "retry_transcription": "Riprovare la trascrizione", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Quando l'URL è un host contenente didascalie (YouTube, Vimeo, TED…), estrai anche i sottotitoli originali e le eventuali traduzioni automatiche di YouTube. Avvia l'editor senza eseguire Whisper; salta Traduci tutto per le lingue già coperte da YouTube.", @@ -1249,7 +1208,6 @@ "engine_label": "Motore", "install_engine": "Installa questo motore", "installing_engine": "…installazione", - "needs_dev_install": "necessita dell'installazione dello sviluppatore", "install_disabled_title": "Le installazioni sono disabilitate nelle build in pacchetto", "needs_install_suffix": "- necessita di installazione", "glossary_title": "Traduzioni dei pin per termini ricorrenti (nomi, parole del marchio, gergo)", @@ -1283,7 +1241,6 @@ "original_track": "Originale", "selected_dub": "{{code}} (duplicazione selezionata)", "dub_track": "{{code}} (Doppiaggio)", - "export_tracks": "Esporta tracce:", "stopping": "Arresto…", "stop_progress": "Arresto ({{current}}/{{total}})", "regen_changed": "Rigenerazione {{count}} modificata", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} installato", "install_failed": "Installazione non riuscita: {{message}}", "prep_elapsed": "{{time}} trascorso", - "add_language": "Aggiungi lingua", "search_languages": "Cerca lingue…", "more_to_narrow": "+{{count}} altro: digita per restringere", "no_matches": "Nessuna corrispondenza", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Tempo", - "spkr": "Spkr", "text": "Testo", "lang": "Lang", "voice": "Voce", @@ -1403,8 +1358,6 @@ "select_all_title": "Seleziona tutto filtrato", "sel_count": "{{count}} sel", "select_title": "Seleziona il segmento (maiusc+clic per l'intervallo)", - "sync_title": "L'audio generato ha una durata pari al {{pct}}% dell'originale", - "sync_label": "Sincronizzazione: {{pct}}%", "rate_title": "Adattamento della velocità della parola: {{ratio}}× relativo allo slot{{error}}", "speaker_id": "ID dell'oratore", "translate_error_title": "Errore di traduzione: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Ripristina il testo originale", "lang_default": "(Dif)", "voice_default": "Predefinito", - "from_video": "Dal video", - "clone_profiles": "Clona profili", - "design_presets": "Preimpostazioni di progettazione", "preview_title": "Anteprima dal vivo", "set_direction": "Imposta direzione...", "edit_direction": "Modifica direzione...", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Personalità", - "pick_personality": "Scegli una personalità preimpostata...", "instruct": "Istruire", "reference_text": "Testo di riferimento", "language": "Lingua", @@ -1535,9 +1484,7 @@ "stories": "Storie", "untitled_story": "Storia senza titolo", "story_lines": "riga {{count}}", - "story_lines_plural": "{{count}} linee", "story_voices": "{{count}} voce", - "story_voices_plural": "{{count}} voci", "audiobooks": "Audiolibri", "audiobook": "Audiolibro", "story": "Storia", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galleria", - "search_placeholder": "Cerca su YouTube...", - "all_voices": "Tutte le voci ({{count}})", - "no_voices": "Nessuna voce ancora", "download_hint": "Cerca qui sopra per scaricare clip", - "search_btn": "Cerca", - "download_btn": "Scarica", "saving": "Salvataggio…", "trim": "Tagliare", - "delete_confirm": "Eliminare questo clip vocale?", - "view_mode_list": "Elenco", - "view_mode_grid": "Griglia", - "youtube_results": "Risultati di YouTube ({{count}})", - "clone_profile": "Profilo clone", - "crop_audio": "Ritaglia l'audio", "subtitle": "Centinaia di voci progettate già pronte: scegline una e vai.", "zone_archetypes": "Archetipi", "zone_imports": "Le mie importazioni", @@ -1658,8 +1594,6 @@ "delete": "Elimina", "community_empty": "Nessuna voce della community caricata — connettiti a internet e riapri, oppure sii il primo a inviarne una.", "community_explainer": "Preimpostazioni progettate e voci registrate condivise dalla community, caricate dalla omnivoice-gallery.", - "no_designer": "Voce registrata — usa invece \"Usa la voce\".", - "no_preview": "Nessuna anteprima — aggiungila con \"Usa la voce\" per ascoltarla.", "submit_failed": "Impossibile aprire il modulo di invio.", "submit_preset": "Invia una preimpostazione", "submit_voice": "Invia una voce", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Puoi creare la tua prima voce. Configura la dettatura ora o più tardi nelle Impostazioni.", - "welcome": "Benvenuto", "system_check": "Controllo del sistema", "install_models": "Installa modelli", - "pick_engines": "Scegli i motori", "system_check_desc": "Analizza RAM, disco, GPU, ffmpeg e rete. I bloccanti vengono contrassegnati in anticipo in modo da saperlo prima del download.", "install_models_desc": "Scarica ~2,4 GB di pesi: è richiesto solo il modello TTS. Aggiungi i modelli di trascrizione curati per il tuo hardware ora o in seguito dalle Impostazioni.", "pick_engines_desc": "Scegli i backend TTS/ASR/LLM. Le impostazioni predefinite funzionano immediatamente: personalizza in qualsiasi momento nelle Impostazioni.", "hero_desc": "Duplicazione, clonazione e progettazione vocale: tutto in esecuzione localmente sul tuo computer.", - "first_run": "Il download della prima esecuzione richiede 5-10 minuti. Successivamente, ogni lancio è istantaneo e completamente offline.", - "get_started": "Inizia", "back": "Indietro", "continue_ok": "Tutto bene, continua", "continue_warn": "Continua (con avvisi)", @@ -1727,7 +1657,6 @@ "try_dictation": "Prova la dettatura", "try_dictation_desc": "Premi la tua scorciatoia una volta per verificare che la dettatura funzioni prima di completare la configurazione.", "dictation_step_label": "Dettatura", - "next_try_dictation": "Successiva: Prova la dettatura", "step_aria": "Passaggio {{num}}: {{label}}", "step_completed": "completato", "cache_label": "Cache del modello" @@ -1743,7 +1672,6 @@ "platforms": "Piattaforme", "footer": "Ogni contributo aiuta ad ampliare i confini dell'intelligenza artificiale locale.", "back": "Ritorno allo Studio", - "commercial": "Licenza commerciale", "pill": "Supporta VoiceStudio", "suggested_title": "Scegli un importo", "most_common": "più comune", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Sostieni lo sviluppo di VoiceStudio", - "lead_default": "Sono contento che abbia funzionato! VoiceStudio è gratuito e completamente locale. Se ti fa risparmiare tempo, un piccolo chip-in mensile finanzia il Claude Max che sta dietro di esso.", - "lead_first_clone": "Il tuo primo clone vocale è pronto: fantastico! VoiceStudio funziona interamente sul tuo computer e il tuo supporto lo mantiene così.", - "lead_tenth_dub": "Dieci doppiaggi: lo stai chiaramente mettendo in pratica. Un piccolo chip-in mensile finanzia il Claude Max che fornisce queste funzionalità.", - "lead_sustained": "Lavori con VoiceStudio da un mese. Se si è guadagnato un posto nel tuo flusso di lavoro, valuta la possibilità di contribuire a finanziare il futuro.", - "chip_in": "Partecipa", - "later": "Forse più tardi", - "star": "Stella su GitHub", - "opt_out": "Non chiedermelo più", - "dismiss_aria": "Ignora" + "later": "Forse più tardi" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Licenza commerciale", "hero_title": "Spedisci voci AI in produzione", "hero_desc": "VoiceStudio è software libero e open source sotto licenza GNU Affero General Public License v3 (AGPL-3.0): gratuito per qualsiasi uso, incluso quello commerciale e aziendale interno. Una licenza commerciale serve solo se vuoi integrare VoiceStudio in un prodotto o servizio proprietario o closed-source senza gli obblighi copyleft della AGPL-3.0.", - "why_title": "Perché le aziende scelgono VoiceStudio", - "pricing_title": "Prezzi", - "faq_title": "Domande comuni", "pricing_desc": "Livelli e prezzi sono ancora in fase di definizione.", - "pricing_detail": "Fino a quando non saranno pubbliche, ogni implementazione commerciale verrà quotata individualmente in modo da poter dimensionare correttamente il tuo team e il tuo carico di lavoro.", "request_quote": "Richiedi un preventivo", - "footer_email": "Domande? Contatta hi@voicestudio.sh", - "footer_discord": "Unisciti al nostro Discord per il supporto della comunità.", "benefit_ip": "Proprietà IP completa", "benefit_ip_desc": "Possiedi ogni risultato, ogni modello, ogni derivato. Nessuna locazione condivisa, nessuna fuga di dati.", "benefit_cost": "Zero costi al minuto", "benefit_cost_desc": "Nessuna fattura API, nessuna eccedenza a sorpresa. Funziona sul tuo hardware fin dal primo giorno.", - "benefit_team": "Accesso a livello di team", "benefit_team_desc": "Una licenza copre un numero illimitato di utenti all'interno della tua organizzazione.", "benefit_support": "Supporto diretto", "benefit_support_desc": "Supporto prioritario via email e videochiamata da parte del team tecnico principale.", - "benefit_source": "Nucleo disponibile alla fonte", "benefit_source_desc": "Visibilità completa nello stack. Controlla, effettua il fork e adatta entro i termini della licenza.", - "benefit_lang": "646 lingue", "benefit_lang_desc": "Trascrivi, traduci e doppia in 646 lingue con una qualità a livello umano.", - "hero_note": "Uso, self-hosting e uso commerciale sono tutti gratuiti sotto AGPL-3.0, anche su larga scala. La AGPL è una licenza copyleft di rete: se modifichi VoiceStudio e offri quella versione modificata ad altri tramite rete, devi condividere il tuo codice sorgente modificato agli stessi termini. Una licenza commerciale rimuove questi obblighi copyleft per i deployment proprietari closed-source. I piani tariffari arriveranno presto: nel frattempo contattaci.", - "hero_simple": "VoiceStudio è gratuito e open source secondo AGPL-3.0, anche per uso commerciale e aziendale interno. Hai solo bisogno di una licenza commerciale per incorporarlo in un prodotto closed source senza gli obblighi di copyleft di AGPL.", - "contact_lead": "Il prezzo è indicato per distribuzione in modo che si adatti al tuo team e al carico di lavoro. Raccontami il tuo caso d'uso e ti farò un preventivo." + "hero_simple": "VoiceStudio è gratuito e open source secondo AGPL-3.0, anche per uso commerciale e aziendale interno. Hai solo bisogno di una licenza commerciale per incorporarlo in un prodotto closed source senza gli obblighi di copyleft di AGPL." }, "exportModal": { "export": "Esportazione", @@ -2006,7 +1915,6 @@ "loaded_ready": "Carico e pronto", "loading_first_run": "Caricamento... (l'operazione potrebbe richiedere 1-2 minuti alla prima esecuzione)", "failed_to_load": "Impossibile caricare", - "not_loaded_yet": "Non ancora caricato: verrà caricato alla prima trascrizione", "tts_not_loaded_yet": "Non ancora caricato: verrà caricato alla prima generazione vocale", "error_check_logs": "Errore: {{error}}. Controlla i log e prova a riavviare.", "check_logs_restart": "Controlla i log per eventuali errori di caricamento del modello. Prova a riavviare.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Casting dei relatori", - "auto_assign_title": "Assegna automaticamente le voci dai cloni degli altoparlanti estratti", - "auto_cast": "Trasmissione automatica", - "all_cast": "Tutto il cast", - "segments_count": "{{count}} segmenti", - "assign_voice": "Assegna voce...", - "from_video": "🎤 Dal video ({{name}})", - "no_profiles": "Nessun profilo vocale ancora salvato.", - "preview_voice": "Anteprima della voce" + "segments_count": "{{count}} segmenti" }, "checkpoint": { "timing_review": "{{count}} segmenti richiedono una verifica dei tempi", @@ -2203,8 +2104,6 @@ "play_failed": "Riproduzione non riuscita: {{message}}" }, "models": { - "hf_token_set_toast": "Set di token HuggingFace: download più rapidi abilitati", - "hf_token_save_failed": "Impossibile salvare il token", "install_started": "Installazione avviata: avanzamento nella riga", "delete_confirm": "Eliminare {{repoId}}? Puoi reinstallarlo più tardi.", "delete_confirm_title": "Elimina modello", @@ -2247,8 +2146,6 @@ "not_installed": "non installato", "required_tag": "richiesto", "delete_btn": "Elimina", - "hf_token_btn": "Gettone HF", - "hf_set_title": "Imposta il token HuggingFace per download più rapidi", "get_token": "Ottieni gettone →", "reco_installed_for": "Pacchetto consigliato installato per {{device}}", "reco_for": "Consigliato per {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Richiesto ~{{size}} GB", "all_size": "Tutti ~{{size}} GB", "req_tag": "rich", - "search_placeholder": "Cerca modelli…", - "search_label": "Cerca modelli", "no_matches": "Nessun modello corrisponde ai tuoi filtri.", "sort_by": "Ordina per {{column}}", "column_model": "Modello", @@ -2267,12 +2162,8 @@ "ready_badge": "Pronto", "loading_badge": "Caricamento…", "idle_badge": "Inattivo", - "started_downloading_required_one": "Avviato il download del modello richiesto {{count}}", - "started_downloading_required_other": "Iniziato il download dei {{count}} modelli richiesti", "to_download": "da scaricare", "cached": "memorizzato nella cache", - "fast_download_badge": "download veloce", - "fast_download_title": "Download veloci tramite Xet {{version}}: trasferimento parallelo in blocchi", "voice_previews": "Anteprime vocali", "voice_previews_desc": "Scarica anteprime già generate così le voci si ascoltano subito, anche prima che un modello vocale finisca di scaricarsi. Non viene scaricato nulla finché non attivi questa opzione.", "voice_previews_off": "Disattivato — le anteprime vengono generate su questo dispositivo", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Ho bisogno di una licenza per gli strumenti interni?", - "a_internal_tools": "No. L'uso da parte di dipendenti e collaboratori — inclusi modifica e self-hosting interni — è gratuito sotto AGPL-3.0. Una licenza commerciale serve solo se integri VoiceStudio in un prodotto o servizio proprietario o closed-source e non vuoi rispettare gli obblighi di condivisione del codice sorgente della AGPL.", - "q_try_before": "Posso provare prima di impegnarmi?", - "a_try_before": "Sì. L'app completa è gratuita da scaricare, eseguire e self-hostare sotto AGPL-3.0, senza alcun accordo. Quando vorrai parlare di una licenza commerciale (uso proprietario), scrivici e definiremo i dettagli insieme.", - "q_watermark": "E la filigrana?", - "a_watermark": "La filigrana invisibile AudioSeal viene incorporata per impostazione predefinita per tutti. Può disattivarla in Impostazioni → Privacy; riguarda solo l'audio generato dopo la modifica." - }, "gallery_extra": { - "save_prompt": "Inserisci un nome per questo profilo vocale:", - "saved_as_profile": "Voce salvata come profilo!", - "save_profile_failed": "Impossibile salvare il profilo", - "download_failed": "Download non riuscito: {{message}}", - "trim_load_failed": "Impossibile caricare l'audio per il taglio: {{message}}", - "upload_crop_failed": "Impossibile caricare la voce ritagliata: {{message}}" + "trim_load_failed": "Impossibile caricare l'audio per il taglio: {{message}}" }, "dub_workflow": { "preparing_audio": "Preparazione dell'audio…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Caricamento…", - "trimmed_loaded": "Audio tagliato caricato", - "toast_exported": "Esportato: {{name}}", - "toast_export_failed": "Esportazione non riuscita: {{message}}", - "toast_open_folder_failed": "Impossibile aprire la cartella: {{message}}", "toast_saving": "Salvataggio {{name}}...", "toast_saved": "Salvato: {{path}}", "toast_save_error": "Errore di salvataggio: {{message}}", "toast_processing": "Elaborazione {{name}}...", "toast_downloaded": "Scaricato {{name}}", "toast_download_error": "Errore di download: {{message}}", - "toast_upload_first": "Fai prima clic su \"Carica e trascrivi\" in modo che il video venga elaborato sul server prima di essere salvato.", - "toast_save_failed": "Salvataggio non riuscito: {{message}}", - "toast_opened": "Aperto: {{name}}", - "toast_project_deleted": "Progetto eliminato", - "toast_restored_state": "Stato della generazione precedente ripristinato", - "toast_history_deleted": "Elemento della cronologia eliminato", "toast_flushed": "Scaricato — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· modello scarico", "toast_flush_failed": "Eliminazione non riuscita: {{message}}", - "toast_project_saved": "Progetto salvato", - "toast_project_created": "Progetto creato" + "toast_project_saved": "Progetto salvato" }, "update": { "available": "Aggiornamento {{version}} disponibile", @@ -2531,13 +2398,11 @@ "not_writable": "non scrivibile", "change": "Cambia…", "compute_title": "Calcolo", - "compute_label": "GPU / acceleratore", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Canale di aggiornamento", "channel_stable": "Stabile", "channel_preview": "Anteprima (ultimo main)", - "network_title": "Rete", "region_label": "Area di download", "mirrors_title": "Mirror personalizzati (avanzato)", "mirror_pypi": "URL indice PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "Privacy prima di tutto: le tue voci, registrazioni e progetti non lasciano mai questa macchina — niente account, niente elaborazione nel cloud.", "resume_note": "I download interrotti riprendono da soli — chiudere l’app è sicuro.", "eta_left": "~{{eta}} rimanenti", - "first_sound_text": "Benvenuto nel tuo studio. Ogni parola che senti è stata generata su questa macchina, proprio ora.", - "first_sound_done": "Quella voce? Generata pochi secondi fa, in locale. Benvenuto.", - "hf_token_title": "Token Hugging Face (opzionale)", - "hf_token_hint": "Sblocca i modelli riservati — es. diarizzazione dei parlanti per il doppiaggio multivoce (pyannote). Token di account gratuito; resta su questa macchina.", "hf_token_save": "Salva", "hf_token_saving": "salvataggio…", "hf_token_saved": "Token Hugging Face salvato", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} a sinistra", "hf_token_card_title": "Aggiungi un token Hugging Face gratuito per download più rapidi", "hf_token_get": "Non hai un gettone? Ottienine uno gratis →", - "hf_token_saved_fast": "Token del volto abbracciato salvato: i download ora sono più veloci", "hf_token_inline_prompt": "Velocizza i download con un token Hugging Face gratuito", "hf_token_inline_ph": "Incolla il token hf_… (facoltativo)", "hf_token_get_short": "Ottienine uno gratis →", @@ -2607,15 +2467,11 @@ "unstar_take": "Rimuovi stella — consenti la pulizia", "play_take": "Carica come output attivo", "filter_starred": "Con stella", - "load_take_failed": "Impossibile caricare il take: {{message}}", "mode_clone": "Clonazione", "mode_synth": "Sintesi", "generation_seconds": "{{duration}} s", - "expand_text": "Espandi testo", - "collapse_text": "Comprimi testo", "seed": "Seed {{seed}}", - "load_config": "Carica impostazioni", - "delete_confirm": "Eliminare questo elemento dalla cronologia?" + "load_config": "Carica impostazioni" }, "openapi": { "title": "API VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Anteprima sponsor", "message": "Il tuo marchio, sito e messaggio", "email_app": "Apri l’app email", - "copy_email": "Copia email", - "preview_detail": "Qui potrebbero apparire il tuo logo, link e presentazione." + "copy_email": "Copia email" }, "supportPlans": { "remove": "Rimuovi barra sponsor", "title": "Gratis o Pro", "free": "Gratis", "pro": "Pro", - "sponsor_bar": "Barra sponsor", "visible": "Visibile", - "hideable": "Può essere nascosta", "unavailable": "L’attivazione di Pro non è ancora configurata.", "telemetry": "Telemetria", - "opt_in": "Facoltativa, con consenso", "disabled": "Disattivata", - "badge": "Badge Pro", - "advanced": "Strumenti avanzati", - "standard": "Standard", - "included": "Inclusi" + "badge": "Badge Pro" }, "integrationCatalog": { "title": "Integrazioni", @@ -3042,8 +2891,7 @@ "n8nHint": "Importa questo flusso in n8n, modifica la richiesta ed eseguilo. Mantieni VoiceStudio aperto. Docker e n8n remoto richiedono un indirizzo backend raggiungibile e le credenziali necessarie." }, "directoryExamples": { - "example": "Esempio di catalogo", - "notice": "Solo esempi di catalogo: non sponsor né integrazioni collegate." + "example": "Esempio di catalogo" }, "workflowRun": { "run": "Esegui flusso", diff --git a/electron/src/shared/i18n/locales/ja.json b/electron/src/shared/i18n/locales/ja.json index 879681829..a5b7f41ca 100644 --- a/electron/src/shared/i18n/locales/ja.json +++ b/electron/src/shared/i18n/locales/ja.json @@ -59,15 +59,10 @@ "title": "VoiceStudio Agent に相談", "notInstalled": "未インストール", "noSource": "エージェントが修復できるよう、書き込み可能な VoiceStudio のソースチェックアウトを開いてください。", - "noneFound": "ローカル修復を使うには Codex、Claude Code、OpenCode、または Pi をインストールしてください。", "ready": "準備完了", "placeholder": "何が起きましたか?手順、表示されたエラー、期待する動作を入力してください。", - "prReady": "修復が完了しました。変更を確認し、対象テストが通ったら PR を提案してください。", "diagnose": "診断", "fix": "修復", - "chooseDefault": "この画面がクラッシュしたときにVoiceStudioが自動で使うエージェントを選択します。", - "useAutomatically": "{{agent}}を自動的に使用", - "captured": "問題のあるログを{{count}}行取得しました", "contextNotice": "選択したエージェントには、この報告、現在の画面、最近のアプリとバックエンドのログ、システム診断が渡されます。", "complete": "完了" }, @@ -607,7 +602,6 @@ "characters": "{{count}} 文字", "minutes": "~{{count}} 分", "chars": "{{count}} 文字", - "exportDone": "オーディオブックをダウンロードしました", "exportFailed": "エクスポートに失敗しました — バックエンドが実行中であることを確認してください。", "global_speed": "速度", "global_speed_hint": "独自の速度オーバーライドを行わないすべての行の読み取り速度", @@ -818,7 +812,6 @@ "data_dir": "データディレクトリ", "outputs": "出力", "crash_log": "クラッシュログ", - "update_endpoint": "エンドポイントの更新", "yes": "はい", "no": "いいえ", "web_preview": "ウェブプレビュー", @@ -879,38 +872,21 @@ "title": "資格情報", "desc": "API キーとトークンは、このセッションに対してのみ <1> に設定されます。再起動後の永続性を維持するには、シェル プロファイルで環境変数として設定します。", "hf_token": "ハグフェイストークン", - "hf_help": "スピーカーのダイアライゼーションとモデルのダウンロードを高速化するために必要です。ハグフェイス.co/settings/tokens から入手してください。", - "translate_key": "LLM API キー", - "translate_help": "オプション — DeepL、OpenAI、または有料翻訳プロバイダーの場合。 Google 翻訳には必要ありません(無料枠)。", "save": "保存", "saved": "✓ 設定する", - "not_set": "✗ 未設定", "get_token": "トークンを取得 →", - "saved_session": "{{key}} がこのセッション用に保存されました", "save_failed": "保存に失敗しました", "save_error": "保存に失敗しました: {{message}}", "deepl_key": "DeepL APIキー", - "deepl_help": "DeepL翻訳エンジン用。 deepl.com/pro-api でキーを取得します。", "deepl_base_url": "DeepL ベース URL", "deepl_base_url_help": "カスタム DeepL API エンドポイント (デフォルト: https://api.deepl.com/v2)。プロキシ/セルフホスト用に変更します。", "microsoft_key": "Microsoft トランスレータ キー", - "microsoft_help": "Microsoft 翻訳エンジン用。 Azure Portal からキーを取得します。", "microsoft_base_url": "Microsoft トランスレータのベース URL", "microsoft_base_url_help": "カスタム Azure Translator エンドポイント (既定: https://api.cognitive.microsofttranslator.com)。", - "llm_base_url": "LLM ベース URL", - "llm_base_url_help": "映画のような品質または LLM エンジン用。任意の OpenAI 互換エンドポイント (OpenAI、Ollama、LM Studio など)。", - "llm_model": "LLMモデル", - "llm_model_help": "LLM エンジンで使用するモデル名 (gpt-4o、deepseek-chat、qwen2.5 など)。", - "group_huggingface": "ハグ顔", - "group_translate": "翻訳 (LLM / 映画)", - "group_deepl": "DeepL カスタム エンドポイント", - "group_microsoft": "Microsoft 翻訳者のカスタム エンドポイント", "region": "リージョン", "aws_profile": "AWSプロファイル" }, "engines": { - "banners_on": "ステージバナーが貼られています", - "banners_off": "ステージバナーが消えた", "backend": "バックエンド", "status": "ステータス", "ready": "準備完了", @@ -941,7 +917,6 @@ "dictation_model_hint": "ライブディクテーションと、このエンジンでの文字起こしに使用されます。", "dictation_not_installed": "未インストール — 選択してダウンロード", "active": "アクティブな", - "whyUnavailable": "必要なもの", "diskDetails": "ディスクの詳細", "diskModelDownload": "モデルのダウンロード", "diskPackageDownload": "パッケージのダウンロード", @@ -965,11 +940,8 @@ "sectionReady": "すぐに使える", "sectionMore": "エンジンを追加", "lastError": "最後のエラー: {{error}}", - "installedAndReady": "インストールされて準備完了", - "notInstalled": "インストールされていません", "available": "利用可能", "subprocessTitle": "独自のサブプロセス + venv で実行", - "inProcessTitle": "VoiceStudio Python プロセスで実行されます", "testEngine": "テストエンジン", "testing": "テスト中…", "recheck": "再確認", @@ -986,7 +958,6 @@ "routingUnknown": "不明", "routingEffectiveChip": "このマシンの {{device}} で実行されます", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU が選択されましたが: {{reason}}", "selectCpuFallback": "{{engine}}: CPU で実行中 — {{reason}}", "selectWithCaveat": "{{engine}} に切り替えました — {{reason}}", "generateCaveat": "このマシンでは {{engine}} の生成が遅くなる可能性があります — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Tauri ログは利用できません。デスクトップ シェルでのみ実行されます。", "empty_backend": "実行時ログが空です。バックエンドがログを記録すると、アクティビティがここに表示されます。", "title": "ログ", - "source_backend": "バックエンド", - "source_frontend": "フロントエンド", - "source_tauri": "タウリ", "expand": "ログを展開する", "collapse": "ログを折りたたむ", "expand_aria": "ログパネルを展開します", @@ -1104,12 +1072,10 @@ "unlock": "ロックを解除する", "try_voice": "この声を試してみてください", "test_phrase": "テストフレーズ", - "test_help": "何かを入力してください。この音声の現在の設定で生成されます。", "test_placeholder": "何かを入力すると、この声がそれを言うのが聞こえます...", "gen_preview": "プレビューの生成", "generating": "生成中…", "used_title": "この音声が使用された場所", - "used_empty": "この音声はまだ使用されていません。上でプレビューを生成するか、ダビングで選択します。", "synth_clips_one": "{{count}} シンセ クリップ", "synth_clips_other": "{{count}} シンセ クリップ", "projects_count_one": "{{count}} プロジェクト", @@ -1152,9 +1118,7 @@ "translate": "翻訳する", "generate": "生成する", "export": "エクスポート", - "no_video": "ビデオがロードされていません", "segments": "セグメント", - "speakers": "スピーカー", "video_dubbing_studio": "ビデオダビングスタジオ", "drop_here": "ここにビデオまたはオーディオをドロップします", "paste_url": "…または YouTube / ビデオの URL を貼り付けます", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Cookie のエクスポートは 1 MB 以下にしてください。", "save": "保存", "reset": "リセット", - "save_project": "プロジェクトの保存", "change_file": "ファイルを変更する", "import_srt": ".srtをインポートする", "import_srt_alt": "代わりに .srt をインポートしてください", @@ -1186,9 +1149,6 @@ "retranslate": "再翻訳", "transcript": "成績証明書", "generate_dub": "ダブの生成", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "アップロードと文字起こし", "multi_lang": "多言語対応", "prep_download": "ビデオをダウンロード中…", @@ -1206,7 +1166,6 @@ "transcribing": "音声を文字起こし中…", "elapsed": "経過した", "remaining": "残り", - "sidebar_toggle": "サイドバーの切り替え", "retry_transcription": "転写を再試行する", "supported_formats": "MP4・MOV・MKV・WEBM・MP3・WAV・FLAC・M4A", "pull_captions_title": "URL がキャプション付きホスト (YouTube、Vimeo、TED など) の場合は、元のキャプションと YouTube の自動翻訳も取得します。 Whisper を実行せずにエディターをシードします。 YouTube がすでにカバーしている言語については、「すべて翻訳」をスキップしてください。", @@ -1249,7 +1208,6 @@ "engine_label": "エンジン", "install_engine": "このエンジンをインストールする", "installing_engine": "…インストール中", - "needs_dev_install": "開発者のインストールが必要です", "install_disabled_title": "パッケージ化されたビルドではインストールが無効になっています", "needs_install_suffix": "— インストールが必要です", "glossary_title": "繰り返し現れる用語 (名前、ブランド単語、専門用語) の翻訳をピン留めする", @@ -1283,7 +1241,6 @@ "original_track": "オリジナル", "selected_dub": "{{code}} (選択されたダブ)", "dub_track": "{{code}} (ダブ)", - "export_tracks": "トラックのエクスポート:", "stopping": "停止中…", "stop_progress": "停止 ({{current}}/{{total}})", "regen_changed": "リジェネ {{count}} が変更されました", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} がインストールされました", "install_failed": "インストールに失敗しました: {{message}}", "prep_elapsed": "{{time}} 経過しました", - "add_language": "言語を追加する", "search_languages": "言語を検索…", "languages_selected_one": "{{count}} 言語が選択されました", "languages_selected_other": "{{count}} 言語が選択されました", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "時間", - "spkr": "スプクル", "text": "テキスト", "lang": "ラング", "voice": "声", @@ -1403,8 +1358,6 @@ "select_all_title": "フィルタリングされたすべてを選択", "sel_count": "{{count}} 選択", "select_title": "セグメントを選択 (範囲を選択するには Shift+クリック)", - "sync_title": "生成された音声の長さは元の音声の {{pct}}% です", - "sync_label": "同期: {{pct}}%", "rate_title": "音声速度の適合: {{ratio}}× (スロット{{error}} に対して)", "speaker_id": "スピーカーID", "translate_error_title": "翻訳エラー: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "元のテキストを復元する", "lang_default": "(防御)", "voice_default": "デフォルト", - "from_video": "ビデオから", - "clone_profiles": "プロファイルのクローンを作成する", - "design_presets": "デザインプリセット", "preview_title": "ライブプレビュー", "set_direction": "方向を設定…", "edit_direction": "方向を編集…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "性格", - "pick_personality": "性格のプリセットを選択してください…", "instruct": "指示する", "reference_text": "参考テキスト", "language": "言語", @@ -1535,9 +1484,7 @@ "stories": "ストーリー", "untitled_story": "題名のない物語", "story_lines": "{{count}}行", - "story_lines_plural": "{{count}} 行", "story_voices": "{{count}} の声", - "story_voices_plural": "{{count}} の声", "audiobooks": "オーディオブック", "audiobook": "オーディオブック", "story": "ストーリー", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio ギャラリー", - "search_placeholder": "YouTube を検索…", - "all_voices": "すべての声 ({{count}})", - "no_voices": "まだ声はありません", "download_hint": "上で検索してクリップをダウンロードしてください", - "search_btn": "検索", - "download_btn": "ダウンロード", "saving": "保存中…", "trim": "トリム", - "delete_confirm": "このボイスクリップを削除しますか?", - "view_mode_list": "リスト", - "view_mode_grid": "グリッド", - "youtube_results": "YouTube の結果 ({{count}})", - "clone_profile": "プロファイルのクローンを作成する", - "crop_audio": "オーディオをトリミングする", "subtitle": "何百もの既製のデザインされた音声 - 1 つを選んですぐに使用できます。", "zone_archetypes": "原型", "zone_imports": "私の輸入品", @@ -1658,8 +1594,6 @@ "delete": "削除", "community_empty": "コミュニティの音声はまだ読み込まれていません — インターネットに接続して開き直すか、最初の投稿者になりましょう。", "community_explainer": "コミュニティが共有したデザイン済みプリセットと録音音声です。omnivoice-gallery から読み込まれます。", - "no_designer": "録音された音声です — 代わりに「音声を使用する」をお使いください。", - "no_preview": "プレビューはありません — 「音声を使用する」で追加すると聞けます。", "submit_failed": "投稿フォームを開けませんでした。", "submit_preset": "プリセットを投稿", "submit_voice": "音声を投稿", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "最初の音声を作成する準備ができました。音声入力は今すぐ、または後から設定で有効にできます。", - "welcome": "ようこそ", "system_check": "システムチェック", "install_models": "モデルのインストール", - "pick_engines": "エンジンを選択する", "system_check_desc": "RAM、ディスク、GPU、ffmpeg、およびネットワークを調査します。ブロッカーには事前にフラグが付けられるため、ダウンロードする前にわかります。", "install_models_desc": "約 2.4 GB のウェイトをダウンロードします — 必須なのは TTS モデルのみです。ハードウェアに合わせた文字起こしモデルは、今すぐ、または後から設定で追加できます。", "pick_engines_desc": "TTS / ASR / LLM バックエンドを選択します。デフォルトはそのまま使用できます。設定でいつでもカスタマイズできます。", "hero_desc": "ダビング、音声クローン、音声デザインはすべてマシン上でローカルに実行されます。", - "first_run": "最初の実行のダウンロードには 5 ~ 10 分かかります。その後は、すべての起動が瞬時に行われ、完全にオフラインになります。", - "get_started": "始めましょう", "back": "戻る", "continue_ok": "大丈夫です — 続けてください", "continue_warn": "続行 (警告あり)", @@ -1727,7 +1657,6 @@ "try_dictation": "ディクテーションを試してみる", "try_dictation_desc": "セットアップを完了する前に、ショートカットを一度押してディクテーションが機能することを確認してください。", "dictation_step_label": "ディクテーション", - "next_try_dictation": "次へ: ディクテーションを試してみる", "step_aria": "ステップ {{num}}: {{label}}", "step_completed": "完了", "cache_label": "モデルキャッシュ" @@ -1743,7 +1672,6 @@ "platforms": "プラットフォーム", "footer": "あらゆる貢献は、ローカル AI の限界を押し広げるのに役立ちます。", "back": "スタジオに戻る", - "commercial": "商用ライセンス", "pill": "オムニボイスのサポート", "suggested_title": "金額を選択してください", "most_common": "最も一般的な", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "VoiceStudio の開発を支援", - "lead_default": "うまくいってよかったです! VoiceStudio は無料で完全にローカルです。時間を節約できるのであれば、毎月少額のチップインがその背後にいるクロード・マックスに資金を提供します。", - "lead_first_clone": "最初の音声クローンが完成しました。いいですね! VoiceStudio は完全にマシン上で実行され、サポートがその状態を維持します。", - "lead_tenth_dub": "10 個のダブが入っています。明らかに効果を発揮しています。これらの機能を提供する Claude Max には、毎月少額のチップインが資金として提供されます。", - "lead_sustained": "VoiceStudio を利用して 1 か月になります。ワークフローにそれが組み込まれている場合は、次の活動に資金を提供することを検討してください。", - "chip_in": "チップイン", - "later": "たぶん後で", - "star": "GitHub でスターを付ける", - "opt_out": "二度と聞かないでください", - "dismiss_aria": "閉じる" + "later": "たぶん後で" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "商用ライセンス", "hero_title": "本番環境で AI 音声を出荷", "hero_desc": "VoiceStudio は GNU Affero General Public License v3(AGPL-3.0)の下で提供される自由なオープンソースソフトウェアです。商用利用や社内業務利用を含め、無料でご利用いただけます。商用ライセンスが必要になるのは、AGPL-3.0 のコピーレフト義務なしに VoiceStudio をクローズドソースまたはプロプライエタリな製品・サービスに組み込みたい場合のみです。", - "why_title": "企業が VoiceStudio を選ぶ理由", - "pricing_title": "価格設定", - "faq_title": "よくある質問", "pricing_desc": "レベルと価格はまだ最終決定中です。", - "pricing_detail": "公開されるまで、すべての商用展開は個別に見積もられるため、お客様のチームとワークロードに合わせて適切な規模を設定できます。", "request_quote": "見積もりを依頼する", - "footer_email": "質問がありますか? hi@voicestudio.sh までご連絡ください。", - "footer_discord": "コミュニティサポートのために Discord に参加してください。", "benefit_ip": "完全な IP 所有権", "benefit_ip_desc": "あなたはすべての出力、すべてのモデル、すべての派生製品を所有します。共有テナントやデータ漏洩はありません。", "benefit_cost": "分当たりのコストはゼロ", "benefit_cost_desc": "API の請求や、予期せぬ超過料金は発生しません。初日から独自のハードウェアで実行できます。", - "benefit_team": "チーム全体のアクセス", "benefit_team_desc": "1 つのライセンスで、組織内の無制限のユーザーをカバーできます。", "benefit_support": "直接サポート", "benefit_support_desc": "コアエンジニアリングチームからの優先的な電子メールおよびビデオ通話サポート。", - "benefit_source": "ソースが利用可能なコア", "benefit_source_desc": "スタックを完全に可視化します。ライセンス条項内で監査、フォーク、適応を行います。", - "benefit_lang": "646の言語", "benefit_lang_desc": "人間レベルの品質で 646 言語の文字起こし、翻訳、吹き替えを行います。", - "hero_note": "利用、セルフホスティング、商用利用はすべて AGPL-3.0 の下で無料です(大規模利用も含む)。AGPL はネットワーク・コピーレフト型ライセンスです。VoiceStudio を改変し、その改変版をネットワーク経由で他者に提供する場合、改変したソースコードを同じ条件で公開する必要があります。商用ライセンスを取得すると、プロプライエタリ/クローズドソースの展開についてこのコピーレフト義務が免除されます。料金プランは近日公開予定です。それまでの間はお問い合わせください。", - "hero_simple": "VoiceStudio は、AGPL-3.0 に基づく無料のオープンソースであり、商用および社内ビジネスでの使用も含まれます。 AGPL のコピーレフト義務なしで、クローズドソース製品に埋め込むには商用ライセンスのみが必要です。", - "contact_lead": "価格は展開ごとに見積もられるため、チームとワークロードに適合します。使用例を教えてください。見積もりを出します。" + "hero_simple": "VoiceStudio は、AGPL-3.0 に基づく無料のオープンソースであり、商用および社内ビジネスでの使用も含まれます。 AGPL のコピーレフト義務なしで、クローズドソース製品に埋め込むには商用ライセンスのみが必要です。" }, "exportModal": { "export": "エクスポート", @@ -2006,7 +1915,6 @@ "loaded_ready": "ロードされて準備完了", "loading_first_run": "読み込み中… (初回実行時は 1 ~ 2 分かかる場合があります)", "failed_to_load": "ロードに失敗しました", - "not_loaded_yet": "まだロードされていません - 最初の文字起こし時にロードされます", "tts_not_loaded_yet": "未ロード — 初回の音声生成時にロードされます", "error_check_logs": "エラー: {{error}}。ログを確認して再起動してみてください。", "check_logs_restart": "モデルの読み込みエラーがないかログを確認します。再起動してみてください。", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "スピーカーの鋳造", - "auto_assign_title": "抽出されたスピーカークローンから音声を自動割り当て", - "auto_cast": "オートキャスト", - "all_cast": "オールキャスト", - "segments_count": "{{count}} セグメント", - "assign_voice": "音声を割り当て…", - "from_video": "🎤 動画より ({{name}})", - "no_profiles": "音声プロファイルはまだ保存されていません。", - "preview_voice": "プレビュー音声" + "segments_count": "{{count}} セグメント" }, "checkpoint": { "timing_review": "{{count}} 個のセグメントでタイミングの確認が必要です", @@ -2203,8 +2104,6 @@ "play_failed": "プレイに失敗しました: {{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace トークン セット - より高速なダウンロードが可能", - "hf_token_save_failed": "トークンの保存に失敗しました", "install_started": "インストールが開始されました - 進行状況", "delete_confirm": "{{repoId}} を削除しますか?後で再インストールできます。", "delete_confirm_title": "モデルの削除", @@ -2247,8 +2146,6 @@ "not_installed": "インストールされていません", "required_tag": "必須", "delete_btn": "削除", - "hf_token_btn": "HFトークン", - "hf_set_title": "ダウンロードを高速化するために HuggingFace トークンを設定する", "get_token": "トークンを取得 →", "reco_installed_for": "{{device}} にインストールされている推奨バンドル", "reco_for": "{{device}} に推奨", @@ -2256,8 +2153,6 @@ "required_size": "必要 ~{{size}} GB", "all_size": "すべて ~{{size}} GB", "req_tag": "要求", - "search_placeholder": "モデルを検索…", - "search_label": "モデルを検索する", "no_matches": "フィルターに一致するモデルはありません。", "sort_by": "{{column}} で並べ替え", "column_model": "モデル", @@ -2267,12 +2162,8 @@ "ready_badge": "準備完了", "loading_badge": "読み込み中…", "idle_badge": "アイドル状態", - "started_downloading_required_one": "{{count}} 必須モデルのダウンロードを開始しました", - "started_downloading_required_other": "{{count}} 必須モデルのダウンロードを開始しました", "to_download": "ダウンロードする", "cached": "キャッシュされた", - "fast_download_badge": "高速ダウンロード", - "fast_download_title": "Xet {{version}} による高速ダウンロード — 並列チャンク転送", "voice_previews": "ボイスプレビュー", "voice_previews_desc": "事前にレンダリングされたプレビューをダウンロードすると、音声モデルのダウンロードが終わる前でも声をすぐに再生できます。オンにするまで何もダウンロードされません。", "voice_previews_off": "オフ — プレビューはこの端末で生成されます", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "内部ツールにはライセンスが必要ですか?", - "a_internal_tools": "いいえ。従業員や委託先による利用(社内での改変やセルフホスティングを含む)は AGPL-3.0 の下で無料です。商用ライセンスが必要なのは、VoiceStudio をクローズドソースまたはプロプライエタリな製品・サービスに組み込み、AGPL のソースコード公開義務に従いたくない場合のみです。", - "q_try_before": "コミットする前に試してみることはできますか?", - "a_try_before": "はい。アプリ全体を AGPL-3.0 の下で無料でダウンロード・実行・セルフホストでき、契約は不要です。商用(プロプライエタリ利用)ライセンスについて相談する準備ができましたら、メールでご連絡ください。詳細を一緒に詰めていきましょう。", - "q_watermark": "透かしについてはどうですか?", - "a_watermark": "不可視の AudioSeal 透かしは、既定ですべてのユーザーに埋め込まれます。設定 → プライバシーで無効にできます。変更後に生成された音声にのみ適用されます。" - }, "gallery_extra": { - "save_prompt": "この音声プロファイルの名前を入力してください:", - "saved_as_profile": "音声をプロフィールとして保存しました!", - "save_profile_failed": "プロファイルの保存に失敗しました", - "download_failed": "ダウンロードに失敗しました: {{message}}", - "trim_load_failed": "トリミング用のオーディオのロードに失敗しました: {{message}}", - "upload_crop_failed": "トリミングされた音声のアップロードに失敗しました: {{message}}" + "trim_load_failed": "トリミング用のオーディオのロードに失敗しました: {{message}}" }, "dub_workflow": { "preparing_audio": "オーディオを準備しています…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "読み込み中…", - "trimmed_loaded": "トリミングされたオーディオがロードされました", - "toast_exported": "エクスポート済み: {{name}}", - "toast_export_failed": "エクスポートに失敗しました: {{message}}", - "toast_open_folder_failed": "フォルダーを開けませんでした: {{message}}", "toast_saving": "{{name}} を保存しています...", "toast_saved": "保存済み: {{path}}", "toast_save_error": "保存エラー: {{message}}", "toast_processing": "{{name}} を処理しています...", "toast_downloaded": "{{name}} をダウンロードしました", "toast_download_error": "ダウンロード エラー: {{message}}", - "toast_upload_first": "まず「アップロードと文字起こし」をクリックして、ビデオが保存前にサーバー上で処理されるようにしてください。", - "toast_save_failed": "保存に失敗しました: {{message}}", - "toast_opened": "オープン済み: {{name}}", - "toast_project_deleted": "プロジェクトが削除されました", - "toast_restored_state": "前世代の状態を復元", - "toast_history_deleted": "履歴項目が削除されました", "toast_flushed": "フラッシュ — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· モデルがアンロードされました", "toast_flush_failed": "フラッシュに失敗しました: {{message}}", - "toast_project_saved": "プロジェクトが保存されました", - "toast_project_created": "プロジェクトが作成されました" + "toast_project_saved": "プロジェクトが保存されました" }, "update": { "available": "アップデート {{version}} が利用可能です", @@ -2531,13 +2398,11 @@ "not_writable": "書き込み不可", "change": "変更…", "compute_title": "コンピュート", - "compute_label": "GPU / アクセラレーター", "compute_auto": "自動(NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD GPU(ROCm、Linux)", "channel_label": "更新チャンネル", "channel_stable": "安定版", "channel_preview": "プレビュー(最新 main)", - "network_title": "ネットワーク", "region_label": "ダウンロード地域", "mirrors_title": "カスタムミラー(上級者向け)", "mirror_pypi": "PyPI インデックス URL", @@ -2570,10 +2435,6 @@ "trust_line": "プライバシー第一。音声・録音・プロジェクトがこのマシンから出ることはありません。アカウント不要、クラウド処理なし。", "resume_note": "中断したダウンロードは自動的に再開されます。アプリを閉じても安全です。", "eta_left": "残り約 {{eta}}", - "first_sound_text": "あなたのスタジオへようこそ。いま聞こえている言葉はすべて、たった今このマシンで生成されたものです。", - "first_sound_done": "この声、数秒前にローカルで生成されました。ようこそ。", - "hf_token_title": "Hugging Face トークン(任意)", - "hf_token_hint": "制限付きモデルを解放します。例:複数話者ダビング用の話者分離(pyannote)。無料アカウントのトークンで、このマシンにのみ保存されます。", "hf_token_save": "保存", "hf_token_saving": "保存中…", "hf_token_saved": "Hugging Face トークンを保存しました", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} 残っています", "hf_token_card_title": "ダウンロードを高速化するために無料の Hugging Face トークンを追加します", "hf_token_get": "トークンをお持ちでない場合は、 1 つ無料で入手 →", - "hf_token_saved_fast": "ハグフェイストークンが保存されました — ダウンロードが速くなりました", "hf_token_inline_prompt": "無料の Hugging Face トークンでダウンロードを高速化", "hf_token_inline_ph": "hf_… トークンを貼り付けます (オプション)", "hf_token_get_short": "1 つ無料で入手 →", @@ -2607,15 +2467,11 @@ "unstar_take": "スター解除 — クリーンアップを許可", "play_take": "アクティブな出力として読み込む", "filter_starred": "スター付き", - "load_take_failed": "テイクを読み込めませんでした: {{message}}", "mode_clone": "クローン", "mode_synth": "音声合成", "generation_seconds": "{{duration}}秒", - "expand_text": "テキストを展開", - "collapse_text": "テキストを折りたたむ", "seed": "シード {{seed}}", - "load_config": "設定を読み込む", - "delete_confirm": "この履歴項目を削除しますか?" + "load_config": "設定を読み込む" }, "openapi": { "title": "VoiceStudio API", @@ -3015,25 +2871,18 @@ "preview": "スポンサーの表示例", "message": "ブランド名・ウェブサイト・メッセージ", "email_app": "メールアプリを開く", - "copy_email": "メールアドレスをコピー", - "preview_detail": "ここにロゴ、リンク、紹介文を掲載できます。" + "copy_email": "メールアドレスをコピー" }, "supportPlans": { "remove": "スポンサー欄を非表示にする", "title": "無料版とProの比較", "free": "無料", "pro": "Pro", - "sponsor_bar": "スポンサー欄", "visible": "表示", - "hideable": "非表示にできます", "unavailable": "Proの有効化はまだ設定されていません。", "telemetry": "テレメトリ", - "opt_in": "任意・同意が必要", "disabled": "無効", - "badge": "Proバッジ", - "advanced": "高度なツール", - "standard": "標準", - "included": "利用可能" + "badge": "Proバッジ" }, "integrationCatalog": { "title": "連携", @@ -3042,8 +2891,7 @@ "n8nHint": "このワークフローをn8nにインポートし、リクエストを編集して実行します。VoiceStudioは起動したままにしてください。Dockerやリモートのn8nには、接続可能なバックエンドのアドレスと必要な認証情報を設定してください。" }, "directoryExamples": { - "example": "掲載例", - "notice": "ディレクトリの掲載例です。スポンサーや接続済みの連携ではありません。" + "example": "掲載例" }, "workflowRun": { "run": "ワークフローを実行", diff --git a/electron/src/shared/i18n/locales/ko.json b/electron/src/shared/i18n/locales/ko.json index feaf62c35..f8ed53363 100644 --- a/electron/src/shared/i18n/locales/ko.json +++ b/electron/src/shared/i18n/locales/ko.json @@ -59,15 +59,10 @@ "title": "VoiceStudio Agent에게 물어보기", "notInstalled": "설치되지 않음", "noSource": "에이전트가 수정할 수 있도록 쓰기 가능한 VoiceStudio 소스 체크아웃을 여세요.", - "noneFound": "로컬 복구를 사용하려면 Codex, Claude Code, OpenCode 또는 Pi를 설치하세요.", "ready": "준비됨", "placeholder": "무엇이 잘못되었나요? 단계, 표시된 오류 또는 예상 동작을 추가하세요.", - "prReady": "수정이 완료되었습니다. 변경 사항을 검토하고 대상 테스트가 통과하면 PR을 제안하세요.", "diagnose": "진단", "fix": "수정", - "chooseDefault": "이 화면이 충돌할 때 VoiceStudio가 자동으로 사용할 에이전트를 선택하세요.", - "useAutomatically": "{{agent}} 자동 사용", - "captured": "문제 로그 {{count}}줄을 수집했습니다", "contextNotice": "선택한 에이전트는 이 보고서, 현재 화면, 최근 앱 및 백엔드 로그와 시스템 진단 정보를 받습니다.", "complete": "완료" }, @@ -835,7 +830,6 @@ "characters": "{{count}}명", "minutes": "~{{count}}분", "chars": "{{count}}자", - "exportDone": "오디오북 다운로드됨", "exportFailed": "내보내기 실패 - 백엔드가 실행 중인지 확인하세요.", "global_speed": "속도", "global_speed_hint": "자체 속도 재정의 없이 모든 라인에 대한 읽기 속도", @@ -1047,7 +1041,6 @@ "data_dir": "데이터 디렉터리", "outputs": "출력", "crash_log": "충돌 로그", - "update_endpoint": "업데이트 엔드포인트", "yes": "예", "no": "아니요", "web_preview": "웹 미리보기", @@ -1118,39 +1111,22 @@ "title": "자격 증명", "desc": "API 키와 토큰은 <1>이 세션에 대해서만 설정됩니다. 다시 시작해도 지속성을 유지하려면 셸 프로필에서 환경 변수로 설정하세요.", "hf_token": "HuggingFace 토큰", - "hf_help": "화자 분할 및 빠른 모델 다운로드에 필요합니다. huggingface.co/settings/tokens에서 발급받으세요.", - "translate_key": "LLM API 키", - "translate_help": "선택 사항 — DeepL, OpenAI 또는 유료 번역 제공업체의 경우. Google 번역(무료 등급)에는 필요하지 않습니다.", "save": "저장", "saved": "✓ 설정", - "not_set": "✗ 설정되지 않음", "get_token": "토큰 받기 →", - "saved_session": "이 세션을 위해 {{key}}이(가) 저장되었습니다", "save_failed": "저장하지 못했습니다.", "save_error": "저장 실패: {{message}}", "deepl_key": "DeepL API 키", - "deepl_help": "DeepL 번역 엔진용. deepl.com/pro-api에서 키를 받으세요.", "deepl_base_url": "DeepL 기본 URL", "deepl_base_url_help": "사용자 정의 DeepL API 엔드포인트(기본값: https://api.deepl.com/v2). 프록시/자체 호스팅으로 변경합니다.", "microsoft_key": "마이크로소프트 번역기 키", - "microsoft_help": "Microsoft 번역기 엔진용. Azure Portal에서 키를 가져옵니다.", "microsoft_base_url": "Microsoft 번역기 기본 URL", "microsoft_base_url_help": "사용자 지정 Azure Translator 엔드포인트(기본값: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "LLM 기본 URL", - "llm_base_url_help": "시네마틱 품질 또는 LLM 엔진용. 모든 OpenAI 호환 엔드포인트(OpenAI, Ollama, LM Studio 등).", - "llm_model": "LLM 모델", - "llm_model_help": "LLM 엔진과 함께 사용할 모델 이름(예: gpt-4o, deepseek-chat, qwen2.5)", - "group_huggingface": "HuggingFace", - "group_translate": "번역(LLM/시네마틱)", - "group_deepl": "DeepL 사용자 정의 엔드포인트", - "group_microsoft": "Microsoft 번역기 사용자 지정 엔드포인트", "saved_persisted": "{{key}} 저장됨", "region": "리전", "aws_profile": "AWS 프로필" }, "engines": { - "banners_on": "무대 배너 켜짐", - "banners_off": "무대 배너 꺼짐", "backend": "백엔드", "status": "상태", "ready": "준비", @@ -1181,7 +1157,6 @@ "dictation_model_hint": "실시간 받아쓰기와 이 엔진의 전사에 사용됩니다.", "dictation_not_installed": "설치되지 않음 — 선택하여 다운로드", "active": "활성", - "whyUnavailable": "필요한 것", "diskDetails": "디스크 세부 정보", "diskModelDownload": "모델 다운로드", "diskPackageDownload": "패키지 다운로드", @@ -1205,11 +1180,8 @@ "sectionReady": "바로 사용 가능", "sectionMore": "엔진 추가", "lastError": "마지막 오류: {{error}}", - "installedAndReady": "설치 및 준비 완료", - "notInstalled": "설치되지 않음", "available": "사용 가능", "subprocessTitle": "자체 하위 프로세스 + venv에서 실행됩니다.", - "inProcessTitle": "VoiceStudio Python 프로세스에서 실행됩니다.", "testEngine": "테스트 엔진", "testing": "테스트 중…", "recheck": "재확인", @@ -1226,7 +1198,6 @@ "routingUnknown": "알 수 없음", "routingEffectiveChip": "이 머신의 {{device}}에서 실행됩니다.", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU가 선택되었지만: {{reason}}", "selectCpuFallback": "{{engine}}: CPU에서 실행 중 — {{reason}}", "selectWithCaveat": "{{engine}}(으)로 전환했습니다 — {{reason}}", "generateCaveat": "이 컴퓨터에서는 {{engine}}이(가) 느릴 수 있습니다 — {{reason}}", @@ -1240,8 +1211,6 @@ "cloneCapableTitle": "짧은 참조 클립으로 음성을 복제할 수 있습니다", "colActions": "작업", "colEngine": "엔진", - "colGpuCompat": "GPU 호환성", - "colIsolation": "격리", "copied": "복사됨", "copy": "복사", "copySetup": "{{engine}} 설정 명령 복사", @@ -1319,9 +1288,6 @@ "empty_tauri": "Tauri 로그를 사용할 수 없습니다. 데스크톱 셸에서만 실행됩니다.", "empty_backend": "런타임 로그가 비어 있습니다. 백엔드가 기록할 때 활동이 여기에 표시됩니다.", "title": "로그", - "source_backend": "백엔드", - "source_frontend": "프런트엔드", - "source_tauri": "타우리", "expand": "로그 펼치기", "collapse": "로그 접기", "expand_aria": "로그 패널 확장", @@ -1395,12 +1361,10 @@ "unlock": "잠금 해제", "try_voice": "이 목소리를 들어보세요", "test_phrase": "테스트 문구", - "test_help": "아무거나 입력하면 이 음성의 현재 설정으로 생성됩니다.", "test_placeholder": "이 음성을 들으려면 뭔가를 입력하세요...", "gen_preview": "미리보기 생성", "generating": "생성 중…", "used_title": "이 목소리가 사용된 곳", - "used_empty": "이 음성은 아직 사용되지 않았습니다. 위에서 미리보기를 생성하거나 더빙에서 선택하세요.", "synth_clips_one": "{{count}}개의 합성 클립", "synth_clips_other": "{{count}}개의 합성 클립", "projects_count_one": "{{count}} 프로젝트", @@ -1441,9 +1405,7 @@ "translate": "번역", "generate": "생성", "export": "내보내기", - "no_video": "로드된 동영상이 없습니다.", "segments": "세그먼트", - "speakers": "화자", "video_dubbing_studio": "비디오 더빙 스튜디오", "drop_here": "여기에 비디오나 오디오를 드롭하세요.", "paste_url": "...또는 YouTube/동영상 URL을 붙여넣으세요.", @@ -1457,7 +1419,6 @@ "cookie_size_error": "쿠키 내보내기 파일은 1MB 이하여야 합니다.", "save": "저장", "reset": "재설정", - "save_project": "프로젝트 저장", "change_file": "파일 변경", "import_srt": ".srt 가져오기", "import_srt_alt": "대신 .srt를 가져오세요.", @@ -1475,9 +1436,6 @@ "retranslate": "재번역", "transcript": "전사본", "generate_dub": "더빙 생성", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "업로드 및 전사", "multi_lang": "다중언어", "prep_download": "동영상 다운로드 중…", @@ -1495,7 +1453,6 @@ "transcribing": "오디오 전사 중…", "elapsed": "경과", "remaining": "s 남음", - "sidebar_toggle": "사이드바 전환", "retry_transcription": "전사 재시도", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "URL이 캡션이 포함된 호스트(YouTube, Vimeo, TED…)인 경우 원본 캡션과 YouTube 자동 번역도 가져옵니다. Whisper를 실행하지 않고 편집기를 시드합니다. YouTube에서 이미 다루고 있는 언어는 모두 번역을 건너뛰세요.", @@ -1538,7 +1495,6 @@ "engine_label": "엔진", "install_engine": "이 엔진을 설치하세요", "installing_engine": "…설치 중", - "needs_dev_install": "개발자 설치 필요", "install_disabled_title": "패키지 빌드에서는 설치가 비활성화됩니다.", "needs_install_suffix": "— 설치 필요", "glossary_title": "반복되는 용어(이름, 브랜드 단어, 전문 용어)에 대한 번역 고정", @@ -1572,7 +1528,6 @@ "original_track": "원본", "selected_dub": "{{code}}(선택된 더빙)", "dub_track": "{{code}} (더빙)", - "export_tracks": "트랙 내보내기:", "stopping": "중지 중…", "stop_progress": "중지({{current}}/{{total}})", "regen_changed": "재생성 {{count}} 변경됨", @@ -1584,7 +1539,6 @@ "install_ok": "{{engine}} 설치됨", "install_failed": "설치 실패: {{message}}", "prep_elapsed": "{{time}} 경과", - "add_language": "언어 추가", "search_languages": "언어 검색…", "languages_selected_one": "{{count}} 언어 선택됨", "languages_selected_other": "{{count}} 언어 선택됨", @@ -1641,7 +1595,6 @@ "condense_label": "더 짧은 줄 제안", "condense_title": "번역된 줄이 시간 슬롯에 맞지 않을 것으로 예상되면, 세그먼트별로 적용할 수 있는 더 짧고 의미를 보존하는 재작성을 LLM에 요청합니다. 기본적으로 꺼져 있으며 LLM 설정이 필요합니다.", "hq_needs_llm_hint": "고품질 번역은 로컬 또는 클라우드 LLM을 사용하여 각 줄을 세그먼트 시간에 맞춥니다. 사용하려면 LLM을 설정하세요.", - "set_up_llm": "설정", "timing_concise": "간결하게", "timing_stretch_video": "영상 늘리기", "timing_strict_slot": "엄격한 슬롯", @@ -1690,7 +1643,6 @@ }, "segment": { "time": "시간", - "spkr": "화자", "text": "텍스트", "lang": "언어", "voice": "음성", @@ -1701,8 +1653,6 @@ "select_all_title": "필터링된 항목 모두 선택", "sel_count": "{{count}} 선택", "select_title": "세그먼트 선택(범위를 보려면 Shift+클릭)", - "sync_title": "생성된 오디오는 원본 길이의 {{pct}}%입니다.", - "sync_label": "동기화: {{pct}}%", "rate_title": "음성 속도 맞춤: 슬롯에 상대적인 {{ratio}}×{{error}}", "speaker_id": "화자 ID", "translate_error_title": "번역 오류: {{error}}", @@ -1713,9 +1663,6 @@ "restore_title": "원본 텍스트 복원", "lang_default": "(데프)", "voice_default": "기본값", - "from_video": "비디오에서", - "clone_profiles": "복제 프로필", - "design_presets": "디자인 사전 설정", "preview_title": "실시간 미리보기", "set_direction": "방향 설정…", "edit_direction": "방향 수정…", @@ -1772,7 +1719,6 @@ }, "voice": { "personality": "성격", - "pick_personality": "성격 사전 설정을 선택하세요…", "instruct": "지시", "reference_text": "참조 텍스트", "language": "언어", @@ -1833,9 +1779,7 @@ "stories": "스토리", "untitled_story": "제목없는 이야기", "story_lines": "{{count}} 라인", - "story_lines_plural": "{{count}} 라인", "story_voices": "{{count}} 목소리", - "story_voices_plural": "{{count}} 목소리", "audiobooks": "오디오북", "audiobook": "오디오북", "story": "스토리", @@ -1907,20 +1851,9 @@ }, "gallery": { "title": "VoiceStudio 갤러리", - "search_placeholder": "유튜브 검색…", - "all_voices": "모든 음색({{count}})", - "no_voices": "아직 음성이 없습니다.", "download_hint": "클립을 다운로드하려면 위에서 검색하세요.", - "search_btn": "검색", - "download_btn": "다운로드", "saving": "저장 중…", "trim": "다듬기", - "delete_confirm": "이 음성 클립을 삭제하시겠습니까?", - "view_mode_list": "목록", - "view_mode_grid": "그리드", - "youtube_results": "YouTube 결과({{count}})", - "clone_profile": "프로필 복제", - "crop_audio": "오디오 자르기", "subtitle": "수백 가지의 미리 만들어진 디자인 음성 중 하나를 선택하여 사용하세요.", "zone_archetypes": "원형", "zone_imports": "내가 가져온 항목", @@ -1956,8 +1889,6 @@ "delete": "삭제", "community_empty": "아직 로드된 커뮤니티 음성이 없습니다 — 인터넷에 연결한 뒤 다시 열거나, 첫 번째로 제출해 보세요.", "community_explainer": "커뮤니티가 공유한 디자인 프리셋과 녹음 음성으로, omnivoice-gallery에서 로드됩니다.", - "no_designer": "녹음된 음성입니다 — 대신 \"음성 사용\"을 이용하세요.", - "no_preview": "미리보기 없음 — \"음성 사용\"으로 추가하면 들을 수 있습니다.", "submit_failed": "제출 양식을 열 수 없습니다.", "submit_preset": "프리셋 제출", "submit_voice": "음성 제출", @@ -1996,16 +1927,12 @@ }, "setup": { "ready_desc": "첫 음성을 만들 준비가 되었습니다. 받아쓰기는 지금 또는 나중에 설정에서 구성할 수 있습니다.", - "welcome": "환영합니다", "system_check": "시스템 점검", "install_models": "모델 설치", - "pick_engines": "엔진 선택", "system_check_desc": "RAM, 디스크, GPU, ffmpeg 및 네트워크를 프로브합니다. 차단기는 미리 표시되어 있으므로 다운로드하기 전에 알 수 있습니다.", "install_models_desc": "약 2.4GB의 가중치 다운로드 — TTS 모델만 필수입니다. 하드웨어에 맞게 선별된 전사 모델은 지금 또는 나중에 설정에서 추가하세요.", "pick_engines_desc": "TTS/ASR/LLM 백엔드를 선택합니다. 기본값은 즉시 적용됩니다. 설정에서 언제든지 맞춤설정할 수 있습니다.", "hero_desc": "더빙, 음성 복제, 음성 디자인 등이 모두 컴퓨터에서 로컬로 실행됩니다.", - "first_run": "처음 실행하면 다운로드하는 데 5~10분 정도 걸립니다. 그 이후에는 모든 출시가 즉시 이루어지며 완전히 오프라인 상태가 됩니다.", - "get_started": "시작하기", "back": "뒤로", "continue_ok": "모두 괜찮습니다 — 계속하세요", "continue_warn": "계속(경고 포함)", @@ -2026,16 +1953,13 @@ "try_dictation": "받아쓰기를 시도해보세요", "try_dictation_desc": "설정을 마치기 전에 단축키를 한 번 눌러 받아쓰기가 작동하는지 확인하세요.", "dictation_step_label": "받아쓰기", - "next_try_dictation": "다음: 받아쓰기 사용해 보기", "step_aria": "{{num}}단계: {{label}}", "step_completed": "완료됨", "cache_label": "모델 캐시", "media_engine_choose_file": "파일 선택…", - "media_engine_detect_failed": "시스템에 설치된 사본을 찾지 못했습니다 — 다운로드를 다시 시도하거나 파일을 직접 선택하세요.", "media_engine_failed_desc": "앱이 번들 오디오/비디오 엔진(FFmpeg)을 가져오지 못했습니다. 다시 시도하거나 이 컴퓨터에 이미 있는 사본을 지정하세요.", "media_engine_failed_title": "미디어 엔진 다운로드 실패", "media_engine_preparing": "미디어 엔진 준비 중…", - "media_engine_ready": "미디어 엔진이 구성되었습니다.", "media_engine_retry": "다시 시도", "media_engine_use_system": "시스템 사본 사용", "mirror_apply": "적용 및 재확인", @@ -2054,7 +1978,6 @@ "platforms": "플랫폼", "footer": "모든 기여는 로컬 AI의 경계를 넓히는 데 도움이 됩니다.", "back": "스튜디오로 돌아가기", - "commercial": "상업용 라이선스", "pill": "VoiceStudio 지원", "suggested_title": "금액을 선택하세요", "most_common": "가장 일반적인", @@ -2072,15 +1995,7 @@ }, "postcard": { "title": "VoiceStudio 개발 지원", - "lead_default": "효과가 있어서 다행이에요! VoiceStudio는 무료이며 완전히 로컬입니다. 시간을 절약할 수 있다면 매월 소액의 칩인을 통해 Claude Max에 자금을 지원할 수 있습니다.", - "lead_first_clone": "첫 번째 음성 복제가 완료되었습니다. 좋습니다! VoiceStudio는 전적으로 귀하의 컴퓨터에서 실행되며 귀하의 지원은 이를 그대로 유지합니다.", - "lead_tenth_dub": "10개의 더빙이 포함되어 있습니다. 확실히 효과를 발휘하고 계십니다. 이러한 기능을 제공하는 Claude Max에는 매월 소액의 칩인 자금이 지원됩니다.", - "lead_sustained": "VoiceStudio를 사용한 지 한 달이 되었습니다. 귀하의 작업 흐름에서 한 자리를 차지했다면 다음 단계에 자금을 지원하는 것을 고려해 보십시오.", - "chip_in": "칩인", - "later": "어쩌면 나중에", - "star": "GitHub에서 스타 누르기", - "opt_out": "다시 묻지 마세요", - "dismiss_aria": "닫기" + "later": "어쩌면 나중에" } }, "enterprise": { @@ -2090,29 +2005,18 @@ "badge": "상업용 라이선스", "hero_title": "프로덕션에 AI 음성 전달", "hero_desc": "VoiceStudio는 GNU Affero General Public License v3(AGPL-3.0)에 따라 제공되는 자유 오픈소스 소프트웨어입니다. 상업적 이용과 사내 업무 이용을 포함해 무료로 사용할 수 있습니다. 상업용 라이선스는 AGPL-3.0의 카피레프트 의무 없이 VoiceStudio를 클로즈드 소스 또는 독점 제품·서비스에 포함하려는 경우에만 필요합니다.", - "why_title": "기업이 VoiceStudio를 선택하는 이유", - "pricing_title": "가격", - "faq_title": "일반적인 질문", "pricing_desc": "등급과 가격은 아직 확정 중입니다.", - "pricing_detail": "공개될 때까지 모든 상용 배포는 개별적으로 견적이 나오므로 귀하의 팀과 작업량에 맞게 규모를 조정할 수 있습니다.", "request_quote": "견적 요청", - "footer_email": "질문이 있으신가요? hi@voicestudio.sh로 문의하세요.", - "footer_discord": "커뮤니티 지원을 위해 Discord에 참여하세요.", "benefit_ip": "전체 IP 소유권", "benefit_ip_desc": "귀하는 모든 산출물, 모든 모델, 모든 파생물을 소유합니다. 공유 테넌시가 없고 데이터 유출도 없습니다.", "benefit_cost": "분당 비용이 없습니다.", "benefit_cost_desc": "API 청구서가 없고 예상치 못한 초과분도 없습니다. 첫날부터 자체 하드웨어에서 실행됩니다.", - "benefit_team": "팀 전체 액세스", "benefit_team_desc": "하나의 라이선스로 조직 내 사용자 수 제한 없이 이용할 수 있습니다.", "benefit_support": "직접 지원", "benefit_support_desc": "핵심 엔지니어링 팀의 우선 이메일 및 화상 통화 지원.", - "benefit_source": "소스 사용 가능한 코어", "benefit_source_desc": "스택에 대한 완전한 가시성. 라이선스 조건 내에서 감사, 분기 및 조정합니다.", - "benefit_lang": "646개 언어", "benefit_lang_desc": "인간 수준의 품질로 646개 언어로 전사, 번역, 더빙하세요.", - "hero_note": "사용, 자체 호스팅, 상업적 이용 모두 AGPL-3.0에 따라 무료이며, 대규모 사용도 마찬가지입니다. AGPL은 네트워크 카피레프트 라이선스입니다. VoiceStudio를 수정하여 그 수정 버전을 네트워크를 통해 다른 사람에게 제공하는 경우, 동일한 조건으로 수정된 소스 코드를 공개해야 합니다. 상업용 라이선스를 구매하면 독점·클로즈드 소스 배포에 대해 이러한 카피레프트 의무가 면제됩니다. 가격 정책은 곧 공개됩니다. 그동안은 문의해 주세요.", - "hero_simple": "VoiceStudio는 상업용 및 내부 업무용을 포함하여 AGPL-3.0에 따른 무료 오픈 소스입니다. AGPL의 카피레프트 의무 없이 비공개 소스 제품에 포함하려면 상용 라이선스만 필요합니다.", - "contact_lead": "가격은 배포별로 견적되므로 팀과 워크로드에 적합합니다. 사용 사례를 알려주시면 견적을 받아보겠습니다." + "hero_simple": "VoiceStudio는 상업용 및 내부 업무용을 포함하여 AGPL-3.0에 따른 무료 오픈 소스입니다. AGPL의 카피레프트 의무 없이 비공개 소스 제품에 포함하려면 상용 라이선스만 필요합니다." }, "exportModal": { "export": "내보내기", @@ -2317,7 +2221,6 @@ "loaded_ready": "로드 및 준비됨", "loading_first_run": "로드 중… (처음 실행 시 1~2분 정도 소요될 수 있음)", "failed_to_load": "로드하지 못했습니다.", - "not_loaded_yet": "아직 로드되지 않음 - 첫 번째 기록 시 로드됩니다.", "tts_not_loaded_yet": "아직 로드되지 않음 — 첫 음성 생성 시 로드됩니다", "error_check_logs": "오류: {{error}}. 로그를 확인하고 다시 시작해 보세요.", "check_logs_restart": "모델 로드 오류에 대한 로그를 확인하세요. 다시 시작해 보세요.", @@ -2466,14 +2369,7 @@ }, "casting": { "title": "화자 캐스팅", - "auto_assign_title": "추출된 화자 클론에서 음성 자동 할당", - "auto_cast": "자동 캐스팅", - "all_cast": "모두 캐스팅됨", - "segments_count": "{{count}} 세그먼트", - "assign_voice": "음성 할당…", - "from_video": "🎤 영상에서 ({{name}})", - "no_profiles": "아직 저장된 음성 프로필이 없습니다.", - "preview_voice": "음성 미리보기" + "segments_count": "{{count}} 세그먼트" }, "checkpoint": { "timing_review": "{{count}}개 구간의 타이밍을 검토해야 합니다", @@ -2515,8 +2411,6 @@ "play_failed": "플레이 실패: {{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace 토큰이 설정됨 — 더 빠른 다운로드 가능", - "hf_token_save_failed": "토큰을 저장하지 못했습니다.", "install_started": "설치 시작됨 - 행의 진행률", "delete_confirm": "{{repoId}}을 삭제하시겠습니까? 나중에 다시 설치할 수 있습니다.", "delete_confirm_title": "모델 삭제", @@ -2559,8 +2453,6 @@ "not_installed": "설치되지 않음", "required_tag": "필수", "delete_btn": "삭제", - "hf_token_btn": "HF 토큰", - "hf_set_title": "더 빠른 다운로드를 위해 HuggingFace 토큰 설정", "get_token": "토큰 받기 →", "reco_installed_for": "{{device}}에 권장 번들 설치", "reco_for": "{{device}}에 권장됨", @@ -2568,8 +2460,6 @@ "required_size": "필수 ~{{size}}GB", "all_size": "전체 ~{{size}}GB", "req_tag": "필수", - "search_placeholder": "모델 검색…", - "search_label": "모델 검색", "no_matches": "필터와 일치하는 모델이 없습니다.", "sort_by": "{{column}} 기준으로 정렬", "column_model": "모델", @@ -2579,12 +2469,8 @@ "ready_badge": "준비", "loading_badge": "로드 중…", "idle_badge": "유휴", - "started_downloading_required_one": "{{count}} 필수 모델 다운로드 시작", - "started_downloading_required_other": "{{count}} 필수 모델 다운로드 시작", "to_download": "다운로드 필요", "cached": "캐시된", - "fast_download_badge": "빠른 다운로드", - "fast_download_title": "Xet {{version}}을 통한 빠른 다운로드 — 병렬 청크 전송", "voice_previews": "음성 미리듣기", "voice_previews_desc": "미리 렌더링된 미리듣기를 내려받으면 음성 모델 다운로드가 끝나기 전에도 목소리를 바로 재생할 수 있습니다. 켜기 전까지는 아무것도 내려받지 않습니다.", "voice_previews_off": "끔 — 미리듣기를 이 기기에서 생성합니다", @@ -2666,21 +2552,8 @@ "unload_failed": "언로드할 수 없음: {{message}}", "unloaded_toast": "언로드됨 — 메모리 확보됨" }, - "enterprise_faq": { - "q_internal_tools": "내부 도구에 대한 라이선스가 필요합니까?", - "a_internal_tools": "아니요. 직원과 계약자의 사용은 — 내부 수정 및 자체 호스팅을 포함해 — AGPL-3.0에 따라 무료입니다. 상업용 라이선스는 VoiceStudio를 클로즈드 소스 또는 독점 제품·서비스에 포함하면서 AGPL의 소스 공개 의무를 따르고 싶지 않은 경우에만 필요합니다.", - "q_try_before": "커밋하기 전에 시도해 볼 수 있나요?", - "a_try_before": "예. 전체 앱은 AGPL-3.0에 따라 무료로 다운로드, 실행, 자체 호스팅할 수 있으며 별도의 계약이 필요 없습니다. 상업용(독점 사용) 라이선스에 대해 논의할 준비가 되면 이메일로 연락해 주세요. 함께 세부 사항을 조율하겠습니다.", - "q_watermark": "워터마크는 어떻습니까?", - "a_watermark": "보이지 않는 AudioSeal 워터마크는 모든 사용자에게 기본으로 삽입됩니다. 설정 → 개인정보에서 끌 수 있으며, 변경 이후에 생성된 오디오에만 적용됩니다." - }, "gallery_extra": { - "save_prompt": "이 음성 프로필의 이름을 입력하세요.", - "saved_as_profile": "음성이 프로필로 저장되었습니다!", - "save_profile_failed": "프로필을 저장하지 못했습니다.", - "download_failed": "다운로드 실패: {{message}}", - "trim_load_failed": "자르기를 위해 오디오를 로드하지 못했습니다: {{message}}", - "upload_crop_failed": "잘린 음성을 업로드하지 못했습니다: {{message}}" + "trim_load_failed": "자르기를 위해 오디오를 로드하지 못했습니다: {{message}}" }, "dub_workflow": { "preparing_audio": "오디오 준비 중…", @@ -2806,27 +2679,16 @@ }, "app": { "loading": "로드 중…", - "trimmed_loaded": "잘린 오디오 로드됨", - "toast_exported": "내보냄: {{name}}", - "toast_export_failed": "내보내기 실패: {{message}}", - "toast_open_folder_failed": "폴더를 열 수 없습니다: {{message}}", "toast_saving": "{{name}} 저장 중...", "toast_saved": "저장됨: {{path}}", "toast_save_error": "저장 오류: {{message}}", "toast_processing": "{{name}} 처리 중...", "toast_downloaded": "{{name}} 다운로드됨", "toast_download_error": "다운로드 오류: {{message}}", - "toast_upload_first": "저장하기 전에 동영상이 서버에서 처리되도록 먼저 '업로드 및 전사'를 클릭하세요.", - "toast_save_failed": "저장 실패: {{message}}", - "toast_opened": "열림: {{name}}", - "toast_project_deleted": "프로젝트가 삭제되었습니다.", - "toast_restored_state": "이전 생성 상태를 복원했습니다.", - "toast_history_deleted": "기록 항목이 삭제되었습니다.", "toast_flushed": "플러시 — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· 모델 언로드됨", "toast_flush_failed": "플러시 실패: {{message}}", - "toast_project_saved": "프로젝트가 저장되었습니다", - "toast_project_created": "프로젝트가 생성되었습니다." + "toast_project_saved": "프로젝트가 저장되었습니다" }, "update": { "available": "업데이트 {{version}} 사용 가능", @@ -2876,7 +2738,6 @@ "sponsors_lead": "VoiceStudio를 무료, 로컬, 오픈소스로 유지해주는 기업과 사람들입니다.", "sponsors_learn_more": "스폰서 혜택 안내", "sponsors_logo_aria": "VoiceStudio 스폰서인 {{name}} 방문하기", - "sponsors_perk": "스폰서는 앱, README, 프로젝트 사이트에 로고를 노출할 수 있습니다.", "sponsors_tier_bronze": "브론즈", "sponsors_tier_gold": "골드", "sponsors_tier_platinum": "플래티넘", @@ -2929,13 +2790,11 @@ "not_writable": "쓰기 불가", "change": "변경…", "compute_title": "연산", - "compute_label": "GPU / 가속기", "compute_auto": "자동 (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD GPU (ROCm, Linux)", "channel_label": "업데이트 채널", "channel_stable": "안정", "channel_preview": "프리뷰 (최신 main)", - "network_title": "네트워크", "region_label": "다운로드 지역", "mirrors_title": "사용자 지정 미러 (고급)", "mirror_pypi": "PyPI 인덱스 URL", @@ -2968,10 +2827,6 @@ "trust_line": "프라이버시 우선 — 음성, 녹음, 프로젝트가 이 컴퓨터를 벗어나지 않습니다. 계정 불필요, 클라우드 처리 없음.", "resume_note": "중단된 다운로드는 자동으로 이어집니다 — 앱을 닫아도 안전합니다.", "eta_left": "약 {{eta}} 남음", - "first_sound_text": "당신의 스튜디오에 오신 것을 환영합니다. 지금 들리는 모든 말은 방금 이 컴퓨터에서 생성되었습니다.", - "first_sound_done": "방금 그 목소리, 몇 초 전 로컬에서 생성됐습니다. 환영합니다.", - "hf_token_title": "Hugging Face 토큰(선택)", - "hf_token_hint": "제한된 모델을 사용할 수 있게 합니다 — 예: 다중 화자 더빙용 화자 분리(pyannote). 무료 계정 토큰이며 이 컴퓨터에만 저장됩니다.", "hf_token_save": "저장", "hf_token_saving": "저장 중…", "hf_token_saved": "Hugging Face 토큰 저장됨", @@ -2981,7 +2836,6 @@ "size_left": "{{size}} 남음", "hf_token_card_title": "더 빠른 다운로드를 위해 무료 Hugging Face 토큰을 추가하세요.", "hf_token_get": "토큰이 없나요? 하나를 무료로 받으세요 →", - "hf_token_saved_fast": "Hugging Face 토큰이 저장되었습니다. 이제 다운로드가 더 빨라졌습니다.", "hf_token_inline_prompt": "무료 Hugging Face 토큰으로 다운로드 속도를 높이세요", "hf_token_inline_ph": "hf_… 토큰 붙여넣기(선택 사항)", "hf_token_get_short": "하나를 무료로 받으세요 →", @@ -3007,15 +2861,11 @@ "unstar_take": "별표 해제 — 정리 허용", "play_take": "활성 출력으로 불러오기", "filter_starred": "별표됨", - "load_take_failed": "테이크를 불러오지 못했습니다: {{message}}", "mode_clone": "복제", "mode_synth": "합성", "generation_seconds": "{{duration}}초", - "expand_text": "텍스트 펼치기", - "collapse_text": "텍스트 접기", "seed": "시드 {{seed}}", - "load_config": "설정 불러오기", - "delete_confirm": "이 기록 항목을 삭제할까요?" + "load_config": "설정 불러오기" }, "openapi": { "title": "VoiceStudio API", @@ -3306,7 +3156,6 @@ "feature_cta": "GitHub Issues 열기", "feature_desc": "아이디어, 질문, 혹은 불편한 워크플로가 있으신가요? GitHub 이슈를 열어 공개적으로 추적되게 하면 다른 사람들도 의견을 낼 수 있습니다 — 먼저 검색해 보면 이미 논의된 내용을 찾을 수 있는 경우가 많습니다.", "feature_title": "기능 요청 또는 문의", - "other_ways": "그 외 연락 방법", "security_cta": "비공개로 신고", "security_desc": "취약점을 발견하셨나요? 공개 이슈로 등록하지 말아 주세요. GitHub Security Advisories를 통해 비공개로 신고하시면 공개되기 전에 수정할 수 있습니다.", "security_title": "보안 문제 신고", @@ -3492,25 +3341,18 @@ "preview": "스폰서 미리보기", "message": "브랜드, 웹사이트 및 메시지", "email_app": "이메일 앱 열기", - "copy_email": "이메일 주소 복사", - "preview_detail": "여기에 로고, 링크 및 소개를 표시할 수 있습니다." + "copy_email": "이메일 주소 복사" }, "supportPlans": { "remove": "스폰서 바 제거", "title": "무료와 Pro 비교", "free": "무료", "pro": "Pro", - "sponsor_bar": "스폰서 바", "visible": "표시됨", - "hideable": "숨길 수 있음", "unavailable": "Pro 활성화가 아직 구성되지 않았습니다.", "telemetry": "사용 정보 수집", - "opt_in": "선택 사항, 동의 필요", "disabled": "비활성화", - "badge": "Pro 배지", - "advanced": "고급 도구", - "standard": "기본", - "included": "포함" + "badge": "Pro 배지" }, "integrationCatalog": { "title": "연동", @@ -3519,8 +3361,7 @@ "n8nHint": "이 워크플로를 n8n으로 가져와 요청을 편집한 뒤 실행하세요. VoiceStudio를 실행 상태로 유지하세요. Docker 및 원격 n8n에는 접근 가능한 백엔드 주소와 필요한 인증 정보가 있어야 합니다." }, "directoryExamples": { - "example": "디렉터리 예시", - "notice": "디렉터리 예시이며 스폰서나 연결된 연동이 아닙니다." + "example": "디렉터리 예시" }, "workflowRun": { "run": "워크플로 실행", diff --git a/electron/src/shared/i18n/locales/nl.json b/electron/src/shared/i18n/locales/nl.json index 4942b1131..bd9ad3e2a 100644 --- a/electron/src/shared/i18n/locales/nl.json +++ b/electron/src/shared/i18n/locales/nl.json @@ -59,15 +59,10 @@ "title": "Vraag het VoiceStudio Agent", "notInstalled": "Niet geïnstalleerd", "noSource": "Open een beschrijfbare VoiceStudio-broncheckout zodat een agent deze kan repareren.", - "noneFound": "Installeer Codex, Claude Code, OpenCode of Pi voor lokale reparatie.", "ready": "Gereed", "placeholder": "Wat ging er mis? Voeg stappen, zichtbare fouten of verwacht gedrag toe.", - "prReady": "Reparatie voltooid. Controleer de wijzigingen en stel een PR voor zodra de gerichte tests slagen.", "diagnose": "Diagnosticeren", "fix": "Repareren", - "chooseDefault": "Kies de agent die VoiceStudio automatisch gebruikt als dit scherm crasht.", - "useAutomatically": "{{agent}} automatisch gebruiken", - "captured": "{{count}} problematische logregels vastgelegd", "contextNotice": "De geselecteerde agent ontvangt dit rapport, het huidige scherm, recente app- en backendlogboeken en systeemdiagnostiek.", "complete": "Voltooid" }, @@ -607,7 +602,6 @@ "characters": "{{count}} tekens", "minutes": "~{{count}} min", "chars": "{{count}} tekens", - "exportDone": "Audioboek gedownload", "exportFailed": "Exporteren mislukt. Controleer of de backend actief is.", "global_speed": "Snelheid", "global_speed_hint": "Leessnelheid voor alle lijnen zonder hun eigen snelheidsoverschrijving", @@ -818,7 +812,6 @@ "data_dir": "Gegevensmap", "outputs": "Uitgangen", "crash_log": "Crashlogboek", - "update_endpoint": "Eindpunt bijwerken", "yes": "ja", "no": "nee", "web_preview": "webvoorbeeld", @@ -879,38 +872,21 @@ "title": "Referenties", "desc": "API-sleutels en tokens zijn <1>alleen voor deze sessie ingesteld. Voor persistentie bij opnieuw opstarten stelt u ze in als omgevingsvariabelen in uw shell-profiel.", "hf_token": "Knuffelgezicht-token", - "hf_help": "Vereist voor het bijhouden van sprekers en snellere modeldownloads. Haal de jouwe op Huggingface.co/settings/tokens.", - "translate_key": "LLM API-sleutel", - "translate_help": "Optioneel — voor DeepL, OpenAI of betaalde vertaalaanbieders. Niet nodig voor Google Translate (gratis niveau).", "save": "Opslaan", "saved": "✓ Instellen", - "not_set": "✗ Niet ingesteld", "get_token": "Token verkrijgen →", - "saved_session": "{{key}} opgeslagen voor deze sessie", "save_failed": "Kan niet opslaan", "save_error": "Opslaan mislukt: {{message}}", "deepl_key": "DeepL API-sleutel", - "deepl_help": "Voor DeepL-vertaalmachine. Haal uw sleutel op deepl.com/pro-api.", "deepl_base_url": "DeepL-basis-URL", "deepl_base_url_help": "Aangepast DeepL API-eindpunt (standaard: https://api.deepl.com/v2). Wijzigen voor proxy/zelf-gehost.", "microsoft_key": "Microsoft Translator-sleutel", - "microsoft_help": "Voor Microsoft Translator-engine. Haal uw sleutel op via Azure Portal.", "microsoft_base_url": "Basis-URL van Microsoft Translator", "microsoft_base_url_help": "Aangepast Azure Translator-eindpunt (standaard: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "LLM-basis-URL", - "llm_base_url_help": "Voor filmische kwaliteit of LLM-engine. Elk OpenAI-compatibel eindpunt (OpenAI, Ollama, LM Studio, enz.).", - "llm_model": "LLM-model", - "llm_model_help": "Modelnaam die moet worden gebruikt met de LLM-engine (bijvoorbeeld gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "Knuffelend Gezicht", - "group_translate": "Vertaling (LLM / filmisch)", - "group_deepl": "DeepL aangepast eindpunt", - "group_microsoft": "Aangepast eindpunt van Microsoft Translator", "region": "Regio", "aws_profile": "AWS-profiel" }, "engines": { - "banners_on": "Podiumbanners aan", - "banners_off": "Podiumbanners uit", "backend": "Achterkant", "status": "Status", "ready": "klaar", @@ -941,7 +917,6 @@ "dictation_model_hint": "Gebruikt door live dicteren en door deze engine bij het transcriberen.", "dictation_not_installed": "Niet geïnstalleerd — selecteer om te downloaden", "active": "actief", - "whyUnavailable": "Wat het nodig heeft", "diskDetails": "Schijfdetails", "diskModelDownload": "Modeldownload", "diskPackageDownload": "Pakketdownload", @@ -965,11 +940,8 @@ "sectionReady": "Klaar voor gebruik", "sectionMore": "Meer motoren toevoegen", "lastError": "Laatste fout: {{error}}", - "installedAndReady": "Geïnstalleerd en klaar", - "notInstalled": "Niet geïnstalleerd", "available": "Beschikbaar", "subprocessTitle": "Draait in een eigen subproces + venv", - "inProcessTitle": "Wordt uitgevoerd in het VoiceStudio Python-proces", "testEngine": "Testmotor", "testing": "Testen…", "recheck": "Controleer opnieuw", @@ -986,7 +958,6 @@ "routingUnknown": "Onbekend", "routingEffectiveChip": "Draait op {{device}} op deze machine", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU geselecteerd, maar: {{reason}}", "selectCpuFallback": "{{engine}}: draait op de CPU — {{reason}}", "selectWithCaveat": "Overgeschakeld naar {{engine}} — {{reason}}", "generateCaveat": "{{engine}} kan traag zijn op deze machine — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Geen Tauri-logboek beschikbaar. Werkt alleen in de desktopshell.", "empty_backend": "Runtimelogboek is leeg. Activiteit wordt hier weergegeven terwijl de backend deze registreert.", "title": "Logboeken", - "source_backend": "Achterkant", - "source_frontend": "Frontend", - "source_tauri": "Tauri", "expand": "Vouw logboeken uit", "collapse": "Logboeken samenvouwen", "expand_aria": "Vouw het logboekpaneel uit", @@ -1104,12 +1072,10 @@ "unlock": "Ontgrendelen", "try_voice": "Probeer deze stem", "test_phrase": "Testzin", - "test_help": "Typ wat dan ook - we genereren het met de huidige instellingen van deze stem.", "test_placeholder": "Typ iets om deze stem het te horen zeggen...", "gen_preview": "Voorbeeld genereren", "generating": "Genereren…", "used_title": "Waar deze stem is gebruikt", - "used_empty": "Deze stem is nog niet gebruikt. Genereer hierboven een voorbeeld of kies het in een kopie.", "synth_clips_one": "{{count}} synthclip", "synth_clips_other": "{{count}} synthclips", "projects_count_one": "{{count}}-project", @@ -1152,9 +1118,7 @@ "translate": "Vertalen", "generate": "Genereer", "export": "Exporteren", - "no_video": "Geen video geladen", "segments": "segmenten", - "speakers": "luidsprekers", "video_dubbing_studio": "Video-nasynchronisatiestudio", "drop_here": "Zet hier video of audio neer", "paste_url": "…of plak de YouTube-/video-URL", @@ -1168,7 +1132,6 @@ "cookie_size_error": "De cookie-export mag maximaal 1 MB zijn.", "save": "Opslaan", "reset": "Opnieuw instellen", - "save_project": "Project opslaan", "change_file": "Wijzig bestand", "import_srt": "Importeer .srt", "import_srt_alt": "Importeer in plaats daarvan .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Opnieuw vertalen", "transcript": "Afschrift", "generate_dub": "Dub genereren", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Uploaden en transcriberen", "multi_lang": "Meertalig", "prep_download": "Video downloaden…", @@ -1206,7 +1166,6 @@ "transcribing": "Audio transcriberen…", "elapsed": "verstreken", "remaining": "er resteert", - "sidebar_toggle": "Schakel zijbalk in", "retry_transcription": "Probeer de transcriptie opnieuw", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Wanneer de URL een host is met ondertiteling (YouTube, Vimeo, TED...), haal dan ook de originele ondertiteling en eventuele automatische vertalingen van YouTube op. Zaadt de editor zonder Whisper uit te voeren; overslaan Alles vertalen voor talen die YouTube al behandelt.", @@ -1249,7 +1208,6 @@ "engine_label": "Motor", "install_engine": "Installeer deze motor", "installing_engine": "…installeren", - "needs_dev_install": "heeft een ontwikkelaarinstallatie nodig", "install_disabled_title": "Installaties zijn uitgeschakeld in verpakte builds", "needs_install_suffix": "- moet worden geïnstalleerd", "glossary_title": "Pinvertalingen voor terugkerende termen (namen, merkwoorden, jargon)", @@ -1283,7 +1241,6 @@ "original_track": "Origineel", "selected_dub": "{{code}} (geselecteerde kopie)", "dub_track": "{{code}} (kopiëren)", - "export_tracks": "Tracks exporteren:", "stopping": "Stoppen…", "stop_progress": "Stoppen ({{current}}/{{total}})", "regen_changed": "Regen {{count}} gewijzigd", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} geïnstalleerd", "install_failed": "Installatie mislukt: {{message}}", "prep_elapsed": "{{time}} verstreken", - "add_language": "Taal toevoegen", "search_languages": "Talen zoeken…", "more_to_narrow": "+{{count}} meer — typ om te verkleinen", "no_matches": "Geen overeenkomsten", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Tijd", - "spkr": "Spkr", "text": "Tekst", "lang": "Lang", "voice": "Stem", @@ -1403,8 +1358,6 @@ "select_all_title": "Selecteer alles gefilterd", "sel_count": "{{count}} gekozen", "select_title": "Selecteer segment (shift+klik voor bereik)", - "sync_title": "Gegenereerde audio is {{pct}}% van de duur van het origineel", - "sync_label": "Synchronisatie: {{pct}}%", "rate_title": "Passing op spraaksnelheid: {{ratio}}× relatief aan slot{{error}}", "speaker_id": "Luidspreker-ID", "translate_error_title": "Translation error: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Originele tekst herstellen", "lang_default": "(Def)", "voice_default": "Standaard", - "from_video": "Van video", - "clone_profiles": "Kloon profielen", - "design_presets": "Ontwerpvoorinstellingen", "preview_title": "Live voorbeeld", "set_direction": "Richting instellen…", "edit_direction": "Richting bewerken…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Persoonlijkheid", - "pick_personality": "Kies een persoonlijkheidsvoorinstelling...", "instruct": "Instrueer", "reference_text": "Referentietekst", "language": "Taal", @@ -1535,9 +1484,7 @@ "stories": "Verhalen", "untitled_story": "Naamloos verhaal", "story_lines": "{{count}} lijn", - "story_lines_plural": "{{count}} lijnen", "story_voices": "{{count}} stem", - "story_voices_plural": "{{count}} stemmen", "audiobooks": "Audioboeken", "audiobook": "Audioboek", "story": "Verhaal", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galerij", - "search_placeholder": "Zoek op YouTube...", - "all_voices": "Alle stemmen ({{count}})", - "no_voices": "Nog geen stemmen", "download_hint": "Zoek hierboven om clips te downloaden", - "search_btn": "Zoeken", - "download_btn": "Downloaden", "saving": "Opslaan…", "trim": "Trimmen", - "delete_confirm": "Deze spraakclip verwijderen?", - "view_mode_list": "Lijst", - "view_mode_grid": "Raster", - "youtube_results": "YouTube-resultaten ({{count}})", - "clone_profile": "Kloon profiel", - "crop_audio": "Audio bijsnijden", "subtitle": "Honderden kant-en-klare ontworpen stemmen: kies er één en ga aan de slag.", "zone_archetypes": "Archetypen", "zone_imports": "Mijn import", @@ -1658,8 +1594,6 @@ "delete": "Verwijderen", "community_empty": "Nog geen community-stemmen geladen — maak verbinding met internet en open opnieuw, of wees de eerste die er een indient.", "community_explainer": "Ontworpen presets en opgenomen stemmen gedeeld door de community, geladen vanuit de omnivoice-gallery.", - "no_designer": "Opgenomen stem — gebruik in plaats daarvan \"Gebruik stem\".", - "no_preview": "Geen voorbeeld — voeg de stem toe met \"Gebruik stem\" om hem te horen.", "submit_failed": "Het indieningsformulier kon niet worden geopend.", "submit_preset": "Preset indienen", "submit_voice": "Stem indienen", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Je kunt je eerste stem maken. Stel dicteren nu in of later via Instellingen.", - "welcome": "Welkom", "system_check": "Systeemcontrole", "install_models": "Modellen installeren", - "pick_engines": "Kies motoren", "system_check_desc": "Onderzoek RAM, schijf, GPU, ffmpeg en netwerk. Blokkers worden vooraf gemarkeerd, zodat u het weet voordat u gaat downloaden.", "install_models_desc": "Download ~2,4 GB aan gewichten — alleen het TTS-model is vereist. Voeg transcriptiemodellen op maat van uw hardware nu toe, of later via Instellingen.", "pick_engines_desc": "Kies voor TTS/ASR/LLM-backends. De standaardinstellingen werken kant-en-klaar en kunnen op elk gewenst moment worden aangepast via Instellingen.", "hero_desc": "Dubben, stemklonen en stemontwerp: allemaal lokaal op uw machine.", - "first_run": "Het duurt 5 tot 10 minuten om de eerste keer te downloaden. Daarna is elke lancering direct en volledig offline.", - "get_started": "Ga aan de slag", "back": "Terug", "continue_ok": "Alles goed - ga door", "continue_warn": "Doorgaan (met waarschuwingen)", @@ -1727,7 +1657,6 @@ "try_dictation": "Probeer dicteren", "try_dictation_desc": "Druk één keer op je sneltoets om te controleren of dictee werkt voordat je de installatie voltooit.", "dictation_step_label": "Dictee", - "next_try_dictation": "Volgende: Probeer dicteren", "step_aria": "Stap {{num}}: {{label}}", "step_completed": "voltooid", "cache_label": "Modelcache" @@ -1743,7 +1672,6 @@ "platforms": "Platformen", "footer": "Elke bijdrage helpt de grenzen van lokale AI te verleggen.", "back": "Terug naar Studio", - "commercial": "Commerciële licentie", "pill": "Ondersteuning VoiceStudio", "suggested_title": "Kies een bedrag", "most_common": "meest voorkomende", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Steun de ontwikkeling van VoiceStudio", - "lead_default": "Fijn dat dat werkte! VoiceStudio is gratis en volledig lokaal. Als het u tijd bespaart, financiert een kleine maandelijkse chip-in de Claude Max erachter.", - "lead_first_clone": "Je eerste stemkloon is klaar - leuk! VoiceStudio draait volledig op uw machine, en uw ondersteuning zorgt ervoor dat dit zo blijft.", - "lead_tenth_dub": "Tien dubs erin - je zet het duidelijk aan het werk. Een kleine maandelijkse chip-in financiert de Claude Max die deze functies levert.", - "lead_sustained": "Je werkt al een maand bij VoiceStudio. Als het een plek in uw workflow heeft verdiend, overweeg dan om te helpen bij het financieren van de toekomst.", - "chip_in": "Chip erin", - "later": "Misschien later", - "star": "Ster op GitHub", - "opt_out": "Vraag het niet opnieuw", - "dismiss_aria": "Negeren" + "later": "Misschien later" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Commerciële licentie", "hero_title": "Verzend AI-stemmen in productie", "hero_desc": "VoiceStudio is vrije en open-source software onder de GNU Affero General Public License v3 (AGPL-3.0) — gratis te gebruiken, ook voor commercieel en intern zakelijk gebruik. Een commerciële licentie is alleen nodig als je VoiceStudio wilt inbouwen in een closed-source of propriëtair product of dienst zonder de copyleft-verplichtingen van de AGPL-3.0.", - "why_title": "Waarom bedrijven voor VoiceStudio kiezen", - "pricing_title": "Prijzen", - "faq_title": "Veelgestelde vragen", "pricing_desc": "Niveaus en prijzen worden nog afgerond.", - "pricing_detail": "Totdat ze openbaar zijn, wordt elke commerciële implementatie afzonderlijk geciteerd, zodat we de juiste maat kunnen vinden voor uw team en werklast.", "request_quote": "Vraag een offerte aan", - "footer_email": "Vragen? Neem contact op met hi@voicestudio.sh", - "footer_discord": "Sluit je aan bij onze Discord voor community-ondersteuning.", "benefit_ip": "Volledig IP-eigendom", "benefit_ip_desc": "U bezit elke output, elk model, elke afgeleide. Geen gedeelde huur, geen datalekken.", "benefit_cost": "Geen kosten per minuut", "benefit_cost_desc": "Geen API-facturen, geen onverwachte overschrijdingen. Draait vanaf dag één op uw eigen hardware.", - "benefit_team": "Teambrede toegang", "benefit_team_desc": "Eén licentie geldt voor een onbeperkt aantal gebruikers binnen uw organisatie.", "benefit_support": "Directe ondersteuning", "benefit_support_desc": "Prioritaire e-mail- en videogesprekondersteuning van het kerntechnische team.", - "benefit_source": "Bronbeschikbare kern", "benefit_source_desc": "Volledig zicht op de stapel. Audit, fork en pas aan binnen de licentievoorwaarden.", - "benefit_lang": "646 talen", "benefit_lang_desc": "Transcribeer, vertaal en kopieer in 646 talen met kwaliteit op menselijk niveau.", - "hero_note": "Gebruik, self-hosting en commercieel gebruik zijn allemaal gratis onder de AGPL-3.0 — ook op grote schaal. De AGPL is een netwerk-copyleft-licentie: als je VoiceStudio aanpast en die aangepaste versie via een netwerk aan anderen aanbiedt, moet je je gewijzigde broncode onder dezelfde voorwaarden delen. Een commerciële licentie heft die copyleft-verplichtingen op voor propriëtaire closed-source-implementaties. Prijsplannen volgen binnenkort — neem in de tussentijd contact op.", - "hero_simple": "VoiceStudio is gratis en open-source onder AGPL-3.0 — ook voor commercieel en intern zakelijk gebruik. U hebt alleen een commerciële licentie nodig om het in een gesloten bronproduct in te sluiten, zonder de copyleft-verplichtingen van AGPL.", - "contact_lead": "De prijzen worden per implementatie vermeld, zodat deze passen bij uw team en werklast. Vertel mij uw gebruiksscenario en ik bezorg u een offerte." + "hero_simple": "VoiceStudio is gratis en open-source onder AGPL-3.0 — ook voor commercieel en intern zakelijk gebruik. U hebt alleen een commerciële licentie nodig om het in een gesloten bronproduct in te sluiten, zonder de copyleft-verplichtingen van AGPL." }, "exportModal": { "export": "Exporteren", @@ -2006,7 +1915,6 @@ "loaded_ready": "Geladen en klaar", "loading_first_run": "Laden... (dit kan bij de eerste run 1-2 minuten duren)", "failed_to_load": "Kan niet laden", - "not_loaded_yet": "Nog niet geladen: wordt geladen bij de eerste transcriptie", "tts_not_loaded_yet": "Nog niet geladen — wordt geladen bij de eerste spraakgeneratie", "error_check_logs": "Fout: {{error}}. Controleer de logboeken en probeer opnieuw op te starten.", "check_logs_restart": "Controleer logboeken op fouten bij het laden van modellen. Probeer opnieuw op te starten.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Sprekerscasting", - "auto_assign_title": "Wijs automatisch stemmen toe uit geëxtraheerde luidsprekerklonen", - "auto_cast": "Automatisch casten", - "all_cast": "Allemaal gegoten", - "segments_count": "{{count}} segmenten", - "assign_voice": "Stem toewijzen…", - "from_video": "🎤 Uit video ({{name}})", - "no_profiles": "Er zijn nog geen stemprofielen opgeslagen.", - "preview_voice": "Voorbeeldstem" + "segments_count": "{{count}} segmenten" }, "checkpoint": { "timing_review": "{{count}} segmenten vereisen controle van de timing", @@ -2203,8 +2104,6 @@ "play_failed": "Afspelen mislukt: {{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace-tokenset — snellere downloads ingeschakeld", - "hf_token_save_failed": "Kan token niet opslaan", "install_started": "Installatie gestart — voortgang in de rij", "delete_confirm": "{{repoId}} verwijderen? U kunt het later opnieuw installeren.", "delete_confirm_title": "Model verwijderen", @@ -2247,8 +2146,6 @@ "not_installed": "niet geïnstalleerd", "required_tag": "vereist", "delete_btn": "Verwijderen", - "hf_token_btn": "HF-token", - "hf_set_title": "Stel HuggingFace-token in voor snellere downloads", "get_token": "Token verkrijgen →", "reco_installed_for": "Aanbevolen bundel geïnstalleerd voor {{device}}", "reco_for": "Aanbevolen voor {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Vereist ~{{size}} GB", "all_size": "Alles ~{{size}} GB", "req_tag": "aanvr", - "search_placeholder": "Modellen zoeken…", - "search_label": "Zoek modellen", "no_matches": "Er zijn geen modellen die overeenkomen met uw filters.", "sort_by": "Sorteren op {{column}}", "column_model": "Model", @@ -2267,12 +2162,8 @@ "ready_badge": "Klaar", "loading_badge": "Laden…", "idle_badge": "Inactief", - "started_downloading_required_one": "Begonnen met het downloaden van het vereiste model {{count}}", - "started_downloading_required_other": "Begonnen met het downloaden van {{count}} vereiste modellen", "to_download": "downloaden", "cached": "in de cache opgeslagen", - "fast_download_badge": "snel downloaden", - "fast_download_title": "Snelle downloads via Xet {{version}} — parallelle gefragmenteerde overdracht", "voice_previews": "Stemvoorbeelden", "voice_previews_desc": "Download vooraf gerenderde voorbeelden zodat stemmen meteen afspelen, zelfs voordat een spraakmodel klaar is met downloaden. Er wordt niets gedownload totdat je dit inschakelt.", "voice_previews_off": "Uit — voorbeelden worden op dit apparaat gerenderd", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Heb ik een licentie nodig voor interne tools?", - "a_internal_tools": "Nee. Gebruik door je medewerkers en contractors — inclusief intern aanpassen en self-hosten — is gratis onder de AGPL-3.0. Een commerciële licentie is alleen nodig als je VoiceStudio inbouwt in een closed-source of propriëtair product of dienst en niet wilt voldoen aan de broncode-deelverplichtingen van de AGPL.", - "q_try_before": "Kan ik het proberen voordat ik me vastleg?", - "a_try_before": "Ja. De volledige app is gratis te downloaden, draaien en zelf te hosten onder de AGPL-3.0 — geen overeenkomst nodig. Wil je een commerciële licentie (propriëtair gebruik) bespreken, mail ons dan en we werken de details samen uit.", - "q_watermark": "Hoe zit het met het watermerk?", - "a_watermark": "Het onzichtbare AudioSeal-watermerk wordt standaard voor iedereen ingesloten. U kunt het uitschakelen bij Instellingen → Privacy; het geldt alleen voor audio die na de wijziging wordt gegenereerd." - }, "gallery_extra": { - "save_prompt": "Voer een naam in voor dit stemprofiel:", - "saved_as_profile": "Stem opgeslagen als profiel!", - "save_profile_failed": "Kan profiel niet opslaan", - "download_failed": "Downloaden mislukt: {{message}}", - "trim_load_failed": "Kan audio niet laden voor bijsnijden: {{message}}", - "upload_crop_failed": "Kan bijgesneden stem niet uploaden: {{message}}" + "trim_load_failed": "Kan audio niet laden voor bijsnijden: {{message}}" }, "dub_workflow": { "preparing_audio": "Audio voorbereiden…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Laden…", - "trimmed_loaded": "Bijgesneden audio geladen", - "toast_exported": "Geëxporteerd: {{name}}", - "toast_export_failed": "Exporteren mislukt: {{message}}", - "toast_open_folder_failed": "Kan map niet openen: {{message}}", "toast_saving": "{{name}} opslaan...", "toast_saved": "Opgeslagen: {{path}}", "toast_save_error": "Fout bij opslaan: {{message}}", "toast_processing": "Verwerking {{name}}...", "toast_downloaded": "Gedownload {{name}}", "toast_download_error": "Downloadfout: {{message}}", - "toast_upload_first": "Klik eerst op 'Uploaden en transcriberen', zodat de video op de server wordt verwerkt voordat deze wordt opgeslagen.", - "toast_save_failed": "Opslaan mislukt: {{message}}", - "toast_opened": "Geopend: {{name}}", - "toast_project_deleted": "Project verwijderd", - "toast_restored_state": "Herstelde staat van de vorige generatie", - "toast_history_deleted": "Geschiedenisitem verwijderd", "toast_flushed": "Gespoeld — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· model onbeladen", "toast_flush_failed": "Spoelen mislukt: {{message}}", - "toast_project_saved": "Project opgeslagen", - "toast_project_created": "Project gemaakt" + "toast_project_saved": "Project opgeslagen" }, "update": { "available": "Update {{version}} beschikbaar", @@ -2531,13 +2398,11 @@ "not_writable": "niet beschrijfbaar", "change": "Wijzigen…", "compute_title": "Rekenkracht", - "compute_label": "GPU / versneller", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD GPU (ROCm, Linux)", "channel_label": "Updatekanaal", "channel_stable": "Stabiel", "channel_preview": "Preview (nieuwste main)", - "network_title": "Netwerk", "region_label": "Downloadregio", "mirrors_title": "Eigen mirrors (geavanceerd)", "mirror_pypi": "PyPI-index-URL", @@ -2570,10 +2435,6 @@ "trust_line": "Privacy voorop: je stemmen, opnames en projecten verlaten deze machine nooit — geen account, geen cloudverwerking.", "resume_note": "Onderbroken downloads hervatten automatisch — de app sluiten is veilig.", "eta_left": "nog ~{{eta}}", - "first_sound_text": "Welkom in je studio. Elk woord dat je hoort is zojuist op deze machine gegenereerd.", - "first_sound_done": "Die stem? Seconden geleden lokaal gegenereerd. Welkom.", - "hf_token_title": "Hugging Face-token (optioneel)", - "hf_token_hint": "Ontgrendelt afgeschermde modellen — bijv. sprekerdiarisatie voor dubben met meerdere stemmen (pyannote). Token van gratis account; blijft op deze machine.", "hf_token_save": "Opslaan", "hf_token_saving": "opslaan…", "hf_token_saved": "Hugging Face-token opgeslagen", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} over", "hf_token_card_title": "Voeg een gratis Hugging Face-token toe voor snellere downloads", "hf_token_get": "Heeft u geen token? Ontvang er één gratis →", - "hf_token_saved_fast": "Knuffelgezicht-token opgeslagen — downloads zijn nu sneller", "hf_token_inline_prompt": "Versnel downloads met een gratis Hugging Face-token", "hf_token_inline_ph": "Plak het hf_…-token (optioneel)", "hf_token_get_short": "Ontvang er één gratis →", @@ -2607,15 +2467,11 @@ "unstar_take": "Ster verwijderen — opruimen toestaan", "play_take": "Laden als actieve uitvoer", "filter_starred": "Met ster", - "load_take_failed": "Kon die take niet laden: {{message}}", "mode_clone": "Kloon", "mode_synth": "Synthese", "generation_seconds": "{{duration}} s", - "expand_text": "Tekst uitvouwen", - "collapse_text": "Tekst invouwen", "seed": "Seed {{seed}}", - "load_config": "Instellingen laden", - "delete_confirm": "Dit geschiedenisitem verwijderen?" + "load_config": "Instellingen laden" }, "openapi": { "title": "VoiceStudio-API", @@ -3015,25 +2871,18 @@ "preview": "Sponsorvoorbeeld", "message": "Je merk, website en bericht", "email_app": "E-mailapp openen", - "copy_email": "E-mailadres kopiëren", - "preview_detail": "Hier kunnen je logo, link en introductie staan." + "copy_email": "E-mailadres kopiëren" }, "supportPlans": { "remove": "Sponsorbalk verwijderen", "title": "Gratis versus Pro", "free": "Gratis", "pro": "Pro", - "sponsor_bar": "Sponsorbalk", "visible": "Zichtbaar", - "hideable": "Kan worden verborgen", "unavailable": "Pro-activering is nog niet ingesteld.", "telemetry": "Telemetrie", - "opt_in": "Optioneel, met toestemming", "disabled": "Uitgeschakeld", - "badge": "Pro-badge", - "advanced": "Geavanceerde tools", - "standard": "Standaard", - "included": "Inbegrepen" + "badge": "Pro-badge" }, "integrationCatalog": { "title": "Integraties", @@ -3042,8 +2891,7 @@ "n8nHint": "Importeer deze workflow in n8n, bewerk het verzoek en voer hem uit. Laat VoiceStudio draaien. Docker en externe n8n-instanties hebben een bereikbaar backendadres en eventuele inloggegevens nodig." }, "directoryExamples": { - "example": "Directoryvoorbeeld", - "notice": "Alleen directoryvoorbeelden — geen sponsors of verbonden integraties." + "example": "Directoryvoorbeeld" }, "workflowRun": { "run": "Workflow uitvoeren", diff --git a/electron/src/shared/i18n/locales/pl.json b/electron/src/shared/i18n/locales/pl.json index ccd27ef0a..985b00ed8 100644 --- a/electron/src/shared/i18n/locales/pl.json +++ b/electron/src/shared/i18n/locales/pl.json @@ -59,15 +59,10 @@ "title": "Zapytaj VoiceStudio Agent", "notInstalled": "Nie zainstalowano", "noSource": "Otwórz zapisywalną kasę źródłową VoiceStudio, aby agent mógł ją naprawić.", - "noneFound": "Zainstaluj Codex, Claude Code, OpenCode lub Pi, aby skorzystać z naprawy lokalnej.", "ready": "Gotowy", "placeholder": "Co poszło nie tak? Dodaj kroki, widoczne błędy lub oczekiwane działanie.", - "prReady": "Naprawa zakończona. Przejrzyj zmiany i zaproponuj PR, gdy testy docelowe przejdą pomyślnie.", "diagnose": "Diagnozuj", "fix": "Napraw", - "chooseDefault": "Wybierz agenta, którego VoiceStudio ma używać automatycznie po awarii tego ekranu.", - "useAutomatically": "Używaj {{agent}} automatycznie", - "captured": "Przechwycono {{count}} problematycznych wierszy dziennika", "contextNotice": "Wybrany agent otrzyma to zgłoszenie, bieżący ekran, ostatnie dzienniki aplikacji i backendu oraz diagnostykę systemu.", "complete": "Zakończono" }, @@ -607,7 +602,6 @@ "characters": "{{count}} znaków", "minutes": "~{{count}} min", "chars": "{{count}} znaków", - "exportDone": "Pobrano audiobooka", "exportFailed": "Eksport nie powiódł się — sprawdź, czy backend działa.", "global_speed": "Prędkość", "global_speed_hint": "Prędkość odczytu dla wszystkich linii bez własnego sterowania prędkością", @@ -818,7 +812,6 @@ "data_dir": "Katalog danych", "outputs": "Wyjścia", "crash_log": "Dziennik awarii", - "update_endpoint": "Zaktualizuj punkt końcowy", "yes": "tak", "no": "nie", "web_preview": "podgląd internetowy", @@ -879,38 +872,21 @@ "title": "Poświadczenia", "desc": "Klucze i tokeny API są ustawione <1>tylko dla tej sesji. Aby zachować trwałość po ponownym uruchomieniu, ustaw je jako zmienne środowiskowe w profilu powłoki.", "hf_token": "Żeton Przytulającej Twarzy", - "hf_help": "Wymagane do diaryzacji głośników i szybszego pobierania modeli. Zdobądź swój na huggingface.co/settings/tokens.", - "translate_key": "Klucz API LLM", - "translate_help": "Opcjonalnie — dla DeepL, OpenAI lub dostawców płatnych tłumaczeń. Nie potrzebne w przypadku Tłumacza Google (poziom bezpłatny).", "save": "Zapisz", "saved": "✓ Ustaw", - "not_set": "✗ Nie ustawiono", "get_token": "Zdobądź token →", - "saved_session": "{{key}} zapisany dla tej sesji", "save_failed": "Nie udało się zapisać", "save_error": "Zapis nie powiódł się: {{message}}", "deepl_key": "Klucz API DeepL", - "deepl_help": "Dla silnika tłumaczącego DeepL. Zdobądź klucz na deepl.com/pro-api.", "deepl_base_url": "Podstawowy adres URL DeepL", "deepl_base_url_help": "Niestandardowy punkt końcowy API DeepL (domyślnie: https://api.deepl.com/v2). Zmień na serwer proxy/samodzielnie hostowany.", "microsoft_key": "Klucz tłumacza Microsoft", - "microsoft_help": "Dla silnika Microsoft Translator. Zdobądź klucz z Azure Portal.", "microsoft_base_url": "Podstawowy adres URL tłumacza Microsoft", "microsoft_base_url_help": "Niestandardowy punkt końcowy usługi Azure Translator (domyślnie: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "Podstawowy adres URL LLM", - "llm_base_url_help": "Dla jakości kinowej lub silnika LLM. Dowolny punkt końcowy zgodny z OpenAI (OpenAI, Ollama, LM Studio itp.).", - "llm_model": "Model LLM", - "llm_model_help": "Nazwa modelu do użycia z silnikiem LLM (np. gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "Przytulana twarz", - "group_translate": "Tłumaczenie (LLM / filmowe)", - "group_deepl": "Niestandardowy punkt końcowy DeepL", - "group_microsoft": "Niestandardowy punkt końcowy usługi Microsoft Translator", "region": "Region", "aws_profile": "Profil AWS" }, "engines": { - "banners_on": "Włączono banery sceniczne", - "banners_off": "Banery sceniczne wyłączone", "backend": "Zaplecze", "status": "Stan", "ready": "gotowy", @@ -941,7 +917,6 @@ "dictation_model_hint": "Używany przez dyktowanie na żywo i przez ten silnik podczas transkrypcji.", "dictation_not_installed": "Nie zainstalowano — wybierz, aby pobrać", "active": "aktywny", - "whyUnavailable": "Czego potrzebuje", "diskDetails": "Szczegóły dysku", "diskModelDownload": "Pobieranie modelu", "diskPackageDownload": "Pobieranie pakietów", @@ -965,11 +940,8 @@ "sectionReady": "Gotowe do użycia", "sectionMore": "Dodaj więcej silników", "lastError": "Ostatni błąd: {{error}}", - "installedAndReady": "Zainstalowany i gotowy", - "notInstalled": "Nie zainstalowano", "available": "Dostępne", "subprocessTitle": "Działa we własnym podprocesie + venv", - "inProcessTitle": "Działa w procesie VoiceStudio Python", "testEngine": "Silnik testowy", "testing": "Testowanie…", "recheck": "Sprawdź ponownie", @@ -986,7 +958,6 @@ "routingUnknown": "Nieznany", "routingEffectiveChip": "Działa na {{device}} na tym komputerze", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "Wybrano procesor graficzny, ale: {{reason}}", "selectCpuFallback": "{{engine}}: działa na CPU — {{reason}}", "selectWithCaveat": "Przełączono na {{engine}} — {{reason}}", "generateCaveat": "{{engine}} może działać wolno na tym komputerze — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Brak dostępnego dziennika Tauri. Działa tylko w powłoce pulpitu.", "empty_backend": "Dziennik wykonawczy jest pusty. Aktywność pojawi się tutaj, gdy backend ją zarejestruje.", "title": "Dzienniki", - "source_backend": "Zaplecze", - "source_frontend": "Frontend", - "source_tauri": "Tauri", "expand": "Rozwiń logi", "collapse": "Zwiń dzienniki", "expand_aria": "Rozwiń panel dzienników", @@ -1104,12 +1072,10 @@ "unlock": "Odblokuj", "try_voice": "Spróbuj tego głosu", "test_phrase": "Fraza testowa", - "test_help": "Wpisz cokolwiek — wygenerujemy to z bieżącymi ustawieniami tego głosu.", "test_placeholder": "Wpisz coś, aby usłyszeć ten głos…", "gen_preview": "Wygeneruj podgląd", "generating": "Generowanie…", "used_title": "Gdzie ten głos został użyty", - "used_empty": "Ten głos nie był jeszcze używany. Wygeneruj podgląd powyżej lub wybierz go w dubbingu.", "synth_clips_one": "{{count}} klip syntezatorowy", "synth_clips_other": "{{count}} klipy syntezatorowe", "projects_count_one": "{{count}} projekt", @@ -1152,9 +1118,7 @@ "translate": "Przetłumacz", "generate": "Wygeneruj", "export": "Eksportuj", - "no_video": "Brak załadowanego filmu", "segments": "segmenty", - "speakers": "głośniki", "video_dubbing_studio": "Studio dubbingu wideo", "drop_here": "Upuść tutaj wideo lub audio", "paste_url": "…lub wklej adres URL YouTube/wideo", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Eksport plików cookie może mieć najwyżej 1 MB.", "save": "Zapisz", "reset": "Zresetuj", - "save_project": "Zapisz projekt", "change_file": "Zmień plik", "import_srt": "Importuj .srt", "import_srt_alt": "Zamiast tego zaimportuj plik .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Przetłumacz ponownie", "transcript": "Transkrypcja", "generate_dub": "Wygeneruj dub", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Prześlij i dokonaj transkrypcji", "multi_lang": "Wielojęzyczny", "prep_download": "Pobieram wideo…", @@ -1206,7 +1166,6 @@ "transcribing": "Transkrypcja dźwięku…", "elapsed": "upłynął", "remaining": "pozostało", - "sidebar_toggle": "Przełącz pasek boczny", "retry_transcription": "Ponów transkrypcję", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Jeśli adres URL jest hostem z napisami (YouTube, Vimeo, TED…), pobierz także oryginalne napisy i wszelkie automatyczne tłumaczenia z YouTube. Uruchamia edytor bez uruchamiania Whisper; pomiń Przetłumacz wszystko dla języków, które YouTube już obsługuje.", @@ -1249,7 +1208,6 @@ "engine_label": "Silnik", "install_engine": "Zainstaluj ten silnik", "installing_engine": "…instalacja", - "needs_dev_install": "wymaga instalacji deweloperskiej", "install_disabled_title": "Instalacje są wyłączone w kompilacjach spakowanych", "needs_install_suffix": "— wymaga instalacji", "glossary_title": "Przypinanie tłumaczeń powtarzających się terminów (nazwy, słowa marki, żargon)", @@ -1283,7 +1241,6 @@ "original_track": "Oryginał", "selected_dub": "{{code}} (wybrany dubbing)", "dub_track": "{{code}} (dubbing)", - "export_tracks": "Eksportuj ścieżki:", "stopping": "Zatrzymywanie…", "stop_progress": "Zatrzymaj ({{current}}/{{total}})", "regen_changed": "Regen {{count}} zmieniony", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} zainstalowany", "install_failed": "Instalacja nie powiodła się: {{message}}", "prep_elapsed": "upłynął {{time}}", - "add_language": "Dodaj język", "search_languages": "Wyszukaj języki…", "more_to_narrow": "+{{count}} więcej — wpisz, aby zawęzić", "no_matches": "Brak dopasowań", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Czas", - "spkr": "Spkr", "text": "Tekst", "lang": "Lang", "voice": "Głos", @@ -1403,8 +1358,6 @@ "select_all_title": "Wybierz wszystkie filtrowane", "sel_count": "{{count}} wyb", "select_title": "Wybierz segment (shift+kliknięcie, aby wyświetlić zakres)", - "sync_title": "Wygenerowany dźwięk wynosi {{pct}}% czasu trwania oryginału", - "sync_label": "Synchronizacja: {{pct}}%", "rate_title": "Dopasowanie szybkości mowy: {{ratio}}× względem szczeliny{{error}}", "speaker_id": "Identyfikator głośnika", "translate_error_title": "Błąd w tłumaczeniu: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Przywróć oryginalny tekst", "lang_default": "(Obrona)", "voice_default": "Domyślne", - "from_video": "Z wideo", - "clone_profiles": "Profile klonowania", - "design_presets": "Ustawienia wstępne projektu", "preview_title": "Podgląd na żywo", "set_direction": "Wyznacz kierunek…", "edit_direction": "Edytuj kierunek…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Osobowość", - "pick_personality": "Wybierz gotowe ustawienie osobowości…", "instruct": "Poinstruuj", "reference_text": "Tekst referencyjny", "language": "Język", @@ -1535,9 +1484,7 @@ "stories": "Historie", "untitled_story": "Historia bez tytułu", "story_lines": "Linia {{count}}", - "story_lines_plural": "{{count}} linii", "story_voices": "{{count}} głos", - "story_voices_plural": "{{count}} głosy", "audiobooks": "Audiobooki", "audiobook": "Książka audio", "story": "Historia", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galeria", - "search_placeholder": "Wyszukaj w YouTube…", - "all_voices": "Wszystkie głosy ({{count}})", - "no_voices": "Nie ma jeszcze głosów", "download_hint": "Wyszukaj powyżej, aby pobrać klipy", - "search_btn": "Szukaj", - "download_btn": "Pobierz", "saving": "Zapisuję…", "trim": "Przytnij", - "delete_confirm": "Usunąć ten klip głosowy?", - "view_mode_list": "Lista", - "view_mode_grid": "Siatka", - "youtube_results": "Wyniki w YouTube ({{count}})", - "clone_profile": "Klonuj profil", - "crop_audio": "Przytnij dźwięk", "subtitle": "Setki gotowych, zaprojektowanych głosów — wybierz jeden i gotowe.", "zone_archetypes": "Archetypy", "zone_imports": "Mój import", @@ -1658,8 +1594,6 @@ "delete": "Usuń", "community_empty": "Nie załadowano jeszcze głosów społeczności — połącz się z internetem i otwórz ponownie albo prześlij pierwszy.", "community_explainer": "Zaprojektowane ustawienia wstępne i nagrane głosy udostępnione przez społeczność, ładowane z omnivoice-gallery.", - "no_designer": "Nagrany głos — użyj zamiast tego opcji „Użyj głosu”.", - "no_preview": "Brak podglądu — dodaj go opcją „Użyj głosu”, aby odsłuchać.", "submit_failed": "Nie udało się otworzyć formularza zgłoszenia.", "submit_preset": "Prześlij ustawienie wstępne", "submit_voice": "Prześlij głos", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Możesz już utworzyć swój pierwszy głos. Dyktowanie skonfigurujesz teraz lub później w Ustawieniach.", - "welcome": "Witamy", "system_check": "Kontrola systemu", "install_models": "Zainstaluj modele", - "pick_engines": "Wybierz silniki", "system_check_desc": "Sprawdź pamięć RAM, dysk, procesor graficzny, ffmpeg i sieć. Blokery są oznaczone od razu, więc wiesz o tym przed pobraniem.", "install_models_desc": "Pobierz ~2,4 GB wag — wymagany jest tylko model TTS. Dodaj modele transkrypcji dobrane do Twojego sprzętu teraz lub później w Ustawieniach.", "pick_engines_desc": "Wybierz backendy TTS / ASR / LLM. Ustawienia domyślne działają od razu po wyjęciu z pudełka — dostosuj je w dowolnym momencie w Ustawieniach.", "hero_desc": "Dubbing, klonowanie głosu i projektowanie głosu — wszystko to działa lokalnie na Twoim komputerze.", - "first_run": "Pobranie pierwszego uruchomienia zajmuje 5–10 minut. Następnie każde uruchomienie jest natychmiastowe i całkowicie offline.", - "get_started": "Zacznij", "back": "Powrót", "continue_ok": "Wszystko w porządku — kontynuuj", "continue_warn": "Kontynuuj (z ostrzeżeniami)", @@ -1727,7 +1657,6 @@ "try_dictation": "Spróbuj dyktowania", "try_dictation_desc": "Naciśnij swój skrót raz, aby upewnić się, że dyktowanie działa, zanim zakończysz konfigurację.", "dictation_step_label": "Dyktowanie", - "next_try_dictation": "Dalej: Wypróbuj dyktowanie", "step_aria": "Krok {{num}}: {{label}}", "step_completed": "ukończono", "cache_label": "Pamięć podręczna modelu" @@ -1743,7 +1672,6 @@ "platforms": "Platformy", "footer": "Każdy wkład pomaga przesuwać granice lokalnej sztucznej inteligencji.", "back": "Powrót do Studia", - "commercial": "Licencja komercyjna", "pill": "Wspieraj VoiceStudio", "suggested_title": "Wybierz kwotę", "most_common": "najczęściej", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Wesprzyj rozwój VoiceStudio", - "lead_default": "Cieszę się, że to zadziałało! Usługa VoiceStudio jest bezpłatna i w pełni lokalna. Jeśli oszczędzi to Twój czas, niewielka miesięczna wpłata sfinansuje stojącego za nim Claude'a Maxa.", - "lead_first_clone": "Twój pierwszy klon głosu jest gotowy — świetnie! VoiceStudio działa całkowicie na Twoim komputerze, a dzięki Twojemu wsparciu tak będzie.", - "lead_tenth_dub": "Dziesięć dubów — wyraźnie to robisz. Niewielki miesięczny wkład finansuje Claude Max, który udostępnia te funkcje.", - "lead_sustained": "Jesteś z VoiceStudio od miesiąca. Jeśli zasłużyło na miejsce w Twoim przepływie pracy, zastanów się nad wsparciem w sfinansowaniu kolejnego projektu.", - "chip_in": "Wrzuć", - "later": "Może później", - "star": "Gwiazda na GitHubie", - "opt_out": "Nie pytaj ponownie", - "dismiss_aria": "Odrzuć" + "later": "Może później" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Licencja komercyjna", "hero_title": "Wysyłaj głosy AI w produkcji", "hero_desc": "VoiceStudio to wolne i otwarte oprogramowanie na licencji GNU Affero General Public License v3 (AGPL-3.0) — bezpłatne w użyciu, także komercyjnym i wewnętrznym firmowym. Licencja komercyjna jest potrzebna tylko wtedy, gdy chcesz osadzić VoiceStudio w zamkniętym lub własnościowym produkcie bądź usłudze bez zobowiązań copyleft licencji AGPL-3.0.", - "why_title": "Dlaczego firmy wybierają VoiceStudio", - "pricing_title": "Ceny", - "faq_title": "Często zadawane pytania", "pricing_desc": "Poziomy i ceny są wciąż finalizowane.", - "pricing_detail": "Dopóki nie zostaną one upublicznione, każde wdrożenie komercyjne jest wyceniane indywidualnie, abyśmy mogli dopasować rozmiar do Twojego zespołu i obciążenia pracą.", "request_quote": "Poproś o wycenę", - "footer_email": "Pytania? Skontaktuj się z nami pod adresem hi@voicestudio.sh", - "footer_discord": "Dołącz do naszego Discorda, aby uzyskać wsparcie społeczności.", "benefit_ip": "Pełna własność IP", "benefit_ip_desc": "Jesteś właścicielem każdego produktu, każdego modelu i każdej pochodnej. Żadnych wspólnych najemców, żadnych wycieków danych.", "benefit_cost": "Zero kosztów za minutę", "benefit_cost_desc": "Żadnych rachunków za API, żadnych niespodziewanych nadwyżek. Działa na Twoim sprzęcie od pierwszego dnia.", - "benefit_team": "Dostęp dla całego zespołu", "benefit_team_desc": "Jedna licencja obejmuje nieograniczoną liczbę użytkowników w Twojej organizacji.", "benefit_support": "Bezpośrednie wsparcie", "benefit_support_desc": "Priorytetowa pomoc przez e-mail i rozmowy wideo ze strony głównego zespołu inżynierów.", - "benefit_source": "Rdzeń dostępny ze źródła", "benefit_source_desc": "Pełna widoczność stosu. Audytuj, rozwidlaj i dostosowuj zgodnie z warunkami licencji.", - "benefit_lang": "646 języków", "benefit_lang_desc": "Transkrypcja, tłumaczenie i kopiowanie w 646 językach z jakością na poziomie ludzkim.", - "hero_note": "Użytkowanie, self-hosting i zastosowania komercyjne są bezpłatne na licencji AGPL-3.0 — również na dużą skalę. AGPL to licencja copyleft działająca także przez sieć: jeśli zmodyfikujesz VoiceStudio i udostępnisz tę zmodyfikowaną wersję innym przez sieć, musisz udostępnić swój zmodyfikowany kod źródłowy na tych samych warunkach. Licencja komercyjna znosi te zobowiązania copyleft dla wdrożeń własnościowych o zamkniętym kodzie. Cenniki pojawią się wkrótce — w międzyczasie napisz do nas.", - "hero_simple": "VoiceStudio jest bezpłatne i ma otwarte oprogramowanie na licencji AGPL-3.0 — w tym do użytku komercyjnego i wewnętrznego. Aby osadzić go w produkcie o zamkniętym źródle, potrzebujesz jedynie licencji komercyjnej, bez zobowiązań AGPL związanych z prawami autorskimi.", - "contact_lead": "Ceny są podawane za wdrożenie, więc odpowiadają Twojemu zespołowi i obciążeniu pracą. Podaj mi swój przypadek użycia, a otrzymasz wycenę." + "hero_simple": "VoiceStudio jest bezpłatne i ma otwarte oprogramowanie na licencji AGPL-3.0 — w tym do użytku komercyjnego i wewnętrznego. Aby osadzić go w produkcie o zamkniętym źródle, potrzebujesz jedynie licencji komercyjnej, bez zobowiązań AGPL związanych z prawami autorskimi." }, "exportModal": { "export": "Eksportuj", @@ -2006,7 +1915,6 @@ "loaded_ready": "Załadowany i gotowy", "loading_first_run": "Ładowanie… (przy pierwszym uruchomieniu może to zająć 1–2 minuty)", "failed_to_load": "Nie udało się załadować", - "not_loaded_yet": "Jeszcze nie załadowano — zostanie załadowane przy pierwszej transkrypcji", "tts_not_loaded_yet": "Jeszcze nie załadowano — zostanie załadowany przy pierwszym generowaniu mowy", "error_check_logs": "Błąd: {{error}}. Sprawdź dzienniki i spróbuj uruchomić ponownie.", "check_logs_restart": "Sprawdź dzienniki pod kątem błędów ładowania modelu. Spróbuj uruchomić ponownie.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Casting głośników", - "auto_assign_title": "Automatyczne przypisywanie głosów z wyodrębnionych klonów głośników", - "auto_cast": "Automatyczne przesyłanie", - "all_cast": "Wszystko w obsadzie", - "segments_count": "{{count}} segmentów", - "assign_voice": "Przypisz głos…", - "from_video": "🎤 Z filmu ({{name}})", - "no_profiles": "Nie zapisano jeszcze żadnych profili głosowych.", - "preview_voice": "Podgląd głosu" + "segments_count": "{{count}} segmentów" }, "checkpoint": { "timing_review": "{{count}} segmentów wymaga sprawdzenia synchronizacji", @@ -2203,8 +2104,6 @@ "play_failed": "Odtwarzanie nie powiodło się: {{message}}" }, "models": { - "hf_token_set_toast": "Zestaw tokenów HuggingFace — włączone szybsze pobieranie", - "hf_token_save_failed": "Nie udało się zapisać tokena", "install_started": "Instalacja rozpoczęta — postęp w rzędzie", "delete_confirm": "Usunąć {{repoId}}? Możesz zainstalować go ponownie później.", "delete_confirm_title": "Usuń model", @@ -2247,8 +2146,6 @@ "not_installed": "nie zainstalowany", "required_tag": "wymagane", "delete_btn": "Usuń", - "hf_token_btn": "Token HF", - "hf_set_title": "Ustaw token HuggingFace, aby przyspieszyć pobieranie", "get_token": "Zdobądź token →", "reco_installed_for": "Zalecany pakiet zainstalowany dla {{device}}", "reco_for": "Zalecane dla {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Wymagane ~{{size}} GB", "all_size": "Wszystko ~{{size}} GB", "req_tag": "wymaganie", - "search_placeholder": "Wyszukaj modele…", - "search_label": "Wyszukaj modele", "no_matches": "Żaden model nie pasuje do Twoich filtrów.", "sort_by": "Sortuj według {{column}}", "column_model": "Modelka", @@ -2267,12 +2162,8 @@ "ready_badge": "Gotowy", "loading_badge": "Ładowanie…", "idle_badge": "Bezczynny", - "started_downloading_required_one": "Rozpoczęto pobieranie {{count}} wymaganego modelu", - "started_downloading_required_other": "Rozpoczęto pobieranie {{count}} wymaganych modeli", "to_download": "pobrać", "cached": "buforowane", - "fast_download_badge": "szybkie pobieranie", - "fast_download_title": "Szybkie pobieranie poprzez Xet {{version}} — równoległy transfer fragmentaryczny", "voice_previews": "Podglądy głosów", "voice_previews_desc": "Pobierz wcześniej wygenerowane podglądy, aby głosy odtwarzały się od razu — jeszcze zanim model głosu skończy się pobierać. Nic nie jest pobierane, dopóki tego nie włączysz.", "voice_previews_off": "Wyłączone — podglądy są generowane na tym urządzeniu", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Czy potrzebuję licencji na narzędzia wewnętrzne?", - "a_internal_tools": "Nie. Użycie przez Twoich pracowników i współpracowników — w tym wewnętrzne modyfikacje i self-hosting — jest bezpłatne na licencji AGPL-3.0. Licencja komercyjna jest potrzebna tylko wtedy, gdy osadzasz VoiceStudio w zamkniętym lub własnościowym produkcie bądź usłudze i nie chcesz spełniać wymogów AGPL dotyczących udostępniania kodu źródłowego.", - "q_try_before": "Czy mogę spróbować przed zatwierdzeniem?", - "a_try_before": "Tak. Pełną aplikację można bezpłatnie pobrać, uruchomić i hostować samodzielnie na licencji AGPL-3.0 — bez żadnej umowy. Gdy zechcesz porozmawiać o licencji komercyjnej (użycie własnościowe), napisz do nas, a wspólnie ustalimy szczegóły.", - "q_watermark": "A co ze znakiem wodnym?", - "a_watermark": "Niewidoczny znak wodny AudioSeal jest domyślnie osadzany u wszystkich. Możesz go wyłączyć w Ustawieniach → Prywatność; dotyczy tylko dźwięku wygenerowanego po zmianie." - }, "gallery_extra": { - "save_prompt": "Wprowadź nazwę tego profilu głosowego:", - "saved_as_profile": "Głos zapisany jako profil!", - "save_profile_failed": "Nie udało się zapisać profilu", - "download_failed": "Pobieranie nie powiodło się: {{message}}", - "trim_load_failed": "Nie udało się załadować dźwięku do przycięcia: {{message}}", - "upload_crop_failed": "Nie udało się przesłać przyciętego głosu: {{message}}" + "trim_load_failed": "Nie udało się załadować dźwięku do przycięcia: {{message}}" }, "dub_workflow": { "preparing_audio": "Przygotowuję dźwięk…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Ładowanie…", - "trimmed_loaded": "Wczytano przycięty dźwięk", - "toast_exported": "Wyeksportowano: {{name}}", - "toast_export_failed": "Eksport nie powiódł się: {{message}}", - "toast_open_folder_failed": "Nie można otworzyć folderu: {{message}}", "toast_saving": "Zapisywanie {{name}}...", "toast_saved": "Zapisano: {{path}}", "toast_save_error": "Zapisz błąd: {{message}}", "toast_processing": "Przetwarzanie {{name}}...", "toast_downloaded": "Pobrano {{name}}", "toast_download_error": "Błąd pobierania: {{message}}", - "toast_upload_first": "Najpierw kliknij „Prześlij i transkrybuj”, aby wideo zostało przetworzone na serwerze przed zapisaniem.", - "toast_save_failed": "Zapis nie powiódł się: {{message}}", - "toast_opened": "Otwarto: {{name}}", - "toast_project_deleted": "Projekt usunięty", - "toast_restored_state": "Przywrócono stan poprzedniej generacji", - "toast_history_deleted": "Element historii usunięty", "toast_flushed": "Opróżniony — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· model rozładowany", "toast_flush_failed": "Spłukiwanie nie powiodło się: {{message}}", - "toast_project_saved": "Projekt zapisany", - "toast_project_created": "Projekt stworzony" + "toast_project_saved": "Projekt zapisany" }, "update": { "available": "Dostępna aktualizacja {{version}}", @@ -2531,13 +2398,11 @@ "not_writable": "brak zapisu", "change": "Zmień…", "compute_title": "Obliczenia", - "compute_label": "GPU / akcelerator", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Kanał aktualizacji", "channel_stable": "Stabilny", "channel_preview": "Podgląd (najnowszy main)", - "network_title": "Sieć", "region_label": "Region pobierania", "mirrors_title": "Własne serwery lustrzane (zaawansowane)", "mirror_pypi": "URL indeksu PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "Prywatność przede wszystkim: Twoje głosy, nagrania i projekty nigdy nie opuszczają tego komputera — bez konta i bez przetwarzania w chmurze.", "resume_note": "Przerwane pobierania wznawiają się same — zamknięcie aplikacji jest bezpieczne.", "eta_left": "pozostało ~{{eta}}", - "first_sound_text": "Witaj w swoim studiu. Każde słowo, które słyszysz, zostało wygenerowane na tym komputerze — przed chwilą.", - "first_sound_done": "Ten głos? Wygenerowany sekundy temu, lokalnie. Witaj.", - "hf_token_title": "Token Hugging Face (opcjonalny)", - "hf_token_hint": "Odblokowuje modele z ograniczeniami — np. diaryzację mówców do dubbingu wielogłosowego (pyannote). Token darmowego konta; zostaje na tym komputerze.", "hf_token_save": "Zapisz", "hf_token_saving": "zapisywanie…", "hf_token_saved": "Token Hugging Face zapisany", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} wyszedł", "hf_token_card_title": "Dodaj darmowy token Przytulonej Twarzy, aby przyspieszyć pobieranie", "hf_token_get": "Nie masz tokena? Zdobądź jeden za darmo →", - "hf_token_saved_fast": "Token Przytulonej Twarzy został zapisany — pobieranie jest teraz szybsze", "hf_token_inline_prompt": "Przyspiesz pobieranie dzięki darmowemu tokenowi Przytulająca Twarz", "hf_token_inline_ph": "Wklej token hf_… (opcjonalnie)", "hf_token_get_short": "Zdobądź jeden za darmo →", @@ -2607,15 +2467,11 @@ "unstar_take": "Usuń gwiazdkę — pozwól na czyszczenie", "play_take": "Wczytaj jako aktywne wyjście", "filter_starred": "Z gwiazdką", - "load_take_failed": "Nie udało się wczytać nagrania: {{message}}", "mode_clone": "Klonowanie", "mode_synth": "Synteza", "generation_seconds": "{{duration}} s", - "expand_text": "Rozwiń tekst", - "collapse_text": "Zwiń tekst", "seed": "Ziarno {{seed}}", - "load_config": "Wczytaj ustawienia", - "delete_confirm": "Usunąć ten element historii?" + "load_config": "Wczytaj ustawienia" }, "openapi": { "title": "API VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Podgląd sponsora", "message": "Twoja marka, strona i wiadomość", "email_app": "Otwórz pocztę", - "copy_email": "Kopiuj e-mail", - "preview_detail": "Tutaj mogą pojawić się Twoje logo, link i opis." + "copy_email": "Kopiuj e-mail" }, "supportPlans": { "remove": "Usuń pasek sponsorów", "title": "Darmowa a Pro", "free": "Darmowa", "pro": "Pro", - "sponsor_bar": "Pasek sponsorów", "visible": "Widoczny", - "hideable": "Można ukryć", "unavailable": "Aktywacja Pro nie została jeszcze skonfigurowana.", "telemetry": "Telemetria", - "opt_in": "Opcjonalna, za zgodą", "disabled": "Wyłączona", - "badge": "Odznaka Pro", - "advanced": "Zaawansowane narzędzia", - "standard": "Standardowe", - "included": "W zestawie" + "badge": "Odznaka Pro" }, "integrationCatalog": { "title": "Integracje", @@ -3042,8 +2891,7 @@ "n8nHint": "Zaimportuj ten przepływ do n8n, edytuj żądanie i uruchom go. Pozostaw VoiceStudio uruchomione. Docker i zdalne n8n wymagają dostępnego adresu backendu oraz wymaganych danych uwierzytelniających." }, "directoryExamples": { - "example": "Przykład katalogowy", - "notice": "Wyłącznie przykłady katalogowe — nie sponsorzy ani połączone integracje." + "example": "Przykład katalogowy" }, "workflowRun": { "run": "Uruchom przepływ", diff --git a/electron/src/shared/i18n/locales/pt.json b/electron/src/shared/i18n/locales/pt.json index 9749ea2d4..5c25937fe 100644 --- a/electron/src/shared/i18n/locales/pt.json +++ b/electron/src/shared/i18n/locales/pt.json @@ -59,15 +59,10 @@ "title": "Pergunte ao VoiceStudio Agent", "notInstalled": "Não instalado", "noSource": "Abra um check-out de origem VoiceStudio gravável para permitir que um agente o repare.", - "noneFound": "Instale Codex, Claude Code, OpenCode ou Pi para usar o reparo local.", "ready": "Pronto", "placeholder": "O que deu errado? Adicione etapas, erros visíveis ou o comportamento esperado.", - "prReady": "Correção concluída. Revise as alterações e proponha um PR quando os testes direcionados forem aprovados.", "diagnose": "Diagnosticar", "fix": "Corrigir", - "chooseDefault": "Escolha o agente que o VoiceStudio usará automaticamente se esta tela falhar.", - "useAutomatically": "Usar {{agent}} automaticamente", - "captured": "{{count}} linhas de log com problemas capturadas", "contextNotice": "O agente selecionado recebe este relatório, a tela atual, os logs recentes do aplicativo e do backend e os diagnósticos do sistema.", "complete": "Completo" }, @@ -607,7 +602,6 @@ "characters": "{{count}} caracteres", "minutes": "~{{count}} minutos", "chars": "{{count}} caracteres", - "exportDone": "Audiolivro baixado", "exportFailed": "Falha na exportação – verifique se o back-end está em execução.", "global_speed": "Velocidade", "global_speed_hint": "Velocidade de leitura para todas as linhas sem sua própria substituição de velocidade", @@ -818,7 +812,6 @@ "data_dir": "Diretório de dados", "outputs": "Resultados", "crash_log": "Registro de falhas", - "update_endpoint": "Atualizar ponto de extremidade", "yes": "sim", "no": "não", "web_preview": "visualização da web", @@ -879,38 +872,21 @@ "title": "Credenciais", "desc": "As chaves e tokens de API são definidos <1>apenas para esta sessão. Para persistência entre reinicializações, defina-as como variáveis ​​de ambiente em seu perfil de shell.", "hf_token": "Token AbraçandoFace", - "hf_help": "Necessário para diarização de alto-falantes e downloads de modelos mais rápidos. Obtenha o seu em huggingface.co/settings/tokens.", - "translate_key": "Chave de API LLM", - "translate_help": "Opcional — para DeepL, OpenAI ou provedores de tradução pagos. Não é necessário para o Google Translate (nível gratuito).", "save": "Salvar", "saved": "✓ Definir", - "not_set": "✗ Não definido", "get_token": "Obter token →", - "saved_session": "{{key}} salvo para esta sessão", "save_failed": "Falha ao salvar", "save_error": "Falha ao salvar: {{message}}", "deepl_key": "Chave API DeepL", - "deepl_help": "Para mecanismo de tradução DeepL. Obtenha sua chave em deepl.com/pro-api.", "deepl_base_url": "URL base do DeepL", "deepl_base_url_help": "Endpoint personalizado da API DeepL (padrão: https://api.deepl.com/v2). Mudança para proxy/auto-hospedado.", "microsoft_key": "Chave do Microsoft Tradutor", - "microsoft_help": "Para mecanismo Microsoft Translator. Obtenha sua chave no Portal do Azure.", "microsoft_base_url": "URL base do Microsoft Tradutor", "microsoft_base_url_help": "Ponto de extremidade personalizado do Azure Translator (padrão: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "URL base do LLM", - "llm_base_url_help": "Para qualidade cinematográfica ou mecanismo LLM. Qualquer endpoint compatível com OpenAI (OpenAI, Ollama, LM Studio, etc.).", - "llm_model": "Modelo LLM", - "llm_model_help": "Nome do modelo a ser usado com o mecanismo LLM (por exemplo, gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "Abraçando o rosto", - "group_translate": "Tradução (LLM / Cinematográfica)", - "group_deepl": "Ponto final personalizado do DeepL", - "group_microsoft": "Ponto final personalizado do Microsoft Translator", "region": "Região", "aws_profile": "Perfil AWS" }, "engines": { - "banners_on": "Banners de palco ativados", - "banners_off": "Banners de palco desativados", "backend": "Back-end", "status": "Estado", "ready": "pronto", @@ -941,7 +917,6 @@ "dictation_model_hint": "Usado pelo ditado ao vivo e por este mecanismo ao transcrever.", "dictation_not_installed": "Não instalado — selecione para baixar", "active": "ativo", - "whyUnavailable": "O que falta", "diskDetails": "Detalhes do disco", "diskModelDownload": "Download do modelo", "diskPackageDownload": "Download de pacotes", @@ -965,11 +940,8 @@ "sectionReady": "Prontos para usar", "sectionMore": "Adicionar mais motores", "lastError": "Último erro: {{error}}", - "installedAndReady": "Instalado e pronto", - "notInstalled": "Não instalado", "available": "Disponível", "subprocessTitle": "Executa em seu próprio subprocesso + venv", - "inProcessTitle": "É executado no processo VoiceStudio Python", "testEngine": "Motor de teste", "testing": "Testando…", "recheck": "Verifique novamente", @@ -986,7 +958,6 @@ "routingUnknown": "Desconhecido", "routingEffectiveChip": "Funciona em {{device}} nesta máquina", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU selecionada, mas: {{reason}}", "selectCpuFallback": "{{engine}}: em execução na CPU — {{reason}}", "selectWithCaveat": "Alterado para {{engine}} — {{reason}}", "generateCaveat": "{{engine}} pode ficar lento nesta máquina — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Nenhum registro Tauri disponível. É executado apenas no shell da área de trabalho.", "empty_backend": "O log de tempo de execução está vazio. A atividade aparecerá aqui conforme o back-end a registra.", "title": "Registros", - "source_backend": "Back-end", - "source_frontend": "Interface", - "source_tauri": "Touro", "expand": "Expandir registros", "collapse": "Recolher registros", "expand_aria": "Expandir painel de registros", @@ -1104,12 +1072,10 @@ "unlock": "Desbloquear", "try_voice": "Experimente esta voz", "test_phrase": "Frase de teste", - "test_help": "Digite qualquer coisa – nós iremos gerá-la com as configurações atuais desta voz.", "test_placeholder": "Digite algo para ouvir essa voz dizer…", "gen_preview": "Gerar visualização", "generating": "Gerando…", "used_title": "Onde esta voz foi usada", - "used_empty": "Esta voz ainda não foi usada. Gere uma prévia acima ou escolha-a em uma dublagem.", "synth_clips_one": "{{count}} clipe de sintetizador", "synth_clips_other": "{{count}} clipes de sintetizador", "projects_count_one": "Projeto {{count}}", @@ -1152,9 +1118,7 @@ "translate": "Traduzir", "generate": "Gerar", "export": "Exportar", - "no_video": "Nenhum vídeo carregado", "segments": "segmentos", - "speakers": "alto-falantes", "video_dubbing_studio": "Estúdio de Dublagem de Vídeo", "drop_here": "Solte vídeo ou áudio aqui", "paste_url": "…ou cole o URL do YouTube/vídeo", @@ -1168,7 +1132,6 @@ "cookie_size_error": "A exportação de cookies deve ter no máximo 1 MB.", "save": "Salvar", "reset": "Redefinir", - "save_project": "Salvar projeto", "change_file": "Alterar arquivo", "import_srt": "Importar .srt", "import_srt_alt": "Importe .srt em vez disso", @@ -1186,9 +1149,6 @@ "retranslate": "Traduzir novamente", "transcript": "Transcrição", "generate_dub": "Gerar dublagem", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Carregar e transcrever", "multi_lang": "Multi-idioma", "prep_download": "Baixando vídeo…", @@ -1206,7 +1166,6 @@ "transcribing": "Transcrevendo áudio…", "elapsed": "decorrido", "remaining": "resta", - "sidebar_toggle": "Alternar barra lateral", "retry_transcription": "Tentar novamente a transcrição", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Quando o URL for um host com legenda (YouTube, Vimeo, TED…), extraia também as legendas originais e quaisquer traduções automáticas do YouTube. Semeia o editor sem executar o Whisper; pule Traduzir tudo para idiomas que o YouTube já cobre.", @@ -1249,7 +1208,6 @@ "engine_label": "Motor", "install_engine": "Instale este motor", "installing_engine": "…instalando", - "needs_dev_install": "precisa de instalação do desenvolvedor", "install_disabled_title": "As instalações estão desativadas em compilações empacotadas", "needs_install_suffix": "- precisa ser instalado", "glossary_title": "Fixar traduções para termos recorrentes (nomes, palavras de marca, jargão)", @@ -1283,7 +1241,6 @@ "original_track": "Originais", "selected_dub": "{{code}} (dublagem selecionada)", "dub_track": "{{code}} (dublagem)", - "export_tracks": "Exportar faixas:", "stopping": "Parando…", "stop_progress": "Parar ({{current}}/{{total}})", "regen_changed": "Regeneração {{count}} alterada", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} instalado", "install_failed": "Falha na instalação: {{message}}", "prep_elapsed": "{{time}} decorrido", - "add_language": "Adicionar idioma", "search_languages": "Pesquisar idiomas…", "more_to_narrow": "+{{count}} mais — digite para restringir", "no_matches": "Nenhuma correspondência", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Hora", - "spkr": "Spkr", "text": "Texto", "lang": "Lang", "voice": "Voz", @@ -1403,8 +1358,6 @@ "select_all_title": "Selecione tudo filtrado", "sel_count": "{{count}} sel", "select_title": "Selecione o segmento (shift+clique para ver o intervalo)", - "sync_title": "O áudio gerado tem {{pct}}% a duração do original", - "sync_label": "Sincronizar: {{pct}}%", "rate_title": "Ajuste da taxa de fala: {{ratio}}× em relação ao slot{{error}}", "speaker_id": "ID do palestrante", "translate_error_title": "Erro de tradução: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Restaurar texto original", "lang_default": "(Definitivo)", "voice_default": "Padrão", - "from_video": "Do vídeo", - "clone_profiles": "Clonar perfis", - "design_presets": "Predefinições de projeto", "preview_title": "Visualização ao vivo", "set_direction": "Defina a direção…", "edit_direction": "Editar direção…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Personalidade", - "pick_personality": "Escolha uma predefinição de personalidade…", "instruct": "Instruir", "reference_text": "Texto de referência", "language": "Idioma", @@ -1535,9 +1484,7 @@ "stories": "Histórias", "untitled_story": "História sem título", "story_lines": "{{count}} linha", - "story_lines_plural": "{{count}} linhas", "story_voices": "{{count}} voz", - "story_voices_plural": "{{count}} vozes", "audiobooks": "Audiolivros", "audiobook": "Audiolivro", "story": "História", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galeria", - "search_placeholder": "Pesquisar no YouTube…", - "all_voices": "Todas as vozes ({{count}})", - "no_voices": "Ainda não há vozes", "download_hint": "Pesquise acima para baixar clipes", - "search_btn": "Pesquisar", - "download_btn": "Baixar", "saving": "Salvando…", "trim": "Aparar", - "delete_confirm": "Excluir este clipe de voz?", - "view_mode_list": "Lista", - "view_mode_grid": "Grade", - "youtube_results": "Resultados do YouTube ({{count}})", - "clone_profile": "Clonar perfil", - "crop_audio": "Cortar áudio", "subtitle": "Centenas de vozes projetadas e prontas - escolha uma e pronto.", "zone_archetypes": "Arquétipos", "zone_imports": "Minhas importações", @@ -1658,8 +1594,6 @@ "delete": "Excluir", "community_empty": "Nenhuma voz da comunidade carregada ainda — conecte-se à internet e reabra, ou seja o primeiro a enviar uma.", "community_explainer": "Predefinições projetadas e vozes gravadas compartilhadas pela comunidade, carregadas da omnivoice-gallery.", - "no_designer": "Voz gravada — use \"Usar voz\" em vez disso.", - "no_preview": "Sem visualização — adicione-a com \"Usar voz\" para ouvi-la.", "submit_failed": "Não foi possível abrir o formulário de envio.", "submit_preset": "Enviar uma predefinição", "submit_voice": "Enviar uma voz", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Você já pode criar sua primeira voz. Configure o ditado agora ou depois nas Configurações.", - "welcome": "Bem vindo", "system_check": "Verificação do sistema", "install_models": "Instalar modelos", - "pick_engines": "Escolha motores", "system_check_desc": "Teste RAM, disco, GPU, ffmpeg e rede. Os bloqueadores são sinalizados antecipadamente para que você saiba antes de fazer o download.", "install_models_desc": "Baixe ~2,4 GB de pesos — apenas o modelo TTS é obrigatório. Adicione modelos de transcrição selecionados para seu hardware agora ou depois nas Configurações.", "pick_engines_desc": "Escolha back-ends TTS/ASR/LLM. Os padrões funcionam imediatamente: personalize a qualquer momento nas Configurações.", "hero_desc": "Dublagem, clonagem de voz e design de voz — tudo executado localmente em sua máquina.", - "first_run": "A primeira execução leva de 5 a 10 minutos para ser baixada. Depois disso, cada lançamento é instantâneo e totalmente offline.", - "get_started": "Comece", "back": "Voltar", "continue_ok": "Tudo bem - continuar", "continue_warn": "Continuar (com avisos)", @@ -1727,7 +1657,6 @@ "try_dictation": "Experimente o ditado", "try_dictation_desc": "Pressione seu atalho uma vez para confirmar que o ditado funciona antes de concluir a configuração.", "dictation_step_label": "Ditado", - "next_try_dictation": "Próximo: Experimente o ditado", "step_aria": "Etapa {{num}}: {{label}}", "step_completed": "concluída", "cache_label": "Cache de modelo" @@ -1743,7 +1672,6 @@ "platforms": "Plataformas", "footer": "Cada contribuição ajuda a ultrapassar os limites da IA local.", "back": "De volta ao estúdio", - "commercial": "Licença Comercial", "pill": "Suporte VoiceStudio", "suggested_title": "Escolha um valor", "most_common": "mais comum", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Apoie o desenvolvimento do VoiceStudio", - "lead_default": "Que bom que funcionou! VoiceStudio é gratuito e totalmente local. Se você economizar tempo, uma pequena contribuição mensal financiará Claude Max por trás disso.", - "lead_first_clone": "Seu primeiro clone de voz está pronto – ótimo! VoiceStudio é executado inteiramente em sua máquina e seu suporte mantém isso assim.", - "lead_tenth_dub": "Dez dublagens - você está claramente colocando isso para funcionar. Uma pequena contribuição mensal financia o Claude Max que fornece esses recursos.", - "lead_sustained": "Você está no VoiceStudio há um mês. Se conquistou um lugar em seu fluxo de trabalho, considere ajudar a financiar o que vem a seguir.", - "chip_in": "Contribua", - "later": "Talvez mais tarde", - "star": "Estrela no GitHub", - "opt_out": "Não pergunte novamente", - "dismiss_aria": "Dispensar" + "later": "Talvez mais tarde" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Licença Comercial", "hero_title": "Envie vozes de IA em produção", "hero_desc": "O VoiceStudio é um software livre e de código aberto sob a GNU Affero General Public License v3 (AGPL-3.0) — gratuito para qualquer uso, incluindo uso comercial e empresarial interno. Uma licença comercial só é necessária se você quiser incorporar o VoiceStudio em um produto ou serviço proprietário ou de código fechado sem as obrigações copyleft da AGPL-3.0.", - "why_title": "Por que as empresas escolhem VoiceStudio", - "pricing_title": "Preços", - "faq_title": "Perguntas comuns", "pricing_desc": "Os níveis e os preços ainda estão sendo finalizados.", - "pricing_detail": "Até que sejam públicas, cada implantação comercial será cotada individualmente para que possamos dimensionar adequadamente sua equipe e carga de trabalho.", "request_quote": "Solicite um orçamento", - "footer_email": "Perguntas? Entre em contato com hi@voicestudio.sh", - "footer_discord": "Junte-se ao nosso Discord para obter suporte da comunidade.", "benefit_ip": "Propriedade total de IP", "benefit_ip_desc": "Você possui todos os resultados, todos os modelos, todas as derivadas. Sem locação compartilhada, sem vazamento de dados.", "benefit_cost": "Custo zero por minuto", "benefit_cost_desc": "Sem contas de API, sem excedentes surpresa. Funciona em seu próprio hardware desde o primeiro dia.", - "benefit_team": "Acesso para toda a equipe", "benefit_team_desc": "Uma licença cobre usuários ilimitados em sua organização.", "benefit_support": "Suporte direto", "benefit_support_desc": "Suporte prioritário por e-mail e videochamada da equipe principal de engenharia.", - "benefit_source": "Núcleo disponível na fonte", "benefit_source_desc": "Visibilidade total da pilha. Audite, bifurque e adapte-se aos termos da licença.", - "benefit_lang": "646 idiomas", "benefit_lang_desc": "Transcreva, traduza e duble em 646 idiomas com qualidade de nível humano.", - "hero_note": "Uso, auto-hospedagem e uso comercial são gratuitos sob a AGPL-3.0 — inclusive em grande escala. A AGPL é uma licença copyleft de rede: se você modificar o VoiceStudio e oferecer essa versão modificada a terceiros pela rede, deverá compartilhar seu código-fonte modificado sob os mesmos termos. Uma licença comercial remove essas obrigações copyleft para implantações proprietárias de código fechado. Os planos de preços chegam em breve — entre em contato enquanto isso.", - "hero_simple": "VoiceStudio é gratuito e de código aberto sob a AGPL-3.0 – inclusive para uso comercial e interno. Você só precisa de uma licença comercial para incorporá-lo em um produto de código fechado, sem as obrigações de copyleft da AGPL.", - "contact_lead": "O preço é cotado por implantação para se adequar à sua equipe e carga de trabalho. Diga-me seu caso de uso e eu lhe farei um orçamento." + "hero_simple": "VoiceStudio é gratuito e de código aberto sob a AGPL-3.0 – inclusive para uso comercial e interno. Você só precisa de uma licença comercial para incorporá-lo em um produto de código fechado, sem as obrigações de copyleft da AGPL." }, "exportModal": { "export": "Exportar", @@ -2006,7 +1915,6 @@ "loaded_ready": "Carregado e pronto", "loading_first_run": "Carregando… (isso pode levar de 1 a 2 minutos na primeira execução)", "failed_to_load": "Falha ao carregar", - "not_loaded_yet": "Ainda não carregado — será carregado na primeira transcrição", "tts_not_loaded_yet": "Ainda não carregado — será carregado na primeira geração de voz", "error_check_logs": "Erro: {{error}}. Verifique os logs e tente reiniciar.", "check_logs_restart": "Verifique os logs para erros de carregamento do modelo. Tente reiniciar.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Elenco de alto-falante", - "auto_assign_title": "Atribuir automaticamente vozes de clones de alto-falantes extraídos", - "auto_cast": "Transmissão automática", - "all_cast": "Todo o elenco", - "segments_count": "{{count}} segmentos", - "assign_voice": "Atribuir voz…", - "from_video": "🎤 Do vídeo ({{name}})", - "no_profiles": "Nenhum perfil de voz salvo ainda.", - "preview_voice": "Visualizar voz" + "segments_count": "{{count}} segmentos" }, "checkpoint": { "timing_review": "{{count}} segmentos precisam de revisão de tempo", @@ -2203,8 +2104,6 @@ "play_failed": "Falha na reprodução: {{message}}" }, "models": { - "hf_token_set_toast": "Conjunto de tokens HuggingFace – downloads mais rápidos habilitados", - "hf_token_save_failed": "Falha ao salvar o token", "install_started": "Instalação iniciada — progresso na linha", "delete_confirm": "Excluir {{repoId}}? Você pode reinstalá-lo mais tarde.", "delete_confirm_title": "Excluir modelo", @@ -2247,8 +2146,6 @@ "not_installed": "não instalado", "required_tag": "obrigatório", "delete_btn": "Excluir", - "hf_token_btn": "Token HF", - "hf_set_title": "Defina o token HuggingFace para downloads mais rápidos", "get_token": "Obter token →", "reco_installed_for": "Pacote recomendado instalado para {{device}}", "reco_for": "Recomendado para {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Obrigatório ~{{size}} GB", "all_size": "Todos ~{{size}} GB", "req_tag": "solicitação", - "search_placeholder": "Pesquisar modelos…", - "search_label": "Pesquisar modelos", "no_matches": "Nenhum modelo corresponde aos seus filtros.", "sort_by": "Classificar por {{column}}", "column_model": "Modelo", @@ -2267,12 +2162,8 @@ "ready_badge": "Pronto", "loading_badge": "Carregando…", "idle_badge": "Inativo", - "started_downloading_required_one": "Iniciou o download do modelo necessário {{count}}", - "started_downloading_required_other": "Iniciou o download de {{count}} modelos necessários", "to_download": "baixar", "cached": "armazenado em cache", - "fast_download_badge": "download rápido", - "fast_download_title": "Downloads rápidos via Xet {{version}} — transferência paralela em partes", "voice_previews": "Prévias de voz", "voice_previews_desc": "Baixe prévias renderizadas com antecedência para que as vozes toquem na hora, mesmo antes de um modelo de voz terminar de baixar. Nada é baixado até você ativar isto.", "voice_previews_off": "Desativado — as prévias são geradas neste dispositivo", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Preciso de uma licença para ferramentas internas?", - "a_internal_tools": "Não. O uso por seus funcionários e contratados — incluindo modificação e auto-hospedagem internas — é gratuito sob a AGPL-3.0. Uma licença comercial só é necessária se você incorporar o VoiceStudio em um produto ou serviço proprietário ou de código fechado e não quiser cumprir as obrigações de compartilhamento de código-fonte da AGPL.", - "q_try_before": "Posso tentar antes de me comprometer?", - "a_try_before": "Sim. O aplicativo completo é gratuito para baixar, executar e auto-hospedar sob a AGPL-3.0 — nenhum acordo necessário. Quando quiser discutir uma licença comercial (uso proprietário), envie um e-mail e resolveremos os detalhes juntos.", - "q_watermark": "E a marca d’água?", - "a_watermark": "A marca de água invisível AudioSeal é incorporada por predefinição para todos. Pode desativá-la em Definições → Privacidade; afeta apenas o áudio gerado após a alteração." - }, "gallery_extra": { - "save_prompt": "Digite um nome para este perfil de voz:", - "saved_as_profile": "Voz salva como perfil!", - "save_profile_failed": "Falha ao salvar o perfil", - "download_failed": "Falha no download: {{message}}", - "trim_load_failed": "Falha ao carregar áudio para corte: {{message}}", - "upload_crop_failed": "Falha ao enviar voz cortada: {{message}}" + "trim_load_failed": "Falha ao carregar áudio para corte: {{message}}" }, "dub_workflow": { "preparing_audio": "Preparando áudio…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Carregando…", - "trimmed_loaded": "Áudio cortado carregado", - "toast_exported": "Exportado: {{name}}", - "toast_export_failed": "Falha na exportação: {{message}}", - "toast_open_folder_failed": "Não foi possível abrir a pasta: {{message}}", "toast_saving": "Salvando {{name}}...", "toast_saved": "Salvo: {{path}}", "toast_save_error": "Erro ao salvar: {{message}}", "toast_processing": "Processando {{name}}...", "toast_downloaded": "Baixado {{name}}", "toast_download_error": "Erro de download: {{message}}", - "toast_upload_first": "Clique primeiro em 'Carregar e transcrever' para que o vídeo seja processado no servidor antes de salvá-lo.", - "toast_save_failed": "Falha ao salvar: {{message}}", - "toast_opened": "Aberto: {{name}}", - "toast_project_deleted": "Projeto excluído", - "toast_restored_state": "Estado restaurado da geração anterior", - "toast_history_deleted": "Item do histórico excluído", "toast_flushed": "Liberado — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· modelo descarregado", "toast_flush_failed": "Falha na liberação: {{message}}", - "toast_project_saved": "Projeto salvo", - "toast_project_created": "Projeto criado" + "toast_project_saved": "Projeto salvo" }, "update": { "available": "Atualização {{version}} disponível", @@ -2531,13 +2398,11 @@ "not_writable": "sem permissão de escrita", "change": "Alterar…", "compute_title": "Computação", - "compute_label": "GPU / acelerador", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Canal de atualização", "channel_stable": "Estável", "channel_preview": "Preview (main mais recente)", - "network_title": "Rede", "region_label": "Região de download", "mirrors_title": "Mirrors personalizados (avançado)", "mirror_pypi": "URL do índice PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "Privacidade em primeiro lugar: suas vozes, gravações e projetos nunca saem desta máquina — sem conta, sem processamento na nuvem.", "resume_note": "Downloads interrompidos retomam sozinhos — fechar o app é seguro.", "eta_left": "faltam ~{{eta}}", - "first_sound_text": "Bem-vindo ao seu estúdio. Cada palavra que você ouve foi gerada nesta máquina, agora mesmo.", - "first_sound_done": "Essa voz? Gerada há segundos, localmente. Bem-vindo.", - "hf_token_title": "Token do Hugging Face (opcional)", - "hf_token_hint": "Desbloqueia modelos restritos — ex.: diarização de falantes para dublagem multivoz (pyannote). Token de conta gratuita; fica nesta máquina.", "hf_token_save": "Salvar", "hf_token_saving": "salvando…", "hf_token_saved": "Token do Hugging Face salvo", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} esquerda", "hf_token_card_title": "Adicione um token Hugging Face gratuito para downloads mais rápidos", "hf_token_get": "Não tem um token? Ganhe um grátis →", - "hf_token_saved_fast": "Token Hugging Face salvo – downloads agora são mais rápidos", "hf_token_inline_prompt": "Acelere os downloads com um token Hugging Face gratuito", "hf_token_inline_ph": "Cole o token hf_… (opcional)", "hf_token_get_short": "Ganhe um grátis →", @@ -2607,15 +2467,11 @@ "unstar_take": "Desmarcar — permitir limpeza", "play_take": "Carregar como saída ativa", "filter_starred": "Marcadas", - "load_take_failed": "Não foi possível carregar essa take: {{message}}", "mode_clone": "Clonagem", "mode_synth": "Síntese", "generation_seconds": "{{duration}} s", - "expand_text": "Expandir texto", - "collapse_text": "Recolher texto", "seed": "Seed {{seed}}", - "load_config": "Carregar configurações", - "delete_confirm": "Excluir este item do histórico?" + "load_config": "Carregar configurações" }, "openapi": { "title": "API do VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Prévia do patrocinador", "message": "Sua marca, site e mensagem", "email_app": "Abrir app de e-mail", - "copy_email": "Copiar e-mail", - "preview_detail": "Seu logo, link e apresentação podem aparecer aqui." + "copy_email": "Copiar e-mail" }, "supportPlans": { "remove": "Remover barra de patrocinadores", "title": "Grátis vs Pro", "free": "Grátis", "pro": "Pro", - "sponsor_bar": "Barra de patrocinadores", "visible": "Visível", - "hideable": "Pode ser ocultada", "unavailable": "A ativação do Pro ainda não foi configurada.", "telemetry": "Telemetria", - "opt_in": "Opcional, com consentimento", "disabled": "Desativada", - "badge": "Selo Pro", - "advanced": "Ferramentas avançadas", - "standard": "Padrão", - "included": "Incluídas" + "badge": "Selo Pro" }, "integrationCatalog": { "title": "Integrações", @@ -3042,8 +2891,7 @@ "n8nHint": "Importe este fluxo no n8n, edite a solicitação e execute-o. Mantenha o VoiceStudio aberto. Docker e n8n remoto precisam de um endereço de backend acessível e das credenciais necessárias." }, "directoryExamples": { - "example": "Exemplo do diretório", - "notice": "Apenas exemplos do diretório — não são patrocinadores nem integrações conectadas." + "example": "Exemplo do diretório" }, "workflowRun": { "run": "Executar fluxo", diff --git a/electron/src/shared/i18n/locales/ru.json b/electron/src/shared/i18n/locales/ru.json index 5f3c9e729..45b9e4be9 100644 --- a/electron/src/shared/i18n/locales/ru.json +++ b/electron/src/shared/i18n/locales/ru.json @@ -59,15 +59,10 @@ "title": "Спросить VoiceStudio Agent", "notInstalled": "Не установлено", "noSource": "Откройте доступную для записи копию исходного кода VoiceStudio, чтобы агент мог внести исправления.", - "noneFound": "Установите Codex, Claude Code, OpenCode или Pi для локального исправления.", "ready": "Готов", "placeholder": "Что пошло не так? Укажите шаги, видимые ошибки или ожидаемое поведение.", - "prReady": "Исправление завершено. Проверьте изменения и предложите PR после прохождения целевых тестов.", "diagnose": "Диагностика", "fix": "Исправить", - "chooseDefault": "Выберите агента, которого VoiceStudio будет автоматически использовать при сбое этого экрана.", - "useAutomatically": "Автоматически использовать {{agent}}", - "captured": "Собрано строк журнала с проблемами: {{count}}", "contextNotice": "Выбранный агент получит этот отчёт, текущий экран, последние журналы приложения и бэкенда, а также диагностику системы.", "complete": "Завершено" }, @@ -607,7 +602,6 @@ "characters": "{{count}} символов", "minutes": "~{{count}} мин.", "chars": "{{count}} символов", - "exportDone": "Аудиокнига скачана", "exportFailed": "Экспорт не удался — проверьте, работает ли серверная часть.", "global_speed": "Скорость", "global_speed_hint": "Скорость чтения для всех строк без собственного переопределения скорости.", @@ -818,7 +812,6 @@ "data_dir": "Каталог данных", "outputs": "Выходы", "crash_log": "Журнал сбоев", - "update_endpoint": "Обновить конечную точку", "yes": "да", "no": "нет", "web_preview": "веб-предварительный просмотр", @@ -879,38 +872,21 @@ "title": "Реквизиты для входа", "desc": "Ключи API и токены установлены <1>только для этого сеанса. Чтобы обеспечить сохранение при перезапуске, установите их как переменные среды в профиле оболочки.", "hf_token": "Токен Обнимающего Лица", - "hf_help": "Требуется для диаризации говорящих и более быстрой загрузки моделей. Получите свой на Huggingface.co/settings/tokens.", - "translate_key": "Ключ API LLM", - "translate_help": "Необязательно — для DeepL, OpenAI или платных поставщиков переводов. Не требуется для Google Translate (уровень бесплатного пользования).", "save": "Сохранять", "saved": "✓ Установить", - "not_set": "✗ Не установлено", "get_token": "Получить токен →", - "saved_session": "{{key}} сохранено для этого сеанса", "save_failed": "Не удалось сохранить", "save_error": "Не удалось сохранить: {{message}}", "deepl_key": "API-ключ DeepL", - "deepl_help": "Для механизма перевода DeepL. Получите ключ на deepl.com/pro-api.", "deepl_base_url": "Базовый URL-адрес DeepL", "deepl_base_url_help": "Пользовательская конечная точка API DeepL (по умолчанию: https://api.deepl.com/v2). Изменение на прокси/самостоятельное размещение.", "microsoft_key": "Ключ переводчика Microsoft", - "microsoft_help": "Для движка Microsoft Translator. Получите ключ на портале Azure.", "microsoft_base_url": "Базовый URL-адрес переводчика Microsoft", "microsoft_base_url_help": "Пользовательская конечная точка переводчика Azure (по умолчанию: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "Базовый URL-адрес LLM", - "llm_base_url_help": "Для кинематографического качества или движка LLM. Любая конечная точка, совместимая с OpenAI (OpenAI, Ollama, LM Studio и т. д.).", - "llm_model": "Модель LLM", - "llm_model_help": "Название модели для использования с механизмом LLM (например, gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "ОбниматьЛицо", - "group_translate": "Перевод (LLM/Кинематографический)", - "group_deepl": "Пользовательская конечная точка DeepL", - "group_microsoft": "Пользовательская конечная точка Microsoft Translator", "region": "Регион", "aws_profile": "Профиль AWS" }, "engines": { - "banners_on": "Баннеры на сцене", - "banners_off": "Сценические баннеры отключены", "backend": "Бэкэнд", "status": "Статус", "ready": "готовый", @@ -941,7 +917,6 @@ "dictation_model_hint": "Используется для живой диктовки и этим движком при транскрибации.", "dictation_not_installed": "Не установлена — выберите, чтобы скачать", "active": "активный", - "whyUnavailable": "Что нужно", "diskDetails": "Сведения о диске", "diskModelDownload": "Загрузка модели", "diskPackageDownload": "Загрузка пакетов", @@ -965,11 +940,8 @@ "sectionReady": "Готовы к использованию", "sectionMore": "Добавить больше двигателей", "lastError": "Последняя ошибка: {{error}}", - "installedAndReady": "Установлен и готов", - "notInstalled": "Не установлено", "available": "Доступный", "subprocessTitle": "Запускается в собственном подпроцессе + venv", - "inProcessTitle": "Запускается в процессе VoiceStudio Python.", "testEngine": "Тестовый двигатель", "testing": "Тестирование…", "recheck": "Перепроверить", @@ -986,7 +958,6 @@ "routingUnknown": "Неизвестный", "routingEffectiveChip": "Работает на {{device}} на этом компьютере", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "Графический процессор выбран, но: {{reason}}", "selectCpuFallback": "{{engine}}: работает на CPU — {{reason}}", "selectWithCaveat": "Переключено на {{engine}} — {{reason}}", "generateCaveat": "{{engine}} может работать медленно на этом компьютере — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Журнал Таури недоступен. Запускается только в оболочке рабочего стола.", "empty_backend": "Журнал выполнения пуст. Действия появятся здесь, когда серверная часть зарегистрирует их.", "title": "Журналы", - "source_backend": "Бэкэнд", - "source_frontend": "Внешний интерфейс", - "source_tauri": "Тавр", "expand": "Развернуть журналы", "collapse": "Свернуть журналы", "expand_aria": "Развернуть панель журналов", @@ -1104,12 +1072,10 @@ "unlock": "Разблокировать", "try_voice": "Попробуйте этот голос", "test_phrase": "Тестовая фраза", - "test_help": "Введите что угодно — мы сгенерируем это с текущими настройками этого голоса.", "test_placeholder": "Введите что-нибудь, чтобы услышать, как этот голос произнесет это…", "gen_preview": "Создать предварительный просмотр", "generating": "Создание…", "used_title": "Где использовался этот голос", - "used_empty": "Этот голос еще не использовался. Создайте предварительный просмотр выше или выберите его в дубляже.", "synth_clips_one": "{{count}} синтезаторный клип", "synth_clips_other": "{{count}} синтезаторных клипов", "projects_count_one": "{{count}} проект", @@ -1152,9 +1118,7 @@ "translate": "Переводить", "generate": "Генерировать", "export": "Экспорт", - "no_video": "Видео не загружено", "segments": "сегменты", - "speakers": "ораторы", "video_dubbing_studio": "Студия видео дубляжа", "drop_here": "Перекиньте сюда видео или аудио", "paste_url": "…или вставьте URL-адрес YouTube/видео", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Размер экспорта cookie не должен превышать 1 МБ.", "save": "Сохранять", "reset": "Перезагрузить", - "save_project": "Сохранить проект", "change_file": "Изменить файл", "import_srt": "Импортировать .srt", "import_srt_alt": "Вместо этого импортируйте .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Повторно перевести", "transcript": "Стенограмма", "generate_dub": "Создать дубляж", - "export_mp4": "МП4", - "export_wav": "WAV", - "export_srt": "СТО", "upload_transcribe": "Загрузить и расшифровать", "multi_lang": "Многоязычный", "prep_download": "Загрузка видео…", @@ -1206,7 +1166,6 @@ "transcribing": "Транскрипция аудио…", "elapsed": "истек", "remaining": "осталось", - "sidebar_toggle": "Переключить боковую панель", "retry_transcription": "Повторить транскрипцию", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Если URL-адрес является хостом с субтитрами (YouTube, Vimeo, TED…), также извлеките исходные субтитры и любые автоматические переводы YouTube. Заполняет редактор без запуска Whisper; пропустить «Перевести все» для языков, которые уже поддерживает YouTube.", @@ -1249,7 +1208,6 @@ "engine_label": "Двигатель", "install_engine": "Установите этот двигатель", "installing_engine": "…установка", - "needs_dev_install": "требуется установка разработчика", "install_disabled_title": "Установки отключены в упакованных сборках", "needs_install_suffix": "— требует установки", "glossary_title": "Закрепите переводы повторяющихся терминов (имен, слов бренда, жаргона)", @@ -1283,7 +1241,6 @@ "original_track": "Оригинал", "selected_dub": "{{code}} (Избранный дубляж)", "dub_track": "{{code}} (Дубляж)", - "export_tracks": "Экспорт треков:", "stopping": "Остановка…", "stop_progress": "Стоп ({{current}}/{{total}})", "regen_changed": "Регенерация {{count}} изменена", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} установлено", "install_failed": "Не удалось установить: {{message}}", "prep_elapsed": "{{time}} прошло", - "add_language": "Добавить язык", "search_languages": "Поиск языков…", "more_to_narrow": "+{{count}} еще — введите текст, чтобы сузить", "no_matches": "Нет совпадений", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Время", - "spkr": "Спкр", "text": "Текст", "lang": "Ланг", "voice": "Голос", @@ -1403,8 +1358,6 @@ "select_all_title": "Выбрать все отфильтрованные", "sel_count": "{{count}} выбрать", "select_title": "Выбрать сегмент (Shift+клик для выбора диапазона)", - "sync_title": "Сгенерированный звук составляет {{pct}}% продолжительности оригинала.", - "sync_label": "Синхронизация: {{pct}}%", "rate_title": "Соответствие скорости речи: {{ratio}}× относительно слота {{error}}", "speaker_id": "Идентификатор докладчика", "translate_error_title": "Ошибка перевода: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Восстановить исходный текст", "lang_default": "(Защита)", "voice_default": "По умолчанию", - "from_video": "Из видео", - "clone_profiles": "Клонировать профили", - "design_presets": "Пресеты дизайна", "preview_title": "Живой просмотр", "set_direction": "Установить направление…", "edit_direction": "Изменить направление…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Личность", - "pick_personality": "Выберите персональную предустановку…", "instruct": "Инструктировать", "reference_text": "Справочный текст", "language": "Язык", @@ -1535,9 +1484,7 @@ "stories": "Истории", "untitled_story": "История без названия", "story_lines": "{{count}} строка", - "story_lines_plural": "{{count}} строк", "story_voices": "{{count}} голос", - "story_voices_plural": "{{count}} голосов", "audiobooks": "Аудиокниги", "audiobook": "Аудиокнига", "story": "История", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Галерея", - "search_placeholder": "Искать на YouTube…", - "all_voices": "Все голоса ({{count}})", - "no_voices": "Голосов пока нет", "download_hint": "Выполните поиск выше, чтобы скачать клипы.", - "search_btn": "Поиск", - "download_btn": "Скачать", "saving": "Сохранение…", "trim": "Подрезать", - "delete_confirm": "Удалить эту голосовую запись?", - "view_mode_list": "Список", - "view_mode_grid": "Сетка", - "youtube_results": "Результаты YouTube ({{count}})", - "clone_profile": "Клонировать профиль", - "crop_audio": "Обрезать аудио", "subtitle": "Сотни готовых голосов — выберите один и вперед.", "zone_archetypes": "Архетипы", "zone_imports": "Мой импорт", @@ -1658,8 +1594,6 @@ "delete": "Удалить", "community_empty": "Голоса сообщества ещё не загружены — подключитесь к интернету и откройте снова, или станьте первым, кто поделится своим.", "community_explainer": "Созданные пресеты и записанные голоса, которыми поделилось сообщество; загружаются из omnivoice-gallery.", - "no_designer": "Записанный голос — используйте «Использовать голос».", - "no_preview": "Нет предпросмотра — добавьте через «Использовать голос», чтобы услышать его.", "submit_failed": "Не удалось открыть форму отправки.", "submit_preset": "Отправить пресет", "submit_voice": "Отправить голос", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Всё готово для создания первого голоса. Диктовку можно настроить сейчас или позже в настройках.", - "welcome": "Добро пожаловать", "system_check": "Проверка системы", "install_models": "Установить модели", - "pick_engines": "Выберите двигатели", "system_check_desc": "Проверьте оперативную память, диск, графический процессор, ffmpeg и сеть. Блокировщики помечаются заранее, поэтому вы узнаете об этом перед загрузкой.", "install_models_desc": "Скачайте ~2,4 ГБ весов — обязательна только модель TTS. Добавьте модели транскрипции, подобранные под ваше оборудование, сейчас или позже в Настройках.", "pick_engines_desc": "Выберите серверные части TTS/ASR/LLM. Значения по умолчанию работают «из коробки» — измените их в любое время в настройках.", "hero_desc": "Дублирование, клонирование голоса и оформление голоса — все это выполняется локально на вашем компьютере.", - "first_run": "Загрузка первого запуска занимает 5–10 минут. После этого каждый запуск происходит мгновенно и полностью оффлайн.", - "get_started": "Начать", "back": "Назад", "continue_ok": "Все хорошо — продолжайте", "continue_warn": "Продолжить (с предупреждениями)", @@ -1727,7 +1657,6 @@ "try_dictation": "Попробуйте диктовку", "try_dictation_desc": "Нажмите сочетание клавиш один раз, чтобы убедиться, что надиктовка работает, прежде чем завершить настройку.", "dictation_step_label": "Надиктовка", - "next_try_dictation": "Далее: Попробуйте диктовку", "step_aria": "Шаг {{num}}: {{label}}", "step_completed": "завершён", "cache_label": "Кэш модели" @@ -1743,7 +1672,6 @@ "platforms": "Платформы", "footer": "Каждый вклад помогает расширить границы местного ИИ.", "back": "Вернуться в студию", - "commercial": "Коммерческая лицензия", "pill": "Поддержка VoiceStudio", "suggested_title": "Выберите сумму", "most_common": "наиболее распространенный", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Поддержите разработку VoiceStudio", - "lead_default": "Рад, что это сработало! VoiceStudio бесплатен и полностью локальный. Если это сэкономит вам время, небольшой ежемесячный взнос пополнит фонд Claude Max, стоящий за этим.", - "lead_first_clone": "Ваш первый голосовой клон готов — отлично! VoiceStudio полностью работает на вашем компьютере, и ваша поддержка поддерживает эту функцию.", - "lead_tenth_dub": "Десять дублей — вы явно прикладываете усилия. Небольшой ежемесячный взнос финансирует Claude Max, который предоставляет эти функции.", - "lead_sustained": "Вы работаете с VoiceStudio уже месяц. Если он занял место в вашем рабочем процессе, подумайте о том, чтобы помочь профинансировать следующий проект.", - "chip_in": "Внесите свой вклад", - "later": "Может быть, позже", - "star": "Звезда на GitHub", - "opt_out": "Не спрашивай больше", - "dismiss_aria": "Закрыть" + "later": "Может быть, позже" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Коммерческая лицензия", "hero_title": "Запуск голосов ИИ в производство", "hero_desc": "VoiceStudio — свободное программное обеспечение с открытым исходным кодом под лицензией GNU Affero General Public License v3 (AGPL-3.0): бесплатно для любого использования, включая коммерческое и внутрикорпоративное. Коммерческая лицензия нужна только в том случае, если вы хотите встроить VoiceStudio в закрытый или проприетарный продукт или сервис без обязательств копилефта AGPL-3.0.", - "why_title": "Почему компании выбирают VoiceStudio", - "pricing_title": "Цены", - "faq_title": "Общие вопросы", "pricing_desc": "Уровни и цены еще уточняются.", - "pricing_detail": "Пока они не станут общедоступными, каждое коммерческое развертывание оценивается индивидуально, чтобы мы могли подобрать правильный размер для вашей команды и рабочей нагрузки.", "request_quote": "Запросить ценовое предложение", - "footer_email": "Вопросы? Свяжитесь с нами по адресу hi@voicestudio.sh.", - "footer_discord": "Присоединяйтесь к нашему Discord для поддержки сообщества.", "benefit_ip": "Полное владение интеллектуальной собственностью", "benefit_ip_desc": "Вы владеете каждым продуктом, каждой моделью, каждым производным продуктом. Никакой совместной аренды, никаких утечек данных.", "benefit_cost": "Нулевая поминутная стоимость", "benefit_cost_desc": "Никаких счетов за API и неожиданных перерасходов. Работает на вашем собственном оборудовании с первого дня.", - "benefit_team": "Доступ для всей команды", "benefit_team_desc": "Одна лицензия распространяется на неограниченное количество пользователей в вашей организации.", "benefit_support": "Прямая поддержка", "benefit_support_desc": "Приоритетная поддержка по электронной почте и видеозвонкам от основной команды инженеров.", - "benefit_source": "Ядро, доступное в исходном коде", "benefit_source_desc": "Полная видимость стека. Аудит, форк и адаптация в рамках условий лицензии.", - "benefit_lang": "646 языков", "benefit_lang_desc": "Транскрибируйте, переводите и дублируйте на 646 языках с качеством человеческого уровня.", - "hero_note": "Использование, самостоятельный хостинг и коммерческое применение бесплатны по лицензии AGPL-3.0 — в том числе в больших масштабах. AGPL — это лицензия с сетевым копилефтом: если вы модифицируете VoiceStudio и предоставляете изменённую версию другим по сети, вы обязаны открыть свой изменённый исходный код на тех же условиях. Коммерческая лицензия снимает эти обязательства копилефта для проприетарных закрытых развертываний. Тарифы появятся в ближайшее время — а пока свяжитесь с нами.", - "hero_simple": "VoiceStudio бесплатна и имеет открытый исходный код в соответствии с AGPL-3.0, в том числе для коммерческого и внутреннего использования. Вам нужна только коммерческая лицензия, чтобы встроить ее в продукт с закрытым исходным кодом без обязательств AGPL по авторскому левому.", - "contact_lead": "Цены указаны за развертывание, поэтому они соответствуют вашей команде и рабочей нагрузке. Расскажите мне свой вариант использования, и я сделаю вам предложение." + "hero_simple": "VoiceStudio бесплатна и имеет открытый исходный код в соответствии с AGPL-3.0, в том числе для коммерческого и внутреннего использования. Вам нужна только коммерческая лицензия, чтобы встроить ее в продукт с закрытым исходным кодом без обязательств AGPL по авторскому левому." }, "exportModal": { "export": "Экспорт", @@ -2006,7 +1915,6 @@ "loaded_ready": "Загружено и готово", "loading_first_run": "Загрузка… (при первом запуске это может занять 1–2 минуты)", "failed_to_load": "Не удалось загрузить", - "not_loaded_yet": "Еще не загружено — загрузится при первой транскрипции", "tts_not_loaded_yet": "Ещё не загружена — загрузится при первой генерации речи", "error_check_logs": "Ошибка: {{error}}. Проверьте журналы и попробуйте перезагрузить компьютер.", "check_logs_restart": "Проверьте журналы на предмет ошибок загрузки модели. Попробуйте перезапустить.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Кастинг спикеров", - "auto_assign_title": "Автоматическое назначение голосов из извлеченных клонов динамиков", - "auto_cast": "Автотрансляция", - "all_cast": "Все актеры", - "segments_count": "{{count}} сегментов", - "assign_voice": "Назначить голос…", - "from_video": "🎤 Из видео ({{name}})", - "no_profiles": "Голосовые профили пока не сохранены.", - "preview_voice": "Предварительный просмотр голоса" + "segments_count": "{{count}} сегментов" }, "checkpoint": { "timing_review": "Необходимо проверить тайминг {{count}} сегментов", @@ -2203,8 +2104,6 @@ "play_failed": "Игра не удалась: {{message}}" }, "models": { - "hf_token_set_toast": "Набор токенов HuggingFace — включена более быстрая загрузка", - "hf_token_save_failed": "Не удалось сохранить токен", "install_started": "Установка началась — прогресс по строке", "delete_confirm": "Удалить {{repoId}}? Вы можете переустановить его позже.", "delete_confirm_title": "Удалить модель", @@ -2247,8 +2146,6 @@ "not_installed": "не установлен", "required_tag": "необходимый", "delete_btn": "Удалить", - "hf_token_btn": "HF-токен", - "hf_set_title": "Установите токен HuggingFace для более быстрой загрузки", "get_token": "Получить токен →", "reco_installed_for": "Рекомендуемый пакет установлен для {{device}}", "reco_for": "Рекомендуется для {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Требуется ~{{size}} ГБ", "all_size": "Все ~{{size}} ГБ", "req_tag": "запрос", - "search_placeholder": "Поиск моделей…", - "search_label": "Поиск моделей", "no_matches": "Ни одна модель не соответствует вашим фильтрам.", "sort_by": "Сортировать по {{column}}", "column_model": "Модель", @@ -2267,12 +2162,8 @@ "ready_badge": "Готовый", "loading_badge": "Загрузка…", "idle_badge": "Праздный", - "started_downloading_required_one": "Начата загрузка {{count}} необходимой модели.", - "started_downloading_required_other": "Начата загрузка {{count}} необходимых моделей.", "to_download": "скачать", "cached": "кэшированный", - "fast_download_badge": "быстрая загрузка", - "fast_download_title": "Быстрая загрузка через Xet {{version}} — параллельная поблочная передача", "voice_previews": "Превью голосов", "voice_previews_desc": "Загрузите заранее подготовленные превью, чтобы голоса воспроизводились мгновенно — даже до того, как голосовая модель закончит загружаться. Ничего не загружается, пока вы это не включите.", "voice_previews_off": "Выключено — превью создаются на этом устройстве", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Нужна ли мне лицензия на внутренние инструменты?", - "a_internal_tools": "Нет. Использование вашими сотрудниками и подрядчиками — включая внутренние модификации и самостоятельный хостинг — бесплатно по лицензии AGPL-3.0. Коммерческая лицензия нужна только если вы встраиваете VoiceStudio в закрытый или проприетарный продукт или сервис и не хотите выполнять требования AGPL по раскрытию исходного кода.", - "q_try_before": "Могу ли я попробовать, прежде чем совершать?", - "a_try_before": "Да. Полную версию приложения можно бесплатно скачать, запускать и размещать самостоятельно по лицензии AGPL-3.0 — без какого-либо договора. Когда будете готовы обсудить коммерческую лицензию (проприетарное использование), напишите нам — вместе разберём детали.", - "q_watermark": "А что насчет водяного знака?", - "a_watermark": "Невидимый водяной знак AudioSeal по умолчанию встраивается для всех. Его можно отключить в Настройках → Конфиденциальность; это касается только аудио, созданного после изменения." - }, "gallery_extra": { - "save_prompt": "Введите имя для этого голосового профиля:", - "saved_as_profile": "Голос сохранен как профиль!", - "save_profile_failed": "Не удалось сохранить профиль", - "download_failed": "Загрузка не удалась: {{message}}", - "trim_load_failed": "Не удалось загрузить аудио для обрезки: {{message}}.", - "upload_crop_failed": "Не удалось загрузить обрезанный голос: {{message}}." + "trim_load_failed": "Не удалось загрузить аудио для обрезки: {{message}}." }, "dub_workflow": { "preparing_audio": "Подготовка аудио…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Загрузка…", - "trimmed_loaded": "Обрезанный звук загружен.", - "toast_exported": "Экспортировано: {{name}}", - "toast_export_failed": "Не удалось экспортировать: {{message}}", - "toast_open_folder_failed": "Не удалось открыть папку: {{message}}", "toast_saving": "Сохранение {{name}}...", "toast_saved": "Сохранено: {{path}}", "toast_save_error": "Ошибка сохранения: {{message}}", "toast_processing": "Обработка {{name}}...", "toast_downloaded": "Скачано {{name}}", "toast_download_error": "Ошибка загрузки: {{message}}", - "toast_upload_first": "Сначала нажмите «Загрузить и расшифровать», чтобы видео обработалось на сервере перед сохранением.", - "toast_save_failed": "Не удалось сохранить: {{message}}", - "toast_opened": "Открыто: {{name}}", - "toast_project_deleted": "Проект удален", - "toast_restored_state": "Восстановлено состояние предыдущего поколения", - "toast_history_deleted": "Элемент истории удален.", "toast_flushed": "Сброшено — ОЗУ {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· модель выгружена", "toast_flush_failed": "Сброс не выполнен: {{message}}", - "toast_project_saved": "Проект сохранен.", - "toast_project_created": "Проект создан" + "toast_project_saved": "Проект сохранен." }, "update": { "available": "Доступно обновление {{version}}", @@ -2531,13 +2398,11 @@ "not_writable": "нет записи", "change": "Изменить…", "compute_title": "Вычисления", - "compute_label": "GPU / ускоритель", "compute_auto": "Авто (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Канал обновлений", "channel_stable": "Стабильный", "channel_preview": "Предварительный (последний main)", - "network_title": "Сеть", "region_label": "Регион загрузки", "mirrors_title": "Свои зеркала (дополнительно)", "mirror_pypi": "URL индекса PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "Приватность прежде всего: ваши голоса, записи и проекты никогда не покидают эту машину — без аккаунта и без облачной обработки.", "resume_note": "Прерванные загрузки возобновляются автоматически — закрывать приложение безопасно.", "eta_left": "осталось ~{{eta}}", - "first_sound_text": "Добро пожаловать в вашу студию. Каждое слово, которое вы слышите, сгенерировано на этой машине только что.", - "first_sound_done": "Этот голос? Сгенерирован секунды назад, локально. Добро пожаловать.", - "hf_token_title": "Токен Hugging Face (необязательно)", - "hf_token_hint": "Открывает закрытые модели — например, диаризацию для многоголосого дубляжа (pyannote). Токен бесплатного аккаунта; остаётся на этой машине.", "hf_token_save": "Сохранить", "hf_token_saving": "сохранение…", "hf_token_saved": "Токен Hugging Face сохранён", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} осталось", "hf_token_card_title": "Добавьте бесплатный токен Hugging Face для более быстрой загрузки.", "hf_token_get": "У вас нет жетона? Получите один бесплатно →", - "hf_token_saved_fast": "Токен Hugging Face сохранен — загрузка теперь происходит быстрее", "hf_token_inline_prompt": "Ускорьте загрузку с помощью бесплатного токена Hugging Face.", "hf_token_inline_ph": "Вставьте токен hf_… (необязательно)", "hf_token_get_short": "Получите один бесплатно →", @@ -2607,15 +2467,11 @@ "unstar_take": "Снять отметку — разрешить очистку", "play_take": "Загрузить как активный вывод", "filter_starred": "Избранные", - "load_take_failed": "Не удалось загрузить дубль: {{message}}", "mode_clone": "Клонирование", "mode_synth": "Синтез", "generation_seconds": "{{duration}} с", - "expand_text": "Развернуть текст", - "collapse_text": "Свернуть текст", "seed": "Сид {{seed}}", - "load_config": "Загрузить настройки", - "delete_confirm": "Удалить этот элемент истории?" + "load_config": "Загрузить настройки" }, "openapi": { "title": "API VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Пример спонсорского блока", "message": "Ваш бренд, сайт и сообщение", "email_app": "Открыть почтовое приложение", - "copy_email": "Скопировать email", - "preview_detail": "Здесь могут появиться ваш логотип, ссылка и описание." + "copy_email": "Скопировать email" }, "supportPlans": { "remove": "Убрать панель спонсоров", "title": "Бесплатная версия и Pro", "free": "Бесплатно", "pro": "Pro", - "sponsor_bar": "Панель спонсоров", "visible": "Видна", - "hideable": "Можно скрыть", "unavailable": "Активация Pro ещё не настроена.", "telemetry": "Телеметрия", - "opt_in": "Необязательно, по согласию", "disabled": "Отключена", - "badge": "Значок Pro", - "advanced": "Расширенные инструменты", - "standard": "Стандартные", - "included": "Включены" + "badge": "Значок Pro" }, "integrationCatalog": { "title": "Интеграции", @@ -3042,8 +2891,7 @@ "n8nHint": "Импортируйте этот процесс в n8n, измените запрос и запустите его. Оставьте VoiceStudio запущенным. Для Docker и удалённого n8n нужны доступный адрес бэкенда и необходимые учётные данные." }, "directoryExamples": { - "example": "Пример в каталоге", - "notice": "Только примеры каталога — не спонсоры и не подключённые интеграции." + "example": "Пример в каталоге" }, "workflowRun": { "run": "Запустить процесс", diff --git a/electron/src/shared/i18n/locales/sv.json b/electron/src/shared/i18n/locales/sv.json index ccec1f04e..f63302e0e 100644 --- a/electron/src/shared/i18n/locales/sv.json +++ b/electron/src/shared/i18n/locales/sv.json @@ -59,15 +59,10 @@ "title": "Fråga VoiceStudio Agent", "notInstalled": "Inte installerad", "noSource": "Öppna en skrivbar VoiceStudio källcheck för att låta en agent reparera den.", - "noneFound": "Installera Codex, Claude Code, OpenCode eller Pi för att använda lokal reparation.", "ready": "Redo", "placeholder": "Vad gick fel? Lägg till steg, synliga fel eller förväntat beteende.", - "prReady": "Fixning klar. Se över ändringarna och föreslå en PR när de målinriktade testerna passerar.", "diagnose": "Diagnostisera", "fix": "Reparera", - "chooseDefault": "Välj agenten som VoiceStudio ska använda automatiskt om den här vyn kraschar.", - "useAutomatically": "Använd {{agent}} automatiskt", - "captured": "{{count}} problemrader i loggen samlades in", "contextNotice": "Den valda agenten får den här rapporten, den aktuella vyn, aktuella app- och backendloggar samt systemdiagnostik.", "complete": "Klart" }, @@ -607,7 +602,6 @@ "characters": "{{count}} tecken", "minutes": "~{{count}} min", "chars": "{{count}} tecken", - "exportDone": "Ljudbok nedladdad", "exportFailed": "Exporten misslyckades – kontrollera att backend körs.", "global_speed": "Hastighet", "global_speed_hint": "Läshastighet för alla linjer utan egen hastighetsöverstyrning", @@ -818,7 +812,6 @@ "data_dir": "Datakatalog", "outputs": "Utgångar", "crash_log": "Krocklogg", - "update_endpoint": "Uppdatera slutpunkt", "yes": "ja", "no": "nej", "web_preview": "webbförhandsvisning", @@ -879,38 +872,21 @@ "title": "Inloggningsuppgifter", "desc": "API-nycklar och tokens är inställda <1>endast för denna session. För beständighet över omstarter, ställ in dem som miljövariabler i din skalprofil.", "hf_token": "HuggingFace Token", - "hf_help": "Krävs för högtalardiarisering och snabbare modellnedladdningar. Få din på huggingface.co/settings/tokens.", - "translate_key": "LLM API-nyckel", - "translate_help": "Valfritt – för leverantörer av DeepL, OpenAI eller betalda översättningar. Behövs inte för Google Translate (gratis nivå).", "save": "Spara", "saved": "✓ Ställ in", - "not_set": "✗ Ej inställd", "get_token": "Få token →", - "saved_session": "{{key}} har sparats för denna session", "save_failed": "Det gick inte att spara", "save_error": "Det gick inte att spara: {{message}}", "deepl_key": "DeepL API-nyckel", - "deepl_help": "För DeepL översättningsmotor. Få din nyckel på deepl.com/pro-api.", "deepl_base_url": "DeepL Base URL", "deepl_base_url_help": "Anpassad DeepL API-slutpunkt (standard: https://api.deepl.com/v2). Ändra för proxy/självvärd.", "microsoft_key": "Microsoft Translator Key", - "microsoft_help": "För Microsoft Translator-motorn. Få din nyckel från Azure Portal.", "microsoft_base_url": "Microsoft Translator Base URL", "microsoft_base_url_help": "Anpassad Azure Translator-slutpunkt (standard: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "LLM Bas URL", - "llm_base_url_help": "För filmisk kvalitet eller LLM-motor. Alla OpenAI-kompatibla slutpunkter (OpenAI, Ollama, LM Studio, etc.).", - "llm_model": "LLM modell", - "llm_model_help": "Modellnamn som ska användas med LLM-motorn (t.ex. gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "KramarFace", - "group_translate": "Översättning (LLM / filmisk)", - "group_deepl": "DeepL Custom Endpoint", - "group_microsoft": "Microsoft Translator anpassad slutpunkt", "region": "Region", "aws_profile": "AWS-profil" }, "engines": { - "banners_on": "Scenbanderoller på", - "banners_off": "Stage banderoller av", "backend": "Backend", "status": "Status", "ready": "redo", @@ -941,7 +917,6 @@ "dictation_model_hint": "Används av livediktering och av den här motorn vid transkribering.", "dictation_not_installed": "Inte installerad — välj för att ladda ner", "active": "aktiv", - "whyUnavailable": "Vad som krävs", "diskDetails": "Diskdetaljer", "diskModelDownload": "Modellhämtning", "diskPackageDownload": "Pakethämtning", @@ -965,11 +940,8 @@ "sectionReady": "Redo att använda", "sectionMore": "Lägg till fler motorer", "lastError": "Senaste fel: {{error}}", - "installedAndReady": "Installerad och klar", - "notInstalled": "Ej installerad", "available": "Tillgänglig", "subprocessTitle": "Körs i sin egen delprocess + venv", - "inProcessTitle": "Körs i VoiceStudio Python-processen", "testEngine": "Testa motor", "testing": "Testar...", "recheck": "Kontrollera igen", @@ -986,7 +958,6 @@ "routingUnknown": "Okänd", "routingEffectiveChip": "Körs på {{device}} på den här maskinen", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU vald, men: {{reason}}", "selectCpuFallback": "{{engine}}: körs på CPU — {{reason}}", "selectWithCaveat": "Bytte till {{engine}} — {{reason}}", "generateCaveat": "{{engine}} kan vara långsam på den här datorn — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Ingen Tauri-logg tillgänglig. Körs endast i skrivbordsskalet.", "empty_backend": "Körtidsloggen är tom. Aktivitet kommer att visas här när backend loggar den.", "title": "Loggar", - "source_backend": "Backend", - "source_frontend": "Frontend", - "source_tauri": "Tauri", "expand": "Expandera loggar", "collapse": "Komprimera loggar", "expand_aria": "Expandera loggpanelen", @@ -1104,12 +1072,10 @@ "unlock": "Lås upp", "try_voice": "Prova den här rösten", "test_phrase": "Testfras", - "test_help": "Skriv vad som helst – vi genererar det med den här röstens nuvarande inställningar.", "test_placeholder": "Skriv något för att höra den här rösten säga det...", "gen_preview": "Skapa förhandsvisning", "generating": "Genererar...", "used_title": "Där denna röst har använts", - "used_empty": "Den här rösten har inte använts ännu. Skapa en förhandsvisning ovan eller välj den i en dubbning.", "synth_clips_one": "{{count}} synthklipp", "synth_clips_other": "{{count}} synthklipp", "projects_count_one": "{{count}} projekt", @@ -1152,9 +1118,7 @@ "translate": "Översätt", "generate": "Generera", "export": "Exportera", - "no_video": "Ingen video laddad", "segments": "segment", - "speakers": "högtalare", "video_dubbing_studio": "Videodubbningsstudio", "drop_here": "Släpp video eller ljud här", "paste_url": "…eller klistra in YouTube/videons URL", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Cookie-exporten får vara högst 1 MB.", "save": "Spara", "reset": "Återställ", - "save_project": "Spara projekt", "change_file": "Byt fil", "import_srt": "Importera .srt", "import_srt_alt": "Importera .srt istället", @@ -1186,9 +1149,6 @@ "retranslate": "Översätt igen", "transcript": "Avskrift", "generate_dub": "Generera Dub", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Ladda upp och transkribera", "multi_lang": "Flerspråkig", "prep_download": "Laddar ned video...", @@ -1206,7 +1166,6 @@ "transcribing": "Transkriberar ljud...", "elapsed": "förflutit", "remaining": "s kvar", - "sidebar_toggle": "Växla sidofält", "retry_transcription": "Försök att transkriptera igen", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "När webbadressen är en bildtextbärande värd (YouTube, Vimeo, TED...), dra även de ursprungliga bildtexterna och eventuella automatiska översättningar på YouTube. Såddar redaktören utan att köra Whisper; hoppa över Översätt alla för språk som YouTube redan täcker.", @@ -1249,7 +1208,6 @@ "engine_label": "Motor", "install_engine": "Installera denna motor", "installing_engine": "...installerar", - "needs_dev_install": "behöver dev-installation", "install_disabled_title": "Installationer är inaktiverade i paketerade versioner", "needs_install_suffix": "- behöver installeras", "glossary_title": "Fäst översättningar för återkommande termer (namn, varumärkesord, jargong)", @@ -1283,7 +1241,6 @@ "original_track": "Original", "selected_dub": "{{code}} (vald dub)", "dub_track": "{{code}} (Dubb)", - "export_tracks": "Exportera spår:", "stopping": "Stoppar...", "stop_progress": "Stopp ({{current}}/{{total}})", "regen_changed": "Regen {{count}} ändrades", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} installerad", "install_failed": "Installationen misslyckades: {{message}}", "prep_elapsed": "{{time}} har gått", - "add_language": "Lägg till språk", "search_languages": "Sök efter språk...", "languages_selected_one": "{{count}} språk valt", "languages_selected_other": "{{count}} språk har valts", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Tid", - "spkr": "Spkr", "text": "Text", "lang": "Lang", "voice": "Röst", @@ -1403,8 +1358,6 @@ "select_all_title": "Välj alla filtrerade", "sel_count": "{{count}} sel", "select_title": "Välj segment (skift+klicka för intervall)", - "sync_title": "Genererat ljud är {{pct}}% av originalets längd", - "sync_label": "Synkronisera: {{pct}}%", "rate_title": "Talhastighetsanpassning: {{ratio}}× i förhållande till plats{{error}}", "speaker_id": "Högtalar-ID", "translate_error_title": "Översättningsfel: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Återställ originaltext", "lang_default": "(Def)", "voice_default": "Standard", - "from_video": "Från video", - "clone_profiles": "Klona profiler", - "design_presets": "Designförinställningar", "preview_title": "Live Preview", "set_direction": "Ställ in riktning...", "edit_direction": "Redigera riktning...", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Personlighet", - "pick_personality": "Välj en personlighetsförinställning...", "instruct": "Instruera", "reference_text": "Referenstext", "language": "Språk", @@ -1535,9 +1484,7 @@ "stories": "Berättelser", "untitled_story": "Berättelse utan titel", "story_lines": "{{count}} linje", - "story_lines_plural": "{{count}} rader", "story_voices": "{{count}} röst", - "story_voices_plural": "{{count}} röster", "audiobooks": "Ljudböcker", "audiobook": "Ljudbok", "story": "Berättelse", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galleri", - "search_placeholder": "Sök på YouTube...", - "all_voices": "Alla röster ({{count}})", - "no_voices": "Inga röster än", "download_hint": "Sök ovan för att ladda ner klipp", - "search_btn": "Sök", - "download_btn": "Ladda ner", "saving": "Sparar...", "trim": "Trimma", - "delete_confirm": "Vill du ta bort det här röstklippet?", - "view_mode_list": "Lista", - "view_mode_grid": "Grid", - "youtube_results": "YouTube-resultat ({{count}})", - "clone_profile": "Klona profil", - "crop_audio": "Beskär ljud", "subtitle": "Hundratals färdigdesignade röster — välj en och gå.", "zone_archetypes": "Arketyper", "zone_imports": "Mina importer", @@ -1658,8 +1594,6 @@ "delete": "Ta bort", "community_empty": "Inga communityröster har laddats ännu — anslut till internet och öppna igen, eller bli först med att skicka in en.", "community_explainer": "Designade förinställningar och inspelade röster som delas av communityn, laddade från omnivoice-gallery.", - "no_designer": "Inspelad röst — använd \"Använd röst\" i stället.", - "no_preview": "Ingen förhandsgranskning — lägg till den med \"Använd röst\" för att höra den.", "submit_failed": "Det gick inte att öppna inskickningsformuläret.", "submit_preset": "Skicka in en förinställning", "submit_voice": "Skicka in en röst", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Du kan nu skapa din första röst. Ställ in diktering nu eller senare i Inställningar.", - "welcome": "Välkommen", "system_check": "Systemkontroll", "install_models": "Installera modeller", - "pick_engines": "Välj motorer", "system_check_desc": "Probe RAM, disk, GPU, ffmpeg och nätverk. Blockerare flaggas i förväg så att du vet innan du laddar ner.", "install_models_desc": "Ladda ner ~2,4 GB vikter — endast TTS-modellen krävs. Lägg till transkriptionsmodeller utvalda för din hårdvara nu eller senare via Inställningar.", "pick_engines_desc": "Välj TTS / ASR / LLM backends. Standardinställningarna fungerar direkt – anpassa när som helst i Inställningar.", "hero_desc": "Dubbning, röstkloning och röstdesign – allt körs lokalt på din maskin.", - "first_run": "Första körningen tar 5–10 minuter att ladda ner. Efter det är varje lansering omedelbar och helt offline.", - "get_started": "Kom igång", "back": "Tillbaka", "continue_ok": "Allt bra - fortsätt", "continue_warn": "Fortsätt (med varningar)", @@ -1727,7 +1657,6 @@ "try_dictation": "Prova diktering", "try_dictation_desc": "Tryck på din genväg en gång för att se att diktering fungerar innan du slutför installationen.", "dictation_step_label": "Diktering", - "next_try_dictation": "Nästa: Prova diktering", "step_aria": "Steg {{num}}: {{label}}", "step_completed": "klart", "cache_label": "Modellcache" @@ -1743,7 +1672,6 @@ "platforms": "Plattformar", "footer": "Varje bidrag hjälper till att tänja på gränserna för lokal AI.", "back": "Tillbaka till Studio", - "commercial": "Kommersiell licens", "pill": "Stöd VoiceStudio", "suggested_title": "Välj ett belopp", "most_common": "vanligaste", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Stöd utvecklingen av VoiceStudio", - "lead_default": "Kul att det fungerade! VoiceStudio är gratis och helt lokalt. Om det sparar tid, finansierar ett litet månatligt chip-in Claude Max bakom det.", - "lead_first_clone": "Din första röstklon är klar – trevligt! VoiceStudio körs helt och hållet på din maskin, och din support håller det så.", - "lead_tenth_dub": "Tio dubbar in — du sätter helt klart igång det. Ett litet månatligt chip-in finansierar Claude Max som levererar dessa funktioner.", - "lead_sustained": "Du har varit hos VoiceStudio i en månad. Om det har fått en plats i ditt arbetsflöde, överväg att hjälpa till att finansiera vad som händer härnäst.", - "chip_in": "Chip in", - "later": "Kanske senare", - "star": "Stjärna på GitHub", - "opt_out": "Fråga inte igen", - "dismiss_aria": "Avvisa" + "later": "Kanske senare" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Kommersiell licens", "hero_title": "Ship AI-röster i produktion", "hero_desc": "VoiceStudio är fri programvara med öppen källkod under GNU Affero General Public License v3 (AGPL-3.0) — gratis att använda, även för kommersiellt och internt företagsbruk. En kommersiell licens behövs bara om du vill bädda in VoiceStudio i en proprietär produkt eller tjänst med stängd källkod utan AGPL-3.0:s copyleft-skyldigheter.", - "why_title": "Varför företag väljer VoiceStudio", - "pricing_title": "Prissättning", - "faq_title": "Vanliga frågor", "pricing_desc": "Nivåer och prissättning håller fortfarande på att slutföras.", - "pricing_detail": "Tills de är offentliga citeras varje kommersiell distribution individuellt så att vi kan anpassa storleken för ditt team och din arbetsbelastning.", "request_quote": "Begär en offert", - "footer_email": "Frågor? Kontakta hi@voicestudio.sh", - "footer_discord": "Gå med i vår Discord för samhällsstöd.", "benefit_ip": "Fullt IP-ägande", "benefit_ip_desc": "Du äger varje utgång, varje modell, varje derivat. Ingen delad hyresrätt, inga dataläckor.", "benefit_cost": "Noll kostnad per minut", "benefit_cost_desc": "Inga API-räkningar, inga överraskande överskott. Körs på din egen hårdvara från dag ett.", - "benefit_team": "Teamomfattande åtkomst", "benefit_team_desc": "En licens täcker obegränsade användare inom din organisation.", "benefit_support": "Direkt stöd", "benefit_support_desc": "Prioriterad support för e-post och videosamtal från kärnteknikteamet.", - "benefit_source": "Källa-tillgänglig kärna", "benefit_source_desc": "Full insyn i stapeln. Granska, fördela och anpassa inom licensvillkoren.", - "benefit_lang": "646 språk", "benefit_lang_desc": "Transkribera, översätt och dubba på 646 språk med kvalitet på mänsklig nivå.", - "hero_note": "Användning, självhosting och kommersiellt bruk är gratis under AGPL-3.0 — även i stor skala. AGPL är en nätverks-copyleft-licens: om du modifierar VoiceStudio och erbjuder den modifierade versionen till andra över ett nätverk måste du dela din modifierade källkod på samma villkor. En kommersiell licens lyfter dessa copyleft-skyldigheter för proprietära driftsättningar med stängd källkod. Prisplaner kommer snart — hör av dig under tiden.", - "hero_simple": "VoiceStudio är gratis och öppen källkod enligt AGPL-3.0 – inklusive för kommersiellt och internt affärsbruk. Du behöver bara en kommersiell licens för att bädda in den i en produkt med sluten källkod utan AGPL:s copyleft-skyldigheter.", - "contact_lead": "Priset anges per implementering så att det passar ditt team och arbetsbelastning. Berätta för mig ditt användningsfall så ska jag ge dig en offert." + "hero_simple": "VoiceStudio är gratis och öppen källkod enligt AGPL-3.0 – inklusive för kommersiellt och internt affärsbruk. Du behöver bara en kommersiell licens för att bädda in den i en produkt med sluten källkod utan AGPL:s copyleft-skyldigheter." }, "exportModal": { "export": "Exportera", @@ -2006,7 +1915,6 @@ "loaded_ready": "Laddat och klart", "loading_first_run": "Laddar... (detta kan ta 1-2 minuter vid första körningen)", "failed_to_load": "Det gick inte att ladda", - "not_loaded_yet": "Inte laddad än – kommer att laddas vid första transkription", "tts_not_loaded_yet": "Inte inläst ännu – läses in vid den första röstgenereringen", "error_check_logs": "Fel: {{error}}. Kontrollera loggar och försök starta om.", "check_logs_restart": "Kontrollera loggar för modellladdningsfel. Testa att starta om.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Högtalargjutning", - "auto_assign_title": "Autotilldela röster från extraherade högtalarkloner", - "auto_cast": "Autocast", - "all_cast": "Alla medverkande", - "segments_count": "{{count}} segment", - "assign_voice": "Tilldela röst...", - "from_video": "🎤 Från video ({{name}})", - "no_profiles": "Inga röstprofiler har sparats ännu.", - "preview_voice": "Förhandsgranska röst" + "segments_count": "{{count}} segment" }, "checkpoint": { "timing_review": "{{count}} segment behöver tidsgranskas", @@ -2203,8 +2104,6 @@ "play_failed": "Uppspelning misslyckades: {{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace token set — snabbare nedladdningar aktiverade", - "hf_token_save_failed": "Det gick inte att spara token", "install_started": "Installationen påbörjad — framsteg i raden", "delete_confirm": "Ta bort {{repoId}}? Du kan installera om det senare.", "delete_confirm_title": "Ta bort modell", @@ -2247,8 +2146,6 @@ "not_installed": "inte installerat", "required_tag": "krävs", "delete_btn": "Ta bort", - "hf_token_btn": "HF-token", - "hf_set_title": "Ställ in HuggingFace-token för snabbare nedladdningar", "get_token": "Få token →", "reco_installed_for": "Rekommenderat paket installerat för {{device}}", "reco_for": "Rekommenderas för {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Krävs ~{{size}} GB", "all_size": "Alla ~{{size}} GB", "req_tag": "req", - "search_placeholder": "Sök efter modeller...", - "search_label": "Sök efter modeller", "no_matches": "Inga modeller matchar dina filter.", "sort_by": "Sortera efter {{column}}", "column_model": "Modell", @@ -2267,12 +2162,8 @@ "ready_badge": "Klar", "loading_badge": "Laddar...", "idle_badge": "Inaktiv", - "started_downloading_required_one": "Började ladda ner {{count}} obligatorisk modell", - "started_downloading_required_other": "Började ladda ner {{count}} nödvändiga modeller", "to_download": "att ladda ner", "cached": "cachad", - "fast_download_badge": "snabb nedladdning", - "fast_download_title": "Snabba nedladdningar via Xet {{version}} — parallell överföring i bitar", "voice_previews": "Röstförhandsvisningar", "voice_previews_desc": "Ladda ner förrenderade förhandsvisningar så att röster spelas upp direkt – redan innan en röstmodell har laddats ner. Inget laddas ner förrän du slår på det här.", "voice_previews_off": "Av – förhandsvisningar renderas på den här datorn", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Behöver jag en licens för interna verktyg?", - "a_internal_tools": "Nej. Användning av dina anställda och konsulter — inklusive intern modifiering och självhosting — är gratis under AGPL-3.0. En kommersiell licens behövs bara om du bäddar in VoiceStudio i en proprietär produkt eller tjänst med stängd källkod och inte vill följa AGPL:s krav på källkodsdelning.", - "q_try_before": "Kan jag prova innan jag binder mig?", - "a_try_before": "Ja. Hela appen är gratis att ladda ner, köra och självhosta under AGPL-3.0 — inget avtal krävs. När du vill diskutera en kommersiell licens (proprietär användning), mejla oss så går vi igenom detaljerna tillsammans.", - "q_watermark": "Hur är det med vattenstämpeln?", - "a_watermark": "Den osynliga AudioSeal-vattenstämpeln bäddas in som standard för alla. Du kan stänga av den under Inställningar → Integritet; den påverkar bara ljud som genereras efter ändringen." - }, "gallery_extra": { - "save_prompt": "Ange ett namn för denna röstprofil:", - "saved_as_profile": "Rösten har sparats som profil!", - "save_profile_failed": "Det gick inte att spara profilen", - "download_failed": "Nedladdning misslyckades: {{message}}", - "trim_load_failed": "Det gick inte att ladda ljud för trimning: {{message}}", - "upload_crop_failed": "Det gick inte att ladda upp beskuren röst: {{message}}" + "trim_load_failed": "Det gick inte att ladda ljud för trimning: {{message}}" }, "dub_workflow": { "preparing_audio": "Förbereder ljud...", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Laddar...", - "trimmed_loaded": "Trimmat ljud har laddats", - "toast_exported": "Exporterad: {{name}}", - "toast_export_failed": "Export misslyckades: {{message}}", - "toast_open_folder_failed": "Kunde inte öppna mappen: {{message}}", "toast_saving": "Sparar {{name}}...", "toast_saved": "Sparad: {{path}}", "toast_save_error": "Spara fel: {{message}}", "toast_processing": "Bearbetar {{name}}...", "toast_downloaded": "Nedladdat {{name}}", "toast_download_error": "Nedladdningsfel: {{message}}", - "toast_upload_first": "Klicka på \"Ladda upp och transkribera\" först så att videon bearbetas på servern innan du sparar.", - "toast_save_failed": "Det gick inte att spara: {{message}}", - "toast_opened": "Öppnad: {{name}}", - "toast_project_deleted": "Projektet raderat", - "toast_restored_state": "Återställd föregående generations tillstånd", - "toast_history_deleted": "Historikobjektet har tagits bort", "toast_flushed": "Spolat — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· modell avlastad", "toast_flush_failed": "Spolning misslyckades: {{message}}", - "toast_project_saved": "Projektet sparat", - "toast_project_created": "Projekt skapat" + "toast_project_saved": "Projektet sparat" }, "update": { "available": "Uppdatering {{version}} tillgänglig", @@ -2531,13 +2398,11 @@ "not_writable": "ej skrivbar", "change": "Ändra…", "compute_title": "Beräkning", - "compute_label": "GPU / accelerator", "compute_auto": "Auto (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD GPU (ROCm, Linux)", "channel_label": "Uppdateringskanal", "channel_stable": "Stabil", "channel_preview": "Förhandsvisning (senaste main)", - "network_title": "Nätverk", "region_label": "Nedladdningsregion", "mirrors_title": "Egna speglar (avancerat)", "mirror_pypi": "PyPI-index-URL", @@ -2570,10 +2435,6 @@ "trust_line": "Integritet först: dina röster, inspelningar och projekt lämnar aldrig den här datorn — inget konto, ingen molnbearbetning.", "resume_note": "Avbrutna nedladdningar återupptas automatiskt — det är säkert att stänga appen.", "eta_left": "~{{eta}} kvar", - "first_sound_text": "Välkommen till din studio. Varje ord du hör genererades på den här datorn, alldeles nyss.", - "first_sound_done": "Den rösten? Genererad för några sekunder sedan, lokalt. Välkommen.", - "hf_token_title": "Hugging Face-token (valfritt)", - "hf_token_hint": "Låser upp begränsade modeller — t.ex. talardiarisering för dubbning med flera röster (pyannote). Token från gratiskonto; stannar på den här datorn.", "hf_token_save": "Spara", "hf_token_saving": "sparar…", "hf_token_saved": "Hugging Face-token sparad", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} kvar", "hf_token_card_title": "Lägg till ett gratis Hugging Face-token för snabbare nedladdningar", "hf_token_get": "Har du ingen token? Få en gratis →", - "hf_token_saved_fast": "Hugging Face-token har sparats — nedladdningar går nu snabbare", "hf_token_inline_prompt": "Snabba upp nedladdningarna med ett gratis Hugging Face-token", "hf_token_inline_ph": "Klistra in hf_… token (valfritt)", "hf_token_get_short": "Få en gratis →", @@ -2607,15 +2467,11 @@ "unstar_take": "Ta bort stjärna — tillåt rensning", "play_take": "Läs in som aktiv utmatning", "filter_starred": "Stjärnmärkta", - "load_take_failed": "Kunde inte läsa in tagningen: {{message}}", "mode_clone": "Kloning", "mode_synth": "Syntes", "generation_seconds": "{{duration}} s", - "expand_text": "Expandera text", - "collapse_text": "Fäll ihop text", "seed": "Seed {{seed}}", - "load_config": "Läs in inställningar", - "delete_confirm": "Ta bort det här historikobjektet?" + "load_config": "Läs in inställningar" }, "openapi": { "title": "VoiceStudio API", @@ -3015,25 +2871,18 @@ "preview": "Förhandsvisning för sponsorer", "message": "Ditt varumärke, webbplats och meddelande", "email_app": "Öppna e-postappen", - "copy_email": "Kopiera e-postadress", - "preview_detail": "Här kan din logotyp, länk och presentation visas." + "copy_email": "Kopiera e-postadress" }, "supportPlans": { "remove": "Ta bort sponsorfältet", "title": "Gratis jämfört med Pro", "free": "Gratis", "pro": "Pro", - "sponsor_bar": "Sponsorfält", "visible": "Synligt", - "hideable": "Kan döljas", "unavailable": "Pro-aktivering har inte konfigurerats ännu.", "telemetry": "Telemetri", - "opt_in": "Valfritt, med samtycke", "disabled": "Avstängd", - "badge": "Pro-märke", - "advanced": "Avancerade verktyg", - "standard": "Standard", - "included": "Ingår" + "badge": "Pro-märke" }, "integrationCatalog": { "title": "Integrationer", @@ -3042,8 +2891,7 @@ "n8nHint": "Importera arbetsflödet till n8n, redigera begäran och kör det. Låt VoiceStudio vara igång. Docker och fjärranslutna n8n behöver en nåbar backendadress och eventuella inloggningsuppgifter." }, "directoryExamples": { - "example": "Katalogexempel", - "notice": "Endast katalogexempel — inte sponsorer eller anslutna integrationer." + "example": "Katalogexempel" }, "workflowRun": { "run": "Kör arbetsflöde", diff --git a/electron/src/shared/i18n/locales/th.json b/electron/src/shared/i18n/locales/th.json index 4f9c5cbb3..b69abbf3d 100644 --- a/electron/src/shared/i18n/locales/th.json +++ b/electron/src/shared/i18n/locales/th.json @@ -59,15 +59,10 @@ "title": "ถาม VoiceStudio Agent", "notInstalled": "ยังไม่ได้ติดตั้ง", "noSource": "เปิดสำเนาซอร์สโค้ด VoiceStudio ที่เขียนได้เพื่อให้เอเจนต์ซ่อมแซม", - "noneFound": "ติดตั้ง Codex, Claude Code, OpenCode หรือ Pi เพื่อใช้การซ่อมแซมในเครื่อง", "ready": "พร้อม", "placeholder": "เกิดอะไรขึ้น? เพิ่มขั้นตอน ข้อผิดพลาดที่เห็น หรือพฤติกรรมที่คาดหวัง", - "prReady": "แก้ไขเสร็จแล้ว ตรวจสอบการเปลี่ยนแปลงและเสนอ PR เมื่อการทดสอบเฉพาะผ่าน", "diagnose": "วินิจฉัย", "fix": "แก้ไข", - "chooseDefault": "เลือกเอเจนต์ที่ VoiceStudio จะใช้โดยอัตโนมัติเมื่อหน้าจอนี้ขัดข้อง", - "useAutomatically": "ใช้ {{agent}} โดยอัตโนมัติ", - "captured": "บันทึกบรรทัดปัญหาจากล็อกแล้ว {{count}} บรรทัด", "contextNotice": "เอเจนต์ที่เลือกจะได้รับรายงานนี้ หน้าจอปัจจุบัน บันทึกล่าสุดของแอปและแบ็กเอนด์ และข้อมูลวินิจฉัยระบบ", "complete": "เสร็จสิ้น" }, @@ -607,7 +602,6 @@ "characters": "{{count}} อักขระ", "minutes": "~{{count}} นาที", "chars": "{{count}} ตัวอักษร", - "exportDone": "ดาวน์โหลดหนังสือเสียงแล้ว", "exportFailed": "การส่งออกล้มเหลว — ตรวจสอบว่าแบ็กเอนด์กำลังทำงานอยู่", "global_speed": "ความเร็ว", "global_speed_hint": "ความเร็วในการอ่านสำหรับทุกบรรทัดโดยไม่มีการแทนที่ความเร็วของตัวเอง", @@ -818,7 +812,6 @@ "data_dir": "ไดเร็กทอรีข้อมูล", "outputs": "เอาท์พุต", "crash_log": "บันทึกความผิดพลาด", - "update_endpoint": "อัปเดตจุดสิ้นสุด", "yes": "ใช่", "no": "ไม่", "web_preview": "ตัวอย่างเว็บ", @@ -879,38 +872,21 @@ "title": "ข้อมูลรับรอง", "desc": "คีย์ API และโทเค็นได้รับการตั้งค่า <1> สำหรับเซสชันนี้เท่านั้น เพื่อความคงอยู่ในการรีสตาร์ท ให้ตั้งค่าเป็นตัวแปรสภาพแวดล้อมในเชลล์โปรไฟล์ของคุณ", "hf_token": "โทเค็นใบหน้ากอด", - "hf_help": "จำเป็นสำหรับการแยกเสียงของผู้พูดและการดาวน์โหลดโมเดลที่เร็วขึ้น รับของคุณที่huggingface.co/settings/tokens", - "translate_key": "คีย์ LLM API", - "translate_help": "ตัวเลือกเสริม — สำหรับ DeepL, OpenAI หรือผู้ให้บริการแปลแบบชำระเงิน ไม่จำเป็นสำหรับ Google Translate (รุ่นฟรี)", "save": "บันทึก", "saved": "✓ ตั้งค่า", - "not_set": "✗ ไม่ได้ตั้งค่า", "get_token": "รับโทเค็น →", - "saved_session": "{{key}} บันทึกไว้สำหรับเซสชันนี้", "save_failed": "บันทึกไม่สำเร็จ", "save_error": "บันทึกล้มเหลว: {{message}}", "deepl_key": "คีย์ DeepL API", - "deepl_help": "สำหรับเครื่องมือแปล DeepL รับกุญแจของคุณที่ deepl.com/pro-api", "deepl_base_url": "URL ฐาน DeepL", "deepl_base_url_help": "ตำแหน่งข้อมูล DeepL API ที่กำหนดเอง (ค่าเริ่มต้น: https://api.deepl.com/v2) การเปลี่ยนแปลงสำหรับพร็อกซี/โฮสต์ด้วยตนเอง", "microsoft_key": "คีย์นักแปลของ Microsoft", - "microsoft_help": "สำหรับกลไก Microsoft Translator รับคีย์ของคุณจาก Azure Portal", "microsoft_base_url": "URL ฐานนักแปลของ Microsoft", "microsoft_base_url_help": "จุดสิ้นสุด Azure Translator แบบกำหนดเอง (ค่าเริ่มต้น: https://api.cognitive.microsofttranslator.com)", - "llm_base_url": "URL ฐาน LLM", - "llm_base_url_help": "สำหรับคุณภาพระดับภาพยนตร์หรือเครื่องยนต์ LLM ตำแหน่งข้อมูลใดๆ ที่เข้ากันได้กับ OpenAI (OpenAI, Ollama, LM Studio ฯลฯ)", - "llm_model": "แบบจำลอง LLM", - "llm_model_help": "ชื่อรุ่นที่จะใช้กับกลไก LLM (เช่น gpt-4o, deepseek-chat, qwen2.5)", - "group_huggingface": "กอดใบหน้า", - "group_translate": "การแปล (LLM / ภาพยนตร์)", - "group_deepl": "ตำแหน่งข้อมูลแบบกำหนดเองของ DeepL", - "group_microsoft": "จุดสิ้นสุดแบบกำหนดเองของนักแปลของ Microsoft", "region": "ภูมิภาค", "aws_profile": "โปรไฟล์ AWS" }, "engines": { - "banners_on": "ป้ายบนเวทีเปิดอยู่", - "banners_off": "ปิดแบนเนอร์บนเวที", "backend": "แบ็กเอนด์", "status": "สถานะ", "ready": "พร้อม", @@ -941,7 +917,6 @@ "dictation_model_hint": "ใช้โดยการป้อนเสียงสดและโดยเอนจินนี้เมื่อถอดความ", "dictation_not_installed": "ยังไม่ได้ติดตั้ง — เลือกเพื่อดาวน์โหลด", "active": "ใช้งานอยู่", - "whyUnavailable": "สิ่งที่ต้องมี", "diskDetails": "รายละเอียดดิสก์", "diskModelDownload": "ดาวน์โหลดโมเดล", "diskPackageDownload": "ดาวน์โหลดแพ็กเกจ", @@ -965,11 +940,8 @@ "sectionReady": "พร้อมใช้งาน", "sectionMore": "เพิ่มเครื่องยนต์อื่น ๆ", "lastError": "ข้อผิดพลาดครั้งล่าสุด: {{error}}", - "installedAndReady": "ติดตั้งแล้วพร้อม", - "notInstalled": "ไม่ได้ติดตั้ง", "available": "มีจำหน่าย", "subprocessTitle": "ทำงานในกระบวนการย่อยของตัวเอง + venv", - "inProcessTitle": "ทำงานในกระบวนการ VoiceStudio Python", "testEngine": "ทดสอบเครื่องยนต์", "testing": "กำลังทดสอบ...", "recheck": "ตรวจสอบอีกครั้ง", @@ -986,7 +958,6 @@ "routingUnknown": "ไม่ทราบ", "routingEffectiveChip": "ทำงานบน {{device}} บนเครื่องนี้", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "เลือก GPU แล้ว แต่: {{reason}}", "selectCpuFallback": "{{engine}}: กำลังทำงานบน CPU — {{reason}}", "selectWithCaveat": "สลับไปที่ {{engine}} — {{reason}}", "generateCaveat": "{{engine}} อาจทำงานช้าบนเครื่องนี้ — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "ไม่มีบันทึก Tauri ทำงานในเชลล์เดสก์ท็อปเท่านั้น", "empty_backend": "บันทึกรันไทม์ว่างเปล่า กิจกรรมจะปรากฏที่นี่เมื่อแบ็กเอนด์บันทึก", "title": "บันทึก", - "source_backend": "แบ็กเอนด์", - "source_frontend": "ส่วนหน้า", - "source_tauri": "ทอรี", "expand": "ขยายบันทึก", "collapse": "ยุบบันทึก", "expand_aria": "ขยายแผงบันทึก", @@ -1104,12 +1072,10 @@ "unlock": "ปลดล็อค", "try_voice": "ลองเสียงนี้สิ", "test_phrase": "วลีทดสอบ", - "test_help": "พิมพ์อะไรก็ได้ เราจะสร้างมันขึ้นมาด้วยการตั้งค่าปัจจุบันของเสียงนี้", "test_placeholder": "พิมพ์อะไรบางอย่างเพื่อฟังเสียงนี้พูด...", "gen_preview": "สร้างการแสดงตัวอย่าง", "generating": "กำลังสร้าง...", "used_title": "เสียงนี้ถูกนำมาใช้ที่ไหน", - "used_empty": "เสียงนี้ยังไม่ได้ใช้ สร้างตัวอย่างด้านบน หรือเลือกในรูปแบบพากย์", "synth_clips_one": "{{count}} คลิปสังเคราะห์", "synth_clips_other": "{{count}} คลิปสังเคราะห์", "projects_count_one": "{{count}} โปรเจ็กต์", @@ -1152,9 +1118,7 @@ "translate": "แปล", "generate": "สร้าง", "export": "ส่งออก", - "no_video": "ไม่มีการโหลดวิดีโอ", "segments": "เซ็กเมนต์", - "speakers": "ลำโพง", "video_dubbing_studio": "สตูดิโอพากย์วิดีโอ", "drop_here": "วางวิดีโอหรือเสียงที่นี่", "paste_url": "…หรือวาง URL ของ YouTube / วิดีโอ", @@ -1168,7 +1132,6 @@ "cookie_size_error": "ไฟล์ส่งออกคุกกี้ต้องมีขนาดไม่เกิน 1 MB", "save": "บันทึก", "reset": "รีเซ็ต", - "save_project": "บันทึกโครงการ", "change_file": "เปลี่ยนไฟล์", "import_srt": "นำเข้า .srt", "import_srt_alt": "นำเข้า .srt แทน", @@ -1186,9 +1149,6 @@ "retranslate": "แปลใหม่อีกครั้ง", "transcript": "การถอดเสียง", "generate_dub": "สร้างพากย์", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "รฟท", "upload_transcribe": "อัปโหลดและถอดเสียง", "multi_lang": "หลายภาษา", "prep_download": "กำลังดาวน์โหลดวิดีโอ...", @@ -1206,7 +1166,6 @@ "transcribing": "กำลังถอดเสียง...", "elapsed": "ผ่านไปแล้ว", "remaining": "เหลืออยู่", - "sidebar_toggle": "สลับแถบด้านข้าง", "retry_transcription": "ลองถอดเสียงอีกครั้ง", "supported_formats": "MP4 · MOV · MKV · เว็บเอ็ม · MP3 · WAV · FLAC · M4A", "pull_captions_title": "เมื่อ URL เป็นโฮสต์คำบรรยาย (YouTube, Vimeo, TED…) ให้ดึงคำบรรยายต้นฉบับและการแปลอัตโนมัติของ YouTube ด้วย เมล็ดเครื่องมือแก้ไขโดยไม่ต้องเรียกใช้ Whisper; ข้ามการแปลทั้งหมดสำหรับภาษาที่ YouTube ครอบคลุมอยู่แล้ว", @@ -1249,7 +1208,6 @@ "engine_label": "เครื่องยนต์", "install_engine": "ติดตั้งเครื่องยนต์นี้", "installing_engine": "…กำลังติดตั้ง", - "needs_dev_install": "ต้องการการติดตั้ง dev", "install_disabled_title": "การติดตั้งถูกปิดใช้งานในบิลด์แพ็คเกจ", "needs_install_suffix": "- จำเป็นต้องติดตั้ง", "glossary_title": "ปักหมุดคำแปลสำหรับคำที่เกิดซ้ำ (ชื่อ คำของแบรนด์ ศัพท์เฉพาะ)", @@ -1283,7 +1241,6 @@ "original_track": "ต้นฉบับ", "selected_dub": "{{code}} (พากย์ที่เลือก)", "dub_track": "{{code}} (พากย์)", - "export_tracks": "ส่งออกแทร็ก:", "stopping": "กำลังหยุด...", "stop_progress": "หยุด ({{current}}/{{total}})", "regen_changed": "รีเจน {{count}} เปลี่ยนไป", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} ติดตั้งแล้ว", "install_failed": "การติดตั้งล้มเหลว: {{message}}", "prep_elapsed": "{{time}} ผ่านไป", - "add_language": "เพิ่มภาษา", "search_languages": "ค้นหาภาษา...", "languages_selected_one": "เลือก {{count}} ภาษาแล้ว", "languages_selected_other": "เลือก {{count}} ภาษาแล้ว", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "เวลา", - "spkr": "สปคร", "text": "ข้อความ", "lang": "หรั่ง", "voice": "เสียง", @@ -1403,8 +1358,6 @@ "select_all_title": "เลือกกรองทั้งหมด", "sel_count": "{{count}} เซล", "select_title": "เลือกส่วน (shift+คลิกเพื่อดูช่วง)", - "sync_title": "เสียงที่สร้างขึ้นคือ {{pct}}% ระยะเวลาของต้นฉบับ", - "sync_label": "ซิงค์: {{pct}}%", "rate_title": "อัตราคำพูดพอดี: {{ratio}}× สัมพันธ์กับช่อง{{error}}", "speaker_id": "รหัสผู้พูด", "translate_error_title": "ข้อผิดพลาดในการแปล: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "คืนค่าข้อความต้นฉบับ", "lang_default": "(ป้องกัน)", "voice_default": "ค่าเริ่มต้น", - "from_video": "จากวิดีโอ", - "clone_profiles": "โปรไฟล์โคลน", - "design_presets": "การออกแบบที่ตั้งไว้ล่วงหน้า", "preview_title": "ดูตัวอย่างสด", "set_direction": "กำหนดทิศทาง…", "edit_direction": "แก้ไขทิศทาง...", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "บุคลิกภาพ", - "pick_personality": "เลือกพรีเซ็ตบุคลิกภาพ...", "instruct": "สั่งสอน", "reference_text": "ข้อความอ้างอิง", "language": "ภาษา", @@ -1535,9 +1484,7 @@ "stories": "เรื่องราว", "untitled_story": "เรื่องราวที่ไม่มีชื่อเรื่อง", "story_lines": "{{count}} บรรทัด", - "story_lines_plural": "{{count}} บรรทัด", "story_voices": "{{count}} เสียง", - "story_voices_plural": "{{count}} เสียง", "audiobooks": "หนังสือเสียง", "audiobook": "หนังสือเสียง", "story": "เรื่องราว", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio แกลเลอรี่", - "search_placeholder": "ค้นหา YouTube...", - "all_voices": "ทุกเสียง ({{count}})", - "no_voices": "ยังไม่มีเสียง.", "download_hint": "ค้นหาด้านบนเพื่อดาวน์โหลดคลิป", - "search_btn": "ค้นหา", - "download_btn": "ดาวน์โหลด", "saving": "กำลังบันทึก...", "trim": "ตัดแต่ง", - "delete_confirm": "ลบคลิปเสียงนี้ใช่ไหม", - "view_mode_list": "รายการ", - "view_mode_grid": "กริด", - "youtube_results": "ผลลัพธ์ของ YouTube ({{count}})", - "clone_profile": "โปรไฟล์โคลน", - "crop_audio": "ครอบตัดเสียง", "subtitle": "เสียงที่ออกแบบมาสำเร็จรูปหลายร้อยเสียง — เลือกหนึ่งเสียงแล้วไปได้เลย", "zone_archetypes": "ต้นแบบ", "zone_imports": "การนำเข้าของฉัน", @@ -1658,8 +1594,6 @@ "delete": "ลบ", "community_empty": "ยังไม่มีเสียงจากชุมชน — เชื่อมต่ออินเทอร์เน็ตแล้วเปิดใหม่ หรือเป็นคนแรกที่ส่งเสียงเข้ามา", "community_explainer": "พรีเซ็ตที่ออกแบบไว้และเสียงที่บันทึกไว้ซึ่งแชร์โดยชุมชน โหลดจาก omnivoice-gallery", - "no_designer": "เสียงที่บันทึกไว้ — ใช้ \"ใช้เสียง\" แทน", - "no_preview": "ไม่มีตัวอย่าง — เพิ่มด้วย \"ใช้เสียง\" เพื่อฟัง", "submit_failed": "ไม่สามารถเปิดแบบฟอร์มส่งได้", "submit_preset": "ส่งพรีเซ็ต", "submit_voice": "ส่งเสียง", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "คุณพร้อมสร้างเสียงแรกแล้ว ตั้งค่าการพิมพ์ด้วยเสียงตอนนี้หรือภายหลังในการตั้งค่าได้", - "welcome": "ยินดีต้อนรับ", "system_check": "การตรวจสอบระบบ", "install_models": "ติดตั้งโมเดล", - "pick_engines": "เลือกเครื่องยนต์", "system_check_desc": "โพรบ RAM, ดิสก์, GPU, ffmpeg และเครือข่าย ตัวบล็อกจะถูกตั้งค่าสถานะล่วงหน้าเพื่อให้คุณทราบก่อนที่จะดาวน์โหลด", "install_models_desc": "ดาวน์โหลดน้ำหนักประมาณ 2.4 GB — จำเป็นต้องใช้เฉพาะโมเดล TTS เท่านั้น เพิ่มโมเดลถอดความที่คัดสรรสำหรับฮาร์ดแวร์ของคุณตอนนี้ หรือภายหลังจากการตั้งค่า", "pick_engines_desc": "เลือกแบ็กเอนด์ TTS / ASR / LLM ค่าเริ่มต้นใช้งานได้ทันที — ปรับแต่งได้ทุกเมื่อในการตั้งค่า", "hero_desc": "Dubbing, voice cloning, and voice design — all running locally on your machine.", - "first_run": "การรันครั้งแรกใช้เวลาดาวน์โหลด 5–10 นาที หลังจากนั้น ทุกการเปิดตัวจะเป็นทันทีและออฟไลน์โดยสมบูรณ์", - "get_started": "เริ่มต้นเลย", "back": "กลับ", "continue_ok": "ดีไปหมด — ดำเนินการต่อ", "continue_warn": "ดำเนินการต่อ (พร้อมคำเตือน)", @@ -1727,7 +1657,6 @@ "try_dictation": "ลองเขียนตามคำบอก", "try_dictation_desc": "กดปุ่มลัดของคุณหนึ่งครั้งเพื่อให้แน่ใจว่าการสั่งงานด้วยเสียงทำงานก่อนที่คุณจะตั้งค่าเสร็จ", "dictation_step_label": "การสั่งงานด้วยเสียง", - "next_try_dictation": "ถัดไป: ลองใช้การเขียนตามคำบอก", "step_aria": "ขั้นตอนที่ {{num}}: {{label}}", "step_completed": "เสร็จสิ้น", "cache_label": "แคชโมเดล" @@ -1743,7 +1672,6 @@ "platforms": "แพลตฟอร์ม", "footer": "ทุกการมีส่วนร่วมช่วยผลักดันขอบเขตของ AI ในท้องถิ่น", "back": "กลับไปที่สตูดิโอ", - "commercial": "ใบอนุญาตการค้า", "pill": "รองรับ VoiceStudio", "suggested_title": "เลือกจำนวนเงิน", "most_common": "ที่พบบ่อยที่สุด", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "สนับสนุนการพัฒนา VoiceStudio", - "lead_default": "ดีใจที่ได้ผล! VoiceStudio เป็นบริการฟรีและอยู่ในเครื่องโดยสมบูรณ์ ถ้ามันช่วยคุณประหยัดเวลาได้ เงินสมทบรายเดือนเล็กๆ น้อยๆ ที่ Claude Max อยู่เบื้องหลัง", - "lead_first_clone": "การโคลนเสียงครั้งแรกของคุณเสร็จสิ้นแล้ว เยี่ยมมาก! VoiceStudio ทำงานบนเครื่องของคุณทั้งหมด และฝ่ายสนับสนุนของคุณก็จะเป็นเช่นนั้น", - "lead_tenth_dub": "มีพากย์สิบตอน — คุณเห็นได้ชัดว่ามันใช้งานได้จริง กองทุนรายเดือนเล็กๆ น้อยๆ ของ Claude Max ที่จัดส่งฟีเจอร์เหล่านี้", - "lead_sustained": "คุณใช้งาน VoiceStudio มาหนึ่งเดือนแล้ว หากได้รับจุดหนึ่งในขั้นตอนการทำงานของคุณ ให้พิจารณาช่วยเหลือด้านทุนสำหรับการดำเนินการต่อไป", - "chip_in": "ชิปเข้า", - "later": "อาจจะในภายหลัง", - "star": "ติดดาวบน GitHub", - "opt_out": "อย่าถามอีก", - "dismiss_aria": "ยกเลิก" + "later": "อาจจะในภายหลัง" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "ใบอนุญาตการค้า", "hero_title": "จัดส่งเสียง AI ในการผลิต", "hero_desc": "VoiceStudio เป็นซอฟต์แวร์เสรีและโอเพนซอร์สภายใต้สัญญาอนุญาต GNU Affero General Public License v3 (AGPL-3.0) — ใช้งานได้ฟรี รวมถึงการใช้งานเชิงพาณิชย์และการใช้งานภายในองค์กร คุณต้องมีใบอนุญาตเชิงพาณิชย์เฉพาะเมื่อต้องการฝัง VoiceStudio ในผลิตภัณฑ์หรือบริการแบบปิดซอร์สหรือกรรมสิทธิ์ โดยไม่ปฏิบัติตามข้อผูกพัน copyleft ของ AGPL-3.0", - "why_title": "เหตุใดธุรกิจจึงเลือก VoiceStudio", - "pricing_title": "ราคา", - "faq_title": "คำถามทั่วไป", "pricing_desc": "ระดับและราคายังอยู่ในระหว่างการสรุปผล", - "pricing_detail": "จนกว่าจะเปิดเผยต่อสาธารณะ การใช้งานเชิงพาณิชย์ทุกครั้งจะถูกเสนอราคาเป็นรายบุคคล เพื่อให้เราสามารถปรับขนาดให้เหมาะสมกับทีมและปริมาณงานของคุณได้", "request_quote": "ขอใบเสนอราคา", - "footer_email": "มีคำถาม? ติดต่อได้ที่ hi@voicestudio.sh", - "footer_discord": "เข้าร่วม Discord ของเราเพื่อรับการสนับสนุนจากชุมชน", "benefit_ip": "ความเป็นเจ้าของ IP เต็มรูปแบบ", "benefit_ip_desc": "คุณเป็นเจ้าของทุกผลผลิต ทุกรุ่น และอนุพันธ์ทั้งหมด ไม่มีการเช่าร่วมกัน ไม่มีข้อมูลรั่วไหล", "benefit_cost": "ค่าใช้จ่ายเป็นศูนย์ต่อนาที", "benefit_cost_desc": "ไม่มีการเรียกเก็บเงิน API ไม่มีการโอเวอร์เกินอย่างน่าประหลาดใจ ทำงานบนฮาร์ดแวร์ของคุณเองตั้งแต่วันแรก", - "benefit_team": "การเข้าถึงทั่วทั้งทีม", "benefit_team_desc": "ใบอนุญาตหนึ่งใบครอบคลุมผู้ใช้ไม่จำกัดจำนวนภายในองค์กรของคุณ", "benefit_support": "การสนับสนุนโดยตรง", "benefit_support_desc": "การสนับสนุนทางอีเมลและแฮงเอาท์วิดีโอสำคัญจากทีมวิศวกรหลัก", - "benefit_source": "แกนหลักที่มีแหล่งที่มา", "benefit_source_desc": "มองเห็นสแต็กได้เต็มรูปแบบ ตรวจสอบ แยก และปรับเปลี่ยนภายในข้อกำหนดสิทธิ์การใช้งาน", - "benefit_lang": "646 ภาษา", "benefit_lang_desc": "ถอดเสียง แปล และพากย์ใน 646 ภาษาด้วยคุณภาพระดับมนุษย์", - "hero_note": "การใช้งาน การโฮสต์ด้วยตนเอง และการใช้งานเชิงพาณิชย์ ทั้งหมดฟรีภายใต้ AGPL-3.0 — รวมถึงการใช้งานขนาดใหญ่ AGPL เป็นสัญญาอนุญาตแบบ copyleft ผ่านเครือข่าย: หากคุณแก้ไข VoiceStudio และให้บริการเวอร์ชันที่แก้ไขนั้นแก่ผู้อื่นผ่านเครือข่าย คุณต้องเปิดเผยซอร์สโค้ดที่แก้ไขของคุณภายใต้เงื่อนไขเดียวกัน ใบอนุญาตเชิงพาณิชย์จะยกเว้นข้อผูกพัน copyleft เหล่านี้สำหรับการใช้งานแบบกรรมสิทธิ์/ปิดซอร์ส แผนราคาจะมาเร็ว ๆ นี้ — ระหว่างนี้ติดต่อเราได้เลย", - "hero_simple": "VoiceStudio เป็นซอฟต์แวร์โอเพ่นซอร์สฟรีภายใต้ AGPL-3.0 — รวมถึงการใช้งานเชิงพาณิชย์และธุรกิจภายใน คุณเพียงต้องมีใบอนุญาตเชิงพาณิชย์เพื่อฝังลงในผลิตภัณฑ์แบบปิดโดยไม่มีภาระผูกพันด้านลิขสิทธิ์ของ AGPL", - "contact_lead": "ราคาเป็นราคาต่อการปรับใช้เพื่อให้เหมาะสมกับทีมและปริมาณงานของคุณ บอกกรณีการใช้งานของคุณให้ฉันทราบ แล้วฉันจะเสนอราคาให้คุณ" + "hero_simple": "VoiceStudio เป็นซอฟต์แวร์โอเพ่นซอร์สฟรีภายใต้ AGPL-3.0 — รวมถึงการใช้งานเชิงพาณิชย์และธุรกิจภายใน คุณเพียงต้องมีใบอนุญาตเชิงพาณิชย์เพื่อฝังลงในผลิตภัณฑ์แบบปิดโดยไม่มีภาระผูกพันด้านลิขสิทธิ์ของ AGPL" }, "exportModal": { "export": "ส่งออก", @@ -2006,7 +1915,6 @@ "loaded_ready": "โหลดแล้วพร้อมครับ", "loading_first_run": "กำลังโหลด... (อาจใช้เวลา 1-2 นาทีในการเรียกใช้ครั้งแรก)", "failed_to_load": "โหลดไม่สำเร็จ", - "not_loaded_yet": "ยังไม่ได้โหลด — จะโหลดเมื่อถอดเสียงเป็นคำครั้งแรก", "tts_not_loaded_yet": "ยังไม่ได้โหลด — จะโหลดเมื่อสร้างเสียงครั้งแรก", "error_check_logs": "ข้อผิดพลาด: {{error}} ตรวจสอบบันทึกและลองรีสตาร์ท", "check_logs_restart": "ตรวจสอบบันทึกเพื่อหาข้อผิดพลาดในการโหลดโมเดล ลองรีสตาร์ท", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "การหล่อลำโพง", - "auto_assign_title": "กำหนดเสียงอัตโนมัติจากโคลนลำโพงที่แยกออกมา", - "auto_cast": "แคสต์อัตโนมัติ", - "all_cast": "นักแสดงทั้งหมด", - "segments_count": "{{count}} ส่วน", - "assign_voice": "กำหนดเสียง...", - "from_video": "🎶 จากวีดีโอ ({{name}})", - "no_profiles": "ยังไม่มีการบันทึกโปรไฟล์เสียง", - "preview_voice": "ดูตัวอย่างเสียง" + "segments_count": "{{count}} ส่วน" }, "checkpoint": { "timing_review": "ต้องตรวจสอบเวลาของ {{count}} ช่วง", @@ -2203,8 +2104,6 @@ "play_failed": "การเล่นล้มเหลว: {{message}}" }, "models": { - "hf_token_set_toast": "ชุดโทเค็น HuggingFace — เปิดใช้งานการดาวน์โหลดที่เร็วขึ้น", - "hf_token_save_failed": "บันทึกโทเค็นไม่สำเร็จ", "install_started": "เริ่มการติดตั้ง — ความคืบหน้าในแถว", "delete_confirm": "ลบ {{repoId}}? คุณสามารถติดตั้งใหม่ได้ในภายหลัง", "delete_confirm_title": "ลบโมเดล", @@ -2247,8 +2146,6 @@ "not_installed": "ไม่ได้ติดตั้ง", "required_tag": "จำเป็น", "delete_btn": "ลบ", - "hf_token_btn": "โทเค็น HF", - "hf_set_title": "ตั้งค่าโทเค็น HuggingFace เพื่อการดาวน์โหลดที่เร็วขึ้น", "get_token": "รับโทเค็น →", "reco_installed_for": "บันเดิลที่แนะนำที่ติดตั้งสำหรับ {{device}}", "reco_for": "แนะนำสำหรับ {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "จำเป็น ~{{size}} GB", "all_size": "ทั้งหมด ~{{size}} GB", "req_tag": "คำขอ", - "search_placeholder": "ค้นหารุ่น...", - "search_label": "ค้นหาโมเดล", "no_matches": "ไม่มีรุ่นที่ตรงกับตัวกรองของคุณ", "sort_by": "จัดเรียงตาม {{column}}", "column_model": "รุ่น", @@ -2267,12 +2162,8 @@ "ready_badge": "พร้อม", "loading_badge": "กำลังโหลด...", "idle_badge": "ไม่ได้ใช้งาน", - "started_downloading_required_one": "เริ่มดาวน์โหลด {{count}} โมเดลที่ต้องการ", - "started_downloading_required_other": "เริ่มดาวน์โหลด {{count}} โมเดลที่ต้องการ", "to_download": "เพื่อดาวน์โหลด", "cached": "แคชไว้", - "fast_download_badge": "ดาวน์โหลดอย่างรวดเร็ว", - "fast_download_title": "ดาวน์โหลดอย่างรวดเร็วผ่าน Xet {{version}} — การถ่ายโอนแบบขนาน", "voice_previews": "ตัวอย่างเสียง", "voice_previews_desc": "ดาวน์โหลดตัวอย่างที่เรนเดอร์ไว้ล่วงหน้า เพื่อให้เสียงเล่นได้ทันที แม้ก่อนที่โมเดลเสียงจะดาวน์โหลดเสร็จ จะไม่มีการดาวน์โหลดใด ๆ จนกว่าคุณจะเปิดใช้งาน", "voice_previews_off": "ปิด — ตัวอย่างจะถูกเรนเดอร์บนเครื่องนี้", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "ฉันจำเป็นต้องมีใบอนุญาตสำหรับเครื่องมือภายในหรือไม่?", - "a_internal_tools": "ไม่ต้อง การใช้งานโดยพนักงานและผู้รับเหมาของคุณ — รวมถึงการแก้ไขและโฮสต์เองภายในองค์กร — ฟรีภายใต้ AGPL-3.0 ใบอนุญาตเชิงพาณิชย์จำเป็นเฉพาะเมื่อคุณฝัง VoiceStudio ในผลิตภัณฑ์หรือบริการแบบปิดซอร์สหรือกรรมสิทธิ์ และไม่ต้องการปฏิบัติตามข้อกำหนดการเปิดเผยซอร์สโค้ดของ AGPL", - "q_try_before": "ฉันสามารถลองก่อนที่จะกระทำได้หรือไม่?", - "a_try_before": "ได้ แอปฉบับเต็มดาวน์โหลด ใช้งาน และโฮสต์เองได้ฟรีภายใต้ AGPL-3.0 — ไม่ต้องมีข้อตกลงใด ๆ เมื่อคุณพร้อมจะหารือเรื่องใบอนุญาตเชิงพาณิชย์ (การใช้งานแบบกรรมสิทธิ์) ส่งอีเมลถึงเรา แล้วเราจะช่วยจัดการรายละเอียดร่วมกัน", - "q_watermark": "แล้วลายน้ำล่ะ?", - "a_watermark": "ลายน้ำ AudioSeal แบบมองไม่เห็นถูกฝังไว้เป็นค่าเริ่มต้นสำหรับทุกคน คุณสามารถปิดได้ที่ การตั้งค่า → ความเป็นส่วนตัว โดยมีผลเฉพาะกับเสียงที่สร้างหลังจากเปลี่ยนค่าเท่านั้น" - }, "gallery_extra": { - "save_prompt": "ป้อนชื่อโปรไฟล์เสียงนี้:", - "saved_as_profile": "บันทึกเสียงเป็นโปรไฟล์แล้ว!", - "save_profile_failed": "ไม่สามารถบันทึกโปรไฟล์ได้", - "download_failed": "ดาวน์โหลดล้มเหลว: {{message}}", - "trim_load_failed": "ไม่สามารถโหลดเสียงสำหรับการตัด: {{message}}", - "upload_crop_failed": "ไม่สามารถอัปโหลดเสียงที่ครอบตัด: {{message}}" + "trim_load_failed": "ไม่สามารถโหลดเสียงสำหรับการตัด: {{message}}" }, "dub_workflow": { "preparing_audio": "กำลังเตรียมเสียง...", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "กำลังโหลด...", - "trimmed_loaded": "โหลดเสียงที่ตัดแล้ว", - "toast_exported": "ส่งออกแล้ว: {{name}}", - "toast_export_failed": "การส่งออกล้มเหลว: {{message}}", - "toast_open_folder_failed": "ไม่สามารถเปิดโฟลเดอร์: {{message}}", "toast_saving": "กำลังบันทึก {{name}}...", "toast_saved": "บันทึกแล้ว: {{path}}", "toast_save_error": "บันทึกข้อผิดพลาด: {{message}}", "toast_processing": "กำลังประมวลผล {{name}}...", "toast_downloaded": "ดาวน์โหลดแล้ว {{name}}", "toast_download_error": "ข้อผิดพลาดในการดาวน์โหลด: {{message}}", - "toast_upload_first": "โปรดคลิก 'อัปโหลดและถอดเสียง' ก่อน เพื่อให้วิดีโอได้รับการประมวลผลบนเซิร์ฟเวอร์ก่อนบันทึก", - "toast_save_failed": "บันทึกล้มเหลว: {{message}}", - "toast_opened": "เปิดแล้ว: {{name}}", - "toast_project_deleted": "ลบโครงการแล้ว", - "toast_restored_state": "คืนค่าสถานะรุ่นก่อนหน้า", - "toast_history_deleted": "ลบรายการประวัติแล้ว", "toast_flushed": "ฟลัช — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· โมเดลยกเลิกการโหลดแล้ว", "toast_flush_failed": "การฟลัชล้มเหลว: {{message}}", - "toast_project_saved": "บันทึกโครงการแล้ว", - "toast_project_created": "สร้างโครงการแล้ว" + "toast_project_saved": "บันทึกโครงการแล้ว" }, "update": { "available": "มีอัปเดต {{version}}", @@ -2531,13 +2398,11 @@ "not_writable": "เขียนไม่ได้", "change": "เปลี่ยน…", "compute_title": "การประมวลผล", - "compute_label": "GPU / ตัวเร่งความเร็ว", "compute_auto": "อัตโนมัติ (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "ช่องทางอัปเดต", "channel_stable": "เสถียร", "channel_preview": "พรีวิว (main ล่าสุด)", - "network_title": "เครือข่าย", "region_label": "ภูมิภาคการดาวน์โหลด", "mirrors_title": "มิเรอร์กำหนดเอง (ขั้นสูง)", "mirror_pypi": "URL ดัชนี PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "ความเป็นส่วนตัวมาก่อน: เสียง การบันทึก และโปรเจกต์ของคุณไม่เคยออกจากเครื่องนี้ — ไม่มีบัญชี ไม่มีการประมวลผลบนคลาวด์", "resume_note": "การดาวน์โหลดที่ขาดตอนจะทำต่ออัตโนมัติ — ปิดแอปได้อย่างปลอดภัย", "eta_left": "เหลืออีก ~{{eta}}", - "first_sound_text": "ยินดีต้อนรับสู่สตูดิโอของคุณ ทุกคำที่คุณได้ยินถูกสร้างขึ้นบนเครื่องนี้เมื่อครู่นี้เอง", - "first_sound_done": "เสียงเมื่อกี้? สร้างขึ้นเมื่อไม่กี่วินาทีก่อน บนเครื่องนี้ ยินดีต้อนรับ", - "hf_token_title": "โทเคน Hugging Face (ไม่บังคับ)", - "hf_token_hint": "ปลดล็อกโมเดลที่จำกัดสิทธิ์ — เช่น การแยกผู้พูดสำหรับพากย์หลายเสียง (pyannote) โทเคนบัญชีฟรี เก็บไว้บนเครื่องนี้เท่านั้น", "hf_token_save": "บันทึก", "hf_token_saving": "กำลังบันทึก…", "hf_token_saved": "บันทึกโทเคน Hugging Face แล้ว", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} ออกไป", "hf_token_card_title": "เพิ่มโทเค็น Hugging Face ฟรีเพื่อการดาวน์โหลดที่เร็วขึ้น", "hf_token_get": "ไม่ได้มีโทเค็น? รับฟรีหนึ่งรายการ →", - "hf_token_saved_fast": "บันทึกโทเค็น Hugging Face แล้ว - ตอนนี้การดาวน์โหลดเร็วขึ้น", "hf_token_inline_prompt": "เร่งความเร็วการดาวน์โหลดด้วยโทเค็น Hugging Face ฟรี", "hf_token_inline_ph": "วางโทเค็น hf_… (ไม่บังคับ)", "hf_token_get_short": "รับฟรีหนึ่งรายการ →", @@ -2607,15 +2467,11 @@ "unstar_take": "เอาดาวออก — อนุญาตให้ล้างได้", "play_take": "โหลดเป็นเอาต์พุตปัจจุบัน", "filter_starred": "ติดดาว", - "load_take_failed": "โหลดเทคนั้นไม่ได้: {{message}}", "mode_clone": "โคลน", "mode_synth": "สังเคราะห์", "generation_seconds": "{{duration}} วินาที", - "expand_text": "ขยายข้อความ", - "collapse_text": "ย่อข้อความ", "seed": "ซีด {{seed}}", - "load_config": "โหลดการตั้งค่า", - "delete_confirm": "ลบรายการประวัตินี้หรือไม่" + "load_config": "โหลดการตั้งค่า" }, "openapi": { "title": "API ของ VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "ตัวอย่างผู้สนับสนุน", "message": "แบรนด์ เว็บไซต์ และข้อความของคุณ", "email_app": "เปิดแอปอีเมล", - "copy_email": "คัดลอกอีเมล", - "preview_detail": "โลโก้ ลิงก์ และคำแนะนำของคุณอาจปรากฏที่นี่" + "copy_email": "คัดลอกอีเมล" }, "supportPlans": { "remove": "ลบแถบผู้สนับสนุน", "title": "เปรียบเทียบฟรีกับ Pro", "free": "ฟรี", "pro": "Pro", - "sponsor_bar": "แถบผู้สนับสนุน", "visible": "แสดง", - "hideable": "ซ่อนได้", "unavailable": "ยังไม่ได้ตั้งค่าการเปิดใช้งาน Pro", "telemetry": "ข้อมูลการใช้งาน", - "opt_in": "ไม่บังคับ ต้องยินยอมก่อน", "disabled": "ปิดใช้งาน", - "badge": "ป้าย Pro", - "advanced": "เครื่องมือขั้นสูง", - "standard": "มาตรฐาน", - "included": "รวมอยู่ด้วย" + "badge": "ป้าย Pro" }, "integrationCatalog": { "title": "การเชื่อมต่อ", @@ -3042,8 +2891,7 @@ "n8nHint": "นำเข้าเวิร์กโฟลว์นี้ใน n8n แก้ไขคำขอแล้วเรียกใช้ เปิด VoiceStudio ไว้ Docker และ n8n ระยะไกลต้องใช้ที่อยู่แบ็กเอนด์ที่เข้าถึงได้และข้อมูลรับรองที่จำเป็น" }, "directoryExamples": { - "example": "ตัวอย่างในไดเรกทอรี", - "notice": "เป็นเพียงตัวอย่างในไดเรกทอรี ไม่ใช่ผู้สนับสนุนหรือการเชื่อมต่อที่เปิดใช้" + "example": "ตัวอย่างในไดเรกทอรี" }, "workflowRun": { "run": "เรียกใช้เวิร์กโฟลว์", diff --git a/electron/src/shared/i18n/locales/tr.json b/electron/src/shared/i18n/locales/tr.json index 8a5f804d6..dd48e8cb9 100644 --- a/electron/src/shared/i18n/locales/tr.json +++ b/electron/src/shared/i18n/locales/tr.json @@ -59,15 +59,10 @@ "title": "VoiceStudio Agent’a sor", "notInstalled": "Yüklü değil", "noSource": "Bir ajanın onarabilmesi için yazılabilir bir VoiceStudio kaynak çalışma kopyası açın.", - "noneFound": "Yerel onarım için Codex, Claude Code, OpenCode veya Pi yükleyin.", "ready": "Hazır", "placeholder": "Ne yanlış gitti? Adımları, görünen hataları veya beklenen davranışı ekleyin.", - "prReady": "Düzeltme tamamlandı. Değişiklikleri inceleyin ve hedefli testler geçince bir PR önerin.", "diagnose": "Tanıla", "fix": "Düzelt", - "chooseDefault": "Bu ekran çöktüğünde VoiceStudio'nun otomatik kullanacağı aracıyı seçin.", - "useAutomatically": "{{agent}} aracısını otomatik kullan", - "captured": "{{count}} sorunlu günlük satırı yakalandı", "contextNotice": "Seçilen aracı bu raporu, mevcut ekranı, son uygulama ve arka uç günlüklerini ve sistem tanılamasını alır.", "complete": "Tamamlandı" }, @@ -607,7 +602,6 @@ "characters": "{{count}} karakter", "minutes": "~{{count}} dk.", "chars": "{{count}} karakter", - "exportDone": "Sesli kitap indirildi", "exportFailed": "Dışa aktarma başarısız oldu — arka ucun çalıştığını kontrol edin.", "global_speed": "Hız", "global_speed_hint": "Kendi hız geçersiz kılma özelliği olmadan tüm satırlar için okuma hızı", @@ -818,7 +812,6 @@ "data_dir": "Veri dizini", "outputs": "Çıkışlar", "crash_log": "Kilitlenme günlüğü", - "update_endpoint": "Uç noktayı güncelle", "yes": "evet", "no": "hayır", "web_preview": "web önizlemesi", @@ -879,38 +872,21 @@ "title": "Kimlik bilgileri", "desc": "API anahtarları ve belirteçleri <1>yalnızca bu oturum için olarak ayarlandı. Yeniden başlatmalarda kalıcılık sağlamak için bunları kabuk profilinizde ortam değişkenleri olarak ayarlayın.", "hf_token": "SarılmaYüz Jetonu", - "hf_help": "Konuşmacı günlüğü oluşturma ve daha hızlı model indirme için gereklidir. Huggingface.co/settings/tokens adresinden sizinkini alın.", - "translate_key": "Yüksek Lisans API Anahtarı", - "translate_help": "İsteğe bağlı — DeepL, OpenAI veya ücretli çeviri sağlayıcıları için. Google Çeviri için gerekli değildir (ücretsiz katman).", "save": "Kaydet", "saved": "✓ Ayarla", - "not_set": "✗ Ayarlanmadı", "get_token": "Jeton al →", - "saved_session": "{{key}} bu oturum için kaydedildi", "save_failed": "Kaydedilemedi", "save_error": "Kaydetme başarısız oldu: {{message}}", "deepl_key": "DeepL API Anahtarı", - "deepl_help": "DeepL çeviri motoru için. Anahtarınızı deepl.com/pro-api adresinden alın.", "deepl_base_url": "DeepL Temel URL'si", "deepl_base_url_help": "Özel DeepL API uç noktası (varsayılan: https://api.deepl.com/v2). Proxy/kendi kendine barındırılan için değişiklik.", "microsoft_key": "Microsoft Çevirmen Anahtarı", - "microsoft_help": "Microsoft Translator motoru için. Anahtarınızı Azure Portal'dan alın.", "microsoft_base_url": "Microsoft Çevirmen Temel URL'si", "microsoft_base_url_help": "Özel Azure Translator uç noktası (varsayılan: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "Yüksek Lisans Temel URL'si", - "llm_base_url_help": "Sinematik kalite veya LLM motoru için. OpenAI uyumlu herhangi bir uç nokta (OpenAI, Ollama, LM Studio vb.).", - "llm_model": "Yüksek Lisans Modeli", - "llm_model_help": "LLM motoruyla kullanılacak model adı (ör. gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "SarılmaYüz", - "group_translate": "Çeviri (LLM / Sinematik)", - "group_deepl": "DeepL Özel Uç Nokta", - "group_microsoft": "Microsoft Translator Özel Uç Noktası", "region": "Bölge", "aws_profile": "AWS profili" }, "engines": { - "banners_on": "Sahne pankartları açık", - "banners_off": "Sahne pankartları kapalı", "backend": "Arka uç", "status": "Durum", "ready": "hazır", @@ -941,7 +917,6 @@ "dictation_model_hint": "Canlı dikte ve bu motorun transkripsiyonu tarafından kullanılır.", "dictation_not_installed": "Yüklü değil — indirmek için seçin", "active": "aktif", - "whyUnavailable": "Neye ihtiyacı var", "diskDetails": "Disk ayrıntıları", "diskModelDownload": "Model indirmesi", "diskPackageDownload": "Paket indirmesi", @@ -965,11 +940,8 @@ "sectionReady": "Kullanıma hazır", "sectionMore": "Daha fazla motor ekle", "lastError": "Son hata: {{error}}", - "installedAndReady": "Kurulu ve hazır", - "notInstalled": "Kurulu değil", "available": "Mevcut", "subprocessTitle": "Kendi alt sürecinde + venv'de çalışır", - "inProcessTitle": "VoiceStudio Python sürecinde çalışır", "testEngine": "Test motoru", "testing": "Test ediliyor…", "recheck": "Tekrar kontrol et", @@ -986,7 +958,6 @@ "routingUnknown": "Bilinmiyor", "routingEffectiveChip": "Bu makinede {{device}} tarihinde çalışıyor", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "GPU seçildi, ancak: {{reason}}", "selectCpuFallback": "{{engine}}: CPU üzerinde çalışıyor — {{reason}}", "selectWithCaveat": "{{engine}} motoruna geçildi — {{reason}}", "generateCaveat": "{{engine}} bu bilgisayarda yavaş çalışabilir — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Tauri günlüğü mevcut değil. Yalnızca masaüstü kabuğunda çalışır.", "empty_backend": "Çalışma zamanı günlüğü boş. Arka uç günlüğe kaydettikçe etkinlik burada görünecektir.", "title": "Günlükler", - "source_backend": "Arka uç", - "source_frontend": "Ön uç", - "source_tauri": "Tauri", "expand": "Günlükleri genişlet", "collapse": "Günlükleri daralt", "expand_aria": "Günlükler panelini genişlet", @@ -1104,12 +1072,10 @@ "unlock": "Kilidi aç", "try_voice": "Bu sesi dene", "test_phrase": "Test cümlesi", - "test_help": "Herhangi bir şey yazın; bu sesin mevcut ayarlarıyla onu oluşturacağız.", "test_placeholder": "Bu sesin söylediğini duymak için bir şeyler yazın…", "gen_preview": "Önizleme oluştur", "generating": "Oluşturuluyor…", "used_title": "Bu sesin kullanıldığı yerler", - "used_empty": "Bu ses henüz kullanılmadı. Yukarıda bir önizleme oluşturun veya dublajdan seçin.", "synth_clips_one": "{{count}} sentez klibi", "synth_clips_other": "{{count}} sentetik klipler", "projects_count_one": "{{count}} projesi", @@ -1152,9 +1118,7 @@ "translate": "Çevir", "generate": "Oluştur", "export": "İhracat", - "no_video": "Video yüklenmedi", "segments": "bölümler", - "speakers": "hoparlörler", "video_dubbing_studio": "Video Dublaj Stüdyosu", "drop_here": "Videoyu veya sesi buraya bırakın", "paste_url": "…veya YouTube / video URL'sini yapıştırın", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Çerez dışa aktarımı en fazla 1 MB olmalıdır.", "save": "Kaydet", "reset": "Sıfırla", - "save_project": "Projeyi kaydet", "change_file": "Dosyayı değiştir", "import_srt": ".srt'yi içe aktar", "import_srt_alt": "Bunun yerine .srt dosyasını içe aktarın", @@ -1186,9 +1149,6 @@ "retranslate": "Yeniden çevir", "transcript": "Transkript", "generate_dub": "Dub Oluştur", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Yükleme ve Metne Dönüştürme", "multi_lang": "Çok dilli", "prep_download": "Video indiriliyor…", @@ -1206,7 +1166,6 @@ "transcribing": "Ses yazıya aktarılıyor…", "elapsed": "geçti", "remaining": "kaldı", - "sidebar_toggle": "Kenar Çubuğunu Değiştir", "retry_transcription": "Transkripsiyonu yeniden dene", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "URL, altyazı taşıyan bir ana bilgisayar olduğunda (YouTube, Vimeo, TED...), orijinal altyazıları ve YouTube otomatik çevirilerini de çekin. Düzenleyiciyi Whisper'ı çalıştırmadan yayınlar; YouTube'un halihazırda kapsadığı diller için Tümünü Çevir seçeneğini atlayın.", @@ -1249,7 +1208,6 @@ "engine_label": "Motor", "install_engine": "Bu motoru yükle", "installing_engine": "…yükleniyor", - "needs_dev_install": "geliştirme kurulumu gerekiyor", "install_disabled_title": "Paketlenmiş yapılarda yüklemeler devre dışı bırakıldı", "needs_install_suffix": "— kurulumu gerekiyor", "glossary_title": "Tekrarlanan terimler (isimler, marka kelimeleri, jargon) için çevirileri sabitleyin", @@ -1283,7 +1241,6 @@ "original_track": "Orijinal", "selected_dub": "{{code}} (Seçilmiş Dub)", "dub_track": "{{code}} (Dublaj)", - "export_tracks": "Parçaları Dışa Aktar:", "stopping": "Durduruluyor...", "stop_progress": "Durdur ({{current}}/{{total}})", "regen_changed": "Yenilenme {{count}} değiştirildi", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} kuruldu", "install_failed": "Yükleme başarısız oldu: {{message}}", "prep_elapsed": "{{time}} geçti", - "add_language": "Dil ekle", "search_languages": "Dilleri ara…", "languages_selected_one": "{{count}} dil seçildi", "languages_selected_other": "{{count}} dil seçildi", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "Zaman", - "spkr": "Spkr", "text": "Metin", "lang": "Uzunluk", "voice": "Ses", @@ -1403,8 +1358,6 @@ "select_all_title": "Filtrelenenlerin tümünü seç", "sel_count": "{{count}} seç", "select_title": "Segment seçin (aralık için üst karakter+tıklama)", - "sync_title": "Oluşturulan ses, orijinalin süresinin %{{pct}} kadarı kadardır", - "sync_label": "Senkronizasyon: %{{pct}}", "rate_title": "Konuşma hızı uyumu: {{ratio}}× yuvaya göre{{error}}", "speaker_id": "Hoparlör Kimliği", "translate_error_title": "Çeviri hatası: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Orijinal metni geri yükle", "lang_default": "(Tanım)", "voice_default": "Varsayılan", - "from_video": "Videodan", - "clone_profiles": "Profilleri Klonla", - "design_presets": "Tasarım Ön Ayarları", "preview_title": "Canlı Önizleme", "set_direction": "Yönü ayarla…", "edit_direction": "Yönü düzenle…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Kişilik", - "pick_personality": "Bir kişilik ön ayarı seçin…", "instruct": "talimat", "reference_text": "Referans Metni", "language": "Dil", @@ -1535,9 +1484,7 @@ "stories": "Hikayeler", "untitled_story": "Başlıksız hikaye", "story_lines": "{{count}} satırı", - "story_lines_plural": "{{count}} satır", "story_voices": "{{count}} ses", - "story_voices_plural": "{{count}} sesler", "audiobooks": "Sesli kitaplar", "audiobook": "Sesli kitap", "story": "Hikaye", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Galeri", - "search_placeholder": "YouTube'da ara…", - "all_voices": "Tüm Sesler ({{count}})", - "no_voices": "Henüz ses yok", "download_hint": "Klipleri indirmek için yukarıda arama yapın", - "search_btn": "Ara", - "download_btn": "İndir", "saving": "Kaydediliyor…", "trim": "Kırpma", - "delete_confirm": "Bu ses klibi silinsin mi?", - "view_mode_list": "Liste", - "view_mode_grid": "Izgara", - "youtube_results": "YouTube Sonuçları ({{count}})", - "clone_profile": "Profili klonla", - "crop_audio": "Sesi kırp", "subtitle": "Yüzlerce hazır tasarlanmış ses; birini seçin ve gidin.", "zone_archetypes": "Arketipler", "zone_imports": "İthalatlarım", @@ -1658,8 +1594,6 @@ "delete": "Sil", "community_empty": "Henüz topluluk sesi yüklenmedi — internete bağlanıp yeniden açın veya ilk gönderen siz olun.", "community_explainer": "Topluluk tarafından paylaşılan tasarlanmış ön ayarlar ve kayıtlı sesler; omnivoice-gallery'den yüklenir.", - "no_designer": "Kayıtlı ses — bunun yerine \"Sesi kullan\"ı kullanın.", - "no_preview": "Önizleme yok — duymak için \"Sesi kullan\" ile ekleyin.", "submit_failed": "Gönderim formu açılamadı.", "submit_preset": "Ön ayar gönder", "submit_voice": "Ses gönder", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "İlk sesinizi oluşturmaya hazırsınız. Dikteyi şimdi veya daha sonra Ayarlar’dan yapılandırabilirsiniz.", - "welcome": "Hoş geldiniz", "system_check": "Sistem kontrolü", "install_models": "Modelleri yükle", - "pick_engines": "Motorları seç", "system_check_desc": "RAM, disk, GPU, ffmpeg ve ağı araştırın. Engelleyiciler önceden işaretlenir, böylece indirmeden önce bilgi sahibi olursunuz.", "install_models_desc": "~2,4 GB ağırlık indirin — yalnızca TTS modeli gereklidir. Donanımınız için seçilmiş transkripsiyon modellerini şimdi veya daha sonra Ayarlar'dan ekleyin.", "pick_engines_desc": "TTS / ASR / LLM arka uçlarını seçin. Varsayılanlar kutudan çıktığı gibi çalışır; istediğiniz zaman Ayarlar'dan özelleştirebilirsiniz.", "hero_desc": "Dublaj, ses klonlama ve ses tasarımı; tümü makinenizde yerel olarak çalışır.", - "first_run": "İlk çalıştırmanın indirilmesi 5-10 dakika sürer. Bundan sonra her başlatma anında ve tamamen çevrimdışı olur.", - "get_started": "Başlayın", "back": "Geri", "continue_ok": "Her şey yolunda - devam et", "continue_warn": "Devam et (uyarılarla)", @@ -1727,7 +1657,6 @@ "try_dictation": "Dikte etmeyi deneyin", "try_dictation_desc": "Kurulumu bitirmeden önce dikteyi çalıştığından emin olmak için kısayolunuza bir kez basın.", "dictation_step_label": "Dikte", - "next_try_dictation": "Sonraki: Dikteyi deneyin", "step_aria": "Adım {{num}}: {{label}}", "step_completed": "tamamlandı", "cache_label": "Model önbelleği" @@ -1743,7 +1672,6 @@ "platforms": "Platformlar", "footer": "Her katkı, yerel yapay zekanın sınırlarını zorlamaya yardımcı olur.", "back": "Studio'ya geri dön", - "commercial": "Ticari Lisans", "pill": "VoiceStudio'u destekleyin", "suggested_title": "Bir miktar seçin", "most_common": "en yaygın", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "VoiceStudio geliştirmelerini destekleyin", - "lead_default": "İşe yaradığına sevindim! VoiceStudio ücretsizdir ve tamamen yereldir. Size zaman kazandıracaksa, aylık küçük bir chip-in, arkasındaki Claude Max'e fon sağlar.", - "lead_first_clone": "İlk ses klonunuz tamamlandı — güzel! VoiceStudio tamamen makinenizde çalışır ve desteğiniz de bu şekilde kalmasını sağlar.", - "lead_tenth_dub": "On dublaj - açıkça işe koyuyorsunuz. Küçük bir aylık chip-in, bu özellikleri sunan Claude Max'e fon sağlıyor.", - "lead_sustained": "Bir aydır VoiceStudio'la birliktesiniz. İş akışınızda bir yer kazanmışsa, bir sonraki adımın finansmanına yardımcı olmayı düşünün.", - "chip_in": "Çip yapın", - "later": "Belki daha sonra", - "star": "GitHub'da yıldız", - "opt_out": "Bir daha sorma", - "dismiss_aria": "Reddet" + "later": "Belki daha sonra" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Ticari Lisans", "hero_title": "Üretimde yapay zeka seslerini gönderin", "hero_desc": "VoiceStudio, GNU Affero Genel Kamu Lisansı v3 (AGPL-3.0) kapsamında özgür ve açık kaynaklı bir yazılımdır — ticari ve kurum içi iş kullanımı dahil, kullanımı ücretsizdir. Ticari lisans yalnızca VoiceStudio'yu AGPL-3.0'ın copyleft yükümlülükleri olmadan kapalı kaynaklı veya tescilli bir ürün ya da hizmete gömmek istediğinizde gereklidir.", - "why_title": "İşletmeler neden VoiceStudio'u seçiyor?", - "pricing_title": "Fiyatlandırma", - "faq_title": "Sık sorulan sorular", "pricing_desc": "Katmanlar ve fiyatlandırma henüz kesinleşme aşamasındadır.", - "pricing_detail": "Herkese açık hale gelene kadar her ticari dağıtım ayrı ayrı fiyatlandırılır, böylece ekibinize ve iş yükünüze göre doğru boyutu belirleyebiliriz.", "request_quote": "Fiyat teklifi isteyin", - "footer_email": "Sorunuz mu var? hi@voicestudio.sh adresinden bize ulaşın", - "footer_discord": "Topluluk desteği için Discord'umuza katılın.", "benefit_ip": "Tam IP sahipliği", "benefit_ip_desc": "Her çıktının, her modelin, her türevinin sahibi sizsiniz. Paylaşılan kiracılık yok, veri sızıntısı yok.", "benefit_cost": "Dakika başına sıfır maliyet", "benefit_cost_desc": "API faturası yok, sürpriz aşımlar yok. İlk günden itibaren kendi donanımınızda çalışır.", - "benefit_team": "Ekip çapında erişim", "benefit_team_desc": "Bir lisans, kuruluşunuzdaki sınırsız kullanıcıyı kapsar.", "benefit_support": "Doğrudan destek", "benefit_support_desc": "Çekirdek mühendislik ekibinden öncelikli e-posta ve görüntülü arama desteği.", - "benefit_source": "Kaynakta mevcut çekirdek", "benefit_source_desc": "Yığına tam görünürlük. Lisans koşulları dahilinde denetleyin, çatallayın ve uyarlayın.", - "benefit_lang": "646 dil", "benefit_lang_desc": "646 dilde insan düzeyinde kaliteyle metne dönüştürün, tercüme edin ve dublaj yapın.", - "hero_note": "Kullanım, kendi sunucunuzda barındırma ve ticari kullanım AGPL-3.0 kapsamında tamamen ücretsizdir — büyük ölçekte bile. AGPL bir ağ copyleft lisansıdır: VoiceStudio'u değiştirir ve bu değiştirilmiş sürümü ağ üzerinden başkalarına sunarsanız, değiştirilmiş kaynak kodunuzu aynı koşullarla paylaşmanız gerekir. Ticari lisans, tescilli ve kapalı kaynaklı dağıtımlar için bu copyleft yükümlülüklerini kaldırır. Fiyatlandırma planları yakında — bu arada bizimle iletişime geçin.", - "hero_simple": "VoiceStudio, ticari ve dahili iş kullanımı da dahil olmak üzere, AGPL-3.0 kapsamında ücretsiz ve açık kaynaktır. AGPL'nin copyleft yükümlülükleri olmadan kapalı kaynaklı bir ürüne yerleştirmek için yalnızca ticari bir lisansa ihtiyacınız vardır.", - "contact_lead": "Fiyatlandırma, ekibinize ve iş yükünüze uyacak şekilde dağıtım başına belirtilir. Bana kullanım durumunuzu söyleyin, ben de size bir fiyat teklifi vereyim." + "hero_simple": "VoiceStudio, ticari ve dahili iş kullanımı da dahil olmak üzere, AGPL-3.0 kapsamında ücretsiz ve açık kaynaktır. AGPL'nin copyleft yükümlülükleri olmadan kapalı kaynaklı bir ürüne yerleştirmek için yalnızca ticari bir lisansa ihtiyacınız vardır." }, "exportModal": { "export": "İhracat", @@ -2006,7 +1915,6 @@ "loaded_ready": "Yüklendi ve hazır", "loading_first_run": "Yükleniyor… (ilk çalıştırmada bu işlem 1-2 dakika sürebilir)", "failed_to_load": "Yüklenemedi", - "not_loaded_yet": "Henüz yüklenmedi — ilk transkripsiyonda yüklenecek", "tts_not_loaded_yet": "Henüz yüklenmedi — ilk ses oluşturmada yüklenecek", "error_check_logs": "Hata: {{error}}. Günlükleri kontrol edin ve yeniden başlatmayı deneyin.", "check_logs_restart": "Model yükleme hataları için günlükleri kontrol edin. Yeniden başlatmayı deneyin.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Hoparlör Dökümü", - "auto_assign_title": "Çıkarılan hoparlör klonlarından sesleri otomatik olarak atayın", - "auto_cast": "Otomatik yayınlama", - "all_cast": "Tüm oyuncular", - "segments_count": "{{count}} segment", - "assign_voice": "Sesi ata…", - "from_video": "🎤 Videodan ({{name}})", - "no_profiles": "Henüz kayıtlı ses profili yok.", - "preview_voice": "Sesi önizle" + "segments_count": "{{count}} segment" }, "checkpoint": { "timing_review": "{{count}} bölümün zamanlaması incelenmeli", @@ -2203,8 +2104,6 @@ "play_failed": "Oynatma başarısız oldu: {{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace token seti — daha hızlı indirmeler etkinleştirildi", - "hf_token_save_failed": "Jeton kaydedilemedi", "install_started": "Yükleme başladı — satırda ilerleme", "delete_confirm": "{{repoId}} silinsin mi? Daha sonra yeniden yükleyebilirsiniz.", "delete_confirm_title": "Modeli sil", @@ -2247,8 +2146,6 @@ "not_installed": "yüklü değil", "required_tag": "gerekli", "delete_btn": "Sil", - "hf_token_btn": "HF Jetonu", - "hf_set_title": "Daha hızlı indirmeler için HuggingFace jetonunu ayarlayın", "get_token": "Jeton al →", "reco_installed_for": "{{device}} için önerilen paket yüklendi", "reco_for": "{{device}} için önerilir", @@ -2256,8 +2153,6 @@ "required_size": "Gerekli ~{{size}} GB", "all_size": "Hepsi ~{{size}} GB", "req_tag": "talep", - "search_placeholder": "Modelleri ara…", - "search_label": "Modelleri ara", "no_matches": "Filtrelerinizle eşleşen model yok.", "sort_by": "{{column}}'ye göre sırala", "column_model": "Modeli", @@ -2267,12 +2162,8 @@ "ready_badge": "Hazır", "loading_badge": "Yükleniyor…", "idle_badge": "Boşta", - "started_downloading_required_one": "{{count}} gerekli modeli indirmeye başlandı", - "started_downloading_required_other": "{{count}} gerekli model indirilmeye başlandı", "to_download": "indirmek için", "cached": "önbelleğe alınmış", - "fast_download_badge": "hızlı indirme", - "fast_download_title": "Xet {{version}} aracılığıyla hızlı indirmeler — paralel yığın halinde aktarım", "voice_previews": "Ses önizlemeleri", "voice_previews_desc": "Önceden oluşturulmuş önizlemeleri indirin; böylece sesler, bir ses modeli inmeyi bitirmeden önce bile anında çalar. Bunu açana kadar hiçbir şey indirilmez.", "voice_previews_off": "Kapalı — önizlemeler bu cihazda oluşturulur", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Dahili araçlar için lisansa ihtiyacım var mı?", - "a_internal_tools": "Hayır. Çalışanlarınızın ve yüklenicilerinizin kullanımı — kurum içi değişiklik ve kendi sunucunuzda barındırma dahil — AGPL-3.0 kapsamında ücretsizdir. Ticari lisans yalnızca VoiceStudio'u kapalı kaynaklı veya tescilli bir ürün ya da hizmete gömüp AGPL'nin kaynak paylaşım yükümlülüklerine uymak istemediğinizde gereklidir.", - "q_try_before": "Taahhüt etmeden önce deneyebilir miyim?", - "a_try_before": "Evet. Uygulamanın tamamı AGPL-3.0 kapsamında ücretsiz indirilebilir, çalıştırılabilir ve kendi sunucunuzda barındırılabilir — hiçbir sözleşme gerekmez. Ticari (tescilli kullanım) lisansını görüşmeye hazır olduğunuzda bize e-posta gönderin; ayrıntıları birlikte netleştirelim.", - "q_watermark": "Filigran ne olacak?", - "a_watermark": "Görünmez AudioSeal filigranı varsayılan olarak herkes için gömülür. Ayarlar → Gizlilik bölümünden kapatabilirsiniz; yalnızca değişiklikten sonra üretilen sesi etkiler." - }, "gallery_extra": { - "save_prompt": "Bu ses profili için bir ad girin:", - "saved_as_profile": "Ses profil olarak kaydedildi!", - "save_profile_failed": "Profil kaydedilemedi", - "download_failed": "İndirme başarısız oldu: {{message}}", - "trim_load_failed": "Kırpma için ses yüklenemedi: {{message}}", - "upload_crop_failed": "Kırpılmış ses yüklenemedi: {{message}}" + "trim_load_failed": "Kırpma için ses yüklenemedi: {{message}}" }, "dub_workflow": { "preparing_audio": "Ses hazırlanıyor…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Yükleniyor…", - "trimmed_loaded": "Kırpılmış ses yüklendi", - "toast_exported": "Dışa aktarıldı: {{name}}", - "toast_export_failed": "Dışa aktarma başarısız oldu: {{message}}", - "toast_open_folder_failed": "Klasör açılamadı: {{message}}", "toast_saving": "{{name}} kaydediliyor...", "toast_saved": "Kaydedildi: {{path}}", "toast_save_error": "Kaydetme hatası: {{message}}", "toast_processing": "{{name}} işleniyor...", "toast_downloaded": "İndirildi {{name}}", "toast_download_error": "İndirme hatası: {{message}}", - "toast_upload_first": "Videonun kaydedilmeden önce sunucuda işlenmesi için lütfen önce 'Yükle ve Metne Yaz'ı tıklayın.", - "toast_save_failed": "Kaydetme başarısız oldu: {{message}}", - "toast_opened": "Açılış: {{name}}", - "toast_project_deleted": "Proje silindi", - "toast_restored_state": "Önceki nesil durumu geri yüklendi", - "toast_history_deleted": "Geçmiş öğesi silindi", "toast_flushed": "Temizlenmiş — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· model boşaltıldı", "toast_flush_failed": "Temizleme başarısız oldu: {{message}}", - "toast_project_saved": "Proje kaydedildi", - "toast_project_created": "Proje oluşturuldu" + "toast_project_saved": "Proje kaydedildi" }, "update": { "available": "{{version}} güncellemesi mevcut", @@ -2531,13 +2398,11 @@ "not_writable": "yazılamaz", "change": "Değiştir…", "compute_title": "Hesaplama", - "compute_label": "GPU / hızlandırıcı", "compute_auto": "Otomatik (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD GPU (ROCm, Linux)", "channel_label": "Güncelleme kanalı", "channel_stable": "Kararlı", "channel_preview": "Önizleme (en yeni main)", - "network_title": "Ağ", "region_label": "İndirme bölgesi", "mirrors_title": "Özel yansılar (gelişmiş)", "mirror_pypi": "PyPI dizin URL'si", @@ -2570,10 +2435,6 @@ "trust_line": "Önce gizlilik: sesleriniz, kayıtlarınız ve projeleriniz bu makineden asla ayrılmaz — hesap yok, bulut işleme yok.", "resume_note": "Yarıda kalan indirmeler otomatik devam eder — uygulamayı kapatmak güvenlidir.", "eta_left": "~{{eta}} kaldı", - "first_sound_text": "Stüdyona hoş geldin. Duyduğun her kelime az önce bu makinede üretildi.", - "first_sound_done": "O ses mi? Saniyeler önce, yerelde üretildi. Hoş geldin.", - "hf_token_title": "Hugging Face belirteci (isteğe bağlı)", - "hf_token_hint": "Kısıtlı modellerin kilidini açar — ör. çok konuşmacılı dublaj için konuşmacı ayrıştırma (pyannote). Ücretsiz hesap belirteci; bu makinede kalır.", "hf_token_save": "Kaydet", "hf_token_saving": "kaydediliyor…", "hf_token_saved": "Hugging Face belirteci kaydedildi", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} kaldı", "hf_token_card_title": "Daha hızlı indirmeler için ücretsiz bir Hugging Face belirteci ekleyin", "hf_token_get": "Jetonunuz yok mu? Ücretsiz bir tane alın →", - "hf_token_saved_fast": "Hugging Face jetonu kaydedildi; indirme işlemleri artık daha hızlı", "hf_token_inline_prompt": "Ücretsiz Hugging Face belirteci ile indirmeleri hızlandırın", "hf_token_inline_ph": "hf_… jetonunu yapıştırın (isteğe bağlı)", "hf_token_get_short": "Ücretsiz bir tane alın →", @@ -2607,15 +2467,11 @@ "unstar_take": "Yıldızı kaldır — temizliğe izin ver", "play_take": "Etkin çıktı olarak yükle", "filter_starred": "Yıldızlı", - "load_take_failed": "Kayıt yüklenemedi: {{message}}", "mode_clone": "Klonlama", "mode_synth": "Sentez", "generation_seconds": "{{duration}} sn", - "expand_text": "Metni genişlet", - "collapse_text": "Metni daralt", "seed": "Seed {{seed}}", - "load_config": "Ayarları yükle", - "delete_confirm": "Bu geçmiş öğesi silinsin mi?" + "load_config": "Ayarları yükle" }, "openapi": { "title": "VoiceStudio API", @@ -3015,25 +2871,18 @@ "preview": "Sponsor önizlemesi", "message": "Markanız, web siteniz ve mesajınız", "email_app": "E-posta uygulamasını aç", - "copy_email": "E-postayı kopyala", - "preview_detail": "Logonuz, bağlantınız ve tanıtımınız burada görünebilir." + "copy_email": "E-postayı kopyala" }, "supportPlans": { "remove": "Sponsor çubuğunu kaldır", "title": "Ücretsiz ve Pro", "free": "Ücretsiz", "pro": "Pro", - "sponsor_bar": "Sponsor çubuğu", "visible": "Görünür", - "hideable": "Gizlenebilir", "unavailable": "Pro etkinleştirmesi henüz yapılandırılmadı.", "telemetry": "Telemetri", - "opt_in": "İsteğe bağlı, onay ile", "disabled": "Devre dışı", - "badge": "Pro rozeti", - "advanced": "Gelişmiş araçlar", - "standard": "Standart", - "included": "Dahil" + "badge": "Pro rozeti" }, "integrationCatalog": { "title": "Entegrasyonlar", @@ -3042,8 +2891,7 @@ "n8nHint": "Bu iş akışını n8n içine aktarın, isteği düzenleyip çalıştırın. VoiceStudio açık kalsın. Docker ve uzak n8n için erişilebilir bir arka uç adresi ve gerekli kimlik bilgileri gerekir." }, "directoryExamples": { - "example": "Dizin örneği", - "notice": "Yalnızca dizin örnekleri; sponsor veya bağlı entegrasyon değildir." + "example": "Dizin örneği" }, "workflowRun": { "run": "İş akışını çalıştır", diff --git a/electron/src/shared/i18n/locales/uk.json b/electron/src/shared/i18n/locales/uk.json index ea581578f..981f58f68 100644 --- a/electron/src/shared/i18n/locales/uk.json +++ b/electron/src/shared/i18n/locales/uk.json @@ -59,15 +59,10 @@ "title": "Запитати VoiceStudio Agent", "notInstalled": "Не встановлено", "noSource": "Відкрийте доступну для запису копію коду VoiceStudio, щоб агент міг її виправити.", - "noneFound": "Встановіть Codex, Claude Code, OpenCode або Pi для локального виправлення.", "ready": "Готовий", "placeholder": "Що пішло не так? Додайте кроки, видимі помилки або очікувану поведінку.", - "prReady": "Виправлення завершено. Перевірте зміни та запропонуйте PR після проходження цільових тестів.", "diagnose": "Діагностика", "fix": "Виправити", - "chooseDefault": "Виберіть агента, якого VoiceStudio автоматично використовуватиме в разі збою цього екрана.", - "useAutomatically": "Автоматично використовувати {{agent}}", - "captured": "Зібрано рядків журналу з проблемами: {{count}}", "contextNotice": "Вибраний агент отримає цей звіт, поточний екран, останні журнали застосунку й бекенду та діагностику системи.", "complete": "Завершено" }, @@ -607,7 +602,6 @@ "characters": "{{count}} символів", "minutes": "~{{count}} хв", "chars": "{{count}} символів", - "exportDone": "Аудіокнигу завантажено", "exportFailed": "Помилка експорту — перевірте, чи працює серверна частина.", "global_speed": "швидкість", "global_speed_hint": "Швидкість читання для всіх рядків без власного перевизначення швидкості", @@ -818,7 +812,6 @@ "data_dir": "Каталог даних", "outputs": "Виходи", "crash_log": "Журнал збоїв", - "update_endpoint": "Оновити кінцеву точку", "yes": "так", "no": "немає", "web_preview": "веб-перегляд", @@ -879,38 +872,21 @@ "title": "Облікові дані", "desc": "Ключі та маркери API встановлено <1>лише для цього сеансу. Для постійності під час перезапусків установіть їх як змінні середовища у своєму профілі оболонки.", "hf_token": "Токен HuggingFace", - "hf_help": "Потрібний для щоденника доповідача та швидшого завантаження моделі. Отримайте свій на huggingface.co/settings/tokens.", - "translate_key": "Ключ API LLM", - "translate_help": "Додатково — для DeepL, OpenAI або платних постачальників послуг перекладу. Не потрібно для Google Translate (безкоштовний рівень).", "save": "зберегти", "saved": "✓ Набір", - "not_set": "✗ Не встановлено", "get_token": "Отримати маркер →", - "saved_session": "{{key}} збережено для цього сеансу", "save_failed": "Не вдалося зберегти", "save_error": "Не вдалося зберегти: {{message}}", "deepl_key": "Ключ API DeepL", - "deepl_help": "Для системи перекладу DeepL. Отримайте свій ключ на deepl.com/pro-api.", "deepl_base_url": "Базова URL-адреса DeepL", "deepl_base_url_help": "Спеціальна кінцева точка DeepL API (за замовчуванням: https://api.deepl.com/v2). Змінити на проксі/самостійне розміщення.", "microsoft_key": "Ключ перекладача Microsoft", - "microsoft_help": "Для двигуна Microsoft Translator. Отримайте свій ключ із порталу Azure.", "microsoft_base_url": "URL-адреса бази перекладачів Microsoft", "microsoft_base_url_help": "Спеціальна кінцева точка Azure Translator (за замовчуванням: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "LLM Base URL", - "llm_base_url_help": "Для кінематографічної якості або двигуна LLM. Будь-яка кінцева точка, сумісна з OpenAI (OpenAI, Ollama, LM Studio тощо).", - "llm_model": "Модель LLM", - "llm_model_help": "Назва моделі для використання з механізмом LLM (наприклад, gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "HuggingFace", - "group_translate": "Переклад (магістр права / кіно)", - "group_deepl": "Спеціальна кінцева точка DeepL", - "group_microsoft": "Спеціальна кінцева точка Microsoft Translator", "region": "Регіон", "aws_profile": "Профіль AWS" }, "engines": { - "banners_on": "Етап банери на", - "banners_off": "Сценічні банери вимкнуті", "backend": "Backend", "status": "Статус", "ready": "готовий", @@ -941,7 +917,6 @@ "dictation_model_hint": "Використовується для живого диктування та цим рушієм під час транскрибування.", "dictation_not_installed": "Не встановлено — виберіть, щоб завантажити", "active": "активний", - "whyUnavailable": "Що потрібно", "diskDetails": "Відомості про диск", "diskModelDownload": "Завантаження моделі", "diskPackageDownload": "Завантаження пакетів", @@ -965,11 +940,8 @@ "sectionReady": "Готові до використання", "sectionMore": "Додати більше двигунів", "lastError": "Остання помилка: {{error}}", - "installedAndReady": "Встановлено і готово", - "notInstalled": "Не встановлено", "available": "в наявності", "subprocessTitle": "Працює у власному підпроцесі + venv", - "inProcessTitle": "Працює в процесі VoiceStudio Python", "testEngine": "Тестовий двигун", "testing": "Тестування…", "recheck": "Повторна перевірка", @@ -986,7 +958,6 @@ "routingUnknown": "Невідомий", "routingEffectiveChip": "Працює на {{device}} на цій машині", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "Графічний процесор вибрано, але: {{reason}}", "selectCpuFallback": "{{engine}}: працює на CPU — {{reason}}", "selectWithCaveat": "Перемкнено на {{engine}} — {{reason}}", "generateCaveat": "{{engine}} може працювати повільно на цьому комп'ютері — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Немає журналу Tauri. Працює лише в оболонці робочого столу.", "empty_backend": "Журнал виконання порожній. Діяльність відображатиметься тут, оскільки серверна система реєструватиме її.", "title": "Журнали", - "source_backend": "Backend", - "source_frontend": "Інтерфейс", - "source_tauri": "Телець", "expand": "Розгорнути журнали", "collapse": "Згорнути журнали", "expand_aria": "Розгорнути панель журналів", @@ -1104,12 +1072,10 @@ "unlock": "Розблокувати", "try_voice": "Спробуйте цей голос", "test_phrase": "Тестова фраза", - "test_help": "Введіть що завгодно — ми згенеруємо його з поточними налаштуваннями цього голосу.", "test_placeholder": "Введіть щось, щоб почути цей голос…", "gen_preview": "Створити попередній перегляд", "generating": "Створення…", "used_title": "Де використовувався цей голос", - "used_empty": "Цей голос ще не використовувався. Згенеруйте попередній перегляд вище або виберіть його в дубляжі.", "synth_clips_one": "{{count}} синтезаторний кліп", "synth_clips_other": "{{count}} синтезаторні кліпи", "projects_count_one": "{{count}} проект", @@ -1152,9 +1118,7 @@ "translate": "Перекласти", "generate": "Генерувати", "export": "Експорт", - "no_video": "Відео не завантажено", "segments": "сегменти", - "speakers": "колонки", "video_dubbing_studio": "Студія дубляжу відео", "drop_here": "Перетягніть відео або аудіо сюди", "paste_url": "…або вставте URL-адресу YouTube/відео", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Розмір експорту cookie не повинен перевищувати 1 МБ.", "save": "зберегти", "reset": "Скинути", - "save_project": "Зберегти проект", "change_file": "Змінити файл", "import_srt": "Імпорт .srt", "import_srt_alt": "Натомість імпортуйте .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Перекласти", "transcript": "Стенограма", "generate_dub": "Згенерувати Dub", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Завантажити та транскрибувати", "multi_lang": "Багатомовний", "prep_download": "Завантаження відео…", @@ -1206,7 +1166,6 @@ "transcribing": "Транскрибування аудіо…", "elapsed": "минув", "remaining": "s залишилося", - "sidebar_toggle": "Перемкнути бічну панель", "retry_transcription": "Повторити транскрипцію", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Якщо URL-адреса є хостом із субтитрами (YouTube, Vimeo, TED…), також витягніть оригінальні субтитри та будь-які автоматичні переклади YouTube. Засідає редактор без запуску Whisper; пропустити Перекласти все для мов, які вже охоплює YouTube.", @@ -1249,7 +1208,6 @@ "engine_label": "Двигун", "install_engine": "Встановіть цей двигун", "installing_engine": "… встановлення", - "needs_dev_install": "потребує інсталяції розробника", "install_disabled_title": "У пакетних збірках встановлення вимкнено", "needs_install_suffix": "— потребує встановлення", "glossary_title": "Закріпити переклади повторюваних термінів (імен, слів брендів, жаргону)", @@ -1283,7 +1241,6 @@ "original_track": "Оригінал", "selected_dub": "{{code}} (вибраний дубляж)", "dub_track": "{{code}} (дубляж)", - "export_tracks": "Експорт треків:", "stopping": "Зупинка…", "stop_progress": "Зупинити ({{current}}/{{total}})", "regen_changed": "Regen {{count}} змінено", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} встановлено", "install_failed": "Помилка встановлення: {{message}}", "prep_elapsed": "{{time}} минуло", - "add_language": "Додайте мову", "search_languages": "Пошук мов…", "languages_selected_one": "Вибрано {{count}} мову", "languages_selected_other": "Вибрано {{count}} мов", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "час", - "spkr": "Spkr", "text": "текст", "lang": "ланг", "voice": "Голос", @@ -1403,8 +1358,6 @@ "select_all_title": "Виберіть усі відфільтровані", "sel_count": "{{count}} сел", "select_title": "Виберіть сегмент (Shift+клацніть для діапазону)", - "sync_title": "Згенероване аудіо становить {{pct}}% тривалості оригіналу", - "sync_label": "Синхронізація: {{pct}}%", "rate_title": "Підгонка швидкості мовлення: {{ratio}}× відносно слота{{error}}", "speaker_id": "ID спікера", "translate_error_title": "Помилка перекладу: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Відновити вихідний текст", "lang_default": "(зах.)", "voice_default": "За замовчуванням", - "from_video": "З відео", - "clone_profiles": "Профілі клонів", - "design_presets": "Попередні налаштування дизайну", "preview_title": "Попередній перегляд", "set_direction": "Встановити напрямок…", "edit_direction": "Редагувати напрямок…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Особистість", - "pick_personality": "Виберіть налаштування особистості…", "instruct": "Інструктувати", "reference_text": "Довідковий текст", "language": "Мова", @@ -1535,9 +1484,7 @@ "stories": "оповідання", "untitled_story": "Розповідь без назви", "story_lines": "{{count}} рядок", - "story_lines_plural": "{{count}} рядків", "story_voices": "{{count}} голос", - "story_voices_plural": "{{count}} голосів", "audiobooks": "Аудіокниги", "audiobook": "Аудіокнига", "story": "Розповідь", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Галерея", - "search_placeholder": "Пошук на YouTube…", - "all_voices": "Усі голоси ({{count}})", - "no_voices": "Голосів ще немає", "download_hint": "Шукайте вище, щоб завантажити кліпи", - "search_btn": "Пошук", - "download_btn": "Завантажити", "saving": "Збереження…", "trim": "Обрізка", - "delete_confirm": "Видалити цей голосовий запис?", - "view_mode_list": "Список", - "view_mode_grid": "Сітка", - "youtube_results": "Результати YouTube ({{count}})", - "clone_profile": "Клонувати профіль", - "crop_audio": "Обрізати аудіо", "subtitle": "Сотні готових розроблених голосів — виберіть один і вперед.", "zone_archetypes": "Архетипи", "zone_imports": "Мій імпорт", @@ -1658,8 +1594,6 @@ "delete": "Видалити", "community_empty": "Голоси спільноти ще не завантажено — підключіться до інтернету й відкрийте знову, або станьте першим, хто надішле свій.", "community_explainer": "Розроблені пресети та записані голоси, якими поділилася спільнота, завантажені з omnivoice-gallery.", - "no_designer": "Записаний голос — натомість скористайтеся \"Використовуйте голос\".", - "no_preview": "Немає попереднього перегляду — додайте його через \"Використовуйте голос\", щоб почути.", "submit_failed": "Не вдалося відкрити форму подання.", "submit_preset": "Надіслати пресет", "submit_voice": "Надіслати голос", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Усе готово для створення першого голосу. Диктування можна налаштувати зараз або пізніше в налаштуваннях.", - "welcome": "Ласкаво просимо", "system_check": "Перевірка системи", "install_models": "Встановити моделі", - "pick_engines": "Вибирайте двигуни", "system_check_desc": "Перевірте оперативну пам’ять, диск, графічний процесор, ffmpeg і мережу. Blockers are flagged upfront so you know before downloading.", "install_models_desc": "Завантажте ~2,4 ГБ ваг — обов'язковою є лише модель TTS. Додайте моделі транскрипції, підібрані для вашого обладнання, зараз або пізніше в Налаштуваннях.", "pick_engines_desc": "Виберіть серверні модулі TTS / ASR / LLM. Налаштування за замовчуванням працюють із коробки — налаштуйте будь-коли в налаштуваннях.", "hero_desc": "Дубляж, клонування голосу та дизайн голосу — усе це працює локально на вашому комп’ютері.", - "first_run": "Завантаження першого запуску займає 5–10 хвилин. Після цього кожен запуск відбувається миттєво та повністю в автономному режимі.", - "get_started": "Почніть роботу", "back": "Назад", "continue_ok": "Все добре — продовжуйте", "continue_warn": "Продовжити (з попередженнями)", @@ -1727,7 +1657,6 @@ "try_dictation": "Спробуйте диктант", "try_dictation_desc": "Натисніть своє поєднання клавіш один раз, щоб переконатися, що диктування працює, перш ніж завершити налаштування.", "dictation_step_label": "Диктування", - "next_try_dictation": "Далі: спробуйте надиктувати", "step_aria": "Крок {{num}}: {{label}}", "step_completed": "завершено", "cache_label": "Кеш моделі" @@ -1743,7 +1672,6 @@ "platforms": "Платформи", "footer": "Кожен внесок допомагає розширити межі місцевого ШІ.", "back": "Назад до студії", - "commercial": "Комерційна ліцензія", "pill": "Підтримка VoiceStudio", "suggested_title": "Виберіть суму", "most_common": "найпоширеніший", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Підтримайте розробку VoiceStudio", - "lead_default": "Радий, що спрацювало! VoiceStudio є безкоштовним і повністю локальним. Якщо це економить ваш час, невеликий щомісячний внесок фінансує Клода Макса, який стоїть за цим.", - "lead_first_clone": "Ваш перший голосовий клон готовий — чудово! VoiceStudio повністю працює на вашому комп’ютері, і ваша підтримка підтримує його.", - "lead_tenth_dub": "Десять дубляжів — ви явно вводите це в роботу. Невеликий щомісячний внесок фінансує Claude Max, який постачає ці функції.", - "lead_sustained": "Ви працюєте в VoiceStudio протягом місяця. Якщо він заслужив місце у вашому робочому процесі, подумайте про те, щоб допомогти фінансувати те, що буде далі.", - "chip_in": "Чип в", - "later": "Можливо пізніше", - "star": "Зірка на GitHub", - "opt_out": "Більше не питай", - "dismiss_aria": "Відхилити" + "later": "Можливо пізніше" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Комерційна ліцензія", "hero_title": "Надішліть голоси AI у виробництво", "hero_desc": "VoiceStudio — це вільне програмне забезпечення з відкритим кодом за ліцензією GNU Affero General Public License v3 (AGPL-3.0): безкоштовне для будь-якого використання, зокрема комерційного та внутрішньокорпоративного. Комерційна ліцензія потрібна лише тоді, коли ви хочете вбудувати VoiceStudio в закритий або пропрієтарний продукт чи сервіс без копілефт-зобов'язань AGPL-3.0.", - "why_title": "Чому компанії обирають VoiceStudio", - "pricing_title": "Ціноутворення", - "faq_title": "Загальні запитання", "pricing_desc": "Рівні та ціни ще завершуються.", - "pricing_detail": "Поки вони не стануть загальнодоступними, кожне комерційне розгортання оцінюється окремо, щоб ми могли підібрати правильний розмір для вашої команди та робочого навантаження.", "request_quote": "Запитати цінову пропозицію", - "footer_email": "Запитання? Зверніться за адресою hi@voicestudio.sh", - "footer_discord": "Приєднуйтесь до нашого Discord для підтримки спільноти.", "benefit_ip": "Повне володіння IP", "benefit_ip_desc": "Ви володієте кожним результатом, кожною моделлю, кожною похідною. Жодної спільної оренди, жодних витоків даних.", "benefit_cost": "Нульова вартість хвилини", "benefit_cost_desc": "Жодних рахунків за API, жодних несподіваних надлишків. Працює на вашому апаратному забезпеченні з першого дня.", - "benefit_team": "Доступ для всієї команди", "benefit_team_desc": "Одна ліцензія поширюється на необмежену кількість користувачів у вашій організації.", "benefit_support": "Пряма підтримка", "benefit_support_desc": "Пріоритетна підтримка електронною поштою та відеодзвінками від основної команди інженерів.", - "benefit_source": "Доступне джерело ядра", "benefit_source_desc": "Повна видимість стека. Аудит, розгалуження та адаптація в рамках умов ліцензії.", - "benefit_lang": "646 мов", "benefit_lang_desc": "Транскрибуйте, перекладайте та дублюйте 646 мовами з якістю людського рівня.", - "hero_note": "Використання, самостійний хостинг і комерційне застосування безкоштовні за AGPL-3.0 — зокрема й у великих масштабах. AGPL — це ліцензія з мережевим копілефтом: якщо ви модифікуєте VoiceStudio і надаєте змінену версію іншим через мережу, ви зобов'язані відкрити свій змінений вихідний код на тих самих умовах. Комерційна ліцензія знімає ці копілефт-зобов'язання для пропрієтарних закритих розгортань. Тарифи з'являться незабаром — а поки що зв'яжіться з нами.", - "hero_simple": "VoiceStudio є безкоштовною програмою з відкритим вихідним кодом відповідно до AGPL-3.0, зокрема для комерційного та внутрішнього ділового використання. Вам потрібна лише комерційна ліцензія, щоб вставити його в продукт із закритим кодом без зобов’язань AGPL щодо копілефту.", - "contact_lead": "Ціна вказана за розгортання, щоб вона відповідала вашій команді та робочому навантаженню. Розкажіть мені про свій варіант використання, і я отримаю вам ціну." + "hero_simple": "VoiceStudio є безкоштовною програмою з відкритим вихідним кодом відповідно до AGPL-3.0, зокрема для комерційного та внутрішнього ділового використання. Вам потрібна лише комерційна ліцензія, щоб вставити його в продукт із закритим кодом без зобов’язань AGPL щодо копілефту." }, "exportModal": { "export": "Експорт", @@ -2006,7 +1915,6 @@ "loaded_ready": "Завантажений і готовий", "loading_first_run": "Завантаження… (це може зайняти 1-2 хвилини під час першого запуску)", "failed_to_load": "Не вдалося завантажити", - "not_loaded_yet": "Ще не завантажено — буде завантажено під час першої транскрипції", "tts_not_loaded_yet": "Ще не завантажена — завантажиться під час першої генерації мовлення", "error_check_logs": "Помилка: {{error}}. Перевірте журнали та спробуйте перезапустити.", "check_logs_restart": "Перевірте журнали на наявність помилок завантаження моделі. Спробуйте перезапустити.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Кастинг динаміків", - "auto_assign_title": "Автоматичне призначення голосів із вилучених клонів динаміків", - "auto_cast": "Автокаст", - "all_cast": "Всі актори", - "segments_count": "{{count}} сегментів", - "assign_voice": "Призначити голос…", - "from_video": "🎤 З відео ({{name}})", - "no_profiles": "Голосові профілі ще не збережено.", - "preview_voice": "Попередній перегляд голосу" + "segments_count": "{{count}} сегментів" }, "checkpoint": { "timing_review": "Потрібно перевірити таймінг {{count}} сегментів", @@ -2203,8 +2104,6 @@ "play_failed": "Помилка відтворення: {{message}}" }, "models": { - "hf_token_set_toast": "Набір токенів HuggingFace — увімкнено швидші завантаження", - "hf_token_save_failed": "Не вдалося зберегти маркер", "install_started": "Встановлення розпочато — прогрес у рядку", "delete_confirm": "Видалити {{repoId}}? Ви можете перевстановити його пізніше.", "delete_confirm_title": "Видалити модель", @@ -2247,8 +2146,6 @@ "not_installed": "не встановлено", "required_tag": "потрібно", "delete_btn": "Видалити", - "hf_token_btn": "Токен HF", - "hf_set_title": "Встановіть маркер HuggingFace для швидшого завантаження", "get_token": "Отримати маркер →", "reco_installed_for": "Рекомендований пакет, встановлений для {{device}}", "reco_for": "Рекомендовано для {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Потрібно ~{{size}} Гб", "all_size": "Всі ~{{size}} Гб", "req_tag": "вимога", - "search_placeholder": "Пошук моделей…", - "search_label": "Пошук моделей", "no_matches": "Жодна модель не відповідає вашим фільтрам.", "sort_by": "Сортувати за {{column}}", "column_model": "Модель", @@ -2267,12 +2162,8 @@ "ready_badge": "Готовий", "loading_badge": "Завантаження…", "idle_badge": "Бездіяльність", - "started_downloading_required_one": "Розпочато завантаження {{count}} потрібної моделі", - "started_downloading_required_other": "Розпочато завантаження {{count}} необхідних моделей", "to_download": "завантажити", "cached": "кешується", - "fast_download_badge": "швидке завантаження", - "fast_download_title": "Швидке завантаження через Xet {{version}} — паралельна передача фрагментів", "voice_previews": "Зразки голосів", "voice_previews_desc": "Завантажте заздалегідь створені зразки, щоб голоси відтворювалися миттєво — навіть до того, як голосова модель завершить завантаження. Нічого не завантажується, доки ви це не увімкнете.", "voice_previews_off": "Вимкнено — зразки створюються на цьому пристрої", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Чи потрібна мені ліцензія на внутрішні інструменти?", - "a_internal_tools": "Ні. Використання вашими працівниками та підрядниками — включно з внутрішніми модифікаціями й самостійним хостингом — безкоштовне за AGPL-3.0. Комерційна ліцензія потрібна лише якщо ви вбудовуєте VoiceStudio у закритий або пропрієтарний продукт чи сервіс і не хочете виконувати вимоги AGPL щодо розкриття вихідного коду.", - "q_try_before": "Чи можу я спробувати перед тим, як взяти участь?", - "a_try_before": "Так. Повну версію застосунку можна безкоштовно завантажити, запускати та хостити самостійно за AGPL-3.0 — без жодної угоди. Коли будете готові обговорити комерційну ліцензію (пропрієтарне використання), напишіть нам — разом узгодимо деталі.", - "q_watermark": "А як щодо водяного знака?", - "a_watermark": "Невидимий водяний знак AudioSeal вбудовується за умовчанням для всіх. Його можна вимкнути в Налаштуваннях → Приватність; це стосується лише аудіо, створеного після зміни." - }, "gallery_extra": { - "save_prompt": "Введіть назву для цього голосового профілю:", - "saved_as_profile": "Голос збережено як профіль!", - "save_profile_failed": "Не вдалося зберегти профіль", - "download_failed": "Помилка завантаження: {{message}}", - "trim_load_failed": "Не вдалося завантажити аудіо для обрізання: {{message}}", - "upload_crop_failed": "Не вдалося завантажити обрізаний голос: {{message}}" + "trim_load_failed": "Не вдалося завантажити аудіо для обрізання: {{message}}" }, "dub_workflow": { "preparing_audio": "Підготовка аудіо…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Завантаження…", - "trimmed_loaded": "Завантажено обрізаний звук", - "toast_exported": "Експортовано: {{name}}", - "toast_export_failed": "Помилка експорту: {{message}}", - "toast_open_folder_failed": "Не вдалося відкрити папку: {{message}}", "toast_saving": "Збереження {{name}}...", "toast_saved": "Збережено: {{path}}", "toast_save_error": "Помилка збереження: {{message}}", "toast_processing": "Обробка {{name}}...", "toast_downloaded": "Завантажено {{name}}", "toast_download_error": "Помилка завантаження: {{message}}", - "toast_upload_first": "Будь ласка, спочатку натисніть «Завантажити та транскрибувати», щоб відео було оброблено на сервері перед збереженням.", - "toast_save_failed": "Не вдалося зберегти: {{message}}", - "toast_opened": "Відкрито: {{name}}", - "toast_project_deleted": "Проект видалено", - "toast_restored_state": "Відновлено стан попереднього покоління", - "toast_history_deleted": "Елемент історії видалено", "toast_flushed": "Очищено — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· модель ненавантажена", "toast_flush_failed": "Помилка очищення: {{message}}", - "toast_project_saved": "Проект збережено", - "toast_project_created": "Проект створено" + "toast_project_saved": "Проект збережено" }, "update": { "available": "Доступне оновлення {{version}}", @@ -2531,13 +2398,11 @@ "not_writable": "запис неможливий", "change": "Змінити…", "compute_title": "Обчислення", - "compute_label": "GPU / прискорювач", "compute_auto": "Авто (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Канал оновлень", "channel_stable": "Стабільний", "channel_preview": "Попередній (останній main)", - "network_title": "Мережа", "region_label": "Регіон завантаження", "mirrors_title": "Власні дзеркала (додатково)", "mirror_pypi": "URL індексу PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "Приватність понад усе: ваші голоси, записи та проєкти ніколи не залишають цю машину — без облікового запису та без хмарної обробки.", "resume_note": "Перервані завантаження відновлюються автоматично — закривати застосунок безпечно.", "eta_left": "залишилось ~{{eta}}", - "first_sound_text": "Вітаємо у вашій студії. Кожне слово, яке ви чуєте, щойно згенеровано на цій машині.", - "first_sound_done": "Той голос? Згенерований секунди тому, локально. Вітаємо.", - "hf_token_title": "Токен Hugging Face (необов’язково)", - "hf_token_hint": "Відкриває обмежені моделі — напр., діаризацію мовців для багатоголосого дубляжу (pyannote). Токен безкоштовного акаунта; залишається на цій машині.", "hf_token_save": "Зберегти", "hf_token_saving": "збереження…", "hf_token_saved": "Токен Hugging Face збережено", @@ -2583,7 +2444,6 @@ "size_left": "залишилося {{size}}", "hf_token_card_title": "Додайте безкоштовний жетон Hugging Face для швидшого завантаження", "hf_token_get": "Немає жетона? Отримайте один безкоштовно →", - "hf_token_saved_fast": "Токен Hugging Face збережено — завантаження тепер відбуваються швидше", "hf_token_inline_prompt": "Прискоріть завантаження за допомогою безкоштовного маркера Hugging Face", "hf_token_inline_ph": "Вставте маркер hf_… (необов’язково)", "hf_token_get_short": "Отримайте один безкоштовно →", @@ -2607,15 +2467,11 @@ "unstar_take": "Зняти зірку — дозволити очищення", "play_take": "Завантажити як активний вивід", "filter_starred": "Із зіркою", - "load_take_failed": "Не вдалося завантажити дубль: {{message}}", "mode_clone": "Клонування", "mode_synth": "Синтез", "generation_seconds": "{{duration}} с", - "expand_text": "Розгорнути текст", - "collapse_text": "Згорнути текст", "seed": "Сід {{seed}}", - "load_config": "Завантажити налаштування", - "delete_confirm": "Видалити цей елемент історії?" + "load_config": "Завантажити налаштування" }, "openapi": { "title": "API VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Приклад спонсорського блоку", "message": "Ваш бренд, сайт і повідомлення", "email_app": "Відкрити поштовий застосунок", - "copy_email": "Скопіювати email", - "preview_detail": "Тут можуть з’явитися ваш логотип, посилання та опис." + "copy_email": "Скопіювати email" }, "supportPlans": { "remove": "Прибрати панель спонсорів", "title": "Безкоштовна версія та Pro", "free": "Безкоштовно", "pro": "Pro", - "sponsor_bar": "Панель спонсорів", "visible": "Видима", - "hideable": "Можна приховати", "unavailable": "Активацію Pro ще не налаштовано.", "telemetry": "Телеметрія", - "opt_in": "Необов’язкова, за згодою", "disabled": "Вимкнена", - "badge": "Значок Pro", - "advanced": "Розширені інструменти", - "standard": "Стандартні", - "included": "Включені" + "badge": "Значок Pro" }, "integrationCatalog": { "title": "Інтеграції", @@ -3042,8 +2891,7 @@ "n8nHint": "Імпортуйте цей процес у n8n, змініть запит і запустіть його. Залиште VoiceStudio запущеним. Для Docker і віддаленого n8n потрібні доступна адреса бекенду та необхідні облікові дані." }, "directoryExamples": { - "example": "Приклад у каталозі", - "notice": "Лише приклади каталогу — не спонсори й не підключені інтеграції." + "example": "Приклад у каталозі" }, "workflowRun": { "run": "Запустити процес", diff --git a/electron/src/shared/i18n/locales/vi.json b/electron/src/shared/i18n/locales/vi.json index d8c2c3240..f40b00f58 100644 --- a/electron/src/shared/i18n/locales/vi.json +++ b/electron/src/shared/i18n/locales/vi.json @@ -59,15 +59,10 @@ "title": "Hỏi VoiceStudio Agent", "notInstalled": "Chưa được cài đặt", "noSource": "Mở bản mã nguồn VoiceStudio có quyền ghi để trợ lý có thể sửa lỗi.", - "noneFound": "Cài đặt Codex, Claude Code, OpenCode hoặc Pi để sử dụng tính năng sửa chữa cục bộ.", "ready": "Sẵn sàng", "placeholder": "Điều gì đã xảy ra? Thêm các bước, lỗi hiển thị hoặc hành vi mong đợi.", - "prReady": "Sửa xong. Xem xét các thay đổi và đề xuất PR khi các bài kiểm tra mục tiêu vượt qua.", "diagnose": "Chẩn đoán", "fix": "Sửa chữa", - "chooseDefault": "Chọn tác nhân mà VoiceStudio sẽ tự động dùng khi màn hình này gặp sự cố.", - "useAutomatically": "Tự động dùng {{agent}}", - "captured": "Đã thu thập {{count}} dòng nhật ký có vấn đề", "contextNotice": "Tác nhân đã chọn nhận báo cáo này, màn hình hiện tại, nhật ký ứng dụng và backend gần đây cùng thông tin chẩn đoán hệ thống.", "complete": "Hoàn thành" }, @@ -607,7 +602,6 @@ "characters": "{{count}} ký tự", "minutes": "~{{count}} phút", "chars": "{{count}} ký tự", - "exportDone": "Đã tải sách nói xuống", "exportFailed": "Xuất không thành công - kiểm tra xem chương trình phụ trợ có đang chạy không.", "global_speed": "Tốc độ", "global_speed_hint": "Tốc độ đọc cho tất cả các dòng mà không cần ghi đè tốc độ riêng", @@ -818,7 +812,6 @@ "data_dir": "Thư mục dữ liệu", "outputs": "đầu ra", "crash_log": "Nhật ký sự cố", - "update_endpoint": "Cập nhật điểm cuối", "yes": "vâng", "no": "không", "web_preview": "xem trước trang web", @@ -879,38 +872,21 @@ "title": "Thông tin xác thực", "desc": "Khóa API và mã thông báo chỉ được đặt <1>cho phiên này. Để duy trì sự kiên trì trong quá trình khởi động lại, hãy đặt chúng làm biến môi trường trong cấu hình shell của bạn.", "hf_token": "Mã thông báo ôm mặt", - "hf_help": "Cần thiết để phân loại loa và tải xuống mô hình nhanh hơn. Nhận của bạn tại ôm mặt.co/settings/tokens.", - "translate_key": "Khóa API LLM", - "translate_help": "Tùy chọn — dành cho DeepL, OpenAI hoặc nhà cung cấp dịch thuật trả phí. Không cần thiết cho Google Dịch (cấp miễn phí).", "save": "Lưu", "saved": "✓ Đặt", - "not_set": "✗ Chưa đặt", "get_token": "Nhận mã thông báo →", - "saved_session": "{{key}} đã lưu cho phiên này", "save_failed": "Không lưu được", "save_error": "Lưu không thành công: {{message}}", "deepl_key": "Khóa API DeepL", - "deepl_help": "Đối với công cụ dịch DeepL. Nhận chìa khóa của bạn tại deepl.com/pro-api.", "deepl_base_url": "URL cơ sở DeepL", "deepl_base_url_help": "Điểm cuối API DeepL tùy chỉnh (mặc định: https://api.deepl.com/v2). Thay đổi cho proxy/tự lưu trữ.", "microsoft_key": "Khóa dịch của Microsoft", - "microsoft_help": "Đối với công cụ Microsoft Translator. Nhận khóa của bạn từ Azure Portal.", "microsoft_base_url": "URL cơ sở của Microsoft Translator", "microsoft_base_url_help": "Điểm cuối Azure Translator tùy chỉnh (mặc định: https://api.cognitive.microsofttranslator.com).", - "llm_base_url": "URL cơ sở LLM", - "llm_base_url_help": "Đối với chất lượng điện ảnh hoặc công cụ LLM. Bất kỳ điểm cuối tương thích OpenAI nào (OpenAI, Ollama, LM Studio, v.v.).", - "llm_model": "Mô hình LLM", - "llm_model_help": "Tên mô hình để sử dụng với công cụ LLM (ví dụ: gpt-4o, deepseek-chat, qwen2.5).", - "group_huggingface": "ôm mặt", - "group_translate": "Dịch thuật (LLM / Điện ảnh)", - "group_deepl": "Điểm cuối tùy chỉnh DeepL", - "group_microsoft": "Điểm cuối tùy chỉnh của Microsoft Translator", "region": "Khu vực", "aws_profile": "Hồ sơ AWS" }, "engines": { - "banners_on": "Biểu ngữ sân khấu bật", - "banners_off": "Tắt biểu ngữ sân khấu", "backend": "Phần phụ trợ", "status": "Trạng thái", "ready": "sẵn sàng", @@ -941,7 +917,6 @@ "dictation_model_hint": "Dùng cho đọc chính tả trực tiếp và cho engine này khi phiên âm.", "dictation_not_installed": "Chưa cài đặt — chọn để tải xuống", "active": "hoạt động", - "whyUnavailable": "Cần những gì", "diskDetails": "Chi tiết ổ đĩa", "diskModelDownload": "Tải mô hình", "diskPackageDownload": "Tải gói", @@ -965,11 +940,8 @@ "sectionReady": "Sẵn sàng sử dụng", "sectionMore": "Thêm động cơ khác", "lastError": "Lỗi cuối cùng: {{error}}", - "installedAndReady": "Đã cài đặt và sẵn sàng", - "notInstalled": "Chưa được cài đặt", "available": "Có sẵn", "subprocessTitle": "Chạy trong quy trình con + venv của chính nó", - "inProcessTitle": "Chạy trong quy trình VoiceStudio Python", "testEngine": "Động cơ thử nghiệm", "testing": "Đang thử nghiệm…", "recheck": "Kiểm tra lại", @@ -986,7 +958,6 @@ "routingUnknown": "Không xác định", "routingEffectiveChip": "Chạy trên {{device}} trên máy này", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "Đã chọn GPU nhưng: {{reason}}", "selectCpuFallback": "{{engine}}: đang chạy trên CPU — {{reason}}", "selectWithCaveat": "Đã chuyển sang {{engine}} — {{reason}}", "generateCaveat": "{{engine}} có thể chạy chậm trên máy này — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "Không có nhật ký Tauri. Chỉ chạy trong vỏ máy tính để bàn.", "empty_backend": "Nhật ký thời gian chạy trống. Hoạt động sẽ xuất hiện ở đây khi chương trình phụ trợ ghi lại hoạt động đó.", "title": "Nhật ký", - "source_backend": "Phần phụ trợ", - "source_frontend": "Giao diện người dùng", - "source_tauri": "Tauri", "expand": "Mở rộng nhật ký", "collapse": "Thu gọn nhật ký", "expand_aria": "Mở rộng bảng nhật ký", @@ -1104,12 +1072,10 @@ "unlock": "Mở khóa", "try_voice": "Hãy thử giọng nói này", "test_phrase": "Cụm từ kiểm tra", - "test_help": "Nhập bất cứ thứ gì - chúng tôi sẽ tạo nó bằng cài đặt hiện tại của giọng nói này.", "test_placeholder": "Nhập nội dung nào đó để nghe giọng nói này nói…", "gen_preview": "Tạo bản xem trước", "generating": "Đang tạo…", "used_title": "Giọng nói này đã được sử dụng ở đâu", - "used_empty": "Giọng nói này vẫn chưa được sử dụng. Tạo bản xem trước ở trên hoặc chọn nó trong bản lồng tiếng.", "synth_clips_one": "{{count}} clip tổng hợp", "synth_clips_other": "{{count}} clip tổng hợp", "projects_count_one": "dự án {{count}}", @@ -1152,9 +1118,7 @@ "translate": "Dịch", "generate": "Tạo", "export": "Xuất khẩu", - "no_video": "Không có video nào được tải", "segments": "phân đoạn", - "speakers": "loa", "video_dubbing_studio": "Studio lồng tiếng video", "drop_here": "Thả video hoặc âm thanh vào đây", "paste_url": "…hoặc dán URL YouTube/video", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Tệp cookie xuất phải có dung lượng không quá 1 MB.", "save": "Lưu", "reset": "Đặt lại", - "save_project": "Lưu dự án", "change_file": "Thay đổi tập tin", "import_srt": "Nhập .srt", "import_srt_alt": "Thay vào đó hãy nhập .srt", @@ -1186,9 +1149,6 @@ "retranslate": "Dịch lại", "transcript": "bảng điểm", "generate_dub": "Tạo lồng tiếng", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "Tải lên và chép lời", "multi_lang": "Đa ngôn ngữ", "prep_download": "Đang tải xuống video…", @@ -1206,7 +1166,6 @@ "transcribing": "Đang phiên âm…", "elapsed": "trôi qua", "remaining": "còn lại", - "sidebar_toggle": "Chuyển đổi thanh bên", "retry_transcription": "Thử chép lại", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "Khi URL là máy chủ chứa phụ đề (YouTube, Vimeo, TED…), hãy lấy cả phụ đề gốc và mọi bản dịch tự động của YouTube. Tạo trình soạn thảo mà không cần chạy Whisper; bỏ qua Dịch tất cả đối với các ngôn ngữ mà YouTube đã hỗ trợ.", @@ -1249,7 +1208,6 @@ "engine_label": "Động cơ", "install_engine": "Cài đặt công cụ này", "installing_engine": "…cài đặt", - "needs_dev_install": "cần cài đặt dev", "install_disabled_title": "Cài đặt bị vô hiệu hóa trong các bản dựng đóng gói", "needs_install_suffix": "- cần cài đặt", "glossary_title": "Ghim bản dịch cho các thuật ngữ định kỳ (tên, từ thương hiệu, biệt ngữ)", @@ -1283,7 +1241,6 @@ "original_track": "Bản gốc", "selected_dub": "{{code}} (Lồng tiếng được chọn)", "dub_track": "{{code}} (Dub)", - "export_tracks": "Xuất bản nhạc:", "stopping": "Đang dừng…", "stop_progress": "Dừng lại ({{current}}/{{total}})", "regen_changed": "Regen {{count}} đã thay đổi", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} đã cài đặt", "install_failed": "Cài đặt không thành công: {{message}}", "prep_elapsed": "{{time}} đã trôi qua", - "add_language": "Thêm ngôn ngữ", "search_languages": "Tìm kiếm ngôn ngữ…", "languages_selected_one": "{{count}} ngôn ngữ đã chọn", "languages_selected_other": "{{count}} ngôn ngữ đã chọn", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "thời gian", - "spkr": "Spkr", "text": "văn bản", "lang": "Lang", "voice": "Giọng nói", @@ -1403,8 +1358,6 @@ "select_all_title": "Chọn tất cả đã lọc", "sel_count": "{{count}} sel", "select_title": "Chọn phân đoạn (shift+click cho phạm vi)", - "sync_title": "Âm thanh được tạo có thời lượng {{pct}}% của bản gốc", - "sync_label": "Đồng bộ hóa: {{pct}}%", "rate_title": "Tốc độ nói phù hợp: {{ratio}}× so với vị trí{{error}}", "speaker_id": "ID người nói", "translate_error_title": "Lỗi dịch thuật: {{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "Khôi phục văn bản gốc", "lang_default": "(Def)", "voice_default": "Mặc định", - "from_video": "Từ Video", - "clone_profiles": "Hồ sơ nhân bản", - "design_presets": "Cài đặt trước thiết kế", "preview_title": "Xem trước trực tiếp", "set_direction": "Đặt hướng…", "edit_direction": "Chỉnh sửa hướng…", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "Tính cách", - "pick_personality": "Chọn cài đặt trước về tính cách…", "instruct": "Hướng dẫn", "reference_text": "Văn bản tham khảo", "language": "Ngôn ngữ", @@ -1535,9 +1484,7 @@ "stories": "Truyện", "untitled_story": "Truyện không tên", "story_lines": "dòng {{count}}", - "story_lines_plural": "{{count}} dòng", "story_voices": "{{count}} giọng nói", - "story_voices_plural": "{{count}} giọng nói", "audiobooks": "Sách nói", "audiobook": "Sách nói", "story": "Câu chuyện", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio Thư viện ảnh", - "search_placeholder": "Tìm kiếm YouTube…", - "all_voices": "Tất cả các giọng nói ({{count}})", - "no_voices": "Chưa có tiếng nói nào", "download_hint": "Tìm kiếm ở trên để tải clip", - "search_btn": "Tìm kiếm", - "download_btn": "Tải xuống", "saving": "Đang lưu…", "trim": "Cắt tỉa", - "delete_confirm": "Xóa đoạn thoại này?", - "view_mode_list": "Danh sách", - "view_mode_grid": "Lưới", - "youtube_results": "Kết quả YouTube ({{count}})", - "clone_profile": "Sao chép hồ sơ", - "crop_audio": "Cắt âm thanh", "subtitle": "Hàng trăm giọng nói được thiết kế sẵn — hãy chọn một giọng nói và sử dụng.", "zone_archetypes": "nguyên mẫu", "zone_imports": "Nhập khẩu của tôi", @@ -1658,8 +1594,6 @@ "delete": "Xóa", "community_empty": "Chưa tải được giọng nói cộng đồng nào — hãy kết nối internet rồi mở lại, hoặc trở thành người đầu tiên gửi một giọng nói.", "community_explainer": "Các cài đặt trước được thiết kế và giọng nói ghi âm do cộng đồng chia sẻ, được tải từ omnivoice-gallery.", - "no_designer": "Giọng nói ghi âm — hãy dùng \"Sử dụng giọng nói\" thay thế.", - "no_preview": "Không có bản xem trước — thêm bằng \"Sử dụng giọng nói\" để nghe thử.", "submit_failed": "Không thể mở biểu mẫu gửi.", "submit_preset": "Gửi cài đặt trước", "submit_voice": "Gửi giọng nói", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "Bạn đã sẵn sàng tạo giọng nói đầu tiên. Có thể thiết lập nhập liệu bằng giọng nói ngay hoặc sau này trong Cài đặt.", - "welcome": "Chào mừng", "system_check": "Kiểm tra hệ thống", "install_models": "Cài đặt mô hình", - "pick_engines": "Chọn động cơ", "system_check_desc": "Thăm dò RAM, đĩa, GPU, ffmpeg và mạng. Trình chặn được gắn cờ trước để bạn biết trước khi tải xuống.", "install_models_desc": "Tải xuống ~2,4 GB trọng số — chỉ bắt buộc mô hình TTS. Thêm các mô hình phiên âm được chọn cho phần cứng của bạn ngay bây giờ hoặc sau này trong Cài đặt.", "pick_engines_desc": "Chọn phụ trợ TTS / ASR / LLM. Các cài đặt mặc định hoạt động ngay lập tức — tùy chỉnh bất cứ lúc nào trong Cài đặt.", "hero_desc": "Lồng tiếng, sao chép giọng nói và thiết kế giọng nói — tất cả đều chạy cục bộ trên máy của bạn.", - "first_run": "Lần chạy đầu tiên mất 5–10 phút để tải xuống. Sau đó, mọi lần khởi chạy đều diễn ra ngay lập tức và hoàn toàn ngoại tuyến.", - "get_started": "Bắt đầu", "back": "Quay lại", "continue_ok": "Tất cả đều tốt - tiếp tục", "continue_warn": "Tiếp tục (có cảnh báo)", @@ -1727,7 +1657,6 @@ "try_dictation": "Thử đọc chính tả", "try_dictation_desc": "Nhấn phím tắt của bạn một lần để chắc chắn tính năng đọc chính tả hoạt động trước khi hoàn tất thiết lập.", "dictation_step_label": "Đọc chính tả", - "next_try_dictation": "Tiếp theo: Thử đọc chính tả", "step_aria": "Bước {{num}}: {{label}}", "step_completed": "đã hoàn thành", "cache_label": "Bộ đệm mô hình" @@ -1743,7 +1672,6 @@ "platforms": "Nền tảng", "footer": "Mọi đóng góp đều giúp mở rộng ranh giới của AI địa phương.", "back": "Quay lại Studio", - "commercial": "Giấy phép thương mại", "pill": "Hỗ trợ VoiceStudio", "suggested_title": "Chọn một số tiền", "most_common": "phổ biến nhất", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "Hỗ trợ phát triển VoiceStudio", - "lead_default": "Vui mừng vì nó đã hiệu quả! VoiceStudio là miễn phí và hoàn toàn cục bộ. Nếu việc này giúp bạn tiết kiệm thời gian, một khoản chip-in nhỏ hàng tháng sẽ tài trợ cho Claude Max đằng sau nó.", - "lead_first_clone": "Bản sao giọng nói đầu tiên của bạn đã hoàn tất - thật tuyệt! VoiceStudio chạy hoàn toàn trên máy của bạn và bộ phận hỗ trợ của bạn sẽ duy trì hoạt động đó.", - "lead_tenth_dub": "Mười bản lồng tiếng - rõ ràng bạn đang thực hiện nó. Một khoản chip-in nhỏ hàng tháng sẽ tài trợ cho Claude Max cung cấp các tính năng này.", - "lead_sustained": "Bạn đã sử dụng VoiceStudio được một tháng. Nếu nó giành được một vị trí trong quy trình làm việc của bạn, hãy cân nhắc việc hỗ trợ tài trợ cho những gì tiếp theo.", - "chip_in": "Chip vào", - "later": "Có lẽ sau này", - "star": "Sao trên GitHub", - "opt_out": "Đừng hỏi lại", - "dismiss_aria": "Loại bỏ" + "later": "Có lẽ sau này" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "Giấy phép thương mại", "hero_title": "Vận chuyển giọng nói AI trong quá trình sản xuất", "hero_desc": "VoiceStudio là phần mềm tự do, mã nguồn mở theo giấy phép GNU Affero General Public License v3 (AGPL-3.0) — miễn phí sử dụng, bao gồm cả mục đích thương mại và sử dụng nội bộ doanh nghiệp. Bạn chỉ cần giấy phép thương mại nếu muốn nhúng VoiceStudio vào sản phẩm hoặc dịch vụ mã nguồn đóng hay độc quyền mà không chịu các nghĩa vụ copyleft của AGPL-3.0.", - "why_title": "Tại sao doanh nghiệp chọn VoiceStudio", - "pricing_title": "Định giá", - "faq_title": "Câu hỏi thường gặp", "pricing_desc": "Các cấp độ và giá cả vẫn đang được hoàn thiện.", - "pricing_detail": "Cho đến khi chúng được công khai, mọi hoạt động triển khai thương mại đều được báo giá riêng lẻ để chúng tôi có thể điều chỉnh quy mô phù hợp với nhóm và khối lượng công việc của bạn.", "request_quote": "Yêu cầu báo giá", - "footer_email": "Câu hỏi? Hãy liên hệ tại hi@voicestudio.sh", - "footer_discord": "Tham gia Discord của chúng tôi để được hỗ trợ cộng đồng.", "benefit_ip": "Quyền sở hữu IP đầy đủ", "benefit_ip_desc": "Bạn sở hữu mọi đầu ra, mọi mô hình, mọi công cụ phái sinh. Không có thuê chung, không có rò rỉ dữ liệu.", "benefit_cost": "Không có chi phí mỗi phút", "benefit_cost_desc": "Không có hóa đơn API, không có tình trạng dư thừa bất ngờ. Chạy trên phần cứng của riêng bạn ngay từ ngày đầu tiên.", - "benefit_team": "Quyền truy cập toàn nhóm", "benefit_team_desc": "Một giấy phép bao gồm số lượng người dùng không giới hạn trong tổ chức của bạn.", "benefit_support": "Hỗ trợ trực tiếp", "benefit_support_desc": "Hỗ trợ qua email và cuộc gọi video ưu tiên từ nhóm kỹ thuật cốt lõi.", - "benefit_source": "Lõi có sẵn nguồn", "benefit_source_desc": "Hiển thị đầy đủ vào ngăn xếp. Kiểm tra, phân nhánh và điều chỉnh theo các điều khoản cấp phép.", - "benefit_lang": "646 ngôn ngữ", "benefit_lang_desc": "Phiên âm, dịch và lồng tiếng trên 646 ngôn ngữ với chất lượng ngang tầm con người.", - "hero_note": "Sử dụng, tự lưu trữ và dùng cho mục đích thương mại đều miễn phí theo AGPL-3.0 — kể cả ở quy mô lớn. AGPL là giấy phép copyleft qua mạng: nếu bạn chỉnh sửa VoiceStudio và cung cấp phiên bản chỉnh sửa đó cho người khác qua mạng, bạn phải chia sẻ mã nguồn đã chỉnh sửa theo cùng điều khoản. Giấy phép thương mại sẽ gỡ bỏ các nghĩa vụ copyleft này cho các triển khai độc quyền, mã nguồn đóng. Các gói giá sẽ sớm ra mắt — trong lúc đó hãy liên hệ với chúng tôi.", - "hero_simple": "VoiceStudio là nguồn mở và miễn phí theo AGPL-3.0 — bao gồm cả mục đích sử dụng cho mục đích thương mại và nội bộ doanh nghiệp. Bạn chỉ cần giấy phép thương mại để nhúng nó vào sản phẩm nguồn đóng mà không có nghĩa vụ về bản quyền của AGPL.", - "contact_lead": "Giá được báo cho mỗi lần triển khai để phù hợp với nhóm và khối lượng công việc của bạn. Hãy cho tôi biết trường hợp sử dụng của bạn và tôi sẽ nhận được báo giá cho bạn." + "hero_simple": "VoiceStudio là nguồn mở và miễn phí theo AGPL-3.0 — bao gồm cả mục đích sử dụng cho mục đích thương mại và nội bộ doanh nghiệp. Bạn chỉ cần giấy phép thương mại để nhúng nó vào sản phẩm nguồn đóng mà không có nghĩa vụ về bản quyền của AGPL." }, "exportModal": { "export": "Xuất khẩu", @@ -2006,7 +1915,6 @@ "loaded_ready": "Đã tải và sẵn sàng", "loading_first_run": "Đang tải… (việc này có thể mất 1-2 phút trong lần chạy đầu tiên)", "failed_to_load": "Không tải được", - "not_loaded_yet": "Chưa được tải - sẽ tải vào lần phiên âm đầu tiên", "tts_not_loaded_yet": "Chưa tải — sẽ tải khi tạo giọng nói lần đầu", "error_check_logs": "Lỗi: {{error}}. Kiểm tra nhật ký và thử khởi động lại.", "check_logs_restart": "Kiểm tra nhật ký để tìm lỗi tải mô hình. Hãy thử khởi động lại.", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "Đúc loa", - "auto_assign_title": "Tự động gán giọng nói từ bản sao loa được trích xuất", - "auto_cast": "Tự động truyền", - "all_cast": "Tất cả diễn viên", - "segments_count": "{{count}} phân đoạn", - "assign_voice": "Chỉ định giọng nói…", - "from_video": "🎤 Từ video ({{name}})", - "no_profiles": "Chưa có cấu hình giọng nói nào được lưu.", - "preview_voice": "Xem trước giọng nói" + "segments_count": "{{count}} phân đoạn" }, "checkpoint": { "timing_review": "{{count}} đoạn cần kiểm tra thời gian", @@ -2203,8 +2104,6 @@ "play_failed": "Chơi không thành công: {{message}}" }, "models": { - "hf_token_set_toast": "Bộ mã thông báo HuggingFace - cho phép tải xuống nhanh hơn", - "hf_token_save_failed": "Không lưu được mã thông báo", "install_started": "Đã bắt đầu cài đặt - tiến trình trong hàng", "delete_confirm": "Xóa {{repoId}}? Bạn có thể cài đặt lại nó sau.", "delete_confirm_title": "Xóa mô hình", @@ -2247,8 +2146,6 @@ "not_installed": "chưa được cài đặt", "required_tag": "bắt buộc", "delete_btn": "Xóa", - "hf_token_btn": "Mã thông báo HF", - "hf_set_title": "Đặt mã thông báo HuggingFace để tải xuống nhanh hơn", "get_token": "Nhận mã thông báo →", "reco_installed_for": "Gói được đề xuất đã cài đặt cho {{device}}", "reco_for": "Được đề xuất cho {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "Bắt buộc ~{{size}} GB", "all_size": "Tất cả ~{{size}} GB", "req_tag": "yêu cầu", - "search_placeholder": "Tìm kiếm mô hình…", - "search_label": "Tìm kiếm mô hình", "no_matches": "Không có mô hình nào phù hợp với bộ lọc của bạn.", "sort_by": "Sắp xếp theo {{column}}", "column_model": "người mẫu", @@ -2267,12 +2162,8 @@ "ready_badge": "Sẵn sàng", "loading_badge": "Đang tải…", "idle_badge": "Nhàn rỗi", - "started_downloading_required_one": "Đã bắt đầu tải xuống {{count}} mô hình bắt buộc", - "started_downloading_required_other": "Đã bắt đầu tải xuống {{count}} mô hình bắt buộc", "to_download": "để tải xuống", "cached": "được lưu vào bộ nhớ đệm", - "fast_download_badge": "tải xuống nhanh", - "fast_download_title": "Tải xuống nhanh qua Xet {{version}} — truyền tải song song", "voice_previews": "Bản nghe thử giọng", "voice_previews_desc": "Tải các bản nghe thử đã dựng sẵn để giọng phát ngay lập tức — ngay cả trước khi mô hình giọng nói tải xong. Không có gì được tải về cho đến khi bạn bật tùy chọn này.", "voice_previews_off": "Tắt — bản nghe thử được dựng trên máy này", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "Tôi có cần giấy phép cho các công cụ nội bộ không?", - "a_internal_tools": "Không. Việc sử dụng bởi nhân viên và nhà thầu của bạn — bao gồm chỉnh sửa và tự lưu trữ nội bộ — là miễn phí theo AGPL-3.0. Giấy phép thương mại chỉ cần thiết nếu bạn nhúng VoiceStudio vào sản phẩm hoặc dịch vụ mã nguồn đóng hay độc quyền và không muốn tuân thủ nghĩa vụ chia sẻ mã nguồn của AGPL.", - "q_try_before": "Tôi có thể thử trước khi cam kết không?", - "a_try_before": "Có. Toàn bộ ứng dụng có thể tải xuống, chạy và tự lưu trữ miễn phí theo AGPL-3.0 — không cần bất kỳ thỏa thuận nào. Khi bạn sẵn sàng trao đổi về giấy phép thương mại (sử dụng độc quyền), hãy gửi email cho chúng tôi và chúng ta sẽ cùng thống nhất chi tiết.", - "q_watermark": "Còn hình mờ thì sao?", - "a_watermark": "Hình mờ vô hình AudioSeal được nhúng theo mặc định cho mọi người. Bạn có thể tắt trong Cài đặt → Quyền riêng tư; nó chỉ ảnh hưởng đến âm thanh được tạo sau khi thay đổi." - }, "gallery_extra": { - "save_prompt": "Nhập tên cho cấu hình giọng nói này:", - "saved_as_profile": "Giọng nói được lưu dưới dạng hồ sơ!", - "save_profile_failed": "Không lưu được hồ sơ", - "download_failed": "Tải xuống không thành công: {{message}}", - "trim_load_failed": "Không thể tải âm thanh để cắt xén: {{message}}", - "upload_crop_failed": "Không thể tải lên giọng nói đã bị cắt: {{message}}" + "trim_load_failed": "Không thể tải âm thanh để cắt xén: {{message}}" }, "dub_workflow": { "preparing_audio": "Đang chuẩn bị âm thanh…", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "Đang tải…", - "trimmed_loaded": "Đã tải âm thanh đã cắt bớt", - "toast_exported": "Đã xuất: {{name}}", - "toast_export_failed": "Xuất không thành công: {{message}}", - "toast_open_folder_failed": "Không thể mở thư mục: {{message}}", "toast_saving": "Đang lưu {{name}}...", "toast_saved": "Đã lưu: {{path}}", "toast_save_error": "Lỗi lưu: {{message}}", "toast_processing": "Đang xử lý {{name}}...", "toast_downloaded": "Đã tải xuống {{name}}", "toast_download_error": "Lỗi tải xuống: {{message}}", - "toast_upload_first": "Vui lòng nhấp vào 'Tải lên & Phiên âm' trước để video được xử lý trên máy chủ trước khi lưu.", - "toast_save_failed": "Lưu không thành công: {{message}}", - "toast_opened": "Đã mở: {{name}}", - "toast_project_deleted": "Dự án đã bị xóa", - "toast_restored_state": "Đã khôi phục trạng thái thế hệ trước", - "toast_history_deleted": "Mục lịch sử đã bị xóa", "toast_flushed": "Đã xóa — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· dỡ mô hình", "toast_flush_failed": "Xóa không thành công: {{message}}", - "toast_project_saved": "Đã lưu dự án", - "toast_project_created": "Dự án đã được tạo" + "toast_project_saved": "Đã lưu dự án" }, "update": { "available": "Đã có bản cập nhật {{version}}", @@ -2531,13 +2398,11 @@ "not_writable": "không ghi được", "change": "Thay đổi…", "compute_title": "Tính toán", - "compute_label": "GPU / bộ tăng tốc", "compute_auto": "Tự động (NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "GPU AMD (ROCm, Linux)", "channel_label": "Kênh cập nhật", "channel_stable": "Ổn định", "channel_preview": "Xem trước (main mới nhất)", - "network_title": "Mạng", "region_label": "Khu vực tải xuống", "mirrors_title": "Mirror tùy chỉnh (nâng cao)", "mirror_pypi": "URL chỉ mục PyPI", @@ -2570,10 +2435,6 @@ "trust_line": "Quyền riêng tư là trên hết: giọng nói, bản ghi và dự án của bạn không bao giờ rời khỏi máy này — không tài khoản, không xử lý đám mây.", "resume_note": "Tải xuống bị gián đoạn sẽ tự tiếp tục — đóng ứng dụng vẫn an toàn.", "eta_left": "còn ~{{eta}}", - "first_sound_text": "Chào mừng đến với studio của bạn. Mỗi từ bạn đang nghe vừa được tạo trên chính máy này.", - "first_sound_done": "Giọng nói đó? Vừa được tạo vài giây trước, ngay trên máy. Chào mừng.", - "hf_token_title": "Token Hugging Face (tùy chọn)", - "hf_token_hint": "Mở khóa các mô hình bị giới hạn — ví dụ tách người nói cho lồng tiếng nhiều giọng (pyannote). Token tài khoản miễn phí; chỉ lưu trên máy này.", "hf_token_save": "Lưu", "hf_token_saving": "đang lưu…", "hf_token_saved": "Đã lưu token Hugging Face", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} còn lại", "hf_token_card_title": "Thêm mã thông báo Ôm Mặt miễn phí để tải xuống nhanh hơn", "hf_token_get": "Bạn không có mã thông báo? Nhận một cái miễn phí →", - "hf_token_saved_fast": "Đã lưu mã thông báo Ôm Mặt - tải xuống hiện nhanh hơn", "hf_token_inline_prompt": "Tăng tốc độ tải xuống với mã thông báo Ôm mặt miễn phí", "hf_token_inline_ph": "Dán mã thông báo hf_… (tùy chọn)", "hf_token_get_short": "Nhận một cái miễn phí →", @@ -2607,15 +2467,11 @@ "unstar_take": "Bỏ sao — cho phép dọn dẹp", "play_take": "Tải làm đầu ra hiện tại", "filter_starred": "Đã gắn sao", - "load_take_failed": "Không thể tải bản thu đó: {{message}}", "mode_clone": "Nhân bản", "mode_synth": "Tổng hợp", "generation_seconds": "{{duration}} giây", - "expand_text": "Mở rộng văn bản", - "collapse_text": "Thu gọn văn bản", "seed": "Seed {{seed}}", - "load_config": "Tải cài đặt", - "delete_confirm": "Xóa mục lịch sử này?" + "load_config": "Tải cài đặt" }, "openapi": { "title": "API VoiceStudio", @@ -3015,25 +2871,18 @@ "preview": "Xem trước nhà tài trợ", "message": "Thương hiệu, trang web và lời nhắn", "email_app": "Mở ứng dụng email", - "copy_email": "Sao chép email", - "preview_detail": "Logo, liên kết và phần giới thiệu của bạn có thể xuất hiện ở đây." + "copy_email": "Sao chép email" }, "supportPlans": { "remove": "Xóa thanh nhà tài trợ", "title": "Miễn phí và Pro", "free": "Miễn phí", "pro": "Pro", - "sponsor_bar": "Thanh nhà tài trợ", "visible": "Hiển thị", - "hideable": "Có thể ẩn", "unavailable": "Chưa cấu hình kích hoạt Pro.", "telemetry": "Dữ liệu sử dụng", - "opt_in": "Tùy chọn, cần đồng ý", "disabled": "Tắt", - "badge": "Huy hiệu Pro", - "advanced": "Công cụ nâng cao", - "standard": "Tiêu chuẩn", - "included": "Bao gồm" + "badge": "Huy hiệu Pro" }, "integrationCatalog": { "title": "Tích hợp", @@ -3042,8 +2891,7 @@ "n8nHint": "Nhập quy trình này vào n8n, chỉnh sửa yêu cầu rồi chạy. Giữ VoiceStudio hoạt động. Docker và n8n từ xa cần địa chỉ máy chủ có thể truy cập và thông tin xác thực cần thiết." }, "directoryExamples": { - "example": "Ví dụ trong danh mục", - "notice": "Chỉ là ví dụ trong danh mục, không phải nhà tài trợ hay tích hợp đã kết nối." + "example": "Ví dụ trong danh mục" }, "workflowRun": { "run": "Chạy quy trình", diff --git a/electron/src/shared/i18n/locales/zh-CN.json b/electron/src/shared/i18n/locales/zh-CN.json index 38b145142..6c873907c 100644 --- a/electron/src/shared/i18n/locales/zh-CN.json +++ b/electron/src/shared/i18n/locales/zh-CN.json @@ -59,15 +59,10 @@ "title": "询问 VoiceStudio Agent", "notInstalled": "未安装", "noSource": "请打开可写的 VoiceStudio 源码检出目录,让智能体进行修复。", - "noneFound": "安装 Codex、Claude Code、OpenCode 或 Pi 以使用本地修复。", "ready": "就绪", "placeholder": "出了什么问题?请添加操作步骤、可见错误或预期行为。", - "prReady": "修复已完成。请检查更改,并在目标测试通过后提交 PR。", "diagnose": "诊断", "fix": "修复", - "chooseDefault": "选择此页面崩溃时 VoiceStudio 自动使用的智能体。", - "useAutomatically": "自动使用 {{agent}}", - "captured": "已捕获 {{count}} 行问题日志", "contextNotice": "所选代理将收到此报告、当前界面、最近的应用与后端日志以及系统诊断信息。", "complete": "已完成" }, @@ -208,7 +203,6 @@ "characters": "{{count}} 个角色", "minutes": "约 {{count}} 分钟", "chars": "{{count}} 字符", - "exportDone": "有声书已下载", "exportFailed": "导出失败 — 请检查后端是否运行。", "global_speed": "速度", "global_speed_hint": "所有台词的朗读速度(未单独设置语速的台词)", @@ -999,7 +993,6 @@ "data_dir": "数据目录", "outputs": "输出目录", "crash_log": "崩溃日志", - "update_endpoint": "更新地址", "yes": "是", "no": "否", "web_preview": "网页预览", @@ -1070,39 +1063,22 @@ "title": "凭证", "desc": "API 密钥和令牌<1>仅在此会话中有效。如需跨重启持久化,请在 shell 配置文件中设置环境变量。", "hf_token": "HuggingFace 令牌", - "hf_help": "用于说话人分离和更快的模型下载。在 huggingface.co/settings/tokens 获取。", - "translate_key": "LLM API 密钥", - "translate_help": "可选 — 用于 DeepL、OpenAI 或付费翻译提供商。Google 翻译(免费版)不需要。", "save": "保存", "saved": "✓ 已设置", - "not_set": "✗ 未设置", "get_token": "获取令牌 →", - "saved_session": "{{key}} 已为此会话保存", "saved_persisted": "{{key}} 已保存", "save_failed": "保存失败", "save_error": "保存失败:{{message}}", "deepl_key": "DeepL API 密钥", - "deepl_help": "用于 DeepL 翻译引擎。在 deepl.com/pro-api 获取。", "deepl_base_url": "DeepL 自定义地址", "deepl_base_url_help": "自定义 DeepL API 端点(默认 https://api.deepl.com/v2)。代理或自托管时修改。", "microsoft_key": "Microsoft Translator 密钥", - "microsoft_help": "用于 Microsoft Translator 引擎。从 Azure 门户获取。", "microsoft_base_url": "Microsoft Translator 自定义地址", "microsoft_base_url_help": "自定义 Azure Translator 端点(默认 https://api.cognitive.microsofttranslator.com)。", - "llm_base_url": "LLM 基础地址", - "llm_base_url_help": "用于精译质量或 LLM 引擎。任何 OpenAI 兼容的端点(OpenAI、Ollama、LM Studio 等)。", - "llm_model": "LLM 模型名称", - "llm_model_help": "LLM 引擎使用的模型名称(如 gpt-4o、deepseek-chat、qwen2.5)。", - "group_huggingface": "HuggingFace", - "group_translate": "翻译引擎(LLM / 精译)", - "group_deepl": "DeepL 自定义地址", - "group_microsoft": "Microsoft Translator 自定义地址", "region": "区域", "aws_profile": "AWS 配置" }, "engines": { - "banners_on": "阶段横幅开启", - "banners_off": "阶段横幅关闭", "backend": "后端", "status": "状态", "ready": "就绪", @@ -1133,7 +1109,6 @@ "dictation_model_hint": "用于实时听写,以及此引擎进行转写时。", "dictation_not_installed": "未安装 — 选择以下载", "active": "当前", - "whyUnavailable": "所需条件", "diskDetails": "磁盘详情", "diskModelDownload": "模型下载", "diskPackageDownload": "软件包下载", @@ -1157,11 +1132,8 @@ "sectionReady": "即可使用", "sectionMore": "添加更多引擎", "lastError": "最后一个错误:{{error}}", - "installedAndReady": "安装并准备就绪", - "notInstalled": "未安装", "available": "可用", "subprocessTitle": "在自己的子进程+venv中运行", - "inProcessTitle": "在 VoiceStudio Python 进程中运行", "testEngine": "测试引擎", "testing": "测试...", "recheck": "重新检查", @@ -1188,7 +1160,6 @@ "routingUnknown": "未知", "routingEffectiveChip": "在本机的 {{device}} 上运行", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "已选择 GPU,但是:{{reason}}", "curatedModelLabel": "模型", "curatedModelAria": "{{engine}} 的模型", "cloneCapable": "语音克隆", @@ -1205,8 +1176,6 @@ "cpuLongTextTuned": "{{engine}} 在本机上使用 CPU 运行,而这段文本较长——生成可能超出时间预算。缩短文本,或分成几次生成会快得多。", "familyMatrixTitle": "{{family}} 引擎", "colEngine": "引擎", - "colGpuCompat": "GPU 兼容", - "colIsolation": "隔离", "colActions": "操作", "familyDesc_tts": "将脚本转换为语音。标记为启用的引擎供工作室、配音和批处理使用。", "familyDesc_asr": "将音频转换为文本,用于转录、配音、字幕和听写。", @@ -1271,9 +1240,6 @@ "empty_tauri": "无 Tauri 日志可用。仅在桌面 Shell 中运行。", "empty_backend": "运行时日志为空。后端记录活动后将显示在此处。", "title": "日志", - "source_backend": "后端", - "source_frontend": "前端", - "source_tauri": "Tauri", "expand": "展开日志", "collapse": "折叠日志", "expand_aria": "展开日志面板", @@ -1347,12 +1313,10 @@ "unlock": "解锁", "try_voice": "试试这个声音", "test_phrase": "测试短语", - "test_help": "输入任何内容 — 我们将用此声音的当前设置生成它。", "test_placeholder": "输入内容以听到此声音说话…", "gen_preview": "生成预览", "generating": "生成中…", "used_title": "此声音的使用位置", - "used_empty": "此声音尚未使用。在上方生成预览,或在配音中选择它。", "synth_clips_one": "{{count}} 个合成片段", "synth_clips_other": "{{count}} 个合成片段", "projects_count_one": "{{count}} 个项目", @@ -1395,9 +1359,7 @@ "translate": "翻译", "generate": "生成", "export": "导出", - "no_video": "未加载视频", "segments": "片段", - "speakers": "说话人", "video_dubbing_studio": "视频配音工作室", "drop_here": "拖放视频或音频文件", "paste_url": "…或粘贴 YouTube / 视频链接", @@ -1411,7 +1373,6 @@ "cookie_size_error": "Cookie 导出文件必须小于或等于 1 MB。", "save": "保存", "reset": "重置", - "save_project": "保存项目", "change_file": "更换文件", "import_srt": "导入 SRT", "import_srt_alt": "改为导入 SRT", @@ -1429,9 +1390,6 @@ "retranslate": "重新翻译", "transcript": "转录文本", "generate_dub": "生成配音", - "export_mp4": "MP4", - "export_wav": "WAV", - "export_srt": "SRT", "upload_transcribe": "上传并转录", "multi_lang": "多语言", "prep_download": "正在下载视频…", @@ -1449,7 +1407,6 @@ "transcribing": "正在转录音频…", "elapsed": "已用", "remaining": "秒剩余", - "sidebar_toggle": "切换侧边栏", "retry_transcription": "重新转录", "supported_formats": "MP4 · MOV · MKV · WEBM · MP3 · WAV · FLAC · M4A", "pull_captions_title": "当网址支持字幕时(YouTube、Vimeo、TED 等),也会拉取原字幕和 YouTube 自动翻译。无需运行 Whisper 即可编辑;对于 YouTube 已覆盖的语言,可跳过“全部翻译”。", @@ -1495,7 +1452,6 @@ "engine_label": "引擎", "install_engine": "安装此引擎", "installing_engine": "…安装中", - "needs_dev_install": "需要开发者安装", "install_disabled_title": "打包版本中禁用安装", "needs_install_suffix": " — 需要安装", "glossary_title": "为重复出现的术语固定翻译(名称、品牌词、术语)", @@ -1532,7 +1488,6 @@ "original_track": "原声", "selected_dub": "{{code}}(选定配音)", "dub_track": "{{code}}(配音)", - "export_tracks": "导出轨道:", "stopping": "停止中…", "stop_progress": "停止({{current}}/{{total}})", "regen_changed": "重新生成 {{count}} 个更改", @@ -1544,7 +1499,6 @@ "install_ok": "{{engine}} 已安装", "install_failed": "安装失败:{{message}}", "prep_elapsed": "已用 {{time}}", - "add_language": "添加语言", "search_languages": "搜索语言...", "languages_selected_one": "已选择 {{count}} 语言", "languages_selected_other": "已选择 {{count}} 种语言", @@ -1563,7 +1517,6 @@ "advanced": "高级", "cinematic_needs_llm_hint": "精译需要 LLM。请在 设置 → 凭证 → LLM 端点 中配置(Ollama 在本地运行,无需密钥)。", "hq_needs_llm_hint": "高质量翻译会借助本地或云端 LLM 让每句台词贴合片段时长。配置一个即可启用。", - "set_up_llm": "去配置", "generate_dub_multi": "生成 {{count}} 个配音", "pipeline": "配音流程", "preview_language": "预览语言", @@ -1642,7 +1595,6 @@ }, "segment": { "time": "时间", - "spkr": "说话人", "text": "文本", "lang": "语种", "voice": "音色", @@ -1653,8 +1605,6 @@ "select_all_title": "全选当前筛选", "sel_count": "已选 {{count}} 条", "select_title": "选择片段(按住 Shift 可多选范围)", - "sync_title": "生成的音频时长是原版的 {{pct}}%", - "sync_label": "同步:{{pct}}%", "rate_title": "语速适配:{{ratio}}×(相对于插槽){{error}}", "speaker_id": "说话人 ID", "translate_error_title": "翻译错误:{{error}}", @@ -1665,9 +1615,6 @@ "restore_title": "恢复原文", "lang_default": "(默认)", "voice_default": "默认", - "from_video": "来自视频", - "clone_profiles": "克隆配置", - "design_presets": "设计预设", "preview_title": "实时预览", "set_direction": "设置方向…", "edit_direction": "编辑方向…", @@ -1724,7 +1671,6 @@ }, "voice": { "personality": "风格", - "pick_personality": "选择一个风格预设…", "instruct": "指令", "reference_text": "参考文本", "language": "语言", @@ -1785,9 +1731,7 @@ "stories": "故事", "untitled_story": "无题故事", "story_lines": "{{count}} 行", - "story_lines_plural": "{{count}} 行", "story_voices": "{{count}} 声音", - "story_voices_plural": "{{count}} 声音", "audiobooks": "有声读物", "audiobook": "有声读物", "story": "故事", @@ -1859,20 +1803,9 @@ }, "gallery": { "title": "VoiceStudio 音色库", - "search_placeholder": "搜索 YouTube…", - "all_voices": "全部声音({{count}})", - "no_voices": "暂无声音", "download_hint": "在上方搜索以下载片段", - "search_btn": "搜索", - "download_btn": "下载", "saving": "保存中…", "trim": "裁剪", - "delete_confirm": "删除此声音片段?", - "view_mode_list": "列表", - "view_mode_grid": "网格", - "youtube_results": "YouTube 搜索结果({{count}})", - "clone_profile": "克隆配置", - "crop_audio": "裁剪音频", "subtitle": "数百个现成的设计语音——选一个即可使用。", "zone_archetypes": "原型", "zone_imports": "我的导入", @@ -1909,8 +1842,6 @@ "delete": "删除", "community_empty": "尚未加载社区声音——请联网后重新打开,或成为第一位提交者。", "community_explainer": "由社区分享的设计预设与录制声音,加载自 omnivoice-gallery。", - "no_designer": "录制声音——请改用“使用此声音”。", - "no_preview": "暂无预览——通过“使用此声音”添加后即可收听。", "submit_failed": "无法打开提交表单。", "submit_preset": "提交预设", "submit_voice": "提交声音", @@ -1948,24 +1879,18 @@ }, "setup": { "ready_desc": "现在可以创建你的第一个声音了。你可以立即设置语音输入,也可以稍后在设置中完成。", - "welcome": "欢迎", "system_check": "系统检查", "install_models": "安装模型", - "pick_engines": "选择引擎", "system_check_desc": "检查内存、磁盘、GPU、ffmpeg 和网络。提前发现阻塞项。", "media_engine_preparing": "正在准备媒体引擎…", "media_engine_failed_title": "媒体引擎下载失败", "media_engine_failed_desc": "应用无法获取自带的音视频引擎(FFmpeg)。请重试,或指向这台电脑上已有的副本。", "media_engine_retry": "重试", "media_engine_use_system": "使用系统副本", - "media_engine_detect_failed": "未找到系统副本——请重试下载或手动选择文件。", "media_engine_choose_file": "选择文件…", - "media_engine_ready": "媒体引擎已配置。", "install_models_desc": "下载约 2.4 GB 模型权重 — 仅 TTS 模型为必需。可现在安装为您的硬件精选的转录模型,也可稍后在设置中添加。", "pick_engines_desc": "选择 TTS/ASR/LLM 后端。默认配置开箱即用 — 随时在设置中自定义。", "hero_desc": "配音、声音克隆和声音设计 — 全部在本地运行。", - "first_run": "首次运行需要 5-10 分钟下载。之后每次启动都是即时且完全离线的。", - "get_started": "开始使用", "back": "返回", "continue_ok": "一切正常 — 继续", "continue_warn": "继续(有警告)", @@ -1986,7 +1911,6 @@ "try_dictation": "试用听写", "try_dictation_desc": "完成设置前,按一次快捷键确认听写功能可以正常使用。", "dictation_step_label": "听写", - "next_try_dictation": "下一步:试用听写", "step_aria": "第 {{num}} 步:{{label}}", "step_completed": "已完成", "cache_label": "模型缓存", @@ -2006,7 +1930,6 @@ "platforms": "捐赠平台", "footer": "每一份贡献都在推动本地 AI 的边界。", "back": "返回工作室", - "commercial": "商业许可", "pill": "支持VoiceStudio", "suggested_title": "选择金额", "most_common": "最常见金额", @@ -2024,15 +1947,7 @@ }, "postcard": { "title": "支持 VoiceStudio 开发", - "lead_default": "很高兴它对你有用!VoiceStudio 免费且完全本地运行。如果它能节省你的时间,每月一小笔赞助就能支撑其背后的 Claude Max。", - "lead_first_clone": "你的第一次声音克隆已完成——太好了!VoiceStudio 完全在你的电脑上运行,你的支持让它保持如此。", - "lead_tenth_dub": "十次配音——你显然正在让它发挥作用。每月一小笔赞助为这些功能背后的 Claude Max 提供资金。", - "lead_sustained": "你使用 VoiceStudio 已满一个月。如果它已在你的工作流中占有一席之地,请考虑资助下一步的工作。", - "chip_in": "赞助", - "later": "以后再说", - "star": "在 GitHub 上加星标", - "opt_out": "不要再问", - "dismiss_aria": "关闭" + "later": "以后再说" } }, "enterprise": { @@ -2042,29 +1957,18 @@ "badge": "商业许可", "hero_title": "在生产环境中部署 AI 语音", "hero_desc": "VoiceStudio 是基于 GNU Affero 通用公共许可证第 3 版(AGPL-3.0)的自由开源软件——可免费使用,包括商业用途和企业内部用途。只有当您希望在不承担 AGPL-3.0 著佐权(copyleft)义务的情况下,将 VoiceStudio 嵌入闭源或专有产品/服务时,才需要商业许可证。", - "why_title": "为什么企业选择 VoiceStudio", - "pricing_title": "定价", - "faq_title": "常见问题", "pricing_desc": "定价层级仍在确定中。", - "pricing_detail": "在公开之前,每个商业部署都将单独报价,以便为您的团队和工作负载量身定制。", "request_quote": "请求报价", - "footer_email": "有问题?请联系 hi@voicestudio.sh", - "footer_discord": "加入我们的 Discord 获取社区支持。", "benefit_ip": "完全 IP 所有权", "benefit_ip_desc": "您拥有所有输出、模型和衍生作品。无共享租户,无数据泄露。", "benefit_cost": "零按分钟计费", "benefit_cost_desc": "无 API 账单,无意外超额。从第一天起就在自己的硬件上运行。", - "benefit_team": "团队级访问", "benefit_team_desc": "一个许可证涵盖您组织内的无限用户。", "benefit_support": "直接支持", "benefit_support_desc": "核心工程团队提供优先邮件和视频通话支持。", - "benefit_source": "源码可用", "benefit_source_desc": "完全可见的技术栈。在许可条款内审计、分叉和适配。", - "benefit_lang": "646 种语言", "benefit_lang_desc": "以人类级质量转录、翻译和配音 646 种语言。", - "hero_note": "使用、自托管和商业用途在 AGPL-3.0 下均免费——包括大规模使用。AGPL 是网络著佐权许可证:如果您修改 VoiceStudio 并通过网络向他人提供该修改版本,则必须以相同条款公开您修改后的源代码。商业许可证可为专有闭源部署免除这些著佐权义务。定价方案即将推出——在此期间欢迎与我们联系。", - "hero_simple": "VoiceStudio 在 AGPL-3.0 下是免费和开源的 — 包括商业和内部业务用途。您只需要商业许可即可将其嵌入闭源产品中,无需 AGPL 的 Copyleft 义务。", - "contact_lead": "定价按部署进行报价,因此适合您的团队和工作负载。告诉我您的用例,我会给您报价。" + "hero_simple": "VoiceStudio 在 AGPL-3.0 下是免费和开源的 — 包括商业和内部业务用途。您只需要商业许可即可将其嵌入闭源产品中,无需 AGPL 的 Copyleft 义务。" }, "exportModal": { "export": "导出", @@ -2322,7 +2226,6 @@ "loaded_ready": "已加载并准备就绪", "loading_first_run": "正在加载...(第一次运行可能需要 1-2 分钟)", "failed_to_load": "加载失败", - "not_loaded_yet": "尚未加载 - 将在第一次转录时加载", "tts_not_loaded_yet": "尚未加载 — 将在首次语音生成时加载", "error_check_logs": "错误:{{error}}。检查日志并尝试重新启动。", "check_logs_restart": "检查日志中是否有模型加载错误。尝试重新启动。", @@ -2471,14 +2374,7 @@ }, "casting": { "title": "说话人分配", - "auto_assign_title": "自动为提取的说话人克隆分配声音", - "auto_cast": "自动分配", - "all_cast": "全部说话人", - "segments_count": "{{count}} 段", - "assign_voice": "分配声音…", - "from_video": "🎤 来自视频 ({{name}})", - "no_profiles": "尚未保存语音配置。", - "preview_voice": "预览声音" + "segments_count": "{{count}} 段" }, "checkpoint": { "timing_review": "{{count}} 个片段需要检查时间同步", @@ -2520,8 +2416,6 @@ "play_failed": "播放失败:{{message}}" }, "models": { - "hf_token_set_toast": "Hugging Face 令牌已设置 — 更快的下载已启用", - "hf_token_save_failed": "保存令牌失败", "install_started": "安装已开始——进度显示在行中", "delete_confirm": "删除{{repoId}}?您可以稍后重新安装。", "delete_confirm_title": "删除模型", @@ -2572,8 +2466,6 @@ "repair_title": "完成中断的下载", "required_tag": "必需", "delete_btn": "删除", - "hf_token_btn": "HF 令牌", - "hf_set_title": "设置 Hugging Face 令牌以加快下载速度", "get_token": "获取令牌 →", "reco_installed_for": "为 {{device}} 安装的推荐捆绑包", "reco_for": "推荐用于 {{device}}", @@ -2581,8 +2473,6 @@ "required_size": "需要 ~{{size}} GB", "all_size": "全部 ~{{size}} GB", "req_tag": "必需", - "search_placeholder": "搜索模型…", - "search_label": "搜索模型", "no_matches": "没有符合筛选条件的模型。", "clear_filters": "清除筛选", "in_memory": "已加载", @@ -2601,8 +2491,6 @@ "ready_badge": "准备好", "loading_badge": "加载中...", "idle_badge": "空闲", - "started_downloading_required_one": "开始下载 {{count}} 个必需模型", - "started_downloading_required_other": "开始下载 {{count}} 个必需模型", "role_all": "全部", "role_tts": "TTS", "role_asr": "ASR", @@ -2659,8 +2547,6 @@ "asrOpenAICompatTestFailed": "连接测试失败", "to_download": "待下载", "cached": "已缓存", - "fast_download_badge": "快速下载", - "fast_download_title": "通过 Xet {{version}} 进行快速下载 — 并行分块传输", "voice_previews": "声音试听", "voice_previews_desc": "下载预先渲染的试听片段,让声音立即播放——甚至在语音模型下载完成之前。在你打开此项之前不会下载任何内容。", "voice_previews_off": "已关闭 — 试听在本机渲染", @@ -2671,21 +2557,8 @@ "voice_previews_check": "立即检查", "voice_previews_rejected": "语音库更新被拒绝:{{message}}" }, - "enterprise_faq": { - "q_internal_tools": "我需要内部工具的许可证吗?", - "a_internal_tools": "不需要。您的员工和承包商的使用——包括内部修改和自托管——在 AGPL-3.0 下是免费的。只有当您将 VoiceStudio 嵌入闭源或专有产品/服务、且不愿遵守 AGPL 的源代码公开义务时,才需要商业许可证。", - "q_try_before": "我可以在提交之前尝试一下吗?", - "a_try_before": "可以。完整应用可在 AGPL-3.0 下免费下载、运行和自托管——无需任何协议。当您准备讨论商业(专有用途)许可证时,请发邮件联系我们,我们将一起敲定细节。", - "q_watermark": "水印呢?", - "a_watermark": "不可见的 AudioSeal 水印默认为所有人嵌入。你可以在设置 → 隐私中关闭它;该设置仅影响更改之后生成的音频。" - }, "gallery_extra": { - "save_prompt": "输入此语音配置文件的名称:", - "saved_as_profile": "声音已保存为配置!", - "save_profile_failed": "保存配置失败", - "download_failed": "下载失败:{{message}}", - "trim_load_failed": "无法加载音频进行修剪:{{message}}", - "upload_crop_failed": "无法上传裁剪后的语音:{{message}}" + "trim_load_failed": "无法加载音频进行修剪:{{message}}" }, "dub_workflow": { "preparing_audio": "正在准备音频...", @@ -2811,27 +2684,16 @@ }, "app": { "loading": "加载中...", - "trimmed_loaded": "已加载修剪后的音频", - "toast_exported": "导出:{{name}}", - "toast_export_failed": "导出失败:{{message}}", - "toast_open_folder_failed": "无法打开文件夹:{{message}}", "toast_saving": "正在保存 {{name}}...", "toast_saved": "已保存:{{path}}", "toast_save_error": "保存错误:{{message}}", "toast_processing": "正在处理 {{name}}...", "toast_downloaded": "已下载{{name}}", "toast_download_error": "下载错误:{{message}}", - "toast_upload_first": "请先点击“上传并转录”,以便在保存前在服务器上处理视频。", - "toast_save_failed": "保存失败:{{message}}", - "toast_opened": "已打开:{{name}}", - "toast_project_deleted": "项目已删除", - "toast_restored_state": "已恢复上一次生成状态", - "toast_history_deleted": "历史记录项已删除", "toast_flushed": "刷新 — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· 模型卸载", "toast_flush_failed": "刷新失败:{{message}}", - "toast_project_saved": "项目已保存", - "toast_project_created": "项目已创建" + "toast_project_saved": "项目已保存" }, "update": { "available": "有可用更新 {{version}}", @@ -2880,7 +2742,6 @@ "sponsors_empty_desc": "你的 Logo 会显示在这里", "sponsors_become": "成为赞助者", "sponsors_become_aria": "成为赞助者——在浏览器中打开预填好的 GitHub issue", - "sponsors_perk": "赞助者的 Logo 会出现在应用内、README 中,以及项目网站的一个展示位。", "sponsors_learn_more": "赞助者权益", "sponsors_logo_aria": "访问 {{name}}(VoiceStudio 赞助者)", "sponsors_tier_platinum": "铂金", @@ -2934,13 +2795,11 @@ "not_writable": "不可写", "change": "更改…", "compute_title": "计算", - "compute_label": "GPU / 加速器", "compute_auto": "自动(NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD 显卡(ROCm,Linux)", "channel_label": "更新通道", "channel_stable": "稳定版", "channel_preview": "预览版(最新 main)", - "network_title": "网络", "region_label": "下载区域", "mirrors_title": "自定义镜像(高级)", "mirror_pypi": "PyPI 索引地址", @@ -2975,10 +2834,6 @@ "trust_line": "隐私优先:你的声音、录音和项目从不离开这台设备——无需账号,无云端处理。", "resume_note": "下载中断后会自动续传——关闭应用也没关系。", "eta_left": "剩余约 {{eta}}", - "first_sound_text": "欢迎来到你的工作室。你听到的每一个字,都是刚刚在这台设备上生成的。", - "first_sound_done": "刚才的声音?几秒前在本地生成。欢迎入驻。", - "hf_token_title": "Hugging Face 令牌(可选)", - "hf_token_hint": "免费账户令牌可解锁受限模型——例如多说话人配音的说话人分离(pyannote)。仅保存在本机。", "hf_token_save": "保存", "hf_token_saving": "保存中…", "hf_token_saved": "Hugging Face 令牌已保存", @@ -2988,7 +2843,6 @@ "size_left": "剩余 {{size}}", "hf_token_card_title": "添加免费的 Hugging Face 令牌以加快下载速度", "hf_token_get": "没有令牌?免费获得一份 →", - "hf_token_saved_fast": "Hugging Face 令牌已保存——下载速度更快", "hf_token_inline_prompt": "使用免费的 Hugging Face 令牌加快下载速度", "hf_token_inline_ph": "粘贴 hf_… 令牌(可选)", "hf_token_get_short": "免费获得一份 →", @@ -3012,15 +2866,11 @@ "unstar_take": "取消加星 — 允许清理", "play_take": "载入为当前输出", "filter_starred": "已加星", - "load_take_failed": "无法加载该条生成:{{message}}", "mode_clone": "克隆", "mode_synth": "合成", "generation_seconds": "{{duration}} 秒", - "expand_text": "展开文本", - "collapse_text": "收起文本", "seed": "种子 {{seed}}", - "load_config": "加载设置", - "delete_confirm": "删除此历史记录吗?" + "load_config": "加载设置" }, "openapi": { "title": "VoiceStudio API", @@ -3356,7 +3206,6 @@ "security_title": "报告安全问题", "security_desc": "发现漏洞?请不要公开提 issue。请通过 GitHub Security Advisories 私下报告,以便在披露前得到修复。", "security_cta": "私下报告", - "other_ways": "其他联系方式", "email": "电子邮件", "email_desc": "邮件——许可、合作或任何私密事项。", "website": "网站", @@ -3492,25 +3341,18 @@ "preview": "赞助商展示预览", "message": "您的品牌、网站和留言", "email_app": "打开邮件应用", - "copy_email": "复制邮箱", - "preview_detail": "这里可以展示您的标志、链接和介绍。" + "copy_email": "复制邮箱" }, "supportPlans": { "remove": "移除赞助商栏", "title": "免费版与 Pro 对比", "free": "免费版", "pro": "Pro", - "sponsor_bar": "赞助商栏", "visible": "显示", - "hideable": "可隐藏", "unavailable": "尚未配置 Pro 激活。", "telemetry": "遥测", - "opt_in": "可选,需主动同意", "disabled": "已禁用", - "badge": "Pro 徽章", - "advanced": "高级工具", - "standard": "标准", - "included": "包含" + "badge": "Pro 徽章" }, "integrationCatalog": { "title": "集成", @@ -3519,8 +3361,7 @@ "n8nHint": "将此工作流导入 n8n,编辑请求后运行。请保持 VoiceStudio 运行。Docker 和远程 n8n 需要可访问的后端地址及必要的凭据。" }, "directoryExamples": { - "example": "目录示例", - "notice": "仅为目录示例,并非赞助商或已连接的集成。" + "example": "目录示例" }, "workflowRun": { "run": "运行工作流", diff --git a/electron/src/shared/i18n/locales/zh-TW.json b/electron/src/shared/i18n/locales/zh-TW.json index c01653424..24e5c9d9f 100644 --- a/electron/src/shared/i18n/locales/zh-TW.json +++ b/electron/src/shared/i18n/locales/zh-TW.json @@ -59,15 +59,10 @@ "title": "詢問 VoiceStudio Agent", "notInstalled": "未安裝", "noSource": "請開啟可寫入的 VoiceStudio 原始碼簽出目錄,讓代理進行修復。", - "noneFound": "安裝 Codex、Claude Code、OpenCode 或 Pi 以使用本機修復。", "ready": "就緒", "placeholder": "出了什麼問題?請加入操作步驟、可見錯誤或預期行為。", - "prReady": "修復已完成。請檢查變更,並在目標測試通過後提出 PR。", "diagnose": "診斷", "fix": "修復", - "chooseDefault": "選擇此畫面當機時 VoiceStudio 自動使用的代理程式。", - "useAutomatically": "自動使用 {{agent}}", - "captured": "已擷取 {{count}} 行問題日誌", "contextNotice": "所選代理將收到此報告、目前畫面、最近的應用程式與後端記錄以及系統診斷資訊。", "complete": "已完成" }, @@ -607,7 +602,6 @@ "characters": "{{count}} 個字符", "minutes": "~{{count}} 分鐘", "chars": "{{count}} 字符", - "exportDone": "有聲書已下載", "exportFailed": "匯出失敗 — 檢查後端是否正在運作。", "global_speed": "速度", "global_speed_hint": "所有線路的讀取速度,沒有自己的速度覆蓋", @@ -818,7 +812,6 @@ "data_dir": "數據目錄", "outputs": "輸出", "crash_log": "崩潰日誌", - "update_endpoint": "更新端點", "yes": "是的", "no": "不", "web_preview": "網頁預覽", @@ -879,38 +872,21 @@ "title": "憑證", "desc": "API 金鑰和令牌僅針對此會話設定 <1>。為了在重新啟動後保持持久性,請將它們設定為 shell 設定檔中的環境變數。", "hf_token": "HuggingFace 代幣", - "hf_help": "說話者分類和更快的模型下載所需。請造訪 Huggingface.co/settings/tokens 取得。", - "translate_key": "法學碩士 API 金鑰", - "translate_help": "可選 — 對於 DeepL、OpenAI 或付費翻譯提供者。 Google 翻譯不需要(免費套餐)。", "save": "儲存", "saved": "✓ 設定", - "not_set": "✗ 未設定", "get_token": "獲取代幣 →", - "saved_session": "{{key}} 已為此會話儲存", "save_failed": "保存失敗", "save_error": "儲存失敗:{{message}}", "deepl_key": "DeepL API 金鑰", - "deepl_help": "對於 DeepL 翻譯引擎。請造訪 deepl.com/pro-api 以取得密鑰。", "deepl_base_url": "DeepL 基本 URL", "deepl_base_url_help": "自訂 DeepL API 端點(預設:https://api.deepl.com/v2)。變更為代理/自架。", "microsoft_key": "微軟翻譯金鑰", - "microsoft_help": "對於微軟翻譯引擎。從 Azure 入口網站取得金鑰。", "microsoft_base_url": "微軟翻譯機基本 URL", "microsoft_base_url_help": "自訂 Azure 翻譯端點(預設:https://api.cognitive.microsofttranslator.com)。", - "llm_base_url": "法學碩士基本網址", - "llm_base_url_help": "對於電影品質或 LLM 引擎。任何 OpenAI 相容端點(OpenAI、Ollama、LM Studio 等)。", - "llm_model": "法學碩士模式", - "llm_model_help": "與 LLM 引擎一起使用的模型名稱(例如 gpt-4o、deepseek-chat、qwen2.5)。", - "group_huggingface": "抱臉", - "group_translate": "翻譯(法學碩士/電影)", - "group_deepl": "DeepL 自訂端點", - "group_microsoft": "Microsoft Translator 自訂端點", "region": "區域", "aws_profile": "AWS 設定檔" }, "engines": { - "banners_on": "舞台橫幅", - "banners_off": "舞台橫幅關閉", "backend": "後端", "status": "狀態", "ready": "準備好了", @@ -941,7 +917,6 @@ "dictation_model_hint": "用於即時聽寫,以及此引擎進行轉錄時。", "dictation_not_installed": "未安裝 — 選取以下載", "active": "活躍的", - "whyUnavailable": "所需條件", "diskDetails": "磁碟詳細資料", "diskModelDownload": "模型下載", "diskPackageDownload": "套件下載", @@ -965,11 +940,8 @@ "sectionReady": "隨時可用", "sectionMore": "新增更多引擎", "lastError": "最後一個錯誤:{{error}}", - "installedAndReady": "安裝並準備就緒", - "notInstalled": "未安裝", "available": "可用", "subprocessTitle": "在自己的子進程+venv中運行", - "inProcessTitle": "在 VoiceStudio Python 進程中執行", "testEngine": "測試引擎", "testing": "測試...", "recheck": "重新檢查", @@ -986,7 +958,6 @@ "routingUnknown": "未知", "routingEffectiveChip": "在此機器上的 {{device}} 上執行", "gpuVulkan": "Vulkan", - "routingCaveatTitle": "已選擇 GPU,但是:{{reason}}", "selectCpuFallback": "{{engine}}:正在 CPU 上執行 — {{reason}}", "selectWithCaveat": "已切換至 {{engine}} — {{reason}}", "generateCaveat": "{{engine}} 在這台機器上可能較慢 — {{reason}}", @@ -1035,9 +1006,6 @@ "empty_tauri": "沒有可用的 Tauri 日誌。僅在桌面 shell 中運行。", "empty_backend": "運行時日誌為空。當後端記錄活動時,活動將顯示在此。", "title": "紀錄", - "source_backend": "後端", - "source_frontend": "前端", - "source_tauri": "金牛座", "expand": "展開日誌", "collapse": "折疊日誌", "expand_aria": "展開日誌面板", @@ -1104,12 +1072,10 @@ "unlock": "解鎖", "try_voice": "試試這個聲音", "test_phrase": "測試短語", - "test_help": "輸入任何內容 - 我們將使用該語音的當前設定來產生它。", "test_placeholder": "輸入一些內容來聽這個聲音說出來...", "gen_preview": "生成預覽", "generating": "生成…", "used_title": "使用此聲音的地方", - "used_empty": "這個聲音還沒有被使用過。產生上面的預覽,或在配音中選擇它。", "synth_clips_one": "{{count}} 合成器剪輯", "synth_clips_other": "{{count}} 合成剪輯", "projects_count_one": "{{count}} 項目", @@ -1152,9 +1118,7 @@ "translate": "翻譯", "generate": "產生", "export": "出口", - "no_video": "沒有加載視頻", "segments": "段", - "speakers": "揚聲器", "video_dubbing_studio": "視訊配音工作室", "drop_here": "將視訊或音訊拖放到此處", "paste_url": "…或貼上 YouTube/影片 URL", @@ -1168,7 +1132,6 @@ "cookie_size_error": "Cookie 匯出檔必須小於或等於 1 MB。", "save": "儲存", "reset": "重置", - "save_project": "保存項目", "change_file": "更改文件", "import_srt": "導入.srt", "import_srt_alt": "改為導入 .srt", @@ -1186,9 +1149,6 @@ "retranslate": "重新翻譯", "transcript": "成績單", "generate_dub": "生成配音", - "export_mp4": "MP4", - "export_wav": "音訊格式", - "export_srt": "選擇性RT", "upload_transcribe": "上傳和轉錄", "multi_lang": "多語言", "prep_download": "正在下載影片...", @@ -1206,7 +1166,6 @@ "transcribing": "正在轉錄音訊...", "elapsed": "過去了", "remaining": "剩餘 s", - "sidebar_toggle": "切換側邊欄", "retry_transcription": "重試轉錄", "supported_formats": "MP4·MOV·MKV·WEBM·MP3·WAV·FLAC·M4A", "pull_captions_title": "当 URL 是带有字幕的主机(YouTube、Vimeo、TED...)时,还要提取原始字幕和任何 YouTube 自动翻译。在不运行 Whisper 的情况下为编辑器播种;对于 YouTube 已涵盖的语言,请跳过“全部翻译”。", @@ -1249,7 +1208,6 @@ "engine_label": "引擎", "install_engine": "安裝這個引擎", "installing_engine": "…正在安裝", - "needs_dev_install": "需要開發安裝", "install_disabled_title": "在打包版本中停用安裝", "needs_install_suffix": "— 需要安裝", "glossary_title": "重複出現的術語(名稱、品牌詞、行話)的 Pin 翻譯", @@ -1283,7 +1241,6 @@ "original_track": "原創", "selected_dub": "{{code}}(精選配音)", "dub_track": "{{code}}(配音)", - "export_tracks": "導出曲目:", "stopping": "停止…", "stop_progress": "停止 ({{current}}/{{total}})", "regen_changed": "再生 {{count}} 已更改", @@ -1295,7 +1252,6 @@ "install_ok": "{{engine}} 已安裝", "install_failed": "安裝失敗:{{message}}", "prep_elapsed": "{{time}} 已過", - "add_language": "新增語言", "search_languages": "搜尋語言...", "languages_selected_one": "已選擇 {{count}} 語言", "languages_selected_other": "已選擇 {{count}} 種語言", @@ -1392,7 +1348,6 @@ }, "segment": { "time": "時間", - "spkr": "斯帕克爾", "text": "文字", "lang": "郎", "voice": "語音", @@ -1403,8 +1358,6 @@ "select_all_title": "選擇所有過濾的", "sel_count": "{{count}} 選擇", "select_title": "選擇段(Shift+點選選擇範圍)", - "sync_title": "產生的音訊是原始音訊持續時間的 {{pct}}%", - "sync_label": "同步:{{pct}}%", "rate_title": "語速擬合:{{ratio}}× 相對於插槽{{error}}", "speaker_id": "發言者ID", "translate_error_title": "翻譯錯誤:{{error}}", @@ -1415,9 +1368,6 @@ "restore_title": "恢復原文", "lang_default": "(預設)", "voice_default": "預設", - "from_video": "來自視頻", - "clone_profiles": "克隆設定檔", - "design_presets": "設計預設", "preview_title": "即時預覽", "set_direction": "設定方向...", "edit_direction": "編輯方向...", @@ -1474,7 +1424,6 @@ }, "voice": { "personality": "個性", - "pick_personality": "選擇個性預設...", "instruct": "指導", "reference_text": "參考文本", "language": "語言", @@ -1535,9 +1484,7 @@ "stories": "故事", "untitled_story": "無題故事", "story_lines": "{{count}} 線", - "story_lines_plural": "{{count}} 行", "story_voices": "{{count}} 聲音", - "story_voices_plural": "{{count}} 聲音", "audiobooks": "有聲書", "audiobook": "有聲書", "story": "故事", @@ -1609,20 +1556,9 @@ }, "gallery": { "title": "VoiceStudio 畫廊", - "search_placeholder": "搜尋 YouTube...", - "all_voices": "所有聲音 ({{count}})", - "no_voices": "還沒有聲音", "download_hint": "在上方搜尋即可下載剪輯", - "search_btn": "搜尋", - "download_btn": "下載", "saving": "正在儲存...", "trim": "修剪", - "delete_confirm": "刪除該語音片段嗎?", - "view_mode_list": "清單", - "view_mode_grid": "網格", - "youtube_results": "YouTube 結果 ({{count}})", - "clone_profile": "克隆設定檔", - "crop_audio": "裁切音訊", "subtitle": "數百種現成的設計聲音 - 選擇一種即可使用。", "zone_archetypes": "原型", "zone_imports": "我的進口", @@ -1658,8 +1594,6 @@ "delete": "刪除", "community_empty": "尚未載入任何社群語音 — 請連上網路後重新開啟,或成為第一個投稿的人。", "community_explainer": "由社群分享的設計預設與錄製語音,從 omnivoice-gallery 載入。", - "no_designer": "錄製的語音 — 請改用「使用語音」。", - "no_preview": "沒有預覽 — 透過「使用語音」加入後即可試聽。", "submit_failed": "無法開啟投稿表單。", "submit_preset": "投稿預設", "submit_voice": "投稿語音", @@ -1697,16 +1631,12 @@ }, "setup": { "ready_desc": "現在可以建立你的第一個聲音了。你可以立即設定語音輸入,也可以稍後在設定中完成。", - "welcome": "歡迎", "system_check": "系統檢查", "install_models": "安裝型號", - "pick_engines": "選擇引擎", "system_check_desc": "探測 RAM、磁碟、GPU、ffmpeg 和網路。攔截器會預先標記,以便您在下載前知道。", "install_models_desc": "下載約 2.4 GB 的權重 — 僅 TTS 模型為必需。可立即安裝為您的硬體精選的轉錄模型,或稍後在設定中新增。", "pick_engines_desc": "選擇 TTS / ASR / LLM 後端。預設設定開箱即用 - 可以隨時在“設定”中自訂。", "hero_desc": "配音、語音克隆和語音設計 - 所有這些都在您的電腦上本地運行。", - "first_run": "首次運行需要 5-10 分鐘才能下載。此後,每次啟動都是即時且完全離線的。", - "get_started": "開始使用", "back": "返回", "continue_ok": "一切順利——繼續", "continue_warn": "繼續(有警告)", @@ -1727,7 +1657,6 @@ "try_dictation": "試著聽寫", "try_dictation_desc": "完成設定前,按一次快捷鍵確認聽寫功能可以正常運作。", "dictation_step_label": "聽寫", - "next_try_dictation": "下一步:嘗試聽寫", "step_aria": "第 {{num}} 步:{{label}}", "step_completed": "已完成", "cache_label": "模型緩存" @@ -1743,7 +1672,6 @@ "platforms": "平台", "footer": "每一項貢獻都有助於突破本地人工智慧的界限。", "back": "返回工作室", - "commercial": "商業許可", "pill": "支持VoiceStudio", "suggested_title": "選擇金額", "most_common": "最常見的", @@ -1761,15 +1689,7 @@ }, "postcard": { "title": "支持 VoiceStudio 開發", - "lead_default": "很高興這有效! VoiceStudio 是免費的並且完全本地化。如果可以節省您的時間,每月只需投入一小筆資金即可資助其背後的 Claude Max。", - "lead_first_clone": "你的第一個聲音克隆已經完成——太好了! VoiceStudio 完全在您的電腦上運行,並且您的支援使其保持這種狀態。", - "lead_tenth_dub": "十次配音——你顯然正在讓它發揮作用。每月一小筆資金為 Claude Max 提供這些功能的資金。", - "lead_sustained": "您使用 VoiceStudio 已經一個月了。如果它在您的工作流程中贏得了一席之地,請考慮幫助資助下一步的工作。", - "chip_in": "籌碼", - "later": "也許稍後", - "star": "在 GitHub 上加星標", - "opt_out": "不要再問", - "dismiss_aria": "關閉" + "later": "也許稍後" } }, "enterprise": { @@ -1779,29 +1699,18 @@ "badge": "商業許可", "hero_title": "在生產中傳送人工智慧聲音", "hero_desc": "VoiceStudio 是基於 GNU Affero 通用公眾授權條款第 3 版(AGPL-3.0)的自由開源軟體——可免費使用,包括商業用途與企業內部用途。只有當您想在不承擔 AGPL-3.0 著佐權(copyleft)義務的情況下,將 VoiceStudio 嵌入閉源或專有產品/服務時,才需要商業授權。", - "why_title": "為什麼企業選擇 VoiceStudio", - "pricing_title": "定價", - "faq_title": "常見問題", "pricing_desc": "等級和定價仍在最終確定中。", - "pricing_detail": "在公開之前,每個商業部署都會單獨報價,以便我們可以根據您的團隊和工作負載調整規模。", "request_quote": "索取報價", - "footer_email": "有問題嗎?請透過 hi@voicestudio.sh 聯繫", - "footer_discord": "加入我們的 Discord 以獲得社群支持。", "benefit_ip": "完全IP所有權", "benefit_ip_desc": "您擁有每一個輸出、每一個模型、每一個衍生品。無共享租賃,無資料外洩。", "benefit_cost": "每分鐘零成本", "benefit_cost_desc": "沒有 API 帳單,也沒有意外超額。從第一天起就在您自己的硬體上運行。", - "benefit_team": "團隊範圍內的訪問", "benefit_team_desc": "一份許可證涵蓋您組織內的無限用戶。", "benefit_support": "直接支援", "benefit_support_desc": "來自核心工程團隊的優先電子郵件和視訊通話支援。", - "benefit_source": "可用原始碼核心", "benefit_source_desc": "對堆疊的完全可見性。在許可條款範圍內進行審核、分叉和調整。", - "benefit_lang": "646 種語言", "benefit_lang_desc": "以人類層次的品質轉錄、翻譯和配音 646 種語言。", - "hero_note": "使用、自行架設與商業用途在 AGPL-3.0 下皆免費——包括大規模使用。AGPL 是網路著佐權授權條款:如果您修改 VoiceStudio 並透過網路向他人提供該修改版本,就必須以相同條款公開您修改後的原始碼。商業授權可為專有閉源部署免除這些著佐權義務。定價方案即將推出——在此期間歡迎與我們聯繫。", - "hero_simple": "VoiceStudio 在 AGPL-3.0 下是免費且開源的 — 包括商業和內部業務用途。您只需要商業許可即可將其嵌入閉源產品中,無需 AGPL 的 Copyleft 義務。", - "contact_lead": "定價按部署報價,因此適合您的團隊和工作負載。告訴我您的用例,我會給您報價。" + "hero_simple": "VoiceStudio 在 AGPL-3.0 下是免費且開源的 — 包括商業和內部業務用途。您只需要商業許可即可將其嵌入閉源產品中,無需 AGPL 的 Copyleft 義務。" }, "exportModal": { "export": "出口", @@ -2006,7 +1915,6 @@ "loaded_ready": "已載入並準備就緒", "loading_first_run": "正在加載...(第一次運行可能需要 1-2 分鐘)", "failed_to_load": "載入失敗", - "not_loaded_yet": "尚未加載 - 將在第一次轉錄時加載", "tts_not_loaded_yet": "尚未載入 — 將在首次語音生成時載入", "error_check_logs": "錯誤:{{error}}。檢查日誌並嘗試重新啟動。", "check_logs_restart": "檢查日誌中是否有模型載入錯誤。嘗試重新啟動。", @@ -2154,14 +2062,7 @@ }, "casting": { "title": "揚聲器選角", - "auto_assign_title": "自動分配提取的說話者克隆的聲音", - "auto_cast": "自動施法", - "all_cast": "所有演員", - "segments_count": "{{count}} 段", - "assign_voice": "指定語音...", - "from_video": "🎤 來自影片 ({{name}})", - "no_profiles": "尚未儲存語音設定檔。", - "preview_voice": "預覽語音" + "segments_count": "{{count}} 段" }, "checkpoint": { "timing_review": "{{count}} 個片段需要檢查時間同步", @@ -2203,8 +2104,6 @@ "play_failed": "播放失敗:{{message}}" }, "models": { - "hf_token_set_toast": "HuggingFace 令牌集 — 啟用更快的下載", - "hf_token_save_failed": "保存令牌失敗", "install_started": "安裝已開始 — 行中的進度", "delete_confirm": "刪除{{repoId}}?您可以稍後重新安裝。", "delete_confirm_title": "刪除模型", @@ -2247,8 +2146,6 @@ "not_installed": "未安裝", "required_tag": "必填", "delete_btn": "刪除", - "hf_token_btn": "HF代幣", - "hf_set_title": "設定 HuggingFace 令牌以加快下載速度", "get_token": "獲取代幣 →", "reco_installed_for": "為 {{device}} 安裝的建議捆綁包", "reco_for": "推薦用於 {{device}}", @@ -2256,8 +2153,6 @@ "required_size": "需要 ~{{size}} GB", "all_size": "全部 ~{{size}} GB", "req_tag": "要求", - "search_placeholder": "搜尋型號...", - "search_label": "搜尋型號", "no_matches": "沒有型號與您的過濾器相符。", "sort_by": "按 {{column}} 排序", "column_model": "型號", @@ -2267,12 +2162,8 @@ "ready_badge": "準備好", "loading_badge": "加載中...", "idle_badge": "空閒", - "started_downloading_required_one": "開始下載 {{count}} 所需模型", - "started_downloading_required_other": "開始下載 {{count}} 所需模型", "to_download": "下載", "cached": "快取的", - "fast_download_badge": "快速下載", - "fast_download_title": "透過 Xet {{version}} 進行快速下載 — 平行分塊傳輸", "voice_previews": "語音試聽", "voice_previews_desc": "下載預先算好的試聽片段,讓聲音立即播放——甚至在語音模型下載完成之前。在你開啟此項之前不會下載任何內容。", "voice_previews_off": "已關閉 — 試聽在本機算繪", @@ -2286,21 +2177,8 @@ "role_tts": "TTS", "role_asr": "ASR" }, - "enterprise_faq": { - "q_internal_tools": "我需要內部工具的授權嗎?", - "a_internal_tools": "不需要。您的員工與承包商的使用——包括內部修改與自行架設——在 AGPL-3.0 下是免費的。只有當您將 VoiceStudio 嵌入閉源或專有產品/服務,且不願遵守 AGPL 的原始碼公開義務時,才需要商業授權。", - "q_try_before": "我可以在提交之前嘗試一下嗎?", - "a_try_before": "可以。完整應用程式可在 AGPL-3.0 下免費下載、執行與自行架設——無需任何協議。當您準備討論商業(專有用途)授權時,請寄信給我們,我們會一起確認細節。", - "q_watermark": "水印呢?", - "a_watermark": "不可見的 AudioSeal 浮水印預設會為所有人嵌入。你可以在設定 → 隱私中關閉;此設定僅影響變更之後產生的音訊。" - }, "gallery_extra": { - "save_prompt": "輸入此語音設定檔的名稱:", - "saved_as_profile": "語音已儲存為個人檔案!", - "save_profile_failed": "儲存個人資料失敗", - "download_failed": "下載失敗:{{message}}", - "trim_load_failed": "無法載入音訊進行修剪:{{message}}", - "upload_crop_failed": "無法上傳裁剪後的語音:{{message}}" + "trim_load_failed": "無法載入音訊進行修剪:{{message}}" }, "dub_workflow": { "preparing_audio": "正在準備音訊...", @@ -2426,27 +2304,16 @@ }, "app": { "loading": "加載中...", - "trimmed_loaded": "已載入修剪後的音頻", - "toast_exported": "匯出:{{name}}", - "toast_export_failed": "匯出失敗:{{message}}", - "toast_open_folder_failed": "無法開啟資料夾:{{message}}", "toast_saving": "正在儲存 {{name}}...", "toast_saved": "已儲存:{{path}}", "toast_save_error": "儲存錯誤:{{message}}", "toast_processing": "正在處理 {{name}}...", "toast_downloaded": "已下載{{name}}", "toast_download_error": "下載錯誤:{{message}}", - "toast_upload_first": "請先點擊“上傳和轉錄”,以便在儲存之前在伺服器上處理影片。", - "toast_save_failed": "儲存失敗:{{message}}", - "toast_opened": "開放時間:{{name}}", - "toast_project_deleted": "項目已刪除", - "toast_restored_state": "恢復了上一代狀態", - "toast_history_deleted": "歷史記錄項目已刪除", "toast_flushed": "刷新 — RAM {{ram}}G · VRAM {{vram}}G{{unloaded}}", "toast_model_unloaded": "· 模型卸載", "toast_flush_failed": "刷新失敗:{{message}}", - "toast_project_saved": "項目已儲存", - "toast_project_created": "專案已建立" + "toast_project_saved": "項目已儲存" }, "update": { "available": "有可用更新 {{version}}", @@ -2531,13 +2398,11 @@ "not_writable": "不可寫入", "change": "變更…", "compute_title": "運算", - "compute_label": "GPU / 加速器", "compute_auto": "自動(NVIDIA CUDA / Apple MPS / CPU)", "compute_rocm": "AMD 顯示卡(ROCm,Linux)", "channel_label": "更新頻道", "channel_stable": "穩定版", "channel_preview": "預覽版(最新 main)", - "network_title": "網路", "region_label": "下載區域", "mirrors_title": "自訂鏡像(進階)", "mirror_pypi": "PyPI 索引網址", @@ -2570,10 +2435,6 @@ "trust_line": "隱私優先:你的聲音、錄音與專案從不離開這台裝置——無需帳號,無雲端處理。", "resume_note": "下載中斷後會自動續傳——關閉應用程式也沒關係。", "eta_left": "剩餘約 {{eta}}", - "first_sound_text": "歡迎來到你的工作室。你聽到的每一個字,都是剛剛在這台裝置上生成的。", - "first_sound_done": "剛才的聲音?幾秒前在本機生成。歡迎加入。", - "hf_token_title": "Hugging Face 權杖(可選)", - "hf_token_hint": "解鎖受限模型——例如多語者配音的語者分離(pyannote)。免費帳號權杖,僅保存在本機。", "hf_token_save": "儲存", "hf_token_saving": "儲存中…", "hf_token_saved": "Hugging Face 權杖已儲存", @@ -2583,7 +2444,6 @@ "size_left": "{{size}} 左", "hf_token_card_title": "添加免費的 Hugging Face 令牌以加快下載速度", "hf_token_get": "沒有令牌?免費獲得一份 →", - "hf_token_saved_fast": "擁抱臉部令牌已儲存 — 下載速度更快", "hf_token_inline_prompt": "使用免費的 Hugging Face 令牌加快下載速度", "hf_token_inline_ph": "貼上 hf_… 令牌(可選)", "hf_token_get_short": "免費獲得一份 →", @@ -2607,15 +2467,11 @@ "unstar_take": "取消加星 — 允許清理", "play_take": "載入為目前輸出", "filter_starred": "已加星", - "load_take_failed": "無法載入該次生成:{{message}}", "mode_clone": "複製", "mode_synth": "合成", "generation_seconds": "{{duration}} 秒", - "expand_text": "展開文字", - "collapse_text": "收合文字", "seed": "種子 {{seed}}", - "load_config": "載入設定", - "delete_confirm": "刪除此歷史記錄嗎?" + "load_config": "載入設定" }, "openapi": { "title": "VoiceStudio API", @@ -3015,25 +2871,18 @@ "preview": "贊助商展示預覽", "message": "您的品牌、網站和留言", "email_app": "開啟郵件應用程式", - "copy_email": "複製信箱", - "preview_detail": "這裡可以展示您的標誌、連結和介紹。" + "copy_email": "複製信箱" }, "supportPlans": { "remove": "移除贊助商列", "title": "免費版與 Pro 比較", "free": "免費版", "pro": "Pro", - "sponsor_bar": "贊助商列", "visible": "顯示", - "hideable": "可隱藏", "unavailable": "尚未設定 Pro 啟用。", "telemetry": "遙測", - "opt_in": "可選,需主動同意", "disabled": "已停用", - "badge": "Pro 徽章", - "advanced": "進階工具", - "standard": "標準", - "included": "包含" + "badge": "Pro 徽章" }, "integrationCatalog": { "title": "整合", @@ -3042,8 +2891,7 @@ "n8nHint": "將此工作流程匯入 n8n,編輯請求後執行。請保持 VoiceStudio 執行。Docker 和遠端 n8n 需要可存取的後端位址及必要的認證資訊。" }, "directoryExamples": { - "example": "目錄範例", - "notice": "僅為目錄範例,並非贊助商或已連接的整合。" + "example": "目錄範例" }, "workflowRun": { "run": "執行工作流程", From f4c980a2c4a928d26f0b13284f386d94106b5317 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:19:45 +0530 Subject: [PATCH 38/85] fix(i18n): keep the seconds unit in five locales and cover the runtime catalog ja, ko, pl, th and vi rendered clone.duration_seconds as a bare number. test_locale_parity now checks the renderer catalog the app loads: full key parity, no orphans beyond CLDR plural forms, and intact placeholders. --- .../src/renderer/src/i18n/locales/ja.json | 2 +- .../src/renderer/src/i18n/locales/ko.json | 2 +- .../src/renderer/src/i18n/locales/pl.json | 2 +- .../src/renderer/src/i18n/locales/th.json | 2 +- .../src/renderer/src/i18n/locales/vi.json | 2 +- tests/test_locale_parity.py | 85 +++++++++++++++++-- 6 files changed, 82 insertions(+), 13 deletions(-) diff --git a/electron/src/renderer/src/i18n/locales/ja.json b/electron/src/renderer/src/i18n/locales/ja.json index 56297fcb0..cf69f1fb6 100644 --- a/electron/src/renderer/src/i18n/locales/ja.json +++ b/electron/src/renderer/src/i18n/locales/ja.json @@ -602,7 +602,7 @@ "history_reuse": "スクリプトを再利用する", "characters_one": "{{count}} 文字", "cancel_generation": "生成をキャンセルする", - "duration_seconds": "{{seconds}}", + "duration_seconds": "{{seconds}}秒", "download_failed": "ファイルを保存できませんでした: {{message}}", "history_clear_confirm": "履歴からすべてのテイクを削除しますか?音声ファイルも削除されます。", "shortcut_hint": "Ctrl/Cmd + Enter", diff --git a/electron/src/renderer/src/i18n/locales/ko.json b/electron/src/renderer/src/i18n/locales/ko.json index dcde670ab..7689b4730 100644 --- a/electron/src/renderer/src/i18n/locales/ko.json +++ b/electron/src/renderer/src/i18n/locales/ko.json @@ -602,7 +602,7 @@ "history_reuse": "스크립트 재사용", "characters_one": "{{count}} 문자", "cancel_generation": "생성 취소", - "duration_seconds": "{{seconds}}", + "duration_seconds": "{{seconds}}초", "download_failed": "파일을 저장할 수 없습니다: {{message}}", "history_clear_failed": "기록을 지울 수 없습니다: {{message}}", "history_star_failed": "테이크를 업데이트할 수 없습니다: {{message}}", diff --git a/electron/src/renderer/src/i18n/locales/pl.json b/electron/src/renderer/src/i18n/locales/pl.json index 8ec16b051..c033ba0ca 100644 --- a/electron/src/renderer/src/i18n/locales/pl.json +++ b/electron/src/renderer/src/i18n/locales/pl.json @@ -602,7 +602,7 @@ "history_reuse": "Użyj skryptu ponownie", "characters_one": "Znak {{count}}", "cancel_generation": "Anuluj generowanie", - "duration_seconds": "{{seconds}}", + "duration_seconds": "{{seconds}} s", "download_failed": "Nie można zapisać pliku: {{message}}", "no_language_match": "Żaden język nie pasuje do „{{query}}”.", "delete_profile_failed": "Nie udało się usunąć głosu: {{message}}", diff --git a/electron/src/renderer/src/i18n/locales/th.json b/electron/src/renderer/src/i18n/locales/th.json index 0a9904c31..ef792a905 100644 --- a/electron/src/renderer/src/i18n/locales/th.json +++ b/electron/src/renderer/src/i18n/locales/th.json @@ -604,7 +604,7 @@ "history_reuse": "ใช้สคริปต์ซ้ำ", "characters_one": "อักขระ {{count}}", "cancel_generation": "ยกเลิกการสร้าง", - "duration_seconds": "{{seconds}}", + "duration_seconds": "{{seconds}} วินาที", "download_failed": "ไม่สามารถบันทึกไฟล์: {{message}}", "no_profiles": "ยังไม่มีเสียงที่บันทึกไว้ อัปโหลดหรือบันทึกข้อมูลอ้างอิง จากนั้นบันทึกเป็นโปรไฟล์", "edit_voice": "แก้ไขเสียง", diff --git a/electron/src/renderer/src/i18n/locales/vi.json b/electron/src/renderer/src/i18n/locales/vi.json index aef0b0b17..467a0cfd3 100644 --- a/electron/src/renderer/src/i18n/locales/vi.json +++ b/electron/src/renderer/src/i18n/locales/vi.json @@ -604,7 +604,7 @@ "history_reuse": "Tái sử dụng tập lệnh", "characters_one": "Ký tự {{count}}", "cancel_generation": "Hủy tạo", - "duration_seconds": "{{seconds}}", + "duration_seconds": "{{seconds}} giây", "download_failed": "Không thể lưu tệp: {{message}}", "history_clear_confirm": "Xóa mọi cảnh quay khỏi lịch sử? Các tập tin âm thanh cũng bị xóa.", "edit_voice": "Chỉnh sửa giọng", diff --git a/tests/test_locale_parity.py b/tests/test_locale_parity.py index 6d716e9e3..64e4305a2 100644 --- a/tests/test_locale_parity.py +++ b/tests/test_locale_parity.py @@ -1,9 +1,11 @@ """Locale key parity — all 21 i18n files stay in lockstep with en.json. -Replaces the manual locale sweeps: CLAUDE.md's Localization rule routes every -UI string through ``electron/src/shared/i18n/locales/*.json``, which only works if -every file parses, carries no keys en.json doesn't have, and preserves en's -``{{placeholder}}`` tokens. Real bug classes this pins down: +Two catalogs exist. ``electron/src/renderer/src/i18n/locales`` is the one the +app loads at runtime; it must have full key parity (see the ``renderer`` +tests at the end). ``electron/src/shared/i18n/locales`` is loaded only by the +transitional shared modules' tests and is ratcheted below. Both must parse, +carry no keys en.json doesn't have, and preserve en's ``{{placeholder}}`` +tokens. Real bug classes this pins down: * a translation that drops ``{{message}}`` shows users a bare error with the detail silently lost (six ``gallery.*`` keys drifted this way in all 20 @@ -99,10 +101,10 @@ # Never raise one: if this fails after adding en.json keys, add the keys to # every locale (translated) in the same change instead. _MISSING_BASELINE = { - "ar": 475, "de": 475, "es": 475, "fr": 475, "hi": 475, "id": 475, - "it": 475, "ja": 475, "ko": 0, "nl": 475, "pl": 475, "pt": 475, - "ru": 475, "sv": 475, "th": 475, "tr": 475, "uk": 475, "vi": 475, - "zh-CN": 0, "zh-TW": 475, + "ar": 464, "de": 464, "es": 464, "fr": 464, "hi": 464, "id": 464, + "it": 464, "ja": 464, "ko": 0, "nl": 464, "pl": 464, "pt": 464, + "ru": 464, "sv": 464, "th": 464, "tr": 464, "uk": 464, "vi": 464, + "zh-CN": 0, "zh-TW": 464, } #: Keys every locale must carry regardless of the aggregate ratchet above. @@ -474,3 +476,70 @@ def unique_object(pairs): result[key] = value return result json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique_object) + + +# ── Runtime catalog (electron/src/renderer/src/i18n/locales) ───────────── +# The catalog the app actually loads gets the same integrity checks, and full +# parity rather than a ratchet: missing keys and orphans (zh-CN once carried +# four player.* keys English never defined) both fail. +_RENDERER_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "electron", "src", "renderer", "src", "i18n", "locales", +) +_RENDERER_LOCALES = sorted(f[:-5] for f in os.listdir(_RENDERER_DIR) if f.endswith(".json")) +_CLDR_PLURAL = re.compile(r"^(.*)_(?:zero|one|two|few|many|other)$") + + +def _renderer(name): + with open(os.path.join(_RENDERER_DIR, f"{name}.json"), encoding="utf-8") as fh: + return _flatten(json.load(fh, object_pairs_hook=_no_duplicates_hook)) + + +def _english_key(key, en): + """The en.json key a translation answers to; CLDR plural forms English + lacks (ar ``_few``, ru ``_many``) map to the English ``_other`` form.""" + if key in en: + return key + match = _CLDR_PLURAL.match(key) + if match and f"{match.group(1)}_other" in en: + return f"{match.group(1)}_other" + return None + + +def test_renderer_catalog_has_the_same_locales(): + assert _RENDERER_LOCALES == _LOCALES + + +@pytest.mark.parametrize("locale", [loc for loc in _RENDERER_LOCALES if loc != _EN]) +def test_renderer_locale_has_full_parity(locale): + en, loc = _renderer(_EN), _renderer(locale) + missing = sorted(set(en) - set(loc)) + orphans = sorted(k for k in loc if _english_key(k, en) is None) + assert not missing, f"{locale}.json lacks {len(missing)} en.json keys: {missing[:20]}" + assert not orphans, f"{locale}.json has keys en.json never defines: {orphans[:20]}" + + +@pytest.mark.parametrize("locale", _RENDERER_LOCALES) +def test_renderer_values_keep_their_placeholders(locale): + en, loc = _renderer(_EN), _renderer(locale) + problems = [] + for key, value in sorted(loc.items()): + english = _english_key(key, en) + if not isinstance(value, str) or english is None: + continue + want = set(_PLACEHOLDER.findall(en[english])) + got = set(_PLACEHOLDER.findall(value)) + dropped = want - got + if key.endswith(("_zero", "_one", "_two")): + dropped -= {"count"} + if key in _PLACEHOLDER_ALLOWLIST: + dropped = got = want = set() + if dropped or got - want: + problems.append(f" {key}: en={en[english]!r} vs {locale}={value!r}") + if _CORRUPTED_TOKEN.search(value): + problems.append(f" {key}: corrupted token in {value!r}") + if _PLACEHOLDER.search(value) and not re.search(r"\w", _PLACEHOLDER.sub("", value)): + problems.append(f" {key}: placeholder-only value {value!r}") + if key in _ENGINE_AGNOSTIC_KEYS and _ENGINE_BRANDS.search(value): + problems.append(f" {key}: stage label names an engine: {value!r}") + assert not problems, f"{locale}.json:\n" + "\n".join(problems[:25]) From 9bd8137eae06e93e82bee6bb486d94d9eb689fd9 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:21:21 +0530 Subject: [PATCH 39/85] ci(security): say gitleaks fails CI rather than blocking merge The secret scan is not a required check, so a hit fails the run; it does not mechanically block merging. --- .github/workflows/security.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 4fdf879e9..d20213128 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -1,14 +1,15 @@ # Security scanning — runs on every PR, on push to main, and weekly. # # Complements the CodeRabbit + Greptile app reviews (which fire on PR creation) -# with deterministic, gating checks: -# • gitleaks — secret scanning. HARD FAIL: a leaked credential blocks merge. +# with deterministic checks: +# • gitleaks — secret scanning. HARD FAIL: a leaked credential fails CI +# (not a required check; treat a red run as a merge blocker). # • CodeQL — Python + JS/TS SAST. Results land in the Security tab. # • bandit — Python SAST (SARIF → Security tab). Reporting, non-gating. # • pip-audit — Python dependency advisories. Reporting, non-gating. # • bun audit — Electron dependency advisories. Reporting, non-gating. # -# Only the secret scan gates the PR. Dependency advisories and bandit findings +# Only the secret scan fails the run. Dependency advisories and bandit findings # are surfaced as signal (Security tab / job log) rather than blocking every PR # on a transitive upstream advisory — consistent with the "no ceremony, # continuous-to-main" cadence. From 1a22ca89b66678d1a702c53719c358abaf2e36f0 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:21:21 +0530 Subject: [PATCH 40/85] ci(electron): drop the preview update feed from packaging No `preview` GitHub release exists, yet rehearsal builds stamped electron-preview-* channels whose updater URL, AppImage update info and zsync URL pointed at it. Rehearsal artifacts now carry the stable feed; the builder config, AppImage embedder, package contract and release verifier accept only the stable channel. --- .github/workflows/electron-build.yml | 3 +- electron/electron-builder.config.mjs | 10 ++-- electron/scripts/embed-appimage-update.mjs | 12 ++--- electron/tests/update-package-contract.mjs | 10 ++-- scripts/check_electron_release_assets.py | 12 ++--- tests/test_electron_release_assets.py | 58 ++++++++++++---------- 6 files changed, 56 insertions(+), 49 deletions(-) diff --git a/.github/workflows/electron-build.yml b/.github/workflows/electron-build.yml index 3cee8128a..51445dace 100644 --- a/.github/workflows/electron-build.yml +++ b/.github/workflows/electron-build.yml @@ -58,7 +58,8 @@ jobs: shell: bash env: VOICESTUDIO_RUST_TARGET: ${{ matrix.target }} - VOICESTUDIO_UPDATE_CHANNEL: electron-preview-${{ matrix.platform }}-${{ matrix.arch }} + # Rehearsal artifacts carry the stable feed; there is no preview release. + VOICESTUDIO_UPDATE_CHANNEL: electron-stable-${{ matrix.platform }}-${{ matrix.arch }} CSC_IDENTITY_AUTO_DISCOVERY: 'false' steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 diff --git a/electron/electron-builder.config.mjs b/electron/electron-builder.config.mjs index e0bd7fce9..7c34cb3bd 100644 --- a/electron/electron-builder.config.mjs +++ b/electron/electron-builder.config.mjs @@ -11,11 +11,15 @@ import { packageNativeHelper } from './native-helper-build.mjs'; const here = dirname(fileURLToPath(import.meta.url)); const { version } = JSON.parse(readFileSync(resolve(here, '../package.json'), 'utf-8')); +// Stable is the only Electron update feed: there is no `preview` release to +// point at. Packaging rehearsals from main are stamped stable too; they are +// review artifacts and are never published. const updateChannel = process.env.VOICESTUDIO_UPDATE_CHANNEL || `electron-stable-${process.platform}-${process.arch}`; -const updateUrl = updateChannel.startsWith('electron-preview-') - ? 'https://github.com/debpalash/VoiceStudio/releases/download/preview' - : 'https://github.com/debpalash/VoiceStudio/releases/latest/download'; +if (!/^electron-stable-(?:win32|darwin|linux)-(?:x64|arm64)$/.test(updateChannel)) { + throw new Error(`Unsupported Electron update channel: ${updateChannel}`); +} +const updateUrl = 'https://github.com/debpalash/VoiceStudio/releases/latest/download'; const defaultRustTarget = { diff --git a/electron/scripts/embed-appimage-update.mjs b/electron/scripts/embed-appimage-update.mjs index 0be2146dd..6d87617af 100644 --- a/electron/scripts/embed-appimage-update.mjs +++ b/electron/scripts/embed-appimage-update.mjs @@ -16,16 +16,12 @@ const filename = `VoiceStudio-Electron-${version}-linux-x64.AppImage`; const image = resolve(releaseDir, filename); const zsync = `${image}.zsync`; const channel = process.env.VOICESTUDIO_UPDATE_CHANNEL || 'electron-stable-linux-x64'; -assert( - ['electron-stable-linux-x64', 'electron-preview-linux-x64'].includes(channel), - 'Known Linux updater channel', -); -const preview = channel === 'electron-preview-linux-x64'; -const releaseTag = preview ? 'preview' : `v${version}`; +// Stable is the only Linux update feed (no `preview` release exists). +assert.equal(channel, 'electron-stable-linux-x64', 'Known Linux updater channel'); +const releaseTag = `v${version}`; const manifestPath = resolve(releaseDir, `${channel}-linux.yml`); const updateInfo = - `gh-releases-zsync|debpalash|VoiceStudio|${preview ? 'preview' : 'latest'}|` + - 'VoiceStudio-Electron-*-linux-x64.AppImage.zsync'; + 'gh-releases-zsync|debpalash|VoiceStudio|latest|VoiceStudio-Electron-*-linux-x64.AppImage.zsync'; // The pinned electron-builder runtime reserves a fixed-size .upd_info ELF // section. Read its actual offset instead of assuming a particular runtime diff --git a/electron/tests/update-package-contract.mjs b/electron/tests/update-package-contract.mjs index cbe659cd6..e916ae447 100644 --- a/electron/tests/update-package-contract.mjs +++ b/electron/tests/update-package-contract.mjs @@ -48,6 +48,7 @@ const channel = option( process.env.VOICESTUDIO_UPDATE_CHANNEL || `electron-stable-${platform}-${arch}`, ); const releaseDir = resolve(electronRoot, option('release', 'release')); +assert.equal(channel, `electron-stable-${platform}-${arch}`, 'Stable is the only update channel'); assert(['win32', 'darwin', 'linux'].includes(platform), `Supported platform: ${platform}`); assert(['x64', 'arm64'].includes(arch), `Supported architecture: ${arch}`); @@ -117,23 +118,22 @@ if (platform === 'win32') { /^\s*\[\s*\d+\]\s+\.static\s+PROGBITS/m, 'AppImage uses the FUSE2-independent static runtime', ); - const preview = channel === 'electron-preview-linux-x64'; const updateInfo = execFileSync(artifactPath, ['--appimage-updateinformation'], { encoding: 'utf8', }).trim(); assert.equal( updateInfo, - `gh-releases-zsync|debpalash|VoiceStudio|${preview ? 'preview' : 'latest'}|VoiceStudio-Electron-*-linux-x64.AppImage.zsync`, - 'External AppImage updaters follow the selected Linux release channel', + 'gh-releases-zsync|debpalash|VoiceStudio|latest|VoiceStudio-Electron-*-linux-x64.AppImage.zsync', + 'External AppImage updaters follow the stable Linux release feed', ); const control = `${artifactPath}.zsync`; assert(existsSync(control), 'Versioned AppImage zsync control file exists'); const header = readFileSync(control).subarray(0, 4096).toString('utf8'); assert( header.includes( - `URL: https://github.com/debpalash/VoiceStudio/releases/download/${preview ? 'preview' : `v${expectedVersion}`}/${fileUrl}\n`, + `URL: https://github.com/debpalash/VoiceStudio/releases/download/v${expectedVersion}/${fileUrl}\n`, ), - 'zsync downloads the matching release-channel artifact', + 'zsync downloads the matching release artifact', ); assert(header.includes(`Length: ${declaredSize}\n`), 'zsync describes final AppImage size'); assert(header.includes(`SHA-1: ${hashes.sha1}\n`), 'zsync verifies the final AppImage bytes'); diff --git a/scripts/check_electron_release_assets.py b/scripts/check_electron_release_assets.py index 6b2b78ee7..8218ef3d2 100644 --- a/scripts/check_electron_release_assets.py +++ b/scripts/check_electron_release_assets.py @@ -57,14 +57,14 @@ def _asset_map(release: dict[str, Any]) -> dict[str, dict[str, Any]]: def verify_release( release: dict[str, Any], manifest_dir: Path, *, channel: str, version: str ) -> list[str]: - if channel not in {"stable", "preview"}: + # Stable is the only Electron update feed; there is no `preview` release. + if channel != "stable": raise ReleaseContractError(f"unsupported channel: {channel}") - version_pattern = r"\d+\.\d+\.\d+-\d+" if channel == "preview" else r"\d+\.\d+\.\d+" - if re.fullmatch(version_pattern, version) is None: + if re.fullmatch(r"\d+\.\d+\.\d+", version) is None: raise ReleaseContractError(f"{channel} version has the wrong shape: {version}") - if bool(release.get("isPrerelease")) != (channel == "preview"): + if release.get("isPrerelease"): raise ReleaseContractError(f"{channel} prerelease flag does not match the channel") - expected_tag = "preview" if channel == "preview" else f"v{version}" + expected_tag = f"v{version}" if release.get("tagName") != expected_tag: raise ReleaseContractError(f"{channel} release tag does not match {expected_tag}") @@ -131,7 +131,7 @@ def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--release-json", type=Path, required=True) parser.add_argument("--manifest-dir", type=Path, required=True) - parser.add_argument("--channel", choices=("stable", "preview"), required=True) + parser.add_argument("--channel", choices=("stable",), required=True) parser.add_argument("--version", required=True) args = parser.parse_args() release = json.loads(args.release_json.read_text(encoding="utf-8")) diff --git a/tests/test_electron_release_assets.py b/tests/test_electron_release_assets.py index 789888f5b..4dfec0a6c 100644 --- a/tests/test_electron_release_assets.py +++ b/tests/test_electron_release_assets.py @@ -19,8 +19,8 @@ def _release( tmp_path: Path, *, - channel: str = "preview", - version: str = "0.5.3-144", + channel: str = "stable", + version: str = "0.5.3", with_arm64: bool = False, ): assets = [] @@ -52,28 +52,28 @@ def _release( if target.startswith("win32-"): assets.append({"name": f"{artifact}.blockmap", "size": 10, "digest": "sha256:map"}) return { - "tagName": "preview" if channel == "preview" else f"v{version}", - "isPrerelease": channel == "preview", + "tagName": f"v{version}", + "isPrerelease": False, "assets": assets, } def test_complete_four_target_release_passes(tmp_path: Path): - result = verify_release(_release(tmp_path), tmp_path, channel="preview", version="0.5.3-144") + result = verify_release(_release(tmp_path), tmp_path, channel="stable", version="0.5.3") assert len(result) == 4 assert {item.split(" -> ", 1)[0] for item in result} == { - "electron-preview-darwin-arm64-mac.yml", - "electron-preview-darwin-x64-mac.yml", - "electron-preview-linux-x64-linux.yml", - "electron-preview-win32-x64.yml", + "electron-stable-darwin-arm64-mac.yml", + "electron-stable-darwin-x64-mac.yml", + "electron-stable-linux-x64-linux.yml", + "electron-stable-win32-x64.yml", } def test_missing_platform_manifest_fails_closed(tmp_path: Path): release = _release(tmp_path) - (tmp_path / "electron-preview-darwin-arm64-mac.yml").unlink() + (tmp_path / "electron-stable-darwin-arm64-mac.yml").unlink() with pytest.raises(ReleaseContractError, match="missing downloaded manifest"): - verify_release(release, tmp_path, channel="preview", version="0.5.3-144") + verify_release(release, tmp_path, channel="stable", version="0.5.3") def test_published_payload_size_must_match_manifest(tmp_path: Path): @@ -83,19 +83,25 @@ def test_published_payload_size_must_match_manifest(tmp_path: Path): ) payload["size"] = 999 with pytest.raises(ReleaseContractError, match="published size"): - verify_release(release, tmp_path, channel="preview", version="0.5.3-144") + verify_release(release, tmp_path, channel="stable", version="0.5.3") -def test_preview_cannot_masquerade_as_stable(tmp_path: Path): +def test_a_prerelease_cannot_pass_as_stable(tmp_path: Path): release = _release(tmp_path) + release["isPrerelease"] = True with pytest.raises(ReleaseContractError, match="prerelease flag"): - verify_release(release, tmp_path, channel="stable", version="0.5.2") + verify_release(release, tmp_path, channel="stable", version="0.5.3") + +def test_there_is_no_preview_feed(tmp_path: Path): + """No `preview` GitHub release exists; a preview feed must not be verifiable.""" + with pytest.raises(ReleaseContractError, match="unsupported channel"): + verify_release(_release(tmp_path), tmp_path, channel="preview", version="0.5.3") -def test_preview_requires_the_numeric_run_stamp(tmp_path: Path): - release = _release(tmp_path, version="0.5.3") + +def test_stable_requires_a_plain_version(tmp_path: Path): with pytest.raises(ReleaseContractError, match="wrong shape"): - verify_release(release, tmp_path, channel="preview", version="0.5.3") + verify_release(_release(tmp_path), tmp_path, channel="stable", version="0.5.3-144") def test_stable_release_tag_must_match_the_manifest_version(tmp_path: Path): @@ -107,14 +113,14 @@ def test_stable_release_tag_must_match_the_manifest_version(tmp_path: Path): def test_windows_arm64_is_optional_when_wholly_absent(tmp_path: Path): """A failed experimental ARM64 leg must not block the four established targets.""" - result = verify_release(_release(tmp_path), tmp_path, channel="preview", version="0.5.3-144") + result = verify_release(_release(tmp_path), tmp_path, channel="stable", version="0.5.3") assert not any("win32-arm64" in item for item in result) def test_published_windows_arm64_is_verified_in_full(tmp_path: Path): release = _release(tmp_path, with_arm64=True) - result = verify_release(release, tmp_path, channel="preview", version="0.5.3-144") - assert "electron-preview-win32-arm64.yml -> VoiceStudio-Electron-0.5.3-144-win-arm64.exe" in result + result = verify_release(release, tmp_path, channel="stable", version="0.5.3") + assert "electron-stable-win32-arm64.yml -> VoiceStudio-Electron-0.5.3-win-arm64.exe" in result assert len(result) == 5 @@ -122,15 +128,15 @@ def test_half_published_windows_arm64_fails_closed(tmp_path: Path): release = _release(tmp_path, with_arm64=True) # Manifest published but its installer missing -> must not be waved through. release["assets"] = [ - a for a in release["assets"] if a["name"] != "VoiceStudio-Electron-0.5.3-144-win-arm64.exe" + a for a in release["assets"] if a["name"] != "VoiceStudio-Electron-0.5.3-win-arm64.exe" ] with pytest.raises(ReleaseContractError, match="missing updater payload"): - verify_release(release, tmp_path, channel="preview", version="0.5.3-144") + verify_release(release, tmp_path, channel="stable", version="0.5.3") # Asset published but manifest file not downloaded -> also an error. release = _release(tmp_path, with_arm64=True) - (tmp_path / "electron-preview-win32-arm64.yml").unlink() + (tmp_path / "electron-stable-win32-arm64.yml").unlink() with pytest.raises(ReleaseContractError, match="missing downloaded manifest"): - verify_release(release, tmp_path, channel="preview", version="0.5.3-144") + verify_release(release, tmp_path, channel="stable", version="0.5.3") @pytest.mark.parametrize("suffix", ["", ".blockmap"]) @@ -139,10 +145,10 @@ def test_windows_arm64_payload_without_a_manifest_is_rejected(tmp_path: Path, su release = _release(tmp_path) release["assets"].append( { - "name": f"VoiceStudio-Electron-0.5.3-144-win-arm64.exe{suffix}", + "name": f"VoiceStudio-Electron-0.5.3-win-arm64.exe{suffix}", "size": 10, "digest": "sha256:payload", } ) with pytest.raises(ReleaseContractError, match="missing downloaded manifest"): - verify_release(release, tmp_path, channel="preview", version="0.5.3-144") + verify_release(release, tmp_path, channel="stable", version="0.5.3") From efd951da62d1cf69436c8a6392a678d095f5b53c Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:21:59 +0530 Subject: [PATCH 41/85] fix(uninstall): remove the Electron app's folders, not only Tauri's The uninstall scripts only knew the Tauri identifier, so an Electron install kept its VoiceStudio app folder (managed Python runtime, logs), its updater cache and any runtime it created in a custom location. Both scripts now remove those on macOS, Windows and Linux, keep the legacy Tauri cleanup, and the Windows -RemoveApp flag runs the Electron uninstaller. The guide lists the Electron paths and the voicestudio-electron .deb package name. --- docs/install/uninstall.md | 97 +++++++++++------- scripts/uninstall.ps1 | 96 ++++++++++++++---- scripts/uninstall.sh | 67 +++++++++--- tests/scripts/test_uninstall_locations.py | 118 ++++++++++++++++++++++ 4 files changed, 308 insertions(+), 70 deletions(-) create mode 100644 tests/scripts/test_uninstall_locations.py diff --git a/docs/install/uninstall.md b/docs/install/uninstall.md index fd52e4a82..943aa8cab 100644 --- a/docs/install/uninstall.md +++ b/docs/install/uninstall.md @@ -7,7 +7,8 @@ and ships a script that finds and removes them for you (with a dry-run first). > **TL;DR (the space hogs):** the two folders worth deleting are the **model > cache** (the Hugging Face weights — several GB) and the **managed Python -> environment** (`project/.venv` — a few GB). Everything else is small. +> environment** (`runtime/project/.venv` inside the app folder — a few GB). +> Everything else is small. ## In the app (easiest — no repo needed) @@ -19,8 +20,8 @@ The Electron build finishes deletion through its signed desktop helper after the window exits, because Chromium keeps parts of its profile directory locked while the app is open. The same ownership checks and model-cache opt-in still apply. -This is the right path if you installed the **.dmg / .msi / AppImage** — you -don't have the repo, so the script below isn't available to you. +This is the right path if you installed the **.dmg / .exe / AppImage / .deb** — +you don't have the repo, so the script below isn't available to you. > **You may not need to uninstall.** Right above it, **Reset & remove** does the > same job at any scale you like — and leaves you with a working app instead of @@ -83,13 +84,19 @@ so on the console. Best-effort with a 2-second timeout: a dead network never blocks the uninstall. If you never opted in (the default), nothing is sent and nothing is printed; the dry-run never sends anything either way. +It covers the current desktop app and anything a final Tauri-era install +(`com.debpalash.omnivoice-studio`) left behind. Add `--app` (macOS/Linux) or +`-RemoveApp` (Windows) to also remove the installed app itself. + The script honors your custom locations: if you set `OMNIVOICE_DATA_DIR`, `OMNIVOICE_CACHE_DIR`, `HF_HOME`, or `HF_HUB_CACHE` (or picked custom data/model folders during setup), export the same variables before running it and it will target those instead of the defaults. It never touches anything -outside the VoiceStudio folders, and it does **not** delete the app binary itself -(see "Remove the app" below) — so it's safe to run even if you only want to -reclaim disk space and keep the app installed. +outside the VoiceStudio folders, and without `--app` / `-RemoveApp` it does +**not** delete the app itself (see "Remove the app" below) — so it's safe to run +even if you only want to reclaim disk space and keep the app installed. A +runtime you moved to a custom folder is removed only if the app created it +there; an existing environment it merely reused is left alone. ## What VoiceStudio writes, and where @@ -98,39 +105,53 @@ Four kinds of data, in up to four locations: | What | Size | Notes | |---|---|---| | **Model cache** (Hugging Face weights) | GBs | The big one. Shared HF cache — see the caveat below. | -| **Managed Python env** (`project/.venv`) | GBs | Rebuilt automatically if you reinstall. | -| **App data** (voices, projects, DB, generated audio, logs) | small–MBs | Your voice profiles and history live here. | -| **App config + logs** (`config.json`, window state, Tauri logs) | tiny | Settings and desktop-shell logs. | +| **Managed Python env** (`runtime/project/.venv` in the app folder) | GBs | Rebuilt automatically if you reinstall. | +| **App data** (voices, projects, DB, generated audio, sidecar engines, logs) | small–GBs | Your voice profiles and history live here. | +| **App folder** (window state, update settings, desktop logs, updater cache) | tiny | Desktop-shell state. | ### macOS ``` -~/Library/Application Support/OmniVoice/ ← app data (voices, projects, omnivoice.db, outputs, omnivoice.log) -~/Library/Application Support/com.debpalash.omnivoice-studio/ ← config.json + the managed Python env (project/.venv) -~/Library/Logs/OmniVoice/ ← backend logs (backend.log, backend_err.log) -~/Library/Logs/com.debpalash.omnivoice-studio/ ← desktop-shell log (tauri.log) -~/.config/omnivoice/ ← saved env file (cache location, HF token) -~/.cache/huggingface/ ← model weights (shared HF cache — see caveat) +~/Library/Application Support/OmniVoice/ ← app data (voices, projects, omnivoice.db, outputs, omnivoice.log) +~/Library/Application Support/VoiceStudio/ ← app folder + the managed Python env (runtime/project/.venv) +~/Library/Logs/VoiceStudio/ ← desktop-shell logs +~/Library/Caches/voicestudio-electron-updater/ ← downloaded updates +~/.config/omnivoice/ ← saved env file (cache location, HF token) +~/.cache/huggingface/ ← model weights (shared HF cache — see caveat) ``` ### Linux (AppImage / .deb) ``` -~/.omnivoice/ ← app data (voices, projects, omnivoice.db, outputs, omnivoice.log) -~/.local/share/com.debpalash.omnivoice-studio/ ← config.json, shell logs, AND the managed Python env (project/.venv) -~/.local/state/OmniVoice/ ← backend logs (backend.log, backend_err.log) -~/.config/omnivoice/ ← saved env file (cache location, HF token) -~/.cache/huggingface/ ← model weights (shared HF cache — see caveat) +~/.omnivoice/ ← app data (voices, projects, omnivoice.db, outputs, omnivoice.log) +~/.config/VoiceStudio/ ← app folder, desktop logs, AND the managed Python env (runtime/project/.venv) +~/.cache/voicestudio-electron-updater/ ← downloaded updates +~/.config/omnivoice/ ← saved env file (cache location, HF token) +~/.cache/huggingface/ ← model weights (shared HF cache — see caveat) ``` +`$XDG_CONFIG_HOME` / `$XDG_CACHE_HOME` replace `~/.config` / `~/.cache` when set. + ### Windows ``` -%APPDATA%\OmniVoice\ ← app data (voices, projects, omnivoice.db, outputs, omnivoice.log) -%LOCALAPPDATA%\com.debpalash.omnivoice-studio\ ← config.json, shell logs, AND the managed Python env (project\.venv) -%LOCALAPPDATA%\OmniVoice\Logs\ ← backend logs (backend.log, backend_err.log) -%USERPROFILE%\.config\omnivoice\ ← saved env file (cache location, HF token) -%LOCALAPPDATA%\OmniVoice\hf_cache\ ← model weights (VoiceStudio uses a short path here to dodge MAX_PATH) +%APPDATA%\OmniVoice\ ← app data (voices, projects, omnivoice.db, outputs, omnivoice.log) +%APPDATA%\VoiceStudio\ ← app folder, desktop logs, AND the managed Python env (runtime\project\.venv) +%LOCALAPPDATA%\voicestudio-electron-updater\ ← downloaded updates +%USERPROFILE%\.config\omnivoice\ ← saved env file (cache location, HF token) +%LOCALAPPDATA%\OmniVoice\hf_cache\ ← model weights (VoiceStudio uses a short path here to dodge MAX_PATH) +``` + +### Left behind by a Tauri-era install + +If you used VoiceStudio before the Electron desktop app, these may also exist; +the script removes them too: + +``` +macOS: ~/Library/Application Support/com.debpalash.omnivoice-studio/, ~/Library/Logs/OmniVoice/, + ~/Library/Logs/com.debpalash.omnivoice-studio/ +Linux: ~/.local/share/com.debpalash.omnivoice-studio/, ~/.local/state/VoiceStudio/ +Windows: %LOCALAPPDATA%\com.debpalash.omnivoice-studio\, %LOCALAPPDATA%\OmniVoice\Logs\ ``` On Windows, if `HF_HOME` isn't set, VoiceStudio redirects the model cache to @@ -141,15 +162,17 @@ model paths don't hit the 260-character `MAX_PATH` limit. - **Custom folders:** if you chose a custom data or model folder in setup (or set `OMNIVOICE_DATA_DIR` / `OMNIVOICE_CACHE_DIR` / `HF_HOME` / - `HF_HUB_CACHE`), your data is there instead of the defaults above. + `HF_HUB_CACHE`), your data is there instead of the defaults above. A Python + runtime you moved to another drive lives in the `VoiceStudio` folder you + picked. - **Portable mode:** everything lives in an `OmniVoiceStudio-Data/` folder next to the app binary — delete that one folder and you're done. - **App-managed engine sidecars:** if VoiceStudio installed IndexTTS 2.5, - CosyVoice, or another sidecar, its isolated venv lives under the app-config - folder above and is removed with it. A user-managed IndexTTS checkout set via - `OMNIVOICE_INDEXTTS_DIR` is preserved only when its resolved path is outside - the app-config folder. Verify the resolved path before cleanup: anything - inside the app-config folder is removed with it; retain or delete an external + CosyVoice, or another sidecar, its isolated venv lives under `engines/` in the + app-data folder above and is removed with it. A user-managed IndexTTS checkout + set via `OMNIVOICE_INDEXTTS_DIR` is preserved only when its resolved path is + outside the app-data folder. Verify the resolved path before cleanup: anything + inside the app-data folder is removed with it; retain or delete an external checkout separately. > **Shared HF cache caveat:** `~/.cache/huggingface/` is the **standard Hugging @@ -173,13 +196,13 @@ normal per-platform step: without administrator approval. - **Linux (AppImage):** delete the `.AppImage` file. If you integrated it into your menu (e.g. with AppImageLauncher or a hand-written `.desktop` file), - also remove `~/.local/share/applications/*omnivoice*.desktop` and any icon + also remove `~/.local/share/applications/*voicestudio*.desktop` (or `*omnivoice*.desktop` from an older install) and any icon under `~/.local/share/icons/`. -- **Linux (.deb):** `sudo apt remove voicestudio` (this removes the program; - your data folders above are user data and are left in place — delete them - with the script or by hand). Installed before the rename? The old package is - called `omnivoice-studio` — the two are separate packages, so - `sudo apt remove omnivoice-studio` removes the earlier one. +- **Linux (.deb):** `sudo apt remove voicestudio-electron` (this removes the + program; your data folders above are user data and are left in place — + delete them with the script or by hand). An older Tauri-era package may also + be installed: `sudo apt remove voicestudio`, or `omnivoice-studio` from before + the rename — they are separate packages. ## Reinstalling later diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 6dd39c9fb..a8c7ab09f 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -8,9 +8,13 @@ cache, prints each with its size, and removes them. Dry-run by default: it prints what it WOULD delete and stops, so you always see the plan first. - It NEVER deletes the app binary itself (uninstall that via Settings > Apps), - and never touches anything outside the paths it lists. Mirrors - backend/core/config.py and the Electron setup flow. + Covers the Electron desktop app (its VoiceStudio app folder: managed Python + runtime, window state, logs, updater cache) and the folders a final Tauri + install left behind (com.debpalash.omnivoice-studio). + + Without -RemoveApp it never deletes the app itself (uninstall that via + Settings > Apps), and never touches anything outside the paths it lists. + Mirrors backend/core/config.py and electron/src/main/backend.ts. .EXAMPLE powershell -ExecutionPolicy Bypass -File scripts\uninstall.ps1 @@ -25,9 +29,18 @@ param( ) $ErrorActionPreference = 'Stop' -$identifier = 'com.debpalash.omnivoice-studio' - -# Prebuilt app from the default `irm ... | iex` install: an MSI product. +# Final Tauri installs used this identifier for their config + Python env. +$legacyIdentifier = 'com.debpalash.omnivoice-studio' +# Electron app folder (app.getPath('userData'); electron/src/main/app-identity.ts) +# and electron-updater's download cache (package name + "-updater"). +$electronAppName = 'VoiceStudio' +$electronUpdaterCache = 'voicestudio-electron-updater' + +# The installed app: the Electron NSIS installer (default `irm ... | iex` +# install) registers "Uninstall VoiceStudio.exe"; a final Tauri install is an +# MSI product. +$nsisUninstaller = $null +$nsisScope = $null $msiProduct = $null if ($RemoveApp) { $uninstallKeys = @( @@ -35,8 +48,19 @@ if ($RemoveApp) { 'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' ) - $msiProduct = Get-ItemProperty $uninstallKeys -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -match 'VoiceStudio|OmniVoice' } | + $entries = @(Get-ItemProperty $uninstallKeys -ErrorAction SilentlyContinue | + Where-Object { $_.DisplayName -match 'VoiceStudio|OmniVoice' }) + foreach ($entry in $entries) { + if ([string]$entry.UninstallString -match '^"([^"]+\.exe)"(?:\s+(/currentuser|/allusers))?\s*$' -and + (Split-Path $Matches[1] -Leaf) -eq 'Uninstall VoiceStudio.exe' -and + (Test-Path -LiteralPath $Matches[1] -PathType Leaf)) { + $nsisUninstaller = $Matches[1] + $nsisScope = $Matches[2] + break + } + } + $msiProduct = $entries | + Where-Object { [string]$_.UninstallString -match '(?i)msiexec' -and $_.PSChildName -match '^\{[0-9A-Fa-f-]+\}$' } | Select-Object -First 1 } @@ -45,7 +69,10 @@ $appData = [Environment]::GetEnvironmentVariable('APPDATA') $localApp = [Environment]::GetEnvironmentVariable('LOCALAPPDATA') $dataDefault = Join-Path $appData 'OmniVoice' -$configDefault = Join-Path $localApp $identifier +# Electron keeps its runtime, window state and logs in its app folder. +$electronUserData = Join-Path $appData $electronAppName +$electronCache = Join-Path $localApp $electronUpdaterCache +$legacyConfig = @((Join-Path $localApp $legacyIdentifier), (Join-Path $appData $legacyIdentifier)) # Windows model-cache default: OmniVoice redirects HF cache to a short path to # dodge MAX_PATH, unless HF_HOME is set (see backend/core/config.py). $modelsDefault = Join-Path (Join-Path $localApp 'OmniVoice') 'hf_cache' @@ -70,11 +97,31 @@ function Get-FolderSize($path) { } catch { return '?' } } -# The BACKEND writes its own logs here (backend_log_path() in -# src-tauri/src/backend.rs) — a sibling of hf_cache under %LOCALAPPDATA%\OmniVoice, -# so it is covered by neither the app-data nor the config dir. +# Final Tauri installs wrote backend logs here — a sibling of hf_cache under +# %LOCALAPPDATA%\OmniVoice, so it is covered by neither the app-data nor the +# config dir. $logsDefault = Join-Path (Join-Path $localApp 'OmniVoice') 'Logs' +# A custom runtime location the Electron app created (and therefore owns) is +# recorded in runtime-location.json. Only an owned, absolute folder named +# VoiceStudio is removed — the same rule the in-app uninstall applies; a reused +# Tauri environment is recorded unowned and kept. +$electronRuntime = $null +try { + $locationFile = Join-Path $electronUserData 'runtime-location.json' + if (Test-Path -LiteralPath $locationFile) { + $location = Get-Content -LiteralPath $locationFile -Raw | ConvertFrom-Json + $root = [string]$location.root + if ($location.owned -eq $true -and [System.IO.Path]::IsPathRooted($root) -and + (Split-Path $root -Leaf) -eq 'VoiceStudio' -and + $root.TrimEnd('\') -ne (Join-Path $electronUserData 'runtime')) { + $electronRuntime = $root + } + } +} catch { + # A malformed location file only means there is no custom runtime to list. +} + # Durable per-user env file — backend/core/user_env.py uses expanduser('~/.config/ # omnivoice/env') on EVERY OS, so it lands under %USERPROFILE% on Windows too. It # persists OMNIVOICE_CACHE_DIR (and can hold HF_TOKEN); leaving it behind silently @@ -82,9 +129,10 @@ $logsDefault = Join-Path (Join-Path $localApp 'OmniVoice') 'Logs' $userEnvDir = Join-Path ([Environment]::GetEnvironmentVariable('USERPROFILE')) '.config\omnivoice' $appTargets = @() -foreach ($p in @($dataDir, $configDefault, $logsDefault, $userEnvDir)) { - if (Test-Path -LiteralPath $p) { $appTargets += $p } +foreach ($p in @($dataDir, $electronUserData, $electronRuntime, $electronCache) + $legacyConfig + @($logsDefault, $userEnvDir)) { + if ($p -and (Test-Path -LiteralPath $p)) { $appTargets += $p } } +if ($RemoveApp -and $nsisUninstaller) { $appTargets += "App: VoiceStudio ($nsisUninstaller)" } if ($RemoveApp -and $msiProduct) { $appTargets += "MSI product: $($msiProduct.DisplayName)" } Write-Host 'VoiceStudio uninstaller (Windows)' @@ -95,7 +143,7 @@ if ($appTargets.Count -eq 0) { } else { Write-Host 'App data, managed Python env, config, and logs:' foreach ($t in $appTargets) { - if ($t -like 'MSI product:*') { Write-Host " {-} $t" } + if ($t -like 'MSI product:*' -or $t -like 'App: *') { Write-Host " {-} $t" } else { ' {0,-9} {1}' -f (Get-FolderSize $t), $t | Write-Host } } } @@ -113,7 +161,7 @@ if (-not $Yes) { Write-Host 'DRY RUN — nothing deleted. Re-run with -Yes to remove the listed folders' if ($modelsPresent) { Write-Host ' (add -Models to also remove the shared model cache).' } if (-not $RemoveApp) { - Write-Host ' (add -RemoveApp to also uninstall the prebuilt app via msiexec).' + Write-Host ' (add -RemoveApp to also uninstall the installed app).' Write-Host ' Or manually: Settings > Apps > VoiceStudio > Uninstall' Write-Host ' (listed as "OmniVoice Studio" if you have not updated since the rename).' } @@ -156,7 +204,7 @@ try { $deleted = 0 foreach ($t in $appTargets) { - if ($t -like 'MSI product:*') { continue } + if ($t -like 'MSI product:*' -or $t -like 'App: *') { continue } Write-Host "Removing $t" Remove-Item -LiteralPath $t -Recurse -Force -ErrorAction SilentlyContinue $deleted++ @@ -168,11 +216,19 @@ if ($Models -and $modelsPresent) { } elseif ($modelsPresent) { Write-Host "Kept model cache ($modelsDir) — re-run with -Models to remove it." } +if ($RemoveApp -and $nsisUninstaller) { + Write-Host 'Uninstalling VoiceStudio...' + $arguments = @('/S') + if ($nsisScope) { $arguments = @($nsisScope) + $arguments } + $process = Start-Process -FilePath $nsisUninstaller -ArgumentList $arguments -Wait -PassThru + if ($process.ExitCode -in @(0, 3010)) { Write-Host 'VoiceStudio uninstalled.' } + else { Write-Host "The uninstaller exited with $($process.ExitCode) — remove it via Settings > Apps if it is still listed." } +} if ($RemoveApp -and $msiProduct) { Write-Host "Uninstalling $($msiProduct.DisplayName) via msiexec..." - Start-Process msiexec.exe -ArgumentList '/x', $msiProduct.PSChildName, '/norestart', '/qn' -Wait - if ($LASTEXITCODE -eq 0) { Write-Host 'MSI product uninstalled.' } - else { Write-Host "msiexec exited with $LASTEXITCODE — remove it via Settings > Apps if it is still listed." } + $process = Start-Process msiexec.exe -ArgumentList '/x', $msiProduct.PSChildName, '/norestart', '/qn' -Wait -PassThru + if ($process.ExitCode -in @(0, 3010)) { Write-Host 'MSI product uninstalled.' } + else { Write-Host "msiexec exited with $($process.ExitCode) — remove it via Settings > Apps if it is still listed." } } Write-Host '' diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 8033e99b2..24b28f2a4 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -15,9 +15,14 @@ # OMNIVOICE_DATA_DIR, OMNIVOICE_CACHE_DIR, HF_HOME, HF_HUB_CACHE # Export the ones you set for VoiceStudio before running, and it targets those. # -# It NEVER deletes the app binary itself (that's a per-platform step — see -# docs/install/uninstall.md), and it never touches anything outside the paths -# it lists. Mirrors backend/core/config.py and the Electron setup flow. +# Covers the Electron desktop app (its VoiceStudio app folder: managed Python +# runtime, window state, logs, updater cache) and the folders a final Tauri +# install left behind (com.debpalash.omnivoice-studio). +# +# Without --app it never deletes the app binary itself (that's a per-platform +# step — see docs/install/uninstall.md), and it never touches anything outside +# the paths it lists. Mirrors backend/core/config.py and +# electron/src/main/backend.ts. set -euo pipefail APPLY=0 @@ -35,28 +40,46 @@ for arg in "$@"; do esac done -IDENTIFIER="com.debpalash.omnivoice-studio" +# Final Tauri installs used this identifier for their config + Python env. +LEGACY_IDENTIFIER="com.debpalash.omnivoice-studio" +# Electron app folder (app.getPath('userData'); electron/src/main/app-identity.ts) +# and electron-updater's download cache (package name + "-updater"). +ELECTRON_APP_NAME="VoiceStudio" +ELECTRON_UPDATER_CACHE="voicestudio-electron-updater" OS="$(uname -s)" # ── Resolve platform default paths (mirrors the app) ──────────────────────── data_default="" -config_default="" +electron_user_data="" +legacy_config=() logs_extra=() models_default="" case "$OS" in Darwin) data_default="$HOME/Library/Application Support/OmniVoice" - config_default="$HOME/Library/Application Support/$IDENTIFIER" - logs_extra=("$HOME/Library/Logs/OmniVoice" "$HOME/Library/Logs/$IDENTIFIER") + electron_user_data="$HOME/Library/Application Support/$ELECTRON_APP_NAME" + legacy_config=("$HOME/Library/Application Support/$LEGACY_IDENTIFIER") + logs_extra=( + "$HOME/Library/Logs/$ELECTRON_APP_NAME" + "$HOME/Library/Caches/$ELECTRON_UPDATER_CACHE" + "$HOME/Library/Logs/OmniVoice" + "$HOME/Library/Logs/$LEGACY_IDENTIFIER" + ) models_default="$HOME/.cache/huggingface" ;; Linux) data_default="$HOME/.omnivoice" - config_default="${XDG_DATA_HOME:-$HOME/.local/share}/$IDENTIFIER" - # The BACKEND writes its own logs outside the app-data dir — see - # backend_log_path() in src-tauri/src/backend.rs. Missing this left a stray - # log dir behind on every Linux uninstall. - logs_extra=("${XDG_STATE_HOME:-$HOME/.local/state}/VoiceStudio") + # Electron keeps its logs inside the app folder on Linux. + electron_user_data="${XDG_CONFIG_HOME:-$HOME/.config}/$ELECTRON_APP_NAME" + legacy_config=( + "${XDG_DATA_HOME:-$HOME/.local/share}/$LEGACY_IDENTIFIER" + "${XDG_CONFIG_HOME:-$HOME/.config}/$LEGACY_IDENTIFIER" + ) + # Final Tauri installs wrote backend logs outside the app-data dir. + logs_extra=( + "${XDG_CACHE_HOME:-$HOME/.cache}/$ELECTRON_UPDATER_CACHE" + "${XDG_STATE_HOME:-$HOME/.local/state}/VoiceStudio" + ) models_default="$HOME/.cache/huggingface" ;; *) @@ -73,6 +96,22 @@ MODELS_DIR="${OMNIVOICE_CACHE_DIR:-${HF_HOME:-${HF_HUB_CACHE:-$models_default}}} # silently redirected a fresh reinstall's model cache to the old location. USER_ENV_DIR="$HOME/.config/omnivoice" +# A custom runtime location the Electron app created (and therefore owns) is +# recorded in runtime-location.json. Only an owned, absolute folder named +# VoiceStudio is removed — the same rule the in-app uninstall applies; a reused +# Tauri environment is recorded unowned and kept. +electron_owned_runtime() { + local file="$electron_user_data/runtime-location.json" root + [ -f "$file" ] || return 0 + grep -q '"owned"[[:space:]]*:[[:space:]]*true' "$file" 2>/dev/null || return 0 + root=$(sed -n 's/.*"root"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p' "$file" | head -n1) + case "$root" in /*) ;; *) return 0 ;; esac + [ "$root" = "$electron_user_data/runtime" ] && return 0 + [ "$(basename "$root" | tr '[:upper:]' '[:lower:]')" = "voicestudio" ] || return 0 + printf '%s\n' "$root" +} +ELECTRON_RUNTIME="$(electron_owned_runtime)" + # ── Collect existing targets ──────────────────────────────────────────────── app_targets=() @@ -91,7 +130,9 @@ if [ "$REMOVE_APP" -eq 1 ]; then fi [ -e "$DATA_DIR" ] && app_targets+=("$DATA_DIR") -[ -e "$config_default" ] && app_targets+=("$config_default") +[ -e "$electron_user_data" ] && app_targets+=("$electron_user_data") +[ -n "$ELECTRON_RUNTIME" ] && [ -e "$ELECTRON_RUNTIME" ] && app_targets+=("$ELECTRON_RUNTIME") +for d in "${legacy_config[@]}"; do [ -e "$d" ] && app_targets+=("$d"); done [ -e "$USER_ENV_DIR" ] && app_targets+=("$USER_ENV_DIR") for d in "${logs_extra[@]:-}"; do [ -n "$d" ] && [ -e "$d" ] && app_targets+=("$d"); done diff --git a/tests/scripts/test_uninstall_locations.py b/tests/scripts/test_uninstall_locations.py new file mode 100644 index 000000000..1c43018be --- /dev/null +++ b/tests/scripts/test_uninstall_locations.py @@ -0,0 +1,118 @@ +"""The uninstall scripts must find the Electron app's folders, not only the +retired Tauri ones. + +The Electron shell keeps its managed Python runtime, window state, logs and +updater cache under its VoiceStudio app folder (app.getPath('userData'), +electron/src/main/backend.ts). A script that only knew the Tauri identifier +left gigabytes behind on every Electron uninstall. + +uninstall.sh runs for real against a throwaway HOME; uninstall.ps1 is pinned by +static checks because PowerShell is not available on every CI platform. +""" +from __future__ import annotations + +import json +import os +import platform +import subprocess +import sys + +import pytest + +REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +SH = os.path.join(REPO, "scripts", "uninstall.sh") +PS1 = os.path.join(REPO, "scripts", "uninstall.ps1") + + +def _layout(tmp_path): + home = tmp_path / "home" + if platform.system() == "Darwin": + user_data = home / "Library/Application Support/VoiceStudio" + logs = home / "Library/Logs/VoiceStudio" + legacy = home / "Library/Application Support/com.debpalash.omnivoice-studio" + else: + user_data = home / ".config/VoiceStudio" + logs = user_data / "logs" + legacy = home / ".local/share/com.debpalash.omnivoice-studio" + for path in (user_data / "runtime/project/.venv", logs, legacy / "project"): + path.mkdir(parents=True, exist_ok=True) + return home, user_data, logs, legacy + + +def _run(tmp_path, home, *args): + env = { + key: value + for key, value in os.environ.items() + if not key.startswith(("XDG_", "OMNIVOICE_", "HF_")) + } + env.update( + { + "HOME": str(home), + "OMNIVOICE_DATA_DIR": str(tmp_path / "data-nonexistent"), + "OMNIVOICE_CACHE_DIR": str(tmp_path / "models-nonexistent"), + } + ) + return subprocess.run( + ["bash", SH, *args], capture_output=True, text=True, env=env, timeout=60 + ) + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="bash script is macOS/Linux-only") +def test_sh_removes_electron_and_legacy_tauri_folders(tmp_path): + home, user_data, logs, legacy = _layout(tmp_path) + owned = tmp_path / "fast-disk" / "VoiceStudio" + (owned / "project/.venv").mkdir(parents=True) + (user_data / "runtime-location.json").write_text(json.dumps({"root": str(owned), "owned": True})) + + dry = _run(tmp_path, home) + assert dry.returncode == 0, dry.stderr + for path in (user_data, owned, legacy): + assert str(path) in dry.stdout + assert user_data.exists() and owned.exists(), "the dry run must delete nothing" + + applied = _run(tmp_path, home, "--yes") + assert applied.returncode == 0, applied.stderr + assert not user_data.exists() + assert not owned.exists() + assert not legacy.exists() + assert not logs.exists() + + +@pytest.mark.skipif(sys.platform.startswith("win"), reason="bash script is macOS/Linux-only") +@pytest.mark.parametrize( + "location", + [ + {"owned": False}, # a reused Tauri environment is never Electron's to delete + {"owned": True, "name": "Python"}, # not a folder the app created + ], +) +def test_sh_keeps_runtime_roots_the_app_does_not_own(tmp_path, location): + home, user_data, _, _ = _layout(tmp_path) + root = tmp_path / "elsewhere" / location.get("name", "VoiceStudio") + root.mkdir(parents=True) + (user_data / "runtime-location.json").write_text( + json.dumps({"root": str(root), "owned": location["owned"]}) + ) + + applied = _run(tmp_path, home, "--yes") + assert applied.returncode == 0, applied.stderr + assert root.exists() + assert not user_data.exists() + + +def test_scripts_no_longer_cite_the_removed_tauri_backend(): + for path in (SH, PS1): + with open(path, encoding="utf-8-sig") as handle: + assert "src-tauri" not in handle.read() + + +def test_ps1_targets_electron_folders_and_its_uninstaller(): + with open(PS1, encoding="utf-8-sig") as handle: + text = handle.read() + assert "$electronAppName = 'VoiceStudio'" in text + assert "Join-Path $appData $electronAppName" in text + assert "$electronUpdaterCache = 'voicestudio-electron-updater'" in text + assert "runtime-location.json" in text and "$location.owned -eq $true" in text + assert "'Uninstall VoiceStudio.exe'" in text + # The legacy Tauri cleanup stays. + assert "$legacyIdentifier = 'com.debpalash.omnivoice-studio'" in text From 010299b430176f0bfdc724a52071d466912cb35f Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:24:03 +0530 Subject: [PATCH 42/85] fix(speech): retire native dictation control aimed at a dead port backend.speech_client defaulted its control commands to 127.0.0.1:3902, the retired Tauri control server and now the Electron dev server's port. status, start, stop, toggle and transcribe --insert now exit with an explanation instead of calling it; capabilities reads the backend's discovery document and transcribe is unchanged. The unused VOICESTUDIO_SPEECH_CONTROL_PORT plumbing, the Herdr example and the docs describing the control server are removed. --- backend/api/routers/speech_platform.py | 21 +--- backend/speech_client/__main__.py | 113 +++++++-------------- docs/agentic-voice.md | 7 +- docs/features/dictation.md | 6 +- docs/speech-platform.md | 107 +++++-------------- examples/README.md | 1 - examples/speech-platform/herdr-config.toml | 21 ---- tests/test_speech_client.py | 67 +++++------- tests/test_speech_platform.py | 11 +- 9 files changed, 106 insertions(+), 248 deletions(-) delete mode 100644 examples/speech-platform/herdr-config.toml diff --git a/backend/api/routers/speech_platform.py b/backend/api/routers/speech_platform.py index 61da6a988..bb3b5e1b2 100644 --- a/backend/api/routers/speech_platform.py +++ b/backend/api/routers/speech_platform.py @@ -6,7 +6,6 @@ """ from __future__ import annotations -import os from typing import Literal from fastapi import APIRouter @@ -110,20 +109,6 @@ def speech_capabilities() -> SpeechCapabilities: protocol="mcp", ), } - native_control = False - try: - control_port = int(os.environ.get("VOICESTUDIO_SPEECH_CONTROL_PORT", "")) - except (TypeError, ValueError): - control_port = 0 - if 0 < control_port <= 65535: - native_control = True - endpoints["native_dictation_control"] = EndpointCapability( - path=f"http://127.0.0.1:{control_port}/v1/capabilities", - transport="http", - method="GET", - protocol=SPEECH_PROTOCOL, - ) - return SpeechCapabilities( endpoints=endpoints, stream_input=StreamInputCapability( @@ -138,9 +123,9 @@ def speech_capabilities() -> SpeechCapabilities: events=["session.started", "status", "partial", "final", "error"], final_kinds=["utterance", "summary"], ), - features=SpeechFeatureCapabilities( - native_dictation_control=native_control, - ), + # Native dictation control was served by the retired Tauri shell; + # the field stays in the v1 schema and is always false. + features=SpeechFeatureCapabilities(native_dictation_control=False), authentication=SpeechAuthCapabilities(), ) diff --git a/backend/speech_client/__main__.py b/backend/speech_client/__main__.py index f1abb5c2a..b2099f55a 100644 --- a/backend/speech_client/__main__.py +++ b/backend/speech_client/__main__.py @@ -1,7 +1,12 @@ """CLI/module bridge for terminals, editor extensions, and agent hooks. -The desktop app must be running for native dictation control. Batch -transcription can also target a standalone or remote VoiceStudio backend. +Transcribes files and reads the speech capability document from a local, +standalone or remote VoiceStudio backend. + +Native dictation control (start/stop/toggle/status and ``--insert``) was served +by the retired Tauri desktop shell on port 3902. The Electron app has no such +control API, so those commands now fail with an explanation instead of hitting +whatever else listens on that port (the Electron dev server does). """ from __future__ import annotations @@ -17,7 +22,6 @@ from urllib import error, request from urllib.parse import urlsplit -DEFAULT_CONTROL_URL = "http://127.0.0.1:3902" DEFAULT_ENGINE_URL = "http://127.0.0.1:3900" @@ -141,16 +145,18 @@ def _encode_multipart( return b"".join(parts), f"multipart/form-data; boundary={boundary}" -def _control(args: argparse.Namespace, action: str) -> int: - method = "GET" if action in {"status", "capabilities"} else "POST" - path = { - "status": "/v1/status", - "capabilities": "/v1/capabilities", - "start": "/v1/dictation/start", - "stop": "/v1/dictation/stop", - "toggle": "/v1/dictation/toggle", - }[action] - result = _json_request(method, _join_url(args.control_url, path)) +RETIRED_CONTROL_COMMANDS = ("status", "start", "stop", "toggle") +NATIVE_CONTROL_RETIRED = ( + "native dictation control is not available: it was served by the retired " + "Tauri desktop app, and the Electron app has no control API. Use the in-app " + "dictation shortcut, or stream audio to /v1/audio/transcriptions/stream." +) + + +def _capabilities(args: argparse.Namespace) -> int: + result = _json_request( + "GET", _join_url(args.engine_url, "/.well-known/voicestudio-speech") + ) print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 @@ -167,20 +173,9 @@ def _read_audio(path: str, stdin_filename: str) -> tuple[bytes, str]: raise SpeechClientError(f"could not read '{display_name}': {reason}") from exc -def _response_text(body: bytes, content_type: str) -> str: - decoded = body.decode("utf-8", errors="replace") - if "json" not in content_type.lower(): - return decoded - try: - payload = json.loads(decoded) - except json.JSONDecodeError: - return decoded - if isinstance(payload, dict) and isinstance(payload.get("text"), str): - return payload["text"] - return decoded - - def _transcribe(args: argparse.Namespace) -> int: + if args.insert: + raise SpeechClientError(NATIVE_CONTROL_RETIRED) audio, filename = _read_audio(args.audio, args.stdin_filename) fields = { "model": args.model, @@ -194,44 +189,14 @@ def _transcribe(args: argparse.Namespace) -> int: if api_key: headers["Authorization"] = f"Bearer {api_key}" - output_session_id = None - if args.insert: - session = _json_request( - "POST", _join_url(args.control_url, "/v1/output/sessions") + response_body, _ = _open( + request.Request( + _join_url(args.engine_url, "/v1/audio/transcriptions"), + data=body, + headers=headers, + method="POST", ) - output_session_id = session["session_id"] - - session_needs_cleanup = output_session_id is not None - try: - response_body, response_type = _open( - request.Request( - _join_url(args.engine_url, "/v1/audio/transcriptions"), - data=body, - headers=headers, - method="POST", - ) - ) - if output_session_id is not None: - _json_request( - "POST", - _join_url( - args.control_url, - f"/v1/output/sessions/{output_session_id}/insert", - ), - {"text": _response_text(response_body, response_type)}, - ) - session_needs_cleanup = False - finally: - if session_needs_cleanup: - try: - _json_request( - "DELETE", - _join_url(args.control_url, f"/v1/output/sessions/{output_session_id}"), - ) - except Exception: # noqa: BLE001 - # Best-effort cleanup must not replace the original failure or - # KeyboardInterrupt that brought control into this finally. - pass + ) sys.stdout.buffer.write(response_body) if response_body and not response_body.endswith(b"\n"): @@ -242,19 +207,17 @@ def _transcribe(args: argparse.Namespace) -> int: def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="voicestudio-speech", - description="Control and consume VoiceStudio's local speech platform.", - ) - parser.add_argument( - "--control-url", - default=os.environ.get("VOICESTUDIO_SPEECH_URL", DEFAULT_CONTROL_URL), + description="Consume VoiceStudio's local speech platform.", ) parser.add_argument( "--engine-url", default=_default_engine_url(), ) subparsers = parser.add_subparsers(dest="command", required=True) - for command in ("status", "capabilities", "start", "stop", "toggle"): - subparsers.add_parser(command) + subparsers.add_parser("capabilities", help="print the backend's speech capabilities") + for command in RETIRED_CONTROL_COMMANDS: + # Kept so existing hooks get the explanation, not an argparse error. + subparsers.add_parser(command, help=argparse.SUPPRESS) transcribe = subparsers.add_parser("transcribe") transcribe.add_argument("audio", help="audio file, or - for stdin") @@ -267,11 +230,7 @@ def _parser() -> argparse.ArgumentParser: choices=("json", "text", "verbose_json", "srt", "vtt"), default="text", ) - transcribe.add_argument( - "--insert", - action="store_true", - help="insert the result into the app focused when this command starts", - ) + transcribe.add_argument("--insert", action="store_true", help=argparse.SUPPRESS) return parser @@ -280,7 +239,9 @@ def main(argv: list[str] | None = None) -> int: try: if args.command == "transcribe": return _transcribe(args) - return _control(args, args.command) + if args.command == "capabilities": + return _capabilities(args) + raise SpeechClientError(NATIVE_CONTROL_RETIRED) except (SpeechClientError, KeyError) as exc: print(f"voicestudio-speech: {exc}", file=sys.stderr) return 2 diff --git a/docs/agentic-voice.md b/docs/agentic-voice.md index 0b8e8a94a..d29c22842 100644 --- a/docs/agentic-voice.md +++ b/docs/agentic-voice.md @@ -1,13 +1,14 @@ # Agentic voice: VoiceStudio as a TTS/STT provider VoiceStudio exposes a **local speech platform**—OpenAI-compatible batch audio, -a versioned transcription WebSocket, native dictation control, and MCP—so any agent framework that +a versioned transcription WebSocket, and MCP—so any agent framework that speaks to OpenAI's audio endpoints can use your local VoiceStudio for speech — in your own cloned voice, with nothing leaving your machine. You bring the agent runtime; VoiceStudio is the voice. -For dictating directly into Claude Code, Codex, Pi, Antigravity CLI, Herdr, or -another focused prompt, use the [Rust control sidecar](speech-platform.md). +For dictating directly into Claude Code, Codex, Pi, Antigravity CLI, or +another focused prompt, use the app's dictation shortcut; see the +[local speech platform](speech-platform.md) for the streaming API. This is "agentic v1": VoiceStudio is a provider, not the orchestrator. You wire your own agent (a support line, a desk assistant, a Discord persona) and point diff --git a/docs/features/dictation.md b/docs/features/dictation.md index c4042cc61..8cc4d58a9 100644 --- a/docs/features/dictation.md +++ b/docs/features/dictation.md @@ -4,10 +4,8 @@ VoiceStudio dictation records from the system-wide shortcut, transcribes locally, and—where the desktop permits it—inserts the result into the app where the shortcut was pressed. The pill never needs keyboard focus. -The same flow is available to other applications through the bundled Rust -control sidecar. Herdr actions, editor extensions, agent hooks, and scripts can -start or stop VoiceStudio's capture over loopback HTTP/JSON-RPC or stream their -own microphone audio to the versioned WebSocket API. See the +Editor extensions, agent hooks, and scripts can stream their own microphone +audio to the versioned WebSocket API or transcribe files over HTTP. See the [local speech platform](../speech-platform.md) for the protocol and examples. ## Use it diff --git a/docs/speech-platform.md b/docs/speech-platform.md index c04a07f6a..445dd1dad 100644 --- a/docs/speech-platform.md +++ b/docs/speech-platform.md @@ -1,84 +1,47 @@ # Local speech platform VoiceStudio is both a desktop dictation app and a headless local speech -service. The desktop remains one app: its bundled Rust control sidecar owns -microphone activation, focused-target capture, clipboard safety, and native -insertion; the Python backend keeps ASR models warm and exposes the audio data -plane. - -This split lets an integration choose how much it owns: +service. The desktop app owns microphone capture, the dictation shortcut and +native insertion; the Python backend keeps ASR models warm and exposes the +audio data plane to any other interface. ```text -Herdr / terminal / desktop app ── start, stop, toggle ──> Rust control :3902 - │ - ├─ captures target - ├─ opens VoiceStudio mic - └─ inserts final text - VS Code / custom GUI / remote mic ── PCM or WebM ───────> WS/HTTP :3900 │ └─ partial/final text - reserve target / insert final ─> Rust control :3902 + +Scripts / TUIs / hooks ── audio files ──────────────────> HTTP :3900 Claude Code / Codex / Pi / agents ── MCP HTTP/stdio ───> MCP :3900 ``` -The Rust sidecar is part of the VoiceStudio process, not a second application. -It starts with the desktop app and binds only to `127.0.0.1`. +> **Native dictation control is retired.** The Tauri desktop app bundled a +> loopback control server on port 3902 (`/v1/dictation/*`, JSON-RPC, +> `--dictate-*` flags, output sessions). The Electron app does not provide it, +> and 3902 is now its development server's port. Use the in-app dictation +> shortcut, or capture audio yourself and stream it as described below. ## Discover capabilities -Desktop/native discovery: - -```bash -curl http://127.0.0.1:3902/.well-known/voicestudio-speech -``` - -Engine/data-plane discovery: - ```bash curl http://127.0.0.1:3900/.well-known/voicestudio-speech ``` -Both return `voicestudio.speech.v1`. The desktop document includes absolute -control, batch, streaming, output-session, and MCP endpoints. The backend -document uses relative URLs so it also works behind Tailscale or a reverse -proxy; it advertises native control only when launched by the desktop app. - -## Use VoiceStudio capture from any app - -These calls use VoiceStudio's existing microphone, model selection, pill, -refinement, and session-bound insertion. The app under the cursor remains the -destination. - -```bash -curl -X POST http://127.0.0.1:3902/v1/dictation/start -curl -X POST http://127.0.0.1:3902/v1/dictation/stop -curl -X POST http://127.0.0.1:3902/v1/dictation/toggle -``` - -JSON-RPC clients use the same actions: - -```json -{"jsonrpc":"2.0","id":1,"method":"dictation.toggle"} -``` - -Send that object to `POST http://127.0.0.1:3902/rpc`. The installed -VoiceStudio executable also accepts `--dictate-start`, `--dictate-stop`, and -`--dictate-toggle`; the single-instance bridge forwards them to the running -app without opening the Studio window. +The document returns `voicestudio.speech.v1` with relative URLs, so it also +works behind Tailscale or a reverse proxy. `features.native_dictation_control` +remains in the schema and is always `false`. The dependency-free Python bridge is convenient for hooks and TUIs: ```bash -python -m backend.speech_client status -python -m backend.speech_client toggle +python -m backend.speech_client capabilities python -m backend.speech_client transcribe recording.wav -python -m backend.speech_client transcribe recording.wav --insert ``` -`--insert` captures the focused destination before transcription starts and -uses the same clipboard-preserving native delivery as the global shortcut. +It targets `VOICESTUDIO_URL`, else the loopback backend on `OMNIVOICE_PORT` +(default 3900), and sends `OMNIVOICE_API_KEY` as a bearer key when set. The +former `status`, `start`, `stop`, `toggle` and `transcribe --insert` commands +exit with an explanation. ## Bring your own capture interface @@ -108,16 +71,6 @@ Streaming Sherpa models can also emit `final_kind: "utterance"` before the authoritative whole-session `summary`. Existing `/ws/transcribe` clients keep their unchanged legacy frames and `EOF` control. -To reuse native insertion with a custom capture client: - -1. `POST /v1/output/sessions` on port 3902 before opening the microphone. -2. Stream audio and receive the final text on port 3900. -3. `POST /v1/output/sessions/{id}/insert` with `{"text":"..."}`. -4. If capture is cancelled, `DELETE /v1/output/sessions/{id}`. - -Only one output session can own a focused destination at a time. Stale IDs are -rejected instead of inserting into a newer target. - ## Batch and agent protocols | Transport | Endpoint | Use | @@ -126,19 +79,16 @@ rejected instead of inserting into a newer target. | WebSocket | `:3900/v1/audio/transcriptions/stream` | Partial and final live text | | MCP Streamable HTTP | `POST :3900/mcp/` (bare `/mcp` on current backends) | Modern agent clients | | MCP stdio | `python -m backend.mcp_shim` | Claude Code, Codex, and stdio-only clients | -| JSON-RPC | `POST :3902/rpc` | Native dictation control | -| Native CLI | VoiceStudio `--dictate-*` flags | Hooks and plugin actions | ## Integration map | Interface | Recommended connection | |---|---| -| Any desktop text field | Existing global shortcut or Rust `dictation.toggle` | -| Herdr | Merge [the example command bindings](../examples/speech-platform/herdr-config.toml) into Herdr's config; detached commands call the Rust API while the pane stays focused | -| Pi, Claude Code, Codex, Antigravity CLI | Dictate into the focused prompt through Rust; add MCP when the agent also needs file transcription or speech tools | -| VS Code | Call Rust HTTP from the extension host for app-wide dictation, or stream editor-owned mic audio over the versioned WebSocket | -| TUI or shell script | `python -m backend.speech_client` or HTTP/JSON-RPC | -| Browser/WebView UI | Stream audio to the Python data plane; browser pages cannot silently call native control | +| Any desktop text field | The app's global dictation shortcut | +| Pi, Claude Code, Codex, Antigravity CLI | Dictate into the focused prompt with the shortcut; add MCP when the agent also needs file transcription or speech tools | +| VS Code | Stream editor-owned mic audio over the versioned WebSocket | +| TUI or shell script | `python -m backend.speech_client` or the OpenAI-compatible HTTP endpoint | +| Browser/WebView UI | Stream audio to the Python data plane | | Remote microphone + local/remote GPU | Capture at the client edge and use the authenticated WebSocket/OpenAI endpoint | Loopback clients need no credential. Remote native WebSocket clients can send @@ -152,10 +102,9 @@ plaintext HTTP/WebSocket. ## Security and privacy -- The native control sidecar binds only to IPv4 loopback and rejects untrusted - browser `Origin` headers, blocking ordinary websites from turning on the mic. -- Native control never accepts audio and is never exposed through Network - Sharing. Remote ASR stays on the existing API-key boundary. +- No interface can turn on the desktop app's microphone remotely; capture + starts only from the app itself. Remote ASR stays on the existing API-key + boundary. - Microphones stay at the interface edge. A remote GPU backend never assumes it owns the user's input device. - No protocol adds a required network call, account, analytics event, or cloud @@ -187,5 +136,5 @@ boundaries: | [Herdr](https://github.com/motionharvest/herdr) | One local control surface behind CLI, socket, hooks, and plugin integrations | The differentiator is the connection layer: one bundled app offers native -capture/output control and a protocol-neutral ASR service, so every interface -does not rebuild model loading, desktop permissions, and insertion safety. +capture and insertion plus a protocol-neutral ASR service, so every interface +does not rebuild model loading. diff --git a/examples/README.md b/examples/README.md index 1bc9a6a0f..c9b6ba799 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,6 @@ training, fine-tuning, and evaluating VoiceStudio. | Training from scratch | [run_emilia.sh](run_emilia.sh) | Full pipeline on the Emilia dataset (data check, tokenization, training) | | Fine-tuning | [run_finetune.sh](run_finetune.sh) | Fine-tune from a pretrained checkpoint using your own JSONL data | | Evaluation | [run_eval.sh](run_eval.sh) | Evaluate WER, speaker similarity, and UTMOS on standard test sets | -| Herdr dictation | [speech-platform/herdr-config.toml](speech-platform/herdr-config.toml) | Trigger the bundled Rust dictation sidecar from detached Herdr command bindings | --- diff --git a/examples/speech-platform/herdr-config.toml b/examples/speech-platform/herdr-config.toml deleted file mode 100644 index c546ca2f1..000000000 --- a/examples/speech-platform/herdr-config.toml +++ /dev/null @@ -1,21 +0,0 @@ -# Merge these detached command bindings into ~/.config/herdr/config.toml. -# Herdr leaves the current pane focused while VoiceStudio captures its native -# insertion target, so the transcript returns to the pane that started it. - -[[keys.command]] -key = "prefix+alt+d" -type = "shell" -command = "curl -fsS -X POST http://127.0.0.1:3902/v1/dictation/toggle" -description = "toggle VoiceStudio dictation" - -[[keys.command]] -key = "prefix+alt+s" -type = "shell" -command = "curl -fsS -X POST http://127.0.0.1:3902/v1/dictation/start" -description = "start VoiceStudio dictation" - -[[keys.command]] -key = "prefix+alt+x" -type = "shell" -command = "curl -fsS -X POST http://127.0.0.1:3902/v1/dictation/stop" -description = "stop VoiceStudio dictation" diff --git a/tests/test_speech_client.py b/tests/test_speech_client.py index d6a25a345..fca63e4e7 100644 --- a/tests/test_speech_client.py +++ b/tests/test_speech_client.py @@ -1,15 +1,13 @@ from __future__ import annotations -from types import SimpleNamespace - import pytest def test_url_join_does_not_duplicate_slashes(): from speech_client.__main__ import _join_url - assert _join_url("http://127.0.0.1:3902/", "/v1/status") == ( - "http://127.0.0.1:3902/v1/status" + assert _join_url("http://127.0.0.1:3900/", "/v1/audio/transcriptions") == ( + "http://127.0.0.1:3900/v1/audio/transcriptions" ) @@ -32,13 +30,6 @@ def test_multipart_matches_openai_audio_contract(): assert body.endswith(b"--fixed-boundary--\r\n") -def test_json_transcription_response_extracts_insertable_text(): - from speech_client.__main__ import _response_text - - assert _response_text(b'{"text":"hello"}', "application/json") == "hello" - assert _response_text(b"hello", "text/plain") == "hello" - - def test_client_rejects_non_http_url_handlers(): from urllib import request @@ -94,39 +85,35 @@ def test_credentialed_redirects_are_rejected(): handler.redirect_request(None, None, 307, "redirect", {}, "https://other.test") -def test_interrupt_releases_focused_output_session(monkeypatch): +@pytest.mark.parametrize( + "argv", + [["start"], ["stop"], ["toggle"], ["status"], ["transcribe", "x.wav", "--insert"]], +) +def test_retired_native_control_fails_clearly_without_network(monkeypatch, capsys, argv): + """These targeted the retired Tauri control server on :3902 — now the + Electron dev server's port. They must explain instead of calling it.""" from speech_client import __main__ as client - calls = [] + def no_network(*_args, **_kwargs): + raise AssertionError("retired commands must not reach the network") + + monkeypatch.setattr(client, "_open", no_network) + monkeypatch.setattr(client, "_json_request", no_network) + monkeypatch.setattr(client, "_read_audio", no_network) + + assert client.main(argv) == 2 + assert "native dictation control is not available" in capsys.readouterr().err - def fake_json(method, url, payload=None): - calls.append((method, url, payload)) - if method == "POST" and url.endswith("/v1/output/sessions"): - return {"session_id": 42} - return {"ok": True} - - monkeypatch.setattr(client, "_read_audio", lambda *_args: (b"audio", "audio.wav")) - monkeypatch.setattr(client, "_json_request", fake_json) - def interrupt(*_args, **_kwargs): - raise KeyboardInterrupt - - monkeypatch.setattr(client, "_open", interrupt) - args = SimpleNamespace( - audio="ignored.wav", - stdin_filename="audio.wav", - model="whisper-1", - response_format="text", - language=None, - insert=True, - control_url="http://127.0.0.1:3902", - engine_url="http://127.0.0.1:3900", - ) - with pytest.raises(KeyboardInterrupt): - client._transcribe(args) +def test_capabilities_reads_the_backend_discovery_document(monkeypatch, capsys): + from speech_client import __main__ as client - assert calls[-1][0] == "DELETE" - assert calls[-1][1].endswith("/v1/output/sessions/42") + calls = [] + monkeypatch.setattr( + client, "_json_request", lambda method, url, payload=None: calls.append((method, url)) or {"ok": 1} + ) + assert client.main(["--engine-url", "http://127.0.0.1:3912", "capabilities"]) == 0 + assert calls == [("GET", "http://127.0.0.1:3912/.well-known/voicestudio-speech")] @pytest.mark.parametrize("env,expected", [ @@ -143,4 +130,4 @@ def test_engine_url_follows_the_backend_port(monkeypatch, env, expected): monkeypatch.setenv(name, value) from speech_client.__main__ import _parser - assert _parser().parse_args(["status"]).engine_url == expected + assert _parser().parse_args(["capabilities"]).engine_url == expected diff --git a/tests/test_speech_platform.py b/tests/test_speech_platform.py index e80d79a89..d88214f13 100644 --- a/tests/test_speech_platform.py +++ b/tests/test_speech_platform.py @@ -33,16 +33,15 @@ def test_capabilities_are_stable_and_side_effect_free(monkeypatch): assert body["authentication"]["websocket_ticket_query_parameter"] == "ws_ticket" -def test_desktop_backend_advertises_rust_control_sidecar(monkeypatch): +def test_stale_control_port_env_no_longer_advertises_a_dead_endpoint(monkeypatch): + """The Tauri shell that served native control (and set this) is retired.""" from api.routers.speech_platform import speech_capabilities - monkeypatch.setenv("VOICESTUDIO_SPEECH_CONTROL_PORT", "4902") + monkeypatch.setenv("VOICESTUDIO_SPEECH_CONTROL_PORT", "3902") body = speech_capabilities().model_dump(by_alias=True) - assert body["features"]["native_dictation_control"] is True - assert body["endpoints"]["native_dictation_control"]["path"] == ( - "http://127.0.0.1:4902/v1/capabilities" - ) + assert body["features"]["native_dictation_control"] is False + assert "native_dictation_control" not in body["endpoints"] @pytest.mark.parametrize( From 85af646211c002e24e5469014b88145796ede664 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:24:35 +0530 Subject: [PATCH 43/85] fix(cla): fail closed on unverified, hidden, and superseded contributors - Read each commit's authors with pageInfo; a commit with more authors than are read fails the check for manual review. - Count commit authors only through the account GitHub linked; an unlinked no-reply address is unknown. Co-author trailers stay self-declared. - Supersedes #N also requires that pull request's commit authors and co-authors, failing closed past MAX_COMMITS. - Recheck on unlabeled and edited pull request events. - Share one agent identity list (.github/scripts/agent_identities.py) instead of treating every openai.com address as a tool. - Join comment strings explicitly instead of by implicit concatenation. - cla_audit: refuse shallow clones, look emails up only through commits they authored, and keep unlinked authors as their own rows. --- .github/scripts/agent_identities.py | 61 +++++++++++++ .github/scripts/cla_check.py | 83 +++++++++++++----- .github/workflows/cla.yml | 4 +- scripts/cla_audit.py | 130 +++++++++++++++++++--------- tests/test_cla_audit.py | 76 ++++++++++++++++ tests/test_cla_check.py | 95 ++++++++++++++++++-- tests/test_cla_workflow.py | 9 ++ 7 files changed, 387 insertions(+), 71 deletions(-) create mode 100644 .github/scripts/agent_identities.py create mode 100644 tests/test_cla_audit.py diff --git a/.github/scripts/agent_identities.py b/.github/scripts/agent_identities.py new file mode 100644 index 000000000..e924ce0af --- /dev/null +++ b/.github/scripts/agent_identities.py @@ -0,0 +1,61 @@ +"""AI agent identities, shared by the CLA check and the commit-identity gate. + +Lives in .github/scripts because .github/workflows/cla.yml checks out only this +directory from the base branch. scripts/check_commit_identities.py loads it by +path. Standard library only. + +Only identities that name an agent are listed, never whole company domains, so +people who commit with a work address at an AI company are treated as people. +""" +from __future__ import annotations + +import re + +# GitHub logins of agent apps and bots. Their no-reply addresses are +# "[+][[bot]]@users.noreply.github.com". +AGENT_LOGINS = ( + "copilot", "claude", "codex", "cursor", "devin-ai-integration", "google-labs-jules", + "copilot-swe-agent", "coderabbitai", "openhands-agent", "sweep-ai", +) +_LOGINS = "|".join(re.escape(login) for login in AGENT_LOGINS) + +AGENT_EMAILS = re.compile( + r"^(noreply@anthropic\.com|cursoragent@cursor\.com|codex@openai\.com|noreply@openai\.com" + r"|noreply@coderabbit\.ai|codex@users\.noreply\.github\.com" + rf"|(\d+\+)?({_LOGINS})(\[bot\])?@users\.noreply\.github\.com)$", + re.IGNORECASE, +) + +# Names an agent signs a Co-authored-by trailer with. Broad on purpose: a +# trailer naming "Claude" or "Cursor" credits the tool. +AGENT_TRAILER_NAMES = re.compile( + r"^(claude( (code|opus|sonnet|haiku|fable|\d).*)?|cursor( agent)?|cursoragent|(github )?copilot" + r"|(openai )?codex|chatgpt|devin( ai)?|gemini( code assist)?|(google )?jules|aider|cline|coderabbit(ai)?)" + r"(\[bot\])?$", + re.IGNORECASE, +) + +# Author and committer names that can only be an agent. People are called +# Claude or Jules, so a bare first name is not enough here. +AGENT_AUTHOR_NAMES = re.compile( + r"^((claude code|claude (opus|sonnet|haiku|fable|instant|\d)\S*( .*)?|cursor agent|cursoragent" + r"|(github )?copilot|(openai )?codex|chatgpt|devin ai|google jules|openhands( agent)?)(\[bot\])?" + rf"|({_LOGINS})\[bot\])$", + re.IGNORECASE, +) + + +def is_agent_email(email: str) -> bool: + return bool(AGENT_EMAILS.match((email or "").strip())) + + +def _name(name: str) -> str: + return " ".join((name or "").split()) + + +def is_agent_trailer_name(name: str) -> bool: + return bool(AGENT_TRAILER_NAMES.match(_name(name))) + + +def is_agent_author_name(name: str) -> bool: + return bool(AGENT_AUTHOR_NAMES.match(_name(name))) diff --git a/.github/scripts/cla_check.py b/.github/scripts/cla_check.py index 505e1e845..f255e528b 100644 --- a/.github/scripts/cla_check.py +++ b/.github/scripts/cla_check.py @@ -31,6 +31,10 @@ import urllib.request from dataclasses import dataclass, field +# cla.yml checks out only .github/scripts, so shared code lives beside this file. +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from agent_identities import AGENT_EMAILS + CLA_VERSION = "1.0" SIGN_PHRASE = f"I have read the VoiceStudio CLA {CLA_VERSION} and I hereby sign it." DOCUMENT_PATH = f".github/CLA-{CLA_VERSION}.md" @@ -43,6 +47,7 @@ OVERRIDE_LABEL = "cla-override" COMMENT_MARKER = "" MAX_COMMITS = 250 +MAX_AUTHORS = 20 # authors (author plus co-authors) read per commit # GitHub account IDs exempt from signing. IDs cannot be claimed by anyone else. MAINTAINER_IDS = {4178343} # debpalash @@ -55,9 +60,7 @@ # GitHub App bots also commit as "+[bot]@users.noreply.github.com"; # a person cannot hold such a login, and the authenticated opener still signs. TOOL_EMAILS = re.compile( - r"^(noreply@anthropic\.com|cursoragent@cursor\.com|[^@]+@openai\.com|noreply@coderabbit\.ai" - r"|codex@users\.noreply\.github\.com|\d+\+copilot@users\.noreply\.github\.com" - r"|\d+\+[^@]*\[bot\]@users\.noreply\.github\.com)$", + rf"{AGENT_EMAILS.pattern}|^\d+\+[^@]*\[bot\]@users\.noreply\.github\.com$", re.I, ) # A pull request that says it supersedes others must also be signed by their authors. @@ -121,7 +124,9 @@ def evaluate(opener: Person, opener_is_bot: bool, actors: list[dict], signed_ids """Decide who still has to sign. `actors` holds one entry per commit author or co-author: - {"name", "email", "login" or None, "id" or None}. + {"name", "email", "login" or None, "id" or None}, plus "trailer": True for + co-authors parsed from a commit message, or {"unknown": text} for anything + that must be reviewed by hand. """ result = Evaluation() seen: set[int] = set() @@ -136,16 +141,23 @@ def require(person: Person) -> None: if not opener_is_bot: require(opener) for actor in actors: + if actor.get("unknown"): + if actor["unknown"] not in result.unknown: + result.unknown.append(actor["unknown"]) + continue email = (actor.get("email") or "").strip() if TOOL_EMAILS.match(email): continue if actor.get("id"): require(Person(actor["login"], int(actor["id"]))) continue - if match := NOREPLY.match(email): - if match.group(1): - require(Person(match.group(2), int(match.group(1)))) - continue + # A commit author counts only through the account GitHub linked to it. + # An unlinked no-reply address may name someone else's account, so it + # stays unknown. Co-author trailers are self-declared either way (the + # opener answers for them), so their no-reply account ID is taken as given. + if actor.get("trailer") and (match := NOREPLY.match(email)) and match.group(1): + require(Person(match.group(2), int(match.group(1)))) + continue if opener_is_bot and "[bot]" in email: continue identity = f"{actor.get('name') or '?'} <{email or 'no email'}>" @@ -155,7 +167,8 @@ def require(person: Person) -> None: def co_authors(message: str) -> list[dict]: - return [{"name": n, "email": e, "login": None, "id": None} for n, e in CO_AUTHOR.findall(message or "")] + return [{"name": n, "email": e, "login": None, "id": None, "trailer": True} + for n, e in CO_AUTHOR.findall(message or "")] def add_signatures(store: dict, new: list[dict]) -> bool: @@ -178,22 +191,25 @@ def _safe(text: str) -> str: def render_comment(evaluation: Evaluation, doc_url: str) -> str: if evaluation.passed: return f"{COMMENT_MARKER}\nAll contributors on this pull request have signed the VoiceStudio CLA. Thank you!" - lines = [ - COMMENT_MARKER, + intro = ( "Thank you for contributing to VoiceStudio. Before this pull request can merge, everyone who " f"contributed to it must sign the [Contributor License Agreement {CLA_VERSION}]({doc_url}) once. " "You keep your copyright; the agreement lets Yupcha Softwares Private Limited, the company that " - "maintains VoiceStudio, ship your work in both the AGPL-3.0 app and commercial builds.", - ] + "maintains VoiceStudio, ship your work in both the AGPL-3.0 app and commercial builds." + ) + lines = [COMMENT_MARKER, intro] if evaluation.unsigned: lines += ["", "**Still to sign:** " + ", ".join(f"@{p.login}" for p in evaluation.unsigned), "", "To sign, post this as a new comment on its own line:", "", "```text", SIGN_PHRASE, "```"] if evaluation.unknown: lines += ["", "**Commits we cannot link to a GitHub account:**", ""] lines += [f"- {_safe(identity)}" for identity in evaluation.unknown] - lines += ["", "Add that email to your GitHub account (Settings → Emails), or rewrite the commits with " - "an email that is on it (`git commit --amend --reset-author`, or an interactive rebase), " - "then push. Each push re-runs this check."] + fix = ( + "Add that email to your GitHub account (Settings → Emails), or rewrite the commits with " + "an email that is on it (`git commit --amend --reset-author`, or an interactive rebase), " + "then push. Each push re-runs this check." + ) + lines += ["", fix] lines += ["", "Comment `recheck` to run the check again."] return "\n".join(lines) @@ -257,15 +273,22 @@ def graphql(self, query: str, variables: dict) -> dict: pullRequest(number: $number) { commits(first: 100, after: $after) { pageInfo { hasNextPage endCursor } - nodes { commit { authors(first: 20) { nodes { name email user { login databaseId } } } } } + nodes { commit { oid authors(first: MAX_AUTHORS) { + pageInfo { hasNextPage } + nodes { name email user { login databaseId } } + } } } } } } } -""" +""".replace("MAX_AUTHORS", str(MAX_AUTHORS)) def pr_actors(gh: GitHub, number: int) -> tuple[list[dict], int]: + """Commit authors and co-authors of a pull request, and how many commits were read. + + Stops after MAX_COMMITS commits; the caller fails the check when it does. + """ owner, name = gh.repo.split("/") actors: list[dict] = [] count, after = 0, None @@ -274,10 +297,15 @@ def pr_actors(gh: GitHub, number: int) -> tuple[list[dict], int]: commits = data["repository"]["pullRequest"]["commits"] for node in commits["nodes"]: count += 1 - for author in node["commit"]["authors"]["nodes"]: + authors = node["commit"]["authors"] + for author in authors["nodes"]: user = author.get("user") or {} actors.append({"name": author.get("name"), "email": author.get("email"), "login": user.get("login"), "id": user.get("databaseId")}) + if (authors.get("pageInfo") or {}).get("hasNextPage"): + # Unread co-authors could include someone who has not signed. + actors.append({"unknown": f"commit {(node['commit'].get('oid') or '?')[:7]} has more than " + f"{MAX_AUTHORS} authors; a maintainer must review it by hand"}) if not commits["pageInfo"]["hasNextPage"] or count >= MAX_COMMITS: return actors, count after = commits["pageInfo"]["endCursor"] @@ -380,11 +408,21 @@ def run(gh: GitHub, event_name: str, event: dict, server_url: str = "https://git return Evaluation() actors, count = pr_actors(gh, number) + overflow = [number] if count >= MAX_COMMITS else [] + # A superseded pull request's opener, commit authors and co-authors sign too. for ref in superseded_numbers(pr.get("body")): + if ref == number: + continue status, other, _ = gh.request("GET", f"/repos/{gh.repo}/pulls/{ref}") - if status == 200 and (other.get("user") or {}).get("type") == "User": + if status != 200: + continue # an issue or a missing number, not a pull request + if (other.get("user") or {}).get("type") == "User": actors.append({"name": other["user"]["login"], "email": "", "login": other["user"]["login"], "id": other["user"]["id"]}) + more, more_count = pr_actors(gh, ref) + actors += more + if more_count >= MAX_COMMITS: + overflow.append(ref) store, _ = load_store(gh) signed_ids = {int(s["id"]) for s in store["signatures"]} evaluation = evaluate(opener, opener_is_bot, actors, signed_ids) @@ -401,8 +439,9 @@ def run(gh: GitHub, event_name: str, event: dict, server_url: str = "https://git save_signatures(gh, new, number) evaluation = evaluate(opener, opener_is_bot, actors, signed_ids | {s["id"] for s in new}) - if count >= MAX_COMMITS: - evaluation.unknown.append(f"more than {MAX_COMMITS} commits; a maintainer can review them and apply `{OVERRIDE_LABEL}`") + for ref in overflow: + evaluation.unknown.append(f"#{ref} has more than {MAX_COMMITS} commits; a maintainer can review them " + f"and apply `{OVERRIDE_LABEL}`") description = ("All contributors have signed the CLA" if evaluation.passed else f"{len(evaluation.unsigned) + len(evaluation.unknown)} contributor(s) still need to sign") gh.request("POST", f"/repos/{gh.repo}/statuses/{pr['head']['sha']}", { diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 74a56c080..5b6ce3db3 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -16,7 +16,9 @@ name: CLA on: pull_request_target: - types: [opened, synchronize, reopened, labeled] + # unlabeled: removing `cla-override` rechecks; edited: so does changing + # `Supersedes #N` in the description. + types: [opened, synchronize, reopened, labeled, unlabeled, edited] issue_comment: types: [created] diff --git a/scripts/cla_audit.py b/scripts/cla_audit.py index 54cbb287d..2216b632c 100644 --- a/scripts/cla_audit.py +++ b/scripts/cla_audit.py @@ -11,10 +11,13 @@ GH_TOKEN=... python scripts/cla_audit.py --json out.json People are matched by GitHub account ID. An author is mapped by their GitHub -no-reply address, else by the account linked to the commit email. Commits by AI -agents or unlinked emails are credited to the author of the pull request that -merged them, because that person submitted the work. Without a token the GitHub -API rate limit runs out quickly; unresolved authors are then listed by email. +no-reply address, else by the account GitHub linked to a commit that email +authored. Commits by AI agents or placeholder identities are credited to the +author of the pull request that merged them, because that person submitted the +work. People whose email is not linked to an account keep their own row, keyed +by email and noting who submitted their work, so a signed submitter never hides +them. Without a token the GitHub API rate limit runs out quickly; unresolved +authors are then listed by email. Needs full history (not a shallow clone). Blame follows moves within a file only, so code copied between files is credited to whoever moved it, and squash commits that fold in other people's pull requests are listed separately for manual review. @@ -51,12 +54,14 @@ def is_agent(email: str) -> bool: return bool(cla.TOOL_EMAILS.match(email) or PLACEHOLDER.search(email)) + + FOLDS_IN = re.compile(r"\bsupersed", re.I) NOTES = { "lookup-failed": "GitHub lookup failed; set GH_TOKEN and rerun", - "name": "matched by display name; confirm", + "unlinked": "email not linked to a GitHub account", "unresolved": "no linked account or pull request; resolve by hand", - "submitted": "submitted from an unlinked or agent identity; confirm the work is theirs", + "submitted": "submitted from an agent or placeholder identity; confirm the work is theirs", } @@ -75,6 +80,13 @@ def github(path: str) -> object: return json.load(response) +def require_full_history() -> None: + """Blame on a shallow clone credits all older lines to the oldest fetched commit.""" + if git("rev-parse", "--is-shallow-repository").strip() == "true": + raise SystemExit("This clone is shallow, so blame would credit truncated history to the wrong people. " + "Run `git fetch --unshallow` and rerun.") + + def blame(path: str) -> collections.Counter: lines: collections.Counter = collections.Counter() commit = name = email = None @@ -101,16 +113,37 @@ def commit_messages(commits: set[str]) -> dict[str, str]: return messages -def account_for(email: str, commit: str) -> tuple[str | None, int | None, str]: - """GitHub account for an author email: (login, id, noreply|api|unlinked|lookup-failed).""" +def authored_commits() -> dict[str, str]: + """The newest commit each author email authored, for account lookups.""" + commits: dict[str, str] = {} + for line in git("log", "--format=%H %ae").splitlines(): + sha, _, email = line.partition(" ") + commits.setdefault(email.strip().lower(), sha) + return commits + + +def account_for(email: str, commit: str | None) -> tuple[str | None, int | None, str]: + """GitHub account for an author email. + + `commit` must be one the email authored; a co-author found only in + trailers has none. Returns (login, id, source) with source one of noreply, + api, agent, unlinked, unresolved or lookup-failed. + """ if is_agent(email): - return None, None, "unlinked" + return None, None, "agent" if (match := cla.NOREPLY.match(email)) and match.group(1): return match.group(2), int(match.group(1)), "noreply" + if not commit: + return None, None, "unresolved" try: - author = github(f"commits/{commit}").get("author") or {} + payload = github(f"commits/{commit}") except (OSError, ValueError, urllib.error.HTTPError): return None, None, "lookup-failed" + # The API's account belongs to the commit's author; accept it only for that email. + commit_email = (((payload.get("commit") or {}).get("author") or {}).get("email") or "").strip().lower() + if commit_email != email: + return None, None, "unresolved" + author = payload.get("author") or {} return (author["login"], author["id"], "api") if author.get("id") else (None, None, "unlinked") @@ -143,21 +176,8 @@ def signed_ids(allow_missing: bool) -> set[int]: return {int(s["id"]) for s in json.loads(result.stdout)["signatures"]} -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) - parser.add_argument("--json", type=Path, help="also write the full report as JSON") - parser.add_argument("--no-signatures", action="store_true", help="report everyone as unsigned") - args = parser.parse_args() - signed = signed_ids(args.no_signatures) - - files = [f for f in git("ls-files").splitlines() if f and not BINARY.search(f)] - blamed: collections.Counter = collections.Counter() - with ThreadPoolExecutor(16) as pool: - for counts in pool.map(blame, files): - blamed.update(counts) - messages = commit_messages({c for _, _, c, _ in blamed}) - - # Credit each line to the commit author and to every person co-author. +def credit_lines(blamed: collections.Counter, messages: dict[str, str]) -> collections.Counter: + """Credit each blamed line to the commit author and to every person co-author.""" credits: collections.Counter = collections.Counter() # (email, name, commit, path) -> lines for (email, name, commit, path), n in blamed.items(): credits[(email, name, commit, path)] += n @@ -166,27 +186,28 @@ def main() -> None: for co_email, co_name in co_emails.items(): if not cla.TOOL_EMAILS.match(co_email) and co_email != email: credits[(co_email, co_name, commit, path)] += n + return credits - first_commit: dict[str, str] = {} - for email, _, commit, _ in credits: - first_commit.setdefault(email, commit) - with ThreadPoolExecutor(4) as pool: - accounts = dict(zip(first_commit, pool.map(lambda e: account_for(e, first_commit[e]), first_commit))) - per_commit = sorted({c for e, _, c, _ in credits if accounts[e][2] == "unlinked"}) - with ThreadPoolExecutor(4) as pool: - submitters = dict(zip(per_commit, pool.map(pr_author_for, per_commit))) - names = names_to_logins() +def attribute(credits: collections.Counter, accounts: dict, submitters: dict, names: dict, + messages: dict[str, str]) -> tuple[dict[str, dict], collections.Counter]: + """Group credited lines by person: (people keyed by account ID or email, folded commits).""" people: dict[str, dict] = collections.defaultdict( lambda: {"lines": 0, "files": collections.Counter(), "emails": set(), "notes": set(), "login": None, "id": None} ) folded: collections.Counter = collections.Counter() for (email, name, commit, path), n in credits.items(): login, account_id, source = accounts[email] - if source == "unlinked": - login, account_id, source = submitters[commit] + notes = set() + if source == "agent": + # The person who opened the pull request submitted the agent's work. + login, account_id, source = submitters.get(commit) or (None, None, "unresolved") + elif source == "unlinked": + # Never merged into the submitter: they may have signed while this author has not. + submitter = (submitters.get(commit) or (None,))[0] + notes.add(f"submitted by @{submitter} in a PR" if submitter else NOTES["unresolved"]) if not account_id and (guess := names.get((name or "").strip().lower())): - (login, account_id), source = guess, "name" + notes.add(f"same display name as @{guess[0]}; confirm") # a hint only, never a match if account_id in cla.MAINTAINER_IDS and source in ("noreply", "api"): if FOLDS_IN.search(messages.get(commit, "")): folded[commit] += n @@ -200,8 +221,39 @@ def main() -> None: person["emails"].add(email) person["login"] = person["login"] or login person["id"] = account_id + person["notes"] |= notes if source in NOTES: - person["notes"].add(source) + person["notes"].add(NOTES[source]) + return people, folded + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("--json", type=Path, help="also write the full report as JSON") + parser.add_argument("--no-signatures", action="store_true", help="report everyone as unsigned") + args = parser.parse_args() + require_full_history() + signed = signed_ids(args.no_signatures) + + files = [f for f in git("ls-files").splitlines() if f and not BINARY.search(f)] + blamed: collections.Counter = collections.Counter() + with ThreadPoolExecutor(16) as pool: + for counts in pool.map(blame, files): + blamed.update(counts) + messages = commit_messages({c for _, _, c, _ in blamed}) + + credits = credit_lines(blamed, messages) + + # Look each email up through a commit it authored, never one it only co-authored. + authored = authored_commits() + emails = {email for email, _, _, _ in credits} + with ThreadPoolExecutor(4) as pool: + accounts = dict(zip(emails, pool.map(lambda e: account_for(e, authored.get(e)), emails))) + per_commit = sorted({c for e, _, c, _ in credits if accounts[e][2] in ("agent", "unlinked")}) + with ThreadPoolExecutor(4) as pool: + submitters = dict(zip(per_commit, pool.map(pr_author_for, per_commit))) + + people, folded = attribute(credits, accounts, submitters, names_to_logins(), messages) unsigned = sorted( ((key, p) for key, p in people.items() if p["id"] not in signed), @@ -212,7 +264,7 @@ def main() -> None: for key, p in unsigned: label = f"@{p['login']}" if p["login"] else f"`{key}`" if p["notes"]: - label += " (" + "; ".join(NOTES[n] for n in sorted(p["notes"])) + ")" + label += " (" + "; ".join(sorted(p["notes"])) + ")" top = ", ".join(f"`{f}` ({n})" for f, n in p["files"].most_common(3)) print(f"| {label} | {p['lines']} | {top} |") if folded: diff --git a/tests/test_cla_audit.py b/tests/test_cla_audit.py new file mode 100644 index 000000000..be2ea192a --- /dev/null +++ b/tests/test_cla_audit.py @@ -0,0 +1,76 @@ +"""Unit tests for scripts/cla_audit.py (who has code at HEAD but no CLA).""" +from __future__ import annotations + +import collections +import importlib.util +import sys +from pathlib import Path + +import pytest + +_SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "cla_audit.py" +_spec = importlib.util.spec_from_file_location("cla_audit", _SCRIPT) +audit = importlib.util.module_from_spec(_spec) +sys.modules["cla_audit"] = audit +_spec.loader.exec_module(audit) + +PRIMARY = "primary@example.com" +CO = "co@example.com" + + +def _commit_payload(email, login=None, account_id=None): + return {"commit": {"author": {"email": email}}, + "author": {"login": login, "id": account_id} if account_id else None} + + +def test_shallow_clone_is_refused(monkeypatch): + monkeypatch.setattr(audit, "git", lambda *args: "true\n") + with pytest.raises(SystemExit, match="git fetch --unshallow"): + audit.require_full_history() + monkeypatch.setattr(audit, "git", lambda *args: "false\n") + audit.require_full_history() + + +def test_account_is_accepted_only_for_the_commits_own_author(monkeypatch): + payloads = {"c1": _commit_payload(PRIMARY, "pat", 2020)} + monkeypatch.setattr(audit, "github", lambda path: payloads[path.split("/")[1]]) + assert audit.account_for(PRIMARY, "c1") == ("pat", 2020, "api") + # A trailer-only co-author looked up through the same commit would inherit + # the primary author's account; it stays unresolved instead. + assert audit.account_for(CO, "c1") == (None, None, "unresolved") + assert audit.account_for(CO, None) == (None, None, "unresolved") + + +def test_unlinked_and_agent_emails_are_told_apart(monkeypatch): + monkeypatch.setattr(audit, "github", lambda path: _commit_payload(PRIMARY)) + assert audit.account_for(PRIMARY, "c1") == (None, None, "unlinked") + assert audit.account_for("cursoragent@cursor.com", "c1") == (None, None, "agent") + + +def _credits(*rows): + return collections.Counter({(email, name, commit, "a.py"): n for email, name, commit, n in rows}) + + +def test_unlinked_author_keeps_a_row_even_when_the_submitter_signed(): + credits = _credits((PRIMARY, "Pat", "c1", 10)) + people, _ = audit.attribute(credits, {PRIMARY: (None, None, "unlinked")}, + {"c1": ("opener", 1001, "submitted")}, {}, {}) + assert set(people) == {PRIMARY} + row = people[PRIMARY] + assert row["id"] is None and row["lines"] == 10 + assert "submitted by @opener in a PR" in row["notes"] + + +def test_agent_commits_are_credited_to_the_submitter(): + credits = _credits(("cursoragent@cursor.com", "Cursor Agent", "c1", 4)) + people, _ = audit.attribute(credits, {"cursoragent@cursor.com": (None, None, "agent")}, + {"c1": ("opener", 1001, "submitted")}, {}, {}) + assert set(people) == {"1001"} and people["1001"]["lines"] == 4 + + +def test_unresolved_co_author_gets_no_pr_or_name_fallback(): + credits = _credits((CO, "Pat", "c1", 3)) + people, _ = audit.attribute(credits, {CO: (None, None, "unresolved")}, + {"c1": ("opener", 1001, "submitted")}, {"pat": ("pat", 2020)}, {}) + assert set(people) == {CO} and people[CO]["id"] is None + assert audit.NOTES["unresolved"] in people[CO]["notes"] diff --git a/tests/test_cla_check.py b/tests/test_cla_check.py index bc82cb443..a4a5cbcf8 100644 --- a/tests/test_cla_check.py +++ b/tests/test_cla_check.py @@ -78,11 +78,26 @@ def test_maintainer_and_bots_are_exempt_by_id(): assert cla.evaluate(MAINTAINER, False, actors, set()).passed -def test_noreply_email_maps_to_account_id(): - result = cla.evaluate(OPENER, False, [actor(email="2002+carol@users.noreply.github.com")], {1001}) +def test_co_author_trailer_noreply_email_maps_to_account_id(): + actors = cla.co_authors("fix: x\n\nCo-authored-by: Carol <2002+carol@users.noreply.github.com>") + result = cla.evaluate(OPENER, False, actors, {1001}) assert result.unsigned == [cla.Person("carol", 2002)] +def test_unlinked_commit_author_noreply_email_is_not_trusted(): + # GitHub links real no-reply addresses itself; an unlinked one may name + # someone else's account, so it must be reviewed rather than mapped. + result = cla.evaluate(OPENER, False, [actor(name="Carol", email="2002+carol@users.noreply.github.com")], + {1001, 2002}) + assert not result.passed and result.unknown == ["Carol <2002+carol@users.noreply.github.com>"] + + +@pytest.mark.parametrize("email", ["someone@openai.com", "someone@anthropic.com", "dev@cursor.com"]) +def test_people_with_work_emails_at_ai_companies_still_sign(email): + assert cla.evaluate(OPENER, False, [actor(email=email)], {1001}).unknown == [f"Someone <{email}>"] + assert cla.evaluate(OPENER, False, [actor("pat", 2020, email=email)], {1001}).unsigned == [cla.Person("pat", 2020)] + + def test_ai_tool_co_authors_are_skipped_but_people_are_not(): actors = cla.co_authors( "fix: thing\n\nCo-authored-by: Cursor Agent \n" @@ -128,8 +143,9 @@ def test_comment_renders_untrusted_identities_as_inert_code(): class FakeGitHub: repo = "debpalash/VoiceStudio" - def __init__(self, comments, actors, store=None, pr=None, others=None): + def __init__(self, comments, actors, store=None, pr=None, others=None, other_commits=None): self.comments, self.actors = comments, actors + self.other_commits = other_commits or {} # PR number -> one actor per commit self.pr, self.others = pr or {}, others or {} self.files = {} if store is None else {cla.SIGNATURE_PATH: store} self.branch = store is not None @@ -144,9 +160,12 @@ def paginate(self, path, limit=None): return self.comments def graphql(self, query, variables): - nodes = [{"commit": {"authors": {"nodes": [ - {"name": a["name"], "email": a["email"], - "user": {"login": a["login"], "databaseId": a["id"]} if a["id"] else None}]}}} for a in self.actors] + commits = self.actors if variables["number"] == 7 else self.other_commits.get(variables["number"], []) + nodes = [{"commit": {"oid": "f" * 40, "authors": { + "pageInfo": {"hasNextPage": bool(a.get("more"))}, + "nodes": [{"name": a["name"], "email": a["email"], + "user": {"login": a["login"], "databaseId": a["id"]} if a["id"] else None}]}}} + for a in commits] return {"repository": {"pullRequest": {"commits": { "pageInfo": {"hasNextPage": False, "endCursor": None}, "nodes": nodes}}}} @@ -234,7 +253,7 @@ def test_stored_signature_records_a_hash_of_the_comment(): @pytest.mark.parametrize("email", [ - "cline@openai.com", "codex@openai.com", "175728472+Copilot@users.noreply.github.com", + "noreply@openai.com", "codex@openai.com", "175728472+Copilot@users.noreply.github.com", "cursoragent@cursor.com", "41898282+github-actions[bot]@users.noreply.github.com", ]) def test_agent_identities_never_need_to_sign(email): @@ -285,10 +304,68 @@ def test_unlabelled_issues_record_nothing(): def test_superseding_pr_waits_for_the_original_authors(): others = {12: {"login": "frank", "id": 6006, "type": "User"}, 13: {"login": "dependabot[bot]", "id": 49699333, "type": "Bot"}} + commits = {12: [actor("frank", 6006), actor("gina", 7007), actor(name="hal", email="hal@example.com")], + 13: [actor(email="49699333+dependabot[bot]@users.noreply.github.com")]} gh = FakeGitHub([], [actor("alice", 1001)], store=cla.empty_store(), - pr={"body": "Supersedes #12 and #13"}, others=others) + pr={"body": "Supersedes #12 and #13"}, others=others, other_commits=commits) + result = cla.run(gh, "pull_request_target", {"pull_request": {"number": 7}}) + assert result.unsigned == [cla.Person("alice", 1001), cla.Person("frank", 6006), cla.Person("gina", 7007)] + assert result.unknown == ["hal "] + + +def test_superseded_pr_with_too_many_commits_fails_closed(monkeypatch): + monkeypatch.setattr(cla, "MAX_COMMITS", 2) + gh = FakeGitHub([], [actor("alice", 1001)], store={"version": "1.0", "signatures": [{"id": 1001}]}, + pr={"body": "Supersedes #12"}, others={12: {"login": "alice", "id": 1001, "type": "User"}}, + other_commits={12: [actor("alice", 1001)] * 2}) + result = cla.run(gh, "pull_request_target", {"pull_request": {"number": 7}}) + assert not result.passed and not result.unsigned + assert len(result.unknown) == 1 and result.unknown[0].startswith("#12 has more than 2 commits") + + +def test_commit_with_more_authors_than_read_fails_closed(): + signed = {"version": "1.0", "signatures": [{"id": 1001}]} + gh = FakeGitHub([], [{**actor("alice", 1001), "more": True}], store=signed) result = cla.run(gh, "pull_request_target", {"pull_request": {"number": 7}}) - assert result.unsigned == [cla.Person("alice", 1001), cla.Person("frank", 6006)] + assert not result.passed and result.unknown == [ + f"commit fffffff has more than {cla.MAX_AUTHORS} authors; a maintainer must review it by hand"] + assert gh.statuses[-1]["state"] == "failure" + assert f"authors(first: {cla.MAX_AUTHORS})" in cla.COMMITS_QUERY and "hasNextPage" in cla.COMMITS_QUERY + + +def _implicitly_concatenated_elements(source: str) -> list[int]: + """Line numbers of list/tuple/set elements made of adjacent string literals.""" + import ast + import io + import tokenize + + starts = {"STRING", "FSTRING_START", "TSTRING_START"} + found, lines = [], source.splitlines(keepends=True) + for node in ast.walk(ast.parse(source)): + if isinstance(node, (ast.List, ast.Tuple, ast.Set)): + for element in node.elts: + if isinstance(element, (ast.Constant, ast.JoinedStr)) and isinstance(getattr(element, "value", ""), str): + segment = ast.get_source_segment(source, element) or "" + before = "".join(lines[: element.lineno - 1]) + lines[element.lineno - 1].encode()[: element.col_offset].decode() + if before.rstrip().endswith("("): + continue # parenthesized, so the join is deliberate + tokens = tokenize.generate_tokens(io.StringIO(segment).readline) + if sum(tokenize.tok_name[t.type] in starts for t in tokens) > 1: + found.append(element.lineno) + return found + + +def test_detector_flags_implicit_concatenation_in_lists(): + assert _implicitly_concatenated_elements('x = ["a" "b", "c"]\n') == [1] + assert _implicitly_concatenated_elements('x = ["a", ("b" "c")]\ny = ("a" "b")\n') == [] + + +@pytest.mark.parametrize("path", [_SCRIPT, _SCRIPT.parents[2] / "scripts" / "cla_audit.py", + _SCRIPT.parents[2] / "scripts" / "check_commit_identities.py"]) +def test_no_implicit_string_concatenation_in_lists(path): + # Adjacent literals in a list read like a missing comma (CodeQL + # py/implicit-string-concatenation-in-list): join them in parentheses. + assert _implicitly_concatenated_elements(path.read_text(encoding="utf-8")) == [] def test_maintainer_override_label_passes_without_checking(): diff --git a/tests/test_cla_workflow.py b/tests/test_cla_workflow.py index ae30832ca..9e75febc9 100644 --- a/tests/test_cla_workflow.py +++ b/tests/test_cla_workflow.py @@ -66,6 +66,15 @@ def test_workflow_comment_filter_admits_the_sign_phrase_and_recheck(): assert "recheck" in needles +def test_pull_request_changes_that_affect_the_result_rerun_the_check(): + workflow = _load(_CLA_WORKFLOW) + types = set(workflow["on"]["pull_request_target"]["types"]) + # New commits, (un)applying `cla-override`, and editing `Supersedes #N` + # all change who must sign, so each must refresh the CLA status. + assert {"opened", "synchronize", "reopened", "labeled", "unlabeled", "edited"} <= types + assert workflow["jobs"]["cla"]["if"].startswith("github.event_name == 'pull_request_target' ||") + + def test_published_agreements_are_never_edited(): for path, expected in _PUBLISHED_AGREEMENTS.items(): text = (_REPO / path).read_text(encoding="utf-8").replace("\r\n", "\n") From 8d3b2a16deedd6d64c00221b7403cbc9b9a7e625 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:25:44 +0530 Subject: [PATCH 44/85] fix(ci): match only agent identities and catch agent names and session links - Block known agent addresses and agent app no-reply forms from the shared list instead of whole AI-company domains, so people with work emails pass. - Check author and committer names against unambiguous agent names; a person named Claude still passes. Trailer names keep the broader list. - Flag agent session and share links (Claude and ChatGPT web apps) as agent attribution. - Pinned-action scan reads quoted uses: scalars and fails on any reference it cannot find; identity tests no longer hard-code the digest count or the checkout SHA. --- scripts/check_commit_identities.py | 44 ++++++++++--------- tests/test_actions_pinned.py | 37 +++++++++++++--- tests/test_commit_identities.py | 68 ++++++++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 31 deletions(-) diff --git a/scripts/check_commit_identities.py b/scripts/check_commit_identities.py index d3c37af09..16e112861 100755 --- a/scripts/check_commit_identities.py +++ b/scripts/check_commit_identities.py @@ -10,9 +10,13 @@ * literal placeholder/tool identities that are not personal data (``mergetest``, ``test@local``, ``you@example.com``) and hostname-style auto-detected emails (``*.local``, ``*.localdomain``, ``*.(none)``); -* AI agents: an agent email in any field, an agent name in a trailer, or an - agent attribution line ("Generated with ...", session links) in the commit - message or, with ``--event``, the pull request description. Commits carry +* AI agents: an agent email in any field, an unambiguous agent name as author + or committer (a person may be called Claude, so only names such as "Claude + Code" or "Cursor Agent"), any agent name in a trailer, or an agent + attribution line ("Generated with ...", session or share links) in the + commit message or, with ``--event``, the pull request description. Agent + identities live in ``.github/scripts/agent_identities.py``, shared with the + CLA check. Commits carry the git identity of the person submitting them and nobody else's; human co-authors are allowed. @@ -28,6 +32,7 @@ import argparse import hashlib +import importlib.util import json import os import re @@ -36,6 +41,10 @@ from pathlib import Path DEFAULT_HASH_FILE = Path(__file__).with_name("blocked_identity_hashes.txt") +_AGENTS_FILE = Path(__file__).resolve().parents[1] / ".github" / "scripts" / "agent_identities.py" +_spec = importlib.util.spec_from_file_location("agent_identities", _AGENTS_FILE) +agents = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(agents) HASHED_KIND = "blocked personal email (hashed)" BLOCKED_NAMES = {"mergetest": "placeholder identity name 'mergetest'"} @@ -50,24 +59,16 @@ AI_KIND = "AI agent identity" AI_ATTRIBUTION_KIND = "AI agent attribution" -# Agents that commit or get credited as co-authors. GitHub App bots such as -# dependabot[bot] are not agents and stay allowed. -AI_EMAILS = re.compile( - r"^([^@]+@(anthropic\.com|cursor\.com|openai\.com|coderabbit\.ai|devin\.ai|cognition\.ai)" - r"|codex@users\.noreply\.github\.com" - r"|(\d+\+)?(copilot|claude|codex|cursor|devin-ai-integration|google-labs-jules|copilot-swe-agent" - r"|coderabbitai|openhands-agent|sweep-ai)(\[bot\])?@users\.noreply\.github\.com)$" -) -# Only checked on trailers: a human author may be called Claude. -AI_NAMES = re.compile( - r"^(claude( (code|opus|sonnet|haiku|fable|\d).*)?|cursor( agent)?|cursoragent|(github )?copilot" - r"|(openai )?codex|chatgpt|devin( ai)?|gemini( code assist)?|(google )?jules|aider|cline|coderabbit(ai)?)" - r"(\[bot\])?$" -) +# Agents that commit or get credited as co-authors (agent_identities.py). GitHub +# App bots such as dependabot[bot] are not agents and stay allowed, and so do +# people with a work address at an AI company. _AGENTS = r"(claude|cursor|copilot|codex|chatgpt|openai|gemini|devin|jules|aider|cline|windsurf|an? ai\b|ai\b)" AI_ATTRIBUTION = re.compile( rf"^[ \t>*_-]*(generated|written|created|authored)[ \t]+(with|by|using)[ \t]+\[?{_AGENTS}" - r"|🤖[ \t]*(generated|written|created|authored)\b|claude\.(com|ai)/(claude-)?code|^[ \t]*claude-session[ \t]*:", + r"|🤖[ \t]*(generated|written|created|authored)\b|^[ \t]*claude-session[ \t]*:" + # Agent session and share links. + r"|\bclaude\.ai/(c|chat|share|code|new)\b|\bclaude\.com/(claude-)?code\b" + r"|\b(chatgpt\.com|chat\.openai\.com)/(c|share)/", re.IGNORECASE | re.MULTILINE, ) @@ -110,18 +111,19 @@ def classify_email(email: str, hashes: set[str]) -> list[str]: kinds.append(BLOCKED_LOCALPARTS[local]) if "@" in value and value.endswith(HOSTNAME_SUFFIXES): kinds.append(HOSTNAME_KIND) - if AI_EMAILS.match(value): + if agents.is_agent_email(value): kinds.append(AI_KIND) return kinds def classify_name(name: str) -> list[str]: + """Author or committer name: placeholders and unambiguous agent names.""" kind = BLOCKED_NAMES.get(name.strip().lower()) - return [kind] if kind else [] + return ([kind] if kind else []) + ([AI_KIND] if agents.is_agent_author_name(name) else []) def classify_trailer_name(name: str) -> list[str]: - return [AI_KIND] if AI_NAMES.match(" ".join(name.lower().split())) else [] + return [AI_KIND] if agents.is_agent_trailer_name(name) else [] def classify_text(text: str) -> list[str]: diff --git a/tests/test_actions_pinned.py b/tests/test_actions_pinned.py index 1bf15758f..84172c006 100644 --- a/tests/test_actions_pinned.py +++ b/tests/test_actions_pinned.py @@ -10,6 +10,7 @@ """ from __future__ import annotations +import collections import pathlib import re @@ -72,14 +73,36 @@ def test_remote_actions_pinned_to_commit_sha(): ) -def test_pins_carry_version_comment(): +def _missing_version_comments(text: str, refs: list[str]) -> list[str]: + """References whose ``uses:`` lines lack a trailing comment. + + Matches plain, single- and double-quoted scalars. Every collected + reference must be found on as many lines as it is used, so a form this + scan cannot read fails instead of passing unchecked. + """ bad = [] - for path, refs in _collect().items(): - text = path.read_text(encoding="utf-8") - for u in set(refs): - for m in re.finditer(rf"uses:\s*{re.escape(u)}([^\n]*)", text): - if not re.match(r"\s+#\s*\S", m.group(1)): - bad.append(f"{path.relative_to(_ROOT)}: {u}") + for u, count in collections.Counter(refs).items(): + lines = re.findall(rf"""uses:[ \t]*(["']?){re.escape(u)}\1([^\n]*)""", text) + if len(lines) < count or any(not re.match(r"\s+#\s*\S", rest) for _, rest in lines): + bad.append(u) + return bad + + +def test_version_comment_scan_reads_quoted_scalars(): + sha = "actions/checkout@" + "0" * 40 + for quote in ("", "'", '"'): + line = f" - uses: {quote}{sha}{quote}" + assert _missing_version_comments(line + " # v5.0.0\n", [sha]) == [] + assert _missing_version_comments(line + "\n", [sha]) == [sha] + assert _missing_version_comments("", [sha]) == [sha] # unreadable form fails closed + + +def test_pins_carry_version_comment(): + bad = [ + f"{path.relative_to(_ROOT)}: {u}" + for path, refs in _collect().items() + for u in _missing_version_comments(path.read_text(encoding="utf-8"), refs) + ] assert not bad, "Add a '# vX.Y.Z' comment after each SHA pin:\n" + "\n".join(bad) diff --git a/tests/test_commit_identities.py b/tests/test_commit_identities.py index d1c046ca0..da2d73c6a 100644 --- a/tests/test_commit_identities.py +++ b/tests/test_commit_identities.py @@ -144,8 +144,7 @@ def test_shipped_hash_list_is_digests_only(): for line in HASH_FILE.read_text(encoding="utf-8").splitlines() if line.strip() and not line.strip().startswith("#") ] - assert len(entries) == 5 - assert len(set(entries)) == 5 + assert entries and len(set(entries)) == len(entries) assert all(re.fullmatch(r"[0-9a-f]{64}", e) for e in entries) @@ -156,7 +155,7 @@ def test_workflow_runs_on_untrusted_safe_pull_request(): assert wf["permissions"] == {"contents": "read"} (job,) = wf["jobs"].values() checkout, run = job["steps"] - assert checkout["uses"] == "actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8" + assert re.fullmatch(r"actions/checkout@[0-9a-f]{40}", checkout["uses"]) assert checkout["with"] == {"fetch-depth": "0", "persist-credentials": "false"} assert "${{" not in run["run"] assert run["env"]["BASE_REF"] == "${{ github.base_ref }}" @@ -180,6 +179,15 @@ def test_workflow_runs_on_untrusted_safe_pull_request(): ("feat: x\n\n\U0001f916 Generated with [Some Agent](https://example.com)", "message: AI agent attribution"), ("feat: x\n\nGenerated with Codex", "message: AI agent attribution"), ("feat: x\n\nClaude-Session: abc123", "message: AI agent attribution"), + ("feat: x\n\nSession: https://claude.ai/code/session_abc", "message: AI agent attribution"), + ("feat: x\n\nSee https://claude.ai/share/0f1e2d", "message: AI agent attribution"), + ("feat: x\n\nSee https://claude.ai/chat/0f1e2d", "message: AI agent attribution"), + ("feat: x\n\nSee claude.ai/c/0f1e2d", "message: AI agent attribution"), + ("feat: x\n\nVia https://claude.ai/new", "message: AI agent attribution"), + ("feat: x\n\nBuilt in https://claude.com/claude-code", "message: AI agent attribution"), + ("feat: x\n\nPlan: https://chatgpt.com/share/abc-123", "message: AI agent attribution"), + ("feat: x\n\nPlan: https://chatgpt.com/c/abc-123", "message: AI agent attribution"), + ("feat: x\n\nPlan: https://chat.openai.com/share/abc-123", "message: AI agent attribution"), ], ) def test_ai_agent_attribution_in_commits_is_blocked(repo, message, expected): @@ -196,12 +204,66 @@ def test_ai_agent_as_author_is_blocked(repo): assert result.returncode == 1 and "author email: AI agent identity" in result.stdout +def _at(local: str, domain: str) -> str: + return local + "@" + domain + + +@pytest.mark.parametrize( + "email", + [ + _at("noreply", "anthropic.com"), _at("cursoragent", "cursor.com"), _at("codex", "openai.com"), + _at("noreply", "openai.com"), _at("noreply", "coderabbit.ai"), _at("codex", "users.noreply.github.com"), + _at("198982749+Copilot", "users.noreply.github.com"), _at("209825114+claude[bot]", "users.noreply.github.com"), + _at("158243242+devin-ai-integration[bot]", "users.noreply.github.com"), + _at("161369871+google-labs-jules[bot]", "users.noreply.github.com"), + _at("136622811+coderabbitai[bot]", "users.noreply.github.com"), + ], +) +def test_agent_emails_are_blocked(repo, email): + _, commit, check = repo + sha = commit("feat: x", author=("Someone", email)) + result = check(*_only(sha)) + assert result.returncode == 1 and "author email: AI agent identity" in result.stdout + + +@pytest.mark.parametrize( + "email", [_at("someone", "anthropic.com"), _at("someone", "openai.com"), _at("dev", "cursor.com"), + _at("dev", "coderabbit.ai"), _at("dev", "devin.ai")], +) +def test_people_with_work_emails_at_ai_companies_pass(repo, email): + _, commit, check = repo + sha = commit(f"feat: x\n\nCo-authored-by: Sam <{email}>", author=("Sam", email), committer=("Sam", email)) + assert check(*_only(sha)).returncode == 0 + + +@pytest.mark.parametrize( + "name", + ["Claude Code", "Claude Opus 5.5", "claude 3.5 sonnet", "Cursor Agent", "cursoragent", "GitHub Copilot", + "Copilot", "Codex", "OpenAI Codex", "Devin AI", "devin-ai-integration[bot]", "google-labs-jules[bot]", + "coderabbitai[bot]", "claude[bot]"], +) +def test_agent_author_and_committer_names_are_blocked(repo, name): + _, commit, check = repo + for field, kwargs in (("author", {"author": (name, CLEAN[1])}), ("committer", {"committer": (name, CLEAN[1])})): + sha = commit("feat: x", **kwargs) + result = check(*_only(sha)) + assert result.returncode == 1 and f"{field} name: AI agent identity" in result.stdout, (field, name) + + +@pytest.mark.parametrize("name", ["Claude", "Claude Monet", "Jules", "Devin", "Cursor Smith"]) +def test_people_named_like_agents_can_author(repo, name): + _, commit, check = repo + sha = commit("feat: x", author=(name, CLEAN[1]), committer=(name, CLEAN[1])) + assert check(*_only(sha)).returncode == 0 + + @pytest.mark.parametrize( "message", [ f"feat: pair work\n\nCo-authored-by: {HUMAN}", "feat: x\n\nCo-authored-by: Claude Monet ", # a person called Claude "fix: audio generated with the default model sounded clipped", + "docs: link the claude.ai status page and chatgpt.com/pricing", ], ) def test_people_and_ordinary_prose_pass(repo, message): From ffd503c878cb6826aff737bc85290e41733fc7cb Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:26:10 +0530 Subject: [PATCH 45/85] docs(maintainers): require pull requests on main and state the CLA section 4 limit - The main ruleset adds a pull_request rule (no required approvals for a solo maintainer) so a direct push with passing checks is rejected; the UI steps match. The admin bypass stays pull-request only. - The contributing guide says the open-source commitment covers contributions while they are in the public repository, under AGPL-3.0 or another OSI-approved licence. - Tests keep both in step with the agreement and ruleset. --- .github/CONTRIBUTING.md | 5 +++-- docs/maintainers/repository-settings.md | 25 ++++++++++++++++----- tests/test_cla_workflow.py | 29 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7ead51f8c..c4a1caa61 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -357,8 +357,9 @@ a pull request can merge, the person who opened it and every commit author and co-author sign the [Contributor License Agreement](CLA-1.0.md) once. You keep your copyright. The agreement lets Yupcha Softwares Private Limited, the company that maintains VoiceStudio, ship your work in both the AGPL-3.0 app and -commercial builds, and commits to keeping it available under an open-source -licence. AI agents can't be co-authors (see +commercial builds. In return, the company commits that while your contribution +is in the public VoiceStudio repository, it stays available there under +AGPL-3.0 or another OSI-approved licence (CLA section 4). AI agents can't be co-authors (see [Contributing with AI agents](#contributing-with-ai-agents)); the person submitting the work signs for it. diff --git a/docs/maintainers/repository-settings.md b/docs/maintainers/repository-settings.md index 452592f9a..580fac3a8 100644 --- a/docs/maintainers/repository-settings.md +++ b/docs/maintainers/repository-settings.md @@ -24,9 +24,11 @@ the description: the check then asks their authors to sign too. ## Ruleset: `main` -Requires the backend/frontend test job and the CLA check, and blocks force-push -and deletion. Required checks also stop direct pushes, so changes land through -pull requests. +Requires a pull request, the backend/frontend test job and the CLA check, and +blocks force-push and deletion. Without the pull request rule, a direct push +whose commit already carries passing checks would be accepted. The rule needs +no approving review, because `@debpalash` is the only maintainer and GitHub +does not let authors approve their own pull requests. The `context` values must match the check names shown on a pull request: @@ -54,6 +56,16 @@ gh api --method POST repos/debpalash/VoiceStudio/rulesets --input - <<'JSON' "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false + } + }, { "type": "required_status_checks", "parameters": { @@ -78,9 +90,10 @@ Code Owners" while `@debpalash` is the only code owner, because GitHub does not let authors approve their own pull requests. UI: **Settings → Rules → Rulesets → New ruleset → New branch ruleset**. Set the -target to the default branch, enable **Restrict deletions**, **Block force -pushes** and **Require status checks to pass**, then add both checks with source -**GitHub Actions**. +target to the default branch, enable **Restrict deletions**, **Require a pull +request before merging** (required approvals: 0, all other options off), +**Block force pushes** and **Require status checks to pass**, then add both +checks with source **GitHub Actions**. ## Ruleset: `cla-signatures` diff --git a/tests/test_cla_workflow.py b/tests/test_cla_workflow.py index 9e75febc9..fc6a19865 100644 --- a/tests/test_cla_workflow.py +++ b/tests/test_cla_workflow.py @@ -10,6 +10,7 @@ import hashlib import importlib.util +import json import re import sys from pathlib import Path @@ -113,3 +114,31 @@ def test_privileged_workflows_never_run_pull_request_code(): assert not {"ref", "repository"} & set(options), f"{where} checks out a non-base ref" assert options.get("persist-credentials") is False, f"{where} keeps credentials" assert "${{ github.event" not in str(step.get("run", "")), f"{where} interpolates event data" + + +def _main_ruleset() -> dict: + text = (_REPO / "docs" / "maintainers" / "repository-settings.md").read_text(encoding="utf-8") + blocks = [json.loads(b) for b in re.findall(r"<<'JSON'\n(.*?)\nJSON\n", text, re.DOTALL)] + return next(b for b in blocks if b.get("name") == "main") + + +def test_main_ruleset_requires_pull_requests_and_the_cla_status(): + rules = {rule["type"]: rule.get("parameters", {}) for rule in _main_ruleset()["rules"]} + # Required checks alone accept a direct push whose commit already passed. + assert rules["pull_request"] == { + "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": False, + "require_code_owner_review": False, "require_last_push_approval": False, + "required_review_thread_resolution": False, + } + contexts = {c["context"] for c in rules["required_status_checks"]["required_status_checks"]} + assert cla.STATUS_CONTEXT in contexts + assert {"deletion", "non_fast_forward"} <= set(rules) + + +def test_contributing_states_the_open_source_commitment_limits(): + agreement = (_REPO / cla.DOCUMENT_PATH).read_text(encoding="utf-8") + assert "Open Source\nInitiative" in agreement and "public source repository" in agreement + section = (_REPO / ".github" / "CONTRIBUTING.md").read_text(encoding="utf-8").split("## Contribution licensing")[1] + summary = " ".join(section.split("\n## ")[0].split()) + assert "while your contribution is in the public VoiceStudio repository" in summary + assert "AGPL-3.0 or another OSI-approved licence" in summary From 7c0b5d253938a2b419c4d2913a0cc98f6af3c056 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:27:17 +0530 Subject: [PATCH 46/85] fix(docker): pull released images and mount the cache the image uses - Compose quick start pulls :stable / :stable-rocm instead of the rolling :latest / :rocm previews; the build blocks are commented out to match the instructions, so a local build is never tagged as the release. - Drop OMNIVOICE_BIND_HOST from Compose and the in-app snippets: the image's uvicorn ENTRYPOINT binds 0.0.0.0 itself, so the variable did nothing. - The in-app docker run snippets mount the host model cache at the image's HF_HOME (/app/omnivoice_data/huggingface) instead of an unused path. - The --health-check help no longer cites the removed release workflow. --- backend/main.py | 8 +-- deploy/docker-compose.yml | 56 +++++++++---------- docs/production-private-api.md | 3 +- docs/sharing.md | 2 +- .../integrations/integration-setup.test.ts | 14 +++++ .../features/integrations/setup-registry.ts | 6 +- tests/test_bind_host.py | 4 +- tests/test_headless_worker_packaging.py | 20 ++++++- 8 files changed, 71 insertions(+), 42 deletions(-) diff --git a/backend/main.py b/backend/main.py index 9444a2b6f..a2e2fd3f7 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1953,7 +1953,7 @@ def startup_progress_endpoint(): "--health-check", action="store_true", help="Boot the server, poll /health, exit 0 on success / 1 on timeout. " - "Used by the release-time installer smoke step in .github/workflows/release.yml.", + "A self-contained smoke test for an installed or packaged backend.", ) parser.add_argument( "--diagnose", @@ -2033,9 +2033,9 @@ def _serve(): # SECURITY: default to loopback (127.0.0.1) so the API isn't reachable # from the LAN out of the box. VoiceStudio ships no authentication; binding # to 0.0.0.0 by default would expose every router on this process to any - # host on the user's network. Docker images that need to publish the port - # set OMNIVOICE_BIND_HOST=0.0.0.0 explicitly (see deploy/docker-compose.yml) - # — the host-side port mapping is what enforces 127.0.0.1-only there. + # host on the user's network. The Docker image never reaches this block: + # its uvicorn ENTRYPOINT binds 0.0.0.0 itself (deploy/Dockerfile), and the + # host-side `127.0.0.1:` port mapping is what keeps it loopback-only there. _bind_host = os.environ.get("OMNIVOICE_BIND_HOST", "127.0.0.1") def _port_taken(host: str, port: int) -> "OSError | None": diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 60e165a6b..c79349bef 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -14,6 +14,9 @@ # First run downloads ~4 GB of models. Progress is shown in logs. # Open http://localhost:3900 once the health check passes. # +# Images: `:stable` / `:stable-rocm` are the latest tagged release; use +# `:latest` / `:rocm` to follow the rolling preview built from main. +# # SECURITY: The port is bound to 127.0.0.1 by default — only this # machine can reach the API. To expose VoiceStudio on your LAN (or # through a reverse proxy / tunnel), change the port mapping to @@ -25,12 +28,12 @@ services: # ── CPU mode — activate with: docker compose --profile cpu up omnivoice: - image: ghcr.io/debpalash/voicestudio:latest + image: ghcr.io/debpalash/voicestudio:stable # To build from source instead of pulling, comment out `image:` and - # uncomment the two lines below: - build: - context: .. - dockerfile: deploy/Dockerfile + # uncomment the three lines below: + # build: + # context: .. + # dockerfile: deploy/Dockerfile container_name: omnivoice-studio profiles: ["cpu"] ports: @@ -44,12 +47,10 @@ services: - OMNIVOICE_DATA_DIR=/app/omnivoice_data - PYTHONPATH=/app/backend - PYTHONUNBUFFERED=1 - # Bind uvicorn to 0.0.0.0 *inside* the container so the host-side port - # mapping above can forward traffic in. The 127.0.0.1 prefix on the - # `ports:` mapping is what enforces loopback-only on the host — - # OMNIVOICE_BIND_HOST=0.0.0.0 here only opens the container's own - # interface. The backend default is 127.0.0.1 (see backend/main.py). - - OMNIVOICE_BIND_HOST=0.0.0.0 + # The image's uvicorn ENTRYPOINT binds 0.0.0.0 *inside* the container so + # the host-side port mapping above can forward traffic in. The 127.0.0.1 + # prefix on the `ports:` mapping is what enforces loopback-only on the + # host. # Headless server: relax the desktop-only loopback origin gate so the # web UI's /system/* and /api/settings/* routes work through Docker's # NAT (issue #261). Already baked into the image; shown here so it's @@ -71,10 +72,11 @@ services: # ── GPU mode — activate with: docker compose --profile gpu up omnivoice-gpu: - image: ghcr.io/debpalash/voicestudio:latest - build: - context: .. - dockerfile: deploy/Dockerfile + image: ghcr.io/debpalash/voicestudio:stable + # To build from source, swap `image:` for the commented `build:` lines. + # build: + # context: .. + # dockerfile: deploy/Dockerfile container_name: omnivoice-studio-gpu profiles: ["gpu"] ports: @@ -88,10 +90,9 @@ services: - OMNIVOICE_DATA_DIR=/app/omnivoice_data - PYTHONPATH=/app/backend - PYTHONUNBUFFERED=1 - # Bind uvicorn to 0.0.0.0 *inside* the container — same as the CPU - # service above. The host-side `127.0.0.1:3900:3900` mapping keeps - # LAN reachability off by default. - - OMNIVOICE_BIND_HOST=0.0.0.0 + # Container-internal 0.0.0.0 bind comes from the image ENTRYPOINT — same + # as the CPU service above. The host-side `127.0.0.1:3900:3900` mapping + # keeps LAN reachability off by default. # See the CPU service above — relaxes the loopback origin gate for the # headless Docker deployment (issue #261). Set to 0 to re-enable it. - OMNIVOICE_SERVER_MODE=1 @@ -119,7 +120,7 @@ services: # the amdgpu kernel driver (the ROCm userspace ships inside the image). # Podman works with the same two --device flags (Quadlet: AddDevice=). omnivoice-rocm: - image: ghcr.io/debpalash/voicestudio:rocm + image: ghcr.io/debpalash/voicestudio:stable-rocm # To build from source instead of pulling, comment out `image:` and # uncomment the lines below. BASE_IMAGE/GPU_FLAVOR are required — the # Dockerfile's defaults build the CUDA variant. @@ -145,9 +146,8 @@ services: - OMNIVOICE_DATA_DIR=/app/omnivoice_data - PYTHONPATH=/app/backend - PYTHONUNBUFFERED=1 - # See the CPU service above — container-internal bind + relaxed - # loopback origin gate for the headless Docker deployment. - - OMNIVOICE_BIND_HOST=0.0.0.0 + # See the CPU service above — relaxed loopback origin gate for the + # headless Docker deployment. - OMNIVOICE_SERVER_MODE=1 - OMNIVOICE_API_KEY=${OMNIVOICE_API_KEY:?export a long random OMNIVOICE_API_KEY before starting a Studio profile} - OMNIVOICE_WORKER_PORT=${OMNIVOICE_WORKER_PORT:-7443} @@ -171,10 +171,10 @@ services: # No port is published: uvicorn only hosts the application lifespan that # owns the outbound worker agent. The browser UI is not needed or exposed. omnivoice-worker-gpu: - image: ghcr.io/debpalash/voicestudio:latest - build: - context: .. - dockerfile: deploy/Dockerfile + image: ghcr.io/debpalash/voicestudio:stable + # build: + # context: .. + # dockerfile: deploy/Dockerfile container_name: omnivoice-worker-gpu profiles: ["worker-gpu"] entrypoint: ["python3", "-m", "uvicorn"] @@ -208,7 +208,7 @@ services: # ── Worker-only AMD GPU (ROCm) mode omnivoice-worker-rocm: - image: ghcr.io/debpalash/voicestudio:rocm + image: ghcr.io/debpalash/voicestudio:stable-rocm container_name: omnivoice-worker-rocm profiles: ["worker-rocm"] entrypoint: ["python3", "-m", "uvicorn"] diff --git a/docs/production-private-api.md b/docs/production-private-api.md index da399a881..9cd9b7dc1 100644 --- a/docs/production-private-api.md +++ b/docs/production-private-api.md @@ -13,7 +13,6 @@ services: restart: unless-stopped environment: OMNIVOICE_API_KEY: ${OMNIVOICE_API_KEY:?set a long random key} - OMNIVOICE_BIND_HOST: 0.0.0.0 OMNIVOICE_DATA_DIR: /app/omnivoice_data ports: - "127.0.0.1:3900:3900" @@ -37,7 +36,7 @@ Generate `OMNIVOICE_API_KEY` with a password manager or deployment platform's secret store, not in the Compose file or source control. Send it from InterviewAce as `Authorization: Bearer `. -`OMNIVOICE_BIND_HOST=0.0.0.0` is required inside the container; the host-side +The image binds `0.0.0.0` inside the container on its own; the host-side `127.0.0.1` port binding still prevents LAN or public access. `OMNIVOICE_DATA_DIR=/app/omnivoice_data` keeps application state on the named volume across container recreation. diff --git a/docs/sharing.md b/docs/sharing.md index acb397456..7b41d3818 100644 --- a/docs/sharing.md +++ b/docs/sharing.md @@ -46,4 +46,4 @@ Tailscale proxies the loopback backend directly, so — like LAN sharing — it ## Notes - Both paths leave the running model and in-flight jobs **completely untouched**. -- Server deployments (docker, `OMNIVOICE_BIND_HOST=0.0.0.0`) manage their own networking; the in-app toggle is for the desktop app and is unaffected by these flows. +- Server deployments (Docker, or a source backend started with `OMNIVOICE_BIND_HOST=0.0.0.0`) manage their own networking; the in-app toggle is for the desktop app and is unaffected by these flows. diff --git a/electron/src/renderer/src/features/integrations/integration-setup.test.ts b/electron/src/renderer/src/features/integrations/integration-setup.test.ts index 859754742..e1b2fb7ae 100644 --- a/electron/src/renderer/src/features/integrations/integration-setup.test.ts +++ b/electron/src/renderer/src/features/integrations/integration-setup.test.ts @@ -279,6 +279,20 @@ it('gives Windows a PowerShell docker run with a CSPRNG key and backtick continu expect(ps.text.trimEnd()).toMatch(/(?:voicestudio|omnivoice-studio):stable$/); } }); +it('mounts the host model cache at the image HF_HOME and adds no dead bind knob', () => { + for (const slug of ['docker', 'github-container-registry']) { + const text = INTEGRATION_SETUPS[slug] + .blocks('')! + .map((block) => block.text) + .join('\n'); + // deploy/Dockerfile: HF_HOME=/app/omnivoice_data/huggingface. + expect(text).toContain('.cache/huggingface:/app/omnivoice_data/huggingface'); + expect(text).not.toContain('/root/.cache/huggingface'); + // The image ENTRYPOINT binds 0.0.0.0 itself; the env var does nothing. + expect(text).not.toContain('OMNIVOICE_BIND_HOST'); + expect(text).toMatch(/:stable\b/); + } +}); it('reads the Agents SDK key only for a remote https backend', () => { for (const url of ['http://192.168.1.5:3900', 'http://127.0.0.1:3900']) { const text = openaiAgentsSetup('openai-agents', url)!.text; diff --git a/electron/src/renderer/src/features/integrations/setup-registry.ts b/electron/src/renderer/src/features/integrations/setup-registry.ts index 891059cb8..4269ae2fc 100644 --- a/electron/src/renderer/src/features/integrations/setup-registry.ts +++ b/electron/src/renderer/src/features/integrations/setup-registry.ts @@ -102,7 +102,8 @@ function dockerRun(image: string) { ' -p 127.0.0.1:3900:3900 \\', ' -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \\', ' -v omnivoice-data:/app/omnivoice_data \\', - ' -v ~/.cache/huggingface:/root/.cache/huggingface \\', + // The image's HF_HOME; mounting the host cache there reuses its downloads. + ' -v ~/.cache/huggingface:/app/omnivoice_data/huggingface \\', ` ${image}:stable`, '', ].join('\n'); @@ -119,7 +120,7 @@ function dockerRunPowerShell(image: string) { ' -p 127.0.0.1:3900:3900 `', ' -e OMNIVOICE_API_KEY="$env:OMNIVOICE_API_KEY" `', ' -v omnivoice-data:/app/omnivoice_data `', - ' -v "${HOME}/.cache/huggingface:/root/.cache/huggingface" `', + ' -v "${HOME}/.cache/huggingface:/app/omnivoice_data/huggingface" `', ` ${image}:stable`, '', ].join('\n'); @@ -133,7 +134,6 @@ function dockerCompose(image: string) { ' ports:', ' - "127.0.0.1:3900:3900"', ' environment:', - ' - OMNIVOICE_BIND_HOST=0.0.0.0', ' - OMNIVOICE_API_KEY=${OMNIVOICE_API_KEY:?set a long random key}', ' - OMNIVOICE_DATA_DIR=/app/omnivoice_data', ' - HF_HOME=/app/omnivoice_data/huggingface', diff --git a/tests/test_bind_host.py b/tests/test_bind_host.py index ab03c3a33..88ff647f7 100644 --- a/tests/test_bind_host.py +++ b/tests/test_bind_host.py @@ -4,7 +4,7 @@ `backend/main.py` resolves its uvicorn bind host from `OMNIVOICE_BIND_HOST` with a default of `127.0.0.1`. Binding to `0.0.0.0` by default would expose every (currently unauthenticated) router on the user's LAN — see the security -note in `deploy/docker-compose.yml`. +note above the bind in `backend/main.py`. These tests don't boot uvicorn. They re-evaluate the resolution expression used in `main.py`'s `__main__` block, both as a string-level guard against @@ -39,7 +39,7 @@ def test_explicit_loopback_env_var_is_honored(): def test_explicit_all_interfaces_env_var_is_honored(): - # Used by deploy/docker-compose.yml — must still work as an opt-in override. + # An explicit opt-in for `python backend/main.py` (and `bun run dev:api`). assert _resolve_bind_host({"OMNIVOICE_BIND_HOST": "0.0.0.0"}) == "0.0.0.0" diff --git a/tests/test_headless_worker_packaging.py b/tests/test_headless_worker_packaging.py index f0b840958..41ec4b7b9 100644 --- a/tests/test_headless_worker_packaging.py +++ b/tests/test_headless_worker_packaging.py @@ -28,8 +28,8 @@ def _environment(service: dict) -> dict[str, str]: @pytest.mark.parametrize( ("service_name", "profile", "image_suffix"), [ - ("omnivoice-worker-gpu", "worker-gpu", ":latest"), - ("omnivoice-worker-rocm", "worker-rocm", ":rocm"), + ("omnivoice-worker-gpu", "worker-gpu", ":stable"), + ("omnivoice-worker-rocm", "worker-rocm", ":stable-rocm"), ], ) def test_compose_has_worker_only_gpu_profiles(service_name, profile, image_suffix): @@ -90,3 +90,19 @@ def test_headless_docs_and_acceptance_script_use_the_supported_backend_command() assert HEADLESS_SOURCE_COMMAND in acceptance assert "OMNIVOICE_WORKER_MODE=1 omnivoice" not in guide assert "OMNIVOICE_WORKER_MODE=1 omnivoice" not in acceptance + + +def test_compose_quick_start_pulls_released_images_without_dead_knobs(): + """`:latest` is the rolling main preview; the quick start must pull the + release. An active `build:` next to `image:` would tag a local build as + that release, and OMNIVOICE_BIND_HOST is a no-op under the image's uvicorn + ENTRYPOINT, which binds 0.0.0.0 itself.""" + compose = yaml.safe_load( + (ROOT / "deploy" / "docker-compose.yml").read_text(encoding="utf-8") + ) + dockerfile = (ROOT / "deploy" / "Dockerfile").read_text(encoding="utf-8") + assert '"--host", "0.0.0.0"' in dockerfile + for name, service in compose["services"].items(): + assert service["image"].rsplit(":", 1)[1] in {"stable", "stable-rocm"}, name + assert "build" not in service, name + assert "OMNIVOICE_BIND_HOST" not in _environment(service), name From 59a28bb509e1c4367b621b87ba390244b85a1dea Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:28:49 +0530 Subject: [PATCH 47/85] fix(prefs): drop preference reads that nothing ever writes mcp_default_profile_id, pronunciation_enabled and text_normalization_enabled were read but no Settings surface or route ever wrote them, so they could only be set by hand-editing prefs.json. The reads are removed; the OMNIVOICE_PRONUNCIATION and OMNIVOICE_TEXT_NORMALIZATION env switches keep working, and MCP voice resolution is explicit argument, then the agent's binding, then the default voice. --- backend/api/routers/generation.py | 21 ++++++++++---------- backend/mcp_server.py | 8 ++++---- backend/services/mcp_bindings.py | 18 +++-------------- backend/services/text_normalization.py | 18 ++++++----------- docs/mcp.md | 3 +-- tests/test_mcp_bindings.py | 8 +++++--- tests/test_text_normalization.py | 27 ++++++++++++++++++-------- 7 files changed, 49 insertions(+), 54 deletions(-) diff --git a/backend/api/routers/generation.py b/backend/api/routers/generation.py index d476e1ad3..953da0955 100644 --- a/backend/api/routers/generation.py +++ b/backend/api/routers/generation.py @@ -1714,8 +1714,8 @@ async def generate_speech( max_chunk_chars: int = Form(800, ge=0), crossfade_ms: int = Form(50, ge=0, le=1000), # Expressive-TTS Spec 01: apply the user pronunciation dictionary + inline - # [[…]] overrides to the text before synthesis. Default ON; the global - # OMNIVOICE_PRONUNCIATION pref can disable it for power users. Omitting it + # [[…]] overrides to the text before synthesis. Default ON; the + # OMNIVOICE_PRONUNCIATION env var can disable it for power users. Omitting it # with an empty dictionary is byte-identical to legacy behavior. pronounce: bool = Form(True), # Streaming preview: when true, the response is application/x-ndjson — @@ -2061,14 +2061,7 @@ async def generate_speech( # covers generate for every engine. Pure text substitution → identical on # mac/Win/Linux. A disabled pref or empty dictionary is a pass-through, so # plain text stays byte-identical (#G5 backward-compat). - from core import prefs as _prefs - _pron_env = os.environ.get("OMNIVOICE_PRONUNCIATION") - if _pron_env is not None: - # Env wins (power-user override); "0"/"false"/"no"/"off" disable it. - _pron_enabled = _pron_env.strip().lower() not in ("0", "false", "no", "off", "") - else: - _pron_enabled = bool(_prefs.get("pronunciation_enabled", True)) - if pronounce and _pron_enabled: + if pronounce and pronunciation_enabled(): from services.pronunciation import apply_pronunciation, load_entries_from_db try: _pron_rows = load_entries_from_db() @@ -2930,6 +2923,14 @@ def _remove_wav_if_unreferenced(conn, audio_path, exclude_ids=(), defer=None): # WAVs). User-tunable via Settings → Storage; 0 = unlimited. The pref key is # shared with api/routers/settings.py (the GET/PUT endpoint) — same pattern as # perf.torch_compile_disabled, which settings.py and engine_env.py both name. +def pronunciation_enabled() -> bool: + """The pronunciation dictionary is on unless OMNIVOICE_PRONUNCIATION is + "0"/"false"/"no"/"off". Env only: a prefs key was once read here too, but + no Settings surface ever wrote it.""" + value = os.environ.get("OMNIVOICE_PRONUNCIATION") + return value is None or value.strip().lower() not in ("0", "false", "no", "off", "") + + HISTORY_CAP_PREF_KEY = "generation_history_cap" DEFAULT_HISTORY_CAP = 200 diff --git a/backend/mcp_server.py b/backend/mcp_server.py index 834a0adf8..2875dda43 100644 --- a/backend/mcp_server.py +++ b/backend/mcp_server.py @@ -494,8 +494,8 @@ def _current_client_id() -> str | None: FastMCP exposes the HTTP request via its request context on the Streamable-HTTP transport; stdio clients (and any version where the - accessor differs) simply resolve to None and fall back to the - global default voice.""" + accessor differs) simply resolve to None and use the backend's + default voice.""" try: req = mcp.get_context().request_context.request if req is not None: @@ -522,7 +522,7 @@ async def generate_speech( supported. Omit to use the voice profile's saved language; an explicit 'Auto' overrides it. profile_id: ID of a saved voice profile to clone. Omit to use this - agent's bound voice (Settings → MCP), else the global default. + agent's bound voice (Settings → MCP), else the default voice. instruct: Style instruction (e.g. 'whisper', 'excited', 'narrator'). speed: Speech speed multiplier (0.5–2.0, default 1.0). steps: Diffusion steps (8=fast/draft, 16=balanced, 32=quality). @@ -545,7 +545,7 @@ async def generate_speech( if not await asyncio.to_thread(find_ffmpeg): raise RuntimeError("Ogg/Opus file output requires local ffmpeg; install it or set FFMPEG_PATH") # Per-agent voice binding (Wave 2.2): explicit arg wins; otherwise - # resolve this client's bound profile, then the global default. + # resolve this client's bound profile. client_id = _current_client_id() try: from services import mcp_bindings diff --git a/backend/services/mcp_bindings.py b/backend/services/mcp_bindings.py index 6b7285795..6c8542dab 100644 --- a/backend/services/mcp_bindings.py +++ b/backend/services/mcp_bindings.py @@ -91,24 +91,15 @@ def touch_last_seen(client_id: str) -> None: pass -def _global_default_profile() -> Optional[str]: - """The fallback voice when a client has no binding. Reads the same - pref the Settings 'default playback voice' would set; None if unset.""" - try: - from core import prefs - return prefs.get("mcp_default_profile_id") or None - except Exception: - return None - - def resolve_voice(client_id: Optional[str], explicit_profile_id: Optional[str]) -> dict: """Resolve which voice an MCP speak call should use. Precedence (Spec 2): explicit tool arg → the client's binding → - the global default → nothing (caller decides / errors with a hint). + nothing (the backend then uses its default voice). A "global default" + tier read a preference nothing ever wrote, so it was removed. Returns ``{profile_id, default_engine, source}`` where ``source`` is one - of ``explicit`` | ``binding`` | ``global`` | ``none`` for diagnostics. + of ``explicit`` | ``binding`` | ``none`` for diagnostics. """ if explicit_profile_id: return {"profile_id": explicit_profile_id, "default_engine": None, "source": "explicit"} @@ -120,7 +111,4 @@ def resolve_voice(client_id: Optional[str], explicit_profile_id: Optional[str]) "default_engine": binding.get("default_engine"), "source": "binding", } - g = _global_default_profile() - if g: - return {"profile_id": g, "default_engine": None, "source": "global"} return {"profile_id": None, "default_engine": None, "source": "none"} diff --git a/backend/services/text_normalization.py b/backend/services/text_normalization.py index 38b7f1bf6..7009d6720 100644 --- a/backend/services/text_normalization.py +++ b/backend/services/text_normalization.py @@ -51,9 +51,8 @@ Pinned by ``tests/test_text_normalization.py`` (dictionary-order test). -Gate: prefs key ``text_normalization_enabled`` (default ON) with env override -``OMNIVOICE_TEXT_NORMALIZATION`` — the same env-wins contract as -``OMNIVOICE_PRONUNCIATION`` ("0"/"false"/"no"/"off" disable). +Gate: default ON; the ``OMNIVOICE_TEXT_NORMALIZATION`` env var disables it, +the same contract as ``OMNIVOICE_PRONUNCIATION`` ("0"/"false"/"no"/"off"). :func:`normalize_for_tts` is the gated entry point every pipeline calls; it never raises — normalization is never allowed to break synthesis. """ @@ -69,7 +68,6 @@ logger = logging.getLogger("omnivoice.text_normalization") ENV_VAR = "OMNIVOICE_TEXT_NORMALIZATION" -PREF_KEY = "text_normalization_enabled" # ── Language resolution ─────────────────────────────────────────────────────── @@ -680,16 +678,12 @@ def normalize_text(text: str, language: Optional[str] = None) -> str: def normalization_enabled() -> bool: - """Env wins (power-user override, mirrors OMNIVOICE_PRONUNCIATION); - otherwise the ``text_normalization_enabled`` pref, default ON.""" + """Default ON; the env var is the power-user off switch. (A prefs key was + once read here too, but nothing ever wrote it.)""" env = os.environ.get(ENV_VAR) - if env is not None: - return env.strip().lower() not in ("0", "false", "no", "off", "") - try: - from core import prefs - return bool(prefs.get(PREF_KEY, True)) - except Exception: # noqa: BLE001 — prefs unreadable → default ON + if env is None: return True + return env.strip().lower() not in ("0", "false", "no", "off", "") def normalize_for_tts(text: str, language: Optional[str] = None) -> str: diff --git a/docs/mcp.md b/docs/mcp.md index 55d8026c7..8050f6877 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -193,8 +193,7 @@ in Scarlett". Voice resolution precedence on every `generate_speech` call: 1. an explicit `profile_id` argument, else 2. the calling agent's binding, else -3. the global default voice, else -4. VoiceStudio's default voice. +3. VoiceStudio's default voice. Manage bindings over the loopback REST API (the Settings UI uses these): diff --git a/tests/test_mcp_bindings.py b/tests/test_mcp_bindings.py index f28e408a4..e68b6709d 100644 --- a/tests/test_mcp_bindings.py +++ b/tests/test_mcp_bindings.py @@ -77,16 +77,18 @@ def test_resolution_precedence(db): r = db.resolve_voice("cursor", None) assert r["profile_id"] == "bound-voice" and r["source"] == "binding" - # Unknown client, no global default → none. + # Unknown client → none. r = db.resolve_voice("unknown", None) assert r["source"] == "none" and r["profile_id"] is None -def test_resolution_global_default(db, monkeypatch): +def test_resolution_ignores_the_never_written_global_default_pref(db, monkeypatch): + """No Settings surface ever wrote mcp_default_profile_id, so honoring a + stale or hand-edited value would pick a voice no UI shows.""" from core import prefs monkeypatch.setattr(prefs, "get", lambda k, default=None: "global-voice" if k == "mcp_default_profile_id" else default) r = db.resolve_voice("no-binding-client", None) - assert r == {"profile_id": "global-voice", "default_engine": None, "source": "global"} + assert r == {"profile_id": None, "default_engine": None, "source": "none"} def test_touch_last_seen_is_best_effort(db): diff --git a/tests/test_text_normalization.py b/tests/test_text_normalization.py index e62773b9b..05d85a5d2 100644 --- a/tests/test_text_normalization.py +++ b/tests/test_text_normalization.py @@ -298,7 +298,7 @@ def test_idempotent_double_encoded_entity(): assert normalize_text(once, None) == once -# ── Toggle: pref (default ON) + env override ───────────────────────────────── +# ── Toggle: default ON + env override ──────────────────────────────────────── def test_enabled_by_default(monkeypatch): monkeypatch.delenv(text_normalization.ENV_VAR, raising=False) @@ -306,15 +306,12 @@ def test_enabled_by_default(monkeypatch): assert normalize_for_tts("I have 2 cats", "English") == "I have two cats" -def test_pref_off_bypasses(monkeypatch): +def test_never_written_pref_does_not_toggle_normalization(monkeypatch): + """Only the env var switches it off; no Settings surface writes a pref.""" monkeypatch.delenv(text_normalization.ENV_VAR, raising=False) import core.prefs as prefs_mod - monkeypatch.setattr( - prefs_mod, "get", - lambda key, default=None: False if key == text_normalization.PREF_KEY else default, - ) - raw = "I have 2 cats!!!!!!" - assert normalize_for_tts(raw, "English") == raw # byte-identical bypass + monkeypatch.setattr(prefs_mod, "get", lambda key, default=None: False) + assert normalization_enabled() is True def test_env_off_bypasses(monkeypatch): @@ -542,3 +539,17 @@ def test_complete_signed_decimal_ranges(raw, expected): ]) def test_malformed_or_protected_ranges_remain_unchanged(raw): assert normalize_text(raw, "ko") == raw + + +@pytest.mark.parametrize(("env", "expected"), [(None, True), ("1", True), ("off", False), ("0", False)]) +def test_pronunciation_switch_is_env_only(monkeypatch, env, expected): + from api.routers.generation import pronunciation_enabled + import core.prefs as prefs_mod + + if env is None: + monkeypatch.delenv("OMNIVOICE_PRONUNCIATION", raising=False) + else: + monkeypatch.setenv("OMNIVOICE_PRONUNCIATION", env) + # A never-written pref must not change the outcome. + monkeypatch.setattr(prefs_mod, "get", lambda key, default=None: False) + assert pronunciation_enabled() is expected From eefc1499170631719d517c081e23656d48b75be8 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:31:18 +0530 Subject: [PATCH 48/85] fix(generate): typecheck the shared generate budget module --- electron/tsconfig.web.json | 1 + 1 file changed, 1 insertion(+) diff --git a/electron/tsconfig.web.json b/electron/tsconfig.web.json index 17ebb0f3b..c72de41d2 100644 --- a/electron/tsconfig.web.json +++ b/electron/tsconfig.web.json @@ -29,6 +29,7 @@ "src/renderer/env.d.ts", "src/shared/utils/languageRejection.ts", "src/shared/utils/generationFailureMessage.ts", + "src/shared/utils/generateBudget.ts", "src/shared/utils/coalescedJsonStorage.ts", "src/shared/utils/indexedDbLongformStore.ts", "src/shared/utils/cookieExport.ts", From 787f7a6367bc5200bace015659ccaf79a13cf3f3 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:31:18 +0530 Subject: [PATCH 49/85] fix(updates): remove the Preview update channel No workflow publishes a preview update feed, so choosing Preview silently served Stable. The desktop updater now always follows tagged releases: the channel picker and its IPC are gone, a saved preview choice is forgotten on start without errors, release history lists tagged releases, and the now-unused hint string is removed from every locale. --- electron/src/main/updater.test.ts | 100 ++++--------- electron/src/main/updater.ts | 139 ++++-------------- electron/src/preload/index.d.ts | 5 +- electron/src/preload/index.ts | 6 +- .../src/features/settings/settings-page.tsx | 2 +- .../settings/update-settings.test.tsx | 26 ++-- .../src/features/settings/update-settings.tsx | 50 +------ electron/src/shared/i18n/locales/ar.json | 1 - electron/src/shared/i18n/locales/de.json | 1 - electron/src/shared/i18n/locales/en.json | 1 - electron/src/shared/i18n/locales/es.json | 1 - electron/src/shared/i18n/locales/fr.json | 1 - electron/src/shared/i18n/locales/hi.json | 1 - electron/src/shared/i18n/locales/id.json | 1 - electron/src/shared/i18n/locales/it.json | 1 - electron/src/shared/i18n/locales/ja.json | 1 - electron/src/shared/i18n/locales/ko.json | 1 - electron/src/shared/i18n/locales/nl.json | 1 - electron/src/shared/i18n/locales/pl.json | 1 - electron/src/shared/i18n/locales/pt.json | 1 - electron/src/shared/i18n/locales/ru.json | 1 - electron/src/shared/i18n/locales/sv.json | 1 - electron/src/shared/i18n/locales/th.json | 1 - electron/src/shared/i18n/locales/tr.json | 1 - electron/src/shared/i18n/locales/uk.json | 1 - electron/src/shared/i18n/locales/vi.json | 1 - electron/src/shared/i18n/locales/zh-CN.json | 1 - electron/src/shared/i18n/locales/zh-TW.json | 1 - 28 files changed, 77 insertions(+), 272 deletions(-) diff --git a/electron/src/main/updater.test.ts b/electron/src/main/updater.test.ts index b6ab96c33..023b36650 100644 --- a/electron/src/main/updater.test.ts +++ b/electron/src/main/updater.test.ts @@ -3,16 +3,12 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; const mocks = vi.hoisted(() => { const handlers = new Map void>>(); const ipcHandlers = new Map unknown>(); + // updates.json once stored the retired channel choice. const disk = { channel: undefined as 'stable' | 'preview' | undefined }; const fs = { - readFileSync: vi.fn(() => { - if (!disk.channel) throw new Error('missing'); - return JSON.stringify({ channel: disk.channel }); + rmSync: vi.fn((_path: string) => { + disk.channel = undefined; }), - writeFileSync: vi.fn((_path: string, value: string) => { - disk.channel = JSON.parse(value).channel; - }), - renameSync: vi.fn(), }; const autoUpdater = { autoDownload: true, @@ -64,12 +60,10 @@ vi.mock('node:fs', () => ({ ...mocks.fs, default: mocks.fs })); vi.mock('./trusted-renderer', () => ({ isTrustedRenderer: mocks.trusted })); import { - compareReleaseVersions, DesktopUpdater, feedManifestName, listDesktopReleases, registerUpdateIpc, - resolvePreviewFeed, UPDATE_CHANNELS, } from './updater'; @@ -163,47 +157,33 @@ describe('DesktopUpdater', () => { }); }); - test('switches and persists the preview channel before checking it', async () => { - const fetcher = vi.fn(async (input: string | URL | Request) => { - const url = String(input); - const version = url.includes('/preview/') ? '0.5.3-144' : '0.5.2'; - return new Response(`version: ${version}\n`); - }) as typeof fetch; - const updater = new DesktopUpdater(fetcher); - await updater.setChannel('preview'); - - expect(mocks.disk.channel).toBe('preview'); - expect(mocks.autoUpdater.allowPrerelease).toBe(true); - expect(mocks.autoUpdater.channel).toBe(`electron-preview-${process.platform}-${process.arch}`); - expect(mocks.autoUpdater.setFeedURL).toHaveBeenLastCalledWith({ - provider: 'generic', - url: 'https://github.com/debpalash/VoiceStudio/releases/download/preview', - channel: `electron-preview-${process.platform}-${process.arch}`, - }); - expect(mocks.autoUpdater.checkForUpdates).toHaveBeenCalledOnce(); - expect(fetcher).toHaveBeenCalledTimes(2); - expect(updater.snapshot().channel).toBe('preview'); - await expect(updater.setChannel('nightly' as any)).rejects.toThrow('Invalid update channel'); - }); - - test('keeps Preview selected while using a newer Stable feed without allowing downgrade', async () => { + test('migrates a saved Preview choice to Stable without errors', async () => { mocks.disk.channel = 'preview'; - const fetcher = vi.fn(async (input: string | URL | Request) => { - const version = String(input).includes('/preview/') ? '0.5.3-144' : '0.5.3'; - return new Response(`version: ${version}\n`); - }) as typeof fetch; - const updater = new DesktopUpdater(fetcher); + const updater = new DesktopUpdater(); + expect(mocks.fs.rmSync).toHaveBeenCalledWith(expect.stringContaining('updates.json'), { + force: true, + }); + expect(mocks.disk.channel).toBeUndefined(); await updater.check(); - expect(updater.snapshot().channel).toBe('preview'); - expect(mocks.autoUpdater.allowPrerelease).toBe(true); + expect(updater.snapshot().status).toBe('idle'); + expect(updater.snapshot()).not.toHaveProperty('channel'); + expect(mocks.autoUpdater.allowPrerelease).toBe(false); expect(mocks.autoUpdater.allowDowngrade).toBe(false); expect(mocks.autoUpdater.setFeedURL).toHaveBeenLastCalledWith({ provider: 'generic', url: 'https://github.com/debpalash/VoiceStudio/releases/latest/download', channel: `electron-stable-${process.platform}-${process.arch}`, }); + expect(updater).not.toHaveProperty('setChannel'); + }); + + test('a leftover settings file that cannot be removed never blocks the updater', () => { + mocks.fs.rmSync.mockImplementationOnce(() => { + throw new Error('EPERM'); + }); + expect(() => new DesktopUpdater()).not.toThrow(); }); test('keeps startup checks quiet while reporting manual check failures', async () => { @@ -240,7 +220,7 @@ describe('DesktopUpdater', () => { test('exposes unsupported state without touching updater APIs in development', async () => { mocks.app.isPackaged = false; const updater = new DesktopUpdater(); - expect(updater.snapshot()).toMatchObject({ status: 'unsupported', channel: 'stable' }); + expect(updater.snapshot()).toMatchObject({ status: 'unsupported' }); await updater.check(); await updater.download(); updater.install(); @@ -286,39 +266,10 @@ describe('DesktopUpdater', () => { describe('update feed selection', () => { test('matches electron-updater platform manifest names', () => { - expect(feedManifestName('preview', 'win32', 'x64')).toBe('electron-preview-win32-x64.yml'); - expect(feedManifestName('preview', 'darwin', 'arm64')).toBe( - 'electron-preview-darwin-arm64-mac.yml', - ); - expect(feedManifestName('preview', 'linux', 'x64')).toBe( - 'electron-preview-linux-x64-linux.yml', - ); + expect(feedManifestName('win32', 'x64')).toBe('electron-stable-win32-x64.yml'); + expect(feedManifestName('darwin', 'arm64')).toBe('electron-stable-darwin-arm64-mac.yml'); + expect(feedManifestName('linux', 'x64')).toBe('electron-stable-linux-x64-linux.yml'); }); - - test('orders numeric previews below the stable release with the same core version', () => { - expect(compareReleaseVersions('0.5.3-145', '0.5.3-144')).toBe(1); - expect(compareReleaseVersions('0.5.3', '0.5.3-999')).toBe(1); - expect(compareReleaseVersions('0.5.2', '0.5.3-1')).toBe(-1); - }); - - test.each([ - ['preview', '0.5.3-144', '0.5.2', 200, 200], - ['stable', '0.5.3-144', '0.5.3', 200, 200], - ['stable', '', '0.5.2', 404, 200], - ['preview', '0.5.3-144', '', 200, 404], - ['preview', '', '', 404, 404], - ] as const)( - 'selects %s for preview=%s stable=%s', - async (expected, previewVersion, stableVersion, previewStatus, stableStatus) => { - const fetcher = vi.fn(async (input: string | URL | Request) => { - const preview = String(input).includes('/preview/'); - return new Response(`version: ${preview ? previewVersion : stableVersion}\n`, { - status: preview ? previewStatus : stableStatus, - }); - }) as typeof fetch; - await expect(resolvePreviewFeed(fetcher, 'win32', 'x64')).resolves.toBe(expected); - }, - ); }); describe('update IPC', () => { @@ -355,7 +306,8 @@ describe('update IPC', () => { }), ).toThrow('Untrusted update request'); + expect(UPDATE_CHANNELS).not.toHaveProperty('setChannel'); dispose(); - expect(mocks.ipcMain.removeHandler).toHaveBeenCalledTimes(7); + expect(mocks.ipcMain.removeHandler).toHaveBeenCalledTimes(6); }); }); diff --git a/electron/src/main/updater.ts b/electron/src/main/updater.ts index e2e5ecfb1..689988f1b 100644 --- a/electron/src/main/updater.ts +++ b/electron/src/main/updater.ts @@ -1,91 +1,34 @@ import { app, BrowserWindow, ipcMain } from 'electron'; import updaterPackage, { type ProgressInfo, type UpdateInfo } from 'electron-updater'; -import { readFileSync, renameSync, writeFileSync } from 'node:fs'; +import { rmSync } from 'node:fs'; import { join } from 'node:path'; import { isTrustedRenderer } from './trusted-renderer'; -import type { UpdateChannel, UpdateReleaseInfo, UpdateState } from '../preload/index.d'; +import type { UpdateReleaseInfo, UpdateState } from '../preload/index.d'; import { sendToLiveWindow } from './window-safety'; // electron-updater is CommonJS. Electron executes our main bundle as ESM, so // named runtime imports fail before the app can create a window. const { autoUpdater } = updaterPackage; -const FEEDS: Record = { - stable: 'https://github.com/debpalash/VoiceStudio/releases/latest/download', - preview: 'https://github.com/debpalash/VoiceStudio/releases/download/preview', -}; +// Only tagged releases are published as an update feed. A Preview channel used +// to exist, but no workflow ever published its feed, so it silently served +// Stable; it was removed rather than left as a choice that does nothing. +const STABLE_FEED = 'https://github.com/debpalash/VoiceStudio/releases/latest/download'; const RELEASES_API = 'https://api.github.com/repos/debpalash/VoiceStudio/releases?per_page=30'; function feedChannelName( - channel: UpdateChannel, platform: NodeJS.Platform = process.platform, arch: string = process.arch, ): string { - return `electron-${channel}-${platform}-${arch}`; + return `electron-stable-${platform}-${arch}`; } export function feedManifestName( - channel: UpdateChannel, platform: NodeJS.Platform = process.platform, arch: string = process.arch, ): string { const suffix = platform === 'darwin' ? '-mac' : platform === 'linux' ? '-linux' : ''; - return `${feedChannelName(channel, platform, arch)}${suffix}.yml`; -} - -function releaseVersion(value: string): [number, number, number, number | null] | null { - const match = /^(\d+)\.(\d+)\.(\d+)(?:-(\d+))?$/.exec(value.trim()); - if (!match) return null; - return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] ? Number(match[4]) : null]; -} - -export function compareReleaseVersions(left: string, right: string): number { - const a = releaseVersion(left); - const b = releaseVersion(right); - if (!a || !b) return 0; - for (let index = 0; index < 3; index += 1) { - if (a[index]! !== b[index]!) return a[index]! > b[index]! ? 1 : -1; - } - if (a[3] === b[3]) return 0; - if (a[3] === null) return 1; - if (b[3] === null) return -1; - return a[3] > b[3] ? 1 : -1; -} - -async function feedVersion( - channel: UpdateChannel, - fetcher: typeof fetch, - platform: NodeJS.Platform, - arch: string, -): Promise { - try { - const response = await fetcher( - `${FEEDS[channel]}/${feedManifestName(channel, platform, arch)}`, - { - headers: { Accept: 'text/yaml', 'User-Agent': 'VoiceStudio' }, - signal: AbortSignal.timeout(5_000), - }, - ); - if (!response.ok) return null; - const version = /^version:\s*['"]?([^'"\s]+)['"]?\s*$/m.exec(await response.text())?.[1]; - return version && releaseVersion(version) ? version : null; - } catch { - return null; - } -} - -export async function resolvePreviewFeed( - fetcher: typeof fetch = fetch, - platform: NodeJS.Platform = process.platform, - arch: string = process.arch, -): Promise { - const [preview, stable] = await Promise.all([ - feedVersion('preview', fetcher, platform, arch), - feedVersion('stable', fetcher, platform, arch), - ]); - if (!preview && stable) return 'stable'; - if (preview && stable && compareReleaseVersions(stable, preview) > 0) return 'stable'; - return 'preview'; + return `${feedChannelName(platform, arch)}${suffix}.yml`; } export const UPDATE_CHANNELS = { @@ -94,7 +37,6 @@ export const UPDATE_CHANNELS = { download: 'updates:download', dismiss: 'updates:dismiss', install: 'updates:install', - setChannel: 'updates:setChannel', listReleases: 'updates:listReleases', state: 'updates:state', } as const; @@ -147,39 +89,32 @@ function releaseSize(info: UpdateInfo): number | undefined { return total > 0 ? total : undefined; } -function readChannel(): UpdateChannel { +/** + * updates.json only ever stored the retired channel choice. Forget it so a + * saved "preview" cannot linger; the updater always follows Stable now. + */ +export function forgetSavedUpdateChannel(userData: string = app.getPath('userData')): void { try { - const value = JSON.parse(readFileSync(join(app.getPath('userData'), 'updates.json'), 'utf8')); - return value?.channel === 'preview' ? 'preview' : 'stable'; + rmSync(join(userData, 'updates.json'), { force: true }); } catch { - return 'stable'; + // Best effort: an unreadable leftover is ignored either way. } } -function writeChannel(channel: UpdateChannel): void { - const path = join(app.getPath('userData'), 'updates.json'); - const temporary = `${path}.tmp`; - writeFileSync(temporary, JSON.stringify({ channel }, null, 2), { - encoding: 'utf8', - mode: 0o600, - }); - renameSync(temporary, path); -} - export class DesktopUpdater { private listeners = new Set<(state: UpdateState) => void>(); private state: UpdateState; private checkInFlight: Promise | null = null; private downloadInFlight: Promise | null = null; - constructor(private readonly fetcher: typeof fetch = fetch) { + constructor() { const supported = app.isPackaged; this.state = { status: supported ? 'idle' : 'unsupported', currentVersion: app.getVersion(), - channel: readChannel(), progress: 0, }; + forgetSavedUpdateChannel(); if (!supported) return; autoUpdater.autoDownload = false; @@ -274,9 +209,7 @@ export class DesktopUpdater { if (this.checkInFlight) return this.checkInFlight; const operation = (async () => { try { - const feed = - this.state.channel === 'preview' ? await resolvePreviewFeed(this.fetcher) : 'stable'; - this.configureFeed(feed); + this.configureFeed(); await autoUpdater.checkForUpdates(); } catch (error) { if (quiet) this.patch({ status: 'idle', error: undefined }); @@ -331,7 +264,6 @@ export class DesktopUpdater { this.state = { status: 'idle', currentVersion: this.state.currentVersion, - channel: this.state.channel, progress: 0, lastCheckedAt: this.state.lastCheckedAt, }; @@ -344,32 +276,18 @@ export class DesktopUpdater { autoUpdater.quitAndInstall(false, true); } - async setChannel(channel: UpdateChannel): Promise { - if (channel !== 'stable' && channel !== 'preview') throw new Error('Invalid update channel'); - writeChannel(channel); - this.state = { - status: app.isPackaged ? 'idle' : 'unsupported', - currentVersion: app.getVersion(), - channel, - progress: 0, - }; - this.configureFeed(); - this.emit(); - if (app.isPackaged) await this.check(); - return this.snapshot(); - } - - private configureFeed(feed: UpdateChannel = this.state.channel): void { + private configureFeed(): void { if (!app.isPackaged) return; - autoUpdater.allowPrerelease = this.state.channel === 'preview'; - const feedChannel = feedChannelName(feed); + autoUpdater.allowPrerelease = false; + const feedChannel = feedChannelName(); autoUpdater.channel = feedChannel; - // electron-updater's channel setter silently enables downgrades. Keep the - // app monotonic even when Preview temporarily resolves to the Stable feed. + // electron-updater's channel setter silently enables downgrades; keep the + // app monotonic. autoUpdater.allowDowngrade = false; + // Explicit, so a build packaged with another feed URL still follows Stable. autoUpdater.setFeedURL({ provider: 'generic', - url: FEEDS[feed], + url: STABLE_FEED, channel: feedChannel, }); } @@ -422,13 +340,8 @@ export function registerUpdateIpc( await beforeInstall(); updater.install(); }); - ipcMain.handle(UPDATE_CHANNELS.setChannel, (event, channel: UpdateChannel) => { - trusted(event); - return updater.setChannel(channel); - }); - ipcMain.handle(UPDATE_CHANNELS.listReleases, (event, channel: UpdateChannel) => { + ipcMain.handle(UPDATE_CHANNELS.listReleases, (event) => { trusted(event); - if (channel !== 'stable' && channel !== 'preview') throw new Error('Invalid update channel'); return listDesktopReleases(); }); const unsubscribe = updater.subscribe((state) => { diff --git a/electron/src/preload/index.d.ts b/electron/src/preload/index.d.ts index 0bc7f222c..922479b07 100644 --- a/electron/src/preload/index.d.ts +++ b/electron/src/preload/index.d.ts @@ -87,7 +87,6 @@ export interface BackendConnection { authenticated: boolean; } -export type UpdateChannel = 'stable' | 'preview'; export type UpdateStatus = | 'unsupported' | 'idle' @@ -100,7 +99,6 @@ export interface UpdateState { status: UpdateStatus; currentVersion: string; availableVersion?: string; - channel: UpdateChannel; notes?: string | null; progress: number; transferredBytes?: number; @@ -317,8 +315,7 @@ export interface VoiceStudioBridge { download(): Promise; dismiss(): Promise; install(): Promise; - setChannel(channel: UpdateChannel): Promise; - listReleases(channel: UpdateChannel): Promise; + listReleases(): Promise; onState(cb: (state: UpdateState) => void): () => void; }; maintenance: { diff --git a/electron/src/preload/index.ts b/electron/src/preload/index.ts index f450af917..d6e76a1c3 100644 --- a/electron/src/preload/index.ts +++ b/electron/src/preload/index.ts @@ -56,10 +56,8 @@ const bridge: VoiceStudioBridge = { download: () => ipcRenderer.invoke('updates:download') as Promise, dismiss: () => ipcRenderer.invoke('updates:dismiss') as Promise, install: () => ipcRenderer.invoke('updates:install') as Promise, - setChannel: (channel) => - ipcRenderer.invoke('updates:setChannel', channel) as Promise, - listReleases: (channel) => - ipcRenderer.invoke('updates:listReleases', channel) as Promise, + listReleases: () => + ipcRenderer.invoke('updates:listReleases') as Promise, onState: (cb) => subscribe('updates:state', cb), }, watch: { diff --git a/electron/src/renderer/src/features/settings/settings-page.tsx b/electron/src/renderer/src/features/settings/settings-page.tsx index 82bc2c529..84d9c43e9 100644 --- a/electron/src/renderer/src/features/settings/settings-page.tsx +++ b/electron/src/renderer/src/features/settings/settings-page.tsx @@ -112,7 +112,7 @@ const extraSettings = [ label: 'updates.tab', Icon: RefreshCwIcon, Component: UpdateSettings, - fields: ['updates.tab', 'updates.check_now', 'about.update_channel'], + fields: ['updates.tab', 'updates.check_now'], }, { to: '/settings/support', diff --git a/electron/src/renderer/src/features/settings/update-settings.test.tsx b/electron/src/renderer/src/features/settings/update-settings.test.tsx index cfba4a14a..989d4fa34 100644 --- a/electron/src/renderer/src/features/settings/update-settings.test.tsx +++ b/electron/src/renderer/src/features/settings/update-settings.test.tsx @@ -7,7 +7,6 @@ const mocks = vi.hoisted(() => ({ state: { status: 'idle', currentVersion: '0.5.2', - channel: 'stable', progress: 0, } as UpdateState, listener: undefined as ((state: UpdateState) => void) | undefined, @@ -17,7 +16,6 @@ const mocks = vi.hoisted(() => ({ download: vi.fn(), dismiss: vi.fn(), install: vi.fn(), - setChannel: vi.fn(), listReleases: vi.fn(), flushDubDraft: vi.fn(), api: vi.fn(), @@ -34,7 +32,6 @@ vi.mock('@/components/bridge', () => ({ download: mocks.download, dismiss: mocks.dismiss, install: mocks.install, - setChannel: mocks.setChannel, listReleases: mocks.listReleases, onState: (listener: (state: UpdateState) => void) => { mocks.listener = listener; @@ -77,7 +74,6 @@ describe('UpdateSettings', () => { mocks.state = { status: 'idle', currentVersion: '0.5.2', - channel: 'stable', progress: 0, }; mocks.listener = undefined; @@ -92,10 +88,6 @@ describe('UpdateSettings', () => { mocks.download.mockResolvedValue(mocks.state); mocks.dismiss.mockResolvedValue(mocks.state); mocks.install.mockResolvedValue(undefined); - mocks.setChannel.mockImplementation(async (channel: 'stable' | 'preview') => ({ - ...mocks.state, - channel, - })); mocks.listReleases.mockResolvedValue([]); mocks.api.mockImplementation(async (path: string) => { if (path.includes('/models/install/status')) return { jobs: [] }; @@ -108,13 +100,25 @@ describe('UpdateSettings', () => { afterEach(cleanup); - test('checks for updates and switches between Stable and Preview', async () => { + test('checks for updates and offers no update-channel choice', async () => { renderSettings(); fireEvent.click(await screen.findByRole('button', { name: 'updates.check_now' })); await waitFor(() => expect(mocks.check).toHaveBeenCalledOnce()); - fireEvent.click(screen.getByRole('button', { name: 'about.channel_preview' })); - await waitFor(() => expect(mocks.setChannel).toHaveBeenCalledWith('preview')); + // No Preview feed is published, so a channel picker would only mislead. + expect(screen.queryByText('about.update_channel')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'about.channel_preview' })).not.toBeInTheDocument(); + }); + + test('lists tagged releases only', async () => { + mocks.listReleases.mockResolvedValue([ + { version: '0.5.2', name: 'v0.5.2', date: '2026-09-12T00:00:00Z', prerelease: false, notes: '' }, + { version: '0.5.3-7', name: 'preview', date: '2026-09-13T00:00:00Z', prerelease: true, notes: '' }, + ]); + renderSettings(); + expect(await screen.findByText('v0.5.2')).toBeInTheDocument(); + expect(screen.queryByText('v0.5.3-7')).not.toBeInTheDocument(); + expect(mocks.listReleases).toHaveBeenCalledWith(); }); test('keeps progress live and downloads an available update', async () => { diff --git a/electron/src/renderer/src/features/settings/update-settings.tsx b/electron/src/renderer/src/features/settings/update-settings.tsx index 20cb4f19a..65b191f28 100644 --- a/electron/src/renderer/src/features/settings/update-settings.tsx +++ b/electron/src/renderer/src/features/settings/update-settings.tsx @@ -20,13 +20,12 @@ import { appVersion, getBridge } from '@/components/bridge'; import { dubSession, flushDubDraft, useDubSession } from '@/features/dub/dub-session'; import { hasActiveAppWork, useAppActivityCount } from '@/lib/app-activity'; import { apiJson, describeError } from '@/lib/api/client'; -import { SettingsRow, SettingsSection } from './settings-layout'; -import type { UpdateChannel, UpdateReleaseInfo, UpdateState } from '../../../../preload/index.d'; +import { SettingsSection } from './settings-layout'; +import type { UpdateReleaseInfo, UpdateState } from '../../../../preload/index.d'; const initial: UpdateState = { status: 'unsupported', currentVersion: appVersion(), - channel: 'stable', progress: 0, }; @@ -73,10 +72,6 @@ function formatEta(seconds: number): string { export function UpdateSettings() { const { t } = useTranslation(); - const channelLabels = { - stable: t('about.channel_stable'), - preview: t('about.channel_preview'), - } as const; const updates = getBridge()?.updates; const canListReleases = typeof updates?.listReleases === 'function'; const dub = useDubSession(); @@ -111,13 +106,14 @@ export function UpdateSettings() { staleTime: 300_000, }); const releaseHistory = useQuery({ - queryKey: ['desktop-release-history', state.channel], - queryFn: () => updates!.listReleases(state.channel), + queryKey: ['desktop-release-history'], + queryFn: () => updates!.listReleases(), enabled: canListReleases, staleTime: 15 * 60_000, }); const releaseRows = (releaseHistory.data || []) - .filter((release) => state.channel === 'preview' || !release.prerelease) + // Updates follow tagged releases only, so the history lists those. + .filter((release) => !release.prerelease) .slice() .sort((left, right) => right.date.localeCompare(left.date)) .map((release) => ({ @@ -156,9 +152,6 @@ export function UpdateSettings() { setAction(false); } }; - const setChannel = (channel: UpdateChannel) => { - if (updates && channel !== state.channel) void run(() => updates.setChannel(channel)); - }; const restart = async () => { setAction(true); try { @@ -323,32 +316,6 @@ export function UpdateSettings() { - - - {(['stable', 'preview'] as const).map((channel) => ( - - ))} - - - {state.notes && ( )} - {release.prerelease && ( - - {t('updates.prerelease')} - - )} diff --git a/electron/src/shared/i18n/locales/ar.json b/electron/src/shared/i18n/locales/ar.json index 3d73ff631..d3a5a4268 100644 --- a/electron/src/shared/i18n/locales/ar.json +++ b/electron/src/shared/i18n/locales/ar.json @@ -831,7 +831,6 @@ "channel_stable": "مستقر", "channel_preview": "معاينة", "channel_set": "تم ضبط قناة التحديث على {{channel}}", - "channel_preview_hint": "تتابع المعاينة أحدث إصدار من main — ميزات أحدث وأقل اختبارًا. تعود إلى المستقر إذا كان هناك إصدار مستقر أحدث.", "self_check": "تشغيل الفحص الذاتي", "self_check_failed": "فشل الفحص الذاتي: {{message}}", "self_check_ok": "سليم", diff --git a/electron/src/shared/i18n/locales/de.json b/electron/src/shared/i18n/locales/de.json index c3cd4b8ba..20cb66b14 100644 --- a/electron/src/shared/i18n/locales/de.json +++ b/electron/src/shared/i18n/locales/de.json @@ -831,7 +831,6 @@ "channel_stable": "Stabil", "channel_preview": "Vorschau", "channel_set": "Update-Kanal auf {{channel}} gesetzt", - "channel_preview_hint": "Die Vorschau folgt dem neuesten main-Build – neuere Funktionen, weniger getestet. Wechselt zurück zu Stabil, wenn eine stabile Version voraus ist.", "self_check": "Selbsttest ausführen", "self_check_failed": "Selbsttest fehlgeschlagen: {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/en.json b/electron/src/shared/i18n/locales/en.json index 56a020d95..01e2cd976 100644 --- a/electron/src/shared/i18n/locales/en.json +++ b/electron/src/shared/i18n/locales/en.json @@ -1127,7 +1127,6 @@ "channel_stable": "Stable", "channel_preview": "Preview", "channel_set": "Update channel set to {{channel}}", - "channel_preview_hint": "Preview tracks the latest main build — newer features, less tested. Falls back to stable if a stable release is ahead.", "yes": "yes", "no": "no", "web_preview": "web preview", diff --git a/electron/src/shared/i18n/locales/es.json b/electron/src/shared/i18n/locales/es.json index 3b72b91ac..42e50ccf9 100644 --- a/electron/src/shared/i18n/locales/es.json +++ b/electron/src/shared/i18n/locales/es.json @@ -831,7 +831,6 @@ "channel_stable": "Estable", "channel_preview": "Vista previa", "channel_set": "Canal de actualización configurado en {{channel}}", - "channel_preview_hint": "La vista previa sigue la última compilación de main: funciones más nuevas, menos probadas. Vuelve a estable si hay una versión estable más reciente.", "self_check": "Ejecutar autocomprobación", "self_check_failed": "La autocomprobación falló: {{message}}", "self_check_ok": "Correcto", diff --git a/electron/src/shared/i18n/locales/fr.json b/electron/src/shared/i18n/locales/fr.json index b4e410dfd..0e37fa8f0 100644 --- a/electron/src/shared/i18n/locales/fr.json +++ b/electron/src/shared/i18n/locales/fr.json @@ -831,7 +831,6 @@ "channel_stable": "Stable", "channel_preview": "Aperçu", "channel_set": "Canal de mise à jour réglé sur {{channel}}", - "channel_preview_hint": "L'aperçu suit la dernière version de main : fonctionnalités plus récentes, moins testées. Revient à la version stable si une version stable est plus avancée.", "self_check": "Lancer l'auto-diagnostic", "self_check_failed": "Échec de l'auto-diagnostic : {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/hi.json b/electron/src/shared/i18n/locales/hi.json index 0b3972a05..0fb6aa261 100644 --- a/electron/src/shared/i18n/locales/hi.json +++ b/electron/src/shared/i18n/locales/hi.json @@ -831,7 +831,6 @@ "channel_stable": "स्थिर", "channel_preview": "प्रीव्यू", "channel_set": "अपडेट चैनल {{channel}} पर सेट किया गया", - "channel_preview_hint": "प्रीव्यू नवीनतम main बिल्ड को ट्रैक करता है — नई सुविधाएँ, कम परीक्षण। यदि कोई स्थिर रिलीज़ आगे है तो स्थिर पर वापस आ जाता है।", "self_check": "सेल्फ़-चेक चलाएँ", "self_check_failed": "सेल्फ़-चेक विफल: {{message}}", "self_check_ok": "ठीक", diff --git a/electron/src/shared/i18n/locales/id.json b/electron/src/shared/i18n/locales/id.json index fdd1e5348..466b7fff2 100644 --- a/electron/src/shared/i18n/locales/id.json +++ b/electron/src/shared/i18n/locales/id.json @@ -831,7 +831,6 @@ "channel_stable": "Stabil", "channel_preview": "Pratinjau", "channel_set": "Saluran pembaruan disetel ke {{channel}}", - "channel_preview_hint": "Pratinjau mengikuti build main terbaru — fitur lebih baru, lebih sedikit diuji. Kembali ke stabil jika ada rilis stabil yang lebih baru.", "self_check": "Jalankan pemeriksaan mandiri", "self_check_failed": "Pemeriksaan mandiri gagal: {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/it.json b/electron/src/shared/i18n/locales/it.json index ed62ff418..8b001a309 100644 --- a/electron/src/shared/i18n/locales/it.json +++ b/electron/src/shared/i18n/locales/it.json @@ -831,7 +831,6 @@ "channel_stable": "Stabile", "channel_preview": "Anteprima", "channel_set": "Canale di aggiornamento impostato su {{channel}}", - "channel_preview_hint": "L'anteprima segue l'ultima build di main: funzionalità più recenti, meno testate. Torna alla versione stabile se una release stabile è più avanti.", "self_check": "Esegui autodiagnosi", "self_check_failed": "Autodiagnosi non riuscita: {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/ja.json b/electron/src/shared/i18n/locales/ja.json index 879681829..8eb1f5c12 100644 --- a/electron/src/shared/i18n/locales/ja.json +++ b/electron/src/shared/i18n/locales/ja.json @@ -831,7 +831,6 @@ "channel_stable": "安定版", "channel_preview": "プレビュー", "channel_set": "アップデートチャンネルを {{channel}} に設定しました", - "channel_preview_hint": "プレビューは最新の main ビルドを追跡します。新機能をいち早く利用できますが、テストは少なめです。安定版が先行している場合は安定版にフォールバックします。", "self_check": "セルフチェックを実行", "self_check_failed": "セルフチェックに失敗しました: {{message}}", "self_check_ok": "正常", diff --git a/electron/src/shared/i18n/locales/ko.json b/electron/src/shared/i18n/locales/ko.json index feaf62c35..5ea4988b9 100644 --- a/electron/src/shared/i18n/locales/ko.json +++ b/electron/src/shared/i18n/locales/ko.json @@ -1060,7 +1060,6 @@ "channel_stable": "안정 버전", "channel_preview": "미리 보기", "channel_set": "업데이트 채널이 {{channel}}(으)로 설정됨", - "channel_preview_hint": "미리 보기는 최신 main 빌드를 따릅니다. 새로운 기능을 빨리 사용할 수 있지만 테스트는 적습니다. 안정 버전이 더 최신이면 안정 버전으로 대체됩니다.", "self_check": "자체 점검 실행", "self_check_failed": "자체 점검 실패: {{message}}", "self_check_ok": "정상", diff --git a/electron/src/shared/i18n/locales/nl.json b/electron/src/shared/i18n/locales/nl.json index 4942b1131..8d07fd2f8 100644 --- a/electron/src/shared/i18n/locales/nl.json +++ b/electron/src/shared/i18n/locales/nl.json @@ -831,7 +831,6 @@ "channel_stable": "Stabiel", "channel_preview": "Voorbeeld", "channel_set": "Updatekanaal ingesteld op {{channel}}", - "channel_preview_hint": "Voorbeeld volgt de nieuwste main-build — nieuwere functies, minder getest. Valt terug op stabiel als er een nieuwere stabiele release is.", "self_check": "Zelfcontrole uitvoeren", "self_check_failed": "Zelfcontrole mislukt: {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/pl.json b/electron/src/shared/i18n/locales/pl.json index ccd27ef0a..fb3d505c5 100644 --- a/electron/src/shared/i18n/locales/pl.json +++ b/electron/src/shared/i18n/locales/pl.json @@ -831,7 +831,6 @@ "channel_stable": "Stabilny", "channel_preview": "Podgląd", "channel_set": "Kanał aktualizacji ustawiony na {{channel}}", - "channel_preview_hint": "Podgląd śledzi najnowszą kompilację main — nowsze funkcje, mniej testów. Wraca do stabilnej, jeśli stabilne wydanie jest nowsze.", "self_check": "Uruchom autodiagnostykę", "self_check_failed": "Autodiagnostyka nie powiodła się: {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/pt.json b/electron/src/shared/i18n/locales/pt.json index 9749ea2d4..e70e93648 100644 --- a/electron/src/shared/i18n/locales/pt.json +++ b/electron/src/shared/i18n/locales/pt.json @@ -831,7 +831,6 @@ "channel_stable": "Estável", "channel_preview": "Prévia", "channel_set": "Canal de atualização definido como {{channel}}", - "channel_preview_hint": "A prévia acompanha a compilação mais recente da main — recursos mais novos, menos testados. Volta para a estável se uma versão estável estiver à frente.", "self_check": "Executar autoverificação", "self_check_failed": "A autoverificação falhou: {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/ru.json b/electron/src/shared/i18n/locales/ru.json index 5f3c9e729..80b8170a1 100644 --- a/electron/src/shared/i18n/locales/ru.json +++ b/electron/src/shared/i18n/locales/ru.json @@ -831,7 +831,6 @@ "channel_stable": "Стабильный", "channel_preview": "Предварительный", "channel_set": "Канал обновлений установлен на {{channel}}", - "channel_preview_hint": "Предварительный канал отслеживает последнюю сборку main — новые функции, меньше тестирования. Возвращается к стабильному, если стабильный выпуск новее.", "self_check": "Запустить самопроверку", "self_check_failed": "Самопроверка не удалась: {{message}}", "self_check_ok": "ОК", diff --git a/electron/src/shared/i18n/locales/sv.json b/electron/src/shared/i18n/locales/sv.json index ccec1f04e..5b364f056 100644 --- a/electron/src/shared/i18n/locales/sv.json +++ b/electron/src/shared/i18n/locales/sv.json @@ -831,7 +831,6 @@ "channel_stable": "Stabil", "channel_preview": "Förhandsvisning", "channel_set": "Uppdateringskanal inställd på {{channel}}", - "channel_preview_hint": "Förhandsvisning följer det senaste main-bygget – nyare funktioner, mindre testade. Återgår till stabil om en stabil version ligger före.", "self_check": "Kör självtest", "self_check_failed": "Självtestet misslyckades: {{message}}", "self_check_ok": "OK", diff --git a/electron/src/shared/i18n/locales/th.json b/electron/src/shared/i18n/locales/th.json index 4f9c5cbb3..dfbb9115a 100644 --- a/electron/src/shared/i18n/locales/th.json +++ b/electron/src/shared/i18n/locales/th.json @@ -831,7 +831,6 @@ "channel_stable": "เสถียร", "channel_preview": "พรีวิว", "channel_set": "ตั้งค่าช่องอัปเดตเป็น {{channel}} แล้ว", - "channel_preview_hint": "พรีวิวจะติดตามบิลด์ main ล่าสุด — ฟีเจอร์ใหม่กว่า แต่ทดสอบน้อยกว่า จะกลับไปใช้เวอร์ชันเสถียรหากมีรุ่นเสถียรที่ใหม่กว่า", "self_check": "เรียกใช้การตรวจสอบตนเอง", "self_check_failed": "การตรวจสอบตนเองล้มเหลว: {{message}}", "self_check_ok": "ปกติ", diff --git a/electron/src/shared/i18n/locales/tr.json b/electron/src/shared/i18n/locales/tr.json index 8a5f804d6..8780ae1c0 100644 --- a/electron/src/shared/i18n/locales/tr.json +++ b/electron/src/shared/i18n/locales/tr.json @@ -831,7 +831,6 @@ "channel_stable": "Kararlı", "channel_preview": "Önizleme", "channel_set": "Güncelleme kanalı {{channel}} olarak ayarlandı", - "channel_preview_hint": "Önizleme en son main yapısını izler — daha yeni özellikler, daha az test. Daha yeni bir kararlı sürüm varsa kararlıya geri döner.", "self_check": "Öz denetimi çalıştır", "self_check_failed": "Öz denetim başarısız: {{message}}", "self_check_ok": "Tamam", diff --git a/electron/src/shared/i18n/locales/uk.json b/electron/src/shared/i18n/locales/uk.json index ea581578f..8f1c6d12f 100644 --- a/electron/src/shared/i18n/locales/uk.json +++ b/electron/src/shared/i18n/locales/uk.json @@ -831,7 +831,6 @@ "channel_stable": "Стабільний", "channel_preview": "Попередній перегляд", "channel_set": "Канал оновлень встановлено на {{channel}}", - "channel_preview_hint": "Попередній перегляд відстежує найновішу збірку main — новіші функції, менше тестування. Повертається до стабільного, якщо стабільний випуск новіший.", "self_check": "Запустити самоперевірку", "self_check_failed": "Самоперевірка не вдалася: {{message}}", "self_check_ok": "Гаразд", diff --git a/electron/src/shared/i18n/locales/vi.json b/electron/src/shared/i18n/locales/vi.json index d8c2c3240..3740cc4d1 100644 --- a/electron/src/shared/i18n/locales/vi.json +++ b/electron/src/shared/i18n/locales/vi.json @@ -831,7 +831,6 @@ "channel_stable": "Ổn định", "channel_preview": "Xem trước", "channel_set": "Đã đặt kênh cập nhật thành {{channel}}", - "channel_preview_hint": "Bản xem trước theo dõi bản dựng main mới nhất — tính năng mới hơn, ít được kiểm thử hơn. Quay lại bản ổn định nếu có bản phát hành ổn định mới hơn.", "self_check": "Chạy tự kiểm tra", "self_check_failed": "Tự kiểm tra thất bại: {{message}}", "self_check_ok": "Ổn", diff --git a/electron/src/shared/i18n/locales/zh-CN.json b/electron/src/shared/i18n/locales/zh-CN.json index 38b145142..68a5d3bc3 100644 --- a/electron/src/shared/i18n/locales/zh-CN.json +++ b/electron/src/shared/i18n/locales/zh-CN.json @@ -1012,7 +1012,6 @@ "channel_stable": "稳定版", "channel_preview": "预览版", "channel_set": "更新通道已设置为 {{channel}}", - "channel_preview_hint": "预览版跟踪最新的 main 构建——功能更新,但测试较少。若稳定版更新,则回退到稳定版。", "self_check": "运行自检", "open_fix_category": "打开 {{category}}", "self_check_failed": "自检失败:{{message}}", diff --git a/electron/src/shared/i18n/locales/zh-TW.json b/electron/src/shared/i18n/locales/zh-TW.json index c01653424..1c4229db5 100644 --- a/electron/src/shared/i18n/locales/zh-TW.json +++ b/electron/src/shared/i18n/locales/zh-TW.json @@ -831,7 +831,6 @@ "channel_stable": "穩定版", "channel_preview": "預覽版", "channel_set": "更新通道已設定為 {{channel}}", - "channel_preview_hint": "預覽版追蹤最新的 main 組建——功能較新,但測試較少。若穩定版較新,則回退至穩定版。", "self_check": "執行自我檢查", "self_check_failed": "自我檢查失敗:{{message}}", "self_check_ok": "正常", From 49791be7d0a32c517a037812bb1a8be0b9cb2236 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:31:52 +0530 Subject: [PATCH 50/85] docs: point i18n rules at the catalog the app loads and drop backend.spec from the structure map --- AGENTS.md | 2 +- CLAUDE.md | 2 +- docs/STRUCTURE.md | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8502f0298..bec4b58ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,7 @@ Binding for every AI agent (Claude, Codex, Cursor, review bots, …). CLAUDE.md - Root-cause the class, not the instance; fail-before/pass-after regression test; smallest correct change. - Default behavior identical on macOS/Windows/Linux; platform-only features go behind explicit opt-in. Divergent default = P0. - Local-first: no new required network calls; any HF download gated on installed-ness or explicit user action; all synthetic audio through the `mark_synthetic` chokepoint. Sanctioned calls are listed in `.github/CONTRIBUTING.md` → Quality gates: user-started first-run setup (ffmpeg from GitHub `zackees/ffmpeg_bins`, the huggingface.co/hf-mirror.com reachability probe, HF model downloads), opening/enabling the galleries (jsDelivr manifest, GitHub Releases), packaged update checks, consent-gated analytics, prefilled-URL bug reports, and the Pro licence check after a key is entered. Owner-approved exception (2026-09-28): the public GitHub star count may refresh automatically every 20 minutes, without credentials/referrer or user content; offline use remains unaffected. -- Every user-facing string via i18n, present in ALL 21 locale files of its catalog with real translations: `electron/src/shared/i18n/locales/*.json` (`tests/test_locale_parity.py`) or `electron/src/renderer/src/i18n/locales/*.json` (Electron `locale:check`). +- Every user-facing string via i18n, present in ALL 21 `electron/src/renderer/src/i18n/locales/*.json` files (the catalog the app loads) with real translations; `tests/test_locale_parity.py` and Electron `locale:check` enforce it. The legacy `electron/src/shared/i18n/` catalog is loaded only by shared-module tests. - Docs-sync in the same PR. CHANGELOG Unreleased: quiet one-liners ending `(#N)` + `— thanks @user!` for community work, under a short `**Highlights**` list. - Tagged release announcements lead with the biggest user-visible change; redesigns need real UI screenshots and migrations need installer links and steps. Verify all contributor credits from the tag comparison and included PRs; list authors and bug reporters separately (see `docs/RELEASING.md`). - Versioning: root `package.json` is the single source of truth; never bump without the owner asking. diff --git a/CLAUDE.md b/CLAUDE.md index 87482aaec..41769cbd1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ For anything new: prefer what's already pinned in `pyproject.toml` / `electron/p **Release presentation and credits (owner-set 2026-09-17):** Tagged release announcements lead with the biggest user-visible change; redesigns need real UI screenshots and migrations need installer links and steps. Verify all contributor credits from the tag comparison and included PRs; list authors and bug reporters separately (see `docs/RELEASING.md`). Keep Highlights to 3–5 bullets; the release introduction can include prose, images, and a download table before the concise change entries. -**Localization (hard rule):** No hardcoded non-English (CJK) **user-facing text** anywhere in the codebase except the translation layers (`electron/src/shared/i18n/`, parity-checked by `tests/test_locale_parity.py`, and `electron/src/renderer/src/i18n/`, checked by Electron `locale:check`). All UI strings go through i18n (`t('...')` keys in `locales/*.json`); native language names live in `i18n/index.ts` (`LANGUAGES`). Functional CJK is allowed and tracked via the allowlist in `tests/test_no_hardcoded_cjk.py` — text-processing regexes, model/engine vocabulary & identifiers (e.g. CosyVoice speaker IDs), localized error matching, demo/eval data, and test fixtures. CI fails on any hardcoded CJK outside the allowlist; to add legitimate functional CJK, extend `_ALLOWED_FILES` there with a justification. +**Localization (hard rule):** No hardcoded non-English (CJK) **user-facing text** anywhere in the codebase except the translation layers: `electron/src/renderer/src/i18n/` (the catalog the app loads; checked by `tests/test_locale_parity.py` and Electron `locale:check`) and the legacy `electron/src/shared/i18n/` (loaded only by shared-module tests). All UI strings go through i18n (`t('...')` keys in `locales/*.json`); native language names live in `i18n/index.ts` (`LANGUAGES`). Functional CJK is allowed and tracked via the allowlist in `tests/test_no_hardcoded_cjk.py` — text-processing regexes, model/engine vocabulary & identifiers (e.g. CosyVoice speaker IDs), localized error matching, demo/eval data, and test fixtures. CI fails on any hardcoded CJK outside the allowlist; to add legitimate functional CJK, extend `_ALLOWED_FILES` there with a justification. **Release deployment channels (hard rule, owner-set 2026-07-16):** a version bump is not "released" until **every** maintained channel ships it — the full checklist lives in `docs/RELEASING.md`. The channels are the Electron GitHub Release (Linux x64, Windows x64 + experimental ARM64, macOS arm64 + x64), GHCR and Docker Hub in CUDA and ROCm flavors, and the Docker Hub overview page synced from `deploy/dockerhub-overview.md`. The final Tauri feeds are immutable compatibility assets copied into releases for old clients; they are never rebuilt. Verify all channels after tagging. Preview/RC builds always source from `main`; never cut a side-branch build. diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index acb6d75da..8eff45f47 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -26,7 +26,6 @@ VoiceStudio/ ├── .gitmodules ⟵ omnivoice-gallery submodule ├── .python-version ├── .dockerignore ⟵ Docker build context filter -├── backend.spec ⟵ pyinstaller spec (stays at root by pyinstaller convention) ├── alembic.ini ⟵ DB migration config (stays at root by alembic convention) │ ├── .gitignore ⟵ a repo-local .env stays ignored, but user config is NOT @@ -227,7 +226,6 @@ VoiceStudio/ - `pyproject.toml` `[tool.hatch.build.targets.{sdist,wheel}]` paths - `package.json` workspaces and scripts - `turbo.json`, `Dockerfile`, `docker-compose.yml` paths -- `backend.spec` (`['backend/main.py']`, `pathex=['.']`) - Electron Builder, native-helper, updater and packaged-smoke paths - every import that reads `from backend.main import …` (tests, scripts) - the root `package.json` as the version source of truth and its mirrors From 6fa3f13b445d9c5007ccc2acc6230518995d5679 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:32:00 +0530 Subject: [PATCH 51/85] docs(backend): point stale Tauri references at the Electron equivalents The gallery signing-key comments cited the removed tauri.conf.json, and the path-authorization docstrings named the Tauri process that no longer writes the capability files. --- backend/core/path_authorization.py | 17 +++++++++-------- backend/services/gallery.py | 15 +++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/core/path_authorization.py b/backend/core/path_authorization.py index c7d867547..d4dc4a93a 100644 --- a/backend/core/path_authorization.py +++ b/backend/core/path_authorization.py @@ -1,8 +1,9 @@ -"""Consume one-shot host paths authorized by the native Tauri process. +"""Consume one-shot host paths authorized by the native desktop process. -The web API never accepts a filesystem destination or executable path. Tauri -validates the user's native IPC request, writes a private capability file, and -only the unguessable capability token crosses loopback HTTP. +The web API never accepts a filesystem destination or executable path. The +Electron main process (electron/src/main/media-authorization.ts) validates the +user's native IPC request, writes a private capability file, and only the +unguessable capability token crosses loopback HTTP. """ from __future__ import annotations @@ -34,11 +35,11 @@ class PathAuthorizationError(ValueError): def consume(token: str, expected_kind: str) -> str: - """Consume and return a single Tauri-authorized path. + """Consume and return a single desktop-authorized path. - Capability files are one-shot and opened without following symlinks. Tauri - writes them into the app's private data directory; source/Docker callers - cannot mint a valid token through HTTP. + Capability files are one-shot and opened without following symlinks. The + desktop app writes them into the app's private data directory; + source/Docker callers cannot mint a valid token through HTTP. """ if expected_kind not in _KINDS or not _TOKEN_RE.fullmatch(token or ""): raise PathAuthorizationError("Invalid or expired desktop authorization") diff --git a/backend/services/gallery.py b/backend/services/gallery.py index 4ace6ffb3..8ee4de2b8 100644 --- a/backend/services/gallery.py +++ b/backend/services/gallery.py @@ -9,10 +9,10 @@ Trust ===== -``manifest.json`` is signed with the **existing Tauri release key** and verified -against :data:`UPDATER_PUBKEY` — the same public key the updater already carries -in ``frontend/src-tauri/tauri.conf.json``, kept in lockstep by -``tests/test_gallery_previews.py``. Signature verification is the *only* thing +``manifest.json`` is signed with the minisign key that signed the final Tauri +release feeds and verified against :data:`UPDATER_PUBKEY`, which this module +carries verbatim (``tests/test_gallery_previews.py`` checks it decodes as a +valid minisign key). Signature verification is the *only* thing that makes the per-file SHA-256 digests meaningful, so a manifest that fails it is discarded outright (including a manifest already on disk: it is re-verified on every load, not trusted because it was trusted once). No new key, no new @@ -71,10 +71,9 @@ #: was not written against, and the signature proves nothing about semantics. SCHEMA_VERSION = 1 -#: Minisign public key of the Tauri release signing key, verbatim from -#: ``frontend/src-tauri/tauri.conf.json`` (plugins.updater.pubkey). Duplicated -#: rather than read from the config because the frozen backend does not ship -#: tauri.conf.json; the ratchet test keeps the two byte-identical. +#: Minisign public key of the release signing key the final Tauri builds' updater +#: carried. The Tauri shell and its config are gone, so this constant is now the +#: only copy; changing it orphans every manifest signed with the old key. UPDATER_PUBKEY = ( "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDhFMDQ1QkZCQ0I4RDlCQkYKUl" "dTL200M0wrMXNFamdPSGF3VkUzVjBRY1FFOE0yTkxSMVZKNUowL2wyZEw2OG1TWXNLMDlSeTQK" From 474631619ce021d5eee058ce84660a282b5f49d7 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:38:24 +0530 Subject: [PATCH 52/85] fix(windows): start the Electron runtime under a non-English profile path Python 3.11 decodes .pth files in the ANSI code page (encoding="locale", which ignores PYTHONUTF8), and uv's editable install writes the runtime project path there as UTF-8. On a CJK Windows a non-English username made the interpreter die in init_import_site before any VoiceStudio code ran; the Tauri-era short-path workaround never carried over to Electron. Non-ASCII .pth lines are now rewritten into equivalent ASCII-only lines after every runtime install and before each packaged launch, which also heals existing installs without a repair. ASCII files are untouched. (#1783) --- electron/src/main/backend.ts | 3 + electron/src/main/pth-ascii.test.ts | 85 +++++++++++++++++++++++++ electron/src/main/pth-ascii.ts | 92 ++++++++++++++++++++++++++++ electron/src/main/runtime-project.ts | 7 +++ 4 files changed, 187 insertions(+) create mode 100644 electron/src/main/pth-ascii.test.ts create mode 100644 electron/src/main/pth-ascii.ts diff --git a/electron/src/main/backend.ts b/electron/src/main/backend.ts index 79a9d641c..0da59213e 100644 --- a/electron/src/main/backend.ts +++ b/electron/src/main/backend.ts @@ -9,6 +9,7 @@ import { stageRuntimeSources, type RuntimeRegion, } from './runtime-project'; +import { asciiSafePthFiles } from './pth-ascii'; import { CrashJournal } from './crash-journal'; import { availableBackendPort } from './backend-port'; import { legacyStorageEnv } from './legacy-storage'; @@ -353,6 +354,8 @@ export async function resolveSpawnPlan( if (override) return { argv: override, cwd: root }; if (app.isPackaged) { const project = packagedProject ?? join(defaultRuntimeRoot(), 'project'); + // Heals runtimes installed before #1783 was fixed without a repair run. + await asciiSafePthFiles(join(project, '.venv')).catch(() => []); return { argv: [runtimePython(project), '-m', ...UVICORN_ARGS, '--port', String(port)], cwd: project, diff --git a/electron/src/main/pth-ascii.test.ts b/electron/src/main/pth-ascii.test.ts new file mode 100644 index 000000000..2baf9af73 --- /dev/null +++ b/electron/src/main/pth-ascii.test.ts @@ -0,0 +1,85 @@ +import { spawnSync } from 'node:child_process'; +import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { asciiSafePthFiles, asciiSafePthText, pythonLiteral } from './pth-ascii'; + +const CJK_USER = '\u5f20\u4e09'; // a typical non-English Windows username + +function python(): string | null { + for (const candidate of ['python3', 'python']) { + const probe = spawnSync(candidate, ['-c', 'import sys; print(sys.version_info[0])'], { + encoding: 'utf8', + }); + if (probe.status === 0 && probe.stdout.trim() === '3') return candidate; + } + return null; +} + +describe('asciiSafePthFiles (#1783)', () => { + it('rewrites a non-ASCII editable path line and leaves ASCII files untouched', async () => { + const venv = mkdtempSync(join(tmpdir(), 'vs-pth-')); + const posix = join(venv, 'lib', 'python3.11', 'site-packages'); + const windows = join(venv, 'Lib', 'site-packages'); + mkdirSync(posix, { recursive: true }); + mkdirSync(windows, { recursive: true }); + const project = join(venv, CJK_USER, 'runtime', 'project'); + writeFileSync(join(posix, '_editable_impl_omnivoice.pth'), project, 'utf8'); + writeFileSync(join(windows, '_editable_impl_omnivoice.pth'), project, 'utf8'); + const ascii = 'import _virtualenv'; + writeFileSync(join(posix, '_virtualenv.pth'), ascii, 'utf8'); + + const rewritten = await asciiSafePthFiles(venv); + + // Case-insensitive file systems see one directory under both spellings. + expect(rewritten.length).toBeGreaterThanOrEqual(1); + for (const file of rewritten) { + const bytes = readFileSync(file); + expect(bytes.every((byte) => byte < 0x80)).toBe(true); + expect(bytes.toString('utf8')).toContain('\\u5f20\\u4e09'); + } + expect(readFileSync(join(posix, '_virtualenv.pth'), 'utf8')).toBe(ascii); + expect(await asciiSafePthFiles(venv)).toEqual([]); + }); + + it('drops non-ASCII comments and escapes code lines', () => { + expect(asciiSafePthText(`# ${CJK_USER}\n./lib`)).toBe('./lib'); + expect(asciiSafePthText(`import os; x = '${CJK_USER}'`)).toBe( + "import builtins; builtins.exec('import os; x = \\'\\u5f20\\u4e09\\'')", + ); + expect(pythonLiteral('C:\\Users\\\u00e9\ud83d\ude00')).toBe( + "'C:\\\\Users\\\\\\xe9\\U0001f600'", + ); + }); + + it.skipIf(!python())('lets site add the original directory under an ASCII locale', () => { + const venv = mkdtempSync(join(tmpdir(), 'vs-pth-site-')); + const sitePackages = join(venv, 'site-packages'); + const target = join(venv, CJK_USER, 'project'); + mkdirSync(sitePackages, { recursive: true }); + mkdirSync(target, { recursive: true }); + const pth = join(sitePackages, '_editable_impl_omnivoice.pth'); + const script = + 'import site, sys; before = len(sys.path); ' + + `site.addpackage(sys.argv[1], '_editable_impl_omnivoice.pth', set()); ` + + 'print(sys.version_info[1], sys.path[before:] == [sys.argv[2]])'; + // Python 3.11 decodes .pth files in the locale encoding even in UTF-8 + // mode; an ASCII locale reproduces the cp936 failure off Windows while + // UTF-8 mode keeps the file-system encoding able to name the directory. + const env = { ...process.env, LC_ALL: 'C', LANG: 'C', PYTHONCOERCECLOCALE: '0', PYTHONUTF8: '1' }; + const run = () => + spawnSync(python()!, ['-c', script, sitePackages, target], { encoding: 'utf8', env }); + + writeFileSync(pth, target, 'utf8'); + const before = run(); + const minor = Number(before.stdout.split(' ')[0]); + // 3.13+ reads .pth files as UTF-8 first, so only older interpreters fail. + if (before.status !== 0 || minor < 13) expect(before.stdout.trim().endsWith('True')).toBe(false); + + writeFileSync(pth, asciiSafePthText(target), 'utf8'); + const after = run(); + expect(after.stderr).toBe(''); + expect(after.stdout.trim().endsWith('True')).toBe(true); + }); +}); diff --git a/electron/src/main/pth-ascii.ts b/electron/src/main/pth-ascii.ts new file mode 100644 index 000000000..0c92da539 --- /dev/null +++ b/electron/src/main/pth-ascii.ts @@ -0,0 +1,92 @@ +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +/** + * Keep a runtime's `.pth` files ASCII so Python can start (#1783). + * + * Python 3.11 decodes `.pth` files in the ANSI code page (site.addpackage uses + * `encoding="locale"`, which ignores PYTHONUTF8). uv's editable install writes + * the project path into `_editable_impl_omnivoice.pth` as UTF-8, so on a CJK + * Windows (cp936/cp932/...) a non-English username made the interpreter die in + * `init_import_site` before any VoiceStudio code ran. + * + * A non-ASCII path line is rewritten into an equivalent ASCII-only `import` + * line that appends the same directory with escaped characters; non-ASCII + * comment lines are dropped. ASCII files are never touched, so this is a no-op + * everywhere except the affected installs. + */ +export async function asciiSafePthFiles(venv: string): Promise { + const rewritten: string[] = []; + for (const sitePackages of await sitePackagesDirs(venv)) { + let names: string[]; + try { + names = await readdir(sitePackages); + } catch { + continue; + } + for (const name of names.filter((entry) => entry.toLowerCase().endsWith('.pth'))) { + const file = join(sitePackages, name); + const bytes = await readFile(file).catch(() => null); + if (!bytes || bytes.every((byte) => byte < 0x80)) continue; + let text: string; + try { + text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); + } catch { + continue; // Not written by uv/pip as UTF-8; leave it alone. + } + await writeFile(file, asciiSafePthText(text), 'utf8'); + rewritten.push(file); + } + } + return rewritten; +} + +/** The ASCII-only equivalent of a `.pth` file's text. */ +export function asciiSafePthText(text: string): string { + return text + .split(/\r?\n/) + .flatMap((line) => { + if (isAscii(line)) return [line]; + const trimmed = line.trim(); + if (trimmed.startsWith('#')) return []; + if (/^import[ \t]/.test(line)) return [`import builtins; builtins.exec(${pythonLiteral(line)})`]; + // site.addpackage: a path line is relative to sitedir, added once, and + // only when it exists. `sitedir` is in scope where site execs the line. + return [ + `import os, sys; p = os.path.abspath(os.path.join(sitedir, ${pythonLiteral(line.trimEnd())})); ` + + 'os.path.exists(p) and p not in sys.path and sys.path.append(p)', + ]; + }) + .join('\n'); +} + +function isAscii(value: string): boolean { + return [...value].every((char) => char.charCodeAt(0) < 0x80); +} + +/** A single-quoted Python string literal using only printable ASCII. */ +export function pythonLiteral(value: string): string { + let out = "'"; + for (const char of value) { + const code = char.codePointAt(0)!; + if (char === '\\') out += '\\\\'; + else if (char === "'") out += "\\'"; + else if (code >= 0x20 && code < 0x7f) out += char; + else if (code <= 0xff) out += `\\x${code.toString(16).padStart(2, '0')}`; + else if (code <= 0xffff) out += `\\u${code.toString(16).padStart(4, '0')}`; + else out += `\\U${code.toString(16).padStart(8, '0')}`; + } + return `${out}'`; +} + +async function sitePackagesDirs(venv: string): Promise { + const dirs = [join(venv, 'Lib', 'site-packages')]; + try { + for (const entry of await readdir(join(venv, 'lib'))) { + if (entry.startsWith('python')) dirs.push(join(venv, 'lib', entry, 'site-packages')); + } + } catch { + // Windows layout, or no venv yet. + } + return dirs; +} diff --git a/electron/src/main/runtime-project.ts b/electron/src/main/runtime-project.ts index 3152d1c62..a904085a9 100644 --- a/electron/src/main/runtime-project.ts +++ b/electron/src/main/runtime-project.ts @@ -1,5 +1,6 @@ import { downloadProxyEnv } from './proxy-env'; import { downloadRuntimeInstaller } from './runtime-download'; +import { asciiSafePthFiles } from './pth-ascii'; import { execFile } from 'node:child_process'; import { createHash, randomUUID } from 'node:crypto'; import { existsSync } from 'node:fs'; @@ -644,6 +645,9 @@ export async function installRuntime( // New environments must not borrow another application's Python from PATH. // Existing compatible environments are kept; Clean & Retry rebuilds explicitly. phase('installing_deps'); + // Heal an existing environment first so its interpreter can run the probes + // below (and uv's own interpreter query) instead of dying in `site` (#1783). + await asciiSafePthFiles(join(project, '.venv')); const interpreterExists = await stat(runtimePython(project)).then( (info) => info.isFile(), () => false, @@ -768,6 +772,9 @@ export async function installRuntime( } await ensureCudnn8Compat(uv, project, run, env, signal); signal.throwIfAborted(); + // A non-English profile path in uv's editable .pth crashes Python 3.11 at + // startup on a non-UTF-8 Windows code page (#1783). + await asciiSafePthFiles(join(project, '.venv')); phase('verifying'); await run(runtimePython(project), ['-c', RUNTIME_IMPORT_PROBE], project); signal.throwIfAborted(); From 86850d3a2d61074f820a3f8b016ede03dc459a07 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:48:40 +0530 Subject: [PATCH 53/85] docs: default Docker quick starts to :stable and finish the audit follow-ups --- .github/CONTRIBUTING.md | 2 ++ AGENTS.md | 2 +- CLAUDE.md | 2 +- deploy/dockerhub-overview.md | 4 ++-- docs/STRUCTURE.md | 2 +- docs/install/docker.md | 10 +++++----- docs/install/troubleshooting.md | 5 ++++- electron/src/shared/pages/Settings.jsx | 6 +----- 8 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index a3be314a9..c986a6adf 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -352,6 +352,8 @@ hard rules from the first prompt. on in Settings (`backend/services/gallery.py`). - **Packaged-app update checks** against GitHub Releases; downloads wait for the user (`electron/src/main/updater.ts`). + - **yt-dlp updates** from PyPI, only when the user clicks update + (`backend/services/media_tools.py`). - Bug reports as prefilled GitHub Issue URLs opened in the user's browser. - PostHog analytics only after a yes at the first-run consent prompt (`backend/core/analytics.py`, allowlisted content-free metadata). diff --git a/AGENTS.md b/AGENTS.md index bec4b58ad..ac6a71af7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ Binding for every AI agent (Claude, Codex, Cursor, review bots, …). CLAUDE.md ## Change rules (see CLAUDE.md for full text) - Root-cause the class, not the instance; fail-before/pass-after regression test; smallest correct change. - Default behavior identical on macOS/Windows/Linux; platform-only features go behind explicit opt-in. Divergent default = P0. -- Local-first: no new required network calls; any HF download gated on installed-ness or explicit user action; all synthetic audio through the `mark_synthetic` chokepoint. Sanctioned calls are listed in `.github/CONTRIBUTING.md` → Quality gates: user-started first-run setup (ffmpeg from GitHub `zackees/ffmpeg_bins`, the huggingface.co/hf-mirror.com reachability probe, HF model downloads), opening/enabling the galleries (jsDelivr manifest, GitHub Releases), packaged update checks, consent-gated analytics, prefilled-URL bug reports, and the Pro licence check after a key is entered. Owner-approved exception (2026-09-28): the public GitHub star count may refresh automatically every 20 minutes, without credentials/referrer or user content; offline use remains unaffected. +- Local-first: no new required network calls; any HF download gated on installed-ness or explicit user action; all synthetic audio through the `mark_synthetic` chokepoint. Sanctioned calls are listed in `.github/CONTRIBUTING.md` → Quality gates: user-started first-run setup (ffmpeg from GitHub `zackees/ffmpeg_bins`, the huggingface.co/hf-mirror.com reachability probe, HF model downloads), opening/enabling the galleries (jsDelivr manifest, GitHub Releases), packaged update checks, user-clicked yt-dlp updates from PyPI, consent-gated analytics, prefilled-URL bug reports, and the Pro licence check after a key is entered. Owner-approved exception (2026-09-28): the public GitHub star count may refresh automatically every 20 minutes, without credentials/referrer or user content; offline use remains unaffected. - Every user-facing string via i18n, present in ALL 21 `electron/src/renderer/src/i18n/locales/*.json` files (the catalog the app loads) with real translations; `tests/test_locale_parity.py` and Electron `locale:check` enforce it. The legacy `electron/src/shared/i18n/` catalog is loaded only by shared-module tests. - Docs-sync in the same PR. CHANGELOG Unreleased: quiet one-liners ending `(#N)` + `— thanks @user!` for community work, under a short `**Highlights**` list. - Tagged release announcements lead with the biggest user-visible change; redesigns need real UI screenshots and migrations need installer links and steps. Verify all contributor credits from the tag comparison and included PRs; list authors and bug reporters separately (see `docs/RELEASING.md`). diff --git a/CLAUDE.md b/CLAUDE.md index 41769cbd1..bcb1a4674 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ Everything else (new engines, fancy features) is downstream of "the thing instal - **Cross-platform parity**: Every fix must work on macOS (Apple Silicon; Intel Macs are limited to the app UI with a remote backend, as README.md and `docs/install/macos.md` document), Windows (x64), and Linux (AppImage + deb). No platform-only regressions; the cross-platform bug bash (PR #51) is the baseline. - **Default features must work on every platform (strict rule, 2026-05-20):** A feature that ships in default mode — out-of-the-box, no user customization, no opt-in toggle — must behave identically on macOS, Windows, and Linux. Platform-specific *implementation code* is allowed for OS APIs / shells / packaging, but the user-visible *default behavior* cannot diverge. Platform-only features (e.g., a macOS-only global shortcut, a Windows-only path picker) must go behind explicit user opt-in: Settings toggle, env var, or CLI flag. When a default doesn't work on a platform, that's a P0 bug — either fix it on the missing platform or move it behind opt-in. No third option. **This rule governs BEHAVIOUR, not PERFORMANCE** (clarified 2026-07-30, council): hardware acceleration is expected to vary by host — CUDA, MPS, DirectML, Triton availability and `torch.compile` are all host-dependent by design, and reading the rule to forbid that would forbid GPU support itself. An optimization that is skipped where it cannot work (missing Triton, an arch the wheel lacks, a path its toolchain cannot link) is NOT a parity violation; a *feature* the user can see and use on one OS but not another is. - **Backward-compatible project data**: Existing `omnivoice_data/` (user voices, projects, settings) must keep working without manual migration. Any DB schema change goes through alembic with a tested upgrade path. -- **Local-first guarantee preserved**: nothing leaves the machine without the user's **explicit yes**, and the app must remain fully functional with everything declined. Auto bug reporting is opt-in and submits only to GitHub Issues (prefilled-URL, from the user's own browser). Product analytics (owner-sanctioned 2026-07-16; US project migration 2026-09-26) is opt-in PostHog US with a **first-run consent prompt** — two equal-weight Yes/No buttons, never default-on, skipping = off; consent-gated, allowlisted content-free metadata only (`backend/core/analytics.py`); every build — installer, Docker, and source alike (owner reversal 2026-07-20, #1193) — carries the in-repo publishable write-only token and shows the same consent ask, with env/baked token overriding it. No required cloud calls, accounts, or API keys. Sanctioned calls (owner-set 2026-10-02; full list in `.github/CONTRIBUTING.md` → Quality gates): during first-run setup the user starts — the pinned ffmpeg download from GitHub `zackees/ffmpeg_bins` (`backend/services/media_tools.py`), the huggingface.co/hf-mirror.com reachability probe for restricted networks (`backend/services/endpoint_race.py`), and Hugging Face model downloads; opening the community gallery (jsDelivr manifest, `backend/api/routers/community.py`) or enabling the voice-preview gallery (GitHub Releases, `backend/services/gallery.py`); and packaged update checks against GitHub Releases (`electron/src/main/updater.ts`). Narrow owner-approved exception (2026-09-28): the GitHub Star badge may fetch the public repository star count when shown and refresh every 20 minutes while visible, without credentials or a referrer. It sends no project, recording, or account data, retains the last count on failure, and never blocks offline use. +- **Local-first guarantee preserved**: nothing leaves the machine without the user's **explicit yes**, and the app must remain fully functional with everything declined. Auto bug reporting is opt-in and submits only to GitHub Issues (prefilled-URL, from the user's own browser). Product analytics (owner-sanctioned 2026-07-16; US project migration 2026-09-26) is opt-in PostHog US with a **first-run consent prompt** — two equal-weight Yes/No buttons, never default-on, skipping = off; consent-gated, allowlisted content-free metadata only (`backend/core/analytics.py`); every build — installer, Docker, and source alike (owner reversal 2026-07-20, #1193) — carries the in-repo publishable write-only token and shows the same consent ask, with env/baked token overriding it. No required cloud calls, accounts, or API keys. Sanctioned calls (owner-set 2026-10-02; full list in `.github/CONTRIBUTING.md` → Quality gates): during first-run setup the user starts — the pinned ffmpeg download from GitHub `zackees/ffmpeg_bins` (`backend/services/media_tools.py`), the huggingface.co/hf-mirror.com reachability probe for restricted networks (`backend/services/endpoint_race.py`), and Hugging Face model downloads; opening the community gallery (jsDelivr manifest, `backend/api/routers/community.py`) or enabling the voice-preview gallery (GitHub Releases, `backend/services/gallery.py`); packaged update checks against GitHub Releases (`electron/src/main/updater.ts`); and yt-dlp updates from PyPI when the user clicks update. Narrow owner-approved exception (2026-09-28): the GitHub Star badge may fetch the public repository star count when shown and refresh every 20 minutes while visible, without credentials or a referrer. It sends no project, recording, or account data, retains the last count on failure, and never blocks offline use. - **Beta release cadence (no RC, no ceremony — strict rule, 2026-05-20):** the current release line (0.5.x today) has **no release candidates, no 48h soak, no formal release ceremony**. Every fix goes continuous-to-main; the owner tags a patch from main whenever the current state is worth cutting. No `-rc` tags. No phased release. No deferrals to a future minor or major version — every open issue and every open community PR gets absorbed into the current line or explicitly declined. Users follow `main` for previews; users wanting stable stay on the latest tagged release. diff --git a/deploy/dockerhub-overview.md b/deploy/dockerhub-overview.md index ce80718c1..7e7971c34 100644 --- a/deploy/dockerhub-overview.md +++ b/deploy/dockerhub-overview.md @@ -60,7 +60,7 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - palashdeb/omnivoice-studio:latest + palashdeb/omnivoice-studio:stable ``` Open . The first run downloads a few GB of model weights — @@ -78,7 +78,7 @@ docker run -d --name omnivoice --gpus all \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - palashdeb/omnivoice-studio:latest + palashdeb/omnivoice-studio:stable ``` GPU mode needs the diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index 8eff45f47..cd7126b25 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -125,7 +125,7 @@ VoiceStudio/ │ ├── media/, screenshot-*.png, preview.png, logo.* │ └── languages.md, training.md, data_preparation.md, evaluation.md, voice-design.md │ -├── examples/ ⟵ runnable demos + sample inputs (agentic/, speech-platform/) +├── examples/ ⟵ runnable demos + sample inputs (agentic/, config/) │ ├── notebooks/ ⟵ OmniVoice_Studio_Colab.ipynb │ diff --git a/docs/install/docker.md b/docs/install/docker.md index cc5e66634..f8d448a03 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -73,18 +73,18 @@ master key. ## Pull and run (CPU) ```bash -docker pull ghcr.io/debpalash/voicestudio:latest +docker pull ghcr.io/debpalash/voicestudio:stable docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - ghcr.io/debpalash/voicestudio:latest + ghcr.io/debpalash/voicestudio:stable ``` > **Docker Hub mirror:** the same images are published to > `palashdeb/omnivoice-studio` on Docker Hub with identical tags — swap the -> image for `palashdeb/omnivoice-studio:latest` if you prefer Docker Hub. +> image for `palashdeb/omnivoice-studio:stable` if you prefer Docker Hub. > Tag semantics (`:latest` = rolling main preview, `:stable`/`:X.Y.Z` = > releases) are the same on both registries. @@ -98,7 +98,7 @@ docker run -d --name omnivoice --gpus all \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - ghcr.io/debpalash/voicestudio:latest + ghcr.io/debpalash/voicestudio:stable ``` GPU mode requires the @@ -363,7 +363,7 @@ prebuilt image via `docker run -e` (the older `VITE_OMNIVOICE_API` is inlined at docker run -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -e OMNIVOICE_PUBLIC_API_BASE=https://api.your-host.example \ -p 0.0.0.0:3900:3900 \ - ghcr.io/debpalash/voicestudio:latest + ghcr.io/debpalash/voicestudio:stable ``` > `OMNIVOICE_PUBLIC_API_BASE` must be a plain `http(s)://…` URL; anything else diff --git a/docs/install/troubleshooting.md b/docs/install/troubleshooting.md index 3926ebc42..7a4fe4940 100644 --- a/docs/install/troubleshooting.md +++ b/docs/install/troubleshooting.md @@ -1075,7 +1075,10 @@ install `.pth` embeds that path in UTF-8, which is rarely a valid byte sequence in the active ANSI code page (`gbk`, `shift_jis`, etc.), and the interpreter can never start (#1783). -**Fix:** keep the app environment on an ASCII-only path. On the first-run +**Fix:** VoiceStudio now rewrites those `.pth` entries to ASCII-only paths after +each runtime install and before each launch, so existing environments recover +on the next start. If the backend still cannot start, keep the app +environment on an ASCII-only path. On the first-run setup screen, press **Change…** beside **App environment** and pick a folder such as `C:\VoiceStudio` before setup starts. An environment that cannot start fails VoiceStudio's runtime check, so setup opens again: choose an ASCII-only diff --git a/electron/src/shared/pages/Settings.jsx b/electron/src/shared/pages/Settings.jsx index 0f7674432..9b6fd124e 100644 --- a/electron/src/shared/pages/Settings.jsx +++ b/electron/src/shared/pages/Settings.jsx @@ -242,11 +242,7 @@ export default function Settings() { `- **Outputs directory:** ${info?.outputs_dir || '—'}`, `- **Crash log:** ${info?.crash_log_path || '—'}`, `- **Update channel:** ${updateChannel}`, - `- **Update endpoint:** ${ - updateChannel === 'preview' - ? 'https://github.com/debpalash/VoiceStudio/releases/download/preview/latest.json' - : 'https://github.com/debpalash/VoiceStudio/releases/latest/download/latest.json' - }`, + `- **Update endpoint:** https://github.com/debpalash/VoiceStudio/releases/latest/download/latest.json`, `- **User agent:** ${ua}`, ]; try { From 42e75b82090281f55a0031b3559ff0c344f79a27 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:57:51 +0530 Subject: [PATCH 54/85] fix(cla): count tool addresses only in co-author trailers or on verified bot accounts --- .github/scripts/cla_check.py | 11 ++++++++--- tests/test_cla_check.py | 22 ++++++++++++++++++---- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/scripts/cla_check.py b/.github/scripts/cla_check.py index f255e528b..46aee5b85 100644 --- a/.github/scripts/cla_check.py +++ b/.github/scripts/cla_check.py @@ -146,10 +146,15 @@ def require(person: Person) -> None: result.unknown.append(actor["unknown"]) continue email = (actor.get("email") or "").strip() - if TOOL_EMAILS.match(email): - continue if actor.get("id"): - require(Person(actor["login"], int(actor["id"]))) + # GitHub verified this account; a person cannot hold a [bot] login. + if not (actor.get("login") or "").lower().endswith("[bot]"): + require(Person(actor["login"], int(actor["id"]))) + continue + # A tool named in a co-author trailer is skipped: the person who + # submits the work signs for it. An unlinked commit author with a tool + # address is not, because anyone can commit under that address. + if actor.get("trailer") and TOOL_EMAILS.match(email): continue # A commit author counts only through the account GitHub linked to it. # An unlinked no-reply address may name someone else's account, so it diff --git a/tests/test_cla_check.py b/tests/test_cla_check.py index a4a5cbcf8..d749d7be0 100644 --- a/tests/test_cla_check.py +++ b/tests/test_cla_check.py @@ -256,9 +256,23 @@ def test_stored_signature_records_a_hash_of_the_comment(): "noreply@openai.com", "codex@openai.com", "175728472+Copilot@users.noreply.github.com", "cursoragent@cursor.com", "41898282+github-actions[bot]@users.noreply.github.com", ]) -def test_agent_identities_never_need_to_sign(email): - linked = actor("Copilot", 175728472, email=email) if "Copilot" in email else actor(email=email) - assert cla.evaluate(OPENER, False, [linked], {1001}).passed +def test_agent_co_author_trailers_never_need_to_sign(email): + trailer = {**actor(email=email), "trailer": True} + assert cla.evaluate(OPENER, False, [trailer], {1001}).passed + + +@pytest.mark.parametrize("email", ["claude@users.noreply.github.com", "noreply@openai.com"]) +def test_unlinked_commit_author_with_a_tool_address_fails_closed(email): + # Anyone can commit under a tool address; only GitHub's account link counts. + result = cla.evaluate(OPENER, False, [actor(email=email)], {1001}) + assert not result.passed and result.unknown + + +def test_linked_bot_accounts_are_skipped_but_linked_people_are_not(): + bot = actor("renovate[bot]", 29139614, email="29139614+renovate[bot]@users.noreply.github.com") + person = actor("claude", 5005, email="claude@users.noreply.github.com") + result = cla.evaluate(OPENER, False, [bot, person], {1001}) + assert result.unsigned == [cla.Person("claude", 5005)] def test_superseded_pull_request_authors_must_sign(): @@ -305,7 +319,7 @@ def test_superseding_pr_waits_for_the_original_authors(): others = {12: {"login": "frank", "id": 6006, "type": "User"}, 13: {"login": "dependabot[bot]", "id": 49699333, "type": "Bot"}} commits = {12: [actor("frank", 6006), actor("gina", 7007), actor(name="hal", email="hal@example.com")], - 13: [actor(email="49699333+dependabot[bot]@users.noreply.github.com")]} + 13: [actor("dependabot[bot]", 49699333, email="49699333+dependabot[bot]@users.noreply.github.com")]} gh = FakeGitHub([], [actor("alice", 1001)], store=cla.empty_store(), pr={"body": "Supersedes #12 and #13"}, others=others, other_commits=commits) result = cla.run(gh, "pull_request_target", {"pull_request": {"number": 7}}) From d16d9543cd53f4148341b6585b3c522e8eda5269 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:57:51 +0530 Subject: [PATCH 55/85] fix(ci): run the identity gate from the base branch, require it, and widen agent attribution --- .github/CONTRIBUTING.md | 9 ++++++--- .github/scripts/agent_identities.py | 2 +- .github/workflows/commit-identity.yml | 16 +++++++++++++++- docs/maintainers/repository-settings.md | 6 +++++- scripts/check_commit_identities.py | 5 ++++- tests/test_actions_pinned.py | 4 +++- tests/test_cla_workflow.py | 5 ++++- tests/test_commit_identities.py | 11 +++++++++-- 8 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index c4a1caa61..b692af679 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -245,8 +245,10 @@ Commit with a real identity (your GitHub noreply address works). The PR commit's author, committer, or `Co-authored-by:`/`Signed-off-by:` email is a placeholder (`test@local`, `you@example.com`, `mergetest`, hostname-style `*.local`), an AI agent, or on the hashed block list, or when a commit message -or the PR description credits an AI agent; fix it with -`git rebase --exec 'git commit --amend --no-edit --reset-author' origin/main`. +or the PR description credits an AI agent. Fix an identity with +`git rebase --exec 'git commit --amend --no-edit --reset-author' origin/main`; +fix agent credit by rewording the commit messages (`git rebase -i`, then +`reword`) and editing the PR description. --- @@ -301,7 +303,8 @@ correct fix, regression test; see the quality gates below). You submit agent-assisted work as your own, under your own git identity. Don't credit agents in commits or the PR description: no `Co-authored-by:` trailer -for an AI agent, no "Generated with …" line, no session links. The +for an AI agent, no "Generated with …" line, no agent session or share links +(claude.ai, chatgpt.com) and no `Claude-Session:` trailer. The `commit-identity` check fails PRs that carry them. Co-authors who are people are welcome. diff --git a/.github/scripts/agent_identities.py b/.github/scripts/agent_identities.py index e924ce0af..093da7898 100644 --- a/.github/scripts/agent_identities.py +++ b/.github/scripts/agent_identities.py @@ -38,7 +38,7 @@ # Author and committer names that can only be an agent. People are called # Claude or Jules, so a bare first name is not enough here. AGENT_AUTHOR_NAMES = re.compile( - r"^((claude code|claude (opus|sonnet|haiku|fable|instant|\d)\S*( .*)?|cursor agent|cursoragent" + r"^((claude code( .*)?|claude (opus|sonnet|haiku|fable|instant|\d)\S*( .*)?|cursor agent|cursoragent" r"|(github )?copilot|(openai )?codex|chatgpt|devin ai|google jules|openhands( agent)?)(\[bot\])?" rf"|({_LOGINS})\[bot\])$", re.IGNORECASE, diff --git a/.github/workflows/commit-identity.yml b/.github/workflows/commit-identity.yml index c00964eb0..0114c8afe 100644 --- a/.github/workflows/commit-identity.yml +++ b/.github/workflows/commit-identity.yml @@ -35,4 +35,18 @@ jobs: - name: Check PR commit identities env: BASE_REF: ${{ github.base_ref }} - run: python3 scripts/check_commit_identities.py --range "origin/${BASE_REF}..HEAD" --event "$GITHUB_EVENT_PATH" + # Run the base branch's checker and lists, so a pull request cannot + # edit the gate it is judged by. Its commits are only read as data. + run: | + policy="$RUNNER_TEMP/policy" + mkdir -p "$policy" + files="scripts/check_commit_identities.py scripts/blocked_identity_hashes.txt .github/scripts/agent_identities.py" + if git cat-file -e "origin/${BASE_REF}:scripts/check_commit_identities.py" 2>/dev/null; then + git archive "origin/${BASE_REF}" $files | tar -x -C "$policy" + else + # Only the pull request that introduces the gate reaches this branch. + tar -c $files | tar -x -C "$policy" + fi + python3 "$policy/scripts/check_commit_identities.py" \ + --hash-file "$policy/scripts/blocked_identity_hashes.txt" \ + --range "origin/${BASE_REF}..HEAD" --event "$GITHUB_EVENT_PATH" diff --git a/docs/maintainers/repository-settings.md b/docs/maintainers/repository-settings.md index 580fac3a8..bd9121ddb 100644 --- a/docs/maintainers/repository-settings.md +++ b/docs/maintainers/repository-settings.md @@ -24,7 +24,8 @@ the description: the check then asks their authors to sign too. ## Ruleset: `main` -Requires a pull request, the backend/frontend test job and the CLA check, and +Requires a pull request, the backend/frontend test job, the commit-identity +check and the CLA check, and blocks force-push and deletion. Without the pull request rule, a direct push whose commit already carries passing checks would be accepted. The rule needs no approving review, because `@debpalash` is the only maintainer and GitHub @@ -33,6 +34,8 @@ does not let authors approve their own pull requests. The `context` values must match the check names shown on a pull request: - `Tests (backend + frontend)`: the `name:` of the `test` job in `.github/workflows/ci.yml`. +- `Commit identities`: the `name:` of the job in `.github/workflows/commit-identity.yml`. + Without it, agent and placeholder identities are reported but not blocked. - `CLA`: the commit status that `.github/scripts/cla_check.py` sets on the pull request's head commit. Require this status, not the `cla` job: the job succeeds whenever the checker runs, and comment-triggered runs are not @@ -72,6 +75,7 @@ gh api --method POST repos/debpalash/VoiceStudio/rulesets --input - <<'JSON' "strict_required_status_checks_policy": true, "required_status_checks": [ { "context": "Tests (backend + frontend)", "integration_id": 15368 }, + { "context": "Commit identities", "integration_id": 15368 }, { "context": "CLA" } ] } diff --git a/scripts/check_commit_identities.py b/scripts/check_commit_identities.py index 16e112861..add80c67c 100755 --- a/scripts/check_commit_identities.py +++ b/scripts/check_commit_identities.py @@ -62,7 +62,10 @@ # Agents that commit or get credited as co-authors (agent_identities.py). GitHub # App bots such as dependabot[bot] are not agents and stay allowed, and so do # people with a work address at an AI company. -_AGENTS = r"(claude|cursor|copilot|codex|chatgpt|openai|gemini|devin|jules|aider|cline|windsurf|an? ai\b|ai\b)" +_AGENTS = ( + r"(claude|cursor|copilot|codex|chatgpt|openai|gemini|devin|jules|aider|cline|windsurf|coderabbit|greptile" + r"|sourcery|tabnine|codeium|amazon q|kiro|qodo|openhands|sweep|an? ai\b|ai\b|an? llm\b|llm\b)" +) AI_ATTRIBUTION = re.compile( rf"^[ \t>*_-]*(generated|written|created|authored)[ \t]+(with|by|using)[ \t]+\[?{_AGENTS}" r"|🤖[ \t]*(generated|written|created|authored)\b|^[ \t]*claude-session[ \t]*:" diff --git a/tests/test_actions_pinned.py b/tests/test_actions_pinned.py index 84172c006..272ec2de1 100644 --- a/tests/test_actions_pinned.py +++ b/tests/test_actions_pinned.py @@ -82,7 +82,9 @@ def _missing_version_comments(text: str, refs: list[str]) -> list[str]: """ bad = [] for u, count in collections.Counter(refs).items(): - lines = re.findall(rf"""uses:[ \t]*(["']?){re.escape(u)}\1([^\n]*)""", text) + lines = re.findall( + rf"""^[ \t]*(?:-[ \t]+)?uses:[ \t]*(["']?){re.escape(u)}\1([^\n]*)""", text, re.MULTILINE + ) if len(lines) < count or any(not re.match(r"\s+#\s*\S", rest) for _, rest in lines): bad.append(u) return bad diff --git a/tests/test_cla_workflow.py b/tests/test_cla_workflow.py index fc6a19865..ebb53df20 100644 --- a/tests/test_cla_workflow.py +++ b/tests/test_cla_workflow.py @@ -122,7 +122,7 @@ def _main_ruleset() -> dict: return next(b for b in blocks if b.get("name") == "main") -def test_main_ruleset_requires_pull_requests_and_the_cla_status(): +def test_main_ruleset_requires_pull_requests_identities_and_the_cla_status(): rules = {rule["type"]: rule.get("parameters", {}) for rule in _main_ruleset()["rules"]} # Required checks alone accept a direct push whose commit already passed. assert rules["pull_request"] == { @@ -132,6 +132,9 @@ def test_main_ruleset_requires_pull_requests_and_the_cla_status(): } contexts = {c["context"] for c in rules["required_status_checks"]["required_status_checks"]} assert cla.STATUS_CONTEXT in contexts + # Without the identity gate, agent identities that skip the CLA go unblocked. + identity_job = next(iter(_load(_WORKFLOWS / "commit-identity.yml")["jobs"].values()))["name"] + assert identity_job in contexts assert {"deletion", "non_fast_forward"} <= set(rules) diff --git a/tests/test_commit_identities.py b/tests/test_commit_identities.py index da2d73c6a..e9cb55168 100644 --- a/tests/test_commit_identities.py +++ b/tests/test_commit_identities.py @@ -159,8 +159,15 @@ def test_workflow_runs_on_untrusted_safe_pull_request(): assert checkout["with"] == {"fetch-depth": "0", "persist-credentials": "false"} assert "${{" not in run["run"] assert run["env"]["BASE_REF"] == "${{ github.base_ref }}" - assert "scripts/check_commit_identities.py" in run["run"] and "origin/${BASE_REF}..HEAD" in run["run"] - assert '--event "$GITHUB_EVENT_PATH"' in run["run"] + assert "origin/${BASE_REF}..HEAD" in run["run"] and '--event "$GITHUB_EVENT_PATH"' in run["run"] + # The gate runs the base branch's checker and lists, never the PR's copy. + assert 'git archive "origin/${BASE_REF}"' in run["run"] + assert 'python3 "$policy/scripts/check_commit_identities.py"' in run["run"] + assert '--hash-file "$policy/' in run["run"] + assert "python3 scripts/" not in run["run"] + archived = re.search(r'files="([^"]+)"', run["run"]).group(1).split() + assert {"scripts/check_commit_identities.py", "scripts/blocked_identity_hashes.txt", + ".github/scripts/agent_identities.py"} == set(archived) # ── AI agents ─────────────────────────────────────────────────────────── From 2179fb4cd5f3949a6aba93e08df5fd05db2d21c7 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 08:43:28 +0530 Subject: [PATCH 56/85] fix: close the sweep review gaps (stable ROCm docs, older-backend tiers, retired CLI flag, unused channel strings) --- CLAUDE.md | 2 +- backend/speech_client/__main__.py | 3 +++ deploy/dockerhub-overview.md | 2 +- docs/STRUCTURE.md | 4 +--- docs/electron-parity.md | 2 +- docs/install/docker.md | 6 +++--- docs/install/linux.md | 2 +- .../src/renderer/src/hooks/use-performance-profile.ts | 3 ++- electron/src/renderer/src/i18n/locales/ar.json | 7 +------ electron/src/renderer/src/i18n/locales/de.json | 7 +------ electron/src/renderer/src/i18n/locales/en.json | 7 +------ electron/src/renderer/src/i18n/locales/es.json | 7 +------ electron/src/renderer/src/i18n/locales/fr.json | 7 +------ electron/src/renderer/src/i18n/locales/hi.json | 7 +------ electron/src/renderer/src/i18n/locales/id.json | 7 +------ electron/src/renderer/src/i18n/locales/it.json | 7 +------ electron/src/renderer/src/i18n/locales/ja.json | 7 +------ electron/src/renderer/src/i18n/locales/ko.json | 7 +------ electron/src/renderer/src/i18n/locales/nl.json | 7 +------ electron/src/renderer/src/i18n/locales/pl.json | 7 +------ electron/src/renderer/src/i18n/locales/pt.json | 7 +------ electron/src/renderer/src/i18n/locales/ru.json | 7 +------ electron/src/renderer/src/i18n/locales/sv.json | 7 +------ electron/src/renderer/src/i18n/locales/th.json | 7 +------ electron/src/renderer/src/i18n/locales/tr.json | 7 +------ electron/src/renderer/src/i18n/locales/uk.json | 7 +------ electron/src/renderer/src/i18n/locales/vi.json | 7 +------ electron/src/renderer/src/i18n/locales/zh-CN.json | 7 +------ electron/src/renderer/src/i18n/locales/zh-TW.json | 7 +------ tests/test_docker_workflow.py | 9 +++++++++ 30 files changed, 43 insertions(+), 137 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bcb1a4674..36526f905 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ Everything else (new engines, fancy features) is downstream of "the thing instal - **Cross-platform parity**: Every fix must work on macOS (Apple Silicon; Intel Macs are limited to the app UI with a remote backend, as README.md and `docs/install/macos.md` document), Windows (x64), and Linux (AppImage + deb). No platform-only regressions; the cross-platform bug bash (PR #51) is the baseline. - **Default features must work on every platform (strict rule, 2026-05-20):** A feature that ships in default mode — out-of-the-box, no user customization, no opt-in toggle — must behave identically on macOS, Windows, and Linux. Platform-specific *implementation code* is allowed for OS APIs / shells / packaging, but the user-visible *default behavior* cannot diverge. Platform-only features (e.g., a macOS-only global shortcut, a Windows-only path picker) must go behind explicit user opt-in: Settings toggle, env var, or CLI flag. When a default doesn't work on a platform, that's a P0 bug — either fix it on the missing platform or move it behind opt-in. No third option. **This rule governs BEHAVIOUR, not PERFORMANCE** (clarified 2026-07-30, council): hardware acceleration is expected to vary by host — CUDA, MPS, DirectML, Triton availability and `torch.compile` are all host-dependent by design, and reading the rule to forbid that would forbid GPU support itself. An optimization that is skipped where it cannot work (missing Triton, an arch the wheel lacks, a path its toolchain cannot link) is NOT a parity violation; a *feature* the user can see and use on one OS but not another is. - **Backward-compatible project data**: Existing `omnivoice_data/` (user voices, projects, settings) must keep working without manual migration. Any DB schema change goes through alembic with a tested upgrade path. -- **Local-first guarantee preserved**: nothing leaves the machine without the user's **explicit yes**, and the app must remain fully functional with everything declined. Auto bug reporting is opt-in and submits only to GitHub Issues (prefilled-URL, from the user's own browser). Product analytics (owner-sanctioned 2026-07-16; US project migration 2026-09-26) is opt-in PostHog US with a **first-run consent prompt** — two equal-weight Yes/No buttons, never default-on, skipping = off; consent-gated, allowlisted content-free metadata only (`backend/core/analytics.py`); every build — installer, Docker, and source alike (owner reversal 2026-07-20, #1193) — carries the in-repo publishable write-only token and shows the same consent ask, with env/baked token overriding it. No required cloud calls, accounts, or API keys. Sanctioned calls (owner-set 2026-10-02; full list in `.github/CONTRIBUTING.md` → Quality gates): during first-run setup the user starts — the pinned ffmpeg download from GitHub `zackees/ffmpeg_bins` (`backend/services/media_tools.py`), the huggingface.co/hf-mirror.com reachability probe for restricted networks (`backend/services/endpoint_race.py`), and Hugging Face model downloads; opening the community gallery (jsDelivr manifest, `backend/api/routers/community.py`) or enabling the voice-preview gallery (GitHub Releases, `backend/services/gallery.py`); packaged update checks against GitHub Releases (`electron/src/main/updater.ts`); and yt-dlp updates from PyPI when the user clicks update. Narrow owner-approved exception (2026-09-28): the GitHub Star badge may fetch the public repository star count when shown and refresh every 20 minutes while visible, without credentials or a referrer. It sends no project, recording, or account data, retains the last count on failure, and never blocks offline use. +- **Local-first guarantee preserved**: nothing leaves the machine without the user's **explicit yes**, and the app must remain fully functional with everything declined. Auto bug reporting is opt-in and submits only to GitHub Issues (prefilled-URL, from the user's own browser). Product analytics (owner-sanctioned 2026-07-16; US project migration 2026-09-26) is opt-in PostHog US with a **first-run consent prompt** — two equal-weight Yes/No buttons, never default-on, skipping = off; consent-gated, allowlisted content-free metadata only (`backend/core/analytics.py`); every build — installer, Docker, and source alike (owner reversal 2026-07-20, #1193) — carries the in-repo publishable write-only token and shows the same consent ask, with env/baked token overriding it. No required cloud calls, accounts, or API keys. Sanctioned calls (owner-set 2026-10-02; full list in `.github/CONTRIBUTING.md` → Quality gates): during first-run setup the user starts — the pinned ffmpeg download from GitHub `zackees/ffmpeg_bins` (`backend/services/media_tools.py`), the huggingface.co/hf-mirror.com reachability probe for restricted networks (`backend/services/endpoint_race.py`), and Hugging Face model downloads; opening the community gallery (jsDelivr manifest, `backend/api/routers/community.py`) or enabling the voice-preview gallery (GitHub Releases, `backend/services/gallery.py`); packaged update checks against GitHub Releases (`electron/src/main/updater.ts`); yt-dlp updates from PyPI when the user clicks update; and the Lemon Squeezy Pro licence check after the user enters a key. Narrow owner-approved exception (2026-09-28): the GitHub Star badge may fetch the public repository star count when shown and refresh every 20 minutes while visible, without credentials or a referrer. It sends no project, recording, or account data, retains the last count on failure, and never blocks offline use. - **Beta release cadence (no RC, no ceremony — strict rule, 2026-05-20):** the current release line (0.5.x today) has **no release candidates, no 48h soak, no formal release ceremony**. Every fix goes continuous-to-main; the owner tags a patch from main whenever the current state is worth cutting. No `-rc` tags. No phased release. No deferrals to a future minor or major version — every open issue and every open community PR gets absorbed into the current line or explicitly declined. Users follow `main` for previews; users wanting stable stay on the latest tagged release. diff --git a/backend/speech_client/__main__.py b/backend/speech_client/__main__.py index b2099f55a..ff48542b7 100644 --- a/backend/speech_client/__main__.py +++ b/backend/speech_client/__main__.py @@ -213,6 +213,9 @@ def _parser() -> argparse.ArgumentParser: "--engine-url", default=_default_engine_url(), ) + # Accepted and ignored so hooks written for the retired control server get + # the explanation instead of an argparse error. + parser.add_argument("--control-url", help=argparse.SUPPRESS) subparsers = parser.add_subparsers(dest="command", required=True) subparsers.add_parser("capabilities", help="print the backend's speech capabilities") for command in RETIRED_CONTROL_COMMANDS: diff --git a/deploy/dockerhub-overview.md b/deploy/dockerhub-overview.md index 7e7971c34..f1c890278 100644 --- a/deploy/dockerhub-overview.md +++ b/deploy/dockerhub-overview.md @@ -99,7 +99,7 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - palashdeb/omnivoice-studio:rocm + palashdeb/omnivoice-studio:stable-rocm ``` Podman users: same two `--device` flags (Quadlet: `AddDevice=/dev/kfd` + diff --git a/docs/STRUCTURE.md b/docs/STRUCTURE.md index cd7126b25..7ba699f41 100644 --- a/docs/STRUCTURE.md +++ b/docs/STRUCTURE.md @@ -52,7 +52,6 @@ VoiceStudio/ │ ├── schemas/ pydantic request/response shapes │ ├── migrations/versions/ alembic revisions — every schema change goes through here │ ├── plugins/ plugin drop-in point (see services/plugin_sdk.py) -│ ├── hooks/ pyinstaller runtime hooks │ ├── config/models.yaml model catalogue │ └── tests/ the isolated pytest session — see "Where tests live" │ @@ -216,8 +215,7 @@ VoiceStudio/ │ ├── omnivoice-model/ ← was omnivoice/ │ └── tts-adapters/ ← new; the pluggable TTS interface from ROADMAP phase 3 ├── config/ -│ ├── docker/ -│ └── pyinstaller/ +│ └── docker/ ├── tests/ └── docs/ ``` diff --git a/docs/electron-parity.md b/docs/electron-parity.md index 7ee41a2ab..75c54a30b 100644 --- a/docs/electron-parity.md +++ b/docs/electron-parity.md @@ -24,7 +24,7 @@ same task and backend contract. Platform-specific performance may differ. A nati | Model catalogue and engine selection | Implemented | model-library component tests, recommendations smoke, live install/cancel/delete/unload verification, current in-process/isolated Faster-Whisper selection and restoration, and repeatable app-managed audio.cpp v0.7.4 Sortformer inference/cancellation on Windows Vulkan | Native install matrix across model families and desktop OSes | | Local and remote compute target | Implemented | worker API tests, compute-target UI, remote-target recommendation smoke, Ubuntu 26.04 WSL real TTS plus disconnect/reconnect fallback | Native-machine remote run outside WSL | | Settings, logs, API reference and support | Implemented | settings smoke tests, responsive live runtime/hardware identity, 640 px layout smoke, packaged Linux 150% DPR/no-overflow visual smoke and `docs/electron-logs.md` | Native visual capture on macOS | -| Updates, notifications, app/tray/taskbar branding | Implemented; published-update verification pending | updater IPC lifecycle/authorization regression, platform-specific feed names, bounded channel-aware release history, byte/SHA-512 release contract, release-matrix validation of each unpacked Electron app, branded Windows NSIS/app icon, launched AppImage and installed Debian package with shared brand assets/native helper | First signed Stable/Preview update and installed-shell visual check on macOS/Linux | +| Updates, notifications, app/tray/taskbar branding | Implemented; published-update verification pending | updater IPC lifecycle/authorization regression, platform-specific feed names, bounded channel-aware release history, byte/SHA-512 release contract, release-matrix validation of each unpacked Electron app, branded Windows NSIS/app icon, launched AppImage and installed Debian package with shared brand assets/native helper | First signed Stable update and installed-shell visual check on macOS/Linux | | Watch folders and global capture | Verification pending | Windows native watch lifecycle/global shortcut/insertion plus Linux AppImage Wayland tray/watch and clipboard-fallback smokes | macOS permissions, Linux portal global shortcut and physical microphone matrix | | T3-inspired shell, themes, scale and keyboard navigation | Implemented | shared resizable secondary panes, sidebar, locale-layout, workspace-shell and all-route accessibility smoke tests, including 640–1920 px secondary-pane geometry, 38 routes at 1440/640 px, packaged Linux 150% DPR and main-process empty-renderer recovery with a localized asset-independent fallback | Native screen-reader pass on macOS/Linux and high-DPI pass on macOS | | In-app agent repair dock | Electron extension | `docs/electron-repair.md`, native smoke, packaged Codex/Claude/OpenCode acceptance on Windows, and packaged Linux host-shim rejection | Pi acceptance when installed; native macOS/Linux CLI matrix | diff --git a/docs/install/docker.md b/docs/install/docker.md index f8d448a03..6f34a9413 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -118,7 +118,7 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - ghcr.io/debpalash/voicestudio:rocm + ghcr.io/debpalash/voicestudio:stable-rocm ``` ### AMD GPU on WSL2 @@ -141,7 +141,7 @@ docker run -d --name omnivoice \ -p 127.0.0.1:3900:3900 \ -e OMNIVOICE_API_KEY="$OMNIVOICE_API_KEY" \ -v omnivoice-data:/app/omnivoice_data \ - ghcr.io/debpalash/voicestudio:rocm + ghcr.io/debpalash/voicestudio:stable-rocm ``` The image currently uses ROCm 7.2.x, so `HSA_ENABLE_DXG_DETECTION=1` is @@ -184,7 +184,7 @@ The same flags work with **Podman** (`podman run --device /dev/kfd ```ini # ~/.config/containers/systemd/omnivoice.container [Container] -Image=ghcr.io/debpalash/voicestudio:rocm +Image=ghcr.io/debpalash/voicestudio:stable-rocm AddDevice=/dev/kfd AddDevice=/dev/dri PublishPort=127.0.0.1:3900:3900 diff --git a/docs/install/linux.md b/docs/install/linux.md index 33bffe4c7..f49d6bde8 100644 --- a/docs/install/linux.md +++ b/docs/install/linux.md @@ -250,7 +250,7 @@ an AMD-only machine `torch.cuda.is_available()` is `False` and VoiceStudio runs on CPU until you opt into the ROCm variant. > **Running in Docker or Podman instead?** There's a prebuilt ROCm image — -> `ghcr.io/debpalash/voicestudio:rocm` — with GPU acceleration out of the +> `ghcr.io/debpalash/voicestudio:stable-rocm` — with GPU acceleration out of the > box; see [docker.md](docker.md#pull-and-run-amd-gpu--rocm). The rest of this > section is about source/desktop installs. (VoiceStudio has no ROCm path on Windows; see [windows.md](windows.md#gpu-support) for what a Radeon card can diff --git a/electron/src/renderer/src/hooks/use-performance-profile.ts b/electron/src/renderer/src/hooks/use-performance-profile.ts index 25fe73547..f9e33ad71 100644 --- a/electron/src/renderer/src/hooks/use-performance-profile.ts +++ b/electron/src/renderer/src/hooks/use-performance-profile.ts @@ -96,7 +96,8 @@ export function usePerformanceProfile() { }); const target = state.targets.tts; if ( - state.tts_tiered_engines?.includes(state.selections.tts.engine) && + // Backends before 0.5.7 don't report the list; they tier only OmniVoice. + (state.tts_tiered_engines ?? ['omnivoice', 'omnivoice-subprocess']).includes(state.selections.tts.engine) && typeof target.steps === 'number' && typeof target.postprocess === 'boolean' ) { diff --git a/electron/src/renderer/src/i18n/locales/ar.json b/electron/src/renderer/src/i18n/locales/ar.json index 8f6cf32ef..d01330063 100644 --- a/electron/src/renderer/src/i18n/locales/ar.json +++ b/electron/src/renderer/src/i18n/locales/ar.json @@ -1985,11 +1985,7 @@ "save_bundle": "حفظ حزمة التشخيص", "bundle_saved": "تم حفظ حزمة التشخيص: {{filename}}", "title": "حول", - "diagnostics": "الاختبار المبدئي للنظام", - "update_channel": "قناة التحديث", - "channel_stable": "مستقر", - "channel_preview": "معاينة", - "channel_preview_hint": "تتابع المعاينة أحدث إصدار من main — ميزات أحدث وأقل اختبارًا. تعود إلى المستقر إذا كان هناك إصدار مستقر أحدث." + "diagnostics": "الاختبار المبدئي للنظام" }, "privacy": { "translator_online": "المترجم متصل: {{provider}}", @@ -2006,7 +2002,6 @@ "check_now": "تحقق الآن", "releases": "الإصدارات", "current": "الحالي", - "prerelease": "معاينة", "loading": "جارٍ تحميل الإصدارات…", "none": "لم يتم العثور على أي إصدارات", "load_error": "تعذر تحميل الإصدارات (غير متصل؟)", diff --git a/electron/src/renderer/src/i18n/locales/de.json b/electron/src/renderer/src/i18n/locales/de.json index c64660b1a..48ee6a34d 100644 --- a/electron/src/renderer/src/i18n/locales/de.json +++ b/electron/src/renderer/src/i18n/locales/de.json @@ -1977,11 +1977,7 @@ "save_bundle": "Diagnosepaket speichern", "bundle_saved": "Diagnosepaket gespeichert: {{filename}}", "title": "Über", - "diagnostics": "System-Preflight", - "update_channel": "Update-Kanal", - "channel_stable": "Stabil", - "channel_preview": "Vorschau", - "channel_preview_hint": "Die Vorschau folgt dem neuesten main-Build – neuere Funktionen, weniger getestet. Wechselt zurück zu Stabil, wenn eine stabile Version voraus ist." + "diagnostics": "System-Preflight" }, "privacy": { "translator_online": "Übersetzer ist online: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "Jetzt prüfen", "releases": "Veröffentlichungen", "current": "aktuell", - "prerelease": "Vorschau", "loading": "Veröffentlichungen werden geladen…", "none": "Keine Veröffentlichungen gefunden", "load_error": "Veröffentlichungen konnten nicht geladen werden (offline?)", diff --git a/electron/src/renderer/src/i18n/locales/en.json b/electron/src/renderer/src/i18n/locales/en.json index 2f78ae7d3..7eca141b1 100644 --- a/electron/src/renderer/src/i18n/locales/en.json +++ b/electron/src/renderer/src/i18n/locales/en.json @@ -2080,11 +2080,7 @@ "save_bundle": "Save diagnostic bundle", "bundle_saved": "Diagnostic bundle saved: {{filename}}", "title": "About", - "diagnostics": "System preflight", - "update_channel": "Update channel", - "channel_stable": "Stable", - "channel_preview": "Preview", - "channel_preview_hint": "Preview tracks the latest main build — newer features, less tested. Falls back to stable if a stable release is ahead." + "diagnostics": "System preflight" }, "privacy": { "translator_online": "Translator is online: {{provider}}", @@ -2101,7 +2097,6 @@ "check_now": "Check now", "releases": "Releases", "current": "current", - "prerelease": "preview", "loading": "Loading releases…", "none": "No releases found", "load_error": "Couldn't load releases (offline?)", diff --git a/electron/src/renderer/src/i18n/locales/es.json b/electron/src/renderer/src/i18n/locales/es.json index 2c25ed87f..a740ae878 100644 --- a/electron/src/renderer/src/i18n/locales/es.json +++ b/electron/src/renderer/src/i18n/locales/es.json @@ -1979,11 +1979,7 @@ "save_bundle": "Guardar paquete de diagnóstico", "bundle_saved": "Paquete de diagnóstico guardado: {{filename}}", "title": "Acerca de", - "diagnostics": "verificación previa del sistema", - "update_channel": "Canal de actualización", - "channel_stable": "Estable", - "channel_preview": "Vista previa", - "channel_preview_hint": "La vista previa sigue la última compilación de main: funciones más nuevas, menos probadas. Vuelve a estable si hay una versión estable más reciente." + "diagnostics": "verificación previa del sistema" }, "privacy": { "translator_online": "El traductor está en línea: {{provider}}", @@ -2000,7 +1996,6 @@ "check_now": "Compruébalo ahora", "releases": "Lanzamientos", "current": "actual", - "prerelease": "vista previa", "loading": "Cargando lanzamientos…", "none": "No se encontraron lanzamientos", "load_error": "No se pudieron cargar las versiones (¿sin conexión?)", diff --git a/electron/src/renderer/src/i18n/locales/fr.json b/electron/src/renderer/src/i18n/locales/fr.json index cedec259d..80ae91a88 100644 --- a/electron/src/renderer/src/i18n/locales/fr.json +++ b/electron/src/renderer/src/i18n/locales/fr.json @@ -1979,11 +1979,7 @@ "save_bundle": "Enregistrer le paquet de diagnostic", "bundle_saved": "Paquet de diagnostic enregistré : {{filename}}", "title": "À propos", - "diagnostics": "Contrôle en amont du système", - "update_channel": "Canal de mise à jour", - "channel_stable": "Stable", - "channel_preview": "Aperçu", - "channel_preview_hint": "L'aperçu suit la dernière version de main : fonctionnalités plus récentes, moins testées. Revient à la version stable si une version stable est plus avancée." + "diagnostics": "Contrôle en amont du système" }, "privacy": { "translator_online": "Le traducteur est en ligne : {{provider}}", @@ -2000,7 +1996,6 @@ "check_now": "Vérifiez maintenant", "releases": "Versions", "current": "actuelle", - "prerelease": "aperçu", "loading": "Chargement des versions…", "none": "Aucune version trouvée", "load_error": "Impossible de charger les versions (hors ligne ?)", diff --git a/electron/src/renderer/src/i18n/locales/hi.json b/electron/src/renderer/src/i18n/locales/hi.json index 23c4eb13e..ee7e1e2d9 100644 --- a/electron/src/renderer/src/i18n/locales/hi.json +++ b/electron/src/renderer/src/i18n/locales/hi.json @@ -1977,11 +1977,7 @@ "save_bundle": "डायग्नोस्टिक बंडल सहेजें", "bundle_saved": "डायग्नोस्टिक बंडल सहेजा गया: {{filename}}", "title": "विवरण", - "diagnostics": "सिस्टम प्रीफ्लाइट", - "update_channel": "अपडेट चैनल", - "channel_stable": "स्थिर", - "channel_preview": "प्रीव्यू", - "channel_preview_hint": "प्रीव्यू नवीनतम main बिल्ड को ट्रैक करता है — नई सुविधाएँ, कम परीक्षण। यदि कोई स्थिर रिलीज़ आगे है तो स्थिर पर वापस आ जाता है।" + "diagnostics": "सिस्टम प्रीफ्लाइट" }, "privacy": { "translator_online": "अनुवादक ऑनलाइन है: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "अभी जांचें", "releases": "रिलीज़", "current": "वर्तमान", - "prerelease": "पूर्वावलोकन", "loading": "रिलीज़ लोड हो रही हैं...", "none": "कोई रिलीज़ नहीं मिली", "load_error": "रिलीज़ लोड नहीं हो सकी (ऑफ़लाइन?)", diff --git a/electron/src/renderer/src/i18n/locales/id.json b/electron/src/renderer/src/i18n/locales/id.json index 05ec82d20..f8e2c96d9 100644 --- a/electron/src/renderer/src/i18n/locales/id.json +++ b/electron/src/renderer/src/i18n/locales/id.json @@ -1977,11 +1977,7 @@ "save_bundle": "Simpan bundel diagnostik", "bundle_saved": "Bundel diagnostik tersimpan: {{filename}}", "title": "Tentang", - "diagnostics": "Pra-penerbangan sistem", - "update_channel": "Saluran pembaruan", - "channel_stable": "Stabil", - "channel_preview": "Pratinjau", - "channel_preview_hint": "Pratinjau mengikuti build main terbaru — fitur lebih baru, lebih sedikit diuji. Kembali ke stabil jika ada rilis stabil yang lebih baru." + "diagnostics": "Pra-penerbangan sistem" }, "privacy": { "translator_online": "Penerjemah sedang online: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "Periksa sekarang", "releases": "Rilis", "current": "saat ini", - "prerelease": "pratinjau", "loading": "Memuat rilis…", "none": "Tidak ada rilis yang ditemukan", "load_error": "Tidak dapat memuat rilis (offline?)", diff --git a/electron/src/renderer/src/i18n/locales/it.json b/electron/src/renderer/src/i18n/locales/it.json index b3bc76c71..04676a7e3 100644 --- a/electron/src/renderer/src/i18n/locales/it.json +++ b/electron/src/renderer/src/i18n/locales/it.json @@ -1979,11 +1979,7 @@ "save_bundle": "Salva pacchetto diagnostico", "bundle_saved": "Pacchetto diagnostico salvato: {{filename}}", "title": "Informazioni", - "diagnostics": "Verifica preliminare del sistema", - "update_channel": "Canale di aggiornamento", - "channel_stable": "Stabile", - "channel_preview": "Anteprima", - "channel_preview_hint": "L'anteprima segue l'ultima build di main: funzionalità più recenti, meno testate. Torna alla versione stabile se una release stabile è più avanti." + "diagnostics": "Verifica preliminare del sistema" }, "privacy": { "translator_online": "Il traduttore è online: {{provider}}", @@ -2000,7 +1996,6 @@ "check_now": "Controlla ora", "releases": "Rilasci", "current": "corrente", - "prerelease": "anteprima", "loading": "Caricamento versioni…", "none": "Nessuna versione trovata", "load_error": "Impossibile caricare le versioni (offline?)", diff --git a/electron/src/renderer/src/i18n/locales/ja.json b/electron/src/renderer/src/i18n/locales/ja.json index cf69f1fb6..a47e38a98 100644 --- a/electron/src/renderer/src/i18n/locales/ja.json +++ b/electron/src/renderer/src/i18n/locales/ja.json @@ -1977,11 +1977,7 @@ "save_bundle": "診断バンドルを保存", "bundle_saved": "診断バンドルを保存しました: {{filename}}", "title": "情報", - "diagnostics": "システムプリフライト", - "update_channel": "アップデートチャンネル", - "channel_stable": "安定版", - "channel_preview": "プレビュー", - "channel_preview_hint": "プレビューは最新の main ビルドを追跡します。新機能をいち早く利用できますが、テストは少なめです。安定版が先行している場合は安定版にフォールバックします。" + "diagnostics": "システムプリフライト" }, "privacy": { "translator_online": "翻訳者はオンラインです: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "今すぐチェック", "releases": "リリース", "current": "現在", - "prerelease": "プレビュー", "loading": "リリースを読み込み中…", "none": "リリースが見つかりませんでした", "load_error": "リリースをロードできませんでした (オフライン?)", diff --git a/electron/src/renderer/src/i18n/locales/ko.json b/electron/src/renderer/src/i18n/locales/ko.json index 7689b4730..4ca6b3520 100644 --- a/electron/src/renderer/src/i18n/locales/ko.json +++ b/electron/src/renderer/src/i18n/locales/ko.json @@ -1977,11 +1977,7 @@ "save_bundle": "진단 번들 저장", "bundle_saved": "진단 번들 저장됨: {{filename}}", "title": "정보", - "diagnostics": "시스템 프리플라이트", - "update_channel": "업데이트 채널", - "channel_stable": "안정 버전", - "channel_preview": "미리 보기", - "channel_preview_hint": "미리 보기는 최신 main 빌드를 따릅니다. 새로운 기능을 빨리 사용할 수 있지만 테스트는 적습니다. 안정 버전이 더 최신이면 안정 버전으로 대체됩니다." + "diagnostics": "시스템 프리플라이트" }, "privacy": { "translator_online": "번역가가 온라인 상태입니다: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "지금 확인하세요", "releases": "릴리스", "current": "현재", - "prerelease": "미리보기", "loading": "릴리스 로드 중…", "none": "릴리스를 찾을 수 없습니다.", "load_error": "릴리스를 로드할 수 없습니다(오프라인?)", diff --git a/electron/src/renderer/src/i18n/locales/nl.json b/electron/src/renderer/src/i18n/locales/nl.json index d68c2304e..70aa02371 100644 --- a/electron/src/renderer/src/i18n/locales/nl.json +++ b/electron/src/renderer/src/i18n/locales/nl.json @@ -1977,11 +1977,7 @@ "save_bundle": "Diagnosepakket opslaan", "bundle_saved": "Diagnosepakket opgeslagen: {{filename}}", "title": "Over", - "diagnostics": "Systeem preflight", - "update_channel": "Updatekanaal", - "channel_stable": "Stabiel", - "channel_preview": "Voorbeeld", - "channel_preview_hint": "Voorbeeld volgt de nieuwste main-build — nieuwere functies, minder getest. Valt terug op stabiel als er een nieuwere stabiele release is." + "diagnostics": "Systeem preflight" }, "privacy": { "translator_online": "Vertaler is online: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "Controleer nu", "releases": "Releases", "current": "actueel", - "prerelease": "voorbeeld", "loading": "Releases laden…", "none": "Geen releases gevonden", "load_error": "Kan releases niet laden (offline?)", diff --git a/electron/src/renderer/src/i18n/locales/pl.json b/electron/src/renderer/src/i18n/locales/pl.json index c033ba0ca..a4ec136ee 100644 --- a/electron/src/renderer/src/i18n/locales/pl.json +++ b/electron/src/renderer/src/i18n/locales/pl.json @@ -1981,11 +1981,7 @@ "save_bundle": "Zapisz pakiet diagnostyczny", "bundle_saved": "Pakiet diagnostyczny zapisany: {{filename}}", "title": "O programie", - "diagnostics": "Wstępna inspekcja systemu", - "update_channel": "Kanał aktualizacji", - "channel_stable": "Stabilny", - "channel_preview": "Podgląd", - "channel_preview_hint": "Podgląd śledzi najnowszą kompilację main — nowsze funkcje, mniej testów. Wraca do stabilnej, jeśli stabilne wydanie jest nowsze." + "diagnostics": "Wstępna inspekcja systemu" }, "privacy": { "translator_online": "Tłumacz jest online: {{provider}}", @@ -2002,7 +1998,6 @@ "check_now": "Sprawdź teraz", "releases": "Wydania", "current": "aktualna", - "prerelease": "podgląd", "loading": "Ładowanie wersji…", "none": "Nie znaleziono żadnych wydań", "load_error": "Nie można wczytać wersji (offline?)", diff --git a/electron/src/renderer/src/i18n/locales/pt.json b/electron/src/renderer/src/i18n/locales/pt.json index 106ef84a2..23ec49155 100644 --- a/electron/src/renderer/src/i18n/locales/pt.json +++ b/electron/src/renderer/src/i18n/locales/pt.json @@ -1979,11 +1979,7 @@ "save_bundle": "Salvar pacote de diagnóstico", "bundle_saved": "Pacote de diagnóstico salvo: {{filename}}", "title": "Sobre", - "diagnostics": "Comprovação do sistema", - "update_channel": "Canal de atualização", - "channel_stable": "Estável", - "channel_preview": "Prévia", - "channel_preview_hint": "A prévia acompanha a compilação mais recente da main — recursos mais novos, menos testados. Volta para a estável se uma versão estável estiver à frente." + "diagnostics": "Comprovação do sistema" }, "privacy": { "translator_online": "O tradutor está online: {{provider}}", @@ -2000,7 +1996,6 @@ "check_now": "Verifique agora", "releases": "Lançamentos", "current": "atual", - "prerelease": "visualização", "loading": "Carregando lançamentos…", "none": "Nenhum lançamento encontrado", "load_error": "Não foi possível carregar as versões (off-line?)", diff --git a/electron/src/renderer/src/i18n/locales/ru.json b/electron/src/renderer/src/i18n/locales/ru.json index 2f732fce7..632cf8548 100644 --- a/electron/src/renderer/src/i18n/locales/ru.json +++ b/electron/src/renderer/src/i18n/locales/ru.json @@ -1981,11 +1981,7 @@ "save_bundle": "Сохранить диагностический пакет", "bundle_saved": "Диагностический пакет сохранён: {{filename}}", "title": "О программе", - "diagnostics": "Предполетная подготовка системы", - "update_channel": "Канал обновлений", - "channel_stable": "Стабильный", - "channel_preview": "Предварительный", - "channel_preview_hint": "Предварительный канал отслеживает последнюю сборку main — новые функции, меньше тестирования. Возвращается к стабильному, если стабильный выпуск новее." + "diagnostics": "Предполетная подготовка системы" }, "privacy": { "translator_online": "Переводчик онлайн: {{provider}}", @@ -2002,7 +1998,6 @@ "check_now": "Проверьте сейчас", "releases": "Релизы", "current": "текущий", - "prerelease": "предварительный просмотр", "loading": "Загрузка выпусков…", "none": "Релизов не найдено", "load_error": "Не удалось загрузить выпуски (офлайн?)", diff --git a/electron/src/renderer/src/i18n/locales/sv.json b/electron/src/renderer/src/i18n/locales/sv.json index 8a2245e52..ff1260428 100644 --- a/electron/src/renderer/src/i18n/locales/sv.json +++ b/electron/src/renderer/src/i18n/locales/sv.json @@ -1977,11 +1977,7 @@ "save_bundle": "Spara diagnostikpaket", "bundle_saved": "Diagnostikpaket sparat: {{filename}}", "title": "Om", - "diagnostics": "System preflight", - "update_channel": "Uppdateringskanal", - "channel_stable": "Stabil", - "channel_preview": "Förhandsvisning", - "channel_preview_hint": "Förhandsvisning följer det senaste main-bygget – nyare funktioner, mindre testade. Återgår till stabil om en stabil version ligger före." + "diagnostics": "System preflight" }, "privacy": { "translator_online": "Översättaren är online: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "Kolla nu", "releases": "Utgåvor", "current": "nuvarande", - "prerelease": "förhandsgranskning", "loading": "Läser in utgåvor...", "none": "Inga utgåvor hittades", "load_error": "Det gick inte att läsa in utgåvor (offline?)", diff --git a/electron/src/renderer/src/i18n/locales/th.json b/electron/src/renderer/src/i18n/locales/th.json index ef792a905..a057cffe2 100644 --- a/electron/src/renderer/src/i18n/locales/th.json +++ b/electron/src/renderer/src/i18n/locales/th.json @@ -1977,11 +1977,7 @@ "save_bundle": "บันทึกชุดวินิจฉัย", "bundle_saved": "บันทึกชุดวินิจฉัยแล้ว: {{filename}}", "title": "เกี่ยวกับ", - "diagnostics": "ระบบพรีไฟลท์", - "update_channel": "ช่องอัปเดต", - "channel_stable": "เสถียร", - "channel_preview": "พรีวิว", - "channel_preview_hint": "พรีวิวจะติดตามบิลด์ main ล่าสุด — ฟีเจอร์ใหม่กว่า แต่ทดสอบน้อยกว่า จะกลับไปใช้เวอร์ชันเสถียรหากมีรุ่นเสถียรที่ใหม่กว่า" + "diagnostics": "ระบบพรีไฟลท์" }, "privacy": { "translator_online": "นักแปลออนไลน์อยู่: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "ตรวจสอบตอนนี้", "releases": "ประวัติการออกเวอร์ชัน", "current": "ปัจจุบัน", - "prerelease": "ดูตัวอย่าง", "loading": "กำลังโหลดรุ่น...", "none": "ไม่พบการเผยแพร่", "load_error": "ไม่สามารถโหลดรุ่นต่างๆ (ออฟไลน์?)", diff --git a/electron/src/renderer/src/i18n/locales/tr.json b/electron/src/renderer/src/i18n/locales/tr.json index 10cfe28a6..12a05c690 100644 --- a/electron/src/renderer/src/i18n/locales/tr.json +++ b/electron/src/renderer/src/i18n/locales/tr.json @@ -1977,11 +1977,7 @@ "save_bundle": "Tanılama paketini kaydet", "bundle_saved": "Tanılama paketi kaydedildi: {{filename}}", "title": "Hakkında", - "diagnostics": "Sistem ön kontrolü", - "update_channel": "Güncelleme kanalı", - "channel_stable": "Kararlı", - "channel_preview": "Önizleme", - "channel_preview_hint": "Önizleme en son main yapısını izler — daha yeni özellikler, daha az test. Daha yeni bir kararlı sürüm varsa kararlıya geri döner." + "diagnostics": "Sistem ön kontrolü" }, "privacy": { "translator_online": "Çevirmen çevrimiçi: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "Şimdi kontrol et", "releases": "Sürümler", "current": "geçerli", - "prerelease": "önizleme", "loading": "Sürümler yükleniyor…", "none": "Yayın bulunamadı", "load_error": "Sürümler yüklenemedi (çevrimdışı mı?)", diff --git a/electron/src/renderer/src/i18n/locales/uk.json b/electron/src/renderer/src/i18n/locales/uk.json index 747635fe6..08ce5041e 100644 --- a/electron/src/renderer/src/i18n/locales/uk.json +++ b/electron/src/renderer/src/i18n/locales/uk.json @@ -1981,11 +1981,7 @@ "save_bundle": "Зберегти діагностичний пакет", "bundle_saved": "Діагностичний пакет збережено: {{filename}}", "title": "Про програму", - "diagnostics": "Система передполіт", - "update_channel": "Канал оновлень", - "channel_stable": "Стабільний", - "channel_preview": "Попередній перегляд", - "channel_preview_hint": "Попередній перегляд відстежує найновішу збірку main — новіші функції, менше тестування. Повертається до стабільного, якщо стабільний випуск новіший." + "diagnostics": "Система передполіт" }, "privacy": { "translator_online": "Перекладач онлайн: {{provider}}", @@ -2002,7 +1998,6 @@ "check_now": "Перевір зараз", "releases": "Релізи", "current": "поточний", - "prerelease": "попередній перегляд", "loading": "Завантаження випусків…", "none": "Випусків не знайдено", "load_error": "Не вдалося завантажити випуски (офлайн?)", diff --git a/electron/src/renderer/src/i18n/locales/vi.json b/electron/src/renderer/src/i18n/locales/vi.json index 467a0cfd3..51534a0ec 100644 --- a/electron/src/renderer/src/i18n/locales/vi.json +++ b/electron/src/renderer/src/i18n/locales/vi.json @@ -1977,11 +1977,7 @@ "save_bundle": "Lưu gói chẩn đoán", "bundle_saved": "Đã lưu gói chẩn đoán: {{filename}}", "title": "Giới thiệu", - "diagnostics": "Đèn chiếu trước hệ thống", - "update_channel": "Kênh cập nhật", - "channel_stable": "Ổn định", - "channel_preview": "Xem trước", - "channel_preview_hint": "Bản xem trước theo dõi bản dựng main mới nhất — tính năng mới hơn, ít được kiểm thử hơn. Quay lại bản ổn định nếu có bản phát hành ổn định mới hơn." + "diagnostics": "Đèn chiếu trước hệ thống" }, "privacy": { "translator_online": "Người dịch đang trực tuyến: {{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "Kiểm tra ngay", "releases": "Bản phát hành", "current": "hiện tại", - "prerelease": "xem trước", "loading": "Đang tải bản phát hành…", "none": "Không tìm thấy bản phát hành nào", "load_error": "Không thể tải bản phát hành (ngoại tuyến?)", diff --git a/electron/src/renderer/src/i18n/locales/zh-CN.json b/electron/src/renderer/src/i18n/locales/zh-CN.json index 60d706c09..0986d1ef1 100644 --- a/electron/src/renderer/src/i18n/locales/zh-CN.json +++ b/electron/src/renderer/src/i18n/locales/zh-CN.json @@ -1977,11 +1977,7 @@ "save_bundle": "保存诊断包", "bundle_saved": "诊断包已保存:{{filename}}", "title": "关于", - "diagnostics": "系统预检", - "update_channel": "更新通道", - "channel_stable": "稳定版", - "channel_preview": "预览版", - "channel_preview_hint": "预览版跟踪最新的 main 构建——功能更新,但测试较少。若稳定版更新,则回退到稳定版。" + "diagnostics": "系统预检" }, "privacy": { "translator_online": "翻译引擎在线:{{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "立即查看", "releases": "发布", "current": "当前", - "prerelease": "预览", "loading": "正在加载版本...", "none": "未找到任何版本", "load_error": "无法加载版本(离线?)", diff --git a/electron/src/renderer/src/i18n/locales/zh-TW.json b/electron/src/renderer/src/i18n/locales/zh-TW.json index 12769c1ce..4078a3b19 100644 --- a/electron/src/renderer/src/i18n/locales/zh-TW.json +++ b/electron/src/renderer/src/i18n/locales/zh-TW.json @@ -1977,11 +1977,7 @@ "save_bundle": "儲存診斷套件", "bundle_saved": "診斷套件已儲存:{{filename}}", "title": "關於軟體", - "diagnostics": "系統預檢", - "update_channel": "更新通道", - "channel_stable": "穩定版", - "channel_preview": "預覽版", - "channel_preview_hint": "預覽版追蹤最新的 main 組建——功能較新,但測試較少。若穩定版較新,則回退至穩定版。" + "diagnostics": "系統預檢" }, "privacy": { "translator_online": "譯者在線:{{provider}}", @@ -1998,7 +1994,6 @@ "check_now": "立即檢查", "releases": "發布", "current": "目前", - "prerelease": "預覽", "loading": "正在載入版本...", "none": "未找到任何版本", "load_error": "無法載入版本(離線?)", diff --git a/tests/test_docker_workflow.py b/tests/test_docker_workflow.py index a274ef09e..898e106fa 100644 --- a/tests/test_docker_workflow.py +++ b/tests/test_docker_workflow.py @@ -7,6 +7,8 @@ """ from __future__ import annotations +import re + from pathlib import Path import yaml @@ -76,3 +78,10 @@ def test_token_published_releases_still_publish_images(): assert "gh workflow run docker.yml" in runs assert "backfill_release=true" in runs assert "promote_stable=true" in runs + # Every -f key must be a real docker.yml dispatch input, or the run fails at publish time. + command = runs[runs.index("gh workflow run docker.yml"):] + command = command[: command.find("\n", command.find("promote_stable"))] + keys = set(re.findall(r"-f\s+([A-Za-z_]+)=", command)) + on = _load(_DOCKER)["on"] + inputs = set((on.get("workflow_dispatch") or {}).get("inputs") or {}) + assert {"backfill_release", "release_ref", "promote_stable"} <= keys <= inputs, (keys, inputs) From 744bd6cfbb00169325d9cc3852effd190be892c8 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:13:59 +0530 Subject: [PATCH 57/85] docs(changelog): record the consistency sweep --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a15681828..993838484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,13 @@ metadata and the backend fallback mirror it. - Footer sponsor hover details show dated GitHub reach, and its X opens the Pro page (#2302) - Linux AppImages use the static runtime so launching them no longer needs libfuse2 (#2328) — thanks @shuvashish76! - Voice Clone shows upload and record side by side instead of behind a toggle (#2307) +- Removed the Preview update channel, which never had a published feed; a saved Preview choice moves to Stable (#2578) +- Docker Compose and the Docker quick starts pull the `:stable` and `:stable-rocm` release images (#2578) +- `OMNIVOICE_UI_PORT` is the one UI-port setting (`VOICESTUDIO_UI_PORT` still works), and ports are no longer saved to settings (#2578) +- `OMNIVOICE_PROGRESS_EXTENSION_CAP_S` replaces `OMNIVOICE_MODEL_LOAD_TIMEOUT_S`, which is still accepted (#2578) +- The backend no longer accepts the retired Tauri app's web origins, and installs no longer pull PyInstaller (#2578) +- `backend.speech_client` dictation control commands, which drove the retired Tauri app, now exit with an explanation (#2578) +- Removed three hidden settings that nothing could set; the `OMNIVOICE_PRONUNCIATION` and `OMNIVOICE_TEXT_NORMALIZATION` switches remain (#2578) ### Docs - Maintainer guide for repository settings that can't live in code; the licence notice scope and the contributing guide's list of network calls match the current app (#2556) @@ -88,6 +95,10 @@ metadata and the backend fallback mirror it. - New call agent guide covering setup, disclosure, recording consent and safeguards (#2306) - The Twilio guide and integration directory describe the guided setup and in-app integration pages (#2304) +- Install guides cover the Electron app: real download names, Linux AppImage and `.deb`, current source builds and uninstall folders (#2578) +- Docker docs: the model-cache mount that works, what each image tag means, and opt-in analytics (#2578) +- Security, contributing, release, structure and update-channel docs match the current app; the security policy supports the latest 0.5.x (#2578) +- License notice: commercial use is free under the AGPL; the paid licence is for closed-source use, with Pro plans linked (#2578) ### Fixed - Keep audiobook chapter boundaries when importing CR-only manuscripts (#2508) — thanks @rudycelekli! @@ -152,10 +163,22 @@ metadata and the backend fallback mirror it. - A reference longer than 20 s is transcribed with the speech-to-text model already installed, instead of failing when OmniVoice's own Whisper snapshot is not cached (#2301) — thanks @Cengokill! - Pronunciation dictionary entries in Japanese, Chinese and Thai apply inside a sentence, not only to a line that is the key alone (#2392) — thanks @kevin9327! +- Speed/quality tiers apply to the crash-isolated OmniVoice engine too (#2578) +- A browser UI on another origin receives each take's id, seed, timing and routing details (#2578) +- Long or queued generations no longer show a failure after 21 minutes while the backend is still working (#2578) +- The desktop backend starts on Windows when the user profile path has non-English characters (#1783, #2578) +- Uninstall scripts remove the Electron app's runtime, logs and updater cache (#2578) +- The in-app Docker instructions reuse your model cache at the path the image reads (#2578) +- Recording durations show their unit again in Japanese, Korean, Polish, Thai and Vietnamese (#2578) +- `bun run dev:api` binds to loopback by default and follows `OMNIVOICE_PORT` (#2578) +- New bug and install reports land in the `needs-triage` queue (#2578) ### CI - Pull requests fail when a commit uses a leaked personal email, a placeholder identity, or an AI agent identity, or when a commit or the description credits an AI agent (#2556) - GitHub Actions are pinned to exact commits and kept current by Dependabot (#2556) +- Release Docker images publish when the GitHub Release is published; `:latest` only ever comes from `main` (#2578) +- One bun, TypeScript and Playwright version and one lockfile across the workspace; frontend unit tests run in the required check (#2578) +- Tests keep the review-bot configs, issue-template labels and locale catalogs consistent (#2578) ## [0.5.6] — 2026-09-23 From 9f2f07918abd26fd32b32d2e2d6e05b3d61ff2e8 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:48:34 +0530 Subject: [PATCH 58/85] docs(macos): keep the Intel support statement the smoke job checks, with a test on every platform --- docs/install/macos.md | 3 ++- tests/test_macos_intel_docs.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/test_macos_intel_docs.py diff --git a/docs/install/macos.md b/docs/install/macos.md index bddf0acda..d81dcf6b8 100644 --- a/docs/install/macos.md +++ b/docs/install/macos.md @@ -5,7 +5,8 @@ is no longer maintained; if you still run it, follow the [migration guide](../electron-migration.md). > [!IMPORTANT] -> **Intel Macs: app UI only.** PyTorch stopped shipping Intel-Mac (macOS +> **Intel Macs are not supported** for the local backend (app UI only). +> PyTorch stopped shipping Intel-Mac (macOS > x86_64) wheels after 2.2.x and VoiceStudio's dependencies need a newer torch, > so a local Python backend cannot install on an Intel Mac — from the DMG *or* > from source ([#889](https://github.com/debpalash/VoiceStudio/issues/889)). diff --git a/tests/test_macos_intel_docs.py b/tests/test_macos_intel_docs.py new file mode 100644 index 000000000..f9ba75cdc --- /dev/null +++ b/tests/test_macos_intel_docs.py @@ -0,0 +1,17 @@ +"""The macOS install guide must keep the Intel support statement. + +The macOS Intel smoke job in .github/workflows/ci.yml asserts this sentence, +but only on that runner; this keeps the contract visible on every platform. +""" +from pathlib import Path + +_ROOT = Path(__file__).resolve().parents[1] +_PHRASE = "Intel Macs are not supported" + + +def test_macos_guide_states_intel_support(): + assert _PHRASE in (_ROOT / "docs" / "install" / "macos.md").read_text(encoding="utf-8") + + +def test_smoke_job_checks_the_same_sentence(): + assert f'"{_PHRASE}"' in (_ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8") From 3eb842227a3df55e3c3bf3a53f762b419255f862 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:52:27 +0530 Subject: [PATCH 59/85] fix(uninstall): accept only the exact runtime record the app writes, for a folder holding its project --- scripts/uninstall.ps1 | 5 +++-- scripts/uninstall.sh | 14 +++++++++----- tests/scripts/test_uninstall_locations.py | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index a8c7ab09f..d223089c8 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -104,7 +104,7 @@ $logsDefault = Join-Path (Join-Path $localApp 'OmniVoice') 'Logs' # A custom runtime location the Electron app created (and therefore owns) is # recorded in runtime-location.json. Only an owned, absolute folder named -# VoiceStudio is removed — the same rule the in-app uninstall applies; a reused +# VoiceStudio that holds the app's project is removed — the same rule the in-app uninstall applies; a reused # Tauri environment is recorded unowned and kept. $electronRuntime = $null try { @@ -114,7 +114,8 @@ try { $root = [string]$location.root if ($location.owned -eq $true -and [System.IO.Path]::IsPathRooted($root) -and (Split-Path $root -Leaf) -eq 'VoiceStudio' -and - $root.TrimEnd('\') -ne (Join-Path $electronUserData 'runtime')) { + $root.TrimEnd('\') -ne (Join-Path $electronUserData 'runtime') -and + (Test-Path -LiteralPath (Join-Path $root 'project') -PathType Container)) { $electronRuntime = $root } } diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 24b28f2a4..18c066c09 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -98,16 +98,20 @@ USER_ENV_DIR="$HOME/.config/omnivoice" # A custom runtime location the Electron app created (and therefore owns) is # recorded in runtime-location.json. Only an owned, absolute folder named -# VoiceStudio is removed — the same rule the in-app uninstall applies; a reused -# Tauri environment is recorded unowned and kept. +# VoiceStudio that holds the app's project is removed — the same rule the +# in-app uninstall applies; a reused Tauri environment is recorded unowned and +# kept. The file must be exactly the object the app writes +# ({"root": "...", "owned": true}); anything else is ignored, so a damaged or +# hand-edited file can never point the deletion at another folder. electron_owned_runtime() { - local file="$electron_user_data/runtime-location.json" root + local file="$electron_user_data/runtime-location.json" content root [ -f "$file" ] || return 0 - grep -q '"owned"[[:space:]]*:[[:space:]]*true' "$file" 2>/dev/null || return 0 - root=$(sed -n 's/.*"root"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p' "$file" | head -n1) + content=$(tr -d '\r\n' < "$file") + root=$(printf '%s' "$content" | sed -n 's/^[[:space:]]*{[[:space:]]*"root"[[:space:]]*:[[:space:]]*"\([^"\\]*\)"[[:space:]]*,[[:space:]]*"owned"[[:space:]]*:[[:space:]]*true[[:space:]]*}[[:space:]]*$/\1/p') case "$root" in /*) ;; *) return 0 ;; esac [ "$root" = "$electron_user_data/runtime" ] && return 0 [ "$(basename "$root" | tr '[:upper:]' '[:lower:]')" = "voicestudio" ] || return 0 + [ -d "$root/project" ] || return 0 printf '%s\n' "$root" } ELECTRON_RUNTIME="$(electron_owned_runtime)" diff --git a/tests/scripts/test_uninstall_locations.py b/tests/scripts/test_uninstall_locations.py index 1c43018be..d7457b980 100644 --- a/tests/scripts/test_uninstall_locations.py +++ b/tests/scripts/test_uninstall_locations.py @@ -100,6 +100,25 @@ def test_sh_keeps_runtime_roots_the_app_does_not_own(tmp_path, location): assert not user_data.exists() +@pytest.mark.skipif(sys.platform.startswith("win"), reason="bash script is macOS/Linux-only") +@pytest.mark.parametrize("make_file", [ + # Damaged: an owned flag and an unrelated root spread over separate lines. + lambda root: '{\n "owned": true,\n "other": {"root": "%s"}\n}' % root, + lambda root: '{"root": "/somewhere/else", "owned": true}\n{"root": "%s"}' % root, + # Well formed, but the folder holds no app project. + lambda root: json.dumps({"root": root, "owned": True}), +]) +def test_sh_ignores_damaged_or_unverified_runtime_records(tmp_path, make_file): + home, user_data, _, _ = _layout(tmp_path) + root = tmp_path / "unrelated" / "VoiceStudio" + (root / "keep-me").mkdir(parents=True) + (user_data / "runtime-location.json").write_text(make_file(str(root))) + + applied = _run(tmp_path, home, "--yes") + assert applied.returncode == 0, applied.stderr + assert (root / "keep-me").exists() + + def test_scripts_no_longer_cite_the_removed_tauri_backend(): for path in (SH, PS1): with open(path, encoding="utf-8-sig") as handle: @@ -113,6 +132,7 @@ def test_ps1_targets_electron_folders_and_its_uninstaller(): assert "Join-Path $appData $electronAppName" in text assert "$electronUpdaterCache = 'voicestudio-electron-updater'" in text assert "runtime-location.json" in text and "$location.owned -eq $true" in text + assert "(Join-Path $root 'project') -PathType Container" in text assert "'Uninstall VoiceStudio.exe'" in text # The legacy Tauri cleanup stays. assert "$legacyIdentifier = 'com.debpalash.omnivoice-studio'" in text From 322e67f58e3c04e8130b70345f342605931e4d70 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:52:27 +0530 Subject: [PATCH 60/85] docs(macos): drop the duplicated token and troubleshooting sections, with a guard --- docs/install/macos.md | 19 ------------------- tests/test_install_docs_headings.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 19 deletions(-) create mode 100644 tests/test_install_docs_headings.py diff --git a/docs/install/macos.md b/docs/install/macos.md index d81dcf6b8..768970817 100644 --- a/docs/install/macos.md +++ b/docs/install/macos.md @@ -173,25 +173,6 @@ Full details: [docs/setup/huggingface-token.md](../setup/huggingface-token.md). Hit a wall? See [docs/install/troubleshooting.md](troubleshooting.md). -The in-app error UI (the React error boundary that fires on backend errors) -includes an **"Open docs for this error"** button — that button deeplinks -back into this docs tree at the right section for the error class. - -## Hugging Face token (optional but recommended) - -The default install works without a token, but diarization (the -`pyannote/speaker-diarization-3.1` model) is gated and the larger -voice-design engines also download faster with a token attached. - -- Open **Settings → API Keys** in the app. -- Or set the env var `export HF_TOKEN=hf_…` in `~/.zshrc`. - -Full details: [docs/setup/huggingface-token.md](../setup/huggingface-token.md). - -## Troubleshooting - -Hit a wall? See [docs/install/troubleshooting.md](troubleshooting.md). - The in-app error UI includes an **"Open docs for this error"** button that deeplinks back into this docs tree at the right section for the error class. diff --git a/tests/test_install_docs_headings.py b/tests/test_install_docs_headings.py new file mode 100644 index 000000000..7ba64e011 --- /dev/null +++ b/tests/test_install_docs_headings.py @@ -0,0 +1,15 @@ +"""Install guides must not repeat a section: a duplicated copy drifts out of date.""" +from collections import Counter +from pathlib import Path + +import pytest + +_ROOT = Path(__file__).resolve().parents[1] +_GUIDES = sorted((_ROOT / "docs" / "install").glob("*.md")) + [_ROOT / "README.md"] + + +@pytest.mark.parametrize("path", _GUIDES, ids=lambda p: p.name) +def test_no_repeated_top_level_sections(path): + headings = [line for line in path.read_text(encoding="utf-8").splitlines() if line.startswith("## ")] + repeated = [h for h, n in Counter(headings).items() if n > 1] + assert not repeated, f"{path.name} repeats {repeated}" From 70d26b6c3795a8f84971ddad133da8f640412aa2 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:52:27 +0530 Subject: [PATCH 61/85] fix(generate): follow backend timeouts an operator raised when sizing the client backstop --- backend/api/routers/generation.py | 8 ++++ backend/services/model_manager.py | 15 +++++++ electron/src/renderer/src/app.tsx | 2 + .../src/hooks/use-generate-budget-sync.tsx | 13 ++++++ .../src/lib/api/generate-budget.test.ts | 41 +++++++++++++++++++ .../renderer/src/lib/api/generate-budget.ts | 27 ++++++++++++ electron/src/renderer/src/lib/api/generate.ts | 6 ++- electron/src/shared/utils/generateBudget.ts | 24 +++++++++-- tests/test_generate_budget_endpoint.py | 33 +++++++++++++++ 9 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 electron/src/renderer/src/hooks/use-generate-budget-sync.tsx create mode 100644 electron/src/renderer/src/lib/api/generate-budget.test.ts create mode 100644 electron/src/renderer/src/lib/api/generate-budget.ts create mode 100644 tests/test_generate_budget_endpoint.py diff --git a/backend/api/routers/generation.py b/backend/api/routers/generation.py index 953da0955..4551629e5 100644 --- a/backend/api/routers/generation.py +++ b/backend/api/routers/generation.py @@ -1684,6 +1684,14 @@ def _apply_routing_headers(headers, engine_notice, decision): return headers +@router.get("/generate/budget") +def generate_budget(): + """Active generate budgets, so the UI's backstop follows operator overrides.""" + from services.model_manager import generate_budget_s + + return generate_budget_s() + + @router.post("/generate") async def generate_speech( text: str = Form(...), diff --git a/backend/services/model_manager.py b/backend/services/model_manager.py index 25d909829..1854f1222 100644 --- a/backend/services/model_manager.py +++ b/backend/services/model_manager.py @@ -4078,3 +4078,18 @@ def unload_diarization_pipeline() -> bool: except Exception: logger.debug("Could not clear accelerator cache after diarisation unload", exc_info=True) return True + + +def generate_budget_s() -> dict[str, float]: + """The active /generate budgets, including operator overrides. + + The client backstop (electron/src/shared/utils/generateBudget.ts) takes the + larger of these and its built-in defaults, so raising a timeout through the + environment never makes the UI give up on a job that is still running. + """ + return { + "modelLoad": _model_load_timeout(), + "queueWait": GPU_QUEUE_TIMEOUT_S, + "executionBase": max(GPU_JOB_TIMEOUT_S, CPU_JOB_TIMEOUT_S), + "progressExtensionCap": progress_extension_cap_s(), + } diff --git a/electron/src/renderer/src/app.tsx b/electron/src/renderer/src/app.tsx index c329c5ff8..a115b883b 100644 --- a/electron/src/renderer/src/app.tsx +++ b/electron/src/renderer/src/app.tsx @@ -17,6 +17,7 @@ import { stepAppearanceScale } from '@/hooks/use-appearance'; import { recordRouteBreadcrumb } from '@/lib/report-breadcrumb'; import { ModelInstallSync } from '@/hooks/use-model-install-sync'; import { RealtimeEventSync } from '@/hooks/use-realtime-events'; +import { GenerateBudgetSync } from '@/hooks/use-generate-budget-sync'; import { runRendererTask } from '@/lib/global-error-recovery'; import { WebAuthGate } from '@/components/web-auth-gate'; @@ -116,6 +117,7 @@ export function App() { + diff --git a/electron/src/renderer/src/hooks/use-generate-budget-sync.tsx b/electron/src/renderer/src/hooks/use-generate-budget-sync.tsx new file mode 100644 index 000000000..1bb21d12b --- /dev/null +++ b/electron/src/renderer/src/hooks/use-generate-budget-sync.tsx @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import { isBackendReachable } from '@shared/utils/backendStage'; +import { primeGenerateBudget } from '@/lib/api/generate-budget'; +import { useBackendStatus } from './use-backend-status'; + +/** Re-reads the backend's generate budgets whenever the backend becomes reachable. */ +export function GenerateBudgetSync(): null { + const reachable = isBackendReachable(useBackendStatus().stage); + useEffect(() => { + if (reachable) void primeGenerateBudget(); + }, [reachable]); + return null; +} diff --git a/electron/src/renderer/src/lib/api/generate-budget.test.ts b/electron/src/renderer/src/lib/api/generate-budget.test.ts new file mode 100644 index 000000000..4713223e6 --- /dev/null +++ b/electron/src/renderer/src/lib/api/generate-budget.test.ts @@ -0,0 +1,41 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { BACKEND_GENERATE_BUDGET_S, generateAbortMs } from '@shared/utils/generateBudget'; +import { + primeGenerateBudget, + reportedGenerateBudget, + resetGenerateBudgetForTests, +} from './generate-budget'; + +afterEach(() => { + resetGenerateBudgetForTests(); + vi.unstubAllGlobals(); +}); + +describe('generate budget', () => { + it('outlasts budgets an operator raised on the backend', () => { + const queueWait = BACKEND_GENERATE_BUDGET_S.queueWait + 3_600; + expect(generateAbortMs(0, { queueWait })).toBe(generateAbortMs(0) + 3_600_000); + }); + + it('never drops below the built-in defaults or trusts malformed values', () => { + expect(generateAbortMs(0, { queueWait: 1, modelLoad: 'x', executionBase: Number.NaN })).toBe( + generateAbortMs(0), + ); + }); + + it('reads the backend report and keeps it when a later read fails', async () => { + const body = { + modelLoad: 5_000, + queueWait: 1_800, + executionBase: 900, + progressExtensionCap: 1_800, + }; + vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify(body)))); + await primeGenerateBudget(); + expect(reportedGenerateBudget()).toEqual(body); + + vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('offline'))); + await primeGenerateBudget(); + expect(reportedGenerateBudget()).toEqual(body); + }); +}); diff --git a/electron/src/renderer/src/lib/api/generate-budget.ts b/electron/src/renderer/src/lib/api/generate-budget.ts new file mode 100644 index 000000000..bc4643c84 --- /dev/null +++ b/electron/src/renderer/src/lib/api/generate-budget.ts @@ -0,0 +1,27 @@ +import type { ReportedGenerateBudget } from '@shared/utils/generateBudget'; +import { apiJson } from './client'; + +let reported: ReportedGenerateBudget = {}; + +/** The backend's active generate budgets, as last reported; empty until then. */ +export function reportedGenerateBudget(): ReportedGenerateBudget { + return reported; +} + +/** + * Read GET /generate/budget so the /generate backstop follows timeouts an + * operator raised through the environment. Backends before 0.5.7 have no such + * route; the built-in defaults then apply. + */ +export async function primeGenerateBudget(): Promise { + try { + const body = await apiJson('/generate/budget'); + if (body && typeof body === 'object') reported = body; + } catch { + // Keep the last known values. + } +} + +export function resetGenerateBudgetForTests(): void { + reported = {}; +} diff --git a/electron/src/renderer/src/lib/api/generate.ts b/electron/src/renderer/src/lib/api/generate.ts index 493a162ad..62aaf867f 100644 --- a/electron/src/renderer/src/lib/api/generate.ts +++ b/electron/src/renderer/src/lib/api/generate.ts @@ -3,6 +3,7 @@ import { generationFailureMessage } from '@shared/utils/generationFailureMessage import i18next from 'i18next'; import { languageRejectionMessage } from '@shared/utils/languageRejection.ts'; import { ApiError, apiFetch, isAbortError } from './client'; +import { reportedGenerateBudget } from './generate-budget'; import type { CloneGenerateInput, GenerateResult } from './types'; import { beginAppActivity } from '@/lib/app-activity'; import { createStreamingPreview } from '@/lib/audio/streaming-preview'; @@ -402,7 +403,10 @@ export async function generateClone( if (opts.signal?.aborted) controller.abort(); else opts.signal?.addEventListener('abort', forwardAbort, { once: true }); // Outlasts the backend's own budget so its descriptive error always wins. - const backstop = setTimeout(forwardAbort, generateAbortMs(input.text.length)); + const backstop = setTimeout( + forwardAbort, + generateAbortMs(input.text.length, reportedGenerateBudget()), + ); const finishActivity = beginAppActivity('synthesis'); try { const res = await apiFetch('/generate', { diff --git a/electron/src/shared/utils/generateBudget.ts b/electron/src/shared/utils/generateBudget.ts index 28d7a732f..f61c4ae9a 100644 --- a/electron/src/shared/utils/generateBudget.ts +++ b/electron/src/shared/utils/generateBudget.ts @@ -19,8 +19,9 @@ * max(OMNIVOICE_PROGRESS_EXTENSION_CAP_S, budgets x execution budget) * - freeChars / charsPerSecond: the execution budget's length scaling * - * Operators who raise those budgets through the environment are beyond what - * the client can know; the margin covers scheduling jitter only. + * Operators can raise those budgets through the environment; the backend + * reports its active values at GET /generate/budget, and the larger of each + * reported value and its default is used. The margin covers scheduling jitter. */ export const BACKEND_GENERATE_BUDGET_S = { modelLoad: 1200, @@ -35,9 +36,24 @@ export const BACKEND_GENERATE_BUDGET_S = { const CLIENT_MARGIN_S = 60; +export type ReportedGenerateBudget = Partial< + Record<'modelLoad' | 'queueWait' | 'executionBase' | 'progressExtensionCap', unknown> +>; + /** Milliseconds before the client gives up on a /generate for this text. */ -export function generateAbortMs(textLength = 0): number { - const budget = BACKEND_GENERATE_BUDGET_S; +export function generateAbortMs(textLength = 0, reported: ReportedGenerateBudget = {}): number { + const raise = (key: keyof ReportedGenerateBudget): number => { + const value = reported[key]; + const base = BACKEND_GENERATE_BUDGET_S[key]; + return typeof value === 'number' && Number.isFinite(value) ? Math.max(base, value) : base; + }; + const budget = { + ...BACKEND_GENERATE_BUDGET_S, + modelLoad: raise('modelLoad'), + queueWait: raise('queueWait'), + executionBase: raise('executionBase'), + progressExtensionCap: raise('progressExtensionCap'), + }; const execution = budget.executionBase + budget.sidecarGrace + diff --git a/tests/test_generate_budget_endpoint.py b/tests/test_generate_budget_endpoint.py new file mode 100644 index 000000000..f57f738f3 --- /dev/null +++ b/tests/test_generate_budget_endpoint.py @@ -0,0 +1,33 @@ +"""GET /generate/budget reports the active budgets the UI backstop must outlast.""" +from __future__ import annotations + +import re +from pathlib import Path + +from services import model_manager + +ROOT = Path(__file__).resolve().parents[1] +BUDGET_TS = ROOT / "electron/src/shared/utils/generateBudget.ts" + + +def test_reported_keys_are_ones_the_client_reads(): + reported = set(model_manager.generate_budget_s()) + client = re.search(r"Record<\s*([^,]+),\s*unknown", BUDGET_TS.read_text(encoding="utf-8")).group(1) + assert reported == set(re.findall(r"'(\w+)'", client)) + + +def test_operator_overrides_are_reported(monkeypatch): + monkeypatch.setattr(model_manager, "GPU_QUEUE_TIMEOUT_S", 7200.0) + monkeypatch.setattr(model_manager, "CPU_JOB_TIMEOUT_S", 4000.0) + monkeypatch.setenv("OMNIVOICE_MODEL_LOAD_TIMEOUT", "9000") + budget = model_manager.generate_budget_s() + assert budget["queueWait"] == 7200.0 + assert budget["executionBase"] >= 4000.0 + assert budget["modelLoad"] == 9000.0 + + +def test_route_is_registered(): + from api.routers import generation + + paths = {(route.path, tuple(sorted(route.methods))) for route in generation.router.routes} + assert ("/generate/budget", ("GET",)) in paths From 9d07b44e2ae85f429fc54c0ecb52382935b3aeb0 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:09:52 +0530 Subject: [PATCH 62/85] fix(generate): keep the newest budget report, wait briefly for one in flight, and record the route --- .../src/lib/api/generate-budget.test.ts | 36 +++++++++++++++++ .../renderer/src/lib/api/generate-budget.ts | 40 +++++++++++++++---- electron/src/renderer/src/lib/api/generate.ts | 11 ++++- tests/fixtures/api_routes.txt | 1 + tests/test_generate_budget_endpoint.py | 6 ++- 5 files changed, 82 insertions(+), 12 deletions(-) diff --git a/electron/src/renderer/src/lib/api/generate-budget.test.ts b/electron/src/renderer/src/lib/api/generate-budget.test.ts index 4713223e6..ccf153862 100644 --- a/electron/src/renderer/src/lib/api/generate-budget.test.ts +++ b/electron/src/renderer/src/lib/api/generate-budget.test.ts @@ -1,6 +1,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { BACKEND_GENERATE_BUDGET_S, generateAbortMs } from '@shared/utils/generateBudget'; import { + generateBudgetPending, + generateBudgetSettled, primeGenerateBudget, reportedGenerateBudget, resetGenerateBudgetForTests, @@ -39,3 +41,37 @@ describe('generate budget', () => { expect(reportedGenerateBudget()).toEqual(body); }); }); + +describe('generate budget ordering', () => { + it('keeps the newest read when an older one answers last', async () => { + let answerOld: (r: Response) => void = () => {}; + const fetchMock = vi + .fn() + .mockImplementationOnce(() => new Promise((done) => (answerOld = done))) + .mockResolvedValueOnce(new Response(JSON.stringify({ queueWait: 9_000 }))); + vi.stubGlobal('fetch', fetchMock); + const older = primeGenerateBudget(); + await primeGenerateBudget(); + answerOld(new Response(JSON.stringify({ queueWait: 100 }))); + await older; + expect(reportedGenerateBudget()).toEqual({ queueWait: 9_000 }); + }); + + it('lets a generate wait for a read in flight, without starting one', async () => { + expect(generateBudgetPending()).toBe(false); + await generateBudgetSettled(10); // nothing in flight: returns at once + let answer: (r: Response) => void = () => {}; + vi.stubGlobal( + 'fetch', + vi.fn(() => new Promise((done) => (answer = done))), + ); + const read = primeGenerateBudget(); + expect(generateBudgetPending()).toBe(true); + const settled = generateBudgetSettled(5_000); + answer(new Response(JSON.stringify({ modelLoad: 4_000 }))); + await settled; + await read; + expect(reportedGenerateBudget()).toEqual({ modelLoad: 4_000 }); + expect(generateBudgetPending()).toBe(false); + }); +}); diff --git a/electron/src/renderer/src/lib/api/generate-budget.ts b/electron/src/renderer/src/lib/api/generate-budget.ts index bc4643c84..166d8c460 100644 --- a/electron/src/renderer/src/lib/api/generate-budget.ts +++ b/electron/src/renderer/src/lib/api/generate-budget.ts @@ -2,26 +2,50 @@ import type { ReportedGenerateBudget } from '@shared/utils/generateBudget'; import { apiJson } from './client'; let reported: ReportedGenerateBudget = {}; +let pending: Promise | null = null; +let generation = 0; /** The backend's active generate budgets, as last reported; empty until then. */ export function reportedGenerateBudget(): ReportedGenerateBudget { return reported; } +/** Resolves once a read already in flight settles, or after `timeoutMs`. Never starts a read. */ +export async function generateBudgetSettled(timeoutMs: number): Promise { + if (!pending) return; + let timer: ReturnType | undefined; + await Promise.race([pending, new Promise((done) => (timer = setTimeout(done, timeoutMs)))]); + clearTimeout(timer); +} + +export function generateBudgetPending(): boolean { + return pending !== null; +} + /** * Read GET /generate/budget so the /generate backstop follows timeouts an * operator raised through the environment. Backends before 0.5.7 have no such - * route; the built-in defaults then apply. + * route; the built-in defaults then apply. Only the newest read is kept, so a + * slow answer from before a reconnect cannot replace the current backend's. */ -export async function primeGenerateBudget(): Promise { - try { - const body = await apiJson('/generate/budget'); - if (body && typeof body === 'object') reported = body; - } catch { - // Keep the last known values. - } +export function primeGenerateBudget(): Promise { + const mine = ++generation; + const read = (async () => { + try { + const body = await apiJson('/generate/budget'); + if (mine === generation && body && typeof body === 'object') reported = body; + } catch { + // Keep the last known values. + } finally { + if (mine === generation) pending = null; + } + })(); + pending = read; + return read; } export function resetGenerateBudgetForTests(): void { reported = {}; + pending = null; + generation = 0; } diff --git a/electron/src/renderer/src/lib/api/generate.ts b/electron/src/renderer/src/lib/api/generate.ts index 62aaf867f..ce9391cce 100644 --- a/electron/src/renderer/src/lib/api/generate.ts +++ b/electron/src/renderer/src/lib/api/generate.ts @@ -3,7 +3,11 @@ import { generationFailureMessage } from '@shared/utils/generationFailureMessage import i18next from 'i18next'; import { languageRejectionMessage } from '@shared/utils/languageRejection.ts'; import { ApiError, apiFetch, isAbortError } from './client'; -import { reportedGenerateBudget } from './generate-budget'; +import { + generateBudgetPending, + generateBudgetSettled, + reportedGenerateBudget, +} from './generate-budget'; import type { CloneGenerateInput, GenerateResult } from './types'; import { beginAppActivity } from '@/lib/app-activity'; import { createStreamingPreview } from '@/lib/audio/streaming-preview'; @@ -402,7 +406,10 @@ export async function generateClone( const forwardAbort = (): void => controller.abort(); if (opts.signal?.aborted) controller.abort(); else opts.signal?.addEventListener('abort', forwardAbort, { once: true }); - // Outlasts the backend's own budget so its descriptive error always wins. + // Outlasts the backend's own budget so its descriptive error always wins. A + // budget read still in flight (just after connecting) is awaited briefly so + // an operator-raised timeout is not missed. + if (generateBudgetPending()) await generateBudgetSettled(2_000); const backstop = setTimeout( forwardAbort, generateAbortMs(input.text.length, reportedGenerateBudget()), diff --git a/tests/fixtures/api_routes.txt b/tests/fixtures/api_routes.txt index ec5229c97..bf96f5b60 100644 --- a/tests/fixtures/api_routes.txt +++ b/tests/fixtures/api_routes.txt @@ -117,6 +117,7 @@ GET /gallery/categories GET /gallery/voices GET /gallery/voices/{voice_id} GET /gallery/voices/{voice_id}/preview +GET /generate/budget GET /glossary/{project_id} GET /health GET /history diff --git a/tests/test_generate_budget_endpoint.py b/tests/test_generate_budget_endpoint.py index f57f738f3..79a15d444 100644 --- a/tests/test_generate_budget_endpoint.py +++ b/tests/test_generate_budget_endpoint.py @@ -4,19 +4,21 @@ import re from pathlib import Path -from services import model_manager - ROOT = Path(__file__).resolve().parents[1] BUDGET_TS = ROOT / "electron/src/shared/utils/generateBudget.ts" def test_reported_keys_are_ones_the_client_reads(): + from services import model_manager + reported = set(model_manager.generate_budget_s()) client = re.search(r"Record<\s*([^,]+),\s*unknown", BUDGET_TS.read_text(encoding="utf-8")).group(1) assert reported == set(re.findall(r"'(\w+)'", client)) def test_operator_overrides_are_reported(monkeypatch): + from services import model_manager + monkeypatch.setattr(model_manager, "GPU_QUEUE_TIMEOUT_S", 7200.0) monkeypatch.setattr(model_manager, "CPU_JOB_TIMEOUT_S", 4000.0) monkeypatch.setenv("OMNIVOICE_MODEL_LOAD_TIMEOUT", "9000") From ee2de2d8d461797799f4e344680b7f22f002d89b Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:39:22 +0530 Subject: [PATCH 63/85] fix(ci): credit a person in 'by an LLM engineer', rewrite only listed commits, and cover open PRs in the rollout --- .github/CONTRIBUTING.md | 8 +++++--- docs/maintainers/repository-settings.md | 3 +++ scripts/check_commit_identities.py | 13 ++++++++---- tests/test_commit_identities.py | 27 +++++++++++++++++++++++++ 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b692af679..7636fec35 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -245,9 +245,11 @@ Commit with a real identity (your GitHub noreply address works). The PR commit's author, committer, or `Co-authored-by:`/`Signed-off-by:` email is a placeholder (`test@local`, `you@example.com`, `mergetest`, hostname-style `*.local`), an AI agent, or on the hashed block list, or when a commit message -or the PR description credits an AI agent. Fix an identity with -`git rebase --exec 'git commit --amend --no-edit --reset-author' origin/main`; -fix agent credit by rewording the commit messages (`git rebase -i`, then +or the PR description credits an AI agent. Fix an identity on the listed +commits only: `git rebase -i origin/main`, mark each listed commit `edit`, and +at each stop run `git commit --amend --no-edit --reset-author` and +`git rebase --continue` (a blanket `--exec` would also take over other +people's commits); fix agent credit by rewording the commit messages (`git rebase -i`, then `reword`) and editing the PR description. --- diff --git a/docs/maintainers/repository-settings.md b/docs/maintainers/repository-settings.md index bd9121ddb..14a9c9549 100644 --- a/docs/maintainers/repository-settings.md +++ b/docs/maintainers/repository-settings.md @@ -14,6 +14,9 @@ is merged: review): `gh label create cla` and `gh label create cla-override`. 2. Open the public signing issue, label it `cla`, and pin it. 3. Comment `recheck` on each open pull request, so it gets a `CLA` status. + Pull requests opened before `commit-identity.yml` existed also need a + `Commit identities` run: merge current `main` into them (the merge + protocol asks for that anyway), or close and reopen them. 4. Then apply the `main` ruleset below. A required check that never reported blocks the merge with "Expected — waiting for status". 5. Apply the `cla-signatures` ruleset once the first signature creates the diff --git a/scripts/check_commit_identities.py b/scripts/check_commit_identities.py index add80c67c..16e17e149 100755 --- a/scripts/check_commit_identities.py +++ b/scripts/check_commit_identities.py @@ -64,10 +64,13 @@ # people with a work address at an AI company. _AGENTS = ( r"(claude|cursor|copilot|codex|chatgpt|openai|gemini|devin|jules|aider|cline|windsurf|coderabbit|greptile" - r"|sourcery|tabnine|codeium|amazon q|kiro|qodo|openhands|sweep|an? ai\b|ai\b|an? llm\b|llm\b)" + r"|sourcery|tabnine|codeium|amazon q|kiro|qodo|openhands|sweep)\b" + # A generic "AI"/"LLM" credits a tool only when the line ends there, so + # "Written by an LLM engineer" still credits a person. + r"|(an? )?(ai|llm)( (model|agent|assistant|tool))?[ \t]*[.!)\]]?[ \t]*$" ) AI_ATTRIBUTION = re.compile( - rf"^[ \t>*_-]*(generated|written|created|authored)[ \t]+(with|by|using)[ \t]+\[?{_AGENTS}" + rf"^[ \t>*_-]*(generated|written|created|authored)[ \t]+(with|by|using)[ \t]+\[?({_AGENTS})" r"|🤖[ \t]*(generated|written|created|authored)\b|^[ \t]*claude-session[ \t]*:" # Agent session and share links. r"|\bclaude\.ai/(c|chat|share|code|new)\b|\bclaude\.com/(claude-)?code\b" @@ -217,8 +220,10 @@ def main(argv: list[str] | None = None) -> int: for short, field, kind in violations: print(f" {short} {field}: {kind}") print( - "Set an allowed identity (e.g. your GitHub noreply address), then rewrite and force-push:\n" - " git rebase --exec 'git commit --amend --no-edit --reset-author' origin/\n" + "Set an allowed identity (e.g. your GitHub noreply address), then rewrite only the\n" + "commits listed above and force-push: `git rebase -i origin/`, mark each listed\n" + "commit `edit`, and at each stop run `git commit --amend --no-edit --reset-author`\n" + "and `git rebase --continue`. Other people's commits keep their authors.\n" "Remove blocked Co-authored-by/Signed-off-by trailers and AI agent attribution lines\n" "with `git rebase -i` (reword) and from the pull request description. Commits carry\n" "only your own git identity: no AI agent co-authors or 'Generated with ...' lines." diff --git a/tests/test_commit_identities.py b/tests/test_commit_identities.py index e9cb55168..2791d1d56 100644 --- a/tests/test_commit_identities.py +++ b/tests/test_commit_identities.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib +import importlib.util import os import re import secrets @@ -294,3 +295,29 @@ def test_pull_request_description_attribution_is_blocked(repo, tmp_path): assert result.returncode == 1 and "PR description: AI agent attribution" in result.stdout event.write_text('{"pull_request": {"body": null}}') assert check(*_only(sha), "--event", str(event)).returncode == 0 + + +@pytest.mark.parametrize( + "text,flagged", + [ + ("Written by an LLM engineer on the team.", False), + ("Generated by AI researchers at the lab", False), + ("Generated with an AI model.", True), + ("Written by AI", True), + ("Created using an LLM", True), + ("Generated by Claude", True), + ("Generated with Cursor today", True), + ], +) +def test_generic_ai_credit_only_when_it_names_no_person(text, flagged): + spec = importlib.util.spec_from_file_location("cci", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + assert bool(module.classify_text(text)) is flagged + + +def test_remedy_rewrites_only_the_listed_commits(): + text = SCRIPT.read_text(encoding="utf-8") + assert "git rebase --exec 'git commit --amend --no-edit --reset-author'" not in text + contributing = (ROOT / ".github" / "CONTRIBUTING.md").read_text(encoding="utf-8") + assert "mark each listed commit `edit`" in contributing From 723a1d2ac9735f87a0f3c4b443dac1f097b45e75 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:10:29 +0530 Subject: [PATCH 64/85] fix(ci): flag generic AI credit unless a person's role follows it --- scripts/check_commit_identities.py | 5 +++-- tests/test_commit_identities.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/check_commit_identities.py b/scripts/check_commit_identities.py index 16e17e149..32f3bdc98 100755 --- a/scripts/check_commit_identities.py +++ b/scripts/check_commit_identities.py @@ -65,9 +65,10 @@ _AGENTS = ( r"(claude|cursor|copilot|codex|chatgpt|openai|gemini|devin|jules|aider|cline|windsurf|coderabbit|greptile" r"|sourcery|tabnine|codeium|amazon q|kiro|qodo|openhands|sweep)\b" - # A generic "AI"/"LLM" credits a tool only when the line ends there, so + # A generic "AI"/"LLM" credits a tool unless a person's role follows, so # "Written by an LLM engineer" still credits a person. - r"|(an? )?(ai|llm)( (model|agent|assistant|tool))?[ \t]*[.!)\]]?[ \t]*$" + r"|(an? )?(ai|llm)\b(?![ \t-]+(engineers?|researchers?|developers?|scientists?|experts?|specialists?" + r"|teams?|labs?|compan(y|ies)|startups?|groups?|ethicists?|practitioners?)\b)" ) AI_ATTRIBUTION = re.compile( rf"^[ \t>*_-]*(generated|written|created|authored)[ \t]+(with|by|using)[ \t]+\[?({_AGENTS})" diff --git a/tests/test_commit_identities.py b/tests/test_commit_identities.py index 2791d1d56..e246dd9a5 100644 --- a/tests/test_commit_identities.py +++ b/tests/test_commit_identities.py @@ -305,6 +305,11 @@ def test_pull_request_description_attribution_is_blocked(repo, tmp_path): ("Generated with an AI model.", True), ("Written by AI", True), ("Created using an LLM", True), + ("Generated with AI assistance", True), + ("Generated with an AI model for this change", True), + ("Generated by AI assistant and reviewed by me", True), + ("Generated with an AI tool for docs", True), + ("Written by the AI team at Example Corp", False), ("Generated by Claude", True), ("Generated with Cursor today", True), ], From 0d36b670b3ef6e0584d6ba5ad108e83c60378cd4 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 12:10:33 +0530 Subject: [PATCH 65/85] fix(uninstall): run only an uninstaller in the install folders, never a per-user one when elevated --- scripts/uninstall.ps1 | 24 +++++++++++++++++++++-- tests/scripts/test_uninstall_locations.py | 4 ++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index d223089c8..1eb534ca7 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -50,12 +50,32 @@ if ($RemoveApp) { ) $entries = @(Get-ItemProperty $uninstallKeys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'VoiceStudio|OmniVoice' }) + # Only run an uninstaller from where the installer puts it. Per-user installs + # (HKCU, %LOCALAPPDATA%\Programs) are writable by the user, so an elevated run + # never executes one: that would hand admin rights to a user-writable file. + $isElevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator) + $machineRoots = @([Environment]::GetEnvironmentVariable('ProgramFiles'), + [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')) | Where-Object { $_ } + $userRoot = Join-Path ([Environment]::GetEnvironmentVariable('LOCALAPPDATA')) 'Programs' foreach ($entry in $entries) { if ([string]$entry.UninstallString -match '^"([^"]+\.exe)"(?:\s+(/currentuser|/allusers))?\s*$' -and (Split-Path $Matches[1] -Leaf) -eq 'Uninstall VoiceStudio.exe' -and (Test-Path -LiteralPath $Matches[1] -PathType Leaf)) { - $nsisUninstaller = $Matches[1] - $nsisScope = $Matches[2] + $candidate = [System.IO.Path]::GetFullPath($Matches[1]) + $scope = $Matches[2] + $parent = Split-Path (Split-Path $candidate -Parent) -Parent + $trusted = if ($entry.PSPath -like '*HKEY_CURRENT_USER*') { + -not $isElevated -and $parent -eq $userRoot + } else { + $machineRoots -contains $parent + } + if (-not $trusted) { + Write-Host "Skipped an uninstaller outside the install folders: $candidate" + continue + } + $nsisUninstaller = $candidate + $nsisScope = $scope break } } diff --git a/tests/scripts/test_uninstall_locations.py b/tests/scripts/test_uninstall_locations.py index d7457b980..f2f15ec19 100644 --- a/tests/scripts/test_uninstall_locations.py +++ b/tests/scripts/test_uninstall_locations.py @@ -134,5 +134,9 @@ def test_ps1_targets_electron_folders_and_its_uninstaller(): assert "runtime-location.json" in text and "$location.owned -eq $true" in text assert "(Join-Path $root 'project') -PathType Container" in text assert "'Uninstall VoiceStudio.exe'" in text + # Only an uninstaller in the install folders runs; an elevated run never + # executes a per-user (user-writable) one. + assert "$machineRoots -contains $parent" in text + assert "-not $isElevated -and $parent -eq $userRoot" in text # The legacy Tauri cleanup stays. assert "$legacyIdentifier = 'com.debpalash.omnivoice-studio'" in text From 4c948fcdefa086da4151c633c9d931190b4cbf00 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:56:06 +0530 Subject: [PATCH 66/85] fix(ci): flag generic AI credit only when a tool word follows or the phrase ends --- scripts/check_commit_identities.py | 8 ++++---- tests/test_commit_identities.py | 5 +++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/check_commit_identities.py b/scripts/check_commit_identities.py index 32f3bdc98..1423d88b6 100755 --- a/scripts/check_commit_identities.py +++ b/scripts/check_commit_identities.py @@ -65,10 +65,10 @@ _AGENTS = ( r"(claude|cursor|copilot|codex|chatgpt|openai|gemini|devin|jules|aider|cline|windsurf|coderabbit|greptile" r"|sourcery|tabnine|codeium|amazon q|kiro|qodo|openhands|sweep)\b" - # A generic "AI"/"LLM" credits a tool unless a person's role follows, so - # "Written by an LLM engineer" still credits a person. - r"|(an? )?(ai|llm)\b(?![ \t-]+(engineers?|researchers?|developers?|scientists?|experts?|specialists?" - r"|teams?|labs?|compan(y|ies)|startups?|groups?|ethicists?|practitioners?)\b)" + # A generic "AI"/"LLM" credits a tool only when it ends the phrase or a tool + # word follows, so a person ("an AI consultant", "the LLM team") never trips it. + r"|(an? )?(ai|llm)\b(?=[ \t-]*(assist\w*|models?|tools?|agents?|chat\w*|help|coding|code" + r"|pair[ \t-]?programm\w*)\b|[ \t]*([.,;:!?)]|$))" ) AI_ATTRIBUTION = re.compile( rf"^[ \t>*_-]*(generated|written|created|authored)[ \t]+(with|by|using)[ \t]+\[?({_AGENTS})" diff --git a/tests/test_commit_identities.py b/tests/test_commit_identities.py index e246dd9a5..24e154c12 100644 --- a/tests/test_commit_identities.py +++ b/tests/test_commit_identities.py @@ -310,6 +310,11 @@ def test_pull_request_description_attribution_is_blocked(repo, tmp_path): ("Generated by AI assistant and reviewed by me", True), ("Generated with an AI tool for docs", True), ("Written by the AI team at Example Corp", False), + ("Written by an AI consultant", False), + ("Authored by the LLM working group", False), + ("Written by AI, then edited", True), + ("Generated with AI-assisted tooling", True), + ("Created with an AI coding agent", True), ("Generated by Claude", True), ("Generated with Cursor today", True), ], From 475c7fc18a33d11af0b19ac4166aa1b7acada73c Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 22:56:18 +0530 Subject: [PATCH 67/85] fix(uninstall): run the registered uninstaller from any install folder, never a per-user one when elevated --- scripts/uninstall.ps1 | 19 +++++-------------- tests/scripts/test_uninstall_locations.py | 8 ++++---- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 1eb534ca7..dd37d5a7b 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -50,28 +50,19 @@ if ($RemoveApp) { ) $entries = @(Get-ItemProperty $uninstallKeys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'VoiceStudio|OmniVoice' }) - # Only run an uninstaller from where the installer puts it. Per-user installs - # (HKCU, %LOCALAPPDATA%\Programs) are writable by the user, so an elevated run - # never executes one: that would hand admin rights to a user-writable file. + # Run the uninstaller the registry entry names, wherever the user installed + # it. Per-user entries (HKCU) are user-writable, so an elevated run never + # executes one: that would hand admin rights to a user-writable file. $isElevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) - $machineRoots = @([Environment]::GetEnvironmentVariable('ProgramFiles'), - [Environment]::GetEnvironmentVariable('ProgramFiles(x86)')) | Where-Object { $_ } - $userRoot = Join-Path ([Environment]::GetEnvironmentVariable('LOCALAPPDATA')) 'Programs' foreach ($entry in $entries) { if ([string]$entry.UninstallString -match '^"([^"]+\.exe)"(?:\s+(/currentuser|/allusers))?\s*$' -and (Split-Path $Matches[1] -Leaf) -eq 'Uninstall VoiceStudio.exe' -and (Test-Path -LiteralPath $Matches[1] -PathType Leaf)) { $candidate = [System.IO.Path]::GetFullPath($Matches[1]) $scope = $Matches[2] - $parent = Split-Path (Split-Path $candidate -Parent) -Parent - $trusted = if ($entry.PSPath -like '*HKEY_CURRENT_USER*') { - -not $isElevated -and $parent -eq $userRoot - } else { - $machineRoots -contains $parent - } - if (-not $trusted) { - Write-Host "Skipped an uninstaller outside the install folders: $candidate" + if ($isElevated -and $entry.PSPath -like '*HKEY_CURRENT_USER*') { + Write-Host "Skipped a per-user uninstaller in an elevated run; run this script without admin rights: $candidate" continue } $nsisUninstaller = $candidate diff --git a/tests/scripts/test_uninstall_locations.py b/tests/scripts/test_uninstall_locations.py index f2f15ec19..c9c4a85fc 100644 --- a/tests/scripts/test_uninstall_locations.py +++ b/tests/scripts/test_uninstall_locations.py @@ -134,9 +134,9 @@ def test_ps1_targets_electron_folders_and_its_uninstaller(): assert "runtime-location.json" in text and "$location.owned -eq $true" in text assert "(Join-Path $root 'project') -PathType Container" in text assert "'Uninstall VoiceStudio.exe'" in text - # Only an uninstaller in the install folders runs; an elevated run never - # executes a per-user (user-writable) one. - assert "$machineRoots -contains $parent" in text - assert "-not $isElevated -and $parent -eq $userRoot" in text + # The registered uninstaller runs from any install folder, but an elevated + # run never executes a per-user (user-writable) one. + assert "$isElevated -and $entry.PSPath -like '*HKEY_CURRENT_USER*'" in text + assert "machineRoots" not in text and "userRoot" not in text # The legacy Tauri cleanup stays. assert "$legacyIdentifier = 'com.debpalash.omnivoice-studio'" in text From 484d0dbf5b32293e38bff59accaa763f5dc5da69 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:28:51 +0530 Subject: [PATCH 68/85] fix(uninstall): skip an uninstaller other users can modify in an elevated run; parse every PowerShell script in CI --- .github/workflows/ci.yml | 12 ++++++++- scripts/uninstall.ps1 | 30 +++++++++++++++++++---- tests/scripts/test_uninstall_locations.py | 6 ++++- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d11edce69..d85a3c9ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,8 +29,18 @@ jobs: - name: Shell installers and Worker routing working-directory: electron run: | - sh -n ../scripts/install.sh + sh -n ../scripts/install.sh && sh -n ../scripts/uninstall.sh node --test tests/shell-installer.test.mjs tests/installer-worker.test.mjs + - name: PowerShell scripts parse + shell: pwsh + run: | + $failed = $false + foreach ($file in git ls-files '*.ps1') { + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $file), [ref]$null, [ref]$errors) | Out-Null + foreach ($e in $errors) { Write-Host "::error file=$file,line=$($e.Extent.StartLineNumber)::$($e.Message)"; $failed = $true } + } + if ($failed) { exit 1 } - name: PowerShell installer flows shell: pwsh run: ./electron/tests/powershell-installer.ps1 diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index dd37d5a7b..c2bb1eb20 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -51,18 +51,38 @@ if ($RemoveApp) { $entries = @(Get-ItemProperty $uninstallKeys -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -match 'VoiceStudio|OmniVoice' }) # Run the uninstaller the registry entry names, wherever the user installed - # it. Per-user entries (HKCU) are user-writable, so an elevated run never - # executes one: that would hand admin rights to a user-writable file. - $isElevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole( + # it. An elevated run must not hand admin rights to a file another user can + # replace, so it skips per-user (HKCU) entries and any uninstaller whose file + # or folder a non-admin can modify. Without elevation the uninstaller asks + # for consent itself. + $currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent() + $isElevated = ([Security.Principal.WindowsPrincipal]$currentIdentity).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) + # SYSTEM, Administrators, TrustedInstaller, and the elevated user running this. + $adminSids = @('S-1-5-18', 'S-1-5-32-544', + 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464', $currentIdentity.User.Value) + # Write, delete, re-permission, or take ownership, including generic write/all. + $modifyMask = [int]([Security.AccessControl.FileSystemRights]'Write, Delete, DeleteSubdirectoriesAndFiles, ChangePermissions, TakeOwnership') -bor 0x50000000 + function Test-AdminOnlyWritable([string]$path) { + $acl = Get-Acl -LiteralPath $path + if ($adminSids -notcontains $acl.GetOwner([Security.Principal.SecurityIdentifier]).Value) { return $false } + foreach ($rule in $acl.GetAccessRules($true, $true, [Security.Principal.SecurityIdentifier])) { + if ($rule.AccessControlType -ne 'Allow') { continue } + if ($rule.PropagationFlags -band [Security.AccessControl.PropagationFlags]::InheritOnly) { continue } + if (([int]$rule.FileSystemRights -band $modifyMask) -eq 0) { continue } + if ($adminSids -notcontains $rule.IdentityReference.Value) { return $false } + } + return $true + } foreach ($entry in $entries) { if ([string]$entry.UninstallString -match '^"([^"]+\.exe)"(?:\s+(/currentuser|/allusers))?\s*$' -and (Split-Path $Matches[1] -Leaf) -eq 'Uninstall VoiceStudio.exe' -and (Test-Path -LiteralPath $Matches[1] -PathType Leaf)) { $candidate = [System.IO.Path]::GetFullPath($Matches[1]) $scope = $Matches[2] - if ($isElevated -and $entry.PSPath -like '*HKEY_CURRENT_USER*') { - Write-Host "Skipped a per-user uninstaller in an elevated run; run this script without admin rights: $candidate" + if ($isElevated -and ($entry.PSPath -like '*HKEY_CURRENT_USER*' -or + -not (Test-AdminOnlyWritable $candidate) -or -not (Test-AdminOnlyWritable (Split-Path $candidate -Parent)))) { + Write-Host "Skipped an uninstaller other users can modify in an elevated run; run this script without admin rights: $candidate" continue } $nsisUninstaller = $candidate diff --git a/tests/scripts/test_uninstall_locations.py b/tests/scripts/test_uninstall_locations.py index c9c4a85fc..aa2694277 100644 --- a/tests/scripts/test_uninstall_locations.py +++ b/tests/scripts/test_uninstall_locations.py @@ -136,7 +136,11 @@ def test_ps1_targets_electron_folders_and_its_uninstaller(): assert "'Uninstall VoiceStudio.exe'" in text # The registered uninstaller runs from any install folder, but an elevated # run never executes a per-user (user-writable) one. - assert "$isElevated -and $entry.PSPath -like '*HKEY_CURRENT_USER*'" in text + assert "$isElevated -and ($entry.PSPath -like '*HKEY_CURRENT_USER*'" in text + # ...nor one whose file or folder a non-admin can modify. + assert "-not (Test-AdminOnlyWritable $candidate)" in text + assert "-not (Test-AdminOnlyWritable (Split-Path $candidate -Parent))" in text + assert "'S-1-5-32-544'" in text and "InheritOnly" in text assert "machineRoots" not in text and "userRoot" not in text # The legacy Tauri cleanup stays. assert "$legacyIdentifier = 'com.debpalash.omnivoice-studio'" in text From 37bd07e8a81382d21753779390311a2db7c49d85 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Fri, 2 Oct 2026 23:35:35 +0530 Subject: [PATCH 69/85] fix(ci): check the bash uninstall script with bash, not sh --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d85a3c9ef..7cdcfa8b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: - name: Shell installers and Worker routing working-directory: electron run: | - sh -n ../scripts/install.sh && sh -n ../scripts/uninstall.sh + sh -n ../scripts/install.sh && bash -n ../scripts/uninstall.sh node --test tests/shell-installer.test.mjs tests/installer-worker.test.mjs - name: PowerShell scripts parse shell: pwsh From 44e56a1ef1dd2d8ad81aa56c85e56d2cda29ac88 Mon Sep 17 00:00:00 2001 From: debpalash <4178343+debpalash@users.noreply.github.com> Date: Sat, 3 Oct 2026 04:46:11 +0530 Subject: [PATCH 70/85] chore: remove unused Remotion player integration --- bun.lock | 3 --- docs/electron-playback.md | 3 +-- electron/PARITY.md | 6 +++--- electron/package.json | 1 - electron/src/renderer/src/components/media-player.tsx | 9 +++------ electron/src/renderer/src/components/video-player.tsx | 2 +- 6 files changed, 8 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index 8e6162fad..edc850b58 100644 --- a/bun.lock +++ b/bun.lock @@ -64,7 +64,6 @@ "react-hot-toast": "^2.6.0", "react-i18next": "^17.0.14", "react-window": "^2.3.1", - "remotion": "4.0.524", "shadcn": "^4.21.0", "sonner": "^2.0.8", "tailwind-merge": "^3.6.0", @@ -2082,8 +2081,6 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - "remotion": ["remotion@4.0.524", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-jtoQbO7+UD7/4gcl0Onjq/Q27DP3qjI9hRimJJGuvU9p6OekY+Oyn1wNjYxG+hGH4i6InDFneN3sazFLuSF4Og=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], diff --git a/docs/electron-playback.md b/docs/electron-playback.md index 44e6c9866..18cab7aca 100644 --- a/docs/electron-playback.md +++ b/docs/electron-playback.md @@ -16,8 +16,7 @@ The test uses synthetic WAV/profile/generation responses and never saves user da Set `PLAYWRIGHT_CHANNEL` or `VOICESTUDIO_UI_URL` for another installed browser/server. HLS and DASH libraries are bundled and lazy-loaded. The shared provider includes -Vidstack's native audio/video, HLS, DASH, YouTube and Vimeo selection plus the -Remotion loader. Gallery search previews exercise the embedded YouTube provider; +Vidstack's native audio/video, HLS, DASH, YouTube and Vimeo selection. Gallery search previews exercise the embedded YouTube provider; Dubbing uses the custom Vidstack video controls for local and normalized URL imports. Native video MIME hints select a provider before its ` + +