[251016] fix: VUI Test-1

🕐 커밋 시간: 2025. 10. 16. 14:10:17

📊 변경 통계:
  • 총 파일: 3개
  • 추가: +160줄
  • 삭제: -3줄

📁 추가된 파일:
  + com.twin.app.shoptime/vui-guide.2.md

📝 수정된 파일:
  ~ com.twin.app.shoptime/src/App/App.js
  ~ com.twin.app.shoptime/src/actions/voiceActions.js

🔧 함수 변경 내용:
  📄 com.twin.app.shoptime/src/App/App.js (javascript):
     Added: processArgs(), sendVoiceLogToPanel()
  📄 com.twin.app.shoptime/src/actions/voiceActions.js (javascript):
    🔄 Modified: addLog()

🔧 주요 변경 내용:
  • 핵심 비즈니스 로직 개선
  • 개발 문서 및 가이드 개선
This commit is contained in:
2025-10-16 14:10:19 +09:00
parent 56e872ad69
commit 31cdfedf3f
3 changed files with 534 additions and 3 deletions

View File

@@ -63,6 +63,7 @@ import css from './App.module.less';
import { handleBypassLink } from './bypassLinkHandler'; import { handleBypassLink } from './bypassLinkHandler';
import { handleDeepLink } from './deepLinkHandler'; import { handleDeepLink } from './deepLinkHandler';
import { sendLogTotalRecommend } from '../actions/logActions'; import { sendLogTotalRecommend } from '../actions/logActions';
import { types } from '../actions/actionTypes';
// import { // import {
// startFocusMonitoring, // startFocusMonitoring,
// stopFocusMonitoring, // stopFocusMonitoring,
@@ -109,15 +110,158 @@ const processArgs = (args) => {
}); });
}; };
// Voice 관련 로그를 VoicePanel로 전송하는 헬퍼 함수
const sendVoiceLogToPanel = (args) => {
try {
const firstArg = args[0];
// [Voice] 또는 [VoiceConductor] 태그 확인
if (
typeof firstArg === 'string' &&
(firstArg.includes('[Voice]') || firstArg.includes('[VoiceConductor]'))
) {
// 로그 타입 결정
let logType = 'INFO';
let title = firstArg;
if (
firstArg.includes('ERROR') ||
firstArg.includes('Error') ||
firstArg.includes('Failed') ||
firstArg.includes('failed')
) {
logType = 'ERROR';
} else if (
firstArg.includes('Response') ||
firstArg.includes('response') ||
firstArg.includes('success')
) {
logType = 'RESPONSE';
} else if (
firstArg.includes('Registering') ||
firstArg.includes('Sending') ||
firstArg.includes('request')
) {
logType = 'REQUEST';
} else if (
firstArg.includes('received') ||
firstArg.includes('Handling') ||
firstArg.includes('⭐')
) {
logType = 'ACTION';
} else if (firstArg.includes('command')) {
logType = 'COMMAND';
}
// 데이터 수집
const logData = {};
if (args.length > 1) {
args.slice(1).forEach((arg, index) => {
if (typeof arg === 'object') {
Object.assign(logData, arg);
} else {
logData[`arg${index + 1}`] = arg;
}
});
}
// Redux로 dispatch
store.dispatch({
type: types.VOICE_ADD_LOG,
payload: {
timestamp: new Date().toISOString(),
type: logType,
title: title.replace(/^\[Voice\]\s*/, '').replace(/^\[VoiceConductor\]\s*/, ''),
data: Object.keys(logData).length > 0 ? logData : { message: firstArg },
success: logType !== 'ERROR',
},
});
}
} catch (error) {
// 로깅 중 에러가 발생해도 원래 로그는 출력되어야 함
originalConsoleLog.call(console, '[VoiceLog] Error sending to panel:', error);
}
};
console.log = function (...args) { console.log = function (...args) {
// Voice 로그를 VoicePanel로 전송
sendVoiceLogToPanel(args);
// 원래 console.log 실행
originalConsoleLog.apply(console, processArgs(args)); originalConsoleLog.apply(console, processArgs(args));
}; };
console.error = function (...args) { console.error = function (...args) {
// Voice 로그를 VoicePanel로 전송 (에러는 강제로 ERROR 타입)
try {
const firstArg = args[0];
if (
typeof firstArg === 'string' &&
(firstArg.includes('[Voice]') || firstArg.includes('[VoiceConductor]'))
) {
const logData = {};
if (args.length > 1) {
args.slice(1).forEach((arg, index) => {
if (typeof arg === 'object') {
Object.assign(logData, arg);
} else {
logData[`arg${index + 1}`] = arg;
}
});
}
store.dispatch({
type: types.VOICE_ADD_LOG,
payload: {
timestamp: new Date().toISOString(),
type: 'ERROR',
title: firstArg.replace(/^\[Voice\]\s*/, '').replace(/^\[VoiceConductor\]\s*/, ''),
data: Object.keys(logData).length > 0 ? logData : { message: firstArg },
success: false,
},
});
}
} catch (error) {
originalConsoleError.call(console, '[VoiceLog] Error sending error to panel:', error);
}
originalConsoleError.apply(console, processArgs(args)); originalConsoleError.apply(console, processArgs(args));
}; };
console.warn = function (...args) { console.warn = function (...args) {
// Voice 로그를 VoicePanel로 전송 (경고는 ERROR 타입으로)
try {
const firstArg = args[0];
if (
typeof firstArg === 'string' &&
(firstArg.includes('[Voice]') || firstArg.includes('[VoiceConductor]'))
) {
const logData = {};
if (args.length > 1) {
args.slice(1).forEach((arg, index) => {
if (typeof arg === 'object') {
Object.assign(logData, arg);
} else {
logData[`arg${index + 1}`] = arg;
}
});
}
store.dispatch({
type: types.VOICE_ADD_LOG,
payload: {
timestamp: new Date().toISOString(),
type: 'ERROR',
title:
'WARNING: ' +
firstArg.replace(/^\[Voice\]\s*/, '').replace(/^\[VoiceConductor\]\s*/, ''),
data: Object.keys(logData).length > 0 ? logData : { message: firstArg },
success: false,
},
});
}
} catch (error) {
originalConsoleWarn.call(console, '[VoiceLog] Error sending warning to panel:', error);
}
originalConsoleWarn.apply(console, processArgs(args)); originalConsoleWarn.apply(console, processArgs(args));
}; };

View File

@@ -99,20 +99,30 @@ export const registerVoiceFramework = () => (dispatch, getState) => {
// performAction command received // performAction command received
if (res.command === 'performAction' && res.action) { if (res.command === 'performAction' && res.action) {
console.log('[Voice] performAction command received:', res.action); console.log('[Voice] ⭐⭐⭐ performAction command received:', res.action);
// ⭐ 중요: performAction 수신 성공 로그 (명확하게)
dispatch( dispatch(
addLog('COMMAND', 'performAction Command Received', { addLog('COMMAND', 'performAction RECEIVED!', {
message: '✅ SUCCESS! Voice framework sent performAction event.',
command: res.command, command: res.command,
action: res.action, action: res.action,
intent: res.action?.intent,
value: res.action?.value || res.action?.itemId,
}) })
); );
dispatch({ dispatch({
type: types.VOICE_PERFORM_ACTION, type: types.VOICE_PERFORM_ACTION,
payload: res.action, payload: res.action,
}); });
// Get voiceTicket from Redux state (performAction response doesn't include voiceTicket)
const { voiceTicket } = getState().voice;
console.log('[Voice] Using voiceTicket from state:', voiceTicket);
// Process the action and report result // Process the action and report result
dispatch(handleVoiceAction(res.voiceTicket, res.action)); dispatch(handleVoiceAction(voiceTicket, res.action));
} }
}, },
@@ -215,6 +225,36 @@ export const sendVoiceIntents = (voiceTicket) => (dispatch, getState) => {
type: types.VOICE_SET_CONTEXT_SUCCESS, type: types.VOICE_SET_CONTEXT_SUCCESS,
payload: res, payload: res,
}); });
// ⭐ 중요: Voice input 대기 상태 로그
dispatch(
addLog('ACTION', '🎤 Ready for Voice Input', {
message: 'Context set successfully. Press the MIC button on remote and speak.',
nextStep: 'Waiting for performAction event...',
voiceTicket: voiceTicket,
})
);
// 15초 후에도 performAction이 안 오면 경고 로그 (타이머)
setTimeout(() => {
const currentState = getState().voice;
// lastSTTText가 업데이트되지 않았으면 (performAction이 안 왔으면)
if (!currentState.lastSTTText || currentState.sttTimestamp < Date.now() - 14000) {
dispatch(
addLog('ERROR', '⚠️ No performAction received yet', {
message: 'performAction event was not received within 15 seconds after setContext.',
possibleReasons: [
'1. Did you press the MIC button on the remote control?',
'2. Did you speak after pressing the MIC button?',
'3. UseIME intent might not be supported on this webOS version',
'4. Voice framework might not be routing events correctly',
],
suggestion:
'Try pressing the remote MIC button and speaking clearly. Check VoicePanel logs for performAction event.',
})
);
}
}, 15000);
}, },
onFailure: (err) => { onFailure: (err) => {

View File

@@ -0,0 +1,347 @@
# `com.webos.service.voiceconductor` v1.1 API 문서
> **생성일**: 2024/04/18
> **최종 수정자**: soonwon.hong
> **문서 생성 도구**: APIEditorScript (`apieditor.script`)
> **대상 플랫폼**: webOS TV
---
## 📌 요약 (Summary)
`voiceconductor`는 webOS TV의 **음성 프레임워크의 핵심 서비스**로, 전체 음성 처리 흐름을 조율(Conduct)합니다.
모든 음성 관련 요청은 이 서비스를 통해 시작되며, 다음 주요 기능을 제공합니다:
- **STT (Speech-to-Text)**: 사용자 음성을 텍스트로 변환
- **Intent 인식**: 음성 또는 텍스트를 기반으로 사용자의 의도(Intent) 분석
---
## 🔍 개요 (Overview)
### API 카테고리
| 카테고리 | 설명 |
|--------|------|
| `/` | 국가별 비즈니스 계약에 따라 **Google, Alexa, Alibaba, LG** 등 다양한 플러그인 중 하나가 자동 선택되어 실행 |
| `/system` | **LG 자사 플러그인**을 강제로 사용 (기본값). `/`와 동일한 API 형식을 가지며, 중복 설명 생략 |
| `/interactor` | **In-App Control** 기능 지원 (webOS 4.5+) — 앱 내에서 음성 명령 처리 가능 |
---
## 🔧 메서드 (Methods)
### `/getRecognitionStatus`
> 현재 진행 중인 음성 인식 작업 상태 조회 (Subscription 지원)
- **지원 플랫폼**: 없음 (`[Private]`)
- **ACG**: 없음
- **파라미터**:
- `taskTypes` (String[]): `["fullVoiceNlp", "voiceNlp", "textNlp", "stt"]`
- `subscribe` (Boolean): 구독 여부
- **반환**:
- `tasks`: 실행 중인 `voiceTask` 객체 배열
- `subscribed`, `returnValue`, `errorCode`, `errorText`
**예시**:
```json
{
"tasks": [
{
"voiceTicket": "V00001",
"taskType": "fullVoiceNlp",
"voiceEngine": "googleAssistant",
"recognitionSource": { "input": "voiceinput", "type": "remote", "key": "mrcu" },
"status": "init"
}
],
"subscribed": true,
"returnValue": true
}
```
---
### `/cancelRecognition`
> 진행 중인 음성 인식 작업 취소
- **지원**: TV (`[Private]`)
- **파라미터**: `voiceTicket` (String)
- **반환**: `returnValue`, `errorCode`, `errorText`
> ⚠️ `stopRecordingVoice`와 달리, 녹음 중이 아닐 경우 STT 플러그인에 종료 요청을 보내지 않음.
---
### `/checkUpdate`
> 특정 패키지에 대한 음성 기능 업데이트 필요 여부 확인
- **지원**: TV (`[Do Not Publish]`)
- **파라미터**:
- `type`: `"package"` (고정)
- `id`: 패키지 ID (예: `"amazon.alexa"`)
- **반환**: `needUpdate` (Boolean)
---
### `/getSupportedLanguages`
> 현재 지원하는 언어 목록 조회
- **지원**: TV (`[Public]`)
- **파라미터**:
- `languageCodes`: BCP-47 언어 코드 배열 (예: `["ko-KR", "en-US"]`)
- `voiceEngine`: `"stt"` (기본) 또는 `"nlp"`
- `showAll`: 엔진별 상세 언어 반환 여부
- `loadDefault`: 서버 없이 기본 언어 반환
- **반환**:
- `voiceLanguages`: `{ "ko-KR": "ko-KR", "xx-XX": "notSupported" }`
- `voiceEngines`: (showAll=true 시)
**성공 응답**:
```json
{
"returnValue": true,
"voiceLanguages": {
"ko-KR": "ko-KR",
"en-US": "en-US",
"xx-XX": "notSupported"
}
}
```
---
### `/getUtteranceEvents`
> **`getVoiceUiEvents`와 동일** — 설명 생략
---
### `/getVoiceKey`
> 서버 통신에 필요한 보안 키 반환
- **반환**: `vsn`, `staticVoiceKey`, `dynamicVoiceKey`
---
### `/getVoiceUiEvents`
> 음성 처리 중 UI 이벤트 수신 (Subscription 필수)
- **지원**: TV (`[Public]`)
- **이벤트 종류**:
- `registered`, `sttStart`, `sttVoiceLevel`, `sttPartialResult`, `sttResult`, `sttEnd`
- `nlpStart`, `nlpEnd`, `actionFeedback`, `sessionEnd`, `error`
**이벤트 예시**:
```json
{ "event": "sttVoiceLevel", "level": 45, "subscribed": true }
{ "event": "actionFeedback", "feedback": { "systemUtterance": "{{1로}} 검색 결과입니다." } }
{ "event": "sessionEnd", "subscribed": false }
```
---
## 🎮 In-App Control (`/interactor`)
### `/interactor/register`
> 앱을 음성 프레임워크에 등록 (Subscription 필수)
- **명령어**:
- `setContext`: 앱이 `setContext` 호출 필요
- `performAction`: 앱이 액션 수행 후 `reportActionResult` 호출
**응답 예시**:
```json
{ "command": "setContext", "voiceTicket": "V00000006" }
{ "command": "performAction", "action": { "intent": "Select", "itemId": "test" } }
```
---
### `/interactor/reportActionResult`
> In-App 액션 처리 결과 보고
- **필수 파라미터**: `voiceTicket`, `result` (Boolean)
- **옵션**: `feedback` (`inAppFeedback`)
---
### `/interactor/setContext`
> 앱이 처리 가능한 Intent 목록 등록
- **파라미터**: `voiceTicket`, `inAppIntents` (배열)
- **Intent 유형**: `Select`, `Scroll`, `PlayContent`, `Zoom` 등 9가지
**예시**:
```json
{
"inAppIntents": [
{
"intent": "Select",
"items": [{ "itemId": "test", "value": ["test"] }]
}
]
}
```
---
## 🎤 음성 인식 API
### `/recognizeIntentByText`
> 텍스트 기반 Intent 분석 및 실행
- **지원**: TV (`[Public]`)
- **파라미터**: `text`, `language`, `runVoiceUi`, `inAppControl`, `source`
- **반환**: `serverResponse` (`responseCode`, `responseMessage`)
---
### `/recognizeIntentByVoice`
> PCM 파일 기반 음성 Intent 분석 (**테스트 전용**)
- **지원**: TV (`[Private]`)
- **필수**: `pcmPath`
---
### `/recognizeVoice`
> 음성을 텍스트로 변환 (STT)
- **반환**: `text` (String 배열)
---
### `/recognizeVoiceWithDetails`
> STT 과정의 상세 이벤트 스트림 (Subscription 필수)
- **이벤트**: `sttVoiceLevel`, `sttPartialResult`, `sttResult`, `sessionEnd`
- **반환**: `event`, `text`, `level`, `feedback`, `voiceTicket`
**응답 예시**:
```json
{ "event": "sttResult", "text": ["배트맨", "베트맨"], "voiceTicket": "V00000007" }
```
---
### `/setInputEvent`
> 하드웨어/소프트웨어 버튼 이벤트 처리
- **inputType**: `"hold"` (down/up), `"wakeword"`
- **deviceId**: `"remote"`, `"amazonAlexa"`
---
### `/recordVoice` & `/stopRecordingVoice`
> 음성 데이터 스트리밍 녹음 시작/종료
- **recordVoice**: WebSocket URL 반환 (`websocketUrl`)
- **stopRecordingVoice**: 녹음 중지
**recordVoice 응답**:
```json
{ "event": "recordingStart", "websocketUrl": "wss://..." }
{ "event": "recordingEnd" }
```
---
## 📦 시스템 API
### `/system/checkUpdate`
> LG 자사 플러그인 기반 업데이트 확인
- **동작**: 필요한 경우 팝업 표시 후 음성 기능 중단
---
## ❌ 오류 코드 참조
| 코드 | 텍스트 | 설명 |
|------|--------|------|
| 100 | `bad params` | 파라미터 오류 |
| 101 | `deprecated api` | 더 이상 지원되지 않음 |
| 300 | `precondition not satisfied` | 네트워크/설정 미완료 |
| 301 | `internal processing error` | 내부 오류 (메모리 등) |
| 302 | `failed recognize voice` | STT 실패 |
| 303 | `failed recognize intent` | NLP 실패 |
| 304 | `unsupported language` | 언어 미지원 |
| 305 | `already processing another request` | 다른 요청 진행 중 |
---
## 🧱 객체 정의 (Objects)
### `actionFeedback`
- `systemUtterance`: 사용자에게 표시할 문구
- `exception`: 예외 유형 (예: `"alreadyCompleted"`)
---
### `serverResponse`
- `responseCode`: 처리 결과 코드
- `responseMessage`: 피드백 메시지
---
### `inAppAction`
- `type`: `"IntentMatch"`
- `intent`: `"Select"`, `"Scroll"`, `"Zoom"`
- `itemId`: 앱 내 고유 ID
- 추가 필드: `scroll`, `checked`, `state`, `control`, `zoom`
---
### `inAppIntent`
- `intent`: 지원 Intent 유형
- `items`: 선택 가능한 항목 (`inAppIntentItem` 배열)
- `itemId`: 스크롤/줌 등 단일 액션용 ID
---
### `inAppIntentItem`
- `itemId`: 고유 ID
- `value`: STT 매칭 문구 배열
- `title`: UI 표시 문구
- `checked` / `state`: 체크박스/토글 상태
---
### `inAppFeedback`
- `voiceUi`: `actionFeedback` 객체
---
### `voiceEngines`
- 엔진별 언어 지원 정보:
```json
{
"thinQtv": { "ko-KR": "ko-KR" },
"googleAssistant": { "ko-KR": "notSupported" }
}
```
---
### `voiceTask`
- `voiceTicket`: 세션 ID
- `status`: `"init"`, `"running"`, `"finished"`
- `taskType`: `"stt"`, `"fullVoiceNlp"` 등
- `voiceEngine`: 사용 중인 엔진 (예: `"googleAssistant"`)
- `recognitionSource`: 입력 소스 정보
---
### `voiceRecognitionSource`
- `input`: `"voiceinput"` 또는 `"text"`
- `type`: `"remote"`, `"mrcu"`, `"amazonEcho"` 등
- `key`: 소스 세부 키 (예: `"mrcu"`)
---
> 📎 **관련 문서**: [LG Collab 문서](http://collab.lge.com/main/pages/viewpage.action?pageId=789627390)
---
✅ 이 문서는 **webOS TV 음성 프레임워크 개발자**를 위한 전체 API 참조입니다.
🔒 `[Private]` 또는 `[Do Not Publish]`로 표시된 API는 외부 공개용이 아닙니다.