├── src ├── pch.cpp ├── pch.h └── main.cpp ├── doc └── image.png ├── .gitmodules ├── vcpkg.json ├── .gitignore ├── CMakeLists.txt ├── .github └── FUNDING.yml ├── CMakePresets.json ├── README.md └── LICENSE /src/pch.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" -------------------------------------------------------------------------------- /doc/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/corbamico/get-livecaptions-cpp/HEAD/doc/image.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vcpkg"] 2 | path = vcpkg 3 | url = https://github.com/microsoft/vcpkg 4 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "main", 3 | "version-string": "latest", 4 | "dependencies": [ 5 | "asio", 6 | "wil", 7 | "argparse" 8 | ] 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeLists.txt.user 2 | CMakeCache.txt 3 | CMakeFiles 4 | CMakeScripts 5 | Testing 6 | Makefile 7 | cmake_install.cmake 8 | install_manifest.txt 9 | compile_commands.json 10 | CTestTestfile.cmake 11 | _deps 12 | 13 | .vscode 14 | build 15 | build* 16 | vcpkg 17 | -------------------------------------------------------------------------------- /src/pch.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define ASIO_NO_WIN32_LEAN_AND_MEAN 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.25) 2 | 3 | set(CMAKE_CXX_STANDARD 23) 4 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 5 | set(CMAKE_CXX_EXTENSIONS OFF) 6 | 7 | project(get-livecaptions) 8 | add_executable(get-livecaptions src/main.cpp src/pch.cpp) 9 | target_precompile_headers(get-livecaptions PRIVATE src/pch.h) 10 | 11 | find_package(asio CONFIG REQUIRED) 12 | find_package(wil CONFIG REQUIRED) 13 | find_package(argparse CONFIG REQUIRED) 14 | target_link_libraries(get-livecaptions PRIVATE WIL::WIL asio::asio argparse::argparse) 15 | 16 | #if you use lastest cppwinrt from vcpkg, uncomment following 17 | #find_package(cppwinrt CONFIG REQUIRED) 18 | #target_link_libraries(get-livecaptions PRIVATE WIL::WIL asio::asio Microsoft::CppWinRT RuntimeObject.lib) 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: corbamico # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 21, 6 | "patch": 0 7 | }, 8 | "configurePresets": [ 9 | { 10 | "name": "ninja-multi-vcpkg", 11 | "displayName": "Ninja Multi-Config", 12 | "description": "Configure with vcpkg toolchain and generate Ninja project files for all configurations", 13 | "binaryDir": "${sourceDir}/builds/${presetName}", 14 | "generator": "Ninja Multi-Config", 15 | "toolchainFile": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake" 16 | } 17 | ], 18 | "buildPresets": [ 19 | { 20 | "name": "ninja-vcpkg-debug", 21 | "configurePreset": "ninja-multi-vcpkg", 22 | "displayName": "Build (Debug)", 23 | "description": "Build with Ninja/vcpkg (Debug)", 24 | "configuration": "Debug" 25 | }, 26 | { 27 | "name": "ninja-vcpkg-release", 28 | "configurePreset": "ninja-multi-vcpkg", 29 | "displayName": "Build (Release)", 30 | "description": "Build with Ninja/vcpkg (Release)", 31 | "configuration": "Release" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # get-livecaptions-cpp 2 | Get real time content of Windows System APP "Live Captions" [win+ctrl+L], write content into file. using c++/winrt, asio 3 | 4 | check slibing project [get-livecaptions-rs](https://github.com/corbamico/get-livecaptions-rs) 5 | 6 | ## Usage 7 | 8 | ```cmd 9 | Optional arguments: 10 | -h, --help shows help message and exits 11 | -v, --version prints version information and exits 12 | -o, --output file filename, write content into file. use - for console. [required] 13 | ``` 14 | 15 | ## UIAutomation 16 | 17 | To find the LiveCaptions GUI AutomationID, you may need tools as [inspect](https://learn.microsoft.com/en-us/windows/win32/winauto/inspect-objects), or [Automation-Spy](https://github.com/ddeltasolutions/Automation-Spy) 18 | 19 | ![](./doc/image.png) 20 | 21 | ## License 22 | 23 | Licensed under either of 24 | 25 | * Apache License, Version 2.0 26 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 27 | * MIT license 28 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 29 | 30 | at your option. 31 | 32 | ## Contribution 33 | 34 | Unless you explicitly state otherwise, any contribution intentionally submitted 35 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 36 | dual licensed as above, without any additional terms or conditions. 37 | 38 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "pch.h" 2 | 3 | using namespace winrt; 4 | using namespace winrt::Windows::Foundation; 5 | using namespace winrt::Windows::UI::UIAutomation; 6 | 7 | #define VERSION_STRING "0.1.240911" 8 | 9 | std::string get_current_time() 10 | { 11 | // [TIPS]c+20 runtime is too expensive, + 500k for following implement. 12 | // 13 | // auto const time = std::chrono::current_zone() 14 | // ->to_local(std::chrono::system_clock::now()); 15 | // return std::format("{:%Y/%m/%d %X}", time); 16 | 17 | auto now = std::chrono::system_clock::now(); 18 | std::stringstream ss; 19 | std::time_t now_c = std::chrono::system_clock::to_time_t(now); 20 | ss< _automation; 27 | winrt::com_ptr _condition; 28 | std::string _prebuffer; 29 | std::string _sfilename; 30 | 31 | winrt::hstring get_livecaptions() 32 | { 33 | wil::unique_bstr text; 34 | winrt::com_ptr window_element; 35 | winrt::com_ptr text_element; 36 | 37 | try{ 38 | auto window = FindWindowW(L"LiveCaptionsDesktopWindow", nullptr); 39 | winrt::check_hresult(_automation->ElementFromHandle(window, window_element.put())); 40 | winrt::check_hresult(window_element->FindFirst(TreeScope_Descendants, _condition.get(), text_element.put())); 41 | if (text_element) 42 | { 43 | winrt::check_hresult(text_element->get_CurrentName(text.put())); 44 | return text.get(); 45 | } 46 | 47 | return winrt::hstring(); 48 | } 49 | catch (winrt::hresult_error &e){} 50 | catch (std::exception &e){} 51 | return winrt::hstring(); 52 | } 53 | void ostream_captions(std::ostream &os) 54 | { 55 | auto hs_current = get_livecaptions(); 56 | if(hs_current.empty()) return; 57 | auto current = winrt::to_string(hs_current); 58 | 59 | std::vector lines; 60 | std::string line; 61 | std::istringstream iss(current); 62 | while (std::getline(iss, line)) 63 | { 64 | lines.emplace_back(line); 65 | } 66 | // Find the first line not in the prebuffer 67 | size_t first_new_line = lines.size(); 68 | for (size_t i = 0; i < lines.size(); i++) 69 | { 70 | if (_prebuffer.find(lines[i]) == std::string::npos) 71 | { 72 | first_new_line = i; 73 | break; 74 | } 75 | } 76 | _prebuffer = current; 77 | 78 | if (first_new_line < lines.size()) 79 | { 80 | // Append new lines to the file and prebuffer 81 | os << "[" << get_current_time() << "] " << std::endl; 82 | 83 | for (size_t i = first_new_line; i < lines.size(); ++i) 84 | { 85 | os << lines[i] << std::endl; 86 | } 87 | } 88 | } 89 | public: 90 | Engine(const std::string &filename) : _sfilename{filename} 91 | { 92 | winrt::init_apartment(); 93 | _automation = try_create_instance(guid_of()); 94 | winrt::check_hresult(_automation->CreatePropertyCondition(UIA_AutomationIdPropertyId, wil::make_variant_bstr(L"CaptionsTextBlock"), _condition.put())); 95 | } 96 | ~Engine() { winrt::uninit_apartment(); } 97 | 98 | static bool is_livecaption_running() 99 | { 100 | return FindWindowW(L"LiveCaptionsDesktopWindow", nullptr) != NULL; 101 | } 102 | 103 | void ouput_captions() 104 | { 105 | if(_sfilename.compare("-")==0) 106 | { 107 | ostream_captions(std::cout); 108 | return; 109 | } 110 | std::ofstream of(_sfilename,std::ios::app); 111 | if (of.is_open()) 112 | { 113 | ostream_captions(of); 114 | of.flush(); 115 | of.close(); 116 | } 117 | } 118 | bool touch_file() 119 | { 120 | if(_sfilename.compare("-")==0) return true; 121 | 122 | std::ofstream file(_sfilename,std::ios::app); 123 | auto ret = file.is_open(); 124 | file.close(); 125 | return ret; 126 | } 127 | }; 128 | 129 | int main(int argc, char *argv[]) 130 | { 131 | 132 | 133 | std::string strFileName; 134 | argparse::ArgumentParser program("get-livecaptions",VERSION_STRING); 135 | program.add_argument("-o", "--output") 136 | .metavar("file") 137 | .help("filename, write content into file. use - for console.") 138 | .required(); 139 | 140 | program.add_description("Write the content of LiveCaptions Windows System Program into file, continually."); 141 | program.add_epilog("use ctrl-c to exit program."); 142 | 143 | try { 144 | if(argc==1) {program.print_help();exit(1);} 145 | program.parse_args(argc, argv); 146 | strFileName = program.get("--output"); 147 | 148 | if (!Engine::is_livecaption_running()) 149 | { 150 | std::cerr << "[Error]Live Captions is not running." < asio::awaitable 173 | { 174 | asio::steady_timer timer_10s(io_context); 175 | ULONG64 ulCount{0}; 176 | while(true){ 177 | timer_10s.expires_after(asio::chrono::seconds(10)); 178 | co_await timer_10s.async_wait(asio::use_awaitable); 179 | //std::cout << "every 10s" <