Skip to content

Commit 6f29319

Browse files
authored
release: prepare FunClip v2.1.0 (#194)
Add versioned, reproducible source archives and a retry-safe GitHub Release workflow with bilingual download routes.
1 parent dea4b40 commit 6f29319

7 files changed

Lines changed: 545 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags: ["v*.*.*"]
6+
7+
permissions:
8+
contents: write
9+
10+
concurrency:
11+
group: release-${{ github.ref }}
12+
cancel-in-progress: false
13+
14+
jobs:
15+
release:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Check out tagged source
19+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
25+
with:
26+
python-version: "3.12"
27+
28+
- name: Install release-test dependency
29+
run: python -m pip install --disable-pip-version-check pytest==8.3.5
30+
31+
- name: Verify tag and release notes
32+
shell: bash
33+
run: |
34+
version="$(<VERSION)"
35+
test "$GITHUB_REF_NAME" = "v${version}"
36+
test -f "docs/releases/${GITHUB_REF_NAME}.md"
37+
38+
- name: Test release contract
39+
run: python -m pytest -q tests/test_release_contract.py
40+
41+
- name: Build release archives
42+
run: python scripts/build_release_assets.py --ref "$GITHUB_SHA" --output-dir dist
43+
44+
- name: Publish GitHub release
45+
env:
46+
GH_TOKEN: ${{ github.token }}
47+
shell: bash
48+
run: |
49+
set -u
50+
release_assets=(dist/FunClip-*.tar.gz dist/FunClip-*.zip dist/SHA256SUMS)
51+
52+
if ! release_state="$(
53+
gh release view "$GITHUB_REF_NAME" \
54+
--repo "$GITHUB_REPOSITORY" \
55+
--json apiUrl,isDraft,isImmutable 2>/dev/null
56+
)"; then
57+
gh release create "$GITHUB_REF_NAME" \
58+
--repo "$GITHUB_REPOSITORY" \
59+
--verify-tag \
60+
--draft \
61+
--title "FunClip ${GITHUB_REF_NAME}" \
62+
--notes-file "docs/releases/${GITHUB_REF_NAME}.md"
63+
release_state="$(
64+
gh release view "$GITHUB_REF_NAME" \
65+
--repo "$GITHUB_REPOSITORY" \
66+
--json apiUrl,isDraft,isImmutable
67+
)"
68+
fi
69+
70+
release_api="$(jq -r '.apiUrl' <<<"$release_state")"
71+
draft="$(jq -r '.isDraft' <<<"$release_state")"
72+
immutable="$(jq -r '.isImmutable' <<<"$release_state")"
73+
74+
verify_release_assets() {
75+
local actual expected
76+
expected="$(
77+
for asset in "${release_assets[@]}"; do
78+
printf '%s\t%s\tsha256:%s\n' \
79+
"$(basename "$asset")" \
80+
"$(wc -c < "$asset")" \
81+
"$(sha256sum "$asset" | cut -d ' ' -f 1)"
82+
done | sort
83+
)"
84+
actual="$(
85+
gh api "$release_api" \
86+
--jq '.assets[] | [.name, (.size | tostring), .digest] | @tsv' | sort
87+
)"
88+
if [[ "$actual" != "$expected" ]]; then
89+
printf 'Expected release assets:\n%s\nActual release assets:\n%s\n' \
90+
"$expected" "$actual" >&2
91+
return 1
92+
fi
93+
}
94+
95+
if [[ "$draft" == "false" ]]; then
96+
verify_release_assets
97+
echo "Release is already published and verified (immutable=${immutable})."
98+
exit 0
99+
fi
100+
101+
gh release edit "$GITHUB_REF_NAME" \
102+
--repo "$GITHUB_REPOSITORY" \
103+
--draft \
104+
--title "FunClip ${GITHUB_REF_NAME}" \
105+
--notes-file "docs/releases/${GITHUB_REF_NAME}.md"
106+
gh release upload "$GITHUB_REF_NAME" \
107+
--repo "$GITHUB_REPOSITORY" \
108+
--clobber \
109+
"${release_assets[@]}"
110+
111+
verified=false
112+
for _attempt in 1 2 3 4 5; do
113+
if verify_release_assets; then
114+
verified=true
115+
break
116+
fi
117+
sleep 2
118+
done
119+
test "$verified" = true
120+
121+
gh release edit "$GITHUB_REF_NAME" \
122+
--repo "$GITHUB_REPOSITORY" \
123+
--draft=false \
124+
--latest
125+
test "$(
126+
gh release view "$GITHUB_REF_NAME" \
127+
--repo "$GITHUB_REPOSITORY" \
128+
--json isDraft \
129+
--jq '.isDraft'
130+
)" = false

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
<a name="What's New"></a>
3636
## What's New🚀
37+
- 2026/07/24 [FunClip v2.1.0](https://github.com/modelscope/FunClip/releases/tag/v2.1.0) is the first versioned GitHub release. It packages the current Fun-ASR-Nano, SenseVoice, Paraformer, and LLM-assisted clipping application as checksum-protected source archives for a stable rollback point.
3738
- 2026/05/20 FunClip now supports [Fun-ASR-Nano](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512) and [SenseVoice](https://huggingface.co/FunAudioLLM/SenseVoiceSmall) models. The `fun-asr-nano` option loads the flagship Fun-ASR-Nano-2512 checkpoint for Mandarin, English, Japanese, 7 Chinese dialect groups, and 26 regional accents; it does not load the separate 31-language Fun-ASR-MLT-Nano-2512 checkpoint. SenseVoice adds emotion recognition and audio event detection. Run `python funclip/launch.py -m fun-asr-nano` or `-m sensevoice` to try. For precise text-based clipping, use Paraformer because the released Nano checkpoint does not provide reliable character-level timestamps.
3839
- 2024/06/12 FunClip now supports recognizing and clipping English audio files. Run `python funclip/launch.py -l en` to try.
3940
- 🔥2024/05/13 FunClip v2.0.0 now supports smart clipping with large language models, integrating models from the qwen series, GPT series, etc., providing default prompts. You can also explore and share tips for setting prompts, the usage is as follows:
@@ -74,6 +75,8 @@ cd FunClip
7475
pip install -r ./requirements.txt
7576
```
7677

78+
For a versioned snapshot, download [FunClip-2.1.0.tar.gz](https://github.com/modelscope/FunClip/releases/download/v2.1.0/FunClip-2.1.0.tar.gz) or [FunClip-2.1.0.zip](https://github.com/modelscope/FunClip/releases/download/v2.1.0/FunClip-2.1.0.zip), then verify it with the published [SHA256SUMS](https://github.com/modelscope/FunClip/releases/download/v2.1.0/SHA256SUMS). Model weights are downloaded separately when FunClip starts and are not included in these source archives.
79+
7780
FunClip's Fun-ASR-Nano, SenseVoice, and subtitle compatibility paths require `funasr>=1.3.29`. This release returns every SenseVoice VAD region through `sentence_info` when token timestamps are unavailable, so clipping and subtitle clients receive segment boundaries instead of an empty timeline. It also includes the real-time final-text and short-tail fixes from 1.3.28. If you installed FunClip before this requirement was updated, run `pip install -U "funasr>=1.3.29"` before starting the Gradio service. [Release notes](https://github.com/modelscope/FunASR/releases/tag/v1.3.29) · [PyPI](https://pypi.org/project/funasr/1.3.29/)
7881

7982
### imagemagick install (Optional)

README_zh.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<a name="近期更新"></a>
3737
## 近期更新🚀
3838

39+
- 2026/07/24 [FunClip v2.1.0](https://github.com/modelscope/FunClip/releases/tag/v2.1.0) 是首个带版本号的 GitHub Release,将当前支持 Fun-ASR-Nano、SenseVoice、Paraformer 与大模型智能剪辑的应用打包为带 SHA-256 校验的源码归档,提供稳定的下载与回退节点。
3940
- 2026/05/20 FunClip 现在支持 [Fun-ASR-Nano](https://huggingface.co/FunAudioLLM/Fun-ASR-Nano-2512)[SenseVoice](https://huggingface.co/FunAudioLLM/SenseVoiceSmall) 模型。`fun-asr-nano` 选项加载旗舰版 Fun-ASR-Nano-2512,支持普通话、英语、日语、7 类中文方言和 26 种地域口音;该选项不会加载独立的 31 语种 Fun-ASR-MLT-Nano-2512。SenseVoice 支持多语种识别,并额外输出情绪识别与音频事件检测标签。可通过 `python funclip/launch.py -m fun-asr-nano``python funclip/launch.py -m sensevoice` 启动体验。需要精确按文本裁剪时请使用 Paraformer,因为当前发布的 Nano checkpoint 不提供可靠的字符级时间戳。
4041
- 2024/06/12 FunClip现在支持识别与裁剪英文视频,通过`python funclip/launch.py -l en`来启动英文版本服务。
4142
- 🔥2024/05/13 FunClip v2.0.0加入大语言模型智能裁剪功能,集成qwen系列,gpt系列等模型,提供默认prompt,您也可以探索并分享prompt的设置技巧,使用方法如下:
@@ -75,6 +76,8 @@ cd FunClip
7576
pip install -r ./requirements.txt
7677
```
7778

79+
如需固定版本,可下载 [FunClip-2.1.0.tar.gz](https://github.com/modelscope/FunClip/releases/download/v2.1.0/FunClip-2.1.0.tar.gz)[FunClip-2.1.0.zip](https://github.com/modelscope/FunClip/releases/download/v2.1.0/FunClip-2.1.0.zip),并使用发布页提供的 [SHA256SUMS](https://github.com/modelscope/FunClip/releases/download/v2.1.0/SHA256SUMS) 校验文件。模型权重会在 FunClip 启动时单独下载,不包含在源码归档中。
80+
7881
FunClip 的 Fun-ASR-Nano、SenseVoice 与字幕兼容路径需要 `funasr>=1.3.29`。当 SenseVoice 没有 token 时间戳时,该版本会通过 `sentence_info` 返回每个 VAD 语音区域,让智能剪辑与字幕客户端获得分段边界,而不再收到空时间线;同时包含 1.3.28 的实时最终文本和短尾语音修复。如果你之前已经安装过 FunClip,请先执行 `pip install -U "funasr>=1.3.29"`,再启动 Gradio 服务。[发布说明](https://github.com/modelscope/FunASR/releases/tag/v1.3.29) · [PyPI](https://pypi.org/project/funasr/1.3.29/)
7982

8083
### 安装imagemagick(可选)

VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
2.1.0

docs/releases/v2.1.0.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# FunClip v2.1.0
2+
3+
FunClip v2.1.0 is the project's first versioned GitHub release and a stable rollback point for the current local video-clipping application.
4+
5+
## Highlights
6+
7+
- Select Paraformer for precise timestamp-based clipping, Fun-ASR-Nano for high-accuracy transcription, or SenseVoice for multilingual ASR with emotion and audio-event tags.
8+
- Use transcript-driven LLM clipping through the supported providers, or optional TwelveLabs Pegasus for content-aware video selection.
9+
- Require `funasr>=1.3.29`, including SenseVoice VAD-region `sentence_info` timestamps plus the real-time final-text and short-tail fixes from 1.3.28.
10+
- Run the same public application in the [FunClip Hugging Face Space](https://huggingface.co/spaces/FunAudioLLM/FunClip).
11+
12+
## Install
13+
14+
Download either source archive below, verify it with `SHA256SUMS`, extract it, and install the declared dependencies:
15+
16+
```bash
17+
sha256sum -c SHA256SUMS --ignore-missing
18+
pip install -r requirements.txt
19+
python funclip/launch.py
20+
```
21+
22+
Windows users can verify a downloaded archive with `Get-FileHash -Algorithm SHA256` and compare the result with `SHA256SUMS`.
23+
24+
## Assets
25+
26+
- `FunClip-2.1.0.tar.gz`: versioned source archive for Linux and macOS workflows.
27+
- `FunClip-2.1.0.zip`: versioned source archive for Windows and general-purpose extraction.
28+
- `SHA256SUMS`: SHA-256 digests for both archives.
29+
30+
The archives contain the tracked FunClip application source, documentation, and dependency manifest. Runtime dependencies and model weights are not bundled; model weights are not bundled because each model retains its own license and distribution terms.
31+
32+
---
33+
34+
FunClip v2.1.0 是项目首个带版本号的 GitHub Release,为当前本地视频剪辑应用提供稳定下载和回退节点。版本支持 Paraformer、Fun-ASR-Nano、SenseVoice、基于字幕的大模型智能剪辑及可选的 TwelveLabs Pegasus,并要求 `funasr>=1.3.29`。下载 `tar.gz``zip` 后,请先使用 `SHA256SUMS` 校验,再安装 `requirements.txt`。归档仅包含 Git 已跟踪的应用源码、文档和依赖清单,不包含运行依赖或模型权重。

scripts/build_release_assets.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import argparse
2+
import hashlib
3+
import os
4+
import re
5+
import subprocess
6+
import tarfile
7+
import tempfile
8+
import zipfile
9+
from pathlib import Path
10+
11+
VERSION_PATTERN = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+$")
12+
REQUIRED_FILES = {"VERSION", "README.md", "requirements.txt", "LICENSE"}
13+
14+
15+
def git_output(repository, *args):
16+
return subprocess.run(
17+
["git", "-C", str(repository), *args],
18+
check=True,
19+
capture_output=True,
20+
text=True,
21+
).stdout
22+
23+
24+
def sha256(path):
25+
digest = hashlib.sha256()
26+
with path.open("rb") as stream:
27+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
28+
digest.update(chunk)
29+
return digest.hexdigest()
30+
31+
32+
def resolve_commit(repository, ref):
33+
try:
34+
return git_output(
35+
repository, "rev-parse", "--verify", f"{ref}^{{commit}}"
36+
).strip()
37+
except subprocess.CalledProcessError as error:
38+
raise RuntimeError("release ref must resolve to a commit") from error
39+
40+
41+
def archive_members(path):
42+
if path.suffix == ".zip":
43+
with zipfile.ZipFile(path) as archive:
44+
return {name for name in archive.namelist() if not name.endswith("/")}
45+
with tarfile.open(path, "r:gz") as archive:
46+
return {member.name for member in archive.getmembers() if member.isfile()}
47+
48+
49+
def validate_members(members, prefix):
50+
if not members or any(not member.startswith(prefix) for member in members):
51+
raise RuntimeError(f"release archive contains a path outside {prefix}")
52+
53+
relative_members = {member.removeprefix(prefix) for member in members}
54+
missing = REQUIRED_FILES - relative_members
55+
if missing:
56+
raise RuntimeError("release archive is missing: " + ", ".join(sorted(missing)))
57+
58+
unsafe = [
59+
member
60+
for member in relative_members
61+
if ".git" in Path(member).parts
62+
or "__pycache__" in Path(member).parts
63+
or member.endswith((".pyc", ".pyo"))
64+
]
65+
if unsafe:
66+
raise RuntimeError(
67+
"release archive contains generated metadata: " + ", ".join(unsafe)
68+
)
69+
70+
71+
def build_release_assets(repository, ref, output_dir):
72+
repository = repository.resolve()
73+
output_dir = output_dir.resolve()
74+
commit = resolve_commit(repository, ref)
75+
version = git_output(repository, "show", f"{commit}:VERSION").strip()
76+
if not VERSION_PATTERN.fullmatch(version):
77+
raise RuntimeError(f"VERSION must be semantic x.y.z, got {version!r}")
78+
79+
prefix = f"FunClip-{version}/"
80+
archive_names = [f"FunClip-{version}.tar.gz", f"FunClip-{version}.zip"]
81+
output_dir.mkdir(parents=True, exist_ok=True)
82+
83+
with tempfile.TemporaryDirectory(
84+
prefix=".funclip-release-", dir=output_dir
85+
) as temporary:
86+
temporary_dir = Path(temporary)
87+
temporary_archives = []
88+
for archive_name in archive_names:
89+
archive_path = temporary_dir / archive_name
90+
archive_format = "zip" if archive_name.endswith(".zip") else "tar.gz"
91+
subprocess.run(
92+
[
93+
"git",
94+
"-C",
95+
str(repository),
96+
"archive",
97+
f"--format={archive_format}",
98+
f"--prefix={prefix}",
99+
f"--output={archive_path}",
100+
commit,
101+
],
102+
check=True,
103+
)
104+
validate_members(archive_members(archive_path), prefix)
105+
temporary_archives.append(archive_path)
106+
107+
first_members = archive_members(temporary_archives[0])
108+
if archive_members(temporary_archives[1]) != first_members:
109+
raise RuntimeError("tar.gz and zip archives contain different files")
110+
111+
checksum_path = temporary_dir / "SHA256SUMS"
112+
checksum_path.write_text(
113+
"".join(
114+
f"{sha256(archive_path)} {archive_path.name}\n"
115+
for archive_path in temporary_archives
116+
),
117+
encoding="utf-8",
118+
)
119+
120+
for path in [*temporary_archives, checksum_path]:
121+
os.replace(path, output_dir / path.name)
122+
123+
return [output_dir / name for name in [*archive_names, "SHA256SUMS"]]
124+
125+
126+
def main():
127+
parser = argparse.ArgumentParser(
128+
description="Build versioned FunClip release archives"
129+
)
130+
parser.add_argument("--repository", type=Path, default=Path.cwd())
131+
parser.add_argument("--ref", default="HEAD")
132+
parser.add_argument("--output-dir", type=Path, default=Path("dist"))
133+
args = parser.parse_args()
134+
135+
for path in build_release_assets(args.repository, args.ref, args.output_dir):
136+
print(f"{sha256(path)} {path.name}")
137+
138+
139+
if __name__ == "__main__":
140+
main()

0 commit comments

Comments
 (0)