bigbossmonster commited on
Commit
221f4f9
·
verified ·
1 Parent(s): 1125a81

Delete services

Browse files
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();