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:
+11
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 7
|
||||
groups:
|
||||
actions:
|
||||
patterns: ["*"]
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user