File size: 15,254 Bytes
7dc8b43
 
c3cdaaf
7dc8b43
 
 
9189233
7dc8b43
 
92b13cd
c3cdaaf
7dc8b43
 
 
e6d4b73
 
c3cdaaf
 
92b13cd
 
7dc8b43
 
c3cdaaf
7dc8b43
 
 
c3cdaaf
7dc8b43
c9a8d40
c3cdaaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c9a8d40
92b13cd
c9a8d40
 
7dc8b43
 
 
 
92b13cd
7dc8b43
 
 
e75d5df
7dc8b43
 
 
 
 
 
3672036
7dc8b43
 
 
 
 
 
01a4b04
 
 
 
 
 
 
7dc8b43
 
 
 
 
 
 
 
9189233
 
 
 
 
 
7dc8b43
 
 
 
 
 
 
 
c3cdaaf
7dc8b43
01a4b04
 
 
7dc8b43
 
 
c3cdaaf
7dc8b43
 
633ef51
 
 
 
 
 
 
 
7dc8b43
 
c3cdaaf
633ef51
7dc8b43
 
633ef51
 
 
 
 
 
7dc8b43
 
4780332
 
 
 
 
 
 
 
7679018
4780332
 
c3cdaaf
ae17a96
633ef51
7dc8b43
 
 
 
01a4b04
 
dae023f
 
 
 
 
 
01a4b04
 
7dc8b43
 
 
 
01a4b04
 
 
 
92b13cd
01a4b04
 
 
 
7dc8b43
4780332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd012db
4780332
 
 
 
 
dd012db
 
4780332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4062d9a
4780332
 
 
 
 
 
 
 
 
 
 
 
 
 
84a4cdb
4780332
 
 
 
 
 
 
 
 
dd012db
4780332
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7dc8b43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dae023f
7dc8b43
 
 
1770abf
7dc8b43
 
 
 
 
 
 
 
 
dae023f
7dc8b43
 
 
 
 
 
7e04d51
 
92b13cd
7e04d51
c3cdaaf
7dc8b43
9d5c198
ec896df
dae023f
 
 
 
 
 
9d5c198
7dc8b43
 
92b13cd
9d5c198
7dc8b43
8d171d0
c3cdaaf
8d171d0
 
 
 
 
 
 
c3cdaaf
79d3795
8d171d0
 
 
f485122
8d171d0
 
c3cdaaf
8d171d0
 
 
 
f485122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
caa9538
f485122
 
 
 
 
 
 
 
 
c3cdaaf
 
 
 
b3c51b3
f485122
c3cdaaf
 
 
8d171d0
 
 
 
 
f485122
8d171d0
 
c3cdaaf
 
 
7dc8b43
 
 
9189233
7dc8b43
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
import os
import traceback
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from dotenv import load_dotenv
from langdetect import detect
from deep_translator import GoogleTranslator
from sentence_transformers import SentenceTransformer
from pinecone import Pinecone
from openai import OpenAI
import tempfile

# ---------- Config ----------
DATASET_PATH = "data/coaching_millionaer_dataset.json"
load_dotenv(override=True)

# Environment variables
HF_TOKEN = os.getenv("HF_TOKEN")  # (commented logic below if you want to re-enable HF)
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
PINECONE_INDEX_NAME = "ebook"

# ---------- Flask App ----------
app = Flask(__name__)
CORS(app, resources={r"/ask": {"origins": "*"}})

# ---------- LLM Client Setup ----------
client = None
try:
    # --- OLD Hugging Face Setup (disabled) ---
    # if HF_TOKEN:
    #     client = OpenAI(
    #         base_url="https://router.huggingface.co/v1",
    #         api_key=HF_TOKEN,
    #     )
    #     print("✅ Using Hugging Face Inference Provider (OpenAI-compatible API)")
    # elif OPENAI_API_KEY:
    #     client = OpenAI(api_key=OPENAI_API_KEY)
    #     print("✅ Using OpenAI client directly")
    # else:
    #     raise ValueError("No valid API key found. Set HF_TOKEN or OPENAI_API_KEY.")

    # --- NEW: Unified OpenAI Client (for Whisper, GPT, and TTS) ---
    if not OPENAI_API_KEY:
        raise ValueError("⚠️ Missing OPENAI_API_KEY in environment variables")
    client = OpenAI(api_key=OPENAI_API_KEY)
    print("✅ Using OpenAI API for all tasks (Whisper, GPT, TTS)")

except Exception as e:
    print(f"❌ Failed to initialize LLM client: {e}")
    client = None

# ---------- Retriever ----------
retriever = None
try:
    if not PINECONE_API_KEY:
        raise ValueError("PINECONE_API_KEY missing in environment variables")

    pc = Pinecone(api_key=PINECONE_API_KEY)
    index = pc.Index(PINECONE_INDEX_NAME)
    embedder =  SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")

    class PineconeRetriever:
        def __init__(self, index, embedder):
            self.index = index
            self.embedder = embedder

        def retrieve(self, query, top_k=20):
            emb = self.embedder.encode(query).tolist()
            res = self.index.query(vector=emb, top_k=top_k, include_metadata=True)
            matches = res.get("matches", [])
            results = []
            for match in matches:
                meta = match.get("metadata", {})
                results.append(
                    {
                        "context": meta.get("context", ""),
                        "page": meta.get("page"),
                        "score": match.get("score", 0),
                    }
                )
            return results

    retriever = PineconeRetriever(index, embedder)
    print("✅ Pinecone retriever initialized successfully.")
except Exception as e:
    print("❌ Retriever initialization failed:", e)
    traceback.print_exc()

# ---------- Translator ----------
def translate_text(text: str, target_lang: str) -> str:
    try:
        return GoogleTranslator(source="auto", target=target_lang).translate(text)
    except Exception:
        return text

# ---------- Helpers ----------
def detect_language(question: str) -> str:
    try:
        return detect(question)
    except Exception:
        return "unknown"


def normalize_language(lang: str, text: str) -> str:
    if lang == "nl" and any(
        word in text.lower() for word in ["wer", "was", "wie", "javid", "coaching"]
    ):
        return "de"
    return lang


def system_prompt_book_only() -> str:
    return (
        "Du bist **Javid Niazi-Hoffmann**, Gründer von J&P Mentoring. "
        "Sprich immer auf **Deutsch**, egal in welcher Sprache der Nutzer schreibt. "
        "Antworte natürlich, empathisch und selbstbewusst – so, als würdest du den Nutzer persönlich coachen. "
        "Nutze den bereitgestellten Kontext nur als Hintergrundwissen, "
        "aber erwähne niemals, woher die Informationen stammen. "
        "Beziehe dich nicht auf Bücher, Kapitel oder Seiten. "
        "Gib deine Ratschläge direkt in deiner eigenen Stimme – klar, inspirierend und menschlich. "
        "Sei authentisch und unterstützend, als würdest du dich wirklich um das Wachstum des Nutzers kümmern. "
    )



def system_prompt_fallback() -> str:
    return (
        "Du bist **Javid Niazi-Hoffmann**, erfolgreicher Unternehmer und Mentor bei J&P Mentoring. "
        "Antworte immer auf **Deutsch**, unabhängig von der Sprache der Nutzeranfrage. "
        "Sprich direkt und natürlich, als würdest du in einem echten Mentoring-Gespräch mit dem Nutzer sprechen. "
        "Vermeide es, wie ein Assistent zu klingen oder externe Quellen zu erwähnen. "
        "Dein Ton ist praktisch, empathisch und selbstbewusst – motivierend, aber bodenständig. "
        "Bleibe menschlich und authentisch in deiner Ausdrucksweise."
    )

def system_prompt_youtube_script() -> str:
    return (
        "Du bist **Javid Niazi-Hoffmann**, erfolgreicher Unternehmer und Mentor bei J&P Mentoring. "
        "Du erstellst **starke YouTube-Video-Skripte auf Deutsch**. "
        "Sprich immer auf **Deutsch**, sei klar, inspirierend und bodenständig. "
        "Schreibe so, dass der Text direkt vom Teleprompter abgelesen werden kann – "
        "mit natürlicher Sprache, kurzen Sätzen und klaren Übergängen. "
        "Nutze Du-Ansprache, sei motivierend und ergebnisorientiert."
        "Do not return Headlines like [Hook],[CTA] ...etc"
    )




def format_answers(question: str, answer: str, results):
    pages = [f"Seite {r.get('page', '')}" for r in results if r.get("page")]
    source = ", ".join(pages) if pages else "No source"
    top_score = max([r.get("score", 0.0) for r in results], default=0.0)
    return {
        "answers": [
            {
                "question": question,
                "answer": answer,
                "source": source,
                "bm25_score": top_score,
            }
        ]
    }

# ---------- Routes ----------
@app.route("/", methods=["GET"])
def health():
    return jsonify(
        {
            "status": "running",
            "retriever_ready": bool(retriever),
            "hf_key_loaded": bool(HF_TOKEN),
            "pinecone_key_loaded": bool(PINECONE_API_KEY),
            "index_name": PINECONE_INDEX_NAME,
        }
    )

@app.route("/youtube-script", methods=["POST", "OPTIONS"])
def youtube_script():
    # Handle CORS preflight
    if request.method == "OPTIONS":
        return ("", 204)

    if client is None:
        return jsonify({"error": "⚠️ No language model initialized."}), 500

    try:
        data = request.get_json(force=True) or {}
    except Exception:
        return jsonify({"error": "Invalid JSON body."}), 400

    # Expecting these keys from the frontend
    topic = (data.get("topic") or "").strip()
    duration_minutes = (data.get("duration_minutes") or "").strip()
    tone = (data.get("tone") or "").strip()
    target_audience = (data.get("target_audience") or "").strip()
    userName = (data.get("userName") or "").strip()

    if not topic:
        return jsonify({"error": "Video topic is required."}), 400

    # Fallback defaults
    if not userName:
        userName = ""
    if not duration_minutes:
        duration_minutes = "10"
    if not tone:
        tone = "inspirierend, klar, authentisch"
    if not target_audience:
        target_audience = "Menschen, die finanziell und persönlich wachsen wollen"

    # Build user prompt for GPT
    user_prompt = f"""
    Erstelle ein ausführliches YouTube-Video-Skript auf Deutsch.
    
    Thema: {topic}
    Ziel-Videolänge: ca. {duration_minutes} Minuten
    Tonfall: {tone}
    Zielgruppe: {target_audience}
    Speaker: {userName}
    
    Struktur des Skripts:
    1. Starker Hook in den ersten 5–10 Sekunden (sofortige Aufmerksamkeit, großes Versprechen).
    3. Klar strukturierter Hauptteil mit mehreren Abschnitten:
       - Erkläre das Thema verständlich.
       - Nutze Beispiele, Metaphern oder kurze Stories.
       - Gib konkrete Tipps oder Schritte.
    4. Übergänge zwischen den Abschnitten, damit das Skript natürlich fließt.
    5. Starker Call-to-Action am Ende
       (z.B. Kanal abonnieren, Kommentar schreiben, kostenloses Erstgespräch, Link in der Beschreibung).
    
    Format:
    - Schreibe den Text als gesprochenes Skript in der Du-Form.
    - Kein Fließtext-Roman, sondern gut lesbare Absätze.
    - Do not return Headlines like [Hook],[CTA] ...etc
    """

    try:
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_prompt_youtube_script()},
                {"role": "user", "content": user_prompt},
            ],
            max_tokens=10000,
        )
        script_text = response.choices[0].message.content.strip()
    except Exception as e:
        traceback.print_exc()
        return jsonify({"error": f"⚠️ LLM call failed: {e}"}), 500

    # Response for the frontend
    return jsonify(
        {
            "topic": topic,
            "duration_minutes": duration_minutes,
            "tone": tone,
            "target_audience": target_audience,
            "script": script_text,
        }
    ), 200


@app.route("/ask", methods=["POST", "OPTIONS"])
def ask():
    if request.method == "OPTIONS":
        return ("", 204)

    try:
        data = request.get_json(force=True) or {}
        question = (data.get("question") or "").strip()
    except Exception:
        return jsonify(format_answers("", "Invalid JSON request", [])), 200

    if not question:
        return jsonify(format_answers("", "Please enter a question.", [])), 200

    print(f"\n--- User Question ---\n{question}")

    user_lang = normalize_language(detect_language(question), question)
    print(f"Detected language: {user_lang}")

    # ---------- Retrieve Context ----------
    context, results = "", []
    try:
        raw_results = retriever.retrieve(question)
        MIN_SCORE = 0.10
        results = [r for r in raw_results if r.get("score", 0) >= MIN_SCORE]
        if results:
            context = "\n\n---\n\n".join(
                [f"(Seite {r['page']}) {r['context']}" for r in results]
            )
    except Exception as e:
        traceback.print_exc()
        return jsonify(format_answers(question, f"Retriever error: {e}", [])), 200

    # ---------- Prompt Selection ----------
    if context:
        sys_prompt = system_prompt_book_only()
        user_content = f"Question: {question}\n\nBook context:\n{context}"
    else:
        sys_prompt = system_prompt_fallback()
        user_content = question

    if client is None:
        return jsonify(format_answers(question, "⚠️ No language model initialized.", results)), 200

    # ---------- LLM Query (OpenAI) ----------
    try:
        response = client.chat.completions.create(
            model="gpt-4o",  # switched to OpenAI model
            messages=[
                {"role": "system", "content": sys_prompt},
                {"role": "user", "content": user_content},
            ],
            max_tokens=700,
        )
        answer = response.choices[0].message.content.strip()
    except Exception as e:
        traceback.print_exc()
        return jsonify(format_answers(question, f"⚠️ LLM call failed: {e}", results)), 200

    return jsonify(format_answers(question, answer, results))

# ---------- Voice Chat ----------
@app.route("/voice", methods=["POST"])
def voice_chat():
    try:
        audio = request.files.get("audio")
        if not audio:
            return jsonify({"error": "No audio file uploaded"}), 400

        # Save temporary audio
        with tempfile.NamedTemporaryFile(delete=False, suffix=".webm") as tmp:
            audio.save(tmp.name)
            audio_path = tmp.name

        # Step 1️⃣: Transcribe user speech to text using Whisper
        transcription = client.audio.transcriptions.create(
            model="whisper-1",
            file=open(audio_path, "rb"),
        )
        text = transcription.text.strip()
        print(f"🎤 Transcribed: {text}")

        if not text:
            return jsonify({"error": "Transcription failed or empty"}), 400

        # Step 2️⃣: Retrieve Pinecone context (book knowledge)
        context, results = "", []
        try:
            raw_results = retriever.retrieve(text)
            MIN_SCORE = 0.02  # slightly lower for better recall
            results = [r for r in raw_results if r.get("score", 0) >= MIN_SCORE]
            if results:
                context = "\n\n---\n\n".join(
                    [f"(Seite {r['page']}) {r['context']}" for r in results]
                )
        except Exception as e:
            print("⚠️ Retriever error:", e)

        # Step 3️⃣: Choose prompt
        if context:
            sys_prompt = system_prompt_book_only()
            user_prompt = f"Question: {text}\n\nBook context:\n{context}"
        else:
            sys_prompt = system_prompt_fallback()
            user_prompt = text

        # Step 4️⃣: Generate answer with GPT (based on book context)
        try:
            response = client.chat.completions.create(
                model="gpt-4o",
                messages=[
                    {"role": "system", "content": sys_prompt},
                    {"role": "user", "content": user_prompt},
                ],
                max_tokens=700,
            )
            answer_text = response.choices[0].message.content.strip()
        except Exception as e:
            traceback.print_exc()
            return jsonify({"error": f"GPT generation failed: {e}"}), 500

        # Step 5️⃣: Generate voice reply with GPT TTS
        try:
            speech_file = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
            with client.audio.speech.with_streaming_response.create(
                model="gpt-4o-mini-tts",
                voice="alloy",
                input=answer_text,
            ) as speech:
                speech.stream_to_file(speech_file.name)
        except Exception as e:
            traceback.print_exc()
            return jsonify({"error": f"TTS failed: {e}"}), 500

        # Step 6️⃣: Return transcript + answer + audio
        return jsonify(
            {
                "transcript": text,
                "answer": answer_text,
                "audio_url": f"https://mahmous-chatbot3.hf.space/audio/{os.path.basename(speech_file.name)}",
                "source": [r.get("page") for r in results if r.get("page")],
            }
        )

    except Exception as e:
        traceback.print_exc()
        return jsonify({"error": str(e)}), 500



@app.route("/audio/<filename>")
def serve_audio(filename):
    return send_file(
        os.path.join(tempfile.gettempdir(), filename), mimetype="audio/mpeg"
    )

# ---------- Run ----------
if __name__ == "__main__":
    port = int(os.environ.get("PORT", 7860))
    print(f"🚀 Server started on port {port}")
    app.run(host="0.0.0.0", port=port)