This adds: - a VAD node to detect voice within an audio to avoid having too much noise - Bump distil-whisper to whisper-turbo - Use Argotranslate and OPUS-MT for translation - Add example dataflow within the example folder ## Get started ```bash cd examples/translation dora up dora build dataflow_zh_en_terminal.yml dora start dataflow_zh_en_terminal.yml --detach python pretty_print.py ```tags/v0.3.7rc0
| @@ -0,0 +1 @@ | |||
| *.pt | |||
| @@ -0,0 +1,24 @@ | |||
| # Dora argo example | |||
| Make sure to have, dora, pip and cargo installed. | |||
| ```bash | |||
| dora up | |||
| ## For chinese | |||
| dora build dataflow_zh_en_terminal.yml | |||
| dora start dataflow_zh_en_terminal.yml --detach | |||
| python pretty_print.py | |||
| dora stop | |||
| ## For chinese | |||
| dora build dataflow_en_zh_terminal.yml | |||
| dora start dataflow_en_zh_terminal.yml --detach | |||
| python pretty_print.py | |||
| dora stop | |||
| ``` | |||
| @@ -0,0 +1,45 @@ | |||
| nodes: | |||
| - id: dora-microphone | |||
| build: pip install -e ../../node-hub/dora-microphone | |||
| path: dora-microphone | |||
| outputs: | |||
| - audio | |||
| - id: dora-vad | |||
| build: pip install -e ../../node-hub/dora-vad | |||
| path: dora-vad | |||
| inputs: | |||
| audio: dora-microphone/audio | |||
| outputs: | |||
| - audio | |||
| - id: dora-distil-whisper | |||
| build: pip install -e ../../node-hub/dora-distil-whisper | |||
| path: dora-distil-whisper | |||
| inputs: | |||
| input: dora-vad/audio | |||
| outputs: | |||
| - text | |||
| env: | |||
| TARGET_LANGUAGE: english | |||
| TRANSLATE: false | |||
| - id: dora-argotranslate | |||
| build: pip install -e ../../node-hub/dora-argotranslate | |||
| path: dora-argotranslate | |||
| inputs: | |||
| text: dora-distil-whisper/text | |||
| outputs: | |||
| - text | |||
| env: | |||
| SOURCE_LANGUAGE: en | |||
| TARGET_LANGUAGE: zh | |||
| - id: dora-rerun | |||
| build: cargo build -p dora-rerun --release | |||
| path: dora-rerun | |||
| inputs: | |||
| text: dora-argotranslate/text | |||
| env: | |||
| IMAGE_WIDTH: 640 | |||
| IMAGE_HEIGHT: 480 | |||
| @@ -0,0 +1,45 @@ | |||
| nodes: | |||
| - id: dora-microphone | |||
| build: pip install -e ../../node-hub/dora-microphone | |||
| path: dora-microphone | |||
| outputs: | |||
| - audio | |||
| - id: dora-vad | |||
| build: pip install -e ../../node-hub/dora-vad | |||
| path: dora-vad | |||
| inputs: | |||
| audio: dora-microphone/audio | |||
| outputs: | |||
| - audio | |||
| - id: dora-distil-whisper | |||
| build: pip install -e ../../node-hub/dora-distil-whisper | |||
| path: dora-distil-whisper | |||
| inputs: | |||
| input: dora-vad/audio | |||
| outputs: | |||
| - text | |||
| env: | |||
| TARGET_LANGUAGE: english | |||
| TRANSLATE: false | |||
| - id: dora-opus | |||
| build: pip install -e ../../node-hub/dora-opus | |||
| path: dora-opus | |||
| inputs: | |||
| text: dora-distil-whisper/text | |||
| outputs: | |||
| - text | |||
| env: | |||
| SOURCE_LANGUAGE: en | |||
| TARGET_LANGUAGE: zh | |||
| - id: pretty-print | |||
| path: dynamic | |||
| inputs: | |||
| translated_text: dora-opus/text | |||
| original_text: dora-distil-whisper/text | |||
| env: | |||
| IMAGE_WIDTH: 640 | |||
| IMAGE_HEIGHT: 480 | |||
| @@ -0,0 +1,46 @@ | |||
| nodes: | |||
| - id: dora-microphone | |||
| build: pip install -e ../../node-hub/dora-microphone | |||
| path: dora-microphone | |||
| outputs: | |||
| - audio | |||
| - id: dora-vad | |||
| build: pip install -e ../../node-hub/dora-vad | |||
| path: dora-vad | |||
| inputs: | |||
| audio: dora-microphone/audio | |||
| outputs: | |||
| - audio | |||
| - id: dora-distil-whisper | |||
| build: pip install -e ../../node-hub/dora-distil-whisper | |||
| path: dora-distil-whisper | |||
| inputs: | |||
| input: dora-vad/audio | |||
| outputs: | |||
| - text | |||
| env: | |||
| TARGET_LANGUAGE: english | |||
| TRANSLATE: false | |||
| - id: dora-argotranslate | |||
| build: pip install -e ../../node-hub/dora-argotranslate | |||
| path: dora-argotranslate | |||
| inputs: | |||
| text: dora-distil-whisper/text | |||
| outputs: | |||
| - text | |||
| env: | |||
| SOURCE_LANGUAGE: en | |||
| TARGET_LANGUAGE: zh | |||
| - id: pretty-print | |||
| build: cargo build -p dora-rerun --release | |||
| path: dynamic | |||
| inputs: | |||
| translated_text: dora-argotranslate/text | |||
| original_text: dora-distil-whisper/text | |||
| env: | |||
| IMAGE_WIDTH: 640 | |||
| IMAGE_HEIGHT: 480 | |||
| @@ -0,0 +1,46 @@ | |||
| nodes: | |||
| - id: dora-microphone | |||
| build: pip install -e ../../node-hub/dora-microphone | |||
| path: dora-microphone | |||
| outputs: | |||
| - audio | |||
| - id: dora-vad | |||
| build: pip install -e ../../node-hub/dora-vad | |||
| path: dora-vad | |||
| inputs: | |||
| audio: dora-microphone/audio | |||
| outputs: | |||
| - audio | |||
| - id: dora-distil-whisper | |||
| build: pip install -e ../../node-hub/dora-distil-whisper | |||
| path: dora-distil-whisper | |||
| inputs: | |||
| input: dora-vad/audio | |||
| outputs: | |||
| - text | |||
| env: | |||
| TARGET_LANGUAGE: french | |||
| TRANSLATE: false | |||
| - id: dora-argotranslate | |||
| build: pip install -e ../../node-hub/dora-argotranslate | |||
| path: dora-argotranslate | |||
| inputs: | |||
| text: dora-distil-whisper/text | |||
| outputs: | |||
| - text | |||
| env: | |||
| SOURCE_LANGUAGE: fr | |||
| TARGET_LANGUAGE: en | |||
| - id: dora-rerun | |||
| build: cargo build -p dora-rerun --release | |||
| path: dora-rerun | |||
| inputs: | |||
| translated_text: dora-argotranslate/text | |||
| original_text: dora-distil-whisper/text | |||
| env: | |||
| IMAGE_WIDTH: 640 | |||
| IMAGE_HEIGHT: 480 | |||
| @@ -0,0 +1,46 @@ | |||
| nodes: | |||
| - id: dora-microphone | |||
| build: pip install -e ../../node-hub/dora-microphone | |||
| path: dora-microphone | |||
| outputs: | |||
| - audio | |||
| - id: dora-vad | |||
| build: pip install -e ../../node-hub/dora-vad | |||
| path: dora-vad | |||
| inputs: | |||
| audio: dora-microphone/audio | |||
| outputs: | |||
| - audio | |||
| - id: dora-distil-whisper | |||
| build: pip install -e ../../node-hub/dora-distil-whisper | |||
| path: dora-distil-whisper | |||
| inputs: | |||
| input: dora-vad/audio | |||
| outputs: | |||
| - text | |||
| env: | |||
| TARGET_LANGUAGE: chinese | |||
| TRANSLATE: false | |||
| - id: dora-opus | |||
| build: pip install -e ../../node-hub/dora-opus | |||
| path: dora-opus | |||
| inputs: | |||
| text: dora-distil-whisper/text | |||
| outputs: | |||
| - text | |||
| env: | |||
| SOURCE_LANGUAGE: zh | |||
| TARGET_LANGUAGE: en | |||
| - id: plot | |||
| build: cargo build -p dora-rerun --release | |||
| path: dora-rerun | |||
| inputs: | |||
| translated_text: dora-opus/text | |||
| original_text: dora-distil-whisper/text | |||
| env: | |||
| IMAGE_WIDTH: 640 | |||
| IMAGE_HEIGHT: 480 | |||
| @@ -0,0 +1,42 @@ | |||
| nodes: | |||
| - id: dora-microphone | |||
| build: pip install -e ../../node-hub/dora-microphone | |||
| path: dora-microphone | |||
| outputs: | |||
| - audio | |||
| - id: dora-vad | |||
| build: pip install -e ../../node-hub/dora-vad | |||
| path: dora-vad | |||
| inputs: | |||
| audio: dora-microphone/audio | |||
| outputs: | |||
| - audio | |||
| - id: dora-distil-whisper | |||
| build: pip install -e ../../node-hub/dora-distil-whisper | |||
| path: dora-distil-whisper | |||
| inputs: | |||
| input: dora-vad/audio | |||
| outputs: | |||
| - text | |||
| env: | |||
| TARGET_LANGUAGE: chinese | |||
| TRANSLATE: false | |||
| - id: dora-opus | |||
| build: pip install -e ../../node-hub/dora-opus | |||
| path: dora-opus | |||
| inputs: | |||
| text: dora-distil-whisper/text | |||
| outputs: | |||
| - text | |||
| env: | |||
| SOURCE_LANGUAGE: zh | |||
| TARGET_LANGUAGE: en | |||
| - id: pretty-print | |||
| path: dynamic | |||
| inputs: | |||
| translated_text: dora-opus/text | |||
| original_text: dora-distil-whisper/text | |||
| @@ -0,0 +1,46 @@ | |||
| import os | |||
| import shutil | |||
| def clear_screen(): | |||
| # Clear the screen based on the operating system | |||
| os.system("cls" if os.name == "nt" else "clear") | |||
| def print_centered(texts): | |||
| # Get terminal size | |||
| terminal_size = shutil.get_terminal_size() | |||
| # Print newlines to move cursor to the middle vertically | |||
| for k, v in texts.items(): | |||
| print(k) | |||
| print("\n" * 1) | |||
| # Calculate horizontal padding and print the centered text | |||
| for l in v: | |||
| print(l.center(terminal_size.columns)) | |||
| print("\n" * 1) | |||
| from dora import Node | |||
| node = Node("pretty-print") | |||
| previous_texts = {} | |||
| clear_screen() | |||
| print("Waiting for speech...") | |||
| for event in node: | |||
| if event["type"] == "INPUT": | |||
| # The sentence to be printed | |||
| sentence = event["value"][0].as_py() | |||
| if event["id"] not in previous_texts.keys(): | |||
| previous_texts[event["id"]] = ["", "", sentence] | |||
| else: | |||
| previous_texts[event["id"]] += [sentence] | |||
| previous_texts[event["id"]] = previous_texts[event["id"]][-3:] | |||
| # Clear the screen | |||
| clear_screen() | |||
| # Print the sentence in the middle of the terminal | |||
| print_centered(previous_texts) | |||
| @@ -0,0 +1,2 @@ | |||
| # Dora text translation Node using Argo translate | |||
| @@ -0,0 +1,11 @@ | |||
| import os | |||
| # Define the path to the README file relative to the package directory | |||
| readme_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "README.md") | |||
| # Read the content of the README file | |||
| try: | |||
| with open(readme_path, "r", encoding="utf-8") as f: | |||
| __doc__ = f.read() | |||
| except FileNotFoundError: | |||
| __doc__ = "README file not found." | |||
| @@ -0,0 +1,43 @@ | |||
| import os | |||
| os.environ["ARGOS_DEVICE_TYPE"] = "auto" | |||
| from dora import Node | |||
| import pyarrow as pa | |||
| import argostranslate.package | |||
| import argostranslate.translate | |||
| from_code = os.getenv("SOURCE_LANGUAGE", "fr") | |||
| to_code = os.getenv("TARGET_LANGUAGE", "en") | |||
| # Download and install Argos Translate package | |||
| argostranslate.package.update_package_index() | |||
| available_packages = argostranslate.package.get_available_packages() | |||
| package_to_install = next( | |||
| filter( | |||
| lambda x: x.from_code == from_code and x.to_code == to_code, available_packages | |||
| ) | |||
| ) | |||
| argostranslate.package.install_from_path(package_to_install.download()) | |||
| def main(): | |||
| node = Node() | |||
| while True: | |||
| event = node.next() | |||
| if event is None: | |||
| break | |||
| if event["type"] == "INPUT" and event["id"] == "text": | |||
| text = event["value"][0].as_py() | |||
| translatedText = argostranslate.translate.translate( | |||
| text, | |||
| from_code, | |||
| to_code, | |||
| ) | |||
| print(text, flush=True) | |||
| print("translated: " + translatedText, flush=True) | |||
| node.send_output( | |||
| "text", | |||
| pa.array([translatedText]), | |||
| {"language": to_code}, | |||
| ) | |||
| @@ -0,0 +1,25 @@ | |||
| [tool.poetry] | |||
| name = "dora-argotranslate" | |||
| version = "0.3.6" | |||
| description = "Dora Node for Text translating using Argostranslate" | |||
| readme = "README.md" | |||
| authors = [ | |||
| "Haixuan Xavier Tao <tao.xavier@outlook.com>", | |||
| "Enzo Le Van <dev@enzo-le-van.fr>", | |||
| "Félix Huang <felix.huang.net@gmail.com>", | |||
| ] | |||
| packages = [{ include = "dora_argotranslate" }] | |||
| [tool.poetry.dependencies] | |||
| dora-rs = "^0.3.6" | |||
| numpy = "< 2.0.0" | |||
| python = "^3.7" | |||
| argostranslate = "^1.9.6" | |||
| [tool.poetry.scripts] | |||
| dora-argotranslate = "dora_argotranslate.main:main" | |||
| [build-system] | |||
| requires = ["poetry-core>=1.8.0"] | |||
| build-backend = "poetry.core.masonry.api" | |||
| @@ -0,0 +1,9 @@ | |||
| import pytest | |||
| def test_import_main(): | |||
| from dora_argotranslate.main import main | |||
| # Check that everything is working, and catch dora Runtime Exception as we're not running in a dora dataflow. | |||
| with pytest.raises(RuntimeError): | |||
| main() | |||
| @@ -2,34 +2,110 @@ import torch | |||
| from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline | |||
| from dora import Node | |||
| import pyarrow as pa | |||
| import os | |||
| from pathlib import Path | |||
| DEFAULT_PATH = "openai/whisper-large-v3-turbo" | |||
| TARGET_LANGUAGE = os.getenv("TARGET_LANGUAGE", "chinese") | |||
| TRANSLATE = bool(os.getenv("TRANSLATE", "False")) | |||
| MODEL_NAME_OR_PATH = os.getenv("MODEL_NAME_OR_PATH", DEFAULT_PATH) | |||
| if bool(os.getenv("USE_MODELSCOPE_HUB")) is True: | |||
| from modelscope import snapshot_download | |||
| if not Path(MODEL_NAME_OR_PATH).exists(): | |||
| MODEL_NAME_OR_PATH = snapshot_download(MODEL_NAME_OR_PATH) | |||
| device = "cuda:0" if torch.cuda.is_available() else "cpu" | |||
| torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32 | |||
| model_id = "distil-whisper/distil-large-v3" | |||
| model = AutoModelForSpeechSeq2Seq.from_pretrained( | |||
| model_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True, use_safetensors=True | |||
| MODEL_NAME_OR_PATH, | |||
| torch_dtype=torch_dtype, | |||
| low_cpu_mem_usage=True, | |||
| use_safetensors=True, | |||
| ) | |||
| model.to(device) | |||
| processor = AutoProcessor.from_pretrained(model_id) | |||
| processor = AutoProcessor.from_pretrained(MODEL_NAME_OR_PATH) | |||
| pipe = pipeline( | |||
| "automatic-speech-recognition", | |||
| model=model, | |||
| tokenizer=processor.tokenizer, | |||
| feature_extractor=processor.feature_extractor, | |||
| max_new_tokens=128, | |||
| max_new_tokens=400, | |||
| torch_dtype=torch_dtype, | |||
| device=device, | |||
| ) | |||
| BAD_SENTENCES = [ | |||
| "字幕", | |||
| "字幕志愿", | |||
| "中文字幕", | |||
| "我", | |||
| "你", | |||
| "THANK YOU", | |||
| " Thank you.", | |||
| " www.microsoft.com", | |||
| " The", | |||
| " BANG", | |||
| " Silence.", | |||
| " Sous-titrage Société Radio-Canada", | |||
| " Sous", | |||
| " Sous-", | |||
| ] | |||
| def cut_repetition(text, min_repeat_length=4, max_repeat_length=50): | |||
| # Check if the text is primarily Chinese (you may need to adjust this threshold) | |||
| if sum(1 for char in text if "\u4e00" <= char <= "\u9fff") / len(text) > 0.5: | |||
| # Chinese text processing | |||
| for repeat_length in range( | |||
| min_repeat_length, min(max_repeat_length, len(text) // 2) | |||
| ): | |||
| for i in range(len(text) - repeat_length * 2 + 1): | |||
| chunk1 = text[i : i + repeat_length] | |||
| chunk2 = text[i + repeat_length : i + repeat_length * 2] | |||
| if chunk1 == chunk2: | |||
| return text[: i + repeat_length] | |||
| else: | |||
| # Non-Chinese (space-separated) text processing | |||
| words = text.split() | |||
| for repeat_length in range( | |||
| min_repeat_length, min(max_repeat_length, len(words) // 2) | |||
| ): | |||
| for i in range(len(words) - repeat_length * 2 + 1): | |||
| chunk1 = " ".join(words[i : i + repeat_length]) | |||
| chunk2 = " ".join(words[i + repeat_length : i + repeat_length * 2]) | |||
| if chunk1 == chunk2: | |||
| return " ".join(words[: i + repeat_length]) | |||
| return text | |||
| def main(): | |||
| node = Node() | |||
| for event in node: | |||
| if event["type"] == "INPUT": | |||
| audio = event["value"].to_numpy() | |||
| result = pipe(audio) | |||
| node.send_output("text", pa.array([result["text"]])) | |||
| confg = ( | |||
| {"language": TARGET_LANGUAGE, "task": "translate"} | |||
| if TRANSLATE | |||
| else { | |||
| "language": TARGET_LANGUAGE, | |||
| } | |||
| ) | |||
| result = pipe( | |||
| audio, | |||
| generate_kwargs=confg, | |||
| ) | |||
| if result["text"] in BAD_SENTENCES: | |||
| continue | |||
| text = cut_repetition(result["text"]) | |||
| print(text, flush=True) | |||
| node.send_output("text", pa.array([text]), {"language": TARGET_LANGUAGE}) | |||
| @@ -20,6 +20,7 @@ transformers = "^4.0.0" | |||
| accelerate = "^0.29.2" | |||
| torch = "^2.2.0" | |||
| python = "^3.7" | |||
| modelscope = "^1.18.1" | |||
| [tool.poetry.scripts] | |||
| dora-distil-whisper = "dora_distil_whisper.main:main" | |||
| @@ -2,66 +2,31 @@ import sounddevice as sd | |||
| import numpy as np | |||
| import pyarrow as pa | |||
| import time as tm | |||
| from enum import Enum | |||
| import os | |||
| from dora import Node | |||
| class RecordingState(Enum): | |||
| """Enum for recording states.""" | |||
| PENDING = 0 | |||
| RUNNING = 1 | |||
| SILENCE = 2 | |||
| def detect_speech(audio_data, threshold): | |||
| """Check if the amplitude of the audio signal exceeds the threshold.""" | |||
| return np.any(np.abs(audio_data) > threshold) | |||
| MAX_DURATION = float(os.getenv("MAX_DURATION", "0.1")) | |||
| SAMPLE_RATE = int(os.getenv("SAMPLE_RATE", "16000")) | |||
| def main(): | |||
| # Parameters | |||
| threshold = 500 # Threshold for detecting speech (adjust this as needed) | |||
| SAMPLE_RATE = 16000 | |||
| silence_duration = 0.5 # Duration of silence before stopping the recording | |||
| # Initialize buffer and recording flag | |||
| buffer = [] | |||
| state = RecordingState.PENDING | |||
| silence_start_time = tm.time() | |||
| start_recording_time = tm.time() | |||
| max_duration = 20 | |||
| node = Node() | |||
| # pylint: disable=unused-argument | |||
| def callback(indata, frames, time, status): | |||
| nonlocal buffer, state, silence_start_time, node, max_duration, start_recording_time | |||
| nonlocal buffer, node, start_recording_time | |||
| is_speaking = detect_speech(indata[:, 0], threshold) | |||
| if is_speaking: | |||
| if state == RecordingState.PENDING: | |||
| buffer = [] | |||
| state = RecordingState.RUNNING | |||
| start_recording_time = tm.time() | |||
| buffer.extend(indata[:, 0]) | |||
| elif not is_speaking and state == RecordingState.RUNNING: | |||
| silence_start_time = tm.time() # Reset silence timer | |||
| buffer.extend(indata[:, 0]) | |||
| state = RecordingState.SILENCE | |||
| elif ( | |||
| state == RecordingState.RUNNING or state == RecordingState.SILENCE | |||
| ) and tm.time() - start_recording_time > max_duration: | |||
| if tm.time() - start_recording_time > MAX_DURATION: | |||
| audio_data = np.array(buffer).ravel().astype(np.float32) / 32768.0 | |||
| node.send_output("audio", pa.array(audio_data)) | |||
| state = RecordingState.PENDING | |||
| elif not is_speaking and state == RecordingState.SILENCE: | |||
| if tm.time() - silence_start_time > silence_duration: | |||
| audio_data = np.array(buffer).ravel().astype(np.float32) / 32768.0 | |||
| node.send_output("audio", pa.array(audio_data)) | |||
| state = RecordingState.PENDING | |||
| else: | |||
| buffer.extend(indata[:, 0]) | |||
| buffer = [] | |||
| start_recording_time = tm.time() | |||
| else: | |||
| buffer.extend(indata[:, 0]) | |||
| # Start recording | |||
| with sd.InputStream( | |||
| @@ -0,0 +1 @@ | |||
| # Dora text translation Node using OPUS MT | |||
| @@ -0,0 +1,11 @@ | |||
| import os | |||
| # Define the path to the README file relative to the package directory | |||
| readme_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "README.md") | |||
| # Read the content of the README file | |||
| try: | |||
| with open(readme_path, "r", encoding="utf-8") as f: | |||
| __doc__ = f.read() | |||
| except FileNotFoundError: | |||
| __doc__ = "README file not found." | |||
| @@ -0,0 +1,78 @@ | |||
| import os | |||
| from pathlib import Path | |||
| from dora import Node | |||
| import pyarrow as pa | |||
| import numpy as np | |||
| from transformers import AutoTokenizer, AutoModelForSeq2SeqLM | |||
| from_code = os.getenv("SOURCE_LANGUAGE", "zh") | |||
| to_code = os.getenv("TARGET_LANGUAGE", "en") | |||
| DEFAULT_PATH = f"Helsinki-NLP/opus-mt-{from_code}-{to_code}" | |||
| MODEL_NAME_OR_PATH = os.getenv("MODEL_NAME_OR_PATH", DEFAULT_PATH) | |||
| if bool(os.getenv("USE_MODELSCOPE_HUB")) is True: | |||
| from modelscope import snapshot_download | |||
| if not Path(MODEL_NAME_OR_PATH).exists(): | |||
| MODEL_NAME_OR_PATH = snapshot_download(MODEL_NAME_OR_PATH) | |||
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME_OR_PATH) | |||
| model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME_OR_PATH) | |||
| def cut_repetition(text, min_repeat_length=4, max_repeat_length=50): | |||
| # Check if the text is primarily Chinese (you may need to adjust this threshold) | |||
| if sum(1 for char in text if "\u4e00" <= char <= "\u9fff") / len(text) > 0.5: | |||
| # Chinese text processing | |||
| for repeat_length in range( | |||
| min_repeat_length, min(max_repeat_length, len(text) // 2) | |||
| ): | |||
| for i in range(len(text) - repeat_length * 2 + 1): | |||
| chunk1 = text[i : i + repeat_length] | |||
| chunk2 = text[i + repeat_length : i + repeat_length * 2] | |||
| if chunk1 == chunk2: | |||
| return text[: i + repeat_length] | |||
| else: | |||
| # Non-Chinese (space-separated) text processing | |||
| words = text.split() | |||
| for repeat_length in range( | |||
| min_repeat_length, min(max_repeat_length, len(words) // 2) | |||
| ): | |||
| for i in range(len(words) - repeat_length * 2 + 1): | |||
| chunk1 = " ".join(words[i : i + repeat_length]) | |||
| chunk2 = " ".join(words[i + repeat_length : i + repeat_length * 2]) | |||
| if chunk1 == chunk2: | |||
| return " ".join(words[: i + repeat_length]) | |||
| return text | |||
| def main(): | |||
| node = Node() | |||
| while True: | |||
| event = node.next() | |||
| if event is None: | |||
| break | |||
| if event["type"] == "INPUT" and event["id"] == "text": | |||
| text = [str(event["value"][0].as_py())] | |||
| translated = ( | |||
| model.generate(**tokenizer(text, return_tensors="pt", padding=True)) | |||
| .to("cpu") | |||
| .detach() | |||
| .numpy() | |||
| .ravel() | |||
| ) | |||
| array = np.array(tokenizer.decode(translated, skip_special_tokens=True)) | |||
| array = np.array(array).ravel() | |||
| array = [cut_repetition(array[0])] | |||
| node.send_output( | |||
| "text", | |||
| pa.array(array), | |||
| {"language": to_code}, | |||
| ) | |||
| @@ -0,0 +1,26 @@ | |||
| [tool.poetry] | |||
| name = "dora-opus" | |||
| version = "0.3.6" | |||
| description = "Dora Node for Text translating using Opus" | |||
| readme = "README.md" | |||
| authors = [ | |||
| "Haixuan Xavier Tao <tao.xavier@outlook.com>", | |||
| "Enzo Le Van <dev@enzo-le-van.fr>", | |||
| "Félix Huang <felix.huang.net@gmail.com>", | |||
| ] | |||
| packages = [{ include = "dora_opus" }] | |||
| [tool.poetry.dependencies] | |||
| dora-rs = "^0.3.6" | |||
| numpy = "< 2.0.0" | |||
| python = "^3.7" | |||
| transformers = "^4.45" | |||
| modelscope = "^1.18.1" | |||
| [tool.poetry.scripts] | |||
| dora-opus = "dora_opus.main:main" | |||
| [build-system] | |||
| requires = ["poetry-core>=1.8.0"] | |||
| build-backend = "poetry.core.masonry.api" | |||
| @@ -0,0 +1,9 @@ | |||
| import pytest | |||
| def test_import_main(): | |||
| from dora_opus.main import main | |||
| # Check that everything is working, and catch dora Runtime Exception as we're not running in a dora dataflow. | |||
| with pytest.raises(RuntimeError): | |||
| main() | |||
| @@ -1,6 +1,7 @@ | |||
| from threading import Thread | |||
| from dora import Node | |||
| import os | |||
| from pathlib import Path | |||
| import numpy as np | |||
| import torch | |||
| import time | |||
| @@ -18,16 +19,25 @@ from transformers import ( | |||
| device = "cuda:0" # if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" | |||
| torch_dtype = torch.float16 if device != "cpu" else torch.float32 | |||
| repo_id = "ylacombe/parler-tts-mini-jenny-30H" | |||
| DEFAULT_PATH = "ylacombe/parler-tts-mini-jenny-30H" | |||
| MODEL_NAME_OR_PATH = os.getenv("MODEL_NAME_OR_PATH", DEFAULT_PATH) | |||
| if bool(os.getenv("USE_MODELSCOPE_HUB")) is True: | |||
| from modelscope import snapshot_download | |||
| if not Path(MODEL_NAME_OR_PATH).exists(): | |||
| MODEL_NAME_OR_PATH = snapshot_download(MODEL_NAME_OR_PATH) | |||
| model = ParlerTTSForConditionalGeneration.from_pretrained( | |||
| repo_id, torch_dtype=torch_dtype, low_cpu_mem_usage=True | |||
| MODEL_NAME_OR_PATH, torch_dtype=torch_dtype, low_cpu_mem_usage=True | |||
| ).to(device) | |||
| model.generation_config.cache_implementation = "static" | |||
| model.forward = torch.compile(model.forward, mode="default") | |||
| tokenizer = AutoTokenizer.from_pretrained(repo_id) | |||
| feature_extractor = AutoFeatureExtractor.from_pretrained(repo_id) | |||
| tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME_OR_PATH) | |||
| feature_extractor = AutoFeatureExtractor.from_pretrained(MODEL_NAME_OR_PATH) | |||
| SAMPLE_RATE = feature_extractor.sampling_rate | |||
| SEED = 42 | |||
| @@ -59,6 +69,7 @@ def play_audio(audio_array): | |||
| class InterruptStoppingCriteria(StoppingCriteria): | |||
| def __init__(self): | |||
| super().__init__() | |||
| self.stop_signal = False | |||
| def __call__( | |||
| @@ -20,6 +20,7 @@ torchaudio = "^2.2.2" | |||
| sentencepiece = "^0.1.99" | |||
| python = "^3.7" | |||
| pyaudio = "^0.2.14" | |||
| modelscope = "^1.18.1" | |||
| [tool.poetry.scripts] | |||
| @@ -12,7 +12,7 @@ DEFAULT_PATH = "Qwen/Qwen2-VL-2B-Instruct" | |||
| MODEL_NAME_OR_PATH = os.getenv("MODEL_NAME_OR_PATH", DEFAULT_PATH) | |||
| if bool(os.getenv("MODELSCOPE")) is True: | |||
| if bool(os.getenv("USE_MODELSCOPE_HUB")) is True: | |||
| from modelscope import snapshot_download | |||
| if not Path(MODEL_NAME_OR_PATH).exists(): | |||
| @@ -0,0 +1,3 @@ | |||
| # Speech Activity Detection(VAD) | |||
| This is using Silero VAD. | |||
| @@ -0,0 +1,11 @@ | |||
| import os | |||
| # Define the path to the README file relative to the package directory | |||
| readme_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "README.md") | |||
| # Read the content of the README file | |||
| try: | |||
| with open(readme_path, "r", encoding="utf-8") as f: | |||
| __doc__ = f.read() | |||
| except FileNotFoundError: | |||
| __doc__ = "README file not found." | |||
| @@ -0,0 +1,53 @@ | |||
| from dora import Node | |||
| import pyarrow as pa | |||
| import numpy as np | |||
| import os | |||
| from silero_vad import load_silero_vad, get_speech_timestamps | |||
| import torch | |||
| model = load_silero_vad() | |||
| MIN_SILENCE_DURATION_MS = int(os.getenv("MIN_SILENCE_DURATION_MS", "200")) | |||
| MIN_SPEECH_DURATION_MS = int(os.getenv("MIN_SPEECH_DURATION_MS", "1000")) | |||
| MIN_AUDIO_SAMPLING_DURAION_S = int(os.getenv("MAX_AUDIO_DURATION_S", "20")) | |||
| MAX_AUDIO_DURAION_S = int(os.getenv("MAX_AUDIO_DURATION_S", "75")) | |||
| def main(): | |||
| node = Node() | |||
| last_audios = [] | |||
| while True: | |||
| event = node.next() | |||
| if event is None: | |||
| break | |||
| if event["type"] == "INPUT" and event["id"] == "audio": | |||
| audio = event["value"].to_numpy() | |||
| last_audios += [audio] | |||
| last_audios = last_audios[-100:] | |||
| audio = np.concatenate(last_audios) | |||
| speech_timestamps = get_speech_timestamps( | |||
| torch.from_numpy(audio), | |||
| model, | |||
| threshold=0.2, | |||
| min_speech_duration_ms=MIN_SPEECH_DURATION_MS, | |||
| min_silence_duration_ms=MIN_SILENCE_DURATION_MS, | |||
| ) | |||
| # Check ig there is timestamp | |||
| if ( | |||
| len(speech_timestamps) > 0 | |||
| and len(last_audios) > MIN_AUDIO_SAMPLING_DURAION_S | |||
| ): | |||
| # Check if the audio is not cut at the end. And only return if there is a long time spent | |||
| if speech_timestamps[-1]["end"] == len(audio): | |||
| continue | |||
| else: | |||
| audio = audio[0 : speech_timestamps[-1]["end"]] | |||
| node.send_output("audio", pa.array(audio)) | |||
| last_audios = [audio[speech_timestamps[-1]["end"] :]] | |||
| # If there is no sound for too long return the audio | |||
| elif len(last_audios) > 75: | |||
| node.send_output("audio", pa.array(audio)) | |||
| last_audios = [] | |||
| @@ -0,0 +1,26 @@ | |||
| [tool.poetry] | |||
| name = "dora-vad" | |||
| version = "0.3.6" | |||
| description = "Dora Node for Text translating using Argostranslate" | |||
| readme = "README.md" | |||
| authors = [ | |||
| "Haixuan Xavier Tao <tao.xavier@outlook.com>", | |||
| "Enzo Le Van <dev@enzo-le-van.fr>", | |||
| "Félix Huang <felix.huang.net@gmail.com>", | |||
| ] | |||
| packages = [{ include = "dora_vad" }] | |||
| [tool.poetry.dependencies] | |||
| dora-rs = "^0.3.6" | |||
| numpy = "< 2.0.0" | |||
| python = "^3.7" | |||
| silero-vad = "^5.1" | |||
| [tool.poetry.scripts] | |||
| dora-vad = "dora_vad.main:main" | |||
| [build-system] | |||
| requires = ["poetry-core>=1.8.0"] | |||
| build-backend = "poetry.core.masonry.api" | |||
| @@ -0,0 +1,9 @@ | |||
| import pytest | |||
| def test_import_main(): | |||
| from dora_vad.main import main | |||
| # Check that everything is working, and catch dora Runtime Exception as we're not running in a dora dataflow. | |||
| with pytest.raises(RuntimeError): | |||
| main() | |||