LovnishVerma commited on
Commit
1504e2f
·
verified ·
1 Parent(s): 1b04586

Upload 5 files

Browse files
Files changed (5) hide show
  1. LICENSE +201 -0
  2. app.py +304 -0
  3. requirements.txt +4 -0
  4. templates/index.html +1034 -0
  5. test-gemini-api.py +44 -0
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
app.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import requests
4
+ import logging
5
+ from flask import Flask, request, jsonify, render_template
6
+ from dotenv import load_dotenv
7
+
8
+ # Try to import pydub for audio chunking
9
+ try:
10
+ from pydub import AudioSegment
11
+ HAS_PYDUB = True
12
+ except ImportError:
13
+ HAS_PYDUB = False
14
+
15
+ load_dotenv()
16
+
17
+ # Set up logging for the dynamic model selector
18
+ logging.basicConfig(level=logging.INFO)
19
+ logger = logging.getLogger(__name__)
20
+
21
+ app = Flask(__name__)
22
+ # Restrict max upload size to 32MB to prevent memory exhaustion
23
+ app.config['MAX_CONTENT_LENGTH'] = 32 * 1024 * 1024
24
+
25
+ SARVAM_API_URL = "https://api.sarvam.ai/speech-to-text"
26
+
27
+
28
+ def get_available_model_name(api_key):
29
+ """
30
+ Dynamically finds a working model from the user's account using the REST API.
31
+ Adapted to be thread-safe for dynamic per-request API keys.
32
+ """
33
+ try:
34
+ url = f"https://generativelanguage.googleapis.com/v1beta/models?key={api_key}"
35
+ resp = requests.get(url, timeout=10)
36
+
37
+ if resp.status_code != 200:
38
+ logger.error(f"Failed to list models: {resp.text}")
39
+ # Fallback to the most likely stable alias if the list request fails
40
+ return "models/gemini-1.5-flash"
41
+
42
+ data = resp.json()
43
+ available_models = []
44
+
45
+ for m in data.get('models', []):
46
+ if 'generateContent' in m.get('supportedGenerationMethods', []):
47
+ available_models.append(m.get('name'))
48
+
49
+ if not available_models:
50
+ logger.error("No models found.")
51
+ return None
52
+
53
+ # Priority list: Try to find these specific powerful models first
54
+ preferred_order = [
55
+ "models/gemini-1.5-flash",
56
+ "models/gemini-1.5-pro",
57
+ "models/gemini-pro",
58
+ "models/gemini-1.0-pro"
59
+ ]
60
+
61
+ # 1. Check if any preferred model is in the available list
62
+ for preferred in preferred_order:
63
+ if preferred in available_models:
64
+ logger.info(f"Selected Preferred Model: {preferred}")
65
+ return preferred
66
+
67
+ # 2. If none of the preferred ones exist, take the first available one
68
+ fallback = available_models[0]
69
+ logger.warning(
70
+ f"Preferred models missing. Falling back to: {fallback}")
71
+ return fallback
72
+
73
+ except Exception as e:
74
+ logger.error(f"Error listing models: {e}")
75
+ return "models/gemini-1.5-flash" # Safe default
76
+
77
+
78
+ @app.route('/')
79
+ def index():
80
+ return render_template('index.html')
81
+
82
+
83
+ @app.route('/transcribe', methods=['POST'])
84
+ def transcribe():
85
+ # Cascade: Use client key if provided, fallback to environment variable
86
+ sarvam_key = os.getenv("SARVAM_API_KEY")
87
+ language = request.form.get('language', 'en-IN')
88
+
89
+ if not sarvam_key:
90
+ return jsonify({'error': 'Sarvam API key is required. Provide it in the UI or set SARVAM_API_KEY env var.'}), 401
91
+
92
+ if 'audio' not in request.files:
93
+ return jsonify({'error': 'No audio file provided in request.'}), 400
94
+
95
+ file = request.files['audio']
96
+ if file.filename == '':
97
+ return jsonify({'error': 'No file selected.'}), 400
98
+
99
+ # Ephemeral file handling
100
+ try:
101
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.webm') as temp_audio:
102
+ file.save(temp_audio.name)
103
+ temp_filepath = temp_audio.name
104
+
105
+ # --- CHUNKING LOGIC FOR AUDIO > 30s ---
106
+ if HAS_PYDUB:
107
+ try:
108
+ audio = AudioSegment.from_file(temp_filepath)
109
+ # 29 seconds (safe margin below 30s)
110
+ chunk_length_ms = 29 * 1000
111
+
112
+ # If audio is longer than 29s, process in chunks
113
+ if len(audio) > chunk_length_ms:
114
+ chunks = [audio[i:i + chunk_length_ms]
115
+ for i in range(0, len(audio), chunk_length_ms)]
116
+ full_transcript = []
117
+
118
+ for i, chunk in enumerate(chunks):
119
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as chunk_file:
120
+ chunk.export(chunk_file.name, format="wav")
121
+
122
+ with open(chunk_file.name, 'rb') as f:
123
+ files = {
124
+ 'file': (f"chunk_{i}.wav", f, 'audio/wav')}
125
+ data = {
126
+ 'language_code': language,
127
+ 'model': 'saarika:v2.5',
128
+ 'with_timestamps': 'false'
129
+ }
130
+ headers = {'api-subscription-key': sarvam_key}
131
+
132
+ response = requests.post(
133
+ SARVAM_API_URL,
134
+ headers=headers,
135
+ files=files,
136
+ data=data,
137
+ timeout=60
138
+ )
139
+ os.remove(chunk_file.name)
140
+
141
+ if response.status_code == 200:
142
+ result = response.json()
143
+ transcript = result.get(
144
+ 'transcript') or result.get('text') or ''
145
+ if transcript:
146
+ full_transcript.append(transcript.strip())
147
+ else:
148
+ # Clean up and bubble error if any chunk fails
149
+ os.remove(temp_filepath)
150
+ try:
151
+ err_msg = response.json().get('message', response.text)
152
+ except ValueError:
153
+ err_msg = response.text
154
+ return jsonify({'error': f'Sarvam API error on chunk {i+1}: {err_msg}'}), response.status_code
155
+
156
+ os.remove(temp_filepath)
157
+ return jsonify({'success': True, 'transcript': " ".join(full_transcript)})
158
+
159
+ except Exception as e:
160
+ logger.warning(
161
+ f"Pydub chunking failed (ffmpeg likely missing): {e}. Falling back to direct upload.")
162
+ pass # Proceed to direct upload fallback
163
+
164
+ # --- DIRECT UPLOAD FALLBACK (For <30s files or missing ffmpeg) ---
165
+ with open(temp_filepath, 'rb') as f:
166
+ files = {'file': (file.filename, f, file.mimetype or 'audio/webm')}
167
+ data = {
168
+ 'language_code': language,
169
+ 'model': 'saarika:v2.5',
170
+ 'with_timestamps': 'false'
171
+ }
172
+ headers = {'api-subscription-key': sarvam_key}
173
+
174
+ response = requests.post(
175
+ SARVAM_API_URL,
176
+ headers=headers,
177
+ files=files,
178
+ data=data,
179
+ timeout=60
180
+ )
181
+
182
+ os.remove(temp_filepath)
183
+
184
+ if response.status_code == 200:
185
+ result = response.json()
186
+ transcript = result.get('transcript') or result.get('text') or ''
187
+ return jsonify({'success': True, 'transcript': transcript})
188
+ else:
189
+ try:
190
+ err_msg = response.json().get('message', response.text)
191
+ except ValueError:
192
+ err_msg = response.text
193
+
194
+ # Intercept the specific 30s limit error to guide the user to the fix
195
+ if "duration exceeds" in err_msg.lower() or "30 seconds" in err_msg.lower():
196
+ return jsonify({'error': 'Audio > 30s. To enable automatic chunking, run: "pip install pydub" AND install ffmpeg on your server.'}), 400
197
+
198
+ return jsonify({'error': f'Sarvam API error: {err_msg}'}), response.status_code
199
+
200
+ except Exception as e:
201
+ if 'temp_filepath' in locals() and os.path.exists(temp_filepath):
202
+ os.remove(temp_filepath)
203
+ return jsonify({'error': f'Transcription processing failed: {str(e)}'}), 500
204
+
205
+
206
+ @app.route('/refine', methods=['POST'])
207
+ def refine():
208
+ data = request.get_json() or {}
209
+ gemini_key = os.getenv("GEMINI_API_KEY")
210
+ transcript = data.get('transcript', '').strip()
211
+ tone = data.get('tone', 'clear')
212
+
213
+ if not gemini_key:
214
+ return jsonify({'error': 'Gemini API key is required. Provide it in the UI or set GEMINI_API_KEY env var.'}), 401
215
+
216
+ if not transcript:
217
+ return jsonify({'error': 'No transcript provided for refinement.'}), 400
218
+
219
+ # Dynamically select the best model based on the provided API key
220
+ model_name = get_available_model_name(gemini_key)
221
+ if not model_name:
222
+ return jsonify({'error': 'No suitable Gemini models found for this API key.'}), 500
223
+
224
+ dynamic_gemini_url = f"https://generativelanguage.googleapis.com/v1beta/{model_name}:generateContent"
225
+
226
+ tone_instructions = {
227
+ 'clear': 'Refine into a clear, direct, and well-structured prompt. Remove filler words while preserving the original intent.',
228
+ 'detailed': 'Act as an expert prompt engineer. Expand this raw idea into a highly detailed, comprehensive prompt. Flesh out implicit requirements. Include specific structured sections such as: [Objective], [Target Audience], [Core Features], and [Technical Constraints]. Ensure the resulting prompt leaves no ambiguity.',
229
+ 'concise': 'Distill this into an ultra-concise, powerful AI prompt containing only the essential keywords and core intent.',
230
+ 'creative': 'Transform this into a creative AI prompt, suggesting innovative angles, vivid language, and out-of-the-box features related to the core idea.',
231
+ 'technical': 'Convert this into a precise, highly technical prompt suitable for a senior engineer. Explicitly propose appropriate domain terminology, specific tech stacks, data models, or algorithms (e.g., recommendation engines, APIs) relevant to the idea.',
232
+ 'step-by-step': 'Structure the AI prompt as a comprehensive series of actionable, numbered implementation phases.'
233
+ }
234
+
235
+ instruction = tone_instructions.get(tone, tone_instructions['clear'])
236
+
237
+ system_prompt = (
238
+ "You are a Master Prompt Engineer. The user will provide a raw, unstructured voice transcript of an idea.\n"
239
+ "Your ONLY job is to write a professional, highly effective prompt based on their idea that they can feed into another AI to build or execute their vision.\n"
240
+ "Do NOT answer or fulfill their idea yourself. Just write the expansive prompt FOR them.\n"
241
+ f"Style and Structure Directive: {instruction}\n"
242
+ "IMPORTANT RULES:\n"
243
+ "1. Return ONLY the final generated prompt.\n"
244
+ "2. Do NOT include any conversational filler (e.g., 'Here is your prompt:').\n"
245
+ "3. Do NOT wrap your entire response in a global markdown code block (like ```markdown), but you MAY use internal markdown like bolding, lists, and headings to structure the prompt thoroughly.\n"
246
+ "4. CRITICAL: Ensure the response is completely finished and fully fleshed out. DO NOT truncate or cut off mid-sentence."
247
+ )
248
+
249
+ headers = {"Content-Type": "application/json"}
250
+
251
+ # Merged system_prompt directly into the user contents block to ensure schema compatibility
252
+ payload = {
253
+ "contents": [
254
+ {
255
+ "role": "user",
256
+ "parts": [{"text": f"{system_prompt}\n\n---\nRaw Voice Transcript:\n{transcript}"}]
257
+ }
258
+ ],
259
+ "generationConfig": {
260
+ "temperature": 0.8,
261
+ # Tripled max tokens to prevent truncation on extremely detailed responses
262
+ "maxOutputTokens": 8192
263
+ }
264
+ }
265
+
266
+ try:
267
+ # Extended timeout to 60s to accommodate massive generated prompts
268
+ resp = requests.post(
269
+ f"{dynamic_gemini_url}?key={gemini_key}",
270
+ headers=headers,
271
+ json=payload,
272
+ timeout=60
273
+ )
274
+
275
+ if resp.status_code != 200:
276
+ try:
277
+ err_msg = resp.json().get("error", {}).get("message", resp.text)
278
+ except ValueError:
279
+ err_msg = resp.text
280
+ return jsonify({'error': f'Gemini API error: {err_msg}'}), resp.status_code
281
+
282
+ result = resp.json()
283
+ refined = (
284
+ result.get("candidates", [{}])[0]
285
+ .get("content", {})
286
+ .get("parts", [{}])[0]
287
+ .get("text", "")
288
+ .strip()
289
+ )
290
+
291
+ if not refined:
292
+ return jsonify({'error': 'Empty response from Gemini API. The model returned no text.'}), 500
293
+
294
+ return jsonify({'success': True, 'prompt': refined})
295
+
296
+ except requests.exceptions.Timeout:
297
+ return jsonify({'error': 'Gemini API request timed out while generating a response.'}), 504
298
+ except Exception as e:
299
+ return jsonify({'error': f'Refinement processing failed: {str(e)}'}), 500
300
+
301
+
302
+ if __name__ == '__main__':
303
+ # Make sure 'templates' folder exists in the same directory as app.py
304
+ app.run(debug=True, port=5000)
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Flask==3.0.3
2
+ requests==2.31.0
3
+ python-dotenv==1.0.1
4
+ pydub==0.25.1
templates/index.html ADDED
@@ -0,0 +1,1034 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Voice → Prompt Studio</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=Syne+Mono&display=swap" rel="stylesheet" />
9
+ <style>
10
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
11
+
12
+ :root {
13
+ --ink: #0d0c0a;
14
+ --ink2: #3a3830;
15
+ --ink3: #8a8880;
16
+ --ink4: #b8b6ae;
17
+ --surface: #f5f4f0;
18
+ --surface2: #eeede8;
19
+ --surface3: #e4e3dd;
20
+ --white: #fefefe;
21
+ --accent: #c94a1a;
22
+ --accent-light: #f0997b;
23
+ --accent-bg: #fdf0eb;
24
+ --accent-border: #f0c4b0;
25
+ --green: #2d5a14;
26
+ --green-bg: #e8f2de;
27
+ --red-bg: #fdeaea;
28
+ --red: #a32020;
29
+ --border: rgba(20,18,12,0.09);
30
+ --border2: rgba(20,18,12,0.15);
31
+ --border3: rgba(20,18,12,0.22);
32
+ --r: 18px;
33
+ --rm: 12px;
34
+ --rs: 8px;
35
+ }
36
+
37
+ html { background: var(--surface); color: var(--ink); scroll-behavior: smooth; }
38
+
39
+ body {
40
+ font-family: 'Syne', sans-serif;
41
+ min-height: 100vh;
42
+ background: var(--surface);
43
+ background-image:
44
+ radial-gradient(ellipse 60% 40% at 80% 10%, rgba(201,74,26,0.06) 0%, transparent 70%),
45
+ radial-gradient(ellipse 50% 30% at 10% 90%, rgba(201,74,26,0.04) 0%, transparent 70%);
46
+ }
47
+
48
+ .page {
49
+ max-width: 680px;
50
+ margin: 0 auto;
51
+ padding: 3rem 1.5rem 5rem;
52
+ }
53
+
54
+ /* ─── HEADER ─── */
55
+ .header {
56
+ margin-bottom: 2.5rem;
57
+ position: relative;
58
+ }
59
+ .header-top {
60
+ display: flex;
61
+ align-items: flex-start;
62
+ justify-content: space-between;
63
+ margin-bottom: 0.25rem;
64
+ }
65
+ .brand {
66
+ font-size: 36px;
67
+ font-weight: 800;
68
+ letter-spacing: -1.5px;
69
+ line-height: 1;
70
+ color: var(--ink);
71
+ }
72
+ .brand .arrow {
73
+ color: var(--accent);
74
+ font-style: normal;
75
+ display: inline-block;
76
+ animation: arrowPop 2.4s ease-in-out infinite;
77
+ }
78
+ @keyframes arrowPop {
79
+ 0%,100% { transform: translateX(0); opacity: 1; }
80
+ 50% { transform: translateX(4px); opacity: 0.6; }
81
+ }
82
+ .studio-pill {
83
+ font-size: 9px;
84
+ font-weight: 700;
85
+ letter-spacing: 2px;
86
+ text-transform: uppercase;
87
+ background: var(--ink);
88
+ color: var(--surface);
89
+ padding: 5px 11px;
90
+ border-radius: 20px;
91
+ margin-top: 5px;
92
+ }
93
+ .header-sub {
94
+ font-size: 13px;
95
+ color: var(--ink3);
96
+ font-weight: 400;
97
+ letter-spacing: 0.2px;
98
+ }
99
+
100
+ /* ─── LANGUAGE ROW ─── */
101
+ .lang-row {
102
+ display: flex;
103
+ gap: 6px;
104
+ flex-wrap: wrap;
105
+ margin-bottom: 1.5rem;
106
+ }
107
+ .lang-btn {
108
+ font-family: 'Syne', sans-serif;
109
+ font-size: 11px;
110
+ font-weight: 600;
111
+ padding: 5px 14px;
112
+ border: 0.5px solid var(--border2);
113
+ border-radius: 20px;
114
+ background: var(--white);
115
+ color: var(--ink3);
116
+ cursor: pointer;
117
+ transition: all 0.15s ease;
118
+ letter-spacing: 0.3px;
119
+ }
120
+ .lang-btn:hover { background: var(--surface2); color: var(--ink2); border-color: var(--border3); }
121
+ .lang-btn.active {
122
+ background: var(--ink);
123
+ color: var(--white);
124
+ border-color: var(--ink);
125
+ box-shadow: 0 2px 8px rgba(13,12,10,0.18);
126
+ }
127
+
128
+ /* ─── RECORDER CARD ─── */
129
+ .recorder-card {
130
+ background: var(--white);
131
+ border: 0.5px solid var(--border2);
132
+ border-radius: var(--r);
133
+ padding: 2rem 1.5rem 1.5rem;
134
+ margin-bottom: 0.75rem;
135
+ position: relative;
136
+ overflow: hidden;
137
+ }
138
+ .recorder-card::before {
139
+ content: '';
140
+ position: absolute;
141
+ top: 0; left: 0; right: 0;
142
+ height: 2px;
143
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
144
+ opacity: 0;
145
+ transition: opacity 0.4s;
146
+ }
147
+ .recorder-card.is-recording::before { opacity: 1; }
148
+
149
+ .rec-inner {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 1.5rem;
153
+ }
154
+
155
+ /* Orb */
156
+ .orb-area { position: relative; flex-shrink: 0; }
157
+ .rec-orb {
158
+ width: 76px; height: 76px;
159
+ border-radius: 50%;
160
+ border: 0.5px solid var(--border3);
161
+ background: var(--surface2);
162
+ cursor: pointer;
163
+ display: flex;
164
+ align-items: center;
165
+ justify-content: center;
166
+ transition: all 0.25s ease;
167
+ position: relative;
168
+ z-index: 1;
169
+ outline: none;
170
+ }
171
+ .rec-orb:hover { background: var(--surface3); transform: scale(1.04); }
172
+ .rec-orb:active { transform: scale(0.97); }
173
+ .rec-orb.recording {
174
+ background: var(--accent);
175
+ border-color: var(--accent);
176
+ box-shadow: 0 0 0 6px rgba(201,74,26,0.12);
177
+ animation: orbBreath 1.6s ease-in-out infinite;
178
+ }
179
+ .rec-orb.processing {
180
+ background: var(--surface3);
181
+ cursor: default;
182
+ border-color: var(--ink4);
183
+ }
184
+ .orb-icon {
185
+ width: 26px; height: 26px;
186
+ position: relative;
187
+ display: flex; align-items: center; justify-content: center;
188
+ }
189
+ /* Mic icon SVG */
190
+ .orb-icon svg { width: 22px; height: 22px; }
191
+
192
+ @keyframes orbBreath {
193
+ 0%,100% { box-shadow: 0 0 0 6px rgba(201,74,26,0.12); }
194
+ 50% { box-shadow: 0 0 0 14px rgba(201,74,26,0.04); }
195
+ }
196
+
197
+ /* Ripple rings */
198
+ .rec-ring {
199
+ position: absolute;
200
+ inset: -10px;
201
+ border-radius: 50%;
202
+ border: 1px solid var(--accent);
203
+ opacity: 0;
204
+ pointer-events: none;
205
+ }
206
+ .rec-orb.recording ~ .rec-ring { animation: ringOut 1.6s ease-out infinite; }
207
+ .rec-orb.recording ~ .rec-ring:nth-child(3) { animation-delay: 0.4s; }
208
+ @keyframes ringOut {
209
+ 0% { transform: scale(1); opacity: 0.5; }
210
+ 100% { transform: scale(1.7); opacity: 0; }
211
+ }
212
+
213
+ /* Right side of recorder */
214
+ .rec-right { flex: 1; }
215
+ .timer {
216
+ font-size: 38px;
217
+ font-weight: 700;
218
+ letter-spacing: -1px;
219
+ color: var(--ink);
220
+ line-height: 1;
221
+ margin-bottom: 6px;
222
+ font-variant-numeric: tabular-nums;
223
+ }
224
+ .timer.recording { color: var(--accent); }
225
+ .rec-label {
226
+ font-size: 11px;
227
+ color: var(--ink3);
228
+ letter-spacing: 0.5px;
229
+ text-transform: uppercase;
230
+ font-weight: 600;
231
+ margin-bottom: 10px;
232
+ }
233
+ .waveform {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 2.5px;
237
+ height: 28px;
238
+ }
239
+ .wave-bar {
240
+ width: 3px;
241
+ border-radius: 2px;
242
+ background: var(--accent-light);
243
+ height: 4px;
244
+ transition: height 0.05s ease;
245
+ }
246
+
247
+ /* Upload row */
248
+ .upload-row {
249
+ display: flex;
250
+ justify-content: flex-end;
251
+ margin-top: 1rem;
252
+ padding-top: 0.75rem;
253
+ border-top: 0.5px solid var(--border);
254
+ }
255
+ .upload-btn {
256
+ font-family: 'Syne', sans-serif;
257
+ font-size: 11px;
258
+ font-weight: 600;
259
+ padding: 6px 16px;
260
+ border: 0.5px solid var(--border2);
261
+ border-radius: 20px;
262
+ background: var(--surface);
263
+ color: var(--ink3);
264
+ cursor: pointer;
265
+ transition: all 0.15s;
266
+ letter-spacing: 0.3px;
267
+ display: flex; align-items: center; gap: 6px;
268
+ }
269
+ .upload-btn:hover { background: var(--surface2); color: var(--ink2); border-color: var(--border3); }
270
+
271
+ /* ─── STATUS BAR ─── */
272
+ .status-bar {
273
+ min-height: 28px;
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ font-size: 11.5px;
278
+ color: var(--ink3);
279
+ letter-spacing: 0.3px;
280
+ margin-bottom: 1rem;
281
+ font-weight: 500;
282
+ padding: 0 4px;
283
+ transition: color 0.2s;
284
+ }
285
+ .status-bar.err {
286
+ color: var(--red);
287
+ background: var(--red-bg);
288
+ border-radius: var(--rs);
289
+ padding: 6px 12px;
290
+ }
291
+ .status-bar.ok {
292
+ color: var(--green);
293
+ background: var(--green-bg);
294
+ border-radius: var(--rs);
295
+ padding: 6px 12px;
296
+ }
297
+ .status-dot {
298
+ width: 6px; height: 6px;
299
+ border-radius: 50%;
300
+ background: currentColor;
301
+ margin-right: 7px;
302
+ flex-shrink: 0;
303
+ }
304
+
305
+ /* ─── PANELS ─── */
306
+ .panel {
307
+ background: var(--white);
308
+ border: 0.5px solid var(--border2);
309
+ border-radius: var(--r);
310
+ margin-bottom: 0.75rem;
311
+ overflow: hidden;
312
+ }
313
+ .panel-head {
314
+ display: flex;
315
+ align-items: center;
316
+ justify-content: space-between;
317
+ padding: 10px 16px;
318
+ background: var(--surface);
319
+ border-bottom: 0.5px solid var(--border);
320
+ }
321
+ .panel-label {
322
+ font-size: 9px;
323
+ letter-spacing: 1.8px;
324
+ color: var(--ink3);
325
+ font-weight: 700;
326
+ text-transform: uppercase;
327
+ }
328
+ .panel-meta { font-size: 11px; color: var(--ink4); font-weight: 500; }
329
+ .panel-body { padding: 16px 18px; }
330
+
331
+ .editable {
332
+ font-family: 'Syne Mono', monospace;
333
+ font-size: 13.5px;
334
+ line-height: 1.9;
335
+ color: var(--ink);
336
+ outline: none;
337
+ min-height: 60px;
338
+ white-space: pre-wrap;
339
+ }
340
+ .editable:empty::before { content: attr(data-ph); color: var(--ink4); }
341
+ .edit-hint {
342
+ font-size: 10.5px;
343
+ color: var(--ink4);
344
+ margin-top: 8px;
345
+ font-weight: 500;
346
+ }
347
+
348
+ /* ─── REFINE SECTION ─── */
349
+ .refine-section { margin-bottom: 0.75rem; }
350
+ .section-label {
351
+ font-size: 9px;
352
+ letter-spacing: 1.8px;
353
+ color: var(--ink3);
354
+ font-weight: 700;
355
+ text-transform: uppercase;
356
+ margin-bottom: 10px;
357
+ }
358
+ .tone-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
359
+ .tone-btn {
360
+ font-family: 'Syne', sans-serif;
361
+ font-size: 11px;
362
+ font-weight: 600;
363
+ padding: 6px 15px;
364
+ border: 0.5px solid var(--border2);
365
+ border-radius: 20px;
366
+ background: var(--white);
367
+ color: var(--ink3);
368
+ cursor: pointer;
369
+ transition: all 0.15s;
370
+ letter-spacing: 0.2px;
371
+ }
372
+ .tone-btn:hover { background: var(--surface2); color: var(--ink2); }
373
+ .tone-btn.active {
374
+ background: var(--accent-bg);
375
+ color: var(--accent);
376
+ border-color: var(--accent-border);
377
+ font-weight: 700;
378
+ }
379
+
380
+ .refine-btn {
381
+ font-family: 'Syne', sans-serif;
382
+ font-size: 13px;
383
+ font-weight: 700;
384
+ padding: 12px 26px;
385
+ background: var(--ink);
386
+ color: var(--white);
387
+ border: none;
388
+ border-radius: 40px;
389
+ cursor: pointer;
390
+ transition: all 0.2s;
391
+ letter-spacing: 0.3px;
392
+ display: inline-flex;
393
+ align-items: center;
394
+ gap: 8px;
395
+ }
396
+ .refine-btn:hover { background: #2a2820; transform: translateY(-1px); box-shadow: 0 4px 16px rgba(13,12,10,0.2); }
397
+ .refine-btn:active { transform: translateY(0); }
398
+ .refine-btn:disabled {
399
+ opacity: 0.4;
400
+ cursor: not-allowed;
401
+ pointer-events: none;
402
+ transform: none;
403
+ box-shadow: none;
404
+ }
405
+ .refine-btn.cooldown { background: var(--red); }
406
+ .btn-arrow {
407
+ display: inline-block;
408
+ transition: transform 0.2s;
409
+ }
410
+ .refine-btn:not(:disabled):hover .btn-arrow { transform: translateX(3px); }
411
+
412
+ /* ─── PROMPT OUTPUT ─── */
413
+ .prompt-panel {
414
+ background: var(--accent-bg);
415
+ border: 0.5px solid var(--accent-border);
416
+ border-radius: var(--r);
417
+ margin-bottom: 0.75rem;
418
+ overflow: hidden;
419
+ }
420
+ .prompt-panel .panel-head {
421
+ background: rgba(201,74,26,0.06);
422
+ border-bottom-color: var(--accent-border);
423
+ }
424
+ .prompt-panel .panel-label { color: var(--accent); }
425
+ .prompt-text {
426
+ font-family: 'Syne Mono', monospace;
427
+ font-size: 13.5px;
428
+ line-height: 1.85;
429
+ color: var(--ink);
430
+ white-space: pre-wrap;
431
+ max-height: 420px;
432
+ overflow-y: auto;
433
+ padding-right: 6px;
434
+ }
435
+ .prompt-text::-webkit-scrollbar { width: 4px; }
436
+ .prompt-text::-webkit-scrollbar-thumb { background: var(--accent-border); border-radius: 4px; }
437
+
438
+ .copy-btn {
439
+ font-family: 'Syne', sans-serif;
440
+ font-size: 10px;
441
+ font-weight: 700;
442
+ letter-spacing: 0.5px;
443
+ padding: 5px 13px;
444
+ border: 0.5px solid var(--accent-border);
445
+ border-radius: 20px;
446
+ background: transparent;
447
+ color: var(--accent);
448
+ cursor: pointer;
449
+ transition: all 0.15s;
450
+ }
451
+ .copy-btn:hover { background: var(--accent); color: var(--white); }
452
+
453
+ /* ─── HISTORY ─── */
454
+ .divider {
455
+ height: 0.5px;
456
+ background: var(--border);
457
+ margin: 2rem 0 1.25rem;
458
+ }
459
+ .history-item {
460
+ display: flex;
461
+ align-items: flex-start;
462
+ gap: 12px;
463
+ border: 0.5px solid var(--border);
464
+ border-radius: var(--rm);
465
+ padding: 11px 14px;
466
+ margin-bottom: 7px;
467
+ cursor: pointer;
468
+ transition: all 0.12s;
469
+ background: var(--white);
470
+ }
471
+ .history-item:hover { background: var(--surface2); border-color: var(--border2); }
472
+ .history-num {
473
+ font-size: 10px;
474
+ font-weight: 700;
475
+ color: var(--ink4);
476
+ padding-top: 1px;
477
+ min-width: 18px;
478
+ letter-spacing: 0.3px;
479
+ }
480
+ .history-content { flex: 1; overflow: hidden; }
481
+ .history-snippet {
482
+ font-size: 12.5px;
483
+ color: var(--ink2);
484
+ white-space: nowrap;
485
+ overflow: hidden;
486
+ text-overflow: ellipsis;
487
+ font-weight: 500;
488
+ }
489
+ .history-meta { font-size: 10px; color: var(--ink4); margin-top: 3px; font-weight: 600; letter-spacing: 0.3px; }
490
+ .history-tone {
491
+ font-size: 9px;
492
+ font-weight: 700;
493
+ text-transform: uppercase;
494
+ letter-spacing: 1px;
495
+ background: var(--surface2);
496
+ color: var(--ink3);
497
+ padding: 2px 8px;
498
+ border-radius: 10px;
499
+ align-self: flex-start;
500
+ margin-top: 2px;
501
+ }
502
+
503
+ /* ─── PROCESSING SPINNER ─── */
504
+ .spin {
505
+ display: inline-block;
506
+ width: 16px; height: 16px;
507
+ border: 2px solid rgba(255,255,255,0.25);
508
+ border-top-color: #fff;
509
+ border-radius: 50%;
510
+ animation: spin 0.7s linear infinite;
511
+ margin-left: 2px;
512
+ }
513
+ @keyframes spin { to { transform: rotate(360deg); } }
514
+
515
+ /* ─── FOOTER ─── */
516
+ .footer {
517
+ text-align: center;
518
+ margin-top: 3rem;
519
+ padding-top: 1.5rem;
520
+ border-top: 0.5px solid var(--border);
521
+ }
522
+ .footer-text {
523
+ font-size: 11px;
524
+ color: var(--ink4);
525
+ font-weight: 600;
526
+ letter-spacing: 0.5px;
527
+ }
528
+ .footer-text .heart { color: var(--accent); }
529
+ .footer-text .name { color: var(--ink3); }
530
+
531
+ /* ─── UTILITY ─── */
532
+ .hidden { display: none !important; }
533
+ #fileInput { display: none; }
534
+
535
+ /* ─── REVEAL ANIMATION ─── */
536
+ .reveal {
537
+ opacity: 0;
538
+ transform: translateY(10px);
539
+ transition: opacity 0.35s ease, transform 0.35s ease;
540
+ }
541
+ .reveal.shown {
542
+ opacity: 1;
543
+ transform: translateY(0);
544
+ }
545
+ </style>
546
+ </head>
547
+ <body>
548
+ <div class="page">
549
+
550
+ <!-- Header -->
551
+ <div class="header">
552
+ <div class="header-top">
553
+ <h1 class="brand">Voice <span class="arrow">→</span> Prompt</h1>
554
+ <span class="studio-pill">Studio</span>
555
+ </div>
556
+ <p class="header-sub">Speak naturally — get a production-ready AI prompt</p>
557
+ </div>
558
+
559
+ <!-- Language Row -->
560
+ <div class="lang-row" id="langRow">
561
+ <button class="lang-btn active" data-lang="en-IN">EN</button>
562
+ <button class="lang-btn" data-lang="hi-IN">हि</button>
563
+ <button class="lang-btn" data-lang="ta-IN">த</button>
564
+ <button class="lang-btn" data-lang="te-IN">తె</button>
565
+ <button class="lang-btn" data-lang="kn-IN">ಕ</button>
566
+ <button class="lang-btn" data-lang="mr-IN">म</button>
567
+ <button class="lang-btn" data-lang="bn-IN">ব</button>
568
+ <button class="lang-btn" data-lang="gu-IN">ગ</button>
569
+ </div>
570
+
571
+ <!-- Recorder Card -->
572
+ <div class="recorder-card" id="recCard">
573
+ <div class="rec-inner">
574
+ <div class="orb-area">
575
+ <button class="rec-orb" id="recBtn" onclick="toggleRecord()" aria-label="Record voice">
576
+ <div class="orb-icon" id="orbIcon">
577
+ <!-- Mic icon -->
578
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="micSvg">
579
+ <rect x="8.5" y="2" width="7" height="12" rx="3.5" fill="currentColor"/>
580
+ <path d="M5 11c0 3.866 3.134 7 7 7s7-3.134 7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
581
+ <line x1="12" y1="18" x2="12" y2="22" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
582
+ <line x1="9" y1="22" x2="15" y2="22" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
583
+ </svg>
584
+ </div>
585
+ </button>
586
+ <div class="rec-ring" id="ring1"></div>
587
+ <div class="rec-ring" id="ring2"></div>
588
+ </div>
589
+
590
+ <div class="rec-right">
591
+ <div class="timer" id="timer">0:00</div>
592
+ <div class="rec-label" id="recLabel">tap to record</div>
593
+ <div class="waveform" id="waveform"></div>
594
+ </div>
595
+ </div>
596
+
597
+ <div class="upload-row">
598
+ <button class="upload-btn" onclick="document.getElementById('fileInput').click()">
599
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
600
+ <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
601
+ </svg>
602
+ Upload audio file
603
+ </button>
604
+ <input type="file" id="fileInput" accept="audio/*,video/webm" onchange="handleUpload(event)" />
605
+ </div>
606
+ </div>
607
+
608
+ <!-- Status Bar -->
609
+ <div class="status-bar" id="statusBar">
610
+ <span class="status-dot hidden" id="statusDot"></span>
611
+ <span id="statusText"></span>
612
+ </div>
613
+
614
+ <!-- Transcript Panel -->
615
+ <div class="panel reveal hidden" id="transcriptCard">
616
+ <div class="panel-head">
617
+ <span class="panel-label">Transcript</span>
618
+ <span class="panel-meta" id="wordCount">0 words</span>
619
+ </div>
620
+ <div class="panel-body">
621
+ <div class="editable" id="transcriptText" contenteditable="true" data-ph="Your speech will appear here…"></div>
622
+ <div class="edit-hint">Editable — fix any mistakes before refining</div>
623
+ </div>
624
+ </div>
625
+
626
+ <!-- Refine Controls -->
627
+ <div class="refine-section reveal hidden" id="refineSection">
628
+ <div class="section-label">Refine Tone</div>
629
+ <div class="tone-row">
630
+ <button class="tone-btn active" data-tone="clear">Clear</button>
631
+ <button class="tone-btn" data-tone="detailed">Detailed</button>
632
+ <button class="tone-btn" data-tone="concise">Concise</button>
633
+ <button class="tone-btn" data-tone="creative">Creative</button>
634
+ <button class="tone-btn" data-tone="technical">Technical</button>
635
+ <button class="tone-btn" data-tone="step-by-step">Step-by-step</button>
636
+ </div>
637
+ <button class="refine-btn" id="refineBtn" onclick="refinePrompt()">
638
+ Refine with Gemini <span class="btn-arrow">↗</span>
639
+ </button>
640
+ </div>
641
+
642
+ <!-- Prompt Output Panel -->
643
+ <div class="prompt-panel panel reveal hidden" id="promptCard">
644
+ <div class="panel-head">
645
+ <span class="panel-label">Refined Prompt</span>
646
+ <button class="copy-btn" onclick="copyPrompt()">Copy</button>
647
+ </div>
648
+ <div class="panel-body">
649
+ <div class="prompt-text" id="promptText"></div>
650
+ </div>
651
+ </div>
652
+
653
+ <!-- History -->
654
+ <div class="hidden" id="historyWrap">
655
+ <div class="divider"></div>
656
+ <div class="section-label">Recent Prompts</div>
657
+ <div id="historyList"></div>
658
+ </div>
659
+
660
+ <!-- Footer -->
661
+ <div class="footer">
662
+ <p class="footer-text">Made with <span class="heart">♥</span> by <span class="name">Lovnish Verma</span> · © 2026</p>
663
+ </div>
664
+
665
+ </div>
666
+
667
+ <script>
668
+ // ── Audio Context for waveform ─��
669
+ let audioContext, analyser, dataArray, source;
670
+ const NUM_BARS = 22;
671
+
672
+ const wf = document.getElementById('waveform');
673
+ for (let i = 0; i < NUM_BARS; i++) {
674
+ const b = document.createElement('div');
675
+ b.className = 'wave-bar';
676
+ wf.appendChild(b);
677
+ }
678
+
679
+ let selectedLang = 'en-IN', selectedTone = 'clear';
680
+ let mediaRecorder, audioChunks = [], timerInterval, seconds = 0;
681
+ let isRecording = false, waveAnim;
682
+ const history = [];
683
+ let cooldownInterval = null;
684
+
685
+ // ── Language Selectors ──
686
+ document.querySelectorAll('.lang-btn').forEach(b => {
687
+ b.addEventListener('click', () => {
688
+ document.querySelectorAll('.lang-btn').forEach(x => x.classList.remove('active'));
689
+ b.classList.add('active');
690
+ selectedLang = b.dataset.lang;
691
+ });
692
+ });
693
+
694
+ // ── Tone Selectors ──
695
+ document.querySelectorAll('.tone-btn').forEach(b => {
696
+ b.addEventListener('click', () => {
697
+ document.querySelectorAll('.tone-btn').forEach(x => x.classList.remove('active'));
698
+ b.classList.add('active');
699
+ selectedTone = b.dataset.tone;
700
+ });
701
+ });
702
+
703
+ // ── Transcript word counter ──
704
+ const transcriptEl = document.getElementById('transcriptText');
705
+ transcriptEl.addEventListener('input', function () {
706
+ const wc = this.textContent.trim().split(/\s+/).filter(Boolean).length;
707
+ document.getElementById('wordCount').textContent = wc + ' words';
708
+ });
709
+ transcriptEl.addEventListener('paste', function(e) {
710
+ e.preventDefault();
711
+ const text = (e.originalEvent || e).clipboardData.getData('text/plain');
712
+ document.execCommand('insertText', false, text);
713
+ });
714
+
715
+ // ── Status ──
716
+ function setStatus(msg, type = '') {
717
+ const bar = document.getElementById('statusBar');
718
+ const txt = document.getElementById('statusText');
719
+ const dot = document.getElementById('statusDot');
720
+ txt.textContent = msg;
721
+ bar.className = 'status-bar ' + type;
722
+ dot.classList.toggle('hidden', !type);
723
+ }
724
+
725
+ // ── Timer ──
726
+ function updateTimer() {
727
+ seconds++;
728
+ const m = Math.floor(seconds / 60);
729
+ const s = seconds % 60;
730
+ const el = document.getElementById('timer');
731
+ el.textContent = `${m}:${s.toString().padStart(2, '0')}`;
732
+ }
733
+
734
+ // ── Waveform ──
735
+ function animateRealWave() {
736
+ if (!isRecording || !analyser) return;
737
+ waveAnim = requestAnimationFrame(animateRealWave);
738
+ analyser.getByteFrequencyData(dataArray);
739
+ const bars = document.querySelectorAll('.wave-bar');
740
+ const step = Math.floor(dataArray.length / NUM_BARS);
741
+ for (let i = 0; i < NUM_BARS; i++) {
742
+ const val = dataArray[i * step] || 0;
743
+ bars[i].style.height = (4 + (val / 255) * 22) + 'px';
744
+ }
745
+ }
746
+
747
+ function stopWave() {
748
+ cancelAnimationFrame(waveAnim);
749
+ document.querySelectorAll('.wave-bar').forEach(b => b.style.height = '4px');
750
+ if (audioContext && audioContext.state !== 'closed') audioContext.close();
751
+ }
752
+
753
+ // ── Record toggle ──
754
+ async function toggleRecord() {
755
+ if (!isRecording) await startRecord();
756
+ else stopRecord();
757
+ }
758
+
759
+ async function startRecord() {
760
+ try {
761
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
762
+ audioChunks = [];
763
+ mediaRecorder = new MediaRecorder(stream);
764
+ mediaRecorder.ondataavailable = e => { if (e.data.size > 0) audioChunks.push(e.data); };
765
+ mediaRecorder.start(100);
766
+
767
+ const AC = window.AudioContext || window.webkitAudioContext;
768
+ audioContext = new AC();
769
+ analyser = audioContext.createAnalyser();
770
+ source = audioContext.createMediaStreamSource(stream);
771
+ source.connect(analyser);
772
+ analyser.fftSize = 64;
773
+ dataArray = new Uint8Array(analyser.frequencyBinCount);
774
+
775
+ isRecording = true;
776
+ seconds = 0;
777
+ timerInterval = setInterval(updateTimer, 1000);
778
+ animateRealWave();
779
+ setOrbState('recording');
780
+ setStatus('Recording…');
781
+ document.getElementById('recCard').classList.add('is-recording');
782
+ document.getElementById('timer').classList.add('recording');
783
+ } catch (e) {
784
+ setStatus('Microphone access denied or unavailable', 'err');
785
+ }
786
+ }
787
+
788
+ function stopRecord() {
789
+ mediaRecorder.stop();
790
+ mediaRecorder.stream.getTracks().forEach(t => t.stop());
791
+ clearInterval(timerInterval);
792
+ stopWave();
793
+ isRecording = false;
794
+ setOrbState('processing');
795
+ setStatus('Processing audio…');
796
+ document.getElementById('recCard').classList.remove('is-recording');
797
+ document.getElementById('timer').classList.remove('recording');
798
+
799
+ mediaRecorder.onstop = async () => {
800
+ const blob = new Blob(audioChunks, { type: 'audio/webm' });
801
+ await sendToServer(blob, 'audio.webm');
802
+ };
803
+ }
804
+
805
+ async function handleUpload(e) {
806
+ const file = e.target.files[0];
807
+ if (!file) return;
808
+ setOrbState('processing');
809
+ setStatus('Uploading and processing…');
810
+ await sendToServer(file, file.name);
811
+ e.target.value = '';
812
+ }
813
+
814
+ async function sendToServer(blob, filename) {
815
+ const form = new FormData();
816
+ form.append('audio', blob, filename);
817
+ form.append('language', selectedLang);
818
+ try {
819
+ const res = await fetch('/transcribe', { method: 'POST', body: form });
820
+ const data = await res.json();
821
+ if (data.success) {
822
+ showTranscript(data.transcript);
823
+ setStatus('Transcription complete', 'ok');
824
+ } else {
825
+ let errMsg = data.error || 'Transcription failed';
826
+ if (errMsg.toLowerCase().includes('quota') || errMsg.includes('429')) {
827
+ errMsg = 'API rate limit reached. Please wait a moment and try again.';
828
+ }
829
+ setStatus(errMsg, 'err');
830
+ }
831
+ } catch (e) {
832
+ setStatus('Network or server error: ' + e.message, 'err');
833
+ }
834
+ setOrbState('idle');
835
+ }
836
+
837
+ // ── Orb States ──
838
+ function setOrbState(state) {
839
+ const btn = document.getElementById('recBtn');
840
+ const icon = document.getElementById('orbIcon');
841
+ const label = document.getElementById('recLabel');
842
+ const timer = document.getElementById('timer');
843
+
844
+ btn.className = 'rec-orb ' + state;
845
+
846
+ if (state === 'idle') {
847
+ icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="22" height="22">
848
+ <rect x="8.5" y="2" width="7" height="12" rx="3.5" fill="currentColor"/>
849
+ <path d="M5 11c0 3.866 3.134 7 7 7s7-3.134 7-7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
850
+ <line x1="12" y1="18" x2="12" y2="22" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
851
+ <line x1="9" y1="22" x2="15" y2="22" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
852
+ </svg>`;
853
+ label.textContent = 'tap to record';
854
+ timer.textContent = '0:00';
855
+ icon.style.color = 'var(--ink2)';
856
+ } else if (state === 'recording') {
857
+ icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" width="22" height="22">
858
+ <rect x="6" y="6" width="12" height="12" rx="2" fill="white"/>
859
+ </svg>`;
860
+ icon.style.color = 'white';
861
+ label.textContent = 'recording — tap to stop';
862
+ } else {
863
+ icon.innerHTML = `<svg viewBox="0 0 24 24" fill="none" width="22" height="22">
864
+ <path d="M12 2a10 10 0 1 0 10 10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
865
+ </svg>`;
866
+ icon.style.color = 'var(--ink3)';
867
+ icon.style.animation = 'spin 0.7s linear infinite';
868
+ label.textContent = 'processing…';
869
+ }
870
+ if (state !== 'processing') icon.style.animation = '';
871
+ }
872
+
873
+ // ── Show / hide panels with animation ──
874
+ function revealPanel(id) {
875
+ const el = document.getElementById(id);
876
+ el.classList.remove('hidden');
877
+ setTimeout(() => el.classList.add('shown'), 20);
878
+ }
879
+
880
+ function showTranscript(text) {
881
+ transcriptEl.textContent = text;
882
+ transcriptEl.dispatchEvent(new Event('input'));
883
+ revealPanel('transcriptCard');
884
+ revealPanel('refineSection');
885
+ }
886
+
887
+ // ── Cooldown ──
888
+ function startCooldownTimer(secs) {
889
+ const btn = document.getElementById('refineBtn');
890
+ btn.disabled = true;
891
+ btn.classList.add('cooldown');
892
+ let remaining = secs;
893
+ if (cooldownInterval) clearInterval(cooldownInterval);
894
+ cooldownInterval = setInterval(() => {
895
+ if (remaining <= 0) {
896
+ clearInterval(cooldownInterval);
897
+ cooldownInterval = null;
898
+ btn.disabled = false;
899
+ btn.classList.remove('cooldown');
900
+ btn.innerHTML = 'Refine with Gemini <span class="btn-arrow">↗</span>';
901
+ setStatus('Ready to retry. Click the button again.', 'ok');
902
+ } else {
903
+ btn.innerHTML = `Locked — wait ${remaining}s`;
904
+ setStatus(`API quota reached. Please wait ${remaining} seconds.`, 'err');
905
+ remaining--;
906
+ }
907
+ }, 1000);
908
+ }
909
+
910
+ // ── Refine ──
911
+ async function refinePrompt() {
912
+ const transcript = transcriptEl.textContent.trim();
913
+ if (!transcript) { setStatus('No transcript to refine', 'err'); return; }
914
+
915
+ const btn = document.getElementById('refineBtn');
916
+ btn.disabled = true;
917
+ btn.innerHTML = `Engineering prompt… <span class="spin"></span>`;
918
+ setStatus('Engineering prompt with Gemini…');
919
+
920
+ await new Promise(r => setTimeout(r, 600));
921
+
922
+ try {
923
+ const res = await fetch('/refine', {
924
+ method: 'POST',
925
+ headers: { 'Content-Type': 'application/json' },
926
+ body: JSON.stringify({ transcript, tone: selectedTone })
927
+ });
928
+ const data = await res.json();
929
+
930
+ if (data.success) {
931
+ showPrompt(data.prompt, transcript);
932
+ setStatus('Prompt ready', 'ok');
933
+ btn.disabled = false;
934
+ btn.innerHTML = 'Refine with Gemini <span class="btn-arrow">↗</span>';
935
+ } else {
936
+ let errMsg = data.error || 'Refinement failed';
937
+ if (errMsg.includes('Quota exceeded') || errMsg.includes('429') || errMsg.includes('rate-limit')) {
938
+ if (errMsg.includes('limit: 20') || errMsg.includes('free_tier_requests') || errMsg.includes('daily')) {
939
+ setStatus('Google API Daily Quota Exhausted. Update backend to gemini-2.0-flash to fix.', 'err');
940
+ if (cooldownInterval) clearInterval(cooldownInterval);
941
+ btn.disabled = true;
942
+ btn.classList.add('cooldown');
943
+ btn.innerHTML = 'Daily Limit Reached';
944
+ return;
945
+ }
946
+ const timeMatch = errMsg.match(/retry in ([\d.]+)\s*s?/i);
947
+ if (timeMatch) {
948
+ startCooldownTimer(Math.ceil(parseFloat(timeMatch[1])) + 5);
949
+ } else {
950
+ startCooldownTimer(60);
951
+ }
952
+ return;
953
+ }
954
+ setStatus(errMsg, 'err');
955
+ btn.disabled = false;
956
+ btn.innerHTML = 'Refine with Gemini <span class="btn-arrow">↗</span>';
957
+ }
958
+ } catch (e) {
959
+ setStatus('Network or server error: ' + e.message, 'err');
960
+ btn.disabled = false;
961
+ btn.innerHTML = 'Refine with Gemini <span class="btn-arrow">↗</span>';
962
+ }
963
+ }
964
+
965
+ function showPrompt(text, original) {
966
+ document.getElementById('promptText').textContent = text;
967
+ revealPanel('promptCard');
968
+ addHistory(text, original);
969
+ setTimeout(() => document.getElementById('promptCard').scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 100);
970
+ }
971
+
972
+ // ── Copy ──
973
+ function copyPrompt() {
974
+ const text = document.getElementById('promptText').textContent;
975
+ const done = () => {
976
+ const b = document.querySelector('.copy-btn');
977
+ b.textContent = 'Copied!';
978
+ setTimeout(() => b.textContent = 'Copy', 1800);
979
+ };
980
+ if (navigator.clipboard && window.isSecureContext) {
981
+ navigator.clipboard.writeText(text).then(done);
982
+ } else {
983
+ const ta = document.createElement('textarea');
984
+ ta.value = text;
985
+ ta.style.position = 'fixed';
986
+ document.body.appendChild(ta);
987
+ ta.focus(); ta.select();
988
+ try { document.execCommand('copy'); done(); } catch {}
989
+ document.body.removeChild(ta);
990
+ }
991
+ }
992
+
993
+ // ── History ──
994
+ function addHistory(prompt, original) {
995
+ const now = new Date();
996
+ const time = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
997
+ if (history.length > 0 && history[0].prompt === prompt) return;
998
+ history.unshift({ prompt, original, time, tone: selectedTone });
999
+ if (history.length > 10) history.pop();
1000
+ renderHistory();
1001
+ }
1002
+
1003
+ function renderHistory() {
1004
+ const list = document.getElementById('historyList');
1005
+ const wrap = document.getElementById('historyWrap');
1006
+ list.innerHTML = '';
1007
+ history.forEach((h, i) => {
1008
+ const el = document.createElement('div');
1009
+ el.className = 'history-item';
1010
+ el.innerHTML = `
1011
+ <div class="history-num">${String(i+1).padStart(2,'0')}</div>
1012
+ <div class="history-content">
1013
+ <div class="history-snippet">${h.prompt}</div>
1014
+ <div class="history-meta">${h.time}</div>
1015
+ </div>
1016
+ <div class="history-tone">${h.tone}</div>
1017
+ `;
1018
+ el.onclick = () => {
1019
+ showTranscript(h.original);
1020
+ document.getElementById('promptText').textContent = h.prompt;
1021
+ revealPanel('promptCard');
1022
+ document.querySelectorAll('.tone-btn').forEach(b => {
1023
+ b.classList.toggle('active', b.dataset.tone === h.tone);
1024
+ if (b.dataset.tone === h.tone) selectedTone = h.tone;
1025
+ });
1026
+ setStatus('');
1027
+ };
1028
+ list.appendChild(el);
1029
+ });
1030
+ wrap.classList.remove('hidden');
1031
+ }
1032
+ </script>
1033
+ </body>
1034
+ </html>
test-gemini-api.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import google.generativeai as genai
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ api_key = os.getenv("GEMINI_API_KEY")
8
+ if not api_key:
9
+ print("❌ Error: GEMINI_API_KEY is missing.")
10
+ exit(1)
11
+
12
+ genai.configure(api_key=api_key)
13
+
14
+ def check_connectivity():
15
+ print("🔄 Checking Gemini API Connectivity...\n")
16
+
17
+ try:
18
+ print("--- Available Models for Generation ---")
19
+ for m in genai.list_models():
20
+ if 'generateContent' in m.supported_generation_methods:
21
+ print(f"✅ {m.name}")
22
+
23
+ except Exception as e:
24
+ print(f"❌ Failed to connect. Error: {e}")
25
+ return
26
+
27
+ # Explicitly using a model we know is on your account
28
+ test_model_name = "models/gemini-2.5-flash"
29
+
30
+ print(f"\n--- Testing connection using: {test_model_name} ---")
31
+
32
+ try:
33
+ model = genai.GenerativeModel(test_model_name)
34
+ response = model.generate_content("Hello! This is an API connectivity test. Please reply only with: 'Connection successful!'")
35
+
36
+ print("\nResponse received:")
37
+ print(f"> {response.text.strip()}\n")
38
+ print("✅ API Connectivity is fully functional!")
39
+
40
+ except Exception as e:
41
+ print(f"❌ Failed to generate content with {test_model_name}.\nError Details: {e}")
42
+
43
+ if __name__ == "__main__":
44
+ check_connectivity()