Spaces:
Running
Running
Delete services
Browse files- services/adminService.js +0 -64
- services/ai.js +0 -2
- services/ai/bookRecap.js +0 -46
- services/ai/comicTranslator.js +0 -68
- services/ai/creator.js +0 -54
- services/ai/index.js +0 -23
- services/ai/recap.js +0 -32
- services/ai/srtTranslate.js +0 -82
- services/ai/subtitle.js +0 -53
- services/ai/transcribe.js +0 -30
- services/ai/translate.js +0 -34
- services/ai/tts.js +0 -43
- services/ai/utils.js +0 -73
- services/authService.js +0 -39
- services/credit.js +0 -123
- services/creditService.js +0 -153
- services/firebase.js +0 -36
services/adminService.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { db } from '@/backend/services/firebase';
|
| 3 |
-
|
| 4 |
-
export const AdminService = {
|
| 5 |
-
async updateSettings(settings) {
|
| 6 |
-
await db.ref('system/settings').update(settings);
|
| 7 |
-
return { success: true };
|
| 8 |
-
},
|
| 9 |
-
|
| 10 |
-
async addUser(userData, referrerCode) {
|
| 11 |
-
const membershipId = userData.Active_Session_ID;
|
| 12 |
-
|
| 13 |
-
const userPayload = {
|
| 14 |
-
...userData,
|
| 15 |
-
Login_Key: membershipId,
|
| 16 |
-
Active_Session_ID: membershipId,
|
| 17 |
-
Referrer: referrerCode || "NONE",
|
| 18 |
-
Status: 'ACTIVE',
|
| 19 |
-
Usage: {},
|
| 20 |
-
Last_Usage_Date: new Date().toISOString().split('T')[0]
|
| 21 |
-
};
|
| 22 |
-
|
| 23 |
-
await db.ref(`users/${Date.now()}`).set(userPayload);
|
| 24 |
-
return { success: true };
|
| 25 |
-
},
|
| 26 |
-
|
| 27 |
-
async reactivateUser(nodeKey, userClass, credits) {
|
| 28 |
-
const today = new Date();
|
| 29 |
-
const expDate = new Date(today.getTime() + 30 * 86400000).toISOString().split('T')[0];
|
| 30 |
-
|
| 31 |
-
const updates = {
|
| 32 |
-
Class: userClass,
|
| 33 |
-
Credits: credits,
|
| 34 |
-
Expired_Date: expDate,
|
| 35 |
-
Status: 'ACTIVE'
|
| 36 |
-
};
|
| 37 |
-
|
| 38 |
-
await db.ref(`users/${nodeKey}`).update(updates);
|
| 39 |
-
return { success: true };
|
| 40 |
-
},
|
| 41 |
-
|
| 42 |
-
async updateUserClass(nodeKey, userClass, credits) {
|
| 43 |
-
const updates = {
|
| 44 |
-
Class: userClass,
|
| 45 |
-
Credits: credits
|
| 46 |
-
};
|
| 47 |
-
await db.ref(`users/${nodeKey}`).update(updates);
|
| 48 |
-
return { success: true };
|
| 49 |
-
},
|
| 50 |
-
|
| 51 |
-
async topUpCredits(sessionId, amount) {
|
| 52 |
-
const snapshot = await db.ref('users').orderByChild('Active_Session_ID').equalTo(sessionId).once('value');
|
| 53 |
-
if (!snapshot.exists()) throw new Error("User not found");
|
| 54 |
-
const userKey = Object.keys(snapshot.val())[0];
|
| 55 |
-
const currentCredits = snapshot.val()[userKey].Credits || 0;
|
| 56 |
-
await db.ref(`users/${userKey}`).update({ Credits: currentCredits + amount });
|
| 57 |
-
return { success: true };
|
| 58 |
-
},
|
| 59 |
-
|
| 60 |
-
async deleteUser(nodeKey) {
|
| 61 |
-
await db.ref(`users/${nodeKey}`).remove();
|
| 62 |
-
return { success: true };
|
| 63 |
-
}
|
| 64 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
export { AIService } from '@/backend/services/ai/index';
|
|
|
|
|
|
|
|
|
services/ai/bookRecap.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 3 |
-
|
| 4 |
-
export async function bookRecap(media, mimeType, targetLanguage, apiKey, isOwnApi = false) {
|
| 5 |
-
const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
|
| 6 |
-
const isBurmese = targetLanguage.toLowerCase().includes('burm') || targetLanguage.includes('မြန်မာ');
|
| 7 |
-
|
| 8 |
-
// Clean language name in case any legacy strings remain
|
| 9 |
-
const cleanLanguage = targetLanguage.split(' (')[0];
|
| 10 |
-
|
| 11 |
-
let burmeseRules = isBurmese ? getPrompt('burmese_rules.txt') : "";
|
| 12 |
-
let template = getPrompt('book_recap.txt');
|
| 13 |
-
|
| 14 |
-
const authorPersona = isBurmese
|
| 15 |
-
? `You are a world-class Literary Translator and Novelist. Your task is to translate the provided text into beautiful, natural Burmese.`
|
| 16 |
-
: `You are a professional Book Translator and Author. Provide a complete, high-quality translation of this document into ${cleanLanguage}.`;
|
| 17 |
-
|
| 18 |
-
let finalPrompt = template
|
| 19 |
-
.replace('{{targetLanguage}}', cleanLanguage)
|
| 20 |
-
.replace('{{burmeseRules}}', burmeseRules)
|
| 21 |
-
.replace('{{authorPersona}}', authorPersona);
|
| 22 |
-
|
| 23 |
-
return await tryModels(apiKey, models, async (ai, model) => {
|
| 24 |
-
const response = await ai.models.generateContent({
|
| 25 |
-
model: model,
|
| 26 |
-
contents: {
|
| 27 |
-
parts: [
|
| 28 |
-
{ inlineData: { data: media, mimeType } },
|
| 29 |
-
{ text: `Translate this entire document COMPLETELY (100% length) into ${cleanLanguage}. Do not summarize or skip any content.` }
|
| 30 |
-
]
|
| 31 |
-
},
|
| 32 |
-
config: {
|
| 33 |
-
temperature: 0.3,
|
| 34 |
-
systemInstruction: finalPrompt,
|
| 35 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
| 36 |
-
thinkingConfig: { thinkingBudget: 0 }
|
| 37 |
-
}
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
const text = response.text;
|
| 41 |
-
if (!text || text.trim().length < 50) {
|
| 42 |
-
throw new Error("MODEL_FAILED_TO_TRANSLATE_DOCUMENT");
|
| 43 |
-
}
|
| 44 |
-
return text;
|
| 45 |
-
});
|
| 46 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/comicTranslator.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { Type } from '@google/genai';
|
| 3 |
-
import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS, cleanJson } from '@/backend/services/ai/utils';
|
| 4 |
-
|
| 5 |
-
export async function comicTranslate(media, mimeType, targetLanguage, apiKey, isOwnApi = false) {
|
| 6 |
-
const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
|
| 7 |
-
const isBurmese = targetLanguage.toLowerCase().includes('burm') || targetLanguage.includes('မြန်မာ');
|
| 8 |
-
|
| 9 |
-
let burmeseRules = isBurmese ? getPrompt('burmese_rules.txt') : "";
|
| 10 |
-
let template = getPrompt('comic_translator.txt');
|
| 11 |
-
|
| 12 |
-
let finalPrompt = template
|
| 13 |
-
.replace('{{targetLanguage}}', targetLanguage)
|
| 14 |
-
.replace('{{burmeseRules}}', burmeseRules);
|
| 15 |
-
|
| 16 |
-
// AI identifies text locations and translations
|
| 17 |
-
return await tryModels(apiKey, models, async (ai, model) => {
|
| 18 |
-
const response = await ai.models.generateContent({
|
| 19 |
-
model: model,
|
| 20 |
-
contents: {
|
| 21 |
-
parts: [
|
| 22 |
-
{ inlineData: { data: media, mimeType } },
|
| 23 |
-
{ text: "TASK: Process this document page by page. For each page, identify all text bubbles. Provide their [ymin, xmin, ymax, xmax] coordinates and the translated text in " + targetLanguage + ". Output ONLY valid JSON." }
|
| 24 |
-
]
|
| 25 |
-
},
|
| 26 |
-
config: {
|
| 27 |
-
temperature: 0.1,
|
| 28 |
-
systemInstruction: finalPrompt,
|
| 29 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
| 30 |
-
responseMimeType: "application/json",
|
| 31 |
-
responseSchema: {
|
| 32 |
-
type: Type.OBJECT,
|
| 33 |
-
properties: {
|
| 34 |
-
pages: {
|
| 35 |
-
type: Type.ARRAY,
|
| 36 |
-
items: {
|
| 37 |
-
type: Type.OBJECT,
|
| 38 |
-
properties: {
|
| 39 |
-
page_number: { type: Type.INTEGER },
|
| 40 |
-
text_blocks: {
|
| 41 |
-
type: Type.ARRAY,
|
| 42 |
-
items: {
|
| 43 |
-
type: Type.OBJECT,
|
| 44 |
-
properties: {
|
| 45 |
-
translated_text: { type: Type.STRING },
|
| 46 |
-
box_2d: {
|
| 47 |
-
type: Type.ARRAY,
|
| 48 |
-
items: { type: Type.NUMBER },
|
| 49 |
-
description: "[ymin, xmin, ymax, xmax] coordinates normalized 0-1000"
|
| 50 |
-
},
|
| 51 |
-
background_color: { type: Type.STRING }
|
| 52 |
-
}
|
| 53 |
-
}
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
-
}
|
| 57 |
-
}
|
| 58 |
-
},
|
| 59 |
-
required: ['pages']
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
});
|
| 63 |
-
|
| 64 |
-
// The backend server will receive this JSON and perform the heavy image manipulation
|
| 65 |
-
// returning a final processed URL or Base64 to the client.
|
| 66 |
-
return JSON.parse(cleanJson(response.text));
|
| 67 |
-
});
|
| 68 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/creator.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 3 |
-
|
| 4 |
-
export async function contentCreator(topic, category, subTopics, contentType, gender, targetLang, apiKey, isOwnApi = false) {
|
| 5 |
-
const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
|
| 6 |
-
const isBurmese = targetLang.toLowerCase().includes('burm') || targetLang.includes('မြန်မာ');
|
| 7 |
-
let burmeseRules = "";
|
| 8 |
-
if (isBurmese) {
|
| 9 |
-
const pronoun = gender === 'male' ? "ကျွန်တော်" : "ကျွန်မ";
|
| 10 |
-
burmeseRules = getPrompt('creator_burmese_rules.txt').replace('{{pronoun}}', pronoun).replace('{{topic}}', topic).replace('{{category}}', category).replace('{{subTopics}}', subTopics.join(', '));
|
| 11 |
-
}
|
| 12 |
-
const template = getPrompt('content_creator.txt');
|
| 13 |
-
const finalPrompt = template.replace('{{contentType}}', contentType).replace('{{topic}}', topic).replace('{{category}}', category).replace('{{targetLang}}', targetLang).replace('{{burmeseRules}}', burmeseRules);
|
| 14 |
-
return await tryModels(apiKey, models, async (ai, model) => {
|
| 15 |
-
const response = await ai.models.generateContent({
|
| 16 |
-
model,
|
| 17 |
-
contents: [{ parts: [{ text: `Topic: ${topic}. Category: ${category}.` }] }],
|
| 18 |
-
config: { temperature: 0.8, systemInstruction: finalPrompt, safetySettings: DEFAULT_SAFETY_SETTINGS }
|
| 19 |
-
});
|
| 20 |
-
return response.text;
|
| 21 |
-
});
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
export async function generateImage(prompt, apiKey, isOwnApi = false) {
|
| 25 |
-
const models = ['gemini-2.5-flash-image', 'gemini-3-pro-image-preview'];
|
| 26 |
-
|
| 27 |
-
// Improved visual prompt for better generation
|
| 28 |
-
const visualPrompt = `High-quality cinematic illustrative 3D character design or scene showing: ${prompt}. Vivid colors, detailed environment, 8k resolution style.`;
|
| 29 |
-
|
| 30 |
-
return await tryModels(apiKey, models, async (ai, model) => {
|
| 31 |
-
const response = await ai.models.generateContent({
|
| 32 |
-
model: model,
|
| 33 |
-
contents: {
|
| 34 |
-
parts: [
|
| 35 |
-
{ text: visualPrompt }
|
| 36 |
-
]
|
| 37 |
-
},
|
| 38 |
-
config: {
|
| 39 |
-
imageConfig: { aspectRatio: "1:1" }
|
| 40 |
-
}
|
| 41 |
-
});
|
| 42 |
-
|
| 43 |
-
// Thoroughly check all candidates and parts for inlineData
|
| 44 |
-
for (const candidate of response.candidates || []) {
|
| 45 |
-
for (const part of candidate.content?.parts || []) {
|
| 46 |
-
if (part.inlineData) {
|
| 47 |
-
return `data:image/png;base64,${part.inlineData.data}`;
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
throw new Error("EMPTY_IMAGE_DATA_RESPONSE");
|
| 53 |
-
});
|
| 54 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/index.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { transcribe } from '@/backend/services/ai/transcribe';
|
| 3 |
-
import { recap } from '@/backend/services/ai/recap';
|
| 4 |
-
import { bookRecap } from '@/backend/services/ai/bookRecap';
|
| 5 |
-
import { comicTranslate } from '@/backend/services/ai/comicTranslator';
|
| 6 |
-
import { translate } from '@/backend/services/ai/translate';
|
| 7 |
-
import { srtTranslate } from '@/backend/services/ai/srtTranslate';
|
| 8 |
-
import { tts } from '@/backend/services/ai/tts';
|
| 9 |
-
import { subtitle } from '@/backend/services/ai/subtitle';
|
| 10 |
-
import { contentCreator, generateImage } from '@/backend/services/ai/creator';
|
| 11 |
-
|
| 12 |
-
export const AIService = {
|
| 13 |
-
transcribe,
|
| 14 |
-
recap,
|
| 15 |
-
bookRecap,
|
| 16 |
-
comicTranslate,
|
| 17 |
-
translate,
|
| 18 |
-
srtTranslate,
|
| 19 |
-
tts,
|
| 20 |
-
subtitle,
|
| 21 |
-
contentCreator,
|
| 22 |
-
generateImage
|
| 23 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/recap.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 3 |
-
|
| 4 |
-
export async function recap(media, mimeType, targetLanguage, apiKey, isOwnApi = false) {
|
| 5 |
-
// UPDATED: Aligned with the 'no Pro' preference
|
| 6 |
-
const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
|
| 7 |
-
const template = getPrompt('recap.txt');
|
| 8 |
-
const finalPrompt = template.replace('{{targetLanguage}}', targetLanguage);
|
| 9 |
-
return await tryModels(apiKey, models, async (ai, model) => {
|
| 10 |
-
const response = await ai.models.generateContent({
|
| 11 |
-
model: model,
|
| 12 |
-
contents: {
|
| 13 |
-
parts: [
|
| 14 |
-
{ inlineData: { data: media, mimeType } },
|
| 15 |
-
{ text: "Narrate an extremely detailed cinematic recap." }
|
| 16 |
-
]
|
| 17 |
-
},
|
| 18 |
-
config: {
|
| 19 |
-
temperature: 0.4,
|
| 20 |
-
systemInstruction: finalPrompt,
|
| 21 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
| 22 |
-
thinkingConfig: { thinkingBudget: 0 }
|
| 23 |
-
}
|
| 24 |
-
});
|
| 25 |
-
|
| 26 |
-
const text = response.text;
|
| 27 |
-
if (!text || text.trim().length < 10) {
|
| 28 |
-
throw new Error("MODEL_FAILED_TO_GENERATE_RECAP");
|
| 29 |
-
}
|
| 30 |
-
return text;
|
| 31 |
-
});
|
| 32 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/srtTranslate.js
DELETED
|
@@ -1,82 +0,0 @@
|
|
| 1 |
-
import { tryModels, getPrompt, cleanSRTOutput, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 2 |
-
|
| 3 |
-
/**
|
| 4 |
-
* Counts the number of SRT blocks in a given text.
|
| 5 |
-
*/
|
| 6 |
-
function countSrtBlocks(text) {
|
| 7 |
-
if (!text) return 0;
|
| 8 |
-
// Improved regex to count standard SRT blocks accurately
|
| 9 |
-
const matches = text.match(/^\d+\s*\r?\n\d{2}:\d{2}:\d{2},\d{3}/gm);
|
| 10 |
-
return matches ? matches.length : 0;
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
export async function srtTranslate(srtContent, sourceLanguage, targetLanguage, apiKey, isOwnApi = false) {
|
| 14 |
-
const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
|
| 15 |
-
const template = getPrompt('srt_translation.txt');
|
| 16 |
-
const finalPrompt = template.replace('{{sourceLanguage}}', sourceLanguage).replace(/{{targetLanguage}}/g, targetLanguage);
|
| 17 |
-
|
| 18 |
-
// Normalize line endings and split into blocks
|
| 19 |
-
const blocks = srtContent.replace(/\r\n/g, '\n').split(/\n\s*\n/).filter(b => b.trim().length > 0);
|
| 20 |
-
|
| 21 |
-
const CHUNK_SIZE = 120; // Increased to 120 as requested for higher throughput
|
| 22 |
-
const BATCH_SIZE = 2; // Enabled Parallel processing (2 at a time)
|
| 23 |
-
const COOLDOWN = 1000; // Minimal cooldown between parallel batches
|
| 24 |
-
|
| 25 |
-
console.log(`[AI-SRT-SAFE-MAX] Segment Blocks: ${blocks.length}. Chunks: ${Math.ceil(blocks.length/CHUNK_SIZE)}.`);
|
| 26 |
-
|
| 27 |
-
const chunkTexts = [];
|
| 28 |
-
const chunkBlockCounts = [];
|
| 29 |
-
for (let i = 0; i < blocks.length; i += CHUNK_SIZE) {
|
| 30 |
-
const slice = blocks.slice(i, i + CHUNK_SIZE);
|
| 31 |
-
chunkTexts.push(slice.join('\n\n'));
|
| 32 |
-
chunkBlockCounts.push(slice.length);
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
const results = [];
|
| 36 |
-
for (let i = 0; i < chunkTexts.length; i += BATCH_SIZE) {
|
| 37 |
-
const currentBatchIndices = Array.from({ length: Math.min(BATCH_SIZE, chunkTexts.length - i) }, (_, k) => i + k);
|
| 38 |
-
|
| 39 |
-
const batchPromises = currentBatchIndices.map(idx =>
|
| 40 |
-
tryModels(apiKey, models, async (ai, model) => {
|
| 41 |
-
const inputText = chunkTexts[idx];
|
| 42 |
-
const expectedCount = chunkBlockCounts[idx];
|
| 43 |
-
|
| 44 |
-
let response = await ai.models.generateContent({
|
| 45 |
-
model: model,
|
| 46 |
-
contents: [{ parts: [{ text: `Translate these ${expectedCount} blocks now. Maintain all IDs:\n\n${inputText}` }] }],
|
| 47 |
-
config: {
|
| 48 |
-
temperature: 0.1,
|
| 49 |
-
systemInstruction: finalPrompt,
|
| 50 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
| 51 |
-
thinkingConfig: { thinkingBudget: 0 }
|
| 52 |
-
}
|
| 53 |
-
});
|
| 54 |
-
|
| 55 |
-
let translatedText = cleanSRTOutput(response.text);
|
| 56 |
-
let actualCount = countSrtBlocks(translatedText);
|
| 57 |
-
|
| 58 |
-
// --- INTEGRITY AUTO-RECOVERY ---
|
| 59 |
-
if (actualCount < expectedCount) {
|
| 60 |
-
console.warn(`[AI-SRT] Gap detected in chunk ${idx} (${actualCount}/${expectedCount}). Retrying with max precision...`);
|
| 61 |
-
response = await ai.models.generateContent({
|
| 62 |
-
model: model,
|
| 63 |
-
contents: [{ parts: [{ text: `CRITICAL: Do not skip lines. You MUST return exactly ${expectedCount} blocks. Translate ALL now:\n\n${inputText}` }] }],
|
| 64 |
-
config: { temperature: 0.0, systemInstruction: finalPrompt, safetySettings: DEFAULT_SAFETY_SETTINGS }
|
| 65 |
-
});
|
| 66 |
-
translatedText = cleanSRTOutput(response.text);
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
return translatedText;
|
| 70 |
-
})
|
| 71 |
-
);
|
| 72 |
-
|
| 73 |
-
const batchResults = await Promise.all(batchPromises);
|
| 74 |
-
results.push(...batchResults);
|
| 75 |
-
|
| 76 |
-
if (i + BATCH_SIZE < chunkTexts.length) {
|
| 77 |
-
await new Promise(r => setTimeout(r, COOLDOWN));
|
| 78 |
-
}
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
return results.join('\n\n').trim();
|
| 82 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/subtitle.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { tryModels, getPrompt, cleanSRTOutput, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 3 |
-
import { transcribe } from '@/backend/services/ai/transcribe';
|
| 4 |
-
|
| 5 |
-
const formatMsToSRT = (ms) => {
|
| 6 |
-
const hours = Math.floor(ms / 3600000);
|
| 7 |
-
const mins = Math.floor((ms % 3600000) / 60000);
|
| 8 |
-
const secs = Math.floor((ms % 60000) / 1000);
|
| 9 |
-
const mms = Math.floor(ms % 1000);
|
| 10 |
-
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},${mms.toString().padStart(3, '0')}`;
|
| 11 |
-
};
|
| 12 |
-
|
| 13 |
-
export async function subtitle(mediaBase64, mimeType, fullScript, apiKey, isOwnApi = false, sourceLanguage = 'English', startOffsetMs = 0, lastScriptIndex = 0) {
|
| 14 |
-
let scriptToProcess = fullScript || "";
|
| 15 |
-
|
| 16 |
-
// If script is empty, transcribe the whole media
|
| 17 |
-
if (!scriptToProcess) {
|
| 18 |
-
scriptToProcess = await transcribe(mediaBase64, mimeType, apiKey, isOwnApi);
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
const isBurmese = sourceLanguage.toLowerCase().includes('burm') || sourceLanguage.includes('မြန်မာ');
|
| 22 |
-
const promptTemplate = getPrompt('subtitle.txt');
|
| 23 |
-
|
| 24 |
-
// REMOVED: 15-second restriction logic.
|
| 25 |
-
// ADDED: Explicit instruction to process the ENTIRE audio.
|
| 26 |
-
const finalPrompt = promptTemplate
|
| 27 |
-
.replace('{{script}}', scriptToProcess)
|
| 28 |
-
.replace('{{language}}', isBurmese ? "Burmese (Conversational)" : sourceLanguage)
|
| 29 |
-
.replace('15-second audio input', 'the provided audio file')
|
| 30 |
-
.replace('Align the script', 'Align the COMPLETE script from start to finish');
|
| 31 |
-
|
| 32 |
-
const rawSRT = await tryModels(apiKey, ['gemini-3-flash-preview'], async (ai, model) => {
|
| 33 |
-
const response = await ai.models.generateContent({
|
| 34 |
-
model,
|
| 35 |
-
contents: {
|
| 36 |
-
parts: [
|
| 37 |
-
{ inlineData: { data: mediaBase64, mimeType: 'audio/wav' } },
|
| 38 |
-
{ text: `GENERATE FULL SRT: Listen to this entire file and align every word from the reference script. Return the complete SRT.` }
|
| 39 |
-
]
|
| 40 |
-
},
|
| 41 |
-
config: {
|
| 42 |
-
temperature: 0,
|
| 43 |
-
systemInstruction: finalPrompt,
|
| 44 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS
|
| 45 |
-
}
|
| 46 |
-
});
|
| 47 |
-
return cleanSRTOutput(response.text);
|
| 48 |
-
});
|
| 49 |
-
|
| 50 |
-
return {
|
| 51 |
-
srt: rawSRT || ""
|
| 52 |
-
};
|
| 53 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/transcribe.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 3 |
-
|
| 4 |
-
export async function transcribe(media, mimeType, apiKey, isOwnApi = false) {
|
| 5 |
-
const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
|
| 6 |
-
const systemPrompt = getPrompt('transcription.txt');
|
| 7 |
-
return await tryModels(apiKey, models, async (ai, model) => {
|
| 8 |
-
const response = await ai.models.generateContent({
|
| 9 |
-
model: model,
|
| 10 |
-
contents: {
|
| 11 |
-
parts: [
|
| 12 |
-
{ inlineData: { data: media, mimeType } },
|
| 13 |
-
{ text: "Transcribe accurately and completely. Do not skip any dialogue." }
|
| 14 |
-
]
|
| 15 |
-
},
|
| 16 |
-
config: {
|
| 17 |
-
temperature: 0.1,
|
| 18 |
-
systemInstruction: systemPrompt,
|
| 19 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
| 20 |
-
thinkingConfig: { thinkingBudget: 0 }
|
| 21 |
-
}
|
| 22 |
-
});
|
| 23 |
-
|
| 24 |
-
const text = response.text;
|
| 25 |
-
if (!text || text.trim().length < 2) {
|
| 26 |
-
throw new Error("EMPTY_RESPONSE_FROM_MODEL");
|
| 27 |
-
}
|
| 28 |
-
return text;
|
| 29 |
-
});
|
| 30 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/translate.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { Type } from '@google/genai';
|
| 3 |
-
import { tryModels, getPrompt, cleanJson, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 4 |
-
|
| 5 |
-
export async function translate(text, targetLanguage, options, apiKey, isOwnApi = false) {
|
| 6 |
-
const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
|
| 7 |
-
const isBurmese = targetLanguage.toLowerCase().includes('burm') || targetLanguage.includes('မြန်မာ');
|
| 8 |
-
let burmeseRules = isBurmese ? getPrompt('burmese_rules.txt') : "";
|
| 9 |
-
let template = getPrompt('translation.txt');
|
| 10 |
-
let finalPrompt = template.replace('{{targetLanguage}}', targetLanguage).replace('{{burmeseRules}}', burmeseRules).replace('{{deepMeaningRules}}', options.includes('meaning') ? "REQUESTED" : "NULL").replace('{{suggestionRules}}', options.includes('suggestions') ? "REQUESTED" : "NULL");
|
| 11 |
-
|
| 12 |
-
return await tryModels(apiKey, models, async (ai, model) => {
|
| 13 |
-
const response = await ai.models.generateContent({
|
| 14 |
-
model: model,
|
| 15 |
-
contents: [{ parts: [{ text: `CONTENT: ${text}` }] }],
|
| 16 |
-
config: {
|
| 17 |
-
temperature: 0.7,
|
| 18 |
-
systemInstruction: finalPrompt,
|
| 19 |
-
responseMimeType: "application/json",
|
| 20 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
| 21 |
-
responseSchema: {
|
| 22 |
-
type: Type.OBJECT,
|
| 23 |
-
properties: {
|
| 24 |
-
translation: { type: Type.STRING },
|
| 25 |
-
deepMeaning: { type: Type.STRING, nullable: true },
|
| 26 |
-
suggestions: { type: Type.OBJECT, nullable: true, properties: { videoTitles: { type: Type.ARRAY, items: { type: Type.STRING } }, thumbnailTexts: { type: Type.ARRAY, items: { type: Type.STRING } } } }
|
| 27 |
-
},
|
| 28 |
-
required: ['translation']
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
-
});
|
| 32 |
-
return JSON.parse(cleanJson(response.text));
|
| 33 |
-
});
|
| 34 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/tts.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
|
| 3 |
-
import { Modality } from '@google/genai';
|
| 4 |
-
|
| 5 |
-
export async function tts(text, voiceName, tone, apiKey, isOwnApi = false) {
|
| 6 |
-
// Strict requirement: Max 3500 characters
|
| 7 |
-
const ABSOLUTE_MAX_LENGTH = 3500;
|
| 8 |
-
if (text && text.length > ABSOLUTE_MAX_LENGTH) {
|
| 9 |
-
throw new Error(`Text is too long (${text.length} chars). Maximum allowed is ${ABSOLUTE_MAX_LENGTH} characters.`);
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const models = ['gemini-2.5-flash-preview-tts', 'gemini-2.5-pro-preview-tts'];
|
| 13 |
-
const promptTemplate = getPrompt('ai_voice.txt');
|
| 14 |
-
|
| 15 |
-
// For TTS models, prepending the tone instructions to the text is more stable than systemInstruction
|
| 16 |
-
const textWithInstructions = `${promptTemplate.replace('{{tone}}', tone)}\n\nSCRIPT TO SPEAK:\n${text}`;
|
| 17 |
-
|
| 18 |
-
console.log(`[TTS] Generating content... Length: ${text?.length || 0}`);
|
| 19 |
-
|
| 20 |
-
const chunkBase64 = await tryModels(apiKey, models, async (ai, model) => {
|
| 21 |
-
const response = await ai.models.generateContent({
|
| 22 |
-
model: model,
|
| 23 |
-
contents: [{ parts: [{ text: textWithInstructions }] }],
|
| 24 |
-
config: {
|
| 25 |
-
responseModalities: [Modality.AUDIO],
|
| 26 |
-
safetySettings: DEFAULT_SAFETY_SETTINGS,
|
| 27 |
-
speechConfig: {
|
| 28 |
-
voiceConfig: {
|
| 29 |
-
prebuiltVoiceConfig: {
|
| 30 |
-
voiceName: voiceName || 'Zephyr'
|
| 31 |
-
}
|
| 32 |
-
}
|
| 33 |
-
}
|
| 34 |
-
}
|
| 35 |
-
});
|
| 36 |
-
|
| 37 |
-
const part = response.candidates?.[0]?.content?.parts.find(p => p.inlineData);
|
| 38 |
-
if (part?.inlineData?.data) return part.inlineData.data;
|
| 39 |
-
throw new Error("EMPTY_AUDIO_DATA_FROM_GEMINI");
|
| 40 |
-
});
|
| 41 |
-
|
| 42 |
-
return chunkBase64;
|
| 43 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/ai/utils.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { GoogleGenAI } from '@google/genai';
|
| 3 |
-
import fs from 'fs';
|
| 4 |
-
import path from 'path';
|
| 5 |
-
import { fileURLToPath } from 'url';
|
| 6 |
-
|
| 7 |
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 8 |
-
|
| 9 |
-
export const getPrompt = (filename) => {
|
| 10 |
-
try {
|
| 11 |
-
const filePath = path.resolve(__dirname, '..', '..', 'prompts', filename);
|
| 12 |
-
if (fs.existsSync(filePath)) {
|
| 13 |
-
return fs.readFileSync(filePath, 'utf8');
|
| 14 |
-
} else {
|
| 15 |
-
const internalPath = path.resolve(__dirname, '..', '..', '..', 'prompts', filename);
|
| 16 |
-
return fs.existsSync(internalPath) ? fs.readFileSync(internalPath, 'utf8') : "";
|
| 17 |
-
}
|
| 18 |
-
} catch (e) {
|
| 19 |
-
console.error(`[AI-ENGINE] Prompt Load Error (${filename}):`, e.message);
|
| 20 |
-
return "";
|
| 21 |
-
}
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
export function cleanJson(raw) {
|
| 25 |
-
if (!raw) return "";
|
| 26 |
-
return raw.replace(/```json/gi, '').replace(/```/gi, '').trim();
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export function cleanSRTOutput(text) {
|
| 30 |
-
if (!text) return "";
|
| 31 |
-
let cleaned = text.replace(/```srt/gi, '').replace(/```/gi, '').trim();
|
| 32 |
-
const firstIndex = cleaned.search(/^\d+\s*\r?\n\d{2}:\d{2}:\d{2},\d{3}/m);
|
| 33 |
-
if (firstIndex !== -1) {
|
| 34 |
-
cleaned = cleaned.substring(firstIndex);
|
| 35 |
-
}
|
| 36 |
-
return cleaned;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
export const DEFAULT_SAFETY_SETTINGS = [
|
| 40 |
-
{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_ONLY_HIGH' },
|
| 41 |
-
{ category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_ONLY_HIGH' },
|
| 42 |
-
{ category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_ONLY_HIGH' },
|
| 43 |
-
{ category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_ONLY_HIGH' },
|
| 44 |
-
{ category: 'HARM_CATEGORY_CIVIC_INTEGRITY', threshold: 'BLOCK_ONLY_HIGH' }
|
| 45 |
-
];
|
| 46 |
-
|
| 47 |
-
export async function tryModels(apiKey, modelList, taskFn) {
|
| 48 |
-
let lastError = null;
|
| 49 |
-
const ai = new GoogleGenAI({ apiKey });
|
| 50 |
-
const MAX_RETRIES_PER_MODEL = 2;
|
| 51 |
-
|
| 52 |
-
for (const modelName of modelList) {
|
| 53 |
-
for (let attempt = 0; attempt <= MAX_RETRIES_PER_MODEL; attempt++) {
|
| 54 |
-
try {
|
| 55 |
-
return await taskFn(ai, modelName);
|
| 56 |
-
} catch (e) {
|
| 57 |
-
const rawError = (e.message || "Unknown error").toLowerCase();
|
| 58 |
-
const isRateLimit = rawError.includes('429') || rawError.includes('quota') || rawError.includes('limit');
|
| 59 |
-
|
| 60 |
-
if (isRateLimit && attempt < MAX_RETRIES_PER_MODEL) {
|
| 61 |
-
const waitTime = (attempt + 1) * 5000;
|
| 62 |
-
console.warn(`[AI-ENGINE] Rate limit on ${modelName}. Retry in ${waitTime}ms...`);
|
| 63 |
-
await new Promise(r => setTimeout(r, waitTime));
|
| 64 |
-
continue;
|
| 65 |
-
}
|
| 66 |
-
console.error(`[AI-ENGINE] Error on ${modelName}:`, rawError);
|
| 67 |
-
lastError = e;
|
| 68 |
-
break;
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
}
|
| 72 |
-
throw lastError;
|
| 73 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/authService.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
import { db } from '@/backend/services/firebase';
|
| 2 |
-
|
| 3 |
-
export const AuthService = {
|
| 4 |
-
async saveCustomKey(sessionId, apiKey) {
|
| 5 |
-
const snapshot = await db.ref('users').orderByChild('Active_Session_ID').equalTo(sessionId).once('value');
|
| 6 |
-
if (!snapshot.exists()) throw new Error("User not found.");
|
| 7 |
-
const userKey = Object.keys(snapshot.val())[0];
|
| 8 |
-
await db.ref(`users/${userKey}`).update({ Private_API_Key: apiKey });
|
| 9 |
-
return { success: true };
|
| 10 |
-
},
|
| 11 |
-
|
| 12 |
-
async updateActiveSession(userId, newSessionId) {
|
| 13 |
-
if (!userId) throw new Error("Membership ID is required.");
|
| 14 |
-
|
| 15 |
-
// Find user by their PERMANENT Login_Key first
|
| 16 |
-
let snapshot = await db.ref('users').orderByChild('Login_Key').equalTo(userId).once('value');
|
| 17 |
-
|
| 18 |
-
// Fallback: If not found, try searching by Active_Session_ID (for legacy users)
|
| 19 |
-
if (!snapshot.exists()) {
|
| 20 |
-
snapshot = await db.ref('users').orderByChild('Active_Session_ID').equalTo(userId).once('value');
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
if (!snapshot.exists()) throw new Error("Membership ID not found in records.");
|
| 24 |
-
|
| 25 |
-
const userKey = Object.keys(snapshot.val())[0];
|
| 26 |
-
const userData = snapshot.val()[userKey];
|
| 27 |
-
|
| 28 |
-
const updates = { Active_Session_ID: newSessionId };
|
| 29 |
-
|
| 30 |
-
// If this was a legacy user without a Login_Key, set it now to stabilize their account
|
| 31 |
-
if (!userData.Login_Key) {
|
| 32 |
-
updates.Login_Key = userId;
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
await db.ref(`users/${userKey}`).update(updates);
|
| 36 |
-
|
| 37 |
-
return { success: true };
|
| 38 |
-
}
|
| 39 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/credit.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { db } from '@/backend/services/firebase';
|
| 3 |
-
|
| 4 |
-
export async function checkEligibility(sessionId, toolKey, isOwnKey) {
|
| 5 |
-
if (!sessionId) throw new Error("Authentication failed. No session ID provided.");
|
| 6 |
-
|
| 7 |
-
const now = new Date();
|
| 8 |
-
const today = now.toLocaleDateString('en-CA'); // Robust YYYY-MM-DD
|
| 9 |
-
|
| 10 |
-
// Map toolKey to prefix-aware usage key
|
| 11 |
-
const usageKey = isOwnKey ? `own_${toolKey}` : `app_${toolKey}`;
|
| 12 |
-
|
| 13 |
-
// 1. Fetch System Tier Settings
|
| 14 |
-
const sysSnapshot = await db.ref('system/settings/tiers').once('value');
|
| 15 |
-
const tiers = sysSnapshot.val() || {};
|
| 16 |
-
|
| 17 |
-
const toolMapper = {
|
| 18 |
-
'count_transcript': 'transcript',
|
| 19 |
-
'count_translate': 'translate',
|
| 20 |
-
'count_srt_translate': 'srt_translator',
|
| 21 |
-
'count_tts': 'tts',
|
| 22 |
-
'count_subtitle': 'subtitle_gen',
|
| 23 |
-
'count_creator_text': 'content_creator',
|
| 24 |
-
'count_creator_image': 'ai_image'
|
| 25 |
-
};
|
| 26 |
-
const tierToolKey = toolMapper[toolKey] || toolKey;
|
| 27 |
-
|
| 28 |
-
let userKey, userData, userClass;
|
| 29 |
-
|
| 30 |
-
if (sessionId === 'free_access') {
|
| 31 |
-
userClass = 'FREE';
|
| 32 |
-
userData = { Usage: {}, Credits: 0, Class: 'FREE' };
|
| 33 |
-
} else {
|
| 34 |
-
const snapshot = await db.ref('users').orderByChild('Active_Session_ID').equalTo(sessionId).once('value');
|
| 35 |
-
if (!snapshot.exists()) throw new Error("Invalid Membership ID. Please login again.");
|
| 36 |
-
userKey = Object.keys(snapshot.val())[0];
|
| 37 |
-
userData = snapshot.val()[userKey];
|
| 38 |
-
if (userData.Status !== 'ACTIVE') throw new Error("Account suspended. Please contact admin.");
|
| 39 |
-
userClass = userData.Class === 'MEMBER+' ? 'MEMBER_PLUS' : 'MEMBER';
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
const tierConfig = tiers[userClass] || { tools: {}, ownTools: {}, limits: {}, ownLimits: {}, apiAccess: { app: true, own: true } };
|
| 43 |
-
|
| 44 |
-
// 2. CHECK API ACCESS PERMISSIONS
|
| 45 |
-
if (isOwnKey && tierConfig.apiAccess?.own === false) {
|
| 46 |
-
throw new Error(`OWN_API_RESTRICTED: Your ${userClass} class is not allowed to use private API keys.`);
|
| 47 |
-
}
|
| 48 |
-
if (!isOwnKey && tierConfig.apiAccess?.app === false) {
|
| 49 |
-
throw new Error(`APP_API_RESTRICTED: Shared App API is currently disabled for your class.`);
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
// 3. CHECK SPECIFIC TOOL STATUS FOR TIER (Mode Specific)
|
| 53 |
-
const toolMap = isOwnKey ? (tierConfig.ownTools || {}) : (tierConfig.tools || {});
|
| 54 |
-
if (toolMap[tierToolKey] === false) {
|
| 55 |
-
throw new Error(`TOOL_DISABLED: This tool is disabled in ${isOwnKey ? 'Own API' : 'App API'} mode for ${userClass} class.`);
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
const usage = (userData.Last_Usage_Date === today) ? (userData.Usage || {}) : {};
|
| 59 |
-
|
| 60 |
-
// 4. CHECK LIMITS (Mode Specific)
|
| 61 |
-
const limit = isOwnKey ? (tierConfig.ownLimits?.[tierToolKey] ?? -1) : (tierConfig.limits?.[tierToolKey] ?? -1);
|
| 62 |
-
|
| 63 |
-
if (limit !== -1) {
|
| 64 |
-
const currentCount = parseInt(usage[usageKey] || 0);
|
| 65 |
-
if (currentCount >= limit) {
|
| 66 |
-
throw new Error(`DAILY_QUOTA_REACHED: Your ${userClass} daily limit for ${isOwnKey ? 'Own' : 'App'} API has been reached.`);
|
| 67 |
-
}
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
// 5. CHECK CREDITS (Only if using App API)
|
| 71 |
-
let cost = 0;
|
| 72 |
-
if (!isOwnKey) {
|
| 73 |
-
const costMap = {
|
| 74 |
-
'count_transcript': 1, 'count_translate': 1, 'count_srt_translate': 5,
|
| 75 |
-
'count_tts': 3, 'count_subtitle': 1, 'count_creator_text': 1, 'count_creator_image': 5
|
| 76 |
-
};
|
| 77 |
-
cost = costMap[toolKey] || 1;
|
| 78 |
-
|
| 79 |
-
if (userClass === 'MEMBER_PLUS') {
|
| 80 |
-
const freeTools = ['count_transcript', 'count_translate', 'count_subtitle', 'count_creator_text'];
|
| 81 |
-
if (freeTools.includes(toolKey)) cost = 0;
|
| 82 |
-
}
|
| 83 |
-
if (userClass === 'FREE') cost = 0;
|
| 84 |
-
|
| 85 |
-
const currentCredits = userData.Credits || 0;
|
| 86 |
-
if (cost > 0 && currentCredits < cost) {
|
| 87 |
-
throw new Error(`INSUFFICIENT_BALANCE: This task costs ${cost} credits. Your balance is ${currentCredits}.`);
|
| 88 |
-
}
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
return { userKey, userData, usageKey, cost, today, isGuest: (userClass === 'FREE'), isOwnKey };
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
export async function commitDeduction(eligibilityData, toolKey) {
|
| 95 |
-
if (!eligibilityData) return;
|
| 96 |
-
const { userKey, userData, usageKey, cost, today, isGuest } = eligibilityData;
|
| 97 |
-
|
| 98 |
-
if (isGuest) return;
|
| 99 |
-
|
| 100 |
-
const updates = {};
|
| 101 |
-
if (cost > 0) {
|
| 102 |
-
const newBalance = (userData.Credits || 0) - cost;
|
| 103 |
-
updates[`users/${userKey}/Credits`] = newBalance >= 0 ? newBalance : 0;
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
if (userData.Last_Usage_Date !== today) {
|
| 107 |
-
updates[`users/${userKey}/Last_Usage_Date`] = today;
|
| 108 |
-
updates[`users/${userKey}/Usage`] = { [usageKey]: 1 };
|
| 109 |
-
} else {
|
| 110 |
-
const currentUsage = (userData.Usage && userData.Usage[usageKey]) || 0;
|
| 111 |
-
updates[`users/${userKey}/Usage/${usageKey}`] = currentUsage + 1;
|
| 112 |
-
}
|
| 113 |
-
|
| 114 |
-
await db.ref().update(updates);
|
| 115 |
-
}
|
| 116 |
-
|
| 117 |
-
export async function processFreePool(toolKey) {
|
| 118 |
-
const today = new Date().toLocaleDateString('en-CA');
|
| 119 |
-
const poolPath = `system/daily_pool/${today}/app_${toolKey}`; // Default to app prefix for free pool
|
| 120 |
-
const snapshot = await db.ref(poolPath).once('value');
|
| 121 |
-
const current = snapshot.val() || 0;
|
| 122 |
-
await db.ref(poolPath).set(current + 1);
|
| 123 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/creditService.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import { db } from '@/backend/services/firebase';
|
| 3 |
-
|
| 4 |
-
export const CreditService = {
|
| 5 |
-
async checkEligibility(sessionId, guestId, toolKey, isOwnKey, creditCostOverride = null) {
|
| 6 |
-
if (!sessionId) throw new Error("Authentication failed.");
|
| 7 |
-
|
| 8 |
-
const now = new Date();
|
| 9 |
-
const today = now.toLocaleDateString('en-CA');
|
| 10 |
-
const usageKey = isOwnKey ? `own_${toolKey}` : `app_${toolKey}`;
|
| 11 |
-
|
| 12 |
-
// 1. Fetch System Tier Settings
|
| 13 |
-
const sysSnapshot = await db.ref('system/settings/tiers').once('value');
|
| 14 |
-
const tiers = sysSnapshot.val() || {};
|
| 15 |
-
|
| 16 |
-
const toolMapper = {
|
| 17 |
-
'count_transcript': 'transcript',
|
| 18 |
-
'count_translate': 'translate',
|
| 19 |
-
'count_srt_translate': 'srt_translator',
|
| 20 |
-
'count_tts': 'tts',
|
| 21 |
-
'count_subtitle': 'subtitle_gen',
|
| 22 |
-
'count_creator_text': 'content_creator',
|
| 23 |
-
'count_creator_image': 'ai_image',
|
| 24 |
-
'downloader': 'downloader',
|
| 25 |
-
'book_recap': 'book_recap'
|
| 26 |
-
};
|
| 27 |
-
const tierToolKey = toolMapper[toolKey] || toolKey;
|
| 28 |
-
|
| 29 |
-
let userKey, userData, userClass, storagePath;
|
| 30 |
-
|
| 31 |
-
if (sessionId === 'free_access') {
|
| 32 |
-
userClass = 'FREE';
|
| 33 |
-
if (!guestId || guestId === 'anonymous') {
|
| 34 |
-
// Return generic guest data for free mode
|
| 35 |
-
userData = { Usage: {}, Credits: 0, Class: 'FREE', Last_Usage_Date: today };
|
| 36 |
-
storagePath = `guests/temp_guest`;
|
| 37 |
-
} else {
|
| 38 |
-
storagePath = `guests/${guestId}/${today}`;
|
| 39 |
-
const usageSnapshot = await db.ref(storagePath).once('value');
|
| 40 |
-
const currentUsage = usageSnapshot.val() || {};
|
| 41 |
-
userData = { Usage: currentUsage, Credits: 0, Class: 'FREE', Last_Usage_Date: today };
|
| 42 |
-
}
|
| 43 |
-
} else {
|
| 44 |
-
// Find user by temporary session ID
|
| 45 |
-
const snapshot = await db.ref('users').orderByChild('Active_Session_ID').equalTo(sessionId).once('value');
|
| 46 |
-
if (!snapshot.exists()) throw new Error("Invalid Session. Please login again.");
|
| 47 |
-
|
| 48 |
-
userKey = Object.keys(snapshot.val())[0];
|
| 49 |
-
userData = snapshot.val()[userKey];
|
| 50 |
-
if (userData.Status !== 'ACTIVE') throw new Error("Account suspended.");
|
| 51 |
-
|
| 52 |
-
userClass = userData.Class === 'MEMBER+' ? 'MEMBER_PLUS' : (userData.Class === 'BASIC' ? 'BASIC' : 'MEMBER');
|
| 53 |
-
storagePath = `users/${userKey}`;
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
const tierConfig = tiers[userClass] || { apiAccess: { app: true, own: true } };
|
| 57 |
-
|
| 58 |
-
// Check if user is trying to use their own API but it's disabled for their class
|
| 59 |
-
if (isOwnKey && tierConfig.apiAccess?.own === false) throw new Error("OWN_API_RESTRICTED");
|
| 60 |
-
if (!isOwnKey && tierConfig.apiAccess?.app === false) throw new Error("APP_API_RESTRICTED");
|
| 61 |
-
|
| 62 |
-
// Check Specific Tool Toggle (Mode Specific)
|
| 63 |
-
const toolToggleMap = isOwnKey ? (tierConfig.ownTools || {}) : (tierConfig.tools || {});
|
| 64 |
-
if (toolToggleMap[tierToolKey] === false) throw new Error("TOOL_DISABLED");
|
| 65 |
-
|
| 66 |
-
const usage = (userData.Last_Usage_Date === today) ? (userData.Usage || {}) : {};
|
| 67 |
-
|
| 68 |
-
// 1. Declare the limit variable based on the tier settings
|
| 69 |
-
let limit = isOwnKey
|
| 70 |
-
? (tierConfig.ownLimits?.[tierToolKey] ?? -1)
|
| 71 |
-
: (tierConfig.limits?.[tierToolKey] ?? -1);
|
| 72 |
-
|
| 73 |
-
// 2. Override: If using Own API Key, force the limit to -1 (Unlimited)
|
| 74 |
-
if (isOwnKey) {
|
| 75 |
-
limit = -1;
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
// 3. Perform the check only if a limit exists
|
| 79 |
-
if (limit !== -1) {
|
| 80 |
-
const currentCount = parseInt(usage[usageKey] || 0);
|
| 81 |
-
if (currentCount >= limit) {
|
| 82 |
-
throw new Error("DAILY_LIMIT_REACHED");
|
| 83 |
-
}
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
if (isOwnKey) {
|
| 87 |
-
limit = -1; // -1 means "No Limit" or "Unlimited" in your logic (LOL kyaw Gyi)
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
// Check Daily Frequency Limit (Mode Specific)
|
| 91 |
-
// let limit = isOwnKey ? (tierConfig.ownLimits?.[tierToolKey] ?? -1) : (tierConfig.limits?.[tierToolKey] ?? -1);
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
if (limit !== -1) {
|
| 95 |
-
const currentCount = parseInt(usage[usageKey] || 0);
|
| 96 |
-
if (currentCount >= limit) throw new Error("DAILY_LIMIT_REACHED");
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
let cost = 0;
|
| 100 |
-
if (userClass !== 'FREE' && !isOwnKey) {
|
| 101 |
-
const costMap = {
|
| 102 |
-
'count_transcript': 4,
|
| 103 |
-
'count_translate': 4,
|
| 104 |
-
'count_srt_translate': 7,
|
| 105 |
-
'count_tts': 3,
|
| 106 |
-
'count_subtitle': 2,
|
| 107 |
-
'count_creator_text': 3,
|
| 108 |
-
'count_creator_image': 5,
|
| 109 |
-
'downloader': 2,
|
| 110 |
-
'book_recap': 8
|
| 111 |
-
};
|
| 112 |
-
|
| 113 |
-
cost = (creditCostOverride !== null) ? creditCostOverride : (costMap[toolKey] || 1);
|
| 114 |
-
|
| 115 |
-
if (cost > 0 && (userData.Credits || 0) < cost) {
|
| 116 |
-
throw new Error(`INSUFFICIENT_BALANCE: Needs ${cost} credits.`);
|
| 117 |
-
}
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
return { userKey, storagePath, userData, usageKey, cost, today, isGuest: (userClass === 'FREE'), isOwnKey };
|
| 121 |
-
},
|
| 122 |
-
|
| 123 |
-
async commitDeduction(eligibilityData) {
|
| 124 |
-
if (!eligibilityData) return;
|
| 125 |
-
const { storagePath, userData, usageKey, cost, today, isGuest, isOwnKey } = eligibilityData;
|
| 126 |
-
|
| 127 |
-
// Own API mode records usage but never deducts credits
|
| 128 |
-
const finalCost = isOwnKey ? 0 : cost;
|
| 129 |
-
|
| 130 |
-
const updates = {};
|
| 131 |
-
if (isGuest) {
|
| 132 |
-
if (storagePath === 'guests/temp_guest') return;
|
| 133 |
-
const currentUsage = (userData.Usage?.[usageKey]) || 0;
|
| 134 |
-
updates[`${storagePath}/${usageKey}`] = currentUsage + 1;
|
| 135 |
-
await db.ref().update(updates);
|
| 136 |
-
return;
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
-
if (finalCost > 0) {
|
| 140 |
-
const newBalance = (userData.Credits || 0) - finalCost;
|
| 141 |
-
updates[`${storagePath}/Credits`] = Math.max(0, newBalance);
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
if (userData.Last_Usage_Date !== today) {
|
| 145 |
-
updates[`${storagePath}/Last_Usage_Date`] = today;
|
| 146 |
-
updates[`${storagePath}/Usage`] = { [usageKey]: 1 };
|
| 147 |
-
} else {
|
| 148 |
-
const currentUsage = (userData.Usage?.[usageKey]) || 0;
|
| 149 |
-
updates[`${storagePath}/Usage/${usageKey}`] = currentUsage + 1;
|
| 150 |
-
}
|
| 151 |
-
await db.ref().update(updates);
|
| 152 |
-
}
|
| 153 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
services/firebase.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
|
| 2 |
-
import admin from 'firebase-admin';
|
| 3 |
-
import fs from 'fs';
|
| 4 |
-
import path from 'path';
|
| 5 |
-
|
| 6 |
-
// Service Account file path - MUST be from transcript-master project
|
| 7 |
-
const serviceAccountPath = path.resolve(process.cwd(), 'service-account.json');
|
| 8 |
-
|
| 9 |
-
try {
|
| 10 |
-
if (!admin.apps.length) {
|
| 11 |
-
// Explicitly using transcript-master project details
|
| 12 |
-
const config = {
|
| 13 |
-
databaseURL: "https://klingwatermark-default-rtdb.asia-southeast1.firebasedatabase.app",
|
| 14 |
-
projectId: "klingwatermark"
|
| 15 |
-
};
|
| 16 |
-
|
| 17 |
-
if (fs.existsSync(serviceAccountPath)) {
|
| 18 |
-
const serviceAccount = JSON.parse(fs.readFileSync(serviceAccountPath, 'utf8'));
|
| 19 |
-
// Safety check: ensure the file matches the database project
|
| 20 |
-
if (serviceAccount.project_id !== "transcript-master") {
|
| 21 |
-
console.error(`❌ CRITICAL: service-account.json is for project "${serviceAccount.project_id}", but database is "transcript-master". Please download the correct key.`);
|
| 22 |
-
}
|
| 23 |
-
config.credential = admin.credential.cert(serviceAccount);
|
| 24 |
-
console.log("✅ Firebase Admin: Using service-account.json for transcript-master");
|
| 25 |
-
} else {
|
| 26 |
-
console.warn("ℹ️ Firebase Admin: service-account.json NOT FOUND. DB access will fail.");
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
admin.initializeApp(config);
|
| 30 |
-
console.log("🚀 Firebase Admin Service Initialized for transcript-master");
|
| 31 |
-
}
|
| 32 |
-
} catch (error) {
|
| 33 |
-
console.error("❌ Firebase Init Error:", error.message);
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
export const db = admin.database();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|