Skip to content

Commit 18106fe

Browse files
committed
fix live2d load error
The issue was caused by a confUid validation check in live2d-config-context that prevented modelInfo from being set when received via WebSocket. In Electron mode, there's a timing difference where modelInfo arrives before confUid is updated, causing the validation to block model initialization. Removed the confUid check that was preventing modelInfo updates and added basic null checks in Live2D SDK components to prevent runtime errors.
1 parent 097bc6c commit 18106fe

File tree

8 files changed

+129
-33
lines changed

8 files changed

+129
-33
lines changed

CLAUDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Development Commands
6+
7+
### Install dependencies
8+
```bash
9+
npm install
10+
```
11+
12+
### Run development server
13+
```bash
14+
npm run dev # Run Electron app in dev mode
15+
npm run dev:web # Run web-only version
16+
```
17+
18+
### Build commands
19+
```bash
20+
npm run build:win # Build for Windows
21+
npm run build:mac # Build for macOS
22+
npm run build:linux # Build for Linux
23+
npm run build:web # Build web version
24+
```
25+
26+
### Code quality
27+
```bash
28+
npm run lint # Run ESLint
29+
npm run lint:fix # Run ESLint with auto-fix
30+
npm run typecheck # Run TypeScript type checking (both node and web)
31+
npm run format # Format code with Prettier
32+
```
33+
34+
### Translation extraction
35+
```bash
36+
npm run extract-translations # Extract i18n strings
37+
```
38+
39+
## Architecture Overview
40+
41+
This is an Electron + React application for an AI VTuber system with Live2D integration. The architecture consists of:
42+
43+
### Main Process (`src/main/`)
44+
- **index.ts**: Entry point, sets up IPC handlers for window management, mouse events, and screen capture
45+
- **window-manager.ts**: Manages window state, modes (window/pet), and window properties
46+
- **menu-manager.ts**: Handles system tray and context menus
47+
48+
### Renderer Process (`src/renderer/src/`)
49+
- **App.tsx**: Root component that sets up providers and renders the main layout
50+
- **Two display modes**:
51+
- **Window mode**: Full UI with sidebar, footer, and Live2D canvas
52+
- **Pet mode**: Minimal overlay with just Live2D and input subtitle
53+
54+
### Core Services
55+
- **WebSocket Handler** (`services/websocket-handler.tsx`): Central communication hub that:
56+
- Manages WebSocket connection to backend server
57+
- Handles incoming messages (audio, control, model updates, chat history)
58+
- Coordinates state updates across multiple contexts
59+
- Manages audio playback queue
60+
61+
### Context Providers (State Management)
62+
The app uses React Context for state management with multiple specialized contexts:
63+
- **AiStateContext**: AI conversation state (idle, thinking, speaking, listening)
64+
- **Live2DConfigContext**: Live2D model configuration and loading
65+
- **ChatHistoryContext**: Conversation history and messages
66+
- **VADContext**: Voice Activity Detection for microphone control
67+
- **WebSocketContext**: WebSocket connection state and messaging
68+
- **SubtitleContext**: Subtitle display management
69+
- **GroupContext**: Multi-user group session management
70+
71+
### Live2D Integration
72+
- Uses Cubism SDK (WebSDK folder) for Live2D model rendering
73+
- **live2d.tsx**: Main Live2D component handling model loading, animation, and lip sync
74+
- Supports model switching, expressions, and motion playback
75+
- Audio-driven lip sync with volume-based animation
76+
77+
### Key Features
78+
- Real-time voice interaction with VAD (Voice Activity Detection)
79+
- WebSocket-based communication with backend AI server
80+
- Live2D character animation with expressions and lip sync
81+
- Multi-language support (i18n)
82+
- Group/collaborative sessions
83+
- Screen capture support
84+
- Customizable backgrounds and UI themes
85+
86+
## Important Notes
87+
- The app requires a backend server connection (WebSocket) for AI functionality
88+
- Live2D models are loaded from URLs provided by the backend
89+
- Audio is streamed as base64-encoded data with volume arrays for lip sync
90+
- The app uses Chakra UI v3 for the component library
91+
- ESLint is configured with relaxed rules (many checks disabled in .eslintrc.js)

src/main/index.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,18 @@ app.whenReady().then(() => {
9696
return false;
9797
});
9898

99-
// if (process.env.NODE_ENV === "development") {
100-
// globalShortcut.register("F12", () => {
101-
// const window = windowManager.getWindow();
102-
// if (!window) return;
103-
104-
// if (window.webContents.isDevToolsOpened()) {
105-
// window.webContents.closeDevTools();
106-
// } else {
107-
// window.webContents.openDevTools();
108-
// }
109-
// });
110-
// }
99+
if (process.env.NODE_ENV === "development") {
100+
globalShortcut.register("F12", () => {
101+
const window = windowManager.getWindow();
102+
if (!window) return;
103+
104+
if (window.webContents.isDevToolsOpened()) {
105+
window.webContents.closeDevTools();
106+
} else {
107+
window.webContents.openDevTools();
108+
}
109+
});
110+
}
111111

112112
setupIPC();
113113

src/renderer/WebSDK/src/lappdelegate.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export class LAppDelegate {
120120
this._resizeCanvas();
121121

122122
// Ensure view is properly initialized
123-
if (this._view) {
123+
if (this._view && canvas) {
124124
this._view.initialize();
125125
this._view.initializeSprite();
126126

@@ -336,9 +336,15 @@ export class LAppDelegate {
336336
* Resize the canvas to fill the screen.
337337
*/
338338
private _resizeCanvas(): void {
339-
canvas!.width = canvas!.clientWidth * window.devicePixelRatio;
340-
canvas!.height = canvas!.clientHeight * window.devicePixelRatio;
341-
gl!.viewport(0, 0, gl!.drawingBufferWidth, gl!.drawingBufferHeight);
339+
if (!canvas) {
340+
console.warn("Canvas is null, skipping resize");
341+
return;
342+
}
343+
canvas.width = canvas.clientWidth * window.devicePixelRatio;
344+
canvas.height = canvas.clientHeight * window.devicePixelRatio;
345+
if (gl) {
346+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
347+
}
342348
}
343349

344350
_cubismOption: Option; // Cubism SDK Option

src/renderer/WebSDK/src/lappglmanager.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ export class LAppGlManager {
4242
canvas = document.getElementById('canvas') as HTMLCanvasElement;
4343
// canvas = document.createElement("canvas");
4444

45+
if (!canvas) {
46+
console.warn("Canvas element not found during LAppGlManager initialization");
47+
return;
48+
}
49+
4550
gl = canvas.getContext("webgl2");
4651

4752
if (!gl) {

src/renderer/WebSDK/src/lappview.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export class LAppView {
4444
* 初期化する。
4545
*/
4646
public initialize(): void {
47-
const { width, height } = canvas!;
47+
if (!canvas) {
48+
console.warn("Canvas is null, cannot initialize LAppView");
49+
return;
50+
}
51+
const { width, height } = canvas;
4852

4953
const ratio: number = width / height;
5054
const left: number = -ratio;

src/renderer/src/context/live2d-config-context.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,36 +121,25 @@ export function Live2DConfigProvider({ children }: { children: React.ReactNode }
121121

122122
const setModelInfo = (info: ModelInfo | undefined) => {
123123
if (!info?.url) {
124-
setModelInfoState(undefined); // Clear state if no URL
125-
return;
126-
}
127-
128-
if (!confUid) {
129-
console.warn("Attempting to set model info without confUid");
130-
// toaster.create({
131-
// title: "Attempting to set model info without confUid",
132-
// type: "error",
133-
// duration: 2000,
134-
// });
124+
setModelInfoState(undefined);
135125
return;
136126
}
137127

138128
// Always use the scale defined in the incoming info object (from config)
139-
const finalScale = Number(info.kScale || 0.5) * 2; // Use default scale if kScale is missing
129+
const finalScale = Number(info.kScale || 0.5) * 2;
140130
console.log("Setting model info with default scale:", finalScale);
141131

142132
setModelInfoState({
143133
...info,
144134
kScale: finalScale,
145-
// Preserve other potentially user-modified settings if needed, otherwise use defaults from info
146135
pointerInteractive:
147136
"pointerInteractive" in info
148137
? info.pointerInteractive
149-
: (modelInfo?.pointerInteractive ?? true), // Or default to info.pointerInteractive
138+
: (modelInfo?.pointerInteractive ?? true),
150139
scrollToResize:
151140
"scrollToResize" in info
152141
? info.scrollToResize
153-
: (modelInfo?.scrollToResize ?? true), // Or default to info.scrollToResize
142+
: (modelInfo?.scrollToResize ?? true),
154143
});
155144
};
156145

src/renderer/src/hooks/canvas/use-live2d-resize.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ export const useLive2DResize = ({
248248
}
249249
}, []);
250250

251+
251252
// Monitor container size changes using ResizeObserver
252253
useEffect(() => {
253254
const containerElement = containerRef.current;

src/renderer/src/services/websocket-handler.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ function WebSocketHandler({ children }: { children: React.ReactNode }) {
4646
}, [autoStartMicOnConvEnd]);
4747

4848
useEffect(() => {
49-
if (pendingModelInfo) {
49+
if (pendingModelInfo && confUid) {
5050
setModelInfo(pendingModelInfo);
5151
setPendingModelInfo(undefined);
5252
}

0 commit comments

Comments
 (0)