diff --git a/docs/community_growth_20k.md b/docs/community_growth_20k.md index acf67f4be..da53008ec 100644 --- a/docs/community_growth_20k.md +++ b/docs/community_growth_20k.md @@ -39,9 +39,8 @@ As of 2026-06-30, the ecosystem has 34,622 combined GitHub stars, or 3,398 addit Keep this snapshot fresh during weekly planning: ```bash -for repo in modelscope/FunASR FunAudioLLM/Fun-ASR FunAudioLLM/SenseVoice modelscope/FunClip; do - gh api "repos/$repo" --jq '{repo: .full_name, stars: .stargazers_count}' -done | jq -s '{total_stars: (map(.stars) | add), repositories: .}' +python scripts/collect_growth_metrics.py --ecosystem +python scripts/collect_growth_metrics.py --ecosystem --format json ``` ## Workstream 1: Convert first-time visitors diff --git a/scripts/collect_growth_metrics.py b/scripts/collect_growth_metrics.py index 970547cf1..e72737f6f 100755 --- a/scripts/collect_growth_metrics.py +++ b/scripts/collect_growth_metrics.py @@ -14,10 +14,18 @@ import os import sys import urllib.error import urllib.request -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Sequence DEFAULT_REPO = "modelscope/FunASR" +DEFAULT_ECOSYSTEM_REPOS = [ + "modelscope/FunASR", + "FunAudioLLM/Fun-ASR", + "FunAudioLLM/SenseVoice", + "modelscope/FunClip", +] DEFAULT_PACKAGE = "funasr" +DEFAULT_BASELINE_STARS = 31224 +DEFAULT_TARGET_ADDITIONAL_STARS = 20000 def fetch_json(url: str, headers: Optional[Dict[str, str]] = None) -> Any: @@ -44,23 +52,27 @@ def github_headers() -> Dict[str, str]: return headers -def collect_metrics(repo: str, package: str) -> Dict[str, Any]: +def collect_github_repo_metrics(repo: str) -> Dict[str, Any]: owner_repo = repo.strip("/") github = fetch_json(f"https://api.github.com/repos/{owner_repo}", github_headers()) + return { + "repo": owner_repo, + "stars": github.get("stargazers_count"), + "forks": github.get("forks_count"), + "watchers": github.get("subscribers_count"), + "open_issues": github.get("open_issues_count"), + "default_branch": github.get("default_branch"), + "pushed_at": github.get("pushed_at"), + "html_url": github.get("html_url"), + } + + +def collect_metrics(repo: str, package: str) -> Dict[str, Any]: + github = collect_github_repo_metrics(repo) pypi = fetch_json(f"https://pypi.org/pypi/{package}/json", {"User-Agent": "funasr-growth-metrics"}) - latest_release = github.get("pushed_at") metrics = { "collected_at_utc": dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat(), - "github": { - "repo": owner_repo, - "stars": github.get("stargazers_count"), - "forks": github.get("forks_count"), - "watchers": github.get("subscribers_count"), - "open_issues": github.get("open_issues_count"), - "default_branch": github.get("default_branch"), - "pushed_at": latest_release, - "html_url": github.get("html_url"), - }, + "github": github, "pypi": { "package": package, "version": pypi.get("info", {}).get("version"), @@ -71,6 +83,36 @@ def collect_metrics(repo: str, package: str) -> Dict[str, Any]: return metrics +def collect_ecosystem_metrics( + repos: Sequence[str], + package: str, + baseline_stars: int, + target_additional_stars: int, +) -> Dict[str, Any]: + repositories = [collect_github_repo_metrics(repo) for repo in repos] + pypi = fetch_json(f"https://pypi.org/pypi/{package}/json", {"User-Agent": "funasr-growth-metrics"}) + total_stars = sum(repo.get("stars") or 0 for repo in repositories) + added_stars = total_stars - baseline_stars + remaining_to_target = max(target_additional_stars - added_stars, 0) + return { + "collected_at_utc": dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat(), + "ecosystem": { + "repositories": repositories, + "total_stars": total_stars, + "baseline_stars": baseline_stars, + "target_additional_stars": target_additional_stars, + "added_stars": added_stars, + "remaining_to_target": remaining_to_target, + }, + "pypi": { + "package": package, + "version": pypi.get("info", {}).get("version"), + "summary": pypi.get("info", {}).get("summary"), + "project_url": pypi.get("info", {}).get("project_url"), + }, + } + + def format_markdown(metrics: Dict[str, Any], star_goal: int) -> str: github = metrics["github"] pypi = metrics["pypi"] @@ -96,11 +138,63 @@ def format_markdown(metrics: Dict[str, Any], star_goal: int) -> str: return "\n".join(lines) + "\n" +def format_ecosystem_markdown(metrics: Dict[str, Any]) -> str: + ecosystem = metrics["ecosystem"] + pypi = metrics["pypi"] + lines = [ + f"# FunASR Ecosystem Growth Snapshot ({metrics['collected_at_utc']})", + "", + f"- Combined GitHub stars: **{ecosystem['total_stars']:,}**", + f"- Baseline stars: **{ecosystem['baseline_stars']:,}**", + f"- Added since baseline: **{ecosystem['added_stars']:,}** / {ecosystem['target_additional_stars']:,}", + f"- Remaining to target: **{ecosystem['remaining_to_target']:,}**", + f"- PyPI package: **{pypi.get('package')} {pypi.get('version')}**", + "", + "## Repositories", + "", + "| Repository | Stars | Forks | Open issues | Last push |", + "|---|---:|---:|---:|---|", + ] + for repo in ecosystem["repositories"]: + lines.append( + f"| [{repo['repo']}]({repo.get('html_url')}) | " + f"{(repo.get('stars') or 0):,} | " + f"{(repo.get('forks') or 0):,} | " + f"{(repo.get('open_issues') or 0):,} | " + f"`{repo.get('pushed_at')}` |" + ) + lines.extend( + [ + "", + "## Links", + "", + f"- PyPI: {pypi.get('project_url')}", + "- GitHub Pages: https://modelscope.github.io/FunASR/", + "- Trendshift: https://trendshift.io/repositories/10479", + ] + ) + return "\n".join(lines) + "\n" + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Collect FunASR growth metrics.") parser.add_argument("--repo", default=DEFAULT_REPO, help="GitHub repository, e.g. modelscope/FunASR") + parser.add_argument( + "--repos", + nargs="+", + default=DEFAULT_ECOSYSTEM_REPOS, + help="GitHub repositories for --ecosystem mode", + ) + parser.add_argument("--ecosystem", action="store_true", help="Collect the four-repository FunASR ecosystem") parser.add_argument("--pypi-package", default=DEFAULT_PACKAGE, help="PyPI package name") parser.add_argument("--star-goal", type=int, default=20000, help="Target GitHub star count") + parser.add_argument("--baseline-stars", type=int, default=DEFAULT_BASELINE_STARS, help="Ecosystem baseline stars") + parser.add_argument( + "--target-additional-stars", + type=int, + default=DEFAULT_TARGET_ADDITIONAL_STARS, + help="Ecosystem added-star target", + ) parser.add_argument("--format", choices=["markdown", "json"], default="markdown", help="Output format") return parser.parse_args() @@ -108,13 +202,23 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() try: - metrics = collect_metrics(args.repo, args.pypi_package) + if args.ecosystem: + metrics = collect_ecosystem_metrics( + args.repos, + args.pypi_package, + args.baseline_stars, + args.target_additional_stars, + ) + else: + metrics = collect_metrics(args.repo, args.pypi_package) except RuntimeError as exc: print(exc, file=sys.stderr) return 1 if args.format == "json": print(json.dumps(metrics, ensure_ascii=False, indent=2)) + elif args.ecosystem: + print(format_ecosystem_markdown(metrics), end="") else: print(format_markdown(metrics, args.star_goal), end="") return 0 diff --git a/tests/test_collect_growth_metrics.py b/tests/test_collect_growth_metrics.py new file mode 100644 index 000000000..271bc11cb --- /dev/null +++ b/tests/test_collect_growth_metrics.py @@ -0,0 +1,135 @@ +import importlib.util +import io +import json +import sys +from contextlib import redirect_stdout +from pathlib import Path + + +def load_growth_metrics_module(): + script_path = Path(__file__).resolve().parents[1] / "scripts" / "collect_growth_metrics.py" + spec = importlib.util.spec_from_file_location("collect_growth_metrics", script_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_collect_ecosystem_metrics_sums_repositories_and_target_gap(monkeypatch): + module = load_growth_metrics_module() + + github_payloads = { + "modelscope/FunASR": { + "stargazers_count": 18714, + "forks_count": 3100, + "subscribers_count": 210, + "open_issues_count": 3, + "default_branch": "main", + "pushed_at": "2026-06-30T01:48:35Z", + "html_url": "https://github.com/modelscope/FunASR", + }, + "FunAudioLLM/Fun-ASR": { + "stargazers_count": 1318, + "forks_count": 120, + "subscribers_count": 20, + "open_issues_count": 0, + "default_branch": "main", + "pushed_at": "2026-06-29T13:24:05Z", + "html_url": "https://github.com/FunAudioLLM/Fun-ASR", + }, + "FunAudioLLM/SenseVoice": { + "stargazers_count": 8718, + "forks_count": 800, + "subscribers_count": 90, + "open_issues_count": 0, + "default_branch": "main", + "pushed_at": "2026-06-29T13:24:06Z", + "html_url": "https://github.com/FunAudioLLM/SenseVoice", + }, + "modelscope/FunClip": { + "stargazers_count": 5872, + "forks_count": 500, + "subscribers_count": 70, + "open_issues_count": 0, + "default_branch": "main", + "pushed_at": "2026-06-29T20:47:23Z", + "html_url": "https://github.com/modelscope/FunClip", + }, + } + + def fake_fetch_json(url, headers=None): + if url.startswith("https://api.github.com/repos/"): + repo = url.removeprefix("https://api.github.com/repos/") + return github_payloads[repo] + if url == "https://pypi.org/pypi/funasr/json": + return {"info": {"version": "1.3.14", "summary": "FunASR", "project_url": "https://pypi.org/project/funasr/"}} + raise AssertionError(f"unexpected URL: {url}") + + monkeypatch.setattr(module, "fetch_json", fake_fetch_json) + + metrics = module.collect_ecosystem_metrics( + ["modelscope/FunASR", "FunAudioLLM/Fun-ASR", "FunAudioLLM/SenseVoice", "modelscope/FunClip"], + package="funasr", + baseline_stars=31224, + target_additional_stars=20000, + ) + + assert metrics["ecosystem"]["total_stars"] == 34622 + assert metrics["ecosystem"]["baseline_stars"] == 31224 + assert metrics["ecosystem"]["added_stars"] == 3398 + assert metrics["ecosystem"]["remaining_to_target"] == 16602 + assert [repo["repo"] for repo in metrics["ecosystem"]["repositories"]] == [ + "modelscope/FunASR", + "FunAudioLLM/Fun-ASR", + "FunAudioLLM/SenseVoice", + "modelscope/FunClip", + ] + + +def test_main_outputs_ecosystem_json(monkeypatch): + module = load_growth_metrics_module() + + repo_stars = { + "modelscope/FunASR": 18714, + "FunAudioLLM/Fun-ASR": 1318, + "FunAudioLLM/SenseVoice": 8718, + "modelscope/FunClip": 5872, + } + + def fake_fetch_json(url, headers=None): + if url.startswith("https://api.github.com/repos/"): + repo = url.removeprefix("https://api.github.com/repos/") + return { + "stargazers_count": repo_stars[repo], + "forks_count": 0, + "subscribers_count": 0, + "open_issues_count": 0, + "default_branch": "main", + "pushed_at": "2026-06-30T00:00:00Z", + "html_url": f"https://github.com/{repo}", + } + if url == "https://pypi.org/pypi/funasr/json": + return {"info": {"version": "1.3.14", "summary": "FunASR", "project_url": "https://pypi.org/project/funasr/"}} + raise AssertionError(f"unexpected URL: {url}") + + monkeypatch.setattr(module, "fetch_json", fake_fetch_json) + monkeypatch.setattr( + sys, + "argv", + [ + "collect_growth_metrics.py", + "--ecosystem", + "--format", + "json", + "--baseline-stars", + "31224", + "--target-additional-stars", + "20000", + ], + ) + + with redirect_stdout(io.StringIO()) as stdout: + assert module.main() == 0 + + payload = json.loads(stdout.getvalue()) + assert payload["ecosystem"]["total_stars"] == 34622 + assert payload["ecosystem"]["remaining_to_target"] == 16602