Merge branch 'for_webui' into remove_files

# Conflicts:
#	build.bat
#	install.bat
#	locales/en.yml
#	locales/ja.yml
#	locales/ko.yml
#	locales/readme_first.txt
#	locales/zh-Hant.yml
#	requirements.txt
#	src-python/models/overlay/overlay_utils.py
#	src-python/models/transcription/transcription_languages.py
#	src-python/models/transcription/transcription_recorder.py
#	src-python/models/transcription/transcription_transcriber.py
#	src-python/models/transcription/transcription_whisper.py
#	src-python/models/translation/translation_languages.py
#	src-python/models/translation/translation_translator.py
#	src-python/models/translation/translation_utils.py
#	src-ui/assets/about_vrct/contributors_members.png
#	src-ui/assets/about_vrct/contributors_section_title.png
#	src-ui/assets/about_vrct/dev_section_title.png
#	src-ui/assets/about_vrct/localization_section_title.png
#	src-ui/assets/about_vrct/poster_showcase_section_title.png
#	src-ui/assets/about_vrct/project_link_booth.png
#	src-ui/assets/about_vrct/project_link_contact_us.png
#	src-ui/assets/about_vrct/project_link_documents.png
#	src-ui/assets/about_vrct/project_link_vrct_github.png
#	src-ui/assets/about_vrct/showcased_worlds/language_exchange_tervern.png
#	src-ui/assets/about_vrct/showcased_worlds/silakan_datang_ke_rumahku.png
#	src-ui/assets/about_vrct/showcased_worlds/sushi_stand_guruguru.png
#	src-ui/assets/about_vrct/special_thanks_message_en.png
#	src-ui/assets/about_vrct/special_thanks_message_ja.png
#	src-ui/assets/about_vrct/special_thanks_section_title.png
#	src-ui/assets/about_vrct/vrchat_disclaimer.png
#	src-ui/assets/about_vrct/vrct_logo_for_about_vrct.png
#	src-ui/assets/about_vrct/vrct_posters/authors/poster_images_authors_en.png
#	src-ui/assets/about_vrct/vrct_posters/authors/poster_images_authors_ja.png
#	src-ui/assets/about_vrct/vrct_posters/authors/poster_images_authors_m_en.png
#	src-ui/assets/about_vrct/vrct_posters/authors/poster_images_authors_m_ja.png
#	src-ui/assets/about_vrct/vrct_posters/iya_vrct_manga_en.png
#	src-ui/assets/about_vrct/vrct_posters/iya_vrct_manga_ja.png
#	src-ui/assets/about_vrct/vrct_posters/iya_vrct_manga_ko.png
#	src-ui/assets/about_vrct/vrct_posters/iya_vrct_poster_cn.png
#	src-ui/assets/about_vrct/vrct_posters/iya_vrct_poster_en.png
#	src-ui/assets/about_vrct/vrct_posters/iya_vrct_poster_ja.png
#	src-ui/assets/about_vrct/vrct_posters/iya_vrct_poster_ko.png
#	src-ui/assets/chato_white.png
#	src-ui/assets/chato_white_square.png
#	src-ui/assets/swap_icon.png
#	src-ui/assets/vrct_logo_for_dark_mode.png
This commit is contained in:
misyaguziya
2024-12-28 17:56:14 +09:00
444 changed files with 33051 additions and 1 deletions

24
.eslintrc.json Normal file
View File

@@ -0,0 +1,24 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true // Allows for the parsing of JSX
}
},
"rules": {
"react/prop-types": "off",
"@typescript-eslint/no-empty-interface": 0,
"no-unreachable": "warn",
"no-unused-vars": "warn",
"react/react-in-jsx-scope": "off",
"semi": ["error", "always"]
},
"settings": {
"react": {
"version": "detect"
}
}
}

34
.gitignore vendored
View File

@@ -3,12 +3,44 @@ build/
dist/ dist/
/config.json /config.json
memo.txt memo.txt
VRCT.spec
*.pyc *.pyc
logs/ logs/
.venv/ .venv/
.venv_cuda/
weights/ weights/
.vscode .vscode
error.log error.log
*.exe *.exe
*.ipynb *.ipynb
# Added by WebUI migration
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
.vscode
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.venv
# Customize
/build

45
backend.spec Normal file
View File

@@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['src-python\\mainloop.py'],
pathex=[],
binaries=[],
datas=[('./fonts', 'fonts/'), ('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv/Lib/site-packages/openvr', 'openvr/'), ('.venv/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['pandas', 'matplotlib', 'PyQt5'],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='VRCT-sidecar-x86_64-pc-windows-msvc',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=[],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='.',
)

45
backend_cuda.spec Normal file
View File

@@ -0,0 +1,45 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['src-python\\mainloop.py'],
pathex=[],
binaries=[],
datas=[('./fonts', 'fonts/'), ('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('.venv_cuda/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['pandas', 'matplotlib', 'PyQt5'],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='VRCT-sidecar-x86_64-pc-windows-msvc',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=[],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='.',
)

2
build.bat Normal file
View File

@@ -0,0 +1,2 @@
call .venv/Scripts/activate
pyinstaller backend.spec --distpath src-tauri/bin --clean --noconfirm

2
build_cuda.bat Normal file
View File

@@ -0,0 +1,2 @@
call .venv_cuda/Scripts/activate
pyinstaller backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm

14
index.html Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="./assets/chato_icon_fill.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VRCT</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src-ui/app/index.jsx"></script>
</body>
</html>

10
install.bat Normal file
View File

@@ -0,0 +1,10 @@
python -m venv .venv
python -m venv .venv_cuda
call .venv/Scripts/activate
python.exe -m pip install --upgrade pip
pip install -r requirements.txt
call .venv_cuda/Scripts/activate
python.exe -m pip install --upgrade pip
pip install -r requirements_cuda.txt

38
locales/config.js Normal file
View File

@@ -0,0 +1,38 @@
import yaml from "js-yaml";
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en_yml from "./en.yml?raw";
import ja_yml from "./ja.yml?raw";
import ko_yml from "./ko.yml?raw";
import zh_hant_yml from "./zh-Hant.yml?raw";
import zh_hans_yml from "./zh-Hans.yml?raw";
const translation_en = yaml.load(en_yml);
const translation_ja = yaml.load(ja_yml);
const translation_ko = yaml.load(ko_yml);
const translation_zh_Hant = yaml.load(zh_hant_yml);
const translation_zh_Hans = yaml.load(zh_hans_yml);
const resources = {
en: { translation: translation_en },
ja: { translation: translation_ja },
ko: { translation: translation_ko },
"zh-Hant": { translation: translation_zh_Hant },
"zh-Hans": { translation: translation_zh_Hans },
};
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
lng: "en",
fallbackLng: "en",
// debug: true,
interpolation: {
escapeValue: false, // react already safes from xss
},
});
export default i18n;

268
locales/en.yml Normal file
View File

@@ -0,0 +1,268 @@
# =================================
# IMPORTANT:
# Please read 'readme_first.txt' before making any changes.
# =================================
common:
go_back_button_label: Go Back
main_page:
translation: Translation
transcription_send: Voice2Chatbox
transcription_receive: Speaker2Log
foreground: Foreground
language_settings: Language Settings
your_language: Your Language
translate_each_other_label: Translate Each Other
swap_button_label: Swap Languages
target_language: Target Language
translator: Translator
translator_ctranslate2: Internal (Default)
translator_selector:
is_selected_same_language: |-
Since the same language is selected for both '{{your_language}}' and '{{target_language}}', only '{{translator_ctranslate2}}' is available.
message_log:
all: All
sent: Sent
received: Received
system: System
show_resend_button: Show Resend Button
resend_button_on_hover_desc: Press and hold to send
# textbox_system_message:
# enabled_easter_egg: Whoa! You caught us! There is something...like...easter-egg-ish function has enabled! It'll affect to Overlay(VR) for now;).
# enabled_translation: Translation feature is turned on.
# disabled_translation: Translation feature is turned off.
# enabled_voice2chatbox: Transcription from the microphone has started.
# disabled_voice2chatbox: Transcription from the microphone has been stopped.
# enabled_speaker2log: Transcription from the speaker has started.
# disabled_speaker2log: Transcription from the speaker has been stopped.
# enabled_foreground: The screen is fixed in the foreground.
# disabled_foreground: The foreground fixation has been released.
# auth_key_success: Auth key update completed.
# auth_key_error: Auth Key is incorrect or Usage limit reached.
# no_mic_device_detected_error: No mic device detected.
# no_speaker_device_detected_error: No speaker device detected.
# translation_engine_limit_error: It has automatically changed the translation engine. Access has been temporarily restricted due to an excessive number of requests to the translation engine. If you want to use the same translation engine, please wait for a while, restart VRCT, and try again.
# detected_by_word_filter: The word {{detected_message}} has not been sent due to detection by the word filter.
# selected_your_language: '"Your Language" has set to {{your_language}}.'
# selected_target_language: '"Target Language" has set to {{target_language}}.'
# switched_language_preset_tab: Switched to Language Preset Tab No.{{tab_no}}."
# latest_language_setting: Currently, "Your Language" is set to {{your_language}}, and "Target Language" is set to {{target_language}}.
# opened_web_page_booth: Opened Booth page in your web browser.
# opened_web_page_vrct_documents: |-
# Opened VRCT Documents page in your web browser.
# For any issues, requests, or inquiries, please feel free to contact us through the links at the bottom of the documents page, the "Contact Form," or via X (formerly Twitter)!
state_text_enabled: Enabled
state_text_disabled: Disabled
language_selector:
title_your_language: Select Your Language
title_target_language: Select Target Language
update_available: New version is here!
updating: Now updating...
update_modal:
cpu_desc: Use CPU only as the compute device.
cuda_desc: Selectable between CPU and NVIDIA GPUs as compute devices.
cuda_compare_cpu_desc: With GPU selection, processing is faster compared to a CPU.
cuda_disk_space_desc: Requires approximately {{size}} of disk space.
close_modal: Close
download_latest_and_restart: |-
The latest version will be downloaded,
and the app will automatically restart.
is_latest_version_already: Already using the latest version
is_current_compute_device: Currently using this version
config_page:
version: version {{version}}
# config_title: Settings
# compact_mode: Compact Mode
# restart_message: Apply changes with a restart.
# common_error_message:
# invalid_value: Invalid value.
model_download_button_label: Download
side_menu_labels:
device: Device
appearance: Appearance
translation: Translation
transcription: Transcription
vr: VR
others: Others
advanced_settings: Advanced Settings
supporters: Supporters
about_vrct: About VRCT
device:
check_volume: Check Volume
mic_host_device:
label: Mic Device
label_auto_select: Auto Select
label_host: Host/Driver
label_device: Device
mic_dynamic_energy_threshold:
label_for_automatic: 'Mic Energy Threshold (Current Setting: Automatic)'
desc_for_automatic: Automatically determine microphone input sensitivity.
label_for_manual: 'Mic Energy Threshold (Current Setting: Manual)'
desc_for_manual: Manually determine the microphone input sensitivity using the slider. Press the microphone icon to input your voice and adjust the sensitivity while monitoring the volume.
error_message: You can set it with a value between 0 to {{max}}.
speaker_device:
label: Speaker Device
label_auto_select: Auto Select
label_device: Device
speaker_dynamic_energy_threshold:
label_for_automatic: 'Speaker Energy Threshold (Current Setting: Automatic)'
desc_for_automatic: Automatically determine speaker input sensitivity.
label_for_manual: 'Speaker Energy Threshold (Current Setting: Manual)'
desc_for_manual: Manually determine the speaker input sensitivity using the slider. Press the headphones icon to listen to the audio and adjust the sensitivity while monitoring the volume.
error_message: You can set it with a value between 0 to {{max}}.
no_device_error_message: No speaker device detected.
appearance:
transparency:
label: Transparency
desc: Change the main window's transparency.
ui_size:
label: UI Size
textbox_ui_size:
label: Message Logs Font Size
desc: You can adjust the font size used in the logs relative to the UI size.
send_message_button_type:
label: Send Message Button
hide: Hide (Use enter key to send)
show: Show
show_and_disable_enter_key: Show and disable to send when pressed enter key
font_family:
label: Font Family
ui_language:
label: UI Language
translation:
ctranslate2_weight_type:
label: Internal Translation Model
desc: You can choose the translation model to use for the internal translation engine.
small: Basic model ({{capacity}})
large: High accuracy model ({{capacity}})
ctranslate2_compute_device:
label: Internal Translation Compute Device
deepl_auth_key:
label: DeepL Auth Key
desc: Please select {{translator}} on the main screen with DeepL_API when using. ※Some languages may not be supported.
save: Save
edit: Edit
open_auth_key_webpage: Open DeepL Account Webpage
auth_key_success: Auth key update completed.
auth_key_error: Auth Key is incorrect or Usage limit reached.
transcription:
section_label_mic: Mic
section_label_speaker: Speaker
section_label_transcription_engines: Transcription Engines
mic_record_timeout:
label: Mic Record Timeout
desc: Detects silence and, when the specified number of seconds has passed, considers the mic input to have ended. (Second(s))
error_message: It cannot be greater than '{{mic_phrase_timeout_label}}' with a value of 0 or more.
mic_phrase_timeout:
label: Mic Phrase Timeout
desc: Transcription processing is performed at intervals of the specified number of seconds.
error_message: It cannot be set lower than '{{mic_record_timeout_label}}' with a value of 0 or more.
mic_max_phrase:
label: Mic Max Words
desc: It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs and send to VRChat.
error_message: You can set a number equal to or greater than 0.
mic_word_filter:
label: Mic Word Filter
desc: |-
If a registered word is detected, the text will not be sent. To add multiple words at once, separate them with a "," (comma).
*Duplicate words will not be registered.
add_button_label: Add
count_desc: 'Current registered word count: {{count}}'
speaker_record_timeout:
label: Speaker Record Timeout
desc: Detects silence and, when the specified number of seconds has passed, considers the speaker input to have ended. (Second(s))
error_message: It cannot be greater than '{{speaker_phrase_timeout_label}}' with a value of 0 or more.
speaker_phrase_timeout:
label: Speaker Phrase Timeout
desc: Transcription processing is performed at intervals of the specified number of seconds.
error_message: It cannot be set lower than '{{speaker_record_timeout_label}}' with a value of 0 or more.
speaker_max_phrase:
label: Speaker Max Words
desc: It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs.
error_message: You can set a number equal to or greater than 0.
select_transcription_engine:
label: Transcription Engine
whisper_weight_type:
label: Whisper Model
desc: |-
Larger models tend to have higher accuracy, but they also consume more CPU or GPU resources.
Especially for models larger than medium, it may be difficult or even impossible to use them depending on the performance of your CPU/GPU.
model_template: '{{model_name}} model ({{capacity}})'
recommended_model_template: '{{model_name}} model ({{capacity}}) (Recommended)'
whisper_compute_device:
label: Whisper Compute Device
vr:
single_line: Single line
multi_lines: Multi lines
overlay_enable: Enable
restore_default_settings: Restore Default Settings
position: Position
rotation: Rotation
x_position: X-axis (left-right)
y_position: Y-axis (up-down)
z_position: Z-axis (front-back)
x_rotation: X-axis rotation
y_rotation: Y-axis rotation
z_rotation: Z-axis rotation
sample_text_button:
start: |-
Send sample texts
to Overlay
stop: Stop Sending
sample_text: Sample text.
opacity: Opacity
ui_scaling: UI Scaling
display_duration: Display duration
fadeout_duration: Fadeout duration
tracker: Tracker
hmd: HMD
left_hand: Left hand
right_hand: Right hand
common_settings: Common Settings
overlay_show_only_translated_messages:
label: Show Only Translated Messages
others:
auto_clear_the_message_box:
label: Auto Clear The Message Box
send_only_translated_messages:
label: Send Only Translated Messages
auto_export_message_logs:
label: Auto Export Message Logs
desc: Automatically export the conversation messages as a text file.
vrc_mic_mute_sync:
label: VRC Mic Mute Sync
desc: |-
VRCT will not send the message to VRChat while VRChat's mic is muted.
*There is a bit latency and Push-To-Talk is not supported.
send_message_to_vrc:
label: Send Message To VRChat
desc: There is a way to use it without sending messages to VRChat, but it is not supported. Enable this feature when you intend to send a message to VRChat.
send_received_message_to_vrc:
label: Send Received Message To VRChat
desc: Send the message you received from the speaker's sound to VRChat's chatbox.
advanced_settings:
osc_ip_address:
label: OSC IP Address
osc_port:
label: OSC Port
open_config_filepath:
label: Open Config File
switch_compute_device:
label: Switch VRCT to CPU/GPU Version

266
locales/ja.yml Normal file
View File

@@ -0,0 +1,266 @@
# =================================
# IMPORTANT:
# Please read 'readme_first.txt' before making any changes.
# =================================
common:
go_back_button_label: 戻る
main_page:
translation: 翻訳
transcription_send: 音声認識(マイク)
transcription_receive: 音声認識(スピーカー)
foreground: 最前面表示
language_settings: 言語設定
your_language: あなたの言語
translate_each_other_label: 双方向に翻訳
swap_button_label: 言語を入れ替え
target_language: 相手の言語
translator: 翻訳エンジン
translator_ctranslate2: オフライン翻訳 (Default)
translator_selector:
is_selected_same_language: |-
「{{your_language}}」と「{{target_language}}」に同じ言語が選択がされているため、「{{translator_ctranslate2}}」のみが使用できます。
message_log:
all: 全て
sent: 送信
received: 受信
system: システム
show_resend_button: 再送信ボタンを表示する
resend_button_on_hover_desc: 長押しで送信
# textbox_system_message:
# enabled_translation: 翻訳機能をONにしました。
# disabled_translation: 翻訳機能をOFFしました。
# enabled_voice2chatbox: マイクからの音声入力、文字起こしを開始します。
# disabled_voice2chatbox: マイクからの音声入力、文字起こしを終了しました。
# enabled_speaker2log: スピーカーからの音声聞き取り、文字起こしを開始します。
# disabled_speaker2log: スピーカーからの音声聞き取り、文字起こしを終了しました。
# enabled_foreground: 画面を常に最前面へ固定します。
# disabled_foreground: 最前面への固定を解除しました。
# auth_key_success: 認証キーの更新が完了しました。
# auth_key_error: 認証キーが間違っているか、API使用制限が上限に達しています。
# no_mic_device_detected_error: マイクデバイスが検出されませんでした。
# no_speaker_device_detected_error: スピーカーデバイスが検出されませんでした。
# translation_engine_limit_error: 翻訳エンジンを自動的に変更しました。対象翻訳エンジンへのリクエストが多すぎるため、一時的にアクセスが制限されています。同じ翻訳エンジンを使用したい場合はしばらく待ってから、VRCTの再起動をしてもう一度試してみてください。
# detected_by_word_filter: ワードフィルターに登録されている単語 {{detected_message}} が検出されたため送信しませんでした。
# selected_your_language: 「あなたの言語」 を {{your_language}} に設定しました。
# selected_target_language: 「相手の言語」 を {{target_language}} に設定しました。
# switched_language_preset_tab: 言語プリセット番号 {{tab_no}} に切り替わりました。
# latest_language_setting: 現在「あなたの言語」は {{your_language}}、「相手の言語」は {{target_language}} に設定されています。
# opened_web_page_booth: お使いのブラウザで、Boothのページを開きました。
# opened_web_page_vrct_documents: |-
# お使いのブラウザで、VRCTのドキュメントを開きました。使用方法などはそちらに記載されています。
# 不具合、ご要望、その他お問い合わせはドキュメント最下部にあるLinks、「お問合せフォーム」もしくはX (元Twitter) にて気軽にご連絡ください!
state_text_enabled: 有効
state_text_disabled: 無効
language_selector:
title_your_language: あなたの言語
title_target_language: 相手の言語
update_available: 新しいバージョンが出ました!
updating: アップデート中...
update_modal:
cpu_desc: 処理デバイスとしてCPUのみを使用
cuda_desc: 処理デバイスとしてCPUとNVIDIA製のGPUを選択可能
cuda_compare_cpu_desc: GPU選択時、CPUと比べて処理が高速
cuda_disk_space_desc: 約{{size}}のディスク容量が必要
close_modal: 閉じる
download_latest_and_restart: |-
最新版がダウンロードされ、
アプリは自動的に再起動します。
is_latest_version_already: すでに最新版を使用中
is_current_compute_device: 現在使用中のバージョン
config_page:
version: バージョン {{version}}
# config_title: 設定
# compact_mode: コンパクトモード
# restart_message: 再起動して変更を適用する。
# common_error_message:
# invalid_value: 無効な値です。
model_download_button_label: ダウンロード
side_menu_labels:
device: デバイス
appearance: デザイン
translation: 翻訳
transcription: 音声認識
others: その他
advanced_settings: 高度な設定
device:
check_volume: 音量チェック
mic_host_device:
label: マイク (デバイス)
label_auto_select: 自動選択
label_host: ホスト/ドライバー
label_device: デバイス
mic_dynamic_energy_threshold:
label_for_automatic: 'マイク入力感度の調整 (現在の設定: 自動)'
desc_for_automatic: マイクの入力感度を自動的に調節する。
label_for_manual: 'マイク入力感度の調整 (現在の設定: 手動)'
desc_for_manual: スライダーを調整して入力感度を手動で決められます。マイクのアイコンを押すと、実際に声を入力し、音量を確認しながら調節できます。
error_message: 0 から {{max}} までの数値で設定できます。
speaker_device:
label: スピーカー (デバイス)
label_auto_select: 自動選択
speaker_dynamic_energy_threshold:
label_for_automatic: 'スピーカー入力感度の調整 (現在の設定: 自動)'
desc_for_automatic: スピーカーの入力感度を自動的に調節する。
label_for_manual: 'スピーカー入力感度の調整 (現在の設定: 手動)'
desc_for_manual: スライダーを調整して入力感度を手動で決められます。ヘッドフォンのアイコンを押すと、実際に音声を聞き取り、音量を確認しながら調節できます。
error_message: 0 から {{max}} までの数値で設定できます。
no_device_error_message: スピーカーデバイスが検出されませんでした。
appearance:
transparency:
label: 透明度
desc: メイン画面の透明度を変更できます。
ui_size:
label: UIサイズ
textbox_ui_size:
label: ログのフォントサイズ
desc: ログに表示されるフォントのサイズを、UIサイズを基準にして倍率を変えられます。
send_message_button_type:
label: メッセージ送信ボタン
hide: 非表示 (エンターキーを使って送信)
show: 表示
show_and_disable_enter_key: 表示し、エンターキーでの送信を無効
font_family:
label: 使用フォント
ui_language:
label: UIの言語
translation:
ctranslate2_weight_type:
label: オフライン翻訳のタイプ
desc: 翻訳エンジン(オフライン翻訳)で翻訳する際に、使用する翻訳モデルを選択できます。
small: 通常モデル ({{capacity}})
large: 高精度モデル ({{capacity}})
ctranslate2_compute_device:
label: オフライン翻訳の処理デバイス
deepl_auth_key:
label: DeepL 認証キー
desc: |-
使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。
※対応していない言語もあります。
open_auth_key_webpage: DeepLアカウントページを開く
save: 保存
edit: 編集
auth_key_success: 認証キーの更新が完了しました。
auth_key_error: 認証キーが間違っているか、API使用制限が上限に達しています。
transcription:
section_label_mic: マイク
section_label_speaker: スピーカー
section_label_transcription_engines: 音声認識エンジン
mic_record_timeout:
label: 入力が終了したとみなす無音時間
desc: 無音を検出し、設定された秒数経過すると、音声入力が終了したとみなします。
error_message: 0 以上で 「{{mic_phrase_timeout_label}}」より大きくすることはできません。
mic_phrase_timeout:
label: 一度に文字起こしする時間の長さ
desc: 設定された秒数ごとに文字起こし処理が行われます。
error_message: 0 以上で 「{{mic_record_timeout_label}}」より小さくすることはできません。
mic_max_phrase:
label: 送信するまでに保持する単語数
desc: 文字起こしされた単語数の下限値で、この数値を超えた場合のみ結果をVRChatへ送信し、ログに表示します。
error_message: 0以上の数値を設定できます。
mic_word_filter:
label: ワードフィルター
desc: |-
登録された単語を検出すると、その文章は送信されません。
「,」カンマで区切ると、まとめて複数の単語を追加できます。
※重複した単語は登録されません。
add_button_label: 追加
count_desc: '現在登録されている単語数: {{count}}'
speaker_record_timeout:
label: 入力が終了したとみなす無音時間
desc: 無音を検出し、設定された秒数経過すると、音声入力が終了したとみなします。
error_message: 0 以上で 「{{speaker_phrase_timeout_label}}」より大きくすることはできません。
speaker_phrase_timeout:
label: 一度に文字起こしする時間の長さ
desc: 設定された秒数ごとに文字起こし処理が行われます。
error_message: 0 以上で 「{{speaker_record_timeout_label}}」より小さくすることはできません。
speaker_max_phrase:
label: ログとして表示するまでに保持する単語数
desc: 文字起こしされた単語数の下限値で、この数値を超えた場合のみ結果をログに表示します。
error_message: 0以上の数値を設定できます。
select_transcription_engine:
label: 音声認識で使用するエンジン
whisper_weight_type:
label: Whisperモデルのタイプ
desc: |-
容量が大きいモデルほど精度は高いですが、その分CPUやGPUを占有します。
※特にmediumより容量の大きいモデルは、CPU/GPUの性能によっては使用すらも困難です。
model_template: '{{model_name}} モデル ({{capacity}})'
recommended_model_template: '{{model_name}} モデル ({{capacity}}) (推奨)'
whisper_compute_device:
label: Whisperで使用する処理デバイス
vr:
single_line: 一行
multi_lines: 複数行
overlay_enable: 有効にする
restore_default_settings: 初期値に戻す
position: 位置
rotation: 回転
x_position: X軸左右
y_position: Y軸上下
z_position: Z軸前後
x_rotation: X軸の回転
y_rotation: Y軸の回転
z_rotation: Z軸の回転
sample_text_button:
start: |-
サンプルテキストを
Overlayに送信する
stop: 送信を停止
sample_text: サンプルテキスト
opacity: 透明度
ui_scaling: サイズ
display_duration: 表示時間
fadeout_duration: フェードアウト時間
common_settings: 共通設定
hmd: HMD
left_hand: 左手
right_hand: 右手
tracker: 表示するトラッカーの位置
overlay_show_only_translated_messages:
label: 翻訳後のメッセージのみ表示する
others:
auto_clear_the_message_box:
label: 送信後はチャットボックスを空にする
send_only_translated_messages:
label: 翻訳後のメッセージのみ送信する
auto_export_message_logs:
label: 会話ログを自動的に保存する
desc: テキストファイルとしてログがlogsフォルダ内に保存されます。
vrc_mic_mute_sync:
label: VRCマイクミュート同期
desc: |-
VRChatのマイクがミュートされている間は、メッセージをVRChatに送信しません。
※若干の遅延はあります。また、Push-To-Talkは非対応です。
send_message_to_vrc:
label: VRChatにメッセージを送信する
desc: サポート対象外ですが、VRChatにメッセージを送信せずに使う方法があります。送信したい場合、この機能を有効にする事を忘れないでください。
send_received_message_to_vrc:
label: 受信したメッセージをVRChatに送信する
desc: スピーカーから聞き取り、文字起こしされたメッセージをVRChatに送信します。
advanced_settings:
osc_ip_address:
label: OSC IP Address
osc_port:
label: OSC Port
open_config_filepath:
label: 設定ファイルを開く
switch_compute_device:
label: VRCT CPU/GPUバージョンの切り替え

204
locales/ko.yml Normal file
View File

@@ -0,0 +1,204 @@
# =================================
# IMPORTANT:
# Please read 'readme_first.txt' before making any changes.
# =================================
common:
go_back_button_label: 돌아가기
main_page:
translation: 번역
transcription_send: 음성인식 (마이크)
transcription_receive: 음성인식 (스피커)
foreground: 항상 위로
language_settings: 언어 설정
your_language: 당신의 언어
translate_each_other_label: 양방향으로 번역
swap_button_label: 언어 교체
target_language: 상대방의 언어
translator: 번역 엔진
translator_ctranslate2: 오프라인 번역 (기본값)
message_log:
all: 전체
sent: 전송
received: 수신
system: 시스템
# textbox_system_message:
# enabled_translation: 번역 기능을 시작합니다.
# disabled_translation: 번역 기능이 중지되었습니다.
# enabled_voice2chatbox: 마이크에서의 음성인식을 시작합니다.
# disabled_voice2chatbox: 마이크에서의 음성인식이 중지되었습니다.
# enabled_speaker2log: 스피커에서의 음성인식을 시작합니다.
# disabled_speaker2log: 스피커에서의 음성인식이 중지되었습니다.
# enabled_foreground: 화면을 항상 위로 고정합니다.
# disabled_foreground: 화면 고정이 해제되었습니다.
# auth_key_success: 인증키 갱신이 완료되었습니다.
# auth_key_error: 인증 키가 잘못되었거나 API 사용 제한이 상한선에 도달했습니다.
# no_mic_device_detected_error: 마이크 디바이스를 찾지 못했습니다.
# no_speaker_device_detected_error: 스피커 디바이스를 찾지 못했습니다.
# translation_engine_limit_error: 번역 엔진을 자동으로 변경했습니다. 대상 번역 엔진에 대한 요청이 너무 많아 일시적으로 접근이 제한되었습니다. 해당 번역 엔진을 사용하려면 잠시 기다린 후 VRCT를 재시작하여 다시 시도해 보시기 바랍니다
# detected_by_word_filter: 단어 필터에 등록된 단어 {{detected_message}}(이)가 감지되어 전송하지 않았습니다.
# selected_your_language: "'당신의 언어'가 {{your_language}}(으)로 설정되었습니다."
# selected_target_language: "'상대방의 언어'가 {{target_language}}(으)로 설정되었습니다."
# switched_language_preset_tab: 언어 프리셋 번호 {{tab_no}}로 전환되었습니다.
# latest_language_setting: 현재 '당신의 언어'는 {{your_language}}, '상대방의 언어'는 {{target_language}}(으)로 설정되어 있습니다.
# opened_web_page_booth: 브라우저에서 Booth 페이지를 열었습니다.
# opened_web_page_vrct_documents: |-
# 웹 브라우저에서 VRCT 문서 페이지가 열렸습니다.
# 문제, 요청 또는 문의 사항이 있는 경우 문서 페이지 하단의 링크, '문의 양식' 또는 X(구 트위터)를 통해 언제든지 문의해 주세요!
state_text_enabled: Enabled
state_text_disabled: Disabled
language_selector:
title_your_language: 당신의 언어
title_target_language: 상대방의 언어
update_available: 새로운 버전이 나왔습니다!
updating: 업데이트 중...
update_modal:
update_software: |-
새 버전을 다운로드하고 재시작합니다.
조금 시간이 걸립니다. 지금 시작할까요?
deny_update_software: 나중에 하기
accept_update_software: 업데이트 및 재시작
config_page:
version: 버전 {{version}}
# config_title: 설정
# compact_mode: 컴팩트 모드
# restart_message: 재시작하여 변경 사항을 적용합니다.
# common_error_message:
# invalid_value: 유효하지 않은 값입니다.
side_menu_labels:
appearance: 모양
translation: 번역
transcription: 음성인식
others: 기타
advanced_settings: 고급 설정
device:
mic_host:
label: 마이크 호스트/드라이버
mic_device:
label: 마이크 장치
mic_dynamic_energy_threshold:
label_for_automatic: '음성 입력 최소 볼륨 (현재 설정: 자동)'
desc_for_automatic: 마이크의 입력 감도를 자동으로 조절합니다.
label_for_manual: '음성 입력 최소 볼륨 (현재 설정: 수동)'
desc_for_manual: 슬라이더를 움직여 입력 감도를 수동으로 조절합니다. 마이크 아이콘을 누르면 실제 음성의 볼륨을 확인하며 감도를 조절할 수 있습니다.
error_message: 0에서 {{max}}까지의 숫자로만 설정할 수 있습니다.
speaker_device:
label: 스피커 장치
speaker_dynamic_energy_threshold:
label_for_automatic: '음성 입력 최소 볼륨 (현재 설정: 자동)'
desc_for_automatic: 스피커의 입력 감도를 자동으로 조절합니다.
label_for_manual: '음성 입력 최소 볼륨 (현재 설정: 수동)'
desc_for_manual: 슬라이더를 움직여 입력 감도를 수동으로 조절합니다. 헤드폰 아이콘을 누르면 실제 음성의 볼륨을 확인하며 감도를 조절할 수 있습니다.
error_message: 0에서 {{max}}까지의 숫자로만 설정할 수 있습니다.
no_device_error_message: 스피커 디바이스를 찾지 못했습니다.
appearance:
transparency:
label: 투명도
desc: 메인 화면의 투명도를 변경합니다.
ui_size:
label: UI 크기
textbox_ui_size:
label: 텍스트 박스 글자 크기
desc: 로그에 표시되는 글자 크기의 배율을 UI 크기에 따라 변경합니다.
send_message_button_type:
label: 메시지 전송 버튼
hide: 숨김 (Enter 키를 사용하여 전송)
show: 표시
show_and_disable_enter_key: 표시 (Enter 키 전송 비활성화)
font_family:
label: 폰트
ui_language:
label: UI 언어
translation:
ctranslate2_weight_type:
label: 번역 모델
desc: 오프라인 번역 시의 번역 모델을 변경합니다.
small: 일반 모델 ({{capacity}})
large: 정밀 모델 ({{capacity}})
deepl_auth_key:
label: DeepL 인증키
desc: |-
사용시 메인화면에 있는 {{translator}}를 DeepL_API로 변경해 주세요.
지원하지 않는 언어도 있습니다.
open_auth_key_webpage: DeepL 계정 페이지 열기
auth_key_success: 인증키 갱신이 완료되었습니다.
auth_key_error: 인증키가 잘못되었거나 API 사용 제한이 상한에 도달했습니다.
transcription:
section_label_mic: 마이크
section_label_speaker: 스피커
mic_record_timeout:
label: 최대 무음 시간
desc: 무음을 감지하고 설정된 시간(초)만큼의 시간이 지나면 음성 입력이 종료된 것으로 판단합니다.
error_message: 0 이상에서 '{{mic_phrase_timeout_label}}'보다 클 수 없습니다.
mic_phrase_timeout:
label: 최대 인식 시간
desc: 설정된 초 단위로 음성인식 처리가 이루어집니다.
error_message: 0 이상에서 '{{mic_record_timeout_label}}'보다 작을 수 없습니다.
mic_max_phrase:
label: 최대 입력 절(phrases) 수
desc: 인식된 단어 수의 하한값으로, 이 수치를 초과하는 경우에만 결과를 VRChat으로 전송하고 로그에 표시합니다.
error_message: 0 이상의 숫자만 설정할 수 있습니다.
mic_word_filter:
label: 단어 필터
desc: |-
등록된 단어가 감지되면 해당 문장은 전송되지 않습니다.
',' 쉼표로 구분하면 여러 단어를 추가할 수 있습니다.
* 중복된 단어는 등록되지 않습니다.
add_button_label: 추가
count_desc: '현재 등록되어 있는 단어 수: {{count}}'
speaker_record_timeout:
label: 최대 무음 시간
desc: 무음을 감지하고 설정된 시간(초)만큼의 시간이 지나면 음성 입력이 종료된 것으로 판단합니다.
error_message: 0 이상에서 '{{speaker_phrase_timeout_label}}'보다 클 수 없습니다.
speaker_phrase_timeout:
label: 최대 인식 시간
desc: 설정된 초 단위로 음성인식 처리가 이루어집니다.
error_message: 0 이상에서 '{{speaker_record_timeout_label}}'보다 작을 수 없습니다.
speaker_max_phrase:
label: 최대 입력 절(phrases) 수
desc: 식된 단어 수의 하한값으로, 이 수치를 초과하는 경우에만 결과를 로그에 표시합니다.
error_message: 0 이상의 숫자만 설정할 수 있습니다.
use_whisper_feature:
label: 음성 인식에 Whisper 모델을 사용
desc: 일부 언어에서는 음성 인식의 정확도가 향상될 수 있어요. 음성 인식 중 CPU 사용률이 올라가기 때문에 사용하시는 PC의 사양을 고려하여 이 기능을 사용해주세요.
whisper_weight_type:
label: Whisper 모델 타입
# desc: "기본적으로 용량이 많은 모델일수록 정밀도는 높지만, 음성 인식의 시간이 늘어나며 CPU 사용률도 늘어나요.각 모델의 설명은 문서를 참조해주세요.\n※특히 medium보다 용량이 큰 모델은 CPU의 성능에 따라서는 사용조차 어려울 수 있어요. "
model_template: '{{model_name}} 모델 ({{capacity}})'
recommended_model_template: '{{model_name}} 모델 ({{capacity}}) (권장)'
others:
auto_clear_the_message_box:
label: 챗박스 자동 삭제
send_only_translated_messages:
label: 번역된 메시지만 전송
notice_xsoverlay:
label: XSOverlay에서 알림 수신 기능 활성화
desc: 수신된 메시지를 XSOverlay의 기능을 통해 알림으로 받아볼 수 있습니다.
auto_export_message_logs:
label: 대화 로그 자동 저장
desc: logs 폴더에 텍스트 파일로 로그가 저장됩니다.
send_message_to_vrc:
label: VRChat에 메시지 전송
desc: VRChat에 메시지를 보내지 않고 사용할 수 있는 방법이 있지만 지원되지 않습니다. VRChat에 메시지를 보내려면 이 기능을 활성화하세요.
advanced_settings:
osc_ip_address:
label: OSC IP 주소
osc_port:
label: OSC 포트
open_config_filepath:
label: 설정 파일 열기

1
locales/readme_first.txt Normal file
View File

@@ -0,0 +1 @@
Thank you for considering translating VRCT's UI. However, please refrain from making any changes at this time. I am currently organizing the files, including reordering, adding, and removing elements, and some parts may change frequently until the UI becomes stable. (Note: This message was updated in December 2024.)

222
locales/zh-Hans.yml Normal file
View File

@@ -0,0 +1,222 @@
# =================================
# IMPORTANT:
# Please read 'readme_first.txt' before making any changes.
# =================================
common:
go_back_button_label: 返回
main_page:
translation: 翻译
transcription_send: 你的语音转文字
transcription_receive: 他人语音转文字
foreground: 顶层显示
language_settings: 语言设定
your_language: 你的语言
translate_each_other_label: 双向翻译
swap_button_label: 互换
target_language: 目标语言
translator: 翻译器
translator_ctranslate2: 离线翻译(默认)
message_log:
all: 全部
sent: 发送
received: 接受
system: 系统
# textbox_system_message:
# enabled_translation: 翻译已启动.
# disabled_translation: 翻译已关闭.
# enabled_voice2chatbox: 正在翻译你的语音并转成文字.
# disabled_voice2chatbox: 你的语音翻译结束了.
# enabled_speaker2log: 正在翻译他人语音并转成文字.
# disabled_speaker2log: 第三者的语音翻译结束了.
# enabled_foreground: 顶层显示开启.
# disabled_foreground: 顶层显示关闭.
# auth_key_success: 授权密匙更新完毕
# auth_key_error: 授权密匙错误或已达到翻译API(翻译器决定)使用次数上限.
# no_mic_device_detected_error: 未检测到你的麦克风.
# no_speaker_device_detected_error: 未检测到他人语音输入.
# translation_engine_limit_error: 自动更换了翻译器.原因是对该翻译器请求太频繁,它暂时拒绝了接收翻译请求.如仍想使用原本翻译器,请稍等片刻后在重启VRCT.
# detected_by_word_filter: 该单词 {{detected_message}} 被单词过滤器检测出所以没有发送.
# selected_your_language: '[你的语言]设定为 {{your_language}} '
# selected_target_language: '[目标语言]设定为 {{target_language}} '
# switched_language_preset_tab: 已切换为第 {{tab_no}} 号语言设定
# latest_language_setting: 现在,你的语言是 {{your_language}},目标语言是 {{target_language}} .
# opened_web_page_booth: 在你的默认浏览器上打开了Booth页面
# opened_web_page_vrct_documents: |-
# 在你的默认浏览器上打开了VRCT文档,有着关于VRCT的使用方法
# 其他问题、请求、查询等请通过文档底部的链接或X (Twitter) 联系我们!
state_text_enabled: 启用
state_text_disabled: 停用
language_selector:
title_your_language: 你的语言
title_target_language: 目标语言
update_available: 有新版本可供使用!
updating: 更新中...
update_modal:
update_software_desc: |-
下载新版本并自动启动
会花少许时间,现在更新吗?
deny_update_software: 稍后再说
accept_update_software: 更新后自动启动
config_page:
version: 版本 {{version}}
# config_title: 设定
# compact_mode: 精简模式
# restart_message: 重启并应用设定
# common_error_message:
# invalid_value: 无效的值
side_menu_labels:
appearance: 外观
translation: 翻译
transcription: 转录
others: 其他
advanced_settings: 高级设置
device:
mic_host:
label: 麦克风(host/driver)
mic_device:
label: 麦克风 (设备)
mic_dynamic_energy_threshold:
label_for_automatic: 麦克风输入阈值(当前设置:自动)
desc_for_automatic: 自动调整麦克风输入阈值
label_for_manual: 麦克风输入阈值(当前设置:手动)
desc_for_manual: 使用滑杆手动确定麦克风输入灵敏度。按下麦克风图标输入语音,并在监控音量的同时调节灵敏度。
error_message: 数值应为 0 至 {{max}} 之间。
speaker_device:
label: 他人语音 (设备)
speaker_dynamic_energy_threshold:
label_for_automatic: 他人语音接收阈值(当前设置:自动)
desc_for_automatic: 自动调节他人语音接收阈值
label_for_manual: 他人语音接收阈值(当前设置:手动)
desc_for_manual: 使用滑杆手动调整他人语音接收阈值.在按下耳机按钮时,请根据实际听到的声音调整该大小
error_message: '设定的数值从 0 到 {{max}} '
no_device_error_message: 未检测到他人语音
appearance:
transparency:
label: 透明度
desc: 更改主视窗透明度
ui_size:
label: 界面大小
textbox_ui_size:
label: 文本框字体大小
desc: 你可以根据用户界面大小调整文本框中使用的字体大小。
send_message_button_type:
label: 发送信息按钮
hide: 隐藏 (可使用回车发送信息)
show: 显示
show_and_disable_enter_key: 显示,并且停用‘回车发送信息’
font_family:
label: 字体
ui_language:
label: 界面语言
translation:
ctranslate2_weight_type:
label: 选择离线翻译模型
desc: 可以选择用于离线翻译的翻译模型
small: 普通模型 ({{capacity}})
large: 高精度模型 ({{capacity}})
deepl_auth_key:
label: DeepL 授权密匙
desc: |-
在使用的时候,使用时请在主屏幕上通过 DeepL_API 选择 {{translator}}
※某些语言可能不支持
open_auth_key_webpage: 打开DeepL账号页面
auth_key_success: 授权密匙认证完成。
auth_key_error: 授权密匙错误或已达API使用上限\
transcription:
section_label_mic: 你的麦克风
section_label_speaker: 他人声音
mic_record_timeout:
label: 语音输入结束后的静音时间
desc: 当检测到静音并经过设定的秒数后,语音输入即被视为完成。
error_message: 数值应为 0 至 [{{mic_phrase_timeout_label}}]
mic_phrase_timeout:
label: 转录间隔
desc: 在经过设定的时间后执行转录
error_message: 转录间隔时间大于0秒且不能小于「{{mic_record_timeout_label}}」
mic_max_phrase:
label: 麦克风发送时的最小单词数
desc: 转录字数的下限,只有超过这个数字,才会记录翻译结果并发送到VRC
error_message: 数值应为 0 以上
mic_word_filter:
label: 单词过滤器
desc: |-
检测出被记录的单词时,不会发送这段话
如要添加多个单词,可以用逗号来分割
※不会记录重复的单词
add_button_label: 添加
count_desc: '现在被记录的单词数: {{count}}'
speaker_record_timeout:
label: 语音接收结束后的静音时间
desc: 当检测到静音并经过设定的秒数后,语音接收即被视为完成。
error_message: 数值应为 0 至 「{{speaker_phrase_timeout_label}}」
speaker_phrase_timeout:
label: 转录间隔
desc: 在经过设定的时间后执行转录
error_message: 转录间隔时间大于0秒且不能小于「{{speaker_record_timeout_label}}」
speaker_max_phrase:
label: 语音接收时的最小单词数
desc: 转录字数的下限,只有超过这个数字,才会记录转录结果
error_message: 数值应为 0 以上
use_whisper_feature:
label: 使用Whisper模型翻译
desc: 在某些语言中,语音识别的准确性可能会提高.语音识别的过程中,CPU占有率可能会提高,请根据你的pc性能来决定是否使用它.
whisper_weight_type:
label: 选择某个Whisper模型
# desc: |-
# 通常来说,容量越大的模型精度也会越高,但也会增加文字显示所需要的时间和CPU的使用率。请浏览各个模型的文档
# ※特别是大于medium容量的模型、因CPU性能原因甚至无法使用。
model_template: '{{model_name}} 模型 ({{capacity}})'
recommended_model_template: '{{model_name}} 模型 ({{capacity}}) (推荐)'
vr:
restore_default_settings: 恢复默认设置
opacity: 透明度
ui_scaling: 大小
x_position: X轴(左右)
y_position: Y轴(上下)
z_position: Z轴(前后)
x_rotation: X轴旋转
y_rotation: Y轴旋转
z_rotation: Z轴旋转
display_duration: 显示持续时间
fadeout_duration: 渐隐持续时间
others:
auto_clear_the_message_box:
label: 发言后自动清空chatbox
send_only_translated_messages:
label: 只发送翻译后的信息
auto_export_message_logs:
label: 自动导出聊天记录
desc: 以文本文件的形式在logs文件夹中保存。
vrc_mic_mute_sync:
label: 与VRC中的麦克风静音同步
desc: |-
当VRChat的麦克风处于静音时,不在VRChat中发送信息
※存在少许延迟且不支持按键发言.
send_message_to_vrc:
label: 发送信息至VRChat
desc: 不发送信息至VRChat的情况下也能使用它,但该功能现在并未完成.在想要发送信息时,请不要忘记打开这个功能.
advanced_settings:
osc_ip_address:
label: OSC IP 地址
osc_port:
label: OSC 端口
open_config_filepath:
label: 打开设置文件

223
locales/zh-Hant.yml Normal file
View File

@@ -0,0 +1,223 @@
# =================================
# IMPORTANT:
# Please read 'readme_first.txt' before making any changes.
# =================================
common:
go_back_button_label: 返回
main_page:
translation: 翻譯
transcription_send: 麥克風轉文字
transcription_receive: 喇叭轉文字
foreground: 最上層顯示
language_settings: 語言設定
your_language: 你的語言
translate_each_other_label: 互相翻譯
swap_button_label: 交換語言
target_language: 目標語言
translator: 翻譯器
translator_ctranslate2: 離線翻譯(預設)
message_log:
all: 全部
sent: 已發送
received: 已接收
system: 系統
# textbox_system_message:
# enabled_easter_egg: 你找到了彩蛋!看看你的 VR Overlay 有沒有什麼變化?
# enabled_translation: 翻譯功能已啟用。
# disabled_translation: 翻譯功能已停用。
# enabled_voice2chatbox: 麥克風轉文字已啟用。
# disabled_voice2chatbox: 麥克風轉文字已停用。
# enabled_speaker2log: 喇叭轉文字已啟用。
# disabled_speaker2log: 喇叭轉文字已停用。
# enabled_foreground: 最上層顯示已啟用。
# disabled_foreground: 最上層顯示已停用。
# auth_key_success: 授權金鑰更新完成。
# auth_key_error: 授權金鑰錯誤或已達使用上限。
# no_mic_device_detected_error: 未偵測到麥克風。
# no_speaker_device_detected_error: 未偵測到喇叭。
# translation_engine_limit_error: 翻譯引擎已自動變更。由於請求太頻繁,已被這個翻譯引擎暫時受限。如果你想使用相同的翻譯引擎,請稍等片刻,重新啟動 VRCT 並重試。
# detected_by_word_filter: 由於詞語過濾器的偵測,「{{detected_message}}」未被發送。
# selected_your_language: 「你的語言」已設為 {{your_language}}。
# selected_target_language: 「目標語言」已設為 {{target_language}}。
# switched_language_preset_tab: 已切換到第 {{tab_no}} 個語言設定。
# latest_language_setting: 目前「你的語言」設為 {{your_language}},「目標語言」設為 {{target_language}}。
# opened_web_page_booth: 已在瀏覽器中打開 Booth 頁面。
# opened_web_page_vrct_documents: |-
# 已在瀏覽器中打開VRCT文件頁面。
# 如有任何問題、請求或查詢,請通過文件頁面底部的連結、「聯絡表單」或 X (Twitter) 聯絡我們!
state_text_enabled: 啟用
state_text_disabled: 停用
language_selector:
title_your_language: 選擇你的語言
title_target_language: 選擇目標語言
update_available: 有新版本可供使用!
updating: 正在更新...
update_modal:
update_software_desc: |-
下載新版本並自動更新 VRCT。
會花一些時間,現在更新嗎?
deny_update_software: 稍後再說
accept_update_software: 更新
config_page:
version: 版本 {{version}}
# config_title: 設定
# compact_mode: 精簡模式
# restart_message: 重新啟動以應用變更。
# common_error_message:
# invalid_value: 無效值。
side_menu_labels:
appearance: 外觀
translation: 翻譯
transcription: 轉錄
vr: VR
others: 其他
advanced_settings: 進階設定
device:
mic_host:
label: 麥克風 Host/Driver
mic_device:
label: 麥克風裝置
mic_dynamic_energy_threshold:
label_for_automatic: 麥克風能量閾值(當前設置:自動)
desc_for_automatic: 自動判定麥克風輸入靈敏度。
label_for_manual: 麥克風能量閾值(當前設置:手動)
desc_for_manual: 使用滑桿調整麥克風輸入靈敏度,你可以按下麥克風圖示來測試。
error_message: 可以設置 0 到 {{max}} 之間的值。
speaker_device:
label: 喇叭裝置
speaker_dynamic_energy_threshold:
label_for_automatic: 喇叭能量閾值(當前設置:自動)
desc_for_automatic: 自動確定喇叭輸入靈敏度。
label_for_manual: 喇叭能量閾值(當前設置:手動)
desc_for_manual: 使用滑桿調整喇叭輸入靈敏度,你可以按下喇叭圖示來測試。
error_message: 可以設置 0 到 {{max}} 之間的值。
no_device_error_message: 未偵測到喇叭裝置。
appearance:
transparency:
label: 透明度
desc: 變更主視窗的透明度。
ui_size:
label: 介面大小
textbox_ui_size:
label: 訊息框字體大小
desc: 你可以根據介面大小調整記錄中使用的字體大小。
send_message_button_type:
label: 發送訊息按鈕
hide: 隱藏(使用 Enter 鍵發送)
show: 顯示
show_and_disable_enter_key: 顯示並停用 Enter 鍵發送
font_family:
label: 字型
ui_language:
label: 介面語言
translation:
ctranslate2_weight_type:
label: 選擇離線翻譯模型
desc: 你可以選擇用於離線翻譯引擎的翻譯模型。
small: 基本模型({{capacity}}
large: 高準確率模型({{capacity}}
deepl_auth_key:
label: DeepL 授權金鑰
desc: 使用 DeepL API 時請在主螢幕選擇 {{translator}}。※可能不支援某些語言。
open_auth_key_webpage: 打開 DeepL 帳號頁面
auth_key_success: 授權金鑰更新完成。
auth_key_error: 授權金鑰錯誤或已達使用上限。
transcription:
section_label_mic: 麥克風
section_label_speaker: 喇叭
mic_record_timeout:
label: 麥克風音訊 - 判定結束時間
desc: 麥克風未收到音訊後,結束一段話的判定時間(秒)。
error_message: 不能大於「{{mic_phrase_timeout_label}}」,應為 0 或更高。
mic_phrase_timeout:
label: 麥克風音訊 - 紀錄間隔時間
desc: 每隔多久要紀錄一次音訊。
error_message: 不能小於「{{mic_record_timeout_label}}」,應為 0 或更高。
mic_max_phrase:
label: 麥克風音訊 - 最大單詞數量
desc: 只有在單詞超過此數量時,才會記錄結果並發送到 VRChat。
error_message: 可以設置為 0 或更高的數值。
mic_word_filter:
label: 麥克風單詞過濾器
desc: |-
如果偵測到清單內的單詞,則不會發送訊息。要一次新增多個詞語,請用「,」(半形逗號)分隔。
*重複詞語會被忽略。
add_button_label: 新增
count_desc: 當前註冊詞語數量:{{count}}
speaker_record_timeout:
label: 喇叭音訊 - 判定結束時間
desc: 偵測到靜音並在指定秒數後認為喇叭輸入已結束。(秒)
error_message: 不能大於「{{speaker_phrase_timeout_label}}」,應為 0 或更高。
speaker_phrase_timeout:
label: 喇叭音訊 - 紀錄間隔時間
desc: 以指定秒數間隔進行轉錄處理。
error_message: 不能小於「{{speaker_record_timeout_label}}」,應為 0 或更高。
speaker_max_phrase:
label: 喇叭音訊 - 最大單詞數量
desc: 只有在單詞超過此數量時,才會記錄結果並發送到 VRChat。
error_message: 可以設置 0 或更高的數值。
use_whisper_feature:
label: 使用 Whisper 模型進行轉錄
desc: 在某些語言中語音識別的準確性可能會提高。使用語音識別時CPU使用率會增加請根據你的PC規格考慮是否使用此功能。
whisper_weight_type:
label: 選擇 Whisper 模型
# desc: |-
# 一般來說容量較大的模型往往具有更高的準確性但這也導致轉錄時間較長和CPU使用率增加。請參考文檔了解各模型的說明。
# ※特別是超過中等大小的模型根據CPU性能可能難以運行。
model_template: '{{model_name}}模型({{capacity}}'
recommended_model_template: '{{model_name}}模型({{capacity}})(推薦)'
vr:
restore_default_settings: 恢復預設設定
opacity: 透明度
ui_scaling: 介面縮放
x_position: X軸左右
y_position: Y軸上下
z_position: Z軸前後
x_rotation: X軸旋轉
y_rotation: Y軸旋轉
z_rotation: Z軸旋轉
display_duration: 顯示持續時間
fadeout_duration: 淡出持續時間
others:
auto_clear_the_message_box:
label: 自動清除 Chatbox
send_only_translated_messages:
label: 僅發送翻譯訊息
notice_xsoverlay:
label: XSOverlay 通知
desc: 從 XSOverlay 的通知功能接收訊息。
auto_export_message_logs:
label: 自動匯出訊息記錄
desc: 自動將對話訊息匯出為文字文件。
vrc_mic_mute_sync:
label: VRC 麥克風靜音同步
desc: |-
當 VRChat 的麥克風靜音時VRCT 將不會向 VRChat 發送訊息。
*存在一些延遲且不支援按鍵發話 (PTT)。
send_message_to_vrc:
label: 發送訊息到 VRChat
desc: 當你打算向 VRChat 發送訊息時啟用此功能。
advanced_settings:
osc_ip_address:
label: OSC IP 位址
osc_port:
label: OSC 端口
open_config_filepath:
label: 打開設定文件

5576
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
package.json Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "tauri-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"setup-python": "install.bat",
"build-python": "build.bat",
"build-python-cuda": "build_cuda.bat",
"vite": "vite",
"vite-build": "vite build",
"vite-preview": "vite preview",
"tauri": "tauri",
"tauri-dev": "tauri dev",
"dev": "npm run build-python && npm run dev-ui",
"dev-cuda": "npm run build-python-cuda && npm run dev-ui",
"dev-ui": "npm-run-all --parallel vite tauri-dev",
"build": "npm run build-python && npm run vite-build && npm run tauri build",
"build-cuda": "npm run build-python-cuda && npm run vite-build && npm run tauri build",
"build-ui": "npm run vite-build && npm run tauri build",
"release": "python zip.py --zip_name VRCT.zip",
"release-cuda": "python zip.py --zip_name VRCT_cuda.zip",
"release-all": "npm run build && npm run release && npm run build-cuda && npm run release-cuda"
},
"dependencies": {
"@emotion/react": "11.14.0",
"@emotion/styled": "11.14.0",
"@mui/material": "6.2.0",
"@tauri-apps/api": "1.6.0",
"@vitejs/plugin-react": "4.3.4",
"clsx": "2.1.1",
"eslint": "8.57.0",
"eslint-plugin-react": "7.37.2",
"i18next": "24.1.0",
"jotai": "2.10.3",
"js-base64": "3.7.7",
"js-yaml": "4.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "15.2.0",
"react-resizable-layout": "0.7.2"
},
"devDependencies": {
"@tauri-apps/cli": "1.6.3",
"npm-run-all": "4.1.5",
"sass": "1.79.4",
"vite": "6.0.3",
"vite-plugin-svgr": "4.3.0"
}
}

20
requirements.txt Normal file
View File

@@ -0,0 +1,20 @@
torch==2.2.2
faster-whisper==1.0.3
ctranslate2==4.3.1
transformers==4.40.2
pillow == 10.0.0
PyAudioWPatch == 0.2.12.6
python-osc == 1.9.0
deepl == 1.15.0
flashtext ==2.7
pyinstaller==6.10.0
numpy==1.26.4
sentencepiece==0.2.0
openvr==1.26.701
pydub==0.25.1
psutil==5.9.8
pykakasi==2.3.0
pycaw==20240210
translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3

21
requirements_cuda.txt Normal file
View File

@@ -0,0 +1,21 @@
torch==2.2.2
--extra-index-url https://download.pytorch.org/whl/cu121
faster-whisper==1.0.3
ctranslate2==4.3.1
transformers==4.40.2
pillow == 10.0.0
PyAudioWPatch == 0.2.12.6
python-osc == 1.9.0
deepl == 1.15.0
flashtext ==2.7
pyinstaller==6.10.0
numpy==1.26.4
sentencepiece==0.2.0
openvr==1.26.701
pydub==0.25.1
psutil==5.9.8
pykakasi==2.3.0
pycaw==20240210
translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3

1105
src-python/config.py Normal file

File diff suppressed because it is too large Load Diff

1757
src-python/controller.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,327 @@
from typing import Callable
from time import sleep
from threading import Thread
import comtypes
from pyaudiowpatch import PyAudio, paWASAPI
from pycaw.callbacks import MMNotificationClient
from pycaw.utils import AudioUtilities
from utils import errorLogging
class Client(MMNotificationClient):
def __init__(self):
super().__init__()
self.loop = True
def on_default_device_changed(self, flow, flow_id, role, role_id, default_device_id):
self.loop = False
def on_device_added(self, added_device_id):
self.loop = False
def on_device_removed(self, removed_device_id):
self.loop = False
def on_device_state_changed(self, device_id, state):
self.loop = False
# def on_property_value_changed(self, device_id, key):
# self.loop = False
class DeviceManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(DeviceManager, cls).__new__(cls)
cls._instance.init()
return cls._instance
def init(self):
self.mic_devices = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
self.default_mic_device = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
self.speaker_devices = [{"index": -1, "name": "NoDevice"}]
self.default_speaker_device = {"device": {"index": -1, "name": "NoDevice"}}
self.update()
self.prev_mic_host = [host for host in self.mic_devices]
self.prev_mic_devices = self.mic_devices
self.prev_default_mic_device = self.default_mic_device
self.prev_speaker_devices = self.speaker_devices
self.prev_default_speaker_device = self.default_speaker_device
self.update_flag_default_mic_device = False
self.update_flag_default_speaker_device = False
self.update_flag_host_list = False
self.update_flag_mic_device_list = False
self.update_flag_speaker_device_list = False
self.callback_default_mic_device = None
self.callback_default_speaker_device = None
self.callback_host_list = None
self.callback_mic_device_list = None
self.callback_speaker_device_list = None
self.callback_process_before_update_devices = None
self.callback_process_after_update_devices = None
self.monitoring_flag = False
self.startMonitoring()
def update(self):
buffer_mic_devices = {}
buffer_default_mic_device = {"host": {"index": -1, "name": "NoHost"}, "device": {"index": -1, "name": "NoDevice"}}
buffer_speaker_devices = []
buffer_default_speaker_device = {"device": {"index": -1, "name": "NoDevice"}}
with PyAudio() as p:
for host_index in range(p.get_host_api_count()):
host = p.get_host_api_info_by_index(host_index)
device_count = host.get('deviceCount', 0)
for device_index in range(device_count):
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
if device.get("maxInputChannels", 0) > 0 and not device.get("isLoopbackDevice", True):
buffer_mic_devices.setdefault(host["name"], []).append(device)
if not buffer_mic_devices:
buffer_mic_devices = {"NoHost": [{"index": -1, "name": "NoDevice"}]}
api_info = p.get_default_host_api_info()
default_mic_device = api_info["defaultInputDevice"]
for host_index in range(p.get_host_api_count()):
host = p.get_host_api_info_by_index(host_index)
device_count = host.get('deviceCount', 0)
for device_index in range(device_count):
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
if device["index"] == default_mic_device:
buffer_default_mic_device = {"host": host, "device": device}
break
else:
continue
break
speaker_devices = []
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
wasapi_name = wasapi_info["name"]
for host_index in range(p.get_host_api_count()):
host = p.get_host_api_info_by_index(host_index)
if host["name"] == wasapi_name:
device_count = host.get('deviceCount', 0)
for device_index in range(device_count):
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
if not device.get("isLoopbackDevice", True):
for loopback in p.get_loopback_device_info_generator():
if device["name"] in loopback["name"]:
speaker_devices.append(loopback)
speaker_devices = [dict(t) for t in {tuple(d.items()) for d in speaker_devices}] or [{"index": -1, "name": "NoDevice"}]
buffer_speaker_devices = sorted(speaker_devices, key=lambda d: d['index'])
wasapi_info = p.get_host_api_info_by_type(paWASAPI)
default_speaker_device_index = wasapi_info["defaultOutputDevice"]
for host_index in range(p.get_host_api_count()):
host_info = p.get_host_api_info_by_index(host_index)
device_count = host_info.get('deviceCount', 0)
for device_index in range(0, device_count):
device = p.get_device_info_by_host_api_device_index(host_index, device_index)
if device["index"] == default_speaker_device_index:
default_speakers = device
if not default_speakers.get("isLoopbackDevice", True):
for loopback in p.get_loopback_device_info_generator():
if default_speakers["name"] in loopback["name"]:
buffer_default_speaker_device = {"device": loopback}
break
break
if buffer_default_speaker_device["device"]["name"] != "NoDevice":
break
self.mic_devices = buffer_mic_devices
self.default_mic_device = buffer_default_mic_device
self.speaker_devices = buffer_speaker_devices
self.default_speaker_device = buffer_default_speaker_device
def checkUpdate(self):
if self.prev_default_mic_device["device"]["name"] != self.default_mic_device["device"]["name"]:
self.update_flag_default_mic_device = True
self.prev_default_mic_device = self.default_mic_device
if self.prev_default_speaker_device["device"]["name"] != self.default_speaker_device["device"]["name"]:
self.update_flag_default_speaker_device = True
self.prev_default_speaker_device = self.default_speaker_device
if self.prev_mic_host != [host for host in self.mic_devices]:
self.update_flag_host_list = True
self.prev_mic_host = [host for host in self.mic_devices]
if {key: [device['name'] for device in devices] for key, devices in self.prev_mic_devices.items()} != {key: [device['name'] for device in devices] for key, devices in self.mic_devices.items()}:
self.update_flag_mic_device_list = True
self.prev_mic_devices = self.mic_devices
if [device['name'] for device in self.prev_speaker_devices] != [device['name'] for device in self.speaker_devices]:
self.update_flag_speaker_device_list = True
self.prev_speaker_devices = self.speaker_devices
update_flag = (
self.update_flag_default_mic_device or
self.update_flag_default_speaker_device or
self.update_flag_host_list or
self.update_flag_mic_device_list or
self.update_flag_speaker_device_list
)
return update_flag
def monitoring(self):
try:
while self.monitoring_flag is True:
try:
comtypes.CoInitialize()
cb = Client()
enumerator = AudioUtilities.GetDeviceEnumerator()
enumerator.RegisterEndpointNotificationCallback(cb)
while cb.loop is True:
sleep(1)
enumerator.UnregisterEndpointNotificationCallback(cb)
comtypes.CoUninitialize()
self.runProcessBeforeUpdateDevices()
sleep(2)
for _ in range(10):
self.update()
if self.checkUpdate():
break
sleep(2)
self.noticeUpdateDevices()
self.runProcessAfterUpdateDevices()
except Exception:
errorLogging()
finally:
pass
except Exception:
errorLogging()
def startMonitoring(self):
self.monitoring_flag = True
self.th_monitoring = Thread(target=self.monitoring)
self.th_monitoring.daemon = True
self.th_monitoring.start()
def stopMonitoring(self):
self.monitoring_flag = False
self.th_monitoring.join()
def setCallbackDefaultMicDevice(self, callback):
self.callback_default_mic_device = callback
def clearCallbackDefaultMicDevice(self):
self.callback_default_mic_device = None
def setCallbackDefaultSpeakerDevice(self, callback):
self.callback_default_speaker_device = callback
def clearCallbackDefaultSpeakerDevice(self):
self.callback_default_speaker_device = None
def setCallbackHostList(self, callback):
self.callback_host_list = callback
def clearCallbackHostList(self):
self.callback_host_list = None
def setCallbackMicDeviceList(self, callback):
self.callback_mic_device_list = callback
def clearCallbackMicDeviceList(self):
self.callback_mic_device_list = None
def setCallbackSpeakerDeviceList(self, callback):
self.callback_speaker_device_list = callback
def clearCallbackSpeakerDeviceList(self):
self.callback_speaker_device_list = None
def setCallbackProcessBeforeUpdateDevices(self, callback):
self.callback_process_before_update_devices = callback
def clearCallbackProcessBeforeUpdateDevices(self):
self.callback_process_before_update_devices = None
def runProcessBeforeUpdateDevices(self):
if isinstance(self.callback_process_before_update_devices, Callable):
self.callback_process_before_update_devices()
def setCallbackProcessAfterUpdateDevices(self, callback):
self.callback_process_after_update_devices = callback
def clearCallbackProcessAfterUpdateDevices(self):
self.callback_process_after_update_devices = None
def runProcessAfterUpdateDevices(self):
if isinstance(self.callback_process_after_update_devices, Callable):
self.callback_process_after_update_devices()
def noticeUpdateDevices(self):
if self.update_flag_default_mic_device is True:
self.setMicDefaultDevice()
if self.update_flag_default_speaker_device is True:
self.setSpeakerDefaultDevice()
if self.update_flag_host_list is True:
self.setMicHostList()
if self.update_flag_mic_device_list is True:
self.setMicDeviceList()
if self.update_flag_speaker_device_list is True:
self.setSpeakerDeviceList()
self.update_flag_default_mic_device = False
self.update_flag_default_speaker_device = False
self.update_flag_host_list = False
self.update_flag_mic_device_list = False
self.update_flag_speaker_device_list = False
def setMicDefaultDevice(self):
if isinstance(self.callback_default_mic_device, Callable):
self.callback_default_mic_device(self.default_mic_device["host"]["name"], self.default_mic_device["device"]["name"])
def setSpeakerDefaultDevice(self):
if isinstance(self.callback_default_speaker_device, Callable):
self.callback_default_speaker_device(self.default_speaker_device["device"]["name"])
def setMicHostList(self):
if isinstance(self.callback_host_list, Callable):
self.callback_host_list()
def setMicDeviceList(self):
if isinstance(self.callback_mic_device_list, Callable):
self.callback_mic_device_list()
def setSpeakerDeviceList(self):
if isinstance(self.callback_speaker_device_list, Callable):
self.callback_speaker_device_list()
def getMicDevices(self):
return self.mic_devices
def getDefaultMicDevice(self):
return self.default_mic_device
def getSpeakerDevices(self):
return self.speaker_devices
def getDefaultSpeakerDevice(self):
return self.default_speaker_device
def forceUpdateAndSetMicDevices(self):
self.update()
self.setMicHostList()
self.setMicDeviceList()
self.setMicDefaultDevice()
def forceUpdateAndSetSpeakerDevices(self):
self.update()
self.setSpeakerDeviceList()
self.setSpeakerDefaultDevice()
device_manager = DeviceManager()
if __name__ == "__main__":
# print("getMicDevices()", device_manager.getMicDevices())
# print("getDefaultMicDevice()", device_manager.getDefaultMicDevice())
# print("getSpeakerDevices()", device_manager.getSpeakerDevices())
# print("getDefaultSpeakerDevice()", device_manager.getDefaultSpeakerDevice())
while True:
sleep(1)

576
src-python/mainloop.py Normal file
View File

@@ -0,0 +1,576 @@
import sys
import json
import time
from typing import Any
from threading import Thread
from queue import Queue
from controller import Controller
from utils import printLog, printResponse, errorLogging, encodeBase64
controller = Controller()
run_mapping = {
"transcription_mic":"/run/transcription_send_mic_message",
"transcription_speaker":"/run/transcription_receive_speaker_message",
"check_mic_volume":"/run/check_mic_volume",
"check_speaker_volume":"/run/check_speaker_volume",
"error_device":"/run/error_device",
"error_translation_engine":"/run/error_translation_engine",
"word_filter":"/run/word_filter",
"download_progress_ctranslate2_weight":"/run/download_progress_ctranslate2_weight",
"downloaded_ctranslate2_weight":"/run/downloaded_ctranslate2_weight",
"download_progress_whisper_weight":"/run/download_progress_whisper_weight",
"downloaded_whisper_weight":"/run/downloaded_whisper_weight",
"selected_mic_device":"/run/selected_mic_device",
"selected_speaker_device":"/run/selected_speaker_device",
"selected_translation_engines":"/run/selected_translation_engines",
"translation_engines":"/run/translation_engines",
"mic_host_list":"/run/mic_host_list",
"mic_device_list":"/run/mic_device_list",
"speaker_device_list":"/run/speaker_device_list",
"update_software_flag":"/run/update_software_flag",
"initialization_progress":"/run/initialization_progress",
"initialization_complete":"/run/initialization_complete",
}
controller.setRunMapping(run_mapping)
def run(status:int, endpoint:str, result:Any) -> None:
printResponse(status, endpoint, result)
controller.setRun(run)
mapping = {
# Main Window
"/set/enable/translation": {"status": False, "variable":controller.setEnableTranslation},
"/set/disable/translation": {"status": False, "variable":controller.setDisableTranslation},
"/set/enable/transcription_send": {"status": False, "variable":controller.setEnableTranscriptionSend},
"/set/disable/transcription_send": {"status": False, "variable":controller.setDisableTranscriptionSend},
"/set/enable/transcription_receive": {"status": False, "variable":controller.setEnableTranscriptionReceive},
"/set/disable/transcription_receive": {"status": False, "variable":controller.setDisableTranscriptionReceive},
"/set/enable/foreground": {"status": True, "variable":controller.setEnableForeground},
"/set/disable/foreground": {"status": True, "variable":controller.setDisableForeground},
"/get/data/selected_tab_no": {"status": True, "variable":controller.getSelectedTabNo},
"/set/data/selected_tab_no": {"status": True, "variable":controller.setSelectedTabNo},
"/get/data/main_window_sidebar_compact_mode": {"status": True, "variable":controller.getMainWindowSidebarCompactMode},
"/set/enable/main_window_sidebar_compact_mode": {"status": True, "variable":controller.setEnableMainWindowSidebarCompactMode},
"/set/disable/main_window_sidebar_compact_mode": {"status": True, "variable":controller.setDisableMainWindowSidebarCompactMode},
"/get/data/translation_engines": {"status": True, "variable":controller.getTranslationEngines},
"/get/data/selectable_language_list": {"status": True, "variable":controller.getListLanguageAndCountry},
"/get/data/selected_translation_engines": {"status": False, "variable":controller.getSelectedTranslationEngines},
"/set/data/selected_translation_engines": {"status": True, "variable":controller.setSelectedTranslationEngines},
"/get/data/selected_your_languages": {"status": True, "variable":controller.getSelectedYourLanguages},
"/set/data/selected_your_languages": {"status": True, "variable":controller.setSelectedYourLanguages},
"/get/data/selected_target_languages": {"status": True, "variable":controller.getSelectedTargetLanguages},
"/set/data/selected_target_languages": {"status": True, "variable":controller.setSelectedTargetLanguages},
"/get/data/selected_transcription_engine": {"status": False, "variable":controller.getSelectedTranscriptionEngine},
"/set/data/selected_transcription_engine": {"status": False, "variable":controller.setSelectedTranscriptionEngine},
"/run/send_message_box": {"status": False, "variable":controller.sendMessageBox},
"/run/typing_message_box": {"status": False, "variable":controller.typingMessageBox},
"/run/stop_typing_message_box": {"status": False, "variable":controller.stopTypingMessageBox},
"/run/send_text_overlay": {"status": True, "variable":controller.sendTextOverlay},
"/run/swap_your_language_and_target_language": {"status": True, "variable":controller.swapYourLanguageAndTargetLanguage},
"/run/update_software": {"status": True, "variable":controller.updateSoftware},
"/run/update_cuda_software": {"status": True, "variable":controller.updateCudaSoftware},
# Config Window
# Appearance
"/get/data/version": {"status": True, "variable":controller.getVersion},
"/get/data/transparency": {"status": True, "variable":controller.getTransparency},
"/set/data/transparency": {"status": True, "variable":controller.setTransparency},
"/get/data/ui_scaling": {"status": True, "variable":controller.getUiScaling},
"/set/data/ui_scaling": {"status": True, "variable":controller.setUiScaling},
"/get/data/textbox_ui_scaling": {"status": True, "variable":controller.getTextboxUiScaling},
"/set/data/textbox_ui_scaling": {"status": True, "variable":controller.setTextboxUiScaling},
"/get/data/message_box_ratio": {"status": True, "variable":controller.getMessageBoxRatio},
"/set/data/message_box_ratio": {"status": True, "variable":controller.setMessageBoxRatio},
"/get/data/font_family": {"status": True, "variable":controller.getFontFamily},
"/set/data/font_family": {"status": True, "variable":controller.setFontFamily},
"/get/data/ui_language": {"status": True, "variable":controller.getUiLanguage},
"/set/data/ui_language": {"status": True, "variable":controller.setUiLanguage},
"/get/data/main_window_geometry": {"status": True, "variable":controller.getMainWindowGeometry},
"/set/data/main_window_geometry": {"status": True, "variable":controller.setMainWindowGeometry},
# Compute device
"/get/data/compute_mode": {"status": True, "variable":controller.getComputeMode},
"/get/data/translation_compute_device_list": {"status": True, "variable":controller.getComputeDeviceList},
"/get/data/selected_translation_compute_device": {"status": True, "variable":controller.getSelectedTranslationComputeDevice},
"/set/data/selected_translation_compute_device": {"status": True, "variable":controller.setSelectedTranslationComputeDevice},
"/get/data/transcription_compute_device_list": {"status": True, "variable":controller.getComputeDeviceList},
"/get/data/selected_transcription_compute_device": {"status": True, "variable":controller.getSelectedTranscriptionComputeDevice},
"/set/data/selected_transcription_compute_device": {"status": True, "variable":controller.setSelectedTranscriptionComputeDevice},
# Translation
"/get/data/selectable_ctranslate2_weight_type_dict": {"status": True, "variable":controller.getSelectableCtranslate2WeightTypeDict},
"/get/data/ctranslate2_weight_type": {"status": True, "variable":controller.getCtranslate2WeightType},
"/set/data/ctranslate2_weight_type": {"status": True, "variable":controller.setCtranslate2WeightType},
"/run/download_ctranslate2_weight": {"status": True, "variable":controller.downloadCtranslate2Weight},
"/get/data/deepl_auth_key": {"status": False, "variable":controller.getDeepLAuthKey},
"/set/data/deepl_auth_key": {"status": False, "variable":controller.setDeeplAuthKey},
"/delete/data/deepl_auth_key": {"status": False, "variable":controller.delDeeplAuthKey},
"/get/data/convert_message_to_romaji": {"status": True, "variable":controller.getConvertMessageToRomaji},
"/set/enable/convert_message_to_romaji": {"status": True, "variable":controller.setEnableConvertMessageToRomaji},
"/set/disable/convert_message_to_romaji": {"status": True, "variable":controller.setDisableConvertMessageToRomaji},
"/get/data/convert_message_to_hiragana": {"status": True, "variable":controller.getConvertMessageToHiragana},
"/set/enable/convert_message_to_hiragana": {"status": True, "variable":controller.setEnableConvertMessageToHiragana},
"/set/disable/convert_message_to_hiragana": {"status": True, "variable":controller.setDisableConvertMessageToHiragana},
# Transcription
"/get/data/mic_host_list": {"status": True, "variable":controller.getMicHostList},
"/get/data/mic_device_list": {"status": True, "variable":controller.getMicDeviceList},
"/get/data/speaker_device_list": {"status": True, "variable":controller.getSpeakerDeviceList},
# "/get/data/max_mic_threshold": {"status": True, "variable":controller.getMaxMicThreshold},
# "/get/data/max_speaker_threshold": {"status": True, "variable":controller.getMaxSpeakerThreshold},
"/get/data/auto_mic_select": {"status": True, "variable":controller.getAutoMicSelect},
"/set/enable/auto_mic_select": {"status": True, "variable":controller.setEnableAutoMicSelect},
"/set/disable/auto_mic_select": {"status": True, "variable":controller.setDisableAutoMicSelect},
"/get/data/selected_mic_host": {"status": True, "variable":controller.getSelectedMicHost},
"/set/data/selected_mic_host": {"status": True, "variable":controller.setSelectedMicHost},
"/get/data/selected_mic_device": {"status": True, "variable":controller.getSelectedMicDevice},
"/set/data/selected_mic_device": {"status": True, "variable":controller.setSelectedMicDevice},
"/get/data/mic_threshold": {"status": True, "variable":controller.getMicThreshold},
"/set/data/mic_threshold": {"status": True, "variable":controller.setMicThreshold},
"/get/data/mic_automatic_threshold": {"status": True, "variable":controller.getMicAutomaticThreshold},
"/set/enable/mic_automatic_threshold": {"status": True, "variable":controller.setEnableMicAutomaticThreshold},
"/set/disable/mic_automatic_threshold": {"status": True, "variable":controller.setDisableMicAutomaticThreshold},
"/get/data/mic_record_timeout": {"status": True, "variable":controller.getMicRecordTimeout},
"/set/data/mic_record_timeout": {"status": True, "variable":controller.setMicRecordTimeout},
"/get/data/mic_phrase_timeout": {"status": True, "variable":controller.getMicPhraseTimeout},
"/set/data/mic_phrase_timeout": {"status": True, "variable":controller.setMicPhraseTimeout},
"/get/data/mic_max_phrases": {"status": True, "variable":controller.getMicMaxPhrases},
"/set/data/mic_max_phrases": {"status": True, "variable":controller.setMicMaxPhrases},
"/get/data/mic_avg_logprob": {"status": True, "variable":controller.getMicAvgLogprob},
"/set/data/mic_avg_logprob": {"status": True, "variable":controller.setMicAvgLogprob},
"/get/data/mic_no_speech_prob": {"status": True, "variable":controller.getMicNoSpeechProb},
"/set/data/mic_no_speech_prob": {"status": True, "variable":controller.setMicNoSpeechProb},
"/set/enable/check_mic_threshold": {"status": True, "variable":controller.setEnableCheckMicThreshold},
"/set/disable/check_mic_threshold": {"status": True, "variable":controller.setDisableCheckMicThreshold},
"/get/data/mic_word_filter": {"status": True, "variable":controller.getMicWordFilter},
"/set/data/mic_word_filter": {"status": True, "variable":controller.setMicWordFilter},
"/get/data/auto_speaker_select": {"status": True, "variable":controller.getAutoSpeakerSelect},
"/set/enable/auto_speaker_select": {"status": True, "variable":controller.setEnableAutoSpeakerSelect},
"/set/disable/auto_speaker_select": {"status": True, "variable":controller.setDisableAutoSpeakerSelect},
"/get/data/selected_speaker_device": {"status": True, "variable":controller.getSelectedSpeakerDevice},
"/set/data/selected_speaker_device": {"status": True, "variable":controller.setSelectedSpeakerDevice},
"/get/data/speaker_threshold": {"status": True, "variable":controller.getSpeakerThreshold},
"/set/data/speaker_threshold": {"status": True, "variable":controller.setSpeakerThreshold},
"/get/data/speaker_automatic_threshold": {"status": True, "variable":controller.getSpeakerAutomaticThreshold},
"/set/enable/speaker_automatic_threshold": {"status": True, "variable":controller.setEnableSpeakerAutomaticThreshold},
"/set/disable/speaker_automatic_threshold": {"status": True, "variable":controller.setDisableSpeakerAutomaticThreshold},
"/get/data/speaker_record_timeout": {"status": True, "variable":controller.getSpeakerRecordTimeout},
"/set/data/speaker_record_timeout": {"status": True, "variable":controller.setSpeakerRecordTimeout},
"/get/data/speaker_phrase_timeout": {"status": True, "variable":controller.getSpeakerPhraseTimeout},
"/set/data/speaker_phrase_timeout": {"status": True, "variable":controller.setSpeakerPhraseTimeout},
"/get/data/speaker_max_phrases": {"status": True, "variable":controller.getSpeakerMaxPhrases},
"/set/data/speaker_max_phrases": {"status": True, "variable":controller.setSpeakerMaxPhrases},
"/get/data/speaker_avg_logprob": {"status": True, "variable":controller.getSpeakerAvgLogprob},
"/set/data/speaker_avg_logprob": {"status": True, "variable":controller.setSpeakerAvgLogprob},
"/get/data/speaker_no_speech_prob": {"status": True, "variable":controller.getSpeakerNoSpeechProb},
"/set/data/speaker_no_speech_prob": {"status": True, "variable":controller.setSpeakerNoSpeechProb},
"/set/enable/check_speaker_threshold": {"status": True, "variable":controller.setEnableCheckSpeakerThreshold},
"/set/disable/check_speaker_threshold": {"status": True, "variable":controller.setDisableCheckSpeakerThreshold},
"/get/data/selectable_whisper_weight_type_dict": {"status": True, "variable":controller.getSelectableWhisperWeightTypeDict},
"/get/data/whisper_weight_type": {"status": True, "variable":controller.getWhisperWeightType},
"/set/data/whisper_weight_type": {"status": True, "variable":controller.setWhisperWeightType},
"/run/download_whisper_weight": {"status": True, "variable":controller.downloadWhisperWeight},
# VR
"/get/data/overlay_small_log": {"status": True, "variable":controller.getOverlaySmallLog},
"/set/enable/overlay_small_log": {"status": True, "variable":controller.setEnableOverlaySmallLog},
"/set/disable/overlay_small_log": {"status": True, "variable":controller.setDisableOverlaySmallLog},
"/get/data/overlay_small_log_settings": {"status": True, "variable":controller.getOverlaySmallLogSettings},
"/set/data/overlay_small_log_settings": {"status": True, "variable":controller.setOverlaySmallLogSettings},
"/get/data/overlay_large_log": {"status": True, "variable":controller.getOverlayLargeLog},
"/set/enable/overlay_large_log": {"status": True, "variable":controller.setEnableOverlayLargeLog},
"/set/disable/overlay_large_log": {"status": True, "variable":controller.setDisableOverlayLargeLog},
"/get/data/overlay_large_log_settings": {"status": True, "variable":controller.getOverlayLargeLogSettings},
"/set/data/overlay_large_log_settings": {"status": True, "variable":controller.setOverlayLargeLogSettings},
"/get/data/overlay_show_only_translated_messages": {"status": True, "variable":controller.getOverlayShowOnlyTranslatedMessages},
"/set/enable/overlay_show_only_translated_messages": {"status": True, "variable":controller.setEnableOverlayShowOnlyTranslatedMessages},
"/set/disable/overlay_show_only_translated_messages": {"status": True, "variable":controller.setDisableOverlayShowOnlyTranslatedMessages},
# Others
"/get/data/auto_clear_message_box": {"status": True, "variable":controller.getAutoClearMessageBox},
"/set/enable/auto_clear_message_box": {"status": True, "variable":controller.setEnableAutoClearMessageBox},
"/set/disable/auto_clear_message_box": {"status": True, "variable":controller.setDisableAutoClearMessageBox},
"/get/data/send_only_translated_messages": {"status": True, "variable":controller.getSendOnlyTranslatedMessages},
"/set/enable/send_only_translated_messages": {"status": True, "variable":controller.setEnableSendOnlyTranslatedMessages},
"/set/disable/send_only_translated_messages": {"status": True, "variable":controller.setDisableSendOnlyTranslatedMessages},
"/get/data/send_message_button_type": {"status": True, "variable":controller.getSendMessageButtonType},
"/set/data/send_message_button_type": {"status": True, "variable":controller.setSendMessageButtonType},
"/get/data/logger_feature": {"status": True, "variable":controller.getLoggerFeature},
"/set/enable/logger_feature": {"status": True, "variable":controller.setEnableLoggerFeature},
"/set/disable/logger_feature": {"status": True, "variable":controller.setDisableLoggerFeature},
"/run/open_filepath_logs": {"status": True, "variable":controller.openFilepathLogs},
"/get/data/vrc_mic_mute_sync": {"status": True, "variable":controller.getVrcMicMuteSync},
"/set/enable/vrc_mic_mute_sync": {"status": True, "variable":controller.setEnableVrcMicMuteSync},
"/set/disable/vrc_mic_mute_sync": {"status": True, "variable":controller.setDisableVrcMicMuteSync},
"/get/data/send_message_to_vrc": {"status": True, "variable":controller.getSendMessageToVrc},
"/set/enable/send_message_to_vrc": {"status": True, "variable":controller.setEnableSendMessageToVrc},
"/set/disable/send_message_to_vrc": {"status": True, "variable":controller.setDisableSendMessageToVrc},
"/get/data/send_received_message_to_vrc": {"status": True, "variable":controller.getSendReceivedMessageToVrc},
"/set/enable/send_received_message_to_vrc": {"status": True, "variable":controller.setEnableSendReceivedMessageToVrc},
"/set/disable/send_received_message_to_vrc": {"status": True, "variable":controller.setDisableSendReceivedMessageToVrc},
# Advanced Settings
"/get/data/osc_ip_address": {"status": True, "variable":controller.getOscIpAddress},
"/set/data/osc_ip_address": {"status": True, "variable":controller.setOscIpAddress},
"/get/data/osc_port": {"status": True, "variable":controller.getOscPort},
"/set/data/osc_port": {"status": True, "variable":controller.setOscPort},
"/run/open_filepath_config_file": {"status": True, "variable":controller.openFilepathConfigFile},
# "/run/start_watchdog": {"status": True, "variable":controller.startWatchdog},
"/run/feed_watchdog": {"status": True, "variable":controller.feedWatchdog},
# "/run/stop_watchdog": {"status": True, "variable":controller.stopWatchdog},
}
init_mapping = {key:value for key, value in mapping.items() if key.startswith("/get/data/")}
controller.setInitMapping(init_mapping)
class Main:
def __init__(self) -> None:
self.queue = Queue()
self.main_loop = True
def receiver(self) -> None:
while True:
received_data = sys.stdin.readline().strip()
received_data = json.loads(received_data)
if received_data:
endpoint = received_data.get("endpoint", None)
data = received_data.get("data", None)
data = encodeBase64(data) if data is not None else None
printLog(endpoint, {"receive_data":data})
self.queue.put((endpoint, data))
def startReceiver(self) -> None:
th_receiver = Thread(target=self.receiver)
th_receiver.daemon = True
th_receiver.start()
def handleRequest(self, endpoint, data=None) -> tuple:
handler = mapping.get(endpoint)
if handler is None:
response = "Invalid endpoint"
status = 404
elif handler["status"] is False:
response = "Locked endpoint"
status = 423
else:
try:
response = handler["variable"](data)
status = response.get("status", None)
result = response.get("result", None)
except Exception as e:
errorLogging()
result = str(e)
status = 500
return result, status
def handler(self) -> None:
while True:
if not self.queue.empty():
try:
endpoint, data = self.queue.get()
result, status = self.handleRequest(endpoint, data)
except Exception as e:
errorLogging()
result = str(e)
status = 500
if status == 423:
self.queue.put((endpoint, data))
else:
printLog(endpoint, {"send_data":result})
printResponse(status, endpoint, result)
time.sleep(0.1)
def startHandler(self) -> None:
th_handler = Thread(target=self.handler)
th_handler.daemon = True
th_handler.start()
def start(self) -> None:
while self.main_loop:
time.sleep(1)
def stop(self) -> None:
self.main_loop = False
if __name__ == "__main__":
main = Main()
main.startReceiver()
main.startHandler()
controller.setWatchdogCallback(main.stop)
controller.init()
# mappingのすべてのstatusをTrueにする
for key in mapping.keys():
mapping[key]["status"] = True
process = "main"
match process:
case "main":
main.start()
case "test":
for _ in range(100):
time.sleep(0.5)
endpoint = "/get/data/mic_host_list"
result, status = main.handleRequest(endpoint)
printResponse(status, endpoint, result)
case "test_all":
import time
for endpoint, value in mapping.items():
printLog("endpoint", endpoint)
match endpoint:
case "/run/send_message_box":
# handleRequest("/set/enable/translation")
# handleRequest("/set/enable/convert_message_to_romaji")
data = {"id":"123456", "message":"テスト"}
case "/set/data/selected_translation_engines":
data = {
"1":"CTranslate2",
"2":"CTranslate2",
"3":"CTranslate2",
}
case "/set/data/selected_your_languages":
data = {
"1":{
"1":{
"language": "English",
"country": "Hong Kong"
},
},
"2":{
"1":{
"language":"Japanese",
"country":"Japan"
},
},
"3":{
"1":{
"language":"Japanese",
"country":"Japan"
},
},
}
case "/set/data/selected_target_languages":
data ={
"1":{
"1": {
"language": "Japanese",
"country": "Japan",
"enabled": True,
},
"secondary": {
"language": "English",
"country": "United States",
"enabled": True,
},
"tertiary": {
"language": "Chinese Simplified",
"country": "China",
"enabled": True,
}
},
"2":{
"1":{
"language":"English",
"country":"United States",
"enabled": True,
},
"secondary":{
"language":"English",
"country":"United States",
"enabled": True,
},
"tertiary":{
"language":"English",
"country":"United States",
"enabled": True,
},
},
"3":{
"1":{
"language":"English",
"country":"United States",
"enabled": True,
},
"secondary":{
"language":"English",
"country":"United States",
"enabled": True,
},
"tertiary":{
"language":"English",
"country":"United States",
"enabled": True,
},
},
}
case "/set/data/transparency":
data = 0.5
case "/set/appearance":
data = "Dark"
case "/set/data/ui_scaling":
data = 1.5
case "/set/data/appearance_theme":
data = "Dark"
case "/set/data/textbox_ui_scaling":
data = 1.5
case "/set/data/message_box_ratio":
data = 0.5
case "/set/data/font_family":
data = "Yu Gothic UI"
case "/set/data/ui_language":
data = "ja"
case "/set/data/ctranslate2_weight_type":
data = "small"
case "/set/data/deepl_auth_key":
data = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:fx"
case "/set/data/selected_mic_host":
data = "MME"
case "/set/data/selected_mic_device":
data = "マイク (Realtek High Definition Audio)"
case "/set/data/mic_threshold":
data = 0.5
case "/set/data/mic_record_timeout":
data = 1
case "/set/data/mic_phrase_timeout":
data = 5
case "/set/data/mic_max_phrases":
data = 5
case "/set//data/mic_word_filter":
data = "test0, test1, test2"
case "/set/data/selected_speaker_device":
data = "スピーカー (Realtek High Definition Audio)"
case "/set/data/speaker_threshold":
data = 0.5
case "/set/data/speaker_record_timeout":
data = 5
case "/set/data/speaker_phrase_timeout":
data = 5
case "/set/data/speaker_max_phrases":
data = 5
case "/set/data/whisper_weight_type":
data = "base"
case "/set/data/overlay_settings":
data = {
"opacity": 0.5,
"ui_scaling": 1.5,
}
case "/set/data/overlay_small_log_settings":
data = {
"x_pos": 0,
"y_pos": 0,
"z_pos": 0,
"x_rotation": 0,
"y_rotation": 0,
"z_rotation": 0,
"display_duration": 5,
"fadeout_duration": 0.5,
}
case "/set/data/send_message_button_type":
data = "show"
case "/set/data/send_message_format":
data = "[message]"
case "/set/data/send_message_format_with_t":
data = "[message]([translation])"
case "/set/data/received_message_format":
data = "[message]"
case "/set/data/received_message_format_with_t":
data = "[message]([translation])"
case "/set/data/osc_ip_address":
data = "127.0.0.1"
case "/set/data/osc_port":
data = 8000
case "/set/data/speaker_no_speech_prob":
data = 0.5
case "/set/data/speaker_avg_logprob":
data = 0.5
case "/set/data/mic_no_speech_prob":
data = 0.5
case "/set/data/mic_avg_logprob":
data = 0.5
case _:
data = None
result, status = main.handleRequest(endpoint, data)
printResponse(status, endpoint, result)
time.sleep(0.5)

815
src-python/model.py Normal file
View File

@@ -0,0 +1,815 @@
import copy
import gc
from subprocess import Popen
from os import makedirs as os_makedirs
from os import path as os_path
from datetime import datetime
from time import sleep
from queue import Queue
from threading import Thread
from requests import get as requests_get
from typing import Callable
from packaging.version import parse
from flashtext import KeywordProcessor
from pykakasi import kakasi
from device_manager import device_manager
from config import config
from models.translation.translation_translator import Translator
from models.osc.osc import OSCHandler
from models.transcription.transcription_recorder import SelectedMicEnergyAndAudioRecorder, SelectedSpeakerEnergyAndAudioRecorder
from models.transcription.transcription_recorder import SelectedMicEnergyRecorder, SelectedSpeakerEnergyRecorder
from models.transcription.transcription_transcriber import AudioTranscriber
from models.translation.translation_languages import translation_lang
from models.transcription.transcription_languages import transcription_lang
from models.translation.translation_utils import checkCTranslate2Weight, downloadCTranslate2Weight
from models.transcription.transcription_whisper import checkWhisperWeight, downloadWhisperWeight
from models.overlay.overlay import Overlay
from models.overlay.overlay_image import OverlayImage
from models.watchdog.watchdog import Watchdog
from utils import errorLogging, setupLogger
class threadFnc(Thread):
def __init__(self, fnc, end_fnc=None, daemon=True, *args, **kwargs):
super(threadFnc, self).__init__(daemon=daemon, target=fnc, *args, **kwargs)
self.fnc = fnc
self.end_fnc = end_fnc
self.loop = True
self._pause = False
def stop(self):
self.loop = False
def pause(self):
self._pause = True
def resume(self):
self._pause = False
def run(self):
while self.loop:
self.fnc(*self._args, **self._kwargs)
while self._pause:
sleep(0.1)
if callable(self.end_fnc):
self.end_fnc()
return
class Model:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(Model, cls).__new__(cls)
cls._instance.init()
return cls._instance
def init(self):
self.logger = None
self.th_check_device = None
self.mic_print_transcript = None
self.mic_audio_recorder = None
self.mic_energy_recorder = None
self.mic_energy_plot_progressbar = None
self.speaker_print_transcript = None
self.speaker_audio_recorder = None
self.speaker_energy_recorder = None
self.speaker_energy_plot_progressbar = None
self.previous_send_message = ""
self.previous_receive_message = ""
self.translator = Translator()
self.keyword_processor = KeywordProcessor()
overlay_small_log_settings = copy.deepcopy(config.OVERLAY_SMALL_LOG_SETTINGS)
overlay_large_log_settings = copy.deepcopy(config.OVERLAY_LARGE_LOG_SETTINGS)
overlay_large_log_settings["ui_scaling"] = overlay_large_log_settings["ui_scaling"] * 0.25
overlay_settings = {
"small": overlay_small_log_settings,
"large": overlay_large_log_settings,
}
self.overlay = Overlay(overlay_settings)
self.overlay_image = OverlayImage()
self.mic_audio_queue = None
self.mic_mute_status = None
self.kks = kakasi()
self.watchdog = Watchdog(config.WATCHDOG_TIMEOUT, config.WATCHDOG_INTERVAL)
self.osc_handler = OSCHandler(config.OSC_IP_ADDRESS, config.OSC_PORT)
def checkTranslatorCTranslate2ModelWeight(self, weight_type:str):
return checkCTranslate2Weight(config.PATH_LOCAL, weight_type)
def changeTranslatorCTranslate2Model(self):
self.translator.changeCTranslate2Model(
config.PATH_LOCAL,
config.CTRANSLATE2_WEIGHT_TYPE,
config.SELECTED_TRANSLATION_COMPUTE_DEVICE["device"],
config.SELECTED_TRANSLATION_COMPUTE_DEVICE["device_index"])
def downloadCTranslate2ModelWeight(self, weight_type, callback=None, end_callback=None):
return downloadCTranslate2Weight(config.PATH_LOCAL, weight_type, callback, end_callback)
def isLoadedCTranslate2Model(self):
return self.translator.isLoadedCTranslate2Model()
def checkTranscriptionWhisperModelWeight(self, weight_type:str):
return checkWhisperWeight(config.PATH_LOCAL, weight_type)
def downloadWhisperModelWeight(self, weight_type, callback=None, end_callback=None):
return downloadWhisperWeight(config.PATH_LOCAL, weight_type, callback, end_callback)
def resetKeywordProcessor(self):
del self.keyword_processor
self.keyword_processor = KeywordProcessor()
def authenticationTranslatorDeepLAuthKey(self, auth_key):
result = self.translator.authenticationDeepLAuthKey(auth_key)
return result
def startLogger(self):
os_makedirs(config.PATH_LOGS, exist_ok=True)
file_name = os_path.join(config.PATH_LOGS, f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log")
self.logger = setupLogger("log", file_name)
self.logger.disabled = False
def stopLogger(self):
self.logger.disabled = True
self.logger = None
def getListLanguageAndCountry(self):
transcription_langs = list(transcription_lang.keys())
translation_langs = []
for tl_key in translation_lang.keys():
for lang in translation_lang[tl_key]["source"]:
translation_langs.append(lang)
translation_langs = list(set(translation_langs))
supported_langs = list(filter(lambda x: x in transcription_langs, translation_langs))
languages = []
for language in supported_langs:
for country in transcription_lang[language]:
languages.append(
{
"language" : language,
"country" : country,
}
)
languages = sorted(languages, key=lambda x: x['language'])
return languages
def findTranslationEngines(self, source_lang, target_lang, engines_status):
selectable_engines = [key for key, value in engines_status.items() if value is True]
compatible_engines = []
for engine in list(translation_lang.keys()):
languages = translation_lang.get(engine, {}).get("source", {})
source_langs = [e["language"] for e in list(source_lang.values()) if e["enable"] is True]
target_langs = [e["language"] for e in list(target_lang.values()) if e["enable"] is True]
language_list = list(languages.keys())
if all(e in language_list for e in source_langs) and all(e in language_list for e in target_langs):
if engine in selectable_engines:
compatible_engines.append(engine)
return compatible_engines
def getTranslate(self, translator_name, source_language, target_language, target_country, message):
success_flag = False
translation = self.translator.translate(
translator_name=translator_name,
source_language=source_language,
target_language=target_language,
target_country=target_country,
message=message
)
# 翻訳失敗時のフェールセーフ処理
if isinstance(translation, str):
success_flag = True
else:
while True:
translation = self.translator.translate(
translator_name="CTranslate2",
source_language=source_language,
target_language=target_language,
target_country=target_country,
message=message
)
if translation is not False:
break
sleep(0.1)
return translation, success_flag
def getInputTranslate(self, message, source_language=None):
translator_name=config.SELECTED_TRANSLATION_ENGINES[config.SELECTED_TAB_NO]
if source_language is None:
source_language=config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"]
target_languages=config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO]
translations = []
success_flags = []
for value in target_languages.values():
if value["enable"] is True:
target_language = value["language"]
target_country = value["country"]
if target_language is not None or target_country is not None:
translation, success_flag = self.getTranslate(
translator_name,
source_language,
target_language,
target_country,
message
)
translations.append(translation)
success_flags.append(success_flag)
return translations, success_flags
def getOutputTranslate(self, message, source_language=None):
translator_name=config.SELECTED_TRANSLATION_ENGINES[config.SELECTED_TAB_NO]
if source_language is None:
source_language=config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"]
target_language=config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"]
target_country=config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["country"]
translation, success_flag = self.getTranslate(
translator_name,
source_language,
target_language,
target_country,
message
)
return [translation], [success_flag]
def addKeywords(self):
for f in config.MIC_WORD_FILTER:
self.keyword_processor.add_keyword(f)
def checkKeywords(self, message):
return len(self.keyword_processor.extract_keywords(message)) != 0
def detectRepeatSendMessage(self, message):
repeat_flag = False
if self.previous_send_message == message:
repeat_flag = True
self.previous_send_message = message
return repeat_flag
def detectRepeatReceiveMessage(self, message):
repeat_flag = False
if self.previous_receive_message == message:
repeat_flag = True
self.previous_receive_message = message
return repeat_flag
def convertMessageToTransliteration(self, message: str) -> str:
data_list = self.kks.convert(message)
keys_to_keep = {"orig", "hira", "hepburn"}
filtered_list = []
for item in data_list:
filtered_item = {key: value for key, value in item.items() if key in keys_to_keep}
filtered_list.append(filtered_item)
return filtered_list
def setOscIpAddress(self, ip_address):
self.osc_handler.setOscIpAddress(ip_address)
def setOscPort(self, port):
self.osc_handler.setOscPort(port)
def oscStartSendTyping(self):
self.osc_handler.sendTyping(flag=True)
def oscStopSendTyping(self):
self.osc_handler.sendTyping(flag=False)
def oscSendMessage(self, message, notification=True):
self.osc_handler.sendMessage(message=message, notification=notification)
def getMuteSelfStatus(self):
return self.osc_handler.getOSCParameterMuteSelf()
def setMuteSelfStatus(self):
self.mic_mute_status = self.getMuteSelfStatus()
def startReceiveOSC(self):
def changeHandlerMute(address, osc_arguments):
if config.ENABLE_TRANSCRIPTION_SEND is True:
if osc_arguments is True and self.mic_mute_status is False:
self.mic_mute_status = osc_arguments
self.changeMicTranscriptStatus()
elif osc_arguments is False and self.mic_mute_status is True:
self.mic_mute_status = osc_arguments
self.changeMicTranscriptStatus()
dict_filter_and_target = {
self.osc_handler.osc_parameter_muteself: changeHandlerMute,
}
self.osc_handler.receiveOscParameters(dict_filter_and_target)
def stopReceiveOSC(self):
self.osc_handler.oscServerStop()
@staticmethod
def checkSoftwareUpdated():
# check update
update_flag = False
try:
response = requests_get(config.GITHUB_URL)
json_data = response.json()
version = json_data.get("name", None)
if isinstance(version, str):
new_version = parse(version)
current_version = parse(config.VERSION)
if new_version > current_version:
update_flag = True
except Exception:
errorLogging()
return update_flag
@staticmethod
def updateSoftware():
# try to update at most 5 times
for _ in range(5):
try:
program_name = "update.exe"
current_directory = config.PATH_LOCAL
res = requests_get(config.UPDATER_URL)
assets = res.json()['assets']
url = [i["browser_download_url"] for i in assets if i["name"] == program_name][0]
res = requests_get(url, stream=True)
with open(os_path.join(current_directory, program_name), 'wb') as file:
for chunk in res.iter_content(chunk_size=1024*5):
file.write(chunk)
break
except Exception:
errorLogging()
# run updater
Popen(program_name, cwd=current_directory)
@staticmethod
def updateCudaSoftware():
# try to update at most 5 times
for _ in range(5):
try:
program_name = "update.exe"
current_directory = config.PATH_LOCAL
res = requests_get(config.UPDATER_URL)
assets = res.json()['assets']
url = [i["browser_download_url"] for i in assets if i["name"] == program_name][0]
res = requests_get(url, stream=True)
with open(os_path.join(current_directory, program_name), 'wb') as file:
for chunk in res.iter_content(chunk_size=1024*5):
file.write(chunk)
break
except Exception:
errorLogging()
# run updater
Popen([program_name, "--cuda"], cwd=current_directory)
def getListMicHost(self):
result = [host for host in device_manager.getMicDevices().keys()]
return result
def getMicDefaultDevice(self):
result = device_manager.getMicDevices().get(config.SELECTED_MIC_HOST, [{"name": "NoDevice"}])[0]["name"]
return result
def getListMicDevice(self):
result = [device["name"] for device in device_manager.getMicDevices().get(config.SELECTED_MIC_HOST, [{"name": "NoDevice"}])]
return result
def getListSpeakerDevice(self):
result = [device["name"] for device in device_manager.getSpeakerDevices()]
return result
def startMicTranscript(self, fnc):
mic_host_name = config.SELECTED_MIC_HOST
mic_device_name = config.SELECTED_MIC_DEVICE
mic_device_list = device_manager.getMicDevices().get(mic_host_name, [{"name": "NoDevice"}])
selected_mic_device = [device for device in mic_device_list if device["name"] == mic_device_name]
if len(selected_mic_device) == 0:
return False
self.mic_audio_queue = Queue()
# self.mic_energy_queue = Queue()
mic_device = selected_mic_device[0]
record_timeout = config.MIC_RECORD_TIMEOUT
phrase_timeout = config.MIC_PHRASE_TIMEOUT
if record_timeout > phrase_timeout:
record_timeout = phrase_timeout
self.mic_audio_recorder = SelectedMicEnergyAndAudioRecorder(
device=mic_device,
energy_threshold=config.MIC_THRESHOLD,
dynamic_energy_threshold=config.MIC_AUTOMATIC_THRESHOLD,
phrase_time_limit=record_timeout,
)
# self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, mic_energy_queue)
self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, None)
self.mic_transcriber = AudioTranscriber(
speaker=False,
source=self.mic_audio_recorder.source,
phrase_timeout=phrase_timeout,
max_phrases=config.MIC_MAX_PHRASES,
transcription_engine=config.SELECTED_TRANSCRIPTION_ENGINE,
root=config.PATH_LOCAL,
whisper_weight_type=config.WHISPER_WEIGHT_TYPE,
device=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device"],
device_index=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device_index"],
)
def sendMicTranscript():
try:
selected_your_languages = config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]
languages = [data["language"] for data in selected_your_languages.values() if data["enable"] is True]
countries = [data["country"] for data in selected_your_languages.values() if data["enable"] is True]
res = self.mic_transcriber.transcribeAudioQueue(
self.mic_audio_queue,
languages,
countries,
config.MIC_AVG_LOGPROB,
config.MIC_NO_SPEECH_PROB
)
if res:
result = self.mic_transcriber.getTranscript()
fnc(result)
except Exception:
errorLogging()
def endMicTranscript():
while not self.mic_audio_queue.empty():
self.mic_audio_queue.get()
# while not self.mic_energy_queue.empty():
# self.mic_energy_queue.get()
del self.mic_transcriber
gc.collect()
# def sendMicEnergy():
# if mic_energy_queue.empty() is False:
# energy = mic_energy_queue.get()
# # print("mic energy:", energy)
# try:
# fnc(energy)
# except Exception:
# pass
# sleep(0.01)
self.mic_print_transcript = threadFnc(sendMicTranscript, end_fnc=endMicTranscript)
self.mic_print_transcript.daemon = True
self.mic_print_transcript.start()
# self.mic_get_energy = threadFnc(sendMicEnergy)
# self.mic_get_energy.daemon = True
# self.mic_get_energy.start()
self.changeMicTranscriptStatus()
def resumeMicTranscript(self):
# キューをクリア
if isinstance(self.mic_audio_queue, Queue):
while not self.mic_audio_queue.empty():
self.mic_audio_queue.get()
# 文字起こしを再開
# if isinstance(self.mic_print_transcript, threadFnc):
# self.mic_print_transcript.resume()
# 音声のレコードを再開
if isinstance(self.mic_audio_recorder, SelectedMicEnergyAndAudioRecorder):
self.mic_audio_recorder.resume()
def pauseMicTranscript(self):
# 文字起こしを一時停止
# if isinstance(self.mic_print_transcript, threadFnc):
# self.mic_print_transcript.pause()
# 音声のレコードを一時停止
if isinstance(self.mic_audio_recorder, SelectedMicEnergyAndAudioRecorder):
self.mic_audio_recorder.pause()
def changeMicTranscriptStatus(self):
if config.VRC_MIC_MUTE_SYNC is True:
if self.mic_mute_status is True:
self.pauseMicTranscript()
elif self.mic_mute_status is False:
self.resumeMicTranscript()
else:
pass
else:
self.resumeMicTranscript()
def stopMicTranscript(self):
if isinstance(self.mic_print_transcript, threadFnc):
self.mic_print_transcript.stop()
self.mic_print_transcript.join()
self.mic_print_transcript = None
if isinstance(self.mic_audio_recorder, SelectedMicEnergyAndAudioRecorder):
self.mic_audio_recorder.resume()
self.mic_audio_recorder.stop()
self.mic_audio_recorder = None
# if isinstance(self.mic_get_energy, threadFnc):
# self.mic_get_energy.stop()
# self.mic_get_energy = None
def startCheckMicEnergy(self, fnc:Callable[[float], None]=None) -> None:
if isinstance(fnc, Callable):
self.check_mic_energy_fnc = fnc
mic_host_name = config.SELECTED_MIC_HOST
mic_device_name = config.SELECTED_MIC_DEVICE
mic_device_list = device_manager.getMicDevices().get(mic_host_name, [{"name": "NoDevice"}])
selected_mic_device = [device for device in mic_device_list if device["name"] == mic_device_name]
if len(selected_mic_device) == 0:
return False
def sendMicEnergy():
if mic_energy_queue.empty() is False:
energy = mic_energy_queue.get()
try:
self.check_mic_energy_fnc(energy)
except Exception:
errorLogging()
sleep(0.01)
mic_energy_queue = Queue()
mic_device = selected_mic_device[0]
self.mic_energy_recorder = SelectedMicEnergyRecorder(mic_device)
self.mic_energy_recorder.recordIntoQueue(mic_energy_queue)
self.mic_energy_plot_progressbar = threadFnc(sendMicEnergy)
self.mic_energy_plot_progressbar.daemon = True
self.mic_energy_plot_progressbar.start()
def stopCheckMicEnergy(self):
if isinstance(self.mic_energy_plot_progressbar, threadFnc):
self.mic_energy_plot_progressbar.stop()
self.mic_energy_plot_progressbar.join()
self.mic_energy_plot_progressbar = None
if isinstance(self.mic_energy_recorder, SelectedMicEnergyRecorder):
self.mic_energy_recorder.resume()
self.mic_energy_recorder.stop()
self.mic_energy_recorder = None
def startSpeakerTranscript(self, fnc):
speaker_device_list = device_manager.getSpeakerDevices()
selected_speaker_device = [device for device in speaker_device_list if device["name"] == config.SELECTED_SPEAKER_DEVICE]
if len(selected_speaker_device) == 0:
return False
speaker_audio_queue = Queue()
# speaker_energy_queue = Queue()
speaker_device = selected_speaker_device[0]
record_timeout = config.SPEAKER_RECORD_TIMEOUT
phrase_timeout = config.SPEAKER_PHRASE_TIMEOUT
if record_timeout > phrase_timeout:
record_timeout = phrase_timeout
self.speaker_audio_recorder = SelectedSpeakerEnergyAndAudioRecorder(
device=speaker_device,
energy_threshold=config.SPEAKER_THRESHOLD,
dynamic_energy_threshold=config.SPEAKER_AUTOMATIC_THRESHOLD,
phrase_time_limit=record_timeout,
)
# self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, speaker_energy_queue)
self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, None)
self.speaker_transcriber = AudioTranscriber(
speaker=True,
source=self.speaker_audio_recorder.source,
phrase_timeout=phrase_timeout,
max_phrases=config.SPEAKER_MAX_PHRASES,
transcription_engine=config.SELECTED_TRANSCRIPTION_ENGINE,
root=config.PATH_LOCAL,
whisper_weight_type=config.WHISPER_WEIGHT_TYPE,
device=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device"],
device_index=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device_index"],
)
def sendSpeakerTranscript():
try:
selected_target_languages = config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO]
languages = [data["language"] for data in selected_target_languages.values() if data["enable"] is True]
countries = [data["country"] for data in selected_target_languages.values() if data["enable"] is True]
res = self.speaker_transcriber.transcribeAudioQueue(
speaker_audio_queue,
languages,
countries,
config.SPEAKER_AVG_LOGPROB,
config.SPEAKER_NO_SPEECH_PROB
)
if res:
result = self.speaker_transcriber.getTranscript()
fnc(result)
except Exception:
errorLogging()
def endSpeakerTranscript():
while not speaker_audio_queue.empty():
speaker_audio_queue.get()
# while not speaker_energy_queue.empty():
# speaker_energy_queue.get()
del self.speaker_transcriber
gc.collect()
# def sendSpeakerEnergy():
# if speaker_energy_queue.empty() is False:
# energy = speaker_energy_queue.get()
# # print("speaker energy:", energy)
# try:
# fnc(energy)
# except Exception:
# pass
# sleep(0.01)
self.speaker_print_transcript = threadFnc(sendSpeakerTranscript, end_fnc=endSpeakerTranscript)
self.speaker_print_transcript.daemon = True
self.speaker_print_transcript.start()
# self.speaker_get_energy = threadFnc(sendSpeakerEnergy)
# self.speaker_get_energy.daemon = True
# self.speaker_get_energy.start()
def stopSpeakerTranscript(self):
if isinstance(self.speaker_print_transcript, threadFnc):
self.speaker_print_transcript.stop()
self.speaker_print_transcript.join()
self.speaker_print_transcript = None
if isinstance(self.speaker_audio_recorder, SelectedSpeakerEnergyAndAudioRecorder):
self.speaker_audio_recorder.stop()
self.speaker_audio_recorder = None
# if isinstance(self.speaker_get_energy, threadFnc):
# self.speaker_get_energy.stop()
# self.speaker_get_energy = None
def startCheckSpeakerEnergy(self, fnc:Callable[[float], None]=None) -> None:
if isinstance(fnc, Callable):
self.check_speaker_energy_fnc = fnc
speaker_device_list = device_manager.getSpeakerDevices()
selected_speaker_device = [device for device in speaker_device_list if device["name"] == config.SELECTED_SPEAKER_DEVICE]
if len(selected_speaker_device) == 0:
return False
def sendSpeakerEnergy():
if speaker_energy_queue.empty() is False:
energy = speaker_energy_queue.get()
try:
self.check_speaker_energy_fnc(energy)
except Exception:
errorLogging()
sleep(0.01)
speaker_energy_queue = Queue()
speaker_device = selected_speaker_device[0]
self.speaker_energy_recorder = SelectedSpeakerEnergyRecorder(speaker_device)
self.speaker_energy_recorder.recordIntoQueue(speaker_energy_queue)
self.speaker_energy_plot_progressbar = threadFnc(sendSpeakerEnergy)
self.speaker_energy_plot_progressbar.daemon = True
self.speaker_energy_plot_progressbar.start()
def stopCheckSpeakerEnergy(self):
if isinstance(self.speaker_energy_plot_progressbar, threadFnc):
self.speaker_energy_plot_progressbar.stop()
self.speaker_energy_plot_progressbar.join()
self.speaker_energy_plot_progressbar = None
if isinstance(self.speaker_energy_recorder, SelectedSpeakerEnergyRecorder):
self.speaker_energy_recorder.resume()
self.speaker_energy_recorder.stop()
self.speaker_energy_recorder = None
def createOverlayImageSmallLog(self, message, translation):
your_language = config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"]
target_language = config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"]
return self.overlay_image.createOverlayImageSmallLog(message, your_language, translation, target_language)
def createOverlayImageSmallMessage(self, message):
ui_language = config.UI_LANGUAGE
convert_languages = {
"en": "Japanese",
"jp": "Japanese",
"ko":"Korean",
"zh-Hans":"Chinese Simplified",
"zh-Hant":"Chinese Traditional",
}
language = convert_languages.get(ui_language, "Japanese")
return self.overlay_image.createOverlayImageSmallLog(message, language)
def clearOverlayImageSmallLog(self):
self.overlay.clearImage("small")
def updateOverlaySmallLog(self, img):
self.overlay.updateImage(img, "small")
def updateOverlaySmallLogSettings(self):
size = "small"
if (self.overlay.settings[size]["x_pos"] != config.OVERLAY_SMALL_LOG_SETTINGS["x_pos"] or
self.overlay.settings[size]["y_pos"] != config.OVERLAY_SMALL_LOG_SETTINGS["y_pos"] or
self.overlay.settings[size]["z_pos"] != config.OVERLAY_SMALL_LOG_SETTINGS["z_pos"] or
self.overlay.settings[size]["x_rotation"] != config.OVERLAY_SMALL_LOG_SETTINGS["x_rotation"] or
self.overlay.settings[size]["y_rotation"] != config.OVERLAY_SMALL_LOG_SETTINGS["y_rotation"] or
self.overlay.settings[size]["z_rotation"] != config.OVERLAY_SMALL_LOG_SETTINGS["z_rotation"] or
self.overlay.settings[size]["tracker"] != config.OVERLAY_SMALL_LOG_SETTINGS["tracker"]):
self.overlay.updatePosition(
config.OVERLAY_SMALL_LOG_SETTINGS["x_pos"],
config.OVERLAY_SMALL_LOG_SETTINGS["y_pos"],
config.OVERLAY_SMALL_LOG_SETTINGS["z_pos"],
config.OVERLAY_SMALL_LOG_SETTINGS["x_rotation"],
config.OVERLAY_SMALL_LOG_SETTINGS["y_rotation"],
config.OVERLAY_SMALL_LOG_SETTINGS["z_rotation"],
config.OVERLAY_SMALL_LOG_SETTINGS["tracker"],
size,
)
if (self.overlay.settings[size]["display_duration"] != config.OVERLAY_SMALL_LOG_SETTINGS["display_duration"]):
self.overlay.updateDisplayDuration(config.OVERLAY_SMALL_LOG_SETTINGS["display_duration"], size)
if (self.overlay.settings[size]["fadeout_duration"] != config.OVERLAY_SMALL_LOG_SETTINGS["fadeout_duration"]):
self.overlay.updateFadeoutDuration(config.OVERLAY_SMALL_LOG_SETTINGS["fadeout_duration"], size)
if (self.overlay.settings[size]["opacity"] != config.OVERLAY_SMALL_LOG_SETTINGS["opacity"]):
self.overlay.updateOpacity(config.OVERLAY_SMALL_LOG_SETTINGS["opacity"], size, True)
if (self.overlay.settings[size]["ui_scaling"] != config.OVERLAY_SMALL_LOG_SETTINGS["ui_scaling"]):
self.overlay.updateUiScaling(config.OVERLAY_SMALL_LOG_SETTINGS["ui_scaling"], size)
def createOverlayImageLargeLog(self, message_type:str, message:str, translation:str):
your_language = config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"]
target_language = config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO]["1"]["language"]
return self.overlay_image.createOverlayImageLargeLog(message_type, message, your_language, translation, target_language)
def createOverlayImageLargeMessage(self, message):
ui_language = config.UI_LANGUAGE
convert_languages = {
"en": "Japanese",
"jp": "Japanese",
"ko":"Korean",
"zh-Hans":"Chinese Simplified",
"zh-Hant":"Chinese Traditional",
}
language = convert_languages.get(ui_language, "Japanese")
overlay_image = OverlayImage()
for _ in range(2):
overlay_image.createOverlayImageLargeLog("send", message, language)
overlay_image.createOverlayImageLargeLog("receive", message, language)
return overlay_image.createOverlayImageLargeLog("send", message, language)
def clearOverlayImageLargeLog(self):
self.overlay.clearImage("large")
def updateOverlayLargeLog(self, img):
self.overlay.updateImage(img, "large")
def updateOverlayLargeLogSettings(self):
size = "large"
if (self.overlay.settings[size]["x_pos"] != config.OVERLAY_LARGE_LOG_SETTINGS["x_pos"] or
self.overlay.settings[size]["y_pos"] != config.OVERLAY_LARGE_LOG_SETTINGS["y_pos"] or
self.overlay.settings[size]["z_pos"] != config.OVERLAY_LARGE_LOG_SETTINGS["z_pos"] or
self.overlay.settings[size]["x_rotation"] != config.OVERLAY_LARGE_LOG_SETTINGS["x_rotation"] or
self.overlay.settings[size]["y_rotation"] != config.OVERLAY_LARGE_LOG_SETTINGS["y_rotation"] or
self.overlay.settings[size]["z_rotation"] != config.OVERLAY_LARGE_LOG_SETTINGS["z_rotation"] or
self.overlay.settings[size]["tracker"] != config.OVERLAY_LARGE_LOG_SETTINGS["tracker"]):
self.overlay.updatePosition(
config.OVERLAY_LARGE_LOG_SETTINGS["x_pos"],
config.OVERLAY_LARGE_LOG_SETTINGS["y_pos"],
config.OVERLAY_LARGE_LOG_SETTINGS["z_pos"],
config.OVERLAY_LARGE_LOG_SETTINGS["x_rotation"],
config.OVERLAY_LARGE_LOG_SETTINGS["y_rotation"],
config.OVERLAY_LARGE_LOG_SETTINGS["z_rotation"],
config.OVERLAY_LARGE_LOG_SETTINGS["tracker"],
size,
)
if (self.overlay.settings[size]["display_duration"] != config.OVERLAY_LARGE_LOG_SETTINGS["display_duration"]):
self.overlay.updateDisplayDuration(config.OVERLAY_LARGE_LOG_SETTINGS["display_duration"], size)
if (self.overlay.settings[size]["fadeout_duration"] != config.OVERLAY_LARGE_LOG_SETTINGS["fadeout_duration"]):
self.overlay.updateFadeoutDuration(config.OVERLAY_LARGE_LOG_SETTINGS["fadeout_duration"], size)
if (self.overlay.settings[size]["opacity"] != config.OVERLAY_LARGE_LOG_SETTINGS["opacity"]):
self.overlay.updateOpacity(config.OVERLAY_LARGE_LOG_SETTINGS["opacity"], size, True)
if (self.overlay.settings[size]["ui_scaling"] != config.OVERLAY_LARGE_LOG_SETTINGS["ui_scaling"]):
self.overlay.updateUiScaling(config.OVERLAY_LARGE_LOG_SETTINGS["ui_scaling"] * 0.25, size)
def startOverlay(self):
self.overlay.startOverlay()
def shutdownOverlay(self):
self.overlay.shutdownOverlay()
def startWatchdog(self):
self.th_watchdog = threadFnc(self.watchdog.start)
self.th_watchdog.daemon = True
self.th_watchdog.start()
def feedWatchdog(self):
self.watchdog.feed()
def setWatchdogCallback(self, callback):
self.watchdog.setCallback(callback)
def stopWatchdog(self):
if isinstance(self.th_watchdog, threadFnc):
self.th_watchdog.stop()
self.th_watchdog.join()
self.th_watchdog = None
model = Model()

View File

@@ -0,0 +1,105 @@
import asyncio
from typing import Any
from time import sleep
from threading import Thread
from pythonosc import udp_client, dispatcher, osc_server
from tinyoscquery.queryservice import OSCQueryService
from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient
from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port
from tinyoscquery.shared.node import OSCAccess
from utils import errorLogging
class OSCHandler:
def __init__(self, ip_address="127.0.0.1", port=9000) -> None:
self.osc_ip_address = ip_address
self.osc_port = port
self.osc_parameter_muteself = "/avatar/parameters/MuteSelf"
self.osc_parameter_chatbox_typing = "/chatbox/typing"
self.osc_parameter_chatbox_input = "/chatbox/input"
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
self.osc_server_name = "VRChat-Client"
self.osc_server = None
self.osc_query_service = None
self.osc_query_service_name = "VRCT"
self.osc_server_ip_address = ip_address
self.http_port = None
self.osc_server_port = None
def setOscIpAddress(self, ip_address:str) -> None:
self.osc_ip_address = ip_address
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
def setOscPort(self, port:int) -> None:
self.osc_port = port
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
# send OSC message typing
def sendTyping(self, flag:bool=False) -> None:
self.udp_client.send_message(self.osc_parameter_chatbox_typing, [flag])
# send OSC message
def sendMessage(self, message:str="", notification:bool=True) -> None:
if len(message) > 0:
self.udp_client.send_message(self.osc_parameter_chatbox_input, [f"{message}", True, notification])
def getOSCParameterValue(self, address:str) -> Any:
value = None
try:
browser = OSCQueryBrowser()
sleep(1)
service = browser.find_service_by_name(self.osc_server_name)
if service is not None:
osc_query_client = OSCQueryClient(service)
mute_self_node = osc_query_client.query_node(address)
value = mute_self_node.value[0]
browser.zc.close()
browser.browser.cancel()
except Exception:
errorLogging()
return value
def getOSCParameterMuteSelf(self) -> bool:
return self.getOSCParameterValue(self.osc_parameter_muteself)
def receiveOscParameters(self, dict_filter_and_target:dict) -> None:
self.osc_server_port = get_open_udp_port()
self.http_port = get_open_tcp_port()
osc_dispatcher = dispatcher.Dispatcher()
for filter, target in dict_filter_and_target.items():
osc_dispatcher.map(filter, target)
self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher, asyncio.get_event_loop())
Thread(target=self.oscServerServe, daemon=True).start()
while True:
try:
self.osc_query_service = OSCQueryService(self.osc_query_service_name, self.http_port, self.osc_server_port)
for filter, target in dict_filter_and_target.items():
self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE)
break
except Exception:
errorLogging()
sleep(1)
def oscServerServe(self) -> None:
self.osc_server.serve_forever(2)
def oscServerStop(self) -> None:
if isinstance(self.osc_server, osc_server.ThreadingOSCUDPServer):
self.osc_server.shutdown()
self.osc_server = None
if isinstance(self.osc_query_service, OSCQueryService):
self.osc_query_service.http_server.shutdown()
self.osc_query_service = None
if __name__ == "__main__":
handler = OSCHandler()
handler.receiveOscParameters({
"/avatar/parameters/MuteSelf": print,
})
sleep(5)
handler.sendTyping(True)
sleep(1)
handler.sendMessage(message="Hello World", notification=True)
sleep(60)
handler.oscServerStop()

View File

@@ -0,0 +1,369 @@
import os
import ctypes
import time
from psutil import process_iter
from threading import Thread
import openvr
import numpy as np
from PIL import Image
try:
from utils import errorLogging
except ImportError:
def errorLogging():
import traceback
print(traceback.format_exc())
try:
from . import overlay_utils as utils
except ImportError:
import overlay_utils as utils
def mat34Id(array):
arr = openvr.HmdMatrix34_t()
for i in range(3):
for j in range(4):
arr[i][j] = array[i][j]
return arr
def getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation):
arr = np.zeros((3, 4))
rot = utils.euler_to_rotation_matrix((x_rotation, y_rotation, z_rotation))
for i in range(3):
for j in range(3):
arr[i][j] = rot[i][j]
arr[0][3] = x_pos * z_pos
arr[1][3] = y_pos * z_pos
arr[2][3] = - z_pos
return arr
def getHMDBaseMatrix():
x_pos = 0.0
y_pos = -0.4
z_pos = 1.0
x_rotation = 0.0
y_rotation = 0.0
z_rotation = 0.0
arr = getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation)
return arr
def getLeftHandBaseMatrix():
x_pos = 0.3
y_pos = 0.1
z_pos = -0.31
x_rotation = -65.0
y_rotation = 165.0
z_rotation = 115.0
arr = getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation)
return arr
def getRightHandBaseMatrix():
x_pos = -0.3
y_pos = 0.1
z_pos = -0.31
x_rotation = -65.0
y_rotation = -165.0
z_rotation = -115.0
arr = getBaseMatrix(x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation)
return arr
class Overlay:
def __init__(self, settings_dict):
self.system = None
self.overlay = None
self.handle = None
self.init_process = False
self.initialized = False
self.loop = False
self.thread_overlay = None
self.settings = {}
self.lastUpdate = {}
self.fadeRatio = {}
for key, value in settings_dict.items():
self.settings[key] = value
self.lastUpdate[key] = time.monotonic()
self.fadeRatio[key] = 1
def init(self):
try:
self.system = openvr.init(openvr.VRApplication_Background)
self.overlay = openvr.IVROverlay()
self.overlay_system = openvr.IVRSystem()
self.handle = {}
for i, size in enumerate(self.settings.keys()):
self.handle[size] = self.overlay.createOverlay(f"VRCT{i}", f"VRCT{i}")
self.overlay.showOverlay(self.handle[size])
self.initialized = True
for size in self.settings.keys():
self.updateImage(Image.new("RGBA", (1, 1), (0, 0, 0, 0)), size)
self.updateColor([1, 1, 1], size)
self.updateOpacity(self.settings[size]["opacity"], size)
self.updateUiScaling(self.settings[size]["ui_scaling"], size)
self.updatePosition(
self.settings[size]["x_pos"],
self.settings[size]["y_pos"],
self.settings[size]["z_pos"],
self.settings[size]["x_rotation"],
self.settings[size]["y_rotation"],
self.settings[size]["z_rotation"],
self.settings[size]["tracker"],
size
)
self.updateDisplayDuration(self.settings[size]["display_duration"], size)
self.updateFadeoutDuration(self.settings[size]["fadeout_duration"], size)
self.init_process = False
except Exception:
errorLogging()
def updateImage(self, img, size):
if self.initialized is True:
width, height = img.size
img = img.tobytes()
img = (ctypes.c_char * len(img)).from_buffer_copy(img)
try:
self.overlay.setOverlayRaw(self.handle[size], img, width, height, 4)
except Exception:
errorLogging()
self.reStartOverlay()
while self.initialized is False:
time.sleep(0.1)
self.overlay.setOverlayRaw(self.handle[size], img, width, height, 4)
self.updateOpacity(self.settings[size]["opacity"], size)
self.lastUpdate[size] = time.monotonic()
def clearImage(self, size):
if self.initialized is True:
self.updateImage(Image.new("RGBA", (1, 1), (0, 0, 0, 0)), size)
def updateColor(self, col, size):
"""
col is a 3-tuple representing (r, g, b)
"""
if self.initialized is True:
r, g, b = col
self.overlay.setOverlayColor(self.handle[size], r, g, b)
def updateOpacity(self, opacity, size, with_fade=False):
self.settings[size]["opacity"] = opacity
if self.initialized is True:
if with_fade is True:
if self.fadeRatio[size] > 0:
self.overlay.setOverlayAlpha(self.handle[size], self.fadeRatio[size] * self.settings[size]["opacity"])
else:
self.overlay.setOverlayAlpha(self.handle[size], self.settings[size]["opacity"])
def updateUiScaling(self, ui_scaling, size):
self.settings[size]["ui_scaling"] = ui_scaling
if self.initialized is True:
self.overlay.setOverlayWidthInMeters(self.handle[size], self.settings[size]["ui_scaling"])
def updatePosition(self, x_pos, y_pos, z_pos, x_rotation, y_rotation, z_rotation, tracker, size):
"""
x_pos, y_pos, z_pos are floats representing the position of overlay
x_rotation, y_rotation, z_rotation are floats representing the rotation of overlay
tracker is a string representing the tracker to use ("HMD", "LeftHand", "RightHand")
"""
self.settings[size]["x_pos"] = x_pos
self.settings[size]["y_pos"] = y_pos
self.settings[size]["z_pos"] = z_pos
self.settings[size]["x_rotation"] = x_rotation
self.settings[size]["y_rotation"] = y_rotation
self.settings[size]["z_rotation"] = z_rotation
self.settings[size]["tracker"] = tracker
if self.initialized is True:
match tracker:
case "HMD":
base_matrix = getHMDBaseMatrix()
trackerIndex = openvr.k_unTrackedDeviceIndex_Hmd
case "LeftHand":
base_matrix = getLeftHandBaseMatrix()
trackerIndex = self.overlay_system.getTrackedDeviceIndexForControllerRole(openvr.TrackedControllerRole_LeftHand)
case "RightHand":
base_matrix = getRightHandBaseMatrix()
trackerIndex = self.overlay_system.getTrackedDeviceIndexForControllerRole(openvr.TrackedControllerRole_RightHand)
case _:
base_matrix = getHMDBaseMatrix()
trackerIndex = openvr.k_unTrackedDeviceIndex_Hmd
translation = (self.settings[size]["x_pos"], self.settings[size]["y_pos"], - self.settings[size]["z_pos"])
rotation = (self.settings[size]["x_rotation"], self.settings[size]["y_rotation"], self.settings[size]["z_rotation"])
transform = utils.transform_matrix(base_matrix, translation, rotation)
transform = mat34Id(transform)
self.overlay.setOverlayTransformTrackedDeviceRelative(
self.handle[size],
trackerIndex,
transform
)
def updateDisplayDuration(self, display_duration, size):
self.settings[size]["display_duration"] = display_duration
def updateFadeoutDuration(self, fadeout_duration, size):
self.settings[size]["fadeout_duration"] = fadeout_duration
def checkActive(self):
try:
if self.system is not None and self.initialized is True:
new_event = openvr.VREvent_t()
while self.system.pollNextEvent(new_event):
if new_event.eventType == openvr.VREvent_Quit:
return False
return True
except Exception:
errorLogging()
return False
def evaluateOpacityFade(self, size):
currentTime = time.monotonic()
if (currentTime - self.lastUpdate[size]) > self.settings[size]["display_duration"]:
timeThroughInterval = currentTime - self.lastUpdate[size] - self.settings[size]["display_duration"]
self.fadeRatio[size] = 1 - timeThroughInterval / self.settings[size]["fadeout_duration"]
if self.fadeRatio[size] < 0:
self.fadeRatio[size] = 0
self.overlay.setOverlayAlpha(self.handle[size], self.fadeRatio[size] * self.settings[size]["opacity"])
def update(self, size):
if self.settings[size]["fadeout_duration"] != 0:
self.evaluateOpacityFade(size)
else:
self.updateOpacity(self.settings[size]["opacity"], size)
def mainloop(self):
self.loop = True
while self.checkActive() is True and self.loop is True:
startTime = time.monotonic()
for size in self.settings.keys():
self.update(size)
sleepTime = (1 / 16) - (time.monotonic() - startTime)
if sleepTime > 0:
time.sleep(sleepTime)
def main(self):
while self.checkSteamvrRunning() is False:
time.sleep(10)
self.init()
if self.initialized is True:
self.mainloop()
def startOverlay(self):
if self.initialized is False and self.init_process is False:
self.init_process = True
self.thread_overlay = Thread(target=self.main)
self.thread_overlay.daemon = True
self.thread_overlay.start()
def shutdownOverlay(self):
if self.initialized is True and self.init_process is False:
if isinstance(self.thread_overlay, Thread):
self.loop = False
self.thread_overlay.join()
self.thread_overlay = None
if isinstance(self.overlay, openvr.IVROverlay):
for size in self.settings.keys():
if isinstance(self.handle[size], int):
self.overlay.destroyOverlay(self.handle[size])
self.overlay = None
if isinstance(self.system, openvr.IVRSystem):
openvr.shutdown()
self.system = None
self.initialized = False
def reStartOverlay(self):
self.shutdownOverlay()
self.startOverlay()
@staticmethod
def checkSteamvrRunning() -> bool:
_proc_name = "vrmonitor.exe" if os.name == "nt" else "vrmonitor"
return _proc_name in (p.name() for p in process_iter())
if __name__ == "__main__":
from overlay_image import OverlayImage
import logging
logging.basicConfig(level=logging.DEBUG)
small_settings = {
"x_pos": 0.0,
"y_pos": 0.0,
"z_pos": 0.0,
"x_rotation": 0.0,
"y_rotation": 0.0,
"z_rotation": 0.0,
"display_duration": 5,
"fadeout_duration": 2,
"opacity": 1.0,
"ui_scaling": 1.0,
"tracker": "HMD",
}
large_settings = {
"x_pos": 0.0,
"y_pos": 0.0,
"z_pos": 0.0,
"x_rotation": 0.0,
"y_rotation": 0.0,
"z_rotation": 0.0,
"display_duration": 5,
"fadeout_duration": 2,
"opacity": 1.0,
"ui_scaling": 0.25,
"tracker": "LeftHand",
}
settings_dict = {
"small": small_settings,
"large": large_settings
}
# オーバーレイの初期化設定を確認
logging.debug(f"Settings Dict: {settings_dict}")
overlay_image = OverlayImage()
overlay = Overlay(settings_dict)
overlay.startOverlay()
while overlay.initialized is False:
time.sleep(1)
# Example usage
for i in range(1000):
try:
print(i)
img = overlay_image.createOverlayImageLargeLog("send", f"こんにちは、世界!さようなら {i}", "Japanese", "Hello,World!Goodbye", "Japanese")
logging.debug(f"Generated Image: {img}")
overlay.updateImage(img, "large")
img = overlay_image.createOverlayImageSmallLog(f"こんにちは、世界さようなら_{i}", "Japanese", "Hello,World!Goodbye", "Japanese")
overlay.updateImage(img, "small")
time.sleep(1)
except openvr.error_code.OverlayError_InvalidParameter as e:
errorLogging()
logging.error(f"OverlayError_InvalidParameter: {e}")
break
except Exception as e:
errorLogging()
logging.error(f"Unexpected error: {e}")
break
# for i in range(100):
# print(i)
# # Example usage
# img = overlay_image.createOverlayImageSmallLog(f"こんにちは、世界さようなら_{i}", "Japanese", "Hello,World!Goodbye", "Japanese")
# overlay.updateImage(img, "small")
# time.sleep(5)
# if i%2 == 0:
# overlay.updatePosition(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, "HMD", "small")
# else:
# overlay.updatePosition(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, "RightHand", "small")
overlay.shutdownOverlay()

View File

@@ -0,0 +1,263 @@
from os import path as os_path
from datetime import datetime
from typing import Tuple
from PIL import Image, ImageDraw, ImageFont
try:
from utils import errorLogging
except ImportError:
def errorLogging():
import traceback
print(traceback.format_exc())
class OverlayImage:
LANGUAGES = {
"Japanese": "NotoSansJP-Regular",
"Korean": "NotoSansKR-Regular",
"Chinese Simplified": "NotoSansSC-Regular",
"Chinese Traditional": "NotoSansTC-Regular",
}
def __init__(self):
self.message_log = []
@staticmethod
def concatenateImagesVertically(img1: Image, img2: Image, margin: int = 0) -> Image:
total_height = img1.height + img2.height + margin
dst = Image.new("RGBA", (img1.width, total_height))
dst.paste(img1, (0, 0))
dst.paste(img2, (0, img1.height + margin))
return dst
@staticmethod
def addImageMargin(image: Image, top: int, right: int, bottom: int, left: int, color: Tuple[int, int, int, int]) -> Image:
new_width = image.width + right + left
new_height = image.height + top + bottom
result = Image.new(image.mode, (new_width, new_height), color)
result.paste(image, (left, top))
return result
@staticmethod
def getUiSizeSmallLog() -> dict:
return {
"width": 3840,
"height": 92,
"font_size": 92,
}
@staticmethod
def getUiColorSmallLog() -> dict:
colors = {
"background_color": (41, 42, 45),
"background_outline_color": (41, 42, 45),
"text_color": (223, 223, 223)
}
return colors
def createTextboxSmallLog(self, text:str, language:str, text_color:tuple, base_width:int, base_height:int, font_size:int) -> Image:
font_family = self.LANGUAGES.get(language, "NotoSansJP-Regular")
img = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
try:
font_path = os_path.join(os_path.dirname(os_path.dirname(os_path.dirname(__file__))), "fonts", f"{font_family}.ttf")
font = ImageFont.truetype(font_path, font_size)
except Exception:
errorLogging()
font_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", f"{font_family}.ttf")
font = ImageFont.truetype(font_path, font_size)
text_width = draw.textlength(text, font)
character_width = text_width // len(text)
character_line_num = (base_width // character_width) - 12
if len(text) > character_line_num:
text = "\n".join([text[i:i + character_line_num] for i in range(0, len(text), character_line_num)])
text_height = font_size * (len(text.split("\n")) + 1) + 20
img = Image.new("RGBA", (base_width, text_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
text_x = base_width // 2
text_y = text_height // 2
draw.text((text_x, text_y), text, text_color, anchor="mm", stroke_width=0, font=font, align="center")
return img
def createOverlayImageSmallLog(self, message:str, your_language:str, translation:str="", target_language:str=None) -> Image:
ui_size = self.getUiSizeSmallLog()
width, height, font_size = ui_size["width"], ui_size["height"], ui_size["font_size"]
ui_colors = self.getUiColorSmallLog()
text_color = ui_colors["text_color"]
background_color = ui_colors["background_color"]
background_outline_color = ui_colors["background_outline_color"]
img = self.createTextboxSmallLog(message, your_language, text_color, width, height, font_size)
if translation and target_language:
translation_img = self.createTextboxSmallLog(translation, target_language, text_color, width, height, font_size)
img = self.concatenateImagesVertically(img, translation_img)
background = Image.new("RGBA", img.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(background)
draw.rounded_rectangle([(0, 0), img.size], radius=50, fill=background_color, outline=background_outline_color, width=5)
return Image.alpha_composite(background, img)
@staticmethod
def getUiSizeLargeLog() -> dict:
return {
"width": 960,
"font_size_large": 30,
"font_size_small": 20,
"margin": 25,
"radius": 25,
"padding": 10,
"clause_margin": 20,
}
@staticmethod
def getUiColorLargeLog() -> dict:
return {
"background_color": (41, 42, 45),
"background_outline_color": (41, 42, 45),
"text_color_large": (223, 223, 223),
"text_color_small": (190, 190, 190),
"text_color_send": (97, 151, 180),
"text_color_receive": (168, 97, 180),
"text_color_time": (120, 120, 120)
}
def createTextImageLargeLog(self, message_type:str, size:str, text:str, language:str) -> Image:
ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_large"] if size == "large" else ui_size["font_size_small"]
text_color = self.getUiColorLargeLog()[f"text_color_{size}"]
anchor = "lm" if message_type == "receive" else "rm"
text_x = 0 if message_type == "receive" else ui_size["width"]
align = "left" if message_type == "receive" else "right"
font_family = self.LANGUAGES.get(language, "NotoSansJP-Regular")
img = Image.new("RGBA", (0, 0), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
try:
font_path = os_path.join(os_path.dirname(os_path.dirname(os_path.dirname(__file__))), "fonts", f"{font_family}.ttf")
font = ImageFont.truetype(font_path, font_size)
except Exception:
errorLogging()
font_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", f"{font_family}.ttf")
font = ImageFont.truetype(font_path, font_size)
text_width = draw.textlength(text, font)
character_width = text_width // len(text)
character_line_num = int((ui_size["width"] // character_width) - 1)
if len(text) > character_line_num:
text = "\n".join([text[i:i + character_line_num] for i in range(0, len(text), character_line_num)])
text_height = font_size * len(text.split("\n")) + ui_size["padding"]
img = Image.new("RGBA", (ui_size["width"], text_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
text_y = text_height // 2
draw.multiline_text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font, align=align)
return img
def createTextImageMessageType(self, message_type:str, date_time:str) -> Image:
ui_size = self.getUiSizeLargeLog()
font_size = ui_size["font_size_small"]
ui_padding = ui_size["padding"]
ui_color = self.getUiColorLargeLog()
text_color = ui_color[f"text_color_{message_type}"]
text_color_time = ui_color["text_color_time"]
anchor = "lm" if message_type == "receive" else "rm"
text = "Receive" if message_type == "receive" else "Send"
img = Image.new("RGBA", (0, 0), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
try:
font_path = os_path.join(os_path.dirname(os_path.dirname(os_path.dirname(__file__))), "fonts", "NotoSansJP-Regular.ttf")
font = ImageFont.truetype(font_path, font_size)
except Exception:
errorLogging()
font_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", "NotoSansJP-Regular.ttf")
font = ImageFont.truetype(font_path, font_size)
text_height = font_size + ui_padding
text_width = draw.textlength(date_time, font)
character_width = text_width // len(date_time)
img = Image.new("RGBA", (ui_size["width"], text_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
text_y = text_height // 2
text_time_x = 0 if message_type == "receive" else ui_size["width"] - (text_width + character_width)
text_x = (text_width + character_width) if message_type == "receive" else ui_size["width"]
draw.text((text_time_x, text_y), date_time, text_color_time, anchor=anchor, stroke_width=0, font=font)
draw.text((text_x, text_y), text, text_color, anchor=anchor, stroke_width=0, font=font)
return img
def createTextboxLargeLog(self, message_type:str, message:str, your_language:str, translation:str, target_language:str, date_time:str) -> Image:
message_type_img = self.createTextImageMessageType(message_type, date_time)
if translation and target_language:
img = self.createTextImageLargeLog(message_type, "small", message, your_language)
translation_img = self.createTextImageLargeLog(message_type, "large", translation, target_language)
img = self.concatenateImagesVertically(img, translation_img)
else:
img = self.createTextImageLargeLog(message_type, "large", message, your_language)
return self.concatenateImagesVertically(message_type_img, img)
def createOverlayImageLargeLog(self, message_type:str, message:str, your_language:str, translation:str="", target_language:str=None) -> Image:
ui_color = self.getUiColorLargeLog()
background_color = ui_color["background_color"]
background_outline_color = ui_color["background_outline_color"]
ui_size = self.getUiSizeLargeLog()
ui_margin = ui_size["margin"]
ui_radius = ui_size["radius"]
ui_clause_margin = ui_size["clause_margin"]
self.message_log.append({
"message_type": message_type,
"message": message,
"your_language": your_language,
"translation": translation,
"target_language": target_language,
"datetime": datetime.now().strftime("%H:%M")
})
if len(self.message_log) > 5:
self.message_log = self.message_log[-5:]
imgs = [
self.createTextboxLargeLog(
log["message_type"],
log["message"],
log["your_language"],
log["translation"],
log["target_language"],
log["datetime"]) for log in self.message_log
]
img = imgs[0]
for i in imgs[1:]:
img = self.concatenateImagesVertically(img, i, ui_clause_margin)
img = self.addImageMargin(img, ui_margin, ui_margin, ui_margin, ui_margin, (0, 0, 0, 0))
width, height = img.size
background = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(background)
draw.rounded_rectangle([(0, 0), (width, height)], radius=ui_radius, fill=background_color, outline=background_outline_color, width=5)
return Image.alpha_composite(background, img)
if __name__ == "__main__":
overlay = OverlayImage()
img = overlay.createOverlayImageSmallLog("Hello, World!", "English", "こんにちは、世界!", "Japanese")
img.save("overlay_small.png")
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", "こんにちは、世界!", "Japanese")
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", "Hello, World!", "English")
img = overlay.createOverlayImageLargeLog("send", "Hello, World!aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "English", "aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああこんにちは、世界", "Japanese")
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "Japanese", "Hello, World!aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "English")
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", "こんにちは、世界!", "Japanese")
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", "Hello, World!", "English")
img = overlay.createOverlayImageLargeLog("send", "Hello, World!aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "English", "aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああこんにちは、世界", "Japanese")
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "Japanese", "Hello, World!aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "English")
img = overlay.createOverlayImageLargeLog("send", "Hello, World!", "English", "こんにちは、世界!", "Japanese")
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界!", "Japanese", "Hello, World!", "English")
img = overlay.createOverlayImageLargeLog("send", "Hello, World!aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "English", "aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああこんにちは、世界", "Japanese")
img = overlay.createOverlayImageLargeLog("receive", "こんにちは、世界aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "Japanese", "Hello, World!aaaaaaaaaaaaaaaaaあああああああああああああああaaaaaaaaaaaaaaaaaあああああああああああああああ", "English")
img.save("overlay_large.png")

View File

@@ -0,0 +1,87 @@
import numpy as np
def toHomogeneous(matrix):
homogeneous_matrix = np.vstack([matrix, [0, 0, 0, 1]])
return homogeneous_matrix
# 移動行列を生成する関数
def calcTranslationMatrix(translation):
tx, ty, tz = translation
return np.array([
[1, 0, 0, tx],
[0, 1, 0, ty],
[0, 0, 1, tz],
[0, 0, 0, 1]
])
# X軸周りの回転行列を生成する関数
def calcRotationMatrixX(angle):
c = np.cos(np.pi/180*angle)
s = np.sin(np.pi/180*angle)
return np.array([
[1, 0, 0, 0],
[0, c, -s, 0],
[0, s, c, 0],
[0, 0, 0, 1]
])
# Y軸周りの回転行列を生成する関数
def calcRotationMatrixY(angle):
c = np.cos(np.pi/180*angle)
s = np.sin(np.pi/180*angle)
return np.array([
[c, 0, s, 0],
[0, 1, 0, 0],
[-s, 0, c, 0],
[0, 0, 0, 1]
])
# Z軸周りの回転行列を生成する関数
def calcRotationMatrixZ(angle):
c = np.cos(np.pi/180*angle)
s = np.sin(np.pi/180*angle)
return np.array([
[c, -s, 0, 0],
[s, c, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
])
# 3x4行列の座標を基準として回転や移動を行う関数
def transform_matrix(base_matrix, translation, rotation):
homogeneous_base_matrix = toHomogeneous(base_matrix)
translation_matrix = calcTranslationMatrix(translation)
rotation_matrix_x = calcRotationMatrixX(rotation[0])
rotation_matrix_y = calcRotationMatrixY(rotation[1])
rotation_matrix_z = calcRotationMatrixZ(rotation[2])
rotation_matrix = np.dot(rotation_matrix_z, np.dot(rotation_matrix_y, rotation_matrix_x))
transformation_matrix = translation_matrix.copy()
transformation_matrix[:3, :3] = rotation_matrix[:3, :3]
result_matrix = np.dot(homogeneous_base_matrix, transformation_matrix)
return result_matrix[:3, :]
def euler_to_rotation_matrix(angles):
phi = angles[0] * np.pi / 180
theta = angles[1] * np.pi / 180
psi = angles[2]* np.pi / 180
R_x = np.array([[1, 0, 0],
[0, np.cos(phi), -np.sin(phi)],
[0, np.sin(phi), np.cos(phi)]])
R_y = np.array([[np.cos(theta), 0, np.sin(theta)],
[0, 1, 0],
[-np.sin(theta), 0, np.cos(theta)]])
R_z = np.array([[np.cos(psi), -np.sin(psi), 0],
[np.sin(psi), np.cos(psi), 0],
[0, 0, 1]])
return np.dot(R_z, np.dot(R_y, R_x))
if __name__ == "__main__":
base_matrix = np.array([
[1, 0, 0, 1],
[0, 1, 0, 1],
[0, 0, 1, 1]
])
translation = [1, 2, 3]
rotation = [0, 0, 90]
result_matrix = transform_matrix(base_matrix, translation, rotation)
print(result_matrix)

View File

@@ -0,0 +1,730 @@
transcription_lang = {
"Afrikaans":{
"South Africa":{
"Google": "af-ZA",
"Whisper": "af",
},
},
"Albanian":{
"Albania":{
"Google": "sq-AL",
"Whisper": "sq",
},
},
"Amharic":{
"Ethiopia":{
"Google": "am-ET",
"Whisper": "am",
},
},
"Arabic":{
"Algeria":{
"Google": "ar-DZ",
"Whisper": "ar",
},
"Bahrain":{
"Google": "ar-BH",
"Whisper": "ar",
},
"Egypt":{
"Google": "ar-EG",
"Whisper": "ar",
},
"Israel":{
"Google": "ar-IL",
"Whisper": "ar",
},
"Iraq":{
"Google": "ar-IQ",
"Whisper": "ar",
},
"Jordan":{
"Google": "ar-JO",
"Whisper": "ar",
},
"Kuwait":{
"Google": "ar-KW",
"Whisper": "ar",
},
"Lebanon":{
"Google": "ar-LB",
"Whisper": "ar",
},
"Mauritania":{
"Google": "ar-MR",
"Whisper": "ar",
},
"Morocco":{
"Google": "ar-MA",
"Whisper": "ar",
},
"Oman":{
"Google": "ar-OM",
"Whisper": "ar",
},
"Qatar":{
"Google": "ar-QA",
"Whisper": "ar",
},
"Saudi Arabia":{
"Google": "ar-SA",
"Whisper": "ar",
},
"Palestine":{
"Google": "ar-PS",
"Whisper": "ar",
},
"Syria":{
"Google": "ar-SY",
"Whisper": "ar",
},
"Tunisia":{
"Google": "ar-TN",
"Whisper": "ar",
},
"United Arab Emirates":{
"Google": "ar-AE",
"Whisper": "ar",
},
"Yemen":{
"Google": "ar-YE",
"Whisper": "ar",
},
},
"Armenian": {
"Armenia": {
"Google": "hy-AM",
"Whisper": "hy",
},
},
"Azerbaijani": {
"Azerbaijan": {
"Google": "az-AZ",
"Whisper": "az",
},
},
"Basque":{
"Spain":{
"Google": "eu-ES",
"Whisper": "eu",
},
},
"Bengali":{
"Bangladesh":{
"Google": "bn-BD",
"Whisper": "bn",
},
"India":{
"Google": "bn-IN",
"Whisper": "bn",
},
},
"Bosnian":{
"Bosnia and Herzegovina":{
"Google": "bs-BA",
"Whisper": "bs",
}
},
"Bulgarian":{
"Bulgaria":{
"Google": "bg-BG",
"Whisper": "bg",
},
},
"Burmese":{
"Myanmar":{
"Google": "my-MM",
"Whisper": "my",
},
},
"Catalan":{
"Spain":{
"Google": "ca-ES",
"Whisper": "ca",
},
},
"Chinese Simplified":{
"China":{
"Google": "cmn-Hans-CN",
"Whisper": "zh",
},
"Hong Kong":{
"Google": "cmn-Hans-HK",
"Whisper": "zh",
},
},
"Chinese Traditional":{
"Taiwan":{
"Google": "cmn-Hant-TW",
"Whisper": "zh",
},
"Hong Kong":{
"Google": "yue-Hant-HK",
"Whisper": "yue",
},
},
"Croatian":{
"Croatia":{
"Google": "hr-HR",
"Whisper": "hr",
},
},
"Czech":{
"Czech Republic":{
"Google": "cs-CZ",
"Whisper": "cs",
},
},
"Danish":{
"Denmark":{
"Google": "da-DK",
"Whisper": "da",
},
},
"Dutch":{
"Belgium":{
"Google": "nl-BE",
"Whisper": "nl",
},
"Netherlands":{
"Google": "nl-NL",
"Whisper": "nl",
},
},
"English": {
"Australia":{
"Google": "en-AU",
"Whisper": "en",
},
"Canada":{
"Google": "en-CA",
"Whisper": "en",
},
"Ghana":{
"Google": "en-GH",
"Whisper": "en",
},
"Hong Kong":{
"Google": "en-HK",
"Whisper": "en",
},
"India":{
"Google": "en-IN",
"Whisper": "en",
},
"Ireland":{
"Google": "en-IE",
"Whisper": "en",
},
"Kenya":{
"Google": "en-KE",
"Whisper": "en",
},
"New Zealand":{
"Google": "en-NZ",
"Whisper": "en",
},
"Nigeria":{
"Google": "en-NG",
"Whisper": "en",
},
"Philippines":{
"Google": "en-PH",
"Whisper": "en",
},
"Singapore":{
"Google": "en-SG",
"Whisper": "en",
},
"South Africa":{
"Google": "en-ZA",
"Whisper": "en",
},
"Tanzania":{
"Google": "en-TZ",
"Whisper": "en",
},
"United Kingdom":{
"Google": "en-GB",
"Whisper": "en",
},
"United States":{
"Google": "en-US",
"Whisper": "en",
},
},
"Estonian":{
"Estonia":{
"Google": "et-EE",
"Whisper": "et",
},
},
"Filipino":{
"Philippines":{
"Google": "fil-PH",
"Whisper": "tl",
},
},
"Finnish":{
"Finland":{
"Google": "fi-FI",
"Whisper": "fi",
},
},
"French":{
"Belgium":{
"Google": "fr-BE",
"Whisper": "fr",
},
"Canada":{
"Google": "fr-CA",
"Whisper": "fr",
},
"France":{
"Google": "fr-FR",
"Whisper": "fr",
},
"Switzerland":{
"Google": "fr-CH",
"Whisper": "fr",
},
},
"Galician":{
"Spain":{
"Google": "gl-ES",
"Whisper": "gl",
},
},
"Georgian":{
"Georgia":{
"Google": "ka-GE",
"Whisper": "ka",
},
},
"German":{
"Austria":{
"Google": "de-AT",
"Whisper": "de",
},
"Germany":{
"Google": "de-DE",
"Whisper": "de",
},
"Switzerland":{
"Google": "de-CH",
"Whisper": "de",
},
},
"Greek":{
"Greece":{
"Google": "el-GR",
"Whisper": "el",
},
},
"Gujarati":{
"India":{
"Google": "gu-IN",
"Whisper": "gu",
},
},
"Hebrew":{
"Israel":{
"Google": "iw-IL",
"Whisper": "he",
},
},
"Hindi": {
"India":{
"Google": "hi-IN",
"Whisper": "hi",
},
},
"Hungarian":{
"Hungary":{
"Google": "hu-HU",
"Whisper": "hu",
},
},
"Icelandic":{
"Iceland":{
"Google": "is-IS",
"Whisper": "is",
},
},
"Indonesian":{
"Indonesia":{
"Google": "id-ID",
"Whisper": "id",
},
},
"Italian":{
"Italy":{
"Google": "it-IT",
"Whisper": "it",
},
"Switzerland":{
"Google": "it-CH",
"Whisper": "it",
},
},
"Japanese":{
"Japan":{
"Google": "ja-JP",
"Whisper": "ja",
},
},
# "Javanese":{
# "Indonesia":{
# "Google": "jv-ID",
# },
# },
"Kannada":{
"India":{
"Google": "kn-IN",
"Whisper": "kn",
},
},
"Kazakh":{
"Kazakhstan":{
"Google": "kk-KZ",
"Whisper": "kk",
},
},
"Khmer":{
"Cambodia":{
"Google": "km-KH",
"Whisper": "km",
},
},
# "Kinyarwanda":{
# "rwanda":{
# "Google": "rw-RW",
# },
# },
"Korean":{
"South Korea":{
"Google": "ko-KR",
"Whisper": "ko",
},
},
"Lao":{
"Laos":{
"Google": "lo-LA",
"Whisper": "lo",
},
},
"Latvian":{
"Latvia":{
"Google": "lv-LV",
"Whisper": "lv",
},
},
"Lithuanian":{
"Lithuania":{
"Google": "lt-LT",
"Whisper": "lt",
},
},
"Macedonian":{
"North Macedonia":{
"Google": "mk-MK",
"Whisper": "mk",
},
},
"Malay":{
"Malaysia":{
"Google": "ms-MY",
"Whisper": "ms",
},
},
"Malayalam":{
"India":{
"Google": "ml-IN",
"Whisper": "ml",
},
},
"Mongolian":{
"Mongolia":{
"Google": "mn-MN",
"Whisper": "mn",
},
},
"Nepali":{
"Nepal":{
"Google": "ne-NP",
"Whisper": "ne",
},
},
"Norwegian":{
"Norway":{
"Google": "no-NO",
"Whisper": "no",
},
},
"Persian":{
"Iran":{
"Google": "fa-IR",
"Whisper": "fa",
},
},
"Polish":{
"Poland":{
"Google": "pl-PL",
"Whisper": "pl",
},
},
"Portuguese":{
"Brazil":{
"Google": "pt-BR",
"Whisper": "pt",
},
"Portugal":{
"Google": "pt-PT",
"Whisper": "pt",
},
},
# "Punjabi":{
# "India":{
# "Google": "pa-Guru-IN",
# },
# },
"Romanian":{
"Romania":{
"Google": "ro-RO",
"Whisper": "ro",
},
},
"Russian":{
"Russia":{
"Google": "ru-RU",
"Whisper": "ru",
},
},
"Serbian":{
"Serbia":{
"Google": "sr-RS",
"Whisper": "sr",
},
},
"Sinhala":{
"Sri Lanka":{
"Google": "si-LK",
"Whisper": "si",
},
},
"Slovak":{
"Slovakia":{
"Google": "sk-SK",
"Whisper": "sk",
},
},
"Slovenian":{
"Slovenia":{
"Google": "sl-SI",
"Whisper": "sl",
},
},
# "Sesotho":{
# "South Africa":{
# "Google": "st-ZA",
# },
# },
"Spanish":{
"Argentina":{
"Google": "es-AR",
"Whisper": "es",
},
"Bolivia":{
"Google": "es-BO",
"Whisper": "es",
},
"Chile":{
"Google": "es-CL",
"Whisper": "es",
},
"Colombia":{
"Google": "es-CO",
"Whisper": "es",
},
"Costa Rica":{
"Google": "es-CR",
"Whisper": "es",
},
"Dominican Republic":{
"Google": "es-DO",
"Whisper": "es",
},
"Ecuador":{
"Google": "es-EC",
"Whisper": "es",
},
"El Salvador":{
"Google": "es-SV",
"Whisper": "es",
},
"Guatemala":{
"Google": "es-GT",
"Whisper": "es",
},
"Honduras":{
"Google": "es-HN",
"Whisper": "es",
},
"Mexico":{
"Google": "es-MX",
"Whisper": "es",
},
"Nicaragua":{
"Google": "es-NI",
"Whisper": "es",
},
"Panama":{
"Google": "es-PA",
"Whisper": "es",
},
"Paraguay":{
"Google": "es-PY",
"Whisper": "es",
},
"Peru":{
"Google": "es-PE",
"Whisper": "es",
},
"Puerto Rico":{
"Google": "es-PR",
"Whisper": "es",
},
"Spain":{
"Google": "es-ES",
"Whisper": "es",
},
"United States":{
"Google": "es-US",
"Whisper": "es",
},
"Uruguay":{
"Google": "es-UY",
"Whisper": "es",
},
"Venezuela":{
"Google": "es-VE",
"Whisper": "es",
},
},
"Sundanese":{
"Indonesia":{
"Google": "su-ID",
"Whisper": "su",
},
},
"Swahili":{
"Kenya":{
"Google": "sw-KE",
"Whisper": "sw",
},
"Tanzania":{
"Google": "sw-TZ",
"Whisper": "sw",
},
},
# "Swazi":{
# "Eswatini":{
# "Google": "ss-Latn-ZA",
# },
# },
"Swedish":{
"Sweden":{
"Google": "sv-SE",
"Whisper": "sv",
},
},
"Tamil":{
"India":{
"Google": "ta-IN",
"Whisper": "ta",
},
"malaysia":{
"Google": "ta-MY",
"Whisper": "ta",
},
"Singapore":{
"Google": "ta-SG",
"Whisper": "ta",
},
"Sri Lanka":{
"Google": "ta-LK",
"Whisper": "ta",
},
},
"Telugu":{
"India":{
"Google": "te-IN",
"Whisper": "te",
},
},
"Thai":{
"Thailand":{
"Google": "th-TH",
"Whisper": "th",
},
},
# "Tsonga":{
# "South Africa":{
# "Google": "ts-ZA",
# },
# },
# "Setswana":{
# "South Africa":{
# "Google": "tn-Latn-ZA",
# },
# },
"Turkish":{
"Turkey":{
"Google": "tr-TR",
"Whisper": "tr",
},
},
"Ukrainian":{
"Ukraine":{
"Google": "uk-UA",
"Whisper": "uk",
},
},
"Urdu":{
"India":{
"Google": "ur-IN",
"Whisper": "ur",
},
"Pakistan":{
"Google": "ur-PK",
"Whisper": "ur",
},
},
"Uzbek":{
"Uzbekistan":{
"Google": "uz-UZ",
"Whisper": "uz",
},
},
# "Venda":{
# "South Africa":{
# "Google": "ve-ZA",
# },
# },
"Vietnamese":{
"Vietnam":{
"Google": "vi-VN",
"Whisper": "vi",
},
},
# "Xhosa":{
# "South Africa":{
# "Google": "xh-ZA",
# },
# },
# "Zulu":{
# "South Africa":{
# "Google": "zu-ZA",
# },
# },
}

View File

@@ -0,0 +1,160 @@
from speech_recognition import Recognizer, Microphone
from pyaudiowpatch import get_sample_size, paInt16
from datetime import datetime
from queue import Queue
class BaseRecorder:
def __init__(self, source, energy_threshold, dynamic_energy_threshold, record_timeout):
self.recorder = Recognizer()
self.recorder.energy_threshold = energy_threshold
self.recorder.dynamic_energy_threshold = dynamic_energy_threshold
self.record_timeout = record_timeout
self.stop = None
if source is None:
raise ValueError("audio source can't be None")
self.source = source
def adjustForNoise(self):
with self.source:
self.recorder.adjust_for_ambient_noise(self.source)
def recordIntoQueue(self, audio_queue):
def record_callback(_, audio):
audio_queue.put((audio.get_raw_data(), datetime.now()))
self.stop, self.pause, self.resume = self.recorder.listen_in_background(self.source, record_callback, phrase_time_limit=self.record_timeout)
class SelectedMicRecorder(BaseRecorder):
def __init__(self, device, energy_threshold, dynamic_energy_threshold, record_timeout):
source=Microphone(
device_index=device['index'],
sample_rate=int(device["defaultSampleRate"]),
)
super().__init__(source=source, energy_threshold=energy_threshold, dynamic_energy_threshold=dynamic_energy_threshold, record_timeout=record_timeout)
# self.adjustForNoise()
class SelectedSpeakerRecorder(BaseRecorder):
def __init__(self, device, energy_threshold, dynamic_energy_threshold, record_timeout):
source = Microphone(speaker=True,
device_index= device["index"],
sample_rate=int(device["defaultSampleRate"]),
chunk_size=get_sample_size(paInt16),
channels=device["maxInputChannels"]
)
super().__init__(source=source, energy_threshold=energy_threshold, dynamic_energy_threshold=dynamic_energy_threshold, record_timeout=record_timeout)
# self.adjustForNoise()
class BaseEnergyRecorder:
def __init__(self, source):
self.recorder = Recognizer()
self.recorder.energy_threshold = 0
self.recorder.dynamic_energy_threshold = False
self.record_timeout = 0
self.stop = None
if source is None:
raise ValueError("audio source can't be None")
self.source = source
def adjustForNoise(self):
with self.source:
self.recorder.adjust_for_ambient_noise(self.source)
def recordIntoQueue(self, energy_queue):
def recordCallback(_, energy):
energy_queue.put(energy)
self.stop, self.pause, self.resume = self.recorder.listen_energy_in_background(self.source, recordCallback)
class SelectedMicEnergyRecorder(BaseEnergyRecorder):
def __init__(self, device):
source=Microphone(
device_index=device['index'],
sample_rate=int(device["defaultSampleRate"]),
)
super().__init__(source=source)
# self.adjustForNoise()
class SelectedSpeakerEnergyRecorder(BaseEnergyRecorder):
def __init__(self, device):
source = Microphone(speaker=True,
device_index= device["index"],
sample_rate=int(device["defaultSampleRate"]),
channels=device["maxInputChannels"]
)
super().__init__(source=source)
# self.adjustForNoise()
class BaseEnergyAndAudioRecorder:
def __init__(self, source, energy_threshold, dynamic_energy_threshold, phrase_time_limit, phrase_timeout, record_timeout):
self.recorder = Recognizer()
self.recorder.energy_threshold = energy_threshold
self.recorder.dynamic_energy_threshold = dynamic_energy_threshold
self.phrase_time_limit = phrase_time_limit
self.phrase_timeout = phrase_timeout
self.record_timeout = record_timeout
self.stop = None
if source is None:
raise ValueError("audio source can't be None")
self.source = source
def adjustForNoise(self):
with self.source:
self.recorder.adjust_for_ambient_noise(self.source)
def recordIntoQueue(self, audio_queue, energy_queue=None):
def audioRecordCallback(_, audio):
audio_queue.put((audio.get_raw_data(), datetime.now()))
def energyRecordCallback(energy):
energy_queue.put(energy)
self.stop, self.pause, self.resume = self.recorder.listen_energy_and_audio_in_background(
source=self.source,
callback=audioRecordCallback,
phrase_time_limit=self.phrase_time_limit,
callback_energy=energyRecordCallback if energy_queue is not None else None,
phrase_timeout=self.phrase_timeout,
record_timeout=self.record_timeout)
class SelectedMicEnergyAndAudioRecorder(BaseEnergyAndAudioRecorder):
def __init__(self, device, energy_threshold, dynamic_energy_threshold, phrase_time_limit, phrase_timeout:int=1, record_timeout:int=5):
source=Microphone(
device_index=device['index'],
sample_rate=int(device["defaultSampleRate"]),
)
super().__init__(
source=source,
energy_threshold=energy_threshold,
dynamic_energy_threshold=dynamic_energy_threshold,
phrase_time_limit=phrase_time_limit,
phrase_timeout=phrase_timeout,
record_timeout=record_timeout,
)
# self.adjustForNoise()
class SelectedSpeakerEnergyAndAudioRecorder(BaseEnergyAndAudioRecorder):
def __init__(self, device, energy_threshold, dynamic_energy_threshold, phrase_time_limit, phrase_timeout:int=1, record_timeout:int=5):
source = Microphone(speaker=True,
device_index= device["index"],
sample_rate=int(device["defaultSampleRate"]),
chunk_size=get_sample_size(paInt16),
channels=device["maxInputChannels"]
)
super().__init__(
source=source,
energy_threshold=energy_threshold,
dynamic_energy_threshold=dynamic_energy_threshold,
phrase_time_limit=phrase_time_limit,
phrase_timeout=phrase_timeout,
record_timeout=record_timeout,
)
# self.adjustForNoise()

View File

@@ -0,0 +1,165 @@
import time
from io import BytesIO
from threading import Event
import wave
from speech_recognition import Recognizer, AudioData, AudioFile
from speech_recognition.exceptions import UnknownValueError
from datetime import timedelta
from pyaudiowpatch import get_sample_size, paInt16
from .transcription_languages import transcription_lang
from .transcription_whisper import getWhisperModel, checkWhisperWeight
import torch
import numpy as np
from pydub import AudioSegment
from utils import errorLogging
import warnings
warnings.simplefilter('ignore', RuntimeWarning)
PHRASE_TIMEOUT = 3
MAX_PHRASES = 10
class AudioTranscriber:
def __init__(self, speaker, source, phrase_timeout, max_phrases, transcription_engine, root=None, whisper_weight_type=None, device="cpu", device_index=0):
self.speaker = speaker
self.phrase_timeout = phrase_timeout
self.max_phrases = max_phrases
self.transcript_data = []
self.transcript_changed_event = Event()
self.audio_recognizer = Recognizer()
self.transcription_engine = "Google"
self.whisper_model = None
self.audio_sources = {
"sample_rate": source.SAMPLE_RATE,
"sample_width": source.SAMPLE_WIDTH,
"channels": source.channels,
"last_sample": bytes(),
"last_spoken": None,
"new_phrase": True,
"process_data_func": self.processSpeakerData if speaker else self.processSpeakerData
}
if transcription_engine == "Whisper" and checkWhisperWeight(root, whisper_weight_type) is True:
self.whisper_model = getWhisperModel(root, whisper_weight_type, device=device, device_index=device_index)
self.transcription_engine = "Whisper"
def transcribeAudioQueue(self, audio_queue, languages, countries, avg_logprob=-0.8, no_speech_prob=0.6):
if audio_queue.empty():
time.sleep(0.01)
return False
audio, time_spoken = audio_queue.get()
self.updateLastSampleAndPhraseStatus(audio, time_spoken)
confidences = [{"confidence": 0, "text": "", "language": None}]
try:
audio_data = self.audio_sources["process_data_func"]()
match self.transcription_engine:
case "Google":
for language, country in zip(languages, countries):
try:
text, confidence = self.audio_recognizer.recognize_google(
audio_data,
language=transcription_lang[language][country][self.transcription_engine],
with_confidence=True
)
confidences.append({"confidence": confidence, "text": text, "language": language})
except Exception:
pass
case "Whisper":
audio_data = np.frombuffer(audio_data.get_raw_data(convert_rate=16000, convert_width=2), np.int16).flatten().astype(np.float32) / 32768.0
if isinstance(audio_data, torch.Tensor):
audio_data = audio_data.detach().numpy()
for language, country in zip(languages, countries):
text = ""
source_language = transcription_lang[language][country][self.transcription_engine] if len(languages) == 1 else None
segments, info = self.whisper_model.transcribe(
audio_data,
beam_size=5,
temperature=0.0,
log_prob_threshold=-0.8,
no_speech_threshold=0.6,
language=source_language,
word_timestamps=False,
without_timestamps=True,
task="transcribe",
vad_filter=False,
)
for s in segments:
if s.avg_logprob < avg_logprob or s.no_speech_prob > no_speech_prob:
continue
text += s.text
confidences.append({"confidence": info.language_probability, "text": text, "language": language})
if (len(languages) == 1) or (transcription_lang[language][country][self.transcription_engine] == info.language):
break
except UnknownValueError:
pass
except Exception:
errorLogging()
finally:
pass
result = max(confidences, key=lambda x: x["confidence"])
if result["text"] != "":
self.updateTranscript(result)
return True
def updateLastSampleAndPhraseStatus(self, data, time_spoken):
source_info = self.audio_sources
if source_info["last_spoken"] and time_spoken - source_info["last_spoken"] > timedelta(seconds=self.phrase_timeout):
source_info["last_sample"] = bytes()
source_info["new_phrase"] = True
else:
source_info["new_phrase"] = False
source_info["last_sample"] += data
source_info["last_spoken"] = time_spoken
def processMicData(self):
audio_data = AudioData(self.audio_sources["last_sample"], self.audio_sources["sample_rate"], self.audio_sources["sample_width"])
return audio_data
def processSpeakerData(self):
temp_file = BytesIO()
with wave.open(temp_file, 'wb') as wf:
wf.setnchannels(self.audio_sources["channels"])
wf.setsampwidth(get_sample_size(paInt16))
wf.setframerate(self.audio_sources["sample_rate"])
wf.writeframes(self.audio_sources["last_sample"])
temp_file.seek(0)
if self.audio_sources["channels"] > 2:
audio = AudioSegment.from_file(temp_file, format="wav")
mono_audio = audio.set_channels(1)
temp_file = BytesIO()
mono_audio.export(temp_file, format="wav")
temp_file.seek(0)
with AudioFile(temp_file) as source:
audio = self.audio_recognizer.record(source)
return audio
def updateTranscript(self, result):
source_info = self.audio_sources
transcript = self.transcript_data
if source_info["new_phrase"] or len(transcript) == 0:
if len(transcript) > self.max_phrases:
transcript.pop(-1)
transcript.insert(0, result)
else:
transcript[0] = result
def getTranscript(self):
if len(self.transcript_data) > 0:
result = self.transcript_data.pop(-1)
else:
result = {"confidence": 0, "text": "", "language": None}
return result
def clearTranscriptData(self):
self.transcript_data.clear()
self.audio_sources["last_sample"] = bytes()
self.audio_sources["new_phrase"] = True

View File

@@ -0,0 +1,102 @@
from os import path as os_path, makedirs as os_makedirs
from requests import get as requests_get
from typing import Callable
import huggingface_hub
from faster_whisper import WhisperModel
import logging
logger = logging.getLogger('faster_whisper')
logger.setLevel(logging.CRITICAL)
_MODELS = {
"tiny": "Systran/faster-whisper-tiny",
"base": "Systran/faster-whisper-base",
"small": "Systran/faster-whisper-small",
"medium": "Systran/faster-whisper-medium",
"large-v1": "Systran/faster-whisper-large-v1",
"large-v2": "Systran/faster-whisper-large-v2",
"large-v3": "Systran/faster-whisper-large-v3",
}
_FILENAMES = [
"config.json",
"preprocessor_config.json",
"model.bin",
"tokenizer.json",
"vocabulary.txt",
"vocabulary.json",
]
def downloadFile(url, path, func=None):
try:
res = requests_get(url, stream=True)
res.raise_for_status()
file_size = int(res.headers.get('content-length', 0))
total_chunk = 0
with open(os_path.join(path), 'wb') as file:
for chunk in res.iter_content(chunk_size=1024*2000):
file.write(chunk)
if isinstance(func, Callable):
total_chunk += len(chunk)
func(total_chunk/file_size)
except Exception:
pass
def checkWhisperWeight(root, weight_type):
path = os_path.join(root, "weights", "whisper", weight_type)
result = False
try:
WhisperModel(
path,
device="cpu",
device_index=0,
compute_type="int8",
cpu_threads=4,
num_workers=1,
local_files_only=True,
)
result = True
except Exception:
pass
return result
def downloadWhisperWeight(root, weight_type, callback=None, end_callback=None):
path = os_path.join(root, "weights", "whisper", weight_type)
os_makedirs(path, exist_ok=True)
if checkWhisperWeight(root, weight_type) is False:
for filename in _FILENAMES:
file_path = os_path.join(path, filename)
url = huggingface_hub.hf_hub_url(_MODELS[weight_type], filename)
downloadFile(url, file_path, func=callback if filename == "model.bin" else None)
if isinstance(end_callback, Callable):
end_callback()
def getWhisperModel(root, weight_type, device="cpu", device_index=0):
path = os_path.join(root, "weights", "whisper", weight_type)
compute_type = "int8" if device == "cpu" else "float16"
return WhisperModel(
path,
device=device,
device_index=device_index,
compute_type=compute_type,
cpu_threads=4,
num_workers=1,
local_files_only=True,
)
if __name__ == "__main__":
def callback(value):
print(value)
pass
def end_callback():
print("end")
pass
downloadWhisperWeight("./", "tiny", callback, end_callback)
downloadWhisperWeight("./", "base", callback, end_callback)
downloadWhisperWeight("./", "small", callback, end_callback)
downloadWhisperWeight("./", "medium", callback, end_callback)
downloadWhisperWeight("./", "large-v1", callback, end_callback)
downloadWhisperWeight("./", "large-v2", callback, end_callback)
downloadWhisperWeight("./", "large-v3", callback, end_callback)

View File

@@ -0,0 +1,384 @@
translation_lang = {}
dict_deepl_languages = {
"Arabic":"ar",
"Bulgarian":"bg",
"Czech":"cs",
"Danish":"da",
"German":"de",
"Greek":"el",
"English":"en",
"Spanish":"es",
"Estonian":"et",
"Finnish":"fi",
"French":"fr",
"Irish":"ga",
"Croatian":"hr",
"Hungarian":"hu",
"Indonesian":"id",
"Icelandic":"is",
"Italian":"it",
"Japanese":"ja",
"Korean":"ko",
"Lithuanian":"lt",
"Latvian":"lv",
"Maltese":"mt",
"Bokmal":"nb",
"Dutch":"nl",
"Norwegian":"no",
"Polish":"pl",
"Portuguese":"pt",
"Romanian":"ro",
"Russian":"ru",
"Slovak":"sk",
"Slovenian":"sl",
"Swedish":"sv",
"Turkish":"tr",
"Ukrainian":"uk",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh"
}
translation_lang["DeepL"] = {
"source":dict_deepl_languages,
"target":dict_deepl_languages,
}
dict_deepl_api_source_languages = {
"Japanese":"ja",
"English":"en",
"Bulgarian":"bg",
"Czech":"cs",
"Danish":"da",
"German":"de",
"Greek":"el",
"Spanish":"es",
"Estonian":"et",
"Finnish":"fi",
"French":"fr",
"Hungarian":"hu",
"Indonesian":"id",
"Italian":"it",
"Korean":"ko",
"Lithuanian":"lt",
"Latvian":"lv",
"Norwegian":"nb",
"Dutch":"nl",
"Polish":"pl",
"Portuguese":"pt",
"Romanian":"ro",
"Russian":"ru",
"Slovak":"sk",
"Slovenian":"sl",
"Swedish":"sv",
"Turkish":"tr",
"Ukrainian":"uk",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh"
}
dict_deepl_api_target_languages = {
"Japanese":"ja",
"English American":"en-US",
"English British":"en-GB",
"Bulgarian":"bg",
"Czech":"cs",
"Danish":"da",
"German":"de",
"Greek":"el",
"English":"en",
"Spanish":"es",
"Estonian":"et",
"Finnish":"fi",
"French":"fr",
"Hungarian":"hu",
"Indonesian":"id",
"Italian":"it",
"Korean":"ko",
"Lithuanian":"lt",
"Latvian":"lv",
"Norwegian":"nb",
"Dutch":"nl",
"Polish":"pl",
"Portuguese Brazilian":"pt-BR",
"Portuguese European":"pt-PT",
"Romanian":"ro",
"Russian":"ru",
"Slovak":"sk",
"Slovenian":"sl",
"Swedish":"sv",
"Turkish":"tr",
"Ukrainian":"uk",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh"
}
translation_lang["DeepL_API"] = {
"source": dict_deepl_api_source_languages,
"target": dict_deepl_api_target_languages,
}
dict_google_languages = {
"Japanese":"ja",
"English":"en",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh-TW",
"Arabic":"ar",
"Russian":"ru",
"French":"fr",
"German":"de",
"Spanish":"es",
"Portuguese":"pt",
"Italian":"it",
"Korean":"ko",
"Greek":"el",
"Dutch":"nl",
"Hindi":"hi",
"Turkish":"tr",
"Malay":"ms",
"Thai":"th",
"Vietnamese":"vi",
"Indonesian":"id",
"Hebrew":"he",
"Polish":"pl",
"Mongolian":"mn",
"Czech":"cs",
"Hungarian":"hu",
"Estonian":"et",
"Bulgarian":"bg",
"Danish":"da",
"Finnish":"fi",
"Romanian":"ro",
"Swedish":"sv",
"Slovenian":"sl",
"Persian/Farsi":"fa",
"Bosnian":"bs",
"Serbian":"sr",
"Filipino":"tl",
"Haitiancreole":"ht",
"Catalan":"ca",
"Croatian":"hr",
"Latvian":"lv",
"Lithuanian":"lt",
"Urdu":"ur",
"Ukrainian":"uk",
"Welsh":"cy",
"Swahili":"sw",
"Samoan":"sm",
"Slovak":"sk",
"Afrikaans":"af",
"Norwegian":"no",
"Bengali":"bn",
"Malagasy":"mg",
"Maltese":"mt",
"Gujarati":"gu",
"Tamil":"ta",
"Telugu":"te",
"Punjabi":"pa",
"Amharic":"am",
"Azerbaijani":"az",
"Belarusian":"be",
"Cebuano":"ceb",
"Esperanto":"eo",
"Basque":"eu",
"Irish":"ga"
}
translation_lang["Google"] = {
"source":dict_google_languages,
"target":dict_google_languages,
}
dict_bing_languages = {
"Japanese":"ja",
"English":"en",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh-Hant",
"Arabic":"ar",
"Russian":"ru",
"French":"fr",
"German":"de",
"Spanish":"es",
"Portuguese":"pt",
"Italian":"it",
"Korean":"ko",
"Greek":"el",
"Dutch":"nl",
"Hindi":"hi",
"Turkish":"tr",
"Malay":"ms",
"Thai":"th",
"Vietnamese":"vi",
"Indonesian":"id",
"Hebrew":"he",
"Polish":"pl",
"Czech":"cs",
"Hungarian":"hu",
"Estonian":"et",
"Bulgarian":"bg",
"Danish":"da",
"Finnish":"fi",
"Romanian":"ro",
"Swedish":"sv",
"Slovenian":"sl",
"Persian/Farsi":"fa",
"Bosnian":"bs",
"Serbian":"sr",
"Fijian":"fj",
"Filipino":"tl",
"Haitiancreole":"ht",
"Catalan":"ca",
"Croatian":"hr",
"Latvian":"lv",
"Lithuanian":"lt",
"Urdu":"ur",
"Ukrainian":"uk",
"Welsh":"cy",
"Tahiti":"ty",
"Tongan":"to",
"Swahili":"sw",
"Samoan":"sm",
"Slovak":"sk",
"Afrikaans":"af",
"Norwegian":"no",
"Bengali":"bn",
"Malagasy":"mg",
"Maltese":"mt",
"Queretaro otomi":"otq",
"Klingon/tlhingan Hol":"tlh",
"Gujarati":"gu",
"Tamil":"ta",
"Telugu":"te",
"Punjabi":"pa",
"Irish":"ga"
}
translation_lang["Bing"] = {
"source":dict_bing_languages,
"target":dict_bing_languages,
}
dict_papago_languages = {
"German": "de",
"English": "en",
"Spanish":"es",
"French": "fr",
"Hindi": "hi",
"Indonesian": "id",
"Italian": "it",
"Japanese": "ja",
"Korean": "ko",
"Portuguese": "pt",
"Russian": "ru",
"Thai": "th",
"Vietnamese": "vi",
"Chinese Simplified":"zh-CN",
"Chinese Traditional":"zh-TW",
}
translation_lang["Papago"] = {
"source":dict_papago_languages,
"target":dict_papago_languages,
}
dict_ctranslate2_languages = {
"English": "en",
"Chinese Simplified": "zh",
"Chinese Traditional":"zh",
"German": "de",
"Spanish": "es",
"Russian": "ru",
"Korean": "ko",
"French": "fr",
"Japanese": "ja",
"Portuguese": "pt",
"Turkish": "tr",
"Polish": "pl",
"Catalan": "ca",
"Dutch": "nl",
"Arabic": "ar",
"Swedish": "sv",
"Italian": "it",
"Indonesian": "id",
"Hindi": "hi",
"Finnish": "fi",
"Vietnamese": "vi",
"Hebrew": "he",
"Ukrainian": "uk",
"Greek": "el",
"Malay": "ms",
"Czech": "cs",
"Romanian": "ro",
"Danish": "da",
"Hungarian": "hu",
"Tamil": "ta",
"Norwegian": "no",
"Thai": "th",
"Urdu": "ur",
"Croatian": "hr",
"Bulgarian": "bg",
"Lithuanian": "lt",
"Latin": "la",
"Maori": "mi",
"Malayalam": "ml",
"Welsh": "cy",
"Slovak": "sk",
"Telugu": "te",
"Persian": "fa",
"Latvian": "lv",
"Bengali": "bn",
"Serbian": "sr",
"Azerbaijani": "az",
"Slovenian": "sl",
"Kannada": "kn",
"Estonian": "et",
"Macedonian": "mk",
"Breton": "br",
"Basque": "eu",
"Icelandic": "is",
"Armenian": "hy",
"Nepali": "ne",
"Mongolian": "mn",
"Bosnian": "bs",
"Kazakh": "kk",
"Albanian": "sq",
"Swahili": "sw",
"Galician": "gl",
"Marathi": "mr",
"Punjabi": "pa",
"Sinhala": "si",
"Khmer": "km",
"Shona": "sn",
"Yoruba": "yo",
"Somali": "so",
"Afrikaans": "af",
"Occitan": "oc",
"Georgian": "ka",
"Belarusian": "be",
"Tajik": "tg",
"Sindhi": "sd",
"Gujarati": "gu",
"Amharic": "am",
"Yiddish": "yi",
"Lao": "lo",
"Uzbek": "uz",
"Faroese": "fo",
"Haitian creole": "ht",
"Pashto": "ps",
"Turkmen": "tk",
"Nynorsk": "nn",
"Maltese": "mt",
"Sanskrit": "sa",
"Luxembourgish": "lb",
"Myanmar": "my",
"Tibetan": "bo",
"Filipino": "tl",
"Malagasy": "mg",
"Assamese": "as",
"Tatar": "tt",
"Hawaiian": "haw",
"Lingala": "ln",
"Hausa": "ha",
"Bashkir": "ba",
"Javanese": "jw",
"Sundanese": "su"
}
translation_lang["CTranslate2"] = {
"source":dict_ctranslate2_languages,
"target":dict_ctranslate2_languages,
}

View File

@@ -0,0 +1,145 @@
from os import path as os_path
from deepl import Translator as deepl_Translator
from translators import translate_text as other_web_Translator
from .translation_languages import translation_lang
from .translation_utils import ctranslate2_weights
import ctranslate2
import transformers
from utils import errorLogging
import warnings
warnings.filterwarnings("ignore")
# Translator
class Translator():
def __init__(self):
self.deepl_client = None
self.ctranslate2_translator = None
self.ctranslate2_tokenizer = None
self.is_loaded_ctranslate2_model = False
def authenticationDeepLAuthKey(self, authkey):
result = True
try:
self.deepl_client = deepl_Translator(authkey)
self.deepl_client.translate_text(" ", target_lang="EN-US")
except Exception:
errorLogging()
self.deepl_client = None
result = False
return result
def changeCTranslate2Model(self, path, model_type, device="cpu", device_index=0):
self.is_loaded_ctranslate2_model = False
directory_name = ctranslate2_weights[model_type]["directory_name"]
tokenizer = ctranslate2_weights[model_type]["tokenizer"]
weight_path = os_path.join(path, "weights", "ctranslate2", directory_name)
tokenizer_path = os_path.join(path, "weights", "ctranslate2", directory_name, "tokenizer")
compute_type = "int8" if device == "cpu" else "float16"
self.ctranslate2_translator = ctranslate2.Translator(
weight_path,
device=device,
device_index=device_index,
compute_type=compute_type,
inter_threads=1,
intra_threads=4
)
try:
self.ctranslate2_tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path)
except Exception:
errorLogging()
tokenizer_path = os_path.join("./weights", "ctranslate2", directory_name, "tokenizer")
self.ctranslate2_tokenizer = transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path)
self.is_loaded_ctranslate2_model = True
def isLoadedCTranslate2Model(self):
return self.is_loaded_ctranslate2_model
def translateCTranslate2(self, message, source_language, target_language):
result = False
if self.is_loaded_ctranslate2_model is True:
try:
self.ctranslate2_tokenizer.src_lang = source_language
source = self.ctranslate2_tokenizer.convert_ids_to_tokens(self.ctranslate2_tokenizer.encode(message))
target_prefix = [self.ctranslate2_tokenizer.lang_code_to_token[target_language]]
results = self.ctranslate2_translator.translate_batch([source], target_prefix=[target_prefix])
target = results[0].hypotheses[0][1:]
result = self.ctranslate2_tokenizer.decode(self.ctranslate2_tokenizer.convert_tokens_to_ids(target))
except Exception:
errorLogging()
return result
@staticmethod
def getLanguageCode(translator_name, target_country, source_language, target_language):
match translator_name:
case "DeepL_API":
if target_language == "English":
if target_country in ["United States", "Canada", "Philippines"]:
target_language = "English American"
else:
target_language = "English British"
elif target_language == "Portuguese":
if target_country in ["Portugal"]:
target_language = "Portuguese European"
else:
target_language = "Portuguese Brazilian"
case _:
pass
source_language=translation_lang[translator_name]["source"][source_language]
target_language=translation_lang[translator_name]["target"][target_language]
return source_language, target_language
def translate(self, translator_name, source_language, target_language, target_country, message):
try:
result = ""
source_language, target_language = self.getLanguageCode(translator_name, target_country, source_language, target_language)
match translator_name:
case "DeepL":
result = other_web_Translator(
query_text=message,
translator="deepl",
from_language=source_language,
to_language=target_language,
)
case "DeepL_API":
if self.deepl_client is None:
result = False
else:
result = self.deepl_client.translate_text(
message,
source_lang=source_language,
target_lang=target_language,
).text
case "Google":
result = other_web_Translator(
query_text=message,
translator="google",
from_language=source_language,
to_language=target_language,
)
case "Bing":
result = other_web_Translator(
query_text=message,
translator="bing",
from_language=source_language,
to_language=target_language,
)
case "Papago":
result = other_web_Translator(
query_text=message,
translator="papago",
from_language=source_language,
to_language=target_language,
)
case "CTranslate2":
result = self.translateCTranslate2(
message=message,
source_language=source_language,
target_language=target_language,
)
except Exception:
errorLogging()
result = False
return result

View File

@@ -0,0 +1,89 @@
import tempfile
from zipfile import ZipFile
from os import path as os_path
from os import makedirs as os_makedirs
from requests import get as requests_get
from typing import Callable
import hashlib
from utils import errorLogging
ctranslate2_weights = {
"small": { # M2M-100 418M-parameter model
"url": "https://github.com/misyaguziya/VRCT-weights/releases/download/v1.0/m2m100_418m.zip",
"directory_name": "m2m100_418m",
"tokenizer": "facebook/m2m100_418M",
"hash": {
"model.bin": "e7c26a9abb5260abd0268fbe3040714070dec254a990b4d7fd3f74c5230e3acb",
"sentencepiece.model": "d8f7c76ed2a5e0822be39f0a4f95a55eb19c78f4593ce609e2edbc2aea4d380a",
"shared_vocabulary.txt": "bd440aa21b8ca3453fc792a0018a1f3fe68b3464aadddd4d16a4b72f73c86d8c",
}
},
"large": { # M2M-100 1.2B-parameter model
"url": "https://github.com/misyaguziya/VRCT-weights/releases/download/v1.0/m2m100_12b.zip",
"directory_name": "m2m100_12b",
"tokenizer": "facebook/m2m100_1.2b",
"hash": {
"model.bin": "abb7bf4ba7e5e016b6e3ed480c752459b2f783ac8fca372e7587675e5bf3a919",
"sentencepiece.model": "d8f7c76ed2a5e0822be39f0a4f95a55eb19c78f4593ce609e2edbc2aea4d380a",
"shared_vocabulary.txt": "bd440aa21b8ca3453fc792a0018a1f3fe68b3464aadddd4d16a4b72f73c86d8c",
}
},
}
def calculate_file_hash(file_path, block_size=65536):
hash_object = hashlib.sha256()
with open(file_path, 'rb') as file:
for block in iter(lambda: file.read(block_size), b''):
hash_object.update(block)
return hash_object.hexdigest()
def checkCTranslate2Weight(root, weight_type="small"):
weight_directory_name = ctranslate2_weights[weight_type]["directory_name"]
hash_data = ctranslate2_weights[weight_type]["hash"]
files = [
"model.bin",
"sentencepiece.model",
"shared_vocabulary.txt"
]
path = os_path.join(root, "weights", "ctranslate2")
# check already downloaded
already_downloaded = False
if all(os_path.exists(os_path.join(path, weight_directory_name, file)) for file in files):
# check hash
for file in files:
original_hash = hash_data[file]
current_hash = calculate_file_hash(os_path.join(path, weight_directory_name, file))
if original_hash != current_hash:
break
already_downloaded = True
return already_downloaded
def downloadCTranslate2Weight(root, weight_type="small", callback=None, end_callback=None):
url = ctranslate2_weights[weight_type]["url"]
filename = "weight.zip"
path = os_path.join(root, "weights", "ctranslate2")
os_makedirs(path, exist_ok=True)
if checkCTranslate2Weight(root, weight_type) is False:
try:
with tempfile.TemporaryDirectory() as tmp_path:
res = requests_get(url, stream=True)
file_size = int(res.headers.get('content-length', 0))
total_chunk = 0
with open(os_path.join(tmp_path, filename), 'wb') as file:
for chunk in res.iter_content(chunk_size=1024*2000):
file.write(chunk)
if isinstance(callback, Callable):
total_chunk += len(chunk)
callback(total_chunk/file_size)
with ZipFile(os_path.join(tmp_path, filename)) as zf:
zf.extractall(path)
except Exception:
errorLogging()
if isinstance(end_callback, Callable):
end_callback()

View File

@@ -0,0 +1,20 @@
from typing import Callable
import time
class Watchdog:
def __init__(self, timeout:int=60, interval:int=20):
self.timeout = timeout
self.interval = interval
self.last_feed_time = time.time()
def feed(self):
self.last_feed_time = time.time()
def setCallback(self, callback):
self.callback = callback
def start(self):
if time.time() - self.last_feed_time > self.timeout:
if isinstance(self.callback, Callable):
self.callback()
time.sleep(self.interval)

131
src-python/utils.py Normal file
View File

@@ -0,0 +1,131 @@
import base64
from typing import Any
import json
import random
from typing import Union
from os import path as os_path, rename as os_rename
import traceback
import logging
from PIL.Image import open as Image_open
def getImageFile(file_name):
img = Image_open(os_path.join(os_path.dirname(__file__), "img", file_name))
return img
def callFunctionIfCallable(function, *args):
if callable(function) is True:
function(*args)
def isEven(number):
return number % 2 == 0
def makeEven(number, minus:bool=False):
if minus is True:
return number if isEven(number) else number - 1
return number if isEven(number) else number + 1
def intToPctStr(value:int):
return f"{value}%"
def floatToPctStr(value:float):
return f"{int(value*100)}%"
def strPctToInt(value:str):
return int(value.replace("%", ""))
def isUniqueStrings(unique_strings:Union[str, list], input_string:str, require=False):
import re
if isinstance(unique_strings, str):
unique_strings = [unique_strings]
patterns = [re.escape(s) for s in unique_strings]
counts = [len(re.findall(pattern, input_string)) for pattern in patterns]
if require is True:
# If require is True, unique_strings must appear once
return all(count == 1 for count in counts) and counts.count(1) == 2
else:
# If require is False, check if unique strings are used exactly once
return all(count == 1 for count in counts)
# path先のweightフォルダがある場合にはそのフォルダ名をweightsに変更する
def renameWeightFolder(path):
weight_path = os_path.join(path, "weight")
if os_path.exists(weight_path):
os_rename(weight_path, os_path.join(path, "weights"))
def splitList(lst:list, split_count:int, to_shuffle:bool=False):
if to_shuffle is True:
random.shuffle(lst)
split_lists = []
for i in range(0, len(lst), split_count):
sub_list = lst[i:i+split_count]
split_lists.append(sub_list)
return split_lists
def encodeBase64(data:str) -> dict:
return json.loads(base64.b64decode(data).decode('utf-8'))
def removeLog():
with open('process.log', 'w', encoding="utf-8") as f:
f.write("")
def setupLogger(name, log_file, level=logging.INFO):
"""
特定の名前とログファイルを持つロガーを設定します。
"""
# ロガーを作成
logger = logging.getLogger(name)
logger.setLevel(level)
logger.propagate = False # 親ロガーへの伝播を防ぐ
# ハンドラーを作成
file_handler = logging.FileHandler(log_file, encoding="utf-8", delay=True)
file_handler.setLevel(level)
# フォーマッターを設定
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
# ロガーにハンドラーを追加
logger.addHandler(file_handler)
return logger
process_logger = None
def printLog(log:str, data:Any=None) -> None:
global process_logger
if process_logger is None:
process_logger = setupLogger("process", "process.log", logging.INFO)
response = {
"status": 348,
"log": log,
"data": str(data),
}
process_logger.info(response)
response = json.dumps(response)
print(response, flush=True)
def printResponse(status:int, endpoint:str, result:Any=None) -> None:
global process_logger
if process_logger is None:
process_logger = setupLogger("process", "process.log", logging.INFO)
response = {
"status": status,
"endpoint": endpoint,
"result": result,
}
process_logger.info(response)
response = json.dumps(response)
print(response, flush=True)
error_logger = None
def errorLogging() -> None:
global error_logger
if error_logger is None:
error_logger = setupLogger("error", "error.log", logging.ERROR)
error_logger.error(traceback.format_exc())

11
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
# Customize
/bin

3985
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,22 @@
[package]
name = "VRCT"
version = "0.0.0"
description = "VRCT Application"
authors = ["misyaguziya"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1", features = [] }
[dependencies]
tauri = { version = "1", features = [ "window-set-size", "window-set-position", "window-unmaximize", "window-close", "window-maximize", "window-minimize", "window-unminimize", "window-start-dragging", "window-set-decorations", "window-set-always-on-top", "shell-sidecar", "shell-open", "devtools"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
font-kit = "0.14.2"
window-shadows = { git = "https://github.com/tauri-apps/window-shadows.git" }
[features]
# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

3
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

1025
src-tauri/nsis/template.nsi Normal file

File diff suppressed because it is too large Load Diff

43
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,43 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
// use tauri::command;
use tauri::Manager;
use window_shadows::set_shadow;
fn main() {
tauri::Builder::default()
.setup(|app| {
let main_window = app.get_window("main").unwrap(); // `main_window` is declared here for all builds
#[cfg(debug_assertions)]
{ main_window.open_devtools(); }
#[cfg(any(windows, target_os = "macos"))]
set_shadow(main_window, true).unwrap();
Ok(())
})
.invoke_handler(tauri::generate_handler![get_font_list])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
use font_kit::{source::SystemSource};
use std::collections::HashSet;
#[tauri::command]
async fn get_font_list() -> Vec<String> {
let source = SystemSource::new();
let mut font_families = HashSet::new();
if let Ok(fonts) = source.all_fonts() {
for font in fonts {
if let Ok(info) = font.load() {
font_families.insert(info.family_name().to_string());
}
}
}
font_families.into_iter().collect()
}

84
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,84 @@
{
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "",
"devPath": "http://localhost:1420",
"distDir": "../dist"
},
"package": {
"productName": "VRCT",
"version": "3.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"window": {
"all": false,
"setAlwaysOnTop": true,
"setDecorations": true,
"close": true,
"setPosition": true,
"setSize": true,
"maximize": true,
"minimize": true,
"unmaximize": true,
"unminimize": true,
"startDragging": true
},
"shell": {
"all": false,
"open": true,
"sidecar": true,
"scope": [
{
"name": "bin/VRCT-sidecar", "sidecar": true,"args": true
}
]
}
},
"windows": [
{
"title": "VRCT",
"center": true,
"width": 450,
"height": 220,
"minWidth": 400,
"minHeight": 200,
"transparent": true,
"decorations": false
}
],
"security": {
"csp": null
},
"bundle": {
"active": true,
"targets": "nsis",
"identifier": "com.vrct.dev",
"publisher": "m's software",
"copyright": "Copyright m's software",
"shortDescription": "VRCT",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": [
"bin/VRCT-sidecar"
],
"resources":{
"bin/_internal": "_internal"
},
"windows": {
"nsis": {
"template": "nsis/template.nsi",
"license": "../LICENSE",
"installMode": "both",
"displayLanguageSelector": true
}
}
}
}
}

71
src-ui/app/App.jsx Normal file
View File

@@ -0,0 +1,71 @@
import { useTranslation } from "react-i18next";
import {
useWindow,
} from "@logics_common";
// import React from "react";
import {
KeyEventController,
StartPythonController,
UiLanguageController,
ConfigPageCloseTriggerController,
UiSizeController,
FontFamilyController,
TransparencyController,
} from "./_app_controllers/index.js";
import { WindowTitleBar } from "./window_title_bar/WindowTitleBar";
import { MainPage } from "./main_page/MainPage";
import { ConfigPage } from "./config_page/ConfigPage";
import { SplashComponent } from "./splash_component/SplashComponent";
import { UpdatingComponent } from "./updating_component/UpdatingComponent";
import { ModalController } from "./modal_controller/ModalController";
import { SnackbarController } from "./snackbar_controller/SnackbarController";
import styles from "./App.module.scss";
import { useIsBackendReady, useIsSoftwareUpdating } from "@logics_common";
export const App = () => {
const { currentIsBackendReady } = useIsBackendReady();
const { WindowGeometryController } = useWindow();
const { i18n } = useTranslation();
return (
<div className={styles.container}>
<KeyEventController />
<StartPythonController />
<UiLanguageController />
<ConfigPageCloseTriggerController />
<UiSizeController />
<FontFamilyController />
<TransparencyController />
<WindowGeometryController />
{currentIsBackendReady.data === false
? <SplashComponent />
: <Contents key={i18n.language}/>
}
</div>
);
};
const Contents = () => {
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
return (
<>
<WindowTitleBar />
{currentIsSoftwareUpdating.data === false
?
<div className={styles.pages_wrapper}>
<ConfigPage />
<MainPage />
<ModalController />
<SnackbarController />
</div>
:
<UpdatingComponent />
}
</>
);
};

View File

@@ -0,0 +1,18 @@
.container {
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
background-color: var(--dark_888_color);
align-items: center;
}
.pages_wrapper {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}

View File

@@ -0,0 +1,55 @@
import { useEffect } from "react";
import {
useVolume,
useIsOpenedConfigPage,
} from "@logics_common";
import {
useMainFunction,
} from "@logics_main";
import { useStore_MainFunctionsStateMemory } from "@store";
export const ConfigPageCloseTriggerController = () => {
const { currentIsOpenedConfigPage } = useIsOpenedConfigPage();
const { currentMainFunctionsStateMemory, updateMainFunctionsStateMemory} = useStore_MainFunctionsStateMemory();
const {
currentTranscriptionSendStatus,
setTranscriptionSend,
currentTranscriptionReceiveStatus,
setTranscriptionReceive,
} = useMainFunction();
const {
currentMicThresholdCheckStatus,
volumeCheckStop_Mic,
currentSpeakerThresholdCheckStatus,
volumeCheckStop_Speaker,
} = useVolume();
const memorizeLatestMainFunctionsState = () => {
updateMainFunctionsStateMemory({
transcription_send: currentTranscriptionSendStatus.data,
transcription_receive: currentTranscriptionReceiveStatus.data,
});
};
const restoreMainFunctionState = () => {
if (currentMainFunctionsStateMemory.data.transcription_send === true) setTranscriptionSend(true);
if (currentMainFunctionsStateMemory.data.transcription_receive === true) setTranscriptionReceive(true);
};
useEffect(() => {
if (currentIsOpenedConfigPage.data === true) { // When config page is opened.
memorizeLatestMainFunctionsState();
if (currentTranscriptionSendStatus.data === true) setTranscriptionSend(false);
if (currentTranscriptionReceiveStatus.data === true) setTranscriptionReceive(false);
} else if (currentIsOpenedConfigPage.data === false) { // When config page is closed.
if (currentMicThresholdCheckStatus.data === true) volumeCheckStop_Mic();
if (currentSpeakerThresholdCheckStatus.data === true) volumeCheckStop_Speaker();
restoreMainFunctionState();
}
}, [currentIsOpenedConfigPage.data]);
return null;
};

View File

@@ -0,0 +1,11 @@
import { useEffect } from "react";
import { useSelectedFontFamily } from "@logics_configs";
export const FontFamilyController = () => {
const { currentSelectedFontFamily } = useSelectedFontFamily();
useEffect(() => {
document.documentElement.style.setProperty("--font_family", currentSelectedFontFamily.data);
}, [currentSelectedFontFamily.data]);
return null;
};

View File

@@ -0,0 +1,26 @@
import { useEffect } from "react";
export const KeyEventController = () => {
useEffect(() => {
const handleKeydown = (event) => {
if (event.key === "F5" || (event.ctrlKey && event.key === "r") ||
(event.metaKey && event.key === "r")) {
event.preventDefault();
}
};
const handleContextmenu = (event) => {
event.preventDefault();
};
document.addEventListener("keydown", handleKeydown);
document.addEventListener("contextmenu", handleContextmenu);
return () => {
document.removeEventListener("keydown", handleKeydown);
document.removeEventListener("contextmenu", handleContextmenu);
};
}, []);
return null;
};

View File

@@ -0,0 +1,48 @@
import { invoke } from "@tauri-apps/api/tauri";
import { useEffect, useRef } from "react";
import { useStartPython } from "@logics/useStartPython";
import { useStdoutToPython } from "@logics/useStdoutToPython";
import { useStore_SelectableFontFamilyList } from "@store";
import { arrayToObject } from "@utils";
export const StartPythonController = () => {
const { asyncStartPython } = useStartPython();
const hasRunRef = useRef(false);
const { asyncFetchFonts } = useAsyncFetchFonts();
useEffect(() => {
if (!hasRunRef.current) {
asyncStartPython().then(() => {
startFeedingToWatchDogController();
asyncFetchFonts();
}).catch((err) => {
console.error(err);
});
}
return () => hasRunRef.current = true;
}, []);
return null;
};
const useAsyncFetchFonts = () => {
const { updateSelectableFontFamilyList } = useStore_SelectableFontFamilyList();
const asyncFetchFonts = async () => {
try {
let fonts = await invoke("get_font_list");
fonts = fonts.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
updateSelectableFontFamilyList(arrayToObject(fonts));
} catch (error) {
console.error("Error fetching fonts:", error);
}
};
return { asyncFetchFonts };
};
const startFeedingToWatchDogController = () => {
const { asyncStdoutToPython } = useStdoutToPython();
setInterval(() => {
asyncStdoutToPython("/run/feed_watchdog");
}, 20000); // 20000ミリ秒 = 20秒
};

View File

@@ -0,0 +1,11 @@
import { useEffect } from "react";
import { useTransparency } from "@logics_configs";
export const TransparencyController = () => {
const { currentTransparency } = useTransparency();
useEffect(() => {
document.documentElement.style.setProperty("opacity", `${currentTransparency.data / 100}`);
}, [currentTransparency.data]);
return null;
};

View File

@@ -0,0 +1,14 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useUiLanguage } from "@logics_configs";
export const UiLanguageController = () => {
const { currentUiLanguage } = useUiLanguage();
const { i18n } = useTranslation();
useEffect(() => {
i18n.changeLanguage(currentUiLanguage.data);
}, [currentUiLanguage.data]);
return null;
};

View File

@@ -0,0 +1,13 @@
import { useEffect } from "react";
import { useUiScaling } from "@logics_configs";
export const UiSizeController = () => {
const { currentUiScaling } = useUiScaling();
const font_size = 62.5 * currentUiScaling.data / 100;
useEffect(() => {
document.documentElement.style.setProperty("font-size", `${font_size}%`);
}, [currentUiScaling.data]);
return null;
};

View File

@@ -0,0 +1,7 @@
export { KeyEventController } from "./KeyEventController";
export { StartPythonController } from "./StartPythonController";
export { UiLanguageController } from "./UiLanguageController";
export { ConfigPageCloseTriggerController } from "./ConfigPageCloseTriggerController";
export { UiSizeController } from "./UiSizeController";
export { FontFamilyController } from "./FontFamilyController";
export { TransparencyController } from "./TransparencyController";

View File

@@ -0,0 +1,418 @@
/*! destyle.css v4.0.1 | MIT License | https://github.com/nicolas-cusan/destyle.css */
/* Reset box-model and set borders */
/* ============================================ */
*,
::before,
::after {
box-sizing: border-box;
border-style: solid;
border-width: 0;
min-width: 0;
}
/* Document */
/* ============================================ */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
* 3. Remove gray overlay on links for iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-webkit-tap-highlight-color: transparent; /* 3*/
}
/* Sections */
/* ============================================ */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/* Vertical rhythm */
/* ============================================ */
p,
table,
blockquote,
address,
pre,
iframe,
form,
figure,
dl {
margin: 0;
}
/* Headings */
/* ============================================ */
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
margin: 0;
}
/* Lists (enumeration) */
/* ============================================ */
ul,
ol {
margin: 0;
padding: 0;
list-style: none;
}
/* Lists (definition) */
/* ============================================ */
dt {
font-weight: bold;
}
dd {
margin-left: 0;
}
/* Grouping content */
/* ============================================ */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
border-top-width: 1px;
margin: 0;
clear: both;
color: inherit;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: inherit; /* 2 */
}
address {
font-style: inherit;
}
/* Text-level semantics */
/* ============================================ */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
text-decoration: none;
color: inherit;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: inherit; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Replaced content */
/* ============================================ */
/**
* Prevent vertical alignment issues.
*/
svg,
img,
embed,
object,
iframe {
vertical-align: bottom;
}
/* Forms */
/* ============================================ */
/**
* Reset form fields to make them styleable.
* 1. Make form elements stylable across systems iOS especially.
* 2. Inherit text-transform from parent.
*/
button,
input,
optgroup,
select,
textarea {
-webkit-appearance: none; /* 1 */
appearance: none;
vertical-align: middle;
color: inherit;
font: inherit;
background: transparent;
padding: 0;
margin: 0;
border-radius: 0;
text-align: inherit;
text-transform: inherit; /* 2 */
/* Customize */
outline: none;
}
/**
* Correct cursors for clickable elements.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
cursor: pointer;
}
button:disabled,
[type="button"]:disabled,
[type="reset"]:disabled,
[type="submit"]:disabled {
cursor: default;
}
/**
* Improve outlines for Firefox and unify style with input elements & buttons.
*/
:-moz-focusring {
outline: auto;
}
select:disabled {
opacity: inherit;
}
/**
* Remove padding
*/
option {
padding: 0;
}
/**
* Reset to invisible
*/
fieldset {
margin: 0;
padding: 0;
min-width: 0;
}
legend {
padding: 0;
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* Correct the outline style in Safari.
*/
[type="search"] {
outline-offset: -2px; /* 1 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Fix font inheritance.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/**
* Fix appearance for Firefox
*/
[type="number"] {
appearance: textfield;
}
/**
* Clickable labels
*/
label[for] {
cursor: pointer;
}
/* Interactive */
/* ============================================ */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/*
* Remove outline for editable content.
*/
[contenteditable]:focus {
outline: auto;
}
/* Tables */
/* ============================================ */
/**
1. Correct table border color inheritance in all Chrome and Safari.
*/
table {
border-color: inherit; /* 1 */
border-collapse: collapse;
}
caption {
text-align: left;
}
td,
th {
vertical-align: top;
padding: 0;
}
th {
text-align: left;
font-weight: bold;
}

View File

@@ -0,0 +1,45 @@
@import "./reset.css";
@import "./variables.css";
:root {
font-size: 62.5%;
color: #F2F2F2;
}
* {
user-select: none;
&::-webkit-scrollbar {
width: 0.8rem;
}
&::-webkit-scrollbar-track {
background-color: var(--dark_925_color);
border-radius: 0.4rem;
}
&::-webkit-scrollbar-thumb {
background-color: var(--dark_800_color);
border-radius: 0.4rem;
}
}
html, body {
height: 100%;
font-family: var(--font_family); /* If not found the font family where 'root:' that is selected by user*/
border-radius: 1.8rem;
}
#root {
height: 100%;
/* For controlling a whole window transparency */
opacity: 1;
}
/* SVG内のすべての要素にfillを適用 (colorの調整をcssでするため) */
svg {
fill: currentColor;
}
p {
white-space: pre-wrap;
}

View File

@@ -0,0 +1,61 @@
:root {
--primary_100_color: #b7ded8;
--primary_150_color: #a1d4cc;
--primary_200_color: #8acac0;
--primary_250_color: #76bfb4;
--primary_300_color: #61b4a7;
--primary_350_color: #55ac9e;
--primary_400_color: #48a495;
--primary_450_color: #429c8c;
--primary_500_color: #3b9483;
--primary_550_color: #398E7D;
--primary_600_color: #368777;
--primary_650_color: #347f6f;
--primary_700_color: #317767;
--primary_750_color: #2f6f60;
--primary_800_color: #2c6759;
--primary_900_color: #214b3f;
--primary_600_color_44: #36877744;
--sent_400_color: #6197b4;
--received_300_color: #a861b4;
--dark_basic_text_color: #f2f2f2;
--dark_100_color: #f5f7fb;
--dark_200_color: #f1f2f6;
--dark_300_color: #e9eaee;
--dark_350_color: #d8d9dd;
--dark_400_color: #c7c8cc;
--dark_450_color: #b8b9bd;
--dark_500_color: #a9aaae;
--dark_550_color: #949599;
--dark_600_color: #7f8084;
--dark_650_color: #75767a;
--dark_700_color: #6a6c6f;
--dark_725_color: #636467;
--dark_750_color: #5b5c5f;
--dark_775_color: #535457;
--dark_800_color: #4b4c4f;
--dark_825_color: #434447;
--dark_850_color: #3a3b3e;
--dark_863_color: #36373a;
--dark_875_color: #323336;
--dark_888_color: #2e2f32;
--dark_900_color: #292a2d;
--dark_925_color: #242528;
--dark_950_color: #1f2022;
--dark_975_color: #1a1b1d;
--dark_1000_color: #151517;
--dark_825_color_cc: #434447cc;
--dark_550_color_22: #94959922;
--title_bar_height: 2rem;
--main_page_topbar_height: 4.8rem;
--config_page_sidebar_width: 16.8rem;
--config_page_topbar_height: 8rem;
--font_family: "Yu Gothic UI";
}

View File

@@ -0,0 +1,34 @@
import styles from "./ConfigPage.module.scss";
import { Topbar } from "./topbar/Topbar.jsx";
import { SidebarSection } from "./sidebar_section/SidebarSection.jsx";
import { SettingSection } from "./setting_section/SettingSection.jsx";
import { useSoftwareVersion } from "@logics_configs";
import { useComputeMode } from "@logics_common";
import { useTranslation } from "react-i18next";
export const ConfigPage = () => {
const { t } = useTranslation();
const { currentSoftwareVersion } = useSoftwareVersion();
const { currentComputeMode } = useComputeMode();
const version_label = currentComputeMode.data === "cpu"
? t("config_page.version", { version: currentSoftwareVersion.data })
: currentComputeMode.data === "cuda"
? t("config_page.version", { version: currentSoftwareVersion.data }) + " CUDA"
: t("config_page.version", { version: currentSoftwareVersion.data });
return (
<div className={styles.page}>
<div className={styles.container}>
<Topbar />
<div className={styles.main_container}>
<SidebarSection />
<SettingSection />
</div>
<p className={styles.software_version}>{version_label}</p>
</div>
</div>
);
};

View File

@@ -0,0 +1,34 @@
.page {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: var(--dark_900_color);
overflow: hidden;
position: relative;
}
.main_container {
width: 100%;
height: 100%;
display: flex;
padding-top: var(--config_page_topbar_height);
}
.software_version {
position: absolute;
bottom: 0.8rem;
left: 1.2rem;
font-size: 1.2rem;
color: var(--dark_400_color);
}

View File

@@ -0,0 +1,28 @@
import { useRef, useLayoutEffect, useEffect } from "react";
import styles from "./SettingSection.module.scss";
import { SettingBox } from "./setting_box/SettingBox";
import { store, useStore_SelectedConfigTabId } from "@store";
import { useSettingBoxScrollPosition } from "@logics_configs";
export const SettingSection = () => {
const { currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
const { resetScrollPosition } = useSettingBoxScrollPosition();
const scrollContainerRef = useRef(null);
useLayoutEffect(() => {
store.setting_box_scroll_container = scrollContainerRef;
}, []);
useEffect(() => {
resetScrollPosition();
}, [currentSelectedConfigTabId.data]);
return (
<div ref={scrollContainerRef} className={styles.scroll_container}>
<div className={styles.container}>
<SettingBox />
</div>
</div>
);
};

View File

@@ -0,0 +1,9 @@
.scroll_container {
width: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.container {
margin: 0rem 4rem 16rem 0.6rem;
}

View File

@@ -0,0 +1,40 @@
import { useStore_SelectedConfigTabId } from "@store";
import {
Device,
Appearance,
Translation,
Transcription,
Others,
AdvancedSettings,
Vr,
Supporters,
AboutVrct,
} from "@setting_box";
export const SettingBox = () => {
const { currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
switch (currentSelectedConfigTabId.data) {
case "device":
return <Device />;
case "appearance":
return <Appearance />;
case "translation":
return <Translation />;
case "transcription":
return <Transcription />;
case "others":
return <Others />;
case "vr":
return <Vr />;
case "advanced_settings":
return <AdvancedSettings />;
case "supporters":
return <Supporters />;
case "about_vrct":
return <AboutVrct />;
default:
return null;
}
};

View File

@@ -0,0 +1,36 @@
import clsx from "clsx";
import React, { useRef, forwardRef, useImperativeHandle } from "react";
import styles from "./_Entry.module.scss";
const _Entry = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
const input_class_names = clsx(styles.entry_input_area, {
[styles.is_disabled]: props.is_disabled
});
return (
<div className={styles.entry_container}>
<div
className={styles.entry_wrapper}
style={{width: props.width }}
>
<input
ref={inputRef}
className={input_class_names}
value={props.ui_variable === null ? "" : props.ui_variable}
onChange={(e) => props.onChange(e)}
/>
</div>
</div>
);
});
_Entry.displayName = "_Entry";
export { _Entry };

View File

@@ -0,0 +1,24 @@
.entry_container {
width: 100%;
}
.entry_wrapper {
width: 10rem;
height: 100%;
padding: 0.6rem;
background-color: var(--dark_875_color);
border: 0.1rem solid var(--dark_750_color);
border-radius: 0.4rem;
}
.entry_input_area {
width: 100%;
height: 100%;
font-size: 1.4rem;
resize: none;
color: var(--dark_basic_text_color);
&.is_disabled {
color: var(--dark_500_color);
pointer-events: none;
}
}

View File

@@ -0,0 +1,11 @@
import styles from "./ActionButton.module.scss";
export const ActionButton = ({IconComponent, onclickFunction}) => {
return (
<div className={styles.container}>
<button className={styles.button_wrapper} onClick={onclickFunction}>
<IconComponent className={styles.button_svg}/>
</button>
</div>
);
};

View File

@@ -0,0 +1,15 @@
.button_wrapper {
padding: 1.6rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.button_svg {
width: 2.4rem;
color: var(--dark_400_color);
}

View File

@@ -0,0 +1,72 @@
import styles from "./DeeplAuthKey.module.scss";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import CircularProgress from "@mui/material/CircularProgress";
import ExternalLink from "@images/external_link.svg?react";
import { _Entry } from "../_atoms/_entry/_Entry";
import { useState, useRef } from "react";
import { useEffect } from "react";
export const DeeplAuthKey = (props) => {
const { t } = useTranslation();
const [is_editable, seIsEditable] = useState(false);
const entryRef = useRef(null);
const revealEditAuthKey = () => {
seIsEditable(true);
entryRef.current.focus();
};
const onchangeEntryAuthKey = (e) => {
props.onChangeFunction(e.target.value);
};
const saveAuthKey = () => {
props.saveFunction();
};
useEffect(() => {
if (props.variable === "" || props.variable === null) {
seIsEditable(true);
}
}, [props.variable]);
const is_disabled = props.state === "pending";
const save_button_class_names = clsx(styles.save_button, {
[styles.is_disabled]: is_disabled
});
return (
<div className={styles.container}>
<div className={styles.entry_section_wrapper}>
<_Entry ref={entryRef} width="30rem" onChange={onchangeEntryAuthKey} ui_variable={props.variable} is_disabled={is_disabled}/>
<button className={save_button_class_names} onClick={saveAuthKey}>
{is_disabled
? <CircularProgress size="1.4rem" sx={{ color: "var(--dark_basic_text_color)" }}/>
: <p className={styles.save_button_label}>{t("config_page.translation.deepl_auth_key.save")}</p>
}
</button>
{is_editable
? null
:
<div className={styles.entry_edit_cover} onClick={revealEditAuthKey}>
<button className={styles.edit_button}>{t("config_page.translation.deepl_auth_key.edit")}</button>
</div>
}
</div>
</div>
);
};
export const OpenWebpage_DeeplAuthKey = () => {
const { t } = useTranslation();
return (
<div className={styles.open_webpage_button_wrapper}>
<a className={styles.open_webpage_button} href="https://www.deepl.com/ja/your-account/keys" target="_blank" rel="noreferrer" >
<p className={styles.open_webpage_text}>{t("config_page.translation.deepl_auth_key.open_auth_key_webpage")}</p>
<ExternalLink className={styles.external_link_svg} />
</a>
</div>
);
};

View File

@@ -0,0 +1,101 @@
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.entry_section_wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
position: relative;
}
.entry_edit_cover {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 0.4rem;
background-color: (#00000044);
backdrop-filter: blur(4rem);
border: solid 0.1rem var(--dark_700_color);
&:hover {
background-color: (#00000088);
}
&:active {
backdrop-filter: blur(1.4rem);
}
}
.edit_button {
padding: 0.8rem 1.2rem;
color: var(--dark_basic_text_color);
height: 100%;
width: 100%;
font-size: 1.4rem;
text-align: center;
}
.save_button {
padding: 0.8rem 1.2rem;
background-color: var(--primary_600_color);
border-radius: 0.4rem;
text-align: center;
flex-shrink: 0;
min-width: 5.4rem;
&:hover {
background-color: var(--primary_500_color);
}
&:active {
background-color: var(--primary_700_color);
}
&.is_disabled {
pointer-events: none;
background-color: var(--primary_800_color);
}
}
.save_button_label {
color: var(--dark_basic_text_color);
font-size: 1.4rem;
}
.open_webpage_button_wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.open_webpage_button {
padding: 0.6rem 2.8rem;
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
border-radius: 0.4rem;
cursor: pointer;
flex-shrink: 0;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.open_webpage_text {
font-size: 1.2rem;
color: var(--dark_basic_text_color);
}
.external_link_svg {
color: var(--dark_500_color);
width: 1.6rem;
flex-shrink: 0;
}

View File

@@ -0,0 +1,81 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import CircularProgress from "@mui/material/CircularProgress";
import styles from "./DownloadModels.module.scss";
import {
RadioButton,
// DownloadModels,
} from "../index";
export const DownloadModels = (props) => {
const options = props.options.map(item => ({
...item,
disabled: !item.is_downloaded
}));
return (
<>
<RadioButton
selectFunction={props.selectFunction}
name={props.name}
options={options}
checked_variable={props.checked_variable}
column={true}
ChildComponent={ModelSelector}
downloadStartFunction={props.downloadStartFunction}
/>
</>
// <div className={styles.container}>
// {props.models.map((option) => (
// <ModelSelector key={option.model_id} option={option} {...props}/>
// ))}
// </div>
);
};
const ModelSelector = ({option, ...props}) => {
const { t } = useTranslation();
const [circular_color, setCircularColor] = useState("");
const [circular_color_2, setCircularColor2] = useState("");
useEffect(() => {
const circular_color = getComputedStyle(document.documentElement).getPropertyValue("--dark_600_color");
setCircularColor(circular_color.trim());
const circular_color_2 = getComputedStyle(document.documentElement).getPropertyValue("--primary_300_color");
setCircularColor2(circular_color_2.trim());
}, []);
const renderContent = () => {
const circular_progress = Math.floor(option.progress / 10) * 10;
switch (true) {
case option.progress !== null:
return (
<>
<CircularProgress
variant={(option.progress === 100) ? "indeterminate" : "determinate"}
value={circular_progress}
size="3rem"
sx={{ color: circular_color_2 }}
/>
<p className={styles.progress_label}>{`${Math.round(option.progress)}%`}</p>
</>
);
case option.is_pending:
return <CircularProgress size="3rem" sx={{ color: circular_color }}/>;
case !option.is_downloaded:
return (
<button
className={styles.download_button}
onClick={() => props.downloadStartFunction(option.id)}
>
<p className={styles.download_button_label}>{t("config_page.model_download_button_label")}</p>
</button>
);
default:
return null;
}
};
return <div className={styles.download_container}>{renderContent()}</div>;
};

View File

@@ -0,0 +1,32 @@
@import "@scss_mixins";
.download_container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 8rem;
}
.download_button {
pointer-events: auto;
background-color: var(--dark_800_color);
padding: 0.8rem;
flex-shrink: 0;
&:hover {
background-color: var(--dark_750_color);
}
&:active {
background-color: var(--dark_800_color);
}
}
.download_button_label {
font-size: 1.2rem;
color: var(--dark_basic_text_color);
}
.progress_label {
position: absolute;
font-size: 1rem;
color: var(--dark_basic_text_color);
}

View File

@@ -0,0 +1,76 @@
import styles from "./DropdownMenu.module.scss";
import clsx from "clsx";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import { useStore_IsOpenedDropdownMenu } from "@store";
export const DropdownMenu = (props) => {
const { updateIsOpenedDropdownMenu, currentIsOpenedDropdownMenu } = useStore_IsOpenedDropdownMenu();
const toggleDropdownMenu = () => {
if (currentIsOpenedDropdownMenu.data === props.dropdown_id) {
updateIsOpenedDropdownMenu("");
} else {
if (props.openListFunction !== undefined) props.openListFunction();
updateIsOpenedDropdownMenu(props.dropdown_id);
}
};
const selectValue = (key) => {
updateIsOpenedDropdownMenu("");
props.selectFunction({
dropdown_id: props.dropdown_id,
selected_id: key,
});
};
const dropdown_content_wrapper_class_name = clsx(styles["dropdown_content_wrapper"], {
[styles.is_opened]: (currentIsOpenedDropdownMenu.data === props.dropdown_id) ? true : false,
[styles.is_disabled]: props.is_disabled,
});
const dropdown_toggle_button_class_name = clsx(styles["dropdown_toggle_button"], {
[styles.is_pending]: (props.state === "pending") ? true : false,
[styles.is_disabled]: props.is_disabled,
});
const arrow_class_names = clsx(styles["arrow_left_svg"], {
[styles.is_opened]: (currentIsOpenedDropdownMenu.data === props.dropdown_id) ? true : false
});
const getSelectedText = () => {
if (props.state !== "ok") return;
if (props.list[props.selected_id] === undefined) return props.selected_id; // [Fix me]
return props.list[props.selected_id];
};
const list = (props.list === undefined) ? {} : props.list;
return (
<div className={styles.container}>
<div className={dropdown_toggle_button_class_name} onClick={toggleDropdownMenu} style={props.style}>
{(props.state === "pending")
? <p className={styles.dropdown_selected_text}>Loading...</p>
: <p className={styles.dropdown_selected_text}>{getSelectedText()}</p>
}
{(props.state === "pending")
? <span className={styles.loader}></span>
: <ArrowLeftSvg className={arrow_class_names} />
}
</div>
<div className={dropdown_content_wrapper_class_name}>
<div className={styles.dropdown_content}>
{(props.state === "ok")
? Object.entries(list).map(([key, value]) => {
return (
<div key={key} className={styles.value_button} onClick={() => selectValue(key)}>
<p className={styles.value_text}>{value}</p>
</div>
);
})
: null
}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,97 @@
@import "@scss_mixins";
.container {
position: relative;
}
.dropdown_toggle_button {
position: relative;
background-color: var(--dark_950_color);
min-width: 20rem;
padding: 0.8rem 1.4rem;
cursor: pointer;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_925_color);
}
&:active {
background-color: var(--dark_975_color);
}
&.is_pending {
pointer-events: none;
}
&.is_disabled {
pointer-events: none;
.dropdown_selected_text, .arrow_left_svg {
color: var(--dark_550_color);
}
}
}
.dropdown_selected_text {
font-size: 1.4rem;
padding-right: 2.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown_content_wrapper {
display: none;
position: absolute;
top: 100%; // Position it below the toggle button
right: 0;
min-width: 20rem;
z-index: 1;
&.is_opened {
display: block;
}
&.is_disabled {
pointer-events: none;
.value_text {
color: var(--dark_550_color);
}
}
}
.dropdown_content {
background-color: var(--dark_900_color);
border: 0.1rem solid var(--dark_600_color);
display: flex;
flex-direction: column;
gap: 0.1rem;
white-space: nowrap;
max-height: 20rem;
overflow-y: scroll;
}
.value_button {
background-color: var(--dark_875_color);
padding: 1.2rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.value_text {
font-size: 1.4rem;
}
.loader {
@include loader(2rem, 0.2rem, right, 0);
}
.arrow_left_svg {
position: absolute;
top: 50%;
right: 0;
transform: translate(-50%, -50%) rotate(-90deg);
width: 1.4rem;
&.is_opened {
transform: translate(-50%, -50%) rotate(90deg);
}
}

View File

@@ -0,0 +1,10 @@
import styles from "./Entry.module.scss";
import { _Entry } from "../_atoms/_entry/_Entry";
export const Entry = (props) => {
return (
<div className={styles.entry_container}>
<_Entry {...props} />
</div>
);
};

View File

@@ -0,0 +1,12 @@
export { ActionButton } from "./action_button/ActionButton";
export { DeeplAuthKey, OpenWebpage_DeeplAuthKey } from "./deepl_auth_key/DeeplAuthKey";
export { DropdownMenu } from "./dropdown_menu/DropdownMenu";
export { Entry } from "./entry/Entry";
export { LabelComponent } from "./label_component/LabelComponent";
export { RadioButton } from "./radio_button/RadioButton";
export { SectionLabelComponent } from "./section_label_component/SectionLabelComponent";
export { Slider } from "./slider/Slider";
export { SwitchBox } from "./switch_box/SwitchBox";
export { ThresholdComponent } from "./threshold_component/ThresholdComponent";
export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
export { DownloadModels } from "./download_models/DownloadModels";

View File

@@ -0,0 +1,13 @@
import styles from "./LabelComponent.module.scss";
export const LabelComponent = (props) => {
return (
<div className={styles.label_component}>
<p className={styles.label}>{props.label}</p>
{props.desc
? <p className={styles.desc}>{props.desc}</p>
: null
}
</div>
);
};

View File

@@ -0,0 +1,22 @@
.label_component {
display: flex;
flex-direction: column;
gap: 0.4rem;
// flex-shrink: 0;
}
.label {
font-size: 1.6rem;
font-weight: 400;
color: var(--dark_basic_text_color);
white-space: nowrap;
width: max-content;
}
.desc {
font-size: 1.4rem;
font-weight: 300;
color: var(--dark_500_color);
max-width: 38rem;
overflow-wrap: break-word;
}

View File

@@ -0,0 +1,42 @@
import styles from "./RadioButton.module.scss";
import clsx from "clsx";
export const RadioButton = (props) => {
const containerClass = clsx(styles.container, {
[styles.column]: props.column === true,
});
return (
<div className={containerClass}>
{props.checked_variable.state === "pending" && <span className={styles.loader}></span>}
{props.options.map((option) => {
const radioWrapperClass = clsx(styles.radio_button_container, {
[styles.is_selected]: props.checked_variable.data === option.id,
});
const labelClass = clsx(styles.radio_button_wrapper, {
[styles.is_selected]: props.checked_variable.data === option.id,
[styles.disabled]: option.disabled === true || props.checked_variable.state === "pending",
});
return (
<div key={option.id} className={radioWrapperClass}>
<label className={labelClass}>
<input
className={styles.radio_button_input}
type="radio"
name={props.name}
value={option.id}
onChange={() => props.selectFunction(option.id)}
checked={props.checked_variable.data === option.id}
disabled={option.disabled === true || props.checked_variable.state === "pending"}
/>
<p className={styles.radio_button_label}>{option.label}</p>
</label>
{props.ChildComponent && <props.ChildComponent option={option} {...props} />}
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,74 @@
@import "@scss_mixins";
.container {
display: flex;
gap: 0.4rem;
position: relative;
flex-shrink: 0;
flex-wrap: wrap;
max-width: 70%;
&.column {
flex-direction: column;
}
}
.radio_button_container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
position: relative;
}
.radio_button_wrapper {
display: flex;
flex-shrink: 0;
align-items: center;
cursor: pointer;
gap: 1rem;
padding: 0.6rem 0.8rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_925_color);
}
&.is_selected {
pointer-events: none;
}
&.disabled {
pointer-events: none;
color: var(--dark_600_color);
}
}
.radio_button_input {
appearance: none;
margin: 0;
width: 2rem;
height: 2rem;
border: 0.3rem solid var(--dark_600_color);
border-radius: 50%;
transition: border-color .1s ease, border-width .1s ease;
flex-shrink: 0;
cursor: inherit;
&:checked {
border-color: var(--primary_400_color);
border-width: 0.6rem;
}
&:disabled {
border-color: var(--dark_825_color);
}
}
.radio_button_label {
font-size: 1.4rem;
font-weight: 400;
flex-shrink: 0;
}
.loader {
@include loader(2rem, 0.2rem, left, -1.6rem);
}

View File

@@ -0,0 +1,11 @@
import styles from "./SectionLabelComponent.module.scss";
import clsx from "clsx";
export const SectionLabelComponent = (props) => {
return (
<div className={styles.container}>
<label className={styles.section_label}>{props.label}</label>
<div className={styles.section_line}></div>
</div>
);
};

View File

@@ -0,0 +1,16 @@
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
padding-bottom: 2rem;
}
.section_label {
font-size: 2rem;
flex-shrink: 0;
}
.section_line {
width: 100%;
height: 0.1rem;
background: linear-gradient(90deg, var(--dark_400_color) 0%, var(--dark_600_color) 35%, var(--dark_800_color) 100%);
}

Some files were not shown because too many files have changed in this diff Show More