refactor(backend): 启动环境初始化与资源项目化(零跨项目依赖)

- main.py:uv run main.py 一键启动——读取 backend/.env、TORCH_HOME/NLTK_DATA 指向项目内缓存、certifi SSL + NO_PROXY 网络直连、默认开启 s2s,启动横幅
- vendor/:补丁后 s2s 云化栈移入项目(backend/vendor/s2s-cloud),pyproject sources 改相对路径
- .gitignore:忽略 .torch-cache / nltk_data 缓存
- AGENTS.md:进程管理铁律(AI 不启动/重启服务,由用户操作)
This commit is contained in:
Pine
2026-08-18 01:37:57 +08:00
parent 41e9681aa0
commit 7f25c8f6bb
198 changed files with 44792 additions and 4 deletions
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
actions:
patterns: ["*"]
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
"""Fetch stargazer history for a repo and render it as a static SVG.
Runs in CI with the automatic GITHUB_TOKEN. No third-party dependencies.
Usage:
GITHUB_TOKEN=... python star_history.py owner/repo output.svg
"""
import json
import math
import os
import sys
import urllib.request
from datetime import datetime, timezone
API = "https://api.github.com/graphql"
PER_PAGE = 100
MAX_PAGES = 400 # preserve the REST implementation's 40k-star cap
MAX_POINTS = 120 # downsample the curve to at most this many points
def gh_post(query: str, variables: dict, token: str) -> dict:
payload = json.dumps({"query": query, "variables": variables}).encode()
req = urllib.request.Request(API, data=payload)
req.add_header("Accept", "application/vnd.github+json")
req.add_header("Content-Type", "application/json")
req.add_header("X-GitHub-Api-Version", "2022-11-28")
if token:
req.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(req, timeout=30) as resp:
result = json.loads(resp.read())
if result.get("errors"):
raise RuntimeError(json.dumps(result["errors"]))
return result
def fetch_star_dates(repo: str, token: str) -> list[datetime]:
owner, name = repo.split("/", 1)
query = """
query($owner: String!, $name: String!, $cursor: String, $perPage: Int!) {
repository(owner: $owner, name: $name) {
stargazers(first: $perPage, after: $cursor) {
edges { starredAt }
pageInfo { hasNextPage endCursor }
}
}
}
"""
dates = []
page = 1
cursor = None
while True:
result = gh_post(
query,
{
"owner": owner,
"name": name,
"cursor": cursor,
"perPage": PER_PAGE,
},
token,
)
stargazers = result["data"]["repository"]["stargazers"]
for edge in stargazers["edges"]:
ts = edge.get("starredAt")
if ts:
dates.append(
datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ").replace(
tzinfo=timezone.utc
)
)
page_info = stargazers["pageInfo"]
if not page_info["hasNextPage"]:
break
page += 1
if page > MAX_PAGES:
break
cursor = page_info["endCursor"]
dates.sort()
return dates
def downsample(dates: list[datetime]) -> list[tuple[datetime, int]]:
n = len(dates)
points = [(dates[0], 1)]
if n > 1:
step = max(1, n // MAX_POINTS)
for i in range(step, n, step):
points.append((dates[i], i + 1))
points.append((dates[-1], n))
now = datetime.now(timezone.utc)
points.append((now, n))
return points
def nice_ceil(v: float) -> int:
if v <= 0:
return 1
mag = 10 ** int(math.floor(math.log10(v)))
for mult in (1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10):
if v <= mag * mult:
return int(mag * mult)
return int(mag * 10)
def render_svg(points: list[tuple[datetime, int]], repo: str) -> str:
width, height = 800, 420
ml, mr, mt, mb = 70, 30, 50, 60
pw, ph = width - ml - mr, height - mt - mb
t0 = points[0][0].timestamp()
t1 = points[-1][0].timestamp()
tspan = max(t1 - t0, 1)
ymax = nice_ceil(points[-1][1] * 1.08)
def x(ts: float) -> float:
return ml + (ts - t0) / tspan * pw
def y(v: float) -> float:
return mt + ph - v / ymax * ph
line = " ".join(
f"{'M' if i == 0 else 'L'}{x(d.timestamp()):.1f},{y(c):.1f}"
for i, (d, c) in enumerate(points)
)
area = (
line
+ f" L{x(points[-1][0].timestamp()):.1f},{y(0):.1f}"
+ f" L{x(points[0][0].timestamp()):.1f},{y(0):.1f} Z"
)
grid, ylabels = [], []
for i in range(6):
v = ymax * i / 5
yy = y(v)
grid.append(
f'<line x1="{ml}" y1="{yy:.1f}" x2="{ml + pw}" y2="{yy:.1f}" '
f'stroke="#8b949e" stroke-opacity="0.25" stroke-width="1"/>'
)
label = f"{v / 1000:.1f}k".replace(".0k", "k") if v >= 1000 else f"{int(v)}"
ylabels.append(
f'<text x="{ml - 10}" y="{yy + 4:.1f}" text-anchor="end" '
f'class="lbl">{label}</text>'
)
xlabels = []
for i in range(6):
ts = t0 + tspan * i / 5
d = datetime.fromtimestamp(ts, tz=timezone.utc)
xlabels.append(
f'<text x="{x(ts):.1f}" y="{mt + ph + 22}" text-anchor="middle" '
f'class="lbl">{d.strftime("%b %Y")}</text>'
)
total = points[-1][1]
return f"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" font-family="-apple-system,'Segoe UI',Helvetica,Arial,sans-serif">
<style>
.lbl {{ font-size: 12px; fill: #8b949e; }}
.title {{ font-size: 16px; font-weight: 600; fill: #8b949e; }}
.total {{ font-size: 13px; fill: #8b949e; }}
</style>
<text x="{ml}" y="28" class="title">{repo} star history</text>
<text x="{ml + pw}" y="28" text-anchor="end" class="total">{total:,} stars</text>
{"".join(grid)}
{"".join(ylabels)}
{"".join(xlabels)}
<path d="{area}" fill="#f4b400" fill-opacity="0.12"/>
<path d="{line}" fill="none" stroke="#f4b400" stroke-width="2.5" stroke-linejoin="round"/>
<circle cx="{x(points[-1][0].timestamp()):.1f}" cy="{y(total):.1f}" r="4" fill="#f4b400"/>
</svg>
"""
def main() -> int:
if len(sys.argv) != 3:
print(__doc__)
return 2
repo, out = sys.argv[1], sys.argv[2]
token = os.environ.get("GITHUB_TOKEN", "")
dates = fetch_star_dates(repo, token)
if not dates:
print("no stargazers found", file=sys.stderr)
return 1
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
svg = render_svg(downsample(dates), repo)
with open(out, "w") as f:
f.write(svg)
print(f"wrote {out} ({len(dates):,} stars)")
return 0
if __name__ == "__main__":
sys.exit(main())
+98
View File
@@ -0,0 +1,98 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"
- run: uv sync --group dev
- name: Ruff check
run: uv run ruff check src/ tests/
- name: Ruff format
run: uv run ruff format --check src/ tests/
mypy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"
- run: uv sync --group dev
- name: Mypy
run: uv run mypy src/
pytest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"
- run: uv sync --group dev
- name: NLTK data (punkt_tab for sent_tokenize)
run: uv run python -c "import nltk; nltk.download('punkt_tab')"
- name: Pytest
run: uv run pytest tests/ -x -q
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7.0.0
- uses: astral-sh/setup-uv@v8.2.0
with:
version: "latest"
- name: Build distributions
run: uv build
- name: Check package metadata
run: uvx twine check --strict dist/*
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/*
install-smoke:
name: Install smoke (${{ matrix.label }})
needs: package
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- label: linux
os: ubuntu-latest
- label: macos-arm64
os: macos-14
steps:
- uses: actions/checkout@v7.0.0
- uses: actions/setup-python@v6.3.0
with:
python-version: "3.11"
- uses: actions/download-artifact@v8
with:
name: dist
path: dist
- name: Install wheel with pip
shell: bash
run: |
python -m venv .venv-smoke
source .venv-smoke/bin/activate
python -m pip install --upgrade pip
python -m pip install dist/speech_to_speech-*.whl
python -m pip check
- name: Run installed CLI smoke test
shell: bash
run: |
source .venv-smoke/bin/activate
unset OPENAI_API_KEY
# Validate the installed entry point without loading models or calling OpenAI.
python tests/install_smoke.py
+48
View File
@@ -0,0 +1,48 @@
name: Publish
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
version: "latest"
enable-cache: false
- name: Build distributions
run: uv build
- name: Check package metadata
run: uvx twine check --strict dist/*
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist/*
publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/speech-to-speech/
permissions:
contents: read
id-token: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dist
path: dist
- uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
@@ -0,0 +1,38 @@
name: Update star history chart
on:
workflow_dispatch:
permissions:
contents: write
jobs:
update-chart:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
- name: Generate chart
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 .github/scripts/star_history.py "${{ github.repository }}" assets/star-history.svg
- name: Commit to update branch if changed
env:
BASE_BRANCH: ${{ github.event.repository.default_branch }}
UPDATE_BRANCH: star-history-update-${{ github.run_id }}-${{ github.run_attempt }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add assets/star-history.svg
if ! git diff --cached --quiet; then
git commit -m "chore: update star history chart [skip ci]"
git push origin "HEAD:refs/heads/${UPDATE_BRANCH}"
pr_url="https://github.com/${{ github.repository }}/compare/${BASE_BRANCH}...${UPDATE_BRANCH}?expand=1"
echo "Open a PR: ${pr_url}"
echo "Open a PR: [${UPDATE_BRANCH}](${pr_url})" >> "$GITHUB_STEP_SUMMARY"
else
echo "No changes"
fi