daafa999 commited on
Commit
8890ccf
Β·
verified Β·
1 Parent(s): 3148b23

Upload 5 files

Browse files
Files changed (5) hide show
  1. .env +1 -0
  2. __pycache__/main.cpython-312.pyc +0 -0
  3. main.py +197 -0
  4. requirements.txt +4 -0
  5. templates/index.html +216 -0
.env ADDED
@@ -0,0 +1 @@
 
 
1
+ GEMINI_API_KEY="AIzaSyAEClWf-cTS67e8JDAa8f_25DfaMistvRo"
__pycache__/main.cpython-312.pyc ADDED
Binary file (9.76 kB). View file
 
main.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import mimetypes
3
+ import struct
4
+ from fastapi import FastAPI, Request, HTTPException
5
+ from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
6
+ from fastapi.staticfiles import StaticFiles
7
+ from fastapi.templating import Jinja2Templates
8
+ from pydantic import BaseModel
9
+ from dotenv import load_dotenv
10
+ from google import genai
11
+ from google.genai import types
12
+
13
+ # Memuat variabel dari file .env
14
+ load_dotenv()
15
+
16
+ api_key = os.getenv("GEMINI_API_KEY")
17
+ if not api_key:
18
+ raise ValueError("GEMINI_API_KEY tidak ditemukan di file .env")
19
+
20
+ # Inisialisasi Google GenAI Client
21
+ client = genai.Client(api_key=api_key)
22
+
23
+ app = FastAPI(title="Gemini AI TTS Studio Pro - Web Edition")
24
+
25
+ # Setup template engine
26
+ templates = Jinja2Templates(directory="templates")
27
+
28
+ # Matrix Data
29
+ VOICE_GENDER = {
30
+ "Charon": "Male", "Fenrir": "Male",
31
+ "Kore": "Female", "Aoede": "Female", "Leda": "Female"
32
+ }
33
+
34
+ CONTEXT_MATRIX = {
35
+ "Funny": {
36
+ "Scene": "Stand-up comedy club or a lively local coffee shop discussion",
37
+ "Context": "{gender} voice, highly expressive, comedic timing, laughing naturally inside sentences, playful tone, high dynamic range, sarcastic giggles"
38
+ },
39
+ "Serious": {
40
+ "Scene": "Formal presentation and news anchoring room",
41
+ "Context": "{gender} voice, authoritative, highly professional tone, clear articulation, strict executive pacing"
42
+ },
43
+ "Dramatic": {
44
+ "Scene": "A cinematic and intense storytelling arena",
45
+ "Context": "{gender} voice, theatrical style, deep breathing pauses, heavy emotional weight, movie trailer vibe"
46
+ },
47
+ "Empathetic": {
48
+ "Scene": "Warm psychological counseling studio",
49
+ "Context": "{gender} voice, soft and soothing tone, gentle breath control, deeply comforting, intimate close-mic feel"
50
+ },
51
+ "Excited": {
52
+ "Scene": "High-energy sports stadium or product launch stage",
53
+ "Context": "{gender} voice, enthusiastic delivery, bright and vibrant tone, dynamic pitch shifting, stadium hype vibe"
54
+ },
55
+ "Energetic": {
56
+ "Scene": "Fast-paced corporate hype or tech commercial arena",
57
+ "Context": "{gender} voice, highly motivational, punching keywords, crisp delivery, driving power"
58
+ },
59
+ "Calm": {
60
+ "Scene": "Meditation oasis and relaxation sanctuary",
61
+ "Context": "{gender} voice, slow rhythmic breathing, tranquil flat tone, whisper-like softness, serene ambient vibe"
62
+ },
63
+ "Narrative": {
64
+ "Scene": "Audiobook production house",
65
+ "Context": "{gender} voice, classic storytelling flow, rhythmic prose delivery, engaging and immersive reading tone"
66
+ }
67
+ }
68
+
69
+ class MatrixRequest(BaseModel):
70
+ voice: str
71
+ emotion: str
72
+ pace: str
73
+ accent: str
74
+
75
+ class TTSRequest(BaseModel):
76
+ transcript: str
77
+ voice: str
78
+ emotion: str
79
+ pace: str
80
+ accent: str
81
+ scene: str
82
+ context: str
83
+ temperature: float
84
+
85
+ def convert_to_wav(audio_data: bytes) -> bytes:
86
+ bits_per_sample = 16
87
+ sample_rate = 24000
88
+ num_channels = 1
89
+ data_size = len(audio_data)
90
+ bytes_per_sample = bits_per_sample // 8
91
+ block_align = num_channels * bytes_per_sample
92
+ byte_rate = sample_rate * block_align
93
+ chunk_size = 36 + data_size
94
+ header = struct.pack(
95
+ "<4sI4s4sIHHIIHH4sI",
96
+ b"RIFF", chunk_size, b"WAVE", b"fmt ", 16, 1,
97
+ num_channels, sample_rate, byte_rate, block_align,
98
+ bits_per_sample, b"data", data_size,
99
+ )
100
+ return header + audio_data
101
+
102
+ @app.get("/", response_class=HTMLResponse)
103
+ async def index(request: Request):
104
+ return templates.TemplateResponse("index.html", {"request": request, "voices": list(VOICE_GENDER.keys()), "emotions": list(CONTEXT_MATRIX.keys())})
105
+
106
+ @app.post("/api/matrix")
107
+ async def get_matrix(data: MatrixRequest):
108
+ gender = VOICE_GENDER.get(data.voice, "Neutral")
109
+ matrix = CONTEXT_MATRIX.get(data.emotion, CONTEXT_MATRIX["Funny"])
110
+
111
+ base_scene = matrix["Scene"]
112
+ base_context = matrix["Context"].format(gender=gender)
113
+
114
+ if data.pace == "Slow":
115
+ base_context += ", deliberate pauses for comedic anticipation"
116
+ elif data.pace == "Fast":
117
+ base_context += ", hyper-active conversational speed"
118
+
119
+ if data.accent == "Jawa":
120
+ base_scene += " with an authentic Javanese cultural nuance"
121
+ base_context += ", speak with a clear, thick Javanese comedic accent ('medok' style), friendly and warm local curves"
122
+ elif data.accent == "Sunda":
123
+ base_scene += " with an authentic Sundanese cultural nuance"
124
+ base_context += ", speak with a clear, thick Sundanese comedic accent, utilizing soft lilt and flexible melodic rise and fall"
125
+ else:
126
+ base_scene += f" ({data.accent} setting)"
127
+ base_context += f", localized {data.accent} vocal pattern"
128
+
129
+ return {"scene": base_scene, "context": base_context}
130
+
131
+ @app.post("/api/generate-tts")
132
+ async def generate_tts(data: TTSRequest):
133
+ if not data.transcript.strip():
134
+ raise HTTPException(status_code=400, detail="Transcript tidak boleh kosong")
135
+
136
+ accent_map = {
137
+ "American": "American (Gen)",
138
+ "British": "British (RP)",
139
+ "Australian": "Australian",
140
+ "Neutral": "Neutral",
141
+ "Jawa": "Indonesian with heavy Javanese regional dialect profile",
142
+ "Sunda": "Indonesian with heavy Sundanese regional dialect profile"
143
+ }
144
+
145
+ prompt = f"""You are a professional voice actor. Act the following transcript naturally with extreme emotional realism based on the text cues.
146
+
147
+ Vocal Environment: {data.scene}
148
+ Primary Acting Style: {data.emotion}
149
+ Speed Profile: {data.pace}
150
+ Accent Target: {accent_map.get(data.accent, data.accent)}
151
+ Detailed Execution Guide: {data.context}
152
+
153
+ Transcript:
154
+ {data.transcript}"""
155
+
156
+ try:
157
+ model = "gemini-2.5-flash-preview-tts"
158
+ contents = [types.Content(role="user", parts=[types.Part.from_text(text=prompt)])]
159
+ config = types.GenerateContentConfig(
160
+ temperature=data.temperature,
161
+ response_modalities=["audio"],
162
+ speech_config=types.SpeechConfig(
163
+ voice_config=types.VoiceConfig(
164
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name=data.voice)
165
+ )
166
+ ),
167
+ )
168
+
169
+ # Mengumpulkan generator stream
170
+ raw_audio_buffer = bytearray()
171
+ is_wav_bawaan = False
172
+
173
+ for chunk in client.models.generate_content_stream(model=model, contents=contents, config=config):
174
+ if chunk.parts and chunk.parts[0].inline_data and chunk.parts[0].inline_data.data:
175
+ inline_data = chunk.parts[0].inline_data
176
+ raw_audio_buffer.extend(inline_data.data)
177
+ ext = mimetypes.guess_extension(inline_data.mime_type)
178
+ if ext and ext in ['.wav', '.mp3']:
179
+ is_wav_bawaan = True
180
+
181
+ if len(raw_audio_buffer) == 0:
182
+ raise HTTPException(status_code=500, detail="Data audio kosong dari Gemini API.")
183
+
184
+ final_data = bytes(raw_audio_buffer) if is_wav_bawaan else convert_to_wav(bytes(raw_audio_buffer))
185
+
186
+ return StreamingResponse(
187
+ iter([final_data]),
188
+ media_type="audio/wav",
189
+ headers={"Content-Disposition": "attachment; filename=generated_tts.wav"}
190
+ )
191
+
192
+ except Exception as e:
193
+ raise HTTPException(status_code=500, detail=str(e))
194
+
195
+ if __name__ == "__main__":
196
+ import uvicorn
197
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.111.0
2
+ uvicorn==0.30.1
3
+ python-dotenv==1.0.1
4
+ google-genai
templates/index.html ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Gemini AI TTS Studio Pro</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ body { background-color: #0d1117; color: #e6edf3; }
10
+ .card { background-color: #161b22; border: 1px solid #30363d; }
11
+ .input-field { background-color: #21262d; border: 1px solid #30363d; color: #e6edf3; }
12
+ .input-field:focus { border-color: #58a6ff; outline: none; }
13
+ </style>
14
+ </head>
15
+ <body class="p-6 font-sans">
16
+ <div class="max-w-7xl mx-auto">
17
+ <header class="card p-4 rounded-xl flex justify-between items-center mb-6">
18
+ <h1 class="text-2xl font-bold text-[#58a6ff]">✦ Gemini AI TTS Studio Pro</h1>
19
+ <p class="text-sm text-[#8b949e]">Advanced Text-to-Speech β€’ Web Edition v1.4</p>
20
+ </header>
21
+
22
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
23
+ <div class="lg:col-span-1 space-y-6">
24
+ <div class="card p-5 rounded-xl">
25
+ <h2 class="text-lg font-bold text-[#58a6ff] mb-4 flex items-center">πŸŽ™οΈ Voice Settings</h2>
26
+
27
+ <div class="space-y-4">
28
+ <div>
29
+ <label class="block text-sm text-[#8b949e] mb-1">Voice Player</label>
30
+ <select id="voice" class="input-field w-full p-2.5 rounded-lg text-sm" onchange="updateMatrix()">
31
+ {% for voice in voices %}
32
+ <option value="{{ voice }}">{{ voice }}</option>
33
+ {% endfor %}
34
+ </select>
35
+ </div>
36
+
37
+ <div>
38
+ <label class="block text-sm text-[#8b949e] mb-1">Emotion Style</label>
39
+ <select id="emotion" class="input-field w-full p-2.5 rounded-lg text-sm" onchange="updateMatrix()">
40
+ {% for emotion in emotions %}
41
+ <option value="{{ emotion }}">{{ emotion }}</option>
42
+ {% endfor %}
43
+ </select>
44
+ </div>
45
+
46
+ <div>
47
+ <label class="block text-sm text-[#8b949e] mb-1">Speaking Pace</label>
48
+ <select id="pace" class="input-field w-full p-2.5 rounded-lg text-sm" onchange="updateMatrix()">
49
+ <option value="Slow">Slow</option>
50
+ <option value="Natural" selected>Natural</option>
51
+ <option value="Fast">Fast</option>
52
+ </select>
53
+ </div>
54
+
55
+ <div>
56
+ <label class="block text-sm text-[#8b949e] mb-1">Accent Profile</label>
57
+ <select id="accent" class="input-field w-full p-2.5 rounded-lg text-sm" onchange="updateMatrix()">
58
+ <option value="Jawa">Jawa</option>
59
+ <option value="Sunda">Sunda</option>
60
+ <option value="American">American</option>
61
+ <option value="British">British</option>
62
+ <option value="Australian">Australian</option>
63
+ <option value="Neutral">Neutral</option>
64
+ </select>
65
+ </div>
66
+ </div>
67
+ </div>
68
+
69
+ <div class="card p-5 rounded-xl">
70
+ <h2 class="text-lg font-bold text-[#58a6ff] mb-4 flex items-center">🎬 AI Smart Direction</h2>
71
+ <div class="space-y-3">
72
+ <div>
73
+ <label class="block text-xs text-[#8b949e] mb-1">Generated Scene Direction</label>
74
+ <input type="text" id="scene" class="input-field w-full p-2.5 rounded-lg text-xs bg-opacity-50" readonly>
75
+ </div>
76
+ <div>
77
+ <label class="block text-xs text-[#8b949e] mb-1">Performance Context</label>
78
+ <textarea id="context" rows="3" class="input-field w-full p-2.5 rounded-lg text-xs bg-opacity-50 resize-none" readonly></textarea>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="card p-5 rounded-xl">
84
+ <h2 class="text-lg font-bold text-[#58a6ff] mb-4 flex items-center">⚑ Advanced Settings</h2>
85
+ <label class="block text-sm text-[#8b949e] mb-1">Temperature: <span id="temp-val" class="text-[#58a6ff]">1.15</span></label>
86
+ <input type="range" id="temperature" min="0" max="2" step="0.05" value="1.15" class="w-full h-1.5 bg-[#21262d] rounded-lg appearance-none cursor-pointer accent-[#58a6ff]" oninput="document.getElementById('temp-val').innerText = this.value">
87
+ </div>
88
+ </div>
89
+
90
+ <div class="lg:col-span-2 flex flex-col space-y-6">
91
+ <div class="card p-5 rounded-xl flex-1 flex flex-col">
92
+ <div class="flex justify-between items-center mb-4">
93
+ <h2 class="text-lg font-bold text-[#58a6ff]">πŸ“ Transcript Content</h2>
94
+ <span id="char-count" class="text-xs text-[#8b949e]">0 chars</span>
95
+ </div>
96
+ <textarea id="transcript" class="input-field w-full flex-1 p-4 rounded-xl text-base font-normal resize-none mb-4" oninput="countChars()">Hhhh... Capek aku tuh, ampun! (mendesah berat) Lagian kamu lagakna sok tahu pisan toh! Hehehe... (tertawa kecil mengejek) Sekarang rasakan sendiri akibatnya! CUKUP YA! JANGAN PERNAH BERANI-BERANI KAMU DATANG LAGI!!! Pergi sana... Huhu... (menangis manja) Tega-teganga kamu melakukan ini semua padaku... jahat!</textarea>
97
+
98
+ <button onclick="generateAudio()" id="btn-submit" class="w-full bg-[#58a6ff] hover:bg-[#79b8ff] text-[#0d1117] font-bold py-3 px-6 rounded-xl transition duration-200">
99
+ πŸš€ Generate Audio
100
+ </button>
101
+ </div>
102
+
103
+ <div id="player-card" class="card p-5 rounded-xl hidden">
104
+ <h2 class="text-base font-bold text-[#79b8ff] mb-3">🎡 Output Audio Player</h2>
105
+ <audio id="audio-player" controls class="w-full rounded-md accent-[#58a6ff]"></audio>
106
+ </div>
107
+
108
+ <div class="card p-5 rounded-xl h-48 flex flex-col">
109
+ <div class="flex justify-between items-center mb-2">
110
+ <h2 class="text-sm font-bold text-[#58a6ff]">πŸ“‹ Activity Log Studio</h2>
111
+ <button onclick="document.getElementById('log-box').innerHTML=''" class="text-xs text-[#8b949e] hover:text-[#e6edf3]">Clear Logs</button>
112
+ </div>
113
+ <div id="log-box" class="input-field flex-1 p-3 rounded-lg font-mono text-xs overflow-y-auto text-[#8b949e] space-y-1">
114
+ <div>✨ Studio Humor Akting Berhasil Dimuat. Siap Menghasilkan Ekspresi Teatrikal!</div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ <script>
122
+ function logMsg(msg) {
123
+ const logBox = document.getElementById('log-box');
124
+ const entry = document.createElement('div');
125
+ entry.innerText = msg;
126
+ logBox.appendChild(entry);
127
+ logBox.scrollTop = logBox.scrollHeight;
128
+ }
129
+
130
+ function countChars() {
131
+ const text = document.getElementById('transcript').value;
132
+ document.getElementById('char-count').innerText = `${text.length} chars`;
133
+ }
134
+
135
+ async function updateMatrix() {
136
+ const payload = {
137
+ voice: document.getElementById('voice').value,
138
+ emotion: document.getElementById('emotion').value,
139
+ pace: document.getElementById('pace').value,
140
+ accent: document.getElementById('accent').value
141
+ };
142
+ try {
143
+ const response = await fetch('/api/matrix', {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify(payload)
147
+ });
148
+ const resData = await response.json();
149
+ document.getElementById('scene').value = resData.scene;
150
+ document.getElementById('context').value = resData.context;
151
+ } catch (err) {
152
+ console.error("Gagal memperbarui matriks arah", err);
153
+ }
154
+ }
155
+
156
+ async function generateAudio() {
157
+ const btn = document.getElementById('btn-submit');
158
+ const transcript = document.getElementById('transcript').value;
159
+ if(!transcript.trim()) return alert("Transcript tidak boleh kosong!");
160
+
161
+ btn.disabled = true;
162
+ btn.innerText = "⏳ Generating...";
163
+ btn.classList.add('opacity-50');
164
+ logMsg("πŸš€ Connecting to Gemini API via Backend...");
165
+
166
+ const payload = {
167
+ transcript: transcript,
168
+ voice: document.getElementById('voice').value,
169
+ emotion: document.getElementById('emotion').value,
170
+ pace: document.getElementById('pace').value,
171
+ accent: document.getElementById('accent').value,
172
+ scene: document.getElementById('scene').value,
173
+ context: document.getElementById('context').value,
174
+ temperature: parseFloat(document.getElementById('temperature').value)
175
+ };
176
+
177
+ try {
178
+ const response = await fetch('/api/generate-tts', {
179
+ method: 'POST',
180
+ headers: { 'Content-Type': 'application/json' },
181
+ body: JSON.stringify(payload)
182
+ });
183
+
184
+ if(!response.ok) throw new Error("Terjadi kesalahan sistem di server API.");
185
+
186
+ const blob = await response.blob();
187
+ const audioUrl = URL.createObjectURL(blob);
188
+
189
+ const playerCard = document.getElementById('player-card');
190
+ const player = document.getElementById('audio-player');
191
+
192
+ player.src = audioUrl;
193
+ playerCard.classList.remove('hidden');
194
+
195
+ logMsg("βœ… File audio berhasil diterima!");
196
+ logMsg("πŸŽ‰ Done! Audio generated successfully.");
197
+ player.play();
198
+
199
+ } catch (error) {
200
+ logMsg(`❌ Error: ${error.message}`);
201
+ alert(error.message);
202
+ } finally {
203
+ btn.disabled = false;
204
+ btn.innerText = "πŸš€ Generate Audio";
205
+ btn.classList.remove('opacity-50');
206
+ }
207
+ }
208
+
209
+ // Jalankan saat pertama kali dibuka
210
+ window.onload = () => {
211
+ updateMatrix();
212
+ countChars();
213
+ };
214
+ </script>
215
+ </body>
216
+ </html>