Compare commits
10 Commits
597b0e15b0
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
942a257e79 | ||
|
|
6b2be0f22b | ||
|
|
242c022e33 | ||
|
|
0765908184 | ||
|
|
f6a3b0e8fd | ||
|
|
5811158231 | ||
|
|
0cb2b7e6ae | ||
|
|
623d17e885 | ||
|
|
4c95b66dd8 | ||
|
|
1656620ce4 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "vrct",
|
"name": "vrct",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "3.4.0",
|
"version": "3.4.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"setup-python": "bat\\install.bat",
|
"setup-python": "bat\\install.bat",
|
||||||
|
|||||||
@@ -764,7 +764,7 @@ class Config:
|
|||||||
|
|
||||||
def init_config(self):
|
def init_config(self):
|
||||||
# Read Only
|
# Read Only
|
||||||
self._VERSION = "3.4.0"
|
self._VERSION = "3.4.2"
|
||||||
if getattr(sys, 'frozen', False):
|
if getattr(sys, 'frozen', False):
|
||||||
self._PATH_LOCAL = os_path.dirname(sys.executable)
|
self._PATH_LOCAL = os_path.dirname(sys.executable)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -284,7 +284,6 @@ class Controller:
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(message, str) and len(message) > 0:
|
elif isinstance(message, str) and len(message) > 0:
|
||||||
model.telemetryTouchActivity()
|
|
||||||
model.telemetryTrackCoreFeature("mic_speech_to_text")
|
model.telemetryTrackCoreFeature("mic_speech_to_text")
|
||||||
translation = []
|
translation = []
|
||||||
transliteration_message = []
|
transliteration_message = []
|
||||||
@@ -457,7 +456,6 @@ class Controller:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
elif isinstance(message, str) and len(message) > 0:
|
elif isinstance(message, str) and len(message) > 0:
|
||||||
model.telemetryTouchActivity()
|
|
||||||
model.telemetryTrackCoreFeature("speaker_speech_to_text")
|
model.telemetryTrackCoreFeature("speaker_speech_to_text")
|
||||||
translation = []
|
translation = []
|
||||||
transliteration_message = []
|
transliteration_message = []
|
||||||
@@ -640,7 +638,6 @@ class Controller:
|
|||||||
id = data["id"]
|
id = data["id"]
|
||||||
message = data["message"]
|
message = data["message"]
|
||||||
if len(message) > 0:
|
if len(message) > 0:
|
||||||
model.telemetryTouchActivity()
|
|
||||||
model.telemetryTrackCoreFeature("text_input")
|
model.telemetryTrackCoreFeature("text_input")
|
||||||
translation = []
|
translation = []
|
||||||
transliteration_message: List[Any] = []
|
transliteration_message: List[Any] = []
|
||||||
@@ -2607,7 +2604,7 @@ class Controller:
|
|||||||
def setEnableTelemetry(*args, **kwargs) -> dict:
|
def setEnableTelemetry(*args, **kwargs) -> dict:
|
||||||
if config.ENABLE_TELEMETRY is False:
|
if config.ENABLE_TELEMETRY is False:
|
||||||
config.ENABLE_TELEMETRY = True
|
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}
|
return {"status":200, "result":config.ENABLE_TELEMETRY}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -55,9 +55,8 @@ src-python/
|
|||||||
│ └── telemetry/
|
│ └── telemetry/
|
||||||
│ ├── __init__.py # Public API + singleton管理
|
│ ├── __init__.py # Public API + singleton管理
|
||||||
│ ├── client.py # Aptabase wrapper
|
│ ├── client.py # Aptabase wrapper
|
||||||
│ ├── state.py # Enable/disable, last_activity
|
│ ├── state.py # Enable/disable、セッション内送信済み機能管理
|
||||||
│ ├── heartbeat.py # 5分間隔heartbeatスレッド
|
│ └── core.py # イベント送信ロジック、重複検出
|
||||||
│ └── core.py # イベント送信ロジック
|
|
||||||
└── docs/
|
└── docs/
|
||||||
└── details/
|
└── details/
|
||||||
└── telemetry.md # 実装詳細ドキュメント
|
└── telemetry.md # 実装詳細ドキュメント
|
||||||
@@ -67,10 +66,9 @@ src-python/
|
|||||||
|
|
||||||
| モジュール | 責任 |
|
| モジュール | 責任 |
|
||||||
|-----------|------|
|
|-----------|------|
|
||||||
| `__init__.py` | パブリック API、シングルトン管理 |
|
| `__init__.py` | パブリック API、シングルトン管理、イベントループ管理 |
|
||||||
| `client.py` | Aptabase SDK ラッパー、HTTP通信 |
|
| `client.py` | Aptabase SDK ラッパー、HTTP通信、ログレベル設定 |
|
||||||
| `state.py` | ON/OFF 状態、最終操作時刻管理 |
|
| `state.py` | ON/OFF 状態、セッション内送信済み機能の追跡 |
|
||||||
| `heartbeat.py` | 5分間隔スレッド、タイムアウト判定 |
|
|
||||||
| `core.py` | イベント構築・送信、重複検出 |
|
| `core.py` | イベント構築・送信、重複検出 |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -83,7 +81,6 @@ src-python/
|
|||||||
|-----------|------|----------|
|
|-----------|------|----------|
|
||||||
| `app_started` | アプリ起動時 | `{}` |
|
| `app_started` | アプリ起動時 | `{}` |
|
||||||
| `app_closed` | アプリ終了時 | `{}` |
|
| `app_closed` | アプリ終了時 | `{}` |
|
||||||
| `session_heartbeat` | 5分間隔アクティブ確認 | `{}` |
|
|
||||||
| `core_feature` | コア機能開始 | `{"feature": "translation" \| "mic_speech_to_text" \| "speaker_speech_to_text" \| "text_input"}` |
|
| `core_feature` | コア機能開始 | `{"feature": "translation" \| "mic_speech_to_text" \| "speaker_speech_to_text" \| "text_input"}` |
|
||||||
| `settings_opened` | 設定画面を開く | `{}` |
|
| `settings_opened` | 設定画面を開く | `{}` |
|
||||||
| `config_changed` | 設定変更 | `{"section": str}` |
|
| `config_changed` | 設定変更 | `{"section": str}` |
|
||||||
@@ -124,37 +121,6 @@ telemetry.track_core_feature("text_input")
|
|||||||
send_message() # 実処理
|
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)
|
3. telemetry.init(enabled=config.ENABLE_TELEMETRY)
|
||||||
├─ enabled=True の場合
|
├─ enabled=True の場合
|
||||||
│ ├─ Aptabase SDK 初期化
|
│ ├─ Aptabase SDK 初期化
|
||||||
│ ├─ heartbeat スレッド開始
|
|
||||||
│ └─ app_started イベント送信
|
│ └─ app_started イベント送信
|
||||||
└─ enabled=False の場合
|
└─ enabled=False の場合
|
||||||
└─ すべての処理をスキップ
|
└─ すべての処理をスキップ
|
||||||
@@ -183,8 +148,6 @@ Timeline:
|
|||||||
ENABLE_TELEMETRY チェック
|
ENABLE_TELEMETRY チェック
|
||||||
├─ False → 何もしない
|
├─ False → 何もしない
|
||||||
└─ True
|
└─ True
|
||||||
↓
|
|
||||||
touch_activity() → 最終時刻更新
|
|
||||||
↓
|
↓
|
||||||
1セッション内で同一 feature 送信済み?
|
1セッション内で同一 feature 送信済み?
|
||||||
├─ Yes → スキップ
|
├─ Yes → スキップ
|
||||||
@@ -208,7 +171,7 @@ OFF 状態(設定保存)
|
|||||||
telemetry.init(enabled=True)
|
telemetry.init(enabled=True)
|
||||||
├─ 既知例外は握りつぶし
|
├─ 既知例外は握りつぶし
|
||||||
├─ Aptabase 初期化
|
├─ Aptabase 初期化
|
||||||
├─ heartbeat スレッド開始
|
├─ asyncio イベントループ開始
|
||||||
└─ 次のイベントから通常送信
|
└─ 次のイベントから通常送信
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -220,7 +183,7 @@ config.json 更新
|
|||||||
ENABLE_TELEMETRY: false に変更
|
ENABLE_TELEMETRY: false に変更
|
||||||
↓
|
↓
|
||||||
telemetry.shutdown()
|
telemetry.shutdown()
|
||||||
├─ heartbeat スレッド停止
|
├─ asyncio イベントループ停止
|
||||||
├─ 内部状態をリセット
|
├─ 内部状態をリセット
|
||||||
└─ メモリ解放
|
└─ メモリ解放
|
||||||
↓
|
↓
|
||||||
@@ -246,8 +209,9 @@ def telemetry.init(enabled: bool) -> None:
|
|||||||
enabled (bool): テレメトリを有効にするか
|
enabled (bool): テレメトリを有効にするか
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
- enabled=True: Aptabase SDK初期化、heartbeat開始、app_started送信
|
- enabled=True: Aptabase SDK初期化、asyncioイベントループ開始、app_started送信
|
||||||
- enabled=False: すべてのスキップ、以後のtrack()は何もしない
|
- enabled=False: すべてのスキップ、以後のtrack()は何もしない
|
||||||
|
- 複数回呼び出し時: _init_called フラグで二重初期化を防止
|
||||||
|
|
||||||
Exception:
|
Exception:
|
||||||
- SDK初期化失敗時: 例外握りつぶし、機能は停止するが無言
|
- SDK初期化失敗時: 例外握りつぶし、機能は停止するが無言
|
||||||
@@ -261,7 +225,7 @@ def telemetry.shutdown() -> None:
|
|||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
- app_closed イベント送信
|
- 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:
|
Behavior:
|
||||||
- enabled=False なら何もしない
|
- app_closed イベント送信
|
||||||
- 内部的に datetime.now() を記録
|
- イベントループ停止(フラッシュ待機)
|
||||||
- 自動呼び出し: track_core_feature() 内から呼び出される
|
- Aptabase クライアント停止
|
||||||
- 手動呼び出し: テキスト入力検出時など、明示的に呼び出し可
|
- Tauri sidecar 環境では呼び出し後にプロセス終了
|
||||||
|
|
||||||
Example:
|
|
||||||
# テキスト入力時
|
|
||||||
telemetry.touch_activity()
|
|
||||||
|
|
||||||
# track_core_feature() は内部で自動呼び出し
|
|
||||||
telemetry.track_core_feature("translation")
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -360,9 +317,7 @@ def telemetry.get_state() -> dict:
|
|||||||
Returns:
|
Returns:
|
||||||
dict: {
|
dict: {
|
||||||
"enabled": bool,
|
"enabled": bool,
|
||||||
"last_activity": datetime | None,
|
|
||||||
"session_features_sent": list[str],
|
"session_features_sent": list[str],
|
||||||
"heartbeat_active": bool,
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
@@ -471,8 +426,11 @@ def setUiLanguage(data):
|
|||||||
|
|
||||||
パブリック API を提供し、内部実装を隠蔽する。
|
パブリック API を提供し、内部実装を隠蔽する。
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
from concurrent.futures import Future
|
||||||
|
|
||||||
from .state import TelemetryState
|
from .state import TelemetryState
|
||||||
from .heartbeat import HeartbeatManager
|
|
||||||
from .core import TelemetryCore
|
from .core import TelemetryCore
|
||||||
|
|
||||||
|
|
||||||
@@ -489,56 +447,128 @@ class Telemetry:
|
|||||||
if self._initialized:
|
if self._initialized:
|
||||||
return
|
return
|
||||||
self.state = TelemetryState()
|
self.state = TelemetryState()
|
||||||
self.heartbeat = HeartbeatManager(self.state)
|
|
||||||
self.core = TelemetryCore(self.state)
|
self.core = TelemetryCore(self.state)
|
||||||
|
self._loop = None
|
||||||
|
self._loop_thread = None
|
||||||
|
self._init_called = False
|
||||||
self._initialized = True
|
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)
|
self.state.set_enabled(enabled)
|
||||||
if enabled:
|
if enabled:
|
||||||
try:
|
self._start_event_loop()
|
||||||
self.core.send_event("app_started")
|
self._run_async(self._init_async(app_version))
|
||||||
self.heartbeat.start()
|
|
||||||
except Exception:
|
async def _init_async(self, app_version: str):
|
||||||
pass # 握りつぶし
|
"""非同期初期化処理"""
|
||||||
|
await self.core.start(app_version=app_version)
|
||||||
|
await self.core.send_event("app_started")
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""テレメトリ終了"""
|
"""テレメトリ終了"""
|
||||||
|
try:
|
||||||
if self.state.is_enabled():
|
if self.state.is_enabled():
|
||||||
try:
|
try:
|
||||||
self.core.send_event("app_closed")
|
self._run_async(self._shutdown_async())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
self.heartbeat.stop()
|
|
||||||
|
self._stop_event_loop(timeout=5.0)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
self.state.reset()
|
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):
|
def track(self, event: str, payload: dict = None):
|
||||||
"""汎用イベント送信"""
|
"""汎用イベント送信"""
|
||||||
if not self.state.is_enabled():
|
if not self.state.is_enabled():
|
||||||
return
|
return
|
||||||
try:
|
self._schedule_async(self.core.send_event(event, payload))
|
||||||
self.core.send_event(event, payload)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def track_core_feature(self, feature: str):
|
def track_core_feature(self, feature: str):
|
||||||
"""コア機能イベント送信(重複検出付き)"""
|
"""コア機能イベント送信"""
|
||||||
if not self.state.is_enabled():
|
if not self.state.is_enabled():
|
||||||
return
|
return
|
||||||
self.state.touch_activity()
|
|
||||||
if self.core.is_duplicate_core_feature(feature):
|
if self.core.is_duplicate_core_feature(feature):
|
||||||
return
|
return
|
||||||
try:
|
self._schedule_async(self._track_core_feature_async(feature))
|
||||||
self.core.send_event("core_feature", {"feature": feature})
|
|
||||||
self.state.record_feature_sent(feature)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def touch_activity(self):
|
async def _track_core_feature_async(self, feature: str):
|
||||||
"""アクティビティ時刻更新"""
|
"""非同期コア機能送信処理"""
|
||||||
if self.state.is_enabled():
|
await self.core.send_event("core_feature", {"feature": feature})
|
||||||
self.state.touch_activity()
|
self.state.record_feature_sent(feature)
|
||||||
|
|
||||||
def is_enabled(self) -> bool:
|
def is_enabled(self) -> bool:
|
||||||
"""有効状態確認"""
|
"""有効状態確認"""
|
||||||
@@ -559,19 +589,17 @@ telemetry = Telemetry()
|
|||||||
"""
|
"""
|
||||||
テレメトリ状態管理
|
テレメトリ状態管理
|
||||||
- enable/disable フラグ
|
- enable/disable フラグ
|
||||||
- 最終操作時刻
|
|
||||||
- セッション内送信済み機能リスト
|
- セッション内送信済み機能リスト
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
|
|
||||||
|
|
||||||
class TelemetryState:
|
class TelemetryState:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._enabled = True # デフォルト有効
|
self._enabled = True # デフォルト有効
|
||||||
self._last_activity = None
|
|
||||||
self._session_features_sent = set()
|
self._session_features_sent = set()
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
|
self._lock = Lock()
|
||||||
|
|
||||||
def set_enabled(self, value: bool):
|
def set_enabled(self, value: bool):
|
||||||
"""有効/無効設定"""
|
"""有効/無効設定"""
|
||||||
@@ -585,15 +613,20 @@ class TelemetryState:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return self._enabled
|
return self._enabled
|
||||||
|
|
||||||
def touch_activity(self):
|
def record_feature_sent(self, feature: str):
|
||||||
"""最終操作時刻更新"""
|
"""送信済み機能を記録"""
|
||||||
with self._lock:
|
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:
|
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):
|
def record_feature_sent(self, feature: str):
|
||||||
"""送信済み機能を記録"""
|
"""送信済み機能を記録"""
|
||||||
@@ -608,7 +641,6 @@ class TelemetryState:
|
|||||||
def reset(self):
|
def reset(self):
|
||||||
"""状態をリセット"""
|
"""状態をリセット"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._last_activity = None
|
|
||||||
self._session_features_sent.clear()
|
self._session_features_sent.clear()
|
||||||
|
|
||||||
def get_debug_info(self) -> dict:
|
def get_debug_info(self) -> dict:
|
||||||
@@ -616,74 +648,10 @@ class TelemetryState:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return {
|
return {
|
||||||
"enabled": self._enabled,
|
"enabled": self._enabled,
|
||||||
"last_activity": self._last_activity.isoformat() if self._last_activity else None,
|
|
||||||
"session_features_sent": list(self._session_features_sent),
|
"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`
|
### `models/telemetry/client.py`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -828,7 +796,7 @@ class Main:
|
|||||||
th.join(timeout=remaining)
|
th.join(timeout=remaining)
|
||||||
```
|
```
|
||||||
|
|
||||||
**重要**: `controller.shutdown()` は **スレッド停止前** に呼び出す必要があります。そうしないと heartbeat スレッドが動作していない状態で shutdown に失敗する可能性があります。
|
**重要**: `controller.shutdown()` は最初に呼び出され、その中で `model.shutdown()` が実行され、最後に `telemetry.shutdown()` が呼び出されます。
|
||||||
|
|
||||||
#### パターン2: Controller.setWatchdogCallback
|
#### パターン2: Controller.setWatchdogCallback
|
||||||
|
|
||||||
@@ -882,7 +850,7 @@ def start(self) -> None:
|
|||||||
main_instance.stop() 呼び出し
|
main_instance.stop() 呼び出し
|
||||||
↓
|
↓
|
||||||
1. try:
|
1. try:
|
||||||
telemetry.shutdown() # app_closed 送信 + heartbeat停止
|
self.controller.shutdown() # app_closed 送信 + すべてのクリーンアップ
|
||||||
except:
|
except:
|
||||||
pass # 握りつぶし(通信失敗でも続行)
|
pass # 握りつぶし(通信失敗でも続行)
|
||||||
↓
|
↓
|
||||||
@@ -890,9 +858,7 @@ main_instance.stop() 呼び出し
|
|||||||
↓
|
↓
|
||||||
3. スレッド停止待機(最大 2.0秒)
|
3. スレッド停止待機(最大 2.0秒)
|
||||||
↓
|
↓
|
||||||
4. モデルクリーンアップ(if model が初期化されていれば)
|
4. メモリ解放
|
||||||
↓
|
|
||||||
5. メモリ解放
|
|
||||||
↓
|
↓
|
||||||
プロセス終了
|
プロセス終了
|
||||||
```
|
```
|
||||||
@@ -967,11 +933,10 @@ def stop(self, wait: float = 2.0) -> None:
|
|||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if self.state.is_enabled():
|
if self.state.is_enabled():
|
||||||
try:
|
try:
|
||||||
self.core.send_event("app_closed") # ← 実行されない
|
self.core.send_event("app_closed") # ← 実行される
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.heartbeat.stop() # OFF時も停止する(スレッド安全)
|
|
||||||
self.state.reset() # 状態リセット
|
self.state.reset() # 状態リセット
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1027,16 +992,6 @@ def test_track_core_feature_no_duplicate():
|
|||||||
|
|
||||||
# 2回目はスキップ(内部的に重複検出)
|
# 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():
|
def test_shutdown_sends_app_closed():
|
||||||
"""shutdown() が app_closed を送信"""
|
"""shutdown() が app_closed を送信"""
|
||||||
telemetry = Telemetry()
|
telemetry = Telemetry()
|
||||||
@@ -1059,10 +1014,6 @@ def test_full_flow():
|
|||||||
|
|
||||||
# 操作
|
# 操作
|
||||||
telemetry.track_core_feature("translation")
|
telemetry.track_core_feature("translation")
|
||||||
telemetry.touch_activity()
|
|
||||||
|
|
||||||
# 5分待機(テスト時は短縮)
|
|
||||||
# heartbeat 送信確認
|
|
||||||
|
|
||||||
# 終了
|
# 終了
|
||||||
telemetry.shutdown()
|
telemetry.shutdown()
|
||||||
@@ -1099,7 +1050,6 @@ ENABLE_TELEMETRY = True # デフォルト有効
|
|||||||
|
|
||||||
- [ ] イベントペイロードに個人情報が含まれていない
|
- [ ] イベントペイロードに個人情報が含まれていない
|
||||||
- [ ] マイク/スピーカーイベントが分離している
|
- [ ] マイク/スピーカーイベントが分離している
|
||||||
- [ ] heartbeat が UI 状態に依存していない
|
|
||||||
- [ ] OFF 時にすべての通信が停止する
|
- [ ] OFF 時にすべての通信が停止する
|
||||||
- [ ] 例外が握りつぶされ、ユーザーに見えない
|
- [ ] 例外が握りつぶされ、ユーザーに見えない
|
||||||
- [ ] Aptabase SDK の最新版を使用
|
- [ ] Aptabase SDK の最新版を使用
|
||||||
@@ -1121,13 +1071,12 @@ UI に以下を表示:
|
|||||||
|---------|--------|--------|
|
|---------|--------|--------|
|
||||||
| 1 | `__init__.py`, `state.py` 作成 | 高 |
|
| 1 | `__init__.py`, `state.py` 作成 | 高 |
|
||||||
| 2 | `client.py`, `core.py` 作成 | 高 |
|
| 2 | `client.py`, `core.py` 作成 | 高 |
|
||||||
| 3 | `heartbeat.py` 作成・テスト | 高 |
|
| 3 | `config.py` に `ENABLE_TELEMETRY` 追加 | 高 |
|
||||||
| 4 | `config.py` に `ENABLE_TELEMETRY` 追加 | 高 |
|
| 4 | `controller.py` に API 呼び出し追加 | 高 |
|
||||||
| 5 | `controller.py` に API 呼び出し追加 | 高 |
|
| 5 | `mainloop.py` に init/shutdown 追加 | 高 |
|
||||||
| 6 | `mainloop.py` に init/shutdown 追加 | 高 |
|
| 6 | ユニット・統合テスト | 高 |
|
||||||
| 7 | ユニット・統合テスト | 高 |
|
| 7 | ドキュメント更新 | 中 |
|
||||||
| 8 | ドキュメント更新 | 中 |
|
| 8 | UI 文言追加 | 中 |
|
||||||
| 9 | UI 文言追加 | 中 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1136,7 +1085,6 @@ UI に以下を表示:
|
|||||||
- [x] テレメトリ OFF 時、一切の通信が発生しない
|
- [x] テレメトリ OFF 時、一切の通信が発生しない
|
||||||
- [x] オフライン状態でもアプリが正常に動作する
|
- [x] オフライン状態でもアプリが正常に動作する
|
||||||
- [x] `core_feature` は1セッション中に1回のみ送信される
|
- [x] `core_feature` は1セッション中に1回のみ送信される
|
||||||
- [x] heartbeat は 5分以上操作がないと送信を停止する
|
|
||||||
- [x] 個人情報・入力内容・音声データは絶対に送信されない
|
- [x] 個人情報・入力内容・音声データは絶対に送信されない
|
||||||
- [x] 送信失敗時は例外を握りつぶし、無言で続行する
|
- [x] 送信失敗時は例外を握りつぶし、無言で続行する
|
||||||
- [x] マイク/スピーカーイベントが分離している
|
- [x] マイク/スピーカーイベントが分離している
|
||||||
@@ -1149,3 +1097,4 @@ UI に以下を表示:
|
|||||||
- [Aptabase Dashboard](https://aptabase.com)
|
- [Aptabase Dashboard](https://aptabase.com)
|
||||||
- [Privacy by Design](https://en.wikipedia.org/wiki/Privacy_by_design)
|
- [Privacy by Design](https://en.wikipedia.org/wiki/Privacy_by_design)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -112,18 +112,13 @@ Aptabase を用いた匿名な使用状況データ収集。デフォルト有
|
|||||||
### イベント種別(送信固定)
|
### イベント種別(送信固定)
|
||||||
- `app_started`: アプリ起動
|
- `app_started`: アプリ起動
|
||||||
- `app_closed`: アプリ終了(最後のイベント)
|
- `app_closed`: アプリ終了(最後のイベント)
|
||||||
- `session_heartbeat`: 5 分間隔アクティブ確認
|
|
||||||
- `core_feature`: 機能開始(translation / mic_speech_to_text / speaker_speech_to_text / text_input)
|
- `core_feature`: 機能開始(translation / mic_speech_to_text / speaker_speech_to_text / text_input)
|
||||||
- `settings_opened`: 設定画面開閉
|
|
||||||
- `config_changed`: 設定変更
|
|
||||||
- `error`: エラー発生
|
|
||||||
|
|
||||||
### 動作フロー
|
### 動作フロー
|
||||||
1. アプリ起動時に telemetryInit() 呼び出し
|
1. アプリ起動時に telemetryInit() 呼び出し
|
||||||
2. ユーザーアクティビティ検出時に telemetryTouchActivity() 呼び出し
|
2. 機能開始時に track_core_feature() で 1 セッション 1 回のみ送信
|
||||||
3. 機能開始時に track_core_feature() で 1 セッション 1 回のみ送信
|
3. アプリ終了時に telemetryShutdown() で app_closed 送信
|
||||||
4. アプリ終了時に telemetryShutdown() で app_closed 送信
|
4. config.ENABLE_TELEMETRY = False で一切の通信・スレッド停止
|
||||||
5. config.ENABLE_TELEMETRY = False で一切の通信・スレッド停止
|
|
||||||
|
|
||||||
### 設定・制御
|
### 設定・制御
|
||||||
- `config.ENABLE_TELEMETRY`: True/False で機能制御
|
- `config.ENABLE_TELEMETRY`: True/False で機能制御
|
||||||
@@ -137,6 +132,7 @@ Aptabase を用いた匿名な使用状況データ収集。デフォルト有
|
|||||||
- API 通信失敗時: 例外握りつぶし、アプリ動作に影響なし
|
- API 通信失敗時: 例外握りつぶし、アプリ動作に影響なし
|
||||||
- オフライン時: 機能停止のみ、再送・バッファリングなし
|
- オフライン時: 機能停止のみ、再送・バッファリングなし
|
||||||
- 無効化時: 一切の通信・スレッド・処理停止
|
- 無効化時: 一切の通信・スレッド・処理停止
|
||||||
|
- Aptabase SDK ログ: CRITICAL レベルのみに制限(ノイズ削減)
|
||||||
|
|
||||||
## モデル重みダウンロード
|
## モデル重みダウンロード
|
||||||
- `models.translation.translation_utils` と `models.transcription.transcription_whisper` にダウンロード/チェック関数があり、チェックサムやファイル存在を検証する。
|
- `models.translation.translation_utils` と `models.transcription.transcription_whisper` にダウンロード/チェック関数があり、チェックサムやファイル存在を検証する。
|
||||||
|
|||||||
@@ -1331,9 +1331,4 @@ class Model:
|
|||||||
if hasattr(self, "telemetry") and self.telemetry:
|
if hasattr(self, "telemetry") and self.telemetry:
|
||||||
self.telemetry.track_core_feature(feature)
|
self.telemetry.track_core_feature(feature)
|
||||||
|
|
||||||
def telemetryTouchActivity(self):
|
|
||||||
"""テレメトリアクティビティ更新 (Model ラッパー)"""
|
|
||||||
if hasattr(self, "telemetry") and self.telemetry:
|
|
||||||
self.telemetry.touch_activity()
|
|
||||||
|
|
||||||
model = Model()
|
model = Model()
|
||||||
@@ -10,7 +10,6 @@ from concurrent.futures import Future
|
|||||||
# Allow running as a script for quick verification.
|
# Allow running as a script for quick verification.
|
||||||
try:
|
try:
|
||||||
from .state import TelemetryState
|
from .state import TelemetryState
|
||||||
from .heartbeat import HeartbeatManager
|
|
||||||
from .core import TelemetryCore
|
from .core import TelemetryCore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import os
|
import os
|
||||||
@@ -18,7 +17,6 @@ except ImportError:
|
|||||||
|
|
||||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||||
from models.telemetry.state import TelemetryState
|
from models.telemetry.state import TelemetryState
|
||||||
from models.telemetry.heartbeat import HeartbeatManager
|
|
||||||
from models.telemetry.core import TelemetryCore
|
from models.telemetry.core import TelemetryCore
|
||||||
|
|
||||||
|
|
||||||
@@ -36,9 +34,9 @@ class Telemetry:
|
|||||||
return
|
return
|
||||||
self.state = TelemetryState()
|
self.state = TelemetryState()
|
||||||
self.core = TelemetryCore(self.state)
|
self.core = TelemetryCore(self.state)
|
||||||
self.heartbeat = HeartbeatManager(self.state, self.core, self._schedule_async_with_loop)
|
|
||||||
self._loop = None
|
self._loop = None
|
||||||
self._loop_thread = None
|
self._loop_thread = None
|
||||||
|
self._init_called = False # init()が呼ばれたかを追跡(重複初期化防止)
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
def _start_event_loop(self):
|
def _start_event_loop(self):
|
||||||
@@ -96,12 +94,20 @@ class Telemetry:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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"):
|
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)
|
self.state.set_enabled(enabled)
|
||||||
if enabled:
|
if enabled:
|
||||||
self._start_event_loop()
|
self._start_event_loop()
|
||||||
@@ -111,23 +117,18 @@ class Telemetry:
|
|||||||
"""非同期初期化処理"""
|
"""非同期初期化処理"""
|
||||||
await self.core.start(app_version=app_version)
|
await self.core.start(app_version=app_version)
|
||||||
await self.core.send_event("app_started")
|
await self.core.send_event("app_started")
|
||||||
self.heartbeat.start()
|
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
"""テレメトリ終了(同期インターフェース)
|
"""テレメトリ終了(同期インターフェース)
|
||||||
|
|
||||||
重要:Tauri sidecar環境では、このメソッド実行後にプロセス終了が発生します。
|
重要:Tauri sidecar環境では、このメソッド実行後にプロセス終了が発生します。
|
||||||
app_closed イベントが確実に送信されるように、以下の手順を実行します:
|
app_closed イベントが確実に送信されるように、以下の手順を実行します:
|
||||||
1. heartbeat スレッド停止
|
1. app_closed イベント送信を同期待機
|
||||||
2. app_closed イベント送信を同期待機
|
2. Aptabase クライアント停止(フラッシュ含む)
|
||||||
3. Aptabase クライアント停止(フラッシュ含む)
|
3. イベントループ完全停止を待機
|
||||||
4. イベントループ完全停止を待機
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Step 1: Heartbeat 停止(5分待機中の送信を防ぐ)
|
# app_closed 送信とクライアント停止を同期待機
|
||||||
self.heartbeat.stop()
|
|
||||||
|
|
||||||
# Step 2-3: app_closed 送信とクライアント停止を同期待機
|
|
||||||
if self.state.is_enabled():
|
if self.state.is_enabled():
|
||||||
try:
|
try:
|
||||||
# _run_async で最大5秒間待機(Aptabase のフラッシュ含む)
|
# _run_async で最大5秒間待機(Aptabase のフラッシュ含む)
|
||||||
@@ -135,7 +136,7 @@ class Telemetry:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Step 4: イベントループを完全停止(フラッシュ確認を待つ)
|
# イベントループを完全停止(フラッシュ確認を待つ)
|
||||||
# sidecar 終了前に確実に完了させるため、タイムアウトを長めに設定
|
# sidecar 終了前に確実に完了させるため、タイムアウトを長めに設定
|
||||||
self._stop_event_loop(timeout=5.0)
|
self._stop_event_loop(timeout=5.0)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -144,6 +145,7 @@ class Telemetry:
|
|||||||
finally:
|
finally:
|
||||||
# 状態をリセット
|
# 状態をリセット
|
||||||
self.state.reset()
|
self.state.reset()
|
||||||
|
self._init_called = False # 初期化フラグもリセット
|
||||||
|
|
||||||
async def _shutdown_async(self):
|
async def _shutdown_async(self):
|
||||||
"""非同期終了処理"""
|
"""非同期終了処理"""
|
||||||
@@ -160,7 +162,6 @@ class Telemetry:
|
|||||||
"""コア機能イベント送信(同期インターフェース)"""
|
"""コア機能イベント送信(同期インターフェース)"""
|
||||||
if not self.state.is_enabled():
|
if not self.state.is_enabled():
|
||||||
return
|
return
|
||||||
self.state.touch_activity()
|
|
||||||
if self.core.is_duplicate_core_feature(feature):
|
if self.core.is_duplicate_core_feature(feature):
|
||||||
return
|
return
|
||||||
self._schedule_async(self._track_core_feature_async(feature))
|
self._schedule_async(self._track_core_feature_async(feature))
|
||||||
@@ -170,11 +171,6 @@ class Telemetry:
|
|||||||
await self.core.send_event("core_feature", {"feature": feature})
|
await self.core.send_event("core_feature", {"feature": feature})
|
||||||
self.state.record_feature_sent(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:
|
def is_enabled(self) -> bool:
|
||||||
"""有効状態確認"""
|
"""有効状態確認"""
|
||||||
return self.state.is_enabled()
|
return self.state.is_enabled()
|
||||||
@@ -190,7 +186,6 @@ if __name__ == "__main__":
|
|||||||
telemetry.init(enabled=True)
|
telemetry.init(enabled=True)
|
||||||
telemetry.track("debug_test", {"message": "telemetry main demo"})
|
telemetry.track("debug_test", {"message": "telemetry main demo"})
|
||||||
telemetry.track_core_feature("text_input")
|
telemetry.track_core_feature("text_input")
|
||||||
telemetry.touch_activity()
|
|
||||||
print("state:", telemetry.get_state())
|
print("state:", telemetry.get_state())
|
||||||
|
|
||||||
# イベント送信完了を待つ
|
# イベント送信完了を待つ
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Aptabase SDK ラッパー(非同期版)
|
Aptabase SDK ラッパー(非同期版)
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
# Aptabase SDK のインポート
|
# Aptabase SDK のインポート
|
||||||
@@ -11,10 +12,12 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
class AptabaseWrapper:
|
class AptabaseWrapper:
|
||||||
APP_KEY = "A-US-2082730845"
|
APP_KEY = "A-US-6044063021"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.client = None
|
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"):
|
async def start(self, app_version: str = "1.0.0"):
|
||||||
"""Aptabase クライアント開始"""
|
"""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:
|
class TelemetryState:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._enabled = True # デフォルト有効
|
self._enabled = True # デフォルト有効
|
||||||
self._last_activity = None
|
|
||||||
self._session_features_sent = set()
|
self._session_features_sent = set()
|
||||||
self._lock = Lock()
|
self._lock = Lock()
|
||||||
|
|
||||||
@@ -27,16 +26,6 @@ class TelemetryState:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return self._enabled
|
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):
|
def record_feature_sent(self, feature: str):
|
||||||
"""送信済み機能を記録"""
|
"""送信済み機能を記録"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -50,7 +39,6 @@ class TelemetryState:
|
|||||||
def reset(self):
|
def reset(self):
|
||||||
"""状態をリセット"""
|
"""状態をリセット"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._last_activity = None
|
|
||||||
self._session_features_sent.clear()
|
self._session_features_sent.clear()
|
||||||
|
|
||||||
def get_debug_info(self) -> dict:
|
def get_debug_info(self) -> dict:
|
||||||
@@ -58,6 +46,5 @@ class TelemetryState:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
return {
|
return {
|
||||||
"enabled": self._enabled,
|
"enabled": self._enabled,
|
||||||
"last_activity": self._last_activity.isoformat() if self._last_activity else None,
|
|
||||||
"session_features_sent": list(self._session_features_sent),
|
"session_features_sent": list(self._session_features_sent),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "VRCT",
|
"productName": "VRCT",
|
||||||
"version": "3.4.0",
|
"version": "3.4.2",
|
||||||
"identifier": "com.vrct.app",
|
"identifier": "com.vrct.app",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "",
|
"beforeDevCommand": "",
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export const SETTINGS_ARRAY = [
|
|||||||
default_value: "",
|
default_value: "",
|
||||||
ui_template_id: "select",
|
ui_template_id: "select",
|
||||||
logics_template_id: "get_set",
|
logics_template_id: "get_set",
|
||||||
|
add_endpoint_run_array: ["from_backend"],
|
||||||
base_endpoint_name: "selected_translation_compute_type",
|
base_endpoint_name: "selected_translation_compute_type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -517,6 +518,7 @@ export const SETTINGS_ARRAY = [
|
|||||||
default_value: "",
|
default_value: "",
|
||||||
ui_template_id: "select",
|
ui_template_id: "select",
|
||||||
logics_template_id: "get_set",
|
logics_template_id: "get_set",
|
||||||
|
add_endpoint_run_array: ["from_backend"],
|
||||||
base_endpoint_name: "selected_transcription_compute_type",
|
base_endpoint_name: "selected_transcription_compute_type",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -99,12 +99,12 @@ const OpenVrcMicMuteSyncQuickSetting = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const SoftwareUpdateAvailableButton = () => {
|
const SoftwareUpdateAvailableButton = () => {
|
||||||
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null;
|
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
|
||||||
|
|
||||||
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
|
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
|
||||||
|
|
||||||
|
if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={styles.software_update_button} onClick={()=>updateOpenedQuickSetting("update_software")}>
|
<button className={styles.software_update_button} onClick={()=>updateOpenedQuickSetting("update_software")}>
|
||||||
<RefreshSvg className={styles.refresh_svg}/>
|
<RefreshSvg className={styles.refresh_svg}/>
|
||||||
|
|||||||
Reference in New Issue
Block a user