mahwizzzz commited on
Commit
cac0e06
·
verified ·
1 Parent(s): c42e020

Load any Canopy variant from the Hub or from GitHub

Browse files
README.md CHANGED
@@ -49,27 +49,38 @@ One block is half SwiGLU, rotary attention, a squeeze-excite depthwise convoluti
49
 
50
  ## Use
51
 
52
- The model repository is private. Log in once with a token that can read `ProximaAI/Canopy-M`.
 
 
53
 
54
  ```bash
55
  pip install torch torchaudio soundfile numpy pyyaml huggingface_hub
56
  hf auth login
57
  hf download ProximaAI/Canopy-M --local-dir Canopy-M
58
- python Canopy-M/canopy_m.py clip.wav --language urd
59
- python Canopy-M/canopy_m.py clip.wav --language snd --device cpu
60
  ```
61
 
62
- `--language` accepts `urd`, `ur`, `urdu`, `snd`, `sd`, or `sindhi`. That selects the language embedding and keeps the CTC output inside that language’s letters. Decoding is greedy CTC, the same setup as the table above.
 
 
 
 
 
 
 
 
63
 
64
  ```python
65
- from canopy_m import CanopyM
66
 
67
- asr = CanopyM.from_pretrained("ProximaAI/Canopy-M") # cuda when available, otherwise cpu
 
68
  print(asr.transcribe("clip.wav", language="urd"))
69
  print(asr.transcribe("clip.wav", language="snd"))
70
  ```
71
 
72
- Run that from the `Canopy-M` directory, or add it to `PYTHONPATH`. `canopy_m.py` and the `canopy` package are both in this repository, next to `canopy-m.pt`. A 16 kHz mono file is the expected input. Other sample rates are resampled. Files longer than 30 seconds are transcribed in chunks.
73
 
74
  ## Training data
75
 
 
49
 
50
  ## Use
51
 
52
+ Each Canopy variant is its own Hub repository. This one is `canopy-m` (`ProximaAI/Canopy-M`). The same loader accepts a short name or any `org/repo` id, so the next checkpoint does not need a new API. Log in once with a token that can read the repo.
53
+
54
+ From this snapshot, without GitHub:
55
 
56
  ```bash
57
  pip install torch torchaudio soundfile numpy pyyaml huggingface_hub
58
  hf auth login
59
  hf download ProximaAI/Canopy-M --local-dir Canopy-M
60
+ python Canopy-M/canopy_m.py clip.wav --model canopy-m --language urd
61
+ python Canopy-M/canopy_m.py clip.wav --model ProximaAI/Canopy-M --language snd --device cpu
62
  ```
63
 
64
+ From GitHub, one install for every variant:
65
+
66
+ ```bash
67
+ pip install torch torchaudio soundfile numpy pyyaml huggingface_hub
68
+ pip install "git+https://github.com/mwzkhalil/canopy.git@danish#subdirectory=canopy"
69
+ hf auth login
70
+ canopy-transcribe clip.wav --model canopy-m --language urd
71
+ canopy-transcribe clip.wav --model ProximaAI/Canopy-M --language snd
72
+ ```
73
 
74
  ```python
75
+ from canopy.inference import Canopy
76
 
77
+ asr = Canopy.from_pretrained("canopy-m") # ProximaAI/Canopy-M
78
+ asr = Canopy.from_pretrained("ProximaAI/Canopy-M") # same checkpoint
79
  print(asr.transcribe("clip.wav", language="urd"))
80
  print(asr.transcribe("clip.wav", language="snd"))
81
  ```
82
 
83
+ `language` accepts `urd` / `ur` / `urdu` and `snd` / `sd` / `sindhi` for this checkpoint. A future repo is loaded with its own id, for example `Canopy.from_pretrained("ProximaAI/Canopy-S")`, once that repository exists. If a repository contains more than one `.pt` file, pass `filename=`. Decoding here is greedy CTC. Audio may be any sample rate; it is resampled to 16 kHz. Files longer than 30 seconds are transcribed in chunks.
84
 
85
  ## Training data
86
 
canopy/inference/__init__.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from canopy.inference.transcriber import Transcriber
2
 
3
- __all__ = ["Transcriber"]
 
1
+ from canopy.inference.api import Canopy
2
  from canopy.inference.transcriber import Transcriber
3
 
4
+ __all__ = ["Canopy", "Transcriber"]
canopy/inference/__main__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from canopy.inference.cli import main
2
+
3
+ main()
canopy/inference/api.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """User-facing loader for every Canopy checkpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import torch
8
+
9
+ from canopy.inference.hub import resolve_repo
10
+ from canopy.inference.transcriber import Transcriber
11
+
12
+ LANGUAGE_ALIASES = {"ur": "urd", "urdu": "urd", "sd": "snd", "sindhi": "snd"}
13
+
14
+
15
+ def resolve_language(language: str | None) -> str | None:
16
+ if language is None:
17
+ return None
18
+ return LANGUAGE_ALIASES.get(language.strip().lower(), language.strip().lower())
19
+
20
+
21
+ class Canopy:
22
+ """One interface for every Canopy variant.
23
+
24
+ ``model`` is a short name (``canopy-m``) or a Hub id (``ProximaAI/Canopy-M``).
25
+ A path to a local ``.pt`` file loads that file and does not contact the Hub.
26
+ """
27
+
28
+ def __init__(self, transcriber: Transcriber, model: str):
29
+ self.transcriber = transcriber
30
+ self.model = model
31
+ self.languages = list(transcriber.languages)
32
+
33
+ @classmethod
34
+ def from_pretrained(cls, model: str = "canopy-m", *, filename: str | None = None,
35
+ device: str | None = None, token: str | None = None,
36
+ revision: str | None = None) -> Canopy:
37
+ if device is None:
38
+ device = "cuda" if torch.cuda.is_available() else "cpu"
39
+ path = Path(model)
40
+ if path.is_file():
41
+ asr = Transcriber.from_checkpoint(path, device=device)
42
+ return cls(asr, str(path))
43
+ repo_id = resolve_repo(model)
44
+ asr = Transcriber.from_pretrained(
45
+ repo_id, filename=filename, device=device, token=token, revision=revision,
46
+ )
47
+ return cls(asr, repo_id)
48
+
49
+ def transcribe(self, audio, language: str | None = "urd", decoder: str = "greedy", **kw) -> str:
50
+ code = resolve_language(language)
51
+ if code is not None and self.languages and code not in self.languages:
52
+ known = ", ".join(self.languages)
53
+ raise ValueError(f"{self.model} was trained on {known}, not {language!r}")
54
+ return self.transcriber.transcribe(audio, language=code, decoder=decoder, **kw)
canopy/inference/cli.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transcribe with any published Canopy checkpoint.
2
+
3
+ GitHub install (one package, every variant):
4
+
5
+ pip install torch torchaudio soundfile numpy pyyaml huggingface_hub
6
+ pip install "git+https://github.com/mwzkhalil/canopy.git@danish#subdirectory=canopy"
7
+ hf auth login
8
+ canopy-transcribe clip.wav --model canopy-m --language urd
9
+ canopy-transcribe clip.wav --model ProximaAI/Canopy-M --language snd
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+
16
+ from canopy.inference.api import LANGUAGE_ALIASES, Canopy
17
+ from canopy.inference.hub import VARIANTS
18
+
19
+
20
+ def main() -> None:
21
+ names = ", ".join(sorted(VARIANTS))
22
+ ap = argparse.ArgumentParser(description="Transcribe with a Canopy checkpoint")
23
+ ap.add_argument("audio", nargs="+", help="wav, flac, or mp3 paths")
24
+ ap.add_argument("--model", default="canopy-m", help=f"variant name ({names}) or org/repo id")
25
+ ap.add_argument("--filename", default=None, help="checkpoint filename when a repo has several")
26
+ ap.add_argument("--language", required=True, help="ISO code, for example urd or snd")
27
+ ap.add_argument("--device", default=None, help="cpu, cuda, or omit for auto")
28
+ ap.add_argument("--decoder", choices=["greedy", "beam"], default="greedy")
29
+ args = ap.parse_args()
30
+ asr = Canopy.from_pretrained(args.model, filename=args.filename, device=args.device)
31
+ for path in args.audio:
32
+ text = asr.transcribe(path, language=args.language, decoder=args.decoder)
33
+ print(f"{path}\t{text}")
34
+
35
+
36
+ __all__ = ["LANGUAGE_ALIASES", "main"]
canopy/inference/hub.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Resolve a Canopy variant to a Hugging Face repo and checkpoint file.
2
+
3
+ A variant is either a short name from ``VARIANTS`` or a full ``org/repo`` id.
4
+ Add a line to ``VARIANTS`` when a new checkpoint is published. Passing
5
+ ``org/repo`` directly works before that line exists.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ # Short name -> Hub repo. Weights stay on the Hub; this table is only the address.
13
+ VARIANTS: dict[str, str] = {
14
+ "canopy-m": "ProximaAI/Canopy-M",
15
+ }
16
+
17
+
18
+ def resolve_repo(model: str) -> str:
19
+ """Return an ``org/repo`` id. Local checkpoint paths are not repos."""
20
+ key = model.strip()
21
+ if not key:
22
+ raise ValueError("model name is empty")
23
+ if key.lower() in VARIANTS:
24
+ return VARIANTS[key.lower()]
25
+ if "/" in key and not Path(key).exists():
26
+ return key
27
+ known = ", ".join(sorted(VARIANTS))
28
+ raise ValueError(
29
+ f"unknown Canopy variant {model!r}. Use one of: {known}. "
30
+ "Or pass a Hub id such as ProximaAI/Canopy-M."
31
+ )
32
+
33
+
34
+ def checkpoint_name(repo_id: str) -> str:
35
+ """Default weight file: the repo name, lowercased, with a .pt suffix."""
36
+ name = repo_id.split("/")[-1].strip().lower()
37
+ return name if name.endswith(".pt") else f"{name}.pt"
38
+
39
+
40
+ def pick_checkpoint(repo_id: str, files: list[str], filename: str | None = None) -> str:
41
+ """Choose one ``.pt`` at the repo root. ``filename`` wins when it is present."""
42
+ root = [f for f in files if "/" not in f and f.endswith(".pt")]
43
+ if filename:
44
+ if filename not in files and filename not in root:
45
+ raise FileNotFoundError(f"{repo_id} has no file {filename!r}. Root checkpoints: {root or 'none'}")
46
+ return filename
47
+ preferred = checkpoint_name(repo_id)
48
+ if preferred in root:
49
+ return preferred
50
+ if len(root) == 1:
51
+ return root[0]
52
+ raise FileNotFoundError(
53
+ f"{repo_id} has {len(root)} checkpoint files {root}. Pass filename= to choose one."
54
+ )
canopy/inference/transcriber.py CHANGED
@@ -50,14 +50,22 @@ class Transcriber:
50
  return cls(model, _tokenizer_from_checkpoint(ck, tokenizer_dir), device=device, **kw)
51
 
52
  @classmethod
53
- def from_pretrained(cls, repo_id: str = "ProximaAI/Canopy-M", *, filename: str = "canopy-m.pt",
54
  device: str = "cpu", revision: str | None = None, token: str | None = None, **kw) -> Transcriber:
55
- """Download an inference checkpoint from the Hugging Face Hub and load it."""
 
 
 
 
56
  try:
57
- from huggingface_hub import hf_hub_download
58
  except ImportError as exc:
59
  raise ImportError("pip install huggingface_hub") from exc
60
- path = hf_hub_download(repo_id=repo_id, filename=filename, revision=revision, token=token)
 
 
 
 
61
  return cls.from_checkpoint(path, device=device, **kw)
62
 
63
  def attach_lm(self, language: str, lm) -> None:
 
50
  return cls(model, _tokenizer_from_checkpoint(ck, tokenizer_dir), device=device, **kw)
51
 
52
  @classmethod
53
+ def from_pretrained(cls, repo_id: str = "ProximaAI/Canopy-M", *, filename: str | None = None,
54
  device: str = "cpu", revision: str | None = None, token: str | None = None, **kw) -> Transcriber:
55
+ """Download an inference checkpoint from the Hugging Face Hub and load it.
56
+
57
+ ``repo_id`` is an ``org/name`` id. ``filename`` defaults to the single
58
+ ``.pt`` at the repo root, preferring ``<repo-name>.pt``.
59
+ """
60
  try:
61
+ from huggingface_hub import hf_hub_download, list_repo_files
62
  except ImportError as exc:
63
  raise ImportError("pip install huggingface_hub") from exc
64
+ from canopy.inference.hub import pick_checkpoint
65
+
66
+ files = list_repo_files(repo_id, revision=revision, token=token)
67
+ chosen = pick_checkpoint(repo_id, files, filename)
68
+ path = hf_hub_download(repo_id=repo_id, filename=chosen, revision=revision, token=token)
69
  return cls.from_checkpoint(path, device=device, **kw)
70
 
71
  def attach_lm(self, language: str, lm) -> None:
canopy_m.py CHANGED
@@ -1,26 +1,31 @@
1
  #!/usr/bin/env python
2
- """Transcribe Urdu or Sindhi with ProximaAI/Canopy-M.
3
 
4
- Install once:
 
 
 
 
5
 
6
  pip install torch torchaudio soundfile numpy pyyaml huggingface_hub
7
  hf auth login
 
 
8
 
9
- Download this repository (it contains this script and the Canopy code), then:
10
 
11
- hf download ProximaAI/Canopy-M --local-dir Canopy-M
12
- python Canopy-M/canopy_m.py clip.wav --language urd
13
- python Canopy-M/canopy_m.py clip.wav --language snd --device cpu
 
 
14
 
15
- From Python, with this directory on the path:
16
 
17
- from canopy_m import CanopyM
18
- asr = CanopyM.from_pretrained("ProximaAI/Canopy-M")
 
19
  print(asr.transcribe("clip.wav", language="urd"))
20
- print(asr.transcribe("clip.wav", language="sindhi"))
21
-
22
- ``language`` is ``urd`` or ``snd``. ``ur`` / ``urdu`` and ``sd`` / ``sindhi`` are accepted.
23
- The published scores use greedy CTC with the language id on.
24
  """
25
 
26
  from __future__ import annotations
@@ -30,58 +35,36 @@ import sys
30
  from pathlib import Path
31
 
32
  _HERE = Path(__file__).resolve().parent
33
- for _root in (_HERE, _HERE.parent):
34
- if (_root / "canopy" / "__init__.py").is_file():
35
- sys.path.insert(0, str(_root))
36
- break
37
-
38
- import torch
39
-
40
- from canopy.inference.transcriber import Transcriber
41
-
42
- REPO_ID = "ProximaAI/Canopy-M"
43
- ALIASES = {"ur": "urd", "urdu": "urd", "urd": "urd", "sd": "snd", "sindhi": "snd", "snd": "snd"}
44
-
45
-
46
- def resolve_language(language: str) -> str:
47
- key = language.strip().lower()
48
- if key not in ALIASES:
49
- known = "urd, snd (also ur, urdu, sd, sindhi)"
50
- raise ValueError(f"unknown language {language!r}. Use one of: {known}")
51
- return ALIASES[key]
52
-
53
-
54
- class CanopyM:
55
- """Urdu and Sindhi CTC recogniser backed by the Canopy-M checkpoint."""
56
-
57
- def __init__(self, transcriber: Transcriber):
58
- self.transcriber = transcriber
59
 
60
- @classmethod
61
- def from_pretrained(cls, repo_id: str = REPO_ID, *, device: str | None = None, token: str | None = None) -> CanopyM:
62
- if device is None:
63
- device = "cuda" if torch.cuda.is_available() else "cpu"
64
- local = _HERE / "canopy-m.pt"
65
- if local.is_file():
66
- asr = Transcriber.from_checkpoint(local, device=device)
67
- else:
68
- asr = Transcriber.from_pretrained(repo_id, device=device, token=token)
69
- return cls(asr)
70
 
71
- def transcribe(self, audio, language: str = "urd", decoder: str = "greedy") -> str:
72
- return self.transcriber.transcribe(audio, language=resolve_language(language), decoder=decoder)
 
 
 
 
 
 
 
 
 
 
 
73
 
74
 
75
  def main() -> None:
76
- ap = argparse.ArgumentParser(description="Transcribe with ProximaAI/Canopy-M")
 
77
  ap.add_argument("audio", nargs="+", help="wav, flac, or mp3 paths")
78
- ap.add_argument("--language", required=True, help="urd or snd")
 
 
79
  ap.add_argument("--device", default=None, help="cpu, cuda, or omit for auto")
80
- ap.add_argument("--repo", default=REPO_ID)
81
  args = ap.parse_args()
82
- asr = CanopyM.from_pretrained(args.repo, device=args.device)
83
  for path in args.audio:
84
- print(f"{path}\t{asr.transcribe(path, language=args.language)}")
85
 
86
 
87
  if __name__ == "__main__":
 
1
  #!/usr/bin/env python
2
+ """Transcribe with any Canopy variant.
3
 
4
+ The weights for each variant live in their own Hugging Face repo. This script
5
+ is the same for all of them. The code can come from this model snapshot or
6
+ from GitHub.
7
+
8
+ Snapshot (no GitHub):
9
 
10
  pip install torch torchaudio soundfile numpy pyyaml huggingface_hub
11
  hf auth login
12
+ hf download ProximaAI/Canopy-M --local-dir Canopy-M
13
+ python Canopy-M/canopy_m.py clip.wav --model canopy-m --language urd
14
 
15
+ GitHub (one install, then any Hub repo):
16
 
17
+ pip install "git+https://github.com/mwzkhalil/canopy.git@danish#subdirectory=canopy"
18
+ pip install huggingface_hub
19
+ hf auth login
20
+ canopy-transcribe clip.wav --model canopy-m --language urd
21
+ canopy-transcribe clip.wav --model ProximaAI/Canopy-M --language snd
22
 
23
+ Python:
24
 
25
+ from canopy.inference import Canopy
26
+ asr = Canopy.from_pretrained("canopy-m")
27
+ asr = Canopy.from_pretrained("ProximaAI/Canopy-M")
28
  print(asr.transcribe("clip.wav", language="urd"))
 
 
 
 
29
  """
30
 
31
  from __future__ import annotations
 
35
  from pathlib import Path
36
 
37
  _HERE = Path(__file__).resolve().parent
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ def _load_api():
41
+ try:
42
+ from canopy.inference.api import Canopy
43
+ from canopy.inference.hub import VARIANTS
44
+ return Canopy, VARIANTS
45
+ except ImportError:
46
+ for root in (_HERE, _HERE.parent):
47
+ if (root / "canopy" / "__init__.py").is_file():
48
+ sys.path.insert(0, str(root))
49
+ break
50
+ from canopy.inference.api import Canopy
51
+ from canopy.inference.hub import VARIANTS
52
+ return Canopy, VARIANTS
53
 
54
 
55
  def main() -> None:
56
+ Canopy, variants = _load_api()
57
+ ap = argparse.ArgumentParser(description="Transcribe with a Canopy checkpoint")
58
  ap.add_argument("audio", nargs="+", help="wav, flac, or mp3 paths")
59
+ ap.add_argument("--model", default="canopy-m", help="variant name or org/repo. Known: " + ", ".join(sorted(variants)))
60
+ ap.add_argument("--filename", default=None, help="checkpoint file when a repo contains several")
61
+ ap.add_argument("--language", required=True, help="language code, for example urd or snd")
62
  ap.add_argument("--device", default=None, help="cpu, cuda, or omit for auto")
63
+ ap.add_argument("--decoder", choices=["greedy", "beam"], default="greedy")
64
  args = ap.parse_args()
65
+ asr = Canopy.from_pretrained(args.model, filename=args.filename, device=args.device)
66
  for path in args.audio:
67
+ print(f"{path}\t{asr.transcribe(path, language=args.language, decoder=args.decoder)}")
68
 
69
 
70
  if __name__ == "__main__":