logic fix: TTS
Browse files- services/geminiService.ts +21 -13
services/geminiService.ts
CHANGED
|
@@ -29,10 +29,10 @@ const ai = new GoogleGenAI({ apiKey: API_KEY });
|
|
| 29 |
|
| 30 |
// --- TIERED MODEL STRATEGY ---
|
| 31 |
// 1. Primary: Gemini 2.5 Flash (Highest Quality/Speed Balance)
|
| 32 |
-
// 2. Secondary: Gemini
|
| 33 |
// 3. Tertiary: Local/HuggingFace Backends (Privacy/Offline/No-Quota Fallback)
|
| 34 |
const MODEL_PRIMARY = 'gemini-2.5-flash';
|
| 35 |
-
const MODEL_SECONDARY = 'gemini-
|
| 36 |
const MODEL_TTS = 'gemini-2.5-flash-preview-tts';
|
| 37 |
|
| 38 |
// --- UTILITIES ---
|
|
@@ -108,7 +108,7 @@ const callBackend = async (baseUrl: string, endpoint: string, payload: any, onSt
|
|
| 108 |
if (typeof data === 'string') return data;
|
| 109 |
if (data.text) return data.text;
|
| 110 |
if (data.response) return data.response;
|
| 111 |
-
|
| 112 |
|
| 113 |
return JSON.stringify(data);
|
| 114 |
|
|
@@ -142,14 +142,13 @@ async function executePipeline<T>(
|
|
| 142 |
if (onStatus) onStatus("⚡ Using Gemini Flash...");
|
| 143 |
return await geminiTask(MODEL_PRIMARY);
|
| 144 |
} catch (error: any) {
|
| 145 |
-
// Check for Quota/Rate Limits
|
| 146 |
-
if (error.toString().includes('429') || error.toString().includes('Quota') || error.toString().includes('
|
| 147 |
try {
|
| 148 |
// 2. Secondary Model
|
| 149 |
if (onStatus) onStatus("⚠️ Quota limit. Switching to Flash-Lite...");
|
| 150 |
return await geminiTask(MODEL_SECONDARY);
|
| 151 |
} catch (secondaryError) {
|
| 152 |
-
// Fall through to backend
|
| 153 |
console.warn("Secondary model failed:", secondaryError);
|
| 154 |
}
|
| 155 |
}
|
|
@@ -210,13 +209,22 @@ export const generateSpeech = async (text: string): Promise<string | null> => {
|
|
| 210 |
return response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data || null;
|
| 211 |
};
|
| 212 |
|
| 213 |
-
// Fallback
|
| 214 |
-
//
|
|
|
|
| 215 |
const fallbackTask = async () => {
|
| 216 |
-
return
|
| 217 |
};
|
| 218 |
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
};
|
| 221 |
|
| 222 |
// --- STT (Transcription) ---
|
|
@@ -287,7 +295,7 @@ export const analyzeRisk = async (
|
|
| 287 |
});
|
| 288 |
|
| 289 |
const parsed = parseRiskResponse(response.text || "{}", calculatedScore);
|
| 290 |
-
return { ...parsed, source: model === MODEL_PRIMARY ? 'Gemini 2.5 Flash' : 'Gemini
|
| 291 |
};
|
| 292 |
|
| 293 |
const fallbackTask = async () => {
|
|
@@ -321,7 +329,7 @@ export const generateChatResponse = async (
|
|
| 321 |
contents.push({ role: 'user', parts: [{ text: context + "\nUser: " + currentMessage }, ...(image ? [{ inlineData: { mimeType: 'image/jpeg', data: image.split('base64,')[1] } }] : [])] });
|
| 322 |
|
| 323 |
const geminiTask = async (model: string) => {
|
| 324 |
-
onSource(model === MODEL_PRIMARY ? 'Gemini 2.5 Flash' : 'Gemini
|
| 325 |
const response = await ai.models.generateContent({
|
| 326 |
model: model,
|
| 327 |
contents: contents,
|
|
@@ -344,7 +352,7 @@ export const generateChatResponse = async (
|
|
| 344 |
const parseRiskResponse = (text: string, calculatedScore: number): RiskAnalysisResult => {
|
| 345 |
try {
|
| 346 |
let jsonStr = text;
|
| 347 |
-
// Clean markdown code blocks
|
| 348 |
jsonStr = jsonStr.replace(/```json/g, '').replace(/```/g, '');
|
| 349 |
const data = JSON.parse(jsonStr);
|
| 350 |
|
|
|
|
| 29 |
|
| 30 |
// --- TIERED MODEL STRATEGY ---
|
| 31 |
// 1. Primary: Gemini 2.5 Flash (Highest Quality/Speed Balance)
|
| 32 |
+
// 2. Secondary: Gemini Flash Lite (Quota Rescue)
|
| 33 |
// 3. Tertiary: Local/HuggingFace Backends (Privacy/Offline/No-Quota Fallback)
|
| 34 |
const MODEL_PRIMARY = 'gemini-2.5-flash';
|
| 35 |
+
const MODEL_SECONDARY = 'gemini-flash-lite-latest';
|
| 36 |
const MODEL_TTS = 'gemini-2.5-flash-preview-tts';
|
| 37 |
|
| 38 |
// --- UTILITIES ---
|
|
|
|
| 108 |
if (typeof data === 'string') return data;
|
| 109 |
if (data.text) return data.text;
|
| 110 |
if (data.response) return data.response;
|
| 111 |
+
// Note: Backend does not support TTS, so we don't check for audio here.
|
| 112 |
|
| 113 |
return JSON.stringify(data);
|
| 114 |
|
|
|
|
| 142 |
if (onStatus) onStatus("⚡ Using Gemini Flash...");
|
| 143 |
return await geminiTask(MODEL_PRIMARY);
|
| 144 |
} catch (error: any) {
|
| 145 |
+
// Check for Quota/Rate Limits or Model Overload
|
| 146 |
+
if (error.toString().includes('429') || error.toString().includes('Quota') || error.toString().includes('503')) {
|
| 147 |
try {
|
| 148 |
// 2. Secondary Model
|
| 149 |
if (onStatus) onStatus("⚠️ Quota limit. Switching to Flash-Lite...");
|
| 150 |
return await geminiTask(MODEL_SECONDARY);
|
| 151 |
} catch (secondaryError) {
|
|
|
|
| 152 |
console.warn("Secondary model failed:", secondaryError);
|
| 153 |
}
|
| 154 |
}
|
|
|
|
| 209 |
return response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data || null;
|
| 210 |
};
|
| 211 |
|
| 212 |
+
// Fallback: Return NULL.
|
| 213 |
+
// The Frontend (Chat.tsx) will detect NULL and use `window.speechSynthesis` (Browser Native TTS).
|
| 214 |
+
// The backend does not have a /tts endpoint.
|
| 215 |
const fallbackTask = async () => {
|
| 216 |
+
return null;
|
| 217 |
};
|
| 218 |
|
| 219 |
+
// We manually handle pipeline here to ensure fallback returns null instead of throwing
|
| 220 |
+
if (API_KEY) {
|
| 221 |
+
try {
|
| 222 |
+
return await geminiTask();
|
| 223 |
+
} catch (e) {
|
| 224 |
+
// Fallthrough
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
return await fallbackTask();
|
| 228 |
};
|
| 229 |
|
| 230 |
// --- STT (Transcription) ---
|
|
|
|
| 295 |
});
|
| 296 |
|
| 297 |
const parsed = parseRiskResponse(response.text || "{}", calculatedScore);
|
| 298 |
+
return { ...parsed, source: model === MODEL_PRIMARY ? 'Gemini 2.5 Flash' : 'Gemini Flash Lite' };
|
| 299 |
};
|
| 300 |
|
| 301 |
const fallbackTask = async () => {
|
|
|
|
| 329 |
contents.push({ role: 'user', parts: [{ text: context + "\nUser: " + currentMessage }, ...(image ? [{ inlineData: { mimeType: 'image/jpeg', data: image.split('base64,')[1] } }] : [])] });
|
| 330 |
|
| 331 |
const geminiTask = async (model: string) => {
|
| 332 |
+
onSource(model === MODEL_PRIMARY ? 'Gemini 2.5 Flash' : 'Gemini Flash Lite');
|
| 333 |
const response = await ai.models.generateContent({
|
| 334 |
model: model,
|
| 335 |
contents: contents,
|
|
|
|
| 352 |
const parseRiskResponse = (text: string, calculatedScore: number): RiskAnalysisResult => {
|
| 353 |
try {
|
| 354 |
let jsonStr = text;
|
| 355 |
+
// Clean markdown code blocks if any
|
| 356 |
jsonStr = jsonStr.replace(/```json/g, '').replace(/```/g, '');
|
| 357 |
const data = JSON.parse(jsonStr);
|
| 358 |
|