MH0386 commited on
Commit
0a1c297
·
verified ·
1 Parent(s): ed702c2

Upload folder using huggingface_hub

Browse files
.github/actions/uv/action.yml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: 'Setup UV'
2
+ description: 'Reusable set of steps to Setup UV'
3
+ runs:
4
+ using: "composite"
5
+ steps:
6
+ - name: Install uv
7
+ uses: astral-sh/setup-uv@v6
8
+ id: uv
9
+ with:
10
+ enable-cache: true
11
+ cache-dependency-glob: "uv.lock"
12
+ activate-environment: true
13
+ - name: Install the project
14
+ shell: bash
15
+ if: steps.uv.outputs.cache-hit != 'true'
16
+ run: uv sync --frozen
.github/dependabot.yml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ # version: 2
7
+ # updates:
8
+ # - package-ecosystem: "docker"
9
+ # directory: "/"
10
+ # schedule:
11
+ # interval: "weekly"
12
+ # - package-ecosystem: "github-actions"
13
+ # directory: "/"
14
+ # schedule:
15
+ # interval: "weekly"
16
+ # - package-ecosystem: "uv"
17
+ # directory: "/"
18
+ # schedule:
19
+ # interval: "weekly"
.github/hadolint.yml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ ignored:
2
+ - DL3008
3
+
4
+ trustedRegistries:
5
+ - docker.io
6
+ - ghcr.io
.github/mergify.yml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pull_request_rules:
2
+ - name: Automatically merge Renovate PRs
3
+ conditions:
4
+ - author = renovate[bot]
5
+ - check-success = spelling
6
+ - check-success = ruff
7
+ - check-success = pyright
8
+ - check-success = pylint
9
+ actions:
10
+ queue:
11
+ label:
12
+ add:
13
+ - dependencies
14
+ queue_rules:
15
+ - queue_branch_merge_method: fast-forward
16
+ allow_queue_branch_edit: true
17
+ update_method: merge
18
+ name: default
.github/workflows/code_analysis.yml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Code Analysis
2
+ on:
3
+ push:
4
+ pull_request:
5
+ permissions:
6
+ contents: read
7
+ jobs:
8
+ spelling:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Checkout Actions Repository
12
+ uses: actions/checkout@v4
13
+ - name: Setup UV
14
+ uses: ./.github/actions/uv
15
+ - name: Run typos
16
+ run: uv run typos . --config ./pyproject.toml
17
+ ruff:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - name: Checkout Actions Repository
21
+ uses: actions/checkout@v4
22
+ - name: Setup UV
23
+ uses: ./.github/actions/uv
24
+ - name: Run Ruff
25
+ run: uv run ruff check .
26
+ pyright:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - name: Checkout Actions Repository
30
+ uses: actions/checkout@v4
31
+ - name: Setup UV
32
+ uses: ./.github/actions/uv
33
+ - name: Run Pyright
34
+ run: uv run pyright
35
+ pylint:
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - name: Checkout Actions Repository
39
+ uses: actions/checkout@v4
40
+ - name: Setup UV
41
+ uses: ./.github/actions/uv
42
+ - name: Analysing the code with Pylint
43
+ run: uv run pylint $(git ls-files '*.py') --output-format=github --rcfile pyproject.toml
44
+ dockerfile:
45
+ runs-on: ubuntu-latest
46
+ steps:
47
+ - name: Checkout Actions Repository
48
+ uses: actions/checkout@v4
49
+ - uses: hadolint/[email protected]
50
+ with:
51
+ config: ./.github/hadolint.yml
.github/workflows/docker.yml ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker Images
2
+ on:
3
+ workflow_dispatch:
4
+ pull_request:
5
+ push:
6
+ permissions:
7
+ contents: read
8
+ packages: write
9
+ attestations: write
10
+ id-token: write
11
+ jobs:
12
+ check_image:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Checkout repository
16
+ uses: actions/checkout@v4
17
+ - name: Log in to the Container registry
18
+ uses: docker/login-action@v3
19
+ with:
20
+ registry: ghcr.io
21
+ username: ${{ github.actor }}
22
+ password: ${{ secrets.TOKEN_KEY_GITHUB }}
23
+ - name: Validate build configuration
24
+ uses: docker/build-push-action@v6
25
+ with:
26
+ call: check
27
+ build_image:
28
+ needs: check_image
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - name: Checkout repository
32
+ uses: actions/checkout@v4
33
+ - name: Log in to the Container registry
34
+ uses: docker/login-action@v3
35
+ with:
36
+ registry: ghcr.io
37
+ username: ${{ github.actor }}
38
+ password: ${{ secrets.TOKEN_KEY_GITHUB }}
39
+ - name: Set up QEMU
40
+ uses: docker/setup-qemu-action@v3
41
+ - name: Set up Docker Buildx
42
+ uses: docker/setup-buildx-action@v3
43
+ - name: Docker meta
44
+ id: meta
45
+ uses: docker/metadata-action@v5
46
+ with:
47
+ images: ghcr.io/${{ github.repository }}
48
+ tags: type=raw,value=latest,enable={{is_default_branch}}
49
+ - name: Build
50
+ uses: docker/build-push-action@v6
51
+ with:
52
+ cache-from: type=gha
53
+ cache-to: type=gha,mode=max
54
+ push: true
55
+ tags: ${{ steps.meta.outputs.tags }}
56
+ labels: ${{ steps.meta.outputs.labels }}
57
+ annotations: ${{ steps.meta.outputs.annotations }}
58
+ docker_scout:
59
+ needs:
60
+ - build_image
61
+ - check_image
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - name: Docker Scout
65
+ continue-on-error: true
66
+ uses: docker/scout-action@v1
67
+ with:
68
+ command: quickview,cves,recommendations
69
+ dockerhub-user: mh0386
70
+ dockerhub-password: ${{ secrets.TOKEN_KEY_DOCKER }}
71
+ image: ghcr.io/${{ github.repository }}:latest
.github/workflows/github.yaml ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: GitHub Release
2
+ on:
3
+ workflow_dispatch:
4
+ push:
5
+ tags:
6
+ - "[0-9]+.[0-9]+.[0-9]+"
7
+ - "[0-9]+.[0-9]+.[0-9]+a[0-9]+"
8
+ - "[0-9]+.[0-9]+.[0-9]+b[0-9]+"
9
+ - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
10
+ env:
11
+ PACKAGE_NAME: vocalizr
12
+ OWNER: AlphaSphereDotAI
13
+ permissions:
14
+ contents: write
15
+ jobs:
16
+ details:
17
+ runs-on: ubuntu-latest
18
+ outputs:
19
+ new_version: ${{ steps.release.outputs.new_version }}
20
+ tag_name: ${{ steps.release.outputs.tag_name }}
21
+ steps:
22
+ - name: Checkout Code
23
+ uses: actions/[email protected]
24
+ - name: Extract tag and Details
25
+ id: release
26
+ run: |
27
+ if [ "${{ github.ref_type }}" = "tag" ]; then
28
+ TAG_NAME=${GITHUB_REF#refs/tags/}
29
+ NEW_VERSION=$(echo $TAG_NAME | awk -F'-' '{print $1}')
30
+ SUFFIX=$(echo $TAG_NAME | grep -oP '[a-z]+[0-9]+' || echo "")
31
+ echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
32
+ echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT"
33
+ echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
34
+ echo "Version is $NEW_VERSION"
35
+ echo "Suffix is $SUFFIX"
36
+ echo "Tag name is $TAG_NAME"
37
+ else
38
+ echo "No tag found"
39
+ exit 1
40
+ fi
41
+ setup_and_build:
42
+ needs: [details]
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - name: Checkout Code
46
+ uses: actions/[email protected]
47
+ - name: Setup UV
48
+ uses: ./.github/actions/uv
49
+ - name: Update Project Version
50
+ run: uv version ${{ needs.details.outputs.new_version }}
51
+ - name: Build source and wheel distribution
52
+ run: uv build --all-packages
53
+ - name: Upload artifacts
54
+ uses: actions/upload-artifact@v4
55
+ with:
56
+ name: dist
57
+ path: dist/
58
+ github_release:
59
+ name: Create GitHub Release
60
+ needs: [setup_and_build, details]
61
+ runs-on: ubuntu-latest
62
+ steps:
63
+ - name: Checkout Code
64
+ uses: actions/[email protected]
65
+ with:
66
+ fetch-depth: 0
67
+ - name: Download artifacts
68
+ uses: actions/download-artifact@v4
69
+ with:
70
+ name: dist
71
+ path: dist/
72
+ - name: Create GitHub Release
73
+ id: create_release
74
+ env:
75
+ GH_TOKEN: ${{ github.token }}
76
+ run: gh release create ${{ needs.details.outputs.tag_name }} dist/* --title ${{ needs.details.outputs.tag_name }} --generate-notes
.github/workflows/huggingface.yml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Push to HuggingFace
2
+ on:
3
+ push:
4
+ env:
5
+ HF_HUB_ENABLE_HF_TRANSFER: 1
6
+ jobs:
7
+ huggingface:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - name: Checkout
11
+ uses: actions/checkout@v4
12
+ - name: Setup UV
13
+ uses: ./.github/actions/uv
14
+ - name: Remove Unnecessary Files
15
+ run: |
16
+ echo ".github" >> .gitignore
17
+ echo ".idea" >> .gitignore
18
+ echo "docker-bake.hcl" >> .gitignore
19
+ echo "renovate.json" >> .gitignore
20
+ echo ".deepsource.toml" >> .gitignore
21
+ - name: Add to git credentials
22
+ run: git config --global credential.helper store
23
+ - name: Login to HuggingFace Hub
24
+ run: uv run huggingface-cli login --add-to-git-credential --token ${{ secrets.HF_TOKEN }}
25
+ - name: Check if logged in
26
+ run: uv run huggingface-cli whoami
27
+ - name: Upload
28
+ run: uv run huggingface-cli upload AlphaSphereDotAI/Vocalizr . . --repo-type space
29
+ # - name: Minimize uv cache
30
+ # run: uv cache prune --ci
.gitignore CHANGED
@@ -4,7 +4,7 @@ tmp/
4
  .mypy_cache/
5
  **/__pycache__/
6
  .env
7
- .github
8
  .idea
9
  docker-bake.hcl
10
  renovate.json
 
4
  .mypy_cache/
5
  **/__pycache__/
6
  .env
7
+ results/.github
8
  .idea
9
  docker-bake.hcl
10
  renovate.json
src/vocalizr/__init__.py CHANGED
@@ -10,7 +10,7 @@ load_dotenv()
10
 
11
  BASE_DIR: Path = Path(__file__).parent.parent.parent
12
  DEBUG: bool = getenv(key="DEBUG", default="False").lower() == "true"
13
- CHAR_LIMIT: int = int(getenv(key="CHAR_LIMIT", default="5000"))
14
  SERVER_NAME: str = getenv(key="GRADIO_SERVER_NAME", default="localhost")
15
  SERVER_PORT: int = int(getenv(key="GRADIO_SERVER_PORT", default="8080"))
16
  PIPELINE: KPipeline = KPipeline(lang_code="a", repo_id="hexgrad/Kokoro-82M")
 
10
 
11
  BASE_DIR: Path = Path(__file__).parent.parent.parent
12
  DEBUG: bool = getenv(key="DEBUG", default="False").lower() == "true"
13
+ CHAR_LIMIT: int = int(getenv(key="CHAR_LIMIT", default="-1"))
14
  SERVER_NAME: str = getenv(key="GRADIO_SERVER_NAME", default="localhost")
15
  SERVER_PORT: int = int(getenv(key="GRADIO_SERVER_PORT", default="8080"))
16
  PIPELINE: KPipeline = KPipeline(lang_code="a", repo_id="hexgrad/Kokoro-82M")
src/vocalizr/__main__.py CHANGED
@@ -7,7 +7,7 @@ from vocalizr.gui import app_block
7
  def main() -> None:
8
  """Launch the Gradio voice generation web application."""
9
  app: Blocks = app_block()
10
- app.launch(
11
  server_name=SERVER_NAME,
12
  server_port=SERVER_PORT,
13
  debug=DEBUG,
 
7
  def main() -> None:
8
  """Launch the Gradio voice generation web application."""
9
  app: Blocks = app_block()
10
+ app.queue(api_open=True).launch(
11
  server_name=SERVER_NAME,
12
  server_port=SERVER_PORT,
13
  debug=DEBUG,
src/vocalizr/gui.py CHANGED
@@ -10,7 +10,7 @@ from gradio import (
10
  Textbox,
11
  )
12
 
13
- from vocalizr import CHAR_LIMIT, CHOICES, CUDA_AVAILABLE
14
  from vocalizr.model import generate_audio_for_text
15
 
16
 
@@ -24,13 +24,7 @@ def app_block() -> Blocks:
24
  with Column():
25
  text: Textbox = Textbox(
26
  label="Input Text",
27
- info=(
28
- f"""
29
- Up to ~500 characters per Generate,
30
- or {"∞" if CHAR_LIMIT is None else CHAR_LIMIT}
31
- characters per Stream
32
- """
33
- ),
34
  )
35
  with Row():
36
  voice: Dropdown = Dropdown(
@@ -47,7 +41,8 @@ def app_block() -> Blocks:
47
  interactive=CUDA_AVAILABLE,
48
  )
49
  save_file = Checkbox(
50
- label="Save Audio", info="Save audio to local storage"
 
51
  )
52
  speed: Slider = Slider(
53
  minimum=0.5,
@@ -60,13 +55,25 @@ def app_block() -> Blocks:
60
  out_audio: Audio = Audio(
61
  label="Output Audio",
62
  interactive=False,
63
- streaming=False,
64
  autoplay=True,
65
  )
66
- generate_btn: Button = Button("Generate", variant="primary")
67
- generate_btn.click(
 
 
 
 
 
 
 
 
68
  fn=generate_audio_for_text,
69
  inputs=[text, voice, speed, save_file],
70
  outputs=[out_audio],
71
  )
 
 
 
 
72
  return app
 
10
  Textbox,
11
  )
12
 
13
+ from vocalizr import CHOICES, CUDA_AVAILABLE
14
  from vocalizr.model import generate_audio_for_text
15
 
16
 
 
24
  with Column():
25
  text: Textbox = Textbox(
26
  label="Input Text",
27
+ info=("""Enter your text here"""),
 
 
 
 
 
 
28
  )
29
  with Row():
30
  voice: Dropdown = Dropdown(
 
41
  interactive=CUDA_AVAILABLE,
42
  )
43
  save_file = Checkbox(
44
+ label="Save Audio",
45
+ info="Save audio to local storage",
46
  )
47
  speed: Slider = Slider(
48
  minimum=0.5,
 
55
  out_audio: Audio = Audio(
56
  label="Output Audio",
57
  interactive=False,
58
+ streaming=True,
59
  autoplay=True,
60
  )
61
+ with Row():
62
+ stream_btn: Button = Button(
63
+ value="Generate",
64
+ variant="primary",
65
+ )
66
+ stop_btn: Button = Button(
67
+ value="Stop",
68
+ variant="stop",
69
+ )
70
+ stream_event = stream_btn.click(
71
  fn=generate_audio_for_text,
72
  inputs=[text, voice, speed, save_file],
73
  outputs=[out_audio],
74
  )
75
+ stop_btn.click(
76
+ fn=None,
77
+ cancels=stream_event,
78
+ )
79
  return app
src/vocalizr/model.py CHANGED
@@ -1,55 +1,73 @@
1
- from datetime import datetime
2
- from os import makedirs
3
-
4
  from gradio import Error
 
5
  from loguru import logger
6
- from numpy import ndarray
 
7
  from soundfile import write
8
- from torch import Tensor
9
 
10
- from vocalizr import BASE_DIR, CHAR_LIMIT, PIPELINE
11
 
12
 
13
- def save_file_wav(audio: ndarray) -> None:
 
14
  """Save audio data to a WAV file in the 'results' directory.
15
-
16
  Creates a timestamped WAV file in the 'results' directory with
17
  the provided audio data at a fixed sample rate of 24,000 Hz.
18
 
19
- :param audio: Data to save.
20
- :return: None
21
- :raise OSError: If an error occurs while saving the file.
 
 
22
  """
23
- makedirs(name="results", exist_ok=True)
24
- filename: str = (
25
- f"{BASE_DIR}/results/{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.wav"
26
- )
27
  try:
28
- logger.info(f"Saving audio to {filename}")
29
- write(file=filename, data=audio, samplerate=24000)
30
- except OSError as e:
31
- raise OSError(f"Failed to save audio to {filename}: {e}") from e
 
32
 
33
 
 
 
34
  def generate_audio_for_text(
35
- text: str, voice: str = "af_heart", speed: float = 1, save_file: bool = False
36
- ) -> tuple[int, ndarray]:
 
 
 
37
  """Generate audio for the input text.
38
 
39
- :param text: Input text to convert to speech
40
- :param voice: Voice identifier
41
- :param speed: Speech speed multiplier
42
- :param save_file: If to save the audio file to disk.
43
- :return: Tuple containing the audio sample rate and raw audio data.
44
- :raise Error: If an error occurs during generation.
 
 
 
 
 
 
 
45
  """
46
- text = text if CHAR_LIMIT is None else text.strip()[:CHAR_LIMIT]
47
  try:
48
- for _, _, audio in PIPELINE(text, voice, speed):
49
- audio = Tensor(audio).numpy()
50
- if save_file:
51
- save_file_wav(audio=audio)
52
- return 24000, audio
53
- except Error as e:
54
- raise Error(message=str(e)) from e
55
- raise RuntimeError("No audio generated")
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Generator, Literal
 
 
2
  from gradio import Error
3
+ from kokoro import KPipeline
4
  from loguru import logger
5
+ from numpy import float32
6
+ from numpy.typing import NDArray
7
  from soundfile import write
 
8
 
9
+ from vocalizr import CHAR_LIMIT, PIPELINE, AUDIO_FILE_PATH
10
 
11
 
12
+ @logger.catch
13
+ def save_file_wav(audio: NDArray[float32]) -> None:
14
  """Save audio data to a WAV file in the 'results' directory.
 
15
  Creates a timestamped WAV file in the 'results' directory with
16
  the provided audio data at a fixed sample rate of 24,000 Hz.
17
 
18
+ Args:
19
+ audio (NDArray[float32]): raw audio data.
20
+
21
+ Raises:
22
+ RuntimeError: If there are problems with saving the audio file locally.
23
  """
 
 
 
 
24
  try:
25
+ logger.info(f"Saving audio to {AUDIO_FILE_PATH}")
26
+ write(file=AUDIO_FILE_PATH, data=audio, samplerate=24000)
27
+ except Exception as e:
28
+ logger.exception(f"Failed to save audio to {AUDIO_FILE_PATH}: {e}")
29
+ raise RuntimeError(f"Failed to save audio to {AUDIO_FILE_PATH}: {e}") from e
30
 
31
 
32
+ # noinspection PyTypeChecker
33
+ @logger.catch
34
  def generate_audio_for_text(
35
+ text: str,
36
+ voice: str = "af_heart",
37
+ speed: float = 1,
38
+ save_file: bool = False,
39
+ ) -> Generator[tuple[Literal[24000], NDArray[float32]], Any, None]:
40
  """Generate audio for the input text.
41
 
42
+ Args:
43
+ text (str): Input text to convert to speech
44
+ voice (str, optional): Voice identifier. Defaults to "af_heart".
45
+ speed (float, optional): Speech speed. Defaults to 1.
46
+ save_file (bool, optional): If to save the audio file to disk. Defaults to False.
47
+
48
+ Raises:
49
+ Error: If text (str) is empty
50
+ Error: If audio (NDArray[float32]) is str
51
+ Error: If audio (NDArray[float32]) is None
52
+
53
+ Yields:
54
+ Generator[tuple[Literal[24000], NDArray[float32]], Any, None]: Tuple containing the audio sample rate and raw audio data.
55
  """
 
56
  try:
57
+ text = text if CHAR_LIMIT == -1 else text.strip()[:CHAR_LIMIT]
58
+ except Exception as e:
59
+ logger.exception(str(object=e))
60
+ raise Error(message=str(object=e)) from e
61
+ generator: Generator[KPipeline.Result, None, None] = PIPELINE(
62
+ text=text, voice=voice, speed=speed
63
+ )
64
+ logger.info(f"Generating audio for '{text}'")
65
+ for _, _, audio in generator:
66
+ if audio is None or isinstance(audio, str):
67
+ logger.exception(f"Unexpected type (audio): {type(audio)}")
68
+ raise Error(message=f"Unexpected type (audio): {type(audio)}")
69
+ audio_np: NDArray[float32] = audio.numpy()
70
+ if save_file:
71
+ save_file_wav(audio=audio_np)
72
+ logger.info(f"Yielding audio for '{text}'")
73
+ yield 24000, audio_np