bigbossmonster commited on
Commit
ea81969
·
verified ·
1 Parent(s): 7efe65f

Upload 24 files

Browse files
backend/.env ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ PORT=8080
2
+ API_KEY="AIzaSyAUuxggsn9uNOEHDW4NiKpyrfCUpt8vLi4"
3
+ ADMIN_PASSWORD="Hello"
backend/jsconfig.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "paths": {
5
+ "@/backend/*": ["./*"]
6
+ }
7
+ }
8
+ }
backend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
backend/package.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "dependencies": {
3
+ "@google/genai": "^1.35.0",
4
+ "@google/generative-ai": "^0.24.1",
5
+ "axios": "^1.13.2",
6
+ "cors": "^2.8.5",
7
+ "dotenv": "^17.2.3",
8
+ "express": "^5.2.1",
9
+ "firebase-admin": "^13.6.0",
10
+ "fluent-ffmpeg": "^2.1.3"
11
+ }
12
+ }
backend/run.bat ADDED
@@ -0,0 +1 @@
 
 
1
+ npx tsx --tsconfig jsconfig.json server.js
backend/server.js ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import express from 'express';
3
+ import cors from 'cors';
4
+ import dotenv from 'dotenv';
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { AIService } from '@/backend/services/ai';
10
+ import { AuthService } from '@/backend/services/authService';
11
+ import { CreditService } from '@/backend/services/creditService';
12
+ import { AdminService } from '@/backend/services/adminService';
13
+
14
+ const execPromise = promisify(exec);
15
+ dotenv.config();
16
+
17
+ const app = express();
18
+
19
+ // CRITICAL FIX: Increased limits to 100mb to allow processing of long audio/video base64 data
20
+ app.use(express.json({ limit: '100mb' }));
21
+ app.use(express.urlencoded({ limit: '100mb', extended: true }));
22
+
23
+ app.use(cors());
24
+
25
+ const apiRouter = express.Router();
26
+
27
+ apiRouter.post('/download', async (req, res) => {
28
+ const { url, quality, platform, sessionId, creditCost } = req.body;
29
+
30
+ try {
31
+ const eligibility = await CreditService.checkEligibility(sessionId, null, 'downloader', false, creditCost);
32
+
33
+ const timestamp = Date.now();
34
+ const fileBaseName = `tm_dl_${timestamp}`;
35
+ const outputDir = '/tmp';
36
+
37
+ // Determine file extension and format string
38
+ const extension = quality === 'audio' ? 'mp3' : 'mp4';
39
+ const outputPath = path.join(outputDir, `${fileBaseName}.${extension}`);
40
+
41
+ let formatStr = "";
42
+ if (quality === 'audio') {
43
+ formatStr = "-f 'ba' -x --audio-format mp3";
44
+ } else {
45
+ const qMap = {
46
+ '360p': 'bestvideo[height<=360]+bestaudio/best[height<=360]',
47
+ '720p': 'bestvideo[height<=720]+bestaudio/best[height<=720]',
48
+ '1080p': 'bestvideo[height<=1080]+bestaudio/best[height<=1080]'
49
+ };
50
+ const format = qMap[quality] || qMap['720p'];
51
+ formatStr = `-f "${format}" --merge-output-format mp4`;
52
+ }
53
+
54
+ const tiktokArgs = platform === 'tiktok' ? '--referer "https://www.tiktok.com/"' : '';
55
+
56
+ // CRITICAL FIX: Added --js-runtime node to fix JS engine missing error.
57
+ // Added --no-warnings and --no-check-certificate for better reliability on Cloud Run.
58
+ const command = `yt-dlp --no-playlist --no-warnings --no-check-certificate --js-runtime node ${formatStr} ${tiktokArgs} -o "${outputPath}" "${url.trim()}"`;
59
+
60
+ console.log(`[DOWNLOADER] Executing: ${command}`);
61
+
62
+ try {
63
+ await execPromise(command);
64
+ } catch (execErr) {
65
+ console.error("[DL-EXEC-FAIL]", execErr.stderr || execErr.message);
66
+ // Even if warnings are printed to stderr, check if file exists before crashing
67
+ if (!fs.existsSync(outputPath)) {
68
+ throw new Error(`Engine Error: ${execErr.stderr || execErr.message}`);
69
+ }
70
+ }
71
+
72
+ // Verify file existence explicitly
73
+ if (!fs.existsSync(outputPath)) {
74
+ throw new Error("Download engine failed. File not generated.");
75
+ }
76
+
77
+ await CreditService.commitDeduction(eligibility);
78
+
79
+ res.download(outputPath, `${fileBaseName}.${extension}`, (err) => {
80
+ if (err) console.error("Stream Error:", err);
81
+ try {
82
+ if (fs.existsSync(outputPath)) fs.unlinkSync(outputPath);
83
+ } catch (e) { console.error("Cleanup Error:", e); }
84
+ });
85
+
86
+ } catch (error) {
87
+ console.error("[DOWNLOAD-ERROR]", error.message);
88
+ res.status(500).json({ error: error.message });
89
+ }
90
+ });
91
+
92
+ const handleAiRequest = async (req, res, toolKey, taskFn) => {
93
+ const sessionId = req.headers['x-session-id'];
94
+ const guestId = req.headers['x-guest-id'];
95
+ const { isOwnApi, customApiKey, creditCost } = req.body;
96
+
97
+ try {
98
+ const eligibility = await CreditService.checkEligibility(sessionId, guestId, toolKey, isOwnApi, creditCost);
99
+ const apiKey = isOwnApi ? (customApiKey || process.env.API_KEY) : process.env.API_KEY;
100
+ const result = await taskFn(apiKey, isOwnApi);
101
+ await CreditService.commitDeduction(eligibility);
102
+ res.json(result);
103
+ } catch (error) {
104
+ console.error(`[BACKEND-API] Error in ${toolKey}:`, error.message);
105
+ res.status(error.message.includes('REACHED') || error.message.includes('INSUFFICIENT') ? 403 : 500).json({
106
+ error: error.message
107
+ });
108
+ }
109
+ };
110
+
111
+ apiRouter.post('/transcribe', (req, res) => {
112
+ handleAiRequest(req, res, 'count_transcript', async (key, isOwn) => {
113
+ return { text: await AIService.transcribe(req.body.media, req.body.mimeType, key, isOwn) };
114
+ });
115
+ });
116
+
117
+ apiRouter.post('/recap', (req, res) => {
118
+ handleAiRequest(req, res, 'count_transcript', async (key, isOwn) => {
119
+ return { recap: await AIService.recap(req.body.media, req.body.mimeType, req.body.targetLanguage, key, isOwn) };
120
+ });
121
+ });
122
+
123
+ apiRouter.post('/book-recap', (req, res) => {
124
+ handleAiRequest(req, res, 'count_translate', async (key, isOwn) => {
125
+ return { recap: await AIService.bookRecap(req.body.media, req.body.mimeType, req.body.targetLanguage, key, isOwn) };
126
+ });
127
+ });
128
+
129
+ apiRouter.post('/comic-translate', (req, res) => {
130
+ handleAiRequest(req, res, 'count_translate', async (key, isOwn) => {
131
+ return { pdfData: await AIService.comicTranslate(req.body.media, req.body.mimeType, req.body.targetLanguage, key, isOwn) };
132
+ });
133
+ });
134
+
135
+ apiRouter.post('/translate', (req, res) => {
136
+ handleAiRequest(req, res, 'count_translate', async (key, isOwn) => {
137
+ return await AIService.translate(req.body.text, req.body.targetLanguage, req.body.options, key, isOwn);
138
+ });
139
+ });
140
+
141
+ apiRouter.post('/srt-translate', (req, res) => {
142
+ handleAiRequest(req, res, 'count_srt_translate', async (key, isOwn) => {
143
+ return { srt: await AIService.srtTranslate(req.body.srtContent, req.body.sourceLanguage, req.body.targetLanguage, key, isOwn) };
144
+ });
145
+ });
146
+
147
+ apiRouter.post('/tts', (req, res) => {
148
+ handleAiRequest(req, res, 'count_tts', async (key, isOwn) => {
149
+ return { audioData: await AIService.tts(req.body.text, req.body.voiceId, req.body.tone, key, isOwn) };
150
+ });
151
+ });
152
+
153
+ apiRouter.post('/subtitle', (req, res) => {
154
+ req.setTimeout(1200000);
155
+ handleAiRequest(req, res, 'count_subtitle', async (key, isOwn) => {
156
+ return await AIService.subtitle(
157
+ req.body.media,
158
+ req.body.mimeType,
159
+ req.body.script,
160
+ key,
161
+ isOwn,
162
+ req.body.sourceLanguage,
163
+ req.body.startOffsetMs || 0,
164
+ req.body.lastScriptIndex || 0
165
+ );
166
+ });
167
+ });
168
+
169
+ apiRouter.post('/content-creator', (req, res) => {
170
+ handleAiRequest(req, res, 'count_creator_text', async (key, isOwn) => {
171
+ const script = await AIService.contentCreator(
172
+ req.body.topic, req.body.category, req.body.subTopics,
173
+ req.body.contentType, req.body.creatorGender, req.body.targetLanguage, key, isOwn
174
+ );
175
+ let imageUrl = null;
176
+ if (req.body.withImage) {
177
+ try { imageUrl = await AIService.generateImage(req.body.topic, key, isOwn); } catch(e) {}
178
+ }
179
+ return { script, imageUrl };
180
+ });
181
+ });
182
+
183
+ apiRouter.post('/secure/save-key', async (req, res) => {
184
+ try {
185
+ const { sessionId, apiKey } = req.body;
186
+ res.json(await AuthService.saveCustomKey(sessionId, apiKey));
187
+ } catch (e) { res.status(500).json({ error: e.message }); }
188
+ });
189
+
190
+ apiRouter.post('/secure/update-session', async (req, res) => {
191
+ try {
192
+ const { userId, newSessionId } = req.body;
193
+ res.json(await AuthService.updateActiveSession(userId, newSessionId));
194
+ } catch (e) { res.status(500).json({ error: e.message }); }
195
+ });
196
+
197
+ apiRouter.post('/admin/verify', async (req, res) => {
198
+ const { adminPassword } = req.body;
199
+ if (adminPassword === process.env.ADMIN_PASSWORD) res.json({ success: true });
200
+ else res.status(403).json({ error: "Unauthorized" });
201
+ });
202
+
203
+ apiRouter.post('/admin/update-settings', async (req, res) => {
204
+ const { adminPassword, settings } = req.body;
205
+ if (adminPassword !== process.env.ADMIN_PASSWORD) return res.status(403).json({ error: "Denied" });
206
+ try { res.json(await AdminService.updateSettings(settings)); } catch (e) { res.status(500).json({ error: e.message }); }
207
+ });
208
+
209
+ apiRouter.post('/admin/add-user', async (req, res) => {
210
+ const { adminPassword, userData, referrerCode } = req.body;
211
+ if (adminPassword !== process.env.ADMIN_PASSWORD) return res.status(403).json({ error: "Denied" });
212
+ try { res.json(await AdminService.addUser(userData, referrerCode)); } catch (e) { res.status(500).json({ error: e.message }); }
213
+ });
214
+
215
+ apiRouter.post('/admin/reactivate-user', async (req, res) => {
216
+ const { adminPassword, nodeKey, userClass, credits } = req.body;
217
+ if (adminPassword !== process.env.ADMIN_PASSWORD) return res.status(403).json({ error: "Denied" });
218
+ try { res.json(await AdminService.reactivateUser(nodeKey, userClass, credits)); } catch (e) { res.status(500).json({ error: e.message }); }
219
+ });
220
+
221
+ apiRouter.post('/admin/update-user-class', async (req, res) => {
222
+ const { adminPassword, nodeKey, userClass, credits } = req.body;
223
+ if (adminPassword !== process.env.ADMIN_PASSWORD) return res.status(403).json({ error: "Denied" });
224
+ try { res.json(await AdminService.updateUserClass(nodeKey, userClass, credits)); } catch (e) { res.status(500).json({ error: e.message }); }
225
+ });
226
+
227
+ apiRouter.post('/admin/topup-credits', async (req, res) => {
228
+ const { adminPassword, sessionId, amount } = req.body;
229
+ if (adminPassword !== process.env.ADMIN_PASSWORD) return res.status(403).json({ error: "Denied" });
230
+ try { res.json(await AdminService.topUpCredits(sessionId, amount)); } catch (e) { res.status(500).json({ error: e.message }); }
231
+ });
232
+
233
+ apiRouter.post('/admin/delete-user', async (req, res) => {
234
+ const { adminPassword, nodeKey } = req.body;
235
+ if (adminPassword !== process.env.ADMIN_PASSWORD) return res.status(403).json({ error: "Denied" });
236
+ try { res.json(await AdminService.deleteUser(nodeKey)); } catch (e) { res.status(500).json({ error: e.message }); }
237
+ });
238
+
239
+ app.use('/', apiRouter);
240
+
241
+ const PORT = process.env.PORT || 8080;
242
+ app.listen(PORT, () => {
243
+ console.log(`🚀 Master Backend Engine running on port ${PORT}`);
244
+ });
backend/service-account.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "service_account",
3
+ "project_id": "klingwatermark",
4
+ "private_key_id": "8ac9baff7cdb9605fd3c390740659f5edae75bbe",
5
+ "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDmZlD5OF0UhHst\ntgf8gDuQ9D2OurWowM9s9OutRoEby+XJ6ndRZhOyCzH5iFfMqVlWBes0Dr/zSMcE\nrP09DH+bntAUDZJ4iIhV2ODLE+m/hSNdM74uSUt8xlTXpZlpBFbX77VzgNlB+iTV\nkJ3edzrFvAnzbZcXrWA8iYA10yotvCKEk1sPm3h4SrsPS1DFgywCMtkgnLAjv47d\nqW8klGuzMVKGWysBlG/THzXmxr1CzxUdiSgbMIASLLS67nBNmTwd3doAfqF7HWvY\n3e8+Iw3qKNJw7o5QAYvLr2kfRJ68sCCI8ib2+OEhLdxDru7lRUllzlFt/+CyYkaX\nM1JS+mUDAgMBAAECggEAINtCfOf+zig+cLxe0zFuTTOne61ArrogWodq/KDrVI11\nTaA0N9V4xZE3JUC+VJ9p8AyUCC56TTV5vD73NjeMEyNIxTNSSgIefy+Yjx1d3kQc\nUPRKEMu/CiI2gzJIeGN4Bv9WPwyG7xO/IP2opcsXBI/WU3Kzc1r0OG3bbNGaWD8Q\n3looKPcvVsgxceDaH3zs0+tEtP1JQ2YKlXZVnpr1lFUExkNhLPOhva+H1aPgryRG\nu380OTTnnceXMfwlYXiXSBZNLA7LToQR856k3Wbtj75Zr8+7f/y6kxQCcwYIjDUF\nfxDy0QZpM6MAMHrGc4Xzy3MZBYoWTe4ZSEttGtGasQKBgQD2hIldQxL9D5OBLmxl\nOXGJZnZaxd0gXpCCe360bTUSFUrrExznDwIkimJQZK4y5UXh30nkULWdh4KjjwIA\nAUVWSKPLGWwDl2AH9f0b3JTpHF5RIkyloGBowXrFEK9tcX5mlFxgOWqdtJaF7zes\npQJdvZ736JVxMepwcPPVSfasZQKBgQDvQxCpLv7qBDO9vRboeFGA1soeCg2syU/B\nEhql+4+p1pV1qJjyQd/1BRngRZGaAV1uokhBwLKhmvTfqNit8HUvYzw3DG99sju0\n3RwWYRFgf6xV3Cb+xW09/4/P9udE2DeKnuSAnoPZMvRA7JI9wCsNzZPA7wGx8/Bv\nXbk7dBhxRwKBgQD0VgdEjd+7PX4JEzdS2S3Ebu8uJ2F13OTEv5ylPnzUkJAyET6b\ncc/A4fxyDGhwf7jVGJjHmIt6OL7uWCc2VAwialsWSfs6UAZZvaICxI4/wuNk7Wck\n3qHQEr0Zp+EIy+3pxHEO2rnm2AA9fg4jq2V9/h0bQMcma8AfdITpSacZ1QKBgAki\nYFJ1Lto0St1liKhbX5Exogm/jIIaNWdDj6zii7uKK66QPzaQeUJbbX94aHSetLhy\njZulBazRw6N+SLdrRK4IddYMLX14/nqLLnVUQ1uRxDyK6Ro041TImu7vmCiysHwk\nUMjwRExYe6a24WZmHb6rKIbnGQN4MqetxlKUvhIlAoGAH7cTswMWvGMkrk+g87PJ\n+jsiLixAXeQLf0Je+dYs7gZGDcyXDpxcFe66bIv90en35N4aYQdiqdJu+NOoLYPM\nvAhtWg1qy9puzpf6U719jODYduE0CNIxBncj4RUlbj9PK1JRvvPvKVq3GFuGbz6C\nex5i3ryBmIdFq2d69rh43Ew=\n-----END PRIVATE KEY-----\n",
6
+ "client_email": "firebase-adminsdk-fbsvc@klingwatermark.iam.gserviceaccount.com",
7
+ "client_id": "104214968547846116587",
8
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
9
+ "token_uri": "https://oauth2.googleapis.com/token",
10
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11
+ "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40klingwatermark.iam.gserviceaccount.com",
12
+ "universe_domain": "googleapis.com"
13
+ }
backend/services/adminService.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ };
backend/services/ai.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+
2
+ export { AIService } from '@/backend/services/ai/index';
backend/services/ai/bookRecap.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BOOK_RECAP_PROMPT } from '../../../prompts/bookRecap.js';
2
+
3
+ import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
4
+
5
+ export async function bookRecap(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
+ // Clean language name in case any legacy strings remain
10
+ const cleanLanguage = targetLanguage.split(' (')[0];
11
+
12
+ const finalPrompt = BOOK_RECAP_PROMPT(targetLanguage);
13
+
14
+ return await tryModels(apiKey, models, async (ai, model) => {
15
+ const response = await ai.models.generateContent({
16
+ model: model,
17
+ contents: {
18
+ parts: [
19
+ { inlineData: { data: media, mimeType } },
20
+ { text: `Translate this entire document COMPLETELY (100% length) into ${cleanLanguage}. Do not summarize or skip any content.` }
21
+ ]
22
+ },
23
+ config: {
24
+ temperature: 0.3,
25
+ systemInstruction: finalPrompt,
26
+ safetySettings: DEFAULT_SAFETY_SETTINGS,
27
+ thinkingConfig: { thinkingBudget: 0 }
28
+ }
29
+ });
30
+
31
+ const text = response.text;
32
+ if (!text || text.trim().length < 50) {
33
+ throw new Error("MODEL_FAILED_TO_TRANSLATE_DOCUMENT");
34
+ }
35
+ return text;
36
+ });
37
+ }
backend/services/ai/comicTranslator.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { COMIC_TRANSLATOR_PROMPT } from '../../../prompts/comicTranslator.js';
2
+
3
+ import { Type } from '@google/genai';
4
+ import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS, cleanJson } from '@/backend/services/ai/utils';
5
+
6
+ export async function comicTranslate(media, mimeType, targetLanguage, apiKey, isOwnApi = false) {
7
+ const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
8
+ const isBurmese = targetLanguage.toLowerCase().includes('burm') || targetLanguage.includes('မြန်မာ');
9
+
10
+ const finalPrompt = COMIC_TRANSLATOR_PROMPT(targetLanguage);
11
+
12
+ // AI identifies text locations and translations
13
+ return await tryModels(apiKey, models, async (ai, model) => {
14
+ const response = await ai.models.generateContent({
15
+ model: model,
16
+ contents: {
17
+ parts: [
18
+ { inlineData: { data: media, mimeType } },
19
+ { 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." }
20
+ ]
21
+ },
22
+ config: {
23
+ temperature: 0.1,
24
+ systemInstruction: finalPrompt,
25
+ safetySettings: DEFAULT_SAFETY_SETTINGS,
26
+ responseMimeType: "application/json",
27
+ responseSchema: {
28
+ type: Type.OBJECT,
29
+ properties: {
30
+ pages: {
31
+ type: Type.ARRAY,
32
+ items: {
33
+ type: Type.OBJECT,
34
+ properties: {
35
+ page_number: { type: Type.INTEGER },
36
+ text_blocks: {
37
+ type: Type.ARRAY,
38
+ items: {
39
+ type: Type.OBJECT,
40
+ properties: {
41
+ translated_text: { type: Type.STRING },
42
+ box_2d: {
43
+ type: Type.ARRAY,
44
+ items: { type: Type.NUMBER },
45
+ description: "[ymin, xmin, ymax, xmax] coordinates normalized 0-1000"
46
+ },
47
+ background_color: { type: Type.STRING }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ },
55
+ required: ['pages']
56
+ }
57
+ }
58
+ });
59
+
60
+ // The backend server will receive this JSON and perform the heavy image manipulation
61
+ // returning a final processed URL or Base64 to the client.
62
+ return JSON.parse(cleanJson(response.text));
63
+ });
64
+ }
backend/services/ai/creator.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CONTENT_CREATOR_PROMPT } from '../../../prompts/contentCreator.js';
2
+
3
+ import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
4
+
5
+ export async function contentCreator(topic, category, subTopics, contentType, gender, targetLang, apiKey, isOwnApi = false) {
6
+ const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
7
+ const isBurmese = targetLang.toLowerCase().includes('burm') || targetLang.includes('မြန်မာ');
8
+ const finalPrompt = CONTENT_CREATOR_PROMPT(topic, category, subTopics, contentType, gender, targetLang);
9
+ return await tryModels(apiKey, models, async (ai, model) => {
10
+ const response = await ai.models.generateContent({
11
+ model,
12
+ contents: [{ parts: [{ text: `Topic: ${topic}. Category: ${category}.` }] }],
13
+ config: { temperature: 0.8, systemInstruction: finalPrompt, safetySettings: DEFAULT_SAFETY_SETTINGS }
14
+ });
15
+ return response.text;
16
+ });
17
+ }
18
+
19
+ export async function generateImage(prompt, apiKey, isOwnApi = false) {
20
+ const models = ['gemini-2.5-flash-image', 'gemini-3-pro-image-preview'];
21
+
22
+ // Improved visual prompt for better generation
23
+ const visualPrompt = `High-quality cinematic illustrative 3D character design or scene showing: ${prompt}. Vivid colors, detailed environment, 8k resolution style.`;
24
+
25
+ return await tryModels(apiKey, models, async (ai, model) => {
26
+ const response = await ai.models.generateContent({
27
+ model: model,
28
+ contents: {
29
+ parts: [
30
+ { text: visualPrompt }
31
+ ]
32
+ },
33
+ config: {
34
+ imageConfig: { aspectRatio: "1:1" }
35
+ }
36
+ });
37
+
38
+ // Thoroughly check all candidates and parts for inlineData
39
+ for (const candidate of response.candidates || []) {
40
+ for (const part of candidate.content?.parts || []) {
41
+ if (part.inlineData) {
42
+ return `data:image/png;base64,${part.inlineData.data}`;
43
+ }
44
+ }
45
+ }
46
+
47
+ throw new Error("EMPTY_IMAGE_DATA_RESPONSE");
48
+ });
49
+ }
backend/services/ai/index.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ };
backend/services/ai/recap.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { RECAP_PROMPT } from '../../../prompts/recap.js';
2
+
3
+ import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
4
+
5
+ export async function recap(media, mimeType, targetLanguage, apiKey, isOwnApi = false) {
6
+ // UPDATED: Aligned with the 'no Pro' preference
7
+ const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
8
+ const finalPrompt = RECAP_PROMPT(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
+ }
backend/services/ai/srtTranslate.js ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SRT_TRANSLATOR_PROMPT } from '../../../prompts/srttranslator.js';
2
+ import { tryModels, getPrompt, cleanSRTOutput, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
3
+
4
+ /**
5
+ * Counts the number of SRT blocks in a given text.
6
+ */
7
+ function countSrtBlocks(text) {
8
+ if (!text) return 0;
9
+ // Improved regex to count standard SRT blocks accurately
10
+ const matches = text.match(/^\d+\s*\r?\n\d{2}:\d{2}:\d{2},\d{3}/gm);
11
+ return matches ? matches.length : 0;
12
+ }
13
+
14
+ export async function srtTranslate(srtContent, sourceLanguage, targetLanguage, apiKey, isOwnApi = false) {
15
+ const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
16
+ const finalPrompt = SRT_TRANSLATOR_PROMPT(sourceLanguage, 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
+ }
backend/services/ai/subtitle.js ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SUBTITLE_PROMPT } from '../../../prompts/subtitle.js';
2
+
3
+ import { tryModels, getPrompt, cleanSRTOutput, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
4
+ import { transcribe } from '@/backend/services/ai/transcribe';
5
+
6
+ const formatMsToSRT = (ms) => {
7
+ const hours = Math.floor(ms / 3600000);
8
+ const mins = Math.floor((ms % 3600000) / 60000);
9
+ const secs = Math.floor((ms % 60000) / 1000);
10
+ const mms = Math.floor(ms % 1000);
11
+ return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')},${mms.toString().padStart(3, '0')}`;
12
+ };
13
+
14
+ export async function subtitle(mediaBase64, mimeType, fullScript, apiKey, isOwnApi = false, sourceLanguage = 'English', startOffsetMs = 0, lastScriptIndex = 0) {
15
+ let scriptToProcess = fullScript || "";
16
+
17
+ // If script is empty, transcribe the whole media
18
+ if (!scriptToProcess) {
19
+ scriptToProcess = await transcribe(mediaBase64, mimeType, apiKey, isOwnApi);
20
+ }
21
+
22
+ const isBurmese = sourceLanguage.toLowerCase().includes('burm') || sourceLanguage.includes('မြန်မာ');
23
+ const promptTemplate = getPrompt('subtitle.txt');
24
+
25
+ // REMOVED: 15-second restriction logic.
26
+ // ADDED: Explicit instruction to process the ENTIRE audio.
27
+ const finalPrompt = promptTemplate
28
+ .replace('{{script}}', scriptToProcess)
29
+ .replace('{{language}}', isBurmese ? "Burmese (Conversational)" : sourceLanguage)
30
+ .replace('15-second audio input', 'the provided audio file')
31
+ .replace('Align the script', 'Align the COMPLETE script from start to finish');
32
+
33
+ const rawSRT = await tryModels(apiKey, ['gemini-3-flash-preview'], async (ai, model) => {
34
+ const response = await ai.models.generateContent({
35
+ model,
36
+ contents: {
37
+ parts: [
38
+ { inlineData: { data: mediaBase64, mimeType: 'audio/wav' } },
39
+ { text: `GENERATE FULL SRT: Listen to this entire file and align every word from the reference script. Return the complete SRT.` }
40
+ ]
41
+ },
42
+ config: {
43
+ temperature: 0,
44
+ systemInstruction: finalPrompt,
45
+ safetySettings: DEFAULT_SAFETY_SETTINGS
46
+ }
47
+ });
48
+ return cleanSRTOutput(response.text);
49
+ });
50
+
51
+ return {
52
+ srt: rawSRT || ""
53
+ };
54
+ }
backend/services/ai/transcribe.js ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TRANSCRIPTION_PROMPT } from '../../../prompts/transcription.js';
2
+
3
+ import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
4
+
5
+ export async function transcribe(media, mimeType, apiKey, isOwnApi = false) {
6
+ const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
7
+ const systemPrompt = TRANSCRIPTION_PROMPT;
8
+ return await tryModels(apiKey, models, async (ai, model) => {
9
+ const response = await ai.models.generateContent({
10
+ model: model,
11
+ contents: {
12
+ parts: [
13
+ { inlineData: { data: media, mimeType } },
14
+ { text: "Transcribe accurately and completely. Do not skip any dialogue." }
15
+ ]
16
+ },
17
+ config: {
18
+ temperature: 0.1,
19
+ systemInstruction: systemPrompt,
20
+ safetySettings: DEFAULT_SAFETY_SETTINGS,
21
+ thinkingConfig: { thinkingBudget: 0 }
22
+ }
23
+ });
24
+
25
+ const text = response.text;
26
+ if (!text || text.trim().length < 2) {
27
+ throw new Error("EMPTY_RESPONSE_FROM_MODEL");
28
+ }
29
+ return text;
30
+ });
31
+ }
backend/services/ai/translate.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { TRANSLATION_PROMPT } from '../../../prompts/translation.js';
2
+
3
+ import { Type } from '@google/genai';
4
+ import { tryModels, getPrompt, cleanJson, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
5
+
6
+ export async function translate(text, targetLanguage, options, apiKey, isOwnApi = false) {
7
+ const models = ['gemini-3-flash-preview', 'gemini-flash-lite-latest'];
8
+ const isBurmese = targetLanguage.toLowerCase().includes('burm') || targetLanguage.includes('မြန်မာ');
9
+ const finalPrompt = TRANSLATION_PROMPT(targetLanguage, options);
10
+
11
+ return await tryModels(apiKey, models, async (ai, model) => {
12
+ const response = await ai.models.generateContent({
13
+ model: model,
14
+ contents: [{ parts: [{ text: `CONTENT: ${text}` }] }],
15
+ config: {
16
+ temperature: 0.7,
17
+ systemInstruction: finalPrompt,
18
+ responseMimeType: "application/json",
19
+ safetySettings: DEFAULT_SAFETY_SETTINGS,
20
+ responseSchema: {
21
+ type: Type.OBJECT,
22
+ properties: {
23
+ translation: { type: Type.STRING },
24
+ deepMeaning: { type: Type.STRING, nullable: true },
25
+ suggestions: { type: Type.OBJECT, nullable: true, properties: { videoTitles: { type: Type.ARRAY, items: { type: Type.STRING } }, thumbnailTexts: { type: Type.ARRAY, items: { type: Type.STRING } } } }
26
+ },
27
+ required: ['translation']
28
+ }
29
+ }
30
+ });
31
+ return JSON.parse(cleanJson(response.text));
32
+ });
33
+ }
backend/services/ai/tts.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AI_VOICE_PROMPT } from '../../../prompts/aiVoice.js';
2
+
3
+ import { tryModels, getPrompt, DEFAULT_SAFETY_SETTINGS } from '@/backend/services/ai/utils';
4
+ import { Modality } from '@google/genai';
5
+
6
+ export async function tts(text, voiceName, tone, apiKey, isOwnApi = false) {
7
+ // Strict requirement: Max 3500 characters
8
+ const ABSOLUTE_MAX_LENGTH = 3500;
9
+ if (text && text.length > ABSOLUTE_MAX_LENGTH) {
10
+ throw new Error(`Text is too long (${text.length} chars). Maximum allowed is ${ABSOLUTE_MAX_LENGTH} characters.`);
11
+ }
12
+
13
+ const models = ['gemini-2.5-flash-preview-tts', 'gemini-2.5-pro-preview-tts'];
14
+ const promptInstructions = AI_VOICE_PROMPT(tone);
15
+
16
+ // For TTS models, prepending the tone instructions to the text is more stable than systemInstruction
17
+ const textWithInstructions = `${promptInstructions}\n\nSCRIPT TO SPEAK:\n${text}`;
18
+
19
+ console.log(`[TTS] Generating content... Length: ${text?.length || 0}`);
20
+
21
+ const chunkBase64 = await tryModels(apiKey, models, async (ai, model) => {
22
+ const response = await ai.models.generateContent({
23
+ model: model,
24
+ contents: [{ parts: [{ text: textWithInstructions }] }],
25
+ config: {
26
+ responseModalities: [Modality.AUDIO],
27
+ safetySettings: DEFAULT_SAFETY_SETTINGS,
28
+ speechConfig: {
29
+ voiceConfig: {
30
+ prebuiltVoiceConfig: {
31
+ voiceName: voiceName || 'Zephyr'
32
+ }
33
+ }
34
+ }
35
+ }
36
+ });
37
+
38
+ const part = response.candidates?.[0]?.content?.parts.find(p => p.inlineData);
39
+ if (part?.inlineData?.data) return part.inlineData.data;
40
+ throw new Error("EMPTY_AUDIO_DATA_FROM_GEMINI");
41
+ });
42
+
43
+ return chunkBase64;
44
+ }
backend/services/ai/utils.js ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
backend/services/authService.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ };
backend/services/credit.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
backend/services/creditService.js ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ };
backend/services/firebase.js ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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();