Phase 14 구현: 회의 모드 (Meeting Mode)

실시간 녹음 + 타임스탬프 메모 + STT 전사 + LLM 구조화 회의록 + PDF/MD 내보내기.
CaptionService 연동, 동시 사용 충돌 방지, 긴 회의 2-pass 요약 지원.
DB 2테이블, IPC 15채널, UI 3뷰(목록/녹음/상세), 12개 locale i18n.
This commit is contained in:
Yun Chan 2026-04-08 01:16:52 +09:00
parent dd68fc9ccd
commit d0c4f9ee53
38 changed files with 2550 additions and 769 deletions

View file

@ -12,36 +12,37 @@ tools:
# UI Specialist # UI Specialist
Speakly 렌더러 분석(781KB 번들)에서 추출한 UI 패턴을 적용. D3RO-VOICE의 "정밀 기기(Meteorological Instrument)" 디자인 정체성을 구현.
## 컴포넌트 구조 (Speakly 패턴) ## 디자인 토큰 체계 (SSOT)
``` - **d3roPalette**: 색상 토큰 (theme.ts에서 export)
App (root) - **d3roTypo**: 13단계 타이포 (hero 42px ~ nano 7px)
├── ThemeProvider (MUI, light/dark/auto) - **d3roShadow**: 10종 그림자 (chassis/card/inset/button 등)
├── Drawer (240px 사이드바) - **d3roRadius**: 7종 반경 (outer 24px ~ pill 999px)
│ ├── NavItems - hex 리터럴 직접 사용 금지 (theme.ts 제외)
│ └── BottomBar
└── Content Area
├── Dashboard (통계, 최근 세션)
├── History (검색, 재시도)
├── Dictionary (사전 관리)
└── Settings (Modal)
```
## MUI 테마 (Speakly 참조) ## DS 컴포넌트 (src/renderer/components/ds/)
- primary: rgb(31, 93, 242) - CrtDisplay: WebGL CRT 셰이더 (파형+스캔라인+비네팅+글리치)
- borderRadius: 12 - InstrumentPanel: 메탈 섀시 컨테이너
- fontFamily: -apple-system, BlinkMacSystemFont, "Segoe UI" - Led: LED 인디케이터 (amber/green/red, pulse)
- textTransform: 'none' - PhysicalButton: 물리 버튼 (돌출 그림자, 눌림)
- WebkitAppRegion: 'drag' (타이틀바) - MetalCard: 메탈 카드 컨테이너
- PhosphorText: 인광 텍스트 (13종 변형)
- MetalDial: 메탈 다이얼 (드래그 회전)
- ScreenPanel: 글래스 스크린 패널
- ButtonGroup: 인셋 버튼 클러스터
## 녹음 UI 상태 머신 ## 녹음 UI 상태 머신
opening → recording → thinking → result/error/cancelled opening → recording → thinking → result/error/cancelled
- 9개 wave-bar, cos(n*PI/2) 분포 가중치 - 9개 wave-bar, cos(n*PI/2) 분포 가중치
- 100ms 간격 setInterval, smoothing: v += (S-v)*0.5 - 100ms 간격, smoothing: v += (S-v)*0.5
- thinking: `min(95, (1 - 1/(1+1.5*t)) * 100)%` 점근 수렴 - thinking: `min(95, (1 - 1/(1+1.5*t)) * 100)%` 점근 수렴
## 팝업 윈도우 (Vanilla JS) ## 팝업 윈도우 (Vanilla JS)
- 별도 HTML 엔트리포인트, React 미사용 - 별도 HTML, React 미사용
- 2-phase 리사이즈: prepare → measured → show - 2-phase 리사이즈: prepare → measured → show
- CSS transition + transitionend + 200ms setTimeout 폴백 - 앰버 악센트(#f25b29)
## i18n
- React: t() 함수 사용, 하드코딩 한국어/영어 금지
- 리소스: src/renderer/locales/{ko,en}.json

View file

@ -28,16 +28,17 @@ AudioState: IDLE → INITIALIZING → STREAMING → STOPPED (별도 추적)
- 이중 조건 플러시: 모델 로딩 + 오디오 캡처 병렬 - 이중 조건 플러시: 모델 로딩 + 오디오 캡처 병렬
- `_tryFlushAll()`: 양쪽 조건 모두 true 시 flush - `_tryFlushAll()`: 양쪽 조건 모두 true 시 flush
- 순서 보장: 설정 메시지 → 오디오 데이터 - 순서 보장: 설정 메시지 → 오디오 데이터
- "오디오 손실 방지 > 컨텍스트 품질" 원칙
## 오디오 설정 ## 오디오 설정
- sampleRate: 24000Hz → 16000Hz (Whisper 기본) - sampleRate: 16000Hz (Whisper 기본)
- channels: 1 (mono) - channels: 1 (mono), bitDepth: 16 (PCM16)
- noiseSuppression: false (초기 프레임 손실 방지) - noiseSuppression: false, echoCancellation: false, autoGainControl: false
- echoCancellation: false (일방향 입력)
- autoGainControl: false
## 로컬 STT 연동 ## 로컬 STT 연동
- Whisper에 PCM 16-bit 16kHz mono 직접 전달 - PCM 16-bit 16kHz mono → faster-whisper Python sidecar (FastAPI HTTP)
- Opus 인코딩 불필요 (로컬이므로) - Opus 인코딩 불필요 (로컬)
- faster-whisper Python sidecar 또는 whisper.cpp addon
## 타이밍 상수
- 더블프레스: 300ms, 최소 오디오: 700ms
- 녹음 후 대기: 4초 (버퍼 있으면 6초)
- 완료 아이들 타임아웃: 30초, 절대 최대: 120초

View file

@ -10,35 +10,26 @@
"Bash(git *)", "Bash(git *)",
"Bash(ls *)", "Bash(ls *)",
"Bash(mkdir *)", "Bash(mkdir *)",
"Bash(cat *)",
"Bash(which *)", "Bash(which *)",
"Bash(python *)", "Bash(python *)",
"Bash(pip *)", "Bash(pip *)",
"Bash(taskkill *)", "Bash(taskkill *)",
"Bash(sleep *)",
"Bash(mv *)", "Bash(mv *)",
"Bash(tail *)", "Bash(rm *)",
"Bash(head *)",
"Bash(grep *)",
"Bash(wc *)",
"Edit(src/**)", "Edit(src/**)",
"Edit(tests/**)", "Edit(tests/**)",
"Edit(docs/**)", "Edit(docs/**)",
"Edit(scripts/**)",
"Edit(.claude/**)",
"Edit(*.md)", "Edit(*.md)",
"Edit(*.json)", "Edit(*.json)",
"Edit(*.ts)",
"Edit(*.tsx)",
"Edit(*.js)",
"Edit(*.css)",
"Edit(*.html)",
"Write(src/**)", "Write(src/**)",
"Write(tests/**)", "Write(tests/**)",
"Write(docs/**)", "Write(docs/**)",
"Write(scripts/**)", "Write(scripts/**)",
"Write(.claude/**)",
"Write(*.md)", "Write(*.md)",
"Write(*.json)", "Write(*.json)"
"Write(*.ts)",
"Write(*.tsx)"
], ],
"deny": [ "deny": [
"Bash(rm -rf /)", "Bash(rm -rf /)",
@ -84,7 +75,16 @@
"hooks": [ "hooks": [
{ {
"type": "command", "type": "command",
"command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE 'src/renderer/.*\\.(tsx|ts)$' && ! echo \"$FILE\" | grep -q 'theme\\.ts' && ! echo \"$FILE\" | grep -q 'theme-vars'; then HC=$(grep -noE \"'#[0-9a-fA-F]{6}'\" \"$FILE\" 2>/dev/null | grep -v '//' | head -3); if [ -n \"$HC\" ]; then echo \"[SSOT WARN] 하드코딩 hex 색상 — d3roPalette/CSS var 사용:\"; echo \"$HC\"; fi; fi" "command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE 'src/renderer/.*\\.(tsx|ts)$' && ! echo \"$FILE\" | grep -q 'theme\\.ts' && ! echo \"$FILE\" | grep -q 'theme-vars'; then HC=$(grep -noE '#[0-9a-fA-F]{6}' \"$FILE\" 2>/dev/null | grep -v '//' | grep -v 'var(' | head -3); if [ -n \"$HC\" ]; then echo \"[SSOT WARN] 하드코딩 hex 색상 — d3roPalette/CSS var 사용:\"; echo \"$HC\"; fi; fi"
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat /dev/stdin | python -c \"import sys,json; d=json.load(sys.stdin); print(d.get('tool_input',{}).get('file_path',''))\" 2>/dev/null); if echo \"$FILE\" | grep -qE 'src/renderer/.*\\.tsx$' && ! echo \"$FILE\" | grep -qE 'locales/|i18n|test'; then I18N=$(python -c \"import re; lines=open('$FILE','r',encoding='utf-8').readlines(); [print(f'{i+1}:{l.rstrip()}') for i,l in enumerate(lines) if re.search(r'[\\uAC00-\\uD7AF]{2,}',l) and '//' not in l and 'import' not in l]\" 2>/dev/null | head -3); if [ -n \"$I18N\" ]; then echo \"[i18n WARN] 하드코딩 한국어 문자열 — t() 함수 사용 필요:\"; echo \"$I18N\"; fi; fi"
} }
] ]
} }

View file

@ -1,29 +1,46 @@
--- ---
name: debug-electron name: debug-electron
description: Electron 앱 디버깅. IPC 문제, 메모리 누수, 렌더러 크래시 등 진단. description: Electron 앱 디버깅. IPC 문제, 메모리 누수, 렌더러 크래시 등 진단.
allowed-tools: Read Grep Glob Bash allowed-tools: Read Grep Glob Bash Edit
--- ---
# Electron Debug # Electron Debug
## 일반 디버깅 증상 또는 에러 메시지를 받으면 아래 진단 흐름을 자동 실행합니다.
1. 로그 확인 (`electron-log` 출력)
2. IPC 채널 매칭 검증 (`shared/ipc-channels.ts` vs 실제 핸들러)
3. 프리로드 스크립트 노출 API 확인
## IPC 문제 ## 0. 자동 진단 파이프라인
- `shared/ipc-channels.ts`에 채널 정의 확인
- `src/main/ipc/`에 핸들러 등록 확인
- `src/preload/index.ts`에 브릿지 노출 확인
- 렌더러에서 `window.electronAPI.*` 호출 확인
## 오디오 문제 증상 분류 → 해당 섹션 실행 → 원인 보고 → 수정 제안
- Web Audio API 초기화 상태
- 마이크 권한 확인
- AudioContext sampleRate (24000Hz)
- noiseSuppression: false 확인
## 메모리 누수 1. **로그 수집**: `npm run dev` 출력 또는 electron-log 파일 확인
- useEffect cleanup 함수 확인 2. **에러 분류**: IPC / 오디오 / 메모리 / 렌더러 크래시 / 빌드
- IPC 이벤트 리스너 제거 확인 3. **해당 섹션 진단 실행**
- BrowserWindow destroy 확인 4. **원인 + 수정 방안 보고**
## IPC 문제 진단
자동 실행할 검증 체인:
1. `shared/ipc-channels.ts`에서 문제 채널 Grep → 채널 정의 존재?
2. `src/main/ipc/`에서 해당 채널 핸들러 Grep → `ipcMain.handle`/`on` 등록?
3. `src/preload/index.ts`에서 `contextBridge.exposeInMainWorld` Grep → 브릿지 노출?
4. `src/renderer/`에서 `window.electronAPI.*` 호출 Grep → 렌더러 사용?
5. 1~4 중 끊어진 체인 지점이 원인
## 오디오 문제 진단
1. SoX 경로 확인: `src/main/utils/paths.ts``getSoxPath` 읽기
2. AudioCaptureService 설정 확인: sampleRate 16000, noiseSuppression false
3. faster-whisper sidecar 프로세스 상태: `tasklist | grep python`
4. 마이크 권한: Electron `systemPreferences.getMediaAccessStatus('microphone')`
## 메모리 누수 진단
1. `useEffect` cleanup 누락 Grep: `useEffect` 있는데 return 없는 패턴
2. IPC 리스너 제거 누락: `ipcRenderer.on` 대비 `removeListener`/`removeAllListeners` 검색
3. BrowserWindow destroy 확인: `close` 이벤트에서 `destroy()` 호출 여부
## 빌드/타입 문제
1. `npm run typecheck 2>&1 | head -30` 실행
2. 에러 파일/라인 추출 후 해당 코드 Read
3. 원인 분석 후 수정 제안

View file

@ -1,47 +1,65 @@
--- ---
name: implement-phase name: implement-phase
description: Speakly RE 노하우를 기반으로 D3RO-VOICE의 특정 페이즈를 구현. 페이즈 번호를 인자로 전달. description: Speakly RE 노하우를 기반으로 D3RO-VOICE의 특정 페이즈를 구현. 페이즈 번호를 인자로 전달.
argument-hint: "[phase number, e.g. 1]" argument-hint: "[phase number or feature name]"
allowed-tools: Read Write Edit Bash Glob Grep Agent allowed-tools: Read Write Edit Bash Glob Grep Agent
--- ---
# Phase Implementation # Feature / Phase Implementation
지정된 페이즈를 구현합니다. 페이즈 또는 새 기능을 구현합니다.
## 실행 순서 ## 0. 사전 조건 가드 (스킵 불가)
1. **설계서 로드** (필수, 반드시 먼저): 구현 시작 전 반드시 아래를 확인하고, 실패 시 유저에게 보고 후 중단:
- `docs/design/00-master-architecture.md` — 파일 트리, 초기화 순서, 서비스 의존관계
- `docs/design/01-service-specifications.md` — 서비스 인터페이스, 상태 머신, 이벤트 1. **Git 클린 상태**: `git status --porcelain` — 커밋되지 않은 변경이 있으면 경고 후 커밋/스태시 제안
- `docs/design/02-ipc-and-types.md` — IPC 채널, TypeScript 타입, 에러 코드 2. **페이즈 문서 존재**: 페이즈 번호 전달 시 `docs/phases/phase-$ARGUMENTS.md` 파일 존재 확인. 없으면 중단
- `docs/design/03-db-and-ui.md` — DB 스키마, 컴포넌트 명세, MUI 테마 3. **이전 페이즈 완료 확인**: `memory/project_status.md` 읽어서 선행 페이즈 완료 여부 확인. 미완료 시 경고
2. **페이즈 문서 로드**: `docs/phases/phase-$ARGUMENTS.md` 읽기 4. **설계서 4종 존재 확인**: `docs/design/00~03` 파일 모두 존재 확인
3. **RE 참조 문서 확인**: `docs/re-findings/` 에서 관련 노하우 확인
4. **현재 코드 상태 파악**: 기존 구현된 코드 검토 ## 1. 설계서 로드 (필수)
4. **구현 계획 수립**: TaskCreate로 세부 태스크 생성
5. **구현**: 태스크별로 순차 구현 - `docs/design/00-master-architecture.md` — 아키텍처, 서비스 의존관계
- 타입 정의 (shared/) 먼저 - `docs/design/01-service-specifications.md` — 서비스 인터페이스, 상태 머신
- 메인 프로세스 서비스 구현 - `docs/design/02-ipc-and-types.md` — IPC 채널, 타입, 에러 코드
- IPC 핸들러 등록 - `docs/design/03-db-and-ui.md` — DB 스키마, 컴포넌트 명세
- 렌더러 UI 구현
- 프리로드 브릿지 업데이트 ## 2. 요구사항 파악
6. **테스트**: 각 모듈 단위 테스트
7. **CLAUDE.md 업데이트**: 현재 상태 갱신 - 페이즈 번호 전달 시: `docs/phases/phase-$ARGUMENTS.md` 읽기
8. **Memory 갱신** (필수): `project_status.md`에 완료된 Phase, 작업 내역, 기술 결정 기록 - 피처명 전달 시: 유저 요구사항 분석 + 기존 코드 검토
## 3. RE 참조
`docs/re-findings/`에서 관련 Speakly 패턴 확인
## 4. 현재 코드 파악
기존 구현 검토, 영향 범위 분석
## 5. 구현 (순서 중요)
- shared/ 타입 정의
- 메인 프로세스 서비스
- IPC 핸들러
- Preload 브릿지
- 렌더러 UI (d3roPalette + DS 컴포넌트 활용)
- i18n 키 추가 (ko/en)
## 6. 검증
`npm run typecheck` 통과
## 7. 후속 워크플로우
구현 완료 후 아래 순서로 유저에게 안내:
1. `/review-phase $ARGUMENTS` 실행을 제안 (코드 리뷰)
2. 리뷰 통과 시 `/test-and-commit` 실행을 제안 (테스트 + 커밋)
3. `memory/project_status.md` 갱신 (즉시, 미루지 말 것)
## 구현 규칙 (위반 불가) ## 구현 규칙 (위반 불가)
- 서비스는 **설계서 01**의 인터페이스를 그대로 구현 (메서드 시그니처, 이벤트 페이로드 일치) - 서비스: 싱글톤 + EventEmitter, 설계서 01 인터페이스 준수
- IPC 채널은 **설계서 02**의 `IPC_CHANNELS` 상수를 그대로 사용 (임의 채널명 금지) - IPC: 설계서 02의 IPC_CHANNELS 상수 사용
- 타입은 **설계서 02**의 `shared/types.ts` 정의를 그대로 사용 (임의 타입 금지) - 에러: D3ROError + ErrorCode enum
- 에러는 **설계서 02**의 `D3ROError` + `ErrorCode` enum 사용 - DB: drizzle-orm 스키마
- DB는 **설계서 03**의 drizzle-orm 스키마 그대로 사용 - UI: d3roPalette/d3roTypo/d3roShadow 토큰만 (hex 리터럴 금지)
- UI는 **설계서 03**의 컴포넌트 Props/상태 명세 준수 - i18n: t() 함수 (하드코딩 문자열 금지)
- 팝업은 **설계서 03**의 HTML/CSS/애니메이션 수학 공식 준수 - any 타입 절대 금지, console.log 절대 금지
## 완료 조건
- 모든 태스크 completed
- `npm run typecheck` 통과
- `npm run test` 통과
- CLAUDE.md "현재 상태" 업데이트됨
- Memory `project_status.md` 갱신됨 (Phase 완료 내역, 기술 결정, 다음 작업)

View file

@ -1,27 +1,39 @@
--- ---
name: review-phase name: review-phase
description: 완료된 페이즈를 검토하고 다음 페이즈 준비 상태를 판단. 페이즈 번호를 인자로 전달. description: 완료된 페이즈를 검토하고 다음 페이즈 준비 상태를 판단. 페이즈 번호를 인자로 전달.
argument-hint: "[phase number, e.g. 1]" argument-hint: "[phase number or 'all' for full review]"
allowed-tools: Read Glob Grep Bash Agent allowed-tools: Read Edit Write Glob Grep Bash Agent
--- ---
# Phase Review # Code Review
완료된 페이즈를 검토합니다. 완료된 페이즈 또는 전체 코드베이스를 검토합니다.
## 검토 항목 ## 검토 항목
1. **요구사항 충족**: `docs/phases/phase-$ARGUMENTS.md` 대비 구현 완성도 1. **요구사항 충족**: phase 문서 또는 유저 요구사항 대비 구현 완성도
2. **Speakly 패턴 적용**: RE 노하우가 제대로 반영되었는지 2. **Speakly 패턴 적용**:
- 상태 머신 설계가 Speakly 수준인가? - 상태 머신 (RecognitionState + AudioState 분리)
- 에러 처리가 포괄적인가? - 이중 조건 플러시
- 윈도우 관리가 프리로딩 + 2-phase 리사이즈인가? - 프리로딩 + 2-phase 리사이즈
3. **코드 품질**: TypeScript strict, 일관된 네이밍, 중복 없음 - 에러 처리 포괄성
4. **테스트 커버리지**: 핵심 서비스에 단위 테스트 존재 3. **SSOT 준수** (Grep 도구 사용):
- hex 하드코딩: `src/renderer/``.tsx`/`.ts`에서 `'#[0-9a-fA-F]{6}'` 검색 (theme.ts 제외 0건이어야 함)
- i18n: `.tsx` 파일에서 한국어(`[\uAC00-\uD7AF]`) 또는 JSX 내 영어 문자열 리터럴 검색
- CSS var 폴백 hex는 허용 (예: `var(--color, #fff)`)
4. **코드 품질**: TypeScript strict, any 없음, console.log 없음
5. **타입 안전성**: `npm run typecheck` 통과 5. **타입 안전성**: `npm run typecheck` 통과
6. **빌드**: `npm run build` 통과 6. **빌드**: `npm run build` 통과
## 자동 수정
리뷰 중 발견된 minor 이슈는 유저 확인 후 직접 수정 가능 (Edit/Write 권한 있음):
- CLAUDE.md 서비스 목록/현재 상태 갱신
- memory/project_status.md 갱신
critical/major 이슈는 수정하지 말고 보고만 할 것.
## 출력 ## 출력
- 발견된 이슈 목록 (심각도: critical/major/minor) - 이슈 목록 (심각도: critical/major/minor)
- 다음 페이즈 진행 가능 여부 (GO/NO-GO) - 진행 가능 여부 (GO/NO-GO)
- CLAUDE.md 업데이트 제안 - GO일 경우 `/test-and-commit` 실행을 제안

View file

@ -2,30 +2,54 @@
name: scaffold-component name: scaffold-component
description: D3RO-VOICE 프로젝트 컨벤션에 맞게 새 컴포넌트/서비스를 스캐폴딩. 이름을 인자로 전달. description: D3RO-VOICE 프로젝트 컨벤션에 맞게 새 컴포넌트/서비스를 스캐폴딩. 이름을 인자로 전달.
argument-hint: "[component or service name]" argument-hint: "[component or service name]"
allowed-tools: Read Write Glob allowed-tools: Read Write Glob Grep Edit
--- ---
# Scaffold # Scaffold
프로젝트 컨벤션에 맞는 새 모듈을 생성합니다. 프로젝트 컨벤션에 맞는 새 모듈을 생성합니다.
모듈명: $ARGUMENTS
## 0. 중복 방지 가드 (스킵 불가)
생성 전 반드시 확인:
1. **서비스**: `src/main/services/` 내 동일 이름 파일 Glob 검색. 존재 시 유저에게 확인
2. **컴포넌트**: `src/renderer/components/` 내 동일 이름 Glob 검색. 존재 시 유저에게 확인
3. **IPC 채널**: `shared/ipc-channels.ts`에서 해당 feature 네임스페이스 Grep 검색. 충돌 시 경고
4. **i18n 키**: `src/renderer/locales/ko.json`에서 해당 키 접두사 검색. 충돌 시 경고
## 서비스 스캐폴딩 (메인 프로세스) ## 서비스 스캐폴딩 (메인 프로세스)
Speakly 패턴 적용:
Speakly 패턴:
- 싱글톤 + EventEmitter - 싱글톤 + EventEmitter
- 로거 태그 `[ServiceName]` - 로거 태그 `[ServiceName]` (electron-log, console.log 금지)
- 초기화/종료 메서드 - 초기화/종료 메서드 (`initialize()` / `dispose()`)
- IPC 채널 `shared/ipc-channels.ts`에 추가 - IPC 채널 `shared/ipc-channels.ts`에 추가
- 에러: D3ROError + ErrorCode enum
생성 파일:
- `src/main/services/$ARGUMENTS.ts` — 서비스 본체
- `shared/ipc-channels.ts` — 채널 추가 (Edit)
- `src/main/ipc/` — 핸들러 등록 (Edit)
## React 컴포넌트 스캐폴딩 (렌더러) ## React 컴포넌트 스캐폴딩 (렌더러)
- 함수형 컴포넌트 + TypeScript Props 인터페이스 - 함수형 컴포넌트 + TypeScript Props 인터페이스
- MUI sx prop 기반 스타일링 - 스타일: d3roPalette/d3roTypo/d3roShadow 토큰만 (hex 리터럴 금지)
- electronAPI IPC 호출 패턴 - DS 컴포넌트(MetalCard, PhosphorText, Led, PhysicalButton 등) 적극 활용
- i18n: t() 함수 사용, 하드코딩 문자열 금지
- IPC: window.electronAPI.* 호출
생성 파일:
- `src/renderer/components/$ARGUMENTS.tsx` — 컴포넌트 본체
- `src/renderer/locales/ko.json` — i18n 키 추가 (Edit)
- `src/renderer/locales/en.json` — i18n 키 추가 (Edit)
## Vanilla JS 팝업 스캐폴딩 ## Vanilla JS 팝업 스캐폴딩
Speakly 패턴:
- 별도 HTML 엔트리포인트
- 순수 DOM 조작 (React 미사용)
- 2-phase 리사이즈: prepare(측정) → resize → show
- mouseenter/leave 이벤트 메인 프로세스 전달
모듈명: $ARGUMENTS Speakly 패턴:
- 별도 HTML 엔트리포인트, React 미사용
- 순수 DOM 조작
- 2-phase 리사이즈: prepare(측정) → resize → show
- d3roPalette 토큰의 CSS var 사용

View file

@ -1,32 +1,34 @@
--- ---
name: test-and-commit name: test-and-commit
description: 테스트 실행 후 통과하면 커밋. 코드 변경 완료 후 사용. description: 테스트 실행 후 통과하면 커밋. 코드 변경 완료 후 사용.
allowed-tools: Bash Read allowed-tools: Bash Read Edit Write Glob Grep
--- ---
# Test & Commit # Test & Commit
1. **타입 체크** ## 1. 검증 파이프라인 (순서대로)
```bash
npm run typecheck
```
2. **린트** ```bash
```bash npm run typecheck
npm run lint npm run lint
``` npm run test
```
3. **테스트** ## 2. 실패 시 자동 수정
```bash
npm run test
```
4. **모두 통과 시 커밋** - 에러 메시지를 분석하고 원인 파일을 찾아 수정 (Edit/Write 사용)
- git status로 변경 파일 확인 - 수정 후 실패한 단계부터 재실행
- git diff로 변경 내용 확인 - 최대 3회 재시도. 3회 초과 시 유저에게 보고 후 중단
- 적절한 커밋 메시지 작성 (한글, Conventional Commits) - 수정 범위는 에러를 고치는 최소한으로 제한 (리팩토링 금지)
- Co-Authored-By 절대 추가 금지
5. **실패 시** ## 3. 모두 통과 시 커밋
- 에러 내용 분석
- 수정 후 재시도 - `git status`로 변경 파일 확인
- `git diff`로 변경 내용 확인
- `git log --oneline -5`로 기존 커밋 스타일 확인
- 적절한 커밋 메시지 작성 (한글, Conventional Commits)
- Co-Authored-By, Claude 관련 문구 절대 추가 금지
## 4. 커밋 후
- `memory/project_status.md` 갱신 필요 여부 유저에게 확인

366
CLAUDE.md
View file

@ -1,339 +1,59 @@
# D3RO-VOICE — 로컬 AI 음성 어시스턴트 # D3RO-VOICE — 로컬 AI 음성 어시스턴트
Genspark Speakly를 리버스엔지니어링하여 얻은 노하우를 기반으로 만드는 **완전 로컬** 음성 어시스턴트. Speakly RE 기반 완전 로컬 음성 어시스턴트. Ollama + Whisper + TTS.
클라우드 의존성 없이 Ollama + Whisper + TTS를 사용한다.
## 기술 스택 (고정) ## 기술 스택
- **Framework**: Electron 33+ (contextIsolation: true, nodeIntegration: false) Electron 33+ | React 19 + MUI 7 + Vite | TypeScript 5.7+ strict
- **Frontend**: React 19 + MUI 7 + Vite better-sqlite3 + drizzle-orm | faster-whisper (Python sidecar) | Ollama REST
- **Language**: TypeScript 5.7+ (strict mode) uiohook-napi | @nut-tree-fork/nut-js | electron-store | electron-log
- **DB**: better-sqlite3 + drizzle-orm
- **STT**: faster-whisper (Python sidecar) 또는 whisper.cpp
- **TTS**: piper-tts 또는 kokoro
- **LLM**: Ollama REST API (localhost:11434)
- **Audio**: Web Audio API (renderer) + node-record-lpcm16 (main)
- **Hotkey**: uiohook-napi (글로벌 키보드 후킹)
- **Text Insert**: @nut-tree/nut-js (클립보드 + Ctrl+V)
- **Config**: electron-store
## 프로젝트 구조 ## 빌드
```
D3ROVoice/
├── src/
│ ├── main/ # Electron 메인 프로세스
│ │ ├── services/ # STT, TTS, LLM, Audio, Hotkey, TextInsert
│ │ ├── windows/ # 윈도우 매니저 (팁, 팝업, 메인)
│ │ ├── ipc/ # IPC 핸들러 등록
│ │ ├── db/ # SQLite (history, dictionary)
│ │ └── index.ts # 앱 진입점
│ ├── renderer/ # React 앱
│ │ ├── components/ # React 컴포넌트
│ │ ├── popups/ # Vanilla JS 팝업 (recording-tip, result-popup)
│ │ └── main.tsx # 렌더러 진입점
│ ├── preload/ # contextBridge IPC 브릿지
│ │ └── index.ts
│ └── shared/ # 공유 타입, IPC 채널 정의, 에러 코드
│ ├── ipc-channels.ts
│ ├── types.ts
│ └── errors.ts
├── docs/ # 설계 문서
│ ├── phases/ # 페이즈별 요구사항
│ └── re-findings/ # Speakly 리버스엔지니어링 결과
├── scripts/ # 빌드/유틸 스크립트
└── tests/
```
## 빌드 & 실행
```bash ```bash
npm run dev # Electron + Vite dev server npm run dev # Electron + Vite dev
npm run build # 프로덕션 빌드 npm run build # 프로덕션 빌드
npm run test # vitest 테스트 npm run test # vitest
npm run lint # eslint + prettier npm run typecheck # tsc --noEmit
npm run typecheck # tsc --noEmit
``` ```
## 코딩 규칙 ## 코딩 규칙
- TypeScript strict mode 필수 (noImplicitAny, strictNullChecks) - strict mode, any 금지, console.log 금지 (logger 사용)
- 함수형 React 컴포넌트 + hooks만 사용 - 서비스: 싱글톤 + EventEmitter
- 메인 앱 = React + MUI, 팝업 윈도우 = Vanilla JS (Speakly 패턴) - IPC: `${feature}:${action}`, D3ROError + ErrorCode
- IPC 채널명: `${feature}:${action}` (예: `voice:startRecording`, `config:getLanguage`) - 메인앱=React+MUI, 팝업=Vanilla JS
- 에러 처리: NXError 패턴 (에러 코드 + 메시지) - 테마 SSOT: d3roPalette/d3roShadow만 사용, hex 하드코딩 금지 (theme.ts 제외)
- 로거: electron-log 사용, console.log 금지 - i18n: t() 함수, 하드코딩 한국어/영어 금지
- Co-Authored-By, Claude 관련 커밋 문구 금지
## 절대 하지 말 것 ## 핵심 패턴 (Speakly 차용)
- any 타입 사용 금지 - RecognitionState(9) + AudioState(4) 이중 상태머신
- renderer에서 Node.js API 직접 import 금지 - 이중 조건 플러시 (모델 로딩 + 오디오 버퍼링 병렬)
- electron의 remote 모듈 사용 금지 - 텍스트 삽입: clipboard save→set→Ctrl+V→restore
- console.log 남기지 말 것 (logger 사용) - 윈도우: 프리로딩 + 2-phase 리사이즈
- 하드코딩된 시크릿/비밀번호 금지 - 녹음 UI: 9개 웨이브 바, cos 분포, 100ms
- Co-Authored-By, Claude 관련 문구 커밋 메시지에 추가 금지
## Speakly에서 채택한 핵심 패턴
1. **상태 머신**: RecognitionState + AudioState 분리 추적
2. **이중 조건 플러시**: 모델 로딩 + 오디오 버퍼링 동시 진행, 둘 다 준비 시 플러시
3. **텍스트 삽입**: 클립보드 save → set → Ctrl+V → restore
4. **윈도우 관리**: 프리로딩 + 2-phase 리사이즈 (측정→resize→show)
5. **팝업**: 메인 앱=React, 경량 팝업=Vanilla JS
6. **녹음 UI**: 9개 웨이브 바, cos 분포 가중치, 100ms 애니메이션
## 현재 상태 ## 현재 상태
Phase: 13 전체 완료 + 랜딩 페이지 완성 (Phase 1~13 + site/) Phase 1~14 전체 완료 + 랜딩 페이지(site/).
마지막 작업: 랜딩 페이지 디자인 통합 + 10개 국어 i18n
다음 작업: 빌드/배포 안정화, E2E 테스트, 또는 새 피처
차단 이슈: @nut-tree-fork/nut-js 포크 사용 차단 이슈: @nut-tree-fork/nut-js 포크 사용
### 랜딩 페이지 (site/) ## 설계 문서 (구현 시 Read 도구로 참조)
- Vite + React 19 + TypeScript + Tailwind CSS - docs/design/00-master-architecture.md — 시스템 아키텍처, 초기화/종료, 서비스 목록
- 4가지 디자인 레퍼런스 통합 (KKG 3종 + Meteorological → D3RO 인더스트리얼) - docs/design/01-service-specifications.md — 서비스 인터페이스, 상태머신, 이벤트
- SSOT: tokens.ts + tailwind.config.js 토큰 중앙 관리 - docs/design/02-ipc-and-types.md — IPC 채널 전체, 타입, 에러코드, preload API
- 재사용 컴포넌트 8개: Badge/Container/Crosshair/DataPoint/GlowButton/InstrumentCard/Led/SectionHeader - docs/design/03-db-and-ui.md — DB 스키마, React 컴포넌트, 팝업 명세, MUI 테마
- CSS 이펙트: CRT scanline, crosshair, noise texture, glow button (WebGL 없음)
- i18n 10개 국어: en/ko/ja/zh/es/fr/de/pt/ru/vi (Context+Hook, LanguageSwitcher, CJK line-height 보정)
- GitHub Pages 자동 배포: .github/workflows/deploy-site.yml
### DS 컴포넌트 현황 (src/renderer/components/ds/) ## 서비스 목록 (src/main/services/)
- CrtDisplay.tsx: WebGL CRT 셰이더 (파형+스캔라인+비네팅+글리치) ✅ Core: LoggerService, ConfigService, VoiceModeService(오케스트레이터)
- InstrumentPanel.tsx: 메탈 섀시 컨테이너 (각인 텍스트, 노이즈) ✅ Audio: AudioCaptureService, LocalSTTService, LocalLLMService, TTSPlaybackService
- Led.tsx: LED 인디케이터 (amber/green/red/orange, pulse) ✅ Input: HotkeyService, TextInsertService, SoundEffectService, AutoLaunchService
- PhysicalButton.tsx: 물리 버튼 (돌출 그림자, 눌림 피드백) ✅ Data: HistoryService, DictionaryService, CustomInstructionService, LicenseService
- MetalCard.tsx: 메탈 카드 컨테이너 ✅ Phase10+: MemoService, VoiceCommandService, ScreenContextService, ChainService, CaptionService
- PhosphorText.tsx: 인광 텍스트 (13종 변형: hero/title/value/heading/body/compact/small/meta/label/dim/engrave/micro/nano) ✅ Phase12+: FileTranscriptionService, MeetingSummaryService, DictationTemplateService
- MetalDial.tsx: 메탈 다이얼 (동심원 텍스처, 드래그 회전, conic-gradient 금속 광택) ✅ Phase13+: VoiceConversationService, RAGService, VoiceActionService
- ScreenPanel.tsx: 글래스 스크린 패널 (CRT 없는 인셋 베젤 + 반사) ✅ Phase14: MeetingModeService
- ButtonGroup.tsx: 인셋 버튼 클러스터 (레퍼런스 .button-group) ✅
### 디자인 토큰 현황 (theme.ts) ## DS 컴포넌트 (src/renderer/components/ds/)
- d3roTypo: 13단계 타이포 (hero 42px ~ nano 7px, size/weight/spacing/line) CrtDisplay, InstrumentPanel, Led, PhysicalButton, MetalCard,
- d3roShadow: 10종 그림자 (chassis/card/inset/button/dialog/tooltip 등) PhosphorText(13변형), MetalDial, ScreenPanel, ButtonGroup
- d3roRadius: 7종 반경 (outer 24px ~ pill 999px)
### Phase 13.2 구현 내용 (로컬 RAG) ## 디자인 토큰 (theme.ts)
- **RAGService**: Ollama nomic-embed-text 임베딩, SQLite에 JSON 벡터 저장, 코사인 유사도 검색 d3roPalette(색상) | d3roTypo(13단계) | d3roShadow(10종) | d3roRadius(7종)
- **문서 관리**: .txt/.md/.pdf/.docx 지원, 500자 청크 분할 (50자 오버랩), 비동기 인덱싱
- **벡터 검색**: 쿼리 임베딩 → 전체 청크 코사인 유사도 → topK 결과 → LLM 컨텍스트 주입
- **KnowledgeBasePage**: 전용 UI (문서 목록, 인덱싱 진행률, 질문 입력, 답변+참조 소스 표시)
- **AppLayout**: 'knowledge' 라우트 + RAG 네비 항목
- **DB 테이블**: rag_documents + rag_chunks (Phase 13.2 신규)
- **IPC**: RAG 네임스페이스 9채널
- **ErrorCode**: 870-874 (DocumentNotFound, IndexingFailed, EmbeddingFailed, QueryFailed, UnsupportedFormat)
### Phase 13.3 구현 내용 (OS 자동화)
- **VoiceActionService**: 음성→LLM이 JSON 액션 플랜 생성→실행, 프리셋 6개 (크롬/메모장/탐색기/볼륨)
- **액션 타입**: open_app, open_url, open_file, keyboard_shortcut, type_text, system_command
- **안전장치**: 위험 명령어 블랙리스트 (rm, del, format, shutdown 등), safe=false 시 차단
- **IPC**: VOICE_ACTION 네임스페이스 9채널
- **ErrorCode**: 875-878 (PlanFailed, ExecutionFailed, Blocked, InvalidPlan)
### Phase 13.1 구현 내용 (음성 대화 모드)
- **VoiceConversationService**: STT→LLM→TTS 대화 루프 오케스트레이션, 세션 관리, 대화 히스토리 최근 10턴
- **TTSPlaybackService**: Windows SAPI (PowerShell System.Speech) 기반 로컬 TTS, 문장 단위 큐 재생
- **LocalLLMService.chatStream**: Ollama /api/chat 스트리밍, 대화 히스토리 messages 배열 전달
- **VoiceConversationPage**: 전용 대화 UI (채팅 메시지 목록, 녹음/정지/취소 버튼, 텍스트 입력)
- **AppLayout**: 'conversation' 라우트 + TALK 네비게이션 항목 추가
- **IPC**: VOICE_CONVERSATION 네임스페이스 15채널 (startSession, sendMessage, assistantDelta 등)
- **Preload**: voiceConversation API 네임스페이스 (17메서드/이벤트)
- **ErrorCode**: 795-798 (ConversationSessionAlreadyActive, TTSFailed, LLMFailed 등)
- **i18n**: conversation.* 번역 키 추가 (ko/en)
### Phase 12 구현 내용 (Pro+ 피처)
- **FileTranscriptionService**: ffmpeg(fluent-ffmpeg + @ffmpeg-installer/ffmpeg)로 미디어→PCM 변환, 30초 청크 분할 STT, 진행률 이벤트, 히스토리 자동 저장 (mode: 'file-transcription')
- **MeetingSummaryService**: CaptionService session-saved 이벤트 → Ollama 자동 요약, 마크다운 파싱 (요약/결정/할일), history.summaryText 컬럼 저장, 마크다운 내보내기
- **DictationTemplateService**: 템플릿 CRUD (electron-store), 세션 상태 머신 (idle→field-prompting→field-recording→completing), 프리셋 3개 (이메일/회의록/보고서), 출력 포맷 mustache 치환
- **FileDropZone UI**: DashboardPage 드래그앤드롭 존, 진행률/결과/에러 상태 표시
- **HistoryEntryCard 확장**: 요약 배지(Chip), 확장 뷰(요약/결정/할일), 요약 생성/내보내기 버튼
- **TemplateSection UI**: CommandsPage 하단 섹션, 템플릿 목록/생성/편집/삭제/세션 시작
- **DB 마이그레이션**: history 테이블 summary_text 컬럼 추가 (ALTER TABLE), mode enum에 'file-transcription' 추가
- **IPC**: FILE_TRANSCRIPTION(6채널) + MEETING_SUMMARY(5채널) + DICTATION_TEMPLATE(11채널) = 22개 신규 채널
- **Preload**: fileTranscription/meetingSummary/dictationTemplate 3개 API 네임스페이스
- **ErrorCode**: 780-794 범위 (FFmpegFailed, InvalidFormat, ChunkFailed, SummaryGenerationFailed, TemplateNotFound 등)
- **i18n**: fileTranscription.*/meetingSummary.*/template.* 번역 키 추가 (ko/en)
### Phase 11 구현 내용 (수익화 기반)
- **LicenseService**: Free/Pro/Pro+ 3단계 티어, electron-store 대신 별도 JSON 파일, 오프라인 우선 설계
- **Feature Gating**: Feature enum (16개 기능), 티어별 접근 매핑, 쿼터 한도 (Free: 받아쓰기 20회/일, LLM 10회/일)
- **daily_usage DB 테이블**: 날짜+기능별 사용량 추적 (drizzle-orm, UPSERT)
- **LemonSqueezy 연동**: 키 활성화/검증 API, variant_name→tier 매핑, 오프라인 유예 30일
- **IPC**: LICENSE 네임스페이스 9개 채널 (getInfo, activate, deactivate, checkFeature, getUsage, getAllUsage, getTierComparison, upgradePrompt, tierChanged)
- **Preload**: license API 네임스페이스 추가 (9개 메서드 + 2개 이벤트 리스너)
- **ErrorCode**: 850-862 범위 라이센스 에러 (LicenseKeyInvalid, QuotaExceeded, TierRequired 등)
- **UpgradePromptModal**: 쿼터 소진/잠긴 기능 접근 시 업그레이드 유도 모달 (혜택 목록, 쿼터 바)
- **SettingsModal License 탭**: 티어 표시, 키 입력/활성화, 사용량 바, 티어 비교표
- **DashboardPage 사용량**: Free 티어에서 오늘 사용량 바 (받아쓰기/LLM) 표시
- **ProBadge + useProFeature**: 잠긴 기능 오버레이 래퍼 컴포넌트 + 훅
- **VoiceModeService gating**: startSession에서 DICTATION 쿼터 체크/소비
- **LocalLLMService gating**: processText에서 LLM_PROCESS 쿼터 체크/소비 (차단 시 원본 텍스트 폴백)
- **i18n**: license.* 네임스페이스 40+ 키 추가 (ko/en)
### Phase 10 구현 내용 (서비스 레이어 + UI 통합)
- **MemoService**: memo_tags DB 테이블 + 태그 CRUD + 마크다운 내보내기
- **VoiceCommandService**: 키워드→명령어 매칭 엔진, 프리셋 키워드 4종 (번역/요약/다듬기/설명)
- **ScreenContextService**: 활성 윈도우 감지(PowerShell) + 선택 텍스트 캡처(Ctrl+C)
- **ChainService**: LLM 명령어 순차 실행 파이프라인, 단계별 진행 이벤트
- **CaptionService**: 3초 청크 연속 전사, caption-overlay 팝업(Vanilla JS)
- **VoiceModeService 통합**: 파이프라인 훅 3개 (컨텍스트 캡처→키워드 매칭→체인/컨텍스트 주입)
- **공유 컴포넌트**: EmptyStateCard, SearchInput, PageHeader, HistoryEntryCard
- **공유 유틸**: formatters.ts (formatDuration, getDateKey, formatNumber 등)
- **IPC**: 5개 새 네임스페이스 (MEMO, VOICE_COMMAND, CONTEXT, CHAIN, CAPTION)
- **Preload**: 5개 API 네임스페이스 추가 (memo, voiceCommand, context, chain, caption)
- **HistoryPage 태그 통합**: HistoryEntryCard에 태그 표시/추가/삭제, 태그 필터 바, 마크다운 내보내기
- **CommandsPage 키워드/체인**: VoiceCommandService 키워드 편집 섹션 + ChainService 체인 CRUD/실행 UI
- **DashboardPage 자막 토글**: 실시간 자막 시작/종료 버튼, CaptionState LED 표시
- **SettingsModal 설정 추가**: 음성 명령어 on/off, 스크린 컨텍스트 on/off 토글 (LLM 탭)
- **i18n**: memo/voiceCommand/chain/context/dashboard.caption 번역 키 추가 (ko/en)
### Phase 9 구현 내용
- **Settings About 탭**: 앱/Electron 버전, 기술 스택, 프로젝트 링크, 정밀기기 디자인
- **녹음 WAV 저장**: VoiceModeService 세션 완료 시 WAV 파일 저장 + HistoryService audioLocalPath 연동
- **마이크 테스트 UI**: Settings Audio 탭 레벨 바 (3초 캡처, audio-level 이벤트)
- **온보딩 플로우**: 4단계 위저드 (환영→마이크 설정→핫키 설정→Ollama 설정→완료)
- **MetalDial 컴포넌트**: 동심원 텍스처, 드래그 회전, 금속 광택 — 시안 A 핵심 요소 완성
- **오디오 디바이스 선택**: AudioEndpoint + 디바이스 캐싱, Settings에서 실시간 전환
- **Commands CRUD IPC 연결**: instruction:* 핸들러 완전 연동 + 활성 명령어 선택 UI
- **Ctrl+Shift+C 커맨드 팝업**: 커서 위치에 명령어 선택 팝업, 키보드 탐색 (Arrow/Enter/ESC)
- **Dictionary 편집**: 인라인 편집 + STT initialPrompt에 사전 단어 주입
- **Ollama 설치 안내**: OllamaGuideModal + 넛징 버블 (Ollama 미실행 시 자동 표시)
- **실시간 UI 갱신**: app:dataChanged IPC 이벤트 → Dashboard/History/Dictionary 자동 리프레시
- **Settings 속도 개선**: 비동기 디바이스 로드 + 부팅 시 프리캐싱으로 모달 열기 지연 제거
### Phase 8 구현 내용
- **SSOT 완료**: d3roPalette에 11개 토큰 추가 (sidebar, chassis, inactive, dimLabel 등)
- **매직넘버 제거**: tsx/ts 파일 매직넘버 0개 달성 (theme.ts 제외)
- **DS 컴포넌트 SSOT**: 6개 모두 d3roPalette 참조로 교체
- **HotkeyRecordModal**: 신규 — 커스텀 핫키 녹화 모달 (키 조합 입력→Chip 표시→저장)
- **SettingsModal 재작성**: 음성 모드 3개(받아쓰기/Agent/원터치) 토글+핫키 표시+변경 UI
- **DashboardPage 재작성**: 기능 중심 — Hero(핫키 표시) + 통계 4카드 + CRT 서비스 상태 + 최근 히스토리(날짜 그룹핑)
- **recording-tip 수정**: 웨이브 바/프로그레스 바 색상 #1F5DF2(파란)→#f25b29(앰버)
- **페이지 SSOT**: AppLayout, StatusBar, HistoryPage, DictionaryPage, CommandsPage 모두 완료
- **설계 문서**: docs/phases/phase-8.md, docs/re-findings/speakly-settings-ui.md 작성
### Phase 7.5 구현 내용
- SoundEffectService: WAV 프리로드, fire-and-forget, PowerShell SoundPlayer로 재생
- AutoLaunchService: app.setLoginItemSettings, ConfigService 연동, syncWithConfig
- TextInsertService: 간이 삽입 검증 (EditMonitor 경량), 클립보드 확인
- VoiceModeService 효과음 연동: session-started(start), completed(stop), cancelled(cancel), error(error)
- IPC: system:playSound/setSoundEnabled/isSoundEnabled, config:setAutoLaunch/setCloseToTray
- Bootstrap: sound-effects, auto-launch 초기화 단계 추가
- 효과음 WAV 파일: scripts/generate-sounds.js로 생성 (recording-start/stop, error)
### Phase 7 구현 내용
- vitest 테스트 환경: vitest.config.ts, tests/setup.ts (electron/electron-log 모킹)
- 테스트 헬퍼: tests/helpers/createTestDb.ts (in-memory SQLite + drizzle)
- 단위 테스트 41개: HistoryService, DictionaryService, CustomInstructionService, VoiceModeService, D3ROError
- electron-builder: electron-builder.yml (NSIS, asarUnpack, extraResources)
- GitLab CI/CD: .gitlab-ci.yml (lint, typecheck, test, build, release)
- 빌드 스크립트: pack, dist, test:unit 추가
- 참고: better-sqlite3는 Electron용 빌드라 vitest에서 직접 사용 불가 → DB 서비스는 모킹 테스트
- 번들링 인프라: SoX 다운로드 스크립트, PyInstaller 빌드 스크립트, 경로 해상도 유틸
- src/main/utils/paths.ts: dev vs production 경로 자동 감지 (SoX, sidecar, sounds)
- AudioCaptureService: 번들 SoX 경로 사용 (getSoxPath)
- LocalSTTService: 번들 sidecar 경로 사용 (getSidecarCommand)
- scripts/download-sox.ps1: SoX Windows 바이너리 다운로드 → resources/sox/
- scripts/build-sidecar.py: PyInstaller → sidecar-dist/sidecar/sidecar.exe
### Phase 6 구현 내용
- CustomInstructionService: electron-store 기반, 프리셋 5개 (번역/요약/전문리라이트/코드설명/자유프롬프트)
- 커스텀 명령어 CRUD: create/update/delete/reorder, 프리셋 보호 (삭제 불가)
- CommandsPage: React MUI 명령어 목록 + 추가/편집 다이얼로그
- i18n: ko.json/en.json 리소스, t() 함수, React 컨텍스트 (useI18n)
- IPC: instruction:getAll/getById/create/update/delete/reorder 핸들러
### Phase 3.5 구현 내용
- HistoryPopup: Vanilla JS 팝업, 다크 카드(#242427), 앰버 악센트(#f25b29)
- Ctrl+Shift+V 글로벌 단축키 → 커서 위치에 최근 10건 히스토리 팝업
- Arrow↑↓ 선택, Enter 붙여넣기, 1-9 직접 선택, ESC 닫기
- focusable: false → 활성 앱 포커스 유지
- 2-phase 리사이즈, 등장/퇴장 애니메이션 (0.15s/0.1s)
### Phase 5 구현 내용
- DB: better-sqlite3 + drizzle-orm (history/dictionary/stats 테이블, WAL 모드)
- HistoryService: CRUD + 검색 + 통계 + 보존 정책(30일), 세션 완료 시 자동 이력 저장
- DictionaryService: CRUD + 검색 + 사용 횟수 추적 + STT 프롬프트 힌트
- History UI: 목록 + 검색 + 삭제 + 복사 + 페이지네이션
- Dictionary UI: 목록 + 검색 + 추가 다이얼로그 + 삭제
- Dashboard: 실데이터 통계 (총/오늘 세션수, 시간, 단어수, 연속일수)
- IPC: history, dictionary, stats 핸들러 + preload API
- Bootstrap: DB 초기화 단계 추가 (critical)
### Phase 4 구현 내용
- LocalLLMService: Ollama REST API 연동, 스트리밍 NDJSON 파싱, 가용성 폴링(5초)
- 시스템 프롬프트: refine/translate/summarize/grammar/expand/custom 6개 액션
- VoiceModeService LLM 연동: 전사→LLM 후처리→텍스트 삽입, LLM 실패 시 원본 폴백
- LLM IPC 핸들러: llm:getStatus/getModels/setModel/process/cancelProcess/getServerUrl/setServerUrl
- StatusBar: Ollama 연결 상태 + 활성 모델 표시
- Bootstrap: LLM 가용성 폴링 초기화 단계 추가
### Phase 3 구현 내용
- TextInsertService: clipboard save→set→Ctrl+V→restore (@nut-tree-fork/nut-js, lazy dynamic import)
- RecordingTip 팝업: Vanilla JS, 9개 웨이브바 cos분포 가중치, thinking 점근수렴, 2-phase 리사이즈
- ResultPopup 팝업: Vanilla JS, 복사 버튼, auto-close(5초), 마우스 호버 유지, 다크모드
- WindowManager 리팩토링: 팝업 프리로딩, 커서 위치 표시, 멀티모니터 보정
- Settings 모달: General/Audio/STT/LLM 탭, 실시간 설정 변경
- VoiceModeService 연동: 전사 완료 시 자동 텍스트 삽입 + RecordingTip→ResultPopup 전환
- Bootstrap: 9단계 초기화 (popup-preload 추가)
- electron-vite: 팝업 HTML 멀티 엔트리 + popup preload 빌드
### Phase 2 구현 내용
- AudioCaptureService: node-record-lpcm16 + SoX 실제 마이크 캡처 (PCM16 16kHz mono, 60ms 프레임, RMS 레벨)
- HotkeyService: uiohook-napi 글로벌 키후킹, 더블프레스(300ms), holdMode/toggleMode, VK→uiohook 매핑
- LocalSTTService: faster-whisper Python sidecar 관리, STTState 상태머신, 이중 조건 플러시, 자동 재시작(3회)
- VoiceModeService: RecognitionState(9상태) + AudioState(4상태) 이중 상태머신, Action Queue 직렬화, accidentalPress(<700ms), Dictation/HandsFree 모드
- Python sidecar: FastAPI (GET /health, POST /load, POST /transcribe, POST /shutdown)
- IPC 핸들러: voice, stt, hotkey 추가 (기존 audio, config, window, system에 추가)
- Preload: voice/stt/hotkey API 전체 노출
- Bootstrap: 7단계 초기화 (logger→config→windows→tray→ipc→hotkey→voice-mode)
### Phase 1 구현 내용
- 프로젝트 초기화: package.json, TypeScript strict, electron-vite, ESLint, Prettier
- shared 타입: ipc-channels.ts (113채널), types.ts, errors.ts (ErrorCode enum), constants.ts
- 메인 프로세스: index.ts (단일 인스턴스), bootstrap.ts, lifecycle.ts
- 서비스: LoggerService, ConfigService (ESM dynamic import)
- Renderer: React 19 + MUI 7, AppLayout (Drawer 240px), DashboardPage
- 시스템 트레이, closeToTray
## 페이즈 로드맵
- Phase 1: 프로젝트 초기화 + Electron 뼈대 + 마이크 캡처
- Phase 2: 로컬 STT 연동 (Whisper) + 핫키
- Phase 3: 텍스트 삽입 + 기본 UI (Dashboard, RecordingTip)
- Phase 3.5: 커서 위치 히스토리 팝업 (D3RO 고유 기능)
- Phase 4: Ollama LLM 연동 (텍스트 다듬기, 번역)
- Phase 5: TTS + 히스토리/사전 DB
- Phase 6: 커스텀 명령어 + 설정 UI 고도화
- Phase 7: 테스트 + 빌드 + 배포
- Phase 7.5: Speakly 패턴 보강 (SoundEffect, AutoLaunch, EditMonitor, 오디오 뮤트)
- Phase 8: UI 디자인 재설계 + SSOT + DS 컴포넌트 정비
- Phase 9: 품질 보강 + UX 개선 (온보딩, 마이크 테스트, WAV 저장, 커맨드 팝업, Ollama 안내)
- Phase 10: 킬러 피처 — Speakly를 넘어서 (실시간 자막, 스크린 컨텍스트, 음성 메모장, 멀티 LLM 체인, 음성 단축키)
- Phase 11: 수익화 기반 — LicenseService + Feature Gating + Freemium UI
- Phase 12: Pro 피처 — 파일 전사, 회의록 자동 요약, 딕테이션 템플릿 ✅
- Phase 13.1: Pro+ 프리미엄 — 음성 대화 모드 (STT→LLM→TTS 루프) ✅
- Phase 13.2: Pro+ 프리미엄 — 로컬 RAG (문서 임베딩+검색) ✅
- Phase 13.3: Pro+ 프리미엄 — OS 자동화 (음성→JSON 액션) ✅
## 설계 문서 (구현 시 반드시 참조)
@docs/design/00-master-architecture.md
@docs/design/01-service-specifications.md
@docs/design/02-ipc-and-types.md
@docs/design/03-db-and-ui.md
## 페이즈 문서
@docs/phases/phase-1.md
@docs/phases/phase-2.md
@docs/phases/phase-3.md
@docs/phases/phase-3.5.md
@docs/phases/phase-4.md
@docs/phases/phase-5.md
@docs/phases/phase-6.md
@docs/phases/phase-7.md
@docs/phases/phase-7.5.md
@docs/phases/phase-8.md
@docs/phases/phase-9.md
@docs/phases/phase-10-killer-features.md
@docs/phases/phase-11-monetization.md
@docs/phases/phase-12-pro-features.md
## RE 노하우 (패턴 적용 근거)
@docs/re-findings/speakly-architecture.md
@docs/re-findings/voice-pipeline-patterns.md
@docs/re-findings/text-insertion-patterns.md
@docs/re-findings/ui-patterns.md
@docs/re-findings/native-dll-patterns.md

View file

@ -58,6 +58,7 @@ export async function bootstrap(): Promise<void> {
{ name: 'voice-mode', critical: false, fn: initVoiceMode }, { name: 'voice-mode', critical: false, fn: initVoiceMode },
{ name: 'llm-polling', critical: false, fn: initLLMPolling }, { name: 'llm-polling', critical: false, fn: initLLMPolling },
{ name: 'meeting-summary-wiring', critical: false, fn: initMeetingSummaryWiring }, { name: 'meeting-summary-wiring', critical: false, fn: initMeetingSummaryWiring },
{ name: 'meeting-mode', critical: false, fn: initMeetingMode },
] ]
for (const step of steps) { for (const step of steps) {
@ -273,6 +274,12 @@ async function initMeetingSummaryWiring(): Promise<void> {
const summaryService = getMeetingSummaryService() const summaryService = getMeetingSummaryService()
captionService.on('session-saved', (summary: { sessionId: string }) => { captionService.on('session-saved', (summary: { sessionId: string }) => {
// 회의 모드가 CaptionService를 사용 중이면 MeetingSummary 자동 생성 건너뛰기
const { getMeetingModeService } = require('./services/MeetingModeService') as { getMeetingModeService: () => { isMeetingModeActive: () => boolean } }
if (getMeetingModeService().isMeetingModeActive()) {
logger.info('Skipping auto meeting summary: meeting mode active')
return
}
summaryService.onCaptionSessionSaved(summary).catch((err) => { summaryService.onCaptionSessionSaved(summary).catch((err) => {
logger.warn('Auto meeting summary failed:', err) logger.warn('Auto meeting summary failed:', err)
}) })
@ -282,6 +289,12 @@ async function initMeetingSummaryWiring(): Promise<void> {
} }
} }
async function initMeetingMode(): Promise<void> {
// MeetingModeService 싱글톤 초기화 (첫 접근 시 인스턴스 생성)
const { getMeetingModeService } = await import('./services/MeetingModeService')
getMeetingModeService()
}
// ── HistoryPopup IPC 연동 ──────────────────────────── // ── HistoryPopup IPC 연동 ────────────────────────────
function setupHistoryPopupIPC(): void { function setupHistoryPopupIPC(): void {

View file

@ -129,6 +129,40 @@ export function initDatabase(): BetterSQLite3Database<typeof schema> {
CREATE INDEX IF NOT EXISTS idx_rag_chunks_document_id ON rag_chunks(document_id); CREATE INDEX IF NOT EXISTS idx_rag_chunks_document_id ON rag_chunks(document_id);
`) `)
// Phase 14: Meeting Mode 테이블 생성
sqlite.exec(`
CREATE TABLE IF NOT EXISTS meeting_sessions (
id TEXT PRIMARY KEY,
title TEXT,
status TEXT NOT NULL DEFAULT 'recording',
started_at INTEGER NOT NULL,
ended_at INTEGER,
duration_ms INTEGER,
raw_transcript TEXT,
minutes_markdown TEXT,
minutes_json TEXT,
stt_model TEXT,
llm_model TEXT,
stt_latency_ms INTEGER,
llm_latency_ms INTEGER,
error_message TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_meeting_sessions_created_at ON meeting_sessions(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_meeting_sessions_status ON meeting_sessions(status);
CREATE TABLE IF NOT EXISTS meeting_memos (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
content TEXT NOT NULL,
timestamp_ms INTEGER NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_meeting_memos_session_id ON meeting_memos(session_id);
CREATE INDEX IF NOT EXISTS idx_meeting_memos_timestamp ON meeting_memos(timestamp_ms);
`)
// Phase 12.2: history 테이블에 summary_text 컬럼 마이그레이션 // Phase 12.2: history 테이블에 summary_text 컬럼 마이그레이션
try { try {
const columns = sqlite.pragma('table_info(history)') as Array<{ name: string }> const columns = sqlite.pragma('table_info(history)') as Array<{ name: string }>

View file

@ -152,6 +152,56 @@ export type NewRAGDocumentRow = typeof ragDocuments.$inferInsert
export type RAGChunkRow = typeof ragChunks.$inferSelect export type RAGChunkRow = typeof ragChunks.$inferSelect
export type NewRAGChunkRow = typeof ragChunks.$inferInsert export type NewRAGChunkRow = typeof ragChunks.$inferInsert
// ── meetingSessions (Phase 14) ──────────────────────────
export const meetingSessions = sqliteTable(
'meeting_sessions',
{
id: text('id').primaryKey(),
title: text('title'),
status: text('status', {
enum: ['recording', 'processing', 'completed', 'error'],
}).notNull().default('recording'),
startedAt: integer('started_at').notNull(),
endedAt: integer('ended_at'),
durationMs: integer('duration_ms'),
rawTranscript: text('raw_transcript'),
minutesMarkdown: text('minutes_markdown'),
minutesJson: text('minutes_json'),
sttModel: text('stt_model'),
llmModel: text('llm_model'),
sttLatencyMs: integer('stt_latency_ms'),
llmLatencyMs: integer('llm_latency_ms'),
errorMessage: text('error_message'),
createdAt: integer('created_at').notNull(),
updatedAt: integer('updated_at').notNull(),
},
(table) => [
index('idx_meeting_sessions_created_at').on(table.createdAt),
index('idx_meeting_sessions_status').on(table.status),
]
)
// ── meetingMemos (Phase 14) ─────────────────────────────
export const meetingMemos = sqliteTable(
'meeting_memos',
{
id: text('id').primaryKey(),
sessionId: text('session_id').notNull(),
content: text('content').notNull(),
timestampMs: integer('timestamp_ms').notNull(),
createdAt: integer('created_at').notNull(),
},
(table) => [
index('idx_meeting_memos_session_id').on(table.sessionId),
index('idx_meeting_memos_timestamp').on(table.timestampMs),
]
)
export type MeetingSessionRow = typeof meetingSessions.$inferSelect
export type NewMeetingSessionRow = typeof meetingSessions.$inferInsert
export type MeetingMemoRow = typeof meetingMemos.$inferSelect
export type NewMeetingMemoRow = typeof meetingMemos.$inferInsert
// ── 타입 추출 ──────────────────────────────────────────── // ── 타입 추출 ────────────────────────────────────────────
export type History = typeof history.$inferSelect export type History = typeof history.$inferSelect
export type NewHistory = typeof history.$inferInsert export type NewHistory = typeof history.$inferInsert

View file

@ -23,6 +23,7 @@ import { registerTemplateHandlers } from './template-handlers'
import { registerVoiceConversationHandlers } from './voice-conversation-handlers' import { registerVoiceConversationHandlers } from './voice-conversation-handlers'
import { registerRAGHandlers } from './rag-handlers' import { registerRAGHandlers } from './rag-handlers'
import { registerVoiceActionHandlers } from './voice-action-handlers' import { registerVoiceActionHandlers } from './voice-action-handlers'
import { registerMeetingModeHandlers } from './meeting-mode-handlers'
import { getLogger } from '../services/LoggerService' import { getLogger } from '../services/LoggerService'
const logger = getLogger('ipc') const logger = getLogger('ipc')
@ -51,5 +52,6 @@ export function registerAllIpcHandlers(): void {
registerVoiceConversationHandlers() registerVoiceConversationHandlers()
registerRAGHandlers() registerRAGHandlers()
registerVoiceActionHandlers() registerVoiceActionHandlers()
registerMeetingModeHandlers()
logger.info('All IPC handlers registered') logger.info('All IPC handlers registered')
} }

View file

@ -0,0 +1,115 @@
// src/main/ipc/meeting-mode-handlers.ts
// Phase 14: Meeting Mode IPC 핸들러
import { ipcMain } from 'electron'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { ErrorCode, ipcSuccess, ipcError } from '@shared/errors'
import { getMeetingModeService } from '../services/MeetingModeService'
import { getLogger } from '../services/LoggerService'
import type {
MeetingAddMemoParams,
MeetingGetSessionsParams,
MeetingGetSessionParams,
MeetingDeleteSessionParams,
MeetingExportParams,
} from '@shared/types'
const logger = getLogger('meeting-mode-handlers')
export function registerMeetingModeHandlers(): void {
const ch = IPC_CHANNELS.MEETING_MODE
ipcMain.handle(ch.START_RECORDING, async () => {
try {
const result = await getMeetingModeService().startRecording()
return ipcSuccess(result)
} catch (err) {
logger.error(`startRecording 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingAlreadyRecording, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.STOP_RECORDING, async () => {
try {
await getMeetingModeService().stopRecording()
return ipcSuccess(undefined)
} catch (err) {
logger.error(`stopRecording 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingNotRecording, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.ADD_MEMO, async (_event, params: MeetingAddMemoParams) => {
try {
const memo = await getMeetingModeService().addMemo(params.content)
return ipcSuccess(memo)
} catch (err) {
logger.error(`addMemo 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingNotRecording, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.GET_STATE, () => {
try {
return ipcSuccess(getMeetingModeService().getStateInfo())
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.GET_SESSIONS, (_event, params: MeetingGetSessionsParams) => {
try {
return ipcSuccess(getMeetingModeService().getSessions(params.page, params.pageSize))
} catch (err) {
return ipcError(ErrorCode.UnknownError, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.GET_SESSION, (_event, params: MeetingGetSessionParams) => {
try {
return ipcSuccess(getMeetingModeService().getSession(params.sessionId))
} catch (err) {
return ipcError(ErrorCode.MeetingSessionNotFound, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.DELETE_SESSION, (_event, params: MeetingDeleteSessionParams) => {
try {
getMeetingModeService().deleteSession(params.sessionId)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.MeetingSessionNotFound, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.UPDATE_TITLE, (_event, params: { sessionId: string; title: string }) => {
try {
getMeetingModeService().updateTitle(params.sessionId, params.title)
return ipcSuccess(undefined)
} catch (err) {
return ipcError(ErrorCode.MeetingSessionNotFound, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.EXPORT_PDF, async (_event, params: MeetingExportParams) => {
try {
const filePath = await getMeetingModeService().exportPdf(params.sessionId)
return ipcSuccess(filePath)
} catch (err) {
logger.error(`exportPdf 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingPdfFailed, err instanceof Error ? err.message : String(err))
}
})
ipcMain.handle(ch.EXPORT_MARKDOWN, async (_event, params: MeetingExportParams) => {
try {
const filePath = await getMeetingModeService().exportMarkdown(params.sessionId)
return ipcSuccess(filePath)
} catch (err) {
logger.error(`exportMarkdown 실패: ${err instanceof Error ? err.message : String(err)}`)
return ipcError(ErrorCode.MeetingExportFailed, err instanceof Error ? err.message : String(err))
}
})
logger.info('Meeting Mode IPC handlers registered')
}

View file

@ -0,0 +1,750 @@
// src/main/services/MeetingModeService.ts
// Phase 14: Meeting Mode — 실시간 녹음 + 타임스탬프 메모 + LLM 구조화 회의록
// 싱글톤 + EventEmitter 패턴. CaptionService 연동.
import { EventEmitter } from 'events'
import { BrowserWindow, Notification, dialog } from 'electron'
import fs from 'fs'
import { nanoid } from 'nanoid'
import { eq, desc, sql } from 'drizzle-orm'
import { getLogger } from './LoggerService'
import { configGet } from './ConfigService'
import { getMainWindow } from '../windows/WindowManager'
import { getDatabase } from '../db'
import { meetingSessions, meetingMemos } from '../db/schema'
import { IPC_CHANNELS } from '@shared/ipc-channels'
import { D3ROError, ErrorCode } from '@shared/errors'
import type {
MeetingModeState,
MeetingMemo,
MeetingSessionSummary,
MeetingSessionDetail,
MeetingMinutes,
MeetingSessionPage,
MeetingStartResult,
MeetingModeStateInfo,
MeetingProcessingStep,
MeetingProcessingProgress,
CaptionSegment,
} from '@shared/types'
const logger = getLogger('MeetingModeService')
const MEETING_MODE_SYSTEM_PROMPT = `당신은 전문 회의록 작성 비서입니다.
.
:
- /
- (📝)
-
- ( )
:
##
(3-5 )
##
- ( 1)
- ( 2)
##
- [ ] ( ) ( )
##
| | |
|------|------|
| MM:SS | / |`
/** 긴 회의 청크 분할 기준 (자) */
const LONG_MEETING_THRESHOLD = 20000
const CHUNK_SIZE = 5000
interface MeetingModeServiceEvents {
'state-changed': (state: MeetingModeState) => void
'segment': (segment: CaptionSegment) => void
'memo-added': (memo: MeetingMemo) => void
'processing-progress': (progress: MeetingProcessingProgress) => void
'session-completed': (detail: MeetingSessionDetail) => void
'error': (error: D3ROError) => void
}
class MeetingModeService extends EventEmitter {
private _state: MeetingModeState = 'idle'
private _sessionId: string | null = null
private _sessionStartedAt: number | null = null
private _segments: CaptionSegment[] = []
private _memos: MeetingMemo[] = []
private _segmentHandler: ((segment: CaptionSegment) => void) | null = null
/** CaptionService session-saved 방지 플래그 */
private _meetingModeActive = false
// ── 공개 접근자 ──
getState(): MeetingModeState {
return this._state
}
getStateInfo(): MeetingModeStateInfo {
return {
state: this._state,
sessionId: this._sessionId,
elapsedMs: this._sessionStartedAt ? Date.now() - this._sessionStartedAt : 0,
memoCount: this._memos.length,
segmentCount: this._segments.length,
}
}
isMeetingModeActive(): boolean {
return this._meetingModeActive
}
// ── 녹음 시작 ──
async startRecording(): Promise<MeetingStartResult> {
if (this._state !== 'idle') {
throw new D3ROError(
ErrorCode.MeetingAlreadyRecording,
`회의 모드가 이미 활성 상태입니다: ${this._state}`,
)
}
// CaptionService 상태 확인
const { getCaptionService } = await import('./CaptionService')
const captionService = getCaptionService()
const captionState = captionService.getState()
if (captionState !== 'inactive') {
throw new D3ROError(
ErrorCode.MeetingAlreadyRecording,
'자막 모드가 활성 상태입니다. 먼저 자막을 종료해주세요.',
)
}
this._setState('recording')
this._meetingModeActive = true
const sessionId = nanoid()
const now = Date.now()
this._sessionId = sessionId
this._sessionStartedAt = now
this._segments = []
this._memos = []
// DB에 세션 생성
const db = getDatabase()
db.insert(meetingSessions).values({
id: sessionId,
status: 'recording',
startedAt: now,
createdAt: now,
updatedAt: now,
}).run()
// CaptionService 시작 (오버레이 숨김 상태로 — 회의 모드 UI에서 자체 표시)
this._segmentHandler = (segment: CaptionSegment) => {
this._segments.push(segment)
this.emit('segment', segment)
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.SEGMENT, segment)
}
captionService.on('segment', this._segmentHandler)
try {
await captionService.start()
} catch (err) {
// 시작 실패 시 복원
captionService.off('segment', this._segmentHandler)
this._segmentHandler = null
this._meetingModeActive = false
this._sessionId = null
this._sessionStartedAt = null
db.update(meetingSessions).set({
status: 'error',
errorMessage: err instanceof Error ? err.message : String(err),
updatedAt: Date.now(),
}).where(eq(meetingSessions.id, sessionId)).run()
this._setState('idle')
throw err
}
logger.info(`회의 녹음 시작: sessionId=${sessionId}`)
this._sendStateToRenderer()
return { sessionId }
}
// ── 메모 추가 ──
async addMemo(content: string): Promise<MeetingMemo> {
if (this._state !== 'recording' || !this._sessionId || !this._sessionStartedAt) {
throw new D3ROError(ErrorCode.MeetingNotRecording, '녹음 중이 아닙니다')
}
const memo: MeetingMemo = {
id: nanoid(),
sessionId: this._sessionId,
content,
timestampMs: Date.now() - this._sessionStartedAt,
createdAt: Date.now(),
}
this._memos.push(memo)
// DB 저장
const db = getDatabase()
db.insert(meetingMemos).values({
id: memo.id,
sessionId: memo.sessionId,
content: memo.content,
timestampMs: memo.timestampMs,
createdAt: memo.createdAt,
}).run()
this.emit('memo-added', memo)
logger.debug(`메모 추가: [${this._formatTime(memo.timestampMs)}] ${content}`)
return memo
}
// ── 녹음 종료 ──
async stopRecording(): Promise<void> {
if (this._state !== 'recording') {
throw new D3ROError(ErrorCode.MeetingNotRecording, '녹음 중이 아닙니다')
}
const sessionId = this._sessionId!
const now = Date.now()
// CaptionService 종료
const { getCaptionService } = await import('./CaptionService')
const captionService = getCaptionService()
if (this._segmentHandler) {
captionService.off('segment', this._segmentHandler)
this._segmentHandler = null
}
await captionService.stop()
// 세션 종료 시간 기록
const db = getDatabase()
db.update(meetingSessions).set({
endedAt: now,
durationMs: this._sessionStartedAt ? now - this._sessionStartedAt : 0,
updatedAt: now,
}).where(eq(meetingSessions.id, sessionId)).run()
logger.info(`회의 녹음 종료: sessionId=${sessionId}, segments=${this._segments.length}`)
// 후처리 시작 (비동기)
this._runPostProcessing().catch((err: unknown) => {
logger.error(`후처리 실패: ${err instanceof Error ? err.message : String(err)}`)
})
}
// ── 후처리 파이프라인 ──
private async _runPostProcessing(): Promise<void> {
this._setState('processing')
this._sendStateToRenderer()
const sessionId = this._sessionId!
const db = getDatabase()
try {
db.update(meetingSessions).set({
status: 'processing',
updatedAt: Date.now(),
}).where(eq(meetingSessions.id, sessionId)).run()
// Step 1: 전사 텍스트 합산
this._sendProgress(sessionId, 'merging', 20)
const rawTranscript = this._segments
.map((s) => `[${this._formatTime(s.timestamp)}] ${s.text}`)
.join('\n')
// Step 2: 메모 포맷팅
const formattedMemos = this._memos
.map((m) => `[${this._formatTime(m.timestampMs)}] 📝 ${m.content}`)
.join('\n')
// Step 3: LLM 구조화
this._sendProgress(sessionId, 'generating', 40)
const { getLocalLLMService } = await import('./LocalLLMService')
const llmService = getLocalLLMService()
const prompt = this._buildPrompt(rawTranscript, formattedMemos)
const sttEnd = Date.now()
let resultText: string
let resultModel: string
if (rawTranscript.length > LONG_MEETING_THRESHOLD) {
// 긴 회의: 2-pass 요약
const chunks = this._splitIntoChunks(rawTranscript, CHUNK_SIZE)
const chunkResults = []
for (let i = 0; i < chunks.length; i++) {
const pct = 40 + Math.floor((i / chunks.length) * 30)
this._sendProgress(sessionId, 'generating', pct)
const chunkResult = await llmService.generate(
`다음 회의 전사 일부를 3줄로 요약하세요:\n\n${chunks[i]}`,
{ temperature: 0.3 },
)
chunkResults.push(chunkResult.text)
}
const condensed = chunkResults.join('\n\n')
const finalResult = await llmService.generate(
this._buildPrompt(condensed, formattedMemos),
{
systemPrompt: MEETING_MODE_SYSTEM_PROMPT,
temperature: 0.3,
maxTokens: 4096,
},
)
resultText = finalResult.text
resultModel = finalResult.model
} else {
const result = await llmService.generate(prompt, {
systemPrompt: MEETING_MODE_SYSTEM_PROMPT,
temperature: 0.3,
maxTokens: 4096,
})
resultText = result.text
resultModel = result.model
}
const llmEnd = Date.now()
// Step 4: 마크다운 파싱
this._sendProgress(sessionId, 'parsing', 85)
const minutes = this._parseMinutes(resultText)
// Step 5: DB 저장
this._sendProgress(sessionId, 'saving', 90)
const now = Date.now()
db.update(meetingSessions).set({
status: 'completed',
rawTranscript,
minutesMarkdown: resultText,
minutesJson: JSON.stringify(minutes),
sttModel: configGet('sttModelId') as string | undefined,
llmModel: resultModel,
sttLatencyMs: sttEnd - (this._sessionStartedAt ?? sttEnd),
llmLatencyMs: llmEnd - sttEnd,
updatedAt: now,
}).where(eq(meetingSessions.id, sessionId)).run()
// 자동 제목 생성
const autoTitle = minutes.summary.split('\n')[0].slice(0, 50) || '무제 회의'
db.update(meetingSessions).set({
title: autoTitle,
updatedAt: now,
}).where(eq(meetingSessions.id, sessionId)).run()
// Step 6: Windows 알림
this._sendProgress(sessionId, 'notifying', 95)
new Notification({ title: '회의록 준비 완료', body: '회의록이 생성되었습니다.' }).show()
// 완료 이벤트
this._sendProgress(sessionId, 'notifying', 100)
const detail = this._loadSessionDetail(sessionId)
if (detail) {
this.emit('session-completed', detail)
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.SESSION_COMPLETED, detail)
}
this._resetSession()
this._setState('idle')
this._sendStateToRenderer()
logger.info(`회의록 생성 완료: sessionId=${sessionId}`)
} catch (err) {
db.update(meetingSessions).set({
status: 'error',
errorMessage: err instanceof Error ? err.message : String(err),
updatedAt: Date.now(),
}).where(eq(meetingSessions.id, sessionId)).run()
this._resetSession()
this._setState('idle')
this._sendStateToRenderer()
const d3roErr = new D3ROError(
ErrorCode.MeetingProcessingFailed,
`회의록 생성 실패: ${err instanceof Error ? err.message : String(err)}`,
)
this.emit('error', d3roErr)
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.ERROR, {
code: d3roErr.code,
message: d3roErr.message,
})
logger.error(`후처리 실패: ${d3roErr.message}`)
}
}
// ── 세션 조회 ──
getSessions(page: number, pageSize: number): MeetingSessionPage {
const db = getDatabase()
const offset = (page - 1) * pageSize
const countResult = db.select({ count: sql<number>`count(*)` })
.from(meetingSessions)
.get()
const total = countResult?.count ?? 0
const rows = db.select()
.from(meetingSessions)
.orderBy(desc(meetingSessions.createdAt))
.limit(pageSize)
.offset(offset)
.all()
const sessions: MeetingSessionSummary[] = rows.map((row) => {
const memoCount = db.select({ count: sql<number>`count(*)` })
.from(meetingMemos)
.where(eq(meetingMemos.sessionId, row.id))
.get()?.count ?? 0
return {
id: row.id,
title: row.title,
status: row.status as MeetingSessionSummary['status'],
startedAt: row.startedAt,
endedAt: row.endedAt,
durationMs: row.durationMs,
memoCount,
}
})
return {
sessions,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
}
}
getSession(sessionId: string): MeetingSessionDetail {
const detail = this._loadSessionDetail(sessionId)
if (!detail) {
throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션을 찾을 수 없습니다: ${sessionId}`)
}
return detail
}
deleteSession(sessionId: string): void {
const db = getDatabase()
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
if (!row) {
throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션을 찾을 수 없습니다: ${sessionId}`)
}
db.delete(meetingMemos).where(eq(meetingMemos.sessionId, sessionId)).run()
db.delete(meetingSessions).where(eq(meetingSessions.id, sessionId)).run()
logger.info(`회의 세션 삭제: ${sessionId}`)
}
updateTitle(sessionId: string, title: string): void {
const db = getDatabase()
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
if (!row) {
throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션을 찾을 수 없습니다: ${sessionId}`)
}
db.update(meetingSessions).set({ title, updatedAt: Date.now() })
.where(eq(meetingSessions.id, sessionId)).run()
}
// ── 내보내기 ──
async exportPdf(sessionId: string): Promise<string> {
const session = this._loadSessionDetail(sessionId)
if (!session) {
throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션을 찾을 수 없습니다: ${sessionId}`)
}
const html = this._buildPdfHtml(session)
const { filePath } = await dialog.showSaveDialog({
title: 'PDF로 내보내기',
defaultPath: `회의록_${this._formatDateFile(session.startedAt)}.pdf`,
filters: [{ name: 'PDF', extensions: ['pdf'] }],
})
if (!filePath) return ''
const win = new BrowserWindow({ show: false, width: 800, height: 600 })
try {
await win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`)
const pdfBuffer = await win.webContents.printToPDF({
printBackground: true,
pageSize: 'A4',
margins: { top: 20, bottom: 20, left: 20, right: 20 },
})
fs.writeFileSync(filePath, pdfBuffer)
logger.info(`PDF 내보내기 완료: ${filePath}`)
return filePath
} finally {
win.close()
}
}
async exportMarkdown(sessionId: string): Promise<string> {
const session = this._loadSessionDetail(sessionId)
if (!session) {
throw new D3ROError(ErrorCode.MeetingSessionNotFound, `세션을 찾을 수 없습니다: ${sessionId}`)
}
const { filePath } = await dialog.showSaveDialog({
title: '마크다운으로 내보내기',
defaultPath: `회의록_${this._formatDateFile(session.startedAt)}.md`,
filters: [{ name: 'Markdown', extensions: ['md'] }],
})
if (!filePath) return ''
const md = this._buildExportMarkdown(session)
fs.writeFileSync(filePath, md, 'utf-8')
logger.info(`마크다운 내보내기 완료: ${filePath}`)
return filePath
}
// ── 내부 헬퍼 ──
private _setState(state: MeetingModeState): void {
this._state = state
this.emit('state-changed', state)
}
private _resetSession(): void {
this._sessionId = null
this._sessionStartedAt = null
this._segments = []
this._memos = []
this._meetingModeActive = false
}
private _sendToRenderer(channel: string, data: unknown): void {
const win = getMainWindow()
if (win && !win.isDestroyed()) {
win.webContents.send(channel, data)
}
}
private _sendStateToRenderer(): void {
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.STATE_CHANGED, this.getStateInfo())
}
private _sendProgress(sessionId: string, step: MeetingProcessingStep, percent: number): void {
const progress: MeetingProcessingProgress = { sessionId, step, percent }
this.emit('processing-progress', progress)
this._sendToRenderer(IPC_CHANNELS.MEETING_MODE.PROCESSING_PROGRESS, progress)
}
private _formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000)
const minutes = Math.floor(totalSeconds / 60)
const seconds = totalSeconds % 60
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`
}
private _formatDateFile(epochMs: number): string {
const d = new Date(epochMs)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}_${String(d.getHours()).padStart(2, '0')}${String(d.getMinutes()).padStart(2, '0')}`
}
private _formatDateRange(startedAt: number, endedAt: number | null): string {
const fmt = (ts: number): string => {
const d = new Date(ts)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}
if (!endedAt) return fmt(startedAt)
return `${fmt(startedAt)} ~ ${new Date(endedAt).getHours()}:${String(new Date(endedAt).getMinutes()).padStart(2, '0')}`
}
private _buildPrompt(rawTranscript: string, formattedMemos: string): string {
return `## 전사록\n${rawTranscript}\n\n## 참석자 메모 (📝 표시)\n${formattedMemos}\n\n위 전사록과 메모를 바탕으로 회의록을 작성해주세요.`
}
private _parseMinutes(markdown: string): MeetingMinutes {
const minutes: MeetingMinutes = {
summary: '',
decisions: [],
actionItems: [],
timeline: [],
}
const sections = markdown.split(/^## /m)
for (const section of sections) {
const lines = section.trim().split('\n')
const heading = lines[0]?.toLowerCase() ?? ''
const body = lines.slice(1).join('\n').trim()
if (heading.includes('요약') || heading.includes('summary')) {
minutes.summary = body
} else if (heading.includes('결정') || heading.includes('decision')) {
minutes.decisions = body
.split('\n')
.filter((l) => l.startsWith('-') || l.startsWith('*'))
.map((l) => l.replace(/^[-*]\s*/, '').trim())
} else if (heading.includes('할 일') || heading.includes('action')) {
minutes.actionItems = body
.split('\n')
.filter((l) => l.startsWith('-') || l.startsWith('*'))
.map((l) => {
const text = l.replace(/^[-*]\s*(\[.\]\s*)?/, '').trim()
return { task: text }
})
} else if (heading.includes('타임라인') || heading.includes('timeline')) {
const tableRows = body
.split('\n')
.filter((l) => l.includes('|') && !l.includes('---'))
for (const row of tableRows) {
const cells = row.split('|').map((c) => c.trim()).filter(Boolean)
if (cells.length >= 2) {
minutes.timeline.push({
time: cells[0],
content: cells[1],
type: cells[1].includes('📝') ? 'memo' : 'transcript',
})
}
}
}
}
return minutes
}
private _loadSessionDetail(sessionId: string): MeetingSessionDetail | null {
const db = getDatabase()
const row = db.select().from(meetingSessions).where(eq(meetingSessions.id, sessionId)).get()
if (!row) return null
const memoRows = db.select().from(meetingMemos)
.where(eq(meetingMemos.sessionId, sessionId))
.orderBy(meetingMemos.timestampMs)
.all()
const memos: MeetingMemo[] = memoRows.map((m) => ({
id: m.id,
sessionId: m.sessionId,
content: m.content,
timestampMs: m.timestampMs,
createdAt: m.createdAt,
}))
let minutes: MeetingMinutes | null = null
if (row.minutesJson) {
try {
minutes = JSON.parse(row.minutesJson) as MeetingMinutes
} catch {
logger.warn(`회의록 JSON 파싱 실패: sessionId=${sessionId}`)
}
}
return {
id: row.id,
title: row.title,
status: row.status as MeetingSessionDetail['status'],
startedAt: row.startedAt,
endedAt: row.endedAt,
durationMs: row.durationMs,
memoCount: memos.length,
rawTranscript: row.rawTranscript,
minutesMarkdown: row.minutesMarkdown,
minutes,
memos,
sttModel: row.sttModel,
llmModel: row.llmModel,
errorMessage: row.errorMessage,
}
}
private _splitIntoChunks(text: string, chunkSize: number): string[] {
const chunks: string[] = []
for (let i = 0; i < text.length; i += chunkSize) {
chunks.push(text.slice(i, i + chunkSize))
}
return chunks
}
private _buildPdfHtml(session: MeetingSessionDetail): string {
const durationMin = session.durationMs ? Math.round(session.durationMs / 60000) : 0
const minutesHtml = session.minutesMarkdown
? this._markdownToSimpleHtml(session.minutesMarkdown)
: '<p>회의록이 생성되지 않았습니다.</p>'
return `<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<style>
body { font-family: 'Malgun Gothic', sans-serif; margin: 40px; color: #222; }
h1 { font-size: 22px; border-bottom: 2px solid #f25b29; padding-bottom: 8px; }
h2 { font-size: 16px; color: #444; margin-top: 24px; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; font-size: 13px; }
th { background: #f5f5f5; font-weight: 600; }
.memo-badge { background: #fff3cd; padding: 2px 6px; border-radius: 4px; font-size: 11px; }
.meta { color: #888; font-size: 12px; margin-bottom: 20px; }
ul { padding-left: 20px; }
li { margin: 4px 0; }
</style>
</head>
<body>
<h1>${session.title ?? '무제 회의'}</h1>
<p class="meta">일시: ${this._formatDateRange(session.startedAt, session.endedAt)} | 소요: ${durationMin}</p>
${minutesHtml}
</body>
</html>`
}
private _buildExportMarkdown(session: MeetingSessionDetail): string {
const durationMin = session.durationMs ? Math.round(session.durationMs / 60000) : 0
const memoTable = session.memos.length > 0
? `## 참석자 메모\n\n| 시간 | 메모 |\n|------|------|\n${session.memos.map((m) => `| ${this._formatTime(m.timestampMs)} | ${m.content} |`).join('\n')}`
: ''
return `# 회의록 — ${session.title ?? '무제 회의'}
- ****: ${this._formatDateRange(session.startedAt, session.endedAt)}
- ** **: ${durationMin}
- **STT **: ${session.sttModel ?? '-'}
- **LLM **: ${session.llmModel ?? '-'}
---
${session.minutesMarkdown ?? '회의록이 생성되지 않았습니다.'}
---
${memoTable}
---
##
${session.rawTranscript ?? '(없음)'}
`
}
/** 간단한 마크다운→HTML 변환 (PDF 생성용) */
private _markdownToSimpleHtml(md: string): string {
return md
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^\- \[.\] (.+)$/gm, '<li>$1</li>')
.replace(/^\- (.+)$/gm, '<li>$1</li>')
.replace(/^\* (.+)$/gm, '<li>$1</li>')
.replace(/\n{2,}/g, '</p><p>')
.replace(/^(?!<[h|l|t|p])/gm, '')
}
}
// ── 싱글톤 ──
let instance: MeetingModeService | null = null
export function getMeetingModeService(): MeetingModeService {
if (!instance) {
instance = new MeetingModeService()
}
return instance
}

View file

@ -821,6 +821,12 @@ class VoiceModeService extends EventEmitter {
logger.warn('Cannot start caption: dictation session active') logger.warn('Cannot start caption: dictation session active')
return return
} }
// 회의 모드 녹음 중이면 자막 시작 불가 (상호 배제)
const { getMeetingModeService } = await import('./MeetingModeService')
if (getMeetingModeService().getState() === 'recording') {
logger.warn('Cannot start caption: meeting mode recording active')
return
}
await caption.start() await caption.start()
logger.info('Caption started via hotkey') logger.info('Caption started via hotkey')
} }

View file

@ -136,6 +136,18 @@ import type {
TemplateSessionInfo, TemplateSessionInfo,
TemplateFieldCompletedEvent, TemplateFieldCompletedEvent,
TemplateSessionCompletedEvent, TemplateSessionCompletedEvent,
MeetingStartResult,
MeetingMemo,
MeetingAddMemoParams,
MeetingModeStateInfo,
MeetingSessionPage,
MeetingGetSessionsParams,
MeetingSessionDetail,
MeetingGetSessionParams,
MeetingDeleteSessionParams,
MeetingExportParams,
MeetingProcessingProgress,
CaptionSegment,
} from '@shared/types' } from '@shared/types'
import { Feature } from '@shared/types' import { Feature } from '@shared/types'
import type { IPCResult } from '@shared/errors' import type { IPCResult } from '@shared/errors'
@ -553,6 +565,40 @@ const electronAPI = {
onError: (cb: (data: ConversationError) => void): Unsubscribe => onError: (cb: (data: ConversationError) => void): Unsubscribe =>
on(IPC_CHANNELS.VOICE_CONVERSATION.ERROR, cb), on(IPC_CHANNELS.VOICE_CONVERSATION.ERROR, cb),
}, },
// ── Meeting Mode (Phase 14) ───────────────────────────
meetingMode: {
startRecording: () =>
invoke<MeetingStartResult>(IPC_CHANNELS.MEETING_MODE.START_RECORDING),
stopRecording: () =>
invoke<void>(IPC_CHANNELS.MEETING_MODE.STOP_RECORDING),
addMemo: (params: MeetingAddMemoParams) =>
invoke<MeetingMemo>(IPC_CHANNELS.MEETING_MODE.ADD_MEMO, params),
getState: () =>
invoke<MeetingModeStateInfo>(IPC_CHANNELS.MEETING_MODE.GET_STATE),
getSessions: (params: MeetingGetSessionsParams) =>
invoke<MeetingSessionPage>(IPC_CHANNELS.MEETING_MODE.GET_SESSIONS, params),
getSession: (params: MeetingGetSessionParams) =>
invoke<MeetingSessionDetail>(IPC_CHANNELS.MEETING_MODE.GET_SESSION, params),
deleteSession: (params: MeetingDeleteSessionParams) =>
invoke<void>(IPC_CHANNELS.MEETING_MODE.DELETE_SESSION, params),
updateTitle: (params: { sessionId: string; title: string }) =>
invoke<void>(IPC_CHANNELS.MEETING_MODE.UPDATE_TITLE, params),
exportPdf: (params: MeetingExportParams) =>
invoke<string>(IPC_CHANNELS.MEETING_MODE.EXPORT_PDF, params),
exportMarkdown: (params: MeetingExportParams) =>
invoke<string>(IPC_CHANNELS.MEETING_MODE.EXPORT_MARKDOWN, params),
onStateChanged: (cb: (e: MeetingModeStateInfo) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_MODE.STATE_CHANGED, cb),
onSegment: (cb: (e: CaptionSegment) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_MODE.SEGMENT, cb),
onProcessingProgress: (cb: (e: MeetingProcessingProgress) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_MODE.PROCESSING_PROGRESS, cb),
onSessionCompleted: (cb: (e: MeetingSessionDetail) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_MODE.SESSION_COMPLETED, cb),
onError: (cb: (e: { code: number; message: string }) => void): Unsubscribe =>
on(IPC_CHANNELS.MEETING_MODE.ERROR, cb),
},
} as const } as const
contextBridge.exposeInMainWorld('electronAPI', electronAPI) contextBridge.exposeInMainWorld('electronAPI', electronAPI)

View file

@ -9,6 +9,7 @@ import MenuBookIcon from '@mui/icons-material/MenuBook'
import ExtensionIcon from '@mui/icons-material/Extension' import ExtensionIcon from '@mui/icons-material/Extension'
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver' import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver'
import AutoStoriesIcon from '@mui/icons-material/AutoStories' import AutoStoriesIcon from '@mui/icons-material/AutoStories'
import GroupsIcon from '@mui/icons-material/Groups'
import SettingsIcon from '@mui/icons-material/Settings' import SettingsIcon from '@mui/icons-material/Settings'
import { Led } from './ds' import { Led } from './ds'
import { DashboardPage } from '../pages/DashboardPage' import { DashboardPage } from '../pages/DashboardPage'
@ -17,6 +18,7 @@ import { DictionaryPage } from '../pages/DictionaryPage'
import { CommandsPage } from '../pages/CommandsPage' import { CommandsPage } from '../pages/CommandsPage'
import { VoiceConversationPage } from '../pages/VoiceConversationPage' import { VoiceConversationPage } from '../pages/VoiceConversationPage'
import { KnowledgeBasePage } from '../pages/KnowledgeBasePage' import { KnowledgeBasePage } from '../pages/KnowledgeBasePage'
import { MeetingModePage } from '../pages/MeetingModePage'
import { SettingsModal } from './SettingsModal' import { SettingsModal } from './SettingsModal'
import { LicenseModal } from './LicenseModal' import { LicenseModal } from './LicenseModal'
import { OnboardingModal } from './OnboardingModal' import { OnboardingModal } from './OnboardingModal'
@ -26,7 +28,7 @@ import { useI18n } from '../i18n'
import type { TranslationKey } from '../i18n' import type { TranslationKey } from '../i18n'
import type { LicenseTier } from '@shared/types' import type { LicenseTier } from '@shared/types'
type Route = 'dashboard' | 'history' | 'dictionary' | 'commands' | 'conversation' | 'knowledge' type Route = 'dashboard' | 'history' | 'dictionary' | 'commands' | 'conversation' | 'knowledge' | 'meeting'
interface NavItem { interface NavItem {
route: Route route: Route
@ -42,6 +44,7 @@ const NAV_ITEMS: NavItem[] = [
{ route: 'commands', labelKey: 'nav.commands', abbr: 'CMD', icon: <ExtensionIcon sx={{ fontSize: 20 }} /> }, { route: 'commands', labelKey: 'nav.commands', abbr: 'CMD', icon: <ExtensionIcon sx={{ fontSize: 20 }} /> },
{ route: 'conversation', labelKey: 'nav.conversation', abbr: 'TALK', icon: <RecordVoiceOverIcon sx={{ fontSize: 20 }} /> }, { route: 'conversation', labelKey: 'nav.conversation', abbr: 'TALK', icon: <RecordVoiceOverIcon sx={{ fontSize: 20 }} /> },
{ route: 'knowledge', labelKey: 'nav.knowledge', abbr: 'RAG', icon: <AutoStoriesIcon sx={{ fontSize: 20 }} /> }, { route: 'knowledge', labelKey: 'nav.knowledge', abbr: 'RAG', icon: <AutoStoriesIcon sx={{ fontSize: 20 }} /> },
{ route: 'meeting', labelKey: 'nav.meeting', abbr: 'MTG', icon: <GroupsIcon sx={{ fontSize: 20 }} /> },
] ]
function tierToLedColor(tier: LicenseTier): 'amber' | 'green' { function tierToLedColor(tier: LicenseTier): 'amber' | 'green' {
@ -225,6 +228,7 @@ export function AppLayout(): React.ReactElement {
{currentRoute === 'commands' && <CommandsPage />} {currentRoute === 'commands' && <CommandsPage />}
{currentRoute === 'conversation' && <VoiceConversationPage />} {currentRoute === 'conversation' && <VoiceConversationPage />}
{currentRoute === 'knowledge' && <KnowledgeBasePage />} {currentRoute === 'knowledge' && <KnowledgeBasePage />}
{currentRoute === 'meeting' && <MeetingModePage />}
</Box> </Box>
</Box> </Box>
<StatusBar /> <StatusBar />

View file

@ -455,10 +455,10 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
<MenuItem value="auto">{t('settings.theme.system')}</MenuItem> <MenuItem value="auto">{t('settings.theme.system')}</MenuItem>
<MenuItem value="light">{t('settings.theme.light')}</MenuItem> <MenuItem value="light">{t('settings.theme.light')}</MenuItem>
<MenuItem value="dark">{t('settings.theme.dark')}</MenuItem> <MenuItem value="dark">{t('settings.theme.dark')}</MenuItem>
<MenuItem value="nord">Nord</MenuItem> <MenuItem value="nord">{t('settings.theme.nord')}</MenuItem>
<MenuItem value="solarized">Solarized</MenuItem> <MenuItem value="solarized">{t('settings.theme.solarized')}</MenuItem>
<MenuItem value="catppuccin">Catppuccin</MenuItem> <MenuItem value="catppuccin">{t('settings.theme.catppuccin')}</MenuItem>
<MenuItem value="dracula">Dracula</MenuItem> <MenuItem value="dracula">{t('settings.theme.dracula')}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
@ -627,10 +627,10 @@ export function SettingsModal({ open, onClose }: SettingsModalProps): React.Reac
onChange={(e) => updateConfig('sttLanguage', e.target.value)} onChange={(e) => updateConfig('sttLanguage', e.target.value)}
> >
<MenuItem value="auto">{t('settings.sttLang.auto')}</MenuItem> <MenuItem value="auto">{t('settings.sttLang.auto')}</MenuItem>
<MenuItem value="ko"></MenuItem> <MenuItem value="ko">{t('settings.sttLang.ko')}</MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">{t('settings.sttLang.en')}</MenuItem>
<MenuItem value="ja"></MenuItem> <MenuItem value="ja">{t('settings.sttLang.ja')}</MenuItem>
<MenuItem value="zh"></MenuItem> <MenuItem value="zh">{t('settings.sttLang.zh')}</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Box> </Box>

View file

@ -277,7 +277,7 @@ export function TemplateSection(): React.ReactElement {
fullWidth fullWidth
multiline multiline
rows={3} rows={3}
helperText="Use {{fieldName}} for placeholders" helperText={t('template.outputHelperText')}
/> />
</DialogContent> </DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}> <DialogActions sx={{ px: 3, pb: 2 }}>

View file

@ -56,10 +56,10 @@ export function MetalDial({
background: ` background: `
repeating-radial-gradient( repeating-radial-gradient(
circle at 50% 50%, circle at 50% 50%,
#e6e7e9 0px, ${d3roPalette.text.disabled} 0px,
#e6e7e9 1px, ${d3roPalette.text.disabled} 1px,
#b0b2b5 1.5px, ${d3roPalette.text.label} 1.5px,
#b0b2b5 2.5px ${d3roPalette.text.label} 2.5px
) )
`, `,
transform: `rotate(${rotation}deg)`, transform: `rotate(${rotation}deg)`,

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "Lokaler KI-Sprachassistent", "app.tagline": "Lokaler KI-Sprachassistent",
"nav.dashboard": "Dashboard", "nav.dashboard": "Dashboard",
"nav.history": "Verlauf", "nav.history": "Verlauf",
"nav.dictionary": "Wörterbuch", "nav.dictionary": "Wörterbuch",
"nav.commands": "Befehle", "nav.commands": "Befehle",
"nav.settings": "Einstellungen", "nav.settings": "Einstellungen",
"dashboard.sessionOverview": "Sitzungsübersicht", "dashboard.sessionOverview": "Sitzungsübersicht",
"dashboard.systemStatus": "Systemstatus", "dashboard.systemStatus": "Systemstatus",
"dashboard.sessionsToday": "Sitzungen heute", "dashboard.sessionsToday": "Sitzungen heute",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} Einträge", "dashboard.entries": "{{count}} Einträge",
"dashboard.stat": "Statistiken", "dashboard.stat": "Statistiken",
"dashboard.sys": "System", "dashboard.sys": "System",
"history.title": "Transkriptionsverlauf", "history.title": "Transkriptionsverlauf",
"history.search": "Suchen...", "history.search": "Suchen...",
"history.entries": "{{count}} Einträge", "history.entries": "{{count}} Einträge",
@ -34,7 +31,6 @@
"history.noResults": "Keine Ergebnisse", "history.noResults": "Keine Ergebnisse",
"history.noHistory": "Kein Verlauf — starte eine Aufnahme", "history.noHistory": "Kein Verlauf — starte eine Aufnahme",
"history.count": "{{count}} Einträge", "history.count": "{{count}} Einträge",
"dictionary.title": "Benutzerwörterbuch", "dictionary.title": "Benutzerwörterbuch",
"dictionary.words": "{{count}} Wörter", "dictionary.words": "{{count}} Wörter",
"dictionary.add": "Hinzufügen", "dictionary.add": "Hinzufügen",
@ -47,7 +43,6 @@
"dictionary.addTitle": "Wort hinzufügen", "dictionary.addTitle": "Wort hinzufügen",
"dictionary.word": "Wort", "dictionary.word": "Wort",
"dictionary.pronunciation": "Aussprache (optional)", "dictionary.pronunciation": "Aussprache (optional)",
"commands.title": "LLM-Befehle", "commands.title": "LLM-Befehle",
"commands.count": "{{count}} Befehle", "commands.count": "{{count}} Befehle",
"commands.add": "Hinzufügen", "commands.add": "Hinzufügen",
@ -62,14 +57,12 @@
"commands.promptTemplate": "Prompt-Vorlage", "commands.promptTemplate": "Prompt-Vorlage",
"commands.promptHelp": "{{text}} wird durch den transkribierten Text ersetzt", "commands.promptHelp": "{{text}} wird durch den transkribierten Text ersetzt",
"commands.defaultPrompt": "Bitte verbessere {{text}}.", "commands.defaultPrompt": "Bitte verbessere {{text}}.",
"settings.title": "Einstellungen", "settings.title": "Einstellungen",
"settings.tabs.general": "Allgemein", "settings.tabs.general": "Allgemein",
"settings.tabs.audio": "Audio", "settings.tabs.audio": "Audio",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "Über", "settings.tabs.about": "Über",
"settings.shortcuts": "Tastenkürzel", "settings.shortcuts": "Tastenkürzel",
"settings.dictation": "Diktat", "settings.dictation": "Diktat",
"settings.dictation.desc": "Gedrückt halten zum Sprechen. Die Transkription beginnt beim Loslassen.", "settings.dictation.desc": "Gedrückt halten zum Sprechen. Die Transkription beginnt beim Loslassen.",
@ -82,20 +75,17 @@
"settings.notSet": "Nicht festgelegt", "settings.notSet": "Nicht festgelegt",
"settings.enabled": "Aktiviert", "settings.enabled": "Aktiviert",
"settings.disabled": "Deaktiviert", "settings.disabled": "Deaktiviert",
"settings.interface": "Oberfläche", "settings.interface": "Oberfläche",
"settings.theme": "Design", "settings.theme": "Design",
"settings.theme.system": "System", "settings.theme.system": "System",
"settings.theme.light": "Hell", "settings.theme.light": "Hell",
"settings.theme.dark": "Dunkel", "settings.theme.dark": "Dunkel",
"settings.language": "Sprache", "settings.language": "Sprache",
"settings.appBehavior": "App-Verhalten", "settings.appBehavior": "App-Verhalten",
"settings.closeToTray": "In Systembereich minimieren", "settings.closeToTray": "In Systembereich minimieren",
"settings.autoLaunch": "Beim Systemstart automatisch starten", "settings.autoLaunch": "Beim Systemstart automatisch starten",
"settings.autoInsert": "Text nach der Transkription automatisch einfügen", "settings.autoInsert": "Text nach der Transkription automatisch einfügen",
"settings.soundEffects": "Soundeffekte", "settings.soundEffects": "Soundeffekte",
"settings.microphone": "Mikrofon", "settings.microphone": "Mikrofon",
"settings.inputDevice": "Eingabegerät", "settings.inputDevice": "Eingabegerät",
"settings.deviceDefault": "(Standard)", "settings.deviceDefault": "(Standard)",
@ -103,7 +93,6 @@
"settings.insertMethod": "Einfügemethode", "settings.insertMethod": "Einfügemethode",
"settings.insertClipboard": "Zwischenablage (Ctrl+V)", "settings.insertClipboard": "Zwischenablage (Ctrl+V)",
"settings.insertKeyboard": "Tastatureingabe", "settings.insertKeyboard": "Tastatureingabe",
"settings.whisperModel": "Whisper-Modell", "settings.whisperModel": "Whisper-Modell",
"settings.model.tiny": "tiny (39 MB, am schnellsten)", "settings.model.tiny": "tiny (39 MB, am schnellsten)",
"settings.model.base": "base (74 MB, ausgewogen)", "settings.model.base": "base (74 MB, ausgewogen)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1,5 GB, beste Qualität)", "settings.model.large": "large-v3 (1,5 GB, beste Qualität)",
"settings.sttLanguage": "Erkennungssprache", "settings.sttLanguage": "Erkennungssprache",
"settings.sttLang.auto": "Automatische Erkennung", "settings.sttLang.auto": "Automatische Erkennung",
"settings.ollamaServer": "Ollama-Server", "settings.ollamaServer": "Ollama-Server",
"settings.ollamaUrl": "Ollama-Server-URL", "settings.ollamaUrl": "Ollama-Server-URL",
"settings.ollamaHint": "D3RO Voice verbindet sich automatisch, wenn Ollama läuft. Lade Modelle direkt über Ollama herunter (z. B.: ollama pull qwen3:4b).", "settings.ollamaHint": "D3RO Voice verbindet sich automatisch, wenn Ollama läuft. Lade Modelle direkt über Ollama herunter (z. B.: ollama pull qwen3:4b).",
@ -125,7 +113,6 @@
"settings.action.grammar": "Grammatikkorrektur", "settings.action.grammar": "Grammatikkorrektur",
"settings.action.custom": "Benutzerdefinierter Prompt", "settings.action.custom": "Benutzerdefinierter Prompt",
"settings.actionHint": "Die gewählte LLM-Nachbearbeitung wird auf den transkribierten Text angewendet, nachdem mit dem Tastenkürzel aufgenommen wurde. Funktioniert nur, wenn Ollama verbunden ist.", "settings.actionHint": "Die gewählte LLM-Nachbearbeitung wird auf den transkribierten Text angewendet, nachdem mit dem Tastenkürzel aufgenommen wurde. Funktioniert nur, wenn Ollama verbunden ist.",
"settings.about.version": "Version", "settings.about.version": "Version",
"settings.about.techStack": "Tech-Stack", "settings.about.techStack": "Tech-Stack",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (lokal) / LLM: Ollama (lokal)", "settings.about.voiceEngineValue": "STT: faster-whisper (lokal) / LLM: Ollama (lokal)",
"settings.about.description": "Vollständig lokaler KI-Sprachassistent, basierend auf Reverse Engineering von Speakly. Funktioniert ohne Cloud-Abhängigkeiten.", "settings.about.description": "Vollständig lokaler KI-Sprachassistent, basierend auf Reverse Engineering von Speakly. Funktioniert ohne Cloud-Abhängigkeiten.",
"settings.about.restartOnboarding": "Einrichtungsanleitung erneut anzeigen", "settings.about.restartOnboarding": "Einrichtungsanleitung erneut anzeigen",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama läuft nicht", "status.nudge.title": "Ollama läuft nicht",
"status.nudge.desc": "Ollama wird für die LLM-Nachbearbeitung (Übersetzung, Zusammenfassung usw.) benötigt.", "status.nudge.desc": "Ollama wird für die LLM-Nachbearbeitung (Übersetzung, Zusammenfassung usw.) benötigt.",
"status.nudge.guide": "Installationsanleitung anzeigen →", "status.nudge.guide": "Installationsanleitung anzeigen →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "Ohne Tippen, per Sprache. Dein lokaler KI-Sprachassistent.", "onboarding.welcome.desc": "Ohne Tippen, per Sprache. Dein lokaler KI-Sprachassistent.",
"onboarding.welcome.start": "Loslegen", "onboarding.welcome.start": "Loslegen",
@ -160,7 +145,6 @@
"onboarding.done.start": "Loslegen", "onboarding.done.start": "Loslegen",
"onboarding.back": "Zurück", "onboarding.back": "Zurück",
"onboarding.next": "Weiter", "onboarding.next": "Weiter",
"hotkey.title": "Tastenkürzel einrichten", "hotkey.title": "Tastenkürzel einrichten",
"hotkey.dictationTitle": "Diktat-Tastenkürzel einrichten", "hotkey.dictationTitle": "Diktat-Tastenkürzel einrichten",
"hotkey.oneTouchTitle": "Tastenkürzel für Ein-Tasten-Modus einrichten", "hotkey.oneTouchTitle": "Tastenkürzel für Ein-Tasten-Modus einrichten",
@ -171,7 +155,6 @@
"hotkey.current": "Aktuell: {{keys}}", "hotkey.current": "Aktuell: {{keys}}",
"hotkey.hint": "Gib eine Kombination (z. B. Ctrl+Shift+Q) oder eine einzelne Taste (z. B. F5) ein", "hotkey.hint": "Gib eine Kombination (z. B. Ctrl+Shift+Q) oder eine einzelne Taste (z. B. F5) ein",
"hotkey.reset": "Erneut eingeben", "hotkey.reset": "Erneut eingeben",
"ollama.title": "OLLAMA EINRICHTUNGSANLEITUNG", "ollama.title": "OLLAMA EINRICHTUNGSANLEITUNG",
"ollama.step1.title": "SCHRITT 1 — Ollama installieren", "ollama.step1.title": "SCHRITT 1 — Ollama installieren",
"ollama.step1.desc": "Ollama ist ein kostenloses Tool zum lokalen Ausführen von LLMs.", "ollama.step1.desc": "Ollama ist ein kostenloses Tool zum lokalen Ausführen von LLMs.",
@ -180,7 +163,6 @@
"ollama.step2.alt": "Oder ein größeres Modell: ollama pull qwen3:8b (genauer, langsamer)", "ollama.step2.alt": "Oder ein größeres Modell: ollama pull qwen3:8b (genauer, langsamer)",
"ollama.step3.title": "SCHRITT 3 — Automatische Verbindung", "ollama.step3.title": "SCHRITT 3 — Automatische Verbindung",
"ollama.step3.desc": "D3RO-VOICE erkennt automatisch, wenn Ollama läuft. Wenn die LED in der Statusleiste von Rot auf Grün wechselt, ist alles bereit!", "ollama.step3.desc": "D3RO-VOICE erkennt automatisch, wenn Ollama läuft. Wenn die LED in der Statusleiste von Rot auf Grün wechselt, ist alles bereit!",
"service.sttEngine": "STT-ENGINE", "service.sttEngine": "STT-ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "TASTENKÜRZEL", "service.hotkeyHook": "TASTENKÜRZEL",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "AKTIV", "service.active": "AKTIV",
"service.standby": "BEREITSCHAFT", "service.standby": "BEREITSCHAFT",
"common.cancel": "Abbrechen", "common.cancel": "Abbrechen",
"common.save": "Speichern", "common.save": "Speichern",
"common.delete": "Löschen", "common.delete": "Löschen",
@ -202,7 +183,51 @@
"common.copy": "Kopieren", "common.copy": "Kopieren",
"common.test": "Testen", "common.test": "Testen",
"common.stop": "Stoppen", "common.stop": "Stoppen",
"date.today": "Heute", "date.today": "Heute",
"date.yesterday": "Gestern" "date.yesterday": "Gestern",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "Koreanisch",
"settings.sttLang.en": "Englisch",
"settings.sttLang.ja": "Japanisch",
"settings.sttLang.zh": "Chinesisch",
"template.outputHelperText": "Verwenden Sie {{Feldname}} für Platzhalter",
"nav.meeting": "Meeting",
"meeting.title": "Meeting-Modus",
"meeting.newMeeting": "Neues Meeting",
"meeting.startRecording": "Aufnahme starten",
"meeting.stopRecording": "Aufnahme beenden",
"meeting.recording": "Aufnahme läuft",
"meeting.processing": "Protokoll wird erstellt...",
"meeting.completed": "Abgeschlossen",
"meeting.error": "Fehler",
"meeting.elapsed": "Vergangen",
"meeting.memoPlaceholder": "Memo eingeben (Enter zum Hinzufügen)",
"meeting.memoAdded": "Memo hinzugefügt",
"meeting.noSessions": "Keine Meeting-Aufnahmen",
"meeting.noSessionsDesc": "Drücke «Neues Meeting», um die Aufnahme zu starten",
"meeting.summary": "Zusammenfassung",
"meeting.decisions": "Beschlüsse",
"meeting.actionItems": "Aufgaben",
"meeting.timeline": "Zeitlinie",
"meeting.transcript": "Rohtranskript",
"meeting.memos": "Memos",
"meeting.exportPdf": "Als PDF exportieren",
"meeting.exportMarkdown": "Als Markdown exportieren",
"meeting.delete": "Löschen",
"meeting.deleteConfirm": "Diesen Meeting-Eintrag löschen?",
"meeting.backToList": "Zurück zur Liste",
"meeting.notificationTitle": "Protokoll fertig",
"meeting.notificationBody": "Das Meeting-Protokoll wurde erstellt.",
"meeting.sessions": "{{count}} Sitzungen",
"meeting.duration": "{{minutes}} Min.",
"meeting.editTitle": "Titel bearbeiten",
"meeting.untitled": "Meeting ohne Titel",
"meeting.processingStep.merging": "Transkript zusammenführen...",
"meeting.processingStep.generating": "Protokoll mit LLM erstellen...",
"meeting.processingStep.parsing": "Protokoll analysieren...",
"meeting.processingStep.saving": "Speichern...",
"meeting.processingStep.notifying": "Benachrichtigung senden..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "Local AI Voice Assistant", "app.tagline": "Local AI Voice Assistant",
"nav.dashboard": "Dashboard", "nav.dashboard": "Dashboard",
"nav.history": "History", "nav.history": "History",
"nav.dictionary": "Dictionary", "nav.dictionary": "Dictionary",
"nav.commands": "Commands", "nav.commands": "Commands",
"nav.settings": "Settings", "nav.settings": "Settings",
"dashboard.sessionOverview": "Session Overview", "dashboard.sessionOverview": "Session Overview",
"dashboard.systemStatus": "System Status", "dashboard.systemStatus": "System Status",
"dashboard.sessionsToday": "Today's Sessions", "dashboard.sessionsToday": "Today's Sessions",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} entries", "dashboard.entries": "{{count}} entries",
"dashboard.stat": "Stats", "dashboard.stat": "Stats",
"dashboard.sys": "System", "dashboard.sys": "System",
"history.title": "Transcription History", "history.title": "Transcription History",
"history.search": "Search...", "history.search": "Search...",
"history.entries": "{{count}} entries", "history.entries": "{{count}} entries",
@ -34,7 +31,6 @@
"history.noResults": "No results found", "history.noResults": "No results found",
"history.noHistory": "No history — start recording", "history.noHistory": "No history — start recording",
"history.count": "{{count}} entries", "history.count": "{{count}} entries",
"dictionary.title": "Custom Dictionary", "dictionary.title": "Custom Dictionary",
"dictionary.words": "{{count}} words", "dictionary.words": "{{count}} words",
"dictionary.add": "Add", "dictionary.add": "Add",
@ -47,7 +43,6 @@
"dictionary.addTitle": "Add Word", "dictionary.addTitle": "Add Word",
"dictionary.word": "Word", "dictionary.word": "Word",
"dictionary.pronunciation": "Pronunciation (optional)", "dictionary.pronunciation": "Pronunciation (optional)",
"commands.title": "LLM Commands", "commands.title": "LLM Commands",
"commands.count": "{{count}} commands", "commands.count": "{{count}} commands",
"commands.add": "Add", "commands.add": "Add",
@ -62,14 +57,12 @@
"commands.promptTemplate": "Prompt Template", "commands.promptTemplate": "Prompt Template",
"commands.promptHelp": "{{text}} will be replaced with the transcribed text", "commands.promptHelp": "{{text}} will be replaced with the transcribed text",
"commands.defaultPrompt": "Please refine the following: {{text}}", "commands.defaultPrompt": "Please refine the following: {{text}}",
"settings.title": "Settings", "settings.title": "Settings",
"settings.tabs.general": "General", "settings.tabs.general": "General",
"settings.tabs.audio": "Audio", "settings.tabs.audio": "Audio",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "About", "settings.tabs.about": "About",
"settings.shortcuts": "Shortcuts", "settings.shortcuts": "Shortcuts",
"settings.dictation": "Dictation", "settings.dictation": "Dictation",
"settings.dictation.desc": "Hold to speak. Transcription begins when you release the key.", "settings.dictation.desc": "Hold to speak. Transcription begins when you release the key.",
@ -89,20 +82,17 @@
"settings.notSet": "Not set", "settings.notSet": "Not set",
"settings.enabled": "Enabled", "settings.enabled": "Enabled",
"settings.disabled": "Disabled", "settings.disabled": "Disabled",
"settings.interface": "Interface", "settings.interface": "Interface",
"settings.theme": "Theme", "settings.theme": "Theme",
"settings.theme.system": "System", "settings.theme.system": "System",
"settings.theme.light": "Light", "settings.theme.light": "Light",
"settings.theme.dark": "Dark", "settings.theme.dark": "Dark",
"settings.language": "Language", "settings.language": "Language",
"settings.appBehavior": "App Behavior", "settings.appBehavior": "App Behavior",
"settings.closeToTray": "Minimize to tray on close", "settings.closeToTray": "Minimize to tray on close",
"settings.autoLaunch": "Launch at system startup", "settings.autoLaunch": "Launch at system startup",
"settings.autoInsert": "Auto-insert text after transcription", "settings.autoInsert": "Auto-insert text after transcription",
"settings.soundEffects": "Sound Effects", "settings.soundEffects": "Sound Effects",
"settings.microphone": "Microphone", "settings.microphone": "Microphone",
"settings.inputDevice": "Input Device", "settings.inputDevice": "Input Device",
"settings.deviceDefault": "(Default)", "settings.deviceDefault": "(Default)",
@ -110,7 +100,6 @@
"settings.insertMethod": "Insert Method", "settings.insertMethod": "Insert Method",
"settings.insertClipboard": "Clipboard (Ctrl+V)", "settings.insertClipboard": "Clipboard (Ctrl+V)",
"settings.insertKeyboard": "Keyboard Typing", "settings.insertKeyboard": "Keyboard Typing",
"settings.whisperModel": "Whisper Model", "settings.whisperModel": "Whisper Model",
"settings.model.tiny": "tiny (39 MB, fastest)", "settings.model.tiny": "tiny (39 MB, fastest)",
"settings.model.base": "base (74 MB, balanced)", "settings.model.base": "base (74 MB, balanced)",
@ -119,7 +108,6 @@
"settings.model.large": "large-v3 (1.5 GB, best)", "settings.model.large": "large-v3 (1.5 GB, best)",
"settings.sttLanguage": "Recognition Language", "settings.sttLanguage": "Recognition Language",
"settings.sttLang.auto": "Auto-detect", "settings.sttLang.auto": "Auto-detect",
"settings.ollamaServer": "Ollama Server", "settings.ollamaServer": "Ollama Server",
"settings.ollamaUrl": "Ollama Server URL", "settings.ollamaUrl": "Ollama Server URL",
"settings.ollamaHint": "Connects automatically when Ollama is running. Pull models directly in Ollama (e.g. ollama pull qwen3:4b).", "settings.ollamaHint": "Connects automatically when Ollama is running. Pull models directly in Ollama (e.g. ollama pull qwen3:4b).",
@ -132,7 +120,6 @@
"settings.action.grammar": "Grammar Correction", "settings.action.grammar": "Grammar Correction",
"settings.action.custom": "Custom Prompt", "settings.action.custom": "Custom Prompt",
"settings.actionHint": "The selected LLM post-processing will be applied to your transcription after recording. Only works when Ollama is connected.", "settings.actionHint": "The selected LLM post-processing will be applied to your transcription after recording. Only works when Ollama is connected.",
"settings.about.version": "Version", "settings.about.version": "Version",
"settings.about.techStack": "Tech Stack", "settings.about.techStack": "Tech Stack",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -140,13 +127,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (local) / LLM: Ollama (local)", "settings.about.voiceEngineValue": "STT: faster-whisper (local) / LLM: Ollama (local)",
"settings.about.description": "A fully local AI voice assistant built on Speakly reverse-engineering insights. Runs entirely offline with no cloud dependencies.", "settings.about.description": "A fully local AI voice assistant built on Speakly reverse-engineering insights. Runs entirely offline with no cloud dependencies.",
"settings.about.restartOnboarding": "Restart Setup Wizard", "settings.about.restartOnboarding": "Restart Setup Wizard",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama is not running", "status.nudge.title": "Ollama is not running",
"status.nudge.desc": "Ollama is required for LLM post-processing (translation, summarization, etc.).", "status.nudge.desc": "Ollama is required for LLM post-processing (translation, summarization, etc.).",
"status.nudge.guide": "View setup guide →", "status.nudge.guide": "View setup guide →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "No typing needed — just speak. Your local AI voice assistant.", "onboarding.welcome.desc": "No typing needed — just speak. Your local AI voice assistant.",
"onboarding.welcome.start": "Get Started", "onboarding.welcome.start": "Get Started",
@ -167,7 +152,6 @@
"onboarding.done.start": "Start Using", "onboarding.done.start": "Start Using",
"onboarding.back": "Back", "onboarding.back": "Back",
"onboarding.next": "Next", "onboarding.next": "Next",
"hotkey.title": "Set Shortcut", "hotkey.title": "Set Shortcut",
"hotkey.dictationTitle": "Set Dictation Shortcut", "hotkey.dictationTitle": "Set Dictation Shortcut",
"hotkey.oneTouchTitle": "Set One-Touch Mode Shortcut", "hotkey.oneTouchTitle": "Set One-Touch Mode Shortcut",
@ -179,7 +163,6 @@
"hotkey.current": "Current: {{keys}}", "hotkey.current": "Current: {{keys}}",
"hotkey.hint": "Enter a combination (e.g. Ctrl+Shift+Q) or a single key (e.g. F5)", "hotkey.hint": "Enter a combination (e.g. Ctrl+Shift+Q) or a single key (e.g. F5)",
"hotkey.reset": "Clear", "hotkey.reset": "Clear",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "STEP 1 — Install Ollama", "ollama.step1.title": "STEP 1 — Install Ollama",
"ollama.step1.desc": "Ollama is a free tool for running LLMs locally.", "ollama.step1.desc": "Ollama is a free tool for running LLMs locally.",
@ -188,7 +171,6 @@
"ollama.step2.alt": "Or a larger model: ollama pull qwen3:8b (more accurate, slower)", "ollama.step2.alt": "Or a larger model: ollama pull qwen3:8b (more accurate, slower)",
"ollama.step3.title": "STEP 3 — Auto-Connect", "ollama.step3.title": "STEP 3 — Auto-Connect",
"ollama.step3.desc": "Once Ollama is running, D3RO-VOICE will detect it automatically. When the status bar LED turns from red to green, you're ready!", "ollama.step3.desc": "Once Ollama is running, D3RO-VOICE will detect it automatically. When the status bar LED turns from red to green, you're ready!",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -198,7 +180,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "Cancel", "common.cancel": "Cancel",
"common.save": "Save", "common.save": "Save",
"common.delete": "Delete", "common.delete": "Delete",
@ -210,7 +191,6 @@
"common.copy": "Copy", "common.copy": "Copy",
"common.test": "Test", "common.test": "Test",
"common.stop": "Stop", "common.stop": "Stop",
"memo.tags": "Tags", "memo.tags": "Tags",
"memo.addTag": "Add Tag", "memo.addTag": "Add Tag",
"memo.removeTag": "Remove Tag", "memo.removeTag": "Remove Tag",
@ -221,7 +201,6 @@
"memo.filterByTag": "Filter by tag", "memo.filterByTag": "Filter by tag",
"memo.allTags": "All Tags", "memo.allTags": "All Tags",
"memo.clearFilter": "Clear filter", "memo.clearFilter": "Clear filter",
"voiceCommand.title": "Voice Commands", "voiceCommand.title": "Voice Commands",
"voiceCommand.enabled": "Voice Keyword Recognition", "voiceCommand.enabled": "Voice Keyword Recognition",
"voiceCommand.enabledDesc": "Detects keywords at the start of transcription to auto-select commands.", "voiceCommand.enabledDesc": "Detects keywords at the start of transcription to auto-select commands.",
@ -232,7 +211,6 @@
"voiceCommand.matchMode.prefix": "Starts with", "voiceCommand.matchMode.prefix": "Starts with",
"voiceCommand.matchMode.suffix": "Ends with", "voiceCommand.matchMode.suffix": "Ends with",
"voiceCommand.matchMode.contains": "Contains", "voiceCommand.matchMode.contains": "Contains",
"chain.title": "LLM Chains", "chain.title": "LLM Chains",
"chain.count": "{{count}} chains", "chain.count": "{{count}} chains",
"chain.add": "Add Chain", "chain.add": "Add Chain",
@ -251,24 +229,19 @@
"chain.noChains": "No chains — create a pipeline to run multiple commands in sequence", "chain.noChains": "No chains — create a pipeline to run multiple commands in sequence",
"chain.step": "Step {{n}}", "chain.step": "Step {{n}}",
"chain.progress": "Processing step {{current}}/{{total}}", "chain.progress": "Processing step {{current}}/{{total}}",
"context.title": "Screen Context", "context.title": "Screen Context",
"context.enabled": "Screen Context", "context.enabled": "Screen Context",
"context.enabledDesc": "Captures active app and selected text when recording starts, and sends it to the LLM.", "context.enabledDesc": "Captures active app and selected text when recording starts, and sends it to the LLM.",
"dashboard.caption": "Live Caption", "dashboard.caption": "Live Caption",
"dashboard.captionStart": "Start Caption", "dashboard.captionStart": "Start Caption",
"dashboard.captionStop": "Stop Caption", "dashboard.captionStop": "Stop Caption",
"dashboard.captionActive": "Caption Active", "dashboard.captionActive": "Caption Active",
"settings.voiceCommands": "Voice Commands", "settings.voiceCommands": "Voice Commands",
"settings.voiceCommands.desc": "Detects keywords in transcription to auto-select commands.", "settings.voiceCommands.desc": "Detects keywords in transcription to auto-select commands.",
"settings.screenContext": "Screen Context", "settings.screenContext": "Screen Context",
"settings.screenContext.desc": "Sends active app info and selected text to the LLM during recording.", "settings.screenContext.desc": "Sends active app info and selected text to the LLM during recording.",
"date.today": "Today", "date.today": "Today",
"date.yesterday": "Yesterday", "date.yesterday": "Yesterday",
"license.title": "License", "license.title": "License",
"license.currentTier": "Current Tier", "license.currentTier": "Current Tier",
"license.free": "FREE", "license.free": "FREE",
@ -343,7 +316,6 @@
"license.unlimited": "Unlimited", "license.unlimited": "Unlimited",
"license.locked": "Locked", "license.locked": "Locked",
"license.nav": "License", "license.nav": "License",
"fileTranscription.title": "File Transcription", "fileTranscription.title": "File Transcription",
"fileTranscription.dropZone": "Drop audio/video file here", "fileTranscription.dropZone": "Drop audio/video file here",
"fileTranscription.dropZoneHint": "Supports MP3, WAV, M4A, MP4, MKV, WEBM", "fileTranscription.dropZoneHint": "Supports MP3, WAV, M4A, MP4, MKV, WEBM",
@ -356,7 +328,6 @@
"fileTranscription.retry": "Retry", "fileTranscription.retry": "Retry",
"fileTranscription.error.invalidFormat": "Unsupported file format", "fileTranscription.error.invalidFormat": "Unsupported file format",
"fileTranscription.error.unknown": "Unknown error", "fileTranscription.error.unknown": "Unknown error",
"meetingSummary.title": "Meeting Summary", "meetingSummary.title": "Meeting Summary",
"meetingSummary.generating": "Generating summary...", "meetingSummary.generating": "Generating summary...",
"meetingSummary.summary": "Summary", "meetingSummary.summary": "Summary",
@ -365,7 +336,6 @@
"meetingSummary.exportMarkdown": "Export as Markdown", "meetingSummary.exportMarkdown": "Export as Markdown",
"meetingSummary.noSummary": "No summary available", "meetingSummary.noSummary": "No summary available",
"meetingSummary.generate": "Generate Summary", "meetingSummary.generate": "Generate Summary",
"template.title": "Dictation Templates", "template.title": "Dictation Templates",
"template.create": "New Template", "template.create": "New Template",
"template.edit": "Edit Template", "template.edit": "Edit Template",
@ -381,7 +351,6 @@
"template.startSession": "Start", "template.startSession": "Start",
"template.cancelSession": "Cancel Session", "template.cancelSession": "Cancel Session",
"template.empty": "No templates", "template.empty": "No templates",
"nav.conversation": "Talk", "nav.conversation": "Talk",
"conversation.title": "Voice Conversation", "conversation.title": "Voice Conversation",
"conversation.idle": "Idle", "conversation.idle": "Idle",
@ -393,7 +362,6 @@
"conversation.inputPlaceholder": "Type a message...", "conversation.inputPlaceholder": "Type a message...",
"conversation.end": "End", "conversation.end": "End",
"conversation.clearHistory": "Clear history", "conversation.clearHistory": "Clear history",
"nav.knowledge": "Knowledge", "nav.knowledge": "Knowledge",
"rag.title": "Knowledge Base", "rag.title": "Knowledge Base",
"rag.addDocument": "Add Document", "rag.addDocument": "Add Document",
@ -410,12 +378,56 @@
"rag.sources": "Sources", "rag.sources": "Sources",
"rag.adding": "Adding...", "rag.adding": "Adding...",
"rag.parsing": "Parsing document...", "rag.parsing": "Parsing document...",
"voiceAction.title": "Voice Actions", "voiceAction.title": "Voice Actions",
"voiceAction.execute": "Execute", "voiceAction.execute": "Execute",
"voiceAction.presets": "Preset Commands", "voiceAction.presets": "Preset Commands",
"voiceAction.history": "Action History", "voiceAction.history": "Action History",
"voiceAction.noHistory": "No action history", "voiceAction.noHistory": "No action history",
"voiceAction.blocked": "Blocked (unsafe)", "voiceAction.blocked": "Blocked (unsafe)",
"voiceAction.executed": "Executed" "voiceAction.executed": "Executed",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "한국어 (Korean)",
"settings.sttLang.en": "English",
"settings.sttLang.ja": "日本語 (Japanese)",
"settings.sttLang.zh": "中文 (Chinese)",
"template.outputHelperText": "Use {{fieldName}} for placeholders",
"nav.meeting": "Meeting",
"meeting.title": "Meeting Mode",
"meeting.newMeeting": "New Meeting",
"meeting.startRecording": "Start Recording",
"meeting.stopRecording": "Stop Recording",
"meeting.recording": "Recording",
"meeting.processing": "Generating minutes...",
"meeting.completed": "Completed",
"meeting.error": "Error",
"meeting.elapsed": "Elapsed",
"meeting.memoPlaceholder": "Type a memo (Enter to add)",
"meeting.memoAdded": "Memo added",
"meeting.noSessions": "No meeting recordings",
"meeting.noSessionsDesc": "Press New Meeting to start recording",
"meeting.summary": "Summary",
"meeting.decisions": "Decisions",
"meeting.actionItems": "Action Items",
"meeting.timeline": "Timeline",
"meeting.transcript": "Raw Transcript",
"meeting.memos": "Memos",
"meeting.exportPdf": "Export PDF",
"meeting.exportMarkdown": "Export Markdown",
"meeting.delete": "Delete",
"meeting.deleteConfirm": "Delete this meeting record?",
"meeting.backToList": "Back to List",
"meeting.notificationTitle": "Minutes Ready",
"meeting.notificationBody": "Meeting minutes have been generated.",
"meeting.sessions": "{{count}} sessions",
"meeting.duration": "{{minutes}} min",
"meeting.editTitle": "Edit Title",
"meeting.untitled": "Untitled Meeting",
"meeting.processingStep.merging": "Merging transcript...",
"meeting.processingStep.generating": "Generating minutes with LLM...",
"meeting.processingStep.parsing": "Parsing minutes...",
"meeting.processingStep.saving": "Saving...",
"meeting.processingStep.notifying": "Sending notification..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "Asistente de voz con IA local", "app.tagline": "Asistente de voz con IA local",
"nav.dashboard": "Panel", "nav.dashboard": "Panel",
"nav.history": "Historial", "nav.history": "Historial",
"nav.dictionary": "Diccionario", "nav.dictionary": "Diccionario",
"nav.commands": "Comandos", "nav.commands": "Comandos",
"nav.settings": "Ajustes", "nav.settings": "Ajustes",
"dashboard.sessionOverview": "Resumen de sesión", "dashboard.sessionOverview": "Resumen de sesión",
"dashboard.systemStatus": "Estado del sistema", "dashboard.systemStatus": "Estado del sistema",
"dashboard.sessionsToday": "Sesiones hoy", "dashboard.sessionsToday": "Sesiones hoy",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} entradas", "dashboard.entries": "{{count}} entradas",
"dashboard.stat": "estadísticas", "dashboard.stat": "estadísticas",
"dashboard.sys": "sistema", "dashboard.sys": "sistema",
"history.title": "Historial de transcripciones", "history.title": "Historial de transcripciones",
"history.search": "Buscar...", "history.search": "Buscar...",
"history.entries": "{{count}} entradas", "history.entries": "{{count}} entradas",
@ -34,7 +31,6 @@
"history.noResults": "Sin resultados", "history.noResults": "Sin resultados",
"history.noHistory": "Sin historial — empieza a grabar", "history.noHistory": "Sin historial — empieza a grabar",
"history.count": "{{count}} entradas", "history.count": "{{count}} entradas",
"dictionary.title": "Diccionario personalizado", "dictionary.title": "Diccionario personalizado",
"dictionary.words": "{{count}} palabras", "dictionary.words": "{{count}} palabras",
"dictionary.add": "Añadir", "dictionary.add": "Añadir",
@ -47,7 +43,6 @@
"dictionary.addTitle": "Añadir palabra", "dictionary.addTitle": "Añadir palabra",
"dictionary.word": "Palabra", "dictionary.word": "Palabra",
"dictionary.pronunciation": "Pronunciación (opcional)", "dictionary.pronunciation": "Pronunciación (opcional)",
"commands.title": "Comandos LLM", "commands.title": "Comandos LLM",
"commands.count": "{{count}} comandos", "commands.count": "{{count}} comandos",
"commands.add": "Añadir", "commands.add": "Añadir",
@ -62,14 +57,12 @@
"commands.promptTemplate": "Plantilla de prompt", "commands.promptTemplate": "Plantilla de prompt",
"commands.promptHelp": "{{text}} se reemplazará por el texto transcrito", "commands.promptHelp": "{{text}} se reemplazará por el texto transcrito",
"commands.defaultPrompt": "Por favor, mejora {{text}}.", "commands.defaultPrompt": "Por favor, mejora {{text}}.",
"settings.title": "Ajustes", "settings.title": "Ajustes",
"settings.tabs.general": "General", "settings.tabs.general": "General",
"settings.tabs.audio": "Audio", "settings.tabs.audio": "Audio",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "Acerca de", "settings.tabs.about": "Acerca de",
"settings.shortcuts": "Atajos", "settings.shortcuts": "Atajos",
"settings.dictation": "Dictado", "settings.dictation": "Dictado",
"settings.dictation.desc": "Mantén pulsado para hablar. Al soltar, comienza la transcripción.", "settings.dictation.desc": "Mantén pulsado para hablar. Al soltar, comienza la transcripción.",
@ -82,20 +75,17 @@
"settings.notSet": "No configurado", "settings.notSet": "No configurado",
"settings.enabled": "Activado", "settings.enabled": "Activado",
"settings.disabled": "Desactivado", "settings.disabled": "Desactivado",
"settings.interface": "Interfaz", "settings.interface": "Interfaz",
"settings.theme": "Tema", "settings.theme": "Tema",
"settings.theme.system": "Sistema", "settings.theme.system": "Sistema",
"settings.theme.light": "Claro", "settings.theme.light": "Claro",
"settings.theme.dark": "Oscuro", "settings.theme.dark": "Oscuro",
"settings.language": "Idioma", "settings.language": "Idioma",
"settings.appBehavior": "Comportamiento de la app", "settings.appBehavior": "Comportamiento de la app",
"settings.closeToTray": "Minimizar a la bandeja", "settings.closeToTray": "Minimizar a la bandeja",
"settings.autoLaunch": "Iniciar con el sistema", "settings.autoLaunch": "Iniciar con el sistema",
"settings.autoInsert": "Insertar texto automáticamente tras transcripción", "settings.autoInsert": "Insertar texto automáticamente tras transcripción",
"settings.soundEffects": "Efectos de sonido", "settings.soundEffects": "Efectos de sonido",
"settings.microphone": "Micrófono", "settings.microphone": "Micrófono",
"settings.inputDevice": "Dispositivo de entrada", "settings.inputDevice": "Dispositivo de entrada",
"settings.deviceDefault": "(predeterminado)", "settings.deviceDefault": "(predeterminado)",
@ -103,7 +93,6 @@
"settings.insertMethod": "Método de inserción", "settings.insertMethod": "Método de inserción",
"settings.insertClipboard": "Portapapeles (Ctrl+V)", "settings.insertClipboard": "Portapapeles (Ctrl+V)",
"settings.insertKeyboard": "Escritura con teclado", "settings.insertKeyboard": "Escritura con teclado",
"settings.whisperModel": "Modelo Whisper", "settings.whisperModel": "Modelo Whisper",
"settings.model.tiny": "tiny (39 MB, más rápido)", "settings.model.tiny": "tiny (39 MB, más rápido)",
"settings.model.base": "base (74 MB, equilibrado)", "settings.model.base": "base (74 MB, equilibrado)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1.5 GB, mejor)", "settings.model.large": "large-v3 (1.5 GB, mejor)",
"settings.sttLanguage": "Idioma de reconocimiento", "settings.sttLanguage": "Idioma de reconocimiento",
"settings.sttLang.auto": "Detección automática", "settings.sttLang.auto": "Detección automática",
"settings.ollamaServer": "Servidor Ollama", "settings.ollamaServer": "Servidor Ollama",
"settings.ollamaUrl": "URL del servidor Ollama", "settings.ollamaUrl": "URL del servidor Ollama",
"settings.ollamaHint": "D3RO Voice se conecta automáticamente cuando Ollama está en ejecución. Descarga modelos directamente desde Ollama (ej: ollama pull qwen3:4b).", "settings.ollamaHint": "D3RO Voice se conecta automáticamente cuando Ollama está en ejecución. Descarga modelos directamente desde Ollama (ej: ollama pull qwen3:4b).",
@ -125,7 +113,6 @@
"settings.action.grammar": "Corrección gramatical", "settings.action.grammar": "Corrección gramatical",
"settings.action.custom": "Prompt personalizado", "settings.action.custom": "Prompt personalizado",
"settings.actionHint": "El postprocesado LLM seleccionado se aplica al texto transcrito tras grabar con el atajo. Solo funciona cuando Ollama está conectado.", "settings.actionHint": "El postprocesado LLM seleccionado se aplica al texto transcrito tras grabar con el atajo. Solo funciona cuando Ollama está conectado.",
"settings.about.version": "Versión", "settings.about.version": "Versión",
"settings.about.techStack": "Stack tecnológico", "settings.about.techStack": "Stack tecnológico",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (local) / LLM: Ollama (local)", "settings.about.voiceEngineValue": "STT: faster-whisper (local) / LLM: Ollama (local)",
"settings.about.description": "Asistente de voz con IA completamente local, basado en ingeniería inversa de Speakly. Funciona sin dependencias en la nube.", "settings.about.description": "Asistente de voz con IA completamente local, basado en ingeniería inversa de Speakly. Funciona sin dependencias en la nube.",
"settings.about.restartOnboarding": "Ver guía de configuración inicial de nuevo", "settings.about.restartOnboarding": "Ver guía de configuración inicial de nuevo",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama no está en ejecución", "status.nudge.title": "Ollama no está en ejecución",
"status.nudge.desc": "Se necesita Ollama para el postprocesado LLM (traducción, resumen, etc.).", "status.nudge.desc": "Se necesita Ollama para el postprocesado LLM (traducción, resumen, etc.).",
"status.nudge.guide": "Ver guía de instalación →", "status.nudge.guide": "Ver guía de instalación →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "Sin escribir, con voz. Tu asistente de voz con IA local.", "onboarding.welcome.desc": "Sin escribir, con voz. Tu asistente de voz con IA local.",
"onboarding.welcome.start": "Comenzar", "onboarding.welcome.start": "Comenzar",
@ -160,7 +145,6 @@
"onboarding.done.start": "Comenzar", "onboarding.done.start": "Comenzar",
"onboarding.back": "Atrás", "onboarding.back": "Atrás",
"onboarding.next": "Siguiente", "onboarding.next": "Siguiente",
"hotkey.title": "Configurar atajo", "hotkey.title": "Configurar atajo",
"hotkey.dictationTitle": "Configurar atajo de dictado", "hotkey.dictationTitle": "Configurar atajo de dictado",
"hotkey.oneTouchTitle": "Configurar atajo de modo un toque", "hotkey.oneTouchTitle": "Configurar atajo de modo un toque",
@ -171,7 +155,6 @@
"hotkey.current": "Actual: {{keys}}", "hotkey.current": "Actual: {{keys}}",
"hotkey.hint": "Introduce una combinación (ej: Ctrl+Shift+Q) o una tecla sola (ej: F5)", "hotkey.hint": "Introduce una combinación (ej: Ctrl+Shift+Q) o una tecla sola (ej: F5)",
"hotkey.reset": "Volver a introducir", "hotkey.reset": "Volver a introducir",
"ollama.title": "GUÍA DE CONFIGURACIÓN DE OLLAMA", "ollama.title": "GUÍA DE CONFIGURACIÓN DE OLLAMA",
"ollama.step1.title": "PASO 1 — Instalar Ollama", "ollama.step1.title": "PASO 1 — Instalar Ollama",
"ollama.step1.desc": "Ollama es una herramienta gratuita para ejecutar LLMs de forma local.", "ollama.step1.desc": "Ollama es una herramienta gratuita para ejecutar LLMs de forma local.",
@ -180,7 +163,6 @@
"ollama.step2.alt": "O un modelo más grande: ollama pull qwen3:8b (más preciso, más lento)", "ollama.step2.alt": "O un modelo más grande: ollama pull qwen3:8b (más preciso, más lento)",
"ollama.step3.title": "PASO 3 — Conexión automática", "ollama.step3.title": "PASO 3 — Conexión automática",
"ollama.step3.desc": "D3RO-VOICE detecta automáticamente cuando Ollama está en ejecución. Cuando el LED de la barra de estado pase de rojo a verde, ¡estará listo!", "ollama.step3.desc": "D3RO-VOICE detecta automáticamente cuando Ollama está en ejecución. Cuando el LED de la barra de estado pase de rojo a verde, ¡estará listo!",
"service.sttEngine": "MOTOR STT", "service.sttEngine": "MOTOR STT",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "ATAJO DE TECLADO", "service.hotkeyHook": "ATAJO DE TECLADO",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVO", "service.active": "ACTIVO",
"service.standby": "EN ESPERA", "service.standby": "EN ESPERA",
"common.cancel": "Cancelar", "common.cancel": "Cancelar",
"common.save": "Guardar", "common.save": "Guardar",
"common.delete": "Eliminar", "common.delete": "Eliminar",
@ -202,7 +183,51 @@
"common.copy": "Copiar", "common.copy": "Copiar",
"common.test": "Probar", "common.test": "Probar",
"common.stop": "Detener", "common.stop": "Detener",
"date.today": "Hoy", "date.today": "Hoy",
"date.yesterday": "Ayer" "date.yesterday": "Ayer",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "Coreano",
"settings.sttLang.en": "Inglés",
"settings.sttLang.ja": "Japonés",
"settings.sttLang.zh": "Chino",
"template.outputHelperText": "Use {{nombreCampo}} para marcadores de posición",
"nav.meeting": "Reunión",
"meeting.title": "Modo Reunión",
"meeting.newMeeting": "Nueva Reunión",
"meeting.startRecording": "Iniciar Grabación",
"meeting.stopRecording": "Detener Grabación",
"meeting.recording": "Grabando",
"meeting.processing": "Generando actas...",
"meeting.completed": "Completado",
"meeting.error": "Error",
"meeting.elapsed": "Tiempo transcurrido",
"meeting.memoPlaceholder": "Escribe un memo (Enter para añadir)",
"meeting.memoAdded": "Memo añadido",
"meeting.noSessions": "Sin grabaciones de reuniones",
"meeting.noSessionsDesc": "Pulsa Nueva Reunión para empezar a grabar",
"meeting.summary": "Resumen",
"meeting.decisions": "Decisiones",
"meeting.actionItems": "Tareas",
"meeting.timeline": "Línea de tiempo",
"meeting.transcript": "Transcripción bruta",
"meeting.memos": "Memos",
"meeting.exportPdf": "Exportar PDF",
"meeting.exportMarkdown": "Exportar Markdown",
"meeting.delete": "Eliminar",
"meeting.deleteConfirm": "¿Eliminar este registro de reunión?",
"meeting.backToList": "Volver a la lista",
"meeting.notificationTitle": "Actas listas",
"meeting.notificationBody": "Las actas de la reunión han sido generadas.",
"meeting.sessions": "{{count}} sesiones",
"meeting.duration": "{{minutes}} min",
"meeting.editTitle": "Editar título",
"meeting.untitled": "Reunión sin título",
"meeting.processingStep.merging": "Fusionando transcripción...",
"meeting.processingStep.generating": "Generando actas con LLM...",
"meeting.processingStep.parsing": "Analizando actas...",
"meeting.processingStep.saving": "Guardando...",
"meeting.processingStep.notifying": "Enviando notificación..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "Assistant vocal IA local", "app.tagline": "Assistant vocal IA local",
"nav.dashboard": "Tableau de bord", "nav.dashboard": "Tableau de bord",
"nav.history": "Historique", "nav.history": "Historique",
"nav.dictionary": "Dictionnaire", "nav.dictionary": "Dictionnaire",
"nav.commands": "Commandes", "nav.commands": "Commandes",
"nav.settings": "Paramètres", "nav.settings": "Paramètres",
"dashboard.sessionOverview": "Aperçu de session", "dashboard.sessionOverview": "Aperçu de session",
"dashboard.systemStatus": "État du système", "dashboard.systemStatus": "État du système",
"dashboard.sessionsToday": "Sessions aujourd'hui", "dashboard.sessionsToday": "Sessions aujourd'hui",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} entrées", "dashboard.entries": "{{count}} entrées",
"dashboard.stat": "statistiques", "dashboard.stat": "statistiques",
"dashboard.sys": "système", "dashboard.sys": "système",
"history.title": "Historique des transcriptions", "history.title": "Historique des transcriptions",
"history.search": "Rechercher...", "history.search": "Rechercher...",
"history.entries": "{{count}} entrées", "history.entries": "{{count}} entrées",
@ -34,7 +31,6 @@
"history.noResults": "Aucun résultat", "history.noResults": "Aucun résultat",
"history.noHistory": "Aucun historique — commencez à enregistrer", "history.noHistory": "Aucun historique — commencez à enregistrer",
"history.count": "{{count}} entrées", "history.count": "{{count}} entrées",
"dictionary.title": "Dictionnaire personnalisé", "dictionary.title": "Dictionnaire personnalisé",
"dictionary.words": "{{count}} mots", "dictionary.words": "{{count}} mots",
"dictionary.add": "Ajouter", "dictionary.add": "Ajouter",
@ -47,7 +43,6 @@
"dictionary.addTitle": "Ajouter un mot", "dictionary.addTitle": "Ajouter un mot",
"dictionary.word": "Mot", "dictionary.word": "Mot",
"dictionary.pronunciation": "Prononciation (facultatif)", "dictionary.pronunciation": "Prononciation (facultatif)",
"commands.title": "Commandes LLM", "commands.title": "Commandes LLM",
"commands.count": "{{count}} commandes", "commands.count": "{{count}} commandes",
"commands.add": "Ajouter", "commands.add": "Ajouter",
@ -62,14 +57,12 @@
"commands.promptTemplate": "Modèle de prompt", "commands.promptTemplate": "Modèle de prompt",
"commands.promptHelp": "{{text}} sera remplacé par le texte transcrit", "commands.promptHelp": "{{text}} sera remplacé par le texte transcrit",
"commands.defaultPrompt": "Veuillez améliorer {{text}}.", "commands.defaultPrompt": "Veuillez améliorer {{text}}.",
"settings.title": "Paramètres", "settings.title": "Paramètres",
"settings.tabs.general": "Général", "settings.tabs.general": "Général",
"settings.tabs.audio": "Audio", "settings.tabs.audio": "Audio",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "À propos", "settings.tabs.about": "À propos",
"settings.shortcuts": "Raccourcis", "settings.shortcuts": "Raccourcis",
"settings.dictation": "Dictée", "settings.dictation": "Dictée",
"settings.dictation.desc": "Maintenez enfoncé pour parler. La transcription démarre au relâchement.", "settings.dictation.desc": "Maintenez enfoncé pour parler. La transcription démarre au relâchement.",
@ -82,20 +75,17 @@
"settings.notSet": "Non configuré", "settings.notSet": "Non configuré",
"settings.enabled": "Activé", "settings.enabled": "Activé",
"settings.disabled": "Désactivé", "settings.disabled": "Désactivé",
"settings.interface": "Interface", "settings.interface": "Interface",
"settings.theme": "Thème", "settings.theme": "Thème",
"settings.theme.system": "Système", "settings.theme.system": "Système",
"settings.theme.light": "Clair", "settings.theme.light": "Clair",
"settings.theme.dark": "Sombre", "settings.theme.dark": "Sombre",
"settings.language": "Langue", "settings.language": "Langue",
"settings.appBehavior": "Comportement de l'application", "settings.appBehavior": "Comportement de l'application",
"settings.closeToTray": "Réduire dans la barre système", "settings.closeToTray": "Réduire dans la barre système",
"settings.autoLaunch": "Lancer au démarrage du système", "settings.autoLaunch": "Lancer au démarrage du système",
"settings.autoInsert": "Insérer le texte automatiquement après la transcription", "settings.autoInsert": "Insérer le texte automatiquement après la transcription",
"settings.soundEffects": "Effets sonores", "settings.soundEffects": "Effets sonores",
"settings.microphone": "Microphone", "settings.microphone": "Microphone",
"settings.inputDevice": "Périphérique d'entrée", "settings.inputDevice": "Périphérique d'entrée",
"settings.deviceDefault": "(par défaut)", "settings.deviceDefault": "(par défaut)",
@ -103,7 +93,6 @@
"settings.insertMethod": "Méthode d'insertion", "settings.insertMethod": "Méthode d'insertion",
"settings.insertClipboard": "Presse-papiers (Ctrl+V)", "settings.insertClipboard": "Presse-papiers (Ctrl+V)",
"settings.insertKeyboard": "Frappe au clavier", "settings.insertKeyboard": "Frappe au clavier",
"settings.whisperModel": "Modèle Whisper", "settings.whisperModel": "Modèle Whisper",
"settings.model.tiny": "tiny (39 Mo, le plus rapide)", "settings.model.tiny": "tiny (39 Mo, le plus rapide)",
"settings.model.base": "base (74 Mo, équilibré)", "settings.model.base": "base (74 Mo, équilibré)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1,5 Go, meilleur)", "settings.model.large": "large-v3 (1,5 Go, meilleur)",
"settings.sttLanguage": "Langue de reconnaissance", "settings.sttLanguage": "Langue de reconnaissance",
"settings.sttLang.auto": "Détection automatique", "settings.sttLang.auto": "Détection automatique",
"settings.ollamaServer": "Serveur Ollama", "settings.ollamaServer": "Serveur Ollama",
"settings.ollamaUrl": "URL du serveur Ollama", "settings.ollamaUrl": "URL du serveur Ollama",
"settings.ollamaHint": "D3RO Voice se connecte automatiquement quand Ollama est en cours d'exécution. Téléchargez les modèles directement depuis Ollama (ex : ollama pull qwen3:4b).", "settings.ollamaHint": "D3RO Voice se connecte automatiquement quand Ollama est en cours d'exécution. Téléchargez les modèles directement depuis Ollama (ex : ollama pull qwen3:4b).",
@ -125,7 +113,6 @@
"settings.action.grammar": "Correction grammaticale", "settings.action.grammar": "Correction grammaticale",
"settings.action.custom": "Prompt personnalisé", "settings.action.custom": "Prompt personnalisé",
"settings.actionHint": "Le post-traitement LLM sélectionné est appliqué au texte transcrit après l'enregistrement avec le raccourci. Ne fonctionne que lorsque Ollama est connecté.", "settings.actionHint": "Le post-traitement LLM sélectionné est appliqué au texte transcrit après l'enregistrement avec le raccourci. Ne fonctionne que lorsque Ollama est connecté.",
"settings.about.version": "Version", "settings.about.version": "Version",
"settings.about.techStack": "Stack technologique", "settings.about.techStack": "Stack technologique",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT : faster-whisper (local) / LLM : Ollama (local)", "settings.about.voiceEngineValue": "STT : faster-whisper (local) / LLM : Ollama (local)",
"settings.about.description": "Assistant vocal IA entièrement local, basé sur l'ingénierie inverse de Speakly. Fonctionne sans dépendance au cloud.", "settings.about.description": "Assistant vocal IA entièrement local, basé sur l'ingénierie inverse de Speakly. Fonctionne sans dépendance au cloud.",
"settings.about.restartOnboarding": "Revoir le guide de configuration initiale", "settings.about.restartOnboarding": "Revoir le guide de configuration initiale",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "HORS LIGNE", "status.offline": "HORS LIGNE",
"status.nudge.title": "Ollama n'est pas en cours d'exécution", "status.nudge.title": "Ollama n'est pas en cours d'exécution",
"status.nudge.desc": "Ollama est nécessaire pour le post-traitement LLM (traduction, résumé, etc.).", "status.nudge.desc": "Ollama est nécessaire pour le post-traitement LLM (traduction, résumé, etc.).",
"status.nudge.guide": "Voir le guide d'installation →", "status.nudge.guide": "Voir le guide d'installation →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "Sans taper, par la voix. Votre assistant vocal IA local.", "onboarding.welcome.desc": "Sans taper, par la voix. Votre assistant vocal IA local.",
"onboarding.welcome.start": "Commencer", "onboarding.welcome.start": "Commencer",
@ -160,7 +145,6 @@
"onboarding.done.start": "Commencer", "onboarding.done.start": "Commencer",
"onboarding.back": "Retour", "onboarding.back": "Retour",
"onboarding.next": "Suivant", "onboarding.next": "Suivant",
"hotkey.title": "Configurer le raccourci", "hotkey.title": "Configurer le raccourci",
"hotkey.dictationTitle": "Configurer le raccourci de dictée", "hotkey.dictationTitle": "Configurer le raccourci de dictée",
"hotkey.oneTouchTitle": "Configurer le raccourci du mode une touche", "hotkey.oneTouchTitle": "Configurer le raccourci du mode une touche",
@ -171,7 +155,6 @@
"hotkey.current": "Actuel : {{keys}}", "hotkey.current": "Actuel : {{keys}}",
"hotkey.hint": "Saisissez une combinaison (ex : Ctrl+Shift+Q) ou une touche seule (ex : F5)", "hotkey.hint": "Saisissez une combinaison (ex : Ctrl+Shift+Q) ou une touche seule (ex : F5)",
"hotkey.reset": "Ressaisir", "hotkey.reset": "Ressaisir",
"ollama.title": "GUIDE DE CONFIGURATION OLLAMA", "ollama.title": "GUIDE DE CONFIGURATION OLLAMA",
"ollama.step1.title": "ÉTAPE 1 — Installer Ollama", "ollama.step1.title": "ÉTAPE 1 — Installer Ollama",
"ollama.step1.desc": "Ollama est un outil gratuit pour exécuter des LLM en local.", "ollama.step1.desc": "Ollama est un outil gratuit pour exécuter des LLM en local.",
@ -180,7 +163,6 @@
"ollama.step2.alt": "Ou un modèle plus grand : ollama pull qwen3:8b (plus précis, plus lent)", "ollama.step2.alt": "Ou un modèle plus grand : ollama pull qwen3:8b (plus précis, plus lent)",
"ollama.step3.title": "ÉTAPE 3 — Connexion automatique", "ollama.step3.title": "ÉTAPE 3 — Connexion automatique",
"ollama.step3.desc": "D3RO-VOICE détecte automatiquement quand Ollama est en cours d'exécution. Quand la LED de la barre d'état passe du rouge au vert, c'est prêt !", "ollama.step3.desc": "D3RO-VOICE détecte automatiquement quand Ollama est en cours d'exécution. Quand la LED de la barre d'état passe du rouge au vert, c'est prêt !",
"service.sttEngine": "MOTEUR STT", "service.sttEngine": "MOTEUR STT",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "RACCOURCI CLAVIER", "service.hotkeyHook": "RACCOURCI CLAVIER",
@ -190,7 +172,6 @@
"service.offline": "HORS LIGNE", "service.offline": "HORS LIGNE",
"service.active": "ACTIF", "service.active": "ACTIF",
"service.standby": "EN VEILLE", "service.standby": "EN VEILLE",
"common.cancel": "Annuler", "common.cancel": "Annuler",
"common.save": "Enregistrer", "common.save": "Enregistrer",
"common.delete": "Supprimer", "common.delete": "Supprimer",
@ -202,7 +183,51 @@
"common.copy": "Copier", "common.copy": "Copier",
"common.test": "Tester", "common.test": "Tester",
"common.stop": "Arrêter", "common.stop": "Arrêter",
"date.today": "Aujourd'hui", "date.today": "Aujourd'hui",
"date.yesterday": "Hier" "date.yesterday": "Hier",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "Coréen",
"settings.sttLang.en": "Anglais",
"settings.sttLang.ja": "Japonais",
"settings.sttLang.zh": "Chinois",
"template.outputHelperText": "Utilisez {{nomChamp}} pour les espaces réservés",
"nav.meeting": "Réunion",
"meeting.title": "Mode Réunion",
"meeting.newMeeting": "Nouvelle Réunion",
"meeting.startRecording": "Démarrer l'enregistrement",
"meeting.stopRecording": "Arrêter l'enregistrement",
"meeting.recording": "Enregistrement en cours",
"meeting.processing": "Génération du compte-rendu...",
"meeting.completed": "Terminé",
"meeting.error": "Erreur",
"meeting.elapsed": "Temps écoulé",
"meeting.memoPlaceholder": "Saisir un mémo (Entrée pour ajouter)",
"meeting.memoAdded": "Mémo ajouté",
"meeting.noSessions": "Aucun enregistrement de réunion",
"meeting.noSessionsDesc": "Appuyez sur Nouvelle Réunion pour commencer l'enregistrement",
"meeting.summary": "Résumé",
"meeting.decisions": "Décisions",
"meeting.actionItems": "Actions à mener",
"meeting.timeline": "Chronologie",
"meeting.transcript": "Transcription brute",
"meeting.memos": "Mémos",
"meeting.exportPdf": "Exporter en PDF",
"meeting.exportMarkdown": "Exporter en Markdown",
"meeting.delete": "Supprimer",
"meeting.deleteConfirm": "Supprimer cet enregistrement de réunion ?",
"meeting.backToList": "Retour à la liste",
"meeting.notificationTitle": "Compte-rendu prêt",
"meeting.notificationBody": "Le compte-rendu de la réunion a été généré.",
"meeting.sessions": "{{count}} séances",
"meeting.duration": "{{minutes}} min",
"meeting.editTitle": "Modifier le titre",
"meeting.untitled": "Réunion sans titre",
"meeting.processingStep.merging": "Fusion de la transcription...",
"meeting.processingStep.generating": "Génération du compte-rendu avec LLM...",
"meeting.processingStep.parsing": "Analyse du compte-rendu...",
"meeting.processingStep.saving": "Enregistrement...",
"meeting.processingStep.notifying": "Envoi de la notification..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "ローカルAI音声アシスタント", "app.tagline": "ローカルAI音声アシスタント",
"nav.dashboard": "ダッシュボード", "nav.dashboard": "ダッシュボード",
"nav.history": "履歴", "nav.history": "履歴",
"nav.dictionary": "辞書", "nav.dictionary": "辞書",
"nav.commands": "コマンド", "nav.commands": "コマンド",
"nav.settings": "設定", "nav.settings": "設定",
"dashboard.sessionOverview": "セッション概要", "dashboard.sessionOverview": "セッション概要",
"dashboard.systemStatus": "システム状態", "dashboard.systemStatus": "システム状態",
"dashboard.sessionsToday": "本日のセッション", "dashboard.sessionsToday": "本日のセッション",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}}件", "dashboard.entries": "{{count}}件",
"dashboard.stat": "統計", "dashboard.stat": "統計",
"dashboard.sys": "システム", "dashboard.sys": "システム",
"history.title": "文字起こし履歴", "history.title": "文字起こし履歴",
"history.search": "検索...", "history.search": "検索...",
"history.entries": "{{count}}件", "history.entries": "{{count}}件",
@ -34,7 +31,6 @@
"history.noResults": "検索結果なし", "history.noResults": "検索結果なし",
"history.noHistory": "履歴なし — 録音を開始してください", "history.noHistory": "履歴なし — 録音を開始してください",
"history.count": "{{count}}件", "history.count": "{{count}}件",
"dictionary.title": "カスタム辞書", "dictionary.title": "カスタム辞書",
"dictionary.words": "{{count}}語", "dictionary.words": "{{count}}語",
"dictionary.add": "追加", "dictionary.add": "追加",
@ -47,7 +43,6 @@
"dictionary.addTitle": "単語を追加", "dictionary.addTitle": "単語を追加",
"dictionary.word": "単語", "dictionary.word": "単語",
"dictionary.pronunciation": "読み仮名 (任意)", "dictionary.pronunciation": "読み仮名 (任意)",
"commands.title": "LLMコマンド", "commands.title": "LLMコマンド",
"commands.count": "{{count}}個", "commands.count": "{{count}}個",
"commands.add": "追加", "commands.add": "追加",
@ -62,14 +57,12 @@
"commands.promptTemplate": "プロンプトテンプレート", "commands.promptTemplate": "プロンプトテンプレート",
"commands.promptHelp": "{{text}} は文字起こしされたテキストに置換されます", "commands.promptHelp": "{{text}} は文字起こしされたテキストに置換されます",
"commands.defaultPrompt": "{{text}} を整えてください。", "commands.defaultPrompt": "{{text}} を整えてください。",
"settings.title": "設定", "settings.title": "設定",
"settings.tabs.general": "一般", "settings.tabs.general": "一般",
"settings.tabs.audio": "オーディオ", "settings.tabs.audio": "オーディオ",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "情報", "settings.tabs.about": "情報",
"settings.shortcuts": "ショートカット", "settings.shortcuts": "ショートカット",
"settings.dictation": "ディクテーション", "settings.dictation": "ディクテーション",
"settings.dictation.desc": "押しながら話します。キーを離すと文字起こしが始まります。", "settings.dictation.desc": "押しながら話します。キーを離すと文字起こしが始まります。",
@ -82,20 +75,17 @@
"settings.notSet": "未設定", "settings.notSet": "未設定",
"settings.enabled": "有効", "settings.enabled": "有効",
"settings.disabled": "無効", "settings.disabled": "無効",
"settings.interface": "インターフェース", "settings.interface": "インターフェース",
"settings.theme": "テーマ", "settings.theme": "テーマ",
"settings.theme.system": "システム", "settings.theme.system": "システム",
"settings.theme.light": "ライト", "settings.theme.light": "ライト",
"settings.theme.dark": "ダーク", "settings.theme.dark": "ダーク",
"settings.language": "言語", "settings.language": "言語",
"settings.appBehavior": "アプリの動作", "settings.appBehavior": "アプリの動作",
"settings.closeToTray": "トレイに最小化", "settings.closeToTray": "トレイに最小化",
"settings.autoLaunch": "システム起動時に自動起動", "settings.autoLaunch": "システム起動時に自動起動",
"settings.autoInsert": "文字起こし後に自動テキスト挿入", "settings.autoInsert": "文字起こし後に自動テキスト挿入",
"settings.soundEffects": "効果音", "settings.soundEffects": "効果音",
"settings.microphone": "マイク", "settings.microphone": "マイク",
"settings.inputDevice": "入力デバイス", "settings.inputDevice": "入力デバイス",
"settings.deviceDefault": "(デフォルト)", "settings.deviceDefault": "(デフォルト)",
@ -103,7 +93,6 @@
"settings.insertMethod": "挿入方式", "settings.insertMethod": "挿入方式",
"settings.insertClipboard": "クリップボード (Ctrl+V)", "settings.insertClipboard": "クリップボード (Ctrl+V)",
"settings.insertKeyboard": "キーボード入力", "settings.insertKeyboard": "キーボード入力",
"settings.whisperModel": "Whisperモデル", "settings.whisperModel": "Whisperモデル",
"settings.model.tiny": "tiny (39 MB, 最速)", "settings.model.tiny": "tiny (39 MB, 最速)",
"settings.model.base": "base (74 MB, バランス)", "settings.model.base": "base (74 MB, バランス)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1.5 GB, 最高)", "settings.model.large": "large-v3 (1.5 GB, 最高)",
"settings.sttLanguage": "認識言語", "settings.sttLanguage": "認識言語",
"settings.sttLang.auto": "自動検出", "settings.sttLang.auto": "自動検出",
"settings.ollamaServer": "Ollamaサーバー", "settings.ollamaServer": "Ollamaサーバー",
"settings.ollamaUrl": "OllamaサーバーURL", "settings.ollamaUrl": "OllamaサーバーURL",
"settings.ollamaHint": "Ollamaが起動していれば自動的に接続されます。モデルはOllamaから直接pullしてください例: ollama pull qwen3:4b。", "settings.ollamaHint": "Ollamaが起動していれば自動的に接続されます。モデルはOllamaから直接pullしてください例: ollama pull qwen3:4b。",
@ -125,7 +113,6 @@
"settings.action.grammar": "文法修正", "settings.action.grammar": "文法修正",
"settings.action.custom": "カスタムプロンプト", "settings.action.custom": "カスタムプロンプト",
"settings.actionHint": "ホットキーで録音後、文字起こしされたテキストに選択したLLM後処理が適用されます。Ollamaが接続されている場合のみ動作します。", "settings.actionHint": "ホットキーで録音後、文字起こしされたテキストに選択したLLM後処理が適用されます。Ollamaが接続されている場合のみ動作します。",
"settings.about.version": "バージョン", "settings.about.version": "バージョン",
"settings.about.techStack": "技術スタック", "settings.about.techStack": "技術スタック",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (ローカル) / LLM: Ollama (ローカル)", "settings.about.voiceEngineValue": "STT: faster-whisper (ローカル) / LLM: Ollama (ローカル)",
"settings.about.description": "Speaklyリバースエンジニアリングのウハウをもとに構築したローカルAI音声アシスタント。クラウドに依存せず完全ローカルで動作します。", "settings.about.description": "Speaklyリバースエンジニアリングのウハウをもとに構築したローカルAI音声アシスタント。クラウドに依存せず完全ローカルで動作します。",
"settings.about.restartOnboarding": "初期設定ガイドをもう一度見る", "settings.about.restartOnboarding": "初期設定ガイドをもう一度見る",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollamaが起動していません", "status.nudge.title": "Ollamaが起動していません",
"status.nudge.desc": "LLM後処理翻訳、要約などを使用するにはOllamaが必要です。", "status.nudge.desc": "LLM後処理翻訳、要約などを使用するにはOllamaが必要です。",
"status.nudge.guide": "インストールガイドを見る →", "status.nudge.guide": "インストールガイドを見る →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "タイピング不要、音声で。ローカルAI音声アシスタントです。", "onboarding.welcome.desc": "タイピング不要、音声で。ローカルAI音声アシスタントです。",
"onboarding.welcome.start": "始める", "onboarding.welcome.start": "始める",
@ -160,7 +145,6 @@
"onboarding.done.start": "始める", "onboarding.done.start": "始める",
"onboarding.back": "戻る", "onboarding.back": "戻る",
"onboarding.next": "次へ", "onboarding.next": "次へ",
"hotkey.title": "ショートカット設定", "hotkey.title": "ショートカット設定",
"hotkey.dictationTitle": "ディクテーションショートカットの設定", "hotkey.dictationTitle": "ディクテーションショートカットの設定",
"hotkey.oneTouchTitle": "ワンタッチモードショートカットの設定", "hotkey.oneTouchTitle": "ワンタッチモードショートカットの設定",
@ -171,7 +155,6 @@
"hotkey.current": "現在: {{keys}}", "hotkey.current": "現在: {{keys}}",
"hotkey.hint": "組み合わせキー(例: Ctrl+Shift+Qまたは単一キー例: F5を入力してください", "hotkey.hint": "組み合わせキー(例: Ctrl+Shift+Qまたは単一キー例: F5を入力してください",
"hotkey.reset": "再入力", "hotkey.reset": "再入力",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "STEP 1 — Ollamaのインストール", "ollama.step1.title": "STEP 1 — Ollamaのインストール",
"ollama.step1.desc": "OllamaはローカルでLLMを実行するための無料ツールです。", "ollama.step1.desc": "OllamaはローカルでLLMを実行するための無料ツールです。",
@ -180,7 +163,6 @@
"ollama.step2.alt": "または大きいモデル: ollama pull qwen3:8b (より正確、より遅い)", "ollama.step2.alt": "または大きいモデル: ollama pull qwen3:8b (より正確、より遅い)",
"ollama.step3.title": "STEP 3 — 自動接続", "ollama.step3.title": "STEP 3 — 自動接続",
"ollama.step3.desc": "Ollamaが起動するとD3RO-VOICEが自動的に検出します。下部ステータスバーのLEDが赤から緑に変われば準備完了", "ollama.step3.desc": "Ollamaが起動するとD3RO-VOICEが自動的に検出します。下部ステータスバーのLEDが赤から緑に変われば準備完了",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "キャンセル", "common.cancel": "キャンセル",
"common.save": "保存", "common.save": "保存",
"common.delete": "削除", "common.delete": "削除",
@ -202,7 +183,51 @@
"common.copy": "コピー", "common.copy": "コピー",
"common.test": "テスト", "common.test": "テスト",
"common.stop": "停止", "common.stop": "停止",
"date.today": "今日", "date.today": "今日",
"date.yesterday": "昨日" "date.yesterday": "昨日",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "한국語",
"settings.sttLang.en": "English",
"settings.sttLang.ja": "日本語",
"settings.sttLang.zh": "中文",
"template.outputHelperText": "{{フィールド名}} でプレースホルダーを使用",
"nav.meeting": "ミーティング",
"meeting.title": "ミーティングモード",
"meeting.newMeeting": "新規ミーティング",
"meeting.startRecording": "録音開始",
"meeting.stopRecording": "録音停止",
"meeting.recording": "録音中",
"meeting.processing": "議事録を生成中...",
"meeting.completed": "完了",
"meeting.error": "エラー",
"meeting.elapsed": "経過時間",
"meeting.memoPlaceholder": "メモを入力Enterで追加",
"meeting.memoAdded": "メモを追加しました",
"meeting.noSessions": "ミーティングの録音がありません",
"meeting.noSessionsDesc": "「新規ミーティング」を押して録音を開始してください",
"meeting.summary": "要約",
"meeting.decisions": "決定事項",
"meeting.actionItems": "アクションアイテム",
"meeting.timeline": "タイムライン",
"meeting.transcript": "生のトランスクリプト",
"meeting.memos": "メモ",
"meeting.exportPdf": "PDFエクスポート",
"meeting.exportMarkdown": "Markdownエクスポート",
"meeting.delete": "削除",
"meeting.deleteConfirm": "このミーティング記録を削除しますか?",
"meeting.backToList": "一覧に戻る",
"meeting.notificationTitle": "議事録が完成しました",
"meeting.notificationBody": "ミーティングの議事録が生成されました。",
"meeting.sessions": "{{count}} セッション",
"meeting.duration": "{{minutes}} 分",
"meeting.editTitle": "タイトルを編集",
"meeting.untitled": "無題のミーティング",
"meeting.processingStep.merging": "トランスクリプトをマージ中...",
"meeting.processingStep.generating": "LLMで議事録を生成中...",
"meeting.processingStep.parsing": "議事録を解析中...",
"meeting.processingStep.saving": "保存中...",
"meeting.processingStep.notifying": "通知を送信中..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "로컬 AI 음성 어시스턴트", "app.tagline": "로컬 AI 음성 어시스턴트",
"nav.dashboard": "대시보드", "nav.dashboard": "대시보드",
"nav.history": "히스토리", "nav.history": "히스토리",
"nav.dictionary": "사전", "nav.dictionary": "사전",
"nav.commands": "명령어", "nav.commands": "명령어",
"nav.settings": "설정", "nav.settings": "설정",
"dashboard.sessionOverview": "세션 개요", "dashboard.sessionOverview": "세션 개요",
"dashboard.systemStatus": "시스템 상태", "dashboard.systemStatus": "시스템 상태",
"dashboard.sessionsToday": "오늘 세션", "dashboard.sessionsToday": "오늘 세션",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}}건", "dashboard.entries": "{{count}}건",
"dashboard.stat": "통계", "dashboard.stat": "통계",
"dashboard.sys": "시스템", "dashboard.sys": "시스템",
"history.title": "전사 기록", "history.title": "전사 기록",
"history.search": "검색...", "history.search": "검색...",
"history.entries": "{{count}}건", "history.entries": "{{count}}건",
@ -34,7 +31,6 @@
"history.noResults": "검색 결과 없음", "history.noResults": "검색 결과 없음",
"history.noHistory": "히스토리 없음 — 녹음을 시작하세요", "history.noHistory": "히스토리 없음 — 녹음을 시작하세요",
"history.count": "{{count}}건", "history.count": "{{count}}건",
"dictionary.title": "커스텀 사전", "dictionary.title": "커스텀 사전",
"dictionary.words": "{{count}}개", "dictionary.words": "{{count}}개",
"dictionary.add": "추가", "dictionary.add": "추가",
@ -47,7 +43,6 @@
"dictionary.addTitle": "단어 추가", "dictionary.addTitle": "단어 추가",
"dictionary.word": "단어", "dictionary.word": "단어",
"dictionary.pronunciation": "발음 (선택)", "dictionary.pronunciation": "발음 (선택)",
"commands.title": "LLM 명령어", "commands.title": "LLM 명령어",
"commands.count": "{{count}}개", "commands.count": "{{count}}개",
"commands.add": "추가", "commands.add": "추가",
@ -62,14 +57,12 @@
"commands.promptTemplate": "프롬프트 템플릿", "commands.promptTemplate": "프롬프트 템플릿",
"commands.promptHelp": "{{text}}는 전사된 텍스트로 치환됩니다", "commands.promptHelp": "{{text}}는 전사된 텍스트로 치환됩니다",
"commands.defaultPrompt": "{{text}}를 다듬어주세요.", "commands.defaultPrompt": "{{text}}를 다듬어주세요.",
"settings.title": "설정", "settings.title": "설정",
"settings.tabs.general": "일반", "settings.tabs.general": "일반",
"settings.tabs.audio": "오디오", "settings.tabs.audio": "오디오",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "정보", "settings.tabs.about": "정보",
"settings.shortcuts": "단축키", "settings.shortcuts": "단축키",
"settings.dictation": "받아쓰기", "settings.dictation": "받아쓰기",
"settings.dictation.desc": "누른 상태에서 말하기. 키를 놓으면 전사가 시작됩니다.", "settings.dictation.desc": "누른 상태에서 말하기. 키를 놓으면 전사가 시작됩니다.",
@ -89,20 +82,17 @@
"settings.notSet": "미설정", "settings.notSet": "미설정",
"settings.enabled": "활성화됨", "settings.enabled": "활성화됨",
"settings.disabled": "비활성화", "settings.disabled": "비활성화",
"settings.interface": "인터페이스", "settings.interface": "인터페이스",
"settings.theme": "테마", "settings.theme": "테마",
"settings.theme.system": "시스템", "settings.theme.system": "시스템",
"settings.theme.light": "라이트", "settings.theme.light": "라이트",
"settings.theme.dark": "다크", "settings.theme.dark": "다크",
"settings.language": "언어", "settings.language": "언어",
"settings.appBehavior": "앱 동작", "settings.appBehavior": "앱 동작",
"settings.closeToTray": "트레이로 최소화", "settings.closeToTray": "트레이로 최소화",
"settings.autoLaunch": "시스템 시작 시 자동 실행", "settings.autoLaunch": "시스템 시작 시 자동 실행",
"settings.autoInsert": "전사 후 자동 텍스트 삽입", "settings.autoInsert": "전사 후 자동 텍스트 삽입",
"settings.soundEffects": "효과음", "settings.soundEffects": "효과음",
"settings.microphone": "마이크", "settings.microphone": "마이크",
"settings.inputDevice": "입력 장치", "settings.inputDevice": "입력 장치",
"settings.deviceDefault": "(기본)", "settings.deviceDefault": "(기본)",
@ -110,7 +100,6 @@
"settings.insertMethod": "삽입 방식", "settings.insertMethod": "삽입 방식",
"settings.insertClipboard": "클립보드 (Ctrl+V)", "settings.insertClipboard": "클립보드 (Ctrl+V)",
"settings.insertKeyboard": "키보드 타이핑", "settings.insertKeyboard": "키보드 타이핑",
"settings.whisperModel": "Whisper 모델", "settings.whisperModel": "Whisper 모델",
"settings.model.tiny": "tiny (39 MB, 가장 빠름)", "settings.model.tiny": "tiny (39 MB, 가장 빠름)",
"settings.model.base": "base (74 MB, 균형)", "settings.model.base": "base (74 MB, 균형)",
@ -119,7 +108,6 @@
"settings.model.large": "large-v3 (1.5 GB, 최고)", "settings.model.large": "large-v3 (1.5 GB, 최고)",
"settings.sttLanguage": "인식 언어", "settings.sttLanguage": "인식 언어",
"settings.sttLang.auto": "자동 감지", "settings.sttLang.auto": "자동 감지",
"settings.ollamaServer": "Ollama 서버", "settings.ollamaServer": "Ollama 서버",
"settings.ollamaUrl": "Ollama 서버 URL", "settings.ollamaUrl": "Ollama 서버 URL",
"settings.ollamaHint": "Ollama가 실행 중이면 자동으로 연결됩니다. 모델은 Ollama에서 직접 pull하세요 (예: ollama pull qwen3:4b).", "settings.ollamaHint": "Ollama가 실행 중이면 자동으로 연결됩니다. 모델은 Ollama에서 직접 pull하세요 (예: ollama pull qwen3:4b).",
@ -132,7 +120,6 @@
"settings.action.grammar": "문법 교정", "settings.action.grammar": "문법 교정",
"settings.action.custom": "커스텀 프롬프트", "settings.action.custom": "커스텀 프롬프트",
"settings.actionHint": "핫키로 녹음 후 전사된 텍스트에 선택한 LLM 후처리가 적용됩니다. Ollama가 연결되어 있을 때만 동작합니다.", "settings.actionHint": "핫키로 녹음 후 전사된 텍스트에 선택한 LLM 후처리가 적용됩니다. Ollama가 연결되어 있을 때만 동작합니다.",
"settings.about.version": "버전", "settings.about.version": "버전",
"settings.about.techStack": "기술 스택", "settings.about.techStack": "기술 스택",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -140,13 +127,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (로컬) / LLM: Ollama (로컬)", "settings.about.voiceEngineValue": "STT: faster-whisper (로컬) / LLM: Ollama (로컬)",
"settings.about.description": "Speakly 리버스엔지니어링 노하우 기반 로컬 AI 음성 어시스턴트. 클라우드 의존성 없이 완전 로컬로 동작합니다.", "settings.about.description": "Speakly 리버스엔지니어링 노하우 기반 로컬 AI 음성 어시스턴트. 클라우드 의존성 없이 완전 로컬로 동작합니다.",
"settings.about.restartOnboarding": "초기 설정 안내 다시 보기", "settings.about.restartOnboarding": "초기 설정 안내 다시 보기",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama가 실행되지 않고 있어요", "status.nudge.title": "Ollama가 실행되지 않고 있어요",
"status.nudge.desc": "LLM 후처리(번역, 요약 등)를 사용하려면 Ollama가 필요합니다.", "status.nudge.desc": "LLM 후처리(번역, 요약 등)를 사용하려면 Ollama가 필요합니다.",
"status.nudge.guide": "설치 안내 보기 →", "status.nudge.guide": "설치 안내 보기 →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "타이핑 없이, 음성으로. 로컬 AI 음성 어시스턴트입니다.", "onboarding.welcome.desc": "타이핑 없이, 음성으로. 로컬 AI 음성 어시스턴트입니다.",
"onboarding.welcome.start": "시작하기", "onboarding.welcome.start": "시작하기",
@ -167,7 +152,6 @@
"onboarding.done.start": "시작하기", "onboarding.done.start": "시작하기",
"onboarding.back": "뒤로", "onboarding.back": "뒤로",
"onboarding.next": "다음", "onboarding.next": "다음",
"hotkey.title": "단축키 설정", "hotkey.title": "단축키 설정",
"hotkey.dictationTitle": "받아쓰기 단축키 설정", "hotkey.dictationTitle": "받아쓰기 단축키 설정",
"hotkey.oneTouchTitle": "원터치 모드 단축키 설정", "hotkey.oneTouchTitle": "원터치 모드 단축키 설정",
@ -179,7 +163,6 @@
"hotkey.current": "현재: {{keys}}", "hotkey.current": "현재: {{keys}}",
"hotkey.hint": "조합키(예: Ctrl+Shift+Q) 또는 단일키(예: F5)를 입력하세요", "hotkey.hint": "조합키(예: Ctrl+Shift+Q) 또는 단일키(예: F5)를 입력하세요",
"hotkey.reset": "다시 입력", "hotkey.reset": "다시 입력",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "STEP 1 — Ollama 설치", "ollama.step1.title": "STEP 1 — Ollama 설치",
"ollama.step1.desc": "Ollama는 로컬에서 LLM을 실행하는 무료 도구입니다.", "ollama.step1.desc": "Ollama는 로컬에서 LLM을 실행하는 무료 도구입니다.",
@ -188,7 +171,6 @@
"ollama.step2.alt": "또는 더 큰 모델: ollama pull qwen3:8b (더 정확, 더 느림)", "ollama.step2.alt": "또는 더 큰 모델: ollama pull qwen3:8b (더 정확, 더 느림)",
"ollama.step3.title": "STEP 3 — 자동 연결", "ollama.step3.title": "STEP 3 — 자동 연결",
"ollama.step3.desc": "Ollama가 실행되면 D3RO-VOICE가 자동으로 감지합니다. 하단 상태 바의 LED가 빨간색에서 초록색으로 바뀌면 준비 완료!", "ollama.step3.desc": "Ollama가 실행되면 D3RO-VOICE가 자동으로 감지합니다. 하단 상태 바의 LED가 빨간색에서 초록색으로 바뀌면 준비 완료!",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -198,7 +180,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "취소", "common.cancel": "취소",
"common.save": "저장", "common.save": "저장",
"common.delete": "삭제", "common.delete": "삭제",
@ -210,7 +191,6 @@
"common.copy": "복사", "common.copy": "복사",
"common.test": "테스트", "common.test": "테스트",
"common.stop": "중지", "common.stop": "중지",
"memo.tags": "태그", "memo.tags": "태그",
"memo.addTag": "태그 추가", "memo.addTag": "태그 추가",
"memo.removeTag": "태그 제거", "memo.removeTag": "태그 제거",
@ -221,7 +201,6 @@
"memo.filterByTag": "태그로 필터", "memo.filterByTag": "태그로 필터",
"memo.allTags": "전체 태그", "memo.allTags": "전체 태그",
"memo.clearFilter": "필터 해제", "memo.clearFilter": "필터 해제",
"voiceCommand.title": "음성 명령어", "voiceCommand.title": "음성 명령어",
"voiceCommand.enabled": "음성 키워드 인식", "voiceCommand.enabled": "음성 키워드 인식",
"voiceCommand.enabledDesc": "전사 시작 시 키워드를 감지하여 명령어를 자동 선택합니다.", "voiceCommand.enabledDesc": "전사 시작 시 키워드를 감지하여 명령어를 자동 선택합니다.",
@ -232,7 +211,6 @@
"voiceCommand.matchMode.prefix": "앞부분 일치", "voiceCommand.matchMode.prefix": "앞부분 일치",
"voiceCommand.matchMode.suffix": "뒷부분 일치", "voiceCommand.matchMode.suffix": "뒷부분 일치",
"voiceCommand.matchMode.contains": "포함", "voiceCommand.matchMode.contains": "포함",
"chain.title": "LLM 체인", "chain.title": "LLM 체인",
"chain.count": "{{count}}개", "chain.count": "{{count}}개",
"chain.add": "체인 추가", "chain.add": "체인 추가",
@ -251,24 +229,19 @@
"chain.noChains": "체인 없음 — 여러 명령어를 순차 실행하는 파이프라인을 만들어 보세요", "chain.noChains": "체인 없음 — 여러 명령어를 순차 실행하는 파이프라인을 만들어 보세요",
"chain.step": "{{n}}단계", "chain.step": "{{n}}단계",
"chain.progress": "{{current}}/{{total}} 단계 처리 중", "chain.progress": "{{current}}/{{total}} 단계 처리 중",
"context.title": "화면 컨텍스트", "context.title": "화면 컨텍스트",
"context.enabled": "스크린 컨텍스트", "context.enabled": "스크린 컨텍스트",
"context.enabledDesc": "녹음 시작 시 활성 앱과 선택된 텍스트를 캡처하여 LLM에 전달합니다.", "context.enabledDesc": "녹음 시작 시 활성 앱과 선택된 텍스트를 캡처하여 LLM에 전달합니다.",
"dashboard.caption": "실시간 자막", "dashboard.caption": "실시간 자막",
"dashboard.captionStart": "자막 시작", "dashboard.captionStart": "자막 시작",
"dashboard.captionStop": "자막 종료", "dashboard.captionStop": "자막 종료",
"dashboard.captionActive": "자막 활성", "dashboard.captionActive": "자막 활성",
"settings.voiceCommands": "음성 명령어", "settings.voiceCommands": "음성 명령어",
"settings.voiceCommands.desc": "전사된 텍스트에서 키워드를 감지하여 명령어를 자동 선택합니다.", "settings.voiceCommands.desc": "전사된 텍스트에서 키워드를 감지하여 명령어를 자동 선택합니다.",
"settings.screenContext": "화면 컨텍스트", "settings.screenContext": "화면 컨텍스트",
"settings.screenContext.desc": "녹음 시 활성 앱 정보와 선택된 텍스트를 LLM에 함께 전달합니다.", "settings.screenContext.desc": "녹음 시 활성 앱 정보와 선택된 텍스트를 LLM에 함께 전달합니다.",
"date.today": "오늘", "date.today": "오늘",
"date.yesterday": "어제", "date.yesterday": "어제",
"license.title": "라이선스", "license.title": "라이선스",
"license.currentTier": "현재 등급", "license.currentTier": "현재 등급",
"license.free": "FREE", "license.free": "FREE",
@ -343,7 +316,6 @@
"license.unlimited": "무제한", "license.unlimited": "무제한",
"license.locked": "잠금", "license.locked": "잠금",
"license.nav": "라이선스", "license.nav": "라이선스",
"fileTranscription.title": "파일 전사", "fileTranscription.title": "파일 전사",
"fileTranscription.dropZone": "오디오/비디오 파일을 여기에 드래그하세요", "fileTranscription.dropZone": "오디오/비디오 파일을 여기에 드래그하세요",
"fileTranscription.dropZoneHint": "MP3, WAV, M4A, MP4, MKV, WEBM 지원", "fileTranscription.dropZoneHint": "MP3, WAV, M4A, MP4, MKV, WEBM 지원",
@ -356,7 +328,6 @@
"fileTranscription.retry": "다시 시도", "fileTranscription.retry": "다시 시도",
"fileTranscription.error.invalidFormat": "지원하지 않는 파일 형식입니다", "fileTranscription.error.invalidFormat": "지원하지 않는 파일 형식입니다",
"fileTranscription.error.unknown": "알 수 없는 오류", "fileTranscription.error.unknown": "알 수 없는 오류",
"meetingSummary.title": "회의록 요약", "meetingSummary.title": "회의록 요약",
"meetingSummary.generating": "요약 생성 중...", "meetingSummary.generating": "요약 생성 중...",
"meetingSummary.summary": "요약", "meetingSummary.summary": "요약",
@ -365,7 +336,6 @@
"meetingSummary.exportMarkdown": "마크다운 내보내기", "meetingSummary.exportMarkdown": "마크다운 내보내기",
"meetingSummary.noSummary": "요약이 없습니다", "meetingSummary.noSummary": "요약이 없습니다",
"meetingSummary.generate": "요약 생성", "meetingSummary.generate": "요약 생성",
"template.title": "딕테이션 템플릿", "template.title": "딕테이션 템플릿",
"template.create": "새 템플릿", "template.create": "새 템플릿",
"template.edit": "템플릿 편집", "template.edit": "템플릿 편집",
@ -381,7 +351,6 @@
"template.startSession": "시작", "template.startSession": "시작",
"template.cancelSession": "세션 취소", "template.cancelSession": "세션 취소",
"template.empty": "템플릿이 없습니다", "template.empty": "템플릿이 없습니다",
"nav.conversation": "대화", "nav.conversation": "대화",
"conversation.title": "음성 대화", "conversation.title": "음성 대화",
"conversation.idle": "대기", "conversation.idle": "대기",
@ -393,7 +362,6 @@
"conversation.inputPlaceholder": "메시지 입력...", "conversation.inputPlaceholder": "메시지 입력...",
"conversation.end": "종료", "conversation.end": "종료",
"conversation.clearHistory": "대화 초기화", "conversation.clearHistory": "대화 초기화",
"nav.knowledge": "지식 베이스", "nav.knowledge": "지식 베이스",
"rag.title": "지식 베이스", "rag.title": "지식 베이스",
"rag.addDocument": "문서 추가", "rag.addDocument": "문서 추가",
@ -410,12 +378,56 @@
"rag.sources": "참조 문서", "rag.sources": "참조 문서",
"rag.adding": "추가 중...", "rag.adding": "추가 중...",
"rag.parsing": "문서 분석 중...", "rag.parsing": "문서 분석 중...",
"voiceAction.title": "음성 액션", "voiceAction.title": "음성 액션",
"voiceAction.execute": "실행", "voiceAction.execute": "실행",
"voiceAction.presets": "프리셋 명령", "voiceAction.presets": "프리셋 명령",
"voiceAction.history": "실행 이력", "voiceAction.history": "실행 이력",
"voiceAction.noHistory": "실행 이력이 없습니다", "voiceAction.noHistory": "실행 이력이 없습니다",
"voiceAction.blocked": "차단됨 (위험 명령)", "voiceAction.blocked": "차단됨 (위험 명령)",
"voiceAction.executed": "실행됨" "voiceAction.executed": "실행됨",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "한국어",
"settings.sttLang.en": "English",
"settings.sttLang.ja": "日本語",
"settings.sttLang.zh": "中文",
"template.outputHelperText": "{{필드명}} 형식으로 플레이스홀더를 사용하세요",
"nav.meeting": "회의",
"meeting.title": "회의 모드",
"meeting.newMeeting": "새 회의",
"meeting.startRecording": "녹음 시작",
"meeting.stopRecording": "녹음 종료",
"meeting.recording": "녹음 중",
"meeting.processing": "회의록 생성 중...",
"meeting.completed": "완료",
"meeting.error": "오류",
"meeting.elapsed": "경과 시간",
"meeting.memoPlaceholder": "메모 입력 (Enter로 추가)",
"meeting.memoAdded": "메모 추가됨",
"meeting.noSessions": "회의 녹음이 없습니다",
"meeting.noSessionsDesc": "새 회의 버튼을 눌러 녹음을 시작하세요",
"meeting.summary": "요약",
"meeting.decisions": "결정사항",
"meeting.actionItems": "할 일",
"meeting.timeline": "타임라인",
"meeting.transcript": "원문 전사",
"meeting.memos": "메모",
"meeting.exportPdf": "PDF 내보내기",
"meeting.exportMarkdown": "마크다운 내보내기",
"meeting.delete": "삭제",
"meeting.deleteConfirm": "이 회의록을 삭제하시겠습니까?",
"meeting.backToList": "목록으로",
"meeting.notificationTitle": "회의록 준비 완료",
"meeting.notificationBody": "회의록이 생성되었습니다. 클릭하여 확인하세요.",
"meeting.sessions": "{{count}}건",
"meeting.duration": "{{minutes}}분",
"meeting.editTitle": "제목 편집",
"meeting.untitled": "무제 회의",
"meeting.processingStep.merging": "전사 텍스트 합산 중...",
"meeting.processingStep.generating": "LLM 회의록 생성 중...",
"meeting.processingStep.parsing": "회의록 파싱 중...",
"meeting.processingStep.saving": "저장 중...",
"meeting.processingStep.notifying": "완료 알림 중..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "Assistente de voz com IA local", "app.tagline": "Assistente de voz com IA local",
"nav.dashboard": "Painel", "nav.dashboard": "Painel",
"nav.history": "Histórico", "nav.history": "Histórico",
"nav.dictionary": "Dicionário", "nav.dictionary": "Dicionário",
"nav.commands": "Comandos", "nav.commands": "Comandos",
"nav.settings": "Configurações", "nav.settings": "Configurações",
"dashboard.sessionOverview": "Visão geral da sessão", "dashboard.sessionOverview": "Visão geral da sessão",
"dashboard.systemStatus": "Status do sistema", "dashboard.systemStatus": "Status do sistema",
"dashboard.sessionsToday": "Sessões hoje", "dashboard.sessionsToday": "Sessões hoje",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} entradas", "dashboard.entries": "{{count}} entradas",
"dashboard.stat": "estatísticas", "dashboard.stat": "estatísticas",
"dashboard.sys": "sistema", "dashboard.sys": "sistema",
"history.title": "Histórico de transcrições", "history.title": "Histórico de transcrições",
"history.search": "Pesquisar...", "history.search": "Pesquisar...",
"history.entries": "{{count}} entradas", "history.entries": "{{count}} entradas",
@ -34,7 +31,6 @@
"history.noResults": "Sem resultados", "history.noResults": "Sem resultados",
"history.noHistory": "Sem histórico — comece a gravar", "history.noHistory": "Sem histórico — comece a gravar",
"history.count": "{{count}} entradas", "history.count": "{{count}} entradas",
"dictionary.title": "Dicionário personalizado", "dictionary.title": "Dicionário personalizado",
"dictionary.words": "{{count}} palavras", "dictionary.words": "{{count}} palavras",
"dictionary.add": "Adicionar", "dictionary.add": "Adicionar",
@ -47,7 +43,6 @@
"dictionary.addTitle": "Adicionar palavra", "dictionary.addTitle": "Adicionar palavra",
"dictionary.word": "Palavra", "dictionary.word": "Palavra",
"dictionary.pronunciation": "Pronúncia (opcional)", "dictionary.pronunciation": "Pronúncia (opcional)",
"commands.title": "Comandos LLM", "commands.title": "Comandos LLM",
"commands.count": "{{count}} comandos", "commands.count": "{{count}} comandos",
"commands.add": "Adicionar", "commands.add": "Adicionar",
@ -62,14 +57,12 @@
"commands.promptTemplate": "Modelo de prompt", "commands.promptTemplate": "Modelo de prompt",
"commands.promptHelp": "{{text}} será substituído pelo texto transcrito", "commands.promptHelp": "{{text}} será substituído pelo texto transcrito",
"commands.defaultPrompt": "Por favor, melhore {{text}}.", "commands.defaultPrompt": "Por favor, melhore {{text}}.",
"settings.title": "Configurações", "settings.title": "Configurações",
"settings.tabs.general": "Geral", "settings.tabs.general": "Geral",
"settings.tabs.audio": "Áudio", "settings.tabs.audio": "Áudio",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "Sobre", "settings.tabs.about": "Sobre",
"settings.shortcuts": "Atalhos", "settings.shortcuts": "Atalhos",
"settings.dictation": "Ditado", "settings.dictation": "Ditado",
"settings.dictation.desc": "Mantenha pressionado para falar. A transcrição começa ao soltar.", "settings.dictation.desc": "Mantenha pressionado para falar. A transcrição começa ao soltar.",
@ -82,20 +75,17 @@
"settings.notSet": "Não configurado", "settings.notSet": "Não configurado",
"settings.enabled": "Ativado", "settings.enabled": "Ativado",
"settings.disabled": "Desativado", "settings.disabled": "Desativado",
"settings.interface": "Interface", "settings.interface": "Interface",
"settings.theme": "Tema", "settings.theme": "Tema",
"settings.theme.system": "Sistema", "settings.theme.system": "Sistema",
"settings.theme.light": "Claro", "settings.theme.light": "Claro",
"settings.theme.dark": "Escuro", "settings.theme.dark": "Escuro",
"settings.language": "Idioma", "settings.language": "Idioma",
"settings.appBehavior": "Comportamento do aplicativo", "settings.appBehavior": "Comportamento do aplicativo",
"settings.closeToTray": "Minimizar para a bandeja", "settings.closeToTray": "Minimizar para a bandeja",
"settings.autoLaunch": "Iniciar com o sistema", "settings.autoLaunch": "Iniciar com o sistema",
"settings.autoInsert": "Inserir texto automaticamente após a transcrição", "settings.autoInsert": "Inserir texto automaticamente após a transcrição",
"settings.soundEffects": "Efeitos sonoros", "settings.soundEffects": "Efeitos sonoros",
"settings.microphone": "Microfone", "settings.microphone": "Microfone",
"settings.inputDevice": "Dispositivo de entrada", "settings.inputDevice": "Dispositivo de entrada",
"settings.deviceDefault": "(padrão)", "settings.deviceDefault": "(padrão)",
@ -103,7 +93,6 @@
"settings.insertMethod": "Método de inserção", "settings.insertMethod": "Método de inserção",
"settings.insertClipboard": "Área de transferência (Ctrl+V)", "settings.insertClipboard": "Área de transferência (Ctrl+V)",
"settings.insertKeyboard": "Digitação pelo teclado", "settings.insertKeyboard": "Digitação pelo teclado",
"settings.whisperModel": "Modelo Whisper", "settings.whisperModel": "Modelo Whisper",
"settings.model.tiny": "tiny (39 MB, mais rápido)", "settings.model.tiny": "tiny (39 MB, mais rápido)",
"settings.model.base": "base (74 MB, equilibrado)", "settings.model.base": "base (74 MB, equilibrado)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1,5 GB, melhor)", "settings.model.large": "large-v3 (1,5 GB, melhor)",
"settings.sttLanguage": "Idioma de reconhecimento", "settings.sttLanguage": "Idioma de reconhecimento",
"settings.sttLang.auto": "Detecção automática", "settings.sttLang.auto": "Detecção automática",
"settings.ollamaServer": "Servidor Ollama", "settings.ollamaServer": "Servidor Ollama",
"settings.ollamaUrl": "URL do servidor Ollama", "settings.ollamaUrl": "URL do servidor Ollama",
"settings.ollamaHint": "D3RO Voice conecta-se automaticamente quando o Ollama está em execução. Baixe modelos diretamente pelo Ollama (ex: ollama pull qwen3:4b).", "settings.ollamaHint": "D3RO Voice conecta-se automaticamente quando o Ollama está em execução. Baixe modelos diretamente pelo Ollama (ex: ollama pull qwen3:4b).",
@ -125,7 +113,6 @@
"settings.action.grammar": "Correção gramatical", "settings.action.grammar": "Correção gramatical",
"settings.action.custom": "Prompt personalizado", "settings.action.custom": "Prompt personalizado",
"settings.actionHint": "O pós-processamento LLM selecionado é aplicado ao texto transcrito após gravar com o atalho. Só funciona quando o Ollama está conectado.", "settings.actionHint": "O pós-processamento LLM selecionado é aplicado ao texto transcrito após gravar com o atalho. Só funciona quando o Ollama está conectado.",
"settings.about.version": "Versão", "settings.about.version": "Versão",
"settings.about.techStack": "Stack tecnológico", "settings.about.techStack": "Stack tecnológico",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (local) / LLM: Ollama (local)", "settings.about.voiceEngineValue": "STT: faster-whisper (local) / LLM: Ollama (local)",
"settings.about.description": "Assistente de voz com IA totalmente local, baseado em engenharia reversa do Speakly. Funciona sem dependências de nuvem.", "settings.about.description": "Assistente de voz com IA totalmente local, baseado em engenharia reversa do Speakly. Funciona sem dependências de nuvem.",
"settings.about.restartOnboarding": "Ver guia de configuração inicial novamente", "settings.about.restartOnboarding": "Ver guia de configuração inicial novamente",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama não está em execução", "status.nudge.title": "Ollama não está em execução",
"status.nudge.desc": "O Ollama é necessário para o pós-processamento LLM (tradução, resumo, etc.).", "status.nudge.desc": "O Ollama é necessário para o pós-processamento LLM (tradução, resumo, etc.).",
"status.nudge.guide": "Ver guia de instalação →", "status.nudge.guide": "Ver guia de instalação →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "Sem digitar, por voz. Seu assistente de voz com IA local.", "onboarding.welcome.desc": "Sem digitar, por voz. Seu assistente de voz com IA local.",
"onboarding.welcome.start": "Começar", "onboarding.welcome.start": "Começar",
@ -160,7 +145,6 @@
"onboarding.done.start": "Começar", "onboarding.done.start": "Começar",
"onboarding.back": "Voltar", "onboarding.back": "Voltar",
"onboarding.next": "Próximo", "onboarding.next": "Próximo",
"hotkey.title": "Configurar atalho", "hotkey.title": "Configurar atalho",
"hotkey.dictationTitle": "Configurar atalho de ditado", "hotkey.dictationTitle": "Configurar atalho de ditado",
"hotkey.oneTouchTitle": "Configurar atalho do modo de um toque", "hotkey.oneTouchTitle": "Configurar atalho do modo de um toque",
@ -171,7 +155,6 @@
"hotkey.current": "Atual: {{keys}}", "hotkey.current": "Atual: {{keys}}",
"hotkey.hint": "Insira uma combinação (ex: Ctrl+Shift+Q) ou uma tecla única (ex: F5)", "hotkey.hint": "Insira uma combinação (ex: Ctrl+Shift+Q) ou uma tecla única (ex: F5)",
"hotkey.reset": "Inserir novamente", "hotkey.reset": "Inserir novamente",
"ollama.title": "GUIA DE CONFIGURAÇÃO DO OLLAMA", "ollama.title": "GUIA DE CONFIGURAÇÃO DO OLLAMA",
"ollama.step1.title": "PASSO 1 — Instalar o Ollama", "ollama.step1.title": "PASSO 1 — Instalar o Ollama",
"ollama.step1.desc": "O Ollama é uma ferramenta gratuita para executar LLMs localmente.", "ollama.step1.desc": "O Ollama é uma ferramenta gratuita para executar LLMs localmente.",
@ -180,7 +163,6 @@
"ollama.step2.alt": "Ou um modelo maior: ollama pull qwen3:8b (mais preciso, mais lento)", "ollama.step2.alt": "Ou um modelo maior: ollama pull qwen3:8b (mais preciso, mais lento)",
"ollama.step3.title": "PASSO 3 — Conexão automática", "ollama.step3.title": "PASSO 3 — Conexão automática",
"ollama.step3.desc": "D3RO-VOICE detecta automaticamente quando o Ollama está em execução. Quando o LED da barra de status mudar de vermelho para verde, está pronto!", "ollama.step3.desc": "D3RO-VOICE detecta automaticamente quando o Ollama está em execução. Quando o LED da barra de status mudar de vermelho para verde, está pronto!",
"service.sttEngine": "MOTOR STT", "service.sttEngine": "MOTOR STT",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "ATALHO DE TECLADO", "service.hotkeyHook": "ATALHO DE TECLADO",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ATIVO", "service.active": "ATIVO",
"service.standby": "EM ESPERA", "service.standby": "EM ESPERA",
"common.cancel": "Cancelar", "common.cancel": "Cancelar",
"common.save": "Salvar", "common.save": "Salvar",
"common.delete": "Excluir", "common.delete": "Excluir",
@ -202,7 +183,51 @@
"common.copy": "Copiar", "common.copy": "Copiar",
"common.test": "Testar", "common.test": "Testar",
"common.stop": "Parar", "common.stop": "Parar",
"date.today": "Hoje", "date.today": "Hoje",
"date.yesterday": "Ontem" "date.yesterday": "Ontem",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "Coreano",
"settings.sttLang.en": "Inglês",
"settings.sttLang.ja": "Japonês",
"settings.sttLang.zh": "Chinês",
"template.outputHelperText": "Use {{nomeCampo}} para marcadores de posição",
"nav.meeting": "Reunião",
"meeting.title": "Modo Reunião",
"meeting.newMeeting": "Nova Reunião",
"meeting.startRecording": "Iniciar Gravação",
"meeting.stopRecording": "Parar Gravação",
"meeting.recording": "Gravando",
"meeting.processing": "Gerando ata...",
"meeting.completed": "Concluído",
"meeting.error": "Erro",
"meeting.elapsed": "Tempo decorrido",
"meeting.memoPlaceholder": "Digite um memo (Enter para adicionar)",
"meeting.memoAdded": "Memo adicionado",
"meeting.noSessions": "Nenhuma gravação de reunião",
"meeting.noSessionsDesc": "Pressione Nova Reunião para começar a gravar",
"meeting.summary": "Resumo",
"meeting.decisions": "Decisões",
"meeting.actionItems": "Itens de ação",
"meeting.timeline": "Linha do tempo",
"meeting.transcript": "Transcrição bruta",
"meeting.memos": "Memos",
"meeting.exportPdf": "Exportar PDF",
"meeting.exportMarkdown": "Exportar Markdown",
"meeting.delete": "Excluir",
"meeting.deleteConfirm": "Excluir este registro de reunião?",
"meeting.backToList": "Voltar à lista",
"meeting.notificationTitle": "Ata pronta",
"meeting.notificationBody": "A ata da reunião foi gerada.",
"meeting.sessions": "{{count}} sessões",
"meeting.duration": "{{minutes}} min",
"meeting.editTitle": "Editar título",
"meeting.untitled": "Reunião sem título",
"meeting.processingStep.merging": "Mesclando transcrição...",
"meeting.processingStep.generating": "Gerando ata com LLM...",
"meeting.processingStep.parsing": "Analisando ata...",
"meeting.processingStep.saving": "Salvando...",
"meeting.processingStep.notifying": "Enviando notificação..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "Локальный голосовой ИИ-ассистент", "app.tagline": "Локальный голосовой ИИ-ассистент",
"nav.dashboard": "Панель", "nav.dashboard": "Панель",
"nav.history": "История", "nav.history": "История",
"nav.dictionary": "Словарь", "nav.dictionary": "Словарь",
"nav.commands": "Команды", "nav.commands": "Команды",
"nav.settings": "Настройки", "nav.settings": "Настройки",
"dashboard.sessionOverview": "Обзор сессий", "dashboard.sessionOverview": "Обзор сессий",
"dashboard.systemStatus": "Состояние системы", "dashboard.systemStatus": "Состояние системы",
"dashboard.sessionsToday": "Сессий сегодня", "dashboard.sessionsToday": "Сессий сегодня",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} записей", "dashboard.entries": "{{count}} записей",
"dashboard.stat": "Статистика", "dashboard.stat": "Статистика",
"dashboard.sys": "Система", "dashboard.sys": "Система",
"history.title": "История транскрипций", "history.title": "История транскрипций",
"history.search": "Поиск...", "history.search": "Поиск...",
"history.entries": "{{count}} записей", "history.entries": "{{count}} записей",
@ -34,7 +31,6 @@
"history.noResults": "Результаты не найдены", "history.noResults": "Результаты не найдены",
"history.noHistory": "История пуста — начните запись", "history.noHistory": "История пуста — начните запись",
"history.count": "{{count}} записей", "history.count": "{{count}} записей",
"dictionary.title": "Пользовательский словарь", "dictionary.title": "Пользовательский словарь",
"dictionary.words": "{{count}} слов", "dictionary.words": "{{count}} слов",
"dictionary.add": "Добавить", "dictionary.add": "Добавить",
@ -47,7 +43,6 @@
"dictionary.addTitle": "Добавить слово", "dictionary.addTitle": "Добавить слово",
"dictionary.word": "Слово", "dictionary.word": "Слово",
"dictionary.pronunciation": "Произношение (необязательно)", "dictionary.pronunciation": "Произношение (необязательно)",
"commands.title": "Команды LLM", "commands.title": "Команды LLM",
"commands.count": "{{count}} шт.", "commands.count": "{{count}} шт.",
"commands.add": "Добавить", "commands.add": "Добавить",
@ -62,14 +57,12 @@
"commands.promptTemplate": "Шаблон промпта", "commands.promptTemplate": "Шаблон промпта",
"commands.promptHelp": "{{text}} будет заменено на транскрибированный текст", "commands.promptHelp": "{{text}} будет заменено на транскрибированный текст",
"commands.defaultPrompt": "Пожалуйста, улучшите {{text}}.", "commands.defaultPrompt": "Пожалуйста, улучшите {{text}}.",
"settings.title": "Настройки", "settings.title": "Настройки",
"settings.tabs.general": "Общие", "settings.tabs.general": "Общие",
"settings.tabs.audio": "Аудио", "settings.tabs.audio": "Аудио",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "О программе", "settings.tabs.about": "О программе",
"settings.shortcuts": "Горячие клавиши", "settings.shortcuts": "Горячие клавиши",
"settings.dictation": "Диктовка", "settings.dictation": "Диктовка",
"settings.dictation.desc": "Говорите, удерживая клавишу. Отпустите для начала транскрипции.", "settings.dictation.desc": "Говорите, удерживая клавишу. Отпустите для начала транскрипции.",
@ -82,20 +75,17 @@
"settings.notSet": "Не задано", "settings.notSet": "Не задано",
"settings.enabled": "Включено", "settings.enabled": "Включено",
"settings.disabled": "Выключено", "settings.disabled": "Выключено",
"settings.interface": "Интерфейс", "settings.interface": "Интерфейс",
"settings.theme": "Тема", "settings.theme": "Тема",
"settings.theme.system": "Системная", "settings.theme.system": "Системная",
"settings.theme.light": "Светлая", "settings.theme.light": "Светлая",
"settings.theme.dark": "Тёмная", "settings.theme.dark": "Тёмная",
"settings.language": "Язык", "settings.language": "Язык",
"settings.appBehavior": "Поведение приложения", "settings.appBehavior": "Поведение приложения",
"settings.closeToTray": "Сворачивать в трей", "settings.closeToTray": "Сворачивать в трей",
"settings.autoLaunch": "Автозапуск при старте системы", "settings.autoLaunch": "Автозапуск при старте системы",
"settings.autoInsert": "Автоматически вставлять текст после транскрипции", "settings.autoInsert": "Автоматически вставлять текст после транскрипции",
"settings.soundEffects": "Звуковые эффекты", "settings.soundEffects": "Звуковые эффекты",
"settings.microphone": "Микрофон", "settings.microphone": "Микрофон",
"settings.inputDevice": "Устройство ввода", "settings.inputDevice": "Устройство ввода",
"settings.deviceDefault": "(По умолчанию)", "settings.deviceDefault": "(По умолчанию)",
@ -103,7 +93,6 @@
"settings.insertMethod": "Метод вставки", "settings.insertMethod": "Метод вставки",
"settings.insertClipboard": "Буфер обмена (Ctrl+V)", "settings.insertClipboard": "Буфер обмена (Ctrl+V)",
"settings.insertKeyboard": "Эмуляция клавиатуры", "settings.insertKeyboard": "Эмуляция клавиатуры",
"settings.whisperModel": "Модель Whisper", "settings.whisperModel": "Модель Whisper",
"settings.model.tiny": "tiny (39 МБ, самая быстрая)", "settings.model.tiny": "tiny (39 МБ, самая быстрая)",
"settings.model.base": "base (74 МБ, баланс)", "settings.model.base": "base (74 МБ, баланс)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1.5 ГБ, лучшая)", "settings.model.large": "large-v3 (1.5 ГБ, лучшая)",
"settings.sttLanguage": "Язык распознавания", "settings.sttLanguage": "Язык распознавания",
"settings.sttLang.auto": "Автоопределение", "settings.sttLang.auto": "Автоопределение",
"settings.ollamaServer": "Сервер Ollama", "settings.ollamaServer": "Сервер Ollama",
"settings.ollamaUrl": "URL сервера Ollama", "settings.ollamaUrl": "URL сервера Ollama",
"settings.ollamaHint": "Если Ollama запущена, подключение произойдёт автоматически. Загружайте модели напрямую через Ollama (например: ollama pull qwen3:4b).", "settings.ollamaHint": "Если Ollama запущена, подключение произойдёт автоматически. Загружайте модели напрямую через Ollama (например: ollama pull qwen3:4b).",
@ -125,7 +113,6 @@
"settings.action.grammar": "Исправить грамматику", "settings.action.grammar": "Исправить грамматику",
"settings.action.custom": "Пользовательский промпт", "settings.action.custom": "Пользовательский промпт",
"settings.actionHint": "После записи и транскрипции к тексту будет применена выбранная постобработка LLM. Работает только при подключённом Ollama.", "settings.actionHint": "После записи и транскрипции к тексту будет применена выбранная постобработка LLM. Работает только при подключённом Ollama.",
"settings.about.version": "Версия", "settings.about.version": "Версия",
"settings.about.techStack": "Технический стек", "settings.about.techStack": "Технический стек",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (локально) / LLM: Ollama (локально)", "settings.about.voiceEngineValue": "STT: faster-whisper (локально) / LLM: Ollama (локально)",
"settings.about.description": "Локальный голосовой ИИ-ассистент, основанный на методах обратной разработки Speakly. Работает полностью локально без зависимости от облачных сервисов.", "settings.about.description": "Локальный голосовой ИИ-ассистент, основанный на методах обратной разработки Speakly. Работает полностью локально без зависимости от облачных сервисов.",
"settings.about.restartOnboarding": "Показать начальную настройку снова", "settings.about.restartOnboarding": "Показать начальную настройку снова",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama не запущена", "status.nudge.title": "Ollama не запущена",
"status.nudge.desc": "Для постобработки LLM (перевод, резюмирование и т.д.) требуется Ollama.", "status.nudge.desc": "Для постобработки LLM (перевод, резюмирование и т.д.) требуется Ollama.",
"status.nudge.guide": "Смотреть инструкцию по установке →", "status.nudge.guide": "Смотреть инструкцию по установке →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "Без набора текста — только голос. Локальный голосовой ИИ-ассистент.", "onboarding.welcome.desc": "Без набора текста — только голос. Локальный голосовой ИИ-ассистент.",
"onboarding.welcome.start": "Начать", "onboarding.welcome.start": "Начать",
@ -160,7 +145,6 @@
"onboarding.done.start": "Начать", "onboarding.done.start": "Начать",
"onboarding.back": "Назад", "onboarding.back": "Назад",
"onboarding.next": "Далее", "onboarding.next": "Далее",
"hotkey.title": "Настройка горячей клавиши", "hotkey.title": "Настройка горячей клавиши",
"hotkey.dictationTitle": "Задать горячую клавишу диктовки", "hotkey.dictationTitle": "Задать горячую клавишу диктовки",
"hotkey.oneTouchTitle": "Задать горячую клавишу режима одного нажатия", "hotkey.oneTouchTitle": "Задать горячую клавишу режима одного нажатия",
@ -171,7 +155,6 @@
"hotkey.current": "Текущее: {{keys}}", "hotkey.current": "Текущее: {{keys}}",
"hotkey.hint": "Введите сочетание клавиш (например: Ctrl+Shift+Q) или одну клавишу (например: F5)", "hotkey.hint": "Введите сочетание клавиш (например: Ctrl+Shift+Q) или одну клавишу (например: F5)",
"hotkey.reset": "Ввести снова", "hotkey.reset": "Ввести снова",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "ШАГ 1 — Установка Ollama", "ollama.step1.title": "ШАГ 1 — Установка Ollama",
"ollama.step1.desc": "Ollama — бесплатный инструмент для локального запуска LLM.", "ollama.step1.desc": "Ollama — бесплатный инструмент для локального запуска LLM.",
@ -180,7 +163,6 @@
"ollama.step2.alt": "Или более крупная модель: ollama pull qwen3:8b (точнее, медленнее)", "ollama.step2.alt": "Или более крупная модель: ollama pull qwen3:8b (точнее, медленнее)",
"ollama.step3.title": "ШАГ 3 — Автоподключение", "ollama.step3.title": "ШАГ 3 — Автоподключение",
"ollama.step3.desc": "При запуске Ollama D3RO-VOICE обнаружит её автоматически. Когда индикатор LED на нижней панели сменится с красного на зелёный — готово!", "ollama.step3.desc": "При запуске Ollama D3RO-VOICE обнаружит её автоматически. Когда индикатор LED на нижней панели сменится с красного на зелёный — готово!",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "Отмена", "common.cancel": "Отмена",
"common.save": "Сохранить", "common.save": "Сохранить",
"common.delete": "Удалить", "common.delete": "Удалить",
@ -202,7 +183,51 @@
"common.copy": "Копировать", "common.copy": "Копировать",
"common.test": "Тест", "common.test": "Тест",
"common.stop": "Стоп", "common.stop": "Стоп",
"date.today": "Сегодня", "date.today": "Сегодня",
"date.yesterday": "Вчера" "date.yesterday": "Вчера",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "Корейский",
"settings.sttLang.en": "Английский",
"settings.sttLang.ja": "Японский",
"settings.sttLang.zh": "Китайский",
"template.outputHelperText": "Используйте {{имяПоля}} для заполнителей",
"nav.meeting": "Совещание",
"meeting.title": "Режим совещания",
"meeting.newMeeting": "Новое совещание",
"meeting.startRecording": "Начать запись",
"meeting.stopRecording": "Остановить запись",
"meeting.recording": "Запись",
"meeting.processing": "Создание протокола...",
"meeting.completed": "Завершено",
"meeting.error": "Ошибка",
"meeting.elapsed": "Прошло",
"meeting.memoPlaceholder": "Введите заметку (Enter для добавления)",
"meeting.memoAdded": "Заметка добавлена",
"meeting.noSessions": "Нет записей совещаний",
"meeting.noSessionsDesc": "Нажмите «Новое совещание», чтобы начать запись",
"meeting.summary": "Сводка",
"meeting.decisions": "Решения",
"meeting.actionItems": "Задачи",
"meeting.timeline": "Хронология",
"meeting.transcript": "Исходная расшифровка",
"meeting.memos": "Заметки",
"meeting.exportPdf": "Экспорт в PDF",
"meeting.exportMarkdown": "Экспорт в Markdown",
"meeting.delete": "Удалить",
"meeting.deleteConfirm": "Удалить эту запись совещания?",
"meeting.backToList": "Вернуться к списку",
"meeting.notificationTitle": "Протокол готов",
"meeting.notificationBody": "Протокол совещания сформирован.",
"meeting.sessions": "{{count}} сессий",
"meeting.duration": "{{minutes}} мин",
"meeting.editTitle": "Изменить название",
"meeting.untitled": "Совещание без названия",
"meeting.processingStep.merging": "Объединение расшифровки...",
"meeting.processingStep.generating": "Создание протокола с помощью LLM...",
"meeting.processingStep.parsing": "Разбор протокола...",
"meeting.processingStep.saving": "Сохранение...",
"meeting.processingStep.notifying": "Отправка уведомления..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "ผู้ช่วยเสียง AI ในเครื่อง", "app.tagline": "ผู้ช่วยเสียง AI ในเครื่อง",
"nav.dashboard": "แดชบอร์ด", "nav.dashboard": "แดชบอร์ด",
"nav.history": "ประวัติ", "nav.history": "ประวัติ",
"nav.dictionary": "พจนานุกรม", "nav.dictionary": "พจนานุกรม",
"nav.commands": "คำสั่ง", "nav.commands": "คำสั่ง",
"nav.settings": "การตั้งค่า", "nav.settings": "การตั้งค่า",
"dashboard.sessionOverview": "ภาพรวมเซสชัน", "dashboard.sessionOverview": "ภาพรวมเซสชัน",
"dashboard.systemStatus": "สถานะระบบ", "dashboard.systemStatus": "สถานะระบบ",
"dashboard.sessionsToday": "เซสชันวันนี้", "dashboard.sessionsToday": "เซสชันวันนี้",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} รายการ", "dashboard.entries": "{{count}} รายการ",
"dashboard.stat": "สถิติ", "dashboard.stat": "สถิติ",
"dashboard.sys": "ระบบ", "dashboard.sys": "ระบบ",
"history.title": "ประวัติการถอดความ", "history.title": "ประวัติการถอดความ",
"history.search": "ค้นหา...", "history.search": "ค้นหา...",
"history.entries": "{{count}} รายการ", "history.entries": "{{count}} รายการ",
@ -34,7 +31,6 @@
"history.noResults": "ไม่พบผลลัพธ์", "history.noResults": "ไม่พบผลลัพธ์",
"history.noHistory": "ไม่มีประวัติ — เริ่มบันทึกเสียง", "history.noHistory": "ไม่มีประวัติ — เริ่มบันทึกเสียง",
"history.count": "{{count}} รายการ", "history.count": "{{count}} รายการ",
"dictionary.title": "พจนานุกรมที่กำหนดเอง", "dictionary.title": "พจนานุกรมที่กำหนดเอง",
"dictionary.words": "{{count}} คำ", "dictionary.words": "{{count}} คำ",
"dictionary.add": "เพิ่ม", "dictionary.add": "เพิ่ม",
@ -47,7 +43,6 @@
"dictionary.addTitle": "เพิ่มคำ", "dictionary.addTitle": "เพิ่มคำ",
"dictionary.word": "คำ", "dictionary.word": "คำ",
"dictionary.pronunciation": "การออกเสียง (ไม่บังคับ)", "dictionary.pronunciation": "การออกเสียง (ไม่บังคับ)",
"commands.title": "คำสั่ง LLM", "commands.title": "คำสั่ง LLM",
"commands.count": "{{count}} รายการ", "commands.count": "{{count}} รายการ",
"commands.add": "เพิ่ม", "commands.add": "เพิ่ม",
@ -62,14 +57,12 @@
"commands.promptTemplate": "เทมเพลตพรอมต์", "commands.promptTemplate": "เทมเพลตพรอมต์",
"commands.promptHelp": "{{text}} จะถูกแทนที่ด้วยข้อความที่ถอดความ", "commands.promptHelp": "{{text}} จะถูกแทนที่ด้วยข้อความที่ถอดความ",
"commands.defaultPrompt": "กรุณาปรับปรุง {{text}}", "commands.defaultPrompt": "กรุณาปรับปรุง {{text}}",
"settings.title": "การตั้งค่า", "settings.title": "การตั้งค่า",
"settings.tabs.general": "ทั่วไป", "settings.tabs.general": "ทั่วไป",
"settings.tabs.audio": "เสียง", "settings.tabs.audio": "เสียง",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "เกี่ยวกับ", "settings.tabs.about": "เกี่ยวกับ",
"settings.shortcuts": "ปุ่มลัด", "settings.shortcuts": "ปุ่มลัด",
"settings.dictation": "การบอกเล่า", "settings.dictation": "การบอกเล่า",
"settings.dictation.desc": "กดค้างไว้แล้วพูด เมื่อปล่อยจะเริ่มถอดความ", "settings.dictation.desc": "กดค้างไว้แล้วพูด เมื่อปล่อยจะเริ่มถอดความ",
@ -82,20 +75,17 @@
"settings.notSet": "ยังไม่ได้ตั้งค่า", "settings.notSet": "ยังไม่ได้ตั้งค่า",
"settings.enabled": "เปิดใช้งาน", "settings.enabled": "เปิดใช้งาน",
"settings.disabled": "ปิดใช้งาน", "settings.disabled": "ปิดใช้งาน",
"settings.interface": "อินเทอร์เฟซ", "settings.interface": "อินเทอร์เฟซ",
"settings.theme": "ธีม", "settings.theme": "ธีม",
"settings.theme.system": "ระบบ", "settings.theme.system": "ระบบ",
"settings.theme.light": "สว่าง", "settings.theme.light": "สว่าง",
"settings.theme.dark": "มืด", "settings.theme.dark": "มืด",
"settings.language": "ภาษา", "settings.language": "ภาษา",
"settings.appBehavior": "พฤติกรรมแอป", "settings.appBehavior": "พฤติกรรมแอป",
"settings.closeToTray": "ย่อลงถาดระบบ", "settings.closeToTray": "ย่อลงถาดระบบ",
"settings.autoLaunch": "เปิดอัตโนมัติเมื่อเริ่มระบบ", "settings.autoLaunch": "เปิดอัตโนมัติเมื่อเริ่มระบบ",
"settings.autoInsert": "แทรกข้อความอัตโนมัติหลังถอดความ", "settings.autoInsert": "แทรกข้อความอัตโนมัติหลังถอดความ",
"settings.soundEffects": "เอฟเฟกต์เสียง", "settings.soundEffects": "เอฟเฟกต์เสียง",
"settings.microphone": "ไมโครโฟน", "settings.microphone": "ไมโครโฟน",
"settings.inputDevice": "อุปกรณ์อินพุต", "settings.inputDevice": "อุปกรณ์อินพุต",
"settings.deviceDefault": "(ค่าเริ่มต้น)", "settings.deviceDefault": "(ค่าเริ่มต้น)",
@ -103,7 +93,6 @@
"settings.insertMethod": "วิธีการแทรก", "settings.insertMethod": "วิธีการแทรก",
"settings.insertClipboard": "คลิปบอร์ด (Ctrl+V)", "settings.insertClipboard": "คลิปบอร์ด (Ctrl+V)",
"settings.insertKeyboard": "จำลองการพิมพ์", "settings.insertKeyboard": "จำลองการพิมพ์",
"settings.whisperModel": "โมเดล Whisper", "settings.whisperModel": "โมเดล Whisper",
"settings.model.tiny": "tiny (39 MB, เร็วที่สุด)", "settings.model.tiny": "tiny (39 MB, เร็วที่สุด)",
"settings.model.base": "base (74 MB, สมดุล)", "settings.model.base": "base (74 MB, สมดุล)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1.5 GB, ดีที่สุด)", "settings.model.large": "large-v3 (1.5 GB, ดีที่สุด)",
"settings.sttLanguage": "ภาษาที่รู้จัก", "settings.sttLanguage": "ภาษาที่รู้จัก",
"settings.sttLang.auto": "ตรวจจับอัตโนมัติ", "settings.sttLang.auto": "ตรวจจับอัตโนมัติ",
"settings.ollamaServer": "เซิร์ฟเวอร์ Ollama", "settings.ollamaServer": "เซิร์ฟเวอร์ Ollama",
"settings.ollamaUrl": "URL เซิร์ฟเวอร์ Ollama", "settings.ollamaUrl": "URL เซิร์ฟเวอร์ Ollama",
"settings.ollamaHint": "หาก Ollama กำลังทำงาน จะเชื่อมต่ออัตโนมัติ ดาวน์โหลดโมเดลโดยตรงจาก Ollama (เช่น: ollama pull qwen3:4b)", "settings.ollamaHint": "หาก Ollama กำลังทำงาน จะเชื่อมต่ออัตโนมัติ ดาวน์โหลดโมเดลโดยตรงจาก Ollama (เช่น: ollama pull qwen3:4b)",
@ -125,7 +113,6 @@
"settings.action.grammar": "แก้ไขไวยากรณ์", "settings.action.grammar": "แก้ไขไวยากรณ์",
"settings.action.custom": "พรอมต์ที่กำหนดเอง", "settings.action.custom": "พรอมต์ที่กำหนดเอง",
"settings.actionHint": "การประมวลผล LLM ที่เลือกจะถูกนำไปใช้กับข้อความที่ถอดความหลังจากบันทึก ทำงานเฉพาะเมื่อเชื่อมต่อ Ollama", "settings.actionHint": "การประมวลผล LLM ที่เลือกจะถูกนำไปใช้กับข้อความที่ถอดความหลังจากบันทึก ทำงานเฉพาะเมื่อเชื่อมต่อ Ollama",
"settings.about.version": "เวอร์ชัน", "settings.about.version": "เวอร์ชัน",
"settings.about.techStack": "เทคโนโลยีที่ใช้", "settings.about.techStack": "เทคโนโลยีที่ใช้",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (ในเครื่อง) / LLM: Ollama (ในเครื่อง)", "settings.about.voiceEngineValue": "STT: faster-whisper (ในเครื่อง) / LLM: Ollama (ในเครื่อง)",
"settings.about.description": "ผู้ช่วยเสียง AI ในเครื่องที่พัฒนาจากความรู้การวิศวกรรมย้อนกลับ Speakly ทำงานได้อย่างสมบูรณ์ในเครื่องโดยไม่พึ่งพาบริการคลาวด์", "settings.about.description": "ผู้ช่วยเสียง AI ในเครื่องที่พัฒนาจากความรู้การวิศวกรรมย้อนกลับ Speakly ทำงานได้อย่างสมบูรณ์ในเครื่องโดยไม่พึ่งพาบริการคลาวด์",
"settings.about.restartOnboarding": "ดูคู่มือการตั้งค่าเริ่มต้นอีกครั้ง", "settings.about.restartOnboarding": "ดูคู่มือการตั้งค่าเริ่มต้นอีกครั้ง",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama ยังไม่ได้ทำงาน", "status.nudge.title": "Ollama ยังไม่ได้ทำงาน",
"status.nudge.desc": "ต้องใช้ Ollama สำหรับการประมวลผล LLM (แปล สรุป ฯลฯ)", "status.nudge.desc": "ต้องใช้ Ollama สำหรับการประมวลผล LLM (แปล สรุป ฯลฯ)",
"status.nudge.guide": "ดูคำแนะนำการติดตั้ง →", "status.nudge.guide": "ดูคำแนะนำการติดตั้ง →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "ไม่ต้องพิมพ์ — แค่พูด ผู้ช่วยเสียง AI ในเครื่อง", "onboarding.welcome.desc": "ไม่ต้องพิมพ์ — แค่พูด ผู้ช่วยเสียง AI ในเครื่อง",
"onboarding.welcome.start": "เริ่มต้น", "onboarding.welcome.start": "เริ่มต้น",
@ -160,7 +145,6 @@
"onboarding.done.start": "เริ่มต้น", "onboarding.done.start": "เริ่มต้น",
"onboarding.back": "ย้อนกลับ", "onboarding.back": "ย้อนกลับ",
"onboarding.next": "ถัดไป", "onboarding.next": "ถัดไป",
"hotkey.title": "การตั้งค่าปุ่มลัด", "hotkey.title": "การตั้งค่าปุ่มลัด",
"hotkey.dictationTitle": "ตั้งค่าปุ่มลัดการบอกเล่า", "hotkey.dictationTitle": "ตั้งค่าปุ่มลัดการบอกเล่า",
"hotkey.oneTouchTitle": "ตั้งค่าปุ่มลัดโหมดสัมผัสเดียว", "hotkey.oneTouchTitle": "ตั้งค่าปุ่มลัดโหมดสัมผัสเดียว",
@ -171,7 +155,6 @@
"hotkey.current": "ปัจจุบัน: {{keys}}", "hotkey.current": "ปัจจุบัน: {{keys}}",
"hotkey.hint": "กรอกปุ่มลัด (เช่น: Ctrl+Shift+Q) หรือปุ่มเดี่ยว (เช่น: F5)", "hotkey.hint": "กรอกปุ่มลัด (เช่น: Ctrl+Shift+Q) หรือปุ่มเดี่ยว (เช่น: F5)",
"hotkey.reset": "กรอกใหม่", "hotkey.reset": "กรอกใหม่",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "ขั้นตอนที่ 1 — ติดตั้ง Ollama", "ollama.step1.title": "ขั้นตอนที่ 1 — ติดตั้ง Ollama",
"ollama.step1.desc": "Ollama เป็นเครื่องมือฟรีสำหรับรัน LLM ในเครื่อง", "ollama.step1.desc": "Ollama เป็นเครื่องมือฟรีสำหรับรัน LLM ในเครื่อง",
@ -180,7 +163,6 @@
"ollama.step2.alt": "หรือโมเดลที่ใหญ่กว่า: ollama pull qwen3:8b (แม่นยำกว่า ช้ากว่า)", "ollama.step2.alt": "หรือโมเดลที่ใหญ่กว่า: ollama pull qwen3:8b (แม่นยำกว่า ช้ากว่า)",
"ollama.step3.title": "ขั้นตอนที่ 3 — เชื่อมต่ออัตโนมัติ", "ollama.step3.title": "ขั้นตอนที่ 3 — เชื่อมต่ออัตโนมัติ",
"ollama.step3.desc": "เมื่อ Ollama ทำงาน D3RO-VOICE จะตรวจพบโดยอัตโนมัติ เมื่อไฟ LED บนแถบสถานะเปลี่ยนจากสีแดงเป็นสีเขียว — พร้อมใช้งาน!", "ollama.step3.desc": "เมื่อ Ollama ทำงาน D3RO-VOICE จะตรวจพบโดยอัตโนมัติ เมื่อไฟ LED บนแถบสถานะเปลี่ยนจากสีแดงเป็นสีเขียว — พร้อมใช้งาน!",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "ยกเลิก", "common.cancel": "ยกเลิก",
"common.save": "บันทึก", "common.save": "บันทึก",
"common.delete": "ลบ", "common.delete": "ลบ",
@ -202,7 +183,51 @@
"common.copy": "คัดลอก", "common.copy": "คัดลอก",
"common.test": "ทดสอบ", "common.test": "ทดสอบ",
"common.stop": "หยุด", "common.stop": "หยุด",
"date.today": "วันนี้", "date.today": "วันนี้",
"date.yesterday": "เมื่อวาน" "date.yesterday": "เมื่อวาน",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "เกาหลี",
"settings.sttLang.en": "อังกฤษ",
"settings.sttLang.ja": "ญี่ปุ่น",
"settings.sttLang.zh": "จีน",
"template.outputHelperText": "ใช้ {{ชื่อฟิลด์}} สำหรับตัวยึดตำแหน่ง",
"nav.meeting": "การประชุม",
"meeting.title": "โหมดการประชุม",
"meeting.newMeeting": "การประชุมใหม่",
"meeting.startRecording": "เริ่มบันทึก",
"meeting.stopRecording": "หยุดบันทึก",
"meeting.recording": "กำลังบันทึก",
"meeting.processing": "กำลังสร้างรายงานการประชุม...",
"meeting.completed": "เสร็จสิ้น",
"meeting.error": "ข้อผิดพลาด",
"meeting.elapsed": "เวลาที่ผ่านไป",
"meeting.memoPlaceholder": "พิมพ์บันทึก (Enter เพื่อเพิ่ม)",
"meeting.memoAdded": "เพิ่มบันทึกแล้ว",
"meeting.noSessions": "ไม่มีการบันทึกการประชุม",
"meeting.noSessionsDesc": "กด «การประชุมใหม่» เพื่อเริ่มบันทึก",
"meeting.summary": "สรุป",
"meeting.decisions": "การตัดสินใจ",
"meeting.actionItems": "รายการดำเนินการ",
"meeting.timeline": "ไทม์ไลน์",
"meeting.transcript": "ถอดความดิบ",
"meeting.memos": "บันทึก",
"meeting.exportPdf": "ส่งออก PDF",
"meeting.exportMarkdown": "ส่งออก Markdown",
"meeting.delete": "ลบ",
"meeting.deleteConfirm": "ลบรายงานการประชุมนี้?",
"meeting.backToList": "กลับไปยังรายการ",
"meeting.notificationTitle": "รายงานการประชุมพร้อมแล้ว",
"meeting.notificationBody": "สร้างรายงานการประชุมเสร็จแล้ว",
"meeting.sessions": "{{count}} เซสชัน",
"meeting.duration": "{{minutes}} นาที",
"meeting.editTitle": "แก้ไขชื่อ",
"meeting.untitled": "การประชุมที่ไม่มีชื่อ",
"meeting.processingStep.merging": "กำลังรวมการถอดความ...",
"meeting.processingStep.generating": "กำลังสร้างรายงานด้วย LLM...",
"meeting.processingStep.parsing": "กำลังวิเคราะห์รายงาน...",
"meeting.processingStep.saving": "กำลังบันทึก...",
"meeting.processingStep.notifying": "กำลังส่งการแจ้งเตือน..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "Trợ lý giọng nói AI cục bộ", "app.tagline": "Trợ lý giọng nói AI cục bộ",
"nav.dashboard": "Bảng điều khiển", "nav.dashboard": "Bảng điều khiển",
"nav.history": "Lịch sử", "nav.history": "Lịch sử",
"nav.dictionary": "Từ điển", "nav.dictionary": "Từ điển",
"nav.commands": "Lệnh", "nav.commands": "Lệnh",
"nav.settings": "Cài đặt", "nav.settings": "Cài đặt",
"dashboard.sessionOverview": "Tổng quan phiên", "dashboard.sessionOverview": "Tổng quan phiên",
"dashboard.systemStatus": "Trạng thái hệ thống", "dashboard.systemStatus": "Trạng thái hệ thống",
"dashboard.sessionsToday": "Phiên hôm nay", "dashboard.sessionsToday": "Phiên hôm nay",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}} mục", "dashboard.entries": "{{count}} mục",
"dashboard.stat": "Thống kê", "dashboard.stat": "Thống kê",
"dashboard.sys": "Hệ thống", "dashboard.sys": "Hệ thống",
"history.title": "Lịch sử phiên âm", "history.title": "Lịch sử phiên âm",
"history.search": "Tìm kiếm...", "history.search": "Tìm kiếm...",
"history.entries": "{{count}} mục", "history.entries": "{{count}} mục",
@ -34,7 +31,6 @@
"history.noResults": "Không tìm thấy kết quả", "history.noResults": "Không tìm thấy kết quả",
"history.noHistory": "Chưa có lịch sử — hãy bắt đầu ghi âm", "history.noHistory": "Chưa có lịch sử — hãy bắt đầu ghi âm",
"history.count": "{{count}} mục", "history.count": "{{count}} mục",
"dictionary.title": "Từ điển tùy chỉnh", "dictionary.title": "Từ điển tùy chỉnh",
"dictionary.words": "{{count}} từ", "dictionary.words": "{{count}} từ",
"dictionary.add": "Thêm", "dictionary.add": "Thêm",
@ -47,7 +43,6 @@
"dictionary.addTitle": "Thêm từ", "dictionary.addTitle": "Thêm từ",
"dictionary.word": "Từ", "dictionary.word": "Từ",
"dictionary.pronunciation": "Phát âm (tùy chọn)", "dictionary.pronunciation": "Phát âm (tùy chọn)",
"commands.title": "Lệnh LLM", "commands.title": "Lệnh LLM",
"commands.count": "{{count}} lệnh", "commands.count": "{{count}} lệnh",
"commands.add": "Thêm", "commands.add": "Thêm",
@ -62,14 +57,12 @@
"commands.promptTemplate": "Mẫu prompt", "commands.promptTemplate": "Mẫu prompt",
"commands.promptHelp": "{{text}} sẽ được thay thế bằng văn bản phiên âm", "commands.promptHelp": "{{text}} sẽ được thay thế bằng văn bản phiên âm",
"commands.defaultPrompt": "Vui lòng cải thiện {{text}}.", "commands.defaultPrompt": "Vui lòng cải thiện {{text}}.",
"settings.title": "Cài đặt", "settings.title": "Cài đặt",
"settings.tabs.general": "Chung", "settings.tabs.general": "Chung",
"settings.tabs.audio": "Âm thanh", "settings.tabs.audio": "Âm thanh",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "Giới thiệu", "settings.tabs.about": "Giới thiệu",
"settings.shortcuts": "Phím tắt", "settings.shortcuts": "Phím tắt",
"settings.dictation": "Chính tả", "settings.dictation": "Chính tả",
"settings.dictation.desc": "Giữ phím và nói. Thả phím để bắt đầu phiên âm.", "settings.dictation.desc": "Giữ phím và nói. Thả phím để bắt đầu phiên âm.",
@ -82,20 +75,17 @@
"settings.notSet": "Chưa đặt", "settings.notSet": "Chưa đặt",
"settings.enabled": "Đã bật", "settings.enabled": "Đã bật",
"settings.disabled": "Đã tắt", "settings.disabled": "Đã tắt",
"settings.interface": "Giao diện", "settings.interface": "Giao diện",
"settings.theme": "Giao diện", "settings.theme": "Giao diện",
"settings.theme.system": "Hệ thống", "settings.theme.system": "Hệ thống",
"settings.theme.light": "Sáng", "settings.theme.light": "Sáng",
"settings.theme.dark": "Tối", "settings.theme.dark": "Tối",
"settings.language": "Ngôn ngữ", "settings.language": "Ngôn ngữ",
"settings.appBehavior": "Hành vi ứng dụng", "settings.appBehavior": "Hành vi ứng dụng",
"settings.closeToTray": "Thu nhỏ xuống khay hệ thống", "settings.closeToTray": "Thu nhỏ xuống khay hệ thống",
"settings.autoLaunch": "Tự động khởi động cùng hệ thống", "settings.autoLaunch": "Tự động khởi động cùng hệ thống",
"settings.autoInsert": "Tự động chèn văn bản sau khi phiên âm", "settings.autoInsert": "Tự động chèn văn bản sau khi phiên âm",
"settings.soundEffects": "Hiệu ứng âm thanh", "settings.soundEffects": "Hiệu ứng âm thanh",
"settings.microphone": "Micrô", "settings.microphone": "Micrô",
"settings.inputDevice": "Thiết bị đầu vào", "settings.inputDevice": "Thiết bị đầu vào",
"settings.deviceDefault": "(Mặc định)", "settings.deviceDefault": "(Mặc định)",
@ -103,7 +93,6 @@
"settings.insertMethod": "Phương thức chèn", "settings.insertMethod": "Phương thức chèn",
"settings.insertClipboard": "Bộ nhớ tạm (Ctrl+V)", "settings.insertClipboard": "Bộ nhớ tạm (Ctrl+V)",
"settings.insertKeyboard": "Giả lập bàn phím", "settings.insertKeyboard": "Giả lập bàn phím",
"settings.whisperModel": "Mô hình Whisper", "settings.whisperModel": "Mô hình Whisper",
"settings.model.tiny": "tiny (39 MB, nhanh nhất)", "settings.model.tiny": "tiny (39 MB, nhanh nhất)",
"settings.model.base": "base (74 MB, cân bằng)", "settings.model.base": "base (74 MB, cân bằng)",
@ -112,7 +101,6 @@
"settings.model.large": "large-v3 (1.5 GB, tốt nhất)", "settings.model.large": "large-v3 (1.5 GB, tốt nhất)",
"settings.sttLanguage": "Ngôn ngữ nhận dạng", "settings.sttLanguage": "Ngôn ngữ nhận dạng",
"settings.sttLang.auto": "Tự động phát hiện", "settings.sttLang.auto": "Tự động phát hiện",
"settings.ollamaServer": "Máy chủ Ollama", "settings.ollamaServer": "Máy chủ Ollama",
"settings.ollamaUrl": "URL máy chủ Ollama", "settings.ollamaUrl": "URL máy chủ Ollama",
"settings.ollamaHint": "Nếu Ollama đang chạy, kết nối sẽ tự động. Tải mô hình trực tiếp từ Ollama (ví dụ: ollama pull qwen3:4b).", "settings.ollamaHint": "Nếu Ollama đang chạy, kết nối sẽ tự động. Tải mô hình trực tiếp từ Ollama (ví dụ: ollama pull qwen3:4b).",
@ -125,7 +113,6 @@
"settings.action.grammar": "Sửa ngữ pháp", "settings.action.grammar": "Sửa ngữ pháp",
"settings.action.custom": "Prompt tùy chỉnh", "settings.action.custom": "Prompt tùy chỉnh",
"settings.actionHint": "Hậu xử lý LLM đã chọn sẽ được áp dụng cho văn bản phiên âm sau khi ghi âm. Chỉ hoạt động khi Ollama được kết nối.", "settings.actionHint": "Hậu xử lý LLM đã chọn sẽ được áp dụng cho văn bản phiên âm sau khi ghi âm. Chỉ hoạt động khi Ollama được kết nối.",
"settings.about.version": "Phiên bản", "settings.about.version": "Phiên bản",
"settings.about.techStack": "Ngăn xếp công nghệ", "settings.about.techStack": "Ngăn xếp công nghệ",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper (cục bộ) / LLM: Ollama (cục bộ)", "settings.about.voiceEngineValue": "STT: faster-whisper (cục bộ) / LLM: Ollama (cục bộ)",
"settings.about.description": "Trợ lý giọng nói AI cục bộ dựa trên kỹ thuật dịch ngược Speakly. Hoạt động hoàn toàn cục bộ, không phụ thuộc vào dịch vụ đám mây.", "settings.about.description": "Trợ lý giọng nói AI cục bộ dựa trên kỹ thuật dịch ngược Speakly. Hoạt động hoàn toàn cục bộ, không phụ thuộc vào dịch vụ đám mây.",
"settings.about.restartOnboarding": "Xem lại hướng dẫn cài đặt ban đầu", "settings.about.restartOnboarding": "Xem lại hướng dẫn cài đặt ban đầu",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama chưa chạy", "status.nudge.title": "Ollama chưa chạy",
"status.nudge.desc": "Cần Ollama để sử dụng hậu xử lý LLM (dịch, tóm tắt, v.v.).", "status.nudge.desc": "Cần Ollama để sử dụng hậu xử lý LLM (dịch, tóm tắt, v.v.).",
"status.nudge.guide": "Xem hướng dẫn cài đặt →", "status.nudge.guide": "Xem hướng dẫn cài đặt →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "Không cần gõ phím — chỉ cần nói. Trợ lý giọng nói AI cục bộ.", "onboarding.welcome.desc": "Không cần gõ phím — chỉ cần nói. Trợ lý giọng nói AI cục bộ.",
"onboarding.welcome.start": "Bắt đầu", "onboarding.welcome.start": "Bắt đầu",
@ -160,7 +145,6 @@
"onboarding.done.start": "Bắt đầu", "onboarding.done.start": "Bắt đầu",
"onboarding.back": "Quay lại", "onboarding.back": "Quay lại",
"onboarding.next": "Tiếp theo", "onboarding.next": "Tiếp theo",
"hotkey.title": "Cài đặt phím tắt", "hotkey.title": "Cài đặt phím tắt",
"hotkey.dictationTitle": "Đặt phím tắt chính tả", "hotkey.dictationTitle": "Đặt phím tắt chính tả",
"hotkey.oneTouchTitle": "Đặt phím tắt chế độ một chạm", "hotkey.oneTouchTitle": "Đặt phím tắt chế độ một chạm",
@ -171,7 +155,6 @@
"hotkey.current": "Hiện tại: {{keys}}", "hotkey.current": "Hiện tại: {{keys}}",
"hotkey.hint": "Nhập tổ hợp phím (ví dụ: Ctrl+Shift+Q) hoặc phím đơn (ví dụ: F5)", "hotkey.hint": "Nhập tổ hợp phím (ví dụ: Ctrl+Shift+Q) hoặc phím đơn (ví dụ: F5)",
"hotkey.reset": "Nhập lại", "hotkey.reset": "Nhập lại",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "BƯỚC 1 — Cài đặt Ollama", "ollama.step1.title": "BƯỚC 1 — Cài đặt Ollama",
"ollama.step1.desc": "Ollama là công cụ miễn phí để chạy LLM cục bộ.", "ollama.step1.desc": "Ollama là công cụ miễn phí để chạy LLM cục bộ.",
@ -180,7 +163,6 @@
"ollama.step2.alt": "Hoặc mô hình lớn hơn: ollama pull qwen3:8b (chính xác hơn, chậm hơn)", "ollama.step2.alt": "Hoặc mô hình lớn hơn: ollama pull qwen3:8b (chính xác hơn, chậm hơn)",
"ollama.step3.title": "BƯỚC 3 — Tự động kết nối", "ollama.step3.title": "BƯỚC 3 — Tự động kết nối",
"ollama.step3.desc": "Khi Ollama chạy, D3RO-VOICE sẽ tự động phát hiện. Khi đèn LED trên thanh trạng thái chuyển từ đỏ sang xanh — đã sẵn sàng!", "ollama.step3.desc": "Khi Ollama chạy, D3RO-VOICE sẽ tự động phát hiện. Khi đèn LED trên thanh trạng thái chuyển từ đỏ sang xanh — đã sẵn sàng!",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "Hủy", "common.cancel": "Hủy",
"common.save": "Lưu", "common.save": "Lưu",
"common.delete": "Xóa", "common.delete": "Xóa",
@ -202,7 +183,51 @@
"common.copy": "Sao chép", "common.copy": "Sao chép",
"common.test": "Kiểm tra", "common.test": "Kiểm tra",
"common.stop": "Dừng", "common.stop": "Dừng",
"date.today": "Hôm nay", "date.today": "Hôm nay",
"date.yesterday": "Hôm qua" "date.yesterday": "Hôm qua",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "Tiếng Hàn",
"settings.sttLang.en": "Tiếng Anh",
"settings.sttLang.ja": "Tiếng Nhật",
"settings.sttLang.zh": "Tiếng Trung",
"template.outputHelperText": "Sử dụng {{tênTrường}} cho trình giữ chỗ",
"nav.meeting": "Cuộc họp",
"meeting.title": "Chế độ Cuộc họp",
"meeting.newMeeting": "Cuộc họp mới",
"meeting.startRecording": "Bắt đầu ghi âm",
"meeting.stopRecording": "Dừng ghi âm",
"meeting.recording": "Đang ghi âm",
"meeting.processing": "Đang tạo biên bản...",
"meeting.completed": "Hoàn thành",
"meeting.error": "Lỗi",
"meeting.elapsed": "Thời gian đã qua",
"meeting.memoPlaceholder": "Nhập ghi chú (Enter để thêm)",
"meeting.memoAdded": "Đã thêm ghi chú",
"meeting.noSessions": "Không có bản ghi cuộc họp",
"meeting.noSessionsDesc": "Nhấn «Cuộc họp mới» để bắt đầu ghi âm",
"meeting.summary": "Tóm tắt",
"meeting.decisions": "Quyết định",
"meeting.actionItems": "Hành động cần thực hiện",
"meeting.timeline": "Dòng thời gian",
"meeting.transcript": "Bản ghi thô",
"meeting.memos": "Ghi chú",
"meeting.exportPdf": "Xuất PDF",
"meeting.exportMarkdown": "Xuất Markdown",
"meeting.delete": "Xóa",
"meeting.deleteConfirm": "Xóa bản ghi cuộc họp này?",
"meeting.backToList": "Quay lại danh sách",
"meeting.notificationTitle": "Biên bản đã sẵn sàng",
"meeting.notificationBody": "Biên bản cuộc họp đã được tạo.",
"meeting.sessions": "{{count}} phiên",
"meeting.duration": "{{minutes}} phút",
"meeting.editTitle": "Chỉnh sửa tiêu đề",
"meeting.untitled": "Cuộc họp không có tiêu đề",
"meeting.processingStep.merging": "Đang ghép bản ghi...",
"meeting.processingStep.generating": "Đang tạo biên bản với LLM...",
"meeting.processingStep.parsing": "Đang phân tích biên bản...",
"meeting.processingStep.saving": "Đang lưu...",
"meeting.processingStep.notifying": "Đang gửi thông báo..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "本地AI語音助手", "app.tagline": "本地AI語音助手",
"nav.dashboard": "儀表板", "nav.dashboard": "儀表板",
"nav.history": "歷史記錄", "nav.history": "歷史記錄",
"nav.dictionary": "辭典", "nav.dictionary": "辭典",
"nav.commands": "指令", "nav.commands": "指令",
"nav.settings": "設定", "nav.settings": "設定",
"dashboard.sessionOverview": "工作階段總覽", "dashboard.sessionOverview": "工作階段總覽",
"dashboard.systemStatus": "系統狀態", "dashboard.systemStatus": "系統狀態",
"dashboard.sessionsToday": "今日工作階段", "dashboard.sessionsToday": "今日工作階段",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}}筆", "dashboard.entries": "{{count}}筆",
"dashboard.stat": "統計", "dashboard.stat": "統計",
"dashboard.sys": "系統", "dashboard.sys": "系統",
"history.title": "轉錄歷史", "history.title": "轉錄歷史",
"history.search": "搜尋...", "history.search": "搜尋...",
"history.entries": "{{count}}筆", "history.entries": "{{count}}筆",
@ -34,7 +31,6 @@
"history.noResults": "無搜尋結果", "history.noResults": "無搜尋結果",
"history.noHistory": "尚無歷史記錄 — 請開始錄音", "history.noHistory": "尚無歷史記錄 — 請開始錄音",
"history.count": "{{count}}筆", "history.count": "{{count}}筆",
"dictionary.title": "自訂辭典", "dictionary.title": "自訂辭典",
"dictionary.words": "{{count}}個詞", "dictionary.words": "{{count}}個詞",
"dictionary.add": "新增", "dictionary.add": "新增",
@ -47,7 +43,6 @@
"dictionary.addTitle": "新增詞條", "dictionary.addTitle": "新增詞條",
"dictionary.word": "詞彙", "dictionary.word": "詞彙",
"dictionary.pronunciation": "發音(選填)", "dictionary.pronunciation": "發音(選填)",
"commands.title": "LLM指令", "commands.title": "LLM指令",
"commands.count": "{{count}}個", "commands.count": "{{count}}個",
"commands.add": "新增", "commands.add": "新增",
@ -62,14 +57,12 @@
"commands.promptTemplate": "提示詞範本", "commands.promptTemplate": "提示詞範本",
"commands.promptHelp": "{{text}} 將被替換為轉錄的文字", "commands.promptHelp": "{{text}} 將被替換為轉錄的文字",
"commands.defaultPrompt": "請潤飾以下內容:{{text}}", "commands.defaultPrompt": "請潤飾以下內容:{{text}}",
"settings.title": "設定", "settings.title": "設定",
"settings.tabs.general": "一般", "settings.tabs.general": "一般",
"settings.tabs.audio": "音訊", "settings.tabs.audio": "音訊",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "關於", "settings.tabs.about": "關於",
"settings.shortcuts": "快速鍵", "settings.shortcuts": "快速鍵",
"settings.dictation": "聽寫", "settings.dictation": "聽寫",
"settings.dictation.desc": "按住鍵開始說話,放開鍵後開始轉錄。", "settings.dictation.desc": "按住鍵開始說話,放開鍵後開始轉錄。",
@ -82,20 +75,17 @@
"settings.notSet": "未設定", "settings.notSet": "未設定",
"settings.enabled": "已啟用", "settings.enabled": "已啟用",
"settings.disabled": "已停用", "settings.disabled": "已停用",
"settings.interface": "介面", "settings.interface": "介面",
"settings.theme": "主題", "settings.theme": "主題",
"settings.theme.system": "跟隨系統", "settings.theme.system": "跟隨系統",
"settings.theme.light": "淺色", "settings.theme.light": "淺色",
"settings.theme.dark": "深色", "settings.theme.dark": "深色",
"settings.language": "語言", "settings.language": "語言",
"settings.appBehavior": "應用程式行為", "settings.appBehavior": "應用程式行為",
"settings.closeToTray": "最小化至系統匣", "settings.closeToTray": "最小化至系統匣",
"settings.autoLaunch": "開機時自動啟動", "settings.autoLaunch": "開機時自動啟動",
"settings.autoInsert": "轉錄後自動插入文字", "settings.autoInsert": "轉錄後自動插入文字",
"settings.soundEffects": "音效", "settings.soundEffects": "音效",
"settings.microphone": "麥克風", "settings.microphone": "麥克風",
"settings.inputDevice": "輸入裝置", "settings.inputDevice": "輸入裝置",
"settings.deviceDefault": "(預設)", "settings.deviceDefault": "(預設)",
@ -103,7 +93,6 @@
"settings.insertMethod": "插入方式", "settings.insertMethod": "插入方式",
"settings.insertClipboard": "剪貼簿Ctrl+V", "settings.insertClipboard": "剪貼簿Ctrl+V",
"settings.insertKeyboard": "鍵盤輸入", "settings.insertKeyboard": "鍵盤輸入",
"settings.whisperModel": "Whisper模型", "settings.whisperModel": "Whisper模型",
"settings.model.tiny": "tiny39 MB最快", "settings.model.tiny": "tiny39 MB最快",
"settings.model.base": "base74 MB均衡", "settings.model.base": "base74 MB均衡",
@ -112,7 +101,6 @@
"settings.model.large": "large-v31.5 GB最佳", "settings.model.large": "large-v31.5 GB最佳",
"settings.sttLanguage": "識別語言", "settings.sttLanguage": "識別語言",
"settings.sttLang.auto": "自動偵測", "settings.sttLang.auto": "自動偵測",
"settings.ollamaServer": "Ollama伺服器", "settings.ollamaServer": "Ollama伺服器",
"settings.ollamaUrl": "Ollama伺服器URL", "settings.ollamaUrl": "Ollama伺服器URL",
"settings.ollamaHint": "Ollama執行時將自動連線。請直接在Ollama中pull模型例如ollama pull qwen3:4b。", "settings.ollamaHint": "Ollama執行時將自動連線。請直接在Ollama中pull模型例如ollama pull qwen3:4b。",
@ -125,7 +113,6 @@
"settings.action.grammar": "語法校正", "settings.action.grammar": "語法校正",
"settings.action.custom": "自訂提示詞", "settings.action.custom": "自訂提示詞",
"settings.actionHint": "透過熱鍵錄音後所選LLM後處理將套用至轉錄文字。僅在Ollama連線時有效。", "settings.actionHint": "透過熱鍵錄音後所選LLM後處理將套用至轉錄文字。僅在Ollama連線時有效。",
"settings.about.version": "版本", "settings.about.version": "版本",
"settings.about.techStack": "技術堆疊", "settings.about.techStack": "技術堆疊",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper本地/ LLM: Ollama本地", "settings.about.voiceEngineValue": "STT: faster-whisper本地/ LLM: Ollama本地",
"settings.about.description": "基於Speakly逆向工程經驗打造的本地AI語音助手無需依賴雲端完全在本地端執行。", "settings.about.description": "基於Speakly逆向工程經驗打造的本地AI語音助手無需依賴雲端完全在本地端執行。",
"settings.about.restartOnboarding": "重新檢視初始設定引導", "settings.about.restartOnboarding": "重新檢視初始設定引導",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama尚未執行", "status.nudge.title": "Ollama尚未執行",
"status.nudge.desc": "使用LLM後處理翻譯、摘要等需要Ollama。", "status.nudge.desc": "使用LLM後處理翻譯、摘要等需要Ollama。",
"status.nudge.guide": "查看安裝指南 →", "status.nudge.guide": "查看安裝指南 →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "無需打字用聲音操作。本地AI語音助手。", "onboarding.welcome.desc": "無需打字用聲音操作。本地AI語音助手。",
"onboarding.welcome.start": "開始", "onboarding.welcome.start": "開始",
@ -160,7 +145,6 @@
"onboarding.done.start": "開始", "onboarding.done.start": "開始",
"onboarding.back": "返回", "onboarding.back": "返回",
"onboarding.next": "下一步", "onboarding.next": "下一步",
"hotkey.title": "快速鍵設定", "hotkey.title": "快速鍵設定",
"hotkey.dictationTitle": "設定聽寫快速鍵", "hotkey.dictationTitle": "設定聽寫快速鍵",
"hotkey.oneTouchTitle": "設定一鍵模式快速鍵", "hotkey.oneTouchTitle": "設定一鍵模式快速鍵",
@ -171,7 +155,6 @@
"hotkey.current": "目前: {{keys}}", "hotkey.current": "目前: {{keys}}",
"hotkey.hint": "請輸入組合鍵例如Ctrl+Shift+Q或單一鍵例如F5", "hotkey.hint": "請輸入組合鍵例如Ctrl+Shift+Q或單一鍵例如F5",
"hotkey.reset": "重新輸入", "hotkey.reset": "重新輸入",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "STEP 1 — 安裝Ollama", "ollama.step1.title": "STEP 1 — 安裝Ollama",
"ollama.step1.desc": "Ollama是一款在本地端執行LLM的免費工具。", "ollama.step1.desc": "Ollama是一款在本地端執行LLM的免費工具。",
@ -180,7 +163,6 @@
"ollama.step2.alt": "或更大的模型ollama pull qwen3:8b更精準速度較慢", "ollama.step2.alt": "或更大的模型ollama pull qwen3:8b更精準速度較慢",
"ollama.step3.title": "STEP 3 — 自動連線", "ollama.step3.title": "STEP 3 — 自動連線",
"ollama.step3.desc": "Ollama啟動後D3RO-VOICE將自動偵測。底部狀態列的LED從紅色變為綠色即表示準備就緒", "ollama.step3.desc": "Ollama啟動後D3RO-VOICE將自動偵測。底部狀態列的LED從紅色變為綠色即表示準備就緒",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "取消", "common.cancel": "取消",
"common.save": "儲存", "common.save": "儲存",
"common.delete": "刪除", "common.delete": "刪除",
@ -202,7 +183,51 @@
"common.copy": "複製", "common.copy": "複製",
"common.test": "測試", "common.test": "測試",
"common.stop": "停止", "common.stop": "停止",
"date.today": "今天", "date.today": "今天",
"date.yesterday": "昨天" "date.yesterday": "昨天",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "韓語",
"settings.sttLang.en": "English",
"settings.sttLang.ja": "日語",
"settings.sttLang.zh": "中文",
"template.outputHelperText": "使用 {{欄位名稱}} 作為佔位符",
"nav.meeting": "會議",
"meeting.title": "會議模式",
"meeting.newMeeting": "新增會議",
"meeting.startRecording": "開始錄音",
"meeting.stopRecording": "停止錄音",
"meeting.recording": "錄音中",
"meeting.processing": "正在產生會議記錄...",
"meeting.completed": "已完成",
"meeting.error": "錯誤",
"meeting.elapsed": "已用時",
"meeting.memoPlaceholder": "輸入備忘Enter 新增)",
"meeting.memoAdded": "備忘已新增",
"meeting.noSessions": "暫無會議錄音",
"meeting.noSessionsDesc": "點擊「新增會議」開始錄音",
"meeting.summary": "摘要",
"meeting.decisions": "決議",
"meeting.actionItems": "待辦事項",
"meeting.timeline": "時間軸",
"meeting.transcript": "原始逐字稿",
"meeting.memos": "備忘",
"meeting.exportPdf": "匯出 PDF",
"meeting.exportMarkdown": "匯出 Markdown",
"meeting.delete": "刪除",
"meeting.deleteConfirm": "刪除此會議記錄?",
"meeting.backToList": "返回列表",
"meeting.notificationTitle": "會議記錄已就緒",
"meeting.notificationBody": "會議記錄已產生。",
"meeting.sessions": "{{count}} 場會議",
"meeting.duration": "{{minutes}} 分鐘",
"meeting.editTitle": "編輯標題",
"meeting.untitled": "無標題會議",
"meeting.processingStep.merging": "正在合併逐字稿...",
"meeting.processingStep.generating": "正在以 LLM 產生會議記錄...",
"meeting.processingStep.parsing": "正在解析會議記錄...",
"meeting.processingStep.saving": "正在儲存...",
"meeting.processingStep.notifying": "正在發送通知..."
} }

View file

@ -1,13 +1,11 @@
{ {
"app.name": "D3RO Voice", "app.name": "D3RO Voice",
"app.tagline": "本地AI语音助手", "app.tagline": "本地AI语音助手",
"nav.dashboard": "仪表板", "nav.dashboard": "仪表板",
"nav.history": "历史记录", "nav.history": "历史记录",
"nav.dictionary": "词典", "nav.dictionary": "词典",
"nav.commands": "命令", "nav.commands": "命令",
"nav.settings": "设置", "nav.settings": "设置",
"dashboard.sessionOverview": "会话概览", "dashboard.sessionOverview": "会话概览",
"dashboard.systemStatus": "系统状态", "dashboard.systemStatus": "系统状态",
"dashboard.sessionsToday": "今日会话", "dashboard.sessionsToday": "今日会话",
@ -26,7 +24,6 @@
"dashboard.entries": "{{count}}条", "dashboard.entries": "{{count}}条",
"dashboard.stat": "统计", "dashboard.stat": "统计",
"dashboard.sys": "系统", "dashboard.sys": "系统",
"history.title": "转录历史", "history.title": "转录历史",
"history.search": "搜索...", "history.search": "搜索...",
"history.entries": "{{count}}条", "history.entries": "{{count}}条",
@ -34,7 +31,6 @@
"history.noResults": "无搜索结果", "history.noResults": "无搜索结果",
"history.noHistory": "暂无历史记录 — 请开始录音", "history.noHistory": "暂无历史记录 — 请开始录音",
"history.count": "{{count}}条", "history.count": "{{count}}条",
"dictionary.title": "自定义词典", "dictionary.title": "自定义词典",
"dictionary.words": "{{count}}个词", "dictionary.words": "{{count}}个词",
"dictionary.add": "添加", "dictionary.add": "添加",
@ -47,7 +43,6 @@
"dictionary.addTitle": "添加词条", "dictionary.addTitle": "添加词条",
"dictionary.word": "词汇", "dictionary.word": "词汇",
"dictionary.pronunciation": "发音(可选)", "dictionary.pronunciation": "发音(可选)",
"commands.title": "LLM命令", "commands.title": "LLM命令",
"commands.count": "{{count}}个", "commands.count": "{{count}}个",
"commands.add": "添加", "commands.add": "添加",
@ -62,14 +57,12 @@
"commands.promptTemplate": "提示词模板", "commands.promptTemplate": "提示词模板",
"commands.promptHelp": "{{text}} 将被替换为转录的文本", "commands.promptHelp": "{{text}} 将被替换为转录的文本",
"commands.defaultPrompt": "请润色以下内容:{{text}}", "commands.defaultPrompt": "请润色以下内容:{{text}}",
"settings.title": "设置", "settings.title": "设置",
"settings.tabs.general": "通用", "settings.tabs.general": "通用",
"settings.tabs.audio": "音频", "settings.tabs.audio": "音频",
"settings.tabs.stt": "STT", "settings.tabs.stt": "STT",
"settings.tabs.llm": "LLM", "settings.tabs.llm": "LLM",
"settings.tabs.about": "关于", "settings.tabs.about": "关于",
"settings.shortcuts": "快捷键", "settings.shortcuts": "快捷键",
"settings.dictation": "听写", "settings.dictation": "听写",
"settings.dictation.desc": "按住键开始说话,松开键后开始转录。", "settings.dictation.desc": "按住键开始说话,松开键后开始转录。",
@ -82,20 +75,17 @@
"settings.notSet": "未设置", "settings.notSet": "未设置",
"settings.enabled": "已启用", "settings.enabled": "已启用",
"settings.disabled": "已禁用", "settings.disabled": "已禁用",
"settings.interface": "界面", "settings.interface": "界面",
"settings.theme": "主题", "settings.theme": "主题",
"settings.theme.system": "跟随系统", "settings.theme.system": "跟随系统",
"settings.theme.light": "浅色", "settings.theme.light": "浅色",
"settings.theme.dark": "深色", "settings.theme.dark": "深色",
"settings.language": "语言", "settings.language": "语言",
"settings.appBehavior": "应用行为", "settings.appBehavior": "应用行为",
"settings.closeToTray": "最小化到托盘", "settings.closeToTray": "最小化到托盘",
"settings.autoLaunch": "开机自动启动", "settings.autoLaunch": "开机自动启动",
"settings.autoInsert": "转录后自动插入文本", "settings.autoInsert": "转录后自动插入文本",
"settings.soundEffects": "音效", "settings.soundEffects": "音效",
"settings.microphone": "麦克风", "settings.microphone": "麦克风",
"settings.inputDevice": "输入设备", "settings.inputDevice": "输入设备",
"settings.deviceDefault": "(默认)", "settings.deviceDefault": "(默认)",
@ -103,7 +93,6 @@
"settings.insertMethod": "插入方式", "settings.insertMethod": "插入方式",
"settings.insertClipboard": "剪贴板Ctrl+V", "settings.insertClipboard": "剪贴板Ctrl+V",
"settings.insertKeyboard": "键盘输入", "settings.insertKeyboard": "键盘输入",
"settings.whisperModel": "Whisper模型", "settings.whisperModel": "Whisper模型",
"settings.model.tiny": "tiny39 MB最快", "settings.model.tiny": "tiny39 MB最快",
"settings.model.base": "base74 MB均衡", "settings.model.base": "base74 MB均衡",
@ -112,7 +101,6 @@
"settings.model.large": "large-v31.5 GB最佳", "settings.model.large": "large-v31.5 GB最佳",
"settings.sttLanguage": "识别语言", "settings.sttLanguage": "识别语言",
"settings.sttLang.auto": "自动检测", "settings.sttLang.auto": "自动检测",
"settings.ollamaServer": "Ollama服务器", "settings.ollamaServer": "Ollama服务器",
"settings.ollamaUrl": "Ollama服务器URL", "settings.ollamaUrl": "Ollama服务器URL",
"settings.ollamaHint": "Ollama运行时将自动连接。请直接在Ollama中pull模型例如ollama pull qwen3:4b。", "settings.ollamaHint": "Ollama运行时将自动连接。请直接在Ollama中pull模型例如ollama pull qwen3:4b。",
@ -125,7 +113,6 @@
"settings.action.grammar": "语法校正", "settings.action.grammar": "语法校正",
"settings.action.custom": "自定义提示词", "settings.action.custom": "自定义提示词",
"settings.actionHint": "通过热键录音后所选LLM后处理将应用于转录文本。仅在Ollama连接时有效。", "settings.actionHint": "通过热键录音后所选LLM后处理将应用于转录文本。仅在Ollama连接时有效。",
"settings.about.version": "版本", "settings.about.version": "版本",
"settings.about.techStack": "技术栈", "settings.about.techStack": "技术栈",
"settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript", "settings.about.techStackValue": "Electron + React 19 + MUI 7 + TypeScript",
@ -133,13 +120,11 @@
"settings.about.voiceEngineValue": "STT: faster-whisper本地/ LLM: Ollama本地", "settings.about.voiceEngineValue": "STT: faster-whisper本地/ LLM: Ollama本地",
"settings.about.description": "基于Speakly逆向工程经验构建的本地AI语音助手无需依赖云端完全在本地运行。", "settings.about.description": "基于Speakly逆向工程经验构建的本地AI语音助手无需依赖云端完全在本地运行。",
"settings.about.restartOnboarding": "重新查看初始设置引导", "settings.about.restartOnboarding": "重新查看初始设置引导",
"status.ollama": "OLLAMA", "status.ollama": "OLLAMA",
"status.offline": "OFFLINE", "status.offline": "OFFLINE",
"status.nudge.title": "Ollama未运行", "status.nudge.title": "Ollama未运行",
"status.nudge.desc": "使用LLM后处理翻译、摘要等需要Ollama。", "status.nudge.desc": "使用LLM后处理翻译、摘要等需要Ollama。",
"status.nudge.guide": "查看安装指南 →", "status.nudge.guide": "查看安装指南 →",
"onboarding.welcome.title": "D3RO-VOICE", "onboarding.welcome.title": "D3RO-VOICE",
"onboarding.welcome.desc": "无需打字用声音操作。本地AI语音助手。", "onboarding.welcome.desc": "无需打字用声音操作。本地AI语音助手。",
"onboarding.welcome.start": "开始", "onboarding.welcome.start": "开始",
@ -160,7 +145,6 @@
"onboarding.done.start": "开始", "onboarding.done.start": "开始",
"onboarding.back": "返回", "onboarding.back": "返回",
"onboarding.next": "下一步", "onboarding.next": "下一步",
"hotkey.title": "快捷键设置", "hotkey.title": "快捷键设置",
"hotkey.dictationTitle": "设置听写快捷键", "hotkey.dictationTitle": "设置听写快捷键",
"hotkey.oneTouchTitle": "设置一触即发模式快捷键", "hotkey.oneTouchTitle": "设置一触即发模式快捷键",
@ -171,7 +155,6 @@
"hotkey.current": "当前: {{keys}}", "hotkey.current": "当前: {{keys}}",
"hotkey.hint": "请输入组合键例如Ctrl+Shift+Q或单个键例如F5", "hotkey.hint": "请输入组合键例如Ctrl+Shift+Q或单个键例如F5",
"hotkey.reset": "重新输入", "hotkey.reset": "重新输入",
"ollama.title": "OLLAMA SETUP GUIDE", "ollama.title": "OLLAMA SETUP GUIDE",
"ollama.step1.title": "STEP 1 — 安装Ollama", "ollama.step1.title": "STEP 1 — 安装Ollama",
"ollama.step1.desc": "Ollama是一款在本地运行LLM的免费工具。", "ollama.step1.desc": "Ollama是一款在本地运行LLM的免费工具。",
@ -180,7 +163,6 @@
"ollama.step2.alt": "或更大的模型ollama pull qwen3:8b更精准更慢", "ollama.step2.alt": "或更大的模型ollama pull qwen3:8b更精准更慢",
"ollama.step3.title": "STEP 3 — 自动连接", "ollama.step3.title": "STEP 3 — 自动连接",
"ollama.step3.desc": "Ollama启动后D3RO-VOICE将自动检测。底部状态栏的LED从红色变为绿色即表示准备就绪", "ollama.step3.desc": "Ollama启动后D3RO-VOICE将自动检测。底部状态栏的LED从红色变为绿色即表示准备就绪",
"service.sttEngine": "STT ENGINE", "service.sttEngine": "STT ENGINE",
"service.ollamaLlm": "OLLAMA LLM", "service.ollamaLlm": "OLLAMA LLM",
"service.hotkeyHook": "HOTKEY HOOK", "service.hotkeyHook": "HOTKEY HOOK",
@ -190,7 +172,6 @@
"service.offline": "OFFLINE", "service.offline": "OFFLINE",
"service.active": "ACTIVE", "service.active": "ACTIVE",
"service.standby": "STANDBY", "service.standby": "STANDBY",
"common.cancel": "取消", "common.cancel": "取消",
"common.save": "保存", "common.save": "保存",
"common.delete": "删除", "common.delete": "删除",
@ -202,7 +183,51 @@
"common.copy": "复制", "common.copy": "复制",
"common.test": "测试", "common.test": "测试",
"common.stop": "停止", "common.stop": "停止",
"date.today": "今天", "date.today": "今天",
"date.yesterday": "昨天" "date.yesterday": "昨天",
"settings.theme.nord": "Nord",
"settings.theme.solarized": "Solarized",
"settings.theme.catppuccin": "Catppuccin",
"settings.theme.dracula": "Dracula",
"settings.sttLang.ko": "韩语",
"settings.sttLang.en": "English",
"settings.sttLang.ja": "日语",
"settings.sttLang.zh": "中文",
"template.outputHelperText": "使用 {{字段名}} 作为占位符",
"nav.meeting": "会议",
"meeting.title": "会议模式",
"meeting.newMeeting": "新建会议",
"meeting.startRecording": "开始录音",
"meeting.stopRecording": "停止录音",
"meeting.recording": "录音中",
"meeting.processing": "正在生成会议纪要...",
"meeting.completed": "已完成",
"meeting.error": "错误",
"meeting.elapsed": "已用时",
"meeting.memoPlaceholder": "输入备忘Enter 添加)",
"meeting.memoAdded": "备忘已添加",
"meeting.noSessions": "暂无会议录音",
"meeting.noSessionsDesc": "点击「新建会议」开始录音",
"meeting.summary": "摘要",
"meeting.decisions": "决议",
"meeting.actionItems": "待办事项",
"meeting.timeline": "时间轴",
"meeting.transcript": "原始转录",
"meeting.memos": "备忘",
"meeting.exportPdf": "导出 PDF",
"meeting.exportMarkdown": "导出 Markdown",
"meeting.delete": "删除",
"meeting.deleteConfirm": "删除此会议记录?",
"meeting.backToList": "返回列表",
"meeting.notificationTitle": "会议纪要已就绪",
"meeting.notificationBody": "会议纪要已生成。",
"meeting.sessions": "{{count}} 场会议",
"meeting.duration": "{{minutes}} 分钟",
"meeting.editTitle": "编辑标题",
"meeting.untitled": "无标题会议",
"meeting.processingStep.merging": "正在合并转录...",
"meeting.processingStep.generating": "正在用 LLM 生成会议纪要...",
"meeting.processingStep.parsing": "正在解析会议纪要...",
"meeting.processingStep.saving": "正在保存...",
"meeting.processingStep.notifying": "正在发送通知..."
} }

View file

@ -0,0 +1,570 @@
// src/renderer/pages/MeetingModePage.tsx
// Phase 14: Meeting Mode — 실시간 녹음 + 메모 + 회의록 UI
import { useState, useEffect, useCallback, useRef } from 'react'
import {
Box,
TextField,
IconButton,
Tooltip,
LinearProgress,
Typography,
} from '@mui/material'
import AddIcon from '@mui/icons-material/Add'
import StopIcon from '@mui/icons-material/Stop'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import DeleteIcon from '@mui/icons-material/Delete'
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf'
import DescriptionIcon from '@mui/icons-material/Description'
import EditIcon from '@mui/icons-material/Edit'
import CheckIcon from '@mui/icons-material/Check'
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
import { MetalCard, PhosphorText, Led, PhysicalButton, ScreenPanel } from '../components/ds'
import { PageHeader, EmptyStateCard } from '../components/shared'
import { d3roPalette, d3roFontMono, d3roTypo } from '../theme'
import { useI18n } from '../i18n'
import type {
MeetingSessionSummary,
MeetingSessionDetail,
MeetingModeStateInfo,
MeetingProcessingProgress,
CaptionSegment,
MeetingMemo,
} from '@shared/types'
type MeetingView = 'list' | 'recording' | 'detail'
export function MeetingModePage(): React.ReactElement {
const { t } = useI18n()
const [view, setView] = useState<MeetingView>('list')
const [sessions, setSessions] = useState<MeetingSessionSummary[]>([])
const [totalSessions, setTotalSessions] = useState(0)
const [loading, setLoading] = useState(true)
const [page, setPage] = useState(1)
const pageSize = 20
// 녹음 뷰 상태
const [stateInfo, setStateInfo] = useState<MeetingModeStateInfo | null>(null)
const [segments, setSegments] = useState<CaptionSegment[]>([])
const [memos, setMemos] = useState<MeetingMemo[]>([])
const [memoInput, setMemoInput] = useState('')
const [elapsedMs, setElapsedMs] = useState(0)
const [progress, setProgress] = useState<MeetingProcessingProgress | null>(null)
const transcriptRef = useRef<HTMLDivElement>(null)
const elapsedTimerRef = useRef<ReturnType<typeof setInterval> | null>(null)
const recordingStartRef = useRef<number>(0)
// 상세 뷰 상태
const [detail, setDetail] = useState<MeetingSessionDetail | null>(null)
const [editingTitle, setEditingTitle] = useState(false)
const [titleDraft, setTitleDraft] = useState('')
// ── 세션 목록 로드 ──
const loadSessions = useCallback(async () => {
setLoading(true)
const resp = await window.electronAPI.meetingMode.getSessions({ page, pageSize })
if (resp.success) {
setSessions(resp.data.sessions)
setTotalSessions(resp.data.total)
}
setLoading(false)
}, [page])
useEffect(() => {
if (view === 'list') loadSessions()
}, [view, loadSessions])
// ── IPC 이벤트 구독 ──
useEffect(() => {
const unsubState = window.electronAPI.meetingMode.onStateChanged((info) => {
setStateInfo(info)
if (info.state === 'idle' && view === 'recording') {
// 후처리 완료 → 목록으로 이동
if (!progress) {
setView('list')
}
}
})
const unsubSegment = window.electronAPI.meetingMode.onSegment((seg) => {
setSegments((prev) => [...prev, seg])
// 자동 스크롤
setTimeout(() => {
if (transcriptRef.current) {
transcriptRef.current.scrollTop = transcriptRef.current.scrollHeight
}
}, 50)
})
const unsubProgress = window.electronAPI.meetingMode.onProcessingProgress((p) => {
setProgress(p)
})
const unsubCompleted = window.electronAPI.meetingMode.onSessionCompleted(() => {
setProgress(null)
setView('list')
})
const unsubError = window.electronAPI.meetingMode.onError(() => {
setProgress(null)
setView('list')
})
return () => {
unsubState()
unsubSegment()
unsubProgress()
unsubCompleted()
unsubError()
}
}, [view, progress])
// ── 경과 시간 타이머 ──
useEffect(() => {
if (view === 'recording') {
recordingStartRef.current = Date.now()
elapsedTimerRef.current = setInterval(() => {
setElapsedMs(Date.now() - recordingStartRef.current)
}, 1000)
}
return () => {
if (elapsedTimerRef.current) {
clearInterval(elapsedTimerRef.current)
elapsedTimerRef.current = null
}
}
}, [view])
// ── 핸들러 ──
const handleStartRecording = useCallback(async () => {
const resp = await window.electronAPI.meetingMode.startRecording()
if (resp.success) {
setSegments([])
setMemos([])
setElapsedMs(0)
setProgress(null)
setView('recording')
}
}, [])
const handleStopRecording = useCallback(async () => {
await window.electronAPI.meetingMode.stopRecording()
}, [])
const handleAddMemo = useCallback(async () => {
if (!memoInput.trim()) return
const resp = await window.electronAPI.meetingMode.addMemo({ content: memoInput.trim() })
if (resp.success) {
setMemos((prev) => [...prev, resp.data])
setMemoInput('')
}
}, [memoInput])
const handleMemoKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleAddMemo()
}
},
[handleAddMemo],
)
const handleViewDetail = useCallback(async (sessionId: string) => {
const resp = await window.electronAPI.meetingMode.getSession({ sessionId })
if (resp.success) {
setDetail(resp.data)
setView('detail')
}
}, [])
const handleDelete = useCallback(async () => {
if (!detail) return
await window.electronAPI.meetingMode.deleteSession({ sessionId: detail.id })
setDetail(null)
setView('list')
}, [detail])
const handleExportPdf = useCallback(async () => {
if (!detail) return
await window.electronAPI.meetingMode.exportPdf({ sessionId: detail.id })
}, [detail])
const handleExportMarkdown = useCallback(async () => {
if (!detail) return
await window.electronAPI.meetingMode.exportMarkdown({ sessionId: detail.id })
}, [detail])
const handleSaveTitle = useCallback(async () => {
if (!detail || !titleDraft.trim()) return
await window.electronAPI.meetingMode.updateTitle({
sessionId: detail.id,
title: titleDraft.trim(),
})
setDetail({ ...detail, title: titleDraft.trim() })
setEditingTitle(false)
}, [detail, titleDraft])
// ── 유틸 ──
const formatTime = (ms: number): string => {
const totalSec = Math.floor(ms / 1000)
const min = Math.floor(totalSec / 60)
const sec = totalSec % 60
return `${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
}
const formatDate = (ts: number): string => {
const d = new Date(ts)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}
const statusColor = (status: string): 'green' | 'amber' | 'red' | 'blue' => {
switch (status) {
case 'completed': return 'green'
case 'recording': return 'red'
case 'processing': return 'amber'
case 'error': return 'red'
default: return 'blue'
}
}
// ── 렌더: 목록 뷰 ──
if (view === 'list') {
return (
<Box sx={{ p: 3 }}>
<PageHeader
title={t('meeting.title')}
count={totalSessions > 0 ? t('meeting.sessions', { count: totalSessions }) : undefined}
action={
<PhysicalButton size="small" onClick={handleStartRecording}>
<AddIcon sx={{ fontSize: 16, mr: 0.5 }} />
{t('meeting.newMeeting')}
</PhysicalButton>
}
/>
{loading ? (
<LinearProgress sx={{ mt: 2 }} />
) : sessions.length === 0 ? (
<EmptyStateCard message={`${t('meeting.noSessions')}\n${t('meeting.noSessionsDesc')}`} />
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{sessions.map((session) => (
<MetalCard
key={session.id}
sx={{ cursor: 'pointer', '&:hover': { opacity: 0.85 } }}
onClick={() => handleViewDetail(session.id)}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Box>
<PhosphorText variant="label">
{session.title ?? t('meeting.untitled')}
</PhosphorText>
<PhosphorText variant="dim" sx={{ mt: 0.5 }}>
{session.durationMs != null
? t('meeting.duration', { minutes: Math.round(session.durationMs / 60000) })
: '—'}
{' · '}
{t('meeting.memos')}: {session.memoCount}
</PhosphorText>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PhosphorText variant="dim">{formatDate(session.startedAt)}</PhosphorText>
<Led color={statusColor(session.status)} size={8} />
</Box>
</Box>
{/* 프로세싱 중인 세션 표시 */}
{session.status === 'processing' && progress && progress.sessionId === session.id && (
<Box sx={{ mt: 1 }}>
<LinearProgress
variant="determinate"
value={progress.percent}
sx={{ height: 4, borderRadius: 2 }}
/>
<PhosphorText variant="dim" sx={{ mt: 0.5, fontSize: 11 }}>
{t(`meeting.processingStep.${progress.step}` as Parameters<typeof t>[0])}
</PhosphorText>
</Box>
)}
</MetalCard>
))}
</Box>
)}
</Box>
)
}
// ── 렌더: 녹음 뷰 ──
if (view === 'recording') {
const isProcessing = stateInfo?.state === 'processing'
return (
<Box sx={{ p: 3, height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* 상단 바 */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<FiberManualRecordIcon sx={{ color: d3roPalette.indicator.red, fontSize: 16, animation: 'pulse 1s infinite' }} />
<PhosphorText variant="label" sx={{ fontFamily: d3roFontMono }}>
{formatTime(elapsedMs)}
</PhosphorText>
<PhosphorText variant="dim">
{isProcessing ? t('meeting.processing') : t('meeting.recording')}
</PhosphorText>
</Box>
{!isProcessing && (
<PhysicalButton size="small" color="error" onClick={handleStopRecording}>
<StopIcon sx={{ fontSize: 16, mr: 0.5 }} />
{t('meeting.stopRecording')}
</PhysicalButton>
)}
</Box>
{/* 프로세싱 진행률 */}
{isProcessing && progress && (
<Box sx={{ mb: 2 }}>
<LinearProgress variant="determinate" value={progress.percent} sx={{ height: 6, borderRadius: 3 }} />
<PhosphorText variant="dim" sx={{ mt: 0.5 }}>
{t(`meeting.processingStep.${progress.step}` as Parameters<typeof t>[0])} ({progress.percent}%)
</PhosphorText>
</Box>
)}
{/* 메인 패널: 전사 + 메모 */}
<Box sx={{ flex: 1, display: 'flex', gap: 2, minHeight: 0 }}>
{/* 좌: 실시간 전사 */}
<ScreenPanel sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<PhosphorText variant="dim" sx={{ mb: 1, fontSize: 11 }}>
{t('meeting.transcript')}
</PhosphorText>
<Box
ref={transcriptRef}
sx={{
flex: 1,
overflow: 'auto',
fontFamily: d3roFontMono,
fontSize: d3roTypo.reading.fontSize,
color: d3roPalette.text.primary,
lineHeight: 1.8,
}}
>
{segments.map((seg) => (
<Box key={seg.id} sx={{ mb: 0.5 }}>
<Typography
component="span"
sx={{ color: d3roPalette.text.inactive, fontSize: 11, mr: 1, fontFamily: d3roFontMono }}
>
[{formatTime(seg.timestamp)}]
</Typography>
<Typography component="span" sx={{ fontSize: 13, fontFamily: d3roFontMono }}>
{seg.text}
</Typography>
</Box>
))}
</Box>
</ScreenPanel>
{/* 우: 메모 입력 */}
<Box sx={{ width: 300, display: 'flex', flexDirection: 'column' }}>
<PhosphorText variant="dim" sx={{ mb: 1, fontSize: 11 }}>
{t('meeting.memos')}
</PhosphorText>
<Box sx={{ flex: 1, overflow: 'auto', mb: 1 }}>
{memos.map((memo) => (
<Box
key={memo.id}
sx={{
mb: 0.5,
p: 1,
borderRadius: 1,
bgcolor: d3roPalette.bg.chassis,
}}
>
<PhosphorText variant="dim" sx={{ fontSize: 10, fontFamily: d3roFontMono }}>
{formatTime(memo.timestampMs)}
</PhosphorText>
<PhosphorText variant="reading" sx={{ fontSize: 12 }}>
{memo.content}
</PhosphorText>
</Box>
))}
</Box>
{!isProcessing && (
<TextField
size="small"
fullWidth
placeholder={t('meeting.memoPlaceholder')}
value={memoInput}
onChange={(e) => setMemoInput(e.target.value)}
onKeyDown={handleMemoKeyDown}
sx={{
'& .MuiInputBase-root': {
fontFamily: d3roFontMono,
fontSize: 13,
},
}}
/>
)}
</Box>
</Box>
</Box>
)
}
// ── 렌더: 상세 뷰 ──
if (view === 'detail' && detail) {
const durationMin = detail.durationMs ? Math.round(detail.durationMs / 60000) : 0
return (
<Box sx={{ p: 3, overflow: 'auto' }}>
{/* 헤더 */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<IconButton size="small" onClick={() => { setDetail(null); setView('list') }}>
<ArrowBackIcon sx={{ fontSize: 18 }} />
</IconButton>
{editingTitle ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flex: 1 }}>
<TextField
size="small"
value={titleDraft}
onChange={(e) => setTitleDraft(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSaveTitle()}
sx={{ flex: 1 }}
autoFocus
/>
<IconButton size="small" onClick={handleSaveTitle}>
<CheckIcon sx={{ fontSize: 16 }} />
</IconButton>
</Box>
) : (
<>
<PhosphorText variant="label" sx={{ flex: 1 }}>
{detail.title ?? t('meeting.untitled')}
</PhosphorText>
<Tooltip title={t('meeting.editTitle')}>
<IconButton
size="small"
onClick={() => { setEditingTitle(true); setTitleDraft(detail.title ?? '') }}
>
<EditIcon sx={{ fontSize: 16 }} />
</IconButton>
</Tooltip>
</>
)}
</Box>
{/* 메타 */}
<PhosphorText variant="dim" sx={{ mb: 3 }}>
{formatDate(detail.startedAt)}
{detail.endedAt ? ` ~ ${new Date(detail.endedAt).getHours()}:${String(new Date(detail.endedAt).getMinutes()).padStart(2, '0')}` : ''}
{' · '}
{t('meeting.duration', { minutes: durationMin })}
</PhosphorText>
{/* 회의록 본문 */}
{detail.minutes && (
<>
{/* 요약 */}
<MetalCard sx={{ mb: 2 }}>
<PhosphorText variant="label" sx={{ mb: 1 }}>{t('meeting.summary')}</PhosphorText>
<PhosphorText variant="reading" sx={{ whiteSpace: 'pre-wrap' }}>
{detail.minutes.summary}
</PhosphorText>
</MetalCard>
{/* 결정사항 */}
{detail.minutes.decisions.length > 0 && (
<MetalCard sx={{ mb: 2 }}>
<PhosphorText variant="label" sx={{ mb: 1 }}>{t('meeting.decisions')}</PhosphorText>
{detail.minutes.decisions.map((d, i) => (
<PhosphorText key={i} variant="reading" sx={{ mb: 0.5 }}>
{'• '}{d}
</PhosphorText>
))}
</MetalCard>
)}
{/* 할 일 */}
{detail.minutes.actionItems.length > 0 && (
<MetalCard sx={{ mb: 2 }}>
<PhosphorText variant="label" sx={{ mb: 1 }}>{t('meeting.actionItems')}</PhosphorText>
{detail.minutes.actionItems.map((item, i) => (
<PhosphorText key={i} variant="reading" sx={{ mb: 0.5 }}>
{'☐ '}{item.assignee ? `${item.assignee}: ` : ''}{item.task}
{item.deadline ? ` (${item.deadline})` : ''}
</PhosphorText>
))}
</MetalCard>
)}
{/* 타임라인 */}
{detail.minutes.timeline.length > 0 && (
<MetalCard sx={{ mb: 2 }}>
<PhosphorText variant="label" sx={{ mb: 1 }}>{t('meeting.timeline')}</PhosphorText>
{detail.minutes.timeline.map((tl, i) => (
<Box key={i} sx={{ display: 'flex', gap: 2, mb: 0.5 }}>
<PhosphorText variant="dim" sx={{ fontFamily: d3roFontMono, minWidth: 50 }}>
{tl.time}
</PhosphorText>
<PhosphorText variant="reading" sx={{ flex: 1 }}>
{tl.type === 'memo' ? '📝 ' : ''}{tl.content}
</PhosphorText>
</Box>
))}
</MetalCard>
)}
</>
)}
{/* 에러 메시지 */}
{detail.status === 'error' && detail.errorMessage && (
<MetalCard sx={{ mb: 2 }}>
<PhosphorText variant="label" sx={{ color: d3roPalette.indicator.red, mb: 1 }}>
{t('meeting.error')}
</PhosphorText>
<PhosphorText variant="reading">{detail.errorMessage}</PhosphorText>
</MetalCard>
)}
{/* 메모 */}
{detail.memos.length > 0 && (
<MetalCard sx={{ mb: 2 }}>
<PhosphorText variant="label" sx={{ mb: 1 }}>{t('meeting.memos')}</PhosphorText>
{detail.memos.map((m) => (
<Box key={m.id} sx={{ display: 'flex', gap: 2, mb: 0.5 }}>
<PhosphorText variant="dim" sx={{ fontFamily: d3roFontMono, minWidth: 50 }}>
{formatTime(m.timestampMs)}
</PhosphorText>
<PhosphorText variant="reading">{m.content}</PhosphorText>
</Box>
))}
</MetalCard>
)}
{/* 액션 버튼 */}
<Box sx={{ display: 'flex', gap: 1, mt: 3 }}>
<Tooltip title={t('meeting.exportPdf')}>
<PhysicalButton size="small" onClick={handleExportPdf}>
<PictureAsPdfIcon sx={{ fontSize: 16, mr: 0.5 }} />
{t('meeting.exportPdf')}
</PhysicalButton>
</Tooltip>
<Tooltip title={t('meeting.exportMarkdown')}>
<PhysicalButton size="small" onClick={handleExportMarkdown}>
<DescriptionIcon sx={{ fontSize: 16, mr: 0.5 }} />
{t('meeting.exportMarkdown')}
</PhysicalButton>
</Tooltip>
<Tooltip title={t('meeting.delete')}>
<PhysicalButton size="small" color="error" onClick={handleDelete}>
<DeleteIcon sx={{ fontSize: 16, mr: 0.5 }} />
{t('meeting.delete')}
</PhysicalButton>
</Tooltip>
</Box>
</Box>
)
}
return <Box />
}

View file

@ -151,6 +151,14 @@ export enum ErrorCode {
VoiceActionBlocked = 877, VoiceActionBlocked = 877,
VoiceActionInvalidPlan = 878, VoiceActionInvalidPlan = 878,
// === Phase 14: Meeting Mode (880-889) ===
MeetingAlreadyRecording = 880,
MeetingNotRecording = 881,
MeetingProcessingFailed = 882,
MeetingSessionNotFound = 883,
MeetingExportFailed = 884,
MeetingPdfFailed = 885,
// === Config (800-849) === // === Config (800-849) ===
ConfigReadFailed = 800, ConfigReadFailed = 800,
ConfigWriteFailed = 801, ConfigWriteFailed = 801,

View file

@ -305,6 +305,26 @@ export const IPC_CHANNELS = {
ACTION_ERROR: 'voiceAction:actionError', ACTION_ERROR: 'voiceAction:actionError',
}, },
// ── Phase 14: Meeting Mode ──
MEETING_MODE: {
START_RECORDING: 'meetingMode:startRecording',
STOP_RECORDING: 'meetingMode:stopRecording',
ADD_MEMO: 'meetingMode:addMemo',
GET_STATE: 'meetingMode:getState',
GET_SESSIONS: 'meetingMode:getSessions',
GET_SESSION: 'meetingMode:getSession',
DELETE_SESSION: 'meetingMode:deleteSession',
UPDATE_TITLE: 'meetingMode:updateTitle',
EXPORT_PDF: 'meetingMode:exportPdf',
EXPORT_MARKDOWN: 'meetingMode:exportMarkdown',
// Main → Renderer events
STATE_CHANGED: 'meetingMode:stateChanged',
SEGMENT: 'meetingMode:segment',
PROCESSING_PROGRESS: 'meetingMode:processingProgress',
SESSION_COMPLETED: 'meetingMode:sessionCompleted',
ERROR: 'meetingMode:error',
},
// ── Phase 11: License & Monetization ── // ── Phase 11: License & Monetization ──
LICENSE: { LICENSE: {
GET_INFO: 'license:getInfo', GET_INFO: 'license:getInfo',

View file

@ -1262,3 +1262,99 @@ export interface VoiceActionErrorEvent {
message: string message: string
userText: string userText: string
} }
// ============================================================
// Phase 14: Meeting Mode
// ============================================================
export type MeetingModeState = 'idle' | 'recording' | 'processing' | 'completed' | 'error'
export interface MeetingMemo {
id: string
sessionId: string
content: string
/** 녹음 시작 기준 상대 타임스탬프 (ms) */
timestampMs: number
createdAt: number
}
export interface MeetingSessionSummary {
id: string
title: string | null
status: 'recording' | 'processing' | 'completed' | 'error'
startedAt: number
endedAt: number | null
durationMs: number | null
memoCount: number
}
export interface MeetingSessionDetail extends MeetingSessionSummary {
rawTranscript: string | null
minutesMarkdown: string | null
minutes: MeetingMinutes | null
memos: MeetingMemo[]
sttModel: string | null
llmModel: string | null
errorMessage: string | null
}
export interface MeetingMinutes {
summary: string
decisions: string[]
actionItems: Array<{ task: string; assignee?: string; deadline?: string }>
timeline: Array<{ time: string; content: string; type: 'transcript' | 'memo' }>
}
export interface MeetingSessionPage {
sessions: MeetingSessionSummary[]
total: number
page: number
pageSize: number
totalPages: number
}
export interface MeetingStartResult {
sessionId: string
}
export interface MeetingAddMemoParams {
content: string
}
export interface MeetingGetSessionsParams {
page: number
pageSize: number
}
export interface MeetingGetSessionParams {
sessionId: string
}
export interface MeetingDeleteSessionParams {
sessionId: string
}
export interface MeetingExportParams {
sessionId: string
}
export interface MeetingModeStateInfo {
state: MeetingModeState
sessionId: string | null
elapsedMs: number
memoCount: number
segmentCount: number
}
export type MeetingProcessingStep =
| 'merging'
| 'generating'
| 'parsing'
| 'saving'
| 'notifying'
export interface MeetingProcessingProgress {
sessionId: string
step: MeetingProcessingStep
percent: number
}

View file

@ -119,11 +119,9 @@ describe('VoiceModeService', () => {
const svc = getVoiceModeService() const svc = getVoiceModeService()
expect(svc.isActive).toBe(false) expect(svc.isActive).toBe(false)
// startSession은 비동기이므로 STT init이 resolve되면 세션 활성 // startSession은 완전 비동기이므로 await 후 세션 활성 확인
const promise = svc.startSession('dictation') await svc.startSession('dictation')
expect(svc.isActive).toBe(true) // PREPARING 상태 expect(svc.isActive).toBe(true)
await promise
}) })
}) })