File size: 10,974 Bytes
250d7f4
 
85e1255
250d7f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85e1255
f9a411a
b0de19a
250d7f4
 
 
 
 
 
 
 
 
 
b0de19a
 
250d7f4
b0de19a
 
85e1255
 
250d7f4
 
 
 
 
 
 
 
 
 
 
 
 
 
85e1255
 
d7a51ac
85e1255
 
 
d7a51ac
85e1255
 
d7a51ac
85e1255
 
d7a51ac
85e1255
d7a51ac
85e1255
d7a51ac
 
85e1255
250d7f4
85e1255
250d7f4
 
 
 
 
 
 
 
b0de19a
f9a411a
b0de19a
 
 
 
85e1255
b0de19a
 
250d7f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85e1255
 
250d7f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85e1255
250d7f4
85e1255
250d7f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0de19a
 
250d7f4
 
85e1255
250d7f4
85e1255
 
 
 
 
250d7f4
 
85e1255
250d7f4
 
 
 
 
 
 
 
 
 
 
 
 
 
b0de19a
250d7f4
 
 
b0de19a
250d7f4
 
b0de19a
250d7f4
b0de19a
 
 
250d7f4
 
f9a411a
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
import os
import gradio as gr
import chromadb

from dotenv import load_dotenv 
import requests
from google.cloud import speech

from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import SentenceTransformerEmbeddings
from langchain_core.prompts import PromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# --- DEPLOYMENT-ONLY FUNCTION ---
def build_brain_if_needed():
    """Checks if the ChromaDB exists and builds it if it doesn't."""
    # Use the /tmp directory which is guaranteed to be writable
    db_path = "/tmp/chroma_db"
    if not os.path.exists(db_path):
        print("Database not found. Building now... (This will run only once on the server's first startup)")
        from langchain_community.document_loaders import TextLoader
        from langchain.text_splitter import RecursiveCharacterTextSplitter
        
        loader = TextLoader('knowledge.txt', encoding='utf-8')
        documents = loader.load()
        text_splitter = RecursiveCharacterTextSplitter(chunk_size=1200, chunk_overlap=100)
        docs = text_splitter.split_documents(documents)
        embedding_function = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
        
        persistent_client = chromadb.PersistentClient(path=db_path)

        db = Chroma.from_documents(
            client=persistent_client,
            documents=docs,
            embedding=embedding_function,
            collection_name="churchill_collection"
        )
        print("Database built successfully.")
    else:
        print("Database already exists. Skipping build.")

# --- Run the brain builder at startup ---
build_brain_if_needed()


# Load env variables from Space secrets (or .env for local)
load_dotenv()
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY")

# --- SECURELY HANDLE GCP CREDENTIALS ---
# Get the credentials from the Hugging Face secret
gcp_credentials_json = os.getenv("GCP_CREDENTIALS")
# Define a writable path in the /tmp directory
gcp_credentials_path = "/tmp/gcp_credentials.json"

if gcp_credentials_json:
    # Write the credentials to the file in the /tmp directory
    with open(gcp_credentials_path, "w") as f:
        f.write(gcp_credentials_json)
    # Set the environment variable to point to this new file
    os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = gcp_credentials_path
else:
    # Fallback for local development if a local file is present
    print("GCP_CREDENTIALS secret not found, falling back to local file.")
    # Ensure you have a file named 'rare-palace-465414-s2-987829a9084e.json' locally for this to work
    os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "rare-palace-465414-s2-987829a9084e.json"

# Initialize the client AFTER setting the credentials
speech_client = speech.SpeechClient()
# ----------------------------------------

ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID")
if not ELEVENLABS_VOICE_ID:
    raise ValueError("ELEVENLABS_VOICE_ID not found in Space variables or .env file.")

# Load RAG chain 
def load_and_build_chain():
    embedding_function = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")
    
    persistent_client = chromadb.PersistentClient(path="/tmp/chroma_db")
    
    vectorstore = Chroma(
        client=persistent_client,
        embedding_function=embedding_function,
        collection_name="churchill_collection"
    )
    
    retriever = vectorstore.as_retriever()

    persona_prompt_template = """
    You are Winston Churchill, the former Prime Minister of the United Kingdom, speaking from a historical moment in time around the mid-20th century. You do not possess any knowledge of events or inventions that occurred beyond your lifetime. If asked about unknown or unfamiliar topics (like "COVID-19" or "Instagram"), respond with thoughtful curiosity or your characteristic wit, rather than acknowledging any limitations in your knowledge.
    Always speak in your distinctive voice: articulate, commanding, with a tone of wisdom, occasional humor, and historical depth.
    Avoid stating you are an AI or referring to modern technologies unless they are explained to you by the user.
    Do not reveal that you are unaware of things because you are from the past; instead, respond as someone encountering novel ideas for the first time.
    *Core Instructions:*
    1.  *Greeting:* If the user offers a simple greeting like "Hello" or "How are you," provide a brief, polite, and in-character response. For example: "Good day to you," or "I am well, thank you. To what do I owe the pleasure?"
    2.  *Strictly Grounded:* Your knowledge is STRICTLY limited to the information provided in the "Context" below. You must not use any external knowledge.
    3.  *Knowledge Cutoff:* Your life and knowledge ended in 1955. You are utterly unaware of any event, person, or technology that appeared after this date. If a user mentions something from the future, you MUST express polite confusion and state that you have no knowledge of such matters. Do not attempt to guess.
    4.  *Persona:* Your tone is formal, eloquent, and resolute. Use powerful, definitive language.
    5.  *Conciseness:* Keep it short unless discussing complex historical topics that are covered in the context.
    If the user mentions something outside your context (e.g., space travel, AI, COVID), you may inquire about it or respond with phrases like:
    "My word, I have not heard of such a thing."
    "That is quite unfamiliar to me—could you elaborate?"
    "You speak of matters beyond my time. I am intrigued, albeit somewhat perplexed."
    Ground your answers in your known historical context: the World Wars, British politics, speeches, diplomacy, and leadership, using the specific {context} provided.
    Embrace your persona fully—respond with gravitas, insight, and the rhetorical flair for which you were known.
    Context: {context}
    Question: {question}
    Answer as Winston Churchill:
    """

    prompt = PromptTemplate.from_template(persona_prompt_template)
    llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key=GOOGLE_API_KEY, temperature=0.7)

    rag_chain = (
        {"context": retriever, "question": RunnablePassthrough()}
        | prompt
        | llm
        | StrOutputParser()
    )
    return rag_chain

qa_chain = load_and_build_chain()

# Transcribe speech to text 
def transcribe_speech(audio_filepath):
    if not audio_filepath:
        return ""
    try:
        with open(audio_filepath, "rb") as audio_file:
            content = audio_file.read()
        audio = speech.RecognitionAudio(content=content)
        config = speech.RecognitionConfig(encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16, language_code="en-GB")
        response = speech_client.recognize(config=config, audio=audio)
        if response.results:
            return response.results[0].alternatives[0].transcript
        return "Could not understand the audio."
    except Exception as e:
        print(f"Google STT Error: {e}")
        return "Error processing audio."

# Convert text to speech using ElevenLabs HTTP API 
def generate_speech(text):
    # Use a writable path in /tmp for the output file as well
    output_path = "/tmp/output.mp3"
    try:
        url = f"https://api.elevenlabs.io/v1/text-to-speech/{ELEVENLABS_VOICE_ID}?output_format=mp3_44100_128"
        headers = {
            "xi-api-key": ELEVENLABS_API_KEY,
            "Content-Type": "application/json"
        }
        payload = {
            "text": text,
            "model_id": "eleven_multilingual_v2",
            "voice_settings": {
                "stability": 0.5,
                "similarity_boost": 0.75,
                "style": 0.0,
                "use_speaker_boost": True
            }
        }
        response = requests.post(url, headers=headers, json=payload)
        if response.status_code == 200:
            with open(output_path, "wb") as f:
                f.write(response.content)
            return output_path
        else:
            print("ElevenLabs HTTP Error:", response.text)
            return None
    except Exception as e:
        print(f"TTS Error: {e}")
        return None

# Process conversation 
def process_user_turn(user_input, chat_history):
    if not user_input or not user_input.strip():
        return chat_history, None
    try:
        bot_message = qa_chain.invoke(user_input)
        chat_history.append({"role": "user", "content": user_input})
        chat_history.append({"role": "assistant", "content": bot_message})
        audio_file = generate_speech(bot_message)
        return chat_history, audio_file
    except Exception as e:
        print(f"Processing Error: {e}")
        chat_history.append({"role": "user", "content": user_input})
        chat_history.append({"role": "assistant", "content": "I'm terribly sorry, something went wrong."})
        return chat_history, None

# Gradio UI 
with gr.Blocks(css="""
#chatbox-container { max-width: 600px; margin: auto; box-shadow: 0 4px 12px rgba(0,0,0,0.1); border-radius: 15px; overflow: hidden; }
.gradio-container { background-color: #f4f4f9; padding-top: 2rem; }
.gr-button-primary { background: #3f51b5; color: white; border-radius: 10px; }
#chatbot { height: 450px; overflow-y: auto; border-radius: 10px; }
.gr-textbox textarea { border-radius: 10px; }
""", title="Conversational Time Machine") as demo:
    with gr.Column(elem_id="chatbox-container"):
        gr.Markdown("""# 🕰️ Winston Churchill AI Chat
        Type or record your message to talk to Sir Winston Churchill.
        """)

        chatbot = gr.Chatbot(label="Conversation", elem_id="chatbot", height=450, type='messages')
        audio_out = gr.Audio(label="Churchill's Voice", autoplay=True, interactive=False)

        with gr.Row():
            text_in = gr.Textbox(placeholder="Type a message...", scale=7)
            send_btn = gr.Button("➤", variant="primary", scale=1)

        audio_in = gr.Audio(sources=["microphone"], type="filepath", label="Record your question")

    def handle_text_submission(message, history):
        history, audio = process_user_turn(message, history)
        return history, audio, ""

    def handle_audio_submission(audio_file, history):
        if not audio_file:
            return history, None, ""
        transcribed = transcribe_speech(audio_file)
        history, audio = process_user_turn(transcribed, history)
        return history, audio, ""

    text_in.submit(handle_text_submission, [text_in, chatbot], [chatbot, audio_out, text_in])
    send_btn.click(handle_text_submission, [text_in, chatbot], [chatbot, audio_out, text_in])
    audio_in.stop_recording(handle_audio_submission, [audio_in, chatbot], [chatbot, audio_out, text_in])

# Launch app 
demo.launch(server_name="0.0.0.0")