Compare commits

..

10 Commits

Author SHA1 Message Date
Sakamoto Shiina
942a257e79 Merge branch 'bugfix_ui_endpoint' into develop 2026-02-19 14:49:03 +09:00
Sakamoto Shiina
6b2be0f22b [Fix] UI: Add 'run' endpoint for selected translation and transcription compute type. 2026-02-19 14:47:04 +09:00
Sakamoto Shiina
242c022e33 Merge branch 'version' into develop 2026-01-20 03:43:39 +09:00
Sakamoto Shiina
0765908184 👍️[Update] Version 3.4.1 -> 3.4.2 2026-01-20 03:42:23 +09:00
Sakamoto Shiina
f6a3b0e8fd Merge branch 'hotfix_update_available_error' into develop 2026-01-20 03:39:36 +09:00
Sakamoto Shiina
5811158231 [Hotfix] UI: SoftwareUpdateAvailableButton: Fix update check logic and rendering order. 2026-01-20 03:36:59 +09:00
misyaguziya
0cb2b7e6ae Merge branch 'version' into develop 2026-01-19 22:13:07 +09:00
misyaguziya
623d17e885 [Update] バージョンを3.4.0から3.4.1に更新 2026-01-19 22:12:32 +09:00
misyaguziya
4c95b66dd8 Merge branch 'bugfix_telemetry' into develop 2026-01-19 21:14:03 +09:00
misyaguziya
1656620ce4 [Update] テレメトリ機能の改善と不要なコードの削除 2026-01-19 21:12:16 +09:00
13 changed files with 184 additions and 313 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "vrct",
"private": true,
"version": "3.4.0",
"version": "3.4.2",
"type": "module",
"scripts": {
"setup-python": "bat\\install.bat",

View File

@@ -724,7 +724,7 @@ class Config:
NOTIFICATION_VRC_SFX = ManagedProperty('NOTIFICATION_VRC_SFX', type_=bool)
WEBSOCKET_HOST = ManagedProperty('WEBSOCKET_HOST', type_=str)
WEBSOCKET_PORT = ManagedProperty('WEBSOCKET_PORT', type_=int)
# --- Telemetry Settings ---
ENABLE_TELEMETRY = ManagedProperty('ENABLE_TELEMETRY', type_=bool)
@@ -764,7 +764,7 @@ class Config:
def init_config(self):
# Read Only
self._VERSION = "3.4.0"
self._VERSION = "3.4.2"
if getattr(sys, 'frozen', False):
self._PATH_LOCAL = os_path.dirname(sys.executable)
else:

View File

@@ -284,7 +284,6 @@ class Controller:
)
elif isinstance(message, str) and len(message) > 0:
model.telemetryTouchActivity()
model.telemetryTrackCoreFeature("mic_speech_to_text")
translation = []
transliteration_message = []
@@ -457,7 +456,6 @@ class Controller:
},
)
elif isinstance(message, str) and len(message) > 0:
model.telemetryTouchActivity()
model.telemetryTrackCoreFeature("speaker_speech_to_text")
translation = []
transliteration_message = []
@@ -640,7 +638,6 @@ class Controller:
id = data["id"]
message = data["message"]
if len(message) > 0:
model.telemetryTouchActivity()
model.telemetryTrackCoreFeature("text_input")
translation = []
transliteration_message: List[Any] = []
@@ -2607,7 +2604,7 @@ class Controller:
def setEnableTelemetry(*args, **kwargs) -> dict:
if config.ENABLE_TELEMETRY is False:
config.ENABLE_TELEMETRY = True
model.telemetryInit(enabled=config.ENABLE_TELEMETRY, app_version=config.VERSION)
model.telemetryInit(enabled=True, app_version=config.VERSION)
return {"status":200, "result":config.ENABLE_TELEMETRY}
@staticmethod

View File

@@ -55,9 +55,8 @@ src-python/
│ └── telemetry/
│ ├── __init__.py # Public API + singleton管理
│ ├── client.py # Aptabase wrapper
│ ├── state.py # Enable/disable, last_activity
── heartbeat.py # 5分間隔heartbeatスレッド
│ └── core.py # イベント送信ロジック
│ ├── state.py # Enable/disable、セッション内送信済み機能管理
── core.py # イベント送信ロジック、重複検出
└── docs/
└── details/
└── telemetry.md # 実装詳細ドキュメント
@@ -67,10 +66,9 @@ src-python/
| モジュール | 責任 |
|-----------|------|
| `__init__.py` | パブリック API、シングルトン管理 |
| `client.py` | Aptabase SDK ラッパー、HTTP通信 |
| `state.py` | ON/OFF 状態、最終操作時刻管理 |
| `heartbeat.py` | 5分間隔スレッド、タイムアウト判定 |
| `__init__.py` | パブリック API、シングルトン管理、イベントループ管理 |
| `client.py` | Aptabase SDK ラッパー、HTTP通信、ログレベル設定 |
| `state.py` | ON/OFF 状態、セッション内送信済み機能の追跡 |
| `core.py` | イベント構築・送信、重複検出 |
---
@@ -83,7 +81,6 @@ src-python/
|-----------|------|----------|
| `app_started` | アプリ起動時 | `{}` |
| `app_closed` | アプリ終了時 | `{}` |
| `session_heartbeat` | 5分間隔アクティブ確認 | `{}` |
| `core_feature` | コア機能開始 | `{"feature": "translation" \| "mic_speech_to_text" \| "speaker_speech_to_text" \| "text_input"}` |
| `settings_opened` | 設定画面を開く | `{}` |
| `config_changed` | 設定変更 | `{"section": str}` |
@@ -124,37 +121,6 @@ telemetry.track_core_feature("text_input")
send_message() # 実処理
```
### Session Heartbeat 仕様
#### アクティブ判定条件
以下のいずれかが発生した場合、セッションはアクティブ
1. **テキスト入力**
- チャット送信
- メッセージボックスへの入力
2. **ASR 実処理**(マイク or スピーカー)
- マイク音声認識開始
- スピーカー音声認識開始
- 実際の音声処理UI 状態は使用しない)
#### 送信ルール
- **送信間隔**5分300秒
- **タイムアウト**最後のアクティビティから5分経過で送信停止
- **復帰条件**:アクティビティ発生 → 次の heartbeat から再開
- **無条件停止**:テレメトリ OFF 時は即座にスレッド停止
#### 実装概略図
```
Timeline:
├─ [Activity] touch_activity() →更新
├─ [300秒待機]
├─ [Check] 最後の操作 < 5分? → Yes → send heartbeat
├─ [300秒待機]
├─ [Check] 最後の操作 < 5分? → No → 待機のみ
└─ [Activity] touch_activity() →復帰 → 次heartbeat送信
```
---
## データフロー
@@ -169,7 +135,6 @@ Timeline:
3. telemetry.init(enabled=config.ENABLE_TELEMETRY)
├─ enabled=True の場合
│ ├─ Aptabase SDK 初期化
│ ├─ heartbeat スレッド開始
│ └─ app_started イベント送信
└─ enabled=False の場合
└─ すべての処理をスキップ
@@ -183,8 +148,6 @@ Timeline:
ENABLE_TELEMETRY チェック
├─ False → 何もしない
└─ True
touch_activity() → 最終時刻更新
1セッション内で同一 feature 送信済み?
├─ Yes → スキップ
@@ -208,7 +171,7 @@ OFF 状態(設定保存)
telemetry.init(enabled=True)
├─ 既知例外は握りつぶし
├─ Aptabase 初期化
├─ heartbeat スレッド開始
├─ asyncio イベントループ開始
└─ 次のイベントから通常送信
```
@@ -220,7 +183,7 @@ config.json 更新
ENABLE_TELEMETRY: false に変更
telemetry.shutdown()
├─ heartbeat スレッド停止
├─ asyncio イベントループ停止
├─ 内部状態をリセット
└─ メモリ解放
@@ -246,8 +209,9 @@ def telemetry.init(enabled: bool) -> None:
enabled (bool): テレメトリを有効にするか
Behavior:
- enabled=True: Aptabase SDK初期化、heartbeat開始、app_started送信
- enabled=True: Aptabase SDK初期化、asyncioイベントループ開始、app_started送信
- enabled=False: すべてのスキップ、以後のtrack()は何もしない
- 複数回呼び出し時: _init_called フラグで二重初期化を防止
Exception:
- SDK初期化失敗時: 例外握りつぶし、機能は停止するが無言
@@ -261,7 +225,7 @@ def telemetry.shutdown() -> None:
Behavior:
- app_closed イベント送信
- heartbeat スレッド停止
- asyncio イベントループ停止
- 内部状態をリセット
- 例外は握りつぶし(無言)
"""
@@ -316,25 +280,18 @@ def telemetry.track_core_feature(feature: str) -> None:
# =========================================================================
# ACTIVITY TRACKING
# SHUTDOWN
# =========================================================================
def telemetry.touch_activity() -> None:
def telemetry.shutdown() -> None:
"""
最終アクティビティ時刻を更新heartbeat判定用
テレメトリシャットダウン
Behavior:
- enabled=False なら何もしない
- 内部的に datetime.now() を記録
- 自動呼び出し: track_core_feature() 内から呼び出される
- 手動呼び出し: テキスト入力検出時など、明示的に呼び出し可
Example:
# テキスト入力時
telemetry.touch_activity()
# track_core_feature() は内部で自動呼び出し
telemetry.track_core_feature("translation")
- app_closed イベント送信
- イベントループ停止(フラッシュ待機)
- Aptabase クライアント停止
- Tauri sidecar 環境では呼び出し後にプロセス終了
"""
pass
@@ -360,9 +317,7 @@ def telemetry.get_state() -> dict:
Returns:
dict: {
"enabled": bool,
"last_activity": datetime | None,
"session_features_sent": list[str],
"heartbeat_active": bool,
}
"""
pass
@@ -471,8 +426,11 @@ def setUiLanguage(data):
パブリック API を提供し、内部実装を隠蔽する。
"""
import asyncio
import threading
from concurrent.futures import Future
from .state import TelemetryState
from .heartbeat import HeartbeatManager
from .core import TelemetryCore
@@ -489,56 +447,128 @@ class Telemetry:
if self._initialized:
return
self.state = TelemetryState()
self.heartbeat = HeartbeatManager(self.state)
self.core = TelemetryCore(self.state)
self._loop = None
self._loop_thread = None
self._init_called = False
self._initialized = True
def init(self, enabled: bool):
"""テレメトリ初期化"""
def _start_event_loop(self):
"""バックグラウンドでイベントループを開始"""
if self._loop_thread is not None and self._loop_thread.is_alive():
return
def run_loop():
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._loop.run_forever()
self._loop_thread = threading.Thread(target=run_loop, daemon=True, name="telemetry_loop")
self._loop_thread.start()
# ループが開始されるまで待機
while self._loop is None:
pass
def _stop_event_loop(self, timeout: float = 5.0):
"""イベントループを停止(フラッシュ完了を待つ)"""
if self._loop is None:
return
# ループにstop()を予約
self._loop.call_soon_threadsafe(self._loop.stop)
# ループスレッド終了を待機
if self._loop_thread is not None:
self._loop_thread.join(timeout=timeout)
self._loop = None
self._loop_thread = None
def _run_async(self, coro):
"""同期コンテキストから非同期関数を実行"""
if self._loop is None:
return
future = asyncio.run_coroutine_threadsafe(coro, self._loop)
try:
return future.result(timeout=5.0)
except Exception:
pass
def _schedule_async(self, coro):
"""非同期タスクをバックグラウンドでスケジュール(待機しない)"""
if self._loop is None:
return
try:
asyncio.run_coroutine_threadsafe(coro, self._loop)
except Exception:
pass
def init(self, enabled: bool, app_version: str = "1.0.0"):
"""
テレメトリ初期化(同期インターフェース)
重要:このメソッドは冪等です。複数回呼ばれても安全です。
既に初期化済みの場合は、有効/無効の状態のみを更新します。
"""
# 既に初期化済みの場合は、状態の更新のみ
if self._init_called:
self.state.set_enabled(enabled)
return
# 初回初期化
self._init_called = True
self.state.set_enabled(enabled)
if enabled:
try:
self.core.send_event("app_started")
self.heartbeat.start()
except Exception:
pass # 握りつぶし
self._start_event_loop()
self._run_async(self._init_async(app_version))
async def _init_async(self, app_version: str):
"""非同期初期化処理"""
await self.core.start(app_version=app_version)
await self.core.send_event("app_started")
def shutdown(self):
"""テレメトリ終了"""
if self.state.is_enabled():
try:
self.core.send_event("app_closed")
except Exception:
pass
self.heartbeat.stop()
self.state.reset()
try:
if self.state.is_enabled():
try:
self._run_async(self._shutdown_async())
except Exception:
pass
self._stop_event_loop(timeout=5.0)
except Exception:
pass
finally:
self.state.reset()
self._init_called = False
async def _shutdown_async(self):
"""非同期終了処理"""
await self.core.send_event("app_closed")
await self.core.stop()
def track(self, event: str, payload: dict = None):
"""汎用イベント送信"""
if not self.state.is_enabled():
return
try:
self.core.send_event(event, payload)
except Exception:
pass
self._schedule_async(self.core.send_event(event, payload))
def track_core_feature(self, feature: str):
"""コア機能イベント送信(重複検出付き)"""
"""コア機能イベント送信"""
if not self.state.is_enabled():
return
self.state.touch_activity()
if self.core.is_duplicate_core_feature(feature):
return
try:
self.core.send_event("core_feature", {"feature": feature})
self.state.record_feature_sent(feature)
except Exception:
pass
self._schedule_async(self._track_core_feature_async(feature))
def touch_activity(self):
"""アクティビティ時刻更新"""
if self.state.is_enabled():
self.state.touch_activity()
async def _track_core_feature_async(self, feature: str):
"""非同期コア機能送信処理"""
await self.core.send_event("core_feature", {"feature": feature})
self.state.record_feature_sent(feature)
def is_enabled(self) -> bool:
"""有効状態確認"""
@@ -559,19 +589,17 @@ telemetry = Telemetry()
"""
テレメトリ状態管理
- enable/disable フラグ
- 最終操作時刻
- セッション内送信済み機能リスト
"""
from datetime import datetime
from threading import Lock
class TelemetryState:
def __init__(self):
self._enabled = True # デフォルト有効
self._last_activity = None
self._session_features_sent = set()
self._lock = Lock()
self._lock = Lock()
def set_enabled(self, value: bool):
"""有効/無効設定"""
@@ -585,15 +613,20 @@ class TelemetryState:
with self._lock:
return self._enabled
def touch_activity(self):
"""最終操作時刻更新"""
def record_feature_sent(self, feature: str):
"""送信済み機能を記録"""
with self._lock:
self._last_activity = datetime.now()
self._session_features_sent.add(feature)
def get_last_activity(self) -> datetime:
"""最終操作時刻取得"""
def has_feature_been_sent(self, feature: str) -> bool:
"""機能がこのセッション内で送信済みか"""
with self._lock:
return self._last_activity
return feature in self._session_features_sent
def reset(self):
"""状態をリセット"""
with self._lock:
self._session_features_sent.clear()
def record_feature_sent(self, feature: str):
"""送信済み機能を記録"""
@@ -608,7 +641,6 @@ class TelemetryState:
def reset(self):
"""状態をリセット"""
with self._lock:
self._last_activity = None
self._session_features_sent.clear()
def get_debug_info(self) -> dict:
@@ -616,74 +648,10 @@ class TelemetryState:
with self._lock:
return {
"enabled": self._enabled,
"last_activity": self._last_activity.isoformat() if self._last_activity else None,
"session_features_sent": list(self._session_features_sent),
}
```
### `models/telemetry/heartbeat.py`
```python
"""
Heartbeat スレッド管理
- 5分間隔でアクティブ確認
- 操作なしで5分経過したら送信停止
"""
from datetime import datetime, timedelta
from threading import Thread, Event
import time
class HeartbeatManager:
INTERVAL = 300 # 5 minutes
TIMEOUT = 300 # 5 minutes
def __init__(self, state):
self.state = state
self.thread = None
self._stop_event = Event()
def start(self):
"""Heartbeat スレッド開始"""
if self.thread is not None and self.thread.is_alive():
return
self._stop_event.clear()
self.thread = Thread(target=self._run, daemon=True)
self.thread.start()
def stop(self):
"""Heartbeat スレッド停止"""
self._stop_event.set()
if self.thread is not None:
self.thread.join(timeout=1.0)
self.thread = None
def _run(self):
"""Heartbeat ループ"""
while not self._stop_event.is_set():
try:
time.sleep(self.INTERVAL)
if not self.state.is_enabled():
continue
last_activity = self.state.get_last_activity()
if last_activity is None:
continue
elapsed = (datetime.now() - last_activity).total_seconds()
if elapsed < self.TIMEOUT:
# アクティブなら heartbeat 送信
from .core import TelemetryCore
try:
core = TelemetryCore(self.state)
core.send_event("session_heartbeat")
except Exception:
pass # 握りつぶし
except Exception:
pass # スレッド不停止のため握りつぶし
```
### `models/telemetry/client.py`
```python
@@ -828,7 +796,7 @@ class Main:
th.join(timeout=remaining)
```
**重要**: `controller.shutdown()` **スレッド停止前** に呼び出す必要があります。そうしないと heartbeat スレッドが動作していない状態で shutdown に失敗する可能性があります。
**重要**: `controller.shutdown()`最初に呼び出され、その中で `model.shutdown()` が実行され、最後に `telemetry.shutdown()` が呼び出されます。
#### パターン2: Controller.setWatchdogCallback
@@ -882,7 +850,7 @@ def start(self) -> None:
main_instance.stop() 呼び出し
1. try:
telemetry.shutdown() # app_closed 送信 + heartbeat停止
self.controller.shutdown() # app_closed 送信 + すべてのクリーンアップ
except:
pass # 握りつぶし(通信失敗でも続行)
@@ -890,9 +858,7 @@ main_instance.stop() 呼び出し
3. スレッド停止待機(最大 2.0秒)
4. モデルクリーンアップif model が初期化されていれば)
5. メモリ解放
4. メモリ解放
プロセス終了
```
@@ -967,11 +933,10 @@ def stop(self, wait: float = 2.0) -> None:
def shutdown(self):
if self.state.is_enabled():
try:
self.core.send_event("app_closed") # ← 実行されない
self.core.send_event("app_closed") # ← 実行され
except Exception:
pass
self.heartbeat.stop() # OFF時も停止するスレッド安全
self.state.reset() # 状態リセット
```
@@ -1027,16 +992,6 @@ def test_track_core_feature_no_duplicate():
# 2回目はスキップ内部的に重複検出
def test_heartbeat_sends_when_active():
"""5分操作なしなら heartbeat 送信"""
# heartbeat スレッドがちゃんと5分待機後に
# touch_activity() 後のアクティブ判定で送信
def test_heartbeat_stops_when_inactive():
"""5分以上操作なしなら heartbeat 停止"""
# last_activity が 5分以上古い場合、
# heartbeat 送信しない
def test_shutdown_sends_app_closed():
"""shutdown() が app_closed を送信"""
telemetry = Telemetry()
@@ -1059,10 +1014,6 @@ def test_full_flow():
# 操作
telemetry.track_core_feature("translation")
telemetry.touch_activity()
# 5分待機テスト時は短縮
# heartbeat 送信確認
# 終了
telemetry.shutdown()
@@ -1099,7 +1050,6 @@ ENABLE_TELEMETRY = True # デフォルト有効
- [ ] イベントペイロードに個人情報が含まれていない
- [ ] マイク/スピーカーイベントが分離している
- [ ] heartbeat が UI 状態に依存していない
- [ ] OFF 時にすべての通信が停止する
- [ ] 例外が握りつぶされ、ユーザーに見えない
- [ ] Aptabase SDK の最新版を使用
@@ -1121,13 +1071,12 @@ UI に以下を表示:
|---------|--------|--------|
| 1 | `__init__.py`, `state.py` 作成 | 高 |
| 2 | `client.py`, `core.py` 作成 | 高 |
| 3 | `heartbeat.py` 作成・テスト | 高 |
| 4 | `config.py` に `ENABLE_TELEMETRY` 追加 | 高 |
| 5 | `controller.py` に API 呼び出し追加 | 高 |
| 6 | `mainloop.py` に init/shutdown 追加 | 高 |
| 7 | ユニット・統合テスト | |
| 8 | ドキュメント更新 | 中 |
| 9 | UI 文言追加 | 中 |
| 3 | `config.py` に `ENABLE_TELEMETRY` 追加 | 高 |
| 4 | `controller.py` に API 呼び出し追加 | 高 |
| 5 | `mainloop.py` に init/shutdown 追加 | 高 |
| 6 | ユニット・統合テスト | 高 |
| 7 | ドキュメント更新 | |
| 8 | UI 文言追加 | 中 |
---
@@ -1136,7 +1085,6 @@ UI に以下を表示:
- [x] テレメトリ OFF 時、一切の通信が発生しない
- [x] オフライン状態でもアプリが正常に動作する
- [x] `core_feature` は1セッション中に1回のみ送信される
- [x] heartbeat は 5分以上操作がないと送信を停止する
- [x] 個人情報・入力内容・音声データは絶対に送信されない
- [x] 送信失敗時は例外を握りつぶし、無言で続行する
- [x] マイク/スピーカーイベントが分離している
@@ -1149,3 +1097,4 @@ UI に以下を表示:
- [Aptabase Dashboard](https://aptabase.com)
- [Privacy by Design](https://en.wikipedia.org/wiki/Privacy_by_design)

View File

@@ -112,18 +112,13 @@ Aptabase を用いた匿名な使用状況データ収集。デフォルト有
### イベント種別(送信固定)
- `app_started`: アプリ起動
- `app_closed`: アプリ終了(最後のイベント)
- `session_heartbeat`: 5 分間隔アクティブ確認
- `core_feature`: 機能開始translation / mic_speech_to_text / speaker_speech_to_text / text_input
- `settings_opened`: 設定画面開閉
- `config_changed`: 設定変更
- `error`: エラー発生
### 動作フロー
1. アプリ起動時に telemetryInit() 呼び出し
2. ユーザーアクティビティ検出時に telemetryTouchActivity() 呼び出し
3. 機能開始時に track_core_feature() で 1 セッション 1 回のみ送信
4. アプリ終了時に telemetryShutdown() で app_closed 送信
5. config.ENABLE_TELEMETRY = False で一切の通信・スレッド停止
2. 機能開始時に track_core_feature() で 1 セッション 1 回のみ送信
3. アプリ終了時に telemetryShutdown() で app_closed 送信
4. config.ENABLE_TELEMETRY = False で一切の通信・スレッド停止
### 設定・制御
- `config.ENABLE_TELEMETRY`: True/False で機能制御
@@ -137,6 +132,7 @@ Aptabase を用いた匿名な使用状況データ収集。デフォルト有
- API 通信失敗時: 例外握りつぶし、アプリ動作に影響なし
- オフライン時: 機能停止のみ、再送・バッファリングなし
- 無効化時: 一切の通信・スレッド・処理停止
- Aptabase SDK ログ: CRITICAL レベルのみに制限(ノイズ削減)
## モデル重みダウンロード
- `models.translation.translation_utils``models.transcription.transcription_whisper` にダウンロード/チェック関数があり、チェックサムやファイル存在を検証する。

View File

@@ -1331,9 +1331,4 @@ class Model:
if hasattr(self, "telemetry") and self.telemetry:
self.telemetry.track_core_feature(feature)
def telemetryTouchActivity(self):
"""テレメトリアクティビティ更新 (Model ラッパー)"""
if hasattr(self, "telemetry") and self.telemetry:
self.telemetry.touch_activity()
model = Model()

View File

@@ -10,7 +10,6 @@ from concurrent.futures import Future
# Allow running as a script for quick verification.
try:
from .state import TelemetryState
from .heartbeat import HeartbeatManager
from .core import TelemetryCore
except ImportError:
import os
@@ -18,7 +17,6 @@ except ImportError:
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from models.telemetry.state import TelemetryState
from models.telemetry.heartbeat import HeartbeatManager
from models.telemetry.core import TelemetryCore
@@ -36,9 +34,9 @@ class Telemetry:
return
self.state = TelemetryState()
self.core = TelemetryCore(self.state)
self.heartbeat = HeartbeatManager(self.state, self.core, self._schedule_async_with_loop)
self._loop = None
self._loop_thread = None
self._init_called = False # init()が呼ばれたかを追跡(重複初期化防止)
self._initialized = True
def _start_event_loop(self):
@@ -96,12 +94,20 @@ class Telemetry:
except Exception:
pass
def _schedule_async_with_loop(self, coro):
"""ループ取得とスケジューリングのヘルパーheartbeat用"""
return self._schedule_async(coro)
def init(self, enabled: bool, app_version: str = "1.0.0"):
"""テレメトリ初期化(同期インターフェース)"""
"""テレメトリ初期化(同期インターフェース)
重要:このメソッドは冪等です。複数回呼ばれても安全です。
既に初期化済みの場合は、有効/無効の状態のみを更新します。
これにより、設定変更時などに誤ってapp_startedイベントが重複送信される問題を防ぎます。
"""
# 既に初期化済みの場合は、状態の更新のみ
if self._init_called:
self.state.set_enabled(enabled)
return
# 初回初期化
self._init_called = True
self.state.set_enabled(enabled)
if enabled:
self._start_event_loop()
@@ -111,23 +117,18 @@ class Telemetry:
"""非同期初期化処理"""
await self.core.start(app_version=app_version)
await self.core.send_event("app_started")
self.heartbeat.start()
def shutdown(self):
"""テレメトリ終了(同期インターフェース)
重要Tauri sidecar環境では、このメソッド実行後にプロセス終了が発生します。
app_closed イベントが確実に送信されるように、以下の手順を実行します:
1. heartbeat スレッド停止
2. app_closed イベント送信を同期待機
3. Aptabase クライアント停止(フラッシュ含む)
4. イベントループ完全停止を待機
1. app_closed イベント送信を同期待機
2. Aptabase クライアント停止(フラッシュ含む)
3. イベントループ完全停止を待機
"""
try:
# Step 1: Heartbeat 停止5分待機中の送信を防ぐ
self.heartbeat.stop()
# Step 2-3: app_closed 送信とクライアント停止を同期待機
# app_closed 送信とクライアント停止を同期待機
if self.state.is_enabled():
try:
# _run_async で最大5秒間待機Aptabase のフラッシュ含む)
@@ -135,7 +136,7 @@ class Telemetry:
except Exception:
pass
# Step 4: イベントループを完全停止(フラッシュ確認を待つ)
# イベントループを完全停止(フラッシュ確認を待つ)
# sidecar 終了前に確実に完了させるため、タイムアウトを長めに設定
self._stop_event_loop(timeout=5.0)
except Exception:
@@ -144,6 +145,7 @@ class Telemetry:
finally:
# 状態をリセット
self.state.reset()
self._init_called = False # 初期化フラグもリセット
async def _shutdown_async(self):
"""非同期終了処理"""
@@ -160,7 +162,6 @@ class Telemetry:
"""コア機能イベント送信(同期インターフェース)"""
if not self.state.is_enabled():
return
self.state.touch_activity()
if self.core.is_duplicate_core_feature(feature):
return
self._schedule_async(self._track_core_feature_async(feature))
@@ -170,11 +171,6 @@ class Telemetry:
await self.core.send_event("core_feature", {"feature": feature})
self.state.record_feature_sent(feature)
def touch_activity(self):
"""アクティビティ時刻更新"""
if self.state.is_enabled():
self.state.touch_activity()
def is_enabled(self) -> bool:
"""有効状態確認"""
return self.state.is_enabled()
@@ -190,7 +186,6 @@ if __name__ == "__main__":
telemetry.init(enabled=True)
telemetry.track("debug_test", {"message": "telemetry main demo"})
telemetry.track_core_feature("text_input")
telemetry.touch_activity()
print("state:", telemetry.get_state())
# イベント送信完了を待つ

View File

@@ -1,6 +1,7 @@
"""
Aptabase SDK ラッパー(非同期版)
"""
import logging
from typing import Optional, Dict, Any
# Aptabase SDK のインポート
@@ -11,10 +12,12 @@ except ImportError:
class AptabaseWrapper:
APP_KEY = "A-US-2082730845"
APP_KEY = "A-US-6044063021"
def __init__(self):
self.client = None
# Suppress noisy logs from the Aptabase SDK (only CRITICAL allowed)
logging.getLogger("aptabase").setLevel(logging.CRITICAL)
async def start(self, app_version: str = "1.0.0"):
"""Aptabase クライアント開始"""

View File

@@ -1,53 +0,0 @@
"""
Heartbeat スレッド管理
- 5分間隔でアクティブ確認
- 操作なしで5分経過したら送信停止
"""
from datetime import datetime
from threading import Thread, Event
import time
class HeartbeatManager:
INTERVAL = 300 # 5 minutes
TIMEOUT = 300 # 5 minutes
def __init__(self, state, core, schedule_async):
self.state = state
self.core = core
self.schedule_async = schedule_async
self.thread = None
self._stop_event = Event()
def start(self):
"""Heartbeat スレッド開始"""
if self.thread is not None and self.thread.is_alive():
return
self._stop_event.clear()
self.thread = Thread(target=self._run, daemon=True, name="telemetry_heartbeat")
self.thread.start()
def stop(self):
"""Heartbeat スレッド停止"""
self._stop_event.set()
if self.thread is not None:
self.thread.join(timeout=1.0)
self.thread = None
def _run(self):
"""Heartbeat ループ"""
while not self._stop_event.is_set():
time.sleep(self.INTERVAL)
if not self.state.is_enabled():
continue
last_activity = self.state.get_last_activity()
if last_activity is None:
continue
elapsed = (datetime.now() - last_activity).total_seconds()
if elapsed < self.TIMEOUT:
# アクティブなら heartbeat 送信(非同期スケジュール)
if self.core.client is not None:
self.schedule_async(self.core.send_event("session_heartbeat"))

View File

@@ -11,7 +11,6 @@ from threading import Lock
class TelemetryState:
def __init__(self):
self._enabled = True # デフォルト有効
self._last_activity = None
self._session_features_sent = set()
self._lock = Lock()
@@ -27,16 +26,6 @@ class TelemetryState:
with self._lock:
return self._enabled
def touch_activity(self):
"""最終操作時刻更新"""
with self._lock:
self._last_activity = datetime.now()
def get_last_activity(self):
"""最終操作時刻取得"""
with self._lock:
return self._last_activity
def record_feature_sent(self, feature: str):
"""送信済み機能を記録"""
with self._lock:
@@ -50,7 +39,6 @@ class TelemetryState:
def reset(self):
"""状態をリセット"""
with self._lock:
self._last_activity = None
self._session_features_sent.clear()
def get_debug_info(self) -> dict:
@@ -58,6 +46,5 @@ class TelemetryState:
with self._lock:
return {
"enabled": self._enabled,
"last_activity": self._last_activity.isoformat() if self._last_activity else None,
"session_features_sent": list(self._session_features_sent),
}

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "VRCT",
"version": "3.4.0",
"version": "3.4.2",
"identifier": "com.vrct.app",
"build": {
"beforeDevCommand": "",

View File

@@ -202,6 +202,7 @@ export const SETTINGS_ARRAY = [
default_value: "",
ui_template_id: "select",
logics_template_id: "get_set",
add_endpoint_run_array: ["from_backend"],
base_endpoint_name: "selected_translation_compute_type",
},
{
@@ -517,6 +518,7 @@ export const SETTINGS_ARRAY = [
default_value: "",
ui_template_id: "select",
logics_template_id: "get_set",
add_endpoint_run_array: ["from_backend"],
base_endpoint_name: "selected_transcription_compute_type",
},
{

View File

@@ -99,12 +99,12 @@ const OpenVrcMicMuteSyncQuickSetting = () => {
};
const SoftwareUpdateAvailableButton = () => {
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
const { t } = useI18n();
if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null;
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null;
return (
<button className={styles.software_update_button} onClick={()=>updateOpenedQuickSetting("update_software")}>
<RefreshSvg className={styles.refresh_svg}/>