mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-29 12:45:28 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b8d829bba | ||
|
|
ad03b92d2d | ||
|
|
88fc412900 | ||
|
|
c2ec6848aa | ||
|
|
01da2877b0 | ||
|
|
107d77f1b8 | ||
|
|
1030868a75 | ||
|
|
797dc2ceb1 | ||
|
|
b971bfe800 | ||
|
|
f88407b731 | ||
|
|
725e3cacb8 | ||
|
|
c9eed5d997 | ||
|
|
bf682136c3 | ||
|
|
7be2a4f544 | ||
|
|
616664e6f3 | ||
|
|
0caf8628b7 | ||
|
|
463e7caa2c | ||
|
|
0e2315807f | ||
|
|
46bf6c666f | ||
|
|
902be7759d |
1
.github/scripts/.Aptfile
vendored
1
.github/scripts/.Aptfile
vendored
@@ -8,6 +8,7 @@ package 'libcurl4-openssl-dev'
|
||||
package 'libxtst-dev'
|
||||
package 'libxss-dev'
|
||||
package 'libopencv-dev'
|
||||
package 'libopencv-contrib-dev'
|
||||
package 'libtesseract-dev'
|
||||
package 'libproc2-dev'
|
||||
package 'libusb-1.0-0-dev'
|
||||
|
||||
58
.github/scripts/.build-deps.zsh
vendored
58
.github/scripts/.build-deps.zsh
vendored
@@ -284,30 +284,56 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
case ${host_os} {
|
||||
macos)
|
||||
local opencv_dir="${project_root}/deps/opencv"
|
||||
local opencv_build_dir="${opencv_dir}/build_${target##*-}"
|
||||
local opencv_contrib_dir="${project_root}/deps/opencv_contrib"
|
||||
local opencv_install_x86="${opencv_dir}/install_x86_64"
|
||||
local opencv_install_arm="${opencv_dir}/install_arm64"
|
||||
|
||||
local -a opencv_cmake_args=(
|
||||
local -a opencv_cmake_args_common=(
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
-DBUILD_LIST=core,imgproc,objdetect
|
||||
-DCMAKE_OSX_ARCHITECTURES=${${target##*-}//universal/x86_64;arm64}
|
||||
-DBUILD_LIST=core,imgproc,objdetect,xobjdetect,dnn
|
||||
-DOPENCV_EXTRA_MODULES_PATH="${opencv_contrib_dir}/modules"
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DCMAKE_PREFIX_PATH="${advss_dep_path};${_plugin_deps}"
|
||||
-DCMAKE_INSTALL_PREFIX="${advss_dep_path}"
|
||||
-DWITH_KLEIDICV=OFF
|
||||
)
|
||||
|
||||
if [ "${target}" != "macos-x86_64" ]; then
|
||||
opencv_cmake_args+=(-DWITH_IPP=OFF)
|
||||
fi
|
||||
|
||||
pushd ${opencv_dir}
|
||||
log_info "Configure OpenCV ..."
|
||||
cmake -S . -B ${opencv_build_dir} ${opencv_cmake_args}
|
||||
|
||||
log_info "Building OpenCV ..."
|
||||
cmake --build ${opencv_build_dir} --config Release
|
||||
log_info "Configure OpenCV (x86_64) ..."
|
||||
cmake -S . -B build_x86_64 ${opencv_cmake_args_common} \
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_PROJECT_INCLUDE="${SCRIPT_HOME}/opencv-force-processor-x86_64.cmake" \
|
||||
-DCMAKE_INSTALL_PREFIX="${opencv_install_x86}" \
|
||||
-DWITH_IPP=OFF
|
||||
|
||||
log_info "Installing OpenCV ..."
|
||||
cmake --install ${opencv_build_dir} --prefix "${advss_dep_path}" --config Release || true
|
||||
log_info "Building OpenCV (x86_64) ..."
|
||||
cmake --build build_x86_64 --config Release
|
||||
|
||||
log_info "Installing OpenCV (x86_64) ..."
|
||||
cmake --install build_x86_64 --prefix "${opencv_install_x86}" --config Release || true
|
||||
|
||||
log_info "Configure OpenCV (arm64) ..."
|
||||
cmake -S . -B build_arm64 ${opencv_cmake_args_common} \
|
||||
-DCMAKE_OSX_ARCHITECTURES=arm64 \
|
||||
-DCMAKE_INSTALL_PREFIX="${opencv_install_arm}"
|
||||
|
||||
log_info "Building OpenCV (arm64) ..."
|
||||
cmake --build build_arm64 --config Release
|
||||
|
||||
log_info "Installing OpenCV (arm64) ..."
|
||||
cmake --install build_arm64 --prefix "${opencv_install_arm}" --config Release || true
|
||||
|
||||
log_info "Merging OpenCV into universal binaries ..."
|
||||
cp -R "${opencv_install_arm}/." "${advss_dep_path}"
|
||||
for arm_lib in ${opencv_install_arm}/lib/**/*.(dylib|a)(.); do
|
||||
local rel="${arm_lib#${opencv_install_arm}/}"
|
||||
local x86_lib="${opencv_install_x86}/${rel}"
|
||||
if [[ -f "${x86_lib}" ]]; then
|
||||
lipo -create "${x86_lib}" "${arm_lib}" -output "${advss_dep_path}/${rel}"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf "${opencv_install_x86}" "${opencv_install_arm}"
|
||||
popd
|
||||
|
||||
local leptonica_dir="${project_root}/deps/leptonica"
|
||||
@@ -319,7 +345,7 @@ Usage: %B${functrace[1]%:*}%b <option> [<options>]
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=${DEPLOYMENT_TARGET:-10.15}
|
||||
-DSW_BUILD=OFF
|
||||
-DOPENJPEG_SUPPORT=OFF
|
||||
-DLIBWEBP_SUPPORT=OFF
|
||||
-DENABLE_WEBP=OFF
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_GIF=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_JPEG=TRUE
|
||||
-DCMAKE_DISABLE_FIND_PACKAGE_TIFF=TRUE
|
||||
|
||||
2
.github/scripts/.package.zsh
vendored
2
.github/scripts/.package.zsh
vendored
@@ -259,7 +259,7 @@ ${_usage_host:-}"
|
||||
cmake --build build_${target##*-} --config ${config} -t package ${cmake_args}
|
||||
|
||||
# Mark certain deps as optional
|
||||
build-aux/CI/linux/demote-deps.sh ${project_root}/release/*.deb Recommends '(mqtt)|(opencv)|(tesseract)|(usb)|(x11)'
|
||||
build-aux/CI/linux/demote-deps.sh ${project_root}/release/*.deb Recommends '(mqtt)|(opencv)|(tesseract)|(usb)|(x11)|(libobs)'
|
||||
|
||||
if [ ! -e ${project_root}/release/${output_name}.deb ]; then
|
||||
mv ${project_root}/release/*.deb ${project_root}/release/${output_name}.deb
|
||||
|
||||
4
.github/scripts/Build-Deps-Windows.ps1
vendored
4
.github/scripts/Build-Deps-Windows.ps1
vendored
@@ -106,6 +106,7 @@ function Build {
|
||||
$ADVSSDepPath = "$(Resolve-Path -Path ${ProjectRoot}/${OutDirName})"
|
||||
|
||||
$OpenCVPath = "${ProjectRoot}/deps/opencv"
|
||||
$OpenCVContribPath = "${ProjectRoot}/deps/opencv_contrib"
|
||||
$OpenCVBuildPath = "${OpenCVPath}/build"
|
||||
|
||||
Push-Location -Stack BuildOpenCVTemp
|
||||
@@ -115,7 +116,8 @@ function Build {
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_PREFIX_PATH:PATH=${OBSDepPath}"
|
||||
"-DCMAKE_INSTALL_PREFIX:PATH=${ADVSSDepPath}"
|
||||
"-DBUILD_LIST=core,imgproc,objdetect"
|
||||
"-DBUILD_LIST=core,imgproc,objdetect,xobjdetect"
|
||||
"-DOPENCV_EXTRA_MODULES_PATH:PATH=${OpenCVContribPath}/modules"
|
||||
)
|
||||
|
||||
Log-Information "Configuring OpenCV..."
|
||||
|
||||
7
.github/scripts/opencv-force-processor-x86_64.cmake
vendored
Normal file
7
.github/scripts/opencv-force-processor-x86_64.cmake
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Injected via CMAKE_PROJECT_INCLUDE after opencv's project() call. Forces
|
||||
# CMAKE_SYSTEM_PROCESSOR into the cache so that third-party subdirectories
|
||||
# (mlas) that read the cache directly see x86_64 instead of the arm64 host
|
||||
# processor on Apple Silicon CI runners.
|
||||
set(CMAKE_SYSTEM_PROCESSOR
|
||||
x86_64
|
||||
CACHE INTERNAL "" FORCE)
|
||||
21
.github/scripts/utils.zsh/setup_linux
vendored
21
.github/scripts/utils.zsh/setup_linux
vendored
@@ -35,14 +35,21 @@ if (( ! (${skips[(Ie)all]} + ${skips[(Ie)deps]}) )) {
|
||||
sudo apt-get install ${apt_args} gcc-${${target##*-}//_/-}-linux-gnu g++-${${target##*-}//_/-}-linux-gnu
|
||||
}
|
||||
|
||||
sudo add-apt-repository --yes ppa:obsproject/obs-studio
|
||||
sudo apt update
|
||||
local dist_version
|
||||
read -r dist_version <<< "$(source /etc/os-release; print "${VERSION_ID}")"
|
||||
|
||||
sudo apt-get install ${apt_args} \
|
||||
build-essential \
|
||||
libgles2-mesa-dev \
|
||||
libsimde-dev \
|
||||
obs-studio
|
||||
# The OBS PPA may not yet support newer Ubuntu versions.
|
||||
# On those, use the native obs-studio packages and install libobs-dev separately,
|
||||
# as it is not bundled in obs-studio itself on newer Ubuntu releases.
|
||||
local -a obs_packages=(build-essential libgles2-mesa-dev libsimde-dev obs-studio)
|
||||
if is-at-least 26.04 ${dist_version}; then
|
||||
obs_packages+=(libobs-dev)
|
||||
else
|
||||
sudo add-apt-repository --yes ppa:obsproject/obs-studio
|
||||
sudo apt update
|
||||
fi
|
||||
|
||||
sudo apt-get install ${apt_args} ${obs_packages}
|
||||
|
||||
local -a _qt_packages=()
|
||||
|
||||
|
||||
90
.github/workflows/build-project.yaml
vendored
90
.github/workflows/build-project.yaml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
description: "Project name detected by parsing build spec file"
|
||||
value: ${{ jobs.check-event.outputs.pluginName }}
|
||||
env:
|
||||
DEP_DIR: .deps/advss-build-dependencies-5
|
||||
DEP_DIR: .deps/advss-build-dependencies-6
|
||||
jobs:
|
||||
check-event:
|
||||
name: Check GitHub Event Data 🔎
|
||||
@@ -213,6 +213,17 @@ jobs:
|
||||
- name: Set up Homebrew 🍺
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Install Vulkan SDK 🌋
|
||||
shell: bash
|
||||
run: |
|
||||
wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc \
|
||||
| sudo tee /etc/apt/trusted.gpg.d/lunarg.asc
|
||||
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-noble.list \
|
||||
https://packages.lunarg.com/vulkan/lunarg-vulkan-noble.list
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y vulkan-sdk
|
||||
echo "VULKAN_SDK=/usr" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build Plugin 🧱
|
||||
uses: ./.github/actions/build-plugin
|
||||
with:
|
||||
@@ -252,6 +263,77 @@ jobs:
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
|
||||
if-no-files-found: ignore
|
||||
|
||||
ubuntu-26-build:
|
||||
name: Build for Ubuntu 26 🐧
|
||||
runs-on: ubuntu-26.04
|
||||
needs: check-event
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set Up Environment 🔧
|
||||
id: setup
|
||||
run: |
|
||||
: Set Up Environment 🔧
|
||||
if [[ "${RUNNER_DEBUG}" ]]; then set -x; fi
|
||||
|
||||
git_tag="$(git describe --tags)"
|
||||
|
||||
read -r product_name product_version <<< \
|
||||
"$(jq -r '. | {name, version} | join(" ")' buildspec.json)"
|
||||
|
||||
echo "pluginName=${product_name}" >> $GITHUB_OUTPUT
|
||||
echo "pluginVersion=${git_tag}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v5
|
||||
id: ccache-cache
|
||||
with:
|
||||
path: ${{ github.workspace }}/.ccache
|
||||
key: ${{ runner.os }}-ccache-x86_64-${{ needs.check-event.outputs.config }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ccache-x86_64-
|
||||
|
||||
- name: Set up Homebrew 🍺
|
||||
uses: Homebrew/actions/setup-homebrew@main
|
||||
|
||||
- name: Build Plugin 🧱
|
||||
uses: ./.github/actions/build-plugin
|
||||
with:
|
||||
target: x86_64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
target: x86_64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Package Plugin 📀
|
||||
uses: ./.github/actions/package-plugin
|
||||
with:
|
||||
package: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||
target: x86_64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Upload Artifacts 📡
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-26.04-x86_64-${{ needs.check-event.outputs.commitHash }}
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*.*
|
||||
|
||||
- name: Upload debug symbol artifacts 🪲
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ fromJSON(needs.check-event.outputs.package) }}
|
||||
with:
|
||||
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-ubuntu-26.04-x86_64-${{ needs.check-event.outputs.commitHash }}-dbgsym
|
||||
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-x86_64*-dbgsym.ddeb
|
||||
if-no-files-found: ignore
|
||||
|
||||
windows-build:
|
||||
name: Build for Windows 🪟
|
||||
runs-on: windows-2022
|
||||
@@ -287,6 +369,12 @@ jobs:
|
||||
target: x64
|
||||
config: ${{ needs.check-event.outputs.config }}
|
||||
|
||||
- name: Install Vulkan SDK 🌋
|
||||
uses: jakoch/install-vulkan-sdk-action@v1
|
||||
with:
|
||||
install_runtime: false
|
||||
cache: true
|
||||
|
||||
- name: Build Plugin 🧱
|
||||
uses: ./.github/actions/build-plugin
|
||||
with:
|
||||
|
||||
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -34,3 +34,9 @@
|
||||
[submodule "deps/paho.mqtt.cpp"]
|
||||
path = deps/paho.mqtt.cpp
|
||||
url = https://github.com/eclipse-paho/paho.mqtt.cpp.git
|
||||
[submodule "deps/opencv_contrib"]
|
||||
path = deps/opencv_contrib
|
||||
url = https://github.com/opencv/opencv_contrib.git
|
||||
[submodule "deps/whisper.cpp"]
|
||||
path = deps/whisper.cpp
|
||||
url = https://github.com/ggml-org/whisper.cpp.git
|
||||
|
||||
@@ -16,6 +16,14 @@ if(BUILD_OUT_OF_TREE)
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/common/bootstrap.cmake"
|
||||
NO_POLICY_SCOPE)
|
||||
include(compilerconfig)
|
||||
# OPENSSL_USE_STATIC_LIBS here ensures FindOpenSSL picks up the static libs
|
||||
# preventing DLL name collisions with other plugins (e.g.NVIDIA AR SDK).
|
||||
if(OS_WINDOWS)
|
||||
set(OPENSSL_USE_STATIC_LIBS
|
||||
ON
|
||||
CACHE BOOL "Use static OpenSSL" FORCE)
|
||||
include(cmake/windows/wingetssl.cmake)
|
||||
endif()
|
||||
include(defaults)
|
||||
include(helpers)
|
||||
endif()
|
||||
@@ -34,11 +42,6 @@ include(cmake/common/get_git_revision_description.cmake)
|
||||
get_git_head_revision(GIT_REFSPEC GIT_SHA1)
|
||||
git_describe(GIT_TAG)
|
||||
|
||||
# Helper for OpenSSL
|
||||
if(OS_WINDOWS)
|
||||
include(cmake/windows/wingetssl.cmake)
|
||||
endif()
|
||||
|
||||
if(${GIT_TAG} STREQUAL "GIT-NOTFOUND")
|
||||
set(GIT_TAG ${PROJECT_VERSION})
|
||||
endif()
|
||||
@@ -104,6 +107,8 @@ target_sources(
|
||||
lib/macro/macro-action-edit.hpp
|
||||
lib/macro/macro-action-factory.cpp
|
||||
lib/macro/macro-action-factory.hpp
|
||||
lib/macro/macro-action-loop.cpp
|
||||
lib/macro/macro-action-loop.hpp
|
||||
lib/macro/macro-action-macro.cpp
|
||||
lib/macro/macro-action-macro.hpp
|
||||
lib/macro/macro-action-queue.cpp
|
||||
@@ -374,6 +379,9 @@ set_target_properties(${LIB_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden)
|
||||
target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_17)
|
||||
target_compile_features(${LIB_NAME} PUBLIC cxx_std_17)
|
||||
|
||||
target_precompile_headers(${LIB_NAME} PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/lib/pch.hpp")
|
||||
|
||||
target_include_directories(
|
||||
${LIB_NAME} PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/deps/obs-websocket/lib"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/deps/exprtk")
|
||||
@@ -551,3 +559,59 @@ else()
|
||||
set_target_properties_obs(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- Install third-party dependency licenses ---
|
||||
#
|
||||
# Each entry is "display-name:path/to/license/file" relative to deps/. Files are
|
||||
# silently skipped when a submodule is not checked out.
|
||||
set(_dep_license_files
|
||||
"asio:asio/asio/LICENSE_1_0.txt"
|
||||
"cpp-httplib:cpp-httplib/LICENSE"
|
||||
"json:json/LICENSE.MIT"
|
||||
"jsoncons:jsoncons/LICENSE"
|
||||
"leptonica:leptonica/leptonica-license.txt"
|
||||
"libremidi:libremidi/LICENSE.md"
|
||||
"libusb:libusb/COPYING"
|
||||
"opencv:opencv/LICENSE"
|
||||
"openvr:openvr/LICENSE"
|
||||
"paho-mqtt:paho.mqtt.cpp/LICENSE"
|
||||
"tesseract:tesseract/LICENSE"
|
||||
"websocketpp:websocketpp/COPYING"
|
||||
"whisper:whisper.cpp/LICENSE")
|
||||
|
||||
if(DEB_INSTALL)
|
||||
if(NOT DATA_OUT_DIR)
|
||||
set(DATA_OUT_DIR "/share/obs/obs-plugins/${PROJECT_NAME}")
|
||||
endif()
|
||||
set(_license_dest "${CMAKE_INSTALL_PREFIX}/${DATA_OUT_DIR}/licenses")
|
||||
elseif(OS_MACOS)
|
||||
# macOS: license files become bundle Resources/licenses/* entries
|
||||
set(_license_dest "")
|
||||
else()
|
||||
# Windows and non-DEB Linux: alongside the plugin's data directory
|
||||
set(_license_dest "${CMAKE_PROJECT_NAME}/data/licenses")
|
||||
endif()
|
||||
|
||||
foreach(_entry IN LISTS _dep_license_files)
|
||||
string(REGEX MATCH "^([^:]+):(.+)$" _m "${_entry}")
|
||||
set(_dep_name "${CMAKE_MATCH_1}")
|
||||
set(_dep_file "${CMAKE_CURRENT_SOURCE_DIR}/deps/${CMAKE_MATCH_2}")
|
||||
|
||||
if(NOT EXISTS "${_dep_file}")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
if(OS_MACOS)
|
||||
# Copy to binary dir so we can give the file a clean name
|
||||
set(_staged "${CMAKE_CURRENT_BINARY_DIR}/licenses/LICENSE-${_dep_name}")
|
||||
configure_file("${_dep_file}" "${_staged}" COPYONLY)
|
||||
target_sources(${PROJECT_NAME} PRIVATE "${_staged}")
|
||||
set_source_files_properties("${_staged}" PROPERTIES MACOSX_PACKAGE_LOCATION
|
||||
"Resources/licenses")
|
||||
else()
|
||||
install(
|
||||
FILES "${_dep_file}"
|
||||
DESTINATION "${_license_dest}"
|
||||
RENAME "LICENSE-${_dep_name}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
@@ -333,6 +333,8 @@ function(setup_advss_plugin target)
|
||||
PRIVATE "${ADVSS_SOURCE_DIR}/lib" "${ADVSS_SOURCE_DIR}/lib/legacy"
|
||||
"${ADVSS_SOURCE_DIR}/lib/macro" "${ADVSS_SOURCE_DIR}/lib/utils"
|
||||
"${ADVSS_SOURCE_DIR}/lib/variables" "${ADVSS_SOURCE_DIR}/forms")
|
||||
|
||||
target_precompile_headers(${target} PRIVATE "${ADVSS_SOURCE_DIR}/lib/pch.hpp")
|
||||
endfunction()
|
||||
|
||||
function(install_advss_plugin_dependency)
|
||||
|
||||
@@ -22,6 +22,7 @@ set(CPACK_GENERATOR
|
||||
"DEB"
|
||||
CACHE STRING "CPack generator to use")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
set(CPACK_DEBIAN_PACKAGE_DEPENDS "obs-studio")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "${PLUGIN_EMAIL}")
|
||||
set(CPACK_SET_DESTDIR ON)
|
||||
|
||||
|
||||
@@ -40,19 +40,27 @@ if(WIN32 AND (NOT OpenSSL_FOUND))
|
||||
message(STATUS "Looking for OpenSSL built with CRT variant: ${_crt_kind}")
|
||||
|
||||
# Try to find the root and corresponding lib path
|
||||
if(OPENSSL_USE_STATIC_LIBS)
|
||||
set(_crypto_lib_name "libcrypto_static.lib")
|
||||
set(_ssl_lib_name "libssl_static.lib")
|
||||
else()
|
||||
set(_crypto_lib_name "libcrypto.lib")
|
||||
set(_ssl_lib_name "libssl.lib")
|
||||
endif()
|
||||
|
||||
foreach(_root ${_openssl_roots})
|
||||
if(EXISTS "${_root}/include/openssl/ssl.h")
|
||||
foreach(_suffix ${_openssl_lib_suffixes})
|
||||
if(_suffix MATCHES "${_crt_kind}$"
|
||||
AND EXISTS "${_root}/${_suffix}/libcrypto.lib")
|
||||
AND EXISTS "${_root}/${_suffix}/${_crypto_lib_name}")
|
||||
set(OPENSSL_ROOT_DIR
|
||||
"${_root}"
|
||||
CACHE PATH "Path to OpenSSL root")
|
||||
set(OPENSSL_CRYPTO_LIBRARY
|
||||
"${_root}/${_suffix}/libcrypto.lib"
|
||||
"${_root}/${_suffix}/${_crypto_lib_name}"
|
||||
CACHE FILEPATH "OpenSSL crypto lib")
|
||||
set(OPENSSL_SSL_LIBRARY
|
||||
"${_root}/${_suffix}/libssl.lib"
|
||||
"${_root}/${_suffix}/${_ssl_lib_name}"
|
||||
CACHE FILEPATH "OpenSSL ssl lib")
|
||||
set(OPENSSL_INCLUDE_DIR
|
||||
"${_root}/include"
|
||||
|
||||
@@ -206,6 +206,8 @@ AdvSceneSwitcher.macroTab.expandAllGroups="Expand all Groups"
|
||||
AdvSceneSwitcher.macroTab.collapseAllGroups="Collapse all Groups"
|
||||
AdvSceneSwitcher.macroTab.rename="Rename"
|
||||
AdvSceneSwitcher.macroTab.remove="Remove"
|
||||
AdvSceneSwitcher.macroTab.pause="Pause"
|
||||
AdvSceneSwitcher.macroTab.unpause="Unpause"
|
||||
AdvSceneSwitcher.macroTab.export="Export"
|
||||
AdvSceneSwitcher.macroTab.export.info="Paste the string below into the import dialog to import the selected macros:"
|
||||
AdvSceneSwitcher.macroTab.export.usePlainText="Use plain text"
|
||||
@@ -838,6 +840,38 @@ AdvSceneSwitcher.condition.clipboard.condition.isImage="Clipboard contains an im
|
||||
AdvSceneSwitcher.condition.clipboard.condition.isURL="Clipboard contains an URL"
|
||||
AdvSceneSwitcher.condition.clipboard.condition.matches="Clipboard content matches"
|
||||
AdvSceneSwitcher.condition.clipboard.condition.entry="{{conditions}}{{regex}}{{urlInfo}}"
|
||||
AdvSceneSwitcher.condition.speech="Speech Recognition (beta)"
|
||||
AdvSceneSwitcher.condition.speech.condition.any="Any speech is detected for"
|
||||
AdvSceneSwitcher.condition.speech.condition.contains="contains phrase"
|
||||
AdvSceneSwitcher.condition.speech.condition.matches="matches"
|
||||
AdvSceneSwitcher.condition.speech.layout.any="{{conditions}}{{source}}"
|
||||
AdvSceneSwitcher.condition.speech.layout.contains="Transcript of{{source}}{{conditions}}:"
|
||||
AdvSceneSwitcher.condition.speech.layout.matches="Transcript of{{source}}{{conditions}}:"
|
||||
AdvSceneSwitcher.condition.speech.layout.phrase="{{phrase}}{{regex}}"
|
||||
AdvSceneSwitcher.condition.speech.layout.model="Whisper model:{{modelPath}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.model.help="GGML model files can be downloaded from https://huggingface.co/ggerganov/whisper.cpp (e.g. ggml-base.bin).\nLarger models are more accurate but slower."
|
||||
AdvSceneSwitcher.condition.speech.layout.buffer="Audio buffer:{{bufferDuration}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.browse="Browse..."
|
||||
AdvSceneSwitcher.condition.speech.browse.title="Select Whisper model file"
|
||||
AdvSceneSwitcher.condition.speech.browse.filter="GGML model files (*.bin);;All files (*)"
|
||||
AdvSceneSwitcher.condition.speech.buffer.help="Longer buffer durations improve accuracy but increase latency."
|
||||
AdvSceneSwitcher.condition.speech.advanced="Advanced"
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.threads="Threads:{{threads}}"
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.language="Language:{{language}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.language.help="Whisper language code (e.g. 'en', 'de', 'fr') or 'auto' to detect automatically."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.translate="{{translate}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.translate="Translate to English"
|
||||
AdvSceneSwitcher.condition.speech.advanced.translate.help="Translate non-English speech to English before transcribing.\nUseful when the phrase or regex is written in English but the source speaks another language."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.vad="VAD energy threshold:{{vad}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.vad.help="Minimum RMS energy a buffer must have before running inference. Buffers below this level are treated as silence and skipped. Lower values are more sensitive; raise it if inference triggers on background noise."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.suppress="{{suppress}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.suppress="Suppress non-speech tokens"
|
||||
AdvSceneSwitcher.condition.speech.advanced.suppress.help="Remove filler tokens such as [MUSIC] or (applause) that Whisper tends to insert when it detects non-speech sounds."
|
||||
AdvSceneSwitcher.condition.speech.layout.advanced.noContext="{{noContext}}{{help}}"
|
||||
AdvSceneSwitcher.condition.speech.advanced.noContext="No context"
|
||||
AdvSceneSwitcher.condition.speech.advanced.noContext.help="Do not feed the previous transcription back as a prompt for the next buffer.\nPrevents repetition across buffer boundaries at the cost of slightly reduced coherence."
|
||||
AdvSceneSwitcher.condition.speech.advanced.listenWhenMuted="Listen when source is muted"
|
||||
AdvSceneSwitcher.condition.speech.advanced.useGpu="Use GPU"
|
||||
AdvSceneSwitcher.condition.folder="Folder watch"
|
||||
AdvSceneSwitcher.condition.folder.tooltip="This condition type will allow you to monitor the contents of a folder.\nNote that the monitoring will *not* recursively scan for changes in sub directories within directories of the selected folder!\nNote that if there are several changes during a short period of time, some of the changes might not emit this signal.\nHowever, the last change in the sequence of changes always will."
|
||||
AdvSceneSwitcher.condition.folder.condition.any="Any change happened"
|
||||
@@ -994,7 +1028,7 @@ AdvSceneSwitcher.action.filter.refresh.tooltip="Repopulate the filter settings s
|
||||
AdvSceneSwitcher.action.filter.entry="On{{sources}}{{actions}}{{filters}}{{refresh}}{{settingsButtons}}"
|
||||
AdvSceneSwitcher.action.filter.entry.settings="{{settings}}{{settingsInputMethod}}{{settingValue}}{{tempVar}}"
|
||||
AdvSceneSwitcher.action.filter.getSettings="Get current settings"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualManual="Set to fixed value"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualManual="Set to value"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualListEntryManual="Set to list entry"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.individualTempvar="Set to macro property"
|
||||
AdvSceneSwitcher.action.filter.inputMethod.json="Set setting JSON string"
|
||||
@@ -1030,7 +1064,7 @@ AdvSceneSwitcher.action.source.deinterlaceMode.yadif="Yadif"
|
||||
AdvSceneSwitcher.action.source.deinterlaceMode.yadif2x="Yadif 2x"
|
||||
AdvSceneSwitcher.action.source.deinterlaceOrder.topFieldFirst="Top Field First"
|
||||
AdvSceneSwitcher.action.source.deinterlaceOrder.bottomFieldFirst="Bottom Field First"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualManual="Set to fixed value"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualManual="Set to value"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualListEntryManual="Set to list entry"
|
||||
AdvSceneSwitcher.action.source.inputMethod.individualTempvar="Set to macro property"
|
||||
AdvSceneSwitcher.action.source.inputMethod.json="Set setting JSON string"
|
||||
@@ -1122,6 +1156,12 @@ AdvSceneSwitcher.action.macro.type.nestedMacro="Nested macro"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.index="at index"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.label="with label"
|
||||
AdvSceneSwitcher.action.macro.actionSelectionType.id="of action type"
|
||||
AdvSceneSwitcher.action.loop="Loop"
|
||||
AdvSceneSwitcher.action.loop.maxIterations="Max iterations"
|
||||
AdvSceneSwitcher.action.loop.conditionHelp="Define the loop condition. The actions below will be repeated while these conditions are met.\n\nClick the plus button below to add a new condition."
|
||||
AdvSceneSwitcher.action.loop.actionHelp="Define the actions to perform each iteration.\n\nClick the plus button below to add a new action."
|
||||
AdvSceneSwitcher.tempVar.loop.count="Iteration count"
|
||||
AdvSceneSwitcher.tempVar.loop.count.description="The total number of iterations completed."
|
||||
AdvSceneSwitcher.action.macro.type.nestedMacro.conditionHelp="This section allows you to define macro conditions.\n\nClick the plus button below to add a new condition."
|
||||
AdvSceneSwitcher.action.macro.type.nestedMacro.actionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are met.\n\nClick the plus button below to add a new action."
|
||||
AdvSceneSwitcher.action.macro.type.nestedMacro.elseActionHelp="This section allows you to define macro actions.\nThe actions in this section will be performed when the conditions are *not* met.\n\nClick the plus button below to add a new action."
|
||||
@@ -1622,6 +1662,8 @@ AdvSceneSwitcher.actionQueues.invalid="Invalid action queue selection"
|
||||
AdvSceneSwitcher.actionQueues.name="Name:"
|
||||
AdvSceneSwitcher.actionQueues.runOnStartup="Run action queue when starting the plugin"
|
||||
AdvSceneSwitcher.actionQueues.resolveVariablesOnAdd="Resolve variables when action is inserted into the queue"
|
||||
AdvSceneSwitcher.actionQueues.cloneVariableContext="Clone variable context when action is inserted into the queue"
|
||||
AdvSceneSwitcher.actionQueues.cloneVariableContext.tooltip="Captures the current values of all variables when the action is added to the queue.\nDuring execution, actions read from and write to this snapshot instead of the global variables.\nUnlike \"Resolve variables\", changes made by one action in the queue are visible to subsequent actions."
|
||||
AdvSceneSwitcher.actionQueues.running="Queue is running"
|
||||
AdvSceneSwitcher.actionQueues.stopped="Queue is stopped"
|
||||
AdvSceneSwitcher.actionQueues.start="Start action queue"
|
||||
@@ -2450,6 +2492,9 @@ AdvSceneSwitcher.tempVar.streaming.serviceName.description="The name of the stre
|
||||
AdvSceneSwitcher.tempVar.clipboard.text="Clipboard text"
|
||||
AdvSceneSwitcher.tempVar.clipboard.text.description="The text contained in the clipboard.\nWill be empty if the clipboard does not contain text."
|
||||
|
||||
AdvSceneSwitcher.tempVar.speech.speech="Transcribed speech"
|
||||
AdvSceneSwitcher.tempVar.speech.speech.description="The text transcribed from the last audio buffer. Only populated when the condition matched."
|
||||
|
||||
AdvSceneSwitcher.tempVar.file.content="File content"
|
||||
AdvSceneSwitcher.tempVar.file.date="File modification date"
|
||||
AdvSceneSwitcher.tempVar.file.basename="File basename"
|
||||
|
||||
BIN
data/res/speech/ggml-tiny-q8_0.bin
Normal file
BIN
data/res/speech/ggml-tiny-q8_0.bin
Normal file
Binary file not shown.
2
deps/opencv
vendored
2
deps/opencv
vendored
Submodule deps/opencv updated: 71d3237a09...40738fb16c
1
deps/opencv_contrib
vendored
Submodule
1
deps/opencv_contrib
vendored
Submodule
Submodule deps/opencv_contrib added at 755e50675d
1
deps/whisper.cpp
vendored
Submodule
1
deps/whisper.cpp
vendored
Submodule
Submodule deps/whisper.cpp added at 306c88f4d1
@@ -1,7 +1,4 @@
|
||||
#pragma once
|
||||
#include "macro-segment-list.hpp"
|
||||
#include "condition-logic.hpp"
|
||||
#include "log-helper.hpp"
|
||||
|
||||
#include <ui_advanced-scene-switcher.h>
|
||||
|
||||
@@ -117,6 +114,8 @@ public slots:
|
||||
void RenameSelectedMacro();
|
||||
void ExportMacros() const;
|
||||
void ImportMacros();
|
||||
void PauseSelectedMacros();
|
||||
void UnpauseSelectedMacros();
|
||||
void HighlightOnChange() const;
|
||||
void on_macroSettings_clicked();
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
#include "filter-combo-box.hpp"
|
||||
#include "first-run-wizard.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-search.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "source-helpers.hpp"
|
||||
#include "splitter-helpers.hpp"
|
||||
@@ -18,6 +19,7 @@
|
||||
#include "version.h"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QMessageBox>
|
||||
|
||||
196
lib/macro/macro-action-loop.cpp
Normal file
196
lib/macro/macro-action-loop.cpp
Normal file
@@ -0,0 +1,196 @@
|
||||
#include "macro-action-loop.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
const std::string MacroActionLoop::id = "loop";
|
||||
|
||||
bool MacroActionLoop::_registered = MacroActionFactory::Register(
|
||||
MacroActionLoop::id,
|
||||
{MacroActionLoop::Create, MacroActionLoopEdit::Create,
|
||||
"AdvSceneSwitcher.action.loop"});
|
||||
|
||||
bool MacroActionLoop::PerformAction()
|
||||
{
|
||||
int iterations = 0;
|
||||
const int maxIterations = _maxIterations;
|
||||
Macro *parentMacro = GetMacro();
|
||||
|
||||
if (maxIterations <= 0) {
|
||||
blog(LOG_WARNING,
|
||||
"loop action has invalid max iterations value of %d",
|
||||
maxIterations);
|
||||
return true;
|
||||
}
|
||||
|
||||
while (_loopMacro->CheckConditions()) {
|
||||
if (iterations >= maxIterations) {
|
||||
blog(LOG_WARNING,
|
||||
"loop action reached iteration limit of %d",
|
||||
maxIterations);
|
||||
break;
|
||||
}
|
||||
ablog(LOG_INFO, "loop iteration %d", iterations);
|
||||
if (!_loopMacro->PerformActions(true, false, true)) {
|
||||
break;
|
||||
}
|
||||
++iterations;
|
||||
if (parentMacro && parentMacro->GetStop()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ablog(LOG_INFO, "loop completed after %d iteration(s)", iterations);
|
||||
SetTempVarValue("count", std::to_string(iterations));
|
||||
return true;
|
||||
}
|
||||
|
||||
void MacroActionLoop::LogAction() const
|
||||
{
|
||||
ablog(LOG_INFO, "running loop (max %d iterations)",
|
||||
_maxIterations.GetValue());
|
||||
}
|
||||
|
||||
bool MacroActionLoop::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroAction::Save(obj);
|
||||
_maxIterations.Save(obj, "maxIterations");
|
||||
OBSDataAutoRelease loopMacroData = obs_data_create();
|
||||
_loopMacro->Save(loopMacroData);
|
||||
obs_data_set_obj(obj, "loopMacro", loopMacroData);
|
||||
obs_data_set_int(obj, "customWidgetHeight", _customWidgetHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionLoop::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroAction::Load(obj);
|
||||
_maxIterations.Load(obj, "maxIterations");
|
||||
if (obs_data_has_user_value(obj, "loopMacro")) {
|
||||
OBSDataAutoRelease loopMacroData =
|
||||
obs_data_get_obj(obj, "loopMacro");
|
||||
_loopMacro = std::make_shared<Macro>();
|
||||
_loopMacro->Load(loopMacroData);
|
||||
}
|
||||
_customWidgetHeight = obs_data_get_int(obj, "customWidgetHeight");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroActionLoop::PostLoad()
|
||||
{
|
||||
MacroAction::PostLoad();
|
||||
_loopMacro->PostLoad();
|
||||
_loopMacro->SetActionTriggerMode(Macro::ActionTriggerMode::ALWAYS);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionLoop::Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroActionLoop>(m);
|
||||
}
|
||||
|
||||
std::shared_ptr<MacroAction> MacroActionLoop::Copy() const
|
||||
{
|
||||
auto copy = std::make_shared<MacroActionLoop>(*this);
|
||||
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
_loopMacro->Save(data);
|
||||
|
||||
copy->_loopMacro = std::make_shared<Macro>();
|
||||
copy->_loopMacro->Load(data);
|
||||
copy->_loopMacro->PostLoad();
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
void MacroActionLoop::ResolveVariablesToFixedValues()
|
||||
{
|
||||
_maxIterations.ResolveVariables();
|
||||
for (auto &action : _loopMacro->Actions()) {
|
||||
action->ResolveVariablesToFixedValues();
|
||||
}
|
||||
}
|
||||
|
||||
void MacroActionLoop::SetupTempVars()
|
||||
{
|
||||
MacroAction::SetupTempVars();
|
||||
AddTempvar("count",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.loop.count"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.loop.count.description"));
|
||||
}
|
||||
|
||||
MacroActionLoopEdit::MacroActionLoopEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroActionLoop> entryData)
|
||||
: ResizableWidget(parent),
|
||||
_macroEdit(new MacroEdit(
|
||||
this, QStringList()
|
||||
<< "AdvSceneSwitcher.action.loop.conditionHelp"
|
||||
<< "AdvSceneSwitcher.action.loop.actionHelp"
|
||||
<< "")),
|
||||
_maxIterations(new VariableSpinBox())
|
||||
{
|
||||
_maxIterations->setMinimum(1);
|
||||
_maxIterations->setMaximum(10000000);
|
||||
|
||||
QWidget::connect(
|
||||
_maxIterations,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
|
||||
this, SLOT(MaxIterationsChanged(const NumberVariable<int> &)));
|
||||
|
||||
auto controlsLayout = new QHBoxLayout();
|
||||
controlsLayout->addWidget(new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.action.loop.maxIterations")));
|
||||
controlsLayout->addWidget(_maxIterations);
|
||||
controlsLayout->addStretch();
|
||||
|
||||
auto layout = new QVBoxLayout();
|
||||
layout->addLayout(controlsLayout);
|
||||
layout->addWidget(_macroEdit);
|
||||
setLayout(layout);
|
||||
|
||||
_macroEdit->HideElseSection();
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
MacroActionLoopEdit::~MacroActionLoopEdit()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
_entryData->_customWidgetHeight = GetCustomHeight();
|
||||
_macroEdit->SetMacro({});
|
||||
}
|
||||
|
||||
void MacroActionLoopEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_maxIterations->SetValue(_entryData->_maxIterations);
|
||||
_macroEdit->SetMacro(_entryData->_loopMacro);
|
||||
|
||||
if (_macroEdit->IsEmpty()) {
|
||||
_macroEdit->ShowAllMacroSections();
|
||||
_entryData->_customWidgetHeight = 600;
|
||||
}
|
||||
SetResizingEnabled(true);
|
||||
SetCustomHeight(_entryData->_customWidgetHeight);
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void MacroActionLoopEdit::MaxIterationsChanged(const NumberVariable<int> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_maxIterations = value;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
64
lib/macro/macro-action-loop.hpp
Normal file
64
lib/macro/macro-action-loop.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-edit.hpp"
|
||||
#include "resizable-widget.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroActionLoop : public MacroAction {
|
||||
public:
|
||||
MacroActionLoop(Macro *m) : MacroAction(m) {}
|
||||
bool PerformAction();
|
||||
void LogAction() const;
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
bool PostLoad();
|
||||
std::string GetId() const { return id; }
|
||||
static std::shared_ptr<MacroAction> Create(Macro *m);
|
||||
std::shared_ptr<MacroAction> Copy() const;
|
||||
void ResolveVariablesToFixedValues();
|
||||
|
||||
std::shared_ptr<Macro> _loopMacro = std::make_shared<Macro>();
|
||||
IntVariable _maxIterations = 100;
|
||||
int _customWidgetHeight = 0;
|
||||
|
||||
private:
|
||||
void SetupTempVars();
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroActionLoopEdit : public ResizableWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroActionLoopEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroActionLoop> entryData = nullptr);
|
||||
~MacroActionLoopEdit();
|
||||
void UpdateEntryData();
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroAction> action)
|
||||
{
|
||||
return new MacroActionLoopEdit(
|
||||
parent,
|
||||
std::dynamic_pointer_cast<MacroActionLoop>(action));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void MaxIterationsChanged(const NumberVariable<int> &value);
|
||||
|
||||
private:
|
||||
MacroEdit *_macroEdit;
|
||||
VariableSpinBox *_maxIterations;
|
||||
|
||||
std::shared_ptr<MacroActionLoop> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "macro-action-macro.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
#include "help-icon.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#include "macro-condition-edit.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "condition-logic.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "macro-condition-macro.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-ref.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "macro-condition.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
MacroCondition::MacroCondition(Macro *m, bool supportsVariableValue)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "macro-dock-settings.hpp"
|
||||
#include "macro-dock.hpp"
|
||||
|
||||
#include "macro-dock-window.hpp"
|
||||
#include "macro-dock.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "variable-string.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <util/platform.h>
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
#include "macro-edit.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
#include "cursor-shape-changer.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-action-macro.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-segment-copy-paste.hpp"
|
||||
#include "macro-segment-list.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "math-helpers.hpp"
|
||||
#include "name-dialog.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "splitter-helpers.hpp"
|
||||
#include "tab-helpers.hpp"
|
||||
@@ -1850,6 +1853,13 @@ bool MacroEdit::IsEmpty() const
|
||||
ui->elseActionsList->IsEmpty();
|
||||
}
|
||||
|
||||
void MacroEdit::HideElseSection() const
|
||||
{
|
||||
ui->toggleElseActions->setVisible(false);
|
||||
ui->macroElseActions->setMaximumHeight(0);
|
||||
ui->macroElseActionSplitter->handle(1)->setCursor(Qt::ArrowCursor);
|
||||
}
|
||||
|
||||
void MacroEdit::ShowAllMacroSections()
|
||||
{
|
||||
if (!ElseSectionIsVisible()) {
|
||||
|
||||
@@ -34,6 +34,7 @@ public:
|
||||
void PasteMacroSegment();
|
||||
bool IsEmpty() const;
|
||||
void ShowAllMacroSections();
|
||||
void HideElseSection() const;
|
||||
|
||||
private slots:
|
||||
void on_conditionAdd_clicked();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "macro-ref.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "macro-segment-copy-paste.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "condition-logic.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include <QShortcut>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "macro-segment.hpp"
|
||||
|
||||
#include "macro.hpp"
|
||||
#include "mouse-wheel-guard.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
@@ -8,10 +9,13 @@
|
||||
|
||||
#include <QApplication>
|
||||
#include <QEvent>
|
||||
#include <QFrame>
|
||||
#include <QGraphicsOpacityEffect>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QPushButton>
|
||||
#include <QScrollBar>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
|
||||
@@ -7,15 +7,17 @@
|
||||
#include "sync-helpers.hpp"
|
||||
#include "temp-variable.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QFrame>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QTimer>
|
||||
#include <obs-data.h>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QWidget>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QFrame;
|
||||
class QLabel;
|
||||
class QVBoxLayout;
|
||||
|
||||
namespace advss {
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#include "macro-selection.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-ref.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
#include "macro-action-edit.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "macro-export-import-dialog.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-search.hpp"
|
||||
#include "macro-settings.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro-tree.hpp"
|
||||
#include "macro.hpp"
|
||||
@@ -17,6 +18,7 @@
|
||||
#include "version.h"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QColor>
|
||||
#include <QMenu>
|
||||
|
||||
@@ -743,6 +745,17 @@ void AdvSceneSwitcher::ShowMacroContextMenu(const QPoint &pos)
|
||||
remove->setDisabled(ui->macros->SelectionEmpty());
|
||||
menu.addSeparator();
|
||||
|
||||
auto pause = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.pause"), this,
|
||||
&AdvSceneSwitcher::PauseSelectedMacros);
|
||||
pause->setDisabled(ui->macros->SelectionEmpty());
|
||||
|
||||
auto unpause = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.unpause"), this,
|
||||
&AdvSceneSwitcher::UnpauseSelectedMacros);
|
||||
unpause->setDisabled(ui->macros->SelectionEmpty());
|
||||
menu.addSeparator();
|
||||
|
||||
auto group = menu.addAction(
|
||||
obs_module_text("AdvSceneSwitcher.macroTab.group"), ui->macros,
|
||||
&MacroTree::GroupSelectedItems);
|
||||
@@ -803,6 +816,48 @@ void AdvSceneSwitcher::CopyMacro()
|
||||
MacroSignalManager::Instance()->Add(QString::fromStdString(name));
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::PauseSelectedMacros()
|
||||
{
|
||||
auto selectedMacros = GetSelectedMacros();
|
||||
if (selectedMacros.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
for (const auto ¯o : selectedMacros) {
|
||||
if (macro->IsGroup()) {
|
||||
for (const auto &subitem :
|
||||
GetGroupMacroEntries(macro.get())) {
|
||||
subitem->SetPaused(true);
|
||||
}
|
||||
} else {
|
||||
macro->SetPaused(true);
|
||||
}
|
||||
}
|
||||
ui->macros->UpdateRunningStates();
|
||||
}
|
||||
|
||||
void AdvSceneSwitcher::UnpauseSelectedMacros()
|
||||
{
|
||||
auto selectedMacros = GetSelectedMacros();
|
||||
if (selectedMacros.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto lock = LockContext();
|
||||
for (const auto ¯o : selectedMacros) {
|
||||
if (macro->IsGroup()) {
|
||||
for (const auto &subitem :
|
||||
GetGroupMacroEntries(macro.get())) {
|
||||
subitem->SetPaused(false);
|
||||
}
|
||||
} else {
|
||||
macro->SetPaused(false);
|
||||
}
|
||||
}
|
||||
ui->macros->UpdateRunningStates();
|
||||
}
|
||||
|
||||
bool MacroTabIsInFocus()
|
||||
{
|
||||
return AdvSceneSwitcher::window &&
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
#include "macro-tree.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-search.hpp"
|
||||
#include "macro-signals.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "path-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <obs.h>
|
||||
#include <string>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QSpacerItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
#include <QSpacerItem>
|
||||
#include <QStylePainter>
|
||||
#include <QToolTip>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <string>
|
||||
|
||||
Q_DECLARE_METATYPE(std::shared_ptr<advss::Macro>);
|
||||
|
||||
@@ -977,6 +981,17 @@ void MacroTree::UpdateWidgets(bool force)
|
||||
}
|
||||
}
|
||||
|
||||
void MacroTree::UpdateRunningStates()
|
||||
{
|
||||
MacroTreeModel *mtm = GetModel();
|
||||
for (int i = 0; i < (int)mtm->_macros.size(); i++) {
|
||||
auto widget = GetItemWidget(i);
|
||||
if (widget) {
|
||||
widget->UpdateRunning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline void MoveItem(std::deque<std::shared_ptr<Macro>> &items,
|
||||
std::shared_ptr<Macro> &item, int to)
|
||||
{
|
||||
|
||||
@@ -145,6 +145,7 @@ public:
|
||||
void ExpandGroup(std::shared_ptr<Macro> item) const;
|
||||
void CollapseGroup(std::shared_ptr<Macro> item) const;
|
||||
void RefreshFilter();
|
||||
void UpdateRunningStates();
|
||||
|
||||
public slots:
|
||||
void GroupSelectedItems();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "condition-logic.hpp"
|
||||
#include "macro-action-factory.hpp"
|
||||
#include "macro-condition-factory.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
@@ -7,12 +9,14 @@
|
||||
#include "splitter-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QAction>
|
||||
#include <QMainWindow>
|
||||
|
||||
#include <chrono>
|
||||
#include <limits>
|
||||
#undef max
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QAction>
|
||||
#include <QMainWindow>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "macro-action.hpp"
|
||||
#include "macro-condition.hpp"
|
||||
#include "macro-dock-settings.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-input.hpp"
|
||||
#include "macro-ref.hpp"
|
||||
#include "variable-string.hpp"
|
||||
#include "temp-variable.hpp"
|
||||
|
||||
#include <future>
|
||||
#include <string>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <thread>
|
||||
#include <obs.hpp>
|
||||
#include <obs-module-helper.hpp>
|
||||
|
||||
#include <QList>
|
||||
|
||||
#include <deque>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -225,12 +224,7 @@ private:
|
||||
MacroDockSettings _dockSettings;
|
||||
};
|
||||
|
||||
void LoadMacros(obs_data_t *obj);
|
||||
void SaveMacros(obs_data_t *obj);
|
||||
bool CheckMacros();
|
||||
bool RunMacros();
|
||||
void WaitForAllMacros();
|
||||
void InvalidateMacroTempVarValues();
|
||||
std::shared_ptr<Macro> GetMacroWithInvalidConditionInterval();
|
||||
|
||||
} // namespace advss
|
||||
|
||||
40
lib/pch.hpp
Normal file
40
lib/pch.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
// OBS
|
||||
#include <obs-data.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <obs.hpp>
|
||||
|
||||
// Qt
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QList>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
// Standard library
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static std::deque<std::shared_ptr<Item>> queues;
|
||||
@@ -42,6 +45,7 @@ void ActionQueue::Save(obs_data_t *obj) const
|
||||
obs_data_set_string(obj, "name", _name.c_str());
|
||||
obs_data_set_bool(obj, "runOnStartup", _runOnStartup);
|
||||
obs_data_set_bool(obj, "resolveVariablesOnAdd", _resolveVariablesOnAdd);
|
||||
obs_data_set_bool(obj, "cloneVariableContext", _cloneVariableContext);
|
||||
}
|
||||
|
||||
void ActionQueue::Load(obs_data_t *obj)
|
||||
@@ -51,6 +55,7 @@ void ActionQueue::Load(obs_data_t *obj)
|
||||
_runOnStartup = obs_data_get_bool(obj, "runOnStartup");
|
||||
_resolveVariablesOnAdd =
|
||||
obs_data_get_bool(obj, "resolveVariablesOnAdd");
|
||||
_cloneVariableContext = obs_data_get_bool(obj, "cloneVariableContext");
|
||||
|
||||
if (_runOnStartup) {
|
||||
Start();
|
||||
@@ -111,9 +116,17 @@ void ActionQueue::Add(const std::shared_ptr<MacroAction> &action)
|
||||
copy->PostLoad();
|
||||
RunAndClearPostLoadSteps();
|
||||
copy->ResolveVariablesToFixedValues();
|
||||
_actions.emplace_back(copy);
|
||||
_actions.push_back({copy, {}});
|
||||
} else if (_cloneVariableContext) {
|
||||
auto copy = action->Copy();
|
||||
OBSDataAutoRelease data = obs_data_create();
|
||||
action->Save(data);
|
||||
copy->Load(data);
|
||||
copy->PostLoad();
|
||||
RunAndClearPostLoadSteps();
|
||||
_actions.push_back({copy, CreateVariableContext()});
|
||||
} else {
|
||||
_actions.emplace_back(action);
|
||||
_actions.push_back({action, {}});
|
||||
}
|
||||
_cv.notify_all();
|
||||
}
|
||||
@@ -140,7 +153,7 @@ size_t ActionQueue::Size()
|
||||
|
||||
void ActionQueue::RunActions()
|
||||
{
|
||||
std::shared_ptr<MacroAction> action;
|
||||
QueueEntry entry;
|
||||
while (true) {
|
||||
{ // Grab next action to run
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
@@ -153,20 +166,25 @@ void ActionQueue::RunActions()
|
||||
if (_stop) {
|
||||
return;
|
||||
}
|
||||
action = _actions.front();
|
||||
entry = _actions.front();
|
||||
_actions.pop_front();
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
if (!entry.action) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ActionLoggingEnabled()) {
|
||||
blog(LOG_INFO, "Performing action '%s' in queue '%s'",
|
||||
action->GetId().c_str(), _name.c_str());
|
||||
action->LogAction();
|
||||
entry.action->GetId().c_str(), _name.c_str());
|
||||
entry.action->LogAction();
|
||||
}
|
||||
action->PerformAction();
|
||||
|
||||
if (entry.context) {
|
||||
SetActiveVariableContext(&*entry.context);
|
||||
}
|
||||
entry.action->PerformAction();
|
||||
SetActiveVariableContext(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +202,7 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
|
||||
obs_module_text("AdvSceneSwitcher.actionQueues.clear"))),
|
||||
_runOnStartup(new QCheckBox()),
|
||||
_resolveVariablesOnAdd(new QCheckBox()),
|
||||
_cloneVariableContext(new QCheckBox()),
|
||||
_queue(settings)
|
||||
{
|
||||
QWidget::connect(_startStopToggle, SIGNAL(clicked()), this,
|
||||
@@ -192,6 +211,7 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
|
||||
|
||||
_runOnStartup->setChecked(settings._runOnStartup);
|
||||
_resolveVariablesOnAdd->setChecked(settings._resolveVariablesOnAdd);
|
||||
_cloneVariableContext->setChecked(settings._cloneVariableContext);
|
||||
UpdateLabels();
|
||||
|
||||
auto layout = new QGridLayout();
|
||||
@@ -221,6 +241,14 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
|
||||
_resolveVariablesOnAdd->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.actionQueues.resolveVariablesOnAdd"));
|
||||
++row;
|
||||
layout->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.actionQueues.cloneVariableContext")),
|
||||
row, 0);
|
||||
layout->addWidget(_cloneVariableContext, row, 1);
|
||||
_cloneVariableContext->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.actionQueues.cloneVariableContext.tooltip"));
|
||||
++row;
|
||||
layout->addWidget(_queueRunStatus, row, 0);
|
||||
layout->addWidget(_startStopToggle, row, 1);
|
||||
++row;
|
||||
@@ -250,6 +278,8 @@ bool ActionQueueSettingsDialog::AskForSettings(QWidget *parent,
|
||||
settings._runOnStartup = dialog._runOnStartup->isChecked();
|
||||
settings._resolveVariablesOnAdd =
|
||||
dialog._resolveVariablesOnAdd->isChecked();
|
||||
settings._cloneVariableContext =
|
||||
dialog._cloneVariableContext->isChecked();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
#include "item-selection-helpers.hpp"
|
||||
#include "macro-action.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
#include <obs-data.h>
|
||||
#include <optional>
|
||||
#include <QCheckBox>
|
||||
#include <thread>
|
||||
|
||||
@@ -17,6 +19,11 @@ class ActionQueueSettingsDialog;
|
||||
class ActionQueue : public Item {
|
||||
using TimePoint = std::chrono::high_resolution_clock::time_point;
|
||||
|
||||
struct QueueEntry {
|
||||
std::shared_ptr<MacroAction> action;
|
||||
std::optional<VariableContext> context;
|
||||
};
|
||||
|
||||
public:
|
||||
ActionQueue();
|
||||
~ActionQueue();
|
||||
@@ -43,11 +50,12 @@ private:
|
||||
|
||||
bool _runOnStartup = true;
|
||||
bool _resolveVariablesOnAdd = true;
|
||||
bool _cloneVariableContext = false;
|
||||
std::atomic_bool _stop = {true};
|
||||
std::mutex _mutex;
|
||||
std::condition_variable _cv;
|
||||
std::thread _thread;
|
||||
std::deque<std::shared_ptr<MacroAction>> _actions;
|
||||
std::deque<QueueEntry> _actions;
|
||||
TimePoint _lastEmpty;
|
||||
|
||||
friend ActionQueueSelection;
|
||||
@@ -73,6 +81,7 @@ private:
|
||||
QPushButton *_clear;
|
||||
QCheckBox *_runOnStartup;
|
||||
QCheckBox *_resolveVariablesOnAdd;
|
||||
QCheckBox *_cloneVariableContext;
|
||||
|
||||
ActionQueue &_queue;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "priority-helper.hpp"
|
||||
#include "switcher-data.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "scene-group.hpp"
|
||||
#include "switch-audio.hpp"
|
||||
#include "switch-executable.hpp"
|
||||
@@ -10,15 +12,16 @@
|
||||
#include "switch-pause.hpp"
|
||||
#include "switch-random.hpp"
|
||||
#include "switch-screen-region.hpp"
|
||||
#include "switch-sequence.hpp"
|
||||
#include "switch-time.hpp"
|
||||
#include "switch-transitions.hpp"
|
||||
#include "switch-window.hpp"
|
||||
#include "switch-sequence.hpp"
|
||||
#include "switch-video.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "switch-window.hpp"
|
||||
#include "switcher-data.hpp"
|
||||
|
||||
#include <QThread>
|
||||
|
||||
#include <algorithm>
|
||||
#include <QThread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <export-symbol-helper.hpp>
|
||||
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QParallelAnimationGroup>
|
||||
@@ -8,7 +10,7 @@
|
||||
|
||||
namespace advss {
|
||||
|
||||
class Section : public QWidget {
|
||||
class ADVSS_EXPORT Section : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
#include <QStandardItemModel>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -42,34 +43,46 @@ static void hasFilterEnum(obs_source_t *, obs_source_t *filter, void *ptr)
|
||||
|
||||
QStringList GetSourcesWithFilterNames()
|
||||
{
|
||||
struct EnumParam {
|
||||
QStringList list;
|
||||
// Some items appear in both obs_enum_sources and
|
||||
// obs_enum_scenes, so track pointers to avoid adding them
|
||||
// twice.
|
||||
std::unordered_set<obs_source_t *> seen;
|
||||
};
|
||||
|
||||
static auto enumSourcesWithFilters = [](void *param,
|
||||
obs_source_t *source) {
|
||||
if (!source) {
|
||||
return true;
|
||||
}
|
||||
QStringList *list = reinterpret_cast<QStringList *>(param);
|
||||
auto *ep = reinterpret_cast<EnumParam *>(param);
|
||||
const auto [_, inserted] = ep->seen.insert(source);
|
||||
if (!inserted) {
|
||||
return true;
|
||||
}
|
||||
bool hasFilter = false;
|
||||
obs_source_enum_filters(source, hasFilterEnum, &hasFilter);
|
||||
if (hasFilter) {
|
||||
*list << obs_source_get_name(source);
|
||||
ep->list << obs_source_get_name(source);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
QStringList list;
|
||||
obs_enum_sources(enumSourcesWithFilters, &list);
|
||||
EnumParam ep;
|
||||
obs_enum_sources(enumSourcesWithFilters, &ep);
|
||||
|
||||
#if LIBOBS_API_VER < MAKE_SEMANTIC_VERSION(31, 1, 0)
|
||||
obs_enum_scenes(enumSourcesWithFilters, &list);
|
||||
obs_enum_scenes(enumSourcesWithFilters, &ep);
|
||||
#else
|
||||
static const auto enumCanvases = [](void *listPtr,
|
||||
static const auto enumCanvases = [](void *param,
|
||||
obs_canvas_t *canvas) -> bool {
|
||||
obs_canvas_enum_scenes(canvas, enumSourcesWithFilters, listPtr);
|
||||
obs_canvas_enum_scenes(canvas, enumSourcesWithFilters, param);
|
||||
return true;
|
||||
};
|
||||
obs_enum_canvases(enumCanvases, &list);
|
||||
obs_enum_canvases(enumCanvases, &ep);
|
||||
#endif
|
||||
return list;
|
||||
return ep.list;
|
||||
}
|
||||
|
||||
QStringList GetMediaSourceNames()
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "temp-variable.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "macro.hpp"
|
||||
|
||||
#include "macro-action-macro.hpp"
|
||||
#include "macro-edit.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "macro-segment.hpp"
|
||||
#include "macro.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "sync-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include "advanced-scene-switcher.hpp"
|
||||
#include "non-modal-dialog.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QComboBox>
|
||||
#include <QCursor>
|
||||
#include <QGraphicsColorizeEffect>
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMainWindow>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QScrollBar>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "export-symbol-helper.hpp"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QColor>
|
||||
#include <QComboBox>
|
||||
#include <QIcon>
|
||||
#include <QListWidget>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include <string>
|
||||
|
||||
class QAbstractButton;
|
||||
class QComboBox;
|
||||
class QListWidget;
|
||||
class QWidget;
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Returns QObject* to QPropertyAnimation object
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "utility.hpp"
|
||||
|
||||
#include <QCursor>
|
||||
#include <QListWidget>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "export-symbol-helper.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <QListWidget>
|
||||
#include <QMetaObject>
|
||||
#include <QPushButton>
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
#include <string>
|
||||
|
||||
class QListWidget;
|
||||
class QObject;
|
||||
class QWidget;
|
||||
|
||||
namespace advss {
|
||||
|
||||
EXPORT std::pair<int, int> GetCursorPos();
|
||||
|
||||
@@ -18,6 +18,30 @@ static std::deque<std::shared_ptr<Item>> variables;
|
||||
static std::mutex lastVariableChangeMutex;
|
||||
static std::chrono::high_resolution_clock::time_point lastVariableChange{};
|
||||
|
||||
// When set, Variable::Value() and Variable::SetValue() operate on this context
|
||||
// instead of the global variable state. Used by action queues to isolate
|
||||
// variable reads and writes to a snapshot taken at the time the action was
|
||||
// added to the queue, so that actions can modify variables without affecting
|
||||
// the global state or other queue entries.
|
||||
thread_local static VariableContext *activeVarContext = nullptr;
|
||||
|
||||
VariableContext CreateVariableContext()
|
||||
{
|
||||
VariableContext context;
|
||||
for (const auto &v : variables) {
|
||||
const auto &var = std::dynamic_pointer_cast<Variable>(v);
|
||||
if (var) {
|
||||
context[var->Name()] = var->Value(false);
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
void SetActiveVariableContext(VariableContext *context)
|
||||
{
|
||||
activeVarContext = context;
|
||||
}
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddEarlySaveStep(SaveVariables);
|
||||
@@ -76,6 +100,14 @@ void Variable::Save(obs_data_t *obj) const
|
||||
|
||||
std::string Variable::Value(bool updateLastUsed) const
|
||||
{
|
||||
if (activeVarContext) {
|
||||
auto it = activeVarContext->find(Name());
|
||||
if (it == activeVarContext->end()) {
|
||||
return "";
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
if (updateLastUsed) {
|
||||
UpdateLastUsed();
|
||||
@@ -108,6 +140,16 @@ std::optional<int> Variable::IntValue() const
|
||||
|
||||
void Variable::SetValue(const std::string &value)
|
||||
{
|
||||
if (activeVarContext) {
|
||||
auto it = activeVarContext->find(Name());
|
||||
if (it == activeVarContext->end()) {
|
||||
return;
|
||||
}
|
||||
it->second = value;
|
||||
setLastVariableChangeTime();
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_mutex);
|
||||
_previousValue = _value;
|
||||
@@ -121,6 +163,7 @@ void Variable::SetValue(const std::string &value)
|
||||
}
|
||||
setLastVariableChangeTime();
|
||||
}
|
||||
|
||||
_cv.notify_all();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <obs-data.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <QStringList>
|
||||
|
||||
namespace advss {
|
||||
@@ -116,6 +117,10 @@ signals:
|
||||
void Remove(const QString &);
|
||||
};
|
||||
|
||||
using VariableContext = std::unordered_map<std::string, std::string>;
|
||||
VariableContext CreateVariableContext();
|
||||
void SetActiveVariableContext(VariableContext *context);
|
||||
|
||||
std::deque<std::shared_ptr<Item>> &GetVariables();
|
||||
EXPORT Variable *GetVariableByName(const std::string &name);
|
||||
EXPORT Variable *GetVariableByQString(const QString &name);
|
||||
|
||||
@@ -41,6 +41,7 @@ add_plugin(stream-deck)
|
||||
add_plugin(twitch)
|
||||
add_plugin(usb)
|
||||
add_plugin(video)
|
||||
add_plugin(speech)
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ if(NOT TARGET httplib)
|
||||
EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
if(OS_MACOS)
|
||||
if(OS_MACOS OR OS_WINDOWS)
|
||||
set(OPENSSL_USE_STATIC_LIBS
|
||||
ON
|
||||
CACHE BOOL "Use static OpenSSL" FORCE)
|
||||
@@ -25,6 +25,14 @@ if(NOT OPENSSL_FOUND)
|
||||
message(WARNING "OpenSSL not found!\n" "HTTP support will be disabled!\n\n")
|
||||
return()
|
||||
endif()
|
||||
if(OS_WINDOWS AND NOT OPENSSL_CRYPTO_LIBRARY MATCHES "_static\\.lib$")
|
||||
message(
|
||||
WARNING
|
||||
"Static OpenSSL libraries (libcrypto_static.lib / libssl_static.lib) not found!\n"
|
||||
"HTTP support will be disabled to avoid DLL name collisions with other plugins.\n\n"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
find_package(ZLIB)
|
||||
if(NOT ZLIB_FOUND)
|
||||
@@ -65,29 +73,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
|
||||
"${OPENSSL_INCLUDE_DIR}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(OS_WINDOWS)
|
||||
# Couldn't really find a better way to install runtime dependencies for
|
||||
# Windows TODO: Clean this up at some point
|
||||
function(FIND_FILES_WITH_PATTERN result pattern dir)
|
||||
execute_process(
|
||||
COMMAND
|
||||
powershell -Command
|
||||
"Get-ChildItem -Path '${dir}' -Recurse -Include ${pattern} |"
|
||||
"Select-Object -First 1 |"
|
||||
"ForEach-Object { $_.FullName -replace '\\\\', '\\\\' }"
|
||||
OUTPUT_VARIABLE files
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set(${result}
|
||||
${files}
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(OPENSSL_DLL_SEARCH_DIR "${OPENSSL_INCLUDE_DIR}/..")
|
||||
find_files_with_pattern(CRYPTO_DLL_FILES "libcrypto*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
find_files_with_pattern(SSL_DLL_FILES "libssl*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
"${CRYPTO_DLL_FILES}" "${SSL_DLL_FILES}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32 bcrypt)
|
||||
endif()
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
|
||||
@@ -3,12 +3,21 @@ project(advanced-scene-switcher-mqtt)
|
||||
|
||||
# --- Check paho.mqtt.cpp requirements ---
|
||||
|
||||
if(OS_MACOS)
|
||||
if(OS_MACOS OR OS_WINDOWS)
|
||||
set(OPENSSL_USE_STATIC_LIBS
|
||||
ON
|
||||
CACHE BOOL "Use static OpenSSL" FORCE)
|
||||
endif()
|
||||
find_package(OpenSSL)
|
||||
if(OS_WINDOWS
|
||||
AND OPENSSL_FOUND
|
||||
AND NOT OPENSSL_CRYPTO_LIBRARY MATCHES "_static\\.lib$")
|
||||
message(
|
||||
WARNING
|
||||
"Static OpenSSL libraries (libcrypto_static.lib / libssl_static.lib) not found!\n"
|
||||
"MQTT may fail at runtime due to OpenSSL DLL name collisions with other plugins.\n\n"
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(PahoMqttCpp)
|
||||
if(NOT PahoMqttCpp_FOUND)
|
||||
@@ -51,4 +60,7 @@ else()
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE PahoMqttCpp::paho-mqttpp3-static)
|
||||
endif()
|
||||
if(OS_WINDOWS)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32 bcrypt)
|
||||
endif()
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "macro-script-handler.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "macro-action-script.hpp"
|
||||
#include "macro-condition-script.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "variable.hpp"
|
||||
|
||||
#include <obs-properties.h>
|
||||
|
||||
125
plugins/speech/CMakeLists.txt
Normal file
125
plugins/speech/CMakeLists.txt
Normal file
@@ -0,0 +1,125 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(advanced-scene-switcher-speech)
|
||||
|
||||
# --- Check requirements ---
|
||||
|
||||
get_target_property(ADVSS_SOURCE_DIR advanced-scene-switcher-lib SOURCE_DIR)
|
||||
set(WHISPER_DIR "${ADVSS_SOURCE_DIR}/deps/whisper.cpp")
|
||||
if(NOT EXISTS "${WHISPER_DIR}/CMakeLists.txt")
|
||||
message(WARNING "whisper.cpp directory \"${WHISPER_DIR}\" not found!\n"
|
||||
"Speech condition will be disabled!\n\n"
|
||||
"Clone whisper.cpp into: ${WHISPER_DIR}")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(WHISPER_BUILD_TESTS
|
||||
OFF
|
||||
CACHE BOOL "" FORCE)
|
||||
set(WHISPER_BUILD_EXAMPLES
|
||||
OFF
|
||||
CACHE BOOL "" FORCE)
|
||||
set(BUILD_SHARED_LIBS
|
||||
OFF
|
||||
CACHE BOOL "" FORCE)
|
||||
|
||||
# Vulkan GPU backend. Auto-enabled when VULKAN_SDK is set (e.g. in CI).
|
||||
option(ADVSS_SPEECH_VULKAN "Use Vulkan GPU backend for speech recognition" OFF)
|
||||
|
||||
if(NOT ADVSS_SPEECH_VULKAN AND DEFINED ENV{VULKAN_SDK})
|
||||
set(ADVSS_SPEECH_VULKAN ON)
|
||||
endif()
|
||||
|
||||
if(ADVSS_SPEECH_VULKAN)
|
||||
set(GGML_VULKAN
|
||||
ON
|
||||
CACHE BOOL "" FORCE)
|
||||
endif()
|
||||
|
||||
# Suppress warnings-as-errors on all targets in a whisper.cpp source subtree.
|
||||
function(_advss_whisper_suppress_werror dir)
|
||||
get_property(
|
||||
_subdirs
|
||||
DIRECTORY "${dir}"
|
||||
PROPERTY SUBDIRECTORIES)
|
||||
foreach(_sub IN LISTS _subdirs)
|
||||
_advss_whisper_suppress_werror("${_sub}")
|
||||
endforeach()
|
||||
get_property(
|
||||
_targets
|
||||
DIRECTORY "${dir}"
|
||||
PROPERTY BUILDSYSTEM_TARGETS)
|
||||
foreach(_target IN LISTS _targets)
|
||||
get_target_property(_type ${_target} TYPE)
|
||||
if(_type STREQUAL "INTERFACE_LIBRARY" OR _type STREQUAL "UTILITY")
|
||||
continue()
|
||||
endif()
|
||||
get_target_property(_opts ${_target} COMPILE_OPTIONS)
|
||||
if(_opts)
|
||||
list(FILTER _opts EXCLUDE REGEX "^-Werror")
|
||||
list(REMOVE_ITEM _opts /WX)
|
||||
set_target_properties(${_target} PROPERTIES COMPILE_OPTIONS "${_opts}")
|
||||
endif()
|
||||
target_compile_options(
|
||||
${_target}
|
||||
PRIVATE $<$<C_COMPILER_ID:GNU,Clang,AppleClang>:-Wno-error>
|
||||
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wno-error>
|
||||
$<$<C_COMPILER_ID:MSVC>:/WX-> $<$<CXX_COMPILER_ID:MSVC>:/WX->)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
set(_advss_saved_werror ${CMAKE_COMPILE_WARNING_AS_ERROR})
|
||||
set(_advss_saved_pic ${CMAKE_POSITION_INDEPENDENT_CODE})
|
||||
set(CMAKE_COMPILE_WARNING_AS_ERROR OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if(NOT TARGET whisper)
|
||||
add_subdirectory("${WHISPER_DIR}" "${CMAKE_BINARY_DIR}/whisper.cpp"
|
||||
EXCLUDE_FROM_ALL)
|
||||
_advss_whisper_suppress_werror("${WHISPER_DIR}")
|
||||
# ggml-metal's .m files use manual retain/release; disable ARC on that target.
|
||||
if(TARGET ggml-metal)
|
||||
set_target_properties(ggml-metal
|
||||
PROPERTIES XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC NO)
|
||||
target_compile_options(ggml-metal
|
||||
PRIVATE $<$<COMPILE_LANGUAGE:OBJC>:-fno-objc-arc>)
|
||||
endif()
|
||||
endif()
|
||||
set(CMAKE_COMPILE_WARNING_AS_ERROR ${_advss_saved_werror})
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ${_advss_saved_pic})
|
||||
|
||||
# ggml-vulkan finds SPIRV-Headers but does not link it.
|
||||
if(ADVSS_SPEECH_VULKAN
|
||||
AND TARGET ggml-vulkan
|
||||
AND TARGET SPIRV-Headers::SPIRV-Headers)
|
||||
target_link_libraries(ggml-vulkan PRIVATE SPIRV-Headers::SPIRV-Headers)
|
||||
endif()
|
||||
|
||||
# OBS deps ship older Vulkan headers; put the SDK headers first so ggml-vulkan
|
||||
# sees the full API.
|
||||
if(ADVSS_SPEECH_VULKAN
|
||||
AND TARGET ggml-vulkan
|
||||
AND DEFINED ENV{VULKAN_SDK})
|
||||
if(WIN32)
|
||||
set(_vk_sdk_include "$ENV{VULKAN_SDK}/Include")
|
||||
else()
|
||||
set(_vk_sdk_include "$ENV{VULKAN_SDK}/include")
|
||||
endif()
|
||||
if(EXISTS "${_vk_sdk_include}")
|
||||
target_include_directories(ggml-vulkan BEFORE PRIVATE "${_vk_sdk_include}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(${PROJECT_NAME} MODULE)
|
||||
|
||||
target_sources(
|
||||
${PROJECT_NAME} PRIVATE macro-condition-speech.cpp macro-condition-speech.hpp
|
||||
speech-recognizer.cpp speech-recognizer.hpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${WHISPER_DIR}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE whisper)
|
||||
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
660
plugins/speech/macro-condition-speech.cpp
Normal file
660
plugins/speech/macro-condition-speech.cpp
Normal file
@@ -0,0 +1,660 @@
|
||||
#include "macro-condition-speech.hpp"
|
||||
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-helpers.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
|
||||
#include <obs-module.h>
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static int defaultNThreads()
|
||||
{
|
||||
return (int)std::min(
|
||||
4u, std::max(1u, std::thread::hardware_concurrency() / 4));
|
||||
}
|
||||
|
||||
static std::string getDefaultModelPath()
|
||||
{
|
||||
return std::string(obs_get_module_data_path(obs_current_module())) +
|
||||
"/res/speech/ggml-tiny-q8_0.bin";
|
||||
}
|
||||
|
||||
static QStringList getAudioSourcesList()
|
||||
{
|
||||
auto sources = GetAudioSourceNames();
|
||||
sources.sort();
|
||||
return sources;
|
||||
}
|
||||
|
||||
const std::string MacroConditionSpeech::id = "speech";
|
||||
|
||||
bool MacroConditionSpeech::_registered = MacroConditionFactory::Register(
|
||||
MacroConditionSpeech::id,
|
||||
{MacroConditionSpeech::Create, MacroConditionSpeechEdit::Create,
|
||||
"AdvSceneSwitcher.condition.speech"});
|
||||
|
||||
MacroConditionSpeech::MacroConditionSpeech(Macro *m)
|
||||
: MacroCondition(m),
|
||||
_modelPath(getDefaultModelPath()),
|
||||
_nThreads(defaultNThreads()),
|
||||
_language("auto"),
|
||||
_messageBuffer(_recognizer.RegisterClient())
|
||||
{
|
||||
_recognizer.SetNThreads(defaultNThreads());
|
||||
_recognizer.SetLanguage("auto");
|
||||
_recognizer.SetTranslate(false);
|
||||
_recognizer.SetVadEnergyThreshold(1e-4f);
|
||||
_recognizer.SetSuppressNonSpeechTokens(true);
|
||||
_recognizer.SetNoContext(true);
|
||||
}
|
||||
|
||||
MacroConditionSpeech::~MacroConditionSpeech()
|
||||
{
|
||||
if (_rebuildThread.joinable()) {
|
||||
_rebuildThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetCondition(Condition c)
|
||||
{
|
||||
_condition = c;
|
||||
SetupTempVars();
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetModelPath(const std::string &path)
|
||||
{
|
||||
_modelPath = path;
|
||||
RebuildRecognizer();
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetBufferDuration(const DoubleVariable &value)
|
||||
{
|
||||
_bufferDuration = value;
|
||||
_recognizer.SetBufferDuration((double)_bufferDuration);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetNThreads(const IntVariable &value)
|
||||
{
|
||||
_nThreads = value;
|
||||
_recognizer.SetNThreads((int)_nThreads);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetLanguage(const std::string &lang)
|
||||
{
|
||||
_language = lang;
|
||||
_recognizer.SetLanguage(lang);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetTranslate(bool translate)
|
||||
{
|
||||
_translate = translate;
|
||||
_recognizer.SetTranslate(translate);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetVadEnergyThreshold(const DoubleVariable &value)
|
||||
{
|
||||
_vadEnergyThreshold = value;
|
||||
_recognizer.SetVadEnergyThreshold((float)(double)_vadEnergyThreshold);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetSuppressNonSpeechTokens(bool suppress)
|
||||
{
|
||||
_suppressNonSpeechTokens = suppress;
|
||||
_recognizer.SetSuppressNonSpeechTokens(suppress);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetNoContext(bool noContext)
|
||||
{
|
||||
_noContext = noContext;
|
||||
_recognizer.SetNoContext(noContext);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetListenWhenMuted(bool listen)
|
||||
{
|
||||
_listenWhenMuted = listen;
|
||||
_recognizer.SetListenWhenMuted(listen);
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetUseGpu(bool useGpu)
|
||||
{
|
||||
_useGpu = useGpu;
|
||||
_recognizer.SetUseGpu(useGpu);
|
||||
RebuildRecognizer();
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::RebuildRecognizer()
|
||||
{
|
||||
_recognizer.StopCapture();
|
||||
|
||||
if (_rebuildThread.joinable()) {
|
||||
_rebuildThread.join();
|
||||
}
|
||||
|
||||
const std::string path = _modelPath;
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
OBSWeakSource weakSource = _source.GetSource();
|
||||
_messageBuffer = _recognizer.RegisterClient();
|
||||
|
||||
_rebuildThread = std::thread([this, path, weakSource]() {
|
||||
if (!_recognizer.LoadModel(path)) {
|
||||
return;
|
||||
}
|
||||
OBSSource source = OBSGetStrongRef(weakSource);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
_recognizer.StartCapture(source);
|
||||
});
|
||||
}
|
||||
|
||||
bool MacroConditionSpeech::CheckCondition()
|
||||
{
|
||||
std::string lastTranscript;
|
||||
bool anyReceived = false;
|
||||
|
||||
while (!_messageBuffer->Empty()) {
|
||||
auto msg = _messageBuffer->ConsumeMessage();
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
lastTranscript = *msg;
|
||||
anyReceived = true;
|
||||
}
|
||||
|
||||
if (anyReceived) {
|
||||
SetTempVarValue("speech", lastTranscript);
|
||||
}
|
||||
|
||||
switch (_condition) {
|
||||
case Condition::ANY:
|
||||
if (anyReceived) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
case Condition::CONTAINS: {
|
||||
if (!anyReceived) {
|
||||
return false;
|
||||
}
|
||||
const std::string phrase = _phrase;
|
||||
const QRegularExpression re(
|
||||
"\\b" +
|
||||
QRegularExpression::escape(
|
||||
QString::fromStdString(phrase)) +
|
||||
"\\b",
|
||||
QRegularExpression::CaseInsensitiveOption);
|
||||
if (re.match(QString::fromStdString(lastTranscript)).hasMatch()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
case Condition::MATCHES:
|
||||
if (!anyReceived) {
|
||||
return false;
|
||||
}
|
||||
if (_regex.Enabled() &&
|
||||
_regex.Matches(lastTranscript, _phrase)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MacroConditionSpeech::SetupTempVars()
|
||||
{
|
||||
MacroCondition::SetupTempVars();
|
||||
AddTempvar(
|
||||
"speech",
|
||||
obs_module_text("AdvSceneSwitcher.tempVar.speech.speech"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.speech.speech.description"));
|
||||
}
|
||||
|
||||
std::string MacroConditionSpeech::GetShortDesc() const
|
||||
{
|
||||
return _source.ToString();
|
||||
}
|
||||
|
||||
bool MacroConditionSpeech::Save(obs_data_t *obj) const
|
||||
{
|
||||
MacroCondition::Save(obj);
|
||||
_source.Save(obj, "source");
|
||||
obs_data_set_int(obj, "condition", static_cast<int>(_condition));
|
||||
_phrase.Save(obj, "phrase");
|
||||
_regex.Save(obj);
|
||||
_modelPath.Save(obj, "modelPath");
|
||||
_bufferDuration.Save(obj, "bufferDuration");
|
||||
_nThreads.Save(obj, "nThreads");
|
||||
_language.Save(obj, "language");
|
||||
obs_data_set_bool(obj, "translate", _translate);
|
||||
_vadEnergyThreshold.Save(obj, "vadEnergyThreshold");
|
||||
obs_data_set_bool(obj, "suppressNonSpeechTokens",
|
||||
_suppressNonSpeechTokens);
|
||||
obs_data_set_bool(obj, "noContext", _noContext);
|
||||
obs_data_set_bool(obj, "listenWhenMuted", _listenWhenMuted);
|
||||
obs_data_set_bool(obj, "useGpu", _useGpu);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MacroConditionSpeech::Load(obs_data_t *obj)
|
||||
{
|
||||
MacroCondition::Load(obj);
|
||||
_source.Load(obj, "source");
|
||||
SetCondition(
|
||||
static_cast<Condition>(obs_data_get_int(obj, "condition")));
|
||||
_phrase.Load(obj, "phrase");
|
||||
_regex.Load(obj);
|
||||
_modelPath.Load(obj, "modelPath");
|
||||
_bufferDuration.Load(obj, "bufferDuration");
|
||||
_nThreads.Load(obj, "nThreads");
|
||||
_language.Load(obj, "language");
|
||||
_translate = obs_data_get_bool(obj, "translate");
|
||||
_vadEnergyThreshold.Load(obj, "vadEnergyThreshold");
|
||||
_suppressNonSpeechTokens =
|
||||
obs_data_get_bool(obj, "suppressNonSpeechTokens");
|
||||
_noContext = obs_data_get_bool(obj, "noContext");
|
||||
_listenWhenMuted = obs_data_get_bool(obj, "listenWhenMuted");
|
||||
_useGpu = obs_data_get_bool(obj, "useGpu");
|
||||
_recognizer.SetNThreads((int)_nThreads);
|
||||
_recognizer.SetLanguage(std::string(_language));
|
||||
_recognizer.SetTranslate(_translate);
|
||||
_recognizer.SetVadEnergyThreshold((float)(double)_vadEnergyThreshold);
|
||||
_recognizer.SetSuppressNonSpeechTokens(_suppressNonSpeechTokens);
|
||||
_recognizer.SetNoContext(_noContext);
|
||||
_recognizer.SetListenWhenMuted(_listenWhenMuted);
|
||||
_recognizer.SetUseGpu(_useGpu);
|
||||
RebuildRecognizer();
|
||||
return true;
|
||||
}
|
||||
|
||||
static void populateConditionSelection(QComboBox *list)
|
||||
{
|
||||
static const std::map<MacroConditionSpeech::Condition, std::string>
|
||||
conditionTypes = {
|
||||
{MacroConditionSpeech::Condition::ANY,
|
||||
"AdvSceneSwitcher.condition.speech.condition.any"},
|
||||
{MacroConditionSpeech::Condition::CONTAINS,
|
||||
"AdvSceneSwitcher.condition.speech.condition.contains"},
|
||||
{MacroConditionSpeech::Condition::MATCHES,
|
||||
"AdvSceneSwitcher.condition.speech.condition.matches"},
|
||||
};
|
||||
|
||||
for (const auto &[cond, name] : conditionTypes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(cond));
|
||||
}
|
||||
}
|
||||
|
||||
MacroConditionSpeechEdit::MacroConditionSpeechEdit(
|
||||
QWidget *parent, std::shared_ptr<MacroConditionSpeech> entryData)
|
||||
: QWidget(parent),
|
||||
_source(new SourceSelectionWidget(this, getAudioSourcesList, true)),
|
||||
_conditions(new QComboBox(this)),
|
||||
_phrase(new VariableLineEdit(this)),
|
||||
_regex(new RegexConfigWidget(parent)),
|
||||
_modelPath(new FileSelection(
|
||||
FileSelection::Type::READ, this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.browse.title"))),
|
||||
_modelHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.model.help"),
|
||||
this)),
|
||||
_bufferDuration(new VariableDoubleSpinBox(this)),
|
||||
_bufferHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.buffer.help"),
|
||||
this)),
|
||||
_advancedSection(new Section(300, this)),
|
||||
_nThreads(new VariableSpinBox(this)),
|
||||
_language(new VariableLineEdit(this)),
|
||||
_languageHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.language.help"),
|
||||
this)),
|
||||
_translate(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.translate"),
|
||||
this)),
|
||||
_translateHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.translate.help"),
|
||||
this)),
|
||||
_vadEnergyThreshold(new VariableDoubleSpinBox(this)),
|
||||
_vadHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.vad.help"),
|
||||
this)),
|
||||
_suppressNonSpeechTokens(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.suppress"),
|
||||
this)),
|
||||
_suppressHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.suppress.help"),
|
||||
this)),
|
||||
_noContext(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.noContext"),
|
||||
this)),
|
||||
_noContextHelp(new HelpIcon(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.noContext.help"),
|
||||
this)),
|
||||
_listenWhenMuted(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.listenWhenMuted"),
|
||||
this)),
|
||||
_useGpu(new QCheckBox(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.advanced.useGpu"),
|
||||
this))
|
||||
{
|
||||
populateConditionSelection(_conditions);
|
||||
|
||||
_bufferDuration->setMinimum(1.0);
|
||||
_bufferDuration->setMaximum(30.0);
|
||||
_bufferDuration->SpinBox()->setSingleStep(0.5);
|
||||
_bufferDuration->setSuffix(" s");
|
||||
|
||||
_nThreads->setMinimum(1);
|
||||
_nThreads->setMaximum(32);
|
||||
|
||||
_vadEnergyThreshold->setMinimum(0.0);
|
||||
_vadEnergyThreshold->setMaximum(1.0);
|
||||
_vadEnergyThreshold->SpinBox()->setSingleStep(1e-5);
|
||||
_vadEnergyThreshold->SpinBox()->setDecimals(6);
|
||||
|
||||
QWidget::connect(_source,
|
||||
SIGNAL(SourceChanged(const SourceSelection &)), this,
|
||||
SLOT(SourceChanged(const SourceSelection &)));
|
||||
QWidget::connect(_conditions, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(ConditionChanged(int)));
|
||||
QWidget::connect(_phrase, SIGNAL(editingFinished()), this,
|
||||
SLOT(PhraseChanged()));
|
||||
QWidget::connect(_regex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(RegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_modelPath, SIGNAL(PathChanged(const QString &)), this,
|
||||
SLOT(ModelPathChanged(const QString &)));
|
||||
QWidget::connect(
|
||||
_bufferDuration,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(BufferDurationChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(
|
||||
_nThreads,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<int> &)),
|
||||
this, SLOT(NThreadsChanged(const NumberVariable<int> &)));
|
||||
QWidget::connect(_language, SIGNAL(editingFinished()), this,
|
||||
SLOT(LanguageChanged()));
|
||||
QWidget::connect(_translate, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(TranslateChanged(int)));
|
||||
QWidget::connect(
|
||||
_vadEnergyThreshold,
|
||||
SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(VadEnergyThresholdChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(_suppressNonSpeechTokens, SIGNAL(stateChanged(int)),
|
||||
this, SLOT(SuppressNonSpeechTokensChanged(int)));
|
||||
QWidget::connect(_noContext, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(NoContextChanged(int)));
|
||||
QWidget::connect(_listenWhenMuted, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(ListenWhenMutedChanged(int)));
|
||||
QWidget::connect(_useGpu, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseGpuChanged(int)));
|
||||
|
||||
_condSourceLayout = new QHBoxLayout;
|
||||
|
||||
_phraseLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.phrase"),
|
||||
_phraseLayout,
|
||||
{{"{{phrase}}", _phrase}, {"{{regex}}", _regex}}, false);
|
||||
|
||||
auto *modelLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.model"),
|
||||
modelLayout,
|
||||
{{"{{modelPath}}", _modelPath}, {"{{help}}", _modelHelp}},
|
||||
false);
|
||||
|
||||
auto *bufferLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.buffer"),
|
||||
bufferLayout,
|
||||
{{"{{bufferDuration}}", _bufferDuration},
|
||||
{"{{help}}", _bufferHelp}});
|
||||
|
||||
auto *threadsLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.threads"),
|
||||
threadsLayout, {{"{{threads}}", _nThreads}});
|
||||
|
||||
auto *languageLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.language"),
|
||||
languageLayout,
|
||||
{{"{{language}}", _language}, {"{{help}}", _languageHelp}});
|
||||
|
||||
auto *translateLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.translate"),
|
||||
translateLayout,
|
||||
{{"{{translate}}", _translate}, {"{{help}}", _translateHelp}});
|
||||
|
||||
auto *vadLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.vad"),
|
||||
vadLayout,
|
||||
{{"{{vad}}", _vadEnergyThreshold}, {"{{help}}", _vadHelp}});
|
||||
|
||||
auto *suppressLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.suppress"),
|
||||
suppressLayout,
|
||||
{{"{{suppress}}", _suppressNonSpeechTokens},
|
||||
{"{{help}}", _suppressHelp}});
|
||||
|
||||
auto *noContextLayout = new QHBoxLayout;
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.speech.layout.advanced.noContext"),
|
||||
noContextLayout,
|
||||
{{"{{noContext}}", _noContext}, {"{{help}}", _noContextHelp}});
|
||||
|
||||
auto *advancedContent = new QWidget(this);
|
||||
auto *advancedLayout = new QVBoxLayout;
|
||||
advancedLayout->addLayout(threadsLayout);
|
||||
advancedLayout->addLayout(languageLayout);
|
||||
advancedLayout->addLayout(translateLayout);
|
||||
advancedLayout->addLayout(vadLayout);
|
||||
advancedLayout->addLayout(suppressLayout);
|
||||
advancedLayout->addLayout(noContextLayout);
|
||||
advancedLayout->addWidget(_listenWhenMuted);
|
||||
advancedLayout->addWidget(_useGpu);
|
||||
advancedContent->setLayout(advancedLayout);
|
||||
|
||||
_advancedSection->AddHeaderWidget(new QLabel(
|
||||
obs_module_text("AdvSceneSwitcher.condition.speech.advanced"),
|
||||
this));
|
||||
_advancedSection->SetContent(advancedContent, true);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout;
|
||||
mainLayout->addLayout(_condSourceLayout);
|
||||
mainLayout->addLayout(_phraseLayout);
|
||||
mainLayout->addLayout(modelLayout);
|
||||
mainLayout->addLayout(bufferLayout);
|
||||
mainLayout->addWidget(_advancedSection);
|
||||
setLayout(mainLayout);
|
||||
|
||||
_entryData = entryData;
|
||||
UpdateEntryData();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::UpdateEntryData()
|
||||
{
|
||||
if (!_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
_source->SetSource(_entryData->_source);
|
||||
_conditions->setCurrentIndex(
|
||||
static_cast<int>(_entryData->GetCondition()));
|
||||
_phrase->setText(QString::fromStdString(_entryData->_phrase));
|
||||
_regex->SetRegexConfig(_entryData->_regex);
|
||||
_modelPath->SetPath(_entryData->GetModelPath());
|
||||
_bufferDuration->SetValue(_entryData->GetBufferDuration());
|
||||
_nThreads->SetValue(_entryData->GetNThreads());
|
||||
_language->setText(QString::fromStdString(_entryData->GetLanguage()));
|
||||
_translate->setChecked(_entryData->GetTranslate());
|
||||
_vadEnergyThreshold->SetValue(_entryData->GetVadEnergyThreshold());
|
||||
_suppressNonSpeechTokens->setChecked(
|
||||
_entryData->GetSuppressNonSpeechTokens());
|
||||
_noContext->setChecked(_entryData->GetNoContext());
|
||||
_listenWhenMuted->setChecked(_entryData->GetListenWhenMuted());
|
||||
_useGpu->setChecked(_entryData->GetUseGpu());
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::SourceChanged(const SourceSelection &source)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_source = source;
|
||||
_entryData->RebuildRecognizer();
|
||||
emit HeaderInfoChanged(
|
||||
QString::fromStdString(_entryData->GetShortDesc()));
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::ConditionChanged(int idx)
|
||||
{
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetCondition(
|
||||
static_cast<MacroConditionSpeech::Condition>(
|
||||
_conditions->itemData(idx).toInt()));
|
||||
}
|
||||
SetWidgetVisibility();
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::PhraseChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_phrase = _phrase->text().toStdString();
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::RegexChanged(const RegexConfig &conf)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_regex = conf;
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::ModelPathChanged(const QString &path)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetModelPath(path.toStdString());
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::BufferDurationChanged(
|
||||
const NumberVariable<double> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetBufferDuration(value);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::NThreadsChanged(const NumberVariable<int> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetNThreads(value);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::LanguageChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetLanguage(_language->text().toStdString());
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::TranslateChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetTranslate(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::VadEnergyThresholdChanged(
|
||||
const NumberVariable<double> &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetVadEnergyThreshold(value);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::SuppressNonSpeechTokensChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetSuppressNonSpeechTokens(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::NoContextChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetNoContext(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::ListenWhenMutedChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetListenWhenMuted(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::UseGpuChanged(int state)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetUseGpu(state == Qt::Checked);
|
||||
}
|
||||
|
||||
void MacroConditionSpeechEdit::SetWidgetVisibility()
|
||||
{
|
||||
const auto condition = _entryData->GetCondition();
|
||||
const bool hasPhrase = condition !=
|
||||
MacroConditionSpeech::Condition::ANY;
|
||||
|
||||
_condSourceLayout->removeWidget(_conditions);
|
||||
_condSourceLayout->removeWidget(_source);
|
||||
ClearLayout(_condSourceLayout);
|
||||
const char *layoutKey = "AdvSceneSwitcher.condition.speech.layout.any";
|
||||
if (condition == MacroConditionSpeech::Condition::CONTAINS) {
|
||||
layoutKey = "AdvSceneSwitcher.condition.speech.layout.contains";
|
||||
} else if (condition == MacroConditionSpeech::Condition::MATCHES) {
|
||||
layoutKey = "AdvSceneSwitcher.condition.speech.layout.matches";
|
||||
}
|
||||
PlaceWidgets(obs_module_text(layoutKey), _condSourceLayout,
|
||||
{{"{{conditions}}", _conditions},
|
||||
{"{{source}}", _source}});
|
||||
|
||||
SetLayoutVisible(_phraseLayout, hasPhrase);
|
||||
_regex->setVisible(condition ==
|
||||
MacroConditionSpeech::Condition::MATCHES);
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
182
plugins/speech/macro-condition-speech.hpp
Normal file
182
plugins/speech/macro-condition-speech.hpp
Normal file
@@ -0,0 +1,182 @@
|
||||
#pragma once
|
||||
#include "file-selection.hpp"
|
||||
#include "help-icon.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "regex-config.hpp"
|
||||
#include "section.hpp"
|
||||
#include "source-selection.hpp"
|
||||
#include "speech-recognizer.hpp"
|
||||
#include "variable-line-edit.hpp"
|
||||
#include "variable-number.hpp"
|
||||
#include "variable-spinbox.hpp"
|
||||
#include "variable-string.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
#include <thread>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class MacroConditionSpeech : public MacroCondition {
|
||||
public:
|
||||
MacroConditionSpeech(Macro *m);
|
||||
~MacroConditionSpeech();
|
||||
|
||||
bool CheckCondition() override;
|
||||
bool Save(obs_data_t *obj) const override;
|
||||
bool Load(obs_data_t *obj) override;
|
||||
std::string GetShortDesc() const override;
|
||||
std::string GetId() const override { return id; }
|
||||
|
||||
static std::shared_ptr<MacroCondition> Create(Macro *m)
|
||||
{
|
||||
return std::make_shared<MacroConditionSpeech>(m);
|
||||
}
|
||||
|
||||
enum class Condition {
|
||||
ANY,
|
||||
CONTAINS, // Just a more user friendly variant of "matches"
|
||||
MATCHES,
|
||||
};
|
||||
|
||||
void SetCondition(Condition c);
|
||||
Condition GetCondition() const { return _condition; }
|
||||
|
||||
void SetModelPath(const std::string &path);
|
||||
const StringVariable &GetModelPath() const { return _modelPath; }
|
||||
|
||||
void SetBufferDuration(const DoubleVariable &value);
|
||||
DoubleVariable GetBufferDuration() const { return _bufferDuration; }
|
||||
|
||||
void SetNThreads(const IntVariable &value);
|
||||
IntVariable GetNThreads() const { return _nThreads; }
|
||||
|
||||
void SetLanguage(const std::string &lang);
|
||||
const StringVariable &GetLanguage() const { return _language; }
|
||||
|
||||
void SetTranslate(bool translate);
|
||||
bool GetTranslate() const { return _translate; }
|
||||
|
||||
void SetVadEnergyThreshold(const DoubleVariable &value);
|
||||
DoubleVariable GetVadEnergyThreshold() const
|
||||
{
|
||||
return _vadEnergyThreshold;
|
||||
}
|
||||
|
||||
void SetSuppressNonSpeechTokens(bool suppress);
|
||||
bool GetSuppressNonSpeechTokens() const
|
||||
{
|
||||
return _suppressNonSpeechTokens;
|
||||
}
|
||||
|
||||
void SetNoContext(bool noContext);
|
||||
bool GetNoContext() const { return _noContext; }
|
||||
|
||||
void SetListenWhenMuted(bool listen);
|
||||
bool GetListenWhenMuted() const { return _listenWhenMuted; }
|
||||
|
||||
void SetUseGpu(bool useGpu);
|
||||
bool GetUseGpu() const { return _useGpu; }
|
||||
|
||||
SourceSelection _source;
|
||||
StringVariable _phrase = "";
|
||||
RegexConfig _regex;
|
||||
|
||||
void RebuildRecognizer();
|
||||
|
||||
private:
|
||||
void SetupTempVars() override;
|
||||
|
||||
Condition _condition = Condition::ANY;
|
||||
StringVariable _modelPath;
|
||||
DoubleVariable _bufferDuration = 5.0;
|
||||
IntVariable _nThreads;
|
||||
StringVariable _language;
|
||||
bool _translate = false;
|
||||
DoubleVariable _vadEnergyThreshold = 1e-4;
|
||||
bool _suppressNonSpeechTokens = true;
|
||||
bool _noContext = true;
|
||||
bool _listenWhenMuted = false;
|
||||
bool _useGpu = true;
|
||||
|
||||
SpeechRecognizer _recognizer;
|
||||
std::shared_ptr<MessageBuffer<std::string>> _messageBuffer;
|
||||
std::thread _rebuildThread;
|
||||
|
||||
static bool _registered;
|
||||
static const std::string id;
|
||||
};
|
||||
|
||||
class MacroConditionSpeechEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MacroConditionSpeechEdit(
|
||||
QWidget *parent,
|
||||
std::shared_ptr<MacroConditionSpeech> entryData = nullptr);
|
||||
void UpdateEntryData();
|
||||
|
||||
static QWidget *Create(QWidget *parent,
|
||||
std::shared_ptr<MacroCondition> cond)
|
||||
{
|
||||
return new MacroConditionSpeechEdit(
|
||||
parent,
|
||||
std::dynamic_pointer_cast<MacroConditionSpeech>(cond));
|
||||
}
|
||||
|
||||
private slots:
|
||||
void SourceChanged(const SourceSelection &);
|
||||
void ConditionChanged(int);
|
||||
void PhraseChanged();
|
||||
void RegexChanged(const RegexConfig &);
|
||||
void ModelPathChanged(const QString &);
|
||||
void BufferDurationChanged(const NumberVariable<double> &);
|
||||
void NThreadsChanged(const NumberVariable<int> &);
|
||||
void LanguageChanged();
|
||||
void TranslateChanged(int);
|
||||
void VadEnergyThresholdChanged(const NumberVariable<double> &);
|
||||
void SuppressNonSpeechTokensChanged(int);
|
||||
void NoContextChanged(int);
|
||||
void ListenWhenMutedChanged(int);
|
||||
void UseGpuChanged(int);
|
||||
|
||||
signals:
|
||||
void HeaderInfoChanged(const QString &);
|
||||
|
||||
private:
|
||||
void SetWidgetVisibility();
|
||||
|
||||
SourceSelectionWidget *_source;
|
||||
QComboBox *_conditions;
|
||||
VariableLineEdit *_phrase;
|
||||
RegexConfigWidget *_regex;
|
||||
FileSelection *_modelPath;
|
||||
HelpIcon *_modelHelp;
|
||||
VariableDoubleSpinBox *_bufferDuration;
|
||||
HelpIcon *_bufferHelp;
|
||||
Section *_advancedSection;
|
||||
VariableSpinBox *_nThreads;
|
||||
VariableLineEdit *_language;
|
||||
HelpIcon *_languageHelp;
|
||||
QCheckBox *_translate;
|
||||
HelpIcon *_translateHelp;
|
||||
VariableDoubleSpinBox *_vadEnergyThreshold;
|
||||
HelpIcon *_vadHelp;
|
||||
QCheckBox *_suppressNonSpeechTokens;
|
||||
HelpIcon *_suppressHelp;
|
||||
QCheckBox *_noContext;
|
||||
HelpIcon *_noContextHelp;
|
||||
QCheckBox *_listenWhenMuted;
|
||||
QCheckBox *_useGpu;
|
||||
|
||||
QHBoxLayout *_condSourceLayout;
|
||||
QHBoxLayout *_phraseLayout;
|
||||
|
||||
std::shared_ptr<MacroConditionSpeech> _entryData;
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
402
plugins/speech/speech-recognizer.cpp
Normal file
402
plugins/speech/speech-recognizer.cpp
Normal file
@@ -0,0 +1,402 @@
|
||||
#include "speech-recognizer.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <obs.h>
|
||||
#include <media-io/audio-resampler.h>
|
||||
#include <util/platform.h>
|
||||
|
||||
#include <whisper.h>
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Ignore configured log level until after loading is complete to ensure we
|
||||
// catch the initial whisper configuration logs
|
||||
static bool ignoreLogFilter = true;
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
AddFinishedLoadingStep([]() { ignoreLogFilter = false; });
|
||||
return true;
|
||||
}
|
||||
const bool _ = setup();
|
||||
|
||||
static void whisperLogCallback(ggml_log_level level, const char *text, void *)
|
||||
{
|
||||
if (!text || *text == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
int obsLevel = LOG_INFO;
|
||||
if (level == GGML_LOG_LEVEL_WARN) {
|
||||
obsLevel = LOG_WARNING;
|
||||
} else if (level == GGML_LOG_LEVEL_ERROR) {
|
||||
obsLevel = LOG_ERROR;
|
||||
}
|
||||
|
||||
std::string msg(text);
|
||||
if (!msg.empty() && msg.back() == '\n') {
|
||||
msg.pop_back();
|
||||
}
|
||||
|
||||
if (msg.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ignoreLogFilter) {
|
||||
blog(obsLevel, "[speech] %s", msg.c_str());
|
||||
} else {
|
||||
vblog(obsLevel, "[speech] %s", msg.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr int whisperSampleRate = 16000;
|
||||
|
||||
// How often to evaluate VAD and potentially trigger inference
|
||||
static constexpr double stepDurationSeconds = 1.0;
|
||||
|
||||
// Audio kept from the previous inference run to provide word-boundary context
|
||||
// for the next run.
|
||||
// I guess repeating something is better than potentially missing stuff.
|
||||
static constexpr double keepDurationSeconds = 0.2;
|
||||
|
||||
SpeechRecognizer::SpeechRecognizer()
|
||||
{
|
||||
_inferenceThread = std::thread(&SpeechRecognizer::InferenceLoop, this);
|
||||
}
|
||||
|
||||
SpeechRecognizer::~SpeechRecognizer()
|
||||
{
|
||||
StopCapture();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_inferenceMutex);
|
||||
_stopThread = true;
|
||||
_bufferReady = true;
|
||||
}
|
||||
_inferenceCV.notify_one();
|
||||
if (_inferenceThread.joinable()) {
|
||||
_inferenceThread.join();
|
||||
}
|
||||
|
||||
if (_ctx) {
|
||||
whisper_free(_ctx);
|
||||
}
|
||||
if (_resampler) {
|
||||
audio_resampler_destroy(
|
||||
static_cast<audio_resampler_t *>(_resampler));
|
||||
}
|
||||
}
|
||||
|
||||
bool SpeechRecognizer::LoadModel(const std::string &modelPath)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_ctxMutex);
|
||||
|
||||
if (_ctx) {
|
||||
whisper_free(_ctx);
|
||||
_ctx = nullptr;
|
||||
}
|
||||
|
||||
whisper_log_set(whisperLogCallback, nullptr);
|
||||
|
||||
whisper_context_params cparams = whisper_context_default_params();
|
||||
cparams.use_gpu = _useGpu;
|
||||
_ctx = whisper_init_from_file_with_params(modelPath.c_str(), cparams);
|
||||
if (!_ctx) {
|
||||
blog(LOG_WARNING, "failed to load whisper model: %s",
|
||||
modelPath.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpeechRecognizer::StartCapture(obs_source_t *source)
|
||||
{
|
||||
StopCapture();
|
||||
|
||||
if (!source) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const audio_t *audio = obs_get_audio();
|
||||
if (!audio) {
|
||||
return false;
|
||||
}
|
||||
const struct audio_output_info *aoi = audio_output_get_info(audio);
|
||||
_sourceSampleRate = (int)aoi->samples_per_sec;
|
||||
_sourceChannelCount = (int)get_audio_channels(aoi->speakers);
|
||||
|
||||
if (_resampler) {
|
||||
audio_resampler_destroy(
|
||||
static_cast<audio_resampler_t *>(_resampler));
|
||||
_resampler = nullptr;
|
||||
}
|
||||
|
||||
struct resample_info srcInfo = {};
|
||||
srcInfo.samples_per_sec = (uint32_t)_sourceSampleRate;
|
||||
srcInfo.format = AUDIO_FORMAT_FLOAT_PLANAR;
|
||||
srcInfo.speakers = aoi->speakers;
|
||||
|
||||
struct resample_info dstInfo = {};
|
||||
dstInfo.samples_per_sec = whisperSampleRate;
|
||||
dstInfo.format = AUDIO_FORMAT_FLOAT;
|
||||
dstInfo.speakers = SPEAKERS_MONO;
|
||||
|
||||
_resampler = audio_resampler_create(&dstInfo, &srcInfo);
|
||||
if (!_resampler) {
|
||||
blog(LOG_WARNING,
|
||||
"failed to create audio resampler for speech condition");
|
||||
return false;
|
||||
}
|
||||
|
||||
_captureSource = obs_source_get_weak_source(source);
|
||||
obs_source_add_audio_capture_callback(source, AudioCaptureCallback,
|
||||
this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::StopCapture()
|
||||
{
|
||||
OBSSource source = OBSGetStrongRef(_captureSource);
|
||||
if (source) {
|
||||
obs_source_remove_audio_capture_callback(
|
||||
source, AudioCaptureCallback, this);
|
||||
}
|
||||
_captureSource = OBSWeakSource{};
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetBufferDuration(double seconds)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_audioMutex);
|
||||
_bufferDurationSeconds = seconds;
|
||||
_audioBuffer.clear();
|
||||
_framesSinceLastStep = 0;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetNThreads(int n)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_nThreads = std::max(1, n);
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetLanguage(const std::string &lang)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_language = lang.empty() ? "auto" : lang;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetTranslate(bool translate)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_translate = translate;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetVadEnergyThreshold(float threshold)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_audioMutex);
|
||||
_vadEnergyThreshold = threshold;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetSuppressNonSpeechTokens(bool suppress)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_suppressNonSpeechTokens = suppress;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetNoContext(bool noContext)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_inferenceMutex);
|
||||
_noContext = noContext;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetListenWhenMuted(bool listen)
|
||||
{
|
||||
_listenWhenMuted = listen;
|
||||
}
|
||||
|
||||
void SpeechRecognizer::SetUseGpu(bool useGpu)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_ctxMutex);
|
||||
_useGpu = useGpu;
|
||||
}
|
||||
|
||||
std::shared_ptr<MessageBuffer<std::string>> SpeechRecognizer::RegisterClient()
|
||||
{
|
||||
return _dispatcher.RegisterClient();
|
||||
}
|
||||
|
||||
void SpeechRecognizer::AudioCaptureCallback(void *param, obs_source_t *,
|
||||
const struct audio_data *audio,
|
||||
bool muted)
|
||||
{
|
||||
if (!audio || !audio->data[0]) {
|
||||
return;
|
||||
}
|
||||
auto *self = static_cast<SpeechRecognizer *>(param);
|
||||
if (muted && !self->_listenWhenMuted) {
|
||||
return;
|
||||
}
|
||||
self->AppendResampledAudio(audio);
|
||||
}
|
||||
|
||||
void SpeechRecognizer::AppendResampledAudio(const struct audio_data *audio)
|
||||
{
|
||||
if (!_resampler) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t *resampledData[MAX_AV_PLANES] = {};
|
||||
uint32_t outFrames = 0;
|
||||
uint64_t tsOffset = 0;
|
||||
|
||||
bool ok = audio_resampler_resample(
|
||||
static_cast<audio_resampler_t *>(_resampler), resampledData,
|
||||
&outFrames, &tsOffset, (const uint8_t *const *)audio->data,
|
||||
audio->frames);
|
||||
if (!ok || outFrames == 0 || !resampledData[0]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float *samples =
|
||||
reinterpret_cast<const float *>(resampledData[0]);
|
||||
|
||||
std::unique_lock<std::mutex> lock(_audioMutex);
|
||||
_audioBuffer.insert(_audioBuffer.end(), samples, samples + outFrames);
|
||||
_framesSinceLastStep += outFrames;
|
||||
|
||||
// Keep the rolling buffer capped at the configured context window.
|
||||
const size_t maxFrames =
|
||||
(size_t)(_bufferDurationSeconds * whisperSampleRate);
|
||||
if (_audioBuffer.size() > maxFrames) {
|
||||
_audioBuffer.erase(_audioBuffer.begin(),
|
||||
_audioBuffer.begin() +
|
||||
(_audioBuffer.size() - maxFrames));
|
||||
}
|
||||
|
||||
// Only consider triggering inference once per step interval.
|
||||
const size_t stepFrames =
|
||||
(size_t)(stepDurationSeconds * whisperSampleRate);
|
||||
if (_framesSinceLastStep < stepFrames) {
|
||||
return;
|
||||
}
|
||||
_framesSinceLastStep = 0;
|
||||
|
||||
// VAD: measure energy over just the most recent step window so that a
|
||||
// short utterance at the end of a longer silent buffer is not diluted.
|
||||
const size_t vadWindow = std::min(_audioBuffer.size(), stepFrames);
|
||||
const size_t vadStart = _audioBuffer.size() - vadWindow;
|
||||
float energy = 0.0f;
|
||||
for (size_t i = vadStart; i < _audioBuffer.size(); ++i) {
|
||||
energy += _audioBuffer[i] * _audioBuffer[i];
|
||||
}
|
||||
energy /= (float)vadWindow;
|
||||
const bool tooSilent = energy < _vadEnergyThreshold;
|
||||
if (tooSilent) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> infLock(_inferenceMutex);
|
||||
if (_bufferReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
_inferenceBuffer = _audioBuffer;
|
||||
_bufferReady = true;
|
||||
_inferenceCV.notify_one();
|
||||
}
|
||||
|
||||
// Retain a short overlap so the next inference has word-boundary context.
|
||||
const size_t keepFrames =
|
||||
(size_t)(keepDurationSeconds * whisperSampleRate);
|
||||
if (_audioBuffer.size() > keepFrames) {
|
||||
_audioBuffer.erase(_audioBuffer.begin(),
|
||||
_audioBuffer.begin() +
|
||||
(_audioBuffer.size() - keepFrames));
|
||||
}
|
||||
}
|
||||
|
||||
void SpeechRecognizer::InferenceLoop()
|
||||
{
|
||||
while (true) {
|
||||
std::vector<float> buffer;
|
||||
int nThreads;
|
||||
std::string language;
|
||||
bool translate;
|
||||
bool suppressNonSpeechTokens;
|
||||
bool noContext;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_inferenceMutex);
|
||||
_inferenceCV.wait(lock,
|
||||
[this] { return _bufferReady; });
|
||||
_bufferReady = false;
|
||||
if (_stopThread) {
|
||||
break;
|
||||
}
|
||||
buffer = std::move(_inferenceBuffer);
|
||||
nThreads = _nThreads;
|
||||
language = _language;
|
||||
translate = _translate;
|
||||
suppressNonSpeechTokens = _suppressNonSpeechTokens;
|
||||
noContext = _noContext;
|
||||
}
|
||||
|
||||
if (buffer.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> ctxLock(_ctxMutex);
|
||||
|
||||
if (!_ctx) {
|
||||
continue;
|
||||
}
|
||||
|
||||
whisper_full_params params =
|
||||
whisper_full_default_params(WHISPER_SAMPLING_GREEDY);
|
||||
params.print_realtime = false;
|
||||
params.print_progress = false;
|
||||
params.print_timestamps = false;
|
||||
params.print_special = false;
|
||||
params.translate = translate;
|
||||
params.language = language.c_str();
|
||||
params.n_threads = nThreads;
|
||||
params.single_segment = false;
|
||||
params.suppress_nst = suppressNonSpeechTokens;
|
||||
params.no_context = noContext;
|
||||
|
||||
// Limit the encoder to the actual audio length
|
||||
params.audio_ctx =
|
||||
std::min(1500, (int)((float)buffer.size() /
|
||||
(float)whisperSampleRate * 50.0f));
|
||||
|
||||
int rc = whisper_full(_ctx, params, buffer.data(),
|
||||
(int)buffer.size());
|
||||
if (rc != 0) {
|
||||
blog(LOG_WARNING, "whisper_full returned %d", rc);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string transcript;
|
||||
const int nSegments = whisper_full_n_segments(_ctx);
|
||||
for (int i = 0; i < nSegments; ++i) {
|
||||
const char *text =
|
||||
whisper_full_get_segment_text(_ctx, i);
|
||||
if (text) {
|
||||
transcript += text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!transcript.empty()) {
|
||||
const auto begin =
|
||||
transcript.find_first_not_of(" \t\r\n");
|
||||
if (begin != std::string::npos) {
|
||||
transcript = transcript.substr(begin);
|
||||
}
|
||||
_dispatcher.DispatchMessage(transcript);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
86
plugins/speech/speech-recognizer.hpp
Normal file
86
plugins/speech/speech-recognizer.hpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
#include "message-buffer.hpp"
|
||||
#include "message-dispatcher.hpp"
|
||||
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
struct whisper_context;
|
||||
struct audio_data;
|
||||
|
||||
namespace advss {
|
||||
|
||||
// Captures audio from one OBS source, resamples to 16 kHz mono, runs
|
||||
// whisper.cpp inference on a background thread, and dispatches the resulting
|
||||
// transcript text to registered MessageBuffers.
|
||||
class SpeechRecognizer {
|
||||
public:
|
||||
SpeechRecognizer();
|
||||
~SpeechRecognizer();
|
||||
|
||||
bool LoadModel(const std::string &modelPath);
|
||||
|
||||
bool StartCapture(obs_source_t *source);
|
||||
void StopCapture();
|
||||
|
||||
void SetBufferDuration(double seconds);
|
||||
void SetNThreads(int n);
|
||||
void SetLanguage(const std::string &lang);
|
||||
void SetTranslate(bool translate);
|
||||
void SetVadEnergyThreshold(float threshold);
|
||||
void SetSuppressNonSpeechTokens(bool suppress);
|
||||
void SetNoContext(bool noContext);
|
||||
void SetListenWhenMuted(bool listen);
|
||||
void SetUseGpu(bool useGpu);
|
||||
|
||||
[[nodiscard]] std::shared_ptr<MessageBuffer<std::string>>
|
||||
RegisterClient();
|
||||
|
||||
private:
|
||||
static void AudioCaptureCallback(void *param, obs_source_t *source,
|
||||
const struct audio_data *audio,
|
||||
bool muted);
|
||||
void AppendResampledAudio(const struct audio_data *audio);
|
||||
void InferenceLoop();
|
||||
|
||||
// Held during whisper_full and when freeing/replacing _ctx.
|
||||
std::mutex _ctxMutex;
|
||||
whisper_context *_ctx = nullptr;
|
||||
// Stored as void* to avoid pulling <media-io/audio-resampler.h> into
|
||||
// this header. Cast to audio_resampler_t* in the .cpp.
|
||||
void *_resampler = nullptr;
|
||||
|
||||
std::vector<float> _audioBuffer;
|
||||
std::mutex _audioMutex;
|
||||
double _bufferDurationSeconds = 5.0;
|
||||
float _vadEnergyThreshold = 1e-4f;
|
||||
size_t _framesSinceLastStep = 0;
|
||||
|
||||
std::vector<float> _inferenceBuffer;
|
||||
std::thread _inferenceThread;
|
||||
std::atomic_bool _stopThread{false};
|
||||
std::condition_variable _inferenceCV;
|
||||
std::mutex _inferenceMutex;
|
||||
bool _bufferReady = false;
|
||||
int _nThreads = 4;
|
||||
std::string _language = "auto";
|
||||
bool _translate = false;
|
||||
bool _suppressNonSpeechTokens = true;
|
||||
bool _noContext = true;
|
||||
bool _listenWhenMuted = false;
|
||||
bool _useGpu = true;
|
||||
|
||||
OBSWeakSource _captureSource;
|
||||
int _sourceSampleRate = 44100;
|
||||
int _sourceChannelCount = 2;
|
||||
|
||||
MessageDispatcher<std::string> _dispatcher;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -15,7 +15,7 @@ if(NOT TARGET httplib)
|
||||
EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
if(OS_MACOS)
|
||||
if(OS_MACOS OR OS_WINDOWS)
|
||||
set(OPENSSL_USE_STATIC_LIBS
|
||||
ON
|
||||
CACHE BOOL "Use static OpenSSL" FORCE)
|
||||
@@ -25,6 +25,14 @@ if(NOT OPENSSL_FOUND)
|
||||
message(WARNING "OpenSSL not found!\n" "Twitch support will be disabled!\n\n")
|
||||
return()
|
||||
endif()
|
||||
if(OS_WINDOWS AND NOT OPENSSL_CRYPTO_LIBRARY MATCHES "_static\\.lib$")
|
||||
message(
|
||||
WARNING
|
||||
"Static OpenSSL libraries (libcrypto_static.lib / libssl_static.lib) not found!\n"
|
||||
"Twitch support will be disabled to avoid DLL name collisions with other plugins.\n\n"
|
||||
)
|
||||
return()
|
||||
endif()
|
||||
|
||||
find_package(ZLIB)
|
||||
if(NOT ZLIB_FOUND)
|
||||
@@ -90,30 +98,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE "${CPP_HTTPLIB_DIR}/"
|
||||
"${OPENSSL_INCLUDE_DIR}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ${OPENSSL_LIBRARIES} ZLIB::ZLIB)
|
||||
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
if(OS_WINDOWS)
|
||||
# Couldn't really find a better way to install runtime dependencies for
|
||||
# Windows TODO: Clean this up at some point
|
||||
function(FIND_FILES_WITH_PATTERN result pattern dir)
|
||||
execute_process(
|
||||
COMMAND
|
||||
powershell -Command
|
||||
"Get-ChildItem -Path '${dir}' -Recurse -Include ${pattern} |"
|
||||
"Select-Object -First 1 |"
|
||||
"ForEach-Object { $_.FullName -replace '\\\\', '\\\\' }"
|
||||
OUTPUT_VARIABLE files
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
set(${result}
|
||||
${files}
|
||||
PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(OPENSSL_DLL_SEARCH_DIR "${OPENSSL_INCLUDE_DIR}/..")
|
||||
find_files_with_pattern(CRYPTO_DLL_FILES "libcrypto*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
find_files_with_pattern(SSL_DLL_FILES "libssl*.dll"
|
||||
"${OPENSSL_DLL_SEARCH_DIR}")
|
||||
install_advss_plugin_dependency(TARGET ${PROJECT_NAME} DEPENDENCIES
|
||||
"${CRYPTO_DLL_FILES}" "${SSL_DLL_FILES}")
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32 bcrypt)
|
||||
endif()
|
||||
install_advss_plugin(${PROJECT_NAME})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "chat-connection.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "token.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "content-classification.hpp"
|
||||
|
||||
#include "channel-selection.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
#include "event-sub.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "token.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
#include "twitch-timestamp.hpp"
|
||||
|
||||
#include <log-helper.hpp>
|
||||
#include <plugin-state-helpers.hpp>
|
||||
#include "twitch-timestamp.hpp"
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "tag-selection.hpp"
|
||||
|
||||
#include "channel-selection.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <QHBoxLayout>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <QRegularExpression>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
void TwitchTagList::Load(obs_data_t *obj)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#include "token.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
#include <obs-module-helper.hpp>
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QGroupBox>
|
||||
#include <QScrollArea>
|
||||
#include <QUrl>
|
||||
|
||||
#include <help-icon.hpp>
|
||||
#include <layout-helpers.hpp>
|
||||
#include <log-helper.hpp>
|
||||
#include <macro-export-extensions.hpp>
|
||||
#include <obs-module-helper.hpp>
|
||||
#include <plugin-state-helpers.hpp>
|
||||
#include <QDesktopServices>
|
||||
#include <QGroupBox>
|
||||
#include <QScrollArea>
|
||||
#include <QUrl>
|
||||
#include <ui-helpers.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "twitch-helpers.hpp"
|
||||
|
||||
#include "log-helper.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "token.hpp"
|
||||
|
||||
|
||||
@@ -30,10 +30,29 @@ if(OS_LINUX AND NOT Leptonica_FOUND)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- Check cascade classifier availability ---
|
||||
# CascadeClassifier was moved to the xobjdetect contrib module in OpenCV 5
|
||||
if(OpenCV_VERSION_MAJOR LESS 5
|
||||
OR OpenCV_xobjdetect_FOUND
|
||||
OR "opencv_xobjdetect" IN_LIST OpenCV_LIBS)
|
||||
set(ADVSS_CASCADE_SUPPORT ON)
|
||||
else()
|
||||
message(
|
||||
WARNING
|
||||
"Cascade classifier support disabled!\n"
|
||||
"OpenCV 5+ requires the opencv_xobjdetect contrib module.\n\n"
|
||||
"Rebuild OpenCV with -DBUILD_LIST=...,xobjdetect and -DOPENCV_EXTRA_MODULES_PATH=<opencv_contrib>/modules"
|
||||
)
|
||||
endif()
|
||||
|
||||
# --- End of section ---
|
||||
|
||||
add_library(${PROJECT_NAME} MODULE)
|
||||
|
||||
if(ADVSS_CASCADE_SUPPORT)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE ADVSS_CASCADE_SUPPORT)
|
||||
endif()
|
||||
|
||||
if(Leptonica_FOUND AND Tesseract_FOUND)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE OCR_SUPPORT)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE Tesseract::libtesseract
|
||||
@@ -54,8 +73,11 @@ target_sources(
|
||||
${PROJECT_NAME}
|
||||
PRIVATE area-selection.cpp
|
||||
area-selection.hpp
|
||||
cascade-classifier-detector.cpp
|
||||
cascade-classifier-detector.hpp
|
||||
macro-condition-video.cpp
|
||||
macro-condition-video.hpp
|
||||
object-detector.hpp
|
||||
opencv-helpers.cpp
|
||||
opencv-helpers.hpp
|
||||
parameter-wrappers.cpp
|
||||
@@ -63,7 +85,12 @@ target_sources(
|
||||
preview-dialog.cpp
|
||||
preview-dialog.hpp
|
||||
screenshot-dialog.cpp
|
||||
screenshot-dialog.hpp)
|
||||
screenshot-dialog.hpp
|
||||
video-edit-area.cpp
|
||||
video-edit-brightness.cpp
|
||||
video-edit-color.cpp
|
||||
video-edit-object.cpp
|
||||
video-edit-ocr.cpp)
|
||||
|
||||
setup_advss_plugin(${PROJECT_NAME})
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "area-selection.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <obs-module-helper.hpp>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
#include <variable-spinbox.hpp>
|
||||
#include "variable-spinbox.hpp"
|
||||
|
||||
#include <QWidget>
|
||||
#include <obs-data.h>
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct Size {
|
||||
|
||||
109
plugins/video/cascade-classifier-detector.cpp
Normal file
109
plugins/video/cascade-classifier-detector.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
#include "cascade-classifier-detector.hpp"
|
||||
|
||||
#ifdef ADVSS_CASCADE_SUPPORT
|
||||
|
||||
#include "opencv-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
|
||||
#if CV_VERSION_MAJOR < 5
|
||||
#include <opencv2/objdetect.hpp>
|
||||
#else
|
||||
#include <opencv2/xobjdetect.hpp>
|
||||
#endif
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct CascadeClassifierDetector::Impl {
|
||||
cv::CascadeClassifier cascade;
|
||||
};
|
||||
|
||||
CascadeClassifierDetector::CascadeClassifierDetector()
|
||||
: _impl(std::make_unique<Impl>())
|
||||
{
|
||||
}
|
||||
|
||||
CascadeClassifierDetector::~CascadeClassifierDetector() = default;
|
||||
|
||||
bool CascadeClassifierDetector::IsSupported()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CascadeClassifierDetector::Load(const std::string &modelPath)
|
||||
{
|
||||
try {
|
||||
if (!_impl->cascade.load(modelPath)) {
|
||||
blog(LOG_WARNING, "failed to load cascade model \"%s\"",
|
||||
modelPath.c_str());
|
||||
return false;
|
||||
}
|
||||
} catch (...) {
|
||||
blog(LOG_WARNING, "failed to load cascade model \"%s\"",
|
||||
modelPath.c_str());
|
||||
return false;
|
||||
}
|
||||
return !_impl->cascade.empty();
|
||||
}
|
||||
|
||||
bool CascadeClassifierDetector::IsLoaded() const
|
||||
{
|
||||
return !_impl->cascade.empty();
|
||||
}
|
||||
|
||||
std::vector<cv::Rect> CascadeClassifierDetector::Detect(QImage &img)
|
||||
{
|
||||
if (img.isNull() || _impl->cascade.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto image = QImageToMat(img);
|
||||
cv::Mat frameGray;
|
||||
cv::cvtColor(image, frameGray, cv::COLOR_RGBA2GRAY);
|
||||
cv::equalizeHist(frameGray, frameGray);
|
||||
|
||||
std::vector<cv::Rect> objects;
|
||||
try {
|
||||
_impl->cascade.detectMultiScale(frameGray, objects, scaleFactor,
|
||||
minNeighbors, 0, minSize,
|
||||
maxSize);
|
||||
} catch (const std::exception &e) {
|
||||
vblog(LOG_INFO, "detectMultiScale failed: %s", e.what());
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
#else // ADVSS_CASCADE_SUPPORT
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct CascadeClassifierDetector::Impl {};
|
||||
|
||||
CascadeClassifierDetector::CascadeClassifierDetector()
|
||||
: _impl(std::make_unique<Impl>())
|
||||
{
|
||||
}
|
||||
|
||||
CascadeClassifierDetector::~CascadeClassifierDetector() = default;
|
||||
|
||||
bool CascadeClassifierDetector::IsSupported()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool CascadeClassifierDetector::Load(const std::string &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool CascadeClassifierDetector::IsLoaded() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
std::vector<cv::Rect> CascadeClassifierDetector::Detect(QImage &)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
|
||||
#endif // ADVSS_CASCADE_SUPPORT
|
||||
27
plugins/video/cascade-classifier-detector.hpp
Normal file
27
plugins/video/cascade-classifier-detector.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include "object-detector.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class CascadeClassifierDetector : public ObjectDetector {
|
||||
public:
|
||||
CascadeClassifierDetector();
|
||||
~CascadeClassifierDetector() override;
|
||||
|
||||
static bool IsSupported();
|
||||
bool Load(const std::string &modelPath) override;
|
||||
bool IsLoaded() const override;
|
||||
std::vector<cv::Rect> Detect(QImage &img) override;
|
||||
|
||||
double scaleFactor = 1.1;
|
||||
int minNeighbors = 3;
|
||||
cv::Size minSize{0, 0};
|
||||
cv::Size maxSize{0, 0};
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> _impl;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -1,17 +1,15 @@
|
||||
#include "macro-condition-video.hpp"
|
||||
#include "cascade-classifier-detector.hpp"
|
||||
#include "screenshot-dialog.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "selection-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <layout-helpers.hpp>
|
||||
#include <macro-condition-edit.hpp>
|
||||
#include <plugin-state-helpers.hpp>
|
||||
#include <QBuffer>
|
||||
#include <QDesktopServices>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QtGlobal>
|
||||
#include <QToolTip>
|
||||
#include <ui-helpers.hpp>
|
||||
#include <selection-helpers.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -22,30 +20,6 @@ bool MacroConditionVideo::_registered = MacroConditionFactory::Register(
|
||||
{MacroConditionVideo::Create, MacroConditionVideoEdit::Create,
|
||||
"AdvSceneSwitcher.condition.video"});
|
||||
|
||||
const static std::map<VideoCondition, std::string> conditionTypes = {
|
||||
{VideoCondition::MATCH,
|
||||
"AdvSceneSwitcher.condition.video.condition.match"},
|
||||
{VideoCondition::DIFFER,
|
||||
"AdvSceneSwitcher.condition.video.condition.differ"},
|
||||
{VideoCondition::HAS_NOT_CHANGED,
|
||||
"AdvSceneSwitcher.condition.video.condition.hasNotChanged"},
|
||||
{VideoCondition::HAS_CHANGED,
|
||||
"AdvSceneSwitcher.condition.video.condition.hasChanged"},
|
||||
{VideoCondition::NO_IMAGE,
|
||||
"AdvSceneSwitcher.condition.video.condition.noImage"},
|
||||
{VideoCondition::PATTERN,
|
||||
"AdvSceneSwitcher.condition.video.condition.pattern"},
|
||||
{VideoCondition::OBJECT,
|
||||
"AdvSceneSwitcher.condition.video.condition.object"},
|
||||
{VideoCondition::BRIGHTNESS,
|
||||
"AdvSceneSwitcher.condition.video.condition.brightness"},
|
||||
#ifdef OCR_SUPPORT
|
||||
{VideoCondition::OCR, "AdvSceneSwitcher.condition.video.condition.ocr"},
|
||||
#endif
|
||||
{VideoCondition::COLOR,
|
||||
"AdvSceneSwitcher.condition.video.condition.color"},
|
||||
};
|
||||
|
||||
const static std::map<VideoInput::Type, std::string> videoInputTypes = {
|
||||
{VideoInput::Type::OBS_MAIN_OUTPUT,
|
||||
"AdvSceneSwitcher.condition.video.type.main"},
|
||||
@@ -64,27 +38,6 @@ const static std::map<cv::TemplateMatchModes, std::string> patternMatchModes = {
|
||||
"AdvSceneSwitcher.condition.video.patternMatchMode.squaredDifference"},
|
||||
};
|
||||
|
||||
const static std::map<tesseract::PageSegMode, std::string> pageSegModes = {
|
||||
{tesseract::PageSegMode::PSM_SINGLE_COLUMN,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleColumn"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_BLOCK_VERT_TEXT,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_BLOCK,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleBlock"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_LINE,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleLine"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_WORD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleWord"},
|
||||
{tesseract::PageSegMode::PSM_CIRCLE_WORD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.circleWord"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_CHAR,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleChar"},
|
||||
{tesseract::PageSegMode::PSM_SPARSE_TEXT,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.sparseText"},
|
||||
{tesseract::PageSegMode::PSM_SPARSE_TEXT_OSD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.sparseTextOSD"},
|
||||
};
|
||||
|
||||
static bool requiresFileInput(VideoCondition t)
|
||||
{
|
||||
return t == VideoCondition::MATCH || t == VideoCondition::DIFFER ||
|
||||
@@ -94,7 +47,7 @@ static bool requiresFileInput(VideoCondition t)
|
||||
bool MacroConditionVideo::CheckShouldBeSkipped()
|
||||
{
|
||||
if (_condition != VideoCondition::PATTERN &&
|
||||
_condition != VideoCondition::OBJECT &&
|
||||
_condition != VideoCondition::OBJECT_CASCADE &&
|
||||
_condition != VideoCondition::HAS_CHANGED &&
|
||||
_condition != VideoCondition::HAS_NOT_CHANGED) {
|
||||
return false;
|
||||
@@ -185,7 +138,7 @@ bool MacroConditionVideo::Save(obs_data_t *obj) const
|
||||
_blockUntilScreenshotDone);
|
||||
_brightnessThreshold.Save(obj, "brightnessThreshold");
|
||||
_patternMatchParameters.Save(obj);
|
||||
_objMatchParameters.Save(obj);
|
||||
_cascadeMatchParameters.Save(obj);
|
||||
_ocrParameters.Save(obj);
|
||||
_colorParameters.Save(obj);
|
||||
obs_data_set_bool(obj, "throttleEnabled", _throttleEnabled);
|
||||
@@ -211,7 +164,7 @@ bool MacroConditionVideo::Load(obs_data_t *obj)
|
||||
_brightnessThreshold.Load(obj, "brightnessThreshold");
|
||||
}
|
||||
_patternMatchParameters.Load(obj);
|
||||
_objMatchParameters.Load(obj);
|
||||
_cascadeMatchParameters.Load(obj);
|
||||
_ocrParameters.Load(obj);
|
||||
_colorParameters.Load(obj);
|
||||
_throttleEnabled = obs_data_get_bool(obj, "throttleEnabled");
|
||||
@@ -378,15 +331,11 @@ bool MacroConditionVideo::OutputChanged()
|
||||
|
||||
bool MacroConditionVideo::ScreenshotContainsObject()
|
||||
{
|
||||
auto model = _objMatchParameters.GetModel();
|
||||
if (!model) {
|
||||
auto *detector = _cascadeMatchParameters.GetDetector();
|
||||
if (!detector) {
|
||||
return false;
|
||||
}
|
||||
auto objects = MatchObject(_screenshotData.GetImage(), *model,
|
||||
_objMatchParameters.scaleFactor,
|
||||
_objMatchParameters.minNeighbors,
|
||||
_objMatchParameters.minSize.CV(),
|
||||
_objMatchParameters.maxSize.CV());
|
||||
auto objects = detector->Detect(_screenshotData.GetImage());
|
||||
const auto count = objects.size();
|
||||
SetTempVarValue("objectCount", std::to_string(count));
|
||||
return count > 0;
|
||||
@@ -463,7 +412,7 @@ bool MacroConditionVideo::Compare()
|
||||
return _screenshotData.GetImage().isNull();
|
||||
case VideoCondition::PATTERN:
|
||||
return ScreenshotContainsPattern();
|
||||
case VideoCondition::OBJECT:
|
||||
case VideoCondition::OBJECT_CASCADE:
|
||||
return ScreenshotContainsObject();
|
||||
case VideoCondition::BRIGHTNESS:
|
||||
return CheckBrightnessThreshold();
|
||||
@@ -531,7 +480,7 @@ void MacroConditionVideo::SetupTempVars()
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.tempVar.video.matchHeight.description"));
|
||||
break;
|
||||
case VideoCondition::OBJECT:
|
||||
case VideoCondition::OBJECT_CASCADE:
|
||||
AddTempvar(
|
||||
"objectCount",
|
||||
obs_module_text(
|
||||
@@ -584,18 +533,41 @@ static inline void populateVideoInputSelection(QComboBox *list)
|
||||
|
||||
static inline void populateConditionSelection(QComboBox *list)
|
||||
{
|
||||
const static std::vector<std::pair<VideoCondition, std::string>>
|
||||
conditionTypes = {
|
||||
{VideoCondition::MATCH,
|
||||
"AdvSceneSwitcher.condition.video.condition.match"},
|
||||
{VideoCondition::DIFFER,
|
||||
"AdvSceneSwitcher.condition.video.condition.differ"},
|
||||
{VideoCondition::HAS_NOT_CHANGED,
|
||||
"AdvSceneSwitcher.condition.video.condition.hasNotChanged"},
|
||||
{VideoCondition::HAS_CHANGED,
|
||||
"AdvSceneSwitcher.condition.video.condition.hasChanged"},
|
||||
{VideoCondition::NO_IMAGE,
|
||||
"AdvSceneSwitcher.condition.video.condition.noImage"},
|
||||
{VideoCondition::PATTERN,
|
||||
"AdvSceneSwitcher.condition.video.condition.pattern"},
|
||||
{VideoCondition::OBJECT_CASCADE,
|
||||
"AdvSceneSwitcher.condition.video.condition.object"},
|
||||
{VideoCondition::BRIGHTNESS,
|
||||
"AdvSceneSwitcher.condition.video.condition.brightness"},
|
||||
#ifdef OCR_SUPPORT
|
||||
{VideoCondition::OCR,
|
||||
"AdvSceneSwitcher.condition.video.condition.ocr"},
|
||||
#endif
|
||||
{VideoCondition::COLOR,
|
||||
"AdvSceneSwitcher.condition.video.condition.color"},
|
||||
};
|
||||
|
||||
for (auto &[value, name] : conditionTypes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(value));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void populatePageSegModeSelection(QComboBox *list)
|
||||
{
|
||||
for (const auto &[mode, name] : pageSegModes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(mode));
|
||||
}
|
||||
SetRowVisibleByValue(
|
||||
list,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.condition.object"),
|
||||
CascadeClassifierDetector::IsSupported());
|
||||
}
|
||||
|
||||
static inline void populatePatternMatchModeSelection(QComboBox *list)
|
||||
@@ -606,628 +578,6 @@ static inline void populatePatternMatchModeSelection(QComboBox *list)
|
||||
}
|
||||
}
|
||||
|
||||
BrightnessEdit::BrightnessEdit(QWidget *parent,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_threshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.brightnessThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.brightnessThresholdDescription"))),
|
||||
_current(new QLabel),
|
||||
_entryData(data)
|
||||
{
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(_threshold);
|
||||
layout->addWidget(_current);
|
||||
setLayout(layout);
|
||||
|
||||
QWidget::connect(
|
||||
_threshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(BrightnessThresholdChanged(
|
||||
const NumberVariable<double> &)));
|
||||
QWidget::connect(&_timer, &QTimer::timeout, this,
|
||||
&BrightnessEdit::UpdateCurrentBrightness);
|
||||
_timer.start(1000);
|
||||
|
||||
_threshold->SetDoubleValue(_entryData->_brightnessThreshold);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void BrightnessEdit::UpdateCurrentBrightness()
|
||||
{
|
||||
QString text = obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.currentBrightness");
|
||||
_current->setText(text.arg(_entryData->GetCurrentBrightness()));
|
||||
}
|
||||
|
||||
void BrightnessEdit::BrightnessThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_brightnessThreshold = value;
|
||||
}
|
||||
|
||||
static void openFileInEditor(const std::string &filepath)
|
||||
{
|
||||
const auto path = QString::fromStdString(filepath);
|
||||
const QFileInfo fileInfo(path);
|
||||
if (!fileInfo.exists()) {
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrOpenConfig.createFailed"));
|
||||
return;
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
QUrl fileUrl = QUrl::fromLocalFile(path);
|
||||
if (!QDesktopServices::openUrl(fileUrl)) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrOpenConfig.openFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
OCREdit::OCREdit(QWidget *parent, PreviewDialog *previewDialog,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_matchText(new VariableTextEdit(this)),
|
||||
_regex(new RegexConfigWidget(this)),
|
||||
_colorButton(new VariableColorButton(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectColor"))),
|
||||
_colorThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
|
||||
true)),
|
||||
_pageSegMode(new QComboBox()),
|
||||
_tesseractBaseDir(new FileSelection(FileSelection::Type::FOLDER)),
|
||||
_languageCode(new VariableLineEdit(this)),
|
||||
_useConfig(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrUseConfigFile"))),
|
||||
_configFile(new FileSelection(FileSelection::Type::WRITE, this)),
|
||||
_openConfigFile(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrOpenConfigFile"))),
|
||||
_reloadConfig(new QPushButton()),
|
||||
_configLayout(new QHBoxLayout()),
|
||||
_previewDialog(previewDialog),
|
||||
_entryData(data)
|
||||
{
|
||||
populatePageSegModeSelection(_pageSegMode);
|
||||
|
||||
_reloadConfig->setMaximumWidth(22);
|
||||
SetButtonIcon(_reloadConfig, GetThemeTypeName() == "Light"
|
||||
? ":res/images/refresh.svg"
|
||||
: "theme:Dark/refresh.svg");
|
||||
_reloadConfig->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrConfigReload"));
|
||||
|
||||
QWidget::connect(_colorButton,
|
||||
SIGNAL(ColorVariableChanged(const ColorVariable &)),
|
||||
this, SLOT(ColorChanged(const ColorVariable &)));
|
||||
QWidget::connect(
|
||||
_colorThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(_matchText, SIGNAL(textChanged()), this,
|
||||
SLOT(MatchTextChanged()));
|
||||
QWidget::connect(_regex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(RegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_pageSegMode, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(PageSegModeChanged(int)));
|
||||
QWidget::connect(_tesseractBaseDir,
|
||||
SIGNAL(PathChanged(const QString &)), this,
|
||||
SLOT(TesseractBaseDirChanged(const QString &)));
|
||||
QWidget::connect(_languageCode, SIGNAL(editingFinished()), this,
|
||||
SLOT(LanguageChanged()));
|
||||
QWidget::connect(_useConfig, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseConfigChanged(int)));
|
||||
QWidget::connect(_configFile, SIGNAL(PathChanged(const QString &)),
|
||||
this, SLOT(ConfigFileChanged(const QString &)));
|
||||
QWidget::connect(_openConfigFile, &QPushButton::clicked, [this](bool) {
|
||||
openFileInEditor(
|
||||
_entryData->_ocrParameters.GetCustomConfigFile());
|
||||
});
|
||||
QWidget::connect(_reloadConfig, &QPushButton::clicked, [this](bool) {
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.EnableCustomConfig(true);
|
||||
_previewDialog->OCRParametersChanged(
|
||||
_entryData->_ocrParameters);
|
||||
});
|
||||
|
||||
auto configFileHint = new QLabel();
|
||||
const QString path = GetThemeTypeName() == "Light"
|
||||
? ":/res/images/help.svg"
|
||||
: ":/res/images/help_light.svg";
|
||||
const QIcon icon(path);
|
||||
const QPixmap pixmap = icon.pixmap(QSize(16, 16));
|
||||
configFileHint->setPixmap(pixmap);
|
||||
configFileHint->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrConfigHint"));
|
||||
|
||||
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{color}}", _colorButton},
|
||||
{"{{textType}}", _pageSegMode},
|
||||
{"{{tesseractBaseDir}}", _tesseractBaseDir},
|
||||
{"{{languageCode}}", _languageCode},
|
||||
{"{{configFile}}", _configFile},
|
||||
{"{{openConfigFile}}", _openConfigFile},
|
||||
{"{{reloadConfig}}", _reloadConfig},
|
||||
{"{{configFileHint}}", configFileHint},
|
||||
};
|
||||
|
||||
auto layout = new QVBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
auto textLayout = new QHBoxLayout();
|
||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
||||
textLayout->addWidget(_matchText);
|
||||
textLayout->addWidget(_regex);
|
||||
layout->addLayout(textLayout);
|
||||
auto pageModeSegLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrTextType"),
|
||||
pageModeSegLayout, widgetPlaceholders);
|
||||
layout->addLayout(pageModeSegLayout);
|
||||
auto baseDirLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrBaseDir"),
|
||||
baseDirLayout, widgetPlaceholders, false);
|
||||
layout->addLayout(baseDirLayout);
|
||||
auto languageLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrLanguage"),
|
||||
languageLayout, widgetPlaceholders);
|
||||
layout->addLayout(languageLayout);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrConfig"),
|
||||
_configLayout, widgetPlaceholders, false);
|
||||
layout->addWidget(_useConfig);
|
||||
layout->addLayout(_configLayout);
|
||||
auto colorPickLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrColorPick"),
|
||||
colorPickLayout, widgetPlaceholders);
|
||||
layout->addLayout(colorPickLayout);
|
||||
layout->addWidget(_colorThreshold);
|
||||
setLayout(layout);
|
||||
|
||||
_matchText->setPlainText(_entryData->_ocrParameters.text);
|
||||
_regex->SetRegexConfig(_entryData->_ocrParameters.regex);
|
||||
_colorButton->SetValue(_entryData->_ocrParameters.color);
|
||||
_colorThreshold->SetDoubleValue(
|
||||
_entryData->_ocrParameters.colorThreshold);
|
||||
_pageSegMode->setCurrentIndex(_pageSegMode->findData(
|
||||
static_cast<int>(_entryData->_ocrParameters.GetPageMode())));
|
||||
_tesseractBaseDir->SetPath(
|
||||
_entryData->_ocrParameters.GetTesseractBasePath());
|
||||
_languageCode->setText(_entryData->_ocrParameters.GetLanguageCode());
|
||||
_useConfig->setChecked(
|
||||
_entryData->_ocrParameters.CustomConfigIsEnabled());
|
||||
_configFile->SetPath(_entryData->_ocrParameters.GetCustomConfigFile());
|
||||
SetLayoutVisible(_configLayout,
|
||||
_entryData->_ocrParameters.CustomConfigIsEnabled());
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void OCREdit::ColorChanged(const ColorVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.color = value;
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::ColorThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.colorThreshold = value;
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::MatchTextChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.text =
|
||||
_matchText->toPlainText().toUtf8().constData();
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::RegexChanged(const RegexConfig &conf)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.regex = conf;
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::PageSegModeChanged(int idx)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetPageSegMode(static_cast<tesseract::PageSegMode>(
|
||||
_pageSegMode->itemData(idx).toInt()));
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::TesseractBaseDirChanged(const QString &path)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
if (!_entryData->SetTesseractBaseDir(path.toStdString())) {
|
||||
const QString message(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
|
||||
const QDir dataDir(path);
|
||||
const QString fileName(_languageCode->text() + ".traineddata");
|
||||
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
|
||||
|
||||
// Reset to previous value
|
||||
const QSignalBlocker b(this);
|
||||
_tesseractBaseDir->SetPath(
|
||||
_entryData->_ocrParameters.GetTesseractBasePath());
|
||||
return;
|
||||
}
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::LanguageChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
if (!_entryData->SetLanguageCode(_languageCode->text().toStdString())) {
|
||||
const QString message(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
|
||||
const QDir dataDir(QString::fromStdString(
|
||||
_entryData->_ocrParameters.GetTesseractBasePath()));
|
||||
const QString fileName(_languageCode->text() + ".traineddata");
|
||||
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
|
||||
|
||||
// Reset to previous value
|
||||
const QSignalBlocker b(this);
|
||||
_languageCode->setText(
|
||||
_entryData->_ocrParameters.GetLanguageCode());
|
||||
return;
|
||||
}
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::UseConfigChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.EnableCustomConfig(value);
|
||||
SetLayoutVisible(_configLayout, value);
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::ConfigFileChanged(const QString &path)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.SetCustomConfigFile(path.toStdString());
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
ObjectDetectEdit::ObjectDetectEdit(
|
||||
QWidget *parent, PreviewDialog *previewDialog,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_modelDataPath(new FileSelection()),
|
||||
_objectScaleThreshold(new SliderSpinBox(
|
||||
1.1, 5.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectScaleThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectScaleThresholdDescription"))),
|
||||
_minNeighbors(new QSpinBox()),
|
||||
_minNeighborsDescription(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.minNeighborDescription"))),
|
||||
_minSize(new SizeSelection(0, 1024)),
|
||||
_maxSize(new SizeSelection(0, 4096)),
|
||||
_previewDialog(previewDialog),
|
||||
_entryData(data)
|
||||
{
|
||||
_minNeighbors->setMinimum(minMinNeighbors);
|
||||
_minNeighbors->setMaximum(maxMinNeighbors);
|
||||
|
||||
QWidget::connect(
|
||||
_objectScaleThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(ObjectScaleThresholdChanged(
|
||||
const NumberVariable<double> &)));
|
||||
QWidget::connect(_minNeighbors, SIGNAL(valueChanged(int)), this,
|
||||
SLOT(MinNeighborsChanged(int)));
|
||||
QWidget::connect(_minSize, SIGNAL(SizeChanged(Size)), this,
|
||||
SLOT(MinSizeChanged(Size)));
|
||||
QWidget::connect(_maxSize, SIGNAL(SizeChanged(Size)), this,
|
||||
SLOT(MaxSizeChanged(Size)));
|
||||
QWidget::connect(_modelDataPath, SIGNAL(PathChanged(const QString &)),
|
||||
this, SLOT(ModelPathChanged(const QString &)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{minNeighbors}}", _minNeighbors},
|
||||
{"{{minSize}}", _minSize},
|
||||
{"{{maxSize}}", _maxSize},
|
||||
{"{{modelDataPath}}", _modelDataPath},
|
||||
};
|
||||
|
||||
auto pathLayout = new QHBoxLayout;
|
||||
pathLayout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.modelPath"),
|
||||
pathLayout, widgetPlaceholders);
|
||||
|
||||
auto neighborsLayout = new QHBoxLayout;
|
||||
neighborsLayout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.minNeighbor"),
|
||||
neighborsLayout, widgetPlaceholders);
|
||||
|
||||
auto sizeGrid = new QGridLayout;
|
||||
sizeGrid->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.minSize")),
|
||||
0, 0);
|
||||
sizeGrid->addWidget(_minSize, 0, 1);
|
||||
sizeGrid->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.maxSize")),
|
||||
1, 0);
|
||||
sizeGrid->addWidget(_maxSize, 1, 1);
|
||||
auto sizeLayout = new QHBoxLayout;
|
||||
sizeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
sizeLayout->addLayout(sizeGrid);
|
||||
sizeLayout->addStretch();
|
||||
|
||||
auto layout = new QVBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addLayout(pathLayout);
|
||||
layout->addLayout(neighborsLayout);
|
||||
layout->addLayout(sizeLayout);
|
||||
setLayout(layout);
|
||||
|
||||
_modelDataPath->SetPath(_entryData->_objMatchParameters.GetModelPath());
|
||||
_objectScaleThreshold->SetDoubleValue(
|
||||
_entryData->_objMatchParameters.scaleFactor);
|
||||
_minNeighbors->setValue(_entryData->_objMatchParameters.minNeighbors);
|
||||
_minSize->SetSize(_entryData->_objMatchParameters.minSize);
|
||||
_maxSize->SetSize(_entryData->_objMatchParameters.maxSize);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void ObjectDetectEdit::ObjectScaleThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_objMatchParameters.scaleFactor = value;
|
||||
_previewDialog->ObjDetectParametersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
}
|
||||
|
||||
void ObjectDetectEdit::MinNeighborsChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_objMatchParameters.minNeighbors = value;
|
||||
_previewDialog->ObjDetectParametersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
}
|
||||
|
||||
void ObjectDetectEdit::MinSizeChanged(advss::Size value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_objMatchParameters.minSize = value;
|
||||
_previewDialog->ObjDetectParametersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
}
|
||||
|
||||
void ObjectDetectEdit::MaxSizeChanged(advss::Size value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_objMatchParameters.maxSize = value;
|
||||
_previewDialog->ObjDetectParametersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
}
|
||||
|
||||
void ObjectDetectEdit::ModelPathChanged(const QString &text)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool dataLoaded = false;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
std::string path = text.toStdString();
|
||||
dataLoaded = _entryData->_objMatchParameters.SetModelPath(path);
|
||||
}
|
||||
if (!dataLoaded) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.modelLoadFail"));
|
||||
}
|
||||
_previewDialog->ObjDetectParametersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
}
|
||||
|
||||
ColorEdit::ColorEdit(QWidget *parent,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_matchThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorMatchThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorMatchThresholdDescription"),
|
||||
true)),
|
||||
_colorThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
|
||||
true)),
|
||||
_colorButton(new VariableColorButton(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectColor"))),
|
||||
_entryData(data)
|
||||
{
|
||||
QWidget::connect(_colorButton,
|
||||
SIGNAL(ColorVariableChanged(const ColorVariable &)),
|
||||
this, SLOT(ColorChanged(const ColorVariable &)));
|
||||
QWidget::connect(
|
||||
_matchThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(MatchThresholdChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(
|
||||
_colorThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{color}}", _colorButton},
|
||||
};
|
||||
|
||||
auto colorLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.color"),
|
||||
colorLayout, widgetPlaceholders);
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addLayout(colorLayout);
|
||||
layout->addWidget(_colorThreshold);
|
||||
layout->addWidget(_matchThreshold);
|
||||
setLayout(layout);
|
||||
|
||||
_matchThreshold->SetDoubleValue(
|
||||
_entryData->_colorParameters.matchThreshold);
|
||||
_colorThreshold->SetDoubleValue(
|
||||
_entryData->_colorParameters.colorThreshold);
|
||||
_colorButton->SetValue(_entryData->_colorParameters.color);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void ColorEdit::ColorChanged(const ColorVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_colorParameters.color = value;
|
||||
}
|
||||
|
||||
void ColorEdit::MatchThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_colorParameters.matchThreshold = value;
|
||||
}
|
||||
|
||||
void ColorEdit::ColorThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_colorParameters.colorThreshold = value;
|
||||
}
|
||||
|
||||
AreaEdit::AreaEdit(QWidget *parent, PreviewDialog *previewDialog,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_checkAreaEnable(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.checkAreaEnable"))),
|
||||
_checkArea(new AreaSelection(0, 99999)),
|
||||
_selectArea(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectArea"))),
|
||||
_previewDialog(previewDialog),
|
||||
_entryData(data)
|
||||
{
|
||||
QWidget::connect(_checkAreaEnable, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(CheckAreaEnableChanged(int)));
|
||||
QWidget::connect(_checkArea, SIGNAL(AreaChanged(Area)), this,
|
||||
SLOT(CheckAreaChanged(Area)));
|
||||
QWidget::connect(_selectArea, SIGNAL(clicked()), this,
|
||||
SLOT(SelectAreaClicked()));
|
||||
QWidget::connect(_previewDialog, SIGNAL(SelectionAreaChanged(QRect)),
|
||||
this, SLOT(CheckAreaChanged(QRect)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{checkAreaEnable}}", _checkAreaEnable},
|
||||
{"{{checkArea}}", _checkArea},
|
||||
{"{{selectArea}}", _selectArea},
|
||||
};
|
||||
|
||||
auto layout = new QHBoxLayout;
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.checkArea"),
|
||||
layout, widgetPlaceholders);
|
||||
setLayout(layout);
|
||||
|
||||
_checkAreaEnable->setChecked(_entryData->_areaParameters.enable);
|
||||
_checkArea->SetArea(_entryData->_areaParameters.area);
|
||||
SetWidgetVisibility();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void AreaEdit::SetWidgetVisibility()
|
||||
{
|
||||
_checkArea->setVisible(_entryData->_areaParameters.enable);
|
||||
_selectArea->setVisible(_entryData->_areaParameters.enable);
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void AreaEdit::SelectAreaClicked()
|
||||
{
|
||||
_previewDialog->show();
|
||||
_previewDialog->raise();
|
||||
_previewDialog->activateWindow();
|
||||
_previewDialog->SelectArea();
|
||||
}
|
||||
|
||||
void AreaEdit::CheckAreaEnableChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_areaParameters.enable = value;
|
||||
SetWidgetVisibility();
|
||||
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
|
||||
emit Resized();
|
||||
}
|
||||
|
||||
void AreaEdit::CheckAreaChanged(Area value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_areaParameters.area = value;
|
||||
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
|
||||
}
|
||||
|
||||
void AreaEdit::CheckAreaChanged(QRect rect)
|
||||
{
|
||||
const QSignalBlocker b(_checkArea);
|
||||
Area area{rect.topLeft().x(), rect.y(), rect.width(), rect.height()};
|
||||
_checkArea->SetArea(area);
|
||||
CheckAreaChanged(area);
|
||||
}
|
||||
|
||||
static QStringList getVideoSourcesList()
|
||||
{
|
||||
auto sources = GetVideoSourceNames();
|
||||
@@ -1263,7 +613,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
_previewDialog(this),
|
||||
_brightness(new BrightnessEdit(this, entryData)),
|
||||
_ocr(new OCREdit(this, &_previewDialog, entryData)),
|
||||
_objectDetect(new ObjectDetectEdit(this, &_previewDialog, entryData)),
|
||||
_cascadeClassifierEdit(
|
||||
new CascadeClassifierEdit(this, &_previewDialog, entryData)),
|
||||
_color(new ColorEdit(this, entryData)),
|
||||
_area(new AreaEdit(this, &_previewDialog, entryData)),
|
||||
_throttleControlLayout(new QHBoxLayout),
|
||||
@@ -1292,8 +643,8 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
QSizePolicy::Preferred);
|
||||
_ocr->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
QSizePolicy::Preferred);
|
||||
_objectDetect->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
QSizePolicy::Preferred);
|
||||
_cascadeClassifierEdit->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
QSizePolicy::Preferred);
|
||||
_color->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
QSizePolicy::Preferred);
|
||||
_area->setSizePolicy(QSizePolicy::MinimumExpanding,
|
||||
@@ -1392,7 +743,7 @@ MacroConditionVideoEdit::MacroConditionVideoEdit(
|
||||
mainLayout->addLayout(_patternMatchModeLayout);
|
||||
mainLayout->addWidget(_brightness);
|
||||
mainLayout->addWidget(_ocr);
|
||||
mainLayout->addWidget(_objectDetect);
|
||||
mainLayout->addWidget(_cascadeClassifierEdit);
|
||||
mainLayout->addWidget(_color);
|
||||
mainLayout->addLayout(_throttleControlLayout);
|
||||
mainLayout->addWidget(_area);
|
||||
@@ -1634,13 +985,14 @@ void MacroConditionVideoEdit::ShowMatchClicked()
|
||||
static bool needsShowMatch(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::OBJECT || cond == VideoCondition::OCR;
|
||||
cond == VideoCondition::OBJECT_CASCADE ||
|
||||
cond == VideoCondition::OCR;
|
||||
}
|
||||
|
||||
static bool needsThrottleControls(VideoCondition cond)
|
||||
{
|
||||
return cond == VideoCondition::PATTERN ||
|
||||
cond == VideoCondition::OBJECT ||
|
||||
cond == VideoCondition::OBJECT_CASCADE ||
|
||||
cond == VideoCondition::HAS_CHANGED ||
|
||||
cond == VideoCondition::HAS_NOT_CHANGED;
|
||||
}
|
||||
@@ -1681,8 +1033,8 @@ void MacroConditionVideoEdit::SetWidgetVisibility()
|
||||
VideoCondition::BRIGHTNESS);
|
||||
_showMatch->setVisible(needsShowMatch(_entryData->GetCondition()));
|
||||
_ocr->setVisible(_entryData->GetCondition() == VideoCondition::OCR);
|
||||
_objectDetect->setVisible(_entryData->GetCondition() ==
|
||||
VideoCondition::OBJECT);
|
||||
_cascadeClassifierEdit->setVisible(_entryData->GetCondition() ==
|
||||
VideoCondition::OBJECT_CASCADE);
|
||||
_color->setVisible(_entryData->GetCondition() == VideoCondition::COLOR);
|
||||
SetLayoutVisible(_throttleControlLayout,
|
||||
needsThrottleControls(_entryData->GetCondition()));
|
||||
@@ -1725,8 +1077,8 @@ void MacroConditionVideoEdit::SetupPreviewDialogParams()
|
||||
{
|
||||
_previewDialog.PatternMatchParametersChanged(
|
||||
_entryData->_patternMatchParameters);
|
||||
_previewDialog.ObjDetectParametersChanged(
|
||||
_entryData->_objMatchParameters);
|
||||
_previewDialog.CascadeClassifierParametersChanged(
|
||||
_entryData->_cascadeMatchParameters);
|
||||
_previewDialog.OCRParametersChanged(_entryData->_ocrParameters);
|
||||
_previewDialog.VideoSelectionChanged(_entryData->_video);
|
||||
_previewDialog.AreaParametersChanged(_entryData->_areaParameters);
|
||||
|
||||
@@ -4,18 +4,21 @@
|
||||
#include "parameter-wrappers.hpp"
|
||||
#include "preview-dialog.hpp"
|
||||
|
||||
#include <help-icon.hpp>
|
||||
#include <macro-condition-edit.hpp>
|
||||
#include <file-selection.hpp>
|
||||
#include <screenshot-helper.hpp>
|
||||
#include <slider-spinbox.hpp>
|
||||
#include <source-helpers.hpp>
|
||||
#include <variable-color-button.hpp>
|
||||
#include <variable-line-edit.hpp>
|
||||
#include <variable-text-edit.hpp>
|
||||
#include "help-icon.hpp"
|
||||
#include "section.hpp"
|
||||
#include "macro-condition-edit.hpp"
|
||||
#include "file-selection.hpp"
|
||||
#include "screenshot-helper.hpp"
|
||||
#include "slider-spinbox.hpp"
|
||||
#include "source-helpers.hpp"
|
||||
#include "variable-color-button.hpp"
|
||||
#include "variable-line-edit.hpp"
|
||||
#include "variable-text-edit.hpp"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QLineEdit>
|
||||
#include <QDateTime>
|
||||
#include <QGridLayout>
|
||||
#include <QHBoxLayout>
|
||||
@@ -70,7 +73,7 @@ public:
|
||||
bool _blockUntilScreenshotDone = true;
|
||||
NumberVariable<double> _brightnessThreshold = 0.5;
|
||||
PatternMatchParameters _patternMatchParameters;
|
||||
ObjDetectParameters _objMatchParameters;
|
||||
CascadeClassifierParameters _cascadeMatchParameters;
|
||||
OCRParameters _ocrParameters;
|
||||
ColorParameters _colorParameters;
|
||||
AreaParameters _areaParameters;
|
||||
@@ -174,12 +177,12 @@ private:
|
||||
bool _loading = true;
|
||||
};
|
||||
|
||||
class ObjectDetectEdit : public QWidget {
|
||||
class CascadeClassifierEdit : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ObjectDetectEdit(QWidget *parent, PreviewDialog *,
|
||||
const std::shared_ptr<MacroConditionVideo> &);
|
||||
CascadeClassifierEdit(QWidget *parent, PreviewDialog *,
|
||||
const std::shared_ptr<MacroConditionVideo> &);
|
||||
|
||||
private slots:
|
||||
void ModelPathChanged(const QString &text);
|
||||
@@ -320,7 +323,7 @@ private:
|
||||
|
||||
BrightnessEdit *_brightness;
|
||||
OCREdit *_ocr;
|
||||
ObjectDetectEdit *_objectDetect;
|
||||
CascadeClassifierEdit *_cascadeClassifierEdit;
|
||||
ColorEdit *_color;
|
||||
AreaEdit *_area;
|
||||
|
||||
|
||||
18
plugins/video/object-detector.hpp
Normal file
18
plugins/video/object-detector.hpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
#include <QImage>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace advss {
|
||||
|
||||
struct ObjectDetector {
|
||||
virtual bool Load(const std::string &modelPath) = 0;
|
||||
virtual bool IsLoaded() const = 0;
|
||||
virtual std::vector<cv::Rect> Detect(QImage &img) = 0;
|
||||
virtual ~ObjectDetector() = default;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "opencv-helpers.hpp"
|
||||
|
||||
#include <log-helper.hpp>
|
||||
#include "log-helper.hpp"
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -135,29 +134,6 @@ double MatchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
matchColor);
|
||||
}
|
||||
|
||||
std::vector<cv::Rect> MatchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
const cv::Size &minSize,
|
||||
const cv::Size &maxSize)
|
||||
{
|
||||
if (img.isNull() || cascade.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto image = QImageToMat(img);
|
||||
cv::Mat frameGray;
|
||||
cv::cvtColor(image, frameGray, cv::COLOR_RGBA2GRAY);
|
||||
cv::equalizeHist(frameGray, frameGray);
|
||||
std::vector<cv::Rect> objects;
|
||||
try {
|
||||
cascade.detectMultiScale(frameGray, objects, scaleFactor,
|
||||
minNeighbors, 0, minSize, maxSize);
|
||||
} catch (const std::exception &e) {
|
||||
vblog(LOG_INFO, "detectMultiScale failed: %s", e.what());
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
uchar GetAvgBrightness(QImage &img)
|
||||
{
|
||||
if (img.isNull()) {
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#undef NO // MacOS macro that can conflict with OpenCV
|
||||
#include <cstddef>
|
||||
#include <opencv2/opencv.hpp>
|
||||
#if CV_VERSION_MAJOR >= 5
|
||||
#include <opencv2/xobjdetect.hpp>
|
||||
#endif
|
||||
|
||||
#ifdef OCR_SUPPORT
|
||||
#include <tesseract/baseapi.h>
|
||||
@@ -69,10 +72,6 @@ double MatchPattern(QImage &img, QImage &pattern, double threshold,
|
||||
cv::Mat &result, bool useAlphaAsMask,
|
||||
cv::TemplateMatchModes matchMode);
|
||||
int CountPatternMatches(const cv::Mat &result, const cv::Size &patternSize);
|
||||
std::vector<cv::Rect> MatchObject(QImage &img, cv::CascadeClassifier &cascade,
|
||||
double scaleFactor, int minNeighbors,
|
||||
const cv::Size &minSize,
|
||||
const cv::Size &maxSize);
|
||||
uchar GetAvgBrightness(QImage &img);
|
||||
cv::Mat PreprocessForOCR(const QImage &image, const QColor &color,
|
||||
double colorDiff);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#include "parameter-wrappers.hpp"
|
||||
#include "cascade-classifier-detector.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "source-helpers.hpp"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <source-helpers.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -48,45 +49,6 @@ bool PatternMatchParameters::Load(obs_data_t *obj)
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::shared_ptr<cv::CascadeClassifier>
|
||||
initObjectCascade(std::string &path)
|
||||
{
|
||||
auto cascade = std::make_shared<cv::CascadeClassifier>();
|
||||
try {
|
||||
cascade->load(path);
|
||||
} catch (...) {
|
||||
blog(LOG_WARNING, "failed to load model data \"%s\"",
|
||||
path.c_str());
|
||||
}
|
||||
return cascade;
|
||||
}
|
||||
|
||||
bool ObjDetectParameters::LoadModelData()
|
||||
{
|
||||
const auto path = QString::fromStdString(modelPath);
|
||||
if (!QFileInfo(path).exists(path)) {
|
||||
cascade.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
cascade = initObjectCascade(modelPath);
|
||||
return !cascade->empty();
|
||||
}
|
||||
|
||||
bool ObjDetectParameters::Save(obs_data_t *obj) const
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
obs_data_set_string(data, "modelPath", modelPath.c_str());
|
||||
scaleFactor.Save(data, "scaleFactor");
|
||||
obs_data_set_int(data, "minNeighbors", minNeighbors);
|
||||
minSize.Save(data, "minSize");
|
||||
maxSize.Save(data, "maxSize");
|
||||
obs_data_set_obj(obj, "objectMatchData", data);
|
||||
obs_data_set_int(data, "version", 2);
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool isScaleFactorValid(double scaleFactor)
|
||||
{
|
||||
return scaleFactor > 1.;
|
||||
@@ -98,11 +60,41 @@ static bool isMinNeighborsValid(int minNeighbors)
|
||||
minNeighbors <= maxMinNeighbors;
|
||||
}
|
||||
|
||||
bool ObjDetectParameters::Load(obs_data_t *obj)
|
||||
bool CascadeClassifierParameters::LoadModelData()
|
||||
{
|
||||
const auto path = QString::fromStdString(_modelPath);
|
||||
if (!QFileInfo(path).exists(path)) {
|
||||
_detector.reset();
|
||||
return false;
|
||||
}
|
||||
auto det = std::make_unique<CascadeClassifierDetector>();
|
||||
if (!det->Load(_modelPath)) {
|
||||
_detector.reset();
|
||||
return false;
|
||||
}
|
||||
_detector = std::move(det);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CascadeClassifierParameters::Save(obs_data_t *obj) const
|
||||
{
|
||||
auto data = obs_data_create();
|
||||
obs_data_set_string(data, "modelPath", _modelPath.c_str());
|
||||
scaleFactor.Save(data, "scaleFactor");
|
||||
obs_data_set_int(data, "minNeighbors", minNeighbors);
|
||||
minSize.Save(data, "minSize");
|
||||
maxSize.Save(data, "maxSize");
|
||||
obs_data_set_int(data, "version", 2);
|
||||
obs_data_set_obj(obj, "objectMatchData", data);
|
||||
obs_data_release(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CascadeClassifierParameters::Load(obs_data_t *obj)
|
||||
{
|
||||
// TODO: Remove this fallback in a future version
|
||||
if (!obs_data_has_user_value(obj, "patternMatchData")) {
|
||||
modelPath = obs_data_get_string(obj, "modelDataPath");
|
||||
_modelPath = obs_data_get_string(obj, "modelDataPath");
|
||||
scaleFactor = obs_data_get_double(obj, "scaleFactor");
|
||||
if (!isScaleFactorValid(scaleFactor)) {
|
||||
scaleFactor = 1.1;
|
||||
@@ -116,7 +108,7 @@ bool ObjDetectParameters::Load(obs_data_t *obj)
|
||||
return true;
|
||||
}
|
||||
auto data = obs_data_get_obj(obj, "objectMatchData");
|
||||
modelPath = obs_data_get_string(data, "modelPath");
|
||||
_modelPath = obs_data_get_string(data, "modelPath");
|
||||
scaleFactor.Load(data, "scaleFactor");
|
||||
// TODO: Remove this fallback in a future version
|
||||
if (!obs_data_has_user_value(data, "version")) {
|
||||
@@ -129,11 +121,11 @@ bool ObjDetectParameters::Load(obs_data_t *obj)
|
||||
// which invalidates previously saved default model paths.
|
||||
const std::string oldPrefix =
|
||||
"../../data/obs-plugins/advanced-scene-switcher/res/cascadeClassifiers/";
|
||||
if (modelPath.substr(0, oldPrefix.size()) == oldPrefix) {
|
||||
modelPath = std::string(obs_get_module_data_path(
|
||||
obs_current_module())) +
|
||||
"/res/cascadeClassifiers/" +
|
||||
modelPath.substr(oldPrefix.size());
|
||||
if (_modelPath.substr(0, oldPrefix.size()) == oldPrefix) {
|
||||
_modelPath = std::string(obs_get_module_data_path(
|
||||
obs_current_module())) +
|
||||
"/res/cascadeClassifiers/" +
|
||||
_modelPath.substr(oldPrefix.size());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -147,27 +139,29 @@ bool ObjDetectParameters::Load(obs_data_t *obj)
|
||||
minSize.Load(data, "minSize");
|
||||
maxSize.Load(data, "maxSize");
|
||||
obs_data_release(data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ObjDetectParameters::SetModelPath(const std::string &path)
|
||||
bool CascadeClassifierParameters::SetModelPath(const std::string &path)
|
||||
{
|
||||
modelPath = path;
|
||||
_modelPath = path;
|
||||
return LoadModelData();
|
||||
}
|
||||
|
||||
std::shared_ptr<cv::CascadeClassifier> ObjDetectParameters::GetModel()
|
||||
ObjectDetector *CascadeClassifierParameters::GetDetector()
|
||||
{
|
||||
if (cascade && !cascade->empty()) {
|
||||
return cascade;
|
||||
if (!_detector || !_detector->IsLoaded()) {
|
||||
if (!LoadModelData()) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (!LoadModelData()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return cascade;
|
||||
auto *cascade =
|
||||
static_cast<CascadeClassifierDetector *>(_detector.get());
|
||||
cascade->scaleFactor = scaleFactor;
|
||||
cascade->minNeighbors = minNeighbors;
|
||||
cascade->minSize = minSize.CV();
|
||||
cascade->maxSize = maxSize.CV();
|
||||
return _detector.get();
|
||||
}
|
||||
|
||||
bool AreaParameters::Save(obs_data_t *obj) const
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
#pragma once
|
||||
#include "object-detector.hpp"
|
||||
#include "opencv-helpers.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
#include "area-selection.hpp"
|
||||
#include "source-selection.hpp"
|
||||
#include "scene-selection.hpp"
|
||||
#include "regex-config.hpp"
|
||||
#include "variable-color.hpp"
|
||||
#include "variable-string.hpp"
|
||||
#include "variable-number.hpp"
|
||||
|
||||
#include <source-selection.hpp>
|
||||
#include <scene-selection.hpp>
|
||||
#include <regex-config.hpp>
|
||||
#include <variable-color.hpp>
|
||||
#include <variable-string.hpp>
|
||||
#include <variable-number.hpp>
|
||||
#include <obs.hpp>
|
||||
#include <obs-module.h>
|
||||
|
||||
@@ -27,7 +28,7 @@ enum class VideoCondition {
|
||||
HAS_CHANGED,
|
||||
NO_IMAGE,
|
||||
PATTERN,
|
||||
OBJECT,
|
||||
OBJECT_CASCADE,
|
||||
BRIGHTNESS,
|
||||
OCR,
|
||||
COLOR,
|
||||
@@ -64,14 +65,37 @@ public:
|
||||
NumberVariable<double> threshold = 0.999;
|
||||
};
|
||||
|
||||
class ObjDetectParameters {
|
||||
class CascadeClassifierParameters {
|
||||
public:
|
||||
bool Save(obs_data_t *obj) const;
|
||||
bool Load(obs_data_t *obj);
|
||||
|
||||
CascadeClassifierParameters() = default;
|
||||
CascadeClassifierParameters(const CascadeClassifierParameters &other)
|
||||
: scaleFactor(other.scaleFactor),
|
||||
minNeighbors(other.minNeighbors),
|
||||
minSize(other.minSize),
|
||||
maxSize(other.maxSize),
|
||||
_modelPath(other._modelPath)
|
||||
{
|
||||
}
|
||||
CascadeClassifierParameters &
|
||||
operator=(const CascadeClassifierParameters &other)
|
||||
{
|
||||
if (this != &other) {
|
||||
scaleFactor = other.scaleFactor;
|
||||
minNeighbors = other.minNeighbors;
|
||||
minSize = other.minSize;
|
||||
maxSize = other.maxSize;
|
||||
_modelPath = other._modelPath;
|
||||
_detector.reset();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool SetModelPath(const std::string &path);
|
||||
const std::string &GetModelPath() const { return modelPath; }
|
||||
std::shared_ptr<cv::CascadeClassifier> GetModel();
|
||||
const std::string &GetModelPath() const { return _modelPath; }
|
||||
ObjectDetector *GetDetector();
|
||||
|
||||
NumberVariable<double> scaleFactor = defaultScaleFactor;
|
||||
int minNeighbors = minMinNeighbors;
|
||||
@@ -81,8 +105,8 @@ public:
|
||||
private:
|
||||
bool LoadModelData();
|
||||
|
||||
std::shared_ptr<cv::CascadeClassifier> cascade;
|
||||
std::string modelPath =
|
||||
std::unique_ptr<ObjectDetector> _detector;
|
||||
std::string _modelPath =
|
||||
obs_get_module_data_path(obs_current_module()) +
|
||||
std::string(
|
||||
"/res/cascadeClassifiers/haarcascade_frontalface_alt.xml");
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#include "preview-dialog.hpp"
|
||||
#include "opencv-helpers.hpp"
|
||||
#include "screenshot-helper.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QLayout>
|
||||
#include <screenshot-helper.hpp>
|
||||
|
||||
namespace advss {
|
||||
|
||||
@@ -131,10 +131,11 @@ void PreviewDialog::PatternMatchParametersChanged(
|
||||
_patternImageData = CreatePatternData(_patternMatchParams.image);
|
||||
}
|
||||
|
||||
void PreviewDialog::ObjDetectParametersChanged(const ObjDetectParameters ¶ms)
|
||||
void PreviewDialog::CascadeClassifierParametersChanged(
|
||||
const CascadeClassifierParameters ¶ms)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
_objDetectParams = std::make_shared<ObjDetectParameters>(params);
|
||||
_cascadeParams = std::make_shared<CascadeClassifierParameters>(params);
|
||||
}
|
||||
|
||||
void PreviewDialog::OCRParametersChanged(const OCRParameters ¶ms)
|
||||
@@ -185,7 +186,7 @@ void PreviewDialog::UpdateImage(const QPixmap &image)
|
||||
DrawFrame();
|
||||
}
|
||||
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
|
||||
_objDetectParams, _ocrParams, _areaParams, _condition);
|
||||
_cascadeParams, _ocrParams, _areaParams, _condition);
|
||||
}
|
||||
|
||||
void PreviewDialog::Start()
|
||||
@@ -215,7 +216,7 @@ void PreviewDialog::Start()
|
||||
_thread.start();
|
||||
|
||||
emit NeedImage(_video, _type, _patternMatchParams, _patternImageData,
|
||||
_objDetectParams, _ocrParams, _areaParams, _condition);
|
||||
_cascadeParams, _ocrParams, _areaParams, _condition);
|
||||
}
|
||||
|
||||
void PreviewDialog::DrawFrame()
|
||||
@@ -266,7 +267,7 @@ void PreviewImage::CreateImage(
|
||||
const VideoInput &video, PreviewType type,
|
||||
const PatternMatchParameters &patternMatchParams,
|
||||
const PatternImageData &patternImageData,
|
||||
std::shared_ptr<ObjDetectParameters> objDetectParams,
|
||||
std::shared_ptr<CascadeClassifierParameters> cascadeParams,
|
||||
std::shared_ptr<OCRParameters> ocrParams,
|
||||
const AreaParameters &areaParams, VideoCondition condition)
|
||||
{
|
||||
@@ -299,7 +300,7 @@ void PreviewImage::CreateImage(
|
||||
std::unique_lock<std::mutex> lock(_mtx);
|
||||
// Will emit status label update
|
||||
MarkMatch(screenshot.GetImage(), patternMatchParams,
|
||||
patternImageData, objDetectParams, ocrParams,
|
||||
patternImageData, cascadeParams, ocrParams,
|
||||
condition);
|
||||
} else {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
@@ -311,14 +312,16 @@ void PreviewImage::CreateImage(
|
||||
void PreviewImage::MarkMatch(
|
||||
QImage &screenshot, const PatternMatchParameters &patternMatchParams,
|
||||
const PatternImageData &patternImageData,
|
||||
std::shared_ptr<ObjDetectParameters> objDetectParams,
|
||||
std::shared_ptr<CascadeClassifierParameters> cascadeParams,
|
||||
std::shared_ptr<OCRParameters> ocrParams, VideoCondition condition)
|
||||
{
|
||||
if (condition == VideoCondition::PATTERN) {
|
||||
MarkPatternMatch(screenshot, patternMatchParams,
|
||||
patternImageData);
|
||||
} else if (condition == VideoCondition::OBJECT) {
|
||||
MarkObjectMatch(screenshot, objDetectParams);
|
||||
} else if (condition == VideoCondition::OBJECT_CASCADE) {
|
||||
MarkObjectsFromDetector(
|
||||
screenshot,
|
||||
cascadeParams ? cascadeParams->GetDetector() : nullptr);
|
||||
} else if (condition == VideoCondition::OCR) {
|
||||
MarkOCRMatch(screenshot, ocrParams);
|
||||
}
|
||||
@@ -347,26 +350,15 @@ void PreviewImage::MarkPatternMatch(
|
||||
}
|
||||
}
|
||||
|
||||
void PreviewImage::MarkObjectMatch(
|
||||
QImage &screenshot,
|
||||
const std::shared_ptr<ObjDetectParameters> &objDetectParams)
|
||||
void PreviewImage::MarkObjectsFromDetector(QImage &screenshot,
|
||||
ObjectDetector *detector)
|
||||
{
|
||||
if (!objDetectParams) {
|
||||
if (!detector) {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectMatchFail"));
|
||||
return;
|
||||
}
|
||||
auto model = objDetectParams->GetModel();
|
||||
if (!model) {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectMatchFail"));
|
||||
return;
|
||||
}
|
||||
auto objects = MatchObject(screenshot, *model,
|
||||
objDetectParams->scaleFactor,
|
||||
objDetectParams->minNeighbors,
|
||||
objDetectParams->minSize.CV(),
|
||||
objDetectParams->maxSize.CV());
|
||||
auto objects = detector->Detect(screenshot);
|
||||
if (objects.empty()) {
|
||||
emit StatusUpdate(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectMatchFail"));
|
||||
|
||||
@@ -27,7 +27,7 @@ public slots:
|
||||
void CreateImage(const VideoInput &, PreviewType,
|
||||
const PatternMatchParameters &,
|
||||
const PatternImageData &,
|
||||
std::shared_ptr<ObjDetectParameters>,
|
||||
std::shared_ptr<CascadeClassifierParameters>,
|
||||
std::shared_ptr<OCRParameters>, const AreaParameters &,
|
||||
VideoCondition);
|
||||
signals:
|
||||
@@ -38,12 +38,11 @@ signals:
|
||||
private:
|
||||
void MarkMatch(QImage &screenshot, const PatternMatchParameters &,
|
||||
const PatternImageData &,
|
||||
std::shared_ptr<ObjDetectParameters>,
|
||||
std::shared_ptr<CascadeClassifierParameters>,
|
||||
std::shared_ptr<OCRParameters>, VideoCondition);
|
||||
void MarkPatternMatch(QImage &, const PatternMatchParameters &,
|
||||
const PatternImageData &);
|
||||
void MarkObjectMatch(QImage &,
|
||||
const std::shared_ptr<ObjDetectParameters> &);
|
||||
void MarkObjectsFromDetector(QImage &, ObjectDetector *);
|
||||
void MarkOCRMatch(QImage &, const std::shared_ptr<OCRParameters> &);
|
||||
|
||||
std::mutex &_mtx;
|
||||
@@ -62,7 +61,8 @@ public:
|
||||
|
||||
public slots:
|
||||
void PatternMatchParametersChanged(const PatternMatchParameters &);
|
||||
void ObjDetectParametersChanged(const ObjDetectParameters &);
|
||||
void
|
||||
CascadeClassifierParametersChanged(const CascadeClassifierParameters &);
|
||||
void OCRParametersChanged(const OCRParameters &);
|
||||
void VideoSelectionChanged(const VideoInput &);
|
||||
void AreaParametersChanged(const AreaParameters &);
|
||||
@@ -75,7 +75,7 @@ signals:
|
||||
void SelectionAreaChanged(QRect area);
|
||||
void NeedImage(const VideoInput &, PreviewType,
|
||||
const PatternMatchParameters &, const PatternImageData &,
|
||||
std::shared_ptr<ObjDetectParameters>,
|
||||
std::shared_ptr<CascadeClassifierParameters>,
|
||||
std::shared_ptr<OCRParameters>, const AreaParameters &,
|
||||
VideoCondition);
|
||||
|
||||
@@ -90,7 +90,7 @@ private:
|
||||
VideoInput _video;
|
||||
PatternMatchParameters _patternMatchParams;
|
||||
PatternImageData _patternImageData;
|
||||
std::shared_ptr<ObjDetectParameters> _objDetectParams;
|
||||
std::shared_ptr<CascadeClassifierParameters> _cascadeParams;
|
||||
std::shared_ptr<OCRParameters> _ocrParams;
|
||||
AreaParameters _areaParams;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
#include "screenshot-helper.hpp"
|
||||
#include "parameter-wrappers.hpp"
|
||||
#include "screenshot-helper.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <obs.h>
|
||||
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QImage>
|
||||
@@ -11,6 +11,8 @@
|
||||
#include <QRubberBand>
|
||||
#include <QScrollArea>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class ScreenshotDialog : public QDialog {
|
||||
|
||||
89
plugins/video/video-edit-area.cpp
Normal file
89
plugins/video/video-edit-area.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
#include "macro-condition-video.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <QPushButton>
|
||||
|
||||
namespace advss {
|
||||
|
||||
AreaEdit::AreaEdit(QWidget *parent, PreviewDialog *previewDialog,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_checkAreaEnable(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.checkAreaEnable"))),
|
||||
_checkArea(new AreaSelection(0, 99999)),
|
||||
_selectArea(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectArea"))),
|
||||
_previewDialog(previewDialog),
|
||||
_entryData(data)
|
||||
{
|
||||
QWidget::connect(_checkAreaEnable, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(CheckAreaEnableChanged(int)));
|
||||
QWidget::connect(_checkArea, SIGNAL(AreaChanged(Area)), this,
|
||||
SLOT(CheckAreaChanged(Area)));
|
||||
QWidget::connect(_selectArea, SIGNAL(clicked()), this,
|
||||
SLOT(SelectAreaClicked()));
|
||||
QWidget::connect(_previewDialog, SIGNAL(SelectionAreaChanged(QRect)),
|
||||
this, SLOT(CheckAreaChanged(QRect)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{checkAreaEnable}}", _checkAreaEnable},
|
||||
{"{{checkArea}}", _checkArea},
|
||||
{"{{selectArea}}", _selectArea},
|
||||
};
|
||||
|
||||
auto layout = new QHBoxLayout;
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.checkArea"),
|
||||
layout, widgetPlaceholders);
|
||||
setLayout(layout);
|
||||
|
||||
_checkAreaEnable->setChecked(_entryData->_areaParameters.enable);
|
||||
_checkArea->SetArea(_entryData->_areaParameters.area);
|
||||
SetWidgetVisibility();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void AreaEdit::SetWidgetVisibility()
|
||||
{
|
||||
_checkArea->setVisible(_entryData->_areaParameters.enable);
|
||||
_selectArea->setVisible(_entryData->_areaParameters.enable);
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
}
|
||||
|
||||
void AreaEdit::SelectAreaClicked()
|
||||
{
|
||||
_previewDialog->show();
|
||||
_previewDialog->raise();
|
||||
_previewDialog->activateWindow();
|
||||
_previewDialog->SelectArea();
|
||||
}
|
||||
|
||||
void AreaEdit::CheckAreaEnableChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_areaParameters.enable = value;
|
||||
SetWidgetVisibility();
|
||||
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
|
||||
emit Resized();
|
||||
}
|
||||
|
||||
void AreaEdit::CheckAreaChanged(Area value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_areaParameters.area = value;
|
||||
_previewDialog->AreaParametersChanged(_entryData->_areaParameters);
|
||||
}
|
||||
|
||||
void AreaEdit::CheckAreaChanged(QRect rect)
|
||||
{
|
||||
const QSignalBlocker b(_checkArea);
|
||||
Area area{rect.topLeft().x(), rect.y(), rect.width(), rect.height()};
|
||||
_checkArea->SetArea(area);
|
||||
CheckAreaChanged(area);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
53
plugins/video/video-edit-brightness.cpp
Normal file
53
plugins/video/video-edit-brightness.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "macro-condition-video.hpp"
|
||||
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
BrightnessEdit::BrightnessEdit(QWidget *parent,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_threshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.brightnessThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.brightnessThresholdDescription"))),
|
||||
_current(new QLabel),
|
||||
_entryData(data)
|
||||
{
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(_threshold);
|
||||
layout->addWidget(_current);
|
||||
setLayout(layout);
|
||||
|
||||
QWidget::connect(
|
||||
_threshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(BrightnessThresholdChanged(
|
||||
const NumberVariable<double> &)));
|
||||
QWidget::connect(&_timer, &QTimer::timeout, this,
|
||||
&BrightnessEdit::UpdateCurrentBrightness);
|
||||
_timer.start(1000);
|
||||
|
||||
_threshold->SetDoubleValue(_entryData->_brightnessThreshold);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void BrightnessEdit::UpdateCurrentBrightness()
|
||||
{
|
||||
QString text = obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.currentBrightness");
|
||||
_current->setText(text.arg(_entryData->GetCurrentBrightness()));
|
||||
}
|
||||
|
||||
void BrightnessEdit::BrightnessThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_brightnessThreshold = value;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
88
plugins/video/video-edit-color.cpp
Normal file
88
plugins/video/video-edit-color.cpp
Normal file
@@ -0,0 +1,88 @@
|
||||
#include "macro-condition-video.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
ColorEdit::ColorEdit(QWidget *parent,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_matchThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorMatchThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorMatchThresholdDescription"),
|
||||
true)),
|
||||
_colorThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
|
||||
true)),
|
||||
_colorButton(new VariableColorButton(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectColor"))),
|
||||
_entryData(data)
|
||||
{
|
||||
QWidget::connect(_colorButton,
|
||||
SIGNAL(ColorVariableChanged(const ColorVariable &)),
|
||||
this, SLOT(ColorChanged(const ColorVariable &)));
|
||||
QWidget::connect(
|
||||
_matchThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(MatchThresholdChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(
|
||||
_colorThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{color}}", _colorButton},
|
||||
};
|
||||
|
||||
auto colorLayout = new QHBoxLayout;
|
||||
PlaceWidgets(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.color"),
|
||||
colorLayout, widgetPlaceholders);
|
||||
|
||||
auto layout = new QVBoxLayout;
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addLayout(colorLayout);
|
||||
layout->addWidget(_colorThreshold);
|
||||
layout->addWidget(_matchThreshold);
|
||||
setLayout(layout);
|
||||
|
||||
_matchThreshold->SetDoubleValue(
|
||||
_entryData->_colorParameters.matchThreshold);
|
||||
_colorThreshold->SetDoubleValue(
|
||||
_entryData->_colorParameters.colorThreshold);
|
||||
_colorButton->SetValue(_entryData->_colorParameters.color);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void ColorEdit::ColorChanged(const ColorVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_colorParameters.color = value;
|
||||
}
|
||||
|
||||
void ColorEdit::MatchThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_colorParameters.matchThreshold = value;
|
||||
}
|
||||
|
||||
void ColorEdit::ColorThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_colorParameters.colorThreshold = value;
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
157
plugins/video/video-edit-object.cpp
Normal file
157
plugins/video/video-edit-object.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "macro-condition-video.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
CascadeClassifierEdit::CascadeClassifierEdit(
|
||||
QWidget *parent, PreviewDialog *previewDialog,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_modelDataPath(new FileSelection()),
|
||||
_objectScaleThreshold(new SliderSpinBox(
|
||||
1.1, 5.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectScaleThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.objectScaleThresholdDescription"))),
|
||||
_minNeighbors(new QSpinBox()),
|
||||
_minNeighborsDescription(new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.minNeighborDescription"))),
|
||||
_minSize(new SizeSelection(0, 1024)),
|
||||
_maxSize(new SizeSelection(0, 4096)),
|
||||
_previewDialog(previewDialog),
|
||||
_entryData(data)
|
||||
{
|
||||
_minNeighbors->setMinimum(minMinNeighbors);
|
||||
_minNeighbors->setMaximum(maxMinNeighbors);
|
||||
|
||||
QWidget::connect(
|
||||
_objectScaleThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(ObjectScaleThresholdChanged(
|
||||
const NumberVariable<double> &)));
|
||||
QWidget::connect(_minNeighbors, SIGNAL(valueChanged(int)), this,
|
||||
SLOT(MinNeighborsChanged(int)));
|
||||
QWidget::connect(_minSize, SIGNAL(SizeChanged(Size)), this,
|
||||
SLOT(MinSizeChanged(Size)));
|
||||
QWidget::connect(_maxSize, SIGNAL(SizeChanged(Size)), this,
|
||||
SLOT(MaxSizeChanged(Size)));
|
||||
QWidget::connect(_modelDataPath, SIGNAL(PathChanged(const QString &)),
|
||||
this, SLOT(ModelPathChanged(const QString &)));
|
||||
|
||||
std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{minNeighbors}}", _minNeighbors},
|
||||
{"{{minSize}}", _minSize},
|
||||
{"{{maxSize}}", _maxSize},
|
||||
{"{{modelDataPath}}", _modelDataPath},
|
||||
};
|
||||
|
||||
auto pathLayout = new QHBoxLayout;
|
||||
pathLayout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.modelPath"),
|
||||
pathLayout, widgetPlaceholders);
|
||||
|
||||
auto neighborsLayout = new QHBoxLayout;
|
||||
neighborsLayout->setContentsMargins(0, 0, 0, 0);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.minNeighbor"),
|
||||
neighborsLayout, widgetPlaceholders);
|
||||
|
||||
auto sizeGrid = new QGridLayout;
|
||||
sizeGrid->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.minSize")),
|
||||
0, 0);
|
||||
sizeGrid->addWidget(_minSize, 0, 1);
|
||||
sizeGrid->addWidget(
|
||||
new QLabel(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.maxSize")),
|
||||
1, 0);
|
||||
sizeGrid->addWidget(_maxSize, 1, 1);
|
||||
auto sizeLayout = new QHBoxLayout;
|
||||
sizeLayout->setContentsMargins(0, 0, 0, 0);
|
||||
sizeLayout->addLayout(sizeGrid);
|
||||
sizeLayout->addStretch();
|
||||
|
||||
auto layout = new QVBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addLayout(pathLayout);
|
||||
layout->addLayout(neighborsLayout);
|
||||
layout->addLayout(sizeLayout);
|
||||
setLayout(layout);
|
||||
|
||||
_modelDataPath->SetPath(
|
||||
_entryData->_cascadeMatchParameters.GetModelPath());
|
||||
_objectScaleThreshold->SetDoubleValue(
|
||||
_entryData->_cascadeMatchParameters.scaleFactor);
|
||||
_minNeighbors->setValue(
|
||||
_entryData->_cascadeMatchParameters.minNeighbors);
|
||||
_minSize->SetSize(_entryData->_cascadeMatchParameters.minSize);
|
||||
_maxSize->SetSize(_entryData->_cascadeMatchParameters.maxSize);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void CascadeClassifierEdit::ObjectScaleThresholdChanged(
|
||||
const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_cascadeMatchParameters.scaleFactor = value;
|
||||
_previewDialog->CascadeClassifierParametersChanged(
|
||||
_entryData->_cascadeMatchParameters);
|
||||
}
|
||||
|
||||
void CascadeClassifierEdit::MinNeighborsChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_cascadeMatchParameters.minNeighbors = value;
|
||||
_previewDialog->CascadeClassifierParametersChanged(
|
||||
_entryData->_cascadeMatchParameters);
|
||||
}
|
||||
|
||||
void CascadeClassifierEdit::MinSizeChanged(advss::Size value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_cascadeMatchParameters.minSize = value;
|
||||
_previewDialog->CascadeClassifierParametersChanged(
|
||||
_entryData->_cascadeMatchParameters);
|
||||
}
|
||||
|
||||
void CascadeClassifierEdit::MaxSizeChanged(advss::Size value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_cascadeMatchParameters.maxSize = value;
|
||||
_previewDialog->CascadeClassifierParametersChanged(
|
||||
_entryData->_cascadeMatchParameters);
|
||||
}
|
||||
|
||||
void CascadeClassifierEdit::ModelPathChanged(const QString &text)
|
||||
{
|
||||
if (_loading || !_entryData) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool dataLoaded = false;
|
||||
{
|
||||
auto lock = LockContext();
|
||||
std::string path = text.toStdString();
|
||||
dataLoaded =
|
||||
_entryData->_cascadeMatchParameters.SetModelPath(path);
|
||||
}
|
||||
if (!dataLoaded) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.modelLoadFail"));
|
||||
}
|
||||
_previewDialog->CascadeClassifierParametersChanged(
|
||||
_entryData->_cascadeMatchParameters);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
320
plugins/video/video-edit-ocr.cpp
Normal file
320
plugins/video/video-edit-ocr.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#include "macro-condition-video.hpp"
|
||||
#include "layout-helpers.hpp"
|
||||
#include "plugin-state-helpers.hpp"
|
||||
#include "ui-helpers.hpp"
|
||||
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace advss {
|
||||
|
||||
const static std::map<tesseract::PageSegMode, std::string> pageSegModes = {
|
||||
{tesseract::PageSegMode::PSM_SINGLE_COLUMN,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleColumn"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_BLOCK_VERT_TEXT,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleBlockVertText"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_BLOCK,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleBlock"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_LINE,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleLine"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_WORD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleWord"},
|
||||
{tesseract::PageSegMode::PSM_CIRCLE_WORD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.circleWord"},
|
||||
{tesseract::PageSegMode::PSM_SINGLE_CHAR,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.singleChar"},
|
||||
{tesseract::PageSegMode::PSM_SPARSE_TEXT,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.sparseText"},
|
||||
{tesseract::PageSegMode::PSM_SPARSE_TEXT_OSD,
|
||||
"AdvSceneSwitcher.condition.video.ocrMode.sparseTextOSD"},
|
||||
};
|
||||
|
||||
static inline void populatePageSegModeSelection(QComboBox *list)
|
||||
{
|
||||
for (const auto &[mode, name] : pageSegModes) {
|
||||
list->addItem(obs_module_text(name.c_str()),
|
||||
static_cast<int>(mode));
|
||||
}
|
||||
}
|
||||
|
||||
static void openFileInEditor(const std::string &filepath)
|
||||
{
|
||||
const auto path = QString::fromStdString(filepath);
|
||||
const QFileInfo fileInfo(path);
|
||||
if (!fileInfo.exists()) {
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrOpenConfig.createFailed"));
|
||||
return;
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
QUrl fileUrl = QUrl::fromLocalFile(path);
|
||||
if (!QDesktopServices::openUrl(fileUrl)) {
|
||||
DisplayMessage(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrOpenConfig.openFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
OCREdit::OCREdit(QWidget *parent, PreviewDialog *previewDialog,
|
||||
const std::shared_ptr<MacroConditionVideo> &data)
|
||||
: QWidget(parent),
|
||||
_matchText(new VariableTextEdit(this)),
|
||||
_regex(new RegexConfigWidget(this)),
|
||||
_colorButton(new VariableColorButton(
|
||||
this,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.selectColor"))),
|
||||
_colorThreshold(new SliderSpinBox(
|
||||
0., 1.,
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThreshold"),
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.colorDeviationThresholdDescription"),
|
||||
true)),
|
||||
_pageSegMode(new QComboBox()),
|
||||
_tesseractBaseDir(new FileSelection(FileSelection::Type::FOLDER)),
|
||||
_languageCode(new VariableLineEdit(this)),
|
||||
_useConfig(new QCheckBox(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrUseConfigFile"))),
|
||||
_configFile(new FileSelection(FileSelection::Type::WRITE, this)),
|
||||
_openConfigFile(new QPushButton(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrOpenConfigFile"))),
|
||||
_reloadConfig(new QPushButton()),
|
||||
_configLayout(new QHBoxLayout()),
|
||||
_previewDialog(previewDialog),
|
||||
_entryData(data)
|
||||
{
|
||||
populatePageSegModeSelection(_pageSegMode);
|
||||
|
||||
_reloadConfig->setMaximumWidth(22);
|
||||
SetButtonIcon(_reloadConfig, GetThemeTypeName() == "Light"
|
||||
? ":res/images/refresh.svg"
|
||||
: "theme:Dark/refresh.svg");
|
||||
_reloadConfig->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrConfigReload"));
|
||||
|
||||
QWidget::connect(_colorButton,
|
||||
SIGNAL(ColorVariableChanged(const ColorVariable &)),
|
||||
this, SLOT(ColorChanged(const ColorVariable &)));
|
||||
QWidget::connect(
|
||||
_colorThreshold,
|
||||
SIGNAL(DoubleValueChanged(const NumberVariable<double> &)),
|
||||
this,
|
||||
SLOT(ColorThresholdChanged(const NumberVariable<double> &)));
|
||||
QWidget::connect(_matchText, SIGNAL(textChanged()), this,
|
||||
SLOT(MatchTextChanged()));
|
||||
QWidget::connect(_regex,
|
||||
SIGNAL(RegexConfigChanged(const RegexConfig &)), this,
|
||||
SLOT(RegexChanged(const RegexConfig &)));
|
||||
QWidget::connect(_pageSegMode, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(PageSegModeChanged(int)));
|
||||
QWidget::connect(_tesseractBaseDir,
|
||||
SIGNAL(PathChanged(const QString &)), this,
|
||||
SLOT(TesseractBaseDirChanged(const QString &)));
|
||||
QWidget::connect(_languageCode, SIGNAL(editingFinished()), this,
|
||||
SLOT(LanguageChanged()));
|
||||
QWidget::connect(_useConfig, SIGNAL(stateChanged(int)), this,
|
||||
SLOT(UseConfigChanged(int)));
|
||||
QWidget::connect(_configFile, SIGNAL(PathChanged(const QString &)),
|
||||
this, SLOT(ConfigFileChanged(const QString &)));
|
||||
QWidget::connect(_openConfigFile, &QPushButton::clicked, [this](bool) {
|
||||
openFileInEditor(
|
||||
_entryData->_ocrParameters.GetCustomConfigFile());
|
||||
});
|
||||
QWidget::connect(_reloadConfig, &QPushButton::clicked, [this](bool) {
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.EnableCustomConfig(true);
|
||||
_previewDialog->OCRParametersChanged(
|
||||
_entryData->_ocrParameters);
|
||||
});
|
||||
|
||||
auto configFileHint = new QLabel();
|
||||
const QString path = GetThemeTypeName() == "Light"
|
||||
? ":/res/images/help.svg"
|
||||
: ":/res/images/help_light.svg";
|
||||
const QIcon icon(path);
|
||||
const QPixmap pixmap = icon.pixmap(QSize(16, 16));
|
||||
configFileHint->setPixmap(pixmap);
|
||||
configFileHint->setToolTip(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrConfigHint"));
|
||||
|
||||
const std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
|
||||
{"{{color}}", _colorButton},
|
||||
{"{{textType}}", _pageSegMode},
|
||||
{"{{tesseractBaseDir}}", _tesseractBaseDir},
|
||||
{"{{languageCode}}", _languageCode},
|
||||
{"{{configFile}}", _configFile},
|
||||
{"{{openConfigFile}}", _openConfigFile},
|
||||
{"{{reloadConfig}}", _reloadConfig},
|
||||
{"{{configFileHint}}", configFileHint},
|
||||
};
|
||||
|
||||
auto layout = new QVBoxLayout();
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
auto textLayout = new QHBoxLayout();
|
||||
textLayout->setContentsMargins(0, 0, 0, 0);
|
||||
textLayout->addWidget(_matchText);
|
||||
textLayout->addWidget(_regex);
|
||||
layout->addLayout(textLayout);
|
||||
auto pageModeSegLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrTextType"),
|
||||
pageModeSegLayout, widgetPlaceholders);
|
||||
layout->addLayout(pageModeSegLayout);
|
||||
auto baseDirLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrBaseDir"),
|
||||
baseDirLayout, widgetPlaceholders, false);
|
||||
layout->addLayout(baseDirLayout);
|
||||
auto languageLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrLanguage"),
|
||||
languageLayout, widgetPlaceholders);
|
||||
layout->addLayout(languageLayout);
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrConfig"),
|
||||
_configLayout, widgetPlaceholders, false);
|
||||
layout->addWidget(_useConfig);
|
||||
layout->addLayout(_configLayout);
|
||||
auto colorPickLayout = new QHBoxLayout();
|
||||
PlaceWidgets(
|
||||
obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.layout.ocrColorPick"),
|
||||
colorPickLayout, widgetPlaceholders);
|
||||
layout->addLayout(colorPickLayout);
|
||||
layout->addWidget(_colorThreshold);
|
||||
setLayout(layout);
|
||||
|
||||
_matchText->setPlainText(_entryData->_ocrParameters.text);
|
||||
_regex->SetRegexConfig(_entryData->_ocrParameters.regex);
|
||||
_colorButton->SetValue(_entryData->_ocrParameters.color);
|
||||
_colorThreshold->SetDoubleValue(
|
||||
_entryData->_ocrParameters.colorThreshold);
|
||||
_pageSegMode->setCurrentIndex(_pageSegMode->findData(
|
||||
static_cast<int>(_entryData->_ocrParameters.GetPageMode())));
|
||||
_tesseractBaseDir->SetPath(
|
||||
_entryData->_ocrParameters.GetTesseractBasePath());
|
||||
_languageCode->setText(_entryData->_ocrParameters.GetLanguageCode());
|
||||
_useConfig->setChecked(
|
||||
_entryData->_ocrParameters.CustomConfigIsEnabled());
|
||||
_configFile->SetPath(_entryData->_ocrParameters.GetCustomConfigFile());
|
||||
SetLayoutVisible(_configLayout,
|
||||
_entryData->_ocrParameters.CustomConfigIsEnabled());
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
void OCREdit::ColorChanged(const ColorVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.color = value;
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::ColorThresholdChanged(const DoubleVariable &value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.colorThreshold = value;
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::MatchTextChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.text =
|
||||
_matchText->toPlainText().toUtf8().constData();
|
||||
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::RegexChanged(const RegexConfig &conf)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.regex = conf;
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::PageSegModeChanged(int idx)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->SetPageSegMode(static_cast<tesseract::PageSegMode>(
|
||||
_pageSegMode->itemData(idx).toInt()));
|
||||
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::TesseractBaseDirChanged(const QString &path)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
if (!_entryData->SetTesseractBaseDir(path.toStdString())) {
|
||||
const QString message(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
|
||||
const QDir dataDir(path);
|
||||
const QString fileName(_languageCode->text() + ".traineddata");
|
||||
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
|
||||
|
||||
// Reset to previous value
|
||||
const QSignalBlocker b(this);
|
||||
_tesseractBaseDir->SetPath(
|
||||
_entryData->_ocrParameters.GetTesseractBasePath());
|
||||
return;
|
||||
}
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::LanguageChanged()
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
if (!_entryData->SetLanguageCode(_languageCode->text().toStdString())) {
|
||||
const QString message(obs_module_text(
|
||||
"AdvSceneSwitcher.condition.video.ocrLanguageNotFound"));
|
||||
const QDir dataDir(QString::fromStdString(
|
||||
_entryData->_ocrParameters.GetTesseractBasePath()));
|
||||
const QString fileName(_languageCode->text() + ".traineddata");
|
||||
DisplayMessage(message.arg(fileName, dataDir.absolutePath()));
|
||||
|
||||
// Reset to previous value
|
||||
const QSignalBlocker b(this);
|
||||
_languageCode->setText(
|
||||
_entryData->_ocrParameters.GetLanguageCode());
|
||||
return;
|
||||
}
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::UseConfigChanged(int value)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.EnableCustomConfig(value);
|
||||
SetLayoutVisible(_configLayout, value);
|
||||
adjustSize();
|
||||
updateGeometry();
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
void OCREdit::ConfigFileChanged(const QString &path)
|
||||
{
|
||||
GUARD_LOADING_AND_LOCK();
|
||||
_entryData->_ocrParameters.SetCustomConfigFile(path.toStdString());
|
||||
_previewDialog->OCRParametersChanged(_entryData->_ocrParameters);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
Reference in New Issue
Block a user