diff --git a/Makefile b/Makefile index 559f72e5b7..b0f17df9f7 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Change also in pyproject.toml and buzz/__version__.py -version := 1.4.5 +version := 1.4.6 mac_app_path := ./dist/Buzz.app mac_zip_path := ./dist/Buzz-${version}-mac.zip diff --git a/buzz/__version__.py b/buzz/__version__.py index 1512303e71..664747ebab 100644 --- a/buzz/__version__.py +++ b/buzz/__version__.py @@ -1 +1 @@ -VERSION = "1.4.5" +VERSION = "1.4.6" diff --git a/buzz/widgets/application.py b/buzz/widgets/application.py index e517b710bf..8fe105eeb4 100755 --- a/buzz/widgets/application.py +++ b/buzz/widgets/application.py @@ -105,6 +105,10 @@ def show_main_window(self): def add_task(self, task: FileTranscriptionTask, quit_on_complete: bool = False): self.window.quit_on_complete = quit_on_complete + if quit_on_complete: + # Track every queued task so the app only quits once the whole + # batch is done. + self.window.pending_quit_task_uids.add(task.uid) self.window.add_task(task) def close_database(self): diff --git a/buzz/widgets/main_window.py b/buzz/widgets/main_window.py index 0e47725a7c..d64f950356 100644 --- a/buzz/widgets/main_window.py +++ b/buzz/widgets/main_window.py @@ -1,6 +1,6 @@ import os import logging -from typing import Tuple, List, Optional +from typing import Tuple, List, Optional, Set from uuid import UUID from PyQt6 import QtGui @@ -90,6 +90,7 @@ def __init__(self, transcription_service: TranscriptionService): self.shortcuts = Shortcuts(settings=self.settings) self.quit_on_complete = False + self.pending_quit_task_uids: Set[UUID] = set() self.transcription_service = transcription_service self.plugin_manager = PluginManager(self.transcription_service, self.settings) @@ -484,9 +485,7 @@ def on_task_completed(self, task: FileTranscriptionTask, segments: List[Segment] if task.status == FileTranscriptionTask.Status.SKIPPED: self.transcription_service.update_transcription_as_skipped(task.uid, segments) self.table_widget.refresh_row(task.uid) - if self.quit_on_complete: - self.close() - QApplication.quit() + self.quit_if_all_tasks_done(task) return # Update file path in database only for URL imports where file is downloaded @@ -523,18 +522,24 @@ def on_task_completed(self, task: FileTranscriptionTask, segments: List[Segment] self.transcription_service.update_transcription_as_completed(task.uid, segments) self.table_widget.refresh_row(task.uid) - if self.quit_on_complete: - self.close() - QApplication.quit() + self.quit_if_all_tasks_done(task) + def quit_if_all_tasks_done(self, task: FileTranscriptionTask): + if not self.quit_on_complete: + return + + self.pending_quit_task_uids.discard(task.uid) + if len(self.pending_quit_task_uids) > 0: + return + + self.close() + QApplication.quit() def on_task_error(self, task: FileTranscriptionTask, error: str): self.transcription_service.update_transcription_as_failed(task.uid, error) self.table_widget.refresh_row(task.uid) - if self.quit_on_complete: - self.close() - QApplication.quit() + self.quit_if_all_tasks_done(task) def on_shortcuts_changed(self): self.menu_bar.reset_shortcuts() diff --git a/docs/docs/cli.md b/docs/docs/cli.md index a8df135a75..92d37fdc84 100644 --- a/docs/docs/cli.md +++ b/docs/docs/cli.md @@ -60,29 +60,45 @@ Options: (Yiddish), yo (Yoruba), zh (Chinese). Leave empty to detect language. -p, --prompt Initial prompt. - -w, --word-timestamps Generate word-level timestamps. (available since 1.2.0) - -e, --extract-speech Extract speech from audio before transcribing. (available since 1.3.0) + -w, --word-timestamps Generate word-level timestamps. + -e, --extract-speech Extract speech from audio before transcribing. --openai-token OpenAI access token. Use only when --model-type is openaiapi. Defaults to your previously saved access token, if one exists. + -d, --output-directory + Output directory. Defaults to the directory of + the input file. --srt Output result in an SRT file. --vtt Output result in a VTT file. --txt Output result in a TXT file. - --hide-gui Hide the main application window. (available since 1.2.0) + --hide-gui Hide the main application window. -h, --help Displays help on commandline options. --help-all Displays help including Qt specific options. -v, --version Displays version information. Arguments: - files or urls Input file paths or urls. Url import availalbe since 1.2.0. + files or urls Input file paths or urls. ``` **Examples**: ```shell -# Translate two MP3 files from French to English using OpenAI Whisper API -buzz add --task translate --language fr --model-type openaiapi /Users/user/Downloads/1b3b03e4-8db5-ea2c-ace5-b71ff32e3304.mp3 /Users/user/Downloads/koaf9083k1lkpsfdi0.mp3 +# Transcribe a single file and save the transcript as a TXT file next to it +buzz add --txt interview.mp3 -# Transcribe an MP4 using Whisper.cpp "small" model and immediately export to SRT and VTT files -buzz add --task transcribe --model-type whispercpp --model-size small --prompt "My initial prompt" --srt --vtt /Users/user/Downloads/buzz/1b3b03e4-8db5-ea2c-ace5-b71ff32e3304.mp4 +# Transcribe several files at once, they will be processed one after another. +# Support for multiple input files available since version 1.4.6 +buzz add --txt lecture-1.mp3 lecture-2.mp3 lecture-3.mp3 + +# Transcribe a whole folder of recordings into a separate output folder +buzz add --srt --output-directory ./subtitles recordings/*.mp4 + +# Translate French audio to English with the OpenAI Whisper API +buzz add --task translate --language fr --model-type openaiapi meeting-fr.mp3 + +# Transcribe a video with the Whisper.cpp "small" model, exporting SRT and VTT subtitles +buzz add --model-type whispercpp --model-size small --srt --vtt presentation.mp4 + +# Transcribe a YouTube video without opening the app window +buzz add --txt --hide-gui https://www.youtube.com/watch?v=dQw4w9WgXcQ ``` diff --git a/docs/docs/faq.md b/docs/docs/faq.md index 822097ce6b..906d3e4bdd 100644 --- a/docs/docs/faq.md +++ b/docs/docs/faq.md @@ -41,7 +41,7 @@ In addition to choosing an appropriate model size you also can choose whisper ty - **Whisper.cpp** is optimized C++ implementation, it quite fast and efficient and will use any brand of GPU. Whisper.cpp is capable of running real time transcription even on a modern laptop with integrated GPU. It can also run on CPU only. Use this option if you do not have Nvidia GPU. This is best option to us on Macs. - **HuggingFace** option is a `Transformers` implementation and is good in that it supports wide range of custom models that may be optimized for a particular language. - Since version `1.4.0` This option also supports [MMS](https://ai.meta.com/blog/multilingual-model-speech-recognition/) family of models from Meta AI that support over 1000 of worlds languages as well as [PEFT](https://github.com/huggingface/peft) adjustments to Whisper models. - - Since version `1.5.0` [Parakeet](https://huggingface.co/models?sort=trending&search=parakeet), [Qwen3-ASR](https://huggingface.co/models?sort=trending&search=qwen+asr+hf) and [VibeVoice ASR](https://huggingface.co/models?sort=trending&search=vibevoice+hf+) models are also supported. + - Since version `1.4.5` [Parakeet](https://huggingface.co/models?sort=trending&search=parakeet), [Qwen3-ASR](https://huggingface.co/models?sort=trending&search=qwen+asr+hf) and [VibeVoice ASR](https://huggingface.co/models?sort=trending&search=vibevoice+hf+) models are also supported. Models with `.En` in their names are for English and will not work for other languages. diff --git a/docs/docs/preferences.md b/docs/docs/preferences.md index 57c5c2f1e8..2f064fa38c 100644 --- a/docs/docs/preferences.md +++ b/docs/docs/preferences.md @@ -109,17 +109,17 @@ Defaults to [user_cache_dir](https://pypi.org/project/platformdirs/). **BUZZ_DOWNLOAD_COOKIEFILE** - Location of a [cookiefile](https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp) to use for downloading private videos or as workaround for anti-bot protection. -**BUZZ_FORCE_CPU** - Will force Buzz to use CPU and not GPU, useful for setups with older GPU if that is slower than GPU or GPU has issues. Example usage `BUZZ_FORCE_CPU=true`. Available since `1.2.1` +**BUZZ_FORCE_CPU** - Will force Buzz to use CPU and not GPU, useful for setups with older GPU if that is slower than GPU or GPU has issues. Example usage `BUZZ_FORCE_CPU=true`. **BUZZ_REDUCE_GPU_MEMORY** - Will use 8bit quantization for Huggingface, Faster Whisper and Whisper.cpp transcriptions to reduce required GPU memory. Example usage `BUZZ_REDUCE_GPU_MEMORY=true`. Available since `1.4.0` -**BUZZ_MERGE_REGROUP_RULE** - Custom regroup merge rule to use when combining transcripts with word-level timings. More information on available options [in stable-ts repo](https://github.com/jianfch/stable-ts?tab=readme-ov-file#regrouping-methods). Available since `1.3.0` +**BUZZ_MERGE_REGROUP_RULE** - Custom regroup merge rule to use when combining transcripts with word-level timings. More information on available options [in stable-ts repo](https://github.com/jianfch/stable-ts?tab=readme-ov-file#regrouping-methods). -**BUZZ_DISABLE_TELEMETRY** - Buzz collects basic OS name and architecture usage statistics to better focus development efforts. This variable lets disable collection of these statistics. Example usage `BUZZ_DISABLE_TELEMETRY=true`. Available since `1.3.0` +**BUZZ_DISABLE_TELEMETRY** - Buzz collects basic OS name and architecture usage statistics to better focus development efforts. This variable lets disable collection of these statistics. Example usage `BUZZ_DISABLE_TELEMETRY=true`. **BUZZ_DISABLE_UPDATE_CHECK** - Disable checks for updates. Example usage `BUZZ_DISABLE_UPDATE_CHECK=true`. Available since `1.4.5` -**BUZZ_UPLOAD_URL** - Live recording transcripts and translations can be uploaded to a server for display on the web. Set this variable to the desired upload url. You can use [buzz-transcription-server](https://github.com/raivisdejus/buzz-transcription-server) as a server. Buzz will upload the following `json` via `POST` requests - `{"kind": "transcript", "text": "Sample transcript"}` or `{"kind": "translation", "text": "Sample translation"}`. Example usage `BUZZ_UPLOAD_URL=http://localhost:5000/upload`. Available since `1.3.0` +**BUZZ_UPLOAD_URL** - Live recording transcripts and translations can be uploaded to a server for display on the web. Set this variable to the desired upload url. You can use [buzz-transcription-server](https://github.com/raivisdejus/buzz-transcription-server) as a server. Buzz will upload the following `json` via `POST` requests - `{"kind": "transcript", "text": "Sample transcript"}` or `{"kind": "translation", "text": "Sample translation"}`. Example usage `BUZZ_UPLOAD_URL=http://localhost:5000/upload`. **HF_ENDPOINT** - To speed up downloads in China set `HF_ENDPOINT=https://hf-mirror.com` @@ -127,4 +127,4 @@ Example of data collected by telemetry: ``` Buzz: 1.3.0, locale: ('lv_LV', 'UTF-8'), system: Linux, release: 6.14.0-27-generic, machine: x86_64, version: #27~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 17:38:49 UTC 2, ``` -**BUZZ_PARAGRAPH_SPLIT_TIME** - Time in milliseconds of silence to split paragraphs in transcript and add two newlines when exporting the transcripts as text. Default is `2000` or 2 seconds. Available since `1.3.0` \ No newline at end of file +**BUZZ_PARAGRAPH_SPLIT_TIME** - Time in milliseconds of silence to split paragraphs in transcript and add two newlines when exporting the transcripts as text. Default is `2000` or 2 seconds. \ No newline at end of file diff --git a/docs/i18n/zh/docusaurus-plugin-content-docs/current/cli.md b/docs/i18n/zh/docusaurus-plugin-content-docs/current/cli.md index eca461b28d..c534368cfa 100644 --- a/docs/i18n/zh/docusaurus-plugin-content-docs/current/cli.md +++ b/docs/i18n/zh/docusaurus-plugin-content-docs/current/cli.md @@ -60,20 +60,20 @@ Options: (Yiddish), yo (Yoruba), zh (Chinese). Leave empty to detect language. -p, --prompt Initial prompt. - -w, --word-timestamps Generate word-level timestamps. (available since 1.2.0) + -w, --word-timestamps Generate word-level timestamps. --openai-token OpenAI access token. Use only when --model-type is openaiapi. Defaults to your previously saved access token, if one exists. --srt Output result in an SRT file. --vtt Output result in a VTT file. --txt Output result in a TXT file. - --hide-gui Hide the main application window. (available since 1.2.0) + --hide-gui Hide the main application window. -h, --help Displays help on commandline options. --help-all Displays help including Qt specific options. -v, --version Displays version information. Arguments: - files or urls Input file paths or urls. Url import availalbe since 1.2.0. + files or urls Input file paths or urls. ``` **示例**: diff --git a/pyproject.toml b/pyproject.toml index c80c7472ab..f8291940f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "buzz-captions" # Change also in Makefile and buzz/__version__.py -version = "1.4.5" +version = "1.4.6" description = "" authors = [{ name = "Chidi Williams", email = "williamschidi1@gmail.com" }] requires-python = ">=3.13,<3.14" diff --git a/tests/cli_test.py b/tests/cli_test.py index 7887acf310..ec73f8df0b 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,15 +1,17 @@ import os -import sys from tempfile import mkdtemp import pytest from pytestqt.qtbot import QtBot from buzz.cli import parse_command_line -from tests.audio import test_audio_path +from tests.audio import test_audio_path, test_multibyte_utf8_audio_path class TestCLI: + # A batch of more than one file also covers the single file case. The app + # closes its database when the CLI run finishes, so only one CLI test can + # run per pytest process. @pytest.mark.parametrize( "qapp_args", [ @@ -25,17 +27,23 @@ class TestCLI: mkdtemp(), "--txt", test_audio_path, + test_multibyte_utf8_audio_path, ], ) ], indirect=True, ) - def test_cli(self, qapp, qapp_args, qtbot: QtBot): + def test_cli_multiple_files(self, qapp, qapp_args, qtbot: QtBot): + """Every file in a batch must be transcribed, not just the first one. + """ output_directory = qapp_args[7] parse_command_line(qapp) - def output_exists_at_output_directory(): - assert any(file.endswith(".txt") for file in os.listdir(output_directory)) + def all_outputs_exist(): + outputs = [ + file for file in os.listdir(output_directory) if file.endswith(".txt") + ] + assert len(outputs) == 2 - qtbot.wait_until(output_exists_at_output_directory, timeout=5 * 60 * 1000) + qtbot.wait_until(all_outputs_exist, timeout=5 * 60 * 1000)