Compare commits

..

24 Commits

Author SHA1 Message Date
WarmUpTill
a13a0774b4 Add "Now Playing" condition type 2026-08-24 21:35:14 +02:00
WarmUpTill
e76df589e2 Fix cmake configure errors during download retry
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-08-14 22:45:29 +02:00
WarmUpTill
1390565fc6 Disable additional warnings for whisper.cpp 2026-08-14 22:45:29 +02:00
WarmUpTill
d76a37c785 Add AdvancedSceneSwitcherVersion vendor request
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Returns version and commit information
2026-08-12 20:55:34 +02:00
WarmUpTill
ce8a35adea CI: Include Ubuntu 26 artifacts in release draft 2026-08-12 20:55:34 +02:00
Rygtx
9030ba1adf Update locale (zh-CN.ini)
Some checks failed
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-08-10 21:27:25 +02:00
WarmUpTill
5b8d829bba Add "Speech Recognition" condition
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
Allows checking for speech patterns on a given OBS audio source
2026-08-05 12:00:34 +02:00
WarmUpTill
ad03b92d2d CI: Install vulkan SDK
Required to build whisper.cpp with GPU acceleration support
2026-08-05 12:00:34 +02:00
WarmUpTill
88fc412900 deps: Add whisper.cpp 2026-08-05 12:00:34 +02:00
WarmUpTill
c2ec6848aa Fix sources of type "Group" being listed twice in filter selections
Some checks are pending
debian-build / build (push) Waiting to run
Push to master / Check Formatting 🔍 (push) Waiting to run
Push to master / Build Project 🧱 (push) Waiting to run
Push to master / Create Release 🛫 (push) Blocked by required conditions
2026-08-04 21:05:55 +02:00
WarmUpTill
01da2877b0 CI: Add Ubuntu 26 build 2026-08-03 20:52:39 +02:00
WarmUpTill
107d77f1b8 Split macro-condition-video.cpp UI into per-widget files
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
This makes it easier to conditionally exclude video-edit-object.cpp
from the build when OpenCV >= 5 drops CascadeClassifier support.
2026-08-01 22:34:01 +02:00
WarmUpTill
1030868a75 CI: Build "xobjdetect" when building OpenCV 5.x 2026-08-01 22:34:01 +02:00
WarmUpTill
797dc2ceb1 deps: Add OpenCV contrib repo
Needed for Haar Cascade Classifiers support since OpenCV 5.0.0
2026-08-01 22:34:01 +02:00
WarmUpTill
b971bfe800 deps: Switch to OpenCV 5.0.0 2026-08-01 22:34:01 +02:00
WarmUpTill
f88407b731 Export Section symbols 2026-08-01 22:34:01 +02:00
WarmUpTill
725e3cacb8 Add license files
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-07-30 20:47:39 +02:00
WarmUpTill
c9eed5d997 Add un-/pause option to macro tab context menu
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-07-22 11:27:15 +02:00
Arimil
bf682136c3 OpenCV 5 support
Some checks failed
debian-build / build (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-07-19 19:56:07 +02:00
WarmUpTill
7be2a4f544 Add support to snapshot vars before queue insertion
Some checks failed
debian-build / build (push) Has been cancelled
Check locale / ubuntu64 (push) Has been cancelled
Push to master / Check Formatting 🔍 (push) Has been cancelled
Push to master / Build Project 🧱 (push) Has been cancelled
Push to master / Create Release 🛫 (push) Has been cancelled
2026-07-17 23:24:02 +02:00
WarmUpTill
616664e6f3 Add "Loop" action 2026-07-17 23:24:02 +02:00
WarmUpTill
0caf8628b7 Add HideElseSection() in preparation for loop action 2026-07-17 23:24:02 +02:00
WarmUpTill
463e7caa2c Reword locale keys 2026-07-17 23:24:02 +02:00
WarmUpTill
0e2315807f Statically link SSL to avoid dll conflicts on Windows 2026-07-17 21:43:58 +02:00
72 changed files with 6120 additions and 2637 deletions

View File

@@ -38,4 +38,4 @@ runs:
if: ${{ runner.os == 'Windows' }}
shell: pwsh
run: |
${{ inputs.workingDirectory }}/build_${{ inputs.target }}/tests/${{ inputs.config }}/advanced-scene-switcher-tests.exe
${{ inputs.workingDirectory }}/build_x64/tests/${{ inputs.config }}/advanced-scene-switcher-tests.exe

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -2,7 +2,7 @@
param(
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo',
[ValidateSet('x86', 'x64', 'arm64')]
[ValidateSet('x86', 'x64')]
[string] $Target,
[ValidateSet('Visual Studio 17 2022', 'Visual Studio 16 2019')]
[string] $CMakeGenerator,
@@ -69,7 +69,7 @@ function Build {
$CmakeArgs += ('--debug-output')
}
$Preset = "windows-$(if ( $null -ne $Env:CI ) { 'ci-' })${Target}"
$Preset = "windows-$(if ( $Env:CI -ne $null ) { 'ci-' })${Target}"
$CmakeArgs += @(
'--preset', $Preset
@@ -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..."
@@ -209,14 +211,13 @@ function Build {
$msbuildExe = vswhere -latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe | select-object -first 1
if ($msbuildExe) {
$LibusbPlatform = if ($Target -eq 'arm64') { 'arm64' } else { 'x64' }
$env:CL="/wd5287"
Invoke-External $msbuildExe "${LibusbPath}/msvc/libusb.sln" /property:Configuration=Release /property:Platform=$LibusbPlatform
Invoke-External $msbuildExe "${LibusbPath}/msvc/libusb.sln" /property:Configuration=Release /property:Platform=x64
Remove-Item Env:CL
$libusbBuildResultDirectory = "${LibusbPath}/build/v143/${LibusbPlatform}/Release"
$libusbBuildResultDirectory = "${LibusbPath}/build/v143/x64/Release"
if (-not (Test-Path -Path $libusbBuildResultDirectory)) {
$libusbBuildResultDirectory = "${LibusbPath}/${LibusbPlatform}/Release/dll"
$libusbBuildResultDirectory = "${LibusbPath}/x64/Release/dll"
}
Copy-Item -Path "${libusbBuildResultDirectory}/*" -Destination ${ADVSSDepPath} -Recurse -Force
} else {
@@ -246,10 +247,9 @@ function Build {
if ($opensslDir) {
Write-Host "Detected OpenSSL at: $opensslDir"
$OpenSSLArch = if ($Target -eq 'arm64') { 'arm64' } else { 'x64' }
$MqttCmakeArgs += "-DOPENSSL_ROOT_DIR=$opensslDir"
$MqttCmakeArgs += "-DOPENSSL_CRYPTO_LIBRARY=$opensslDir\lib\VC\$OpenSSLArch\MD\libcrypto.lib"
$MqttCmakeArgs += "-DOPENSSL_SSL_LIBRARY=$opensslDir\lib\VC\$OpenSSLArch\MD\libssl.lib"
$MqttCmakeArgs += "-DOPENSSL_CRYPTO_LIBRARY=$opensslDir\lib\VC\x64\MD\libcrypto.lib"
$MqttCmakeArgs += "-DOPENSSL_SSL_LIBRARY=$opensslDir\lib\VC\x64\MD\libssl.lib"
} else {
Write-Warning "OpenSSL not found - maybe cmake will find it ..."
}

View File

@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[ValidateSet('x64', 'arm64')]
[ValidateSet('x64')]
[string] $Target = 'x64',
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo',
@@ -93,7 +93,7 @@ function Build {
$CmakeArgs += ('--debug-output')
}
$Preset = "windows-$(if ( $null -ne $Env:CI ) { 'ci-' })${Target}"
$Preset = "windows-$(if ( $Env:CI -ne $null ) { 'ci-' })${Target}"
$CmakeArgs += @(
'--preset', $Preset

View File

@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[ValidateSet('x64', 'arm64')]
[ValidateSet('x64')]
[string] $Target = 'x64',
[ValidateSet('Debug', 'RelWithDebInfo', 'Release', 'MinSizeRel')]
[string] $Configuration = 'RelWithDebInfo',
@@ -69,8 +69,7 @@ function Package {
Remove-Item @RemoveArgs
$ReleasePath = "${ProjectRoot}/release/${Configuration}"
$BinDirName = if ($Target -eq 'arm64') { 'arm64' } else { '64bit' }
$NewBinPath = "${ReleasePath}/${ProductName}/bin/${BinDirName}"
$NewBinPath = "${ReleasePath}/${ProductName}/bin/64bit"
$NewDataPath = "${ReleasePath}/${ProductName}/data"
$CIWindowsDir = "${ProjectRoot}/build-aux/CI/windows"
@@ -81,7 +80,7 @@ function Package {
New-Item -ItemType Directory -Force -Path $RecStaging | Out-Null
Copy-Item -Path "${CIWindowsDir}/README.txt" -Destination "${RecStaging}/README.txt"
if ( Test-Path -Path $NewBinPath ) {
$RecBinPath = "${RecStaging}/${ProductName}/bin/${BinDirName}"
$RecBinPath = "${RecStaging}/${ProductName}/bin/64bit"
New-Item -ItemType Directory -Force -Path $RecBinPath | Out-Null
Copy-Item -Path "${NewBinPath}/*" -Destination $RecBinPath -Recurse -Force
}
@@ -103,7 +102,7 @@ function Package {
New-Item -ItemType Directory -Force -Path $PortableStaging | Out-Null
Copy-Item -Path "${CIWindowsDir}/README-portable.txt" -Destination "${PortableStaging}/README.txt"
if ( Test-Path -Path $NewBinPath ) {
$PortableBinPath = "${PortableStaging}/obs-plugins/${BinDirName}"
$PortableBinPath = "${PortableStaging}/obs-plugins/64bit"
New-Item -ItemType Directory -Force -Path $PortableBinPath | Out-Null
Copy-Item -Path "${NewBinPath}/*" -Destination $PortableBinPath -Recurse -Force
}
@@ -137,10 +136,10 @@ function Package {
Copy-Item -Path "${Configuration}/*" -Destination $PkgRec -Recurse -Force
# Legacy layout (for OBS installation directory)
if ( Test-Path "${Configuration}/${ProductName}/bin/${BinDirName}" ) {
$PkgLegBin = "Package/portable/obs-plugins/${BinDirName}"
if ( Test-Path "${Configuration}/${ProductName}/bin/64bit" ) {
$PkgLegBin = "Package/portable/obs-plugins/64bit"
New-Item -ItemType Directory -Force -Path $PkgLegBin | Out-Null
Copy-Item -Path "${Configuration}/${ProductName}/bin/${BinDirName}/*" -Destination $PkgLegBin -Recurse -Force
Copy-Item -Path "${Configuration}/${ProductName}/bin/64bit/*" -Destination $PkgLegBin -Recurse -Force
}
if ( Test-Path "${Configuration}/${ProductName}/data" ) {
$PkgLegData = "Package/portable/data/obs-plugins/${ProductName}"

View 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)

View File

@@ -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=()

View File

@@ -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:
@@ -238,6 +249,14 @@ jobs:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-sources-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-source.*
- name: Rename artifacts for Ubuntu 24 🏷️
run: |
: Rename artifacts for Ubuntu 24 🏷️
for f in ${{ github.workspace }}/release/*-x86_64-linux-gnu.*; do
[ -e "${f}" ] || continue
mv "${f}" "${f/x86_64-linux-gnu/x86_64-ubuntu24.04-linux-gnu}"
done
- name: Upload Artifacts 📡
uses: actions/upload-artifact@v7
with:
@@ -252,6 +271,85 @@ 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: Rename artifacts for Ubuntu 26 🏷️
run: |
: Rename artifacts for Ubuntu 26 🏷️
for f in ${{ github.workspace }}/release/*-x86_64-linux-gnu.*; do
[ -e "${f}" ] || continue
mv "${f}" "${f/x86_64-linux-gnu/x86_64-ubuntu26.04-linux-gnu}"
done
- 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 +385,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:
@@ -311,63 +415,3 @@ jobs:
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-windows-x64-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-windows-x64*.*
windows-arm64-build:
name: Build for Windows ARM64 🪟
runs-on: windows-11-arm
needs: check-event
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
fetch-depth: 0
- name: Set Up Environment 🔧
id: setup
run: |
# Set Up Environment 🔧
if ( $Env:RUNNER_DEBUG -ne $null ) {
Set-PSDebug -Trace 1
}
$BuildSpec = Get-Content -Path buildspec.json -Raw | ConvertFrom-Json
$ProductName = $BuildSpec.name
$GitOutput = git describe --tags
"pluginName=${ProductName}" >> $env:GITHUB_OUTPUT
"pluginVersion=${GitOutput}" >> $env:GITHUB_OUTPUT
- name: Build Dependencies 🏗️
uses: ./.github/actions/build-dependencies
with:
workingDirectory: ${{ github.workspace }}/plugin
target: arm64
config: ${{ needs.check-event.outputs.config }}
- name: Build Plugin 🧱
uses: ./.github/actions/build-plugin
with:
target: arm64
config: ${{ needs.check-event.outputs.config }}
- name: Run tests
uses: ./.github/actions/run-tests
with:
target: arm64
config: ${{ needs.check-event.outputs.config }}
- name: Package Plugin 📀
uses: ./.github/actions/package-plugin
with:
target: arm64
config: ${{ needs.check-event.outputs.config }}
package: ${{ fromJSON(needs.check-event.outputs.package) }}
- name: Upload Artifacts 📡
uses: actions/upload-artifact@v7
with:
name: ${{ steps.setup.outputs.pluginName }}-${{ steps.setup.outputs.pluginVersion }}-windows-arm64-${{ needs.check-event.outputs.commitHash }}
path: ${{ github.workspace }}/release/${{ steps.setup.outputs.pluginName }}-*-windows-arm64*.*

View File

@@ -78,6 +78,7 @@ jobs:
'windows-x64;zip|exe'
'macos-universal;tar.xz|pkg'
'ubuntu-24.04-x86_64;tar.xz|deb|ddeb'
'ubuntu-26.04-x86_64;tar.xz|deb|ddeb'
'sources;tar.xz'
)

6
.gitmodules vendored
View File

@@ -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

View File

@@ -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
@@ -554,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()

View File

@@ -74,35 +74,6 @@
"ADVSS_ENABLE_TESTS": true
}
},
{
"name": "windows-arm64",
"displayName": "Windows ARM64",
"description": "Build for Windows ARM64",
"inherits": ["template"],
"binaryDir": "${sourceDir}/build_arm64",
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
},
"generator": "Visual Studio 17 2022",
"architecture": "ARM64",
"warnings": {"dev": true, "deprecated": true},
"cacheVariables": {
"QT_VERSION": "6",
"CMAKE_SYSTEM_VERSION": "10.0.18363.657"
}
},
{
"name": "windows-ci-arm64",
"inherits": ["windows-arm64"],
"displayName": "Windows ARM64 CI build",
"description": "Build for Windows ARM64 on CI",
"cacheVariables": {
"CMAKE_COMPILE_WARNING_AS_ERROR": true,
"ADVSS_ENABLE_TESTS": true
}
},
{
"name": "linux-x86_64",
"displayName": "Linux x86_64",
@@ -191,20 +162,6 @@
"description": "Windows CI build for x64 (RelWithDebInfo configuration)",
"configuration": "RelWithDebInfo"
},
{
"name": "windows-arm64",
"configurePreset": "windows-arm64",
"displayName": "Windows ARM64",
"description": "Windows build for ARM64",
"configuration": "RelWithDebInfo"
},
{
"name": "windows-ci-arm64",
"configurePreset": "windows-ci-arm64",
"displayName": "Windows ARM64 CI",
"description": "Windows CI build for ARM64 (RelWithDebInfo configuration)",
"configuration": "RelWithDebInfo"
},
{
"name": "linux-x86_64",
"configurePreset": "linux-x86_64",

View File

@@ -6,8 +6,7 @@
"label": "OBS sources",
"hashes": {
"macos": "39751f067bacc13d44b116c5138491b5f1391f91516d3d590d874edd21292291",
"windows-x64": "2c8427c10b55ac6d68008df2e9a3e82f4647aaad18f105e30d4713c2de678ccf",
"windows-ARM64": "2c8427c10b55ac6d68008df2e9a3e82f4647aaad18f105e30d4713c2de678ccf"
"windows-x64": "2c8427c10b55ac6d68008df2e9a3e82f4647aaad18f105e30d4713c2de678ccf"
}
},
"prebuilt": {
@@ -16,8 +15,7 @@
"label": "Pre-Built obs-deps",
"hashes": {
"macos": "495687e63383d1a287684b6e2e9bfe246bb8f156fe265926afb1a325af1edd2a",
"windows-x64": "c8c642c1070dc31ce9a0f1e4cef5bb992f4bff4882255788b5da12129e85caa7",
"windows-ARM64": "f581cc61e8f734a8b12d485fc8662a408ca59d222814e4b37bce115bd442fb04"
"windows-x64": "c8c642c1070dc31ce9a0f1e4cef5bb992f4bff4882255788b5da12129e85caa7"
}
},
"qt6": {
@@ -26,12 +24,10 @@
"label": "Pre-Built Qt6",
"hashes": {
"macos": "d3f5f04b6ea486e032530bdf0187cbda9a54e0a49621a4c8ba984c5023998867",
"windows-x64": "0e76bf0555dd5382838850b748d3dcfab44a1e1058441309ab54e1a65b156d0a",
"windows-ARM64": "7aab240504931f32ea8e8d208a912a0d6ddbd78c16858f2e559c7c9b29ab9326"
"windows-x64": "0e76bf0555dd5382838850b748d3dcfab44a1e1058441309ab54e1a65b156d0a"
},
"debugSymbols": {
"windows-x64": "11b7be92cf66a273299b8f3515c07a5cfb61614b59a4e67f7fc5ecba5e2bdf21",
"windows-ARM64": "884ae08291b5c87d83a7b0004ea8db356530c1f4ff9bdc814b5d599a8d28a152"
"windows-x64": "11b7be92cf66a273299b8f3515c07a5cfb61614b59a4e67f7fc5ecba5e2bdf21"
}
}
},

View File

@@ -191,14 +191,21 @@ function(_check_dependencies)
foreach(i RANGE 1 ${MAX_DOWNLOAD_RETRIES})
message(STATUS "Attempt ${i}/${MAX_DOWNLOAD_RETRIES} for ${url}")
file(
DOWNLOAD "${url}" "${dependencies_dir}/${file}"
STATUS download_status
EXPECTED_HASH SHA256=${hash})
file(DOWNLOAD "${url}" "${dependencies_dir}/${file}"
STATUS download_status)
list(GET download_status 0 error_code)
list(GET download_status 1 error_message)
if(error_code EQUAL 0)
file(SHA256 "${dependencies_dir}/${file}" actual_hash)
if(NOT actual_hash STREQUAL hash)
set(error_code 1)
set(error_message
"hash mismatch (expected ${hash}, got ${actual_hash})")
endif()
endif()
if(error_code EQUAL 0)
message(STATUS "Downloading ${url} - success on attempt ${i}")
set(download_success TRUE)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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"
@@ -870,9 +904,19 @@ AdvSceneSwitcher.condition.streamDeck.stopListen="Stop listening"
AdvSceneSwitcher.condition.streamDeck.pluginDownload="<html><head/><body><p>The Stream Deck plugin can be found <a href=\"https://github.com/WarmUpTill/advanced-scene-switcher-streamdeck-plugin/releases\"><span style=\" text-decoration: underline; color:#268bd2;\">here on GitHub</span></a>.</p></body></html>"
AdvSceneSwitcher.condition.gameCapture="Game capture"
AdvSceneSwitcher.condition.gameCapture.entry="{{sources}}hooked a game."
AdvSceneSwitcher.condition.screenshot="Screenshot"
AdvSceneSwitcher.condition.screenshot.entry="A screenshot was taken"
AdvSceneSwitcher.condition.nowPlaying="Now Playing"
AdvSceneSwitcher.condition.nowPlaying.checkType.playbackState="Playback state"
AdvSceneSwitcher.condition.nowPlaying.checkType.title="Title"
AdvSceneSwitcher.condition.nowPlaying.checkType.artist="Artist"
AdvSceneSwitcher.condition.nowPlaying.checkType.album="Album"
AdvSceneSwitcher.condition.nowPlaying.checkType.appName="App name"
AdvSceneSwitcher.condition.nowPlaying.playbackState.playing="Playing"
AdvSceneSwitcher.condition.nowPlaying.playbackState.paused="Paused"
AdvSceneSwitcher.condition.nowPlaying.playbackState.stopped="Stopped"
AdvSceneSwitcher.condition.nowPlaying.playbackState.opening="Opening"
AdvSceneSwitcher.condition.nowPlaying.playbackState.changing="Changing"
AdvSceneSwitcher.condition.mqtt="MQTT"
AdvSceneSwitcher.condition.mqtt.layout.match="Message was received from{{connection}} which matches{{regex}}:"
AdvSceneSwitcher.condition.mqtt.layout.listen="Set message selection to incoming message:{{listenButton}}"
@@ -994,7 +1038,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 +1074,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 +1166,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 +1672,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 +2502,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"
@@ -2554,6 +2609,12 @@ AdvSceneSwitcher.tempVar.gameCapture.class.description="Window class of the appl
AdvSceneSwitcher.tempVar.gameCapture.executable="Executable"
AdvSceneSwitcher.tempVar.gameCapture.executable.description="Executable name of the application captured by the source."
AdvSceneSwitcher.tempVar.nowPlaying.title="Title"
AdvSceneSwitcher.tempVar.nowPlaying.artist="Artist"
AdvSceneSwitcher.tempVar.nowPlaying.album="Album"
AdvSceneSwitcher.tempVar.nowPlaying.appName="App name"
AdvSceneSwitcher.tempVar.nowPlaying.playbackStatus="Playback status"
AdvSceneSwitcher.tempVar.http.status="Status code"
AdvSceneSwitcher.tempVar.http.error="Error"
AdvSceneSwitcher.tempVar.http.error.description="Empty when no error occurred.\nOther possible values:\n\n * Could not establish connection\n * Failed to bind IP address\n * Failed to read connection\n * Failed to write connection\n * Maximum redirect count exceeded\n * Connection handling canceled\n * SSL connection failed\n * SSL certificate loading failed\n * SSL server verification failed\n * Unsupported HTTP multipart boundary characters\n * Compression failed\n * Connection timed out\n * Proxy connection failed\n * Unknown"

File diff suppressed because it is too large Load Diff

Binary file not shown.

2
deps/opencv vendored

1
deps/opencv_contrib vendored Submodule

Submodule deps/opencv_contrib added at 755e50675d

1
deps/whisper.cpp vendored Submodule

Submodule deps/whisper.cpp added at 306c88f4d1

View File

@@ -114,6 +114,8 @@ public slots:
void RenameSelectedMacro();
void ExportMacros() const;
void ImportMacros();
void PauseSelectedMacros();
void UnpauseSelectedMacros();
void HighlightOnChange() const;
void on_macroSettings_clicked();

View 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

View 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

View File

@@ -1853,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()) {

View File

@@ -34,6 +34,7 @@ public:
void PasteMacroSegment();
bool IsEmpty() const;
void ShowAllMacroSections();
void HideElseSection() const;
private slots:
void on_conditionAdd_clicked();

View File

@@ -745,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);
@@ -805,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 &macro : 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 &macro : 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 &&

View File

@@ -981,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)
{

View File

@@ -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();

View File

@@ -45,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)
@@ -54,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();
@@ -114,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();
}
@@ -143,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);
@@ -156,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);
}
}
@@ -187,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,
@@ -195,6 +211,7 @@ ActionQueueSettingsDialog::ActionQueueSettingsDialog(QWidget *parent,
_runOnStartup->setChecked(settings._runOnStartup);
_resolveVariablesOnAdd->setChecked(settings._resolveVariablesOnAdd);
_cloneVariableContext->setChecked(settings._cloneVariableContext);
UpdateLabels();
auto layout = new QGridLayout();
@@ -224,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;
@@ -253,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;
}

View File

@@ -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;
};

View File

@@ -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:

View File

@@ -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()

View File

@@ -1,6 +1,7 @@
#include "websocket-api.hpp"
#include "obs-websocket-api.h"
#include "plugin-state-helpers.hpp"
#include "version.h"
// Must be after "obs-websocket-api.h" to avoid logging function conflict
#include "log-helper.hpp"
@@ -15,6 +16,7 @@ static constexpr char VendorName[] = "AdvancedSceneSwitcher";
static constexpr char VendorRequestStart[] = "AdvancedSceneSwitcherStart";
static constexpr char VendorRequestStop[] = "AdvancedSceneSwitcherStop";
static constexpr char VendorRequestStatus[] = "IsAdvancedSceneSwitcherRunning";
static constexpr char VendorRequestVersion[] = "AdvancedSceneSwitcherVersion";
static obs_websocket_vendor vendor;
static void registerWebsocketVendor();
@@ -82,6 +84,12 @@ static void registerWebsocketVendor()
obs_data_set_bool(response, "isRunning",
PluginIsRunning());
});
registerWebsocketVendorRequest(
VendorRequestVersion,
[](obs_data_t *, obs_data_t *response, void *) {
obs_data_set_string(response, "version", g_GIT_TAG);
obs_data_set_string(response, "commit", g_GIT_SHA1);
});
}
const char *GetWebsocketVendorName()

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -41,6 +41,8 @@ add_plugin(stream-deck)
add_plugin(twitch)
add_plugin(usb)
add_plugin(video)
add_plugin(speech)
add_plugin(now-playing)
# ---------------------------------------------------------------------------- #

View File

@@ -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})

View File

@@ -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})

View File

@@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.14)
project(advanced-scene-switcher-now-playing)
if(NOT WIN32)
message(WARNING "Now playing condition is only supported on Windows.")
return()
endif()
add_library(${PROJECT_NAME} MODULE)
target_sources(${PROJECT_NAME} PRIVATE macro-condition-now-playing.cpp
macro-condition-now-playing.hpp)
target_link_libraries(${PROJECT_NAME} PRIVATE windowsapp)
setup_advss_plugin(${PROJECT_NAME})
set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "")
install_advss_plugin(${PROJECT_NAME})

View File

@@ -0,0 +1,338 @@
#include "macro-condition-now-playing.hpp"
#include "layout-helpers.hpp"
#include "sync-helpers.hpp"
#include <util/base.h>
#include <roapi.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Media.Control.h>
namespace advss {
using namespace winrt::Windows::Media::Control;
const std::string MacroConditionNowPlaying::id = "now_playing";
bool MacroConditionNowPlaying::_registered = MacroConditionFactory::Register(
MacroConditionNowPlaying::id,
{MacroConditionNowPlaying::Create, MacroConditionNowPlayingEdit::Create,
"AdvSceneSwitcher.condition.nowPlaying"});
// ---------------------------------------------------------------------------
struct SessionInfo {
bool hasSession = false;
std::string title;
std::string artist;
std::string album;
std::string appName;
GlobalSystemMediaTransportControlsSessionPlaybackStatus playbackStatus =
GlobalSystemMediaTransportControlsSessionPlaybackStatus::Closed;
};
static bool initWinRT()
{
HRESULT hr = RoInitialize(RO_INIT_MULTITHREADED);
// S_OK: initialized, S_FALSE: already initialized with same type,
// RPC_E_CHANGED_MODE: thread is STA (OBS UI thread) - WinRT still usable
return SUCCEEDED(hr) || hr == RPC_E_CHANGED_MODE;
}
static SessionInfo getCurrentSessionInfo()
{
thread_local bool initialized = initWinRT();
(void)initialized;
SessionInfo info;
try {
auto manager =
GlobalSystemMediaTransportControlsSessionManager::
RequestAsync()
.get();
auto session = manager.GetCurrentSession();
if (!session) {
return info;
}
info.hasSession = true;
info.appName = winrt::to_string(session.SourceAppUserModelId());
auto playbackInfo = session.GetPlaybackInfo();
info.playbackStatus = playbackInfo.PlaybackStatus();
auto props = session.TryGetMediaPropertiesAsync().get();
if (props) {
info.title = winrt::to_string(props.Title());
info.artist = winrt::to_string(props.Artist());
info.album = winrt::to_string(props.AlbumTitle());
}
} catch (const winrt::hresult_error &e) {
blog(LOG_WARNING, "now playing: WinRT error 0x%08X",
static_cast<uint32_t>(e.code()));
}
return info;
}
static std::string PlaybackStatusToString(
GlobalSystemMediaTransportControlsSessionPlaybackStatus status)
{
switch (status) {
case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Playing:
return "Playing";
case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Paused:
return "Paused";
case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Stopped:
return "Stopped";
case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Opened:
return "Opening";
case GlobalSystemMediaTransportControlsSessionPlaybackStatus::Changing:
return "Changing";
default:
return "Closed";
}
}
// ---------------------------------------------------------------------------
bool MacroConditionNowPlaying::CheckCondition()
{
const auto info = getCurrentSessionInfo();
SetTempVarValue("title", info.title);
SetTempVarValue("artist", info.artist);
SetTempVarValue("album", info.album);
SetTempVarValue("appName", info.appName);
SetTempVarValue("playbackStatus",
PlaybackStatusToString(info.playbackStatus));
if (!info.hasSession) {
return false;
}
auto textMatches = [this](const std::string &field) -> bool {
const QString pattern = QString::fromStdString(_matchText);
const QString value = QString::fromStdString(field);
if (_regex.Enabled()) {
return _regex.Matches(value, pattern);
}
return value == pattern;
};
switch (_checkType) {
case CheckType::PLAYBACK_STATE: {
using S =
GlobalSystemMediaTransportControlsSessionPlaybackStatus;
switch (_playbackState) {
case PlaybackState::PLAYING:
return info.playbackStatus == S::Playing;
case PlaybackState::PAUSED:
return info.playbackStatus == S::Paused;
case PlaybackState::STOPPED:
return info.playbackStatus == S::Stopped;
case PlaybackState::OPENING:
return info.playbackStatus == S::Opened;
case PlaybackState::CHANGING:
return info.playbackStatus == S::Changing;
}
break;
}
case CheckType::TITLE:
return textMatches(info.title);
case CheckType::ARTIST:
return textMatches(info.artist);
case CheckType::ALBUM:
return textMatches(info.album);
case CheckType::APP_NAME:
return textMatches(info.appName);
}
return false;
}
bool MacroConditionNowPlaying::Save(obs_data_t *obj) const
{
MacroCondition::Save(obj);
obs_data_set_int(obj, "checkType", static_cast<int>(_checkType));
obs_data_set_int(obj, "playbackState",
static_cast<int>(_playbackState));
_matchText.Save(obj, "matchText");
_regex.Save(obj);
return true;
}
bool MacroConditionNowPlaying::Load(obs_data_t *obj)
{
MacroCondition::Load(obj);
_checkType = static_cast<CheckType>(obs_data_get_int(obj, "checkType"));
_playbackState = static_cast<PlaybackState>(
obs_data_get_int(obj, "playbackState"));
_matchText.Load(obj, "matchText");
_regex.Load(obj);
return true;
}
void MacroConditionNowPlaying::SetupTempVars()
{
AddTempvar(
"title",
obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.title"));
AddTempvar(
"artist",
obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.artist"));
AddTempvar(
"album",
obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.album"));
AddTempvar(
"appName",
obs_module_text("AdvSceneSwitcher.tempVar.nowPlaying.appName"));
AddTempvar(
"playbackStatus",
obs_module_text(
"AdvSceneSwitcher.tempVar.nowPlaying.playbackStatus"));
}
// ---------------------------------------------------------------------------
static void populateCheckTypeSelection(QComboBox *list)
{
static const std::map<MacroConditionNowPlaying::CheckType, std::string> checkTypes = {
{MacroConditionNowPlaying::CheckType::PLAYBACK_STATE,
"AdvSceneSwitcher.condition.nowPlaying.checkType.playbackState"},
{MacroConditionNowPlaying::CheckType::TITLE,
"AdvSceneSwitcher.condition.nowPlaying.checkType.title"},
{MacroConditionNowPlaying::CheckType::ARTIST,
"AdvSceneSwitcher.condition.nowPlaying.checkType.artist"},
{MacroConditionNowPlaying::CheckType::ALBUM,
"AdvSceneSwitcher.condition.nowPlaying.checkType.album"},
{MacroConditionNowPlaying::CheckType::APP_NAME,
"AdvSceneSwitcher.condition.nowPlaying.checkType.appName"},
};
for (const auto &[type, name] : checkTypes) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(type));
}
}
static void populatePlaybackStateSelection(QComboBox *list)
{
static const std::map<MacroConditionNowPlaying::PlaybackState, std::string> playbackStates = {
{MacroConditionNowPlaying::PlaybackState::PLAYING,
"AdvSceneSwitcher.condition.nowPlaying.playbackState.playing"},
{MacroConditionNowPlaying::PlaybackState::PAUSED,
"AdvSceneSwitcher.condition.nowPlaying.playbackState.paused"},
{MacroConditionNowPlaying::PlaybackState::STOPPED,
"AdvSceneSwitcher.condition.nowPlaying.playbackState.stopped"},
{MacroConditionNowPlaying::PlaybackState::OPENING,
"AdvSceneSwitcher.condition.nowPlaying.playbackState.opening"},
{MacroConditionNowPlaying::PlaybackState::CHANGING,
"AdvSceneSwitcher.condition.nowPlaying.playbackState.changing"},
};
for (const auto &[state, name] : playbackStates) {
list->addItem(obs_module_text(name.c_str()),
static_cast<int>(state));
}
}
MacroConditionNowPlayingEdit::MacroConditionNowPlayingEdit(
QWidget *parent, std::shared_ptr<MacroConditionNowPlaying> entryData)
: QWidget(parent),
_checkType(new QComboBox(this)),
_playbackState(new QComboBox(this)),
_matchText(new VariableLineEdit(this)),
_regex(new RegexConfigWidget(this))
{
populateCheckTypeSelection(_checkType);
populatePlaybackStateSelection(_playbackState);
connect(_checkType, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MacroConditionNowPlayingEdit::CheckTypeChanged);
connect(_playbackState,
QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&MacroConditionNowPlayingEdit::PlaybackStateChanged);
connect(_matchText, &VariableLineEdit::textEdited, this,
&MacroConditionNowPlayingEdit::MatchTextChanged);
connect(_regex, &RegexConfigWidget::RegexConfigChanged, this,
&MacroConditionNowPlayingEdit::RegexChanged);
_layout = new QHBoxLayout();
_layout->addWidget(_checkType);
_layout->addWidget(_playbackState);
_layout->addWidget(_matchText);
_layout->addWidget(_regex);
setLayout(_layout);
_entryData = entryData;
UpdateEntryData();
_loading = false;
}
void MacroConditionNowPlayingEdit::UpdateEntryData()
{
if (!_entryData) {
return;
}
_checkType->setCurrentIndex(
_checkType->findData(static_cast<int>(_entryData->_checkType)));
_playbackState->setCurrentIndex(_playbackState->findData(
static_cast<int>(_entryData->_playbackState)));
_matchText->setText(QString::fromStdString(_entryData->_matchText));
_regex->SetRegexConfig(_entryData->_regex);
SetWidgetVisibility();
}
void MacroConditionNowPlayingEdit::CheckTypeChanged(int)
{
GUARD_LOADING_AND_LOCK();
_entryData->_checkType =
static_cast<MacroConditionNowPlaying::CheckType>(
_checkType->currentData().toInt());
SetWidgetVisibility();
}
void MacroConditionNowPlayingEdit::PlaybackStateChanged(int)
{
GUARD_LOADING_AND_LOCK();
_entryData->_playbackState =
static_cast<MacroConditionNowPlaying::PlaybackState>(
_playbackState->currentData().toInt());
}
void MacroConditionNowPlayingEdit::MatchTextChanged(const QString &text)
{
GUARD_LOADING_AND_LOCK();
_entryData->_matchText = text.toStdString();
}
void MacroConditionNowPlayingEdit::RegexChanged(const RegexConfig &regex)
{
GUARD_LOADING_AND_LOCK();
_entryData->_regex = regex;
}
void MacroConditionNowPlayingEdit::SetWidgetVisibility()
{
const bool isStateCheck =
_entryData->_checkType ==
MacroConditionNowPlaying::CheckType::PLAYBACK_STATE;
_playbackState->setVisible(isStateCheck);
_matchText->setVisible(!isStateCheck);
_regex->setVisible(!isStateCheck);
if (isStateCheck) {
AddStretchIfNecessary(_layout);
} else {
RemoveStretchIfPresent(_layout);
}
adjustSize();
updateGeometry();
}
} // namespace advss

View File

@@ -0,0 +1,86 @@
#pragma once
#include "macro-condition-edit.hpp"
#include "regex-config.hpp"
#include "variable-line-edit.hpp"
#include <QComboBox>
namespace advss {
class MacroConditionNowPlaying : public MacroCondition {
public:
enum class CheckType {
PLAYBACK_STATE,
TITLE,
ARTIST,
ALBUM,
APP_NAME,
};
enum class PlaybackState {
PLAYING,
PAUSED,
STOPPED,
OPENING,
CHANGING,
};
MacroConditionNowPlaying(Macro *m) : MacroCondition(m) {}
bool CheckCondition();
bool Save(obs_data_t *obj) const;
bool Load(obs_data_t *obj);
std::string GetId() const { return id; }
static std::shared_ptr<MacroCondition> Create(Macro *m)
{
return std::make_shared<MacroConditionNowPlaying>(m);
}
CheckType _checkType = CheckType::PLAYBACK_STATE;
PlaybackState _playbackState = PlaybackState::PLAYING;
StringVariable _matchText;
RegexConfig _regex;
private:
void SetupTempVars();
static bool _registered;
static const std::string id;
};
class MacroConditionNowPlayingEdit : public QWidget {
Q_OBJECT
public:
MacroConditionNowPlayingEdit(
QWidget *parent,
std::shared_ptr<MacroConditionNowPlaying> cond = nullptr);
void UpdateEntryData();
static QWidget *Create(QWidget *parent,
std::shared_ptr<MacroCondition> cond)
{
return new MacroConditionNowPlayingEdit(
parent,
std::dynamic_pointer_cast<MacroConditionNowPlaying>(
cond));
}
private slots:
void CheckTypeChanged(int);
void PlaybackStateChanged(int);
void MatchTextChanged(const QString &);
void RegexChanged(const RegexConfig &);
private:
void SetWidgetVisibility();
QHBoxLayout *_layout;
QComboBox *_checkType;
QComboBox *_playbackState;
VariableLineEdit *_matchText;
RegexConfigWidget *_regex;
std::shared_ptr<MacroConditionNowPlaying> _entryData;
bool _loading = true;
};
} // namespace advss

View File

@@ -28,9 +28,7 @@ endif()
if(EXISTS "${OpenVR_DIR}/CMakeLists.txt")
set(SIZEOF_VOIDP ${CMAKE_SIZEOF_VOID_P})
if(WIN32 AND CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|arm64|aarch64")
set(PROCESSOR_ARCH "arm64")
elseif((NOT APPLE) AND (CMAKE_SIZEOF_VOID_P EQUAL 8))
if((NOT APPLE) AND (CMAKE_SIZEOF_VOID_P EQUAL 8))
set(PROCESSOR_ARCH "64")
else()
set(PROCESSOR_ARCH "32")

View File

@@ -0,0 +1,130 @@
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
-Wno-shorten-64-to-32
-Wno-ambiguous-macro>
$<$<CXX_COMPILER_ID:GNU,Clang,AppleClang>:-Wno-error
-Wno-shorten-64-to-32
-Wno-ambiguous-macro>
$<$<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})

View 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

View 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

View 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

View 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

View File

@@ -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})

View File

@@ -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 "")

View File

@@ -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 {

View File

@@ -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 {

View 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

View 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

View File

@@ -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);

View File

@@ -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;

View 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

View File

@@ -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()) {

View File

@@ -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);

View File

@@ -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

View File

@@ -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");

View File

@@ -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 &params)
void PreviewDialog::CascadeClassifierParametersChanged(
const CascadeClassifierParameters &params)
{
std::unique_lock<std::mutex> lock(_mtx);
_objDetectParams = std::make_shared<ObjDetectParameters>(params);
_cascadeParams = std::make_shared<CascadeClassifierParameters>(params);
}
void PreviewDialog::OCRParametersChanged(const OCRParameters &params)
@@ -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"));

View File

@@ -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;

View File

@@ -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 {

View 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

View 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

View 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

View 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

View 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