Compare commits
10 Commits
597b0e15b0
...
942a257e79
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942a257e79 | ||
|
|
6b2be0f22b | ||
|
|
242c022e33 | ||
|
|
0765908184 | ||
|
|
f6a3b0e8fd | ||
|
|
5811158231 | ||
|
|
0cb2b7e6ae | ||
|
|
623d17e885 | ||
|
|
4c95b66dd8 | ||
|
|
1656620ce4 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vrct",
|
||||
"private": true,
|
||||
"version": "3.4.0",
|
||||
"version": "3.4.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"setup-python": "bat\\install.bat",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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` にダウンロード/チェック関数があり、チェックサムやファイル存在を検証する。
|
||||
|
||||
@@ -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()
|
||||
@@ -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())
|
||||
|
||||
# イベント送信完了を待つ
|
||||
|
||||
@@ -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 クライアント開始"""
|
||||
|
||||
@@ -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"))
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}/>
|
||||
|
||||
Reference in New Issue
Block a user