Add support for image input in model resources and enhance related configurations

- Introduce a new `support_image_input` field in model resources, allowing models to indicate support for image input.
- Update the backend models, schemas, and database seed scripts to accommodate the new field.
- Enhance the AssistantConfig and related routes to handle image input capabilities, ensuring proper validation and error handling.
- Modify the frontend components to include toggles for enabling visual understanding and filtering models based on image input support.
- Implement necessary adjustments in the voice preview and pipeline to integrate video stream handling alongside audio functionalities.
This commit is contained in:
Xin Wang
2026-07-07 19:41:45 +08:00
parent 32a52d318a
commit c5db918830
18 changed files with 411 additions and 116 deletions
+10 -8
View File
@@ -3,9 +3,8 @@
/**
* 摄像头预览:纯前端 getUserMedia 视频流,供调试抽屉的「视频流」视图使用。
*
* 与 use-voice-preview 分离:麦克风/语音会话走 WebRTC 到后端,摄像头目前只在
* 本地预览。后续接入视觉理解管线时,把这条 video track 加进现有 PeerConnection
* (pc.addTrack)并在后端 transport 打开 video_in_enabled 即可。
* 与 use-voice-preview 分离:这里负责摄像头设备枚举与本地预览;开始视觉会话时,
* 调用方会把这条 video track 加进语音 WebRTC PeerConnection。
*/
import { useCallback, useEffect, useRef, useState } from "react";
@@ -50,12 +49,12 @@ export function useCameraPreview() {
setStream(null);
}, []);
const start = useCallback(async () => {
if (streamRef.current) return;
const start = useCallback(async (): Promise<MediaStream | null> => {
if (streamRef.current) return streamRef.current;
setError(null);
if (!window.isSecureContext || !navigator.mediaDevices?.getUserMedia) {
setError("当前页面无法访问摄像头,请通过 https 或 localhost 访问。");
return;
return null;
}
setStarting(true);
try {
@@ -66,21 +65,24 @@ export function useCameraPreview() {
streamRef.current = media;
setStream(media);
void refreshCameras();
return media;
} catch (mediaError) {
setError(cameraErrorMessage(mediaError));
return null;
} finally {
setStarting(false);
}
}, [refreshCameras]);
const selectCamera = useCallback(
async (id: string) => {
async (id: string): Promise<MediaStream | null> => {
deviceIdRef.current = id;
setDeviceId(id);
if (streamRef.current) {
stop();
await start();
return await start();
}
return null;
},
[start, stop],
);
+24 -1
View File
@@ -23,6 +23,11 @@ import { API_BASE, webrtcApi } from "@/lib/api";
export type VoicePreviewStatus = "idle" | "connecting" | "connected" | "failed";
type ConnectOptions = {
visionEnabled?: boolean;
videoStream?: MediaStream | null;
};
export type ChatMessage = {
id: string;
role: "user" | "assistant";
@@ -218,7 +223,7 @@ export function useVoicePreview(
[disconnect, fail],
);
const connect = useCallback(async () => {
const connect = useCallback(async (options: ConnectOptions = {}) => {
if (startingRef.current || pcRef.current || wsRef.current) return;
if (!assistantId) {
setError("请先保存助手,再开始语音预览。");
@@ -456,6 +461,11 @@ export function useVoicePreview(
} else {
pc.addTransceiver("audio", { direction: "recvonly" });
}
if (options.videoStream) {
options.videoStream
.getVideoTracks()
.forEach((track) => pc.addTrack(track, options.videoStream!));
}
// 4) 生成 offer 并发给后端(assistant_id 在 payload 顶层)
const offer = await pc.createOffer();
@@ -472,6 +482,7 @@ export function useVoicePreview(
sdp: localDescription.sdp,
type: localDescription.type,
assistant_id: assistantId,
vision_enabled: Boolean(options.visionEnabled),
},
}),
);
@@ -482,6 +493,17 @@ export function useVoicePreview(
}
}, [assistantId, fail, closeOnRemoteEnd, refreshDevices]);
const replaceVideoStream = useCallback(
async (videoStream: MediaStream | null) => {
const pc = pcRef.current;
if (!pc) return;
const sender = pc.getSenders().find((s) => s.track?.kind === "video");
if (!sender) return;
await sender.replaceTrack(videoStream?.getVideoTracks()[0] ?? null);
},
[],
);
// 选择麦克风:更新选择;若会话正在发送麦克风音频,则用 WebRTC replaceTrack
// 热切换轨道(无需重新协商),并把波形可视化重新接到新流。
// 未连接时仅记下选择,留待下次 connect 生效。
@@ -558,6 +580,7 @@ export function useVoicePreview(
selectDevice,
sendText,
connect,
replaceVideoStream,
disconnect,
audioRef,
};