├── .github ├── scripts │ ├── build-linux │ ├── build-macos │ ├── package-linux │ ├── package-macos │ ├── utils.zsh │ │ ├── mkcd │ │ ├── log_error │ │ ├── log_warning │ │ ├── log_debug │ │ ├── log_output │ │ ├── log_status │ │ ├── log_info │ │ ├── read_codesign_user │ │ ├── read_codesign_team │ │ ├── read_codesign_installer │ │ ├── read_codesign │ │ ├── log_group │ │ ├── set_loglevel │ │ ├── check_macos │ │ ├── setup_ccache │ │ ├── read_codesign_pass │ │ ├── setup_linux │ │ └── check_linux │ ├── .Brewfile │ ├── .Wingetfile │ ├── .Aptfile │ ├── utils.pwsh │ │ ├── Ensure-Location.ps1 │ │ ├── Invoke-External.ps1 │ │ ├── Install-BuildDependencies.ps1 │ │ ├── Expand-ArchiveExt.ps1 │ │ └── Logger.ps1 │ ├── Package-Windows.ps1 │ └── Build-Windows.ps1 ├── workflows │ ├── dispatch.yaml │ ├── check-format.yaml │ ├── pr-pull.yaml │ └── push.yaml └── actions │ ├── run-cmake-format │ └── action.yaml │ ├── run-clang-format │ └── action.yaml │ ├── package-plugin │ └── action.yaml │ └── build-plugin │ └── action.yaml ├── src ├── cloud-providers │ ├── aws │ │ ├── CMakeLists.txt │ │ ├── eventstream.h │ │ ├── aws_provider.h │ │ ├── presigned_url.h │ │ └── presigned_url.cpp │ ├── clova │ │ ├── nest.proto │ │ └── clova-provider.h │ ├── deepgram │ │ ├── deepgram-provider.h │ │ └── deepgram-provider.cpp │ ├── google │ │ ├── google-provider.h │ │ └── google-provider.cpp │ ├── revai │ │ ├── revai-provider.h │ │ └── WebSocket protocol.md │ ├── cloud-provider.cpp │ └── cloud-provider.h ├── utils │ ├── ssl-utils.h │ ├── curl-helper.h │ ├── curl-helper.cpp │ └── ssl-utils.cpp ├── timed-metadata │ └── timed-metadata-utils.h ├── cloud-translation │ ├── CMakeLists.txt │ ├── deepl.h │ ├── google-cloud.h │ ├── translation-cloud.h │ ├── papago.h │ ├── openai.h │ ├── claude.h │ ├── azure.h │ ├── custom-api.h │ ├── ITranslator.h │ ├── aws.h │ ├── translation-cloud.cpp │ ├── google-cloud.cpp │ ├── azure.cpp │ ├── custom-api.cpp │ ├── deepl.cpp │ ├── claude.cpp │ └── openai.cpp ├── cloudvocal.c ├── cloudvocal-processing.h ├── cloudvocal.h ├── plugin-support.h ├── cloudvocal-callbacks.h ├── cloudvocal-utils.h ├── plugin-main.c ├── plugin-support.c.in ├── language-codes │ └── language-codes.h ├── cloudvocal-utils.cpp ├── cloudvocal-data.h └── cloudvocal-processing.cpp ├── cmake ├── windows │ ├── defaults.cmake │ ├── buildspec.cmake │ ├── resources │ │ ├── resource.rc.in │ │ └── installer-Windows.iss.in │ └── compilerconfig.cmake ├── FetchNlohmannJSON.cmake ├── macos │ ├── resources │ │ ├── ccache-launcher-c.in │ │ ├── ccache-launcher-cxx.in │ │ ├── distribution.in │ │ └── create-package.cmake.in │ ├── buildspec.cmake │ ├── defaults.cmake │ ├── compilerconfig.cmake │ └── helpers.cmake ├── common │ ├── ccache.cmake │ ├── buildnumber.cmake │ ├── osconfig.cmake │ ├── helpers_common.cmake │ ├── compiler_common.cmake │ └── bootstrap.cmake ├── linux │ ├── toolchains │ │ ├── x86_64-linux-gcc.cmake │ │ ├── aarch64-linux-gcc.cmake │ │ ├── x86_64-linux-clang.cmake │ │ └── aarch64-linux-clang.cmake │ ├── helpers.cmake │ ├── defaults.cmake │ └── compilerconfig.cmake ├── BuildClovaAPIs.cmake ├── BuildMyCurl.cmake ├── BuildGoogleAPIs.cmake └── BuildDependencies.cmake ├── conanfile.txt ├── .gitignore ├── .cmake-format.json ├── data └── locale │ └── en-US.ini └── CMakeLists.txt /.github/scripts/build-linux: -------------------------------------------------------------------------------- 1 | .build.zsh -------------------------------------------------------------------------------- /.github/scripts/build-macos: -------------------------------------------------------------------------------- 1 | .build.zsh -------------------------------------------------------------------------------- /.github/scripts/package-linux: -------------------------------------------------------------------------------- 1 | .package.zsh -------------------------------------------------------------------------------- /.github/scripts/package-macos: -------------------------------------------------------------------------------- 1 | .package.zsh -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/mkcd: -------------------------------------------------------------------------------- 1 | [[ -n ${1} ]] && mkdir -p ${1} && builtin cd ${1} 2 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_error: -------------------------------------------------------------------------------- 1 | local icon=' ✖︎ ' 2 | 3 | print -u2 -PR "${CI:+::error::}%F{1} ${icon} %f ${@}" 4 | -------------------------------------------------------------------------------- /.github/scripts/.Brewfile: -------------------------------------------------------------------------------- 1 | brew "ccache" 2 | brew "coreutils" 3 | brew "cmake" 4 | brew "git" 5 | brew "jq" 6 | brew "xcbeautify" 7 | -------------------------------------------------------------------------------- /src/cloud-providers/aws/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | target_sources(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/aws_sdk_provider.cpp) 2 | -------------------------------------------------------------------------------- /.github/scripts/.Wingetfile: -------------------------------------------------------------------------------- 1 | package 'cmake', path: 'Cmake\bin', bin: 'cmake' 2 | package 'innosetup', path: 'Inno Setup 6', bin: 'iscc' 3 | -------------------------------------------------------------------------------- /.github/scripts/.Aptfile: -------------------------------------------------------------------------------- 1 | package 'cmake' 2 | package 'ccache' 3 | package 'git' 4 | package 'jq' 5 | package 'ninja-build', bin: 'ninja' 6 | package 'pkg-config' 7 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_warning: -------------------------------------------------------------------------------- 1 | if (( _loglevel > 0 )) { 2 | local icon=' =>' 3 | 4 | print -PR "${CI:+::warning::}%F{3} ${(r:5:)icon} ${@}%f" 5 | } 6 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_debug: -------------------------------------------------------------------------------- 1 | if (( ! ${+_loglevel} )) typeset -g _loglevel=1 2 | 3 | if (( _loglevel > 2 )) print -PR -e -- "${CI:+::debug::}%F{220}DEBUG: ${@}%f" 4 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_output: -------------------------------------------------------------------------------- 1 | if (( ! ${+_loglevel} )) typeset -g _loglevel=1 2 | 3 | if (( _loglevel > 0 )) { 4 | local icon='' 5 | 6 | print -PR " ${(r:5:)icon} ${@}" 7 | } 8 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_status: -------------------------------------------------------------------------------- 1 | if (( ! ${+_loglevel} )) typeset -g _loglevel=1 2 | 3 | if (( _loglevel > 0 )) { 4 | local icon=' >' 5 | 6 | print -PR "%F{2} ${(r:5:)icon}%f ${@}" 7 | } 8 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_info: -------------------------------------------------------------------------------- 1 | if (( ! ${+_loglevel} )) typeset -g _loglevel=1 2 | 3 | if (( _loglevel > 0 )) { 4 | local icon=' =>' 5 | 6 | print -PR "%F{4} ${(r:5:)icon}%f %B${@}%b" 7 | } 8 | -------------------------------------------------------------------------------- /cmake/windows/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Enable find_package targets to become globally available targets 6 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 7 | 8 | include(buildspec) 9 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/read_codesign_user: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info 2 | 3 | if (( ! ${+CODESIGN_IDENT_USER} )) { 4 | typeset -g CODESIGN_IDENT_USER 5 | log_info 'Setting up Apple ID for notarization...' 6 | read CODESIGN_IDENT_USER'?Apple ID: ' 7 | } 8 | -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | boost/1.86.0 3 | grpc/1.48.4 4 | protobuf/3.21.12 5 | abseil/20230125.3 6 | openssl/3.3.2 7 | c-ares/1.19.1 8 | zlib/1.3.1 9 | aws-sdk-cpp/1.11.352 10 | 11 | [generators] 12 | CMakeDeps 13 | 14 | [options] 15 | aws-sdk-cpp/1.11.352:transcribestreaming=True 16 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/read_codesign_team: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info 2 | 3 | if (( ! ${+CODESIGN_TEAM} )) { 4 | typeset -g CODESIGN_TEAM 5 | log_info 'Setting up Apple Developer Team ID for codesigning...' 6 | read CODESIGN_TEAM'?Apple Developer Team ID (leave empty to use Apple Developer ID instead): ' 7 | } 8 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/read_codesign_installer: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info 2 | 3 | if (( ! ${+CODESIGN_IDENT_INSTALLER} )) { 4 | typeset -g CODESIGN_IDENT_INSTALLER 5 | log_info 'Setting up Apple Developer Installer ID for installer package codesigning...' 6 | read CODESIGN_IDENT_INSTALLER'?Apple Developer Installer ID: ' 7 | } 8 | -------------------------------------------------------------------------------- /cmake/FetchNlohmannJSON.cmake: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | 3 | FetchContent_Declare( 4 | nlohmann_json 5 | GIT_REPOSITORY https://github.com/nlohmann/json.git 6 | GIT_TAG v3.11.3) 7 | 8 | FetchContent_MakeAvailable(nlohmann_json) 9 | 10 | # Add include directories 11 | target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${nlohmann_json_SOURCE_DIR}/include) 12 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/read_codesign: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info 2 | 3 | if (( ! ${+CODESIGN_IDENT} )) { 4 | typeset -g CODESIGN_IDENT 5 | log_info 'Setting up Apple Developer ID for application codesigning...' 6 | read CODESIGN_IDENT'?Apple Developer Application ID: ' 7 | } 8 | 9 | typeset -g CODESIGN_TEAM=$(print "${CODESIGN_IDENT}" | /usr/bin/sed -En 's/.+\((.+)\)/\1/p') 10 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/log_group: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info 2 | 3 | if (( ! ${+_log_group} )) typeset -g _log_group=0 4 | 5 | if (( ${+CI} )) { 6 | if (( _log_group )) { 7 | print "::endgroup::" 8 | typeset -g _log_group=0 9 | } 10 | if (( # )) { 11 | print "::group::${@}" 12 | typeset -g _log_group=1 13 | } 14 | } else { 15 | if (( # )) log_info ${@} 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/ssl-utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void init_openssl(); 6 | std::string hmacSha256(const std::string &key, const std::string &data, bool isHexKey = false); 7 | std::string sha256(const std::string &data); 8 | std::string getCurrentTimestamp(); 9 | std::string getCurrentDate(); 10 | std::string PEMrootCerts(); 11 | std::string PEMrootCertsPath(); 12 | -------------------------------------------------------------------------------- /.github/workflows/dispatch.yaml: -------------------------------------------------------------------------------- 1 | name: Dispatch 2 | run-name: Dispatched Repository Actions - ${{ inputs.job }} ⌛️ 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | job: 7 | description: Dispatch job to run 8 | required: true 9 | type: choice 10 | options: 11 | - build 12 | permissions: 13 | contents: write 14 | jobs: 15 | check-and-build: 16 | if: inputs.job == 'build' 17 | uses: ./.github/workflows/build-project.yaml 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/set_loglevel: -------------------------------------------------------------------------------- 1 | autoload -Uz log_debug log_error 2 | 3 | local -r _usage="Usage: %B${0}%b 4 | 5 | Set log level, following levels are supported: 0 (quiet), 1 (normal), 2 (verbose), 3 (debug)" 6 | 7 | if (( ! # )); then 8 | log_error 'Called without arguments.' 9 | log_output ${_usage} 10 | return 2 11 | elif (( ${1} >= 4 )); then 12 | log_error 'Called with loglevel > 3.' 13 | log_output ${_usage} 14 | fi 15 | 16 | typeset -g -i -r _loglevel=${1} 17 | log_debug "Log level set to '${1}'" 18 | -------------------------------------------------------------------------------- /cmake/macos/resources/ccache-launcher-c.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ "$1" == "${CMAKE_C_COMPILER}" ]] ; then 4 | shift 5 | fi 6 | 7 | export CCACHE_CPP2=true 8 | export CCACHE_DEPEND=true 9 | export CCACHE_DIRECT=true 10 | export CCACHE_FILECLONE=true 11 | export CCACHE_INODECACHE=true 12 | export CCACHE_NOCOMPILERCHECK='content' 13 | export CCACHE_SLOPPINESS='include_file_mtime,include_file_ctime,clang_index_store,system_headers' 14 | if [[ "${CI}" ]]; then 15 | export CCACHE_NOHASHDIR=true 16 | fi 17 | exec "${CMAKE_C_COMPILER_LAUNCHER}" "${CMAKE_C_COMPILER}" "$@" 18 | -------------------------------------------------------------------------------- /src/timed-metadata/timed-metadata-utils.h: -------------------------------------------------------------------------------- 1 | #ifndef TIMED_METADATA_UTILS_H 2 | #define TIMED_METADATA_UTILS_H 3 | 4 | #include 5 | #include 6 | 7 | #include "cloudvocal-data.h" 8 | 9 | enum Translation_Mode { ONLY_TARGET, SOURCE_AND_TARGET, ONLY_SOURCE }; 10 | 11 | void send_timed_metadata_to_server(struct cloudvocal_data *gf, Translation_Mode mode, 12 | const std::string &source_text, const std::string &source_lang, 13 | const std::string &target_text, const std::string &target_lang); 14 | 15 | #endif // TIMED_METADATA_UTILS_H 16 | -------------------------------------------------------------------------------- /cmake/macos/resources/ccache-launcher-cxx.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ "$1" == "${CMAKE_CXX_COMPILER}" ]] ; then 4 | shift 5 | fi 6 | 7 | export CCACHE_CPP2=true 8 | export CCACHE_NODEPEND=true 9 | export CCACHE_DIRECT=true 10 | export CCACHE_FILECLONE=true 11 | export CCACHE_INODECACHE=true 12 | export CCACHE_NOCOMPILERCHECK='content' 13 | export CCACHE_SLOPPINESS='include_file_mtime,include_file_ctime,clang_index_store,system_headers' 14 | if [[ "${CI}" ]]; then 15 | export CCACHE_NOHASHDIR=true 16 | fi 17 | exec "${CMAKE_CXX_COMPILER_LAUNCHER}" "${CMAKE_CXX_COMPILER}" "$@" 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude everything 2 | /* 3 | 4 | # Except for default project files 5 | !/.github 6 | !/build-aux 7 | !/cmake 8 | !/data 9 | !/src 10 | !.clang-format 11 | !.cmake-format.json 12 | !.gitignore 13 | !buildspec.json 14 | !CMakeLists.txt 15 | !CMakePresets.json 16 | !LICENSE 17 | !README.md 18 | !conanfile.txt 19 | !conanfile_win.txt 20 | 21 | # Exclude lock files 22 | *.lock.json 23 | 24 | # Exclude macOS legacy resource forks 25 | .DS_Store 26 | 27 | # Exclude CMake build number cache 28 | /cmake/.CMakeBuildNumber 29 | 30 | # protobuf generated files 31 | *.pb.cc 32 | *.pb.h 33 | -------------------------------------------------------------------------------- /src/cloud-translation/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # add source files 2 | target_sources( 3 | ${CMAKE_PROJECT_NAME} 4 | PRIVATE # ${CMAKE_CURRENT_SOURCE_DIR}/aws.cpp 5 | ${CMAKE_CURRENT_SOURCE_DIR}/azure.cpp 6 | ${CMAKE_CURRENT_SOURCE_DIR}/claude.cpp 7 | ${CMAKE_CURRENT_SOURCE_DIR}/custom-api.cpp 8 | ${CMAKE_CURRENT_SOURCE_DIR}/deepl.cpp 9 | ${CMAKE_CURRENT_SOURCE_DIR}/google-cloud.cpp 10 | ${CMAKE_CURRENT_SOURCE_DIR}/openai.cpp 11 | ${CMAKE_CURRENT_SOURCE_DIR}/papago.cpp 12 | ${CMAKE_CURRENT_SOURCE_DIR}/translation-cloud.cpp) 13 | -------------------------------------------------------------------------------- /src/cloud-translation/deepl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | 5 | class CurlHelper; // Forward declaration 6 | 7 | class DeepLTranslator : public ITranslator { 8 | public: 9 | explicit DeepLTranslator(const std::string &api_key, bool free = false); 10 | ~DeepLTranslator() override; 11 | 12 | std::string translate(const std::string &text, const std::string &target_lang, 13 | const std::string &source_lang = "auto") override; 14 | 15 | private: 16 | std::string parseResponse(const std::string &response_str); 17 | 18 | std::string api_key_; 19 | bool free_; 20 | std::unique_ptr curl_helper_; 21 | }; 22 | -------------------------------------------------------------------------------- /src/cloudvocal.c: -------------------------------------------------------------------------------- 1 | #include "cloudvocal.h" 2 | 3 | struct obs_source_info cloudvocal_info = { 4 | .id = "cloudvocal_audio_filter", 5 | .type = OBS_SOURCE_TYPE_FILTER, 6 | .output_flags = OBS_SOURCE_AUDIO, 7 | .get_name = cloudvocal_name, 8 | .create = cloudvocal_create, 9 | .destroy = cloudvocal_destroy, 10 | .get_defaults = cloudvocal_defaults, 11 | .get_properties = cloudvocal_properties, 12 | .update = cloudvocal_update, 13 | .activate = cloudvocal_activate, 14 | .deactivate = cloudvocal_deactivate, 15 | .filter_audio = cloudvocal_filter_audio, 16 | .filter_remove = cloudvocal_remove, 17 | .show = cloudvocal_show, 18 | .hide = cloudvocal_hide, 19 | }; 20 | -------------------------------------------------------------------------------- /src/cloud-translation/google-cloud.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | 5 | class CurlHelper; // Forward declaration 6 | 7 | class GoogleTranslator : public ITranslator { 8 | public: 9 | explicit GoogleTranslator(const std::string &api_key); 10 | ~GoogleTranslator() override; 11 | 12 | std::string translate(const std::string &text, const std::string &target_lang, 13 | const std::string &source_lang = "auto") override; 14 | 15 | private: 16 | std::string parseResponse(const std::string &response_str); 17 | 18 | std::string api_key_; 19 | std::unique_ptr curl_helper_; 20 | std::string target_lang; 21 | std::string url; 22 | }; 23 | -------------------------------------------------------------------------------- /src/cloud-providers/clova/nest.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package com.nbp.cdncp.nest.grpc.proto.v1; 4 | 5 | enum RequestType { 6 | CONFIG = 0; 7 | DATA = 1; 8 | } 9 | 10 | message NestConfig { 11 | string config = 1; 12 | } 13 | 14 | message NestData { 15 | bytes chunk = 1; 16 | string extra_contents = 2; 17 | } 18 | 19 | message NestRequest { 20 | RequestType type = 1; 21 | oneof part { 22 | NestConfig config = 2; 23 | NestData data = 3; 24 | } 25 | } 26 | 27 | message NestResponse { 28 | string contents = 1; 29 | } 30 | 31 | service NestService { 32 | rpc recognize(stream NestRequest) returns (stream NestResponse){}; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/check_macos: -------------------------------------------------------------------------------- 1 | autoload -Uz is-at-least log_group log_info log_error log_status read_codesign 2 | 3 | local macos_version=$(sw_vers -productVersion) 4 | 5 | log_group 'Install macOS build requirements' 6 | log_info 'Checking macOS version...' 7 | if ! is-at-least 11.0 ${macos_version}; then 8 | log_error "Minimum required macOS version is 11.0, but running on macOS ${macos_version}" 9 | return 2 10 | else 11 | log_status "macOS ${macos_version} is recent" 12 | fi 13 | 14 | log_info 'Checking for Homebrew...' 15 | if (( ! ${+commands[brew]} )) { 16 | log_error 'No Homebrew command found. Please install Homebrew (https://brew.sh)' 17 | return 2 18 | } 19 | 20 | brew bundle --file ${SCRIPT_HOME}/.Brewfile 21 | rehash 22 | log_group 23 | -------------------------------------------------------------------------------- /.github/workflows/check-format.yaml: -------------------------------------------------------------------------------- 1 | name: Check Code Formatting 🛠️ 2 | on: 3 | workflow_call: 4 | jobs: 5 | clang-format: 6 | runs-on: ubuntu-22.04 7 | steps: 8 | - uses: actions/checkout@v4 9 | with: 10 | fetch-depth: 0 11 | - name: clang-format check 🐉 12 | id: clang-format 13 | uses: ./.github/actions/run-clang-format 14 | with: 15 | failCondition: error 16 | 17 | cmake-format: 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: cmake-format check 🎛️ 24 | id: cmake-format 25 | uses: ./.github/actions/run-cmake-format 26 | with: 27 | failCondition: error 28 | -------------------------------------------------------------------------------- /.cmake-format.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": { 3 | "line_width": 120, 4 | "tab_size": 2, 5 | "enable_sort": true, 6 | "autosort": true 7 | }, 8 | "additional_commands": { 9 | "set_target_properties_obs": { 10 | "pargs": 1, 11 | "flags": [], 12 | "kwargs": { 13 | "PROPERTIES": { 14 | "kwargs": { 15 | "PREFIX": 1, 16 | "OUTPUT_NAME": 1, 17 | "FOLDER": 1, 18 | "VERSION": 1, 19 | "SOVERSION": 1, 20 | "AUTOMOC": 1, 21 | "AUTOUIC": 1, 22 | "AUTORCC": 1, 23 | "AUTOUIC_SEARCH_PATHS": 1, 24 | "BUILD_RPATH": 1, 25 | "INSTALL_RPATH": 1 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/cloud-translation/translation-cloud.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct CloudTranslatorConfig { 6 | std::string provider; 7 | std::string access_key; // Main API key/Client ID 8 | std::string secret_key; // Secret key/Client secret 9 | std::string region; // For AWS / Azure 10 | std::string model; // For Claude 11 | bool free; // For Deepl 12 | std::string endpoint; // For Custom API 13 | std::string body; // For Custom API 14 | std::string response_json_path; // For Custom API 15 | }; 16 | 17 | std::string translate_cloud(const CloudTranslatorConfig &config, const std::string &text, 18 | const std::string &target_lang, const std::string &source_lang); 19 | -------------------------------------------------------------------------------- /src/cloud-translation/papago.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | 5 | class CurlHelper; // Forward declaration 6 | 7 | class PapagoTranslator : public ITranslator { 8 | public: 9 | PapagoTranslator(const std::string &client_id, const std::string &client_secret); 10 | ~PapagoTranslator() override; 11 | 12 | std::string translate(const std::string &text, const std::string &target_lang, 13 | const std::string &source_lang = "auto") override; 14 | 15 | private: 16 | std::string parseResponse(const std::string &response_str); 17 | bool isLanguagePairSupported(const std::string &source, const std::string &target) const; 18 | 19 | std::string client_id_; 20 | std::string client_secret_; 21 | std::unique_ptr curl_helper_; 22 | }; 23 | -------------------------------------------------------------------------------- /src/cloud-translation/openai.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | 5 | class CurlHelper; // Forward declaration 6 | 7 | class OpenAITranslator : public ITranslator { 8 | public: 9 | explicit OpenAITranslator(const std::string &api_key, 10 | const std::string &model = "gpt-4-turbo-preview"); 11 | ~OpenAITranslator() override; 12 | 13 | std::string translate(const std::string &text, const std::string &target_lang, 14 | const std::string &source_lang = "auto") override; 15 | 16 | private: 17 | std::string parseResponse(const std::string &response_str); 18 | std::string createSystemPrompt(const std::string &target_lang) const; 19 | 20 | std::string api_key_; 21 | std::string model_; 22 | std::unique_ptr curl_helper_; 23 | }; 24 | -------------------------------------------------------------------------------- /src/cloud-translation/claude.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | 5 | class CurlHelper; // Forward declaration 6 | 7 | class ClaudeTranslator : public ITranslator { 8 | public: 9 | explicit ClaudeTranslator(const std::string &api_key, 10 | const std::string &model = "claude-3-sonnet-20240229"); 11 | ~ClaudeTranslator() override; 12 | 13 | std::string translate(const std::string &text, const std::string &target_lang, 14 | const std::string &source_lang = "auto") override; 15 | 16 | private: 17 | std::string parseResponse(const std::string &response_str); 18 | std::string createSystemPrompt(const std::string &target_lang) const; 19 | 20 | std::string api_key_; 21 | std::string model_; 22 | std::unique_ptr curl_helper_; 23 | }; 24 | -------------------------------------------------------------------------------- /cmake/common/ccache.cmake: -------------------------------------------------------------------------------- 1 | # CMake ccache module 2 | 3 | include_guard(GLOBAL) 4 | 5 | if(NOT DEFINED CCACHE_PROGRAM) 6 | message(DEBUG "Trying to find ccache on build host...") 7 | find_program(CCACHE_PROGRAM "ccache") 8 | mark_as_advanced(CCACHE_PROGRAM) 9 | endif() 10 | 11 | if(CCACHE_PROGRAM) 12 | message(DEBUG "Ccache found as ${CCACHE_PROGRAM}...") 13 | option(ENABLE_CCACHE "Enable compiler acceleration with ccache" ON) 14 | 15 | if(ENABLE_CCACHE) 16 | set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 17 | set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 18 | set(CMAKE_OBJC_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 19 | set(CMAKE_OBJCXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 20 | set(CMAKE_CUDA_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") 21 | endif() 22 | endif() 23 | -------------------------------------------------------------------------------- /src/cloud-translation/azure.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | 5 | class CurlHelper; // Forward declaration 6 | 7 | class AzureTranslator : public ITranslator { 8 | public: 9 | AzureTranslator( 10 | const std::string &api_key, const std::string &location = "", 11 | const std::string &endpoint = "https://api.cognitive.microsofttranslator.com"); 12 | ~AzureTranslator() override; 13 | 14 | std::string translate(const std::string &text, const std::string &target_lang, 15 | const std::string &source_lang = "auto") override; 16 | 17 | private: 18 | std::string parseResponse(const std::string &response_str); 19 | 20 | std::string api_key_; 21 | std::string location_; 22 | std::string endpoint_; 23 | std::unique_ptr curl_helper_; 24 | }; 25 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/x86_64-linux-gcc.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR x86_64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/x86_64-linux-gnu-gcc) 6 | set(CMAKE_CXX_COMPILER /usr/bin/x86_64-linux-gnu-g++) 7 | 8 | set(CMAKE_FIND_ROOT_PATH /usr/x86_64-linux-gnu) 9 | 10 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 11 | 12 | set(PKG_CONFIG_EXECUTABLE 13 | /usr/bin/x86_64-linux-gnu-pkg-config 14 | CACHE FILEPATH "pkg-config executable") 15 | 16 | set(CPACK_READELF_EXECUTABLE /usr/bin/x86_64-linux-gnu-readelf) 17 | set(CPACK_OBJCOPY_EXECUTABLE /usr/bin/x86_64-linux-gnu-objcopy) 18 | set(CPACK_OBJDUMP_EXECUTABLE /usr/bin/x86_64-linux-gnu-objdump) 19 | set(CPACK_PACKAGE_ARCHITECTURE x86_64) 20 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE x86_64) 21 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/aarch64-linux-gcc.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR aarch64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/aarch64-linux-gnu-gcc) 6 | set(CMAKE_CXX_COMPILER /usr/bin/aarch64-linux-gnu-g++) 7 | 8 | set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) 9 | 10 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 11 | 12 | set(PKG_CONFIG_EXECUTABLE 13 | /usr/bin/aarch64-linux-gnu-pkg-config 14 | CACHE FILEPATH "pkg-config executable") 15 | 16 | set(CPACK_READELF_EXECUTABLE /usr/bin/aarch64-linux-gnu-readelf) 17 | set(CPACK_OBJCOPY_EXECUTABLE /usr/bin/aarch64-linux-gnu-objcopy) 18 | set(CPACK_OBJDUMP_EXECUTABLE /usr/bin/aarch64-linux-gnu-objdump) 19 | set(CPACK_PACKAGE_ARCHITECTURE arm64) 20 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64) 21 | -------------------------------------------------------------------------------- /src/utils/curl-helper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | class CurlHelper { 8 | public: 9 | CurlHelper(); 10 | ~CurlHelper(); 11 | 12 | // Callback for writing response data 13 | static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); 14 | 15 | // URL encode a string 16 | static std::string urlEncode(const std::string &value); 17 | 18 | // Common request builders 19 | static struct curl_slist * 20 | createBasicHeaders(const std::string &content_type = "application/json"); 21 | 22 | // Verify HTTPS certificate 23 | static void setSSLVerification(CURL *curl, bool verify = true); 24 | 25 | private: 26 | static bool is_initialized_; 27 | static std::mutex curl_mutex_; // For thread-safe global initialization 28 | }; 29 | -------------------------------------------------------------------------------- /src/cloudvocal-processing.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cloudvocal-data.h" 4 | 5 | /** 6 | * @brief Extracts audio data from the buffer, resamples it, and updates timestamp offsets. 7 | * 8 | * This function extracts audio data from the input buffer, resamples it to 16kHz, and updates 9 | * gf->resampled_buffer with the resampled data. 10 | * 11 | * @param gf Pointer to the transcription filter data structure. 12 | * @param start_timestamp_offset_ns Reference to the start timestamp offset in nanoseconds. 13 | * @param end_timestamp_offset_ns Reference to the end timestamp offset in nanoseconds. 14 | * @return Returns 0 on success, 1 if the input buffer is empty. 15 | */ 16 | int get_data_from_buf_and_resample(cloudvocal_data *gf, uint64_t &start_timestamp_offset_ns, 17 | uint64_t &end_timestamp_offset_ns); 18 | -------------------------------------------------------------------------------- /.github/workflows/pr-pull.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | run-name: ${{ github.event.pull_request.title }} pull request run 🚀 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | paths-ignore: 7 | - '**.md' 8 | branches: [master, main] 9 | types: [ opened, synchronize, reopened ] 10 | permissions: 11 | contents: read 12 | concurrency: 13 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 14 | cancel-in-progress: true 15 | jobs: 16 | check-format: 17 | name: Check Formatting 🔍 18 | uses: ./.github/workflows/check-format.yaml 19 | permissions: 20 | contents: read 21 | 22 | build-project: 23 | name: Build Project 🧱 24 | uses: ./.github/workflows/build-project.yaml 25 | secrets: inherit 26 | permissions: 27 | contents: read 28 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Ensure-Location.ps1: -------------------------------------------------------------------------------- 1 | function Ensure-Location { 2 | <# 3 | .SYNOPSIS 4 | Ensures current location to be set to specified directory. 5 | .DESCRIPTION 6 | If specified directory exists, switch to it. Otherwise create it, 7 | then switch. 8 | .EXAMPLE 9 | Ensure-Location "My-Directory" 10 | Ensure-Location -Path "Path-To-My-Directory" 11 | #> 12 | 13 | param( 14 | [Parameter(Mandatory)] 15 | [string] $Path 16 | ) 17 | 18 | if ( ! ( Test-Path $Path ) ) { 19 | $_Params = @{ 20 | ItemType = "Directory" 21 | Path = ${Path} 22 | ErrorAction = "SilentlyContinue" 23 | } 24 | 25 | New-Item @_Params | Set-Location 26 | } else { 27 | Set-Location -Path ${Path} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmake/windows/buildspec.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows build dependencies module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(buildspec_common) 6 | 7 | # _check_dependencies_windows: Set up Windows slice for _check_dependencies 8 | function(_check_dependencies_windows) 9 | set(arch ${CMAKE_GENERATOR_PLATFORM}) 10 | set(platform windows-${arch}) 11 | 12 | set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps") 13 | set(prebuilt_filename "windows-deps-VERSION-ARCH-REVISION.zip") 14 | set(prebuilt_destination "obs-deps-VERSION-ARCH") 15 | set(qt6_filename "windows-deps-qt6-VERSION-ARCH-REVISION.zip") 16 | set(qt6_destination "obs-deps-qt6-VERSION-ARCH") 17 | set(obs-studio_filename "VERSION.zip") 18 | set(obs-studio_destination "obs-studio-VERSION") 19 | set(dependencies_list prebuilt qt6 obs-studio) 20 | 21 | _check_dependencies() 22 | endfunction() 23 | 24 | _check_dependencies_windows() 25 | -------------------------------------------------------------------------------- /cmake/common/buildnumber.cmake: -------------------------------------------------------------------------------- 1 | # CMake build number module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Define build number cache file 6 | set(_BUILD_NUMBER_CACHE 7 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake/.CMakeBuildNumber" 8 | CACHE INTERNAL "OBS build number cache file") 9 | 10 | # Read build number from cache file or manual override 11 | if(NOT DEFINED PLUGIN_BUILD_NUMBER AND EXISTS "${_BUILD_NUMBER_CACHE}") 12 | file(READ "${_BUILD_NUMBER_CACHE}" PLUGIN_BUILD_NUMBER) 13 | math(EXPR PLUGIN_BUILD_NUMBER "${PLUGIN_BUILD_NUMBER}+1") 14 | elseif(NOT DEFINED PLUGIN_BUILD_NUMBER) 15 | if($ENV{CI}) 16 | if($ENV{GITHUB_RUN_ID}) 17 | set(PLUGIN_BUILD_NUMBER "$ENV{GITHUB_RUN_ID}") 18 | elseif($ENV{GITLAB_RUN_ID}) 19 | set(PLUGIN_BUILD_NUMBER "$ENV{GITLAB_RUN_ID}") 20 | else() 21 | set(PLUGIN_BUILD_NUMBER "1") 22 | endif() 23 | else() 24 | set(PLUGIN_BUILD_NUMBER "1") 25 | endif() 26 | endif() 27 | file(WRITE "${_BUILD_NUMBER_CACHE}" "${PLUGIN_BUILD_NUMBER}") 28 | -------------------------------------------------------------------------------- /src/cloud-translation/custom-api.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | #include 5 | #include 6 | 7 | class CurlHelper; // Forward declaration 8 | 9 | class CustomApiTranslator : public ITranslator { 10 | public: 11 | explicit CustomApiTranslator(const std::string &endpoint, const std::string &body_template, 12 | const std::string &response_json_path); 13 | ~CustomApiTranslator() override; 14 | 15 | std::string translate(const std::string &text, const std::string &target_lang, 16 | const std::string &source_lang = "auto") override; 17 | 18 | private: 19 | std::string 20 | replacePlaceholders(const std::string &template_str, 21 | const std::unordered_map &values) const; 22 | std::string parseResponse(const std::string &response_str); 23 | 24 | std::string endpoint_; 25 | std::string body_template_; 26 | std::string response_json_path_; 27 | std::unique_ptr curl_helper_; 28 | }; 29 | -------------------------------------------------------------------------------- /src/cloud-providers/aws/eventstream.h: -------------------------------------------------------------------------------- 1 | #ifndef EVENTSTREAM_H 2 | #define EVENTSTREAM_H 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | using json = nlohmann::json; 10 | struct EventData { 11 | std::unordered_map headers; 12 | json payload; 13 | }; 14 | 15 | // Platform-independent byte swapping functions 16 | uint16_t swap_uint16(uint16_t val); 17 | uint32_t swap_uint32(uint32_t val); 18 | 19 | // Decodes an event message 20 | EventData decode_event(const std::vector &message); 21 | 22 | // Creates an audio event message 23 | //std::vector create_audio_event(const std::vector& payload); 24 | std::vector create_audio_event(const std::vector &payload); 25 | 26 | // Generates headers for the audio event 27 | std::vector get_headers(const std::string &headerName, const std::string &headerValue); 28 | 29 | #endif // AWS_TRANSCRIBE_AUDIO_FRAME_HPP -------------------------------------------------------------------------------- /src/cloud-providers/deepgram/deepgram-provider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "cloud-providers/cloud-provider.h" 9 | 10 | namespace beast = boost::beast; 11 | namespace websocket = beast::websocket; 12 | namespace net = boost::asio; 13 | namespace ssl = boost::asio::ssl; 14 | using tcp = boost::asio::ip::tcp; 15 | 16 | class DeepgramProvider : public CloudProvider { 17 | public: 18 | DeepgramProvider(TranscriptionCallback callback, cloudvocal_data *gf_); 19 | bool init() override; 20 | 21 | protected: 22 | void sendAudioBufferToTranscription(const std::deque &audio_buffer) override; 23 | void readResultsFromTranscription() override; 24 | void shutdown() override; 25 | 26 | private: 27 | net::io_context ioc; 28 | ssl::context ssl_ctx; 29 | tcp::resolver resolver; 30 | websocket::stream> ws; 31 | }; 32 | -------------------------------------------------------------------------------- /cmake/common/osconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake operating system bootstrap module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set minimum CMake version specific to host operating system, add OS-specific module directory to default search paths, 6 | # and set helper variables for OS detection in other CMake list files. 7 | if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") 8 | set(CMAKE_C_EXTENSIONS FALSE) 9 | set(CMAKE_CXX_EXTENSIONS FALSE) 10 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows") 11 | set(OS_WINDOWS TRUE) 12 | elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") 13 | set(CMAKE_C_EXTENSIONS FALSE) 14 | set(CMAKE_CXX_EXTENSIONS FALSE) 15 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos") 16 | set(OS_MACOS TRUE) 17 | elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux|FreeBSD|OpenBSD") 18 | set(CMAKE_CXX_EXTENSIONS FALSE) 19 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/linux") 20 | string(TOUPPER "${CMAKE_HOST_SYSTEM_NAME}" _SYSTEM_NAME_U) 21 | set(OS_${_SYSTEM_NAME_U} TRUE) 22 | endif() 23 | -------------------------------------------------------------------------------- /src/cloudvocal.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef __cplusplus 4 | extern "C" { 5 | #endif 6 | 7 | #define MT_ obs_module_text 8 | 9 | void cloudvocal_activate(void *data); 10 | void *cloudvocal_create(obs_data_t *settings, obs_source_t *filter); 11 | void cloudvocal_update(void *data, obs_data_t *s); 12 | void cloudvocal_destroy(void *data); 13 | const char *cloudvocal_name(void *unused); 14 | struct obs_audio_data *cloudvocal_filter_audio(void *data, struct obs_audio_data *audio); 15 | void cloudvocal_deactivate(void *data); 16 | void cloudvocal_defaults(obs_data_t *s); 17 | obs_properties_t *cloudvocal_properties(void *data); 18 | void cloudvocal_remove(void *data, obs_source_t *source); 19 | void cloudvocal_show(void *data); 20 | void cloudvocal_hide(void *data); 21 | 22 | const char *const PLUGIN_INFO_TEMPLATE = 23 | "CloudVocal ({{plugin_version}}) by " 24 | "Locaal AI ❤️ " 25 | "Support & Follow"; 26 | 27 | #ifdef __cplusplus 28 | } 29 | #endif 30 | -------------------------------------------------------------------------------- /cmake/windows/resources/resource.rc.in: -------------------------------------------------------------------------------- 1 | 1 VERSIONINFO 2 | FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0 3 | PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},0 4 | FILEFLAGSMASK 0x0L 5 | #ifdef _DEBUG 6 | FILEFLAGS 0x1L 7 | #else 8 | FILEFLAGS 0x0L 9 | #endif 10 | FILEOS 0x0L 11 | FILETYPE 0x2L 12 | FILESUBTYPE 0x0L 13 | BEGIN 14 | BLOCK "StringFileInfo" 15 | BEGIN 16 | BLOCK "040904b0" 17 | BEGIN 18 | VALUE "CompanyName", "${PLUGIN_AUTHOR}" 19 | VALUE "FileDescription", "${PROJECT_NAME}" 20 | VALUE "FileVersion", "${PROJECT_VERSION}" 21 | VALUE "InternalName", "${PROJECT_NAME}" 22 | VALUE "LegalCopyright", "(C) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}" 23 | VALUE "OriginalFilename", "${PROJECT_NAME}" 24 | VALUE "ProductName", "${PROJECT_NAME}" 25 | VALUE "ProductVersion", "${PROJECT_VERSION}" 26 | END 27 | END 28 | BLOCK "VarFileInfo" 29 | BEGIN 30 | VALUE "Translation", 0x409, 1200 31 | END 32 | END 33 | -------------------------------------------------------------------------------- /src/cloud-translation/ITranslator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | // Custom exception 7 | class TranslationError : public std::runtime_error { 8 | public: 9 | explicit TranslationError(const std::string &message) : std::runtime_error(message) {} 10 | }; 11 | 12 | // Abstract translator interface 13 | class ITranslator { 14 | public: 15 | virtual ~ITranslator() = default; 16 | 17 | virtual std::string translate(const std::string &text, const std::string &target_lang, 18 | const std::string &source_lang = "auto") = 0; 19 | }; 20 | 21 | // Factory function declaration 22 | std::unique_ptr createTranslator(const std::string &provider, 23 | const std::string &api_key, 24 | const std::string &location = ""); 25 | 26 | inline std::string sanitize_language_code(const std::string &lang_code) 27 | { 28 | // Remove all non-alphabetic characters 29 | std::string sanitized_code; 30 | for (const char &c : lang_code) { 31 | if (isalpha((int)c)) { 32 | sanitized_code += c; 33 | } 34 | } 35 | return sanitized_code; 36 | } 37 | -------------------------------------------------------------------------------- /src/plugin-support.h: -------------------------------------------------------------------------------- 1 | /* 2 | cloudvocal 3 | Copyright (C) 2024 Roy Shilkrot roy.shil@gmail.com 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #pragma once 20 | 21 | #ifdef __cplusplus 22 | extern "C" { 23 | #endif 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | extern const char *PLUGIN_NAME; 31 | extern const char *PLUGIN_VERSION; 32 | 33 | void obs_log(int log_level, const char *format, ...); 34 | 35 | #ifdef __cplusplus 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /src/cloudvocal-callbacks.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "cloudvocal-data.h" 8 | 9 | void send_caption_to_source(const std::string &target_source_name, const std::string &str_copy, 10 | struct cloudvocal_data *gf); 11 | std::string send_sentence_to_translation(const std::string &sentence, struct cloudvocal_data *gf); 12 | 13 | void audio_chunk_callback(struct cloudvocal_data *gf, const float *pcm32f_data, size_t frames, 14 | int vad_state, const DetectionResultWithText &result); 15 | 16 | void set_text_callback(struct cloudvocal_data *gf, const DetectionResultWithText &resultIn); 17 | 18 | void clear_current_caption(cloudvocal_data *gf_); 19 | 20 | void recording_state_callback(enum obs_frontend_event event, void *data); 21 | 22 | void media_play_callback(void *data_, calldata_t *cd); 23 | void media_started_callback(void *data_, calldata_t *cd); 24 | void media_pause_callback(void *data_, calldata_t *cd); 25 | void media_restart_callback(void *data_, calldata_t *cd); 26 | void media_stopped_callback(void *data_, calldata_t *cd); 27 | void enable_callback(void *data_, calldata_t *cd); 28 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Invoke-External.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-External { 2 | <# 3 | .SYNOPSIS 4 | Invokes a non-PowerShell command. 5 | .DESCRIPTION 6 | Runs a non-PowerShell command, and captures its return code. 7 | Throws an exception if the command returns non-zero. 8 | .EXAMPLE 9 | Invoke-External 7z x $MyArchive 10 | #> 11 | 12 | if ( $args.Count -eq 0 ) { 13 | throw 'Invoke-External called without arguments.' 14 | } 15 | 16 | if ( ! ( Test-Path function:Log-Information ) ) { 17 | . $PSScriptRoot/Logger.ps1 18 | } 19 | 20 | $Command = $args[0] 21 | $CommandArgs = @() 22 | 23 | if ( $args.Count -gt 1) { 24 | $CommandArgs = $args[1..($args.Count - 1)] 25 | } 26 | 27 | $_EAP = $ErrorActionPreference 28 | $ErrorActionPreference = "Continue" 29 | 30 | Log-Debug "Invoke-External: ${Command} ${CommandArgs}" 31 | 32 | & $command $commandArgs 33 | $Result = $LASTEXITCODE 34 | 35 | $ErrorActionPreference = $_EAP 36 | 37 | if ( $Result -ne 0 ) { 38 | throw "${Command} ${CommandArgs} exited with non-zero code ${Result}." 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/setup_ccache: -------------------------------------------------------------------------------- 1 | autoload -Uz log_debug log_warning 2 | 3 | if (( ! ${+project_root} )) { 4 | log_error "'project_root' not set. Please set before running ${0}." 5 | return 2 6 | } 7 | 8 | if (( ${+commands[ccache]} )) { 9 | log_debug "Found ccache at ${commands[ccache]}" 10 | 11 | typeset -gx CCACHE_CONFIGPATH="${project_root}/.ccache.conf" 12 | 13 | ccache --set-config=run_second_cpp=true 14 | ccache --set-config=direct_mode=true 15 | ccache --set-config=inode_cache=true 16 | ccache --set-config=compiler_check=content 17 | ccache --set-config=file_clone=true 18 | 19 | local -a sloppiness=( 20 | include_file_mtime 21 | include_file_ctime 22 | file_stat_matches 23 | system_headers 24 | ) 25 | 26 | if [[ ${host_os} == macos ]] { 27 | sloppiness+=( 28 | modules 29 | clang_index_store 30 | ) 31 | 32 | ccache --set-config=sloppiness=${(j:,:)sloppiness} 33 | } 34 | 35 | if (( ${+CI} )) { 36 | ccache --set-config=cache_dir="${GITHUB_WORKSPACE:-${HOME}}/.ccache" 37 | ccache --set-config=max_size="${CCACHE_SIZE:-1G}" 38 | ccache -z > /dev/null 39 | } 40 | } else { 41 | log_warning "No ccache found on the system" 42 | } 43 | -------------------------------------------------------------------------------- /src/cloud-providers/google/google-provider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cloud-providers/cloud-provider.h" 4 | #include 5 | #include "google/cloud/speech/v1/cloud_speech.grpc.pb.h" 6 | 7 | class GoogleProvider : public CloudProvider { 8 | public: 9 | GoogleProvider(TranscriptionCallback callback, cloudvocal_data *gf_) 10 | : CloudProvider(callback, gf_), 11 | initialized(false), 12 | channel(nullptr), 13 | stub(nullptr), 14 | reader_writer(nullptr), 15 | chunk_id(1) 16 | { 17 | needs_results_thread = true; 18 | } 19 | 20 | virtual bool init() override; 21 | 22 | protected: 23 | virtual void sendAudioBufferToTranscription(const std::deque &audio_buffer) override; 24 | virtual void readResultsFromTranscription() override; 25 | virtual void shutdown() override; 26 | 27 | private: 28 | std::shared_ptr channel; 29 | std::unique_ptr stub; 30 | grpc::ClientContext context; 31 | std::unique_ptr< 32 | ::grpc::ClientReaderWriter<::google::cloud::speech::v1::StreamingRecognizeRequest, 33 | ::google::cloud::speech::v1::StreamingRecognizeResponse>> 34 | reader_writer; 35 | bool initialized; 36 | uint64_t chunk_id; 37 | }; 38 | -------------------------------------------------------------------------------- /cmake/macos/buildspec.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS build dependencies module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(buildspec_common) 6 | 7 | # _check_dependencies_macos: Set up macOS slice for _check_dependencies 8 | function(_check_dependencies_macos) 9 | set(arch universal) 10 | set(platform macos) 11 | 12 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) 13 | 14 | set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps") 15 | set(prebuilt_filename "macos-deps-VERSION-ARCH_REVISION.tar.xz") 16 | set(prebuilt_destination "obs-deps-VERSION-ARCH") 17 | set(qt6_filename "macos-deps-qt6-VERSION-ARCH-REVISION.tar.xz") 18 | set(qt6_destination "obs-deps-qt6-VERSION-ARCH") 19 | set(obs-studio_filename "VERSION.tar.gz") 20 | set(obs-studio_destination "obs-studio-VERSION") 21 | set(dependencies_list prebuilt qt6 obs-studio) 22 | 23 | _check_dependencies() 24 | 25 | execute_process(COMMAND "xattr" -r -d com.apple.quarantine "${dependencies_dir}" 26 | RESULT_VARIABLE result COMMAND_ERROR_IS_FATAL ANY) 27 | 28 | list(APPEND CMAKE_FRAMEWORK_PATH "${dependencies_dir}/Frameworks") 29 | set(CMAKE_FRAMEWORK_PATH 30 | ${CMAKE_FRAMEWORK_PATH} 31 | PARENT_SCOPE) 32 | endfunction() 33 | 34 | _check_dependencies_macos() 35 | -------------------------------------------------------------------------------- /src/cloudvocal-utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | // Get the current timestamp in milliseconds since epoch 8 | inline uint64_t now_ms() 9 | { 10 | return std::chrono::duration_cast( 11 | std::chrono::system_clock::now().time_since_epoch()) 12 | .count(); 13 | } 14 | 15 | // Get the current timestamp in nano seconds since epoch 16 | inline uint64_t now_ns() 17 | { 18 | return std::chrono::duration_cast( 19 | std::chrono::system_clock::now().time_since_epoch()) 20 | .count(); 21 | } 22 | 23 | // Convert channels number to a speaker layout 24 | inline enum speaker_layout convert_speaker_layout(uint8_t channels) 25 | { 26 | switch (channels) { 27 | case 0: 28 | return SPEAKERS_UNKNOWN; 29 | case 1: 30 | return SPEAKERS_MONO; 31 | case 2: 32 | return SPEAKERS_STEREO; 33 | case 3: 34 | return SPEAKERS_2POINT1; 35 | case 4: 36 | return SPEAKERS_4POINT0; 37 | case 5: 38 | return SPEAKERS_4POINT1; 39 | case 6: 40 | return SPEAKERS_5POINT1; 41 | case 8: 42 | return SPEAKERS_7POINT1; 43 | default: 44 | return SPEAKERS_UNKNOWN; 45 | } 46 | } 47 | 48 | void create_obs_text_source_if_needed(); 49 | 50 | bool add_sources_to_list(void *list_property, obs_source_t *source); 51 | -------------------------------------------------------------------------------- /src/plugin-main.c: -------------------------------------------------------------------------------- 1 | /* 2 | cloudvocal 3 | Copyright (C) 2024 Roy Shilkrot roy.shil@gmail.com 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #include 20 | 21 | #include "plugin-support.h" 22 | 23 | OBS_DECLARE_MODULE() 24 | OBS_MODULE_USE_DEFAULT_LOCALE(PLUGIN_NAME, "en-US") 25 | 26 | MODULE_EXPORT const char *obs_module_description(void) 27 | { 28 | return obs_module_text(PLUGIN_NAME); 29 | } 30 | 31 | extern struct obs_source_info cloudvocal_info; 32 | 33 | bool obs_module_load(void) 34 | { 35 | obs_register_source(&cloudvocal_info); 36 | obs_log(LOG_INFO, "plugin loaded successfully (version %s)", PLUGIN_VERSION); 37 | return true; 38 | } 39 | 40 | void obs_module_unload(void) 41 | { 42 | obs_log(LOG_INFO, "plugin unloaded"); 43 | } 44 | -------------------------------------------------------------------------------- /src/plugin-support.c.in: -------------------------------------------------------------------------------- 1 | /* 2 | cloudvocal 3 | Copyright (C) 2024 Roy Shilkrot roy.shil@gmail.com 4 | 5 | This program is free software; you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 2 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License along 16 | with this program. If not, see 17 | */ 18 | 19 | #include 20 | 21 | const char *PLUGIN_NAME = "@CMAKE_PROJECT_NAME@"; 22 | const char *PLUGIN_VERSION = "@CMAKE_PROJECT_VERSION@"; 23 | 24 | extern void blogva(int log_level, const char *format, va_list args); 25 | 26 | void obs_log(int log_level, const char *format, ...) 27 | { 28 | size_t length = 4 + strlen(PLUGIN_NAME) + strlen(format); 29 | 30 | char *template = malloc(length + 1); 31 | 32 | snprintf(template, length, "[%s] %s", PLUGIN_NAME, format); 33 | 34 | va_list(args); 35 | 36 | va_start(args, format); 37 | blogva(log_level, template, args); 38 | va_end(args); 39 | 40 | free(template); 41 | } 42 | -------------------------------------------------------------------------------- /src/cloud-translation/aws.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ITranslator.h" 3 | #include 4 | #include 5 | #include 6 | 7 | class CurlHelper; // Forward declaration 8 | 9 | class AWSTranslator : public ITranslator { 10 | public: 11 | AWSTranslator(const std::string &access_key, const std::string &secret_key, 12 | const std::string ®ion = "us-east-1"); 13 | ~AWSTranslator() override; 14 | 15 | std::string translate(const std::string &text, const std::string &target_lang, 16 | const std::string &source_lang = "auto") override; 17 | 18 | private: 19 | // AWS Signature V4 helper functions 20 | std::string createSigningKey(const std::string &date_stamp) const; 21 | std::string calculateSignature(const std::string &string_to_sign, 22 | const std::string &signing_key) const; 23 | std::string getSignedHeaders(const std::map &headers) const; 24 | std::string sha256(const std::string &str) const; 25 | std::string hmacSha256(const std::string &key, const std::string &data) const; 26 | 27 | // Response handling 28 | std::string parseResponse(const std::string &response_str); 29 | 30 | std::string access_key_; 31 | std::string secret_key_; 32 | std::string region_; 33 | std::unique_ptr curl_helper_; 34 | 35 | // AWS specific constants 36 | const std::string SERVICE_NAME = "translate"; 37 | const std::string ALGORITHM = "AWS4-HMAC-SHA256"; 38 | }; 39 | -------------------------------------------------------------------------------- /cmake/macos/resources/distribution.in: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | @CMAKE_PROJECT_NAME@ 10 | 11 | 12 | 13 | 14 | 15 | 16 | #@CMAKE_PROJECT_NAME@.pkg 17 | 18 | 33 | 34 | -------------------------------------------------------------------------------- /src/cloud-providers/revai/revai-provider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cloud-providers/cloud-provider.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace beast = boost::beast; 21 | namespace websocket = beast::websocket; 22 | namespace net = boost::asio; 23 | namespace ssl = boost::asio::ssl; 24 | using tcp = boost::asio::ip::tcp; 25 | 26 | class RevAIProvider : public CloudProvider { 27 | public: 28 | RevAIProvider(TranscriptionCallback callback, cloudvocal_data *gf); 29 | 30 | virtual bool init() override; 31 | 32 | protected: 33 | virtual void sendAudioBufferToTranscription(const std::deque &audio_buffer) override; 34 | virtual void readResultsFromTranscription() override; 35 | virtual void shutdown() override; 36 | 37 | private: 38 | // Utility functions 39 | std::vector convertFloatToS16LE(const std::deque &audio_buffer); 40 | 41 | // Member variables 42 | bool is_connected; 43 | std::string job_id; 44 | 45 | net::io_context ioc_; 46 | ssl::context ctx_; 47 | websocket::stream> ws_; 48 | const std::string host_ = "api.rev.ai"; 49 | const std::string target_ = "/speechtotext/v1/stream"; 50 | }; 51 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/read_codesign_pass: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # Apple Developer credentials necessary: 3 | # 4 | # + Signing for distribution and notarization require an active Apple 5 | # Developer membership 6 | # + An Apple Development identity is needed for code signing 7 | # (i.e. 'Apple Development: YOUR APPLE ID (PROVIDER)') 8 | # + Your Apple developer ID is needed for notarization 9 | # + An app-specific password is necessary for notarization from CLI 10 | # + This password will be stored in your macOS keychain under the identifier 11 | # 'OBS-Codesign-Password'with access Apple's 'altool' only. 12 | ############################################################################## 13 | 14 | autoload -Uz read_codesign read_codesign_user log_info log_warning 15 | 16 | if (( ! ${+CODESIGN_IDENT} )) { 17 | read_codesign 18 | } 19 | 20 | if (( ! ${+CODESIGN_IDENT_USER} )) { 21 | read_codesign_user 22 | } 23 | 24 | log_info 'Setting up password for notarization keychain...' 25 | if (( ! ${+CODESIGN_IDENT_PASS} )) { 26 | read -s CODESIGN_IDENT_PASS'?Apple Developer ID password: ' 27 | } 28 | 29 | print '' 30 | log_info 'Setting up notarization keychain...' 31 | log_warning " 32 | + Your Apple ID and an app-specific password is necessary for notarization from CLI 33 | + This password will be stored in your macOS keychain under the identifier 34 | 'OBS-Codesign-Password' with access Apple's 'altool' only. 35 | 36 | " 37 | xcrun notarytool store-credentials 'OBS-Codesign-Password' --apple-id "${CODESIGN_IDENT_USER}" --team-id "${CODESIGN_TEAM}" --password "${CODESIGN_IDENT_PASS}" 38 | 39 | -------------------------------------------------------------------------------- /cmake/BuildClovaAPIs.cmake: -------------------------------------------------------------------------------- 1 | set(CLOVA_SOURCE_DIR ${CMAKE_SOURCE_DIR}/src/cloud-providers/clova) 2 | set(PROTO_FILE ${CLOVA_SOURCE_DIR}/nest.proto) 3 | set(CLOVA_OUTPUT_DIR ${CLOVA_SOURCE_DIR}) 4 | 5 | message(STATUS "Generating C++ code from ${PROTO_FILE}") 6 | 7 | # run protoc to generate the grpc files 8 | add_custom_command( 9 | OUTPUT ${CLOVA_SOURCE_DIR}/nest.pb.cc ${CLOVA_SOURCE_DIR}/nest.pb.h ${CLOVA_SOURCE_DIR}/nest.grpc.pb.cc 10 | ${CLOVA_SOURCE_DIR}/nest.grpc.pb.h 11 | COMMAND ${PROTOC_EXECUTABLE} --cpp_out=${CLOVA_OUTPUT_DIR} --grpc_out=${CLOVA_OUTPUT_DIR} 12 | --plugin=protoc-gen-grpc=${GRPC_PLUGIN_EXECUTABLE} -I ${CLOVA_OUTPUT_DIR} ${PROTO_FILE} 13 | DEPENDS ${PROTO_FILE}) 14 | 15 | add_library(clova-apis ${CLOVA_SOURCE_DIR}/nest.pb.cc ${CLOVA_SOURCE_DIR}/nest.grpc.pb.cc) 16 | 17 | # disable conversion warnings from the generated files 18 | if(MSVC) 19 | target_compile_options(clova-apis PRIVATE /wd4244 /wd4267 /wd4099) 20 | else() 21 | target_compile_options( 22 | clova-apis 23 | PRIVATE -fPIC 24 | -Wno-conversion 25 | -Wno-sign-conversion 26 | -Wno-unused-parameter 27 | -Wno-unused-variable 28 | -Wno-error=shadow 29 | -Wno-shadow 30 | -Wno-error=conversion) 31 | endif() 32 | 33 | # Add include directories 34 | target_include_directories(clova-apis PUBLIC ${CLOVA_OUTPUT_DIR} ${DEPS_INCLUDE_DIRS}) 35 | 36 | # link the grpc libraries 37 | target_link_libraries(clova-apis PRIVATE ${DEPS_LIBRARIES}) 38 | target_link_directories(clova-apis PRIVATE ${DEPS_LIB_DIRS}) 39 | 40 | # link the library to the main project 41 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE clova-apis) 42 | -------------------------------------------------------------------------------- /src/cloud-providers/aws/aws_provider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "cloud-providers/cloud-provider.h" 12 | 13 | namespace Aws { 14 | namespace TranscribeStreamingService { 15 | class TranscribeStreamingServiceClient; 16 | namespace Model { 17 | class StartStreamTranscriptionRequest; 18 | class StartStreamTranscriptionHandler; 19 | } // namespace Model 20 | } // namespace TranscribeStreamingService 21 | } // namespace Aws 22 | 23 | class AWSProvider : public CloudProvider { 24 | public: 25 | AWSProvider(TranscriptionCallback callback, cloudvocal_data *gf) 26 | : CloudProvider(callback, gf) 27 | { 28 | needs_results_thread = false; 29 | } 30 | 31 | virtual ~AWSProvider() {} 32 | 33 | virtual bool init() override; 34 | 35 | protected: 36 | virtual void sendAudioBufferToTranscription(const std::deque &audio_buffer) override; 37 | 38 | virtual void readResultsFromTranscription() override; 39 | 40 | virtual void shutdown() override; 41 | 42 | private: 43 | std::shared_ptr client; 44 | std::shared_ptr 45 | request; 46 | std::shared_ptr 47 | handler; 48 | 49 | std::queue> audio_buffer_queue; 50 | std::mutex audio_buffer_queue_mutex; 51 | std::condition_variable audio_buffer_queue_cv; 52 | std::atomic stream_open = false; 53 | }; 54 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/setup_linux: -------------------------------------------------------------------------------- 1 | autoload -Uz log_error log_status log_info mkcd 2 | 3 | if (( ! ${+project_root} )) { 4 | log_error "'project_root' not set. Please set before running ${0}." 5 | return 2 6 | } 7 | 8 | if (( ! ${+target} )) { 9 | log_error "'target' not set. Please set before running ${0}." 10 | return 2 11 | } 12 | 13 | pushd ${project_root} 14 | 15 | typeset -g QT_VERSION 16 | 17 | local -a apt_args=( 18 | ${CI:+-y} 19 | --no-install-recommends 20 | ) 21 | if (( _loglevel == 0 )) apt_args+=(--quiet) 22 | 23 | if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) { 24 | log_group 'Installing obs-studio build dependencies...' 25 | 26 | local suffix 27 | if [[ ${CPUTYPE} != "${target##*-}" ]] { 28 | local -A arch_mappings=( 29 | aarch64 arm64 30 | x86_64 amd64 31 | ) 32 | 33 | suffix=":${arch_mappings[${target##*-}]}" 34 | 35 | sudo apt-get install ${apt_args} gcc-${${target##*-}//_/-}-linux-gnu g++-${${target##*-}//_/-}-linux-gnu 36 | } 37 | 38 | sudo add-apt-repository --yes ppa:obsproject/obs-studio 39 | sudo apt update 40 | 41 | sudo apt-get install ${apt_args} \ 42 | build-essential \ 43 | libgles2-mesa-dev \ 44 | obs-studio 45 | 46 | local -a _qt_packages=() 47 | 48 | if (( QT_VERSION == 5 )) { 49 | _qt_packages+=( 50 | qtbase5-dev${suffix} 51 | libqt5svg5-dev${suffix} 52 | qtbase5-private-dev${suffix} 53 | libqt5x11extras5-dev${suffix} 54 | ) 55 | } else { 56 | _qt_packages+=( 57 | qt6-base-dev${suffix} 58 | libqt6svg6-dev${suffix} 59 | qt6-base-private-dev${suffix} 60 | ) 61 | } 62 | 63 | sudo apt-get install ${apt_args} ${_qt_packages} 64 | log_group 65 | } 66 | -------------------------------------------------------------------------------- /src/cloud-providers/cloud-provider.cpp: -------------------------------------------------------------------------------- 1 | #include "cloud-provider.h" 2 | #include "cloudvocal-callbacks.h" 3 | #include "clova/clova-provider.h" 4 | #include "google/google-provider.h" 5 | #include "aws/aws_provider.h" 6 | #include "revai/revai-provider.h" 7 | #include "deepgram/deepgram-provider.h" 8 | 9 | std::shared_ptr createCloudProvider(const std::string &providerType, 10 | CloudProvider::TranscriptionCallback callback, 11 | cloudvocal_data *gf) 12 | { 13 | if (providerType == "clova") { 14 | return std::make_shared(callback, gf); 15 | } else if (providerType == "google") { 16 | return std::make_unique(callback, gf); 17 | } else if (providerType == "aws") { 18 | return std::make_unique(callback, gf); 19 | } else if (providerType == "revai") { 20 | return std::make_unique(callback, gf); 21 | } else if (providerType == "deepgram") { 22 | return std::make_unique(callback, gf); 23 | } 24 | 25 | return nullptr; // Return nullptr if no matching provider is found 26 | } 27 | 28 | void restart_cloud_provider(cloudvocal_data *gf) 29 | { 30 | // stop the current cloud provider 31 | if (gf->cloud_provider != nullptr) { 32 | gf->cloud_provider->stop(); 33 | gf->cloud_provider = nullptr; 34 | } 35 | gf->cloud_provider = createCloudProvider( 36 | gf->cloud_provider_selection, 37 | [gf](const DetectionResultWithText &result) { 38 | // callback 39 | set_text_callback(gf, result); 40 | }, 41 | gf); 42 | if (gf->cloud_provider == nullptr) { 43 | obs_log(LOG_ERROR, "Failed to create cloud provider '%s'", 44 | gf->cloud_provider_selection.c_str()); 45 | gf->active = false; 46 | return; 47 | } 48 | gf->cloud_provider->start(); 49 | } 50 | -------------------------------------------------------------------------------- /src/cloud-providers/clova/clova-provider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cloud-providers/cloud-provider.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "cloud-providers/clova/nest.grpc.pb.h" 13 | #include "nlohmann/json.hpp" 14 | 15 | using grpc::ClientReaderWriter; 16 | using grpc::Channel; 17 | using grpc::ClientContext; 18 | 19 | using NestRequest = com::nbp::cdncp::nest::grpc::proto::v1::NestRequest; 20 | using NestResponse = com::nbp::cdncp::nest::grpc::proto::v1::NestResponse; 21 | using NestService = com::nbp::cdncp::nest::grpc::proto::v1::NestService; 22 | typedef grpc::ClientReaderWriter ClovaReaderWriter; 23 | 24 | class ClovaProvider : public CloudProvider { 25 | public: 26 | ClovaProvider(TranscriptionCallback callback, cloudvocal_data *gf_) 27 | : CloudProvider(callback, gf_), 28 | chunk_id(1), 29 | reader_writer(nullptr), 30 | channel(nullptr), 31 | stub(nullptr), 32 | initialized(false), 33 | current_sentence("") 34 | { 35 | needs_results_thread = true; 36 | } 37 | 38 | virtual bool init() override; 39 | 40 | protected: 41 | virtual void sendAudioBufferToTranscription(const std::deque &audio_buffer) override; 42 | virtual void readResultsFromTranscription() override; 43 | virtual void shutdown() override; 44 | 45 | private: 46 | std::map chunk_start_times; 47 | uint64_t chunk_id; 48 | std::unique_ptr reader_writer; 49 | std::shared_ptr channel; 50 | std::unique_ptr stub; 51 | ClientContext context; 52 | std::map chunk_latencies; 53 | std::string current_sentence; 54 | bool initialized; 55 | }; 56 | -------------------------------------------------------------------------------- /cmake/macos/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS defaults module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set empty codesigning team if not specified as cache variable 6 | if(NOT CODESIGN_TEAM) 7 | set(CODESIGN_TEAM 8 | "" 9 | CACHE STRING "OBS code signing team for macOS" FORCE) 10 | 11 | # Set ad-hoc codesigning identity if not specified as cache variable 12 | if(NOT CODESIGN_IDENTITY) 13 | set(CODESIGN_IDENTITY 14 | "-" 15 | CACHE STRING "OBS code signing identity for macOS" FORCE) 16 | endif() 17 | endif() 18 | 19 | if(XCODE) 20 | include(xcode) 21 | endif() 22 | 23 | include(buildspec) 24 | 25 | # Set default deployment target to 11.0 if not set and enable selection in GUI up to 13.0 26 | if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) 27 | set(CMAKE_OSX_DEPLOYMENT_TARGET 28 | 11.0 29 | CACHE STRING "Minimum macOS version to target for deployment (at runtime). Newer APIs will be weak-linked." FORCE) 30 | endif() 31 | set_property(CACHE CMAKE_OSX_DEPLOYMENT_TARGET PROPERTY STRINGS 13.0 12.0 11.0) 32 | 33 | # Use Applications directory as default install destination 34 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 35 | set(CMAKE_INSTALL_PREFIX 36 | "$ENV{HOME}/Library/Application Support/obs-studio/plugins" 37 | CACHE STRING "Directory to install OBS after building" FORCE) 38 | endif() 39 | 40 | # Enable find_package targets to become globally available targets 41 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 42 | # Enable RPATH support for generated binaries 43 | set(CMAKE_MACOSX_RPATH TRUE) 44 | # Use RPATHs from build tree _in_ the build tree 45 | set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) 46 | # Do not add default linker search paths to RPATH 47 | set(CMAKE_INSTALL_RPATH_USE_LINK_PATH FALSE) 48 | # Use common bundle-relative RPATH for installed targets 49 | set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks") 50 | -------------------------------------------------------------------------------- /.github/scripts/utils.zsh/check_linux: -------------------------------------------------------------------------------- 1 | autoload -Uz log_info log_status log_error log_debug log_warning log_group 2 | 3 | log_group 'Check Linux build requirements' 4 | log_debug 'Checking Linux distribution name and version...' 5 | 6 | # Check for Ubuntu version 22.10 or later, which have srt and librist available via apt-get 7 | typeset -g -i UBUNTU_2210_OR_LATER=0 8 | if [[ -f /etc/os_release ]] { 9 | local dist_name 10 | local dist_version 11 | read -r dist_name dist_version <<< "$(source /etc/os_release; print "${NAME} ${VERSION_ID}")" 12 | 13 | autoload -Uz is-at-least 14 | if [[ ${dist_name} == Ubuntu ]] && is-at-least 22.10 ${dist_version}; then 15 | typeset -g -i UBUNTU_2210_OR_LATER=1 16 | fi 17 | } 18 | 19 | log_debug 'Checking for apt-get...' 20 | if (( ! ${+commands[apt-get]} )) { 21 | log_error 'No apt-get command found. Please install apt' 22 | return 2 23 | } else { 24 | log_debug "Apt-get located at ${commands[apt-get]}" 25 | } 26 | 27 | local -a dependencies=("${(fA)$(<${SCRIPT_HOME}/.Aptfile)}") 28 | local -a install_list 29 | local binary 30 | 31 | sudo apt-get update -qq 32 | 33 | for dependency (${dependencies}) { 34 | local -a tokens=(${=dependency//(,|:|\')/}) 35 | 36 | if [[ ! ${tokens[1]} == 'package' ]] continue 37 | 38 | if [[ ${#tokens} -gt 2 && ${tokens[3]} == 'bin' ]] { 39 | binary=${tokens[4]} 40 | } else { 41 | binary=${tokens[2]} 42 | } 43 | 44 | if (( ! ${+commands[${binary}]} )) install_list+=(${tokens[2]}) 45 | } 46 | 47 | log_debug "List of dependencies to install: ${install_list}" 48 | if (( ${#install_list} )) { 49 | if (( ! ${+CI} )) log_warning 'Dependency installation via apt may require elevated privileges' 50 | 51 | local -a apt_args=( 52 | ${CI:+-y} 53 | --no-install-recommends 54 | ) 55 | if (( _loglevel == 0 )) apt_args+=(--quiet) 56 | 57 | sudo apt-get ${apt_args} install ${install_list} 58 | } 59 | 60 | rehash 61 | log_group 62 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/x86_64-linux-clang.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR x86_64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/clang) 6 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 7 | 8 | set(CMAKE_C_COMPILER_TARGET x86_64-linux-gnu) 9 | set(CMAKE_CXX_COMPILER_TARGET x86_64-linux-gnu) 10 | 11 | set(CMAKE_FIND_ROOT_PATH /usr/x86_64-linux-gnu) 12 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 13 | set(PKG_CONFIG_EXECUTABLE 14 | /usr/bin/x86_64-linux-gnu-pkg-config 15 | CACHE FILEPATH "pkg-config executable") 16 | 17 | execute_process( 18 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ranlib 19 | OUTPUT_VARIABLE CMAKE_RANLIB 20 | OUTPUT_STRIP_TRAILING_WHITESPACE) 21 | 22 | execute_process( 23 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ar 24 | OUTPUT_VARIABLE CMAKE_LLVM_AR 25 | OUTPUT_STRIP_TRAILING_WHITESPACE) 26 | 27 | execute_process( 28 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-readelf 29 | OUTPUT_VARIABLE READELF 30 | OUTPUT_STRIP_TRAILING_WHITESPACE) 31 | 32 | execute_process( 33 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objcopy 34 | OUTPUT_VARIABLE CMAKE_LLVM_OBJCOPY 35 | OUTPUT_STRIP_TRAILING_WHITESPACE) 36 | 37 | execute_process( 38 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objdump 39 | OUTPUT_VARIABLE CMAKE_LLVM_OBJDUMP 40 | OUTPUT_STRIP_TRAILING_WHITESPACE) 41 | 42 | set(CMAKE_AR 43 | "${CMAKE_LLVM_AR}" 44 | CACHE INTERNAL "${CMAKE_SYSTEM_NAME} ar" FORCE) 45 | set(CMAKE_OBJCOPY 46 | "${CMAKE_LLVM_OBJCOPY}" 47 | CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objcopy" FORCE) 48 | set(CMAKE_OBJDUMP 49 | "${CMAKE_LLVM_OBJDUMP}" 50 | CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objdump" FORCE) 51 | 52 | set(CPACK_READELF_EXECUTABLE "${READELF}") 53 | set(CPACK_OBJCOPY_EXECUTABLE "${CMAKE_LLVM_OBJCOPY}") 54 | set(CPACK_OBJDUMP_EXECUTABLE "${CMAKE_LLVM_OBJDUMP}") 55 | set(CPACK_PACKAGE_ARCHITECTURE x86_64) 56 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE x86_64) 57 | -------------------------------------------------------------------------------- /cmake/linux/toolchains/aarch64-linux-clang.cmake: -------------------------------------------------------------------------------- 1 | set(CMAKE_SYSTEM_NAME Linux) 2 | set(CMAKE_SYSTEM_PROCESSOR aarch64) 3 | set(CMAKE_CROSSCOMPILING TRUE) 4 | 5 | set(CMAKE_C_COMPILER /usr/bin/clang) 6 | set(CMAKE_CXX_COMPILER /usr/bin/clang++) 7 | 8 | set(CMAKE_C_COMPILER_TARGET aarch64-linux-gnu) 9 | set(CMAKE_CXX_COMPILER_TARGET aarch64-linux-gnu) 10 | 11 | set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu) 12 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 13 | set(PKG_CONFIG_EXECUTABLE 14 | /usr/bin/aarch64-linux-gnu-pkg-config 15 | CACHE FILEPATH "pkg-config executable") 16 | 17 | execute_process( 18 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ranlib 19 | OUTPUT_VARIABLE CMAKE_RANLIB 20 | OUTPUT_STRIP_TRAILING_WHITESPACE) 21 | 22 | execute_process( 23 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-ar 24 | OUTPUT_VARIABLE CMAKE_LLVM_AR 25 | OUTPUT_STRIP_TRAILING_WHITESPACE) 26 | 27 | execute_process( 28 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-readelf 29 | OUTPUT_VARIABLE READELF 30 | OUTPUT_STRIP_TRAILING_WHITESPACE) 31 | 32 | execute_process( 33 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objcopy 34 | OUTPUT_VARIABLE CMAKE_LLVM_OBJCOPY 35 | OUTPUT_STRIP_TRAILING_WHITESPACE) 36 | 37 | execute_process( 38 | COMMAND ${CMAKE_C_COMPILER} -print-prog-name=llvm-objdump 39 | OUTPUT_VARIABLE CMAKE_LLVM_OBJDUMP 40 | OUTPUT_STRIP_TRAILING_WHITESPACE) 41 | 42 | set(CMAKE_AR 43 | "${CMAKE_LLVM_AR}" 44 | CACHE INTERNAL "${CMAKE_SYSTEM_NAME} ar" FORCE) 45 | set(CMAKE_OBJCOPY 46 | "${CMAKE_LLVM_OBJCOPY}" 47 | CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objcopy" FORCE) 48 | set(CMAKE_OBJDUMP 49 | "${CMAKE_LLVM_OBJDUMP}" 50 | CACHE INTERNAL "${CMAKE_SYSTEM_NAME} objdump" FORCE) 51 | 52 | set(CPACK_READELF_EXECUTABLE "${READELF}") 53 | set(CPACK_OBJCOPY_EXECUTABLE "${CMAKE_LLVM_OBJCOPY}") 54 | set(CPACK_OBJDUMP_EXECUTABLE "${CMAKE_LLVM_OBJDUMP}") 55 | set(CPACK_PACKAGE_ARCHITECTURE arm64) 56 | set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64) 57 | -------------------------------------------------------------------------------- /data/locale/en-US.ini: -------------------------------------------------------------------------------- 1 | cloudvocalAudioFilter="CloudVocal Captions" 2 | advanced_settings_mode="Advanced settings mode" 3 | simple_mode="Simple mode" 4 | advanced_mode="Advanced mode" 5 | general_group="General" 6 | transcription_cloud_provider="Transcription provider" 7 | subtitle_sources="Output source" 8 | none_no_output="None (no output)" 9 | language="Language" 10 | translate_cloud="Translation" 11 | transcription_cloud_provider_api_key="API Key" 12 | transcription_cloud_provider_secret_key="Secret Key" 13 | translate_cloud_explaination="Real-time translation. Requires an API key." 14 | translate_cloud_provider="Translation provider" 15 | target_language="Target language" 16 | translate_output="Translate output" 17 | translate_cloud_only_full_sentences="Translate only full sentences" 18 | translate_cloud_api_key="API Key" 19 | translate_cloud_secret_key="Secret Key" 20 | file_output_group="File output" 21 | file_output_info="Save subtitles to file" 22 | output_filename="Output filename" 23 | save_srt="Save in SRT format" 24 | truncate_output="Truncate output on new sentence" 25 | only_while_recording="Only write to file while recording" 26 | rename_file_to_match_recording="Rename file to match recording" 27 | advanced_group="Advanced" 28 | caption_to_stream="Caption to stream" 29 | min_sub_duration="Min. sub duration" 30 | max_sub_duration="Max. sub duration" 31 | process_while_muted="Process while muted" 32 | log_group="Logging settings" 33 | log_words="Log words" 34 | log_level="Log level" 35 | partial_transcription="Partial transcription" 36 | partial_transcription_info="Transcribe partial sentences" 37 | partial_latency="Partial latency" 38 | timed_metadata_parameters="Timed metadata parameters" 39 | timed_metadata_parameters_info="Timed metadata allows sending captions metadata to a cloud stream channel." 40 | timed_metadata_channel_arn="Channel ARN" 41 | timed_metadata_aws_access_key="AWS Access Key ID" 42 | timed_metadata_aws_secret_key="AWS Secret Key" 43 | timed_metadata_aws_region="AWS Region" 44 | -------------------------------------------------------------------------------- /cmake/common/helpers_common.cmake: -------------------------------------------------------------------------------- 1 | # CMake common helper functions module 2 | 3 | # cmake-format: off 4 | # cmake-lint: disable=C0103 5 | # cmake-format: on 6 | 7 | include_guard(GLOBAL) 8 | 9 | # check_uuid: Helper function to check for valid UUID 10 | function(check_uuid uuid_string return_value) 11 | set(valid_uuid TRUE) 12 | set(uuid_token_lengths 8 4 4 4 12) 13 | set(token_num 0) 14 | 15 | string(REPLACE "-" ";" uuid_tokens ${uuid_string}) 16 | list(LENGTH uuid_tokens uuid_num_tokens) 17 | 18 | if(uuid_num_tokens EQUAL 5) 19 | message(DEBUG "UUID ${uuid_string} is valid with 5 tokens.") 20 | foreach(uuid_token IN LISTS uuid_tokens) 21 | list(GET uuid_token_lengths ${token_num} uuid_target_length) 22 | string(LENGTH "${uuid_token}" uuid_actual_length) 23 | if(uuid_actual_length EQUAL uuid_target_length) 24 | string(REGEX MATCH "[0-9a-fA-F]+" uuid_hex_match ${uuid_token}) 25 | if(NOT uuid_hex_match STREQUAL uuid_token) 26 | set(valid_uuid FALSE) 27 | break() 28 | endif() 29 | else() 30 | set(valid_uuid FALSE) 31 | break() 32 | endif() 33 | math(EXPR token_num "${token_num}+1") 34 | endforeach() 35 | else() 36 | set(valid_uuid FALSE) 37 | endif() 38 | message(DEBUG "UUID ${uuid_string} valid: ${valid_uuid}") 39 | set(${return_value} 40 | ${valid_uuid} 41 | PARENT_SCOPE) 42 | endfunction() 43 | 44 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/plugin-support.c.in") 45 | configure_file(src/plugin-support.c.in plugin-support.c @ONLY) 46 | add_library(plugin-support STATIC) 47 | target_sources( 48 | plugin-support 49 | PRIVATE plugin-support.c 50 | PUBLIC src/plugin-support.h) 51 | target_include_directories(plugin-support PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") 52 | if(OS_LINUX 53 | OR OS_FREEBSD 54 | OR OS_OPENBSD) 55 | # add fPIC on Linux to prevent shared object errors 56 | set_property(TARGET plugin-support PROPERTY POSITION_INDEPENDENT_CODE ON) 57 | endif() 58 | endif() 59 | -------------------------------------------------------------------------------- /cmake/macos/resources/create-package.cmake.in: -------------------------------------------------------------------------------- 1 | make_directory("$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins") 2 | 3 | if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin" AND NOT IS_SYMLINK "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin") 4 | file(INSTALL DESTINATION "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins" 5 | TYPE DIRECTORY FILES "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin" USE_SOURCE_PERMISSIONS) 6 | 7 | if(CMAKE_INSTALL_CONFIG_NAME MATCHES "^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$" OR CMAKE_INSTALL_CONFIG_NAME MATCHES "^([Mm][Ii][Nn][Ss][Ii][Zz][Ee][Rr][Ee][Ll])$") 8 | if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM" AND NOT IS_SYMLINK "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM") 9 | file(INSTALL DESTINATION "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package/Library/Application Support/obs-studio/plugins" TYPE DIRECTORY FILES "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.plugin.dSYM" USE_SOURCE_PERMISSIONS) 10 | endif() 11 | endif() 12 | endif() 13 | 14 | make_directory("$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp") 15 | 16 | execute_process( 17 | COMMAND /usr/bin/pkgbuild 18 | --identifier '@MACOS_BUNDLEID@' 19 | --version '@CMAKE_PROJECT_VERSION@' 20 | --root "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package" 21 | "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp/@CMAKE_PROJECT_NAME@.pkg" 22 | COMMAND_ERROR_IS_FATAL ANY 23 | ) 24 | 25 | execute_process( 26 | COMMAND /usr/bin/productbuild 27 | --distribution "@CMAKE_CURRENT_BINARY_DIR@/distribution" 28 | --package-path "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp" 29 | "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.pkg" 30 | COMMAND_ERROR_IS_FATAL ANY) 31 | 32 | if(EXISTS "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/@CMAKE_PROJECT_NAME@.pkg") 33 | file(REMOVE_RECURSE "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/temp") 34 | file(REMOVE_RECURSE "$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/package") 35 | endif() 36 | -------------------------------------------------------------------------------- /src/cloud-providers/aws/presigned_url.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | class AWSTranscribePresignedURL { 5 | public: 6 | AWSTranscribePresignedURL(const std::string &access_key, const std::string &secret_key, 7 | const std::string ®ion = "us-west-2"); 8 | std::string get_request_url(int sample_rate, const std::string &language_code, 9 | const std::string &media_encoding, int number_of_channels = 1, 10 | bool enable_channel_identification = false); 11 | 12 | private: 13 | std::string access_key_; 14 | std::string secret_key_; 15 | std::string method_; 16 | std::string service_; 17 | std::string region_; 18 | std::string endpoint_; 19 | std::string host_; 20 | std::string amz_date_; 21 | std::string datestamp_; 22 | std::string canonical_uri_; 23 | std::string canonical_headers_; 24 | std::string signed_headers_; 25 | std::string algorithm_; 26 | std::string credential_scope_; 27 | std::string canonical_querystring_; 28 | std::string payload_hash_; 29 | std::string canonical_request_; 30 | std::string string_to_sign_; 31 | std::string signature_; 32 | std::string request_url_; 33 | void create_canonical_querystring(int sample_rate, const std::string &language_code, 34 | const std::string &media_encoding, int number_of_channels, 35 | bool enable_channel_identification); 36 | void create_canonical_request(); 37 | void create_string_to_sign(); 38 | void create_signature(); 39 | void create_url(); 40 | void AWSTranscribePresignedURL::debug_print(const std::string &stage, 41 | const std::string &value); 42 | 43 | static std::string AWSTranscribePresignedURL::CreateCanonicalQueryString( 44 | const std::string &dateTimeString, const std::string &credentialScope, 45 | const std::string &languageCode, const std::string &mediaEncoding, 46 | const std::string &sampleRate, const std::string &accessKey); 47 | static std::string AWSTranscribePresignedURL::UrlEncode(const std::string &value); 48 | static std::string 49 | AWSTranscribePresignedURL::to_hex(const std::vector &data); 50 | }; -------------------------------------------------------------------------------- /src/language-codes/language-codes.h: -------------------------------------------------------------------------------- 1 | #ifndef LANGUAGE_CODES_H 2 | #define LANGUAGE_CODES_H 3 | 4 | #include 5 | #include 6 | 7 | extern std::map language_codes; 8 | extern std::map language_codes_reverse; 9 | extern std::map language_codes_to_underscore; 10 | extern std::map language_codes_from_underscore; 11 | extern std::map language_codes_to_locale; 12 | 13 | inline bool isLanguageSupported(const std::string &lang_code) 14 | { 15 | return language_codes.find(lang_code) != language_codes.end() || 16 | language_codes_to_underscore.find(lang_code) != language_codes_to_underscore.end(); 17 | } 18 | 19 | inline std::string getLanguageName(const std::string &lang_code) 20 | { 21 | auto it = language_codes.find(lang_code); 22 | if (it != language_codes.end()) { 23 | return it->second; 24 | } 25 | // check if it's a underscore language code 26 | it = language_codes_to_underscore.find(lang_code); 27 | if (it != language_codes_to_underscore.end()) { 28 | // convert to the language code 29 | const std::string &underscore_code = it->second; 30 | it = language_codes.find(underscore_code); 31 | if (it != language_codes.end()) { 32 | return it->second; 33 | } 34 | } 35 | return lang_code; // Return the code itself if no mapping exists 36 | } 37 | 38 | inline std::string getLanguageLocale(const std::string &lang_code) 39 | { 40 | auto it = language_codes_to_locale.find(lang_code); 41 | if (it != language_codes_to_locale.end()) { 42 | return it->second; 43 | } 44 | // check if it's a underscore language code 45 | it = language_codes_from_underscore.find(lang_code); 46 | if (it != language_codes_from_underscore.end()) { 47 | // convert to the language code 48 | const std::string &language_code = it->second; 49 | it = language_codes_to_locale.find(language_code); 50 | if (it != language_codes_to_locale.end()) { 51 | return it->second; 52 | } 53 | } 54 | return lang_code; // Return the code itself if no mapping exists 55 | } 56 | 57 | #endif // LANGUAGE_CODES_H 58 | -------------------------------------------------------------------------------- /cmake/windows/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake Windows compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(compiler_common) 6 | 7 | # CMake 3.24 introduces a bug mistakenly interpreting MSVC as supporting the '-pthread' compiler flag 8 | if(CMAKE_VERSION VERSION_EQUAL 3.24.0) 9 | set(THREADS_HAVE_PTHREAD_ARG FALSE) 10 | endif() 11 | 12 | # CMake 3.25 changed the way symbol generation is handled on Windows 13 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.25.0) 14 | if(CMAKE_C_COMPILER_ID STREQUAL "MSVC") 15 | set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT ProgramDatabase) 16 | else() 17 | set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT Embedded) 18 | endif() 19 | endif() 20 | 21 | message(DEBUG "Current Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") 22 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM) 23 | message(DEBUG "Maximum Windows API version: ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION_MAXIMUM}") 24 | endif() 25 | 26 | if(CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION VERSION_LESS 10.0.20348) 27 | message(FATAL_ERROR "OBS requires Windows 10 SDK version 10.0.20348.0 or more recent.\n" 28 | "Please download and install the most recent Windows platform SDK.") 29 | endif() 30 | 31 | add_compile_options( 32 | /W3 33 | /utf-8 34 | /wd4267 35 | /wd4099 36 | "$<$:/MP>" 37 | "$<$:/MP>" 38 | "$<$:${_obs_clang_c_options}>" 39 | "$<$:${_obs_clang_cxx_options}>" 40 | $<$>:/Gy>) 41 | 42 | add_compile_definitions(UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS _CRT_NONSTDC_NO_WARNINGS $<$:DEBUG> 43 | $<$:_DEBUG>) 44 | 45 | # cmake-format: off 46 | add_link_options($<$>:/OPT:REF> 47 | $<$>:/OPT:ICF> 48 | $<$>:/INCREMENTAL:NO> 49 | /DEBUG 50 | /Brepro 51 | /IGNORE:4099) 52 | # cmake-format: on 53 | 54 | if(CMAKE_COMPILE_WARNING_AS_ERROR) 55 | add_link_options(/WX) 56 | endif() 57 | -------------------------------------------------------------------------------- /.github/actions/run-cmake-format/action.yaml: -------------------------------------------------------------------------------- 1 | name: Run cmake-format 2 | description: Runs cmake-format and checks for any changes introduced by it 3 | inputs: 4 | failCondition: 5 | description: Controls whether failed checks also fail the workflow run 6 | required: false 7 | default: 'never' 8 | workingDirectory: 9 | description: Working directory for checks 10 | required: false 11 | default: ${{ github.workspace }} 12 | runs: 13 | using: composite 14 | steps: 15 | - name: Check Runner Operating System 🏃‍♂️ 16 | if: runner.os == 'Windows' 17 | shell: bash 18 | run: | 19 | : Check Runner Operating System 🏃‍♂️ 20 | echo "::notice::run-cmake-format action requires a macOS-based or Linux-based runner." 21 | exit 2 22 | 23 | - name: Install Dependencies 🛍️ 24 | if: runner.os == 'Linux' 25 | shell: bash 26 | run: | 27 | : Install Dependencies 🛍️ 28 | echo ::group::Install Dependencies 29 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 30 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 31 | brew install --quiet zsh 32 | echo ::endgroup:: 33 | 34 | - name: Run cmake-format 🎛️ 35 | id: result 36 | shell: zsh --no-rcs --errexit --pipefail {0} 37 | working-directory: ${{ github.workspace }} 38 | env: 39 | GITHUB_EVENT_FORCED: ${{ github.event.forced }} 40 | GITHUB_REF_BEFORE: ${{ github.event.before }} 41 | run: | 42 | : Run cmake-format 🎛️ 43 | if (( ${+RUNNER_DEBUG} )) setopt XTRACE 44 | 45 | local -a changes=($(git diff --name-only HEAD~1 HEAD)) 46 | case ${GITHUB_EVENT_NAME} { 47 | pull_request) changes=($(git diff --name-only origin/${GITHUB_BASE_REF} HEAD)) ;; 48 | push) if [[ ${GITHUB_EVENT_FORCED} != true ]] changes=($(git diff --name-only ${GITHUB_REF_BEFORE} HEAD)) ;; 49 | *) ;; 50 | } 51 | 52 | if (( ${changes[(I)*.cmake|*CMakeLists.txt]} )) { 53 | echo ::group::Install cmakelang 54 | pip3 install cmakelang 55 | echo ::endgroup:: 56 | echo ::group::Run cmake-format 57 | ./build-aux/run-cmake-format --fail-${{ inputs.failCondition }} --check 58 | echo ::endgroup:: 59 | } 60 | -------------------------------------------------------------------------------- /src/cloud-translation/translation-cloud.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "ITranslator.h" 7 | #include "google-cloud.h" 8 | #include "deepl.h" 9 | #include "azure.h" 10 | #include "papago.h" 11 | #include "claude.h" 12 | #include "openai.h" 13 | #include "custom-api.h" 14 | 15 | #include "plugin-support.h" 16 | #include 17 | 18 | #include "translation-cloud.h" 19 | 20 | std::unique_ptr createTranslator(const CloudTranslatorConfig &config) 21 | { 22 | if (config.provider == "google") { 23 | return std::make_unique(config.access_key); 24 | } else if (config.provider == "deepl") { 25 | return std::make_unique(config.access_key, config.free); 26 | } else if (config.provider == "azure") { 27 | return std::make_unique(config.access_key, config.region); 28 | // } else if (config.provider == "aws") { 29 | // return std::make_unique(config.access_key, config.secret_key, config.region); 30 | } else if (config.provider == "papago") { 31 | return std::make_unique(config.access_key, config.secret_key); 32 | } else if (config.provider == "claude") { 33 | return std::make_unique( 34 | config.access_key, 35 | config.model.empty() ? "claude-3-sonnet-20240229" : config.model); 36 | } else if (config.provider == "openai") { 37 | return std::make_unique( 38 | config.access_key, 39 | config.model.empty() ? "gpt-4-turbo-preview" : config.model); 40 | } else if (config.provider == "api") { 41 | return std::make_unique(config.endpoint, config.body, 42 | config.response_json_path); 43 | } 44 | throw TranslationError("Unknown translation provider: " + config.provider); 45 | } 46 | 47 | std::string translate_cloud(const CloudTranslatorConfig &config, const std::string &text, 48 | const std::string &target_lang, const std::string &source_lang) 49 | { 50 | try { 51 | auto translator = createTranslator(config); 52 | obs_log(LOG_DEBUG, "translate with cloud provider %s. %s -> %s", 53 | config.provider.c_str(), source_lang.c_str(), target_lang.c_str()); 54 | std::string result = translator->translate(text, target_lang, source_lang); 55 | return result; 56 | } catch (const TranslationError &e) { 57 | obs_log(LOG_ERROR, "Translation error: %s\n", e.what()); 58 | } 59 | return ""; 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/curl-helper.cpp: -------------------------------------------------------------------------------- 1 | #include "curl-helper.h" 2 | #include 3 | #include 4 | 5 | bool CurlHelper::is_initialized_ = false; 6 | std::mutex CurlHelper::curl_mutex_; 7 | 8 | CurlHelper::CurlHelper() 9 | { 10 | std::lock_guard lock(curl_mutex_); 11 | if (!is_initialized_) { 12 | if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) { 13 | throw std::runtime_error("Failed to initialize CURL"); 14 | } 15 | is_initialized_ = true; 16 | } 17 | } 18 | 19 | CurlHelper::~CurlHelper() 20 | { 21 | // Don't call curl_global_cleanup() in destructor 22 | // Let it clean up when the program exits 23 | } 24 | 25 | size_t CurlHelper::WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) 26 | { 27 | if (!userp) { 28 | return 0; 29 | } 30 | 31 | size_t realsize = size * nmemb; 32 | auto *str = static_cast(userp); 33 | try { 34 | str->append(static_cast(contents), realsize); 35 | return realsize; 36 | } catch (const std::exception &) { 37 | return 0; // Return 0 to indicate error to libcurl 38 | } 39 | } 40 | 41 | std::string CurlHelper::urlEncode(const std::string &value) 42 | { 43 | std::unique_ptr curl(curl_easy_init(), 44 | curl_easy_cleanup); 45 | 46 | std::unique_ptr escaped( 47 | curl_easy_escape(curl.get(), value.c_str(), (int)value.length()), curl_free); 48 | 49 | if (!escaped) { 50 | throw std::runtime_error("Failed to URL encode string"); 51 | } 52 | 53 | return std::string(escaped.get()); 54 | } 55 | 56 | struct curl_slist *CurlHelper::createBasicHeaders(const std::string &content_type) 57 | { 58 | struct curl_slist *headers = nullptr; 59 | 60 | try { 61 | headers = curl_slist_append(headers, ("Content-Type: " + content_type).c_str()); 62 | 63 | if (!headers) { 64 | throw std::runtime_error("Failed to create HTTP headers"); 65 | } 66 | 67 | return headers; 68 | } catch (...) { 69 | if (headers) { 70 | curl_slist_free_all(headers); 71 | } 72 | throw; 73 | } 74 | } 75 | 76 | void CurlHelper::setSSLVerification(CURL *curl, bool verify) 77 | { 78 | if (!curl) { 79 | throw std::runtime_error("Invalid CURL handle for SSL configuration"); 80 | } 81 | 82 | long verify_peer = verify ? 1L : 0L; 83 | long verify_host = verify ? 2L : 0L; 84 | 85 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, verify_peer); 86 | curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, verify_host); 87 | } 88 | -------------------------------------------------------------------------------- /.github/actions/run-clang-format/action.yaml: -------------------------------------------------------------------------------- 1 | name: Run clang-format 2 | description: Runs clang-format and checks for any changes introduced by it 3 | inputs: 4 | failCondition: 5 | description: Controls whether failed checks also fail the workflow run 6 | required: false 7 | default: 'never' 8 | workingDirectory: 9 | description: Working directory for checks 10 | required: false 11 | default: ${{ github.workspace }} 12 | runs: 13 | using: composite 14 | steps: 15 | - name: Check Runner Operating System 🏃‍♂️ 16 | if: runner.os == 'Windows' 17 | shell: bash 18 | run: | 19 | : Check Runner Operating System 🏃‍♂️ 20 | echo "::notice::run-clang-format action requires a macOS-based or Linux-based runner." 21 | exit 2 22 | 23 | - name: Install Dependencies 🛍️ 24 | if: runner.os == 'Linux' 25 | shell: bash 26 | run: | 27 | : Install Dependencies 🛍️ 28 | echo ::group::Install Dependencies 29 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 30 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 31 | echo "/home/linuxbrew/.linuxbrew/opt/clang-format@17/bin" >> $GITHUB_PATH 32 | brew install --quiet zsh 33 | echo ::endgroup:: 34 | 35 | - name: Run clang-format 🐉 36 | id: result 37 | shell: zsh --no-rcs --errexit --pipefail {0} 38 | working-directory: ${{ inputs.workingDirectory }} 39 | env: 40 | GITHUB_EVENT_FORCED: ${{ github.event.forced }} 41 | GITHUB_REF_BEFORE: ${{ github.event.before }} 42 | run: | 43 | : Run clang-format 🐉 44 | if (( ${+RUNNER_DEBUG} )) setopt XTRACE 45 | 46 | local -a changes=($(git diff --name-only HEAD~1 HEAD)) 47 | case ${GITHUB_EVENT_NAME} { 48 | pull_request) changes=($(git diff --name-only origin/${GITHUB_BASE_REF} HEAD)) ;; 49 | push) if [[ ${GITHUB_EVENT_FORCED} != true ]] changes=($(git diff --name-only ${GITHUB_REF_BEFORE} HEAD)) ;; 50 | *) ;; 51 | } 52 | 53 | if (( ${changes[(I)(*.c|*.h|*.cpp|*.hpp|*.m|*.mm)]} )) { 54 | echo ::group::Install clang-format-17 55 | brew install --quiet obsproject/tools/clang-format@17 56 | echo ::endgroup:: 57 | 58 | echo ::group::Run clang-format-17 59 | ./build-aux/run-clang-format --fail-${{ inputs.failCondition }} --check 60 | echo ::endgroup:: 61 | } 62 | -------------------------------------------------------------------------------- /cmake/common/compiler_common.cmake: -------------------------------------------------------------------------------- 1 | # CMake common compiler options module 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Set C and C++ language standards to C17 and C++17 6 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.21) 7 | set(CMAKE_C_STANDARD 17) 8 | else() 9 | set(CMAKE_C_STANDARD 11) 10 | endif() 11 | set(CMAKE_C_STANDARD_REQUIRED TRUE) 12 | set(CMAKE_CXX_STANDARD 17) 13 | set(CMAKE_CXX_STANDARD_REQUIRED TRUE) 14 | 15 | # Set symbols to be hidden by default for C and C++ 16 | set(CMAKE_C_VISIBILITY_PRESET hidden) 17 | set(CMAKE_CXX_VISIBILITY_PRESET hidden) 18 | set(CMAKE_VISIBILITY_INLINES_HIDDEN TRUE) 19 | 20 | # clang options for C 21 | set(_obs_clang_c_options 22 | # cmake-format: sortable 23 | -fno-strict-aliasing 24 | -Wbool-conversion 25 | -Wconstant-conversion 26 | -Wdeprecated-declarations 27 | -Wempty-body 28 | -Wenum-conversion 29 | -Werror=return-type 30 | -Wextra 31 | -Wformat 32 | -Wformat-security 33 | -Wfour-char-constants 34 | -Winfinite-recursion 35 | -Wint-conversion 36 | -Wno-conversion 37 | -Wno-error=newline-eof 38 | -Wno-float-conversion 39 | -Wno-implicit-fallthrough 40 | -Wno-missing-braces 41 | -Wno-missing-field-initializers 42 | -Wno-missing-prototypes 43 | -Wno-newline-eof 44 | -Wno-semicolon-before-method-body 45 | -Wno-shadow 46 | -Wno-sign-conversion 47 | -Wno-strict-prototypes 48 | -Wno-trigraphs 49 | -Wno-unknown-pragmas 50 | -Wno-unused-function 51 | -Wno-unused-label 52 | -Wnon-literal-null-conversion 53 | -Wobjc-literal-conversion 54 | -Wparentheses 55 | -Wpointer-sign 56 | -Wquoted-include-in-framework-header 57 | -Wshadow 58 | -Wshorten-64-to-32 59 | -Wuninitialized 60 | -Wunreachable-code 61 | -Wunused-parameter 62 | -Wunused-value 63 | -Wunused-variable 64 | -Wvla) 65 | 66 | # clang options for C++ 67 | set(_obs_clang_cxx_options 68 | # cmake-format: sortable 69 | ${_obs_clang_c_options} 70 | -Wconversion 71 | -Wdeprecated-implementations 72 | -Wduplicate-method-match 73 | -Wfloat-conversion 74 | -Wfour-char-constants 75 | -Wimplicit-retain-self 76 | -Winvalid-offsetof 77 | -Wmove 78 | -Wno-c++11-extensions 79 | -Wno-exit-time-destructors 80 | -Wno-implicit-atomic-properties 81 | -Wno-objc-interface-ivars 82 | -Wno-overloaded-virtual 83 | -Wrange-loop-analysis) 84 | 85 | if(NOT DEFINED CMAKE_COMPILE_WARNING_AS_ERROR) 86 | set(CMAKE_COMPILE_WARNING_AS_ERROR ON) 87 | endif() 88 | -------------------------------------------------------------------------------- /src/cloud-providers/revai/WebSocket protocol.md: -------------------------------------------------------------------------------- 1 | WebSocket protocol 2 | Initial connection 3 | All requests begin as an HTTP GET request. A WebSocket request is declared by including the header value Upgrade: websocket and Connection: Upgrade. 4 | 5 | Client --> Rev AI 6 | GET /speechtotext/v1/stream HTTP/1.1 7 | Host: api.rev.ai 8 | Upgrade: websocket 9 | Connection: Upgrade 10 | Sec-WebSocket-Key: Chxzu/uTUCmjkFH9d/8NTg== 11 | Sec-WebSocket-Version: 13 12 | Origin: http://api.rev.ai 13 | If authorization is successful, the request is upgraded to a WebSocket connection. 14 | 15 | Client <-- Rev AI 16 | HTTP/1.1 101 Switching Protocols 17 | Upgrade: websocket 18 | Connection: Upgrade 19 | Sec-WebSocket-Accept: z0pcAwXZZRVlMcca8lmHCPzvrKU= 20 | After the connection has been upgraded, the servers will return a "connected" message. You must wait for this connected message before sending binary audio data. The response includes an id, which is the corresponding job identifier, as shown in the example below: 21 | 22 | { 23 | "type": "connected", 24 | "id": s1d24ax2fd21 25 | } 26 | warning 27 | If Rev AI currently does not have the capacity to handle the request, a WebSocket close message is returned with status code of 4013. A HTTP/1.1 400 Bad Request response indicates that the request is not a WebSocket upgrade request. 28 | 29 | Audio submission 30 | WebSocket messages sent to Rev AI must be of one of these two WebSocket message types: 31 | 32 | Message type Message requirements Notes 33 | Binary Audio data is transmitted as binary data and should be sent in chunks of 250ms or more. 34 | 35 | Streams sending audio chunks that are less than 250ms in size may experience increased transcription latency. The format of the audio must match that specified in the content_type parameter. 36 | Text The client should send an End-Of-Stream("EOS") text message to signal the end of audio data, and thus gracefully close the WebSocket connection. 37 | 38 | On an EOS message, Rev AI will return a final hypothesis along with a WebSocket close message. Currently, only this one text message type is supported. 39 | 40 | WebSocket close type messages are explicitly not supported as a message type and will abruptly close the socket connection with a 1007 Invalid Payload error. Clients will not receive their final hypothesis in this case. 41 | 42 | Any other text messages, including incorrectly capitalized messages such as "eos" and "Eos", are invalid and will also close the socket connection with a 1007 Invalid Payload error. -------------------------------------------------------------------------------- /cmake/windows/resources/installer-Windows.iss.in: -------------------------------------------------------------------------------- 1 | #define MyAppName "@CMAKE_PROJECT_NAME@" 2 | #define MyAppVersion "@CMAKE_PROJECT_VERSION@" 3 | #define MyAppPublisher "@PLUGIN_AUTHOR@" 4 | #define MyAppURL "@PLUGIN_WEBSITE@" 5 | 6 | [Setup] 7 | ; NOTE: The value of AppId uniquely identifies this application. 8 | ; Do not use the same AppId value in installers for other applications. 9 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 10 | AppId={{@UUID_APP@} 11 | AppName={#MyAppName} 12 | AppVersion={#MyAppVersion} 13 | AppPublisher={#MyAppPublisher} 14 | AppPublisherURL={#MyAppURL} 15 | AppSupportURL={#MyAppURL} 16 | AppUpdatesURL={#MyAppURL} 17 | DefaultDirName={code:GetDirName} 18 | DefaultGroupName={#MyAppName} 19 | OutputBaseFilename={#MyAppName}-{#MyAppVersion}-Windows-Installer 20 | Compression=lzma 21 | SolidCompression=yes 22 | DirExistsWarning=no 23 | 24 | [Languages] 25 | Name: "english"; MessagesFile: "compiler:Default.isl" 26 | 27 | [Files] 28 | Source: "..\release\Package\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs 29 | Source: "..\LICENSE"; Flags: dontcopy 30 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 31 | 32 | [Icons] 33 | Name: "{group}\{cm:ProgramOnTheWeb,{#MyAppName}}"; Filename: "{#MyAppURL}" 34 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" 35 | 36 | [Code] 37 | procedure InitializeWizard(); 38 | var 39 | GPLText: AnsiString; 40 | Page: TOutputMsgMemoWizardPage; 41 | begin 42 | ExtractTemporaryFile('LICENSE'); 43 | LoadStringFromFile(ExpandConstant('{tmp}\LICENSE'), GPLText); 44 | Page := CreateOutputMsgMemoPage(wpWelcome, 45 | 'License Information', 'Please review the license terms before installing {#MyAppName}', 46 | 'Press Page Down to see the rest of the agreement. Once you are aware of your rights, click Next to continue.', 47 | String(GPLText) 48 | ); 49 | end; 50 | 51 | // credit where it's due : 52 | // following function come from https://github.com/Xaymar/obs-studio_amf-encoder-plugin/blob/master/%23Resources/Installer.in.iss#L45 53 | function GetDirName(Value: string): string; 54 | var 55 | InstallPath: string; 56 | begin 57 | // initialize default path, which will be returned when the following registry 58 | // key queries fail due to missing keys or for some different reason 59 | Result := '{autopf}\obs-studio'; 60 | // query the first registry value; if this succeeds, return the obtained value 61 | if RegQueryStringValue(HKLM32, 'SOFTWARE\OBS Studio', '', InstallPath) then 62 | Result := InstallPath 63 | end; 64 | 65 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Install-BuildDependencies.ps1: -------------------------------------------------------------------------------- 1 | function Install-BuildDependencies { 2 | <# 3 | .SYNOPSIS 4 | Installs required build dependencies. 5 | .DESCRIPTION 6 | Additional packages might be needed for successful builds. This module contains additional 7 | dependencies available for installation via winget and, if possible, adds their locations 8 | to the environment path for future invocation. 9 | .EXAMPLE 10 | Install-BuildDependencies 11 | #> 12 | 13 | param( 14 | [string] $WingetFile = "$PSScriptRoot/.Wingetfile" 15 | ) 16 | 17 | if ( ! ( Test-Path function:Log-Warning ) ) { 18 | . $PSScriptRoot/Logger.ps1 19 | } 20 | 21 | $Prefixes = @{ 22 | 'x64' = ${Env:ProgramFiles} 23 | 'x86' = ${Env:ProgramFiles(x86)} 24 | 'arm64' = ${Env:ProgramFiles(arm)} 25 | } 26 | 27 | $Paths = $Env:Path -split [System.IO.Path]::PathSeparator 28 | 29 | $WingetOptions = @('install', '--accept-package-agreements', '--accept-source-agreements') 30 | 31 | if ( $script:Quiet ) { 32 | $WingetOptions += '--silent' 33 | } 34 | 35 | Log-Group 'Check Windows build requirements' 36 | Get-Content $WingetFile | ForEach-Object { 37 | $_, $Package, $_, $Path, $_, $Binary, $_, $Version = $_ -replace ',','' -split " +(?=(?:[^\']*\'[^\']*\')*[^\']*$)" -replace "'",'' 38 | 39 | $Prefixes.GetEnumerator() | ForEach-Object { 40 | $Prefix = $_.value 41 | $FullPath = "${Prefix}\${Path}" 42 | if ( ( Test-Path $FullPath ) -and ! ( $Paths -contains $FullPath ) ) { 43 | $Paths = @($FullPath) + $Paths 44 | $Env:Path = $Paths -join [System.IO.Path]::PathSeparator 45 | } 46 | } 47 | 48 | Log-Debug "Checking for command ${Binary}" 49 | $Found = Get-Command -ErrorAction SilentlyContinue $Binary 50 | 51 | if ( $Found ) { 52 | Log-Status "Found dependency ${Binary} as $($Found.Source)" 53 | } else { 54 | Log-Status "Installing package ${Package} $(if ( $Version -ne $null ) { "Version: ${Version}" } )" 55 | 56 | if ( $Version -ne $null ) { 57 | $WingetOptions += @('--version', ${Version}) 58 | } 59 | 60 | try { 61 | $Params = $WingetOptions + $Package 62 | 63 | winget @Params 64 | } catch { 65 | throw "Error while installing winget package ${Package}: $_" 66 | } 67 | } 68 | } 69 | Log-Group 70 | } 71 | -------------------------------------------------------------------------------- /cmake/macos/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(ccache) 6 | include(compiler_common) 7 | 8 | add_compile_options(-fopenmp-simd) 9 | 10 | if(XCODE) 11 | # Use Xcode's standard architecture selection 12 | set(CMAKE_OSX_ARCHITECTURES "$(ARCHS_STANDARD)") 13 | # Enable dSYM generation for Release builds 14 | string(APPEND CMAKE_C_FLAGS_RELEASE " -g") 15 | string(APPEND CMAKE_CXX_FLAGS_RELEASE " -g") 16 | else() 17 | option(ENABLE_COMPILER_TRACE "Enable clang time-trace (requires Ninja)" OFF) 18 | mark_as_advanced(ENABLE_COMPILER_TRACE) 19 | 20 | # clang options for ObjC 21 | set(_obs_clang_objc_options 22 | # cmake-format: sortable 23 | -Werror=block-capture-autoreleasing 24 | -Wno-conversion 25 | -Wno-error=unused-parameter 26 | -Wno-error=unused-variable 27 | -Wno-selector 28 | -Wno-shadow 29 | -Wno-strict-selector-match 30 | -Wno-unused-function 31 | -Wno-unused-parameter 32 | -Wno-unused-private-field 33 | -Wno-unused-variable 34 | -Wnon-virtual-dtor 35 | -Wprotocol 36 | -Wundeclared-selector) 37 | 38 | # clang options for ObjC++ 39 | set(_obs_clang_objcxx_options 40 | # cmake-format: sortable 41 | ${_obs_clang_objc_options} -Warc-repeated-use-of-weak -Wno-arc-maybe-repeated-use-of-weak) 42 | 43 | add_compile_options( 44 | "$<$:${_obs_clang_c_options}>" "$<$:${_obs_clang_cxx_options}>" 45 | "$<$:${_obs_clang_objc_options}>" 46 | "$<$:${_obs_clang_objcxx_options}>") 47 | 48 | # Enable stripping of dead symbols when not building for Debug configuration 49 | set(_release_configs RelWithDebInfo Release MinSizeRel) 50 | if(CMAKE_BUILD_TYPE IN_LIST _release_configs) 51 | add_link_options(LINKER:-dead_strip) 52 | endif() 53 | 54 | # Enable color diagnostics for AppleClang 55 | set(CMAKE_COLOR_DIAGNOSTICS ON) 56 | # Set universal architectures via CMake flag for non-Xcode generators 57 | set(CMAKE_OSX_ARCHITECTURES "arm64") # TODO: add x86_64 58 | 59 | # Enable compiler and build tracing (requires Ninja generator) 60 | if(ENABLE_COMPILER_TRACE AND CMAKE_GENERATOR STREQUAL "Ninja") 61 | add_compile_options($<$:-ftime-trace> $<$:-ftime-trace>) 62 | else() 63 | set(ENABLE_COMPILER_TRACE 64 | OFF 65 | CACHE STRING "Enable clang time-trace (requires Ninja)" FORCE) 66 | endif() 67 | endif() 68 | 69 | add_compile_definitions($<$:DEBUG> $<$:_DEBUG> SIMDE_ENABLE_OPENMP) 70 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Expand-ArchiveExt.ps1: -------------------------------------------------------------------------------- 1 | function Expand-ArchiveExt { 2 | <# 3 | .SYNOPSIS 4 | Expands archive files. 5 | .DESCRIPTION 6 | Allows extraction of zip, 7z, gz, and xz archives. 7 | Requires tar and 7-zip to be available on the system. 8 | Archives ending with .zip but created using LZMA compression are 9 | expanded using 7-zip as a fallback. 10 | .EXAMPLE 11 | Expand-ArchiveExt -Path 12 | Expand-ArchiveExt -Path -DestinationPath 13 | #> 14 | 15 | param( 16 | [Parameter(Mandatory)] 17 | [string] $Path, 18 | [string] $DestinationPath = [System.IO.Path]::GetFileNameWithoutExtension($Path), 19 | [switch] $Force 20 | ) 21 | 22 | switch ( [System.IO.Path]::GetExtension($Path) ) { 23 | .zip { 24 | try { 25 | Expand-Archive -Path $Path -DestinationPath $DestinationPath -Force:$Force 26 | } catch { 27 | if ( Get-Command 7z ) { 28 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 29 | } else { 30 | throw "Fallback utility 7-zip not found. Please install 7-zip first." 31 | } 32 | } 33 | break 34 | } 35 | { ( $_ -eq ".7z" ) -or ( $_ -eq ".exe" ) } { 36 | if ( Get-Command 7z ) { 37 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 38 | } else { 39 | throw "Extraction utility 7-zip not found. Please install 7-zip first." 40 | } 41 | break 42 | } 43 | .gz { 44 | try { 45 | Invoke-External tar -x -o $DestinationPath -f $Path 46 | } catch { 47 | if ( Get-Command 7z ) { 48 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 49 | } else { 50 | throw "Fallback utility 7-zip not found. Please install 7-zip first." 51 | } 52 | } 53 | break 54 | } 55 | .xz { 56 | try { 57 | Invoke-External tar -x -o $DestinationPath -f $Path 58 | } catch { 59 | if ( Get-Command 7z ) { 60 | Invoke-External 7z x -y $Path "-o${DestinationPath}" 61 | } else { 62 | throw "Fallback utility 7-zip not found. Please install 7-zip first." 63 | } 64 | } 65 | } 66 | default { 67 | throw "Unsupported archive extension provided." 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cmake/common/bootstrap.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16...3.26) 2 | 3 | include_guard(GLOBAL) 4 | 5 | # Enable automatic PUSH and POP of policies to parent scope 6 | if(POLICY CMP0011) 7 | cmake_policy(SET CMP0011 NEW) 8 | endif() 9 | 10 | # Enable distinction between Clang and AppleClang 11 | if(POLICY CMP0025) 12 | cmake_policy(SET CMP0025 NEW) 13 | endif() 14 | 15 | # Enable strict checking of "break()" usage 16 | if(POLICY CMP0055) 17 | cmake_policy(SET CMP0055 NEW) 18 | endif() 19 | 20 | # Honor visibility presets for all target types (executable, shared, module, static) 21 | if(POLICY CMP0063) 22 | cmake_policy(SET CMP0063 NEW) 23 | endif() 24 | 25 | # Disable export function calls to populate package registry by default 26 | if(POLICY CMP0090) 27 | cmake_policy(SET CMP0090 NEW) 28 | endif() 29 | 30 | # Prohibit in-source builds 31 | if("${CMAKE_CURRENT_BINARY_DIR}" STREQUAL "${CMAKE_CURRENT_SOURCE_DIR}") 32 | message(FATAL_ERROR "In-source builds are not supported. " 33 | "Specify a build directory via 'cmake -S -B ' instead.") 34 | file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/CMakeCache.txt" "${CMAKE_CURRENT_SOURCE_DIR}/CMakeFiles") 35 | endif() 36 | 37 | # Use folders for source file organization with IDE generators (Visual Studio/Xcode) 38 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 39 | 40 | # Add common module directories to default search path 41 | list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/common") 42 | 43 | file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) 44 | 45 | # cmake-format: off 46 | string(JSON _name GET ${buildspec} name) 47 | string(JSON _website GET ${buildspec} website) 48 | string(JSON _author GET ${buildspec} author) 49 | string(JSON _email GET ${buildspec} email) 50 | string(JSON _version GET ${buildspec} version) 51 | string(JSON _bundleId GET ${buildspec} platformConfig macos bundleId) 52 | string(JSON _windowsAppUUID GET ${buildspec} uuids windowsApp) 53 | # cmake-format: on 54 | 55 | set(PLUGIN_AUTHOR ${_author}) 56 | set(PLUGIN_WEBSITE ${_website}) 57 | set(PLUGIN_EMAIL ${_email}) 58 | set(PLUGIN_VERSION ${_version}) 59 | set(MACOS_BUNDLEID ${_bundleId}) 60 | 61 | include(buildnumber) 62 | include(osconfig) 63 | 64 | # Allow selection of common build types via UI 65 | if(NOT CMAKE_BUILD_TYPE) 66 | set(CMAKE_BUILD_TYPE 67 | "RelWithDebInfo" 68 | CACHE STRING "OBS build type [Release, RelWithDebInfo, Debug, MinSizeRel]" FORCE) 69 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Release RelWithDebInfo Debug MinSizeRel) 70 | endif() 71 | 72 | # Disable exports automatically going into the CMake package registry 73 | set(CMAKE_EXPORT_PACKAGE_REGISTRY FALSE) 74 | # Enable default inclusion of targets' source and binary directory 75 | set(CMAKE_INCLUDE_CURRENT_DIR TRUE) 76 | -------------------------------------------------------------------------------- /cmake/linux/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux helper functions module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(helpers_common) 6 | 7 | # set_target_properties_plugin: Set target properties for use in obs-studio 8 | function(set_target_properties_plugin target) 9 | set(options "") 10 | set(oneValueArgs "") 11 | set(multiValueArgs PROPERTIES) 12 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 13 | 14 | message(DEBUG "Setting additional properties for target ${target}...") 15 | 16 | while(_STPO_PROPERTIES) 17 | list(POP_FRONT _STPO_PROPERTIES key value) 18 | set_property(TARGET ${target} PROPERTY ${key} "${value}") 19 | endwhile() 20 | 21 | set_target_properties( 22 | ${target} 23 | PROPERTIES VERSION 0 24 | SOVERSION ${PLUGIN_VERSION} 25 | PREFIX "") 26 | 27 | install( 28 | TARGETS ${target} 29 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 30 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/obs-plugins) 31 | 32 | if(TARGET plugin-support) 33 | target_link_libraries(${target} PRIVATE plugin-support) 34 | endif() 35 | 36 | target_install_resources(${target}) 37 | 38 | get_target_property(target_sources ${target} SOURCES) 39 | set(target_ui_files ${target_sources}) 40 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 41 | source_group( 42 | TREE "${CMAKE_CURRENT_SOURCE_DIR}" 43 | PREFIX "UI Files" 44 | FILES ${target_ui_files}) 45 | endfunction() 46 | 47 | # Helper function to add resources into bundle 48 | function(target_install_resources target) 49 | message(DEBUG "Installing resources for target ${target}...") 50 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 51 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 52 | foreach(data_file IN LISTS data_files) 53 | cmake_path(RELATIVE_PATH data_file BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" OUTPUT_VARIABLE 54 | relative_path) 55 | cmake_path(GET relative_path PARENT_PATH relative_path) 56 | target_sources(${target} PRIVATE "${data_file}") 57 | source_group("Resources/${relative_path}" FILES "${data_file}") 58 | endforeach() 59 | 60 | install( 61 | DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" 62 | DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target} 63 | USE_SOURCE_PERMISSIONS) 64 | endif() 65 | endfunction() 66 | 67 | # Helper function to add a specific resource to a bundle 68 | function(target_add_resource target resource) 69 | message(DEBUG "Add resource '${resource}' to target ${target} at destination '${target_destination}'...") 70 | 71 | install(FILES "${resource}" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/obs/obs-plugins/${target}) 72 | 73 | source_group("Resources" FILES "${resource}") 74 | endfunction() 75 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16...3.26) 2 | 3 | include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake" NO_POLICY_SCOPE) 4 | 5 | project(${_name} VERSION ${_version}) 6 | 7 | option(ENABLE_FRONTEND_API "Use obs-frontend-api for UI functionality" OFF) 8 | option(ENABLE_QT "Use Qt functionality" OFF) 9 | 10 | include(compilerconfig) 11 | include(defaults) 12 | include(helpers) 13 | 14 | add_library(${CMAKE_PROJECT_NAME} MODULE) 15 | 16 | find_package(libobs REQUIRED) 17 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE OBS::libobs) 18 | 19 | if(ENABLE_FRONTEND_API) 20 | find_package(obs-frontend-api REQUIRED) 21 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE OBS::obs-frontend-api) 22 | endif() 23 | 24 | if(ENABLE_QT) 25 | find_package(Qt6 COMPONENTS Widgets Core) 26 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE Qt6::Core Qt6::Widgets) 27 | target_compile_options( 28 | ${CMAKE_PROJECT_NAME} PRIVATE $<$:-Wno-quoted-include-in-framework-header 29 | -Wno-comma>) 30 | set_target_properties( 31 | ${CMAKE_PROJECT_NAME} 32 | PROPERTIES AUTOMOC ON 33 | AUTOUIC ON 34 | AUTORCC ON) 35 | endif() 36 | 37 | include(cmake/BuildDependencies.cmake) 38 | include(cmake/BuildClovaAPIs.cmake) 39 | include(cmake/BuildGoogleAPIs.cmake) 40 | include(cmake/FetchNlohmannJSON.cmake) 41 | 42 | set(USE_SYSTEM_CURL 43 | OFF 44 | CACHE STRING "Use system cURL") 45 | 46 | if(USE_SYSTEM_CURL) 47 | find_package(CURL REQUIRED) 48 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE "${CURL_LIBRARIES}") 49 | target_include_directories(${CMAKE_PROJECT_NAME} SYSTEM PUBLIC "${CURL_INCLUDE_DIRS}") 50 | else() 51 | include(cmake/BuildMyCurl.cmake) 52 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE libcurl) 53 | endif() 54 | 55 | target_sources( 56 | ${CMAKE_PROJECT_NAME} 57 | PRIVATE src/plugin-main.c 58 | src/cloudvocal.c 59 | src/cloudvocal.cpp 60 | src/cloudvocal-callbacks.cpp 61 | src/cloudvocal-utils.cpp 62 | src/cloudvocal-processing.cpp 63 | src/cloudvocal-properties.cpp 64 | src/language-codes/language-codes.cpp 65 | src/cloud-providers/cloud-provider.cpp 66 | src/cloud-providers/clova/clova-provider.cpp 67 | src/cloud-providers/deepgram/deepgram-provider.cpp 68 | src/cloud-providers/google/google-provider.cpp 69 | src/cloud-providers/revai/revai-provider.cpp 70 | src/utils/ssl-utils.cpp 71 | src/utils/curl-helper.cpp 72 | src/timed-metadata/timed-metadata-utils.cpp) 73 | 74 | add_subdirectory(src/cloud-translation) 75 | add_subdirectory(src/cloud-providers/aws) 76 | 77 | set_target_properties_plugin(${CMAKE_PROJECT_NAME} PROPERTIES OUTPUT_NAME ${_name}) 78 | -------------------------------------------------------------------------------- /src/cloud-translation/google-cloud.cpp: -------------------------------------------------------------------------------- 1 | #include "google-cloud.h" 2 | #include "utils/curl-helper.h" 3 | #include 4 | #include 5 | 6 | using json = nlohmann::json; 7 | 8 | GoogleTranslator::GoogleTranslator(const std::string &api_key) 9 | : api_key_(api_key), 10 | curl_helper_(std::make_unique()), 11 | target_lang("en"), 12 | url("") 13 | { 14 | } 15 | 16 | GoogleTranslator::~GoogleTranslator() = default; 17 | 18 | std::string GoogleTranslator::translate(const std::string &text, const std::string &target_lang, 19 | const std::string &source_lang) 20 | { 21 | std::unique_ptr curl(curl_easy_init(), 22 | curl_easy_cleanup); 23 | 24 | if (!curl) { 25 | throw TranslationError("Failed to initialize CURL session"); 26 | } 27 | 28 | std::string response; 29 | 30 | try { 31 | if (this->url.empty() || this->target_lang != target_lang) { 32 | // Construct URL with parameters 33 | this->url = "https://translation.googleapis.com/language/translate/v2"; 34 | this->url += "?key=" + api_key_; 35 | this->url += "&q=" + CurlHelper::urlEncode(text); 36 | this->url += "&target=" + sanitize_language_code(target_lang); 37 | 38 | if (source_lang != "auto") { 39 | this->url += "&source=" + sanitize_language_code(source_lang); 40 | } 41 | 42 | this->target_lang = target_lang; 43 | } 44 | 45 | // Set up curl options 46 | curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); 47 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, CurlHelper::WriteCallback); 48 | curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response); 49 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L); 50 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L); 51 | curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 30L); 52 | 53 | CURLcode res = curl_easy_perform(curl.get()); 54 | 55 | if (res != CURLE_OK) { 56 | throw TranslationError(std::string("CURL request failed: ") + 57 | curl_easy_strerror(res)); 58 | } 59 | 60 | return parseResponse(response); 61 | 62 | } catch (const json::exception &e) { 63 | throw TranslationError(std::string("JSON parsing error: ") + e.what()); 64 | } 65 | } 66 | 67 | std::string GoogleTranslator::parseResponse(const std::string &response_str) 68 | { 69 | json response = json::parse(response_str); 70 | 71 | if (response.contains("error")) { 72 | const auto &error = response["error"]; 73 | std::stringstream error_msg; 74 | error_msg << "Google API Error: "; 75 | if (error.contains("message")) { 76 | error_msg << error["message"].get(); 77 | } 78 | if (error.contains("code")) { 79 | error_msg << " (Code: " << error["code"].get() << ")"; 80 | } 81 | throw TranslationError(error_msg.str()); 82 | } 83 | 84 | return response["data"]["translations"][0]["translatedText"].get(); 85 | } 86 | -------------------------------------------------------------------------------- /cmake/linux/defaults.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux defaults module 2 | 3 | # cmake-format: off 4 | # cmake-lint: disable=C0103 5 | # cmake-lint: disable=C0111 6 | # cmake-format: on 7 | 8 | include_guard(GLOBAL) 9 | 10 | include(GNUInstallDirs) 11 | 12 | # Enable find_package targets to become globally available targets 13 | set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL TRUE) 14 | 15 | set(CPACK_PACKAGE_NAME "${CMAKE_PROJECT_NAME}") 16 | set(CPACK_PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}") 17 | set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_C_LIBRARY_ARCHITECTURE}") 18 | 19 | set(CPACK_GENERATOR "DEB") 20 | set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) 21 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}") 22 | set(CPACK_SET_DESTDIR ON) 23 | 24 | if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.25.0 OR NOT CMAKE_CROSSCOMPILING) 25 | set(CPACK_DEBIAN_DEBUGINFO_PACKAGE ON) 26 | endif() 27 | 28 | set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/release") 29 | 30 | set(CPACK_SOURCE_GENERATOR "TXZ") 31 | set(CPACK_SOURCE_IGNORE_FILES 32 | # cmake-format: sortable 33 | ".*~$" 34 | \\.git/ 35 | \\.github/ 36 | \\.gitignore 37 | build_.* 38 | cmake/\\.CMakeBuildNumber 39 | release/) 40 | 41 | set(CPACK_VERBATIM_VARIABLES YES) 42 | set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-source") 43 | set(CPACK_ARCHIVE_THREADS 0) 44 | 45 | include(CPack) 46 | 47 | find_package(libobs QUIET) 48 | 49 | if(NOT TARGET OBS::libobs) 50 | find_package(LibObs REQUIRED) 51 | add_library(OBS::libobs ALIAS libobs) 52 | 53 | if(ENABLE_FRONTEND_API) 54 | find_path( 55 | obs-frontend-api_INCLUDE_DIR 56 | NAMES obs-frontend-api.h 57 | PATHS /usr/include /usr/local/include 58 | PATH_SUFFIXES obs) 59 | 60 | find_library( 61 | obs-frontend-api_LIBRARY 62 | NAMES obs-frontend-api 63 | PATHS /usr/lib /usr/local/lib) 64 | 65 | if(obs-frontend-api_LIBRARY) 66 | if(NOT TARGET OBS::obs-frontend-api) 67 | if(IS_ABSOLUTE "${obs-frontend-api_LIBRARY}") 68 | add_library(OBS::obs-frontend-api UNKNOWN IMPORTED) 69 | set_property(TARGET OBS::obs-frontend-api PROPERTY IMPORTED_LOCATION "${obs-frontend-api_LIBRARY}") 70 | else() 71 | add_library(OBS::obs-frontend-api INTERFACE IMPORTED) 72 | set_property(TARGET OBS::obs-frontend-api PROPERTY IMPORTED_LIBNAME "${obs-frontend-api_LIBRARY}") 73 | endif() 74 | 75 | set_target_properties(OBS::obs-frontend-api PROPERTIES INTERFACE_INCLUDE_DIRECTORIES 76 | "${obs-frontend-api_INCLUDE_DIR}") 77 | endif() 78 | endif() 79 | endif() 80 | 81 | macro(find_package) 82 | if(NOT "${ARGV0}" STREQUAL libobs AND NOT "${ARGV0}" STREQUAL obs-frontend-api) 83 | _find_package(${ARGV}) 84 | endif() 85 | endmacro() 86 | endif() 87 | -------------------------------------------------------------------------------- /cmake/linux/compilerconfig.cmake: -------------------------------------------------------------------------------- 1 | # CMake Linux compiler configuration module 2 | 3 | include_guard(GLOBAL) 4 | 5 | include(ccache) 6 | include(compiler_common) 7 | 8 | option(ENABLE_COMPILER_TRACE "Enable Clang time-trace (required Clang and Ninja)" OFF) 9 | mark_as_advanced(ENABLE_COMPILER_TRACE) 10 | 11 | # gcc options for C 12 | set(_obs_gcc_c_options 13 | # cmake-format: sortable 14 | -fno-strict-aliasing 15 | -fopenmp-simd 16 | -Wdeprecated-declarations 17 | -Wempty-body 18 | -Werror=return-type 19 | -Wextra 20 | -Wformat 21 | -Wformat-security 22 | -Wno-conversion 23 | -Wno-error=conversion 24 | -Wno-error=shadow 25 | -Wno-implicit-fallthrough 26 | -Wno-missing-braces 27 | -Wno-missing-field-initializers 28 | -Wno-shadow 29 | -Wno-sign-conversion 30 | -Wno-trigraphs 31 | -Wno-unknown-pragmas 32 | -Wno-unused-function 33 | -Wno-unused-label 34 | -Wno-unused-parameter 35 | -Wparentheses 36 | -Wuninitialized 37 | -Wunreachable-code 38 | -Wunused-value 39 | -Wunused-variable 40 | -Wvla) 41 | 42 | # gcc options for C++ 43 | set(_obs_gcc_cxx_options 44 | # cmake-format: sortable 45 | ${_obs_gcc_c_options} -Wconversion -Wfloat-conversion -Winvalid-offsetof -Wno-overloaded-virtual) 46 | 47 | add_compile_options( 48 | -fopenmp-simd 49 | "$<$:${_obs_gcc_c_options}>" 50 | "$<$:-Wint-conversion;-Wno-missing-prototypes;-Wno-strict-prototypes;-Wpointer-sign>" 51 | "$<$:${_obs_gcc_cxx_options}>" 52 | "$<$:${_obs_clang_c_options}>" 53 | "$<$:${_obs_clang_cxx_options}>") 54 | 55 | # Add support for color diagnostics and CMake switch for warnings as errors to CMake < 3.24 56 | if(CMAKE_VERSION VERSION_LESS 3.24.0) 57 | add_compile_options($<$:-fcolor-diagnostics> $<$:-fcolor-diagnostics>) 58 | if(CMAKE_COMPILE_WARNING_AS_ERROR) 59 | add_compile_options(-Werror) 60 | endif() 61 | else() 62 | set(CMAKE_COLOR_DIAGNOSTICS ON) 63 | endif() 64 | 65 | if(CMAKE_CXX_COMPILER_ID STREQUAL GNU) 66 | # Disable false-positive warning in GCC 12.1.0 and later 67 | add_compile_options(-Wno-error=maybe-uninitialized) 68 | 69 | # Add warning for infinite recursion (added in GCC 12) 70 | if(CMAKE_C_COMPILER_VERSION VERSION_GREATER_EQUAL 12.0.0) 71 | add_compile_options(-Winfinite-recursion) 72 | endif() 73 | endif() 74 | 75 | # Enable compiler and build tracing (requires Ninja generator) 76 | if(ENABLE_COMPILER_TRACE AND CMAKE_GENERATOR STREQUAL "Ninja") 77 | add_compile_options($<$:-ftime-trace> $<$:-ftime-trace>) 78 | else() 79 | set(ENABLE_COMPILER_TRACE 80 | OFF 81 | CACHE STRING "Enable Clang time-trace (required Clang and Ninja)" FORCE) 82 | endif() 83 | 84 | add_compile_definitions($<$:DEBUG> $<$:_DEBUG> SIMDE_ENABLE_OPENMP) 85 | -------------------------------------------------------------------------------- /src/cloudvocal-utils.cpp: -------------------------------------------------------------------------------- 1 | #include "cloudvocal-utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | void create_obs_text_source_if_needed() 8 | { 9 | // check if a source called "CloudVocal Subtitles" exists 10 | obs_source_t *source = obs_get_source_by_name("CloudVocal Subtitles"); 11 | if (source) { 12 | // source already exists, release it 13 | obs_source_release(source); 14 | return; 15 | } 16 | 17 | // create a new OBS text source called "CloudVocal Subtitles" 18 | obs_source_t *scene_as_source = obs_frontend_get_current_scene(); 19 | obs_scene_t *scene = obs_scene_from_source(scene_as_source); 20 | #ifdef _WIN32 21 | source = obs_source_create("text_gdiplus_v3", "CloudVocal Subtitles", nullptr, nullptr); 22 | #else 23 | source = obs_source_create("text_ft2_source_v2", "CloudVocal Subtitles", nullptr, nullptr); 24 | #endif 25 | if (source) { 26 | // add source to the current scene 27 | obs_scene_add(scene, source); 28 | // set source settings 29 | obs_data_t *source_settings = obs_source_get_settings(source); 30 | obs_data_set_bool(source_settings, "word_wrap", true); 31 | obs_data_set_bool(source_settings, "extents", true); 32 | obs_data_set_bool(source_settings, "outline", true); 33 | obs_data_set_int(source_settings, "outline_color", 4278190080); 34 | obs_data_set_int(source_settings, "outline_size", 7); 35 | obs_data_set_int(source_settings, "extents_cx", 1500); 36 | obs_data_set_int(source_settings, "extents_cy", 230); 37 | obs_data_t *font_data = obs_data_create(); 38 | obs_data_set_string(font_data, "face", "Arial"); 39 | obs_data_set_string(font_data, "style", "Regular"); 40 | obs_data_set_int(font_data, "size", 72); 41 | obs_data_set_int(font_data, "flags", 0); 42 | obs_data_set_obj(source_settings, "font", font_data); 43 | obs_data_release(font_data); 44 | obs_source_update(source, source_settings); 45 | obs_data_release(source_settings); 46 | 47 | // set transform settings 48 | obs_transform_info transform_info; 49 | transform_info.pos.x = 962.0; 50 | transform_info.pos.y = 959.0; 51 | transform_info.bounds.x = 1769.0; 52 | transform_info.bounds.y = 145.0; 53 | transform_info.bounds_type = obs_bounds_type::OBS_BOUNDS_SCALE_INNER; 54 | transform_info.bounds_alignment = OBS_ALIGN_CENTER; 55 | transform_info.alignment = OBS_ALIGN_CENTER; 56 | transform_info.scale.x = 1.0; 57 | transform_info.scale.y = 1.0; 58 | transform_info.rot = 0.0; 59 | obs_sceneitem_t *source_sceneitem = obs_scene_sceneitem_from_source(scene, source); 60 | obs_sceneitem_set_info2(source_sceneitem, &transform_info); 61 | obs_sceneitem_release(source_sceneitem); 62 | 63 | obs_source_release(source); 64 | } 65 | obs_source_release(scene_as_source); 66 | } 67 | 68 | bool add_sources_to_list(void *list_property, obs_source_t *source) 69 | { 70 | const char *source_id = obs_source_get_id(source); 71 | if (strncmp(source_id, "text", 4) != 0) { 72 | return true; 73 | } 74 | 75 | obs_property_t *sources = (obs_property_t *)list_property; 76 | const char *name = obs_source_get_name(source); 77 | obs_property_list_add_string(sources, name, name); 78 | return true; 79 | } 80 | -------------------------------------------------------------------------------- /src/cloudvocal-data.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "cloud-translation/translation-cloud.h" 13 | 14 | #define TRANSCRIPTION_SAMPLE_RATE 16000 15 | 16 | // Audio packet info 17 | struct cloudvocal_audio_info { 18 | uint32_t frames; 19 | uint64_t timestamp_offset_ns; // offset (since start of processing) timestamp in ns 20 | }; 21 | 22 | enum DetectionResult { 23 | DETECTION_RESULT_UNKNOWN = 0, 24 | DETECTION_RESULT_SPEECH = 1, 25 | DETECTION_RESULT_SILENCE = 2, 26 | DETECTION_RESULT_PARTIAL = 3 27 | }; 28 | 29 | struct DetectionResultWithText { 30 | uint64_t start_timestamp_ms; 31 | uint64_t end_timestamp_ms; 32 | std::string text; 33 | std::string language; 34 | enum DetectionResult result; 35 | }; 36 | 37 | class CloudProvider; 38 | 39 | struct TimedMetadataConfig { 40 | std::string aws_access_key; 41 | std::string aws_secret_key; 42 | std::string ivs_channel_arn; 43 | std::string aws_region; 44 | }; 45 | 46 | struct cloudvocal_data { 47 | int log_level; 48 | bool active; 49 | bool initial_creation; 50 | bool source_signals_set; 51 | obs_source_t *context; 52 | 53 | size_t channels; 54 | int sample_rate; 55 | std::deque input_buffers[8]; 56 | std::deque info_buffer; 57 | std::deque resampled_buffer; 58 | uint32_t last_num_frames; 59 | 60 | // File output options 61 | bool save_only_while_recording; 62 | bool truncate_output_file; 63 | bool save_srt; 64 | bool save_to_file; 65 | std::string output_file_path; 66 | uint64_t sentence_number; 67 | uint64_t start_timestamp_ms; 68 | bool rename_file_to_match_recording; 69 | 70 | // Transcription options 71 | std::string language; 72 | std::string text_source_name; 73 | bool caption_to_stream; 74 | bool process_while_muted; 75 | uint64_t last_sub_render_time; 76 | bool cleared_last_sub; 77 | std::string last_transcription_sentence; 78 | audio_resampler_t *resampler; 79 | int min_sub_duration; 80 | int max_sub_duration; 81 | bool log_words; 82 | // smart pointer to the cloud provider 83 | std::shared_ptr cloud_provider; 84 | std::string cloud_provider_selection; 85 | std::string cloud_provider_api_key; 86 | std::string cloud_provider_secret_key; 87 | 88 | std::map filter_words_replace; 89 | 90 | // Translation options 91 | bool translate_only_full_sentences; 92 | bool translate; 93 | std::string translation_output; 94 | std::string target_lang; 95 | std::string last_text_for_translation; 96 | std::string last_text_translation; 97 | CloudTranslatorConfig translate_cloud_config; 98 | 99 | // Timed metadata options 100 | bool send_timed_metadata; 101 | TimedMetadataConfig timed_metadata_config; 102 | 103 | std::mutex input_buffers_mutex; 104 | std::condition_variable input_buffers_cv; 105 | std::mutex resampled_buffer_mutex; 106 | }; 107 | -------------------------------------------------------------------------------- /cmake/BuildMyCurl.cmake: -------------------------------------------------------------------------------- 1 | include(FetchContent) 2 | 3 | set(LibCurl_VERSION "8.4.1") 4 | set(LibCurl_BASEURL "https://github.com/occ-ai/obs-ai-libcurl-dep/releases/download/${LibCurl_VERSION}") 5 | 6 | if(${CMAKE_BUILD_TYPE} STREQUAL Release OR ${CMAKE_BUILD_TYPE} STREQUAL RelWithDebInfo) 7 | set(LibCurl_BUILD_TYPE Release) 8 | else() 9 | set(LibCurl_BUILD_TYPE Debug) 10 | endif() 11 | 12 | if(APPLE) 13 | if(LibCurl_BUILD_TYPE STREQUAL Release) 14 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-macos-${LibCurl_VERSION}-Release.tar.gz") 15 | set(LibCurl_HASH SHA256=700dc8ba476978bf8ee60c92fe31f7e1e31b7d67a47452f1d78b38ac7afd8962) 16 | else() 17 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-macos-${LibCurl_VERSION}-Debug.tar.gz") 18 | set(LibCurl_HASH SHA256=ee014693c74bb33d1851e2f136031cf4c490b7400c981a204a61cd5d3afd268a) 19 | endif() 20 | elseif(MSVC) 21 | if(LibCurl_BUILD_TYPE STREQUAL Release) 22 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-windows-${LibCurl_VERSION}-Release.zip") 23 | set(LibCurl_HASH SHA256=7b40e4c1b80f1ade3051fb30077ff9dec6ace5cb0f46ba2ec35b35fdcafef5ff) 24 | else() 25 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-windows-${LibCurl_VERSION}-Debug.zip") 26 | set(LibCurl_HASH SHA256=d972ff7d473f43172f9ad8b9ad32015c4c85621b84d099d748278e0920c60a64) 27 | endif() 28 | else() 29 | if(LibCurl_BUILD_TYPE STREQUAL Release) 30 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-linux-${LibCurl_VERSION}-Release.tar.gz") 31 | set(LibCurl_HASH SHA256=3e4769575682b84bb916f55411eac0541c78199087e127b499f9c18c8afd7203) 32 | else() 33 | set(LibCurl_URL "${LibCurl_BASEURL}/libcurl-linux-${LibCurl_VERSION}-Debug.tar.gz") 34 | set(LibCurl_HASH SHA256=fe0c1164c6b19def6867f0cbae979bb8a165e04ebb02cde6eb9b8af243a34483) 35 | endif() 36 | endif() 37 | 38 | FetchContent_Declare( 39 | libcurl_fetch 40 | URL ${LibCurl_URL} 41 | URL_HASH ${LibCurl_HASH}) 42 | FetchContent_MakeAvailable(libcurl_fetch) 43 | 44 | if(MSVC) 45 | set(libcurl_fetch_lib_location "${libcurl_fetch_SOURCE_DIR}/lib/libcurl.lib") 46 | set(libcurl_fetch_link_libs "\$;\$;\$;\$") 47 | else() 48 | find_package(ZLIB REQUIRED) 49 | set(libcurl_fetch_lib_location "${libcurl_fetch_SOURCE_DIR}/lib/libcurl.a") 50 | if(UNIX AND NOT APPLE) 51 | find_package(OpenSSL REQUIRED) 52 | set(libcurl_fetch_link_libs "\$;\$;\$") 53 | else() 54 | set(libcurl_fetch_link_libs 55 | "-framework SystemConfiguration;-framework Security;-framework CoreFoundation;-framework CoreServices;ZLIB::ZLIB" 56 | ) 57 | endif() 58 | endif() 59 | 60 | # Create imported target 61 | add_library(libcurl STATIC IMPORTED) 62 | 63 | set_target_properties( 64 | libcurl 65 | PROPERTIES INTERFACE_COMPILE_DEFINITIONS "CURL_STATICLIB" 66 | INTERFACE_INCLUDE_DIRECTORIES "${libcurl_fetch_SOURCE_DIR}/include" 67 | INTERFACE_LINK_LIBRARIES "${libcurl_fetch_link_libs}") 68 | set_property( 69 | TARGET libcurl 70 | APPEND 71 | PROPERTY IMPORTED_CONFIGURATIONS RELEASE) 72 | set_target_properties(libcurl PROPERTIES IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "C" IMPORTED_LOCATION_RELEASE 73 | ${libcurl_fetch_lib_location}) 74 | -------------------------------------------------------------------------------- /.github/scripts/Package-Windows.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [ValidateSet('x64')] 4 | [string] $Target = 'x64', 5 | [ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')] 6 | [string] $Configuration = 'RelWithDebInfo', 7 | [switch] $BuildInstaller, 8 | [switch] $SkipDeps 9 | ) 10 | 11 | $ErrorActionPreference = 'Stop' 12 | 13 | if ( $DebugPreference -eq 'Continue' ) { 14 | $VerbosePreference = 'Continue' 15 | $InformationPreference = 'Continue' 16 | } 17 | 18 | if ( ! ( [System.Environment]::Is64BitOperatingSystem ) ) { 19 | throw "Packaging script requires a 64-bit system to build and run." 20 | } 21 | 22 | 23 | if ( $PSVersionTable.PSVersion -lt '7.0.0' ) { 24 | Write-Warning 'The packaging script requires PowerShell Core 7. Install or upgrade your PowerShell version: https://aka.ms/pscore6' 25 | exit 2 26 | } 27 | 28 | function Package { 29 | trap { 30 | Pop-Location -Stack BuildTemp -ErrorAction 'SilentlyContinue' 31 | Write-Error $_ 32 | Log-Group 33 | exit 2 34 | } 35 | 36 | $ScriptHome = $PSScriptRoot 37 | $ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.." 38 | $BuildSpecFile = "${ProjectRoot}/buildspec.json" 39 | 40 | $UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse 41 | 42 | foreach( $Utility in $UtilityFunctions ) { 43 | Write-Debug "Loading $($Utility.FullName)" 44 | . $Utility.FullName 45 | } 46 | 47 | $BuildSpec = Get-Content -Path ${BuildSpecFile} -Raw | ConvertFrom-Json 48 | $ProductName = $BuildSpec.name 49 | $ProductVersion = $BuildSpec.version 50 | 51 | $OutputName = "${ProductName}-${ProductVersion}-windows-${Target}" 52 | 53 | if ( ! $SkipDeps ) { 54 | Install-BuildDependencies -WingetFile "${ScriptHome}/.Wingetfile" 55 | } 56 | 57 | $RemoveArgs = @{ 58 | ErrorAction = 'SilentlyContinue' 59 | Path = @( 60 | "${ProjectRoot}/release/${ProductName}-*-windows-*.zip" 61 | "${ProjectRoot}/release/${ProductName}-*-windows-*.exe" 62 | ) 63 | } 64 | 65 | Remove-Item @RemoveArgs 66 | 67 | Log-Group "Archiving ${ProductName}..." 68 | $CompressArgs = @{ 69 | Path = (Get-ChildItem -Path "${ProjectRoot}/release/${Configuration}" -Exclude "${OutputName}*.*") 70 | CompressionLevel = 'Optimal' 71 | DestinationPath = "${ProjectRoot}/release/${OutputName}.zip" 72 | Verbose = ($Env:CI -ne $null) 73 | } 74 | Compress-Archive -Force @CompressArgs 75 | Log-Group 76 | 77 | if ( ( $BuildInstaller ) ) { 78 | Log-Group "Packaging ${ProductName}..." 79 | 80 | $IsccFile = "${ProjectRoot}/build_${Target}/installer-Windows.generated.iss" 81 | if ( ! ( Test-Path -Path $IsccFile ) ) { 82 | throw 'InnoSetup install script not found. Run the build script or the CMake build and install procedures first.' 83 | } 84 | 85 | Log-Information 'Creating InnoSetup installer...' 86 | Push-Location -Stack BuildTemp 87 | Ensure-Location -Path "${ProjectRoot}/release" 88 | Copy-Item -Path ${Configuration} -Destination Package -Recurse 89 | Invoke-External iscc ${IsccFile} /O"${ProjectRoot}/release" /F"${OutputName}-Installer" 90 | Remove-Item -Path Package -Recurse 91 | Pop-Location -Stack BuildTemp 92 | 93 | Log-Group 94 | } 95 | } 96 | 97 | Package 98 | -------------------------------------------------------------------------------- /.github/scripts/Build-Windows.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param( 3 | [ValidateSet('x64')] 4 | [string] $Target = 'x64', 5 | [ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')] 6 | [string] $Configuration = 'RelWithDebInfo', 7 | [switch] $SkipAll, 8 | [switch] $SkipBuild, 9 | [switch] $SkipDeps 10 | ) 11 | 12 | $ErrorActionPreference = 'Stop' 13 | 14 | if ( $DebugPreference -eq 'Continue' ) { 15 | $VerbosePreference = 'Continue' 16 | $InformationPreference = 'Continue' 17 | } 18 | 19 | if ( ! ( [System.Environment]::Is64BitOperatingSystem ) ) { 20 | throw "A 64-bit system is required to build the project." 21 | } 22 | 23 | if ( $PSVersionTable.PSVersion -lt '7.0.0' ) { 24 | Write-Warning 'The obs-deps PowerShell build script requires PowerShell Core 7. Install or upgrade your PowerShell version: https://aka.ms/pscore6' 25 | exit 2 26 | } 27 | 28 | function Build { 29 | trap { 30 | Pop-Location -Stack BuildTemp -ErrorAction 'SilentlyContinue' 31 | Write-Error $_ 32 | Log-Group 33 | exit 2 34 | } 35 | 36 | $ScriptHome = $PSScriptRoot 37 | $ProjectRoot = Resolve-Path -Path "$PSScriptRoot/../.." 38 | $BuildSpecFile = "${ProjectRoot}/buildspec.json" 39 | 40 | $UtilityFunctions = Get-ChildItem -Path $PSScriptRoot/utils.pwsh/*.ps1 -Recurse 41 | 42 | foreach($Utility in $UtilityFunctions) { 43 | Write-Debug "Loading $($Utility.FullName)" 44 | . $Utility.FullName 45 | } 46 | 47 | $BuildSpec = Get-Content -Path ${BuildSpecFile} -Raw | ConvertFrom-Json 48 | $ProductName = $BuildSpec.name 49 | $ProductVersion = $BuildSpec.version 50 | 51 | if ( ! $SkipDeps ) { 52 | Install-BuildDependencies -WingetFile "${ScriptHome}/.Wingetfile" 53 | } 54 | 55 | Push-Location -Stack BuildTemp 56 | if ( ! ( ( $SkipAll ) -or ( $SkipBuild ) ) ) { 57 | Ensure-Location $ProjectRoot 58 | 59 | $CmakeArgs = @() 60 | $CmakeBuildArgs = @() 61 | $CmakeInstallArgs = @() 62 | 63 | if ( $VerbosePreference -eq 'Continue' ) { 64 | $CmakeBuildArgs += ('--verbose') 65 | $CmakeInstallArgs += ('--verbose') 66 | } 67 | 68 | if ( $DebugPreference -eq 'Continue' ) { 69 | $CmakeArgs += ('--debug-output') 70 | } 71 | 72 | $Preset = "windows-$(if ( $Env:CI -ne $null ) { 'ci-' })${Target}" 73 | 74 | $CmakeArgs += @( 75 | '--preset', $Preset 76 | ) 77 | 78 | $CmakeBuildArgs += @( 79 | '--build' 80 | '--preset', $Preset 81 | '--config', $Configuration 82 | '--parallel' 83 | '--', '/consoleLoggerParameters:Summary', '/noLogo' 84 | ) 85 | 86 | $CmakeInstallArgs += @( 87 | '--install', "build_${Target}" 88 | '--prefix', "${ProjectRoot}/release/${Configuration}" 89 | '--config', $Configuration 90 | ) 91 | 92 | Log-Group "Configuring ${ProductName}..." 93 | Invoke-External cmake @CmakeArgs 94 | 95 | Log-Group "Building ${ProductName}..." 96 | Invoke-External cmake @CmakeBuildArgs 97 | } 98 | Log-Group "Install ${ProductName}..." 99 | Invoke-External cmake @CmakeInstallArgs 100 | 101 | Pop-Location -Stack BuildTemp 102 | Log-Group 103 | } 104 | 105 | Build 106 | -------------------------------------------------------------------------------- /cmake/BuildGoogleAPIs.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.11) 2 | 3 | include(FetchContent) 4 | 5 | # Fetch the googleapis repository 6 | FetchContent_Declare( 7 | googleapis 8 | GIT_REPOSITORY https://github.com/googleapis/googleapis.git 9 | GIT_TAG master) 10 | 11 | FetchContent_MakeAvailable(googleapis) 12 | 13 | # Define the proto file 14 | set(PROTO_FILES 15 | ${googleapis_SOURCE_DIR}/google/cloud/speech/v1/cloud_speech.proto 16 | ${googleapis_SOURCE_DIR}/google/cloud/speech/v1/resource.proto 17 | ${googleapis_SOURCE_DIR}/google/api/annotations.proto 18 | ${googleapis_SOURCE_DIR}/google/api/http.proto 19 | ${googleapis_SOURCE_DIR}/google/api/client.proto 20 | ${googleapis_SOURCE_DIR}/google/api/launch_stage.proto 21 | ${googleapis_SOURCE_DIR}/google/api/field_behavior.proto 22 | ${googleapis_SOURCE_DIR}/google/api/resource.proto 23 | ${googleapis_SOURCE_DIR}/google/longrunning/operations.proto 24 | ${googleapis_SOURCE_DIR}/google/rpc/status.proto) 25 | 26 | set(GOOGLE_APIS_OUTPUT_FOLDER ${CMAKE_SOURCE_DIR}/src/cloud-providers/google) 27 | 28 | set(GOOGLE_APIS_OUTPUT_FILES "") 29 | 30 | # run protoc to generate the grpc files 31 | foreach(PROTO_FILE ${PROTO_FILES}) 32 | message(STATUS "Generating C++ code from ${PROTO_FILE}") 33 | 34 | # create .pb.cc and .pb.h file paths from the proto file 35 | get_filename_component(PROTO_FILE_NAME ${PROTO_FILE} NAME) 36 | string(REPLACE ".proto" ".pb.cc" PROTO_CC_FILE ${PROTO_FILE_NAME}) 37 | string(REPLACE ".proto" ".pb.h" PROTO_H_FILE ${PROTO_FILE_NAME}) 38 | string(REPLACE ".proto" ".grpc.pb.cc" PROTO_GRPC_FILE ${PROTO_FILE_NAME}) 39 | string(REPLACE ".proto" ".grpc.pb.h" PROTO_GRPC_H_FILE ${PROTO_FILE_NAME}) 40 | 41 | # get proto file path relative to the googleapis directory 42 | file(RELATIVE_PATH PROTO_FILE_RELATIVE ${googleapis_SOURCE_DIR} ${PROTO_FILE}) 43 | # get the directory of the proto file 44 | get_filename_component(PROTO_FILE_DIR ${PROTO_FILE_RELATIVE} DIRECTORY) 45 | 46 | # append the output files to the list 47 | list(APPEND GOOGLE_APIS_OUTPUT_FILES ${GOOGLE_APIS_OUTPUT_FOLDER}/${PROTO_FILE_DIR}/${PROTO_CC_FILE} 48 | ${GOOGLE_APIS_OUTPUT_FOLDER}/${PROTO_FILE_DIR}/${PROTO_GRPC_FILE}) 49 | 50 | # Generate the grpc files 51 | add_custom_command( 52 | OUTPUT ${GOOGLE_APIS_OUTPUT_FOLDER}/${PROTO_FILE_DIR}/${PROTO_CC_FILE} 53 | ${GOOGLE_APIS_OUTPUT_FOLDER}/${PROTO_FILE_DIR}/${PROTO_GRPC_FILE} 54 | COMMAND ${PROTOC_EXECUTABLE} --cpp_out=${GOOGLE_APIS_OUTPUT_FOLDER} --grpc_out=${GOOGLE_APIS_OUTPUT_FOLDER} 55 | --plugin=protoc-gen-grpc=${GRPC_PLUGIN_EXECUTABLE} -I ${googleapis_SOURCE_DIR} ${PROTO_FILE} 56 | DEPENDS ${PROTO_FILE}) 57 | endforeach() 58 | 59 | # add a library for the generated files 60 | add_library(google-apis ${GOOGLE_APIS_OUTPUT_FILES}) 61 | 62 | # disable conversion warnings from the generated files 63 | if(MSVC) 64 | target_compile_options(google-apis PRIVATE /wd4244 /wd4267 /wd4099) 65 | else() 66 | target_compile_options( 67 | google-apis 68 | PRIVATE -fPIC 69 | -Wno-conversion 70 | -Wno-sign-conversion 71 | -Wno-unused-parameter 72 | -Wno-unused-variable 73 | -Wno-error=shadow 74 | -Wno-shadow 75 | -Wno-error=conversion) 76 | endif() 77 | 78 | target_include_directories(google-apis PUBLIC ${GOOGLE_APIS_OUTPUT_FOLDER} ${DEPS_INCLUDE_DIRS}) 79 | 80 | # link the grpc libraries 81 | target_link_libraries(google-apis PRIVATE ${DEPS_LIBRARIES}) 82 | target_link_directories(google-apis PRIVATE ${DEPS_LIB_DIRS}) 83 | 84 | # link the library to the main project 85 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE google-apis) 86 | -------------------------------------------------------------------------------- /src/cloud-providers/cloud-provider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "cloudvocal-processing.h" 10 | #include "cloudvocal-data.h" 11 | #include "plugin-support.h" 12 | 13 | class CloudProvider { 14 | public: 15 | using TranscriptionCallback = std::function; 16 | 17 | CloudProvider(TranscriptionCallback callback, cloudvocal_data *gf_) 18 | : transcription_callback(callback), 19 | running(false), 20 | gf(gf_), 21 | stop_requested(false), 22 | needs_results_thread(false) 23 | { 24 | } 25 | 26 | virtual ~CloudProvider() { stop(); } 27 | 28 | virtual bool init() = 0; 29 | 30 | void start() 31 | { 32 | stop_requested = false; 33 | transcription_thread = std::thread(&CloudProvider::processAudio, this); 34 | if (needs_results_thread) { 35 | results_thread = std::thread(&CloudProvider::processResults, this); 36 | } 37 | } 38 | 39 | void stop() 40 | { 41 | obs_log(gf->log_level, "Stopping cloud provider"); 42 | stop_requested = true; 43 | gf->input_buffers_cv.notify_all(); 44 | if (transcription_thread.joinable()) { 45 | obs_log(gf->log_level, "Joining transcription thread..."); 46 | transcription_thread.join(); 47 | } 48 | if (results_thread.joinable()) { 49 | obs_log(gf->log_level, "Joining results thread..."); 50 | results_thread.join(); 51 | } 52 | running = false; 53 | } 54 | 55 | bool isRunning() const { return running; } 56 | 57 | protected: 58 | virtual void sendAudioBufferToTranscription(const std::deque &audio_buffer) = 0; 59 | virtual void readResultsFromTranscription() = 0; 60 | virtual void shutdown() = 0; 61 | 62 | void processAudio() 63 | { 64 | // Initialize the cloud provider 65 | if (!init()) { 66 | obs_log(LOG_ERROR, "Failed to initialize cloud provider"); 67 | running = false; 68 | return; 69 | } 70 | 71 | running = true; 72 | 73 | uint64_t start_timestamp_offset_ns = 0; 74 | uint64_t end_timestamp_offset_ns = 0; 75 | 76 | while (running && !stop_requested) { 77 | get_data_from_buf_and_resample(gf, start_timestamp_offset_ns, 78 | end_timestamp_offset_ns); 79 | 80 | if (gf->resampled_buffer.empty()) { 81 | std::this_thread::sleep_for(std::chrono::milliseconds(10)); 82 | continue; 83 | } 84 | 85 | sendAudioBufferToTranscription(gf->resampled_buffer); 86 | 87 | gf->resampled_buffer.clear(); 88 | 89 | // sleep until the next audio packet is ready 90 | // wait for notificaiton from the audio buffer condition variable 91 | std::unique_lock lock(gf->input_buffers_mutex); 92 | gf->input_buffers_cv.wait(lock, [this] { 93 | return !(gf->input_buffers[0]).empty() || !running || 94 | stop_requested; 95 | }); 96 | } 97 | 98 | // Shutdown the cloud provider 99 | shutdown(); 100 | 101 | obs_log(gf->log_level, "Cloud provider audio thread stopped"); 102 | this->running = false; 103 | } 104 | 105 | void processResults() 106 | { 107 | while (running && !stop_requested) { 108 | readResultsFromTranscription(); 109 | } 110 | 111 | obs_log(gf->log_level, "Cloud provider results thread stopped"); 112 | } 113 | 114 | cloudvocal_data *gf; 115 | std::atomic running; 116 | std::atomic stop_requested; 117 | TranscriptionCallback transcription_callback; 118 | bool needs_results_thread; 119 | 120 | private: 121 | std::thread transcription_thread; 122 | std::thread results_thread; 123 | }; 124 | 125 | std::shared_ptr createCloudProvider(const std::string &providerType, 126 | CloudProvider::TranscriptionCallback callback, 127 | cloudvocal_data *gf); 128 | 129 | void restart_cloud_provider(cloudvocal_data *gf); 130 | -------------------------------------------------------------------------------- /src/cloud-translation/azure.cpp: -------------------------------------------------------------------------------- 1 | #include "azure.h" 2 | #include "utils/curl-helper.h" 3 | #include 4 | #include 5 | 6 | using json = nlohmann::json; 7 | 8 | AzureTranslator::AzureTranslator(const std::string &api_key, const std::string &location, 9 | const std::string &endpoint) 10 | : api_key_(api_key), 11 | location_(location), 12 | endpoint_(endpoint), 13 | curl_helper_(std::make_unique()) 14 | { 15 | } 16 | 17 | AzureTranslator::~AzureTranslator() = default; 18 | 19 | std::string AzureTranslator::translate(const std::string &text, const std::string &target_lang, 20 | const std::string &source_lang) 21 | { 22 | std::unique_ptr curl(curl_easy_init(), 23 | curl_easy_cleanup); 24 | 25 | if (!curl) { 26 | throw TranslationError("Failed to initialize CURL session"); 27 | } 28 | 29 | std::string response; 30 | 31 | try { 32 | // Construct the route 33 | std::stringstream route; 34 | route << "/translate?api-version=3.0" 35 | << "&to=" << sanitize_language_code(target_lang); 36 | 37 | if (source_lang != "auto") { 38 | route << "&from=" << sanitize_language_code(source_lang); 39 | } 40 | 41 | // Create the request body 42 | json body = json::array({{{"Text", text}}}); 43 | std::string requestBody = body.dump(); 44 | 45 | // Construct full URL 46 | std::string url = endpoint_ + route.str(); 47 | 48 | // Set up curl options 49 | curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); 50 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, CurlHelper::WriteCallback); 51 | curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response); 52 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L); 53 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L); 54 | curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 30L); 55 | 56 | // Set up POST request 57 | curl_easy_setopt(curl.get(), CURLOPT_POST, 1L); 58 | curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, requestBody.c_str()); 59 | 60 | // Set up headers 61 | struct curl_slist *headers = nullptr; 62 | headers = curl_slist_append(headers, "Content-Type: application/json"); 63 | 64 | std::string auth_header = "Ocp-Apim-Subscription-Key: " + api_key_; 65 | headers = curl_slist_append(headers, auth_header.c_str()); 66 | 67 | // Add location header if provided 68 | if (!location_.empty()) { 69 | std::string location_header = "Ocp-Apim-Subscription-Region: " + location_; 70 | headers = curl_slist_append(headers, location_header.c_str()); 71 | } 72 | 73 | curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers); 74 | 75 | // Perform request 76 | CURLcode res = curl_easy_perform(curl.get()); 77 | 78 | // Clean up headers 79 | curl_slist_free_all(headers); 80 | 81 | if (res != CURLE_OK) { 82 | throw TranslationError(std::string("CURL request failed: ") + 83 | curl_easy_strerror(res)); 84 | } 85 | 86 | return parseResponse(response); 87 | 88 | } catch (const json::exception &e) { 89 | throw TranslationError(std::string("JSON parsing error: ") + e.what()); 90 | } 91 | } 92 | 93 | std::string AzureTranslator::parseResponse(const std::string &response_str) 94 | { 95 | try { 96 | json response = json::parse(response_str); 97 | 98 | // Check for error response 99 | if (response.contains("error")) { 100 | const auto &error = response["error"]; 101 | throw TranslationError("Azure API Error: " + 102 | error.value("message", "Unknown error")); 103 | } 104 | 105 | // Azure returns an array of translations 106 | // Each translation can have multiple target languages 107 | // We'll take the first translation's first target 108 | return response[0]["translations"][0]["text"].get(); 109 | 110 | } catch (const json::exception &e) { 111 | throw TranslationError(std::string("Failed to parse Azure response: ") + e.what()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/cloud-translation/custom-api.cpp: -------------------------------------------------------------------------------- 1 | #include "custom-api.h" 2 | #include "utils/curl-helper.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | using json = nlohmann::json; 9 | 10 | CustomApiTranslator::CustomApiTranslator(const std::string &endpoint, 11 | const std::string &body_template, 12 | const std::string &response_json_path) 13 | : endpoint_(endpoint), 14 | body_template_(body_template), 15 | response_json_path_(response_json_path), 16 | curl_helper_(std::make_unique()) 17 | { 18 | } 19 | 20 | CustomApiTranslator::~CustomApiTranslator() = default; 21 | 22 | std::string CustomApiTranslator::translate(const std::string &text, const std::string &target_lang, 23 | const std::string &source_lang) 24 | { 25 | // first encode text to JSON compatible string 26 | nlohmann::json tmp = text; 27 | std::string textStr = tmp.dump(); 28 | // remove '"' from the beginning and end of the string 29 | textStr = textStr.substr(1, textStr.size() - 2); 30 | // then replace the placeholders in the body template 31 | std::unordered_map values = { 32 | {"\\{\\{sentence\\}\\}", textStr}, 33 | {"\\{\\{target_lang\\}\\}", target_lang}, 34 | {"\\{\\{source_lang\\}\\}", source_lang}}; 35 | 36 | std::string body = replacePlaceholders(body_template_, values); 37 | std::string response; 38 | 39 | std::unique_ptr curl(curl_easy_init(), 40 | curl_easy_cleanup); 41 | 42 | if (!curl) { 43 | throw std::runtime_error("Failed to initialize CURL session"); 44 | } 45 | 46 | try { 47 | // Set up curl options 48 | curl_easy_setopt(curl.get(), CURLOPT_URL, endpoint_.c_str()); 49 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, CurlHelper::WriteCallback); 50 | curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response); 51 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L); 52 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L); 53 | curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 30L); 54 | 55 | // Set up POST request 56 | curl_easy_setopt(curl.get(), CURLOPT_POST, 1L); 57 | curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, body.c_str()); 58 | 59 | // Set up headers 60 | struct curl_slist *headers = nullptr; 61 | headers = curl_slist_append(headers, "Content-Type: application/json"); 62 | curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers); 63 | 64 | // Perform request 65 | CURLcode res = curl_easy_perform(curl.get()); 66 | 67 | // Clean up headers 68 | curl_slist_free_all(headers); 69 | 70 | if (res != CURLE_OK) { 71 | throw TranslationError(std::string("CURL request failed: ") + 72 | curl_easy_strerror(res)); 73 | } 74 | 75 | return parseResponse(response); 76 | 77 | } catch (const std::exception &e) { 78 | throw TranslationError(std::string("JSON parsing error: ") + e.what()); 79 | } 80 | } 81 | 82 | std::string CustomApiTranslator::replacePlaceholders( 83 | const std::string &template_str, 84 | const std::unordered_map &values) const 85 | { 86 | std::string result = template_str; 87 | for (const auto &pair : values) { 88 | try { 89 | std::regex placeholder(pair.first); 90 | result = std::regex_replace(result, placeholder, pair.second); 91 | } catch (const std::regex_error &e) { 92 | // Handle regex error 93 | throw TranslationError(std::string("Regex error: ") + e.what()); 94 | } 95 | } 96 | return result; 97 | } 98 | 99 | std::string CustomApiTranslator::parseResponse(const std::string &response_str) 100 | { 101 | try { 102 | // parse the JSON response 103 | json response = json::parse(response_str); 104 | 105 | // extract the translation from the JSON response 106 | std::string response_out = response[response_json_path_]; 107 | 108 | return response_out; 109 | } catch (const json::exception &e) { 110 | throw TranslationError(std::string("JSON parsing error: ") + e.what()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/utils/ssl-utils.cpp: -------------------------------------------------------------------------------- 1 | #include "ssl-utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | #include "plugin-support.h" 19 | #include 20 | 21 | void init_openssl() 22 | { 23 | OpenSSL_add_all_algorithms(); 24 | ERR_load_crypto_strings(); 25 | } 26 | 27 | // HMAC SHA-256 function 28 | std::string hmacSha256(const std::string &key, const std::string &data, bool isHexKey) 29 | { 30 | unsigned char *digest = (unsigned char *)bzalloc(EVP_MAX_MD_SIZE); 31 | size_t len = EVP_MAX_MD_SIZE; 32 | 33 | // Prepare the key 34 | std::vector keyBytes; 35 | if (isHexKey) { 36 | for (size_t i = 0; i < key.length(); i += 2) { 37 | std::string byteString = key.substr(i, 2); 38 | unsigned char byte = (unsigned char)strtol(byteString.c_str(), NULL, 16); 39 | keyBytes.push_back(byte); 40 | } 41 | } else { 42 | keyBytes.assign(key.begin(), key.end()); 43 | } 44 | 45 | if (!HMAC(EVP_sha256(), keyBytes.data(), keyBytes.size(), (unsigned char *)data.c_str(), 46 | data.length(), digest, (unsigned int *)&len)) { 47 | obs_log(LOG_ERROR, "hmacSha256 failed during HMAC operation"); 48 | return {}; 49 | } 50 | 51 | std::stringstream ss; 52 | for (size_t i = 0; i < len; ++i) { 53 | ss << std::hex << std::setw(2) << std::setfill('0') << (int)digest[i]; 54 | } 55 | 56 | bfree(digest); 57 | return ss.str(); 58 | } 59 | 60 | std::string sha256(const std::string &data) 61 | { 62 | unsigned char hash[EVP_MAX_MD_SIZE]; 63 | unsigned int lengthOfHash = 0; 64 | 65 | EVP_MD_CTX *context = EVP_MD_CTX_new(); 66 | 67 | if (context != nullptr) { 68 | if (EVP_DigestInit_ex(context, EVP_sha256(), nullptr)) { 69 | if (EVP_DigestUpdate(context, data.c_str(), data.length())) { 70 | if (EVP_DigestFinal_ex(context, hash, &lengthOfHash)) { 71 | EVP_MD_CTX_free(context); 72 | 73 | std::stringstream ss; 74 | for (unsigned int i = 0; i < lengthOfHash; ++i) { 75 | ss << std::hex << std::setw(2) << std::setfill('0') 76 | << (int)hash[i]; 77 | } 78 | return ss.str(); 79 | } 80 | } 81 | } 82 | EVP_MD_CTX_free(context); 83 | } 84 | 85 | return ""; 86 | } 87 | 88 | std::string getCurrentTimestamp() 89 | { 90 | auto now = std::chrono::system_clock::now(); 91 | auto in_time_t = std::chrono::system_clock::to_time_t(now); 92 | std::stringstream ss; 93 | ss << std::put_time(std::gmtime(&in_time_t), "%Y%m%dT%H%M%SZ"); 94 | return ss.str(); 95 | } 96 | 97 | std::string getCurrentDate() 98 | { 99 | auto now = std::chrono::system_clock::now(); 100 | auto in_time_t = std::chrono::system_clock::to_time_t(now); 101 | std::stringstream ss; 102 | ss << std::put_time(std::gmtime(&in_time_t), "%Y%m%d"); 103 | return ss.str(); 104 | } 105 | 106 | std::string PEMrootCerts() 107 | { 108 | // Read the contents of the root CA file bundled with this plugin module 109 | char *root_cert_file_path = obs_module_file("roots.pem"); 110 | if (root_cert_file_path == nullptr) { 111 | obs_log(LOG_ERROR, "Failed to get root certificate file path"); 112 | return ""; 113 | } 114 | std::ifstream root_cert_file(root_cert_file_path); 115 | if (!root_cert_file.is_open()) { 116 | obs_log(LOG_ERROR, "Failed to open certificate file"); 117 | return ""; 118 | } 119 | 120 | std::stringstream cert_stream; 121 | cert_stream << root_cert_file.rdbuf(); 122 | root_cert_file.close(); 123 | return cert_stream.str(); 124 | } 125 | 126 | std::string PEMrootCertsPath() 127 | { 128 | char *root_cert_file_path = obs_module_file("roots.pem"); 129 | if (root_cert_file_path == nullptr) { 130 | obs_log(LOG_ERROR, "Failed to get root certificate file path"); 131 | return ""; 132 | } 133 | 134 | return std::string(root_cert_file_path); 135 | } 136 | -------------------------------------------------------------------------------- /.github/scripts/utils.pwsh/Logger.ps1: -------------------------------------------------------------------------------- 1 | function Log-Debug { 2 | [CmdletBinding()] 3 | param( 4 | [Parameter(Mandatory,ValueFromPipeline)] 5 | [ValidateNotNullOrEmpty()] 6 | [string[]] $Message 7 | ) 8 | 9 | Process { 10 | foreach($m in $Message) { 11 | Write-Debug "$(if ( $env:CI -ne $null ) { '::debug::' })$m" 12 | } 13 | } 14 | } 15 | 16 | function Log-Verbose { 17 | [CmdletBinding()] 18 | param( 19 | [Parameter(Mandatory,ValueFromPipeline)] 20 | [ValidateNotNullOrEmpty()] 21 | [string[]] $Message 22 | ) 23 | 24 | Process { 25 | foreach($m in $Message) { 26 | Write-Verbose $m 27 | } 28 | } 29 | } 30 | 31 | function Log-Warning { 32 | [CmdletBinding()] 33 | param( 34 | [Parameter(Mandatory,ValueFromPipeline)] 35 | [ValidateNotNullOrEmpty()] 36 | [string[]] $Message 37 | ) 38 | 39 | Process { 40 | foreach($m in $Message) { 41 | Write-Warning "$(if ( $env:CI -ne $null ) { '::warning::' })$m" 42 | } 43 | } 44 | } 45 | 46 | function Log-Error { 47 | [CmdletBinding()] 48 | param( 49 | [Parameter(Mandatory,ValueFromPipeline)] 50 | [ValidateNotNullOrEmpty()] 51 | [string[]] $Message 52 | ) 53 | 54 | Process { 55 | foreach($m in $Message) { 56 | Write-Error "$(if ( $env:CI -ne $null ) { '::error::' })$m" 57 | } 58 | } 59 | } 60 | 61 | function Log-Information { 62 | [CmdletBinding()] 63 | param( 64 | [Parameter(Mandatory,ValueFromPipeline)] 65 | [ValidateNotNullOrEmpty()] 66 | [string[]] $Message 67 | ) 68 | 69 | Process { 70 | if ( ! ( $script:Quiet ) ) { 71 | $StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' }) 72 | $Icon = ' =>' 73 | 74 | foreach($m in $Message) { 75 | Write-Host -NoNewLine -ForegroundColor Blue " ${StageName} $($Icon.PadRight(5)) " 76 | Write-Host "${m}" 77 | } 78 | } 79 | } 80 | } 81 | 82 | function Log-Group { 83 | [CmdletBinding()] 84 | param( 85 | [Parameter(ValueFromPipeline)] 86 | [string[]] $Message 87 | ) 88 | 89 | Process { 90 | if ( $Env:CI -ne $null ) { 91 | if ( $script:LogGroup ) { 92 | Write-Output '::endgroup::' 93 | $script:LogGroup = $false 94 | } 95 | 96 | if ( $Message.count -ge 1 ) { 97 | Write-Output "::group::$($Message -join ' ')" 98 | $script:LogGroup = $true 99 | } 100 | } else { 101 | if ( $Message.count -ge 1 ) { 102 | Log-Information $Message 103 | } 104 | } 105 | } 106 | } 107 | 108 | function Log-Status { 109 | [CmdletBinding()] 110 | param( 111 | [Parameter(Mandatory,ValueFromPipeline)] 112 | [ValidateNotNullOrEmpty()] 113 | [string[]] $Message 114 | ) 115 | 116 | Process { 117 | if ( ! ( $script:Quiet ) ) { 118 | $StageName = $( if ( $StageName -ne $null ) { $StageName } else { '' }) 119 | $Icon = ' >' 120 | 121 | foreach($m in $Message) { 122 | Write-Host -NoNewLine -ForegroundColor Green " ${StageName} $($Icon.PadRight(5)) " 123 | Write-Host "${m}" 124 | } 125 | } 126 | } 127 | } 128 | 129 | function Log-Output { 130 | [CmdletBinding()] 131 | param( 132 | [Parameter(Mandatory,ValueFromPipeline)] 133 | [ValidateNotNullOrEmpty()] 134 | [string[]] $Message 135 | ) 136 | 137 | Process { 138 | if ( ! ( $script:Quiet ) ) { 139 | $StageName = $( if ( $script:StageName -ne $null ) { $script:StageName } else { '' }) 140 | $Icon = '' 141 | 142 | foreach($m in $Message) { 143 | Write-Output " ${StageName} $($Icon.PadRight(5)) ${m}" 144 | } 145 | } 146 | } 147 | } 148 | 149 | $Columns = (Get-Host).UI.RawUI.WindowSize.Width - 5 150 | -------------------------------------------------------------------------------- /cmake/macos/helpers.cmake: -------------------------------------------------------------------------------- 1 | # CMake macOS helper functions module 2 | 3 | # cmake-format: off 4 | # cmake-lint: disable=C0103 5 | # cmake-lint: disable=C0307 6 | # cmake-format: on 7 | 8 | include_guard(GLOBAL) 9 | 10 | include(helpers_common) 11 | 12 | # set_target_properties_obs: Set target properties for use in obs-studio 13 | function(set_target_properties_plugin target) 14 | set(options "") 15 | set(oneValueArgs "") 16 | set(multiValueArgs PROPERTIES) 17 | cmake_parse_arguments(PARSE_ARGV 0 _STPO "${options}" "${oneValueArgs}" "${multiValueArgs}") 18 | 19 | message(DEBUG "Setting additional properties for target ${target}...") 20 | 21 | while(_STPO_PROPERTIES) 22 | list(POP_FRONT _STPO_PROPERTIES key value) 23 | set_property(TARGET ${target} PROPERTY ${key} "${value}") 24 | endwhile() 25 | 26 | string(TIMESTAMP CURRENT_YEAR "%Y") 27 | set_target_properties( 28 | ${target} 29 | PROPERTIES BUNDLE TRUE 30 | BUNDLE_EXTENSION plugin 31 | XCODE_ATTRIBUTE_PRODUCT_NAME ${target} 32 | XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER ${MACOS_BUNDLEID} 33 | XCODE_ATTRIBUTE_CURRENT_PROJECT_VERSION ${PLUGIN_BUILD_NUMBER} 34 | XCODE_ATTRIBUTE_MARKETING_VERSION ${PLUGIN_VERSION} 35 | XCODE_ATTRIBUTE_GENERATE_INFOPLIST_FILE YES 36 | XCODE_ATTRIBUTE_INFOPLIST_FILE "" 37 | XCODE_ATTRIBUTE_INFOPLIST_KEY_CFBundleDisplayName ${target} 38 | XCODE_ATTRIBUTE_INFOPLIST_KEY_NSHumanReadableCopyright "(c) ${CURRENT_YEAR} ${PLUGIN_AUTHOR}" 39 | XCODE_ATTRIBUTE_INSTALL_PATH "$(USER_LIBRARY_DIR)/Application Support/obs-studio/plugins") 40 | 41 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist") 42 | set_target_properties(${target} PROPERTIES XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS 43 | "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos/entitlements.plist") 44 | endif() 45 | 46 | if(TARGET plugin-support) 47 | target_link_libraries(${target} PRIVATE plugin-support) 48 | endif() 49 | 50 | target_install_resources(${target}) 51 | 52 | get_target_property(target_sources ${target} SOURCES) 53 | set(target_ui_files ${target_sources}) 54 | list(FILTER target_ui_files INCLUDE REGEX ".+\\.(ui|qrc)") 55 | source_group( 56 | TREE "${CMAKE_CURRENT_SOURCE_DIR}" 57 | PREFIX "UI Files" 58 | FILES ${target_ui_files}) 59 | 60 | install(TARGETS ${target} LIBRARY DESTINATION .) 61 | install( 62 | FILES "$.dsym" 63 | CONFIGURATIONS Release 64 | DESTINATION . 65 | OPTIONAL) 66 | 67 | configure_file(cmake/macos/resources/distribution.in "${CMAKE_CURRENT_BINARY_DIR}/distribution" @ONLY) 68 | configure_file(cmake/macos/resources/create-package.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake" @ONLY) 69 | install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/create-package.cmake") 70 | endfunction() 71 | 72 | # target_install_resources: Helper function to add resources into bundle 73 | function(target_install_resources target) 74 | message(DEBUG "Installing resources for target ${target}...") 75 | if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/data") 76 | file(GLOB_RECURSE data_files "${CMAKE_CURRENT_SOURCE_DIR}/data/*") 77 | foreach(data_file IN LISTS data_files) 78 | cmake_path(RELATIVE_PATH data_file BASE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data/" OUTPUT_VARIABLE 79 | relative_path) 80 | cmake_path(GET relative_path PARENT_PATH relative_path) 81 | target_sources(${target} PRIVATE "${data_file}") 82 | set_property(SOURCE "${data_file}" PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${relative_path}") 83 | source_group("Resources/${relative_path}" FILES "${data_file}") 84 | endforeach() 85 | endif() 86 | endfunction() 87 | 88 | # target_add_resource: Helper function to add a specific resource to a bundle 89 | function(target_add_resource target resource) 90 | message(DEBUG "Add resource ${resource} to target ${target} at destination ${destination}...") 91 | target_sources(${target} PRIVATE "${resource}") 92 | set_property(SOURCE "${resource}" PROPERTY MACOSX_PACKAGE_LOCATION Resources) 93 | source_group("Resources" FILES "${resource}") 94 | endfunction() 95 | -------------------------------------------------------------------------------- /.github/actions/package-plugin/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Package plugin' 2 | description: 'Packages the plugin for specified architecture and build config.' 3 | inputs: 4 | target: 5 | description: 'Build target for dependencies' 6 | required: true 7 | config: 8 | description: 'Build configuration' 9 | required: false 10 | default: 'RelWithDebInfo' 11 | codesign: 12 | description: 'Enable codesigning (macOS only)' 13 | required: false 14 | default: 'false' 15 | notarize: 16 | description: 'Enable notarization (macOS only)' 17 | required: false 18 | default: 'false' 19 | codesignIdent: 20 | description: 'Developer ID for application codesigning (macOS only)' 21 | required: false 22 | default: '-' 23 | installerIdent: 24 | description: 'Developer ID for installer package codesigning (macOS only)' 25 | required: false 26 | default: '' 27 | codesignTeam: 28 | description: 'Developer team for codesigning (macOS only)' 29 | required: false 30 | default: '' 31 | codesignUser: 32 | description: 'Apple ID username for notarization (macOS only)' 33 | required: false 34 | default: '' 35 | codesignPass: 36 | description: 'Apple ID password for notarization (macOS only)' 37 | required: false 38 | default: '' 39 | package: 40 | description: 'Create Windows or macOS installation package' 41 | required: false 42 | default: 'false' 43 | workingDirectory: 44 | description: 'Working directory for packaging' 45 | required: false 46 | default: ${{ github.workspace }} 47 | runs: 48 | using: composite 49 | steps: 50 | - name: Run macOS Packaging 51 | if: runner.os == 'macOS' 52 | shell: zsh --no-rcs --errexit --pipefail {0} 53 | working-directory: ${{ inputs.workingDirectory }} 54 | env: 55 | CODESIGN_IDENT: ${{ inputs.codesignIdent }} 56 | CODESIGN_IDENT_INSTALLER: ${{ inputs.installerIdent }} 57 | CODESIGN_TEAM: ${{ inputs.codesignTeam }} 58 | CODESIGN_IDENT_USER: ${{ inputs.codesignUser }} 59 | CODESIGN_IDENT_PASS: ${{ inputs.codesignPass }} 60 | run: | 61 | : Run macOS Packaging 62 | 63 | local -a package_args=(--config ${{ inputs.config }}) 64 | if (( ${+RUNNER_DEBUG} )) package_args+=(--debug) 65 | 66 | if [[ '${{ inputs.codesign }}' == 'true' ]] package_args+=(--codesign) 67 | if [[ '${{ inputs.notarize }}' == 'true' ]] package_args+=(--notarize) 68 | if [[ '${{ inputs.package }}' == 'true' ]] package_args+=(--package) 69 | 70 | .github/scripts/package-macos ${package_args} 71 | 72 | - name: Install Dependencies 🛍️ 73 | if: runner.os == 'Linux' 74 | shell: bash 75 | run: | 76 | : Install Dependencies 🛍️ 77 | echo ::group::Install Dependencies 78 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 79 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 80 | brew install --quiet zsh 81 | echo ::endgroup:: 82 | 83 | - name: Run Ubuntu Packaging 84 | if: runner.os == 'Linux' 85 | shell: zsh --no-rcs --errexit --pipefail {0} 86 | working-directory: ${{ inputs.workingDirectory }} 87 | run: | 88 | : Run Ubuntu Packaging 89 | package_args=( 90 | --target linux-${{ inputs.target }} 91 | --config ${{ inputs.config }} 92 | ) 93 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 94 | 95 | if [[ '${{ inputs.package }}' == 'true' ]] package_args+=(--package) 96 | 97 | .github/scripts/package-linux ${package_args} 98 | 99 | - name: Run Windows Packaging 100 | if: runner.os == 'Windows' 101 | shell: pwsh 102 | run: | 103 | # Run Windows Packaging 104 | if ( $Env:RUNNER_DEBUG -ne $null ) { 105 | Set-PSDebug -Trace 1 106 | } 107 | 108 | $PackageArgs = @{ 109 | Target = '${{ inputs.target }}' 110 | Configuration = '${{ inputs.config }}' 111 | } 112 | 113 | if ( '${{ inputs.package }}' -eq 'true' ) { 114 | $PackageArgs += @{BuildInstaller = $true} 115 | } 116 | 117 | .github/scripts/Package-Windows.ps1 @PackageArgs 118 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: Push to master 2 | run-name: ${{ github.ref_name }} push run 🚀 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | - 'release/**' 9 | tags: 10 | - '*' 11 | permissions: 12 | contents: write 13 | concurrency: 14 | group: '${{ github.workflow }} @ ${{ github.ref }}' 15 | cancel-in-progress: ${{ github.ref_type == 'tag' }} 16 | jobs: 17 | check-format: 18 | name: Check Formatting 🔍 19 | if: github.ref_name == 'master' 20 | uses: ./.github/workflows/check-format.yaml 21 | permissions: 22 | contents: read 23 | 24 | build-project: 25 | name: Build Project 🧱 26 | uses: ./.github/workflows/build-project.yaml 27 | secrets: inherit 28 | permissions: 29 | contents: read 30 | 31 | create-release: 32 | name: Create Release 🛫 33 | if: github.ref_type == 'tag' 34 | runs-on: ubuntu-22.04 35 | needs: build-project 36 | defaults: 37 | run: 38 | shell: bash 39 | steps: 40 | - name: Check Release Tag ☑️ 41 | id: check 42 | run: | 43 | : Check Release Tag ☑️ 44 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 45 | shopt -s extglob 46 | 47 | case "${GITHUB_REF_NAME}" in 48 | +([0-9]).+([0-9]).+([0-9]) ) 49 | echo 'validTag=true' >> $GITHUB_OUTPUT 50 | echo 'prerelease=false' >> $GITHUB_OUTPUT 51 | echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT 52 | ;; 53 | +([0-9]).+([0-9]).+([0-9])-@(beta|rc)*([0-9]) ) 54 | echo 'validTag=true' >> $GITHUB_OUTPUT 55 | echo 'prerelease=true' >> $GITHUB_OUTPUT 56 | echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT 57 | ;; 58 | *) echo 'validTag=false' >> $GITHUB_OUTPUT ;; 59 | esac 60 | 61 | - name: Download Build Artifacts 📥 62 | uses: actions/download-artifact@v4 63 | if: fromJSON(steps.check.outputs.validTag) 64 | id: download 65 | 66 | - name: Rename Files 🏷️ 67 | if: fromJSON(steps.check.outputs.validTag) 68 | run: | 69 | : Rename Files 🏷️ 70 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 71 | shopt -s extglob 72 | shopt -s nullglob 73 | 74 | root_dir="$(pwd)" 75 | commit_hash="${GITHUB_SHA:0:9}" 76 | 77 | variants=( 78 | 'windows-x64;zip|exe' 79 | 'macos-universal;tar.xz|pkg' 80 | 'ubuntu-22.04-x86_64;tar.xz|deb|ddeb' 81 | 'sources;tar.xz' 82 | ) 83 | 84 | for variant_data in "${variants[@]}"; do 85 | IFS=';' read -r variant suffix <<< "${variant_data}" 86 | 87 | candidates=(*-${variant}-${commit_hash}/@(*|*-dbgsym).@(${suffix})) 88 | 89 | for candidate in "${candidates[@]}"; do 90 | mv "${candidate}" "${root_dir}" 91 | done 92 | done 93 | 94 | - name: Generate Checksums 🪪 95 | if: fromJSON(steps.check.outputs.validTag) 96 | run: | 97 | : Generate Checksums 🪪 98 | if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi 99 | shopt -s extglob 100 | 101 | echo "### Checksums" > ${{ github.workspace }}/CHECKSUMS.txt 102 | for file in ${{ github.workspace }}/@(*.exe|*.deb|*.ddeb|*.pkg|*.tar.xz|*.zip); do 103 | echo " ${file##*/}: $(sha256sum "${file}" | cut -d " " -f 1)" >> ${{ github.workspace }}/CHECKSUMS.txt 104 | done 105 | 106 | - name: Create Release 🛫 107 | if: fromJSON(steps.check.outputs.validTag) 108 | id: create_release 109 | uses: softprops/action-gh-release@v2 110 | with: 111 | draft: true 112 | prerelease: ${{ fromJSON(steps.check.outputs.prerelease) }} 113 | tag_name: ${{ steps.check.outputs.version }} 114 | name: CloudVocal ${{ steps.check.outputs.version }} 115 | body_path: ${{ github.workspace }}/CHECKSUMS.txt 116 | generate_release_notes: true 117 | append_body: true 118 | fail_on_unmatched_files: false 119 | files: | 120 | ${{ github.workspace }}/*.exe 121 | ${{ github.workspace }}/*.zip 122 | ${{ github.workspace }}/*.pkg 123 | ${{ github.workspace }}/*.deb 124 | ${{ github.workspace }}/*.ddeb 125 | ${{ github.workspace }}/*.tar.xz 126 | -------------------------------------------------------------------------------- /src/cloud-translation/deepl.cpp: -------------------------------------------------------------------------------- 1 | #include "deepl.h" 2 | #include "utils/curl-helper.h" 3 | #include 4 | #include 5 | 6 | using json = nlohmann::json; 7 | 8 | DeepLTranslator::DeepLTranslator(const std::string &api_key, bool free) 9 | : api_key_(api_key), 10 | free_(free), 11 | curl_helper_(std::make_unique()) 12 | { 13 | } 14 | 15 | DeepLTranslator::~DeepLTranslator() = default; 16 | 17 | std::string DeepLTranslator::translate(const std::string &text, const std::string &target_lang, 18 | const std::string &source_lang) 19 | { 20 | std::unique_ptr curl(curl_easy_init(), 21 | curl_easy_cleanup); 22 | 23 | if (!curl) { 24 | throw TranslationError("DeepL Failed to initialize CURL session"); 25 | } 26 | 27 | std::string response; 28 | 29 | try { 30 | // Construct URL with parameters 31 | // Note: DeepL uses uppercase language codes 32 | std::string upperTarget = sanitize_language_code(target_lang); 33 | std::string upperSource = sanitize_language_code(source_lang); 34 | for (char &c : upperTarget) 35 | c = (char)std::toupper((int)c); 36 | for (char &c : upperSource) 37 | c = (char)std::toupper((int)c); 38 | 39 | json body = {{"text", {text}}, 40 | {"target_lang", upperTarget}, 41 | {"source_lang", upperSource}}; 42 | const std::string body_str = body.dump(); 43 | 44 | std::string url = "https://api.deepl.com/v2/translate"; 45 | if (free_) { 46 | url = "https://api-free.deepl.com/v2/translate"; 47 | } 48 | 49 | // Set up curl options 50 | curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); 51 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, CurlHelper::WriteCallback); 52 | curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response); 53 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L); 54 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L); 55 | curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 30L); 56 | curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, body_str.c_str()); 57 | curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, body_str.size()); 58 | curl_easy_setopt(curl.get(), CURLOPT_POST, 1L); 59 | 60 | // DeepL requires specific headers 61 | struct curl_slist *headers = nullptr; 62 | headers = curl_slist_append(headers, "Content-Type: application/json"); 63 | headers = curl_slist_append(headers, 64 | ("Authorization: DeepL-Auth-Key " + api_key_).c_str()); 65 | curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers); 66 | 67 | CURLcode res = curl_easy_perform(curl.get()); 68 | 69 | // Clean up headers 70 | curl_slist_free_all(headers); 71 | 72 | if (res != CURLE_OK) { 73 | throw TranslationError(std::string("DeepL: CURL request failed: ") + 74 | curl_easy_strerror(res)); 75 | } 76 | 77 | return parseResponse(response); 78 | 79 | } catch (const json::exception &e) { 80 | throw TranslationError(std::string("DeepL JSON parsing error: ") + e.what() + 81 | ". Response: " + response); 82 | } 83 | } 84 | 85 | std::string DeepLTranslator::parseResponse(const std::string &response_str) 86 | { 87 | // Handle rate limiting errors 88 | long response_code; 89 | curl_easy_getinfo(curl_easy_init(), CURLINFO_RESPONSE_CODE, &response_code); 90 | if (response_code == 429) { 91 | throw TranslationError("DeepL API Error: Rate limit exceeded"); 92 | } 93 | if (response_code == 456) { 94 | throw TranslationError("DeepL API Error: Quota exceeded"); 95 | } 96 | 97 | /* 98 | { 99 | "translations": [ 100 | { 101 | "detected_source_language": "EN", 102 | "text": "Hallo, Welt!" 103 | } 104 | ] 105 | } 106 | */ 107 | json response = json::parse(response_str); 108 | 109 | // Check for API errors 110 | if (response.contains("message")) { 111 | throw TranslationError("DeepL API Error: " + 112 | response["message"].get()); 113 | } 114 | 115 | try { 116 | // DeepL returns translations array with detected language 117 | const auto &translation = response["translations"][0]; 118 | 119 | // Optionally, you can access the detected source language 120 | // if (translation.contains("detected_source_language")) { 121 | // std::string detected = translation["detected_source_language"]; 122 | // } 123 | 124 | return translation["text"].get(); 125 | } catch (const json::exception &) { 126 | throw TranslationError("DeepL: Unexpected response format from DeepL API"); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /cmake/BuildDependencies.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | 3 | list(APPEND CMAKE_PREFIX_PATH ${CMAKE_SOURCE_DIR}/build_conan) 4 | 5 | find_package(gRPC CONFIG REQUIRED) 6 | find_package(protobuf CONFIG REQUIRED) 7 | find_package(absl CONFIG REQUIRED) 8 | find_package(ZLIB CONFIG REQUIRED PATHS ${CMAKE_SOURCE_DIR}/build_conan NO_DEFAULT_PATH) 9 | find_package(OpenSSL CONFIG REQUIRED) 10 | find_package(c-ares CONFIG REQUIRED) 11 | find_package(re2 CONFIG REQUIRED) 12 | find_package(Boost CONFIG REQUIRED) 13 | find_package(AWSSDK CONFIG REQUIRED) 14 | find_package(BZip2 CONFIG REQUIRED) 15 | 16 | set(PROTOC_EXECUTABLE 17 | ${Protobuf_PROTOC_EXECUTABLE} 18 | CACHE STRING "protoc executable") 19 | set(GRPC_PLUGIN_EXECUTABLE 20 | ${GRPC_CPP_PLUGIN_PROGRAM} 21 | CACHE STRING "gRPC plugin executable") 22 | list( 23 | APPEND 24 | DEPS_LIBRARIES 25 | ${grpc_LIBS_RELEASE} 26 | ${abseil_LIBS_RELEASE} 27 | ${protobuf_LIBS_RELEASE} 28 | ${openssl_LIBS_RELEASE} 29 | ${zlib_LIBS_RELEASE} 30 | openssl::openssl 31 | ${c-ares_LIBS_RELEASE} 32 | ${re2_LIBS_RELEASE} 33 | ${boost_Boost_url_LIBS_RELEASE} 34 | ${aws-c-auth_LIBS_RELEASE} 35 | ${aws-c-cal_LIBS_RELEASE} 36 | ${aws-c-common_LIBS_RELEASE} 37 | ${aws-c-compression_LIBS_RELEASE} 38 | ${aws-c-event-stream_LIBS_RELEASE} 39 | ${aws-c-http_LIBS_RELEASE} 40 | ${aws-c-io_LIBS_RELEASE} 41 | ${aws-c-mqtt_LIBS_RELEASE} 42 | ${aws-c-s3_LIBS_RELEASE} 43 | ${aws-c-sdkutils_LIBS_RELEASE} 44 | ${aws-checksums_LIBS_RELEASE} 45 | ${aws-crt-cpp_LIBS_RELEASE} 46 | ${aws-sdk-cpp_LIBS_RELEASE} 47 | ${bzip2_LIBS_RELEASE}) 48 | if(WIN32) 49 | list( 50 | APPEND 51 | DEPS_LIBRARIES 52 | userenv 53 | advapi32 54 | ws2_32 55 | crypt32 56 | bcrypt 57 | winhttp 58 | shlwapi 59 | wininet 60 | secur32 61 | iphlpapi 62 | netapi32 63 | rpcrt4 64 | shell32 65 | version 66 | ncrypt) 67 | endif() 68 | if(APPLE) 69 | list(APPEND DEPS_LIBRARIES resolv) 70 | endif() 71 | 72 | list( 73 | APPEND 74 | DEPS_LIB_DIRS 75 | ${grpc_LIB_DIRS_RELEASE} 76 | ${abseil_LIB_DIRS_RELEASE} 77 | ${protobuf_LIB_DIRS_RELEASE} 78 | ${zlib_LIB_DIRS_RELEASE} 79 | ${openssl_LIB_DIRS_RELEASE} 80 | ${c-ares_LIB_DIRS_RELEASE} 81 | ${re2_LIB_DIRS_RELEASE} 82 | ${boost_LIB_DIRS_RELEASE} 83 | ${aws-c-auth_LIB_DIRS_RELEASE} 84 | ${aws-c-cal_LIB_DIRS_RELEASE} 85 | ${aws-c-common_LIB_DIRS_RELEASE} 86 | ${aws-c-compression_LIB_DIRS_RELEASE} 87 | ${aws-c-event-stream_LIB_DIRS_RELEASE} 88 | ${aws-c-http_LIB_DIRS_RELEASE} 89 | ${aws-c-io_LIB_DIRS_RELEASE} 90 | ${aws-c-mqtt_LIB_DIRS_RELEASE} 91 | ${aws-c-sdkutils_LIB_DIRS_RELEASE} 92 | ${aws-c-s3_LIB_DIRS_RELEASE} 93 | ${aws-checksums_LIB_DIRS_RELEASE} 94 | ${aws-crt-cpp_LIB_DIRS_RELEASE} 95 | ${aws-sdk-cpp_LIB_DIRS_RELEASE} 96 | ${bzip2_LIB_DIRS_RELEASE}) 97 | list( 98 | APPEND 99 | DEPS_INCLUDE_DIRS 100 | ${Boost_INCLUDE_DIRS} 101 | ${gRPC_INCLUDE_DIRS} 102 | ${absl_INCLUDE_DIRS} 103 | ${protobuf_INCLUDE_DIRS_RELEASE} 104 | ${zlib_INCLUDE_DIRS_RELEASE} 105 | ${openssl_INCLUDE_DIRS_RELEASE} 106 | ${c-ares_INCLUDE_DIRS} 107 | ${re2_INCLUDE_DIRS} 108 | ${aws-c-auth_INCLUDE_DIRS_RELEASE} 109 | ${aws-c-cal_INCLUDE_DIRS_RELEASE} 110 | ${aws-c-common_INCLUDE_DIRS_RELEASE} 111 | ${aws-c-compression_INCLUDE_DIRS_RELEASE} 112 | ${aws-c-event-stream_INCLUDE_DIRS_RELEASE} 113 | ${aws-c-http_INCLUDE_DIRS_RELEASE} 114 | ${aws-c-io_INCLUDE_DIRS_RELEASE} 115 | ${aws-c-mqtt_INCLUDE_DIRS_RELEASE} 116 | ${aws-c-sdkutils_INCLUDE_DIRS_RELEASE} 117 | ${aws-c-s3_INCLUDE_DIRS_RELEASE} 118 | ${aws-checksums_INCLUDE_DIRS_RELEASE} 119 | ${aws-crt-cpp_INCLUDE_DIRS_RELEASE} 120 | ${aws-sdk-cpp_INCLUDE_DIRS_RELEASE} 121 | ${bzip2_INCLUDE_DIRS_RELEASE}) 122 | 123 | message(STATUS "Dependencies include directories: ${DEPS_INCLUDE_DIRS}") 124 | message(STATUS "Dependencies library directories: ${DEPS_LIB_DIRS}") 125 | message(STATUS "Dependencies libraries: ${DEPS_LIBRARIES}") 126 | message(STATUS "protoc executable: ${PROTOC_EXECUTABLE}") 127 | message(STATUS "grpc_cpp_plugin executable: ${GRPC_PLUGIN_EXECUTABLE}") 128 | 129 | if(NOT PROTOC_EXECUTABLE OR NOT GRPC_PLUGIN_EXECUTABLE) 130 | message(FATAL_ERROR "protoc or grpc_cpp_plugin not found") 131 | endif() 132 | 133 | # Add include directories 134 | target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${DEPS_INCLUDE_DIRS}) 135 | 136 | # Link libraries 137 | target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE ${DEPS_LIB_DIRS}) 138 | target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${DEPS_LIBRARIES}) 139 | -------------------------------------------------------------------------------- /src/cloudvocal-processing.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "cloudvocal-processing.h" 3 | #include "cloudvocal-data.h" 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include "plugin-support.h" 10 | 11 | int get_data_from_buf_and_resample(cloudvocal_data *gf, uint64_t &start_timestamp_offset_ns, 12 | uint64_t &end_timestamp_offset_ns) 13 | { 14 | uint32_t num_frames_from_infos = 0; 15 | 16 | // copy buffers 17 | std::vector copy_buffers[8]; 18 | 19 | { 20 | // scoped lock the buffer mutex 21 | std::lock_guard lock(gf->input_buffers_mutex); 22 | 23 | if (gf->input_buffers[0].empty()) { 24 | return 1; 25 | } 26 | 27 | #ifdef CLOUDVOCAL_EXTRA_VERBOSE 28 | obs_log(gf->log_level, 29 | "segmentation: currently %lu bytes in the audio input buffer", 30 | gf->input_buffers[0].size); 31 | #endif 32 | 33 | // max number of frames is 10 seconds worth of audio 34 | const size_t max_num_frames = gf->sample_rate * 10; 35 | 36 | // pop all infos from the info buffer and mark the beginning timestamp from the first 37 | // info as the beginning timestamp of the segment 38 | struct cloudvocal_audio_info info_from_buf = {0}; 39 | while (gf->info_buffer.size() > 0) { 40 | info_from_buf = gf->info_buffer.front(); 41 | num_frames_from_infos += info_from_buf.frames; 42 | if (start_timestamp_offset_ns == 0) { 43 | start_timestamp_offset_ns = info_from_buf.timestamp_offset_ns; 44 | } 45 | // Check if we're within the needed segment length 46 | if (num_frames_from_infos > max_num_frames) { 47 | // too big, break out of the loop 48 | num_frames_from_infos -= info_from_buf.frames; 49 | break; 50 | } else { 51 | // pop the info from the info buffer 52 | gf->info_buffer.pop_front(); 53 | } 54 | } 55 | // calculate the end timestamp from the last info plus the number of frames in the packet 56 | end_timestamp_offset_ns = info_from_buf.timestamp_offset_ns + 57 | info_from_buf.frames * 1000000000 / gf->sample_rate; 58 | 59 | if (start_timestamp_offset_ns > end_timestamp_offset_ns) { 60 | // this may happen when the incoming media has a timestamp reset 61 | // in this case, we should figure out the start timestamp from the end timestamp 62 | // and the number of frames 63 | start_timestamp_offset_ns = 64 | end_timestamp_offset_ns - 65 | num_frames_from_infos * 1000000000 / gf->sample_rate; 66 | } 67 | 68 | /* Pop from input circlebuf */ 69 | for (size_t c = 0; c < gf->channels; c++) { 70 | // Push the new data to copy_buffers[c] 71 | copy_buffers[c].resize(num_frames_from_infos); 72 | std::copy(gf->input_buffers[c].begin(), 73 | gf->input_buffers[c].begin() + num_frames_from_infos, 74 | copy_buffers[c].begin()); 75 | // Pop the data from the input buffer 76 | gf->input_buffers[c].erase(gf->input_buffers[c].begin(), 77 | gf->input_buffers[c].begin() + 78 | num_frames_from_infos); 79 | } 80 | } 81 | 82 | #ifdef CLOUDVOCAL_EXTRA_VERBOSE 83 | obs_log(gf->log_level, "found %d frames from info buffer.", num_frames_from_infos); 84 | #endif 85 | gf->last_num_frames = num_frames_from_infos; 86 | 87 | if (num_frames_from_infos <= 0 || copy_buffers[0].empty()) { 88 | obs_log(LOG_ERROR, "No audio data found in the input buffer"); 89 | return 1; 90 | } 91 | 92 | if (gf->resampler == nullptr) { 93 | obs_log(LOG_ERROR, "Resampler is not initialized"); 94 | return 1; 95 | } 96 | 97 | { 98 | // resample to 16kHz 99 | float *resampled_16khz[8]; 100 | uint32_t resampled_16khz_frames; 101 | uint64_t ts_offset; 102 | uint8_t *copy_buffers_8[8]; 103 | for (size_t c = 0; c < gf->channels; c++) { 104 | copy_buffers_8[c] = (uint8_t *)copy_buffers[c].data(); 105 | } 106 | bool success = audio_resampler_resample(gf->resampler, (uint8_t **)resampled_16khz, 107 | &resampled_16khz_frames, &ts_offset, 108 | (const uint8_t **)copy_buffers_8, 109 | (uint32_t)num_frames_from_infos); 110 | 111 | if (!success) { 112 | obs_log(LOG_ERROR, "Failed to resample audio data"); 113 | return 1; 114 | } 115 | 116 | // push back resampled data to resampled buffer 117 | gf->resampled_buffer.insert(gf->resampled_buffer.end(), resampled_16khz[0], 118 | resampled_16khz[0] + resampled_16khz_frames); 119 | #ifdef CLOUDVOCAL_EXTRA_VERBOSE 120 | obs_log(gf->log_level, 121 | "resampled: %d channels, %d frames, %f ms, current size: %lu bytes", 122 | (int)gf->channels, (int)resampled_16khz_frames, 123 | (float)resampled_16khz_frames / TRANSCRIPTION_SAMPLE_RATE * 1000.0f, 124 | gf->resampled_buffer.size); 125 | #endif 126 | } 127 | 128 | return 0; 129 | } 130 | -------------------------------------------------------------------------------- /src/cloud-translation/claude.cpp: -------------------------------------------------------------------------------- 1 | #include "claude.h" 2 | #include "utils/curl-helper.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "language-codes/language-codes.h" 9 | 10 | using json = nlohmann::json; 11 | 12 | ClaudeTranslator::ClaudeTranslator(const std::string &api_key, const std::string &model) 13 | : api_key_(api_key), 14 | model_(model), 15 | curl_helper_(std::make_unique()) 16 | { 17 | } 18 | 19 | ClaudeTranslator::~ClaudeTranslator() = default; 20 | 21 | std::string ClaudeTranslator::createSystemPrompt(const std::string &target_lang) const 22 | { 23 | std::string target_language = getLanguageName(target_lang); 24 | 25 | return "You are a professional translator. Translate the user's text into " + 26 | target_language + " while preserving the meaning, tone, and style. " + 27 | "Provide only the translated text without explanations, notes, or any other content. " + 28 | "Maintain any formatting, line breaks, or special characters from the original text."; 29 | } 30 | 31 | std::string ClaudeTranslator::translate(const std::string &text, const std::string &target_lang, 32 | const std::string &source_lang) 33 | { 34 | if (!isLanguageSupported(target_lang)) { 35 | throw TranslationError("Unsupported target language: " + target_lang); 36 | } 37 | 38 | if (source_lang != "auto" && !isLanguageSupported(source_lang)) { 39 | throw TranslationError("Unsupported source language: " + source_lang); 40 | } 41 | 42 | std::unique_ptr curl(curl_easy_init(), 43 | curl_easy_cleanup); 44 | 45 | if (!curl) { 46 | throw TranslationError("Failed to initialize CURL session"); 47 | } 48 | 49 | std::string response; 50 | 51 | try { 52 | // Prepare the request 53 | std::string url = "https://api.anthropic.com/v1/messages"; 54 | 55 | // Create request body 56 | json request_body = {{"model", model_}, 57 | {"max_tokens", 4096}, 58 | {"system", createSystemPrompt(target_lang)}, 59 | {"messages", 60 | json::array({{{"role", "user"}, {"content", text}}})}}; 61 | 62 | if (source_lang != "auto") { 63 | request_body["system"] = createSystemPrompt(target_lang) + 64 | " The source text is in " + 65 | getLanguageName(source_lang) + "."; 66 | } 67 | 68 | std::string payload = request_body.dump(); 69 | 70 | // Set up headers 71 | struct curl_slist *headers = nullptr; 72 | headers = curl_slist_append(headers, "Content-Type: application/json"); 73 | headers = curl_slist_append(headers, ("x-api-key: " + api_key_).c_str()); 74 | headers = curl_slist_append(headers, "anthropic-version: 2023-06-01"); 75 | 76 | // Set up CURL request 77 | curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); 78 | curl_easy_setopt(curl.get(), CURLOPT_POST, 1L); 79 | curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, payload.c_str()); 80 | curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers); 81 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, CurlHelper::WriteCallback); 82 | curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response); 83 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L); 84 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L); 85 | curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 30L); 86 | 87 | // Perform request 88 | CURLcode res = curl_easy_perform(curl.get()); 89 | 90 | // Clean up 91 | curl_slist_free_all(headers); 92 | 93 | if (res != CURLE_OK) { 94 | throw TranslationError(std::string("CURL request failed: ") + 95 | curl_easy_strerror(res)); 96 | } 97 | 98 | // Check HTTP response code 99 | long response_code; 100 | curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response_code); 101 | 102 | if (response_code != 200) { 103 | throw TranslationError("HTTP error: " + std::to_string(response_code) + 104 | "\nResponse: " + response); 105 | } 106 | 107 | return parseResponse(response); 108 | 109 | } catch (const json::exception &e) { 110 | throw TranslationError(std::string("JSON parsing error: ") + e.what()); 111 | } 112 | } 113 | 114 | std::string ClaudeTranslator::parseResponse(const std::string &response_str) 115 | { 116 | try { 117 | json response = json::parse(response_str); 118 | 119 | if (!response.contains("content") || !response["content"].is_array() || 120 | response["content"].empty() || !response["content"][0].contains("text")) { 121 | throw TranslationError("Invalid response format from Claude API"); 122 | } 123 | 124 | return response["content"][0]["text"].get(); 125 | 126 | } catch (const json::exception &e) { 127 | throw TranslationError(std::string("Failed to parse Claude response: ") + e.what()); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/cloud-providers/deepgram/deepgram-provider.cpp: -------------------------------------------------------------------------------- 1 | #include "deepgram-provider.h" 2 | #include 3 | 4 | #include "language-codes/language-codes.h" 5 | 6 | using json = nlohmann::json; 7 | 8 | namespace http = beast::http; 9 | 10 | DeepgramProvider::DeepgramProvider(TranscriptionCallback callback, cloudvocal_data *gf_) 11 | : CloudProvider(callback, gf_), 12 | ioc(), 13 | ssl_ctx(ssl::context::tlsv12_client), 14 | resolver(ioc), 15 | ws(ioc, ssl_ctx) 16 | { 17 | needs_results_thread = true; // We need a separate thread for reading results 18 | } 19 | 20 | bool DeepgramProvider::init() 21 | { 22 | try { 23 | // Setup SSL context 24 | ssl_ctx.set_verify_mode(ssl::verify_peer); 25 | ssl_ctx.set_default_verify_paths(); 26 | 27 | // Resolve the Deepgram endpoint 28 | auto const results = resolver.resolve("api.deepgram.com", "443"); 29 | 30 | // Connect to Deepgram 31 | net::connect(get_lowest_layer(ws), results); 32 | 33 | // Set SNI hostname (required for TLS) 34 | if (!SSL_set_tlsext_host_name(ws.next_layer().native_handle(), 35 | "api.deepgram.com")) { 36 | throw beast::system_error( 37 | beast::error_code(static_cast(::ERR_get_error()), 38 | net::error::get_ssl_category()), 39 | "Failed to set SNI hostname"); 40 | } 41 | 42 | // Perform SSL handshake 43 | ws.next_layer().handshake(ssl::stream_base::client); 44 | 45 | // Set up WebSocket handshake with API key 46 | ws.set_option( 47 | websocket::stream_base::decorator([this](websocket::request_type &req) { 48 | req.set(http::field::sec_websocket_protocol, 49 | "token, " + std::string(gf->cloud_provider_api_key)); 50 | })); 51 | 52 | std::string query = std::string("/v1/listen?encoding=linear16&sample_rate=16000") + 53 | "&language=" + language_codes_from_underscore[gf->language]; 54 | // Perform WebSocket handshake 55 | ws.handshake("api.deepgram.com", query); 56 | 57 | obs_log(LOG_INFO, "Connected to Deepgram WebSocket successfully"); 58 | return true; 59 | } catch (std::exception const &e) { 60 | obs_log(LOG_ERROR, "Error initializing Deepgram connection: %s", e.what()); 61 | return false; 62 | } 63 | } 64 | 65 | void DeepgramProvider::sendAudioBufferToTranscription(const std::deque &audio_buffer) 66 | { 67 | if (audio_buffer.empty()) 68 | return; 69 | 70 | try { 71 | // Convert float audio to int16_t (linear16 format) 72 | std::vector pcm_data; 73 | pcm_data.reserve(audio_buffer.size()); 74 | 75 | for (float sample : audio_buffer) { 76 | // Clamp and convert to int16 77 | float clamped = std::max(-1.0f, std::min(1.0f, sample)); 78 | pcm_data.push_back(static_cast(clamped * 32767.0f)); 79 | } 80 | 81 | // Send binary message 82 | ws.write(net::buffer(pcm_data.data(), pcm_data.size() * sizeof(int16_t))); 83 | 84 | } catch (std::exception const &e) { 85 | obs_log(LOG_ERROR, "Error sending audio to Deepgram: %s", e.what()); 86 | running = false; 87 | } 88 | } 89 | 90 | void DeepgramProvider::readResultsFromTranscription() 91 | { 92 | try { 93 | // Read message into buffer 94 | beast::flat_buffer buffer; 95 | ws.read(buffer); 96 | 97 | // Convert to string and parse JSON 98 | std::string msg = beast::buffers_to_string(buffer.data()); 99 | json result = json::parse(msg); 100 | 101 | // Check if this is a transcription result 102 | if (result["type"] == "Results" && !result["channel"]["alternatives"].empty()) { 103 | DetectionResultWithText detection_result; 104 | 105 | // Fill the detection result structure 106 | detection_result.text = result["channel"]["alternatives"][0]["transcript"]; 107 | detection_result.result = result["is_final"] ? DETECTION_RESULT_SPEECH 108 | : DETECTION_RESULT_PARTIAL; 109 | 110 | // If there are words with timestamps 111 | if (!result["channel"]["alternatives"][0]["words"].empty()) { 112 | auto &words = result["channel"]["alternatives"][0]["words"]; 113 | detection_result.start_timestamp_ms = words[0]["start"]; 114 | detection_result.end_timestamp_ms = words[words.size() - 1]["end"]; 115 | } 116 | 117 | // Send result through callback 118 | transcription_callback(detection_result); 119 | } 120 | } catch (std::exception const &e) { 121 | obs_log(LOG_ERROR, "Error reading from Deepgram: %s", e.what()); 122 | } 123 | } 124 | 125 | void DeepgramProvider::shutdown() 126 | { 127 | try { 128 | // Send close message 129 | ws.write(net::buffer(R"({"type":"CloseStream"})")); 130 | 131 | // Close WebSocket connection 132 | ws.close(websocket::close_code::normal); 133 | 134 | obs_log(LOG_INFO, "Deepgram connection closed successfully"); 135 | } catch (std::exception const &e) { 136 | obs_log(LOG_ERROR, "Error during Deepgram shutdown: %s", e.what()); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.github/actions/build-plugin/action.yaml: -------------------------------------------------------------------------------- 1 | name: 'Set up and build plugin' 2 | description: 'Builds the plugin for specified architecture and build config' 3 | inputs: 4 | target: 5 | description: 'Target architecture for dependencies' 6 | required: true 7 | config: 8 | description: 'Build configuration' 9 | required: false 10 | default: 'RelWithDebInfo' 11 | codesign: 12 | description: 'Enable codesigning (macOS only)' 13 | required: false 14 | default: 'false' 15 | codesignIdent: 16 | description: 'Developer ID for application codesigning (macOS only)' 17 | required: false 18 | default: '-' 19 | workingDirectory: 20 | description: 'Working directory for packaging' 21 | required: false 22 | default: ${{ github.workspace }} 23 | runs: 24 | using: composite 25 | steps: 26 | - name: Install Python 🐍 27 | uses: actions/setup-python@v5 28 | if: runner.os == 'Linux' || runner.os == 'macOS' 29 | with: 30 | python-version: '3.10' 31 | 32 | - name: Install Conan 🪓 33 | id: conan 34 | uses: turtlebrowser/get-conan@main 35 | 36 | - name: Install Dependencies 🛍️ 37 | if: runner.os == 'Linux' 38 | shell: bash 39 | run: | 40 | : Install Dependencies 🛍️ 41 | echo ::group::Install Dependencies 42 | eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" 43 | echo "/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin" >> $GITHUB_PATH 44 | brew install --quiet zsh 45 | echo ::endgroup:: 46 | 47 | - name: Build Dependencies 🛠️ 48 | if: runner.os == 'Linux' || runner.os == 'macOS' 49 | shell: bash 50 | run: | 51 | : Build Dependencies 🛠️ 52 | echo ::group::Build Dependencies 53 | conan profile detect --force 54 | conan install ${{github.workspace}} --output-folder=${{github.workspace}}/build_conan --build=missing -g CMakeDeps 55 | echo ::endgroup:: 56 | 57 | - name: Build Windows Dependencies 🛠️ 58 | if: runner.os == 'Windows' 59 | shell: pwsh 60 | run: | 61 | Write-Host "::group::Build Windows Dependencies" 62 | conan profile detect --force 63 | conan install ${{github.workspace}} --output-folder=${{github.workspace}}/build_conan --build=missing -g CMakeDeps 64 | Write-Host "::endgroup::" 65 | 66 | - name: Run macOS Build 67 | if: runner.os == 'macOS' 68 | shell: zsh --no-rcs --errexit --pipefail {0} 69 | working-directory: ${{ inputs.workingDirectory }} 70 | env: 71 | CODESIGN_IDENT: ${{ inputs.codesignIdent }} 72 | CODESIGN_TEAM: ${{ inputs.codesignTeam }} 73 | run: | 74 | : Run macOS Build 75 | 76 | local -a build_args=(--config ${{ inputs.config }}) 77 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 78 | 79 | if [[ '${{ inputs.codesign }}' == 'true' ]] build_args+=(--codesign) 80 | 81 | .github/scripts/build-macos ${build_args} 82 | 83 | - name: Run Ubuntu Build 84 | if: runner.os == 'Linux' 85 | shell: zsh --no-rcs --errexit --pipefail {0} 86 | working-directory: ${{ inputs.workingDirectory }} 87 | run: | 88 | : Run Ubuntu Build 89 | 90 | local -a build_args=( 91 | --target linux-${{ inputs.target }} 92 | --config ${{ inputs.config }} 93 | ) 94 | if (( ${+RUNNER_DEBUG} )) build_args+=(--debug) 95 | 96 | .github/scripts/build-linux ${build_args} 97 | 98 | - name: Run Windows Build 99 | if: runner.os == 'Windows' 100 | shell: pwsh 101 | run: | 102 | # Run Windows Build 103 | if ( $Env:RUNNER_DEBUG -ne $null ) { 104 | Set-PSDebug -Trace 1 105 | } 106 | 107 | $BuildArgs = @{ 108 | Target = '${{ inputs.target }}' 109 | Configuration = '${{ inputs.config }}' 110 | } 111 | 112 | .github/scripts/Build-Windows.ps1 @BuildArgs 113 | 114 | - name: Create Summary 📊 115 | if: contains(fromJSON('["Linux", "macOS"]'),runner.os) 116 | shell: zsh --no-rcs --errexit --pipefail {0} 117 | env: 118 | CCACHE_CONFIGPATH: ${{ inputs.workingDirectory }}/.ccache.conf 119 | run: | 120 | : Create Summary 📊 121 | 122 | local -a ccache_data 123 | if (( ${+RUNNER_DEBUG} )) { 124 | setopt XTRACE 125 | ccache_data=("${(fA)$(ccache -s -vv)}") 126 | } else { 127 | ccache_data=("${(fA)$(ccache -s)}") 128 | } 129 | 130 | print '### ${{ runner.os }} Ccache Stats (${{ inputs.target }})' >> $GITHUB_STEP_SUMMARY 131 | print '```' >> $GITHUB_STEP_SUMMARY 132 | for line (${ccache_data}) { 133 | print ${line} >> $GITHUB_STEP_SUMMARY 134 | } 135 | print '```' >> $GITHUB_STEP_SUMMARY 136 | -------------------------------------------------------------------------------- /src/cloud-providers/aws/presigned_url.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include "presigned_url.h" 16 | 17 | #include "utils/ssl-utils.h" 18 | #include "utils/curl-helper.h" 19 | 20 | AWSTranscribePresignedURL::AWSTranscribePresignedURL(const std::string &access_key, 21 | const std::string &secret_key, 22 | const std::string ®ion) 23 | : access_key_(access_key), 24 | secret_key_(secret_key), 25 | region_(region), 26 | method_("GET"), 27 | service_("transcribe"), 28 | canonical_uri_("/stream-transcription-websocket"), 29 | signed_headers_("host"), 30 | algorithm_("AWS4-HMAC-SHA256") 31 | { 32 | endpoint_ = "wss://transcribestreaming." + region_ + ".amazonaws.com:8443"; 33 | host_ = "transcribestreaming." + region_ + ".amazonaws.com"; 34 | } 35 | 36 | std::string AWSTranscribePresignedURL::get_request_url(int sample_rate, 37 | const std::string &language_code, 38 | const std::string &media_encoding, 39 | int number_of_channels, 40 | bool enable_channel_identification) 41 | { 42 | std::string AWS_ACCESS_KEY = access_key_; 43 | std::string AWS_SECRET_KEY = secret_key_; 44 | std::string REGION = region_; 45 | std::string ENDPOINT = endpoint_; 46 | 47 | std::string SERVICE = "transcribe"; 48 | std::string HOST = "transcribestreaming." + REGION + ".amazonaws.com"; 49 | std::string CANONICAL_HEADERS = "host:" + HOST + "\n"; 50 | std::string CANONICAL_URI = "/stream-transcription-websocket"; 51 | std::string DATE = getCurrentDate(); 52 | std::string TIMESTAMP = getCurrentTimestamp(); 53 | std::string PAYLOAD_HASH = sha256(""); 54 | std::string SIGNED_HEADERS = "host"; 55 | std::string METHOD = "GET"; 56 | std::string CREDENTIAL_SCOPE = DATE + "/" + REGION + "/" + SERVICE + "/aws4_request"; 57 | std::string CANONICAL_QUERY_STRING = CreateCanonicalQueryString( 58 | TIMESTAMP, CREDENTIAL_SCOPE, "en-US", "pcm", "8000", AWS_ACCESS_KEY); 59 | 60 | std::string CANONICAL_REQUEST = METHOD + "\n" + CANONICAL_URI + "\n" + 61 | CANONICAL_QUERY_STRING + "\n" + CANONICAL_HEADERS + "\n" + 62 | SIGNED_HEADERS + "\n" + PAYLOAD_HASH; 63 | 64 | std::string HASHED_CANONICAL_REQUEST = sha256(CANONICAL_REQUEST); 65 | std::string ALGORITHM = "AWS4-HMAC-SHA256"; 66 | std::ostringstream stringToSign; 67 | 68 | stringToSign << ALGORITHM << "\n" 69 | << TIMESTAMP << "\n" 70 | << CREDENTIAL_SCOPE << "\n" 71 | << HASHED_CANONICAL_REQUEST; 72 | std::string STRING_TO_SIGN = stringToSign.str(); 73 | 74 | std::string KEY = "AWS4" + AWS_SECRET_KEY; 75 | std::string DATE_KEY = hmacSha256(KEY, DATE); 76 | std::string REGION_KEY = hmacSha256(DATE_KEY, REGION, true); 77 | std::string SERVICE_KEY = hmacSha256(REGION_KEY, SERVICE, true); 78 | std::string SIGNING_KEY = hmacSha256(SERVICE_KEY, "aws4_request", true); 79 | std::string SIGNATURE = hmacSha256(SIGNING_KEY, STRING_TO_SIGN, true); 80 | CANONICAL_QUERY_STRING += "&X-Amz-Signature=" + SIGNATURE; 81 | 82 | std::ostringstream request_url_temp; 83 | request_url_temp << ENDPOINT << CANONICAL_URI << "?" << CANONICAL_QUERY_STRING; 84 | request_url_ = request_url_temp.str(); 85 | return request_url_; 86 | } 87 | 88 | std::string AWSTranscribePresignedURL::CreateCanonicalQueryString( 89 | const std::string &dateTimeString, const std::string &credentialScope, 90 | const std::string &languageCode = "en-US", const std::string &mediaEncoding = "pcm", 91 | const std::string &sampleRate = "8000", const std::string &accessKey = "") 92 | { 93 | std::string accessKeyId = accessKey; // Replace with actual access key ID 94 | std::string credentials = accessKeyId + "/" + credentialScope; 95 | std::map params = {{"X-Amz-Algorithm", "AWS4-HMAC-SHA256"}, 96 | {"X-Amz-Credential", credentials}, 97 | {"X-Amz-Date", dateTimeString}, 98 | {"X-Amz-Expires", "300"}, 99 | {"X-Amz-SignedHeaders", "host"}, 100 | {"enable-channel-identification", "true"}, 101 | {"language-code", languageCode}, 102 | {"media-encoding", mediaEncoding}, 103 | {"number-of-channels", "2"}, 104 | {"sample-rate", sampleRate}}; 105 | std::ostringstream result; 106 | bool first = true; 107 | for (const auto ¶m : params) { 108 | if (!first) { 109 | result << "&"; 110 | } 111 | first = false; 112 | result << CurlHelper::urlEncode(param.first) << "=" 113 | << CurlHelper::urlEncode(param.second); 114 | } 115 | return result.str(); 116 | } 117 | -------------------------------------------------------------------------------- /src/cloud-providers/google/google-provider.cpp: -------------------------------------------------------------------------------- 1 | #include "google-provider.h" 2 | #include "language-codes/language-codes.h" 3 | #include "utils/ssl-utils.h" 4 | 5 | using namespace google::cloud::speech::v1; 6 | 7 | bool GoogleProvider::init() 8 | { 9 | initialized = false; 10 | 11 | grpc::SslCredentialsOptions ssl_opts; 12 | ssl_opts.pem_root_certs = PEMrootCerts(); 13 | 14 | this->channel = 15 | grpc::CreateChannel("speech.googleapis.com", grpc::SslCredentials(ssl_opts)); 16 | this->stub = Speech::NewStub(channel); 17 | this->context.AddMetadata("x-goog-api-key", gf->cloud_provider_api_key); 18 | this->reader_writer = this->stub->StreamingRecognize(&context); 19 | if (!reader_writer) { 20 | obs_log(LOG_ERROR, "Failed to create reader writer for Google"); 21 | return false; 22 | } 23 | 24 | // Send the config request 25 | obs_log(gf->log_level, "Sending config request to Google"); 26 | StreamingRecognizeRequest config_request; 27 | StreamingRecognitionConfig *streaming_config = config_request.mutable_streaming_config(); 28 | RecognitionConfig *config = streaming_config->mutable_config(); 29 | config->set_language_code(getLanguageLocale(gf->language)); 30 | config->set_sample_rate_hertz(16000); 31 | config->set_audio_channel_count(1); 32 | config->set_encoding(RecognitionConfig_AudioEncoding_LINEAR16); 33 | streaming_config->set_single_utterance(false); 34 | streaming_config->set_interim_results(true); 35 | if (!reader_writer->Write(config_request)) { 36 | obs_log(LOG_ERROR, "Failed to send config request to Google"); 37 | return false; 38 | } 39 | obs_log(gf->log_level, "Config request sent to Google"); 40 | 41 | initialized = true; 42 | return initialized; 43 | } 44 | 45 | void GoogleProvider::sendAudioBufferToTranscription(const std::deque &audio_buffer) 46 | { 47 | if (!reader_writer) { 48 | obs_log(LOG_ERROR, "Reader writer is not initialized"); 49 | return; 50 | } 51 | if (audio_buffer.empty()) { 52 | obs_log(LOG_WARNING, "Audio buffer is empty"); 53 | return; 54 | } 55 | 56 | // Send the audio buffer to Clova for transcription 57 | obs_log(gf->log_level, 58 | "Sending audio buffer (%d) to Google for transcription. Chunk ID %llu", 59 | audio_buffer.size(), chunk_id); 60 | 61 | // convert from float [-1,1] to int16 62 | std::vector audio_buffer_int16(audio_buffer.size()); 63 | std::transform(audio_buffer.begin(), audio_buffer.end(), audio_buffer_int16.begin(), 64 | [](float f) { return static_cast(f * 32767); }); 65 | 66 | StreamingRecognizeRequest request; 67 | request.set_audio_content(reinterpret_cast(audio_buffer_int16.data()), 68 | audio_buffer_int16.size() * sizeof(int16_t)); 69 | 70 | try { 71 | bool success = reader_writer->Write(request); 72 | if (!success) { 73 | obs_log(LOG_ERROR, "Failed to send data request to Google"); 74 | this->stop_requested = true; 75 | return; 76 | } 77 | chunk_id++; 78 | } catch (...) { 79 | obs_log(LOG_ERROR, "Exception caught while sending data request to Google"); 80 | } 81 | } 82 | 83 | void GoogleProvider::readResultsFromTranscription() 84 | { 85 | if (!reader_writer || !initialized) { 86 | return; 87 | } 88 | StreamingRecognizeResponse response; 89 | if (reader_writer->Read(&response)) { 90 | if (response.has_error()) { 91 | obs_log(LOG_ERROR, "Google response Error: %s", 92 | response.error().message().c_str()); 93 | return; 94 | } 95 | 96 | std::string overall_transcript; 97 | bool is_final = false; 98 | 99 | for (int i = 0; i < response.results_size(); i++) { 100 | const StreamingRecognitionResult &result = response.results(i); 101 | obs_log(gf->log_level, 102 | "Google Result %d. stability %.3f. is_final %d. duration %d ns", i, 103 | result.stability(), result.is_final(), 104 | result.result_end_time().nanos()); 105 | if (!result.is_final() && result.stability() < 0.5) { 106 | obs_log(gf->log_level, "Google Result %d. Stability too low", i); 107 | continue; 108 | } 109 | if (result.alternatives_size() == 0) { 110 | obs_log(gf->log_level, "Google Result %d. No alternatives", i); 111 | continue; 112 | } 113 | const SpeechRecognitionAlternative &alternative = result.alternatives(0); 114 | std::string transcript = alternative.transcript(); 115 | obs_log(gf->log_level, "Google Transcription: '%s'", transcript.c_str()); 116 | overall_transcript += transcript; 117 | is_final = result.is_final(); 118 | } 119 | 120 | DetectionResultWithText result; 121 | result.text = overall_transcript; 122 | result.result = DETECTION_RESULT_SPEECH; 123 | result.language = language_codes_from_underscore[gf->language]; 124 | this->transcription_callback(result); 125 | } 126 | } 127 | 128 | void GoogleProvider::shutdown() 129 | { 130 | // Shutdown the Clova provider 131 | obs_log(gf->log_level, "Shutting down Google provider"); 132 | if (reader_writer && initialized) { 133 | reader_writer->WritesDone(); 134 | } 135 | initialized = false; 136 | } 137 | -------------------------------------------------------------------------------- /src/cloud-translation/openai.cpp: -------------------------------------------------------------------------------- 1 | #include "openai.h" 2 | #include "utils/curl-helper.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "language-codes/language-codes.h" 9 | 10 | using json = nlohmann::json; 11 | 12 | OpenAITranslator::OpenAITranslator(const std::string &api_key, const std::string &model) 13 | : api_key_(api_key), 14 | model_(model), 15 | curl_helper_(std::make_unique()) 16 | { 17 | } 18 | 19 | OpenAITranslator::~OpenAITranslator() = default; 20 | 21 | std::string OpenAITranslator::createSystemPrompt(const std::string &target_lang) const 22 | { 23 | std::string target_language = getLanguageName(target_lang); 24 | 25 | return "You are a professional translator. Translate the user's text into " + 26 | target_language + ". Maintain the exact meaning, tone, and style. " + 27 | "Respond with only the translated text, without any explanations or additional content. " + 28 | "Preserve all formatting, line breaks, and special characters from the original text."; 29 | } 30 | 31 | std::string OpenAITranslator::translate(const std::string &text, const std::string &target_lang, 32 | const std::string &source_lang) 33 | { 34 | if (!isLanguageSupported(target_lang)) { 35 | throw TranslationError("Unsupported target language: " + target_lang); 36 | } 37 | 38 | if (source_lang != "auto" && !isLanguageSupported(source_lang)) { 39 | throw TranslationError("Unsupported source language: " + source_lang); 40 | } 41 | 42 | std::unique_ptr curl(curl_easy_init(), 43 | curl_easy_cleanup); 44 | 45 | if (!curl) { 46 | throw TranslationError("Failed to initialize CURL session"); 47 | } 48 | 49 | std::string response; 50 | 51 | try { 52 | // Prepare the request 53 | std::string url = "https://api.openai.com/v1/chat/completions"; 54 | 55 | // Create messages array 56 | json messages = json::array(); 57 | 58 | // Add system message 59 | messages.push_back( 60 | {{"role", "system"}, {"content", createSystemPrompt(target_lang)}}); 61 | 62 | // Add user message with source language if specified 63 | std::string user_prompt = text; 64 | if (source_lang != "auto") { 65 | user_prompt = "Translate the following " + getLanguageName(source_lang) + 66 | " text:\n\n" + text; 67 | } 68 | 69 | messages.push_back({{"role", "user"}, {"content", user_prompt}}); 70 | 71 | // Create request body 72 | json request_body = {{"model", model_}, 73 | {"messages", messages}, 74 | {"temperature", 75 | 0.3}, // Lower temperature for more consistent translations 76 | {"max_tokens", 4000}}; 77 | 78 | std::string payload = request_body.dump(); 79 | 80 | // Set up headers 81 | struct curl_slist *headers = nullptr; 82 | headers = curl_slist_append(headers, "Content-Type: application/json"); 83 | headers = curl_slist_append(headers, ("Authorization: Bearer " + api_key_).c_str()); 84 | 85 | // Set up CURL request 86 | curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str()); 87 | curl_easy_setopt(curl.get(), CURLOPT_POST, 1L); 88 | curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, payload.c_str()); 89 | curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers); 90 | curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, CurlHelper::WriteCallback); 91 | curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response); 92 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYPEER, 1L); 93 | curl_easy_setopt(curl.get(), CURLOPT_SSL_VERIFYHOST, 2L); 94 | curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, 30L); 95 | 96 | // Perform request 97 | CURLcode res = curl_easy_perform(curl.get()); 98 | 99 | // Clean up 100 | curl_slist_free_all(headers); 101 | 102 | if (res != CURLE_OK) { 103 | throw TranslationError(std::string("CURL request failed: ") + 104 | curl_easy_strerror(res)); 105 | } 106 | 107 | // Check HTTP response code 108 | long response_code; 109 | curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response_code); 110 | 111 | if (response_code != 200) { 112 | throw TranslationError("HTTP error: " + std::to_string(response_code) + 113 | "\nResponse: " + response); 114 | } 115 | 116 | return parseResponse(response); 117 | 118 | } catch (const json::exception &e) { 119 | throw TranslationError(std::string("JSON parsing error: ") + e.what()); 120 | } 121 | } 122 | 123 | std::string OpenAITranslator::parseResponse(const std::string &response_str) 124 | { 125 | try { 126 | json response = json::parse(response_str); 127 | 128 | if (!response.contains("choices") || response["choices"].empty() || 129 | !response["choices"][0].contains("message") || 130 | !response["choices"][0]["message"].contains("content")) { 131 | throw TranslationError("Invalid response format from OpenAI API"); 132 | } 133 | 134 | return response["choices"][0]["message"]["content"].get(); 135 | 136 | } catch (const json::exception &e) { 137 | throw TranslationError(std::string("Failed to parse OpenAI response: ") + e.what()); 138 | } 139 | } 140 | --------------------------------------------------------------------------------