Files
ai-app-skg/services/geminiService.ts
2025-12-24 14:24:01 +08:00

294 lines
9.0 KiB
TypeScript

import { GoogleGenAI, Modality, Type } from "@google/genai";
import { ChatMode, Message, ChatScenario, AppLanguage } from "../types";
import { TRANSLATIONS, DEFAULT_LANGUAGE } from "../constants";
// 获取 Gemini 客户端实例
const getClient = () => {
// 优先从 LocalStorage 获取用户配置的 API Key
const storedSettings = localStorage.getItem("sociopal_settings");
const settings = storedSettings ? JSON.parse(storedSettings) : {};
const apiKey = settings.apiKey || process.env.API_KEY;
if (!apiKey) {
throw new Error("API Key 未配置。请在设置中输入您的 API Key。");
}
// Create a new GoogleGenAI instance right before making an API call
return new GoogleGenAI({ apiKey });
};
// --- 模型定义 ---
export const MODEL_CHAT_STANDARD = "gemini-3-flash-preview";
export const MODEL_CHAT_DEEP = "gemini-3-pro-preview";
export const MODEL_CHAT_FAST = "gemini-flash-lite-latest";
export const MODEL_IMAGE_GEN = "gemini-3-pro-image-preview";
export const MODEL_VIDEO_GEN = "veo-3.1-fast-generate-preview";
export const MODEL_TTS = "gemini-2.5-flash-preview-tts";
// 获取当前模式对应的模型名称
export const getModelNameForMode = (mode: ChatMode): string => {
switch (mode) {
case ChatMode.DEEP: return MODEL_CHAT_DEEP;
case ChatMode.FAST: return MODEL_CHAT_FAST;
case ChatMode.STANDARD:
default:
return MODEL_CHAT_STANDARD;
}
};
// 格式化模型名称用于展示
export const formatModelName = (modelId: string): string => {
if (modelId === MODEL_CHAT_STANDARD) return "Gemini 3 Flash";
if (modelId === MODEL_CHAT_DEEP) return "Gemini 3 Pro";
if (modelId === MODEL_CHAT_FAST) return "Gemini Flash Lite";
if (modelId === MODEL_IMAGE_GEN) return "Gemini 3 Pro Image";
if (modelId === MODEL_VIDEO_GEN) return "Veo 3.1";
return modelId;
};
// --- 聊天功能 ---
export const streamChatResponse = async (
history: Message[],
currentMessage: string,
mode: ChatMode,
language: AppLanguage,
scenario: ChatScenario = ChatScenario.GENERAL,
attachments: { mimeType: string; data: string }[] = [],
onChunk: (text: string, grounding?: any) => void
) => {
const ai = getClient();
let model = getModelNameForMode(mode);
// 根据场景构造系统指令
let baseInstruction = "";
switch (scenario) {
case ChatScenario.READING:
baseInstruction = `You are a distinguished Sociology Professor specializing in Classical Sociological Theory.
Focus on the works of Marx, Weber, Durkheim, Simmel, and other foundational figures.
When answering:
1. Contextualize the text historically.
2. Explain key arguments precisely.
3. Discuss the critical reception and legacy.
4. Use academic yet accessible language.`;
break;
case ChatScenario.CONCEPT:
baseInstruction = `You are an expert Sociological Concept Analyst.
Your goal is to provide deep, multi-dimensional definitions of sociological terms.
When defining a concept:
1. Provide a clear, concise definition.
2. Explain its etymology or theoretical origin.
3. Contrast it with related or opposing concepts.
4. Provide concrete examples of the concept in action.`;
break;
case ChatScenario.RESEARCH:
baseInstruction = `You are a Senior Research Methodology Consultant.
You help students and researchers design their studies.
Focus on:
1. Refining research questions.
2. Suggesting appropriate methods (Qualitative, Quantitative, Mixed).
3. Discussing sampling, operationalization, and ethics.
4. Suggesting theoretical frameworks suitable for the topic.`;
break;
case ChatScenario.GENERAL:
default:
baseInstruction = `You are a helpful and knowledgeable Sociology Learning Assistant.
Answer questions clearly using sociological perspectives.
Encourage critical thinking and connect daily life examples to sociological theories.`;
break;
}
let config: any = {
systemInstruction: `${baseInstruction} Always reply in the user's preferred language: ${language}.`,
};
// 根据模式配置参数
if (mode === ChatMode.STANDARD) {
config.tools = [{ googleSearch: {} }];
} else if (mode === ChatMode.DEEP) {
config.thinkingConfig = { thinkingBudget: 32768 }; // Pro 模型最大思考预算
}
// Fast 模式仅切换模型,无需额外配置
const chat = ai.chats.create({
model,
config,
history: history.slice(0, -1).map(m => ({
role: m.role,
parts: [
{ text: m.content },
...(m.attachments || []).map(a => ({
inlineData: { mimeType: a.mimeType, data: a.data }
}))
]
}))
});
const parts: any[] = [{ text: currentMessage }];
attachments.forEach(att => {
parts.push({ inlineData: { mimeType: att.mimeType, data: att.data } });
});
try {
const result = await chat.sendMessageStream({
message: parts
});
for await (const chunk of result) {
const text = chunk.text;
const grounding = chunk.candidates?.[0]?.groundingMetadata;
if (text || grounding) {
onChunk(text || '', grounding);
}
}
} catch (e) {
console.error("Chat error", e);
throw e;
}
};
// --- 图像生成 ---
export const generateImage = async (
prompt: string,
size: "1K" | "2K" | "4K"
): Promise<string[]> => {
const ai = getClient();
const response = await ai.models.generateContent({
model: MODEL_IMAGE_GEN,
contents: { parts: [{ text: prompt }] },
config: {
imageConfig: {
imageSize: size,
}
}
});
const images: string[] = [];
if (response.candidates?.[0]?.content?.parts) {
for (const part of response.candidates[0].content.parts) {
if (part.inlineData && part.inlineData.data) {
images.push(`data:${part.inlineData.mimeType};base64,${part.inlineData.data}`);
}
}
}
return images;
};
// --- 视频生成 ---
export const generateVideo = async (
prompt: string,
aspectRatio: "16:9" | "9:16"
): Promise<string> => {
const ai = getClient();
let operation = await ai.models.generateVideos({
model: MODEL_VIDEO_GEN,
prompt: prompt,
config: {
numberOfVideos: 1,
aspectRatio: aspectRatio,
resolution: '720p',
}
});
while (!operation.done) {
await new Promise(resolve => setTimeout(resolve, 5000));
operation = await ai.operations.getVideosOperation({ operation: operation });
}
const uri = operation.response?.generatedVideos?.[0]?.video?.uri;
if (!uri) throw new Error("No video URI returned");
// Fetch with explicit key, prioritizing local setting
const storedSettings = localStorage.getItem("sociopal_settings");
const settings = storedSettings ? JSON.parse(storedSettings) : {};
const apiKey = settings.apiKey || process.env.API_KEY;
const fetchResponse = await fetch(`${uri}&key=${apiKey}`);
const blob = await fetchResponse.blob();
return URL.createObjectURL(blob);
};
// --- 语音转录 ---
export const transcribeAudio = async (
audioBase64: string,
mimeType: string,
language: AppLanguage
): Promise<string> => {
const ai = getClient();
const t = TRANSLATIONS[language] || TRANSLATIONS[DEFAULT_LANGUAGE];
const response = await ai.models.generateContent({
model: MODEL_CHAT_STANDARD,
contents: {
parts: [
{ inlineData: { mimeType, data: audioBase64 } },
{ text: t.transcribePrompt }
]
}
});
return response.text || "";
};
// --- 语音合成 ---
export const generateSpeech = async (
text: string
): Promise<AudioBuffer> => {
const ai = getClient();
const response = await ai.models.generateContent({
model: MODEL_TTS,
contents: { parts: [{ text }] },
config: {
responseModalities: [Modality.AUDIO],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: 'Kore' },
},
},
},
});
const base64Audio = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
if (!base64Audio) throw new Error("No audio generated");
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)({sampleRate: 24000});
const audioBuffer = await decodeAudioData(
decode(base64Audio),
audioContext,
24000,
1
);
return audioBuffer;
};
// 辅助函数
function decode(base64: string) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
async function decodeAudioData(
data: Uint8Array,
ctx: AudioContext,
sampleRate: number,
numChannels: number,
): Promise<AudioBuffer> {
const dataInt16 = new Int16Array(data.buffer);
const frameCount = dataInt16.length / numChannels;
const buffer = ctx.createBuffer(numChannels, frameCount, sampleRate);
for (let channel = 0; channel < numChannels; channel++) {
const channelData = buffer.getChannelData(channel);
for (let i = 0; i < frameCount; i++) {
channelData[i] = dataInt16[i * numChannels + channel] / 32768.0;
}
}
return buffer;
}